Files
goldeneye-pc-port/docs/dev/WIDESCREEN-FOV-PLAN.md
T
James Dansereau 1c60da6f46 docs: clarify rule #2's diagnostic scope + define rule-2 sign-off process
Widens the written scope of AGENTS.md non-negotiable #2's ABI exception to
match established practice (runtime struct/union misreads, not just
ROM-serialized structs — the D209/D210/D255 pattern), states explicitly
that diagnosis into src/game is never restricted (only the fix is), and
defines a real three-bucket taxonomy + sign-off procedure (dev-process.md
§7) for the rare genuine-behavior-difference case, replacing the
undefined ad hoc "ask the user" gesture that's been referenced without
being defined across ~10 places in the docs.

Mirrors the same framing into CONTRIBUTING.md and the PR template for
external contributors. Extends porting-notes.md §A1 with D255 as a third
worked example and a tell-tale-signs checklist, explicitly distinguishing
it from D284 (same epistemic shape, different mechanism, no rule-2
question at all). Appends a ready-to-dispatch pass-10 brief to the D236
entry applying the new taxonomy to its actual next step.

Docs-only; no code/build changes. Per docs/dev/notes/RULE-2-CLARIFICATION-PLAN.md.
2026-09-16 17:45:58 -04:00

18 KiB
Raw Blame History

Widescreen & FOV Plan

Status: proposal, parked — like UNLOCKED-FPS-PLAN.md, resume after audio work (project Phase 3). Research current as of this writing.

Scope decision (agreed): target exactly two display aspects — 4:3 and 16:9. Arbitrary aspect ratios are TBD and out of scope; the window manager should snap/letterbox to one of the two rather than trying to serve anything else.

Standard (agreed): Option B (native 16:9 render, PD-parity) is the goal. The PD PC port's widescreen architecture is the reference standard for this engine family. Option A (4:3-internal + stretch) remains an acceptable case-by-case workaround only where B proves impractical for a specific surface — each such fallback documented individually.

Problem

At high resolutions (e.g. 4K) in a 16:9 window the game is disorienting. Root cause, confirmed in code: the port renders in window-resolution space. All 320×240 logical screen coordinates are scaled per-axis by RATIO_X = win_w/320, RATIO_Y = win_h/240 (port/fast3d/gfx_pc.cpp:53-54). In a 16:9 window everything — world geometry and HUD — is stretched 4/3 horizontally: circles become ellipses, and the horizontal FOV balloons from ~78° to ~91° while vertical stays 60°. That stretch is the disorientation.

Research findings

1. The FOV model (where FOV lives)

  • Vertical FOV is a fixed 60° base: FOV_Y_F (src/fr.h:137); per-player fovy/aspect fields default to 60 / DEFAULT_ASPECT (src/game/player.c:454-455).
  • Setters: viSetFovY / viSetAspect / viSetFov(fovx, fovy) (src/fr.c:911-934) → guPerspectiveF(g_viProjectionMatrixF, …, fovy, aspect, znear, zfar, 1.0f) (src/fr.c:718).
  • FOV is dynamic: aim zoom rewrites fovy every frame via viSetFovY(g_CurrentPlayer->zoominfovy) (src/game/bondview2.c:3123), and the camera path resets fovy/aspect per frame (bondviewMovePlayerUpdateViewport, src/game/bondview2.c:7967-8034). ⇒ Any FOV option must be a multiplicative correction composable with zoom, not an override of a constant.

2. The game ships with a (stretch-based) 16:9 path

This decomp contains a player-facing widescreen path from the original: options-menu "Ratio": Normal / 16:9 (GAME_OPTIONS_INDEX_RATIO, string OPTION_STR_23_169_LF, src/game/options.c:129), save bit OPTION_SCREENRATIO (src/game/file2.c:1266-1270, 1323), get/set_screen_ratio() (src/game/options.c:553-558). When active, the camera sets aspect = (vp_w/vp_h) × 0.75 × 16/9 (= viewport ratio × 4/3, src/game/bondview2.c:8007-8034) — the pre-distortion for a 4:3 render stretched to 16:9 (N64-out-4:3, TV stretches). HUD compensation exists for crosshair (gunfire.c:6316-6318, half-width ×0.75) and rocket aim (gunfire.c:501); EU-only extras need an NTSC audit.

