- D38: implicit function declarations truncated pointer returns on x86-64; added port/include/pc_protos.h prototype shim (398 decls, C-only, anchored in port/shim/PR/ucode.h) + pc_netorder.c for ntohl/ntohs; build warning-clean. - D39: Globalimagetable rebasing - per-symbol offsets + GIMG_OFF() in image_bank.c (== N64 (u32)&sym incl. the 0x02000000 base); segment end markers extended to the true 0x13F8 size (CSV entry truncated by 0x930). - D40: g_ModelHitEntries pool sized for the 40-byte PC ModelHitEntry stride (was a 12KB .bss overflow clobbering is_ramrom_flag -> demo-replay SIGSEGV). - D41: release the GL context from the host main thread after videoInit so the game scheduler thread can bind it (WGL single-thread currency; this SDL2 build's MakeCurrent return value is an inverted ABI artifact). - D42: rsp.c g_gfxTaskSettingsList XOR toggle -> explicit toggle on x86-64. Boot now renders real frames ([NOTE] frame N rendered in ... us). Next blocker D43 (model-file loading ABI mismatch) documented in PCPortResearch.md section F/H; HANDOFF.md rewritten for it.
83 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 1.5 — Boot to first frame (ABI reconciliation)
- Get mainThread through all of
bossInitMainthreadData()→bossEntry()→bossMainloop()and render frame 1. This is the current frontier (post-D31). - 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/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).
F. Phase 2 runtime findings (threads, DRAM, addresses)
Phase 2 replaced the Phase-1 demo loop with the real mainproc() on real OS
threads, compiled GE's real src/sched.c, and brought in PD's fast3d software
RSP (port/fast3d/). The boot path now runs: ROM map → DRAM reserve → video
init → mainThread (real pthread) → bossEntry → bossInitMainthreadData through
mempool init, VI init, rspInit, joyInit + controller-init timers, stanInit,
gameInit — and currently dies inside langInit() (D31).
-
D24 — setjmp/longjmp green threads are unusable on MinGW x64. The first kernel used setjmp/longjmp context switches; under Windows x64 + MinGW the longjmp path corrupts FPU/MC state when it interacts with SEH unwinding (observed: STATUS_DATATYPE_MISALIGNMENT-class crash inside a resumed "thread" with garbage register state). PD's port does not use setjmp at all. Resolved:
port/src/libultra.cnow runs every game thread as a real pthread (PortThreadside table, 8 MB stacks). Message queues keep the N64OSMesgQueuelayout and get aPortQueueside table (mutex + condvar, max 64 queues); all osSendMesg/osRecvMesg paths lock it. A dedicated tick thread posts one VI retrace per frame (NTSC 60 Hz) and services the software timers (both OS_MESG_NOBLOCK — the tick thread can never deadlock).idleThreadis intercepted by ID and parked;mainprocruns as a real pthread so the host main thread is free to pump SDL/OS events (videoPumpEvents()in the host loop — on Windows WndProc only runs on the window-creating thread). The GL context is made current on shedThread per frame viagfx_sdl_make_context_current(). Verified live: VI retrace flowing, all 4 controller-init timers fire, bossmq loop completes. -
D25 — dual-mapped N64 DRAM region. Game code needs two incompatible address forms of the same RAM:
osVirtualToPhysical/s32 arithmetic wants live host pointers that fit in a positive s32, whileoffset | 0x80000000rebuilds (bg.c:3184/3322, propobj.c:8578/8699, title.c:476) want a KSEG0 view at 0x80000000. Resolved: one 8 MB backing store mapped twice — V1 @ 0x70000000 (all game RAM symbols live here) and V2 @ 0x80000000 (byte-identical mirror).port/src/dram.c: Windows usesCreateFileMappingW(INVALID_HANDLE_VALUE,…)+ twoMapViewOfFileEx(…, base)calls (the documented kernel32 API that maps a section at an exact address;NtMapViewOfSectionfails with STATUS_MEMORY_NOT_ALLOCATED 0xc0000045 and segfaults on wrong SECTION_INHERIT values — do not retry it); POSIX uses memfd + two MAP_SHARED|MAP_FIXED mmaps. Both views sanity-checked to alias.port/src/dram_syms.spins the absolute symbols:cfb_16@ 0x70000000 (0x4B000),_bssSegmentEnd@ 0x70050000 (mempool start), tlb block end @ 0x702F4400 (= page_align_down(0x803AB400) − 93×0x2000, the exact N64 value; the tlbmanage stub returns it somempInit's pool size is correct — a NULL return here made the mempool spin inwhile(1)at src/memp.c:164). -
D26 — address-width shims (K0 sign-extension). N64 K0 addresses have bit 31 set; passing them through s32 parameters sign-extends to invalid x86-64 pointers. With the dual map, live RAM sits below 0x80000000 so identity is safe:
port/shim/PR/R4300.h→PHYS_TO_K0(x) = (x);port/shim/PR/os.h→OS_K0_TO_PHYSICAL(x) = (u32)((char *)x - 0x70000000)(GBI w1 words then carry small offsets) andOS_PHYSICAL_TO_K0(x) = (x). fast3d'sseg_addr()(gfx_pc.cpp) resolves both forms: full host pointers pass through, values < 0x800000 get +0x80000000 (landing in V2). Same remap applied to the G_MW_SEGMENT handler. -
D27 — ASLR must be OFF for this exe.
src/bondgame.h:8declaresextern u32 *_bssSegmentEnd;(pointer type), so&_bssSegmentEndin game code emits a.refptr._bssSegmentEndslot holding the absolute value with a BASE relocation. Under ASLR the loader rebases it to runtime_base + 0x70050000 = garbage, andmempCheckMemflagTokensthen spins/AVs with poolAreaStart ≈ 0xF0xxxxxx. (On MIPS&abs_symbolis the symbol value itself; x86-64/PE cannot do that.) Resolved: MINGW link flag-Wl,--disable-dynamicbase(note:--disable-dynamic-baseis NOT recognized by GNU ld 2.47); the image now loads at its preferred base 0x140000000 and relocations are no-ops.main.cfails loudly at startup ifsysImageBase() != 0x140000000ulso any future ASLR regression is a clean error, not silent DRAM corruption. -
D28 — ninja stale-object hazard with new shim headers. If a shim header did not exist when a .obj was last built, it is absent from that object's .d dependency file and ninja will NOT rebuild the TU after the header appears — the old (unshimmed) code silently persists. Observed: boss.c.obj still contained
or $0x80000000,%eax(original PHYS_TO_K0) days after the shim landed. Rule: whenever anything underport/shim/changes, delete all.objfiles and full-rebuild. Verify withgcc -Eusing the exact ninja flags (ninja -C build-pc -t commands <tgt>) plus objdump of the rebuilt object. -
D29 —
osPiStartDmamust post its completion message on every path. The N64 PI posts the caller's OSMesgPI to mq when a DMA completes; GE never inspects the message but always blocks on it (romReceiveMesg()→osRecvMesg(OS_MESG_BLOCK)in ramrom.c:44). The shim did the memcpy synchronously and posted nothing → mainThread deadlocked in the first file load. Resolved: post unconditionally afterpiServiceDma()(also for skipped/dropped DMAs) with OS_MESG_BLOCK, posting the caller's OSMesgPI. -
D30 — crash handler without SEH. MinGW GCC 16 has no
__try/__exceptand does not recognize-fseh-exceptions, so the unhandled-exception filter cannot rely on structured exceptions to protect symbolication. Resolved:port/src/crash.cphase 1 writes raw fault info (registers, modules) toge007.crash.logwithout touching dbghelp (SymInitialize/StackWalk64 can allocate and re-fault inside the filter); phase 2 does a validated manual EBP-chain walk (-fno-omit-frame-pointeron all TUs; each frame is [saved RBP, return address], stop if saved_fp ≤ fp or outside thread stack limits).TerminateProcessinstead of abort (SIGABRT can re-enter the exception machinery).crashDumpThreads()(Toolhelp32 snapshot + SuspendThread + GetThreadContext + StackWalk64 per thread) is called from the heartbeat error dump with TID→name matching; StackWalk64 cannot read DWARF unwind info on MinGW, so multi-frame traces depend entirely on the EBP chain. Symbols are recovered offline:addr2line -e build-pc/ge007.x86_64.exe -f -C <base+rel>(the log's "rel" = address − actual load base). -
D31 — langInit SIGSEGV:
zlib_huft_buildoverflows load_resource's frame via the x86-64-grownstruct huft(RESOLVED). After D24–D29, mainThread reacheslangInit()and dies on the first file load (_fileNameLoadToBank(LnameX_lookuptable[LGUN][…])→ index 670 →fileIndexLoadToBank→mempAllocBytesInBank→load_resource). Crash signature: RIP = 0x140330003 (inside.bss, ing_Props), RSP only ~0xD8 below langInit's entry RSP (a shallow chain — the stack pointer itself is fine), and the frame region above load_resource's return address is filled with 8-byte slots{u32, 0x00000001}where each u32 is the low 32 bits of a live .bss pointer, alternating betweeng_Props(0x14033xxxx) andresource_lookup_data_array(0x14034xxxx). The crash is aretpopping one of these truncated pointers as the return address (observed RIP equals the table value exactly, high word 0x00000001). Something in the load chain is storing 64-bit pointers into s32/int storage (fine on 32-bit MIPS, truncating on x86-64) and that storage overlaps a live frame.Established facts (all under gdb,
gdb -batch -ex "handle SIGSEGV stop"):- All arguments at
load_resourceentry are valid: ptrdata=0x702aa400 (mempool), srcfile->hw_address=0x108ed250, rom_size=0x720, source= 0x702f3ce0.romCopy(PI shim memcpy) completes. zlib_inflate()itself completes normally (finish→ returns 0, rz_wp=3872; the RZ stream is plain deflate after a 2-byte header, so endianness is not an issue in the bitstream). At its entry rz_outbuf = ptrdata ✓, rz_inbuf = source+2 ✓, and the huft table base (tl) sits inside load_resource's 8 KB localbuffer(rz_hlist).- Root cause (confirmed):
struct huft {u8 e; u8 b; union{u16 n; struct huft *t} v;}is 8 B on MIPS (4-byte pointer in the union) but 16 B on x86-64 (8-byte pointer, 8-aligned). GE's gzip-1.2.4 inflate builds its Huffman tables contiguously into load_resource's fixed localu8 buffer[0x2100](8448 B), sized for the 32-bit layout (~1056 entries); on x86-64 only ~528 entries fit, so a stream needing more (observedrz_hufts≈ 797) writes past the buffer and clobbers load_resource's frame — including the return-address slot. The earlier "truncated-pointer table" reading was a misattribution of this same overflow (the 8-byte{u32,0x1}slots are huft records spilling over the frame). - Ruled out by code inspection:
mempAddEntryOfSizeToBank(only touches pool pos/prevpos),fileGetIndex(reads only),decompressdataepilogue (returns rz_wp). load_resourceprologue:push rbp/rdi/rsi/rbx; mov $0x2128,%eax; call ___chkstk_ms; sub %rax,%rsp; lea 0x80(%rsp),%rbp— RBP is a fake frame pointer (RSP+0x80); the real return address is at [RBP+0x20E8]. The compiler reuses pushed-register slots as locals (verified: the saved- rbx slot [RBP+0x20D0] legitimately receivesbuffer+12). Watchpoints on RBP-relative offsets are therefore easy to misplace — compute from entry RSP instead.- Hardware watchpoint on the real retaddr slot (set at the first
instruction, where [RSP] = retaddr): first write changes it from a valid
.text return address to
buffer+12(a pointer into load_resource's own local array), reported RIP at the prologue boundary (push %rdi) — i.e. either a concurrent write by another thread (hardware watchpoints are global) or an 8-byte-attributed access. Athread apply all btat that moment showed every other game thread parked (RtlUserThreadStart / sleep syscalls; gdb could not unwind their stacks further), so the writer is not yet identified.
Resolution (fix in
port/, game code untouched). Mirrors the Perfect Dark port, which replaces its assembly rzip with a real-zlib C impl (pd_port/src/lib/rzip_c.c:inflateInit2+inflate). We excludesrc/game/decompress.c+src/game/zlib.cfrom the PC build (CMakelist(REMOVE_ITEM SRC_GAME …)) and addport/src/rzdecomp.c, which backs the two externally-referenced entry points with host zlib:decompressdata(src,dst,hlist)→inflateInit2(-15)(raw deflate),next_in = src+2(GE's RZ = 2-byte header0x11 0x72+ raw deflate, no size field), loop onZ_OK, returntotal_out.rzipGetSomething()→ returns the storednext_in(consumed input). A generatedport/include/realzlib.h(fromrealzlib.h.in)#includes the absolute host<zlib.h>path, because the game'ssrc/game/zlib.hshadows<zlib.h>on the-Ipath.
Verified: rebuild links clean (238/238); the exe has the new
decompressdata/rzipGetSomethingand nozlib_inflate/zlib_huft_build. Under gdb, mainThread now boots past langInit (all language files load) all the way intobossInitMainthreadData()(boss.c:233) →initWeaponAnimGroups— i.e. the original first-file-load SIGSEGV is gone. The next blocker is D32. - All arguments at
-
D32 — ROM-serialized structs with embedded pointers have divergent N64/x86-64 layout (OPEN). After the D31 fix, mainThread reaches
bossInitMainthreadData()and dies ininit_weapon_animation_groups_maybe()(boss.c:233) →initWeaponAnimGroups→ … →modelAnimReadRootMotionValue(model.c:914), faulting ondesc->bitCountwheredesc = anim->bitDescriptors.Mechanism. N64 pointers are 32-bit; x86-64 pointers are 64-bit with 8-byte alignment.
struct ModelAnimation(src/bondtypes.h:575) declaresModelAnimBitField *bitDescriptorsandu8 *bitStream. In the N64 ROM layout these are 4-byte fields at 0x08 and 0x10; on x86-64 the compiler lays them out as 8-byte fields —ptype /o(gdb) shows bitDescriptors @0x08, a 4-byte hole, then bitStream @0x18, sizeof = 80 (vs 64 on N64). The animation data is loaded byalloc_load_expand_ani_table()(initanitable.c:263) viaromCopy(ptr_animation_table, &_animation_dataSegmentRomStart, size)— raw N64-layout bytes.expand_ani_table_entries()rebases the low 32 bits of bitDescriptors/bitStream but (a) leaves the high 32 bits as adjacent-field junk and (b) writes bitStream's rebase to offset 0x10, which on x86-64 is not where the C struct reads bitStream (0x18). Observed under gdb: anim = 0x702adad4 (base 0x702ad8c0 + 0x214),*(u32*)(anim+8)= 0xc82bd8c0 → as a 64-bit pointer the high word is junk → SIGSEGV.Scope. Any ROM-serialized struct that (i) contains a pointer field and (ii) has other fields after it misreads the same way: each 64-bit pointer's high word holds adjacent-field bytes, and every post-pointer field shifts by the pointer-width delta. ModelAnimation is the first hit; bondtypes.h has ~13 struct blocks with pointer fields (fewer are actually romCopy'd from ROM).
PD ground truth. PD keeps its ROM-data structs at N64 layout on both targets by storing embedded pointers as u32 — e.g.
struct animtableentry { … u32 data; }(pd_port/src/include/types.h:5124) — and casting to a real pointer at the use site. GE's decomp instead uses real pointers in ModelAnimation, so it diverges on x86-64.Resolution — Option 1 chosen (user-approved), in progress. The user approved the PD pattern after confirming PD does it (
animtableentry.u32 data, raw-DMA'd then resolved at use sites). Note PD's exact mechanism (u32 ROM offset + on-demand DMA cache) differs from GE's rebased-DRAM-pointer ModelAnimation, so we adopt the principle (u32 embedded address, cast at use), not PD's code. This required refining non-negotiable #2 (see §H).Part A — struct layout: DONE & proven. Changed
ModelAnimation's two pointer fields to u32 (src/bondtypes.h:575):u32 bitDescriptors@0x08,u32 bitStream@0x10; added casts at the only use sites (model.c:913/921).ptype /onow shows sizeof = 64 with fields at the N64 offsets — layout matches.expand_ani_table_entries()writes s32 to 0x08/0x10 via its ownstruct anim_entry, which now aligns exactly (no change needed there).Part B — pointer-rebase correctness: ROOT-CAUSED (see D33), fix designed, implementation pending. The garbage values are not a bug in
expand_ani_table_entries's arithmetic — the rebase is correct N64 code fed wrong bytes, plus one genuine x86-64 ABI stride bug. Both root causes and the full fix design are in D33 below. -
D33 — ROM file stores structured fields big-endian; animation load needs a per-field endianness fixup + an x86-64 loop-stride fix (root cause of D32 Part B).
Discovery. The
.z64ROM file is not a plain image: multi-byte structured fields (record headers, tables) are stored in big-endian byte order, while bit-packed data streams are raw. Evidence chain:baserom.u.z64is byte-for-byte identical todata/ge007.ntsc-final.z64; the ROM header is GE-specific (magic80 37 12 40fromsrc/rom_header.s, BE u32 CRC at 0x10/0x14 written bytools/n64cksum.c) — not the standard N64 header layout.- Build pipeline:
$(LD)→ ELF →objcopy -O binary --gap-fill=0xff→ RareZip cdata compression (data_compress.sh) →n64cksum. No byte-swap step; the mips64-elf-gcc toolchain (SGI-style BE default) emits BE bytes. tools/utils.hdocuments the convention:swap_bytes()= "convert from v64 to z64 ordering",reverse_endian()= "convert from n64 to z64 ordering" — the project's ".z64" is a word-swapped cart image (community ".n64" format). Real cart = bswap32(file).- Animation-blob analysis: reading headers as BE u32/u16 yields self- consistent values (frame counts, bit widths 0–31, offsets that tile the blob exactly); LE reads yield garbage.
Per-field transform rule (applied at load time to the
animation_datasegment): u32 fields → bswap32; u16 fields → bswap16; u8 fields → identity; bit-packed streams → identity. No single uniform word transform exists (mixed field widths).Blob layout (fully mapped; all 173 entries verified): each C array (
animation_table_ptrs1/2insrc/game/initanitable.c) is a sequence of 20-byte records at thePTR_ANIM_*offsets. Record fields: +0x00 address (entries-segment offset, u32), +0x04 frame count (u16), +0x06 angle bit width (u8, used bymodelAnimReadBitsAsU16Angle), +0x07 loop flag (u8), +0x08 bitDescriptors blob offset (u32), +0x0C bitsPerFrame root motion (u16, used bysub_GAME_7F06D2E4:scaled = unk0C * frame), +0x0E frame size in bits (u16, used byloadAnimationFrame:frameSize = unk0E >> 3), +0x10 bitStream blob offset (u32). Tail (+0x14..+0x3C) is unused by any code — the effective record size is 20 bytes (=struct anim_entry, 5×s32). Interleave rule: entry i's payload [descriptors][stream] sits at [PTR_ANIM_{i-1}+0x14, PTR_ANIM_i); all regions are disjoint; stream size = ceil(frameSizeBits × frames / 8) — exact fit on every entry (fire_standing: 544 bits × 106 frames → 252 bytes). Descriptor array =ModelAnimBitField{u16 bitOffset, u8 bitCount, u8 pad, u16 valueOffset} ×4 (6 bytes each); after the transform all 692 descriptor arrays pack bitOffsets sequentially from 0. Null entries use sentinel 1 (not 0) —expand_ani_table_entriesskips*var_v0 == 1.Validation. Simulated the full transform in Python over all 173 entries: 0 bad descriptor ranges, 0 non-sequential arrays. Root-motion simulation for fire_standing frames 0–3 (widths 6/7/6/0, bitsPerFrame=19) gives smooth per-frame deltas (x = 7, 10, 12, 11; y ≈ 1086–1088) — proving the stream bytes are identity-correct (no transform). The previously observed garbage is reproduced exactly: file bytes
[00 00 01 58]at fire_standing_fast's bd → LE read 0x58010000 + base 0x702ad8c0 = 0xC82BD8C0, the exact value seen under gdb. The "zeros at odd indices" observation was a gdb display artifact — the compiled.dataarray is dense (verified by objdump of build-pc/ge007.x86_64.exe).- Second root cause: x86-64 stride bug in
expand_ani_table_entries. The - loops iterate with
s32** var_v0; var_v0++— +4 bytes/iter on N64, but s32*is 8 bytes on x86-64. Verified in the compiled binary (add $0x8,%rdx)- only even-indexed entries are rebased; odd entries keep raw small offsets (e.g. fire_standing, index 1, would stay 0x144 → instant SIGSEGV on use). This pattern occurs nowhere else in the compiled set.
Fix design (implementation pending — this is the immediate task):
- New port function
romdataFixupAnimationData(u8 *blob, u32 blobSize, const s32 *tableA, const s32 *tableB)inport/src/romdata.c(+ decl inport/include/romdata.h): for each non-null (≠0, ≠1) offset in both tables — bswap the 20-byte record header per-field (u32: +0x00/+0x08/ +0x10; u16: +0x04/+0x0C/+0x0E; u8: +0x06/+0x07 untouched); then transform the descriptor array at [bd, bs) step 6 (bswap16 on words 0 and 4 only). Guards: skip if bd==0 && bs==0; require bd < bs ≤ blobSize and (bs−bd) % 6 == 0. - Call it in
alloc_load_expand_ani_table()(initanitable.c ~line 265) between theromCopyandexpand_ani_table_entries— one line +#include "romdata.h"(port/includeis already on the include path). Mechanical ABI edit per non-negotiable #2; must run before expand (expand reads/writes the fields as LE after fixup). - Fix the stride in
expand_ani_table_entries()(initanitable.c:233): iterate withs32 *instead ofs32**— mechanical ABI fix, semantics- preserving on N64 where it was already correct.
Open item. The entries segment (per-frame joint angles; file offset 0x124AC0, 0x169EC0 bytes) is left as identity for Phase 1.5 — not consumed at boot (only during rendering). Verify visually in Phase 2 when Bond first animates.
D34 ANIM_DATA_* address placeholders break on x86-64. On N64 the
animation_data segment links at VMA 0, so (s32)&ANIM_DATA_x is a small
segment offset; on PC &ANIM_DATA_x is a high PE address and the game code's
ubiquitous s32 truncation yields garbage. Audit: all 82 uses are
address-taking only ((s32)&…, or through ANIM_FRAC() /
ANIM_FRAC_MUL_FIRST() in chraction.c/bondview2.c/title.c/
initBondDATAdefaults.c/initactorpropstuff.c) — no data dereferences, no
sizeof, no static initializers. Fix: on x86-64 each ANIM_DATA_x is
redefined as an lvalue at g_pc_animdata_base + offset where the base is the
PE image address with its low 32 bits zeroed (derived in romdataInit() from
a static probe; fail-fast if not 4 GiB-aligned), so
(s32)&ANIM_DATA_x == PTR_ANIM_x exactly. Files:
assets/animationtable_data.h (guarded macro branch, N64 externs kept under
#else), port/src/romdata.c (g_pc_animdata_base + init),
CMakeLists.txt (excludes assets/animationtable_data.c +
animationtable_entries.c on PC — the arrays are address-only placeholders;
the ROM is the data ground truth via romCopy). Verified under gdb:
D_80030984 = ANIM_FRAC(ANIM_DATA_walking) executes; the walking record at
ptr_animation_table + 0x4018 has entry field 0x10177dcc
(entries base 0x10124AC0 + PTR_ANIM_ENTRY_walking) and frame count 0x25;
after initWeaponAnimGroups() the derived globals hold sane floats
(4.59/11.25/16.71/16.19).
D35 Music sequence table: pointer-width + endianness.
musicSeqPlayerInit() faulted in romCopy() with a NULL destination:
tblSegmentSize = sizeof(RareALSeqData) * seqCount + 4 was garbage because
(1) RareALSeqData.address was u8 * — sizeof 16 on x86-64 vs the ROM's
8-byte records — and (2) the header seqCount and entry fields are stored
big-endian in ROM but read as LE (63 → 0x3F00 = 16128). Fix: address →
u32 (src/music.h; sizeof 8 on both platforms) with (void *) casts at the
three use sites in src/music.c; new romdataFixupMusicSeqTable() in
port/src/romdata.c bswaps seqCount + per-entry address(u32)/
uncompressed_len(u16)/len(u16), called after both romCopys in
musicSeqPlayerInit(). Verified: the header-only (0x10-byte) copy logs the
expected seqCount 63 exceeds blob capacity 1 clamp; boot passes the old
fault.
D36 Music heap / PERMANENT pool sizing on x86-64. alCSPNew() SIGSEGV'd
because the music bump heap was oversubscribed: x86-64 libaudio runtime
structs are larger (ALVoiceState 88B, ALSeqPlayer 248B, ALCSPlayer 224B, …)
and the debug-token state (tokenFind(1,"-level_")==NULL →
g_DebugAndUpdateStageFlag=1 → stale -ml0 -me0 tokens) forces the
fixed-size mempool branch with PERMANENT at 296 KiB. Measured under gdb:
pre-music PERMANENT usage 0x1BCA0 left 189,280 B for music vs a measured init
demand of 0x31660 (shortfall 13,056). Fix (PC-only guards):
MUSIC_ALLOCATION_BYTES 0x2E000 → 0x32000 (src/music.c); PERMANENT fixed
branch 296/308 KiB → 320/340 KiB (src/memp.c, non-JP/JP). STAGE absorbs the
difference. Verified: boot reaches bossMainloop().
D37 libaudio bank trees must be re-laid out, not patched in place. The
audio thread SIGSEGV'd in __initFromBank() (b=0xffffffff00000000):
ALBankFile/ALBank/ALInstrument/ALSound/ALWaveTable/… are serialized in ROM
as big-endian scalars + 4-byte packed table-relative offsets, while the x86-64
C structs use 8-byte pointers — so BE values were LE-misread and 4-byte offset
arrays were walked with 8-byte stride, producing wild rebase pointers in
alBnkfNew()/_bnkfPatch*(). In-place conversion is impossible: each struct's
expanded tail overlaps whatever follows it in ROM (e.g. the ALWaveTable book
slot at +24 lands inside the ALADPCMBook that sits 12 bytes after the
wavetable). Fix: romdataAudioBankPcSize() + romdataFixupAudioBank() in
port/src/romdata.c — a two-pass DFS re-layout into a compact image where
every sub-struct is placed once (8-byte aligned, children before parents) and
each pointer slot stores the sub-struct's NEW offset from the image start,
zero-extended; alBnkfNew()/_bnkfPatch*() then rebase unmodified
(ptr + (s32)file). ALWaveTable.base stays an offset into the separate
wavetable data segment — _bnkfPatchWaveTable() adds its table argument,
verified at runtime (sfx: base+table = 0x102F19A0 =
_sfxtblSegmentRomStart). music.c PC branch allocates the re-layout size
and fixups after romCopy. Memory: bank images grow +0x1460 (sfx) / +0xA70
(instruments); init-time heap demand measures 0x33530, so
MUSIC_ALLOCATION_BYTES → 0x38000 and PERMANENT → 352/368 KiB (PC only).
Verified under gdb: runtime tree matches ROM ground truth (SFX instCount=1,
soundCount=261, bendRange=200, sampleRate=22050; INSTR instCount=75; all
env/keymap/wavetable/book/loop pointers valid heap addresses; book
order=2 npredictors=1); 25 s soak with every thread alive. Implementation
bugs caught during bring-up: a double bswap (afRd* already decodes BE),
afWr32 initially wrote BE, and the size pass used an uninitialized visited
array (undercount) — all fixed.
D38 Implicit function declarations truncate pointer returns on x86-64.
~72 game TUs call ~400 functions with no visible prototype (missing #include
of the declaring header). Under C11 an implicit declaration assumes
int f(): harmless on N64 (32-bit pointers) but on x86-64 it silently
truncates every pointer return to 32 bits — e.g. tokenFind() in
set_mt_tex_alloc() returned a low-32-bit "pointer" that faulted in
strtol(). Fix: a port-layer prototype shim, port/include/pc_protos.h
(398 declarations; one-shot generator scripts/gen_pcprotos.py — the committed
header is the source of truth, hand-adjusted after generation), declaring each
function with its TRUE return type and an empty parameter list — purely
additive (only the returned value's width changes). Anchored in
port/shim/PR/ucode.h, the LAST include of <ultra64.h>: anchoring earlier
(e.g. gbi.h) poisons libaudio.h because the bondtypes chain reaches
snd.h → <PR/libaudio.h> while ultra64.h is still mid-parse. C-only guard:
C++ fast3d TUs reach this header via SDL_stdinc.h → <stdarg.h> → port/shim/stdarg.h, and pulling the bondtypes/bondconstants chain into C++
brakes on struct ALSoundState* in src/bondtypes.h; no C++ TU needs a
fix here. C11 gotcha: an empty parameter name list () cannot match a true
prototype with default-promoted parameters (s8/u8/s16/…), so the 11
such functions get full prototypes with their TRUE parameter types
(substituting int for s8 is NOT compatible). Also: src/bondconstants.h
defines function-like macros ntohl/ntohs over CharArrayTo32/16, which
collide with MinGW <winsock.h> in TUs that parse both — neutralized in the
port shim, real host functions provided in port/src/pc_netorder.c with
winsock-compatible signatures (u_long is 64-bit on LLP64!). No game-TU
edits. Result: build clean, zero warnings; the old strtol() crash is gone.
D39 Globalimagetable rebasing idiom breaks on x86-64. texReset() /
texLoadFromDisplayList() compute ROM-layout pointers as
globalbank_rdram_offset + (u32)&sym with
globalbank_rdram_offset = (u32)pGlobalimagetable + 0xFE000000; on N64
(u32)&sym = 0x02000000 + off and the 0x02 base cancels the 0xFE. On PC
(u32)&sym is a truncated PE address → garbage DL pointers. Fix (in
src/game/image_bank.c, PC branch): an enum of all 49 segment offsets (17
Gfx display lists + 32 sImageTableEntry tables) and
#define GIMG_OFF(sym) (0x02000000u + g_pc_gimg_off_##sym) — exactly the N64
(u32)&sym; all 49 texReset() sites replaced. No changes needed in
gunfire.c / texLoad / texSelect: all game RAM is s32-safe (V1 view,
0x70xxxxxx < 4 GiB), so the u32 math and pointer casts stay lossless.
Also fixed the segment markers: the CSV asset Globalimagetable.bin
(0xAC8) is TRUNCATED — the real linker segment (ge007.ld: oddtextures.o
.data) spans 0x13F8 = Gfx DLs (0xAC8) + ITE tables (0x930); the CSV's
rarewarelogo entry is mis-split (the logo actually starts at ROM
0x29E560). _GlobalimagetableSegment{Rom}End 0x1029DC28 → 0x1029E558 in
port/src/romassets_u.s + a size override in scripts/gen_romassets.py
(regeneration verified stable). All 49 offsets verified against the ROM:
the symbols tile [0x29D160, 0x29E558) exactly with no gaps; gun.c
ammo IconImage values cross-check. Verified under gdb: all 17
texLoadFromDisplayList diffs correct (0, 0x78, 0x120, …); boot passes
texReset. Known Phase-2 follow-up: explosion.c's
g_ExplosionDisplayLists[] is a static table of EXE addresses to Gfx arrays
(16-byte entries on PC) — it must point at the ROM-layout copies in
pGlobalimagetable before explosions render.
D40 N64-sized BSS placeholder pool overflows with pointer-stride structs.
initModelHitEntryFreeList() (initunk_005450.c:38, called from
lvlStageLoad) walks 600 ModelHitEntry records writing next/prev; the
decompiler-emitted BSS chain in objecthandler.c (char g_ModelHitEntries[0xC];
…dwords… char g_ModelHitEntriesPenultimate[0x28]) reproduces N64's
12 000-byte region (600 × 20 B), but PC ModelHitEntry is 40 bytes → the
free-list init writes 24 000 bytes, a 12 KB .bss overflow that clobbered
is_ramrom_flag (writer caught with a hardware watchpoint) → the
demo-replay path in bossMainloop ran with address_demo_loaded == NULL →
SIGSEGV at ramromreplay.c:341. Fix: PC branch declares a properly sized pool
(char g_ModelHitEntries[600 * sizeof(ModelHitEntry)]); the N64 chain stays
verbatim under #else; the sentinel assignment in initunk_005450.c is
computed directly on PC (entries[LEN-1].prev = &entries[LEN-2] — N64's
g_ModelHitEntriesPenultimate labels the start of entry 598). Verified: no
more SIGSEGV; boot progresses into GL rendering.
D41 Cross-thread GL context binding. WGL allows a context to be current
on only ONE thread at a time. The window+context are created and made
current on the host main thread (videoInit → gfx_sdl_init), but all
rendering runs on the game's scheduler thread; while main still holds the
context, wglMakeContextCurrent from the game thread fails with "The
requested resource is in use" — and this SDL2 build (2.32.10, MSYS2)
swallows it: SDL_GL_MakeCurrent returns true on failure / 0 on success
(an inverted int-as-bool ABI artifact; confirmed by a minimal repro and
gdb $1 = 255). The silent failure left wglGetCurrentContext() == NULL
on the game thread → glCreateShader returned 0 → "Vertex shader
compilation failed" with an empty info log. Fix: new
gfx_sdl_release_context() (SDL_GL_MakeCurrent(NULL, NULL)) called at the
end of videoInit() after set_swap_interval (the last GL work on main);
the scheduler thread re-binds per frame via the existing
gfx_sdl_make_context_current() in videoStartFrame(). Do NOT branch on the
MakeCurrent return value (unreliable). Verified: frame 1 renders (69.7 ms),
several frames before the next (separate) fault.
D42 rsp.c task-settings toggle idiom truncates pointers.
g_gfxTaskSettingsList = (GfxInfo_s*)((u32)list ^ (u32)&g_gfxTaskSettings[0] ^ (u32)&g_gfxTaskSettings[1]) toggles between two adjacent settings structs
by XOR — fine on 32-bit, but on x86-64 the u32-truncated PE addresses XOR to
garbage → the next frame's ((GfxInfo_s*)g_gfxTaskSettingsList)->cfb = …
(fr.c:458) SIGSEGV'd. Fix: explicit toggle under
#if defined(__x86_64__) (list == &[0] ? &[1] : &[0] — same semantics,
the list is always one of the two); N64 line verbatim under #else.
D43 (OPEN) Model-file loading ABI mismatch. Stage object load faults in
modelPromoteNodeOffsetsToPointers() (model.c:5688). Model files are
ROM-serialized with N64 layout (ModelFileHeader 24 B, ModelNode 20 B,
4-byte pointer fields) but on PC are read as 8-byte-pointer structs (48 B
each): load_object_fill_header() derives RootNode from numtextures
at the wrong offset, and the PROMOTE rebase ((u32)var + diff) then
operates on misaligned fields. Needs a D37-style re-layout of the model
file image (header + switches array + texture table + node tree +
ModelRoData records). Phase-2 project; see §H.
G. Phase 2 status (current)
Done: PD fast3d integrated (port/fast3d/, replaces the deleted gfxstub.c);
GE's real src/sched.c compiled (shedThread runs __scMain); real pthread
kernel (D24); dual-mapped DRAM + address shims (D25/D26); ASLR off with startup
sanity check (D27); PI completion messages (D29); SEH-free crash handler with
thread dumps (D30). The process now boots, maps the ROM, opens the window,
runs mainproc() → bossInitMainthreadData through controller init / timers /
mempool / VI / rsp / stan / game init. The D31 langInit file-load SIGSEGV is
fixed (real-zlib decompression, §F/D31): mainThread now loads all language
files and proceeds into bossInitMainthreadData() → alloc_load_expand_ani_table
→ init_weapon_animation_groups_maybe (boss.c:233), where it SIGSEGVs on the
D32 ROM-struct pointer-width issue. D32 is decomposed: Part A (struct
layout — u32 pointer fields) is DONE & proven; Part B (the load-time pointer
rebase in expand_ani_table_entries) is OPEN and is the current blocker. No
frames rendered yet. See §H for the handoff + plan.
Reference — the file-load chain (for D31 debugging):
langInit (language.c:230) loads 7 text banks via
_fileNameLoadToBank(LnameX_lookuptable[bank][j_text_trigger], …, 0x100, MEMPOOL_PERMANENT) → fileGetIndex (strcmp over file_resource_table) →
fileIndexLoadToBank(index, …) (ob.c:216): if poolRemaining==0 take
mempGetBankSizeLeft(bank); ptrdata = mempAllocBytesInBank(…); hw_address
nonzero → load_resource(ptrdata, poolRemaining, &file_resource_table[index], &resource_lookup_data_array[index]) (ob.c:37): source = (ptrdata + bytes) − ((rom_size+7)&−8); romCopy(source, hw_address, rom_size);
decompressdata(source, ptrdata, buffer) with u8 buffer[0x2100] on the
stack → sets rz_inbuf=source+2 / rz_outbuf=ptrdata / rz_hlist=buffer →
zlib_inflate() (src/game/zlib.c:668). rom_size for index 670 = 0x720,
decompressed size = 3872 (0xF20). file_resource_table is compiled
(assets/obseg/file_resource_table.inc.c) with hw_address = cart addresses;
rom_size = adjacent-marker differences computed in obInit.
Debugging techniques that worked: gdb launch mode (gdb -batch -ex "handle SIGSEGV stop" -ex run … — attach fails with error 87); breakpoint
chains through init functions to bracket the crash; finish to let a suspect
function complete and inspect its return; hardware watchpoints on exact stack
slots (compute offsets from entry RSP, not RBP — see D31); addr2line for
offline symbolication; objdump + gcc -E with exact ninja flags to verify
shim expansion in compiled code.
H. Handoff & plan (current session)
State. Boot is complete AND the first frames render: mainThread runs through
all of bossInitMainthreadData() + bossEntry(), enters bossMainloop(),
loads the stage, and fast3d renders real display lists — [NOTE] frame N rendered in … us logs appear (frame 1 in ~70 ms). Findings D34–D42 (§F) closed out
the boot + first-render blockers on top of D31–D33:
- D34
ANIM_DATA_*→ address-only lvalues at PE-image-base + ROM offset. - D35 music seq table:
RareALSeqData.address→u32+ BE fixup. - D36 music heap / PERMANENT pool grown for x86-64 libaudio struct bloat.
- D37 libaudio bank trees re-laid out to PC-native form (
romdataFixupAudioBank). - D38 implicit-declaration pointer truncation →
port/include/pc_protos.hprototype shim (398 decls, generator in build-pc/); build now warning-clean. - D39 Globalimagetable rebasing: per-symbol offsets +
GIMG_OFF()macro (== N64(u32)&sym, incl. the 0x02000000 base) in image_bank.c; segment end markers extended to the true 0x13F8 size (CSV truncation). - D40 ModelHitEntry BSS pool sized for the 40-byte PC stride (was 12 KB
overflow clobbering
is_ramrom_flag). - D41 GL context released from the host main thread after init so the game thread can bind it (WGL single-thread currency; SDL2 MakeCurrent return value is an inverted ABI artifact — don't branch on it).
- D42 rsp.c
g_gfxTaskSettingsListXOR toggle → explicit toggle on x86-64.
ROM provenance locked: the working ROM is byte-identical to the No-Intro
good dump GoldenEye 007 (U) [!].z64 (MD5 70c525880240c1e838b8b1be35666c3b,
0xC00000 bytes, country 'E'); embedded header CRCs DCBC50D1/09FD1AA3
re-computed and matched via tools/n64cksum.c's CIC-6102 routine — see
tools_pc/romverify.c (one-shot re-check) and docs/HANDOFF.md Environment.
Current blocker — D43: model-file loading ABI mismatch. After a few frames,
stage object load faults in modelPromoteNodeOffsetsToPointers()
(model.c:5688). Model files are ROM-serialized with N64 layout (ModelFileHeader
24 B / ModelNode 20 B, 4-byte pointer fields) but read on PC as 8-byte-pointer
structs; load_object_fill_header() (objecthandler_2.c) derives RootNode from
numtextures at the wrong offset before any rebase. Plan: a D37-style two-pass
re-layout of the model file image in port/src/romdata.c — header, switches
array, texture table, ModelNode tree (walked via Child/Next/Parent), and the
per-opcode ModelRoData records — with every embedded pointer rewritten as a
zero-extended offset from the image start so the existing
PROMOTE/diff = fileramaddr - vma rebase works unmodified. Map the record
types first (bondtypes.h ModelRoData union, opcodes in bondconstants.h);
validate one small model against ROM ground truth under gdb before generalizing.
Expect more D32-class ABI fixes as other per-level asset types are first
touched — standing sub-task, not a one-off.
Audio (Phase 3 prep). Bank trees are correctly laid out at boot and the
libaudio manager + audio thread run without crashing; actual sound output still
needs an SDL backend for alDriver, and the ASP strategy question (C2) remains
open. No work required until rendering is stable.
D32 repeatable fix procedure (apply to any ROM-serialized struct that faults on a pointer-field read):
- At the fault,
ptype /o <Struct>in gdb. If a pointer field's offset/size diverges from the N64 offset comment (e.g. an 8-byte pointer where N64 has 4), it is this bug class. - Change the embedded pointer fields to
u32in the struct (keeps N64 layout on x86-64). Add casts at every use site ((T *)field). Document as D3x. - Verify the load-time rebase/fixup writes valid V1 DRAM addresses (< 0x80000000) into those u32 fields; if not, debug the fixup (see Part B below).
- Rebuild and confirm the boot advances past this struct to the next init step.
- If the struct is a tree of packed sub-structs (like the libaudio banks, D37,
or model files, D43), an in-place BE→LE patch will not fit — the expanded
8-byte pointer slots overrun the following ROM data. Re-lay it out into a
fresh compact image and rewrite each pointer slot as the sub-struct's new
offset (zero-extended), so the existing
ptr + (s32)baserebase still works. - If the fault is NOT in ROM-loaded data but in an address arithmetic idiom over
exe-resident symbols (
(u32)&sym, XOR toggles, pointer-delta math — D39/D42), guard a PC branch that reproduces the N64 32-bit value exactly (e.g. keep the 0x02000000 base) or replace the idiom with an explicit equivalent; N64 line stays verbatim under#else.
Non-negotiable #2 refinement (applied to AGENTS.md). The original "game code
compiles unmodified / fix belongs in port/" is too absolute: pointer-width layout
cannot be isolated in port/ (no hook between the romCopy and the first read).
Refined to "game logic is unmodified" with a narrow, documented exception for
mechanical, semantics-preserving ABI/layout changes forced by the 32→64-bit
transition (embedded pointers → u32 + cast at use; PC-guarded pool sizing),
following PD ground truth. No logic/behavior changes; each such edit is logged in
§F/D3x.
Environment reminders. MSYS2 tools in /c/msys64/mingw64/bin/ (not on PATH —
prefix export PATH=…). Build: ./build-pc.sh ntsc-final. gdb launch mode
only (attach fails, error 87); symbolicate offline with addr2line -e build-pc/ge007.x86_64.exe -f -C <0x140000000+rel>. Image base 0x140000000.
load_resource/many init fns use a fake RBP — compute stack offsets from entry
RSP. The D30 crash handler did not write ge007.crash.log for the game-thread
SIGSEGV this session (attribute via gdb instead; worth a separate look).