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.
|
||||
@@ -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
|
||||
|
||||
@@ -362,7 +362,7 @@ def build_segments(region, csv, obseg_resolved, ramrom_files, music_tracks,
|
||||
off, size = csv1("animationtable_data.bin")
|
||||
seg["animation_data"] = (off, size, True)
|
||||
off, size = csv1("Globalimagetable.bin")
|
||||
# D39 (docs/PCPortResearch.md): the CSV asset is truncated to the 17 Gfx
|
||||
# D39 (docs/internals.md): the CSV asset is truncated to the 17 Gfx
|
||||
# display lists (0xAC8). The N64 linker segment (ge007.ld) places ALL of
|
||||
# oddtextures.o (.data) here — Gfx DLs + 32 sImageTableEntry tables =
|
||||
# 0x13F8 bytes. texReset() copies End-Start, so the marker must span the
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
to PC layout and write a concatenated sidecar image + manifest.csv (same
|
||||
shape/pattern as tools_pc/d43_emit.py / port/src/pcmodels.c).
|
||||
|
||||
Format spec: docs/PCPortResearch.md D78-D82. Summary:
|
||||
Format spec: docs/internals.md D78-D82. Summary:
|
||||
|
||||
bg/*.seg (raw, uncompressed on ROM; stays uncompressed on PC):
|
||||
header (0x40B, only words 0-4 consulted): word0 must be 0 (bswap32,
|
||||
|
||||
@@ -4,7 +4,7 @@ to PC layout, appending them to the same pccg.bin/manifest.csv sidecar image
|
||||
produced by d69_emit.py (port/src/pccg.c already serves any filename listed
|
||||
in the manifest, so this script just needs to add rows).
|
||||
|
||||
Format spec: docs/PCPortResearch.md D88.x. Summary:
|
||||
Format spec: docs/internals.md D88.x. Summary:
|
||||
|
||||
`struct stagesetup` (bondtypes.h) is a 10-pointer-field header (40B on N64,
|
||||
80B on PC -- pointers grow 4->8B) whose fields store plain (non-segment)
|
||||
@@ -20,7 +20,7 @@ room-offset array, generalized to many interleaved regions).
|
||||
|
||||
Verified against BUNKER1 (UsetupsevbunkerZ) by full byte-level ROM
|
||||
reconstruction (offsets/counts cross-checked against every pointer field in
|
||||
the file -- see tools_pc/d88_emit.py history / PCPortResearch.md D88.2):
|
||||
the file -- see tools_pc/d88_emit.py history / internals.md D88.2):
|
||||
the file's leftover byte-stream regions (propDefs polymorphic prop-def
|
||||
table, AI opcode streams, pad/boundpad `plink` name strings, pad/boundpad
|
||||
name (`pname`) strings) all sit BETWEEN and interleaved with the 8 growing
|
||||
@@ -51,7 +51,7 @@ the cumulative shift from growth ahead of them in the file):
|
||||
KNOWN BUG (D125, unfixed as of M-13): the converted blob does not land
|
||||
correctly in the emitted file for some levels (Aztec/Bunker2/Surface2) --
|
||||
suspect the pass-1 delta / region-`end` handling here (~L308-340), NOT
|
||||
convert_stream itself. See PCPortResearch.md D125 + docs/LEVEL-STATUS.md.
|
||||
convert_stream itself. See internals.md D125 + docs/LEVEL-STATUS.md.
|
||||
- waypoint.neighbours / waygroup.neighbours / waygroup.waypoints /
|
||||
PathRecord.waypoints target arrays: plain `s32` ID lists (NOT file
|
||||
offsets -- graph node indices), NULL/-1 terminated, walked with real
|
||||
|
||||
@@ -6,7 +6,7 @@ Compares a directory of candidate frames against a golden baseline
|
||||
|
||||
WHY STRUCTURAL / TOLERANT BY DEFAULT
|
||||
-----------------------------------
|
||||
The PC port is NOT frame-deterministic (see docs/PCPortResearch.md
|
||||
The PC port is NOT frame-deterministic (see docs/internals.md
|
||||
Section F "D117"): logic advances a wall-clock-variable number of 60Hz
|
||||
ticks per rendered frame (src/game/frametiming.c waitForNextFrame /
|
||||
boss.c mainTickElapsed gate driven by osGetCount(), which the port maps
|
||||
|
||||