GoldenEye 007 — PC Port: architecture & plan
This began as the pre-implementation research note and is kept as the architecture reference. Sections 1–9 describe the design; section 10 lists external references. The phased plan in section 8 is largely done through Phase 2 — for current status see the README and
dev/LEVEL-STATUS.md; for the blow-by-blow finding log seedev/findings.md.
This document captures the research done to plan a PC port of
GoldenEye 007, based on the decompilation this repository is forked from
(n64decomp/007). It is modeled on the
approach used by the
Perfect Dark PC port,
which ports the same Rare “Indy” engine (a later revision) to modern
platforms.
Contents
- Executive summary
- What the PD port actually does (the reference architecture) — 2.1 Emulate the RSP, bypass the RDP · 2.2 How the RSP is invoked · 2.3 Build system · 2.4 PD-port copy-candidate audit
- GoldenEye decomp: what we’re porting — 3.1 Entry / main loop · 3.2 The two libultra trees
- N64 hardware surfaces that must be shimmed
- GE-specific graphics differences vs PD (the RSP work)
- Audio
- Input & saves
- Recommended plan — phases 0–4
- Open questions / risks
- References
1. Executive summary
- Feasibility: HIGH. GoldenEye and Perfect Dark share the same Rare engine family. The PD port proves the whole approach works end-to-end (singleplayer + split-screen multiplayer, mouse-look, widescreen, 60 FPS, mods). GE is, if anything, a simpler graphics pipeline than PD in some respects, though it has a few custom RSP extensions (see §5).
- Recommended strategy: fork this decomp and add a
port/layer, exactly mirroring the PD port’s layout. The N64 build (Makefile + IDO) stays untouched; the PC build is a parallel CMake build that compiles the same game C sources against a set of PC shims. - The single hardest component is the RSP (Reality Signal Processor)
emulator. The PD port ships a complete, working “fast3d” software RSP
(
port/fast3d/, ~7k lines of C++) that interprets GBI display lists and emits OpenGL calls. It already handlesG_TRI4, which is the main custom triangle command GE uses. GE’s other custom command,G_SETTEX, appears unused (its emittergsSPUseTextureis never called), so the remaining GE-specific work is mostly verifying the custom render/color-combiner modes against the RSP ucode. - Audio is straightforward. GE uses the standard libaudio (AL) engine on the R4300, the same as PD. The PD port’s SDL audio + mixer approach carries over directly.
- Saves are EEPROM-based (controller EEPROM via
osEeprom*), not Memory Pak. The PFS/motor code injoy.cis only for accessory (Rumble Pak / Memory Pak) detection and can be stubbed. Phase 4 is therefore a file-backed EEPROM plus no-op PFS/motor shims — simpler than a full Memory Pak emulation.
2. What the PD port actually does (the reference architecture)
The PD port is a fork of the PD decomp with one added top-level
directory, port/. The original game code (src/game/*.c, src/lib/*.c)
is compiled unmodified for the PC. All N64-hardware dependencies are
satisfied by the port/ layer:
port/
├── fast3d/ # Software RSP: interprets GBI display lists -> OpenGL
│ ├── gfx_pc.cpp # the RSP interpreter (matrices, lights, tris, fog)
│ ├── gfx_opengl.cpp # OpenGL rendering backend (shaders, textures, zbuf)
│ ├── gfx_sdl2.cpp # SDL2 window-manager backend
│ ├── gfx_cc.cpp # color-combiner -> GLSL shader compiler
│ └── ...
├── include/ # port-facing headers
│ ├── audio.h config.h fs.h input.h mixer.h mod.h
│ ├── romdata.h system.h video.h utils.h platform.h
└── src/
├── main.c # PC entry point (replaces boot.s / _start.s)
├── libultra.c # shims for the libultra OS API (threads, time, VI, PI, SI, SP, AI)
├── video.c # SDL2 window + GL context + frame pacing
├── audio.c # SDL audio device, buffer queueing
├── mixer.c # audio mixing / RSP-audio-buffer processing
├── mod.c # MOD music playback
├── input.c # SDL2 keyboard/mouse/gamepad -> N64 controller structs
├── fs.c # filesystem abstraction over the ROM + data dir
├── romdata.c # loads the .z64 ROM from disk, sets up segments
├── config.c # INI config (pd.ini)
├── system.c # platform primitives (time, logging, paths)
├── crash.c # crash handler / stack traces
├── pdsched.c # replacement RCP scheduler that drives the software RSP
└── utils.c
2.1 The key architectural insight: emulate the RSP, bypass the RDP
On real hardware the render path is:
R4300 builds a Gfx display list
|
v
RSP (coproc 0) runs "fast3d" ucode, turns GBI cmds into RDP cmds
|
v
RDP (coproc 1) rasterizes into the framebuffer
The PD port emulates the RSP in software (fast3d/gfx_pc.cpp) and
bypasses the RDP entirely: the software RSP translates GBI commands
directly into OpenGL calls. The R4300 game code is run natively and is
unaware of the difference — it just builds display lists and “starts the
RSP” as usual.
The frame flow (from port/src/pdsched.c + port/src/video.c):
videoStartFrame() -> gfx_start_frame()
game builds display list (Gfx*)
videoSubmitCommands(Gfx* cmds) -> gfx_run(cmds) # <- the software RSP runs here
videoEndFrame() -> gfx_end_frame() # swap buffers, FPS accounting
2.2 How the RSP is invoked
The game’s RCP scheduler (src/lib/sched.c in PD, src/sched.c in GE) calls
osSpTaskLoad() + osSpTaskStartGo() to hand a task (gfx or audio) to the
RSP. The port:
- Excludes the original
libultra/io/sptask.c(and the otherio/*.chardware drivers) from the PC build. - Provides its own scheduler (
pdsched.c) that, instead of writing to RSP registers, callsgfx_run()on the display list for gfx tasks and feeds the audio buffer for audio tasks. - Shims the rest of the libultra OS API in
libultra.c(threads become no-ops / single-threaded,osGetTimemaps to a host clock, VI/PI/SI/AI functions map to the port’s video/audio/fs/input layers).
2.3 Build system
The PC build is CMake (the N64 build stays Make + IDO). Dependencies:
SDL2, zlib, OpenGL (plus stdc++, m, dl). It compiles:
src/game/*.c(all of it)- a curated list of
src/lib/*.c(the engine helpers that are platform-independent: mema, memp, model, music, snd, vi, rdp, etc.) src/lib/ultra/audio/*.c(libaudio — runs fine on the CPU)src/lib/ultra/gu/*.c(gu matrix helpers)port/src/*.c+port/src/*.cpp+port/fast3d/*.cpp
It deliberately excludes the N64 I/O drivers (src/lib/ultra/io/*.c),
the boot/TLB assembly, and the RSP microcode (.s under rsp/).
2.4 PD-port copy-candidate audit (session 2026-08-22)
The Perfect Dark port is a standing reference for this project (see CONTRIBUTING.md): same Rare “Indy”/Bond engine family, and its port layer solves the same problem classes we are currently hitting. Audited against our remaining work items:
Direct copy candidates (Phase 3/4 — our stubs were scaffolded to be replaced by these):
| PD file | Lines | Replaces our | Notes |
|---|---|---|---|
port/src/mixer.c |
722 | 31-line stub | libaudio→SDL final mixer; same RareAL family. Adapt to GE’s audi.c wiring (sample rate/buffering) |
port/src/input.c |
1551 | 48-line stub | SDL_GameController backend: hotplug, rumble, keyboard fallback. Remap the button table to GE’s scheme (C-buttons/Z-trig differ from PD) |
port/src/fs.c |
294 | 81 lines | File-backed save dir (--savedir, $S path expansion) — foundation for Phase 4 EEPROM/PFS file backing (ours are no-op stubs today) |
port/src/audio.c |
75 | 70 lines | Near-identical; diff when Phase 3 starts |
Reference implementations (Phase 2 — the D32/D37/D43 class). PD’s port
layer has a port/src/preprocess/ module (~4,125 lines): a per-ROM-segment
preprocessfunc table in their romdata.c that converts N64-layout asset data
to PC-native layout at load time — a systematic solution to the problem class
we have been fixing ad-hoc per asset type.
filemodel.c(1,114) — the direct D43 analogue: samePROMOTEidiom, same vma 0x5000000. NOT a drop-in copy (GE’sModelRoDataunion + opcode set differ — PD has stargunfire/headspot/gundl node types), but the node-walk order, placement/alignment choices, and record handling are Rare-validated against near-identical data: adapt rather than design from scratch.filebg.c(524) — BG tables (an expected D43 follow-on fault).segaudio.c(362) — audio bank-tree conversion; cross-check for our D37romdataFixupAudioBank.filelang.c,filetiles.c,filepads.c,filesetup.c,gbi.c,segfonts.c— the remaining asset types we will hit.
Not copyable: GE-specific ModelRoData record map; fast3d CC/RM
correctness (GE’s custom GBI modes vs gmain.s do not exist in PD); PD extras
(config.c, optionsmenu.c, mpsetups.c, mod.c) — options menu, MP setup,
MOD player; not part of 1:1 fidelity.
Caveats: copy port-layer files only — their decomp was modified for PC
(e.g. types.h keeps N64 offset comments while using real pointers); our
non-negotiables are unchanged. Same family ≠ identical format: validate every
copied conversion per-field against GE headers + ROM ground truth (the standing
D32 procedure).
3. GoldenEye decomp: what we’re porting
- Repo:
n64decomp/007(this repo). WIP decomp that byte-matches the US/EU/JP ROMs. Game code lives insrc/game/(~242 files) plus engine files insrc/. - Engine layout (same family as PD):
src/sched.c— RCP scheduler (osCreateScheduler,__scExec,__scYield). Structurally near-identical to PD’ssrc/lib/sched.c.src/game/rsp.c— builds the Gfx display list, wraps it in anOSScTask, and starts it via the scheduler (rspGfxTaskStart).src/fr.c— frame/VI management, video modes (osViSetMode).src/audi.c,src/snd.c,src/music.c— libaudio-based sound + music.src/joy.c— controllers, EEPROM save handling (viajoyGamePak*->osEeprom*), plus PFS/motor code for accessory (Rumble Pak / Memory Pak) detection only.src/init.c—init()->mainproc()->bossEntry()(game entry). Compiled for the PC (providesmainproc()); its N64-onlyinit()is stubbed (see A3).
- Custom RSP microcode:
rsp/graphics/gmain.s(1545 lines) — a modified fast3d. This is the RSP-side code that consumes the display list. For the PC port we do not run this; we replace it with the software RSP. But it is the authoritative reference for which GBI commands GE actually emits.
3.1 Entry / main loop
boot.s -> init() [src/init.c]
-> decompress data segment
-> osInitialize(), TLB setup
-> osCreateThread(mainThread, mainproc)
mainproc()
-> idleCreateThread(), piCreateManager(), rmonCreateThread()
-> schedulerInitThread() # osCreateScheduler + osScAddClient(gfxClient)
-> bossEntry() # [src/boss.c] real game start
The port’s main.c replaces boot.s/init() and drives mainproc() (or
bossEntry()) directly after setting up video/audio/input/ROM.
3.2 The two libultra trees: src/libultra/ and src/libultrare/
GE ships two libultra trees. src/libultra/ is the standard Nintendo
libultra; src/libultrare/ holds Rare’s modified/replacement files. The N64
build selects which files to compile via src/libultrare/Makefile.libultrare,
which explicitly lists each file as original (from libultra/) or Rare
(from libultrare/). The Rare files override the originals where they
overlap.
What the PC build must do with each:
src/libultra/audio/*.c— standard libaudio (AL). Runs on the CPU; compile (the game’saudi.c/snd.c/music.cdepend on it).src/libultrare/audio/*.c(drvrNew.c,env.c,reverb.c) — Rare’s audio “New” driver.synthesizer.ccallsalSaveNew/alAuxBusNew/alMainBusNew/alLoadNew/alResampleNew/alEnvmixerNew/alEnvmixerParam/alFxParam, all defined here. Runs on the CPU; compile.src/libultra/gu/*.c— matrix helpers (guMtxF,guOrtho, …). Compile.src/libultrare/io/vitbl.c— definesosViModeTable(a data table), referenced ~15× byfr.c. Compile (it’s data, not a driver). This is the onlyio/file we compile.- All other
src/libultra/io/*.candsrc/libultrare/io/*.c— hardware drivers (PI, SI, SP, DP, VI, controller, PFS, motor, EEPROM, …). Exclude and shim inport/src/libultra.c. Notablypfsinit.c/pfsisplug.c(osPfsInit/osPfsIsPlug) andmotor.c(osMotor*) are called byjoy.conly for accessory detection, so they are shimmed as no-ops (see §7). src/libultra/os/*.c+src/libultrare/os/*.c— OS core (threads, message queues, timers, cache, TLB). Exclude and shim inport/src/libultra.c(+n64stubs.cfor the boot/TLB/FPU symbols).
The exact original-vs-Rare file lists are in
src/libultrare/Makefile.libultrare(LIBULTRA_*_C_FILESvsLIBULTRARE_*_C_FILES). Use it as the ground truth when curating the PC build’s source list.
4. N64 hardware surfaces that must be shimmed
| Subsystem | GE API used | Port strategy (from PD) |
|---|---|---|
| RSP (coproc 0) | osSpTaskLoad/StartGo/Yield, display lists |
Software RSP (fast3d) — main work |
| RDP (coproc 1) | osDpSetStatus, osDpSetNextBuffer, render modes |
Bypassed; OpenGL backend |
| VI (video) | osViSetMode, osViVSyncCallback, osViWaitVSync |
SDL2 window + GL context + frame pacing |
| AI (audio) | osAiSetFrequency/SetNextBuffer/GetLength |
SDL audio device + queue |
| PI (peripheral) | osPiRawStartDma, osPiCreateManager, cart reads |
Read from ROM file on disk |
| SI (controller) | osContInit/StartReadData, osEeprom*, osPfs*/osMotor* |
SDL input + file-backed EEPROM; PFS/motor stubbed (accessory detection only) |
| OS core | threads, msg queues, timers, osGetTime, cache |
Single-threaded shims + host clock |
| R4300 specifics | TLB, cache, FPU csr, K0/K1 segments | No-ops / identity (native x86-64) |
5. GE-specific graphics differences vs PD (the RSP work)
GE’s include/gbi_extension.h defines the custom GBI surface. Compared to the
PD port’s fast3d, the deltas are:
G_TRI4— draws up to 4 triangles in one command using 4-bit vertex indices (0–15). A memory-saving optimization (Rare packed 4 tris into the slot normally used forG_TRI2).- Status: already supported by the PD
fast3d(gfx_sp_tri4, and the PD game itself uses it). ✅
- Status: already supported by the PD
G_SETTEX(0xc0) — “use texture from bank”: selects a texture by bank/tile with detail/mipmap type, min-level, detail-id and texture-id. Emitted only bygsSPUseTexture(include/gbi_extension.h:193).- Status: appears UNUSED.
gsSPUseTextureis defined but never called anywhere in the game code or the RSP ucode (rsp/graphics/gmain.s), andG_SETTEXappears nowhere outside its#define. The game uses the standard texture commands (gSPTexture,gSPTextureL,gSPTextureRectangle,gSPTextureRectangleFlip) — all already handled by the PDfast3d. If this holds (verify the decomp is complete), Phase 2 shrinks to verifying the custom CC/RM modes and the PD fast3d may work largely as-is. Keep aG_SETTEXdecode path as a safety net, but it is no longer the headline task.
- Status: appears UNUSED.
- Custom color-combiner modes —
G_CC_MODULATEIFADE,G_CC_FADE,G_CC_DECALFADE,G_CC_BLENDRGBFADEA, etc. (fade/blend variants).- Status: handled generically by the fast3d color-combiner -> GLSL
shader compiler (
gfx_cc.cpp), which compiles arbitrary CC equations. Needs verification that each GE mode produces correct GLSL.
- Status: handled generically by the fast3d color-combiner -> GLSL
shader compiler (
- Custom render mode —
G_RM_CUSTOM_AA_ZB_XLU_SURF(AA_ZB_XLU withZ_UPD).- Status: handled by the render-mode -> GL state mapping; verify.
gDPLoadTLUT06/07— custom TLUT load variants.- Status: minor, map onto the existing
G_LOADTLUTpath.
- Status: minor, map onto the existing
Conclusion: the PD fast3d is a strong starting point. G_SETTEX appears
unused (see item 2), so the concrete new RSP work is verification of the
custom CC/RM modes against gmain.s, with G_SETTEX as a low-priority
safety net. Everything else (matrices, lights, fog, tri1/tri4, texture load,
sync/flush) is shared.
Note: GE’s on-hardware RSP ucode is
rsp/graphics/gmain.s. We do not run it on PC, but it is the ground truth for the exact command encodings GE emits — use it to validate the software RSP’s command decoding.
6. Audio
- GE uses libaudio (AL) on the R4300 (
src/audi.c,src/snd.c,src/music.c) — identical architecture to PD. Sound synthesis (ADPCM decode, mixing, FX) runs on the CPU; the RSP audio ucode is a thin pass-through to the AI DMA. - The PD port runs libaudio natively and routes the mixed output through
audio.c(SDL device) +mixer.c. Carries over directly. - Music: GE uses the same MOD-style music system; PD’s
mod.cis the model. - Output rate: PD uses 22020 Hz stereo s16. GE’s
OUTPUT_RATEinaudi.cshould be matched.
7. Input & saves
- Controllers:
src/joy.cusesosContInit/osContStartReadData. The port maps SDL2 keyboard/mouse/gamepad into the N64OSError/controller structs. PD implements 1964GEPD-style and Xbox-style bindings — a good default for GE too (fire=Z, aim=R, etc.). - Saves: GE saves are EEPROM-based, not Memory Pak. The actual save
I/O (
src/game/file2.c) usesjoyGamePakProbe/LongRead/LongWrite, which map toosEepromProbe/LongRead/LongWrite(controller EEPROM). The PFS/motor code injoy.c(osPfsInit,osMotorInit/Start/Stop) is only for accessory (Rumble Pak / Memory Pak) detection. The port therefore:- backs the EEPROM with a file in the data dir (e.g.
eeprom.bin), and - stubs PFS/motor to report “no accessory” so the game proceeds without rumble/mempak. This is simpler than a full Memory Pak/PFS emulation.
- backs the EEPROM with a file in the data dir (e.g.
8. Recommended plan
Phase 0 — Scaffolding (this change)
- Add
port/directory structure + headers + stub sources. - Add a top-level
CMakeLists.txt(+cmake/modules) for the PC build, parallel to the N64Makefile. - This document +
port/README.md.
Phase 1 — Boot to a window
- Port
system.c,fs.c,romdata.c,config.c,main.c. - Port
video.c+ a minimalfast3d(clear color, present). - Goal: load
ge007.u.z64, open a window, draw a clear color.
Phase 1.5 — Boot to first frame (ABI reconciliation)
- Get mainThread through all of
bossInitMainthreadData()→bossEntry()→bossMainloop()and render frame 1. (Done — D34–D58; see §G.) - Recurring mechanism: ROM-data struct ABI/layout fixes (the D32 pattern) —
a 32-bit-pointer struct read as a 64-bit-pointer struct misaligns; convert
embedded pointer fields to
u32+ cast at use sites (PD ground truth), then verify the load-time rebase yields valid DRAM addresses. See §H for the step-by-step procedure and the standing “non-negotiable #2 ABI exception.” - Goal: title screen / first level actually drawn on the GL surface.
Phase 2 — RSP / rendering
- Bring in the PD
fast3d(gfx_pc / gfx_opengl / gfx_cc / gfx_sdl2). - Verify the custom CC/RM modes against
gmain.s; add aG_SETTEXdecode path only if it turns out to be used (see §5 — it appears unused). - Port
pdsched.c(GE variant) to drive the software RSP. - Goal: render the title screen / first level.
Phase 3 — Audio + input
- Port
audio.c,mixer.c,mod.c; wire libaudio output to SDL. - Decide the ASP strategy (see §11 C2): if audio runs the
aspMainmicrocode, wireaspMainTextStart/aspMainDataStartto the real microcode bytes in the ROM; if audio is CPU-only, theport/src/ucode.cdummies stand. - Port
input.c(keyboard/mouse/gamepad). - Goal: playable with sound.
Phase 4 — Saves + polish
- File-backed EEPROM (saves); PFS/motor already stubbed as no-ops.
- Widescreen, mouse-look, FPS options, config file, crash handler.
- Goal: feature-parity with the original + QoL extras.
9. Open questions / risks
G_SETTEX(low priority) — appears unused (gsSPUseTextureis never called). If a future decomp revision or a missed call site shows it is used, reverse the texture-bank selection (bank -> image, tile, detail/mipmap type) fromgmain.s+tex.c/initmttex.c. Not currently the main unknown.- Custom CC correctness — the GLSL compiler must handle every fade/blend mode GE uses; validate per-mode against the original. This is now the main rendering unknown.
- Scheduler fidelity — GE’s
sched.chas speed-graph / profiling hooks (speedgraphMarkerHandler) and a slightly different task model than PD; the port scheduler must reproduce the gfx/audio interleaving and VSync pacing. - Decomp completeness — the decomp is WIP; some functions may still be
stubs/
unk_. The PC port inherits whatever state the decomp is in. Track the upstreamn64decomp/007and rebase. - Endianness / alignment — GE, like PD, is big-endian N64 code with
unaligned accesses; the PD port handles this with careful casts and
-fno-strict-aliasing. Reuse those flags.
10. References
- PD port: https://github.com/fgsfdsfgs/perfect_dark
- PD decomp: https://github.com/n64decomp/perfect_dark
- GE decomp (this repo): https://github.com/n64decomp/007
- GE docs: https://github.com/kholdfuzion/goldeneye_docs
- N64 devkit / libultra:
include/PR/*.hin this repo - GE custom GBI:
include/gbi_extension.h - GE RSP ucode:
rsp/graphics/gmain.s