From 436484a926651f41a3872afae6390f64a0189824 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Sun, 13 Sep 2026 23:51:27 -0400 Subject: [PATCH] docs: final v0.2.0 pre-tag pass - Drop-in ROM wording everywhere (README quick start, Pages landing, release notes, bundled README): first launch auto-generates the derived assets via the frozen converter; no Python/toolchain needed. PAL/JP marked not-yet-supported with issue #85 pointers. - building.md: Linux build is CI-compiled and ships in the bundle (was 'never built or run on Linux'); bundle note now describes the frozen ge007-convert auto-spawn; PAL/JP sidecar caveat (D258). - Status rewording across README/Pages/release notes: drop the 'first open-source port to our knowledge' claim; cutscene glitch description aligned to 'mostly with James Bond'; index.md status no longer contradicts the open D255 Deck crash. - Em-dash cleanup across all GitHub Pages docs (index, building, internals, porting-notes, dev-process, agentic-development, release notes); internal dev logs untouched. --- .github/release-notes.md | 53 ++--- README.md | 133 ++++++------ docs/building.md | 32 +-- docs/dev-process.md | 46 ++-- docs/dev/agentic-development.md | 52 ++--- docs/index.md | 97 +++++---- docs/internals.md | 114 +++++----- docs/porting-notes.md | 372 ++++++++++++++++---------------- tools_pc/dist/README.md.in | 46 ++-- 9 files changed, 476 insertions(+), 469 deletions(-) diff --git a/.github/release-notes.md b/.github/release-notes.md index 8997542d..062faf8e 100644 --- a/.github/release-notes.md +++ b/.github/release-notes.md @@ -1,4 +1,4 @@ -## GoldenEye 007 PC Port — +## GoldenEye 007 PC Port

> The full single-player campaign runs at a steady 60 fps with audio (music + -> SFX) playing throughout, on Windows and Linux including Steam Deck — and it +> SFX) playing throughout, on Windows and Linux including Steam Deck, and it > is completable end to end: the whole campaign has been playtested through -> all 21 missions (Agent difficulty). To our knowledge this is also the first -> open-source port of the original N64 game to ship as a public release. It's -> an early public cut: all 21 solo missions load and run crash-free, but the -> known issues below are real — feedback is very welcome. +> all 21 missions (Agent difficulty). It's an early public cut: all 21 solo +> missions load and run crash-free, but the known issues below are real; +> feedback is very welcome. ### What's new since v0.1.0 +- **Drop-in ROM, no tooling**: unpack the bundle, drop your NTSC-U (US) ROM + in `data/`, launch; the first run generates the derived assets + automatically. No Python, no installer, nothing to set up. - **Steady 60 fps** in normal play (the software RSP runs off the presentation critical path); `Video.DisplayFPS` in the F10 overlay shows it. - **Audio**: in-level music and sound effects throughout (the alpha was @@ -26,38 +28,39 @@ - **Rendering**: outdoor skies render correctly; water no longer renders green/pulsing; reflective surfaces (glass, chrome weapon skins) work. - **Crash fixes**: the two v0.1.0-era crashing levels (Bunker ii, Statue) and - the AI-pacing bug that broke Cradle are fixed and playtest-verified — all 21 + the AI-pacing bug that broke Cradle are fixed and playtest-verified: all 21 solo missions load and run crash-free. Also fixed: the Steam Deck / Linux Facility crash triggered by walking into the crouch-forcing spot at the - level's opening (D253) — this also revives the auto-crouch and ladder + level's opening (D253); this also revives the auto-crouch and ladder signals, which were dead on PC before. - **QoL**: F10 in-game options overlay (fullscreen, resolution, frame cap, MSAA, texture filtering, FOV/draw distance, sensitivity), F12 screenshot. -- **Everything unlocked by default** — all 21 solo levels are selectable at +- **Everything unlocked by default**: all 21 solo levels are selectable at every difficulty from the first launch, 007 mode is available, and the cheat menu is fully populated. No *active* cheats are enabled (weapons remain per-mission pickups, as on N64); F10 → *All unlocked* switches back to faithful N64 progression. -- **Steam Deck first-run preset** — on SteamOS the first launch seeds +- **Steam Deck first-run preset**: on SteamOS the first launch seeds Deck-friendly defaults (native 1280×800 fullscreen, VSync, MSAA 4, 150% draw/LOD distance); an existing `ge007.ini` always wins. Two more F10 rows: *No hit flash* (suppresses the damage-flash overlay) and *Skip intro*. - **Modern dual-stick controller layout** (the scheme used by the console re-releases): left stick move/strafe, right stick look, right trigger fire, left trigger aim, A/X use, B/Y crouch/cancel, **RB/LB cycle weapons**. -- **Linux / Steam Deck**: the Linux bundle now ships its own SDL2 — it runs +- **Linux / Steam Deck**: the Linux bundle now ships its own SDL2, so it runs as-is on any distro, and sideloads onto a Steam Deck with nothing installed. Saves and F10 settings now persist no matter which directory you launch from (previously Linux wrote them relative to the launch directory - only, so they silently failed elsewhere — D256); the options overlay is + only, so they silently failed elsewhere; D256); the options overlay is fully gamepad-driven on the Deck. ### Known issues -- **Cutscenes frequently glitch** — skipped beats, wrong camera, misplaced or - hovering actors, wrong timing; the Dam level-end cutscene is racy (D243). - The most visible gap in this release. -- **Particle colours cycle through a rainbow palette** — bullet-impact sparks +- **Cutscenes still glitch, mostly with James Bond**: in scripted sequences + Bond is the one who gets misplaced, hovers, or spins; the other actors are + fine for the most part now. The Dam level-end cutscene is racy (D243). + Still the most visible gap in this release. +- **Particle colours cycle through a rainbow palette**: bullet-impact sparks and lingering smoke/explosion residue drift through the hues over time instead of holding their intended grey/orange palette (D252). - Water on `IsWater` levels shows a moving seam between two patterns (D245); @@ -69,14 +72,14 @@ selected** (e.g. the MSAA value appears both on its own row and again at the panel bottom); earlier builds showed it as an intermittent 4K- fullscreen ghost of the top row. -- **Steam Deck: one intermittent crash remains** — the v0.1.0-era Facility +- **Steam Deck: one intermittent crash remains**: the v0.1.0-era Facility crash is confirmed fixed on Deck hardware, but a SIGSEGV can occasionally occur during heavy firefights (D255; seen on Bunker and Frigate). The faulting-register capture in `ge007.crash.log` has pinned it: an object - record whose model reference is NULL while the record is still active — + record whose model reference is NULL while the record is still active; i.e. something in a mid-firefight destruction leaves the record live one tick too long. We're tracking down the exact destruction path; more Deck - playtime across levels is welcome — please report any Deck-specific faults. + playtime across levels is welcome; please report any Deck-specific faults. ### Downloads @@ -87,13 +90,13 @@ Each contains the engine executable, a README, license texts, and the `prepare-assets` tool. **No ROM, no game assets.** The Windows bundle carries -its runtime DLLs; the Linux bundle carries SDL2 — on both platforms nothing +its runtime DLLs; the Linux bundle carries SDL2, so on both platforms nothing needs to be installed first. ### Running it You supply your own **GoldenEye 007 N64 ROM** (`.z64`, big-endian) that you -legally own — the **NTSC-U (US)** release is what this build supports (PAL / JP +legally own: the **NTSC-U (US)** release is what this build supports (PAL / JP are on the roadmap). No Python, no toolchain, nothing to install. 1. Unpack the archive. @@ -103,13 +106,13 @@ are on the roadmap). No Python, no toolchain, nothing to install. seconds: it detects your ROM, generates the two derived asset folders (`data/pcmodels-ntsc-final/`, `data/pccg-ntsc-final/`) once, and saves them for every future run. (The generator is `prepare-assets/ge007-convert` - inside the bundle; you can also run it manually — it prints what it's doing.) + inside the bundle; you can also run it manually; it prints what it's doing.) **Steam Deck:** sideload the unpacked folder (USB or a file manager), do steps 2–3, then add the executable to Games → *Add Game* as a non-Steam game. -**In-game settings on the Deck:** the options overlay is fully gamepad-driven -— it opens with **Select**, the D-pad or left stick (up/down) moves between +**In-game settings on the Deck:** the options overlay is fully gamepad-driven: +it opens with **Select**, the D-pad or left stick (up/down) moves between options, **A** steps the selected option forward, **B** steps it back, and **Start** (or Select again) closes. No keyboard needed. @@ -124,7 +127,7 @@ sha256sum -c goldeneye-pc-port--linux-x86_64.tar.gz.sha256 ### Source & docs - — built on the +, built on the [GoldenEye 007 decompilation](https://github.com/n64decomp/007), architecture after the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark). Non-commercial fan preservation/research project; not affiliated with any diff --git a/README.md b/README.md index 6ed5a44b..129f4c6b 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,10 @@ in a dedicated `port/` layer, following the architecture of the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark), the same Rare "Indy" engine family, one hardware generation apart. -**v0.2.0** is out for Windows and Linux — including Steam Deck, where the -Linux bundle sideloads as-is — and runs the full campaign at a steady 60 fps -with known rough edges ([Status](#status)). To our knowledge it's also the -first open-source port of the original N64 game to ship as a public release: -free to download, build on and modify (you bring the ROM). +**v0.2.0** is out for Windows and Linux (including Steam Deck, where the +Linux bundle sideloads as-is) and runs the full campaign at a steady 60 fps +with known rough edges ([Status](#status)). Free to download, build on and +modify (you bring the ROM). It's also a case study in AI-agent collaboration on a large, low-level codebase: two coding agents, driven by one person part-time, porting ~230 @@ -26,47 +25,45 @@ See [Background](#background). > [!IMPORTANT] > **You must supply your own GoldenEye 007 ROM.** This repository contains no > Nintendo code or assets, and no ROM. Nothing here is distributable as a -> playable game — see [Requirements](#requirements) and [Legal](#legal). +> playable game: see [Requirements](#requirements) and [Legal](#legal).

- Bunker 1, in-engine - ~32 s gameplay montage from live play sessions - Dam, in-engine -
All in-engine, running in the port — Bunker 1 and Dam captures plus a - ~32 s gameplay montage loop from the v0.2.0 playtest. +
All in-engine, running in the port, a ~32 s gameplay montage loop + from the v0.2.0 playtest.

