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.
19 KiB
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 decodeport/src/rzdecomp.c, or theimport_texture_*path ingfx_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 inport/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/Readare currently no-op stubs; build on PD'sfs.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.cexists but is minimal (58 lines: Int + String only; PD's is 300 lines with Float/UInt, clamping, INI sections). Copy-and-adapt frompd_port/port/src/config.c.port/src/video.cis 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 PCblocks exist insrc/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:
- Config system: full
configRegister{Int,UInt,Float,String}+ clamps (pd_port/port/src/config.c). - Window/display (
video.c+ SDL): fullscreen checkbox, fullscreen mode (borderless/exclusive), maximize, center window, resolution dropdown (display-mode list + "Custom"), HiDpi toggle. - 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). - Quality: MSAA Off/2x/4x/8x/16x (FBO
samples— verify our FBO path accepts samples>0 first; PD reads it back viavideoGetMSAA), texture filter Nearest/Bilinear, anisotropic 1–16 slider, mipmap filter, on-screen FPS display toggle. - FOV slider (baseline per owner decision). PD does this game-side
(
Game.PlayerN.FovY,g_PlayerExtCfgin theirmain.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.
- 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 withG_ASPECT_*_EXTextended 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:8300et 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. NoteosMotor*(Rumble Pak) is currently stubbed — accessory detection only (PCPortResearch §4) — so wiring it toSDL_GameControllerRumbleis 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-gatedGE_D63logging insideosSpTaskStartGo(). 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 deferredGE_DETERMmode (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.cmodel. Compared against the PD port's actual design (verified inpd_port/: zero threads — thread API is pure no-op,mainProc()runs as the host main thread,pdsched.cis a gutted frame-callback wrapper, audio driven inline viaamgrFrame(), 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 — classicosSendMesg(cmdQ)protocol, real__scMainloop,amMainblocking on retrace/reply messages — that route means editingsrc/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 theM_AUDTASKbranch (docs/AUDIO-PLAN.md, Option A + sub-decision A3); no dedicated audio worker thread unless host thread priorities provably fail.