Files
goldeneye-pc-port/docs/BACKLOG.md
T
James Dansereau 57e24a374b M-28: watch-menu text (D150/D151) + audio-lock deadlock mitigation (D152)
D150 (src/str.c) — NULL-tolerant strcpy/strncpy/strcat under #ifdef PORT.
The watch BRIEF/OBJECTIVES pages build text with strcat(buf, langGet(id));
langGet() returns NULL on PC for a string bank the in-level flow never
loaded, and str.c's primitives are __nonnull__ builtins so a plain param
NULL check is optimised away — the guard launders the ptr through an empty
asm (GE_IS_NULL). NULL src -> empty string instead of an AV crash.

D151 (src/bondtypes.h) — watch data (objectives / mission background / M /
Q / Moneypenny) all rendered blank. propDef types 35 (WatchMenuObjectiveText)
and 23 (ObjectiveStart) store a plain s32 lang slot id in their 3rd word;
the decomp structs decode it as [u16 reserved][u16 text] reading text at
offset 0xA, which only works because the id is in the low 16 bits of a
big-endian word. d88_propdefs.py bswap32's the whole word -> text @0xA
reads 0 -> langGet(0) -> NULL. Fix: text is a full u32 at the word offset
under #ifdef PORT (N64 layout kept under #else). No converter change, no
sidecar regen. This is the D143/D150 "briefing text renders blank" root cause.

D152 (port/src/libultra.c) — mission-failed / objective-failed permanent
black screen. The D147 recursive-mutex behind osSetIntMask can be leaked
(libaudio has unbalanced / early-return mask calls; a transient thread can
acquire OS_IM_NONE and exit) -> mainThread + amMain both block forever in
alEvtq* -> gfx pipeline stalls. Live gdb.txt from the user's session
confirmed the deadlock (mainThread in the mission-fail audio fade-out
sndSetScalerApplyVolumeAllSfxSlot -> alEvtqPostEvent). Mitigation: model
the mask as a self-healing logical lock (owner/depth under s_imMx + condvar)
— normal microsecond critical sections behave as before, but a waiter
blocked > 2s concludes the holder leaked it, logs the stale owner + the
stealing caller's return address, and steals the section. Worst case is a
~2s audio hiccup + a log line naming the leak, instead of a hung game.
Proper narrow fix (dedicated ALEventQueue lock) still owed.

D153 investigated and dismissed — the "-level_09 crashes at frame ~900"
seen while regression-testing was D117/D134 host-scheduling flakiness under
heavy concurrent build+run load; on a quiet machine -level_09 runs past
frame 2400 clean with all three changes applied.

Build: green (ntsc-final). Golden framediff not run clean (same machine-load
flakiness); one quiet -level_09 run = 2400+ frames, 0 heartbeats.
2026-08-30 20:25:02 -04:00