## Download | Platform | Bundle | Notes | |---|---|---| -| **Windows** (x86_64) | [win64.zip](../../releases) | Engine + runtime DLLs + the one-time asset tool. | -| **Linux** (x86_64) / **Steam Deck** | [linux tarball](../../releases) | SDL2 is bundled — runs as-is on any distro, and sideloads onto a Deck with nothing installed. | +| **Windows** (x86_64) | [win64.zip](/jkdansereau/goldeneye-pc-port/releases) | Engine + runtime DLLs + the one-time asset tool. | +| **Linux** (x86_64) / **Steam Deck** | [linux tarball](/jkdansereau/goldeneye-pc-port/releases) | SDL2 is bundled, so it runs as-is on any distro, and sideloads onto a Deck with nothing installed. | -Both bundles contain **no ROM and no game assets** — you supply your own +Both bundles contain **no ROM and no game assets**: you supply your own (see [Requirements](#requirements)), which keeps the release legal to distribute. Earlier builds: v0.1.0 alpha, same page. You can also build it yourself; see [Building](#building). ### Quick start -You need a GoldenEye 007 N64 ROM (`.z64`, big-endian; NTSC-U recommended — all -three regions are supported). No ROM or game asset is included or distributed. -Then: +You need a GoldenEye 007 N64 ROM (`.z64`, big-endian). This release supports +the **NTSC-U (US)** version; PAL and JP are on the roadmap ([issue +#85](/jkdansereau/goldeneye-pc-port/issues/85)). No ROM or game asset is +included or distributed. Then: -1. Download the Windows or Linux bundle from [Releases](../../releases) and unpack it. +1. Download the Windows or Linux bundle from [Releases](/jkdansereau/goldeneye-pc-port/releases) and unpack it. 2. Make a `data/` folder next to the executable and drop the ROM in as `ge007.ntsc-final.z64`. -3. Run the one-time asset step: `python3 prepare-assets/prepare-assets.py` (Python 3.8+, stdlib only). -4. Launch the executable from that folder. +3. Launch the executable from that folder. The first run takes a few extra seconds: it detects the ROM and generates the derived asset folders once (no Python or other tooling needed). -Read the [Status](#status) caveats first — v0.2.0 has known rough edges, +Read the [Status](#status) caveats first: v0.2.0 has known rough edges, listed plainly there. ## Status -**v0.2.0 — playable, with known rough edges.** The full single-player -campaign is completable end to end (all 21 missions, Agent difficulty — +**v0.2.0 - playable, with known rough edges.** The full single-player +campaign is completable end to end (all 21 missions, Agent difficulty, playtested), at a steady 60 fps; all 21 solo missions load, render and run crash-free, verified on Windows, Linux and real Steam Deck hardware. Feedback is very welcome. @@ -74,8 +71,7 @@ is very welcome. **Working:** boot sequence and front end (menu → mission select → briefing → start); all 21 solo missions load, render and are crash-free (full campaign playtested end to end at Agent difficulty); steady 60 fps -(software RSP off the presentation critical path); full audio — in-level music -and SFX; keyboard + mouse (click-to-lock, proportional aim mode) and a modern +(software RSP off the presentation critical path); full audio: in-level music and SFX; keyboard + mouse (click-to-lock, proportional aim mode) and a modern dual-stick controller layout; file-backed saves; all levels, 007 mode and the full cheat menu unlocked by default (F10 → *All unlocked* restores faithful N64 progression); F10 in-game options overlay (resolution, frame cap, MSAA, filtering, FOV, sensitivity); @@ -83,17 +79,16 @@ Windows and Linux. **Known issues:** -- **Cutscenes glitch frequently** — skipped beats, wrong camera, misplaced or - hovering actors, wrong timing; the Dam level-end cutscene is racy (D243). - The most visible gap in this release. -- A few in-level music tracks sound wrong — a wrong-sounding bass instrument - on some elevator/level tracks (D230). +- **Cutscenes still glitch, mostly with James Bond**: in scripted sequences + Bond is the one who gets misplaced, hovers, or spins; the other actors are + fine for the most part now. The Dam level-end cutscene is racy (D243). + Still the most visible gap in this release. - Particle colours drift through a rainbow palette instead of holding their grey/orange intent (bullet sparks, lingering smoke/explosion residue) (D252). - Water levels show a moving seam between two water patterns (D245); thin pixel strips at the left/right screen edges at non-integer window scales (D246). -- Some front-end 3D models are off — the spinning Nintendo logo renders as two +- Some front-end 3D models are off: the spinning Nintendo logo renders as two white blobs and the Rareware logo's texture filtering looks wrong (D75). - The F10 overlay's bottom row duplicates whatever item is currently selected (D251). @@ -103,23 +98,23 @@ Windows and Linux. [`docs/dev/GRAPHICS-BACKLOG.md`](docs/dev/GRAPHICS-BACKLOG.md). - No macOS or ARM support; no controller rebinding UI. -Root causes and fix status for every item: the [release notes](../../releases) +Root causes and fix status for every item: the [release notes](/jkdansereau/goldeneye-pc-port/releases) and the finding log in [`docs/dev/findings.md`](docs/dev/findings.md). ### Steam Deck The Linux bundle is the Deck build. SFTP it over from your PC, or download -it straight from the [releases page](../../releases) on the Deck itself: -unzip, drop your ROM in `data/`, run `prepare-assets` once, and add the -executable as a non-Steam game. SDL2 is bundled, so no dependencies need +it straight from the [releases page](/jkdansereau/goldeneye-pc-port/releases) on the Deck itself: +unzip, drop your ROM in `data/`, launch it once (the first run generates the +derived assets), and add the executable as a non-Steam game. SDL2 is bundled, so no dependencies need installing. On SteamOS the first launch seeds `ge007.ini` with Deck-friendly -defaults — native 1280×800 fullscreen, VSync, MSAA 4, and 150% draw/LOD +defaults: native 1280×800 fullscreen, VSync, MSAA 4, and 150% draw/LOD distance (the authored N64 fade distances read short on the close-up panel); everything is changeable in the options overlay and persists afterwards. The renderer is CPU-bound (software RSP); expect original N64-era performance at 60 fps rather than more. This release was playtested on real -Deck hardware — the v0.1.0-era Facility crash was never reproduced there, and -its prime suspect (D253) is fixed; one intermittent SIGSEGV in heavy -firefights remains open (D255). +Deck hardware; the v0.1.0-era Facility crash (D203) did not recur: its root +cause was identified and fixed (D253), verified on the Deck; one intermittent +SIGSEGV in heavy firefights remains open (D255). **In-game settings on the Deck.** The options overlay is fully gamepad-driven: it opens with **Select**, the D-pad or left stick (up/down) moves between @@ -130,29 +125,29 @@ overlay is `F10` + arrows/Enter. ## Beyond playing -- **Tweak it** — `ge007.ini` and the F10 in-game overlay expose resolution, +- **Tweak it**: `ge007.ini` and the F10 in-game overlay expose resolution, frame cap, MSAA, texture filtering, FOV/draw distance and mouse feel; launch with `-fresh` for a clean-slate run. -- **Read it** — [`docs/internals.md`](docs/internals.md) maps the +- **Read it**: [`docs/internals.md`](docs/internals.md) maps the architecture and the software RSP; [`docs/porting-notes.md`](docs/porting-notes.md) is the catalogue of N64→PC bug classes hit along the way. Game logic in `src/` is unmodified decompilation; every hardware surface lives in the MIT-licensed `port/` layer. -- **Mod it** — the port layer, build system and `tools_pc/` are yours to +- **Mod it**: the port layer, build system and `tools_pc/` are yours to extend (see [License](#license)); [`CONTRIBUTING.md`](CONTRIBUTING.md) has the ground rules for getting changes in, and [`docs/dev/`](docs/dev/) is the raw engineering record behind every fix. ## Background -The port was built by two coding agents — a local open-weight model +The port was built by two coding agents, a local open-weight model (`unsloth/Qwen3.8-27B-GGUF` on one RTX 5090, via the [pi](https://pi.dev/) agent) doing the groundwork (build, boot chain, software-RSP integration, asset pipeline, first frames), and **Claude Code** (Sonnet 5, Opus 5 for the hardest bugs) joining for the collaborative phase (the 21-level sweep, the -ABI finding catalog, SDL input, front end) — handing work back and forth +ABI finding catalog, SDL input, front end), handing work back and forth through shared written notes, directed by one person part-time. In short: -~3.5 weeks, ~430 commits, 200+ root-caused bugs logged. +~3.5 weeks, ~430 commits, 200+ bugs logged and tracked to resolution. The full write-up (timeline, handoff mechanism, effort breakdown, an honest "what worked / what didn't"): [`docs/dev/agentic-development.md`](docs/dev/agentic-development.md). @@ -169,18 +164,18 @@ different: they machine-translate the shipped binary of the *unreleased Xbox 360 XBLA remaster*, a different game on a different codebase with no shared code with this one. -| | This project | [GoldenEye-Recomp](https://github.com/SunJaycy/GoldenEye-Recomp) / [Steam Deck build](https://github.com/couchk1ng/GoldenEye-Recomp-SteamDeck) | +| | This project | The other well-known "GoldenEye on PC" recompilation projects (and their Steam Deck builds) | |---|---|---| | **What it ports** | The original **Nintendo 64** game (1997) | The **Xbox 360 XBLA** HD remaster (built ~2007, never released) | -| **How** | Decompilation-based source port — human-reconstructed C, compiled for the host; game logic runs as written | Static binary recompilation — the shipped machine code is auto-translated to C; no source-level understanding | +| **How** | Decompilation-based source port: human-reconstructed C, compiled for the host; game logic runs as written | Static binary recompilation: the shipped machine code is auto-translated to C; no source-level understanding | | **Lineage** | [GoldenEye 007 decompilation](https://github.com/n64decomp/007) + [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark) engine family | Xbox 360 "…Recompiled" static-recompilation family | | **Renderer** | Software RSP → OpenGL | Hardware (Vulkan) | -| **Status** | v0.2.0; see [Status](#status) | Playable full game, higher frame rates, online multiplayer | -| **Why it exists** | A [case study in AI-agent collaboration](#background) on a hard low-level codebase | A polished, playable PC release of the remaster | +| **Status** | v0.2.0 public release; full campaign playable at 60 fps (see [Status](#status)) | Playable full game with online multiplayer | +| **Why it exists** | To run the *original* N64 game from source, and as a [case study in AI-agent collaboration](#background) on a hard low-level codebase | To get a playable PC release of the remaster | -If you just want to play GoldenEye on PC today, use one of the recompilation -projects: they are finished and this is not. What's here is the other half, -getting the *original* game running from source. +They answer a different question: how to get the *remaster* onto PC by +machine translation. This project answers how to get the *original 1997 +game* onto PC, running from its reconstructed source. ## Requirements @@ -193,7 +188,9 @@ big-endian (`.z64`) format, matching one of: | PAL (EU) | `pal-final` | `ge007.pal-final.z64` | `167c3c433dec1f1eb921736f7d53fac8cb45ee31` | | NTSC-J (JP) | `jpn-final` | `ge007.jpn-final.z64` | `2a5dade32f7fad6c73c659d2026994632c1b3174` | -US is the recommended and best-tested version. +This release supports the US (NTSC-U) version; PAL and JP ROMs are +recognised by region but not yet supported (on the roadmap, [issue +#85](/jkdansereau/goldeneye-pc-port/issues/85)). The port also relies on the decompilation's asset-extraction step, which pulls the level, model, texture and music data out of your ROM at build time. That @@ -219,7 +216,7 @@ git clone https://github.com/jkdansereau/goldeneye-pc-port.git cd goldeneye-pc-port # 1. extract assets from your ROM (see docs/building.md) # 2. build the port -./build-pc.sh ntsc-final # or pal-final / jpn-final +./build-pc.sh ntsc-final # NTSC-U; PAL/JP engine builds compile, but their asset sidecars are not yet producible (issue #85) ``` ### Linux @@ -314,8 +311,8 @@ Key docs are also published as a site: | [`docs/internals.md`](docs/internals.md) | Architecture, the RSP-emulation approach, GE-vs-PD engine differences, the phased plan. | | [`docs/porting-notes.md`](docs/porting-notes.md) | The recurring N64→PC bug classes hit during the port, with fixes. | | [`docs/dev/agentic-development.md`](docs/dev/agentic-development.md) | The research angle: the two-agent setup, timeline, handoff workflow, and an assessment of what did and didn't work. | -| [`docs/dev-process.md`](docs/dev-process.md) | The investigation workflow in detail — budgets, file partitioning, the finding-log discipline. | -| [`docs/dev/`](docs/dev/) | The raw engineering record: the full finding log, per-level status, graphics backlog, playtest matrices, and [`docs/dev/game-behavior-reference.md`](docs/dev/game-behavior-reference.md) (a secondary-sourced playtest reference for how the retail game is meant to behave — repo-only, code is ground truth). | +| [`docs/dev-process.md`](docs/dev-process.md) | The investigation workflow in detail: budgets, file partitioning, the finding-log discipline. | +| [`docs/dev/`](docs/dev/) | The raw engineering record: the full finding log, per-level status, graphics backlog, playtest matrices, and [`docs/dev/game-behavior-reference.md`](docs/dev/game-behavior-reference.md) (a secondary-sourced playtest reference for how the retail game is meant to behave; repo-only, code is ground truth). | | [`docs/SetupGuide.md`](docs/SetupGuide.md), [`docs/StyleGuide.md`](docs/StyleGuide.md) | Inherited from the decompilation this repo forks. | ## Credits @@ -324,30 +321,30 @@ This port is a thin layer on a large amount of other people's work. **Prior work it is built on** -- The [GoldenEye 007 decompilation](https://github.com/n64decomp/007) — years of +- The [GoldenEye 007 decompilation](https://github.com/n64decomp/007), years of effort by Larry Ficken ("kholdfuzion") and the project's contributors; plus zoinkity's GoldenEye documentation, which the decomp started from. This port is a fork of that repository. - The [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark) - (Ryan Dwyer and contributors) — the reference architecture for this port and + (Ryan Dwyer and contributors), the reference architecture for this port and the source of the `fast3d` software RSP. -- The [Perfect Dark decompilation](https://github.com/n64decomp/perfect_dark) — +- The [Perfect Dark decompilation](https://github.com/n64decomp/perfect_dark), the sibling decomp the PD port is built on. -- **Carnivorous** — author of the *Mouse Injector* input plugin for 1964 (the +- **Carnivorous**: author of the *Mouse Injector* input plugin for 1964 (the "GEPD Edition" bundle). Its mouse-aim behaviour for GoldenEye/Perfect Dark is - what our in-game GEPD-style aim mode is modelled on; the implementation here + what the in-game GEPD-style aim mode is modelled on; the implementation here is an independent reimplementation of that behaviour, not derived code. Thanks also to **Rice** and **schibo** of the 1964 team for the emulator it shipped with. **Vendored / adapted code** -- `port/fast3d/` — the software RSP, adapted from the PD port. It originates +- `port/fast3d/`: the software RSP, adapted from the PD port. It originates with the [Ship of Harkinian](https://github.com/HarbourMasters) / - libultraship fast3d (© Emill, MaikelChan; MIT — see `port/fast3d/LICENSE.txt`), + libultraship fast3d (© Emill, MaikelChan; MIT, see `port/fast3d/LICENSE.txt`), which itself descends from [sm64-port](https://github.com/sm64-port/sm64-port)'s fast3d and audio mixer. -- `port/fast3d/glad/` — OpenGL loader generated by +- `port/fast3d/glad/`: OpenGL loader generated by [glad](https://github.com/Dav1dde/glad) (David Herberth, MIT). - The decompilation toolchain: [`ido-static-recomp`](https://github.com/decompals/ido-static-recomp) (Emill / decompals), [`qemu-irix`](https://github.com/n64decomp/qemu-irix) and @@ -357,7 +354,7 @@ This port is a thin layer on a large amount of other people's work. **Methodology** - Chris Lewis, [*"Decompiling a Nintendo 64 Game in 84 Days"*](https://blog.chrislewis.au/decompiling-a-nintendo-64-game-in-84-days/) (the Snowboard - Kids decompilation write-up) — the agent-workflow practices in + Kids decompilation write-up), the agent-workflow practices in [`docs/dev-process.md`](docs/dev-process.md) are adapted from it. **Tools and models used to develop the port** @@ -367,7 +364,7 @@ This port is a thin layer on a large amount of other people's work. `UD-Q4_K_XL` quant; [Unsloth](https://unsloth.ai) (the GGUF quantisation and Unsloth Desktop, used as the local model server). -- [pi](https://pi.dev/) — the local coding-agent harness. +- [pi](https://pi.dev/): the local coding-agent harness. - [Claude / Claude Code](https://claude.com/claude-code) (Anthropic). ## Legal @@ -376,7 +373,7 @@ This is a non-commercial fan preservation/research project, in the same category as the many other N64 decompilation and native-port repositories on GitHub. It follows the same conventions they do: -- **No ROM and no game assets are distributed** — not in this repository and +- **No ROM and no game assets are distributed**: not in this repository and not in any release. Textures, audio, models, level data and in-game text are extracted from a ROM *you already own*, on *your* machine, at build time. - The repository is a fork of the public @@ -385,7 +382,7 @@ GitHub. It follows the same conventions they do: - No official logos, box art, or marketing assets are used. "GoldenEye 007", "007", "James Bond" and related marks belong to their respective owners (Nintendo, Microsoft/Rare, MGM, Danjaq, EON Productions). -- Pre-built binaries published under [Releases](../../releases) contain +- Pre-built binaries published under [Releases](/jkdansereau/goldeneye-pc-port/releases) contain only the engine (the `port/` layer plus the compiled decompilation, with no game data of any kind), bundled with permissively-licensed runtime libraries (SDL2, zlib, the MinGW runtime; their licenses travel in the download). Any diff --git a/docs/building.md b/docs/building.md index c216858a..d3f59c4e 100644 --- a/docs/building.md +++ b/docs/building.md @@ -7,13 +7,13 @@ description: Full build and asset-extraction guide for the GoldenEye 007 PC port Stages: -1. **Extract assets from your ROM** (§2) — a one-time step using the +1. **Extract assets from your ROM** (§2): a one-time step using the decompilation's own toolchain to pull levels, models, textures, fonts and music into `assets/`. *(Only needed to regenerate the committed data files; a plain `git clone` already has what the PC build compiles.)* -2. **Build the port** (§3) — a CMake build compiling the game sources plus the +2. **Build the port** (§3): a CMake build compiling the game sources plus the `port/` layer into a native executable. Needs no ROM. -3. **Generate the PC asset sidecars** (§4) — two pure-Python converters turn +3. **Generate the PC asset sidecars** (§4): two pure-Python converters turn ROM model / stage data into the PC-layout `data/pcmodels-*` / `data/pccg-*` files the port loads at runtime. **Required to run.** @@ -27,10 +27,11 @@ for accepted versions and hashes. ### Port build -> **Only the Windows (MSYS2 MINGW64) path is tested.** The Debian/Ubuntu and -> macOS columns below are best-effort guidance — the port has never been built -> or run on Linux or macOS. Expect to fix build breaks yourself on those -> platforms. +> **The Windows (MSYS2 MINGW64) path is the primary one.** The Linux build is +> compiled by CI on every push and ships in the release bundle (also +> playtested on Steam Deck hardware); the macOS column is best-effort +> guidance and has never been built or run there. Expect to fix build breaks +> yourself on untested platforms. | Need | Windows (MSYS2 MINGW64) | Debian/Ubuntu | macOS (Homebrew) | |------|------------------------|---------------|------------------| @@ -45,10 +46,10 @@ for accepted versions and hashes. The extraction scripts need `binutils-mips-linux-gnu` (or an equivalent MIPS binutils), `make`, `git`, and `python3`. They build a small host-compiled -`tools/extractor` and slice blobs straight out of the ROM — **no IDO / IRIX +`tools/extractor` and slice blobs straight out of the ROM; **no IDO / IRIX toolchain is involved in extraction or in the PC build.** (The IDO toolchain is only needed to build the N64 ROM itself, and its proprietary SGI binaries are -not distributed here — see [`SetupGuide.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/SetupGuide.md) "Recompile IDO".) +not distributed here; see [`SetupGuide.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/SetupGuide.md) "Recompile IDO".) On Windows this is easiest under WSL or a Linux VM. Full details and alternatives (Docker) are in [`SetupGuide.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/SetupGuide.md). @@ -108,7 +109,7 @@ builds are named `ge007.pal-final.x86_64` / `ge007.jpn-final.x86_64`. ## 4. Generate the PC asset sidecars (required to run) The port does **not** read model geometry, stage bg/stan data, or per-level -setup data from the raw ROM at runtime — it reads them from PC-layout *sidecar* +setup data from the raw ROM at runtime; it reads them from PC-layout *sidecar* files under `data/`, produced offline by three converters. **Without them the game shows the intro logos and then crashes** in `modelPromoteNodeOffsetsToPointers` (finding D179) or on the first level load @@ -130,6 +131,9 @@ python3 tools_pc/d69_emit.py ntsc-final # -> data/pccg-ntsc-final/{pccg python3 tools_pc/d88_emit.py ntsc-final --regen # appends the 21 per-level Usetup*Z stage-setup files -> ~3.6 MB ``` +**PAL / JP note:** sidecar generation for these regions is currently broken at +the source-data level (finding D258); use an NTSC-U ROM until issue #85 lands. + These are **pure-stdlib Python 3** (no MIPS toolchain, independent of the step-2 asset extraction) and read only the ROM plus files already committed to the repo (`scripts/filelist.u.csv`, `assets/obseg/file_resource_table.inc.c`, @@ -138,10 +142,12 @@ deterministic function of the ROM. Re-run after any change to `d43_emit.py` / `d69_emit.py` / `d88_emit.py` or the model/bg converters (`d43_*`, `d69_*`, `d88_propdefs.py`). -> The release bundle ships `prepare-assets/prepare-assets.py`, which runs all -> three passes against your ROM automatically — see the bundled `README.md`. +> The release bundle ships the same converter frozen as +> `prepare-assets/ge007-convert`; the game spawns it on first launch when the +> sidecars are missing, so end users never run it by hand. See the bundled +> `README.md`. -> `data/pcmodels-*/` and `data/pccg-*/` are gitignored ROM-derived game data — +> `data/pcmodels-*/` and `data/pccg-*/` are gitignored ROM-derived game data; > never commit or redistribute them. --- diff --git a/docs/dev-process.md b/docs/dev-process.md index b668d150..e0856387 100644 --- a/docs/dev-process.md +++ b/docs/dev-process.md @@ -1,37 +1,37 @@ --- title: Development Process -description: The investigation workflow behind the port — task budgets, file partitioning, and the finding-log discipline used across sessions and agents. +description: The investigation workflow behind the port; task budgets, file partitioning, and the finding-log discipline used across sessions and agents. --- # How this port is developed -Most of this port's work is not writing code — it is **diagnosis**: figuring +Most of this port's work is not writing code; it is **diagnosis**: figuring out which N64 hardware or ABI behavior a piece of unmodified game code is silently relying on, and satisfying it in the `port/` layer without touching the game's logic. A lot of that diagnosis is done with LLM coding agents, and the workflow below is what makes that productive rather than chaotic. -For the project context — the two-model split, the timeline, and an honest -assessment of what did and did not work — see +For the project context (the two-model split, the timeline, and an honest +assessment of what did and did not work), see [`dev/agentic-development.md`](dev/agentic-development.md). It is adapted from Chris Lewis's write-up [*"Decompiling a Nintendo 64 Game in 84 Days"*](https://blog.chrislewis.au/decompiling-a-nintendo-64-game-in-84-days/) (Snowboard Kids, 2026), an AI-assisted decompilation that ran several times faster than its predecessor. That project bit-matches compiler output; -this one matches runtime behavior, so the mechanics differ — but the +this one matches runtime behavior, so the mechanics differ; but the project-structure lessons carry over directly. ## 1. Every investigation task gets a visible budget -An open-ended tool — for them the permuter, for us "rebuild and run the game -and stare at a frame dump" — wrecks throughput when an agent will grind it +An open-ended tool; for them the permuter, for us "rebuild and run the game +and stare at a frame dump"; wrecks throughput when an agent will grind it indefinitely. Giving the agent a **deadline it can see** lets it trade tool-time against thinking and decide when to give up. So every investigation brief states: -- a **budget** — "≤ N build → run → inspect cycles" or "~M minutes"; and +- a **budget**; "≤ N build → run → inspect cycles" or "~M minutes"; and - the **fallback on expiry**: stop, revert any temporary probes, and write up what was found with an explicit confidence rating. A good write-up of a half-solved bug is a deliverable, not a failure. @@ -45,7 +45,7 @@ more room. Lewis's agents recorded generalisable IDO quirks in a learnings file; later agents read it and did better. Our equivalent is -[`porting-notes.md`](porting-notes.md) — the recurring N64→PC bug classes, +[`porting-notes.md`](porting-notes.md); the recurring N64→PC bug classes, each entry a terse index into the full [`dev/findings.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/findings.md) log. Every investigation brief links it, and every investigation ends by @@ -54,8 +54,8 @@ appending any new generalisable quirk. The classes it currently tracks: - pointer-width struct growth (32→64) in ROM-serialized or pun-allocated - structs — the dominant class; -- 16-byte host `Gfx`/`Vtx` vs 8-byte N64 — reservation and copy sizing; + structs; the dominant class; +- 16-byte host `Gfx`/`Vtx` vs 8-byte N64; reservation and copy sizing; - big-endian rodata read on a little-endian host: `f32` word-pairs, header offset tables, packed bitfields; - N64 hardware idioms the software RSP does not emulate (fill-rect Z clear, @@ -71,7 +71,7 @@ Lewis ran multiple git worktrees and found the real cost was - **Partition tracks by the files they will touch**, so patches never collide. A task that can't be cleanly file-partitioned shouldn't be parallelised. - Each agent reverts its own temporary probes before reporting, leaving only - committed, env-gated, capped diagnostics — and never touches another track's + committed, env-gated, capped diagnostics; and never touches another track's uncommitted edits. - One integrator merges each track with its write-up, then the next round starts from a clean tree. Investigation branches are not allowed to drift. @@ -85,7 +85,7 @@ Before dispatching an investigation agent: - capture a baseline frame dump; - grep the finding log for a prior instance of the same bug class; - check the [Perfect Dark port](https://github.com/fgsfdsfgs/perfect_dark) for - the analogous code — same engine family. + the analogous code; same engine family. Then hand the agent the *findings*, not just the problem. @@ -94,12 +94,12 @@ Then hand the agent the *findings*, not just the problem. - **The match-percentage loop.** This port doesn't bit-match. "Done" is: no fault, and visually correct against N64 reference footage. - **"Understanding the function is the easy part."** True when decompiling; - inverted here — the hard part is identifying the implicit hardware/ABI + inverted here; the hard part is identifying the implicit hardware/ABI contract the already-readable C depends on. ## 6. Keep PR/git-history noise low -A session produces a lot of small, independently-verifiable units of work — +A session produces a lot of small, independently-verifiable units of work; one Dxx row, one docs pass, one mechanical fix. Left unmanaged that turns into a pile of small open PRs that's harder to review than the work justifies. Rules: @@ -107,31 +107,31 @@ justifies. Rules: - **Squash-merge single-purpose and docs-only PRs.** The repo has squash merge enabled; use it (`gh pr merge --squash --delete-branch`) for anything whose in-branch commit history (iteration, probe-add, probe-strip) isn't - itself useful to keep — which is most PRs. Reserve a plain merge commit for + itself useful to keep; which is most PRs. Reserve a plain merge commit for a PR whose individual commits are each a distinct, reviewable unit someone might want to `git revert` independently (e.g. this session's PR #50: a fix commit + a separate analysis-only commit). - **Batch same-session, same-flavor docs updates into one PR/branch** instead of opening a new PR per finding row. A runtime re-check that updates one `findings.md` row (like #48) doesn't need its own PR if - another docs-only branch is already open in the same session — fold it in. + another docs-only branch is already open in the same session; fold it in. Only split into separate PRs when the pieces have genuinely different review/merge timing (e.g. one needs a human playtest, the other doesn't). - **A branch takes only the change it's named for.** Don't let unrelated housekeeping (a stray gitignore fix, a config tweak) ride along on an - active feature/fix branch just because it's the one checked out — cut a + active feature/fix branch just because it's the one checked out; cut a separate branch, even for a one-line change. - **Stacked PRs declare the dependency in the title/body** ("step 2 of N, - step 1 is #N") and get rebased/merged in order promptly — don't let a + step 1 is #N") and get rebased/merged in order promptly; don't let a later step sit open so long the earlier step's content drifts under it. - **Triage the open-PR list at least once a session**, ordered by time-to-close (docs-only and non-draft first, human-playtest-gated last). A PR idle long enough that `main` has moved past the finding rows it - touches is **stale, not just old** — check for real textual conflict + touches is **stale, not just old**; check for real textual conflict (`git merge-tree`), not just calendar age. If `main` already has more current information than the PR (a bug the PR still lists `OPEN` that's since been fixed and verified), **close it rather than force a merge or - spend a cycle rebasing it** — re-derive only whatever part of its content + spend a cycle rebasing it**; re-derive only whatever part of its content is still true as a fresh, small PR against current `main`. Merging stale content back over newer information is a regression, not a save. @@ -141,11 +141,11 @@ justifies. Rules: TASK: . READ FIRST: docs/porting-notes.md; docs/dev/findings.md . FILES YOU MAY TOUCH: . -KNOWN-GOOD / RULED OUT: . +KNOWN-GOOD / RULED OUT: . PRE-FLIGHT ATTACHED: . BUDGET: run cycles / ~M min>. On expiry: revert probes, write up with confidence. CONSTRAINTS: no game-logic changes; ABI/layout/format only; #ifdef PORT; documented in the finding log. -VERIFY: . +VERIFY: . REPORT: (a) root cause + file:line evidence (b) fix diff, or why not (c) probes left in tree (d) confidence. Append any generalisable quirk to docs/porting-notes.md. diff --git a/docs/dev/agentic-development.md b/docs/dev/agentic-development.md index ef865858..f6e808bb 100644 --- a/docs/dev/agentic-development.md +++ b/docs/dev/agentic-development.md @@ -1,6 +1,6 @@ --- title: Agentic Development -description: A case study in two AI coding agents porting a Nintendo 64 game to PC — the setup, timeline, handoff workflow, and an honest assessment of what worked. +description: A case study in two AI coding agents porting a Nintendo 64 game to PC; the setup, timeline, handoff workflow, and an honest assessment of what worked. --- # Agentic development: two AI coding agents porting GoldenEye 007 @@ -16,7 +16,7 @@ what did and didn't work.* - [Why this project exists](#why-this-project-exists) - [The setup](#the-setup) - [Timeline](#timeline) -- [By the numbers](#by-the-numbers) — [Commit velocity](#commit-velocity) · [Who did what](#who-did-what) +- [By the numbers](#by-the-numbers); [Commit velocity](#commit-velocity) · [Who did what](#who-did-what) - [The handoff workflow](#the-handoff-workflow) - [Assessment](#assessment) @@ -24,7 +24,7 @@ what did and didn't work.* The playable port is real, but it is not the primary deliverable. The goal was to **test how well coding agents hold up on a large, unfamiliar, low-level -codebase** — one with none of the properties that make web-app work easy for +codebase**; one with none of the properties that make web-app work easy for an LLM: - ~230 translation units of decompiled Nintendo 64 game C, compiled @@ -39,7 +39,7 @@ an LLM: Concretely it set out to validate a **two-agent arrangement**: a locally-hosted open-weight model doing the bulk of the work on a single consumer GPU, a hosted frontier model brought in for a collaborative phase, -and — the part that turned out most interesting — the two **handing work back +and, the part that turned out most interesting, the two **handing work back and forth** through shared written artifacts, directed by one human. ## The setup @@ -47,17 +47,17 @@ and forth** through shared written artifacts, directed by one human. | | | |---|---| | Local agent | `unsloth/Qwen3.8-27B-GGUF:UD-Q4_K_XL` on a single **NVIDIA RTX 5090**, driven mainly through the **[pi](https://pi.dev/)** coding agent. Unsloth Desktop (Unsloth's local model runtime, which can drive agents such as Claude Code) was also trialed but not used significantly. | -| Hosted agent | **Claude**, via **Claude Code**, on a Claude Pro subscription — mostly **Sonnet 5**, with **Opus 5** used as an escalation tier for the hardest problems and whenever there was subscription budget to spend on it | +| Hosted agent | **Claude**, via **Claude Code**, on a Claude Pro subscription; mostly **Sonnet 5**, with **Opus 5** used as an escalation tier for the hardest problems and whenever there was subscription budget to spend on it | | Human | one person: direction, work partitioning, integration, and every build / playtest / frame-capture the agents could not run | | Base | fork of the [GoldenEye 007 decompilation](https://github.com/n64decomp/007) (years of prior work by Larry Ficken ("kholdfuzion") and contributors) | -| Reused | the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark)'s `fast3d` software RSP — same Rare engine family | +| Reused | the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark)'s `fast3d` software RSP; same Rare engine family | The port is the GoldenEye-specific porting work **on top of** those two existing bodies of work; it is not a from-scratch reimplementation of either. In practice the models formed a **three-tier escalation**: the local model handled well-scoped work, Sonnet 5 took what it stalled on, and Opus 5 was -reserved for the bugs that needed the most reasoning held at once — the same +reserved for the bugs that needed the most reasoning held at once; the same "escalate when stuck" move applied at every level. ## Timeline @@ -68,7 +68,7 @@ All dates from this repository's own commit history (August 2026). gantt dateFormat YYYY-MM-DD axisFormat %b %d - title GoldenEye 007 PC port — from fork to playable front end + title GoldenEye 007 PC port; from fork to playable front end section Local model - Qwen 3.8 via pi Fork + PC-port scaffolding :m1, 2026-08-16, 1d Full compile + link (~230 TUs) :m2, 2026-08-20, 1d @@ -90,7 +90,7 @@ gantt - **Day 4** (20 Aug): the entire ~230-TU game + libultra set compiles and links as a host binary. - **Day 6** (22 Aug): first real frames render. -- **Day 8** (24 Aug): the whole intro sequence renders — logos, gun-barrel, +- **Day 8** (24 Aug): the whole intro sequence renders; logos, gun-barrel, cast roll. - **Day 11** (27 Aug): second agent joins. - **Day 13** (29 Aug): **all 21 solo missions load, render, and survive an @@ -100,7 +100,7 @@ gantt So roughly **two weeks**, one person part-time, to take a decompilation from "builds an N64 ROM" to "boots on desktop, renders every solo level, playable -through the front end into the early game" — with audio and a set of cosmetic +through the front end into the early game"; with audio and a set of cosmetic issues still outstanding. ## By the numbers @@ -139,7 +139,7 @@ pie showData title "Commits by agent (raw count)" "Local model (Qwen 3.8 / pi)" : 62 ``` -(Phase A — the first 26 commits, to 24 Aug — was entirely the local model, +(Phase A, the first 26 commits, to 24 Aug, was entirely the local model, solo; from 27 Aug on the two agents worked in parallel.) | Milestone / workstream | Primary agent | Weight | @@ -150,20 +150,20 @@ solo; from 27 Aug on the two agents worked in parallel.) | Offline asset-conversion architecture + first converters (`tools_pc/`) | local model | large | | Intro rendering (logos → gun-barrel → cast) | local model | medium | | Stage load unblocked (`D69`–`D87`) | Claude | medium | -| 21-level crash sweep — ~12 crash classes root-caused (`D88`–`D169`) | Claude | large | +| 21-level crash sweep; ~12 crash classes root-caused (`D88`–`D169`) | Claude | large | | SDL input layer (keyboard / mouse / gamepad, mouse-look) | Claude | medium | | Front-end flow (menu → briefing → start), EEPROM saves | Claude | medium | | Parallel struct-layout / converter static audits | local model | medium | | Continuing tasks after Claude hit a usage limit | local model | small | -| The hardest structural bugs (matrix handedness, format specs) | Claude | — | -| Every build, playtest, frame capture, integration, and direction | human | — | +| The hardest structural bugs (matrix handedness, format specs) | Claude | n/a | +| Every build, playtest, frame capture, integration, and direction | human | n/a | **Estimated effort split.** The raw commit count above (~28% local / ~72% Claude) undercounts the local model: Phase A landed the build system and boot chain in relatively few, large commits, and the local model kept contributing ~15–20% of Phase B in parallel. Weighting by milestone difficulty rather than -raw commits — the foundation is a heavier third of the project than its commit -share suggests — the developer's estimate is roughly: +raw commits; the foundation is a heavier third of the project than its commit +share suggests; the developer's estimate is roughly: ```mermaid pie showData title "Agent effort, milestone-weighted" @@ -202,28 +202,28 @@ flowchart LR Both agents worked against the **same three written artifacts**, which is what let them substitute for each other: -1. **`HANDOFF.md`** — the current state, the immediate next task, and the +1. **`HANDOFF.md`**; the current state, the immediate next task, and the environment gotchas. Originally a session-to-session note for one agent, it became the **interface between the two agents**: when Claude reached a usage limit mid-problem, the local model picked the task up from the HANDOFF state and continued; when the local model hit a bug that needed deeper structural reasoning, it wrote up where it was and Claude took over. -2. **`findings.md`** — the chronological finding log. 162 numbered entries, +2. **`findings.md`**; the chronological finding log. 162 numbered entries, each a root cause with `file:line` evidence and the fix. New agents (either model) are pointed at the relevant entries before they start. -3. **`porting-notes.md`** — the append-only "recurring bug classes" file. The +3. **`porting-notes.md`**; the append-only "recurring bug classes" file. The single highest-leverage artifact: it stopped both models from re-deriving the same class of N64→PC bug over and over. Around these, the working rules (full detail in [`../dev-process.md`](../dev-process.md)): -- **file-partitioned tasks** — each agent's task scoped to a disjoint set of +- **file-partitioned tasks**; each agent's task scoped to a disjoint set of files so patches never collided; -- **visible budgets** — every investigation task carried an explicit +- **visible budgets**; every investigation task carried an explicit "N build→run cycles" limit and a defined fallback (revert probes, write up with a confidence rating); -- **the human owns verification** — building, running the game, capturing a +- **the human owns verification**; building, running the game, capturing a frame, and judging it against N64 reference footage was never delegated. ## Assessment @@ -232,7 +232,7 @@ Honest notes, for anyone weighing whether this transfers. **Worked well** -- The **local model carried the groundwork phase** — build system, boot +- The **local model carried the groundwork phase**; build system, boot chain, OS shims, and the offline-converter architecture. None of it was rewritten later. A 27B open model on one consumer GPU was genuinely productive on this. @@ -241,14 +241,14 @@ Honest notes, for anyone weighing whether this transfers. because the accumulated notes were good. This is also what made the two models interchangeable on a given task. - **Handoff on limit** turned Claude's usage cap from a hard stop into a - slowdown — the local model kept the problem moving. + slowdown; the local model kept the problem moving. - Bounded behavioural bugs (a truncated pointer, a byte-swap off by one) are a good fit for an agent given a tight loop and a reference to diff against. **Worked poorly / needed the human** - **Anything requiring the running game.** Build, playtest, capture a frame, - decide whether it looks right — that loop was the human's job throughout, + decide whether it looks right; that loop was the human's job throughout, and it was the bottleneck. - **Non-deterministic bugs.** Frame-timing stalls and concurrent-build flakiness repeatedly fooled agents into "fixing" regressions that were not @@ -258,7 +258,7 @@ Honest notes, for anyone weighing whether this transfers. rather than a fix, even with the budget raised. - **The capability gap is a gradient, not a wall.** The local model was strong on well-scoped work and weaker when a bug needed several interacting facts - held at once — those went to Sonnet 5, and the few that stalled Sonnet went + held at once; those went to Sonnet 5, and the few that stalled Sonnet went to Opus 5. Each tier earned its place on the problems the tier below it couldn't close. diff --git a/docs/index.md b/docs/index.md index a38067ac..04c77c05 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,7 +3,7 @@ title: GoldenEye 007 PC Port description: >- A native PC port of the original Nintendo 64 GoldenEye 007, built from its decompiled source with a software RSP. v0.2.0 is out for Windows - and Linux (including Steam Deck) — download it, drop in your own ROM, and + and Linux (including Steam Deck): download it, drop in your own ROM, and play; or dig into the code and the engineering record behind it. --- @@ -11,16 +11,16 @@ description: >- A native PC port of the original 1997 Nintendo 64 _GoldenEye 007_, compiled from the game's [decompiled source](https://github.com/n64decomp/007) with the -N64's graphics coprocessor (RSP) running in software — the same architecture +N64's graphics coprocessor (RSP) running in software, the same architecture as the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark), the same Rare "Indy" engine family, one hardware generation apart. -**Status: v0.2.0** — to our knowledge, the first open-source port of the -original N64 game to ship as a public release. The full single-player -campaign runs at a steady 60 fps with no known crashes, and audio (music + -SFX) plays throughout. -Known rough edges — cutscenes still glitch, a few music tracks sound wrong, a -handful of cosmetic rendering defects remain — are listed plainly under +**Status: v0.2.0.** The full single-player campaign runs at a steady 60 fps +and is completable end to end; all 21 missions load and run clean on Windows +and Linux (one intermittent Steam Deck crash remains, listed below), and +audio (music + SFX) plays throughout. The known +rough edges (cutscenes that still glitch, a few music tracks that sound +wrong, a handful of cosmetic rendering defects) are listed plainly under [Honest status](#honest-status).