⇒ This path is exactly Option A's game-side machinery. It is not the PD standard (which renders native 16:9), so under Option B it stays dormant except as fallback.

3. Current port rendering pipeline (what to build on)

  • Render happens at window size; an FBO path already exists and activates when internal dims ≠ window dims or MSAA > 1 (gfx_pc.cpp:3041-3115+).
  • Native-res bookkeeping: GE_NATIVE_W/H = 640×480 (NTSC) / 640×400 (PAL) (port/src/video.c:40-44); VI size requests clamp to ≤640 (port/src/libultra.c:816). Auto window defaults to 4:3 (port/fast3d/gfx_sdl2.cpp:107).

4. fast3d EXT aspect commands — already present in GE's RSP

GE's software RSP (adapted from PD's) implements Rare's PC-port extension geometry-mode commands G_ASPECT_{LEFT,RIGHT,WIDE,CENTER}_EXT (gfx_pc.cpp:1954-1979, decoder at gfx_sp_extra_geometry_mode), and port/fast3d/gbiex.h carries the gSP…EXT command macros. The RSP side of the PD standard already works in this codebase; only the game-code emission sites are missing.

5. The PD standard (reference architecture — what Option B replicates)

From the pd_port checkout (N64 PD had no widescreen; the port added it):

  1. Projection aspect comes from the port: videoGetAspect() returns the real window aspect (gfx_current_dimensions.aspect_ratio, port/src/video.c:232-235). Game code calls viSetAspect(videoGetAspect()) (e.g. title.c:2495; even #define TITLE_ASPECT (videoGetAspect())). Native 16:9 projection ⇒ correct perspective, vertical FOV preserved.
  2. Per-element screen-space corrections where an element is positioned in screen coords: sightGetAdjustedX(x), ×(SCREEN_ASPECT / videoGetAspect()) scalings (sight.c:77,921, sky.c:2728+, menu.c:2332,4771, player.c:3108).
  3. HUD alignment via the EXT commands: screen-space draw segments wrapped in gSPSetExtraGeometryModeEXT(gdl, G_ASPECT_CENTER_EXT) … gSPClearExtraGeometryModeEXT(…), all under #ifndef PLATFORM_N64. With the mode active, fast3d scales 4:3-logical screen coords by the native aspect and centers (or left/right-aligns) them in the wider window — no distortion. PD scale: 52 emission sites across 10 game files (sight, hudmsg, bondgun, menu, radar, title, propobj, credits, activemenu, game_1531a0).
  4. User-facing: vsync/framerate options + HUD-centering option (g_HudCenter → g_HudAlignModeL/R, port/src/main.c:77-83).

M-87 PD-legacy-survey notes (docs/dev/notes/PD-LEGACY-SURVEY.md):

  • Overscan/safe-frame (candidate 2, deliberate divergence, recorded here): PD hardcodes its native viewport to 320×220 (port/src/video.c:77-79) — the N64-on-a-TV visible area after ~20 lines top/bottom of overscan. GE's port renders the full 640×480/640×400 frame, no overscan crop (port/src/video.c:38-47) — a deliberate PC-default choice (nothing hidden, no HUD-clipping risk from a TV-only limitation), not an oversight, and not something to "fix" toward PD's behavior. Net effect: GE shows ~9% more vertical content than PD at the equivalent native resolution. Revisit only if a Video.SafeAreaOverlay debug knob is ever wanted to visualize the original TV-safe region.
  • "Ratio" 4:3/16:9 option (candidate 3) — correction, M-87 review: PD deleted its N64-era per-player Ratio dropdown on PC (options.c forces SCREENRATIO_NORMAL outside PLATFORM_N64). GE's equivalent (SCREEN_RATIO_16_9, get/set_screen_ratio, options.c:553-558, persisted per-save, consumed by the camera) is a live, functioning code path — but it's an old anamorphic stretch toggle, not true wider-FOV rendering, and this doc's own Open Questions (below) already flag the tension and lean the other way: leave this legacy flag dormant and have a separate, new, port-owned Video.Aspect config be Option B's actual trigger — the same separation-of-concerns choice PD itself made (PD didn't repurpose its legacy flag either). Repurposing the shipped save-flag to drive a brand-new render path risks desync (existing saves silently gaining/missing the modern feature, or the flag and Video.Aspect disagreeing). Not yet decided ("Decide Phase 1" below) — correcting an earlier note here that overstated this as settled.

6. GEVR portal-scissor findings — read before Phase 2/3 game-code work

The sibling GEVR VR port (same n64decomp/007 lineage, reference-only survey in scratchpad/GEVR-TRIAGE.md item A) independently measured, on the same decomp, that widescreen "missing wall / see-through slab" artifacts come from the per-room portal SETSCISSOR rects, not the room-visibility pass (their cull census was byte-identical between arms) — giving every room the full view removed the artifacts (one level, one viewpoint; treat as a hypothesis, not settled). Re-derived in our tree (2026-07):

Class Our sites Notes
SCISSOR — room passes bg.c:685, bg.c:747 (bgScissorCurrentPlayerViewF) primary + secondary/xlu
SCISSOR — default variants bg.c:723, bg.c:1306, chrprop.c:598 (via bgScissorCurrentPlayerViewDefault, bg.c:1332)
SCISSOR — characters chr.c:3070/3074 (chrRender)
SCISSOR — explosions explosion.c:876/880 and explosion.c:1488/1492 (smoke) the smoke site is the one GEVR missed on their first pass
CULL (removes props, not pixels) propobj.c:13584 posIsOnScreen (used at :5881) a too-tight rect deletes a prop — GEVR's black-wall candidate
AI / SPAWN logic chraction.c:10628 chrIsPosOffScreen → bgGet2dBboxByRoomId (bg.c:573) reads the rect for character AI/spawn decisions

Design trap (their hardest-won lesson): do NOT put a clip/widescreen knob inside bgGet2dBboxByRoomId — it's the chokepoint that also feeds chrIsPosOffScreen, so a "clip-only" change there would silently alter what characters do. Scissor sites can be widened directly; the cull box and the AI/spawn consumer must be reasoned about separately.

Caveats: (1) removing all per-room scissoring has an unknown overdraw/perf cost — measure before shipping; (2) their fix evidence is one level + one viewpoint; (3) the "two notions of view width" trap (their doc 278): a port knob that redefines native content width from the window aspect while the game's scissor-tracked canvas stays 4:3 leaves ~29% of columns undrawn → stale-buffer slabs. Whatever this plan's implementation does, the game's canvas notion and the window notion must be reconciled explicitly at one named place — re-verify the line numbers above (cheap greps: bgScissorCurrentPlayerView, bgGet2dBboxByRoomId, posIsOnScreen, chrIsPosOffScreen) before acting on them, since this table predates any Phase 2/3 work actually landing.

Option B — the standard (native 16:9, PD-parity)

Architecture

Render directly at the 16:9 window resolution. World: projection aspect = window aspect (vertical FOV preserved, no post-stretch). HUD/screen-space: 4:3-logical coordinates mapped undistorted via the EXT aspect modes (centered by default; left/right alignment available à la PD). 4:3 windows: byte-identical to today.

Change inventory

Port layer (no rules issues):

  • videoGetAspect() equivalent in port/src/video.c (return window aspect; GE already tracks gfx_current_dimensions.aspect_ratio).
  • Window/aspect policy: Video.Aspect {4:3, 16:9}, snap on create/resize; out-of-scope aspects → nearest + note (TBD).
  • Make port/fast3d/gbiex.h reachable from game TUs (include path or a shim header) — build-system only.
  • HUD alignment mode plumbing if we adopt PD's center/wide option (g_HudAlignModeL/R pattern) — decide in Phase 1; default = centered.

Game code (exception class — see below):

  • (a) Projection aspect path. bondviewMovePlayerUpdateViewport (bondview2.c:8007-8034): under PORT + 16:9, use window aspect instead of the ×4/3 stretch formula. Plus the menu/title/briefing call sites viSetFovY(FOV_Y_F); viSetAspect(ASPECT_RATIO_SD) (front.c — ~7+ sites; enumerate in Phase 1) → PD pattern viSetAspect(videoGetAspect()).
  • (b) HUD / screen-space draw sites. Audit + wrap with EXT aspect modes and/or per-element corrections, per surface: gameplay HUD (crosshair — replaces the existing ×0.75 compensation; ammo/timer/text via textrelated.c), menus (front.c), title/briefing/credits, letterbox bars (fr.c gDPFillRectangle calls). GE's known screen-space emitters: front.c, textrelated.c, gunfire.c, bondwalk2.c (initial grep; Phase 1 completes the audit). Expect PD-comparable scale (~10 files), possibly less.
  • (c) Anything the audit surfaces (e.g. mouse→screen mapping for menus, cutscene camera framing) — each item logged individually.

Rule #2 exception framing (needs explicit sign-off to start Phase 2)

(This project's now-generalized rule-2 sign-off procedure lives in docs/dev-process.md §7; this section is the original case that motivated it, kept for its PD-precedent argument.)

This is a behavior-affecting game-code edit class, outside the ABI-only D3x exception. The case for granting it:

  1. Direct sister-engine precedent: PD's port made exactly these edits on the same Rare engine/fast3d; the project designates PD as standing reference for port patterns. We replicate a proven, bounded change class, not inventing one.
  2. Bounded and gated: every edit is #ifdef PORT (and where sensible runtime aspect-gated) so the N64 build and the 4:3 default path stay byte-identical; golden/determinism tests unaffected at 4:3.
  3. The decomp already contains both a shipped 16:9 branch and #ifdef PORT precedents in game files — the pattern is not foreign to this tree.

Conditions: explicit user sign-off before Phase 2; each change (or tight cluster) gets a findings.md entry with §F index label citing the PD analogue; 4:3 byte-identity verified per phase; any edit that would alter sim behavior (not just presentation) stops the work and comes back for review.

Fallback (case-by-case, documented)

If the Phase 1 audit shows a specific surface is disproportionately costly or can't be made clean under B (candidate: complex cutscene framing), that surface may fall back to Option A — 4:3-internal render + edge-to-edge stretch using the game's shipped SCREEN_RATIO_16_9 path (set_screen_ratio is an existing API; FBO+blit infra exists) — recorded as a finding with the reason. Whole-feature fallback to A only if B fails verification overall.

FOV adjustment design (port-only, applies to both options)

  • Hook: software RSP projection post-scale. Wherever the RSP applies the uploaded projection matrix, scale its vertical row by tan(fov_orig/2) / tan(fov_adj/2). Zoom rewrites the matrix every frame, so post-scaling whatever arrives composes with aim zoom for free. No game code.
  • Rejected: editing viSetFovY call sites (game code); writing g_ViBackData from the port (racy).
  • UI: Video.FovScale (percent of original vertical FOV, e.g. 50–150), default 100 = byte-identical to today.

Phases

Phase 0 — Verification research (port-only, read-mostly)

  • Playtest the in-game Ratio option as-is on PC; document + screenshot what the shipped stretch path does under the current pipeline.
  • Measure baseline FOVs (4:3 and current 16:9-stretch) from the projection matrix or a calibration render; record numbers.
  • Confirm gbiex.h EXT commands round-trip through GE's RSP with a test DL (they should — decoder exists).
  • Audit EU-only aspect constants (g_GunSightAspectRatio, EU_CAMERA_8003642C_ASPECT) for NTSC/PAL effect.
  • Record a Dxxx finding consolidating baselines.

Phase 1 — Port foundation + full change audit (port-only)

  • videoGetAspect(), window/aspect policy, gbiex.h include path.
  • Complete the screen-space draw-site audit for inventory item (b): every emitter, its coordinate assumptions, proposed wrap/correction, PD analogue cited. Output = the signed change list for Phase 2/3.
  • Decide HUD alignment default (centered vs PD's center/wide option).

Phase 2 — Projection aspect (game-code exception class, small wave)

  • Item (a): camera path + front.c call sites, #ifdef PORT-gated.
  • Verify: world perspective correct at 16:9 (FOV measurement vs Phase 0), aim zoom composes, 4:3 byte-identical.

Phase 3 — HUD alignment wave (game-code exception class)

  • Item (b), per surface in order: gameplay HUD → menus → title/briefing/credits → letterbox/misc. Each surface: change, visual verification at 4:3 + 16:9, finding entry. Fallback-to-A decisions happen here, per surface, with documented reason.

Phase 4 — FOV slider (port-only) — LANDED (M-83, D211)

  • RSP projection post-scale (gfx_apply_fov_scale() in port/fast3d/gfx_pc.cpp, load-path only); Video.FovScale config (percent of vertical FOV, 50–150, default 100 = byte-identical) + F10 overlay row. Perspective-only (ortho/HUD untouched); columns 0+1 scaled together so aspect is preserved. Build clean, bunker1 no-op confirmed.
  • Owed: interactive eyeball of composition with aim-zoom at scale ≠ 100.

This phase does not depend on the Phase 2/3 game-code exception — it shipped standalone ahead of the rest of the plan.

Phase 5 — Verification & sign-off

  • FOV measurements per mode/scale vs Phase 0 baselines.
  • Full HUD pass at 4:3 + 16:9; cosmetics → GRAPHICS-BACKLOG.md.
  • 4K performance check; MSAA interaction; config persistence; screenshots.
  • ./build-pc.sh + /linkcheck; findings complete with §F index labels.

Open questions

  • HUD alignment UX: always-centered (simple) vs PD's center/wide option (more faithful, more surface area). Decide Phase 1.
  • In-game Ratio menu entry under Option B: the shipped option means "stretch mode" — keep it as the fallback trigger, hide it, or repurpose? Decide Phase 1 (leaning: leave dormant; port config owns display mode).
  • Mouse/menu coordinate mapping at 16:9 (PD had videoGetAspect()-scaled mouse math in menu.c) — confirm GE's menu input path needs the same.
  • Ordering vs UNLOCKED-FPS-PLAN.md: both parked behind audio; B renders native so the FPS plan's present-stage duplication slots in unchanged. Decide order when audio lands.
  • PAL: GE_NATIVE 640×400 bookkeeping — confirm aspect math per region.
  • Split-screen viewports under 16:9 — likely out of scope for PC; note and close.
  • M-87 QoL ask (user, not critical, backlogged): expose Video.FovScale as a real horizontal-or-vertical degree value instead of the current percent-of-original-vertical-FOV scale (D211) — the "50–150%" knob doesn't tell the user what degree FOV they're actually getting. Needs: pick a convention (report vertical, since that's what frFovY actually is post-D211 refix — fr.c:737) and either (a) relabel the F10 row + INI value as degrees directly (frFovY = clamp(requestedDeg, ~24°, 160°), drop the percent multiply), or (b) keep percent internally but show the computed degree value live in the F10 overlay/INI comment for transparency. (a) is cleaner and removes a level of indirection; check whether anything else reads Video.FovScale as a percent before renaming the key (search besides fr.c/video.c). Cosmetic/UX only, no render-path change beyond the input unit.