- scripts/gen_romassets.py: emit absolute cart-address asset symbols (0x10000000 + rom_offset) from filelist.<r>.csv + the asset .s files, in file_resource_table.inc.c order. US region (romassets_u.s). - romdata.c: load/validate the GE-specific header (magic, name @0x20, cart ID @0x3C, country @0x3E) and map the .z64 at cart base 0x10000000 so every &asset-symbol is a live host pointer. - libultra.c: osPiStartDma/osPiRawStartDma now service reads as bounds-checked memcpys from the mapped ROM (synchronous completion). - system.c: implement sysResolvePath ($S data dir, $E exe dir). - video.c: real SDL2 window + GL context + clear/swap frame loop with FPS accounting; main.c runs a Phase 1 demo loop (ESC quits) until the software RSP/scheduler can drive mainproc(). - CMakeLists: project(... ASM), wire romassets_<region>.s, drop assetstubs.c (replaced by generated symbols); n64stubs.c keeps only pure-RAM segment markers. - docs: findings D18-D23 (header offsets, BE music table, .rz tracks, scanner-prefixed manifest names, CMake ASM gotcha, absolute-symbol approach). Verified: baserom.u.z64 (12MB) passes validation and maps at 0x10000000; window opens and renders until killed; asset offsets spot-checked against ROM bytes (music bank seqCount=63 BE, first track offset 508).
42 KiB
GoldenEye 007 — PC Port: Preliminary Research
This document captures the preliminary research done to plan a PC port of
GoldenEye 007, based on the decompilation in this repository
(n64decomp/007). It is modelled on the approach used by the
Perfect Dark PC port,
which ports the same Rare "Indy" engine (a later revision) to modern
platforms.
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/).
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 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/perfect-dark-pc-port/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
11. Scaffolding review: findings & resolutions
The initial scaffolding was reviewed against the sources. Each finding below
was verified and resolved in the same change. Code comments in CMakeLists.txt
and port/src/ reference these labels (A1–A4, B1–B4).
A. Build-blocking issues
-
A1 — Region/version macros. The N64
Makefiledefines a full per-region macro set (VERSION_*,LANG_*,REFRESH_*,LEFTOVERDEBUG,LEFTOVERSPECTRUM,BUGFIX_R*,BYTEMATCH) that the game code#ifdefs heavily (e.g.VERSION_EUin 60+ files). Resolved:CMakeLists.txtnow emits the matchingREGION_DEFSperROMID, mirroring the Makefile exactly. -
A2 — Missing source files. The initial
SRC_ENGINElist + "exclude allio/*.c" strategy missed files the included code depends on. Resolved: addedsrc/cfb.c(definescfb_16, the framebuffer),src/libultrare/audio/*.c(drvrNew/env/reverb— the audio "New" driversynthesizer.ccalls), andsrc/libultrare/io/vitbl.c(definesosViModeTable, referenced ~15× byfr.c).motor.c/pfsinit.c/pfsisplug.care not compiled —joy.ccallsosMotor*/osPfs*only for accessory detection, so they are shimmed as no-ops (see B2). -
A3 —
mainproc()/init.c+ the excluded engine files' symbols.mainproc()is defined only insrc/init.c, which the initial build excluded, contradictingmain.ccalling it. Resolved:init.cis now compiled (it providesmainproc()+ the thread-setup helpers). Its N64-onlyinit()is compiled but not called.Compiling
init.c(and the other included engine files) pulls in a set of symbols that live in the EXCLUDED files (sched.c,rmon.c,vi.c,stacks.c). Each is now provided:src/stacks.c(added toSRC_ENGINE) — thread-stack arrayssp_boot/sp_rmon/sp_idle/sp_shed/sp_main/sp_audi(+sp_debugunderLEFTOVERDEBUG). Pure data; theSP_*_SZsizes inbondconstants.hmatch exactly, sosizeof()is correct.port/src/gesched.c(replacessched.c) — scheduler globalsos_scheduler,gfxClient[3],g_schedViCurrentFrameBuffer,g_ViChangeVideoModes,g_ViModes,g_ViModePtrs, plusget_counters()andpermit_stderr().port/src/n64stubs.c—init()'s N64 symbols (segment starts, segment boundary pointers,jump_decompressfile, TLB, FPU CSR) and the rmon host-I/O functions (rmonMain,osReadHost,osWriteHost,rmonGetToken,rmonStatus,osSyncPrintf).port/src/libultra.c— the libultra OS callsinit.cmakes, plusosTvType(region-based),viInit,vi_c_debug_MQ(from the excludedvi.c), andosPiReadIo(cartridge token read, called bytoken.c).
Every external symbol referenced by the compiled set was cross-checked against its definition; no duplicates (all other
sp_*/rmon*references areexterndecls). -
A4 —
libultra.cmissing the high-level message API + others. The initial stub had low-levelosEnqueueMesg/osDequeueMesgbut notosSendMesg/osRecvMesg(used in 13/26 files) or several others. ** Resolved:**port/src/libultra.cnow providesosSendMesg/osRecvMesg/osSetEventMesg, the controller query/read API,osPfs*/osMotor*,osSetTimer/osStopTimer/osSetIntMask,osViBlack/osViSetYScale/osViSetXScale/osViSetEvent,osUnmapTLB,__osGetFpcCsr/__osSetFpcCsr,piCreateManager, andviDebugRemoved. The scheduler API (osCreateScheduler/osScAddClient/osScGetCmdQ) lives inport/src/gesched.c(it's game scheduler API, not libultra).
B. Research-doc corrections
- B1 —
G_SETTEXappears unused.gsSPUseTexture(the only emitter) is never called in the game code orgmain.s; the game uses standard texture commands the PDfast3dalready handles. Resolved: §5, §8 (Phase 2), and §9 updated — Phase 2 shrinks to verifying the custom CC/RM modes; aG_SETTEXdecode path is kept only as a safety net. - B2 — Saves are EEPROM-based, not Memory Pak/PFS. The save I/O
(
src/game/file2.c) usesjoyGamePak*->osEeprom*; the PFS/motor code injoy.cis accessory detection only. Resolved: §7 and Phase 4 updated — Phase 4 is a file-backed EEPROM + no-op PFS/motor shims. - B3 —
src/libultrare/was unmentioned. Resolved: new §3.2 documents the two libultra trees and exactly which files the PC build compiles vs. shims, citingMakefile.libultrareas ground truth. - B4 —
src/spectrum.cdoes not exist. Resolved: removed from theCMakeLists.txtexcluded-files comment.
C. Assembly (.s) file symbols
The A/B sweeps covered excluded .c files. A second symbol source is the
MIPS assembly (src/*.s), also not compiled for the PC. Sweeping every global
label in the .s files against the compiled C set found 10 more undefined
symbols, in two distinct classes:
- C1 — PRNG (
src/random.s) — ported to C, not stubbed.randomGetNext,randomGetNextFrom,randomSetSeed, andg_randomSeedare used for real gameplay logic:randomGetNextFrom()feeds the CRC (src/game/crc.c),g_randomSeedis persisted in replay state (src/game/ramromreplay.c), andrandomGetNext()drivesRANDOMFRAC()/RANDOMGETNEXT_F32(). A no-op would silently corrupt CRCs and replays. Resolved:port/src/random.cports the three functions verbatim (each MIPS instruction mirrored with explicitdsll32/dsrl32masks for bit-exactness) and definesu64 g_randomSeed = 0xAB8D9F7781280783ULL(the two.wordsinrandom.s). Signatures matchsrc/random.h. Verified bit-exact against a Python simulation of the assembly (the PRNG is effectively 32-bit stored in a u64; the initial seed's high 32 bits are exercised only on the first call). - C2 — RSP/ASP/GSP microcode segment markers — dummy definitions.
rspbootTextStart/rspbootTextEnd(rspboot.s),gsp3DTextStart/gsp3DDataStart(gspboot.s),aspMainTextStart/aspMainDataStart(aspboot.s) are ROM addresses of the RSP/ASP microcode, referenced bysrc/audi.candsrc/game/rsp.c(declaredextern long long int <name>[]). Resolved:port/src/ucode.cdefines all six aslong long int <name>[1] = {0}. The graphics path is safe permanently (fast3d interprets the GBI list directly and never runs the gsp3D microcode, so the size differences are never read). Carried into Phase 3: the audio path is the one place it could matter — if the port emulates the ASP by running theaspMainmicrocode, those two markers must point at the real microcode bytes in the ROM (PDpd.ldRSP_TEXT_SEGMENTmodel); if audio is CPU-only, the dummies are fine. ATODO(Phase 3)on theaspMain*dummies records this decision.
D. Host-compiler portability (GCC 16 vs IDO)
The N64 build targets IDO (MIPS). The PC build targets GCC 16 (x86-64). IDO
tolerates a number of non-standard constructs that the decompilation relies on;
GCC rejects them as hard errors. Each finding below was verified and resolved
in the same change. Where a fix could not be made purely in port/, the minimal
src/ change is -DPORT-gated (N64 build untouched) and flagged with a
PC port: comment. These are portability fixes, not game-logic changes.
-
D1 —
inherits= struct inlining + duplicate member name.src/bondtypes.h:44does#define inherits struct, soinherits X;inside a struct becomesstruct X;. IDO resolves this by inlining X's members into the enclosing struct (C++-style base), which the decomp depends on (offset comments likeCCTVRecord.unk84at0x84, and positional initializers likeNew_CCTVRecord(pad)={New_PropDefHeaderRecord(6), 0, pad+0}only line up with the inlined layout). GCC 16 also inlinesstruct X;(correct layout) but hard-errors on duplicate member names; Clang treatsstruct X;as a no-op nested tag (wrong layout). No flag/pragma downgrades the duplicate-name error. A sweep ofbondtypes.hfound exactly one parent/child member-name collision:CCTVRecordredeclarespad(s32 @0x80) whichObjectRecordalso has (s16 @0x08). The game code's->pad(e.g.setupCctv, prop.c) is used as the pad index, whichNew_CCTVRecordstores at 0x08 (the inherited pad), so IDO resolves the ambiguous->padto the inherited one; CCTVRecord's ownpad(0x80, "lookpad") is never accessed by name. Resolved: under-DPORTonly, CCTVRecord's ownpadis renamed tolookpad(layout byte-identical); the N64 build keepspad(IDO tolerates it). -
D2 —
port/shim/PR/gbi.hneeded an include guard. The shim redefines theGfxunion members as little-endianG*_letypedefs. With no guard, a TU that includes it twice (directly + via another header) re-creates the anonymousGdma_le/Gtri_le/… struct types → "conflicting types for 'Gdma_le'". The real header's guard is_GBI_H_; the shim now uses a distinct_PORT_SHIM_GBI_H_. -
D3 —
New_Vector()/New_Coord3d()called with zero args. The decomp's macros are declared with exactly 3 params (x,y,z) and use theIF_ELSE(IS_EMPTY(..))trick to default each to 0, but the game code calls them with zero args (New_Vector()at chrai.c:1358/1378,New_Coord3d()at chrai.c:4491), relying on IDO's leniency with empty macro args. GCC rejectsNew_Vector()against a 3-param macro. A sweep found these are the only call sites (all zero-arg). Resolved inport/:port/shim/bondtypes.hredefines both as#define New_Vector(...) {0,0,0}/New_Coord3d(...) {0,0,0}after including the real header (the shim is found first via the include path). -
D4 — local
#define osSyncPrintf()/(x)arg-count mismatch. Five files locally disableosSyncPrintfwith a fixed-arity macro (#define osSyncPrintf()or(x)) but then call it with more args (2–4), relying on IDO's leniency. GCC errors ("passed N arguments, but takes just M"). Resolved: the five local defines (bg.c, debugmenu_handler.c, debug_camera.c, deb_loadallmodels.c, initexplosioncasing.c) are made variadic (#define osSyncPrintf(...)); they still expand to nothing, so behavior is unchanged. -
D5 — array-initialized-from-array.
chraction.c:2485hads16 mrs[3] = metal_ricochet_SFX;(a local array "initialized" by a global array). C requires a constant-expression initializer; IDO treated it as a runtime copy. ** Resolved:** replaced with an explicit 3-element copy (no new includes needed). -
D6 — flexible array member in a nested context.
bg.cdeclareds_special portal specialportalarray[]wheres_specialportalhas a flexible array member (u8 portallist[]). C forbids initializing a FAM in a nested context (an array element); GCC enforces it, IDO tolerated it. The code treats the data as a flat byte array (cast tou8*insub_GAME_7F0B37EC, walked byte-by-byte). ** Resolved:** definedspecialportalarrayas a flatu8[]with the identical byte sequence. -
D7 — AI X-macro system (
chraidata.c) — RESOLVED. The AI command system (bondaicommands.h+aicommands.def+CPPLib.h) uses deep preprocessor metaprogramming (SWITCHwith 49 fixed params +IF_VA/IS_EMPTY,DEFINED(SETUPSUBROUTINES(ID))token-pasting,_AI_CMD_POLYMORPHredefined per-include) that relies on IDO-specific##/expansion behavior. GCC rejects several of these (e.g.pasting ')' and '_'inDEFINED(SETUPSUBROUTINES(ID)),SWITCH requires 49 arguments, but only 20 given). Notesrc/aicommands2.his a pre-generated header (fromtools/cmdbuilder.c) already included bybondaicommands.h:864; the failing path is the rawaicommands.definclude (bondconstants.h:731 for theAI_CMDenum, chrai.c:172/920 for the command table).A general GCC-clean reimplementation of
SWITCH()was investigated and ruled out: its content arguments are single preprocessor arguments that expand to top-level comma lists, and the C preprocessor cannot detect where one content ends and the nextCASE/VALbegins (arity is not recoverable after expansion). The three activeSWITCH()call sites in chraidata.c (m_IdleAnimations, m_BashKeyboard, m_RunToBondPersistent) each have a hand-written equivalent already present in the file behind#if 0— the author's own reference form. Each was verified byte-identical to the IDO expansion of the adjacentSWITCHcall (including theIFNewRandomGreaterThan(N, lbl)==SetNewRandom()+IFRandomGreaterThan(N, lbl)identity, confirmed against the runtime check in chrai.c:1605). Resolved: under-DPORTthe three#if 0blocks are activated (theSWITCHcalls become dead#elsebranches);port/shim/bondaicommands.hkeeps the original 49-paramSWITCHdefined but replaces it with a marker that fails loudly if any new game code usesSWITCH()on the port. See also D8–D11 for the sibling paste/comma issues in the same macro system. -
D8 —
MODELSKELETON/New_ModelSkeletonpaste failure. The model-record macros in bondconstants.h writeSKELETON(##NAME##)-style pastes that IDO tolerates but GCC hard-errors on ("pasting does not give a valid preprocessing token"). Resolved:port/shim/bondconstants.hre-emits the affected macros with equivalent byte-identical expansions. -
D9 —
CPPLib.hhelpers. The CPPLib metaprogramming header'sIS_EMPTY/IF_VA/DEFINEDfamily uses paste tricks that break under GCC. Resolved:port/shim/CPPLib.hprovides a paste-free reimplementation with identical results for every usage in the tree (intercepted via the include path; inert on N64). -
D10 — file-record macros paste
&onto NAME.CHRFILERECORD/GUNFILERECORD/SUIT_LFRECORDandGUNSTATSwrite{& ## NAME ## _header, …}. IDO tolerated the failed&##NAMEpaste; GCC hard-errors. Resolved:port/shim/bondconstants.hrewrites them with the&kept out of the paste; expansion is byte-identical (&NAME_header,&NAME_stats). -
D11 — generated
CALL()double trailing comma. The pre-generatedCALL()(aicommands2.h) concatenatesSetReturnAiList()andSetChrAiList(), each ending in its own trailing comma, then appends its own separator →…, ,inside the array initializer. IDO accepted it; GCC hard-errors. The artifact byte is never executed:AI_SetChrAiList(CHR_SELF)switches to the called list at offset 0 andAI_Returnresumes the return list at offset 0 (chrai.c), so anything after theSetChrAiListrecord in aCALLis dead. Resolved:port/shim/bondaicommands.hre-emitsCALLwithout the artifact byte. -
D12 — 64-bit pointer→integer in static initializers. On a 32-bit target (MIPS) storing an array address in a 32-bit field is fine; on x86-64 GCC 16 makes it a hard error ("initializer element is not computable at load time") that no warning flag suppresses. Two sites, two fixes:
process_monitor_animation_microcodejump targets: the monAnim script tables (chrai.h) stored rawmonAnim*array addresses in the tvcmd word's 32-bit field. Resolved: under-DPORTthe initializers store an index into_PORT_monAnimPtrs[](defined in propobj.c after all 35 monAnim arrays, via a shared_PORT_MONANIM_LISTx-macro in chrai.h) and the interpreter resolves index→pointer atTVCMD_SETCMDLIST/TVCMD_RANDSETCMDLIST. On-script layout unchanged (12-byte tvcmd words, same opcode bytes).assets/obseg/setup/{u,e,j}/UsetuplenZ.c: theintro[]table stored&credits_data_0in ans32slot. Resolved: PORT-gated replacement with0(the N64 initializer is kept in the#elsebranch). The value is only consumed by romCopy-style size arithmetic, which is inert until Phase 2. (These files are currently excluded from the PC build per D16; the patch keeps them compilable should any setup data be pulled into the host link.)
-
D13 — assorted IDO leniency hard errors. Small strictness failures with no semantic content:
front.c:2405: barereturn;in ans32function (GCC error). The sole caller ignores the value → PORT-gatedreturn 0;.audi.caudioInit: C++-style array initializers32 sp48[…] = CUSTOM_FX_PARAMS_N;(IDO accepted, GCC rejects) → PORT-gated explicitmemcpyof the same bytes.
-
D14 —
_Printfprototype clash (xstdio.h vs xprintf.c). The IDO printf engine is compiled for the PC (src/libultrare/libc/xprintf.c+ helperssrc/libultra/libc/xlitob.c/xldtob.c, added to CMakeLists) becausesrc/sprintf.c'ssprintf()calls_Printfdirectly. xstdio.h declares it withu8 *params; xprintf.c defines it withchar *— IDO-compatible, GCC-fatal. Resolved:port/shim/libc/xstdio.hrenames the declaration (_Printf_u8decl) before pulling in the real header via#include_next, so xprintf.c's definition is the sole prototype. The only caller (sprintf.c) doesn't include the header and passes its char*-based outfun, matching the definition exactly. -
D15 — host libc lacks IDO/K&R symbols. MinGW provides none of:
bcopy/bzero(declared in PR/os.h withintsizes),__libm_qnan_f(quiet-NaN helper used by gu/cosf.c and game/zlib.c), thetlbmanage*API (boss.c calls two of them; tlb_manage.c is excluded — N64 TLB management), andg_ViXScales/g_ViYScales(defined in vi.c on N64, written at runtime by fr.c). AlsochrObjRandomGetNext/SetSeed+g_chrObjRandomSeedlive insrc/game/chrObjRandom.s(not built for PC). Resolved: all provided inport/src/n64stubs.c/port/src/libultra.c; the chrObj PRNG is ported verbatim from chrObjRandom.s intoport/src/random.c(same xorshift asrandomGetNext, separate state). -
D16 — asset data strategy for the single host link. The N64 build links each model/level file into its own ELF and
.incbins compressed ROM blobs; none of that transfers. Three buckets:- Compiled for real (self-contained, unique symbol names):
assets/animationtable_data.c,animationtable_entries.c,oddtextures.c,font_chardatae.c,font_chardataj.c,font_dl.c,rarewarelogo.c→ added to CMakeLists (SRC_ASSETS). Real font/image/ animation-table data is therefore live in the binary. - Excluded — symbol collisions: every
assets/obseg/{bg,brief,setup,stan}/*.cdefines generic file-local globals (header,room_data_table,intro,padlist, …) that collide in a single link. The N64 build isolates them by per-file ELF linking; the PC build cannot. Their top-level symbols are stubbed instead. - Stubbed — ROM-derived / absent:
port/src/assetstubs.cdefines the 758 model/level/text symbols referenced by ob.c'sfile_resource_table(one zero word each), the 14ramrom_*replay pointers (NULL), and theunknown2/unknown2_endpair (same address → zero-length romCopy in title.c). All*Segment*linker-script markers (normally from ge007.ld) are defined as NULLu32 *in n64stubs.c, so address arithmetic computes zero lengths and any Phase-1 romCopy is a safe no-op. Real data arrives with ROM loading in Phase 2 (port/src/romdata.c), which will also replace the table's address arithmetic.
- Compiled for real (self-contained, unique symbol names):
-
D17 — fast3d placeholder.
port/fast3d/contains onlygfx_api.h; the real software RSP lands in Phase 2.port/src/gfxstub.cprovides no-op implementations of every entry point (plusgfx_current_dimensions) so video.c links. Delete it when the real fast3d sources are added (same symbols → duplicate-definition error if both remain). -
D18 — GE ROM header offsets. The N64 header is GE-specific (
src/rom_header.s): magic0x80371240@0x00, ROM name "GOLDENEYE" @0x20, cartridge ID "GE" @0x3C, country byte @0x3E ('E' US / 'P' EU / 'J' JP), version @0x3F. The standard-N64 offsets (ID @0x38, country @0x3A) are wrong for GE — verified againstbaserom.u.z64(hexdump:... 47 45 45 00at 0x3C–0x3F). romdata.c validates all four fields. -
D19 — music table is big-endian in the ROM. The
.musicsection ofassets/music/music.s(theRareALSeqBankFileheader + 63 × 8-bytemusic_table_entryrecords) is stored in the ROM in big-endian word order: at 0x419790 the bytes are00 3f 00 00 | 00 00 01 fc= BE u16 seqCount=63 (= NUM_MUSIC_TRACKS), BE u16 unk=0, then BE u32 offset 508 for the first track. The N64 build evidently assembles that section with big-endian output; the PC port never reassembles it — the CSV offsets are unaffected andmusicSeqPlayerInit()'s romCopy +musicSeqFileNew()patching work on the raw bytes unchanged. -
D20 — US music tracks are RLE-compressed in ROM. The
music_filemacro incbinsbuild/u/assets/music/<name>.rzunder.ifdef VERSION_US, so every M* track lives in the ROM 1172/RLE-compressed; the filelist sizes are the compressed lengths and per-trackend_symbols = start + csv_size are correct boundaries. Decompression happens at playback (Phase 3 audio), not at load — no Phase-1/2 action needed. -
D21 — scanner-prefixed manifest names.
filelist.<r>.csvcontains entries whose file column is prefixed with the scan origin, e.g.assets/ge007.u.117880.jfont_dl.bin(real namejfont_dl.bin). gen_romassets.py aliases such names to their suffix after thege007.<r>.<hexoffset>.prefix, so font/image/oddtexture segments resolve. -
D22 — CMake silently skips
.sfiles without ASM enabled. The first assembly source in the PC build (romassets_u.s) was added to the target but never compiled:project(ge007 C CXX)had no ASM language, and CMake does not error on unknown extensions — it just drops them, surfacing only as a wall of undefined asset symbols at link. Fixed withproject(ge007 C CXX ASM). Lesson: when adding a new source extension, verify the object file actually appears in the build graph. -
D23 — absolute cart-address asset symbols (Phase 1 approach). Instead of zeroed stubs (D16), every ROM asset symbol is now an ABSOLUTE cart address:
scripts/gen_romassets.pyemitsport/src/romassets_<r>.sdefining all obseg labels (infile_resource_table.inc.corder — ob.c computes file sizes astable[i+1].hw_address - table[i].hw_address, so order matters), ramrom files, music tracks/markers, and the ge007.ld segment markers as0x10000000 + rom_offsetfromfilelist.<r>.csv. romdata.c maps the .z64 at exactly 0x10000000 (VirtualAlloc preferred address), so&symbolis a live host pointer and the PI shims (osPiStartDma/osPiRawStartDmain libultra.c) service reads as bounds-checked memcpys. This makes both DMA'd assets (models, banks) and direct-read assets (fonts, image display lists) work with unmodified game code.assetstubs.cis deleted; n64stubs.c keeps only the pure-RAM segment symbols (_bssSegmentEnd,_csegment*,_inflate/_gameSegmentVaddr*). US-only for now: EU/JPN manifests have naming inconsistencies (trailing-Z add/drop, region subdirs) that need the same treatment before those regions build.
E. Compile + link milestone (status)
All ~235 translation units compile and the target links: clean build from
scratch is 236/236 steps, zero errors (ninja ge007 -k 0), producing
ge007.x86_64.exe. Remaining warnings (~4.5k) are expected IDO-leniency noise
(int-conversion, implicit declarations, etc.), demoted via the CMake warning
flags.
Phase 1 (boot to window) is done: romdata.c loads and validates the ROM
(baserom.u.z64 at the repo root, or a final .z64 in data/ / next to the
exe), maps it at cart base 0x10000000 (D23); video.c opens an SDL2 640×480
window with a GL context and clears/presents every frame; main.c runs a
demo loop (ESC quits) in place of mainproc(), which is deferred until the
software RSP + scheduler can service real frames. Verified: ROM header passes
validation, 12 MB mapped at 0x10000000, window renders for the full test
duration. Asset symbol spot-checks against the ROM confirm correct offsets
(e.g. music bank BE seqCount=63 @0x419790, first track offset 508 → Mno_music
@0x41998C; sfx/instrument banks start with GE's B1 header).