Prepare for public release

- README, LICENSE (MIT for the port layer), NOTICE, CONTRIBUTING.
- Restructure docs: internals.md, porting-notes.md, dev-process.md +
  dev/agentic-development.md (the research write-up), dev/ (raw record).
- Replace decomp status-badge CI with a PC-port validate workflow.
- Remove session/agent scaffolding and stale planning docs from the tree.
- Add .gitattributes (normalize line endings to LF); add screenshots.

The pre-2026-08 history is the GoldenEye 007 decompilation by Larry Ficken
(KholdFuzion) and contributors, preserved unchanged. The PC-port work from
2026-08-17 on is mine.
This commit is contained in:
James Dansereau committed 2026-09-01 19:28:59 -04:00
1 parent 294e24cc26
commit 52539d10de
118 files changed
+3995 -8319

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
* text=auto eol=lf
*.png binary
*.jpg binary
*.z64 binary
*.exe binary
*.bin binary
-88
View File
@@ -1,88 +0,0 @@
name: EU-Status
on:
push:
branches: master
paths-ignore: # Don't trigger on files that are updated by the CI or other non-breaking files
- 'tools/**'
- '*.md'
- '.gitignore'
- 'CppProperties.json'
pull_request:
branches: master
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
buildeu:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Bypass Cloudflare for GitHub Action
uses: xiaotianxt/bypass-cloudflare-for-github-action@a87c9ac0348806058904e62e5c8e70d8e37b4a65 # v1.1.1
with:
cf_zone_id: ${{ secrets.CF_ZONE_ID }}
cf_api_token: ${{ secrets.CF_API_TOKEN }}
- name: Install Depedencies
run: |
sudo apt update
sudo apt install binutils-mips-linux-gnu make
wget https://github.com/n64decomp/qemu-irix/releases/download/v2.11-deb/qemu-irix-2.11.0-2169-g32ab296eef_amd64.deb
sudo dpkg -i qemu-irix-2.11.0-2169-g32ab296eef_amd64.deb
- name: Get precompiled recomp to save time
env:
IDO_RECOMP: ${{ secrets.FILE_RECOMP }}
run: |
wget -O recomp.zip $IDO_RECOMP
unzip -u recomp.zip -d tools/ido5.3_recomp
- name: Get US File
env:
FILE_US: ${{ secrets.SECRET_FILE_US }}
run: wget -O baserom.u.z64 $FILE_US
- name: Get EU File
env:
FILE_EU: ${{ secrets.SECRET_FILE_EU }}
run: wget -O baserom.e.z64 $FILE_EU
- name: Extract US Assets
run: ./scripts/extract_baserom.u.sh
- name: Extract EU Assets
run: ./scripts/extract_diff.e.sh
- name: Make EU
run: make all -j IDO_RECOMP=YES VERSION=EU VERBOSE=1
- name: Make report
run: make -C tools/report
- name: Stats
run: python3 tools/ge-stats.py --version eu --report
- name: Upload output files
uses: AXDOOMER/copy_file_to_another_repo_action@e54ae5087212906dac7ef96dee19e31190913257
id: upload
continue-on-error: true
env:
API_TOKEN_GITHUB: ${{ secrets.API_TOKEN_GITHUB }}
API_TOKEN_USER: ${{ secrets.API_TOKEN_USER }}
API_TOKEN_EMAIL: ${{ secrets.API_TOKEN_EMAIL }}
with:
destination_repo: 'kholdfuzion/goldeneyestatus'
user_email: $API_TOKEN_EMAIL
user_name: $API_TOKEN_USER
destination_branch: 'master'
source_file: tools/report/EU.htm
- name: Check upload
if: steps.upload.outcome != 'success'
run: echo "::Upload output files::Failed to upload output files. Report and stats were not updated."
-88
View File
@@ -1,88 +0,0 @@
name: JP-Status
on:
push:
branches: master # Only trigger on master
paths-ignore: # Don't trigger on files that are updated by the CI or other non-breaking files
- 'tools/**'
- '*.md'
- '.gitignore'
- 'CppProperties.json'
pull_request:
branches: master # Only trigger on master
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
buildjp:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Bypass Cloudflare for GitHub Action
uses: xiaotianxt/bypass-cloudflare-for-github-action@a87c9ac0348806058904e62e5c8e70d8e37b4a65 # v1.1.1
with:
cf_zone_id: ${{ secrets.CF_ZONE_ID }}
cf_api_token: ${{ secrets.CF_API_TOKEN }}
- name: Install Depedencies
run: |
sudo apt update
sudo apt install binutils-mips-linux-gnu make
wget https://github.com/n64decomp/qemu-irix/releases/download/v2.11-deb/qemu-irix-2.11.0-2169-g32ab296eef_amd64.deb
sudo dpkg -i qemu-irix-2.11.0-2169-g32ab296eef_amd64.deb
- name: Get precompiled recomp to save time
env:
IDO_RECOMP: ${{ secrets.FILE_RECOMP }}
run: |
wget -O recomp.zip $IDO_RECOMP
unzip -u recomp.zip -d tools/ido5.3_recomp
- name: Get US File
env:
FILE_US: ${{ secrets.SECRET_FILE_US }}
run: wget -O baserom.u.z64 $FILE_US
- name: Get JP File
env:
FILE_JP: ${{ secrets.SECRET_FILE_JP }}
run: wget -O baserom.j.z64 $FILE_JP
- name: Extract US Assets
run: ./scripts/extract_baserom.u.sh
- name: Extract JP Assets
run: ./scripts/extract_diff.j.sh
- name: Make JP
run: make all -j IDO_RECOMP=YES VERSION=JP VERBOSE=1
- name: Make report
run: make -C tools/report
- name: Stats
run: python3 tools/ge-stats.py --version jp --report
- name: Upload output files
uses: AXDOOMER/copy_file_to_another_repo_action@e54ae5087212906dac7ef96dee19e31190913257
id: upload
continue-on-error: true
env:
API_TOKEN_GITHUB: ${{ secrets.API_TOKEN_GITHUB }}
API_TOKEN_USER: ${{ secrets.API_TOKEN_USER }}
API_TOKEN_EMAIL: ${{ secrets.API_TOKEN_EMAIL }}
with:
destination_repo: 'kholdfuzion/goldeneyestatus'
user_email: $API_TOKEN_EMAIL
user_name: $API_TOKEN_USER
destination_branch: 'master'
source_file: tools/report/JPN.htm
- name: Check upload
if: steps.upload.outcome != 'success'
run: echo "::Upload output files::Failed to upload output files. Report and stats were not updated."
+43
View File
@@ -0,0 +1,43 @@
name: CI
on:
push:
branches: [ master ]
paths-ignore: [ '**.md', 'docs/**', 'LICENSE', 'NOTICE' ]
pull_request:
workflow_dispatch:
# The full port build needs assets extracted from a copyrighted ROM, which CI
# does not have. This job validates everything that does NOT depend on those
# assets: the PC-port tooling, the generated ROM-asset symbol files, and a
# CMake configure of all three regions (which checks REGION_DEFS, the source
# list, and that SDL2 / zlib / OpenGL are discoverable).
jobs:
validate:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake python3 libsdl2-dev zlib1g-dev libgl1-mesa-dev
- name: Byte-compile Python tooling
run: python3 -m compileall -q scripts tools_pc
- name: Regenerate ROM-asset symbol files
run: |
python3 scripts/gen_romassets.py u
python3 scripts/gen_romassets.py e
python3 scripts/gen_romassets.py j
git diff --stat -- port/src/romassets_u.s
- name: CMake configure (all regions)
run: |
for r in ntsc-final pal-final jpn-final; do
echo "::group::configure $r"
cmake -S . -B "build-$r" -DROMID="$r"
echo "::endgroup::"
done
-85
View File
@@ -1,85 +0,0 @@
name: NTSC-Status
on:
push:
branches: master # Only trigger on master
paths-ignore: # Don't trigger on files that are updated by the CI or other non-breaking files
- 'tools/**'
- '*.md'
- '.gitignore'
- 'CppProperties.json'
pull_request:
branches: master # Only trigger on master
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
buildus:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Bypass Cloudflare for GitHub Action
uses: xiaotianxt/bypass-cloudflare-for-github-action@a87c9ac0348806058904e62e5c8e70d8e37b4a65 # v1.1.1
with:
cf_zone_id: ${{ secrets.CF_ZONE_ID }}
cf_api_token: ${{ secrets.CF_API_TOKEN }}
- name: Install Depedencies
run: |
sudo apt update
sudo apt install binutils-mips-linux-gnu make
wget https://github.com/n64decomp/qemu-irix/releases/download/v2.11-deb/qemu-irix-2.11.0-2169-g32ab296eef_amd64.deb
sudo dpkg -i qemu-irix-2.11.0-2169-g32ab296eef_amd64.deb
- name: Get precompiled recomp to save time
env:
IDO_RECOMP: ${{ secrets.FILE_RECOMP }}
run: |
wget -O recomp.zip $IDO_RECOMP
unzip -u recomp.zip -d tools/ido5.3_recomp
- name: Get US File
env:
FILE_US: ${{ secrets.SECRET_FILE_US }}
run: wget -O baserom.u.z64 $FILE_US
- name: Extract US Assets
run: ./scripts/extract_baserom.u.sh
- name: Make US
run: make all -j IDO_RECOMP=YES VERSION=US VERBOSE=1
- name: Make report
run: make -C tools/report
- name: Stats
run: python3 tools/ge-stats.py --version us --report
- name: Upload output files
uses: AXDOOMER/copy_file_to_another_repo_action@e54ae5087212906dac7ef96dee19e31190913257
id: upload
continue-on-error: true
env:
API_TOKEN_GITHUB: ${{ secrets.API_TOKEN_GITHUB }}
API_TOKEN_USER: ${{ secrets.API_TOKEN_USER }}
API_TOKEN_EMAIL: ${{ secrets.API_TOKEN_EMAIL }}
with:
destination_repo: 'kholdfuzion/goldeneyestatus'
user_email: $API_TOKEN_EMAIL
user_name: $API_TOKEN_USER
destination_branch: 'master'
source_file: |
tools/report/cursor.cur
tools/report/index.html
tools/report/sfx.ogg
tools/report/background.jpg
- name: Check upload
if: steps.upload.outcome != 'success'
run: echo "::Upload output files::Failed to upload output files. Report and stats were not updated."
+4
View File
@@ -0,0 +1,4 @@
# Canonicalize display names (does not rewrite history).
# Pre-2026-08 history is the GoldenEye 007 decompilation.
Larry Ficken <kholdfuzion@gmail.com> <kholdfuzion@gmail.com>
James Dansereau <106631954+jkdansereau@users.noreply.github.com>
-70
View File
@@ -1,70 +0,0 @@
# AGENTS.md — GoldenEye 007 PC Port
## What this is
- `n64decomp/007`: WIP decompilation of GoldenEye 007 (N64), byte-matches US/EU/JP ROMs.
- Active work: **PC port** modelled on the Perfect Dark PC port (same Rare "Indy" engine family).
- **`docs/PCPortResearch.md` is the reference** — architecture, GE-specific RSP deltas, phased plan (§1–§10), and the Dxx finding log (§F + §H, indexed at the top of §F). Read the section you need; don't linear-read it. Current task lives in `docs/HANDOFF.md`.
- **Starting a fresh session?** Read `docs/HANDOFF.md` — the paste-ready brief with the immediate task, standing procedure, and environment gotchas.
- **Dispatching subagents?** `docs/AGENT-WORKFLOW.md` — task budgets/deadlines, file partitioning, pre-flight, the standard brief template. Every investigation subagent reads `docs/PORT-LEARNINGS.md` (recurring bug classes) first and appends to it.
## Non-negotiables
1. **N64 build untouched.** `Makefile`, `tools/`, `rsp/`, `ld/` belong to the N64 build. Never modify them for the PC port.
2. **Game logic is unmodified.** The decomp's control flow and behavior are ground truth for 1:1 fidelity — never change them. All N64 *hardware* dependencies are satisfied by the `port/` layer; if a game file seems to need a behavioral change, stop — the fix belongs in `port/`. **Narrow exception (ABI/layout only):** the 32→64-bit transition forces a small class of mechanical, semantics-preserving edits that cannot be isolated in `port/` — chiefly pointer-width reconciliation in ROM-serialized structs (a struct with a 32-bit-pointer field misaligns when read as 64-bit). These follow the PD ground-truth pattern (store the embedded address as `u32`, cast to a real pointer at the use site), change no logic or behavior, and are each documented in `docs/PCPortResearch.md` §F/D3x. No other game-code edits are permitted.
3. **Region macros mirror the Makefile.** `CMakeLists.txt` `REGION_DEFS` must match the N64 Makefile's per-region macro set exactly (finding A1). Divergence = silent branch divergence + link failures.
4. **`src/libultrare/Makefile.libultrare` is ground truth** for original-vs-Rare libultra files (finding B3). The PC build compiles: `libultra/audio`, `libultrare/audio` (drvrNew/env/reverb), `libultra/gu`, and `libultrare/io/vitbl.c` only. All other `io/` + `os/` files are excluded and shimmed in `port/src/libultra.c`.
5. **`rsp/graphics/gmain.s` is the RSP ground truth** — the authoritative reference for which GBI commands GE emits (modified fast3d, 1545 lines). We do not run it on PC; `port/fast3d/` replaces it. Use it to validate the software RSP's command decoding and the custom CC/RM modes.
## Critical files
| File | Role |
|---|---|
| `docs/PCPortResearch.md` | Research + plan (§1–§10) + Dxx finding log (§F/§H, indexed at top of §F). Reference, not a linear read. |
| `CMakeLists.txt` | PC build (parallel to the N64 Makefile). Source list + `REGION_DEFS` live here. |
| `port/src/` | Shims: `libultra.c` (OS API), `gesched.c` (scheduler), `n64stubs.c` (boot/TLB/FPU/rmon), `random.c` (PRNG ported verbatim from `random.s`), `ucode.c` (microcode segment markers), `main.c`, `video.c`, … |
| `port/fast3d/` | Software RSP (adapted from the PD port). The main Phase 2 work. |
| `rsp/graphics/gmain.s` | GE's RSP ucode — ground truth for GBI/CC/RM. |
| `PD_PORT_CHECKOUT` | Perfect Dark PC port checkout — **standing reference for this project**: consult it whenever a remaining work item has a PD analogue (same Rare engine family). Port-layer ground truth (`port/fast3d/`, crash/system/video), plus copy candidates: `port/src/preprocess/` (N64→PC asset conversion at load; `filemodel.c` is the D43 near-analogue, same vma 0x5000000) and `mixer.c`/`input.c`/`fs.c` (Phase 3/4 — our stubs were scaffolded to be replaced by these). Caveats: port-layer files only; same family ≠ identical format — validate per field. Full audit: `docs/PCPortResearch.md` §2.4. |
## Build
```sh
./build-pc.sh ntsc-final # or pal-final / jpn-final
```
Needs CMake + SDL2 + zlib + OpenGL (MSYS2 packages: see `build-pc.sh` header). ROM goes in `./data/` (not distributed).
## Verification ritual (after any build-affecting change)
1. **Undefined symbols.** Every symbol referenced by the compiled set (see `CMakeLists.txt`: `SRC_GAME`, `SRC_ENGINE`, `SRC_LIBAUDIO`, `SRC_LIBULTRARE_AUDIO`, `SRC_LIBULTRARE_DATA`, `SRC_GU`, `SRC_PORT*`) must be defined exactly once in the compiled set or in `port/`. Symbols that live in EXCLUDED files (`libultra/io/*`, `libultrare/io/*` except `vitbl.c`, `libultra/os/*`, `libultrare/os/*`, `sched.c`, `rmon.c`, `vi.c`, `src/*.s`) must be provided by `port/src/libultra.c`, `gesched.c`, `n64stubs.c`, `random.c`, or `ucode.c`.
2. **Duplicates.** No symbol defined twice across the compiled set (watch `sp_*` stacks, `rmon*`, `os*` shims, segment markers).
3. **Syntax.** Every touched file must parse; once the toolchain is installed, `./build-pc.sh` is the final word.
Run `/linkcheck` for this sweep. Record new findings in `docs/PCPortResearch.md` §F/§H style (next Dxx label after the last used) and add the label to the §F index.
## Phase status
- **Phase 0 (scaffolding):** done, committed.
- **Compile+link milestone:** all ~235 TUs compile and `ge007.x86_64.exe` links clean (`ninja ge007 -k 0`, 236/236). Findings D7–D17 in `docs/PCPortResearch.md` §11; asset stubs in `port/src/assetstubs.c`, fast3d no-ops in `port/src/gfxstub.c` (delete when real fast3d lands).
- **Phase 1 (boot to window):** done — ROM loads/validates/maps at cart base 0x10000000 (`romdata.c`), SDL2 window + GL clear loop (`video.c`), absolute asset symbols from `scripts/gen_romassets.py` → `port/src/romassets_<r>.s` (findings D18–D23).
- **Phase 1.5 (boot to first frame):** DONE — boot chain complete AND real frames render (`[NOTE] frame N rendered in … us`). D34–D42 closed the blockers on top of D31–D33: ANIM_DATA lvalues, music seq-table ABI+endianness, music-heap sizing, libaudio bank-tree re-layout, implicit-declaration prototype shim (D38), Globalimagetable rebasing + true segment size (D39), ModelHitEntry pool sizing (D40), cross-thread GL context release (D41), rsp task-settings toggle (D42). ROM dump verified against the No-Intro good dump (byte-identical, header CRCs re-checked — `tools_pc/romverify.c`).
- **Phase 2 (rendering):** IN PROGRESS. fast3d integrated; the **entire
intro renders** (logos → gun barrel → cast). BUNKER1 (`-level_09`)
**loads, renders recognisably** (textured rooms, skeletal guards) and
**survives a firefight** crash-free. Now pivoted **breadth-first** —
see `docs/PLAN-linear-level-sweep.md` (the plan of record): make
boot→menu→level-select functional, then sweep all 21 solo levels for
load+render+no-crash.
- **Cross-level blocker:** D88.4 — `Usetup*Z` `propDefs` record stream
not byteswapped by `tools_pc/d88_emit.py` (crashes in `setupDoor`).
- **Cosmetic defects are parked** in `docs/GRAPHICS-BACKLOG.md`
(D75 3D model transforms, D76 disclaimer, D77 audio, D114/D116 text
X-mirror). Below crash/level work.
- **Recurring sub-task:** ABI/layout fixes per the D32 procedure
(§H) as new asset types are touched — the classes are catalogued in
`docs/PORT-LEARNINGS.md`.
- Full finding history: `docs/PCPortResearch.md` §F + §H (D31–D121,
indexed at top of §F); session narrative in `docs/HANDOFF-ARCHIVE.md`.
- **Phase 3 (audio + input):** input layer DONE (D118 — `port/src/input.c`, SDL keyboard/mouse/gamepad → N64 pad; polish bugs D118a/b open). Audio remains: libaudio → SDL, copy-and-adapt from PD (`mixer.c` 722 lines vs our 31-line stub; §2.4); decide the ASP strategy (C2: `aspMain*` dummies in `ucode.c`).
- **Phase 4 (saves + polish):** file-backed EEPROM — build on PD's `fs.c` (file-backed save dir); PFS/motor already no-op stubs.
-65
View File
@@ -1,65 +0,0 @@
# CLAUDE.md — GoldenEye 007 PC port
This file auto-loads every session. It is the operating manual. The full
rules are in `AGENTS.md` (imported below); this file adds the two
checklists that were being skipped.
@AGENTS.md
## Doc load order — read deliberately, not everything
Context is scarce. Load by tier; do not blind-read whole files.
- **Tier 0 — always (auto-loaded):** `CLAUDE.md`, `AGENTS.md`. Nothing
else auto-loads.
- **Tier 1 — every session start, read fully (both are short):**
`docs/HANDOFF.md` (current state + next task + environment),
`docs/PORT-LEARNINGS.md` (recurring bug classes).
- **Tier 2 — on demand only, do NOT read start-to-finish:**
- `docs/PCPortResearch.md` §1–§10 — architecture / RSP deltas / phased
plan. Read the section you need.
- `docs/PCPortResearch.md` §F + §H — the Dxx finding log. **Jump to a
specific `Dxx` via the index at the top of §F.** Never linear-read.
- `docs/HANDOFF-ARCHIVE.md` — prior-session narrative (M-2…). Only when
tracing the history of one fix.
- `docs/AGENT-WORKFLOW.md` — before dispatching a subagent (also Tier 1
if you may dispatch this session).
- `docs/PLAN-linear-level-sweep.md` — the active plan of record.
## Session preflight — do this in your FIRST few tool calls, before acting
Having a concrete "next step" from the handoff is NOT license to skip this.
1. Read the Tier 1 docs: `docs/HANDOFF.md` (immediate task, standing
procedure, environment gotchas) and `docs/PORT-LEARNINGS.md`
(recurring bug classes — you will re-derive catalogued bugs if you don't).
2. Skim `docs/PLAN-linear-level-sweep.md` — the active plan of record.
3. If you may dispatch a subagent this session, read `docs/AGENT-WORKFLOW.md`
NOW — not at dispatch time.
## Dispatch preflight — EVERY Agent/Task spawn, no exceptions
A subagent brief is INVALID and must not be sent unless it contains all of:
- **FILES YOU MAY TOUCH** — disjoint from every other running agent.
- **BUDGET** — `<= N build->run->inspect cycles` or `~M min`. Tight for
cheap-class bugs (truncations, off-by-ones); more room for structural ones
(matrix handedness, format specs). Escalate the default as the project hardens.
- **ON EXPIRY** — stop, revert temp probes, write up what you have in
`docs/PCPortResearch.md` §F with an explicit confidence rating. A good
write-up of a half-solved bug is a deliverable, not a failure.
- **KNOWN-GOOD / RULED OUT**, **CONSTRAINTS** (no game-logic changes;
ABI/layout/format only; `#ifdef PORT`; documented in §F), **REPORT** shape.
- Instruction to read `docs/PORT-LEARNINGS.md` first and append any
generalisable quirk to it.
Full template: `docs/AGENT-WORKFLOW.md` -> "Standard investigation-subagent
brief template".
## Verification cadence (integrator / overseer)
- **Per merged patch:** single-frame `GE_PCDUMP` capture diffed against the
committed golden baseline. Cheap. Do NOT gate every patch on a long soak.
- **Per batch / integration checkpoint:** one 60s `-level_09` crash-free run.
- Subagents on small disjoint subsystems (input, audio, saves, converters)
don't run the game — the overseer verifies.
+7 -7
View File
@@ -5,7 +5,7 @@
# game C sources (src/game, src) for a modern host (Windows/Linux/macOS) # game C sources (src/game, src) for a modern host (Windows/Linux/macOS)
# against the PC shims in `port/`. The N64 build is untouched. # against the PC shims in `port/`. The N64 build is untouched.
# #
# See docs/PCPortResearch.md for the architecture and plan. # See docs/internals.md for the architecture and plan.
# Modelled on the Perfect Dark PC port (perfect-dark-pc-port/perfect_dark). # Modelled on the Perfect Dark PC port (perfect-dark-pc-port/perfect_dark).
# #
# Usage: # Usage:
@@ -22,7 +22,7 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
# N64/IDO behaviour the decomp relies on); Clang treats it as a bare forward # N64/IDO behaviour the decomp relies on); Clang treats it as a bare forward
# declaration (no members) and rejects base-member access. So prefer GCC. # declaration (no members) and rejects base-member access. So prefer GCC.
# (GCC does hard-error on the one struct that declares a member with the same # (GCC does hard-error on the one struct that declares a member with the same
# name as an inherited one — CCTVRecord's `pad` — see docs/PCPortResearch.md.) # name as an inherited one — CCTVRecord's `pad` — see docs/internals.md.)
# An explicit -DCMAKE_C_COMPILER=... always wins. # An explicit -DCMAKE_C_COMPILER=... always wins.
if(NOT DEFINED CMAKE_C_COMPILER) if(NOT DEFINED CMAKE_C_COMPILER)
find_program(_GE007_GCC NAMES gcc) find_program(_GE007_GCC NAMES gcc)
@@ -209,7 +209,7 @@ add_compile_options(
) )
# The decomp was written for IDO's leniency. These downgrade hard errors to # The decomp was written for IDO's leniency. These downgrade hard errors to
# warnings so the unmodified game code compiles; each class is a tracked # warnings so the unmodified game code compiles; each class is a tracked
# decomp finding (see docs/PCPortResearch.md): # decomp finding (see docs/internals.md):
# int-conversion: raw 32-bit ROM address constants assigned to pointers # int-conversion: raw 32-bit ROM address constants assigned to pointers
# (WIP decomp; genuine 64-bit hazards to fix upstream as phases progress) # (WIP decomp; genuine 64-bit hazards to fix upstream as phases progress)
# incompatible-pointer-types: Mtxf union vs float[4][4], Gfx array idioms # incompatible-pointer-types: Mtxf union vs float[4][4], Gfx array idioms
@@ -343,7 +343,7 @@ file(GLOB_RECURSE SRC_GAME "${CMAKE_SOURCE_DIR}/src/game/*.c")
# the PD port, which swaps its assembly rzip for a real-zlib-backed rzip_c.c. # the PD port, which swaps its assembly rzip for a real-zlib-backed rzip_c.c.
# The only externally-referenced symbols from these two files are # The only externally-referenced symbols from these two files are
# decompressdata() and rzipGetSomething(); both are provided by # decompressdata() and rzipGetSomething(); both are provided by
# port/src/rzdecomp.c (see the symbol sweep in docs/PCPortResearch.md §F). # port/src/rzdecomp.c (see the symbol sweep in docs/internals.md §F).
list(REMOVE_ITEM SRC_GAME list(REMOVE_ITEM SRC_GAME
"${CMAKE_SOURCE_DIR}/src/game/decompress.c" "${CMAKE_SOURCE_DIR}/src/game/decompress.c"
"${CMAKE_SOURCE_DIR}/src/game/zlib.c") "${CMAKE_SOURCE_DIR}/src/game/zlib.c")
@@ -390,8 +390,8 @@ list(PREPEND SRC_LIBULTRARE_LIBC "${CMAKE_SOURCE_DIR}")
# absolute cart addresses in the generated romassets_<region>.s (see # absolute cart addresses in the generated romassets_<region>.s (see
# scripts/gen_romassets.py), backed by the ROM image that romdata.c maps at # scripts/gen_romassets.py), backed by the ROM image that romdata.c maps at
# 0x10000000. The per-model data itself (chr/*/Model.c etc.) is gitignored # 0x10000000. The per-model data itself (chr/*/Model.c etc.) is gitignored
# ROM-derived and absent from this checkout. See docs/PCPortResearch.md §11. # ROM-derived and absent from this checkout. See docs/internals.md §11.
# D34 (docs/PCPortResearch.md): animationtable_{data,entries}.c are NOT # D34 (docs/internals.md): animationtable_{data,entries}.c are NOT
# compiled on PC. Their arrays exist only so that &ANIM_DATA_x evaluates to # compiled on PC. Their arrays exist only so that &ANIM_DATA_x evaluates to
# the record's offset within the animation_data segment (N64 links it at VMA # the record's offset within the animation_data segment (N64 links it at VMA
# 0); on x86-64 assets/animationtable_data.h redefines them as lvalues at # 0); on x86-64 assets/animationtable_data.h redefines them as lvalues at
@@ -413,7 +413,7 @@ list(PREPEND SRC_ASSETS "${CMAKE_SOURCE_DIR}")
# init.c — provides mainproc() (the game's real main entry) + the thread # init.c — provides mainproc() (the game's real main entry) + the thread
# setup helpers. Its N64-only init() is compiled but NOT called; # setup helpers. Its N64-only init() is compiled but NOT called;
# the N64 symbols init() references are stubbed in # the N64 symbols init() references are stubbed in
# port/src/n64stubs.c (see A3 in docs/PCPortResearch.md). # port/src/n64stubs.c (see A3 in docs/internals.md).
# cfb.c — defines cfb_16 (the framebuffer), referenced by fr.c and game # cfb.c — defines cfb_16 (the framebuffer), referenced by fr.c and game
# code (bondview2.c, front.c). Pure data. # code (bondview2.c, front.c). Pure data.
# stacks.c — thread-stack arrays (sp_boot/sp_rmon/sp_idle/sp_shed/sp_main/ # stacks.c — thread-stack arrays (sp_boot/sp_rmon/sp_idle/sp_shed/sp_main/
+40
View File
@@ -0,0 +1,40 @@
# Contributing
Thanks for your interest. This is a small hobby project; issues and pull
requests are welcome, but please read the ground rules first — they are what
keep the port faithful to the original game.
## Ground rules
1. **The decompilation is not modified for the port.** Everything under `src/`
and `include/` is compiled unmodified. `Makefile`, `tools/`, `rsp/`, `ld/`
belong to the N64 build and are never touched for PC work. If PC code seems
to need a behavioural change in the game, the fix belongs in `port/`.
2. **Narrow ABI exception.** The 32->64-bit transition forces a small class of
mechanical, semantics-preserving edits to ROM-serialized structs (storing an
embedded 32-bit pointer as `u32` and casting at the use site). These are
allowed, must be guarded with `#ifdef PORT`, must change no behaviour, and
must be documented. See `docs/porting-notes.md` for the catalogue of
patterns.
3. **Region macros mirror the Makefile.** `CMakeLists.txt` `REGION_DEFS` must
match the N64 `Makefile`'s per-region macro set exactly.
4. **Verify before you push.** A build-affecting change needs, at minimum, a
clean configure + build for `ntsc-final` and a crash-free run of one level
(`./build-pc/ge007.x86_64 -level_09`).
## Style
- C/C++ formatting follows `.clang-format` and `.editorconfig` in the repo
root. Match the surrounding code.
- Port-layer code is plain C11 / C++17, SDL2 + OpenGL, no extra dependencies.
- Keep commits focused and describe *why*, not just *what*.
## Where to look
- `docs/internals.md` — how the port is structured.
- `docs/porting-notes.md` — the recurring bug classes; check here before
debugging a crash, you are likely looking at a known pattern.
- `docs/dev/` — the full finding log and per-level status.
-237
View File
@@ -1,237 +0,0 @@
{
"configurations": [
{
"name": "MIPS2-DEBUG",
"configurationType": "Debug",
"generator": "Unix Makefiles",
"compilerPath": "",
"compilerSwitches": "-Wab,-r4300_mul -non_shared -Olimit 2000 -G 0 -Xcpluscomm -mips2 -32",
"cStandard": "c89",
"includePath": [
"\\\\wsl.localhost\\Ubuntu\\usr\\include"
/*INCLUDE := -I . -I include -I include/ultra64 -I include/PR -I src -I src/game -I src/inflate*/,
"${workspaceRoot}",
"${workspaceRoot}\\include",
"${workspaceRoot}\\src",
"${workspaceRoot}\\src\\game",
"${workspaceRoot}\\src\\inflate"
],
"intelliSenseMode": "linux-gcc-x86",
"defines": [
"DEBUG",
"TRI4_Ext",
"VERSION_US",
"TARGET_N64",
"OUTCODE=u",
"LANG_US",
"REFRESH_NTSC",
"LEFTOVERDEBUG",
"LEFTOVERSPECTRUM",
"BUGFIX_R0",
"BYTEMATCH"
/* IDO 5.3 Defs below */,
"_MIPS_FPSET=16",
"_MIPS_ISA=2",
"_ABIO32=1",
"_MIPS_SIM=_ABIO32",
"_MIPS_SZINT=32",
"_MIPS_SZLONG=32",
"_MIPS_SZPTR=32",
"__EXTENSIONS__",
"LANGUAGE_C",
"_LANGUAGE_C",
"__INLINE_INTRINSICS",
"sgi",
"__sgi",
"unix",
"mips",
"host_mips",
"__unix",
"__host_mips",
"_SVR4_SOURCE",
"_MODERN_C",
"_SGI_SOURCE",
"__DSO__",
"SYSTYPE_SVR4",
"_SYSTYPE_SVR4",
"_LONGLONG",
"__mips=2",
"_MIPSEB",
"MIPSEB",
"__STDC__=1"
],
"undefines": [
"__cplusplus"
]
},
{
"name": "MIPS2-FINAL US",
"configurationType": "Release",
"generator": "Unix Makefiles",
"cStandard": "c89",
"includePath": [
"\\\\wsl.localhost\\Ubuntu\\usr\\include",
"${workspaceRoot}",
"${workspaceRoot}\\include",
"${workspaceRoot}\\src",
"${workspaceRoot}\\src\\game",
"${workspaceRoot}\\src\\inflate"
],
"intelliSenseMode": "linux-gcc-x86",
"defines": [
"TRI4_Ext",
"VERSION_US",
"TARGET_N64",
"OUTCODE=u",
"LANG_US",
"REFRESH_NTSC",
"LEFTOVERDEBUG",
"LEFTOVERSPECTRUM",
"BUGFIX_R0",
"BYTEMATCH"
/* IDO 5.3 Defs below */,
"_MIPS_FPSET=16",
"_MIPS_ISA=2",
"_ABIO32=1",
"_MIPS_SIM=_ABIO32",
"_MIPS_SZINT=32",
"_MIPS_SZLONG=32",
"_MIPS_SZPTR=32",
"__EXTENSIONS__",
"LANGUAGE_C",
"_LANGUAGE_C",
"__INLINE_INTRINSICS",
"sgi",
"__sgi",
"unix",
"mips",
"host_mips",
"__unix",
"__host_mips",
"_SVR4_SOURCE",
"_MODERN_C",
"_SGI_SOURCE",
"__DSO__",
"SYSTYPE_SVR4",
"_SYSTYPE_SVR4",
"_LONGLONG",
"__mips=2",
"_MIPSEB",
"MIPSEB",
"__STDC__=1"
],
"undefines": [
"__cplusplus"
]
},
{
"name": "LibUltra-Debug",
"configurationType": "Release",
"generator": "Unix Makefiles",
"cStandard": "c89",
"includePath": [
"\\\\wsl.localhost\\Ubuntu\\usr\\include"
/*include -I include/PR -I src/libultra*/,
"${workspaceRoot}",
"${workspaceRoot}\\include",
"${workspaceRoot}\\include\\PR",
"${workspaceRoot}\\src\\libultrare",
"${workspaceRoot}\\src\\libultra"
],
"intelliSenseMode": "linux-gcc-x86",
"defines": [
"_DEBUG",
"TRI4_Ext",
"VERSION_US",
"TARGET_N64"
/* IDO 5.3 Defs below */,
"_MIPS_FPSET=16",
"_MIPS_ISA=2",
"_ABIO32=1",
"_MIPS_SIM=_ABIO32",
"_MIPS_SZINT=32",
"_MIPS_SZLONG=32",
"_MIPS_SZPTR=32",
"__EXTENSIONS__",
"LANGUAGE_C",
"_LANGUAGE_C",
"__INLINE_INTRINSICS",
"sgi",
"__sgi",
"unix",
"mips",
"host_mips",
"__unix",
"__host_mips",
"_SVR4_SOURCE",
"_MODERN_C",
"_SGI_SOURCE",
"__DSO__",
"SYSTYPE_SVR4",
"_SYSTYPE_SVR4",
"_LONGLONG",
"__mips=2",
"_MIPSEB",
"MIPSEB",
"__STDC__=1"
],
"undefines": [
"__cplusplus"
]
},
{
"name": "LibUltra-FINAL",
"configurationType": "Release",
"generator": "Unix Makefiles",
"cStandard": "c89",
"includePath": [
"\\\\wsl.localhost\\Ubuntu\\usr\\include",
"${workspaceRoot}",
"${workspaceRoot}\\include",
"${workspaceRoot}\\include\\PR",
"${workspaceRoot}\\src\\libultrare",
"${workspaceRoot}\\src\\libultra"
],
"intelliSenseMode": "linux-gcc-x86",
"defines": [
"_FINALROM",
"TRI4_Ext",
"VERSION_US",
"TARGET_N64"
/* IDO 5.3 Defs below */,
"_MIPS_FPSET=16",
"_MIPS_ISA=2",
"_ABIO32=1",
"_MIPS_SIM=_ABIO32",
"_MIPS_SZINT=32",
"_MIPS_SZLONG=32",
"_MIPS_SZPTR=32",
"__EXTENSIONS__",
"LANGUAGE_C",
"_LANGUAGE_C",
"__INLINE_INTRINSICS",
"sgi",
"__sgi",
"unix",
"mips",
"host_mips",
"__unix",
"__host_mips",
"_SVR4_SOURCE",
"_MODERN_C",
"_SGI_SOURCE",
"__DSO__",
"SYSTYPE_SVR4",
"_SYSTYPE_SVR4",
"_LONGLONG",
"__mips=2",
"_MIPSEB",
"MIPSEB",
"__STDC__=1"
],
"undefines": [
"__cplusplus"
]
}
]
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 James Dansereau
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+48
View File
@@ -0,0 +1,48 @@
GoldenEye 007 PC port
=====================
WHAT IS AND ISN'T COVERED BY THIS PROJECT'S LICENSE
--------------------------------------------------
The MIT License in the LICENSE file covers the original work in this
repository:
* the port layer port/
* the PC build system CMakeLists.txt, cmake/, build-pc.sh
* PC-port tooling tools_pc/
* documentation docs/, README.md
Third-party code vendored into the port layer keeps its own license:
* port/fast3d/ - software RSP adapted from the Perfect Dark port /
libultraship / sm64-port lineage; see port/fast3d/LICENSE.txt.
* port/fast3d/glad/ - generated OpenGL loader (glad), public domain / MIT.
It does NOT cover:
* src/, include/, assets/, Makefile, tools/, rsp/, ld/, and the other
files inherited from the GoldenEye 007 decompilation
(https://github.com/n64decomp/007). The upstream project is published
without an explicit license; those files are included here on the same
basis, unmodified except where a PC-port ABI fix is marked `#ifdef PORT`
and documented in docs/dev/findings.md. This repository is a downstream
fork of that decompilation with a PC port added on top.
As is standard for console decompilation projects, some game data is
present as re-expressed source (animation tables, display lists, data
tables) inherited verbatim from the upstream repository. The bulk game
assets -- textures, audio, models, level geometry, in-game text -- are
NOT included and are extracted from your own ROM at build time.
NO NINTENDO / RARE CONTENT IS DISTRIBUTED HERE
---------------------------------------------
This repository contains NO copyrighted ROM data, game assets (textures,
models, levels, audio, text), or Nintendo/Rare code. Building a playable
executable requires a GoldenEye 007 Nintendo 64 ROM that you legally own;
the build extracts the required assets from that ROM on your own machine.
GoldenEye 007 is a trademark of its respective rights holders. This project
is a non-commercial, fan-made preservation and porting effort, and is not
affiliated with or endorsed by Nintendo, Rare, MGM, EON Productions, Danjaq,
or any other rights holder.
-68
View File
@@ -1,68 +0,0 @@
# GoldenEye 007 — PC Port
A work-in-progress port of [GoldenEye 007](https://en.wikipedia.org/wiki/GoldenEye_007)
to modern platforms (Windows / Linux / macOS), built on the decompilation in
this repository.
It follows the approach of the
[Perfect Dark PC port](https://github.com/perfect-dark-pc-port/perfect_dark),
which ports the same Rare "Indy" engine (a later revision).
> **Status: Phase 2 (rendering) in progress.** The game boots on PC, plays the
> intro music, and renders the entire intro (logo → gun barrel → cast);
> the current blocker is first stage load (BG-file endianness, finding D69).
> Current session state and immediate tasks: **[docs/HANDOFF.md](docs/HANDOFF.md)**.
## Read this first
* **[docs/PCPortResearch.md](docs/PCPortResearch.md)** — the preliminary
research: how the PD port works, what GE-specific work remains (the `G_SETTEX`
RSP command, custom color-combiner/render modes), the hardware surfaces that
must be shimmed, and the phased plan. **Start here.**
* **[port/README.md](port/README.md)** — the port layer layout and how it fits
together.
## How it works (one paragraph)
The N64 game code (`src/game`, `src`) is compiled **unmodified** for the host.
The **RSP (the N64's 3D coprocessor) is emulated in software**
(`port/fast3d/`): it interprets the display list the game builds and emits
OpenGL calls, bypassing the RDP. All other N64 hardware (VI, AI, PI, SI, OS
threads/timers) is shimmed in `port/src/libultra.c` and friends. Saves
(Memory Pak / EEPROM) are backed by files.
## Layout
```
CMakeLists.txt # PC build (parallel to the N64 Makefile)
cmake/ # CMake helper modules
build-pc.sh # configure + build helper
port/
├── fast3d/ # software RSP (adapted from the PD port)
├── include/ # port-facing headers
└── src/ # port layer (main, libultra shims, video, audio, input, ...)
docs/PCPortResearch.md
```
The N64 build (`Makefile`, `tools/`, `rsp/`, etc.) is **untouched**.
## Building
```sh
./build-pc.sh ntsc-final # or pal-final / jpn-final
```
Then put your ROM in `./data/` and run the binary from the repo root.
(The ROM is not distributed; you must own the game.)
## Phased plan (summary)
| Phase | Goal | Status |
|-------|------|--------|
| 0 | Scaffolding: build system + port-layer interfaces | done |
| 1 (+1.5) | Boot to window / first frame: ROM map, OS shims, ABI reconciliation | done |
| 2 | Rendering: fast3d software RSP, asset conversion/fixups | **in progress** (full intro renders; stage load = D69) |
| 3 | Audio + input (SDL device, mixer, gamepad) | not started |
| 4 | Saves + polish (file-backed EEPROM, widescreen, config) | not started |
Details and risks: [docs/PCPortResearch.md §8–9](docs/PCPortResearch.md).
+320
View File
@@ -0,0 +1,320 @@
# GoldenEye 007 — PC port
A work-in-progress native port of **GoldenEye 007** (Rare, 1997) to modern
desktop platforms, built on top of the
[GoldenEye decompilation](https://github.com/n64decomp/007).
It follows the architecture of the
[Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark) — the same
Rare "Indy" engine family, one hardware generation apart. The unmodified game C
sources are compiled for the host; the N64's Reality Signal Processor is
emulated in software; every other hardware surface (video, audio, input,
timers, save storage) is shimmed in a dedicated `port/` layer.
> **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).
<p align="center">
<img src="docs/img/archives-1.png" width="49%" alt="Archives level rendering in the port">
<img src="docs/img/bunker1-1.png" width="49%" alt="Bunker 1 rendering in the port">
<br>
<img src="docs/img/archives-2.png" width="32%" alt="Guards in the Archives level">
<img src="docs/img/bunker1-2.png" width="32%" alt="Bunker 1 with HUD">
<img src="docs/img/bunker1-3.png" width="32%" alt="Bunker 1 storage room">
<br><em>In-engine captures &mdash; Archives and Bunker&nbsp;1 running in the port.</em>
</p>
## Background
This port is primarily a **research project on agentic software development** —
how far coding agents can be driven, by one person, through a large and
unfamiliar low-level codebase (~230 translation units of unmodified
big-endian MIPS game code, made to run on a 64-bit desktop).
It used two agents:
- a **local open-weight model** —
`unsloth/Qwen3.8-27B-GGUF:UD-Q4_K_XL` on a single **RTX 5090**, driven mainly
through the **[pi](https://pi.dev/)** coding agent (Unsloth Desktop was also
trialed) — which did the groundwork: the CMake build, the boot chain, the
software-RSP integration, the offline asset-conversion pipeline, and the
first rendered frames;
- **Claude** (via **Claude Code** — mostly **Sonnet 5**, with **Opus 5** as an
escalation tier for the hardest bugs), which joined on 27 Aug 2026 for a
collaborative phase: the 21-level load/render/no-crash sweep, the ABI/layout
finding catalogue, the SDL input layer, and the front-end flow.
The two **handed work back and forth** through shared written artifacts:
```mermaid
flowchart LR
H["human<br/>direction · integration<br/>build + playtest"]
C["Claude<br/>(Claude Code)"]
Q["Qwen 3.8<br/>(pi, local RTX 5090)"]
D[("shared artifacts<br/>finding log · bug-class notes · handoff doc")]
H -->|"scoped task + budget"| C
H -->|"scoped task + budget"| Q
C <-->|read / append| D
Q <-->|read / append| D
C -.->|"hit usage limit ->"| Q
Q -.->|"needs deeper reasoning ->"| C
C -->|"patch + write-up"| H
Q -->|"patch + write-up"| H
```
The handoff document was originally a session-to-session note; it became the
**interface between the two models** — when Claude hit a usage limit
mid-problem, the local model picked the task up from that state and continued.
**By the numbers:** ~2 weeks, one person part-time · 217 commits · 160
root-caused bugs logged · ~30 handoff sessions · `#ifdef PORT` ABI edits in 63
game-source files · ~17k lines of port layer + ~6k lines of Python asset
tooling. By day 4 the full ~230-unit codebase compiled and linked; by day 8
the intro rendered; by **day 13 all 21 solo missions ran crash-free**; by
day 14 the front end was playable end to end. Estimated effort split
(milestone-weighted): **~60% Claude / ~40% local model** — the local model
built the entire foundation (build, boot, RSP wiring, converters).
Full write-up, timeline chart, and an honest "what worked / what didn't":
[`docs/dev/agentic-development.md`](docs/dev/agentic-development.md). The
workflow itself: [`docs/dev-process.md`](docs/dev-process.md).
## Status
**Phase 2 of 4 (rendering).** The port boots, renders, and is playable through
the front end into the early game. It is not finished and it is not stable.
**Working**
- Boot -> Rare/Nintendo logos -> gun-barrel -> cast intro, fully rendered.
- Front end: main menu -> mission select -> difficulty -> briefing -> mission start.
- **All 21 solo missions load, render, and survive an unattended play window
without crashing** (see [`docs/dev/LEVEL-STATUS.md`](docs/dev/LEVEL-STATUS.md)).
- Software RSP (fast3d): textured world geometry, skeletal characters, HUD,
the GE-specific color-combiner / render modes and `G_TRI4`.
- Input: keyboard + mouse (with mode-aware mouse-look) and SDL game
controllers, mapped onto the N64 pad. Tunable via `ge007.ini`.
- File-backed EEPROM saves.
**Not yet working**
- **Audio** — not implemented (Phase 3). The game runs silent.
- Front-end 3D models (spinning Nintendo logo, gun-barrel Bond, cast) are
mispositioned or absent.
- Assorted cosmetic defects (some text layout, a few incomplete asset
conversions) are tracked in
[`docs/dev/GRAPHICS-BACKLOG.md`](docs/dev/GRAPHICS-BACKLOG.md) and parked
below crash/level work.
- Only `x86_64` Windows and Linux are exercised regularly. No macOS/ARM
testing yet; no controller rebinding UI; no widescreen.
## Requirements
You need a **GoldenEye 007 (Nintendo 64) ROM** that you legally own, in
big-endian (`.z64`) format, matching one of:
| Region | ROMID | ROM filename (in `data/`) | SHA-1 |
|--------|-------|---------------------------|-------|
| NTSC-U (US) | `ntsc-final` | `ge007.ntsc-final.z64` | `abe01e4aeb033b6c0836819f549c791b26cfde83` |
| 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.
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
step, too, requires your ROM and is part of [Building](#building).
## Building
Prerequisites: CMake >= 3.16, a C/C++ toolchain, SDL2, zlib, OpenGL, Python 3,
plus the decompilation's own build dependencies (an IRIX MIPS toolchain via
`qemu-irix`, used only for the one-time asset extraction). See
[`docs/building.md`](docs/building.md) for the full walkthrough and the
asset-extraction details.
### Windows (MSYS2)
```sh
# in the MINGW64 shell
pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-SDL2 \
mingw-w64-x86_64-zlib mingw-w64-x86_64-cmake \
mingw-w64-x86_64-python make git
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
```
### Linux
```sh
sudo apt install build-essential cmake python3 libsdl2-dev zlib1g-dev libgl1-mesa-dev
git clone https://github.com/jkdansereau/goldeneye-pc-port.git
cd goldeneye-pc-port
# extract assets (docs/building.md), then:
./build-pc.sh ntsc-final
```
The executable is written to `build-pc/ge007.x86_64` (Windows: `.exe`).
## Running
1. Create a `data/` directory in the repo root.
2. Put your ROM in it, named as in the table above
(e.g. `data/ge007.ntsc-final.z64`).
3. Run the executable **from the repo root**:
`./build-pc/ge007.x86_64`.
Configuration is written to `ge007.ini` on first run.
### Default controls
| Action | Keyboard / mouse | Controller |
|-------------------|-------------------------|-----------------------|
| Move / strafe | `W` `A` `S` `D` / arrows | Left stick |
| Aim / look | Mouse | Right stick |
| Fire (Z) | Left mouse / `LCtrl` | Right trigger |
| Aim mode (R) | Right mouse / `LShift` | Left trigger |
| Use / accept (A) | `Space` / `E` | A / X |
| Reload / cancel (B)| `R` / `F` | B / Y / RB |
| L trigger | `Q` | LB |
| Start | `Enter` / `Tab` | Start |
Mouse sensitivity, Y-inversion and the aim/turn split are tunable in the
`[Input]` section of `ge007.ini`.
## How it works
The R4300 game code in `src/` is compiled **completely unmodified** — the
decompilation's control flow is treated as ground truth. Everything that would
touch N64 hardware is redirected into `port/`:
- **`port/fast3d/`** — a software RSP. It interprets the GBI display list the
game builds each frame and emits OpenGL, bypassing the RDP. Adapted from the
Perfect Dark port and extended for GoldenEye's custom GBI.
- **`port/src/gesched.c`** — replaces the RCP scheduler; instead of poking RSP
registers it drives the software RSP directly.
- **`port/src/libultra.c`** — single-threaded shims for the libultra OS API
(threads, messages, timers, PI/SI/AI/VI) over the host.
- **`port/src/{video,audio,input,fs,romdata,config}.c`** — the SDL2 / OpenGL /
filesystem backends.
The 32->64-bit transition forces a small, catalogued class of mechanical
ABI-only edits to ROM-serialized structs (pointer-width reconciliation); these
change no behaviour and are documented individually.
Where it diverges from the Perfect Dark port: GoldenEye's N64 serialized asset
formats (level setup, models, backgrounds) are converted **offline** by a set
of Python "sidecar" converters in `tools_pc/`, rather than fixed up at load
time. See [`docs/internals.md`](docs/internals.md) and
[`docs/porting-notes.md`](docs/porting-notes.md).
## Project layout
```
CMakeLists.txt PC build (parallel to the decomp's Makefile, which is untouched)
build-pc.sh configure + build helper
src/ include/ the decompilation (game + libultra) - compiled unmodified
port/
fast3d/ software RSP -> OpenGL
src/ port layer (main, OS shims, video, audio, input, fs, ...)
include/ port-facing headers
tools/ Makefile the N64 build + asset extraction (from the decomp; do not modify)
tools_pc/ PC-port helper + analysis scripts
docs/ see below
```
## Documentation
- [`docs/building.md`](docs/building.md) — full build + asset-extraction guide.
- [`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 a
candid 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.
- [`docs/SetupGuide.md`](docs/SetupGuide.md),
[`docs/StructureGuide.md`](docs/StructureGuide.md),
[`docs/StyleGuide.md`](docs/StyleGuide.md) — inherited from the decompilation.
## Credits
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
effort by kholdfuzion, Larry Ficken, 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
the source of the `fast3d` software RSP.
- The [Perfect Dark decompilation](https://github.com/n64decomp/perfect_dark) —
the sibling decomp the PD port is built on.
**Vendored / adapted code**
- `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`),
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
[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
`rabbitizer` (n64decomp).
- [SDL2](https://libsdl.org) and [zlib](https://zlib.net).
**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
[`docs/dev-process.md`](docs/dev-process.md) are adapted from it.
**Tools and models used to develop the port**
- [Qwen 3.8](https://github.com/QwenLM/Qwen) (Alibaba Qwen team);
[Unsloth](https://unsloth.ai) (the GGUF quantisation and Unsloth Desktop).
- [pi](https://pi.dev/) — the local coding-agent harness.
- [Claude / Claude Code](https://claude.com/claude-code) (Anthropic).
## Legal
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 bulk game assets are distributed.** 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
[GoldenEye 007 decompilation](https://github.com/n64decomp/007) and inherits
its contents unmodified (see [`NOTICE`](NOTICE) for what that includes).
- 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).
- No binaries containing game data are distributed. Any build you make is
personal to you.
This project is **not affiliated with, endorsed by, or sponsored by** Nintendo,
Rare, Microsoft, MGM, Danjaq, EON Productions, or any rights holder in
GoldenEye or James Bond. If you are a rights holder with a concern, open an
issue and it will be addressed.
## License
The original work in this repository — the port layer (`port/`), the PC build
system, `tools_pc/`, and the documentation — is released under the MIT License,
see [`LICENSE`](LICENSE). Everything inherited from the upstream decompilation
is covered by [`NOTICE`](NOTICE), not by that license.
+21 -21
View File
@@ -1,21 +1,21 @@
# Font Asset Readme # Font Asset Readme
Font information. Font information.
* first font table controller - small watch text * first font table controller - small watch text
* second font table controller - small watch text * second font table controller - small watch text
* first font table controller - large * first font table controller - large
* second font table controller - large * second font table controller - large
### second controller format ### second controller format
Size of 0x18 for each entry Size of 0x18 for each entry
entry layout: entry layout:
* 0x0: * 0x0:
* 0x4: vertical offset * 0x4: vertical offset
* 0x8: height * 0x8: height
* 0xC: width * 0xC: width
* 0x10: * 0x10:
* 0x14: offset to image (converted to pointer at runtime) * 0x14: offset to image (converted to pointer at runtime)
+41 -41
View File
@@ -1,41 +1,41 @@
# Image Assets # Image Assets
## Overview ## Overview
This directory contains the texture image system for GoldenEye 007. This directory contains the texture image system for GoldenEye 007.
Original image files would have been SGI .rgb files. These would be passed through a converter (rgb2c) to get CI byte arrays. Original image files would have been SGI .rgb files. These would be passed through a converter (rgb2c) to get CI byte arrays.
## Directory Structure ## Directory Structure
- **`split/`** - Individual texture files (2698 images) - **`split/`** - Individual texture files (2698 images)
- Named using IMAGE enum from images.def (e.g., COPYICON.bin, X.bin) - Named using IMAGE enum from images.def (e.g., COPYICON.bin, X.bin)
- Extracted from ROM using ROM offsets from imagelist.u.csv - Extracted from ROM using ROM offsets from imagelist.u.csv
- Raw binary texture data - Raw binary texture data
- **`combined/`** - Combined texture binary - **`combined/`** - Combined texture binary
- `combined.bin` - All textures concatenated in ROM order - `combined.bin` - All textures concatenated in ROM order
- Padded to 16-byte (0x10) boundary - Padded to 16-byte (0x10) boundary
- Size: 3075872 bytes (3075864 data + 8 padding) - Size: 3075872 bytes (3075864 data + 8 padding)
- SHA1 (unpadded): 044fca472bf6ef6691fa02ff1b65ff474d86a9fa - SHA1 (unpadded): 044fca472bf6ef6691fa02ff1b65ff474d86a9fa
## Build System ## Build System
The image build system synchronizes two sources: The image build system synchronizes two sources:
- **`imagelist.u.csv`** (root) - ROM offsets and sizes (ground truth for positioning) - **`imagelist.u.csv`** (root) - ROM offsets and sizes (ground truth for positioning)
- **`assets/images.def`** - Image names and properties (ground truth for naming) - **`assets/images.def`** - Image names and properties (ground truth for naming)
See `scripts/IMAGE_BUILD_SYSTEM.md` for detailed documentation. See `scripts/IMAGE_BUILD_SYSTEM.md` for detailed documentation.
## Quick Commands ## Quick Commands
```bash ```bash
# Extract images from ROM # Extract images from ROM
./scripts/extract_baserom.u.sh ./scripts/extract_baserom.u.sh
# Rebuild combined.bin # Rebuild combined.bin
make assets/images/combined/combined.bin make assets/images/combined/combined.bin
# Full ROM build # Full ROM build
make make
``` ```
+3 -3
View File
@@ -1,3 +1,3 @@
# Music Asset Readme # Music Asset Readme
Music information. Music information.
+65 -65
View File
@@ -1,65 +1,65 @@
# obseg Asset Readme # obseg Asset Readme
These assets consist of model and level information, and all text in the game. These assets consist of model and level information, and all text in the game.
There are eight categories (subfolders) of obseg assets included in source code. Visit each subfolder for a more detailed readme. There are eight categories (subfolders) of obseg assets included in source code. Visit each subfolder for a more detailed readme.
obseg asset categories are: obseg asset categories are:
* `bg` * `bg`
* `brief` * `brief`
* `chr` * `chr`
* `gun` * `gun`
* `prop` * `prop`
* `setup` * `setup`
* `stan` * `stan`
* `text` * `text`
## `bg` Assets ## `bg` Assets
Level background geometry display lists and portal info. Level background geometry display lists and portal info.
## `brief` Assets ## `brief` Assets
Maps text strings into mission briefings. Maps text strings into mission briefings.
## `chr` Assets ## `chr` Assets
Character models display lists. Character models display lists.
## `gun` Assets ## `gun` Assets
On screen gun models display lists. On screen gun models display lists.
## `prop` Assets ## `prop` Assets
Prop and item models display lists. Prop and item models display lists.
## `setup` Assets ## `setup` Assets
Setups contain the mission scripting logic, AI Scripting, object information, Pad and Portal placement, guard paths and nav mesh. (see below for descriptions) Setups contain the mission scripting logic, AI Scripting, object information, Pad and Portal placement, guard paths and nav mesh. (see below for descriptions)
## `stan` Assets ## `stan` Assets
Stand Tiles. Refereed to by Rare Staff as "stans" in GE or simply "Tiles" in PD. (see below for descriptions) Stand Tiles. Refereed to by Rare Staff as "stans" in GE or simply "Tiles" in PD. (see below for descriptions)
## `text` Assets ## `text` Assets
All game text. All game text.
## Misc Assets ## Misc Assets
In the root of this folder is a file listing all of the assets included in the build, with path and filename info. This is used at runtime to resolve the list of available assets. In the root of this folder is a file listing all of the assets included in the build, with path and filename info. This is used at runtime to resolve the list of available assets.
----- -----
# Concepts # Concepts
# Room # Room
A level is divided into rooms. A level is divided into rooms.
# Portal # Portal
Portals allow Bond to see through them to connected rooms that are otherwise culled to save unnecessary processing. Portals allow Bond to see through them to connected rooms that are otherwise culled to save unnecessary processing.
# Pad # Pad
A room contains one or more pads. A room contains one or more pads.
# Path # Path
Guard pathing information between pads. Guard pathing information between pads.
# Nav mesh # Nav mesh
Also known as "sets". Colleciton of pads, to aid in the AI algorithm to find the shortest path to Bond. Also known as "sets". Colleciton of pads, to aid in the AI algorithm to find the shortest path to Bond.
# Stan or Tiles # Stan or Tiles
Defines floor information. Defines floor information.
+127 -127
View File
@@ -1,128 +1,128 @@
# Setup File Readme # Setup File Readme
Known as `setup`. Setups contain the mission scripting logic, AI Scripting, object information, Pad and Portal placement, guard paths and nav mesh, intro camera and cutscene configuration, and the end credits. Known as `setup`. Setups contain the mission scripting logic, AI Scripting, object information, Pad and Portal placement, guard paths and nav mesh, intro camera and cutscene configuration, and the end credits.
Values are stored most significant byte first. Values are stored most significant byte first.
## Definitions ## Definitions
This readme uses the term "section" to refer to a continuous sequence of bytes in the setup file. There are "main" sections which are referenced by the pointer in the header. Seperate from "main" sections are "filler" sections, which lie in between main sections. This usually consists of data referenced by a main section. This readme uses the term "section" to refer to a continuous sequence of bytes in the setup file. There are "main" sections which are referenced by the pointer in the header. Seperate from "main" sections are "filler" sections, which lie in between main sections. This usually consists of data referenced by a main section.
If a section (sequence of bytes) is not referenced by anything in the setup file it will be referred to as "unreferenced". A main section can be unreferenced, such as the `padnames`. More common, a filler section is unreferenced e.g., multiplayer maps empty/unused nav mesh. If a section (sequence of bytes) is not referenced by anything in the setup file it will be referred to as "unreferenced". A main section can be unreferenced, such as the `padnames`. More common, a filler section is unreferenced e.g., multiplayer maps empty/unused nav mesh.
# File Strucutre # File Strucutre
The file always starts with the header, followed by the padlist and pad3dlist sections. If there is a credits data block, it should follow the pad3dlist section. Next are the object and intro sections. The file always starts with the header, followed by the padlist and pad3dlist sections. If there is a credits data block, it should follow the pad3dlist section. Next are the object and intro sections.
The remaining sections and data can appear in various order. The remaining sections and data can appear in various order.
## Header ## Header
The header consists of a data structure of 10 pointers in the following order: The header consists of a data structure of 10 pointers in the following order:
- path table section - path table section
- path link section (path list section) - path link section (path list section)
- intro block - intro block
- object / propdef / objective section - object / propdef / objective section
- path sets section - path sets section
- AI List section - AI List section
- pad list section - pad list section
- pad3d list section - pad3d list section
- pad names section - pad names section
- pad3d names section - pad3d names section
## Pad List Section ## Pad List Section
List of sequence of pads. Does not reference any data outside this section. Always ends with an empty entry List of sequence of pads. Does not reference any data outside this section. Always ends with an empty entry
{ {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, NULL, 0 } { {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, NULL, 0 }
## Pad3d List Section ## Pad3d List Section
List of sequence of pad3ds. Does not reference any data outside this section. Always ends with an empty entry List of sequence of pad3ds. Does not reference any data outside this section. Always ends with an empty entry
{ {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, NULL, 0, {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f} } { {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 0.0f}, NULL, 0, {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f} }
## Object / Objective Section ## Object / Objective Section
Each entry is specified by type `enum PROPDEF_TYPE`. This section lists Each entry is specified by type `enum PROPDEF_TYPE`. This section lists
- guards (and hats) - guards (and hats)
- props (doors, vehicle, tank, monitor, etc) - props (doors, vehicle, tank, monitor, etc)
- collectibles (body armor, ammo, weapons, items in safe, etc) - collectibles (body armor, ammo, weapons, items in safe, etc)
- objectives - objectives
- autogun / drones - autogun / drones
- CCTV / surveilance cameras - CCTV / surveilance cameras
- keys, locked doors, and locks - keys, locked doors, and locks
- glass - glass
- watch menu text - watch menu text
- poison gas (facility) - poison gas (facility)
- meta items (rename, link, switch) - meta items (rename, link, switch)
- meta programming (tag, `PROPDEF_END`) - meta programming (tag, `PROPDEF_END`)
This section does not reference any data outside this section. The section always ends with a propdef entry with type `PROPDEF_END`. This section does not reference any data outside this section. The section always ends with a propdef entry with type `PROPDEF_END`.
## Intro Section ## Intro Section
Each entry is specified by type `enum INTRO_TYPE`. This section describes Each entry is specified by type `enum INTRO_TYPE`. This section describes
- Spawn location - Spawn location
- Starting inventory and ammo amounts - Starting inventory and ammo amounts
- Opening cinemas and camera locations - Opening cinemas and camera locations
- Bond's outfit (cuffs) - Bond's outfit (cuffs)
- Special flag to mark as credits - Special flag to mark as credits
The "credits" type can reference data outside this section, otherwise all data is contained within the section itself. The section always ends with a propdef entry with type `INTROTYPE_END`. The "credits" type can reference data outside this section, otherwise all data is contained within the section itself. The section always ends with a propdef entry with type `INTROTYPE_END`.
### Intro: Animation ### Intro: Animation
Discord comment: Discord comment:
> Animation is used by Bond during the intro swirl process. It's actually a bunch of preset values for each animation. To get around this limitations, some setups such as Silo will instead not set an animation used for the intro swirl, but will have a dedicated AI list that'll just watch for when the intro swirl has triggered, and then trigger an animation that isn't selectable by the intro block animation's range. Sometimes this is used because the intro swirl is quite long, so some manual timing is required via AI list. > Animation is used by Bond during the intro swirl process. It's actually a bunch of preset values for each animation. To get around this limitations, some setups such as Silo will instead not set an animation used for the intro swirl, but will have a dedicated AI list that'll just watch for when the intro swirl has triggered, and then trigger an animation that isn't selectable by the intro block animation's range. Sometimes this is used because the intro swirl is quite long, so some manual timing is required via AI list.
### Intro: Credits ### Intro: Credits
A "credits" type intro entry consists of a single pointer to a block of credits data. This just a list of (global) text ids with alignment and position info. See `CreditsEntry` for struct layout. A "credits" type intro entry consists of a single pointer to a block of credits data. This just a list of (global) text ids with alignment and position info. See `CreditsEntry` for struct layout.
The credits block ends with an empty entry The credits block ends with an empty entry
{ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000 } { 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000 }
## Path Table Section ## Path Table Section
List of sequence of path tables. Elements will reference variables defined immediately prior to this section. Always ends with an empty entry List of sequence of path tables. Elements will reference variables defined immediately prior to this section. Always ends with an empty entry
{ 0xffff, 0xffff, NULL, 0x00000000, 0x00000000 } { 0xffff, 0xffff, NULL, 0x00000000, 0x00000000 }
## Path Link Section ## Path Link Section
List of sequence of path links. Elements will reference variables defined immediately prior to this section. Always ends with an empty entry List of sequence of path links. Elements will reference variables defined immediately prior to this section. Always ends with an empty entry
{ NULL, NULL, 0 } { NULL, NULL, 0 }
## Path Sets Section ## Path Sets Section
List of sequence of path sets. Elements will reference variables defined immediately prior to this section. Always ends with an empty entry List of sequence of path sets. Elements will reference variables defined immediately prior to this section. Always ends with an empty entry
{ NULL, 0x00000000 } { NULL, 0x00000000 }
## Padnames Section ## Padnames Section
List of sequence of pad names. This section is optional. It may also consist of data, but be unreferenced. This section references strings in .rodata. If present, the section always ends with a single `NULL` entry. List of sequence of pad names. This section is optional. It may also consist of data, but be unreferenced. This section references strings in .rodata. If present, the section always ends with a single `NULL` entry.
## Pad3dnames Section ## Pad3dnames Section
List of sequence of pad3d names. This section is optional. It may also consist of data, but be unreferenced. This section references strings in .rodata. If present, the section always ends with a single `NULL` entry. List of sequence of pad3d names. This section is optional. It may also consist of data, but be unreferenced. This section references strings in .rodata. If present, the section always ends with a single `NULL` entry.
## AI List Section ## AI List Section
List of pointers and ids to AI functions. These are used by guards or as part of the level. Elements will reference sequences defined immediately prior to this section; sequences are word aligned, but the last data value in the sequence is a single byte `0x04`. The AI List ends with an empty entry List of pointers and ids to AI functions. These are used by guards or as part of the level. Elements will reference sequences defined immediately prior to this section; sequences are word aligned, but the last data value in the sequence is a single byte `0x04`. The AI List ends with an empty entry
{ NULL, 0x00000000 } { NULL, 0x00000000 }
## Filler Blocks ## Filler Blocks
Occasionally a main section will be empty except the "end of sequence" entry (e.g., multiplayer setups). The area immediately prior to the main section then might have an unreferenced/unused sequence of data. This is usually only one or two bytes, of either `{ 0 }` or `{ -1 }`. The AI List entries block may lead with an unreferenced AI Function, the single byte `0x04` (word aligned / zero padded). Occasionally a main section will be empty except the "end of sequence" entry (e.g., multiplayer setups). The area immediately prior to the main section then might have an unreferenced/unused sequence of data. This is usually only one or two bytes, of either `{ 0 }` or `{ -1 }`. The AI List entries block may lead with an unreferenced AI Function, the single byte `0x04` (word aligned / zero padded).
## .rodata ## .rodata
The .bin file will end with the .rodata section consisting of the strings used in padlist, pad3dlist, padnames, and pad3dnames. The .bin file will end with the .rodata section consisting of the strings used in padlist, pad3dlist, padnames, and pad3dnames.
+82 -82
View File
@@ -1,82 +1,82 @@
# Stand Tile Readme # Stand Tile Readme
Known as Tile, Stand Tile, or stan. This defines floor information where Bond can stand. Known as Tile, Stand Tile, or stan. This defines floor information where Bond can stand.
The model conversion tool rare used has at least 2 modes for output. Release/16bit signed; no debug strings. And debug/32bit float with debug strings. The model conversion tool rare used has at least 2 modes for output. Release/16bit signed; no debug strings. And debug/32bit float with debug strings.
Each stan file is composed of a short header, ends with a standard footer, and in between is a list of tiles and points. Each stan file is composed of a short header, ends with a standard footer, and in between is a list of tiles and points.
Values are stored most significant byte first. Values are stored most significant byte first.
## Header ## Header
The header consists of 4 empty bytes, followed by a 32 bit integer giving the offset to tiles from the start of the file (pointer to first tile). There are optional bytes between the tile offset number and the start of the tiles, usually an empty 4 bytes. The header consists of 4 empty bytes, followed by a 32 bit integer giving the offset to tiles from the start of the file (pointer to first tile). There are optional bytes between the tile offset number and the start of the tiles, usually an empty 4 bytes.
The header is the same for both release and debug. The header is the same for both release and debug.
## Tiles ## Tiles
Tiles consist of a 64 bit tile description, followed by an array of points. Tiles consist of a 64 bit tile description, followed by an array of points.
byte 0-3: a name, id or descriptor. One byte seems to be a "group id" and the other two bytes are some kind of tile id, but exact definition is unknown. byte 0-3: a name, id or descriptor. One byte seems to be a "group id" and the other two bytes are some kind of tile id, but exact definition is unknown.
byte 4: room number byte 4: room number
byte 5 (upper 4 bits): flags or attributes. byte 5 (upper 4 bits): flags or attributes.
byte 5 (lower 4 bits): Red byte 5 (lower 4 bits): Red
byte 6 (upper 4 bits): Green byte 6 (upper 4 bits): Green
byte 6 (lower 4 bits): Blue byte 6 (lower 4 bits): Blue
byte 7 (upper 4 bits): number of points within the tile byte 7 (upper 4 bits): number of points within the tile
The remaining 3 fields generally define the most extreme points within the tile The remaining 3 fields generally define the most extreme points within the tile
byte 7 (lower 4 bits): first point byte 7 (lower 4 bits): first point
byte 8 (upper 4 bits): second point byte 8 (upper 4 bits): second point
byte 8 (lower 4 bits): third point byte 8 (lower 4 bits): third point
### Tile Points ### Tile Points
A tile point is described by four 16-bit fields, giving X,Y,Z coordinates of the point as signed 16-bit integer. A tile point is described by four 16-bit fields, giving X,Y,Z coordinates of the point as signed 16-bit integer.
The remaining field is the "link" The remaining field is the "link"
## Beta Tiles ## Beta Tiles
Beta tiles consist of a 96 bit tile description, followed by an array of points. Beta tiles consist of a 96 bit tile description, followed by an array of points.
byte 0-4: pointer to string giving the tile name byte 0-4: pointer to string giving the tile name
byte 5 (upper 4 bits): flags or attributes. (same as regular tile) byte 5 (upper 4 bits): flags or attributes. (same as regular tile)
byte 5 (lower 4 bits): Red (same as regular tile) byte 5 (lower 4 bits): Red (same as regular tile)
byte 6 (upper 4 bits): Green (same as regular tile) byte 6 (upper 4 bits): Green (same as regular tile)
byte 6 (lower 4 bits): Blue (same as regular tile) byte 6 (lower 4 bits): Blue (same as regular tile)
byte 7-8: unknown 16-bit field. This may be the room. byte 7-8: unknown 16-bit field. This may be the room.
byte 9: number of points within the tile byte 9: number of points within the tile
The remaining 3 fields generally define the most extreme points within the tile (same as regular tile, except one full byte) The remaining 3 fields generally define the most extreme points within the tile (same as regular tile, except one full byte)
byte 10: first point byte 10: first point
byte 11: second point byte 11: second point
byte 12: third point byte 12: third point
### Beta Tile Points ### Beta Tile Points
Beta tile points store the same information as regular tiles, but in a different format. Beta tile points store the same information as regular tiles, but in a different format.
The X,Y,Z coordinates are now 32-bit floats. The X,Y,Z coordinates are now 32-bit floats.
The "link" is now a 32-bit int. The "link" is now a 32-bit int.
## Tile list ## Tile list
The tile section in the stand tile file ends with an empty entry. This is a regular stand tile of all zeroes (8 bytes of zero). The beta tile list ends with 8 bytes of zero as well (even though the struct size is 12 bytes). The tile section in the stand tile file ends with an empty entry. This is a regular stand tile of all zeroes (8 bytes of zero). The beta tile list ends with 8 bytes of zero as well (even though the struct size is 12 bytes).
"Tile beginning with room 0 is the true way the file format ends, engine does not check for unstric string". For beta tiles, this is a check against the first 4 bytes of the struct. "Tile beginning with room 0 is the true way the file format ends, engine does not check for unstric string". For beta tiles, this is a check against the first 4 bytes of the struct.
## Tile Attributes ## Tile Attributes
Possible values for tile "flags" or "attributes": Possible values for tile "flags" or "attributes":
0 = Normal 0 = Normal
1 = Kneeling 1 = Kneeling
3 = Ladder 3 = Ladder
## Footer ## Footer
After the tile list is a standard footer. This consists of an 8 character zero terminated string "unstric", followed by 4 null points (16 bytes of zero). After the tile list is a standard footer. This consists of an 8 character zero terminated string "unstric", followed by 4 null points (16 bytes of zero).
+3 -3
View File
@@ -1,3 +1,3 @@
# ramrom Asset Readme # ramrom Asset Readme
Contains information for a pre-recorded demo. Contains information for a pre-recorded demo.
+30 -30
View File
@@ -1,30 +1,30 @@
# Asset Readme # Asset Readme
Assets consist of the things like sounds, images, animations, models, level descriptions, etc. Assets consist of the things like sounds, images, animations, models, level descriptions, etc.
There are five categories (subfolders) of assets included in source code. Visit each subfolder for a more detailed readme. There are five categories (subfolders) of assets included in source code. Visit each subfolder for a more detailed readme.
Asset categories are: Asset categories are:
* `font` * `font`
* `images` * `images`
* `music` * `music`
* `obseg` * `obseg`
* `ramrom` * `ramrom`
## `font` Assets ## `font` Assets
This contains font information for rendering text. This contains font information for rendering text.
## `images` Assets ## `images` Assets
Contains ALL textures used in game (UI, Objects and BG) Contains ALL textures used in game (UI, Objects and BG)
## `music` Assets ## `music` Assets
## `obseg` Assets ## `obseg` Assets
Contains level information, game models, and all text used in the game. Contains level information, game models, and all text used in the game.
## `ramrom` Assets ## `ramrom` Assets
This contains demo information the game plays if no folder is selected from the first screen. This contains demo information the game plays if no folder is selected from the first screen.
## Misc Assets ## Misc Assets
In addition to the assets outlined above, the root assets folder also contains model animation definition, as well as a global image table. In addition to the assets outlined above, the root assets folder also contains model animation definition, as well as a global image table.
+1 -1
View File
@@ -5,7 +5,7 @@
# Usage: # Usage:
# ./build-pc.sh [ntsc-final|pal-final|jpn-final] # ./build-pc.sh [ntsc-final|pal-final|jpn-final]
# #
# Dependencies (see docs/PCPortResearch.md): # Dependencies (see docs/internals.md):
# - CMake >= 3.16 # - CMake >= 3.16
# - SDL2 dev # - SDL2 dev
# - zlib dev # - zlib dev
-131
View File
@@ -1,131 +0,0 @@
# Agent / subagent workflow for this port
Guidance distilled from Chris Lewis's *"Decompiling a Nintendo 64 Game
in 84 Days"* (`docs/reference/`, Snowboard Kids, 2026) — an AI-assisted
N64 decomp run 7× faster than its predecessor — adapted to this project,
which is a **PC port on top of a decomp**, not a byte-match decomp. The
mechanics differ (we have no permuter / m2c loop; our inner loop is
root-cause → narrow `#ifdef PORT` fix → visual verify) but the
project-structure lessons transfer directly.
## 1. Every subagent task gets an explicit budget, stated in the brief
Their key finding: an open-ended tool (the permuter) that "runs until
100% match or manually stopped" wrecks throughput; giving the agent a
**deadline it can see** let it trade tool-time against other
problem-solving and judge when to give up on a hard function.
Our open-ended tools are the same shape: rebuild-and-run-the-game cycles,
gdb, env-gated probe iteration, staring at PPM dumps. So every subagent
brief must state:
- a **wall/iteration budget** — e.g. "≤ N build→run→inspect cycles" or
"~M minutes of investigation"; and
- the **fallback on expiry**: *stop, revert probes, write up what you
have with a confidence rating* — a good write-up of a half-solved bug
is a deliverable, not a failure.
Escalate the budget as the project hardens: early bugs are cheap
(one-line truncations, off-by-ones), so cap tight; once those are gone
the survivors are structural (matrix handedness, format specs) and
deserve more room. Revisit the default each session.
## 2. A shared, append-only learnings file that briefs point every agent at
They had agents record generalisable IDO quirks in
`DECOMPILATION_LEARNINGS.md`; later agents read it and got better. "Agents
helped document IDO, and the documentation made subsequent agents better."
Our equivalent already exists but is diffuse: `docs/PCPortResearch.md` §F
is 200 KB of narrative. The **recurring PC-port quirk classes** should be
distilled into a short cheat-sheet every subagent brief links and every
subagent is told to append to:
- pointer-width struct growth (32→64) in ROM-serialized / pun-allocated
structs — the dominant bug class (D53.2, D56, D79, D98–D102)
- 16-byte PC `Gfx`/`Vtx` vs 8-byte N64 — reservation & copy sizing
(D50.6, D58, D85, D95)
- big-endian rodata read on LE: `f32` word-pairs (D73, D112), header
offset tables (D68, D87), packed bitfields (D78, D83)
- N64 hardware idioms fast3d doesn't emulate (fill-rect Z clear → D105;
LOD/detail tiles → D107; K0 segment address folds → D58, D84)
- offline sidecar converter preferred over runtime fixup for whole
formats (D43, D69, D88)
This is `docs/PORT-LEARNINGS.md` — one screen, an index into §F, not a
replacement. Every investigation-subagent brief links it; every subagent
is told to append any new generalisable quirk before reporting.
## 3. Parallel work: partition by file, consolidate often
They ran 4 git worktrees sharded by a hash of the candidate list, and
found the cost was **synchronisation** — a match in one worktree stayed
invisible to the others until merged, and merging all four took >1 h.
Their fix: make the similarity search read *all* worktrees so a new
reference propagates immediately, and merge to main periodically anyway.
For us (parallel investigation subagents, not worktrees yet):
- **Partition tracks by the files they will touch** so patches don't
collide. This session: Track 1 = `bg.c`, Track 2 = `chr.c`/`model*.c`,
Track 3 = `gunfire.c` + a new audit doc — disjoint, merged clean.
A brief that can't be file-partitioned shouldn't be parallelised.
- Subagents must **revert their own probes** before reporting (leave only
committed, env-gated, capped probes) and must **not** touch another
track's uncommitted edits — call them out in the report instead.
- The integrator (main session) consolidates to `master` per-track with
the §F write-up, then the next round of agents starts from a clean
tree. Don't let investigation branches drift.
- If we move to real worktrees: `data/` must be **copied, never
junctioned** (see the `data-dir-junction-hazard` memory —
`git worktree remove --force` follows the junction and deletes the real
baserom + sidecars).
## 4. Cheap automated pass before spending agent tokens
They scripted `m2c` across every unmatched function first (0.93 % hit
rate, but near-free) and told agents to exhaust known SDK
versions / compiler flags / `#if` paths before writing original code.
Our pre-flight before dispatching an investigation agent:
- `./build-pc.sh` green + `/linkcheck` clean
- run the existing env-gated probes for the area (`GE_D96`, `GE_D104`,
`GE_D69*`, …) and attach the output to the brief
- `GE_PCDUMP` + `tools_pc/pixcount.py` baseline capture
- grep `docs/PCPortResearch.md` §F for a prior instance of the bug class
- check `PD_PORT_CHECKOUT` for the PD analogue
(already a non-negotiable) — the brief should name the file to look at
Give the agent the results, not just the task. "Give the agent the
findings, not just the problem" is the through-line of the whole post.
## 5. What does *not* transfer
- **Model choice.** Their (unscientific) take was Codex > Claude for
reproducing IDO codegen. Our task is behavioural correctness, not
compiler-output matching — not comparable; ignore.
- **The permuter / match-percentage loop.** We don't bit-match. Our
"done" bar is: no fault + visually correct vs. N64 footage
(`docs/reference/n64-footage-*`), measured with `pixcount.py`.
- **"Understanding the function is the easy part."** True for decomp,
inverted here — our hard part is often *what N64 hardware/ABI behaviour
the C is silently relying on*.
## Standard investigation-subagent brief template
```
Repo: REPO_ROOT (git, master, Windows; PowerShell + Bash).
READ FIRST: docs/HANDOFF.md (top), docs/PORT-LEARNINGS.md,
docs/PCPortResearch.md §F <specific Dxx entries>, AGENTS.md non-negotiables.
TASK: <one bug, one subsystem>.
FILES YOU MAY TOUCH: <disjoint from other running agents>.
KNOWN-GOOD / RULED OUT: <list — do not re-investigate>.
PRE-FLIGHT ATTACHED: <probe output, pixcount baseline, pd_port pointer>.
BUDGET: <N build→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 §F.
revert your own temp probes; do not touch other agents' uncommitted edits.
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/PORT-LEARNINGS.md.
```
-303
View File
@@ -1,303 +0,0 @@
All of these items should be completed once the main original game is playable from start to finish on PC with no crashing or major issues.
---
## Playtest defects — logged M-28 (2026-08-30), not yet worked
User-reported during free playtest. Suggested priority order below (cheap
input fixes first, then the texture correctness bug, then the filtering
feature).
- **B1 — texture filtering / "interlaced" shimmer (feature).** Whole game
has an interlaced/shimmering look. Want N64 3-point-filter emulation +
bilinear/trilinear handling like the Nightdive / Quake-remaster ports.
MVP: one good default filtering mode in `port/fast3d/` (3-point emu
shader or clean bilinear). Long-term: full toggle set (see "PC graphics
settings" → Quality + stretch goals below). Biggest of the four.
- **B2 — glitchy textures / wrong colors (correctness bug).** Some
textures render with wrong colors / garbage — **very noticeable on
Depot (`-level_30`)**, present in many other spots too. Likely a
texture-decode/format bug (CI/TLUT, RZ decode `port/src/rzdecomp.c`, or
the `import_texture_*` path in `gfx_pc.cpp`). Distinct from B1. Needs
investigation — could be a real fidelity blocker, not cosmetic-parked.
- **B3 — RMB aim far too mouse-sensitive.** Manual-aim (RMB/LShift) mouse
sensitivity is way too high. `Input.MouseAimSpeed` (default 50) still
overshoots badly. Quick fix: lower default + recheck the aim-band curve
in `port/src/input.c` (the 61..80 stick push added M-24). Port-only.
- **B4 — menu crosshair-cursor screens: PC input is wrong.** Menu screens
that use the crosshair cursor don't behave like a PC pointer. The
original front-end was PC-style "select a file with the mouse" — menu
input should be a **separate mode from in-game input**, not the same
analog-stick emulation. Needs a dedicated menu-pointer path in
`port/src/input.c` (absolute cursor → menu hit-test) rather than
feeding synthetic stick deltas. Port-only.
- **B5 — no game save on level completion.** On the OG cartridge, finishing
a level writes progress to EEPROM (unlocks next mission, records
time/difficulty). On PC nothing is saved — progress is lost on quit.
This is **Phase 4** (file-backed EEPROM): `osEepromWrite/Read` are
currently no-op stubs; build on PD's `fs.c` (file-backed save dir). The
save *logic* already runs in the decomp — it just needs real backing
storage. Blocks any multi-session campaign playthrough.
### PC graphics settings
Optional video/rendering options, modelled on the PD port (baseline) with
Quake 1 remaster / Nightdive-style extras as stretch goals. **Every option
defaults to the current behavior** — zero visual delta at defaults.
**Starting point (verified):**
- `port/src/config.c` exists but is minimal (58 lines: Int + String only;
PD's is 300 lines with Float/UInt, clamping, INI sections). Copy-and-adapt
from `pd_port/port/src/config.c`.
- `port/src/video.c` is a trimmed port of PD's (250 vs 588 lines). Render path
is architecturally identical to PD: native-res FBO (GE = 640×480 NTSC LAN1)
→ scaled blit to window, so all PD display options port over cleanly.
- No `#ifdef PC` blocks exist in `src/` yet; only the documented ABI/layout
exception edits (D3x) are present. Anything below that changes *behavior*
(FOV, HUD placement, menu contents) is a policy decision — see route notes.
**Baseline (PD parity)** — each item cites its PD source:
1. Config system: full `configRegister{Int,UInt,Float,String}` + clamps
(`pd_port/port/src/config.c`).
2. Window/display (`video.c` + SDL): fullscreen checkbox, fullscreen mode
(borderless/exclusive), maximize, center window, resolution dropdown
(display-mode list + "Custom"), HiDpi toggle.
3. Frame pacing: vsync (Adaptive/Off/On/N-frames via `SDL_GL_SetSwapInterval`),
framerate cap slider (PD caps FPS when vsync is off — keep that guard).
4. Quality: MSAA Off/2x/4x/8x/16x (FBO `samples` — **verify our FBO path
accepts samples>0 first**; PD reads it back via `videoGetMSAA`), texture
filter Nearest/Bilinear, anisotropic 1–16 slider, mipmap filter,
on-screen FPS display toggle.
5. **FOV slider (baseline per owner decision).** PD does this game-side
(`Game.PlayerN.FovY`, `g_PlayerExtCfg` in their `main.c` + decomp edits).
Two routes — decide in a spike:
- **(a) Port-layer:** override the camera projection in our fast3d where
the view matrix/projection is consumed. No src changes; must find the
exact injection point (camera math flows through game GBI setup).
- **(b) PD-style port-only `src/` edit** reading a port-provided value.
This would be the first *behavioral* src edit — exceeds the current
ABI/layout exception in AGENTS.md #2; if chosen, expand the documented
exception ("PC-port feature hooks, opt-in via config, default = original
behavior") and record a Dxx finding.
6. **Widescreen handling:** non-4:3 window aspect → stretch vs pillarbox
choice, plus HUD centering (None/4:3/Wide à la PD `Game.CenterHUD`). PD
implements the HUD part with `G_ASPECT_*_EXT` extended GBI mods set
game-side; for us that's either a fast3d viewport intercept of the HUD
quads (route a) or a port-only src edit (route b). Same policy note as FOV.
**In-game video options menu:** v1 = config file (+ CLI flags if trivial).
PD's in-game menu (`pd_port/port/src/optionsmenu.c`, 2042 lines) hooks into
*their* decomp's menu tables with port-side handlers; GE's menu code differs,
so it's an adaptation, not a copy. Feasible under route (b) policy; revisit
after the config-file v1 works.
**Stretch goals (Quake 1 remaster / Nightdive):**
- Internal resolution scale: render at 2x/4x native then downscale (free
supersampling AA, big fidelity win on modern displays).
- FXAA post-process toggle (cheap AA alternative to MSAA).
- SSAO + bloom / light-shaft post-processes — Quake Enhanced ships each
enhancement as an individually-toggleable option; keep that pattern (one
toggle each, all default off, shared post-FBO stage with FXAA/colorblind).
- Integer-scaling option.
- Texture upscaling filter (ESRGAN-style or pre-generated upscales) — the
biggest item; do last. Note GE textures are RZ-compressed at load
(`port/src/rzdecomp.c`) — an upscale pass would hook there or post-decode.
**Acceptance:** every option persists via config file; toggles apply live
where PD's do; `tools_pc/golden/` dumps byte-identical at all-defaults;
no `src/` changes unless route (b) is explicitly chosen and documented.
### LAN deathmatch / multiplayer — the netplay foundation (do first)
**The idea:** play GE's *existing* competitive multiplayer (deathmatch /
mission-based, 2–4P) across up to **4 instances** of the PC port on a local
network. This is the **nettech proving ground**: the game logic already
exists in the decomp (original N64 feature — no new mode to invent), so this
item isolates and de-risks the entire networking layer. Co-op (below) bolts
onto whatever this produces.
**Milestone 0 (prerequisite, real port work):** verify GE's *existing*
split-screen multiplayer works locally on PC — two cameras, split viewport,
two independent input streams, full match lifecycle. Every code path netplay
will drive must first work on one box.
**Net model (research item — owner leans client-server):**
- Leading candidate: **host = server + player** (Source-engine style): host
runs the authoritative sim, clients send inputs and receive state. Friends
connect by manual IP entry on a LAN. Self-healing desync, one machine owns
truth.
- Cost to size up: a full per-tick **state snapshot format** (players,
entities/props, AI, doors/props) must be defined from scratch — no existing
save/replay infra covers frame state (EEPROM saves are mission-level only).
- Alternative for the research phase: **lockstep/deterministic** (exchange
~10 bytes of input per 60Hz tick, both machines run the full sim). Far less
code *if* the sim is bit-deterministic across hosts — PRNG is ported
verbatim (`port/src/random.c`, good), but host-scheduling nondeterminism is
a known risk (see D24-implications note below). Desync = hard stop.
A per-tick state-hash desync detector is cheap insurance either way.
- PD port has **zero networking code** — nothing to copy; the net layer is
new (`port/src/net.c`-style, SDL_net or plain sockets).
**Shape of the work under host-authoritative:** generalize to host + N clients
(N≤3), per-client input channels, state snapshots fanned out to all clients.
The host already runs the full sim, so scaling past 2P is mostly protocol
fan-out — which is why 2P is a natural intermediate milestone on the way to
the 4-instance bar.
**v1 bar:** LAN only, manual IP entry (host listens / client connects),
deathmatch playable; 2P first, then scale to 4 instances. No matchmaking,
discovery, reconnect, or internet play. Stretch: ping display, graceful
reconnect-on-drop, mission-based (non-deathmatch) multiplayer variants,
remote-player model interpolation between snapshots (Quake Enhanced ships
it; hides LAN jitter).
### Co-op (new mode — built on the deathmatch net layer)
**The idea:** two players progress the single-player campaign levels
together — locally on one PC, and over LAN with each player on their own PC.
Sequenced **after** LAN deathmatch: reuse its connection handling, snapshot
format, and input channels; this item adds the mode itself.
**Key fact (defines the scope): GE has no co-op mode.** Unlike PD, GoldenEye
only ships split-screen *competitive* multiplayer. Co-op on solo levels exists
in **PD only** — so this is a net-new game mode: mapping/level and GE game-
code feature work, on top of the (already-existing) networking plumbing.
Consequences:
- Non-negotiable #2 ("game logic unmodified") is about not altering *existing*
behavior; adding a new mode is a separate, explicit owner decision. Record
the policy call when work starts.
- `pd_port/` is the reference implementation: PD's decomp shows how two-player
co-op was wired into this same Indy engine (shared objective progression,
both players' AI/enemy interaction, win/lose conditions, split-screen
presentation). Expect an adaptation study, not a copy — GE's mission/
objective code differs file-by-file.
- Cheaper fallback worth considering during design: "freeplay" co-op (both
players active on a solo level, but only P1's objective progress counts /
or objectives are duplicated per player). Much less new logic; decide after
the PD study.
**v1 bar:** LAN + local 2P co-op on solo levels over the deathmatch net layer.
No matchmaking, discovery, reconnect, or internet play.
### Fun features (remaster research: Quake 1 Remaster / Nightdive)
Feature mining from the Quake 1 Remaster (id, 2016), Nightdive's remaster
line (Duke Nukem 3D: Forever, DOOM, System Shock), and — round 2,
2026-08-29 — Quake Enhanced, Quake II Enhanced, Turok Resurrection, Doom 64
Definitive Edition, SiN Reloaded. Pattern across all of them: classic
fidelity as default + a shelf of opt-in QoL/accessibility/asset options.
Tiered by effort; all stay in `port/` unless noted.
**Cheap wins (config toggles over existing infra):**
- **Screenshot hotkey.** Productize the existing debug dump:
`gfx_opengl_dump_bound_fbo()` (`port/src/video.c:199`) is already there —
wire a keybind + configurable output dir, timestamped filenames. Every
Nightdive title has this; ours is ~an afternoon.
- **Screen-shake intensity slider (0–10x).** All shake funnels through
`viShake()` (`src/game/bondview2.c:8300` et al.) which lands in our port
shim — scale amplitude there, zero src changes. PD precedent:
`Game.ScreenShakeIntensity`.
- **No hit-flash / damage-indicator toggle.** The red full-screen flash on
taking damage is a top community-mod option; Nightdive ships it. Impl: find
how the flash overlay is drawn (likely a full-screen HUD quad) and intercept
in the port layer, or route-(b) one-liner.
- **Auto-pause / input-hold on window focus loss.** SDL focus events; trivial,
prevents drive-by deaths when alt-tabbing.
- **Controller polish (copy from PD `input.c`):** per-player gamepad
assignment (`Input.ControllerIndex`), per-stick deadzone/sensitivity sliders,
rumble scale + rumble mapping to GE's feedback moments. Note `osMotor*`
(Rumble Pak) is currently stubbed — accessory detection only
(PCPortResearch §4) — so wiring it to `SDL_GameControllerRumble` is part of
this item; Doom 64 DE shipped haptic feedback (a KEX first). We have basic
pad support (D118); this is the remaster-grade layer on top.
- **Tick-rate / framerate options.** PD ships `Game.TickRateDivisor` +
framerate cap. Flag: any tick-rate change must be reconciled with the future
netplay determinism requirement before shipping as a user option (lock it to
60Hz default, document).
- **Fog / draw-distance option.** Turok's KEX port removed the N64's heavy
distance fog; Quake II Enhanced added height-based fog with custom colors.
fast3d already decodes fog generically (PCPortResearch §5) — a config
slider scaling/overriding fog density, default = original values. Zero src
changes.
**Medium:**
- **High-res / user texture packs (the big goodwill item).** Quake 1 Remaster's
core feature is swappable asset sets (classic vs remastered toggle). GE
textures are RZ-compressed and decoded at load (`port/src/rzdecomp.c`) — a
per-texture override lookup against a user directory (fall back to ROM) hooks
there. Enables the community to make packs; also our escape hatch for the
blurry-asset complaints without touching fidelity defaults.
- **Accessibility: colorblind post-process filters** (protanopia/
deuteranopia/tritanopia full-screen shader pass) + the screen-shake slider
above. Nightdive includes these; cheap once we have a post-process stage
(shared infra with FXAA from the graphics item — sequence together).
Quake II Enhanced also shipped high-contrast / readability UI options —
add where the port layer can reach (bitmap HUD text limits scope;
document what's not doable under route b).
- **Crosshair customization** (size/opacity at minimum; GE's crosshair is
game-drawn — check whether it's a textured quad we can scale in the port
layer before assuming route b).
- **Widescreen FOV compensation** — already baseline in the graphics item;
listed here because it's the feature every remaster ships and users will
expect.
**Big / stretch:**
- **Demo / replay record + playback.** Record the input stream (+ initial
state), play back headlessly. Quake tradition; Nightdive titles have
replay-ish tooling. **Strong synergy with netplay work:** a working replay
system *is* a determinism test harness — if a recorded demo replays
bit-identically, lockstep netplay's hardest assumption is proven. Consider
building it as the netplay pre-flight tool even before it's user-facing.
- **Photo mode.** Pause + free orbit camera + capture (System Shock
precedent). Free camera = port-layer camera override; pairs with the
screenshot hotkey.
- **In-game console.** Type commands at runtime. Less "remaster feature"
than dev tool, but for a WIP port it's arguably the highest-value item on
this list: toggle debug flags, dump state, force conditions without
restarting. PD has no equivalent; we'd own the design.
- **Benchmark mode.** Scripted auto-run sequence + FPS report (Quake
tradition); trivial once tick-rate options exist.
- **AI opponents in deathmatch.** Quake Enhanced ships bots in DM. GE's N64
MP is human-only, so bots = net-new game logic — same policy class as
co-op (record the owner decision when work starts). Stretch: lets a solo
player exercise the netplay layer and makes MP approachable with 1–2 humans.
- **Animation smoothing ("wiggle elimination").** Quake II Enhanced restored
high-res skeletal animations specifically to kill low-fps "wiggle". GE's
model keyframes are N64-rate; interpolating (slerp) between keyframes in
the port-layer model-transform path would visibly smooth guard/Bond motion
on modern displays. Stretch — sequence with the graphics backlog, do late.
- **Dynamic shadows.** Quake Enhanced's headline graphical feature. For
fast3d this means a shadow-map pass + per-light casting — deep stretch;
park next to texture upscaling (both "do last" items).
- **Remastered audio option (Phase 3 note).** System Shock's remake added
new sounds/music. Keep N64 ADPCM fidelity as the default; optionally offer
a lossless re-encoded soundtrack toggle — owner decision + content work,
park until Phase 3 lands.
- **Optional modern UI skin.** SiN Reloaded shipped upgraded 2D screens /
menu art; System Shock an overhauled interface. For GE this is route (b)
game-side territory (menu tables in `front.c` / `initmenus.c`) — park as
an explicit owner decision; lowest priority on this list.
- **Stereo 3D** — both reference families shipped it; explicitly
deprioritized as anachronistic. Park unless someone asks.
Out of scope by the v1 bars above: online matchmaking, leaderboards,
anti-cheat, cross-play (Quake 1 Remaster's biggest features; revisit only
after LAN netplay is solid).
Also out of scope by policy (community port): online-platform integration —
Steam/GOG/Epic achievements, cloud saves, storefront add-on downloads. No
platform tie-in of any kind.
--Old is above here
---
## Technical cleanup (backlogged; not gated on playability)
- **Remove TEMP D63 debug scaffolding.** ~16 references in `port/src/` (`libultra.c`): the master-DL scanner and env-gated `GE_D63` logging inside `osSpTaskStartGo()`. D63 is closed; this is dead weight in the hottest function in the port.
- **Add a D24-implications bug class to `docs/PORT-LEARNINGS.md`.** "Host-scheduling nondeterminism / fake priority semantics": the pthread kernel does not enforce N64's 0–31 priorities (`osYieldThread` = `Sleep(0)`), so interleavings impossible on console can occur, and frame timing has host jitter. Consequence for future sessions: when a flaky timing bug appears (especially Phase 3 audio underruns), reach first for host thread priorities (`SetThreadPriority`: scheduler thread time-critical, tick normal) and the deferred `GE_DETERM` mode (D117) — not for kernel changes.
- **Watch item: `osYieldThread` = `Sleep(0)`.** The one place we fake cooperative behavior. If a level-sweep hot loop misbehaves under host load, this shim is the first thing to inspect. Keep as-is until then.
## Architecture decision (recorded; no action)
- **Keep the pthread + real `src/sched.c` model.** Compared against the PD port's actual design (verified in `pd_port/`: zero threads — thread API is pure no-op, `mainProc()` runs as the host main thread, `pdsched.c` is a gutted frame-callback wrapper, audio driven inline via `amgrFrame()`, M_AUDTASK dropped). PD could do that only because their *original N64* scheduler already used direct-call submission ("nothing writes to the cmdQ in PD") and they adapted game-side lib code (`audiomgr.c`). For GE — classic `osSendMesg(cmdQ)` protocol, real `__scMain` loop, `amMain` blocking on retrace/reply messages — that route means editing `src/audi.c`/submission points (non-negotiable #2 violation) and regressing Phase 3, which currently runs the full authentic audio scheduling protocol. No rewrite. Related: audio stays inline in the `M_AUDTASK` branch (`docs/AUDIO-PLAN.md`, Option A + sub-decision A3); no dedicated audio worker thread unless host thread priorities provably fail.
-76
View File
@@ -1,76 +0,0 @@
# BRIEF — B2: Depot (-level_30) wrong-colour / garbage textures
M-29 diagnostic pass. **Not fixed.** Evidence captured, leading hypothesis
recorded. Companion: `docs/BACKLOG.md` B1/B2, `docs/SMALL-FIXES.md` C-section.
## Symptom (captured)
`-level_30`, opening loading-bay area, `GE_PCDUMP` frames ~700-1150
(camera panning up toward the roof):
- The **ceiling / roof surface** renders as bright blue with a dense green
speckle and radial "light-ray" streaks emanating from a point. On N64 this
is a dark, low-contrast corrugated roof.
- Walls (green wall panels right, tan crates left) look approximately
correct — mild green tint at most.
- HUD ammo digits mirrored = D116, unrelated.
Frames: `ppm/frame_0007..1150` this session (regenerate with
`GE_PCDUMP="700-1600:150" ./build-pc/ge007.x86_64.exe -level_30`).
## UPDATE (M-29, later): filtering RULED OUT
Enabling N64 3-point filtering + trilinear mips + 4x anisotropy game-wide
(the B1 work, now default) **did not change the Depot roof** — the blue
speckle + radial rays render identically at 640x480 native. So this is NOT
grazing-angle aliasing. It is a real texture-data / UV / effect bug on that
specific surface. The radial rays converging to a point look like either a
broken texture matrix (rotating/scrolling tc), garbage UVs on the ceiling
geometry, or a mis-rendered light-shaft effect (wrong blend/opacity). Needs
the `GE_DTEX` probe below to identify what draws it.
## (superseded) earlier hypothesis: grazing-angle aliasing / B1
The radial-ray + sparkle pattern is the **classic grazing-angle aliasing**
signature of a high-frequency texture sampled with point/bilinear filtering
and no working LOD/mipmap chain. The ceiling is at a very shallow angle to
the camera in these frames — exactly where N64 3-point + LOD filtering
suppresses shimmer and our fast3d path does not (see BACKLOG B1: "whole game
has an interlaced/shimmering look"; `port/fast3d/gfx_pc.cpp` mip handling is
stubbed — `gfx_lod_tile_offset` / "for now just ignore smaller mips" at
~1540, and D107 notes fast3d fabricates a crop when detail textures are off).
**Decode path was audited and looks correct:**
- `gfx_dp_load_tlut` byteswaps entries (`PD_BE16`), `palette_to_rgba32`
decodes RGBA16 5551 and IA16 correctly, `rdp.palette_fmt` is set from
`other_mode_h & (3<<G_MDSFT_TEXTLUT)` (line ~2389).
- `import_texture_rgba16` / `ci4` / `ci8` decode textbook-correct, BE reads.
- D72 already disabled the PD normal-derived texgen for GE, so the streaks
are not env-mapping.
Nothing in the decode path obviously produces "blue + green speckle".
## Next diagnostic step (for a focused session)
1. **Probe the ceiling draw:** in `gfx_pc.cpp` `import_texture` / the tri
setup, `GE_DTEX=1`-gate a log of `{orig_addr, fmt, siz, palette_index,
line_size_bytes, width, height, cms/cmt, other_mode_h & TEXTFILT, tex_lod}`
for the first ~200 unique textures on `-level_30`. Identify which entry is
the ceiling (large, tiled, grazing).
2. If its `fmt/siz` and data look sane → confirm B1: the fix is proper
trilinear + a real mip chain (or an N64 3-point-filter emulation shader),
per BACKLOG B1 "MVP: one good default filtering mode". Do B1 first, then
re-shoot this frame; B2 likely collapses into it.
3. If the data is garbage (wrong TMEM offset, wrong decompressed RZ section)
→ it's a real decode/addressing bug: check `line_size_bytes` vs
`full_image_line_size_bytes` (the `SUPPORT_CHECK` at
`import_texture_rgba16:657` is *commented out* with a TODO about garbage
`full_image_line_size_bytes`), and the RZ section offset from
`rzipGetSomething()` in `src/game/image.c`.
## Risk / scope
B1 (filtering + mips) is a medium fast3d task with a clear reference (PD port,
Nightdive 3-point emu). It is the highest-leverage single graphics fix —
addresses the game-wide shimmer complaint AND (hypothesis) most of B2. Do it
as its own focused effort, not inline.
-122
View File
@@ -1,122 +0,0 @@
# BRIEF C1 — `chrIsNotDeadOrShot` crash on 6 levels (blocks the level sweep)
Repo: `REPO_ROOT` (git, branch `master`, Windows;
PowerShell + Bash tool).
## READ FIRST
- `docs/HANDOFF.md` (top) — state, environment, repro line.
- `docs/PORT-LEARNINGS.md` — **§A (pointer-width struct growth) and §C
(BE rodata on LE) are the prime suspects.** Also §A D119
(`weapons_held[]->chr` type-pun) — closest prior instance.
- `docs/PCPortResearch.md` §F entries **D119, D115, D101, D102, D56, D57**
(jump via the index — do NOT linear-read).
- `docs/AUDIT-M6-player-offsets.md` — the raw-offset landmine class.
- `AGENTS.md` non-negotiables (#2: narrow `#ifdef PORT` ABI/layout only,
N64 line kept under `#else`, logged in §F).
## SYMPTOM (WS4 sweep, this session — `docs/LEVEL-STATUS.md`)
`./build-pc/ge007.x86_64.exe -level_XX` (bare — pools auto-injected, D121),
crashes **early, before frame 1**, deterministically, at:
```
chrIsNotDeadOrShot src/game/chraction.c:4483 s8 currentaction = self->actiontype;
```
`EXCEPTION 0xc0000005`, FAULT ADDR `0x8` (Dam). Crash **`self` = the same
value as backtrace frame #1 = `0x1401296a0`** — an address *inside the
compiled binary's rodata*, in the `chraidata.c` global-AI-list blob
(`m_AimAtBond[]` / `m_*[]` at `chraidata.c:61+`). So a `ChrRecord *self`
is pointing at AI-list bytecode rodata — a pointer swap / pun / wrong
struct-offset read, not mere corruption.
Affected: **Dam 33, Runway 35, Frigate 26, Statue 22, Streets 29,
Cradle 41** (6). NOT Bunker1 09 / Silo 20 / Archives 24 / Train 25 /
Caverns 39 / Egypt 32 / Cuba 54 (those 7 PASS, render ~91%).
## TASK
**One question: why is a `ChrRecord*` actually an AI-list rodata pointer
on these 6 levels, and what is the narrow fix?**
Leading hypotheses:
1. A per-level `Usetup*Z` guard record (`GuardRecord` type 9 /
`GuardAttributeRecord` type 18, or the chr-spawn record) has a pointer
or `s16/s16` packed field converted wrongly — or not at all — by
`tools_pc/d88_propdefs.py` (cf. D122: the "generic" fallback arm
word-swaps sub-word fields). Enumerate every propDef `type` byte the 6
crashing levels emit vs the 7 passing ones
(`tools_pc/d88_propdef_scan.py` histogram) — the delta type is the
suspect. The 7 pass / 6 fail split is the strongest lead.
2. The AI-list bytecode pointers in `chraidata.c` `.rodata`
(`SetChrAiList`, list-table entries) are BE and consumed raw — a chr
gets `aiList` set to a byteswapped pointer, then a subsequent tick
treats it as / derefs it as a `ChrRecord*`. Check `chraifunc.c` /
`chraidata.c` list dispatch and any `AiListRecord`-style table.
3. `ChrRecord` / `GuardRecord` struct is pointer-width-grown on x86-64
and a per-level record is read from ROM bytes at N64 offsets (D56/D57
class) — `self->actiontype` at the wrong byte.
Determine which. Prefer an **offline converter extension**
(`d88_propdefs.py` / `d88_emit.py`, D43/D88 pattern) over a runtime fixup;
else a narrow `#ifdef PORT` ABI edit.
## FILES YOU MAY TOUCH
- `src/game/chraction.c`, `src/game/chr.c`, `src/game/chraidata.c`,
`src/game/chraifunc.c` (narrow `#ifdef PORT` only).
- `tools_pc/d88_propdefs.py`, `tools_pc/d88_emit.py`,
`tools_pc/d88_layoutprobe.c`, `tools_pc/d88_propdef_scan.py`.
- New probe files `tools_pc/c1_*.py` / env-gated `GE_C1` blocks.
- `docs/PORT-LEARNINGS.md` (append), `docs/PCPortResearch.md` §F (write up
as the next Dxx after D122 — check the §F index for the current max —
add to the index), `docs/LEVEL-STATUS.md` (flip the rows you fix).
- **NOT** `port/fast3d/*` or `src/game/propobj.c` (other tracks).
## KNOWN-GOOD / RULED OUT — do not re-investigate
- D121 per-level pool injection — works, committed.
- D122 propDef `obj` half-swap — committed `f2beae4b`; your bug is a
*different* record type/field (BUNKER1/Silo unaffected).
- BUNKER1 + Silo + Archives + Train + Caverns + Egypt + Cuba chr/AI setup
— works. Use them as the "good" control for the propDef-type diff.
- The intermittent BUNKER1 `loadobjectmodel.c:393` crash — D117
nondeterminism, unrelated.
## PRE-FLIGHT ATTACHED
- `./build-pc.sh ntsc-final` green (`export PATH="/c/msys64/mingw64/bin:$PATH"` first).
- Sidecars current for `f2beae4b`. Regen after any converter change:
`python tools_pc/d43_emit.py ntsc-final && python tools_pc/d69_emit.py ntsc-final && python tools_pc/d88_emit.py ntsc-final --regen`
- Repro: `./build-pc/ge007.x86_64.exe -level_33` (Dam). Crash log
`ge007.crash.log` at repo root. Symbolicate:
`addr2line -e build-pc/ge007.x86_64.exe -f -C 0x<PC>` (PATH first;
image base 0x140000000, PC in log is absolute).
- Frame capture `GE_PCDUMP="80-260:40"` → `./ppm/`; `python tools_pc/pixcount.py ppm/<last>.ppm`.
- **`timeout` does NOT kill the game on Windows.** Launch `... &`,
`sleep 24`, `taskkill //F //IM ge007.x86_64.exe`.
- gdb attach is fast: game running via `nohup ... &`, then
`gdb -batch -x cmds.txt -p <winpid>` (winpid = 4th col of `ps -p <bashpid>`).
Hardware watchpoint on the chr's `aiList` field, or on the bad `self`,
catches the corrupting write.
- PD analogue: check `PD_PORT_CHECKOUT` chr / guard
setup + its propdef converter before writing anything new.
## BUDGET
Structural → **~12 build→run→inspect cycles or ~90 min.** On expiry:
revert temp probes (leave only committed env-gated ones), write up spec
progress + best hypothesis in §F with an explicit confidence rating, list
every file you touched. A good half-solved write-up is a deliverable.
## CONSTRAINTS
No game-logic / control-flow changes. ABI/layout/format only, each
`#ifdef PORT` with the N64 line verbatim under `#else`, each documented in
§F. Prefer converter extension over runtime fixup. Revert your own temp
probes before reporting. Do not touch other tracks' uncommitted edits
(none expected).
## REPORT
1. Root cause — `file:line` evidence, which hypothesis, why the 6 differ
from the 7 passing levels.
2. Fix — the diff, or why-not if unresolved.
3. Verification — `-level_33/35/26/22/29/41` each boot to ≥1
non-degenerate frame (`pixcount.py` > a few %), AND `-level_09` +
`-level_20` unregressed (`tools_pc/framediff.py`).
4. Probes left in the tree (env-gated, capped).
5. Confidence rating.
6. Generalisable quirk appended to `docs/PORT-LEARNINGS.md`.
-81
View File
@@ -1,81 +0,0 @@
# BRIEF C2 — fast3d bad texture pointer on Facility + Jungle
Repo: `REPO_ROOT` (git, `master`, Windows;
PowerShell + Bash).
## READ FIRST
- `docs/HANDOFF.md` (top), `docs/PORT-LEARNINGS.md` (§C BE rodata, §D N64
hardware idioms fast3d doesn't emulate).
- `docs/PCPortResearch.md` §F entries **D69, D68, D39, D67, D107, D84,
D58** (texture / segment-address history — jump via §F index).
- `AGENTS.md` non-negotiables. `rsp/graphics/gmain.s` = GBI ground truth.
- PD analogue: `PD_PORT_CHECKOUT` `port/fast3d/`
texture import path — check first.
## SYMPTOM (WS4 sweep — `docs/LEVEL-STATUS.md`)
Bare `./build-pc/ge007.x86_64.exe -level_XX`, early deterministic crash:
| Level | # | Site | Fault addr |
|---|---|---|---|
| Facility | 34 | `import_texture_i8` `port/fast3d/gfx_pc.cpp:821` | `0x72181ee8` (looks like an unresolved N64 segment addr) |
| Jungle | 37 | `gfx_tex_normalize_source` `port/fast3d/gfx_pc.cpp:644` | `0xabcd0824` (sentinel / uninit value) |
fast3d is handed a garbage texture address. 7 levels render fine
(Bunker1/Silo/Archives/Train/Caverns/Egypt/Cuba) so the common path is
sound — Facility/Jungle pull in a texture asset or a GBI texture-image
segment address the others don't.
## TASK
Root-cause both (likely the same class) and apply the narrowest fix.
Hypotheses:
1. A per-level texture asset mis-converted / missing —
`tools_pc/d69_emit.py` (pccg path) or `port/src/pccg.c`. `0xabcd....`
is a classic uninit-slot sentinel — a texture slot that was never
filled because the converter skipped a format/opcode.
2. A GBI `gsDPSetTextureImage` segment address not resolved to a real
pointer before `import_texture_*` (segment-fold class — D58/D84;
`(u32)` 32-bit wrap on `BG_SEG_TO_PTR` / K0 folds). `0x72181ee8` is in
the N64 segment-mapped range.
3. A texture LOD / detail-tile path (D107) hit only by these levels.
## FILES YOU MAY TOUCH
- `port/fast3d/gfx_pc.cpp` (narrow — a resolve/guard at the import site).
- `tools_pc/d69_emit.py` (+ `tools_pc/d69_*` helpers), `port/src/pccg.c`,
`port/src/romdata*.c` (segment resolution).
- New probe files `tools_pc/c2_*` / env-gated `GE_C2` blocks.
- `docs/PORT-LEARNINGS.md` (append), `docs/PCPortResearch.md` §F (next Dxx
after the current §F-index max; add to index), `docs/LEVEL-STATUS.md`.
- **NOT** `src/game/chr*.c` or `src/game/propobj.c` (other tracks).
## KNOWN-GOOD / RULED OUT
- D121 pool injection, D122 propDef fix — committed, work.
- 7 levels render clean — the base texture path is fine.
- HUD/text X-mirror (D114/D116) — parked, do NOT re-trace (PORT-LEARNINGS §D2).
## PRE-FLIGHT
- `export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final` — green.
- Repro `./build-pc/ge007.x86_64.exe -level_34`. Crash log
`ge007.crash.log`. `addr2line -e build-pc/ge007.x86_64.exe -f -C 0x<PC>`.
- `GE_PCDUMP="80-260:40"` → `./ppm/`; `python tools_pc/pixcount.py`.
- `timeout` does NOT kill on Windows: `... &`, `sleep 24`,
`taskkill //F //IM ge007.x86_64.exe`.
- Regen sidecars after converter change:
`python tools_pc/d43_emit.py ntsc-final && python tools_pc/d69_emit.py ntsc-final && python tools_pc/d88_emit.py ntsc-final --regen`.
## BUDGET
**~10 build→run→inspect cycles or ~75 min.** On expiry: revert probes,
write up in §F with confidence rating + files touched.
## CONSTRAINTS
No game-logic changes; ABI/layout/format / port-layer only. Prefer
converter fix over runtime. Revert temp probes. Don't touch other tracks'
uncommitted edits.
## REPORT
1. Root cause + `file:line` evidence, why Facility/Jungle differ.
2. Fix diff or why-not.
3. Verification: `-level_34` + `-level_37` boot to ≥1 non-degenerate
frame; `-level_09` + `-level_20` unregressed (`framediff.py`).
4. Probes left in tree.
5. Confidence.
6. Quirk appended to `docs/PORT-LEARNINGS.md`.
-95
View File
@@ -1,95 +0,0 @@
# BRIEF C2-GDL — model/prop GDL runtime relocation misaligns dst (Facility, Runway)
Repo: `REPO_ROOT` (git `master`, Windows).
## READ FIRST
- `docs/HANDOFF.md` top, `docs/PORT-LEARNINGS.md` **§B (16-byte PC Gfx vs
8-byte N64 — this is the class)** + §D.
- `docs/PCPortResearch.md` §F **D124** (the C2 split — this brief is the
unfinished Facility half), **D80, D82, D83, D85, D95, D58, D50.6**
(Gfx-stride / GDL history — jump via §F index).
- `AGENTS.md` non-negotiables. `rsp/graphics/gmain.s` = GBI ground truth.
- PD analogue: `PD_PORT_CHECKOUT` — its model-GDL /
texture relocation path. Check before writing anything.
## SYMPTOM (WS4 re-sweep — `docs/LEVEL-STATUS.md`)
Facility `-level_34` (fault 0x72181ee8) and Runway `-level_35` (fault
0x721b8ee8) both crash in `import_texture_i8` (`port/fast3d/gfx_pc.cpp:821`)
— fast3d fed a garbage `G_SETTIMG` w1.
Per the D124 investigation: the bad `G_SETTIMG` (`fmt=4 siz=2 w=0`,
preceded by an unconsumed `0xba` GE tex-macro opcode) sits in a
model/prop GDL relocated at runtime by `texLoadFromGdl()` via
`sub_GAME_7F0762E0` (`objecthandler_2.c:82`) — NOT a room GDL, NOT the
global bank. A `GE_C2` probe showed `texLoadFromGdl` writing to
**non-16-byte-aligned `dst`** (`0x701eac01`, `0x701eb7d5`, …): the
`replacementgdl` / `name`(=srcsize) offset arithmetic still mixes N64
8-byte and PC 16-byte `Gfx` strides, so converted commands land mid-slot
and a subsequent `G_SETTIMG` w1 reads as garbage.
12 levels PASS — they either don't hit this model-GDL path or hit it with
a GDL whose misalignment happens to be benign.
## TASK
Pin the exact stride/offset error in the model-GDL runtime relocation and
fix it narrowly (`#ifdef PORT`, N64 line under `#else`, §F-documented).
Likely spots:
- `src/game/tex.c` `texLoadFromGdl()` — src/dst pointer advance per Gfx.
- `src/game/objecthandler_2.c` `sub_GAME_7F0762E0` — `replacementgdl` /
`srcsize` math, GDL buffer sizing.
- `src/game/model.c` `modelNodeReplaceGdl` / rwdata GDL slot stride.
- The `0xba` (`G_GEGBIMACRO_*` / GE tex-load macro) decode — fast3d may
need to consume its extra word (cf. gmain.s).
Use the existing `GE_D86` / model-node probes and add a capped `GE_C2GDL`
probe if needed.
## FILES YOU MAY TOUCH
`src/game/tex.c`, `src/game/objecthandler_2.c`, `src/game/model.c`
(narrow `#ifdef PORT`); `port/fast3d/gfx_pc.cpp` (only a guard / the 0xba
consume, if that's the cause); new `tools_pc/c2gdl_*` probes;
`docs/PORT-LEARNINGS.md`, `docs/PCPortResearch.md` §F (extend D124 or new
Dxx), `docs/LEVEL-STATUS.md`.
**NOT** `src/game/propobj.c` / `prop.c` / `tools_pc/d88*` (C3 agent owns
those), **NOT** `port/src/gimgfixup.c` (D124-Jungle, done).
## KNOWN-GOOD / RULED OUT
- D123 (C1), D124-Jungle (`gimgfixup.c`) — landed, work.
- 12 levels PASS — the common GDL path is fine.
- Global-bank / explosion-DL sync (D124) — separate, fixed.
- HUD/text X-mirror (D114/D116) — parked, do NOT re-trace.
## PRE-FLIGHT
- `export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final` — green.
- Repro `./build-pc/ge007.x86_64.exe -level_34`. `ge007.crash.log`;
`addr2line -e build-pc/ge007.x86_64.exe -f -C 0x<PC>`.
- `GE_PCDUMP="80-260:40"` → `./ppm/`; `python tools_pc/pixcount.py`.
- `timeout` does NOT kill on Windows: `... &`, `sleep 24`,
`taskkill //F //IM ge007.x86_64.exe`.
- gdb attach fast (`gdb -batch -x cmds.txt -p <winpid>`). HW watchpoint on
the `dst` cursor in `texLoadFromGdl` catches the mis-advance.
- Regen sidecars after any converter change (none expected here):
`python tools_pc/d43_emit.py ntsc-final && python tools_pc/d69_emit.py ntsc-final && python tools_pc/d88_emit.py ntsc-final --regen`.
## BUDGET
Structural / shared-infra → **~14 build→run→inspect cycles or ~2 h.** On
expiry: revert temp probes, write up the stride spec + best hypothesis in
§F with a confidence rating + files touched. A good half-solved write-up
is a deliverable.
## CONSTRAINTS
No game-logic / control-flow changes. ABI/layout/format only, `#ifdef
PORT` with N64 line under `#else`, §F-documented. Revert your own probes.
Don't touch the C3 agent's files.
## REPORT
1. Root cause — `file:line`, the exact stride/offset bug, why 12 levels
are unaffected.
2. Fix diff or why-not.
3. Verification — `-level_34` + `-level_35` boot to ≥1 non-degenerate
frame (`pixcount.py` > a few %); `-level_09` + `-level_20` unregressed
(`framediff.py`); re-check Jungle `-level_37` + Streets `-level_29`
(may improve).
4. Probes left in tree.
5. Confidence rating.
6. Generalisable quirk appended to `docs/PORT-LEARNINGS.md`.
-62
View File
@@ -1,62 +0,0 @@
# BRIEF C3+C6 — prop/door/item model-load crashes (Aztec, Bunker2, Surface2)
Dispatch AFTER C1 lands (shares `tools_pc/d88_propdefs.py` +
`src/game/prop*.c` with C1 — do not run concurrently with C1).
Repo: `REPO_ROOT` (git `master`, Windows).
## READ FIRST
- `docs/HANDOFF.md` top, `docs/PORT-LEARNINGS.md` §A + §C (esp. the D122
"polymorphic-record converter with a forgotten type" note).
- `docs/PCPortResearch.md` §F **D122, D120, D112, D53.2, D57, D67**
(jump via index).
- `docs/BRIEF-D122-per-level-modelload.md` — direct predecessor, same class.
- `AGENTS.md` non-negotiables.
## SYMPTOM (WS4 sweep — `docs/LEVEL-STATUS.md`)
| Level | # | Site | Note |
|---|---|---|---|
| Aztec | 28 | `propobj.c:13601` `door->model->obj->RootNode->Child->Child` | fault 0x10 |
| Bunker2 | 27 | `propobj.c:13523` door displacement `linkedDoor` list walk | fault 0x8 |
| Surface2 | 43 | `loadobjectmodel.c:393` `PitemZ_entries[modelid].header->RootNode` | fault 0x0 — direct D122 continuation |
All three: a prop/door/item model pointer or model-id from a `Usetup*Z`
propDef record is wrong. D122 fixed the `[s16 obj][s16 pad]` half-swap for
6 `inherits ObjectRecord` types; these levels emit a door subtype or
another record type whose `model` / `linkedDoor` / `modelid` field is
still converted wrong (or a placeholder-sized type:
`VEHICLE`/`AIRCRAFT`/`TANK`/`AMMO`/`DEPOSIT_IN_ROOM` per
`d88_propdefs.py`). Run `tools_pc/d88_propdef_scan.py` for the per-level
type histogram; diff Aztec/Bunker2/Surface2 against the 7 passing levels.
## TASK
Root-cause all three (likely one converter gap), extend
`tools_pc/d88_propdefs.py` (+ `sizepropdef()` `#ifdef PORT` stride in
`src/game/prop.c` to match, per the D122 pattern). Resolve any
placeholder `PROPDEF_PC_BYTES` via `tools_pc/d88_layoutprobe.c`.
## FILES YOU MAY TOUCH
`tools_pc/d88_propdefs.py`, `d88_emit.py`, `d88_layoutprobe.c`,
`d88_propdef_scan.py`; `src/game/prop.c`, `src/game/propobj.c`,
`src/game/loadobjectmodel.c` (narrow `#ifdef PORT`); new `tools_pc/c3_*`
probes; `docs/PORT-LEARNINGS.md`, `docs/PCPortResearch.md` §F,
`docs/LEVEL-STATUS.md`.
## KNOWN-GOOD / RULED OUT
D121, D122 (committed `f2beae4b`), the 7 passing levels' prop/model load,
C1's chr fix (landed first — rebase on it).
## PRE-FLIGHT / ENV / BUDGET / CONSTRAINTS / REPORT
Same as `docs/BRIEF-C1-chr-airecord.md` (repro `-level_28` / `-level_27` /
`-level_43`; `taskkill //F //IM ge007.x86_64.exe`; regen all 3 sidecars
after converter change; `#ifdef PORT` with N64 line under `#else`; verify
the 3 levels boot to a frame + `-level_09`/`-level_20` unregressed).
Budget: **~12 cycles / ~90 min.**
## STILL OPEN AFTER THIS (singletons, own briefs later)
- **C4** Depot 30 — `prop.c:902` `sp4C->room` after
`walkTilesBetweenPoints_NoCallback` (BG tile/room table).
- **C5** Control 23 — `bg.c:5723` `portal_pts->numPoints`
(`g_BgPortals[].offset_portal` unresolved/BE), via `chrprop.c:62`.
- **C7** Surface1 36 — `snd.c:653` `sndSetupSound` (audio subsystem is
parked; may just need a narrow load-time guard until Phase 3).
-165
View File
@@ -1,165 +0,0 @@
# BRIEF D122 — per-level prop/item model-load crash (blocks the level sweep)
Repo: `REPO_ROOT` (git, branch `master`, Windows;
PowerShell + Bash tool available).
## READ FIRST
- `docs/HANDOFF.md` (top) — current state, environment, repro line.
- `docs/PORT-LEARNINGS.md` — recurring bug classes. **A / C are the
prime suspects here** (pointer-width struct growth; BE rodata on LE).
- `docs/PCPortResearch.md` §F index (top) + entries **D43, D53.2, D56,
D57, D67, D68, D39, D108–D112, D120** — model/asset conversion +
pointer-width history. Jump to each via the index; do NOT linear-read.
- `AGENTS.md` non-negotiables (esp. #2 game logic unmodified — narrow
`#ifdef PORT` ABI/layout/format edits only, each logged in §F).
## CONTEXT (established this session, M-12)
- **WS1 landed** (commit `02c12068`, D121): a bare
`./build-pc/ge007.x86_64.exe -level_XX` now auto-injects that stage's
`memallocstringtable[]` pool row. Every solo level boots with correct
pools now — no manual `-m*` needed.
- **D88.4 (propDefs converter) is effectively done for the 21 solo
levels.** `tools_pc/d88_emit.py` + `d88_propdefs.py` are committed
(`ff563812`, `4ccc0d3c`); `python tools_pc/d88_emit.py ntsc-final
--regen` reports `21/21 converted … ALL CHECKS PASSED`. The placeholder
`PROPDEF_PC_BYTES` for VEHICLE/AIRCRAFT/TANK/AMMO are **not exercised by
any solo level** (`tools_pc/d88_propdef_scan.py` histogram — no
VEHICLE/AIRCRAFT/TANK in any of the 21). So D88.4 is NOT the blocker.
- **The actual blocker: prop/item model loading crashes on most levels.**
Smoke test (each `-level_XX`, `GE_PCDUMP="60-240:60"`, ~25 s):
| level | # | result |
|---|---|---|
| Bunker1 | 09 | renders, playable (known good) |
| Silo | 20 | renders ~91% frame content, no crash |
| Dam | 33 | **CRASH** `modelInitRwData` (`src/game/model.c:6249`) — `node->Opcode` deref, bad `node` in the ModelNode walk |
| Facility | 34 | **CRASH** `modelLoad` (`src/game/loadobjectmodel.c:393`) — `PitemZ_entries[modelid].header->RootNode`, bad `header` (or OOB `modelid`) |
| Runway | 35 | **CRASH** same as Facility (`loadobjectmodel.c:393`) |
Crashes are **early and deterministic** (before frame 1 renders), same
PC every run — distinct from BUNKER1's rare intermittent
`loadobjectmodel.c:393` (D117 nondeterminism). `EXCEPTION 0xc0000005`,
`FAULT ADDR: ffffffffffffffff`. `Rax` at the Facility crash holds ASCII
bytes ("…o ng nol…") — looks like a model/texture name string landed in
a register, consistent with a mis-strided table read.
## TASK
**One question: why do prop/item models load fine for BUNKER1/Silo but
crash for Dam/Facility/Runway, and what is the narrow fix?**
Almost certainly one of:
1. `PitemZ_entries[341]` (`struct ItemModelFileRecord { ModelFileHeader
*header; char *filename; float scale; }`, `src/bondtypes.h:1466`) —
table stride or the `header`/`filename` pointer values wrong on PC.
Find where this array is **defined** (it is `extern` everywhere I
looked — `src/game/prop.c:40`, `chrobjdata.h:20`; the definition is
likely a generated `.s`/rodata blob or an asset — `grep` harder,
check `port/src/romassets_*.s`, `scripts/gen_romassets.py`,
`scripts/gen_*`). If the `header` pointers come from BE ROM rodata
they need rebasing/byteswap (D39 Globalimagetable / D68 class).
2. `modelid` passed to `modelLoad` is out of range / unswapped — it comes
from a propDef `ObjectRecord`-family `modelid` field. If a record type
used by Dam/Facility props (but not BUNKER1) has its `modelid` at a
field offset the `d88_propdefs.py` converter byteswaps wrongly (or
not at all), `modelid` is garbage. Cross-check the per-type field
maps in `tools_pc/d88_propdefs.py` against the record structs.
3. The `pcmodels` sidecar (`tools_pc/d43_emit.py`, loaded at
`0x10C00000`, "512 sidecars") is missing/mis-converting a model
feature (ModelNode opcode, `PointUsage[]` chain — cf. D120,
`d43_emit.py` opcode-0x18 gap) that Dam/Facility prop models use.
`model.c:6249` already has a `GE_D86` env probe for the node walk —
use it.
Determine which, root-cause, and apply the **narrowest** fix (converter
extension preferred over runtime fixup for a whole format — D43/D88
pattern; or a narrow `#ifdef PORT` ABI edit with the N64 line kept under
`#else`).
## FILES YOU MAY TOUCH
- `src/game/model.c`, `src/game/loadobjectmodel.c`, `src/game/prop.c`
(narrow `#ifdef PORT` only).
- `tools_pc/d43_emit.py` (+ `tools_pc/d43_*.py` helpers, `d43_layoutprobe.c`).
- `tools_pc/d88_propdefs.py` / `tools_pc/d88_emit.py` (only if the cause
is a propDef `modelid` field).
- `port/src/` model/asset shims if the table is patched at runtime like
`pcmodelsPatchTable` (`grep pcmodelsPatchTable`, `port/src/romdata*.c`).
- New probe files: `tools_pc/d122_*.py` / env-gated `GE_D122` blocks.
- `docs/PORT-LEARNINGS.md` (append any new generalisable quirk),
`docs/PCPortResearch.md` §F (write up as **D122**, add to the index).
Nothing else is running — no file collisions to worry about.
## KNOWN-GOOD / RULED OUT — do not re-investigate
- WS1 per-level pool injection (D121) — works, committed.
- D88.4 propDefs stream conversion for solo levels — works (21/21).
- BUNKER1 + Silo prop/model loading — works.
- The intermittent BUNKER1 `loadobjectmodel.c:393` crash — that is
D117-class nondeterminism, separate; do not chase it, use the
deterministic Dam/Facility/Runway repro.
- `d88_propdef_scan.py` "STOP … unmapped type NOTHING" — that is the
*scan tool's* limitation, not a converter bug (`d88_emit.py` completes).
## PRE-FLIGHT (done — results above)
- `./build-pc.sh ntsc-final` green; binary at `build-pc/ge007.x86_64.exe`.
- Sidecars regenerated this session (`d43_emit.py`, `d69_emit.py`,
`d88_emit.py --regen`) — all pass.
- Smoke-test table above.
## ENVIRONMENT
```sh
export PATH="/c/msys64/mingw64/bin:$PATH"
./build-pc.sh ntsc-final # ~5 s, from repo root
./build-pc/ge007.x86_64.exe -level_34 # Facility repro (bare — D121)
```
- `timeout` does NOT reliably kill the game on Windows. Launch with
`… &`, `sleep N`, then `kill $PID; pkill -f ge007`.
- Crash log: `ge007.crash.log` (repo root). Symbolicate:
`addr2line -e build-pc/ge007.x86_64.exe -f -C 0x<PC>` (image base
`0x140000000`; PC in the log is already absolute).
- Frame capture: `GE_PCDUMP="a-b:step"` → `./ppm/`; content check
`python tools_pc/pixcount.py ppm/frame_XXXX.ppm`.
- gdb: **attach** mode is fast (`gdb -batch -x cmds.txt -p <winpid>`,
winpid = 4th col of `ps -p <bashpid>`; game must already be running).
Launch mode is too slow. A hardware watchpoint on `PitemZ_entries` or
the bad `node` catches the corrupting write fast.
- Standalone probe compiles: `-std=c11`.
## BUDGET
Structural bug → generous: **~12 build→run→inspect cycles or ~90 min**.
On expiry: revert all temp probes (leave only committed env-gated ones),
write up the spec progress + best hypothesis in `docs/PCPortResearch.md`
§F as **D122** with an explicit confidence rating, and list exactly which
files you changed. A good half-solved write-up is a deliverable.
## CONSTRAINTS
- No game-logic / control-flow changes. ABI/layout/format only, each
`#ifdef PORT` with the N64 line kept verbatim under `#else`, each
documented in §F/D122.
- Prefer an offline sidecar-converter extension over a runtime fixup for
a whole asset format.
- Revert your own temp probes before reporting.
- Regenerate sidecars after any converter change:
`python tools_pc/d43_emit.py ntsc-final && python tools_pc/d69_emit.py
ntsc-final && python tools_pc/d88_emit.py ntsc-final --regen`.
## REPORT
1. Root cause — `file:line` evidence, which of the 3 hypotheses (or
other), why Dam/Facility/Runway differ from BUNKER1/Silo.
2. Fix — the diff, or why-not if unresolved.
3. Verification — `-level_33`, `-level_34`, `-level_35` each boot to ≥1
non-degenerate frame (`pixcount.py` > a few %), AND `-level_09`
(BUNKER1) still renders (no regression — `tools_pc/framediff.py`).
4. Probes left in the tree (env-gated, capped).
5. Confidence rating.
6. Append any generalisable quirk to `docs/PORT-LEARNINGS.md`.
-191
View File
@@ -1,191 +0,0 @@
> **RESOLVED — historical.** D69 is closed (see PCPortResearch.md §F/§H
> D69, D78–D84). Kept for the bg/stan format spec. Not active work.
# Brief — M1 / D69: Stage (BG/stan) loading for PC
_Paste-ready milestone brief. Authoritative context: `AGENTS.md`,
`docs/HANDOFF.md`, `docs/PCPortResearch.md` §F/D69 (+D68), §G, §H._
## Mission
Get the first stage (**BUNKER1**) to load and render after the intro, without
faulting. Then extend the same conversion to **all levels'** `bg/*.seg` +
`Tbg_*_stanZ` files (all three regions). Success bar: **loads without fault +
plausible pixels** — not pixel-perfect (3D pipeline correctness is a separate
milestone, D75; do not let visual wrongness displace this one).
## Non-negotiables (from AGENTS.md)
1. N64 build untouched: `Makefile`, `tools/`, `rsp/`, `ld/`.
2. Game logic unmodified. Only narrow `#ifdef PORT` ABI/layout exceptions,
each documented as its own finding in §F. **The intended solution for this
task requires ZERO game-code edits** — pure offline data conversion +
port-layer table patch. If you believe a game-file edit is needed, stop and
document why as a finding before making it.
3. Offline sidecar conversion preferred over runtime fixup (AGENTS.md; the
D43/Plan-B precedent in `tools_pc/d43_emit.py` + `port/src/pcmodels.c`).
4. The PD port (`PD_PORT_CHECKOUT`) is an *approach*
reference only — its BG format differs (zipped multi-section). Validate
every field against GE's own consumers (`bg.c`, `stan.c`, N64 disasm).
## Current state (verified)
- Build GREEN: `export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final` (~5 s).
- Game boots → full intro (Rareware logo, gun barrel w/ Brosnan, cast; ~frame
2100) → crashes in `load_bg_file`.
- Crash: EXCEPTION 0xc0000005; symbolicated PC = **`load_bg_file`,
src/game/bg.c:865**; the crash stack contains BG header words
`0x0F000334` / `0x0F00032C` — confirms the BE-offset misread below.
## The bug, precisely
- BG-file internal offsets are N64 **big-endian u32s in segment-0x0F form**
(`0x0Fxxxxxx`).
- `BG_SEG_TO_PTR` (bg.h:23):
`((void*)(((u32)(base)) + (((u32)(off)) + 0xF1000000)))` — on N64,
`off = 0x0Fxxxxxx` and `+ 0xF1000000` wraps in 32-bit arithmetic to the
plain file offset `0x00xxxxxx`.
- On PC, `((s32 *)ptr_bg_data)[1]` reads LE: ROM bytes `0F 00 00 14` become
`0x1400000F`; the fold yields `base + 0x0500000F` ≈ 5 MB past the header
buffer → fault dereferencing `[1].pPointTableBin`.
- Scope: every bg `.seg` header word and record (room file-position list,
portals, etc.) **and** the whole `Tbg_*_stanZ` geometry file
(`StanPrefixRecord` + everything `stanDetermineEOF`/`stanLoadFile` walk).
Same class as D68 (image table) but a far larger format surface.
## Known data points (BUNKER1)
- Per-level filenames: `levelinfotable` (bg.c ~line 183) →
`bg_seg_filename` / `bg_stan_filename`. Name→symbol map:
`assets/obseg/file_resource_table.inc.c` (e.g. line 6
`"bg/bg_sev_all_p.seg"` → `&bg_sev_all_p_seg`; line 586
`"Tbg_sev_all_p_stanZ"` → `&Tbg_sev_all_p_stanZ`).
- BUNKER1 files: `bg/bg_sev_all_p.seg` @ cart **0x10438660**, size **0x10DF0**;
`Tbg_sev_all_p_stanZ` @ cart **0x1087C3F0**, size **0x3DD0**.
- Header words (BE values): word0 = `0x00000000`; word1 = `0x0F000014`
(room file-position list @ file offset 0x14); word2 = `0x0F000334`;
word3 = `0x0F00032C`.
- `bg_room_data` records: `pPointTableBin` at record+0x28 (per crash disasm),
more `0x0Fxxxxxx` offsets per record.
- Load path: `obLoadBGFileBytesAtOffset(name, buf, offset, size)` does partial
loads via memcpy from `&fileentry->hw_address[offset]`; stan files load via
`_fileNameLoadToBank(..., bank 2)`. Both already work on PC — the bug is
**interpretation only**, so redirecting `hw_address` at converted bytes is
sufficient.
## Task 1 — decode the formats (write notes as you go)
1. Dump BUNKER1's `.seg` header + first records from the ROM; map every word
to its consumer in `bg.c` (`load_bg_file` ~line 795+ and the table walks it
performs: rooms/portals/bgcmds/lights). Note which words are
`0x0Fxxxxxx` offsets vs plain data.
2. Same for `Tbg_sev_all_p_stanZ`: `StanPrefixRecord` + whatever
`stanDetermineEOF`/`stanLoadFile` walk (`src/game/stan.c`). Identify vertex
batches, bounds (cf. `RoomVtxBatchBounds` in bg.h), Gfx/DL references, and
texture refs inside stan data.
3. **Classify every field:** u32 numeric (bswap32 for PC) / s16 numeric
(bswap16) / byte stream (leave alone — compressed payloads etc.). Where the
decomp is ambiguous, cross-check struct field offsets against the N64
disasm of the consumer function.
4. Write the format notes into `PCPortResearch.md` §F as you go (labels
D78+) — they are the converter spec and must be complete enough to
regenerate the converter from them.
## Task 2 — implement offline conversion (Plan-B pattern)
**Key design constraint: in-place, size-preserving conversion.** If every
converted file is byte-identical in *size* to its ROM original, all internal
`0x0Fxxxxxx` offsets remain valid after `BG_SEG_TO_PTR` folding, and the game
code needs **zero changes** — bswapped words read back as the original BE
values. (bswap32/bswap16 are size-preserving by construction; byte streams
pass through untouched.)
1. New converter `tools_pc/d69_emit.py` (model on `d43_emit.py`): per region,
for every `bg/*.seg` + `Tbg_*_stanZ` referenced by `levelinfotable`:
extract from ROM → convert per the Task 1 spec → concatenate into
`data/pccg-<region>/pccg.bin` + `manifest.csv` (`name,offset,size`,
decimal — same format as pcmodels).
2. Port layer: clone the `port/src/pcmodels.c` pattern (it is the template —
read it first): sidecar image lives in the cart reservation extension at
`[CART_BASE + romSize, ...)`; a one-shot table patch matches
`file_resource_table[]` by filename and rewrites `hw_address` +
`rom_size`. Either extend `pcmodels.c` or add `port/src/pccg.c`. Wiring
points (all small):
- `port/src/romdata.c` ~line 165: include the pccg size in the
`VirtualAlloc(CART_BASE, romSize + sideTotal)` reservation; load the
sidecar next to `pcmodelsLoadSidecars`.
- `port/src/romdata.c` ~line 253 (`romdataCartAddrValid`) and
`port/src/libultra.c` ~line 821 (DMA source validation, D60 safety net):
extend the bounds check to cover pccg bytes.
- One-shot patch call site: same pattern as
`load_object_fill_header → pcmodelsPatchTable()` (or hook the BG load
path's first use — your choice, document it).
3. Regenerate per region (`ntsc-final` / `pal-final` / `jpn-final`); add
`data/pccg-*/` to `.gitignore` like `pcmodels-*`.
**Fallback:** if the decode shows only a handful of fields matter for
first-frame render, a runtime port-layer fixup hook (D68-style bswap after
load) is acceptable as an *interim* step — but stage geometry is deep; expect
most fields to need conversion anyway. Document the choice as a finding.
## Task 3 — verify
1. **Clean run from the repo root** (`cd <repo> && ./build-pc/ge007.x86_64.exe`)
— running from `build-pc/` picks up the wrong ROM and misses sidecars
(spurious early crash). Expect: intro → BUNKER1 loads without fault; stage
geometry visible (plausible pixels, not pixel-perfect).
2. `GE_PCDUMP="2100-2400:5"` frames + `tools_pc/pixcount.py` — confirm
non-black stage content past the old crash point.
3. **All levels:** converter must cover every bg/stan file in
`levelinfotable` (20+); the table patch logs its hit count — verify it
matches the manifest row count exactly.
4. Verification ritual (AGENTS.md): build green; no new undefined/duplicate
symbols (`/linkcheck`); every touched file parses.
5. Strip TEMP probes before committing (keep the `GE_D69` probe in `ob.c`
until resolved, then strip it per the HANDOFF Task 3 list).
## Acceptance criteria (all must hold)
- [ ] Clean run reaches BUNKER1 without fault (no new `ge007.crash.log` past ~frame 2100).
- [ ] Stage renders plausible geometry (PPM-verified, non-black content).
- [ ] Converter covers all levels' bg/stan files; all three regions regenerable from the script.
- [ ] Zero game-code edits — or any `#ifdef PORT` edit individually justified and documented as a D7x finding.
- [ ] Format spec written in `PCPortResearch.md` §F (D78+).
- [ ] Build green + `/linkcheck` clean; TEMP probes stripped.
## Environment gotchas
- MSYS2 tools: `export PATH="/c/msys64/mingw64/bin:$PATH"`. Build ~5 s.
- Run the game from the **repo root**, not `build-pc/`.
- Crash log `ge007.crash.log` (repo root) is the first stop for any fault;
symbolicate offline: `addr2line -e build-pc/ge007.x86_64.exe -f -C <pc>`
(image base 0x140000000).
- gdb is launch-mode only and far too slow for timing-dependent issues —
prefer env-gated probes + the crash log.
- Heavy logging perturbs tick-driven timing (`osGetCount` advances at
~46.5525 ticks/µs) — diagnose stability with clean runs only; use short,
targeted probe windows.
- `Gfx` is 16 bytes on PC; ROM-copied N64 DLs stay 8 bytes in memory — walk
them at 8-byte stride when scanning.
- N64 CPU/RDP data in ROM is big-endian; any raw `*(u32*)` read of a
ROM-copied buffer needs bswap under PORT (D68 pattern) or offline conversion.
- **Don't "fix" the s32 address idioms.** The decomp passes addresses through
`s32`/`u32` (e.g. `ptr_bg_data = (s32)header`); this works because all
emulated addresses (cart 0x10000000, DRAM 0x70/0x80xxxxxx) and host
stack/heap fit in the low 4 GB, so truncation is lossless. Changing these to
64-bit types breaks the `BG_SEG_TO_PTR` fold and is out of scope.
- Standalone probe compiles need `-std=c11`.
## Out of scope (do not let these displace the milestone)
- D75 (3D matrix/model bugs), D76 (copyright-screen 2D partial draw),
D77 (no audio). The stage may render *wrong* — that is expected; the bar is
loads-without-fault + plausible pixels.
- Pixel-perfect intro-logo check vs N64 footage.
## Documentation requirements
- New findings: `PCPortResearch.md` §F, labels **D78+** (D75–D77 are taken).
- Update §G status and `docs/HANDOFF.md` at each sub-milestone.
- Commit per sub-milestone: (1) format spec notes, (2) converter + sidecar,
(3) port wiring, (4) verification + probe strip.
-116
View File
@@ -1,116 +0,0 @@
# Local-agent handoff tasks (Qwen 27B) — M-20
The two M-19 tasks (D63 scaffolding removal in `libultra.c`; PORT-LEARNINGS
§E entries) are **done and committed** (`1fc3cff6`). Below are three fresh
tasks.
**All three are pure static analysis** — read code, produce a table, write
a doc section. **No builds. No running the game. No `.c` edits.** Deliverable
is a markdown write-up appended to `docs/PCPortResearch.md` §F (add the new
label to the §F index at the top of the file) plus, where noted, one line in
`docs/PORT-LEARNINGS.md`.
Disjoint from the active session work (intro-render investigation in
`port/fast3d/`, propDef converter audit in `tools_pc/d88_propdefs.py`). **Do
NOT touch `port/fast3d/*`, `tools_pc/d88*`, or any `.c`/`.cpp` file.**
READ FIRST for all tasks: `docs/PORT-LEARNINGS.md` (whole file — it is one
screen), then the specific §F entries named in each task (jump via the §F
index; do NOT linear-read `PCPortResearch.md`).
---
## Task A — `osVirtualToPhysical()` latent-truncation site audit (D131 corollary)
**Background:** `docs/PORT-LEARNINGS.md` §A "D131 corollary". The port shim
`osVirtualToPhysical(va)` returns `(u32)(uintptr_t)va`, which silently drops
the high 32 bits of a **compiled module symbol** pointer (`.bss` / `.rodata`
address, image-based at `0x140000000`). D131 fixed the one live instance
(`explosionRenderPropSmoke`) centrally in `seg_addr()` in
`port/fast3d/gfx_pc.cpp`, which now restores the module high word for a
fallthrough w1 in `[0x40000000, 0x70000000)`.
**Task:** grep every call site of `osVirtualToPhysical(` across `src/`
(`grep -rn 'osVirtualToPhysical(' src/`). For each, produce a table row:
| file:line | argument expression | arg is: runtime-DRAM ptr / compiled symbol / segmented / unknown | reaches a GBI w1 (`gSPMatrix`/`gSPVertex`/`gSPDisplayList`/`gDma*`)? | covered by D131 `seg_addr` fix? | residual risk |
Classify "compiled symbol" when the argument is `&someGlobal`, a `.bss`/
`.rodata` array/struct address, or a function-local `static`. Classify
"runtime-DRAM ptr" when it is the result of a heap/bank allocation
(`mempAlloc*`, `malloc`, a `->` deref of loaded data). Note which are armed
only when a specific effect first draws (explosion / glass / blood /
bondview2 — the §A note lists these).
**Deliverable:** the table + a short "residual risk" summary (are there sites
that reach fast3d NOT through `seg_addr`, e.g. a raw `memcpy` or a
non-w1 use?) → `docs/PCPortResearch.md` §F, new subsection
**"D131 corollary — osVirtualToPhysical site audit (M-20)"**, label added to
the §F index.
**On expiry:** commit the partial table.
---
## Task B — in-place ROM-struct relayout aliasing audit (D130 corollary)
**Background:** `docs/PORT-LEARNINGS.md` §A "D130 corollary". `romdataFixupFont`
(`port/src/romdata.c`) re-lays-out a ROM struct array (`fontchar` 24→32B) *in
place*: `dst = base + PCstride*i`, `src = base + N64stride*i`, and for small
`i` the two overlap by less than the per-element read span, so a
field-by-field copy loop clobbers a not-yet-read source field. D130 fixed
that one loop by staging all fields into locals first.
**Task:** find every other in-place N64→PC struct-array relayout. Search:
`grep -rn 'romdataFixup\|Fixup\|relayout\|in.place\|24.*32\|->32B' port/src/`
and read `port/src/romdata.c` fully (it is the main offender file). For each
loop that (a) reads struct fields from a source offset and writes to a
different-stride destination **in the same buffer**, tabulate:
| file:line (fn) | struct | N64 stride | PC stride | loop direction | dst/src overlap for low i? | reads all fields before writing? | verdict (SAFE / ALIASING BUG / needs staging) |
**Deliverable:** the table → `docs/PCPortResearch.md` §F, new subsection
**"D130 corollary — in-place relayout audit (M-20)"**, label in the §F index.
If you find a concrete ALIASING BUG, describe the fix (stage fields into
locals, as D130 did) in prose in the write-up — **do not edit the `.c`
file**; flag it for the integrator.
**On expiry:** commit the partial table.
---
## Task C — negative / OOB glyph-index audit (PORT-LEARNINGS §A last bullet)
**Background:** `docs/PORT-LEARNINGS.md` §A final bullet: `chars[*text - 0x21]`
with a control byte (`*text < 0x21`) indexes before `chars[0]`. Benign on
N64 (hits the adjacent 4-byte kerning table → garbage-but-valid glyph);
**fatal on PC** — the widened `fontchar` has an 8-byte `pixeldata` at a
higher offset, so a negative index reads a wild pointer → fast3d AV. No
level is known to feed a control byte to HUD text yet, so this is latent.
**Task:** in `src/game/textrelated.c` (and any other file that indexes a
`fontchar`/`chars` array — grep `chars[` and `- 0x21` / `- 33` across
`src/game/`), enumerate every glyph-index expression on the `textRender*` /
`textMeasure*` ASCII paths. For each, tabulate:
| file:line (fn) | index expression | can the byte be `< 0x21` or `>= last glyph`? (what feeds it — dialogue? HUD? menu? filename?) | guarded already? | consequence on PC if OOB |
**Deliverable:** the table + a recommendation: where exactly a
`GLYPH_IDX` clamp macro (`clamp to [0, nglyphs-1]`, or skip the glyph) would
go, expressed as a prose description of the one-line `#ifdef PORT` guard per
site — **do not edit `.c`**. → `docs/PCPortResearch.md` §F, new subsection
**"Glyph-index OOB audit (M-20)"**, label in the §F index. Add one terse
line to `docs/PORT-LEARNINGS.md` §A noting the audit is done and where the
clamp sites are.
**On expiry:** commit the partial table.
---
## Verify (all tasks)
`git diff --stat` shows **only** `docs/PCPortResearch.md` and (Task C)
`docs/PORT-LEARNINGS.md` changed. No other files. Markdown still renders.
`git status` shows no deleted files. Commit message per task, e.g.
`docs: M-20 Qwen — D131 corollary osVirtualToPhysical site audit`.
**No `Co-Authored-By` / `Claude-Session` trailers** (repo policy).
-157
View File
@@ -1,157 +0,0 @@
> **RESOLVED — historical.** D7 closed at the compile+link milestone.
> Kept for the AI X-macro analysis. Not active work.
# HANDOFF — AI X-macro system (D7), the last build blocker
## Status
Full `ge007` build: **224 objects compile; only `chraidata.c` fails.** All other
`.c` files build clean. Findings D1–D6 are resolved and recorded in
`docs/PCPortResearch.md` §11. D7 (this file) is the remaining blocker.
Build command:
```sh
export PATH="/c/msys64/mingw64/bin:$PATH"
cd build-pc && cmake --build . --target ge007 2>&1 | grep -A3 -i "chraidata\|aicommands\|bondaicommands"
```
## The failing file and its includes
`src/game/chraidata.c` includes (lines 14–17):
```c
#include <ultra64.h>
#include <bondgame.h>
#include <bondaicommands.h> // <- pulls in the whole AI X-macro system
#include "chrobjdata.h"
```
`bondaicommands.h` includes:
- `bondconstants.h` (which at line 17 includes `<CPPLib.h>`, and at line 731
includes `"aicommands.def"` to build the `AI_CMD` enum)
- `aicommands2.h` (line 864) — a **pre-generated** header (from
`tools/cmdbuilder.c`), already preprocessed.
So the failing path is the **raw** `aicommands.def` include (bondconstants.h:731)
plus the `bondaicommands.h` macros (`isSubroutine`, `SWITCH`, `CALL`, …) that
chraidata.c uses directly.
## Exact error chain (verified by rebuild)
Trigger: `CALL(GAILIST_PLAY_IDLE_ANIMATION)` at chraidata.c:114 (and similar at
123, 126, …). The chain:
1. `CALL(...)` expands to code that calls `isSubroutine(<ID>)`.
2. `isSubroutine(ID)` (bondaicommands.h:275–284) contains:
```c
#define isSubroutine(ID) ((ID == GAILIST_PLAY_IDLE_ANIMATION) |\
(ID == GAILIST_BASH_KEYBOARD) | \
(ID == GAILIST_ATTACK_BOND) | \
(ID == GAILIST_RUN_TO_BOND) | \
(ID == GAILIST_STARTLE_AND_RUN_TO_BOND) | \
(ID == GAILIST_WAIT_ONE_SECOND) \
IF(NOT(DEFINED(SETUPSUBROUTINES(ID))))\
(\
| SETUPSUBROUTINES(ID)\
))
```
3. `DEFINED(x)` (CPPLib.h:279–280):
```c
#define DEFINED(x) _DEFINED(x)
#define _DEFINED(x) IS_PROBE(CAT(_DEFINED, _##x##_))
```
With `x = SETUPSUBROUTINES(ID)`, the `##` pasting `_##x##_` tries to paste
`_` against `SETUPSUBROUTINES(ID)` — a **macro call (multiple tokens)**, not a
single pp-token. GCC hard-errors:
```
bondaicommands.h:281:63: error: pasting ')' and '_' does not give a valid
preprocessing token
```
4. CPPLib.h:287 has a special case that was *meant* to handle this:
```c
#define _DEFINED_SETUPSUBROUTINES(ID) PROBE(~),
```
i.e. the intent is that `_##SETUPSUBROUTINES(ID)##_` →
`_DEFINED_SETUPSUBROUTINES(ID)_` → the defined macro above. But GCC's `##`
requires each side to be a single pp-token, so it never reaches that special
case. IDO's preprocessor evidently tolerated / expanded this differently.
### Secondary errors (same root family)
- `chraidata.c:183/202/249/640`: `macro 'SWITCH' requires 49 arguments, but only
20/14/26/26 given`. The `SWITCH` macro (bondaicommands.h, ~line 776) has **49
fixed parameters** (1 VAR + 16×3 case slots) and uses `IF_VA(NOT(IS_EMPTY(..)))`
to detect which cases are present. Game code calls it with fewer args, relying
on IDO leniency. GCC enforces the exact arg count.
- `aicommands.def:514:270 / 550:21 / 2846:21`: "expected expression before ','"
/ "expected '}' before 'AI_Return'" / "expected '}' before
'AI_IFICouldSeeBond'". These are downstream of the `isSubroutine`/`DEFINED`
failure (the `CALL` expansion aborts mid-macro, leaving dangling tokens). Note
the reported line numbers (514/550/2846) do **not** match the raw
`aicommands.def` lines (those are comment/brace lines) — they are positions in
the *expanded* macro text, not the source file. Don't chase them as source
lines.
## Key facts / ground truth
- `aicommands.def` call counts: **255** `_AI_CMD(`, **119** `_AI_CMD_POLYMORPH(`,
**2** `_AI_DEBUG(`.
- `_AI_CMD_POLYMORPH` definitions across the tree:
- `aicommands.def:97` — 4-arg `(PRE, NAME, CMD_PARAMS_POST, POLY_PARAMS)` but
inside a `#if 0` block (lines 88–100) → **inactive**.
- `bondconstants.h:721` — 5-arg `(CMD, A, P, Q, D)` under `#ifdef __sgi`
(not us).
- `bondconstants.h:726` — **variadic `(...)`** under `#else` (this is the one
active for GCC when building the `AI_CMD` enum at bondconstants.h:731).
- `chrai.c:170` — 5-arg `(CMD, A, P, Q, D)`.
- `chrai.c:915` — 5-arg `(C, N, P1, P2, D)`.
- `aicommands.def:85–87`: `#ifndef __GNUC__ #include <CPPLib.h> #endif` → for GCC
CPPLib.h is **not** included from aicommands.def itself; it comes in via
`bondconstants.h:17`.
- `aicommands.def:11888`: `#undef _AI_CMD_POLYMORPH` (late in the file).
- `aicommands2.h` is pre-generated (header says "Do not edit … generated by
cmdbuilder from src/aicommands.def"). It is included by bondaicommands.h:864
and deb_loadallmodels.c:8. It is **not** the failing path.
- `isSubroutine` is the only macro that feeds a *macro call*
(`SETUPSUBROUTINES(ID)`) into `DEFINED()`. That is the unique trigger for the
`##` pasting error.
## Recommended next steps (in order)
1. **Fix the `DEFINED(SETUPSUBROUTINES(ID))` pasting** — the single root cause of
the `CALL`/`isSubroutine` errors. Preferred: a `port/` shim that redefines
`DEFINED` (or `_DEFINED`) *after* CPPLib.h is included, special-casing the
`SETUPSUBROUTINES(ID)` argument so it does not `##`-paste a multi-token
macro-call. Sketch:
```c
/* port/shim/... after <CPPLib.h> */
#undef DEFINED
#define DEFINED(x) _PORT_DEFINED(x)
#define _PORT_DEFINED(x) /* if x is SETUPSUBROUTINES(ID) -> PROBE(~), else fall back */
```
The tricky part: detecting "x is exactly `SETUPSUBROUTINES(ID)`" in the
preprocessor. Alternative: redefine `isSubroutine` in the shim to compute the
`SETUPSUBROUTINES` presence without `DEFINED()` (e.g. via a per-setup
`#define HAS_SETUPSUBROUTINES 0/1` flag set before the include). Investigate
which is less invasive before committing.
- Constraint: `bondaicommands.h` and `CPPLib.h` are game files — do **not**
edit them. All fixes go in `port/` (shim headers found first via include
path) or, if truly unavoidable, a `-DPORT`-gated `src/` change with a
`PC port:` comment.
2. **Fix the `SWITCH` 49-arg requirement** — make a shim `SWITCH` variadic (or
pad the call sites). Verify the `IF_VA`/`IS_EMPTY` case-detection still works
under GCC once variadic.
3. **Re-verify the aicommands.def:514/550/2846 errors disappear** once (1) and
(2) are in — they are likely downstream of the `CALL` expansion abort, not
independent bugs.
4. **Rebuild** and confirm `chraidata.c` compiles → full `ge007` links.
5. **Record the resolution** in `docs/PCPortResearch.md` §11 D7 (replace the
"OPEN" / "TBD" text) and delete this handoff file.
## Do NOT
- Do not edit `tools/`, `Makefile`, `rsp/`, `ld/` (N64 build, non-negotiable).
- Do not edit `bondaicommands.h`, `bondconstants.h`, `CPPLib.h`,
`aicommands.def`, `aicommands2.h` unless a `-DPORT`-gated, layout/semantics-
preserving change is truly unavoidable (and then flag it `PC port:`).
- Do not chase the aicommands.def:514/550/2846 line numbers as source lines
(they are expanded-macro positions).
-202
View File
@@ -1,202 +0,0 @@
> **RESOLVED — historical.** D7/D8 closed at the compile+link milestone.
> Kept for the IDO-vs-GCC preprocessor analysis. Not active work.
# HANDOFF — SWITCH/CALL macro port (D7/D8) — IN PROGRESS
## Status: BLOCKED on SWITCH expansion mangling (root cause now fully understood)
The `ge007` PC build fails on `src/game/chraidata.c` (the **only** file that uses
`SWITCH()`; 4 call sites) due to IDO-vs-GCC preprocessor differences in the AI-list
macro system. The `isSubroutine`/`MODELSKELETON` shims are done and working. The
**variadic `SWITCH` shim produces a mangled expansion** — this session confirmed the
exact mechanism and uncovered the *intended design* of the macro, which reframes the
fix.
---
## What was done this session (investigation + empirical confirmation)
### 1. PD port is NOT a reference (kills the previous top-priority lead)
`PD_PORT_CHECKOUT` has **no `bondaicommands.h` / `aicommands*.h`
at all** — Perfect Dark is a different game with no AI-list macro system. Do not look
there for a SWITCH shim. (It does have `include/PR/os_message.h` with `IS_EMPTY`,
unrelated.)
### 2. Empirically CONFIRMED the content-splitting mechanism
Minimal test `build-pc/swtest.c` (still on disk): a `PlayAnimation(A..E)` macro that
expands to a trailing-comma list, a 6-param `IMPL`, and a variadic
`SWITCH(VAR, ...) → IMPL(VAR, __VA_ARGS__, ,,,,,)`. Result:
```
error: macro 'IMPL' passed 16 arguments, but takes just 6
```
**Mechanism (now proven, not hypothesized):**
1. At SWITCH parse time the content `PlayAnimation(...) BREAK` is **one** argument
(its commas are inside `PlayAnimation`'s parens).
2. C rule: each macro arg is **fully macro-expanded before substitution**. The content
expands to a **top-level comma list** (`AI_PlayAnimation , ... , BREAK`).
3. Forwarding `__VA_ARGS__` into `_PORT_SWITCH_IMPL(...)` **re-parses** that token
stream at top-level commas → the content is **split into N separate args**, which
mis-fill `CASE0..CASE_CONTENTF`. The trailing comma of `PlayAnimation` adds one
extra empty arg (hence 16, not 15).
### 3. Preprocessed the real file — mangled output confirmed
`gcc -E src/game/chraidata.c` (full flags in §Build) shows at the line-183 call:
```
AI_SetNewRandom 0}; Error: Switch Limited to 15 elements + 1 default u8 (183) & 0x00FF[] = {
((0) & 0xFF00) >> 8 ((0) & 0x00FF,lblNext) 0x10(lblDone)
AI_Label , lblNext , AI_PlayAnimation (((ANIM_scratching_leg) & 0xFF00) >> 8,lblNext)
((183) & 0xFF00) >> 8(lblDone) AI_Label , lblNext , AI_GotoNext , ANIM_scratching_leg , & 0x00FF(lblDone) ...
```
Content fragments each get `(lblNext)`/`(lblDone)` spliced in; `CASEF` gets filled
(triggers "Switch Limited"); a fragment forms `BREAK(<2 args>)` → "BREAK passed 2
arguments" at chraidata.c:202.
### 4. Discovered the INTENDED DESIGN of the content (key reframing)
The content is **not** meant to survive as a single arg. It is a **template whose last
token is a dangling function-like macro name**, and the SWITCH body **appends
`(lblDone)` to complete that call**:
- `BREAK(LABEL)` → `GotoNext(LABEL)` → `AI_GotoNext , LABEL ,` (aicommands2.h:70)
- `GotoNext(LABEL)` → `AI_GotoNext , LABEL ,` (aicommands2.h:52)
- `PlayAnimation(...)` → `AI_PlayAnimation , CharArrayFrom16(..) , ... , INTERPOL ,`
(trailing comma; aicommands2.h:386)
- `Label(ID)` → `AI_Label , ID ,` (aicommands2.h:137)
- `SetNewRandom()` → `AI_SetNewRandom ,` (aicommands2.h:1555)
- `IFRandomGreaterThan(BYTE,LBL)` → `AI_IFRandomGreaterThan , BYTE , LBL ,` (aicommands2.h:1607)
- `IFNewRandomGreaterThan(BYTE,LBL)` → `SetNewRandom()IFRandomGreaterThan(BYTE,LBL)` (aicommands2.h:1623)
Game code writes the content as e.g. `PlayAnimation(...) BREAK` (line 183) or
`TRYFiringRun` / `... TRYFireAtBondKneeling` (line 305) — i.e. a run of macro
calls/names **ending in a bare function-like name** (`BREAK`, `TRYFiringRun`,
`TRYFireAtBondKneeling`). The body line
`EXPAND_ARGS_STACK(CASE_CONTENTx)(lblDone)` therefore expands the dangling name into
`NAME(lblDone)`.
**Consequences for the port:**
- `EXPAND_ARGS_STACK(CASE_CONTENTx)` is *not* a 1-arg call in practice: after
substitution the content's comma list fills `A,B,C,...` of its 33 params and the
rest are empty (IDO leniency). Its body re-joins the non-empty args with `COMMA()`
→ it is effectively an **identity / full-expansion pass** over the content. So the
shim's replacement of `EXPAND_ARGS_STACK(CASE_CONTENTx)` with bare `CASE_CONTENTx`
is semantically fine *if* the content arrives intact.
- `IS_EMPTY(CASE_CONTENTx)` becomes a **multi-arg call** to a 1-param macro after
substitution. IDO tolerated it (first-arg → `_IS_EMPTY_<tok>_` undefined → 0 =
"not empty", which is the correct answer when content is present). GCC hard-errors
(the `pasting '_' and '('` at CPPLib.h:257 is this).
### 5. Exact arg counts of the 4 call sites (corrects the old "4–25" note)
Counted top-level commas (comments stripped, paren-aware):
| line | args | shape |
|---|---|---|
| 183 | 20 | VAR + 5×(CASE,VAL,CONTENT) + `,,`default + CONTENT |
| 236 | 14 | VAR + 3×triple + `,,`default + CONTENT |
| 305 | 11 | VAR + 6×triple + `,,` + CONTENT + `,,` + CONTENT (two defaults) |
| 623 | 26 | VAR + 8×triple + `,,`default + CONTENT |
So the range is **11–26 args**, always `VAR` + a run of `(CASE, VAL, CONTENT)` triples
where a "default" is encoded as `,,` (empty CASE, empty VAL) before a CONTENT.
---
## The core blocker (precise statement)
The content must reach the body **as one argument** (so `IS_EMPTY`/`EXPAND_ARGS_STACK`
see it whole and `(lblDone)` lands on the dangling name). But:
- It is **one arg only at parse time**; C expands it to a top-level comma list before
substitution (step 2 above).
- Any forwarding of that expanded list through `__VA_ARGS__` into a fixed-param macro
**re-parses** it and splits it. That is exactly what the current shim does.
The original 49-param SWITCH avoided this because the content was bound to a **named
param** (`CASE_CONTENTx`) at parse time and substituted directly — never re-parsed.
GCC won't let us call a 49-param macro with 11–26 args, which is why we went variadic,
which reintroduces the re-parse. **That tension is the whole problem.**
---
## Directions for the next session (in suggested order)
1. **Protect the content from re-parsing by wrapping, not by re-parsing.**
The only C protection for a comma list is parentheses. Investigate a `SWITCH(VAR,
...)` that forwards each arg **parenthesized** so the content's commas can't split
it, then strips the parens in the body. The open sub-problem: identifying which
args are content vs. simple CASE/VAL without already having split them. Note CASE
and VAL are always simple single tokens (`IFRandomGreaterThan`, `250`); only
CONTENT carries commas. A positional scheme (content = every 3rd arg) may be
exploitable *if* the wrap happens before expansion — verify whether that's
achievable at all, since expansion precedes any body processing.
2. **Write a fresh GCC-clean SWITCH that emits byte-identical output** (abandon the
"verbatim original body" approach). First capture the *clean* IDO output for one
call (e.g. hand-expand the line-236 14-arg call, or reconstruct from the body
semantics now understood), then design a variadic macro that reproduces it. This
sidesteps `IS_EMPTY`/`EXPAND_ARGS_STACK` multi-arg issues entirely if the new body
doesn't route content through them.
3. **Shim `CPPLib.h`** (new `port/shim/CPPLib.h`, PORT-only, includes the real one)
to make `IS_EMPTY`/`_IS_EMPTY`/`EXPAND_ARGS_STACK` tolerant of multi-arg /
short-arg calls (variadic + first-token semantics). This is likely needed *in
addition to* whichever content-protection scheme is chosen, because even an intact
content becomes a multi-arg `IS_EMPTY`/`EXPAND_ARGS_STACK` call after substitution.
Keep the N64 build untouched (shim is PORT-gated).
4. **Verify byte-identity** by diffing the PC preprocessed `m_IdleAnimations[]` (and
the other 3 arrays) against a reference expansion. There is no IDO binary to diff
against, so the reference must be reconstructed from the body semantics (direction
2) — treat "compiles + plausible AI byte layout" as the practical bar, and record
the reasoning.
---
## Files touched / present
| File | State |
|---|---|
| `port/shim/bondaicommands.h` | D7 shim. `isSubroutine` ✅. `SWITCH` variadic → **mangled** (this is the blocker). |
| `port/shim/bondconstants.h` | D8 shim. `MODELSKELETON`/`New_ModelSkeleton` ✅. |
| `build-pc/swtest.c` | Throwaway minimal repro of the split (16-vs-6 args). Keep until fixed, then delete. |
## Build / preprocess commands
```sh
export PATH="/c/msys64/mingw64/bin:$PATH"
cd build-pc && cmake --build . --target ge007 2>&1 | grep -iE "error|FAILED" | grep -vE "Building C object"
```
Preprocess just the failing file (flags mirror `build-pc/compile_commands.json`):
```sh
cd REPO_ROOT
gcc -DAVOID_UB=1 -DBUGFIX_R0 -DBYTEMATCH -DLANG_US -DLEFTOVERDEBUG -DLEFTOVERSPECTRUM \
-DPAL=0 -DPLATFORM_64BIT=1 -DPORT=1 -DREFRESH_NTSC -DVERSION_US -D_LANGUAGE_C=1 \
-Iport/shim -I. -Iinclude -Iinclude/PR -Isrc -Isrc/game -Isrc/libultra -Isrc/libultra/audio \
-Iport/include -Ibuild-pc/port/include -std=c11 -funsigned-char \
-include build-pc/port/include/versioninfo.h -E src/game/chraidata.c
```
## Key references
- `src/bondaicommands.h` ~790–810 — original 49-param `SWITCH` body (ground truth for
intended expansion).
- `src/aicommands2.h` — all AI command macros (lines cited in §4 above).
- `include/CPPLib.h` — `IS_EMPTY`/`_IS_EMPTY` (256–258), `IS_PROBE`/`PROBE` (227–231),
`IF_VA`/`_IF_VA_0/1` (~328, 330–331), `EXPAND_ARGS_STACK` (337+), `EVAL` (85–98),
`COMMA` (16), `CAT` (190).
- `src/game/chraidata.c:183,236,305,623` — the 4 `SWITCH` call sites.
- `docs/PCPortResearch.md` §11 — findings log; record the new findings as **D9+**
(D7 = isSubroutine/SWITCH shim, D8 = MODELSKELETON, both already logged).
## Constraints (from AGENTS.md)
- Game code (`src/game/*.c`, `src/*.c`) compiles **UNMODIFIED**. All fixes go in `port/`.
- Shims are `PORT`-only and inert for the N64 build.
- `port/shim` is first on the include path, so `<bondaicommands.h>` /
`<bondconstants.h>` (and, if added, `<CPPLib.h>`) resolve to the shims; shims
include the real headers via `src/...` / `include/...` paths.
- Output must be byte-identical to the original IDO expansion.
-1093
View File
File diff suppressed because it is too large. Load diff
-166
View File
@@ -1,166 +0,0 @@
# M-24 — input rework + playtest QoL (review sheet)
> **Follow-up work: `docs/M-25-QOL-REVIEW.md`** (`[Video]` ini knobs, …).
Session M-24, 2026-08-30. Three commits on `master`:
| Commit | Title |
|---|---|
| `f3ec5170` | mouse-look rework (D118b/c) + real `ge007.ini` parser |
| `65ed0315` | F12 screenshot + mouse frees on focus loss |
| `33506aee` | gamepad hotplug, wheel weapon-cycle, title FPS |
**Scope guarantee:** every change is in `port/` (shim layer). **No `src/`
file was touched** — no game-logic / ABI / asset-format change. Nothing
here is `#ifdef PORT` in game code. The N64 build is untouched.
Files changed: `port/src/input.c`, `port/src/video.c`, `port/src/config.c`,
`port/include/input.h`, plus docs (`HANDOFF.md`, `LEVEL-PLAYTEST.md`,
`PORT-LEARNINGS.md`, `PCPortResearch.md` §F D118).
---
## 1. Mouse-look rework (`port/src/input.c`) — fixes D118b, D118c
### The bug
The old bridge always converted mouse-Y to digital C-up/C-down presses.
That was wrong two ways, because GE's aim model is **mode-dependent**
(read from `src/game/bondview2.c` `bondviewProcessInput` / `MoveData`):
| Mode | Yaw source | Pitch source | What C-up/C-down do |
|---|---|---|---|
| Hipfire (`!insightaimmode`) | analog stick-X ("natural turn") | **digital C-up/C-down only** (stick-Y is move fwd/back, no pitch) | look up / down |
| Aim mode (R / RMB held) | analog stick past ±60 → `(stick-60)/10` | analog stick past ±60 | **crouch / lean / zoom** (`bondview2.c:5340,5351`) — *not* aim |
- **D118c** (aim + mouse-down → crouch): emitting C-down for "look down"
while aiming triggered `moveData.crouchDown`.
- **D118b** (inverted Y): GE's native pitch is inverted —
`U_CBUTTONS → speedVertaDown` (`bondview2.c:5272-5279`), i.e. C-up looks
*down*. Mapping mouse-down → C-down gave mouse-down = look up.
### The fix
`inputComputePad()` now branches on whether the aim button is held (our own
RMB/LShift state — the exact signal for the default hold-to-aim scheme):
- **Aim mode:** push `stick_x` / `stick_y` into the 61..80 band,
proportional to the per-poll mouse delta (`× MouseAimSpeed/100 × 4`,
clamped). **Emit no C-buttons.** → crouch bug gone; yaw and pitch now
behave identically (both analog).
- **Hipfire:** yaw on `stick_x += dx × MouseTurnSpeed/100 × 6`; pitch =
digital C-up/C-down on `|dy| ≥ 1.5` px/poll.
- "Mouse-down looks down" is the default (accounts for GE's inversion);
`MouseInvertY` flips it.
- Per-poll deltas, **no accumulator** (mouse = displacement device, GE aim
= rate device; consume the whole delta each poll, stop moving → stick
releases immediately).
### Known residual — D118a (lower priority, not a regression)
In **hipfire only**, yaw (analog) and pitch (digital C-button) still feel
different. Aim mode — where GE's precise vertical aiming actually happens —
is now fully analog and consistent. A fully-analog hipfire pitch would need
an `#ifdef PORT` hook in `bondview.c` (would also be needed for
toggle-aim control schemes, where "RMB held" ≠ `insightaimmode`). Deferred.
### Review risk
Low. Pure output-shaping of the N64 pad struct. Worst case is bad *feel*,
tunable at runtime (below). Cannot crash or desync — same `OSContPad`
contract as before.
---
## 2. `ge007.ini` parser (`port/src/config.c`) — was a stub
`configLoad()` / `configSave()` only logged `TODO` before. The registered
options (`configRegisterInt`) therefore could not be changed without a
recompile.
Now: parses `$S/ge007.ini` — `[Section]` blocks, `Key = value`, `#` / `;`
comments; clamps ints to their registered `min`/`max`; writes a defaults
file on first run; rewrites on clean exit (`main.c` already calls
`configLoad()` at startup and `configSave()` on the unreachable-in-practice
shutdown path). Dotted keys (`Input.MouseAimSpeed`) group into
`[Input]` on disk.
**Live-tunable `[Input]` knobs** (edit with the game closed):
| Key | Default | Meaning |
|---|---|---|
| `MouseEnabled` | 1 | mouse-look on/off |
| `MouseAimSpeed` | 50 | aim-mode (RMB) sensitivity, percent |
| `MouseTurnSpeed` | 100 | hipfire yaw sensitivity, percent (new; split from AimSpeed) |
| `MouseInvertY` | 0 | 1 = mouse-down looks up |
Other sections already register options (`[Audio] BufferSize`,
`QueueLimit`) and now round-trip too.
**Implementation notes for review:** no `<ctype.h>` / `atoi` — the project's
`src/str.c` defines `isspace`, which collided with libmsvcrt at link time
once `config.c` referenced the libc one, so `config.c` carries tiny local
`isws` / `lc` / `parseInt` helpers. Options registered *after* `configLoad()`
(none today — all are `PD_CONSTRUCTOR`) would miss the first-run save.
### Review risk
Low-medium. New file I/O in `port/`. Failure modes are contained: missing
file → write defaults; unparseable line → skipped; unknown key → logged and
ignored. No effect on any code path that does not read a registered option.
---
## 3. Playtest QoL (`port/src/video.c` + `port/src/input.c`)
All wired through the existing host-thread SDL event pump
(`videoPumpEvents`).
| Feature | How | Notes |
|---|---|---|
| **F12 screenshot** | keydown sets a flag; consumed in `videoEndFrame` (render thread, GL context current) via the existing `gfx_opengl_dump_bound_fbo` | → `ppm/shot_NNN.ppm`; view with `tools_pc/ppm2bmp.py`. Same mechanism as `GE_PCDUMP`. |
| **Alt-tab frees the mouse** | `SDL_WINDOWEVENT_FOCUS_LOST/GAINED` → `inputSetMouseGrab(0/1)` | New `inputSetMouseGrab()` in `input.c`: toggles SDL relative-mouse mode (respecting `MouseEnabled`), zeroes the aim delta, and gates `inputUpdate()` mouse reads on a `mouseGrabbed` flag so a released cursor can't drift the aim. |
| **Mouse wheel = weapon cycle** | `SDL_MOUSEWHEEL` → `inputPostWheel()` queues a 2-poll `GE_CONT_A` press | GE's default scheme cycles weapon forward on a bare A edge (`invButtons = A_BUTTON`, `bondview2.c:5162` → `weaponForwardOffset`, `:5326`). Both wheel directions cycle **forward** — no clean backward input without the A+fire combo. Same overload the keyboard already has (Space/Z/E = A). |
| **Gamepad hotplug** | `SDL_CONTROLLERDEVICEADDED/REMOVED` → `inputRescanPads()` (close all + re-open) | Caveat: the game latches `inputConnectedMask()` at `osContInit` (boot). A pad added *after* boot still works for play (it merges into controller 0 — `inputComputePad(0)` always checks `pads[0]`) but won't appear as a separate controller channel. Plug it before launch for genuine multi-pad. |
| **Window title shows FPS** | `videoPumpEvents` calls `wmAPI->set_window_title("GoldenEye 007 - N fps")` ~1 Hz from `vidAvgFPS` | Cosmetic. |
Alt-Enter fullscreen toggle was **already present** (`gfx_sdl2.cpp:299`) —
not added here, just documented.
### Review risk
Low. Screenshot reuses a proven path. Focus/grab and hotplug are SDL window
ops on the thread that created the window. Wheel→A is opt-in physical input
identical in effect to tapping the existing keyboard A key.
---
## Verification done
- Build green (`./build-pc.sh ntsc-final`), no new warnings in touched files.
- `-level_09` (BUNKER1): crash-free to frame 900 (30–35 s window), **91.6%
pixel coverage — unregressed** vs the committed baseline, checked after
each of the 3 commits (3/3, 2/2, 2/2 runs; the one early NO-FRAME was the
known D117/D134 machine-load flakiness, cleared on retry).
- `-level_20` (Silo): crash-free.
- `ge007.ini`: written on first run, re-read with **no unknown-key
warnings**, values round-trip.
- Input is a headless-agent blind spot — **live feel of the mouse rework,
the wheel cycle, and a real gamepad are UNVERIFIED**; that's the WS6
playtest's job. See `docs/LEVEL-PLAYTEST.md`.
## What a reviewer should check by playing
1. Mouse aim in **aim mode (hold RMB)** feels smooth and 1:1-ish; no crouch
when you pull the mouse down to look down (D118c).
2. Mouse-Y not inverted at `MouseInvertY = 0` (D118b); flipping the ini
value inverts it.
3. Editing `MouseAimSpeed` / `MouseTurnSpeed` in `data/ge007.ini` (game
closed) actually changes sensitivity next launch.
4. Mouse wheel swaps weapons; F12 drops a `ppm/shot_*.ppm`; alt-tab frees
the cursor and returns cleanly; a gamepad plugged in before launch works.
## Still TODO (not started)
Key rebinding, in-render on-screen FPS overlay, in-game options menu,
backward weapon cycle, the `bondview.c` analog-aim hook (D118a + toggle-aim).
-191
View File
@@ -1,191 +0,0 @@
# M-25 — port-layer QoL / playtest-experience polish (review sheet)
Session M-25, 2026-08-30. Continues `docs/M-24-QOL-REVIEW.md`.
**Scope guarantee:** every change is in `port/` (shim layer). **No `src/`
file touched** — no game-logic / ABI / asset-format change, no `#ifdef PORT`
in game code. N64 build untouched. Every new behavior defaults to the
pre-M-25 behavior — a fresh or partial `ge007.ini` changes nothing.
| Commit | Title |
|---|---|
| `9ebed821` | `[Video]` ge007.ini knobs + config auto-migration |
| `5a7a1035` | `tools_pc/playtest.sh` — name lookup, `--list`, auto-addr2line |
| _(this)_ | `[Input]` knobs — mouse Y scale / smoothing, pad deadzone / trigger / invert-Y |
---
## 1. `[Video]` ge007.ini knobs + config auto-migration
### Rationale
`video.c` hardcoded five display settings that testers reasonably want to
change per-machine (weak GPU → drop MSAA; capture work → nearest-filter for
crisp texels; high-refresh monitor → uncap fps). All were compile-time only.
### Changes
**`port/src/video.c`** — new `PD_CONSTRUCTOR videoConfigInit()` registers:
| Key | Default | Effect | Applied |
|---|---|---|---|
| `Video.VSync` | 1 | swap interval 0/1 | `wmAPI->set_swap_interval` in `videoInit` |
| `Video.FpsCap` | 0 | frame cap in fps, 0 = uncapped | `gfx_set_target_fps` in `videoInit` |
| `Video.MSAA` | 1 | 1/2/4/8 samples (snapped down to a power of two) | `gfx_msaa_level` before `gfx_init` |
| `Video.TextureFilter` | 1 | 0 = nearest, 1 = bilinear | `gfx_set_texture_filter` + mipmap filter |
| `Video.Fullscreen` | 0 | 0 = windowed, 1 = borderless fullscreen | `window_settings.fullscreen` at window create |
Every default reproduces the exact prior hardcoded value
(`set_swap_interval(1)`, no fps target, `gfx_msaa_level = 1`,
`FILTER_LINEAR` / `MIPMAP_LINEAR`, windowed). MSAA is clamped to
`{1,2,4,8}` by a snap-down expression so a bogus `MSAA = 3` can't reach the
GL layer. `gfx_pc.cpp` already self-disables MSAA if framebuffers are
unavailable.
**`port/src/config.c`** — `configLoad()` now tracks, per registered option,
whether the on-disk file actually mentioned it (`seen` flag set in
`applyKV`). If any registered key was missing after the parse, it calls
`configSave()` once to rewrite the file with the new key at its default.
- Without this, a new `[Video]` block would never appear in an existing
`ge007.ini` (M-24: `configLoad` only writes defaults when the file is
*absent*, and the clean-exit `configSave` is unreachable in practice) —
the knobs would exist but be undiscoverable.
- An **up-to-date** file (every registered key present) is **not**
rewritten — user comments and ordering are preserved, exactly as before.
- A file that *is* missing keys gets rewritten in the canonical grouped
form (comments dropped — same contract M-24 already documented for the
clean-exit save). Existing values are preserved (they're parsed into the
option storage before the re-save).
### Review risk
Low. `video.c` changes are all "read an int, pick a code path that already
existed". `config.c` change is one extra conditional `configSave()` on a
path that already round-trips through the same serializer.
Failure modes: a corrupt ini value is clamped (ints) or ignored (unknown
key, already handled); the migration re-save uses `fopen(path,"w")` which
already had a `cannot write` warning path.
### Verification
- Build green (`./build-pc.sh ntsc-final`), no new warnings in `video.c` /
`config.c`. (Pre-existing `getenv` implicit-decl warning in the untouched
`GE_PCDUMP` block is not from this change.)
- Fresh ini (deleted): `[Video]` block written with all five defaults.
- Migration: hand-written ini with only `[Input] MouseAimSpeed = 77` +
`[Audio]` → after one launch, `[Video]` added, `MouseAimSpeed = 77`
preserved.
- No-op: ini with all keys present + a custom leading comment → launch
leaves the comment in place (no rewrite).
- `-level_09`: `MSAA=4 / TextureFilter=0 / VSync=0 / FpsCap=120` →
crash-free to frame 380+, **91.67% pixel coverage (unregressed)**.
Defaults → crash-free to frame 620+, 91.67%.
- `-level_20` (Silo): crash-free to frame 620+ (defaults). NB Silo has a
known pre-existing early freeze on the silo→Bond camera descent (~3 s
in) — reported M-25, unrelated to this change, tracked for a later
session.
### What to check by playing
1. Edit `data/ge007.ini` `[Video]` with the game closed; values take effect
next launch.
2. `TextureFilter = 0` → visibly crisp/pixelated textures. `MSAA = 4` →
smoother polygon edges. `Fullscreen = 1` → starts borderless-fullscreen
(Alt-Enter still toggles).
3. `FpsCap = 30` → title-bar fps pins near 30; `VSync = 0` + `FpsCap = 0` →
uncapped.
---
## 2. `tools_pc/playtest.sh` — usability
Tooling only — not compiled, not in the build. No game-code impact.
- Accepts a **level name** as well as the `-level_XX` number
(`playtest.sh bunker2`, `playtest.sh "surface 1"` — case/space/underscore
insensitive), via an in-script name→number table (mission order).
- `playtest.sh --list` prints the 21-level table (`#`, `-level_XX`, name).
No-arg / `--help` prints usage + the table.
- On a crash it now reads `ge007.crash.log` and runs `addr2line` on the top
faulting PCs automatically (was: "see log"), and copies the crash log
next to the playtest log.
Verified: `--list` output correct; name + number resolution; a real
`bunker1` launch (killed after 15 s) reports cleanly.
---
## 3. `[Input]` knobs (`port/src/input.c`)
### Rationale
Feel tuning that was hardcoded. Every default is the exact prior constant,
so a fresh/partial ini changes nothing (verified against a from-baseline
build — see below).
| Key | Default | Effect |
|---|---|---|
| `Input.MouseYScale` | 100 | extra vertical (pitch) sensitivity multiplier, % — applied to the mouse dy before aim/hipfire mapping |
| `Input.MouseSmoothing` | 0 | 0 = raw deltas (today); 1..90 = exponential low-pass, value = % blend of the previous poll |
| `Input.PadDeadzone` | 7000 | left-stick deadzone, raw 0..32767 (was `STICK_DEADZONE`) |
| `Input.PadTriggerPct` | 23 | trigger press point, % of travel (≈ the old `30*256` threshold: 23 × 327 ≈ 7521 vs 7680) |
| `Input.PadLookInvertY` | 0 | 1 = invert right-stick (look) Y |
`MouseYScale` / `MouseSmoothing` feed through the same mode-aware
mouse→pad path as the M-24 rework (aim-mode band vs hipfire digital pitch);
smoothing state is a pair of static doubles, reset implicitly when the
deltas go to zero. `PadDeadzone` flows through `scaleAxis()` (clamped
0..30000 so the divisor can't go non-positive). `PadTriggerPct`
recomputes the threshold from a percentage; `PadLookInvertY` negates `ry`
before the C-button mapping.
### Review risk
Low. Same class as §1 — read an int, feed an existing code path.
`scaleAxis` divisor is guarded. Worst case is bad feel, all runtime-tunable.
Cannot change the `OSContPad` contract.
### Verification
- Build green; the two `input.c` warnings (`fabs` implicit-decl, `getenv`)
are **pre-existing** — confirmed identical on a stashed baseline build,
not introduced here.
- `-level_09`: crash-free to frame 620, **91.6% coverage (unregressed)**.
- `-level_20` (Silo): crash-free; frame capture stops at ~320 —
**confirmed identical on the pre-change baseline build** (the known Silo
camera-descent freeze the user reported this session, not a regression).
- New keys land in `[Input]` on next launch via the §1 migration path;
existing values (`MouseAimSpeed = 77`) preserved.
### What to check by playing
1. `MouseYScale = 150` → faster vertical look, horizontal unchanged.
2. `MouseSmoothing = 60` → visibly smoothed / slightly laggy aim; `0` crisp.
3. `PadDeadzone = 3000` → stick responds to smaller nudges; `12000` → larger
dead center. `PadLookInvertY = 1` → inverted pad look. `PadTriggerPct`
low → hair trigger.
---
## Known pre-existing bugs noted this session (not addressed — out of scope)
- **Silo (`-level_20`) capture freeze — likely just D117/D134 load
sensitivity, NOT a real bug.** A full-length `GE_PCDUMP` capture froze at
~frame 320 (silo→Bond camera descent) twice, and the pre-M-25 baseline
comparison froze too — but all of those ran on a machine still loaded from
the 21-level sweep. On an idle machine the user drove Silo live past
~1800 frames with a healthy VI pacemaker and no heartbeat stall. Not an
M-25 regression; not currently believed deterministic. Recount N runs on
an idle box if it recurs.
---
## Still TODO (carried from M-24)
Key rebinding, in-render on-screen FPS/pos overlay, in-game options menu,
backward weapon cycle, the `bondview.c` analog-aim hook (D118a), PNG (not
PPM) screenshots (needs stb_image_write vendored into `port/`), window
size/pos persistence (needs a reliable save-on-exit hook — `configSave` is
currently only on the unreachable clean-shutdown path).
-72
View File
@@ -1,72 +0,0 @@
# M-26 QoL review — save-on-exit, window persistence, raw mouse, --help, STALLED verdict
Port-layer only. Zero `src/` / game-logic change. Every default reproduces
prior behaviour exactly (all new knobs default-off / default-current).
## A — `atexit()` save-on-exit + `[Window]` persistence
- `port/src/main.c`: `atexit(portAtExit)` registered right after
`configLoad()`. `portAtExit` = `videoSaveWindowState()` then
`configSave()`. The `exit(0)` in `videoPumpEvents` (SDL_QUIT / ESC /
window-close) is reachable, so the handler fires on every clean quit.
Crash / `sysFatalError` paths call `abort()` → handler does **not** run
(intended — don't persist a broken state).
- `port/src/video.c`: new `[Window]` config section — `Width`, `Height`,
`X`, `Y`, `Maximized`. Sentinels reproduce the old hardcoded behaviour:
`Width/Height = 0` → native (640×480 / 640×400), `X/Y = -1` → SDL
centres the window. `videoInit` uses them for the `GfxWindowInitSettings`.
- `videoSaveWindowState()` reads `wmAPI->get_fullscreen_state` /
`get_maximized_state` / `get_dimensions` (all already in the SDL2 wapi
backend) and writes geometry back into the cfg vars. A maximized or
fullscreen window keeps its last *restored* size/pos on disk and only
updates the flag. `Video.Fullscreen` now round-trips too.
- **Verified:** `-level_09` / `-level_20` boot crash-free (6/6 GE_PCDUMP
frames each, unregressed); `ge007.ini` gains a populated `[Window]`
block on exit; relaunch reads it back with no unknown-key warnings.
## B — `level_sweep.sh` STALLED verdict
Pure shell (not built). A level that renders a few frames then freezes
while the process stays alive (the Silo `-level_20` fly-down case,
D117/D134 machine-load sensitivity) previously reported **PASS** because
`last=$(ls ppm/*.ppm | tail -1)` was non-empty. Now the watchdog records
`stalled=1` when the frame count is frozen for `STALL_SECS`, and `alive=1`
if the process was still running when the watchdog gave up →
`STALLED (froze at N/M frames)`. A complete dump set is still a real PASS.
STALLED is retried once (like NO-FRAMES) before it's believed.
## C — raw mouse input (`Input.MouseRawInput`, default 0)
`port/src/input.c inputInit`: when `MouseRawInput=1`, sets
`SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE=0` and
`SDL_HINT_MOUSE_RELATIVE_MODE_WARP=0` before `SDL_SetRelativeMouseMode` —
removes OS pointer acceleration / warp emulation from aim. Off by default
so nothing changes unless the user opts in.
## E — `--help` / `--version`
`port/src/main.c`, handled before `crashInit()`:
- `--version` → rom id / target platform / build hash, exit 0.
- `--help` / `-h` → the above + usage + the 21 solo-level `-level_XX`
table (matches `tools_pc/level_sweep.sh` / `playtest.sh --list`), exit 0.
## D — `[Debug]` ini flags mirroring the dev env vars
`port/src/config.c`: `Debug.FrameDump` (string, mirrors `GE_PCDUMP`,
`"lo-hi[:step]"`) and `Debug.InputLog` (int, mirrors `GE_INPUTLOG`).
`configGetFrameDump()` / `configGetInputLog()` check the env var first
(existing `tools_pc/*.sh` scripts unaffected), fall back to the ini.
Consumers switched: `video.c videoEndFrame` (was `gfx_opengl_pcdump_enabled()`
+ `getenv`), `input.c inputComputePad` (was `getenv("GE_INPUTLOG")`).
`GE_DETERM` is design-only (D117, no code path) so it is not mirrored.
- **Verified:** ini-only `FrameDump = 40-120:20` produces exactly those 5
frames; `GE_PCDUMP` still overrides; `-level_09` crash-free.
- **Cosmetic:** `configSave` emits int and string opts in separate passes,
so a section with both (`[Debug]`) prints its header twice. Round-trips
fine on reload. Pre-existing quirk, not worth the churn now.
## Not done (Tier 2/3 from the board)
F (minidump), G (PNG screenshots), H (rumble), I/J (in-render overlay /
toasts) — left for a dedicated session.
-94
View File
@@ -1,94 +0,0 @@
# Plan — Session M-6: toward a playable, low-glitch BUNKER1 PC build
Baseline: D112 committed (`67d674da`). Build green. `-level_09` renders
BUNKER1 continuously, crash-free. Three tracks below, in priority order.
Each track: **investigate → root-cause → narrow `#ifdef PORT` fix or
write-up** per AGENTS.md non-negotiables (no game-logic changes; ABI /
layout / format exceptions only, each documented in
`docs/PCPortResearch.md` §F).
## Track 1 — Portal-BFS under-reach ("the void") [highest visual impact]
**Symptom:** in BUNKER storage area (rooms 27–29) the per-frame visible
room count collapses to 1–2; large sky-fill void where an adjacent room
should show through an open doorway. Portal *visibility* is otherwise
healthy post-D106 (room 18 → 8 rooms deep on enabled doorways).
**Known-good / ruled out:** `currentPlayerGetProjectionMatrix()`,
`g_CurrentPlayer->screensize`, the intentional `g_RoomLoadBudget` 200→3
FP-cam drop, `zfar`, the plane-side metric cull (`bg.c:4135/4144`).
**Suspects:** `sub_GAME_7F0B5864` screen-space portal projection
(`bg.c:1619`) — sibling of the D106 z==0 near-plane case, but for
partially-off-screen (not straddling) portals; `bgQueuePortalTraversal`
(`bg.c:3814/3843`) depth/dedup; `bgDetermineVisibleRooms` (`bg.c:4615`)
seed set; `sub_GAME_7F0B5528` clip-point generation (`bg.c:1551`);
`transform3Dto2DWithZScaling` (`bondview.c:730`).
**Deliverable:** root-cause writeup in §F ("D113"); if a narrow PORT fix
is obviously correct (e.g. another degenerate-box / clamp mismatch),
apply it, else stop and document.
**RESULT (Track 1 agent):** NOT a portal-BFS bug. The BFS output is
correct for BUNKER1's topology — rooms 26/27 sit behind portals 25/26
which carry `PORTALFLAG_DISABLED` (closed doors), culled identically on
N64. Stable 3 visible rooms {29,28,25}, not "1–2". The void is the
**closed-door prop models (obj 153/154) rendering with a wrong world
transform** — their leaf DLs are emitted (`GE_D96`) but land outside the
doorway, leaving the wall hole unoccluded → sky. **Folds into Track 2**
(same model-matrix defect as the inverted guard / mislocated crate). No
`bg.c` change. Probes reverted, tree clean.
## Track 2 — Character pose / orientation
**Symptom:** post-D112 a BUNKER guard renders as a coherent humanoid but
appears **inverted**. Also a mispositioned static crate prop (top-left in
the control room).
**Known-good:** joint *rotations* from the anim bitstream; `render_pos[]`
positions now sane (~±60 units) after the D112 f32 fix; `basemtx` from
`camGetWorldToScreenMtxf()` (`bondview.c:824`) was a clean orthonormal
lookat.
**Suspects:** matrix handedness / lookat sign in the joint base matrix;
`modelBuildGroupMatrices` / `process_02_position` / `subcalcmatrices`
composition order on LE; a converter sign/axis issue in `d43_emit.py`
group `Origin` or matrix rodata that survived the D112 fix (byte order
right, axis convention wrong); guMtxF2L / Mtxf→Mtx path.
**Deliverable:** root-cause writeup in §F ("D114"); narrow fix if
obviously correct.
## Track 3 — `struct player` raw-offset audit [gate to playability]
**Symptom:** 1P weapon model doesn't draw. Landmine (§F D100): raw
hardcoded-offset accessors into `struct player` / `struct hand` are NOT
PORT-adjusted and are already PC-wrong.
**Known instances:** `gunfire.c` `THROWMTX_OFFSET 0xAD8` / `THROWPOS`
`0xB08` / `THROWPREV` `0xB48` (all `g_CurrentPlayer + handoffset + K`);
plus D102 (weapon Model pool) already fixed.
**Task:** enumerate *every* raw byte-offset / hardcoded-size access into
`struct player`, `struct hand`, and siblings across `src/game/` (grep for
`+ 0x`, `handoffset`, `(u8 *)`, hardcoded `0x2A80`-class sizes, pointer
arithmetic on player). For each: N64 offset vs PC offset (struct is
0x2A8-class larger per D100), whether it's on a BUNKER1-reachable path,
severity. Produce a table + recommended fix order. Do NOT fix yet —
this is a survey feeding a follow-up.
**Deliverable:** `docs/AUDIT-M6-player-offsets.md` table + §F pointer.
## Execution
- Tracks 1, 2, 3 dispatched as parallel investigation subagents
(read-mostly; each returns a writeup + optional narrow patch).
- Integrator (this session) reviews each, applies/adjusts verified narrow
fixes, rebuilds (`export PATH="/c/msys64/mingw64/bin:$PATH" &&
./build-pc.sh ntsc-final`), spot-checks `-level_09`, commits per-track
with §F docs.
- Repro: `./build-pc/ge007.x86_64.exe -level_09 -ml0 -me0 -mgfx100
-mvtx50 -mt700 -ma150`. Frame capture: `GE_PCDUMP="<range>:<stride>"` +
`tools_pc/pixcount.py` / `tools_pc/pixcount.py`.
- Sidecar regen (if a converter changes): `python tools_pc/d43_emit.py
ntsc-final`.
-254
View File
@@ -1,254 +0,0 @@
# PLAN — Linear pass: boot → menu → every solo level loads & renders
Paste-ready brief for a fresh session. Pivot from depth-first
(BUNKER1 + graphics rabbit-holes) to **breadth-first**: make the normal
player path work end-to-end (intro → credits → main menu → mission/level
select), then confirm **all 21 solo levels load and render** without
crashing. Turns the remaining work into a scoped list.
## Preflight (unchanged)
`CLAUDE.md` + `AGENTS.md` non-negotiables → `docs/HANDOFF.md` →
`docs/PORT-LEARNINGS.md` → `docs/AGENT-WORKFLOW.md` before any subagent.
## Scope decisions (locked with the user)
- **Intro / credits / menus:** bar is *functional, not crashing*. Menus
navigable start to finish; the intro is skippable with any button.
Cosmetic defects are **out of scope** here — log them in
`docs/GRAPHICS-BACKLOG.md`: gun-barrel Bond model missing (D75),
Nintendo-logo transform (D75), mirrored glyphs (D114/D116), no audio
(D77), partial disclaimer screen (D76).
- **Text X-mirror (D114/D116):** stays parked. Menus are navigable with
mirrored text.
- **Level sweep depth:** *load + render + no immediate crash* only. A
"soak" is pointless with no input (level triggers never fire). Play-to-
completion validation is a **separate human step** (complex input),
driven by a per-level checklist this plan produces (WS6).
- **N64 reference captures:** deferred. Use PC golden frames +
`tools_pc/framediff.py`; eyeball against memory/video for "matches
original". `tools_pc/mupen64/` is available if a level looks wrong.
## Facts established (exploration, this session)
**Front end is a stage** — `LEVELID_TITLE` (0x5A). Logic in
`src/game/front.c` (~8700 lines) + `src/game/title.c` (gun-barrel/logo
3D) + `src/game/ramromreplay.c` (attract demos). State machine = `MENU_*`
enum (`src/bondconstants.h:1831`), driven per frame by `menu_init()`
(`front.c:8536`) through three switch tables (`update_/init_/interface_
menuXX_*` at `front.c:8598/8637/8666`); rendered by
`menu_jump_constructor_handler()` (`front.c:8718`). Transitions:
`frontChangeMenu(menu, reload)` (`front.c:8513`). Boot entry:
`initgamedata.c:9` sets `menu_update = MENU_LEGAL_SCREEN`.
**Intro states** (each: `init_` loads assets, `interface_` advances on
timer OR any button, `constructor_` renders):
`MENU_LEGAL_SCREEN` → `NINTENDO_LOGO` → `RAREWARE_LOGO` → `EYE_INTRO`
(gun-barrel walk, "007", blood) → `GOLDENEYE_LOGO` → `DISPLAY_CAST`
(credit roll) → idle → `select_ramrom_to_play()` (attract demo).
Any button during intro sets `prev_keypresses` → logos short-circuit to
`MENU_FILE_SELECT`, cast roll skipped. Gun-barrel sub-state machine is
`gunbarrel_mode` (u8) in `title.c` (`insert_bond_eye_intro` `title.c:297`,
`renderGunbarrelEyeIntroSequence` `title.c:608`).
**Menu → level launch:** mission select →
`selected_stage = mission_folder_setup_entries[…].stage_id`
(`front.c:3301`) → `MENU_DIFFICULTY` → `MENU_BRIEFING` /
`MENU_007_OPTIONS` → `init_menu0B_runstage()` (`front.c:6900`) →
`bossSetLoadedStage(selected_stage)` → `g_MainStageNum` → `bossMainloop`
inner loop exits → `g_StageNum = g_MainStageNum` (`boss.c:647`) →
`lvlStageLoad`. **Level → level always routes back through the front end**
(`MENU_MISSION_COMPLETE`, `interface_menu0D_missioncomplete`
`front.c:7119`; auto-advance scans `mission_folder_setup_entries` for the
next `stage_id >= 0`). No direct in-stage jump.
**21 solo levels; LEVELID value == `-level_XX` number** (`boss.c:337`
decode `d0*10 + d1 - 0x210`). Mission order + numbers:
Dam 33, Facility 34, Runway 35, Surface1 36, Bunker1 09, Silo 20,
Frigate 26, Surface2 43, Bunker2 27, Statue 22, Archives 24, Streets 29,
Depot 30, Train 25, Jungle 37, Control 23, Caverns 39, Cradle 41,
Aztec 28, Egypt 32, Cuba 54. Enum: `src/bondconstants.h:1633-1685`;
mission table `mission_folder_setup_entries[]` `front.c:434-466`.
**Per-level memory args already have a table** — `memallocstringtable[]`
(`boss.c:101-142`), one row/LEVELID (`-ml -me -mgfx -mvtx -mt -ma`). The
auto-inject loop `boss.c:369-414` already selects the right row per stage
load — but it is gated on `g_DebugAndUpdateStageFlag`, which the port
only sets when `-level_` is **absent** (`boss.c:199-202`;
`rmonGetToken()` → 0 on PC, `port/src/n64stubs.c:134`). So bare
`-level_09` today uses default pools; hence the manual
`-ml0 -me0 -mgfx100 -mvtx50 -mt700 -ma150` in the HANDOFF repro line.
Documented TODO: `PCPortResearch.md` §H (D95 area) and `docs/HANDOFF.md`
"Next task".
**D88.4 is a cross-level blocker.** Every level's `Usetup*Z` feeds the
`propDefs` polymorphic record stream, which `tools_pc/d88_emit.py`
deliberately leaves un-byteswapped. BUNKER1 crashes in `setupDoor`
(`prop.c:971`) on a garbage BE `door->obj`. Any level hits the same class
when its prop set is walked. Groundwork: `tools_pc/d88_propdefs.py`,
`tools_pc/d88_layoutprobe.c` (machine `SZ`/`PTR` lines),
`tools_pc/d88_propdef_scan.py` (per-level type histogram).
Placeholder sizes still to resolve in `d88_propdefs.py`:
`VEHICLE`/`AIRCRAFT`/`TANK`/`AMMO`/`DEPOSIT_IN_ROOM`.
**Tooling:** `GE_PCDUMP="a-b:step"` → `./ppm/` (`port/src/video.c:181`);
`tools_pc/pixcount.py` (non-black pixel count / content check),
`tools_pc/framediff.py` (structural regression vs `tools_pc/golden/` —
the port is NOT frame-deterministic, D117), `tools_pc/ppm2bmp.py`.
Crash log `ge007.crash.log`; symbolicate
`addr2line -e build-pc/ge007.x86_64.exe -f -C <pc>` (image base
0x140000000). Objectives: `propDefs` `PROPDEF_OBJECTIVE_*`
(`bondconstants.h:4332`), status `src/game/objective_status.c`
(`get_status_of_objective()` `:161`) — needed only for WS6.
## Workstreams
WS1 is a prerequisite for all. WS2 (`front.c`/`title.c`/`lv.c`/
`ramromreplay.c`/`initmenus.c`/`port/`) and WS3 (`tools_pc/d88*` +
narrow `prop.c`) are disjoint → parallelisable. WS4 depends on WS3.
WS5 iterative. WS6 is the user's.
### WS1 — Frictionless per-level boot (small, first)
Goal: `./build-pc/ge007.x86_64.exe -level_XX` with no other args boots
any level with its correct pools.
Change: in `src/boss.c` under `#ifdef PORT`, also set
`g_DebugAndUpdateStageFlag = 1` when `-level_` **is** present (mirror of
`boss.c:199-202`). The existing loop `boss.c:369-414` then supplies the
right `memallocstringtable` row per stage; the generic preload at
`boss.c:207-210` is harmless (per-loop `tokenSetString` at `:413`
overrides). Debug-harness / ABI-class `#ifdef PORT` edit (same character
as the `-level_` ASCII parse already in the file). Document as a new Dxx
in `PCPortResearch.md` §F. Alt if boss.c is judged out of bounds:
synthesize the string in the port token layer (`port/src/system.c`
`sysGetTokenString`). Boss.c is simpler and preferred.
Also update `docs/HANDOFF.md` repro line; drop the manual `-m*` list once
verified.
Verify: `-level_09` alone == today's full-arg behaviour (same frame,
`framediff.py` green vs `tools_pc/golden/`).
### WS2 — Front-end walkthrough & crash repair (human playtest loop)
Goal: a human can go boot → (watch or skip intro) → file/folder select →
mode select → mission select → difficulty → briefing → level launches,
and after a level, mission-complete → next briefing, with **no crash /
hang / dead-end**. Cosmetic wrongness acceptable and logged.
Method: same loop as the M-10 input playtest — overseer builds, human
drives, defects come back as a list, overseer root-causes each (expect
the pointer-width / BE-rodata / N64-Gfx-sizing classes in
`PORT-LEARNINGS.md`), narrow `#ifdef PORT` fix, re-test.
Files in scope: `src/game/front.c`, `src/game/lv.c`
(`interface_menu05_fileselect` is here), `src/game/title.c`,
`src/game/initmenus.c`, `src/game/ramromreplay.c`, `port/src/*`. NOT
`prop.c` / `tools_pc/d88*` (WS3).
Human checklist:
1. Boot → legal → Nintendo → Rareware → gun-barrel → GE logo → cast roll
→ (idle) attract demo. Note any crash/hang; cosmetic noted separately.
2. Press a key during intro → confirm short-circuit to file select.
3. File/folder select: create a folder, enter it; folder-delete sub-UI;
30 s idle → back to legal screen.
4. Mode select → Mission select: page through all 20 mission entries,
each briefing text/photo loads without fault.
5. Pick mission + Agent difficulty + briefing → Start → level loads.
6. In-level: die / abort → mission-failed → menu. Complete a short level
(or cheat) → mission-complete → auto-advance to next briefing.
7. MP chain (lower priority): MP options → scenario/stage/char/handicap/
teams → MP match loads.
Do NOT fix here (log/confirm in `GRAPHICS-BACKLOG.md`): D75, D76, D77,
D114/D116.
Deliverable: front-end crash list + root causes + fixes;
`GRAPHICS-BACKLOG.md` updated with confirmed cosmetic state.
### WS3 — D88.4 propDefs converter (cross-level blocker)
Goal: `Usetup*Z` `propDefs` converts correctly offline for all 21 levels
so `setupDoor` / prop instantiation stops reading BE garbage.
Method: subagent brief, same shape as D69 / D88.1-3 (offline sidecar
converter). Byte-spec every `PROPDEF_*` record type (~40, enum
`bondconstants.h`), per-type field byteswap + pointer-width audit,
extend `tools_pc/d88_emit.py`. Use `d88_propdefs.py` /
`d88_layoutprobe.c` / `d88_propdef_scan.py`. Resolve the placeholder
sizes (`VEHICLE`/`AIRCRAFT`/`TANK`/`AMMO`/`DEPOSIT_IN_ROOM`) via
`d88_layoutprobe.c`.
Files: `tools_pc/d88_emit.py`, `tools_pc/d88_propdefs.py`,
`tools_pc/d88_layoutprobe.c`, + narrow `#ifdef PORT` in `src/game/prop.c`
if a struct-width mismatch surfaces (D79/D88 class).
Budget: structural — generous. On expiry: write up spec progress in
§F/D88 with confidence rating.
Verify: `-level_09` walks past `setupDoor` to a rendered BUNKER1 frame
with props; spot-check 2-3 other levels' prop walk.
### WS4 — Per-level load + render sweep
Goal: status matrix for all 21 solo levels — (a) `lvlStageLoad`
completes without fault, (b) ≥1 non-degenerate frame renders, (c)
survives ~300 frames / ~15 s with no input (immediate-crash check only).
Prereq: WS1 + WS3 landed.
Method: agent-driven, one level at a time (`-level_XX`). Each: launch
`GE_PCDUMP="1-300:30"`, capture `ge007.crash.log`, run `pixcount.py`.
Record PASS / CRASH@<site> / RENDERS-EMPTY / HANG. Symbolicate every
crash (`addr2line`). Group failures by root-cause class.
Deliverable: `docs/LEVEL-STATUS.md` — table (level, number, load, render,
crash site, notes), refreshed as WS5 closes items.
### WS5 — Per-level crash triage (iterative)
Goal: drive the WS4 matrix to "all 21 load + render".
Method: one focused investigation per distinct failure class (subagent
brief per `AGENT-WORKFLOW.md` template; read `PORT-LEARNINGS.md` first).
Expected classes: pointer-width struct growth in per-level assets,
BE-rodata not converted, N64-Gfx/Vtx sizing, per-level pool exhaustion
(`memallocstringtable` PC bumps, cf. D95). Fix = narrow `#ifdef PORT` or
converter extension; document each in §F.
Verify per fix: target level's WS4 check → PASS *and* `-level_09`
framediff stays green.
### WS6 — Human completion-validation pass (user-run, after WS4 green)
Goal: confirm each level is playable start to finish and matches the
original — the part needing real input.
Deliverable produced for the user: `docs/LEVEL-PLAYTEST.md` — per-level
checklist template: spawn point; each difficulty-gated objective
(`objective_status.c`) reachable and registering COMPLETE; guards / props
/ doors behave; level exit / mission-complete fires clean; timing +
layout match retail. User fills it per level; findings feed a WS5-style
triage round.
## Verification (overall)
- Per merged patch: `export PATH="/c/msys64/mingw64/bin:$PATH" &&
./build-pc.sh ntsc-final` green, `/linkcheck` clean, single-frame
`GE_PCDUMP` diff vs `tools_pc/golden/` via `framediff.py`.
- WS1: `-level_09` alone == old manual-arg behaviour.
- WS3: BUNKER1 renders props past `setupDoor`.
- WS4/WS5: `docs/LEVEL-STATUS.md` all 21 rows = load+render PASS.
- Regenerate sidecars after any converter change: `python
tools_pc/d43_emit.py ntsc-final && python tools_pc/d69_emit.py
ntsc-final && python tools_pc/d88_emit.py ntsc-final --regen`.
## Out of scope (tracked elsewhere — do not start here)
- D75 / D76 / D114 / D116 / D77 and all `docs/GRAPHICS-BACKLOG.md` items.
- D118a/b input polish, rebinding, gamepad hotplug, `ge007.ini`.
- Weapon-model-doesn't-draw (AUDIT-M6 #5), rest of the `struct player`
offset audit.
- Audio (Phase 3), saves/EEPROM (Phase 4), `docs/BACKLOG.md` (PC
graphics options, co-op, netplay).
-459
View File
@@ -1,459 +0,0 @@
# SMALL-FIXES — ready-to-execute fix notes for small / low-level open items
**Read-only document.** This file records *proposed fixes and verification
procedures* for the small, low-level bugs and noted issues that are parked in
`BACKLOG.md`, `GRAPHICS-BACKLOG.md`, `HANDOFF.md`, and the §F/§H finding log.
No code was changed when writing it (M-28). Each item is self-contained enough
to be picked up cold; items marked **VERIFY-ONLY** already have their fix in
the tree and only need confirmation + commit hygiene.
Standing rules for every item below:
- Game-code edits must stay inside the narrow `#ifdef PORT` ABI/layout
exception (AGENTS.md #2); port-layer work has no such limit. Anything
bigger than described here → stop, write it up with a confidence rating.
- New findings get the next free Dxx in `docs/PCPortResearch.md` §F/§H + an
index row; recurring bug classes go to `docs/PORT-LEARNINGS.md`.
- After any converter change: full regen chain
`python tools_pc/d43_emit.py ntsc-final && python tools_pc/d69_emit.py
ntsc-final && python tools_pc/d88_emit.py ntsc-final --regen`.
- Verification anchors (quiet machine only — D153): target level boots to ≥1
non-degenerate frame (`tools_pc/pixcount.py`), `-level_09` + `-level_20`
unregressed (`tools_pc/framediff.py`, use `--mask` for the HUD).
- Crashes: `tools_pc/debug.ps1` (gdb + FATAL line + backtrace → `gdb.txt`).
---
## A. VERIFY-ONLY — fix is in the tree, needs confirmation
### A1. D154 — `bg.c` room-geometry hit-test GBI parser (committed WIP `072b5c44`)
**Status:** written + builds green; **never exercised** (a no-input
`GE_PCDUMP` capture fires no bullets, and the M-28 box was thrashed).
**What it is:** `bgTestRayIntersectionInRoom` (`src/game/bg.c:~3331`, called
from `bgTestBulletHitBackground` on every shot that resolves against
background geometry) was the D135 sibling: raw 8-byte-N64-`Gfx` byte/word
indexing into the 16-byte PC room DL → desync → OOB `vtxbase[idx]` / walk-off
AV the first time a wall or floor is shot. The `#ifdef PORT` port reads
`gdl->words.w0/.w1` (G_TRI1 from `(w1>>16/8/0)&0xff/10`; G_TRI4 nibbles from
`(u32)words.w0/.w1` with the byte-mapping table in the code comment); both
`texturenum` recoveries return `-1` (generic impact decal/sound, parked w/ D77).
**Procedure:**
1. Quiet machine. `./tools_pc/debug.ps1 -level_09`.
2. Get into a firefight and **shoot walls/floor deliberately** (not just
guards) for ~60 s — this is the only trigger.
3. Pass = no AV in `bgTestRayIntersectionInRoom` / `bgTestBulletHitBackground`,
bullets still resolve hits (decal/sound may be generic).
4. `-level_09` + `-level_20` framediff unregressed.
5. Then fix up the commit (`072b5c44` is labelled "WIP, unverified") and close
the §F D154 row.
**Risk:** low — `#ifdef PORT` only, N64 path verbatim under `#else`. If a new
fault appears in this function, the nibble-mapping table is the thing to
re-derive against `rsp/graphics/gmain.s` (ground truth for GBI decoding).
### A2. D139 — stage-unload `cleanupObjects` type byte (fix committed M-23)
**Status:** fix in (`src/game/cleanup_objects.c`, `#ifdef PORT`
`CLEANUP_PDTYPE(o)` = `((PropDefHeaderRecord*)o)->type` instead of `(u8)obj[0]`,
which on LE reads `extrascale` and walks off the propDef blob). **Never
verified** — the fast teardown test hit D140 first (since fixed), and M-27's
level-exit loop was playtested through the *real* front-end flow, which does
exercise `lvlUnloadStageTextData` → `cleanupObjects` on level exit.
**Procedure:**
1. Re-check whether M-27's Dam→Facility playthrough already exercised it:
if a real level exit completed without a teardown crash after the D139
commit, this is done — just record "verified via M-27 exit loop" on the
§F row and stop.
2. Otherwise: `debug.ps1 -level_09`, complete/abort a real level exit (not
pause-abort) and confirm no AV in `objFree` / `objFreeEmbedmentOrProjectile`.
**Risk:** low — one-line read-source change, every other propDef consumer
already uses the struct member.
---
## B. Root-caused small fixes (not yet applied)
### B1. D74 — dead + OOB texture-wrap block in fast3d (`port/fast3d/gfx_pc.cpp:~1570`)
**Status:** latent. Harmless for CLAMP glyphs today; could corrupt wrapped
textures on triangles (room panels, model skins).
**Root cause (confirmed against the tree):** two independent defects in the
D74 pre-wrap block inside the per-vertex UV loop:
1. **Always-false guard.** The codebase's `gbi.h` (`include/PR/gbi.h`, used by
both game and fast3d via `port/shim/PR/gbi.h`) encodes wrap mode as a
2-bit field: `G_TX_NOMIRROR|G_TX_WRAP = 0x0`, `G_TX_MIRROR = 0x1`,
`G_TX_CLAMP = 0x2`. So `cms & G_TX_WRAP` is `& 0` — **always false**. The
"is this tile in wrap mode" test must be **"clamp bit not set"**:
`(cms & G_TX_CLAMP) == 0`. (Compare the correct switch at
`gfx_opengl.cpp:706-712`, which keys on the four combinations.)
2. **OOB index.** `tex_width2[i]` / `tex_height2[i]` are **per-texunit `[2]`
arrays** (set up at `gfx_pc.cpp:~1471`), but the block is inside the
**per-vertex** loop where `i` is 0..2 → read of slot `[2]` if the guard
ever fires.
**Fix:**
1. Hoist the tile-window computation out of the vertex loop: for each used
texunit `t`, compute once — `tw = tex_width2[t]`, `th = tex_height2[t]`,
`wrapS = (cms & G_TX_CLAMP) == 0 && tw < tex_width[t]`, same for T.
2. In the vertex loop, apply `u = fmodf(u, tw); if (u<0) u += tw; u += uls/4`
(and V) only when the precomputed flag is set.
3. Delete nothing else — leave the surrounding D74 comment, extend it with a
line noting the `G_TX_WRAP == 0` encoding trap (PORT-LEARNINGS §D class:
"N64 hardware idioms fast3d does not emulate" — add the bit-encoding note).
**Verify:** build; `-level_09` + `-level_20` framediff; then a level with
visible wrapped triangle textures — **Depot (`-level_30`)** is the known
worst case (overlaps B2, but D74's block only fires for WRAP tiles). Check
`tools_pc/golden/` dumps for no new garbage.
**Risk:** low-medium. The block has never run, so this is *new* behavior —
if wrapped textures suddenly look wrong, the fmod window size (`tw` from
`lrs-uls+4`/4) is the thing to validate against `rsp/graphics/gmain.s`'s
tile-window handling before "fixing" it further.
### B2. D152 — find the leaking `osSetIntMask` site + proper narrow fix
**Status:** mitigated (self-healing lock in `port/src/libultra.c`, M-28) — a
leaked section now costs ~2 s + a log line instead of a permanent black
screen. The **root leak is still unidentified**.
**Procedure to root it:**
1. Replay the trigger: Facility (`-level_34`), kill Trevelyan → mission-fail
fade-out, under `tools_pc/debug.ps1`.
2. When the log shows `D152: ... stealing from owner=...`, take the logged
stale-owner address **and** the stealing caller's return address;
`addr2line -e build-pc/ge007.x86_64.exe -f -C <both>` (image base
`0x140000000`). The *owner* is the leak site.
3. Audit target: libaudio's `alEvtqPostEvent` / `alEvtqNextEvent` /
`sndSetSfxSlotVolume` paths (`src/libultra/audio/event.c`, `src/snd.c`) —
look for early returns between an `osSetIntMask(OS_IM_NONE)` and its
matching `OS_IM_ALL` (the D147 assumption that "the decomp never blocks /
never early-returns while masked" is falsified on this path). Also check
transient threads that acquire-and-exit (`gdb.txt` showed New Thread /
exited churn every few k frames).
**Proper narrow fix (once the site is known), pick one:**
- **(a) Preferred:** a dedicated `ALEventQueue` lock (mutex per queue or one
global in the port layer) taken by `alEvtqPostEvent`/`alEvtqNextEvent`, so
the audio event paths stop using the process-wide `osSetIntMask` section at
all; then the self-healing steal becomes dead weight that can be reverted to
the plain D147 recursive mutex.
- **(b) Cheaper:** make the leak site balanced (the actual bug), keep the
self-healing lock as belt-and-braces.
**Verify:** replay the mission-fail 3×: no `D152:` steal log lines, fade-out
completes, screen returns to the mission-failed UI. `-level_09` framediff.
**Risk:** (a) is port-layer only if the lock lives in the shimmed
`alEvtq*` wrappers — but note `event.c` is *compiled game-side libultra*, so
the lock likely has to go inside `event.c` under `#ifdef PORT` (ABI-exception
class, document as Dxx). Keep the self-healing lock either way until audio
(Phase 3) lands.
### B3. D120 — blood-stain hang: extend `d43_emit.py` for opcode-0x18 — **DONE (M-30)**
**Status:** FIXED (`tools_pc/d43_emit.py`, commit `da98cdf1` + validation
`4a7ab609`). The `CollisionVertices` sub-array + its `.index` /
`CollisionRelatedNode` / `CollisionRelatedIndex` fields were already converted
(`op24_is_collision` path). The real gap was narrower than this note assumed:
**`PointUsage[]` was reserved in the layout pass (`add_region(puo, 2*nv)`) but
the emit pass never wrote it → all-zero → `chr.c`'s decal walk
`index = PointUsage[index]` cycled 0→0** (capped by the guard). Fix:
`op24_pointusage[puo] = nv` + a byteswap loop in the emit pass. Converter now
validates: 1469 regions / 70177 `s16` entries round-trip, every index
< numVertices, none all-zero. The `chr.c` guard is kept as a safety net.
**Verify:** BUNKER1 firefight — blood decals on shot guards should appear, no
hang. Regen chain must be re-run for the sidecar to update.
**Procedure:**
1. Byte-spec the record: dump one converted guard model's opcode-0x18 region
from a sidecar (`data/pccg-*`) and diff against the raw ROM bytes; write
out the N64 layout (fields, sub-array strides, terminators) in the
converter comment — same method as the D69/D88 audits.
2. Extend `d43_emit.py`: emit the widened struct with the 6 pointers via
`put_ptr`, and convert both sub-arrays (endianness + PC stride).
3. Keep the `chr.c` guard until verification passes, then decide whether to
keep it as a safety net (recommended — same philosophy as D85's bounds
check) or revert.
4. Regen full chain; verify: BUNKER1 (`-level_09`) guard firefight — blood
decals now appear and no hang; framediff anchors.
**Risk:** medium — converter work with a runtime guard underneath, so a bad
conversion degrades to today's behavior rather than crashing. Good subagent
brief on its own (per HANDOFF-ARCHIVE M-9).
### B4. B3 — RMB aim too sensitive (`port/src/input.c`)
**Status:** user-reported (M-28 playtest). Port-only.
**Current math (aim mode, `input.c:~471`):**
`m = 61 + |dx| * (MouseAimSpeed/100) * AIM_GAIN(4.0)`, clamped to
`60 + AIM_BAND(20)` = 80. At the default `MouseAimSpeed=50`, **25 px/poll
saturates the band** — a fast flick is "max stick" for many polls, and GE's
aim curve `(stick-60)/10` then runs at full rate the whole time.
**Fix (in order of cheapness):**
1. Lower the default: `mouseAimSpeed = 50 → 25` (`input.c:~128`). One line;
users can still dial it back up in `ge007.ini`.
2. Make the band itself tunable: register `Input.AimBand` (int, 5..40,
default 20) and replace the `AIM_BAND` constant in the two clamp sites —
a smaller band = finer control near the gate, a larger one = more headroom.
3. Optional curve change if it still feels bad: make the mapping into the
band sub-linear (`m = 61 + AIM_BAND * sqrt(gx / saturating_px)`) so small
movements get proportionally less stick — but only after 1+2 are
playtested; don't stack changes.
**Verify:** playtest-only (no headless signal): aim at a guard in BUNKER1,
flick the mouse, confirm no overshoot orbiting the target; hipfire turn
(`MouseTurnSpeed`) is untouched by this.
**Risk:** trivial — port-only, config-backed, default-reversible.
### B5. B4 — menu screens need an absolute pointer mode (`port/src/input.c`)
**Status:** user-reported (M-28). Port-only design item; **no code yet**.
**Problem:** front-end / watch menus with the crosshair cursor behave like
an analog-stick game, not a PC pointer. Today all mouse input is *delta*
based (turn/look), and menu navigation goes through `joyGetStickX/Y` level
checks in `front.c` / `options.c` — synthetic stick deltas make menu cursors
crawl or slam.
**Design (port-only, no `src/` change):**
1. Add a **menu-pointer mode** to the input layer: when active, emit
*absolute* stick values derived from the cursor position —
`sx = (mouseX / windowW) * 2 - 1 → [-80..80]`, same for Y. GE's front-end
maps stick deflection to on-screen cursor position, so absolute→absolute
gives true pointer behavior with zero game-code changes.
2. **Mode detection is the open question** — the port doesn't know when the
game is in a menu. Options, cheapest first:
- **(a)** Heuristic: mouse is *ungrabbed* (cursor visible) ⇒ menu mode.
Pair with a rule that front-end screens release the grab and gameplay
grabs it (the grab state already flips on focus loss; extend it to be
driven by a key, e.g. hold-to-release, or auto-detect via the same
signals D145 used for ESC-as-B). Simple, no game-state peeking.
- **(b)** Peek a game-side "am I in the front end" flag through an existing
exported symbol (check `front.c` / `lv.c` globals) and branch on it in
`inputPoll`. Read-only access to a global is arguably inside the
documented exception class, but **record a Dxx** if used.
- **(c)** Explicit user toggle (config key + hotkey). Ugly but zero risk.
3. While in menu mode: suppress aim/turn delta emission entirely; wheel and
clicks map to A/B as today.
**Verify:** drive the mission-select / SELECT FILE screens with the mouse —
cursor should track 1:1, click selects; in-game aim unaffected.
**Risk:** low (port-only) under option (a)/(c); (b) needs a Dxx write-up.
Sequence *after* B4 so the gameplay feel is settled first.
### B6. D118d latent sibling — `front.c` menu-nav stick level checks
**Status:** latent, noted M-27. Same bug class as the fixed watch-menu
over-scroll (`options.c`, `GE_WATCH_STICK_FAST{UP,DOWN}` → 0 under PORT).
**What to do:** find the `joyGetStick*InRange` / raw `joyGetStickY < -0x46`
style *level* checks in `front.c` menu navigation (main menu, mode-select,
mission-select paging) and apply the same treatment: under `#ifdef PORT`
drop the stick fast-scroll term so keyboard/digital input goes through the
latched single-step path only. Keep N64 verbatim under `#else`.
**Verify:** keyboard W/S (and D-pad) paging on every front-end menu = one
step per press, no multi-skip; controller sticks still fast-scroll.
**Risk:** low — mechanical copy of an already-landed fix. Do it together with
B5 so the menu-input area is touched once.
### B7. D118a residual — hipfire pitch is digital, yaw is analog
**Status:** parked minor feel issue. The *full* fix needs a game-side hook;
the deferred design is:
An `#ifdef PORT` analog-aim hook in `bondview.c`/`bondview2.c` where GE reads
digital C-up/C-down for hipfire pitch — replace the digital read with the
analog stick-Y value (which our input layer can emit even in hipfire, since
stick-Y is "move" only *when the game consumes it as such*; validate that
emitting a small stick-Y does not also make Bond walk). This is the first
*behavioral* `src/` edit of the aim path — it exceeds pure ABI/layout; **write
it up with a confidence rating before implementing** (AGENTS.md #2: "stop and
write it up"), and gate it behind a config key defaulting to *current*
behavior.
**Cheaper alternative worth trying first:** raise `MOUSE_PITCH_THRESH`
granularity by emitting the hipfire pitch as *repeated C-button pulses with
dwell proportional to speed* — pure port-layer, approximates analog. Try this
before touching `src/`.
---
## C. Cosmetic / graphics — investigation plans (root cause not fully known)
These are parked below crash/level work (`GRAPHICS-BACKLOG.md`). Each entry is
the *next diagnostic step*, because the root cause is not yet pinned. Do not
treat them as ready-to-apply fixes.
### C1. D114/D116 — HUD text + ammo digits X-mirrored per glyph
**Hard rule (PORT-LEARNINGS §D2): do NOT re-run the static trace** (four
sessions did; every stage verified non-mirrored at the GL boundary) and do NOT
apply a global texrect S-swap (mirrors the whole screen).
**Next attempt — pick one, in order:**
1. **RenderDoc (or apitrace) capture of exactly one glyph's texrect draw.**
The contradiction is "VBO verified non-mirrored but pixels mirrored" — a
GPU-side capture resolves it in one shot: inspect the bound texture's
upload bytes vs the `gDPLoadTextureBlock` source, and the final quad's UVs.
2. **Asymmetric-1-texel experiment:** temporarily force one HUD glyph's image
to a 2×1 or 3×1 *asymmetric* pattern (e.g. left half white) in the port
layer; if it renders right-half-white, the flip is between upload and
sample (texture origin / `glPixelStorei(GL_UNPACK_ROW_LENGTH)` / Y-flip);
if it renders correctly, the mirror is applied *per-glyph placement*
(texrect S-range inversion in `textRenderGlyph`'s GBI — check the
`gDPTile`/`gDPSetTile` uls/lrs signs for the HUD tiles specifically).
Suspects to check with the capture in hand: 4-bit CI tile upload stride vs
`G_IM_SIZ_4b` sampling, TLUT index endianness (wrong-but-symmetric TLUT would
not mirror — deprioritize), and the `uls/lrs` sign convention for *rect*
draws (D116 is a rect path; triangles were never suspected).
### C2. D75 — front-end 3D models misplaced / absent (logo, gun-barrel Bond, cast)
Triage per §F D75 — **distinguish (a) vs (b) before any fix:**
1. For the Nintendo logo and one cast model, determine which matrix path
builds their transforms: `guRotate`/`guLookAt` (→ candidate (a), D73
scope gap in `guint.h` `DVAL()`) or the `animInit` + embedded raw-offsets
into `struct player` path (→ candidate (b), independent model/RW-pool bug,
cf. D56/D100/D102 — all now-fixed struct-pun classes; a *new* instance of
the class is the leading hypothesis).
2. Capture `GE_PCDUMP` frames across the logo transition and gun-barrel/cast
segments to localize which frame first diverges.
3. Files: `src/libultra/gu/guint.h`, `src/game/model.c`, `bondview2.c`
(animInit / modelSetScale), `port/fast3d/gfx_pc.cpp`.
**Note:** D148/D149 below are the same family — fixing D75's root cause will
likely take them with it. The D149 garbled geometry is also worth one check on
the *converter* side: the malformed sub-DL (`seg5+0x9ee4`, unresolved matrix
ptr + garbage opcodes) may be a `d43_emit.py` conversion defect rather than a
runtime transform bug — dump that sub-DL's raw ROM bytes vs the sidecar before
assuming it's runtime.
### C3. D76 — disclaimer screen only partially drawn
Likely the D68 image-table class, incomplete: some `sImageTableEntry`s the
disclaimer references are missed by `port/src/gimgfixup.c`'s bswap/shadow-sync.
**Procedure:**
1. Enumerate the image entries the disclaimer screen references (trace
`textRender`/`display_image_at_position` calls in the legal-screen code, or
log every `gDPLoadTextureBlock` source during PPM frames f20–f100).
2. For each, confirm it is covered by the D68 fixup (bswapped IMAGESEG w1 +
synced compiled globalDL shadow). The "only the classification line + one
line below draw" symptom = exactly the covered subset rendering.
3. Files: `port/src/gimgfixup.c`, `src/game/tex.c`, image-table consumers.
### C4. D148 / D149 — Dam exit cutscene absent; MISSION COMPLETE models garbled
Both are D75 family (see C2). Additional notes:
- **D148:** the missing piece is a *scripted* sequence — cinematic camera +
Bond rappel anim. Once D75's model path is fixed, verify the cutscene
trigger still fires (check whether the exit trigger even dispatches the
cinematic on PC — it may be gated on a front-end state flag our flow skips;
`GE_STARTMENU` tooling can crash-test the screen in isolation).
- **D149:** see the converter-side check in C2. D144/D146 already keep the
game alive on the corrupt DL; the fix should make the model *correct*, then
D146's "unknown opcode → end DL" guard can stay as a safety net.
### C5. D85 (residual) — room DL `texLoadFromGdl` decode
**Status:** OPEN but safety-netted; geometry renders, so this is below level
work. Remaining: decode the room-specific opcodes / CC-RM-LUT markers that
`texLoadFromGdl` still handles generically, and verify `csize_*_DL_binary`
sizing. Only worth touching if a level shows placeholder/garbage textures on
room geometry that can't be explained by B2 (Depot wrong-colors).
---
## D. Housekeeping / cleanup (no gameplay effect)
### D1. Stale §F index row — D132 says "proposed fix (not applied)"
The D132 fix **is applied** in the tree: `tools_pc/d88_propdefs.py`
(`PROPDEF_PC_BYTES[14/19]=32, [44]=40`; the `(14,19,38,44)` handler emits each
`Index{k}` into the low 4 B of the 8-aligned slot at PC `8+8*k`) and
`src/game/loadobjectmodel.c sizepropdef()` (LINK/SWITCH/LOCK_DOOR → 8 words,
SAFE_ITEM → 10). The §F index row still reads "proposed fix (not applied) —
see below" while the subsection says "APPLIED (M-20, commit pending)".
**Action:** update the index row to `resolved (M-20)` and confirm the M-20
commit landed (git-blame both files). Docs-only.
### D2. Stale BACKLOG technical-cleanup items
`docs/BACKLOG.md` "Technical cleanup" lists:
- ~~"Add a D24-implications bug class to PORT-LEARNINGS"~~ — **done M-19**
(PORT-LEARNINGS §E, lines ~374).
- ~~"Watch item: `osYieldThread` = `Sleep(0)`"~~ — **done M-19** (§E, line
~407). Keep the watch item itself; it's still accurate (the one place we
fake cooperative scheduling — first suspect for level-sweep hot-loop
misbehavior under host load).
- "Remove TEMP D63 debug scaffolding" — **partially done M-19** (`libultra.c`
+ game-side probe blocks removed). Remaining: `port/fast3d/gfx_pc.cpp`
trail code — the `/* TEMP D63 (strip before commit) */` block at ~2462, the
`GE_D63`-gated dram-branch trace at ~2516-2531, and the unknown-opcode dump
at ~2759-2765. All env-gated / inert; strip in one pass (keep the
`cached = getenv("GE_D63")` pattern only if a probe is retained — recommend
removing all of it).
**Action:** one docs+code cleanup commit: mark the two done items, strip the
gfx_pc.cpp D63 trail, rebuild + `-level_09` framediff.
### D3. `osYieldThread = Sleep(0)` watch item (no action — recorded)
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. See PORT-LEARNINGS §E.
---
## Status (updated M-30)
- **B3 / D120** — DONE (PointUsage emit + validation).
- **B4** — DONE (M-29 `0bd0ceec`).
- **B5** — DONE (M-30 velocity mode; **M-31 D165** replaced it with a true
~1:1 P-controller pointer — `Input.MenuPointerMode` 1=pointer default,
0=old velocity). Feel-check owed.
- **B7 / D118a** — DONE (M-31 **D166**, cheaper port-only alternative):
hipfire pitch is now speed-proportional C-button pulses, not a digital
threshold. `Input.HipfirePitchSpeed` tunes it. The full game-side analog
hook remains unimplemented (not needed unless the pulse still feels off).
- **B6** — investigated,
the `front.c` stick checks are already edge-gated / toggle-guarded, not a
real over-scroll (unlike the fixed options.c watch case). No-op.
- **RC4** (palette off-by-one, was in `C` / TEXTURE-GLITCH-ANALYSIS) —
RETRACTED: the current RGBA5551 decode is correct.
- **A2 / D139** — likely closeable: M-27's Dam→Facility playthrough ran a
real `lvlUnloadStageTextData` → `cleanupObjects` with no teardown crash
after the fix. Confirm the commit landed, then close the §F row.
- **D2 (D63 scaffolding strip)** — the `port/fast3d/gfx_pc.cpp` trail is
stripped (M-30 `d0789358`). The remaining `#if defined(PORT)` `GE_D63`
probe blocks in ~10 `src/game/*.c` files are inert but **some sibling
`getenv`/`static` lines nearby are NOT `#if defined(PORT)`-guarded** (e.g.
`bg.c:2880` `d63bgprimarycount`) — a strip pass must verify N64-build
safety first (Non-Negotiable #1). Low value; defer.
## Suggested order for what's left
1. **A1** (D154 wall-shoot GBI parser) — needs a firefight-into-a-wall
playtest, then close/commit.
2. **B1** (D74 wrap-block) — the in-place fix boot-crashes; needs the
hoist-out-of-vertex-loop rework + per-level visual check. Cosmetic, gate
behind a config flag like RC2. Risky, low priority.
3. **B2** (Depot ceiling blue-speckle) — RC2 didn't touch it; needs `GE_DTEX`
/ RenderDoc on that specific surface. RC1 (wallet-Bond) is the D75
front-end-model track.
4. **C1–C4** cosmetics — after in-level crashes / campaign playtest.
+111
View File
@@ -0,0 +1,111 @@
# Building the PC port
Building has two stages:
1. **Extract assets from your ROM** — a one-time step that uses the
decompilation's own toolchain to pull levels, models, textures, fonts and
music out of your ROM into `assets/`.
2. **Build the port** — a CMake build that compiles the game sources plus the
`port/` layer into a native executable.
You need a GoldenEye 007 N64 ROM you legally own (`.z64`, big-endian). See the
[Requirements table in the README](../README.md#requirements) for accepted
versions and hashes.
---
## 1. Dependencies
### Port build
| Need | Windows (MSYS2 MINGW64) | Debian/Ubuntu | macOS (Homebrew) |
|------|------------------------|---------------|------------------|
| toolchain | `mingw-w64-x86_64-toolchain` | `build-essential` | Xcode CLT / `gcc` |
| CMake | `mingw-w64-x86_64-cmake` | `cmake` | `cmake` |
| SDL2 | `mingw-w64-x86_64-SDL2` | `libsdl2-dev` | `sdl2` |
| zlib | `mingw-w64-x86_64-zlib` | `zlib1g-dev` | `zlib` |
| OpenGL | (in the toolchain) | `libgl1-mesa-dev` | (system) |
| Python 3 | `mingw-w64-x86_64-python` | `python3` | `python3` |
### Asset extraction (decompilation toolchain)
The extraction scripts need `binutils-mips-linux-gnu` (or an equivalent MIPS
binutils), `make`, `git`, `python3`, and `qemu-irix` for the IRIX IDO
compiler. On Windows this is easiest under WSL or a Linux VM. Full details and
alternatives (Docker, the recompiled IDO toolchain) are in
[`SetupGuide.md`](SetupGuide.md).
---
## 2. Extract assets
Put your **US** ROM at the repository root as `baserom.u.z64` (this name is
required by the extraction scripts; it is git-ignored and never committed),
then:
```sh
./scripts/extract_baserom.u.sh
```
For PAL or JP, additionally place `baserom.e.z64` / `baserom.j.z64` at the root
and run:
```sh
./scripts/extract_baserom.u.sh && ./scripts/extract_diff.e.sh # PAL
./scripts/extract_baserom.u.sh && ./scripts/extract_diff.j.sh # JP
```
(US extraction is a prerequisite for the others.)
This populates `assets/` with the generated `.bin` blobs the build needs. See
[`SetupGuide.md`](SetupGuide.md) for the in-depth build/asset pipeline.
---
## 3. Build
```sh
./build-pc.sh ntsc-final # or: pal-final / jpn-final
```
which is equivalent to:
```sh
cmake -S . -B build-pc -DROMID=ntsc-final
cmake --build build-pc -j
```
For PAL/JP you must first generate that region's ROM-asset symbol file
(the US one is committed):
```sh
python3 scripts/gen_romassets.py e # PAL -> port/src/romassets_e.s
python3 scripts/gen_romassets.py j # JP -> port/src/romassets_j.s
```
The executable lands at `build-pc/ge007.x86_64` (`.exe` on Windows). PAL/JP
builds are named `ge007.pal-final.x86_64` / `ge007.jpn-final.x86_64`.
---
## 4. Run
```sh
mkdir -p data
cp /path/to/your/rom.z64 data/ge007.ntsc-final.z64
./build-pc/ge007.x86_64 # run from the repo root
```
The port maps the ROM at the N64 cart address at runtime, so the ROM file
itself is still required to run — not just to build. `ge007.ini` is written
next to the working directory on first launch.
### Useful flags / env
| | |
|---|---|
| `-level_NN` | boot straight into a solo level (e.g. `-level_09` = Bunker 1) |
| `GE_PCDUMP="first-last:step"` | dump rendered frames as PPM (debugging) |
More diagnostic switches are catalogued in
[`dev/GE-ENV-PROBES.md`](dev/GE-ENV-PROBES.md).
+107
View File
@@ -0,0 +1,107 @@
# How this port is developed
Most of this port's work is not writing code — it is **diagnosis**: figuring
out which N64 hardware or ABI behaviour 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
[`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 behaviour, 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
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
- 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.
The budget scales with the project's maturity. Early bugs are cheap (one-line
truncations, off-by-ones) and get a tight cap; once those are gone the
survivors are structural (matrix handedness, serialization formats) and get
more room.
## 2. A shared, append-only learnings file
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,
one screen, each entry an index into the full [`dev/findings.md`](dev/findings.md)
log. Every investigation brief links it, and every investigation ends by
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;
- 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,
LOD/detail tiles, segment-address folds);
- whole serialized formats are converted by an **offline sidecar** rather than
patched at runtime.
## 3. Parallelise by file, consolidate often
Lewis ran multiple git worktrees and found the real cost was
*synchronisation*. For parallel investigation agents the same rule applies:
- **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
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.
## 4. Spend the cheap automated pass before spending agent time
Before dispatching an investigation agent:
- confirm the build is green and the link is clean;
- run the existing env-gated probes for the area and attach their output;
- 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.
Then hand the agent the *findings*, not just the problem.
## 5. What does not transfer from the decomp workflow
- **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
contract the already-readable C depends on.
## Investigation-brief template
```
TASK: <one bug, one subsystem>.
READ FIRST: docs/porting-notes.md; docs/dev/findings.md <specific Dxx entries>.
FILES YOU MAY TOUCH: <disjoint from any other in-flight work>.
KNOWN-GOOD / RULED OUT: <list — do not re-investigate>.
PRE-FLIGHT ATTACHED: <probe output, baseline capture, PD-port pointer>.
BUDGET: <N build->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.
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.
```
@@ -1,9 +1,9 @@
# Audio Plan — Phase 3 (libaudio → SDL) # Audio Plan — Phase 3 (libaudio → SDL)
Status: **draft for evaluation** (written read-only; no code changes made). Status: **draft for evaluation** (written read-only; no code changes made).
Supersedes the audio paragraphs of §6 in `docs/PCPortResearch.md` as the plan of record Supersedes the audio paragraphs of §6 in `docs/internals.md` as the plan of record
for closing **D77** (no audible music/sound on PC). Companion docs: `docs/HANDOFF.md`, for closing **D77** (no audible music/sound on PC). Companion docs: `docs/HANDOFF.md`,
`docs/PORT-LEARNINGS.md`. `docs/porting-notes.md`.
--- ---
@@ -88,7 +88,7 @@ problem.
| `aEnvMixer` | env.c:410/414 | yes — `aEnvMixerImpl` | | `aEnvMixer` | env.c:410/414 | yes — `aEnvMixerImpl` |
| `aADPCMdec` | load.c:483 | yes — `aADPCMdecImpl` (SSE4.1/NEON/scalar) | | `aADPCMdec` | load.c:483 | yes — `aADPCMdecImpl` (SSE4.1/NEON/scalar) |
PD reference = `PD_PORT_CHECKOUT` + PD reference = the Perfect Dark port `port/src/mixer.c` +
`port/include/mixer.h`. Reusable: the DSP *math* (ADPCM 8-coefficient loop filter, `port/include/mixer.h`. Reusable: the DSP *math* (ADPCM 8-coefficient loop filter,
resample table + interpolation, envelope ramping, clamps). **Not** reusable as-is: resample table + interpolation, envelope ramping, clamps). **Not** reusable as-is:
signatures and argument conventions (naudio ABI ≠ IDO ABI, §5.3). signatures and argument conventions (naudio ABI ≠ IDO ABI, §5.3).
@@ -305,16 +305,16 @@ Record as a new Dxx finding when implemented (next free label after D127).
aSaveBuffer` (+`aSegment` no-op) in `mixer.c`, reusing PD's DSP math where the ABI aSaveBuffer` (+`aSegment` no-op) in `mixer.c`, reusing PD's DSP math where the ABI
maps. **Milestone: SFX audible (gunshots, doors); dry music plays.** Expect at least maps. **Milestone: SFX audible (gunshots, doors); dry music plays.** Expect at least
one Dxx-class finding here (state-layout or endianness surprise is the usual bug one Dxx-class finding here (state-layout or endianness surprise is the usual bug
class — see `docs/PORT-LEARNINGS.md`). class — see `docs/porting-notes.md`).
- **Phase 3 — FX.** `aPoleFilter` + validate the full `reverb.c` chorus path against - **Phase 3 — FX.** `aPoleFilter` + validate the full `reverb.c` chorus path against
the disassembly. **Milestone: music matches N64 character (chorus/reverb present, no the disassembly. **Milestone: music matches N64 character (chorus/reverb present, no
pumping); close D77.** pumping); close D77.**
- **Phase 4 — Polish & bookkeeping.** Resolve Q1; remove/adjust temporary probes and - **Phase 4 — Polish & bookkeeping.** Resolve Q1; remove/adjust temporary probes and
`GE_DAUDIO` defaults; latency/buffer tuning (`Audio.BufferSize`, `QueueLimit`); `GE_DAUDIO` defaults; latency/buffer tuning (`Audio.BufferSize`, `QueueLimit`);
host thread-priority tuning per A3 *only if* underruns were observed in Phases 2–3; host thread-priority tuning per A3 *only if* underruns were observed in Phases 2–3;
record findings in `docs/PCPortResearch.md` §F/§H (next Dxx after D127) + update the record findings in `docs/internals.md` §F/§H (next Dxx after D127) + update the
§F index, `AGENTS.md` Phase 3 status, `docs/HANDOFF.md`; append recurring bug classes §F index, `AGENTS.md` Phase 3 status, `docs/HANDOFF.md`; append recurring bug classes
to `docs/PORT-LEARNINGS.md`. to `docs/porting-notes.md`.
## 11. Verification ritual (audio-specific, on top of the standard one in AGENTS.md) ## 11. Verification ritual (audio-specific, on top of the standard one in AGENTS.md)
@@ -116,9 +116,9 @@ RW pool dedicated storage, and `gunRenderFirstPersonGunModels` /
## §F pointer ## §F pointer
Add to `docs/PCPortResearch.md` §F after D112: *“**D115 (audit)** — Add to `docs/internals.md` §F after D112: *“**D115 (audit)** —
`struct player`/`struct hand` raw-offset pass: see `struct player`/`struct hand` raw-offset pass: see
`docs/AUDIT-M6-player-offsets.md`. Live HIGH bugs = `gunfire.c` THROWMTX/POS/PREV `docs/dev/AUDIT-M6-player-offsets.md`. Live HIGH bugs = `gunfire.c` THROWMTX/POS/PREV
macros (`+handoffset+0xAD8/0xB08/0xB48`), not PORT-adjusted; fix = field macros (`+handoffset+0xAD8/0xB08/0xB48`), not PORT-adjusted; fix = field
accessors. MED = D102 weapon `render_pos` transient-arena aliasing (likely accessors. MED = D102 weapon `render_pos` transient-arena aliasing (likely
the ‘weapon model doesn’t draw’ cause) and the D56 watch-Model pool the ‘weapon model doesn’t draw’ cause) and the D56 watch-Model pool
File renamed without changes.
@@ -10,7 +10,7 @@ once the game is playable start-to-finish without crashes.
|----|---------|----------------| |----|---------|----------------|
| **D114/D116** | HUD text + ammo digits render each glyph individually X-mirrored (screen positions correct, texture content flipped). Likely also explains "inverted" guard skins / mislocated door props. | Root cause NOT found after 4 sessions (M-6/7/8/11). Every stage `textRenderGlyph`→GBI→fast3d→`buf_vbo`→GL runtime-verified non-mirrored (`GE_D116` `[D116/vbo]` probe). Contradiction unresolved. **Next attempt requires a RenderDoc/apitrace capture of one glyph texrect, or the asymmetric-1-texel-texture experiment** — do NOT re-run the static trace, and do NOT apply a global texrect S-swap (mirrors the whole screen). | | **D114/D116** | HUD text + ammo digits render each glyph individually X-mirrored (screen positions correct, texture content flipped). Likely also explains "inverted" guard skins / mislocated door props. | Root cause NOT found after 4 sessions (M-6/7/8/11). Every stage `textRenderGlyph`→GBI→fast3d→`buf_vbo`→GL runtime-verified non-mirrored (`GE_D116` `[D116/vbo]` probe). Contradiction unresolved. **Next attempt requires a RenderDoc/apitrace capture of one glyph texrect, or the asymmetric-1-texel-texture experiment** — do NOT re-run the static trace, and do NOT apply a global texrect S-swap (mirrors the whole screen). |
| **D74 wrap-block** | `gfx_pc.cpp:1546/1551` PORT block is dead code: guard `cms & G_TX_WRAP` with `G_TX_WRAP==0` is always false; and it indexes per-texunit `[2]` arrays with the vertex index `i` (0..2) → OOB if reached. | Latent. Harmless for CLAMP glyphs; could affect wrapped textures on triangles (room panels, model skins). Fix: `cms == G_TX_WRAP`, and compute the tile window once per tile not per vertex. Verify against `tools_pc/golden/` after. | | **D74 wrap-block** | `gfx_pc.cpp:1546/1551` PORT block is dead code: guard `cms & G_TX_WRAP` with `G_TX_WRAP==0` is always false; and it indexes per-texunit `[2]` arrays with the vertex index `i` (0..2) → OOB if reached. | Latent. Harmless for CLAMP glyphs; could affect wrapped textures on triangles (room panels, model skins). Fix: `cms == G_TX_WRAP`, and compute the tile window once per tile not per vertex. Verify against `tools_pc/golden/` after. |
| **D75** | Nintendo logo mispositioned; gun-barrel Bond model absent; cast-roll character models absent. (Rareware logo OK.) | Front-end 3D model path. Partially overlaps D114 (shared mirror on asymmetric content). Triage plan in `PCPortResearch.md` §F D75. | | **D75** | Nintendo logo mispositioned; gun-barrel Bond model absent; cast-roll character models absent. (Rareware logo OK.) | Front-end 3D model path. Partially overlaps D114 (shared mirror on asymmetric content). Triage plan in `findings.md` §F D75. |
| **D76** | 2D disclaimer screen only partially drawn. | §F D76. | | **D76** | 2D disclaimer screen only partially drawn. | §F D76. |
| **D77** | No audible music on PC. | Phase 3 (audio). §F D77. | | **D77** | No audible music on PC. | Phase 3 (audio). §F D77. |
| **D116 (HUD)** | (subset of D114) proportional-font strings + ammo counter mirrored. | Same root cause as D114. | | **D116 (HUD)** | (subset of D114) proportional-font strings + ammo counter mirrored. | Same root cause as D114. |
@@ -1,7 +1,7 @@
# HANDOFF ARCHIVE — session-by-session narrative # HANDOFF ARCHIVE — session-by-session narrative
> **Reference only.** Current state, next task, and environment live in > **Reference only.** Current state, next task, and environment live in
> `docs/HANDOFF.md`; per-finding detail in `docs/PCPortResearch.md` §F/§H > `docs/HANDOFF.md`; per-finding detail in `docs/internals.md` §F/§H
> (indexed at the top of §F). This file is the frozen accumulation of > (indexed at the top of §F). This file is the frozen accumulation of
> prior sessions' handoff briefs (through session M-11), kept so the > prior sessions' handoff briefs (through session M-11), kept so the
> reasoning behind a fix is recoverable. Everything below is as-written > reasoning behind a fix is recoverable. Everything below is as-written
@@ -16,12 +16,12 @@
1. `CLAUDE.md` (auto-loaded) + `AGENTS.md` non-negotiables. 1. `CLAUDE.md` (auto-loaded) + `AGENTS.md` non-negotiables.
2. This file, top-to-bottom. 2. This file, top-to-bottom.
3. `docs/PORT-LEARNINGS.md` — recurring bug classes. 3. `docs/porting-notes.md` — recurring bug classes.
4. Dispatching a subagent? `docs/AGENT-WORKFLOW.md` FIRST. Every brief needs 4. Dispatching a subagent? `docs/dev-process.md` FIRST. Every brief needs
FILES / BUDGET / ON-EXPIRY / CONSTRAINTS / REPORT (see CLAUDE.md "Dispatch preflight"). FILES / BUDGET / ON-EXPIRY / CONSTRAINTS / REPORT (see CLAUDE.md "Dispatch preflight").
_Paste-ready brief. Authoritative context: `AGENTS.md`, _Paste-ready brief. Authoritative context: `AGENTS.md`,
`docs/PCPortResearch.md` §F (D69, D78-D102), `docs/BRIEF-D69-stage-load.md`._ `docs/internals.md` §F (D69, D78-D102), `docs/BRIEF-D69-stage-load.md`._
**NEW DIRECTION (2026-08-29): `docs/PLAN-linear-level-sweep.md`** — pivot **NEW DIRECTION (2026-08-29): `docs/PLAN-linear-level-sweep.md`** — pivot
to breadth-first: fix the boot→intro→menu→level-select path to to breadth-first: fix the boot→intro→menu→level-select path to
@@ -117,7 +117,7 @@ transition, then the next stage.
they rank BELOW level-progression and crash work. they rank BELOW level-progression and crash work.
4. **Rest of the `struct player` offset pass** — D115 fixed the HIGH 4. **Rest of the `struct player` offset pass** — D115 fixed the HIGH
`gunfire.c` THROW* bugs; #6 (watch-preview Model pool) + the broader `gunfire.c` THROW* bugs; #6 (watch-preview Model pool) + the broader
audit remain (`docs/AUDIT-M6-player-offsets.md`). audit remain (`docs/dev/AUDIT-M6-player-offsets.md`).
5. Add an f32 value spot-check to `d43_emit.py`'s verify pass (it only 5. Add an f32 value spot-check to `d43_emit.py`'s verify pass (it only
checks pointers/opcodes today — the D112 bug passed "ALL CHECKS PASSED"). checks pointers/opcodes today — the D112 bug passed "ALL CHECKS PASSED").
6. **GE_DETERM fixed-tick mode (§F D117)** — deferred as not-narrow; would 6. **GE_DETERM fixed-tick mode (§F D117)** — deferred as not-narrow; would
@@ -244,8 +244,8 @@ each nonzero OSContPad poll. Rebinding + gamepad hotplug are TODO (§F
D118). D118).
**Session M-6 — see §F D113/D114/D115, `docs/PLAN-M6-playable.md`, **Session M-6 — see §F D113/D114/D115, `docs/PLAN-M6-playable.md`,
`docs/AUDIT-M6-player-offsets.md`, `docs/AGENT-WORKFLOW.md`, `docs/dev/AUDIT-M6-player-offsets.md`, `docs/dev-process.md`,
`docs/PORT-LEARNINGS.md`.** D113: portal BFS is correct, not the void. `docs/porting-notes.md`.** D113: portal BFS is correct, not the void.
D114: matrix chain + converter verified clean, residual = shared fast3d D114: matrix chain + converter verified clean, residual = shared fast3d
mirror (open). D115: player raw-offset audit + `gunfire.c` THROW* fix mirror (open). D115: player raw-offset audit + `gunfire.c` THROW* fix
(uncommitted). Build green. (uncommitted). Build green.
@@ -345,7 +345,7 @@ Pattern: textures/text draw; **animated/skeletal character models never
appear**; static 3D (logos) appears but with a bad transform. Leading appear**; static 3D (logos) appears but with a bad transform. Leading
hypothesis is D75(b) — the animated-model path (`animInit` + raw offsets hypothesis is D75(b) — the animated-model path (`animInit` + raw offsets
into `struct player`, cf. D56) is broken independently of the D73 matrix into `struct player`, cf. D56) is broken independently of the D73 matrix
sin/cos fix. Full triage plan in `PCPortResearch.md` §F D75. sin/cos fix. Full triage plan in `findings.md` §F D75.
The rest of this document (below) is the **last known-good, committed** The rest of this document (below) is the **last known-good, committed**
status as of commit `8c9c6a2c` (D86+D87 resolved) — still accurate except status as of commit `8c9c6a2c` (D86+D87 resolved) — still accurate except
@@ -501,7 +501,7 @@ locations attract mode happens to pick).
(`gdb -batch -x cmds.txt -p <winpid>`, `<winpid>` = 4th column of (`gdb -batch -x cmds.txt -p <winpid>`, `<winpid>` = 4th column of
`ps -p <bashpid>`) works well and is fast for "watch a global for a `ps -p <bashpid>`) works well and is fast for "watch a global for a
legitimate vs. corrupted write" questions on an already-running, legitimate vs. corrupted write" questions on an already-running,
not-yet-crashed process — see `docs/PCPortResearch.md` §F environment not-yet-crashed process — see `docs/internals.md` §F environment
reminders for the exact recipe used to root-cause D87. reminders for the exact recipe used to root-cause D87.
## Non-negotiables (unchanged, see AGENTS.md) ## Non-negotiables (unchanged, see AGENTS.md)
@@ -509,7 +509,7 @@ locations attract mode happens to pick).
1. N64 build files untouched. 1. N64 build files untouched.
2. Game logic unmodified except narrow, documented `#ifdef PORT` 2. Game logic unmodified except narrow, documented `#ifdef PORT`
ABI/layout exceptions (D86/D87 this session, both logged in ABI/layout exceptions (D86/D87 this session, both logged in
`PCPortResearch.md` §F). D88 is explicitly **not** patched with a `findings.md` §F). D88 is explicitly **not** patched with a
quick inline hack — it needs the same disciplined quick inline hack — it needs the same disciplined
spec-then-convert/fixup treatment as D69, logged as an open finding spec-then-convert/fixup treatment as D69, logged as an open finding
instead per AGENTS.md's "stop and write it up" guidance for anything instead per AGENTS.md's "stop and write it up" guidance for anything
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,7 +1,7 @@
# Texture Glitch Analysis — PC Port # Texture Glitch Analysis — PC Port
**Original diagnosis:** 2026-08-29 (read-only) · **Status table refreshed:** 2026-08-31 (M-31) **Original diagnosis:** 2026-08-29 (read-only) · **Status table refreshed:** 2026-08-31 (M-31)
Companion artifacts: `CAPTURES_DIR` (outside repo). Companion artifacts: `<local-path>` (outside repo).
--- ---
@@ -185,7 +185,7 @@ Useful ground truth for whoever implements the fixes:
the first N texLoads (~10 lines in fast3d) → one Depot run gives true ground truth and a the first N texLoads (~10 lines in fast3d) → one Depot run gives true ground truth and a
before/after reference (also settles H-C if it ever resurfaces). before/after reference (also settles H-C if it ever resurfaces).
Each item gets its own Dxx entry in `docs/PCPortResearch.md` §F/§H at commit time Each item gets its own Dxx entry in `docs/internals.md` §F/§H at commit time
(proposed: next free labels after D126 — check the §F index first; a stray "D160" appears (proposed: next free labels after D126 — check the §F index first; a stray "D160" appears
once in the doc and may need reconciling). once in the doc and may need reconciling).
@@ -275,7 +275,7 @@ and eyeball the corrugated roof / repeating wall panels vs default; flip the
default in `port/src/video.c` (`cfgWrapFix = 1`) if clean. The overflow-band clamp default in `port/src/video.c` (`cfgWrapFix = 1`) if clean. The overflow-band clamp
is an approximation of console TMEM-smear behaviour, not an exact emulation. is an approximation of console TMEM-smear behaviour, not an exact emulation.
## 7. Artifacts (in `CAPTURES_DIR`, outside repo) ## 7. Artifacts (in `<local-path>`, outside repo)
| File | What it is | | File | What it is |
|------|-----------| |------|-----------|
+246
View File
@@ -0,0 +1,246 @@
# Agentic development: goal, method, timeline, assessment
## Why this project exists
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
an LLM:
- ~230 translation units of decompiled Nintendo 64 game C, compiled
**unmodified**;
- big-endian, 32-bit, MIPS ABI assumptions throughout, run on a little-endian
64-bit host;
- bugs that surface as a fault or a garbled frame hundreds of milliseconds
after the actual cause, often in a different subsystem;
- a graphics coprocessor (the RSP) that has to be emulated in software before
anything draws at all.
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 forth** through shared written artifacts, directed by one human.
## The setup
| | |
|---|---|
| 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 (a newer local agentic-coding harness) 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 |
| 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 |
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
"escalate when stuck" move applied at every level.
## Timeline
All dates from this repository's own commit history (August 2026).
```mermaid
gantt
dateFormat YYYY-MM-DD
axisFormat %b %d
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
Boot to window (ROM map + SDL2) :m3, 2026-08-20, 1d
OS shims, threads, fast3d integration :2026-08-21, 2d
First rendered frames :m4, 2026-08-22, 1d
Offline asset-conversion pipeline :2026-08-22, 2d
Entire intro renders (logos->cast) :m5, 2026-08-24, 1d
section Both agents - handoff workflow
Claude joins :milestone, 2026-08-27, 0d
Stage load; Bunker 1 renders + firefight :2026-08-27, 2d
21 solo levels: load + render + no-crash :m6, 2026-08-29, 1d
SDL input layer (kbd/mouse/gamepad) :2026-08-29, 1d
Front-end flow menu->briefing->start :m7, 2026-08-30, 1d
File-backed EEPROM saves :2026-08-31, 1d
```
- **Day 0** (16 Aug): repository forked, PC-port scaffolding added.
- **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,
cast roll.
- **Day 11** (27 Aug): second agent joins.
- **Day 13** (29 Aug): **all 21 solo missions load, render, and survive an
unattended play window without crashing.**
- **Day 14** (30–31 Aug): front end playable end to end (menu -> mission
select -> difficulty -> briefing -> start); file-backed saves.
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
issues still outstanding.
We are not aware of a prior Nintendo 64 native PC port developed primarily
through coding agents, or of one reaching this state on this timeline. That is
a statement about how little precedent there is, not a claim of a record.
## By the numbers
| | |
|---|---|
| Calendar time | 16 days (16 Aug – 1 Sep 2026), one person part-time |
| Commits on the port | 217 |
| Root-caused bugs logged | 160 (`D1`–`D167` in [`findings.md`](findings.md)) |
| Handoff sessions | ~30 (`M-2` … `M-32`) |
| Game-source files given `#ifdef PORT` ABI edits | 63 files, 241 blocks |
| New port-layer / tooling files | 128 |
| Port layer | ~17,000 lines C/C++ (`port/`) |
| PC asset-conversion tooling | ~6,300 lines Python (`tools_pc/`) |
| Outcome | intro + all 21 solo missions render, front end playable; audio not yet done |
### Commit velocity
```mermaid
xychart-beta
title "Commits per day"
x-axis ["8/16", "8/17", "8/20", "8/21", "8/22", "8/23", "8/24", "8/27", "8/28", "8/29", "8/30", "8/31", "9/1"]
y-axis "commits" 0 --> 60
bar [1, 2, 2, 6, 9, 2, 4, 7, 57, 39, 39, 46, 3]
```
The 8/25–8/26 gap is between phases; the jump on 8/28 is the second agent
joining, plus the parallel multi-agent "bursts" used near the end.
### Who did what
```mermaid
pie showData title "Commits by phase"
"Phase A - local model, solo (16-24 Aug)" : 26
"Phase B - collaborative (27 Aug-1 Sep)" : 191
```
| Milestone / workstream | Primary agent | Weight |
|---|---|---|
| PC build system, region macros, full compile + link of ~230 units | local model | large |
| Boot chain: ROM map, OS-shim layer (`libultra.c`), host threads, dual-mapped DRAM | local model | large |
| Software-RSP integration + replacement scheduler (`gesched.c`) | local model | medium |
| 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`–`D167`) | 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 | — |
**Estimated effort split.** By raw commit count it is ~28% local model /
~72% Claude (Phase A entirely local, ~15–20% of Phase B local). Weighting by
milestone difficulty rather than commit count — the foundation the local model
built is a heavier third of the project than its commit count suggests — the
developer's estimate is roughly:
```mermaid
pie showData title "Estimated agent effort (milestone-weighted)"
"Claude" : 60
"Local model (Qwen 3.8 / pi)" : 40
```
The local model's ~40% is front-loaded and foundational (the build, the boot
chain, the RSP wiring, the converter architecture) plus continuous parallel
support; Claude's ~60% is the higher-volume Phase-B debugging, the two big
discrete features, and the hardest structural bugs. A 27B open-weight model on
one consumer GPU carrying the entire foundation of a project like this is the
result worth taking away.
## The handoff workflow
This is the part worth paying attention to.
```mermaid
flowchart LR
H[Human: direction, integration,<br/>build + playtest verification]
C[Claude / Claude Code<br/>frontier, hosted]
Q["Qwen 3.8 via pi<br/>open-weight, local RTX 5090"]
D[(Shared artifacts:<br/>HANDOFF.md · findings.md · porting-notes.md)]
H -->|scopes task, budget, files| C
H -->|scopes task, budget, files| Q
C <-->|reads / appends| D
Q <-->|reads / appends| D
C -.->|usage limit reached ->| Q
Q -.->|hard structural bug ->| C
C -->|patch + writeup| H
Q -->|patch + writeup| H
```
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
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. 160 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
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
files so patches never collided;
- **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
frame, and judging it against N64 reference footage was never delegated.
## Assessment
Honest notes, for anyone weighing whether this transfers.
**Worked well**
- 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.
- **The written-artifact discipline made agent output compound.** An agent
starting cold late in the project was more effective than one early on,
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.
- 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,
and it was the bottleneck.
- **Non-deterministic bugs.** Frame-timing stalls and concurrent-build
flakiness repeatedly fooled agents into "fixing" regressions that were not
real. A number of findings are corrections of earlier findings.
- **Long structural bugs** (matrix handedness, a whole serialized-format
spec) often ended in a "here is what I know, confidence medium" writeup
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
to Opus 5. Each tier earned its place on the problems the tier below it
couldn't close.
**Still outstanding** (parked below crash/level work): audio is not
implemented; several front-end 3D transforms and some text rendering are
wrong. See [`GRAPHICS-BACKLOG.md`](GRAPHICS-BACKLOG.md).
+8 -430
View File
@@ -1,432 +1,10 @@
# GoldenEye 007 — PC Port: Preliminary Research # PC port — finding log
This document captures the preliminary research done to plan a PC port of The raw, chronological engineering record for the port: every runtime bug,
GoldenEye 007, based on the decompilation in this repository its root cause, and the fix. Kept unedited for transparency. Recurring
(`n64decomp/007`). It is modelled on the approach used by the patterns are distilled in [../porting-notes.md](../porting-notes.md).
[Perfect Dark PC port](https://github.com/perfect-dark-pc-port/perfect_dark),
which ports the same Rare "Indy" engine (a later revision) to modern
platforms.
--- ---
## 1. Executive summary
* **Feasibility: HIGH.** GoldenEye and Perfect Dark share the same Rare
engine family. The PD port proves the whole approach works end-to-end
(singleplayer + split-screen multiplayer, mouse-look, widescreen, 60 FPS,
mods). GE is, if anything, a *simpler* graphics pipeline than PD in some
respects, though it has a few custom RSP extensions (see §5).
* **Recommended strategy: fork this decomp and add a `port/` layer**, exactly
mirroring the PD port's layout. The N64 build (Makefile + IDO) stays
untouched; the PC build is a parallel CMake build that compiles the same
game C sources against a set of PC shims.
* **The single hardest component is the RSP (Reality Signal Processor)
emulator.** The PD port ships a complete, working "fast3d" software RSP
(`port/fast3d/`, ~7k lines of C++) that interprets GBI display lists and
emits OpenGL calls. It **already handles `G_TRI4`**, which is the main
custom triangle command GE uses. GE's other custom command, `G_SETTEX`,
appears **unused** (its emitter `gsSPUseTexture` is never called), so the
remaining GE-specific work is mostly **verifying the custom
render/color-combiner modes** against the RSP ucode.
* **Audio is straightforward.** GE uses the standard libaudio (AL) engine on
the R4300, the same as PD. The PD port's SDL audio + mixer approach carries
over directly.
* **Saves** are **EEPROM-based** (controller EEPROM via `osEeprom*`), not
Memory Pak. The PFS/motor code 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
Pak emulation.
---
## 2. What the PD port actually does (the reference architecture)
The PD port is a **fork of the PD decomp** with one added top-level
directory, `port/`. The original game code (`src/game/*.c`, `src/lib/*.c`)
is compiled **unmodified** for the PC. All N64-hardware dependencies are
satisfied by the `port/` layer:
```
port/
├── fast3d/ # Software RSP: interprets GBI display lists -> OpenGL
│ ├── gfx_pc.cpp # the RSP interpreter (matrices, lights, tris, fog)
│ ├── gfx_opengl.cpp # OpenGL rendering backend (shaders, textures, zbuf)
│ ├── gfx_sdl2.cpp # SDL2 window-manager backend
│ ├── gfx_cc.cpp # color-combiner -> GLSL shader compiler
│ └── ...
├── include/ # port-facing headers
│ ├── audio.h config.h fs.h input.h mixer.h mod.h
│ ├── romdata.h system.h video.h utils.h platform.h
└── src/
├── main.c # PC entry point (replaces boot.s / _start.s)
├── libultra.c # shims for the libultra OS API (threads, time, VI, PI, SI, SP, AI)
├── video.c # SDL2 window + GL context + frame pacing
├── audio.c # SDL audio device, buffer queueing
├── mixer.c # audio mixing / RSP-audio-buffer processing
├── mod.c # MOD music playback
├── input.c # SDL2 keyboard/mouse/gamepad -> N64 controller structs
├── fs.c # filesystem abstraction over the ROM + data dir
├── romdata.c # loads the .z64 ROM from disk, sets up segments
├── config.c # INI config (pd.ini)
├── system.c # platform primitives (time, logging, paths)
├── crash.c # crash handler / stack traces
├── pdsched.c # replacement RCP scheduler that drives the software RSP
└── utils.c
```
### 2.1 The key architectural insight: emulate the RSP, bypass the RDP
On real hardware the render path is:
```
R4300 builds a Gfx display list
|
v
RSP (coproc 0) runs "fast3d" ucode, turns GBI cmds into RDP cmds
|
v
RDP (coproc 1) rasterizes into the framebuffer
```
The PD port **emulates the RSP in software** (`fast3d/gfx_pc.cpp`) and
**bypasses the RDP entirely**: the software RSP translates GBI commands
directly into OpenGL calls. The R4300 game code is run natively and is
unaware of the difference — it just builds display lists and "starts the
RSP" as usual.
The frame flow (from `port/src/pdsched.c` + `port/src/video.c`):
```
videoStartFrame() -> gfx_start_frame()
game builds display list (Gfx*)
videoSubmitCommands(Gfx* cmds) -> gfx_run(cmds) # <- the software RSP runs here
videoEndFrame() -> gfx_end_frame() # swap buffers, FPS accounting
```
### 2.2 How the RSP is invoked
The game's RCP scheduler (`src/lib/sched.c` in PD, `src/sched.c` in GE) calls
`osSpTaskLoad()` + `osSpTaskStartGo()` to hand a task (gfx or audio) to the
RSP. The port:
* **Excludes** the original `libultra/io/sptask.c` (and the other `io/*.c`
hardware drivers) from the PC build.
* Provides its own scheduler (`pdsched.c`) that, instead of writing to RSP
registers, calls `gfx_run()` on the display list for gfx tasks and feeds the
audio buffer for audio tasks.
* Shims the rest of the libultra OS API in `libultra.c` (threads become
no-ops / single-threaded, `osGetTime` maps to a host clock, VI/PI/SI/AI
functions map to the port's video/audio/fs/input layers).
### 2.3 Build system
The PC build is **CMake** (the N64 build stays Make + IDO). Dependencies:
**SDL2, zlib, OpenGL** (plus `stdc++`, `m`, `dl`). It compiles:
* `src/game/*.c` (all of it)
* a curated list of `src/lib/*.c` (the engine helpers that are
platform-independent: mema, memp, model, music, snd, vi, rdp, etc.)
* `src/lib/ultra/audio/*.c` (libaudio — runs fine on the CPU)
* `src/lib/ultra/gu/*.c` (gu matrix helpers)
* `port/src/*.c` + `port/src/*.cpp` + `port/fast3d/*.cpp`
It deliberately **excludes** the N64 I/O drivers (`src/lib/ultra/io/*.c`),
the boot/TLB assembly, and the RSP microcode (`.s` under `rsp/`).
### 2.4 PD-port copy-candidate audit (session 2026-08-22)
The PD port (`PD_PORT_CHECKOUT`) is a **standing reference**
for this project (see AGENTS.md): same Rare "Indy"/Bond engine family, and its
port layer solves the same problem classes we are currently hitting. Audited
against our remaining work items:
**Direct copy candidates (Phase 3/4 — our stubs were scaffolded to be replaced
by these):**
| PD file | Lines | Replaces our | Notes |
|---|---|---|---|
| `port/src/mixer.c` | 722 | 31-line stub | libaudio→SDL final mixer; same RareAL family. Adapt to GE's `audi.c` wiring (sample rate/buffering) |
| `port/src/input.c` | 1551 | 48-line stub | SDL_GameController backend: hotplug, rumble, keyboard fallback. Remap the button table to GE's scheme (C-buttons/Z-trig differ from PD) |
| `port/src/fs.c` | 294 | 81 lines | File-backed save dir (`--savedir`, `$S` path expansion) — foundation for Phase 4 EEPROM/PFS file backing (ours are no-op stubs today) |
| `port/src/audio.c` | 75 | 70 lines | Near-identical; diff when Phase 3 starts |
**Reference implementations (Phase 2 — the D32/D37/D43 class).** PD's port
layer has a `port/src/preprocess/` module (~4,125 lines): a per-ROM-segment
`preprocessfunc` table in their `romdata.c` that converts N64-layout asset data
to PC-native layout *at load time* — a systematic solution to the problem class
we have been fixing ad-hoc per asset type.
- `filemodel.c` (1,114) — the direct **D43** analogue: 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
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
`romdataFixupAudioBank`.
- `filelang.c`, `filetiles.c`, `filepads.c`, `filesetup.c`, `gbi.c`,
`segfonts.c` — the remaining asset types we will hit.
**Not copyable:** GE-specific `ModelRoData` record map; fast3d CC/RM
correctness (GE's custom GBI modes vs `gmain.s` do not exist in PD); PD extras
(`config.c`, `optionsmenu.c`, `mpsetups.c`, `mod.c`) — options menu, MP setup,
MOD player; not part of 1:1 fidelity.
**Caveats:** copy **port-layer files only** — their decomp was modified for PC
(e.g. `types.h` keeps N64 offset comments while using real pointers); our
non-negotiables are unchanged. Same family ≠ identical format: validate every
copied conversion per-field against GE headers + ROM ground truth (the standing
D32 procedure).
---
## 3. GoldenEye decomp: what we're porting
* **Repo:** `n64decomp/007` (this repo). WIP decomp that byte-matches the
US/EU/JP ROMs. Game code lives in `src/game/` (~242 files) plus engine
files in `src/`.
* **Engine layout** (same family as PD):
* `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`,
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*`
-> `osEeprom*`), plus PFS/motor code for accessory (Rumble Pak / Memory
Pak) detection only.
* `src/init.c` — `init()` -> `mainproc()` -> `bossEntry()` (game entry).
**Compiled for the PC** (provides `mainproc()`); its N64-only `init()` is
stubbed (see A3).
* **Custom RSP microcode:** `rsp/graphics/gmain.s` (1545 lines) — a modified
fast3d. This is the RSP-side code that consumes the display list. For the PC
port we do **not** run this; we replace it with the software RSP. But it is
the authoritative reference for which GBI commands GE actually emits.
### 3.1 Entry / main loop
```
boot.s -> init() [src/init.c]
-> decompress data segment
-> osInitialize(), TLB setup
-> osCreateThread(mainThread, mainproc)
mainproc()
-> idleCreateThread(), piCreateManager(), rmonCreateThread()
-> schedulerInitThread() # osCreateScheduler + osScAddClient(gfxClient)
-> bossEntry() # [src/boss.c] real game start
```
The port's `main.c` replaces `boot.s`/`init()` and drives `mainproc()` (or
`bossEntry()`) directly after setting up video/audio/input/ROM.
### 3.2 The two libultra trees: `src/libultra/` and `src/libultrare/`
GE ships **two** libultra trees. `src/libultra/` is the standard Nintendo
libultra; `src/libultrare/` holds Rare's modified/replacement files. The N64
build selects which files to compile via `src/libultrare/Makefile.libultrare`,
which explicitly lists each file as *original* (from `libultra/`) or *Rare*
(from `libultrare/`). The Rare files **override** the originals where they
overlap.
What the PC build must do with each:
* **`src/libultra/audio/*.c`** — standard libaudio (AL). Runs on the CPU;
**compile** (the game's `audi.c`/`snd.c`/`music.c` depend on it).
* **`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`, ...).
**Compile**.
* **`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
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,
message queues, timers, cache, TLB). **Exclude and shim** in
`port/src/libultra.c` (+ `n64stubs.c` for the boot/TLB/FPU symbols).
> The exact original-vs-Rare file lists are in
> `src/libultrare/Makefile.libultrare` (`LIBULTRA_*_C_FILES` vs
> `LIBULTRARE_*_C_FILES`). Use it as the ground truth when curating the PC
> build's source list.
---
## 4. N64 hardware surfaces that must be shimmed
| Subsystem | GE API used | Port strategy (from PD) |
|-----------|-------------|--------------------------|
| RSP (coproc 0) | `osSpTaskLoad/StartGo/Yield`, display lists | Software RSP (`fast3d`) — **main work** |
| RDP (coproc 1) | `osDpSetStatus`, `osDpSetNextBuffer`, render modes | Bypassed; OpenGL backend |
| VI (video) | `osViSetMode`, `osViVSyncCallback`, `osViWaitVSync` | SDL2 window + GL context + frame pacing |
| AI (audio) | `osAiSetFrequency/SetNextBuffer/GetLength` | SDL audio device + queue |
| PI (peripheral) | `osPiRawStartDma`, `osPiCreateManager`, cart reads | Read from ROM file on disk |
| SI (controller) | `osContInit/StartReadData`, `osEeprom*`, `osPfs*`/`osMotor*` | SDL input + file-backed EEPROM; PFS/motor stubbed (accessory detection only) |
| OS core | threads, msg queues, timers, `osGetTime`, cache | Single-threaded shims + host clock |
| R4300 specifics | TLB, cache, FPU csr, K0/K1 segments | No-ops / identity (native x86-64) |
---
## 5. GE-specific graphics differences vs PD (the RSP work)
GE's `include/gbi_extension.h` defines the custom GBI surface. Compared to the
PD port's `fast3d`, the deltas are:
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
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
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`,
`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
`Z_UPD`).
* **Status: handled** by the render-mode -> GL state mapping; verify.
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
unused (see item 2), so the concrete new RSP work is **verification of the
custom CC/RM modes** against `gmain.s`, with `G_SETTEX` as a low-priority
safety net. Everything else (matrices, lights, fog, tri1/tri4, texture load,
sync/flush) is shared.
> Note: GE's on-hardware RSP ucode is `rsp/graphics/gmain.s`. We do not run it
> on PC, but it is the ground truth for the exact command encodings GE emits —
> use it to validate the software RSP's command decoding.
---
## 6. Audio
* GE uses **libaudio (AL)** on the R4300 (`src/audi.c`, `src/snd.c`,
`src/music.c`) — identical architecture to PD. Sound synthesis (ADPCM
decode, mixing, FX) runs on the CPU; the RSP audio ucode is a thin
pass-through to the AI DMA.
* The PD port runs libaudio natively and routes the mixed output through
`audio.c` (SDL device) + `mixer.c`. **Carries over directly.**
* Music: GE uses the same MOD-style music system; PD's `mod.c` is the model.
* Output rate: PD uses 22020 Hz stereo s16. GE's `OUTPUT_RATE` in `audi.c`
should be matched.
---
## 7. Input & saves
* **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
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
map to `osEepromProbe/LongRead/LongWrite` (controller EEPROM). The PFS/motor
code in `joy.c` (`osPfsInit`, `osMotorInit/Start/Stop`) is only for
accessory (Rumble Pak / Memory Pak) detection. The port therefore:
* backs the EEPROM with a file in the data dir (e.g. `eeprom.bin`), and
* stubs PFS/motor to report "no accessory" so the game proceeds without
rumble/mempak.
This is simpler than a full Memory Pak/PFS emulation.
---
## 8. Recommended plan
### Phase 0 — Scaffolding (this change)
* Add `port/` directory structure + headers + stub sources.
* Add a top-level `CMakeLists.txt` (+ `cmake/` modules) for the PC build,
parallel to the N64 `Makefile`.
* This document + `port/README.md`.
### Phase 1 — Boot to a window
* Port `system.c`, `fs.c`, `romdata.c`, `config.c`, `main.c`.
* Port `video.c` + a 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)
* Get mainThread through all of `bossInitMainthreadData()` → `bossEntry()` →
`bossMainloop()` and render frame 1. (Done — D34–D58; see §G.)
* Recurring mechanism: **ROM-data struct ABI/layout fixes** (the D32 pattern) —
a 32-bit-pointer struct read as a 64-bit-pointer struct misaligns; convert
embedded pointer fields to `u32` + cast at use sites (PD ground truth), then
verify the load-time rebase yields valid DRAM addresses. See §H for the
step-by-step procedure and the standing "non-negotiable #2 ABI exception."
* Goal: title screen / first level actually drawn on the GL surface.
### Phase 2 — RSP / rendering
* Bring in the PD `fast3d` (gfx_pc / gfx_opengl / gfx_cc / gfx_sdl2).
* Verify the custom CC/RM modes against `gmain.s`; add a `G_SETTEX` decode
path only if it turns out to be used (see §5 — it appears unused).
* Port `pdsched.c` (GE variant) to drive the software RSP.
* Goal: render the title screen / first level.
### Phase 3 — Audio + input
* Port `audio.c`, `mixer.c`, `mod.c`; wire libaudio output to SDL.
* **Decide the ASP strategy** (see §11 C2): if audio runs the `aspMain`
microcode, wire `aspMainTextStart`/`aspMainDataStart` to the real microcode
bytes in the ROM; if audio is CPU-only, the `port/src/ucode.c` dummies stand.
* Port `input.c` (keyboard/mouse/gamepad).
* Goal: playable with sound.
### Phase 4 — Saves + polish
* File-backed EEPROM (saves); PFS/motor already stubbed as no-ops.
* Widescreen, mouse-look, FPS options, config file, crash handler.
* Goal: feature-parity with the original + QoL extras.
---
## 9. Open questions / risks
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
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
(`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
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
unaligned accesses; the PD port handles this with careful casts and
`-fno-strict-aliasing`. Reuse those flags.
---
## 10. References
* PD port: https://github.com/perfect-dark-pc-port/perfect_dark
* PD decomp: https://github.com/n64decomp/perfect_dark
* GE decomp (this repo): https://github.com/n64decomp/007
* GE docs: https://github.com/kholdfuzion/goldeneye_docs
* N64 devkit / libultra: `include/PR/*.h` in this repo
* GE custom GBI: `include/gbi_extension.h`
* GE RSP ucode: `rsp/graphics/gmain.s`
---
## 11. Scaffolding review: findings & resolutions ## 11. Scaffolding review: findings & resolutions
The initial scaffolding was reviewed against the sources. Each finding below The initial scaffolding was reviewed against the sources. Each finding below
@@ -3736,7 +3314,7 @@ the inline-Model / bondhead-matrix region that could itself have caused
re-apply snippet in the M-6 agent report. re-apply snippet in the M-6 agent report.
**D115 (session M-6) — `struct player` / `struct hand` raw-offset audit + **D115 (session M-6) — `struct player` / `struct hand` raw-offset audit +
first fix.** Full survey in `docs/AUDIT-M6-player-offsets.md` (10 offset first fix.** Full survey in `docs/dev/AUDIT-M6-player-offsets.md` (10 offset
sites; 3 live HIGH, 1 dead const block, 2 MED, 4 already-correct). sites; 3 live HIGH, 1 dead const block, 2 MED, 4 already-correct).
**Fixed:** `gunfire.c:4960-4962` `THROWMTX` / `THROWPOS(k)` / `THROWPREV(k)` **Fixed:** `gunfire.c:4960-4962` `THROWMTX` / `THROWPOS(k)` / `THROWPREV(k)`
were raw byte offsets — `(u8*)g_CurrentPlayer + handoffset + 0xAD8/0xB08/ were raw byte offsets — `(u8*)g_CurrentPlayer + handoffset + 0xAD8/0xB08/
@@ -4404,7 +3982,7 @@ happen to convert cleanly; Facility's do not.
*Probes:* none left in tree — `GE_C2` scratch prints removed after *Probes:* none left in tree — `GE_C2` scratch prints removed after
root-cause. *Files touched:* `port/src/gimgfixup.c` (fix), root-cause. *Files touched:* `port/src/gimgfixup.c` (fix),
`docs/PORT-LEARNINGS.md`, `docs/PCPortResearch.md`, `docs/LEVEL-STATUS.md`. `docs/porting-notes.md`, `docs/internals.md`, `docs/LEVEL-STATUS.md`.
*Confidence:* Jungle fix **high** (mechanism proven with a `[C2sync]` *Confidence:* Jungle fix **high** (mechanism proven with a `[C2sync]`
trace showing 32 slots going `abcd08xx -> 700e****`, verified render). trace showing 32 slots going `abcd08xx -> 700e****`, verified render).
Facility diagnosis **medium** (probe evidence strong; exact off-by in Facility diagnosis **medium** (probe evidence strong; exact off-by in
@@ -4504,7 +4082,7 @@ tree as the offline-side regression guard.
*Probes:* **none left in tree** — the two `#if defined(PORT)`/`GE_C3` *Probes:* **none left in tree** — the two `#if defined(PORT)`/`GE_C3`
scratch prints in `src/game/prop.c` (lines ~1206 and ~1865) and the scratch prints in `src/game/prop.c` (lines ~1206 and ~1865) and the
temp scan scripts `tools_pc/c3_scan.py` / `tools_pc/c3_doors.py` were all temp scan scripts `tools_pc/c3_scan.py` / `tools_pc/c3_doors.py` were all
reverted/deleted. *Files touched:* only `docs/PCPortResearch.md` (this reverted/deleted. *Files touched:* only `docs/internals.md` (this
entry) + `docs/LEVEL-STATUS.md` (one line). No code, no converter, no entry) + `docs/LEVEL-STATUS.md` (one line). No code, no converter, no
sidecar change — **sidecars do NOT need regen**, tree builds clean at sidecar change — **sidecars do NOT need regen**, tree builds clean at
`f2beae4b` + M-13 uncommitted set. `f2beae4b` + M-13 uncommitted set.
@@ -4617,7 +4195,7 @@ handler emitting the leading s32 words then an 8-byte zero `next`;
**Bunker2 `-level_27`, Depot `-level_30`, Surface2 `-level_43` now PASS** **Bunker2 `-level_27`, Depot `-level_30`, Surface2 `-level_43` now PASS**
(91.5% / 79.8% / 70.4%); Bunker1/Silo/Aztec/Archives/Egypt/Train (91.5% / 79.8% / 70.4%); Bunker1/Silo/Aztec/Archives/Egypt/Train
unregressed. Sweep 13 → 16/21. Generalisable quirk appended to unregressed. Sweep 13 → 16/21. Generalisable quirk appended to
`PORT-LEARNINGS.md` §A. Remaining crashes: C2 Runway/Facility (model-GDL `porting-notes.md` §A. Remaining crashes: C2 Runway/Facility (model-GDL
align), C2m Jungle (`G_MTX`), C5 Control (BG portal), C7 Surface1 align), C2m Jungle (`G_MTX`), C5 Control (BG portal), C7 Surface1
(`sndSetupSound`). (`sndSetupSound`).
Binary file not shown.

After

Width:  |  Height:  |  Size: 453 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

+436
View File
@@ -0,0 +1,436 @@
# GoldenEye 007 — PC Port: architecture &amp; plan
> This began as the pre-implementation research note and is kept as the
> architecture reference. Sections 1–9 describe the design; section 10 lists
> external references. The phased plan in section 8 is largely done through
> Phase 2 — for current status see the [README](../README.md) and
> [`dev/LEVEL-STATUS.md`](dev/LEVEL-STATUS.md); for the blow-by-blow finding
> log see [`dev/findings.md`](dev/findings.md).
This document captures the research done to plan a PC port of
GoldenEye 007, based on the decompilation this repository is forked from
([`n64decomp/007`](https://github.com/n64decomp/007)). It is modelled on the
approach used by the
[Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark),
which ports the same Rare "Indy" engine (a later revision) to modern
platforms.
---
## 1. Executive summary
* **Feasibility: HIGH.** GoldenEye and Perfect Dark share the same Rare
engine family. The PD port proves the whole approach works end-to-end
(singleplayer + split-screen multiplayer, mouse-look, widescreen, 60 FPS,
mods). GE is, if anything, a *simpler* graphics pipeline than PD in some
respects, though it has a few custom RSP extensions (see §5).
* **Recommended strategy: fork this decomp and add a `port/` layer**, exactly
mirroring the PD port's layout. The N64 build (Makefile + IDO) stays
untouched; the PC build is a parallel CMake build that compiles the same
game C sources against a set of PC shims.
* **The single hardest component is the RSP (Reality Signal Processor)
emulator.** The PD port ships a complete, working "fast3d" software RSP
(`port/fast3d/`, ~7k lines of C++) that interprets GBI display lists and
emits OpenGL calls. It **already handles `G_TRI4`**, which is the main
custom triangle command GE uses. GE's other custom command, `G_SETTEX`,
appears **unused** (its emitter `gsSPUseTexture` is never called), so the
remaining GE-specific work is mostly **verifying the custom
render/color-combiner modes** against the RSP ucode.
* **Audio is straightforward.** GE uses the standard libaudio (AL) engine on
the R4300, the same as PD. The PD port's SDL audio + mixer approach carries
over directly.
* **Saves** are **EEPROM-based** (controller EEPROM via `osEeprom*`), not
Memory Pak. The PFS/motor code 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
Pak emulation.
---
## 2. What the PD port actually does (the reference architecture)
The PD port is a **fork of the PD decomp** with one added top-level
directory, `port/`. The original game code (`src/game/*.c`, `src/lib/*.c`)
is compiled **unmodified** for the PC. All N64-hardware dependencies are
satisfied by the `port/` layer:
```
port/
├── fast3d/ # Software RSP: interprets GBI display lists -> OpenGL
│ ├── gfx_pc.cpp # the RSP interpreter (matrices, lights, tris, fog)
│ ├── gfx_opengl.cpp # OpenGL rendering backend (shaders, textures, zbuf)
│ ├── gfx_sdl2.cpp # SDL2 window-manager backend
│ ├── gfx_cc.cpp # color-combiner -> GLSL shader compiler
│ └── ...
├── include/ # port-facing headers
│ ├── audio.h config.h fs.h input.h mixer.h mod.h
│ ├── romdata.h system.h video.h utils.h platform.h
└── src/
├── main.c # PC entry point (replaces boot.s / _start.s)
├── libultra.c # shims for the libultra OS API (threads, time, VI, PI, SI, SP, AI)
├── video.c # SDL2 window + GL context + frame pacing
├── audio.c # SDL audio device, buffer queueing
├── mixer.c # audio mixing / RSP-audio-buffer processing
├── mod.c # MOD music playback
├── input.c # SDL2 keyboard/mouse/gamepad -> N64 controller structs
├── fs.c # filesystem abstraction over the ROM + data dir
├── romdata.c # loads the .z64 ROM from disk, sets up segments
├── config.c # INI config (pd.ini)
├── system.c # platform primitives (time, logging, paths)
├── crash.c # crash handler / stack traces
├── pdsched.c # replacement RCP scheduler that drives the software RSP
└── utils.c
```
### 2.1 The key architectural insight: emulate the RSP, bypass the RDP
On real hardware the render path is:
```
R4300 builds a Gfx display list
|
v
RSP (coproc 0) runs "fast3d" ucode, turns GBI cmds into RDP cmds
|
v
RDP (coproc 1) rasterizes into the framebuffer
```
The PD port **emulates the RSP in software** (`fast3d/gfx_pc.cpp`) and
**bypasses the RDP entirely**: the software RSP translates GBI commands
directly into OpenGL calls. The R4300 game code is run natively and is
unaware of the difference — it just builds display lists and "starts the
RSP" as usual.
The frame flow (from `port/src/pdsched.c` + `port/src/video.c`):
```
videoStartFrame() -> gfx_start_frame()
game builds display list (Gfx*)
videoSubmitCommands(Gfx* cmds) -> gfx_run(cmds) # <- the software RSP runs here
videoEndFrame() -> gfx_end_frame() # swap buffers, FPS accounting
```
### 2.2 How the RSP is invoked
The game's RCP scheduler (`src/lib/sched.c` in PD, `src/sched.c` in GE) calls
`osSpTaskLoad()` + `osSpTaskStartGo()` to hand a task (gfx or audio) to the
RSP. The port:
* **Excludes** the original `libultra/io/sptask.c` (and the other `io/*.c`
hardware drivers) from the PC build.
* Provides its own scheduler (`pdsched.c`) that, instead of writing to RSP
registers, calls `gfx_run()` on the display list for gfx tasks and feeds the
audio buffer for audio tasks.
* Shims the rest of the libultra OS API in `libultra.c` (threads become
no-ops / single-threaded, `osGetTime` maps to a host clock, VI/PI/SI/AI
functions map to the port's video/audio/fs/input layers).
### 2.3 Build system
The PC build is **CMake** (the N64 build stays Make + IDO). Dependencies:
**SDL2, zlib, OpenGL** (plus `stdc++`, `m`, `dl`). It compiles:
* `src/game/*.c` (all of it)
* a curated list of `src/lib/*.c` (the engine helpers that are
platform-independent: mema, memp, model, music, snd, vi, rdp, etc.)
* `src/lib/ultra/audio/*.c` (libaudio — runs fine on the CPU)
* `src/lib/ultra/gu/*.c` (gu matrix helpers)
* `port/src/*.c` + `port/src/*.cpp` + `port/fast3d/*.cpp`
It deliberately **excludes** the N64 I/O drivers (`src/lib/ultra/io/*.c`),
the boot/TLB assembly, and the RSP microcode (`.s` under `rsp/`).
### 2.4 PD-port copy-candidate audit (session 2026-08-22)
The [Perfect Dark port](https://github.com/fgsfdsfgs/perfect_dark) is a **standing reference**
for this project (see CONTRIBUTING.md): same Rare "Indy"/Bond engine family, and its
port layer solves the same problem classes we are currently hitting. Audited
against our remaining work items:
**Direct copy candidates (Phase 3/4 — our stubs were scaffolded to be replaced
by these):**
| PD file | Lines | Replaces our | Notes |
|---|---|---|---|
| `port/src/mixer.c` | 722 | 31-line stub | libaudio→SDL final mixer; same RareAL family. Adapt to GE's `audi.c` wiring (sample rate/buffering) |
| `port/src/input.c` | 1551 | 48-line stub | SDL_GameController backend: hotplug, rumble, keyboard fallback. Remap the button table to GE's scheme (C-buttons/Z-trig differ from PD) |
| `port/src/fs.c` | 294 | 81 lines | File-backed save dir (`--savedir`, `$S` path expansion) — foundation for Phase 4 EEPROM/PFS file backing (ours are no-op stubs today) |
| `port/src/audio.c` | 75 | 70 lines | Near-identical; diff when Phase 3 starts |
**Reference implementations (Phase 2 — the D32/D37/D43 class).** PD's port
layer has a `port/src/preprocess/` module (~4,125 lines): a per-ROM-segment
`preprocessfunc` table in their `romdata.c` that converts N64-layout asset data
to PC-native layout *at load time* — a systematic solution to the problem class
we have been fixing ad-hoc per asset type.
- `filemodel.c` (1,114) — the direct **D43** analogue: 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
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
`romdataFixupAudioBank`.
- `filelang.c`, `filetiles.c`, `filepads.c`, `filesetup.c`, `gbi.c`,
`segfonts.c` — the remaining asset types we will hit.
**Not copyable:** GE-specific `ModelRoData` record map; fast3d CC/RM
correctness (GE's custom GBI modes vs `gmain.s` do not exist in PD); PD extras
(`config.c`, `optionsmenu.c`, `mpsetups.c`, `mod.c`) — options menu, MP setup,
MOD player; not part of 1:1 fidelity.
**Caveats:** copy **port-layer files only** — their decomp was modified for PC
(e.g. `types.h` keeps N64 offset comments while using real pointers); our
non-negotiables are unchanged. Same family ≠ identical format: validate every
copied conversion per-field against GE headers + ROM ground truth (the standing
D32 procedure).
---
## 3. GoldenEye decomp: what we're porting
* **Repo:** `n64decomp/007` (this repo). WIP decomp that byte-matches the
US/EU/JP ROMs. Game code lives in `src/game/` (~242 files) plus engine
files in `src/`.
* **Engine layout** (same family as PD):
* `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`,
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*`
-> `osEeprom*`), plus PFS/motor code for accessory (Rumble Pak / Memory
Pak) detection only.
* `src/init.c` — `init()` -> `mainproc()` -> `bossEntry()` (game entry).
**Compiled for the PC** (provides `mainproc()`); its N64-only `init()` is
stubbed (see A3).
* **Custom RSP microcode:** `rsp/graphics/gmain.s` (1545 lines) — a modified
fast3d. This is the RSP-side code that consumes the display list. For the PC
port we do **not** run this; we replace it with the software RSP. But it is
the authoritative reference for which GBI commands GE actually emits.
### 3.1 Entry / main loop
```
boot.s -> init() [src/init.c]
-> decompress data segment
-> osInitialize(), TLB setup
-> osCreateThread(mainThread, mainproc)
mainproc()
-> idleCreateThread(), piCreateManager(), rmonCreateThread()
-> schedulerInitThread() # osCreateScheduler + osScAddClient(gfxClient)
-> bossEntry() # [src/boss.c] real game start
```
The port's `main.c` replaces `boot.s`/`init()` and drives `mainproc()` (or
`bossEntry()`) directly after setting up video/audio/input/ROM.
### 3.2 The two libultra trees: `src/libultra/` and `src/libultrare/`
GE ships **two** libultra trees. `src/libultra/` is the standard Nintendo
libultra; `src/libultrare/` holds Rare's modified/replacement files. The N64
build selects which files to compile via `src/libultrare/Makefile.libultrare`,
which explicitly lists each file as *original* (from `libultra/`) or *Rare*
(from `libultrare/`). The Rare files **override** the originals where they
overlap.
What the PC build must do with each:
* **`src/libultra/audio/*.c`** — standard libaudio (AL). Runs on the CPU;
**compile** (the game's `audi.c`/`snd.c`/`music.c` depend on it).
* **`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`, ...).
**Compile**.
* **`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
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,
message queues, timers, cache, TLB). **Exclude and shim** in
`port/src/libultra.c` (+ `n64stubs.c` for the boot/TLB/FPU symbols).
> The exact original-vs-Rare file lists are in
> `src/libultrare/Makefile.libultrare` (`LIBULTRA_*_C_FILES` vs
> `LIBULTRARE_*_C_FILES`). Use it as the ground truth when curating the PC
> build's source list.
---
## 4. N64 hardware surfaces that must be shimmed
| Subsystem | GE API used | Port strategy (from PD) |
|-----------|-------------|--------------------------|
| RSP (coproc 0) | `osSpTaskLoad/StartGo/Yield`, display lists | Software RSP (`fast3d`) — **main work** |
| RDP (coproc 1) | `osDpSetStatus`, `osDpSetNextBuffer`, render modes | Bypassed; OpenGL backend |
| VI (video) | `osViSetMode`, `osViVSyncCallback`, `osViWaitVSync` | SDL2 window + GL context + frame pacing |
| AI (audio) | `osAiSetFrequency/SetNextBuffer/GetLength` | SDL audio device + queue |
| PI (peripheral) | `osPiRawStartDma`, `osPiCreateManager`, cart reads | Read from ROM file on disk |
| SI (controller) | `osContInit/StartReadData`, `osEeprom*`, `osPfs*`/`osMotor*` | SDL input + file-backed EEPROM; PFS/motor stubbed (accessory detection only) |
| OS core | threads, msg queues, timers, `osGetTime`, cache | Single-threaded shims + host clock |
| R4300 specifics | TLB, cache, FPU csr, K0/K1 segments | No-ops / identity (native x86-64) |
---
## 5. GE-specific graphics differences vs PD (the RSP work)
GE's `include/gbi_extension.h` defines the custom GBI surface. Compared to the
PD port's `fast3d`, the deltas are:
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
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
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`,
`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
`Z_UPD`).
* **Status: handled** by the render-mode -> GL state mapping; verify.
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
unused (see item 2), so the concrete new RSP work is **verification of the
custom CC/RM modes** against `gmain.s`, with `G_SETTEX` as a low-priority
safety net. Everything else (matrices, lights, fog, tri1/tri4, texture load,
sync/flush) is shared.
> Note: GE's on-hardware RSP ucode is `rsp/graphics/gmain.s`. We do not run it
> on PC, but it is the ground truth for the exact command encodings GE emits —
> use it to validate the software RSP's command decoding.
---
## 6. Audio
* GE uses **libaudio (AL)** on the R4300 (`src/audi.c`, `src/snd.c`,
`src/music.c`) — identical architecture to PD. Sound synthesis (ADPCM
decode, mixing, FX) runs on the CPU; the RSP audio ucode is a thin
pass-through to the AI DMA.
* The PD port runs libaudio natively and routes the mixed output through
`audio.c` (SDL device) + `mixer.c`. **Carries over directly.**
* Music: GE uses the same MOD-style music system; PD's `mod.c` is the model.
* Output rate: PD uses 22020 Hz stereo s16. GE's `OUTPUT_RATE` in `audi.c`
should be matched.
---
## 7. Input & saves
* **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
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
map to `osEepromProbe/LongRead/LongWrite` (controller EEPROM). The PFS/motor
code in `joy.c` (`osPfsInit`, `osMotorInit/Start/Stop`) is only for
accessory (Rumble Pak / Memory Pak) detection. The port therefore:
* backs the EEPROM with a file in the data dir (e.g. `eeprom.bin`), and
* stubs PFS/motor to report "no accessory" so the game proceeds without
rumble/mempak.
This is simpler than a full Memory Pak/PFS emulation.
---
## 8. Recommended plan
### Phase 0 — Scaffolding (this change)
* Add `port/` directory structure + headers + stub sources.
* Add a top-level `CMakeLists.txt` (+ `cmake/` modules) for the PC build,
parallel to the N64 `Makefile`.
* This document + `port/README.md`.
### Phase 1 — Boot to a window
* Port `system.c`, `fs.c`, `romdata.c`, `config.c`, `main.c`.
* Port `video.c` + a 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)
* Get mainThread through all of `bossInitMainthreadData()` → `bossEntry()` →
`bossMainloop()` and render frame 1. (Done — D34–D58; see §G.)
* Recurring mechanism: **ROM-data struct ABI/layout fixes** (the D32 pattern) —
a 32-bit-pointer struct read as a 64-bit-pointer struct misaligns; convert
embedded pointer fields to `u32` + cast at use sites (PD ground truth), then
verify the load-time rebase yields valid DRAM addresses. See §H for the
step-by-step procedure and the standing "non-negotiable #2 ABI exception."
* Goal: title screen / first level actually drawn on the GL surface.
### Phase 2 — RSP / rendering
* Bring in the PD `fast3d` (gfx_pc / gfx_opengl / gfx_cc / gfx_sdl2).
* Verify the custom CC/RM modes against `gmain.s`; add a `G_SETTEX` decode
path only if it turns out to be used (see §5 — it appears unused).
* Port `pdsched.c` (GE variant) to drive the software RSP.
* Goal: render the title screen / first level.
### Phase 3 — Audio + input
* Port `audio.c`, `mixer.c`, `mod.c`; wire libaudio output to SDL.
* **Decide the ASP strategy** (see §11 C2): if audio runs the `aspMain`
microcode, wire `aspMainTextStart`/`aspMainDataStart` to the real microcode
bytes in the ROM; if audio is CPU-only, the `port/src/ucode.c` dummies stand.
* Port `input.c` (keyboard/mouse/gamepad).
* Goal: playable with sound.
### Phase 4 — Saves + polish
* File-backed EEPROM (saves); PFS/motor already stubbed as no-ops.
* Widescreen, mouse-look, FPS options, config file, crash handler.
* Goal: feature-parity with the original + QoL extras.
---
## 9. Open questions / risks
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
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
(`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
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
unaligned accesses; the PD port handles this with careful casts and
`-fno-strict-aliasing`. Reuse those flags.
---
## 10. References
* PD port: https://github.com/fgsfdsfgs/perfect_dark
* PD decomp: https://github.com/n64decomp/perfect_dark
* GE decomp (this repo): https://github.com/n64decomp/007
* GE docs: https://github.com/kholdfuzion/goldeneye_docs
* N64 devkit / libultra: `include/PR/*.h` in this repo
* GE custom GBI: `include/gbi_extension.h`
* GE RSP ucode: `rsp/graphics/gmain.s`
---
@@ -1,9 +1,12 @@
# PORT-LEARNINGS — recurring N64→PC bug classes # Porting notes — recurring N64→PC bug classes
One-screen index into `docs/PCPortResearch.md` §F. **Every investigation A one-screen field guide to the bug classes that keep recurring when running
subagent reads this first and appends any new generalisable quirk.** big-endian 32-bit N64 game code, unmodified, on a little-endian 64-bit host.
Details, evidence, and the fix for each instance live in §F under the Each entry cites a `Dxx` label; the full evidence and fix for that instance
cited Dxx label. live in [`dev/findings.md`](dev/findings.md) under the same label.
If you are debugging a crash in this port, read this first — the odds are
good that you are looking at one of these.
## A. Pointer-width struct growth (32→64) — the dominant class ## A. Pointer-width struct growth (32→64) — the dominant class
@@ -30,7 +33,7 @@ state.
`.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). N64 via act-union@0x2C+84; act union moves to ~0x38 on PC).
- **Open landmine:** raw hardcoded-offset accessors into `struct player` - **Open landmine:** raw hardcoded-offset accessors into `struct player`
/ `struct hand` — see `docs/AUDIT-M6-player-offsets.md`. / `struct hand` — see `docs/dev/AUDIT-M6-player-offsets.md`.
- **D126 corollary — a trailing runtime list pointer in a ROM-serialized - **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`, record.** If a fixed-size ROM record ends with a `T *next` (or `*child`,
`*parent`) that game code *writes* while walking the setup stream `*parent`) that game code *writes* while walking the setup stream
@@ -381,7 +384,7 @@ S-swap that mirrors the whole screen — a bad trade for a cosmetic bug.
**Next attempt needs a RenderDoc/apitrace capture of one glyph texrect **Next attempt needs a RenderDoc/apitrace capture of one glyph texrect
OR the asymmetric-1-texel-texture experiment — nothing else.** Cosmetic, OR the asymmetric-1-texel-texture experiment — nothing else.** Cosmetic,
deprioritised below level-progression / crash work. See deprioritised below level-progression / crash work. See
`docs/GRAPHICS-BACKLOG.md`. `docs/dev/GRAPHICS-BACKLOG.md`.
## D3. GCC/mingw makes an all-non-negative `enum` UNSIGNED ## D3. GCC/mingw makes an all-non-negative `enum` UNSIGNED
@@ -492,7 +495,7 @@ past it (walk off the end).
`tools_pc/pixcount.py` vs `docs/reference/n64-footage-*`). `tools_pc/pixcount.py` vs `docs/reference/n64-footage-*`).
- gdb **launch** mode is too slow for timing-dependent faults; gdb - gdb **launch** mode is too slow for timing-dependent faults; gdb
**attach** to an already-running process is fine and fast. **attach** to an already-running process is fine and fast.
- Check `PD_PORT_CHECKOUT` for the PD analogue before - 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 - Don't re-investigate a closed §F finding or re-derive a format spec
that already has a converter. that already has a converter.
@@ -1,510 +0,0 @@
<!DOCTYPE html>
<html lang="en-au" data-darkreader-mode="dynamic" data-darkreader-scheme="dark"><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"><style class="darkreader darkreader--fallback" media="screen"></style><style class="darkreader darkreader--user-agent" media="screen">@layer {
html {
background-color: var(--darkreader-background-ffffff, #181a1b) !important;
}
html {
color-scheme: dark !important;
}
iframe {
color-scheme: dark !important;
}
html, body {
background-color: var(--darkreader-background-ffffff, #181a1b);
}
html, body {
border-color: var(--darkreader-border-4c4c4c, #736b5e);
color: var(--darkreader-text-000000, #e8e6e3);
}
a {
color: var(--darkreader-text-0040ff, #3391ff);
}
table {
border-color: var(--darkreader-border-808080, #545b5e);
}
mark {
color: var(--darkreader-text-000000, #e8e6e3);
}
::placeholder {
color: var(--darkreader-text-a9a9a9, #b2aba1);
}
input:-webkit-autofill,
textarea:-webkit-autofill,
select:-webkit-autofill {
background-color: var(--darkreader-background-faffbd, #404400) !important;
color: var(--darkreader-text-000000, #e8e6e3) !important;
}
* {
scrollbar-color: var(--darkreader-background-b0b0b0, #454a4d) var(--darkreader-background-f1f1f1, #202324);
}
::selection {
background-color: var(--darkreader-background-0060d4, #004daa) !important;
color: var(--darkreader-text-ffffff, #e8e6e3) !important;
}
::-moz-selection {
background-color: var(--darkreader-background-0060d4, #004daa) !important;
color: var(--darkreader-text-ffffff, #e8e6e3) !important;
}
}</style><style class="darkreader darkreader--text" media="screen"></style><style class="darkreader darkreader--invert" media="screen">.jfk-bubble.gtx-bubble, .captcheck_answer_label > input + img, span#closed_text > img[src^="https://www.gstatic.com/images/branding/googlelogo"], span[data-href^="https://www.hcaptcha.com/"] > #icon, img.Wirisformula, a[data-testid="headerMediumLogo"]>svg, .d2l-navigation-link-image-container, .d2l-iframe-loading-container, div.mc-ip-hide[style*="background: rgb(255, 255, 255) !important"], [style*="color: rgb(0, 0, 1) !important;"] {
filter: invert(100%) hue-rotate(180deg) contrast(90%) !important;
}</style><style class="darkreader darkreader--inline" media="screen">[data-darkreader-inline-bgcolor] {
background-color: var(--darkreader-inline-bgcolor) !important;
}
[data-darkreader-inline-bgimage] {
background-image: var(--darkreader-inline-bgimage) !important;
}
[data-darkreader-inline-border] {
border-color: var(--darkreader-inline-border) !important;
}
[data-darkreader-inline-border-bottom] {
border-bottom-color: var(--darkreader-inline-border-bottom) !important;
}
[data-darkreader-inline-border-left] {
border-left-color: var(--darkreader-inline-border-left) !important;
}
[data-darkreader-inline-border-right] {
border-right-color: var(--darkreader-inline-border-right) !important;
}
[data-darkreader-inline-border-top] {
border-top-color: var(--darkreader-inline-border-top) !important;
}
[data-darkreader-inline-boxshadow] {
box-shadow: var(--darkreader-inline-boxshadow) !important;
}
[data-darkreader-inline-color] {
color: var(--darkreader-inline-color) !important;
}
[data-darkreader-inline-fill] {
fill: var(--darkreader-inline-fill) !important;
}
[data-darkreader-inline-stroke] {
stroke: var(--darkreader-inline-stroke) !important;
}
[data-darkreader-inline-outline] {
outline-color: var(--darkreader-inline-outline) !important;
}
[data-darkreader-inline-stopcolor] {
stop-color: var(--darkreader-inline-stopcolor) !important;
}
[data-darkreader-inline-bg] {
background: var(--darkreader-inline-bg) !important;
}
[data-darkreader-inline-border-short] {
border: var(--darkreader-inline-border-short) !important;
}
[data-darkreader-inline-border-bottom-short] {
border-bottom: var(--darkreader-inline-border-bottom-short) !important;
}
[data-darkreader-inline-border-left-short] {
border-left: var(--darkreader-inline-border-left-short) !important;
}
[data-darkreader-inline-border-right-short] {
border-right: var(--darkreader-inline-border-right-short) !important;
}
[data-darkreader-inline-border-top-short] {
border-top: var(--darkreader-inline-border-top-short) !important;
}
[data-darkreader-inline-invert] {
filter: invert(100%) hue-rotate(180deg);
}</style><style class="darkreader darkreader--variables" media="screen">:root {
--darkreader-neutral-background: var(--darkreader-background-ffffff, #181a1b);
--darkreader-neutral-text: var(--darkreader-text-000000, #e8e6e3);
--darkreader-selection-background: var(--darkreader-background-0060d4, #004daa);
--darkreader-selection-text: var(--darkreader-text-ffffff, #e8e6e3);
}</style><style class="darkreader darkreader--root-vars" media="screen"></style><meta http-equiv="X-Clacks-Overhead" content="GNU Terry Pratchett"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Decompiling a Nintendo 64 Game in 84 Days | Chris' Blog</title><meta name="title" content="Decompiling a Nintendo 64 Game in 84 Days"><meta name="description" content="The original Snowboard Kids has been fully decompiled in only 84 days! In this post I delve into what changed and what lessons might apply to future projects."><meta name="keywords" content=""><meta property="og:url" content="http://blog.chrislewis.au/decompiling-a-nintendo-64-game-in-84-days/"><meta property="og:site_name" content="Chris' Blog"><meta property="og:title" content="Decompiling a Nintendo 64 Game in 84 Days"><meta property="og:description" content="The original Snowboard Kids has been fully decompiled in only 84 days! In this post I delve into what changed and what lessons might apply to future projects."><meta property="og:locale" content="en_au"><meta property="og:type" content="article"><meta property="article:section" content="blog"><meta property="article:published_time" content="2026-08-26T20:23:59-07:00"><meta property="article:modified_time" content="2026-08-26T20:23:59-07:00"><meta property="og:image" content="http://blog.chrislewis.au/snowboard-kids-progress.png"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="http://blog.chrislewis.au/snowboard-kids-progress.png"><meta name="twitter:title" content="Decompiling a Nintendo 64 Game in 84 Days"><meta name="twitter:description" content="The original Snowboard Kids has been fully decompiled in only 84 days! In this post I delve into what changed and what lessons might apply to future projects."><meta itemprop="name" content="Decompiling a Nintendo 64 Game in 84 Days"><meta itemprop="description" content="The original Snowboard Kids has been fully decompiled in only 84 days! In this post I delve into what changed and what lessons might apply to future projects."><meta itemprop="datePublished" content="2026-08-26T20:23:59-07:00"><meta itemprop="dateModified" content="2026-08-26T20:23:59-07:00"><meta itemprop="wordCount" content="2266"><meta itemprop="image" content="http://blog.chrislewis.au/snowboard-kids-progress.png"><meta name="referrer" content="no-referrer-when-downgrade"><style>:root{--width:720px;--font-main:Verdana, sans-serif;--font-secondary:Verdana, sans-serif;--font-scale:1em;--background-color:#fff;--heading-color:#222;--text-color:#444;--link-color:#3273dc;--visited-color:#8b6fcb;--blockquote-color:#222}@media(prefers-color-scheme:dark){:root{--background-color:#01242e;--heading-color:#eee;--text-color:#ddd;--link-color:#8cc2dd;--visited-color:#8b6fcb;--blockquote-color:#ccc}}body{font-family:var(--font-secondary);font-size:var(--font-scale);margin:auto;padding:20px;max-width:var(--width);text-align:left;background-color:var(--background-color);word-wrap:break-word;overflow-wrap:break-word;line-height:1.5;color:var(--text-color)}h1,h2,h3,h4,h5,h6{font-family:var(--font-main);color:var(--heading-color)}a{color:var(--link-color);cursor:pointer;text-decoration:none}a:hover{text-decoration:underline}nav a{margin-right:8px}strong,b{color:var(--heading-color)}button{margin:0;cursor:pointer}time{font-family:monospace;font-style:normal;font-size:15px}main{line-height:1.6}table{width:100%}hr{border:0;border-top:1px dashed}img{max-width:100%}code{font-family:monospace;padding:2px;border-radius:3px}blockquote{border-left:1px solid #999;color:var(--blockquote-color);padding-left:20px;font-style:italic}footer{padding:25px 0;text-align:center}.title:hover{text-decoration:none}.title h1{font-size:1.5em}.inline{width:auto!important}.highlight,.code{border-radius:3px;margin-block-start:1em;margin-block-end:1em;overflow-x:auto}ul.blog-posts{list-style-type:none;padding:unset}ul.blog-posts li{display:flex}ul.blog-posts li span{flex:0 0 130px}ul.blog-posts li a:visited{color:var(--visited-color)}</style><style class="darkreader darkreader--sync" media="screen"></style><style type="text/css">img{margin:0 auto;display:block}@media(prefers-color-scheme:dark){img[src$="#darksafe"]{filter:invert(1)}}.image-caption{font-size:calc(var(--font-scale) * .8);margin:0 20px;font-style:italic;opacity:.9;text-align:center}.highlight{background-color:#272822}.bluesky-comments{margin-top:2.5rem}.bluesky-comments h2{font-size:1.25em;margin-bottom:.5rem}.bluesky-comments__status,.bluesky-comments__summary{font-size:calc(var(--font-scale) * .9)}.bluesky-comments__summary{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem 1rem}.bluesky-comments__reply{display:grid;gap:.75rem;grid-template-columns:44px 1fr;margin-top:1rem}.bluesky-comments__avatar{aspect-ratio:1;border-radius:50%;display:block;height:44px;object-fit:cover;width:44px}.bluesky-comments__avatar--blank{background:var(--blockquote-color);opacity:.25}.bLine truncated
background-color: white !important;
}
.vimvixen-hint {
background-color: var(--darkreader-background-ffd76e, #684b00) !important;
border-color: var(--darkreader-background-c59d00, #9e7e00) !important;
color: var(--darkreader-text-302505, #d7d4cf) !important;
}
#vimvixen-console-frame {
color-scheme: light !important;
}
::placeholder {
opacity: 0.5 !important;
}
#edge-translate-panel-body,
.MuiTypography-body1,
.nfe-quote-text {
color: var(--darkreader-neutral-text) !important;
}
gr-main-header {
background-color: var(--darkreader-background-add8e6, #1b4958) !important;
}
.tou-z65h9k,
.tou-mignzq,
.tou-1b6i2ox,
.tou-lnqlqk {
background-color: var(--darkreader-neutral-background) !important;
}
.tou-75mvi {
background-color: var(--darkreader-background-cfecf5, #0f3a47) !important;
}
.tou-ta9e87,
.tou-1w3fhi0,
.tou-1b8t2us,
.tou-py7lfi,
.tou-1lpmd9d,
.tou-1frrtv8,
.tou-17ezmgn {
background-color: var(--darkreader-background-f5f5f5, #1e2021) !important;
}
.tou-uknfeu {
background-color: var(--darkreader-background-faedda, #432c09) !important;
}
.tou-6i3zyv {
background-color: var(--darkreader-background-85c3d8, #245d70) !important;
}
div.mermaid-viewer-control-panel .btn {
background-color: var(--darkreader-neutral-background);
fill: var(--darkreader-neutral-text);
}
svg g rect.er {
fill: var(--darkreader-neutral-background) !important;
}
svg g rect.er.entityBox {
fill: var(--darkreader-neutral-background) !important;
}
svg g rect.er.attributeBoxOdd {
fill: var(--darkreader-neutral-background) !important;
}
svg g rect.er.attributeBoxEven {
fill: var(--darkreader-selection-background);
fill-opacity: 0.8 !important;
}
svg rect.er.relationshipLabelBox {
fill: var(--darkreader-neutral-background) !important;
}
svg g g.nodes rect,
svg g g.nodes polygon {
fill: var(--darkreader-neutral-background) !important;
}
svg g rect.task {
fill: var(--darkreader-selection-background) !important;
}
svg line.messageLine0,
svg line.messageLine1 {
stroke: var(--darkreader-neutral-text) !important;
}
div.mermaid .actor {
fill: var(--darkreader-neutral-background) !important;
}
mitid-authenticators-code-app > .code-app-container {
background-color: white !important;
padding-top: 1rem;
}
iframe#unpaywall[src$="unpaywall.html"] {
color-scheme: light !important;
}
select {
--darkreader-bg--form-control-background-color: rgba(22, 22, 22, 0) !important;
}
body#tumblr {
--darkreader-bg--secondary-accent: 31, 32, 34 !important;
--darkreader-bg--white: 23, 23, 23 !important;
--darkreader-text--black: 228, 224, 218 !important;
}
:host {
--d2l-border-color: var(--darkreader-bg--d2l-color-gypsum) !important;
--d2l-button-icon-background-color-hover: var(--darkreader-bg--d2l-color-gypsum) !important;
--d2l-color-ferrite: var(--darkreader-neutral-text) !important;
--d2l-color-sylvite: var(--darkreader-bg--d2l-color-sylvite) !important;
--d2l-dropdown-background-color: var(--darkreader-neutral-background) !important;
--d2l-dropdown-border-color: var(--darkreader-border--d2l-color-mica) !important;
--d2l-input-backgroud-color: var(--darkreader-neutral-background) !important;
--d2l-menu-border-color: var(--darkreader-bg--d2l-color-gypsum) !important;
--d2l-tooltip-background-color: var(--darkreader-neutral-background) !important;
--d2l-tooltip-border-color: var(--darkreader-bg--d2l-color-gypsum) !important;
}
:host([_floating]) .d2l-floating-buttons-container {
background-color: var(--darkreader-neutral-background) !important;
border-top-color: var(--darkreader-border--d2l-color-mica) !important;
opacity: 0.88 !important;
}
d2l-card {
background: var(--darkreader-neutral-background) !important;
border-color: var(--darkreader-border--d2l-color-gypsum) !important;
}
d2l-dropdown-content > div,
d2l-menu-item {
background-color: var(--darkreader-neutral-background) !important;
border-radius: 10px !important;
}
d2l-empty-state-simple {
border-color: var(--darkreader-bg--d2l-color-gypsum) !important;
}
.d2l-button-filter > ul > li > a.vui-button {
border-color: var(--darkreader-border--d2l-color-mica) !important;
}
.d2l-label-text:has(.d2l-button-subtle-content):hover,
.d2l-label-text:has(.d2l-button-subtle-content):focus,
.d2l-label-text:has(.d2l-button-subtle-content):active {
background-color: var(--darkreader-bg--d2l-color-gypsum) !important;
}
.d2l-navigation-centerer {
color: inherit !important;
}
.d2l-tabs-layout {
border-color: var(--darkreader-border--d2l-color-gypsum) !important;
}
.d2l-input,
.d2l-calendar-date,
.d2l-htmleditor-container {
background-color: var(--darkreader-neutral-background) !important;
}
.d2l-collapsible-panel {
border: 1px solid var(--darkreader-border--d2l-color-mica) !important;
border-radius: 0.4rem !important;
}
.d2l-collapsible-panel-divider {
border-bottom: 1px solid var(--darkreader-border--d2l-color-mica) !important;
}
.d2l-w2d-flex {
border-bottom: 2px solid var(--darkreader-border--d2l-color-mica) !important;
}
.d2l-collapsible-panel scrolled,
.d2l-collapsible-panel-header,
.d2l-w2d-collection-fixed {
background-color: var(--darkreader-neutral-background) !important;
}
.d2l-loading-spinner-bg {
fill: var(--darkreader-bg--d2l-color-gypsum) !important;
}
.d2l-loading-spinner-bg-stroke {
stroke: var(--darkreader-border--d2l-color-mica) !important;
}
.d2l-loading-spinner-wrapper svg path,
.d2l-loading-spinner-wrapper svg circle {
fill: var(--darkreader-neutral-background) !important;
}
.d2l-twopanelselector-side.d2l-twopanelselector-side-sep {
background: var(--darkreader-bg--d2l-color-mica) !important;
}
.d2l-le-TreeAccordionItem-anchor::before {
background: var(--darkreader-bg--d2l-color-corundum) !important;
}
table[class|="moz-header"] {
background: none;
}
*[style*="color: rgba(0,0,0,"]:not([style*="color: rgba(0,0,0,0)"]),
*[style*="color: rgba(0, 0, 0,"]:not([style*="color: rgba(0, 0, 0, 0)"]),
*[style*="color:#000"],
*[style*="color:black"],
*[style*="color:#282828"],
*[style*="color:#444"],
*[style*="color: #222222"] {
color: var(--darkreader-neutral-text) !important;
}
*[style*="background:#fff"],
*[style*="background-color:#fff"],
*[style*="background: #fff"] {
background: var(--darkreader-neutral-background) !important;
}
*[bgcolor="#F5F5F5"],
*[style*="background-color: #F5F5F5"] {
background: var(--darkreader-background-f5f5f5, #1e2021) !important;
}
*[style*="background: #e6e6e6"] {
background: var(--darkreader-background-e6e6e6, #26292b) !important;
}
body[style*="background: rgb(255, 255, 255) !important"] *:not(a),
.body[style*="background-color: rgb(254, 254, 254) !important"] *:not(a) {
background-color: var(--darkreader-neutral-background) !important;
}
div[style*="www.gstatic.com/android/"] {
background: none !important;
}
*[style*="background:#f9f9f9"] {
background: var(--darkreader-background-f9f9f9, #1b1e1f) !important;
}
body.mediawiki.skin-vector #mw-page-base {
background-image: none !important;
}
.overflow-hidden .spinning-bg,
.overflow-hidden .solid-bg {
background: transparent !important;
}
embed[type="application/pdf"] { filter: invert(100%) contrast(90%); }</style><style></style></head><body><header><a href="https://blog.chrislewis.au/" class="title"><h2>Chris' Blog</h2></a><nav></nav></header><main><h1>Decompiling a Nintendo 64 Game in 84 Days</h1><p><i><time datetime="2026-08-26">26 Aug, 2026</time></i></p><content><p>I’m very pleased to announce that the original <em>Snowboard Kids</em> is now 100% decompiled! This means that all functions<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> have matching C implementations that, when compiled, produce identical machine code to the original game.</p><p>This was obviously not a one-person effort. I am particularly grateful to <a href="https://github.com/inspectredc">inspectredc</a>, <a href="https://github.com/Bl00D4NGEL">Bl00D4NGEL</a>, and <a href="https://github.com/queueRAM">queueRAM</a> for their significant contributions to the project. No amount of AI would have been able to replace them.<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> I would also like to thank <a href="https://github.com/iFuzzle">iFuzzle</a>, <a href="https://github.com/JamesBLewis">JamesBLewis</a>, and <a href="https://github.com/douglasjv">douglasjv</a> for lending their tokens to the cause.</p><p>My
hope is that a full decompilation will prove useful to the Snowboard
Kids community. Speedrunners in particular have long focused on the
first game. Working source code can help shed light on externally
observed phenomena such as CPU pathing and the exact factors
contributing to player speed. A full understanding of the source code
will also be useful for static recompilation and more ambitious modding
efforts in the future.</p><p>The speed at which the project was finished is also noteworthy. <em>Snowboard Kids</em> took only 84 days to decompile compared with 596 days for <em>Snowboard Kids 2</em>, roughly one-seventh of the elapsed time.<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p><p><a href="https://decomp.dev/cdlewis/snowboardkids-decomp"><img src="Decompiling%20a%20Nintendo%2064%20Game%20in%2084%20Days%20_%20Chris'%20Blog_files/snowboard-kids-progress_aOx2.svg" alt="chart showing weekly Snowboard Kids decompilation progress"></a></p><p class="image-caption"><a href="https://decomp.dev/cdlewis/snowboardkids-decomp">Weekly progress of the Snowboard Kids decompilation.</a></p><p></p><p>What
explains this difference? Well, it’s 2026, so the answer is at least
partially AI. But it would be a gross oversimplification to attribute
the difference entirely to LLMs.</p><h2 id="what-was-different">What Was Different</h2><p>To
state the obvious, I was not starting from scratch. By this point I had
already spent nearly two years on a similar project and was vastly
faster than when I began. This advantage is difficult to quantify and
was somewhat offset by new challenges such as working with a different
compiler.</p><p>Overall, roughly 4.8% of matching commits involved expert intervention.<sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>
I have already credited these amazing people once, but it’s worth
reiterating. This project would not have been possible without
significant help from the decompilation community, particularly <a href="https://github.com/inspectredc">inspectredc</a>, <a href="https://github.com/queueRAM">queueRAM</a>, and <a href="https://github.com/Bl00D4NGEL">Bl00D4NGEL</a> &lt;3.</p><p>The difficulty generally didn’t come from trying to understand what a function did<sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup> but rather how that logic was expressed in C and how the resulting code was compiled. <em>Snowboard Kids</em> was compiled with IDO 5.3 rather than the GCC 2.7.2 compiler used by <em>Snowboard Kids 2</em>.</p><p>Most
programmers will be familiar with GCC. It is a widely used open-source
compiler still in active development today. IDO, on the other hand, was a
proprietary compiler developed by SGI, whose own story is closely
entwined with that of the Nintendo 64.<sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup>
Its source was not available, and its original development environment
was tied to obsolete SGI hardware and software. To use and properly
understand it today, the decompilation community has had to
reverse-engineer and <a href="https://github.com/n64decomp/ido">decompile parts of the compiler toolchain</a> as well as <a href="https://github.com/decompals/ido-static-recomp">statically recompile the IDO 5.3 and 7.1 suites</a> to run on modern hardware. That closed history makes IDO harder to reason about than an open compiler such as GCC.</p><p>IDO splits optimisation and code generation across several different passes, transforming code quite aggressively along the way.<sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup> Tiny changes to the C can then ripple through those passes and produce a completely different register allocation.</p><p>The
community has made great strides in understanding IDO and its quirks,
but this remains more of an art than a science. LLMs and I are not
particularly good at reproducing its output. The usual workflow, for
both me and the agents, was to figure out what a function did and then
write C that approximated that purpose. From there, small tweaks
assisted by the <a href="https://github.com/simonlindholm/decomp-permuter">permuter</a>
could catch any remaining differences. But you can’t permute yourself
into a match in all cases, particularly when the underlying structure is
wrong. IDO’s behaviour made this workflow far less predictable.</p><p>A motivated human team with the right expertise and intuition can match or even exceed the pace of the <em>Snowboard Kids</em> decompilation. The <a href="https://github.com/gcsmith/Pilotwings64Decomp"><em>Pilotwings 64</em></a> decompilation was completed in only 74 days.<sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup> <em>Pilotwings 64</em> had 16% fewer functions than <em>Snowboard Kids</em>, but more compiled code overall, so this is not a clean comparison either.</p><p><img src="Decompiling%20a%20Nintendo%2064%20Game%20in%2084%20Days%20_%20Chris'%20Blog_files/pilotwings_aOx2.webp" alt="screenshot of Pilotwings 64"></p><p class="image-caption">Pilotwings 64, a charming flight simulator, was a Nintendo 64 launch title and remains a cult classic to this day.</p><p></p><p><em>Snowboard Kids</em> was also smaller than its sequel, containing 2,145 functions compared with 2,995 in <em>Snowboard Kids 2</em>. Function count is a crude measure of difficulty, but there were simply fewer functions to decompile.</p><h2 id="where-agents-did-help">Where Agents Did Help</h2><p>I’ve <a href="https://blog.chrislewis.au/the-long-tail-of-llm-assisted-decompilation/">already written elsewhere</a>
about using agents to decompile functions. The same basic process was
used here, so I’ll focus on what changed. Unlike the previous project,
this one began with access to frontier models and a capable <a href="https://github.com/cdlewis/nigel">agent harness</a>.</p><h3 id="library-code-and-other-low-hanging-fruit">Library Code and Other Low-Hanging Fruit</h3><p>I
was interested to see how agents would fare during the early stages of a
project. One area where they thrived was matching standard-library
code. In theory, this is the most obvious chunk of almost any Nintendo
64 decompilation. The code is not unique to the game, and versions of it
are available online. <em>Snowboard Kids</em> contains more than a hundred source segments from Nintendo’s <code>libultra</code>, alongside functions from the <code>libmus</code> audio library. Tools such as <a href="https://github.com/shygoo/n64sym">N64Sym</a> can identify probable library functions in the ROM.</p><p>This
pass was fairly successful. The main stumbling block was convincing
agents to rely on the existing library source rather than decompile the
same functions again from scratch. This required stronger prompting.
Once a likely library function was identified, agents were instructed to
treat the corresponding source as their starting point and exhaust
plausible SDK versions, compiler options, and conditional compilation
paths before attempting their own implementation.</p><p>Another optimisation was to have an agent write a script that ran <a href="https://github.com/matt-kempster/m2c"><code>m2c</code></a>
against every unmatched function and automatically integrated any exact
matches, rather than relying on agents to attempt those functions
individually.<sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup>
The script matched only 17 of 1,830 functions, an incredibly low
success rate of 0.93%, but anything matched this way was cheaper than
burning agent tokens.</p><h3 id="ido-tooling-and-skills">IDO Tooling and Skills</h3><p>IDO
is weird, but it is often weird in recurring ways. Successfully
matching one function could reveal a compiler quirk that applied to many
others. Codex has become better at carrying lessons between tasks
through features such as local <a href="https://learn.chatgpt.com/docs/customization/memories">memories</a>. To make those lessons available beyond a single agent, I prompted agents to record observed IDO behaviour in a <a href="https://github.com/cdlewis/snowboardkids-decomp/blob/main/DECOMPILATION_LEARNINGS.md">DECOMPILATION_LEARNINGS.md</a>
file. When an agent discovered a generalisable compiler quirk, it could
record the evidence there for later attempts. This created a useful
feedback loop. Agents helped document IDO, and the resulting
documentation made subsequent agents better at matching IDO code.</p><p>But the most useful resource was <a href="https://github.com/akratch/n64-decomp-workbench">N64 Decomp Workbench</a>,
a collection of tooling and documentation for debugging late-stage MIPS
decompilation mismatches. It can classify mismatches, account for
relocations, replay individual compiler passes, and help distinguish a
structural problem from a register-allocation problem. Pass replay
requires the relevant compiler binaries and project-specific setup, but
once configured it exposes information that a raw assembly diff cannot. A
raw diff tells you that two functions differ. The Workbench can give
agents a much better idea of why the functions differ and what sort of
change might fix them.</p><h3 id="worktrees-and-synchronisation">Worktrees and Synchronisation</h3><p>For
this project, I ran the decompilation harness across four Git
worktrees. Each worktree gave an agent an independent copy of the
repository, allowing several functions to be attempted in parallel.</p><p>One small but useful improvement was to give every task an explicit deadline and expose that deadline to the agent. During the <em>Snowboard Kids 2</em>
project, agents often struggled to use the permuter effectively because
it would continue running until it found a 100% match or was manually
stopped. An explicit deadline allowed agents to set sensible timeouts
and trade off permuting time against other forms of problem-solving.
Anecdotally, it also helped them judge how long to keep working on a
difficult function before giving up. I could then increase the time
allowance as the easy functions disappeared and the remaining work
became more difficult.</p><p>Another problem that had existed in <em>Snowboard Kids 2</em> but became more apparent as I added worktrees was synchronisation. As discussed in my <a href="https://blog.chrislewis.au/the-long-tail-of-llm-assisted-decompilation/">previous post</a>,
each agent was given a function to decompile alongside a set of similar
functions that had already been matched. These provided useful
reference points for reproducing particular IDO instruction patterns.
Work was divided between the worktrees using Nigel’s <code>--shard</code> option, which uses basic hashing to partition candidates between a specified number of workers.</p><p>This
allowed more work to happen in parallel without duplication, but
introduced a new problem as the worktrees diverged. One worktree, for
example, might successfully decompile a function that was 99% similar to
a function being attempted elsewhere, but that new reference would
remain invisible to the other agent until the changes were merged.
Periodically merging everything into the main branch and resynchronising
the worktrees fixed this, but synchronising all four could take more
than an hour. Synchronising continuously wasted time, while waiting too
long increased drift.</p><p>To make synchronisation less critical, I
updated the similarity search to inspect every worktree. A newly matched
function could immediately become a reference for another agent without
waiting for it to reach the main branch. The harness would produce
messages such as the following.</p><div class="highlight"><pre tabindex="0" style="color: rgb(248, 248, 242); background-color: rgb(39, 40, 34); tab-size: 4; --darkreader-inline-color: var(--darkreader-text-f8f8f2, #e8e8d5); --darkreader-inline-bgcolor: var(--darkreader-background-272822, #1c1e1f);" data-darkreader-inline-color="" data-darkreader-inline-bgcolor=""><code class="language-text" data-lang="text"><span style="display:flex"><span style="white-space: pre; user-select: none; margin-right: 0.4em; padding: 0px 0.4em; color: rgb(127, 127, 127); --darkreader-inline-color: var(--darkreader-text-7f7f7f, #988f81);" data-darkreader-inline-color="">1</span><span>✓ Candidate ["func_80094A94", "func_80094FF4 (../sbk-c)",
</span></span><span style="display:flex"><span style="white-space: pre; user-select: none; margin-right: 0.4em; padding: 0px 0.4em; color: rgb(127, 127, 127); --darkreader-inline-color: var(--darkreader-text-7f7f7f, #988f81);" data-darkreader-inline-color="">2</span><span> "func_80094808", "func_8009491C (../sbk-a)",
</span></span><span style="display:flex"><span style="white-space: pre; user-select: none; margin-right: 0.4em; padding: 0px 0.4em; color: rgb(127, 127, 127); --darkreader-inline-color: var(--darkreader-text-7f7f7f, #988f81);" data-darkreader-inline-color="">3</span><span> "func_8009469C (../sbk-c)", "func_80093144"] was fixed!
</span></span></code></pre></div><p>This helped the process remain
efficient as the easy functions disappeared. Synchronisation was still
needed to consolidate and push changes, but it was no longer required
for agents to learn from one another.</p><h3 id="model-choice">Model Choice</h3><p>I
tried GPT-5.5 and 5.6, Claude 4.5 and Fable, and GLM 5.2. This is
completely unscientific. The models were tested against a changing set
of difficult functions, sometimes after another model had already made
partial progress. Broadly, though, Codex continued to outperform Claude,
as it had towards the end of the previous project. Sol xhigh was
particularly effective once it became available.</p><p>GLM 5.2, served
by z.ai, was very disappointing. I had previously been a big fan of GLM.
It was effective, even if it was not quite a frontier model, and its
generous usage limits made up for the gap. That tradeoff became much
less attractive as the limits grew less generous while latency remained
awful. The feedback cycles were so long that I stopped giving it work
and eventually cancelled my subscription.</p><h2 id="what-next">What Next?</h2><p>The
immediate priority is to better document the game. A 100% match means
we have C code for every function; it does not mean we understand what
every function does. There are still generated names to replace, unknown
structure fields to identify, awkward matches to clean up, and large
amounts of data to describe.</p><p>Work is also underway on a <em>Snowboard Kids</em> recompilation. Fortunately, the first game shares many of the quirks addressed by patches in <a href="https://blog.chrislewis.au/snowboard-kids-2-is-recompiled/"><em>Snowboard Kids 2: Recompiled</em></a>.</p><p><img src="Decompiling%20a%20Nintendo%2064%20Game%20in%2084%20Days%20_%20Chris'%20Blog_files/snowboard-kids-recomp-screenshot_aOx2.webp" alt="early screenshot of Snowboard Kids: Recompiled"></p><p>I’m
looking into porting the levels and other content from the first game
into the second game’s engine, although I have no idea how much work
that will be yet.</p><p>Beyond that, I’m interested in decompiling <em>Snowboard Kids Plus</em> on the PlayStation, a Japan-exclusive expanded release of the first game with extra levels and characters.</p><p><em>If you’ve made it this far, you’re probably interested in decompilation and Snowboard Kids. Take a look at the <a href="https://github.com/cdlewis/snowboardkids-decomp">Snowboard Kids decompilation project</a>. There is still plenty of cleanup and documentation work to do, and contributions are very welcome.</em></p><div class="footnotes" role="doc-endnotes"><hr><ol><li id="fn:1"><p>Almost.
A small amount of handwritten assembly from system libraries remains
because assembly was the original source rather than a temporary
decompilation placeholder.&nbsp;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p></li><li id="fn:2"><p>In the best case, I think the project would have stalled around 89–90% without their IDO expertise.&nbsp;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p></li><li id="fn:3"><p>For <em>Snowboard Kids 2</em>, I measured from the <a href="https://github.com/cdlewis/snowboardkids2-decomp/commit/f1025d16a8aa1d11ec937f8c721af59149feee7c">first commit on 28 September 2024</a> to the <a href="https://github.com/cdlewis/snowboardkids2-decomp/commit/ec405bd1aa8a6a2fed4421e0ba33e37b973db4ec">final matching function on 17 May 2026</a>, a span of 596 days. The 84-day figure for <em>Snowboard Kids</em> is my project-level count; the public repository itself spans from the <a href="https://github.com/cdlewis/snowboardkids-decomp/commit/08f9b60604fd084215770d4a787f7fa9d4d61865">first commit on 28 May 2026</a> to the <a href="https://github.com/cdlewis/snowboardkids-decomp/commit/875002983e210f492aec61ebe78444d2227e270b">final matching function on 19 August 2026</a>, just over 83 days.&nbsp;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">↩︎</a></p></li><li id="fn:4"><p>Based
on a manual review of the Git history, I identified roughly 41
expert-assisted matches, corresponding to about 4.8% of the commits I
classified as function-matching work. This retrospective classification
is approximate because commits can contain multiple functions, help was
often incorporated into my own commits, and the figure does not measure
person-hours. Note that I am not counting myself as an “expert”.&nbsp;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">↩︎</a></p></li><li id="fn:5"><p>One notable exception was <a href="https://github.com/cdlewis/snowboardkids-decomp/blob/ba90a9ead954514cfaa1e7c0143957fdfc754092/src/menu/main_menu/controller_main_menu_flow.c#L560"><code>validateControllerPakSave</code></a>,
which appears to have been used to debug Controller Pak saves. Most of
its debugging and validation logic was omitted from the final
build.&nbsp;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">↩︎</a></p></li><li id="fn:6"><p><a href="https://iwataasks.nintendo.com/interviews/wii/sinandpunishment/0/0/">Nintendo has described</a>
adopting an architecture from Silicon Graphics for the Nintendo 64. The
original development system was similarly SGI-centric. Nintendo’s <a href="https://ultra64.ca/files/documentation/online-manuals/man-v5-1/pro-man/pro01/index.htm">development documentation</a>
describes connecting its emulator board to an SGI Indy workstation, and
IDO was one of the supported compilers. The relationship therefore
extended from the console’s underlying architecture to the machines and
tools used to develop its games.&nbsp;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">↩︎</a></p></li><li id="fn:7"><p>IDO used a <a href="https://techpubs.jurassic.nl/library/manuals/2000/007-2479-001/sgi_html/ch01.html">multi-stage compilation pipeline</a>. Its <a href="https://techpubs.jurassic.nl/manpages_0530/cat5/uopt.html"><code>uopt</code> documentation</a>
says that eligible loops were unrolled four times by default.
Coincidentally, four is also the number of racers in the game. Loops
with four iterations are therefore candidates for complete unrolling,
and they occur frequently 🫠.&nbsp;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">↩︎</a></p></li><li id="fn:8"><p>Measured from the <a href="https://github.com/gcsmith/Pilotwings64Decomp/commit/64ae65fbc0dcbb1de772361b1dd84faf0816dc56">addition of progress tracking on 26 January 2026</a> to the <a href="https://github.com/gcsmith/Pilotwings64Decomp/commit/c041f5190e232988fec0cd17b1a5e6ad9182c25d">final matching functions on 10 April 2026</a>, the decompilation took almost exactly 74 days. <em>Pilotwings 64</em> had 1,791 functions compared with 2,145 for <em>Snowboard Kids</em>, but approximately 838 KB of code compared with 732 KB for <em>Snowboard Kids</em>.&nbsp;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">↩︎</a></p></li><li id="fn:9"><p>Using a one-shot script like this is not a particularly novel idea. I’m pretty sure I copied it from somewhere, possibly <em>Diddy Kong Racing</em>’s <a href="https://github.com/DavidSM64/Diddy-Kong-Racing/blob/84f0ea569b07903ba8a4f9f252e8dc8e4ba54bdc/m2c_all.sh">m2c_all.sh</a>.&nbsp;<a href="#fnref:9" class="footnote-backref" role="doc-backlink">↩︎</a></p></li></ol></div></content><section class="bluesky-comments" data-bluesky-post="https://bsky.app/profile/did:plc:5j7epmcl35kl5pni4y6ehpl2/post/3mu34bwrh4k2m"><h2>Comments</h2><p class="bluesky-comments__status">Loading Bluesky comments...</p></section><script defer="defer" src="Decompiling%20a%20Nintendo%2064%20Game%20in%2084%20Days%20_%20Chris'%20Blog_files/bluesky-comments_aOx2.js"></script><div style="font-size:.8em;display:flex;gap:16px;justify-content:center"><p><a href="https://blog.chrislewis.au/snowboard-kids-2-is-recompiled/">&lt;&lt; Previous Post</a></p><p>|</p><p><strike>Next Post &gt;&gt;</strike></p></div><p></p></main><footer></footer><script type="module" src="Decompiling%20a%20Nintendo%2064%20Game%20in%2084%20Days%20_%20Chris'%20Blog_files/v3d52b47920f24c319d37e2661827c42b1787588026925_aOx2" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon="{&quot;version&quot;:&quot;2024.11.0&quot;,&quot;token&quot;:&quot;10d5aece9021411ab2fd8081c3b4e020&quot;,&quot;r&quot;:1}" crossorigin="anonymous"></script>
<div>
<div class="vtResetstyles">
<div id="vtoverlay" class="vtoverlay w3-animate-right w3-container vtfadein">
<div class="vtmain" id="vtmain">
<div class="vtcontactus">
<a id="linktest" href="https://www.virustotal.com/">
<img class="vtlogo-popup" style="
width: 24px;
height: 32px;
margin-right: 5px !important;
" src="Decompiling%20a%20Nintendo%2064%20Game%20in%2084%20Days%20_%20Chris'%20Blog_files/gti-vt-icon2_aOx2.png">
</a>
<a href="https://www.virustotal.com/">VT4Browsers + Google TI
</a>
<a id="vtclose" class="vtclose" style="margin-left: 15px;"><svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 24 24" fill="currentColor" style="vertical-align: middle">
<path d="M6.4 18.654 5.346 17.6l5.6-5.6-5.6-5.6L6.4 5.346l5.6 5.6 5.6-5.6L18.654 6.4l-5.6 5.6 5.6 5.6-1.054 1.054-5.6-5.6-5.6 5.6Z"></path>
</svg></a>
</div>
<div class="vttextpopup" id="statustext">
<div class="vtscanstatus" id="vtscanstatus">Fetching file...<br></div>
<div class="vtprogress" id="vtprogress"></div>
<br id="brfile" class="vthidden">
<div class="vtscanstatus vthidden" id="vtscanfilelink"></div>
</div>
<div class="vttextpopup vthidden" id="widgettext">
<div id="vtwidgetError"></div>
</div>
</div>
</div>
</div>
<div id="vt-augment-container" class="vtzindex vt-augment drawer" style="background: rgb(49, 61, 90); --darkreader-inline-bgcolor: var(--darkreader-background-313d5a, #273148); --darkreader-inline-bgimage: none;" data-darkreader-inline-bgcolor="" data-darkreader-inline-bgimage=""></div></div><span class="vttooltiptext vthidden">&nbsp;Click to open IoC report&nbsp;<br>with VirusTotal Augment</span><style></style><style class="darkreader darkreader--sync" media="screen"></style></body></html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

+3 -3
View File
@@ -4,8 +4,8 @@ This directory contains the PC port layer: the code that lets the (unmodified)
GoldenEye game sources run on a modern host instead of N64 hardware. GoldenEye game sources run on a modern host instead of N64 hardware.
It mirrors the layout of the [Perfect Dark PC It mirrors the layout of the [Perfect Dark PC
port](https://github.com/perfect-dark-pc-port/perfect_dark), which ports the port](https://github.com/fgsfdsfgs/perfect_dark), which ports the
same Rare engine family. See `docs/PCPortResearch.md` (repo root) for the full same Rare engine family. See `docs/internals.md` (repo root) for the full
research write-up and phased plan. research write-up and phased plan.
## Layout ## Layout
@@ -66,5 +66,5 @@ exact command encodings.
**Scaffolding only.** The files in `src/` and `fast3d/` are stubs that define **Scaffolding only.** The files in `src/` and `fast3d/` are stubs that define
the interfaces and mark the work to be done. The actual implementation is the interfaces and mark the work to be done. The actual implementation is
brought in phase by phase (see `docs/PCPortResearch.md` §8), largely by brought in phase by phase (see `docs/internals.md` §8), largely by
adapting the PD port's `port/` layer. adapting the PD port's `port/` layer.
+29 -43
View File
@@ -1,51 +1,37 @@
# fast3d — software RSP for the GE PC port # fast3d — software RSP
This directory will hold the **software RSP**: a C++ interpreter that consumes A C++ interpreter that consumes the `Gfx` display list the game builds each
the Gfx display list the game builds and emits OpenGL calls, bypassing the N64 frame and emits OpenGL calls, replacing the Nintendo 64's Reality Signal
RDP entirely. Processor and Reality Display Processor.
## Source: adapt from the Perfect Dark PC port ## Origin and license
The PD port ships a complete, working fast3d at This code is **adapted from the [Perfect Dark PC
`perfect-dark-pc-port/perfect_dark/port/fast3d/`: port](https://github.com/fgsfdsfgs/perfect_dark)** (`port/fast3d/`), which in
turn is the [Ship of Harkinian](https://github.com/HarbourMasters) /
libultraship fast3d, itself descended from
[sm64-port](https://github.com/sm64-port/sm64-port).
| File | Role | ~Lines | `LICENSE.txt` in this directory (MIT, © 2020 Emill, MaikelChan) covers that
|--------------------|-------------------------------------------------------------|--------| lineage. `glad/` is generated by [glad](https://github.com/Dav1dde/glad)
| `gfx_pc.cpp` | The RSP interpreter: matrices, lights, fog, tri1/tri4, vtx | 2800 | (© David Herberth, MIT). GoldenEye-specific changes in this directory are
| `gfx_opengl.cpp` | OpenGL backend: shaders, textures, zbuf, framebuffers | 1300 | under the repository's top-level MIT license.
| `gfx_cc.cpp` | Color-combiner equation -> GLSL shader compiler | 60 |
| `gfx_sdl2.cpp` | SDL2 window-manager backend | 460 |
| `gfx_api.h` | Public API (`gfx_init/run/start_frame/end_frame/...`) | 58 |
| `gfx_pc.h` | Internal state (RSP struct, texture cache, combiner pool) | 60 |
| `gfx_rendering_api.h` | Rendering-API vtable (GL backend implements this) | 61 |
| `gfx_window_manager_api.h` | Window-manager vtable (SDL2 implements this) | 54 |
| `glad/` | GL loader | — |
**The PD fast3d already handles `G_TRI4`** (GE's 4-triangle command) — the PD ## Files
game uses it too. So the bulk of the RSP work is done; we adapt, not rewrite.
## GE-specific work to add | File | Role |
|---|---|
| `gfx_pc.cpp` | the RSP interpreter — matrices, lights, fog, `G_TRI1`/`G_TRI4`, vtx |
| `gfx_opengl.cpp` | OpenGL backend — shaders, textures, z-buffer, framebuffers |
| `gfx_cc.cpp` | color-combiner equation -> GLSL shader |
| `gfx_sdl2.cpp` | SDL2 window-manager backend |
| `gfx_api.h` | public API (`gfx_init` / `gfx_run` / `gfx_start_frame` / ...) |
| `gbiex.h` | GoldenEye custom GBI command definitions |
| `glad/` | OpenGL loader |
1. **`G_SETTEX`** (opcode `0xc0`) — GE's custom texture-bank command ## GoldenEye-specific work
(`include/gbi_extension.h`, `gsSPUseTexture`). Add a case in the `gfx_pc.cpp`
dispatch and a corresponding texture-bank selection in the GL backend.
Reverse the exact semantics from `rsp/graphics/gmain.s` + `src/game/tex.c` /
`initmttex.c`.
2. **Verify custom color-combiner modes** — `G_CC_MODULATEIFADE`, `G_CC_FADE`,
`G_CC_DECALFADE`, `G_CC_BLENDRGBFADEA`, etc. The `gfx_cc.cpp` compiler
handles arbitrary CC equations; confirm each GE mode compiles to correct
GLSL.
3. **Verify the custom render mode** — `G_RM_CUSTOM_AA_ZB_XLU_SURF`
(AA_ZB_XLU with `Z_UPD`).
4. **`gDPLoadTLUT06/07`** — map onto the existing `G_LOADTLUT` path.
## Ground truth The PD fast3d already handles `G_TRI4` (GoldenEye uses it too). The GE additions
are the custom color-combiner / render modes and the handling of GE's GBI
`rsp/graphics/gmain.s` (repo root) is the real GE RSP ucode. We do **not** run extension commands. `rsp/graphics/gmain.s` (GE's real RSP microcode, in the
it on PC, but it is the authoritative reference for the exact command repo root) is the ground truth for the exact command encodings.
encodings GE emits — use it to validate the software RSP's decoding.
## Status
**Empty (scaffolding).** Populate during Phase 2 by adapting the PD fast3d and
adding the GE-specific commands above.
+1 -1
View File
@@ -1,5 +1,5 @@
/* /*
* GE adaptation header for fast3d (see docs/PCPortResearch.md). * GE adaptation header for fast3d (see docs/internals.md).
* *
* fast3d was written against Perfect Dark's extended GBI set * fast3d was written against Perfect Dark's extended GBI set
* (pd_port/src/include/gbiex.h + PD's include/PR/gbi.h). GoldenEye shares the * (pd_port/src/include/gbiex.h + PD's include/PR/gbi.h). GoldenEye shares the
+2 -2
View File
@@ -610,7 +610,7 @@ void gfx_texture_cache_delete_range(const uint8_t* start, const uint8_t* end) {
} }
} }
// D71 (docs/PCPortResearch.md): texture sources arrive in two byte conventions. // D71 (docs/internals.md): texture sources arrive in two byte conventions.
// Raw N64 big-endian byte streams: ROM cart map (0x10xxxxxx), model-sidecar // Raw N64 big-endian byte streams: ROM cart map (0x10xxxxxx), model-sidecar
// blobs (cart extension 0x10Cxxxxx), KSEG0 mirror (0x80xxxxxx) and V1 // blobs (cart extension 0x10Cxxxxx), KSEG0 mirror (0x80xxxxxx) and V1
// dram/BSS/heap buffers (0x70xxxxxx, e.g. tex.c texture pool, rle_expand_8bit // dram/BSS/heap buffers (0x70xxxxxx, e.g. tex.c texture pool, rle_expand_8bit
@@ -1287,7 +1287,7 @@ static void gfx_sp_vertex(size_t n_vertices, size_t dest_index, const Vtx* verti
d->color.g = g > 255 ? 255 : g; d->color.g = g > 255 ? 255 : g;
d->color.b = b > 255 ? 255 : b; d->color.b = b > 255 ? 255 : b;
/* D72 (docs/PCPortResearch.md): unlike the PD port, GE never /* D72 (docs/internals.md): unlike the PD port, GE never
* generates texture coordinates from vertex normals. This PD- * generates texture coordinates from vertex normals. This PD-
* inherited block overwrote the authored tc[] UVs on every * inherited block overwrote the authored tc[] UVs on every
* lit+textured surface (lookat_enabled defaults to true here), * lit+textured surface (lookat_enabled defaults to true here),
+1 -1
View File
@@ -3,7 +3,7 @@
#include <PR/ultratypes.h> #include <PR/ultratypes.h>
/* D68 (docs/PCPortResearch.md): endianness fixup for the Globalimagetable /* D68 (docs/internals.md): endianness fixup for the Globalimagetable
* segment after texReset()'s romCopy(). The N64 CPU is big-endian, so every * segment after texReset()'s romCopy(). The N64 CPU is big-endian, so every
* u32 in the ROM copy is BE-encoded; PC consumers read them natively (LE). * u32 in the ROM copy is BE-encoded; PC consumers read them natively (LE).
* *
+1 -1
View File
@@ -3,6 +3,6 @@
/* Generated by CMake from hoststdlib.h.in — do not edit. /* Generated by CMake from hoststdlib.h.in — do not edit.
* Includes the HOST <stdlib.h> by absolute path so that port/shim/stdlib.h * Includes the HOST <stdlib.h> by absolute path so that port/shim/stdlib.h
* can bypass the N64 include/stdlib.h (whose lldiv_t/ldiv_t typedefs clash * can bypass the N64 include/stdlib.h (whose lldiv_t/ldiv_t typedefs clash
* with the host's in C++ TUs). See docs/PCPortResearch.md §11. */ * with the host's in C++ TUs). See docs/internals.md §11. */
#include "@PORT_HOST_STDLIB_H@" #include "@PORT_HOST_STDLIB_H@"
#endif #endif
+1 -1
View File
@@ -4,6 +4,6 @@
* Includes the HOST <string.h> by absolute path so that port/shim/string.h * Includes the HOST <string.h> by absolute path so that port/shim/string.h
* can bypass the N64 include/string.h (which declares strlen/strchr with * can bypass the N64 include/string.h (which declares strlen/strchr with
* `const unsigned char *` and conflicts with the host declarations that SDL * `const unsigned char *` and conflicts with the host declarations that SDL
* and other system headers pull in). See docs/PCPortResearch.md §11 D12. */ * and other system headers pull in). See docs/internals.md §11 D12. */
#include "@PORT_HOST_STRING_H@" #include "@PORT_HOST_STRING_H@"
#endif #endif
+1 -1
View File
@@ -1,7 +1,7 @@
/* /*
* pc_protos.h - PC-port-only prototypes for implicitly declared functions. * pc_protos.h - PC-port-only prototypes for implicitly declared functions.
* *
* D38 (docs/PCPortResearch.md section F): the decomp has ~72 translation units * D38 (docs/internals.md section F): the decomp has ~72 translation units
* that call ~400 functions without any visible prototype (missing #include of * that call ~400 functions without any visible prototype (missing #include of
* the declaring header). Under C11 an implicit declaration assumes `int f()`, * the declaring header). Under C11 an implicit declaration assumes `int f()`,
* which on N64 (MIPS, 32-bit pointers) is harmless, but on x86-64 it silently * which on N64 (MIPS, 32-bit pointers) is harmless, but on x86-64 it silently
+1 -1
View File
@@ -7,7 +7,7 @@
extern "C" { extern "C" {
#endif #endif
/* D69 (docs/PCPortResearch.md): PC-layout stage bg/*.seg + Tbg_*_stanZ /* D69 (docs/internals.md): PC-layout stage bg/*.seg + Tbg_*_stanZ
* sidecars -- same Plan-B pattern as D50/pcmodels.c. * sidecars -- same Plan-B pattern as D50/pcmodels.c.
* *
* N64 bg/*.seg and Tbg_*_stanZ files are big-endian images with internal * N64 bg/*.seg and Tbg_*_stanZ files are big-endian images with internal
+1 -1
View File
@@ -7,7 +7,7 @@
extern "C" { extern "C" {
#endif #endif
/* D50 (docs/PCPortResearch.md): PC-layout model sidecars — Plan B (D48/D49). /* D50 (docs/internals.md): PC-layout model sidecars — Plan B (D48/D49).
* *
* N64 ROM model files are 32-bit-pointer big-endian images the PC build * N64 ROM model files are 32-bit-pointer big-endian images the PC build
* cannot read in place (D37/D43). tools_pc/d43_emit.py converts all of them * cannot read in place (D37/D43). tools_pc/d43_emit.py converts all of them
+1 -1
View File
@@ -5,6 +5,6 @@
* the real zlib (inflateInit2/inflate) without picking up the game's own * the real zlib (inflateInit2/inflate) without picking up the game's own
* src/game/zlib.h, which sits on the -I search path and shadows a plain * src/game/zlib.h, which sits on the -I search path and shadows a plain
* `#include <zlib.h>` in a port TU. Same absolute-path trick as hoststring.h. * `#include <zlib.h>` in a port TU. Same absolute-path trick as hoststring.h.
* See docs/PCPortResearch.md §F (D32). */ * See docs/internals.md §F (D32). */
#include "@PORT_REAL_ZLIB_H@" #include "@PORT_REAL_ZLIB_H@"
#endif #endif
+6 -6
View File
@@ -37,7 +37,7 @@ u32 romdataGetRomSize(void);
int romdataCartAddrValid(u32 addr, u32 size); int romdataCartAddrValid(u32 addr, u32 size);
/* /*
* D33 (docs/PCPortResearch.md): the .z64 file stores structured multi-byte * D33 (docs/internals.md): the .z64 file stores structured multi-byte
* fields in big-endian byte order (bit-packed streams are raw). Per-field * fields in big-endian byte order (bit-packed streams are raw). Per-field
* BE->LE transform of the animation data segment after romCopy() and before * BE->LE transform of the animation data segment after romCopy() and before
* expand_ani_table_entries(): u32 -> bswap32, u16 -> bswap16, u8 -> identity. * expand_ani_table_entries(): u32 -> bswap32, u16 -> bswap16, u8 -> identity.
@@ -52,7 +52,7 @@ void romdataFixupAnimationData(u8 *blob, u32 blobSize,
const s32 *tableA, const s32 *tableB); const s32 *tableA, const s32 *tableB);
/* /*
* D35 (docs/PCPortResearch.md): the music sequence table segment ( RareALSeqBankFile ) * D35 (docs/internals.md): the music sequence table segment ( RareALSeqBankFile )
* stores its fields big-endian: +0 u16 seqCount, then 8-byte entries at +4: * stores its fields big-endian: +0 u16 seqCount, then 8-byte entries at +4:
* u32 address offset, u16 uncompressed_len, u16 len. Per-field BE->LE * u32 address offset, u16 uncompressed_len, u16 len. Per-field BE->LE
* transform after romCopy(). Entries are decoded only as far as blobSize * transform after romCopy(). Entries are decoded only as far as blobSize
@@ -62,21 +62,21 @@ void romdataFixupAnimationData(u8 *blob, u32 blobSize,
void romdataFixupMusicSeqTable(u8 *blob, u32 blobSize); void romdataFixupMusicSeqTable(u8 *blob, u32 blobSize);
/* /*
* D54 (docs/PCPortResearch.md): decode a decompressed compact-sequence file's * D54 (docs/internals.md): decode a decompressed compact-sequence file's
* big-endian ALCMidiHdr in place (16 x u32 trackOffset + u32 division; * big-endian ALCMidiHdr in place (16 x u32 trackOffset + u32 division;
* see romdata.c). Call after decompressdata(), before alCSeqNew(). * see romdata.c). Call after decompressdata(), before alCSeqNew().
*/ */
void romdataFixupCseq(u8 *blob); void romdataFixupCseq(u8 *blob);
/* /*
* D50 (docs/PCPortResearch.md): decode a language bank's big-endian offset * D50 (docs/internals.md): decode a language bank's big-endian offset
* table in place ([u32 offsets x N][text]; see romdata.c). blobSize is the * table in place ([u32 offsets x N][text]; see romdata.c). blobSize is the
* decompressed bank size (resource_lookup_data_array[].poolRemaining). * decompressed bank size (resource_lookup_data_array[].poolRemaining).
*/ */
void romdataFixupLangBank(u8 *blob, u32 blobSize); void romdataFixupLangBank(u8 *blob, u32 blobSize);
/* /*
* D50 (docs/PCPortResearch.md): re-lay out a font segment (struct font: * D50 (docs/internals.md): re-lay out a font segment (struct font:
* kerning[169] + chars[94] + glyph pixel data) from the N64 ROM layout to * kerning[169] + chars[94] + glyph pixel data) from the N64 ROM layout to
* the PC C layout, shifting the pixel block below the expanded char array. * the PC C layout, shifting the pixel block below the expanded char array.
* src/n64Size locate the N64 image (ROM segment); the caller allocates * src/n64Size locate the N64 image (ROM segment); the caller allocates
@@ -88,7 +88,7 @@ u32 romdataFontPcSize(const u8 *src, u32 n64Size);
void romdataFixupFont(u8 *blob, u32 n64Size); void romdataFixupFont(u8 *blob, u32 n64Size);
/* /*
* D37 (docs/PCPortResearch.md): the libaudio SFX/instrument bank segments * D37 (docs/internals.md): the libaudio SFX/instrument bank segments
* (ALBankFile trees: ALBank -> ALInstrument -> ALSound -> ALWaveTable / * (ALBankFile trees: ALBank -> ALInstrument -> ALSound -> ALWaveTable /
* ALEnvelope / ALKeyMap / ALADPCMloop / ALADPCMBook) are serialized in ROM * ALEnvelope / ALKeyMap / ALADPCMloop / ALADPCMBook) are serialized in ROM
* as big-endian scalars with 4-byte packed table-relative offsets where the * as big-endian scalars with 4-byte packed table-relative offsets where the
+1 -1
View File
@@ -1,5 +1,5 @@
/* /*
* PC port shim for include/CPPLib.h (see docs/PCPortResearch.md §11 D9). * PC port shim for include/CPPLib.h (see docs/internals.md §11 D9).
* *
* CPPLib's emptiness test pastes its argument into a macro name: * CPPLib's emptiness test pastes its argument into a macro name:
* *
+1 -1
View File
@@ -1,5 +1,5 @@
/* /*
* PC port shim for PR/R4300.h (see docs/PCPortResearch.md). * PC port shim for PR/R4300.h (see docs/internals.md).
* *
* On the N64, PHYS_TO_K0 turns a physical address into a KSEG0 virtual * On the N64, PHYS_TO_K0 turns a physical address into a KSEG0 virtual
* address by OR'ing in 0x80000000. On the PC port the game's working RAM * address by OR'ing in 0x80000000. On the PC port the game's working RAM
+1 -1
View File
@@ -1,5 +1,5 @@
/* /*
* PC port shim for PR/gbi.h (see docs/PCPortResearch.md). * PC port shim for PR/gbi.h (see docs/internals.md).
* *
* The real header (include/PR/gbi.h) already has 64-bit-ready Gwords and * The real header (include/PR/gbi.h) already has 64-bit-ready Gwords and
* gSP* macros that pack full 64-bit pointers into words.w1. However, the * gSP* macros that pack full 64-bit pointers into words.w1. However, the
+1 -1
View File
@@ -1,5 +1,5 @@
/* /*
* PC port shim for PR/os.h — DRAM address conversions (see docs/PCPortResearch.md). * PC port shim for PR/os.h — DRAM address conversions (see docs/internals.md).
* *
* The game's working RAM lives in the s32-safe DRAM view at 0x70000000 * The game's working RAM lives in the s32-safe DRAM view at 0x70000000
* ("V1"), with a byte-identical KSEG0 mirror at 0x80000000 ("V2"); see * ("V1"), with a byte-identical KSEG0 mirror at 0x80000000 ("V2"); see
+2 -2
View File
@@ -1,5 +1,5 @@
/* /*
* PC port shim for bondaicommands.h (see docs/PCPortResearch.md §11 D7). * PC port shim for bondaicommands.h (see docs/internals.md §11 D7).
* *
* The real header is src/bondaicommands.h. It was written for the N64/IDO * The real header is src/bondaicommands.h. It was written for the N64/IDO
* preprocessor, which is more lenient than GCC in two ways that break the * preprocessor, which is more lenient than GCC in two ways that break the
@@ -88,7 +88,7 @@
* marker that produces an obvious error if new game code ever uses it here. * marker that produces an obvious error if new game code ever uses it here.
*/ */
#undef SWITCH #undef SWITCH
#define SWITCH(...) _PORT_SWITCH_NOT_SUPPORTED_ON_PORT /* D7: use a #ifdef PORT hand-written equivalent in chraidata.c style; see docs/PCPortResearch.md §11 */ #define SWITCH(...) _PORT_SWITCH_NOT_SUPPORTED_ON_PORT /* D7: use a #ifdef PORT hand-written equivalent in chraidata.c style; see docs/internals.md §11 */
/* --- D11: CALL() without the double trailing comma ------------------------ /* --- D11: CALL() without the double trailing comma ------------------------
* *
+1 -1
View File
@@ -1,5 +1,5 @@
/* /*
* PC port shim for bondconstants.h (see docs/PCPortResearch.md). * PC port shim for bondconstants.h (see docs/internals.md).
* *
* The real header is src/bondconstants.h. Its BITFLAG macro is only defined * The real header is src/bondconstants.h. Its BITFLAG macro is only defined
* under `#ifdef __sgi` (the N64/IDO compiler); on other compilers it is a * under `#ifdef __sgi` (the N64/IDO compiler); on other compilers it is a
+2 -2
View File
@@ -1,5 +1,5 @@
/* /*
* PC port shim for bondtypes.h (see docs/PCPortResearch.md). * PC port shim for bondtypes.h (see docs/internals.md).
* *
* The real header is src/bondtypes.h. It includes game/chrobjdata.h at line 31, * The real header is src/bondtypes.h. It includes game/chrobjdata.h at line 31,
* but chrobjdata.h declares extern arrays of ItemModelFileRecord / * but chrobjdata.h declares extern arrays of ItemModelFileRecord /
@@ -27,7 +27,7 @@
* rejects `New_Vector()` against a 3-parameter macro ("requires 3 arguments, * rejects `New_Vector()` against a 3-parameter macro ("requires 3 arguments,
* but only 1 given"). The game code only ever calls them with 0 args (to make * but only 1 given"). The game code only ever calls them with 0 args (to make
* a zero vector/coord), so redefine to accept any arg count and expand to a * a zero vector/coord), so redefine to accept any arg count and expand to a
* zero initializer. See docs/PCPortResearch.md. */ * zero initializer. See docs/internals.md. */
#undef New_Vector #undef New_Vector
#undef New_Coord3d #undef New_Coord3d
#define New_Vector(...) {0, 0, 0} #define New_Vector(...) {0, 0, 0}
+1 -1
View File
@@ -9,7 +9,7 @@
* `#include <stdlib.h>`, which resolves to the N64 header via -I), and the * `#include <stdlib.h>`, which resolves to the N64 header via -I), and the
* lldiv_t/ldiv_t typedefs then conflict with the host's. The shim routes C++ * lldiv_t/ldiv_t typedefs then conflict with the host's. The shim routes C++
* TUs to the host header by absolute path (generated hoststdlib.h, same * TUs to the host header by absolute path (generated hoststdlib.h, same
* trick as port/shim/string.h — see docs/PCPortResearch.md §11 D12). * trick as port/shim/string.h — see docs/internals.md §11 D12).
* *
* Inert in the N64 build (port/shim is not on its include path). * Inert in the N64 build (port/shim is not on its include path).
*/ */
+1 -1
View File
@@ -1,5 +1,5 @@
/* /*
* PC port shim for include/string.h (see docs/PCPortResearch.md §11 D12). * PC port shim for include/string.h (see docs/internals.md §11 D12).
* *
* The N64 header declares: * The N64 header declares:
* *
+1 -1
View File
@@ -1,5 +1,5 @@
/* /*
* gimgfixup.c - D68 (docs/PCPortResearch.md): Globalimagetable endianness * gimgfixup.c - D68 (docs/internals.md): Globalimagetable endianness
* fixup for the PC port. See port/include/gimgfixup.h for the rationale. * fixup for the PC port. See port/include/gimgfixup.h for the rationale.
* *
* The offset tables below must stay in lockstep with the g_pc_gimg_off_* * The offset tables below must stay in lockstep with the g_pc_gimg_off_*
+1 -1
View File
@@ -14,7 +14,7 @@
* libultra.c needs. * libultra.c needs.
* *
* ------------------------------------------------------------------------ * ------------------------------------------------------------------------
* BINDING SCHEME (documented in docs/PCPortResearch.md sec F "D118") * BINDING SCHEME (documented in docs/internals.md sec F "D118")
* *
* GE's default "1.1" control style: analog stick = move/strafe, the four * GE's default "1.1" control style: analog stick = move/strafe, the four
* C-buttons = aim/turn/look (DIGITAL on N64), R = aim mode, Z = fire. * C-buttons = aim/turn/look (DIGITAL on N64), R = aim mode, Z = fire.
+1 -1
View File
@@ -9,7 +9,7 @@
* osRecvMesg() waits on a per-queue condition variable. (An earlier * osRecvMesg() waits on a per-queue condition variable. (An earlier
* setjmp/longjmp green-thread kernel corrupted FPU state across longjmps * setjmp/longjmp green-thread kernel corrupted FPU state across longjmps
* on Windows x64/MinGW — synthetic STATUS_FLOATING_POINT_INVALID_OPERATION * on Windows x64/MinGW — synthetic STATUS_FLOATING_POINT_INVALID_OPERATION
* crashes at PC=0; see docs/PCPortResearch.md.) * crashes at PC=0; see docs/internals.md.)
* - RSP tasks (osSpTaskStartGo) run the software RSP (fast3d) inline and * - RSP tasks (osSpTaskStartGo) run the software RSP (fast3d) inline and
* post the RSP_DONE / RDP_DONE messages sched.c waits for. * post the RSP_DONE / RDP_DONE messages sched.c waits for.
* - VI retrace is a vsync-paced tick posted by a dedicated kernel thread, * - VI retrace is a vsync-paced tick posted by a dedicated kernel thread,
+1 -1
View File
@@ -9,7 +9,7 @@
* 4. start the thread kernel and run the game's mainproc() as a real OS * 4. start the thread kernel and run the game's mainproc() as a real OS
* thread (it IS the N64 mainThread) — which runs bossEntry(), the real * thread (it IS the N64 mainThread) — which runs bossEntry(), the real
* game loop. The game's own scheduler (src/sched.c) drives frames; see * game loop. The game's own scheduler (src/sched.c) drives frames; see
* docs/PCPortResearch.md. * docs/internals.md.
* 5. the host main thread then owns SDL event pumping for the lifetime of * 5. the host main thread then owns SDL event pumping for the lifetime of
* the process (Windows only dispatches window messages to the creating * the process (Windows only dispatches window messages to the creating
* thread, and every game thread can be blocked on a queue). * thread, and every game thread can be blocked on a queue).
+1 -1
View File
@@ -1,7 +1,7 @@
/* /*
* N64-boot-specific symbol stubs. * N64-boot-specific symbol stubs.
* *
* We compile src/init.c (for mainproc() — see A3 in docs/PCPortResearch.md), * We compile src/init.c (for mainproc() — see A3 in docs/internals.md),
* but its N64-only init() is never called on the PC. The compiler still needs * but its N64-only init() is never called on the PC. The compiler still needs
* the symbols init() references to resolve at link time, so this file provides * the symbols init() references to resolve at link time, so this file provides
* inert stubs for the ones that are NOT part of the libultra OS API (those * inert stubs for the ones that are NOT part of the libultra OS API (those
+1 -1
View File
@@ -1,5 +1,5 @@
/* /*
* pc_netorder.c - D38: host byte-order helpers (see docs/PCPortResearch.md). * pc_netorder.c - D38: host byte-order helpers (see docs/internals.md).
* *
* src/bondconstants.h implements ntohl()/ntohs() as function-like macros over * src/bondconstants.h implements ntohl()/ntohs() as function-like macros over
* CharArrayTo16/32 ("rewrite these to use char array as system provided funcs * CharArrayTo16/32 ("rewrite these to use char array as system provided funcs
+1 -1
View File
@@ -93,7 +93,7 @@ uint32_t pccgReserveSize(const uint8_t *romImg)
if (!pccgFindDataFile(rel, s_binPath, sizeof(s_binPath))) { if (!pccgFindDataFile(rel, s_binPath, sizeof(s_binPath))) {
sysLogPrintf(LOG_WARNING, sysLogPrintf(LOG_WARNING,
"pccg: %s not found -- stage loads will fault; run " "pccg: %s not found -- stage loads will fault; run "
"\"python tools_pc/d69_emit.py\" (see docs/HANDOFF.md)", "\"python tools_pc/d69_emit.py\" (see docs/building.md)",
rel); rel);
return 0; return 0;
} }
+1 -1
View File
@@ -101,7 +101,7 @@ uint32_t pcmodelsReserveSize(const uint8_t *romImg)
if (!pcmodelsFindDataFile(rel, s_binPath, sizeof(s_binPath))) { if (!pcmodelsFindDataFile(rel, s_binPath, sizeof(s_binPath))) {
sysLogPrintf(LOG_WARNING, sysLogPrintf(LOG_WARNING,
"pcmodels: %s not found — model loads will fail; run " "pcmodels: %s not found — model loads will fail; run "
"\"python tools_pc/d43_emit.py\" (see docs/HANDOFF.md)", "\"python tools_pc/d43_emit.py\" (see docs/building.md)",
rel); rel);
return 0; return 0;
} }
+8 -8
View File
@@ -51,7 +51,7 @@ static u8 *rom = NULL; /* heap copy (fallback path) */
static u32 romSize = 0; static u32 romSize = 0;
static int mappedAtCartBase = 0; static int mappedAtCartBase = 0;
/* D34 (docs/PCPortResearch.md): PE image base, low 32 bits zero. On N64 the /* D34 (docs/internals.md): PE image base, low 32 bits zero. On N64 the
* animation_data segment links at VMA 0, so game code computes record * animation_data segment links at VMA 0, so game code computes record
* addresses as `(s32)&ANIM_DATA_x + ptr_animation_table` where &ANIM_DATA_x * addresses as `(s32)&ANIM_DATA_x + ptr_animation_table` where &ANIM_DATA_x
* is a small segment offset. A PE image cannot be placed at a low address, * is a small segment offset. A PE image cannot be placed at a low address,
@@ -184,7 +184,7 @@ int romdataInit(void)
pcmodelsLoadSidecars(CART_BASE, romSize); pcmodelsLoadSidecars(CART_BASE, romSize);
pccgLoadSidecars(CART_BASE + romSize + pcmodelsTotalSize()); pccgLoadSidecars(CART_BASE + romSize + pcmodelsTotalSize());
/* D55 (docs/PCPortResearch.md): the RLE folder-menu background at /* D55 (docs/internals.md): the RLE folder-menu background at
* `unknown2` (romassets_<r>.s) is stored in the ROM with a * `unknown2` (romassets_<r>.s) is stored in the ROM with a
* big-endian w/h header (01 B8 01 2B = 440x299), but * big-endian w/h header (01 B8 01 2B = 440x299), but
* rle_expand_8bit() reads it little-endian. The N64 build embeds * rle_expand_8bit() reads it little-endian. The N64 build embeds
@@ -269,7 +269,7 @@ const void *romdataMapVa(u32 va)
} }
/* /*
* D33 (docs/PCPortResearch.md): the .z64 file stores structured multi-byte * D33 (docs/internals.md): the .z64 file stores structured multi-byte
* fields in big-endian byte order; bit-packed data streams are raw. No single * fields in big-endian byte order; bit-packed data streams are raw. No single
* uniform word transform exists (mixed field widths), so the animation_data * uniform word transform exists (mixed field widths), so the animation_data
* segment is converted per field at load time, after romCopy() and before * segment is converted per field at load time, after romCopy() and before
@@ -356,7 +356,7 @@ void romdataFixupAnimationData(u8 *blob, u32 blobSize,
} }
/* /*
* D35 (docs/PCPortResearch.md): the music sequence table segment stores its * D35 (docs/internals.md): the music sequence table segment stores its
* fields big-endian: * fields big-endian:
* *
* header at blob+0 (RareALSeqBankFile): * header at blob+0 (RareALSeqBankFile):
@@ -399,7 +399,7 @@ void romdataFixupMusicSeqTable(u8 *blob, u32 blobSize)
} }
/* /*
* D54 (docs/PCPortResearch.md): a decompressed compact-sequence (cseq) file * D54 (docs/internals.md): a decompressed compact-sequence (cseq) file
* starts with struct ALCMidiHdr — 16 big-endian u32 trackOffset values plus a * starts with struct ALCMidiHdr — 16 big-endian u32 trackOffset values plus a
* big-endian u32 division. N64 reads them natively; on a little-endian host * big-endian u32 division. N64 reads them natively; on a little-endian host
* alCSeqNew() byte-swaps them (track 0 offset 0x44 becomes 0x44000000), * alCSeqNew() byte-swaps them (track 0 offset 0x44 becomes 0x44000000),
@@ -416,7 +416,7 @@ void romdataFixupCseq(u8 *blob)
} }
/* /*
* D50 (docs/PCPortResearch.md): a language bank is [u32 offsets x N][text]. * D50 (docs/internals.md): a language bank is [u32 offsets x N][text].
* Offsets are big-endian and point into the text region; langGet() reads them * Offsets are big-endian and point into the text region; langGet() reads them
* little-endian on PC and would build garbage pointers. Scan from the front * little-endian on PC and would build garbage pointers. Scan from the front
* and bswap word i while bswap32(word_i) is a plausible offset (strictly past * and bswap word i while bswap32(word_i) is a plausible offset (strictly past
@@ -438,7 +438,7 @@ void romdataFixupLangBank(u8 *blob, u32 blobSize)
} }
/* /*
* D50 (docs/PCPortResearch.md): font segments (Zurich Bold, Bank Gothic) * D50 (docs/internals.md): font segments (Zurich Bold, Bank Gothic)
* serialize `struct font` in the N64 layout — big-endian scalars, 4-byte * serialize `struct font` in the N64 layout — big-endian scalars, 4-byte
* pointers: * pointers:
* [s32 kerning x 169][fontchar x 94 @ stride 24][glyph pixel data] * [s32 kerning x 169][fontchar x 94 @ stride 24][glyph pixel data]
@@ -524,7 +524,7 @@ void romdataFixupFont(u8 *blob, u32 n64Size)
} }
/* /*
* D37 (docs/PCPortResearch.md): convert a ROM-serialized libaudio bank tree * D37 (docs/internals.md): convert a ROM-serialized libaudio bank tree
* (ALBankFile -> ALBank -> ALInstrument -> ALSound -> ALWaveTable / * (ALBankFile -> ALBank -> ALInstrument -> ALSound -> ALWaveTable /
* ALEnvelope / ALKeyMap / ALADPCMloop / ALRawLoop / ALADPCMBook) to * ALEnvelope / ALKeyMap / ALADPCMloop / ALRawLoop / ALADPCMBook) to
* PC-native form. See the header for the contract. * PC-native form. See the header for the contract.
+1 -1
View File
@@ -1,6 +1,6 @@
/* /*
* PC-port replacement for src/game/decompress.c + src/game/zlib.c, which are * PC-port replacement for src/game/decompress.c + src/game/zlib.c, which are
* EXCLUDED from the build (see CMakeLists.txt and docs/PCPortResearch.md §F D32). * EXCLUDED from the build (see CMakeLists.txt and docs/internals.md §F D32).
* *
* GE's RZ files are: a 2-byte header (0x11 0x72) followed by a raw deflate * GE's RZ files are: a 2-byte header (0x11 0x72) followed by a raw deflate
* stream that ends with an end-of-block marker. The original decompressdata() * stream that ends with an end-of-block marker. The original decompressdata()
+1 -1
View File
@@ -277,7 +277,7 @@ void videoEndFrame(void)
} }
gfx_end_frame(); gfx_end_frame();
/* TEMP D70 (env-gated, strip per HANDOFF Task 3): GE_PCDUMP="first-last" /* TEMP D70 (env-gated, debug-only, env-gated): GE_PCDUMP="first-last"
* or "first-last:step" dumps the presented frame as ./ppm/frame_NNNNNN.ppm * or "first-last:step" dumps the presented frame as ./ppm/frame_NNNNNN.ppm
* (one frame behind real time — read happens after SwapWindow). Used to * (one frame behind real time — read happens after SwapWindow). Used to
* pixel-verify the intro's 3D content. Also honours [Debug] FrameDump in * pixel-verify the intro's 3D content. Also honours [Debug] FrameDump in
-32
View File
@@ -1,32 +0,0 @@
# Goldeneye 007
[![NTSC-Status][NTCS-badge]][NTCS-link]
[![JP-Status][JP-badge]][JP-link]
[![PAL-Status][PAL-badge]][PAL-link]
[NTCS-link]: https://kholdfuzion.github.io/goldeneyestatus/
[NTCS-badge]: ../../workflows/NTSC-Status/badge.svg
[JP-link]: https://kholdfuzion.github.io/goldeneyestatus/JPN.htm
[JP-badge]: ../../workflows/JP-Status/badge.svg
[PAL-link]: https://kholdfuzion.github.io/goldeneyestatus/EU.htm
[PAL-badge]: ../../workflows/EU-Status/badge.svg
## About this repository
This is a WIP decompilation of Goldeneye 007!
It builds the following ROMs:
* ge007.u.z64 `sha1: abe01e4aeb033b6c0836819f549c791b26cfde83`
* ge007.j.z64 `sha1: 2a5dade32f7fad6c73c659d2026994632c1b3174`
* ge007.e.z64 `sha1: 167c3c433dec1f1eb921736f7d53fac8cb45ee31`
**Note: This repository does not include all assets necessary for compiling the ROMs. A prior copy of the game is required to extract the assets.**
## Documentation
* [Setup Guide:](./docs/SetupGuide.md) useful information about installing the necessary dependencies and how to use it
* [Structure Guide:](./docs/StructureGuide.md) learn more about the project structure of this repository
* [Style Guide:](./docs/StyleGuide.md) code style conventions if you want to contribute code
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""One-shot D38 generator for port/include/pc_protos.h (see docs/PCPortResearch.md §F/D38). """One-shot D38 generator for port/include/pc_protos.h (see docs/internals.md §F/D38).
Pipeline (all inputs were captured from a PC build of the decomp): Pipeline (all inputs were captured from a PC build of the decomp):
1. Compile the game TUs and collect `implicit function declaration` warnings 1. Compile the game TUs and collect `implicit function declaration` warnings
@@ -50,7 +50,7 @@ for n in sorted(names):
hdr = '''/* hdr = '''/*
* pc_protos.h - PC-port-only prototypes for implicitly declared functions. * pc_protos.h - PC-port-only prototypes for implicitly declared functions.
* *
* D38 (docs/PCPortResearch.md section F): the decomp has ~72 translation units * D38 (docs/internals.md section F): the decomp has ~72 translation units
* that call ~400 functions without any visible prototype (missing #include of * that call ~400 functions without any visible prototype (missing #include of
* the declaring header). Under C11 an implicit declaration assumes `int f()`, * the declaring header). Under C11 an implicit declaration assumes `int f()`,
* which on N64 (MIPS, 32-bit pointers) is harmless, but on x86-64 it silently * which on N64 (MIPS, 32-bit pointers) is harmless, but on x86-64 it silently
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff. Show more