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.
No files matched your search
@@ -0,0 +1,6 @@
|
||||
* text=auto eol=lf
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.z64 binary
|
||||
*.exe binary
|
||||
*.bin binary
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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
|
||||
@@ -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."
|
||||
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -5,7 +5,7 @@
|
||||
# game C sources (src/game, src) for a modern host (Windows/Linux/macOS)
|
||||
# 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).
|
||||
#
|
||||
# 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
|
||||
# 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
|
||||
# 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.
|
||||
if(NOT DEFINED CMAKE_C_COMPILER)
|
||||
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
|
||||
# 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
|
||||
# (WIP decomp; genuine 64-bit hazards to fix upstream as phases progress)
|
||||
# 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 only externally-referenced symbols from these two files are
|
||||
# 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
|
||||
"${CMAKE_SOURCE_DIR}/src/game/decompress.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
|
||||
# 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
|
||||
# ROM-derived and absent from this checkout. See docs/PCPortResearch.md §11.
|
||||
# D34 (docs/PCPortResearch.md): animationtable_{data,entries}.c are NOT
|
||||
# ROM-derived and absent from this checkout. See docs/internals.md §11.
|
||||
# D34 (docs/internals.md): animationtable_{data,entries}.c are NOT
|
||||
# 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
|
||||
# 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
|
||||
# setup helpers. Its N64-only init() is compiled but NOT called;
|
||||
# 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
|
||||
# code (bondview2.c, front.c). Pure data.
|
||||
# stacks.c — thread-stack arrays (sp_boot/sp_rmon/sp_idle/sp_shed/sp_main/
|
||||
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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 — Archives and Bunker 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.
|
||||
@@ -1,21 +1,21 @@
|
||||
# Font Asset Readme
|
||||
|
||||
Font information.
|
||||
|
||||
* first font table controller - small watch text
|
||||
* second font table controller - small watch text
|
||||
* first font table controller - large
|
||||
* second font table controller - large
|
||||
|
||||
|
||||
|
||||
### second controller format
|
||||
Size of 0x18 for each entry
|
||||
|
||||
entry layout:
|
||||
* 0x0:
|
||||
* 0x4: vertical offset
|
||||
* 0x8: height
|
||||
* 0xC: width
|
||||
* 0x10:
|
||||
* 0x14: offset to image (converted to pointer at runtime)
|
||||
# Font Asset Readme
|
||||
|
||||
Font information.
|
||||
|
||||
* first font table controller - small watch text
|
||||
* second font table controller - small watch text
|
||||
* first font table controller - large
|
||||
* second font table controller - large
|
||||
|
||||
|
||||
|
||||
### second controller format
|
||||
Size of 0x18 for each entry
|
||||
|
||||
entry layout:
|
||||
* 0x0:
|
||||
* 0x4: vertical offset
|
||||
* 0x8: height
|
||||
* 0xC: width
|
||||
* 0x10:
|
||||
* 0x14: offset to image (converted to pointer at runtime)
|
||||
@@ -1,41 +1,41 @@
|
||||
# Image Assets
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- **`split/`** - Individual texture files (2698 images)
|
||||
- Named using IMAGE enum from images.def (e.g., COPYICON.bin, X.bin)
|
||||
- Extracted from ROM using ROM offsets from imagelist.u.csv
|
||||
- Raw binary texture data
|
||||
|
||||
- **`combined/`** - Combined texture binary
|
||||
- `combined.bin` - All textures concatenated in ROM order
|
||||
- Padded to 16-byte (0x10) boundary
|
||||
- Size: 3075872 bytes (3075864 data + 8 padding)
|
||||
- SHA1 (unpadded): 044fca472bf6ef6691fa02ff1b65ff474d86a9fa
|
||||
|
||||
## Build System
|
||||
|
||||
The image build system synchronizes two sources:
|
||||
- **`imagelist.u.csv`** (root) - ROM offsets and sizes (ground truth for positioning)
|
||||
- **`assets/images.def`** - Image names and properties (ground truth for naming)
|
||||
|
||||
See `scripts/IMAGE_BUILD_SYSTEM.md` for detailed documentation.
|
||||
|
||||
## Quick Commands
|
||||
|
||||
```bash
|
||||
# Extract images from ROM
|
||||
./scripts/extract_baserom.u.sh
|
||||
|
||||
# Rebuild combined.bin
|
||||
make assets/images/combined/combined.bin
|
||||
|
||||
# Full ROM build
|
||||
make
|
||||
```
|
||||
# Image Assets
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- **`split/`** - Individual texture files (2698 images)
|
||||
- Named using IMAGE enum from images.def (e.g., COPYICON.bin, X.bin)
|
||||
- Extracted from ROM using ROM offsets from imagelist.u.csv
|
||||
- Raw binary texture data
|
||||
|
||||
- **`combined/`** - Combined texture binary
|
||||
- `combined.bin` - All textures concatenated in ROM order
|
||||
- Padded to 16-byte (0x10) boundary
|
||||
- Size: 3075872 bytes (3075864 data + 8 padding)
|
||||
- SHA1 (unpadded): 044fca472bf6ef6691fa02ff1b65ff474d86a9fa
|
||||
|
||||
## Build System
|
||||
|
||||
The image build system synchronizes two sources:
|
||||
- **`imagelist.u.csv`** (root) - ROM offsets and sizes (ground truth for positioning)
|
||||
- **`assets/images.def`** - Image names and properties (ground truth for naming)
|
||||
|
||||
See `scripts/IMAGE_BUILD_SYSTEM.md` for detailed documentation.
|
||||
|
||||
## Quick Commands
|
||||
|
||||
```bash
|
||||
# Extract images from ROM
|
||||
./scripts/extract_baserom.u.sh
|
||||
|
||||
# Rebuild combined.bin
|
||||
make assets/images/combined/combined.bin
|
||||
|
||||
# Full ROM build
|
||||
make
|
||||
```
|
||||
@@ -1,3 +1,3 @@
|
||||
# Music Asset Readme
|
||||
|
||||
Music information.
|
||||
# Music Asset Readme
|
||||
|
||||
Music information.
|
||||
@@ -1,65 +1,65 @@
|
||||
# obseg Asset Readme
|
||||
|
||||
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.
|
||||
|
||||
obseg asset categories are:
|
||||
|
||||
* `bg`
|
||||
* `brief`
|
||||
* `chr`
|
||||
* `gun`
|
||||
* `prop`
|
||||
* `setup`
|
||||
* `stan`
|
||||
* `text`
|
||||
|
||||
## `bg` Assets
|
||||
Level background geometry display lists and portal info.
|
||||
|
||||
## `brief` Assets
|
||||
Maps text strings into mission briefings.
|
||||
|
||||
## `chr` Assets
|
||||
Character models display lists.
|
||||
|
||||
## `gun` Assets
|
||||
On screen gun models display lists.
|
||||
|
||||
## `prop` Assets
|
||||
Prop and item models display lists.
|
||||
|
||||
## `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)
|
||||
|
||||
## `stan` Assets
|
||||
Stand Tiles. Refereed to by Rare Staff as "stans" in GE or simply "Tiles" in PD. (see below for descriptions)
|
||||
|
||||
## `text` Assets
|
||||
All game text.
|
||||
|
||||
## 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.
|
||||
|
||||
-----
|
||||
|
||||
# Concepts
|
||||
|
||||
# Room
|
||||
A level is divided into rooms.
|
||||
|
||||
# Portal
|
||||
Portals allow Bond to see through them to connected rooms that are otherwise culled to save unnecessary processing.
|
||||
|
||||
# Pad
|
||||
A room contains one or more pads.
|
||||
|
||||
# Path
|
||||
Guard pathing information between pads.
|
||||
|
||||
# Nav mesh
|
||||
Also known as "sets". Colleciton of pads, to aid in the AI algorithm to find the shortest path to Bond.
|
||||
|
||||
# Stan or Tiles
|
||||
Defines floor information.
|
||||
# obseg Asset Readme
|
||||
|
||||
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.
|
||||
|
||||
obseg asset categories are:
|
||||
|
||||
* `bg`
|
||||
* `brief`
|
||||
* `chr`
|
||||
* `gun`
|
||||
* `prop`
|
||||
* `setup`
|
||||
* `stan`
|
||||
* `text`
|
||||
|
||||
## `bg` Assets
|
||||
Level background geometry display lists and portal info.
|
||||
|
||||
## `brief` Assets
|
||||
Maps text strings into mission briefings.
|
||||
|
||||
## `chr` Assets
|
||||
Character models display lists.
|
||||
|
||||
## `gun` Assets
|
||||
On screen gun models display lists.
|
||||
|
||||
## `prop` Assets
|
||||
Prop and item models display lists.
|
||||
|
||||
## `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)
|
||||
|
||||
## `stan` Assets
|
||||
Stand Tiles. Refereed to by Rare Staff as "stans" in GE or simply "Tiles" in PD. (see below for descriptions)
|
||||
|
||||
## `text` Assets
|
||||
All game text.
|
||||
|
||||
## 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.
|
||||
|
||||
-----
|
||||
|
||||
# Concepts
|
||||
|
||||
# Room
|
||||
A level is divided into rooms.
|
||||
|
||||
# Portal
|
||||
Portals allow Bond to see through them to connected rooms that are otherwise culled to save unnecessary processing.
|
||||
|
||||
# Pad
|
||||
A room contains one or more pads.
|
||||
|
||||
# Path
|
||||
Guard pathing information between pads.
|
||||
|
||||
# Nav mesh
|
||||
Also known as "sets". Colleciton of pads, to aid in the AI algorithm to find the shortest path to Bond.
|
||||
|
||||
# Stan or Tiles
|
||||
Defines floor information.
|
||||
@@ -1,128 +1,128 @@
|
||||
# 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.
|
||||
|
||||
Values are stored most significant byte first.
|
||||
|
||||
## 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.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
## Header
|
||||
|
||||
The header consists of a data structure of 10 pointers in the following order:
|
||||
|
||||
- path table section
|
||||
- path link section (path list section)
|
||||
- intro block
|
||||
- object / propdef / objective section
|
||||
- path sets section
|
||||
- AI List section
|
||||
- pad list section
|
||||
- pad3d list section
|
||||
- pad names section
|
||||
- pad3d names section
|
||||
|
||||
## Pad List Section
|
||||
|
||||
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 }
|
||||
|
||||
## Pad3d List Section
|
||||
|
||||
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} }
|
||||
|
||||
## Object / Objective Section
|
||||
|
||||
Each entry is specified by type `enum PROPDEF_TYPE`. This section lists
|
||||
|
||||
- guards (and hats)
|
||||
- props (doors, vehicle, tank, monitor, etc)
|
||||
- collectibles (body armor, ammo, weapons, items in safe, etc)
|
||||
- objectives
|
||||
- autogun / drones
|
||||
- CCTV / surveilance cameras
|
||||
- keys, locked doors, and locks
|
||||
- glass
|
||||
- watch menu text
|
||||
- poison gas (facility)
|
||||
- meta items (rename, link, switch)
|
||||
- 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`.
|
||||
|
||||
## Intro Section
|
||||
|
||||
Each entry is specified by type `enum INTRO_TYPE`. This section describes
|
||||
|
||||
- Spawn location
|
||||
- Starting inventory and ammo amounts
|
||||
- Opening cinemas and camera locations
|
||||
- Bond's outfit (cuffs)
|
||||
- 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`.
|
||||
|
||||
### Intro: Animation
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
The credits block ends with an empty entry
|
||||
|
||||
{ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000 }
|
||||
|
||||
## 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
|
||||
|
||||
{ 0xffff, 0xffff, NULL, 0x00000000, 0x00000000 }
|
||||
|
||||
## 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
|
||||
|
||||
{ NULL, NULL, 0 }
|
||||
|
||||
## 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
|
||||
|
||||
{ NULL, 0x00000000 }
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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
|
||||
|
||||
{ NULL, 0x00000000 }
|
||||
|
||||
## 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).
|
||||
|
||||
## .rodata
|
||||
|
||||
# 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.
|
||||
|
||||
Values are stored most significant byte first.
|
||||
|
||||
## 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.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
## Header
|
||||
|
||||
The header consists of a data structure of 10 pointers in the following order:
|
||||
|
||||
- path table section
|
||||
- path link section (path list section)
|
||||
- intro block
|
||||
- object / propdef / objective section
|
||||
- path sets section
|
||||
- AI List section
|
||||
- pad list section
|
||||
- pad3d list section
|
||||
- pad names section
|
||||
- pad3d names section
|
||||
|
||||
## Pad List Section
|
||||
|
||||
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 }
|
||||
|
||||
## Pad3d List Section
|
||||
|
||||
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} }
|
||||
|
||||
## Object / Objective Section
|
||||
|
||||
Each entry is specified by type `enum PROPDEF_TYPE`. This section lists
|
||||
|
||||
- guards (and hats)
|
||||
- props (doors, vehicle, tank, monitor, etc)
|
||||
- collectibles (body armor, ammo, weapons, items in safe, etc)
|
||||
- objectives
|
||||
- autogun / drones
|
||||
- CCTV / surveilance cameras
|
||||
- keys, locked doors, and locks
|
||||
- glass
|
||||
- watch menu text
|
||||
- poison gas (facility)
|
||||
- meta items (rename, link, switch)
|
||||
- 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`.
|
||||
|
||||
## Intro Section
|
||||
|
||||
Each entry is specified by type `enum INTRO_TYPE`. This section describes
|
||||
|
||||
- Spawn location
|
||||
- Starting inventory and ammo amounts
|
||||
- Opening cinemas and camera locations
|
||||
- Bond's outfit (cuffs)
|
||||
- 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`.
|
||||
|
||||
### Intro: Animation
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
The credits block ends with an empty entry
|
||||
|
||||
{ 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000 }
|
||||
|
||||
## 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
|
||||
|
||||
{ 0xffff, 0xffff, NULL, 0x00000000, 0x00000000 }
|
||||
|
||||
## 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
|
||||
|
||||
{ NULL, NULL, 0 }
|
||||
|
||||
## 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
|
||||
|
||||
{ NULL, 0x00000000 }
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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
|
||||
|
||||
{ NULL, 0x00000000 }
|
||||
|
||||
## 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).
|
||||
|
||||
## .rodata
|
||||
|
||||
The .bin file will end with the .rodata section consisting of the strings used in padlist, pad3dlist, padnames, and pad3dnames.
|
||||
@@ -1,82 +1,82 @@
|
||||
# Stand Tile Readme
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## 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 is the same for both release and debug.
|
||||
|
||||
## Tiles
|
||||
|
||||
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 4: room number
|
||||
byte 5 (upper 4 bits): flags or attributes.
|
||||
byte 5 (lower 4 bits): Red
|
||||
byte 6 (upper 4 bits): Green
|
||||
byte 6 (lower 4 bits): Blue
|
||||
byte 7 (upper 4 bits): number of 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 8 (upper 4 bits): second point
|
||||
byte 8 (lower 4 bits): third point
|
||||
|
||||
### 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.
|
||||
|
||||
The remaining field is the "link"
|
||||
|
||||
## Beta Tiles
|
||||
|
||||
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 5 (upper 4 bits): flags or attributes. (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 (lower 4 bits): Blue (same as regular tile)
|
||||
byte 7-8: unknown 16-bit field. This may be the room.
|
||||
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)
|
||||
|
||||
byte 10: first point
|
||||
byte 11: second point
|
||||
byte 12: third point
|
||||
|
||||
### Beta Tile Points
|
||||
|
||||
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 "link" is now a 32-bit int.
|
||||
|
||||
## 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).
|
||||
|
||||
"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
|
||||
|
||||
Possible values for tile "flags" or "attributes":
|
||||
|
||||
0 = Normal
|
||||
1 = Kneeling
|
||||
3 = Ladder
|
||||
|
||||
## 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).
|
||||
# Stand Tile Readme
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## 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 is the same for both release and debug.
|
||||
|
||||
## Tiles
|
||||
|
||||
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 4: room number
|
||||
byte 5 (upper 4 bits): flags or attributes.
|
||||
byte 5 (lower 4 bits): Red
|
||||
byte 6 (upper 4 bits): Green
|
||||
byte 6 (lower 4 bits): Blue
|
||||
byte 7 (upper 4 bits): number of 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 8 (upper 4 bits): second point
|
||||
byte 8 (lower 4 bits): third point
|
||||
|
||||
### 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.
|
||||
|
||||
The remaining field is the "link"
|
||||
|
||||
## Beta Tiles
|
||||
|
||||
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 5 (upper 4 bits): flags or attributes. (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 (lower 4 bits): Blue (same as regular tile)
|
||||
byte 7-8: unknown 16-bit field. This may be the room.
|
||||
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)
|
||||
|
||||
byte 10: first point
|
||||
byte 11: second point
|
||||
byte 12: third point
|
||||
|
||||
### Beta Tile Points
|
||||
|
||||
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 "link" is now a 32-bit int.
|
||||
|
||||
## 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).
|
||||
|
||||
"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
|
||||
|
||||
Possible values for tile "flags" or "attributes":
|
||||
|
||||
0 = Normal
|
||||
1 = Kneeling
|
||||
3 = Ladder
|
||||
|
||||
## 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).
|
||||
@@ -1,3 +1,3 @@
|
||||
# ramrom Asset Readme
|
||||
|
||||
Contains information for a pre-recorded demo.
|
||||
# ramrom Asset Readme
|
||||
|
||||
Contains information for a pre-recorded demo.
|
||||
@@ -1,30 +1,30 @@
|
||||
# Asset Readme
|
||||
|
||||
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.
|
||||
|
||||
Asset categories are:
|
||||
|
||||
* `font`
|
||||
* `images`
|
||||
* `music`
|
||||
* `obseg`
|
||||
* `ramrom`
|
||||
|
||||
## `font` Assets
|
||||
This contains font information for rendering text.
|
||||
|
||||
## `images` Assets
|
||||
Contains ALL textures used in game (UI, Objects and BG)
|
||||
|
||||
## `music` Assets
|
||||
|
||||
## `obseg` Assets
|
||||
Contains level information, game models, and all text used in the game.
|
||||
|
||||
## `ramrom` Assets
|
||||
This contains demo information the game plays if no folder is selected from the first screen.
|
||||
|
||||
## 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.
|
||||
# Asset Readme
|
||||
|
||||
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.
|
||||
|
||||
Asset categories are:
|
||||
|
||||
* `font`
|
||||
* `images`
|
||||
* `music`
|
||||
* `obseg`
|
||||
* `ramrom`
|
||||
|
||||
## `font` Assets
|
||||
This contains font information for rendering text.
|
||||
|
||||
## `images` Assets
|
||||
Contains ALL textures used in game (UI, Objects and BG)
|
||||
|
||||
## `music` Assets
|
||||
|
||||
## `obseg` Assets
|
||||
Contains level information, game models, and all text used in the game.
|
||||
|
||||
## `ramrom` Assets
|
||||
This contains demo information the game plays if no folder is selected from the first screen.
|
||||
|
||||
## 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.
|
||||
@@ -5,7 +5,7 @@
|
||||
# Usage:
|
||||
# ./build-pc.sh [ntsc-final|pal-final|jpn-final]
|
||||
#
|
||||
# Dependencies (see docs/PCPortResearch.md):
|
||||
# Dependencies (see docs/internals.md):
|
||||
# - CMake >= 3.16
|
||||
# - SDL2 dev
|
||||
# - zlib dev
|
||||
|
||||
@@ -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.
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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).
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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)
|
||||
|
||||
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`,
|
||||
`docs/PORT-LEARNINGS.md`.
|
||||
`docs/porting-notes.md`.
|
||||
|
||||
---
|
||||
|
||||
@@ -88,7 +88,7 @@ problem.
|
||||
| `aEnvMixer` | env.c:410/414 | yes — `aEnvMixerImpl` |
|
||||
| `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,
|
||||
resample table + interpolation, envelope ramping, clamps). **Not** reusable as-is:
|
||||
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
|
||||
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
|
||||
class — see `docs/PORT-LEARNINGS.md`).
|
||||
class — see `docs/porting-notes.md`).
|
||||
- **Phase 3 — FX.** `aPoleFilter` + validate the full `reverb.c` chorus path against
|
||||
the disassembly. **Milestone: music matches N64 character (chorus/reverb present, no
|
||||
pumping); close D77.**
|
||||
- **Phase 4 — Polish & bookkeeping.** Resolve Q1; remove/adjust temporary probes and
|
||||
`GE_DAUDIO` defaults; latency/buffer tuning (`Audio.BufferSize`, `QueueLimit`);
|
||||
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
|
||||
to `docs/PORT-LEARNINGS.md`.
|
||||
to `docs/porting-notes.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
|
||||
|
||||
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
|
||||
`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
|
||||
accessors. MED = D102 weapon `render_pos` transient-arena aliasing (likely
|
||||
the ‘weapon model doesn’t draw’ cause) and the D56 watch-Model pool
|
||||
@@ -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). |
|
||||
| **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. |
|
||||
| **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. |
|
||||
@@ -1,7 +1,7 @@
|
||||
# HANDOFF ARCHIVE — session-by-session narrative
|
||||
|
||||
> **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
|
||||
> prior sessions' handoff briefs (through session M-11), kept so the
|
||||
> reasoning behind a fix is recoverable. Everything below is as-written
|
||||
@@ -16,12 +16,12 @@
|
||||
|
||||
1. `CLAUDE.md` (auto-loaded) + `AGENTS.md` non-negotiables.
|
||||
2. This file, top-to-bottom.
|
||||
3. `docs/PORT-LEARNINGS.md` — recurring bug classes.
|
||||
4. Dispatching a subagent? `docs/AGENT-WORKFLOW.md` FIRST. Every brief needs
|
||||
3. `docs/porting-notes.md` — recurring bug classes.
|
||||
4. Dispatching a subagent? `docs/dev-process.md` FIRST. Every brief needs
|
||||
FILES / BUDGET / ON-EXPIRY / CONSTRAINTS / REPORT (see CLAUDE.md "Dispatch preflight").
|
||||
|
||||
_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
|
||||
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.
|
||||
4. **Rest of the `struct player` offset pass** — D115 fixed the HIGH
|
||||
`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
|
||||
checks pointers/opcodes today — the D112 bug passed "ALL CHECKS PASSED").
|
||||
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).
|
||||
|
||||
**Session M-6 — see §F D113/D114/D115, `docs/PLAN-M6-playable.md`,
|
||||
`docs/AUDIT-M6-player-offsets.md`, `docs/AGENT-WORKFLOW.md`,
|
||||
`docs/PORT-LEARNINGS.md`.** D113: portal BFS is correct, not the void.
|
||||
`docs/dev/AUDIT-M6-player-offsets.md`, `docs/dev-process.md`,
|
||||
`docs/porting-notes.md`.** D113: portal BFS is correct, not the void.
|
||||
D114: matrix chain + converter verified clean, residual = shared fast3d
|
||||
mirror (open). D115: player raw-offset audit + `gunfire.c` THROW* fix
|
||||
(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
|
||||
hypothesis is D75(b) — the animated-model path (`animInit` + raw offsets
|
||||
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**
|
||||
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
|
||||
`ps -p <bashpid>`) works well and is fast for "watch a global for a
|
||||
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.
|
||||
|
||||
## Non-negotiables (unchanged, see AGENTS.md)
|
||||
@@ -509,7 +509,7 @@ locations attract mode happens to pick).
|
||||
1. N64 build files untouched.
|
||||
2. Game logic unmodified except narrow, documented `#ifdef PORT`
|
||||
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
|
||||
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
|
||||
@@ -1,7 +1,7 @@
|
||||
# Texture Glitch Analysis — PC Port
|
||||
|
||||
**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
|
||||
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
|
||||
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
|
||||
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 |
|
||||
|------|-----------|
|
||||
@@ -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).
|
||||
@@ -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
|
||||
GoldenEye 007, based on the decompilation in this repository
|
||||
(`n64decomp/007`). It is modelled on the approach used by the
|
||||
[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.
|
||||
The raw, chronological engineering record for the port: every runtime bug,
|
||||
its root cause, and the fix. Kept unedited for transparency. Recurring
|
||||
patterns are distilled in [../porting-notes.md](../porting-notes.md).
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
|
||||
**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).
|
||||
**Fixed:** `gunfire.c:4960-4962` `THROWMTX` / `THROWPOS(k)` / `THROWPREV(k)`
|
||||
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
|
||||
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]`
|
||||
trace showing 32 slots going `abcd08xx -> 700e****`, verified render).
|
||||
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`
|
||||
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
|
||||
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
|
||||
sidecar change — **sidecars do NOT need regen**, tree builds clean at
|
||||
`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**
|
||||
(91.5% / 79.8% / 70.4%); Bunker1/Silo/Aztec/Archives/Egypt/Train
|
||||
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
|
||||
(`sndSetupSound`).
|
||||
|
||||
|
After Width: | Height: | Size: 453 KiB |
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 318 KiB |
|
After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 268 KiB |
@@ -0,0 +1,436 @@
|
||||
# GoldenEye 007 — PC Port: architecture & plan
|
||||
|
||||
> This began as the pre-implementation research note and is kept as the
|
||||
> architecture reference. Sections 1–9 describe the design; section 10 lists
|
||||
> external references. The phased plan in section 8 is largely done through
|
||||
> Phase 2 — for current status see the [README](../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
|
||||
subagent reads this first and appends any new generalisable quirk.**
|
||||
Details, evidence, and the fix for each instance live in §F under the
|
||||
cited Dxx label.
|
||||
A one-screen field guide to the bug classes that keep recurring when running
|
||||
big-endian 32-bit N64 game code, unmodified, on a little-endian 64-bit host.
|
||||
Each entry cites a `Dxx` label; the full evidence and fix for that instance
|
||||
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
|
||||
|
||||
@@ -30,7 +33,7 @@ state.
|
||||
`.act_*.attack_item` — aliased `WeaponObjRecord.weaponnum` at 0x80 on
|
||||
N64 via act-union@0x2C+84; act union moves to ~0x38 on PC).
|
||||
- **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
|
||||
record.** If a fixed-size ROM record ends with a `T *next` (or `*child`,
|
||||
`*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
|
||||
OR the asymmetric-1-texel-texture experiment — nothing else.** Cosmetic,
|
||||
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
|
||||
|
||||
@@ -492,7 +495,7 @@ past it (walk off the end).
|
||||
`tools_pc/pixcount.py` vs `docs/reference/n64-footage-*`).
|
||||
- gdb **launch** mode is too slow for timing-dependent faults; gdb
|
||||
**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.
|
||||
- Don't re-investigate a closed §F finding or re-derive a format spec
|
||||
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> <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. <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. <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. <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”. <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. <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. <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 🫠. <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>. <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>. <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/"><< Previous Post</a></p><p>|</p><p><strike>Next Post >></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="{"version":"2024.11.0","token":"10d5aece9021411ab2fd8081c3b4e020","r":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"> Click to open IoC report <br>with VirusTotal Augment</span><style></style><style class="darkreader darkreader--sync" media="screen"></style></body></html>
|
||||
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 94 KiB |
@@ -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.
|
||||
|
||||
It mirrors the layout of the [Perfect Dark PC
|
||||
port](https://github.com/perfect-dark-pc-port/perfect_dark), which ports the
|
||||
same Rare engine family. See `docs/PCPortResearch.md` (repo root) for the full
|
||||
port](https://github.com/fgsfdsfgs/perfect_dark), which ports the
|
||||
same Rare engine family. See `docs/internals.md` (repo root) for the full
|
||||
research write-up and phased plan.
|
||||
|
||||
## Layout
|
||||
@@ -66,5 +66,5 @@ exact command encodings.
|
||||
|
||||
**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
|
||||
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.
|
||||
@@ -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
|
||||
the Gfx display list the game builds and emits OpenGL calls, bypassing the N64
|
||||
RDP entirely.
|
||||
A C++ interpreter that consumes the `Gfx` display list the game builds each
|
||||
frame and emits OpenGL calls, replacing the Nintendo 64's Reality Signal
|
||||
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
|
||||
`perfect-dark-pc-port/perfect_dark/port/fast3d/`:
|
||||
This code is **adapted from the [Perfect Dark PC
|
||||
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 |
|
||||
|--------------------|-------------------------------------------------------------|--------|
|
||||
| `gfx_pc.cpp` | The RSP interpreter: matrices, lights, fog, tri1/tri4, vtx | 2800 |
|
||||
| `gfx_opengl.cpp` | OpenGL backend: shaders, textures, zbuf, framebuffers | 1300 |
|
||||
| `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 | — |
|
||||
`LICENSE.txt` in this directory (MIT, © 2020 Emill, MaikelChan) covers that
|
||||
lineage. `glad/` is generated by [glad](https://github.com/Dav1dde/glad)
|
||||
(© David Herberth, MIT). GoldenEye-specific changes in this directory are
|
||||
under the repository's top-level MIT license.
|
||||
|
||||
**The PD fast3d already handles `G_TRI4`** (GE's 4-triangle command) — the PD
|
||||
game uses it too. So the bulk of the RSP work is done; we adapt, not rewrite.
|
||||
## Files
|
||||
|
||||
## 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
|
||||
(`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.
|
||||
## GoldenEye-specific work
|
||||
|
||||
## Ground truth
|
||||
|
||||
`rsp/graphics/gmain.s` (repo root) is the real GE RSP ucode. We do **not** run
|
||||
it on PC, but it is the authoritative reference for the exact command
|
||||
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.
|
||||
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
|
||||
extension commands. `rsp/graphics/gmain.s` (GE's real RSP microcode, in the
|
||||
repo root) is the ground truth for the exact command encodings.
|
||||
@@ -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
|
||||
* (pd_port/src/include/gbiex.h + PD's include/PR/gbi.h). GoldenEye shares the
|
||||
|
||||
@@ -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
|
||||
// blobs (cart extension 0x10Cxxxxx), KSEG0 mirror (0x80xxxxxx) and V1
|
||||
// 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.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-
|
||||
* inherited block overwrote the authored tc[] UVs on every
|
||||
* lit+textured surface (lookat_enabled defaults to true here),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
#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
|
||||
* u32 in the ROM copy is BE-encoded; PC consumers read them natively (LE).
|
||||
*
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
/* Generated by CMake from hoststdlib.h.in — do not edit.
|
||||
* 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
|
||||
* 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@"
|
||||
#endif
|
||||
@@ -4,6 +4,6 @@
|
||||
* 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
|
||||
* `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@"
|
||||
#endif
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* 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
|
||||
* 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
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
extern "C" {
|
||||
#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.
|
||||
*
|
||||
* N64 bg/*.seg and Tbg_*_stanZ files are big-endian images with internal
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
extern "C" {
|
||||
#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
|
||||
* cannot read in place (D37/D43). tools_pc/d43_emit.py converts all of them
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
* 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
|
||||
* `#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@"
|
||||
#endif
|
||||
@@ -37,7 +37,7 @@ u32 romdataGetRomSize(void);
|
||||
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
|
||||
* BE->LE transform of the animation data segment after romCopy() and before
|
||||
* 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);
|
||||
|
||||
/*
|
||||
* 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:
|
||||
* u32 address offset, u16 uncompressed_len, u16 len. Per-field BE->LE
|
||||
* 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);
|
||||
|
||||
/*
|
||||
* 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;
|
||||
* see romdata.c). Call after decompressdata(), before alCSeqNew().
|
||||
*/
|
||||
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
|
||||
* decompressed bank size (resource_lookup_data_array[].poolRemaining).
|
||||
*/
|
||||
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
|
||||
* the PC C layout, shifting the pixel block below the expanded char array.
|
||||
* 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);
|
||||
|
||||
/*
|
||||
* 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 /
|
||||
* ALEnvelope / ALKeyMap / ALADPCMloop / ALADPCMBook) are serialized in ROM
|
||||
* as big-endian scalars with 4-byte packed table-relative offsets where the
|
||||
|
||||
@@ -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:
|
||||
*
|
||||
|
||||
@@ -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
|
||||
* address by OR'ing in 0x80000000. On the PC port the game's working RAM
|
||||
|
||||
@@ -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
|
||||
* gSP* macros that pack full 64-bit pointers into words.w1. However, the
|
||||
|
||||
@@ -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
|
||||
* ("V1"), with a byte-identical KSEG0 mirror at 0x80000000 ("V2"); see
|
||||
|
||||
@@ -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
|
||||
* 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.
|
||||
*/
|
||||
#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 ------------------------
|
||||
*
|
||||
|
||||
@@ -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
|
||||
* under `#ifdef __sgi` (the N64/IDO compiler); on other compilers it is a
|
||||
|
||||
@@ -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,
|
||||
* but chrobjdata.h declares extern arrays of ItemModelFileRecord /
|
||||
@@ -27,7 +27,7 @@
|
||||
* 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
|
||||
* 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_Coord3d
|
||||
#define New_Vector(...) {0, 0, 0}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* `#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++
|
||||
* 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).
|
||||
*/
|
||||
|
||||
@@ -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:
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* The offset tables below must stay in lockstep with the g_pc_gimg_off_*
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* 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
|
||||
* C-buttons = aim/turn/look (DIGITAL on N64), R = aim mode, Z = fire.
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* osRecvMesg() waits on a per-queue condition variable. (An earlier
|
||||
* setjmp/longjmp green-thread kernel corrupted FPU state across longjmps
|
||||
* 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
|
||||
* post the RSP_DONE / RDP_DONE messages sched.c waits for.
|
||||
* - VI retrace is a vsync-paced tick posted by a dedicated kernel thread,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* 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
|
||||
* 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
|
||||
* the process (Windows only dispatches window messages to the creating
|
||||
* thread, and every game thread can be blocked on a queue).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* 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
|
||||
* 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
|
||||
|
||||
@@ -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
|
||||
* CharArrayTo16/32 ("rewrite these to use char array as system provided funcs
|
||||
|
||||
@@ -93,7 +93,7 @@ uint32_t pccgReserveSize(const uint8_t *romImg)
|
||||
if (!pccgFindDataFile(rel, s_binPath, sizeof(s_binPath))) {
|
||||
sysLogPrintf(LOG_WARNING,
|
||||
"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);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ uint32_t pcmodelsReserveSize(const uint8_t *romImg)
|
||||
if (!pcmodelsFindDataFile(rel, s_binPath, sizeof(s_binPath))) {
|
||||
sysLogPrintf(LOG_WARNING,
|
||||
"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);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ static u8 *rom = NULL; /* heap copy (fallback path) */
|
||||
static u32 romSize = 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
|
||||
* 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,
|
||||
@@ -184,7 +184,7 @@ int romdataInit(void)
|
||||
pcmodelsLoadSidecars(CART_BASE, romSize);
|
||||
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
|
||||
* big-endian w/h header (01 B8 01 2B = 440x299), but
|
||||
* 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
|
||||
* uniform word transform exists (mixed field widths), so the animation_data
|
||||
* 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:
|
||||
*
|
||||
* 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
|
||||
* big-endian u32 division. N64 reads them natively; on a little-endian host
|
||||
* 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
|
||||
* 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
|
||||
@@ -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
|
||||
* pointers:
|
||||
* [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 /
|
||||
* ALEnvelope / ALKeyMap / ALADPCMloop / ALRawLoop / ALADPCMBook) to
|
||||
* PC-native form. See the header for the contract.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* stream that ends with an end-of-block marker. The original decompressdata()
|
||||
|
||||
@@ -277,7 +277,7 @@ void videoEndFrame(void)
|
||||
}
|
||||
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
|
||||
* (one frame behind real time — read happens after SwapWindow). Used to
|
||||
* pixel-verify the intro's 3D content. Also honours [Debug] FrameDump in
|
||||
|
||||
@@ -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
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/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):
|
||||
1. Compile the game TUs and collect `implicit function declaration` warnings
|
||||
@@ -50,7 +50,7 @@ for n in sorted(names):
|
||||
hdr = '''/*
|
||||
* 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
|
||||
* 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
|
||||
|
||||