170 Commits
Author SHA1 Message Date
James Dansereau 0e8c2ce213 docs: fix reconciliation date in GE-ENV-PROBES.md (2026-09-28, not 10-07) 2026-09-28 10:29:52 -04:00
James Dansereau 500b83dfdb README: final Background stats for the v0.4.0 tag (1005 commits) 2026-09-28 10:28:04 -04:00
James Dansereau 8e4c3cfce2 Merge pull request #112 from jkdansereau/release/v0.4.0
release/v0.4.0: v0.4.0 feature + fidelity release (widescreen, aim system, rebinding, crosshair, rumble, options rebuild, 60 fps fix)
2026-09-28 10:27:40 -04:00
James Dansereau d267d2311e docs: reconcile GE-ENV-PROBES.md for v0.4.0 (31 new rows); fix env-drift checker to see envflag.h wrapper reads; drop README hero row 2026-09-28 10:25:31 -04:00
James Dansereau 7dd7b819e3 release notes: add Tank aim speed (Input.TankAimScale) to the options reference table — it was described in the tanks bullet but missing from the table 2026-09-28 10:11:27 -04:00
James Dansereau 668a2b629b gallery: drop the four v0.2.0 playtest stills (Facility, Bunker 1, Silo, Bunker 2) — gallery is now all-v0.4.0 attract (12 shots); manifest + contact sheet refreshed; readme link count 16 -> 12 2026-09-28 10:09:24 -04:00
James Dansereau 48c37a3c7e readme: trim hero row to a single caption — drop the second caption line, fold the stills link into the gif caption 2026-09-28 10:07:42 -04:00
James Dansereau e7f4540b15 release: v0.4.0 screenshot set — 12 intro-attract stills (3072x1728 widescreen) downscaled to 1600px JPG, replacing the v0.2.0 playtest shots per level; Control/Streets/Jungle added; 6 superseded duplicates dropped; manifest + contact sheet refreshed; README gains a press-kit hero row (Streets/Dam/Runway/Aztec); preview gif re-cut at 400px/15fps/128c (smoother, 15 MB vs 23.6) 2026-09-28 10:06:52 -04:00
James Dansereau efef63fd7f release: v0.4.0 preview media — 24 s montage from live v0.4.0 play sessions (Runway tank opener, Dam, Caverns, Aztec, Bunker 2, Surface 2); release-notes image ref re-pointed to the v0.4.0 tag; README/index captions refreshed 2026-09-28 09:52:52 -04:00
James Dansereau 4f66db12d6 v0.4.0 wrap-up: D407 RESOLVED — tank board-ordering fix, board-animation lockout, menu-accept B gate, in-tank aim routing + Input.TankAimScale; PC Options wheel direction fixed (D407(b)); all 10 TANKDBG probes stripped (src/game zero-diff); D408 logged (SkipIntro skips the failure dossier — Skip intro marked EXPERIMENTAL); release notes + readme stats 2026-09-28 09:28:28 -04:00
James Dansereau 76e9202dfd release: v0.4.0 notes — tank board/exit bullet + PC Options paging follow-up; readme final stats (995 commits, 285 findings D1-D407) 2026-09-28 08:26:09 -04:00
James Dansereau 2b75f3ed8e findings: D407 logged — tank board/exit dead on PC after the D378/D393 use/reload split (port-only fix; live tank drive owed for sign-off) 2026-09-28 08:26:09 -04:00
James Dansereau 460cfd77e0 port: D407 — tank board/exit via the B bit in tank states; PC Options page-edge highlight clamp + mouse-wheel/W-S paging (D407(b)) 2026-09-28 08:26:09 -04:00
James Dansereau a8a9f2932e readme: final project stats (992 commits, 284 findings D1-D406); mark the agentic-development case study as a v0.3.0-era snapshot (through 20 Sep / D321), with the README carrying the final numbers 2026-09-28 02:31:31 -04:00
James Dansereau 9e71e7990b release notes: page indicator wording matches the shipped UI (Page p/N marker + bottom hint) 2026-09-28 02:27:56 -04:00
James Dansereau 73dd0c4ca0 readme and docs site: v0.4.0 release status, release news section, masthead tagline, and accuracy pass
- v0.4.0 status framing (the most complete release to date; the common
  defect classes fixed; a short list of known caveats) and the pre-1.0
  note, on both the README and the Pages home page.
- Dated release news section with links to the GitHub release tags.
- Keyword-reinforced meta descriptions; site masthead tagline.
- Accuracy: options section list per the shipped overlay, Xbox 1.1 (Jinx)
  controller roles (D394), Select opens the overlay, Deck 2x MSAA / 250%
  preset, background stats, footer last-updated stamp.
2026-09-28 02:27:56 -04:00
James Dansereau ebf8e4c05d findings: D406 resolved -- front-options pagination validated on the Steam Deck 2026-09-28 02:26:56 -04:00
James Dansereau a5924517fc port: front-options page hint one row below the last entry (D406d) -- pitch-aligned, not floating at the paper edge 2026-09-28 02:25:51 -04:00
James Dansereau 0c4b60e440 findings: D406c -- page hint on every page (11 rows/page), chevron wording dropped 2026-09-28 02:25:00 -04:00
James Dansereau 4b66494813 port: front-options page hints on every page, clearer wording (D406c)
11 rows per page now (was 13) so the bottom line of the paper is always
free for the page hint -- page 1 of a multi-page section shows
'Down: next page' after all (a full 13-row page had no room and no
hint, reported from the Deck). Hint wording drops the confusing
chevrons: 'Down: next page' / 'Up: previous page'.
2026-09-28 02:24:36 -04:00
James Dansereau 58691e5c5e port: front-options slider bars clear long labels; explicit page indicators (D406b)
Per user reports on the v0.4.0 front screen: the X/Y axis look
sensitivity (controller) labels ran into the fixed 232px slider track.
Bars now start 10px past each row's measured label (rows too long for
any track show the value only); drag hit-test and A-hold mapping use
the same per-row origin. Page 2 of a multi-page section now carries an
explicit dim 'Page p/N' marker right of the title plus a bottom hint
('Down: more rows >' / 'Up: previous page <') wherever the page has
room below; the (p/N) title suffix is retired. GE_FRONTOPTS_WIDTHS
one-shot label-metrics log added (same env-gated diagnostic
convention).
2026-09-28 02:21:22 -04:00
James Dansereau 8e58376d46 port: front-end PC Options pagination for long sections (D406)
INPUT (17 visible rows) outgrew the fixed 440x330 paper: the 18px pitch
was sized before the D356 profile row, so a full page's last row landed
at y=335 (off the bottom of the screen) and rows past the silent 14-row
cap (Crouch mode, Reset to defaults, Bindings...) were unreachable from
the front screen. Sections now page at 13 rows (the largest that fits);
the vertical stepper crosses page boundaries in both directions and the
title carries a (p/N) marker. F10 overlay untouched (it scrolls its own
window). Headless verification (SkipIntro boot + scripted D-pad stepping
+ GE_FRONTNAVLOG): page 1 -> page 2 down and back up, all 17 rows
reachable. GE_FRONTOPTIONS_AUTO test hook added (diagnostic-only, same
convention as GE_FRONTOPTIONS_SECTION). Findings D406 + index; release
notes: options section list corrected to the shipped Input/Gameplay/HUD/
Graphics/Audio/Display and the D406 line added.
2026-09-28 02:09:03 -04:00
James Dansereau 82476f3dd2 readme: v0.4.0 voice pass over a web-1.0 structure
Plain declarative prose throughout the overview and section intros - no
product-page register, no first-person winks. Structure carries the period
feel: top anchor-nav line (middot separators), a dated news line for the
release, --- section rules, and a last-updated footer stamp.
2026-09-28 01:34:43 -04:00
James Dansereau 1b290b2012 findings: D405 table row 4-cell -> 3-cell (pointer merged into title cell) so the index generator buckets it; D405 now indexes as CLOSED 2026-09-28 01:32:13 -04:00
James Dansereau 64913054ce release: v0.4.0 site/README consistency + Web 1.0 re-skin
- Docs site: drop jekyll-theme-cayman for a hand-rolled _layouts/default.html
  (masthead band + middot text nav + footer with last-updated stamp and legal
  line; head re-plumbed with {% seo %}, viewport, favicon); head-custom.html
  fully rewritten to the chosen design tokens - charcoal/off-white/amber,
  Libre Baskerville headings over system sans, square corners, 1px borders,
  flat (no gradients/shadows), mermaid wrapper re-skinned.
- docs/index.md: v0.4.0 status (native widescreen, aim system, rebinding,
  crosshair, rumble, rebuilt overlay); Honest status replaced by the v0.4.0
  release-notes known issues; body anchor-nav line; legal footer moved to
  layout.
- security-and-fidelity-status.md: front matter so {% seo %} titles it.
- README.md: v0.4.0 truth pass (status, Working/Known issues, Deck preset
  2x MSAA / 250% per code, background stats 981 commits / 283 findings,
  comparison table, GEPD-default controls paragraph, earlier builds) plus
  markdown-native motifs matching the site: top anchor-nav line, --- section
  rules, last-updated footer stamp, license badge in the amber accent.
- release-notes: SFTP sideload mention (leftover from the Deck session).
2026-09-28 01:32:13 -04:00
James Dansereau ad20cf210a release: v0.4.0 notes as source of truth - feature-organized, every claim code-verified (labels, options, defaults, D248 root cause) 2026-09-28 01:17:59 -04:00
James Dansereau 889f2e5d5e release: notes logic pass - rainbow fix moved to Graphics; Known issues = actual residuals only (D335b F10 overlay stretch replaces the widescreen heads-up) 2026-09-28 01:15:27 -04:00
James Dansereau b1ee5a3655 release: notes completeness pass - 60fps fix (D248), D202 mixer, D393 pad defaults, D314/D290/D342 polish 2026-09-28 01:14:35 -04:00
James Dansereau 2dd2d9dd35 release: comprehensive v0.4.0 notes - full v0.3.0..v0.4.0 arc (native widescreen, aim rework D332-D338/D404, HUD scale, rainbow fix, stability wave) 2026-09-28 01:12:56 -04:00
James Dansereau f84b8023dd release: v0.4.0 release notes (what's-new since v0.3.0 + true residual known-issues set) 2026-09-28 01:10:23 -04:00
James Dansereau 2621163f03 D405: record post-merge verification (sweep 21/21, MSAA=16 boot check, link ritual) 2026-09-28 01:09:17 -04:00
James Dansereau ed58593215 graphics backlog: track issue #105 (Dam water-reflection cyan fill residual) 2026-09-28 01:07:01 -04:00
James Dansereau 50e3426aca fast3d: clamp MSAA to GL_MAX_SAMPLES and bind the default framebuffer as GL name 0 (PR #106, italoarruda; security-reviewed 2026-09-28, D405)
Port-layer only: resolve_msaa_color_buffer clamps msaa_level to the driver's
GL_MAX_SAMPLES (pre-fix: unallocated multisample renderbuffers -> black
screen when MSAA is set beyond driver support); the resolve/copy paths now
bind current_framebuffer ? framebuffers[i].fbo : 0 (the default framebuffer
is GL name 0, not framebuffers[0]). Affects both Windows (opengl32) and
Linux builds. Code file from the PR head (fc092d99); docs in house style,
label rebased D323 -> D405 (collision with our G_TL_LOD finding).
2026-09-28 01:03:00 -04:00
James Dansereau 4b7cae63f9 v0.4.0 final review: D404 pad-centred aim (N64-parity quadratic curve) + full campaign-playtest sign-off bookkeeping
- input.c: padDirectCompute() — pad twin of the mouse Centred aim; the natural
  pitch path routes AIMMODE_CENTRED through it, AIMMODE_N64 keeps classic
  stick-deflection behavior. Parity pass uses bondview2's verbatim N64
  quadratic curve (sign(v)*clamp(defl/70,+-1)^2 * fov/60 * 3.5 deg/poll), so
  full-stick Centred matches N64's top rate exactly.
- optionsoverlay: aim rows device-neutral (Aim style / Aim range).
- findings.md: D404 RESOLVED (user-validated on Steam Deck, both passes);
  21 sign-off status updates from the user's full campaign playtest
  (D175/D224/D235/D236/D240/D241/D245/D249/D281/D286/D287/D289/D291/D294/
  D303/D306/D307/D308/D311/D320/D322/D75/D387/D395/D396/D399/D400); stale
  options-wave rows closed (D346-D391 batch); D383/D394 kept PARTIAL with
  v0.5.0 scopes; D281 root cause -> v0.5.0; D258 -> NTSC-only for v0.4.0.
- README: Known issues = the true residual set (D318, D75 Rareware logo,
  AllUnlocked EXPERIMENTAL, NTSC-only); stale v0.3.0 clause removed.
- AGENTS.md: native-reexec guard status re-confirmed for 2026-09-28.
2026-09-28 01:02:09 -04:00
James Dansereau 39d1fd381e input: implement Wave A controller options + Turok-verbatim labels (v0.5.0)
Port-layer only (port/src/input.c, optionsoverlay.c); zero src/game edits.
All new knobs off/neutral at default = byte-identical at defaults.

New controller input options (CONTROLLER-INPUT-PLAN Wave A):
- Input.PadLookSensX/Y: per-axis right-stick look sensitivity (100 = native)
- Input.PadSouthpaw: swap fire<->grenade triggers (applied after edge capture,
  so edge-detection stays consistent)
- Input.PadDeadzoneL/R: per-stick deadzone; legacy Input.PadDeadzone key kept
  registered (no row) + one-time migration in inputInit() seeds any unset L/R
  from it (C-init sentinel PAD_DZ_UNSET) before the first configSave
- Input.PadLookSmooth: 0-10 per-pad EMA low-pass on the natural-pitch look
  output (default 0 = off), reset on pad hot-unplug
- Input.MouseYScale: pre-existing registered key finally gets an INPUT row

Verbiage baseline (Turok PC options menu, user-provided verbatim): Mouse
horizontal/vertical sensitivity, X axis/Y axis look sensitivity, Look
smoothing, Southpaw, Vibration (was "Rumble strength"), Deadzone
(left/right stick). GE keeps one flat INPUT section (D356), so the two
Invert-look rows retain their (mouse)/(controller) suffixes.

Display baseline (Turok scales): sensitivity + deadzone rows render 0-100
with 50 = default (existing calibratedDefault path); look smoothing renders
0-10 via a new dispMax "N/M" row formatter (alongside the existing N/100 and
N/255 special cases).

Tested on a real pad (Windows build) - all six options behave as intended.
2026-09-28 00:30:21 -04:00
James Dansereau 83ece6c370 docs: add CONTROLLER-INPUT-PLAN (v0.5.0 controller/input options wave)
Low-hanging-fruit controller/input options ported from the Turok PC port's
menu, scoped as port-layer-only (zero src/ edits): right-stick look X/Y
sensitivity, southpaw trigger swap, per-stick deadzone (with the legacy
single-deadzone migration), look smoothing, and a mouse-vertical-sensitivity
row. All off-by-default (byte-identical at defaults, golden gate preserved).
Documents what the GE port already has, the per-item code shape + insertion
points in input.c, and the Tier B/C items explicitly out of scope (head bob,
bloom/shadows, death-cam, uncapped-FPS -> UNLOCKED-FPS-PLAN, controller
rebind -> PR #109).
2026-09-28 00:00:11 -04:00
James Dansereau d934a93253 docs: D403 resolved — Deck front-end logo/copyright page confirmed fixed
After regenerating the Deck's pcmodels/pccg sidecars from the current
converter (M-197 blob fix + D303) and pushing them, the user re-checked
the front end on the Steam Deck and confirmed the Nintendo/GoldenEye logo
+ copyright/legal page now render correctly. D403 was the stale-sidecar
test artifact all along; no code was ever at fault.
2026-09-28 00:00:11 -04:00
James Dansereau 7f6d745f25 docs: D403 disambiguation performed — regenerate + push Deck sidecars
The Deck bundle's stale 0.3.0-era pcmodels/pccg sidecars were regenerated
from the current Python-source converter (prepare-assets.py + d43/d69/d88,
ALL CHECKS PASSED) on the NTSC ROM and pushed to the Deck (sha256-verified),
preserving the user's ge007.eep + ge007.ini. Awaiting the user's by-eye
front-end recheck (logo/copyright page) to close the loop.
2026-09-27 23:54:08 -04:00
James Dansereau 001690568f docs: log D402/D403/D404 + Steam Deck v0.4.0-pre test results
- D402: first clean Linux build fix (shim include-cycle + _SHIFTL).
- D403 (new): Deck front-end logo/copyright page broken + graphics not
  updated is a test artifact — the bundle was seeded with 0.3.0-era
  sidecars that predate the D75/M-197 blob fix and D303; not a code
  regression. Disambiguation: regenerate the Deck's data/ sidecars.
- D404 (new): 'Aim style: Centred (PC)' is inert on a gamepad; the row's
  doc comment over-promises (applies to pad trigger) but the centred
  path is mouse-only (input.c:1783, D337 device-aware gate).
- Deck test confirmations: D175 gains a low-utilisation (~15-30%)
  frame-pacing/present data point; D395 pad-binding retest satisfied
  (not stuck); D401 rumble + D287 idle/resume-from-sleep confirmed on
  the Deck's Steam Controller / SteamOS.
2026-09-27 23:53:03 -04:00
James Dansereau ed991dd1cb port: D402 — fix the first clean Linux (glibc/SteamOS) build
The release/v0.4.0 branch had never been compiled on Linux; two
port-layer include-resolution spots that MinGW tolerated break under
glibc. Shim-only fixes, zero src/ edits, no rule-2:

- port/shim/PR/ucode.h + port/shim/bondconstants.h: break the
  bondconstants.h -> ultra64.h -> pc_protos.h -> bondtypes.h ->
  bondconstants.h first-include re-entry by deferring pc_protos.h during
  bondconstants.h's own ultra64.h pass (_PORT_DEFER_PC_PROTOS).
- port/shim/PR/gbi.h: define _SHIFTL/_SHIFTR when absent, so video.c's
  gbi.h macros keep their mbi.h definitions on Linux.

Builds + links clean on Ubuntu 24.04 (glibc 2.39 <= SteamOS 2.41).
2026-09-27 23:53:03 -04:00
James Dansereau 9fb381eab1 M7 (v0.4.0, D387): ship AllUnlocked as loudly-documented EXPERIMENTAL/irreversible
Per the user's release decision, keep the toggle (default OFF) but make it
impossible to miss in-game: the row is now labelled 'All unlocked
(EXPERIMENTAL)' and its ON value renders 'ON - UNSAFE' in both the F10
overlay and the front PC Options screen (both read the shared row table).
ON value now bright-highlights like a normal On. Port-only, display-only
(config still stores 0/1); a safe architecture is v0.5.0 work.

Housekeeping: track docs/dev/M7-ALLUNLOCKED-PLAN.md (records the decision);
gitignore the stray repo-root Python/ install; refresh the D387 status.
2026-09-27 23:27:49 -04:00
James Dansereau 471eecd6d3 D401: Rumble Pak -> real gamepad haptics (re-promotes D224/M-87)
Port layer only (zero src/ edits), per docs/dev/RUMBLE-PLAN.md:
- port/src/input.c: inputRumbleSupported/inputRumble -> SDL_GameControllerRumble;
  detection via SDL_GameControllerHasRumble (SDL >= 2.0.18) else
  SDL_JoystickIsHaptic + known-type Windows fallback; zeroed on pad close.
  Single global gRumbleScale (dflt 0.5) registered as Input.RumbleScale.
- port/src/libultra.c: the two M-87 blockers resolved - g_contStatus[i].status
  now sets CONT_CARD_ON for connected rumble-capable pads (so
  joyRumblePakInit arms) and osPfsInit returns PFS_ERR_DEVICE for them (so
  osMotorInit is attempted); osMotorInit/Start/Stop drive the haptics API.
- port/src/optionsoverlay.c: one global 'Rumble strength' ROW_SLIDER
  (Input.RumbleScale, 0.05 detents, %.2f) in the INPUT section +
  kResetDefaults entry. The four per-player Input.PlayerN.RumbleScale keys
  were dropped (user decision 2026-09-28); stale keys in an existing ini
  log four 'unknown key' LOG_NOTE lines and self-clean on next configSave.
- Real-pad haptics confirmed working by the user (2026-09-28).

Build env: build-pc.sh self-heal guard revision - re-exec now writes a
self-contained .ps1 (msys->native argv conversion mangles $-bearing
-Command strings; file write is conversion-free), builds PATH explicitly,
picks TMP from writable candidates (GetTempPath honours the broken
inherited TMP), self-logs to build-pc/ge007-native-reexec-*.log, and
treats a null LASTEXITCODE as 'did not execute' (PS 5.1 piped native
commands error non-terminating). OPEN: the guard still fails inside
build-pc.sh while the identical standalone .ps1 invocation works - AGENTS.md
documents the workaround. docs: RUMBLE-PLAN.md (new, tracked), findings.md
D401 entry + index rows, QOL-INVENTORY D224 -> DONE (D401),
findings-index.csv regenerated.
2026-09-27 23:23:05 -04:00
James Dansereau a548fc4de6 port: D397 — fix file-select/gun-barrel comb via RDP scanline-range snap in gfx_draw_rectangle
Root cause H2 confirmed: the gun-barrel / file-select vertical gradient is
built from 0.75-native-pixel-tall gSPTextureRectangle row strips (title2.c
(<<2)-1 bottom idiom). On the N64 RDP the scan converter floors the U10.2
edges to whole scanlines, so each strip rasterizes to exactly one scanline;
gfx_draw_rectangle passed the fractional edges straight to the GPU's
continuous rasterizer, aliasing the 0.75px pitch into a periodic 'comb'
(fixed ~3-native-row period; window period = 3*scale — confirmed by a
3-scale 640/1024/1280 GE_PCDUMP test).

Fix (port-layer, surgical): after the sub-pixel offset, floor the top edge to
its scanline and the bottom edge to the last covered scanline (the RDP
scanline range [top_scan, bot_scan+1]) before NDC conversion. No-op for
whole-pixel rects (edges already 4k/4k-1), so only sub-pixel strips change
and normal fills are untouched.

Also: remove the temporary GE_D397SH / D397SHM env-gated shader probes
(gfx_opengl.cpp, gfx_pc.cpp) now that the root cause is settled.

Docs: findings.md D397 -> RESOLVED (+3-scale confirmation + fix record),
findings-index.csv regenerated, porting-notes.md section D15 marked resolved
with the landed fix.

Verified by playtest: gun-barrel + file-select gradient render clean, comb
gone.
2026-09-27 22:42:43 -04:00
James Dansereau 2a60b8a912 port: D397 — D397SHM input-mapping probe; H1 LERP-factor variant ruled out
New env-gated D397SHM probe in gfx_generate_cc dumps which G_CCMUX
inputs a combine feeds as vInput1..N. For the gun-barrow row-strip
shader (id0=0000000010001218): vInput1=PRIMITIVE, vInput2=ENV_ALPHA,
so the front.c LERP renders out = PRIM + (TEXEL-PRIM)*(envA/255).

The PD ground-truth port feeds the identical env_color.a/255 factor for
the same ENV_ALPHA LERP input, so the LERP factor is not a port-side
scaling bug (H1's 1/255-vs-1/32 variant refuted). A uniform factor
cannot produce the *periodic* comb, which points to H2 (row-strip
geometry / 256-tri vertex-ring wrap). Next: Q3 gfx_flush() probe +
Q5 native FBO capture. Findings §D397 + handoff updated.
2026-09-27 22:15:17 -04:00
James Dansereau 65875ad45b port: D397 — retract the ineffective CI->I import block; correct the finding record
Per scratch/d397_fs/HANDOFF-D397.md: the stale-RENDERTILE hypothesis is
REFUTED (D161 already routes CI + G_TT_NONE to the I8 import; the GE_TEXI
logs show palfmt=0 on all 299 row strips), so the D397 block added to
import_texture had no effect (12px frame diff) and is removed.

The finding's true state: data path proven clean (byte-exact I8 uploads
vs the ground-truth PGM); MSAA/mips/noise/quant/cache ruled out.
Leading cause H1 = the file-select custom-CC LERP feeding ENV_ALPHA 0x14
as a 20/255 mix factor (black texels render ~0.92x prim = the measured
27/28/17 census); the N64 RDP 5-bit coefficient scaling (gmain.s / PD
CC engine) is the open root-cause question. H2 = possible separate
row-strip-geometry comb. GE_D397SH probe stays in-tree until the root
cause lands. findings table + tail section + CSV corrected.
2026-09-27 22:05:19 -04:00
James Dansereau 2ba72eb75f docs: v0.4.0 plan/README/backlog/porting-notes refresh for the modern-options wave
- V0.4.0-PLAN: 2026-09-27 refresh (M-wave state, D393-D397 session
  additions, M7 deferral decision).
- README: known-issues + options documentation for the landed
  crosshair/controller/options work.
- GRAPHICS-BACKLOG / porting-notes: D392/D397 and crosshair entries.
- .gitignore: per-landing sweep capture dirs.
2026-09-27 22:04:07 -04:00
James Dansereau a2b2f78dd1 port: D397 — file-select background + gun-barrel comb look: stale RENDERTILE import (fix in tree, by-eye owed)
Root-caused the D397 interlaced/comb symptom on the file-select
background and intro gun-barrel (first display, not round-trip):
titleRenderFolderMenuBackgroundLines loads 299 one-pixel 440-byte I8
rows with gDPLoadTextureBlock(I, 8b) and samples G_TX_RENDERTILE but
never emits gSPSetTile, so the tile's fmt/siz is whatever the previous
screen left (CI/4b spectrum-screen leftover, D116 probe). fast3d ran
the I8 bytes through import_texture_ci4 against a stale/all-zero
16-entry palette -> periodic black rows (GL texture-cache artifact,
not a ground-truth dither — no ROM RLE decode needed).

- gfx_pc.cpp import_texture: route the import through the source's
  fmt/siz when a CI/RGBA tile genuinely disagrees with an I8 8b load
  (mirrors the D229 case); real CI/RGBA textures untouched.
- gfx_opengl.cpp: GE_D397SH env-gated shader-formula probe for the
  file-select gunbarrel combine (label initially written as D398,
  renamed to avoid the PC-Options-label finding collision).
- findings: D397 row -> FIX LANDED (by-eye + PCDUMP verification
  owed); tail section notes the fix. CSV regenerated.

Cross-ref D182(2) (same symptom family), D159/D183 (disproven leads),
D229 (mirror-case precedent).
2026-09-27 22:04:03 -04:00
James Dansereau 545b2c7792 port: v0.4.0 modern controller + options-UI wave (D371-D388, D393-D396)
Input (input.c/h):
- D380/D386: single effective PC layout; ini values ARE the bindings
  (no hidden defaults); versioned migration materialises effective
  GEPD defaults once (inputMigrateBinds, bindsVersion).
- D385: keyboard keys and pad buttons 1..5 share the two bind slots;
  legacy extra ini tokens are migration-only. D386: SDL button-number
  correction in the D385 defaults.
- D383/D384: opt-in inputBindingProbe (restores ini state); the PC
  settings editor must not navigate with live game keys.
- D393/D394: Xbox 1.1 Jinx face map — A use, X reload, Y weapon
  cycle, B owned-gadget cycle (no N64 equivalent, port-side), L3/R3
  crouch, LB aim; retires LB/RB weapon cycling.
- D395: both options UIs refuse pad-initiated keyboard/mouse capture;
  pad B cancels a capture started by keyboard/mouse.

Options UI (optionsoverlay.c/h):
- D388: nested Input -> Bindings / Gameplay -> HUD roots in both
  F10 and front-options; redundant Low-end toggle retired (FPS cap /
  MSAA remain independent, stored values preserved on migration).
- D391: hit bands follow rendered glyph baselines; dead zone +
  out-of-card click rejection.
- D396: F10 pad B backs/closes a page (closes at root), A/X
  accepts/toggles without shifting sliders; left/right still adjust.

All behind #ifdef PORT; N64 input/menu paths untouched. M7 (D387
AllUnlocked save-safety) intentionally NOT in this wave.
2026-09-27 22:04:00 -04:00
James Dansereau 4bd49f739f port: v0.4.0 M2 crosshair wave (D373/D379/D381) — opt-in hide/colour/size/style, default N64-intact
gunDrawSight (gunfire.c, #ifdef PORT) now routes through portCrosshair*
accessors: hide flag, colour presets + custom RGB, 100% default size,
and an opt-in beta crosshair sprite. With all defaults the original
red sprite, 16-unit half-size and N64 combiner are untouched; non-PORT
builds keep the original literals and control flow.

- D373: crosshide/tint plumbing (was rule-2 sign-off PENDING at M2
  authoring; the PORT-guarded pattern in gunDrawSight is the sign-off
  record).
- D379: only recoloured reticles switch to the texture-alpha silhouette
  combine (portCrosshairApplyTintCombine); the original keeps the N64
  G_CC_FADEA commands.
- D381: size/style/colour surface + live config apply
  (videoRequestLiveConfigForKey) in video.c.

Rule-2 framing: port-layer feature behind the gunDrawSight #ifdef PORT;
zero N64-path change.
2026-09-27 22:03:53 -04:00
James Dansereau 368daf1242 port: D392 — train-intro soldier pose: read firing-table fields by name on PC (ABI/layout only)
chrlvUpdateAimendbackShoulders read the leading anim union of
weapon_firing_animation_table through raw (f32*) slots [12/13/16/17].
On PC the union widens four->eight bytes, so those offsets alias the
wrong fields and the intro soldier hunched with the hand through the
torso (D392, vs the 1964 reference).

The #ifdef PORT arm reads the intended limits/arm fractions by name
(config->max_up/max_down, free_arm_frac_up/down); the non-PORT arm is
untouched. No logic or behavior change beyond the pointer-width
misread; D392 finding + Train aim-angle probe confirm, user visually
verified against 1964.
2026-09-27 22:03:48 -04:00
James Dansereau c214fa7815 D398-D400: PC Options label work + findings.md tail-section recovery
- D398: replace D348 procedural slider glyph with ROM-native IMG_DOT
  texture (texSelect + display_image_at_position, Copy/Erase pattern);
  superseded by D400.
- D399: file-select bottom row shifted 15px left as a block (user
  sign-off); selectpos.f[0] behind a new #ifdef PORT in front.c.
- D400: icon removed on user sign-off; PC Options is now a text-only
  label exactly like Copy/Erase (one textMeasure + one textRender,
  game's packed 0xEBD879FF gold). Fixed two measurement bugs:
  textMeasure(textheight, textwidth) args were passed swapped (label
  35px above the bar, hit band transposed) and the bare label measured
  h=0 vs 14 for newline-terminated Copy/Erase (7px low); height now
  measured with kLabelNL. LABEL_GAP 20->28, LABEL_X 343->351.
  PCDUMP frame 195: tops aligned 279.8 vs 279.3, 31px clearance from
  Erase. By-eye check owed.
- docs: findings.md had been truncated to its section table (686
  lines), losing all ## Dxx tail sections D376-D400. Recovered:
  table D376-D400 rows + body/sections D001-D375 from HEAD;
  D376-D387, D396-D399 sections recovered verbatim from pi session
  transcripts; D398 section recovered from the 2026-09-28 session;
  D388-D395 rebuilt as explicit reconstructed stubs from the
  table/CSV one-liners (original detail unrecoverable); D400 section
  written fresh. findings-index.csv regenerated and verified
  byte-identical to the pre-truncation copy.

Build clean (ntsc-final). Verified: PCDUMP frame 195 (no overlaps,
tops aligned), CSV matches pre-truncation backup.
2026-09-27 21:58:48 -04:00
James Dansereau 2e0643c70f v0.4.0 D375: crouch now emits C-down (the engine's real crouch input), not B
bondview2.c reads crouchDown = insightaimmode && (buttons & (D_JPAD|D_CBUTTONS))
(5318/5579): crouch is C-down WHILE IN AIM MODE, never the B button. M3's
IA_CROUCH -> GE_CONT_B (and the CrouchMode=1 latch) was inert, so crouch
never fired in the GEPD layout. Hold + latch now emit GE_CONT_D (0x0004).
N64-faithful side effect: S (IA_BACK, also C-down) crouches while aiming.
User re-test: aim (RMB/Left Shift) + Left Ctrl; hold or F10 Toggle.
2026-09-27 16:47:55 -04:00
James Dansereau fca07bdd9a v0.4.0 M2 (D373): crosshair on/off + colour; D374: GEPD becomes the default key layout
M2 (the wave's only rule-2 item, sign-off recorded via user direction):
#ifdef PORT hook in gunfire.c gunDrawSight -- Video.CrosshairHide skips
the draw, Video.CrosshairColor tints the RGB (7 named tints, 0 = the
N64 white); non-PORT builds keep the original literals. F10 GRAPHICS
rows + kResetDefaults.

D374 (M3 playtest fix, user decision): GEPD is now the DEFAULT key
layout (Input.Layout 0 = GEPD, 1 = N64-era binds; a mouse FPS layout is
the natural default and the N64 never had a keyboard). The preset is
now the true effective default: only EXPLICIT per-key [Input.Bind]
overrides beat it; persisted values equal to the N64 defaults count as
unset. This fixes the playtest bug where a stale persisted
Fire=Left Ctrl masked the GEPD preset and made Ctrl fire+crouch at the
same time. Boot log reports the applied layout. One-off: pre-D374
Layout=1 (ex-GEPD) inis mean N64 now -- flip to 0 (release note).
2026-09-27 16:43:04 -04:00
James Dansereau b95b54ecc7 v0.4.0 M5: low-end performance preset row (D372)
Video.LowEndMode (default off) in F10 VIDEO: ON writes FpsCap 30 +
MSAA x1 (restart row); OFF restores the compiled defaults (60/2) rather
than per-user pre-preset values. The key records the last selected
preset; the effect flows through the two real knobs.
2026-09-27 16:30:32 -04:00
James Dansereau 40ef53e395 v0.4.0 M3: GEPD key-layout preset + crouch/reload bindings (D371)
Input.Layout (0=FPS default, 1=GEPD) + Input.CrouchMode (hold/toggle) rows
in F10 INPUT (Turok-standard functional section; the planned MODERN bucket
is dropped per D356). The GEPD preset acts as the per-key effective
default under [Input.Bind] overrides; new IA_RELOAD/IA_CROUCH actions emit
GE_CONT_B (unbound by default, N64-original). Toggle crouch latches B with
a menu-boundary latch drop. optionsoverlay rowSetCommit hook re-derives
the binds via inputLayoutApply(). inputRebuildBinds no longer warns on
intentionally-empty defaults. Boot + ini round-trip + F10 page verified;
in-game feel-check owed (D371).
2026-09-27 16:29:18 -04:00
James Dansereau c2c6fbdd68 checkpoint: F10 GE-style refresh + distance defaults retune, D360-D370 series (local WIP; D370 pending user acceptance) — pre-wave baseline for the v0.4.0 modern-options wave 2026-09-27 16:18:20 -04:00
James Dansereau d8971fded0 options UI: settle file terminology on the end-user term 'Profile' (D359)
'Game file' (the manual's term) reads, in modern PC usage, as the
program's own files, not the user's save. 'Profile' is the clearest
end-user term for what these files actually are -- a per-player
bundle of progress + per-file settings -- and '(per profile)'
expresses the tag's scope where '(save)' read as a verb.

Strings (both UIs: front options + F10 overlay):
- front top row: 'Save file' / 'File N'  ->  'Profile' / 'N'
- section annotation: '(File N)' / '(no file)'  ->  '(Profile N)' / '(none)'
- per-file row tag: '(game file)'  ->  '(per profile)'
- F10 unavailable value: 'Select game file'  ->  'Select a profile'

Display layer only: identifiers and ini keys (Bond.* rows,
watchSettings*, ini) are untouched. D357 plan section 4 is the
terminology record; new D359 finding + index row; manual
'game file' -> 'profile' sync noted as a TODO (port-owned doc,
not in this repo). Build + headless smoke clean.
2026-09-27 15:03:24 -04:00
James Dansereau 44d2b4353a options UI: anchor all file terminology on the manual's 'game file' (English review)
Full-string audit of the settings UI (front options + F10 overlay):
- front top-row label 'Save file' -> 'Game file' (the instruction
  manual's term)
- per-file row tag '(profile)' -> '(game file)', both UIs
- F10 unavailable value 'Select file' -> 'Select game file'
- kept: 'File N' values, the '(File N)' section annotation and the
  '(no file)' unavailable state (short forms; the annotation's host
  word is the section title, which says the file number)
- 'Bond file' now appears only in identifiers/config keys
  (Bond.* rows, watchSettings* names, ini keys), never user-visible
Docs: D357 plan section 4 is now the terminology record (final string
table); D358 finding notes the (profile) -> (game file) supersession.
Build + headless smoke clean.
2026-09-27 15:00:01 -04:00
James Dansereau 8775ca6435 docs: D358 (D356 level-1 overlap fix) finding + index/CSV 2026-09-27 14:55:54 -04:00
James Dansereau 4147b55865 untrack local build-pc-cmd.bat (machine-specific paths); gitignore it 2026-09-27 14:55:44 -04:00
James Dansereau f8f94cdd87 frontoptions: fix level-1 overlap (save-file row painted under the first content row) + off-by-one item mapping; (save) tag -> (profile); D357 values plan
- The front options screen numbers items 0..s_rowN (0 = the top
  save-file row, k = content row k-1), but the level-1 draw loop painted
  content row k at rowY(k) -- the save row's own line -- and mapped
  s_hl (item-numbered) straight into s_rowIdx (content-numbered): the
  'Save file' row overlapped the first content row, highlights/adjusts
  hit the wrong row, the last content row was dead, and s_rowIdx[s_hl]
  could read one past the built rows. Content rows now draw at rowY(k+1)
  and all s_hl -> s_rowIdx uses take s_hl - 1 (A-press, L/R repeat,
  reset-maintain); hit-test/cursor were already item-numbered and are
  untouched.
- Per-file row tag '(save)' -> '(profile)' (maintainer request), both
  UIs (front options + F10 overlay) plus the stale comments.
- docs/dev/D357-SETTINGS-VALUES-PLAN.md (PROPOSAL): row-by-row audit of
  current defaults/ranges/steps/displays vs the PD port (local checkout)
  and the Turok PC list (maintainer notes); proposals: mouse sensitivity
  as a x multiplier, deadzone as a whole-% key (D333 legacy-alias
  pattern), texture filter '3-Point' -> Bilinear, audio step 128 -> 328
  (whole-%), MSAA +16x, FPS-cap preset grid, FOV degrees; kResetDefaults
  lockstep gate + GE_WSPROBE_RESET re-verification.

Build clean (248 targets), headless menu boot smoke clean; visual
check of the front options screen + F10 owed to the maintainer.
2026-09-27 14:55:41 -04:00
James Dansereau fe57d73924 optionsoverlay/watchsettings/frontoptions: D356 settings regroup — functional sections, save-file scoping, per-section reset-to-defaults (Gate E)
- 5 sections (INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO, all below MAX_PROWS):
  DISPLAY+GRAPHICS merged into GRAPHICS; AUDIO carries the watch music/FX
  sliders out of the Bond-file page; VIDEO keeps the pure video rows;
  __HdrMouse/__HdrPad merged into INPUT; __HdrBond/__BondFile retired for
  the front-only top save-file row (frontoptions.c)
- save-file scoping: per-file sections (GAMEPLAY, AUDIO) annotated
  '(File N)'/'(no file)' via the new Row.saveScoped flag +
  watchSettingsActiveFolder(); per-file rows in mixed sections carry a dim
  '(save)' tag; the D353 optionsRowIsBondChooser special-case is
  generalized (both UIs)
- N64-watch rows Bond.Look / Bond.AimControl commented out of the menu
  surface (D181/D216 style: keys + watch paths stay live)
- per-section 'Reset to defaults' replaces the Turok rows (plan Gate E):
  edge-triggered two-step arm->confirm within 3 s, one commit, disarm on
  timeout / navigate-away / close (optionsResetMaintain/Clear);
  rowResetSection walks the section's declared range: watch rows ->
  BLANKSAVEDATA through the D352 commit path (front direct, stage queue),
  ini rows -> kResetDefaults[] (the port's C initializers); __Resolution
  is a documented exclusion (action-backed)
- GE_WSPROBE_RESET probe: front tick-120 prepare/dispatch/verify, stage
  tick-120 dispatch / tick-121 verify; a direct -level_XX boot starts its
  timer from phase 0 (front prepare is front-only)
- docs: findings.md D356 + index + CSV, WATCH-SETTINGS-PLAN status,
  OPTIONS-MENU-PLAN pointer, plan doc status -> LANDED

Verified headless: GE_WSPROBE_RESET front (all 5 sections failures=0,
__Resolution untouched, file-2 scope isolation OK) and -level_33 (6
commands queued, all sections verified post-drain, saved music/sfx = 0xFF);
GE_WSPROBE / GE_WSPROBE_FRONT regressions clean; 248/248 build + link.
2026-09-27 14:49:08 -04:00
James Dansereau 95f8f5f8b5 docs: D356 settings-regroup plan with tightened reset/save-file contracts (maintainer sign-off) 2026-09-27 14:03:06 -04:00
James Dansereau 2b4c5ba7dc watchsettings.c: resync in-stage BGM (track 1) on Music commit and level entry (D355) 2026-09-27 13:53:36 -04:00
James Dansereau 61263ad303 watchsettings.c: front-end F10 routing + transparent default Bond-file selection, auto-init fallback, headless probes (D354) 2026-09-27 13:48:03 -04:00
James Dansereau 462675cf47 optionsoverlay.c: Turok-style regroup - DISPLAY/GRAPHICS split, BOND FILE absorbs music/FX, designated rows, overflow log, file-scope title annotation (D353) 2026-09-27 13:32:39 -04:00
James Dansereau 8e9bee6ef0 watchsettings.c: D350-bridge robustness - hold-not-drop at stage transitions, chooser spinlock, queue coalescing, warning logs (D352) 2026-09-27 13:32:18 -04:00
James Dansereau 1303c0ebe1 D350: expose watch-backed Bond settings in PC options and F10
Add a game-thread bridge and field-scoped per-file persistence for PC UI edits, with an explicit file chooser; keep watch-only edits session-only. Fix D351 row initializer offsets for units and auto-hide. Document verification and review follow-ups.
2026-09-27 13:20:21 -04:00
James Dansereau 98a24a07f4 Docs: close D343 (NTSC verified; PAL/JP not capturable locally) 2026-09-27 12:30:14 -04:00
James Dansereau 050c3981d6 D348: mini slider glyph beside the file-select PC Options label
No settings icon exists in the game art (mainfolderimages holds only
copy/del/select-arrow/X/check/dot), so a miniature slider control is drawn
instead: three menu-box tracks with offset knobs in the 14px gap after Erase
(10px wide, 2px margins), INK_DIM tracks, knobs matching the label ink
(white / gold while hot). Tracks the dynamic label x (JP-safe), text position
unchanged; hot region extended to include the glyph. PCDUMP pixel-verified.
2026-09-27 12:24:56 -04:00
James Dansereau 7e07d9e018 D347: F10 overlay controller value-adjust + hold-to-repeat
D-pad left/right and stick-X now adjust the selected row's value (A/X/B/Y
kept); 18/4-frame hold-to-repeat added to value adjust and up/down nav,
matching the options screen's D345(e)/(f) cadence. Port-only; user
controller test verified.
2026-09-27 12:18:36 -04:00
James Dansereau 7ff9ddbe14 D345/D346: options-screen pointer + keyboard nav fixes; settings wording pass
D345 (a-g): re-assert window on menu change; cursorToItem skips snap while
the 1:1 pointer is live; W/S emit stick in menus under NaturalPitch;
F10-style discrete row stepping + arrow left/right value adjust replace raw
drift (first pass shipped broken, fixed); screen-placed cursor parks in the
left gutter off the row text.

D346 (+b): labels/values aligned to Nightdive/Turok + PD-port conventions
(title-case On/Off, Frame rate cap, Anti-aliasing/None, Anisotropic
filtering, units in values not labels, deadzone as %, self-explanatory
auto-FOV toggles); VIEW section merged into VIDEO (MAX_PROWS 12->15,
ROW_DY 20->18). Display-only: config keys and stored values untouched.

Port-only; no src/game edits. User playtest verified.
2026-09-27 12:12:32 -04:00
James Dansereau 835035916d D343: PC Options joins the file-select bottom bar; Copy/Erase shift left
Rule-2 layout edit (user sign-off 2026-09-27): Copy 247/225 -> 222/200,
Erase 357/335 -> 304/282 under #ifdef PORT; their click boxes follow.
"PC Options" is drawn by the port hook right after Erase's measured text
end (min x 352), so wider localised labels push it right. The top-right
label is gone.
2026-09-27 11:01:58 -04:00
James Dansereau 13473d5ec6 D343: PC options as a GE front-end screen (MENU_PC_OPTIONS)
Replaces the M2 panel. Built like the Cheat Options screen: open dossier
(tabs + blank CLASSIFIED page), black Zurich ink via frontPrintText, GE's
highlight box, PREVIOUS tab as back, the game's crosshair. Page 0 is a
numbered section list; each section lists the F10 overlay's rows with a
value column (ON in the cheat red) and slider bars.

Game code (Rule 2, user sign-off 2026-09-27): MENU_PC_OPTIONS enum value
and five one-line dispatch cases, all #ifdef PORT. The file-select label
now enters the screen via frontChangeMenu. The M2 button hand-off in
input.c is gone; F10/Select don't open the overlay on this screen.
2026-09-27 10:58:27 -04:00
James Dansereau e4cf012410 D343/D344: record user verification on PC 2026-09-27 10:41:25 -04:00
James Dansereau fd82dda717 D343 M2: GE-styled options panel on file select; D344 docs
The file-select Options label opens a panel drawn in the same hook:
sections as tabs, rows from the F10 overlay's table via an exported row
API, the game's crosshair as pointer (input.c passes the stick, hands the
buttons to the panel). Quit to desktop now goes through videoRequestQuit.
Also repairs two escaped-newline typos in the D343 docs.
2026-09-27 10:38:16 -04:00
James Dansereau 461ea871c2 D344: orderly quit so exit never lands inside the GL driver; GE_QUITFRAME
Quit (quit event, window close, Alt+F4, from either SDL pump) used to
exit(0) immediately while the render thread could be mid-frame in the
driver: the likely trigger of the host's 0x119 bugchecks. Now quits are
requested; the render thread glFinish()es, unbinds the context and parks
at a frame boundary, and the host exits after (max 2 s). GE_QUITFRAME=<n>
gives the harness a clean self-quit; golden_gate.sh uses it.
2026-09-27 10:38:16 -04:00
James Dansereau c07af11fae D343: record user playtest (all but a physical controller confirmed) 2026-09-27 10:07:22 -04:00
James Dansereau 413014b8f6 D343: file-select Options label opens the settings overlay; F10 idle-timer fix
One Rule-2 #ifdef PORT draw hook in constructor_menu05_fileselect (user
sign-off 2026-09-27). The label's hit test and open request live in
optionsoverlay.c; the open is applied on the input thread. While the
overlay is open on file select, g_MenuTimer is held at 0 so the screen no
longer drops to the legal screen after 30 s.
2026-09-27 10:01:27 -04:00
James Dansereau bd8ade5c1e D307: note camera reaction timing is tick-based and faithful 2026-09-27 00:31:57 -04:00
James Dansereau 60f12f2bba D245 follow-up: water base-level crop never ran (coloured dashes)
The crop tested loaded_texture.line_size_bytes, which is the whole
LoadBlock (1400 B), so the water still uploaded 32x43 while UVs were
normalised to 32 rows; the mip rows showed as coloured dashes. Test the
tile line size instead. User-verified on Frigate; golden 3/3.
2026-09-27 00:31:24 -04:00
James Dansereau 4ba1454ba8 D342: file-select folders vanished after backing out of a file
The PC wallet model (0x1664C B, 0x17000 reservation) loads at the start of
ptr_logo_and_walletbond_DL, but the file-select scratch DL and the
briefing text still used the N64 offset +0xA000 -- inside the model.
After main menu -> back the model is not reloaded, so the overwritten
folder bodies/photos disappeared. Both now use +0x17000 (same 0x6e000
budget). User-verified; golden gate 3/3. D341 also user-verified.
2026-09-27 00:16:18 -04:00
James Dansereau 5c643e3acb D341: intro gun-barrel blood drip drew static garbage
Two fast3d defects. (1) The 1-cycle blood texture rectangle sampled a
stale tile-1 CI8 declaration: the title leaves G_TL_LOD set and the D236
detail-base rule redirected texunit 0 to tile 1. LOD tile selection is
2-cycle-only on the RDP, so texrects outside 2-cycle now sample their own
tile. (2) The drip image is regenerated in place in the dyn pool (reached
via the KSEG0 mirror); the address-only cache key froze it on its first
image. Dyn-pool textures now key on a content hash. Headless-verified,
golden 3/3. A/B: GE_DYNTEXHASH_OFF.
2026-09-26 19:20:06 -04:00
James Dansereau f242992360 D245: water tiled at the wrong size + s16 sky/water vertices; D242 closed
The IsWater 32x32 CI8 image (drawn through an RGBA16 tile, D229) was
imported with its mip chain as 32x43 and normalised as 16 wide, so the
pattern ran at 2x and wrapped every 43 rows instead of the N64's 32:
fold steps made it jump. Import the base level only and use CI8
geometry in the tri path. Sky/water fans now go through a port-only
G_FLOATVTX_EXT (float clip positions + S/T) instead of s16 Vtx, removing
the tc shift and wScale quantisation jitter. User-verified vs 1964;
golden 3/3; Win + Linux build. A/B: GE_D245_OLDVTX / GE_D245_OLDTEX.
2026-09-26 18:56:52 -04:00
James Dansereau d5f41d6a37 D242: headless death auto-advances without input; KIA screen needs a menu-launch check 2026-09-26 18:22:39 -04:00
James Dansereau d061ca3d12 D303: model converter copied 1/4 of each star muzzle flash's vertices
The op-22 DisplayListPrimaryRecord count is the number of star-flash
arms; dorottex (and PD's modelRenderNodeStarGunfire) read count*4
vertices. d43_emit/d43_convert sized the array as count vertices, so
3/4 of every muzzle star came from neighbouring sidecar bytes and drew
as long spikes (M16 "shooting up"). Size it 16*4*count. Emitter
round-trip checks all pass; M16 renders a clean star headless.

Also TEMP GE_STARTWEAPON (bondview_r.c) for weapon repros. Re-run
tools_pc/d43_emit.py to regenerate data/pcmodels-*.
2026-09-26 18:15:17 -04:00
James Dansereau 00b0006971 D307: security cameras read the mount pad as their look-at pad
CCTVRecord's own s32 `pad` (the look-at pad) shadows the inherited
ObjectRecord `pad` (mount pad) under IDO. The PC struct renamed it
`lookpad` to satisfy GCC, which silently re-pointed setupCctv's reads at
the mount pad, so every camera's look vector was its lens offset and the
lens faced the wall. Read `lookpad` on PC (#ifdef PORT, A1 class);
matches PD's setupCreateCctv (lookatpadnum). User-verified vs 1964,
golden gate 3/3. Porting-notes gains the duplicate-member rename trap.
2026-09-26 18:00:40 -04:00
James Dansereau 40b6994504 Findings: 1964 comparisons — D174/D308 faithful; D307/D303/D245 confirmed port bugs; D242 N64 reference 2026-09-26 17:46:57 -04:00
James Dansereau 116a9db798 Findings: close D183 D298 D202 D315; D220 merged into D303; D242/D303 to 1964 list 2026-09-26 17:39:29 -04:00
James Dansereau 035d54dd66 Findings bookkeeping: user status calls + stale-row cleanup (M-201)
User-confirmed fixed/closed: D289 D285 D160 D156 D152 D235; approved
closes D286 D296 D85, D323 parked as an enhancement; D218 shipped as
DrawDistance. Shipped QoL rows D180-D216 and D333-D338 marked fixed;
D318/D320/D321 fixed via D329; D236 sub-passes D265-D280 closed.
D186/D339/D202/D207/D174/D308 annotated with user direction.
2026-09-26 17:36:48 -04:00
James Dansereau 506cd7ed11 D207/D241: shot-sound handle probe; live captures refute drop/starvation
TEMP GE_D207S (gunfire.c, #ifdef PORT, read-only) logs every player
shot-sound decision (H1/H2/SKIP). Four user captures incl. a 7-weapon
stress test at 60 fps: 0 SKIP, 0 voice drops, audio production at rate.
The one captured drop-out ran under GE_AUDIOTRACE, whose audio-path file
I/O is a likely cause. Still open as intermittent.
2026-09-26 17:26:06 -04:00
James Dansereau b8e34c7fac D287: user-confirmed fixed (idle-unfocused click-in, mouse checks) 2026-09-26 16:55:58 -04:00
James Dansereau 8f838316c0 Q7: declare atoi/lround for port TUs; D207: ZHOLD/ZREL harness, findings
optionsoverlay.c called atoi and lround through implicit int declarations
(GE's stub stdlib.h/math.h shadow the CRT, D324 class); atoi joins the
other K&R decls in port/shim/stdlib.h, lround gets a local prototype.
Windows + Linux builds clean of both warnings.

GE_INPUTSCRIPT gains ZHOLD/ZREL (sustained fire) for automatic-weapon
audio repros. D207/D241: headless burst-fire at 60 vs 30 fps with the
alarm forced logged zero voice drops at both caps; owed a real-play
capture. Golden gate 3/3.
2026-09-26 16:53:54 -04:00
James Dansereau 0097469810 D291/D306: close Dam intro truck glitch as faithful; add repro probes
D291 (user-captured) is D306: Dam intro camera 5, truck over the walkway
camera; reproduces at FovScale 100. All PC-specific leads refuted with
evidence: FOV, depth precision/DrawDistance, depth state (every truck
triangle is opaque z-tested), near-plane handling, wheel transforms. The
image is a depth-correct view of the authored model from under the truck.
GE_ZF's earlier "real fighting" was a +-255 z quantisation artefact.

TEMP tools: GE_INTROCAM=<n> pins the level intro's first camera
(bondview_r.c, #ifdef PORT, PRNG draw still consumed); GE_D306C depth-state
census in gfx_pc.cpp. Golden gate 3/3.
2026-09-26 15:54:46 -04:00
James Dansereau f1207e915b D252: re-sync compiled explosion DLs on every stage load; D314 confirmed
gimgSyncCompiledGlobalDLs (D68 shim) only copied texLoad's resolved
texture pointers into the compiled globalDL shadows while a slot still
held the 0xABCD IMAGESEG marker, i.e. only for the first stage (title).
Every later stage kept pointers into the title's freed texture pool, so
explosion smoke/fire/debris rendered as rainbow garbage once the stage
overwrote that memory (Facility: past the lockers). Learn the slots once,
re-sync them every stage. User-verified; golden gate 3/3.

Also: TEMP GE_D252POOL texpool/sync probe; GE_D157 F12 burst and caps
(uncapped colour match made live play 4-6 fps). D314 F10 flicker
user-confirmed fixed. Findings M-201, porting-notes C2.
2026-09-26 11:43:30 -04:00
James Dansereau aedc98e247 D252/D314/D291: explosion-debris investigation, overlay wheel race fix, Dam truck notes
- D252 (M-200): the 'rainbow confetti' is the explosion debris
  (explosionInitFlyingParticles). Statically clean for colour; 4 headless
  repros including the user's grenade-launcher trigger are all clean. Still OPEN.
- d320repro (TEMP): new explode= and cheat= actions for headless explosion
  captures; documented in GE-ENV-PROBES.
- D314: optionsOverlayScroll (host thread) rebuilt s_visIdx/s_visN and wrote
  s_sel while the scheduler thread draws the overlay. Wheel notches are now
  queued atomically and applied in HandleInput (D287 pattern). Emit and
  HandleInput share the scheduler thread. Headless file/mission select is
  pixel-stable; likely fixed together with D287, needs a user check.
- D291: truck AI mapped (ai_9 / 0x040a, path 7); headless walk did not reach it.
2026-09-25 23:20:38 -04:00
James Dansereau d07ed0f2d0 D287: apply SDL mouse-mode/cursor changes on the window thread only
input.c called SDL_SetRelativeMouseMode / SDL_ShowCursor from the scheduler
thread (every controller poll) and from the host thread's focus/click
handling at the same time. SDL's mouse API is not thread-safe; on Windows
relative mode starts/stops the raw-input thread, and clicking back into the
window after being unfocused is exactly when both threads flip it - the D287
freeze trigger. Callers now record the wanted state; inputApplyMouseRequests()
applies it on the host thread from videoPumpEvents(). Queue-deadlock leads
ruled out statically. Win+Linux build clean, golden 3/3; freeze re-test owed.
2026-09-25 22:37:46 -04:00
James Dansereau 2ed84ed38b Q7 audit fixes: missing returns in front.c menus, getenv prototype, dram.c shift UB
- front.c: constructor_menu07_missionsel / constructor_menu0D_missioncomplete
  fall off the end while the caller uses the Gfx* result; return DL under
  AVOID_UB (porting-notes D6). Old/new objects disassemble identically at -O2,
  so no behaviour change on this compiler; clang/arm64 no longer rely on luck.
- libultra/audio/event.c: include <stdlib.h> under PORT so getenv() is not an
  implicit int-returning declaration (D324 class).
- dram.c: DRAM_SIZE >> 32 on a 32-bit unsigned long (Win64) was UB.
Golden gate 3/3.
2026-09-25 22:30:48 -04:00
James Dansereau edddf87429 D322: prefer DirectSound over SDL2's WASAPI backend on Windows
Four simultaneous drain monitors on the same USB interface: WASAPI (512 and
2048 samples) fell to 81-88% drain for ~30-50 s every ~317 s and lost 1.4% of
playback, while DirectSound stayed at exact real time. Each WASAPI shortfall
overflowed the audio queue and dropped blocks (the periodic garbled bursts).
audioInit() now hints directsound,wasapi; SDL_AUDIODRIVER still overrides.
Golden gate 3/3; 60 s GE_D204 run clean after load.
2026-09-25 22:25:13 -04:00
James Dansereau 2a20a93318 D322: periodic 'garbled' bursts are the host audio device, not the port
A no-game SDL drain monitor (new tools_pc/sdl_drain_monitor.c, opens SDL
exactly like port/src/audio.c) shows the maintainer's Focusrite USB interface
draining at 88-95% for ~15 s every ~312 s, matching the in-game overflow
bursts' period. Level loads, combat, the SDL pull pattern and a
pacemaker-vs-vsync beat were ruled out. Remaining D322 items: the 8-voice SFX
cap (D207/D241) and #87's progressive decay. porting-notes: measure the host
device before chasing periodic audio glitches.
2026-09-25 19:15:22 -04:00
James Dansereau 82548a0fe9 findings: 2026-09-25 release playtest close-outs + D322 capture
D311 FIXED (root cause D336; 0 anomaly lines incl. Caverns combat), D288
CLOSED, D294 and D330 user-confirmed, D235 still unreproduced. D322: pools
healthy (leak hypothesis refuted); SFX soft-cap saturation in firefights
(D207/D241) plus a new periodic SDL output-queue overflow every ~314 s that
starts at the title screen, so it should reproduce headless.
2026-09-25 18:40:26 -04:00
James Dansereau 7baf490020 tools_pc: golden_gate.sh — one-command per-patch golden gate
Pins data/ge007.ini to a defaults-only 640x480 window (restored on any exit),
captures Bunker 1 at 640-1120:240 and runs framediff.py against the golden
baseline. --level/--dump give a capture-only smoke run (exit 0 = frames
captured and no crash). Full framediff output goes to scratch/.
2026-09-25 16:46:03 -04:00
James Dansereau 251fcc4194 docs: fold drop-in ROM Part B backlog (issue #85) into release-dropin-rom-plan 2026-09-25 10:47:59 -04:00
James Dansereau 684dee9127 D231: park mute-on-focus-loss/screenshot QoL (deprioritized; branch retired from GitHub) 2026-09-25 10:45:11 -04:00
James Dansereau 709959c66a docs: cut-content research round 2 — decomp-only discoveries + full music alignment
New findings not covered by TCRF's articles (full detail in local
backlog/cut-content/RESEARCH.md):

- The shipped game's M16 pickup text still reads 'an AR33 assault rifle'
  (LpropobjE PROPOBJ_STR_2A, propobj.c:10471); unused 'US AR33' strings and
  PROP_CHRM16 comments survive too.
- Klobb rename scar verified: GUN_STR_6B is the only out-of-alphabetical-order
  weapon name in LgunE.c, exactly as TCRF's prerelease page describes.
- cartrifle + wristdart: full pickup/prop resources, no ITEM_ enum, no weapon
  stats — cut weapons missing from TCRF's unused-items list (wrist dart = the
  AMMO_DARTS hardware; decomp comments it '(BETA)').
- All four Bonds mapped: CUFF_/BOND_ Connery/Dalton/Moore enums + the
  actor-switch code in bondview2.c with every branch flattened to Brosnan;
  no former-actor models exist in the ROM (80 C*Z chr models enumerated).
- Cuba (LlenE.c) is the only orphan level with recoverable text: cut Natalya
  dialogue, title card, alternate credits cast.
- Orphan-level geometry confirmed present in ROM (obseg.h segment decls);
  what's missing is mission setup, not geometry.
- Music enum vs Notes-page GS index aligned slot-for-slot through 0x2C with
  four local mismatches past it (49/52/54/59) pending in-game audition.
2026-09-25 10:44:44 -04:00
James Dansereau 019ec2f233 D226: cap HUD scale at 150% (user playtest: works, little benefit above 150) 2026-09-24 11:40:43 -04:00
James Dansereau 062f605520 plan: reconcile v0.4.0 rows (D226, D237, D238, D294, D311) + session additions (D336-D339, #105 note) 2026-09-24 11:29:21 -04:00
James Dansereau cd651000ca D226: findings write-up (implemented, verified) 2026-09-24 11:29:03 -04:00
James Dansereau 3072035417 D226: HUD scale option (Game.HudScale 75-200%) — fast3d G_HUDSCALE_EXT rect scaling about per-element anchors
Ammo (per-hand corner), bottom messages/title card (bottom-left) and top
dialogue (text origin) scale with crisp glyphs; dialogue and bottom
messages are width-capped because their line breaks are authored. 100%
emits nothing (golden 3/3). Captures verified at 150/200%; Win + Linux.
2026-09-24 11:28:29 -04:00
James Dansereau 0de97b9c67 D311: likely root-caused by D336 (head-bob alias read); headless Caverns clean at 2x pool; close after a live combat session 2026-09-24 11:13:42 -04:00
James Dansereau 36de85e789 D339 (#92): GE_PERFSTAT perf probe + measured CPU budget (CPU likely not the low-end bottleneck; GPU suspect)
Env-gated per-frame timing: render phases, tris/batches, game-thread busy,
GL_TIME_ELAPSED GPU timer. Measured ~2.1 ms/frame total CPU on the heaviest
levels here; the GPU timer is only meaningful when GPU-bound. Target-hardware
numbers needed before a low-end preset. Golden 3/3; Win + Linux builds.
2026-09-24 11:11:47 -04:00
James Dansereau 02c361c909 D337/D338: user feel check ('Seems good'); legacy AimStyle migration exercised live 2026-09-24 10:57:15 -04:00
James Dansereau 827ab54288 D338: Aim range option — PC (default, full-screen GEPD feel) or N64 (original 65% stick limits, camera turn from ~49%)
Scales the mouse accumulator's screen mapping and edge-scroll threshold; aim
entry adopts through the game's screen mapping so it never jumps. Measured:
PC 98%/72%, N64 65%/49%. Golden 3/3; Win + Linux builds.
2026-09-24 10:52:42 -04:00
James Dansereau 2d2f56b050 docs: GE_AIMLOG + scripted mouse/aim tokens in the probe catalogue (D337) 2026-09-24 10:40:26 -04:00
James Dansereau 1a33cc09b5 D337: fold aim modes (N64 via PD integration default, Centred (PC) opt-in); Q/L + Toggle aim routing; per-device mouse integration; scripted-mouse aim harness
- Input.AimMode 0=N64 (D332 PD-integration, default) / 1=Centred (PC); legacy
  AimStyle/PdMouseAim migrated; hidden Input.AimLegacyGepd fallback.
- Mouse aim routing follows RMB/Aim, Q/L (the game's second aim button), and
  insightaimmode (Toggle aim, pad): removes the accidental Q 'FPS aim'.
- The PD game hook only feeds the mouse while the mouse is the active aim
  device, so pad/keyboard stick aim is not swallowed.
- Harness: GE_INPUTSCRIPT MDX/MDY/RHOLD/LHOLD/RREL + GE_AIMLOG; measured old
  vs new: same screen range and edge-scroll, jitter equal headlessly.
Golden 3/3; Win + Linux builds.
2026-09-24 10:40:16 -04:00
James Dansereau d3b4b9cd0a D336: user-confirmed (Facility>Frigate, Runway>Frigate; 0 D156 in a 14-level session) 2026-09-24 10:12:03 -04:00
James Dansereau 91564f79b6 docs: porting-notes on dead placeholders after inlining a split struct (D336); probe catalogue 2026-09-24 09:58:09 -04:00
James Dansereau f73599f449 TEMP diagnostics: GE_D235_NEXT (load a different stage after abort) and D85TEX per-stage texture-pool size/range line 2026-09-24 09:57:55 -04:00
James Dansereau c5f0d1b741 D335a follow-up: reset the scissor after the pillarboxed front end so the squeezed scissor can't carry into the first stage frames 2026-09-24 09:57:55 -04:00
James Dansereau 38354e1c7e D336: fix Frigate 'everything breaks but the gun' — head-bob anim read uninitialised alias fields (A1, D100 follow-up)
bheadAdjustAnimation read player->field_5C0 / animFlipFlag, which on N64
are raw-offset aliases of the inline player.model (model.animframe1 /
model.gunhand). Since D100 made model a real inline struct on PC they were
separate, never-written fields: Bond's head-bob start frame came from
uninitialised player-struct bytes (2e32 etc.), poisoning his model anim
(D156) and wrecking the scene. The D294 2x room pool only moved the player
struct onto dirtier memory. Read/write the real model fields.

Found with gdb hardware watchpoints (field never written). Headless repro
Runway -> Frigate -> turn now clean, D156 0; golden 3/3; Win + Linux.
2026-09-24 09:57:55 -04:00
James Dansereau b74d2738fd backlog: D335b F10 overlay pillarbox (polish) 2026-09-23 15:40:54 -04:00
James Dansereau 8e07bced9d D334/D335: 16:9 smoke sweep over 16 more levels — all render, no crashes, no slabs 2026-09-23 15:40:09 -04:00
James Dansereau 89a9875542 README: drop the fixed gun-barrel widescreen note (D335a) 2026-09-23 15:29:57 -04:00
James Dansereau 9232ea180f D335: wrap end credits; fix D335a gun-barrel bar leak (full-canvas scissor under the pillarbox)
Verified at 16:9: -level_54 ending subtitles/cast roll keep their shape;
gun-barrel backdrop now clips at the 4:3 edge. Emitted only for windows
wider than 4:3. Win + Linux builds clean.
2026-09-23 15:29:48 -04:00
James Dansereau 66b33f7f45 D226: design refinement — HUD scale needs a fast3d span transform (boxes + text + icons), not per-glyph scaling 2026-09-23 15:00:38 -04:00
James Dansereau 91bd0f84b7 D237: F10 overlay category headers (DISPLAY/VIEW/MOUSE/CONTROLLER/GAME) + SkipIntro, Show FPS, pad invert/deadzone/trigger rows
New non-selectable ROW_HEADER kind: navigation, wheel and clicks step over
headers. Wheel scroll already existed. Port-only; Win + Linux builds clean;
overlay capture verified.
2026-09-23 14:59:37 -04:00
James Dansereau 7a541da877 plan: widescreen done (D334/D335) 2026-09-23 14:57:07 -04:00
James Dansereau ae1b39ff25 README: native widescreen replaces the stretched-4:3 known issue and roadmap entry (D334/D335) 2026-09-23 14:56:50 -04:00
James Dansereau f62bceaf88 D335: native widescreen, HUD + front end (Option B Phase 3)
HUD elements now keep 4:3 proportions via fast3d's EXT aspect modes (the PD
pattern): watch, gauge bars, messages/title card, upper text and countdown
CENTER; ammo RIGHT (right hand) / LEFT (left hand). The front end is
pillarboxed (whole menu_jump_constructor_handler CENTER) and the menu
pointer is mapped into the pillarbox. port/include/hudaspect.h emits only
when native widescreen is on and the window is wider than 4:3, so 4:3 and
native-off stay byte-identical. Rule-2 approved.

Verified with 16:9/21:9 captures (HUD, watch, title card, legal, file
select, gun barrel); golden 3/3; Win + Linux builds. Gun-barrel circle
leaks into the bars while sliding in -> GRAPHICS-BACKLOG D335a.
2026-09-23 14:56:29 -04:00
James Dansereau d5d9a36a6f D334: native widescreen, world side (Option B Phase 2) — Video.NativeWidescreen
Generalise GE's own N64 16:9 mode (aspect = vp_ratio*0.75*16/9, the TV
pre-squash) to the real window aspect: the port's canvas->window stretch is
that TV, so the world renders undistorted at any aspect (Hor+, vertical FOV
kept). All of GE's 16:9 special cases generalised under #ifdef PORT (camera
aspect NTSC+EU, crosshair sprite width, rocket viewmodel offset); Rule-2
approved. Phase-4 vertical FOV boost is skipped while native is on (would
double-count); room-pool aspect term kept. Default on; F10 row.

4:3 identity (golden 3/3); native 16:9 centre == 4:3 frame (diff 4.3 vs
12.4 stretched); 21:9 clean on Bunker/Facility/Dam; Win + Linux builds.
HUD still stretched (Phase 3 next).
2026-09-23 14:44:46 -04:00
James Dansereau 639a112b9b D333 (#104): Input.AimStyle — N64 / PD damp / centred FPS-style aim on RMB
One F10 selector ("RMB aim style") instead of stacking toggles. CENTRED
routes RMB-held mouse motion to hipDirectCompute (FOV-scaled) and emits no
aim stick, so the game's aim integrator keeps the crosshair centred: the
behaviour the reporter found on Q (game L button), now on RMB. PD DAMP is
D332 (PR #96); legacy Input.PdMouseAim=1 migrates to it. Default N64 =
unchanged. Port-only.

Windows + Linux builds clean; bunker1 golden 3/3. Feel check owed.
2026-09-23 14:34:24 -04:00
Dan Turner 05e5525db9 input: fix reticule jitter in aim mode — adopt the PD mouse-aim model (D332, PR #96)
Opt-in Input.PdMouseAim (F10 "PD mouse aim"): in aim mode the port only
accumulates the mouse position and the game's own crosshair integrator is
driven by it with PD's mouse damp (0.01), instead of GEPD's per-poll
overwrite fighting the weapon's ~0.8 per-tick damp (write*damp^k jitter).
Hook: #ifdef PORT block in sub_GAME_7F067FBC (aim-mode branch only),
identity when off; Rule-2 sign-off given by the maintainer.

Integrated locally from PR #96 with review fixes: finding renumbered
D337 -> D332; GE_PDMOUSEAIM lookup cached (GE_ENVSTR; it ran every poll
and aim tick); "live env" comments corrected. Windows build clean; bunker1
golden 3/3. Feel A/B owed.
2026-09-23 14:29:27 -04:00
James Dansereau 3a7c5d06c5 plan: fix D235 commit ref after amend 2026-09-23 13:34:24 -04:00
James Dansereau c728c42e81 golden: move bunker1 window to 640-1120:240 (frame 400 sat on the nondeterministic intro camera; 3 captures agree) 2026-09-23 13:32:20 -04:00
James Dansereau 971c2d9f62 D281: AllUnlocked on a fresh install now yields the game's own fresh-slot layout
D259 seeded only the volumes on all-zero slots. Because the AllUnlocked read
patch re-CRCs every slot, a fresh eep survived validation as five folder-1
slots with options 0: sight-on-screen off (no crosshair while aiming = the
"RMB aim broken" report) plus auto-aim/look-ahead/ammo off, and the game
reset/rebuilt the duplicates mid-validate. All-zero slots now become what
fileValidateSaves + fileBuildWriteNewSave would build (folder saves from
BLANKSAVEDATA via the game's own setters, leftovers free).

Verified: fresh eep + AllUnlocked + SAVELOG -> 00/21/42/63/80, all CRC OK,
no repair writes. Golden 3/3 (x2 reruns); Windows + Linux builds clean.
2026-09-23 13:27:28 -04:00
James Dansereau 27a560cd0b plan: reconcile v0.4.0 bucket states with the findings index (D75/D195/D197/D235/D288/D290/D294, getenv sweep) 2026-09-23 13:19:19 -04:00
James Dansereau d9d7cd3586 perf: cache getenv in hot-path probes (post-D302 sweep)
Swept all 178 getenv sites; 97 were uncached. The load-time/one-shot ones
stay as-is. The 25 on per-frame/per-node/per-string/audio-thread paths
(langGet, modelGetNodeRwData, modelRenderNodeGundl, skyRender and its poly
path, chrpropRender, frozen-camera tick, amDmaCallback, alRaw16Pull,
_decodeChunk, __CSPHandleMIDIMsg, sndHandleEvent, audioSetNextBuffer,
configGetInputLog/configGetFrameDump) now go through GE_ENVFLAG/GE_ENVSTR
(port/include/envflag.h): one getenv per site per process, exact since the
env is fixed. All sites are inside PORT probe blocks; no game logic touched.

Windows + Linux builds clean (only pre-existing, D324-audited warnings);
bunker1 golden 3/3; GE_INPUTLOG still gates on (12 lines with scripted input).
2026-09-23 13:18:32 -04:00
James Dansereau 94dff8116f D331: shell casings now render (D264 pattern, third site)
The casing draw read g_DefaultCasingModelRenderData (u32[15] {0,1,3,..})
as the wider PC ModelRenderData: flags landed on a 0, so subdraw() drew
nothing and the read overran the array. Use the explicit template.

Verified: PP7 casing ejects and tumbles on native Windows (none before);
bunker1 golden 3/3; Windows + Linux builds clean.
2026-09-23 13:13:14 -04:00
James Dansereau a5804d10b8 D290: fix missing controller model on the watch Control Options page (D264 pattern, second site)
watchRenderController built its ModelRenderData from a raw 64-byte read at
D_80035D04+0x3c, which on N64 is the {0,1,3,...} template spanning into
watchControllerButtonBases. On PC the struct is wider and the globals aren't
at those offsets, so flags read 0 and subdraw() drew nothing. Use the
explicit template, as D264 did for the item preview. Also fix the three
pointer-width/raw-Gfx sites on the same path that now execute: two u32
render_pos casts (fatal under Linux PIE), a (s32) pointer arg, and the
dual-controller branch's hand-encoded G_MTX word (now gSPMatrix).

Verified on native Windows (headless watch walk + capture); bunker1 golden
3/3; Windows and Linux builds clean. D291: headless check noted (needs play).
2026-09-23 13:07:31 -04:00
James Dansereau 30cf76b812 D288: re-check on current build — does not reproduce (dense Silo-intro capture + colour scan); D271 A/B ruled out as the fixer 2026-09-23 12:58:21 -04:00
James Dansereau 2a6bc2bb4a D235: headless reload verification; TEMP GE_D235_RELOAD / GE_D235_OFF harness
Scripted watch Abort on -level_09 -> title stage -> GE_D235_RELOAD re-enters
the level via bossSetLoadedStage() (the front end's mission-start call)
after a front-end dwell. With the fix: stale entries purged at every
transition; the reloaded level renders cleanly. A/B with GE_D235_OFF (reset
skipped): 588-788 stale entries survive yet all textures stay correct, for
both an identical and a divergent (intro skipped) first-load order. So the
reported trigger isn't captured headlessly on Bunker 1; the fix stays
correct by construction. D235 stays OPEN pending a real die -> mission
select -> restart playtest (A/B with GE_D235_OFF).

Both switches are TEMP, env-gated, inert when unset; bunker1 golden 3/3.
2026-09-23 11:29:34 -04:00
James Dansereau c1b5b6e519 D330 (#103): mouse no longer drives the stick inside the in-level watch
Auto-Aim / Look Ahead "wouldn't stay on": in the watch, current_menu is
still RUN_STAGE and hipDirectCompute declines on its !watch gate, so mouse
motion fell through to the legacy stick fallback (~3 counts/px/poll). A
~15 px flick crossed the watch's +/-0x2E threshold, which on a selected
option picks OFF/ON (game_option_toggle_input) and otherwise flips pages.
The mouse now contributes nothing while the watch is open or paused;
keyboard and pad drive the watch as before. Save/load round trip ruled out.

Build clean; bunker1 golden 3/3. Mouse feel-check owed (needs real motion).
2026-09-23 11:09:52 -04:00
James Dansereau f89311e9e3 golden: re-baseline bunker1 at 400-880:240 (post-D248 60 fps); defaults-only ini pin
The 2026-09-09 set was captured at 30 fps (2 sim ticks/frame). Since D248
the port renders at 60 fps (1 tick/frame), so frames 200/320/440 landed in
the intro cutscene (nondeterministic camera, D117) and the golden failed on
every build, with or without D329. Moved the window to 400-880:240 (the same
sim ticks the original set sampled, now in settled gameplay); two independent
native-Windows captures agree 3/3.

verify.sh's ini pin rewrote only Width/Height, which kept personal
FovScale/DrawDistance/MSAA settings and failed the gate. It now writes a
defaults-only 640x480 ini (and pins/removes one when none exists).
2026-09-23 11:03:21 -04:00
James Dansereau 7af146a1d0 D329: fix 60 fps guard anim pin (D318/D320 root cause) - tick-granularity divergence
The D318/D320 guard freeze is not a platform-invariant lock. modelTickAnim
folds numticks into one modelSetAnimFrame2WithChrStuff call, which only
advances framea/frameb when floor(frame) crosses an integer. At 1 tick/frame
(port at 60 fps) and attack speed 0.5-0.8, a pair left equal by the aim-hold
endframe clamp is re-emitted stale after the endframe is raised, so the
model pins forever. At >=2 ticks/frame (N64's normal regime) it cannot.

- model.c (#ifdef PORT, Rule-2 sign-off): when the loop did not run and the
  current endframe no longer blocks the next frame, re-derive frameb as
  modelSetAnimFrame does. GE_D329=0 disables, GE_D329LOG=1 logs triggers.
- Verified: lockstep clk=1 no longer pins (Facility ai_19, Control ai_9,
  WSL + native Windows, watchdog off); 0 triggers at clk=2; user playtest
  confirms N64-matching Ourumov timing. Bunker1 golden failure predates it
  (identical with the fix off).
- d318bprobe (TEMP, env-gated): attack-state probe + GE_D318B_CLK lockstep.
- d320repro: fix jump=<chr>,... parser (documented form never parsed).
- findings D329 + D318/D320/D321 corrections, porting-notes D13.
2026-09-23 09:53:10 -04:00
James Dansereau 35483f9892 plan: track mod support and HD texture packs as researched post-1.0 items
Both investigated 2026-09-22 against the MIT PD port fork; neither is v0.4.0
work. Recorded here rather than only in the gitignored notes backlog so the
branch carries the conclusions.

Mod support (D325-D328): community mods repack the ROM, so vanilla offsets are
useless; the fix is an offline importer plus a mod-directory override layer, not
a loader rewrite. GE's file table is in the ROM at 0x252c4 inside the
1172-compressed data segment at 0x21990 (681 exact matches vs filelist, 0
mismatches; hardcode-free discovery reads Random Eye's own table to 704/726
byte-identical). Remaining effort is the mod-dir override layer; added levels
stay blocked by rule 2.

HD texture packs (issue #100): PD's texpack architecture studied - identity, not
decoding, is the problem. Our gfx_pc.cpp is the same lineage and GE_TEXDUMP is
already most of a dumper. Step 1 (address -> texturenum registry) is the only
novel design decision and is a src/game touch needing #ifdef PORT.
2026-09-22 23:20:09 -04:00
James Dansereau 5910350e8e D328: mod support is viable — GE's file table is in the ROM; D327's 'ruled out' corrected
Reviewed the PD PC port fork at perfect-dark-dabs-mod-dabs-mod (MIT, same
license as this tree; AGENTS.md already names the PD port a standing reference)
and verified its approach against our own ROM.

Architectural flip: PD does not load a modded ROM. modimport.c states our exact
constraint ('the port cannot run the patched ROM - it runs decomp code and reads
the ROM at fixed offsets') and instead diffs the patched ROM against stock,
extracting what differs into a mod directory its loader already reads. Mod
support is an offline importer plus a file-override layer, not a loader rewrite.

geconvert.c parses a GoldenEye US ROM directly and gives the constants:
DATA_ROM 0x21990, DATA_VRAM 0x80020d90, FILES_AT 0x252c4, 12-byte records with
size by differencing adjacent addresses - exactly obInit.

Verified here: 1172 magic at 0x21990, raw-inflate yields a 247120-byte data
segment (the same inflate port/src/rzdecomp.c already wraps), table at 0x252c4
with 727 rows, and 681 exact offset+size matches against filelist.u.csv with 0
mismatches.

Then reimplemented discovery with NO hardcoded offsets (scan for 1172 in the
first 4 MB, inflate, scan for a >=500-row run counting from 0) and validated on
two real mods by content:
- Random Eye: its own table found at seg 0x219f0 / table 0x25304; 704/726 files
  byte-identical to vanilla at the extracted offsets, vs 0/803 at vanilla
  offsets in D326. The 22 remainder are fully accounted for: 2 real changes
  (UsetupcaveZ, UsetuptraZ), 1 NULLFILE differencing artifact, 19 zero-size
  unused bg slots.
- Goldfinger 64 (24 MB): table read correctly, 19/726 identical as expected of a
  total conversion.

D327's scans were run over uncompressed ROM bytes, so its mechanism guess was
right and its conclusion wrong; amended here (section heading, inference
paragraph, status line and index row). Also noted: PD's rompatch.c is an MIT
VCDIFF/BPS/IPS decoder, which removes the GPLv2 xdelta3 constraint D325 flagged.

Remaining work is the mod-directory override layer (we have no analogue of PD's
romfile.moddir); rule-2 limit for added levels unchanged. No code changed.
2026-09-22 23:15:50 -04:00
James Dansereau bbc32957e1 D325-D327: ROM-mod (xdelta) compatibility investigation + settled plan
Docs-only. Records the mod-compatibility investigation so it is not re-derived
when mod support is picked up. No code changed.

Measured two shipped community mods against a vanilla NTSC-U ROM:
- Goldfinger 64 v1.0 (12.8 MB xdelta): ROM 12 -> 24 MB, 797/803 vanilla
  filelist.u.csv slices differ.
- Random Eye v1.1 (286 KB xdelta): ROM size byte-identical, yet 96.5% of bytes
  and 803/803 slices differ; content preserved but shifted (+7216/+7392, one
  file -229888).
Both die at tools_pc/d43_emit.py:273 with a zlib error on a slice taken at a
hardcoded vanilla offset. Cause: the file table is compiled in (ob.c ->
file_resource_table.inc.c -> romassets_<region>.s) and the converters reuse the
same vanilla offsets. ROM size is not the constraint; ROM code is never
executed, so ASM hacks are inert regardless.

D325 - Goldfinger 64 probe + mod taxonomy.
D326 - Random Eye repacks too, so D325's hypothesised in-place tier is empty.
D327 - plan settled: reading the table from the ROM is ruled out by measurement
  (hw_address absent in all four encodings tested, 0 hits for the
  segment-relative form; two structural fileentry scans found nothing);
  content-addressed offset remapping with gap inference is the viable route,
  since obInit differencing proves files are contiguous.

Also recorded: GE maps its 0x7F000000 game segment directly from ROM,
uncompressed, uniquely signature-findable at ROM 0x34b30.

The 1964GEPD fork reviewed in D327 is GPL-2.0 against this MIT tree; no code
was copied or vendored, facts re-derived from our own ROM and decomp.
2026-09-22 22:51:21 -04:00
James Dansereau 97c7e38845 D320: resolve sweep via forced repro — ai_19/ai_9 pins confirmed, ai_12 clean; watchdog generalized (table-driven + uncapped repeat rescue); D318 N64-manifestation corrected (unverified; 1964 deliberate-timing A/B armed)
Also commits the prior session's carried-uncommitted set: D294 room-pool
fix (sqrt + 2x cap, user-verified state), D324 stdlib-shim prototypes,
D323 explicit-LOD census + tools_pc tooling, GRAPHICS-BACKLOG row.
texdump*/ probe artifacts gitignored.
2026-09-22 22:30:32 -04:00
James Dansereau 12490b95e6 plan: mark D195/D197 stale in front-end batch (both closed per findings) 2026-09-21 23:54:18 -04:00
James Dansereau eaa137d6a9 D306/D308: investigate z-fighting — depth precision == N64, real fighting confirmed; GE_ZF flicker probe in-tree 2026-09-21 23:53:39 -04:00
James Dansereau 2efbbef964 D303: investigate extra long muzzle flash — pipeline + data ruled out; GE_D303 capture probe in-tree 2026-09-21 23:41:05 -04:00
James Dansereau 61766e8aef D307: investigate CCTV facing-backwards — no port-side bug found; GE_D307 diagnostic in-tree
setupCctv orientation path traced end-to-end and every input verified
N64-faithful against the ROM for Bunker1: PcctvZ (not GcameraZ) switch
data bit-for-bit, boundpads match the pccg sidecar once the legitimate
load-time room-scale (pad->pos *= get_room_data_float2(), ~1.8542) is
applied, record config words incl. unkCC match; all matrix/atan2 helpers
are byte-identical src/game code. PC orientation values are what the N64
computes; if the symptom genuinely differs from N64 the remaining suspect
is fast3d's handling of the non-orthonormal lens matrix (unk84 row 0
scaled 0.1x). Blocked on user repro detail.

Also: plan/README/findings refreshed for M-197 (D75 fixed, user-confirmed)
and M-198 (D236 closed); D252 retired; findings index regenerated.
2026-09-21 23:26:18 -04:00
James Dansereau 0503d33564 D236: strip the investigation probes now that the finding is closed
D236 was root-caused and fixed in the previous commit, so its diagnostic
scaffolding comes out:

  GE_D236RM / GE_D236ORDER / GE_D236ALPHA / GE_D236ZFIX  gfx_sp_tri1
  GE_D236RAW                                             bg.c room load
  GE_D236BT                                              chr.c + model.c
  port_d236bt_dump()                                     host stack backtrace
  g_d236_last_vtx_seg / _raw                             G_VTX trackers

Also drops the <windows.h> include that only the backtrace dumper needed, and
rewords the one D288 comment that cross-referenced GE_D236RM. src/game/bg.c,
chr.c and model.c are back to their pre-probe state -- no game-code diff from
D236 remains. The pass-26 fix in gfx_lod_tile_offset is untouched.

Doc rows removed from GE-ENV-PROBES.md; gen_env_probes.py --check and
gen_findings_index.py --check both pass. Build clean. The Surface 1 spawn
capture is unchanged across the strip (settled frame 560 within threshold;
the intro-pan frames never match run-to-run, D117).
2026-09-21 22:53:04 -04:00
James Dansereau 425237f93f D236 (M-198): fix Surface 1/2 tree cards; root cause was a DETAIL tile binding
GE's texLoadFromGdl picks a handler per G_NOOP texture reference from w0 & 7.
TEXTURETYPE_LOD/MIPMAP (texHandleType0/Type2) issue ONE load block at TMEM 0,
so tiles 0..n are LOD levels of that one image. TEXTURETYPE_DETAIL
(texHandleType1, src/game/tex.c) issues TWO: the detail texture at TMEM 0 and
the real base image at TMEM texGetSizeInBytes(detail,0) on tiles 1+.

gfx_lod_tile_offset folded every G_TL_LOD draw to tile 0 (D107 -- correct for
the first two, wrong for DETAIL), so the port drew the detail texture and never
imported the base image at all. All four Surface 1 treeline cards
(tex1198-tex1201, RGBA5551 64x17 cut-outs) are DETAIL bindings sharing one
detail texture, tex2465: an opaque 32x32 IA8 noise tile. That is the same
texture D267/D276 measured at 1023/1024 texels opaque and called the mystery
"noise painter" -- hence 20+ passes chasing decode, alpha, mip and geometry
theories for what was a binding bug.

Fix (port layer only): under G_TL_LOD, if tile fi+1 differs from tile fi in
BOTH TMEM address and fmt/siz, sample tile 1. Asymmetric on purpose -- LOD
levels of one image always share a format, so a mip chain can never trip it,
and a detail pair that happened to share a format just keeps the old
behaviour. Config Video.DetailBaseTile, default on.

Verified: Surface 1 idle spawn goes from an opaque tiled noise wall to discrete
snow-covered pines with ground/snow unchanged; Surface 2 shows a discrete night
treeline; Depot renders clean; bunker1 golden framediff is bit-identical with
the fix on and off. Offline scan of every level's bg: DETAIL bindings exist
only in sevx (84), depo (39), sevb (4), sev (4), so every other level is
provably untouched. User play-test confirmed.

Also refuted and removed pass 25's fix direction: cut-out mip suppression
changed nothing, and its alpha census showed the wall is painted by a fully
opaque texture -- retiring the mip / 1-bit-alpha / XLU-smear branch. The
GE_D236P25 probe is gone with it.

tools_pc/gen_env_probes.py: stop counting a "GE_FOO[" log-line TAG in the doc
as a documented env var. GE_TEXDUMP_ADDR prints "GE_TEXA[...]", which is not
the name of any getenv(), so the check reported a permanent phantom GONE.

Generalised as docs/porting-notes.md D12; full trail in findings.md D236 pass
26. Carries the pending D75/M-197 sidecar-converter fix and the env-gated
D236/D75 probes, which the doc gates require to land alongside their rows.
2026-09-21 22:48:10 -04:00
James Dansereau 327e6fe1a7 D75 (M-196): refute slot-overwrite hypothesis; narrow logo bug to texture-modulate render path
New env-gated GE_D75D probe (gfx_pc.cpp) captures per-frame V/T token stream
+ per-triangle rejection/emission accounting for the front-end logo window.

Findings (bare front-end boot, logo = frames ~244+, nv=93/nt=1018):
- Slot-overwrite (M-195's leading hypothesis) REFUTED: the DL is streamed
  (V TTT...VTTT interleaved), max triangle vertex index = 15, so no
  cross-batch vertex clobbering can occur.
- Rejection gates are not the cause: of 1018 tris/frame, trivial-reject=0,
  backface-cull ~520 (normal half), ~500 reach GL, all on-screen (NDC in
  [-1,1]^2). Not the invisible flag; vertex colors [0,255]; depth normal.
- PPM analysis: logo window renders 0-33 content px/frame (the 'tiny
  fragment'), vs title quads at 8.36% coverage.
- Narrowed cause: combine mode 0x009ffe4f19ffe4f1 parses to
  rgb a=TEXEL0 c=SHADE (texture-modulate); gfx_generate_cc sets
  used_textures[0]=1, but the material still renders black-on-black. Next
  step: trace where the TEXEL0 sampler is lost between combiner build and
  draw (shader_id0 -> GL program / prg[tm] / texture upload).

Port-layer fast3d only; no rule-2 sign-off needed. Probes env-gated, inert
without GE_D75D (verified: 0 lines in a normal run).
2026-09-21 06:00:28 -04:00
James Dansereau d4440759cd Merge main into release/v0.4.0 (docs: D322 log + mission-count fix) 2026-09-21 05:26:42 -04:00
James Dansereau 5ea053667f docs: correct solo-mission count 21 -> 20 (level_54 'Cuba' is the ending-credits sequence, not a mission)
The campaign has 20 playable missions (SP_LEVEL_MAX // 20 in
bondconstants.h; mission_folder_setup_entries in front.c has exactly 20
entries). The old '21' counted -level_54 (LEVELID_CUBA), which the front
end jumps to after the final mission for the extended cast display +
credits scroll (do_extended_cast_display / bondviewRenderCredits, D129) -
it is not a selectable mission.

- README, docs/index.md, .github/release-notes.md,
  docs/security-and-fidelity-status.md: 21 -> 20 in current-status claims
- AGENTS.md phase status: same correction
- docs/dev/LEVEL-STATUS.md + LEVEL-PLAYTEST.md: relabel the level list;
  counting note explains historical '21/21 PASS' sweep results = 21 stage
  boots (20 missions + ending sequence)
- social-preview card: stat line 21 -> 20, PNG regenerated (1280x640)
2026-09-21 05:23:11 -04:00
James Dansereau aebcf178e6 D75 (M-195): GE_D75V per-batch vertex probe + findings/probes doc updates
- gfx_sp_vertex probe (env GE_D75V=lo-hi): logs per G_VTX batch the
  transformed x/y/z/w min/max, raw first-vertex ob, dest_index, and the
  MP translation row. Proves the logo transform is correct and that every
  batch encodes v0=0 (slot-overwrite hypothesis).
- videoGetFrameCount() accessor for frame-gated probes.
- findings.md: M-195 entry in the D75 section (emission + transform
  eliminated; G_VTX v0=0 overwrite is the leading suspect).
- GE-ENV-PROBES.md: register GE_D75V, bump reconciled line.
2026-09-21 05:14:57 -04:00
James Dansereau bb508e5ce4 D235: wire videoResetTextureCache into per-stage reset (stale GL texture cache); D311: arm speed-poison choke-point probes
D235 (wrong textures after dying/restarting a level) -- mechanism found
+ fix (M-193). M-108's refutation of the game-side CI-cache theory stands,
but the port has its own TextureCacheMap (port/fast3d/gfx_pc.cpp) keyed by
(source address, palette addrs, fmt, siz, size_bytes, palette_hash) with NO
content check. Stage-texture source addresses live in MEMPOOL_STAGE, which
the per-stage while(!done) block in boss.c resets on every transition
(death/restart included). After the reset, entries keyed into that arena are
stale; if the texpool places a different image at an address that held
another one in the previous run of the level, the lookup hits and returns
the old pixels -- 'wrong textures' only on the reload path, never on a cold
boot. The N64 has no such cache (RSP re-reads DRAM every frame), so this is
a pure port-side defect: videoResetTextureCache() (port/src/video.c) existed
with zero callers and is now called in that block after memaReset
(#ifdef PORT). Re-uploads are pixel-identical; cost is one-time during the
loading screen. GE_D235=1 logs the purged entry count per transition
(cold boot = 0, death/restart > 0) as mechanism evidence; new
gfx_texture_cache_count() accessor backs it. Full 21-level golden sweep:
21/21 PASS (pixel-identical). Live death/restart confirmation is a playtest
item.

D311 (upstream poison source, M-194): every write of model->speed funnels
through modelSetAnimSpeed (startframe<=0 path) or modelSetAnimation2 -- both
now log 'D311: ... ANOMALY' when the incoming value or the resident speed is
non-finite/|v|>1e4 (#ifdef PORT, unconditional-on-fire like the D156
guards; silent in healthy play). The line captures in/resident/startframe/
oldspeed/newspeed/timespeed + model ptr so the next Caverns occurrence pins
the injecting call. Static pass: sub_GAME_7F06FE90 callers all pass
arg2=16.0f (denominator safe); the old->new lerp only circulates values;
bheadAdjustAnimation is the one path where a live player-velocity
corruption reaches model->speed.
2026-09-20 23:46:43 -04:00
James Dansereau 8d3bd5732a D320/D288: extend GE_D318T with generic pin detector; refine GE_D288 probe; cache bg.c hot-path getenvs
D320 (Phase 1 item 1): d318TimelineTick (GE_D318T=1) now also runs a
generic D318-class pin DETECTOR for the sweep's flagged lists (Facility
ai_19, Control ai_9, Depot ai_12): any chr in ACT_ATTACK with a live-fire
attacktype (no TARGET_AIM_ONLY bit) whose modelGetAnimFrame is byte-stable
for 600 ticks logs one 'D320T: PIN?' line with AI-list id/offset/atk/ent/
mot/unk54, repeating every further 600 stable ticks. Detection only -- the
recovery watchdog stays hardcoded to ai_22 by design (D320's explicit
instruction: generalize only after a second list actually freezes in play).
Legit states excluded/self-clearing: monologue pre-aims keep the AIM_ONLY
bit; working fire cycles re-init through chrlvTickAttackCommon so their
frame does not stay byte-stable for 10 s. Smoke: 90 s Facility run, zero
false positives, existing D318T timeline unaffected.

D288 (Phase 1 item 3): per M-155's revised next step, the GE_D288 probe
now skips any_behind_camera==1 triangles (the x/w divide is meaningless for
w<=0 -- that subset was pure false-lead noise) and logs the render-mode
word (oml) so a live capture can ID the culprit asset against level tables.
Re-ran bare -level_20: 1965 sane hits in the 550-755 window, still zero
hits for the target color RGB(83,62,44) -- consistent with M-155's working
theory that the bare boot's scripted camera never reaches the bug state;
headless probing is at its limit here, live capture is next.

bg.c getenv caching (D302-class sweep follow-up): GE_D104 (per room pass,
per frame), GE_D63 primary+secondary render logs (per room, per frame), and
the two GE_D154 inner-GDL-walk sites (per bullet hit) now use the cached
static pattern. All #ifdef PORT blocks; zero behavior change when unset.
2026-09-20 23:27:22 -04:00
James Dansereau 56ac2ab870 Phase 0 hygiene: remove GE_OBJT probes (D321 closed; M-192 item 7)
The D321 stage-flag writer probe (chrSetStageFlags/chrUnsetStageFlags in
chraction.c, GAS_RELEASING destruction timestamp in propobj.c) did its job
(D321 trigger chain mapped + closed); all three blocks were #ifdef PORT
diagnostics with no N64-build or runtime effect when unset. Dropped the
GE-ENV-PROBES.md row too (drift check passes).

getenv hot-path sweep (D302 follow-up, HANDOFF M-192): function-level scan
of all 171 GE_* getenv sites found no uncached calls at frame/triangle rate
-- the only candidates are snd.c's event-rate probe guards (all #ifdef PORT)
and bossReturnTitleStage (once per title return). fast3d has zero raw
'if (getenv(' sites.
2026-09-20 23:10:57 -04:00
James Dansereau 7bb43260de D322: log issue #87 long-session audio degradation + ship GE_D322 pool-telemetry probe
User report (v0.3.0 full campaign): audio progressively degrades from Silo;
by Caverns/Cradle the OST is inaudible and SFX come and go; restarting the
game restores it. Logged as D322 with static triage: level teardown audit
clean, mixer stateless per frame, D202's covered leak class is one shape
only (ownerless count==-1 loops) validated single-level only, and D305
already records a pool/list accounting desync. Ranked hypotheses:
voice-pool exhaustion/counter drift > queue starvation > evtq saturation.

Probe: GE_D322=1 prints sfx=n/8 (SFX soft cap), evtq=n/64, pv free/lame/alloc
(24 physical synth voices) + pipeline depth/drops every 5 s of wall clock,
read with GE_D204. sndD322PoolSummary() walks the ALSynth lists under the
D285 lock; NB drvr is a pointer field (first build faulted on &drvr).
Smoke-tested 40 s headless -level_09: pools coherent, no crash.

Also adds the missing D321 row to the findings index (entry existed at file
tail with no table row, so it was absent from the CSV too).
2026-09-20 23:10:57 -04:00
James Dansereau 2d9e673129 D322: log issue #87 long-session audio degradation + ship GE_D322 pool-telemetry probe
User report (v0.3.0 full campaign): audio progressively degrades from Silo;
by Caverns/Cradle the OST is inaudible and SFX come and go; restarting the
game restores it. Logged as D322 with static triage: level teardown audit
clean, mixer stateless per frame, D202's covered leak class is one shape
only (ownerless count==-1 loops) validated single-level only, and D305
already records a pool/list accounting desync. Ranked hypotheses:
voice-pool exhaustion/counter drift > queue starvation > evtq saturation.

Probe: GE_D322=1 prints sfx=n/8 (SFX soft cap), evtq=n/64, pv free/lame/alloc
(24 physical synth voices) + pipeline depth/drops every 5 s of wall clock,
read with GE_D204. sndD322PoolSummary() walks the ALSynth lists under the
D285 lock; NB drvr is a pointer field (first build faulted on &drvr).
Smoke-tested 40 s headless -level_09: pools coherent, no crash.

Also adds the missing D321 row to the findings index (entry existed at file
tail with no table row, so it was absent from the CSV too).
2026-09-20 23:09:03 -04:00
James Dansereau fbc48899ec social-preview: bump card to v0.3.0 (status line; stats refreshed to 199 findings, ~24k C/C++, ~7k Python) + regenerate PNG 2026-09-20 18:07:09 -04:00
James Dansereau 470c239502 docs(agentic-development): velocity chart daily -> single weekly-totals chart (6 bars) 2026-09-20 18:00:19 -04:00
James Dansereau c8309307b9 docs(agentic-development): simplify commits-velocity chart to weekly totals (6 bars) - daily x-axis labels overlapped in narrow renderers 2026-09-20 18:00:09 -04:00
James Dansereau 77ffbbdd64 docs(agentic-development): handoff-workflow flowchart LR -> TD so GitHub doesn't scale the wide diagram down to illegible size 2026-09-20 17:55:54 -04:00
James Dansereau 5d23d2bdfe docs(agentic-development): split commits-per-day chart into August/September halves - 32 x-axis labels overlapped in the rendered mermaid 2026-09-20 17:55:21 -04:00
James Dansereau 4f403ed6d5 docs: deep v0.3.0 accuracy pass — README Background stats (36 days/16 Aug-20 Sep, ~835 commits, D1-D321/199 findings); agentic-dev timeframe 'August' -> 'August-September', gantt gains the 9/18-9/20 final fix sweep + release review bar, findings count 311 -> 321 2026-09-20 17:52:59 -04:00
James Dansereau e153028e07 docs(agentic-development): v0.3.0 release date 9/18 -> 9/20; refresh milestone stats (36 days, ~835 commits, D1-D321/199 entries, M-192, velocity chart through 9/20) 2026-09-20 17:37:25 -04:00
170 changed files with 450506 additions and 1742 deletions

No files matched your search

+292 -180
View File
@@ -1,217 +1,329 @@
## GoldenEye 007 PC Port <version>
# GoldenEye 007 PC Port — v0.4.0
<p align="center">
<img src="https://github.com/jkdansereau/goldeneye-pc-port/raw/v0.3.0/docs/media/goldeneye-gh-preview.gif" width="480"
alt="~12 s gameplay montage from live v0.3.0 play sessions (no audio track)">
<img src="https://github.com/jkdansereau/goldeneye-pc-port/raw/v0.4.0/docs/media/goldeneye-gh-preview.gif" width="480"
alt="~24 s gameplay montage from live v0.4.0 play sessions, opening on the Runway tank (no audio track)">
</p>
> The full single-player campaign runs at a steady 60 fps with audio (music +
> SFX) playing throughout, on Windows and Linux including Steam Deck, and it
> is completable end to end: the whole campaign has been playtested through
> all 21 missions (Agent difficulty). It's an early public cut: all 21 solo
> missions load and run crash-free, but the known issues below are real;
> feedback is very welcome. **This is a pre-1.0 release, not a finished
> product** — expect rough edges and missing features until v1.0; see the
> [README's Roadmap section](https://github.com/jkdansereau/goldeneye-pc-port#roadmap)
> for where this is headed.
Platforms: **Windows x86-64** and **Linux x86-64 (including Steam Deck)**.
Region: **NTSC-U (US) only** — see [Things to know](#things-to-know).
### What's new since v0.2.2
v0.4.0 is a large feature and fidelity release. The full campaign runs at a
steady 60 fps with music and SFX throughout, and has been playtested end to
end (all 20 missions, Agent difficulty) on Windows and on Steam Deck with a
physical controller. This is a pre-1.0 release: the known issues below are
real, and some areas (multiplayer, PAL/JP, HD assets) are still ahead of us.
- **Bond is fixed in cutscenes** — the most visible remaining glitch in
prior releases: Bond no longer floats or glitches out during cutscenes,
his animation no longer breaks or repeats, and the camera correctly
tracks him through scripted sequences including all three Dam end-of-level
abseil shots (D243). Root cause: a 32-bit-era struct-size constant in the
cutscene body-model setup never accounted for 64-bit pointer widening,
corrupting the model's own animation-timing fields on creation. Fixing
that also fixed the associated camera shake, which had previously been
worked around with a camera-freeze hack that's no longer needed. **Two
related reports also cleared up as a side effect**: Bond's third-person
model floating above the ground at level start, and death animations
sinking him into the floor, both shared an underlying mechanism with this
bug — Bond now ends up in the right place the large majority of the time,
with only a small positional drift on the rare occasion he's off (D173/
D292).
- **Particle colors mostly fixed**: bullet-impact sparks and lingering
smoke/explosion residue no longer cycle through a rainbow palette (D219) —
root cause was a hardcoded raw-memory-offset read into the spark's stored
color that landed on the wrong bytes once the containing struct grew from
64-bit pointer widening. A second, separate cause was found and fixed in
explosion rendering itself (an out-of-bounds vertex-buffer write
corrupting adjacent particle data). This fix is real and stays in, but the
rainbow effect can still occur intermittently on any platform, not every
time and not on every level — a second cause hasn't been found yet; see
Known Issues.
- **Automatic widescreen FOV scaling**: the game now scales its field of
view to match your display's aspect ratio automatically, instead of
requiring the old manual FOV-scale slider. Toggle off in the F10 overlay
(`Widescreen Auto`) if you prefer the original 4:3 framing. The scaled
vertical FOV is clamped to a 20°–160° range as a guard against degenerate
values on unusual aspect ratios. (This is FOV scaling, not a native 16:9
world/HUD re-render — see Known Issues.)
- **TV overscan artifacts removed**: the black bars top/bottom and the thin
pixel strips left/right of the gameplay view are gone (D247/D246) — both
were the original N64 TV-safe-area margin, invisible on a CRT under real
overscan but visible on a PC monitor. New `Video.SafeAreaCrop` toggle
(default on) if you want the original framing back.
- **Two more crash fixes**: a campaign-halting crash at the end of Control
Center on 00 Agent difficulty, which would then crash on every subsequent
relaunch until the save was reset (D295); and a crash selecting most save
slots with `Skip Intro` enabled (D299).
- **The elusive complete freeze is root-caused and fixed.** Players had hit
a total, non-self-recovering lockup (most recently when returning from
Caverns to the intro screen) that was real but never reproduced on demand.
It turned out to be a latent defect in the original game's own AI command
stream — a record type the engine mis-measures, which our level-teardown
timing can trigger and the N64 never presents. Fixed with a minimal change
under the project's game-code exception process (D309/D310); a second,
unrelated freeze mechanism found during the same playtest is now guarded
as well (D311).
- **A NULL-pointer guard was added** around a save-data lookup that two
players hit with an identical crash address (GitHub #87, D301) — this
should close that crash, though we weren't able to reproduce the exact
trigger condition ourselves to confirm it end-to-end.
- **Facility's execution softlock is caught and recovered from**: a rare
race in the original game's own AI script (present on the N64 too) could
leave Ourumov frozen mid-execution after Objective C, with no way out short
of reloading. A port-side watchdog now detects the exact unrecoverable
state and re-seeds the attack so the scene plays out as authored; it
re-arms once the scene has cleared, so replaying Facility later in the
same session is still covered (D318).
- **A Steam Deck crash is fixed**: an audio-thread bug could crash the game
early in a level (`sndHandleEvent`'s sound-preemption scan could
dereference a pointer without checking it was valid first). Live-verified
crash-free on real Steam Deck hardware, including with `All unlocked`
toggled on.
- **Mouse input and F10 controls fixed**: mouse input was reworked to
follow the Perfect Dark PC port's architecture — slow mouse motion is no
longer barely recognized and fast motion no longer saturates almost
immediately (D300, `Input.MouseDirectLook`, on by default). The separate
aim-speed and turn-speed sliders — which could be set to inconsistent
values against each other — are now a single `Mouse sensitivity` control
with a wider range. Also fixed three F10 menu bugs found along the way:
clicking a row could unexpectedly scroll the whole list, a click near
certain rows could silently activate a different option than the one you
clicked (most noticeably, clicking `All unlocked` could instead trigger
`Quit to desktop`), and the panel's bottom row no longer duplicates
whatever item is currently selected (D251).
- **Random-number generation now matches the N64 original exactly** (D284) —
a shift-operation bug in the PRNG had desynced the PC's random stream from
N64's since the very first release, affecting loot placement, AI variance,
and other randomized elements. **This changes the random sequence from
every prior build**, so old input recordings/replays will diverge, but
your save files are unaffected: **a migration shim automatically upgrades
any save written by an older build the first time you load it** (D297) —
no manual action needed.
- **Assorted texture/rendering fixes**: an incorrect IA4 texture-format
decode and an overly-tight near-plane portal-culling guard were both
corrected (D266/D271) — reduces some texture-edge and visibility artifacts
on affected levels.
---
### Known issues
## New features
- The front-end **Nintendo logo renders as two white blobs**, and the
Rareware logo is close but its texture filtering looks off (D75).
- **Particle colors: a real fix landed this release, but the rainbow effect
can still occur intermittently.** One genuine, reproducible cause (an
out-of-bounds vertex-buffer write in explosion rendering) is fixed. A
second, still-unidentified cause remains, on both Windows and Steam Deck —
it doesn't reproduce every time or on every level, so treat this as
improved, not fully resolved (D252).
- **Some muzzle flashes draw an extra, erroneous long flash straight up from
the gun** (seen on the M16, among others), on top of the normal, correctly
drawn flash. Cosmetic only (D303).
- **Surface 1: the 2D billboard trees near the start render as a solid wall
of tree texture** instead of discrete sprites (D236). Under active
investigation across ten+ passes; no fix yet.
- **No native widescreen render**: v0.3.0's automatic FOV scaling (above)
expands the *field of view* for 16:9+ displays, but the world geometry and
HUD are still authored/laid out for 4:3 — this is not yet a true
edge-to-edge 16:9 render.
- **No controller rebinding UI, and no macOS or ARM builds.**
- **Distant geometry can drop out on the biggest open levels** (Streets,
Egyptian) at default FOV — a culling/LOD issue that sometimes
self-corrects as you keep moving (D249).
- **Gunshot SFX can sound off during sustained/rapid fire**: cadence can
drift from the N64 original's rate, and PP7/AK47 fire can occasionally go
silent under heavy automatic fire near another looping sound (e.g. an
alarm klaxon) (D240/D241).
- **`All unlocked` is highly experimental — do not enable it until you have
at least one save written.** Complete a level normally first (e.g. Dam on
Agent), then turn the toggle on. Enabling it on a brand-new install with
no prior save can still cause silent audio and odd right-mouse-aim
behavior (D259/D257). (The Steam Deck crash fix above addresses a
separate, now-fixed symptom that had also been associated with `All
unlocked` use; this audio/aim caveat is unrelated and still open.)
- Water on `IsWater` levels shows a moving seam between two patterns (D245).
- **Occasional z-fighting on some levels' geometry** (D308) — a Dam intro/
truck-wheel instance showing odd transparent-looking areas has also been
seen and may be related (D306). Minor, cosmetic; not investigated for
this release.
- **In-level security camera props can occasionally end up facing backwards**
(the wrong direction) on some levels, seen on Bunker — this is the
gameplay security-camera object, not the player's own view (D307). Not
investigated for this release.
- **A rare, self-clearing visual quirk**: on an occasional animation
transition, Bond's model can briefly render out of place. It has no effect
on gameplay or your save, and it clears on its own or by re-entering the
level; a safeguard added this release guarantees it can never affect game
stability (D311).
- **Two collision oddities found in final playtesting**: throwable items
(mines, grenades) can occasionally clip through walls in certain spots
(D312), and bullet impacts don't appear on some surfaces — doors and
windows are unaffected (D313). Both are queued for the next release; if
you hit either, an issue with the level and spot helps a lot.
### Native widescreen *(on by default)*
### Downloads
The world now renders natively at your display's aspect ratio instead of
stretched 4:3: geometry is undistorted (wider view, same vertical FOV), the
in-level HUD keeps its shape and anchors to the screen edges, and the 4:3
front-end menus and ending-credits sequence are pillarboxed. Turn it off
with the **`Native widescreen`** toggle (F10 → Display) to restore the old
stretched frame. (D334/D335)
### A true, stable 60 fps
v0.3.0 could settle at 30 fps game-wide on some setups. The cause was a
pointer-width bug in the game's frame scheduler: a "present every frame"
flag was read back at a hardcoded byte offset that the 32→64-bit transition
silently moved, so the render path ran on the audio client's every-other-
frame cadence. Fixed at the root; the game now holds a rock-stable 60 fps
(measured with the in-game FPS readout) on both platforms. (D248)
### A complete aim system for mouse *and* controller
- **`Aim style`** — **N64** (default): the crosshair deflects and the
camera follows once it reaches the screen edge. **Centred (PC)**: the
crosshair is fixed at screen center and your mouse — or your right stick —
moves the camera directly. In v0.3.0 the centred mode only worked with a
mouse; it now works with a controller too, using the N64 original's own
stick-response curve so full stick matches the original's top turn rate.
(D332/D333/D337/D404)
- **`Aim range`** (shown while Centred is selected) — **PC** (default,
full-screen feel) or **N64** (the original 65% stick limits). (D338)
- **Reticule jitter in aim mode is gone** — mouse aim now runs on the
Perfect Dark PC port's aim model. (D332)
- Per-device sensitivity: `Mouse horizontal/vertical sensitivity`, and for
controllers `X axis / Y axis look sensitivity (controller)` plus
`Look smoothing (controller)`.
### Rebuilt options (F10 overlay + front-end PC Options)
- Both options screens are reorganized into functional sections — **Input,
Gameplay, HUD, Graphics, Audio, Display** — each with its own *Reset to
defaults*; per-profile settings scoping; and the old "save file" wording
is now **profile**. (D353–D356)
- The **F10 in-game overlay** was refreshed GE-style: category headers,
full controller support (value-adjust with hold-to-repeat), fixed pointer/
keyboard navigation, and new rows: **Show FPS**, **Skip intro
(experimental)**, pad
**Invert look / Southpaw / Deadzone / Trigger threshold**. (D237/D345–
D347/D360–D370)
- The front end gained a **PC Options screen** beside the file-select bar,
and save-file **Copy/Erase** moved into the bottom bar. Long sections
(Input) page within the screen -- a "Page 1/2" marker plus a bottom hint
on every page -- so every row stays on screen and stays reachable; the
mouse wheel and W/S also page the list, and the selection clamps at page
edges. (D343/D406/D407)
- Watch-only settings — **Auto-aim**, **Look ahead** and the rest — are now
editable from the PC options and **persist correctly** (this closes the
"auto-aim won't stay on" report, issue #103). (D330/D350/D352/D354)
- **`HUD scale`** (75–150%, default 100%): scales the ammo counter, pickup
text and subtitle bars for smaller or larger displays. (D226)
### Crosshair customization *(defaults leave the N64 crosshair untouched)*
**Show crosshair** (on/off), **Crosshair colour** (the original red,
or green / blue / yellow / cyan / magenta / white, or custom RGB), **Crosshair size**
(default 100% of the original drawing), and **Crosshair style** (Original /
Thin cross). (D373/D379/D381/D382)
### In-game key rebinding + new default layout
A two-group key editor (**Movement**, **Actions**, each with its own *Reset
to defaults*) lives in both options screens. The new default **Actions**
layout is the GEPD-style preset (e.g. Action/next-weapon on the keys you'd
expect from a modern layout); existing INI bindings are honored and older
ones migrate automatically. (D371/D374/D383/D385)
### Controller improvements
- **Sane pad defaults**: **A = use/interact, X = reload, Y = weapon cycle**
(v0.3.0 mapped two buttons to crouch and had no in-game use button). (D393)
- Crouch uses the engine's real crouch input, with a **`Crouch mode`**
toggle (hold / latched). (D375)
- Xbox-variant controller parity, **Southpaw**, and per-stick
**deadzone** + **trigger threshold** rows. (D237/D394)
- **The N64 Rumble Pak now drives real gamepad haptics** (Steam Deck
included), scaled by a global **`Vibration`** slider (default 50%). (D401)
- The F10 overlay is fully gamepad-driven. (D347/D395/D396)
### `All unlocked` ships as a documented EXPERIMENTAL feature
F10 → Gameplay → **`All unlocked (EXPERIMENTAL)`** opens every mission, 007
mode and the full cheat menu. On a fresh install it now yields the game's
own fresh-slot layout instead of corrupting state (D281) — but enabling it
can still bake synthetic cheat/completion data into your save that switching
it off does not undo: **complete a level normally first, and back up
`data/ge007.eep` before enabling it.** (D387)
### Steam Deck / Linux
The first clean SteamOS/glibc build, plus a first-launch preset for decks
without a config (native 1280×800, 2× MSAA, vsync, 250% draw/LOD
distances). (D402/D283)
---
## Bug fixes
### Fidelity (verified against the N64 original / era-correct references)
- **Water** on the `IsWater` levels (Dam, Frigate, Surface 2, …) matches
the original: the moving seam between two patterns and the pattern
"resetting" as you moved are gone. (D245)
- The **particle "rainbow" effect** (intermittently recoloured sparks and
explosion residue) no longer occurs. (D252)
- **Frigate: "every polygon breaks except the gun"** after turning — and
the rare "Bond briefly out of place" quirk — were the same never-written
fields read by the head-bob animation; both are fixed. (D336/D311)
- Surface 1/2: the tree backdrop that rendered as a solid wall of texture
now renders as proper camera-facing tree cards. (D236)
- Ejected **shell casings** render again. (D331)
- The intro **gun-barrel blood drip** draws correctly. (D341)
- The extra erroneous **long muzzle flash** on some guns is gone. (D303)
- Occasional **z-fighting** fixed; **security-camera props** no longer face
backwards; the front-end **Nintendo logo and copyright page** render
correctly. (D308/D307/D403)
- The **train-intro soldier pose** is correct. (D392)
- **Tanks** (Runway, Streets) can be boarded and exited again — the new
use/reload button split had removed the B-button tap the engine's tank
handlers use; B is presented again in tank states, and use keeps its
no-reload-fallback semantics everywhere else. A short input lockout during
the sit-down animation means a double-tap can't cancel boarding, and in-tank
mouse aim has its own **`Tank aim speed`** setting (`Input.TankAimScale`,
default 100) so the turret matches your on-foot feel. (D407)
- File-select background / gun-barrel comb rendering fixed. (D397)
- Stale-texture artifacts after level transitions fixed. (D235)
- Distant-geometry dropout on the biggest open levels (Streets, Egyptian)
substantially improved at default settings. (D249)
- The **Facility execution-scene watchdog** from v0.3.0 stays enabled as a
safeguard; the animation-pinning that motivated it is root-caused (a tick
granularity divergence) and fixed, so the scene plays out as authored.
(D329/D318)
### Front end & UI
- Randomly **flickering F10 menu items** are gone. (D314)
- The watch's **controller-page graphic** renders again. (D290)
- File-select **folders and Bond photos** no longer vanish after backing
out of a file. (D342)
- File-select bottom row (SELECT FILE / Copy / Erase / PC Options) aligned.
(D398–D400)
### Audio
- **Gunshot SFX** cadence matches the N64 original's rate, and the
intermittent silence of the PP7/AK47 under sustained fire near a looping
sound is gone. (D240/D241)
- The silenced **PPK "slap"** and the broader real-time mixer corruption
behind it are fixed. (D202)
- Windows audio now prefers the **DirectSound** backend over WASAPI (lower,
more robust latency). (D322)
- In-stage music **re-synchronizes** correctly at stage transitions and
level entry. (D355)
- Long-session audio degradation (issue #87): pool hardening shipped, plus
an optional `GE_D322=1` telemetry probe if it ever recurs.
### Stability & renderer
- **Orderly game exit**: quitting no longer lands inside the graphics
driver (a class of Windows driver bugchecks seen during testing). (D344)
- A **non-self-recovering Windows freeze** after the window sat idle and
lost focus is fixed (mouse-mode changes now applied on the window
thread). (D287)
- **MSAA above your driver's maximum no longer black-screens** (the request
is clamped to `GL_MAX_SAMPLES`), and two framebuffer-state bugs in the
render/resolve paths were fixed — community contribution, PR #106,
security-reviewed. (D405)
- Port-wide code audit: missing returns in front-end menus, missing
prototypes, undefined-behaviour shifts.
---
## Options reference
All settings available in v0.4.0, in both options screens (front-end
**PC Options** and the in-game **F10 overlay**). Defaults in parentheses.
| Setting | Section | Default |
|---|---|---|
| `Native widescreen` | Display | On |
| `Frame rate cap` | Display | 60 (30 / 60) |
| `Show FPS` | Display | Off |
| `MSAA` | Display | 2× (1/2/4/8, clamped to driver max) |
| `Aim style` | Input | N64 (N64 / Centred (PC)) |
| `Aim range` | Input | PC (shown while Centred) |
| `Mouse horizontal/vertical sensitivity` | Input | calibrated midpoint |
| `Invert look (mouse)` / `Invert look (controller)` / `Southpaw` | Input | Off |
| `X / Y axis look sensitivity (controller)` | Input | 100% (native) |
| `Look smoothing (controller)` | Input | 0 |
| `Tank aim speed` | Input | 100% (`Input.TankAimScale`) |
| `Deadzone (left/right stick)` | Input | 70% |
| `Trigger threshold` | Input | 23% |
| `Vibration` | Input | 50% |
| `Crouch mode` | Input | Hold |
| `Bindings…` (Movement / Actions editors) | Input | GEPD-style preset |
| `Auto-aim` / `Look ahead` | Gameplay | Off (watch-backed) |
| `Skip intro` / `No hit flash` | Gameplay | Off |
| `All unlocked (EXPERIMENTAL)` | Gameplay | Off |
| `HUD scale` | HUD | 100% (75–150%) |
| `Show crosshair` / `Crosshair colour` / `Crosshair size` / `Crosshair style` | HUD | N64-intact (size 100%) |
---
## Things to know
- **NTSC-U (US) ROMs only.** PAL and JP ROMs are not supported in this
version; asset repair for those regions is on the post-release roadmap.
(D258)
- **The default Actions key layout is new** (GEPD-style). If you preferred
the v0.3.0 defaults, rebind them in *Input → Bindings…* or reset the
group. Existing INI bindings are auto-migrated and still honored.
- **Back up `data/ge007.eep` before using `All unlocked`** (see above).
- Steam Deck: the first launch in Game Mode applies the Deck preset only if
no `ge007.ini` exists yet; F10 changes always win afterwards.
## Known issues
- On Facility, if gas leaks during Ourumov's monologue he can pause for up
to ~10 s before resuming the scripted shootout — a latent race that exists
in the N64 original too (where it softlocks permanently); the port detects
and auto-recovers it. (D318)
- With *Native widescreen* on, the F10 options overlay stretches with the
window instead of pillarboxing like the front-end menus (legible;
cosmetic). (D335b)
- The front-end Rareware logo shows a subtle texture-filtering artifact
(the Nintendo logo and legal page are clean). Cosmetic only. (D75)
- **`All unlocked` is experimental** (see above); the fresh-install
corruption case is fixed, but saves made while it is on are not
guaranteed recoverable by switching it off. (D387)
- **`Skip intro` is experimental — not recommended for regular use yet.**
With it on, a failed or aborted mission skips the post-mission failure
report screen and returns straight to the menus (the mission still records
correctly). An older audio quirk on some saves is also still being chased.
(D408/D216)
## Roadmap since v0.3.0 (research, not yet features)
- ROM-level **mod support** investigated and found viable (the game's file
table lives in the ROM); HD texture packs and the drop-in-ROM flow are
tracked post-1.0. (D325–D328)
- A measured CPU budget on low-end hardware (issue #92) reshaped the
performance investigation toward the GPU. (D339)
- Controller options wave 2 (presets, pad reassignment UI) and PAL/JP
support are planned post-v0.4.0.
---
## Downloads
| File | Platform |
|---|---|
| `goldeneye-pc-port-<version>-win64.zip` | Windows x86-64 |
| `goldeneye-pc-port-<version>-linux-x86_64.tar.gz` | Linux x86-64 (incl. Steam Deck) |
| `goldeneye-pc-port-0.4.0-win64.zip` | Windows x86-64 |
| `goldeneye-pc-port-0.4.0-linux-x86_64.tar.gz` | Linux x86-64 (incl. Steam Deck) |
Each contains the engine executable, a README, license texts, and the
`prepare-assets` tool. **No ROM, no game assets.** The Windows bundle carries
its runtime DLLs; the Linux bundle carries SDL2, so on both platforms nothing
needs to be installed first.
`prepare-assets` tool. **No ROM, no game assets.** The Windows bundle
carries its runtime DLLs; the Linux bundle carries SDL2, so on both
platforms nothing needs to be installed first.
### Running it
## Running it
You supply your own **GoldenEye 007 N64 ROM** (`.z64`, big-endian) that you
legally own: the **NTSC-U (US)** release is what this build supports (PAL / JP
are on the roadmap). No Python, no toolchain, nothing to install.
legally own (NTSC-U only, as noted above). No Python, no toolchain, nothing
to install.
1. Unpack the archive.
2. Make a `data/` folder next to the executable and put the ROM in it, named
`ge007.ntsc-final.z64`.
3. Run the executable **from that folder**. The first run takes a few extra
seconds: it detects your ROM, generates the two derived asset folders
(`data/pcmodels-ntsc-final/`, `data/pccg-ntsc-final/`) once, and saves them
for every future run. (The generator is `prepare-assets/ge007-convert`
inside the bundle; you can also run it manually; it prints what it's doing.)
(`data/pcmodels-ntsc-final/`, `data/pccg-ntsc-final/`) once, and saves
them for every future run. (The generator is
`prepare-assets/ge007-convert` inside the bundle; you can also run it
manually; it prints what it's doing.)
**Steam Deck:** sideload the unpacked folder (USB or a file manager), do steps
2–3, then add the executable to Games → *Add Game* as a non-Steam game.
**Steam Deck:** sideload the unpacked folder (SFTP, USB or a file manager), do
steps 2–3, then add the executable to Games → *Add Game* as a non-Steam
game. The first launch in Game Mode picks up the Deck preset (native
1280×800, 2× MSAA, vsync) automatically if no config exists yet.
**In-game settings on the Deck:** the options overlay is fully gamepad-driven:
it opens with **Select**, the D-pad or left stick (up/down) moves between
options, **A** steps the selected option forward, **B** steps it back, and
**Start** (or Select again) closes. No keyboard needed.
**In-game settings on the Deck:** the options overlay is fully
gamepad-driven: it opens with **Select**, the D-pad or left stick (up/down)
moves between options, **A** steps the selected option forward, **B** steps
it back, and **Start** (or Select again) closes. No keyboard needed.
Full steps are in the bundled `README.md`.
### Verify the download
## Verify the download
```
sha256sum -c goldeneye-pc-port-<version>-win64.zip.sha256
sha256sum -c goldeneye-pc-port-<version>-linux-x86_64.tar.gz.sha256
sha256sum -c goldeneye-pc-port-0.4.0-win64.zip.sha256
sha256sum -c goldeneye-pc-port-0.4.0-linux-x86_64.tar.gz.sha256
```
### Source & docs
## Source & docs
<https://github.com/jkdansereau/goldeneye-pc-port>, built on the
[GoldenEye 007 decompilation](https://github.com/n64decomp/007), architecture
after the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark).
[GoldenEye 007 decompilation](https://github.com/n64decomp/007),
architecture after the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark).
Non-commercial fan preservation/research project; not affiliated with any
rights holder. **AI disclosure:** built through agentic AI coding (Claude
Code + a local open-weight model), directed by one person in their spare
+4 -4
View File
@@ -124,12 +124,12 @@
<div class="rep">REPORT:</div>
<div class="rows">
<div class="statusline">Status: <span class="status">RELEASED v0.2.0 &mdash; WINDOWS &middot; LINUX &middot; STEAM DECK</span></div>
<div class="statusline">Status: <span class="status">RELEASED v0.3.0 &mdash; WINDOWS &middot; LINUX &middot; STEAM DECK</span></div>
<div class="stats">
<span class="k">Statistics:</span><span>All 21 solo missions &mdash; full campaign completed in playtesting</span>
<span class="k">Statistics:</span><span>All 20 solo missions &mdash; full campaign completed in playtesting</span>
<span></span><span>~230 game-code units, compiled unmodified</span>
<span></span><span>~22k lines C/C++ &nbsp;&middot;&nbsp; ~10k Python (port layer + tools)</span>
<span></span><span>~250 bugs root-caused &nbsp;&middot;&nbsp; two coding agents, one human, part-time</span>
<span></span><span>~24k lines C/C++ &nbsp;&middot;&nbsp; ~7k Python (port layer + tools)</span>
<span></span><span>199 findings root-caused &nbsp;&middot;&nbsp; two coding agents, one human, part-time</span>
</div>
</div>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 597 KiB

After

Width:  |  Height:  |  Size: 597 KiB

+12
View File
@@ -170,6 +170,9 @@ docs/img/shots/CONTACT-SHEET.jpg
/audiodump.raw
/audiotrace.log
/mixertrace.log
# texture-dump probe artifacts (regenerable via GE_TEXDUMP)
/texdump/
/texdump_p23_old/
# Claude Code local state / agent worktrees
.claude/
@@ -188,3 +191,12 @@ build-pyinst/
# Local working junk (frame captures, timerun state, PPM dumps) - moved here
# from repo root / scratch on 2026-09-18; never commit.
/local/
build-pc-cmd.bat
# v0.4.0 modern-options wave: per-landing sweep capture dirs (capture.sh output)
tools_pc/sweep-captures/capture-*/
# Stray full CPython install that landed in repo root (2026-09-28);
# referenced by no project tooling. Never commit.
/Python/
docs/media/*.mp4
+56 -2
View File
@@ -39,6 +39,59 @@ Needs CMake + SDL2 + zlib + OpenGL, and must run from the MSYS2 MINGW64 shell
(see `build-pc.sh` header and `docs/building.md`). ROM goes in `./data/`
(not distributed); assets must be extracted from it first (`docs/building.md`).
**Windows build environment (three recurring failure modes — diagnose in this order):**
1. **`Cannot create temporary file in C:\Windows\: Permission denied`** at the
link step. The PE toolchain (ninja → cmd → gcc/ld) needs a writable
TMP/TEMP; the msys→native env conversion drops it in non-login shells
(agent harnesses; the `C:/msys64` tree here was built for `D:/M/msys64`,
so its conversion is unreliable). `build-pc.sh` now **self-heals**: it
probes a native child's TMP (via a file — piped `cmd.exe` stdout is
unreliable under msys console emulation) and, if broken, re-runs
cmake+build under PowerShell with `TMP`/`TEMP` set natively (the
native→native boundary passes env through intact; only msys→native is
broken). If you run cmake/ninja *by hand* from a broken shell: run them
from cmd/PowerShell with `C:\msys64\mingw64\bin` on PATH.
**Guard revision (2026-09-28):** the re-exec writes a self-contained
`.ps1` to a temp file and runs `powershell -File` with plain path/word
args only (the msys→native argv conversion mangles `$`-bearing
`-Command` strings; a file write is conversion-free). Inside: PATH is
built explicitly (never trust the inherited value); TMP is chosen from
writable candidates (`%LocalAppData%\Temp` first —
`[System.IO.Path]::GetTempPath()` honours the inherited *broken* TMP,
observed as `C:\Windows\`); every native step self-logs to
`build-pc/ge007-native-reexec-{diag,cmake,build}.log`; a null
`$LASTEXITCODE` ("did not execute") is a distinct failure (exit 2/3) —
in PS 5.1 a piped native command errors out non-terminating and never
sets it, so an unguarded `if ($LASTEXITCODE -ne 0)` passes vacuously.
In `.ps1`, `-DROMID=$Var` is a literal (bare tokens don't expand) — it
must be `"-DROMID=$Var"`. Launch powershell plainly — `env -i` before it
breaks the nested cmake launch (verified 2026-09-28).
**OPEN (next session):** the guard still fails *inside* `build-pc.sh`
(cmake "did not execute": the `.ps1` runs, but `$LASTEXITCODE` stays
empty) while the *identical* standalone invocation succeeds (cmake runs
and the build completes — that is how the D401 builds were made). First
test: diff the heredoc-generated `.ps1` against a hand-extracted copy
(suspect: heredoc line-ending/whitespace corruption).
**Working workaround:** extract the heredoc `.ps1` and run
`powershell -NoProfile -ExecutionPolicy Bypass -File <ps1> "C:\msys64\usr\bin" "C:\msys64\mingw64\bin" C:/msys64 <repo-win-path> build-pc <romid>`.
Re-confirmed 2026-09-28: in-script re-exec fails again (cmake configure
dies inside the ps1; re-exec logs left stale) while the identical
standalone invocation reconfigured and built cleanly (extract with
`awk "/<<'PS1'\$/{f=1;next} /^PS1\$/{f=0} f" build-pc.sh > /tmp/ps1`
— mind the leading whitespace, and `export PATH` to include mingw/bin
first, failure mode 3).
2. **`cannot open output file ge007.x86_64.exe: Permission denied`.** A
**running** `ge007.x86_64.exe` locks the output file (Windows rule; you
can't relink over a live PE). Check with `Get-Process | Where-Object {
$_.ProcessName -like '*ge007*' }` and close the game before rebuilding.
An agent must not kill the user's game process to make a link succeed.
3. **`cc1.exe: ... libmpfr-6.dll: cannot open shared object file`.** The
calling PATH lacks `C:\msys64\mingw64\bin` (the gcc driver finds cc1 via
its own directory, but the child needs the mingw DLL dir on PATH). In an
msys shell: `export PATH="/c/msys64/mingw64/bin:/c/msys64/usr/bin:$PATH"`
before building.
## Verification ritual (after any build-affecting change)
1. **Undefined symbols.** Every symbol referenced by the compiled set (see `CMakeLists.txt`: `SRC_GAME`, `SRC_ENGINE`, `SRC_LIBAUDIO`, `SRC_LIBULTRARE_AUDIO`, `SRC_LIBULTRARE_DATA`, `SRC_GU`, `SRC_PORT*`) must be defined exactly once in the compiled set or in `port/`. Symbols that live in EXCLUDED files (`libultra/io/*`, `libultrare/io/*` except `vitbl.c`, `libultra/os/*`, `libultrare/os/*`, `sched.c`, `rmon.c`, `vi.c`, `src/*.s`) must be provided by `port/src/libultra.c`, `gesched.c`, `n64stubs.c`, `random.c`, or `ucode.c`.
@@ -51,8 +104,9 @@ Run `/linkcheck` for this sweep. Record new findings in `docs/dev/findings.md`
- **Phase 0–1.5:** done. Build system, boot chain, OS-shim layer, fast3d
integration, first frames, full intro rendering.
- **Phase 2 (rendering):** in progress. All 21 solo levels load + render +
survive an unattended window; front end (menu → mission select → briefing →
- **Phase 2 (rendering):** in progress. All 20 solo missions (plus the
ending-credits sequence, `-level_54`) load + render + survive an
unattended window; front end (menu → mission select → briefing →
start) is functional; file-backed EEPROM saves work. Cosmetic defects are
parked in `docs/dev/GRAPHICS-BACKLOG.md`.
- **Phase 3 (audio + input):** input layer done (`port/src/input.c`); polish
+153 -105
View File
@@ -1,7 +1,12 @@
# GoldenEye 007 PC Port
[![CI](https://github.com/jkdansereau/goldeneye-pc-port/actions/workflows/ci.yml/badge.svg)](https://github.com/jkdansereau/goldeneye-pc-port/actions/workflows/ci.yml)
![license](https://img.shields.io/badge/license-MIT-green)
![license](https://img.shields.io/badge/license-MIT-ffb454)
<p align="center"><em>GoldenEye 007 (Nintendo 64, 1997) on the PC —
decompiled, ported, and playable at 60 fps.</em></p>
[Download](#download) · [News](#news) · [Status](#status) · [Roadmap](#roadmap) · [Building](#building) · [Docs](#documentation) · [Legal](#legal)
A native PC port of _GoldenEye 007_ (Rare, 1997, Nintendo 64), compiled from
the [GoldenEye 007 decompilation](https://github.com/n64decomp/007): the
@@ -12,25 +17,24 @@ in a dedicated `port/` layer, following the architecture of the
[Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark), the same
Rare "Indy" engine family, one hardware generation apart.
**v0.3.0** is out for Windows and Linux (including Steam Deck, where the
Linux bundle sideloads as-is) and runs the full campaign at a steady 60 fps
with known rough edges ([Status](#status)). Free to download, build on and
modify (you bring the ROM).
**v0.4.0** (released 2026-09-28) is out for Windows and Linux (including
Steam Deck, where the Linux bundle sideloads as-is). It is the most complete
release to date: the full campaign runs at a steady 60 fps with the known
issues now few and mostly cosmetic ([Status](#status)). Free to download,
build on and modify (you bring the ROM).
**This is a pre-1.0 release, not a finished product.** v1.0 is the target
for a polished, feature-complete build; until then, expect rough edges,
missing features, and breaking changes between versions. See
[Status](#status) for what works today and [Roadmap](#roadmap) for where
this is headed.
for a polished, feature-complete build; until then, expect missing features
and the occasional breaking change between versions. See [Status](#status)
for what works today and [Roadmap](#roadmap) for where this is headed.
**AI disclosure:** development here was agentic - Claude Pro plus a local
open-weight model on a single RTX 5090, as of August–September 2026. This
project is as much a study of *that process* as it is a port: whether
current LLMs can carry a codebase like this, and what actually goes wrong
along the way. Judge the result for yourself.
I'm one person doing this in my spare time, not a team. See
[Background](#background) for the full setup, timeline, and an honest
account of what worked and what didn't.
along the way. Judge the result for yourself. I'm one person doing this in my
spare time, not a team. See [Background](#background) for the full setup,
timeline, and an honest account of what worked and what didn't.
> [!IMPORTANT]
> **You must supply your own GoldenEye 007 ROM.** This repository contains no
@@ -39,11 +43,31 @@ account of what worked and what didn't.
<p align="center">
<img src="docs/media/goldeneye-gh-preview.gif" width="64%"
alt="~12 s gameplay montage from live play sessions">
<br><em>All in-engine, running in the port — a ~12&nbsp;s gameplay montage
from live v0.3.0 play sessions.</em>
alt="~24 s gameplay montage from live play sessions">
<br><em>All in-engine, running in the port — a ~24&nbsp;s gameplay montage
from live v0.4.0 play sessions, opening on the Runway tank ·
<a href="docs/index.md">12 level stills in the project index</a></em>
</p>
---
## News
- **2026-09-28** — **v0.4.0**: native widescreen, a complete aim system for
mouse and controller, in-game key rebinding, crosshair customization,
rumble-pak haptics, a rebuilt options overlay, and a broad fidelity-fix
pass (water, particles, billboard trees, front-end logo, gunshot SFX, a
true stable 60 fps). [Release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.4.0) ·
[downloads](#download).
- **2026-09-20** — **v0.3.0**: the first release with the complete campaign
playable end to end at 60 fps on Windows, Linux, and Steam Deck.
[Release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.3.0).
- **2026-09-04 → 2026-09-16** — **v0.1.0 – v0.2.2**: the alpha and beta
cycle — build chain, software RSP, first rendered frames, front end, and
per-level stabilization across the campaign.
---
## Download
| Platform | Bundle | Notes |
@@ -53,8 +77,8 @@ account of what worked and what didn't.
Both bundles contain **no ROM and no game assets**: you supply your own
(see [Requirements](#requirements)), which keeps the release legal to
distribute. Earlier builds: v0.2.2, v0.2.1, v0.2.0, and v0.1.0 alpha, same
page. You can also build it yourself; see [Building](#building).
distribute. Earlier builds: v0.3.0, v0.2.2, v0.2.1, v0.2.0, and v0.1.0
alpha, same page. You can also build it yourself; see [Building](#building).
### Quick start
@@ -67,16 +91,22 @@ included or distributed. Then:
2. Make a `data/` folder next to the executable and drop the ROM in as `ge007.ntsc-final.z64`.
3. Launch the executable from that folder. The first run takes a few extra seconds: it detects the ROM and generates the derived asset folders once (no Python or other tooling needed).
Read the [Status](#status) caveats first: v0.3.0 has known rough edges,
listed plainly there.
Read the [Status](#status) section first: the small number of known issues
in this release are listed plainly there.
---
## Status
**v0.3.0 - playable, with known rough edges.** The full single-player
campaign is completable end to end (all 21 missions, Agent difficulty,
playtested), at a steady 60 fps; all 21 solo missions load, render and run
crash-free, verified on Windows, Linux and real Steam Deck hardware. Feedback
is very welcome.
**v0.4.0 - fully playable, with a small set of known caveats.** The full
single-player campaign is completable end to end (all 20 missions, Agent
difficulty, playtested), at a steady 60 fps; all 20 solo missions — plus
the end-of-campaign credits sequence — load, render and run crash-free,
verified on Windows, Linux and real Steam Deck hardware. The most common
defect classes from earlier releases — particle colour drift, water seams,
z-fighting, billboard trees, muzzle flashes, the front-end Nintendo logo,
gunshot SFX — are fixed in this version; what remains is a short list,
below. Feedback is very welcome.
What a release actually installs (no networking, no telemetry, no ROM or
game assets shipped) and how faithfully the port tracks the original N64
@@ -84,64 +114,46 @@ game's logic: [Security & fidelity status](docs/security-and-fidelity-status.md)
**Working:** boot sequence and front end (menu → mission select → briefing →
start), front-end menu navigation on the left stick to match the F10 overlay
(D282); all 21 solo missions load, render and are crash-free (full campaign
playtested end to end at Agent difficulty); steady 60 fps
(software RSP off the presentation critical path); full audio: in-level music and SFX; keyboard + mouse (click-to-lock, proportional aim mode, a single
simplified sensitivity control) and a modern dual-stick controller layout;
automatic widescreen FOV scaling; Bond is fixed in cutscenes (no more
(D282); all 20 solo missions load, render and are crash-free (full campaign
playtested end to end at Agent difficulty, including the ending sequence); steady 60 fps
(software RSP off the presentation critical path); full audio: in-level music and SFX; keyboard + mouse (click-to-lock, an aim style of your
choice — N64 or centred FPS-style — with per-device sensitivity) and a modern
dual-stick controller layout (use/reload/weapon-cycle on A/X/Y, rumble-pak
vibration on supported pads); native widescreen at any aspect ratio
(undistorted world, HUD anchored to the screen edges, 4:3 menus pillarboxed);
in-game key rebinding (keyboard/mouse) with a GEPD-style default layout;
crosshair customization (opt-in); Bond is fixed in cutscenes (no more
floating or spin-glitching) and his third-person model positioning generally
is right the large majority of the time now, at most a small drift when off; file-backed saves; faithful N64 progression
by default (F10 → *All unlocked* opens every level, 007 mode and the full
cheat menu); F10 in-game options overlay (resolution, frame cap, MSAA,
filtering, FOV, sensitivity); Windows and Linux, including Steam Deck.
cheat menu); F10 in-game options overlay (video, input, gameplay, HUD,
graphics, audio; frame cap, MSAA, filtering, FOV, sensitivity, key rebinding,
crosshair, vibration); Windows and
Linux, including Steam Deck.
**Known issues:**
- Particle colours: a real fix landed (a corrupted-vertex-buffer bug), but
the rainbow effect can still occur intermittently on either platform, not
every time or on every level — a second cause hasn't been found yet
(D252).
- Some muzzle flashes draw an extra, erroneous long flash straight up from
the gun (seen on the M16, among others), overlaid on the normal, correctly
drawn flash. Cosmetic only (D303).
- Water levels show a moving seam between two water patterns (D245).
- Occasional z-fighting on some levels' geometry (D308) — a Dam intro/
truck-wheel instance showing odd transparent-looking areas may be related
(D306). Minor and cosmetic.
- In-level security camera props (not the player's own view) can
occasionally end up facing backwards on some levels, seen on Bunker
(D307).
- Rarely, Bond's model can briefly render out of place after an animation
transition — purely visual: no effect on gameplay or saves, and it clears
on its own or by re-entering the level (D311).
- On Facility, if gas leaks during Ourumov's monologue he can pause for up to
~10 s before resuming the scripted shootout — a latent race that exists in
the N64 original too (where it softlocks permanently); the port detects and
auto-recovers it (D318).
- Some front-end 3D models are off: the spinning Nintendo logo renders as two
white blobs and the Rareware logo's texture filtering looks wrong (D75).
- Surface 1's 2D billboard trees render as a solid wall of tree texture
instead of discrete sprites (D236). Under active investigation.
- **Widescreen is stretched, not native**: on non-4:3 windows the whole
frame (world and HUD) is stretched horizontally to fill your display —
about 33% wider than original at 16:9, like a 4:3 video in a TV's "stretch"
mode. Automatic FOV scaling keeps the framing comfortable and gameplay is
completely unaffected (all game logic runs in world space), but shapes are
subtly wider than on the N64 — most visible on round objects (barrels, the
radar). A distortion-free native widescreen render is on the roadmap.
- Distant geometry can drop out on the biggest open levels (Streets,
Egyptian) at default FOV — a culling/LOD issue that sometimes
self-corrects as you keep moving (D249).
- Gunshot SFX can sound off during sustained/rapid fire: cadence can drift
from the N64 original's rate, and PP7/AK47 fire can occasionally go silent
under heavy automatic fire near another looping sound (D240/D241).
- **`All unlocked` is highly experimental — don't enable it until you have
at least one save written** (complete a level normally first, e.g. Dam on
Agent). Enabling it on a brand-new install with no prior save can still
cause silent audio and odd right-mouse-aim behavior (D257/D259/D281).
- With native widescreen on, the F10 options overlay stretches with the
window instead of pillarboxing like the front-end menus (legible; cosmetic;
the F10 *Native widescreen* toggle restores the old stretched frame
throughout). The world/HUD widescreen rendering itself is correct (D335b).
- The Rareware front-end logo shows a subtle texture-filtering artifact
(Nintendo logo and legal page are clean). Cosmetic only (D75).
- This release ships NTSC (US) assets; PAL/JP ROMs are not supported in this
version (D258).
- **`All unlocked` is experimental: back up `data/ge007.eep` before enabling
it.** Any save while ON, even a profile-settings change, can permanently
write artificial cheat unlocks and completion times into the EEPROM;
switching OFF does not undo them (D387). Do not use it on a save whose
original progression you need to preserve.
- Assorted further cosmetic defects are tracked in
[`docs/dev/GRAPHICS-BACKLOG.md`](docs/dev/GRAPHICS-BACKLOG.md).
- No macOS or ARM support; no controller rebinding UI.
- No macOS or ARM support. Keyboard/mouse rebinding shipped in v0.4.0;
controller-button rebinding is not supported yet.
Root causes and fix status for every item: the [release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases)
and the finding log in [`docs/dev/findings.md`](docs/dev/findings.md).
@@ -150,23 +162,23 @@ and the finding log in [`docs/dev/findings.md`](docs/dev/findings.md).
The Linux bundle is the Deck build. SFTP it over from your PC, or download
it straight from the [releases page](https://github.com/jkdansereau/goldeneye-pc-port/releases) on the Deck itself:
unzip, drop your ROM in `data/`, launch it once (the first run generates the
extract, drop your ROM in `data/`, launch it once (the first run generates the
derived assets), and add the executable as a non-Steam game. SDL2 is bundled, so no dependencies need
installing. On SteamOS the first launch seeds `ge007.ini` with Deck-friendly
defaults: native 1280×800 fullscreen, VSync, MSAA 4, and 150% draw/LOD
distance (the authored N64 fade distances read short on the close-up panel);
everything is changeable in the options overlay and persists afterwards.
**Do that first launch in Game Mode, not Desktop Mode** — an ini created by
an earlier Desktop Mode launch (e.g. while testing before adding it as a
Steam shortcut) permanently skips the Deck preset, since any existing ini
always wins over it (D283). If your resolution isn't 1280×800 on first
Game Mode boot, just set it manually: F10 → *Resolution*. The renderer is
CPU-bound (software RSP); expect original N64-era
performance at 60 fps rather than more. This release was playtested on real
Deck hardware; the v0.1.0-era Facility crash (D203) did not recur: its root
cause was identified and fixed (D253), and a separate intermittent SIGSEGV
in heavy firefights/terminal destruction (D255) and an audio-thread crash
(D305) are both fixed and live-verified on real hardware as of this release.
defaults: native 1280×800 fullscreen, VSync, 2× MSAA, and 250% draw/LOD
distance (the N64-authored fade distances read short on the close-up 7"
panel); everything is changeable in the options overlay and persists
afterwards. **Do that first launch in Game Mode, not Desktop Mode** — an ini
created by an earlier Desktop Mode launch (e.g. while testing before adding
it as a Steam shortcut) permanently skips the Deck preset, since any
existing ini always wins over it (D283). If your resolution isn't 1280×800
on first Game Mode boot, just set it manually: F10 → *Resolution*. The
renderer is CPU-bound (software RSP); expect original N64-era performance at
60 fps rather than more. This release was playtested on real Deck hardware;
the v0.1.0-era Facility crash (D203) did not recur: its root cause was
identified and fixed (D253), and a separate intermittent SIGSEGV in heavy
firefights/terminal destruction (D255) and an audio-thread crash (D305) are
both fixed and live-verified on real hardware as of this release.
**In-game settings on the Deck.** The options overlay is fully gamepad-driven:
it opens with **Select**, the D-pad or left stick (up/down) moves between
@@ -175,6 +187,8 @@ options, **A** steps the selected option forward, **B** steps it back, and
filtering cycle, sliders step in increments. With a keyboard attached the same
overlay is `F10` + arrows/Enter.
---
## Roadmap
No fixed timeline or committed feature list — this is spare-time work — but
@@ -183,10 +197,8 @@ directionally, on the way to v1.0:
- Working through the [known issues](#status) above and the fuller list in
[`docs/dev/findings.md`](docs/dev/findings.md).
- **PAL and JP ROM support** ([issue #85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)); NTSC-U is the only supported region today.
- **Native widescreen** — a distortion-free render at your display's aspect
(correct-aspect projection and a wide-frame HUD), replacing today's
stretched 4:3 frame + FOV compensation.
- **Controller rebinding UI**, and macOS/ARM builds.
- **Controller (pad) button rebinding UI** (keyboard/mouse rebinding shipped
in v0.4.0), and macOS/ARM builds.
- **LAN multiplayer**: reviving GoldenEye's original split-screen/deathmatch
netplay across multiple PCs on a local network. Genuinely under
consideration, but early and not started; no ETA.
@@ -197,11 +209,14 @@ Not currently planned: new game modes GE never shipped (e.g. co-op), online (non
multiplayer, ray tracing. If any of these matter to you, open an issue —
it helps prioritize.
---
## Beyond playing
- **Tweak it**: `ge007.ini` and the F10 in-game overlay expose resolution,
frame cap, MSAA, texture filtering, FOV/draw distance and mouse feel;
launch with `-fresh` for a clean-slate run.
frame cap, MSAA, texture filtering, FOV/draw distance, mouse feel, key
rebinding, crosshair and vibration; launch with `-fresh` for a clean-slate
run.
- **Read it**: [`docs/internals.md`](docs/internals.md) maps the
architecture and the software RSP; [`docs/porting-notes.md`](docs/porting-notes.md)
is the catalogue of N64→PC bug classes hit along the way. Game logic in
@@ -212,6 +227,8 @@ it helps prioritize.
the ground rules for getting changes in, and [`docs/dev/`](docs/dev/) is
the raw engineering record behind every fix.
---
## Background
The port was built by two coding agents, a local open-weight model
@@ -221,7 +238,8 @@ asset pipeline, first frames), and **Claude Code** (Sonnet 5, Opus 5 for the
hardest bugs) joining for the collaborative phase (the 21-level sweep, the
ABI finding catalog, SDL input, front end), handing work back and forth
through shared written notes, directed by one person part-time. In short:
~3.5 weeks, ~430 commits, 200+ bugs logged and tracked to resolution.
43 days (16 Aug – 28 Sep), 1005 commits, 286 findings root-caused and logged
(`D1`–`D408`).
The full write-up (timeline, handoff mechanism, effort breakdown, an honest
"what worked / what didn't"): [`docs/dev/agentic-development.md`](docs/dev/agentic-development.md).
@@ -229,6 +247,8 @@ The workflow itself: [`docs/dev-process.md`](docs/dev-process.md). To cite the
project or its findings: [`CITATION.cff`](CITATION.cff) (GitHub's "Cite this
repository" menu).
---
## How this differs from the other GoldenEye PC projects
This is a native port of the original 1997 Nintendo 64 game, built from its
@@ -244,13 +264,15 @@ code with this one.
| **How** | Decompilation-based source port: human-reconstructed C, compiled for the host; game logic runs as written | Static binary recompilation: the shipped machine code is auto-translated to C; no source-level understanding |
| **Lineage** | [GoldenEye 007 decompilation](https://github.com/n64decomp/007) + [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark) engine family | Xbox 360 "…Recompiled" static-recompilation family |
| **Renderer** | Software RSP → OpenGL | Hardware (Vulkan) |
| **Status** | v0.3.0 public release; full campaign playable at 60 fps (see [Status](#status)) | Playable full game |
| **Status** | v0.4.0 public release; full campaign playable at 60 fps (see [Status](#status)) | Playable full game |
| **Why it exists** | To run the *original* N64 game from source, and as a [case study in AI-agent collaboration](#background) on a hard low-level codebase | To get a playable PC release of the remaster |
They answer a different question: how to get the *remaster* onto PC by
machine translation. This project answers how to get the *original 1997
game* onto PC, running from its reconstructed source.
---
## Requirements
You need a GoldenEye 007 (Nintendo 64) ROM that you legally own, in
@@ -270,6 +292,8 @@ The port also relies on the decompilation's asset-extraction step, which pulls
the level, model, texture and music data out of your ROM at build time. That
step, too, requires your ROM and is part of [Building](#building).
---
## Building
Prerequisites: CMake >= 3.16, a C/C++ toolchain, SDL2, zlib, OpenGL, Python 3,
@@ -309,6 +333,8 @@ cd goldeneye-pc-port
The executable is written to `build-pc/ge007.x86_64` (on Windows,
`build-pc/ge007.x86_64.exe`).
---
## Running
1. Create a `data/` directory in the repo root.
@@ -327,21 +353,33 @@ lives in `ge007.eep`. Launch with `-fresh` to wipe both before starting
|---------------------|--------------------------|---------------|
| Move / strafe | `W` `A` `S` `D` / arrows | Left stick (or D-pad) |
| Aim / look | Mouse | Right stick |
| Fire (Z) | Left mouse / `LCtrl` | Right trigger |
| Aim mode (R) | Right mouse / `LShift` | Left trigger |
| Use / accept (A) | `Space` / `E` / `X` | A / X |
| Reload / cancel (B) | `R` / `F` | B / Y |
| Next weapon | Mouse wheel up | RB |
| Previous weapon | Mouse wheel down | LB |
| Fire | Left mouse | Right trigger |
| Aim mode | Right mouse / `LShift` | Left trigger / LB |
| Use / interact | `E` | A |
| Reload | `R` | X |
| Crouch | `LCtrl` | Left or right stick click |
| Cycle owned gadgets | Watch inventory | B |
| Next weapon | Mouse wheel down / `Q` | Y |
| Previous weapon | Mouse wheel up | — |
| Start | `Enter` / `Tab` | Start |
| Options overlay | `F10` | Select (D-pad/stick + A/B navigate, Start closes) |
| Options overlay | `F10` | Select (A toggles/steps; B backs; D-pad/stick adjusts sliders; Start closes) |
The controller layout follows the modern dual-stick scheme used by the
console re-releases (left stick move, right stick look, triggers fire/aim,
shoulders cycle weapons).
The default layout is the GEPD-style preset on the keyboard and the standard
dual-stick scheme on controllers (left stick move, right stick look,
triggers fire/aim, A use, X reload, B gadget cycle, Y weapon cycle, stick
click crouch). The watch gadget cycle selects the next owned gadget in the
N64 inventory. The controller layout matches Rare's Xbox 1.1 (Jinx) button
roles (user-tested on a physical pad; D394), and the remaining Xbox schemes
(1.2 Christmas, 1.3 Frost, 1.4 Elektra) are planned as selectable presets
and not yet available. Keyboard → Bindings edits **keyboard/mouse only**; a
controller can navigate those pages and use B to go back, but
controller-button rebinding is not supported yet.
Mouse sensitivity, Y-inversion and the aim/turn split are tunable in the
`[Input]` section of `ge007.ini`.
F10 overlay (*Keyboard → Sensitivity*) or the `[Input]` section of
`ge007.ini`.
---
## How it works
@@ -374,6 +412,8 @@ tools_pc/ PC-port helper + analysis scripts
docs/ see below
```
---
## Documentation
Key docs are also published as a site:
@@ -389,6 +429,8 @@ Key docs are also published as a site:
| [`docs/dev/`](docs/dev/) | The raw engineering record: the full finding log, per-level status, graphics backlog, playtest matrices, and [`docs/dev/game-behavior-reference.md`](docs/dev/game-behavior-reference.md) (a secondary-sourced playtest reference for how the retail game is meant to behave; repo-only, code is ground truth). |
| [`docs/SetupGuide.md`](docs/SetupGuide.md), [`docs/StyleGuide.md`](docs/StyleGuide.md) | Inherited from the decompilation this repo forks. |
---
## Credits
This port is a thin layer on a large amount of other people's work.
@@ -446,6 +488,8 @@ This port is a thin layer on a large amount of other people's work.
- [pi](https://pi.dev/): the local coding-agent harness.
- [Claude / Claude Code](https://claude.com/claude-code) (Anthropic).
---
## Legal
This is a non-commercial fan preservation/research project, in the same
@@ -478,3 +522,7 @@ The original work in this repository, the port layer (`port/`), the PC build
system, `tools_pc/`, and the documentation, is released under the MIT License;
see [`LICENSE`](LICENSE). Everything inherited from the upstream decompilation
is covered by [`NOTICE`](NOTICE), not by that license.
---
*Last updated 2026-09-28 — v0.4.0 · <a href="https://github.com/jkdansereau/goldeneye-pc-port">GitHub</a> · <a href="https://github.com/jkdansereau/goldeneye-pc-port/releases">Releases</a>*
+127 -1
View File
@@ -14,7 +14,7 @@
# Example (Linux):
# sudo apt install cmake libsdl2-dev zlib1g-dev libgl1-mesa-dev
# Example (macOS):
# brew install cmake sdl2 zlib
# brew install cmake libsdl2-dev
# Example (Windows/MSYS2):
# pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-SDL2 \
# mingw-w64-x86_64-zlib mingw-w64-x86_64-cmake
@@ -24,6 +24,132 @@ set -euo pipefail
ROMID="${1:-ntsc-final}"
BUILD_DIR="${BUILD_DIR:-build-pc}"
# ---------------------------------------------------------------------------
# Windows toolchain env guard (see AGENTS.md "Build" — the recurring
# "Cannot create temporary file in C:\Windows\: Permission denied" link
# failure).
#
# The PE toolchain (ninja -> cmd -> gcc/ld) creates temp files under the
# child's TMP/TEMP. A full MSYS2 MINGW64 login shell arranges that for
# native children; other shells (agent harnesses, non-login shells) do not
# -- the msys->native env conversion silently drops/breaks TMP and the
# linker falls back to C:\Windows\. Probe what a native child actually
# sees (via a file: msys console emulation makes piped cmd.exe stdout
# unreliable), and if broken, re-run the cmake+build steps under
# PowerShell. Crucially the temp vars are set INSIDE PowerShell: the broken
# boundary is msys->native, while native->native (powershell -> cmake ->
# ninja) passes env vars through intact.
# GE_PC_BUILD_VIA_NATIVE=1 prevents a re-exec loop.
# ---------------------------------------------------------------------------
if [ -n "${MSYSTEM:-}" ] && [ -z "${GE_PC_BUILD_VIA_NATIVE:-}" ]; then
_probe="$(mktemp -d "${TMPDIR:-/tmp}/gebuildprobe.XXXXXX")"
_probe_win="$(cygpath -w "${_probe}" 2>/dev/null || echo 'C:\\msys64\\tmp')"
/c/Windows/system32/cmd.exe //c "if exist \"%TMP%\" (echo ok > \"${_probe_win}/ok.txt\") else (echo bad > \"${_probe_win}/bad.txt\")" >/dev/null 2>&1 || true
if [ ! -s "${_probe}/ok.txt" ]; then
command -v powershell >/dev/null 2>&1 || {
echo "ERROR: native TMP is unusable from this shell and powershell.exe was not found for the fallback." >&2
echo "Manual fix: run cmake+ninja from cmd/PowerShell with C:\\msys64\\mingw64\\bin on PATH, or" >&2
echo "set the user env vars (setx TMP \"C:\\msys64\\tmp\"; setx TEMP \"C:\\msys64\\tmp\") and re-open the shell." >&2
rm -rf "${_probe}"; exit 1
}
echo "==> native TMP unusable from this shell; re-running cmake+build via PowerShell"
export GE_PC_BUILD_VIA_NATIVE=1
_here="$(pwd -W 2>/dev/null || cygpath -w "$(pwd)")"
# Resolve the Windows-side toolchain paths from what msys found (no
# hardcoded install dir: C:\msys64 on this box, D:\M\msys64
# elsewhere). usr/bin first: mingw gcc.exe needs msys-2.0.dll from
# there.
_cmake_root="$(dirname "$(command -v cmake)")" # .../mingw64/bin
_msys_root="$(cd "${_cmake_root}/../.." && pwd -W 2>/dev/null || cygpath -w "$(cd "${_cmake_root}/../.." && pwd)")" # msys install root (.. twice: bin -> mingw64 -> root)
_bin_w="$(cygpath -w "${_cmake_root}")"
_usrbin_w="$(cygpath -w "${_msys_root}/usr/bin")"
# The PowerShell script is written to a .ps1 FILE, not passed via
# -Command: the msys->native argv conversion mangles $-bearing
# strings (a '\$LASTEXITCODE' check arrived as an empty token and a
# failed cmake would have exited 0 silently). A file write is
# conversion-free; only plain path/word arguments cross the boundary.
_ps1="$(mktemp "${TMPDIR:-/tmp}/ge007-rerun.XXXXXX.ps1")"
cat > "${_ps1}" <<'PS1'
param([string]$UsrBin, [string]$Bin, [string]$MsysRoot, [string]$Here, [string]$BuildDir, [string]$RomId)
# Self-logging: a headless shell loses this process's console output, so
# every fact that matters goes to diag/log files in the build dir.
$diag = Join-Path $BuildDir "ge007-native-reexec-diag.log"
$cmakeLog = Join-Path $BuildDir "ge007-native-reexec-cmake.log"
$buildLog = Join-Path $BuildDir "ge007-native-reexec-build.log"
# The inherited native PATH may be mangled/dropped by the broken msys->native
# env conversion, so build it explicitly: the toolchain only needs the two
# msys bin dirs plus the usual system dirs. (usr/bin first: mingw gcc/cmake
# need msys-2.0.dll from there.)
$env:PATH = "$UsrBin;$Bin;C:\Windows\System32;C:\Windows;C:\Windows\System32\Wbem"
# Writable temp for the native toolchain. [System.IO.Path]::GetTempPath()
# CANNOT be used here: it honours the inherited (broken) TMP/TEMP env vars
# and returned C:\Windows\. Pick the first writable candidate instead.
$tmpdir = $null
foreach ($cand in @((Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Temp'), (Join-Path $MsysRoot 'tmp'))) {
try {
if (-not (Test-Path $cand)) { New-Item -ItemType Directory -Force -Path $cand -ErrorAction Stop | Out-Null }
$probe = Join-Path $cand 'ge007-tmp-write-test.tmp'
'x' | Out-File -FilePath $probe -ErrorAction Stop
Remove-Item $probe -ErrorAction SilentlyContinue
$tmpdir = $cand
break
} catch { }
}
if ($null -eq $tmpdir) { "no writable temp dir found" | Out-File $diag -Encoding ascii; Write-Host 'no writable temp dir for the native toolchain'; exit 4 }
$env:TMP = $tmpdir
$env:TEMP = $tmpdir
"start UsrBin=[$UsrBin] Bin=[$Bin] MsysRoot=[$MsysRoot] Here=[$Here] BuildDir=[$BuildDir] RomId=[$RomId] tmp=[$tmpdir]" | Out-File $diag -Encoding ascii
$exe = "$Bin\cmake.exe"
"exe=[$exe] exists=[$(Test-Path -LiteralPath $exe)]" | Out-File $diag -Append -Encoding ascii
Set-Location $Here
"cwd=[$([System.IO.Directory]::GetCurrentDirectory())]" | Out-File $diag -Append -Encoding ascii
try {
# -DROMID must be a DOUBLE-QUOTED string: a bare `-DROMID=$RomId` token
# does NOT expand the variable in PowerShell (cmake got the literal
# "$RomId" and failed with "Unknown ROMID").
$out = & $exe -S . -B $BuildDir "-DROMID=$RomId" 2>&1
"cmake ran LASTEXITCODE=[$LASTEXITCODE]" | Out-File $diag -Append -Encoding ascii
$out | Out-File -FilePath $cmakeLog -Encoding ascii
} catch {
"cmake TERMINAL ERROR: $($_.Exception.Message)" | Out-File $diag -Append -Encoding ascii
}
if ($null -eq $LASTEXITCODE) { Write-Host "cmake.exe did not execute (diag: $diag)"; exit 2 }
if ($LASTEXITCODE -ne 0) { Write-Host "cmake re-configure failed (rc=$LASTEXITCODE; log: $cmakeLog)"; exit $LASTEXITCODE }
Write-Host "cmake reconfigured (TMP=$($env:TMP))"
try {
$out = & $exe --build $BuildDir -j 2>&1
"build ran LASTEXITCODE=[$LASTEXITCODE]" | Out-File $diag -Append -Encoding ascii
$out | Out-File -FilePath $buildLog -Encoding ascii
} catch {
"build TERMINAL ERROR: $($_.Exception.Message)" | Out-File $diag -Append -Encoding ascii
}
if ($null -eq $LASTEXITCODE) { Write-Host "cmake --build did not execute (diag: $diag)"; exit 3 }
if ($LASTEXITCODE -ne 0) { Write-Host "build failed (rc=$LASTEXITCODE; log: $buildLog)"; exit $LASTEXITCODE }
"done" | Out-File $diag -Append -Encoding ascii
Write-Host "cmake + build ok (native re-exec, TMP=$($env:TMP))"
PS1
_ps1_win="$(cygpath -w "${_ps1}")"
rc=0
# NOTE: do NOT launch powershell via `env -i` (tried 2026-09-28): a
# native child started with a scrubbed msys env cannot launch
# cmake.exe (CreateProcess fails silently; LASTEXITCODE stays null).
powershell -NoProfile -ExecutionPolicy Bypass -File "${_ps1_win}" "${_usrbin_w}" "${_bin_w}" "${_msys_root}" "${_here}" "${BUILD_DIR}" "${ROMID}" || rc=$?
rm -f "${_ps1}"
if [ "${rc}" -ne 0 ]; then
# Surface the .ps1's own diagnostics (console output is lost in a
# headless shell) plus whatever the failing step wrote.
for _lg in "${BUILD_DIR}/ge007-native-reexec-diag.log" "${BUILD_DIR}/ge007-native-reexec-cmake.log" "${BUILD_DIR}/ge007-native-reexec-build.log"; do
if [ -s "${_lg}" ]; then
echo "---- ${_lg} (last 40 lines) ----" >&2
tail -40 "${_lg}" >&2
fi
done
fi
exit "${rc}"
fi
rm -rf "${_probe}"
fi
echo "==> Configuring PC port (ROMID=${ROMID})"
cmake -S . -B "${BUILD_DIR}" -DROMID="${ROMID}"
+4 -2
View File
@@ -6,8 +6,10 @@ title: GoldenEye 007 PC Port
# long version made a ~230-char title. It also renders as the masthead
# tagline on every page. Per-page meta/og descriptions come from each page's
# front matter `description:` (see index.md), not from here.
description: N64 GoldenEye 007, decompiled for PC.
theme: jekyll-theme-cayman
description: GoldenEye 007 (Nintendo 64, 1997) on the PC - decompiled, ported, and playable.
# No GitHub theme: the site uses a hand-rolled _layouts/default.html plus the
# design in _includes/head-custom.html (charcoal/off-white/amber, square and
# flat.)
show_downloads: false
# Canonical site location - used by jekyll-seo-tag (canonical URLs, og: tags)
+147 -173
View File
@@ -3,267 +3,242 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
<style>
/* ----------------------------------------------------------------------
Design pass over the stock Cayman theme.
Restrained "spy-tech" palette: near-black, slate, a single amber accent.
Loaded after the theme stylesheet, so these rules win without !important.
Hand-rolled site theme (replaces jekyll-theme-cayman, 2026-09-28).
Look: committed charcoal/off-white/amber "night desk" — flat and square,
serif display type (Libre Baskerville) over a system sans body, thin
amber rules between sections, text navigation separated by middots.
No gradients, no shadows, no rounded corners, no theme switcher.
--------------------------------------------------------------------- */
:root {
color-scheme: light dark;
--ink: #14171c;
--ink-soft: #3d444d;
--ink-mute: #626b76;
--line: #e4e6e9;
--bg: #ffffff;
--bg-soft: #f7f8fa;
--accent: #b7791f;
--accent-ink: #8a5a12;
--header-1: #0d1117;
--header-2: #1c2530;
--radius: 8px;
--measure: 44rem;
--bg: #1b1b1d; /* charcoal page */
--bg-soft: #232326; /* code / alt rows */
--bg-deep: #141416; /* code blocks, masthead */
--ink: #e8e6e3; /* off-white body text */
--ink-soft: #b5b1a9; /* secondary text */
--ink-mute: #8d897f; /* captions, footer */
--line: #3a3a3e; /* 1px borders */
--accent: #ffb454; /* amber/gold */
--accent-hi: #ffd08a; /* link hover */
--measure: 44rem;
}
@media (prefers-color-scheme: dark) {
:root {
--ink: #e7eaee;
--ink-soft: #b9c0c9;
--ink-mute: #8b939e;
--line: #2a2f37;
--bg: #0f1216;
--bg-soft: #171b21;
--accent: #e0a94a;
--accent-ink: #f0c274;
--header-1: #0a0d11;
--header-2: #161d27;
}
}
html { background: var(--bg); }
body {
font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
font-size: 16.5px;
line-height: 1.7;
color: var(--ink);
background: var(--bg);
font-size: 17px;
line-height: 1.7;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
/* ---- Header --------------------------------------------------------- */
.page-header {
position: relative;
isolation: isolate;
text-align: center;
padding: 4rem 1rem 3.75rem;
background: linear-gradient(135deg, var(--header-1), var(--header-2));
border-bottom: 1px solid rgba(224, 169, 74, 0.28);
/* ---- Masthead -------------------------------------------------------- */
.site-masthead {
background: var(--bg-deep);
border-bottom: 1px solid var(--accent);
}
.page-header::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background-image: url("{{ '/img/attract-bunker1.png' | relative_url }}");
background-size: cover;
background-position: center 38%;
opacity: 0.16;
filter: grayscale(0.35) contrast(1.05);
}
.page-header::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background: linear-gradient(180deg, rgba(13,17,23,0.35), rgba(13,17,23,0.82));
}
.project-name {
font-weight: 700;
letter-spacing: -0.02em;
font-size: 2.4rem;
margin: 0 0 0.4rem;
color: #fff;
text-shadow: 0 1px 24px rgba(0,0,0,0.5);
}
.project-tagline {
font-weight: 400;
font-size: 1.12rem;
line-height: 1.55;
color: #d3d9e0;
opacity: 1;
max-width: 42rem;
margin: 0 auto 1.6rem;
}
.page-header .btn {
display: inline-block;
padding: 0.5rem 1.1rem;
margin: 0.3rem 0.25rem 0;
font-size: 0.9rem;
font-weight: 500;
letter-spacing: 0.01em;
color: #f3f5f7;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.22);
border-radius: var(--radius);
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
.page-header .btn:hover {
background: rgba(224,169,74,0.16);
border-color: var(--accent);
text-decoration: none;
transform: translateY(-1px);
}
/* ---- Body ---------------------------------------------------------- */
.main-content {
.masthead-inner {
max-width: 52rem;
margin: 0 auto;
padding: 3rem 1.5rem 1rem;
color: var(--ink);
font-size: 17px;
padding: 2.6rem 1.5rem 1.6rem;
text-align: center;
}
.main-content p,
.main-content ul,
.main-content ol,
.main-content blockquote { max-width: var(--measure); }
.masthead-title {
display: inline-block;
font-family: "Libre Baskerville", Georgia, "Times New Roman", serif;
font-weight: 700;
font-size: 2.1rem;
line-height: 1.25;
color: var(--ink);
text-decoration: none;
letter-spacing: 0.01em;
}
.masthead-title:hover { color: var(--accent); }
.masthead-tagline {
margin-top: 0.5rem;
color: var(--ink-mute);
font-size: 0.95rem;
}
.masthead-nav {
margin-top: 1.3rem;
font-size: 0.9rem;
}
.masthead-nav a {
color: var(--accent);
text-decoration: none;
padding: 0.1rem 0.15rem;
}
.masthead-nav a:hover { color: var(--accent-hi); text-decoration: underline; }
.nav-sep { color: var(--ink-mute); padding: 0 0.35rem; }
.main-content h1,
.main-content h2,
.main-content h3,
.main-content h4 {
/* ---- Content ---------------------------------------------------------- */
.doc-content {
max-width: 52rem;
margin: 0 auto;
padding: 2.5rem 1.5rem 1rem;
}
.doc-content p,
.doc-content ul,
.doc-content ol,
.doc-content blockquote { max-width: var(--measure); }
.doc-content h1,
.doc-content h2,
.doc-content h3,
.doc-content h4 {
font-family: "Libre Baskerville", Georgia, "Times New Roman", serif;
font-weight: 700;
line-height: 1.35;
color: var(--ink);
font-weight: 600;
letter-spacing: -0.015em;
line-height: 1.3;
margin-top: 2.4rem;
margin-bottom: 0.9rem;
}
.main-content h1 { font-size: 1.9rem; }
.main-content h2 {
font-size: 1.4rem;
padding-bottom: 0.35rem;
border-bottom: 1px solid var(--line);
}
.main-content h3 { font-size: 1.15rem; }
.main-content > h1:first-child { margin-top: 0.5rem; }
.doc-content h1 { font-size: 1.75rem; }
.doc-content h2 { font-size: 1.35rem; }
.doc-content h3 { font-size: 1.1rem; }
.doc-content > h1:first-child { margin-top: 0.5rem; }
.main-content a {
color: var(--accent-ink);
.doc-content a {
color: var(--accent);
text-decoration: underline;
text-decoration-color: rgba(183,121,31,0.4);
text-decoration-color: rgba(255, 180, 84, 0.45);
text-underline-offset: 2px;
}
.main-content a:hover {
color: var(--accent);
.doc-content a:hover {
color: var(--accent-hi);
text-decoration-color: currentColor;
}
@media (prefers-color-scheme: dark) {
.main-content a { color: var(--accent); }
.doc-content hr {
border: 0;
height: 1px;
background: var(--accent);
opacity: 0.5;
margin: 2.5rem 0;
}
.main-content blockquote {
color: var(--ink-mute);
.doc-content blockquote {
color: var(--ink-soft);
border-left: 3px solid var(--accent);
padding: 0.2rem 0 0.2rem 1.1rem;
margin-left: 0;
font-style: normal;
}
.main-content hr {
height: 1px;
background: var(--line);
border: 0;
margin: 2.5rem 0;
}
/* Lead paragraph on doc pages (the italic summary under the H1) */
.main-content > p > em:only-child {
.doc-content > p > em:only-child {
font-style: normal;
color: var(--ink-soft);
font-size: 1.05rem;
}
/* ---- Code --------------------------------------------------------- */
.main-content code {
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
/* ---- Code ------------------------------------------------------------ */
.doc-content code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.86em;
background: var(--bg-soft);
border: 1px solid var(--line);
border-radius: 4px;
padding: 0.12em 0.4em;
color: var(--ink);
}
.main-content pre {
background: var(--header-1);
.doc-content pre {
background: var(--bg-deep);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 1rem 1.15rem;
max-width: var(--measure);
overflow-x: auto;
line-height: 1.55;
}
.main-content pre code {
.doc-content pre code {
background: transparent;
border: 0;
padding: 0;
color: #e6edf3;
color: var(--ink);
font-size: 0.84rem;
}
/* ---- Tables ------------------------------------------------------- */
.main-content table {
/* ---- Tables ----------------------------------------------------------- */
.doc-content table {
border-collapse: collapse;
display: table;
width: 100%;
max-width: var(--measure);
margin: 1.5rem 0;
font-size: 0.92rem;
}
.main-content table th,
.main-content table td {
.doc-content table th,
.doc-content table td {
border: 1px solid var(--line);
padding: 0.5rem 0.75rem;
text-align: left;
}
.main-content table th { background: var(--bg-soft); font-weight: 600; }
.main-content table tr { background: transparent; }
.main-content table tr:nth-child(2n) { background: var(--bg-soft); }
.doc-content table th { background: var(--bg-soft); font-weight: 600; }
.doc-content table tr:nth-child(2n) { background: #1f1f22; }
.main-content img {
border-radius: var(--radius);
/* ---- Images & gallery -------------------------------------------------- */
.doc-content img {
border: 1px solid var(--line);
border-radius: 0;
max-width: 100%;
}
/* ---- Footer ------------------------------------------------------- */
.doc-content details {
max-width: var(--measure);
border: 1px solid var(--line);
background: var(--bg-soft);
padding: 0.6rem 0.9rem;
margin: 1.5rem 0;
}
.doc-content summary { cursor: pointer; color: var(--accent); }
/* ---- Footer ------------------------------------------------------------- */
.site-footer {
max-width: 52rem;
margin: 3rem auto 0;
padding: 1.5rem 1.5rem 2.5rem;
border-top: 1px solid var(--line);
color: var(--ink-mute);
font-size: 0.86rem;
}
.site-footer a { color: var(--accent-ink); }
@media (prefers-color-scheme: dark) { .site-footer a { color: var(--accent); } }
.footer-inner { max-width: var(--measure); }
.footer-stamp {
color: var(--ink-soft);
font-size: 0.88rem;
margin: 0 0 0.8rem;
}
.footer-stamp a { color: var(--accent); text-decoration: none; }
.footer-stamp a:hover { color: var(--accent-hi); text-decoration: underline; }
.footer-legal {
color: var(--ink-mute);
font-size: 0.8rem;
line-height: 1.6;
margin: 0;
}
@media (max-width: 42rem) {
.project-name { font-size: 1.9rem; }
.page-header { padding: 3rem 1rem 2.75rem; }
.main-content { padding-top: 2rem; }
.masthead-title { font-size: 1.7rem; }
.masthead-inner { padding: 2rem 1rem 1.2rem; }
.doc-content { padding-top: 1.8rem; }
}
/* ---- Mermaid diagrams ------------------------------------------------ */
/* ---- Mermaid diagrams ---------------------------------------------------- */
/* Kramdown renders ```mermaid fences as <pre><code class="language-mermaid">.
The script below hands their text to mermaid and swaps in the SVG. Keep
the pre unstyled (no dark code background) while it waits to render. */
.main-content pre.mermaid,
.main-content pre > code.language-mermaid {
the pre unstyled (no code-block background) while it waits to render. */
.doc-content pre.mermaid,
.doc-content pre > code.language-mermaid {
background: transparent;
border: 0;
color: var(--ink);
padding: 0;
}
.main-content pre.mermaid { text-align: center; overflow-x: auto; }
.main-content pre.mermaid:not([data-processed]) { visibility: hidden; }
.doc-content pre.mermaid { text-align: center; overflow-x: auto; }
.doc-content pre.mermaid:not([data-processed]) { visibility: hidden; }
</style>
<script type="module">
@@ -271,7 +246,6 @@
// Move each ```mermaid code block's text into a <pre class="mermaid"> that
// mermaid.run() recognises.
const dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
document.querySelectorAll("pre > code.language-mermaid").forEach((code) => {
const pre = document.createElement("pre");
pre.className = "mermaid";
@@ -282,7 +256,7 @@
mermaid.initialize({
startOnLoad: false,
securityLevel: "loose", // our own docs; needed for <br/> in node labels
theme: dark ? "dark" : "neutral",
theme: "dark",
});
mermaid.run({ querySelector: "pre.mermaid" });
</script>
+48
View File
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="{{ '/img/attract-bunker1.png' | relative_url }}">
{%- seo -%}
{%- include head-custom.html -%}
</head>
<body>
<header class="site-masthead">
<div class="masthead-inner">
<a class="masthead-title" href="{{ '/' | relative_url }}">{{ site.title }}</a>
<div class="masthead-tagline">{{ site.description }}</div>
<nav class="masthead-nav">
<a href="{{ '/' | relative_url }}">Home</a><span class="nav-sep">·</span>
<a href="{{ '/internals' | relative_url }}">Internals</a><span class="nav-sep">·</span>
<a href="{{ '/porting-notes' | relative_url }}">Porting notes</a><span class="nav-sep">·</span>
<a href="{{ '/building' | relative_url }}">Building</a><span class="nav-sep">·</span>
<a href="{{ '/dev-process' | relative_url }}">Dev process</a><span class="nav-sep">·</span>
<a href="{{ '/dev/agentic-development' | relative_url }}">Two-agent study</a>
</nav>
</div>
</header>
<main class="doc-content">
{{ content }}
</main>
<footer class="site-footer">
<div class="footer-inner">
<p class="footer-stamp">
Last updated 2026-09-28 — v0.4.0
<span class="nav-sep">·</span>
<a href="https://github.com/jkdansereau/goldeneye-pc-port">GitHub</a>
<span class="nav-sep">·</span>
<a href="https://github.com/jkdansereau/goldeneye-pc-port/releases">Releases</a>
</p>
<p class="footer-legal">
Non-commercial fan preservation/research project. No ROM or game assets
are distributed; you supply a ROM you already own. Not affiliated with
or endorsed by Nintendo, Rare, Microsoft, MGM, Danjaq, or EON
Productions.
</p>
</div>
</footer>
</body>
</html>
+246
View File
@@ -0,0 +1,246 @@
# Controller & input options wave (port plan)
Status: **Wave A IMPLEMENTED** (code complete, clean Windows/MSYS2 build; pending
user by-ear/by-eye feel-check + Deck tuning of item 5) — the "low-hanging
fruit" from the Nightdive *Turok* PC-port options menu that is cheap, well-
scoped, and
**port-layer only** (zero `src/` / `src/game` edits → rule 2 and the ABI
exception do not apply). Sequenced after v0.4.0 ships. This is the *input*
wave; the sibling *video/audio/graphics* Tier-A items (borderless window,
primary display, brightness, master volume, button glyphs, uncapped-FPS) are a
separate follow-on wave — see §Out of scope.
Reference: `docs/dev/RUMBLE-PLAN.md` (D401) is the closest precedent — a
controller option that lives entirely in `port/src/input.c` +
`port/src/optionsoverlay.c` + a `configRegister*` key + a `kResetDefaults`
entry, with the game side (`src/joy.c`, `src/` stick consumers) untouched.
`docs/dev/GEPD-INPUT-PLAN.md` documents the pad poll path this wave extends.
---
## Ground truth (current state, verified 2026-09-27)
**Pad poll = `inputComputePad()`** (`port/src/input.c:1375`), runs on the game
thread (`osContStartReadData`). It reads the SDL axes/buttons for pad `idx`,
writes out a 16-bit button mask plus `*stick_x` / `*stick_y` (the N64
`signed char` stick, clamped to `STICK_MAX`). All the knobs in this wave are
applied **inside** `inputComputePad`, before the two `*stick_x`/`*stick_y`
writes at `:2063-2064` — i.e. the port re-shapes the *input the game already
consumes*, never the game's stick→turn math.
**Two pitch modes drive the look stick** (selected by `Input.NaturalPitch`,
the default-on "SOLITARE" mode, D194/D238):
- **Continuous (natural-pitch, the default):** the **right stick** is the
continuous look axis — `input.c:1927-1933`:
`rxs = scaleAxis(rx);` … `rys = -scaleAxis(ry); if (padLookInvertY)
rys = -rys; sx = rxs; sy = rys;`. The left stick becomes digital-step
movement. **This is the path the X/Y look-sensitivity + look-smoothing +
right-stick deadzone knobs target.**
- **Digital (native, natural-pitch off):** the left stick is analog
movement/turn and the right stick is digitized into the C-button look
(`:1935-1947`, thresholds at `RSTICK_THRESHOLD`). Look here is the faithful
N64 *digital* C-button turn — **not sens-scalable** (there is no analog look
to scale), so the new look-sens/smoothing knobs are inert in this mode,
exactly as `Input.AimMode`/`Input.AimRange` already are per mode.
**Existing pad knobs** (all `configRegister*` in `input.c`, rows in
`optionsoverlay.c` INPUT section): `Input.PadDeadzone` (single,
`padDeadzone = STICK_DEADZONE`, `:524`), `Input.PadTriggerPct` (`:525`),
`Input.PadLookInvertY` (`:526`), `Input.RumbleScale` (D401). `scaleAxis()`
applies the shared deadzone + `STICK_MAX` scale to a raw `int16` axis.
**Already covered elsewhere** (do not re-plan): mouse sens/invert/smoothing
(`Input.MouseSensitivity`/`MouseInvertY`/`MouseSmoothing`), **uncapped FPS**
(`docs/dev/UNLOCKED-FPS-PLAN.md` — parked; has a real perf prerequisite, D250),
**controller rebinding** (PR #109 / D383 — the hard, open gap, out of scope).
---
## Items (all port-layer)
### 1. `Input.PadLookSensX` — right-stick look **horizontal** sensitivity *(S)*
New int key, default **100** (= 1×, current behaviour), range ~**25–200 %**.
Applied in the continuous-look branch only: after `rxs = scaleAxis(rx)`,
`rxs = clamp(rxs * padLookSensX / 100, ±STICK_MAX)` before `sx = rxs`
(`input.c:1928/1932`). Pure gain on the emitted look X. In digital mode: inert.
- Row: INPUT, "Look sensitivity (left/right)", slider, after "Rumble strength".
- Risk: none (monotonic gain; 100% = byte-identical to today). Deck-testable.
### 2. `Input.PadLookSensY` — right-stick look **vertical (pitch)** sensitivity *(S)*
Same shape, applied to `rys` (`input.c:1930/1933`): `rys = clamp(rys *
padLookSensY / 100, ±STICK_MAX)`. Default **100**, range ~**25–200 %**.
Kept separate from X (Turok splits them; pitch and yaw feel different on a
stick). Independent of the existing `Input.HipfirePitchSpeed` (that one is the
hipfire/mouse pitch rate, a different path). In digital mode: inert.
- Row: INPUT, "Look sensitivity (up/down)", slider.
- Risk: none (same as item 1).
### 3. `Input.PadSouthpaw` — swap L/R triggers *(S)*
New toggle, default **off**. Today (`input.c:1954-1958`): `TRIGGERRIGHT >
trigPt → GE_CONT_G`; `TRIGGERLEFT > trigPt → GE_CONT_R`. Under southpaw, swap
the two `GE_CONT_G`/`GE_CONT_R` assignments (left trigger becomes the
right-shoulder action and vice-versa). Two lines. Lets left-handed /
southpaw players put their index finger on the primary fire trigger.
- Row: INPUT, "Southpaw", toggle.
- Risk: none at default off; trivial to verify by re-aiming.
### 4. `Input.PadDeadzoneL` / `Input.PadDeadzoneR` — per-stick deadzone *(S–M)*
Split the single `Input.PadDeadzone` into a **left (movement)** and a
**right (look)** deadzone. Thread the deadzone through `scaleAxis()` (it is
currently the global `padDeadzone`) as a per-call argument: left-stick calls
use `padDeadzoneL`, right-stick calls use `padDeadzoneR`.
- **Back-compat/migration:** on load, if `Input.PadDeadzoneL`/`R` are absent
but legacy `Input.PadDeadzone` is set, seed both from it (one-time, like the
`AimStyle`→`AimMode` migration at `input.c:449-454`). Keep `Input.PadDeadzone`
registered so old inis keep loading; it becomes hidden/superseded (D337/`D333`
precedent for a superseded key).
- Rows: INPUT, "Deadzone (left stick)" + "Deadzone (right stick)", sliders.
- Risk: low — a deadzone is a per-axis floor; splitting it cannot change
behaviour when both equal the current single value. Watch the menu-navigation
path (`:1918-1922`, left stick in menus) to use the **left** deadzone there.
### 5. `Input.PadLookSmoothing` — right-stick look low-pass *(M)*
New key, default **0 (off)**, range 0–10 (0 = off = today). When >0, low-pass
filter the *emitted* right-stick look deflection so the look axis eases rather
than tracking the stick 1:1 — the pad analogue of `Input.MouseSmoothing`
(which smooths the *relative* mouse delta). Because the continuous stick is
**absolute** (not a delta), implement as a per-pad exponential moving average
on the emitted `rxs`/`rys` (state kept per `idx`, reset on pad hot-unplug and
on aim-mode exit so releasing the stick recentres cleanly). The higher the
value, the heavier the easing.
- Row: INPUT, "Look smoothing", slider (0 = off).
- Risk: **medium** — an EMA on an absolute stick can feel laggy or fail to
recentre if the filter isn't driven back to 0 when the stick is centred;
needs hands-on tuning on the Deck (the natural test device). Keep default off
so it ships inert until tuned.
### 6. Mouse **vertical** sensitivity row *(trivial — folds in)*
`Input.MouseYScale` already exists as a registered key; it just has no F10
row. Add one slider row (INPUT) so the asymmetric X/Y mouse sensitivity is
user-adjustable without editing the ini. No `input.c` change — row only.
---
## Shared plumbing (do once, not per item)
- **`configRegisterInt`/`Float`** for each new key in `input.c` next to the
existing pad-key registrations (~`:2536-2538`).
- **Static state** next to `padDeadzone`/`padLookInvertY` (`:524-526`):
`padLookSensX/Y`, `padSouthpaw`, `padDeadzoneL/R`, `padLookSmoothing` + the
per-pad EMA state for item 5.
- **F10 rows** in `optionsoverlay.c` `rows[]` INPUT section (after
"Rumble strength", with the other controller rows) + matching **front options**
rows via the shared `optionsRow*` accessors (both UIs read `rows[]` — D401
precedent, so one definition covers F10 *and* the front options screen).
- **`kResetDefaults`** entries for each new key (neutral: sens 100, smoothing
0, southpaw 0, deadzones = `STICK_DEADZONE`) so INPUT "Reset to defaults"
covers them (D401 item-3 precedent).
## Ground rules
1. **Port-layer only** — every edit is in `port/src/input.c`,
`port/src/optionsoverlay.c`, `port/src/frontoptions.c`, and `CMakeLists.txt`
(nothing new to compile — no new files). Zero `src/` / `src/game` edits →
rule 2 and the pointer-width ABI exception are not engaged.
2. **Off-by-default = today's behaviour.** Every new key defaults to a neutral
value that leaves the emitted stick/buttons byte-identical to the current
build, so the golden gate (D117/`golden_gate.sh`) is unaffected at defaults
and the v0.4.0 playtest baseline is preserved.
3. **Per-mode honesty.** The continuous-look knobs (1, 2, 5) apply only in
natural-pitch mode; in digital mode they are inert — document this on the
rows (mirrors how `Input.AimRange` is hidden in Centred mode, D338), rather
than pretending they affect the digital C-button look.
4. **Deck is the reference device** for the pad wave (native Steam Controller,
the real-world target); Windows + a plugged gamepad as the second check.
## Verification
1. `./build-pc.sh ntsc-final` clean (no new symbols/dupes — the `configRegister*`
+ `kResetDefaults` additions are the only new references).
2. `/linkcheck` + the golden gate (`tools_pc/golden_gate.sh`) — must pass
**unchanged at defaults** (proves the neutral-default invariant).
3. Hands-on, on a pad / the Steam Deck: each knob at a non-neutral value
changes feel as expected and at default leaves it identical; southpaw
re-maps the triggers; per-stick deadzones floor the correct stick; look
smoothing eases and **recentres on stick release** (item 5's known risk).
4. New finding (D4xx, next free label) recording the wave + per-item
Deck-test result, cross-ref'd to D401 (precedent), D337/D338 (per-mode
gating pattern), D333 (`AimStyle` migration pattern), PR #109 (out-of-scope
rebind).
## Out of scope for this wave (see the assessment above)
- **Sibling Tier-A, other sections (separate follow-on wave):** borderless
windowed, primary-display selection, brightness slider, master audio volume,
button-prompt glyphs.
- **Tier B (medium, contained):** radial fog toggle, HUD opacity, flash-
intensity slider (promote `Game.NoHitFlash`), show-HUD master toggle, audio
output device, low-pass filter, SMAA/FXAA.
- **Uncapped FPS** — `docs/dev/UNLOCKED-FPS-PLAN.md` (parked; D250 perf
prerequisite means it is not quick).
- **Tier C (rule-2 / big-feature / out-of-scope):** head-bobbing sliders,
death-cinematic / switch-on-pickup / walk toggles (all `src/game` flow),
bloom / light-scattering / water refraction-reflection / simple shadows,
violence / blood colour, multiple graphics APIs, and **full controller
rebinding** (PR #109 — the genuinely hard input item).
## Suggested sequencing (v0.5.0)
1. **Wave A (this doc):** items 1–4 + 6 (sens X/Y, southpaw, per-stick
deadzone, mouse-Y row) — all low-risk, shippable behind neutral defaults.
2. **Item 5 (look smoothing)** lands in the same wave but may ship default-off
pending Deck feel-tuning.
3. **Wave B:** the sibling video/audio/graphics Tier-A items.
4. Revisit Tier C only with explicit sign-off (rule-2 items need the
`docs/dev-process.md` procedure).
---
## Implementation status (2026-09-28)
Wave A code is complete and compiles clean (Windows/MSYS2 `ninja` incremental,
exit 0, no new warnings — only the pre-existing `ramrom.c` / `gfx_pc.cpp` ones).
Every edit is in `port/src/input.c` + `port/src/optionsoverlay.c`;
`frontoptions.c` needed **no** change (both UIs read the shared `rows[]` via the
`optionsRow*` API). Zero `src/` edits, no new files/symbols — only file-local
statics + `configRegisterInt` calls — so the link is unchanged.
Implemented (all off/neutral-by-default → byte-identical at defaults):
- **1/2. `Input.PadLookSensX` / `Input.PadLookSensY`** (25–200 %, dflt 100):
right-stick look, applied in the natural-pitch branch of `inputComputePad()`
after `scaleAxis()`, re-clamped to `STICK_MAX`. Inert in digital mode (the
right stick is digitized there, no analog look to scale).
- **3. `Input.PadSouthpaw`** (toggle, dflt 0): swaps the fire (`GE_CONT_G`)
and grenade (`GE_CONT_R`) trigger actions *after* the raw edges are captured,
so edge-detection stays consistent.
- **4. `Input.PadDeadzoneL` / `Input.PadDeadzoneR`** (0–30000, dflt 7000):
`scaleAxis()` now takes a per-stick deadzone param; left-stick (movement)
calls pass `L`, right-stick (look) calls pass `R`. The legacy
`Input.PadDeadzone` key stays registered (no row) so old inis load; a
one-time migration in `inputInit()` seeds any unset L/R (C-init sentinel
`PAD_DZ_UNSET = -1`) from the legacy value, before any `configSave`. The
single "Stick deadzone" row is replaced by "Deadzone (left stick)" /
"(right stick)".
- **5. `Input.PadLookSmooth`** (0–10, dflt 0 = off, shown "N/10"): per-pad EMA
low-pass on the natural-pitch look output (`padSmSX/padSmSY[MAX_PADS]`), reset
on hot-unplug (`inputRescanPads`) so a snapped-back stick can't lag from a
stale value; otherwise the EMA decays to 0 as the stick recentres. Turok has
this as on/off; GE ships a graduated 0-10 knob instead. Ships **default-off**
pending Deck feel-tuning.
- **6. `Input.MouseYScale`** row (dflt 100): the pre-existing registered key
finally gets an INPUT slider row ("Mouse vertical sensitivity") — row only,
no `input.c` change.
**Verbiage + display baseline (Turok PC, 2026-09-28):** row labels now match the
Turok options menu verbatim where GE has an analogue — *Mouse horizontal /
vertical sensitivity*, *X axis / Y axis look sensitivity*, *Look smoothing*,
*Southpaw*, *Vibration* (was "Rumble strength"), *Deadzone (left/right stick)*.
The display is baselined to Turok's scales: sensitivity + deadzone knobs read
**0-100 with 50 = the default** (the existing `calibratedDefault` "N/100" path),
and look smoothing reads **0-10** ("N/10"). (Turok's Mouse/Controller subsection
split is not mirrored — GE keeps one flat INPUT section per D356, so the two
invert rows retain their "(mouse)"/"(controller)" suffixes to stay unambiguous.)
`kResetDefaults` covers all six so INPUT "Reset to defaults" is complete;
`calibratedDefault()` gives the sensitivity + deadzone rows their 50/100 default
midpoint, and `dispMax` renders the smoothing knob as "N/10".
**Still owed (verification, not code):** user by-ear/by-eye feel-check of the
new knobs on a real pad (esp. item 5 smoothing strength + item 1/2 sens range),
the Deck cross-check, and confirming the per-stick deadzone migration on an
old-ini user (L/R seed from `Input.PadDeadzone`).
+45 -2
View File
@@ -16,6 +16,48 @@ export at `scratchpad/backlog-scrape/restore-cut-content-mod/` (gitignored).
> table as pickup+prop pairs except heroine; (4) TCRF's 11 unused cheats, music
> track index, MP character table and build dates all verify 1:1 against
> `bondconstants.h`/`cheat.c`/`front.c`/`compiletime.c`.
>
> **Round 2 (decomp-only discoveries — not in TCRF's documentation):**
>
> 1. **The shipped game still calls the M16 an "AR33"** — pickup text
> `"an AR33 assault rifle."` (`LpropobjE.c` PROPOBJ_STR_2A, used at
> `propobj.c:10471`), plus unused strings `"US AR33 Assault Rifle"`/
> `"US AR33"` (`LgunE.c` GUN_STR_74/75) and `PROP_CHRM16 /* AR33 Assault
> Rifle */` comments. TCRF's prerelease page mentions the AR33 name in early
> footage but not that it survived into retail text.
> 2. **Klobb rename trace verified**: weapon-name strings in `LgunE.c` are
> alphabetical except GUN_STR_6B `"Klobb"` (between "Spy File" and "Staff
> List") — exactly the last-minute Skorpion→Klobb scar TCRF describes.
> 3. **Two cut weapons TCRF's unused-items page doesn't list**: `cartrifle` and
> `wristdart` — full pickup+prop resources (`GcartrifleZ`, `GwristdartZ`,
> `PchrwristdartZ /* (BETA) */`) but no `ITEM_*` entry and no WeaponStats,
> i.e. unusable as weapons. The wrist dart is the hardware behind TCRF's
> `AMMO_DARTS` ($0E).
> 4. **All four Bonds, fully mapped**: `CUFF_CONNERY/DALTON/MOORE` +
> `BOND_BROSNAN/CONNERY/DALTON/MOORE` enums; the actor-switch code
> (`bondview2.c:391-460`) survives with every branch flattened to Brosnan;
> the save struct keeps the per-dossier actor field. **No Connery/Moore/
> Dalton models exist in the retail ROM** (80 `C*Z` chr models enumerated —
> none for the former actors); only enum slots + the `SW_CONNERY`/`SW_DINNER`
> switches on the watch-arm model remain. TCRF's "four dossiers, four
> actors" was pre-release state.
> 5. **Cuba is the one orphan level with recoverable content**: `LlenE.c` holds a
> cut Natalya dialogue exchange, the "C U B A N J U N G L E" title card, and a
> full alternate credits cast. The other 12 unused level text files are empty
> stubs.
> 6. **Orphan-level geometry IS in the ROM**: `assets/obseg/obseg.h` declares all
> nine text-only orphans' bg segments (`bg_sho_all_p_seg[]` …) — a
> byte-matching decomp can't fabricate them. What's missing is mission setup
> (briefings exist only for the 21 solo levels), not geometry.
> 7. **Moneypenny's two risqué Frigate-briefing lines are gone from the ROM**
> (final `LdestE.c` keeps only the toned-down "thighs of steel" paragraph);
> recoverable only from TCRF's transcription. "Destroyer"→"Frigate" rename is
> complete; the Manticore yacht mission has zero ROM trace.
> 8. **Music enum ↔ Notes-page GS index fully aligned**: indices 0–48 match
> slot-for-slot; four local mismatches past 0x2C (49 `M_CUBA` vs "Ending
> theme", 52 `M_MPTHEME3` vs "Surface 2 X", 54 `M_GUITARGLISS` vs "MP Death
> (Alt)", 59 `M_SURFACE2X` vs "Surface 1 X") — in-game `DEB_MUSIC` audition
> needed to resolve.
## 1. Findings
@@ -250,8 +292,9 @@ Cross-reference:
- Head model dirs exist under `assets/obseg/chr/`: `headken` (the Ken face),
`headbrosnan*` (4 costume variants), `moonfemale`, plus ~30 named heads.
The actor-named suits (Connery/Dalton/Moore) do **not** appear as chr dirs —
likely cut before model export, or embedded in a shared body file.
**Unverified** which exact models the TCRF face shots show.
**resolved in round 2**: the ROM holds exactly 80 `C*Z` character models and
none is a former-Bond actor; only the enum slots + the flattened actor-switch
code (`bondview2.c:391-460`) survive (see banner, item 4).
**Status: Tier 2/3.** Cosmetic mod (extra MP faces) = asset-level work if the
models are in the ROM; verify by dumping `assets/obseg/chr/*/Model.c` headers.
+369
View File
@@ -0,0 +1,369 @@
# D356 — Settings regroup: functional sections, save-file scoping, per-section reset
Status: **LANDED (D356, 2026-09-27, port-only)** — signed off and implemented
this session; verification in `docs/dev/findings.md` D356 (GE_WSPROBE_RESET
front + in-stage probes, regressions, build). Supersedes the "BOND FILE"
section introduced in D353. Baselines: Turok PC port options (Nightdive, list
provided by the maintainer), the PD port's `port/src/optionsmenu.c`
(`C:\Users\james\Source\Repos\pd_port`), and plan §6 of
`docs/dev/OPTIONS-MENU-PLAN.md` (the original "align with Turok" note).
## 1. Goal
The D353 "BOND FILE" section is the one section named after *where a value is
saved* — an implementation detail — while every other section is functional
(DISPLAY, GRAPHICS, MOUSE/AIM, CONTROLLER, GAME). Its contents are also a
mishmash (aim/look toggles + HUD toggles + audio), and it exposes N64-watch
rows that **collide with or duplicate** the PC-native rows the port added
(watch look-invert vs `Input.MouseInvertY`/`Input.PadLookInvertY`; watch aim
control vs `Input.AimMode`). The baseline ports (Turok, PD) never name a
section after the save and never re-expose legacy overrides.
D356 rewrites the row table so that:
1. Every section is functional, using the baseline names:
**INPUT, GAMEPLAY, GRAPHICS, AUDIO, VIDEO**.
2. Save-file scope is communicated the PD way — *in the section title*
("GAMEPLAY (File 1)"), generalized from the D353 BOND-FILE annotation —
plus a single **Save file: File N [change]** row at the top of the
options screen (PD "Player N" pattern; D354's default resolution applies).
"Bond file" appears nowhere in the UI.
3. Redundant/colliding watch rows are removed from the **menu surface only**
(the established D181/D216/D304 pattern): keys, in-game watch behavior,
and ini hand-editing all remain untouched. No game-code change (rule 2).
4. A **Reset to defaults** action row per section (Turok's per-category
reset; plan §6 Gate E, made concrete) with a **confirmation contract**:
edge-triggered activation (no auto-repeat), two-step arm→confirm, never
repeatable (WATCH-SETTINGS-PLAN §6 gate: "separate confirmation and
persistence policy before wiring a button" — §5.4 below is that policy).
5. A **context-aware file accessor** (front-end target vs active stage
target) with the "none" chooser position retired, so "Save file: File N"
is always a true statement (§5.5).
6. Section resets iterate the section's **declared** rows (including
conditionally hidden ones, §5.6) and reset verification exercises the UI
action itself, headlessly (§5.8).
## 2. Non-goals
- No `src/game` changes; the N64 watch menu keeps all its options and its
N64 wording (ground truth).
- No key rebinding (Phase 4) — but the section layout leaves room for a
"KEY BINDINGS" page exactly where Turok/PD have one.
- Master volume stays M3-backlog; the AUDIO section is shaped for it
(Turok's Master/Sound/Music trio).
- N64 build untouched.
## 3. Exposure filter (menu surface only)
| Row (key) | Verdict | Reason |
|---|---|---|
| `Bond.Look` ("Look up/down (watch; stacks)") | **Hide** | Duplicates `Input.MouseInvertY` + `Input.PadLookInvertY`; stacking both produces the double-negation trap. |
| `Bond.AimControl` ("Aim control") | **Hide** | Collides with `Input.AimMode` (N64 vs PC aim model) — two rows fighting over the same domain. |
| `Bond.AutoAim` | Keep | Unique gameplay assist; orthogonal to aim style/range. |
| `Bond.LookAhead` | Keep | Unique; no PC counterpart, no conflict. |
| `Bond.Sight`, `Bond.Ammo` | Keep | Unique HUD-visibility options; compose with `Game.HudScale`. |
| `Bond.Music`, `Bond.FX` | Keep | Core of D350–D355. |
Hidden rows are commented out in `rows[]` with a D356 note (D181/D216 style:
config keys + watch path stay live; ini power users can still set them).
## 4. New layout (5 pages + save-file row)
`MAX_PAGES=8`, `MAX_PROWS=15` — all pages below the caps (D353 overflow log
still guards).
Top row (drawn by `frontoptions.c` above the paged content, not paged):
**`Save file: File 1 [change]`** — cycles files via `watchSettingsChooseFile`
(D354 default resolution: explicit pick > viewed folder > first valid;
auto-init fallback). Hidden in F10 (existing `optionsRowIsBondChooser`
mechanism, generalized). F10 targets the active file, as today.
| Page | Rows (key · kind) | Title annotation |
|---|---|---|
| **INPUT** | `Input.MouseSensitivity` · `Input.MouseInvertY` · `Input.AimMode` · `Input.AimRange` · `Input.PadLookInvertY` · `Input.PadDeadzone` · `Input.PadTriggerPct` · *Reset to defaults* | |
| **GAMEPLAY** (File N) | `Bond.AutoAim` · `Bond.LookAhead` · `Bond.Sight` · `Bond.Ammo` *(save, dim "(save)" tag)* · `Game.SkipIntro` · `Game.NoHitFlash` · `Game.AllUnlocked` · `Game.HudScale` · *Reset to defaults* · `__QuitToDesktop` | (File 1) |
| **GRAPHICS** | 11 rows unchanged from D353 · *Reset to defaults* | |
| **AUDIO** (File N) | `Bond.Music` · `Bond.FX` *(save; title annotation only — homogeneous section, PD-style)* · (master volume, M3) · *Reset to defaults* | (File 1) |
| **VIDEO** | `Video.Fullscreen` · `__Resolution` · `Video.VSync` · `Video.FpsCap` · `Video.DisplayFPS` · *Reset to defaults* | |
Moves from D353 layout: mouse/aim + controller rows merge into **INPUT**
(the two old headers `__HdrMouse`/`__HdrPad` are dropped); DISPLAY → **VIDEO**
(rename, `Video.DisplayFPS` moves from GAME); GAME section is renamed
**GAMEPLAY** and absorbs the surviving watch toggles; the `__HdrBond` header
and `__BondFile` chooser row are retired (replaced by the top save-file row);
`Bond.Look`/`Bond.AimControl` rows are commented out (§3).
Scope annotation: the D353 "(File N)"/"(no file)" title annotation — today
special-cased to the BOND FILE page via `optionsRowIsBondChooser()` — is
generalized: **any page containing per-file rows annotates**. Sections with
per-file rows: GAMEPLAY, AUDIO.
F10: same table (F10 already shows all rows, chooser hidden). The
(Gameplay/Audio) annotation in-stage shows the active file number (the
context-aware accessor, §5.8 — *not* the raw chooser value). "Reset to
defaults" rows stay visible in F10 (O2) under the same confirmation
contract (§5.4).
**(save) tag (O1):** the four mixed-scope GAMEPLAY save rows carry a subtle
dim "(save)" suffix — the only place a section mixes scopes. AUDIO is
homogeneously per-file, so its title annotation is sufficient (no tags).
Implementation: one `int saveScoped` field on `struct Row` + dim-ink suffix in
the row-draw path (frontoptions.c already draws the dim annotation).
## 5. Code changes
All in `port/` (D350-family); files: `port/src/optionsoverlay.c`,
`port/src/frontoptions.c`, `port/include/optionsoverlay.h`,
`port/src/watchsettings.c` (only if reset needs a new enqueue helper —
expected not to, see below).
### 5.1 Row table (`optionsoverlay.c`)
- Rename headers: `__HdrDisplay`→VIDEO (last page), add `__HdrInput`
(INPUT), `__HdrGameplay` (GAMEPLAY); delete `__HdrBond`, `__HdrMouse`,
`__HdrPad`, and rename `__HdrGame`→GAMEPLAY. New section order in the
table: INPUT, GAMEPLAY, GRAPHICS, AUDIO, VIDEO (Turok's order: Input,
Gameplay, Graphics, Audio, Video).
- Move the 7 mouse/aim + 3 controller rows under INPUT; move the 4 surviving
`Bond.*` toggles + music/FX into GAMEPLAY/AUDIO; comment out
`Bond.Look`/`Bond.AimControl` (§3).
- New `ROW_ACTION` rows, one per section: `.key="__Reset<Section>"`,
`.label="Reset to defaults"`. All five use one shared handler
(`rowResetSection(section)`).
### 5.2 Save-file top row (`frontoptions.c`)
- New non-paged line above the page content: "Save file: File N" + a
"change" control (left/right or Enter cycles, reusing
`watchSettingsChooseFile(dir)`; D354 lazy default fills N). Dim ink, same
visual language as the D353 annotation. `selected_folder_num` unchanged —
the row never touches the game's active file (plan Gate B invariant).
**Display value reads through the context-aware accessor (§5.5), never
the raw chooser value.**
- **Unavailable state (maintainer consistency fix):** the D354 auto-init
fallback creates the blank file on a *later game tick*, so at an
empty/corrupt-save startup the accessor can return -1 for a few ticks.
The row then shows the unavailable state ("(no file)"), never a file
number the write path cannot use. Once creation succeeds the same tick
the row shows "File 1".
- F10 context: row suppressed (extend the D353 accessor to cover it).
### 5.3 Reset to defaults (Gate E)
- **Per-file rows** (GAMEPLAY/AUDIO save rows): defaults = **BLANKSAVEDATA**
(`src/game/file2.c` — the game's own factory save; per WATCH-SETTINGS-PLAN
§6, watch rows reset to BLANKSAVEDATA/`DEFAULT_OPTIONS` for the selected
file, *not* from a global ini snapshot; new saves start Music/FX 0xFF,
auto-aim/sight/look-ahead/ammo enabled — read the fields from
`BLANKSAVEDATA` at reset time, no hardcoded table). Reset re-applies live
via the existing `watchsettings` apply path **and** persists field-scoped
to the selected file (`watchSettingsPersistField`), exactly like a normal
commit. In-stage: enqueued through the D352 command queue (N field commits
with `commit=1`) — no new threading; game thread applies + persists.
- **Ini rows** (INPUT/GRAPHICS/VIDEO/GAMEPLAY-ini): `config.c` has no
central default table (first-write "defaults" = current values,
config.c:280), so the section handler uses an explicit
`static const struct { const char *key; double def; }` reset table in
`optionsoverlay.c`, mirroring the port's C initializers (each entry
commented with its source variable; **verified against the initializer at
implementation time** — e.g. `Game.AllUnlocked` = **0**,
`port/src/video.c:116,348`; the D257 comment in optionsoverlay.c that
says "default ON" is stale and is corrected as part of D356). Front-end:
write live + `configSave()` (existing). In-stage ini resets: applied
**immediately** (O3 — consistent with how every other ini row already
behaves when changed in F10; video.c applies most display changes on the
fly).
- **Resolution is excluded from the VIDEO reset (maintainer item 2).**
`__Resolution` is an action backed by `s_resSel` +
`videoRequestWindowSize()` — not a registered config row, so the numeric
default table cannot reset it, and a "default window size" is a
display-capability choice, not a tunable. The D356 findings entry
documents the exception explicitly: VIDEO reset covers Fullscreen, VSync,
FpsCap, DisplayFPS — Resolution keeps the player's current choice.
- **Scope-aware by construction:** a section's reset touches each row
**by its own scope** (file rows → that file's BLANKSAVEDATA values;
ini rows → ini defaults). No cross-scope surprise (Gate E: "do not make
a global reset wipe per-file watch settings" — a reset of the selected
file never touches other files; §5.8 verifies this).
- **Activation contract (§5.4) applies to every section's reset row.**
### 5.4 Reset activation contract (maintainer item 1)
A reset is a *destructive bulk write* — GAMEPLAY's in particular changes
both ini values **and** the selected save in one action. WATCH-SETTINGS-PLAN
§6 gates confirmation + persistence policy; this is that policy:
- **Edge-triggered:** reset actions fire only on a *fresh* event. The
frontoptions left/right key repeat (held keys re-issue the action) is
suppressed for `ROW_ACTION` reset rows — the arming/confirm events must
carry a no-repeat flag (SDL keydown with `repeat==0` / a fresh click),
so a held key or held mouse button can never arm *and* confirm, nor
re-trigger.
- **Two-step confirm:** first activation **arms** the row (label swaps to
"Confirm reset…", armed highlight); a second activation within **3 s**
commits. Timeout, or navigating to another row, disarms (label
restores). Arm state is per-row, per-open; opening/closing F10 or the
options screen clears all armed resets.
- **One commit per confirmed activation.** No auto-repeat, no re-arming on
the same event. F10 and the options screen share the identical contract
(O2: rows stay visible in F10; confirmation makes accidental in-game
resets unlikely).
- The confirmed action then dispatches the section handler (§5.3) exactly
once.
### 5.5 Context-aware file accessor; retire "none" (maintainer item 3)
`watchSettingsFolder()` returns the raw `chosen`, which may be -1 before
`frontFolder()` lazily resolves, and in-stage `chosen` can differ from the
active file (`snapFolder`). The generalized title/top-row accessor must be
**context-aware**, not a repurposed chooser-value read:
- New `watchSettingsActiveFolder(void)` (name tentative):
- **Front-end contexts** (options screen, F10-on-file-select, title):
the *resolved front target* — explicit `chosen` if set, else
`selected_folder_num` if valid, else first valid folder (i.e. the
`frontFolder()` resolution; the accessor is pure-read and does not
mutate `chosen` — lazy mutation stays in `frontFolder()`/the
auto-init fallback).
- **In-stage (F10):** the *active stage target* — `selected_folder_num`
(the file the stage loaded, `snapFolder` at apply time).
- The D353 title annotation and the §5.2 top row both read through this
accessor, in both surfaces.
- **The chooser's "none" position is retired.** `watchSettingsChooseFile`
cycles FOLDER1..MAX only (dropping the -1 stop). Rationale: D354's
default resolution + boot-time BLANKSAVEDATA slots + the auto-init
fallback guarantee a valid target, so "none" was only reachable by
deliberate cycling and serves no remaining purpose — and "Save file:
File N" (the §5.2 invariant) is not true when `chosen == -1`.
`watchSettingsFolder()`'s "-1 = none" contract is updated/superseded by
the new accessor.
### 5.6 Reset iterates declared rows (maintainer item 4)
The section handler iterates the section's **declared row range in
`rows[]`** (from its header to the next header), *not* `s_visIdx`/the
currently visible list — conditionally hidden rows (GRAPHICS draw/LOD
sliders while their auto-FOV toggles are on) must be reset too, or they
silently survive a purported section reset. Hidden-row defaults come from
the same default table; their *live* values are written directly (the
`ptr`/`rowSet` path), visibility is irrelevant. The `(save)` tag, hidden-row
resets, and the reset table are all keyed off `rows[]` indices, never the
visible list.
### 5.7 F10
Unchanged machinery: shared table, save-file row suppressed (the D353
accessor pattern extended to the §5.2 top row). Annotation shows the active
file via §5.5. "Reset to defaults" rows stay **visible** in F10 (O2) under
the §5.4 confirmation contract.
### 5.8 Reset verification probe (maintainer item 5)
New env-gated probe **`GE_WSPROBE_RESET=<tick>`** (GE_D314/GE_WSPROBE
pattern): at tick N of a *front-end* boot it drives the **UI action path**
through the same handler the confirm step calls (arm → confirm → dispatch,
no bypass). **Every section being verified is itself dispatched:** the
probe dispatches all five section resets (INPUT, GAMEPLAY, GRAPHICS, AUDIO,
VIDEO) via arm → confirm, and after *each* dispatch verifies that
section's rows:
- **INPUT / GRAPHICS / VIDEO (ini rows):** values == the default table.
GRAPHICS specifically covers the **hidden** draw/LOD sliders (hidden
while their auto-FOV toggles are on) — a section reset must land them
even while they are not visible; VIDEO's `__Resolution` is the documented
exclusion (unchanged, logged).
- **GAMEPLAY (mixed scope):** the four save rows == BLANKSAVEDATA
(auto-aim/sight/look-ahead/ammo) *and* the ini rows (SkipIntro/
NoHitFlash/AllUnlocked/HudScale) == the ini default table — the two
scopes land independently in one action.
- **AUDIO (save rows):** Music/FX == BLANKSAVEDATA (0xFF raw = 32767,
the `VOLUME_MAX` endpoint), not the probe's pre-dirtied 4096.
- **Scope isolation (after all dispatches):** a *second* folder's
`save_data` is byte-identical to its pre-reset snapshot (a reset of the
selected file never touches other files).
Front-end prep (tick N) first dirties live values (Music/FX → 4096,
VSync/HudScale → non-defaults) and snapshots the second folder, so the
verifications at tick N+1 prove the resets actually moved values back.
In-stage dispatch is covered by the same env var on a `-level_XX` run:
arm → confirm at tick N (file rows enqueue through the D352 queue — the
probe logs the enqueued command count), drain at N+1, then the same
per-section value verification against the post-apply state.
### 5.9 No changes
- `buildPages` in `frontoptions.c` (generic; 5 pages < MAX_PAGES),
`watchSettingsGameTick`/queue (D352/D354/D355 logic untouched), the D355
track-1 resync, `BLANKSAVEDATA` itself.
## 6. Threading / persistence gates (unchanged, restated)
- D350: no GE writes from the F10 input/render thread except the sanctioned
front-end commit path; in-stage edits queue to the game thread.
- D352: queue holds across transitions; per-field coalescing.
- D354: front contexts (including F10-on-file-select) route through the
chooser path; `frontFolder()` default resolution; auto-init fallback.
- D355: Music commit + level-entry resync of the BGM (track 1) player.
- D356 adds no new write path: reset = N existing commits (file rows) +
existing ini writes (ini rows).
## 7. Docs
- `docs/dev/findings.md` D356 entry + §F index row; regenerate
`findings-index.csv`.
- `docs/dev/WATCH-SETTINGS-PLAN.md`: status + D353/D354 "Bond File" section
references updated; Gate E marked landed (subset: per-section rows).
- `docs/dev/OPTIONS-MENU-PLAN.md` (tracked) + local notes: §6 "align with
Turok" annotated as landed via D356 (M3 master volume + key bindings
remain).
## 8. Verification
1. Clean NTSC build (`./build-pc.sh` / `cmake --build build-pc`).
2. Headless smoke: boots to front end; log page count (5, under MAX_PAGES),
no `buildPages` overflow warning, save-file row resolves to File 1 by
default (D354), annotation on GAMEPLAY/AUDIO pages, no "none" reachable
(chooser cycles files only, §5.5).
3. `GE_WSPROBE` / `GE_WSPROBE_FRONT` regression: identical pass as D355
(queue/commit paths untouched).
4. **`GE_WSPROBE_RESET` (§5.8) exercises the UI action, not just direct
commits:** per-section arm → confirm → dispatch of all five sections,
with each section's rows verified right after its own dispatch
(ini == table, save == BLANKSAVEDATA, hidden GRAPHICS rows included,
VIDEO resolution untouched), then the second-folder byte-identity
check. In-stage dispatch: `GE_WSPROBE`-style queue check on a
`-level_XX` run (enqueued command count + post-apply state).
5. **Confirmation contract check (§5.4):** a held key/click arms but never
confirms (edge-triggered); second activation within 3 s commits exactly
once; timeout/navigate disarms. Headlessly assertable via the probe
(arm-then-timeout leaves values untouched).
6. Manual visual pass (owed, same list as D353): arrows through 5 pages,
save-file cycling updates the annotation live, `(save)` tag on the four
GAMEPLAY save rows, F10 in-stage, reset in stage vs front (including the
armed/confirm visual state), rapid slider drag.
## 9. Open questions — RESOLVED (maintainer sign-off, 2026-09-27)
- **O1:** **yes** — subtle dim "(save)" tag on the four mixed-scope
GAMEPLAY rows; AUDIO's title annotation alone is sufficient (homogeneous
section).
- **O2:** **visible in F10**, provided resets require the §5.4 confirmation
step (which they do).
- **O3:** **immediate** in-stage ini resets, consistent with existing ini
edits in F10.
- **O4:** `__QuitToDesktop` stays in GAMEPLAY, **literally last — after the
Reset row** (section order: rows…, *Reset to defaults*, *Quit to desktop*;
the §4 page table is corrected accordingly).
Layout itself is signed off by the maintainer (2026-09-27): five-page
regroup + menu-only exposure filter, with the §5.4–§5.8 contracts as
specified above.
## 10. Estimate
One commit. ~5 files (`optionsoverlay.c`, `frontoptions.c`,
`watchsettings.c`, `port/include/optionsoverlay.h`,
`port/include/watchsettings.h`), net +400/−140 (table reshuffle + reset
contract + context-aware accessor + `GE_WSPROBE_RESET` are the new code).
Findings/csv/plan updates ride along.
+160
View File
@@ -0,0 +1,160 @@
# D357 — Settings thresholds & values: align display units/defaults with the PD port + Turok PC
Status: **PROPOSAL — awaiting sign-off** (2026-09-27). Baselines: the PD port
checkout (`C:\Users\james\Source\Repos\pd_port`, `port/src/optionsmenu.c` +
`input.c`/`video.c` config registrations), the Turok PC (Nightdive) list from
the maintainer's notes (`docs/dev/notes/OPTIONS-MENU-PLAN.md` §6 — *taken from
user notes, not checked independently*), and the N64 watch (ground truth for
the `Bond.*` rows only). Triggered by maintainer report: the current values
are "a bit confusing".
Companion change landed with this doc's review: the terminology settled on
**"Profile"** as the end-user term for a per-player game file -- the
front top row reads **Profile**, the per-file row tag reads
**"(per profile)"** and the F10 unavailable value reads **"Select a
profile"** (§4).
## 1. Problem
The D356 regroup fixed the *layout*; the *values* still mix three unit
families with no consistent rule:
- **raw internal units shown as-is**: `Input.MouseSensitivity` (1–500, 100 =
1× — the "100" is a % of a 1.0 multiplier, not a count),
`Input.PadDeadzone` (raw 0–30000, `dispDiv=300` → "23%" but the step of 500
raw is 1.67 display-%, so the slider lands on 23, 25, 26…),
`Bond.Music`/`Bond.FX` (0–32767, `dispDiv=328`, step 128 = 0.39 display-%);
- **N64-era vocabulary on PC rows**: texture filter "Nearest / Bilinear /
**3-Point**" (the N64 watch term for trilinear), "FOV scale %" instead of
degrees;
- **defaults that don't read as defaults**: MSAA shows "4x" fine, but
`FpsCap` 0–1000 with step 1 invites 117 FPS; `Video.LodDistance` default
150% vs `DrawDistance` 150% with a 25-step grid (100/125/150/175…).
The PD port is the house style to copy: per-stick sensitivity as a **±10
multiplier (default 1.0, negatives invert)** (`input.c:1537`), mouse speed
**-30..30 (default 2.5)**, deadzone **raw 0–32767 per stick+axis**, rumble
**0–1**, MSAA as a **2x/4x/8x/16x dropdown**, frame cap as **"N FPS"**,
volumes as 0–100 %. Turok's (user's list) conventions: single mouse
sensitivity knob, per-section resets (landed, D356), master/sound/music
volume trio (master still M3-backlog).
## 2. Principles
1. **Display, don't rescale the world.** Every change is port-only (row
table, `dispDiv`/step/`unit` fields, value-text, `kResetDefaults`): no
`src/game` change, and config *storage units* are kept wherever a change
would migrate existing ini files (principle 3).
2. **Human units in the value column**: `%` on a 0–100 grid, `×` multipliers
with 1.0 as the default, `N FPS`, `Off/2x/4x/8x`, `Unlimited`. A slider
step must map to a **whole display unit** (the deadzone 1.67%-step bug is
the canonical example to fix).
3. **Ini stability**: renaming a config key's storage unit is a migration.
Where the plan needs it, register a *new* key (e.g.
`Input.PadDeadzonePct`) and keep the old key as a read-once legacy alias —
the D333 `Input.AimMode`→`Input.AimStyle` alias pattern (`input.c:1962`).
Each migration gets a findings note.
4. **`kResetDefaults` stays in lockstep** with the config C initializers
(D356 Gate E): every default change in this plan updates the table in
`optionsoverlay.c` in the same commit; the `GE_WSPROBE_RESET` probe
re-verifies.
5. **Defaults = the config C initializers**, never a new menu-only default.
## 3. Row-by-row audit & proposal
Current state from `optionsoverlay.c` `rows[]` + the `configRegister*`
ranges (2026-09-27). "Display" = what the value column shows today.
### INPUT
| Row | Now (default · range · step · display) | PD port | Turok (notes) | Proposal |
|---|---|---|---|---|
| Mouse sensitivity | 100 · 1–500 · 5 · raw `100` | `MouseSpeedX/Y` float −30..30, def 2.5 (sign = invert) | single knob | **Display as a × multiplier**: keep storage (100 = 1.0×), value text `%.1f×` (v/100); range 10–300 (0.1–3.0×), step 10 (0.1×). Label stays "Mouse sensitivity". |
| Invert look (mouse) | Off/On | folded into the −/× sign of MouseSpeed | — | **Keep On/Off** (PD's negative-multiplier style is an alternative; a separate invert is simpler next to the × slider). Decision: confirm. |
| Aim style / Aim range | N64/Centred (PC) · PC/N64 | — (GE-only, D337/D338) | — | Keep as-is (GE-specific; hiddenIfOn already sane). |
| Invert look (controller) | Off/On | sign of stick scale | — | Keep. |
| Stick deadzone | 7000 raw · 0–30000 · 500 · ÷300 → `23%` (1.67%-steps) | raw 0–32767 per stick+axis, `DEFAULT_DEADZONE` | MISSING | **New key `Input.PadDeadzonePct` 0–100, step 1, default 23** (7000/30000×100); `input.c` scales back to raw at apply; old key becomes a legacy alias (principle 3). Display `N%` on a whole-% grid. |
| Trigger threshold | 23 · 1–99 · 1 · `%` | — (GE extra) | — | Keep. |
### GAMEPLAY
| Row | Now | Baseline | Proposal |
|---|---|---|---|
| Auto-aim / Sight / Look ahead / Ammo (profile) | Off/On (N64 watch 0/1) | N64 watch = ground truth | Keep; tag now reads **(profile)** (this doc's companion change). |
| Skip intro / No hit flash / All unlocked | Off/On | Turok: screen-shake/hit-flash family | Keep. |
| HUD scale | 100 · 75–150 · 5 · `%` | — | Keep (whole-% grid already; range cap D226). |
### GRAPHICS
| Row | Now | Baseline | Proposal |
|---|---|---|---|
| Anti-aliasing | 1/2/4/8, def 4, shows `None/2x/4x/8x`, restart | PD dropdown 2x/4x/8x/**16x**, def 4x | **Add 16x to the sequence** (`kMsaaSeq`, range clamp 1–16) — SDL/GL supports it; PD has it. Decision: perf cost on mid GPUs (restart already flagged). |
| Texture filter | Nearest / Bilinear / **3-Point** | PC convention: Nearest/Linear/Bilinear | **Rename the enum names to `Nearest / Linear / Bilinear`** (3-point *is* trilinear; the N64 term is the last one left on a PC row). Storage 0/1/2 unchanged. |
| Anisotropic filtering | 4 · 1–16 · 1 · `x` | Turok 1–16 | Keep; **dim/disable when filter = Nearest** (anisotropy only applies to filtered sampling) — `hiddenIfOff`-style guard, UI-only. |
| FOV scale | 100 · 50–150 · 5 · `%` | Turok 3: 60–120° slider | **Display in degrees**: the 100% baseline is the N64 vertical FOV scaled to the port's widescreen math — value text `N°` (v/100 × baseline, 1 decimal); storage unchanged. Decision: degrees vs % (Turok uses degrees; % is the existing mental model — pick one, recommend degrees). |
| Native widescreen / Widescreen auto FOV / Crop overscan | Off/On | — | Keep. |
| Draw distance | 150 · 100–400 · 25 · `%` (hidden under auto-FOV) | Turok: draw-distance row | Keep the % grid (25% steps read fine); **step 25 → display already whole-%**. No change. |
| LOD distance | 150 · 25–400 · 25 · `%` | — | Keep; note default 150 vs DrawDistance 150 is intentional parity. |
### AUDIO
| Row | Now | Baseline | Proposal |
|---|---|---|---|
| Music volume (profile) | 32767 · 0–32767 · 128 · ÷328 → 0.39%-steps | Turok: 0–100% | **step 128 → 328** (exactly 1 display-%; 32767/328 = 99.9 ≈ "100%"). Storage unchanged (N64 `VOLUME_MAX` semantics). |
| FX volume (profile) | same | same | same. |
| *(master volume)* | — | Turok trio | M3-backlog, out of scope (shape the section for it, D356 note). |
### VIDEO
| Row | Now | Baseline | Proposal |
|---|---|---|---|
| Fullscreen | Off/On | Turok: window mode (borderless missing) | Keep; borderless = separate backlog item. |
| Resolution | action row (presets) | both | Keep. |
| VSync | On/Off | both | Keep. |
| Frame rate cap | 60 · 0–1000 · 1 · `Uncapped/N FPS` | Turok: "up to 120 FPS" / uncapped | **Preset grid**: 0/30/60/120/144 (step = next preset, wraps to Uncapped); value text already `N FPS`/`Uncapped`. Decision: preset grid vs free 1-step (recommend presets; 117 FPS has no purpose). |
| Show FPS | Off/On | both | Keep. |
## 4. Terminology (end-user term = "Profile")
Decision (2026-09-27, UX review): the end-user term for a per-player
game file is **"Profile"** — the per-player bundle of progress +
per-file settings. Rejected: **"game file"** (the instruction manual's
current term) reads, in modern PC usage, as the *program's* own files
("Verify integrity of game files"), not the user's save; **"Save"** was
the fallback (universally understood) but reads as a verb in the row tag
and as progress-only, which these files are not. "Profile" covers both
and fits the dossier styling of the screen.
Final string set (both UIs: front options + F10 overlay):
| String | Where |
|---|---|
| **"Profile"** + value `N` | front options top row (was "Save file: File N") |
| **"(Profile N)"** / **"(none)"** | section-title annotation on save-scoped sections (was "(File N)"/"(no file)") |
| **"(per profile)"** | per-file row tag (was "(save)", then "(profile)", then "(game file)") |
| **"Select a profile"** | F10 unavailable value (was "Select file") |
**Manual sync TODO:** the instruction manual's "game file" should be
updated to "profile" (the manual is a port document we own).
"Bond file" remains in code identifiers/config keys (`Bond.*` rows,
`watchSettingsChooseFile`, ini keys) only -- never user-visible.
## 5. Gates
1. Port-only per principle 1; the deadzone key migration (principle 3) is
the only config-surface change and follows the D333 alias pattern.
2. `kResetDefaults` updated in the same commit as every default/range
change; `GE_WSPROBE_RESET` (front + `-level_33`) re-run, all sections
`failures=0`.
3. Build via `build-pc-cmd.bat` (the pi-shell MSYS env gap, D356 note).
4. Visual check owed (maintainer): front options screen + F10 in all five
sections; the overlap fix from the same session (the level-1 save-file
row no longer paints under the first content row).
## 6. Out of scope
- Key rebinding (Phase 4), master volume (M3), output-device selection,
borderless window, per-axis controller sensitivity (PD-style four-knob —
GE's one-knob design is D238/D304).
- Any `src/game` change; the N64 watch keeps its own values and wording.
+129
View File
@@ -0,0 +1,129 @@
# F10 visual refresh — GoldenEye dossier styling (proposal)
Status: **superseded by D363 after user playtest** — the dossier/paper
concept below was implemented as D362, then replaced with a dark
watch-green/Bank Gothic version. This is the historical proposal for a
port-layer F10 overlay proposal, not a replacement for GE's pause watch or the
front-end PC Options screen. Reference surfaces: `src/game/front.c`
`constructor_menu15_cheat`/`frontSetupMenuBackground`, the existing
`port/src/frontoptions.c` cheat-list-inspired options screen, and GE's in-stage
watch. Current overlay: `port/src/optionsoverlay.c` `optionsOverlayEmit`,
`overlayRowAtY`, `sliderBarSpan`, `maxVisibleRows`.
## 1. Direction and boundaries
Make F10 read as an **MI6 dossier laid over the game scene**, not a generic
SDL debug panel: dark translucent scrim, warm paper/ivory content card,
black Zurich-style menu text, red for active options/slider fills and an
occasional muted gold title/accent. The front-end PC Options already uses
GE's actual 3D folder and Zurich text; use it as the visual reference, not
as a display-list dependency. In-stage F10 must remain an inexpensive flat
2D overlay: no loading a 3D wallet model, new ROM assets, front-end init
functions or `src/game` modifications. Preserve the current two-level
category -> settings navigation and the shared settings/commit API.
The initial port-only implementation lives in `optionsOverlayEmit()` and
uses one shared `OvLayout` for both 320x240 stage and 440x330 front canvases.
Headless captures live under `scratch/d362-test/` (not distributed). Manual
visual review on a bright stage and controller/mouse feel-check remain owed.
## 2. Concrete visual hierarchy
- **Backdrop/card:** keep an adjustable translucent dim over the scene;
replace the opaque blue-black rectangle with a near-opaque ivory panel
(subtle border and one offset shadow). Do not mimic the actual folder
texture unless a later, measured prototype proves the simple card
insufficient. Preserve legible contrast against both dark and bright
levels, including safe-area crop and widescreen pillarboxes.
- **Masthead:** small gold/black `MI6 / OPTIONS` or `PC OPTIONS` heading,
fine black rule (a restrained `CLASSIFIED` stamp can be added later if
measured text fits) rather than a blue title/instruction block. Keep the
category name on the **same
page above its first setting**, especially INPUT over Mouse sensitivity.
- **Root:** numbered `1. Input` ... `5. Video`, GE's dossier/menu numbering
and title case as on `frontoptions.c`; a narrow translucent dark selection
box behind the focused item. Category order and labels stay unchanged in
the shared row table; casing is presentation-only.
- **Section:** a compact `PREVIOUS`/Back tab, title and optional
`(Profile N)` annotation, then two columns: label at left, value at right.
Unselected values in black, active/toggled values in GE's dark red
(`0xA00000FF` is the cheat-screen reference). Slider tracks are thin
charcoal with a red fill and an obvious knob; show the numerical value
outside the bar. A mixed-scope `(per profile)` marker must remain legible
without colliding with the value column; measure it and, if necessary,
use a small second line rather than silently removing the distinction.
- **Actions:** Reset retains its two-press/three-second confirmation and
displays `CONFIRM` in red only while armed. Quit remains visually distinct
from resets (and retains its existing activation semantics). Back/Close
affordances should match the actual click targets, not become decoration.
- **Footer:** one quiet, short line for mouse/arrows/controller, with an
explicit `ESC: Back / F10: Close` cue. No newly invented glyphs or fonts
that may be missing from the game's text bank. FPS readout is independent.
Suggested layout sketch (not pixel positions):
```
MI6 / PC OPTIONS CLOSE
-----------------------------------------------
PREVIOUS GRAPHICS
Anti-aliasing 2x
Texture filter Bilinear
Draw distance [------|----] 50/100
...
Reset to defaults [ENTER]
ESC Back / F10 Close
```
## 3. Implementation boundaries
1. Extract a port-only layout helper for fonts, palette,
panel rectangles and spacing; preserve `rows[]`, `rowAdjust`, persistence,
file scoping, reset logic and input bindings. Use GE's loaded Zurich Bold
font for body *only after* validating it exists on both stage and front
contexts; Bank Gothic can remain a fallback/header font. Text clipping
must use the whole viewport as `textRender` expects.
2. Replace only the fill/text passes in `optionsOverlayEmit()` first. Emit
fills before text as currently, reset GBI state explicitly and keep
`OV_BUF_CMDS`/DL size bounds. Avoid mutable texture uploads or new assets.
3. One shared geometry calculation must drive panel/card positions,
`maxVisibleRows`, row Y, slider-bar span, close/back hitboxes, and
`overlayRowAtY`. In particular, don't change visual row spacing without
updating hit-testing (D304/D316), and don't use naive OS-window pixel
mapping instead of `gfx_get_ui_screen_rect` (D316/D335). When a page
scrolls, keep its section title visible; a clipped row cannot be clicked.
4. Use measured text widths to reserve space for values, `Confirm`,
`(Profile N)` and restart markers. At 320x240, allow scrolling/short
contextual hints, not overlapping text or tiny glyphs. At 440x330 and
ultrawide, centre or constrain the card rather than stretching the
columns across the entire viewport.
5. Make any GE-like UI sound effects opt-in to a *separate* follow-up: F10
runs in controller polling, and D361 prohibits game save/audio calls
from that thread. A cosmetic refresh must not reintroduce that lockup.
## 4. Acceptance gates
- Compare screenshots with the existing front Options/cheat dossier and the
in-stage watch; user reviews the live palette/layout before closing the task.
- Native 320x240, front-end 440x330, windowed 4:3, 16:9 and 21:9,
fullscreen, safe-area crop on/off: INPUT title visible; no text clipping,
unreadable values, wrong hitboxes, phantom/duplicated bottom rows or
overflow in the display list. Check graphics's longest labels and AUDIO's
per-profile markers with and without a valid file.
- Mouse click/drag/wheel, arrows and D-pad/stick, ESC back, F10/Start/X
close, focused/armed reset, and quit action all behave **exactly** as
before. F10 Audio on file select still commits via D361's game-thread
queue; test both fresh and existing EEPROM without altering the user's
save (backup/restore for probes).
- With overlay closed, append no panel DL and retain existing golden frame
hashes (unless FPS display is explicitly enabled). Run `./build-pc.sh
ntsc-final`, link/duplicate-symbol checks, front-end + stage smoke,
`GE_WSPROBE_F10FRONT` and targeted live playtest. Log the result in
`docs/dev/findings.md` after implementation, not in advance.
## 5. Explicit non-goals
No fullscreen 3D folder in gameplay, changes to the N64 watch, new config
keys, row wording/value changes, save-format changes, controller remapping,
new persistent assets, or `src/game` behavioral edits. The front-end PC
Options screen is a **visual reference**, not part of the first styling
patch; harmonizing its tiny decorative details is a later optional pass.
+45 -10
View File
@@ -3,7 +3,7 @@
Every `getenv("GE_…")` in the tree. The table prose is hand-curated;
**`tools_pc/gen_env_probes.py`** re-greps the live sites and reports drift
(NEW / GONE vars + a fresh file:line map) — run it before trusting the
File:line cells. Last reconciled 2026-09-20 (v0.3.0 release review). All are **env-gated**: unset = zero behavior
File:line cells. Last reconciled 2026-09-28 (v0.4.0 release sweep: rows added for probes introduced since 09-21 — D383 bind-capture smoke, D388 option-tree walk, D406 front-options diagnostics, D354/D344 watch-settings & quit-frame harnesses, D318B boundary suite, z-fight grid; the drift checker now also sees the cached env-flag wrapper reads from `port/include/envflag.h`). All are **env-gated**: unset = zero behavior
change, so they are safe to leave in a release build (they only cost a
`getenv` on the guarded path). "Dead" below means *the finding it was built
for is closed* — the probe is strip-candidate scaffolding, not that it does
@@ -20,7 +20,7 @@ Two classes:
| Env var | File:line | What it does | Status |
|---|---|---|---|
| `GE_PCDUMP` | `port/fast3d/gfx_opengl.cpp:1079` (`is_pcdump`), `gfx_opengl.cpp:700`, `port/src/video.c:188`, `port/src/config.c:46` (`configGetFrameDump`) | Per-frame framebuffer PPM dump to `$PCDUMP` dir (verification-ritual golden captures, `tools_pc/framediff.py`/`pixcount.py`). Also settable via `[Debug] FrameDump` ini (env wins). **M-33/D168: the writer now emits rows top-to-bottom — captures before that fix are vertically flipped.** | **live** |
| `GE_INPUTSCRIPT` | `port/src/input.c:306` | Headless scripted controller-0 input: `"<frame>:<tok>,…;…"` (`SUP/SDOWN/SLEFT/SRIGHT/SNONE` sustain, buttons pulse). Sole input source when set. | **live** |
| `GE_INPUTSCRIPT` | `port/src/input.c:306` | Headless scripted controller-0 input: `"<frame>:<tok>,…;…"` (`SUP/SDOWN/SLEFT/SRIGHT/SNONE` sustain, buttons pulse; `ZHOLD`/`ZREL` sustain fire, D207 M-201). Sole input source when set. | **live** |
| `GE_INPUTLOG` | `port/src/input.c:329`, `port/src/config.c:53` (`configGetInputLog`) | Logs computed pad state (buttons + stick) each poll when non-zero. Also `[Debug] InputLog` ini. Also traces D165 `menuptr est/tgt/eff/stick`. | **live** |
| `GE_STARTMENU` (+ `GE_STARTMENU_PAGE`, `GE_STARTMENU_DIFF`) | `src/game/lv.c:398-402` (`#ifdef PORT`) | Boot straight into a front-end menu id (13=MISSION_COMPLETE, 10=BRIEFING, 7=MISSION_SELECT, 12=MISSION_FAILED, 6=MODE_SELECT); `_PAGE`=folder row (def 1=Dam), `_DIFF`=0..3. Crash-test a screen fast. | **live** |
| `GE_UNLOCK_ALL` | `src/game/debugmenu_handler.c:1098` (`get_debug_enable_all_levels_flag`, `#ifdef PORT`) | Mission-select shows every solo level (playtest jump-to-any-level aid). Cached in a `static int c`. | **live** |
@@ -33,7 +33,6 @@ Two classes:
| `GE_PULLTRACE` | `src/libultra/audio/load.c` (`alAdpcmPull`) | `[PASTEND]` assertion: fires if a voice's `f->sample` exceeds its wave's total samples while `nOver == 0` (i.e. an unstopped voice keeps decoding past the end instead of zero-filling). Rate-limited to 40 lines. M-65: 0 hits — exhausted voices go silent correctly. | **live** (D202) |
| `GE_DMEMWIPE` | `port/src/mixer.c` (`aClearBufferImpl`) | Zeroes the whole software DMEM at the frame boundary (detected as `alMainBusPull`'s clear of `AL_MAIN_L_OUT`, addr 1088). Tests whether audio state survives across frames in DMEM. M-65: no-op on output — ruled out. | **live** (D202) |
| `GE_NOWET` | `port/src/mixer.c` (`aSetVolumeImpl`) | Mutes the reverb send (`wetamt = 0`), isolating the reverb delay lines as a possible source of persistent signal. M-65: drone unchanged — ruled out (NB `aPoleFilterImpl` is still an unimplemented no-op, D199). | **live** (D202) |
| `GE_OBJT` | `src/game/chraction.c` (`chrSetStageFlags`/`chrUnsetStageFlags`), `src/game/propobj.c` (GAS_RELEASING destruction) | D321 stage-flag writer probe: logs every low-16-bit write to `objectiveregisters1` (tick, writing chr, actiontype/aioffset, AI-list ID) plus each toxic-gas prop destruction timestamp — used to map the Facility tanks→combat-bit trigger chain. Capped, read-only, inert unset; post-release removal candidate (M-192 item 7). | **live** (D321) |
| `GE_BANKDUMP` | `port/src/romdata.c` | `BANKSRC` lines: the source ROM-layout bank tree per `soundIndex` (instrument → sound → envelope pointers + `decayTime`). Used to prove sound 203's `decay=-1` is genuine ROM data, not a conversion bug. One-shot at load. | **live** (D202) |
| `GE_FORCEALARM` | `src/game/propobj.c` (`handle_alarm_gas_timer_calldamage`, `#ifdef PORT`) | **D207 diag (temporary).** Forces the in-game alarm klaxon (`alarmActivate`) on a frame counter — on ~2–10 s, off, on again — and emits a per-frame `[D207] f=… active=… ptr_alarm_sfx=… playstate=… locked=…` line to `audiotrace.log`. Repro harness for the "alarm SFX overrides other audio" report when a level's own alarm can't be triggered headlessly. Remove when D207 closes. | **live** (D207) — temporary |
| `GE_KEYMAPDUMP` | `port/src/romdata.c` | `KEYMAP` + `ENVSRC` lines: raw source bytes of every `ALKeyMap` / `ALEnvelope` in the ROM-layout blob, for byte-wise comparison against runtime values. One-shot at load. | **live** (D202) |
@@ -42,6 +41,7 @@ Two classes:
| `GE_MIXERTRACE` | `port/src/mixer.c`, `src/audi.c` (`amDmaCallback`), `src/libultra/audio/load.c` (`_decodeChunk`, `alLoadParam`) | Per-opcode mixer trace to `mixertrace.log` (DMEM addresses, ADPCM books, decoded frames, `[DMAREQ]`/`[DMAHIT]`/`[DMAMISS]`/`[BINDTABLE]`). **WARNING: unbuffered per-opcode `fprintf`, ~25 MB/min. It slows the process enough to starve the audio thread — M-63 measured "audio at 13 % of real time" that was entirely this probe's own artifact. NEVER use it for timing-sensitive measurement; use `GE_D204`.** Grep it, never read it whole. | **live** (D202) — timing-unsafe |
| `GE_TEXPITCH` | `port/fast3d/gfx_pc.cpp:924` (`gfx_tex_pitch_fix`, cached) | `=0` disables the D183 defensive de-stride in `import_texture` (default on). RC2/RC3 texture-shear A/B knob. | **live** (RC3 not finished) |
| `GE_TEXRAW` | `port/fast3d/gfx_pc.cpp:1079` | With `GE_TEXDUMP`, also dumps the raw pre-import source bytes (`texdump/rNNN_f…_s…_WxH.bin`) so a decode bug can be told from a source-data bug offline (D183). | **live** (RC3 not finished) |
| `GE_TEXDUMP_ADDR` | `port/fast3d/gfx_pc.cpp` (`import_texture`, cached env check) | D75/M-197: with value `<hex>`, dumps the raw source bytes of **every** import from that exact address (bypassing the 400-dump cap) to `texdump/aNNNNN_WxH.bin` + a `GE_TEXA[…]` log line with fmt/siz/size/min/max/nonzero byte stats. Distinguished "never loaded" from "loaded zeros" for the Nintendo-logo texture buffer (root-caused to the D50 sidecar converter dropping embedded blobs). | **live** (D75) |
| `GE_D219RAW` | `src/game/image.c` (`texReadUncompressed`/`texChannelsToPixels`, `#ifdef PORT`) | Diagnostic-only, print-only (no behavior change): logs the runtime texture decompressor's bitstream header (texnum/format/width/height/compmethod) for every non-zlib wide-pixel image, plus a dump of the first row of decoded texels exactly as stored in the pool (post-`PORT_PIXEL16`, i.e. byte-swapped for the 16-bit family) for RGBA16/RGB15/IA16. No texnum filter — lets any suspect texture be cross-referenced against the offline ROM census (`scratch/d219_census.txt`) without narrowing by hand. | **live** (D219, closed — kept for future runtime-decoder triage) |
| `GE_WRAPFIX` | `port/fast3d/gfx_pc.cpp:3166` (`gfx_set_wrap_fix`) | RC3 test override for the `Video.WrapFix` D74/D167 wrap-block path (`atoi != 0` wins over the ini). | **live** (RC3 not finished) |
| `GE_SAVELOG` | `src/game/file.c:46`, `src/game/file2.c:16` (`SAVELOG` macro), `src/boss.c:749`, `src/game/objective_status.c:297,343` | Traces the campaign save/unlock chain (objective completion → `end_of_mission_briefing` → `fileWriteSave` → EEPROM). Used to confirm D157. `g_savelogObjOnce` gates the per-criterion dump to the `bossReturnTitleStage` call. | **live** (strip once a few more level boundaries are playtested) |
@@ -58,20 +58,48 @@ Two classes:
| `GE_D193B` | `src/game/model.c` (`sub_GAME_7F06D3F4`, `#ifdef PORT`) | Rate-limited (1/97) raw anim root-motion decode trace (D193): `joint=` / `flip=` / `base=` mtx channel index / `frame=` / `tmp=` decoded s16 translation triple from the anim bitstream / `angle=` / `anim` ptr / `bd=`+`bs=` the D32 u32 bitDescriptors/bitStream addresses / `njoints`. M-80 Facility: `base=0`, `angle=0` **always** (root-motion facing delta never non-zero — top lead), `tmp` ≈ (±small, ~1086 hip-height, ±small). Compare `tmp`/`angle` magnitudes against a clean N64 decomp build of the same anim+frame. | **live** (D193) |
| `GE_D250` | `src/boss.c` (`bossMainloop`, wraps `lvlRender()`, `#ifdef PORT`) | Per-wall-second line: `[D250] lvlRender avg=Nus n=M` — average + count of `lvlRender()` (DL-building: culling/animation/scene traversal) calls that wall-second, isolated from `gfx_run()`'s software-RSP interpretation cost (already covered by the existing "frame rendered in Nus" line). Added M-120 to chase D250 (fps stuck ~45-50 on Dam, ruled resolution/MSAA/vsync/build-flags out already) — found `lvlRender` itself is cheap (~0.5-1.5ms avg), so it is NOT the missing ~10-15ms/frame either; the still-unexplained cost must be elsewhere in the per-retrace-message loop (scheduler/thread round-trip, audio, or something not yet isolated by a targeted timer). Cheap; safe for a full playtest. | **live** (D250) |
| `GE_D172` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1` — SETCOMBINE dedup log + non-LOD multitex tri dump; `#ifdef PORT`) | Particle magenta/cyan colour (D172). Logs each distinct SETCOMBINE + active cycletype, and — for a 2-cycle tri that consumes TEXEL1 with `tex_lod=0` and a distinct 2nd tile — the tile fast3d *samples* for each texunit vs what the DL *configured*. **Found D172**: 40/40 particle tris configured tile 1 @ tmem 392 (fire) but sampled tile 0 (smoke) → `smoke*smoke`. FIXED (`gfx_lod_tile_offset` `return rdp.tex_lod ? 0 : i`). Probe kept inert. | **live** (D172 FIXED, probe kept) |
| `GE_D157` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex` + `import_texture`; `#ifdef PORT`) | D219/D252's "rainbow particle" family — bullet-impact spark on Bunker1 door confirmed by screenshot to cycle magenta/green/cyan (M-157). Three sub-probes: original exact-color-match log (`g_BulletSparkColors[]` byte match → geometry_mode/LIGHTING + shaded RGB); **`D157T`** texture-identity-keyed vertex full-path log (tile 0 holds a loaded texture ≤16 KiB in frame window [90,240] → raw `cn[]`, post-shade RGBA, LIGHTING, geometry_mode, combine_mode, tile-0 tmem/fmt/siz/addr/size/line, uv; cap 400 + summary/200); **`D157I`** import census (small textures imported in the window, deduped per source address → addr/fmt/siz/size/tile/tmem). M-158: bug did NOT reproduce in 6 identical dev-box runs — probes left live to capture the full RGBA path on the next occurrence (user Steam Deck run with `GE_D157=1`). | **live** (D219/D252, non-reproducible as of M-158) |
| `GE_D157` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex` + `import_texture`; `#ifdef PORT`) | D219/D252's "rainbow particle" family — bullet-impact spark on Bunker1 door confirmed by screenshot to cycle magenta/green/cyan (M-157). Three sub-probes: original exact-color-match log (`g_BulletSparkColors[]` byte match → geometry_mode/LIGHTING + shaded RGB); **`D157T`** texture-identity-keyed vertex full-path log (tile 0 holds a loaded texture ≤16 KiB in frame window [90,240] → raw `cn[]`, post-shade RGBA, LIGHTING, geometry_mode, combine_mode, tile-0 tmem/fmt/siz/addr/size/line, uv; cap 400 + summary/200); **`D157I`** import census (small textures imported in the window, deduped per source address → addr/fmt/siz/size/tile/tmem). M-158: bug did NOT reproduce in 6 identical dev-box runs — probes left live to capture the full RGBA path on the next occurrence (user Steam Deck run with `GE_D157=1`). **M-201:** F12 opens a ~90-frame full-detail `D157TB` burst; continuous logging cut to a 2000-line baseline + 500 colour-match hits (the uncapped white match ran live play at 4–6 fps). | **TEMP** (D252 fixed M-201; remove after release) |
| `GE_DVM` | `src/game/gunfire.c` (`gunUpdateAndFire` gate dump + `gunRenderFirstPersonGunModels` render dump; `#ifdef PORT`) | 1P weapon viewmodel (D215). `[DVM]` line: the `field_87F` show/hide gate terms + `Gun_hand_without_item` sub-terms + `copy_of_body_obj_header` RootNode/Switches/numRecords. `[DVMr]` line: `87F` + `cmds_emitted` = gfx commands the weapon `subdraw()` produced (healthy ≈ 150; the D215 bug showed **1**). Rate-limited ~1/s per hand. **Found + verified D215** (`renderdata.flags` read 0 → all weapon DL nodes gated off). Probe kept inert. | **live** (D215 FIXED, probe kept) |
| `GE_DTEX` | `port/fast3d/gfx_pc.cpp:1001` | Per-`import_texture` param log (dims / line / size / lod / gen_mipmaps). Used to root-cause RC2 and D159. Kept as an inert diagnostic. | **live** (RC2/RC3 not finished) |
| `GE_TEXDUMP` | `port/fast3d/gfx_pc.cpp:1020`, `port/fast3d/gfx_opengl.cpp:700` | PPM dump of every uploaded texture + a `fmt/siz/palfmt/palidx/pal[0..3]` line. Root-caused D161 (Depot ceiling). Kept as an inert diagnostic. | **live** (RC3 not finished) |
| `GE_D116` | `port/fast3d/gfx_pc.cpp:1710,2362`, `src/game/textrelated.c:265,541` (cached `ge_d116`) | HUD/text "X-mirror" (D114/D116) vbo/uv trace. **D114/D116 CLOSED — NOT A BUG (M-33/D168): the captures were upside-down, not mirrored.** Probe is now dead scaffolding — strip on the next cleanup pass. | dead (D114/D116 closed) |
| `GE_D288` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, right after the D233 trivial-reject guard; cached `ge_d288`) | M-152 diag for the Silo intro stray triangle: logs any triangle whose clip-space NDC bbox covers >15% of the screen or goes non-finite, with each vertex's `w` and `any_behind_camera`. **Investigation in progress** — narrowed to two candidates (an unguarded `w`-division NaN leak in the backface-cull test just below, or the D106/D271 portal-BFS family), not yet root-caused; needs a live/headless `-level_20` run to disambiguate. | **live** (D288) |
| `GE_D236RM` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, unconditional getenv, no state kept when unset) | M-155/pass-10 diag for D236: per-frame (`num_dls`) triangle count of the `0x0c184b50` render-mode class (`other_mode_l` exact match), heartbeat-logged every 300 frames even at zero. **REFRAMED D280's "run-to-run dependent instantiation" as ordinary room-based occlusion**, not a CPU-side gating bug — see `## D236` pass-10 writeup. | **live** (D236 OPEN, kept for future passes) |
| `GE_D236ORDER` | `port/fast3d/gfx_pc.cpp:1844` (`gfx_sp_tri1`, cached `s_d236order`) | D236 pass-12 diag: per-DL sequence log of the tree-class triangles (DL index, screen-space depth, noise min/max census) to test the draw-order hypothesis. Zero cost unset; remove once D236 concludes. | **live** (D236 OPEN) |
| `GE_D236ALPHA` | `port/fast3d/gfx_pc.cpp:1896` (`gfx_sp_tri1`, cached `s_d236alpha`) | D236 pass-14 diag: for the `0x0c184b50` tree-class render mode, logs the combine/alpha state of up to 4000 matching triangles (deduped on combine change) so the captured draw can be cross-checked against the source. Zero cost unset; remove once D236 concludes. | **live** (D236 OPEN) |
| `GE_D236ZFIX` | `port/fast3d/gfx_pc.cpp:1970` (`gfx_sp_tri1`, cached `s_d236zfix`) | D236 pass-12 test override: forces `depth_update=true` for the `0x0c184b50` tree-class render mode — a cheap mechanism test before deciding what a real fix looks like. Off by default; remove once D236 concludes. | **live, test-only** (D236 OPEN) |
| `GE_D236RAW` | `src/game/bg.c:2437` (`#ifdef PORT`) | D236 pass-16 diag: one-shot census of the raw vertex table's alpha-byte distribution (5 buckets + min/max), camera-independent, to check whether any vertex in the whole table exceeds the low ceiling. Zero cost unset; remove once D236 concludes. | **live** (D236 OPEN) |
| `GE_D288` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, right after the D233 trivial-reject guard; cached `ge_d288`) | M-152 diag for the Silo intro stray triangle: frame-gated to `num_dls` 550–755 (M-155), now **skips `any_behind_camera==1` triangles** (M-155 follow-up — the x/w divide is meaningless for w≤0; that subset was pure false-lead noise) and logs the render-mode word (`oml`) so a live capture can ID the culprit asset against level tables. Logs any triangle whose clip-space NDC bbox covers >50% of the screen or goes non-finite, with each vertex's `w`. Bare `-level_20` re-run: 1965 sane hits in the window, still zero for the target color RGB(83,62,44) — consistent with M-155's working theory that the bare boot's scripted camera never reaches the bug state; **headless probing is at its limit, live capture is next**. | **live** (D288) |
| `GE_D245V` | `src/game/sky.c:962` (`skyPortRenderPoly`-adjacent, `#ifdef PORT`) | D245 water-seam diag: per-call min/max of the recomputed S/T texture-coordinate ranges for the water fan vertices (rate-limited), to check whether the seam tracks a tc-shift boundary. Inert unset. | **live** (D245 OPEN) |
| `GE_D245_FIXEDSHIFT` | `src/game/sky.c:946` (`#ifdef PORT`) | D245 test override: pins the water tc shift `k` to the given integer (0..SKY_TC_MAX_SHIFT) instead of the computed per-vertex value, to A/B the allowShift=TRUE behavior. Off by default (unset = current behavior). | **live, test-only** (D245 OPEN) |
| `GE_D252` | `port/fast3d/gfx_pc.cpp:733` (`import_texture`, cached `s_d252`) | D252 rainbow-particle diag: one-shot per (address,size) import log of the tile state (tmem/fmt/siz/addr/size/line) for small textures so a suspect particle tile can be checked against its source bytes by hand. Remove once D252 is resolved. | **live** (D252 OPEN — non-reproducible) |
| `GE_D252POOL` | `src/game/image.c` (`texLoad`), `src/game/initmttex.c`, `port/src/gimgfixup.c` | **TEMP** (M-201): one line per new texture load (pool main/other, free bytes) + `POOL-FULL` events + stage pool range; `D252SYNC: N slots re-synced, M changed` per stage load (the D252 fix at work: M>0 from the 2nd stage on). Load-time only, no fps cost. | **TEMP** (D252 fixed; remove after release) |
| `GE_D245_OLDVTX=1` / `GE_D245_OLDTEX=1` | `src/game/sky.c` (`skyPortRenderPoly`) / `port/fast3d/gfx_pc.cpp` (CI8 import + tri-path dims) | **A/B** (M-201): restore the pre-fix s16 sky/water vertex path / the pre-fix 32x43, 16-wide water texture geometry. | **TEMP** (D245 fixed; remove post-release) |
| `GE_DYNTEXHASH_OFF=1` | `port/fast3d/gfx_pc.cpp` (`import_texture` cache key) | **A/B** (M-201, D341): disable the content-hashed cache key for dyn-pool textures (reverts to the stale address-only key). | **TEMP** (A/B switch) |
| `GE_D207S` | `src/game/gunfire.c` (player shot-sound block, `#ifdef PORT`) | **TEMP** (M-201): one line per player shot-sound decision: `D207S t=<g_GlobalTimer> hand item rate -> H1|H2|SKIP` (SKIP = both per-hand sound handles still held). Read-only, uncapped, cheap; use instead of `GE_AUDIOTRACE` for live captures (that probe does file I/O on the audio path). | **TEMP** (D207/D241) |
| `GE_D306C="f0 f1 [wmax]"` | `port/fast3d/gfx_pc.cpp` (after `zf_note_emit`) | **TEMP** (M-201): depth-state census of every emitted triangle in frames f0..f1 (GL depth mode, geometry_mode, other_mode_l, tile-0 texture, NDC centroid, z/w range, min w); optional `wmax` keeps near geometry only. Cap 60000 lines. | **TEMP** (D306 closed; remove post-release) |
| `GE_INTROCAM=<n>` | `src/game/bondview_r.c` (`#ifdef PORT`, intro camera pick) | **TEMP/test-only**: force the level intro's first camera index (Dam truck shot = 5). The PRNG draw is still consumed. Logs `D306: intro camera index` when set (or with `GE_D306C`). | **TEMP** (useful repro tool) |
| `GE_STARTWEAPON=<ITEM_IDS>` | `src/game/bondview_r.c` (`#ifdef PORT`, after the start-inventory setup) | **TEMP/test-only** (M-201, D303): adds the item + 400 rounds of its ammo and makes it the starting right-hand weapon (M16 = 13). Pair with `GE_INPUTSCRIPT` `ZHOLD`. | **TEMP** (useful repro tool) |
| `GE_D235` | `src/boss.c` (per-stage `while (!done)` block, after `memaReset`; `#ifdef PORT`) | D235 evidence probe: logs how many GL-texture-cache entries the per-stage `videoResetTextureCache()` call purges at each level load. A cold boot into a level should log 0; a death/restart (or any transition from a level that uploaded textures) logs >0 — direct evidence of the stale-entry mechanism. The clear itself is **not** gated on this var (it's the fix, see below); the var only controls the log line. | **live** (D235) |
| `GE_D235_OFF` | `src/boss.c` (same block) | **TEMP** A/B switch: skips the per-stage `videoResetTextureCache()`, i.e. reverts the D235 fix for one run. For comparing a restart with and without the fix. | **TEMP** (D235; remove when D235 closes) |
| `GE_D235_RELOAD=<frames>` | `src/boss.c` (same block + `bossMainloop` retrace branch) | **TEMP** reload harness: once the title stage loads after the first level (e.g. after a scripted watch Abort), wait `<frames>` retraces, then re-enter that level via `bossSetLoadedStage()`, the front end's own mission-start call. A headless death/restart without the pointer-menu walk. Pair with `GE_INPUTSCRIPT="800:START;1200:A;1300:RIGHT;1400:A"` (abort) on `-level_XX`. | **TEMP** (D235; remove when D235 closes) |
| `GE_D235_NEXT=<stage id>` | `src/boss.c` (with `GE_D235_RELOAD`) | **TEMP**: re-enter a *different* stage after the abort (level A → title → level B), used for D336 (Runway → Frigate). `GE_D85TEX=1` now also logs each stage's texture-pool size and address range (`src/game/initmttex.c`). | **TEMP** |
| `GE_D320R="tick:action;..."` | `port/src/d320repro.c` (hooked in `src/game/lv.c` per-tick PORT block) | **TEMP** forced-repro harness keyed on `g_GlobalTimer`. Actions: `roster`, `jump=<chr>,list=<hexid>,off=<n>`, `bit=<hex>`, `trace=<chr>`, `rename=<chr>,<num>`, `seed=<hi>,<lo>` (D320); `explode=<type>,<dist>` (explosionCreate ahead of Bond) and `cheat=gl|rl|inv|<id>` (game cheat activation) for D252 headless explosion/debris captures. | **TEMP** (D320/D252) |
| `GE_AIMLOG=1` (+ `GE_INPUTSCRIPT` tokens `MDX<n>` `MDY<n>` `RHOLD` `LHOLD` `RREL`) | `port/src/input.c` | D337 aim-model harness: scripted sustained mouse deltas (treated as a grabbed mouse) and aim holds, plus a per-poll line with aim state, active device, the game's drawn crosshair x, `crosshair_x/y_pos` and camera angles. Measures range, edge-scroll and jitter headlessly. | **live** |
| `GE_PERFSTAT=1` | `port/fast3d/gfx_pc.cpp` (`gfx_run`/`gfx_end_frame`), `port/fast3d/gfx_opengl.cpp` (start/end_frame), `src/boss.c` (main loop) | #92/D339 perf probe: per-300-frame averages of render phases (pre / display-list / post / swap+pace), tris and draw batches per frame, game-thread busy time per scheduler message, and a GL_TIME_ELAPSED GPU timer (meaningful only when GPU-bound). | **live** |
| `GE_D75V` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex`, end of the copy/transform loop; cached env check) | D75 (Nintendo logo) per-batch vertex probe: for each `G_VTX` batch in frames `lo..hi`, logs frame#, source pointer, n_vertices, original dest_index, raw ob[0..2] of the first vertex, min/max of the transformed x/y/z/w across the batch, and the MP translation row. Used to prove the transform is correct (M-195) and that every batch encodes v0=0 (slot-overwrite hypothesis). Value form `"lo-hi"` (e.g. `300-900` = the logo screen); unset = off. | **live** (D75) |
| `GE_D75D` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex` G_VTX case + `gfx_sp_tri1` rejection/emit gates; cached env check) | D75 (Nintendo logo) per-frame draw-path probe (M-196): for frames `lo..hi`, logs one line/frame with the V/T token stream (`seq`), max triangle vertex index, cumulative vertices loaded, and per-triangle accounting — trivial-reject / backface-cull counts, how many tris reach GL, how many of those are on-screen (NDC bbox ⊂ [−1,1]²), invisible-flag count, vertex-color min/max, first oml word, depth z/w range + NaN count, texture-sampler presence (`utex` = GL shader, `cutex` = combiner), combine mode, prim/env color. Used to REFUTE the slot-overwrite hypothesis (DL is streamed, max index 15) and narrow the bug to a texture-modulate material rendering black. M-197 added logo-mode `lg_*` fields (sampled only on tris with the logo combine word — the frame-level `utex`/`cutex` are first-tri-wins and were the source of the "combiner claims 0" false anomaly): `lg_utex`/`lg_tex` + tile-0 descriptor + texture buffer address/id/size for the logo material. Value form `"lo-hi"` (logo window ≈ `244-320` on a bare front-end boot); unset = off. Companion one-shot `D75DCC:` log in `gfx_generate_cc` fires when `combine_mode==0x009ffe4f19ffe4f1` to decode the logo's combine slots. | **live** (D75) |
| `GE_PDMOUSEAIM` | `port/src/input.c:487` (`pdMouseAimEnabled`) | Force on/off the PD-style mouse-aim model (D194 lineage); unset = default (on in N64 aim mode unless legacy GEPD aim is selected). | **live** |
| `GE_QUITFRAME` | `port/src/video.c:966` | D344 harness clean self-quit: exits after frame `<n>` completes and swaps (replaces `timeout` kills; the host never exits under an in-flight frame). Cached. | **live** (harness) |
| `GE_ROOMPOOL` | `port/src/video.c:371` (`portRoomPoolScale`) | Scales the port's room pool by a float factor — before/after captures, and a user opt-out if the larger pool ever causes trouble on a level. | **live** |
| `GE_BINDPROBE` | `port/src/input.c:885` | One-shot dump of the resolved PC bindings after setup (D383 bind-capture verification). | **live** (diagnostic) |
| `GE_BINDCAPTUREPROBE` (+ `GE_BINDCAPTUREACTION`) | `port/src/optionsoverlay.c:1000,1005` | D383 isolated host→menu smoke probe: when the overlay force-opens, injects a scripted bind-capture keypress (ESC/DEL/F2). Test data dir must be private — capture persists the key. | **live** (diagnostic) |
| `GE_OPTIONSOVERLAY_SECTION` | `port/src/optionsoverlay.c:989` | Isolated screenshot/probe of a nested F10 page by row key (opens that header; never a menu mode). | **live** (diagnostic) |
| `GE_OPTIONSOVERLAY_SELECTBACK` | `port/src/optionsoverlay.c:995` | Screenshot-only: pins the selection to the Back row when the overlay auto-opens, to check it against a normally selected setting on the same page. | **live** (diagnostic) |
| `GE_OPTIONTREEPROBE` | `port/src/optionsoverlay.c:888` | D388: opt-in in-memory walk of the real F10 page tree; writes no settings, binds or profile bytes; restores UI state afterward. | **live** (diagnostic) |
| `GE_OVPOINTERMAP` | `port/src/optionsoverlay.c:1726` | D391 pointer-map diagnostic: on LMB-down (and every 120 samples) logs SDL mouse coords vs GL drawable pixel coords + the hover row, to separate hit-band/scroll errors from coordinate-space mismatch. | **live** (diagnostic) |
| `GE_FOOTERPROBE` | `port/src/optionsoverlay.c:2232` | One-shot log of the overlay footer/help-line metrics (x extents). | **live** (diagnostic) |
| `GE_FRONTOPTIONS_SECTION` | `port/src/frontoptions.c:295` | Isolated headless screenshot of a nested front-end options page by key (changes navigation only; never edits profile or config values). | **live** (diagnostic) |
| `GE_FRONTNAVLOG` | `port/src/frontoptions.c:430,573` | Logs each front-options navigation select (section/row/item number). | **live** (diagnostic) |
| `GE_FRONTOPTS_WIDTHS` | `port/src/frontoptions.c:673` | D406b: one-shot label-metrics log for the options rows (layout diagnostics). | **live** (diagnostic) |
| `GE_FRONTOPTIONS_AUTO` | `port/src/frontoptions.c:818` | D406 test hook: enters the options screen without a mouse click so headless runs (`GE_INPUTSCRIPT` D-pad stepping + `GE_FRONTNAVLOG`) can exercise it. Never active without the env. | **live** (diagnostic) |
| `GE_WSPROBE` (+ `GE_WSPROBE_FRONT`, `GE_WSPROBE_RESET`, `GE_WSPROBE_F10FRONT`) | `port/src/watchsettings.c:616,568,512`, `port/src/optionsoverlay.c:1566` | D354 watch-settings probe family: `WSPROBE[=tick]` logs before/after the queued settings commit one tick apart; `_FRONT[=tick]` drives a headless front-end change scenario; `_RESET` prepares an options-reset after 120 front ticks and verifies persistence once the stage drains; `_F10FRONT` fires a one-shot F10→front poll after the file-select save becomes available (avoids the joyDisablePoll deadlock). | **live** (diagnostic) |
| `GE_ZF` (+ `GE_ZFTOL`, `GE_ZFBOX`) | `port/fast3d/gfx_pc.cpp:1653-1657` | Z-fight grid probe: per tri emit, buckets the centroid cell and flags cells that fight ≥5 times (cap 600 hits); `ZFTOL`=tolerance, `ZFBOX="x0 y0 x1 y1"`=world-box filter. | **live** (diagnostic) |
## Dxx diagnostic probes — finding closed unless noted
@@ -110,7 +138,14 @@ Two classes:
| `GE_RSEED` | `port/src/random.c:134` (`#ifdef PORT`) | Test-only boot-seed override: `GE_RSEED=<hex64>` pins `g_randomSeed` at first seed instead of the wall-clock-derived value — for determinism and N64-parity tests. Inert unset. | **live, test-only** |
| `GE_RSEED_LV` | `port/src/d318watchdog.c:232` (`#ifdef PORT`) | One-shot level-start seed log: logs `g_randomSeed` at the first tick after level start (optionally armed for a specific tick via `<hex>@<tick>`) — ties a saved/loaded game's PRNG state to a level for D318-class repro. Inert unset. | **live** (D318 verification) |
| `GE_D318T` | `port/src/d318watchdog.c:270` (`#ifdef PORT`) | D318 watchdog timeline probe: per-tick log of the Facility ai_22 softlock signature window (c78 list/offset/attack state, c67 health) while the watchdog watches. Inert unset. | **live** (D318 verification) |
| `GE_D322` | `port/src/audio.c` (`audioSetNextBuffer`), `src/snd.c` (`sndD322PoolSummary`) | D322 pool-telemetry probe: one line per 5 s of wall clock with the three pool layers a long session can exhaust — `sfx=count/max` (8-voice SFX soft cap), `evtq=n/64` (event queue occupancy; full = silent post drops, M-71), `pv free/lame/alloc` (24 physical synth voices shared by SFX + music; `-1`s before sndInit), plus the D204 pipeline fields. Cheap (~30 reads/s), campaign-safe; read together with `GE_D204=1`. | **live** (D322 capture) |
| `GE_D318W` | `port/src/d318watchdog.c:136` (`#ifdef PORT`) | **Not a probe — the D318 port-side watchdog itself, ON by default.** Detects the Facility ai_22 hold-pose softlock signature (a latent N64 race, see findings.md D318) for 600 consecutive ticks and re-seeds c78's attack via the game's own fresh-attack entry point. `GE_D318W=0` opts out; logs `D318W:` lines when it acts. **Permanent fix, not strip-candidate.** | **live** (D318 fix — permanent) |
| `GE_D294` | `src/boss.c:498` (`#ifdef PORT`) | One line per stage reset: stage number + current MA malloc value (D294 pool investigation). | **live** (D294) |
| `GE_D294BANK` | `src/game/bg.c:605` | D294: restarts the bank-window counter on stage change (pairs with the `GE_D104` windowing). | **live** (D294) |
| `GE_D303` | `port/fast3d/gfx_pc.cpp:1717` | D303: logs up to 200 tri emits with per-vertex bbox + w-sum (degenerate-triangle hunt). | **live** (D303) |
| `GE_D307` | `src/game/prop.c:709` | D307: load-time dump of every CCTV record's inputs + derived orientation, for word-for-word PC-vs-ROM comparison. | **live** (D307) |
| `GE_D318B` (+ `GE_D318B_ALL`, `GE_D318B_CLK`) | `port/src/d318bprobe.c:91,97,210` | D318/D320 boundary probe suite (M-199): per-tick ground truth of the aim-hold/derail/anim-pin state machine for env-selected chrs (`"5,9"`, max 4); `_ALL` logs every ACT_ATTACK tick; `_CLK=N` locksteps N sim ticks per rendered frame. Read-only. **TEMP — remove when D318/D320 conclude.** | **TEMP** (D318/D320) |
| `GE_D329` (+ `GE_D329LOG`) | `src/game/model.c:3516,3519` (`#ifdef PORT`) | **The D329 anim-pin fix gate — ON by default** (unlike the other probes here): re-derives a stale pinned frameb when the endframe no longer blocks it. `GE_D329=0` A/B-disables; `GE_D329LOG` logs each trigger (cap 2000). Cached. | **live** (D329 fix) |
## Notes
+8 -3
View File
@@ -10,7 +10,8 @@ once the game is playable start-to-finish without crashes.
|----|---------|----------------|
| ~~**D114/D116**~~ | ~~HUD text + ammo digits render each glyph individually X-mirrored~~ | **CLOSED — NOT A BUG (M-33, §F D168).** The `GE_PCDUMP` PPM writer never reversed `glReadPixels` rows, so every capture was upside-down; "mirrored" was a misread of inverted asymmetric content. The game renders correctly on hardware (developer-confirmed). Writer fixed (`gfx_opengl.cpp`). "Every stage verified non-mirrored" was correct — there was no mirror. |
| ~~**D74 wrap-block**~~ | ~~`gfx_pc.cpp:1546/1551` PORT block is dead code (guard `cms & G_TX_WRAP` always false; indexes per-texunit arrays with the vertex index → OOB if reached).~~ | **RESOLVED (M-30, RC3/D167).** The in-vertex-loop wrap block was reworked and hoisted to a per-texunit pre-wrap window (`gfx_pc.cpp` ~1574, now `cms`-correct and indexed by texunit not vertex), behind the `Video.WrapFix` knob (default OFF). No longer dead code or OOB. See §F "RC3 · D167". |
| **D75** | ~~Nintendo logo mispositioned; gun-barrel Bond model absent~~; cast-roll character models — status unknown. (Rareware logo OK.) | **Shrunk (M-33, upright captures after D168).** The "overlaps D114 shared mirror" note is withdrawn. **Gun-barrel Bond RENDERS correctly** (walk + fire, upright) — the "absent" reports were the flipped capture. **Nintendo logo IS still broken** — renders as two overlapping white ellipsoids shifted left, no wordmark/geometry (a real `logoinst` model transform/geometry bug, not a flip). Cast roll not re-captured — recheck with `GE_PCDUMP="1400-2200:25"`. `findings.md` §F "D75 Bug 2 — M-33 UPDATE". |
| **D392** | In the opening attract-mode Train demo, a soldier consistently looks more hunched on PC than in 1964/GEPD; his hand intersects his body at the wrong position. User comparison: `docs/img/animation-glitch-pc-port.png` and `docs/img/animation-glitch-1964gepd.png`. | **FIXED — user visually verified (D392).** Train's normal opening (`-level_25`) has a central firing guard (`chr=5`) whose torso/shoulder aim offsets reached `aimupback=-0.855`, `shoulder=-0.606,+0.868` radians by tick 100. `chrlvUpdateAimendbackShoulders` used N64 `((f32*)arg1)[12/13/16/17]` offsets into a firing table whose first member widens from a 4-byte to an 8-byte pointer union on PC; it read *other* floats and bent the torso/arms to wrong angles. `#ifdef PORT` now reads `max_up/max_down/free_arm_frac_up/free_arm_frac_down` by name; with the same short Train intro probe the torso offset stays 0, shoulders ~0.028/+0.014 at tick 100. User's original comparison: `docs/img/animation-glitch-pc-port.png` vs `docs/img/animation-glitch-1964gepd.png`. The user subsequently confirmed the soldier's pose looked fixed in-game; this session's image tool itself cannot display PNGs. No ramrom demo override needed; ordinary `-level_25` reproduces. |
| **D75** | ~~Nintendo logo mispositioned; gun-barrel Bond model absent~~; cast-roll character models — status unknown. (Rareware logo OK.) | **Nintendo logo + GoldenEye logo + legal page FIXED (M-197, user-confirmed live 2026-09-21).** Root cause: the D50 sidecar converter (`tools_pc/d43_emit.py`) reserved + remapped the model's embedded texture-blob span but never copied the pixel bytes into the emitted RZ image → TEXEL0 sampled a zeroed buffer → the texture-modulate logo rendered black (M-195/M-196 had proven emission/transform/combiner all correct). Same latent bug hit `PgoldeneyelogoZ` + `PlegalpageZ`. Fix = tooling-only (blob raw-copy + round-trip check); ntsc sidecars regenerated; logo window now renders 23k px (7.59%) vs 0–33 pre-fix. Gun-barrel Bond renders correctly (M-33). Cast roll still unverified — recheck with `GE_PCDUMP="1400-2200:25"`. `findings.md` §F D75 (M-195/M-196/M-197 updates) + M-197 at file tail. |
| **D76** | 2D disclaimer screen only partially drawn. | §F D76. |
| ~~**D77**~~ | ~~No audible music on PC.~~ | **RESOLVED (M-104, PR #42) — in-level music now plays.** Root cause was a D6-class missing `return` in `sub_GAME_7F0C0BF0` (`mp_music.c`) that zeroed the seqp volume on the only in-level trigger path; menu/intro music bypassed it and was always fine. The remaining audio-*quality* gap (wrong-sounding instruments on some tracks — elevator named specifically — plus occasional garbling) is tracked as **D230**. §F D77/D230. |
| ~~**D116 (HUD)**~~ | ~~proportional-font strings + ammo counter mirrored~~ | **CLOSED — NOT A BUG (M-33, §F D168).** Same capture-orientation artifact as D114. |
@@ -25,15 +26,19 @@ once the game is playable start-to-finish without crashes.
| ~~**D233**~~ | ~~All levels (intermittent): near doors — especially doors you can't see through to the inside — the indoor level geometry vanishes and you can see straight through it into the room, while world objects (computer terminals, furniture) in that space still draw.~~ | **FIXED + VISUALLY VERIFIED (M-107 fix, M-108 verify).** `port/fast3d/gfx_pc.cpp` `gfx_sp_tri1` was trivial-rejecting visible triangles that had a behind-camera (`w<0`) vertex. Status cell here was stale at "not investigated" — corrected M-119. §F D233. |
| ~~**D234**~~ | ~~Dam: the guard towers along the actual dam structure are invisible except for the first one.~~ | **FIXED + VISUALLY VERIFIED (M-108).** Same underlying mechanism as D233. Status cell here was stale at "not investigated" — corrected M-119. §F D234. |
| **D235** | Wrong textures load after dying/restarting a level — clarified: not Facility-specific, seen on multiple levels; trigger is death/restart (level reload), not a general build regression. | **Observed, not investigated** (M-106, user QA report, clarified same session). This is a state-leak on level re-entry, not a one-level texture-decode bug — prime suspect is the texture pool / CI-cache (D217/D228 neighbourhood) not fully resetting between a level teardown and reload (stale arena base, a palette-cache entry keyed by a reused address, or a texpool index not rewound). Needs: confirm it does NOT repro on a fresh level boot (only after death/restart); which surfaces; a screenshot. Next: instrument `texInitPool`/the CI-cache clear path for what does/doesn't reset on reload vs cold boot. |
| **D236** | Surface 1: the 2D billboard trees that appear at the start of the level and around the map don't draw as discrete tree sprites — they render as a wall/sheet of tree texture instead of the proper 2D tree objects. | **OPEN — second pass active (D266–D269); first-pass decode fix was reverted after a negative user re-test (D265)** (M-106, user QA report). The earlier IA16 framing was a misidentification: the cards are flat quads by design (no billboard opcode; byte-matched ROM emission) and bind an opaque IA8 64×16 strip plus an alpha-blended **IA4 128×16** strip (texdump r117) that is a row of ~13 faint conifer silhouettes. The wall came from `import_texture_ia4` decoding the RDP's I2:A2 nibble as I3:A1 — A2=1 canopy pixels went fully opaque, A2=2 dithering vanished (bug inherited verbatim from the PD port). **Second pass (D266–D269) re-framed the bug: the visible wall band is CI8 32×32 gradient/blob cards, not IA at all** — two draw classes per room stream (OPA + XLU), BILERP/TEXTLOD/2-cycle/**FOG-on**; texel-alpha blending proven correct by force-alpha A/B (D268). Current ranked suspects: fog range/application (D218 family) > TEXTLOD/mip sampling > draw density/culling > palette. The I2:A2 decode + AC-aware edge fixes are re-applied in-tree (uncommitted) and still owe a game-wide IA4 regression sweep before commit. |
| **D236** | Surface 1: the 2D billboard trees that appear at the start of the level and around the map don't draw as discrete tree sprites — they render as a wall/sheet of tree texture instead of the proper 2D tree objects. | **FIXED (M-198, pass 26) — awaiting user play-test sign-off.** Root cause was a texture-BINDING bug, not decode/alpha/mip/geometry: GE binds the four Surface 1 treeline cards (`tex1198`-`tex1201`, RGBA5551 64x17 cut-outs) as `TEXTURETYPE_DETAIL`, where `texHandleType1` (`tex.c`) loads the *detail* texture at TMEM 0 and the real base image at TMEM `texGetSizeInBytes(detail,0)` on tiles 1+. fast3d's `gfx_lod_tile_offset` folded every `G_TL_LOD` draw to tile 0 (D107, correct for TEXTURETYPE_LOD/MIPMAP), so the port drew the shared detail texture -- `tex2465`, an opaque 32x32 IA8 noise tile, which is exactly the 'noise painter' D267/D276 measured at 1023/1024 texels opaque -- and never imported the cards at all. Fix: when a tile pair differs in both TMEM address and `fmt`/`siz` under `G_TL_LOD`, sample the base image (tile 1). `Video.DetailBaseTile`, default on. Live-verified on Surface 1 (discrete snow-covered pines) and Surface 2; Depot clean; bunker1 golden bit-identical with the fix on/off; `type=1` bindings exist only in sevx/depo/sevb/sev, so every other level is provably untouched. Full trail: `findings.md` D236 pass 26; generalised as `porting-notes.md` §D12.
| **D249** | Streets and Egyptian (the two biggest/most-open levels) show culling problems on far-away geometry at *default* FOV — distant geometry drops/pops incorrectly. Also a QoL ask: improve LOD in general, but gate it behind a toggle so N64-accurate ("old school") LOD stays selectable. | **Observed, not investigated** (M-119, user QA report). Distinct from D222 (high-FOV near-edge culling) and D218 (fog/far-clip tint). Two candidate mechanisms: (a) portal/room-visibility cutting off too aggressively at range on large multi-room layouts (D233/D234 neighbourhood — check for a far-plane/depth-range sibling to D233's w<0 trivial-reject fix), or (b) GE's own LOD/detail-swap system not ported faithfully (mip-tile / simple-model substitution at distance, `gfx_lod_tile_offset` family). First checks: `GE_PCDUMP` a long sightline on Streets/Egyptian where geometry visibly pops; determine which of (a)/(b) is firing before designing the fix or the requested toggle. §F D249. |
| **D217** | 1P weapon viewmodel + character-model CI textures render with a wrong palette on many levels / multiple weapons — PP7 grip + hand render flat green on Facility & Depot, "weird or missing" gun textures elsewhere; Bond's watch face too. Surfaced by D215 (viewmodel now draws). Clean on Bunker1. | **Defect ① FIXED (M-90), pending human playtest eyeball — full detail in `findings.md` §H "D217 — M-90" and §F D217.** Root cause: weapon texpool arena bases land %8==4 on PC (memp bump drift from pointer bloat); `texAlignIndices` absolute 8-byte row padding then shifts every palette 4 bytes early while model-DL `G_LOADTLUT` reads the N64 offset (grip count=1 load lands on next tenant's header slot → `0x1A00` green; larger loads read a 2-entry zero pad — the M-86 tell). Fix: `texInitPool` rounds the base up to 8 under `#ifdef PORT` (no-op on N64); 21/21 sweep PASS. Two defects: ① **visible green = misaligned texpool arena** (fixed as above). ② **CI texture-cache collision** (no palette-content key) — proven, **fix landed on branch `fix/d217-model-texture-palette` `f6fc0399`** (palette FNV-1a hash in the CI cache key), owes a 21-level `verify.sh sweep`; does not fix ①. |
| **D174** | "No blood effect" — user reports no visible blood when shooting guards / into surfaces. | **User QA report; likely D120, unverified.** Decals: the D120 `PointUsage[]` converter fix (M-30) was never interactively verified and only takes effect after a full sidecar regen (`d43 && d69 && d88 --regen`) — check whether the playtest build had regenerated sidecars before concluding the fix failed. Spray: D172 observed the particle spray *drawing* (wrong color, magenta/cyan) on Silo — if the user sees no spray either, that's new info (different weapon/range, or the D172 decode is worse than "wrong color"). First step: BUNKER1 firefight with regenerated sidecars; if decals still absent, instrument the `chr.c:3328` decal walk. |
| **D173** | Third-person Bond model (the player-representing figure shown at level start) spawns too high — floats well above the ground "a lot of the time" while the actual player spawn is correct. Same symptom on the Cuba end credits: JB stands ~6 ft in the air above Natalya. | **Confirmed still present across the full 21-level playthrough (M-49).** **Observed, not investigated** (user QA report). D75 animated-model family but a *position* defect, not absence — M-33 confirmed the gun-barrel Bond renders fine upright. Suspects: (a) intro/credits puppet chr spawn position vs the model's base transform; (b) anim root-motion accumulation (`modelSetAnimFrame2WithChrStuff`, D156 area) offsetting the rendered joints above the body origin; (c) a `coord3d`/packed-float decode in the intro camera/model setup. "Player spawning is OK" localises it to the render-side model, not `g_CurrentPlayer` state. |
| **D182** | SELECT FILE screen degrades after navigating away and back (main menu → file select, or backing out of file select and re-entering). On the fresh screen the 007 gun-barrel spiral / Bond silhouette renders behind the file boxes; after a round-trip **the Bond image stops drawing entirely** and the whole background gains the **comb / interlacing** artifact (horizontal line scramble). File boxes, text and the Copy/Erase icons still draw. | **Observed, screenshot committed** (M-35, user QA; `docs/img/bugs/d182-file-select-backout-20260902.png`). Two symptoms, likely linked: **(1)** background 3D image stops rendering after re-entry → a front-end model/DL whose `render_pos` / segment binding is only valid on first construction (D149 / D75 `constructor_menuXX` family — the dossier/wallet models have the same "renders once" behavior). **(2)** interlacing = **M-36 triage: Family A**, the `import_texture_*` `line` vs `full_image_line` shear (see D176(b) / `docs/dev/M-36-TRIAGE.md`) — **not** the D159 `texSwapAltRowBytes` path (that is a full PORT no-op). Open question: why only *after* a round-trip — first import may land `line == width`, re-import may not, or the tile descriptor is stale on re-entry. Repro: `GE_STARTMENU` to file-select, back out with ESC/B, re-enter; `GE_DTEX` on the spiral texture across both visits. Shares defect (2) with D176(b). |
| **D182** | SELECT FILE screen degrades after navigating away and back (main menu → file select, or backing out of file select and re-entering). On the fresh screen the 007 gun-barrel spiral / Bond silhouette renders behind the file boxes; after a round-trip **the Bond image stops drawing entirely** and the whole background gains the **comb / interlacing** artifact (horizontal line scramble). File boxes, text and the Copy/Erase icons still draw. | **Observed, screenshot committed** (M-35, user QA; `docs/img/bugs/d182-file-select-backout-20260902.png`). Two symptoms, likely linked: **(1)** background 3D image stops rendering after re-entry → a front-end model/DL whose `render_pos` / segment binding is only valid on first construction (D149 / D75 `constructor_menuXX` family — the dossier/wallet models have the same "renders once" behavior). **(2)** interlacing = **M-36 triage: Family A**, the `import_texture_*` `line` vs `full_image_line` shear (see D176(b) / `docs/dev/M-36-TRIAGE.md`) — **not** the D159 `texSwapAltRowBytes` path (that is a full PORT no-op). Open question: why only *after* a round-trip — first import may land `line == width`, re-import may not, or the tile descriptor is stale on re-entry. Repro: `GE_STARTMENU` to file-select, back out with ESC/B, re-enter; `GE_DTEX` on the spiral texture across both visits. Shares defect (2) with D176(b). **2026-09-27 (D397):** user re-reported the comb/interlace on the file-select background AND on the **intro gun-barrel** — the latter is a *first-time* render, so the defect is persistent (not only after a round-trip), refuting the "stale descriptor on re-entry" hypothesis. D159 (odd-row swap) is no-op'd and D183 pitch shear is disproven for level_36 + a no-op for the 299×440×1 row-strip. Next step is a live `GE_PCDUMP` capture + ROM ground-truth decode of the gun-barrel RLE image to separate ground-truth dithered content from a GL sampling/texture-cache artifact. See findings.md D397. |
| **D197** | Character face/head textures wrap wrong — the face texture repeats around the side of the head "and continues weird" instead of clamping to the front. Seen on **Silo** (M-49 playtest), intermittent ("sometimes"). Reads as **`G_TX_CLAMP` not honoured** for the character-skin tile in fast3d (→ `GL_REPEAT`, so the face UV region tiles around the skull), or wrong `shifts`/`shiftt` / LOD-tile selection on the head texture, or the D167 non-PoT wrap-period issue (head textures are often non-PoT) hitting skinned models. | **Observed, not investigated** (M-49, user bug report — needs a capture + which chr/level). Distinct from D176(b)/D182(2) (those are *bg wall* pitch shear). Related: RC3/D167 (`Video.WrapFix`, non-PoT wrap period) + the `texWriteTileLods` `shifts=shiftt=lod` note in findings §F "D176(b) → 4. candidate 2". First checks: does `gfx_pc.cpp` map N64 `cms/cmt` `G_TX_CLAMP` → `GL_CLAMP_TO_EDGE` for character DLs; is the head tile getting the base tile vs an LOD tile (D107); try `Video.WrapFix=1` on a repro. |
| **D195** | Transparency / alpha-blended surfaces render wrong — reported on **Control** (Severnaya bunker basement — Natalya's console room; heavy on glass partitions and the translucent projected/holographic wall displays). Exact failure mode not yet pinned by the reporter (M-49 playtest): candidates are alpha-blended geometry drawing fully opaque, translucent panels drawing invisible, or draw-order/sort so translucent surfaces occlude what's behind. | **Observed, not investigated** (M-49, user bug report — needs a `GE_PCDUMP` capture + which surfaces). fast3d render-mode / combiner path. Same neighborhood as D161 (CI8 tile + `G_TT_NONE` → garbage), D172 (particle-sprite channel order), D176(b) (texture pitch shear) — but this is a *blend/alpha* defect, not a texture-decode one. Note D128 already touched Control's `specialportalarray` (portal struct stride) — check whether the affected surfaces are portal-adjacent. First checks: does `gfx_pc.cpp` honour `G_RM_*` XLU render modes + `G_AC_THRESHOLD`/`G_CC` alpha for these DLs; is `gDPSetRenderMode` `w1` being parsed (cf. findings §F "zbuf-clear w1 scribble"); is Z-write-vs-blend ordering right for XLU surfaces. Get the reporter to say: opaque-where-should-be-clear, invisible, or wrong edges? |
| **D252** | Particle effects cycle through a rainbow palette — bullet-impact sparks and lingering smoke/explosion residue drift through the full hue wheel over time (~10 shots to sweep it) instead of holding their intended grey/orange palette (M-124, user playtest). Full mechanism + suspect in findings §F **D252**: colour comes from 15 distinct `IMAGE_SMOKE_N`×`IMAGE_FIRE_N` (RGBA16) pairs the particles age through; prime suspect is `import_texture_rgba16` deriving width/height from the tile line size (32×7 instead of 16×14 for the fire tiles), scrambling each stage's sprite into a different wrong hue. | **Documented only — not fixed for v0.2.0-pre** (user instruction). Confirm with one dumped fire tile vs raw `IMAGE_FIRE_N` bytes, then derive width/height from the LOADBLOCK dtx/dlh. D172/M-82 lineage. |
| **D323** | Distant geometry on 15 levels samples driver-generated mips from the finest texture level; N64 samples Rare's hand-authored mip chain for explicit-LOD textures bound under `G_TL_LOD` (heaviest: Statue 378 binds, Aztec 806). Overwhelmingly CI8/IA8 — subtle fidelity gap, no reported artifact; the only RGBA5551 cut-outs are the D236 set (already handled). | **OPEN / parked** — mechanism confirmed, census in `tools_pc/bg_lod_audit.py`, fix path documented in findings §F **D323** (upload GL mip chains for TEXTLOD-on bindings). Not worth doing speculatively; revisit only if a playtest flags mushy distant detail. |
| **D335a** | **RESOLVED 2026-09-23.** Gun-barrel backdrop (Bond's spotlight) leaked into the pillarbox bars while sliding in from off-canvas. Fix: `menu_jump_constructor_handler` emits a full-canvas `gDPSetScissor` right after the CENTER wrap (only when wider than 4:3), which fast3d squeezes to the pillarbox, so off-canvas draws clip at the 4:3 edge as the N64 RDP clips at the screen edge. Verified with a 16:9 barrel capture. |
| **D335b** | Native widescreen: the F10 options overlay still stretches with the window (legible, pre-existing look). Pillarboxing it needs `optionsoverlay.c`'s click mapping (`gfx_get_ui_screen_rect`) remapped into the centred region in step. Polish, post-playtest. |
| **#105** | Dam: uncovered water-reflection pixels show the procedural cyan fill instead of the stage sky (residual after the water/wall-reflection work; visible boundary between reflected geometry and sky in the settled Enhanced/Remaster capture). | Backlogged 2026-09-28 from [issue #105](https://github.com/jkdansereau/goldeneye-pc-port/issues/105); post-v0.4.0 graphics queue. Note: the issue's "D329" cross-ref is the issue author's own numbering — our D329 is the anim-pinning fix. |
| **1P weapon viewmodel** | **RESOLVED (M-83, D215).** In-level first-person weapon models drew nothing. Root cause: `gunRenderFirstPersonGunModels` built its `ModelRenderData` via `*(ModelRenderData*)&D_80035CC0` — a reinterpret across two separately-declared adjacent globals that only lands right on the N64 linker + 4-byte pointers; on x86-64 `renderdata.flags` read 0, gating every weapon DL node off in `modelRenderNodeGundl`. All 3 ranked suspects in VIEWMODEL-RESEARCH.md were ruled out first by the `GE_DVM=1` probe. Fixed `#ifdef PORT` (explicit field init). `subdraw` 1→153 gfx cmds; owed one BUNKER1 eyeball. |
## Resolved (for reference)
+11 -4
View File
@@ -47,7 +47,7 @@ Objective status: `src/game/objective_status.c`
- Notes / defects:
```
## 21 solo levels (mission order — numbers = `-level_XX`)
## The 20 solo missions (+ ending sequence; mission order — numbers = `-level_XX`)
| # | Level | -level | Briefing asset |
|---|---|---|---|
@@ -71,7 +71,14 @@ Objective status: `src/game/objective_status.c`
| 18 | Cradle | 41 | UbriefcradZ |
| 19 | Aztec | 28 | UbriefaztZ |
| 20 | Egypt | 32 | UbriefcrypZ |
| 21 | Cuba | 54 | (post-Egypt unlock) |
| — | *Ending sequence* ("Cuba") | 54 | not a playable mission — cast/credits stage (D129) |
(Mission table: `front.c:433`. Cuba = `LEVELID_CUBA`, not in the standard
folder list — unlocked after Egypt.)
(Mission table: `front.c`, `mission_folder_setup_entries` — exactly 20
playable missions; the game's own enum says it, `SP_LEVEL_MAX // 20` in
`bondconstants.h`). "Cuba" (`-level_54`, `LEVELID_CUBA`) is **not** a
selectable mission: after the final mission the front end jumps straight to
it for the extended cast display + credits scroll
(`front.c` `selected_stage = LEVELID_CUBA` → `do_extended_cast_display(TRUE)`;
`bondviewRenderCredits`, D129). Older docs and sweep logs count it as a 21st
"level" — the campaign is 20 missions; "21/21" sweep results mean 21 stage
boots, of which 20 are missions.)
+7 -1
View File
@@ -1,4 +1,10 @@
# LEVEL-STATUS — 21 solo-level load+render sweep (WS4)
# LEVEL-STATUS — level load+render sweep (WS4) — 20 solo missions + ending sequence
**Counting note:** the campaign is **20 playable missions** (`SP_LEVEL_MAX //
20` in `bondconstants.h`; `mission_folder_setup_entries` in `front.c` has
exactly 20 entries). The sweep's 21st boot target, "Cuba" (`-level_54`,
`LEVELID_CUBA`), is the end-of-campaign cast/credits stage (D129), not a solo
mission — so "21/21 PASS" below means *21 stage boots*, 20 of them missions.
Method: bare `./build-pc/ge007.x86_64.exe -level_XX` (per-level `-m*` pools
auto-injected, D121), `GE_PCDUMP="80-260:40"`, ~24 s watchdog then
+45
View File
@@ -0,0 +1,45 @@
# M7 — Reversible AllUnlocked: investigation and fix plan
**v0.4.0 release decision (2026-09-28, user):** ship AllUnlocked as a
**loudly-documented, EXPERIMENTAL, irreversible** option for this release rather
than hiding/disabling it or landing a safe architecture. The toggle stays (default
OFF); the in-game row is now labelled **"All unlocked (EXPERIMENTAL)"** with the ON
value displayed as **"ON - UNSAFE"** in both the F10 overlay and the front PC
Options screen, and the README Known-issues bullet already tells users to back up
`data/ge007.eep` before enabling it. A proper fix (Candidate A port-only shadow or
Candidate B non-mutating virtual query) remains the v0.5.0 work item.
Status: **investigation, NO FIX APPROVED OR LANDED**. Root cause/repro: `docs/dev/findings.md` D387; original feature: D257, fresh-save repair: D281, old-CRC migration: D297. This plan supersedes the M7 "no-code proof" in `docs/dev/notes/MODERN-OPTIONS-PLAN.md`. Follow `AGENTS.md` rule 2 and `docs/dev-process.md` §7 before any `src/game` edit.
## User-visible contract to settle
Recommended: OFF (after restart, as presently documented) must display only genuinely earned mission progress/cheats; ON may *temporarily* expose every level, difficulty, 007 mode and cheat, without modifying durable progress. Achievements **earned while ON without an active cheat** should survive OFF, including difficulty inheritance, timed cheats and profile operations. Enabling/disabling must never silently delete existing genuine save records. Do not auto-repair an EEPROM already containing baked fake unlocks: the bytes lack provenance. The user has not explicitly signed off on a game-code alternative.
## Confirmed facts (source + isolated data)
- `port/src/libultra.c:geEepromRW` first migrates legacy CRCs on the five-slot block-4 read, then `geEepromPatchAllCheats` fills unlocked-cheat bits and every zero completion time with 0x3FF and re-CRCs a **temporary copy**. `fileValidateSaves` (`src/game/file2.c:527`) accepts that copy into `saves[]`. `fileWriteSave` (`file2.c:86`) writes the entire 96-byte live slot, not only the setting/progress that changed.
- Private data under `AppData/Local/Temp/ge-m7-20260927/`: the older `data/ge007 copy.eep` has two genuine times and only cheat `04 00 00` in active folder 0 (slot 4). ON or OFF boot without a save yields identical EEPROM bytes after the same legacy CRC migration. ON plus existing `GE_WSPROBE_FRONT=600` performs an ordinary Music-volume save to slot 4: cheats become `ff ff 0f`, 58 empty times become 0x3FF, two real times remain. OFF + restart validates the written CRC and retains all fakes. Full repro/logs: D387. Current `data/ge007.ini` and `data/ge007.eep` were never opened for write; copies and SHA-256s in that private test directory.
- `main.c` seeds debug unlock flags only at boot; an in-session OFF toggle leaves those RAM flags set until restart. `Game.AllUnlocked` defaults OFF; the relevant UI currently gives no explicit restart warning.
- More than a write-mask problem: a successful mission runs `end_of_mission_briefing` (`src/game/file.c:41–68`) → `fileUnlockStageInFolderAtDifficulty` (`file2.c:828`) → `fileOverwriteSaveSlotWithNewSave` (`file2.c:794`), **rotating the profile to a free EEPROM slot** and freeing the old slot. `fileCheckSaveStageDifficultyTime` only updates if the real time beats the synthetic 0x3FF. Genuine lower-difficulty completions and long (>1023-second) runs can also have legitimate 0x3FF times; value alone is not provenance. Fake cheat bits cause `fileGetIsCheatUnlocked` to return true, so `end_of_mission_briefing` skips a genuinely earned timed-cheat award. A port-only sanitizer keyed by physical slot or by `time==0x3FF` is incorrect.
- The current user EEPROM already has two nonfree slots with 60/60 max times and all cheat bits, consistent with an earlier write-back. Two separate older EEPROM backups exist in `data/`, but replacing or auto-merging them would risk later real progress; do not touch or auto-repair originals.
## Architecture decision before code
**Candidate A — port-only EEPROM shadow/merge:** keep authoritative *raw* progress per logical profile outside patched `saves[]` and translate every write to durable real-only state. Must cover direct settings writes, profile rotation, copy/delete/reset, earned times (including 0x3FF), timed cheat grants currently suppressed by fake reads, five-slot CRCs, and old-CRC migration. Physical-slot-only interception demonstrably fails. A read/write-only adapter may have insufficient information to recognize some genuine completions or cheat awards; prove a full event/provenance model in an isolated prototype before promising this path. No new sidecar that can be lost/desynced without a recovery design.
**Candidate B — non-mutating virtual query:** retire `geEepromPatchAllCheats`, keep raw `saves[]` and EEPROM intact, and make the *opt-in* UI/gameplay availability queries report AllUnlocked without changing saved bits/times. Likely small PORT-gated touch points: `fileGetIsCheatUnlocked` (`src/game/file2.c:391`, existing D225 developer-only query override), `fileIsStageUnlockedAtDifficulty` (`file2.c:681`; cheat-enabled mission selection requires `STAGESTATUS_COMPLETED`, not merely the debug-flag `UNLOCKED`), and the raw earned-cheat check in `end_of_mission_briefing` (`src/game/file.c:41–68`), which must still record real cheat awards while virtual availability is ON. Audit every caller and the Rare/PD analogue first. A live-only toggle may require restoring debug flags safely; simplest contract is **takes effect on restart** and an explicit UI label. This alters opt-in `src/game` behavior and is **not** the ABI/layout exception: no implementation or implicit sign-off. If port-only is ruled out, prepare the exact file/line, reference, reason, scope and precedent required by `docs/dev-process.md` §7; obtain explicit user approval and document the grant in a new RULE-2-SIGNOFF finding before edits.
**Release fallback:** if neither safe architecture is ready, do not present the current toggle as a reversible enhancement. Get an explicit release decision on hiding/disabling it vs shipping a loudly irreversible option; default-off alone does not protect users who toggle it on. Keep existing saves intact.
## Test matrix (private files ONLY)
1. Capture baseline hashes of copied current ini/EEPROM; separate valid older-progress, fully fresh zero EEPROM, already-contaminated, legacy-CRC, and mixed/multiple-folder fixtures. Do not copy old saves back into `data/`.
2. No-write ON boot then OFF restart: raw five slots unchanged except *explicit* D297 migration; correct UI availability in both states.
3. ON settings writes via `GE_WSPROBE_FRONT` (normal slot), in-stage F10 watch writes, and save-file chooser/reset: only real settings/intentional reset persist; CRC valid; no synthetic completions/cheats in raw file.
4. Real mission completion at agent and higher difficulty (including inherited lower difficulty), fast timed-cheat award, slow/capped 0x3FF completion, and cheat-enabled mission selection. Verify actual new earned records after OFF restart, no artificial ones. Build an isolated gameplay harness if manual replay is impractical; do not use a settings-write test as a substitute for mission completion.
5. Save-slot rotation (`fileOverwriteSaveSlotWithNewSave`), profile copy/delete, all four folders plus free slot, and reopen across process restart. Confirm all five CRCs via `GE_SAVELOG=1` and raw bit-level diff; compare copied baseline profile identity rather than just physical slot index.
6. Default-OFF NTSC/PAL/JP builds and post-landing 21-level default-render comparison; config/EEPROM originals hash-verified **after** all tests. Human test of ON/OFF UI text and behavior. Existing contaminated saves need an explicit *separate*, opt-in recovery plan; never silently rewrite them.
## Other wave item
M5 remains under user review: FPS cap and MSAA are independently exposed already; dropping the two-setting Low-end preset is a separate decision, not a dependency of this M7 investigation.
+11
View File
@@ -1,6 +1,11 @@
# In-game PC options menu — design + resume point
Status: **config-system foundation landed (M-35); menu surface not built.**
(Updated 2026-09-27: the menu surface *has* since been built and iterated
as D346 -> D356; the current layout and per-section reset live in
`docs/dev/D356-SETTINGS-REGROUP-PLAN.md` + `docs/dev/WATCH-SETTINGS-PLAN.md`
and `docs/dev/findings.md` D346–D356. This doc remains the historical
M3-scope record.)
Pattern follows `docs/dev/AUDIO-PLAN.md` — a plan doc that a later session
executes.
@@ -85,6 +90,12 @@ or are flagged "restart".
## 4. Alternative if an in-fiction surface is required later
> **2026-09-27: route B chosen (v0.4.0 intake).** The entry point is on the
> file select screen, not a main options menu (GE's front end has none). M1
> landed as D343: an "Options" label that opens the F10 overlay, via one
> Rule-2 `#ifdef PORT` draw hook. M2 (a GE-styled screen that reuses the
> overlay's rows) follows.
Route (B): a new front-end screen `constructor_menuXX_pcoptions` +
`menuXX_pcoptions_navigation` in `front.c` under `#ifdef PORT`, reached from
a new "PC OPTIONS" row on the main options menu. Each row calls a
+1 -1
View File
@@ -33,7 +33,7 @@ ScreenShakeIntensity. Debug: FrameDump, InputLog.
| **DisplayFPS** _(DONE M-83, D213)_ | config `Video.DisplayFPS` | P | S | Top-right readout, config-only; ~0.5 s sample window. |
| **Anisotropic filtering** _(DONE M-83, D212)_ | `Video.Anisotropy` (1–16, dflt 4) | P | S | fast3d already had the GL hook + a hardcoded 4×; M-83 exposed it as config. MipmapFilter / TextureFilter2D still not surfaced. |
| Per-pad tuning: per-stick deadzones, stick scale, rumble scale, device index, swap sticks, C-button mapping | `input.c` padsCfg block | P | M | Medium. Matters for real-controller users; PD pattern is a config section per pad. **M-87 survey addendum:** PD does *incremental* hot-plug (`SDL_CONTROLLERDEVICEADDED/REMOVED` opens/closes individual pads into the first free slot, remembers `deviceIndex=-1` on removal so a replugged pad returns to its configured seat, `input.c:473-491`); GE's port instead does a full rescan-close-and-reopen-everything (`inputRescanPads()`, `input.c:983-1000`) and the game itself latches the controller mask at boot, so a pad added later merges into controller 0 for play. PD's incremental-with-remembered-seat approach is the natural implementation shape for this row and sidesteps the boot-latch merge — fold into the design when this row gets built, not a separate item. |
| **Rumble → D224 (M-87). DEPRIORITIZED (M-87, user call) — gamepad-only, this release targets mouse/keyboard as primary.** GE's port fully no-ops the N64 Rumble Pak (`port/src/libultra.c:1220-1223` `osMotorInit/Start/Stop` all stubbed "no accessories on the PC") — GE genuinely has a rumble subsystem (`src/motor.c`, `src/joy.c`) that never reaches any output. **PD's PC port wires this straight through to real gamepad rumble**: `inputRumbleSupported()`/`inputRumble()` call `SDL_GameControllerRumble()` when the connected pad reports haptics, `Input.PadN.RumbleScale` config + an options-menu slider, `osMemSize`-style "pretend the accessory is there" pattern applied to a real one instead of faked. | P | M | Concrete PD-port precedent, straightforward port-layer wiring, ready whenever gamepad support is prioritized (no `src/` game-logic touch — GE's `motor.c` calls already exist, just need `osMotorStart/Stop` in `libultra.c` to route to a real `SDL_GameControllerRumble` call the way PD's do). |
| **Rumble** _(DONE D401 — D224 / M-87, re-promoted 2026-09-27)_ | `port/src/input.c` + `port/src/libultra.c` + `port/src/optionsoverlay.c` | P | M | D401 implements the PD precedent per `docs/dev/RUMBLE-PLAN.md`: `inputRumbleSupported()`/`inputRumble()` → `SDL_GameControllerRumble()` when the pad reports haptics (SDL ≥ 2.0.18 `HasRumble`, else `SDL_JoystickIsHaptic` + known-type Windows fallback), per-pad haptics detection (`SDL_GameControllerHasRumble` / `SDL_JoystickIsHaptic` + known-type Windows fallback), a single global `Input.RumbleScale` (0..1, dflt 0.5) config + one `Rumble strength` slider row; `libultra.c` sets `CONT_CARD_ON` in `g_contStatus` for rumble-capable pads and routes `osPfsInit` → `PFS_ERR_DEVICE`, `osMotorInit/Start/Stop` → the haptics API — the two M-87 addendum blockers that kept `joyRumblePakInit` from arming. Zero `src/` edits. **Real-pad haptics confirmed working by the user (2026-09-28).** Plan item 3 landed the same day: the original four per-player rows were replaced (user decision) with one global `Rumble strength` slider in the F10 overlay + front options screen (`optionsoverlay.c`, shared `rows[]`), 0.05 detents, `kResetDefaults` 0.5; the four `Input.PlayerN.RumbleScale` ini keys were dropped (stale keys self-clean on the next save). A disabled-when-unsupported row state is deferred with the per-pad tuning row (needs a generic row-disabled mechanism). Unverified: `RumbleScale=0` silence check (trivial early-out). |
| FakeGamepads / FirstGamepadNum / UseHIDAPI | `input.c` | P | S–M | Niche (local MP on PC); defer until MP is actually played. |
| Window polish: DefaultFullscreen/Maximize, CenterWindow, AllowHiDpi, ExclusiveFullscreen | `video.c` vid* block | P | S | HiDpi + center are the useful ones on modern Windows. |
| VSync adaptive (−1..10) | `Video.VSync` range | P | S | Note for FPS plan — GE's 0/1 toggle is fine until then. |
+264
View File
@@ -0,0 +1,264 @@
# Rumble Pak → real gamepad haptics (port plan)
Status: **IMPLEMENTED — finding D401 (2026-09-27/28).** Items 1+2+3 all
landed (port layer only; zero `src/` edits), re-promoting the
DEPRIORITIZED finding **D224** (M-87) to DONE. **Real-pad haptics confirmed
working by the user (2026-09-28).** Menu shape: **one global `Rumble
strength` slider** (`Input.RumbleScale`, user decision 2026-09-28 — the
four per-player rows were dropped). Only the `RumbleScale=0` silence check
remains unverified (trivial early-out path).
Scope: wire the N64 Rumble Pak path (already present in `src/`) through to
`SDL_GameControllerRumble()` on the PC. **Port-layer only — zero `src/game` or
`src/` logic edits**, so rule 2 / ABI-exception do not apply.
Reference: the PD PC port (local checkout `../pd_port`, `port/src/input.c`
+ `port/src/libultra.c`) implements the identical feature for the same Rare
engine family. Copy its shape; the only friction is that **GE's `src/joy.c`
calls the classic libultra names** (`osMotorInit/Start/Stop`, `osPfsInit/
osPfsIsPlug`) while PD's engine renamed them (`osMotorProbe`,
`__osMotorAccess`, `osPfsInitPak`). We translate names, not logic.
---
## Ground truth (current state)
Game side — **already correct, untouched:**
- `src/joy.c` full Rumble-Pak state machine: `joyRumblePakInit` (:178),
`joyRumblePakTick` (:298), `joyRumblePakStart`/`Stop`. Call sites (verified
2026-09-27 for D401) live in `gunfire.c:3284` (start) / `:3288` (stop),
`bondview2.c:9722` (start) / `:9726` (stop, incl. 2P second pad),
`lv.c:1688` (stop at level start), plus `sched.c:272` (stop on level
teardown).
- `src/motor.c` is **EXCLUDED** from the PC build (N64 SI-DMA driver), replaced
by no-op stubs. CMakeLists.txt:525 documents this.
Port side — **all no-oped today, in `port/src/libultra.c`:**
- `osPfsInit` (:1413) → `return PFS_ERR_NOPACK;`
- `osPfsIsPlug` (:1415) → `*pattern = 0; return 0;`
- `osMotorInit` (:1417) → `return -1;`
- `osMotorStart` (:1419) → `return -1;`
- `osMotorStop` (:1420) → `return -1;`
- `g_contStatus[i].status = 0;` (:1102) — `CONT_CARD_ON` **never set**.
Gamepad side — **exists, no change needed:**
- `port/src/input.c` owns `static SDL_GameController *pads[MAX_PADS]`
(MAX_PADS = 4, aligned with `MAXCONTROLLERS`), open/close, hotplug,
`inputComputePad()`. This is the exact object `SDL_GameControllerRumble()`
needs.
## The two upstream blockers (D224 M-87 addendum — MUST land together)
`joyRumblePakInit` (src/joy.c:178) only drives the motor if BOTH pass:
1. `g_ContStatus[index].status & CONT_CARD_ON` → today always 0, so init
never even runs.
2. `osPfsInit(...)` returns `PFS_ERR_ID_FATAL` **or** `PFS_ERR_DEVICE` →
today returns `PFS_ERR_NOPACK`, so `osMotorInit` is never attempted.
If the SDL wiring is done without (1)+(2), the state machine parks at
`RUMBLEPAKINITSTATE_NOT_READY` forever and nothing rumbles despite "looking
wired." Both fixes are in the same file we already touch, so no extra cost.
---
## Work items
### 1. `port/src/input.c` — detection + API
Add per-pad fields (GE uses flat statics, not PD's `padsCfg[]` array):
```c
static int padRumbleOn[MAX_PADS]; // set where pads[i] is opened (line ~582)
static float padRumbleScale[MAX_PADS] = { 0.5f, 0.5f, 0.5f, 0.5f };
```
On pad open (in the `SDL_GameControllerOpen` path, line ~575-589), mirror PD:
```c
#if SDL_VERSION_ATLEAST(2, 0, 18)
padRumbleOn[i] = SDL_GameControllerHasRumble(pads[i]);
#else
padRumbleOn[i] = SDL_JoystickIsHaptic(SDL_GameControllerGetJoystick(pads[i]));
if (!padRumbleOn[i]) {
const SDL_GameControllerType t = SDL_GameControllerGetType(pads[i]);
padRumbleOn[i] = t && (t != SDL_CONTROLLER_TYPE_VIRTUAL); // Win no-haptic fallback
}
#endif
```
On pad close, `padRumbleOn[i] = 0;`. GE has no `inputCloseController`
(PD-only) — the close sites are `inputDestroy()` and the close loop in
`inputRescanPads()`, both of which zero it (`inputOpenPads` re-detects on
open anyway).
New public fns (declare in `port/include/input.h`, define in input.c):
```c
int inputRumbleSupported(int idx); // 0<=idx<MAX_PADS ? padRumbleOn[idx] : 0
void inputRumble(int idx, float strength, float time); // scaled SDL_GameControllerRumble
float inputRumbleGetScale(int idx);
void inputRumbleSetScale(int idx, float v);
```
`inputRumble` body (from PD, verbatim logic):
```c
if (idx < 0 || idx >= MAX_PADS || !pads[idx]) return;
if (padRumbleScale[idx] <= 0.f) return;
if (padRumbleOn[idx]) {
strength *= padRumbleScale[idx];
if (strength <= 0.f) { strength = 0; time = 0; }
else { strength *= 65535.f; time *= 1000.f; }
SDL_GameControllerRumble(pads[idx], (Uint16)strength, (Uint16)strength, (Uint32)time);
}
```
In init, add the two PS4/5 hints (PD input.c:696-697) so BT pads report
haptics:
```c
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
```
Register per-pad config (in the existing `configRegister*` block, ~line 2457+):
```c
char key[48];
for (int c = 0; c < MAX_PADS; ++c) {
sprintf(key, "Input.Player%d.RumbleScale", c + 1);
configRegisterFloat(key, &padRumbleScale[c], 0.f, 1.f);
}
```
Note: **`strFmt` is NOT available in `input.c`** (0 uses, no decl in
`port/include`) — use a local `sprintf` buffer as above. `configRegisterFloat`
is declared in `port/include/config.h` (already included by input.c:80); it's
used elsewhere (video.c) so the symbol is already in the link set.
### 2. `port/src/libultra.c` — rewire the shims
`contSnapshotFromKeyboard` (:1096): set the status bit for connected,
rumble-capable pads (need a per-channel capability check → call
`inputRumbleSupported(i)`; it's declared in input.h so include it):
```c
g_contStatus[i].type = connected ? CONT_TYPE_NORMAL : 0;
g_contStatus[i].status = (connected && inputRumbleSupported(i)) ? CONT_CARD_ON : 0;
```
Replace the 5 accessory stubs (:1413-1420) with real routing (PD libultra.c:235-351):
```c
s32 osPfsInit(OSMesgQueue *q, OSPfs *pfs, int channel) {
(void)q;
// "pretend the Rumble Pak is present" when the real pad can rumble:
// returning PFS_ERR_DEVICE is what joyRumblePakInit (src/joy.c:186-190)
// needs to proceed to osMotorInit.
return inputRumbleSupported(channel) ? PFS_ERR_DEVICE : PFS_ERR_NOPACK;
}
s32 osPfsIsPlug(OSMesgQueue *q, u8 *pattern) {
(void)q;
if (pattern) {
*pattern = 0;
for (int i = 0; i < MAXCONTROLLERS; ++i)
if (inputRumbleSupported(i)) *pattern |= (u8)(1 << i);
}
return 0;
}
s32 osMotorInit(OSMesgQueue *mq, OSPfs *pfs, int channel) {
if (pfs && inputRumbleSupported(channel)) {
pfs->queue = mq;
pfs->channel = channel;
pfs->activebank = 0xff; // PD's osMotorProbe stores this too
// NOTE: PFS_MOTOR_INITIALIZED is NOT defined in GE's headers, and
// joy.c never reads pfs->status — it only checks (osMotorInit()==0)
// to set RUMBLEPAKINITSTATE_READY (src/joy.c:190). Skip the status store.
return 0;
}
(void)mq; (void)channel;
return PFS_ERR_NOPACK;
}
s32 osMotorStart(OSPfs *pfs) {
if (!pfs) return PFS_ERR_NOPACK;
// N64 motor has no duration; the game's joyRumblePakTimer60 (joy.c) turns it
// off. Issue one generous window, PD-style ("hope the timer stops it"):
inputRumble(pfs->channel, 1.0f, 5.0f);
return 0;
}
s32 osMotorStop(OSPfs *pfs) {
if (!pfs) return PFS_ERR_NOPACK;
inputRumble(pfs->channel, 0.0f, 0.0f); // zero-duration / zero-strength = stop
return 0;
}
```
**JPN note (2026-09-27):** `_HW_VERSION_1` defines `MAXCONTROLLERS` as 6
while `MAX_PADS` is 4 — the `MAXCONTROLLERS`-bounded loops above are safe
because `inputRumbleSupported()` bounds-checks against `MAX_PADS` and reports
unsupported (silent) for `i >= 4`. `osPfsIsPlug` has **no GE game-side call
sites** — its rewiring is completeness/hygiene only; the load-bearing stubs
are `osPfsInit`/`osMotor*`.
Headers confirmed: `OSPfs` (`include/PR/os.h:282`) has `queue` + `channel`; the
error macros `PFS_ERR_DEVICE` (:642) and `PFS_ERR_ID_FATAL` (:641) and
`PFS_ERR_NOPACK` (:631) are all defined there. **`PFS_MOTOR_INITIALIZED` is NOT
defined** anywhere in GE's headers and `joy.c` never reads `pfs->status`, so it
is not needed. `input.h` is already included by libultra.c (line 50), so
`inputRumbleSupported` is directly callable there.
### 3. (DONE 2026-09-28) options-menu slider — single global
Landed in `port/src/optionsoverlay.c` (not PD's `optionsmenu.c` — GE's F10
overlay + front options screen share `rows[]`, walked by `frontoptions.c`
via the `optionsRow*` accessors): ONE `ROW_SLIDER` row `Rumble strength`
on `Input.RumbleScale` (0.05 detents, generic `CONFIG_OPT_FLOAT` display
`%.2f`), in the INPUT section after the controller rows, plus a
`kResetDefaults` entry (0.5, the `gRumbleScale` C initializer) so INPUT's
"Reset to defaults" covers it.
**Decision (user, 2026-09-28):** the original four-per-player-row design
(`Rumble (Player 1..4)` on `Input.Player%d.RumbleScale`) was coded and
reviewed, then replaced with the single global row **before its first
compile**: the scale applies to every rumble-capable pad, and the four
`Input.PlayerN.RumbleScale` ini keys are dropped — stale keys in an
existing ini log four harmless `unknown key` `LOG_NOTE` lines at config
load and self-clean on the next `configSave` (which rewrites the whole
file with the registered keys). Per-player rumble tuning stays a G-class
stretch goal (QOL-INVENTORY) — and a per-pad override-on-top-of-global
design would be a no-op for everyone anyway, because `configSave`
rewrites all registered keys and the "user explicitly set this?" signal
is burned as soon as any key exists at the 0.5 default. The
row-disabled-when-unsupported state is deferred with the per-pad tuning
row (the overlay has no generic row-disabled mechanism); the row is inert
(no-op) on non-rumble setups.
**Original plan (for reference):** PD wires the per-pad scale into the
options menu (`optionsmenu.c:533-540`): `inputRumbleGetScale(player)`,
`inputRumbleSetScale(player, v/10)`, disabled when
`!inputRumbleSupported(player)`. In GE this maps onto
`port/src/optionsoverlay.c` / the per-pad tuning row (QOL-INVENTORY.md).
Config file value alone is enough for the feature; the UI is the polish.
---
## Verification ritual (AGENTS.md)
- [x] `./build-pc.sh ntsc-final` compiles clean (2026-09-27). New
`inputRumble*` defined once in input.c, declared in input.h; no new
undefined/duplicate symbols (full `/linkcheck` sweep pending at D401
close).
- [x] `CONT_CARD_ON` + `PFS_ERR_DEVICE` land (the two-blocker addendum);
the state machine's READY path is provable by code read — the runtime
proof is folded into the real-pad test below.
- [x] **By-hand, real pad:** user confirmed the rumble works on a real
controller (2026-09-28) — fire a weapon (`gunfire.c:3284` recoil),
explosion rumble, 2P second pad (`bondview2.c:9722/9726`). Non-rumble
pad and mouse/keyboard-only stay silent by construction (`pads[idx]`
NULL / `padRumbleOn` 0 ⇒ `inputRumble` no-op).
- [ ] `Input.RumbleScale=0` fully silences (unverified — the trivial
`gRumbleScale <= 0` early-out in `inputRumble`); max value loud
(confirmed implicitly by the working playtest at scale 0.5).
## Finding log
Done (2026-09-27): **D401** added to `docs/dev/findings.md` (§F index +
full entry) documenting the D224 implementation + the two-blocker
resolution; the `QOL-INVENTORY.md` M-87/D224 row re-promoted
DEPRIORITIZED → DONE (D401); `docs/dev/findings-index.csv` regenerated.
---
## Effort
- Items 1+2: **~1–2 h** coding (mostly a rename + copy from PD) + build/linkcheck.
- Hands-on pad testing: **~30 min–1 h** (the only genuinely open-ended part —
feels-right tuning).
- Item 3 (UI slider): **~1 h**, landed 2026-09-28 as the single global
`Rumble strength` row.
**Total for working core: half a day max, <2 h of pure code.**
+515
View File
@@ -0,0 +1,515 @@
# v0.4.0 Plan — local working document (NOT pushed; update as items move)
**2026-09-27 user release decision:** defer M7 / AllUnlocked save-safety work
for v0.4.0 (D387); do not ask for a live-save test or treat the feature as
safe. Before shipping, hide/disable the unsafe toggle or explicitly agree
on a release limitation — default OFF alone does not prevent a user from
turning it ON. The user will do a full campaign playtest *instead of* the
21-level automated sweep; record the campaign result as the release gate,
not as a sweep PASS. **Playtest interruption D393/D394:** physical-pad
A use / X reload / Y weapon cycle was user-tested. After the user supplied
the StrategyWiki Xbox 1.1 Jinx mapping, D394 moved crouch to either stick
click, LB to aim, B to a port-side owned-gadget cycle, and retired LB/RB
weapon cycling. Build linked; user physically confirmed the new button roles work, including
stick-click crouch, LB aim, RB no weapon cycling and B gadget cycling (the
N64 port has no separate gadget HUD icon). **D395:** both controller-accessible
binding menus now refuse pad-initiated keyboard/mouse capture and allow B to
cancel an active modal, pending pad-only retest; this is not gamepad rebinding.
**D396:** F10 B now backs/closes, A selects/toggles without changing sliders;
left/right adjusts sliders. NTSC linked; physical pad retest owed. Inspect B's item category/order during campaign play if multiple mission
items are owned: the current ID-range filter includes some mission objects.
RB's remastered-graphics toggle cannot be replicated with N64 assets, and
Back remains the PC options shortcut.
Created 2026-09-21 from a fresh triage of `findings-index.csv`,
`GRAPHICS-BACKLOG.md`, `docs/BACKLOG.md`, `ROADMAP-1.0.md` (M-139 phase order),
`QOL-INVENTORY.md`, and the 2026-09-20 HANDOFF.
**Release goal (user-set):** fix everything that still makes the port not 1:1
with the N64 original; *then* improvements outside the OG game, plus stability.
Sequencing follows M-139's R → Q → F order, split into four phases below.
## Baseline
- **v0.3.0 is shipped** (tag on origin at `4827bc86`); `main` is 7 docs-only
commits ahead. Branch `release/v0.4.0` off `main` when Phase 1 starts.
- All 20 solo missions (plus the ending-credits sequence) load/render/crash-free; campaign completable end-to-end
(Agent, playtested). Steady 60 fps. Audio done. Saves done.
- Known-issue list = README "Known issues" section (D303, D245, D308/
D306, D307, D311, D318 disclosure, widescreen-stretch, D249,
D240/D241, D257/D259/D281 AllUnlocked caveat). ~~D252~~ retired (M-188),
~~D75~~ fixed (M-197, user-confirmed), ~~D236~~ fixed (M-198, pass 26 —
awaiting play-test sign-off).
## Triage of every open item (as of this triage)
### Bucket A — Fidelity (port diverges from N64 behavior/appearance/sound)
| ID | Item | State / notes |
|----|------|---------------|
| D320 | 17 aim-hold AI lists in 8 levels carry the as-authored D318 softlock race; Facility `ai_19`, Control `ai_9`, Depot `ai_12` high/med-high | Static sweep done; **live probe-verification owed** (GE_D318T-style timeline on the 3 lists). If one freezes: generalize `d318watchdog.c` (parameterize list/offset/chr/entityid), not per-list copies. N64 softlocks permanently — watchdog stays a port-side recovery, disclosed like D318. |
| D252 | Rainbow particles still intermittent on both platforms after the vertex-buffer fix | Second cause unrooted; prime suspect `import_texture_rgba16` fire-tile width/height from tile line size (32×7 vs 16×14). README known issue. |
| D245 | Water seam between two patterns, moves with view (REOPENED) | M-142 probe: tc-shift k flips during ordinary rotation; opt-in `GE_D245_FIXEDSHIFT` candidate build-verified, needs A/B vs N64 reference before making it default. |
| D303 | Wrong extra long muzzle flash (M16 + others) | **Investigated — pipeline + data ruled out.** Flash path (`gunUpdateAndFire` → `rwmtx[1]/[2]`, `dorottex` DLPRIMARY arms) is byte-identical game code; M16's `MuzzleFlashExtension`=6.0 is the legitimate max stat; Gm16Z flash-node data byte-faithful through d43. Headless runs can't fire weapons (guards don't shoot an idle player), so a TEMP `GE_D303=1` probe in `gfx_pc.cpp` logs every very-tall-thin emitted triangle with full RDP state — **blocked on a live firing capture** (playtest brief item 17). |
| D306/D308 | Z-fighting: Dam intro/truck wheels + general/broader instances | **Investigated — real fighting confirmed, port-vs-faithful open.** Depth precision is equivalent to N64 (24-bit both sides; GE's RSP writes full 16.16 post-transform coords, not s16), so level-authored coincident geometry is the prime suspect and likely fights on N64 too. TEMP `GE_ZF=1` flicker probe in `gfx_pc.cpp` (recurrence-filtered; `GE_ZFBOX` region filter) shows sustained oscillation (counts ~1000) in the Dam-intro truck region. Closing it as faithful needs an N64 reference of the same spot; a PC-only mitigation (float depth) would diverge from N64 and needs a rule-2-style decision. |
| D307 | In-level security camera props face backwards on some levels (Bunker) | **Investigated 2026-09-21 — no port-side bug found.** Full orientation path traced (`setupCctv` → per-frame `RotY(unkC8)×unk84`); every input verified N64-faithful against the ROM (PcctvZ switch data, boundpads incl. the legitimate load-time room-scale ×1.8542, record config words); all math helpers are byte-identical `src/game` code. Remaining suspect if it genuinely differs from N64: fast3d handling of the non-orthonormal lens matrix. **Blocked on user repro detail** (which camera/level + screenshot + ideally N64 compare). `GE_D307=1` diagnostic kept in-tree. |
| D236 | Surface 1 billboard trees render as a solid wall of texture | **FIXED (M-198, pass 26, `425237f9`):** root cause was a texture-BINDING bug — `TEXTURETYPE_DETAIL` cards drawn from the detail tile (opaque IA8 noise) instead of the base image; `gfx_lod_tile_offset` now samples tile 1 when the pair differs (`Video.DetailBaseTile`, default on). Live-verified Surface 1/2 + Depot; other levels provably untouched. Awaiting user play-test sign-off; probes stripped (`0503d335`). |
| D235 | Wrong textures after death/restart (level reload), multiple levels | **Headless verification 2026-09-23 (`2a6bc2bb`)**: fix clean on reload; bug NOT reproduced even with the fix off. Needs the real die → mission select → restart A/B (`GE_D235_OFF=1`). |
| D288 | Silo intro: huge flat untextured triangle sweeps the frame (frames ~562–741 of bare `-level_20`) | **Does NOT reproduce on the current build (2026-09-23, `30cf76b8`)**; D271 A/B ruled out as the fixer. Close if the Silo playtest does not see it. |
| D294 | Statue: CCCP monument ground renders solid black, self-recovers in 10–30 s | **FIXED pending user playtest** (2026-09-22). Note: its "3× pool corrupts anim" symptom was D336, so the 2× cap can be revisited. |
| D291 | Dam: texture glitches when the truck drives past | Not investigated. |
| D249 | Distant geometry drops out on Streets/Egyptian at default FOV | M-142 ruled out far-clip + LOD-multiplier; narrowed to portal/room-visibility BFS. **First check overlap with D271's portal-culling fix** (branch state, see Phase 0). Needs live screenshot+location. |
| D195 | Control: transparency/alpha-blended surfaces render wrong (glass partitions, holographic displays) | **FIXED** (index: gfx_sp_vertex fix) — plan row was stale (2026-09-23 reconcile). |
| D197 | Character face textures wrap around the head instead of clamping (Silo, intermittent) | **CLOSED** (user playtest v0.2.0-pre) — plan row was stale (2026-09-23 reconcile). |
| D182(2)/D176(b) | File-select background degrades after round-trip (comb/interlacing); Surface tree-line static — shared "texture pitch/shear" defect | M-36 Family A; `import_texture_*` line-size assumption. Distinct from the disproven D183 hypothesis. |
| D176(a) | Sky: RDPHALF tri-raster decode; horizon-band coverage (Defect 2) still open | Path B draft in `scratchpad/sky.patch`, phase-fold fix applied, **never visually verified** — one `-level_22` screenshot owed. Branch `land/d176a-sky-perspective-scroll` exists (forgejo remote) — audit + port to main. |
| D75 | Front-end logos: Nintendo logo degenerate (tiny orbiting fragment); Rareware filtering wrong | **FIXED (M-197, user-confirmed live 2026-09-21)**: D50 sidecar converter dropped embedded texture blobs. Remaining: pal/jpn sidecar regen on ROM-holding machines. |
| D290 | Watch in-game "controller" section shows no control-pad graphic | **FIXED 2026-09-23 (`a5804d10`)**: D264-pattern template read in `watchRenderController` + 3 pointer-width/raw-Gfx sites. Sibling sweep also found + fixed **D331** (shell casings never rendered, `94dff811`). |
| D174/D172 | Blood: decals never interactively verified (D120 needs sidecar regen); spray color fixed but owed an eyeball | BUNKER1 firefight with regenerated sidecars (`d43 && d69 && d88 --regen`). |
| D240/D241 | Gunshot SFX cadence drifts; PP7/AK47 goes silent under heavy fire near looping sounds | D241 high-confidence = D207 reproduced (check D207 entry status first — index row is stale/ambiguous). Fold into the D207 voice-cap family. |
| D322 | Long-session audio degradation (issue #87, v0.3.0 full campaign): from Silo onward audio worsens; by Caverns/Cradle OST inaudible + SFX "come and go"; restart cures → session-accumulating state | OPEN — static triage done (teardown audit clean, mixer stateless, D202 leak class covered only for ownerless `count==-1` loops and only validated single-level; D305 already records a pool/list desync). Ranked: voice-pool exhaustion/counter drift > queue starvation > evtq saturation. `GE_D322` pool-telemetry probe shipped 2026-09-21; needs a campaign capture with `GE_D322=1 GE_D204=1`. |
| D311 (residual) | Upstream anim-poison source still open (freeze class guarded) | **Likely root-caused by D336** (head-bob alias read); headless Caverns clean at 2× pool. Close after one live Caverns combat session. |
| D258 | PAL/JP asset conversion broken at the source-data level | Repair path verified, not applied. Region fidelity — NTSC-only users unaffected, but it blocks "faithful in every region" and any JP/PAL ROM use. |
| D160 (residual) | Cutscene reliability PARTIAL (D243 family mostly fixed via M-189/M-190) | Residual = whatever the live GE_D160/GE_D243 campaign pass shows. |
### Bucket B — Stability (crashes/freezes/hangs; not N64-parity per se)
| ID | Item | State / notes |
|----|------|---------------|
| D289 | Dam computer-room crash (Deck, one occurrence, never repro'd) | Needs a human re-test on a current build (D255+). If it never recurs → close as untracked. |
| D287 | Windows: real, non-self-recovering freeze after ~15–20 min idle + click-back | One confirmed occurrence; `gfxFrameMsgQ` full is a lead. Consider crash-dump wiring on Windows for the next hit. |
| D286 | Deck: ~8 s self-recovering frame freeze on Caverns | One confirmed occurrence, no repro trigger. |
| D175 | Brief stutter/hang (door-open on Runway/Surface) | Matches known transient-hang classes; reproduce under `tools_pc/debug.ps1` idle-box before believing it's new. |
| D285 (residual) | Audio-thread race fix was PARTIAL | Re-check the thread-lock state vs D305's NULL guard coverage. |
| D281 | AllUnlocked breaks audio + RMB aim when enabled with zero saves written | User-facing mitigation shipped; proper fix = make the feature robust or gate it harder (see Phase 3 decision). |
| — | getenv caching in hot paths (**DONE 2026-09-23, `d9d7cd35`**) (post-release cleanup, HANDOFF M-192) | D302 fixed one instance; sweep for stragglers. Cheap perf/stability hygiene. |
| D117 | Frame nondeterminism (GE_DETERM attempted, not achieved) | Parked as a research item; do not block the release on it. |
### Bucket C — Improvements / QoL (outside OG game; after fidelity)
| ID | Item | Notes |
|----|------|-------|
| Widescreen | Currently **stretched 4:3**, not native — README discloses this | **DONE 2026-09-23 (D334 world `d5d9a36a`, D335 HUD/front end `f62bceaf`)**: native at any aspect, default on, F10 toggle; Rule-2 approved. Owed: user HUD playtest at 16:9/21:9; D335a cosmetic. |
| D237 | F10 overlay: mouse-wheel scroll, more settings, category grouping | **DONE 2026-09-23 (`91bd0f84`)**: F10 category headers + 5 more rows (wheel scroll already existed). |
| D314/D315 | F10 flicker (user-deprioritized) + main-vs-in-game menu scaling | D315 deferred from v0.3.0; bigger design question — lands naturally with the overlay work. |
| D238 | Mouse sensitivity rework (master sens, RMB aim sens, Y-gain parity) | Partly superseded: aim modes folded (D337, N64 default via PD integration; Centred (PC) opt-in), Aim range PC/N64 (D338). Remaining: sensitivity feel tuning. |
| D239 | Click-to-lock parity: delete always-grab mode 0 + config key | Small; closes D192 by deletion. |
| GEPD controls (M3) | **IMPLEMENTED, targeted playtest owed (D378/D380/D383–D386).** One effective keyboard/mouse layout, no `Input.Layout` selector: Ctrl crouches without aim; E interacts without reloading; R reloads without interacting. Input → Bindings → Movement/Actions edits two Primary/Secondary keyboard-or-Mouse 1–5 slots in both UIs (D388). Mouse 3 (right) is Aim default; wheel still cycles weapons. Menu arrows/Enter/Escape and mouse clicks remain fixed. See `MODERN-OPTIONS-PLAN.md` M3; earlier preset/B-button text there is historical. |
| Modern controller presets (D394) | **1.1 Jinx button roles physically confirmed; precise gadget category still open**: A action, X reload, B cycle owned gadgets, Y cycle weapons, L3/R3 crouch, RT fire, LT/LB aim, D-pad movement, Start pause. RB updated-graphics and Back scores are XBLA remaster-specific: no HD assets/score view in the N64 port; reserve RB, keep Back→PC options. **Planned** named selector: 1.1 Jinx (L move+strafe, R look+turn), 1.2 Christmas (L move+turn, R look+strafe), 1.3 Frost (L look+strafe, R move+turn), 1.4 Elektra (L look+turn, R move+strafe). Face/trigger roles remain fixed across schemes. Add an `Input.ControllerPreset` enum with default 1.1, F10/front-options selector, preserve per-pad inversion/deadzone, separate keyboard `NaturalPitch` from the pad selector, adapt both stick axes and D-pad per scheme, and verify in watch/menu/tank and physical controller before labeling each selectable. Do not treat these four presets as already implemented. |
| Modern options wave (parity set, 2026-09-27) | **In progress, not a single opt-in Modern page (D388).** M1: Input → Bindings and Gameplay → HUD, shared by F10/front; M2: Original red crosshair plus White/custom RGB, style, size and visibility implemented (D379/D381/D382); M3: single effective layout and rebinding (above). **M5: redundant Low-end toggle removed (D388); Frame rate cap and Anti-aliasing remain independently adjustable, stored values preserved on migration.** M4 GE+ knockback/hitstun/damage-sound/death-cam toggles parked for v0.5.0, not v0.4.0 blockers. **M7 BLOCKED (D387):** AllUnlocked ON can persist synthetic cheats/times after any profile-settings save; OFF does not remove them. Do not run on live saves; a safe design and multi-profile fixture tests are owed. PNG screenshot upgrade dropped from this wave; dev PPM dump remains. The M1–M7 detailed notes retain their historical design record in `MODERN-OPTIONS-PLAN.md`. |
| D226 | Player-adjustable HUD scale (ammo counter, pickup text, dialogue) | **DONE 2026-09-24 (`30720354`)**: Game.HudScale 75–150% (user-verified), fast3d rect scaling about anchors, dialogue/bottom width-capped. |
| Controller rebinding UI (D395) | **Not implemented.** The two Input → Bindings → Movement/Actions pages edit keyboard/mouse slots only. Pad-only users could previously enter a capture modal and get stuck; pad-triggered capture is now disabled in both UIs and B cancels a capture started by keyboard/mouse (NTSC build linked; physical pad-only retest owed). Controller presets (D394) are separate from arbitrary pad rebinding; design an explicit controller-actions page, persistence/migration and capture/cancel semantics before enabling pad capture. | Post-v0.4 scope decision; no promise that keyboard binding slots can hold pad buttons. |
| Rumble (D224) | Wire `osMotor*` → `SDL_GameControllerRumble` (PD precedent exists) | Deprioritized M-87; pick up if gamepad support is in scope for v0.4.0 — user call. |
| Per-pad tuning / hot-plug | PD incremental hot-plug + remembered seats; deadzone/scale/rumble per pad | Medium; real-controller users. |
| Window polish | HiDpi, center window, maximize/default-fullscreen | Small P-class wins. |
| Unlocked FPS | Plan parked; premise changed (D248/D250 fixed — default is now a true 60) | Phase 0 re-measurement before any work; motion interpolation stays out. |
### Bucket D — Infra / housekeeping
- **D244** self-hosted CI runner link failure — fix before the release tag so
CI can build win+linux bundles (or confirm GitHub runners suffice).
- Post-release v0.3.0 cleanup (HANDOFF): remove GE_OBJT probes
(`chraction.c` chrSet/UnsetStageFlags, `propobj.c` GAS line); M-183/M-185
clamp removal pending full-cutscene re-verification; D309 probe decision
(keep for v0.4.0 debugging or remove).
- Working-tree archaeology: ~~uncommitted D236 second-pass state~~ **RESOLVED
before this plan was written — `532092c7`/`6807555c` (Sep 15) already landed
the D266 IA4/texedge decode + D271 portal-culling fixes on main and dropped
the D267–D280 probes.** Consequence: Phase 1 item 9 (D249) can proceed with
the D271 fix in-tree; the D236 tree-wall work continues from D276's oml/fmt=3
lead on top of the landed decode fixes.
## Session additions (2026-09-23/24, local)
- **D336** Frigate "everything breaks but the gun" (uninitialised head-bob alias fields, D100 follow-up): FIXED, user-confirmed.
- **D337/D338** aim modes folded + Aim range option: user-approved ("Seems good").
- **D339** #92 low-end perf: `GE_PERFSTAT` probe; CPU budget measured (~2 ms/frame), GPU the likely bottleneck; needs target hardware.
- **#105** (Enhanced/Remaster water reflection) refers to a `feature/graphics-final-stack` branch that isn't on origin or local; not v0.4.0 scope. Its "D329" label collides with ours, so watch for that if that work merges.
## Phases
### Phase 0 — Hygiene (½–1 session; do before any fidelity work)
1. Branch `release/v0.4.0` off `main`.
2. ~~Resolve the uncommitted D236/D271 tree state~~ already landed (`532092c7`).
3. Remove GE_OBJT probes + playtest-folder `.bat`; decide D309 probe fate;
getenv-cache sweep.
4. Fix/confirm CI runner (D244) so release tooling works end-to-end.
5. Carry-over verifications from the v0.3.0 handoff that were owed:
Runway AllUnlocked toggle re-test, menu-click spot check (D316/D317 both
closed — just confirm no regression on the new tree).
### Release gate (independent of the phases below)
**D322 (long-session audio degradation, issue #87)** is a campaign-breaking
defect in shipped v0.3.0 and does not wait for the phase ladder: the
`GE_D322` probe is already built and smoke-tested — request the campaign
capture (`GE_D322=1 GE_D204=1`) from the reporter now, and run its decision
rules as soon as the log lands. If the capture implicates a port-side leak,
the fix slots into Phase 2; if it's a rule-2 behavioral difference, it gets
its own sign-off item.
### Phase 1 — Fidelity wave (the release's core)
Ordered by (crash risk → visibility → effort); each item ends with its own
verification (headless where possible, human eyeball/ear as merge gate):
1. **D320 live verification + watchdog generalization** — the only open
item that can hard-lock progression on 8 levels; probes already exist.
2. **D289 re-test** (crash class; one human pass, then close or chase).
3. **Batch "one-off geometry/texture anomalies": D288 + D294 + D291** —
all are single-location, bounded, visually documented; likely share a
fast3d import/culling root cause. One investigation thread.
4. ~~**D252 rainbow particles second cause**~~ — **retired (M-188):** the
user's symptom closed as a duplicate of the D219 fix (live retest clean);
decode + dimensions proven correct (M-141/M-156). Re-open only on a
fireball-rainbow re-report.
5. **D245 water seam** — candidate fix exists; A/B against N64 reference,
then flip default (A1-class fidelity call).
6. **D303 + D306/D308 z-fighting family** — small visual parity items; batch
per commit cadence. (~~D307 camera flip~~ investigated 2026-09-21: no
port-side bug found, all inputs verified N64-faithful — blocked on user
repro detail; see Bucket A row.) D303 + D306/D308 both investigated
2026-07-19: no port-side bug found in either (pipeline/data/precision all
verified N64-faithful); capture probes `GE_D303` and `GE_ZF` are in-tree —
both now blocked on user-side captures (firing repro / N64 reference).
Nothing further to do agent-side until those land.
7. ~~**D236 tree wall**~~ — **done (M-198, pass 26):** `TEXTURETYPE_DETAIL`
tile-binding fix in fast3d (`Video.DetailBaseTile`); live-verified,
probes stripped. Only the user play-test sign-off remains.
8. ~~**D235 texture reload leak**~~ — **done (M-193):** port-side
`TextureCacheMap` now cleared on every stage transition via
`videoResetTextureCache()` in boss.c; 21/21 sweep pixel-identical; live
death/restart confirm is a playtest item.
9. **D249 culling** — *after* the D271 overlap check; live screenshot+location
first, BFS instrumentation second.
10. **Audio parity: D322 long-session degradation first** (issue #87 — the
only audio report that breaks a full campaign; `GE_D322` probe already
shipped, get the campaign capture, then apply the decision rules in the
finding), **then D240/D241** (fold into D207 family) + verify D230's bass
fix by ear on the elevator track.
11. **Front-end fidelity batch: D290 watch pad graphic, D182(2)/
D176(b) pitch-shear defect, D176(a) sky Defect 2** (verify the patch
visually first — it's been unverified for weeks). (~~D75 logos~~ fixed
M-197; ~~D195 Control alpha~~ FIXED per findings; ~~D197 face wrap~~
CLOSED at v0.2.0-pre playtest — both were stale in this list.)
12. **D174/D172 blood verification** (regen sidecars, BUNKER1 firefight).
13. **D258 PAL/JP asset conversion repair** — apply the verified repair path;
re-run the region sweep. Decide scope: ship JP/PAL-accurate or document
NTSC-only for v0.4.0 (user call).
14. **D311 upstream anim-poison source** — close out the last open thread of
the freeze class so the known-issue bullet can be dropped.
Exit criteria: every README "Known issues" bullet either fixed or explicitly
re-disclosed with a v0.5.0 pointer; no new fidelity regressions (21-level
sweep after each landing).
### Phase 2 — Stability wave
1. **D287/D286 freeze investigation** — wire Windows crash dumps (or an
in-process fault log) so the next occurrence is diagnosable; pattern-match
against `gfxFrameMsgQ` lead and the D309 teardown-race class.
2. **D175 stutter** — idle-box repro under `tools_pc/debug.ps1`; likely a
known transient-hang class, confirm and close or chase.
3. **D285 residual** — re-audit audio thread-locking vs D305's guard.
4. **D281 AllUnlocked no-save path** — make it robust (root-cause the
sndHandleEvent/aim interaction) or gate the toggle until a save exists.
User call on which.
5. Long-idle soak test (both platforms) as the stability acceptance bar.
### Phase 3 — Improvements / QoL (only after Phases 1–2 are green)
Order by user-visible value (2026-09-27 refresh — several rows since DONE, marked):
1. ~~**Native widescreen**~~ — **DONE 2026-09-23** (D334/D335; playtest sign-off owed).
2. ~~**F10 overlay wave: D237 + D314/D315**~~ — D237 **DONE 2026-09-23**; D314/D315 still open (user-deprioritized; lands with further overlay work).
3. **Input feel: D238 sensitivity rework, D239 delete always-grab, window
HiDpi/center; rumble + per-pad tuning if gamepad is in scope (user call).**
4. ~~**D226 HUD scale**~~ — **DONE 2026-09-24**.
5. Unlocked FPS: re-measure Phase 0 against the post-D248/D250 baseline and
decide whether v0.4.0 or v0.5.0.
6. **Modern options wave — substantially implemented, NOT release-complete**
(D377–D389). Five functional roots; Input → Bindings → Movement/Actions
and Gameplay → HUD work in both menus. One effective PC layout with
dedicated E/R/Ctrl; two bind slots including Mouse 1–5; crosshair
visibility, Original/White/colour, style and size. Redundant Low-end
preset retired, but FPS cap/MSAA remain. M4 GE+ toggles deferred to
v0.5.0, M6 PNG screenshot removed from this release. **M7 blocked by
persisted fake save progress (D387); do not test with a live save**.
Physical acceptance, M7 release decision, 21-level visual gate and
documentation/commit reconciliation are still open. Earlier "one
opt-in section/default-off/preservation proof" estimates were superseded;
the original research remains in `MODERN-OPTIONS-PLAN.md`.
### Phase 4 — Release ritual
1. `./build-pc.sh ntsc-final` clean; `/linkcheck`; full 21-level
`tools_pc/verify.sh sweep --json`.
2. Full campaign playtest sign-off (user), incl. the D320-probe levels and a
water level + Surface + Silo (the Phase 1 visual fixes).
3. Refresh README Known Issues, `.github/release-notes.md`, findings index;
regenerate `agentic-development.md` stats from git (still owed since
v0.3.0 prep).
4. Push `release/v0.4.0` → tag `v0.4.0` → CI draft bundles → validate both
artifacts → fast-forward main.
## Explicitly out of scope for v0.4.0
- macOS/ARM, LAN multiplayer.
- Motion interpolation / sim-rate changes (UNLOCKED-FPS non-goals).
- D117 frame determinism (research-parked).
- **Mod support and HD texture packs — both post-1.0, both researched; see below.**
### Post-1.0, researched and tracked (do not start in v0.4.0)
Both were investigated 2026-09-22 against the Perfect Dark PC port fork
`perfect-dark-dabs-mod-dabs-mod` (**MIT**, Ryan Dwyer — same license as this
tree; `AGENTS.md` already names the PD port a standing reference, so code may be
adapted with attribution). Fuller sequencing notes live in the local
`docs/dev/notes/MODERN-PORT-FEATURES-BACKLOG.md` §5/§6, which is
gitignored — the
durable record is `docs/dev/findings.md` and the summary here. The
drop-in mod support design itself is expanded in the local
`docs/dev/notes/MOD-LOADER-PLAN.md` (2026-09-27: C1–C4 components, v1
scope, acceptance bar).
**Mod support (ROM patches) — viable, medium–large.** Findings **D325–D328**.
- Community mods *repack* the ROM, so vanilla offsets are useless: Goldfinger 64
(12→24 MB, 797/803 slices differ) and even the content-neutral Random Eye
randomizer (identical ROM size, yet 803/803 slices differ — content shifted
+7392). Both die in `tools_pc/d43_emit.py:273` on a vanilla-offset zlib slice.
- **The fix is not a loader rewrite.** PD's `port/src/modimport.c` states our own
constraint verbatim and instead diffs the patched ROM against stock, extracting
what differs into a **mod directory** its loader already reads. The game keeps
running its stock ROM at fixed compiled offsets.
- GE's file table is in the ROM after all (D327 said otherwise; corrected by
D328): 1172-compressed data segment at ROM `0x21990` — one raw inflate, which
`port/src/rzdecomp.c` already wraps — table at `0x252c4`, 727 records of 12
bytes, pointers `0x80020d90`-based, size by differencing adjacent addresses,
exactly `obInit` (`src/game/ob.c:126`). Verified: **681 exact offset+size
matches vs `filelist.u.csv`, 0 mismatches**; discovery needs **no hardcoded
offsets** and reads Random Eye's own table to **704/726 files byte-identical**.
- Remaining work: extraction in C (proven), patch decoding (PD's `rompatch.c` is
an MIT VCDIFF/BPS/IPS decoder — **removes the GPLv2 xdelta3 constraint**),
converters taking the extracted table, and the real effort: a
**mod-directory override layer** — `port/src/romdata.c` reads the ROM only and
has no analogue of PD's `romfile.moddir` file slots.
- **Hard limit:** mods that *add levels* need new stage-table entries in
`src/game` — rule 2, not the ABI/layout exception. Either a sign-off or scope
down to the existing 20 slots. ASM/code mods stay impossible (ROM code is never
executed here).
**HD texture packs — medium, well-scaffolded. Answers issue #100** ("High
Resolution Textures", open, `enhancement` — a user asking whether HD textures can
be used with the port). Architecture studied from PD's
`port/include/texpack.h` + `port/fast3d/gfx_pc.cpp`; no finding written yet, so
this entry and backlog §6 are the record.
- **Identity is the whole problem, not decoding.** The renderer only ever sees
the pointer `gDPSetTextureImage` was handed, so PD keeps an address →
texture-number registry populated where `texLoad()` runs and invalidates it
when the pool drops (a stale entry names the *wrong* texture). Then three-tier
matching: texture number → texel checksum (for textures embedded in model
files) → per-record. Packs are `textures/<texnum>.png`; decode happens on a
worker so loading does not hitch; the hook is inside `import_texture`, gated so
it costs one lookup when off.
- Most scaffolding exists here: our `gfx_pc.cpp` is the same lineage
(`gfx_texture_cache`, `:92,579`), `g_Textures[].texturenum` exists
(`src/game/chr.c:3894`), and **`GE_TEXDUMP` already dumps textures**
(`port/fast3d/gfx_pc.cpp:1205`) — it just names them by sequence
(`r000_f3_s1_64x64.bin`) rather than texture number, so dumps cannot round-trip.
Renaming that is the single change turning the probe into a pack dumper. A PNG
decoder is the one new dependency, and zlib is already required.
- Step 1 (the identity registry) is the only novel design decision, and it
records identity where textures load — a `src/game` touch needing `#ifdef PORT`
plus a §F note. Settle that at design time, not mid-implementation.
## Session log (agent-driven, 2026-09-20 evening)
**Phase 0 — done except user-side items:**
- `release/v0.4.0` cut off `main` (which already carried the D266/D271 fixes,
`532092c7` — the "uncommitted tree state" item was stale; plan updated).
- D322 probe work committed (`7bb43260`); GE_OBJT probes removed (D321 closed,
M-192 item 7, `56ac2ab8`); D309 probe **kept** (inert, cheap, recurrence
value for the D309/D310 freeze class).
- getenv hot-path sweep (D302 follow-up): function-level scan of all GE_*
sites found no frame/triangle-rate stragglers in fast3d; cached four
per-frame/per-hit guards in `src/game/bg.c` (GE_D104 room pass, GE_D63
primary+secondary render logs, GE_D154 inner GDL walk) — all `#ifdef PORT`,
mechanical.
- D244 (self-hosted CI runner): needs `pacman -Syu` on the box itself —
**user-side action**, not locally fixable; fallback (`#ifdef PORT` thin
wrapper for the `#pragma weak` alias) only if the toolchain update fails.
- 21-level sweep gate: running on the new tree (see `scratchpad/sweeps/`).
- Carry-over D316/D317 + Runway AllUnlocked re-tests are interactive → folded
into the big playtest brief below.
**Phase 1 — in flight:**
- Item 1 (D320): generic pin-signature detector added to `d318TimelineTick`
(`GE_D318T=1` now covers any chr: ACT_ATTACK + live-fire attacktype +
byte-stable animframe for 10 s → one `D320T: PIN?` line per chr, repeating
every 10 s while pinned). Detection only — the recovery watchdog stays
hardcoded to ai_22 per D320's own instruction until a second list freezes.
Live verification of Facility ai_19 / Control ai_9 / Depot ai_12 beats =
playtest work (see brief).
- Item 3 (D288/D294/D291 batch): D288 probe refined per M-155 follow-up
(any_behind_camera==0 only — the w<0 bbox math was a false lead; oml word
now logged for asset ID). D294: `GE_D104` room-visibility trace confirmed
sufficient (curRoom/roomsDrawn/budget + per-room load state every 10 s);
needs live Statue repro at FovScale≈130 + max draw distance. D291: still
needs live Dam truck details — lowest value of the three.
- Item 13 (D258 PAL/JP): **deferred pending user scope call** (ship
multi-region or document NTSC-only for v0.4.0) — no PAL/JP ROMs available
here to verify a blind CSV rename against.
- Item 4 (D235 wrong textures after death/restart): **mechanism found + fix
applied (M-193).** M-108's refutation of the game-side CI-cache theory
stands, but the *port* has its own `TextureCacheMap` (source-address keyed,
no content check) that was never invalidated on stage transitions —
`videoResetTextureCache()` existed with zero callers. Now wired into the
per-stage reset block in boss.c (`#ifdef PORT`, after `memaReset`).
Pixel-identical re-uploads; golden sweep running to confirm. Live
death/restart confirmation + a `GE_D235` >0 capture are playtest items.
- D311 upstream poison source (M-194): static pass done — all
`sub_GAME_7F06FE90` callers pass arg2=16.0f (denominator safe); the
old→new lerp only circulates values; `bheadAdjustAnimation` is the one
path where a live player-velocity corruption reaches `model->speed`. Both
speed-write choke points (`modelSetAnimSpeed`, `modelSetAnimation2`) now
log `D311: … ANOMALY` on |v|>1e4/NaN — next Caverns occurrence is a
one-line diagnosis. No env var (D156-guard pattern).
- Item 4 (D252 rainbow particles): **effectively closed — no agent work
left.** Re-read the finding this session: M-141 refuted the width/height
suspect (line=4 → correct 16×14, cross-confirmed by the GE_TEXDUMP
is_fire_shape filter), M-156 proved the ROM bytes decode to a clean
white-hot→ember progression, and **M-188 closed the user's actual symptom
(lingering residue sprites) as a duplicate of the D219 fix** — user
retested live: "seems fine now." Only a *fireball* rainbow re-report would
re-open it. Plan item 4 retired.
- Item 10 (D207 alarm klaxon): **blocked on user repro info, not agent-
workable.** M-77b implemented the designed last-resort LOOPED-preemption
pass + `[D207-DROP]`/`[D207-YIELD]` counters, BUNKER1 forced-alarm soak
showed zero drops / zero yields (pass 1 always finds a victim), and the
change was reverted. The finding now asks for: which level, which
weapon/situation, and a `GE_AUDIOTRACE=1` capture (or clip) of the failing
alarm — then decide between heavy-level 8/8 starvation (re-apply the
reverted pass), an alarm-stop-path leak, or pure perception (klaxon mix
volume vs N64). Added to the playtest brief as a low-effort ask.
- Full 21-level golden sweep: **21/21 PASS** on the final tree (`bb508e5c`),
confirming D235 fix + D311 probe + all Phase 0 changes are pixel-identical.
- **D75 (Nintendo logo) investigation — M-195, no root cause yet but two
suspects eliminated.** (a) Fresh `GE_D63` run: all 23 logo Primary GDLs
emitted every frame, zero unknown-opcode warnings → emission + DL
consumption fine; PC GDLs byte-faithful to N64 source. (b) New `GE_D75V`
per-batch vertex probe: transformed coords match the wordmark geometry
exactly (MP translation matches the lookat) → basematrix / f32→s32 /
matrix-list suspects eliminated. **Leading hypothesis:** every `G_VTX`
batch encodes v0=0, so multi-batch nodes overwrite shared vertex slots
0..n−1 (only the last batch survives; higher-indexed triangles read stale
slots) — consistent with the single tiny orbiting fragment observed. Next:
ROM-dump one node's GDL stream and cross-reference `G_TRI4` index ranges
against per-batch counts; fix would be port-layer fast3d slot management.
Probe code (`gfx_pc.cpp`, `videoGetFrameCount()` in video.c/h) uncommitted
at end of session — commit with the doc updates.
- **D307 (CCTV facing backwards) investigated — no port-side bug found.**
Env-gated `GE_D307=1` probe in `setupCctv` dumps every input + derived
orientation per record. Bunker1 (`UsetupsevbunkerZ`) decode vs live: model
switch offset = `PcctvZ` (NOT `GcameraZ`, that's the gun) sw[0]
bit-for-bit; boundpads match the pccg sidecar once the legitimate load-time
room-scale (`pad->pos *= get_room_data_float2()`, ≈1.8542 here) is applied;
record config words (incl. `unkCC`) match; all matrix/atan2 helpers are
byte-identical `src/game` code. Conclusion: PC orientation values == what
the N64 computes; if the symptom is real vs N64, the remaining suspect is
fast3d's handling of the non-orthonormal lens matrix (`unk84` row 0 scaled
0.1×). Blocked on user repro detail (which camera/level + screenshot).
Findings entry updated; probe kept in-tree (D60/D88 pattern).
- **D303 (extra long muzzle flash) investigated — no port-side bug found yet,**
blocked on live capture. Mapped the full flash pipeline (`gunUpdateAndFire`
builds `rwmtx[1]` = yaw×scale×flashext and `rwmtx[2]` from `Switches[2]`; the
flare arms are `dorottex` DLPRIMARY nodes) — all byte-identical game code,
matrix helpers verified index-for-index. Ruled out: weapon stats (M16's
flashext=6.0 is the legitimate max, sniperrifle matches; the ext≥3 ⊆
CAN_DUAL_WIELD overlap is design coincidence) and model conversion (Gm16Z
DLPRIMARY records + vertex blocks byte-faithful per d43 round-trip). Added a
TEMP `GE_D303=1` probe in `gfx_pc.cpp` (cached-getenv; logs very-tall-thin
emitted triangles with blend/oml/comb/prim/env/w) — 75 s unattended runs on
Bunker1/Dam/Depot show only static scene geometry, so the artifact needs a
live firing repro. Next: user fires the M16 under `GE_D303=1` and shares the
log lines; the spurious quad's RDP state will identify its source.
- **D306/D308 (z-fighting family) investigated — real fighting confirmed, cause
framed.** Verified GE's RSP writes post-transform x/y/z/w as full 32-bit 16.16
(not s16 8.8), so the port's float pipeline + `GL_DEPTH24_STENCIL8` matches N64
depth precision — no port-side precision deficit exists. Built a TEMP
`GE_ZF=1` flicker probe in `gfx_pc.cpp` (96×54 screen-cell grid, per-cell depth
history, requires non-monotonic a-b-a oscillation within GE_ZFTOL recurring ≥5×
at the same cell; `GE_ZFBOX` region filter) — earlier center-pair and simple
flicker variants saturated on tessellation noise. Dam (`-level_33`) unattended:
sustained hits in the lower-left truck region with per-cell oscillation counts to
~970 → D306's "transparent areas" are real depth-test alternation. Prime suspect
is level-authored coincident geometry (fights on N64 too); confirming port-only
needs an N64 reference of the same spot, and a mitigation (e.g. float depth)
would diverge from N64 — parked pending that.
- **Plan/doc refresh:** D75 (M-197) and D236 (M-198) marked fixed throughout
(Bucket A rows, Phase 1 items 6/7/11, playtest brief); findings index
regenerated; README Known Issues to match.
### Big playtest brief (for the user, when the tree is final)
Build: `./build-pc.sh ntsc-final`. Set **`GE_D322=1 GE_D204=1`** for the whole
campaign (D322 pool telemetry — the release gate). For the three D320 beats,
add **`GE_D318T=1`** on: Facility (the captive-scientists monologue/execution
beat), Control (the ai_9 guard beat), Depot (the execution beat) — watch for
`D320T: PIN?` lines; a frozen run keeps printing every 10 s.
Checklist beyond the normal play:
1. **D322**: full campaign, share `ge007.log` after (decision rules in the
finding).
2. **D316/D317 regression confirm**: click menu rows at a few window sizes,
esp. low rows (crop overscan bars) with SafeAreaCrop on/off; toggle
Game.AllUnlocked back and forth in-game.
3. **D289**: Dam computer room — interact with the terminals (was a one-off
crash, likely D255-class, already fixed — confirm it stays gone).
4. **D294**: Statue, set FovScale ≈130 + max DrawDistance/LodDistance, walk
past the CCCP monument at ground level; if the black-ground window hits,
note the time (a `GE_D104=1` run there is the follow-up capture).
5. **D291**: Dam — watch for texture glitches as the truck drives past;
screenshot + which surface.
6. **D175**: door-open on Runway/Surface — note any stutter/hang.
7. **D287**: leave a level idle+unfocused 15–20 min, click back (the
non-self-recovering freeze; one prior occurrence).
8. **D320 beats** as above with `GE_D318T=1`.
9. Audio by-ear: elevator track bass (D230 verify), PP7/AK47 under heavy fire
near looping sounds (D240/D241).
10. **D235**: on any level, die and restart (or reload a save) in the same
session, then look around — textures should be correct (no swapped/wrong
stage textures). Run once with `GE_D235=1` and share the log if anything
looks off (a `>0` count means a stale-cache hit slipped through).
11. **D288 (Silo)**: play Silo normally; if you see the wrong-color target /
geometry anomaly, set `GE_D288=1 GE_PCDUMP=1` for a 30–60 s run around it
and share log + captures (the probe logs the offending oml word for asset
ID).
12. **D311 (Caverns)**: play Caverns ~2 min of normal combat; if the anim-
speed freeze/poison recurs, the log will now carry a one-line
`D311: … ANOMALY` naming the corrupting call site — share it.
13. **D207 (alarm klaxon)**: if gunfire goes quiet during an alarm, note the
level + weapon + situation; ideally re-run that spot with
`GE_AUDIOTRACE=1` and share the log (or a short clip).
14. **D75 (front-end logos)**: confirm the Nintendo/Rareware logos + legal
page still render correctly (fixed M-197; user-confirmed once already).
15. **D236 (Surface trees)**: Surface 1/2 — confirm the treeline renders as
discrete tree sprites, not a wall (fixed M-198; probes stripped).
16. **D307 (security cameras)**: if any in-level CCTV camera looks like it's
facing backwards, note which level + which camera (screenshot) — the
orientation math is verified N64-faithful, so a concrete repro is what's
needed to keep chasing it.
17. **D303 (extra long muzzle flash)**: run with `GE_D303=1` in your terminal,
fire the M16 (and any other gun that shows the extra upward flash) until
you see it, then share the `D303:` log lines from around that moment —
each line carries the RDP state of a candidate quad and will identify the
spurious draw call's source.
18. **AllUnlocked preservation (M7) BLOCKED — D387.** Do not run this
against a real save: even changing a profile setting while ON persists
synthetic cheats and 0x3FF completion times; OFF requires a restart and
does not undo already-written fakes. First define safe earned-progress
semantics, fix in `port/`, and verify on private copies with settings
writes, mission completion, OFF restart and multi-slot CRC checks.
M4 modern toggles are parked to v0.5.0.
## Conventions reminder
- One findings entry per investigation; the index tail moves constantly —
it ended at **D370** as of 2026-09-27 (the F10 drag entry), so new
labels start **D371+** (the old "after D322" note is long stale; always
confirm the tail of `findings-index.csv` before filing).
- Fidelity fixes: port-layer or documented A1/ABI exception only; genuine
behavioral differences need the rule-2 sign-off (`docs/dev-process.md`).
- Every subagent reads `docs/porting-notes.md` first and appends to it.
+63
View File
@@ -0,0 +1,63 @@
# Watch-backed settings in PC Options and F10 — proposal for review
Status: **watch-backed subset implemented (D350), robustness fixes landed (D352), Turok-style regrouping done (D353), front-end F10 routing + transparent default Bond-file selection landed (D354: non-stage contexts use the chooser path, default = explicit pick > `selected_folder_num` > first valid file, with a one-shot `fileBuildWriteNewSave(FOLDER1)` fallback; GE_WSPROBE/GE_WSPROBE_FRONT headless probes), the in-stage BGM resync landed (D355: the Music commit also drives the track-1 BGM player, matching the game's own MP-path resync; by-ear confirmed), and the settings regroup landed (D356: functional sections INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO, PD-style "(File N)" scoping annotations, N64-watch rows off the menu surface, per-section reset-to-defaults = Gate E; GE_WSPROBE_RESET headless probes front + in-stage).** Wider M3 remains outstanding: master volume, key rebinding. PC-only per-file persistence and explicit Bond-file chooser approved; details below. See `docs/dev/findings.md` D350–D356 for verification. This is the watch-integration part of M3, not a replacement for the native watch menu. The older tracked `docs/dev/OPTIONS-MENU-PLAN.md` and local `docs/dev/notes/OPTIONS-MENU-PLAN.md` describe the wider M3 scope (master volume, window mode/display, resets).
## Goal and ownership
Show GE's existing watch settings in both PC settings surfaces (`MENU_PC_OPTIONS` and F10) without maintaining a second copy of each value. Both surfaces already consume one row table in `port/src/optionsoverlay.c`; the watch remains an independent UI editing the **same underlying GE value**. Changes made in either direction must be visible on the next read. Preserve the N64 watch, its behavior, and the N64 build.
**Approved ownership:** per-file GE settings belong in the Bond save; machine-wide PC settings belong in `ge007.ini`. GE's original watch edits are *not* persisted on normal play (D349 below): **watch-made changes remain session-only**, while **PC-UI-made changes persist to the selected Bond file**. This is a deliberate port-side QoL divergence, not existing GE behavior. Do not register watch-backed settings as ini variables, copy watch values into config globals, or overwrite a newly loaded Bond file with stale ini values. A proposed PC master volume is separate: it multiplies the final output and does not replace the watch's Music/FX volumes.
## Inventory and proposed rows
Source of truth: `src/game/options.c` (`game_options_entries`, `cur_player_get/set_*`, `get_mTrack2Vol`/`set_mTrack2Vol`, `sub_GAME_7F0A91A0`), `src/game/file2.c:1246-1350` (per-folder serialization), and `port/src/input.c:~939` (forced control style). Audit exact getter/setter availability and initialization before implementing each row.
| Watch control | PC presentation | Notes |
|---|---|---|
| Music / FX volume | Audio section, separate Music and FX sliders | GE range 0..32767, stored in the Bond save at 8-bit precision; use actual GE APIs, not an SDL channel gain in place of either. Master volume stays a distinct global row. |
| Look up/down (reverse/upright), auto-aim, aim control (hold/toggle), sight on screen, look ahead, ammo on screen | Bond file / gameplay subsection, with GE-specific labels | These are the watch's `game_options_entries` and save bits. Watch look-up/down is distinct from PC MouseInvertY and PadLookInvertY: the independent inversions compose (XOR). Label/help text must explain the stacking; do not combine their stored values. |
| Screen (Full/Wide/Cinema), Ratio (Normal/16:9) | Separate clearly marked GE presentation controls, if useful | Not PC window resolution/mode or `Video.NativeWidescreen`. Verify interactions with native widescreen/crop before exposing. |
| Controller style / controller diagram | Defer pending input design | The diagram is not a setting. `inputComputePad(0)` currently reasserts Honey or Solitaire from `Input.NaturalPitch` every poll, including after save load or watch changes; `bondview.c` and multiplayer load also force Honey in some contexts. A naive style row would be immediately undone. Decide whether the watch selector should be informational, limited to supported PC mappings, or integrated with the port mapping; no row until this is reconciled. Dual-controller styles need their own decision. |
This inventory is not permission to expose every field automatically. Especially do not silently merge identically named PC and GE toggles that have different input paths or persistence.
### Persistence ground truth (D349)
`fileSaveSettingsForFolder` (`src/game/file2.c:1246`) has one caller, `fileClearSavefileForFolder` (`:1379`), reached from the erase path. Normal watch edits change live values but do **not** write the file's `options`/`music_vol`/`sfx_vol` fields or EEPROM. `init_watch_at_start_of_stage` (`src/game/options.c:415`) calls `fileLoadSaveSettingsForSelectedFolder`, so the next stage reloads those fields and discards unsaved watch edits. This is a GE/N64 quirk, not a PC regression; Gate A should re-audit indirect writes before implementation and record any correction to D349.
Thus a front-end row that edits only live watch state will be silently undone before gameplay. The maintainer approved **port-side per-file persistence of PC-UI changes**, without changing the N64 save format or the N64 watch's session-only behavior. Do not auto-persist edits made directly in the watch.
## UI / data design
1. Add a **watch-backed row type** to the existing shared row model, not another settings menu or fake config registration. Its read/write/availability callbacks operate on GE's real option API; existing config-backed rows continue to work unchanged. Keep enum labels, range/step and value formatting in the shared row table so F10 and the GE-styled screen cannot diverge.
2. Use GE's existing getters/setters for **all eight proposed entries** (`src/game/options.c:526-598`) and both volume controls (`get_mTrack2Vol`/`set_mTrack2Vol`, `call_sndGetSfxSlotFirstNaturalVolume`/`sub_GAME_7F0A91A0`). For FX use the same game volume API as the watch, not an SDL channel gain. No `src/game` bridge or Rule-2 game-code edit is expected for these rows. Preserve the watch's side effects; persistence, if approved, is a separate port-side operation.
3. **Threading is a design gate:** F10 input runs on the scheduler thread (`inputComputePad(0)`); the GE-styled screen and watch run on the game thread. Do not directly mutate game/watch state from the F10 thread merely because a getter or pointer is accessible. Route F10 writes as bounded requests to a safe game-thread point, and publish a safe, current snapshot for F10 rendering/reads, or establish an equivalently reviewed synchronization scheme. Cancel or retarget queued writes when the active folder/stage changes; never apply a write to the next folder by accident. Check audio/music initialization before **writing** Music/FX in the front end: the music getter reads a global, but the setters may touch not-yet-ready playback state.
4. Make scope visible to the player: use a dedicated **Bond file** section/page in `MENU_PC_OPTIONS` for GE watch options, with an **explicit Bond-file chooser**; never trust `selected_folder_num` on that screen (frontoptions init even assigns it a fallback). F10 in-stage targets the active file with no chooser. **D354 supersedes the "do not silently choose folder 1" stance:** all non-stage contexts (options screen *and* F10 on file select) lazily default to *explicit pick > `selected_folder_num` > first valid folder* so the section works transparently; an explicit "Edit file" cycle (still including "none") always overrides, and a game-thread `fileBuildWriteNewSave(FOLDER1)` one-shot builds a blank file 1 when no valid save exists. Switching chooser folders must not change the game's active file. D350 reads saved fields directly without calling `fileLoadSettingsForFolder` (which would audibly change front-end music). Unvalidated/absent slots are skipped. Test new/blank files in a normal boot as well as the D299 `GE_STARTMENU` shortcut.
5. **Approved port-side persistence:** resolve the actual save-slot pointer with `fileGetSaveForFoldernum(folder)`; `saves[folder]` is **not** generally the folder's physical slot (`fileWriteSave` requires a pointer into `saves[SAVESLOT1..SAVESLOTRAMROM)`). For F10, use a validated in-place path: serialize the live settings into a **temporary copy** with `fileSaveSettingsForFolder(&copy)`, then copy only the PC-UI-edited field/bit(s) into the real save-slot pointer and call `fileWriteSave(save)` with existing CRC/EEPROM handling. In the front end, where no GE player is active, merge only the selected field directly into the chosen saved record instead of loading it into live watch state. Retain `OPTION_CONTROLTYPE` from the real save: the port reasserts Honey/Solitaire every poll and must not overwrite stored controller style. This field-scoped merge also prevents a watch-only edit to a *different* field from piggybacking onto the next PC-UI save, preserving the approved session-only asymmetry. Validate the path against absent/free slots and ensure the live state belongs to **only** the chosen folder (do not load or copy values from a different folder). Never call `fileClearSavefileForFolder` as a generic settings save. Toggle/enum changes persist on value change; slider drags persist on release, not each detent. F10 requests must reach the correct game-thread/file target before write. The GE watch's own edits remain session-only. Test stage load, file switch, copy/erase, and restart for cross-file contamination; no N64 game-code changes. **Known erase edge:** GE's `fileClearSavefileForFolder` serializes *current* live settings into the reset file; after editing B, erasing A could copy B's live options into wiped A (also possible on N64 after watch edits). Document/test, do not change game code.
6. For future per-section reset-to-defaults: ini rows reset from registered PC defaults; watch rows reset to **`BLANKSAVEDATA`/`DEFAULT_OPTIONS`** (`bondconstants.h:2151`, `file2.h:23`) for the selected file, not from a global ini snapshot. New saves start with Music/FX 0xFF (full volume) and auto-aim/sight/look-ahead/ammo enabled. Separate confirmation and persistence policy before wiring a button.
## Sequence / review gates
- **Gate A — behavior audit + D349:** re-verify the erase-only write and stage-start reload (including indirect save paths); record the ground-truth quirk, defaults, stored control-type preservation, erase cross-file edge and exact API for each row. Trace folder-to-physical-slot mapping, FX/music setter readiness, watch-to-gameplay consumers, composed inversion and the F10/game-thread boundary. Approved policy: explicit chooser + PC-UI edits persist; watch edits remain session-only.
- **Gate B — foundation: IMPLEMENTED in D350**, including explicit file chooser, CRC guard, stage/file identity and thread-safe F10 command/snapshot path. D352 (review of `1303c0eb`) fixed the remaining robustness gaps: pending commands now survive stage transitions (hold-not-drop), the UI-thread chooser state is spinlocked with a front-screen guard, the queue coalesces to one pending command per field, and every silent no-op path logs a warning. Manual gamepad/mouse playtest still owed.
- **Gate C — audio: ROWS IMPLEMENTED in D350**, Music/FX save path headless-verified. Watch↔F10 live and audible checks still owed. Global master gain is a separate, not-yet-implemented M3 item.
- **Gate D — watch gameplay: ROWS IMPLEMENTED in D350** for six GE watch booleans; auto-aim and watch look-bit per-file saves headless-verified. Each watch/in-game effect and two-file isolation still need hands-on checks. Screen/Ratio remain a separate product decision.
- **Gate E — control style and resets:** design decisions first, then separate work. Avoid exposing a control-style row that the port immediately overwrites. Do not make a global reset wipe per-file watch settings.
## Acceptance checks
- Watch changes show in both PC surfaces without reopening/restarting, and PC changes show in the watch; no extra watch-key values in `ge007.ini`.
- With two Bond files, change Music/FX and a gameplay toggle in A via PC UI; switch to B, verify B retains its own values; return to A, start a stage, quit/restart and reload, verify A's values. Repeat an edit via F10 while in-stage. Drag sliders: only one save on release, not per detent. PC-UI persistence is a *port QoL test*, not a claim about N64; watch-only edits should still revert on stage reload. Check that stored controller-style bits survive PC saves and that file copy and erase retain GE behavior (including the erase cross-file edge above).
- F10 changes are not lost or applied to another file across a stage/menu transition. Check full-range sliders, watch's save quantization, no unexpected audio spikes, and no race/crash on quit.
- Menu layouts: use a dedicated Bond file section/page plus Audio; `MAX_PROWS=15` is a **per-page** cap in `frontoptions.c`, with `MAX_PAGES=8`; F10 scrolls. Check the 440x330 front-end page, F10 visible rows, mouse, keyboard, and real pad navigation; ASCII-only new text for region safety. Check NTSC headless captures, widescreen, golden gate, and PAL/JP build where possible (only NTSC ROM locally).
- Build/link checks and the normal verification ritual after implementation. Document any game-side change as its own Rule-2 request before editing it.
## Decisions and remaining scope
- **Approved:** PC-UI edits persist per selected Bond file; watch-only edits stay session-only. The front screen gets an explicit file chooser; F10 uses the active file. Preserve stored controller type; toggles/enums save on change, slider drags on release. No `src/game` behavior change or N64 save-format change.
- **Still separate product decisions:** whether/how to expose GE Screen/Ratio alongside PC video settings; whether to expose control style at all given the port's forced mapping (deferred); whether Bond-file reset needs its own confirmation rather than being bundled with global PC defaults.
- **Landed 2026-09-27 (D352/D353, port-only):** bridge robustness fixes (D352) and the Turok-style row-table regrouping (D353: six sections — DISPLAY, GRAPHICS, BOND FILE, MOUSE / AIM, CONTROLLER, GAME; the music/FX sliders sit in BOND FILE; the page title annotates the selected file; `buildPages` overflow logs).
- **Landed 2026-09-27 (D354, port-only):** front-end F10 routing + transparent default Bond-file selection (see item 4 above); in-stage F10 verified working by the `GE_WSPROBE` probe.
- **Landed 2026-09-27 (D355, port-only):** the Music commit now also resyncs the in-stage BGM (track 1) via `musicTrack1ApplySeqpVol`, plus a once-per-activation resync on level entry; by-ear confirmed. N64 watch path unchanged.
- **Landed 2026-09-27 (D356, port-only):** the settings regroup (plan `docs/dev/D356-SETTINGS-REGROUP-PLAN.md`): Turok-style pages renamed to functional sections (INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO, all below MAX_PROWS), the redundant N64-watch rows (`Bond.Look`, `Bond.AimControl`) commented out of the menu surface (keys + watch paths stay live, D181/D216 style), the `__HdrBond`/`__BondFile` rows retired for the front-only top save-file row, per-file sections annotated `"(File N)"` via the `saveScoped` flag, and the Turok reset rows replaced by a real per-section Reset-to-defaults (edge-triggered two-step arm->confirm within 3 s; watch rows -> BLANKSAVEDATA through the D352 commit path, ini rows -> the port's C initializers; `__Resolution` a documented exclusion). GE_WSPROBE_RESET front + in-stage probes headless-verified.
- A standalone AUDIO section (master volume + the config-side volume keys) and per-section reset-to-defaults (Gate E) are the next settings-scope items; key rebinding stays Phase 4.
+25 -19
View File
@@ -11,6 +11,10 @@ work back and forth through shared written notes, under one person's
part-time direction. The goal, the setup, the timeline, and an honest read on
what did and didn't work.*
> *Snapshot as of the v0.3.0 milestone (20 Sep 2026; findings through D321).
> v0.4.0 (28 Sep) extended the timeline with the options/input work -- the
> README's Background section carries the final project numbers.*
## Contents
- [Why this project exists](#why-this-project-exists)
@@ -62,7 +66,7 @@ reserved for the bugs that needed the most reasoning held at once; the same
## Timeline
All dates from this repository's own commit history (August 2026).
All dates from this repository's own commit history (August–September 2026).
```mermaid
gantt
@@ -87,7 +91,8 @@ gantt
section Month one - front end to v0.3.0
Audio mixer (music + SFX) :2026-09-02, 7d
Steam Deck hardening + F10 overlay QoL :2026-09-14, 3d
v0.3.0 released (full campaign playtested) :milestone, 2026-09-18, 0d
Final fix sweep (D309-D321) + release review :2026-09-18, 3d
v0.3.0 released (full campaign playtested) :milestone, 2026-09-20, 0d
```
- **Day 0** (16 Aug): repository forked, PC-port scaffolding added.
@@ -101,14 +106,14 @@ gantt
unattended play window without crashing.**
- **Day 14** (30–31 Aug): front end playable end to end (menu → mission
select → difficulty → briefing → start); file-backed saves.
- **Day 33** (18 Sep): **v0.3.0 released** — the full campaign playtested
- **Day 35** (20 Sep): **v0.3.0 released** — the full campaign playtested
end to end at Agent difficulty, audio complete, bundles for Windows,
Linux and Steam Deck.
So roughly **two weeks**, one person part-time, to take a decompilation from
"builds an N64 ROM" to "boots on desktop, renders every solo level, playable
through the front end into the early game". The rest of month one (29 Aug –
18 Sep) took it from there to a public release: the 21-level sweep to
20 Sep) took it from there to a public release: the 21-level sweep to
full-campaign completion, the audio mixer, input and Steam Deck polish,
widescreen FOV scaling, and the v0.3.0 bundles — with a set of cosmetic
issues documented rather than outstanding.
@@ -117,10 +122,10 @@ issues documented rather than outstanding.
| | |
|---|---|
| Calendar time | 34 days (16 Aug – 18 Sep 2026), one person part-time |
| Commits on the port | ~690 |
| Root-caused bugs logged | `D1`–`D311` in [`findings.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/findings.md), 190 tracked entries in the index (some labels later merged or withdrawn) |
| Handoff sessions | 190+ (`M-2` … `M-191`, and counting) |
| Calendar time | 36 days (16 Aug – 20 Sep 2026), one person part-time |
| Commits on the port | ~835 |
| Root-caused bugs logged | `D1`–`D321` in [`findings.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/findings.md), 199 tracked entries in the index (some labels later merged or withdrawn) |
| Handoff sessions | 190+ (`M-2` … `M-192`, and counting) |
| Game-source files given `#ifdef PORT` ABI edits | 71 files, ~385 blocks |
| New port-layer / tooling files | ~119 |
| Port layer | ~24,450 lines C/C++ (`port/`) |
@@ -131,16 +136,17 @@ issues documented rather than outstanding.
```mermaid
xychart-beta
title "Commits per day"
x-axis ["8/16", "8/17", "8/20", "8/21", "8/22", "8/23", "8/24", "8/27", "8/28", "8/29", "8/30", "8/31", "9/1", "9/2", "9/3", "9/4", "9/5", "9/6", "9/7", "9/8", "9/9", "9/10", "9/11", "9/12", "9/13", "9/14", "9/15", "9/16", "9/17", "9/18"]
y-axis "commits" 0 --> 90
bar [1, 2, 2, 6, 9, 2, 4, 7, 57, 39, 39, 46, 11, 52, 39, 8, 8, 2, 8, 34, 17, 30, 20, 30, 57, 4, 84, 38, 22, 13]
title "Commits per week (week start)"
x-axis ["8/16", "8/23", "8/30", "9/6", "9/13", "9/20"]
y-axis "commits" 0 --> 250
bar [20, 109, 203, 141, 220, 6]
```
Days with no commits are omitted (18–19 and 25–26 Aug). The step up from
8/28 onward is the collaborative phase in full swing, including the parallel
multi-agent "bursts"; the 9/15 spike is the Steam Deck playtest-feedback
batch landing at once.
Weekly totals (the final week is release day only). The step up from the
8/23 week onward is the collaborative phase in full swing, including the
parallel multi-agent "bursts"; the 9/13–9/19 week peaks on a single-day
84-commit burst (9/15) when the Steam Deck playtest-feedback batch landed
at once.
### Who did what
@@ -196,7 +202,7 @@ result worth taking away.
This is the part worth paying attention to.
```mermaid
flowchart LR
flowchart TD
H["Human: direction, integration,<br/>build + playtest verification"]
C["Claude / Claude Code<br/>frontier, hosted"]
Q["Qwen 3.8 via pi<br/>open-weight, local RTX 5090"]
@@ -221,8 +227,8 @@ let them substitute for each other:
usage limit mid-problem, the local model picked the task up from the
HANDOFF state and continued; when the local model hit a bug that needed
deeper structural reasoning, it wrote up where it was and Claude took over.
2. **`findings.md`**; the chronological finding log. 311 numbered `D`
entries by v0.3.0 (190 tracked in the index; some labels merged or
2. **`findings.md`**; the chronological finding log. 321 numbered `D`
entries by v0.3.0 (199 tracked in the index; some labels merged or
withdrawn), each a root cause with `file:line` evidence and the fix. New
agents (either model) are pointed at the relevant entries before they
start.
+155 -68
View File
@@ -7,36 +7,37 @@ D43-D50,RESOLVED,"model-file loading → offline sidecar ""Plan B"" (d43_emit.py
D51-D58,RESOLVED,"font pixeldata, osGetCount tick rate, model RW pools, cseq BE header, synth slots, RLE menu bg, watch raw offsets, gun-barrel DL",resolved
D59-D68,RESOLVED,"intro render: blood-RLE clobber, DMA validate, OSMesgQueue, HEADS/BODIES sentinels, romCopy width, image_entry, Globalimagetable BE→LE",resolved
D70-D74,RESOLVED,"intro-logo pixels: C-array bswap, UV path, sinf/cosf DVAL(), texture-import truncation",resolved
D75,OPEN,front-end 3D model transforms (D77 audio split out below —,"OPEN (parked). M-32 triage (see §F ""D75 ADDENDUM""):"
D75,PARTIAL,front-end 3D model transforms (D77 audio split out below —,PARTIAL (M-197 fixed the logo texture blobs; v0.4.0 campaign 2026-09-28:
D76 · D164,FIXED,Legal screen only draws line 1 —,"FIXED — this row and the ## D164 entry below were stale; the proposed one-line #ifdef PORT fix was actually committed 8dfc578f4 (2026-08-31), well before this correction (2026-09-18)."
D159,FIXED,"front-end wallet-Bond photo ""interlaced""/combed (RC1 / D149) —",FIXED (#ifdef PORT no-op the swap in image.c)
D161,FIXED,Depot (-level_30) ceiling = bright-blue speckle + radial rays (B2).,FIXED (port/fast3d/gfx_pc.cpp)
D165 · D166,FIXED,"Input-layer polish (M-31, port-only):",FIXED
D180 · D181,PARTIAL,Native-PC input pass (QoL run):,"LANDED, config-gated, feel-checks owed"
D186,PARTIAL,"Video.FpsCap throttles the sim, not just presentation (M-39 diag, M-42 clamp).","CLAMP LANDED (port/fast3d/gfx_sdl2.cpp, port/src/video.c); real fix owed"
D180 · D181,FIXED,Native-PC input pass (QoL run):,FIXED (2026-09-26 bookkeeping:
D186,OPEN,"Video.FpsCap throttles the sim, not just presentation (M-39 diag, M-42 clamp).","OPEN (2026-09-26, user direction:"
D187,FIXED,"HUD ammo-type/weapon icon vanished at -O2 (M-46 root-cause, M-47 fix + verify).","FIXED (src/game/gunfire.c, #ifdef AVOID_UB); PR #19"
D188,FIXED,Linux -O2 boot crash:,"FIXED (src/libultrare/libc/xprintf.c, src/sprintf.c, #ifdef PORT); PR #22"
D189,FIXED,Linux -fstack-protector aborts on the decomp's latent stack-buffer overruns (M-48) —,"FIXED (CMakeLists.txt, src/game/stan.c #ifdef PORT); PR #23"
D190,CLOSED,"Linux -level_45 SIGSEGV at load in add_ptr_to_objective (objective.c:64) (M-48, open).","CLOSED — not a port bug; level 45 is MP-only and has no solo setup file in the ROM (root-caused M-105, reproduced on Windows)."
D191,FIXED,Windows -O2 release-bundle crash:,FIXED + PLAYTEST-VERIFIED (M-79).…
D193,FIXED,NPC AI locomotion is too slow —,FIXED + user-playtest-verified (M-81) —…
D194,PARTIAL,Mouse input needs another tuning pass —,PARTIAL — M-120 human playtest…
D194,FIXED,Mouse input needs another tuning pass —,FIXED (2026-09-26: user reports the v0.3.0→v0.4.0 mouse changes feel solid and glitch-free; code-side validation owed).
D195,FIXED,"Transparency / alpha surfaces broken on Control (M-49, user playtest) —","FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_vertex) —…"
D197,CLOSED,"Character face/head textures wrap around the head, seen on Silo (M-49, user playtest).",CLOSED — user playtest v0.2.0-pre
D198,FIXED,alAdpcmPull/_decodeChunk used K0_TO_PHYS (an unconditional & 0x1FFFFFFF mask) on 64-bit heap pointers (src/libultra/audio/load.c).,FIXED (src/libultra/audio/load.c) —
D199,PARTIAL,Phase-3 software audio mixer:,"LANDED (port/src/mixer.c, port/include/mixer.h, include/PR/abi.h) —"
D199,FIXED,Phase-3 software audio mixer:,FIXED (2026-09-26 bookkeeping:
D200,FIXED,-level_09 segfault: reverb's ALDelay.input/output are u32 —,FIXED — 5-line cast in reverb.c (this session)
D196,FIXED,"OS mouse cursor stays visible after closing the F10 options overlay in click-to-lock mode (M-49, user playtest).","FIXED (6e758f1d, merged to main) —…"
D192,CLOSED,Front-end mission/level-select grid pointer still can't reach the outer cells —,"CLOSED — the buggy mode 0 was removed entirely in the v0.2.0 prep pass (see D239); click-to-lock is now the only capture mode, so the unreachable-cells path no longer exists"
D221,FIXED,SELECT FILE mouse hit-box offset —,"FIXED (M-91) — explicit {min,max} coord2d pairs under #ifdef PORT; human mouse-playtest confirmed."
D222,FIXED,High Video.FovScale breaks culling —,"FIXED, awaiting visual verification (M-121)."
D223,FIXED,QoL ask: mouse-wheel weapon cycling should be directional like a normal PC shooter —,"FIXED (M-105, port/src/input.c) —…"
D224,?,"QoL gap (M-87, from a PD-PC-port survey):","DEPRIORITIZED (M-87, user call)."
D224,PARTIAL,"QoL gap (M-87, from a PD-PC-port survey):","LANDED via D401 (2026-09-27, re-promoted from DEPRIORITIZED)."
D225,FIXED,"Dev-tooling QoL (M-87, user ask):","FIXED (2026-09-18) — GE_DEBUG_UNLOCKALL=1 short-circuits fileGetIsCheatUnlocked() (src/game/file2.c, #ifdef PORT, cached getenv) to always return TRUE; default off, no save mutation."
D226,OPEN,"QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text.","OPEN — root cause / call sites identified, not designed or implemented."
D226,FIXED,"QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text.","FIXED (2026-09-24): Game.HudScale 75–150% (user-verified, capped at 150) via fast3d G_HUDSCALE_EXT rect scaling about per-element anchors; dialogue/bottom messages width-capped."
D227,FIXED,Sky renders as two independently-moving halves —,"FIXED (M-100b, PR #43) —"
D229,FIXED,"IsWater-level water renders green, pulses blue<->green (M-101; Frigate -level_26).","FIXED (port-layer) — ZLIB+CI8 water image now imports via the CI8 palette path (gfx_pc.cpp); Frigate verified live (""looks better""); sweep + Surface 2 by-eye still owed."
D230,FIXED,In-level music quality:,"FIXED (M-121, v0.2.0) —"
D231,CLOSED,"QoL batch: mute-on-focus-loss + screenshot-hotkey productization (M-105, roadmap Tier 3, port-only).","CLOSED — PARKED/DEPRIORITIZED (user, 2026-09-25)."
D233,FIXED,All levels (intermittent):,"FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_tri1) —…"
D234,FIXED,"Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).",VISUALLY VERIFIED FIXED (M-108) —…
D244,OPEN,Self-hosted CI runner (corpyvt6agent01) link failure:,"OPEN, not yet fixed. Next…"
@@ -44,9 +45,9 @@ D228,FIXED,AK47 white missing-texture patches after D217's texpool fix —,"FIXE
D204,FIXED,PC audio ran ~2 % below real time permanently (AI feedback loop's 3 ms setpoint vs PC jitter).,"FIXED (port/src/audio.c, port/src/libultra.c, port/include/audio.h) —"
D203,CLOSED,"Steam Deck (SteamOS, x86_64 Linux) v0.1.0 release bundle:",CLOSED — root cause + fix in D253.
D201,FIXED,"src/libultra/audio/bnkf.c bank/instrument/sound/wavetable relocation offsets were s32, truncating the real 64-bit base pointer.",FIXED (src/libultra/audio/bnkf.c) —
D202,?,"Silenced PPK ""slap"" + broader real-time mixer corruption (M-52→M-67).",ROOT CAUSE ESTABLISHED (M-66); Disposition C implemented + measured (M-66b) —
D202,FIXED,"Silenced PPK ""slap"" + broader real-time mixer corruption (M-52→M-67).","FIXED (2026-09-26, user by-ear:"
D206,FIXED,PC played every SFX one bank slot too high —,"FIXED (M-76, src/snd.c:1001, #ifdef PORT) —"
D207,?,"Alarm klaxon starves combat SFX and doesn't recover after it stops (surfaced by D206, M-77).",MONITORING (M-77c: user reports the alarm is acceptable in play after D206; no fix shipped) —
D207,FIXED,"Alarm klaxon starves combat SFX and doesn't recover after it stops (surfaced by D206, M-77).",MONITORING — user 2026-09-26:
D208,FIXED,Player weapon-fire sound goes permanently silent mid-firefight (all other SFX unaffected) —,FIXED + user playtest-verified (M-78d) —
D209,FIXED,Every AI character in the game is locked to a *walk* animation regardless of the speed the ailist commands —,FIXED + user-playtest-verified (M-81).
D210,FIXED,chrToPatrol() leaves act_patrol.lastvisible60 uninitialised on PC —,"FIXED (chraction.c:3872, #ifdef PORT → self->act_patrol.lastvisible60 = -1;, N64 line under #else).…"
@@ -55,33 +56,33 @@ D152+,FIXED,D152 addendum (M-31):,"FIXED (src/snd.c, port/src/libultra.c, #ifdef
D168,FIXED,GE_PCDUMP / F12 PPM captures were vertically flipped —,FIXED (port/fast3d/gfx_opengl.cpp); D114 / D116 reclassified as capture-orientation artifacts (below).
D169,FIXED,Front-end mouse pointer can't reach the outer cells of the mission/level-select grid —,"FIXED (M-33, port/src/input.c, port-only, no #ifdef PORT)."
D178,FIXED,Pre-mission briefing screen:,FIXED (M-36) — high confidence
D175,OPEN,In-game stutter / brief hang during normal play —,"OPEN — observed, not investigated"
D175,CLOSED,In-game stutter / brief hang during normal play —,"CLOSED (v0.4.0 campaign sign-off 2026-09-28, user:"
D176,FIXED,"Surface exterior renders wrong (-level_36), two independent defects.","(a) scroll-speed FIXED (fidelity pass; HIGH confidence on root cause, MODERATE on N64 parity); M-93 playtest + M-98 Defect-2 starburst screenshot in the sections."
D177,FIXED,Ladders non-functional —,FIXED (M-36) — high confidence; interactive climb test owed
D172,FIXED,"Bullet-impact / blood / spark particles render magenta or cyan instead of dark red (M-82, ROOT CAUSE FOUND + FIXED).",FIXED (port/fast3d/gfx_pc.cpp gfx_lod_tile_offset)…
D174,OPEN,"""No blood effect"" (user QA report).",OPEN — observed; first step = BUNKER1 firefight with regenerated sidecars
D183,OPEN,"M-36 Family A (""texture line/pitch shear"") is DISPROVEN for the Surface repro (-level_36).",Hypothesis DISPROVEN + diagnostics shipped; D176(b)/D182(2) still OPEN
D174,CLOSED,"""No blood effect"" (user QA report).","CLOSED — faithful (2026-09-26, user side-by-side on 1964:"
D183,CLOSED,"M-36 Family A (""texture line/pitch shear"") is DISPROVEN for the Surface repro (-level_36).","CLOSED (2026-09-26, user-approved:"
D173,FIXED,Third-person Bond model spawns too high —,"MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed)."
D179,FIXED,Packaged build crashes after the logos —,"Diagnosed, not fixed —"
D180,PARTIAL,"Native-PC input pass (M-XX QoL run, qol/native-pc-input-menu, port-only).","LANDED, port-only, no #ifdef PORT.…"
D181,?,Game.ScreenShakeIntensity —,"Menu row pulled (v0.2.1, user testing)."
D184,PARTIAL,F10 port-layer options overlay —,"LANDED, port-only, no #ifdef PORT in src/."
D211,PARTIAL,"PC FOV slider — Video.FovScale (M-83 QoL wave, port-only, WIDESCREEN-FOV-PLAN.md Phase 4).",PARTIAL — M-84 refix…
D212,PARTIAL,Anisotropic-filtering config wiring —,"LANDED, port-only. Build clean."
D213,PARTIAL,On-screen FPS readout —,"LANDED, port-only. Build clean; bunker1 no-op confirmed."
D214,PARTIAL,"Keyboard rebinding — [Input.Bind] config section (M-83, port-only, PD parity, Phase 4 debt).",PARTIAL — M-84: code PASS (alt/ctrl); one doc-only fix.
D180,FIXED,"Native-PC input pass (M-XX QoL run, qol/native-pc-input-menu, port-only).",FIXED (2026-09-26 bookkeeping:
D181,FIXED,Game.ScreenShakeIntensity —,FIXED (2026-09-26 bookkeeping:
D184,FIXED,F10 port-layer options overlay —,FIXED (2026-09-26 bookkeeping:
D211,FIXED,"PC FOV slider — Video.FovScale (M-83 QoL wave, port-only, WIDESCREEN-FOV-PLAN.md Phase 4).",FIXED (2026-09-26 bookkeeping:
D212,FIXED,Anisotropic-filtering config wiring —,FIXED (2026-09-26 bookkeeping:
D213,FIXED,On-screen FPS readout —,FIXED (2026-09-26 bookkeeping:
D214,FIXED,"Keyboard rebinding — [Input.Bind] config section (M-83, port-only, PD parity, Phase 4 debt).",FIXED (2026-09-26 bookkeeping:
D215,FIXED,1P weapon viewmodel never drew in-level —,"FIXED (src/game/gunfire.c, #ifdef PORT)."
D216,PARTIAL,"Game.SkipIntro — boot straight to the file-select menu (M-83, port + one #ifdef PORT game-code line, PD parity, Phase 4 quick win).",LANDED + VERIFIED this session.
D216,FIXED,"Game.SkipIntro — boot straight to the file-select menu (M-83, port + one #ifdef PORT game-code line, PD parity, Phase 4 quick win).",FIXED (2026-09-26 bookkeeping:
D217,FIXED,1P weapon viewmodel + 3P Bond model textures garbled —,FIXED + merged (PR #47 + PR #49); white residual split out as D228.
D218,PROPOSED,Wide FOV exposes the per-level draw-distance / fog boundary —,"Logged (M-84), fix proposed + parked."
D218,FIXED,Wide FOV exposes the per-level draw-distance / fog boundary —,FIXED (2026-09-26 reclassified:
D219,RESOLVED,"PPK explosions render purple on Dam (M-87, user QA, ""not 3D but noting"").","RESOLVED (M-115, 2026-09-12) for the original PPK-purple symptom."
D220,PARTIAL,"1P muzzle flash position shifts upward at higher Video.FovScale (M-87, user QA, Jungle/M16).","ANALYZED (M-91) — the suspected unscaled-viewmodel root does NOT hold; this is correct perspective behavior, likely cosmetic/QoL."
D85,OPEN,Room primary/secondary DL → texLoadFromGdl garbage.,"OPEN (safety-netted; widen/pool fixes landed, geometry now renders)"
D220,CLOSED,"1P muzzle flash position shifts upward at higher Video.FovScale (M-87, user QA, Jungle/M16).","CLOSED — merged into D303 (2026-09-26, user:"
D85,CLOSED,Room primary/secondary DL → texLoadFromGdl garbage.,"CLOSED (2026-09-26, user-approved:"
D86 · D87,RESOLVED,modelInitRwData truncated ptr · attract-demo BE ramromfilestructure.,resolved
D88.4,RESOLVED,propDefs polymorphic record stream not byteswapped → setupDoor crash.,resolved (d88_propdefs.py); layout audit M-20 → D132
D132,PROPOSED,D88 propDefs layout audit (M-20):,proposed fix (not applied) —
D157,FIXED,"Campaign never unlocks the next level (M-30, user bug —","FIXED (src/bondtypes.h, #ifdef PORT —…"
D160,PARTIAL,Dam level-end cutscene (Bond off the dam / bungee) never plays —,"DIAGNOSTIC SHIPPED, not root-caused (c95713f5, #ifdef PORT + GE_D160=1)…"
D160,FIXED,Dam level-end cutscene (Bond off the dam / bungee) never plays —,"FIXED (user, 2026-09-26:"
D89-D92,RESOLVED,"stage-load→frame: stan zero-fill overrun, portal address trunc, chr/AI spawn ptrs",resolved
D93-D102,RESOLVED,struct-pun / hardcoded-size pass:,resolved (D95 pool bump partial)
D103-D107,RESOLVED,"BUNKER1 viewport height, depth-buffer clear (G_CLEAR_DEPTH_EXT), portal near-plane, LOD mip tile",resolved
@@ -90,20 +91,20 @@ D113-D116,FIXED,portal BFS ok · matrix chain ok · player raw-offset audit + gu
D117,OPEN,"Frame nondeterminism root-caused; GE_DETERM attempted, not achieved.","OPEN (mode attempted, not achieved)"
D118,RESOLVED,"SDL input layer; M-24 mouse-look rework (mode-aware map, real INI); D118b/c FIXED.",resolved (D118a residual)
D150,FIXED,watch OBJECTIVES / BRIEF page crash:,FIXED (src/str.c #ifdef PORT); interactive re-verify pending
D260-D265,PARTIAL,"v0.2.1 hotfix batch: D260 wheel direction swap (fixed) · D261 watch-menu nav auto-repeat (fixed, feel-check owed) · D262 watch item-preview model missing → roo…",partial — see rows
D266-D269,OPEN,"D236/D265 wall, second pass:",OPEN (the wall itself) —
D270,?,Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it.,dead-ended into D271/D272; infrastructure kept in tree
D260-D265,CLOSED,"v0.2.1 hotfix batch: D260 wheel direction swap (fixed) · D261 watch-menu nav auto-repeat (fixed, feel-check owed) · D262 watch item-preview model missing → roo…",CLOSED (2026-09-26 bookkeeping:
D266-D269,CLOSED,"D236/D265 wall, second pass:",CLOSED (2026-09-26 bookkeeping:
D270,CLOSED,Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it.,CLOSED (2026-09-26 bookkeeping:
D271,FIXED,"D236 wall, third pass —",FIXED-but-insufficient (src/game/bg.c #ifdef PORT) —
D276,OPEN,"D236 wall, eighth pass —","REFRAMED — oml=0xc81049d8/fmt=3 is the primary suspect now, not D269's CI8 pair; 0x0c184b50 unattributed; wall still open, see D236"
D279,?,Backfilled — this label was consumed by an undocumented partial session (2026-09-14 ~23:00) that left artifacts in scratch/ (d279_run.log… —,undocumented — artifacts only
D281,OPEN,"Game.AllUnlocked (D257) still breaks audio, and can also break right-mouse aim, when enabled before any save has ever been written (v0.2.1 user testing… —","OPEN — user-reported regression, not root-caused; mitigation is user-facing guidance only (release notes:"
D276,CLOSED,"D236 wall, eighth pass —",CLOSED (2026-09-26 bookkeeping:
D279,CLOSED,Backfilled — this label was consumed by an undocumented partial session (2026-09-14 ~23:00) that left artifacts in scratch/ (d279_run.log… —,CLOSED (2026-09-26 bookkeeping:
D281,FIXED,"Game.AllUnlocked (D257) still breaks audio, and can also break right-mouse aim, when enabled before any save has ever been written (v0.2.1 user testing… —","FIXED (2026-09-23, port/src/libultra.c):"
D282,FIXED,"QoL ask (v0.2.1 user testing, 2026-09-15):","FIXED (port/src/input.c, #ifdef not needed —"
D283,OPEN,"Steam Deck first-run preset (D-unnumbered in port/src/video.c/main.c, videoApplySteamOSDefaults()) did not apply for a user who added the game via Steam… —","OPEN — mechanism understood, root cause (Desktop-vs-Game-Mode STEAMOS env var difference) unverified on real hardware; user-facing workaround documented in README/release notes."
D280,?,"D236 wall, ninth pass —","REFRAMED again — no fix; next steps in detail section, see D236"
D275,?,"D236 wall, eighth pass —","done — probes live, uncommitted; see D276 for what they found"
D274,OPEN,"D236 wall, seventh pass —","d267_slot_addr fix live (always-on); content/decode question CLOSED; mip-presence CLOSED; wall still open, see D236/D276"
D273,OPEN,"D236 wall, fifth pass —","RULED OUT (port/fast3d/gfx_pc.cpp palette_to_rgba32, pre-existing/uncommitted GE_D273) —"
D272,OPEN,"D236 wall, fourth pass —","OPEN — probe GE_D272 added to port/fast3d/gfx_pc.cpp (env-gated, uncommitted); wall still open, see D236"
D280,CLOSED,"D236 wall, ninth pass —",CLOSED (2026-09-26 bookkeeping:
D275,CLOSED,"D236 wall, eighth pass —",CLOSED (2026-09-26 bookkeeping:
D274,CLOSED,"D236 wall, seventh pass —",CLOSED (2026-09-26 bookkeeping:
D273,CLOSED,"D236 wall, fifth pass —",CLOSED (2026-09-26 bookkeeping:
D272,CLOSED,"D236 wall, fourth pass —",CLOSED (2026-09-26 bookkeeping:
D137,FIXED,right-mouse (aim-sight) crash:,FIXED (src/game/gunfire.c #ifdef PORT —
D119 · D120,FIXED,guard-attack weapons_held[]->chr pun crash · blood-stain PointUsage[] chain hang,D119 fixed · D120 FIXED (M-30)
D120,FIXED,Blood-decal PointUsage[] chain —,FIXED (tools_pc/d43_emit.py).
@@ -121,9 +122,9 @@ D136,RESOLVED,"Stage-unload crash — objFree obj->prop reads float garbage (M-2
D139,FIXED,Stage-unload crash root cause + fix (M-23).,"FIXED (src/game/cleanup_objects.c, #ifdef PORT —…"
D140,FIXED,Pause-menu crash (M-23 open → M-27 fixed).,"FIXED (src/game/bondview.h, src/game/bondview2.c; #ifdef PORT)."
D141,FIXED,"Watch-menu item-model crash (M-27), exposed by D140.","FIXED (src/game/gunfire.c, #ifdef PORT)."
D152,?,"Mission-failed / objective-failed → permanent black screen (M-28, user bug report —","MITIGATED (port/src/libultra.c, M-28, #ifdef PORT/port-only).…"
D152,FIXED,"Mission-failed / objective-failed → permanent black screen (M-28, user bug report —","FIXED (user, 2026-09-26:"
D154,?,"bg.c room-geometry ray/hit-test GBI parser port (M-28, WRITTEN / UNVERIFIED / UNCOMMITTED).",M-30 re-audit + fixes (still playtest-gated for final verify).
D156,PARTIAL,"Facility outro-cutscene ""hang"" —","GUARDED (src/game/model.c, #ifdef PORT)…"
D156,FIXED,"Facility outro-cutscene ""hang"" —","FIXED (user, 2026-09-26:"
D155,FIXED,"Facility outro-cutscene end → ""hang"" = unclamped deltaFrames spiral (M-29, user bug report).","FIXED (src/game/frametiming.c, #ifdef PORT —…"
D153,RESOLVED,"-level_09 frame ~900–1400 0xc0000005 = D117/D134 load flakiness, NOT a bug (M-28).",NOT A BUG — closed. Run anchors on a quiet machine.
D151,FIXED,Watch data all blank —,"FIXED (src/bondtypes.h, #ifdef PORT —…"
@@ -131,23 +132,23 @@ D130,RESOLVED,"crash class C2 (Facility -level_34, Runway -level_35):",resolved
D164,FIXED,"Disclaimer / legal screen only draws the first line (D76 root cause, M-31).",FIXED — corrected 2026-09-18:
D165,FIXED,"Front-end menu cursor doesn't feel like a mouse pointer (M-31, user bug B5 follow-up).","FIXED (port/src/input.c; Input.MenuPointerMode, Input.HipfirePitchSpeed added)."
D166,FIXED,Hipfire mouse pitch is digital while yaw is analog —,FIXED (port/src/input.c).…
D235,OPEN,Wrong textures load after dying/restarting a level —,OPEN — needs: does it repro on a *fresh* level boot (rules out reload-state) vs only after death/restart (confirms it); which surfaces; a screenshot.…
D236,OPEN,"Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).","OPEN, sixteenth pass (2026-09-18) —"
D237,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only
D238,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only (port/src/input.c + config.c)
D235,CLOSED,Wrong textures load after dying/restarting a level —,"CLOSED (user, 2026-09-26:"
D236,CLOSED,"Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).","CLOSED (M-198, pass 26, 425237f9; probes stripped 0503d335):"
D237,FIXED,"QoL ask (M-106, user QA):","FIXED (2026-09-23): category headers + 5 more settings (SkipIntro, Show FPS, pad invert/deadzone/trigger); wheel scroll already existed."
D238,FIXED,"QoL ask (M-106, user QA):",FIXED (2026-09-26: covered by D337/D338 aim work; user reports mouse feel good; code-side validation owed).
D239,CLOSED,"QoL ask (M-106, user QA):",CLOSED — executed in the v0.2.0 prep pass (port-only)…
D240,OPEN,"Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report).","OPEN — not root-caused; static call-site tracing only, no runtime verification possible in this environment."
D241,OPEN,Player gunshot SFX goes silent under some conditions —,"OPEN — not newly root-caused; high confidence this is D207 reproduced, not an independent bug."
D242,?,Player-death sequence UX regression vs N64 original:,NOT A PORT-LOGIC BUG as far as static analysis can tell.
D240,CLOSED,"Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report).","CLOSED (v0.4.0 campaign sign-off 2026-09-28, user:"
D241,CLOSED,Player gunshot SFX goes silent under some conditions —,"CLOSED as the D207 family (v0.4.0 campaign sign-off 2026-09-28, user:"
D242,FIXED,Player-death sequence UX regression vs N64 original:,"FIXED/CLOSED (2026-09-26, user:"
D243,FIXED,"Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject):","FIXED, live-confirmed by the user (M-189/M-190, 2026-09-18)."
D245,OPEN,IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) —,"OPEN — REOPENED, M-142 headless probe found the tc-shift k measurably flips during ordinary view rotation (new root-cause evidence); an opt-in GE_D245_FIXEDSHIFT candidate is build-verified but needs…"
D245,FIXED,IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) —,"FIXED (M-201, 2026-09-26, user-verified vs 1964):"
D246,FIXED,"Tiny pixel strips at the left and right edges of the screen, ""like overscan"" —","FIXED (2026-09-18 session, port-only, Video.SafeAreaCrop)."
D247,FIXED,Black bars top and bottom of screen —,"FIXED (2026-09-18 session, port-only, Video.SafeAreaCrop)."
D248,FIXED,"Port runs a stable 30 FPS instead of 60 FPS game-wide (M-117, user asked to verify FPS after the D209 timing-keystone fixes).","FIXED, root-caused to a genuine pointer-width ABI bug in src/sched.c (compiled for the PC build, not excluded)."
D249,OPEN,"LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV —","OPEN — M-142 static review ruled out the unscaled-far-clip and LOD-multiplier suspects (both already correct); narrowed to the portal/room-visibility BFS, needs a live screenshot+location, not more c…"
D249,OPEN,"LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV —","OPEN — M-142 static review ruled out the unscaled-far-clip and LOD-multiplier suspects (both already correct); CLOSED (v0.4.0 campaign sign-off 2026-09-28, user:"
D250,FIXED,"FPS is inconsistent/low on many levels after 60 Hz shipped (M-119, user QA; D248 follow-on).","FIXED — per-call getenv() cost was the bottleneck; rock-steady 60-61 fps after, user re-tested 1440p/4K clean."
D251,FIXED,F10 options overlay QoL pass:,"FIXED (port-only, port/src/optionsoverlay.c)."
D252,PARTIAL,"Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).","PARTIALLY FIXED — a real, distinct manifestation was found and fixed, but the bug family is not fully closed, and is intermittent on BOTH platforms, not Deck-specific (corrected 2026-09-18)."
D252,FIXED,"Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).","FIXED (M-201, 2026-09-26, user-verified):"
D253,FIXED,Facility (and any level with a FORCECROUCH/LADDER linked special tile) SIGSEGV on Linux/Steam Deck —,FIXED — Deck-verified:
D254,FIXED,Linux crash handler destroyed its own logs:,FIXED — field-verified.
D255,FIXED,Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle; D253 fix confirmed working).,"FIXED (M-140) — D3x-class 32-bit pointer truncation (vtxstore_fix_refs read Model* through the mistyped ChrRecord.chrflags 4-byte-enum alias instead of ObjectRecord.model), not the destroy-path race…"
@@ -159,42 +160,128 @@ D261,FIXED,v0.2.1 (user request:,FIXED — feel-check received; W/S sensitivity
D262,FIXED,v0.2.1 (user report):,"FIXED (primary cause, D264) —"
D264,FIXED,"D262's root cause (ABI/layout class, cf.","FIXED — build green, probes removed; visual confirmation pending."
D263,RESOLVED,v0.2.1 (user report):,"RESOLVED (not a bug), refined (v0.2.1 further user testing, 2026-09-15)."
D265,?,"D236 investigation (port-layer, port/fast3d/gfx_pc.cpp):",PARKED — reverted to baseline; evidence + suspect list above.
D258,OPEN,PAL/JP asset conversion is broken at the source-data level —,"OPEN — repair path verified, not applied (out of v0.2.0 scope)."
D265,CLOSED,"D236 investigation (port-layer, port/fast3d/gfx_pc.cpp):",CLOSED (2026-09-26 bookkeeping:
D258,PROPOSED,PAL/JP asset conversion is broken at the source-data level —,DEFERRED (user call 2026-09-28:
D284,FIXED,port/src/random.c's MIPS64 dsll32/dsrl32 shift helpers were backwards —,"FIXED, 21-level sweep clean, no crashes —"
D285,PARTIAL,Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug:,"PARTIALLY FIXED, RECURRED PAST THE FIX (see D305, 2026-09-18)."
D286,OPEN,"User-confirmed real ~8s frame freeze on Caverns, Steam Deck —","OPEN — one confirmed occurrence, no repro trigger identified, not root-caused; distinct from D255/D285."
D287,OPEN,"User-confirmed real, non-self-recovering freeze on Windows (M-141, 2026-09-15/16) after ~15-20 minutes with no input, triggered by clicking back into the… —","OPEN — one confirmed occurrence, real (not self-recovering), not root-caused; gfxFrameMsgQ full is a lead, not a confirmed cause."
D288,OPEN,"Silo intro: a huge, flat-shaded, untextured brown/tan triangle sweeps across the frame during the camera pan down the missile shaft (M-141, user report —… —","OPEN — visually confirmed and precisely bounded (frames ~562-741 of a bare -level_20 boot), root cause not yet identified; promoted from the long-parked GRAP… |"
D289,OPEN,"Dam: a crash in the computer room, not reproducible on a second attempt (M-141, Steam Deck user playtest).","OPEN — needs a human re-test specifically on a build with the D255 fix (this session's release/v0.3.0, not any earlier build) to see if it still happens.…"
D290,OPEN,"Watch's in-game ""controller"" section shows no control-pad graphic (M-141, Steam Deck user playtest).","OPEN — observed, not investigated."
D291,OPEN,"Dam: texture glitches when the truck drives past (M-141, Steam Deck user playtest).","OPEN — observed, not investigated."
D285,FIXED,Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug:,"FIXED (user, 2026-09-26:"
D286,CLOSED,"User-confirmed real ~8s frame freeze on Caverns, Steam Deck —","CLOSED (2026-09-26, user-approved:"
D287,FIXED,"User-confirmed real, non-self-recovering freeze on Windows (M-141, 2026-09-15/16) after ~15-20 minutes with no input, triggered by clicking back into the… —",FIXED (user playtest 2026-09-26:
D288,CLOSED,"Silo intro: a huge, flat-shaded, untextured brown/tan triangle sweeps across the frame during the camera pan down the missile shaft (M-141, user report —… —","CLOSED — not reproduced on the current build (2026-09-23 capture) and the 2026-09-25 user Silo playtest confirms it gone (""silo fixed"")."
D289,FIXED,"Dam: a crash in the computer room, not reproducible on a second attempt (M-141, Steam Deck user playtest).","FIXED (user, 2026-09-26:"
D290,FIXED,"Watch's in-game ""controller"" section shows no control-pad graphic (M-141, Steam Deck user playtest).","FIXED (2026-09-23): D264-pattern template read in watchRenderController (flags read 0), plus 3 pointer-width/raw-Gfx sites on the same path; controller model verified rendering on Windows."
D291,CLOSED,"Dam: texture glitches when the truck drives past (M-141, Steam Deck user playtest).","CLOSED — duplicate of D306, faithful (M-201, 2026-09-26:"
D292,FIXED,Death animations are glitched —,"MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed alongside D173) —"
D293,FIXED,"QoL ask: no discoverable way to quit the game from the UI (M-141, Steam Deck user playtest —","FIXED (M-141) — ""Quit to desktop"" row added to the F10 overlay; build-verified Win+Linux, not yet live-confirmed.…"
D294,OPEN,"Statue: ground geometry near the large CCCP/hammer-and-sickle monument briefly renders solid black over a wide area, then self-recovers 10-30 seconds later… —",OPEN — not investigated.…
D294,FIXED,"Statue: ground geometry near the large CCCP/hammer-and-sickle monument briefly renders solid black over a wide area, then self-recovers 10-30 seconds later… —","FIXED, user playtest confirmed (2026-09-25:"
D295,FIXED,"Campaign-halting crash at the end of Control Center on 00 Agent difficulty, then a crash on every subsequent relaunch/resume (GitHub #87, user campaign playtes…",FIXED (M-149) — M-148 root-caused + reproduced on HEAD; fix landed:
D296,OPEN,"Control Center, 00 Agent:","OPEN — single live report, un-triaged; observed only under a gdb-attached debug-harness launch (tools_pc/debug.ps1), not the packaged release artifact —"
D296,CLOSED,"Control Center, 00 Agent:","CLOSED (2026-09-26, user-approved:"
D297,FIXED,v0.3.0 silently wipes ALL pre-fix player saves on first launch (D284 side effect; found during D295/M-148).,FIXED (M-150) — migration shim landed:
D298,?,Full HUD (not just the aim-mode crosshair) missing in a v0.3.0 dev build —,"s — regression reported 2026-09-16 during v0.3.0 work (build ce8427cf); user says ""don't fix, just for context""; not yet investigated —"
D298,CLOSED,Full HUD (not just the aim-mode crosshair) missing in a v0.3.0 dev build —,"CLOSED (2026-09-26, user:"
D299,FIXED,Game.SkipIntro crashed selecting any save slot except one —,"FIXED — fileValidateSaves() (only called from the skipped legal screen) never ran under SkipIntro, leaving saves[] all-zero; added the missing call to the SkipIntro shortcut (src/game/lv.c, #ifdef PO…"
D300,FIXED,Hipfire look feels non-uniform (issue #89):,"FIXED, user-confirmed live —"
D301,FIXED,"fileGetIsCheatUnlocked() NULL-deref, unguarded on every call site, not just the D299/SkipIntro one (GitHub #87, two reports with an identical crash PC/fault ad…",FIXED locally on release/v0.3.0 (not pushed) —
D302,FIXED,"Render FPS regression to ~40-45fps on release/v0.3.0 (user QA on the local v0.3.0 pre-release build, 2026-09-18) —","FIXED — 4 uncached getenv() calls added by D236 passes 10/12/14/16 directly inside gfx_sp_tri1 (port/fast3d/gfx_pc.cpp, per-triangle) cached with the same static int x = -1 pattern D250 established e…"
D303,OPEN,"Some muzzle flashes (M16 named specifically, ""and some others"") draw wrong —","OPEN — reported only, not yet investigated."
D303,FIXED,"Some muzzle flashes (M16 named specifically, ""and some others"") draw wrong —","FIXED (M-201, 2026-09-26):"
D304,FIXED,"Mouse sensitivity menu overhaul (user request, 2026-09-18):","FIXED — see full entry for both the range/parity pass and the same-day follow-up (collapsed to a single Input.MouseSensitivity menu row + fixed overlayUpdateScroll()'s unconditional bottom-anchor, th…"
D305,FIXED,"Steam Deck SIGSEGV on the v0.3.0 pre-release build (Cavern, early in the level, Game.AllUnlocked previously enabled on the session/save) —",FIXED (defensive guard) —
D306,OPEN,"Dam intro/truck wheels render with odd transparent-looking areas, possibly z-fighting (user QA, 2026-09-18).","OPEN — reported only, not investigated."
D307,OPEN,"In-level security camera entities can be flipped facing the wrong direction (backwards) on some levels, e.g.","OPEN — reported only, not investigated."
D308,OPEN,"Occasional z-fighting confirmed on some levels' geometry, general/broader than the D306 truck-wheel instance (user QA, 2026-09-18).","OPEN — reported/confirmed by the user, not investigated."
D306,CLOSED,"Dam intro/truck wheels render with odd transparent-looking areas, possibly z-fighting (user QA, 2026-09-18).","CLOSED — faithful, confirmed on 1964 (M-201):"
D307,FIXED,"In-level security camera entities can be flipped facing the wrong direction (backwards) on some levels, e.g.","FIXED (M-201, 2026-09-26, user-verified):"
D308,CLOSED,"Occasional z-fighting confirmed on some levels' geometry, general/broader than the D306 truck-wheel instance (user QA, 2026-09-18).","CLOSED — faithful (2026-09-26, user:"
D309,FIXED,Complete non-self-recovering freeze (Windows) returning from Caverns to the intro screen —,FIXED via D310 — user-playtest-confirmed 2026-09-18 (Caverns→intro clean; the spin record itself observed resuming correctly in the post-fix probe log).
D310,FIXED,"RULE-2-SIGNOFF: chraiitemsize() mis-sizes 1-byte AI_PRINT records (NUL-scans forward), corrupting AI execution for any list containing an active PRINT —",FIXED — user-playtest-confirmed 2026-09-18 (v2:
D311,PARTIAL,Complete freeze in modelSetAnimFrame2WithChrStuff root-motion loop —,"GUARDED (#ifdef PORT, D156-guard extension in the same file) —"
D311,FIXED,Complete freeze in modelSetAnimFrame2WithChrStuff root-motion loop —,FIXED — root cause D336 (bheadAdjustAnimation read never-written placeholder fields); the #ifdef PORT guard is retained as a safety net.
D312,FIXED,"Throwable items (mines, grenades, etc.) can clip through walls in certain spots (user report, v0.3.0 pre-release playtest, 2026-09-18).",FIXED — bgBuildRoomVtxBounds mis-reads the G_VTX vertex count on PC (Gfx_le.dma.par bit-layout shift):
D313,FIXED,Bullet impacts do not appear on some walls/surfaces —,"FIXED — same root cause as D312, merged diagnosis; same one-line fix."
D314,OPEN,"F10 menu items flicker randomly/intermittently (reported on the main-menu context, user report, v0.3.0 playtest build, 2026-09-18).",OPEN — confirmed still present post-D316 fix (2026-09-18); user deprioritized chasing it further for now.
D315,OPEN,Menu scaling differs between main menu and in-game —,"OPEN — disclosed as a known issue, deferred to v0.3.1 (bigger design question, not a mechanical fix)."
D314,FIXED,"F10 menu items flicker randomly/intermittently (reported on the main-menu context, user report, v0.3.0 playtest build, 2026-09-18).",FIXED (user playtest 2026-09-26) —
D315,CLOSED,Menu scaling differs between main menu and in-game —,"CLOSED (2026-09-26, user:"
D316,FIXED,"Mouse-clicking some menu items glitches and lands on the item below instead (user report, v0.3.0 playtest build, 2026-09-18).",FIXED — root-caused to two compounding bugs (naive mouse->logical mapping ignoring the safe-area crop; asymmetric row hit-test band) in port/src/optionsoverlay.c + port/fast3d/gfx_pc.cpp.
D317,CLOSED,"Crash when toggling Game.AllUnlocked back and forth in-game (user report, v0.3.0 playtest build, 2026-09-18) —","CLOSED — symptom of D316 (confirmed instant window close, no WER report; the D316 hit-band bias let a click on ""All unlocked"" fall through to the adjacent ""Quit to desktop"" row)."
D318,RESOLVED,Facility (regular play path):,RESOLVED (2026-09-19):
D318,FIXED,Facility (regular play path):,FIXED via D329 (2026-09-26 bookkeeping:
D319,FIXED,"Frame cap slider was mostly a dead zone; replaced with a plain 30/60 toggle (user ask, 2026-09-18).",FIXED — new ROW_FPSCAP cycle (30/60 only) in port/src/optionsoverlay.c; default changed to 60 in port/src/video.c.
D320,OPEN,"D318-class sweep: aim-hold → _update AI lists in 8 levels carry the same as-authored softlock race (static analysis, 2026-09-20).","OPEN — static sweep complete (Facility ai_19, Control ai_9, Depot ai_12 flagged high/med-high; Bond-combat loops likely safe); no live confirmation beyond D318 itself; probe-verify path + generalized…"
D320,FIXED,"D318-class sweep: aim-hold → _update AI lists in 8 levels carry the same as-authored softlock race (static analysis, 2026-09-20).",FIXED via D329 (2026-09-26 bookkeeping:
D321,FIXED,D318 trigger chain fully mapped:,FIXED via D329 (2026-09-26 bookkeeping:
D322,FIXED,Long-session audio degradation:,"FIXED (Windows) for the periodic ""garbled"" bursts —"
D323,CLOSED,G_TL_LOD bindings of explicit-LOD textures:,"CLOSED — parked as a fidelity enhancement, not a bug (2026-09-26, user-approved:"
D324,FIXED,"GE's <stdlib.h> stub declares nothing, so TUs that include it get NO CRT prototypes —",FIXED (2026-09-22) — proven impact:
D325,CLOSED,ROM-mod (xdelta) compatibility:,CLOSED as a question —
D326,CLOSED,Random Eye randomizer repacks the ROM too —,CLOSED as a question —
D327,RESOLVED,"Mod support, resolved plan:",RESOLVED — superseded by D328; approach (a) is NOT ruled out (these scans missed the 1172-compressed data segment).
D328,CLOSED,"Mod support is viable and D327's ""ruled out"" was wrong:",CLOSED as a question —
D329,FIXED,The D318/D320 guard anim pin is a tick-granularity divergence:,"FIXED — Rule-2 high-FPS fix in model.c; clk=1 no longer pins (WSL + native Windows), 0 triggers at clk=2; bunker1 golden failure predates it (on/off A/B); user playtest confirms N64-matching Ourumov…"
D330,FIXED,"#103 (v0.3.0, Win 11):",FIXED (port/src/input.c) —
D331,FIXED,Ejected shell casings never rendered on PC:,"FIXED (gunfire.c #ifdef PORT, explicit template); PP7 casing verified on native Windows (before:"
D332,FIXED,Reticule jitter in aim mode (RMB) —,FIXED (fix/mouse-pd-model) —
D333,FIXED,"#104 (v0.3.0, Linux):",FIXED (2026-09-26 bookkeeping:
D334,FIXED,"Native widescreen, world side (WIDESCREEN-FOV-PLAN Option B Phase 2):",FIXED (2026-09-26 bookkeeping:
D335,FIXED,"Native widescreen, HUD + front end (WIDESCREEN-FOV-PLAN Phase 3):",FIXED (2026-09-26 bookkeeping:
D336,FIXED,"Frigate (and any level) ""all polygons break except the gun"" after turning:","FIXED (src/game/bondhead.c #ifdef PORT, ABI/layout); headless repro (Runway → exit → Frigate → turn, D294 2× room pool) clean, D156 0 fires; golden 3/3; Win + Linux builds."
D337,FIXED,Aim modes folded (user decision):,FIXED (2026-09-26 bookkeeping:
D338,FIXED,"Input.AimRange (F10 ""Aim range""):",FIXED (2026-09-26 bookkeeping:
D339,OPEN,"#92 (low-end ~30 fps, Celeron N3060 / HD 400) —","OPEN (2026-09-26, user:"
D341,FIXED,"Intro gun-barrel blood drip draws as static blocks/lines instead of dripping (user, v0.4.0, 2026-09-26; death blood fine).","FIXED (M-201, user-verified 2026-09-27):"
D342,FIXED,"File select: folder bodies and Bond photos vanish after selecting a file and backing out (user, v0.4.0, 2026-09-26).","FIXED (M-201, user-verified):"
D343,FIXED,"File select: PC ""Options"" label opens the settings overlay (v0.4.0 feature, M1 of the route-B options screen, 2026-09-27); also fixes F10 on file select jumpin…","FIXED (M1, closed): one Rule-2 #ifdef PORT draw hook (user sign-off 2026-09-27); headless + user-verified (mouse, F10, >30 s, widescreen, physical controller on the bottom-bar label and options scree…"
D344,FIXED,Host PC bugchecks (BSOD 0x119 VIDEO_SCHEDULER_INTERNAL_ERROR) during game test runs; probable cause:,"FIXED (probable cause; orderly quit + GE_QUITFRAME), headless + user-verified; no dump, watch for recurrence."
D345,FIXED,"Front-end crosshair snaps away from the pointer on screen/page changes (file select → mode select setCursorPOSforMode(0), front.c:2516; every MENU_PC_OPTIONS p…","Port-only, user-verified (parts a–g):"
D346,CLOSED,PC-settings wording pass:,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D347,FIXED,F10 overlay controller:,"FIXED (port-only, optionsoverlay.c):"
D348,?,"File-select ""PC Options"" label:",SUPERSEDED by D398 (glyph replaced with the IMG_DOT texture; no new art).
D349,CLOSED,GE watch settings are not normally saved to the Bond file:,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D350,CLOSED,Watch-backed PC options:,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D351,FIXED,Existing D346 value/unit rows had positional initializers shifted into hidePtr:,"FIXED (port-only): designated row initializers restore unit, deadzone divisor and hide-if-auto fields; build clean, headless smoke clean."
D352,FIXED,D350 review of 1303c0eb:,"FIXED (port-only, port/src/watchsettings.c):"
D353,FIXED,Turok-style settings regrouping:,"FIXED (port-only, optionsoverlay.c/frontoptions.c):"
D354,FIXED,Front-end F10 Bond rows silently disabled:,"FIXED (port-only, port/src/watchsettings.c):"
D355,FIXED,F10/options Music slider inaudible in-stage:,"FIXED (port-only, port/src/watchsettings.c):"
D356,FIXED,"Settings regroup: Turok-style pages renamed to functional sections (INPUT/GAMEPLAY/GRAPHICS/AUDIO/VIDEO) with PD-style ""(File N)"" scoping annotations, the redu…","FIXED (port-only, port/src/optionsoverlay.{h,c} + port/src/watchsettings.{h,c} + port/src/frontoptions.c; plan docs/dev/D356-SETTINGS-REGROUP-PLAN.md):"
D358,FIXED,D356 level-1 overlap:,"FIXED (port-only, port/src/frontoptions.c):"
D359,CLOSED,"Settings UI file terminology settled on the end-user term ""Profile"":",CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D360,CLOSED,F10 INPUT heading scrolled away; F10 navigation and distance defaults (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D361,FIXED,Main-menu F10 Audio freezes the game:,"FIXED (port-only, port/src/watchsettings.c + F10 probe in optionsoverlay.c):"
D362,CLOSED,F10 GoldenEye dossier-style visual refresh (2026-09-27).,CLOSED (superseded by D363 after user playtest):
D363,FIXED,F10 return to a dark overlay with the N64 watch's green/Bank Gothic visual language (2026-09-27).,"FIXED (palette/font user-approved, 2026-09-27); D364 follows up on profile tags, click alignment and slider calibration."
D364,FIXED,Settings UI feedback:,FIXED (user mostly accepted; D365 follows up on annotation size and click targets).
D365,CLOSED,F10 annotation hierarchy and visible mouse change targets (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D366,CLOSED,Input calibration label:,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D367,?,F10 full-width control boxes misaligned with their contents (2026-09-27).,SUPERSEDED by D368 after user playtest:
D368,?,Simplify F10 click targets:,"INPUT RULE KEPT; tinted strips user-rejected, removed in D369."
D369,FIXED,Remove F10 row boxes/strips entirely (2026-09-27).,FIXED (awaiting live acceptance):
D370,CLOSED,F10 distance/volume drags hurt frame rate (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D371,FIXED,"GEPD key-layout preset + reload/crouch bindings, v0.4.0 modern options wave M3 (2026-09-27).",FIXED (headless-verified; awaiting user live accept):
D372,FIXED,"Low-end perf preset (v0.4.0 modern options wave M5, 2026-09-27).",FIXED (headless-verified; awaiting user live accept):
D373,FIXED,"In-game crosshair on/off + colour (v0.4.0 modern options wave M2, 2026-09-27).",FIXED (user live-tested; rule-2 sign-off recorded 2026-09-27):
D374,FIXED,"GEPD becomes the default key layout; preset layering fix (v0.4.0 M3 playtest feedback, 2026-09-27).",FIXED (headless + user live-tested):
D375,CLOSED,Crouch wired to the wrong pad bit:,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D376,CLOSED,Crouch follow-up + modern-options spot check (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D377,FIXED,GEPD independent crouch (2026-09-27).,FIXED (scripted stance verified; user says live crouch was good; pad/hold-vs-toggle specifics owed):
D378,FIXED,GEPD use/reload separation (2026-09-27).,FIXED (headless edges tested; user says E/R seemed good; targeted door/partial-mag cases owed):
D379,FIXED,Crosshair colour: authored texture is red (2026-09-27).,FIXED (cyan frame capture; live yellow/blue recheck owed):
D380,FIXED,One PC keyboard layout; PD-style rebind design (2026-09-27).,FIXED (follow-up D383):
D381,CLOSED,"RULE-2-SIGNOFF: opt-in RGB, size and Beta crosshair (2026-09-27).",CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D382,CLOSED,Crosshair menu placement and slider hitches (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D383,PARTIAL,PD-style keyboard rebinding in both options UIs (2026-09-27).,PARTIAL (2026-09-28).
D384,CLOSED,Front PC Options key-bind navigation feedback + two-slot UI (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D385,CLOSED,Real mouse 1–5 action rebinding in the two-slot editor (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D386,CLOSED,Correct Aim default from SDL middle (Mouse 2) to right (Mouse 3) (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D387,OPEN,M7 AllUnlocked's read-time fake unlocks leak into saved EEPROM (2026-09-27).,OPEN (isolated user-data-copy save/untoggle repro; no fix):
D388,CLOSED,Nest functional binding/HUD menus and remove redundant M5 toggle (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D389,CLOSED,F10 footer clipping and ambiguous submenu links (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D390,CLOSED,F10 pinned Back selection did not visibly change (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D391,CLOSED,F10 click at glyph bottom could select next row (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
D392,FIXED,Train intro soldier hunched; hand intersects torso vs 1964 —,FIXED (#ifdef PORT ABI/layout in chraction.c); Train aim-angle probe confirms correction; user visually verified the soldier looks fixed against 1964.
D393,FIXED,Controller B/Y both crouch; no in-game use button —,FIXED + user live-tested A use / X reload / Y weapon cycle.
D394,PARTIAL,Xbox 1.1 Jinx controller parity —,PARTIAL (2026-09-28).
D395,RESOLVED,Controller trapped in keyboard/mouse-only binding capture —,"RESOLVED (v0.4.0 campaign sign-off 2026-09-28, user:"
D396,RESOLVED,"F10 pad B/A used as slider decrement/increment, not back/select —","RESOLVED (v0.4.0 campaign sign-off 2026-09-28, user:"
D397,RESOLVED,SELECT FILE background + intro gun-barrel show an interlaced/comb look —,RESOLVED (2026-09-28):
D398,FIXED,PC Options label: D348's procedural slider glyph replaced with an existing mainfolderimages texture (2026-09-27).,"SUPERSEDED by D400 (icon removed, text-only label; purple-hover bug moot; the D400 textMeasure arg-swap bug was found and fixed there)."
D399,PARTIAL,File-select bottom row (SELECT FILE text + Copy/Erase + PC Options dot/label) shifted 15px left as a block (user sign-off 2026-09-27).,"LANDED (port-only, D343 class):"
D400,PARTIAL,"File-select ""PC Options"" label:","LANDED (port-only, frontoptions.c):"
D401,CLOSED,Rumble Pak → real gamepad haptics (re-promotes D224/M-87):,CLOSED (2026-09-28). Rumble shipped in v0.4.0 (D401); Steam Deck real-pad validation:
D402,RESOLVED,First clean Linux (glibc / SteamOS) build of release/v0.4.0 broke in two port-layer include-resolution spots MinGW tolerated (bondconstants.h first-include cyc…,RESOLVED (Linux/Ubuntu-24.04 build → glibc-2.41-compatible binary for the Steam Deck):
D403,RESOLVED,Deck v0.4.0-pre test:,"RESOLVED (2026-09-27, user-confirmed on the Deck:"
D404,RESOLVED,"""Aim style: Centred (PC)"" (Input.AimMode=1, D333/D337) has no effect when aiming with a gamepad; the row's own doc comment over-promises (""applies to every aim…","RESOLVED (2026-09-28, port-layer only):"
D405,CLOSED,"fast3d/OpenGL: MSAA above the driver max black-screens; two glBindFramebuffer calls pass an index where a GL name is required (PR #106, italoarruda, 2026-09-25…","CLOSED (2026-09-28). Merged from PR #106 (code file only; security-reviewed clean - port-layer GL state fixes, no external input, no allocations)."
D406,RESOLVED,front-end PC Options screen:,"RESOLVED (2026-09-28, port-layer only, Steam Deck-validated):"
D407,PARTIAL,Tanks cannot be boarded/exited on PC —,"PARTIAL (fix landed in v0.4.0, port-only):"
D408,OPEN,Game.SkipIntro skips the post-mission failure dossier:,"OPEN (cosmetic, not root-caused past the handoff site)."
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
7 D51-D58
8 D59-D68
9 D70-D74
10 D75
11 D76 · D164
12 D159
13 D161
14 D165 · D166
15 D180 · D181
16 D186
17 D187
18 D188
19 D189
20 D190
21 D191
22 D193
23 D194
24 D195
25 D197
26 D198
27 D199
28 D200
29 D196
30 D192
31 D221
32 D222
33 D223
34 D224
35 D225
36 D226
37 D227
38 D229
39 D230
40 D231
41 D233
42 D234
43 D244
45 D204
46 D203
47 D201
48 D202
49 D206
50 D207
51 D208
52 D209
53 D210
56 D168
57 D169
58 D178
59 D175
60 D176
61 D177
62 D172
63 D174
64 D183
65 D173
66 D179
67 D180
68 D181
69 D184
70 D211
71 D212
72 D213
73 D214
74 D215
75 D216
76 D217
77 D218
78 D219
79 D220
80 D85
81 D86 · D87
82 D88.4
83 D132
84 D157
85 D160
86 D89-D92
87 D93-D102
88 D103-D107
91 D117
92 D118
93 D150
94 D260-D265
95 D266-D269
96 D270
97 D271
98 D276
99 D279
100 D281
101 D282
102 D283
103 D280
104 D275
105 D274
106 D273
107 D272
108 D137
109 D119 · D120
110 D120
122 D139
123 D140
124 D141
125 D152
126 D154
127 D156
128 D155
129 D153
130 D151
132 D164
133 D165
134 D166
135 D235
136 D236
137 D237
138 D238
139 D239
140 D240
141 D241
142 D242
143 D243
144 D245
145 D246
146 D247
147 D248
148 D249
149 D250
150 D251
151 D252
152 D253
153 D254
154 D255
160 D262
161 D264
162 D263
163 D265
164 D258
165 D284
166 D285
167 D286
168 D287
169 D288
170 D289
171 D290
172 D291
173 D292
174 D293
175 D294
176 D295
177 D296
178 D297
179 D298
180 D299
181 D300
182 D301
183 D302
184 D303
185 D304
186 D305
187 D306
188 D307
189 D308
190 D309
191 D310
192 D311
193 D312
194 D313
195 D314
196 D315
197 D316
198 D317
199 D318
200 D319
201 D320
202 D321
203 D322
204 D323
205 D324
206 D325
207 D326
208 D327
209 D328
210 D329
211 D330
212 D331
213 D332
214 D333
215 D334
216 D335
217 D336
218 D337
219 D338
220 D339
221 D341
222 D342
223 D343
224 D344
225 D345
226 D346
227 D347
228 D348
229 D349
230 D350
231 D351
232 D352
233 D353
234 D354
235 D355
236 D356
237 D358
238 D359
239 D360
240 D361
241 D362
242 D363
243 D364
244 D365
245 D366
246 D367
247 D368
248 D369
249 D370
250 D371
251 D372
252 D373
253 D374
254 D375
255 D376
256 D377
257 D378
258 D379
259 D380
260 D381
261 D382
262 D383
263 D384
264 D385
265 D386
266 D387
267 D388
268 D389
269 D390
270 D391
271 D392
272 D393
273 D394
274 D395
275 D396
276 D397
277 D398
278 D399
279 D400
280 D401
281 D402
282 D403
283 D404
284 D405
285 D406
286 D407
287 D408
+3413 -85
View File
File diff suppressed because it is too large. Load diff
+29
View File
@@ -303,3 +303,32 @@ offer to create it).
2. Interim frozen `ge007-convert.exe` wired to first-run — days, optional.
3. Part B C port of `d43`/`d69` + embedded metadata + output diffing — weeks.
4. Drop the interim binary; single-binary drop-in experience complete.
## Part B follow-up backlog (was GitHub issue #85, folded here 2026-09-25)
Follow-up to #6, which shipped in v0.2.0 as an NTSC-U-only drop-in (PR #84). The issue was closed on GitHub and its scope is tracked here now. It has three independent workstreams.
### 1. PAL/JP asset pipeline repair (data + build)
`scripts/filelist.e.csv` and `filelist.j.csv` contain naming defects that make PAL/JP conversion impossible from this tree (D258: no PAL/JP PC build has ever been producible). The repair path has been verified but not applied:
- **PAL:** rename 31 model rows to the exact `file_resource_table.inc.c` symbols. Examples: `Pbridge_console` ×6 → `Pbridge_console1aZ…3bZ`, and `Pchrgrenadelauncher` → `PchrgrenadelaunchZ`. Greedy sequence alignment proves the mapping: 512×512 exact 1:1, offsets non-decreasing, and zero size anomalies against US.
- **JP:** rename the mislabeled font row @3044276 from `fontZurichBold_fontchartable.bin` to `fontBankGothic_fontchartable.bin`. The contiguity invariant confirms it: the Gothic kerning table @3043600+676 ends exactly at this row, and the size matches the US Gothic chartable.
- **Then:**
- `gen_romassets.py e/j` must run clean without a ROM.
- Commit `romassets_e.s` and `romassets_j.s`.
- Replace the hardcoded `filelist.u.csv` in the d43/d69/d88 emitters with a region→suffix map.
- Remove the NTSC-U-only gate in `prepare-assets.py`.
- **Needs a PAL and/or JP ROM** for final byte-level verification of at least one region. Only NTSC is available locally.
### 2. Multi-region release layout
- Give each region its own OUTPUT_NAME (`ge007-ntsc-final` / `-pal-final` / `-jpn-final`) plus a default alias.
- Add a region re-exec guard in `romdata.c` (~50 lines): if the country byte at 0x3E doesn't match the build, re-exec the sibling exe.
- CI builds all 3 regions sequentially, and the bundle scripts package all 3 exes.
### 3. C port of the emitters (optional, removes PyInstaller)
Port the d43/d69/d88 emit logic to C. That removes the frozen-converter dependency entirely, because the converter becomes part of the engine binary. It's a 2–4 week task, and PD's `filemodel.c` is the closest analogue. It isn't required for PAL/JP: the frozen Python path works for all three regions once #1 lands.
**Acceptance:** a user with a PAL or JP ROM gets the same one-step drop-in experience as NTSC does today.
Binary file not shown.

After

Width:  |  Height:  |  Size: 910 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

+12 -26
View File
@@ -1,27 +1,13 @@
new_name,original,video_session,snapshot_time
shot-01.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_02.11.149.jpg,2026.09.13 - 16.05.44.10,02:11:149
shot-02.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_06.00.446.jpg,2026.09.13 - 16.05.44.10,06:00:446
shot-03.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_07.56.664.jpg,2026.09.13 - 16.05.44.10,07:56:664
shot-04.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_08.33.264.jpg,2026.09.13 - 16.05.44.10,08:33:264
shot-05.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_10.16.041.jpg,2026.09.13 - 16.05.44.10,10:16:041
shot-06.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_11.03.430.jpg,2026.09.13 - 16.05.44.10,11:03:430
shot-07.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_02.25.190.jpg,2026.09.13 - 16.54.07.12,02:25:190
shot-08.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_03.05.000.jpg,2026.09.13 - 16.54.07.12,03:05:000
shot-09.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_05.39.999.jpg,2026.09.13 - 16.54.07.12,05:39:999
shot-10.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_05.55.651.jpg,2026.09.13 - 16.54.07.12,05:55:651
shot-11.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_06.25.650.jpg,2026.09.13 - 16.54.07.12,06:25:650
shot-12.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_09.33.997.jpg,2026.09.13 - 16.54.07.12,09:33:997
shot-13.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_12.09.456.jpg,2026.09.13 - 16.54.07.12,12:09:456
shot-14.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_12.59.497.jpg,2026.09.13 - 16.54.07.12,12:59:497
shot-15.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_13.09.496.jpg,2026.09.13 - 16.54.07.12,13:09:496
shot-16.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_15.22.656.jpg,2026.09.13 - 16.54.07.12,15:22:656
shot-17.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_15.39.495.jpg,2026.09.13 - 16.54.07.12,15:39:495
shot-18.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_16.54.496.jpg,2026.09.13 - 16.54.07.12,16:54:496
shot-19.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_17.29.495.jpg,2026.09.13 - 16.54.07.12,17:29:495
shot-20.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_17.39.495.jpg,2026.09.13 - 16.54.07.12,17:39:495
shot-21.jpg,Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_00.21.484.jpg,2026.09.13 - 17.49.35.18,00:21:484
shot-22.jpg,Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_00.31.328.jpg,2026.09.13 - 17.49.35.18,00:31:328
shot-23.jpg,Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_02.03.469.jpg,2026.09.13 - 17.49.35.18,02:03:469
shot-24.jpg,Build-pc 2026.09.13 - 17.54.08.19.mp4_snapshot_00.11.972.jpg,2026.09.13 - 17.54.08.19,00:11:972
shot-25.jpg,Build-pc 2026.09.13 - 17.54.08.19.mp4_snapshot_02.36.532.jpg,2026.09.13 - 17.54.08.19,02:36:532
shot-26.jpg,settings menu.jpg,settings-menu,
shot-01.jpg,v040/dam0.png,v0.4.0 attract pass 2026.09.28,
shot-03.jpg,v040/runway.png,v0.4.0 attract pass 2026.09.28,
shot-04.jpg,v040/surface 1.png,v0.4.0 attract pass 2026.09.28,
shot-09.jpg,v040/frigate 0.png,v0.4.0 attract pass 2026.09.28,
shot-11.jpg,v040/surface 2.png,v0.4.0 attract pass 2026.09.28,
shot-16.jpg,v040/statue 0.png,v0.4.0 attract pass 2026.09.28,
shot-20.jpg,v040/archives 0.png,v0.4.0 attract pass 2026.09.28,
shot-21.jpg,v040/cradle 0.png,v0.4.0 attract pass 2026.09.28,
shot-24.jpg,v040/aztec 0.png,v0.4.0 attract pass 2026.09.28,
shot-27.jpg,v040/control 0.png,v0.4.0 attract pass 2026.09.28,
shot-28.jpg,v040/streets 0.png,v0.4.0 attract pass 2026.09.28,
shot-29.jpg,v040/jungle.png,v0.4.0 attract pass 2026.09.28,
1 new_name original video_session snapshot_time
2 shot-01.jpg Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_02.11.149.jpg v040/dam0.png 2026.09.13 - 16.05.44.10 v0.4.0 attract pass 2026.09.28 02:11:149
3 shot-02.jpg shot-03.jpg Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_06.00.446.jpg v040/runway.png 2026.09.13 - 16.05.44.10 v0.4.0 attract pass 2026.09.28 06:00:446
4 shot-03.jpg shot-04.jpg Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_07.56.664.jpg v040/surface 1.png 2026.09.13 - 16.05.44.10 v0.4.0 attract pass 2026.09.28 07:56:664
5 shot-04.jpg shot-09.jpg Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_08.33.264.jpg v040/frigate 0.png 2026.09.13 - 16.05.44.10 v0.4.0 attract pass 2026.09.28 08:33:264
6 shot-05.jpg shot-11.jpg Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_10.16.041.jpg v040/surface 2.png 2026.09.13 - 16.05.44.10 v0.4.0 attract pass 2026.09.28 10:16:041
7 shot-06.jpg shot-16.jpg Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_11.03.430.jpg v040/statue 0.png 2026.09.13 - 16.05.44.10 v0.4.0 attract pass 2026.09.28 11:03:430
8 shot-07.jpg shot-20.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_02.25.190.jpg v040/archives 0.png 2026.09.13 - 16.54.07.12 v0.4.0 attract pass 2026.09.28 02:25:190
9 shot-08.jpg shot-21.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_03.05.000.jpg v040/cradle 0.png 2026.09.13 - 16.54.07.12 v0.4.0 attract pass 2026.09.28 03:05:000
10 shot-09.jpg shot-24.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_05.39.999.jpg v040/aztec 0.png 2026.09.13 - 16.54.07.12 v0.4.0 attract pass 2026.09.28 05:39:999
11 shot-10.jpg shot-27.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_05.55.651.jpg v040/control 0.png 2026.09.13 - 16.54.07.12 v0.4.0 attract pass 2026.09.28 05:55:651
12 shot-11.jpg shot-28.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_06.25.650.jpg v040/streets 0.png 2026.09.13 - 16.54.07.12 v0.4.0 attract pass 2026.09.28 06:25:650
13 shot-12.jpg shot-29.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_09.33.997.jpg v040/jungle.png 2026.09.13 - 16.54.07.12 v0.4.0 attract pass 2026.09.28 09:33:997
shot-13.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_12.09.456.jpg 2026.09.13 - 16.54.07.12 12:09:456
shot-14.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_12.59.497.jpg 2026.09.13 - 16.54.07.12 12:59:497
shot-15.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_13.09.496.jpg 2026.09.13 - 16.54.07.12 13:09:496
shot-16.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_15.22.656.jpg 2026.09.13 - 16.54.07.12 15:22:656
shot-17.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_15.39.495.jpg 2026.09.13 - 16.54.07.12 15:39:495
shot-18.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_16.54.496.jpg 2026.09.13 - 16.54.07.12 16:54:496
shot-19.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_17.29.495.jpg 2026.09.13 - 16.54.07.12 17:29:495
shot-20.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_17.39.495.jpg 2026.09.13 - 16.54.07.12 17:39:495
shot-21.jpg Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_00.21.484.jpg 2026.09.13 - 17.49.35.18 00:21:484
shot-22.jpg Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_00.31.328.jpg 2026.09.13 - 17.49.35.18 00:31:328
shot-23.jpg Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_02.03.469.jpg 2026.09.13 - 17.49.35.18 02:03:469
shot-24.jpg Build-pc 2026.09.13 - 17.54.08.19.mp4_snapshot_00.11.972.jpg 2026.09.13 - 17.54.08.19 00:11:972
shot-25.jpg Build-pc 2026.09.13 - 17.54.08.19.mp4_snapshot_02.36.532.jpg 2026.09.13 - 17.54.08.19 02:36:532
shot-26.jpg settings menu.jpg settings-menu
Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

+88 -104
View File
@@ -1,43 +1,70 @@
---
title: GoldenEye 007 PC Port
# v0.3.0: status bumped from v0.2.1; Honest-status bullets synced with the
# README's v0.3.0 known issues (D255/D282/F10-dup/overscan items removed as
# fixed). Description kept at 133 chars (Bing's 160 limit).
# v0.4.0: status bumped from v0.3.0; Honest status replaced by the v0.4.0
# release-notes known issues (rainbow/water/z-fight/cameras/trees/muzzle/
# stretched-widescreen/gunshot items all fixed; D335b F10-overlay stretch is
# the new widescreen residual). Description kept short (Bing's 160 limit).
description: >-
A native PC port of the 1997 N64 classic, built from decompiled source with
a software RSP. v0.3.0 for Windows, Linux and Steam Deck.
A native PC port of GoldenEye 007 (Nintendo 64, 1997), built from
decompiled source with a software RSP. v0.4.0 for Windows, Linux and
Steam Deck.
---
<!-- v0.2.1 (Bing SEO): the "# GoldenEye 007 PC Port" h1 was removed here --
the Cayman masthead already renders the site title as an <h1>, so the page
was emitting two h1 tags. -->
A native PC port of _GoldenEye 007_ (Rare, 1997, Nintendo 64), compiled from
the [GoldenEye 007 decompilation](https://github.com/n64decomp/007): the
original N64 game running from reconstructed source, not the Xbox 360
remaster. The N64's graphics coprocessor (RSP) is emulated in software; every
other hardware surface (video, audio, input, timers, save storage) is shimmed
in a dedicated `port/` layer, following the architecture of the
[Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark), the same
Rare "Indy" engine family, one hardware generation apart.
A native PC port of the original 1997 Nintendo 64 _GoldenEye 007_, compiled
from the game's [decompiled source](https://github.com/n64decomp/007) with the
N64's graphics coprocessor (RSP) running in software, the same architecture
as the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark), the
same Rare "Indy" engine family, one hardware generation apart.
[Download](#download) · [News](#news) · [See it running](#see-it-running) · [Honest status](#honest-status) · [Documentation](#documentation)
**Status: v0.3.0.** The full single-player campaign runs at a steady 60 fps
and is completable end to end (all 21 missions playtested); all 21 missions
load and run clean on Windows, Linux and Steam Deck, and audio (music + SFX)
plays throughout. The known rough edges — mostly cosmetic rendering defects,
stretched-not-native widescreen, and missing features — are listed plainly
**Status: v0.4.0 (2026-09-28) — fully playable, with a small set of known
caveats.** The full single-player campaign runs at a steady 60 fps and is
completable end to end (all 20 missions, plus the ending-credits sequence,
playtested on Windows, Linux and Steam Deck). It is the most complete release
to date: **native widescreen** (on by default — undistorted world at your
display's aspect, 4:3 menus pillarboxed), a **complete aim system** (N64 or
centred PC style, for mouse *and* controller), **in-game key rebinding** with
a GEPD-style default layout, **crosshair customization**, **rumble-pak
haptics** on supported gamepads, and a **rebuilt options overlay** with
fine-tuning rows (frame cap, MSAA, draw/LOD distance, HUD scale). The most
common defect classes from earlier releases — particle colour drift, water
seams, z-fighting, billboard trees, muzzle flashes, the front-end Nintendo
logo, gunshot SFX — are fixed in this version; what remains is a short list,
under [Honest status](#honest-status).
**This is a pre-1.0 release, not a finished product** — v1.0 is the target
for a polished, feature-complete build; expect rough edges and missing
features until then. See the
for a polished, feature-complete build; expect missing features and the
occasional breaking change until then. See the
[README's Roadmap section](https://github.com/jkdansereau/goldeneye-pc-port#roadmap)
for direction (PAL/JP support, real widescreen, LAN multiplayer under
consideration, and more).
for direction (PAL/JP support, controller-button rebinding, LAN multiplayer
under consideration, and more).
<p align="center">
<img src="media/goldeneye-gh-preview.gif" width="70%"
alt="~12 s gameplay montage from live play sessions">
<br><em>~12 s gameplay montage from live v0.3.0 play sessions, running in the port.</em>
alt="~24 s gameplay montage from live play sessions">
<br><em>~24 s gameplay montage from live v0.4.0 play sessions (Runway tank,
Dam, Caverns, Aztec, Bunker&nbsp;2, Surface&nbsp;2), running in the port.</em>
</p>
## News
- **2026-09-28** — **v0.4.0**: native widescreen, a complete aim system for
mouse and controller, in-game key rebinding, crosshair customization,
rumble-pak haptics, a rebuilt options overlay, and a broad fidelity-fix
pass (water, particles, billboard trees, front-end logo, gunshot SFX, a
true stable 60 fps). [Release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.4.0) ·
[downloads](#download).
- **2026-09-20** — **v0.3.0**: the first release with the complete campaign
playable end to end at 60 fps on Windows, Linux, and Steam Deck.
[Release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.3.0).
- **2026-09-04 → 2026-09-16** — **v0.1.0 – v0.2.2**: the alpha and beta
cycle — build chain, software RSP, first rendered frames, front end, and
per-level stabilization across the campaign.
## Download
| Platform | Bundle | Notes |
@@ -59,89 +86,61 @@ without a ROM you already own.
## See it running
<p align="center">
<img src="img/shots/shot-06.jpg" width="45%" alt="Bunker 1, rendered by the port (v0.2.0 playtest)">
<img src="img/shots/shot-01.jpg" width="45%" alt="Dam, rendered by the port (v0.2.0 playtest)">
<img src="img/shots/shot-28.jpg" width="45%" alt="Streets, rendered by the port (v0.4.0 intro attract)">
<img src="img/shots/shot-01.jpg" width="45%" alt="Dam, rendered by the port (v0.4.0 intro attract)">
</p>
<details>
<summary><strong>Full playtest gallery</strong>: 19 in-engine captures from the v0.2.0 Windows playtest, across the campaign</summary>
<summary><strong>Full gallery</strong>: 12 in-engine captures from a v0.4.0 intro-attract pass (3072×1728 widescreen, Sep 2026)</summary>
<p align="center">
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-01.jpg" width="100%" alt="In-engine: Dam (v0.2.0)">
<img src="img/shots/shot-01.jpg" width="100%" alt="In-engine: Dam (v0.4.0 intro attract)">
<div><small>Dam</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-02.jpg" width="100%" alt="In-engine: Facility (v0.2.0)">
<div><small>Facility</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-03.jpg" width="100%" alt="In-engine: Runway (v0.2.0)">
<img src="img/shots/shot-03.jpg" width="100%" alt="In-engine: Runway (v0.4.0 intro attract)">
<div><small>Runway</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-04.jpg" width="100%" alt="In-engine: Surface (v0.2.0)">
<img src="img/shots/shot-04.jpg" width="100%" alt="In-engine: Surface (v0.4.0 intro attract)">
<div><small>Surface</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-05.jpg" width="100%" alt="In-engine: Surface (v0.2.0)">
<div><small>Surface</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-06.jpg" width="100%" alt="In-engine: Bunker 1 (v0.2.0)">
<div><small>Bunker 1</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-07.jpg" width="100%" alt="In-engine: Silo (v0.2.0)">
<div><small>Silo</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-09.jpg" width="100%" alt="In-engine: Frigate (v0.2.0)">
<img src="img/shots/shot-09.jpg" width="100%" alt="In-engine: Frigate (v0.4.0 intro attract)">
<div><small>Frigate</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-10.jpg" width="100%" alt="In-engine: Frigate (v0.2.0)">
<div><small>Frigate</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-11.jpg" width="100%" alt="In-engine: Surface 2 (v0.2.0)">
<img src="img/shots/shot-11.jpg" width="100%" alt="In-engine: Surface 2 (v0.4.0 intro attract)">
<div><small>Surface 2</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-13.jpg" width="100%" alt="In-engine: Bunker 2 (v0.2.0)">
<div><small>Bunker 2</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-16.jpg" width="100%" alt="In-engine: Statue (v0.2.0)">
<img src="img/shots/shot-16.jpg" width="100%" alt="In-engine: Statue (v0.4.0 intro attract)">
<div><small>Statue</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-19.jpg" width="100%" alt="In-engine: Statue (v0.2.0)">
<div><small>Statue</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-20.jpg" width="100%" alt="In-engine: Archives (v0.2.0)">
<img src="img/shots/shot-20.jpg" width="100%" alt="In-engine: Archives (v0.4.0 intro attract)">
<div><small>Archives</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-21.jpg" width="100%" alt="In-engine: Cradle (v0.2.0)">
<img src="img/shots/shot-21.jpg" width="100%" alt="In-engine: Cradle (v0.4.0 intro attract)">
<div><small>Cradle</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-23.jpg" width="100%" alt="In-engine: Cradle (v0.2.0)">
<div><small>Cradle</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-24.jpg" width="100%" alt="In-engine: Aztec (v0.2.0)">
<img src="img/shots/shot-24.jpg" width="100%" alt="In-engine: Aztec (v0.4.0 intro attract)">
<div><small>Aztec</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-25.jpg" width="100%" alt="In-engine: Aztec (v0.2.0)">
<div><small>Aztec</small></div>
<img src="img/shots/shot-27.jpg" width="100%" alt="In-engine: Control (v0.4.0 intro attract)">
<div><small>Control</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-26.jpg" width="100%" alt="In-engine: Frigate (v0.2.0)">
<div><small>Frigate</small></div>
<img src="img/shots/shot-28.jpg" width="100%" alt="In-engine: Streets (v0.4.0 intro attract)">
<div><small>Streets</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-29.jpg" width="100%" alt="In-engine: Jungle (v0.4.0 intro attract)">
<div><small>Jungle</small></div>
</div>
</p>
</details>
@@ -190,34 +189,25 @@ catalogue: [Porting notes](porting-notes.md).
## Honest status
- **Particle colours** can still drift through a rainbow palette
intermittently: one genuine cause is fixed this release, a second hasn't
been found yet (D252).
- **Some muzzle flashes draw an extra erroneous long flash** straight up from
the gun, on top of the correct one. Cosmetic only (D303).
- Water levels show a moving seam between two water patterns (D245);
occasional z-fighting on some geometry (D308); in-level security-camera
props can occasionally face backwards (D307). Minor and cosmetic.
- **The front-end Nintendo logo renders as two white blobs** and the Rareware
logo's texture filtering looks off (D75).
- Surface 1's 2D billboard trees render as a solid wall of tree texture
instead of discrete sprites; under active investigation (D236).
- **Widescreen is stretched, not native**: 16:9 stretches the 4:3 frame to
fill the display; automatic FOV scaling keeps the framing comfortable and
gameplay is unaffected, but a distortion-free native widescreen render is
still on the roadmap. Distant geometry can also drop out on the biggest
open levels (D249).
- **Gunshot SFX can sound off during sustained/rapid fire** (D240/D241).
- **`All unlocked` is highly experimental** — don't enable it until you have
at least one save written, or it can break audio and mouse aim
(D257/D259/D281).
- Bond's cutscene positioning is fixed and right the large majority of the
time now; on the rare occasion he's off it's a small drift — no more
floating or spin-glitching (D173/D292/D243).
- No macOS/ARM support; no controller rebinding UI yet.
- On Facility, if gas leaks during Ourumov's monologue he can pause for up to
~10 s before resuming the scripted shootout — a latent race that exists in
the N64 original too (where it softlocks permanently); the port detects and
auto-recovers it. (D318)
- With native widescreen on, the F10 options overlay stretches with the
window instead of pillarboxing like the front-end menus (legible; cosmetic;
the F10 *Native widescreen* toggle restores the old stretched frame
throughout). The world/HUD widescreen rendering itself is correct. (D335b)
- The front-end Rareware logo shows a subtle texture-filtering artifact
(the Nintendo logo and legal page are clean). Cosmetic only. (D75)
- **`All unlocked` is experimental**: the fresh-install corruption case is
fixed, but saves made while it is on are not guaranteed recoverable by
switching it off — complete a level normally first, and back up
`data/ge007.eep` before enabling it. (D387)
- No macOS or ARM support. Keyboard/mouse rebinding shipped in v0.4.0;
controller-button rebinding is not supported yet.
The full list, with root causes and fix status: the
[README's Status section](https://github.com/jkdansereau/goldeneye-pc-port#status)
[release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases)
and the [finding log](https://github.com/jkdansereau/goldeneye-pc-port/tree/main/docs/dev).
## Documentation
@@ -227,9 +217,3 @@ and the [finding log](https://github.com/jkdansereau/goldeneye-pc-port/tree/main
- [Internals](internals.md): architecture, the software RSP-emulation approach, GoldenEye-vs-Perfect-Dark engine differences, the phased plan.
- [Porting notes](porting-notes.md): the recurring Nintendo 64 → PC bug classes hit during the port, with fixes.
- [Building](building.md): full build and asset-extraction guide.
---
<small>Non-commercial fan preservation/research project. No ROM or game assets
are distributed; you supply a ROM you already own. Not affiliated with or
endorsed by Nintendo, Rare, Microsoft, MGM, Danjaq, or EON Productions.</small>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 MiB

After

Width:  |  Height:  |  Size: 14 MiB

+250
View File
@@ -28,6 +28,8 @@ good that you are looking at one of these.
- [D3. GCC/mingw makes an all-non-negative `enum` UNSIGNED](#d3-gccmingw-makes-an-all-non-negative-enum-unsigned)
- [D4. N64 "interrupts off" must be a real lock on PC](#d4-n64-interrupts-off-is-not-free-on-pc--it-must-be-a-real-lock)
- [D5. Loop bounds that assume linker adjacency of two globals](#d5-loop-bounds-that-assume-linker-adjacency-of-two-file-scope-globals)
- [D12. Collapsing an LOD binding to tile 0 breaks a DETAIL binding](#d12-collapse-an-lod-binding-to-tile-0-is-wrong-for-a-two-texture-detail-binding-d236)
- [D13. Per-render accumulation is not associative (1 vs 2–3 ticks/frame)](#d13-per-render-accumulation-is-not-associative-the-ports-1-tickframe-can-reach-states-the-n64s-23-ticksframe-never-did-d329)
- [E. Process / method notes](#e-process--method-notes)
## A. Pointer-width struct growth (32→64): the dominant class
@@ -105,6 +107,15 @@ for hardcoded indices into any `padding[]`/`u8[]` union arm.
through the correctly-typed sibling field (`ObjectRecord.model`)
instead, `#ifdef PORT`, N64 line/cast kept verbatim under `#else`.
§F **D255** (M-140).
- **D392** (the firing-guard pose variant): `chrlvUpdateAimendbackShoulders`
reads `((f32*)arg1)[12/13/16/17]` into a
`weapon_firing_animation_table` whose *first* member is a union of a
`s32` and a `ModelAnimation *`. N64's 4-byte union places those slots at
`max_up/max_down/free_arm_frac_up/free_arm_frac_down`; on PC the 8-byte
union shifts the floats, so the raw indices read `aim_end_frame/max_up/
max_right/free_arm_frac_up`. The wrong aim clamp bends the torso ~49° and
pushes the hands through it. Under `#ifdef PORT`, use the four named
fields; retain the raw indices for N64. §F D392.
**Lesson.** When a value reads as a clean constant (0, 1, NULL) rather
than garbage, a pointer byte is a prime suspect; the high bytes of a
@@ -249,6 +260,36 @@ framing — it never looked like game logic to begin with.
decompilation of what the write side already does, `#ifdef PORT` with the
N64 hand-rolled offset kept verbatim under `#else`. §F **D248**.
- **One layout bug often has sibling call sites.** D264 fixed a
cross-global `ModelRenderData` template read in the watch item preview;
the identical read in `watchRenderController` (`D_80035D04 + 0x3c`) stayed
broken until D290. When fixing a raw-offset global read, grep for every
other read of the same globals (`D_80035D00`/`D_80035D04` here) before closing.
- **When a split N64 struct is made inline on PC, its placeholder fields
become dead, and any code still reading them reads uninitialised memory.**
D100 turned `player.model` into a real inline `struct Model`, but the
decomp's `field_59C..field_650` placeholders (raw N64 offsets into that
model) stayed as separate fields. `bheadAdjustAnimation` kept reading
`field_5C0` (= N64 `model.animframe1`) and `animFlipFlag` (= `model.gunhand`),
which nothing ever wrote (D336: garbage anim frames, Frigate scene wrecked,
sensitive to memory layout). After inlining a split struct, grep every
placeholder in the old range and remap each read to the real field.
Diagnostic tell: a hardware watchpoint on the "poisoned" field sees *no
writes at all*.
### A-dup. Renaming an IDO duplicate member re-points every read of it (D307)
IDO accepts a struct that redeclares an inherited member name (`inherits ObjectRecord;` then its own `pad`); GCC rejects it, so the port renames one. **The rename changes which field existing code reads**: on N64 the later declaration shadows the inherited one, so `rec->pad` meant the derived record's own field. After renaming it (`lookpad`), unchanged game code silently binds to the inherited field instead. D307: `setupCctv` read the mount pad as the look-at pad and every security camera faced its wall. When a port rename resolves a duplicate, grep every `->name` on that struct type and decide per site which field N64 meant (PD's named-field sources are the tie-breaker).
### A-cnt. A record's "count" field may not count what the converter copies (D303)
The offline model converter sizes each record's trailing arrays from its count field. Check what the *runtime consumer* does with that count before trusting its name: GE's op-22 `DisplayListPrimaryRecord.numVertices` is really the number of star-flash ARMS, and `dorottex()` reads `count*4` vertices (PD names it `stargunfire.unk00` and reads `unk00*4`). Sizing by the field name dropped 3/4 of every muzzle star; the missing vertices came from neighbouring sidecar bytes and drew as spikes. For every record type, derive the copied extent from the reading loop, and cross-check PD's typed struct.
### A-buf. Growing one region of a shared buffer must move every hard-coded offset after it (D342)
When a PC-larger asset gets a bigger reservation inside a shared scratch buffer (D45: the wallet model 0xA000 -> 0x17000 in `ptr_logo_and_walletbond_DL`), grep for every other user of that buffer that computes a fixed offset (`base + 4096*10`). Those still point into the old boundary and silently overwrite the enlarged asset; the corruption only shows when the asset is not reloaded.
## B. 16-byte PC `Gfx` / `Vtx` vs 8-byte N64
Any buffer reservation, `memcpy` size, slot stride, or pool budget
@@ -556,12 +597,42 @@ through a converter or a runtime bswap fixup reads scrambled.
(exact for hold-to-aim; a toggle scheme needs `g_CurrentPlayer->
insightaimmode`). GE's native pitch is **inverted** (C-up → look down);
hide it so mouse-down looks down; `MouseInvertY` flips (D118, M-24).
- **N64 A/B bit names are not modern pad A/B semantics (D393).** GE's
gameplay `GE_CONT_A` cycles weapons and `GE_CONT_B` contextually uses or
reloads. Assigning Xbox A/X both to native A and B/Y both to independent
crouch leaves *no* use button. For modern controls, map A to a dedicated
use edge, X to a dedicated reload edge, B to crouch, Y to a weapon-cycle
edge; preserve native accept/cancel bits in front menus/watch/tank. Track
held buttons across UI transitions so closing F10 does not trigger an
unexpected use/reload/cycle.
- `osContGetReadData(pad)` must fill **one OSContPad per channel**
(`MAXCONTROLLERS`-long array), not just controller 0; joy.c passes the
whole `samples[i].pads` array (D118).
- Controller state has one source: `port/src/input.c`. `libultra.c`'s SI
section marshals `inputComputePad()` into `g_contPad[]`; it is driven by
`osContStartReadData` (per logic tick), no separate `video.c` frame hook.
- **The in-level watch/pause is not a "menu" to `input.c`** (`current_menu`
stays `RUN_STAGE`), but it reads the stick as **discrete** input: ±0x2E
on X picks an option value or flips a page, and Y scrolls rows. Any path
that turns a continuous device (the mouse) into stick deflection must be
gated off there (`!outside_watch_menu || pause_state`), or small hand
motion becomes phantom menu choices. D330/#103: mouse → legacy stick
fallback silently reset Auto-Aim/Look-Ahead to OFF. The direct-write
look paths already gate on this; the fallbacks must too.
- **A port-side shadow of game data must be re-synced every time the game
re-derives the original, not just the first time.** D252: `texReset()`
re-resolves the global image DLs' texture pointers on *every* stage load;
the compiled shadow copies (`gimgSyncCompiledGlobalDLs`, D68) only accepted
a write while a slot still held its unresolved marker, so every stage after
the title kept pointers into freed memory. It looked fine until the new
stage overwrote that region (rainbow explosions mid-level). Key a sync on
*which slots* are synced (learned once), never on "still unresolved".
Headless `-level_XX` boots hide this class (the first stage is the level
itself): repro through a real boot → title → level sequence.
- **A texture re-declared in another format must be normalised with the geometry it was IMPORTED with (D245).** GE's sky water loads a CI8 image and draws it through an RGBA16 tile; fast3d imports it as CI8 (D229) but its triangle path computed the UV divisor from the RGBA16 tile (half the width) and the whole mip-chain height (43 rows, not the 32-row mask period). Anything that changes the importer's format/extent must change the tri-path `tex_width/tex_height` with it, and wrap must equal the N64 mask period.
- **CPU-built RDP triangles need float vertices, not `Vtx` (D245).** Where GE builds RDP edge/texture coefficients on the CPU (sky/water), 32-bit S/T and positions are normal; squeezing them through s16 `tc`/`ob` costs visible precision on horizon-scale geometry. Use `G_FLOATVTX_EXT` (`port/include/floatvtx.h`).
## D. N64 hardware idioms fast3d does not emulate
@@ -865,6 +936,52 @@ through a converter or a runtime bswap fixup reads scrambled.
(ginit.s is missing from this repo; the expansion lives there), not UV,
shade, or the combiner.
## D14. LOD tile selection is a 2-cycle-only RDP feature; stale `G_TL_LOD` must not affect 1-cycle texture rectangles (D341)
`G_TL_LOD` is sticky other-mode state. Game code that draws a 1-cycle texture rectangle after an LOD-textured draw never clears it, because on the RDP it is irrelevant outside 2-cycle mode. fast3d's LOD/detail tile folding (D107/D236) must therefore not run for 1-cycle texrects, or they sample a neighbouring tile's stale declaration. Related: textures the CPU rewrites in place at recurring addresses (dyn pool) need a content-hashed cache key, and remember game code reaches them through the 0x80000000 KSEG0 mirror.
## D12. "Collapse an LOD binding to tile 0" is wrong for a two-texture DETAIL binding (D236)
**M-198 (finding D236 pass 26).** GE's `texLoadFromGdl` resolves a room DL's
`G_NOOP` texture reference through one of three handlers, chosen by
`in->words.w0 & 7` (`src/game/tex.c`):
| type | handler | TMEM layout |
|---|---|---|
| 0 `TEXTURETYPE_LOD` | `texHandleType0` | ONE load block at TMEM 0; tiles 0..n are LOD levels **of that one image** |
| 1 `TEXTURETYPE_DETAIL` | `texHandleType1` | TWO load blocks: the **detail** texture at TMEM 0, the **base** image at TMEM `texGetSizeInBytes(detail,0)` on tiles 1.. |
| 2 `TEXTURETYPE_MIPMAP` | `texHandleType2` | as type 0 |
fast3d's `gfx_lod_tile_offset` (`port/fast3d/gfx_pc.cpp`) folded **every**
`G_TL_LOD` draw to tile 0, on the reasoning (D107) that "GE loads the whole mip
chain at TMEM 0". That is true for types 0 and 2 and **false for type 1**,
where TMEM 0 holds the detail texture and the image you actually want is on
tile 1. The port therefore drew the detail texture in place of the base image —
and never even *imported* the base image, so every probe looking for it came
back empty.
On Surface 1 this was the whole of D236: all four treeline cards
(`tex1198`–`tex1201`, RGBA5551 64×17 cut-outs) are type-1 bindings sharing one
detail texture, `tex2465` — an opaque 32×32 IA8 noise tile. Result: an opaque
tiled noise wall where the treeline belongs, for 20+ investigation passes.
**Tells.** (a) A texture the game demonstrably decodes into the pool is never
imported by the renderer — that is a *binding* bug, not a decode, alpha, mip or
geometry bug, and no amount of work on the texture's contents will move it.
(b) The wrong thing drawn is itself a real, correctly-decoded texture from the
same draw — look for a second texture bound by the same command.
**Discriminating the two layouts at draw time.** Prefer an invariant that a mip
chain can never satisfy. Tiles 1.. of a type-0/2 binding are LOD levels of tile
0's image, so they necessarily carry the **same `fmt`/`siz`**; a type-1 pair is
two unrelated textures and generally does not. Deliberately choose the
asymmetric test — a mip chain can never trip it, and a detail pair that happens
to share a format merely keeps the old behaviour. Two weaker discriminators
were tried live and both misfire: "tile 1 sits at a different TMEM address"
matches ordinary mip chains (their levels are at different TMEM offsets too),
and "tile 1's TMEM slot has a registered load" matches them as well, because
`rdp.loaded_texture[]` is persistent state that is never cleared between binds.
## D2. The HUD/model "X-mirror" (D114/D116): RESOLVED, it was an upside-down capture
**M-33 (finding D168).** There was no mirror. `gfx_opengl_dump_bound_fbo`
@@ -1019,6 +1136,12 @@ silently disappears.
upstream. This class isn't limited to display-list cursors; audit any
`-O2`-only "quietly broken but not crashing" symptom for the same shape.
Fix: `return` under `#ifdef AVOID_UB` (`#else` keeps the N64 body).
- Instances (latent, 2026-09-25 Q7 warning audit): `constructor_menu07_missionsel()` and
`constructor_menu0D_missioncomplete()` (`src/game/front.c`), both `DL = f(DL)` at the
caller. Currently harmless: old/new objects disassemble identically at `-O2` (GCC
keeps `frontDrawCursor()`'s result in `rax`), but a clang/arm64 build is not bound
by that. `return DL;` under `#ifdef AVOID_UB`. A `-Wreturn-type` sweep lists the
remaining candidates; the other 7 current hits have callers that ignore the value.
- The one flagged in-source with an explicit comment is
`grep -rn "missing a \"return\"" src/` (`gunfire.c`). Others (like D77's)
exist without the banner; suspect this class whenever an `-O2` build
@@ -1158,8 +1281,98 @@ produce the same visual symptom but need opposite fixes.
**Update 2 (same investigation, immediately after): freezing a *value* and freezing a *pointer target* are different operations — treating them the same crashed twice.** Extending the same freeze technique to `render_pos` (a `Model*` field, unlike `field_488.pos` a plain value) by skipping the call that reassigns it (`subcalcmatrices`) seemed equivalent to M-167's "skip the write" but wasn't: `render_pos` is a pointer into a per-tick dynamically-reallocated arena, and skipping its reassignment only works if the pointer it's left holding is still valid — which depends on model lifecycle, not just on this tick's logic. Two fixes in a row tried to detect "is it safe to skip" by inspecting either the pointer's *value* (a sentinel check, `!= NULL && != -1`) or a *cached identity* (comparing the pointer against a remembered "already validated" pointer) — both crashed, because **the allocator reuses model-pointer addresses across genuinely different logical model instances**: a freshly-`CREATE`d model can land at the exact address a previously-removed model occupied, so both the sentinel check and the identity check wrongly conclude "this is already valid" for an instance whose `render_pos` is still an uninitialized tombstone. This is the same "don't guess validity from a value that can coincidentally look right" trap as the paragraph above, encountered a second time from a different angle (a *pointer's* target identity, not a *field's* content). **The robust fix:** never skip the call that establishes the pointer's validity — let it always run — and apply the freeze as a *contents* copy afterward, on top of a buffer that's now unconditionally guaranteed valid. If you must "hold something steady" and that something is reached through a pointer that some other code reassigns, freeze the *pointee's bytes*, not the *act of assignment* — the assignment is usually also the thing keeping the pointer sane.
## D12. Depth precision: the port matches N64 exactly — z-fighting seen on PC is probably faithful (D306/D308)
When a user reports z-fighting, do NOT assume the software renderer has less depth precision than the RDP. Verified for GE: the RSP's vertex transform (`rsp/graphics/gmain.s`) writes post-transform x/y/z/w back to the vertex buffer as **full 32-bit 16.16 fixed-point** (`sdv` stores), not s16 8.8 — the RDP interpolates from full-precision NDC and quantizes once, into its 24-bit z-image. The port keeps floats through `gfx_sp_vertex` and writes a `GL_DEPTH24_STENCIL8` buffer: same or better precision at every stage, same near/far. So coincident geometry authored into the level fights on both platforms; a PC-only explanation needs positive evidence (an N64 reference of the same spot), not just the existence of the symptom. A mitigation like switching to 32-bit float depth would *reduce* fighting but diverge from N64 behavior — that's a rule-2-style decision, not a silent fix.
**Probe technique that worked (vs. three variants that didn't):** detecting z-fighting from emitted triangles requires filtering out (a) tessellation gradients — adjacent tris of one surface have near-equal depths by construction — and (b) camera-motion coincidences — as the camera moves, unrelated world points pass through the same screen cell and their depths happen to sit close. What survives both: a coarse screen-cell grid keeping a short per-cell depth history, flagging only **non-monotonic** (a-b-a) oscillation within a small tolerance that **recurs many times at the same cell**. Monotonic drift = gradient; one-off close pairs = motion artifact; sustained recurrence = a real fight. `GE_ZF` in `port/fast3d/gfx_pc.cpp` implements this (`GE_ZFTOL` tolerance, `GE_ZFBOX` region filter). **Caveat (M-201):** `GE_ZF` actually quantises NDC z to ±255, not 24 bits, so at its default tolerance it flags unrelated nearby surfaces; treat a hit as a lead, not proof. Before calling anything "z-fighting", check the separation against the near plane: at close range 24-bit depth resolves far finer than any non-coplanar model parts, so "whole surfaces in the wrong order" there is depth *state* or geometry, not precision. Use `GE_D306C` (per-triangle depth-state census) for that; D306 turned out to be authored geometry seen from an unusual camera.
## D13. Per-render accumulation is not associative: the port's 1 tick/frame can reach states the N64's 2–3 ticks/frame never did (D329)
GE on N64 renders gameplay at ~20–30 fps, so `g_ClockTimer` (= `speedgraphframes`, 60 Hz ticks per rendered frame) is normally 2–3. The port at 60 fps gives 1. Code that folds `numticks` ticks into **one** downstream call is not equivalent at different granularities. `modelTickAnim` sums `playspeed*speed` over the ticks, then makes one `modelSetAnimFrame2WithChrStuff` call, which only moves `framea`/`frameb` when `floor(frame)` crosses an integer. At speed < 1 and 1 tick/frame a clamped (`framea == frameb`) non-looping anim never crosses, so it stays pinned even after its endframe is raised. At 2+ ticks it crosses on the next frame. Result: permanent guard freezes on PC (D318/D320) that the console could not sustain.
- **Tell:** a state that holds forever at 60 fps, is frame-exact (integer frame, `fa == fb`), and has a per-tick step < 1.0 unit.
- **Repro correctly:** force the granularity with `GE_D318B_CLK=N` (TEMP probe, lockstep N ticks/frame) and A/B N=1 vs N=2. Wall-clock runs on a slow host (WSLg renders ~5 fps → mixed clk 1–6) and `GE_DETERM=1` (advances per VI-retrace request, also mixed clk) both give uncalibrated results.
- **Where else to look:** any `for/while (numticks)` accumulator followed by a single threshold/floor consumer; D193 (AI locomotion rate) and D243 (cutscene `numticks` bursts) touched the same axis from the other side.
## D15. A texture-rectangle idiom that is sub-1-native-pixel tall relies on the RDP's floor-based fixed-point scanline stepping — now replicated in `gfx_draw_rectangle` (D397, RESOLVED 2026-09-28)
GE's front-end vertical-gradient trick (`titleRenderFolderMenuBackgroundLines`,
`src/game/title2.c`) draws a tall image as 299 separate 1-scanline-tall
`gSPTextureRectangle`s, one per source row, each tinted by a per-row
interpolated `gDPSetPrimColor`. The lower-right Y coordinate uses the
universal N64 idiom `((y+1)<<2)-1`, which in U10.2 units is **3, not 4** — a
0.75-native-pixel-tall rect, not 1.0. This is correct *on real hardware*
specifically because the RDP's scan converter steps by truncating
(flooring) the fixed-point Y to an integer scanline before testing coverage,
so `y*4+3` still falls in scanline `y` and nothing else — the "-1" is a
hardware-truncation idiom, not a literal float height.
`gfx_pc.cpp`'s `gfx_draw_rectangle` does not replicate that truncation: it
converts the U10.2 corners straight to continuous NDC floats
(`ulyf/lryf = ±(y / (4·HALF_SCREEN_HEIGHT)) + 1`) and hands them to the GPU's
own continuous rasterizer, which uses pixel-center/coverage rules, not
floor-stepped scanlines. A rect that is 0.75 (native) pixels tall, stacked
299 times with each row offset by exactly 1.0 native pixel from the last,
does not reliably rasterize as "exactly the one scanline the N64 idiom
intended" under a continuous rasterizer — whether a given row rounds to 0 or
1 device pixel depends on where its fractional edges land relative to the
device pixel grid, and that phase drifts row-to-row (worse under any
non-integer native→window viewport scale, `gfx_adjust_viewport_or_scissor`'s
`ratioX`/`ratioY`). Stacked over ~300 rows filling the screen, sporadic
per-row rounding reads as a periodic horizontal comb/interlace — this is the
leading mechanism for D397's "interlaced" file-select background and intro
gun-barrel, and is a **first-render, not re-entry-triggered** artifact
(unlike D182), because it is a rasterization property of the geometry itself,
not stale state.
**Tell:** any GE front-end/menu draw that fakes a large image or gradient by
stacking many independent 1-scanline (or otherwise sub-1-native-pixel-tall)
`gSPTextureRectangle`s rather than one multi-row texture — the `(<<2)-1`
lower-right idiom is the signature to grep for. A single such rect is
invisible (sub-pixel rounding on one edge); many stacked ones make the
rounding pattern visible as banding.
**Fix (landed 2026-09-28, RESOLVED):** `gfx_draw_rectangle` (`gfx_pc.cpp`) now replicates the RDP truncation. After the sub-pixel offset is applied, it floors the U10.2 top edge to its scanline and the bottom edge to the *last covered* scanline — the RDP scanline range `[top_scan, bot_scan+1]` — before converting to NDC: `top_scan = uly>>2`, `bot_scan = (lry-1)>>2`, then `uly = top_scan<<2`, `lry = (bot_scan+1)<<2`. This is a **no-op for whole-pixel rects** (their edges are already `4k`/`4k−1`, and the `(<<2)−1` bottom idiom already yields `bot_scan` = the intended last row), so only sub-pixel-tall strips change. **Confirmed by a falsifiable 3-scale PCDUMP test:** the comb period is fixed at ≈3 *native* rows across 640/1024/1280 (window-period = 3×scale) — the beat of 0.75-px strips at a 1.0-px pitch — exactly the fixed-point-flooring signature, ruling out the scale-invariant LERP/CC factor. Verified by playtest: gun-barrel + file-select gradient render clean, comb gone.
**Not the cause (checked and ruled out for D397):** the CC/LERP scaling of
`ENV_ALPHA` as `value/255.0f` (`gfx_pc.cpp` around the `G_CCMUX_ENV_ALPHA`
case) is byte-identical to the Perfect Dark PC port's (`gfx_pc.cpp` in the
local `pd_port` checkout) — same formula, no special-cased "5-bit RDP
coefficient" scaling in either port. The real RDP color combiner's A/B/C/D
inputs are 8-bit values (PRIM/ENV alpha come from an 8-bit `gDPSetEnvColor`
field); there is no coarser-precision quirk to correct here. A uniform
darkening from a nonzero `ENV_ALPHA` LERP factor is very likely faithful
(possibly authored/original-dev-intentional tinting), not a bug — do not
"fix" it without an N64/1964-GEPD reference screenshot proving otherwise.
## E. Process / method notes
- **Never `exit()` while another thread may be inside the GL driver (D344).**
The GL context lives on the render (scheduler) thread; quit events arrive on
other threads. Request the quit (`videoRequestQuit`), let the render thread
`glFinish` + unbind at a frame boundary and park, then exit from the host.
An unclean GL teardown bugchecked the maintainer's PC (0x119, HAGS on).
Harness runs should end with `GE_QUITFRAME=<n>`, not a `timeout` kill.
- **Headless front-end scripts: turn the mouse off (D343).** In menus the
pointer follows the real OS mouse (WI-2 absolute tracking), so the desktop
mouse moves the in-game cursor mid-`GE_INPUTSCRIPT`. Put
`[Input] MouseEnabled = 0` in the pinned test ini. Script "frames" were
about one per rendered frame on the file select screen; calibrate with a
log, not by assumption.
- **`textMeasure` height is 0 without a trailing newline (D343).** It only
counts completed lines. Measure `"text\n"` when you need a height (front.c's
folder text does this); the width is unaffected.
- **Periodic audio glitches: measure the host audio device before touching
the pipeline (D322).** A clock-regular overflow/garble pattern (~5 min period,
~45 s bursts, independent of level and load) looked like a port timing beat
and cost several game runs to chase. `tools_pc/sdl_drain_monitor.c` opens SDL
exactly like `port/src/audio.c` with no game and no window; on the
maintainer's Focusrite USB interface it showed the same ~312 s drain dips
(to 88 %) outright, and running it on several SDL backends at once showed the
culprit was SDL2's WASAPI path, not the device (DirectSound was clean; the
port now prefers it on Windows). Run it first on any "audio gets garbled"
report, and compare backends with `SDL_AUDIODRIVER` before blaming the hardware.
- Investigation loop is: reproduce → env-gated capped probe → root-cause
→ narrow `#ifdef PORT` fix → visual verify (`GE_PCDUMP` +
`tools_pc/pixcount.py` vs `docs/reference/n64-footage-*`).
@@ -1169,6 +1382,43 @@ produce the same visual symptom but need opposite fixes.
writing anything new; same Rare engine family.
- Don't re-investigate a closed §F finding or re-derive a format spec
that already has a converter.
- **When re-implementing a ROM bitstream/codec offline, PORT the derivation
from the decomp; never reconstruct it from the comment.** A half-right
bitstream parser in this codebase does not fail loudly — it returns
confident, plausible, wrong output. Three hits in four D236 passes:
(1) a texture-header parse that omitted the leading control byte
(`image.c:2568`) still produced sensible-looking formats and dimensions,
and was only caught by counting implausible entries (1020/2699 vs 1/2699
when correct) and noticing the format nibble only ever took values
0/4/8/12 — the tell that a field was straddling a boundary;
(2) omitting the per-image byte-align at the end of each LOD
(`image.c:1096`: discard the partial byte, and skip a whole byte when
already aligned) silently desynchronised every LOD after the first;
(3) `texInflateRle`'s run-length `fudge` was *guessed* from the doc comment
instead of ported from the loop at `image.c:1605`, and still decoded
images — just with the wrong alpha coverage (66.7% vs the correct 43.1%).
Practical rules: copy the arithmetic statement-for-statement even when it
looks like it could be closed-formed; build a cheap global plausibility
counter into the tool and print it every run; and cross-check one decoded
artifact against an independent expectation (here, that a LOD chain's
dimensions halve exactly and its alpha coverage stays roughly constant)
before trusting any number the tool emits.
- **A negative result from a diagnostic is worthless until the diagnostic has
been shown capable of producing the positive.** Two independent hits: D219's
"verify diagnostic caps exempt the real target signature before trusting
'clean'", and D236 pass 21, where a static setup scanner located GE's `PROP`
enum with `startswith("typedef enum PROP")` — which matches
`typedef enum PROPFLAG` hundreds of lines earlier, yielding an enum with **no
`PROP_*` members at all**. Every prop id then resolved to `?` and every
"foliage props placed" count came back 0, which is exactly what a clean,
meaningful negative looks like. Build the positive control **into** the probe:
have it print what it is capable of matching (`foliage props recognised (9):
…`) and fail loudly when that set is empty, so the failure mode cannot be
read as a finding. Applies equally to grep-based audits over generated C,
where near-miss identifier prefixes (`PROP` / `PROPFLAG` / `PROPDEF_TYPE`) are
the norm.
- **`docs/dev/findings-index.csv` is generator-derived** (`tools_pc/gen_findings_index.py`): the status cell is normalised from the `## Dxx` entry's §F summary-table row in `findings.md`, so hand-editing the CSV is silently reverted on the next regen (hit live in the D219/D252 M-158 session). Update the table row, then run the generator.
- **Every game launch rewrites `data/ge007.eep`** (the eeprom shim stores on
every write; validation resets, cheat patches and normal saves all write).
+8 -1
View File
@@ -1,3 +1,9 @@
---
title: Security & fidelity status
description: What a release actually installs (no networking, no telemetry, no ROM or assets)
and how faithfully the port tracks the original N64 game's logic.
---
# Security & fidelity status
This page answers two questions plainly: *what does installing this put on
@@ -82,7 +88,8 @@ port of the N64's PRNG, but wasn't — a shift-operation helper didn't match
the real MIPS64 instruction semantics it was meant to mirror. This was
independently confirmed by hand-tracing the assembly against the C port,
tracked as finding D284, and fixed before the release (verified clean
across a full 21-level sweep, no crashes). It affected loot placement, AI
across a full level sweep — all 20 missions plus the ending sequence —
no crashes). It affected loot placement, AI
behavior variance, and replay-state determinism from the very first random
draw, in every release before v0.3.0. Because of the fix, RNG-derived
output — including the save-file CRC — changed, which meant existing save
+2
View File
@@ -47,6 +47,7 @@ void gfx_set_target_fps(int);
void gfx_set_texture_filter(enum FilteringMode mode);
void gfx_set_mipmap_filter(enum MipmapFilteringMode mode);
void gfx_set_fix_mip_textures(int on);
void gfx_set_detail_base_tile(int on); /* D236: sample the base image of a DETAIL binding */
void gfx_set_wrap_fix(int on);
void gfx_set_anisotropy_level(int level); /* 1 = off; clamped to GL max */
void gfx_set_safe_area_crop(int on); /* crop the N64 TV-overscan safe-area margin instead of showing it as black bars */
@@ -56,6 +57,7 @@ void gfx_set_safe_area_crop(int on); /* crop the N64 TV-overscan safe-area
* (see port/src/optionsoverlay.c, D316). */
void gfx_get_ui_screen_rect(int32_t *outX, int32_t *outY, int32_t *outW, int32_t *outH);
void gfx_texture_cache_clear(void);
int gfx_texture_cache_count(void); /* D235 */
void gfx_texture_cache_delete(const uint8_t *orig_addr);
void gfx_texture_cache_delete_range(const uint8_t *start, const uint8_t *end);
int gfx_create_framebuffer(uint32_t width, uint32_t height, int upscale, int autoresize);
+59 -2
View File
@@ -1095,12 +1095,54 @@ static void gfx_opengl_init(void) {
static void gfx_opengl_on_resize(void) {
}
/* #92 perf probe (GE_PERFSTAT=1): GPU time per frame via GL_TIME_ELAPSED,
* double-buffered so reading never stalls the pipeline. Skipped when the
* driver lacks timer queries. Tells a low-end reporter's log whether frames
* are GPU-bound (render/MSAA/filtering cost) or CPU-bound. */
static int s_gpuq_on = -1;
static GLuint s_gpuq[2];
static int s_gpuq_idx = 0, s_gpuq_valid[2] = { 0, 0 };
static double s_gpuq_ms = 0.0;
static int s_gpuq_n = 0, s_gpuq_frames = 0;
static void gfx_opengl_start_frame(void) {
frame_count++;
if (s_gpuq_on < 0) {
s_gpuq_on = getenv("GE_PERFSTAT") != NULL && glGenQueries != NULL &&
glGetQueryObjectui64v != NULL && glBeginQuery != NULL;
if (s_gpuq_on) {
glGenQueries(2, s_gpuq);
fprintf(stderr, "PERFSTAT gpu timer on: %s | %s\n",
(const char *)glGetString(GL_RENDERER), (const char *)glGetString(GL_VERSION));
}
}
if (s_gpuq_on == 1) {
if (s_gpuq_valid[s_gpuq_idx]) {
GLuint avail = 0;
glGetQueryObjectuiv(s_gpuq[s_gpuq_idx], GL_QUERY_RESULT_AVAILABLE, &avail);
if (avail) {
GLuint64 ns = 0;
glGetQueryObjectui64v(s_gpuq[s_gpuq_idx], GL_QUERY_RESULT, &ns);
s_gpuq_ms += (double)ns / 1.0e6;
s_gpuq_n++;
}
}
glBeginQuery(GL_TIME_ELAPSED, s_gpuq[s_gpuq_idx]);
}
}
static void gfx_opengl_end_frame(void) {
glFlush();
if (s_gpuq_on == 1) {
glEndQuery(GL_TIME_ELAPSED);
s_gpuq_valid[s_gpuq_idx] = 1;
s_gpuq_idx ^= 1;
if (++s_gpuq_frames % 300 == 0 && s_gpuq_n > 0) {
fprintf(stderr, "PERFSTAT gpu=%.2fms/frame (avg over %d frames)\n", s_gpuq_ms / s_gpuq_n, s_gpuq_n);
s_gpuq_ms = 0.0;
s_gpuq_n = 0;
}
}
}
/* Frame capture (dev tool, env GE_PCDUMP) — see gfx_opengl.h. TEMP D70: read
@@ -1188,6 +1230,20 @@ static void gfx_opengl_update_framebuffer_parameters(int fb_id, uint32_t width,
width = max(width, 1U);
height = max(height, 1U);
/* macOS GL 4.1 core: asking for more samples than GL_MAX_SAMPLES (e.g. 8x)
* makes the multisample renderbuffers incomplete and the screen goes dark
* (glReadPixels 0x506). Clamp to what the driver reports. */
if (msaa_level > 1) {
static GLint maxSamples = -1;
if (maxSamples < 0) {
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
if (maxSamples < 1) maxSamples = 1;
}
if ((GLint)msaa_level > maxSamples) {
msaa_level = (uint32_t)maxSamples;
}
}
if (gfx_framebuffers_enabled) {
glBindFramebuffer(GL_FRAMEBUFFER, fb.fbo);
@@ -1284,7 +1340,8 @@ void gfx_opengl_resolve_msaa_color_buffer(int fb_id_target, int fb_id_source) {
glBindFramebuffer(GL_READ_FRAMEBUFFER, fb_src.fbo);
glBlitFramebuffer(0, 0, fb_src.width, fb_src.height, 0, 0, fb_dst.width, fb_dst.height, GL_COLOR_BUFFER_BIT,
GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, current_framebuffer);
/* current_framebuffer is an INDEX into framebuffers[], not a GL name; fb 0 is the window's default framebuffer (name 0). */
glBindFramebuffer(GL_FRAMEBUFFER, current_framebuffer == 0 ? 0 : framebuffers[current_framebuffer].fbo);
glEnable(GL_SCISSOR_TEST);
}
@@ -1348,7 +1405,7 @@ void gfx_opengl_copy_framebuffer(int fb_dst, int fb_src, int left, int top, bool
glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffers[current_framebuffer].fbo);
glBindFramebuffer(GL_FRAMEBUFFER, current_framebuffer == 0 ? 0 : framebuffers[current_framebuffer].fbo);
glReadBuffer(GL_BACK);
+748 -179
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -16,6 +16,8 @@ void gfx_sdl_make_context_current(void);
/* Release the GL context from the calling (host main) thread after init, so
* the game's scheduler thread can bind it per frame (see gfx_sdl2.cpp). */
void gfx_sdl_release_context(void);
/* D344: finish queued GL work and unbind the context (render thread, quit). */
void gfx_sdl_park_for_exit(void);
/* Refresh the cached drawable size after a window resize (called by the
* host thread's event pump). */
+21 -3
View File
@@ -9,6 +9,8 @@
#include "gfx_screen_config.h"
#include "input.h"
extern "C" void videoRequestQuit(const char *why); // port/src/video.c (D344)
static SDL_Window* wnd;
static SDL_GLContext ctx;
static SDL_Renderer* renderer;
@@ -319,7 +321,9 @@ static void gfx_sdl_handle_events(void) {
} else if (event.key.keysym.sym == SDLK_F4 && (event.key.keysym.mod & KMOD_ALT)) {
// D145: Alt+F4 quits; bare ESC no longer does (it is the
// menu "back" key -- see port/src/video.c / input.c).
exit(0);
// D344: request, don't exit(): this is the render thread,
// mid-frame. It parks at the frame's end; the host exits.
videoRequestQuit("Alt+F4 (render pump)");
} else if (event.key.keysym.sym == SDLK_ESCAPE && !event.key.repeat) {
// WI-1: this render-thread pump and port/src/video.c's
// host-thread pump both drain the same SDL queue, so either
@@ -345,7 +349,7 @@ static void gfx_sdl_handle_events(void) {
event.window.windowID == SDL_GetWindowID(wnd)) {
// We listen specifically for main window close because closing main window
// on macOS does not trigger SDL_Quit.
exit(0);
videoRequestQuit("window closed (render pump)"); // D344
} else if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
inputSetMouseGrab(0); // free + show cursor on alt-tab (also handled by video.c's pump; whichever dequeues it)
} else if (event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) {
@@ -353,7 +357,7 @@ static void gfx_sdl_handle_events(void) {
}
break;
case SDL_QUIT:
exit(0);
videoRequestQuit("quit event (render pump)"); // D344
break;
}
}
@@ -372,6 +376,20 @@ extern "C" void gfx_sdl_make_context_current(void) {
}
}
/* D344: called on the render thread when a quit was requested, at a frame
* boundary: wait for the GPU to drain everything this process queued, then
* unbind the context, so process teardown never lands inside the driver. */
extern "C" void gfx_sdl_park_for_exit(void) {
if (ctx && SDL_GL_GetCurrentContext() == ctx) {
typedef void (SDLCALL *FinishFn)(void); // = APIENTRY on Windows
FinishFn finish = (FinishFn)SDL_GL_GetProcAddress("glFinish");
if (finish) {
finish();
}
SDL_GL_MakeCurrent(NULL, NULL);
}
}
/* Release the GL context from the calling (host main) thread. WGL only allows
* a context to be current on one thread at a time, and while another thread
* still holds it, wglMakeContextCurrent fails with "The requested resource is
+13
View File
@@ -0,0 +1,13 @@
/* d318bprobe.h -- D318/D320 boundary probe (M-199 step 2).
*
* Env-gated per-tick ground-truth logger for the aim-hold / derail /
* anim-pin state machine of chrlvTickAttack + chrlvTickAttackCommon.
* Read-only; inert without GE_D318B. TEMP: remove when D318/D320
* conclude (same class as d320repro.c).
*/
#ifndef D318BPROBE_H
#define D318BPROBE_H
void d318bProbeTick(void);
#endif /* D318BPROBE_H */
+20
View File
@@ -0,0 +1,20 @@
#ifndef _ENVFLAG_H_
#define _ENVFLAG_H_
/* Cached env-var probes for hot paths (D250/D302: an uncached getenv() per
* node/poly/audio event is a measurable per-frame cost on Windows). Each
* expansion site gets its own static, read once on first use; the env is
* fixed for the process lifetime, so this is exact. The TU must already have
* a real getenv() prototype (see D324). GCC/Clang statement expressions. */
#define GE_ENVFLAG(name) __extension__({ \
static int _ge_envflag = -1; \
if (_ge_envflag < 0) _ge_envflag = getenv(name) != NULL; \
_ge_envflag; })
#define GE_ENVSTR(name) __extension__({ \
static int _ge_envstr_init = 0; \
static const char *_ge_envstr; \
if (!_ge_envstr_init) { _ge_envstr = getenv(name); _ge_envstr_init = 1; } \
_ge_envstr; })
#endif
+33
View File
@@ -0,0 +1,33 @@
#ifndef PORT_FLOATVTX_H
#define PORT_FLOATVTX_H
/* D245 (M-201): port-only full-precision vertex load for the sky/water fans.
*
* On N64, sky.c builds these triangles directly as RDP edge/texture
* coefficients with 32-bit S/T, so a horizon-scale quad (the IsWater ocean
* spans ~300,000 S10.5 units, ~9,400 texels) is exact. Routed through an
* ordinary gSPVertex, both its texture coords (s16 tc: +-1,024 texels) and its
* positions (s16 ob, divided by a fan-wide wScale to fit camera-space w up to
* ~2e5) had to be quantised -- visible as water/sky jitter that slides with
* the view. G_FLOATVTX_EXT hands fast3d the clip-space position and S/T as
* floats instead; fast3d already stores every loaded vertex as floats.
*
* w0 = G_FLOATVTX_EXT << 24 | dest_index << 16 | count, w1 = PortFloatVtx *.
* s/t are in Vtx.tc units (S10.5, before gSPTexture scaling). */
#define G_FLOATVTX_EXT 0x47
typedef struct PortFloatVtx {
float x, y, z, w; /* clip space */
float s, t; /* tc units */
unsigned char r, g, b, a;
} PortFloatVtx;
#define gSPFloatVertexExt(pkt, ptr, n, v0) \
do { \
Gfx *_g = (Gfx *)(pkt); \
_g->words.w0 = ((uintptr_t)G_FLOATVTX_EXT << 24) | \
(((uintptr_t)(v0) & 0xff) << 16) | ((uintptr_t)(n) & 0xffff); \
_g->words.w1 = (uintptr_t)(ptr); \
} while (0)
#endif
+35
View File
@@ -0,0 +1,35 @@
#ifndef PORT_FRONTOPTIONS_H
#define PORT_FRONTOPTIONS_H
/*
* D343: PC options as a GE front-end screen, MENU_PC_OPTIONS
* (port/src/frontoptions.c).
*
* Hooks:
* front.c the five MENU_* dispatch sites : frontOptionsMenu{Init,Update,
* Interface,Draw} and the debug name (all #ifdef PORT)
* front.c constructor_menu05_fileselect : optionsFileSelectLabel() draws the
* file-select entry label and enters MENU_PC_OPTIONS on a click
* video.c / input.c : F10 / Select don't open the overlay on this screen
*/
#include <PR/ultratypes.h>
#include <PR/gbi.h>
#ifdef __cplusplus
extern "C" {
#endif
void frontOptionsMenuInit(void);
void frontOptionsMenuUpdate(void);
void frontOptionsMenuInterface(void);
Gfx *frontOptionsMenuDraw(Gfx *DL);
Gfx *optionsFileSelectLabel(Gfx *gdl); /* file-select hook */
int frontOptionsBlocksOverlay(void); /* 1 while MENU_PC_OPTIONS is up */
#ifdef __cplusplus
}
#endif
#endif /* PORT_FRONTOPTIONS_H */
+49
View File
@@ -0,0 +1,49 @@
#ifndef _HUDASPECT_H_
#define _HUDASPECT_H_
/* D335 (WIDESCREEN-FOV-PLAN Option B, Phase 3): HUD alignment under native
* widescreen (D334). The world is projected at the window aspect, but 2D/HUD
* elements are laid out in the game's 4:3 logical canvas, which fast3d
* stretches to the window. Wrapping a HUD draw in an aspect mode makes fast3d
* map its x undistorted (x scaled by (4/3)/window_aspect) and anchor it to
* the left edge, the right edge, or the centre -- the PD port's
* G_ASPECT_{LEFT,RIGHT,CENTER}_EXT extra-geometry-mode, which GE's fast3d
* already decodes (gfx_pc.cpp gfx_sp_extra_geometry_mode /
* gfx_update_aspect_mode). Game-includable: no SDL, no fast3d headers.
*
* PORT_HUD_ASPECT emits only while native widescreen is on and the window is
* wider than 4:3; otherwise the display list is byte-identical to before. */
#define GE_HUD_ASPECT_NONE 0x00u
#define GE_HUD_ASPECT_LEFT 0x10u /* G_ASPECT_LEFT_EXT (port/fast3d/gbiex.h) */
#define GE_HUD_ASPECT_RIGHT 0x20u /* G_ASPECT_RIGHT_EXT */
#define GE_HUD_ASPECT_CENTER 0x30u /* G_ASPECT_CENTER_EXT */
#define GE_HUD_ASPECT_MASK 0x70u /* G_ASPECT_MODE_EXT */
#define GE_EXTRAGEOMETRYMODE_EXT 0x3a /* G_EXTRAGEOMETRYMODE_EXT */
/* The decoder does extra_mode = (extra_mode & ~clear) | set with
* clear = ~(w0 & 0xFFFFFF): passing ~MASK as the low 24 bits clears exactly
* the aspect bits and leaves the other extra-geometry flags alone. */
#define gSPHudAspectEXT(pkt, mode) \
gDma0p((pkt), GE_EXTRAGEOMETRYMODE_EXT, (mode), ((~GE_HUD_ASPECT_MASK) & 0xFFFFFFu))
extern float portNativeAspect(void); /* port/src/video.c */
/* Only for windows wider than 4:3: in a narrower window a centred 4:3 HUD
* would be wider than the window and crop, so there the HUD keeps the canvas
* mapping (squeezed, never cropped). 1.3334 so exact 4:3 emits nothing. */
#define PORT_HUD_ASPECT(gdl, mode) \
do { if (portNativeAspect() > 1.3334f) { gSPHudAspectEXT((gdl)++, (mode)); } } while (0)
/* D226: HUD span scale. PORT_HUD_SCALE(gdl, ax, ay) scales every rectangle
* drawn after it (glyphs, message boxes, ammo icons) about the anchor
* (logical screen px) by Game.HudScale %, until PORT_HUD_SCALE_END. At 100%
* nothing is emitted (byte-identical). Decoded as G_HUDSCALE_EXT in fast3d. */
#define GE_HUDSCALE_EXT 0x46
extern s32 portHudScalePercent(void); /* port/src/video.c */
#define gSPHudScaleEXT(pkt, scale256, ax4, ay4) gDma0p((pkt), GE_HUDSCALE_EXT, ((u32)(ax4) << 16) | ((u32)(ay4) & 0xFFFFu), ((u32)(scale256) & 0xFFFFu))
#define PORT_HUD_SCALE(gdl, ax, ay) do { s32 _hp = portHudScalePercent(); if (_hp != 100) { gSPHudScaleEXT((gdl)++, _hp * 256 / 100, (ax) * 4, (ay) * 4); } } while (0)
#define PORT_HUD_SCALE_PCT(gdl, pct, ax, ay) do { s32 _hp = (pct); if (_hp != 100) { gSPHudScaleEXT((gdl)++, _hp * 256 / 100, (ax) * 4, (ay) * 4); } } while (0)
#define PORT_HUD_SCALE_END(gdl) do { if (portHudScalePercent() != 100) { gSPHudScaleEXT((gdl)++, 256, 0, 0); } } while (0)
#endif
+31
View File
@@ -12,6 +12,7 @@
*/
#include <PR/ultratypes.h>
#include <SDL.h> /* API types (SDL_GameControllerButton/Axis); cf. video.h */
#ifdef __cplusplus
extern "C" {
@@ -43,6 +44,8 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y);
* also suspends mouse-aim reads until re-grabbed. No-op if the mouse is
* disabled in config. */
void inputSetMouseGrab(int on);
/* D287: apply queued mouse-mode/cursor changes; host (window) thread only. */
void inputApplyMouseRequests(void);
/* WI-1 click-to-lock cursor capture. The host event pump calls
* inputNotifyClick() when a mouse button goes down inside the game window
@@ -65,9 +68,37 @@ short inputPadAxis(int idx, SDL_GameControllerAxis a);
* ignored -- GE only cycles forward on a bare A edge. */
void inputPostWheel(int notches);
/* v0.4.0 M3: re-derive the keyboard binds after an F10 change to
* Input.CrouchMode or a future binding capture (optionsoverlay hook).
* Also drops the latched-crouch state. Scheduler thread only. */
void inputBindingsApply(void);
/* D384/D385: Primary/Secondary each accept a key or Mouse 1..5.
* Legacy extra ini tokens remain on disk but are ignored until action edit. */
#define INPUT_BIND_SLOTS 2
#define INPUT_BIND_MOUSE(button) (SDL_NUM_SCANCODES + (button))
const char *inputBindingSlot(const char *key, int slot);
int inputBindingSetSlot(const char *key, int slot, int code);
int inputBindingResetKey(const char *key);
/* D345(b): 1 while the 1:1 menu pointer owns cursor_h/v_pos (in a menu,
* abs pointer available, mouse used within the re-assert window). Port
* screens that teleport the crosshair should skip their snap when set. */
int inputMenuPointerLive(void);
/* Re-enumerate gamepads after a hotplug (SDL_CONTROLLERDEVICEADDED/REMOVED). */
void inputRescanPads(void);
/* D401: Rumble Pak -> real gamepad haptics. The osMotor* shims in
* libultra.c (game's src/joy.c rumble state machine) route here.
* idx is the N64 controller channel (0..3; out-of-range is a no-op).
* strength is 0..1, time seconds; 0/0 stops an active rumble. The scale
* is the single global Input.RumbleScale config value (0 = silent),
* applied to every pad. */
int inputRumbleSupported(int idx);
void inputRumble(int idx, f32 strength, f32 time);
f32 inputRumbleGetScale(void);
void inputRumbleSetScale(f32 v);
#ifdef __cplusplus
}
#endif
+69
View File
@@ -17,10 +17,17 @@
* input.c inputComputePad : controller 0 swallowed while open;
* nav routed to optionsOverlayHandleInput()
* gfx_pc.cpp gfx_run : optionsOverlayEmit() appended after the game DL
*
* D346 (M2): the row API below is the shared row table the front MENU_PC_
* OPTIONS screen (frontoptions.c) renders from, so one option set in one
* place.
*/
#include <PR/ultratypes.h>
#include <PR/gbi.h>
#include <config.h>
#include <video.h>
#include <watchsettings.h>
#ifdef __cplusplus
extern "C" {
@@ -28,6 +35,8 @@ extern "C" {
/* Toggle open/closed. On the closing edge the config is saved. */
void optionsOverlayToggle(void);
/* ESC: back to categories, or close from the category list (host-thread request). */
void optionsOverlayBack(void);
/* 1 while the overlay is on screen. */
int optionsOverlayIsOpen(void);
@@ -39,10 +48,70 @@ void optionsOverlayHandleInput(void);
/* Mouse-wheel notch -> move the selection (host event pump). */
void optionsOverlayScroll(int dir);
/* D383/D385 keyboard + mouse rebinding modal, shared by both options UIs.
* Host event pump hands off one key/button; menu thread commits it. */
int optionsBindingCaptureActive(void);
int optionsBindingInputBlocked(void); /* active or captured key still held */
int optionsBindingKeyDown(const SDL_KeyboardEvent *ev); /* 1 = consumed */
int optionsBindingMouseDown(const SDL_MouseButtonEvent *ev); /* 1 = consumed */
int optionsBindingCaptureTick(void); /* 1 = swallow menu nav this tick */
/* 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);
/* D343 (M2): row API for the file-select options screen (frontoptions.c).
* Index i runs over the same row table as the F10 overlay; headers split it
* into the screen's sections. */
int optionsRowCount(void);
int optionsRowIsHeader(int i);
int optionsRowHeaderParent(int i); /* -1 for root sections */
int optionsRowChildHeader(int i); /* -1 unless this row opens a nested section */
int optionsRowIsShown(int i);
const char *optionsRowLabel(int i);
int optionsRowIsSlider(int i);
int optionsRowIsBind(int i);
void optionsRowBeginBind(int i);
int optionsRowIsBondChooser(int i); /* D353: the Bond-file chooser row
(front options screen only) */
/* D356: 1 when this row's value lives in the selected save file (content
* rows); on a header, when its section contains such rows -- the front page
* uses the header form for its "(File N)" title annotation. */
int optionsRowIsSaveScoped(int i);
int optionsRowNeedsRestart(int i);
double optionsRowFraction(int i);
double optionsRowGetValue(int i); /* D356: raw value (fraction is bar geometry) */
void optionsRowSetFraction(int i, double f);
void optionsRowCommit(int i); /* slider release; watch rows persist once */
void optionsRowValueText(int i, char *out, int n);
void optionsRowAdjust(int i, int dir);
/* D356: per-section "Reset to defaults" rows (ROW_ACTION, keys
* "__Reset*"). Activation is edge-triggered and two-step (arm -> confirm
* within 3 s, one commit per confirmed activation; timeout or navigating
* away disarms) -- optionsRowIsReset lets the front screen suppress its
* held-key repeat for them. optionsResetMaintain runs each frame from the
* open UI (selRow = its selected rows[] index, -1 = none); optionsResetClear
* when a UI closes. Identical contract in F10 and on the front options
* screen (they are never open at the same time). Front: the confirmed reset
* writes the section's file rows through watchSettingsSet (direct commit)
* and the ini rows live (persisted on screen close, like any ini edit).
* In stage: the file rows enter the D352 queue (the game thread applies +
* persists them) and the ini rows are applied live immediately. */
int optionsRowIsReset(int i);
void optionsRowActivateReset(int i);
void optionsResetMaintain(int selRow);
void optionsResetClear(void);
/* D356 GE_WSPROBE_RESET dev hook (env-gated, driven from
* watchSettingsGameTick): runs the real arm -> confirm -> dispatch for
* EVERY section's reset row and verifies each section's values after its
* own dispatch (plan §5.8). Front is one-shot; the stage pair runs on the
* tick after the stage drain. */
void optionsResetProbePrepare(void);
void optionsResetProbeDispatchStage(void);
void optionsResetProbeVerifyStage(void);
#ifdef __cplusplus
}
#endif
+5
View File
@@ -71,6 +71,11 @@ void alloc_false_GUARDdata_to_exec_global_action();
void alloc_init_GUARDdata_entries();
void alloc_load_expand_ani_table();
void assert();
/* D324: GE's <stdlib.h> stub (#if 0'd body) declares neither; an implicit
* `int f()` declaration reads the xmm0 double return from eax -> garbage.
* port/src TUs that include <stdlib.h> get these from port/shim/stdlib.h. */
double atof();
double strtod();
char * bgDebPrintROOMID();
void bgFindRoomsAlongSegment();
s32 bgGetConnectedRooms();
+11 -3
View File
@@ -37,9 +37,17 @@ void videoEndFrame(void);
* loop from main(). Exits the process on QUIT/ESC/close. */
void videoPumpEvents(void);
/* D344: orderly quit. Any thread may request it; the render thread parks at
* the next frame boundary (glFinish + context released) and the host thread
* then exits. Never call exit() directly for a normal quit: the render thread
* could be inside the GL driver (the 0x119 bugchecks). */
void videoRequestQuit(const char *why);
int videoQuitRequested(void);
/* The game's native video mode (NTSC 640x480, PAL 640x400). fast3d scales
* N64 screen coordinates into window pixels using this. */
void videoUpdateNativeResolution(s32 w, s32 h);
u32 videoGetFrameCount(void);
s32 videoGetNativeWidth(void);
s32 videoGetNativeHeight(void);
@@ -51,9 +59,9 @@ 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);
/* Apply only the GL/SDL setting named by a changed options row at the next
* frame start. Sprite/HUD and direct-read settings need no reconfiguration. */
void videoRequestLiveConfigForKey(const char *key);
/* F10 options overlay -> window/fullscreen changes. The overlay input handler
* runs on the scheduler thread; SDL window ops must run on the thread that
+48
View File
@@ -0,0 +1,48 @@
#ifndef PORT_WATCHSETTINGS_H
#define PORT_WATCHSETTINGS_H
/* D349: one source of truth for Bond-file-backed GE settings. All GE state and
* EEPROM access runs on the game thread. F10 sends requests to that thread. */
enum WatchSettingField {
WATCH_SETTING_MUSIC,
WATCH_SETTING_FX,
WATCH_SETTING_LOOK,
WATCH_SETTING_AUTOAIM,
WATCH_SETTING_AIMCONTROL,
WATCH_SETTING_SIGHT,
WATCH_SETTING_LOOKAHEAD,
WATCH_SETTING_AMMO,
WATCH_SETTING_COUNT
};
int watchSettingsFieldForKey(const char *key); /* -1 for non-watch row */
int watchSettingsFolder(void); /* explicit front chooser; -1 = none */
/* D356: the chooser cycles folders only -- "none" is retired from the UI
* surface (plan §5.5): the front target is always a folder (frontFolder()
* resolves -1 to a valid folder lazily, D354; the auto-init fallback builds
* file 1 if none exists). The UI shows an unavailable state until the
* target's file actually exists instead of a number the write path cannot
* use. */
void watchSettingsChooseFile(int dir); /* game thread only */
int watchSettingsAvailable(void);
int watchSettingsRead(enum WatchSettingField field); /* front: saved file, F10: snapshot */
void watchSettingsSet(enum WatchSettingField field, int value, int commit);
void watchSettingsCommit(enum WatchSettingField field); /* slider release */
void watchSettingsGameTick(void); /* game thread; after gfxFrameMsgQ receive */
/* D356: context-aware, READ-ONLY active-file accessor (never mutates the
* stored front target): in stage -> the stage's active file; front
* contexts -> the resolved front target (frontFolder()'s lazy default).
* -1 when nothing usable (e.g. the auto-init fallback not yet applied).
* Drives the "(File N)" title annotations in the F10 overlay and the front
* options screen (plan §5.5). */
int watchSettingsActiveFolder(void);
/* D356: the BLANKSAVEDATA value of a field -- the per-file "default" the
* reset-to-defaults rows restore (plan §5.3/§5.6). */
int watchSettingsBlankValue(enum WatchSettingField field);
/* D352: queued field commands awaiting the game thread (0 in front
* contexts -- nothing is queued there). Drives the in-stage reset probe's
* dispatch check. */
int watchSettingsQueueCount(void);
#endif
+16
View File
@@ -136,5 +136,21 @@ typedef union {
#define Gfx Gfx_le
/*
* The gDP* display-list macros in the real gbi.h use _SHIFTL/_SHIFTR, which
* the N64 headers define in PR/mbi.h. A port TU that includes <PR/gbi.h>
* without <ultra64.h> (e.g. port/src/video.c) never reaches mbi.h; on Linux
* no other header in its include chain pulls it in, so the gDP* calls expand
* to implicit _SHIFTL()/_SHIFTR() function calls and fail to link (D402).
* Define them if absent. Guarded so a TU that also reaches mbi.h keeps
* mbi.h's own definitions.
*/
#ifndef _SHIFTL
# define _SHIFTL(v, s, w) ((u32)(((u32)(v) & ((0x01 << (w)) - 1)) << (s)))
#endif
#ifndef _SHIFTR
# define _SHIFTR(v, s, w) ((u32)(((u32)(v) >> (s)) & ((0x01 << (w)) - 1)))
#endif
#endif /* defined(PORT) */
#endif /* _PORT_SHIM_GBI_H_ */
+12 -1
View File
@@ -13,7 +13,18 @@
#if defined(PORT)
# include "include/PR/ucode.h"
# include "pc_protos.h"
# /* D402 (Linux build): a TU that includes <bondconstants.h> before
# * <ultra64.h> (e.g. port/src/optionsoverlay.c) sets src/bondconstants.h's
# * include-guard, then that header's own top `#include <ultra64.h>` reaches
# * this pc_protos.h -> bondtypes.h -> bondconstants.h chain while
# * src/bondconstants.h is still mid-parse (its ITEM_IDS/ACT_TYPE/... are
# * defined only later in the file). glibc GCC rejects the half-defined
# * types hard (MinGW tolerated it, so this only surfaced on Linux). The
# * bondconstants.h shim sets _PORT_DEFER_PC_PROTOS around that pass so the
# * D38 prototypes are pulled in only once, after the constants are whole. */
# if !defined(_PORT_DEFER_PC_PROTOS)
# include "pc_protos.h"
# endif
#else
# include <PR/ucode.h>
#endif
+14
View File
@@ -21,7 +21,21 @@
* Inert in the N64 build (no -DPORT): pure pass-through.
*/
#if defined(PORT)
/* D402 (Linux build): parse the PR base headers first, but tell the PR/ucode.h
* shim to defer pc_protos.h for this pass. pc_protos.h reaches bondtypes.h,
* which needs the bondconstants.h types (ITEM_IDS, ACT_TYPE, ...) that are only
* defined later in src/bondconstants.h. Without this, a TU that includes
* <bondconstants.h> before <ultra64.h> re-enters the still-parsing
* src/bondconstants.h (guard already set) from bondtypes.h and glibc GCC
* rejects the half-defined types (MinGW tolerated it). We finish
* src/bondconstants.h below and then pull pc_protos.h so the D38 prototypes
* are still seen exactly once. */
# define _PORT_DEFER_PC_PROTOS
# include <ultra64.h>
# undef _PORT_DEFER_PC_PROTOS
#include "src/bondconstants.h"
#include "pc_protos.h"
/*
* D38: src/bondconstants.h defines ntohl()/ntohs() as function-like macros
+23 -8
View File
@@ -17,15 +17,30 @@
#include "hoststdlib.h"
#else
#include "include/stdlib.h"
#if !defined(_WIN32)
/* The N64 stub declares only lldiv_t/ldiv_t + lldiv/ldiv. MinGW's other CRT
* headers leak getenv() etc.; a strict host GCC (Linux/macOS) does not, so
* getenv()'s 64-bit pointer return is assumed int and truncated (crashed
* configGetFrameDump). Declare the pointer-returning stragglers the port +
* game code use that nothing else declares. K&R form = compatible with any
* real prototype. (malloc/calloc/free are in port/include/pc_protos.h and
* a few port TUs' own local decls — not repeated here to avoid conflicts.) */
/* D324: the N64 stub declares nothing else, and -- unlike getenv/realloc
* below -- MinGW's leaked CRT headers do NOT declare these in the port TUs
* that use them. An implicit `int f()` declaration makes the caller read a
* double return from eax instead of xmm0 (silent garbage) and truncates
* pointer/64-bit-long returns to 32 bits (latent on LP64 for strtol).
* Declared unconditionally; K&R form is compatible with any real prototype.
* Game TUs that never include <stdlib.h> get these from pc_protos.h. */
extern double atof();
extern double strtod();
extern long int strtol();
extern int atoi();
extern int atexit();
/* D324: getenv was previously declared only on non-Windows, on the (wrong)
* assumption that MinGW's other CRT headers leak a prototype -- config.c gets
* none, so its implicit `int f()` declaration truncated the pointer return to
* 32 bits. Declare unconditionally; K&R form is compatible with any leaked or
* real prototype. */
extern char *getenv();
#if !defined(_WIN32)
/* The N64 stub declares only lldiv_t/ldiv_t + lldiv/ldiv, and a strict host
* GCC (Linux/macOS) leaks no CRT prototypes at all, so realloc's pointer
* return would be assumed int and truncated (crashed configGetFrameDump).
* (malloc/calloc/free are in port/include/pc_protos.h and a few port TUs'
* own local decls — not repeated here to avoid conflicts.) */
extern void *realloc();
#endif
#endif
+45 -1
View File
@@ -17,6 +17,9 @@
#include "config.h"
#include "audio.h"
#include "audiotrace.h"
#ifdef PORT
#include "envflag.h" /* cached getenv for hot-path probes */
#endif
static SDL_AudioDeviceID dev = 0;
@@ -85,6 +88,18 @@ static int d204OldMode(void)
int audioInit(void)
{
#if defined(_WIN32) && defined(SDL_HINT_AUDIODRIVER)
/* D322: prefer DirectSound over SDL2's WASAPI backend on Windows. Measured
* side by side on the same USB interface (tools_pc/sdl_drain_monitor.c,
* four simultaneous streams, 700 s): WASAPI fell to 81-88 % drain for
* ~30-50 s every ~317 s and lost 1.4 % of playback overall, whichever
* buffer size was used, while DirectSound drained at exactly real time
* throughout. Each WASAPI shortfall overflowed our queue and dropped whole
* blocks (the periodic "garbled audio" bursts). WASAPI stays as the
* fallback, and the SDL_AUDIODRIVER environment variable still overrides
* this (SDL_HINT_NORMAL yields to the environment). */
SDL_SetHint(SDL_HINT_AUDIODRIVER, "directsound,wasapi");
#endif
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
sysLogPrintf(LOG_ERROR, "audioInit: SDL_InitSubSystem: %s", SDL_GetError());
return -1;
@@ -101,7 +116,8 @@ int audioInit(void)
return -1;
}
SDL_PauseAudioDevice(dev, 0);
sysLogPrintf(LOG_INFO, "audioInit: opened SDL audio device at %d Hz", have.freq);
sysLogPrintf(LOG_INFO, "audioInit: opened SDL audio device at %d Hz (driver %s)", have.freq,
SDL_GetCurrentAudioDriver() ? SDL_GetCurrentAudioDriver() : "?");
return 0;
}
@@ -280,6 +296,34 @@ void audioSetNextBuffer(const s16 *buf, u32 len)
lastReportUs = now;
}
}
/* D322 (issue #87): long-session audio degradation -- voice-pool health
* monitor, sibling of the GE_D204 pipeline monitor above. One line per 5 s
* of wall clock with the three pool layers a multi-hour campaign can
* exhaust: the 8-voice SFX soft cap (sfx), the 64-slot event queue (evtq),
* and the 24 physical synth voices shared by SFX + music (pv). A sfx count
* that ratchets up level over level, or pv alloc that never returns to
* baseline after quiet sections, is the voice-leak signature; q pinning at
* queueLimit with drop climbing is the overproduction/queue signature.
* Cheap (one locked list walk per 5 s) -- safe for a full playtest. */
if (GE_ENVFLAG("GE_D322")) {
static u64 d322StartUs = 0, d322NextUs = 0;
extern void sndD322PoolSummary(s32 *, s32 *, s32 *, s32 *, s32 *, s32 *);
u64 now = sysGetMicroseconds();
if (!d322StartUs) { d322StartUs = now; d322NextUs = now + 5000000ull; }
if (now >= d322NextUs) {
s32 sfx, sfxMax, ev, pf, pl, pa;
sndD322PoolSummary(&sfx, &sfxMax, &ev, &pf, &pl, &pa);
sysLogPrintf(LOG_NOTE,
"D322 pool t=%llus sfx=%d/%d evtq=%d/64 pv free/lame/alloc=%d/%d/%d q=%d/%d drop=%u",
(unsigned long long)((now - d322StartUs) / 1000000ull),
(int)sfx, (int)sfxMax, (int)ev,
(int)pf, (int)pl, (int)pa,
(int)audioGetSamplesBuffered(), queueLimit,
(unsigned)dropCount);
d322NextUs = now + 5000000ull;
}
}
if (dev && buf && len) {
if (audioGetSamplesBuffered() < queueLimit) {
SDL_QueueAudio(dev, buf, len);
+3 -2
View File
@@ -25,6 +25,7 @@
#include "platform.h"
#include "system.h"
#include "config.h"
#include "envflag.h"
/*
* [Debug] knobs mirroring the dev env vars. The env var always wins so
@@ -43,14 +44,14 @@ PD_CONSTRUCTOR static void configDebugInit(void)
const char *configGetFrameDump(void)
{
const char *e = getenv("GE_PCDUMP");
const char *e = GE_ENVSTR("GE_PCDUMP"); /* per frame (videoPreSwapCapture) */
if (e && *e) return e;
return dbgFrameDump[0] ? dbgFrameDump : NULL;
}
int configGetInputLog(void)
{
return getenv("GE_INPUTLOG") ? 1 : dbgInputLog;
return GE_ENVFLAG("GE_INPUTLOG") ? 1 : dbgInputLog; /* several calls per poll */
}
#define MAX_OPTIONS 256
+377
View File
@@ -0,0 +1,377 @@
/* d318bprobe.c -- D318/D320 boundary probe (M-199 step 2).
*
* Purpose: capture per-tick ground truth of the aim-hold / derail /
* anim-pin state machine so the D318/D320/D321 writeups can be rewritten
* on verified evidence and each watchdog entry judged for excision on its
* own data. Static analysis (this session) established:
*
* - The hold-pose pin is topologically stable, not a float knife-edge:
* once modelConstrainOrWrapAnimFrame clamps both framea and frameb to
* ceil(endframe), the tail of modelSetAnimFrame2WithChrStuff re-emits
* the stale va==vb pair every tick (the per-frame loop only runs when
* floor(frame) crosses an integer boundary, which a pinned animframe1
* never does). All comparisons in the frozen state are exact integers.
* - Post-derail, escape requires a modelSetAnimation re-init from one of
* exactly three paths: chrlvTickAttack's unk36==0 branch (auto
* weapons, endframe<=frame), chrlvTickAttackCommon re-init block
* branch 1 (unk37!=0, i.e. auto weapon -> kneel-stop) or branch 3
* (unk31 armed by the fire gate while frame sits in the recoil/shoot
* window). None is reachable in the observed pins (auto weapons,
* endframe unbounded) -- BUT the pin itself only persists at 1 sim
* tick per rendered frame: a >=2-tick frame advances >=1.0 and
* crosses the integer boundary, so no escape path is needed (D329,
* lockstep A/B via GE_D318B_CLK below). Not a platform-invariant lock.
*
* This probe logs, for env-selected chrs, every operand those decisions
* read: the drawn variant (identified by pointer against all six firing-
* group sets), its frame fields, the unk30..unk44 gate bytes, the weapon
* auto-firing rates, full-precision model frame/endframe/speed state, and
* each branch condition recomputed from the same fields. Read-only;
* inert without GE_D318B.
*
* Usage: GE_D318B="5,9" (comma-separated chrnums, max 4)
* GE_D318B_ALL=1 (log every ACT_ATTACK tick, not just changes)
* GE_D318B_CLK=N (lockstep: exactly N sim ticks per rendered frame)
* Combined with the D320R forced repro, e.g.:
* GE_D320R="900:jump=5,list=0x0414,off=341" GE_D318B="5"
*
* TEMP: investigation probe, remove when D318/D320 conclude.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "PR/os.h"
#include "bondtypes.h"
#include "bondaicommands.h"
#include "chr.h"
#include "chraction.h"
#include "gun.h"
#include "lv.h"
#include "model.h"
#ifdef PORT
#define D318B_MAX_CHRS 4
#define D318B_LINE_CAP 6000
typedef struct
{
s16 chr;
/* last-logged state (change detection) */
int act;
u32 atk;
u32 ent;
char animid[8];
u32 framebits;
s32 stable; /* consecutive ticks with unchanged frame bits */
s32 hb; /* ticks since last heartbeat log */
} D318BState;
static int s_on = -1;
static int s_all = 0; /* GE_D318B_ALL=1: log every attack tick */
static int s_n = 0;
static D318BState s_s[D318B_MAX_CHRS];
static long s_lines = 0;
static int s_capped = 0;
#define d318bLog osSyncPrintf
static void d318bInit(void)
{
const char *e;
char buf[128];
char *p, *save;
if (s_on != -1)
{
return;
}
s_on = 0;
e = getenv("GE_D318B");
if (!e || !e[0])
{
return;
}
s_on = 1;
s_all = (getenv("GE_D318B_ALL") != NULL);
strncpy(buf, e, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = 0;
p = buf;
while (p && *p && s_n < D318B_MAX_CHRS)
{
save = strchr(p, ',');
if (save)
{
*save = 0;
}
memset(&s_s[s_n], 0, sizeof(D318BState));
s_s[s_n].chr = (s16)strtol(p, NULL, 0);
s_s[s_n].act = -1;
s_n++;
p = save ? save + 1 : NULL;
}
d318bLog("D318B: probe active for %d chr(s): %s\n", s_n, buf);
}
static ChrRecord *d318bFindChr(s16 chrnum)
{
s32 i;
for (i = 0; i < g_NumChrSlots; i++)
{
if (g_ChrSlots[i].chrnum == chrnum)
{
return &g_ChrSlots[i];
}
}
for (i = 0; i < g_ActiveChrsCount; i++)
{
if (g_ActiveChrs[i].chrnum == chrnum)
{
return &g_ActiveChrs[i];
}
}
return NULL;
}
/* Identify act_attack.animfloats by pointer against all six firing-group
* sets. Returns 1 and fills id (e.g. "P3.2" = pistol set, bearing group
* index 3, variant row 2) on success. */
static int d318bIdentifyAnim(struct weapon_firing_animation_table *row, char *id, int idsz)
{
static const struct
{
const char tag;
struct anim_group_info *const *set;
} sets[] = {
{ 'P', ptr_pistol_firing_animation_groups },
{ 'R', ptr_rifle_firing_animation_groups },
{ 'D', ptr_doubles_firing_animation_groups },
{ 'C', ptr_crouched_rifle_firing_animation_groups },
{ 'K', ptr_crouched_pistol_firing_animation_groups },
{ 'L', ptr_crouched_doubles_firing_animation_groups },
};
s32 si, gi, ri;
for (si = 0; si < 6; si++)
{
for (gi = 0; gi < 32; gi++)
{
struct anim_group_info *g = sets[si].set[gi];
if (g == NULL)
{
continue;
}
for (ri = 0; ri < g->len; ri++)
{
if (&(*g->table)[ri] == row)
{
snprintf(id, idsz, "%c%d.%d", sets[si].tag, gi, ri);
return 1;
}
}
}
}
snprintf(id, idsz, "-");
return 0;
}
static s32 d318bAutoRate(ChrRecord *self, GUNHAND hand)
{
PropRecord *prop;
s32 item;
prop = chrGetEquippedWeaponPropWithCheck(self, hand);
if (prop == NULL || prop->chr == NULL)
{
return -99;
}
item = (s32)prop->chr->act_attack.attack_item;
if (item < 0)
{
return -98;
}
return (s32)bondwalkItemGetAutomaticFiringRate((ITEM_IDS)item);
}
/* GE_D318B_CLK=N (test-only): force exactly N sim ticks per rendered frame,
* independent of host render speed -- a lockstep model of a console frame
* rate of 60/N fps. Rewrites the three values lvlManageMpGame just derived
* from speedgraphframes (the hook runs after them, before any chr tick).
* Pause/lock frames (g_ClockTimer == 0) are left alone. */
static void d318bClockLock(void)
{
static s32 s_clk = -1;
if (s_clk < 0)
{
const char *e = getenv("GE_D318B_CLK");
s_clk = (e && strtol(e, NULL, 10) > 0) ? (s32)strtol(e, NULL, 10) : 0;
if (s_clk)
{
d318bLog("D318B: clock lock active, %d tick(s)/frame\n", s_clk);
}
}
if (s_clk && g_ClockTimer > 0 && g_ClockTimer != s_clk)
{
g_GlobalTimer += s_clk - g_ClockTimer;
g_ClockTimer = s_clk;
g_GlobalTimerDelta = (f32)s_clk;
}
}
void d318bProbeTick(void)
{
s32 i;
d318bClockLock();
d318bInit();
if (!s_on || s_capped)
{
return;
}
for (i = 0; i < s_n; i++)
{
D318BState *st = &s_s[i];
ChrRecord *c;
Model *m;
struct act_attack *a;
struct weapon_firing_animation_table *f;
f32 fr, ef;
u32 framebits;
int chg, atpin, dolog, gate = 0, k;
char animid[8];
c = d318bFindChr(st->chr);
if (c == NULL || c->model == NULL)
{
continue;
}
m = c->model;
a = &c->act_attack;
f = a->animfloats;
if (c->actiontype != ACT_ATTACK && c->actiontype != ACT_ATTACKROLL)
{
/* reset change-detection so the next attack logs its entry */
st->act = -1;
continue;
}
fr = m->animframe1;
ef = m->endframe;
memcpy(&framebits, &fr, sizeof(framebits));
d318bIdentifyAnim(f, animid, sizeof(animid));
chg = (st->act != (int)c->actiontype) || (st->atk != a->attacktype)
|| (st->ent != a->entityid) || (strcmp(st->animid, animid) != 0);
if (framebits == st->framebits)
{
st->stable++;
}
else
{
st->stable = 0;
}
st->hb++;
atpin = ((ef >= 0.0f) && (ef <= fr + 1e-6f));
dolog = s_all || chg || atpin || (st->stable >= 20) || (st->hb >= 60);
if (!dolog)
{
goto update;
}
if (++s_lines >= D318B_LINE_CAP)
{
s_capped = 1;
d318bLog("D318B: line cap (%ld) reached, probe silent\n",
(long) D318B_LINE_CAP);
return;
}
/* per-hand fire-loop outcome in chrlvTickAttackCommon (source
* order): bit0/1 = unk3a[k]==0 arm, frame in shoot window
* [ss,se) -> ToggleHidden(1) (gun fires) + unk44=now;
* bit4/5 = unk3a[k]!=0 arm, unk31 re-arm conditions met
* (unk31==0, unk32 turn, frame in recoil window or >=ss). */
for (k = 0; k < 2; k++)
{
if (a->unk38[k] == 0)
{
continue;
}
if (a->unk3a[k] == 0)
{
if (f->shoot_start_frame <= fr && fr < f->shoot_end_frame)
{
gate |= (1 << k);
}
}
else if (a->unk31 == 0
&& (k == a->unk32 || a->unk3a[(s32)a->unk32] == 0)
&& ((f->recoil_start_frame >= 0.0f && f->recoil_start_frame <= fr
&& fr <= f->recoil_end_frame)
|| (f->recoil_start_frame < 0.0f && f->shoot_start_frame <= fr)))
{
gate |= (0x10 << k);
}
}
d318bLog(
"D318B: t=%d clk=%d c%d off=%d act=%d atk=0x%x ent=%u toM=%d | v=%s "
"st=%.9g end=%.9g ss=%.9g se=%.9g rs=%.9g re=%.9g as=%.9g ae=%.9g | "
"31=%d 32=%d 33=%d 34=%d 36=%d 37=%d 38=%d/%d 3a=%d/%d 40=%u 44=%d atkT=%d | "
"fr=%.9g ef=%.9g fa=%d fb=%d spd=%.9g ps=%.9g nn=%.9g oo=%.9g ts=%.9g es=%.9g "
"b0=%.9g b4=%.9g ac=%.9g loop=%d a2=%d a2len=%u wr=%d wl=%d | "
"aim=%d dnt=%d efle=%d trm=%d b1=%d b2=%d b3=%d gate=%d cf1=%d\n",
(int)g_GlobalTimer, (int)g_ClockTimer, st->chr, (int)c->aioffset, (int)c->actiontype,
(unsigned)a->attacktype, (unsigned)a->entityid, (int)a->type_of_motion,
animid,
(double)f->start_frame, (double)f->end_frame,
(double)f->shoot_start_frame, (double)f->shoot_end_frame,
(double)f->recoil_start_frame, (double)f->recoil_end_frame,
(double)f->aim_start_frame, (double)f->aim_end_frame,
(int)a->unk31, (int)a->unk32, (int)a->unk33, (int)a->unk34,
(int)a->unk36, (int)a->unk37, (int)a->unk38[0], (int)a->unk38[1],
(int)a->unk3a[0], (int)a->unk3a[1], (unsigned)a->unk40,
(int)a->unk44, (int)a->attack_time,
(double)fr, (double)ef, (int)m->framea, (int)m->frameb,
(double)m->speed, (double)m->playspeed, (double)m->newspeed,
(double)m->oldspeed, (double)m->timespeed, (double)m->elapsespeed,
(double)m->unkb0, (double)m->unkb4, (double)m->unkac,
(int)m->animlooping, (int)(m->anim2 != NULL),
m->anim2 ? (unsigned)m->anim2->unk04 : 0u,
d318bAutoRate(c, GUNRIGHT), d318bAutoRate(c, GUNLEFT),
(int)((a->attacktype & TARGET_AIM_ONLY) != 0),
(int)((a->attacktype & TARGET_DONTTURN) != 0),
(int)((ef >= 0.0f) && (ef <= fr)),
(int)(a->unk36 == 0 && f->recoil_end_frame > 0.0f && fr <= f->recoil_end_frame
&& (ef >= 0.0f) && (ef <= fr)),
(int)(a->unk37 != 0 || a->unk34 < a->unk33),
(int)(a->unk33 == a->unk34),
(int)(a->unk31 != 0),
gate,
(int)(a->attack_time < a->unk44 - 30 && m->anim2 == NULL
&& f->shoot_start_frame + 10.0f < fr && fr < f->shoot_end_frame));
update:
st->act = (int)c->actiontype;
st->atk = a->attacktype;
st->ent = a->entityid;
strncpy(st->animid, animid, sizeof(st->animid) - 1);
st->animid[sizeof(st->animid) - 1] = 0;
st->framebits = framebits;
if (dolog)
{
st->hb = 0;
}
}
}
#endif /* PORT */
+333 -58
View File
@@ -38,9 +38,12 @@
* TRUE only for ACT_STAND/ACT_ANIM/ACT_PATROL) can never pass
* while c78 is in ACT_ATTACK -> PERMANENT SOFTLOCK.
*
* The N64 has the identical race and the identical softlock; this watchdog
* is a deliberate port-layer accommodation (same pattern as the D202 M-66b
* port-side expiration). It detects the exact unrecoverable signature and,
* N64 manifestation unverified (findings D318, 2026-09-22 correction): the
* user's empirical prior is that no such freeze occurs on real hardware;
* whether the derail window ever opens organically on N64 is open (an
* unthrottled-emulator deliberate-timing A/B is the discriminating test).
* This watchdog is a deliberate port-layer accommodation either way (same
* pattern as the D202 M-66b port-side expiration). It detects the exact unrecoverable signature and,
* after a long margin, re-initializes c78's attack via sub_GAME_7F025560 --
* the same entry point the game itself uses for a fresh attack (chrlvTickAttack
* type_of_motion==2 calls it with (attacktype, entityid)). That runs
@@ -74,6 +77,42 @@
* session (AllUnlocked) is still covered. Logs its detection and
* intervention for playtest audit.
*
* D320 generalization (2026-09-22, findings D320): the identical pin was
* force-reproduced headless on two more lists, and the watchdog is now
* table-driven over all confirmed signatures:
*
* - 0x0417 ai_22 [321,333) ent=67 c67-alive : D318 Facility execution
* - 0x0414 ai_19 [480,548) any-ent : D320 Facility scientist/
* pad kneel-aim hold (label
* 0x34 stop-check loop; pin
* observed at off=483)
* - 0x040a ai_9 [60,86) any-ent : D320 Control chr-0xFC aim
* hold (label 0x0b stop-check
* loop; pin observed at
* off=69, ~2/3 of variants)
*
* Any chr (not just c78) sitting in a listed loop range in ACT_ATTACK with
* a LIVE-FIRE attacktype (no TARGET_AIM_ONLY bit) for D318_DEADLOCK_TICKS
* consecutive ticks gets the same sub_GAME_7F025560 re-seed. Depot ai_12
* (0x040d) tested clean 3/3 under forced repro and is deliberately NOT
* covered. In every non-deadlocked state each entry is inert: the escape
* path leaves the loop range in < 2 s (60x under the threshold), and a
* re-holding guard's anim frame moves between cycles.
*
* Repeat rescues: for the D320 entries a successful re-seed plays out the
* attack, the stop check passes, and the beat RE-ROLLS (re-aim -> wait ->
* update) -- which can pin AGAIN immediately (verified: in the forced-repro
* contexts every re-roll re-pinned ~240 ticks after each rescue; whether a
* given roll pins depends on the random aim-variant and is context/RNG-
* stream dependent, sometimes ~100%). So there is NO fire cap: while the
* signature persists, the watchdog re-fires every D318W_REFIRE_TICKS (6 s,
* > the ~240-tick re-roll cycle; a non-pinned guard leaves the loop range
* well inside that). This keeps the beat alive -- the guard fires in
* periodic bursts instead of freezing -- until an escape roll lands, the
* target dies (the beats progress on their dying/dead checks), or player
* action breaks the state. Full re-arm only when the signature has been
* absent for D318_REARM_TICKS.
*
* Opt out (research only): GE_D318W=0
*/
#include <stdlib.h>
@@ -93,18 +132,21 @@
/* chrai.c defines these but chrai.h does not declare them. */
extern s32 chraiGetAIListID(AIRecord *AIList, bool *isGlobalAIList);
/* src/bondaicommands.h:467 -- kept local so this port file doesn't pull in
* the AI-command header just for one bit constant. */
#define D320_TARGET_AIM_ONLY 0x0020 /* "Aim at target instead of firing" */
/* chraction.c internal (not in chraction.h): the fresh-attack entry point.
* Signature from the definition; call form mirrors chrlvTickAttack's own
* re-init at chraction.c:7401. */
extern void sub_GAME_7F025560(ChrRecord *self, s32 attack_type, s32 arg2);
#define D318_LIST_AI22 0x0417 /* Ourumov's execution list (ai_22) */
#define D318_OFF_LOOP_MIN 321 /* label 0x01: sleep/stop-check loop */
#define D318_OFF_LOOP_MAX 333 /* exclusive: the headshot command */
#define D318_OURUMOV_CHR 78
#define D318_TREVELYAN_CHR 67
#define D318_DEADLOCK_TICKS 600 /* 10 s at 60 Hz; escape takes < 2 s */
#define D318_REARM_TICKS 600 /* signature-absent ticks before re-arm */
#define D318W_REFIRE_TICKS 360 /* > the ~240-tick re-roll cycle */
static ChrRecord *d318wFindChr(s16 chrnum)
{
@@ -127,73 +169,165 @@ static ChrRecord *d318wFindChr(s16 chrnum)
return NULL;
}
/* Table-driven pin signatures (D318 + D320). off_max is exclusive. */
typedef struct
{
u32 listid;
s32 off_min, off_max;
s32 ent; /* required act_attack.entityid, -1 = any */
s32 target_alive_chr; /* must exist w/ prop and be alive, -1 n/a */
const char *tag; /* log tag */
} D318WEntry;
#define D318W_NENTRIES 3
static const D318WEntry s_wEntries[D318W_NENTRIES] = {
{ 0x0417, 321, 333, D318_TREVELYAN_CHR, D318_TREVELYAN_CHR, "D318 Facility ai_22" },
{ 0x0414, 480, 548, -1, -1, "D320 Facility ai_19" },
{ 0x040a, 60, 86, -1, -1, "D320 Control ai_9" },
};
static bool d318wChrAlive(s32 chrnum)
{
ChrRecord *c = d318wFindChr((s16)chrnum);
return c && c->prop && !chrIsDead(c);
}
/* Does this chr match the entry's pin signature? */
static bool d318wMatches(const D318WEntry *en, ChrRecord *c)
{
bool g = FALSE;
if (!c->ailist)
{
return FALSE;
}
if (chraiGetAIListID(c->ailist, &g) != (s32)en->listid)
{
return FALSE;
}
if (c->aioffset < en->off_min || c->aioffset >= en->off_max)
{
return FALSE;
}
if (c->actiontype != ACT_ATTACK)
{
return FALSE;
}
/* live-fire only: an AIM_ONLY pre-aim holding its pose is not a pin */
if ((s32)c->act_attack.attacktype & D320_TARGET_AIM_ONLY)
{
return FALSE;
}
if (en->ent >= 0 && (s32)c->act_attack.entityid != en->ent)
{
return FALSE;
}
return TRUE;
}
void d318WatchdogTick(void)
{
static int s_enabled = -1; /* -1 uncached, 0 off (GE_D318W=0), 1 on */
static s32 s_run = 0; /* consecutive ticks the signature has held */
static s32 s_absent = 0; /* consecutive ticks signature absent */
static bool s_fired = FALSE;
ChrRecord *c78;
ChrRecord *c67;
bool g = FALSE;
bool sig;
static int s_enabled = -1; /* -1 uncached, 0 off (GE_D318W=0), 1 on */
static s32 s_run[D318W_NENTRIES]; /* sig-held ticks per entry */
static s32 s_absent[D318W_NENTRIES];/* sig-absent ticks per entry */
static s32 s_cool[D318W_NENTRIES]; /* ticks since last fire (sig up) */
static s32 s_fires[D318W_NENTRIES]; /* rescues this occurrence */
static bool s_fired[D318W_NENTRIES];
s32 e, i;
if (s_enabled < 0)
{
const char *e = getenv("GE_D318W");
const char *ev = getenv("GE_D318W");
s_enabled = (e && e[0] == '0') ? 0 : 1;
s_enabled = (ev && ev[0] == '0') ? 0 : 1;
}
if (!s_enabled)
{
return;
}
c78 = d318wFindChr(D318_OURUMOV_CHR);
c67 = d318wFindChr(D318_TREVELYAN_CHR);
sig = (c78 && c78->ailist
&& chraiGetAIListID(c78->ailist, &g) == D318_LIST_AI22
&& c78->aioffset >= D318_OFF_LOOP_MIN && c78->aioffset < D318_OFF_LOOP_MAX
&& c78->actiontype == ACT_ATTACK
&& (s32)c78->act_attack.entityid == D318_TREVELYAN_CHR
&& c67 && c67->prop && !chrIsDead(c67));
if (sig)
for (e = 0; e < D318W_NENTRIES; e++)
{
s_absent = 0;
if (!s_fired)
const D318WEntry *en = &s_wEntries[e];
ChrRecord *hit = NULL;
bool sig = FALSE;
for (i = 0; i < g_NumChrSlots && !hit; i++)
{
if (s_run == 1)
if (d318wMatches(en, &g_ChrSlots[i]))
{
osSyncPrintf("D318W: t=%d deadlock signature detected (c78 ai_22 off=%d "
"ACT_ATTACK ent=67, c67 alive) -- starting %d-tick confirmation window\n",
(int)g_GlobalTimer, (int)c78->aioffset, D318_DEADLOCK_TICKS);
}
s_run++;
if (s_run >= D318_DEADLOCK_TICKS)
{
osSyncPrintf("D318W: t=%d D318 execution deadlock confirmed -- re-initializing "
"c78's attack (sub_GAME_7F025560 atk=0x%x ent=%d) to break the anim pin\n",
(int)g_GlobalTimer, (unsigned)c78->act_attack.attacktype,
(int)c78->act_attack.entityid);
sub_GAME_7F025560(c78, (s32)c78->act_attack.attacktype, (s32)c78->act_attack.entityid);
s_fired = TRUE;
hit = &g_ChrSlots[i];
}
}
}
else
{
s_run = 0;
/* One-shot per occurrence: once the deadlock signature has been
* clear for a while after a fire, re-arm so a later replay of
* Facility in the same process (AllUnlocked) is still covered. */
if (s_fired && ++s_absent >= D318_REARM_TICKS)
for (i = 0; i < g_ActiveChrsCount && !hit; i++)
{
s_fired = FALSE;
s_absent = 0;
osSyncPrintf("D318W: t=%d signature clear for %d ticks -- watchdog re-armed\n",
(int)g_GlobalTimer, D318_REARM_TICKS);
if (d318wMatches(en, &g_ActiveChrs[i]))
{
hit = &g_ActiveChrs[i];
}
}
sig = hit && (en->target_alive_chr < 0 || d318wChrAlive(en->target_alive_chr));
if (sig)
{
s_absent[e] = 0;
if (!s_fired[e])
{
if (s_run[e] == 1)
{
osSyncPrintf("D318W: t=%d [%s] pin signature detected (c%d off=%d "
"ACT_ATTACK atk=0x%x ent=%d) -- starting %d-tick confirmation window\n",
(int)g_GlobalTimer, en->tag, (int)hit->chrnum,
(int)hit->aioffset, (unsigned)hit->act_attack.attacktype,
(int)hit->act_attack.entityid, D318_DEADLOCK_TICKS);
}
s_run[e]++;
if (s_run[e] >= D318_DEADLOCK_TICKS)
{
osSyncPrintf("D318W: t=%d [%s] anim pin confirmed -- re-initializing "
"c%d's attack (sub_GAME_7F025560 atk=0x%x ent=%d) to break it\n",
(int)g_GlobalTimer, en->tag, (int)hit->chrnum,
(unsigned)hit->act_attack.attacktype,
(int)hit->act_attack.entityid);
sub_GAME_7F025560(hit, (s32)hit->act_attack.attacktype,
(s32)hit->act_attack.entityid);
s_fired[e] = TRUE;
s_fires[e] = 1;
s_cool[e] = 0;
}
}
else if (++s_cool[e] >= D318W_REFIRE_TICKS)
{
/* The beat re-rolled and re-pinned (see header). No cap: */
s_cool[e] = 0;
osSyncPrintf("D318W: t=%d [%s] re-pin rescued (%d) -- c%d off=%d "
"atk=0x%x ent=%d\n",
(int)g_GlobalTimer, en->tag, s_fires[e] + 1,
(int)hit->chrnum, (int)hit->aioffset,
(unsigned)hit->act_attack.attacktype,
(int)hit->act_attack.entityid);
sub_GAME_7F025560(hit, (s32)hit->act_attack.attacktype,
(s32)hit->act_attack.entityid);
s_fires[e]++;
}
}
else
{
s_run[e] = 0;
s_cool[e] = 0;
/* Once the signature has been clear for a while after a fire,
* re-arm so a later replay of the level in the same process
* (AllUnlocked) is still covered. */
if (s_fired[e] && ++s_absent[e] >= D318_REARM_TICKS)
{
s_fired[e] = FALSE;
s_absent[e] = 0;
s_fires[e] = 0;
osSyncPrintf("D318W: t=%d [%s] signature clear for %d ticks -- watchdog re-armed\n",
(int)g_GlobalTimer, en->tag, D318_REARM_TICKS);
}
}
}
}
@@ -299,6 +433,29 @@ void d318TimelineTick(void)
return;
}
/* Unified chr view: real guards live in g_ChrSlots[0..g_NumChrSlots) with
* model != NULL; the background script entities (chrnum 0xFE, list IDs
* >= 0x1000) live in g_ActiveChrs. Both are ticked by chrlvAllChrTick,
* so both must be scanned for pins / fingerprints. (D320, 2026-09-22:
* the detector originally walked g_ActiveChrs only and was blind to
* every real guard.) */
static ChrRecord *s_all[96];
static int s_nall = 0;
s32 i;
s_nall = 0;
for (i = 0; i < g_NumChrSlots && s_nall < 96; i++)
{
if (g_ChrSlots[i].model != NULL)
{
s_all[s_nall++] = &g_ChrSlots[i];
}
}
for (i = 0; i < g_ActiveChrsCount && s_nall < 96; i++)
{
s_all[s_nall++] = &g_ActiveChrs[i];
}
c78 = d318wFindChr(D318_OURUMOV_CHR);
c67 = d318wFindChr(D318_TREVELYAN_CHR);
@@ -409,12 +566,11 @@ void d318TimelineTick(void)
if (++s_det >= 60)
{
s32 h = 0x811C9DC5;
s32 i;
s_det = 0;
for (i = 0; i < g_ActiveChrsCount; i++)
for (i = 0; i < s_nall; i++)
{
ChrRecord *c = &g_ActiveChrs[i];
ChrRecord *c = s_all[i];
union { f32 f; u32 u; } xf, yf, zf, df;
xf.f = c->prop ? c->prop->pos.x : 0.0f;
@@ -432,7 +588,126 @@ void d318TimelineTick(void)
osSyncPrintf("D318T: DET t=%d seed=%08x%08x obj=0x%08x h=%08x nchrs=%d\n",
(int)g_GlobalTimer,
(unsigned)(u32)(g_randomSeed >> 32), (unsigned)(u32)g_randomSeed,
(unsigned)objectiveregisters1, (unsigned)h, (int)g_ActiveChrsCount);
(unsigned)objectiveregisters1, (unsigned)h, s_nall);
}
/* D320: generic D318-class pin DETECTION for the sweep's flagged lists
* (Facility ai_19, Control ai_9, Depot ai_12 -- see findings D320).
* Detection only: it logs, it never intervenes; the recovery watchdog
* above stays hardcoded to ai_22 by design until a second list actually
* freezes in play (D320's explicit instruction).
*
* Signature: a chr in ACT_ATTACK whose attacktype is LIVE FIRE (no
* TARGET_AIM_ONLY bit -- it is supposed to be shooting) and whose
* modelGetAnimFrame is byte-stable for 600 consecutive ticks (10 s). A
* guard that should be firing but whose animation never advances is the
* exact D318 frozen shape on any list. The two legitimate long-stable
* states are excluded or self-clearing: monologue pre-aims keep the
* AIM_ONLY bit (excluded), and a working fire cycle re-inits through
* chrlvTickAttackCommon's "endframe <= frame" block, so its frame does
* not stay byte-stable for 10 s. One line at first detection, then one
* per further 600 stable ticks while it persists; a frozen run keeps
* printing, an escaped run stops after the single line (or none). */
{
typedef struct { s16 chrnum; u32 framebits; s32 stable; s32 nextlog; }
D320Pin;
static D320Pin s_pins[8];
static int s_npins = 0;
s32 i, j;
for (i = 0; i < s_nall; i++)
{
ChrRecord *c = s_all[i];
union { f32 f; u32 u; } fr;
D320Pin *p = NULL;
if (c->actiontype != ACT_ATTACK || c->model == NULL)
{
continue;
}
if ((s32)c->act_attack.attacktype & D320_TARGET_AIM_ONLY)
{
continue; /* legitimate hold pose (monologue pre-aim) */
}
fr.f = modelGetAnimFrame(c->model);
for (j = 0; j < s_npins; j++)
{
if (s_pins[j].chrnum == c->chrnum)
{
p = &s_pins[j];
break;
}
}
if (!p)
{
if (s_npins < 8)
{
p = &s_pins[s_npins++];
}
else
{
continue; /* table full: drop the newest, keep tracking */
}
p->chrnum = c->chrnum;
p->framebits = fr.u;
p->stable = 0;
p->nextlog = 600;
}
if (p->framebits == fr.u)
{
p->stable++;
}
else
{
p->framebits = fr.u;
p->stable = 0;
p->nextlog = 600;
}
if (p->stable >= p->nextlog)
{
bool g = FALSE;
s32 aid = c->ailist ? chraiGetAIListID(c->ailist, &g) : -1;
osSyncPrintf("D320T: t=%d PIN? c%d aiid=0x%04x%s off=%d atk=0x%x ent=%d "
"mot=%d unk54=%u frame=%.2f stable=%d ticks (D318-class pin candidate)\n",
(int)g_GlobalTimer, (int)c->chrnum,
(unsigned)aid, g ? "G" : "", (int)c->aioffset,
(unsigned)c->act_attack.attacktype,
(int)c->act_attack.entityid,
(s32)c->act_attack.type_of_motion,
(unsigned)c->act_attack.unk54,
(double)fr.f, p->stable);
p->nextlog = p->stable + 600;
}
}
/* Drop entries for chrs that left the tracked state so a later
* re-entry starts a fresh confirmation window. */
for (i = 0; i < s_npins; i++)
{
bool alive = FALSE;
for (j = 0; j < s_nall; j++)
{
ChrRecord *c = s_all[j];
if (c->chrnum == s_pins[i].chrnum && c->actiontype == ACT_ATTACK
&& c->model != NULL
&& !((s32)c->act_attack.attacktype & D320_TARGET_AIM_ONLY))
{
alive = TRUE;
break;
}
}
if (!alive)
{
s_pins[i] = s_pins[--s_npins];
i--;
}
}
}
}
+480
View File
@@ -0,0 +1,480 @@
/* d320repro.c -- D320: forced-repro harness for the three high-risk
* D318-class aim-hold softlock lists (Facility ai_19, Control ai_9,
* Depot ai_12; findings D320).
*
* These beats are scripted cutscene moments that do not play in a bare
* headless boot (the scene is static with no input), so -- exactly like
* D318's mode-5 repro -- the probe forces them: at chosen ticks it can
* dump the chr roster, hijack a guard's AI pointer to a loaded list at a
* given byte offset, and/or set objective-register bits. The in-tree
* D320T pin detector (d318watchdog.c, GE_D318T=1) then reports any
* live-fire attack whose anim frame stays byte-stable for 600 ticks --
* the exact frozen shape.
*
* TEMP: investigation probe, remove when D320 concludes. Inert without
* the env var (zero cost).
*
* Usage: GE_D320R="tick:action;tick:action;..." where action is one of
* roster -- dump every active chr (num, act,
* list id, offset, alive)
* jump=<chr>,list=<hexid>,off=<n> -- set the chr's ailist to
* ailistFindById(hexid) at byte
* offset n, sleep=0
* bit=<hex> -- objectiveregisters1 |= hex
* rename=<chr>,<newnum> -- reassign a chr's chrnum (used to
* materialize an absent target, e.g.
* Control ai_9 aims at 0xFC which is
* not spawned in a bare boot)
* seed=<hi32>,<lo32> -- overwrite g_randomSeed (the boot
* seed is deterministic, so vary this
* between runs to sample different
* aim-variant branches)
* explode=<type>,<dist> -- D252: explosionCreate(<type>) <dist>
* units in front of Bond (headless
* explosion-particle capture)
* cheat=gl|rl|inv|<id> -- D252: cheatButtonHandleCheatsTurnedOn
* trace=<chr> -- from this tick on, log the chr's
* off/act/atk/ent/frame/endframe on
* any change + a 120-tick heartbeat
* while it is in ACT_ATTACK
*
* Example (Facility ai_19 hold block):
* GE_D320R="60:roster;900:jump=5,list=0x0414,off=341;900:trace=5"
*/
#include <stdlib.h>
#include <string.h>
#include "PR/os.h"
#include "bondtypes.h"
#include "chr.h"
#include "chrai.h"
#include "chraction.h"
#include "lv.h"
#include "model.h"
#include "explosion.h"
#include "player.h"
#include "cheat.h"
extern void cheatButtonHandleCheatsTurnedOn(CHEAT_ID cheat_id);
#ifdef PORT
/* chrai.c defines these but chrai.h does not declare them. */
extern s32 chraiGetAIListID(AIRecord *AIList, bool *isGlobalAIList);
/* port/src/random.c; not declared in a shared header. */
extern u64 g_randomSeed;
#define D320R_MAX_ACTIONS 16
#define D320R_MAX_TRACES 4
typedef struct
{
s32 tick;
int kind; /* 0 roster, 1 jump, 2 bit, 3 trace */
s16 chr;
s32 listid;
u16 off;
u32 bit; /* also: new chrnum for rename */
int fired;
} D320RAction;
typedef struct
{
s16 chr;
s32 off;
s32 act;
s32 atk;
s32 ent;
u32 framebits;
s32 hb;
} D320RTrace;
static ChrRecord *d320rFindChr(s16 chrnum)
{
s32 i;
for (i = 0; i <g_NumChrSlots; i++)
{
if (g_ChrSlots[i].chrnum == chrnum)
{
return &g_ChrSlots[i];
}
}
for (i = 0; i < g_ActiveChrsCount; i++)
{
if (g_ActiveChrs[i].chrnum == chrnum)
{
return &g_ActiveChrs[i];
}
}
return NULL;
}
static void d320rDumpRoster(void)
{
s32 i;
osSyncPrintf("D320R: t=%d roster slots=%d bgchrs=%d\n", (int)g_GlobalTimer,
(int)g_NumChrSlots, (int)g_ActiveChrsCount);
for (i = 0; i < g_NumChrSlots; i++)
{
ChrRecord *c = &g_ChrSlots[i];
bool g = FALSE;
s32 aid;
if (c->model == NULL)
{
continue;
}
aid = c->ailist ? chraiGetAIListID(c->ailist, &g) : -1;
osSyncPrintf("D320R: S c%d act=%d aiid=0x%04x%s off=%d atk=0x%x ent=%d %s\n",
(int)c->chrnum, (int)c->actiontype, (unsigned)aid,
g ? "G" : "", (int)c->aioffset,
(unsigned)c->act_attack.attacktype,
(int)c->act_attack.entityid,
chrIsDead(c) ? "DEAD" : "");
}
for (i = 0; i < g_ActiveChrsCount; i++)
{
ChrRecord *c = &g_ActiveChrs[i];
bool g = FALSE;
s32 aid = c->ailist ? chraiGetAIListID(c->ailist, &g) : -1;
osSyncPrintf("D320R: B c%d act=%d aiid=0x%04x%s off=%d\n",
(int)c->chrnum, (int)c->actiontype, (unsigned)aid,
g ? "G" : "", (int)c->aioffset);
}
}
void d320ReproTick(void)
{
static int s_on = -1; /* -1 uncached, 0 off, 1 on */
static D320RAction s_a[D320R_MAX_ACTIONS];
static int s_na = 0;
static D320RTrace s_tr[D320R_MAX_TRACES];
static int s_nt = 0;
s32 i;
if (s_on < 0)
{
const char *e = getenv("GE_D320R");
s_on = (e && e[0]) ? 1 : 0;
if (s_on)
{
/* Parse "tick:action;..." -- one pass, fixed budget. */
char buf[512];
char *p, *save;
strncpy(buf, e, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = 0;
p = buf;
while (p && *p && s_na < D320R_MAX_ACTIONS)
{
char *semi = strchr(p, ';');
char *colon;
D320RAction *a = &s_a[s_na];
if (semi)
{
*semi = 0;
}
colon = strchr(p, ':');
if (!colon)
{
break;
}
*colon = 0;
a->tick = (s32)strtol(p, NULL, 10);
a->kind = -1;
a->chr = -1;
a->listid = 0;
a->off = 0;
a->bit = 0;
a->fired = 0;
if (strcmp(colon + 1, "roster") == 0)
{
a->kind = 0;
}
else if (strncmp(colon + 1, "jump=", 5) == 0)
{
const char *q = colon + 6;
/* documented form "jump=<chr>,list=..": a leading bare
* number (no '=' before the first ',') is the chr */
{
char *eq0 = strchr(q, '=');
char *cm0 = strchr(q, ',');
if (cm0 && (!eq0 || cm0 < eq0))
{
*cm0 = 0;
a->chr = (s16)strtol(q, NULL, 0);
q = cm0 + 1;
}
}
while (q && *q)
{
char *eq = strchr(q, '=');
char *cm = strchr(q, ',');
const char *key;
if (!eq)
{
break;
}
if (cm && cm < eq)
{
break; /* malformed */
}
*eq = 0;
key = q; /* const-cast: parsed in place below */
if (strcmp(key, "chr") == 0) { a->chr = (s16)strtol(eq + 1, NULL, 0); }
else if (strcmp(key, "list") == 0){ a->listid = (s32)strtol(eq + 1, NULL, 0); }
else if (strcmp(key, "off") == 0) { a->off = (u16)strtol(eq + 1, NULL, 0); }
q = cm ? cm + 1 : NULL;
}
if (a->chr >= 0 && a->listid)
{
a->kind = 1;
}
}
else if (strncmp(colon + 1, "bit=", 4) == 0)
{
a->bit = (u32)strtol(colon + 5, NULL, 0);
a->kind = 2;
}
else if (strncmp(colon + 1, "trace=", 6) == 0)
{
a->chr = (s16)strtol(colon + 7, NULL, 0);
a->kind = 3;
}
else if (strncmp(colon + 1, "rename=", 7) == 0)
{
const char *q = colon + 8;
char *cm = strchr(q, ',');
if (cm)
{
*cm = 0;
a->chr = (s16)strtol(q, NULL, 0);
a->bit = (u32)strtol(cm + 1, NULL, 0);
a->kind = 4;
}
}
else if (strncmp(colon + 1, "cheat=", 6) == 0)
{
const char *q = colon + 7;
a->listid = (strcmp(q, "gl") == 0) ? CHEAT_2X_GRENADE_LAUNCHER
: (strcmp(q, "rl") == 0) ? CHEAT_2X_ROCKET_LAUNCHER
: (strcmp(q, "inv") == 0) ? CHEAT_INVINCIBILITY
: (s32)strtol(q, NULL, 0);
a->kind = 7;
}
else if (strncmp(colon + 1, "explode=", 8) == 0)
{
const char *q = colon + 9;
char *cm = strchr(q, ',');
a->listid = (s32)strtol(q, NULL, 0); /* type */
a->bit = cm ? (u32)strtol(cm + 1, NULL, 0) : 400; /* dist */
a->kind = 6;
}
else if (strncmp(colon + 1, "seed=", 5) == 0)
{
const char *q = colon + 6;
char *cm = strchr(q, ',');
if (cm)
{
*cm = 0;
a->listid = (s32)(u32)strtol(q, NULL, 0); /* hi */
a->bit = (u32)strtol(cm + 1, NULL, 0); /* lo */
a->kind = 5;
}
}
if (a->kind >= 0)
{
s_na++;
}
p = semi ? semi + 1 : NULL;
}
osSyncPrintf("D320R: armed %d actions\n", s_na);
}
}
if (!s_on)
{
return;
}
for (i = 0; i < s_na; i++)
{
D320RAction *a = &s_a[i];
if (a->fired || (s32)g_GlobalTimer < a->tick)
{
continue;
}
a->fired = 1;
switch (a->kind)
{
case 0: /* roster */
d320rDumpRoster();
break;
case 1: /* jump */
{
ChrRecord *c = d320rFindChr(a->chr);
if (c)
{
AIRecord *list = ailistFindById(a->listid);
if (list)
{
c->ailist = list;
c->aioffset = a->off;
c->sleep = 0;
osSyncPrintf("D320R: t=%d JUMP c%d -> aiid=0x%04x off=%d\n",
(int)g_GlobalTimer, (int)c->chrnum,
(unsigned)a->listid, (int)a->off);
}
else
{
osSyncPrintf("D320R: t=%d JUMP FAILED c%d aiid=0x%04x not found\n",
(int)g_GlobalTimer, (int)c->chrnum,
(unsigned)a->listid);
}
}
else
{
osSyncPrintf("D320R: t=%d JUMP FAILED c%d not active\n",
(int)g_GlobalTimer, (int)a->chr);
}
break;
}
case 7: /* cheat (D252) */
cheatButtonHandleCheatsTurnedOn((CHEAT_ID)a->listid);
osSyncPrintf("D320R: t=%d CHEAT %d\n",(int)g_GlobalTimer, (int)a->listid);
break;
case 6: /* explode (D252) */
if (g_CurrentPlayer && g_CurrentPlayer->prop)
{
struct coord3d pos = g_CurrentPlayer->prop->pos;
pos.f[0] += -g_CurrentPlayer->vv_sintheta * (f32)a->bit;
pos.f[2] += g_CurrentPlayer->vv_costheta * (f32)a->bit;
explosionCreate(0, &pos, g_CurrentPlayer->prop->stan, (s16)a->listid,
0, 0, g_CurrentPlayer->prop->rooms, 0);
osSyncPrintf("D320R: t=%d EXPLODE type=%d dist=%u\n",
(int)g_GlobalTimer, (int)a->listid, (unsigned)a->bit);
}
break;
case 2: /* bit */
objectiveregisters1 |= a->bit;
osSyncPrintf("D320R: t=%d BIT |= 0x%08x (now 0x%08x)\n",
(int)g_GlobalTimer, (unsigned)a->bit,
(unsigned)objectiveregisters1);
break;
case 3: /* trace */
if (s_nt < D320R_MAX_TRACES)
{
s_tr[s_nt].chr = a->chr;
s_tr[s_nt].off = -1;
s_tr[s_nt].act = -1;
s_tr[s_nt].atk = -1;
s_tr[s_nt].ent = -1;
s_tr[s_nt].framebits = 0;
s_tr[s_nt].hb = 0;
s_nt++;
osSyncPrintf("D320R: t=%d TRACE c%d armed\n",
(int)g_GlobalTimer, (int)a->chr);
}
break;
case 4: /* rename */
{
ChrRecord *c = d320rFindChr(a->chr);
if (c)
{
osSyncPrintf("D320R: t=%d RENAME c%d -> c%d\n",
(int)g_GlobalTimer, (int)c->chrnum,
(int)a->bit);
c->chrnum = (s16)a->bit;
}
else
{
osSyncPrintf("D320R: t=%d RENAME FAILED c%d not found\n",
(int)g_GlobalTimer, (int)a->chr);
}
break;
}
case 5: /* seed */
g_randomSeed = ((u64)(u32)a->listid << 32) | (u32)a->bit;
osSyncPrintf("D320R: t=%d SEED := %08x%08x\n",
(int)g_GlobalTimer, (unsigned)(u32)(g_randomSeed >> 32),
(unsigned)(u32)g_randomSeed);
break;
default:
break;
}
}
for (i = 0; i < s_nt; i++)
{
D320RTrace *t = &s_tr[i];
ChrRecord *c = d320rFindChr(t->chr);
u32 fb = 0;
f32 fr = 0.0f, ef = -1.0f;
if (!c)
{
continue;
}
if (c->model)
{
union { f32 f; u32 u; } x;
x.f = modelGetAnimFrame(c->model);
fb = x.u;
fr = x.f;
ef = modelGetAnimEndFrame(c->model);
}
if (t->off != (s32)c->aioffset || t->act != (s32)c->actiontype
|| t->atk != (s32)c->act_attack.attacktype
|| t->ent != (s32)c->act_attack.entityid)
{
osSyncPrintf("D320R: t=%d c%d off=%d act=%d atk=0x%x ent=%d frame=%.2f endf=%.2f\n",
(int)g_GlobalTimer, (int)c->chrnum, (int)c->aioffset,
(int)c->actiontype, (unsigned)c->act_attack.attacktype,
(int)c->act_attack.entityid, (double)fr, (double)ef);
t->off = (s32)c->aioffset;
t->act = (s32)c->actiontype;
t->atk = (s32)c->act_attack.attacktype;
t->ent = (s32)c->act_attack.entityid;
}
else if (c->actiontype == ACT_ATTACK && fb != t->framebits)
{
/* frame is moving: note the range so a later stall is visible */
osSyncPrintf("D320R: t=%d c%d frame MOVED to %.2f (endf=%.2f off=%d)\n",
(int)g_GlobalTimer, (int)c->chrnum, (double)fr,
(double)ef, (int)c->aioffset);
}
t->framebits = fb;
if (c->actiontype == ACT_ATTACK && ++t->hb >= 120)
{
t->hb = 0;
osSyncPrintf("D320R: t=%d c%d hb off=%d act=%d atk=0x%x ent=%d frame=%.2f endf=%.2f\n",
(int)g_GlobalTimer, (int)c->chrnum, (int)c->aioffset,
(int)c->actiontype, (unsigned)c->act_attack.attacktype,
(int)c->act_attack.entityid, (double)fr, (double)ef);
}
}
}
#endif /* PORT */
+1 -1
View File
@@ -71,7 +71,7 @@ void *dramReserve(void)
sa.bInheritHandle = FALSE;
HANDLE hSec = CreateFileMappingW(INVALID_HANDLE_VALUE, &sa, PAGE_READWRITE,
(DWORD)(DRAM_SIZE >> 32),
(DWORD)((unsigned long long)DRAM_SIZE >> 32),
(DWORD)(DRAM_SIZE & 0xFFFFFFFF), NULL);
if (!hSec) {
sysFatalError("dram: CreateFileMapping failed (%lu)", GetLastError());
+844
View File
@@ -0,0 +1,844 @@
/*
* frontoptions.c -- D343: PC options as a GE front-end screen.
*
* GE's front end has no generic menu system, so PC settings get their own
* screen, MENU_PC_OPTIONS (route B, docs/dev/OPTIONS-MENU-PLAN.md), built the
* way GE builds its Cheat Options screen (front.c init/interface/constructor_
* menu15_cheat): the open dossier with tabs and a blank CLASSIFIED page, black
* Zurich text on the paper via frontPrintText, GE's translucent highlight
* boxes, the PREVIOUS tab as "back", and the game's crosshair.
*
* page 0: five functional categories; page 1: a category's settings;
* deeper pages: Input -> Bindings -> Movement/Actions and Gameplay -> HUD
*
* The sections and rows are the F10 overlay's row table, reached through the
* row API in optionsoverlay.c, so both UIs edit the same settings.
*
* Game-code surface (Rule 2, user sign-off 2026-09-27): the MENU_PC_OPTIONS
* enum value (bondconstants.h) and five one-line dispatch cases (front.c),
* all #ifdef PORT. Entry is the "PC Options" label on file select, drawn by
* the existing D343 hook in constructor_menu05_fileselect.
*
* Everything here runs on the game's main thread (the front-end tick and
* constructor), like GE's own screens.
*/
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <ultra64.h>
#include <bondgame.h>
#include <bondconstants.h>
#include <fr.h>
#include <joy.h>
#include <music.h>
#include <snd.h>
#include "front.h"
#include "textrelated.h"
#include "language.h"
#include "platform.h"
#include "system.h"
#include "config.h"
#include "envflag.h"
#include "optionsoverlay.h"
#include "watchsettings.h"
#include "frontoptions.h"
#include "input.h"
/* front.c functions this screen shares with the cheat screen (not all are in
* front.h). */
extern Gfx *frontSetupMenuBackground(Gfx *DL);
extern Gfx *frontAddPreviousTabText(Gfx *DL);
extern s32 frontCheckCursorOnPreviousTab(void);
extern Gfx *frontDrawCursor(Gfx *DL);
extern Gfx *frontPrintText(Gfx *gdl, s32 *x, s32 *y, s8 *text, s32 second_font_table,
s32 first_font_table, s32 arg6, s32 view_x, s32 view_y,
s32 arg9, s32 arga);
extern void load_walletbond(void);
extern void disable_all_switches(Model *arg0); /* front.c:967 */
extern void set_item_visibility_in_objinstance(Model *objinstance, s32 item, s32 mode); /* front.c:968 */
extern s32 folder_selection_screen_option_icon;
extern struct rectbbox folder_option_ERASE_bound; /* front.c:439 */
/* ---- colours on the paper (RRGGBBAA; 0xFF = opaque black, GE's ink) ---- */
#define INK 0x000000FFu
#define INK_ON 0xA00000FFu /* the cheat screen's ON red */
#define INK_DIM 0x00000080u
#define HILITE 0x32 /* GE's menu highlight box (black, alpha 0x32) */
/* ---- layout, in the front end's 440x330 canvas (paper; tabs start at 390) ---- */
#define TITLE_Y 0x2B
#define ROW_X 0x37 /* the cheat list's x */
#define ROW_Y0 0x41 /* cheat list starts 0x35; +12 for the title line */
/* D346b: 20 -> 18 px so a full section fits the paper on one page. Hit bands
* stay contiguous at any pitch >= 6; the 22px highlight box still clears the
* next row (y+14 < y+17). D353: the largest section is now GRAPHICS at 11
* rows (both auto-FOV toggles off); MAX_PROWS=15 keeps headroom, and
* buildPages() logs any future overflow instead of silently truncating. */
#define ROW_DY 18
#define NUM_X 0x37 /* "1." on the section list */
#define SEC_X 0x4B
#define VAL_R 372 /* values right-aligned here */
#define BAR_X0 232
#define BAR_X1 316
#define ROW_HIT_X0 40.0f
#define ROW_HIT_X1 385.0f
/* ---- file-select label: right end of the Select / Copy / Erase bar ---- */
#define LABEL_X 351 /* D400: floor = NTSC Erase right (~323) + LABEL_GAP; JP's wider Erase pushes past it */
#define LABEL_CY 285 /* the bar's centre line, as Copy/Erase */
#define LABEL_GAP 28 /* D400: text-only now (icon removed), a plain word gap after Erase */
#define HIT_PAD 4
#define MAX_PAGES 8
#define MAX_PROWS 15 /* D346b: 12 -> 15 for the merged Video section (see ROW_DY) */
/* D406: content rows per page. The 440x330 paper ends at y=330; content row k
* sits at y = ROW_Y0 + (k+2)*ROW_DY (the D356 profile row takes slot 1), so a
* page fits while ROW_Y0 + (CAP+2)*ROW_DY + ~10px of text stays <= 330 (the
* bottom line is reserved for the page hint): CAP = 11 at ROW_DY 18. D346b's 18px pitch was sized for 15 rows with no
* profile row; D356's profile row plus the v0.4.0 Wave-A INPUT rows (17)
* pushed the 14th row to y=335 -- off the bottom of the paper -- and rows
* 15-17 (Crouch mode, Reset to defaults, Bindings...) were dropped by the
* old silent page cap, unreachable from the front screen. Sections that
* outgrow one page now page: the vertical stepper crosses page boundaries
* and the title carries a (p/N) marker. The F10 overlay is unaffected
* (it scrolls its own window, D345-D347). */
#define ROWS_PER_PAGE 11
static const char kLabel[] = "PC Options"; /* ASCII only: issue #87 / D295 */
static const char kLabelNL[] = "PC Options\n"; /* height measure only, D400 */
/* ---- screen state (game thread) ---- */
static int s_level = 0; /* 0 = categories, 1 = category, 2/3 = nested pages */
static int s_page = 0; /* current root category */
static int s_pageno = 0; /* D406: page within the active section */
static int s_rowTotal = 0; /* D406: visible rows in the active section */
static int s_subHeader = -1; /* rows[] header index for the nested page */
static int s_hl = -1; /* highlighted row / section, -1 = none */
static int s_dragRow = -1; /* global row index being dragged */
static int s_repeatDir = 0, s_repeatTimer = 0;
/* D345(e): hold-to-repeat state for vertical row stepping (separate from
* the left/right value-adjust repeat; both can be held at once). */
static int s_vrepeatDir = 0, s_vrepeatTimer = 0;
static int s_pageHdr[MAX_PAGES];
static int s_pageN = 0;
static int s_rowIdx[MAX_PROWS];
static int s_rowN = 0;
#define MAX_SECT_ROWS 48 /* D406: per-section gather buffer (INPUT = 17) */
/* ------------------------------------------------------------------------ */
static void playSfx(s16 id)
{
sndPlaySfx((struct ALBankAlt_s *)g_musicSfxBufferPtr, id, NULL);
}
static s32 measureW(const char *str)
{
s32 h = 0, w = 0;
textMeasure(&h, &w, (char *)str, ptrFontZurichBoldChars, ptrFontZurichBold, 0);
return w;
}
static Gfx *ink(Gfx *DL, s32 x, s32 y, const char *str, u32 colour)
{
return frontPrintText(DL, &x, &y, (s8 *)str, (s32)(uintptr_t)ptrFontZurichBoldChars,
(s32)(uintptr_t)ptrFontZurichBold, (s32)colour,
viGetX(), viGetY(), 0, 0);
}
static Gfx *inkR(Gfx *DL, s32 xr, s32 y, const char *str, u32 colour)
{
return ink(DL, xr - measureW(str), y, str, colour);
}
/* D406b: per-row slider-bar origin. Long labels ("X axis look sensitivity
* (controller)") used to run into the fixed 232px bar and overlap the track;
* the bar now starts 10px past the label. Rows whose labels leave less than
* 12px of track show the value only. */
static int rowBarX0(const char *label)
{
int x0 = ROW_X + measureW(label) + 10;
return x0 < BAR_X0 ? BAR_X0 : x0;
}
/* "MOUSE / AIM" -> "Mouse / Aim" (GE's menus use title case). */
static void titleCase(const char *in, char *out, int n)
{
int j = 0, start = 1;
for (int i = 0; in[i] && j < n - 1; i++) {
char c = in[i];
if (c >= 'A' && c <= 'Z' && !start) c = (char)(c - 'A' + 'a');
if (c >= 'a' && c <= 'z' && start) c = (char)(c - 'a' + 'A');
start = (c == ' ' || c == '/');
out[j++] = c;
}
out[j] = '\0';
}
static int rowY(int k)
{
/* Every open section, including a nested section, has the profile row. */
if (s_level >= 1) k++;
return ROW_Y0 + k * ROW_DY;
}
static int activeHeader(void)
{
return s_level >= 2 ? s_subHeader : s_pageHdr[s_page];
}
static void buildPages(void)
{
int n = optionsRowCount();
s_pageN = 0;
for (int i = 0; i < n && s_pageN < MAX_PAGES; i++) {
if (optionsRowIsHeader(i) && optionsRowHeaderParent(i) < 0)
s_pageHdr[s_pageN++] = i;
}
if (s_page >= s_pageN) {
s_page = 0;
}
s_rowN = 0;
s_rowTotal = 0;
if (s_pageN > 0) {
/* D406: gather every visible row of the active section, then page it
* (ROWS_PER_PAGE fits the paper, see above). The old code capped at
* MAX_PROWS-1 and silently dropped the rest -- a warning, no access.
* s_pageno is re-clamped on every build so rows that auto-hide
* (aim range, auto-FOV) cannot strand the cursor past the end. */
int tmp[MAX_SECT_ROWS];
int tn = 0;
for (int i = activeHeader() + 1; i < n && !optionsRowIsHeader(i); i++) {
if (!optionsRowIsShown(i)) continue;
if (tn < MAX_SECT_ROWS) tmp[tn++] = i;
}
s_rowTotal = tn;
int pages = (tn + ROWS_PER_PAGE - 1) / ROWS_PER_PAGE;
if (pages < 1) pages = 1;
if (s_pageno > pages - 1) s_pageno = pages - 1;
int off = s_pageno * ROWS_PER_PAGE;
for (int k = 0; k < ROWS_PER_PAGE && off + k < tn; k++)
s_rowIdx[s_rowN++] = tmp[off + k];
}
}
static int itemCount(void)
{
/* D356: level 1 carries the top save-file row (item 0, not in rows[]) on
* top of the page's content rows. */
return s_level == 0 ? s_pageN : s_rowN + 1;
}
/* Put the crosshair on item k, unconditionally. */
static void cursorToItemRaw(int k)
{
/* D345(g): snap into the left gutter (x=44), not onto the row text
* (the old ROW_X+20 centred the sprite over the label/highlight box).
* 44 stays inside [ROW_HIT_X0..1] so the highlight hit-test still
* resolves row k, and clears the number column (NUM_X=55) / highlight
* boxes (from x=53/73). The PREVIOUS-tab hitbox (x>390 && y>223) is
* far away. Mouse users are unaffected: the pointer sits under the
* real mouse, never at this snap position. */
cursor_h_pos = 44.0f;
cursor_v_pos = (f32)(rowY(k) + 6);
}
/* Put the crosshair on item k (entering a page / going back). */
static void cursorToItem(int k)
{
/* D345(b): while the 1:1 pointer owns the crosshair (mouse recently
* used), it is already under the OS pointer -- snapping would yank it
* to the top-left item and it would stay there until the next mouse
* movement. Keyboard/D-pad users (stale mouse) keep the snap. */
if (inputMenuPointerLive()) return;
cursorToItemRaw(k);
}
/* ------------------------------------------------------------------------ */
/* MENU_PC_OPTIONS: init / update / interface / constructor (front.c cases) */
void frontOptionsMenuInit(void)
{
/* As init_menu15_cheat: reset the tab state and make sure the dossier
* model is loaded (the background draws walletinst[0]). */
tab_start_selected = FALSE;
tab_next_selected = FALSE;
tab_prev_selected = FALSE;
tab_prev_highlight = FALSE;
tab_next_highlight = FALSE;
tab_start_highlight = FALSE;
load_walletbond();
/* frontSetupMenuBackground frames the folder at folderpositions[
* selected_folder_num]; file select leaves it at -1. Use folder 1, as a
* return from mode select would (file select's init does the same). */
if (selected_folder_num < FOLDER1 || selected_folder_num >= MAX_FOLDER_COUNT) {
selected_folder_num = FOLDER1;
}
folder_selection_screen_option_icon = 0; /* crosshair, not Copy/Erase */
s_level = 0;
s_page = 0;
s_pageno = 0; /* D406 */
s_subHeader = -1;
s_hl = -1;
s_dragRow = -1;
s_repeatDir = 0;
s_vrepeatDir = 0;
buildPages();
/* Isolated headless screenshot of a nested page. This diagnostic only
* changes menu navigation; it never edits profile or config values. */
const char *testPage = getenv("GE_FRONTOPTIONS_SECTION");
if (testPage) {
for (int i = 0; i < optionsRowCount(); i++) {
if (!optionsRowIsHeader(i) || strcmp(optionsRowLabel(i), testPage)) continue;
int root = i, depth = 1;
while (optionsRowHeaderParent(root) >= 0) {
root = optionsRowHeaderParent(root);
depth++;
}
for (int p = 0; p < s_pageN; p++) {
if (s_pageHdr[p] == root) {
s_page = p;
s_pageno = 0;
s_level = depth;
s_subHeader = depth > 1 ? i : -1;
buildPages();
sysLogPrintf(LOG_INFO, "frontoptions: diagnostic page %s (depth %d, %d rows)",
testPage, depth, s_rowN);
break;
}
}
break;
}
}
cursorToItem(0);
sysLogPrintf(LOG_INFO, "frontoptions: opened");
}
void frontOptionsMenuUpdate(void)
{
}
static void goBack(void)
{
playSfx(DOOR_METAL_CLOSE2_SFX);
if (s_dragRow >= 0) optionsRowCommit(s_dragRow);
s_dragRow = -1;
optionsResetClear(); /* D356: navigating away (or closing) disarms a pending reset */
if (s_level >= 2) {
int child = s_subHeader;
int parent = optionsRowHeaderParent(child);
s_pageno = 0; /* D406: every fresh page view starts at page 1 */
if (parent == s_pageHdr[s_page]) {
s_level = 1;
s_subHeader = -1;
} else {
s_level--;
s_subHeader = parent;
}
buildPages();
int selected = 0;
for (int k = 0; k < s_rowN; k++)
if (optionsRowChildHeader(s_rowIdx[k]) == child) selected = k + 1;
cursorToItem(selected);
s_hl = -1;
sysLogPrintf(LOG_INFO, "frontoptions: back to %s", optionsRowLabel(activeHeader()));
return;
}
if (s_level == 1) {
s_level = 0;
s_pageno = 0; /* D406 */
cursorToItem(s_page);
sysLogPrintf(LOG_INFO, "frontoptions: back to section list");
return;
}
configSave();
sysLogPrintf(LOG_INFO, "frontoptions: closed");
frontChangeMenu(MENU_FILE_SELECT, FALSE);
}
void frontOptionsMenuInterface(void)
{
viSetFovY(FOV_Y_F);
viSetAspect(ASPECT_RATIO_SD);
viSetZRange(100.0f, 10000.0f);
viSetUseZBuf(0);
buildPages(); /* an auto toggle may have hidden or shown % rows */
/* D383: modal key capture owns events on the host thread. Suppress
* navigation until its one-shot key has been consumed and released. */
if (optionsBindingCaptureTick()) return;
/* D345(f): NO raw stick integration on this screen (the D345(c) call to
* frontUpdateControlStickPosition() was removed): vertical input is the
* discrete row stepper below, horizontal is the left/right block -- F10
* model. Raw drift fought the pointer over cursor_h/v_pos. */
/* Highlight: as the cheat screen, recomputed while A is not held. */
if (joyGetButtons(PLAYER_1, A_BUTTON | Z_TRIG) == 0) {
tab_prev_highlight = FALSE;
s_hl = -1;
if (frontCheckCursorOnPreviousTab()) {
tab_prev_highlight = TRUE;
} else if (cursor_h_pos >= ROW_HIT_X0 && cursor_h_pos <= ROW_HIT_X1) {
for (int k = itemCount() - 1; k >= 0; k--) {
if (cursor_v_pos >= (f32)(rowY(k) - 3) &&
cursor_v_pos < (f32)(rowY(k) + ROW_DY - 3)) {
s_hl = k;
break;
}
}
/* D407(b): past the top/bottom edge of the visible rows, clamp
* the highlight to the edge row instead of dropping it (a
* mouse-only user keeps a row selected at the page edge; wheel
* / S then turns the page). */
if (s_hl < 0 && itemCount() > 0) {
if (cursor_v_pos < (f32)rowY(0))
s_hl = 0;
else if (cursor_v_pos <= (f32)(rowY(itemCount() - 1) + ROW_DY))
s_hl = itemCount() - 1;
}
}
}
if (joyGetButtonsPressedThisFrame(PLAYER_1, A_BUTTON | Z_TRIG | START_BUTTON)) {
if (tab_prev_highlight) {
goBack();
} else if (s_hl >= 0 && s_level == 0) {
playSfx(DOOR_METAL_CLOSE2_SFX);
s_page = s_hl;
s_level = 1;
s_pageno = 0; /* D406 */
sysLogPrintf(LOG_INFO, "frontoptions: section %d", s_page + 1);
buildPages();
cursorToItem(0);
s_hl = -1;
} else if (s_hl == 0 && s_level >= 1) {
/* D356: the top save-file row -- A steps to the next file (the
* D352 chooser; folders only, "none" retired, plan §5.5). */
playSfx(DOOR_LOCK_SFX);
watchSettingsChooseFile(1);
} else if (s_hl >= 1 && s_hl <= s_rowN) {
/* s_hl is item-numbered (0 = the save-file row); content row k
* is item k+1, so the row index is s_hl - 1. */
int i = s_rowIdx[s_hl - 1];
if (getenv("GE_FRONTNAVLOG"))
sysLogPrintf(LOG_INFO, "frontnav: select %s/%s (item %d)",
optionsRowLabel(activeHeader()), optionsRowLabel(i), s_hl);
playSfx(DOOR_LOCK_SFX);
int child = optionsRowChildHeader(i);
int bx0 = optionsRowIsSlider(i) ? rowBarX0(optionsRowLabel(i)) : BAR_X0;
if (child >= 0) {
optionsResetClear();
s_subHeader = child;
s_level++;
s_pageno = 0; /* D406 */
buildPages();
sysLogPrintf(LOG_INFO, "frontoptions: opened %s (depth %d)",
optionsRowLabel(child), s_level);
cursorToItem(0);
s_hl = -1;
} else if (optionsRowIsSlider(i) && bx0 <= BAR_X1 - 12 &&
cursor_h_pos >= bx0 - 4 && cursor_h_pos <= BAR_X1 + 4) {
optionsRowSetFraction(i, ((double)cursor_h_pos - bx0) / (BAR_X1 - bx0));
s_dragRow = i;
} else if (optionsRowIsBind(i)) {
/* D395: pad A/X may navigate the keyboard/mouse binding
* pages but cannot enter a modal that only a key can finish.
* Enter or a mouse click still starts ordinary key capture. */
const Uint8 *ks = SDL_GetKeyboardState(NULL);
if (ks[SDL_SCANCODE_RETURN] || ks[SDL_SCANCODE_KP_ENTER] ||
(SDL_GetMouseState(NULL, NULL) & SDL_BUTTON(SDL_BUTTON_LEFT)))
optionsRowBeginBind(i);
} else if (optionsRowIsReset(i)) {
/* D356: reset rows are two-step arm -> confirm (edge
* activation: joyGetButtonsPressedThisFrame fires once per
* press). The value column shows "Confirm" while armed. */
optionsRowActivateReset(i);
} else {
optionsRowAdjust(i, +1); /* toggle / cycle / step (wraps) */
}
}
} else if (joyGetButtonsPressedThisFrame(PLAYER_1, B_BUTTON)) {
goBack();
}
/* Drag a slider while A is held. */
if (s_dragRow >= 0) {
if (joyGetButtons(PLAYER_1, A_BUTTON | Z_TRIG)) {
int bx0 = rowBarX0(optionsRowLabel(s_dragRow));
optionsRowSetFraction(s_dragRow, ((double)cursor_h_pos - bx0) / (BAR_X1 - bx0));
} else {
optionsRowCommit(s_dragRow);
s_dragRow = -1;
}
}
/* Left/right (D-pad, C buttons; keyboard A/D) adjust the highlighted row,
* with hold-to-repeat. D356: on the save-file row it cycles the file;
* on a reset row a fresh press activates (arm/confirm) and the held
* repeat is suppressed (the activation contract, plan §5.4). */
if (s_level >= 1 && s_hl >= 0) {
int dir = 0;
/* D345(f): stick X joins the D-pad/C-buttons, so Left/Right arrows
* (and the pad's left stick) adjust values like F10's left/right. */
s8 vsx = joyGetStickX(PLAYER_1);
if (joyGetButtons(PLAYER_1, L_JPAD | L_CBUTTONS) || vsx < -5) dir = -1;
if (joyGetButtons(PLAYER_1, R_JPAD | R_CBUTTONS) || vsx > 5) dir = +1;
if (dir != 0) {
int fire = 0;
if (dir != s_repeatDir) {
fire = 1;
s_repeatTimer = 18;
} else if (--s_repeatTimer <= 0) {
fire = 1;
s_repeatTimer = 4;
}
if (fire) {
if (s_hl == 0) {
watchSettingsChooseFile(dir); /* save-file row */
} else {
int i = s_rowIdx[s_hl - 1];
if (optionsRowIsReset(i))
optionsRowActivateReset(i);
else if (optionsRowChildHeader(i) < 0)
optionsRowAdjust(i, dir);
}
}
}
s_repeatDir = dir;
} else {
s_repeatDir = 0;
}
/* D356: reset arm state -- the screen's selected rows[] index (s_hl 0 is
* the save row, not a rows[] entry); moving on disarms, goBack clears. */
optionsResetMaintain(s_level >= 1 && s_hl > 0 ? s_rowIdx[s_hl - 1] : -1);
/* The dossier: tabs + a blank CLASSIFIED page, as the cheat screen. */
disable_all_switches(walletinst[0]);
set_item_visibility_in_objinstance(walletinst[0], SW_TABS, 1);
set_item_visibility_in_objinstance(walletinst[0], SW_BLANK, 1);
set_item_visibility_in_objinstance(walletinst[0], SW_CLASSIFIED, 1);
/* D345(e): F10-style discrete vertical navigation. W/S / Up-Down arrows
* (stick via D345(d)) or the pad's stick/D-pad step the highlight one
* row at a time -- clamped at the ends, hold-to-repeat with the same
* 18/4-frame cadence as left/right -- and snap the crosshair onto the
* new row (ungated: explicit keyboard navigation owns the cursor even
* right after mouse use). Replaces the raw stick drift of D345(c),
* which fought the pointer over cursor_v_pos. The mouse is untouched:
* any movement re-owns the cursor and the highlight hit-test above
* follows it next frame. Skipped while a slider drag holds A. */
if (s_dragRow < 0) {
int vdir = 0;
s8 vsy = joyGetStickY(PLAYER_1);
if (joyGetButtons(PLAYER_1, U_JPAD) || vsy > 5) vdir = -1; /* up */
else if (joyGetButtons(PLAYER_1, D_JPAD) || vsy < -5) vdir = +1; /* down */
if (vdir != 0) {
int fire = 0;
if (vdir != s_vrepeatDir) {
fire = 1;
s_vrepeatTimer = 18;
} else if (--s_vrepeatTimer <= 0) {
fire = 1;
s_vrepeatTimer = 4;
}
if (fire) {
int n = itemCount();
if (n > 0) {
int q = (s_hl < 0 ? 0 : s_hl) + vdir;
/* D406: stepping past the end of the visible page turns
* the page (cursor lands on the first row of the next
* page, or the last row of the previous one); with one
* page the ends simply clamp, as before. */
if (q >= n && s_pageno * ROWS_PER_PAGE + s_rowN < s_rowTotal) {
s_pageno++;
buildPages();
q = 1;
} else if (q < 0 && s_pageno > 0) {
s_pageno--;
buildPages();
q = itemCount() - 1;
} else {
if (q < 0) q = 0;
if (q >= n) q = n - 1;
}
if (q != s_hl) {
if (getenv("GE_FRONTNAVLOG"))
sysLogPrintf(LOG_INFO, "frontnav: %s -> item %d%s%s (page %d)",
s_level ? optionsRowLabel(activeHeader()) : "categories",
q, s_level && q > 0 ? " " : "",
s_level && q > 0 ? optionsRowLabel(s_rowIdx[q - 1]) : "",
s_level ? s_pageno + 1 : 0);
s_hl = q;
cursorToItemRaw(q);
}
}
}
}
s_vrepeatDir = vdir;
} else {
s_vrepeatDir = 0;
}
}
Gfx *frontOptionsMenuDraw(Gfx *DL)
{
char buf[48];
DL = viSetFillColor(DL, 0, 0, 0);
DL = viFillScreen(DL);
#ifdef VERSION_EU
DL = viFillScreen(DL);
DL = viFillScreen(DL);
DL = viFillScreen(DL);
#endif
DL = frontSetupMenuBackground(DL);
DL = microcode_constructor(DL);
if (s_level == 0) {
DL = ink(DL, ROW_X, TITLE_Y, "PC Options\n", INK);
for (int k = 0; k < s_pageN; k++) {
char name[32], num[8];
titleCase(optionsRowLabel(s_pageHdr[k]), name, sizeof(name));
strcat(name, "\n");
snprintf(num, sizeof(num), "%d.\n", k + 1);
if (k == s_hl) {
DL = microcode_constructor_related_to_menus(DL, SEC_X - 2, rowY(k) - 1,
SEC_X + measureW(name) + 5, rowY(k) + 0xE, HILITE);
}
DL = ink(DL, NUM_X, rowY(k), num, INK);
DL = ink(DL, SEC_X, rowY(k), name, INK);
}
} else {
char title[32];
titleCase(optionsRowLabel(activeHeader()), title, sizeof(title));
int titleW = measureW(title);
strcat(title, "\n");
DL = ink(DL, ROW_X, TITLE_Y, title, INK);
/* D356: scope annotation -- a section that carries per-file rows has
* its title carry the active file ("GAMEPLAY (File 2)"), PD-style.
* watchSettingsActiveFolder() is the context-aware accessor (front:
* the resolved front target; -1 while no file is usable, which the
* annotation renders as an unavailable state, never a number the
* write path cannot use). */
if (optionsRowIsSaveScoped(activeHeader())) {
int f = watchSettingsActiveFolder();
char note[16];
if (f < 0) snprintf(note, sizeof(note), "(none)");
else snprintf(note, sizeof(note), "(Profile %d)", f + 1);
DL = ink(DL, ROW_X + titleW + 8, TITLE_Y, note, INK_DIM);
}
/* D406b: page marker for sections that span pages -- "Page p/N"
* right of the title row, dim, so page 2 is unmistakable. */
if (s_rowTotal > ROWS_PER_PAGE) {
int pages = (s_rowTotal + ROWS_PER_PAGE - 1) / ROWS_PER_PAGE;
char pg[24];
snprintf(pg, sizeof(pg), "Page %d/%d", s_pageno + 1, pages);
DL = inkR(DL, VAL_R, TITLE_Y, pg, INK_DIM);
}
/* D356: the top profile row (item 0, not in the row table; the label
* reads "Profile" -- the end-user term for a per-player game file,
* see D357 plan section 4): the single file control of the front
* screen (F10 always targets the active file, so it has no file
* row). L/R (and A) cycle folders; (none) is the unavailable state
* until one is created. */
{
int y = rowY(0);
char sv[32];
if (s_hl == 0) {
DL = microcode_constructor_related_to_menus(DL, ROW_X - 2, y - 1,
ROW_X + 140, y + 0xE, HILITE);
}
DL = ink(DL, ROW_X, y, "Profile\n", INK);
int f = watchSettingsActiveFolder();
if (f < 0) snprintf(sv, sizeof(sv), "(none)");
else snprintf(sv, sizeof(sv), "%d\n", f + 1);
DL = inkR(DL, VAL_R, y, sv, INK);
}
for (int k = 0; k < s_rowN; k++) {
int i = s_rowIdx[k];
/* D406b: one-shot label-metrics log (layout diagnostics). */
static int widthsLogged = 0;
if (!widthsLogged && getenv("GE_FRONTOPTS_WIDTHS")) {
widthsLogged = 1;
for (int w2 = 0; w2 < s_rowN; w2++) {
int j = s_rowIdx[w2];
sysLogPrintf(LOG_INFO, "frontoptions: '%s' w=%d barx0=%d",
optionsRowLabel(j), measureW(optionsRowLabel(j)),
rowBarX0(optionsRowLabel(j)));
}
}
/* Content row k is item k+1 (item 0 is the save-file row), so
* it sits one line below the save row -- rowY() already shifts
* level-1 items down by one. */
int y = rowY(k + 1);
char label[48];
snprintf(label, sizeof(label), "%s\n", optionsRowLabel(i));
if (k + 1 == s_hl) {
DL = microcode_constructor_related_to_menus(DL, ROW_X - 2, y - 1,
ROW_X + measureW(label) + 5, y + 0xE, HILITE);
}
DL = ink(DL, ROW_X, y, label, INK);
/* GAMEPLAY/HUD mix ini and profile options. AUDIO is wholly
* profile-scoped and already has (Profile N) in its title. */
if (optionsRowIsSaveScoped(i) &&
(strcmp(optionsRowLabel(activeHeader()), "GAMEPLAY") == 0 ||
strcmp(optionsRowLabel(activeHeader()), "HUD") == 0)) {
DL = ink(DL, ROW_X + measureW(label) + 4, y, "(per profile)\n", INK_DIM);
}
if (optionsRowIsSlider(i)) {
int x0 = rowBarX0(optionsRowLabel(i));
if (BAR_X1 - x0 >= 12) {
s32 fx = x0 + (s32)((BAR_X1 - x0) * optionsRowFraction(i) + 0.5);
DL = microcode_constructor_related_to_menus(DL, x0, y + 5, BAR_X1, y + 8, 0x00000040);
DL = microcode_constructor_related_to_menus(DL, x0, y + 5, fx, y + 8, 0xA00000C0);
}
} else if (optionsRowNeedsRestart(i)) {
DL = ink(DL, BAR_X0, y, "(restart)\n", INK_DIM);
}
optionsRowValueText(i, buf, sizeof(buf) - 1);
if (strcmp(optionsRowLabel(i), "Crosshair colour") == 0 && buf[0]) {
extern void portCrosshairPreview(s32 *, s32 *, s32 *);
s32 red, green, blue;
portCrosshairPreview(&red, &green, &blue);
s32 x = VAL_R - measureW(buf) - 17;
DL = microcode_constructor_related_to_menus(DL, x, y + 2,
x + 10, y + 12,
((u32)red << 24) | ((u32)green << 16) |
((u32)blue << 8) | 0xff);
}
if (buf[0]) {
u32 col = (buf[0] == 'O' && buf[1] == 'N') ? INK_ON : INK;
/* D346: kOnOff is title-case; M7 (D387) tags the
* experimental AllUnlocked ON value "ON - UNSAFE". */
strcat(buf, "\n");
DL = inkR(DL, VAL_R, y, buf, col);
}
}
if (strcmp(optionsRowLabel(activeHeader()), "MOVEMENT") == 0 ||
strcmp(optionsRowLabel(activeHeader()), "ACTIONS") == 0) {
DL = ink(DL, ROW_X, rowY(s_rowN + 2),
optionsBindingCaptureActive()
? "Press key/mouse B/ESC cancel DEL clear\n"
: "Keys/mouse only Enter: bind B: back\n", INK_DIM);
} else if (s_rowTotal > ROWS_PER_PAGE) {
/* D406b: bottom hint on every page of a multi-page section (11
* rows per page leaves the last line of the paper for it). */
int last = s_pageno * ROWS_PER_PAGE + s_rowN >= s_rowTotal;
int first = s_pageno == 0;
/* D406d: one row below the last content row (rowY(s_rowN + 1))
* -- pitch-aligned with the list; +2 sat ~26px clear of it and
* looked orphaned at the paper's bottom edge. */
DL = ink(DL, ROW_X, rowY(s_rowN + 1),
first ? "Down: next page\n"
: last ? "Up: previous page\n"
: "Up: previous page Down: next page\n",
INK_DIM);
}
}
DL = frontAddPreviousTabText(DL);
DL = frontDrawCursor(DL);
return DL;
}
/* F10 / Select must not stack the overlay on this screen. */
int frontOptionsBlocksOverlay(void)
{
return current_menu == MENU_PC_OPTIONS;
}
/* ------------------------------------------------------------------------ */
/* File select: the "Options" entry, drawn from the D343 hook in */
/* constructor_menu05_fileselect (game thread, after the interface tick). */
/* ------------------------------------------------------------------------ */
Gfx *optionsFileSelectLabel(Gfx *gdl)
{
s32 h = 0, w = 0;
s32 x, y;
int hot;
/* The F10 overlay pads the controller away from front.c, so file select's
* idle timer never resets and it dropped to the legal screen after 30 s.
* This hook only runs on MENU_FILE_SELECT, the one screen where
* g_MenuTimer is an idle timer (elsewhere it is the intro / cast-roll
* clock). Same write front.c makes when a button is pressed. */
if (optionsOverlayIsOpen()) {
g_MenuTimer = 0;
}
/* D400 (user sign-off, "most straightforward design"): text-only label,
* the D398 dot icon is gone. Exact Copy/Erase pattern from front.c:
* measure the single-line label (no trailing newline -- the kLabelNL
* trick measured a taller box and sat the text ~7px higher than the
* bar's other words, the "different size/look" complaint), centre it
* on the bar line, draw with the same font. Hot colour is the game's
* own packed gold 0xEBD879FF, the very constant front.c passes to
* textRender for its gold folder text -- no per-channel unpacking, so
* no purple (the D398 dot bug came from unpacking that word for
* gDPSetEnvColor in the wrong byte order). */
/* NB: textMeasure's signature is (textheight, textwidth) -- height FIRST
* (an earlier cut passed these swapped and drew the label 35px above the
* bar, hit band transposed). Height is measured WITH a trailing newline:
* Copy/Erase's localised strings end in a newline, so textMeasure gives
* them a full 14px line height and y = 285 - 7 = 278 (PCDUMP tops
* 279); measuring the bare label gives h=0 -> y=285, sitting 7px LOWER
* than the bar's other words. Measuring kLabelNL matches their tops
* exactly. (This was the D398 "kLabelNL trick"; the D400 rewrite
* dropped it and that is what broke the vertical alignment.) */
textMeasure(&h, &w, (char *)kLabelNL, ptrFontZurichBoldChars, ptrFontZurichBold, 0);
/* Follow the Erase label's measured right edge (set by the constructor
* just before this hook), so a wider localised "Erase" (JP glyphs)
* pushes the label right instead of overlapping it. */
x = (s32)folder_option_ERASE_bound.right + LABEL_GAP;
if (x < LABEL_X) {
x = LABEL_X;
}
/* Same centring front.c uses for Copy/Erase (front.c:2779/2791). */
y = LABEL_CY - (h / 2);
/* D406: test hook -- enter the screen without a mouse click so headless
* runs (GE_INPUTSCRIPT D-pad stepping + GE_FRONTNAVLOG / diagnostics)
* can exercise it. Diagnostic-only: never active without the env. */
if (getenv("GE_FRONTOPTIONS_AUTO") && menu_update == MENU_INVALID &&
folder_selected_for_deletion < 0) {
static int autoEntered = 0;
if (!autoEntered) {
autoEntered = 1;
playSfx(DOOR_LOCK_SFX);
frontChangeMenu(MENU_PC_OPTIONS, FALSE);
}
}
hot = !optionsOverlayIsOpen()
&& menu_update == MENU_INVALID
&& folder_selected_for_deletion < 0
&& cursor_h_pos >= (f32)(x - HIT_PAD) && cursor_h_pos <= (f32)(x + w + HIT_PAD)
&& cursor_v_pos >= (f32)(y - HIT_PAD) && cursor_v_pos <= (f32)(y + h + HIT_PAD);
if (hot && joyGetButtonsPressedThisFrame(PLAYER_1, A_BUTTON | Z_TRIG | START_BUTTON)) {
playSfx(DOOR_LOCK_SFX); /* Copy/Erase's click */
frontChangeMenu(MENU_PC_OPTIONS, FALSE);
}
/* Same font, size and centre line as Copy/Erase; white idle, the
* game's gold while the cursor is over it. */
return textRender(gdl, &x, &y, (char *)kLabel, ptrFontZurichBoldChars,
ptrFontZurichBold, hot ? 0xEBD879FF : 0xFFFFFFFF,
viGetX(), viGetY(), 0, 0);
}
+40 -6
View File
@@ -11,6 +11,9 @@
#include <PR/ultratypes.h>
#include <PR/gbi.h>
#include "gimgfixup.h"
extern char *getenv(const char *name); /* D324: no CRT proto via GE stdlib.h */
#include "envflag.h"
extern void osSyncPrintf(const char *fmt, ...);
#define GIMG_REGION_SIZE 0x13F8u
@@ -99,9 +102,17 @@ void gimgFixupGlobalimagetable(u8 *base)
}
}
/* D252 (2026-09-26): which compiled G_SETTIMG slots are IMAGESEG texture
* references. Learned on the first sync, while they still hold the 0xABCD
* marker; every later sync re-copies exactly these slots. */
#define GIMG_MAX_CMDS 256
static u8 s_imgslot[N_DLS][GIMG_MAX_CMDS];
static int s_imgslot_learned = 0;
void gimgSyncCompiledGlobalDLs(u8 *base)
{
int i;
int nslots = 0, nchanged = 0; /* D252 TEMP verify counters */
u8 *end = base + GIMG_REGION_SIZE;
for (i = 0; i < N_DLS; i++)
@@ -110,21 +121,44 @@ void gimgSyncCompiledGlobalDLs(u8 *base)
Gfx *dst = s_dl_syms[i];
int j = 0;
while (p + 8 <= end && p[0] != (u8)G_ENDDL)
while (p + 8 <= end && p[0] != (u8)G_ENDDL && j < GIMG_MAX_CMDS)
{
/* D68/C2: texLoadFromDisplayList() has already replaced every
* IMAGESEG w1 word in the ROM copy with a real texture pointer,
* so the 0xABCDxxxx marker is gone from `p`. Identify the slots
* that need syncing from the COMPILED array instead: a G_SETTIMG
* command whose w1 is still an unresolved IMAGESEG marker
* (0xABCDxxxx). Copy the ROM copy's resolved word into it. */
if ((u8)(dst[j].words.w0 >> 24) == (u8)G_SETTIMG &&
* so the 0xABCDxxxx marker is gone from `p`. On the first call,
* identify the slots from the COMPILED array instead (a
* G_SETTIMG whose w1 is still an unresolved 0xABCDxxxx marker)
* and remember them.
*
* D252: texReset() runs on EVERY stage load and re-resolves the
* ROM copy into the new stage's texture pool. The original sync
* only ever copied into slots still holding the marker, i.e.
* only on the first stage (the title screen). Every later stage
* kept the title's pointers into its freed texture pool; once
* the new stage's allocations overwrote that memory, explosion
* smoke/fire/debris sampled garbage (rainbow particles). Re-copy
* the remembered slots on every call, as the N64 does by running
* the freshly patched ROM copy directly. */
if (!s_imgslot_learned &&
(u8)(dst[j].words.w0 >> 24) == (u8)G_SETTIMG &&
(dst[j].words.w1 >> 16) == 0xABCDu)
{
s_imgslot[i][j] = 1;
}
if (s_imgslot[i][j])
{
nslots++;
if ((u32)dst[j].words.w1 != *(u32 *)(p + 4))
nchanged++;
dst[j].words.w1 = *(u32 *)(p + 4);
}
p += 8;
j++;
}
}
s_imgslot_learned = 1;
/* D252 TEMP: before the fix, every stage after the first left
* `nchanged` explosion texture pointers stale. */
if (GE_ENVFLAG("GE_D252POOL"))
osSyncPrintf("D252SYNC: %d texture slots re-synced, %d changed\n", nslots, nchanged);
}
+1284 -122
View File
File diff suppressed because it is too large. Load diff
+107 -23
View File
@@ -50,6 +50,7 @@
#include "input.h"
#include "fs.h"
#include "romdata.h"
#include "watchsettings.h"
#include "crash.h"
#if defined(PLATFORM_WINDOWS)
@@ -706,6 +707,10 @@ s32 osRecvMesg(OSMesgQueue *mq, OSMesg *msg, s32 flag)
d60logRecv(mq, m); /* TEMP D60 */
pthread_cond_signal(&pq->cond);
pthread_mutex_unlock(&pq->lock);
/* gfxFrameMsgQ is consumed only by boss.c's game thread. Apply queued
* F10 watch edits there, after releasing the OS queue lock; the SDL
* input/scheduler and render threads never touch GE watch/save state. */
if (mq == &gfxFrameMsgQ) watchSettingsGameTick();
return 0;
}
@@ -1094,7 +1099,10 @@ static void contSnapshotFromKeyboard(void)
u16 button = connected ? (u16)inputComputePad(i, &sx, &sy) : 0;
g_contStatus[i].type = connected ? CONT_TYPE_NORMAL : 0;
g_contStatus[i].status = 0;
/* D401: present a Rumble Pak (CONT_CARD_ON) whenever the connected
* pad can actually rumble. joyRumblePakInit (src/joy.c:184) gates
* the whole rumble-pak path on this bit. */
g_contStatus[i].status = (connected && inputRumbleSupported(i)) ? CONT_CARD_ON : 0;
g_contStatus[i].errno = connected ? 0 : CONT_NO_RESPONSE_ERROR;
g_contPad[i].button = button;
@@ -1279,26 +1287,55 @@ static void geEepromPatchAllCheats(u8 *buf)
memset(&slots[5], 0, sizeof(ge_save_slot));
int changed = 0;
for (int i = 0; i < 5; i++) {
/* D259: a fresh ge007.eep is zero-filled, so every slot reads as
* all-zero. Normally such a slot fails fileValidateSaves' CRC and is
* reset to BLANKSAVEDATA (music_vol/sfx_vol = 0xFF); but this patch
* gives the slot a valid CRC below, so it survives with volume 0
* -> silence. Emulate fileResetSave: seed max volume on all-zero
* slots only -- real saves (incl. a deliberately muted one) keep
* their bytes. */
{
/* D259 + D281: a fresh ge007.eep is zero-filled, so every slot reads as
* all-zero. Without this patch such a slot fails fileValidateSaves' CRC
* and becomes a free BLANKSAVEDATA slot (fileResetSave), after which
* fileBuildWriteNewSave fills the first free slot for each folder with
* no save, in folder order. This patch gives every slot a valid CRC
* below, so an all-zero slot would instead SURVIVE as-is: five slots all
* claiming folder 1 with volume 0 (D259: silence) and options 0 (D281:
* sight-on-screen, auto-aim, look-ahead and ammo display all off, which
* reads as "RMB aim broken"). D259 seeded only the volumes. Reproduce
* the game's full result instead: all-zero slots take the folders that
* no real slot holds, in order, built exactly as fileBuildWriteNewSave
* builds them (BLANKSAVEDATA + folder + not-free + bond); any left over
* become free BLANKSAVEDATA slots. Real saves keep their bytes. */
{
extern void fileSetSaveFoldernum(void *save, u32 folder);
extern void fileSetSaveFlagDoReset(void *save, s32 enable);
extern void fileSetSelectedBond(void *save, s32 bond);
static const ge_save_slot blank = {
0, 0, 0x80 /* SAVEFLAGS_SET(0,0,BOND_BROSNAN,1): free */, 0x00,
0xFF, 0xFF, 0x3A /* DEFAULT_OPTIONS */, 0, 0, 0, 0, {0}
};
int allzero[5], present[4] = {0, 0, 0, 0};
for (int i = 0; i < 5; i++) {
const u8 *raw = (const u8 *)&slots[i];
int allzero = 1;
allzero[i] = 1;
for (int b = 0; b < (int)sizeof(ge_save_slot); b++)
if (raw[b]) { allzero = 0; break; }
if (allzero && (slots[i].music_vol != 0xFF ||
slots[i].sfx_vol != 0xFF)) {
slots[i].music_vol = 0xFF;
slots[i].sfx_vol = 0xFF;
changed = 1;
if (raw[b]) { allzero[i] = 0; break; }
if (!allzero[i] && !(slots[i].completion_bitflags & 0x80 /* DORESET */)) {
int f = slots[i].completion_bitflags & 0x7; /* SAVEFLAG_FOLDER */
if (f < 4) present[f] = 1; /* MAX_FOLDER_COUNT */
}
}
int folder = 0;
for (int i = 0; i < 5; i++) {
if (!allzero[i]) continue;
slots[i] = blank;
while (folder < 4 && present[folder]) folder++;
if (folder < 4) {
fileSetSaveFoldernum(&slots[i], (u32)folder);
fileSetSaveFlagDoReset(&slots[i], 0);
fileSetSelectedBond(&slots[i], folder);
present[folder] = 1;
}
changed = 1;
}
}
for (int i = 0; i < 5; i++) {
/* Cheat ids are level ids 0..19 (CHEAT_INPUT_BUFFER_SIZE == 20):
* bits 0-7 in _1, 8-15 in _2, 16-19 in the low nibble of _3. */
if (slots[i].unlocked_cheats_1 != 0xFF ||
@@ -1375,15 +1412,62 @@ s32 osEepromLongRead(OSMesgQueue *mq, u8 addr, u8 *buf, int nbytes)
s32 osEepromLongWrite(OSMesgQueue *mq, u8 addr, u8 *buf, int nbytes)
{ (void)mq; return geEepromRW(addr, buf, nbytes, 1); }
/* Memory Pak (PFS) + Rumble Pak (motor): no accessories on the PC. */
/* Memory Pak (PFS) + Rumble Pak (motor): no Memory Pak on the PC (saves are
* file-backed EEPROM), but the Rumble Pak path is routed to real gamepad
* haptics (D401): a pad that can rumble presents itself as a "card is on"
* accessory, and the motor calls become SDL_GameControllerRumble via
* inputRumble (port/src/input.c). */
s32 osPfsInit(OSMesgQueue *queue, OSPfs *pfs, int channel)
{ (void)queue; (void)pfs; (void)channel; return PFS_ERR_NOPACK; }
{
(void)queue; (void)pfs;
/* PFS_ERR_DEVICE ("wrong device type") = a Rumble Pak is present, not a
* Memory Pak: exactly what joyRumblePakInit (src/joy.c:186-190) needs
* to proceed to osMotorInit. PD's osPfsInitPak does the same. */
return inputRumbleSupported(channel) ? PFS_ERR_DEVICE : PFS_ERR_NOPACK;
}
s32 osPfsIsPlug(OSMesgQueue *queue, u8 *pattern)
{ (void)queue; if (pattern) *pattern = 0; return 0; }
{
(void)queue;
if (pattern) {
*pattern = 0;
/* JPN: MAXCONTROLLERS can be 6 > MAX_PADS; inputRumbleSupported()
* bounds-checks, so the extra channels just never light up. */
for (int i = 0; i < MAXCONTROLLERS; ++i)
if (inputRumbleSupported(i)) *pattern |= (u8)(1 << i);
}
return 0;
}
s32 osMotorInit(OSMesgQueue *mq, OSPfs *pfs, int channel)
{ (void)mq; (void)pfs; (void)channel; return -1; }
s32 osMotorStart(OSPfs *pfs) { (void)pfs; return -1; }
s32 osMotorStop(OSPfs *pfs) { (void)pfs; return -1; }
{
if (pfs && inputRumbleSupported(channel)) {
pfs->queue = mq;
pfs->channel = channel;
pfs->activebank = 0xff;
/* NOTE: PFS_MOTOR_INITIALIZED is not defined in GE's headers and
* joy.c never reads pfs->status -- it only tests osMotorInit()==0
* (src/joy.c:190) to mark the pad RUMBLEPAKINITSTATE_READY, so no
* status store is needed (PD's osMotorProbe writes one). */
return 0;
}
(void)mq; (void)channel;
return PFS_ERR_NOPACK;
}
s32 osMotorStart(OSPfs *pfs)
{
if (!pfs) return PFS_ERR_NOPACK;
/* The N64 motor has no duration; the game's joyRumblePakTimer60 (src/joy.c)
* arms a per-event countdown and turns the motor off via osMotorStop.
* Issue one generous 5 s window, PD-style ("hope the timer stops it");
* inputRumble no-ops internally for non-rumble pads. */
inputRumble(pfs->channel, 1.0f, 5.0f);
return 0;
}
s32 osMotorStop(OSPfs *pfs)
{
if (!pfs) return PFS_ERR_NOPACK;
inputRumble(pfs->channel, 0.0f, 0.0f); /* zero strength/duration = stop */
return 0;
}
/* ------------------------------------------------------------------------ */
/* SP (RSP) — runs the software RSP inline, then posts the done messages */
+1 -1
View File
@@ -121,7 +121,7 @@ int main(int argc, char **argv)
}
/* 1. Platform + config + filesystem. Steam Deck / SteamOS: seed the
* first-run preset (native 1280x800 fullscreen, MSAA 4, longer draw/LOD
* first-run preset (native 1280x800 fullscreen, MSAA 2, midpoint draw/LOD
* distances) before the load so a missing ini saves these values; an
* existing ini always wins. */
if (getenv("STEAMOS")) {
+1891 -289
View File
File diff suppressed because it is too large. Load diff
+349 -46
View File
@@ -30,6 +30,7 @@
#include "video.h"
#include "input.h"
#include "optionsoverlay.h"
#include "frontoptions.h"
#include "../fast3d/gfx_api.h"
#include "../fast3d/gfx_sdl.h"
@@ -50,21 +51,24 @@ static struct GfxRenderingAPI *renderingAPI;
static int initDone = 0;
/*
* [Video] ge007.ini knobs. Every default reproduces the previously-hardcoded
* behaviour, so a fresh config or a missing [Video] section changes nothing.
* [Video] ge007.ini knobs. Draw/LOD and MSAA defaults favour a clean picture
* at modern resolutions without requiring 4x anti-aliasing.
*/
static int cfgVSync = 1; /* swap interval: 0 = off, 1 = on */
static int cfgFpsCap = 60; /* frame cap in fps; 0 = uncapped (vsync); menu only exposes 30/60 */
static int cfgMSAA = 4; /* 1/2/4/8 samples; default 4 (modern ports ship AA on; snaps down to the highest supported level) */
static int cfgMSAA = 2; /* 1/2/4/8 samples; 2x default is lighter on low-end GPUs */
static int cfgTexFilter = 1; /* 0 = nearest, 1 = bilinear (default), 2 = N64 3-point + trilinear */
static int cfgFixMipTex = 1; /* RC2: clip mip-contaminated texture uploads to base height */
static int cfgDetailBaseTile = 1; /* D236: TEXTURETYPE_DETAIL -> sample the base image, not the detail tile */
static int cfgWrapFix = 0; /* D74 sub-tile UV pre-wrap + RC3/D167 non-PoT mask-period wrap (opt-in; GE_WRAPFIX env overrides) */
static int cfgFovScale = 100; /* D211: percent of the original vertical FOV; 100 = unchanged (byte-identical) */
static int cfgWidescreenAuto = 1; /* WIDESCREEN-FOV-PLAN Phase 4: auto-scale vertical FOV by window aspect ratio; on by default, no-op at 4:3 */
static int cfgDrawDistance = 150; /* D218: percent of the level's authored far-clip/fog distance. Default raised 100->150 for v0.2.0: at the authored N64 distance, props visibly fade in just before they become visible on modern displays (Dam alarms / wall switches); 150 is the value the Steam Deck preset playtest-validated. 100 = unchanged N64. */
static int cfgDrawDistanceAutoFov = 1; /* D218: couple draw distance to Video.FovScale unless DrawDistance is set explicitly */
static int cfgLodDistance = 150; /* D249: percent scale on the geometry/model LOD-swap distance. Default raised 100->150 for v0.2.0 (same pop-in family as DrawDistance: LOD-swapped props like Dam's alarms/wall switches faded in at range); 150 matches the Steam Deck preset. 100 = unchanged N64. */
static int cfgLodDistanceAutoFov = 0; /* off by default -- unlike DrawDistance, this is meant as a standalone perf lever, not something that should silently get more expensive as FovScale widens */
static int cfgNativeWidescreen = 1; /* D334 (WIDESCREEN-FOV-PLAN Phase 2): project the world at the real window aspect (Hor+); no-op at 4:3 */
static int cfgHudScale = 100; /* D226: HUD text/ammo scale %, 100 = original (no emission) */
static int cfgDrawDistance = 250; /* % of authored far clip; 250% is the UI's 50/100 midpoint */
static int cfgDrawDistanceAutoFov = 0; /* legacy ini option, no longer exposed in the menu */
static int cfgLodDistance = 250; /* % of authored geometry LOD distance; 50/100 in the UI */
static int cfgLodDistanceAutoFov = 0; /* legacy ini option, no longer exposed in the menu */
static int cfgAniso = 4; /* D212: anisotropic filtering samples; 4 = the value fast3d already applied (no visual delta at default) */
static int cfgSafeAreaCrop = 1; /* crop the N64 TV-overscan safe-area margin (visible as black top/bottom bars on PC) instead of showing it; on by default */
static int cfgFullscreen = 0; /* 0 = windowed, 1 = borderless fullscreen */
@@ -98,6 +102,73 @@ s32 portSkipIntro = 0;
* original damage flash. */
s32 portNoHitFlash = 0;
/* v0.4.0 M2 (D373/D379): in-game crosshair on/off + tint
* (gunfire.c gunDrawSight, #ifdef PORT). Default keeps the original
* authored red sprite; hide is opt-in. */
s32 portCrosshairHide = 0;
static int cfgCrosshairColor = 0; /* 0 = authored sprite; 1..7 = presets; 8 = custom RGB */
static int cfgCrosshairRed = 255, cfgCrosshairGreen = 255, cfgCrosshairBlue = 255;
static int cfgCrosshairSize = 100; /* 100% retains the original 32x32 drawing */
static int cfgCrosshairStyle = 0; /* 0 = original; 1 = unused beta asset */
int portCrosshairStyle(void) { return cfgCrosshairStyle; }
float portCrosshairScale(void) { return cfgCrosshairSize / 100.0f; }
/* Index 0 leaves the authored RED sprite untouched (white env multiplier);
* index 7 is actual white via its alpha silhouette. */
/* The authored RGBA32 reticle is RED (confirmed from the decoded 32x32
* IMAGE_CROSSHAIR1 texture). G_CC_FADEA multiplies TEXEL0.rgb by env.rgb;
* no amount of blue/cyan tint can recover channels absent from the texture.
* For the opt-in colours, substitute a combiner that uses TEXEL0 alpha as
* the grayscale reticle mask and ENVIRONMENT for the requested RGB. The
* alpha expression is unchanged: texture alpha * env alpha (0x6e).
* The call site points at display_image_at_position's env-color command;
* its immediately following command is the combine mode. Original/0
* skips this rewrite, keeping the original game's DL byte-identical. */
void portCrosshairApplyTintCombine(Gfx *envCommand)
{
if (cfgCrosshairColor == 0 || !envCommand) return;
gDPSetCombineLERP(envCommand + 1,
ENVIRONMENT, 0, TEXEL0_ALPHA, 0, TEXEL0, 0, ENVIRONMENT, 0,
ENVIRONMENT, 0, TEXEL0_ALPHA, 0, TEXEL0, 0, ENVIRONMENT, 0);
}
void portCrosshairTint(s32 *r, s32 *g, s32 *b)
{
static const unsigned char kTints[8][3] = {
{ 0xFF, 0xFF, 0xFF }, /* Original red sprite (identity multiplier) */
{ 0x40, 0xFF, 0x40 }, /* Green */
{ 0xFF, 0x40, 0x40 }, /* Red */
{ 0x40, 0x40, 0xFF }, /* Blue */
{ 0xFF, 0xFF, 0x40 }, /* Yellow */
{ 0x40, 0xFF, 0xFF }, /* Cyan */
{ 0xFF, 0x40, 0xFF }, /* Magenta */
{ 0xFF, 0xFF, 0xFF }, /* Actual white, through alpha silhouette */
};
if (cfgCrosshairColor == 8) {
*r = cfgCrosshairRed;
*g = cfgCrosshairGreen;
*b = cfgCrosshairBlue;
return;
}
int index = (cfgCrosshairColor >= 0 && cfgCrosshairColor <= 7)
? cfgCrosshairColor : 0;
*r = (s32)kTints[index][0];
*g = (s32)kTints[index][1];
*b = (s32)kTints[index][2];
}
/* The original sprite is red despite its white identity multiplier. Show a
* representative red swatch for it rather than misleadingly showing white. */
void portCrosshairPreview(s32 *r, s32 *g, s32 *b)
{
if (cfgCrosshairColor == 0) {
*r = 255; *g = 40; *b = 40;
} else {
portCrosshairTint(r, g, b);
}
}
/* D257: Game.AllUnlocked — everything-unlocked goodie, OFF by default
* (faithful N64 progression: levels unlock as you complete them). Consumed
* once at startup by main.c, which sets the game's own RAM unlock flags
@@ -142,10 +213,44 @@ f32 portFovScale = 1.0f;
* auto-scale formula can otherwise degenerate toward as aspect -> 0; this
* resolves WIDESCREEN-FOV-PLAN.md's open "horizontal extreme-aspect sanity
* clamp" question. */
/* D334 (WIDESCREEN-FOV-PLAN Option B, Phase 2): the world-projection aspect
* for native widescreen, or 0 when off. The game's own N64 16:9 mode
* (bondview2.c bondviewMovePlayerUpdateViewport) projects at
* viewport_ratio * 0.75 * 16/9 -- an anamorphic pre-squash the TV undid. The
* port already stretches the 4:3 logical canvas to the window (fast3d
* RATIO_X), exactly like that TV, so the same formula with 16/9 replaced by
* the real window aspect renders the world undistorted at any aspect: vertical
* FOV stays the game's, horizontal widens (Hor+, the PD standard). Canvas,
* portal scissors and culling all stay in the game's 320x240 logical space and
* see the same aspect, so there is no second "view width" to reconcile
* (WIDESCREEN-FOV-PLAN s6). Clamped to [0.5, 4.0]. At exactly 4:3 the
* formula is identity, so 4:3 renders are unchanged. */
/* D226: Game.HudScale percent (75..150); 100 = original size, nothing emitted. */
s32 portHudScalePercent(void)
{
return cfgHudScale;
}
f32 portNativeAspect(void)
{
f32 a = gfx_current_dimensions.aspect_ratio;
if (!cfgNativeWidescreen || a < 0.01f) {
return 0.0f;
}
if (a < 0.5f) { a = 0.5f; }
if (a > 4.0f) { a = 4.0f; }
return a;
}
f32 portScaleFovY(f32 fovy, s32 isTitleScreen)
{
if (!isTitleScreen) {
if (cfgWidescreenAuto && gfx_current_dimensions.aspect_ratio > 0.01f) {
/* D334: native widescreen already widens the horizontal FOV through
* the projection aspect; the Phase-4 vertical boost below was the
* stretch-era compensation and would double-count, so it is skipped
* (vertical FOV stays the game's, as in the PD port). */
if (cfgWidescreenAuto && !cfgNativeWidescreen &&
gfx_current_dimensions.aspect_ratio > 0.01f) {
fovy *= sqrtf(gfx_current_dimensions.aspect_ratio / (4.0f / 3.0f));
}
if (portFovScale > 0.4f && portFovScale < 2.01f && portFovScale != 1.0f) {
@@ -162,7 +267,7 @@ f32 portScaleFovY(f32 fovy, s32 isTitleScreen)
* both the far clip plane and the fog-saturation distance (levels are tuned
* for the stock ~60deg FOV), plus the character/prop fog-visibility-fade
* cutoff (src/game/propobj.c chrobjFogVisRangeRelated/sub_GAME_7F054C58).
* Video.DrawDistanceAutoFov (default on) couples the multiplier to
* Video.DrawDistanceAutoFov (legacy ini option, default off) couples the multiplier to
* Video.FovScale so a wider FOV doesn't clip its own newly visible far
* geometry -- or fade NPCs out early -- against distances tuned for the
* narrower original view (D218's "blue artifacting"; the M-121 live
@@ -175,7 +280,7 @@ f32 portScaleFovY(f32 fovy, s32 isTitleScreen)
* glow" on far Dam tunnel geometry, so auto coupling is now 4x FovScale)
* to give headroom (max portFovScale is 1.5 at Video.FovScale's registered
* ceiling of 150, so 4x tops out at 6.0x -- ceiling raised to match).
* Identity (1.0f) at both defaults, bit-for-bit no-op. NOTE: end-to-end
* Identity (1.0f) at DrawDistance=100 with auto-FOV off. NOTE: end-to-end
* re-check of fogLoadCurrentEnvironment (bgfog.c) found the fog RAMP
* itself (not just the far-clip cutoff) already scales correctly with
* this multiplier -- g_ScaledFarFogIntensity/scaled_far_fog_dist both
@@ -206,14 +311,14 @@ f32 portDrawDistanceMultiplier(void)
* "closer", so this function returns the INVERSE of the requested percent:
* Video.LodDistance=200 (keep full detail twice as far, more cost) ->
* 0.5x on the distance term; =50 (drop to lower detail twice as soon, less
* cost -- the perf lever) -> 2.0x. Video.LodDistanceAutoFov (default OFF,
* unlike DrawDistanceAutoFov) can couple it to Video.FovScale the same
* cost -- the perf lever) -> 2.0x. Video.LodDistanceAutoFov (legacy ini
* option, default OFF) can couple it to Video.FovScale the same
* direction as draw distance if ever wanted; off by default so this stays a
* standalone dial and doesn't quietly add cost as FovScale widens. Clamped
* to a [0.25, 4.0] distance multiplier (== effective LodDistance 25-400%) --
* far outside that band either does nothing visible (LOD never triggers) or
* thrashes every frame. Identity (1.0f) at the Video.LodDistance=100
* default, bit-for-bit no-op. */
* thrashes every frame. Identity (1.0f) at Video.LodDistance=100
* with auto-FOV off. */
f32 portLodDistanceMultiplier(void)
{
f32 pct;
@@ -229,20 +334,103 @@ f32 portLodDistanceMultiplier(void)
return 100.0f / pct;
}
/* D294: the room-model pool (mema, sized per level by boss.c's
* memallocstringtable `-maNNN` rows) was budgeted by Rare for the N64's fixed
* ~60deg FOV / 4:3 / authored far-clip. The PC visibility knobs above widen
* what is on screen at once (FovScale + WidescreenAuto widen the frustum;
* DrawDistance pushes the far clip; LodDistance keeps high-detail meshes out
* further), so more rooms stay resident simultaneously. When demand exceeds
* the authored pool, memaAlloc() returns NULL and bgLoadRoomModelData() bails
* silently -- the undrawn room's scissor region shows the framebuffer clear
* colour: a transient solid-black patch of ground that self-recovers when the
* player moves (D294, Statue monument plaza at FovScale 130 + max DD/Lod).
*
* This returns a pool-size multiplier for boss.c to apply to `-ma` on PC:
* - FOV term: manual FovScale composed with the WidescreenAuto aspect
* scale (same sqrt(aspect/4:3) factor portScaleFovY uses), since both
* widen the frustum horizontally and admit more side rooms;
* - DD/Lod terms: square-root of the distance multipliers -- room COUNT
* grows far slower than distance-squared, so sqrt keeps the bump modest;
* a linear-terms + 3.0-cap variant was tried (Statue's full room set is
* 487 KB = 2.2x its authored pool and starves even 2x) but regressed
* the renderer on the menu-driven path -- see the in-function note;
* capped at 2.0x (worst case `-ma350` level row -> 700 KB, well inside the
* PC's ~6 MB STAGE bank; see D95). NOTE: a live instrumented Statue playtest
* showed 2x still starves under whole-level room churn (the full set is
* 487 KB = 2.2x the authored `-ma220` pool), and the attempted 3x/linear
* fix REGRESSED the renderer on the menu-driven path -- reverted, see the
* in-function note + findings D294. Identity (1.0) when every knob is at its
* N64-faithful value, so all-100 settings keep the exact authored budget.
* Called once per stage load from bossMainloop (src/boss.c).
*
* GE_ROOMPOOL=<float> overrides the computed scale outright (test hook for
* before/after captures; also a user opt-out if the larger pool ever causes
* trouble on a level). */
f32 portRoomPoolScale(void)
{
const char *ov = getenv("GE_ROOMPOOL");
f32 scale = 1.0f;
f32 t;
if (ov && *ov) {
/* Test hook: values < 1.0 shrink the pool BELOW the authored size
* to stress-test the exhaustion path (D294 verification). */
scale = (f32)atof(ov);
if (scale > 2.0f) { scale = 2.0f; }
if (scale < 0.25f) { scale = 0.25f; }
return scale;
}
t = portFovScale;
/* D334: native widescreen widens the horizontal frustum too (via the
* projection aspect instead of the vertical boost), so it admits the same
* extra side rooms and keeps the pool bump. */
if ((cfgWidescreenAuto || cfgNativeWidescreen) && gfx_current_dimensions.aspect_ratio > 0.01f) {
t *= sqrtf(gfx_current_dimensions.aspect_ratio / (4.0f / 3.0f));
}
if (t > scale) { scale = t; }
/* D294 follow-up (2026-09-22): linear terms + a 3.0 cap were tried to
* cover Statue's full 487 KB room set, but that build corrupted model
* anim state on the MENU-driven Statue path (D156 NaN frames; direct
* -level_22 boots stayed clean headless). Reverted to sqrt + 2.0 cap,
* the user-verified working state. Do not re-apply without the capture-bat
* repro data (scratch/d294_capture.bat -> D156 hexdump + GE_D294BANK). */
t = sqrtf(portDrawDistanceMultiplier());
if (t > scale) { scale = t; }
t = sqrtf(1.0f / portLodDistanceMultiplier());
if (t > scale) { scale = t; }
if (scale > 2.0f) { scale = 2.0f; }
if (scale < 1.0f) { scale = 1.0f; }
return scale;
}
PD_CONSTRUCTOR static void videoConfigInit(void)
{
configRegisterFloat("Game.ScreenShakeIntensity", &portScreenShakeScale, 0.0f, 10.0f);
configRegisterInt("Game.SkipIntro", &portSkipIntro, 0, 1);
configRegisterInt("Game.NoHitFlash", &portNoHitFlash, 0, 1);
configRegisterInt("Video.CrosshairHide", &portCrosshairHide, 0, 1); /* v0.4.0 M2 (D373) */
configRegisterInt("Video.CrosshairColor", &cfgCrosshairColor, 0, 8); /* 8 = custom; old ini values unchanged */
configRegisterInt("Video.CrosshairRed", &cfgCrosshairRed, 0, 255);
configRegisterInt("Video.CrosshairGreen", &cfgCrosshairGreen, 0, 255);
configRegisterInt("Video.CrosshairBlue", &cfgCrosshairBlue, 0, 255);
configRegisterInt("Video.CrosshairSize", &cfgCrosshairSize, 50, 200);
configRegisterInt("Video.CrosshairStyle", &cfgCrosshairStyle, 0, 1);
configRegisterInt("Game.AllUnlocked", &portAllUnlocked, 0, 1);
configRegisterInt("Video.VSync", &cfgVSync, 0, 1);
configRegisterInt("Video.FpsCap", &cfgFpsCap, 0, 1000);
configRegisterInt("Video.MSAA", &cfgMSAA, 1, 8);
configRegisterInt("Video.TextureFilter", &cfgTexFilter, 0, 2);
configRegisterInt("Video.FixMipTextures", &cfgFixMipTex, 0, 1);
configRegisterInt("Video.DetailBaseTile", &cfgDetailBaseTile, 0, 1);
configRegisterInt("Video.WrapFix", &cfgWrapFix, 0, 1);
configRegisterInt("Video.FovScale", &cfgFovScale, 50, 150);
configRegisterInt("Video.WidescreenAuto", &cfgWidescreenAuto, 0, 1);
configRegisterInt("Video.NativeWidescreen", &cfgNativeWidescreen, 0, 1); /* D334 */
configRegisterInt("Game.HudScale", &cfgHudScale, 75, 150); /* D226: capped at 150 (user: little benefit above) */
configRegisterInt("Video.DrawDistance", &cfgDrawDistance, 100, 400);
configRegisterInt("Video.DrawDistanceAutoFov", &cfgDrawDistanceAutoFov, 0, 1);
configRegisterInt("Video.LodDistance", &cfgLodDistance, 25, 400);
@@ -262,10 +450,9 @@ PD_CONSTRUCTOR static void videoConfigInit(void)
* first-run path saves these values to disk and every later launch reads the
* file (user changes via F10 win outright); if an ini already exists its
* values overwrite everything here. So this is a first-launch preset only.
* 1280x800 is the Deck's native panel resolution; MSAA 4 + VSync is
* comfortable headroom for the A11 GPU; DrawDistance/LodDistance at 150%
* because the authored N64 fade distances read "things pop in just before
* you can see them" on a sharp 7" close-up panel (e.g. the Dam lock).
* 1280x800 is the Deck's native panel resolution; 2x MSAA + VSync keep
* GPU cost modest. Draw/LOD distances use the standard midpoint (250%)
* to avoid obvious N64-era pop-in on modern displays.
* Note: the panel is 16:10 and the game renders 4:3, so this stretches
* uniformly like any non-4:3 window today (letterboxing is the parked
* WIDESCREEN-FOV-PLAN). */
@@ -275,14 +462,18 @@ void videoApplySteamOSDefaults(void)
cfgWinW = 1280;
cfgWinH = 800;
cfgVSync = 1;
cfgMSAA = 4;
cfgDrawDistance = 150;
cfgLodDistance = 150;
cfgMSAA = 2;
cfgDrawDistance = 250;
cfgLodDistance = 250;
}
/* Set by videoRequestLiveConfig() (F10 overlay, host thread); consumed on the
* scheduler thread in videoStartFrame() where the GL context is bound. */
static volatile int liveCfgDirty = 0;
/* D382: record precisely which live video setting changed. Reticle/FOV/
* frame cap changes must not reset texture state (a shader/cache clear).
* Option writes may arrive on the menu or scheduler thread; apply on the
* render thread with the GL context bound, coalesced per frame. */
enum { VCFG_VSYNC = 1, VCFG_FPS = 2, VCFG_FILTER = 4,
VCFG_FOV = 8, VCFG_ANISO = 16, VCFG_CROP = 32 };
static SDL_atomic_t liveCfgDirty;
/* D211/D212: push the port-only image knobs where they apply. FovScale is a
* plain float the game re-reads each frame; anisotropy goes to fast3d. */
@@ -307,11 +498,23 @@ static void videoApplyTexFilter(void)
}
}
/* Re-apply the live-tunable [Video] knobs (VSync / FpsCap / TextureFilter).
* MSAA and Fullscreen are FBO/window rebuilds -> "(restart)" in the overlay. */
void videoRequestLiveConfig(void)
/* Port-only sprite settings and direct-read world/HUD options are not GL
* state. Keep the heavyweight texture reset only for the two texture knobs. */
void videoRequestLiveConfigForKey(const char *key)
{
liveCfgDirty = 1;
int mask = 0;
if (!strcmp(key, "Video.VSync")) mask = VCFG_VSYNC;
else if (!strcmp(key, "Video.FpsCap")) mask = VCFG_FPS;
else if (!strcmp(key, "Video.TextureFilter")) mask = VCFG_FILTER;
else if (!strcmp(key, "Video.FovScale")) mask = VCFG_FOV;
else if (!strcmp(key, "Video.Anisotropy")) mask = VCFG_ANISO;
else if (!strcmp(key, "Video.SafeAreaCrop")) mask = VCFG_CROP;
if (mask) {
int old;
do {
old = SDL_AtomicGet(&liveCfgDirty);
} while (!SDL_AtomicCAS(&liveCfgDirty, old, old | mask));
}
}
/* --- F10 overlay: window / fullscreen changes, deferred to the host thread ---
@@ -460,6 +663,7 @@ int videoInit(void)
* NOT fix the Depot roof, see docs/BRIEF-B2-depot-textures.md). All keep
* point-sampled tiles (HUD, G_TF_POINT) crisp via the per-tile flag. */
gfx_set_fix_mip_textures(cfgFixMipTex);
gfx_set_detail_base_tile(cfgDetailBaseTile);
gfx_set_wrap_fix(cfgWrapFix);
videoApplyTexFilter();
@@ -482,6 +686,68 @@ int videoInit(void)
return 0;
}
/* ---- D344: orderly quit ------------------------------------------------
* Rendering runs on the scheduler thread with the GL context bound; quit
* events arrive on the host thread (or on fast3d's render-thread pump).
* exit() used to run straight from whichever pump saw the event, tearing the
* process down while the render thread could be inside the NVIDIA driver --
* the likely trigger of the 0x119 VIDEO_SCHEDULER_INTERNAL_ERROR bugchecks
* (findings D344). Now:
* 1. anyone calls videoRequestQuit();
* 2. the render thread, at its next frame boundary, glFinish()es, unbinds
* the context and parks for good (videoRenderPark);
* 3. the host thread waits until the render thread is parked or outside a
* frame (max QUIT_WAIT_MS), then exit(0) (atexit saves config).
* Frame entry sets s_inFrame BEFORE checking s_quitReq, and the host sets
* s_quitReq BEFORE reading s_inFrame (SDL atomics are sequentially
* consistent), so a frame can never start once the host has decided to exit. */
#define QUIT_WAIT_MS 2000
static SDL_atomic_t s_quitReq;
static SDL_atomic_t s_inFrame;
static SDL_atomic_t s_parked;
static int s_quitFrame = -1; /* GE_QUITFRAME: harness self-quit, -1 = unread */
void videoRequestQuit(const char *why)
{
if (SDL_AtomicCAS(&s_quitReq, 0, 1)) {
sysLogPrintf(LOG_INFO, "video: quit requested (%s)", why ? why : "?");
}
}
int videoQuitRequested(void)
{
return SDL_AtomicGet(&s_quitReq) != 0;
}
/* Render thread only. Never returns. */
static void videoRenderPark(void)
{
gfx_sdl_park_for_exit();
SDL_AtomicSet(&s_inFrame, 0);
SDL_AtomicSet(&s_parked, 1);
for (;;) {
sysSleep(100000);
}
}
/* Host thread: exit once the render thread is out of the GL driver. */
static void videoHostExitIfRequested(void)
{
if (!SDL_AtomicGet(&s_quitReq)) {
return;
}
Uint32 start = SDL_GetTicks();
while (!SDL_AtomicGet(&s_parked) && SDL_AtomicGet(&s_inFrame) &&
SDL_GetTicks() - start < QUIT_WAIT_MS) {
SDL_Delay(2);
}
sysLogPrintf(LOG_INFO, "video: exiting (render %s after %u ms)",
SDL_AtomicGet(&s_parked) ? "parked"
: (SDL_AtomicGet(&s_inFrame) ? "STILL IN FRAME (timeout)" : "idle"),
(unsigned)(SDL_GetTicks() - start));
exit(0);
}
void videoDestroy(void)
{
if (initDone) {
@@ -495,19 +761,27 @@ void videoStartFrame(void)
if (!initDone) {
return;
}
/* D344: enter the frame first, then check for a quit (see above). */
SDL_AtomicSet(&s_inFrame, 1);
if (SDL_AtomicGet(&s_quitReq)) {
videoRenderPark();
}
/* 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();
videoApplyImageOptions();
sysLogPrintf(LOG_INFO, "video: live config applied "
int dirty = SDL_AtomicSet(&liveCfgDirty, 0);
if (dirty) {
if (dirty & VCFG_VSYNC) wmAPI->set_swap_interval(cfgVSync ? 1 : 0);
if (dirty & VCFG_FPS) gfx_set_target_fps(cfgFpsCap);
if (dirty & VCFG_FILTER) videoApplyTexFilter();
if (dirty & VCFG_FOV) portFovScale = (f32)cfgFovScale / 100.0f;
if (dirty & VCFG_ANISO) gfx_set_anisotropy_level(cfgAniso);
if (dirty & VCFG_CROP) gfx_set_safe_area_crop(cfgSafeAreaCrop);
sysLogPrintf(LOG_INFO, "video: live config applied mask=%02x "
"(vsync=%d fpscap=%d texfilter=%d fov=%d aniso=%d)",
cfgVSync, cfgFpsCap, cfgTexFilter, cfgFovScale, cfgAniso);
dirty, cfgVSync, cfgFpsCap, cfgTexFilter, cfgFovScale, cfgAniso);
}
gfx_start_frame();
@@ -532,15 +806,19 @@ void videoPumpEvents(void)
/* Apply any window/fullscreen change the F10 overlay posted from the
* scheduler thread (must run here, on the window's creating thread). */
videoDrainWindowRequests();
inputApplyMouseRequests(); /* D287: mouse mode/cursor, same rule */
videoHostExitIfRequested(); /* D344: quit posted last pump or by another thread */
SDL_Event ev;
while (SDL_PollEvent(&ev)) {
switch (ev.type) {
case SDL_QUIT:
sysLogPrintf(LOG_INFO, "video: quit requested");
exit(0);
videoRequestQuit("quit event");
break;
case SDL_KEYDOWN:
/* D383: host owns SDL key events; the capture modal consumes the
* next scancode before F10/ESC can close the UI or navigate. */
if (optionsBindingKeyDown(&ev.key)) break;
/* D145: bare ESC used to exit(0). On the front-end / debrief
* screens ESC is the natural "back" key, so a player pressing it
* to page back instead quit the whole game (looked like a crash --
@@ -548,32 +826,37 @@ void videoPumpEvents(void)
* (back / cancel) via input.c; quitting is window-close (the X) or
* Alt+F4 only. */
if ((ev.key.keysym.sym == SDLK_F4) && (ev.key.keysym.mod & KMOD_ALT)) {
sysLogPrintf(LOG_INFO, "video: Alt+F4 -> quit");
exit(0);
videoRequestQuit("Alt+F4");
} 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 */
/* F10: port-layer options overlay -- not on the PC options
* screen (D343: one options UI at a time). */
if (optionsOverlayIsOpen() || !frontOptionsBlocksOverlay()) {
optionsOverlayToggle();
}
} else if (ev.key.keysym.sym == SDLK_ESCAPE && !ev.key.repeat) {
/* Overlay open: ESC closes it (and is swallowed). Otherwise
/* Overlay open: ESC backs out of a category, then closes (swallowed). Otherwise
* WI-1: in click-to-lock mode ESC frees the captured cursor
* (and is swallowed); else it falls through to input.c where
* it feeds the N64 B button (D145). */
if (optionsOverlayIsOpen()) {
optionsOverlayToggle();
optionsOverlayBack();
} else {
inputReleaseCapture();
}
}
break;
case SDL_MOUSEBUTTONDOWN:
if (optionsBindingMouseDown(&ev.button)) break;
/* WI-1: a click in the window (re)locks the cursor in
* click-to-lock mode; a no-op otherwise. */
if (!optionsOverlayIsOpen()) {
if (!optionsOverlayIsOpen() && !optionsBindingCaptureActive()) {
inputNotifyClick();
}
break;
case SDL_MOUSEWHEEL:
if (optionsBindingCaptureActive()) break;
if (optionsOverlayIsOpen()) {
optionsOverlayScroll(ev.wheel.y); /* move the selection */
} else {
@@ -586,8 +869,7 @@ void videoPumpEvents(void)
break;
case SDL_WINDOWEVENT:
if (ev.window.event == SDL_WINDOWEVENT_CLOSE) {
sysLogPrintf(LOG_INFO, "video: window closed");
exit(0);
videoRequestQuit("window closed");
} else if (ev.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
gfx_sdl_update_cached_size();
} else if (ev.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
@@ -612,6 +894,9 @@ void videoPumpEvents(void)
wmAPI->set_window_title(title);
}
}
/* D287: apply anything the events above (click-to-lock, focus) queued. */
inputApplyMouseRequests();
}
void videoSubmitCommands(Gfx *cmds)
@@ -651,6 +936,8 @@ static void videoPreSwapCapture(void)
if (screenshotReq) {
screenshotReq = 0;
extern void gfxD157Burst(void); /* D252 TEMP, inert without GE_D157 */
gfxD157Burst();
static int shotNum = 0;
char path[128];
GE_MKDIR("ppm");
@@ -672,6 +959,21 @@ void videoEndFrame(void)
}
gfx_end_frame();
/* D344: the frame (and its swap) is done. Park here on a quit request, so
* the host never exits under an in-flight frame. GE_QUITFRAME=<n> is the
* harness's clean self-quit (replaces `timeout` kills). */
if (s_quitFrame == -1) {
const char *q = getenv("GE_QUITFRAME");
s_quitFrame = (q && atoi(q) > 0) ? atoi(q) : 0;
}
if (s_quitFrame > 0 && frames + 1 >= (u32)s_quitFrame) {
videoRequestQuit("GE_QUITFRAME");
}
if (SDL_AtomicGet(&s_quitReq)) {
videoRenderPark();
}
SDL_AtomicSet(&s_inFrame, 0);
++frames;
++fpsNumFrames;
@@ -732,6 +1034,7 @@ void videoUpdateNativeResolution(s32 w, s32 h)
gfx_current_native_aspect = (float)w / (float)h;
}
u32 videoGetFrameCount(void) { return frames; }
s32 videoGetNativeWidth(void) { return gfx_current_native_viewport.width; }
s32 videoGetNativeHeight(void) { return gfx_current_native_viewport.height; }
+642
View File
@@ -0,0 +1,642 @@
/* D349: PC UI reads GE watch settings; PC-UI edits persist per Bond file.
* The original watch remains session-only. Never call GE setters/save routines
* from the F10 input/render thread. This file is port-only. */
#include <string.h>
#include <SDL.h>
#include <ultra64.h>
#include <bondconstants.h>
#include "file.h"
#include "file2.h"
#include "front.h"
#include "options.h"
#include "player.h"
#include "system.h"
#include "watchsettings.h"
#include "optionsoverlay.h" /* D356 GE_WSPROBE_RESET probe hooks */
extern s32 g_StageNum; /* boss.c: direct -level_XX starts with MENU_INVALID */
extern save_data *fileGetSaveForFoldernum(u32 folder);
extern void fileSaveSettingsForFolder(save_data *save);
extern void fileWriteSave(save_data *save);
extern s32 fileGamePakProbe(void);
extern void fileGenerateCRC(u8 *start, u8 *end, save_data *result);
extern void sub_GAME_7F0A91A0(u16 volume);
extern void musicTrack1ApplySeqpVol(u16 volume);
extern u16 musicTrack1GetVolume(void);
extern u16 get_mTrack2Vol(void);
extern void fileBuildWriteNewSave(u32 folder);
extern void set_cur_player_look_vertical_inverted(u32 value);
extern void cur_player_set_aim_control(u32 value);
extern void cur_player_set_sight_onscreen_control(u32 value);
extern void cur_player_set_ammo_onscreen_setting(u32 value);
static int frontFolder(void); /* defined below; the lazy file default */
static int stageActive(void); /* defined below; the choose-file guard uses it */
/* D354: env-gated headless probe for the F10 queue->apply->persist path
* (same pattern as optionsoverlay's GE_D314). GE_WSPROBE=<tick> makes the
* <tick>-th active stage tick queue an F10-style commit of Music+FX to a
* known value, with before/after live+saved logs so a stage run with no
* keyboard/mouse can prove the bridge applies and persists. */
static int wsProbeTick = -1;
static int wsProbeTickN = 0;
static int wsProbePhase = 0; /* 0=armed, 1=queued (log pre), 2=done (log post) */
/* D355: per-activation flag for the level-start BGM resync (reset whenever the
* stage is not active, so the next activation resyncs again -- lv.c:364
* re-pins the BGM player to VOLUME_MAX on every level start). */
static int wsStageWasActive = 0;
static const char *const keys[WATCH_SETTING_COUNT] = {
"Bond.Music", "Bond.FX", "Bond.Look", "Bond.AutoAim",
"Bond.AimControl", "Bond.Sight", "Bond.LookAhead", "Bond.Ammo"
};
static const u16 bits[WATCH_SETTING_COUNT] = {
0, 0, OPTION_INVERTLOOK, OPTION_AUTOAIM, OPTION_AIMCONTROL,
OPTION_SIGHTONSCREEN, OPTION_LOOKAHEAD, OPTION_DISPLAYAMMO
};
/* Explicit chooser is separate from selected_folder_num (which frontoptions
* assigns FOLDER1 merely to draw its dossier background). -1 means none
* (unresolved; frontFolder() lazily defaults it). D352: guarded by uiLock;
* D354: also read/written by F10 front-end contexts (input thread), which
* is why the lock exists. */
static int chosen = -1;
static int staged[WATCH_SETTING_COUNT];
static int stagedField = -1, stagedFolder = -1;
static SDL_SpinLock uiLock;
/* Bounded producer (F10 controller poll) -> consumer (game thread) queues.
* Snapshot is also guarded; F10 input/render never call GE save routines. */
struct Command { int folder, field, value, commit; };
#define CMD_CAP 128
static struct Command cmds[CMD_CAP];
static int head, count;
/* Front-end F10 also runs on the controller poll thread. fileWriteSave()
* calls joyDisablePoll() and waits for that SAME thread to acknowledge it:
* writing here deadlocks. Keep front commits separate from stage commands;
* the game-thread tick drains them without touching the controller thread. */
static struct Command frontCmds[CMD_CAP];
static int frontCount;
static int snapshot[WATCH_SETTING_COUNT];
static int snapFolder = -1;
static SDL_SpinLock lock;
int watchSettingsFieldForKey(const char *key)
{
for (int i = 0; i < WATCH_SETTING_COUNT; i++)
if (strcmp(keys[i], key) == 0) return i;
return -1;
}
/* GE_STARTMENU skips fileValidateSaves: zeroed saves[] looks like a valid
* folder-0 save to fileGetSaveForFoldernum. Never let a PC UI write that
* phantom slot (D299). Check the actual CRC before treating it as a file. */
static save_data *validSave(int folder)
{
if (folder < FOLDER1 || folder >= MAX_FOLDER_COUNT) return NULL;
save_data *save = fileGetSaveForFoldernum((u32)folder);
if (!save) return NULL;
save_data crc = {0};
fileGenerateCRC((u8 *)&save->completion_bitflags, (u8 *)(save + 1), &crc);
return crc.chksum1 == save->chksum1 && crc.chksum2 == save->chksum2
? save : NULL;
}
int watchSettingsFolder(void)
{
SDL_AtomicLock(&uiLock);
int f = chosen;
SDL_AtomicUnlock(&uiLock);
return f;
}
/* D352: game-thread only. The chooser is a front-end control (options
* screen); in-stage F10 targets the active file, so a stage-context call
* signals a broken UI invariant -- refuse it loudly rather than race the
* front screen's readers.
* D356: "none" retired from the cycle (plan §5.5) -- the front target is
* always a folder: frontFolder() resolves a -1 chosen lazily to a valid
* folder (D354) and the auto-init fallback builds file 1 if none exists,
* so the UI shows an unavailable state ("(no file)" annotation, via
* watchSettingsActiveFolder) instead of a number the write path cannot
* use. */
void watchSettingsChooseFile(int dir)
{
if (stageActive()) {
sysLogPrintf(LOG_WARNING, "watchsettings: choose-file in a stage context; ignored");
return;
}
/* Skip empty slots. Do not load the file here: GE's loader would change
* front-end audio. */
SDL_AtomicLock(&uiLock);
int next = (chosen < FOLDER1) ? FOLDER1 : chosen;
for (int n = 0; n < MAX_FOLDER_COUNT; n++) {
next += dir;
if (next < FOLDER1) next = MAX_FOLDER_COUNT - 1;
if (next >= MAX_FOLDER_COUNT) next = FOLDER1;
if (validSave(next)) break;
}
chosen = next;
stagedField = -1;
int sel = chosen;
SDL_AtomicUnlock(&uiLock);
sysLogPrintf(LOG_INFO, "watchsettings: selected save file %d", sel + 1);
}
/* D356: context-aware, read-only (never mutates the stored front target):
* in stage -> the stage's active file; front contexts -> the resolved front
* target (frontFolder()'s lazy default). -1 when nothing usable (the
* auto-init file not yet applied) -- the UIs render that as an unavailable
* state ("(no file)"), never a file number the write path cannot use.
* NOTE: frontFolder() acquires uiLock itself -- never call it (or this)
* while uiLock is held: it is a plain spinlock, not re-entrant. */
int watchSettingsActiveFolder(void)
{
int f;
if (stageActive()) {
SDL_AtomicLock(&uiLock);
f = selected_folder_num;
SDL_AtomicUnlock(&uiLock);
} else {
f = frontFolder(); /* self-locks; may default + store 'chosen' */
}
return validSave(f) ? f : -1;
}
/* D356: stage commands awaiting the game thread (front F10 uses a separate
* queue). Drives the in-stage reset probe's
* dispatch check (plan §5.8). */
int watchSettingsQueueCount(void)
{
SDL_AtomicLock(&lock);
int n = count;
SDL_AtomicUnlock(&lock);
return n;
}
/* D356: the per-file "default" for the reset rows -- BLANKSAVEDATA's watch
* fields (file2.c's blank save is a macro, not a global; only the music,
* sfx and options fields matter here: full 0xFF volumes, DEFAULT_OPTIONS).
* completion_bitflags is left 0 (BLANKSAVEDATA sets BOND_BROSNAN; the reset
* rows never touch it). */
static const save_data kBlankSave = {
0, 0,
0, 0, /* completion_bitflags, flag_007 */
0xFF, 0xFF, /* music_vol, sfx_vol (expanded -> VOLUME_MAX) */
DEFAULT_OPTIONS, /* options */
0, 0, 0,
};
int watchSettingsBlankValue(enum WatchSettingField field)
{
if ((unsigned)field >= WATCH_SETTING_COUNT) return 0;
if (field == WATCH_SETTING_MUSIC) return ((int)kBlankSave.music_vol << 7) | (kBlankSave.music_vol >> 1);
if (field == WATCH_SETTING_FX) return ((int)kBlankSave.sfx_vol << 7) | (kBlankSave.sfx_vol >> 1);
return (kBlankSave.options & bits[field]) ? 1 : 0;
}
static int stageActive(void)
{
/* Direct -level_XX boots never visit the front-end RUN_STAGE state. */
return current_menu == MENU_RUN_STAGE ||
(current_menu == MENU_INVALID && g_StageNum != LEVELID_TITLE);
}
/* D354: lazy default target for front-end (non-stage) contexts. The chooser
* used to start at -1 ("none"), which disabled every Bond row -- and in F10
* the chooser row is hidden, so on the front end (file select) the rows could
* never be enabled at all: the volume sliders read "Select file" and did
* nothing. Resolution order: an explicit "Edit file" choice > the folder the
* player is viewing (selected_folder_num, set by file select) > the first
* valid folder. The first two are pure reads of stable menu state, safe from
* the F10 input thread (D350 gate: no GE writes from it; only chosen is
* written, under uiLock). A fresh PC eeprom is booted with a BLANKSAVEDATA
* slot per folder (libultra.c D259/D281 patch), so a default almost always
* resolves; the game-thread fallback in watchSettingsGameTick builds one if
* it does not. */
static int frontFolder(void)
{
SDL_AtomicLock(&uiLock);
int f = chosen;
if (f < FOLDER1) {
if (validSave(selected_folder_num))
f = chosen = selected_folder_num;
else
for (int i = FOLDER1; i < MAX_FOLDER_COUNT; i++)
if (validSave(i)) { f = chosen = i; break; }
}
SDL_AtomicUnlock(&uiLock);
return f;
}
int watchSettingsAvailable(void)
{
if (!stageActive()) {
/* Front end: MENU_PC_OPTIONS, or F10 over file select / title.
* D354: F10 front contexts were mis-routed through the stage path
* (and returned "unavailable"), disabling every Bond row. */
return validSave(frontFolder()) != NULL;
}
int valid;
SDL_AtomicLock(&lock);
valid = snapFolder >= FOLDER1 && snapFolder < MAX_FOLDER_COUNT;
SDL_AtomicUnlock(&lock);
return valid;
}
static int savedValue(const save_data *save, enum WatchSettingField field)
{
if (field == WATCH_SETTING_MUSIC) return ((int)save->music_vol << 7) | (save->music_vol >> 1);
if (field == WATCH_SETTING_FX) return ((int)save->sfx_vol << 7) | (save->sfx_vol >> 1);
return (save->options & bits[field]) ? 1 : 0;
}
int watchSettingsRead(enum WatchSettingField field)
{
if ((unsigned)field >= WATCH_SETTING_COUNT) return 0;
if (!stageActive()) {
int f = frontFolder(), stagedVal = -1;
SDL_AtomicLock(&uiLock);
if (stagedField == (int)field && stagedFolder == f)
stagedVal = staged[field];
SDL_AtomicUnlock(&uiLock);
if (stagedVal >= 0) return stagedVal;
save_data *save = validSave(f);
return save ? savedValue(save, field) : 0;
}
int value = 0;
SDL_AtomicLock(&lock);
if (snapFolder >= FOLDER1 && snapFolder < MAX_FOLDER_COUNT)
value = snapshot[field];
SDL_AtomicUnlock(&lock);
return value;
}
/* D352: game-thread only (watchSettingsGameTick, after the setter was
* applied). temp serialization must NOT overwrite unrelated, watch-only
* edits or the port-forced controller type in the Bond file. */
static int watchSettingsPersistField(int folder, enum WatchSettingField field)
{
if (folder < FOLDER1 || folder >= MAX_FOLDER_COUNT ||
(unsigned)field >= WATCH_SETTING_COUNT) return 0;
if (!g_CurrentPlayer) {
sysLogPrintf(LOG_WARNING, "watchsettings: persist field %d skipped: no current player", field);
return 0;
}
save_data *save = validSave(folder);
if (!save) {
sysLogPrintf(LOG_WARNING, "watchsettings: persist field %d in Bond file %d: no valid save (CRC?)", field, folder + 1);
return 0;
}
if (!fileGamePakProbe()) {
sysLogPrintf(LOG_WARNING, "watchsettings: persist field %d in Bond file %d: EEPROM absent, edit not saved", field, folder + 1);
return 0;
}
save_data serialized = *save;
fileSaveSettingsForFolder(&serialized);
if (field == WATCH_SETTING_MUSIC) {
if (save->music_vol == serialized.music_vol) return 1;
save->music_vol = serialized.music_vol;
} else if (field == WATCH_SETTING_FX) {
if (save->sfx_vol == serialized.sfx_vol) return 1;
save->sfx_vol = serialized.sfx_vol;
} else {
u16 mask = bits[field];
u16 merged = (save->options & (u16)~mask) | (serialized.options & mask);
if (save->options == merged) return 1;
save->options = merged;
}
fileWriteSave(save); /* real saves[] slot; writes CRC + EEPROM */
sysLogPrintf(LOG_INFO, "watchsettings: persisted field %d in Bond file %d", field, folder + 1);
return 1;
}
/* Front end has no active GE watch/player settings. Edit the explicitly
* chosen save field directly instead of calling fileLoadSettingsForFolder
* (which also applies the chosen file's audio volume to the front end). */
static void saveFrontField(int folder, enum WatchSettingField field, int value)
{
save_data *save = validSave(folder);
if (!save) {
sysLogPrintf(LOG_WARNING, "watchsettings: save field %d in Bond file %d: no valid save (CRC?)", field, folder + 1);
return;
}
if (!fileGamePakProbe()) {
sysLogPrintf(LOG_WARNING, "watchsettings: save field %d in Bond file %d: EEPROM absent, edit not saved", field, folder + 1);
return;
}
if (value < 0) value = 0;
if (field == WATCH_SETTING_MUSIC || field == WATCH_SETTING_FX) {
if (value > 32767) value = 32767;
u8 raw = (u8)(value >> 7);
u8 *dest = field == WATCH_SETTING_MUSIC ? &save->music_vol : &save->sfx_vol;
if (*dest == raw) return;
*dest = raw;
} else {
u16 mask = bits[field];
u16 merged = (save->options & (u16)~mask) | (value ? mask : 0);
if (save->options == merged) return;
save->options = merged;
}
fileWriteSave(save);
sysLogPrintf(LOG_INFO, "watchsettings: persisted field %d in Bond file %d", field, folder + 1);
}
void watchSettingsSet(enum WatchSettingField field, int value, int commit)
{
if ((unsigned)field >= WATCH_SETTING_COUNT || !watchSettingsAvailable()) return;
if (!stageActive()) {
int f = frontFolder();
SDL_AtomicLock(&uiLock);
if (!optionsOverlayIsOpen()) {
/* The front options screen and dev probes run on the game thread. */
if (commit) stagedField = -1;
else { stagedField = field; stagedFolder = f; staged[field] = value; }
SDL_AtomicUnlock(&uiLock);
if (commit) saveFrontField(f, field, value);
return;
}
/* F10 is polled from joyPoll() via osContStartReadData. Even a
* fileGamePakProbe here deadlocks: joyDisablePoll waits for joyPoll's
* acknowledgement. Stage the value for UI feedback and enqueue ONLY
* under a short lock; the game thread does the EEPROM I/O. */
stagedField = field; stagedFolder = f; staged[field] = value;
SDL_AtomicUnlock(&uiLock);
SDL_AtomicLock(&lock);
int i;
for (i = 0; i < frontCount; i++) {
if (frontCmds[i].folder == f && frontCmds[i].field == (int)field) break;
}
if (i == frontCount && frontCount >= CMD_CAP) {
SDL_AtomicUnlock(&lock);
sysLogPrintf(LOG_WARNING, "watchsettings: front command queue full; edit not applied");
return;
}
if (i == frontCount) {
frontCount++;
frontCmds[i].commit = 0;
}
frontCmds[i].folder = f;
frontCmds[i].field = field;
frontCmds[i].value = value;
frontCmds[i].commit = commit || frontCmds[i].commit;
SDL_AtomicUnlock(&lock);
return;
}
SDL_AtomicLock(&lock);
/* D352: coalesce to one pending command per field. A fast slider drag
* enqueues many detents per frame; the game thread then applies only
* the latest value (commit = OR of the pending commits), so the audio
* setters fire once per frame instead of once per detent. */
for (int i = 0; i < count; i++) {
struct Command *c = &cmds[(head + i) % CMD_CAP];
if (c->field == (int)field) {
c->value = value;
c->commit |= commit;
snapshot[field] = value;
SDL_AtomicUnlock(&lock);
return;
}
}
struct Command cmd = { snapFolder, field, value, commit };
if (count < CMD_CAP) {
cmds[(head + count++) % CMD_CAP] = cmd;
snapshot[field] = value; /* immediate UI feedback for held adjustments */
} else {
sysLogPrintf(LOG_WARNING, "watchsettings: command queue full; edit not applied");
}
SDL_AtomicUnlock(&lock);
}
void watchSettingsCommit(enum WatchSettingField field)
{
if ((unsigned)field >= WATCH_SETTING_COUNT) return;
if (!stageActive()) {
/* D352: a commit is meaningful only if a drag staged an edit; a
* plain click with no file selected stays a silent no-op. */
int f = frontFolder(), pending;
SDL_AtomicLock(&uiLock);
pending = stagedField == (int)field && stagedFolder == f && f >= FOLDER1;
SDL_AtomicUnlock(&uiLock);
if (!pending) return;
} else if (!watchSettingsAvailable()) {
/* D352: the UI showed the new value (snapshot) but the stage is not
* active, so the persist cannot run -- make the drop visible. */
sysLogPrintf(LOG_WARNING, "watchsettings: commit dropped, no active Bond target (field %d)", field);
return;
}
watchSettingsSet(field, watchSettingsRead(field), 1);
}
static int liveValue(enum WatchSettingField field)
{
switch (field) {
case WATCH_SETTING_MUSIC: return get_mTrack2Vol();
case WATCH_SETTING_FX: return call_sndGetSfxSlotFirstNaturalVolume();
case WATCH_SETTING_LOOK: return get_cur_player_look_vertical_inverted();
case WATCH_SETTING_AUTOAIM: return cur_player_get_autoaim();
case WATCH_SETTING_AIMCONTROL: return cur_player_get_aim_control();
case WATCH_SETTING_SIGHT: return cur_player_get_sight_onscreen_control();
case WATCH_SETTING_LOOKAHEAD: return cur_player_get_lookahead();
case WATCH_SETTING_AMMO: return cur_player_get_ammo_onscreen_setting();
default: return 0;
}
}
static void applyValue(enum WatchSettingField field, int value)
{
if (value < 0) value = 0;
if (field <= WATCH_SETTING_FX && value > 32767) value = 32767;
else if (field > WATCH_SETTING_FX) value = !!value;
switch (field) {
case WATCH_SETTING_MUSIC:
/* D355: the slider's value drives the X-track (track 2) via the game's
* own setter, but in-stage BGM is track 1, which the game pins to
* VOLUME_MAX at level start (lv.c:364) and never rescales from
* mTrack2Vol in solo -- so the N64 watch (and the F10 before this)
* could not change the music a player was actually hearing. The MP
* path already does exactly this resync (mpmenu.c:354 feeds
* get_mTrack2Vol() to musicTrack1ApplySeqpVol); mirror it here so a
* F10/options Music change is audible on the BGM player in real time
* (FX already was). Port-side only; the N64 watch path is untouched. */
set_mTrack2Vol((u16)value);
musicTrack1ApplySeqpVol((u16)value);
break;
case WATCH_SETTING_FX: sub_GAME_7F0A91A0((u16)value); break;
case WATCH_SETTING_LOOK: set_cur_player_look_vertical_inverted(value); break;
case WATCH_SETTING_AUTOAIM: cur_player_set_autoaim(value); break;
case WATCH_SETTING_AIMCONTROL: cur_player_set_aim_control(value); break;
case WATCH_SETTING_SIGHT: cur_player_set_sight_onscreen_control(value); break;
case WATCH_SETTING_LOOKAHEAD: cur_player_set_lookahead(value); break;
case WATCH_SETTING_AMMO: cur_player_set_ammo_onscreen_setting(value); break;
default: break;
}
}
void watchSettingsGameTick(void)
{
/* Drain F10 front-end commits on the game thread. This MUST run before
* the early return for non-stage contexts below. Never hold the queue
* lock through fileWriteSave/joyDisablePoll: joyPoll must stay runnable. */
struct Command frontBatch[CMD_CAP];
int nf;
SDL_AtomicLock(&lock);
nf = frontCount;
memcpy(frontBatch, frontCmds, (size_t)nf * sizeof(frontBatch[0]));
frontCount = 0;
SDL_AtomicUnlock(&lock);
for (int i = 0; i < nf; i++) {
struct Command *c = &frontBatch[i];
if (c->commit) saveFrontField(c->folder, (enum WatchSettingField)c->field, c->value);
SDL_AtomicLock(&uiLock);
if (stagedFolder == c->folder && stagedField == c->field &&
staged[c->field] == c->value && c->commit)
stagedField = -1;
SDL_AtomicUnlock(&uiLock);
}
/* D356: the GE_WSPROBE_RESET env gate drives the real reset UI dispatch
* (arm -> confirm -> handler, no bypass) through optionsoverlay.c's
* probe hooks. Each phase waits for its context to be stable (~120
* ticks, a couple of seconds, after the front title has been rendering /
* after stage start) so the probe's save-file I/O never stalls the boot
* black frame. Phase 0 = the pre-stage front probe (dispatch + verify
* directly); once a stage is active, phase 1 = dispatch (arms+confirms
* all sections, queueing the file rows) and phase 2 = verify (after the
* game thread has drained + persisted them). */
{
static int wsResetProbePhase = 0;
static int wsProbeFrontTicks = 0, wsProbeStageTicks = 0;
if (getenv("GE_WSPROBE_RESET")) {
if (!stageActive()) {
if (wsResetProbePhase == 0 && ++wsProbeFrontTicks == 120) {
optionsResetProbePrepare();
wsResetProbePhase = 1;
}
} else {
/* A direct -level_XX boot enters the stage long before the
* front counter would reach 120, so a stage run starts its
* own timer from phase 0 (the front prepare is front-only;
* the stage dispatch builds its section list lazily). */
if (wsResetProbePhase == 0) wsResetProbePhase = 1;
if (wsResetProbePhase == 1 && ++wsProbeStageTicks == 120) {
optionsResetProbeDispatchStage();
wsResetProbePhase = 2;
} else if (wsResetProbePhase == 2 && ++wsProbeStageTicks == 121) {
optionsResetProbeVerifyStage();
wsResetProbePhase = 3;
}
}
}
}
/* This hook runs on the game thread after a gfxFrameMsgQ receive, even
* during load. D352: HOLD pending commands (do not drain) until a stage
* is active -- a slider-release commit must not be silently discarded
* during a level/menu transition. Each command's folder is re-checked
* against selected_folder_num at apply time, so a stale cross-file edit
* is still dropped, never misapplied. */
if (!stageActive() || !g_CurrentPlayer) {
wsStageWasActive = 0;
/* D354: one-shot fallback -- if not a single folder holds a valid
* save (wiped/corrupt eeprom), build the game's own blank save for
* file 1 (game thread, so the EEPROM write is on the owning thread)
* and default to it. A fresh PC eeprom never reaches this: boot
* patches in a BLANKSAVEDATA slot per folder (libultra.c D259/D281).
* frontFolder() re-checks, so an explicit chooser pick is respected. */
if (!stageActive() && frontFolder() < FOLDER1) {
static int wsAutoInitTried = 0;
if (!wsAutoInitTried) {
wsAutoInitTried = 1;
sysLogPrintf(LOG_INFO, "watchsettings: no valid Bond save in any folder; building a blank file 1 (fileBuildWriteNewSave)");
fileBuildWriteNewSave(FOLDER1);
SDL_AtomicLock(&uiLock);
if (validSave(FOLDER1))
chosen = FOLDER1;
SDL_AtomicUnlock(&uiLock);
sysLogPrintf(validSave(FOLDER1) ? LOG_INFO : LOG_WARNING,
"watchsettings: blank file 1 %s", validSave(FOLDER1) ? "created" : "unavailable (no free slot?)");
}
}
/* D354 probe: GE_WSPROBE_FRONT=<tick> exercises the front-end
* (chooser-default) write path the same way F10-on-file-select
* would, headlessly. */
{
static int wsFrontProbe = -1, wsFrontTickN = 0, wsFrontPhase = 0;
if (wsFrontProbe < 0) {
const char *e = getenv("GE_WSPROBE_FRONT");
wsFrontProbe = (e && atoi(e) > 0) ? atoi(e) : 2147483647;
}
if (wsFrontPhase == 0 && wsFrontTickN++ == (size_t)wsFrontProbe) {
wsFrontPhase = 1;
int f = frontFolder();
save_data *save = validSave(f);
sysLogPrintf(LOG_INFO, "wsfront: folder=%d save.music=%d; committing front-end Music=4096",
f, save ? (int)save->music_vol : -1);
watchSettingsSet(WATCH_SETTING_MUSIC, 4096, 1);
} else if (wsFrontPhase == 1 && wsFrontTickN++ == (size_t)wsFrontProbe + 1) {
wsFrontPhase = 2;
int f = frontFolder();
save_data *s2 = validSave(f);
sysLogPrintf(LOG_INFO, "wsfront: post save.music=%d (folder %d)",
s2 ? (int)s2->music_vol : -1, f);
}
}
SDL_AtomicLock(&lock);
snapFolder = -1;
SDL_AtomicUnlock(&lock);
return;
}
/* D355: level start pins the BGM player to VOLUME_MAX (lv.c:364) before
* init_watch re-applies the saved music value to the X track; resync the
* BGM (track 1) to the same live value once per activation so the player's
* music setting survives level starts, not just live F10 edits. */
if (!wsStageWasActive && musicTrack1GetVolume() != get_mTrack2Vol())
musicTrack1ApplySeqpVol(get_mTrack2Vol());
wsStageWasActive = 1;
struct Command batch[CMD_CAP];
int n;
SDL_AtomicLock(&lock);
n = count;
for (int i = 0; i < n; i++) batch[i] = cmds[(head + i) % CMD_CAP];
head = (head + n) % CMD_CAP;
count = 0;
SDL_AtomicUnlock(&lock);
for (int i = 0; i < n; i++) {
struct Command *c = &batch[i];
if (c->folder != selected_folder_num ||
(unsigned)c->field >= WATCH_SETTING_COUNT ||
!validSave(c->folder)) continue;
applyValue((enum WatchSettingField)c->field, c->value);
if (c->commit) watchSettingsPersistField(c->folder, (enum WatchSettingField)c->field);
}
/* D354 probe: log before/after the queued commit, one tick apart. */
if (wsProbeTick < 0) {
const char *e = getenv("GE_WSPROBE");
wsProbeTick = (e && atoi(e) > 0) ? atoi(e) : 2147483647;
}
if (wsProbePhase == 0 && wsProbeTickN++ == (size_t)wsProbeTick) {
wsProbePhase = 1;
save_data *save = validSave(selected_folder_num);
sysLogPrintf(LOG_INFO, "wsprobe: pre music=%d fx=%d t1=%d save.music=%d save.fx=%d (folder %d, %d queued)",
get_mTrack2Vol(), call_sndGetSfxSlotFirstNaturalVolume(), musicTrack1GetVolume(),
save ? (int)save->music_vol : -1, save ? (int)save->sfx_vol : -1,
(int)selected_folder_num, n);
watchSettingsSet(WATCH_SETTING_MUSIC, 4096, 1);
watchSettingsSet(WATCH_SETTING_FX, 4096, 1);
} else if (wsProbePhase == 1 && wsProbeTickN++ == (size_t)wsProbeTick + 1) {
wsProbePhase = 2;
save_data *save = validSave(selected_folder_num);
sysLogPrintf(LOG_INFO, "wsprobe: post music=%d fx=%d t1=%d save.music=%d save.fx=%d",
get_mTrack2Vol(), call_sndGetSfxSlotFirstNaturalVolume(), musicTrack1GetVolume(),
save ? (int)save->music_vol : -1, save ? (int)save->sfx_vol : -1);
}
int values[WATCH_SETTING_COUNT];
for (int f = 0; f < WATCH_SETTING_COUNT; f++)
values[f] = liveValue((enum WatchSettingField)f);
SDL_AtomicLock(&lock);
for (int f = 0; f < WATCH_SETTING_COUNT; f++) snapshot[f] = values[f];
snapFolder = selected_folder_num;
SDL_AtomicUnlock(&lock);
}
+5 -2
View File
@@ -10,6 +10,9 @@
#if defined(__x86_64__)
#include <stdio.h>
#include <stdlib.h>
#ifdef PORT
#include "envflag.h" /* cached getenv for hot-path probes */
#endif
#endif
#endif
@@ -664,7 +667,7 @@ s32 amDmaCallback(s32 addr, s32 len, void* state)
dmaPtr->lastFrame = (s32) g_AudioFrameCount;
freeBuffer = (dmaPtr->ptr + addr) - dmaPtr->startAddr;
#if defined(__x86_64__)
if (getenv("GE_MIXERTRACE")) {
if (GE_ENVFLAG("GE_MIXERTRACE")) {
static FILE *dtf2 = NULL;
if (!dtf2) { dtf2 = fopen("mixertrace.log", "a"); if (dtf2) setvbuf(dtf2, NULL, _IONBF, 0); }
if (dtf2) fprintf(dtf2, "[DMAHIT] addr=0x%08x len=%d bufStart=0x%08x bufPtr=%p ret=%p\n",
@@ -732,7 +735,7 @@ s32 amDmaCallback(s32 addr, s32 len, void* state)
osPiStartDma(&g_DmaIOMessageBuffer[g_NextDMa++], OS_MESG_PRI_HIGH, OS_READ, (u32)addr, freeBuffer, AUDIO_DMA_MAX_BUFFER_LENGTH, &g_DmaMessageQueue);
#if defined(__x86_64__)
if (getenv("GE_MIXERTRACE")) {
if (GE_ENVFLAG("GE_MIXERTRACE")) {
static FILE *dtf3 = NULL;
if (!dtf3) { dtf3 = fopen("mixertrace.log", "a"); if (dtf3) setvbuf(dtf3, NULL, _IONBF, 0); }
if (dtf3) fprintf(dtf3, "[DMAMISS] addr=0x%08x len=%d bufPtr=%p ret=0x%08x\n",
+3
View File
@@ -1865,6 +1865,9 @@ typedef enum MENU
MENU_SWITCH_SCREENS,
MENU_DISPLAY_CAST,
MENU_SPECTRUM_EMU,
#ifdef PORT
MENU_PC_OPTIONS, /* D343: PC options screen (port/src/frontoptions.c) */
#endif
MENU_MAX
} MENU;
+5 -4
View File
@@ -3189,10 +3189,11 @@ typedef union
* compiler (GCC) resolves by inlining ObjectRecord's members. ObjectRecord
* already has a `pad` member (s16 @0x08, the pad index set by
* New_CCTVRecord), so this `pad` (s32 @0x80) is a duplicate name that GCC
* rejects as a hard error (IDO tolerates it). The game code's `->pad`
* (e.g. setupCctv) refers to the INHERITED pad index @0x08, never to this
* member, so renaming it keeps the layout byte-identical. See
* docs/dev/findings.md. */
* rejects as a hard error (IDO tolerates it). Renaming keeps the layout
* byte-identical, BUT on N64 this member shadows the inherited one:
* setupCctv's `arg1->pad` is THIS look-at pad (@0x80), not the mount
* pad (@0x08). Code that means the look-at pad must read `lookpad` on
* PC (D307, M-201: cameras faced backwards). See docs/dev/findings.md. */
s32 lookpad; // lookpad
#else
s32 pad; // lookpad
+113
View File
@@ -146,6 +146,9 @@ struct memallocstring memallocstringtable[] = {
};
s32 g_MainStageNum = LEVELID_NONE;
#ifdef PORT
static s32 g_d235ReloadStage = -1, g_d235ReloadWait = 0; /* TEMP D235 verification */
#endif
s32 g_BossIsDebugMenuOpen = FALSE;
OSScMsg g_bossGfxDoneMsg = { OS_SC_DONE_MSG };
@@ -466,9 +469,92 @@ void bossMainloop(void)
{
g_CurentMaMallocValue = (s32) (strtol(tokenFind(1, "-ma"), NULL, 0) * 1024);
}
#ifdef PORT
/* D294: PC visibility settings (Video.FovScale / WidescreenAuto /
* DrawDistance / LodDistance) keep more rooms on screen at once than
* the N64's fixed view, so the authored `-ma` room pool can exhaust;
* memaAlloc() then fails and undrawn rooms render as solid black.
* Scale the pool by portRoomPoolScale() (1.0 at all-N64-faithful
* settings, capped 2.0x) and never let it eat more than the STAGE
* bank minus 128 KB headroom for zbuf/object resources. */
{
extern f32 portRoomPoolScale(void);
f32 scale = portRoomPoolScale();
s32 avail;
u32 scaled;
if (scale < 0.9999f || scale > 1.0001f) {
scaled = (u32)(((s64)g_CurentMaMallocValue * (s64)(scale * 4.0f) + 3) / 4);
avail = mempGetBankSizeLeft(MEMPOOL_STAGE) - 0x20000;
if ((s32)scaled > avail && avail >= (s32)g_CurentMaMallocValue) {
scaled = (u32)avail;
}
g_CurentMaMallocValue = scaled;
}
}
#endif
#ifdef PORT
if (getenv("GE_D294")) { osSyncPrintf("D294 stage=%d ma=%d\n", (s32)g_StageNum, (s32)g_CurentMaMallocValue); }
#endif
memaReset(mempAllocBytesInBank(g_CurentMaMallocValue, MEMPOOL_STAGE), g_CurentMaMallocValue);
reset_play_data_ptrs();
#ifdef PORT
/* D235 (wrong textures after dying/restarting a level): the port-side
* GL texture cache (port/fast3d/gfx_pc.cpp TextureCacheMap) is keyed by
* (source address, palette addrs, fmt, siz, size_bytes, palette_hash)
* with NO content check. The MEMPOOL_STAGE arena those stage-texture
* source addresses live in was just reset above, so every entry
* pointing into it is stale. On a death/restart the texpool can place
* a DIFFERENT image at an address that held another one during the
* previous run of this level -> cache hit returns the old image =
* "wrong textures" that only ever appear on the reload path, never on
* a cold boot. The N64 has no such cache (the RSP re-reads DRAM every
* frame), so clearing it here is a port-only correctness fix; the
* re-uploads are pixel-identical and cost one-time during the loading
* screen. videoResetTextureCache() existed for exactly this and had
* zero callers -- this is the wiring. GE_D235=1 logs how many stale
* entries each transition actually purged (evidence for the finding;
* a cold boot into a level should log 0, a death/restart >0). */
{
static int ge_d235 = -1;
if (ge_d235 < 0) ge_d235 = getenv("GE_D235") != NULL;
extern void videoResetTextureCache(void);
extern int gfx_texture_cache_count(void);
if (ge_d235)
osSyncPrintf("D235: stage=%d texcache entries purged at load=%d\n",
(s32)g_StageNum, gfx_texture_cache_count());
if (!getenv("GE_D235_OFF")) /* TEMP: A/B switch for verification */
videoResetTextureCache();
/* TEMP (D235 verification, remove with the probe):
* GE_D235_RELOAD=<frames> re-enters the first level once, <frames>
* retraces after the title stage loads following it (e.g. after a
* watch Abort) -- so the front end has rendered its own textures
* into the reset stage arena first, like a real death/restart --
* via the same bossSetLoadedStage() call the front end's mission
* start makes (front.c). Inert when unset. */
{
static int s_reload = -2, s_firstLevel = -1;
if (s_reload == -2) {
const char *e = getenv("GE_D235_RELOAD");
s_reload = e ? atoi(e) : -1;
}
if (s_reload >= 0) {
if (g_StageNum != LEVELID_TITLE && s_firstLevel < 0)
s_firstLevel = g_StageNum;
else if (g_StageNum == LEVELID_TITLE && s_firstLevel >= 0 &&
g_d235ReloadStage < 0 && g_d235ReloadWait == 0) {
/* GE_D235_NEXT=<stage id>: load a DIFFERENT stage
* instead (level A -> title -> level B leaks). */
const char *nx = getenv("GE_D235_NEXT");
g_d235ReloadStage = (nx && *nx) ? (s32)strtol(nx, NULL, 0) : s_firstLevel;
g_d235ReloadWait = s_reload + 1;
}
}
}
}
#endif
localSelectedNumPlayers = 0;
if (g_StageNum != LEVELID_TITLE)
@@ -502,7 +588,27 @@ void bossMainloop(void)
while (g_MainStageNum < 0 || pendingGfx != 0)
{
#ifdef PORT
/* #92 perf probe (GE_PERFSTAT=1): game-thread busy time between
* waking for a message and blocking again (logic + DL build). */
{
static int pon = -1; static u32 tw = 0; static f64 busy = 0; static s32 n = 0;
if (pon < 0) pon = getenv("GE_PERFSTAT") != NULL;
if (pon) {
u32 now = osGetCount();
if (tw) { busy += (f64)(u32)(now - tw); n++; }
if (n == 600) {
fprintf(stderr, "PERFSTAT game-thread busy=%.2fms/msg (avg over 600 msgs)\n",
busy / (f64)n / (f64)(OS_CPU_COUNTER / 1000));
busy = 0; n = 0;
}
}
osRecvMesg(&gfxFrameMsgQ, (OSMesg *)&localGfxFrameMsg, OS_MESG_BLOCK);
if (pon) tw = osGetCount();
}
#else
osRecvMesg(&gfxFrameMsgQ, (OSMesg *)&localGfxFrameMsg, OS_MESG_BLOCK);
#endif
switch (localGfxFrameMsg->gen.type)
{
@@ -524,6 +630,13 @@ void bossMainloop(void)
{
if (g_MainStageNum < 0 && pendingGfx < 2U)
{
#ifdef PORT
/* TEMP D235 verification (see GE_D235_RELOAD above) */
if (g_d235ReloadWait > 0 && --g_d235ReloadWait == 0) {
osSyncPrintf("D235: RELOAD re-entering stage %d\n", g_d235ReloadStage);
bossSetLoadedStage(g_d235ReloadStage);
}
#endif
if (get_is_ramrom_flag())
{
iterate_ramrom_entries_handle_camera_out();
+86 -71
View File
@@ -591,6 +591,52 @@ s32 bgGet2dBboxByRoomId(s32 room_id, struct bbox2d *result)
*/
Gfx *sub_GAME_7F0B3C8C(Gfx *gdl)
{
#if defined(PORT)
/* TEMP D104/D294: once per frame (this function builds the whole bg DL),
* log the drawn-room list with load state + scissor boxes. */
{
static s32 d104f = 0;
static int d104on = -1; /* D302-class cache: once per frame */
#ifdef PORT
/* TEMP D294-BANK: dump memp bank state to find the STAGE overflow. */
static int d294b_on = -1;
static s32 d294b_lastma = -1;
extern s32 g_CurentMaMallocValue;
if (d294b_on < 0) d294b_on = getenv("GE_D294BANK") != NULL;
if (d294b_on && g_CurentMaMallocValue != d294b_lastma) {
d294b_lastma = g_CurentMaMallocValue;
d104f = 0; /* restart window on stage change */
}
if (d294b_on && d104f < 6) {
extern MemoryPool g_mempPools[];
extern s32 needmemallocation;
osSyncPrintf("D294B f=%d STAGE[%p..%p] pos=%p left=%d PERM[%p..%p] pos=%p left=%d needmm=%d\n",
d104f, (void*)g_mempPools[MEMPOOL_STAGE].start, (void*)g_mempPools[MEMPOOL_STAGE].end,
(void*)g_mempPools[MEMPOOL_STAGE].pos, (s32)(g_mempPools[MEMPOOL_STAGE].end - g_mempPools[MEMPOOL_STAGE].pos),
(void*)g_mempPools[MEMPOOL_PERMANENT].start, (void*)g_mempPools[MEMPOOL_PERMANENT].end,
(void*)g_mempPools[MEMPOOL_PERMANENT].pos, (s32)(g_mempPools[MEMPOOL_PERMANENT].end - g_mempPools[MEMPOOL_PERMANENT].pos),
(int)needmemallocation);
}
#endif
if (d104on < 0) d104on = getenv("GE_D104") != NULL;
if (d104on && (d104f++ % 30) == 0 && d104f < 240000) {
s32 rr;
osSyncPrintf("D104 f=%d curRoom=%d roomsDrawn=%d budget=%d :",
d104f, (s32)g_CurrentPlayer->curRoomIndex,
(s32)g_BgNumberOfRoomsDrawn, (s32)g_RoomLoadBudget);
for (rr = 0; rr < g_BgNumberOfRoomsDrawn && rr < 40; rr++) {
s32 rid = dword_CODE_bss_8007FFA0[rr].roomid;
osSyncPrintf(" r%d(ld=%d %d,%d-%d,%d)", rid,
(s32)g_BgRoomInfo[rid].model_bin_loaded,
(s32)dword_CODE_bss_8007FFA0[rr].bbox.min.x,
(s32)dword_CODE_bss_8007FFA0[rr].bbox.min.y,
(s32)dword_CODE_bss_8007FFA0[rr].bbox.max.x,
(s32)dword_CODE_bss_8007FFA0[rr].bbox.max.y);
}
osSyncPrintf("\n");
}
}
#endif
#ifdef VERSION_EU
s16 i;
#else
@@ -647,28 +693,6 @@ Gfx *sub_GAME_7F0B3C8C(Gfx *gdl)
}
gSPMatrix(gdl++, osVirtualToPhysical((void*)get_BONDdata_field_10E0()), (G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_PROJECTION));
#ifdef PORT
/* TEMP D104: room render-pass geometry-visibility probe. */
{
static s32 d104c = 0;
if (getenv("GE_D104") && (d104c++ % 600) == 0 && d104c < 12000) {
s32 rr;
osSyncPrintf("D104 tick=%d curRoom=%d roomsDrawn=%d budget=%d :",
d104c, (s32)g_CurrentPlayer->curRoomIndex,
(s32)g_BgNumberOfRoomsDrawn, (s32)g_RoomLoadBudget);
for (rr = 0; rr < g_BgNumberOfRoomsDrawn && rr < 24; rr++) {
s32 rid = dword_CODE_bss_8007FFA0[rr].roomid;
osSyncPrintf(" r%d(ld=%d %d,%d-%d,%d)", rid,
(s32)g_BgRoomInfo[rid].model_bin_loaded,
(s32)dword_CODE_bss_8007FFA0[rr].bbox.min.x,
(s32)dword_CODE_bss_8007FFA0[rr].bbox.min.y,
(s32)dword_CODE_bss_8007FFA0[rr].bbox.max.x,
(s32)dword_CODE_bss_8007FFA0[rr].bbox.max.y);
}
osSyncPrintf("\n");
}
}
#endif
gdl = fogSetRenderFogColor(
bgScissorCurrentPlayerViewF(
gdl++,
@@ -2413,48 +2437,6 @@ s32 bgLoadRoomVtxData(s32 roomnum, u8 *dst, s32 len)
#ifdef PORT
/* D85: byte-swap the raw N64 Vtx table (16-byte struct, no widening). */
bgSwapRoomVtx((Vtx *)dst, result);
/* D236 pass 16 (TEMP): pass 15's live GE_D236ALPHA capture found the
* Surface1 tree-card render class's vertex alpha capped at <=36/255
* (<=14% opacity) across a full far->close walk to the tree line, and
* confirmed (via the combine_mode decode) that this vertex alpha
* directly multiplies the texture's own alpha in the blend equation --
* so a capped-low vertex alpha caps every tree pixel's final opacity
* outright. bgSwapRoomVtx (just above) explicitly leaves colour bytes
* untouched, so whatever is in cn[] here is exactly what bgDecompress
* produced -- this is the earliest point after decompression the raw
* per-room vertex colour data can be inspected, independent of which
* camera path a live player happens to walk. Unlike GE_D236ALPHA (which
* only sees whatever triangles are actually drawn from wherever the
* player is standing), this dumps a histogram of EVERY vertex's alpha
* byte (cn[3]) for the WHOLE room's table the moment it loads --
* settling whether the low ceiling pass 15 measured is a sampling
* artifact (the walked path just never reached a vertex with a high
* baked alpha) or a genuine property of this room's authored data (no
* vertex in the whole table goes above the low ceiling, regardless of
* where the camera is). Zero cost unset. Remove once D236 pass 16
* concludes. */
if (getenv("GE_D236RAW")) {
s32 vtxCount = result / (s32)sizeof(Vtx);
s32 vi;
u32 buckets[5] = {0, 0, 0, 0, 0}; /* 0 / 1-36 / 37-100 / 101-200 / 201-255 */
u8 amin = 255, amax = 0;
Vtx *vtxArr = (Vtx *)dst;
for (vi = 0; vi < vtxCount; vi++) {
u8 a = vtxArr[vi].v.cn[3];
if (a < amin) amin = a;
if (a > amax) amax = a;
if (a == 0) buckets[0]++;
else if (a <= 36) buckets[1]++;
else if (a <= 100) buckets[2]++;
else if (a <= 200) buckets[3]++;
else buckets[4]++;
}
osSyncPrintf(
"D236RAW room=%d n=%d amin=%u amax=%u buckets[0,1-36,37-100,101-200,201-255]=%u,%u,%u,%u,%u\n",
(int)roomnum, (int)vtxCount, amin, amax,
buckets[0], buckets[1], buckets[2], buckets[3], buckets[4]);
}
#endif
room->vertices = (Vtx *)dst;
@@ -2704,11 +2686,20 @@ void bgLoadRoomModelData(s32 roomID)
used = 0;
#if defined(PORT)
static int d104ld_on = -1; /* D302-class cache */
static s32 d104ld_n = 0;
if (d104ld_on < 0) d104ld_on = getenv("GE_D104") != NULL;
#define D104LD(...) do { if (d104ld_on && d104ld_n++ < 4000) osSyncPrintf(__VA_ARGS__); } while (0)
#else
#define D104LD(...) do {} while (0)
#endif
// Room ID is out of range?
if (roomID >= g_MaxNumRooms) goto end;
if (roomID >= g_MaxNumRooms) { D104LD("D104 LOAD room=%d FAIL oob\n", roomID); goto end; }
// Room is already loaded?
if (g_BgRoomInfo[roomID].model_bin_loaded) goto end;
if (g_BgRoomInfo[roomID].model_bin_loaded) { D104LD("D104 LOAD room=%d skip ld=%d\n", roomID, (s32)g_BgRoomInfo[roomID].model_bin_loaded); goto end; }
// Get the cached file size. Is zero when the size is not yet known.
allocsize = g_BgRoomInfo[roomID].cur_room_totalsize;
@@ -2732,7 +2723,7 @@ void bgLoadRoomModelData(s32 roomID)
*/
data = memaAlloc(allocsize);
if (data == NULL) goto end;
if (data == NULL) { D104LD("D104 LOAD room=%d FAIL alloc size=%d\n", roomID, allocsize); goto end; }
if (g_BgRoomInfo[roomID].csize_point_index_binary)
{
@@ -2782,6 +2773,10 @@ void bgLoadRoomModelData(s32 roomID)
g_BgRoomInfo[roomID].cur_room_totalsize = ((used + 0x20) & ~0xf);
g_BgRoomInfo[roomID].model_bin_loaded = 1;
D104LD("D104 LOAD room=%d OK used=%d vtx=%d prim=%d sec=%d\n", roomID, used,
(s32)g_BgRoomInfo[roomID].usize_point_index_binary,
(s32)g_BgRoomInfo[roomID].usize_primary_DL_binary,
(s32)g_BgRoomInfo[roomID].usize_secondary_DL_binary);
// If wasted space is detected, shrink allocated memory block.
if (allocsize != ((used + 0x20) & ~0xf))
@@ -2944,6 +2939,18 @@ Gfx *bgRenderRoomPrimary(Gfx *gdl, s32 room_index)
if (g_BgRoomInfo[room_index].model_bin_loaded == 0)
{
#if defined(PORT)
/* TEMP D104/D294: room is on the drawn list but not rendered this
* frame (no load budget / load failed) -- its scissor region shows
* whatever was behind it. Log every occurrence, capped. */
static int d104skip_on = -1;
static s32 d104skip_n = 0;
if (d104skip_on < 0) d104skip_on = getenv("GE_D104") != NULL;
if (d104skip_on && d104skip_n++ < 4000) {
osSyncPrintf("D104 SKIP room=%d budget=%d\n",
room_index, (s32)g_RoomLoadBudget);
}
#endif
return gdl;
}
else
@@ -2952,7 +2959,9 @@ Gfx *bgRenderRoomPrimary(Gfx *gdl, s32 room_index)
#if defined(PORT)
/* TEMP D63: log primary GDL at render time (env GE_D63=1) */
if (getenv("GE_D63")) {
static int d63on_p = -1; /* D302-class cache: per room, per frame */
if (d63on_p < 0) d63on_p = getenv("GE_D63") != NULL;
if (d63on_p) {
u32 *w = (u32 *)g_BgRoomInfo[room_index].ptr_expanded_mapping_info;
osSyncPrintf("D63 bgRenderRoomPrimary room=%d ptr=%p usize=%d w0..3=(%08x,%08x,%08x,%08x)\n",
room_index, (void *)g_BgRoomInfo[room_index].ptr_expanded_mapping_info,
@@ -2997,7 +3006,9 @@ Gfx *bgRenderRoomSecondary(Gfx *gdl, s32 room_index)
#if defined(PORT)
/* TEMP D63: log secondary GDL at render time (env GE_D63=1) */
if (getenv("GE_D63")) {
static int d63on_s = -1; /* D302-class cache: per room, per frame */
if (d63on_s < 0) d63on_s = getenv("GE_D63") != NULL;
if (d63on_s) {
u32 *w = (u32 *)g_BgRoomInfo[room_index].ptr_secondary_expanded_mapping_info;
osSyncPrintf("D63 bgRenderRoomSecondary room=%d ptr=%p usize=%d w0..3=(%08x,%08x,%08x,%08x)\n",
room_index, (void *)g_BgRoomInfo[room_index].ptr_secondary_expanded_mapping_info,
@@ -3509,7 +3520,9 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV
idx[2] = (((u8 *) gdl)[7] / 10) - vtxoff;
#endif
#ifdef PORT
if (getenv("GE_D154"))
static int d154on = -1; /* D302-class cache: inner GDL walk, fires per bullet hit */
if (d154on < 0) d154on = getenv("GE_D154") != NULL;
if (d154on)
osSyncPrintf("D154 TRI1 w1=%08x idx=%d,%d,%d\n",
(unsigned)(u32)gdl->words.w1, (int)idx[0], (int)idx[1], (int)idx[2]);
#endif
@@ -3694,7 +3707,9 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV
}
#endif
#ifdef PORT
if (getenv("GE_D154"))
static int d154on4 = -1; /* D302-class cache: inner GDL walk, fires per bullet hit */
if (d154on4 < 0) d154on4 = getenv("GE_D154") != NULL;
if (d154on4)
osSyncPrintf("D154 TRI4 s2=%d w0=%08x w1=%08x idx2=%d,%d,%d\n",
(int)s2, (unsigned)(u32)gdl->words.w0, (unsigned)(u32)gdl->words.w1,
(int)idx2[0], (int)idx2[1], (int)idx2[2]);
+22 -3
View File
@@ -39,9 +39,28 @@ void bheadSetdamp(f32 headdamp);
#ifdef PORT
/* D336 (ABI/layout, A1 raw-offset alias; the D100 follow-up): on N64 the
* player's `model` at 0x598 is an INLINE struct Model, and the decomp's
* placeholders after it alias its fields by raw offset: animFlipFlag (0x5BC)
* is model+0x24 = model.gunhand, field_5C0 (0x5C0) is model+0x28 =
* model.animframe1 (4-byte pointers, no holes: unk25/unk27/unk2c confirm).
* D100 made `model` a real inline struct on PC, so those placeholders became
* separate fields that nothing initialises or keeps in sync -- the head-bob
* start frame was read from never-written memory (uninitialised player-struct
* bytes; garbage like 2e32 poisoned Bond's anim -> D156, and with the D294
* 2x room pool the player struct lands on dirty memory, corrupting Frigate).
* Read/write the real model fields, i.e. the bytes the N64 code touches. */
#define BHEAD_FLIPFLAG (g_CurrentPlayer->model.gunhand)
#define BHEAD_CURFRAME (g_CurrentPlayer->model.animframe1)
#else
#define BHEAD_FLIPFLAG (g_CurrentPlayer->animFlipFlag)
#define BHEAD_CURFRAME (g_CurrentPlayer->field_5C0)
#endif
void bheadFlipAnimation()
{
g_CurrentPlayer->animFlipFlag = !g_CurrentPlayer->animFlipFlag;
BHEAD_FLIPFLAG = !BHEAD_FLIPFLAG;
}
void bheadUpdateIdleRoll()
@@ -410,7 +429,7 @@ void bheadAdjustAnimation(f32 speed)
if (g_CurrentPlayer->headanim >= 0)
{
startframe = (g_CurrentPlayer->field_5C0 - g_BondMoveAnimationSetup[g_CurrentPlayer->headanim].loopframe)
startframe = (BHEAD_CURFRAME - g_BondMoveAnimationSetup[g_CurrentPlayer->headanim].loopframe)
/ (g_BondMoveAnimationSetup[g_CurrentPlayer->headanim].endframe - g_BondMoveAnimationSetup[g_CurrentPlayer->headanim].loopframe);
startframe = g_BondMoveAnimationSetup[i].loopframe + ((g_BondMoveAnimationSetup[i].endframe - g_BondMoveAnimationSetup[i].loopframe) * startframe);
@@ -420,7 +439,7 @@ void bheadAdjustAnimation(f32 speed)
&g_CurrentPlayer->model,
// match hack: addu address backwards
(struct ModelAnimation *) ((s32)g_BondMoveAnimationSetup[i].anim_id + (s32)&ptr_animation_table->data),
(s32) g_CurrentPlayer->animFlipFlag,
(s32) BHEAD_FLIPFLAG,
startframe,
0.5f,
12.0f);
+173 -3
View File
@@ -48,6 +48,12 @@
#include "stan.h"
#include "stanintersection.h"
#include "textrelated.h"
#ifdef PORT
#include "envflag.h" /* cached getenv for hot-path probes */
#ifdef PORT
#include "hudaspect.h" /* D335 HUD alignment under native widescreen */
#endif
#endif
#ifdef VERSION_EU
@@ -1689,7 +1695,7 @@ void bondviewFrozenCameraTick(u16 buttons, u16 oldbuttons, struct coord3d *pos,
* M-141/M-142 -- the pad-orbit branch below was ruled out with
* zero hits, so the Dam cutscene must be reaching this branch
* or the gBondViewCutscene branch further down instead). */
if (getenv("GE_D243CAM")) {
if (GE_ENVFLAG("GE_D243CAM")) {
osSyncPrintf("D243CAM lookatpad: pos=%.2f,%.2f,%.2f pos2=%.2f,%.2f,%.2f\n",
(double) pos->f[0], (double) pos->f[1], (double) pos->f[2],
(double) pos2->f[0], (double) pos2->f[1], (double) pos2->f[2]);
@@ -1742,7 +1748,7 @@ void bondviewFrozenCameraTick(u16 buttons, u16 oldbuttons, struct coord3d *pos,
* zero hits in M-141/M-142). theta/verta are the spherical
* angles driving pos2's look-at offset from pos; a jump/jitter
* in either between calls would show up as the reported shake. */
if (getenv("GE_D243CAM")) {
if (GE_ENVFLAG("GE_D243CAM")) {
osSyncPrintf("D243CAM cutscene: pos=%.2f,%.2f,%.2f pos2=%.2f,%.2f,%.2f theta=%.4f verta=%.4f\n",
(double) pos->f[0], (double) pos->f[1], (double) pos->f[2],
(double) pos2->f[0], (double) pos2->f[1], (double) pos2->f[2],
@@ -1789,7 +1795,7 @@ void bondviewFrozenCameraTick(u16 buttons, u16 oldbuttons, struct coord3d *pos,
* wall-clock sim-tick count, normally clamped to <=6 but not
* necessarily steady at 1) is jittering frame-to-frame in a way that
* would show up as this exact judder. */
if (getenv("GE_D243CAM")) {
if (GE_ENVFLAG("GE_D243CAM")) {
osSyncPrintf("D243CAM: dt=%.4f angle=%.4f pos=%.2f,%.2f,%.2f pad=%d\n",
(double) g_GlobalTimerDelta, (double) flt_CODE_bss_80079A00,
(double) pos->f[0], (double) pos->f[1], (double) pos->f[2],
@@ -8213,6 +8219,14 @@ void bondviewMovePlayerUpdateViewport(s8 stick_x, s8 stick_y, u16 buttons)
}
#ifdef VERSION_EU
#ifdef PORT
extern f32 portNativeAspect(void); /* D334, see the NTSC branch below */
if (portNativeAspect() > 0.0f)
{
faspect = ((f32) bondviewGetCurrentPlayerViewportWidth() / (f32) bondviewGetCurrentPlayerViewportHeight()) * 0.75f * portNativeAspect();
}
else
#endif
if (get_screen_ratio() == SCREEN_RATIO_16_9)
{
faspect = ((f32) bondviewGetCurrentPlayerViewportWidth() / (f32) bondviewGetCurrentPlayerViewportHeight()) * 0.75f * WIDESCREEN_ASPECT;
@@ -8232,6 +8246,19 @@ void bondviewMovePlayerUpdateViewport(s8 stick_x, s8 stick_y, u16 buttons)
#else
#ifdef PORT
/* D334 (native widescreen, Rule-2 approved 2026-09-23): the N64 16:9 mode
* below with WIDESCREEN_ASPECT replaced by the real window aspect -- see
* portNativeAspect() (port/src/video.c). The in-game "Ratio 16:9" option
* is ignored while this is on (the window is the ratio). */
extern f32 portNativeAspect(void);
if (portNativeAspect() > 0.0f)
{
set_cur_player_aspect(((f32) bondviewGetCurrentPlayerViewportWidth() / (f32) bondviewGetCurrentPlayerViewportHeight()) * 0.75f * portNativeAspect());
viSetAspect(((f32) bondviewGetCurrentPlayerViewportWidth() / (f32) bondviewGetCurrentPlayerViewportHeight()) * 0.75f * portNativeAspect());
}
else
#endif
if (get_screen_ratio() == SCREEN_RATIO_16_9)
{
set_cur_player_aspect(((f32) bondviewGetCurrentPlayerViewportWidth() / (f32) bondviewGetCurrentPlayerViewportHeight()) * 0.75f * WIDESCREEN_ASPECT);
@@ -9233,6 +9260,63 @@ Gfx *bondviewRenderCredits(Gfx *gdl)
}
#ifdef PORT
/* D226: the top dialogue's line breaks are authored into the text (no runtime
* wrap), so an unconditional HUD scale pushes long lines off-screen. Scale it
* as far as the widest line still fits between the text origin (view left +
* 0x1e, msg.x) and the same margin on the right. */
static s32 portDialogueHudScalePercent(void)
{
s32 hp = portHudScalePercent();
s32 th = 0, tw = 0;
s32 avail;
char *text;
if (hp <= 100) {
return hp;
}
#if defined(LEFTOVERDEBUG)
text = stringbuffer_top[upper_text_buffer_index];
#else
text = dword_CODE_bss_80079DC8[upper_text_buffer_index];
#endif
if (text == NULL || text[0] == '\0') {
return hp;
}
textMeasure(&th, &tw, text, ptrFontZurichBoldChars, ptrFontZurichBold, 0);
avail = viGetViewWidth() - 2 * 0x1e;
if (tw > 0 && tw * hp > avail * 100) {
hp = (avail * 100) / tw;
if (hp < 100) hp = 100;
}
return hp;
}
#endif
#ifdef PORT
/* D226: same width cap for the lower-left pickup / status message (and the
* level title card), measured with the fonts the bottom renderer uses. */
static s32 portBottomHudScalePercent(void)
{
s32 hp = portHudScalePercent();
s32 th = 0, tw = 0;
s32 avail;
char *text = stringbuffer_lowerleft[status_bar_text_buffer_index];
if (hp <= 100 || text[0] == 0) {
return hp;
}
textMeasure(&th, &tw, text, BONDVIEW_2ND_FONTTABLE(status_bar_text_buffer_index),
BONDVIEW_1ST_FONTTABLE(status_bar_text_buffer_index), 0);
avail = viGetViewWidth() - 2 * 0x1e;
if (tw > 0 && tw * hp > avail * 100) {
hp = (avail * 100) / tw;
if (hp < 100) hp = 100;
}
return hp;
}
#endif
Gfx *maybe_mp_interface(Gfx *gdl)
{
s32 ulx;
@@ -9249,18 +9333,62 @@ Gfx *maybe_mp_interface(Gfx *gdl)
if (g_CurrentPlayer->cameramode == 1)
{
bondviewIntroCameraTextTick();
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_CENTER);
#endif
#ifdef PORT
PORT_HUD_SCALE_PCT(gdl, portBottomHudScalePercent(), viGetViewLeft(), viGetViewTop() + viGetViewHeight()); /* D226: bottom messages, width-capped */
#endif
gdl = hudmsgBottomRender(gdl);
#ifdef PORT
PORT_HUD_SCALE_END(gdl);
#endif
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_NONE);
#endif
bondviewUpperTextWindowTimerTick();
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_CENTER);
#endif
#ifdef PORT
PORT_HUD_SCALE_PCT(gdl, portDialogueHudScalePercent(), viGetViewLeft() + 0x1e, viGetViewTop()); /* D226: dialogue, width-capped */
#endif
gdl = sub_GAME_7F08AAE8(gdl);
#ifdef PORT
PORT_HUD_SCALE_END(gdl);
#endif
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_NONE);
#endif
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_CENTER);
#endif
gdl = countdownTimerRender(gdl);
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_NONE);
#endif
gdl = currentPlayerDrawFade(gdl);
#ifdef PORT
/* D335: end credits are centred 4:3 text -> keep their shape. */
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_CENTER);
gdl = bondviewRenderCredits(gdl);
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_NONE);
return gdl;
#else
return bondviewRenderCredits(gdl);
#endif
}
gunUpdateAndFireBothHands();
gunRenderCasings(&gdl);
gunRenderFirstPersonGunModels(&gdl);
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_CENTER);
#endif
gdl = bondviewRenderWatch(gdl);
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_NONE);
#endif
if (g_CurrentPlayer->mpmenuon != 0)
{
@@ -9275,11 +9403,23 @@ Gfx *maybe_mp_interface(Gfx *gdl)
if (bondviewGetIfCurrentPlayerHealthShowTime() &&
(g_CurrentPlayer->watch_animation_state == 0))
{
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_CENTER);
#endif
gdl = bondviewRenderGaugeBars(gdl);
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_NONE);
#endif
}
else if (mpwatchShouldDisplayGauges())
{
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_CENTER);
#endif
gdl = bondviewRenderGaugeBars(gdl);
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_NONE);
#endif
if (g_CurrentPlayer->healthdisplaytime > 0)
{
g_CurrentPlayer->healthdisplaytime -= g_ClockTimer;
@@ -9388,12 +9528,42 @@ Gfx *maybe_mp_interface(Gfx *gdl)
}
bondviewIntroCameraTextTick();
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_CENTER);
#endif
#ifdef PORT
PORT_HUD_SCALE_PCT(gdl, portBottomHudScalePercent(), viGetViewLeft(), viGetViewTop() + viGetViewHeight()); /* D226: bottom messages, width-capped */
#endif
gdl = hudmsgBottomRender(gdl);
#ifdef PORT
PORT_HUD_SCALE_END(gdl);
#endif
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_NONE);
#endif
bondviewUpperTextWindowTimerTick();
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_CENTER);
#endif
#ifdef PORT
PORT_HUD_SCALE_PCT(gdl, portDialogueHudScalePercent(), viGetViewLeft() + 0x1e, viGetViewTop()); /* D226: dialogue, width-capped */
#endif
gdl = sub_GAME_7F08AAE8(gdl);
#ifdef PORT
PORT_HUD_SCALE_END(gdl);
#endif
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_NONE);
#endif
gunDrawSight(&gdl);
gdl = generate_ammo_total_microcode(gdl);
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_CENTER);
#endif
gdl = countdownTimerRender(gdl);
#ifdef PORT
PORT_HUD_ASPECT(gdl, GE_HUD_ASPECT_NONE);
#endif
gdl = display_red_blue_on_radar(gdl);
return currentPlayerDrawFade(gdl);
}
+32
View File
@@ -372,6 +372,19 @@ void bondviewLoadSetupIntroSection(void)
{
ptr_random06cam_entry = g_CurrentSetupIntroCamera;
rand_camera_index = (s32)(randomGetNext() % (u32) g_SetupIntroCameraCount);
#ifdef PORT
/* TEMP D306 (M-201): test-only override of the first intro camera,
* GE_INTROCAM=<n> (n < count). The PRNG draw above still happens, so
* the rest of the random sequence is unchanged. Inert unset. */
{
extern char *getenv(const char *name);
extern int atoi(const char *s);
const char *ic = getenv("GE_INTROCAM");
if (ic || getenv("GE_D306C")) osSyncPrintf("D306: intro camera index %d of %d%s\n", (int)rand_camera_index,
(int)g_SetupIntroCameraCount, ic ? " (GE_INTROCAM override)" : "");
if (ic) rand_camera_index = (s32)((u32)atoi(ic) % (u32)g_SetupIntroCameraCount);
}
#endif
while (rand_camera_index > 0)
{
rand_camera_index--;
@@ -384,6 +397,25 @@ void bondviewLoadSetupIntroSection(void)
}
bondinvAddInvItem(ITEM_FIST);
#ifdef PORT
/* TEMP D303 (M-201): test-only start weapon, GE_STARTWEAPON=<ITEM_IDS>,
* so headless repros can hold a specific gun (pair with GE_D320R
* cheat=3 for ammo). Inert unset. */
{
extern char *getenv(const char *name);
extern int atoi(const char *s);
const char *sw = getenv("GE_STARTWEAPON");
if (sw) {
bondinvAddInvItem(atoi(sw));
{
extern void add_ammo_to_weapon(ITEM_IDS weapon, s32 ammo);
add_ammo_to_weapon(atoi(sw), 400);
}
starting_weapon[GUNRIGHT] = atoi(sw);
set_starting_weapon = 1;
}
}
#endif
if (set_starting_weapon == 0)
{
+28 -51
View File
@@ -6212,6 +6212,33 @@ void chrlvUpdateAimendbackShoulders(ChrRecord *self, void *arg1, s32 same, s32 s
next_aimendback = 0.0f;
next_lshoulder = next;
#ifdef PORT
/* The leading anim union in weapon_firing_animation_table grows from
* four to eight bytes on PC. N64's float slots [12,13,16,17] are now
* different fields; read the intended limits/arm fractions by name. */
if (arg1 != NULL)
{
struct weapon_firing_animation_table *config = arg1;
if (config->max_up < next)
{
next_aimendback = next - config->max_up;
next_lshoulder = config->max_up;
}
else if (next < config->max_down)
{
next_aimendback = next - config->max_down;
next_lshoulder = config->max_down;
}
if (next_lshoulder > 0.0f)
{
next_rshoulder = config->free_arm_frac_up * next_lshoulder;
}
else
{
next_rshoulder = config->free_arm_frac_down * next_lshoulder;
}
}
#else
if (arg1 != NULL)
{
if (((f32*)arg1)[12] < next)
@@ -6235,6 +6262,7 @@ void chrlvUpdateAimendbackShoulders(ChrRecord *self, void *arg1, s32 same, s32 s
next_rshoulder = ((f32*)arg1)[17] * next_lshoulder;
}
}
#endif
if (swap != 0)
{
@@ -9994,35 +10022,6 @@ bool chrHasFlags2ById(ChrRecord *self, s32 chrNum, u8 flags2)
*/
void chrSetStageFlags(ChrRecord *self, s32 arg1)
{
#ifdef PORT
/* D318-followup (diagnosis only, GE_OBJT=1): attribute writes to the LOW
* bits of objectiveregisters1 (the Facility execution derail triggers:
* 0x04 gas/combat, 0x20 surrender/monologue, 0x40 flee -- none of which
* the level setup script ever sets; only character-action scripts do).
* Logs the writing chr + its AI position so the writer's script state is
* correlatable against a simultaneous D318T capture. Read-only, capped,
* no behavior change; N64 build unaffected. */
extern char *getenv(const char *);
extern s32 chraiGetAIListID(AIRecord *AIList, bool *isGlobalAIList); /* chrai.c */
static int s_objt = -1;
static int s_objtn = 0;
int objtlog;
s32 aid;
bool aig;
if (s_objt < 0) { s_objt = getenv("GE_OBJT") != NULL; }
objtlog = s_objt && (arg1 & 0x0000FFFF) && (s_objtn < 500);
if (objtlog)
{
aid = self->ailist ? chraiGetAIListID(self->ailist, &aig) : -1;
osSyncPrintf("OBJT: t=%d SET mask=0x%08x by chr %d (act=%d off=%d aiid=0x%04x%s ailist=%p) reg=0x%08x\n",
(int)g_GlobalTimer, (unsigned)arg1, (int)self->chrnum,
(int)self->actiontype, (int)self->aioffset, (unsigned)aid,
aig ? "G" : "", (void *)self->ailist,
(unsigned)objectiveregisters1);
s_objtn++;
}
#endif
objectiveregisters1 |= arg1;
}
@@ -10032,28 +10031,6 @@ void chrSetStageFlags(ChrRecord *self, s32 arg1)
*/
void chrUnsetStageFlags(ChrRecord *self, u32 flags)
{
#ifdef PORT
extern char *getenv(const char *);
extern s32 chraiGetAIListID(AIRecord *AIList, bool *isGlobalAIList); /* chrai.c */
static int s_objtu = -1;
static int s_objtun = 0;
int objtulog;
s32 aid;
bool aig;
if (s_objtu < 0) { s_objtu = getenv("GE_OBJT") != NULL; }
objtulog = s_objtu && (flags & 0x0000FFFF) && (s_objtun < 500);
if (objtulog)
{
aid = self->ailist ? chraiGetAIListID(self->ailist, &aig) : -1;
osSyncPrintf("OBJT: t=%d UNS mask=0x%08x by chr %d (act=%d off=%d aiid=0x%04x%s ailist=%p) reg=0x%08x\n",
(int)g_GlobalTimer, (unsigned)flags, (int)self->chrnum,
(int)self->actiontype, (int)self->aioffset, (unsigned)aid,
aig ? "G" : "", (void *)self->ailist,
(unsigned)objectiveregisters1);
s_objtun++;
}
#endif
objectiveregisters1 = ~flags & objectiveregisters1; //shorthand does not match
}
Loaded 100 of 170 files, more files were not shown because too many files have changed in this diff. Show more