303 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
All of these items should be completed once the main original game is playable from start to finish on PC with no crashing or major issues.
---
## Playtest defects — logged M-28 (2026-08-30), not yet worked
User-reported during free playtest. Suggested priority order below (cheap
input fixes first, then the texture correctness bug, then the filtering
feature).
- **B1 — texture filtering / "interlaced" shimmer (feature).** Whole game
has an interlaced/shimmering look. Want N64 3-point-filter emulation +
bilinear/trilinear handling like the Nightdive / Quake-remaster ports.
MVP: one good default filtering mode in `port/fast3d/` (3-point emu
shader or clean bilinear). Long-term: full toggle set (see "PC graphics
settings" → Quality + stretch goals below). Biggest of the four.
- **B2 — glitchy textures / wrong colors (correctness bug).** Some
textures render with wrong colors / garbage — **very noticeable on
Depot (`-level_30`)**, present in many other spots too. Likely a
texture-decode/format bug (CI/TLUT, RZ decode `port/src/rzdecomp.c`, or
the `import_texture_*` path in `gfx_pc.cpp`). Distinct from B1. Needs
investigation — could be a real fidelity blocker, not cosmetic-parked.
- **B3 — RMB aim far too mouse-sensitive.** Manual-aim (RMB/LShift) mouse
sensitivity is way too high. `Input.MouseAimSpeed` (default 50) still
overshoots badly. Quick fix: lower default + recheck the aim-band curve
in `port/src/input.c` (the 61..80 stick push added M-24). Port-only.
- **B4 — menu crosshair-cursor screens: PC input is wrong.** Menu screens
that use the crosshair cursor don't behave like a PC pointer. The
original front-end was PC-style "select a file with the mouse" — menu
input should be a **separate mode from in-game input**, not the same
analog-stick emulation. Needs a dedicated menu-pointer path in
`port/src/input.c` (absolute cursor → menu hit-test) rather than
feeding synthetic stick deltas. Port-only.
- **B5 — no game save on level completion.** On the OG cartridge, finishing
a level writes progress to EEPROM (unlocks next mission, records
time/difficulty). On PC nothing is saved — progress is lost on quit.
This is **Phase 4** (file-backed EEPROM): `osEepromWrite/Read` are
currently no-op stubs; build on PD's `fs.c` (file-backed save dir). The
save *logic* already runs in the decomp — it just needs real backing
storage. Blocks any multi-session campaign playthrough.
### PC graphics settings
Optional video/rendering options, modelled on the PD port (baseline) with
Quake 1 remaster / Nightdive-style extras as stretch goals. **Every option
defaults to the current behavior** — zero visual delta at defaults.
**Starting point (verified):**
- `port/src/config.c` exists but is minimal (58 lines: Int + String only;
PD's is 300 lines with Float/UInt, clamping, INI sections). Copy-and-adapt
from `pd_port/port/src/config.c`.
- `port/src/video.c` is a trimmed port of PD's (250 vs 588 lines). Render path
is architecturally identical to PD: native-res FBO (GE = 640×480 NTSC LAN1)
→ scaled blit to window, so all PD display options port over cleanly.
- No `#ifdef PC` blocks exist in `src/` yet; only the documented ABI/layout
exception edits (D3x) are present. Anything below that changes *behavior*
(FOV, HUD placement, menu contents) is a policy decision — see route notes.
**Baseline (PD parity)** — each item cites its PD source:
1. Config system: full `configRegister{Int,UInt,Float,String}` + clamps
(`pd_port/port/src/config.c`).
2. Window/display (`video.c` + SDL): fullscreen checkbox, fullscreen mode
(borderless/exclusive), maximize, center window, resolution dropdown
(display-mode list + "Custom"), HiDpi toggle.
3. Frame pacing: vsync (Adaptive/Off/On/N-frames via `SDL_GL_SetSwapInterval`),
framerate cap slider (PD caps FPS when vsync is off — keep that guard).
4. Quality: MSAA Off/2x/4x/8x/16x (FBO `samples` — **verify our FBO path
accepts samples>0 first**; PD reads it back via `videoGetMSAA`), texture
filter Nearest/Bilinear, anisotropic 1–16 slider, mipmap filter,
on-screen FPS display toggle.
5. **FOV slider (baseline per owner decision).** PD does this game-side
(`Game.PlayerN.FovY`, `g_PlayerExtCfg` in their `main.c` + decomp edits).
Two routes — decide in a spike:
- **(a) Port-layer:** override the camera projection in our fast3d where
the view matrix/projection is consumed. No src changes; must find the
exact injection point (camera math flows through game GBI setup).
- **(b) PD-style port-only `src/` edit** reading a port-provided value.
This would be the first *behavioral* src edit — exceeds the current
ABI/layout exception in AGENTS.md #2; if chosen, expand the documented
exception ("PC-port feature hooks, opt-in via config, default = original
behavior") and record a Dxx finding.
6. **Widescreen handling:** non-4:3 window aspect → stretch vs pillarbox
choice, plus HUD centering (None/4:3/Wide à la PD `Game.CenterHUD`). PD
implements the HUD part with `G_ASPECT_*_EXT` extended GBI mods set
game-side; for us that's either a fast3d viewport intercept of the HUD
quads (route a) or a port-only src edit (route b). Same policy note as FOV.
**In-game video options menu:** v1 = config file (+ CLI flags if trivial).
PD's in-game menu (`pd_port/port/src/optionsmenu.c`, 2042 lines) hooks into
*their* decomp's menu tables with port-side handlers; GE's menu code differs,
so it's an adaptation, not a copy. Feasible under route (b) policy; revisit
after the config-file v1 works.
**Stretch goals (Quake 1 remaster / Nightdive):**
- Internal resolution scale: render at 2x/4x native then downscale (free
supersampling AA, big fidelity win on modern displays).
- FXAA post-process toggle (cheap AA alternative to MSAA).
- SSAO + bloom / light-shaft post-processes — Quake Enhanced ships each
enhancement as an individually-toggleable option; keep that pattern (one
toggle each, all default off, shared post-FBO stage with FXAA/colorblind).
- Integer-scaling option.
- Texture upscaling filter (ESRGAN-style or pre-generated upscales) — the
biggest item; do last. Note GE textures are RZ-compressed at load
(`port/src/rzdecomp.c`) — an upscale pass would hook there or post-decode.
**Acceptance:** every option persists via config file; toggles apply live
where PD's do; `tools_pc/golden/` dumps byte-identical at all-defaults;
no `src/` changes unless route (b) is explicitly chosen and documented.
### LAN deathmatch / multiplayer — the netplay foundation (do first)
**The idea:** play GE's *existing* competitive multiplayer (deathmatch /
mission-based, 2–4P) across up to **4 instances** of the PC port on a local
network. This is the **nettech proving ground**: the game logic already
exists in the decomp (original N64 feature — no new mode to invent), so this
item isolates and de-risks the entire networking layer. Co-op (below) bolts
onto whatever this produces.
**Milestone 0 (prerequisite, real port work):** verify GE's *existing*
split-screen multiplayer works locally on PC — two cameras, split viewport,
two independent input streams, full match lifecycle. Every code path netplay
will drive must first work on one box.
**Net model (research item — owner leans client-server):**
- Leading candidate: **host = server + player** (Source-engine style): host
runs the authoritative sim, clients send inputs and receive state. Friends
connect by manual IP entry on a LAN. Self-healing desync, one machine owns
truth.
- Cost to size up: a full per-tick **state snapshot format** (players,
entities/props, AI, doors/props) must be defined from scratch — no existing
save/replay infra covers frame state (EEPROM saves are mission-level only).
- Alternative for the research phase: **lockstep/deterministic** (exchange
~10 bytes of input per 60Hz tick, both machines run the full sim). Far less
code *if* the sim is bit-deterministic across hosts — PRNG is ported
verbatim (`port/src/random.c`, good), but host-scheduling nondeterminism is
a known risk (see D24-implications note below). Desync = hard stop.
A per-tick state-hash desync detector is cheap insurance either way.
- PD port has **zero networking code** — nothing to copy; the net layer is
new (`port/src/net.c`-style, SDL_net or plain sockets).
**Shape of the work under host-authoritative:** generalize to host + N clients
(N≤3), per-client input channels, state snapshots fanned out to all clients.
The host already runs the full sim, so scaling past 2P is mostly protocol
fan-out — which is why 2P is a natural intermediate milestone on the way to
the 4-instance bar.
**v1 bar:** LAN only, manual IP entry (host listens / client connects),
deathmatch playable; 2P first, then scale to 4 instances. No matchmaking,
discovery, reconnect, or internet play. Stretch: ping display, graceful
reconnect-on-drop, mission-based (non-deathmatch) multiplayer variants,
remote-player model interpolation between snapshots (Quake Enhanced ships
it; hides LAN jitter).
### Co-op (new mode — built on the deathmatch net layer)
**The idea:** two players progress the single-player campaign levels
together — locally on one PC, and over LAN with each player on their own PC.
Sequenced **after** LAN deathmatch: reuse its connection handling, snapshot
format, and input channels; this item adds the mode itself.
**Key fact (defines the scope): GE has no co-op mode.** Unlike PD, GoldenEye
only ships split-screen *competitive* multiplayer. Co-op on solo levels exists
in **PD only** — so this is a net-new game mode: mapping/level and GE game-
code feature work, on top of the (already-existing) networking plumbing.
Consequences:
- Non-negotiable #2 ("game logic unmodified") is about not altering *existing*
behavior; adding a new mode is a separate, explicit owner decision. Record
the policy call when work starts.
- `pd_port/` is the reference implementation: PD's decomp shows how two-player
co-op was wired into this same Indy engine (shared objective progression,
both players' AI/enemy interaction, win/lose conditions, split-screen
presentation). Expect an adaptation study, not a copy — GE's mission/
objective code differs file-by-file.
- Cheaper fallback worth considering during design: "freeplay" co-op (both
players active on a solo level, but only P1's objective progress counts /
or objectives are duplicated per player). Much less new logic; decide after
the PD study.
**v1 bar:** LAN + local 2P co-op on solo levels over the deathmatch net layer.
No matchmaking, discovery, reconnect, or internet play.
### Fun features (remaster research: Quake 1 Remaster / Nightdive)
Feature mining from the Quake 1 Remaster (id, 2016), Nightdive's remaster
line (Duke Nukem 3D: Forever, DOOM, System Shock), and — round 2,
2026-08-29 — Quake Enhanced, Quake II Enhanced, Turok Resurrection, Doom 64
Definitive Edition, SiN Reloaded. Pattern across all of them: classic
fidelity as default + a shelf of opt-in QoL/accessibility/asset options.
Tiered by effort; all stay in `port/` unless noted.
**Cheap wins (config toggles over existing infra):**
- **Screenshot hotkey.** Productize the existing debug dump:
`gfx_opengl_dump_bound_fbo()` (`port/src/video.c:199`) is already there —
wire a keybind + configurable output dir, timestamped filenames. Every
Nightdive title has this; ours is ~an afternoon.
- **Screen-shake intensity slider (0–10x).** All shake funnels through
`viShake()` (`src/game/bondview2.c:8300` et al.) which lands in our port
shim — scale amplitude there, zero src changes. PD precedent:
`Game.ScreenShakeIntensity`.
- **No hit-flash / damage-indicator toggle.** The red full-screen flash on
taking damage is a top community-mod option; Nightdive ships it. Impl: find
how the flash overlay is drawn (likely a full-screen HUD quad) and intercept
in the port layer, or route-(b) one-liner.
- **Auto-pause / input-hold on window focus loss.** SDL focus events; trivial,
prevents drive-by deaths when alt-tabbing.
- **Controller polish (copy from PD `input.c`):** per-player gamepad
assignment (`Input.ControllerIndex`), per-stick deadzone/sensitivity sliders,
rumble scale + rumble mapping to GE's feedback moments. Note `osMotor*`
(Rumble Pak) is currently stubbed — accessory detection only
(PCPortResearch §4) — so wiring it to `SDL_GameControllerRumble` is part of
this item; Doom 64 DE shipped haptic feedback (a KEX first). We have basic
pad support (D118); this is the remaster-grade layer on top.
- **Tick-rate / framerate options.** PD ships `Game.TickRateDivisor` +
framerate cap. Flag: any tick-rate change must be reconciled with the future
netplay determinism requirement before shipping as a user option (lock it to
60Hz default, document).
- **Fog / draw-distance option.** Turok's KEX port removed the N64's heavy
distance fog; Quake II Enhanced added height-based fog with custom colors.
fast3d already decodes fog generically (PCPortResearch §5) — a config
slider scaling/overriding fog density, default = original values. Zero src
changes.
**Medium:**
- **High-res / user texture packs (the big goodwill item).** Quake 1 Remaster's
core feature is swappable asset sets (classic vs remastered toggle). GE
textures are RZ-compressed and decoded at load (`port/src/rzdecomp.c`) — a
per-texture override lookup against a user directory (fall back to ROM) hooks
there. Enables the community to make packs; also our escape hatch for the
blurry-asset complaints without touching fidelity defaults.
- **Accessibility: colorblind post-process filters** (protanopia/
deuteranopia/tritanopia full-screen shader pass) + the screen-shake slider
above. Nightdive includes these; cheap once we have a post-process stage
(shared infra with FXAA from the graphics item — sequence together).
Quake II Enhanced also shipped high-contrast / readability UI options —
add where the port layer can reach (bitmap HUD text limits scope;
document what's not doable under route b).
- **Crosshair customization** (size/opacity at minimum; GE's crosshair is
game-drawn — check whether it's a textured quad we can scale in the port
layer before assuming route b).
- **Widescreen FOV compensation** — already baseline in the graphics item;
listed here because it's the feature every remaster ships and users will
expect.
**Big / stretch:**
- **Demo / replay record + playback.** Record the input stream (+ initial
state), play back headlessly. Quake tradition; Nightdive titles have
replay-ish tooling. **Strong synergy with netplay work:** a working replay
system *is* a determinism test harness — if a recorded demo replays
bit-identically, lockstep netplay's hardest assumption is proven. Consider
building it as the netplay pre-flight tool even before it's user-facing.
- **Photo mode.** Pause + free orbit camera + capture (System Shock
precedent). Free camera = port-layer camera override; pairs with the
screenshot hotkey.
- **In-game console.** Type commands at runtime. Less "remaster feature"
than dev tool, but for a WIP port it's arguably the highest-value item on
this list: toggle debug flags, dump state, force conditions without
restarting. PD has no equivalent; we'd own the design.
- **Benchmark mode.** Scripted auto-run sequence + FPS report (Quake
tradition); trivial once tick-rate options exist.
- **AI opponents in deathmatch.** Quake Enhanced ships bots in DM. GE's N64
MP is human-only, so bots = net-new game logic — same policy class as
co-op (record the owner decision when work starts). Stretch: lets a solo
player exercise the netplay layer and makes MP approachable with 1–2 humans.
- **Animation smoothing ("wiggle elimination").** Quake II Enhanced restored
high-res skeletal animations specifically to kill low-fps "wiggle". GE's
model keyframes are N64-rate; interpolating (slerp) between keyframes in
the port-layer model-transform path would visibly smooth guard/Bond motion
on modern displays. Stretch — sequence with the graphics backlog, do late.
- **Dynamic shadows.** Quake Enhanced's headline graphical feature. For
fast3d this means a shadow-map pass + per-light casting — deep stretch;
park next to texture upscaling (both "do last" items).
- **Remastered audio option (Phase 3 note).** System Shock's remake added
new sounds/music. Keep N64 ADPCM fidelity as the default; optionally offer
a lossless re-encoded soundtrack toggle — owner decision + content work,
park until Phase 3 lands.
- **Optional modern UI skin.** SiN Reloaded shipped upgraded 2D screens /
menu art; System Shock an overhauled interface. For GE this is route (b)
game-side territory (menu tables in `front.c` / `initmenus.c`) — park as
an explicit owner decision; lowest priority on this list.
- **Stereo 3D** — both reference families shipped it; explicitly
deprioritized as anachronistic. Park unless someone asks.
Out of scope by the v1 bars above: online matchmaking, leaderboards,
anti-cheat, cross-play (Quake 1 Remaster's biggest features; revisit only
after LAN netplay is solid).
Also out of scope by policy (community port): online-platform integration —
Steam/GOG/Epic achievements, cloud saves, storefront add-on downloads. No
platform tie-in of any kind.
--Old is above here
---
## Technical cleanup (backlogged; not gated on playability)
- **Remove TEMP D63 debug scaffolding.** ~16 references in `port/src/` (`libultra.c`): the master-DL scanner and env-gated `GE_D63` logging inside `osSpTaskStartGo()`. D63 is closed; this is dead weight in the hottest function in the port.
- **Add a D24-implications bug class to `docs/PORT-LEARNINGS.md`.** "Host-scheduling nondeterminism / fake priority semantics": the pthread kernel does not enforce N64's 0–31 priorities (`osYieldThread` = `Sleep(0)`), so interleavings impossible on console can occur, and frame timing has host jitter. Consequence for future sessions: when a flaky timing bug appears (especially Phase 3 audio underruns), reach first for host thread priorities (`SetThreadPriority`: scheduler thread time-critical, tick normal) and the deferred `GE_DETERM` mode (D117) — not for kernel changes.
- **Watch item: `osYieldThread` = `Sleep(0)`.** The one place we fake cooperative behavior. If a level-sweep hot loop misbehaves under host load, this shim is the first thing to inspect. Keep as-is until then.
## Architecture decision (recorded; no action)
- **Keep the pthread + real `src/sched.c` model.** Compared against the PD port's actual design (verified in `pd_port/`: zero threads — thread API is pure no-op, `mainProc()` runs as the host main thread, `pdsched.c` is a gutted frame-callback wrapper, audio driven inline via `amgrFrame()`, M_AUDTASK dropped). PD could do that only because their *original N64* scheduler already used direct-call submission ("nothing writes to the cmdQ in PD") and they adapted game-side lib code (`audiomgr.c`). For GE — classic `osSendMesg(cmdQ)` protocol, real `__scMain` loop, `amMain` blocking on retrace/reply messages — that route means editing `src/audi.c`/submission points (non-negotiable #2 violation) and regressing Phase 3, which currently runs the full authentic audio scheduling protocol. No rewrite. Related: audio stays inline in the `M_AUDTASK` branch (`docs/AUDIO-PLAN.md`, Option A + sub-decision A3); no dedicated audio worker thread unless host thread priorities provably fail.