forked from Ports/goldeneye-pc-port
docs(dev): add live research/plan docs (D172, D176a, viewmodel, QOL, widescreen, unlocked-fps)
All referenced from HANDOFF.md / GRAPHICS-BACKLOG.md but untracked: - D172-PARTICLE-COLOR-RESEARCH.md: magenta/cyan blood root-cause candidate (unhandled GBI opcode 0xB9 in ROM particle records + D146 guard) - D176a-SKY-NOTES.md: RDPHALF sky-tri wire format decoded; Path B draft state - VIEWMODEL-RESEARCH.md: 1P weapon model static audit, three ranked suspects - QOL-INVENTORY.md / WIDESCREEN-FOV-PLAN.md / UNLOCKED-FPS-PLAN.md: parked Phase-4 proposals
This commit is contained in:
1 parent
efe477033f
commit
6736d6ee16
6 files changed
+749
No files matched your search
@@ -0,0 +1,100 @@
|
||||
# D172 — particle/flares wrong color (magenta/cyan blood): static research + root-cause candidate
|
||||
|
||||
Symptom (user-reported, GRAPHICS-BACKLOG line 31): bullet impacts / flares / blood spatter
|
||||
render magenta/cyan instead of dark red. Static audit done; one strong root-cause candidate
|
||||
with a cheap in-game verification. **No code changed.**
|
||||
|
||||
## 1. How particles get their color state (mechanism)
|
||||
|
||||
Particle triangles (`explosion.c`, `gSPVertex` + `G_TRI_FILL`/`G_TRI_SHADE_TXTR`) carry
|
||||
**no per-primitive material command**. Their CC/tile state comes from replaying ROM-resident
|
||||
state records via `gSPDisplayList(gdl++, g_ExplosionDisplayLists[var_s2])`
|
||||
(`explosion.c:894`, 15-record loop, one record + its particles per iteration).
|
||||
|
||||
- Records: `globalDL_0x078 … globalDL_0x9a8`, a 17-entry table at image-bank offset
|
||||
0x28 (`g_pc_gimg_off_globalDL_*`), bank base cart 0x1029D160 (see `image_bank.c:322`).
|
||||
- Each record is also scanned at load by `texLoadFromDisplayList` (`src/game/image.c:475`)
|
||||
for its `G_SETTIMG` + ABCD marker word — that part works (textures upload fine).
|
||||
|
||||
## 2. Record layout (decoded from ROM)
|
||||
|
||||
```
|
||||
+0x00 E7 C3 FF FE G_RDPPIPESYNC
|
||||
+0x08 B9 00 03 1D ??? <-- UNHANDLED by fast3d (see §4)
|
||||
+0x10 <varies> second word of the ??? command
|
||||
+0x18 FC ... G_SETCOMBINE w0 (rgb c0 | alpha c0 <<16)
|
||||
+0x20 FF ... G_SETCOMBINE w1 (rgb c1 | alpha c1 <<16)
|
||||
+0x28 FD ... G_SETTIMG w0 (timg pointer)
|
||||
+0x30 .. 50 50 50 50 G_SETTIMG w1 (ABCD marker)
|
||||
```
|
||||
|
||||
Dominant particle combine mode (14 of 17 records), decoded with the standard libultra
|
||||
bit layout:
|
||||
|
||||
- cycle 0 RGB: `TEXEL0, 0, TEXEL1, COMB_A`
|
||||
- cycle 0 ALP: `TEXEL0A, COMB_A, SHADE, COMB_A`
|
||||
- cycle 1 RGB: `COMBINED, 0, SHADE, COMB_A` ← **2-cycle** modulate
|
||||
- cycle 1 ALP: same as cycle 0
|
||||
|
||||
## 3. fast3d audit (what is right)
|
||||
|
||||
- `G_SETCOMBINE` bit extraction (`gfx_pc.cpp:2786`) matches the standard libultra layout
|
||||
exactly (verified field-by-field against the ROM words). Not the bug.
|
||||
- Store/decode of `rdp.combine_mode` (`gfx_dp_set_combine_mode` vs `gfx_generate_cc`
|
||||
key shifts) are consistent. Not the bug.
|
||||
- The generated GLSL combiner implements the RDP formula with the usual special cases;
|
||||
`SHADER_COMBINED` correctly resolves to the previous cycle's running value.
|
||||
|
||||
## 4. Root-cause candidate: the 0xB9 word aborts the sub-DL (D146 path)
|
||||
|
||||
fast3d's SP walker has **no case for opcode 0xB9** (full case list audited: MTX/MOVEMEM/
|
||||
VTX/DL/COL/syncs/all DP immediates/EXT commands — nothing in the 0x80–0xBF group). Its
|
||||
`default:` (`gfx_pc.cpp:2920`, the D146 guard) **ends the whole (sub-)display list** on an
|
||||
unknown opcode.
|
||||
|
||||
Consequence for every particle record replay:
|
||||
|
||||
```
|
||||
RDPPIPESYNC -> ok (no-op)
|
||||
0xB9 -> D146 default -> RETURN (sub-DL dead)
|
||||
G_SETCOMBINE -> never applied
|
||||
G_SETTIMG -> never applied
|
||||
<particles> -> drawn with STALE combine mode + stale tile/timg state
|
||||
```
|
||||
|
||||
Stale CC/tile state = arbitrary colors = the magenta/cyan symptom. On N64, gmain.s
|
||||
obviously handles 0xB9 (the game works there), so this is a fast3d gap, not a ROM quirk.
|
||||
|
||||
Secondary (only matters if 0xB9 turns out to be a no-op): particle 2-cycle rendering
|
||||
requires `G_CYC_2CYCLE` in other_mode_h (`gfx_pc.cpp:1523`); the records never set
|
||||
other-modes, so particles inherit ambient state — same on N64, but worth confirming if
|
||||
the primary fix doesn't resolve it.
|
||||
|
||||
## 5. Verification (cheap, in-game)
|
||||
|
||||
The D146 guard logs `D146: unknown GBI opcode 0x%02x ...` rate-limited to **20** messages.
|
||||
Run any level, shoot things / trigger flares, watch console output at startup for
|
||||
`unknown GBI opcode 0xb9`. If present → root cause confirmed. (The budget fills in the
|
||||
first frame or two of particles, so check early output.)
|
||||
|
||||
## 6. Fix path (next session)
|
||||
|
||||
1. **Decode 0xB9's semantics.** `ginit.s` (dispatch-table init) is *not in this repo*
|
||||
(`gmain.s:10` includes it; file absent). Options: (a) locate ginit.s in the original
|
||||
source / another checkout; (b) trace gmain.s's 0x80–0xBF group handler by hand;
|
||||
(c) infer from data — w0 is constant `B900031D` across all records, w1 varies
|
||||
(`0C184B50`, `00504B50`, …); if it's an other-modes/cycle-type setter, w1 bits should
|
||||
match G_MDSFT_* fields.
|
||||
2. Implement the handler in fast3d (port-only). Re-run: blood/flares should go dark red;
|
||||
D146 spam for 0xb9 stops.
|
||||
3. If colors still wrong after that: log `use_2cyc` + combine key on particle triangles
|
||||
(env-gated) and check ambient G_CYC_2CYCLE (§4 secondary).
|
||||
|
||||
## Files
|
||||
|
||||
- `src/game/explosion.c` — record table (165), replay loop (894)
|
||||
- `src/game/image_bank.c:322`, `src/game/image.c:475` — record offsets / load-time scan
|
||||
- `port/fast3d/gfx_pc.cpp` — SP walker cases (~2600–2930), D146 default (2920),
|
||||
SETCOMBINE decode (2786), combine store (2270), cc generation (312+), 2CYCLE flag (1523)
|
||||
- `rsp/graphics/gmain.s` — N64 ucode ground truth for 0xB9 (dispatch table at dmem 0xbc;
|
||||
ginit.s missing from repo)
|
||||
@@ -0,0 +1,85 @@
|
||||
# D176(a) Sky — consolidated research notes (read-only)
|
||||
|
||||
Companion to `GRAPHICS-BACKLOG.md` D176(a) and findings.md M-37/M-47.
|
||||
Purpose: make the next implementation session mechanical. No code changed by
|
||||
this doc.
|
||||
|
||||
## State of play
|
||||
|
||||
- **Root cause (M-37):** `skyRenderTri`/`skyRenderFull` emit geometry only as
|
||||
`G_RDPHALF_*` immediates; `gfx_pc.cpp:2901-2903` deliberately no-ops them
|
||||
(same in the PD port) → solid black sky on every cloud-sky level. Clean
|
||||
headless repro: `-level_22` (Statue), frame ~360.
|
||||
- **M-47 draft (Path B)** — `scratchpad/sky.patch`: `#ifdef PORT` hooks in
|
||||
both functions re-emit the sky as ordinary screen-space GBI via
|
||||
`skyPortRenderPoly` (ortho pixel projection + `gSPVertex`/triangles).
|
||||
Geometry reaches GL and textures (top-half luma 0.1 → ~174 on `-level_22`).
|
||||
Two open defects (below). Draft, not shippable.
|
||||
- **Path A** (decode RDPHALF in fast3d) is the backlog's original suggestion;
|
||||
the wire format is now fully decoded (next section), so it is a viable
|
||||
fallback if Path B's units can't be pinned.
|
||||
|
||||
## RDPHALF wire format (decoded from `src/game/sky.c` + `include/PR/gbi.h`)
|
||||
|
||||
- Opcodes (`gbi.h:102-164,216-223`): `G_RDPHALF_1 = 0xe1`,
|
||||
`G_RDPHALF_2 = 0xf1` (final word of a stream), `G_RDPHALF_CONT`
|
||||
(= `G_IMMFIRST-13`). Tri ops: `G_TRI_FILL 0xc8`, `G_TRI_FILL_ZBUFF 0xc9`,
|
||||
`G_TRI_SHADE_TXTR 0xce`, `G_TRI_SHADE_TXTR_ZBUFF 0xcf`.
|
||||
- **All payload values are S15.1 fixed point** — converter is
|
||||
`sub_GAME_7F094298` (`sky.c:218`): `clamp(x, ±32767.9) × 65536`, u32.
|
||||
- **Packing:** each `(G_RDPHALF_1, G_RDPHALF_CONT)` pair carries four
|
||||
halfwords = **two S15.1 values**: `valueA = (w1.hi16, w2.lo16)`,
|
||||
`valueB = (w1.lo16, w2.hi16)`. Multi-value groups are streamed in two
|
||||
passes — all high halves first, then all low halves (see the explicit
|
||||
`(x & 0xffff0000) | (y & 0xffff0000) >> 16` packing at `sky.c:1854-1870`).
|
||||
- **`skyRenderTri` header** (`sky.c:1725-1728`): first word =
|
||||
`(op << 24) | (winding/sign flag bit23) | lo16(first value)`; the stream
|
||||
then carries screen-space positions (`unk28×0.25` = pixel X, `unk2c` = Y×4
|
||||
family) plus per-vertex attributes. The RSP ucode reassembles and DMAs a
|
||||
real edge-walked RDP tri command (per the M-46 comment in sky.patch).
|
||||
- **Vertex attribute vectors are 8 components** (`sky.c:1759-1776`):
|
||||
`(r, g, b, a, S·w′, T·w′, 32767·w′, z)` with perspective weight
|
||||
`w′ = unk34 × min(unk0c)/2` — i.e. color + perspective-corrected S/T +
|
||||
depth. `SkyRelated38` fields: `unk28 = screenX×4`, `unk2c = screenY×4`,
|
||||
`unk30 = z (0..0x7fff)`, `unk34 = 1/w`, `unk20/unk24 = S/T`.
|
||||
- GE emits RDPHALF **nowhere except sky.c** — the command surface is closed
|
||||
and small.
|
||||
|
||||
## Defect 1 — texcoord shear (clouds smeared into streaks)
|
||||
|
||||
`skyPortRenderPoly` uses `tc = unk20 × 32.0f` — a magic number. GBI vertex
|
||||
`tc` is **S8.8 texel** space; the real RSP path applies the sky image's tile
|
||||
shifts to the streamed S/T. Fix spec:
|
||||
|
||||
1. Read the base-tile descriptor of the cloud image (`skywaterimages[0]`,
|
||||
GIMG offset `0xFB4` per M-36) from the extracted assets/sidecar — get W, H
|
||||
and shifts/shiftt.
|
||||
2. Cross-check against how `gfx_pc.cpp` addresses texels for a known-good
|
||||
textured DL (the working reference).
|
||||
3. Replace the magic: if streamed S/T are image fractions f ∈ [0,1), then
|
||||
`tc = f × (W << 8)` / `f × (H << 8)`. Verify on `-level_22`.
|
||||
|
||||
## Defect 2 — vertical coverage (band fills only top ~25%)
|
||||
|
||||
Leading hypothesis, from reading the N64 path: **the quad is not a plain
|
||||
corner quad.** The N64 stream's four vertices are *perspective-interpolated*
|
||||
points computed in the big interpolation block (`sky.c:1797-1813`, the
|
||||
`sp210/sp290/sp2b0/sp230` vectors), not the raw TL/TR/BL/BR corners that
|
||||
Path B's hook passes through. If those interpolated y-values differ from the
|
||||
corner ys (they will, wherever w varies across the quad), the re-emitted
|
||||
quad lands short.
|
||||
|
||||
Next steps (one focused session):
|
||||
1. Per-call probe on `skyPortRenderPoly`: log nverts + each vertex's
|
||||
(x, y) + the tri op-header word; A/B against N64 reference footage of
|
||||
`-level_22`/`-level_36`.
|
||||
2. If confirmed, either (a) replicate the interpolation block under PORT and
|
||||
emit the 4 computed vertices, or (b) drop to Path A with the decoder spec
|
||||
above (the ucode's own reassembly is then irrelevant — we decode the same
|
||||
values directly).
|
||||
|
||||
## Recommendation
|
||||
|
||||
Finish **Path B** first — geometry already works; both defects are unit/
|
||||
vertex-set questions answerable in one render-iterate session. Keep Path A
|
||||
as fallback; it is now fully specified by this doc if B stalls.
|
||||
@@ -0,0 +1,59 @@
|
||||
# QoL Inventory — PD-vs-GE options diff
|
||||
|
||||
Status: **read-only research, parked** (input to project Phase 4). No code
|
||||
changed. Companion to `WIDESCREEN-FOV-PLAN.md` and `UNLOCKED-FPS-PLAN.md`.
|
||||
|
||||
Method: diffed the config surface of both ports — every `configRegister*`
|
||||
key in `pd_port/port/src/*.c` vs `port/src/config.c` + the F10 overlay
|
||||
(`port/src/optionsoverlay.c`) + PD's `optionsmenu.c`. Principle applied:
|
||||
**where PD has a QoL feature that is achievable here, it is the standard;
|
||||
GE keeps its own extras.**
|
||||
|
||||
Classification key — **P** = port-only (no rules issues), **G** = needs a
|
||||
game-code exception (rule #2 sign-off, per `WIDESCREEN-FOV-PLAN.md` framing).
|
||||
|
||||
## GE already has (baseline — keep)
|
||||
|
||||
Input: mouse enable/capture/aim+turn speed/invert Y/Y scale/smoothing/raw
|
||||
input, aim band, hipfire pitch speed, menu pointer speed + mode (GE's menu
|
||||
cursor is injected entirely in `port/src/input.c` — no game code), pad
|
||||
deadzone/trigger %. Video: VSync, FpsCap, MSAA, TextureFilter,
|
||||
FixMipTextures, WrapFix, Fullscreen, Window geometry. Game:
|
||||
ScreenShakeIntensity. Debug: FrameDump, InputLog.
|
||||
|
||||
## PD has, GE lacks (candidates)
|
||||
|
||||
| Feature | PD location | Class | Effort | Notes / priority |
|
||||
|---|---|---|---|---|
|
||||
| **Key rebinding** (config-string binds per controller/action) | `input.c:69,627-675,1548` (`bindStrs`, `inputParseBindString`) | P | M | **High.** Explicit Phase 4 debt ("rebinding UI" — note PD has *no* UI either; config-file driven). Port-only in `port/src/input.c`. |
|
||||
| **SkipIntro** | `main.c:139` (STAGE_TITLE → STAGE_CITRAINING) | P | S | **High.** GE analogue: stage-override path already exists (`boss.c:385-393`, `g_StageNum = LEVELID_TITLE` at :98); wire a flag + arg. 10-min check on what feeds `tokenFindLevel`. |
|
||||
| **DisplayFPS** (+ interval) | config `Video.DisplayFPS(Interval)` | P | S | Cheap; useful for FPS-plan verification too. |
|
||||
| **Anisotropic filtering** (≤16×), MipmapFilter, TextureFilter2D | `gfx_opengl` texparam path | P | S–M | **High at 4K.** GE's `gfx_opengl.cpp` has mipmap nY but no aniso (`GL_TEXTURE_MAX_ANISOTROPY_EXT`). Port-only. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
| **CenterHUD** (0/1/2 = left/center/right) | `Game.CenterHUD` → `g_HudAlignModeL/R` | G* | — | Already scoped in `WIDESCREEN-FOV-PLAN.md` Option B (Phase 1 decision). Not a separate item. |
|
||||
| Per-player FovY + FovAffectsZoom | `Game.Player%d.FovY`, `g_PlayerExtCfg` | G | M | PD does this in game code. GE's port-only RSP FOV slider (widescreen plan Phase 4) covers the global case; per-player is a stretch goal needing the exception class. |
|
||||
| MemorySize (4–2048 MB emulated RAM) | `Game.MemorySize` → `g_OsMemSizeMb` | P? | S | Check how GE sizes its OS memory heap; if fixed, make it a config knob (stability lever for heavy levels). |
|
||||
| MaxExplosions / GEMuzzleFlashes / DisableMpDeathMusic | PD decomp edits | G | S | Low priority; rule-#2 cost > benefit. Skip unless asked. |
|
||||
| GlareBrightness / OverexposureScale / FramebufferEffects | PD post-FX | — | — | Not applicable: GE's fast3d has no framebuffer-effect path. Excluded. |
|
||||
|
||||
## GE has, PD lacks (keep as-is)
|
||||
|
||||
`Video.FixMipTextures`, `Video.WrapFix`, `Input.AimBand`,
|
||||
`Input.HipfirePitchSpeed`, `Input.MouseSmoothing`, `Debug.FrameDump`,
|
||||
`Debug.InputLog`. No action.
|
||||
|
||||
## Suggested Phase 4 order (after audio; ordering vs widescreen/FPS TBD)
|
||||
|
||||
1. **Quick wins (all P, small):** SkipIntro, DisplayFPS, aniso + mipmap
|
||||
filters, window HiDpi/center.
|
||||
2. **Rebinding** (P, medium) — the headline Phase 4 item; PD pattern is
|
||||
config-string driven, no UI required for parity.
|
||||
3. **Per-pad tuning** (P, medium).
|
||||
4. Niche: fake gamepads / HIDAPI (when MP gets played); MemorySize knob
|
||||
(after the heap check).
|
||||
|
||||
Everything here is parked behind audio like the other plans; nothing in this
|
||||
inventory implies an implementation decision yet.
|
||||
@@ -0,0 +1,176 @@
|
||||
# Unlocked FPS Plan
|
||||
|
||||
Status: **proposal, parked** — documented while audio work (project Phase 3) is
|
||||
active. Do not start Phase 0 until audio is done; first action when resumed is
|
||||
the Phase 0 measurement pass.
|
||||
Related: `docs/dev/findings.md` §D117 (GE_DETERM), §D155 (catch-up clamp),
|
||||
`port/src/libultra.c` (pacemaker), `src/game/frametiming.c`, `src/sched.c`.
|
||||
|
||||
## Decisions (agreed)
|
||||
|
||||
1. **Default preset = the original pace** — whatever the game currently does
|
||||
(measured ~30 fps; Phase 0 pins the exact rate). All testing, level
|
||||
sweeps, determinism runs (`GE_DETERM`), and playtest baselines stay on the
|
||||
default preset. Its code path must remain byte-for-byte what it is today.
|
||||
2. **Unlocked FPS (60 / 120 / uncapped) is EXPERIMENTAL.** It ships behind a
|
||||
clearly-labelled option and may be less polished than the default.
|
||||
3. **Hard requirement:** raising the presentation rate must never change game
|
||||
speed. Sim rate is invariant across presets. This is "the main speed-up
|
||||
issue" and it gates everything else.
|
||||
4. **Follow the PD PC port where feasible** (`pd_port` checkout): wall-clock
|
||||
accumulator pacing with remainder carry, vsync + framerate-limit options,
|
||||
safety cap when vsync is off.
|
||||
|
||||
## Non-goals (for now)
|
||||
|
||||
- Motion interpolation between sim ticks (the only thing that would make 120
|
||||
fps *look* like 120 fps). Requires per-entity prev/cur state in the
|
||||
DL-building game code — rule #2 territory, parked as a separate proposal.
|
||||
- Changing the sim rate itself (running logic at 120 Hz with halved constants
|
||||
is explicitly out — game logic).
|
||||
|
||||
## Background: why raising FPS today speeds up the game
|
||||
|
||||
One clock drives everything:
|
||||
|
||||
- `portTickThread` (`port/src/libultra.c`) posts a VI-retrace message on a
|
||||
free-running grid (60 Hz NTSC / 50 Hz PAL, `g_tickIntervalUs`).
|
||||
- `src/sched.c` `__scMain` blocks on that retrace; `__scHandleRetrace`
|
||||
notifies clients → **exactly one sim tick + one render per retrace**.
|
||||
- `osGetCount()` is wall-clock scaled to 46.5 MHz. `waitForNextFrame()`
|
||||
(`src/game/frametiming.c`) converts elapsed time to `deltaFrames`, which
|
||||
becomes `g_ClockTimer` (`src/game/lv.c:1008`) driving the per-tick sim loops.
|
||||
|
||||
The quantization is the trap: `nextFrameTime = (elapsed + Q/2) / Q` (Q = one
|
||||
frame quantum). The `+Q/2` bias **rounds every interval up to at least 1
|
||||
tick**, and `waitForNextFrame` spins until ≥1 tick is due. Consequence:
|
||||
**sim speed tracks the retrace rate almost 1:1** — 30 Hz retrace → 30 ticks/s,
|
||||
120 Hz retrace → ~120 ticks/s (each 8.3 ms interval rounds up to a full
|
||||
tick). That is exactly the observed "anything faster speeds up the game".
|
||||
|
||||
Two useful existing properties:
|
||||
|
||||
- GE's game code already handles `g_ClockTimer == 0` and multi-tick deltas
|
||||
everywhere (`for (i = 0; i < g_ClockTimer; i++)`, explicit `== 0` guards in
|
||||
bondhead/bondview2/propobj/explosion). The N64 used this for dropped frames.
|
||||
- D155 already clamps catch-up to 6 ticks after a real-time hitch.
|
||||
|
||||
## What the PD port does (reference)
|
||||
|
||||
- `src/game/timing.c` `frametimeCalculate()`: wall-clock accumulator with
|
||||
remainder carry (`lostframetime60t`) → `diffframe60` / `diffframe240` ticks
|
||||
due **per rendered frame**; render loop runs at display rate.
|
||||
- Vsync + framerate limit options (`VIDEO_MAX_FPS` 200/240); vsync-off with
|
||||
no cap forces the safety cap.
|
||||
- Feasibility mapping to GE:
|
||||
- *Pacing math (accumulator + carry)* → adoptable **port-only**. ✓
|
||||
- *Render frames carrying zero sim ticks* → blocked in GE by
|
||||
`waitForNextFrame`'s ≥1-tick enforcement (`frameDelay`, only settable
|
||||
under `LEFTOVERDEBUG`). A crafted `osGetCount()` cannot defeat it (the
|
||||
spin + `+Q/2` bias always yield ≥1). Needs a game-code change → **parked**
|
||||
(see "Parked" below), and note it buys little without interpolation.
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 0 — Baseline & ground truth (port-only)
|
||||
|
||||
Goal: know exactly what the default preset does before touching anything.
|
||||
|
||||
- [ ] Measure actual sim rate and present rate of the current default build
|
||||
(speedgraph + a `video.c` counter; add a periodic log line
|
||||
`sim=..Hz render=..Hz`). Confirm NTSC vs PAL build.
|
||||
- [ ] Find why the default is ~30 rather than the console's 60 Hz. Candidates:
|
||||
- tick-thread 4 ms chunked-sleep drift under host load;
|
||||
- `__scTaskReady` double-buffer check (`sched.c`) stalling a task every
|
||||
other retrace;
|
||||
- `__scHandleRetrace`'s client flag read `*((s32*)client + 2)` is
|
||||
**out-of-bounds of `OSScClient`** (struct is only `next`+`msgQ`; the
|
||||
decomp even says "im wrong size"). For `gfxClient[0]` it happens to
|
||||
read `gfxClient[1].next` (zero) — but this is layout luck, worth a
|
||||
documented finding either way.
|
||||
- [ ] Record a Dxxx finding with the measured rates and root cause.
|
||||
- [ ] **Decision gate:** if 30 fps turns out to be a port artifact rather than
|
||||
original behavior, document it and decide with data whether the default
|
||||
preset keeps the current pace (test continuity) or is fixed. Default
|
||||
assumption: keep current behavior as the "original" preset either way;
|
||||
a fix becomes its own item.
|
||||
|
||||
### Phase 1 — PD-style pacing hardening (port-only)
|
||||
|
||||
Makes the default preset stable and drift-free; no game-code changes.
|
||||
|
||||
- [ ] Replace the free-running tick grid in `portTickThread` with a
|
||||
wall-clock accumulator + remainder carry (PD `frametimeCalculate` math):
|
||||
post ticks when real time says they're due, carry the remainder, bounded
|
||||
catch-up after hitches (D155 philosophy). No drift under load, no burst
|
||||
of back-to-back ticks.
|
||||
- [ ] `video.c`: proper frame limiter for fractional caps (spin+sleep hybrid),
|
||||
triple-buffered swap chain, and the PD safety rule (vsync off + no cap →
|
||||
force a max cap so "unlocked" can't spin).
|
||||
- [ ] Config: keep `Video.VSync` / `Video.FpsCap`; add whatever Phase 2 needs.
|
||||
|
||||
### Phase 2 — Experimental presentation rates (port-only)
|
||||
|
||||
The user-visible "60 / 120 / unlocked" feature. Game speed is invariant **by
|
||||
construction**: sim + render stay on the original tick clock; only *present*
|
||||
runs faster by showing each rendered frame more than once.
|
||||
|
||||
- [ ] Frame duplication at the present stage (`video.c` / fast3d SDL swap
|
||||
path, near `gfx_pre_swap_hook`): after a fresh frame is presented,
|
||||
re-present the last finished frame on extra vsyncs until the next sim
|
||||
tick's frame is ready. Target rates: 60, 120, uncapped (safety-capped à
|
||||
la PD `VIDEO_MAX_FPS`).
|
||||
- [ ] Input at present rate: sample SDL input continuously in
|
||||
`port/src/input.c`; each sim tick consumes the latest state (compare
|
||||
against PD `joy.c`). This is the real tangible win of the feature: up
|
||||
to ~half a frame of latency removed even without interpolation.
|
||||
- [ ] Options menu entries, clearly labelled **experimental**
|
||||
(`port/src/optionsoverlay.c`), default preset untouched.
|
||||
- [ ] Audio check: `src/audi.c` uses real-time `osGetTime()` and its own
|
||||
thread; libaudio buffer sends ride the sim/retrace clock. Verify nothing
|
||||
divides by an assumed frame rate; expect no changes.
|
||||
|
||||
### Phase 3 — Verification & sign-off
|
||||
|
||||
- [ ] `GE_DETERM` regression: default preset simulation path byte-identical to
|
||||
pre-change (existing determinism diffs keep passing).
|
||||
- [ ] Preset-invariance playtest: a timed in-game event (e.g. a fixed-length
|
||||
animation or timer) takes the same real time on default / 60 / 120 /
|
||||
unlocked. This is the acceptance test for decision #3.
|
||||
- [ ] Hitch recovery: force a multi-second stall (debugger pause / asset load),
|
||||
confirm D155 clamp holds and no speed spiral on any preset.
|
||||
- [ ] `./build-pc.sh` + `/linkcheck` ritual; new findings in §F/§H with index
|
||||
labels.
|
||||
|
||||
## Timing-consumer audit (each gets a line in the finding)
|
||||
|
||||
| Consumer | Clock class | Action |
|
||||
|---|---|---|
|
||||
| `src/game/frametiming.c` (`waitForNextFrame`, `updateFrameCounters`) | sim-time | unchanged; stays tick-locked |
|
||||
| `src/boss.c:403,517` (main-loop gate, `MAIN_LOOP_TICK_INTERVAL`) | sim-time | unchanged; re-verify vs new pacemaker |
|
||||
| `src/usb.c:387,405` (LibDragon flash timeouts) | real-time | fine as-is; document |
|
||||
| `src/speed_graph.c` | measurement | fine; source of the rate log line |
|
||||
| `libultrare/audio/env.c`, `reverb.c` `lastCnt` | perf counters only | fine; document |
|
||||
| `src/audi.c` `g_DeltaTime` etc. (`osGetTime`) | real-time | fine; verify in Phase 2 |
|
||||
|
||||
## Parked (needs explicit sign-off, separate proposals)
|
||||
|
||||
1. **True decoupled sim/render (PD's zero-tick render frames).** Minimal
|
||||
change class: relax `waitForNextFrame`'s ≥1-tick enforcement so rendered
|
||||
frames can carry `deltaFrames = 0` (GE already guards `g_ClockTimer == 0`
|
||||
everywhere). This is a *behavior-affecting* game-code edit gated off by
|
||||
default — outside the ABI-only D3x exception, needs its own rules decision.
|
||||
Perceptual benefit over Phase 2 duplication is small **without**
|
||||
interpolation, so do this only as a prerequisite for #2.
|
||||
2. **Motion interpolation** (prev/cur entity state, lerp at render time).
|
||||
The actual "120 fps looks like 120 fps" feature. Large, invasive, in
|
||||
DL-building game code. Separate proposal; not implied by this plan.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Phase 0 outcome: is the ~30 fps default a port artifact? (fix vs keep — see
|
||||
decision gate)
|
||||
- PAL builds: default stays 50 Hz pace; experimental present rates identical
|
||||
option set. Confirm no PAL-specific pacing assumptions in Phase 1 math.
|
||||
- Naming/caps for "unlocked": adopt PD's safety-cap value (200/240) or expose
|
||||
the cap as a slider?
|
||||
@@ -0,0 +1,88 @@
|
||||
# 1P weapon viewmodel — static research (read-only)
|
||||
|
||||
Companion to `GRAPHICS-BACKLOG.md` ("1P weapon viewmodel") and findings D115
|
||||
item #5 / AUDIT-M6 #5. Goal: narrow the five audit suspects to a ranked,
|
||||
probe-ready list. No code changed by this doc.
|
||||
|
||||
## Path map (verified in source)
|
||||
|
||||
- **Setup (per frame):** `bondview2.c:9040` → `gunUpdateAndFireBothHands()`
|
||||
(`gunfire.c:1018`) → `gunUpdateAndFire(handnum)` (`gunfire.c:327`). Inside,
|
||||
the weapon-model block at `gunfire.c:599-662`: computes `field_87F` (the
|
||||
show/hide gate), then if set: `dynAllocate(numMatrices × sizeof Mtxf)` →
|
||||
identity-fill → `modelCalculateRwDataLen` → `modelInit(HAND_WEAPON_MODEL,
|
||||
©_of_body_obj_header[handnum], HAND_WEAPON_RWPOOL)` →
|
||||
`hand->mtxlist = rwmtx` and (PORT/D102)
|
||||
`weaponModel.render_pos = (RenderPosView*)rwmtx`.
|
||||
- **Render (same frame, later):** `gunRenderFirstPersonGunModels` loop at
|
||||
`gunfire.c:1532-1540`: **`if (handptr->field_87F == 0) continue;`** then
|
||||
`subdraw(&renderdata, HAND_WEAPON_MODEL(handptr))` (`model.c:5326`) which
|
||||
does `gSPSegment(gdl, 3, osVirtualToPhysical(mdl->render_pos))` and walks
|
||||
`mdl->obj->RootNode` — **a NULL RootNode returns silently, drawing
|
||||
nothing**.
|
||||
|
||||
## Statically ruled out (with evidence)
|
||||
|
||||
1. **Transient-arena lifetime** (the audit's prime suspect). `dynAllocate`
|
||||
is a bump allocator on the vtx pool (`src/game/dyn.c:123`), reset only by
|
||||
`dynSwapBuffers()` at frame end. Setup and render both happen inside one
|
||||
frame, so `render_pos`/`mtxlist` are valid when consumed — in solo play.
|
||||
(Split-screen mid-frame swaps would be a separate question; out of scope.)
|
||||
2. **Word-index / record-size mismatch** (`flashvisptr = RWPOOL +
|
||||
RwDataIndex`, `gunfire.c:650`). D52 kept `Model.datas` as `u32*` on PC
|
||||
precisely so N64 word math holds, and `RwDataIndex` values are computed
|
||||
at runtime by `modelCalculateRwDataIndexes` from the *PC* record sizes —
|
||||
self-consistent. (Self-consistent ≠ in-bounds; see suspect 2.)
|
||||
3. **Segment/render_pos mechanism itself.** Every working model in the game
|
||||
(bodies, props) uses the same `gSPSegment(3, render_pos)` +
|
||||
`bondviewTransformManyPosToViewMatrix` machinery — and it renders. The
|
||||
**watch menu draws the same weapon models** via a different path
|
||||
(`set_enviro_fog_for_items_in_solo_watch_menu`, `gunfire.c:1630+`, local
|
||||
matrices, no 87F gate, no hand rw-pool) and they appear (albeit faint —
|
||||
D75/D141 note). So the model assets and headers are very likely fine; the
|
||||
defect is in-level-path-specific.
|
||||
|
||||
## Ranked suspects + probe spec
|
||||
|
||||
**#1 — `field_87F` gating (`gunfire.c:599-610`).** The clear-condition
|
||||
disjunction contains `(Gun_hand_without_item(handnum) == 0)`. That function
|
||||
(`gun.c:812-816`) returns `hand_invisible[h] > 0 || (hand_item[h] == 0 &&
|
||||
field_2A44[h] < 0)` — so the term is TRUE (→ gate cleared → gun hidden)
|
||||
whenever the hand *has* an item and isn't invisible. Read naively, that
|
||||
hides the gun exactly when you're holding one; since the N64 shows the gun,
|
||||
the runtime values of `hand_item`/`field_2A44`/`hand_invisible` must differ
|
||||
from their names suggest — but this is the single most likely place for a
|
||||
PC-only divergence (e.g. a field the port initializes differently). If 87F
|
||||
is 0, the render loop `continue`s and the symptom is exactly "no gun, no
|
||||
crash".
|
||||
|
||||
**#2 — rw-pool capacity.** PC records are wider than N64's (pointer fields);
|
||||
`weaponRwPool[192]` (768 B) was D102's scaled-up budget, but nothing bounds-
|
||||
checks it. The `#ifdef DEBUG` print at `gunfire.c:620` still tests the *N64*
|
||||
32-word threshold — stale on PC. If `numRecords` words exceed 192,
|
||||
`modelInitRwData` overruns the pool into whatever follows it in `struct
|
||||
hand` → corruption that could kill the draw (or worse, silently).
|
||||
|
||||
**#3 — stale `copy_of_body_obj_header`.** Populated only at item load
|
||||
(`gun.c:924` shallow copy / `gun.c:931-951` `load_object_fill_header`). If a
|
||||
PC asset-load quirk zeroed it for the in-level path, `RootNode == NULL` and
|
||||
`subdraw` no-ops silently. Lower priority given the watch menu renders the
|
||||
same models — but cheap to confirm in the same probe.
|
||||
|
||||
**Probe (one session, PORT-only, env-gated `GE_DVM=1`, zero behavior
|
||||
change):** in the `field_87F != 0` block at `gunfire.c:611+`, print once per
|
||||
frame while a pistol is held in BUNKER1: `handnum`, `item`, each clear-term
|
||||
value + final `field_87F`, `©_of_body_obj_header[h]`, `numMatrices`,
|
||||
`numRecords` (words) vs 192, `RootNode`, `rwmtx`. One run triages all three
|
||||
suspects: 87F==0 → suspect #1 (log which term); numRecords>192 → #2;
|
||||
RootNode NULL/garbage → #3.
|
||||
|
||||
## Fix-shape notes (for when the probe lands)
|
||||
|
||||
- If #1: the fix is a `#ifdef PORT` initialization/correction of whichever
|
||||
hand field diverges — **not** an edit to the gate logic itself (rule #2);
|
||||
document as Dxxx with the N64-vs-PC value table.
|
||||
- If #2: enlarge `weaponRwPool` to the measured max + a PORT bounds guard
|
||||
that prints (mirrors the existing DEBUG print, correct threshold).
|
||||
- If #3: trace the `load_object_fill_header` call for the in-level weapon
|
||||
load; likely an asset-offset issue in the port's loader path.
|
||||
@@ -0,0 +1,241 @@
|
||||
# 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`).
|
||||
|
||||
## 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 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)
|
||||
|
||||
- [ ] RSP projection post-scale; `Video.FovScale` config + F10 overlay;
|
||||
verify composition with aim zoom at non-100 scale.
|
||||
|
||||
### 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.
|
||||
Reference in new issue
Block a user