@@ -34,13 +34,15 @@ handful of cosmetic rendering defects remain — are listed plainly under | Platform | Bundle | Notes | |---|---|---| | **Windows** (x86_64) | [win64.zip](https://github.com/jkdansereau/goldeneye-pc-port/releases) | Engine + runtime DLLs + the one-time asset tool. | -| **Linux** (x86_64) / **Steam Deck** | [linux tarball](https://github.com/jkdansereau/goldeneye-pc-port/releases) | SDL2 is bundled — it runs as-is on any distro, and sideloads onto a Deck with nothing installed. | +| **Linux** (x86_64) / **Steam Deck** | [linux tarball](https://github.com/jkdansereau/goldeneye-pc-port/releases) | SDL2 is bundled, so it runs as-is on any distro, and sideloads onto a Deck with nothing installed. | -**Bring your own ROM.** Both bundles contain no ROM and no game assets — you +**Bring your own ROM.** Both bundles contain no ROM and no game assets: you supply your own GoldenEye 007 N64 ROM (the [Requirements table](https://github.com/jkdansereau/goldeneye-pc-port#requirements) -has the three region filenames and SHA-1s). Then: unpack, drop the ROM in -`data/`, run `prepare-assets` once, launch. The full four steps are in the +has the region filenames and SHA-1s). This release supports the NTSC-U (US) +ROM; PAL and JP are on the roadmap. Then: unpack, drop the ROM in `data/`, +and launch; the first run generates the derived assets automatically (no +Python or other tooling needed). The full steps are in the [Quick start](https://github.com/jkdansereau/goldeneye-pc-port#quick-start); pre-built releases are legal to distribute precisely because they're useless without a ROM you already own. @@ -53,83 +55,83 @@ without a ROM you already own.

-Full playtest gallery — 19 in-engine captures from the v0.2.0-pre Windows playtest, across the campaign +Full playtest gallery: 19 in-engine captures from the v0.2.0 Windows playtest, across the campaign

- In-engine — Dam (v0.2.0-pre) + In-engine: Dam (v0.2.0)
Dam
- In-engine — Facility (v0.2.0-pre) + In-engine: Facility (v0.2.0)
Facility
- In-engine — Runway (v0.2.0-pre) + In-engine: Runway (v0.2.0)
Runway
- In-engine — Surface (v0.2.0-pre) + In-engine: Surface (v0.2.0)
Surface
- In-engine — Surface (v0.2.0-pre) + In-engine: Surface (v0.2.0)
Surface
- In-engine — Bunker 1 (v0.2.0-pre) + In-engine: Bunker 1 (v0.2.0)
Bunker 1
- In-engine — Silo (v0.2.0-pre) + In-engine: Silo (v0.2.0)
Silo
- In-engine — Frigate (v0.2.0-pre) + In-engine: Frigate (v0.2.0)
Frigate
- In-engine — Frigate (v0.2.0-pre) + In-engine: Frigate (v0.2.0)
Frigate
- In-engine — Surface 2 (v0.2.0-pre) + In-engine: Surface 2 (v0.2.0)
Surface 2
- In-engine — Bunker 2 (v0.2.0-pre) + In-engine: Bunker 2 (v0.2.0)
Bunker 2
- In-engine — Statue (v0.2.0-pre) + In-engine: Statue (v0.2.0)
Statue
- In-engine — Statue (v0.2.0-pre) + In-engine: Statue (v0.2.0)
Statue
- In-engine — Archives (v0.2.0-pre) + In-engine: Archives (v0.2.0)
Archives
- In-engine — Cradle (v0.2.0-pre) + In-engine: Cradle (v0.2.0)
Cradle
- In-engine — Cradle (v0.2.0-pre) + In-engine: Cradle (v0.2.0)
Cradle
- In-engine — Aztec (v0.2.0-pre) + In-engine: Aztec (v0.2.0)
Aztec
- In-engine — Aztec (v0.2.0-pre) + In-engine: Aztec (v0.2.0)
Aztec
- In-engine — Frigate (v0.2.0-pre) + In-engine: Frigate (v0.2.0)
Frigate

@@ -137,23 +139,23 @@ without a ROM you already own. More captures may land here as playtesting continues. -## Play it — or take it apart +## Play it, or take it apart -- **Try it yourself** — grab a bundle above, bring your own ROM, and play the +- **Try it yourself**: grab a bundle above, bring your own ROM, and play the campaign. It's an early cut for exactly this: if something breaks, an [issue](https://github.com/jkdansereau/goldeneye-pc-port/issues) with what you were doing is genuinely useful. -- **Read the code** — the game logic in `src/` is unmodified decompilation; +- **Read the code**: the game logic in `src/` is unmodified decompilation; every hardware surface (video, audio, input, save storage, and the software-RSP renderer) is shimmed in the MIT-licensed `port/` layer. [Internals](internals.md) is the map; [Porting notes](porting-notes.md) is - the catalogue of N64→PC bug classes hit along the way — a good read even if + the catalogue of N64→PC bug classes hit along the way, a good read even if you never touch the code. -- **Mod it** — the port layer, build system and `tools_pc/` helpers are MIT +- **Mod it**: the port layer, build system and `tools_pc/` helpers are MIT licensed and yours to extend: new video options, input tweaks, your own asset sidecar. [CONTRIBUTING](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/CONTRIBUTING.md) has the ground rules that keep it faithful to the original game. -- **The research angle** — this is also a case study in AI-agent collaboration +- **The research angle**: this is also a case study in AI-agent collaboration on a large, low-level codebase: two coding agents (a local open-weight model on a single RTX 5090, and Claude / Claude Code), driven by one person part-time through ~230 translation units of unmodified big-endian MIPS game @@ -162,20 +164,21 @@ More captures may land here as playtesting continues. ## How it works -The R4300 game code is compiled completely unmodified — the decompilation's +The R4300 game code is compiled completely unmodified; the decompilation's control flow is ground truth. The N64's Reality Signal Processor, the graphics coprocessor that builds and executes each frame's display list, is emulated in software: the port interprets the GBI stream the game emits and translates it to OpenGL, bypassing the RDP entirely. Everything else that would touch N64 -hardware — video, audio, input, timers, save storage — is shimmed in a small +hardware (video, audio, input, timers, save storage) is shimmed in a small dedicated layer, following the architecture of the Perfect Dark PC port from the same Rare engine family. Full detail: [Internals](internals.md); the bug catalogue: [Porting notes](porting-notes.md). ## Honest status -- **Cutscenes glitch frequently** — skipped beats, wrong camera, misplaced or - hovering actors. The most visible gap in this release. +- **Cutscenes still glitch, mostly with James Bond**: in scripted sequences + Bond is the one who gets misplaced, hovers, or spins; the other actors are + fine for the most part now. Still the most visible gap in this release. - A few in-level music tracks sound wrong (wrong instruments, occasional garbling). - Particle colours drift through a rainbow palette instead of holding grey/orange. - Water levels show a moving seam; pixel strips at screen edges at non-integer @@ -190,11 +193,11 @@ and the [finding log](https://github.com/jkdansereau/goldeneye-pc-port/tree/main ## Documentation -- [The two-agent development case study](dev/agentic-development.md) — goal, setup, timeline, the handoff workflow, and an honest assessment of what did and didn't work. -- [Development process](dev-process.md) — how work was scoped, partitioned, and budgeted across agents; the finding-log discipline. -- [Internals](internals.md) — architecture, the software RSP-emulation approach, GoldenEye-vs-Perfect-Dark engine differences, the phased plan. -- [Porting notes](porting-notes.md) — the recurring Nintendo 64 → PC bug classes hit during the port, with fixes. -- [Building](building.md) — full build and asset-extraction guide. +- [The two-agent development case study](dev/agentic-development.md): goal, setup, timeline, the handoff workflow, and an honest assessment of what did and didn't work. +- [Development process](dev-process.md): how work was scoped, partitioned, and budgeted across agents; the finding-log discipline. +- [Internals](internals.md): architecture, the software RSP-emulation approach, GoldenEye-vs-Perfect-Dark engine differences, the phased plan. +- [Porting notes](porting-notes.md): the recurring Nintendo 64 → PC bug classes hit during the port, with fixes. +- [Building](building.md): full build and asset-extraction guide. --- diff --git a/docs/internals.md b/docs/internals.md index abf5cc35..8e7a4e9d 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -1,14 +1,14 @@ --- title: Internals -description: Architecture behind the PC port — the software RSP-emulation approach, GoldenEye-vs-Perfect-Dark engine differences, and the phased development plan. +description: Architecture behind the PC port; the software RSP-emulation approach, GoldenEye-vs-Perfect-Dark engine differences, and the phased development plan. --- -# GoldenEye 007 — PC Port: architecture & plan +# GoldenEye 007 PC Port: architecture & plan > This began as the pre-implementation research note and is kept as the > architecture reference. Sections 1–9 describe the design; section 10 lists > external references. The phased plan in section 8 is largely done through -> Phase 2 — for current status see the +> Phase 2; for current status see the > [README](https://github.com/jkdansereau/goldeneye-pc-port#status) and > [`dev/LEVEL-STATUS.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/LEVEL-STATUS.md); > for the blow-by-blow finding log see @@ -28,18 +28,18 @@ platforms. 1. [Executive summary](#1-executive-summary) 2. [What the PD port actually does (the reference architecture)](#2-what-the-pd-port-actually-does-the-reference-architecture) - — [2.1 Emulate the RSP, bypass the RDP](#21-the-key-architectural-insight-emulate-the-rsp-bypass-the-rdp) + · [2.1 Emulate the RSP, bypass the RDP](#21-the-key-architectural-insight-emulate-the-rsp-bypass-the-rdp) · [2.2 How the RSP is invoked](#22-how-the-rsp-is-invoked) · [2.3 Build system](#23-build-system) · [2.4 PD-port copy-candidate audit](#24-pd-port-copy-candidate-audit-session-2026-08-22) 3. [GoldenEye decomp: what we're porting](#3-goldeneye-decomp-what-were-porting) - — [3.1 Entry / main loop](#31-entry--main-loop) + · [3.1 Entry / main loop](#31-entry--main-loop) · [3.2 The two libultra trees](#32-the-two-libultra-trees-srclibultra-and-srclibultrare) 4. [N64 hardware surfaces that must be shimmed](#4-n64-hardware-surfaces-that-must-be-shimmed) 5. [GE-specific graphics differences vs PD (the RSP work)](#5-ge-specific-graphics-differences-vs-pd-the-rsp-work) 6. [Audio](#6-audio) 7. [Input & saves](#7-input--saves) -8. [Recommended plan](#8-recommended-plan) — phases 0–4 +8. [Recommended plan](#8-recommended-plan); phases 0–4 9. [Open questions / risks](#9-open-questions--risks) 10. [References](#10-references) @@ -70,7 +70,7 @@ platforms. * **Saves** are **EEPROM-based** (controller EEPROM via `osEeprom*`), not Memory Pak. The PFS/motor code in `joy.c` is 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 + file-backed EEPROM plus no-op PFS/motor shims; simpler than a full Memory Pak emulation. --- @@ -127,7 +127,7 @@ R4300 builds a Gfx display list 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 +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`): @@ -162,7 +162,7 @@ The PC build is **CMake** (the N64 build stays Make + IDO). Dependencies: * `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/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` @@ -176,38 +176,38 @@ for this project (see CONTRIBUTING.md): same Rare "Indy"/Bond engine family, and port layer solves the same problem classes we are currently hitting. Audited against our remaining work items: -**Direct copy candidates (Phase 3/4 — our stubs were scaffolded to be replaced +**Direct copy candidates (Phase 3/4; our stubs were scaffolded to be replaced by these):** | PD file | Lines | Replaces our | Notes | |---|---|---|---| | `port/src/mixer.c` | 722 | 31-line stub | libaudio→SDL final mixer; same RareAL family. Adapt to GE's `audi.c` wiring (sample rate/buffering) | | `port/src/input.c` | 1551 | 48-line stub | SDL_GameController backend: hotplug, rumble, keyboard fallback. Remap the button table to GE's scheme (C-buttons/Z-trig differ from PD) | -| `port/src/fs.c` | 294 | 81 lines | File-backed save dir (`--savedir`, `$S` path expansion) — foundation for Phase 4 EEPROM/PFS file backing (ours are no-op stubs today) | +| `port/src/fs.c` | 294 | 81 lines | File-backed save dir (`--savedir`, `$S` path expansion); foundation for Phase 4 EEPROM/PFS file backing (ours are no-op stubs today) | | `port/src/audio.c` | 75 | 70 lines | Near-identical; diff when Phase 3 starts | -**Reference implementations (Phase 2 — the D32/D37/D43 class).** PD's port +**Reference implementations (Phase 2; the D32/D37/D43 class).** PD's port layer has a `port/src/preprocess/` module (~4,125 lines): a per-ROM-segment `preprocessfunc` table in their `romdata.c` that converts N64-layout asset data -to PC-native layout *at load time* — a systematic solution to the problem class +to PC-native layout *at load time*; a systematic solution to the problem class we have been fixing ad-hoc per asset type. -- `filemodel.c` (1,114) — the direct **D43** analogue: same `PROMOTE` idiom, +- `filemodel.c` (1,114); the direct **D43** analogue: same `PROMOTE` idiom, **same vma 0x5000000**. NOT a drop-in copy (GE's `ModelRoData` union + opcode - set differ — PD has stargunfire/headspot/gundl node types), but the node-walk + set differ; PD has stargunfire/headspot/gundl node types), but the node-walk order, placement/alignment choices, and record handling are Rare-validated against near-identical data: adapt rather than design from scratch. -- `filebg.c` (524) — BG tables (an expected D43 follow-on fault). -- `segaudio.c` (362) — audio bank-tree conversion; cross-check for our D37 +- `filebg.c` (524); BG tables (an expected D43 follow-on fault). +- `segaudio.c` (362); audio bank-tree conversion; cross-check for our D37 `romdataFixupAudioBank`. - `filelang.c`, `filetiles.c`, `filepads.c`, `filesetup.c`, `gbi.c`, - `segfonts.c` — the remaining asset types we will hit. + `segfonts.c`; the remaining asset types we will hit. **Not copyable:** GE-specific `ModelRoData` record map; fast3d CC/RM correctness (GE's custom GBI modes vs `gmain.s` do not exist in PD); PD extras -(`config.c`, `optionsmenu.c`, `mpsetups.c`, `mod.c`) — options menu, MP setup, +(`config.c`, `optionsmenu.c`, `mpsetups.c`, `mod.c`); options menu, MP setup, MOD player; not part of 1:1 fidelity. -**Caveats:** copy **port-layer files only** — their decomp was modified for PC +**Caveats:** copy **port-layer files only**; their decomp was modified for PC (e.g. `types.h` keeps N64 offset comments while using real pointers); our non-negotiables are unchanged. Same family ≠ identical format: validate every copied conversion per-field against GE headers + ROM ground truth (the standing @@ -221,19 +221,19 @@ D32 procedure). US/EU/JP ROMs. Game code lives in `src/game/` (~242 files) plus engine files in `src/`. * **Engine layout** (same family as PD): - * `src/sched.c` — RCP scheduler (`osCreateScheduler`, `__scExec`, + * `src/sched.c`; RCP scheduler (`osCreateScheduler`, `__scExec`, `__scYield`). Structurally near-identical to PD's `src/lib/sched.c`. - * `src/game/rsp.c` — builds the Gfx display list, wraps it in an `OSScTask`, + * `src/game/rsp.c`; builds the Gfx display list, wraps it in an `OSScTask`, 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 (via `joyGamePak*` + * `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 (via `joyGamePak*` -> `osEeprom*`), plus PFS/motor code for accessory (Rumble Pak / Memory Pak) detection only. - * `src/init.c` — `init()` -> `mainproc()` -> `bossEntry()` (game entry). + * `src/init.c`; `init()` -> `mainproc()` -> `bossEntry()` (game entry). **Compiled for the PC** (provides `mainproc()`); its N64-only `init()` is stubbed (see A3). -* **Custom RSP microcode:** `rsp/graphics/gmain.s` (1545 lines) — a modified +* **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. @@ -265,23 +265,23 @@ overlap. What the PC build must do with each: -* **`src/libultra/audio/*.c`** — standard libaudio (AL). Runs on the CPU; +* **`src/libultra/audio/*.c`**; standard libaudio (AL). Runs on the CPU; **compile** (the game's `audi.c`/`snd.c`/`music.c` depend on it). -* **`src/libultrare/audio/*.c`** (`drvrNew.c`, `env.c`, `reverb.c`) — Rare's +* **`src/libultrare/audio/*.c`** (`drvrNew.c`, `env.c`, `reverb.c`); Rare's audio "New" driver. `synthesizer.c` calls `alSaveNew`/`alAuxBusNew`/ `alMainBusNew`/`alLoadNew`/`alResampleNew`/`alEnvmixerNew`/`alEnvmixerParam`/ `alFxParam`, all defined here. Runs on the CPU; **compile**. -* **`src/libultra/gu/*.c`** — matrix helpers (`guMtxF`, `guOrtho`, ...). +* **`src/libultra/gu/*.c`**; matrix helpers (`guMtxF`, `guOrtho`, ...). **Compile**. -* **`src/libultrare/io/vitbl.c`** — defines `osViModeTable` (a data table), +* **`src/libultrare/io/vitbl.c`**; defines `osViModeTable` (a data table), referenced ~15× by `fr.c`. **Compile** (it's data, not a driver). This is the *only* `io/` file we compile. -* **All other `src/libultra/io/*.c` and `src/libultrare/io/*.c`** — hardware +* **All other `src/libultra/io/*.c` and `src/libultrare/io/*.c`**; hardware drivers (PI, SI, SP, DP, VI, controller, PFS, motor, EEPROM, ...). **Exclude and shim** in `port/src/libultra.c`. Notably `pfsinit.c`/`pfsisplug.c` (`osPfsInit`/`osPfsIsPlug`) and `motor.c` (`osMotor*`) are called by `joy.c` only for accessory detection, so they are shimmed as no-ops (see §7). -* **`src/libultra/os/*.c` + `src/libultrare/os/*.c`** — OS core (threads, +* **`src/libultra/os/*.c` + `src/libultrare/os/*.c`**; OS core (threads, message queues, timers, cache, TLB). **Exclude and shim** in `port/src/libultra.c` (+ `n64stubs.c` for the boot/TLB/FPU symbols). @@ -296,7 +296,7 @@ What the PC build must do with each: | Subsystem | GE API used | Port strategy (from PD) | |-----------|-------------|--------------------------| -| RSP (coproc 0) | `osSpTaskLoad/StartGo/Yield`, display lists | Software RSP (`fast3d`) — **main work** | +| 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 | @@ -312,32 +312,32 @@ What the PC build must do with each: GE's `include/gbi_extension.h` defines the custom GBI surface. Compared to the PD port's `fast3d`, the deltas are: -1. **`G_TRI4`** — draws up to 4 triangles in one command using 4-bit vertex +1. **`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 for `G_TRI2`). * **Status: already supported** by the PD `fast3d` (`gfx_sp_tri4`, and the PD game itself uses it). ✅ -2. **`G_SETTEX`** (`0xc0`) — "use texture from bank": selects a texture by +2. **`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 by `gsSPUseTexture` (`include/gbi_extension.h:193`). * **Status: appears UNUSED.** `gsSPUseTexture` is defined but never called anywhere in the game code or the RSP ucode (`rsp/graphics/gmain.s`), and `G_SETTEX` appears nowhere outside its `#define`. The game uses the standard texture commands (`gSPTexture`, `gSPTextureL`, - `gSPTextureRectangle`, `gSPTextureRectangleFlip`) — all already handled + `gSPTextureRectangle`, `gSPTextureRectangleFlip`); all already handled by the PD `fast3d`. **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 a `G_SETTEX` decode path as a safety net, but it is no longer the headline task. -3. **Custom color-combiner modes** — `G_CC_MODULATEIFADE`, `G_CC_FADE`, +3. **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. -4. **Custom render mode** — `G_RM_CUSTOM_AA_ZB_XLU_SURF` (AA_ZB_XLU with +4. **Custom render mode**; `G_RM_CUSTOM_AA_ZB_XLU_SURF` (AA_ZB_XLU with `Z_UPD`). * **Status: handled** by the render-mode -> GL state mapping; verify. -5. **`gDPLoadTLUT06/07`** — custom TLUT load variants. +5. **`gDPLoadTLUT06/07`**; custom TLUT load variants. * **Status: minor**, map onto the existing `G_LOADTLUT` path. **Conclusion:** the PD `fast3d` is a strong starting point. `G_SETTEX` appears @@ -347,7 +347,7 @@ 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 — +> 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. --- @@ -355,7 +355,7 @@ sync/flush) is shared. ## 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 + `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 @@ -370,7 +370,7 @@ sync/flush) is shared. * **Controllers:** `src/joy.c` uses `osContInit` / `osContStartReadData`. The port maps SDL2 keyboard/mouse/gamepad into the N64 `OSError`/controller - structs. PD implements 1964GEPD-style and Xbox-style bindings — a good + 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`) uses `joyGamePakProbe/LongRead/LongWrite`, which @@ -386,35 +386,35 @@ sync/flush) is shared. ## 8. Recommended plan -### Phase 0 — Scaffolding (this change) +### 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 N64 `Makefile`. * This document + `port/README.md`. -### Phase 1 — Boot to a window +### Phase 1: Boot to a window * Port `system.c`, `fs.c`, `romdata.c`, `config.c`, `main.c`. * Port `video.c` + a minimal `fast3d` (clear color, present). * Goal: load `ge007.u.z64`, open a window, draw a clear color. -### Phase 1.5 — Boot to first frame (ABI reconciliation) +### Phase 1.5: Boot to first frame (ABI reconciliation) * Get mainThread through all of `bossInitMainthreadData()` → `bossEntry()` → - `bossMainloop()` and render frame 1. (Done — D34–D58; see §G.) -* Recurring mechanism: **ROM-data struct ABI/layout fixes** (the D32 pattern) — + `bossMainloop()` and render frame 1. (Done; D34–D58; see §G.) +* Recurring mechanism: **ROM-data struct ABI/layout fixes** (the D32 pattern); a 32-bit-pointer struct read as a 64-bit-pointer struct misaligns; convert embedded pointer fields to `u32` + cast at use sites (PD ground truth), then verify the load-time rebase yields valid DRAM addresses. See §H for the step-by-step procedure and the standing "non-negotiable #2 ABI exception." * Goal: title screen / first level actually drawn on the GL surface. -### Phase 2 — RSP / rendering +### 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 a `G_SETTEX` decode - path only if it turns out to be used (see §5 — it appears unused). + 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 +### 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 `aspMain` microcode, wire `aspMainTextStart`/`aspMainDataStart` to the real microcode @@ -422,7 +422,7 @@ sync/flush) is shared. * Port `input.c` (keyboard/mouse/gamepad). * Goal: playable with sound. -### Phase 4 — Saves + polish +### 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. @@ -431,21 +431,21 @@ sync/flush) is shared. ## 9. Open questions / risks -1. **`G_SETTEX` (low priority)** — appears unused (`gsSPUseTexture` is never +1. **`G_SETTEX` (low priority)**; appears unused (`gsSPUseTexture` is 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) from `gmain.s` + `tex.c` / `initmttex.c`. Not currently the main unknown. -2. **Custom CC correctness** — the GLSL compiler must handle every fade/blend +2. **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. -3. **Scheduler fidelity** — GE's `sched.c` has speed-graph / profiling hooks +3. **Scheduler fidelity**; GE's `sched.c` has 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. -4. **Decomp completeness** — the decomp is WIP; some functions may still be +4. **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 upstream `n64decomp/007` and rebase. -5. **Endianness / alignment** — GE, like PD, is big-endian N64 code with +5. **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. diff --git a/docs/porting-notes.md b/docs/porting-notes.md index 2d870939..be801dd7 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -3,34 +3,34 @@ title: Porting Notes description: A field guide to the recurring N64-to-PC bug classes hit while running unmodified big-endian game code on a 64-bit host, each with its symptom and fix. --- -# Porting notes — recurring N64→PC bug classes +# Porting notes: recurring N64→PC bug classes A field guide to the bug classes that keep recurring when running big-endian 32-bit N64 game code, unmodified, on a little-endian 64-bit host. Terse by -design — each entry compresses a full investigation to a symptom, a fix, and a +design; each entry compresses a full investigation to a symptom, a fix, and a grep heuristic for finding siblings. Each entry cites a `Dxx` label; the full evidence and fix for that instance live in [`dev/findings.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/findings.md) under the same label. Skim the section headers; read the classes relevant to the task at hand. -If you are debugging a crash in this port, read this first — the odds are +If you are debugging a crash in this port, read this first; the odds are good that you are looking at one of these. ## Contents -- [A. Pointer-width struct growth (32→64) — the dominant class](#a-pointer-width-struct-growth-3264--the-dominant-class) +- [A. Pointer-width struct growth (32→64); the dominant class](#a-pointer-width-struct-growth-3264--the-dominant-class) - [B. 16-byte PC `Gfx` / `Vtx` vs 8-byte N64](#b-16-byte-pc-gfx--vtx-vs-8-byte-n64) - [C. Big-endian rodata / ROM data read on little-endian PC](#c-big-endian-rodata--rom-data-read-on-little-endian-pc) - [C2. Port-layer / SDL shims](#c2-port-layer--sdl-shims) - [D. N64 hardware idioms fast3d does not emulate](#d-n64-hardware-idioms-fast3d-does-not-emulate) -- [D2. The HUD/model "X-mirror" — RESOLVED](#d2-the-hudmodel-x-mirror-d114d116--resolved-it-was-an-upside-down-capture) +- [D2. The HUD/model "X-mirror"; RESOLVED](#d2-the-hudmodel-x-mirror-d114d116--resolved-it-was-an-upside-down-capture) - [D3. GCC/mingw makes an all-non-negative `enum` UNSIGNED](#d3-gccmingw-makes-an-all-non-negative-enum-unsigned) - [D4. N64 "interrupts off" must be a real lock on PC](#d4-n64-interrupts-off-is-not-free-on-pc--it-must-be-a-real-lock) - [D5. Loop bounds that assume linker adjacency of two globals](#d5-loop-bounds-that-assume-linker-adjacency-of-two-file-scope-globals) - [E. Process / method notes](#e-process--method-notes) -## A. Pointer-width struct growth (32→64) — the dominant class +## A. Pointer-width struct growth (32→64): the dominant class A decomp struct with pointer fields, or one pun-allocated into a fixed N64-sized hole / hardcoded byte count, is **larger on x86-64**. Reading it @@ -50,12 +50,12 @@ state. `sizeof(struct Model)` grows so the "fields" that aliased `.render_pos` / `.scale` / `.animframe1` on N64 no longer overlap → NULL `render_pos` → pause-menu crash; fix = real inline `struct Model` + pool, redirect the - named reads to the member — same as D100/D102), + named reads to the member; same as D100/D102), D119 (`weapons_held[]->chr` punned as `ChrRecord*` to read - `.act_*.attack_item` — aliased `WeaponObjRecord.weaponnum` at 0x80 on + `.act_*.attack_item`; aliased `WeaponObjRecord.weaponnum` at 0x80 on N64 via act-union@0x2C+84; act union moves to ~0x38 on PC). -**A1. Raw-byte aliases into a union (`act_ubytes.padding[N]`) — the silent +**A1. Raw-byte aliases into a union (`act_ubytes.padding[N]`); the silent variant.** The nastiest members of this class do not crash: a literal byte index into a union that overlays pointer-bearing structs still *reads*, it just reads the wrong byte, so the game runs and merely behaves wrong. Grep @@ -67,39 +67,39 @@ for hardcoded indices into any `padding[]`/`u8[]` union arm. waypoint *[6]@20, u8 curindex@44, u8 unk59@45}` on N64; at 64-bit the three pointer members widen and `unk59` moves 45 → 81, so the literal 45 lands on **byte 5 of `waypoints[1]`**. With a low-4GB arena - (`0x00000000_70xxxxxx`) that byte is **always 0x00** — a stable, plausible + (`0x00000000_70xxxxxx`) that byte is **always 0x00**; a stable, plausible value, not garbage. Effect: the locomotion-animation selector received tier 0 forever and every AI character in the game was bound to a *walk* animation; since GE travel is anim-root-motion driven, all AI moved at - walk pace on every level. No crash, no log, no NaN — it took a runtime + walk pace on every level. No crash, no log, no NaN; it took a runtime probe comparing the *commanded* tier (named field, correct) against the *bound* animation (alias, wrong) to see it. - **D210** (sibling, found by the post-D209 sweep): `chrToPatrol` inits `act_patrol.lastvisible60` via `act_init.padding[0x13]` (union byte `0x4c`). - `act_patrol` starts with a `patrol_path *path` — widens 4→8, so `padding[0x13]` + `act_patrol` starts with a `patrol_path *path`; widens 4→8, so `padding[0x13]` no longer hits `lastvisible60` (now `0x50`); it lands in `waydata` and `lastvisible60` is left uninitialised → the patrol "haven't seen the player recently" timer reads garbage for the first ticks. Fix: named field under `#ifdef PORT`. **NB the alias need not land on a *pointer byte*** (as D209 - did) — here it lands on a plain float that just isn't the field intended. + did); here it lands on a plain float that just isn't the field intended. The tell is the same: a hardcoded index into `padding[]` right next to named writes of the *same* arm. The rest of the `padding[N]` sweep (`chrlvTickAnim`/`chrlvTickDead` → `act_anim`/`act_dead`, both pointer-free) is layout-stable. **Lesson.** When a value reads as a clean constant (0, 1) rather than -garbage, a pointer byte is a prime suspect — the high bytes of a low-4GB +garbage, a pointer byte is a prime suspect; the high bytes of a low-4GB heap pointer are all zero, so a misaligned read looks like a legitimate "feature off" value. Prefer the named field under `#ifdef PORT`; the alias is only correct at 32-bit pointer width. Also: verify that every caller reaches the site with the union arm you are naming actually active. - **Open landmine:** raw hardcoded-offset accessors into `struct player` - / `struct hand` — see `docs/dev/AUDIT-M6-player-offsets.md`. -- **D126 corollary — a trailing runtime list pointer in a ROM-serialized + / `struct hand`; see `docs/dev/AUDIT-M6-player-offsets.md`. +- **D126 corollary; a trailing runtime list pointer in a ROM-serialized record.** If a fixed-size ROM record ends with a `T *next` (or `*child`, `*parent`) that game code *writes* while walking the setup stream (linked-list build), that field is 4B/end-of-record on N64 but widens to - 8B and 8-aligns on PC — the store spills past the N64 record size into + 8B and 8-aligns on PC; the store spills past the N64 record size into the *next* record and silently corrupts a polymorphic walk downstream (type byte clobbered → wrong strides → wrong command indices → wrong pointer resolution, crashing far away). Offline converter (`d*_emit.py`) @@ -109,7 +109,7 @@ reaches the site with the union arm you are naming actually active. 22 (`TagObjectRecord.NextTag`, already handled). Check every record type the walk can see for a `set_parent_*` / `*_entry_parent` writer. -- **D132 corollary — a `union { T *ptr; s32 IndexN; }` field is 8B/8-aligned +- **D132 corollary; a `union { T *ptr; s32 IndexN; }` field is 8B/8-aligned on PC even when the ROM image only ever stores the `s32`.** `LinkRecord` (`first`/`Index1`), `LockDoorRecord`, `SafeObjectRecord` etc. pack their index words tight (4B) in the N64 setup stream, but the compiler places @@ -121,11 +121,11 @@ reaches the site with the union arm you are naming actually active. initialise (usually non-crash, because the failed guard also skips the pointer stores). Fix: emit each `IndexN` at PC offset `8 + 8*(N-1)` and size the record with the widened unions + trailing `*next` - (`d88_propdefs.py` types 14/19/38/44 — proposed in §F D132, not yet + (`d88_propdefs.py` types 14/19/38/44; proposed in §F D132, not yet applied). Grep every ROM-serialized record for `union {` with a pointer arm. NB `[u16 ID][s16 x]` NOT in a union (e.g. `TagObjectRecord`) stays - at offset 4 — only the pointer union forces the 8-align. -- **D130 corollary — in-place N64→PC struct-array re-layout aliases when the + at offset 4; only the pointer union forces the 8-align. +- **D130 corollary; in-place N64→PC struct-array re-layout aliases when the stride grew less than the read span.** A ROM struct array widened on PC (`fontchar` 24→32B, `romdataFixupFont`) is often re-laid-out *in place*: for glyph `i`, `dst = base + PCstride*i`, `src = base + N64stride*i`, so @@ -133,7 +133,7 @@ reaches the site with the union arm you are naming actually active. than the per-element field-copy read span*, so `dst` overlaps `src`, and a forward field-by-field `*dst_k = f(*src_k)` loop overwrites a not-yet-read `src_j` (`j > k`). Result: later fields of the first few elements come out - as `f(f(earlier field))` — e.g. glyph 1's `width` became `bswap(bswap(index))`. + as `f(f(earlier field))`; e.g. glyph 1's `width` became `bswap(bswap(index))`. Iterating elements backward does NOT help (the overlap is *within* one element). Fix: read every source field of the element into locals first, then write. Grep every `romdataFixup*` / in-place relayout for a @@ -143,11 +143,11 @@ reaches the site with the union arm you are naming actually active. before `chars[0]`. On N64 that hits the adjacent 4-byte-field kerning table (small values → a garbage-but-TMEM-valid glyph); on PC the struct's 8-byte `pixeldata` reads a wild pointer → fast3d AV. Latent in `textrelated.c` - (`textRender*`/`textMeasure` ASCII paths) — a `GLYPH_IDX` clamp fixes it if + (`textRender*`/`textMeasure` ASCII paths); a `GLYPH_IDX` clamp fixes it if a level actually feeds a control byte to the HUD text (none do yet; D130 was a font-relayout bug, not this). -- **D131 corollary — `osVirtualToPhysical()` truncates a compiled-symbol +- **D131 corollary; `osVirtualToPhysical()` truncates a compiled-symbol pointer to 32 bits.** The port shim is `(u32)(uintptr_t)va` (`libultra.c`). For a runtime DRAM pointer (`0x70xxxxxx`) the cast is lossless; for a **compiled module symbol** (`.bss`/`.rodata` matrix, e.g. @@ -160,35 +160,35 @@ reaches the site with the union arm you are naming actually active. restore the high word for a fallthrough `w1 ∈ [0x40000000, 0x70000000)` (module is fixed-based at `0x140000000`; DRAM/KSEG0/segmented/phys are all handled in earlier branches). `gSPDisplayList(&globalDL_0xNNN)` is NOT - affected — the port's `Gwords.w1` is 64-bit and `gDma1p` stores the full + affected; the port's `Gwords.w1` is 64-bit and `gDma1p` stores the full pointer; only `osVirtualToPhysical` truncates. -- **D177 — a struct passed as `s32 *` and integer-indexed past a leading +- **D177; a struct passed as `s32 *` and integer-indexed past a leading pointer member.** `stanCheckLinkedSpecialTile` takes the caller's `struct StandTileLocusCallbackRecord { s32 *rooms; s32 count; … }` typed as `s32 *outFlags` and does `outFlags[1] = 1`. On N64 `[1]` is `count`; on PC `rooms` is 8 bytes so `[1]` is its *high half* and `count` moved to - `[2]` — the write is silently lost, the reader (`->count`) sees 0. Two + `[2]`; the write is silently lost, the reader (`->count`) sees 0. Two tells for this class: (a) a function parameter typed `s32 *` / `u32 *` / `void *` that is really a named struct (check what the caller passes and what other consumers cast it to); (b) the matching stack local declared as a small "placeholder while matching" struct whose N64 size equals the real - record — it silently under-allocates once any member widens. Fix both: + record; it silently under-allocates once any member widens. Fix both: cast to the real type + write fields by name, and declare the local as the real struct, under `#ifdef PORT`. (`[0]`/`rooms` often still works by luck - on LE — low half at +0 — which masks the bug for one of the two fields.) + on LE, low half at +0, which masks the bug for one of the two fields.) -- **D248 — a hardcoded byte offset used to index "one struct past the end of +- **D248; a hardcoded byte offset used to index "one struct past the end of the array" desyncs from a real field write once the struct grows.** `OSScClient` (`src/sched.h`) is `{next; msgQ;}`, 8B on N64 / 16B on PC. `osScAddClient()` deliberately writes a per-client flag into the *next array element's* first field (`c[1].next = next`) instead of adding a - real member — cheap on N64 since the caller over-allocates the array by + real member; cheap on N64 since the caller over-allocates the array by one slot. The reader, `__scHandleRetrace`, doesn't index the struct back; it recomputes the same address by hand as `*((s32*)client + 2)` (a hardcoded 8-byte offset). That offset only equals `sizeof(OSScClient)` (and thus `&client[1]`) when pointers are 32-bit. On PC it lands 8 bytes - into `client[0]` itself — inside `msgQ`, a real non-NULL pointer — so the + into `client[0]` itself, inside `msgQ`, a real non-NULL pointer, so the flag reads as permanently "set" for every client, silently collapsing a 60Hz/30Hz per-client split onto whichever branch a nonzero flag takes. **General tell:** any place a struct is deliberately under-declared and a @@ -197,7 +197,7 @@ reaches the site with the union arm you are naming actually active. `sizeof`), but a matching *read* done via a raw `(T*)p + N`/byte-offset cast instead of `p[1].field` will silently desync once the struct's size changes across the port. Fix: always index the struct on both ends - (`client[1].next`), never hand-roll the byte math — it's the direct + (`client[1].next`), never hand-roll the byte math; it's the direct decompilation of what the write side already does, `#ifdef PORT` with the N64 hand-rolled offset kept verbatim under `#else`. §F **D248**. @@ -209,7 +209,7 @@ expressed in N64 `Gfx`/`Vtx` units is **half-size** on PC. - Instances: D50.6 (texCopyGdls copied only w0 of each 16-byte slot), D58 (DL reserve 0x100→0x200), D85 (`bgWidenRoomGdl` 8→16 + bswap), D95 (2× master-DL buffer + raised mempool ceiling). -- **D135 corollary — an unported GBI *parser* (not just a buffer size).** Code +- **D135 corollary; an unported GBI *parser* (not just a buffer size).** Code that walks a DL command stream with raw byte/word indices into an 8-byte N64 `Gfx` (`*(s8*)gdl` for the cmd, `((u32*)gdl)[1]` for w1, `((u8*)gdl)[5..7]` for `G_TRI1` vtx indices, `((u32*)gdl)[0/1]` nibble reads for `G_TRI4`) reads @@ -220,30 +220,30 @@ expressed in N64 `Gfx`/`Vtx` units is **half-size** on PC. access with the equivalent shift/mask on w0/w1 (BE byte `i` of a word → `(word >> (8*(3-i))) & 0xff`). `gdl++` (advances by `sizeof(Gfx)`) is already correct. Watch `(s32)ptr` truncation in vtx-base math and `x | 0x80000000` - KSEG0 folds (identity on PC — just drop the OR, and guard segmented w1). + KSEG0 folds (identity on PC; just drop the OR, and guard segmented w1). Instances: `bgTestHitOnObj` (`propobj.c`, FIXED); `bgTestRayIntersectionInRoom` - + `bgTestBulletHitBackground` tail (`bg.c`, D154 — ported M-28, re-audited + + + `bgTestBulletHitBackground` tail (`bg.c`, D154; ported M-28, re-audited + bug-fixed M-30, playtest-gated). PD ground truth: `pd_port` uses `gdl->dma.cmd` + `GFX_W0_BYTE(i)`/`GFX_W1_BYTE(i)` macros (`3-i` / `11-i` on 64-bit LE). -- **D154 corollary — the PC `Gdma_le` shim's `.par` is NOT the N64 params byte.** +- **D154 corollary; the PC `Gdma_le` shim's `.par` is NOT the N64 params byte.** When porting a room/model DL GBI parser, only `.dma.cmd` is safe to read through the `port/shim/PR/gbi.h` `Gfx` union: that shim lays `Gdma_le` out as `par:24` (bits 0-23 of word0 = the *packed length* from `gDma1p`), `cmd:8` (bits 24-31). The N64 `Gdma` has `cmd:8` (byte 0) then `par:8` (**byte 1**, bits 16-23 = the `((n-1)<<4)|v0` G_VTX params). So an N64 `((u8*)gdl)[1]` / - N64-semantics `.dma.par` becomes `((u32)gdl->words.w0 >> 16) & 0xff` on PC — + N64-semantics `.dma.par` becomes `((u32)gdl->words.w0 >> 16) & 0xff` on PC; **`gdl->dma.par` gives bits 0-23 (length), and `& 0xf` on it is always 0** because `len == 16*n`. The original D154 port had exactly this bug (vtxoff forced to 0). Same trap latent in `bgBuildRoomVtxBounds` (`gdl.dma.par>>4&0xf` - reads PC bits 4-7, not N64 bits 20-23 — currently tolerated). Rule: for + reads PC bits 4-7, not N64 bits 20-23; currently tolerated). Rule: for anything but the opcode, extract the bit-field explicitly from `(u32)gdl->words.w0` / `.w1`, don't trust the named `.dma.*` sub-fields. Also: a `words.w0 << k >> m` bit-extract idiom that relied on 32-bit truncation on N64 must be `(u32)`-cast first on PC (`words.w0` is 64-bit). - **Also bites RAW hardcoded struct-stride writes, not just Gfx/Vtx.** - D128: `sub_GAME_7F0B37EC` did `((u8*)g_BgPortals)[(portal<<3)+6] |= 2` - — the N64 `bg_portal_data_entry` is 8B (`ptr@0, cr1@4..cb2@7`); on PC + D128: `sub_GAME_7F0B37EC` did `((u8*)g_BgPortals)[(portal<<3)+6] |= 2`; + the N64 `bg_portal_data_entry` is 8B (`ptr@0, cr1@4..cb2@7`), but on PC the widened `offset_portal` ptr makes it 16B (`cr1@8..cb2@11`), so the write landed in the middle of another portal's pointer. Fix = use the struct accessor under `#ifdef PORT` (every other site already does: @@ -252,13 +252,13 @@ expressed in N64 `Gfx`/`Vtx` units is **half-size** on PC. pointer field. D141: `set_enviro_fog_for_items_in_solo_watch_menu` (`gunfire.c:1720`) indexed a `ModelNode*` array (`ModelFileHeader.Switches`) as - `*(ModelNode**)((u8*)Switches + j + 0x48)` with `j += 4` — the `0x48`/ + `*(ModelNode**)((u8*)Switches + j + 0x48)` with `j += 4`; the `0x48`/ `0x5c` and the step are 4-byte-pointer constants. PC 8-byte stride → wrong + misaligned slot → bogus non-NULL node → crash in `modelGetNodeRwData`. Fix: `Switches[18 + (j>>2)]` / `Switches[23 + (j>>2)]` under `#ifdef PORT` (`0x48/4=18`, `0x5c/4=23`). Grep every `(TYPE**)((u8*)arr + )` and `arr[i << k]` where the array element is a pointer. - D191: `bondviewSelectCuff` (`bondview2.c`) — the **same** `Switches` + D191: `bondviewSelectCuff` (`bondview2.c`); the **same** `Switches` array, missed by D141. `offset = switchindex << 2` then `base = (ModelNode**)((u8*)switches + offset)`, deref'd `base[0..5]`. On PC the 8-byte slot stride makes `base[N]` a half-word-swapped pointer @@ -266,62 +266,62 @@ expressed in N64 `Gfx`/`Vtx` units is **half-size** on PC. the player fires with a fresh-save Bond model (Bunker ii first guard, Statue post-cutscene). Fix: `offset = switchindex * sizeof(ModelNode *)` under `#ifdef PORT`. When you fix one `arr[i<<2]` pointer-array site, grep - the whole file — these travel in packs. + the whole file; these travel in packs. ## C. Big-endian rodata / ROM data read on little-endian PC ROM assets and compiled-in `.rodata` are big-endian. Anything not run through a converter or a runtime bswap fixup reads scrambled. -- `f32` values are BE **word pairs** — a naive byte reversal off-by-one +- `f32` values are BE **word pairs**; a naive byte reversal off-by-one corrupts every float: D73 (sinf/cosf `du` pairs → `DVAL()` macro), D112 (`d43_emit.py put_f32` `src[doff:doff+4][::-1]`). - **D73 scope is now settled (M-32 D75 triage):** the whole - `src/libultra/gu/` tree is endian-clean — only `sinf.c`/`cosf.c` used the + `src/libultra/gu/` tree is endian-clean; only `sinf.c`/`cosf.c` used the `du` union and both are `DVAL()`-wrapped; `rotate/perspective/ortho/ lookat/scale/translate/mtxutil/normalize/align` use plain float literals or pure integer bit-packing (`FTOFIX32`+shift/mask). The game's own `matrixmath.c` (`matrix_4x4_set_lookat*`, `_set_projection`, `_f32_to_s32`) is likewise native-LE (D114). **Do not re-audit gu or - matrixmath for a "float endianness" bug** — if a matrix comes out wrong + matrixmath for a "float endianness" bug**; if a matrix comes out wrong on PC the cause is upstream data, a struct-field pun (D100/D140/D156), - never these files. (The "D114/D116 viewport mirror" example is withdrawn — + never these files. (The "D114/D116 viewport mirror" example is withdrawn; M-33/D168 showed D114/D116 were an upside-down `GE_PCDUMP` capture, not a real flip.) - Header offset tables / pointers: D54 (cseq ALMidiHdr), D68 (Globalimagetable), D87 (ramromfilestructure), D88 (Usetup* tables). - Negative-terminated index chains (`PointUsage[]`) in converted model rodata cycle forever if element endianness/stride is wrong: D120 - (opcode-0x18 collision record, `d43_emit.py` — guarded, not fixed). + (opcode-0x18 collision record, `d43_emit.py`; guarded, not fixed). - Packed bitfields cross byte boundaries differently: D78 / D83 (StandTile id/room, header mid/tail). - **Rule:** prefer an offline sidecar converter (D43, D69, D88 pattern, `tools_pc/d*_emit.py`) over a runtime fixup for a whole format. -- **D139 — `(u8)word` / `(s8)word` to grab "the first field" of a struct +- **D139; `(u8)word` / `(s8)word` to grab "the first field" of a struct reads the WRONG byte on LE.** N64 code that does `(u8)obj[0]` (obj a - `u32*`) to read a byte-3 field of a big-endian header word — e.g. the - propDef type in `[u16 extrascale][u8 state][u8 type]` — gets the low byte, + `u32*`) to read a byte-3 field of a big-endian header word; e.g. the + propDef type in `[u16 extrascale][u8 state][u8 type]`; gets the low byte, which is the *last* BE field. On LE it's `extrascale`. `cleanupObjects` did this for its walk-termination + type dispatch → never saw `PROPDEF_END` → ran off the blob → freed garbage → crash on stage unload. Fix: use the struct member (`pdef->type`) like every other consumer. Grep for `(u8)*`/`(s8)*`/`& 0xff` on a `u32`/`s32` that's really a - serialized multi-field word — especially GBI-word and header-word reads + serialized multi-field word; especially GBI-word and header-word reads that were never routed through a converter or `#ifdef PORT`. -- **D150 — `langGet()` NULL flows into a `str*` primitive.** The watch +- **D150; `langGet()` NULL flows into a `str*` primitive.** The watch BRIEF/OBJECTIVES pages (and `front.c` mission text) build their strings with `strcpy`/`strcat(buf, langGet(id))`. `langGet` returns NULL on PC for any bank the current menu flow never loaded (D129/D143); N64 always resolves these ids so the decomp never guards. `strcat(buf, NULL)` then derefs 0. Fix = NULL-tolerant `strcpy/strncpy/strcat` under `#ifdef PORT` in `str.c` (NULL src → empty string, NULL dst → return). **Gotcha:** these are - `__nonnull__` **builtins** to GCC — a plain `if (src == NULL)` on the + `__nonnull__` **builtins** to GCC; a plain `if (src == NULL)` on the parameter is deleted as provably-dead (confirmed in `-Og` disassembly: no `test` emitted). Launder the pointer through an empty `__asm__("":"+r"(p))` (`GE_IS_NULL()`) so the guard survives. Same class as the D143 textRender NULL guards. Audit any hand-rolled libc primitive in `src/` that a port NULL can reach. -- **D219 (M-112; the finding itself resolved in M-115 — kept as a triage lesson) — "the shared texture decoder is proven correct" is +- **D219 (M-112; the finding itself resolved in M-115, kept as a triage lesson), "the shared texture decoder is proven correct" is a claim about *pixel format*, not *compression method*, and the two are independent knobs on the same runtime `texLoad`.** `src/game/image.c`'s non-zlib texture decompressor reads a per-image 4-bit `TEXCOMPMETHOD_*` @@ -335,7 +335,7 @@ through a converter or a runtime bswap fixup reads scrambled. color table, bits read directly in final pixel layout). "Every other RGBA16 texture in the game renders correctly, so the decoder can't be the bug" is **not** a valid inference unless you've confirmed the *comparison* - texture used the *same* `TEXCOMPMETHOD_*` as the suspect one — they can + texture used the *same* `TEXCOMPMETHOD_*` as the suspect one; they can silently diverge into different functions despite an identical `G_IM_FMT_RGBA,G_IM_SIZ_16b` DL declaration. Also: whether a texture's `G_SETTIMG` marker is baked directly into a compiled display list @@ -343,17 +343,17 @@ through a converter or a runtime bswap fixup reads scrambled. `gimgFixupGlobalimagetable`'s DL-bswap pass) vs. synthesized at runtime from an `sImageTableEntry`'s `index` field (`texSelect`, `src/game/othermodemicrocode.c`) is a real, checkable difference in - *resolution* path worth ruling in/out early — but both funnel into the + *resolution* path worth ruling in/out early; but both funnel into the same `texLoadFromDisplayList`→`texLoad` call, so it is a weaker lead than the compression-method one. See `docs/dev/findings.md` D219 for the full writeup and an env-gated (`GE_D219RAW`) diagnostic left in the tree to check this on the next session that can capture runtime output. -- **D157 — a small value in the LOW BYTE of a BE `s32` word, read as `s8` at +- **D157; a small value in the LOW BYTE of a BE `s32` word, read as `s8` at the word's last offset, reads 0 on LE after the converter byte-swaps it.** Same family as D151/D139. `struct objective_entry.difficulty` is `s8` at offset 0xF; the underlying type-23 propDef word 3 (`MissionObjectiveRecord.MinDificulty`) is a BE `s32` whose value (0..3) sits - in byte 0xF on N64. `d88_propdefs.py` `_bswap32`'s the word (correct — it IS a + in byte 0xF on N64. `d88_propdefs.py` `_bswap32`'s the word (correct; it IS a 32-bit field), moving the value to byte 0xC on LE, so the `s8`@0xF read yields 0 for *every* objective. Consequence was total: `get_difficulty_for_objective` → 0 for all → `objectiveIsAllComplete()` on Agent evaluated objectives that @@ -361,11 +361,11 @@ through a converter or a runtime bswap fixup reads scrambled. campaign-unlock EEPROM write) never fired → **no solo level ever unlocked the next.** Fix: reorder the struct's post-swap tail under `#ifdef PORT` so the named byte reads offset 0xC. **Grep every struct that reads a sub-`s32` field - as `s8`/`u8`/`s16`/`u16` at a NON-zero in-word offset — after a converter + as `s8`/`u8`/`s16`/`u16` at a NON-zero in-word offset; after a converter bswap that offset is wrong.** Offline tools have the same trap (`dump_objectives.py` read byte 0xC = BE high byte = always 0). -- **D159 — an N64 "pre-swap for the RDP" texture massage is poison on PC because +- **D159; an N64 "pre-swap for the RDP" texture massage is poison on PC because fast3d doesn't emulate the RDP.** `texSwapAltRowBytes` (`image.c`) pairwise-swaps the 8-byte (`u32`) groups of every **odd** texture row before upload, to cancel the N64 RDP's odd-line TMEM address XOR (address bit 2) that fires during @@ -373,61 +373,61 @@ through a converter or a runtime bswap fixup reads scrambled. odd-row handling, so the pre-swapped odd rows upload scrambled → an 8-texel "venetian blind" / interlace comb. Invisible on small or distant textures, glaring on large 1:1 front-end images (wallet-Bond photo, passport crest). - Fix: `#ifdef PORT` no-op the function — fast3d wants a plain linear image. + Fix: `#ifdef PORT` no-op the function; fast3d wants a plain linear image. General rule: any `src/` routine whose comment or shape says "for the RDP / TMEM / N64 hardware" and that reorders/massages bytes is a **port-layer** candidate (same family as the K0-fold and interrupt-mask shims), not game - logic — audit `image.c` / `tex.c` for others (`texAlignIndices` row padding is + logic; audit `image.c` / `tex.c` for others (`texAlignIndices` row padding is load-bearing and must stay; the swap is not). -- **D160 — a "propDef command-index walk desync" is only possible when `sizepropdef()` +- **D160; a "propDef command-index walk desync" is only possible when `sizepropdef()` and the offline converter disagree on a record size.** Since D122/D126/D132 closed every gap, `d88_propdefs.PROPDEF_PC_BYTES[t] == sizepropdef()×4` for all 48 types, the propDef stream tiles byte-exactly, and `setupGetPtrToCommandByIndex` / `tagGetCommandIndex` (`object = sizepropdef(object)+object`, `sizeof(PropDefHeaderRecord)==4`) walk in lockstep with the converter. Before hypothesising a propDef walk desync for a new symptom (M-31 exhaustively re-verified: every record type, all 21 levels, - byte-exact tiling), run the stride cross-check first — if it's clean, the bug is + byte-exact tiling), run the stride cross-check first; if it's clean, the bug is elsewhere. The Dam rappel cutscene (D148) reached this wall: data path proven intact; residual cause is runtime AI-script control-flow / `CAMERAMODE_POSEND` cinematic render (D75 family). `GE_D160=1` diagnostic ships; needs a live Dam-to-exit playthrough. - **M-106 addendum:** independently re-derived (not just re-cited) — `CutsceneRecord` + **M-106 addendum:** independently re-derived (not just re-cited); `CutsceneRecord` (propDef type 46) has zero pointer/union fields, so it cannot exhibit this class at all; confirms the M-31 stride audit rather than superseding it. D148/D160's residual cause is still unlocated (needs the live trace); see D173 for a related negative result on the puppet-position side. -- **D132/D126 corollary — a zeroed ROM-serialized pointer-width tail slot is not +- **D132/D126 corollary; a zeroed ROM-serialized pointer-width tail slot is not automatically a converter bug; check the N64 source literal first (M-106).** Before assuming an offline converter wrongly zeroed a widened pointer field (the D122/D126/D132 "dead-on-load" pattern), grep the asset `.c` source tables (`assets/obseg/**/*.c`) for that field's literal initialiser. `PadRecord.stan` - (`src/bondtypes.h:1744-1751`) looked exactly like a D126/D132 miss — a ROM- - serialized tail pointer, zeroed by `tools_pc/d88_emit.py`'s `emit_pad()` — but - every `PadRecord` in every checked setup table (`UsetupdamZ.c` etc.) initialises + (`src/bondtypes.h:1744-1751`) looked exactly like a D126/D132 miss (a ROM- + serialized tail pointer, zeroed by `tools_pc/d88_emit.py`'s `emit_pad()`), + but every `PadRecord` in every checked setup table (`UsetupdamZ.c` etc.) initialises `stan` to literal `0`; N64 never carried a live pointer there either, so the PC zeroing is byte-identical to ground truth, not a regression. The tell that saves the trip: if the *source*, not just the ROM binary, always writes `0`/`NULL` for a field, it was never "dead-on-load" in the D123-corollary sense (a field that - reads a real small int/id before being overwritten) — it's simply always + reads a real small int/id before being overwritten); it's simply always runtime-computed, on both platforms, and a NULL seed into whatever consumes it is the original game's normal case, not a port defect. -- **D151 — a ROM-serialized `s32` slot decoded by the struct as `[u16 hi][u16 lo]` +- **D151; a ROM-serialized `s32` slot decoded by the struct as `[u16 hi][u16 lo]` reads zero on LE.** N64 code frequently splits a 32-bit setup-stream word into `u16 reserved; u16 realvalue;` where the useful value is always small and lands in the **low 16 bits of the big-endian word**. The offline converter correctly `_bswap32`'s the word (it IS a 32-bit field), which puts the value in the low - bytes — but the struct still reads `realvalue` from the *high* offset → 0. + bytes; but the struct still reads `realvalue` from the *high* offset → 0. Instances: `struct watchMenuObjectiveText.text` / `struct objective_entry.text` - (propDef types 35 / 23) — every watch briefing + objective line rendered blank + (propDef types 35 / 23); every watch briefing + objective line rendered blank (`langGet(0)` → NULL). Fix: `#ifdef PORT` widens the field to a full `u32` at the word offset; converter unchanged. Grep every ROM-serialized struct for a `u16 reserved`/`u16 pad` immediately before a `u16` the runtime actually reads. - **Python slice-assignment width is a silent corruptor** (D125): - `out[a:a+8] = b"\x00\x00\x00\x00"` on a `bytearray` does NOT raise — it + `out[a:a+8] = b"\x00\x00\x00\x00"` on a `bytearray` does NOT raise; it deletes `(8 - len(rhs))` bytes, shrinking `out` and shifting everything after `a` down. A widened pointer/`stan` slot filled with a 4-byte literal shrank `d88_emit.py`'s output 4 B per pad; once `len(out)` fell below a later region's write offset, `out[off:off+n] = data` clamped to - an empty range at the current end and *inserted* there — string blobs + an empty range at the current end and *inserted* there; string blobs drifted, boundpad names truncated (`p138d2`→`8d2`), `stanPackId` rejected → door `model=NULL` → crash. When emitting into a pre-sized bytearray, every `out[a:b] = rhs` MUST have `len(rhs) == b - a`; grep the emitter @@ -435,7 +435,7 @@ through a converter or a runtime bswap fixup reads scrambled. - A one-shot "sync patched values from the ROM copy into a compiled shadow array" pass must key its slot-detection on a field that is still intact *at sync time*. D124: `gimgSyncCompiledGlobalDLs` looked for the - `0xABCDxxxx` IMAGESEG marker in the ROM copy — but `texLoad()` had + `0xABCDxxxx` IMAGESEG marker in the ROM copy; but `texLoad()` had already overwritten every one of those with a real pointer, so the sync silently copied nothing and the compiled `globalDL_0xNNN` explosion DLs kept their link-time markers (latent on *all* levels; only tripped @@ -443,7 +443,7 @@ through a converter or a runtime bswap fixup reads scrambled. from the *destination* (compiled) array, which still holds the marker. - A polymorphic-record converter with a per-type handler table + a "generic" fallback silently corrupts any type the table forgot: the - fallback's word-granular bswap is wrong for sub-word fields. D122 — + fallback's word-granular bswap is wrong for sub-word fields. D122; `d88_propdefs.py` had no arm for 6 `inherits ObjectRecord` propDef types (TINTED_GLASS/VEHICHLE/AIRCRAFT/TANK/AUTOGUN/AMMO); the generic arm bswap32'd the `[s16 obj][s16 pad]` word as one u32, putting the model id @@ -467,7 +467,7 @@ through a converter or a runtime bswap fixup reads scrambled. - **Audit for ROM segments loaded raw with no fixup at all** (D178). Every `_fileNameLoadToAddr()` / `_fileNameLoadToBank()` call site loads a raw big-endian ROM image; the ones with a `struct` overlay need a BE→LE pass - and there is nothing in the load path that supplies one — the fixups are + and there is nothing in the load path that supplies one; the fixups are bolted on per call site (`langFixupLoadedBank()` in `language.c`, `romdataFixup*()` in `port/src/romdata.c`). The briefing segment (`Ubrief*Z` = `u16 brief[4]` + 10 × `{u16 textid; u16 difficulty}`) had @@ -493,17 +493,17 @@ through a converter or a runtime bswap fixup reads scrambled. - GE's aim model is **mode-dependent** (`bondview2.c bondviewProcessInput`): in **hipfire** (`!insightaimmode`) yaw = analog stick-X ("natural turn") and pitch = **digital C-up/C-down only** (stick-Y is move fwd/back); in - **aim mode** (R held) yaw *and* pitch are analog — stick pushed past ±60 - → proportional `(stick-60)/10` — and **C-up/C-down mean crouch/lean/zoom, + **aim mode** (R held) yaw *and* pitch are analog; stick pushed past ±60 + → proportional `(stick-60)/10`; and **C-up/C-down mean crouch/lean/zoom, not aim**. So the mouse→pad map must also be mode-aware: aim mode pushes the stick into the 61..80 band and emits **no** C-buttons (emitting C-down for "look down" while aiming = crouch, D118c); hipfire keeps the digital C-button pitch. `input.c` reads its own RMB/LShift state as the mode proxy (exact for hold-to-aim; a toggle scheme needs `g_CurrentPlayer-> - insightaimmode`). GE's native pitch is **inverted** (C-up → look down) — + insightaimmode`). GE's native pitch is **inverted** (C-up → look down); hide it so mouse-down looks down; `MouseInvertY` flips (D118, M-24). - `osContGetReadData(pad)` must fill **one OSContPad per channel** - (`MAXCONTROLLERS`-long array), not just controller 0 — joy.c passes the + (`MAXCONTROLLERS`-long array), not just controller 0; joy.c passes the whole `samples[i].pads` array (D118). - Controller state has one source: `port/src/input.c`. `libultra.c`'s SI section marshals `inputComputePad()` into `g_contPad[]`; it is driven by @@ -513,7 +513,7 @@ through a converter or a runtime bswap fixup reads scrambled. - **Appending a port-owned 2D overlay to the game frame: hook inside `gfx_run()`, not `videoSubmitCommands`.** `gfx_run()` (`gfx_pc.cpp`) is - monolithic — it does `start_frame` → `gfx_run_dl` → `gfx_flush` → + monolithic; it does `start_frame` → `gfx_run_dl` → `gfx_flush` → `end_frame` → `swap_buffers_begin` in one call, and the game DL is executed from `osSpTaskStartGo` (`libultra.c`), *not* through `videoSubmitCommands` (which is dead). So a second `gfx_run()` per frame is not possible (it swaps @@ -523,14 +523,14 @@ through a converter or a runtime bswap fixup reads scrambled. `gDPPipeSync` + `gDPSetCycleType(G_CYC_1CYCLE)` + `gDPSetTexturePersp(G_TP_NONE)` + a full-screen `gDPSetScissor` + `microcode_constructor()` (the game's own 2D combiner/rendermode prologue), then `gDPFillRectangle` / `textRender` - (game symbols, extern'd like `input.c` externs `current_menu` — UI, not + (game symbols, extern'd like `input.c` externs `current_menu`; UI, not logic). 2D pixel space is `viGetX()` x `viGetY()`. Return NULL when the overlay is closed so the frame is byte-for-byte unchanged. - **Verbatim RDP triangle commands chopped into `gImmp1(G_RDPHALF_1/_CONT/_2)` pairs** (GE's `skyRenderTri`/`skyRenderFull` hand-build an edge-walked `G_TRI_FILL`/`G_TRI_SHADE_TXTR` for the modified RSP ucode to reassemble) are - silently dropped by fast3d — it has no RDP triangle rasteriser. Substitute a + silently dropped by fast3d; it has no RDP triangle rasteriser. Substitute a normal `gSPVertex` + `gSP*Triangle` batch behind `#ifdef PORT`: if the game code already screen-space-projected the verts, re-emit them under a `guOrtho(l,r,b,t,…)` + identity modelview matching the projection's pixel @@ -540,19 +540,19 @@ through a converter or a runtime bswap fixup reads scrambled. coefficient block for this idiom carries `S·w′/T·w′/w′` per vertex (a real perspective-correct texture unit divides it back out per pixel) whenever the upstream game math computed S/T from a *world-space* projection without - dividing by `w` itself (GE's sky does — `unk20/24` are texel coords on the + dividing by `w` itself (GE's sky does; `unk20/24` are texel coords on the world cloud-plane intersection, not pre-divided). Re-emitting those under `w=1` makes fast3d's GPU pipeline interpolate texture coords *linearly in - screen space* instead of perspective-correctly — invisible when the + screen space* instead of perspective-correctly; invisible when the primitive's per-vertex `w` barely varies, but a primitive spanning near-camera to horizon-grazing geometry can have >30x `w` spread between its own vertices, and linear interpolation then smears the entire S/T delta evenly across every pixel instead of concentrating it near the - far/horizon vertices — reads as "tiles too densely / scrolls too fast" + far/horizon vertices; reads as "tiles too densely / scrolls too fast" everywhere instead of a gentle gradient. Fix: build a small custom projection matrix that gives each vertex its real `w` (any per-vertex camera-space `w` the game math already computed) while keeping the same - screen-space `x,y` (`clip.xy = ndc.xy·w`, `clip.w = w` — the GPU's normal + screen-space `x,y` (`clip.xy = ndc.xy·w`, `clip.w = w`; the GPU's normal divide recovers the intended `ndc.xy` regardless of `w`, only the interpolation changes); `Vtx.ob[]` is `s16` so pre-scale by a per-primitive `wScale` and have the matrix multiply it back in. **Follow-on caveat @@ -561,12 +561,12 @@ through a converter or a runtime bswap fixup reads scrambled. out across several separate calls to a per-primitive perspective- correction helper (one call per triangle instead of one call for the whole fan) and each call derives its own scale factor, two triangles - sharing an edge/vertex can each independently pick a different scale — + sharing an edge/vertex can each independently pick a different scale; and because the scale feeds into a low-precision (`s16`) vertex field, the same logical vertex quantizes to two slightly different values depending on which call computed it. The position math stays exact for any scale (it's just a rescale that cancels out algebraically), so this - isn't a logic bug and won't show up as wrong geometry — it shows up as a + isn't a logic bug and won't show up as wrong geometry; it shows up as a visible crack/seam at the shared edge, easy to misread as "two separate things happening" rather than one quantization mismatch. Fix: hoist the scale computation to scan every vertex in the whole shared group once, @@ -574,7 +574,7 @@ through a converter or a runtime bswap fixup reads scrambled. of letting each call recompute its own. **Follow-on caveat #2 (D227, M-98): fixing ONE per-primitive-derived quantity this way doesn't fix every one.** The same routine can derive more than one such "shared state - from the whole vertex group" value — GE's sky code also floors S/T into a + from the whole vertex group" value; GE's sky code also floors S/T into a per-primitive range fold to keep it in `s16` (`floorf(minS/64)*64`, `sky.c`'s `skyPortRenderPoly`). Sharing `wScale` across the fan and leaving the fold per-call left the exact same seam-shaped bug alive one @@ -584,14 +584,14 @@ through a converter or a runtime bswap fixup reads scrambled. only my local call's inputs, should derive from the whole shared group" bug in a routine, grep the same function for every other value computed the same way (any per-call min/max/floor/round over a subset of a larger - shared vertex/primitive pool) — don't assume fixing the first one you + shared vertex/primitive pool); don't assume fixing the first one you found closes the whole defect class. - Z buffer cleared by pointing the color image at it + fill-rect → does nothing in fast3d; must emit `G_CLEAR_DEPTH_EXT` (D105). - LOD / detail mip tiles: fast3d fabricates a crop when detail textures are off → force base tile (D107). - **A CI-format tile drawn with the TLUT disabled (`G_TT_NONE`) is not a - palette texture** — the N64 RDP feeds the raw TMEM texel straight into the + palette texture**; the N64 RDP feeds the raw TMEM texel straight into the color pipe, i.e. a CI8+`G_TT_NONE` tile behaves as I8. fast3d's `import_texture()` dispatched purely on `tile.fmt` and did a palette lookup against a stale `rdp.palette` → garbage (D161, GE Depot ceiling = blue @@ -607,7 +607,7 @@ through a converter or a runtime bswap fixup reads scrambled. - **GE model-DL TLUT payloads carry a 2-entry leading zero pad (D217, still open).** Every larger weapon/chr model TLUT decodes with `entry[0]==entry[1] ==0x0000` and real colour from index 2. `count=1` model TLUTs that feed - palette index 0 (solid-fill weapon parts — the PP7 grip) are the canary: if + palette index 0 (solid-fill weapon parts; the PP7 grip) are the canary: if the port's model-TLUT source pointer is even 4 bytes off, index 0 reads adjacent texel/blob bytes and the part renders a wrong flat colour (green on Facility/Depot, black-by-luck on Bunker1) while larger tiles look plausible @@ -617,43 +617,43 @@ through a converter or a runtime bswap fixup reads scrambled. theory above is refuted.** Traced the full reference chain: model files store textures as a numeric `TextureID` (`ModelFileTextures`, `src/bondtypes.h`) into the **global** `Globalimagetable` (fixed once at - runtime by `port/src/gimgfixup.c`, D68 — not per-model, not per-level, and + runtime by `port/src/gimgfixup.c`, D68; not per-model, not per-level, and not touched by any `tools_pc/d43_*.py` script); pixel+palette bytes are decoded fresh each level load by `texInflateZlib`/`NonZlib` (`src/game/image.c`) into a runtime pool, with the palette appended after - the pixel data (`tex->unk0a` records the boundary) — there is no + the pixel data (`tex->unk0a` records the boundary); there is no ROM-serialized or offline-converted "model-TLUT source pointer" to be off by 4 bytes. **General rule this adds to the catalogue: a "same asset, different bytes on different levels" symptom for a texture/model resource - does NOT imply an offline-converter/sidecar bug** — GE's model and image + does NOT imply an offline-converter/sidecar bug**; GE's model and image pipelines resolve almost everything through small numeric IDs into shared, runtime-decoded pools; the offline `tools_pc/d43_*.py` scripts only ever touch model *geometry* (nodes/GDL structure/vertex data), never texture or palette content. Before chasing a converter offset for a texture-content bug, first prove the content is even sidecar-owned (grep - the relevant `d43_*.py`/`d88_*.py`/`d69_*.py` for the field name — if it + the relevant `d43_*.py`/`d88_*.py`/`d69_*.py` for the field name; if it isn't there, the bug is runtime: `src/game/image.c`/`tex.c` (decode) or `port/fast3d/gfx_pc.cpp` (RDP emulation)). Current leading hypothesis for the real mechanism (unconfirmed, needs a fast3d-side probe out of this session's scope): `gfx_dp_load_tlut`'s palette-source addressing (`port/fast3d/gfx_pc.cpp:2202`) uses `pitch = rdp.texture_to_load.width + 1` from the preceding `gDPSetTextureImage`, which `texWriteLoadToTmemAddr` - (`src/game/tex.c:503,519`) always calls with a literal `width=1` — a + (`src/game/tex.c:503,519`) always calls with a literal `width=1`; a software-RDP approximation that may not correctly reproduce hardware LOADTLUT byte-addressing for this width=1 special case. See `docs/dev/findings.md` §D217 "M-88" for the full trace + empirical `GE_TEXDUMP` repro (BUNKER1 `pal[0]=0001` vs FACILITY `pal[0]=1a00`, exact match to the originally reported bytes). -- **RC3 / D167 — GL `GL_REPEAT` wraps at the uploaded image size; the N64 RDP +- **RC3 / D167; GL `GL_REPEAT` wraps at the uploaded image size; the N64 RDP wraps a render tile at `1<1 byte/pixel) +- **D219; a runtime bitstream decoder that stores a "wide" (>1 byte/pixel) reconstructed value via a native machine write needs an explicit byte-order fixup on PC; a compile-time C-array asset does not. And the fixup is per *importer family*, not per texture: scope it by pixel @@ -718,7 +718,7 @@ through a converter or a runtime bswap fixup reads scrambled. `src/game/image.c`'s texture decompressor (`texReadUncompressed`, `texChannelsToPixels`, `texBuildLookup`, `texInflateLookup`, `texInflateLookupFromBuffer`) builds each wide pixel as a shifted-OR - integer and does a native store — correct on N64 (native == big-endian), + integer and does a native store; correct on N64 (native == big-endian), byte-reversed on x86-64. This is the **opposite** of the usual "BE ROM data read as LE" direction in this section: here the PORT *host* writes native-endian and a downstream *shared, otherwise-correct* consumer has @@ -729,14 +729,14 @@ through a converter or a runtime bswap fixup reads scrambled. `import_texture_rgba32` (G_IM_SIZ_32b: RGBA32, RGB24) does `PD_BE32()` on a **native** u32 load → it wants host-order words and a store-time swap *inverts* them (that was the muzzle-flash-blue/ammo-pink - regression). So the correct scope is the decoded `TEXFORMAT_*` — which - every call site already switches on — not compmethod (the fire frames + regression). So the correct scope is the decoded `TEXFORMAT_*` (which + every call site already switches on), not compmethod (the fire frames span LOOKUP/RLE/HUFFMANBLUR/RLELOOKUP) and not a hardcoded texnum range. Static `assets/*.c` C-array textures don't hit this because `gfx_tex_normalize_source` (D71) already normalizes them upstream of `import_texture_*`; only assets that are BOTH genuinely multi-byte-per- pixel AND decompressed at runtime through `texLoad`'s bitstream path are - exposed. Offline ROM census (M-115): 94 such images — the 16-bit family + exposed. Offline ROM census (M-115): 94 such images; the 16-bit family is exactly `IMAGE_FIRE_0..14` + texnum 1198–1201/2430/2510–2523, and every ammo/flare/crosshair/muzzle-flash wide-pixel image is RGBA32. **Only the fire frames were eyeball-verified (M-115); texnum @@ -746,14 +746,14 @@ through a converter or a runtime bswap fixup reads scrambled. decode it with `GE_D219RAW=1` against `scratch/d219_census.txt`). General rule for this bug class: before "fixing" a shared decoder's byte order, enumerate **every consumer** of the buffer and check which - endianness each one assumes — the fix belongs at the store site, keyed + endianness each one assumes; the fix belongs at the store site, keyed on the format each consumer family expects. Final state (M-115, resolved): `PORT_PIXEL16` = bswap16 under `#ifdef PORT` (16-bit family only), `PORT_PIXEL32` = identity, wrapped around every wide-pixel store in the five functions above; CI/I4/I8/IA4/IA8 (single - byte, no byte order) untouched. The intermediate history — M-113's blanket + byte, no byte order) untouched. The intermediate history (M-113's blanket swap regressing muzzle flash/ammo HUD (M-114), reverted to identity, then - M-115's format-scoped re-application — is in `docs/dev/findings.md` D219. + M-115's format-scoped re-application) is in `docs/dev/findings.md` D219. - **The tile's declared format is not the source data's format when GE's custom ucode transforms TMEM at load time (D229).** `texSelect` @@ -761,7 +761,7 @@ through a converter or a runtime bswap fixup reads scrambled. `gDPSetTextureImage(CI,16b)` + `gDPLoadBlock` (the 16b is only the 4KB-per-block load convention: lrs counts 16-bit units, so 700 × 2 = 1400 bytes of raw 8-bit indices), and GE's RSP ucode expands indices→RGBA16 in - TMEM at load — after which `sub_GAME_7F09343C` (`src/game/unk_092E50.c`) + TMEM at load; after which `sub_GAME_7F09343C` (`src/game/unk_092E50.c`) re-declares the slot as RGBA/16b and samples blue. fast3d has no such transform: it trusted the tile declaration, read index byte-pairs as 16-bit texels (g = 2·idx mod 32 dominates → green mottle), and the water's @@ -769,21 +769,21 @@ through a converter or a runtime bswap fixup reads scrambled. (`port/fast3d/gfx_pc.cpp`, M-116): record the `G_IM_FMT` of each SetTexImage into `LoadedTexture.src_fmt` at load time; in `import_texture`, an RGBA/16b tile whose slot's last load came from a CI source routes - through the CI8 palette import — the port equivalent of expand-at-load. + through the CI8 palette import; the port equivalent of expand-at-load. Fires only on a genuine format disagreement, so real RGBA16 textures are untouched. **General rule:** when a GE draw declares a tile format that looks wrong for the data it loaded, suspect the ucode's load-time transform - (ginit.s is missing from this repo — the expansion lives there), not UV, + (ginit.s is missing from this repo; the expansion lives there), not UV, shade, or the combiner. -## D2. The HUD/model "X-mirror" (D114/D116) — RESOLVED: it was an upside-down capture +## D2. The HUD/model "X-mirror" (D114/D116): RESOLVED, it was an upside-down capture **M-33 (finding D168).** There was no mirror. `gfx_opengl_dump_bound_fbo` -(`port/fast3d/gfx_opengl.cpp`) wrote `glReadPixels` output — bottom-row-first, -GL origin — straight into a top-row-first P6 PPM, so **every `GE_PCDUMP` and +(`port/fast3d/gfx_opengl.cpp`) wrote `glReadPixels` output; bottom-row-first, +GL origin; straight into a top-row-first P6 PPM, so **every `GE_PCDUMP` and F12 capture was vertically flipped**. Sessions M-6/M-7/M-8/M-11 kept finding `textRenderGlyph` → GBI → fast3d → `buf_vbo` → GL **each verified -non-mirrored** — because nothing *was* mirrored; they were staring at +non-mirrored**; because nothing *was* mirrored; they were staring at upside-down screenshots of asymmetric content (text, ammo digits, guard skins, the Nintendo logo) and reading "inverted" as "X-mirrored". The developer confirms the game renders correctly on real hardware. @@ -807,16 +807,16 @@ for (s = SP_LEVEL_EGYPT; s >= SP_LEVEL_DAM /* == 0 */; s--) // never ends - Symptom: a silent hang (kernel-heartbeat stall, no crash log) inside a loop over an enum range; the counter holds a huge value in gdb. -- Instances: **D142** — `LEVEL_SOLO_SEQUENCE` in +- Instances: **D142**; `LEVEL_SOLO_SEQUENCE` in `fileGetHighestStageDifficultyCompletedForFolder` froze the SELECT FILE screen. `DIFFICULTY` was already safe (`DIFFICULTY_MULTI = -1`). - Fix: add a never-used negative sentinel enumerator under `#ifdef PORT` - (`SP_LEVEL__PORT_SIGNED = -1`) — forces the type signed, first real + (`SP_LEVEL__PORT_SIGNED = -1`); forces the type signed, first real enumerator stays 0, `sizeof` stays 4, no stored value changes. Audit any `enum` used as a descending / `>= 0` loop counter or in ROM-serialized structs where signedness matters. -## D4. N64 "interrupts off" is not free on PC — it must be a real lock +## D4. N64 "interrupts off" is not free on PC; it must be a real lock `osSetIntMask(OS_IM_NONE) … osSetIntMask(saved)` on N64 makes a region atomic w.r.t. every interrupt (audio, VI, SI). libaudio, the scheduler and @@ -824,7 +824,7 @@ a few others use it as their **only** mutual-exclusion primitive. On PC the "audio interrupt" is a real preemptible thread (`amMain`), so a no-op `osSetIntMask` shim = no mutual exclusion = concurrent linked-list mutation. -- Symptom: hang (spin) inside a list walk that another thread is editing — +- Symptom: hang (spin) inside a list walk that another thread is editing; e.g. **D147**: `alEvtqPostEvent` (main thread, via `sndPlaySfx` on a door close) vs `sndRemoveEvents` (`amMain`) on the same `ALEventQueue`. - Fix: `osSetIntMask` → one process-wide **recursive** mutex. @@ -832,7 +832,7 @@ a few others use it as their **only** mutual-exclusion primitive. On PC the what every paired restore passes, so `OS_IM_ALL` releases; other specific masks (`OS_IM_VI`) pass through. Safe because the decomp never blocks while holding `OS_IM_NONE` (N64 contract). -- **D152 — the recursive-mutex model is fragile; it CAN deadlock.** The +- **D152; the recursive-mutex model is fragile; it CAN deadlock.** The "decomp never blocks while holding `OS_IM_NONE`" assumption fails on heavy `ALEventQueue` paths: libaudio has unbalanced / early-`return` `osSetIntMask` calls, and a transient thread can acquire `OS_IM_NONE` @@ -845,23 +845,23 @@ a few others use it as their **only** mutual-exclusion primitive. On PC the mutex + condvar; a waiter blocked > 2 s **steals** the section and logs the stale owner + the stealing caller's return address. Self-heals any leak (worst case ~2 s audio hiccup). Proper narrow fix (dedicated - `ALEventQueue` lock) still owed — do it once the steal-log names the + `ALEventQueue` lock) still owed; do it once the steal-log names the leaking call site. §F D152. - **D152 addendum (M-31 static audit).** Every `osSetIntMask(OS_IM_NONE)` in the *compiled* audio code was audited: `event.c` (`alEvtqNextEvent`, - `alEvtqPostEvent` — its lone early `return` at `event.c:86` **does** restore - first —, `alEvtqFlush`, `alEvtqFlushType`), `csplayer.c` `__CSPRepostEvent`, + `alEvtqPostEvent`; its lone early `return` at `event.c:86` **does** restore + first; `alEvtqFlush`, `alEvtqFlushType` likewise), `csplayer.c` `__CSPRepostEvent`, `synaddplayer.c` `alSynAddPlayer`, `snd.c` `sndRemoveEvents` / `sndSetupSound` - / `sndDeactivateAllSfxByFlag`. **All balanced on every path** — the §F guess + / `sndDeactivateAllSfxByFlag`. **All balanced on every path**; the §F guess "libaudio has unbalanced early-return mask paths" is *not* borne out. Two real problems remain and were fixed `#ifdef PORT`: (1) `sndSetSfxSlotVolume` (`snd.c`) walks the live `ALSoundState` list and - posts to the shared `ALEventQueue` **without** holding `OS_IM_NONE` — unlike + posts to the shared `ALEventQueue` **without** holding `OS_IM_NONE`; unlike its structural twin `sndDeactivateAllSfxByFlag`, which does. On PC that is an unguarded walk racing `amMain` *and* one lock acquire/release per matching sound; during the mission-failed fade (`sndSetScalerApplyVolumeAllSfxSlot` → `sndApplyVolumeAllSfxSlot` → this, per frame) that is a per-frame - lock-acquire storm — exactly what the §F dump means by "hammers the queue + lock-acquire storm; exactly what the §F dump means by "hammers the queue hard enough that the lock is left owned." Fix: hold the mask once across the whole walk (nested `alEvtqPostEvent` then hits the recursive fast path); also wrap `sndApplyVolumeAllSfxSlot`'s slot loop so the whole update is one @@ -875,7 +875,7 @@ a few others use it as their **only** mutual-exclusion primitive. On PC the pthread id and `imAcquire` mis-detects recursion. Steal-lock stays as the last-resort backstop for a genuinely unbalanced same-thread path. **Rule:** when two sibling functions walk the same list and post to the same - queue, they must take the same lock — grep for one holding `OS_IM_NONE` and + queue, they must take the same lock; grep for one holding `OS_IM_NONE` and the other not. ## D5. Loop bounds that assume linker adjacency of two file-scope globals @@ -887,7 +887,7 @@ array + ARRAY_COUNT(array)`; mingw/GCC on the PC target **reorders** globals, so `end` can land before the array (loop runs 0–1 times) or far past it (walk off the end). -- Instance: **D164** — `constructor_menu00_legalscreen` (`front.c`) bounds +- Instance: **D164**; `constructor_menu00_legalscreen` (`front.c`) bounds the 12-line legal-screen text loop on `&legalscreen_MRD`, which mingw links 0x60 bytes *before* `legalpage_text_array` → only line 1 renders (== the D76 "disclaimer half-drawn" bug; it was never an image-table @@ -896,26 +896,26 @@ past it (walk off the end). any `for`/`while`/`do` whose end pointer is the address of a *different* symbol than the one being iterated. -## D6. Non-void function with no `return` — latent until `-O2` +## D6. Non-void function with no `return`; latent until `-O2` The decomp has a handful of functions declared to return a value (often -`Gfx *`) whose body ends without a `return` — the IDO/N64 build happened to +`Gfx *`) whose body ends without a `return`; the IDO/N64 build happened to leave the intended value in `$v0` (usually the result of a tail call), and the caller's `x = f(...)` kept working by luck. GCC on the PC target does too **at `-Og`**, but at **`-O2`** the return register is genuinely undefined and the caller reads garbage. When the value is a display-list cursor, the caller then keeps writing the DL from a stale offset and -overwrites whatever the function just emitted — the emitted primitive +overwrites whatever the function just emitted; the emitted primitive silently disappears. -- Instance: **D187** — `set_rgba_redirect_generate_microcode()` +- Instance: **D187**; `set_rgba_redirect_generate_microcode()` (`src/game/gunfire.c`), the sole emitter of the HUD ammo-type icon. Invisible under `-Og`; vanished the day release switched to `-O2` (`34885535`). Fix: `return` the tail call under `#ifdef AVOID_UB` (`#else` keeps the N64 body verbatim). -- Instance: **D77** — `sub_GAME_7F0C0BF0()` (`src/game/mp_music.c`), a +- Instance: **D77**; `sub_GAME_7F0C0BF0()` (`src/game/mp_music.c`), a one-line wrapper around `get_mTrack2Vol()` with no `return`. Every - in-level music trigger (`set_missionstate()`'s MISSION_STATE_1/4 cases — + in-level music trigger (`set_missionstate()`'s MISSION_STATE_1/4 cases; i.e. the only path that starts a level's background track; the front-end menu/intro music bypasses this and calls `musicTrack1Play()` directly, which is why menu music worked while level music didn't) feeds this @@ -925,32 +925,32 @@ silently disappears. player keeps processing note-on events (visible in a `GE_AUDIOTRACE` `[MUSICNOTE]` capture with normal-looking key/velocity data) but every note's synthesized volume is scaled by the zeroed track volume, so nothing - reaches the mixed output. A per-note trace alone looks completely healthy - — the tell is a *track/channel-level* gain feed with a missing `return` + reaches the mixed output. A per-note trace alone looks completely healthy; + the tell is a *track/channel-level* gain feed with a missing `return` upstream. This class isn't limited to display-list cursors; audit any `-O2`-only "quietly broken but not crashing" symptom for the same shape. Fix: `return` under `#ifdef AVOID_UB` (`#else` keeps the N64 body). - The one flagged in-source with an explicit comment is `grep -rn "missing a \"return\"" src/` (`gunfire.c`). Others (like D77's) - exist without the banner — suspect this class whenever an `-O2` build + exist without the banner; suspect this class whenever an `-O2` build silently drops or mutes a small on-screen/audio element that an `-Og` build shows/plays correctly. One-line `#ifdef AVOID_UB` fix each. - Not caught headless by a symbol/link check or a default-config framediff (the missing primitive is often small). Needs an `-O2` build + an eyeball or a targeted crop diff. -## D7. `va_list` is an ARRAY type on x86-64 SysV — never take `&` of a by-value `va_list` param +## D7. `va_list` is an ARRAY type on x86-64 SysV; never take `&` of a by-value `va_list` param On Windows x64 `va_list` is a scalar (`char *`), so passing a `va_list` parameter by value and later taking its address (`&args`) to hand a helper a "pointer to the list" happens to work. On the **x86-64 System V ABI** -(Linux/macOS) `va_list` is `__va_list_tag[1]` — an array — so a by-value +(Linux/macOS) `va_list` is `__va_list_tag[1]`, an array, so a by-value `va_list` parameter has already decayed to `__va_list_tag *`. `&args` is then the address of the *local pointer slot*, not the argument list, and a helper doing `va_arg(*args, …)` walks garbage → segfault. Latent at `-Og`, fatal at `-O2`. -- Instance: **D188** — the IDO printf engine `_Printf` +- Instance: **D188**; the IDO printf engine `_Printf` (`src/libultrare/libc/xprintf.c`) passed `&args` to `_Putfld`; crashed at boot on Linux `-O2` via `bossInitMainthreadData` → `sprintf`. Fix: under `#ifdef PORT`, thread a real `va_list *` from the top (`sprintf.c` passes @@ -960,17 +960,17 @@ doing `va_arg(*args, …)` walks garbage → segfault. Latent at `-Og`, fatal at from the outermost `va_start` scope. Never `&` a `va_list` that arrived as a parameter. `va_copy` into a fresh local is the other portable option when the callee must not disturb the caller's position. -- Same "works on MinGW, UB on SysV, exposed at `-O2`" shape as D6 — suspect +- Same "works on MinGW, UB on SysV, exposed at `-O2`" shape as D6; suspect this class for any Linux-only crash whose backtrace runs through `xprintf`/`_Putfld`/`vsnprintf`-alikes. -## D8. Latent fixed-size stack-buffer over/under-runs — fatal only under a stack protector +## D8. Latent fixed-size stack-buffer over/under-runs; fatal only under a stack protector The decomp has a class of local scratch buffers that game code writes a few -slots past (or before) the declared bounds — e.g. a traversal stack whose +slots past (or before) the declared bounds; e.g. a traversal stack whose bail check is looser than its array size, or `&buf[i*3]` with `i` starting at -1. The N64 build and the MinGW/Windows build have **no stack protector**, so -the overrun lands in adjacent stack scratch and is (in practice) harmless — +the overrun lands in adjacent stack scratch and is (in practice) harmless; the Windows build is stable over thousands of frames. **Ubuntu's gcc default is `-fstack-protector-strong`**, which places a canary right after the buffer and turns every such overrun into a fatal `*** stack smashing detected ***` @@ -981,7 +981,7 @@ and turns every such overrun into a fatal `*** stack smashing detected ***` `bondview2.c` `bondviewCalcIntroSwirlCamera` `f32 pointbuf[10]` written `[-3 .. 11]`. - Fix policy: **`-fno-stack-protector` globally** (`CMakeLists.txt`) so Linux - matches the N64 and MinGW builds — the code is byte-matched to the ROM and + matches the N64 and MinGW builds; the code is byte-matched to the ROM and cannot be "fixed" per site without diverging. Where a case is cleanly bounded and the buffer is pure local scratch (never escapes, no ABI role), widening it under `#ifdef PORT` is also fine (`tileStack[64]`). @@ -1000,7 +1000,7 @@ and turns every such overrun into a fatal `*** stack smashing detected ***` - gdb **launch** mode is too slow for timing-dependent faults; gdb **attach** to an already-running process is fine and fast. - Check the [Perfect Dark port](https://github.com/fgsfdsfgs/perfect_dark) for the PD analogue before - writing anything new — same Rare engine family. + writing anything new; same Rare engine family. - Don't re-investigate a closed §F finding or re-derive a format spec that already has a converter. - **The port is NOT frame-deterministic** (D117): `osGetCount()` is @@ -1009,7 +1009,7 @@ and turns every such overrun into a fatal `*** stack smashing detected ***` time elapsed per render), so "frame N" differs 15–40 % between runs. Use `tools_pc/framediff.py ` (structural: 16×12 grid mean-color + non-clear-% + aHash, `--mask X0,Y0,X1,Y1` for the HUD, `--update` to - refresh `tools_pc/golden/`) — NOT an exact compare. A `GE_DETERM=1` + refresh `tools_pc/golden/`); NOT an exact compare. A `GE_DETERM=1` fixed-tick mode was assessed not-narrow (redesigns retrace/tick semantics); design is in §F D117 if someone picks it up, after which `framediff.py --exact` becomes usable. @@ -1019,18 +1019,18 @@ and turns every such overrun into a fatal `*** stack smashing detected ***` busy-wait on that path (frame-rate cap, a lock, an asset load) stops retrace delivery, so every game thread blocked on `osRecvMesg(retraceQ)` stalls with it. On console the CPU never waits on RSP/RDP/VI. A PC frame - cap must therefore drop *presented* frames without sleeping that thread — + cap must therefore drop *presented* frames without sleeping that thread; `Video.FpsCap < 30` is currently just refused (clamped to uncapped) as a stopgap. -- **D24-implications — host-scheduling nondeterminism / fake priority +- **D24-implications; host-scheduling nondeterminism / fake priority semantics:** the pthread kernel does not enforce N64's 0–31 priorities (`osYieldThread` = `Sleep(0)`), so interleavings impossible on console can occur, and frame timing has host jitter. When a flaky timing bug appears (especially Phase 3 audio underruns), reach first for host thread priorities (`SetThreadPriority`: scheduler thread time-critical, tick - normal) and the deferred `GE_DETERM` mode (D117) — not for kernel changes. + normal) and the deferred `GE_DETERM` mode (D117); not for kernel changes. - **A verification tool that shares the code's indexing convention proves - self-consistency, not correctness** (D206 — cost eight sessions). The port + self-consistency, not correctness** (D206; cost eight sessions). The port played every SFX one bank slot too high. Real cause: `struct ALInstrumentAlt_s` (`src/snd.h`) is a cast alias over the on-disk `ALInstrument`, and its `soundArray` member sits at struct offset **12** on @@ -1045,14 +1045,14 @@ and turns every such overrun into a fatal `*** stack smashing detected ***` indexing. Both faithfully reproduced the bug and reported a pass. Two rules: (1) when validating a **mapping** (index → asset, id → record, offset → field), the reference must be produced **independently of the code's own - accessor** — an external rip, a published table, a hand-decoded sample — or + accessor**, an external rip, a published table, a hand-decoded sample, or the test only confirms the code agrees with itself; (2) when a count and an enum disagree by one (261 bank entries vs 262 `SFX_ID` members), suspect a **pointer-width struct-layout shift** in a ROM-serialized struct (the D3x - class) before anything subtler — check every cast-alias struct's member + class) before anything subtler; check every cast-alias struct's member offsets at 32- vs 64-bit. - **`getenv()` is expensive on Windows (a locked, locale-aware linear scan - via msvcrt.dll) — never call it uncached inside a per-frame, per-draw-call, + via msvcrt.dll); never call it uncached inside a per-frame, per-draw-call, or per-item hot path** (D250, M-120). Dozens of `GE_*` debug-probe checks across the codebase already follow the safe pattern (`static int x = -1; if (x < 0) x = getenv("GE_X") != NULL;`), but a @@ -1060,12 +1060,12 @@ and turns every such overrun into a fatal `*** stack smashing detected ***` texture bind) and per-triangle/per-`G_SETCOMBINE` combine-mode checks didn't. A live WPR/xperf CPU trace (Microsoft public symbols resolved via `-symbols`) on a texture-heavy level found `msvcrt.dll!getenv` alone - eating **50% of the hot render thread's total CPU time** — the game's own + eating **50% of the hot render thread's total CPU time**; the game's own code was only 5%. Fixing the caching took the same scene from a hard ~45-50fps ceiling to a rock-solid 60fps, at every resolution tested, with zero other changes. The lesson generalizes: **any new `GE_*` env-gated probe added to a function that runs more than roughly once per frame must - cache its `getenv()` at that call site**, following the existing pattern — + cache its `getenv()` at that call site**, following the existing pattern; don't assume "it's just a debug check" makes it free. `tools_pc/perf.ps1` (sister script to `debug.ps1`) automates this exact trace-and-analyze workflow for the next time a "runs slow but sim/GL-visible-cost checks out @@ -1080,35 +1080,35 @@ and turns every such overrun into a fatal `*** stack smashing detected ***` doubled count and pinned it on converted collision geometry, which M-74 withdrew. Two general rules: (1) grep the timestamped call line (`sndPlaySfx: t=`), not the field; (2) **before calling a rate anomalous, - compare it to the rate the data says is legal** — the "barrage" was ~6x + compare it to the rate the data says is legal**; the "barrage" was ~6x *under* the AK47's own `SoundTriggerRate` ceiling, which one lookup in `assets/obseg/gun/gunWeaponStats.inc.c` would have shown. Corollary: a perfectly monotone index sweep in a trace looks like a broken `random() % n` but is often a deliberate round-robin counter in ground-truth code (e.g. - `male_guard_yelp_counter`, `chraction.c:2454`) — check the selector before + `male_guard_yelp_counter`, `chraction.c:2454`); check the selector before reporting it. - **"Regression vs. steady state" is decided by a build-and-compare, not by reading an old handoff line** (D133). Handoff prose like "the entire intro - renders" is often aspirational — the author's intent, not a measured coverage + renders" is often aspirational; the author's intent, not a measured coverage number. Before bisecting a suspected render regression: `git worktree add` the - commit whose handoff made the claim (COPY `data/`, never junction — see the + commit whose handoff made the claim (COPY `data/`, never junction; see the data-dir-junction-hazard memory), build, capture the *same* `GE_PCDUMP` window, `pixcount.py`. A **pixel-identical** non-clear count on a static 2D screen (e.g. the legal/disclaimer framebuffer, 6677 px here) across two independently-built binaries proves the path is unchanged → not a regression, don't bisect. Clean up the worktree with `git worktree remove --force` (safe only because `data/` was copied). -- **D134 — a dropped `OS_MESG_NOBLOCK` "interrupt" event is a permanent +- **D134; a dropped `OS_MESG_NOBLOCK` "interrupt" event is a permanent stall, and it looks exactly like D117 flakiness.** The port's SP/DP task-done events shared the scheduler's 8-slot `interruptQ` with the 60 Hz VI pacemaker. A gfx task runs the whole frame *synchronously on the sched thread*, so a slow frame lets the pacemaker fill the queue and the done-post is silently discarded → `curRSPTask` never clears → no further frames (`frames=2` heartbeat, retrace queue pinned `valid=8/8 ret=-1`). Rule: - classify every posted event as **droppable** (retrace — N64 drops these too) + classify every posted event as **droppable** (retrace; N64 drops these too) or **must-arrive** (task done, DMA done); a must-arrive event needs guaranteed delivery, and `OS_MESG_BLOCK` is wrong whenever the poster runs - on the queue's only consumer thread (self-deadlock) — drop the oldest + on the queue's only consumer thread (self-deadlock); drop the oldest droppable message instead, and reserve slots in the pacemaker. **Before blaming a hang on D117 / machine load, run it N times and count**: this reproduced 2 of 3 boots on an idle machine and had been written off as @@ -1116,7 +1116,7 @@ and turns every such overrun into a fatal `*** stack smashing detected ***` - **Watch item: `osYieldThread` = `Sleep(0)`.** The one place we fake cooperative behavior. If a level-sweep hot loop misbehaves under host load, this shim is the first thing to inspect. Keep as-is until then. -- **D155 — an unclamped per-frame `deltaFrames` from wall-clock `osGetCount()` +- **D155; an unclamped per-frame `deltaFrames` from wall-clock `osGetCount()` spirals the sim into a "hang".** The N64 was VI-locked so `waitForNextFrame()` never returned more than ~2 elapsed frames. On the port `osGetCount()` is wall-clock (D117), so any real-time stall (asset load at a stage/cutscene @@ -1129,10 +1129,10 @@ and turns every such overrun into a fatal `*** stack smashing detected ***` AnimFrame` under `chrTick`/`playerTick`/`lvlRender` across every dump. Fix: clamp `nextFrameTime` under `#ifdef PORT` in `waitForNextFrame()` (`FRAMETIMING_PORT_MAX_CATCHUP` = 6). Any other place the port lets an N64 - "number of elapsed ticks" value run unbounded is the same latent trap — grep + "number of elapsed ticks" value run unbounded is the same latent trap; grep for `g_ClockTimer` / `speedgraphframes` / `lvupdate*` consumers that loop. §F **D155**. -- **D156 — a NaN / blown-up `f32` frame or speed drives an unbounded +- **D156; a NaN / blown-up `f32` frame or speed drives an unbounded frame-stepping loop → hang.** `modelSetAnimFrame2WithChrStuff`'s `while (1)` (`model.c:3131`) walks one anim frame at a time from `framea` to `frameb`; `frameb` comes from `modelTickAnim`'s `frame += playspeed * speed` per tick. @@ -1144,19 +1144,19 @@ and turns every such overrun into a fatal `*** stack smashing detected ***` `#ifdef PORT` finiteness+magnitude guards (`!(x > -1e6f && x < 1e6f)` also catches NaN) on `frameb` before the loop and on `frame`/`frame2` before they reach `model` state. **When an N64 float pipeline feeds a loop bound or an - array index on PC, guard it** — the console's fixed timestep + bounded + array index on PC, guard it**; the console's fixed timestep + bounded anim/physics data never produced the degenerate value, so the decomp never checks. Suspect a misaligned `Model`/struct field (D100/D140 pun family) as the NaN source before blaming the data. §F **D156**. -- **D247 — a visual "defect" report can be faithful, unmodified N64 behaviour; +- **D247; a visual "defect" report can be faithful, unmodified N64 behaviour; check the decomp's own constants before assuming a port bug.** User reported black bars top/bottom of the screen. The obvious port-side suspects (a present/letterbox path, a framebuffer-vs-window size mismatch) were all - clean — no code anywhere sets `G_ASPECT_MODE_EXT` for GE, so fast3d never + clean; no code anywhere sets `G_ASPECT_MODE_EXT` for GE, so fast3d never letterboxes. The real cause: `bondviewGetCurrentPlayerViewportHeight()` (`bondview2.c:7858`) returns `VIEWPORT_HEIGHT_DEFAULT` = 220 of 240 NTSC scanlines (`fr.h:34`, ~92% fill) whenever `PLAYER_OPTION_SCREEN` is - `SCREEN_SIZE_FULLSCREEN` — the decomp's own default value — unmodified, + `SCREEN_SIZE_FULLSCREEN`, the decomp's own default value, unmodified, no `#ifdef PORT` in sight. `VIEWPORT_HEIGHT_FULLSCREEN` (304, no inset) is a trap name: it's only reachable via the unrelated `cameraBufferToggle` branch, not normal solo play. This is GoldenEye's real TV-safe-area @@ -1165,9 +1165,9 @@ and turns every such overrun into a fatal `*** stack smashing detected ***` explanation by reproducing identically on a fresh EEPROM. **Lesson:** before chasing a `port/` fix for a rendering "bug," grep the reported geometry (viewport height/width, screen offset, etc.) back to its decomp - origin — a visually surprising but *exactly reproduced* N64 quirk isn't a + origin; a visually surprising but *exactly reproduced* N64 quirk isn't a port defect, and the fix is to close the finding, not to write code. -- **D195 — a targeted decomp-side fix (D72) blanket-disabled a whole PC-side +- **D195; a targeted decomp-side fix (D72) blanket-disabled a whole PC-side feature instead of gating it on the GBI bit that actually distinguishes the broken case from the working one.** `gfx_sp_vertex()`'s PD-inherited envmap (`G_TEXTURE_GEN`) UV-generation block was removed wholesale to fix @@ -1178,25 +1178,25 @@ and turns every such overrun into a fatal `*** stack smashing detected ***` material (gold/silver weapon skins, chrome trim, reflective glass), all of which DO set the bit. Deleting the feature instead of gating it on `rsp.geometry_mode & G_TEXTURE_GEN` (which the reference PD port already - does correctly) silently broke every one of those materials game-wide — + does correctly) silently broke every one of those materials game-wide; they'd sample a fixed, usually-dark corner texel forever, reading as solid black. **Lesson:** when a PD-inherited block causes a wrong result for one specific draw, check what GBI bit *that draw* fails to gate on before - assuming the whole mechanism doesn't apply to GE — a feature real N64 + assuming the whole mechanism doesn't apply to GE; a feature real N64 hardware supports and GE's own assets exercise is not "PD-only" just because the first investigation happened to hit it on a draw that shouldn't have taken that path. §F **D195**. -- **D228 — when two decode paths exist for the same underlying format, +- **D228; when two decode paths exist for the same underlying format, check them against each other for a swapped byte order, not just against the spec.** `palette_to_rgba32`'s `G_TT_IA16` branch (CI texture whose palette is intensity-alpha, not RGBA16) read `intensity = palentry & 0xff`, - `alpha = palentry >> 8` — exactly backwards relative to the *already + `alpha = palentry >> 8`; exactly backwards relative to the *already correct* direct-IA16 decode (`import_texture_ia16`) a few lines above it in the same file, which reads `intensity = addr[2*i]` (first/high byte), `alpha = addr[2*i+1]` (second/low byte), matching what `gfx_dp_load_tlut`'s `PD_BE16` byte-swap actually produces. A dim, fully-opaque texel (e.g. intensity=0x58, alpha=0xff) decoded backwards as bright and - mostly-transparent — read as "white missing-texture patches" (AK47 and + mostly-transparent; read as "white missing-texture patches" (AK47 and other metallic weapon materials, whose grayscale-metal-plus-opacity look is a natural fit for IA16 palettes). Two importers for the same source format existing side by side in one file is exactly the situation where diff --git a/tools_pc/dist/README.md.in b/tools_pc/dist/README.md.in index 1f48b11c..f01dcfd6 100644 --- a/tools_pc/dist/README.md.in +++ b/tools_pc/dist/README.md.in @@ -1,16 +1,17 @@ -# GoldenEye 007 PC Port — @VERSION@ (pre-release) +# GoldenEye 007 PC Port - @VERSION@ A native PC port of the original Nintendo 64 *GoldenEye 007*, compiled from the [GoldenEye 007 decompilation](https://github.com/n64decomp/007). -> **Pre-release — playable, with known rough edges.** +> **Playable, with known rough edges.** > The full single-player campaign runs at a steady 60 fps with no known > crashes, and in-level music + sound effects play throughout. Known issues: -> cutscenes glitch frequently (the Dam level-end in particular), a few music -> tracks have wrong-sounding instruments, some front-end 3D models are broken -> (the spinning Nintendo logo, the MISSION COMPLETE / mode-select models), and -> assorted cosmetic rendering defects remain. This cut is for playtesting — -> report what you find. +> cutscenes still glitch, mostly with James Bond (the Dam level-end in +> particular), a few music tracks have wrong-sounding instruments, some +> front-end 3D models are broken (the spinning Nintendo logo, the MISSION +> COMPLETE / mode-select models), and assorted cosmetic rendering defects +> remain. It's an early public cut: report what you find, feedback is very +> welcome. Project & source: @@ -30,8 +31,8 @@ NTSC-U (US): | PAL (EU) | `ge007.pal-final.z64` | `167c3c433dec1f1eb921736f7d53fac8cb45ee31` | | NTSC-J (JP) | `ge007.jpn-final.z64` | `2a5dade32f7fad6c73c659d2026994632c1b3174` | -US is the best-tested. The asset step below detects the region from the ROM's -SHA-1, so any of the three works as-is. +This release supports the **NTSC-U (US)** ROM. PAL and JP ROMs are recognised +by their SHA-1 but not yet supported; they are on the roadmap. @DEPS@ ## 2. Set up (one time) @@ -39,26 +40,22 @@ SHA-1, so any of the three works as-is. 1. Unzip this folder anywhere. 2. Make a `data` folder next to `@EXE@`. 3. Copy your ROM into it, named exactly: `data/ge007.ntsc-final.z64` -4. Prepare the ROM-derived data (needs Python 3.8+ on your PATH): - - ``` - python3 prepare-assets/prepare-assets.py - ``` - - (On Windows, `python prepare-assets\prepare-assets.py`.) - - This reads your ROM and writes `data/pcmodels-ntsc-final/` and - `data/pccg-ntsc-final/` next to the executable. It uses only the Python - standard library — no compiler or N64 toolchain — and takes a few seconds. - Run it again only if you replace the ROM. +4. That's all the setup. On first launch the game detects the ROM and + generates the derived data (`data/pcmodels-ntsc-final/` and + `data/pccg-ntsc-final/`) automatically; that first run takes a few extra + seconds. No Python, no compiler, no N64 toolchain: nothing to install. + If you replace the ROM later, delete those two folders and the game will + regenerate them on the next launch. ## 3. Run Run `@EXE@` **from this folder**. Configuration is written to `ge007.ini` on first run. -> If the game exits immediately reporting missing data, re-run step 2.4 above -> and check it printed `ALL CHECKS PASSED` for both passes. +> If the game exits immediately reporting missing data, run the generator +> manually from this folder (`prepare-assets/ge007-convert`; on Windows, +> `prepare-assets\ge007-convert.exe`). On success it prints one line per pass +> and ends with `prepare-assets: done.` ## 4. Controls (defaults) @@ -74,7 +71,8 @@ first run. | Start | `Enter` / `Tab` | Start | | Options overlay | `F10` | Select (D-pad/stick + A/B navigate, Start closes) | -Mouse sensitivity and Y-inversion are in the `[Input]` section of `ge007.ini`. +Mouse sensitivity (master and aim-mode), Y-inversion and the other input +settings are in the `[Input]` section of `ge007.ini`, or in the F10 overlay. @DECK@ ## 5. Legal