1 Commits
Author SHA1 Message Date
James Dansereau 66e38e27d7 docs: v0.1.0 alpha framing + hygiene pass
Update the public-facing docs to the M-49 state (v0.1.0 alpha, first
full-campaign playtest: 19/21 missions completable, D191 crashes Bunker ii
+ Statue, D193 AI pacing is the top flaw).

- README: lead with what it is + status, research angle second; new
  Quick start section; Status/Download rewritten for the alpha + dual
  platform; "By the numbers" refreshed (~3 weeks, ~320 commits, ~190
  findings, ~49 sessions, 69 PORT files).
- .github/release-notes.md: v0.1.0 title + known-issues rewrite.
- .github/social-preview.html: status/stat lines (regen + re-upload the
  PNG separately).
- docs/index.md, docs/dev/LEVEL-STATUS.md: M-49 status.
- docs/dev/findings.md, docs/dev/GRAPHICS-BACKLOG.md: D191-D197 +
  D148/D170/D173 M-49 escalations.

Hygiene:
- Fix the `&` literal in internals.md H1.
- Repoint two ../README.md links that 404 on the Pages site.
- Standardise British -> American spelling across the public docs
  (behaviour->behavior, colour->color, catalogue->catalog,
  centre->center, etc.). Deep internal logs left untouched.
- .gitignore: document the local-notes convention (docs/HANDOFF.md,
  docs/BACKLOG.md, and a new docs/dev/notes/ drop for roadmaps and
  session plans) so it survives a fresh clone.
2026-09-04 00:05:46 -04:00
160 changed files with 1657 additions and 27248 deletions

No files matched your search

+3 -8
View File
@@ -13,14 +13,9 @@ rules there are non-negotiable.
## Scope check
- [ ] No changes under `src/` or `include/` — **or** the only changes are
the narrow `#ifdef PORT` ABI exception (CONTRIBUTING.md rule 2),
checked against `docs/porting-notes.md` §A1, and documented in
`docs/dev/findings.md` (§F/D3x).
- [ ] If this PR contains a genuine `src/game` behavior change (not an
ABI/layout fix): an explicit rule-2 sign-off was granted
(`docs/dev-process.md` §7) and is recorded in a
`RULE-2-SIGNOFF`-tagged `docs/dev/findings.md` entry linked here.
- [ ] No changes under `src/` or `include/` — **or** the only changes are the
narrow `#ifdef PORT` ABI exception (CONTRIBUTING.md rule 2), and each is
documented in `docs/porting-notes.md` / `docs/dev/findings.md`.
- [ ] `Makefile`, `tools/`, `rsp/`, `ld/` untouched (N64 build).
- [ ] If `CMakeLists.txt` `REGION_DEFS` changed, it still matches the N64
`Makefile` per-region macro set exactly.
+35 -191
View File
@@ -1,202 +1,51 @@
## GoldenEye 007 PC Port <version>
## GoldenEye 007 PC Port — v0.1.0 alpha
<p align="center">
<img src="https://github.com/jkdansereau/goldeneye-pc-port/raw/v0.3.0/docs/media/goldeneye-gh-preview.gif" width="480"
alt="~12 s gameplay montage from live v0.3.0 play sessions (no audio track)">
</p>
> The full single-player campaign runs at a steady 60 fps with audio (music +
> SFX) playing throughout, on Windows and Linux including Steam Deck, and it
> is completable end to end: the whole campaign has been playtested through
> all 21 missions (Agent difficulty). It's an early public cut: all 21 solo
> missions load and run crash-free, but the known issues below are real;
> feedback is very welcome. **This is a pre-1.0 release, not a finished
> product** — expect rough edges and missing features until v1.0; see the
> [README's Roadmap section](https://github.com/jkdansereau/goldeneye-pc-port#roadmap)
> for where this is headed.
### What's new since v0.2.2
- **Bond is fixed in cutscenes** — the most visible remaining glitch in
prior releases: Bond no longer floats or glitches out during cutscenes,
his animation no longer breaks or repeats, and the camera correctly
tracks him through scripted sequences including all three Dam end-of-level
abseil shots (D243). Root cause: a 32-bit-era struct-size constant in the
cutscene body-model setup never accounted for 64-bit pointer widening,
corrupting the model's own animation-timing fields on creation. Fixing
that also fixed the associated camera shake, which had previously been
worked around with a camera-freeze hack that's no longer needed. **Two
related reports also cleared up as a side effect**: Bond's third-person
model floating above the ground at level start, and death animations
sinking him into the floor, both shared an underlying mechanism with this
bug — Bond now ends up in the right place the large majority of the time,
with only a small positional drift on the rare occasion he's off (D173/
D292).
- **Particle colors mostly fixed**: bullet-impact sparks and lingering
smoke/explosion residue no longer cycle through a rainbow palette (D219) —
root cause was a hardcoded raw-memory-offset read into the spark's stored
color that landed on the wrong bytes once the containing struct grew from
64-bit pointer widening. A second, separate cause was found and fixed in
explosion rendering itself (an out-of-bounds vertex-buffer write
corrupting adjacent particle data). This fix is real and stays in, but the
rainbow effect can still occur intermittently on any platform, not every
time and not on every level — a second cause hasn't been found yet; see
Known Issues.
- **Automatic widescreen FOV scaling**: the game now scales its field of
view to match your display's aspect ratio automatically, instead of
requiring the old manual FOV-scale slider. Toggle off in the F10 overlay
(`Widescreen Auto`) if you prefer the original 4:3 framing. The scaled
vertical FOV is clamped to a 20°–160° range as a guard against degenerate
values on unusual aspect ratios. (This is FOV scaling, not a native 16:9
world/HUD re-render — see Known Issues.)
- **TV overscan artifacts removed**: the black bars top/bottom and the thin
pixel strips left/right of the gameplay view are gone (D247/D246) — both
were the original N64 TV-safe-area margin, invisible on a CRT under real
overscan but visible on a PC monitor. New `Video.SafeAreaCrop` toggle
(default on) if you want the original framing back.
- **Two more crash fixes**: a campaign-halting crash at the end of Control
Center on 00 Agent difficulty, which would then crash on every subsequent
relaunch until the save was reset (D295); and a crash selecting most save
slots with `Skip Intro` enabled (D299).
- **The elusive complete freeze is root-caused and fixed.** Players had hit
a total, non-self-recovering lockup (most recently when returning from
Caverns to the intro screen) that was real but never reproduced on demand.
It turned out to be a latent defect in the original game's own AI command
stream — a record type the engine mis-measures, which our level-teardown
timing can trigger and the N64 never presents. Fixed with a minimal change
under the project's game-code exception process (D309/D310); a second,
unrelated freeze mechanism found during the same playtest is now guarded
as well (D311).
- **A NULL-pointer guard was added** around a save-data lookup that two
players hit with an identical crash address (GitHub #87, D301) — this
should close that crash, though we weren't able to reproduce the exact
trigger condition ourselves to confirm it end-to-end.
- **Facility's execution softlock is caught and recovered from**: a rare
race in the original game's own AI script (present on the N64 too) could
leave Ourumov frozen mid-execution after Objective C, with no way out short
of reloading. A port-side watchdog now detects the exact unrecoverable
state and re-seeds the attack so the scene plays out as authored; it
re-arms once the scene has cleared, so replaying Facility later in the
same session is still covered (D318).
- **A Steam Deck crash is fixed**: an audio-thread bug could crash the game
early in a level (`sndHandleEvent`'s sound-preemption scan could
dereference a pointer without checking it was valid first). Live-verified
crash-free on real Steam Deck hardware, including with `All unlocked`
toggled on.
- **Mouse input and F10 controls fixed**: mouse input was reworked to
follow the Perfect Dark PC port's architecture — slow mouse motion is no
longer barely recognized and fast motion no longer saturates almost
immediately (D300, `Input.MouseDirectLook`, on by default). The separate
aim-speed and turn-speed sliders — which could be set to inconsistent
values against each other — are now a single `Mouse sensitivity` control
with a wider range. Also fixed three F10 menu bugs found along the way:
clicking a row could unexpectedly scroll the whole list, a click near
certain rows could silently activate a different option than the one you
clicked (most noticeably, clicking `All unlocked` could instead trigger
`Quit to desktop`), and the panel's bottom row no longer duplicates
whatever item is currently selected (D251).
- **Random-number generation now matches the N64 original exactly** (D284) —
a shift-operation bug in the PRNG had desynced the PC's random stream from
N64's since the very first release, affecting loot placement, AI variance,
and other randomized elements. **This changes the random sequence from
every prior build**, so old input recordings/replays will diverge, but
your save files are unaffected: **a migration shim automatically upgrades
any save written by an older build the first time you load it** (D297) —
no manual action needed.
- **Assorted texture/rendering fixes**: an incorrect IA4 texture-format
decode and an overly-tight near-plane portal-culling guard were both
corrected (D266/D271) — reduces some texture-edge and visibility artifacts
on affected levels.
### Known issues
- The front-end **Nintendo logo renders as two white blobs**, and the
Rareware logo is close but its texture filtering looks off (D75).
- **Particle colors: a real fix landed this release, but the rainbow effect
can still occur intermittently.** One genuine, reproducible cause (an
out-of-bounds vertex-buffer write in explosion rendering) is fixed. A
second, still-unidentified cause remains, on both Windows and Steam Deck —
it doesn't reproduce every time or on every level, so treat this as
improved, not fully resolved (D252).
- **Some muzzle flashes draw an extra, erroneous long flash straight up from
the gun** (seen on the M16, among others), on top of the normal, correctly
drawn flash. Cosmetic only (D303).
- **Surface 1: the 2D billboard trees near the start render as a solid wall
of tree texture** instead of discrete sprites (D236). Under active
investigation across ten+ passes; no fix yet.
- **No native widescreen render**: v0.3.0's automatic FOV scaling (above)
expands the *field of view* for 16:9+ displays, but the world geometry and
HUD are still authored/laid out for 4:3 — this is not yet a true
edge-to-edge 16:9 render.
- **No controller rebinding UI, and no macOS or ARM builds.**
- **Distant geometry can drop out on the biggest open levels** (Streets,
Egyptian) at default FOV — a culling/LOD issue that sometimes
self-corrects as you keep moving (D249).
- **Gunshot SFX can sound off during sustained/rapid fire**: cadence can
drift from the N64 original's rate, and PP7/AK47 fire can occasionally go
silent under heavy automatic fire near another looping sound (e.g. an
alarm klaxon) (D240/D241).
- **`All unlocked` is highly experimental — do not enable it until you have
at least one save written.** Complete a level normally first (e.g. Dam on
Agent), then turn the toggle on. Enabling it on a brand-new install with
no prior save can still cause silent audio and odd right-mouse-aim
behavior (D259/D257). (The Steam Deck crash fix above addresses a
separate, now-fixed symptom that had also been associated with `All
unlocked` use; this audio/aim caveat is unrelated and still open.)
- Water on `IsWater` levels shows a moving seam between two patterns (D245).
- **Occasional z-fighting on some levels' geometry** (D308) — a Dam intro/
truck-wheel instance showing odd transparent-looking areas has also been
seen and may be related (D306). Minor, cosmetic; not investigated for
this release.
- **In-level security camera props can occasionally end up facing backwards**
(the wrong direction) on some levels, seen on Bunker — this is the
gameplay security-camera object, not the player's own view (D307). Not
investigated for this release.
- **A rare, self-clearing visual quirk**: on an occasional animation
transition, Bond's model can briefly render out of place. It has no effect
on gameplay or your save, and it clears on its own or by re-entering the
level; a safeguard added this release guarantees it can never affect game
stability (D311).
- **Two collision oddities found in final playtesting**: throwable items
(mines, grenades) can occasionally clip through walls in certain spots
(D312), and bullet impacts don't appear on some surfaces — doors and
windows are unaffected (D313). Both are queued for the next release; if
you hit either, an issue with the level and spot helps a lot.
> ⚠️ **Research alpha. Playable, not polished.**
> The full single-player campaign runs. In a full-campaign playtest on this
> build, **19 of 21 missions were completable start to finish**. Known issues:
> - **no audio** (Phase 3, not started) — the game runs silent;
> - **two levels crash mid-mission** — Bunker ii and Statue (one root cause, D191);
> - **AI characters move too slowly**, which breaks Cradle (the final level)
> via Trevelyan's scripted behavior (D193);
> - **cutscenes frequently glitch** — skipped, wrong camera, misplaced actors (D148/D160);
> - some front-end 3D models are broken (the spinning Nintendo logo, the
> MISSION COMPLETE / mode-select models);
> - outdoor levels render with a **black sky**;
> - mouse aim and some textures/transparency have rough edges.
>
> If you just want to *play* GoldenEye on PC today, use one of the Xbox 360
> recompilation projects instead — see the
> [project README](https://github.com/jkdansereau/goldeneye-pc-port#how-this-differs-from-the-other-goldeneye-pc-projects).
### Downloads
| File | Platform |
|---|---|
| `goldeneye-pc-port-<version>-win64.zip` | Windows x86-64 |
| `goldeneye-pc-port-<version>-linux-x86_64.tar.gz` | Linux x86-64 (incl. Steam Deck) |
| `goldeneye-pc-port-<version>-linux-x86_64.tar.gz` | Linux x86-64 |
Each contains the engine executable, a README, license texts, and the
`prepare-assets` tool. **No ROM, no game assets.** The Windows bundle carries
its runtime DLLs; the Linux bundle carries SDL2, so on both platforms nothing
needs to be installed first.
`prepare-assets` tool. **No ROM, no game assets.** The Windows bundle also
carries its runtime DLLs; the Linux bundle links against your distro's SDL2 /
zlib / libGL (`sudo apt install libsdl2-2.0-0 zlib1g libgl1`, or the equivalent).
Windows development and playtesting is the primary path; the Linux build boots
and renders (tested on WSLg) but has had far less exercise.
### Running it
You supply your own **GoldenEye 007 N64 ROM** (`.z64`, big-endian) that you
legally own: the **NTSC-U (US)** release is what this build supports (PAL / JP
are on the roadmap). No Python, no toolchain, nothing to install.
You supply your own **NTSC-U GoldenEye 007 N64 ROM** (`.z64`, big-endian,
`SHA-1 abe01e4aeb033b6c0836819f549c791b26cfde83`). Only the US ROM is supported
in this alpha.
1. Unpack the archive.
2. Make a `data/` folder next to the executable and put the ROM in it, named
2. Make a `data/` folder next to the executable and put the ROM in it as
`ge007.ntsc-final.z64`.
3. Run the executable **from that folder**. The first run takes a few extra
seconds: it detects your ROM, generates the two derived asset folders
(`data/pcmodels-ntsc-final/`, `data/pccg-ntsc-final/`) once, and saves them
for every future run. (The generator is `prepare-assets/ge007-convert`
inside the bundle; you can also run it manually; it prints what it's doing.)
**Steam Deck:** sideload the unpacked folder (USB or a file manager), do steps
2–3, then add the executable to Games → *Add Game* as a non-Steam game.
**In-game settings on the Deck:** the options overlay is fully gamepad-driven:
it opens with **Select**, the D-pad or left stick (up/down) moves between
options, **A** steps the selected option forward, **B** steps it back, and
**Start** (or Select again) closes. No keyboard needed.
3. Run the one-time asset step (needs Python 3.8+):
`python3 prepare-assets/prepare-assets.py`
— it reads your ROM and writes the two `data/pc*-ntsc-final/` folders the
engine needs. Standard library only; a few seconds.
4. Run the executable **from that folder**.
Full steps are in the bundled `README.md`.
@@ -209,13 +58,8 @@ sha256sum -c goldeneye-pc-port-<version>-linux-x86_64.tar.gz.sha256
### Source & docs
<https://github.com/jkdansereau/goldeneye-pc-port>, built on the
<https://github.com/jkdansereau/goldeneye-pc-port> — built on the
[GoldenEye 007 decompilation](https://github.com/n64decomp/007), architecture
after the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark).
Non-commercial fan preservation/research project; not affiliated with any
rights holder. **AI disclosure:** built through agentic AI coding (Claude
Code + a local open-weight model), directed by one person in their spare
time — as much a study of what agentic development gets wrong on a
game-sized codebase as it is a port. See the README's
[Background section](https://github.com/jkdansereau/goldeneye-pc-port#background)
for the full account.
rights holder.
+5 -5
View File
@@ -118,18 +118,18 @@
<div class="h1">GOLDENEYE <span class="r">007</span><br><small>PC&nbsp;PORT</small></div>
<div class="meta">
Mission: Native PC port of GoldenEye 007, compiled from the N64 decomp project
Mission: Native PC port of GoldenEye, compiled from the N64 decomp project
</div>
<div class="rep">REPORT:</div>
<div class="rows">
<div class="statusline">Status: <span class="status">RELEASED v0.2.0 &mdash; WINDOWS &middot; LINUX &middot; STEAM DECK</span></div>
<div class="statusline">Status: <span class="status">ALPHA v0.1.0 &mdash; PHASE 2 OF 4</span></div>
<div class="stats">
<span class="k">Statistics:</span><span>All 21 solo missions &mdash; full campaign completed in playtesting</span>
<span class="k">Statistics:</span><span>19 / 21 solo missions completable in a full playthrough</span>
<span></span><span>~230 game-code units, compiled unmodified</span>
<span></span><span>~22k lines C/C++ &nbsp;&middot;&nbsp; ~10k Python (port layer + tools)</span>
<span></span><span>~250 bugs root-caused &nbsp;&middot;&nbsp; two coding agents, one human, part-time</span>
<span></span><span>~17k lines C/C++ &nbsp;&middot;&nbsp; ~6k Python (port layer + tools)</span>
<span></span><span>~190 bugs root-caused &nbsp;&middot;&nbsp; built in ~3 weeks, part-time</span>
</div>
</div>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 597 KiB

After

Width:  |  Height:  |  Size: 583 KiB

+15 -149
View File
@@ -66,31 +66,12 @@ jobs:
runs-on: ubuntu-22.04
outputs:
code: ${{ steps.filter.outputs.code }}
# Windows builds default to the self-hosted box (fast, local ccache;
# docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md §G2) for everything EXCEPT:
# (a) a release build (tag push) -- keep that on a clean, reproducible
# GitHub-hosted runner for the artifact that actually ships, and
# (b) a fork PR -- untrusted PR code must never reach a box that will
# later hold a ROM. Same-repo (non-fork) PRs now also get routed to
# self-hosted per the 2026-09-12 standing preference.
windows_selfhosted: ${{ steps.runner.outputs.selfhosted }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- id: runner
run: |
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
echo "selfhosted=false" >> "$GITHUB_OUTPUT"
elif [ "${{ github.event_name }}" = "pull_request" ] && \
[ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
echo "selfhosted=false" >> "$GITHUB_OUTPUT"
else
echo "selfhosted=true" >> "$GITHUB_OUTPUT"
fi
- id: filter
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
@@ -132,26 +113,6 @@ jobs:
- name: Byte-compile Python tooling
run: python3 -m compileall -q scripts tools_pc
# Generated dev-docs stay in sync with the tree (speed-ups plan Step 4).
- name: Check generated dev indexes are current
run: |
python3 tools_pc/gen_findings_index.py --check
python3 tools_pc/gen_env_probes.py --check
# Docs budget guardrail (ctx-opt P2): warn, don't fail. The §F index
# table should stay one-liners; fat cell content belongs in `## Dxx`
# sections at the file tail (lookup path: docs/dev/findings-index.csv).
- name: Check dev-docs size budgets (warn only)
run: |
# Budget = ~55 KB of table rows + ~110 KB of legacy D24–D33 / M-xxx
# narrative that predates the ctx-opt migration. The signal is fat
# ROWS re-accumulating, so the threshold sits ~15 KB above today's
# 166 KB total.
f=$(awk '/^### F\./{f=1;next} /^### G\./{f=0} f' docs/dev/findings.md | wc -c)
[ "$f" -le 184320 ] || echo "WARNING: findings.md §F region is $f bytes (> 180 KB) — fat rows accumulating; migrate cell content to ## Dxx sections at the file tail"
p=$(wc -c < docs/porting-notes.md)
[ "$p" -le 122880 ] || echo "WARNING: porting-notes.md is $p bytes (> 120 KB) — consider splitting or adding a TOC block"
# US only. gen_romassets.py resolves every symbol for the US region from
# the committed CSVs with no ROM present; PAL/JP each need a handful of
# region-specific prop/font offsets that only come from that region's ROM
@@ -180,12 +141,6 @@ jobs:
env:
CC: gcc-14
CXX: g++-14
# CMakeLists.txt opt-in enables CMAKE_{C,CXX}_COMPILER_LAUNCHER=ccache
# when ccache is on PATH (b4de1c68, speed-ups plan Step 5). Game
# sources are frozen (1:1 with the decomp), so after the first run only
# touched port/ files miss cache -- near-total hit rate.
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 500M
steps:
- uses: actions/checkout@v7
with:
@@ -195,36 +150,11 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y build-essential gcc-14 g++-14 cmake python3 \
python3-pip libsdl2-dev zlib1g-dev libgl1-mesa-dev ccache patchelf
- name: Restore ccache
uses: actions/cache@v4
with:
path: ${{ env.CCACHE_DIR }}
key: ccache-linux-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }}
restore-keys: |
ccache-linux-${{ hashFiles('CMakeLists.txt') }}-
ccache-linux-
libsdl2-dev zlib1g-dev libgl1-mesa-dev
- name: Regenerate ROM-asset symbol file (US)
run: python3 scripts/gen_romassets.py u
# Drop-in ROM support: freeze the asset-prep tool into a single
# ge007-convert binary the engine spawns on first run when the
# pcmodels/pccg sidecars are missing (port/src/romconvert.c). Pure-
# stdlib scripts, so PyInstaller --onefile is clean; lands in
# tools_pc/dist/prepare-assets/ where bundle-linux.sh picks it up.
- name: Freeze asset converter (PyInstaller)
run: |
python3 -c "import PyInstaller" 2>/dev/null \
|| python3 -m pip install --quiet --break-system-packages pyinstaller
python3 -m PyInstaller --onefile --noconfirm --clean \
--name ge007-convert \
--distpath tools_pc/dist/prepare-assets \
--workpath build-pyinst --specpath build-pyinst \
tools_pc/dist/prepare-assets/prepare-assets.py
ls -la tools_pc/dist/prepare-assets/ge007-convert
- name: Configure (ntsc-final)
run: cmake -S . -B build-linux -DROMID=ntsc-final
-DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14
@@ -232,18 +162,15 @@ jobs:
- name: Build (ntsc-final)
run: cmake --build build-linux -j"$(nproc)"
- name: ccache stats
run: ccache -s
- name: Show binary
run: |
ls -la build-linux/ge007* || true
file build-linux/ge007* || true
# Package the Linux bundle on every run so the release path stays
# exercised. SDL2 is bundled (patchelf sets the exe's rpath to $ORIGIN)
# so the tarball runs as-is on any distro / a sideloaded Steam Deck;
# zlib + libGL stay system deps. Same ROM / size guards as Windows.
# exercised. No shared libs are bundled — the README tells the user to
# install SDL2 / zlib / libGL from their distro. Same ROM / size guards
# as the Windows bundle.
- name: Package Linux bundle
run: |
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
@@ -273,85 +200,34 @@ jobs:
windows-build:
needs: changes
if: needs.changes.outputs.code == 'true'
# Self-hosted by default (fast, local ccache); windows-latest only for a
# release (tag) build or a fork PR — see the `changes` job's `runner`
# step for the exact rule (docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md §G2).
runs-on: ${{ needs.changes.outputs.windows_selfhosted == 'true' && fromJSON('["self-hosted", "windows"]') || 'windows-latest' }}
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
env:
# Same opt-in ccache CMakeLists.txt already wires up (Step 5). Note:
# this cache path is read/written by the JS actions/cache step outside
# the msys2 shell too, so it stays a plain workspace-relative path
# rather than an msys /c/... form.
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 500M
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
# windows-latest (release tag / fork PR) is ephemeral: full install +
# update every run. Self-hosted (everything else): `release: false`
# reuses the box's own C:\msys64 as-is and, with update/install both
# empty here, does no pacman -Syu / pacman -S at all (verified against
# the action's source) — it only registers the `msys2 {0}` shell
# wrapper every job needs. Packages themselves are hand-maintained on
# the box; the dependency check below (not this step) verifies they're
# present.
- uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
release: ${{ needs.changes.outputs.windows_selfhosted != 'true' }}
update: ${{ needs.changes.outputs.windows_selfhosted != 'true' }}
install: ${{ needs.changes.outputs.windows_selfhosted != 'true' && 'mingw-w64-x86_64-toolchain mingw-w64-x86_64-cmake mingw-w64-x86_64-SDL2 mingw-w64-x86_64-zlib mingw-w64-x86_64-python mingw-w64-x86_64-ccache zip unzip' || '' }}
# Self-hosted only: fails fast with exact install instructions if the
# box's hand-maintained MSYS2 is missing a required package, instead of
# a confusing failure deeper in the build. windows-latest just installed
# everything above, so this would be redundant there.
- name: Check self-hosted dependencies
if: needs.changes.outputs.windows_selfhosted == 'true'
run: tools_pc/check-selfhosted-deps.sh
- name: Restore ccache
uses: actions/cache@v4
with:
path: ${{ env.CCACHE_DIR }}
key: ccache-windows-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }}
restore-keys: |
ccache-windows-${{ hashFiles('CMakeLists.txt') }}-
ccache-windows-
update: true
install: >-
mingw-w64-x86_64-toolchain
mingw-w64-x86_64-cmake
mingw-w64-x86_64-SDL2
mingw-w64-x86_64-zlib
mingw-w64-x86_64-python
zip
unzip
- name: Regenerate ROM-asset symbol file (US)
run: python scripts/gen_romassets.py u
# Drop-in ROM support: freeze the asset-prep tool into a single
# ge007-convert.exe the engine spawns on first run when the pcmodels/
# pccg sidecars are missing (port/src/romconvert.c). Lands in
# tools_pc/dist/prepare-assets/ where bundle-win.sh picks it up.
- name: Freeze asset converter (PyInstaller)
run: |
# MSYS2's mingw python ships without pip and has ensurepip
# disabled — install pip from pacman, then PyInstaller.
python -c "import PyInstaller" 2>/dev/null || {
pacman -S --noconfirm mingw-w64-x86_64-python-pip
python -m pip install --quiet --break-system-packages pyinstaller
}
python -m PyInstaller --onefile --noconfirm --clean \
--name ge007-convert \
--distpath tools_pc/dist/prepare-assets \
--workpath build-pyinst --specpath build-pyinst \
tools_pc/dist/prepare-assets/prepare-assets.py
ls -la tools_pc/dist/prepare-assets/ge007-convert.exe
- name: Build (ntsc-final)
run: ./build-pc.sh ntsc-final
- name: ccache stats
run: ccache -s
# Package the distributable bundle on every run so the release path is
# always exercised. On a tag build VERSION is the tag; otherwise it is a
# throwaway CI string. bundle-win.sh hard-fails if a ROM image or an
@@ -410,21 +286,11 @@ jobs:
run: |
TAG="${GITHUB_REF_NAME}"
ASSETS=(dist/*.zip dist/*.zip.sha256 dist/*.tar.gz dist/*.tar.gz.sha256)
NOTES="$(mktemp)"
# Asset names are built from the tag with the leading v stripped
# (bundle-{linux,win}.sh), so substitute the same form -- a raw
# TAG would make the notes reference goldeneye-pc-port-v*.* assets
# that don't exist.
NOTES_VERSION="${TAG#v}"
sed "s/<version>/${NOTES_VERSION}/g" .github/release-notes.md > "$NOTES"
if gh release view "$TAG" >/dev/null 2>&1; then
gh release upload "$TAG" "${ASSETS[@]}" --clobber
# Re-stamp the notes body too: it is only set at creation, and
# release-notes.md may have moved since the draft was first cut.
gh release edit "$TAG" --notes-file "$NOTES"
else
gh release create "$TAG" "${ASSETS[@]}" \
--draft --prerelease \
--title "GoldenEye 007 PC Port $TAG" \
--notes-file "$NOTES"
--notes-file .github/release-notes.md
fi
-234
View File
@@ -1,234 +0,0 @@
# Self-hosted runtime (ROM-gated)
#
# Runs the build + the 21-level headless sweep (tools_pc/verify.sh) on the
# self-hosted runner "corpyvt6agent01", the one machine that has a legal ROM.
# This is the job class ci.yml explicitly does NOT do: CI is build-only, no
# ROM, so runtime checks live here. Setup + rollout plan (gitignored, local):
# docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md
#
# Persistence model — everything lives under the runner's own paths, managed
# by the pipeline; no hand-maintained directories:
# - ccache -> actions/cache (stored by the runner under _work/_temp)
# - ROM -> actions/cache, key ge-rom-data-ntsc-v1. Bootstrap: copy the
# three files into {runner_home}/_rom-seed/ on the box; a run
# with an empty cache copies them in and attempts to save the
# cache entry. The seed dir is kept (NOT deleted) rather than
# treated as one-time -- actions/cache/save has been silently
# failing to persist on this box (warning, not a hard step
# failure; root cause open, see SELFHOSTED-RUNNER-PLAN.md), so
# the seed is the durable fallback every run re-copies from
# until that's fixed. Integrity is re-verified every run
# against the committed ge007.u.sha1 (repo-tracked US ROM hash).
# - MSYS2 -> machine-level install at C:\msys64 (verify.sh hardcodes the
# prefix), installed and its packages maintained BY HAND on
# the box. msys2/setup-msys2 still runs every job (it's what
# registers the `msys2 {0}` shell wrapper), but with
# `release: false` and update/install both omitted — verified
# against the action's source to do no pacman -Syu / pacman -S
# / network round-trip, just a fast reuse of the existing
# C:\msys64. Package presence is checked (not installed) by
# tools_pc/check-selfhosted-deps.sh, which fails fast with
# exact install instructions if something's missing.
#
# Trigger gating (non-negotiable):
# - workflow_dispatch ONLY for now. Flip on `push: branches: [main]` once
# the first manual runs are reliably green.
# - NEVER add `pull_request`: unreviewed fork code must not be routable to
# a runner that can see the ROM.
#
# Leak rules (docs/dev/notes/AGENT-VM-FORGEJO-PLAN.md checklist):
# - The ROM + sidecars live in the runner-managed cache only; nothing in
# this workflow uploads from data/.
# - Artifacts here are console logs + a JSON verdict only. Rendered frames
# (ppm/) are ROM-derived content and are NOT uploaded.
name: Self-hosted runtime
on:
workflow_dispatch:
inputs:
levels:
description: "Sweep subset, SWEEP_LEVELS format (blank = all 21)"
required: false
default: ""
type: string
permissions:
contents: read
# One ROM job at a time per ref; never cancel an in-flight sweep out from
# under itself (the sweep is serialized by its own lockdir + watchdog). Group
# + cancel-in-progress:false is what actually serializes runs — GitHub
# Actions' workflow-level `concurrency` has no `max-parallel` key (that only
# exists under a job's `strategy:` for matrix jobs); a stray one here is a
# hard parse error that blocks every dispatch.
concurrency:
group: selfhosted-${{ github.ref }}
cancel-in-progress: false
env:
ROM_CACHE_KEY: ge-rom-data-ntsc-v1
jobs:
build-and-sweep:
runs-on: [ self-hosted, windows ]
defaults:
run:
shell: msys2 {0}
env:
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 500M
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
# `release: false` reuses the box's own C:\msys64 as-is instead of
# downloading/installing MSYS2 fresh (verified against the action's
# source: with this set, and update/install both omitted, it does
# exactly one `uname -a` + a pacman.conf sed tweak — no pacman -Syu,
# no pacman -S, no network round-trip). This step still has to run
# every time — it's what registers the `msys2 {0}` shell wrapper
# above — but does no install/update work.
- uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
release: false
# Packages themselves are hand-maintained on the box (never installed
# by this workflow) — fails fast with exact install instructions if
# one's missing, instead of a confusing failure deeper in the build.
- name: Check self-hosted dependencies
run: tools_pc/check-selfhosted-deps.sh
# --- ROM + sidecars: runner-managed cache, one-time seed --------------
- name: Restore ROM + sidecars (runner cache)
uses: actions/cache/restore@v4
with:
path: |
data/ge007.ntsc-final.z64
data/pcmodels-ntsc-final
data/pccg-ntsc-final
key: ${{ env.ROM_CACHE_KEY }}
- name: Check ROM store presence
id: romcheck
run: |
present=true
for f in ge007.ntsc-final.z64 pcmodels-ntsc-final pccg-ntsc-final; do
if [ ! -f "data/$f" ]; then echo "missing from cache: $f"; present=false; fi
done
echo "present=$present" >> "$GITHUB_OUTPUT"
# {runner_home} = two levels up from RUNNER_WORKSPACE ({home}/_work/{repo}).
# NOT deleted after the cache save below (see that step's comment) --
# kept as a durable fallback source until the cache save is proven to
# actually round-trip on this box.
- name: Seed ROM cache (from durable seed dir)
if: steps.romcheck.outputs.present == 'false'
run: |
seed="$(dirname "$(dirname "$RUNNER_WORKSPACE")")/_rom-seed"
[ -d "$seed" ] || {
echo "ROM cache is empty and no seed found at $seed." >&2
echo "One-time bootstrap: copy ge007.ntsc-final.z64, pcmodels-ntsc-final," >&2
echo "and pccg-ntsc-final into that directory on the runner, then re-run." >&2
exit 2
}
mkdir -p data
# -r: pcmodels-*/pccg-* are directories, not single files (found by
# testing — a plain cp failed with "omitting directory").
cp -r "$seed/ge007.ntsc-final.z64" "$seed/pcmodels-ntsc-final" \
"$seed/pccg-ntsc-final" data/
echo "seeded ROM store from $seed"
# Explicit save (restore alone never saves): persist the seeded store
# immediately, before the build/sweep can fail out from under it.
# KNOWN ISSUE (2026-09-12): this reliably logs a WARNING annotation
# ("Failed to save: ... tar.exe failed with exit code 2") on this box
# and never actually persists anything to the cache -- but
# actions/cache/save treats a save failure as non-fatal by design, so
# the step still reports success and the job carries on. Root cause not
# yet found (see docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md). Consequence:
# do NOT delete the seed dir after this step (previously done, and it
# meant a *silent* cache-save failure combined with the delete wiped
# the only copy of the ROM every run -- exactly what broke rollout
# step 5's repeated dispatches). The seed dir now stays in place
# indefinitely as a durable fallback; "Check ROM store presence" /
# "Seed ROM cache" above already re-copy from it whenever the cache
# comes back empty, so this is safe to leave as a no-op once the cache
# save is fixed for real.
- name: Save ROM cache
if: steps.romcheck.outputs.present == 'false'
uses: actions/cache/save@v4
with:
path: |
data/ge007.ntsc-final.z64
data/pcmodels-ntsc-final
data/pccg-ntsc-final
key: ${{ env.ROM_CACHE_KEY }}
- name: Verify ROM integrity (committed US sha1)
run: |
cd data
want="$(cut -d' ' -f1 ../ge007.u.sha1)"
got="$(sha1sum ge007.ntsc-final.z64 | cut -d' ' -f1)"
[ "$want" = "$got" ] || { echo "ROM hash mismatch: want $want got $got" >&2; exit 2; }
echo "ROM sha1 ok: $got"
# --- build -------------------------------------------------------------
# Same opt-in ccache CMakeLists.txt wires up (speed-ups plan Step 5);
# persisted via actions/cache like ci.yml's windows-build.
- name: Restore ccache
uses: actions/cache/restore@v4
with:
path: ${{ env.CCACHE_DIR }}
key: ccache-selfhosted-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }}
restore-keys: |
ccache-selfhosted-${{ hashFiles('CMakeLists.txt') }}-
ccache-selfhosted-
- name: Regenerate ROM-asset symbol file (US)
run: python scripts/gen_romassets.py u
- name: Build (ntsc-final)
run: ./build-pc.sh ntsc-final
# verify.sh: build-if-stale -> headless GE_PCDUMP per level -> crash
# symbolication -> pixcount -> framediff vs tools_pc/golden/. Serialized
# + watchdog + one retry by design (D117: not frame-deterministic).
- name: Level sweep (all 21, or dispatch subset)
run: |
[ -n "${{ inputs.levels }}" ] && export SWEEP_LEVELS='${{ inputs.levels }}'
tools_pc/verify.sh sweep --json > sweep.log 2>&1
rc=$?
cat sweep.log
exit $rc
# Pull the JSON verdict array out of the mixed stdout for easy review.
- name: Extract JSON verdict
if: always()
run: |
python -c "
import json, sys
s = open('sweep.log', encoding='utf-8', errors='replace').read()
i = s.rfind('[{')
out = '[]'
if i >= 0:
d, _ = json.JSONDecoder().raw_decode(s[i:])
out = json.dumps(d, indent=1)
open('sweep-verdict.json', 'w').write(out + '\n')
print(out)
"
# Console logs + verdict only — no frames, no ROM bytes (leak rules).
- name: Upload sweep report
if: always()
uses: actions/upload-artifact@v7
with:
name: selfhosted-sweep-${{ github.run_number }}
path: |
sweep.log
sweep-verdict.json
ge007.crash.log
if-no-files-found: ignore
retention-days: 14
-29
View File
@@ -142,35 +142,14 @@ ge007.crash.log
run_out.log
ppm/
screenshots/
# tools_pc/perf.ps1 output (raw ETW traces + CSV analysis, regenerable)
perf_captures/
# local Mupen64 Plus checkout (N64 reference footage); binaries, do not commit
tools_pc/mupen64/
# local N64 emulator checkout (D205 A/B reference)
mupen64/
# PC port run/scratch logs
run_*.log
d6*.log
ge007.crash.log
# agent scratch space + audio probe artifacts (regenerable dumps/traces)
scratchpad/
# session logs / investigation scratch — local only, never in the public repo
# (untracked 2026-09-19 during the v0.3.0 release review)
/scratch/
# re-vendored GPL reference source (see reference/mouse-injector/README.md)
/reference/mouse-injector/*
!reference/mouse-injector/README.md
# local research/backlog working docs (TCRF cut-content cross-reference etc.)
backlog/
# montage work artifact: the tiled contact sheet behind docs/img/shots/ and
# the preview video (regenerable from the same session recordings)
docs/img/shots/CONTACT-SHEET.jpg
/audiodump.raw
/audiotrace.log
/mixertrace.log
# Claude Code local state / agent worktrees
.claude/
@@ -180,11 +159,3 @@ nohup.out
gdb.txt
gdb-hang.txt
# frozen asset converter (built by CI via PyInstaller; local freezes land here)
tools_pc/dist/prepare-assets/ge007-convert*
build-pyinst/
# Local working junk (frame captures, timerun state, PPM dumps) - moved here
# from repo root / scratch on 2026-09-18; never commit.
/local/
+4 -7
View File
@@ -4,14 +4,14 @@
- `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).
- **Reference docs:** `docs/internals.md` — architecture, GE-specific RSP deltas, phased plan (§1–§10). `docs/dev/findings.md` — the `Dxx` finding log (§F + §H). **Look up findings via `docs/dev/findings-index.csv` (label, one-liner, status; regenerate with `tools_pc/gen_findings_index.py`), then read only the specific `## Dxx` entry (multi-pass labels like D202/D176(a) have large sections — grep within the section or read with offset/limit, don't slurp it whole). Never linear-read.** `docs/porting-notes.md` — the recurring N64→PC bug classes (dense; skim the headers, read what's relevant).
- **Reference docs:** `docs/internals.md` — architecture, GE-specific RSP deltas, phased plan (§1–§10). `docs/dev/findings.md` — the `Dxx` finding log (§F + §H, indexed at the top of §F); read the entry you need, don't linear-read. `docs/porting-notes.md` — the recurring N64→PC bug classes (dense; skim the headers, read what's relevant).
- **Current status:** the README "Status" section, and `docs/dev/LEVEL-STATUS.md` for the per-level sweep. Current task + environment: `docs/HANDOFF.md` (a rolling local working file — may be absent in a fresh clone; fall back to the README "Status" section).
- **Dispatching subagents?** `docs/dev-process.md` — task budgets/deadlines, file partitioning, pre-flight, the standard brief template. Every investigation subagent reads `docs/porting-notes.md` 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 and check the exception below before assuming the fix belongs in `port/`. **Diagnosis is never restricted — only the fix.** Trace a bug as deep into `src/game` state as the evidence leads, and name the exact struct/field/logic responsible, before deciding which of three buckets it falls in (full framing: `docs/dev-process.md`). **Narrow exception (ABI/layout only):** the 32→64-bit pointer-width transition forces a small class of mechanical, semantics-preserving edits that cannot be isolated in `port/` — **any struct-layout or pointer-width-driven misread caused by 32→64-bit widening**, whether in a ROM-serialized record (a struct with a 32-bit-pointer field misaligns when read as 64-bit) or a **live runtime struct/union** (a raw-byte offset alias into a union arm whose true field shifted because an earlier member in the same union widened — the D209/D210/D255 pattern; see `docs/porting-notes.md` §A1 for the full catalogue and its diagnostic tells). These follow the PD ground-truth pattern (store the embedded address as `u32` and cast to a real pointer at the use site; or read the correctly-named/typed field instead of a raw-offset/mistyped alias), change no logic or behavior, and are each documented in `docs/dev/findings.md` §F/D3x, cross-tagged to §A1 where that pattern applies. **A genuine behavioral difference** — the decomp's byte-identical code, given verified-correct inputs, still diverging from real N64 behavior — is not covered by this exception and requires the rule-2 sign-off procedure (`docs/dev-process.md`) before any `src/game` edit. No other game-code edits are permitted.
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/dev/findings.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.
@@ -21,12 +21,11 @@
| File | Role |
|---|---|
| `docs/internals.md` | Architecture + RSP deltas + phased plan (§1–§10). Reference, not a linear read. |
| `docs/dev/findings.md` | The `Dxx` finding log (§F/§H); lookups via `docs/dev/findings-index.csv`. |
| `docs/dev/findings.md` | The `Dxx` finding log (§F/§H, indexed at top of §F). |
| `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. |
| `reference/mouse-injector/README.md` | **Stub only** — the vendored GEPD-Edition Mouse Injector source (GPLv2) was removed from the public repo 2026-09-19 (license hygiene: GPL code + prebuilt binary in an MIT project; it was never compiled here). The stub records provenance + how to re-vendor locally (gitignored). The ported mouse-aim model lives in `port/src/input.c` (D194 lineage); design record: `docs/dev/GEPD-INPUT-PLAN.md`. |
| A local **Perfect Dark PC port** checkout ([fgsfdsfgs/perfect_dark](https://github.com/fgsfdsfgs/perfect_dark)) | **Standing reference** — consult it whenever a 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; `filemodel.c` is the D43 near-analogue) and `mixer.c`/`input.c`/`fs.c`. Port-layer files only; same family ≠ identical format — validate per field. Full audit: `docs/internals.md` §2.4. |
## Build
@@ -57,8 +56,6 @@ Run `/linkcheck` for this sweep. Record new findings in `docs/dev/findings.md`
parked in `docs/dev/GRAPHICS-BACKLOG.md`.
- **Phase 3 (audio + input):** input layer done (`port/src/input.c`); polish
bugs open (D118* mouse-look residuals; interactive feel-checks owed). Audio
mixer done (libaudio → SDL software mixer, D198–D201); D204 tempo drift
fixed + measured; D202 stuck door loop root-caused with a port-side
expiration (M-66b) awaiting by-ear verification.
not started — libaudio → SDL, copy-and-adapt from the PD port's `mixer.c`.
- **Phase 4 (saves + polish):** file-backed EEPROM done; widescreen, config,
rebinding UI outstanding.
+7 -29
View File
@@ -21,10 +21,9 @@ Context is scarce. Load by tier; do not blind-read whole files.
- **Tier 2 — on demand only, do NOT read start-to-finish:**
- `docs/internals.md` — architecture / RSP deltas / phased plan (§1–§10).
Read the section you need.
- Findings: look up via `docs/dev/findings-index.csv` (label, one-liner,
status), then read only the specific `## Dxx` entry in
`docs/dev/findings.md` (multi-pass labels like D202/D176(a) have large
sections — grep within or offset/limit). Never linear-read.
- `docs/dev/findings.md` — the `Dxx` finding log (§F + §H).
**Jump to a specific `Dxx` via the index at the top of §F.** Never
linear-read.
- `docs/dev/HANDOFF-ARCHIVE.md` — prior-session narrative (M-2…). Only
when tracing the history of one fix.
- `docs/dev-process.md` — before dispatching a subagent (also Tier 1
@@ -55,9 +54,6 @@ A subagent brief is INVALID and must not be sent unless it contains all of:
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.
- **VERIFY** — the exact `tools_pc/verify.sh` / probe invocation that proves
the task done, or an explicit "runtime verification impossible here; a human
must run X".
- Instruction to read `docs/porting-notes.md` first and append any
generalisable quirk to it.
@@ -89,27 +85,9 @@ still owns judgment, planning, and the verification gate.
- **Backend reality:** ~1 inference slot, slow per token — dispatch
**sequentially**, `max_tokens` ~4–8K, treat as batch work. Needs the LiteLLM
proxy (`litellm/run.ps1`) + Unsloth Studio both up.
- **Corrected 2026-09-04:** the "worker's shell is `cmd.exe`, no grep/cat"
claim that used to live here was WRONG — verified directly against the
harness's own `_run_bash` (`C:\Users\james\tools\claude-code-delegate-local`):
`grep`, `sed`, `cat`, `ls`, `head`, pipes, and `grep -rn` all work fine
(Git-for-Windows/MSYS2 toolchain on PATH). The stale claim, baked into 4 of
5 local agent `.md` files, caused a real dispatch (a multi-file
cross-referencing investigation) to burn its whole turn budget
full-`read_file`-ing files instead of grepping them, hit the turn limit, and
returned nothing. Agent files fixed. Always still spot-check the returned
artifact against the real files before integrating — the 27B will
confidently fabricate if a read fails, independent of this.
- **Landing delegate output on its own branch: branch off `main` explicitly,
never off whatever's currently checked out.** (2026-09-12: a delegate's
docs-only writeup got `git branch <name> <sha>` where `<sha>` happened to
sit on top of an unrelated in-flight feature branch — the resulting PR
silently carried that branch's whole history, including code that had
since been reverted, and it only surfaced as a CI failure (a stale
env-probe-index check), not from inspecting the diff.) Before creating the
branch, confirm you're branching from `main`/`origin/main` HEAD, not from
`HEAD` of the session's current checkout; after pushing, sanity-check with
`git log --oneline main..<branch>` that it contains only the commit(s) the
delegate's task actually produced.
- **Windows quirk:** the worker's shell is `cmd.exe`, not bash — its agent
prompt tells it to use `read_file`/`findstr`, not `cat`/`grep`. Always
spot-check the returned artifact against the real files before integrating;
the 27B will confidently fabricate if a read fails.
- Fits the **dispatch preflight** rules above (FILES / BUDGET / ON EXPIRY /
CONSTRAINTS / REPORT) — write the `task` string to that shape.
-6
View File
@@ -145,12 +145,6 @@ execute_process(COMMAND git rev-parse --short HEAD
if(NOT VERSION_HASH)
set(VERSION_HASH "unknown")
endif()
# Provenance, printed by --version and at startup (port/src/main.c): a build
# keeps pointing at this repository no matter who compiles or renames it.
set(VERSION_CODENAME "larkspur")
set(ORIGIN_URL "https://github.com/jkdansereau/goldeneye-pc-port")
configure_file(
"${CMAKE_SOURCE_DIR}/port/include/versioninfo.h.in"
"${CMAKE_BINARY_DIR}/port/include/versioninfo.h" @ONLY)
+6 -31
View File
@@ -11,19 +11,12 @@ keep the port faithful to the original game.
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; diagnosis is always allowed.** Trace a bug as
deep into the decomp's state as the evidence leads — that's never
restricted. Only the *fix* is constrained: the 32→64-bit transition
forces a small class of mechanical, semantics-preserving edits — any
struct-layout or pointer-width-driven misread caused by 32→64-bit
widening, in a ROM-serialized record or a live runtime struct/union alike
(see `docs/porting-notes.md` §A1 for the pattern and its tells). These
are allowed, must be guarded with `#ifdef PORT`, must change no
behaviour, and must be documented. A genuine behavioral difference from
real N64 behavior is a separate, much rarer case that needs explicit
maintainer sign-off first — see `docs/dev-process.md`'s rule-2 sign-off
procedure; it is not something a contributor can self-approve or infer
from this exception.
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.
@@ -39,24 +32,6 @@ keep the port faithful to the original game.
- Port-layer code is plain C11 / C++17, SDL2 + OpenGL, no extra dependencies.
- Keep commits focused and describe *why*, not just *what*.
## Forks and derivative works
Fork away — the code is MIT and the `port/` layer exists to be extended.
Rebranding, localizing and extending are all welcome, and none of them need
permission. If you carry the work forward under your own name, please keep
two things:
- **Attribution.** A clear statement, in your README and about page, that the
project derives from this one — with a link. The engine prints its origin at
startup and in its `--version` output; keeping that intact is part of the
ask.
- **The record.** `docs/dev/` (the finding log, porting notes, per-level
status) is the engineering record behind every fix. Derivative projects are
asked to preserve or link to it rather than fork-and-forget.
Stripping the attribution is the one thing that turns a fork into something
we no longer recognize as a descendant of this project.
## Where to look
- `docs/internals.md` — how the port is structured.
+250 -287
View File
@@ -1,259 +1,232 @@
# GoldenEye 007 PC Port
[![CI](https://github.com/jkdansereau/goldeneye-pc-port/actions/workflows/ci.yml/badge.svg)](https://github.com/jkdansereau/goldeneye-pc-port/actions/workflows/ci.yml)
![status](https://img.shields.io/badge/status-alpha_v0.1.0_(Phase_2_of_4)-orange)
![tested on](https://img.shields.io/badge/tested_on-Windows_%2B_Linux_x86--64-blue)
![license](https://img.shields.io/badge/license-MIT-green)
![built with](https://img.shields.io/badge/built_with-coding_agents-8A2BE2)
A native PC port of _GoldenEye 007_ (Rare, 1997, Nintendo 64), compiled from
the [GoldenEye 007 decompilation](https://github.com/n64decomp/007): the
original N64 game running from reconstructed source, not the Xbox 360
remaster. The N64's graphics coprocessor (RSP) is emulated in software; every
other hardware surface (video, audio, input, timers, save storage) is shimmed
in a dedicated `port/` layer, following the architecture of the
[Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark), the same
Rare "Indy" engine family, one hardware generation apart.
**A native PC port of _GoldenEye 007_ (Rare, 1997, Nintendo 64), compiled from
the [GoldenEye 007 decompilation](https://github.com/n64decomp/007) — the
original N64 game running from reconstructed source, not the Xbox 360 remaster.**
The first alpha, **[v0.1.0](../../releases)**, is out for Windows and Linux: it
boots, renders the full intro and front end, and runs all 21 solo missions — in
a full-campaign playtest 19 of the 21 were completable start to finish (two
levels still crash). **There is no audio yet** and rough edges remain; this is
Phase 2 of 4. See [Status](#status) and [Background](#background).
**v0.3.0** is out for Windows and Linux (including Steam Deck, where the
Linux bundle sideloads as-is) and runs the full campaign at a steady 60 fps
with known rough edges ([Status](#status)). Free to download, build on and
modify (you bring the ROM).
It is also a **research project on AI-agent collaboration in a large,
unfamiliar, low-level codebase** — how far two coding agents (a local
open-weight model and Claude), driven by one person part-time, can be pushed
through ~230 translation units of unmodified big-endian MIPS game code and made
to run on a 64-bit desktop. See [Background](#background).
**This is a pre-1.0 release, not a finished product.** v1.0 is the target
for a polished, feature-complete build; until then, expect rough edges,
missing features, and breaking changes between versions. See
[Status](#status) for what works today and [Roadmap](#roadmap) for where
this is headed.
**AI disclosure:** development here was agentic - Claude Pro plus a local
open-weight model on a single RTX 5090, as of August–September 2026. This
project is as much a study of *that process* as it is a port: whether
current LLMs can carry a codebase like this, and what actually goes wrong
along the way. Judge the result for yourself.
I'm one person doing this in my spare time, not a team. See
[Background](#background) for the full setup, timeline, and an honest
account of what worked and what didn't.
Technically, 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 (RSP) is
emulated in software; every other hardware surface (video, audio, input,
timers, save storage) is shimmed in a dedicated `port/` layer.
> [!IMPORTANT]
> **You must supply your own GoldenEye 007 ROM.** This repository contains no
> Nintendo code or assets, and no ROM. Nothing here is distributable as a
> playable game: see [Requirements](#requirements) and [Legal](#legal).
> playable game — see [Requirements](#requirements) and [Legal](#legal).
<p align="center">
<img src="docs/media/goldeneye-gh-preview.gif" width="64%"
alt="~12 s gameplay montage from live play sessions">
<br><em>All in-engine, running in the port — a ~12&nbsp;s gameplay montage
from live v0.3.0 play sessions.</em>
<img src="docs/img/attract-bunker1.png" width="32%" alt="Bunker 1 intro camera">
<img src="docs/media/goldeneye-demo.gif" width="32%" alt="~15 s of the port running: mission dossier, Facility, Silo, Jungle, Archives">
<img src="docs/img/attract-dam.png" width="32%" alt="Dam intro camera">
<br><em>In-engine, running in the port — Bunker&nbsp;1 and Dam attract views, and a
~15&nbsp;s clip (no audio yet, Phase&nbsp;3): mission dossier &rarr; Facility &rarr; Silo &rarr;
Jungle &rarr; Archives.</em>
</p>
## Download
## Quick start
| Platform | Bundle | Notes |
|---|---|---|
| **Windows** (x86_64) | [win64.zip](https://github.com/jkdansereau/goldeneye-pc-port/releases) | Engine + runtime DLLs + the one-time asset tool. |
| **Linux** (x86_64) / **Steam Deck** | [linux tarball](https://github.com/jkdansereau/goldeneye-pc-port/releases) | SDL2 is bundled, so it runs as-is on any distro, and sideloads onto a Deck with nothing installed. |
You supply your own **NTSC-U GoldenEye 007 N64 ROM** (`.z64`, big-endian) — no
ROM or game asset is included or distributed. Then:
Both bundles contain **no ROM and no game assets**: you supply your own
(see [Requirements](#requirements)), which keeps the release legal to
distribute. Earlier builds: v0.2.2, v0.2.1, v0.2.0, and v0.1.0 alpha, same
page. You can also build it yourself; see [Building](#building).
### Quick start
You need a GoldenEye 007 N64 ROM (`.z64`, big-endian). This release supports
the **NTSC-U (US)** version; PAL and JP are on the roadmap ([issue
#85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)). No ROM or game asset is
included or distributed. Then:
1. Download the Windows or Linux bundle from [Releases](https://github.com/jkdansereau/goldeneye-pc-port/releases) and unpack it.
1. Download the Windows or Linux bundle from [Releases](../../releases) and unpack it.
2. Make a `data/` folder next to the executable and drop the ROM in as `ge007.ntsc-final.z64`.
3. Launch the executable from that folder. The first run takes a few extra seconds: it detects the ROM and generates the derived asset folders once (no Python or other tooling needed).
3. Run the one-time asset step: `python3 prepare-assets/prepare-assets.py` (Python 3.8+, stdlib only).
4. Launch the executable from that folder.
Read the [Status](#status) caveats first: v0.3.0 has known rough edges,
listed plainly there.
Building from source instead: see [Building](#building). Read the
[Status](#status) caveats first — this is an early alpha.
## Status
## Contents
**v0.3.0 - playable, with known rough edges.** The full single-player
campaign is completable end to end (all 21 missions, Agent difficulty,
playtested), at a steady 60 fps; all 21 solo missions load, render and run
crash-free, verified on Windows, Linux and real Steam Deck hardware. Feedback
is very welcome.
What a release actually installs (no networking, no telemetry, no ROM or
game assets shipped) and how faithfully the port tracks the original N64
game's logic: [Security & fidelity status](docs/security-and-fidelity-status.md).
**Working:** boot sequence and front end (menu → mission select → briefing →
start), front-end menu navigation on the left stick to match the F10 overlay
(D282); all 21 solo missions load, render and are crash-free (full campaign
playtested end to end at Agent difficulty); steady 60 fps
(software RSP off the presentation critical path); full audio: in-level music and SFX; keyboard + mouse (click-to-lock, proportional aim mode, a single
simplified sensitivity control) and a modern dual-stick controller layout;
automatic widescreen FOV scaling; Bond is fixed in cutscenes (no more
floating or spin-glitching) and his third-person model positioning generally
is right the large majority of the time now, at most a small drift when off; file-backed saves; faithful N64 progression
by default (F10 → *All unlocked* opens every level, 007 mode and the full
cheat menu); F10 in-game options overlay (resolution, frame cap, MSAA,
filtering, FOV, sensitivity); Windows and Linux, including Steam Deck.
**Known issues:**
- Particle colours: a real fix landed (a corrupted-vertex-buffer bug), but
the rainbow effect can still occur intermittently on either platform, not
every time or on every level — a second cause hasn't been found yet
(D252).
- Some muzzle flashes draw an extra, erroneous long flash straight up from
the gun (seen on the M16, among others), overlaid on the normal, correctly
drawn flash. Cosmetic only (D303).
- Water levels show a moving seam between two water patterns (D245).
- Occasional z-fighting on some levels' geometry (D308) — a Dam intro/
truck-wheel instance showing odd transparent-looking areas may be related
(D306). Minor and cosmetic.
- In-level security camera props (not the player's own view) can
occasionally end up facing backwards on some levels, seen on Bunker
(D307).
- Rarely, Bond's model can briefly render out of place after an animation
transition — purely visual: no effect on gameplay or saves, and it clears
on its own or by re-entering the level (D311).
- On Facility, if gas leaks during Ourumov's monologue he can pause for up to
~10 s before resuming the scripted shootout — a latent race that exists in
the N64 original too (where it softlocks permanently); the port detects and
auto-recovers it (D318).
- Some front-end 3D models are off: the spinning Nintendo logo renders as two
white blobs and the Rareware logo's texture filtering looks wrong (D75).
- Surface 1's 2D billboard trees render as a solid wall of tree texture
instead of discrete sprites (D236). Under active investigation.
- **Widescreen is stretched, not native**: on non-4:3 windows the whole
frame (world and HUD) is stretched horizontally to fill your display —
about 33% wider than original at 16:9, like a 4:3 video in a TV's "stretch"
mode. Automatic FOV scaling keeps the framing comfortable and gameplay is
completely unaffected (all game logic runs in world space), but shapes are
subtly wider than on the N64 — most visible on round objects (barrels, the
radar). A distortion-free native widescreen render is on the roadmap.
- Distant geometry can drop out on the biggest open levels (Streets,
Egyptian) at default FOV — a culling/LOD issue that sometimes
self-corrects as you keep moving (D249).
- Gunshot SFX can sound off during sustained/rapid fire: cadence can drift
from the N64 original's rate, and PP7/AK47 fire can occasionally go silent
under heavy automatic fire near another looping sound (D240/D241).
- **`All unlocked` is highly experimental — don't enable it until you have
at least one save written** (complete a level normally first, e.g. Dam on
Agent). Enabling it on a brand-new install with no prior save can still
cause silent audio and odd right-mouse-aim behavior (D257/D259/D281).
- Assorted further cosmetic defects are tracked in
[`docs/dev/GRAPHICS-BACKLOG.md`](docs/dev/GRAPHICS-BACKLOG.md).
- No macOS or ARM support; no controller rebinding UI.
Root causes and fix status for every item: the [release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases)
and the finding log in [`docs/dev/findings.md`](docs/dev/findings.md).
### Steam Deck
The Linux bundle is the Deck build. SFTP it over from your PC, or download
it straight from the [releases page](https://github.com/jkdansereau/goldeneye-pc-port/releases) on the Deck itself:
unzip, drop your ROM in `data/`, launch it once (the first run generates the
derived assets), and add the executable as a non-Steam game. SDL2 is bundled, so no dependencies need
installing. On SteamOS the first launch seeds `ge007.ini` with Deck-friendly
defaults: native 1280×800 fullscreen, VSync, MSAA 4, and 150% draw/LOD
distance (the authored N64 fade distances read short on the close-up panel);
everything is changeable in the options overlay and persists afterwards.
**Do that first launch in Game Mode, not Desktop Mode** — an ini created by
an earlier Desktop Mode launch (e.g. while testing before adding it as a
Steam shortcut) permanently skips the Deck preset, since any existing ini
always wins over it (D283). If your resolution isn't 1280×800 on first
Game Mode boot, just set it manually: F10 → *Resolution*. The renderer is
CPU-bound (software RSP); expect original N64-era
performance at 60 fps rather than more. This release was playtested on real
Deck hardware; the v0.1.0-era Facility crash (D203) did not recur: its root
cause was identified and fixed (D253), and a separate intermittent SIGSEGV
in heavy firefights/terminal destruction (D255) and an audio-thread crash
(D305) are both fixed and live-verified on real hardware as of this release.
**In-game settings on the Deck.** The options overlay is fully gamepad-driven:
it opens with **Select**, the D-pad or left stick (up/down) moves between
options, **A** steps the selected option forward, **B** steps it back, and
**Start** (or Select again) closes. Toggles flip, resolution / MSAA /
filtering cycle, sliders step in increments. With a keyboard attached the same
overlay is `F10` + arrows/Enter.
## Roadmap
No fixed timeline or committed feature list — this is spare-time work — but
directionally, on the way to v1.0:
- Working through the [known issues](#status) above and the fuller list in
[`docs/dev/findings.md`](docs/dev/findings.md).
- **PAL and JP ROM support** ([issue #85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)); NTSC-U is the only supported region today.
- **Native widescreen** — a distortion-free render at your display's aspect
(correct-aspect projection and a wide-frame HUD), replacing today's
stretched 4:3 frame + FOV compensation.
- **Controller rebinding UI**, and macOS/ARM builds.
- **LAN multiplayer**: reviving GoldenEye's original split-screen/deathmatch
netplay across multiple PCs on a local network. Genuinely under
consideration, but early and not started; no ETA.
- General polish: performance, remaining rendering/audio defects, save/config
robustness.
Not currently planned: new game modes GE never shipped (e.g. co-op), online (non-LAN)
multiplayer, ray tracing. If any of these matter to you, open an issue —
it helps prioritize.
## Beyond playing
- **Tweak it**: `ge007.ini` and the F10 in-game overlay expose resolution,
frame cap, MSAA, texture filtering, FOV/draw distance and mouse feel;
launch with `-fresh` for a clean-slate run.
- **Read it**: [`docs/internals.md`](docs/internals.md) maps the
architecture and the software RSP; [`docs/porting-notes.md`](docs/porting-notes.md)
is the catalogue of N64→PC bug classes hit along the way. Game logic in
`src/` is unmodified decompilation; every hardware surface lives in the
MIT-licensed `port/` layer.
- **Mod it**: the port layer, build system and `tools_pc/` are yours to
extend (see [License](#license)); [`CONTRIBUTING.md`](CONTRIBUTING.md) has
the ground rules for getting changes in, and [`docs/dev/`](docs/dev/) is
the raw engineering record behind every fix.
- [Quick start](#quick-start)
- [Background](#background)
- [How this differs from the other GoldenEye PC projects](#how-this-differs-from-the-other-goldeneye-pc-projects)
- [Status](#status)
- [Download](#download)
- [Requirements](#requirements)
- [Building](#building) · [Windows (MSYS2)](#windows-msys2) · [Linux](#linux)
- [Running](#running) · [Default controls](#default-controls)
- [How it works](#how-it-works)
- [Project layout](#project-layout)
- [Documentation](#documentation)
- [Credits](#credits)
- [Legal](#legal)
- [License](#license)
## Background
The port was built by two coding agents, a local open-weight model
(`unsloth/Qwen3.8-27B-GGUF` on one RTX 5090, via the [pi](https://pi.dev/)
agent) doing the groundwork (build, boot chain, software-RSP integration,
asset pipeline, first frames), and **Claude Code** (Sonnet 5, Opus 5 for the
hardest bugs) joining for the collaborative phase (the 21-level sweep, the
ABI finding catalog, SDL input, front end), handing work back and forth
through shared written notes, directed by one person part-time. In short:
~3.5 weeks, ~430 commits, 200+ bugs logged and tracked to resolution.
Beyond the port itself, this is primarily a **research project on agentic
software development** — specifically, on AI-agent collaboration in a large,
unfamiliar, low-level codebase: how far coding agents can be driven, by one
person working part-time, through ~230 translation units of unmodified
big-endian MIPS game code, made to run on a 64-bit desktop.
The full write-up (timeline, handoff mechanism, effort breakdown, an honest
"what worked / what didn't"): [`docs/dev/agentic-development.md`](docs/dev/agentic-development.md).
The workflow itself: [`docs/dev-process.md`](docs/dev-process.md). To cite the
project or its findings: [`CITATION.cff`](CITATION.cff) (GitHub's "Cite this
repository" menu).
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 catalog, the SDL input layer, and the front-end flow.
The two **handed work back and forth** through shared written artifacts:
```mermaid
flowchart TD
H["human<br/>direction · integration · playtest"]
C["Claude<br/>(Claude Code)"]
Q["Qwen 3.8<br/>(pi, local RTX 5090)"]
D[("shared artifacts<br/>findings · notes · handoff doc")]
H -->|"scoped task + budget"| C
H -->|"scoped task + budget"| Q
C -->|"patch + write-up"| D
Q -->|"patch + write-up"| D
C <-.->|handoff| Q
D --> 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:**
| Metric | Value |
|---|---|
| Timeline | ~3 weeks, one person part-time |
| Commits | ~320 |
| Root-caused bugs logged | ~190 (`D1`–`D196`; some later merged or withdrawn) |
| Handoff sessions | ~49 |
| ABI edits to game code | 69 files, all `#ifdef PORT` |
| Port layer / tooling | ~17k lines C/C++ · ~6k lines Python |
| Effort split (milestone-weighted) | ~60% Claude · ~40% local model |
The local model built the entire foundation — build, boot chain, RSP wiring,
asset converters — then both agents ran the debugging phase together:
- **Day 4** — the full ~230-unit codebase compiles and links
- **Day 8** — the whole intro renders
- **Day 13** — all 21 solo missions run crash-free
- **Day 14** — front end playable end to end
- **Week 3** — v0.1.0 alpha; a full-campaign playtest completes 19 of 21 missions
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). To cite this
project or its findings, use [`CITATION.cff`](CITATION.cff) (GitHub's "Cite
this repository" menu).
## How this differs from the other GoldenEye PC projects
This is a native port of the original 1997 Nintendo 64 game, built from its
actual reconstructed source code, the same lineage as the Perfect Dark PC
port. The other well-known "GoldenEye on PC" projects are something
different: they machine-translate the shipped binary of the *unreleased Xbox
360 XBLA remaster*, a different game on a different codebase with no shared
code with this one.
This is a **native port of the original 1997 Nintendo 64 game, built from its
actual reconstructed source code** — the same lineage as the Perfect Dark PC
port. The other well-known "GoldenEye on PC" projects are something different:
they machine-translate the shipped binary of the *unreleased Xbox 360 XBLA
remaster* — a different game, a different codebase, no shared code with this.
| | This project | The other well-known "GoldenEye on PC" recompilation projects (and their Steam Deck builds) |
| | This project | [GoldenEye-Recomp](https://github.com/SunJaycy/GoldenEye-Recomp) / [Steam Deck build](https://github.com/couchk1ng/GoldenEye-Recomp-SteamDeck) |
|---|---|---|
| **What it ports** | The original **Nintendo 64** game (1997) | The **Xbox 360 XBLA** HD remaster (built ~2007, never released) |
| **How** | Decompilation-based source port: human-reconstructed C, compiled for the host; game logic runs as written | Static binary recompilation: the shipped machine code is auto-translated to C; no source-level understanding |
| **How** | **Decompilation-based source port** — human-reconstructed C, compiled for the host; game logic runs as written | **Static binary recompilation** — the shipped machine code is auto-translated to C; no source-level understanding |
| **Lineage** | [GoldenEye 007 decompilation](https://github.com/n64decomp/007) + [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark) engine family | Xbox 360 "…Recompiled" static-recompilation family |
| **Renderer** | Software RSP → OpenGL | Hardware (Vulkan) |
| **Status** | v0.3.0 public release; full campaign playable at 60 fps (see [Status](#status)) | Playable full game |
| **Why it exists** | To run the *original* N64 game from source, and as a [case study in AI-agent collaboration](#background) on a hard low-level codebase | To get a playable PC release of the remaster |
| **Status** | WIP, Phase 2, alpha v0.1.0 — runs all 21 levels, 19/21 completable in a full playthrough; no audio; rough edges | Playable full game, higher frame rates, online multiplayer |
| **Why it exists** | A [case study in AI-agent collaboration](#background) on a hard low-level codebase; the port is the target, not a product | A polished, playable PC release of the remaster |
They answer a different question: how to get the *remaster* onto PC by
machine translation. This project answers how to get the *original 1997
game* onto PC, running from its reconstructed source.
**If you just want to play GoldenEye on PC today, use one of the recompilation
projects — they are finished and this is not.** What is interesting here is the
other half: getting the *original* game running from source, and how much of
that work was driven by AI agents.
## Status
> [!WARNING]
> **Alpha — playable, not polished.** This is a work-in-progress research port.
> It runs the full single-player campaign, but there is **no audio**, some
> front-end 3D models and cutscenes are broken, AI characters move too slowly,
> input has known rough edges, and two levels (Bunker ii, Statue) still crash.
> Treat it as an early alpha of the porting work, not a finished way to play
> GoldenEye.
**Phase 2 of 4 (rendering). First alpha: [v0.1.0](../../releases).** The port
boots, renders, and plays through the front end and the campaign. It is not
finished and it is not fully 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 and render.** In a full-campaign playtest on the
packaged Windows build, **19 of 21 were completable start to finish**; Bunker
ii and Statue crash mid-level (one root cause, tracked as D191). 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.
- **Windows and Linux** (`x86_64`). Windows is the primary development and
playtest path; the Linux build boots, renders, and passes the level sweep,
with far less human playtime.
**Not yet working**
- **Audio** — not implemented (Phase 3). The game runs silent.
- **AI pacing** — scripted and combat AI characters travel to their
destinations noticeably slower than on N64. This breaks Cradle (the final
level) via Trevelyan's scripted progression. Top post-alpha fix (D193).
- **Cutscenes** — frequently glitch: skipped, wrong camera, misplaced or
hovering actors, wrong timing (D148/D160).
- Some front-end 3D models — the spinning Nintendo logo, and the MISSION
COMPLETE / mode-select models — are mispositioned or absent. (The
gun-barrel Bond intro renders correctly.)
- Outdoor levels render with a **black sky**; assorted other cosmetic defects
are tracked in
[`docs/dev/GRAPHICS-BACKLOG.md`](docs/dev/GRAPHICS-BACKLOG.md).
- No macOS or ARM support; no controller rebinding UI; no widescreen.
Next up: audio (Phase 3), the AI-pacing fix, and the two remaining level
crashes. Cosmetic defects are tracked in
[`docs/dev/GRAPHICS-BACKLOG.md`](docs/dev/GRAPHICS-BACKLOG.md).
## Download
Pre-built **Windows** and **Linux** `x86_64` bundles are published under
[Releases](../../releases), starting with **v0.1.0**. Each contains the engine
executable, its runtime libraries, and the one-time `prepare-assets` tool —
**no ROM and no game assets**. See [Quick start](#quick-start) for the four
steps to get it running, [Requirements](#requirements) for accepted ROMs, and
[Status](#status) for the alpha caveats.
You can also build it yourself; see [Building](#building).
## Requirements
You need a GoldenEye 007 (Nintendo 64) ROM that you legally own, in
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 |
@@ -262,9 +235,7 @@ big-endian (`.z64`) format, matching one of:
| PAL (EU) | `pal-final` | `ge007.pal-final.z64` | `167c3c433dec1f1eb921736f7d53fac8cb45ee31` |
| NTSC-J (JP) | `jpn-final` | `ge007.jpn-final.z64` | `2a5dade32f7fad6c73c659d2026994632c1b3174` |
This release supports the US (NTSC-U) version; PAL and JP ROMs are
recognised by region but not yet supported (on the roadmap, [issue
#85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)).
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
@@ -290,13 +261,13 @@ 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 # NTSC-U; PAL/JP engine builds compile, but their asset sidecars are not yet producible (issue #85)
./build-pc.sh ntsc-final # or pal-final / jpn-final
```
### Linux
The Linux build is compiled on every push by CI (ubuntu-24.04); the release
bundle additionally bundles SDL2 so no system packages are needed at runtime.
> **Untested.** No one has built or run the port on Linux. The steps below are
> the intended path, not a verified one — expect to fix build issues yourself.
```sh
sudo apt install build-essential cmake python3 libsdl2-dev zlib1g-dev libgl1-mesa-dev
@@ -314,53 +285,55 @@ The executable is written to `build-pc/ge007.x86_64` (on Windows,
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:
3. Run the executable **from the repo root**:
`./build-pc/ge007.x86_64`.
Configuration is written to `ge007.ini` on first run; game progress
lives in `ge007.eep`. Launch with `-fresh` to wipe both before starting
(a clean-slate run for playtesting).
Configuration is written to `ge007.ini` on first run.
### Default controls
| Action | Keyboard / mouse | Controller |
|---------------------|--------------------------|---------------|
| Move / strafe | `W` `A` `S` `D` / arrows | Left stick (or D-pad) |
| 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` / `X` | A / X |
| Reload / cancel (B) | `R` / `F` | B / Y |
| Next weapon | Mouse wheel up | RB |
| Previous weapon | Mouse wheel down | LB |
| Use / accept (A) | `Space` / `E` / `X` | A / X |
| Reload / cancel (B) | `R` / `F` | B / Y / RB |
| L trigger | `Q` | LB |
| Start | `Enter` / `Tab` | Start |
| Options overlay | `F10` | Select (D-pad/stick + A/B navigate, Start closes) |
The controller layout follows the modern dual-stick scheme used by the
console re-releases (left stick move, right stick look, triggers fire/aim,
shoulders cycle weapons).
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 ground truth. Everything that would touch N64
hardware is redirected into `port/`: a **software RSP** (`port/fast3d/`,
adapted from the Perfect Dark port) that interprets the GBI display list the
game builds each frame and emits OpenGL, bypassing the RDP; a scheduler
replacement (`port/src/gesched.c`) that drives it directly; single-threaded
libultra OS shims (`port/src/libultra.c`); and SDL2/OpenGL/filesystem backends
for video, audio, input and storage. The 32→64-bit transition forces a small,
cataloged class of mechanical ABI-only edits to ROM-serialized structs
(pointer-width reconciliation); these change no behavior and are documented
individually. Where it diverges from the Perfect Dark port: GoldenEye's N64
serialized asset formats are converted offline by Python "sidecar" converters
in `tools_pc/` rather than fixed up at load time. Full detail:
[`docs/internals.md`](docs/internals.md) and
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, cataloged class of mechanical
ABI-only edits to ROM-serialized structs (pointer-width reconciliation); these
change no behavior 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
@@ -377,17 +350,18 @@ docs/ see below
## Documentation
Key docs are also published as a site:
<https://jkdansereau.github.io/goldeneye-pc-port/>.
**<https://jkdansereau.github.io/goldeneye-pc-port/>**.
| Doc | What's in it |
|---|---|
| [`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/game-behavior-reference.md`](docs/dev/game-behavior-reference.md) | How the retail N64 game is meant to behave — combat/AI model, difficulty scaling, per-level objectives, timers, weapon data, original-game quirks. Playtest reference. |
| [`docs/dev/agentic-development.md`](docs/dev/agentic-development.md) | The research angle: the two-agent setup, timeline, handoff workflow, and an assessment of what did and didn't work. |
| [`docs/dev-process.md`](docs/dev-process.md) | The investigation workflow in detail: budgets, file partitioning, the finding-log discipline. |
| [`docs/dev/`](docs/dev/) | The raw engineering record: the full finding log, per-level status, graphics backlog, playtest matrices, and [`docs/dev/game-behavior-reference.md`](docs/dev/game-behavior-reference.md) (a secondary-sourced playtest reference for how the retail game is meant to behave; repo-only, code is ground truth). |
| [`docs/SetupGuide.md`](docs/SetupGuide.md), [`docs/StyleGuide.md`](docs/StyleGuide.md) | Inherited from the decompilation this repo forks. |
| [`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
@@ -395,30 +369,24 @@ This port is a thin layer on a large amount of other people's work.
**Prior work it is built on**
- The [GoldenEye 007 decompilation](https://github.com/n64decomp/007), years of
- The [GoldenEye 007 decompilation](https://github.com/n64decomp/007) — years of
effort by Larry Ficken ("kholdfuzion") and the project's contributors; plus
zoinkity's GoldenEye documentation, which the decomp started from. This port
is a fork of that repository.
- The [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark)
(Ryan Dwyer and contributors), the reference architecture for this port and
(Ryan Dwyer and contributors) — the reference architecture for this port and
the source of the `fast3d` software RSP.
- The [Perfect Dark decompilation](https://github.com/n64decomp/perfect_dark),
- The [Perfect Dark decompilation](https://github.com/n64decomp/perfect_dark) —
the sibling decomp the PD port is built on.
- **Carnivorous**: author of the *Mouse Injector* input plugin for 1964 (the
"GEPD Edition" bundle). Its mouse-aim behaviour for GoldenEye/Perfect Dark is
what the in-game GEPD-style aim mode is modelled on; the implementation here
is an independent reimplementation of that behaviour, not derived code.
Thanks also to **Rice** and **schibo** of the 1964 team for the emulator it
shipped with.
**Vendored / adapted code**
- `port/fast3d/`: the software RSP, adapted from the PD port. It originates
- `port/fast3d/` — the software RSP, adapted from the PD port. It originates
with the [Ship of Harkinian](https://github.com/HarbourMasters) /
libultraship fast3d (© Emill, MaikelChan; MIT, see `port/fast3d/LICENSE.txt`),
libultraship fast3d (© Emill, MaikelChan; MIT — see `port/fast3d/LICENSE.txt`),
which itself descends from
[sm64-port](https://github.com/sm64-port/sm64-port)'s fast3d and audio mixer.
- `port/fast3d/glad/`: OpenGL loader generated by
- `port/fast3d/glad/` — OpenGL loader generated by
[glad](https://github.com/Dav1dde/glad) (David Herberth, MIT).
- The decompilation toolchain: [`ido-static-recomp`](https://github.com/decompals/ido-static-recomp)
(Emill / decompals), [`qemu-irix`](https://github.com/n64decomp/qemu-irix) and
@@ -428,22 +396,17 @@ This port is a thin layer on a large amount of other people's work.
**Methodology**
- Chris Lewis, [*"Decompiling a Nintendo 64 Game in 84 Days"*](https://blog.chrislewis.au/decompiling-a-nintendo-64-game-in-84-days/) (the Snowboard
Kids decompilation write-up), the agent-workflow practices in
Kids decompilation write-up) — the agent-workflow practices in
[`docs/dev-process.md`](docs/dev-process.md) are adapted from it.
**Consultation**
- **f1zz1ec0ke** ([GitHub](https://github.com/f1zz1ec0ke)): LLM consultation on model
tuning, agent harnesses, and agentic strategy throughout the port's development.
**Tools and models used to develop the port**
- Qwen 3.8 (Alibaba Qwen team), run locally from the
- Qwen 3.8 (Alibaba Qwen team), run locally as the
[`unsloth/Qwen3.8-27B-GGUF`](https://huggingface.co/unsloth/Qwen3.8-27B-GGUF)
repo — `UD-Q4_K_XL` as the primary quant, with other quants used along the
way; [Unsloth](https://unsloth.ai) (the GGUF quantisation and Unsloth
Desktop) as the local model server.
- [pi](https://pi.dev/): the local coding-agent harness.
`UD-Q4_K_XL` quant;
[Unsloth](https://unsloth.ai) (the GGUF quantisation and Unsloth Desktop,
used as the local model server).
- [pi](https://pi.dev/) — the local coding-agent harness.
- [Claude / Claude Code](https://claude.com/claude-code) (Anthropic).
## Legal
@@ -452,7 +415,7 @@ This is a non-commercial fan preservation/research project, in the same
category as the many other N64 decompilation and native-port repositories on
GitHub. It follows the same conventions they do:
- **No ROM and no game assets are distributed**: not in this repository and
- **No ROM and no game assets are distributed** — not in this repository and
not in any release. Textures, audio, models, level data and in-game text are
extracted from a ROM *you already own*, on *your* machine, at build time.
- The repository is a fork of the public
@@ -461,11 +424,11 @@ GitHub. It follows the same conventions they do:
- No official logos, box art, or marketing assets are used. "GoldenEye 007",
"007", "James Bond" and related marks belong to their respective owners
(Nintendo, Microsoft/Rare, MGM, Danjaq, EON Productions).
- Pre-built binaries published under [Releases](https://github.com/jkdansereau/goldeneye-pc-port/releases) contain
only the engine (the `port/` layer plus the compiled decompilation, with no
game data of any kind), bundled with permissively-licensed runtime libraries
- **Pre-built binaries** published under [Releases](../../releases) contain
only the engine — the `port/` layer plus the compiled decompilation, with no
game data of any kind — bundled with permissively-licensed runtime libraries
(SDL2, zlib, the MinGW runtime; their licenses travel in the download). Any
build, yours or ours, is useless without a ROM you supply.
build — yours or ours — is useless without a ROM you supply.
This project is **not affiliated with, endorsed by, or sponsored by** Nintendo,
Rare, Microsoft, MGM, Danjaq, EON Productions, or any rights holder in
@@ -474,7 +437,7 @@ 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;
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
View File
@@ -1,10 +1,5 @@
# Setup Guide
> Inherited from the decompilation this repo forks. It covers building the
> **N64 ROM** with the IDO toolchain — needed only for the one-time
> asset-extraction step referenced from [`building.md`](building.md), not for
> building or running the PC port itself.
<!-- TOC -->
* [Setup Guide](#setup-guide)
* [Prerequisites](#prerequisites)
+39
View File
@@ -0,0 +1,39 @@
# Project Structure
```
goldeneye_src
|-- .github/workflows: GitHub use only
├── assets: game assets
│   ├── font: font data
│   ├── images: image data
│   │   └── split: split image data
│   ├── music: music data
│   ├── obseg: animation data
│   │   ├── bg: bg data
│   │   ├── brief: briefing data
│   │   ├── chr: c model data
│   │   ├── gun: g model data
│   │   ├── prop: p model data
│   │   ├── setup: setup data
│   │   ├── stan: stan data
│   │   └── text: text data
│   └── ramrom: demo data
├── bin: files that haven't been touched
├── build: output directory
├── include: header files
├── rsp: Custom GBI code (Assembly) (C0 and 4Tri)
├── src: C source code for game
│   ├── game: core ge specific code 0x7f000000 range
│   ├── inflate: statically linked initial decompression code
│   ├── libultra: currently used libultra.s
│   └── libultrarare: libultra modified by Rare
└── tools: build tools
```
Documentation has been moved to https://github.com/kholdfuzion/goldeneye_docs/tree/master/notes
Style Guide is https://github.com/kholdfuzion/goldeneye_src/blob/AIListLogic/notes/StyleGuide.md
This decompilation was only made possible thanks to many awesome 00 Agents who will be revealed only if they wish.
GE and PD documentation made by Zoinkity.
+24 -46
View File
@@ -1,12 +1,10 @@
# GitHub Pages config. Site is served from the /docs folder on the default
# branch (Settings -> Pages -> Deploy from a branch -> main / docs).
title: GoldenEye 007 PC Port
# v0.2.1 (Bing SEO): keep this SHORT -- jekyll-seo-tag splices it into the
# home page <title> whenever the page title equals the site title, and the
# long version made a ~230-char title. It also renders as the masthead
# tagline on every page. Per-page meta/og descriptions come from each page's
# front matter `description:` (see index.md), not from here.
description: N64 GoldenEye 007, decompiled for PC.
description: >-
A native PC port of the original Nintendo 64 GoldenEye 007, built from its
decompiled source, and a case study in AI-agent collaboration on a large
low-level codebase.
theme: jekyll-theme-cayman
show_downloads: false
@@ -28,46 +26,26 @@ plugins:
- jekyll-sitemap
# jekyll-relative-links (on by default for Pages) rewrites the .md links in
# these docs to .html so cross-links work on the published site. Links that
# must reach docs kept out of the site build point at the GitHub blob URL
# instead, so they resolve from the published pages too.
# these docs to .html so cross-links work on the published site.
# ---------------------------------------------------------------------------
# The site publishes a small curated set (see docs/dev/notes/DE-SLOP-DOCS-PLAN.md):
# index.md, building.md, internals.md, porting-notes.md, dev-process.md,
# dev/agentic-development.md
# Everything else under docs/ is working material - the finding log, level
# status, backlogs, plans, inherited decomp guides. It stays in the repo (and
# on GitHub) but is excluded from the site build so it never shows up as a
# published page or in sitemap.xml.
# Rolling local working files - not useful as published pages.
exclude:
- BACKLOG.md
- HANDOFF.md
- SetupGuide.md
- StructureGuide.md
- StyleGuide.md
- dev/AUDIO-PLAN.md
- dev/AUDIT-M6-player-offsets.md
- dev/D172-PARTICLE-COLOR-RESEARCH.md
- dev/D176a-SKY-NOTES.md
- dev/GE-ENV-PROBES.md
- dev/GRAPHICS-BACKLOG.md
- dev/HANDOFF-ARCHIVE.md
- dev/LEVEL-OBJECTIVES.md
- dev/LEVEL-PLAYTEST.md
- dev/LEVEL-STATUS.md
- dev/LINUX-PORT.md
- dev/M-35-PLAYTEST-CHECKLIST.md
- dev/M-35-QOL-REVIEW.md
- dev/M-36-TRIAGE.md
- dev/MODERN-PORT-FEATURES-BACKLOG.md
- dev/OPTIONS-MENU-PLAN.md
- dev/QOL-INVENTORY.md
- dev/TEXTURE-GLITCH-ANALYSIS.md
- dev/UNLOCKED-FPS-PLAN.md
- dev/VIEWMODEL-RESEARCH.md
- dev/WIDESCREEN-FOV-PLAN.md
- dev/findings-index.csv
- dev/findings.md
- dev/game-behavior-reference.md
- dev/release-dropin-rom-plan.md
# Keep the raw engineering record (finding log, per-level status, triage
# notes, backlogs) out of sitemap.xml so search engines aren't steered at it.
# The pages still build and stay reachable from cross-links; the two dev/
# docs that are linked from the site index are kept in.
defaults:
- scope:
path: "dev"
values:
sitemap: false
- scope:
path: "dev/agentic-development.md"
values:
sitemap: true
- scope:
path: "dev/game-behavior-reference.md"
values:
sitemap: true
+17 -28
View File
@@ -1,19 +1,14 @@
---
title: Building
description: Full build and asset-extraction guide for the GoldenEye 007 PC port, covering Windows (MSYS2) and Linux.
---
## Building the PC port
# Building the PC port
Stages:
1. **Extract assets from your ROM** (§2): a one-time step using the
1. **Extract assets from your ROM** (§2) — a one-time step using the
decompilation's own toolchain to pull levels, models, textures, fonts and
music into `assets/`. *(Only needed to regenerate the committed data files;
a plain `git clone` already has what the PC build compiles.)*
2. **Build the port** (§3): a CMake build compiling the game sources plus the
2. **Build the port** (§3) — a CMake build compiling the game sources plus the
`port/` layer into a native executable. Needs no ROM.
3. **Generate the PC asset sidecars** (§4): two pure-Python converters turn
3. **Generate the PC asset sidecars** (§4) — two pure-Python converters turn
ROM model / stage data into the PC-layout `data/pcmodels-*` / `data/pccg-*`
files the port loads at runtime. **Required to run.**
@@ -27,11 +22,10 @@ for accepted versions and hashes.
### Port build
> **The Windows (MSYS2 MINGW64) path is the primary one.** The Linux build is
> compiled by CI on every push and ships in the release bundle (also
> playtested on Steam Deck hardware); the macOS column is best-effort
> guidance and has never been built or run there. Expect to fix build breaks
> yourself on untested platforms.
> **Only the Windows (MSYS2 MINGW64) path is tested.** The Debian/Ubuntu and
> macOS columns below are best-effort guidance — the port has never been built
> or run on Linux or macOS. Expect to fix build breaks yourself on those
> platforms.
| Need | Windows (MSYS2 MINGW64) | Debian/Ubuntu | macOS (Homebrew) |
|------|------------------------|---------------|------------------|
@@ -46,12 +40,12 @@ for accepted versions and hashes.
The extraction scripts need `binutils-mips-linux-gnu` (or an equivalent MIPS
binutils), `make`, `git`, and `python3`. They build a small host-compiled
`tools/extractor` and slice blobs straight out of the ROM; **no IDO / IRIX
`tools/extractor` and slice blobs straight out of the ROM — **no IDO / IRIX
toolchain is involved in extraction or in the PC build.** (The IDO toolchain is
only needed to build the N64 ROM itself, and its proprietary SGI binaries are
not distributed here; see [`SetupGuide.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/SetupGuide.md) "Recompile IDO".)
not distributed here — see [`SetupGuide.md`](SetupGuide.md) "Recompile IDO".)
On Windows this is easiest under WSL or a Linux VM. Full details and
alternatives (Docker) are in [`SetupGuide.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/SetupGuide.md).
alternatives (Docker) are in [`SetupGuide.md`](SetupGuide.md).
---
@@ -76,7 +70,7 @@ and run:
(US extraction is a prerequisite for the others.)
This populates `assets/` with the generated `.bin` blobs the build needs. See
[`SetupGuide.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/SetupGuide.md) for the in-depth build/asset pipeline.
[`SetupGuide.md`](SetupGuide.md) for the in-depth build/asset pipeline.
---
@@ -109,7 +103,7 @@ builds are named `ge007.pal-final.x86_64` / `ge007.jpn-final.x86_64`.
## 4. Generate the PC asset sidecars (required to run)
The port does **not** read model geometry, stage bg/stan data, or per-level
setup data from the raw ROM at runtime; it reads them from PC-layout *sidecar*
setup data from the raw ROM at runtime — it reads them from PC-layout *sidecar*
files under `data/`, produced offline by three converters. **Without them the
game shows the intro logos and then crashes** in
`modelPromoteNodeOffsetsToPointers` (finding D179) or on the first level load
@@ -131,9 +125,6 @@ python3 tools_pc/d69_emit.py ntsc-final # -> data/pccg-ntsc-final/{pccg
python3 tools_pc/d88_emit.py ntsc-final --regen # appends the 21 per-level Usetup*Z stage-setup files -> ~3.6 MB
```
**PAL / JP note:** sidecar generation for these regions is currently broken at
the source-data level (finding D258); use an NTSC-U ROM until issue #85 lands.
These are **pure-stdlib Python 3** (no MIPS toolchain, independent of the
step-2 asset extraction) and read only the ROM plus files already committed to
the repo (`scripts/filelist.u.csv`, `assets/obseg/file_resource_table.inc.c`,
@@ -142,12 +133,10 @@ deterministic function of the ROM. Re-run after any change to `d43_emit.py` /
`d69_emit.py` / `d88_emit.py` or the model/bg converters (`d43_*`, `d69_*`,
`d88_propdefs.py`).
> The release bundle ships the same converter frozen as
> `prepare-assets/ge007-convert`; the game spawns it on first launch when the
> sidecars are missing, so end users never run it by hand. See the bundled
> `README.md`.
> The release bundle ships `prepare-assets/prepare-assets.py`, which runs all
> three passes against your ROM automatically — see the bundled `README.md`.
> `data/pcmodels-*/` and `data/pccg-*/` are gitignored ROM-derived game data;
> `data/pcmodels-*/` and `data/pccg-*/` are gitignored ROM-derived game data —
> never commit or redistribute them.
---
@@ -169,4 +158,4 @@ runtime. `ge007.ini` is written under `data/` on first launch.
| `GE_PCDUMP="first-last:step"` | dump rendered frames as PPM (debugging) |
More diagnostic switches are cataloged in
[`dev/GE-ENV-PROBES.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/GE-ENV-PROBES.md).
[`dev/GE-ENV-PROBES.md`](dev/GE-ENV-PROBES.md).
+17 -184
View File
@@ -1,76 +1,32 @@
---
title: Development Process
description: The investigation workflow behind the port; task budgets, file partitioning, and the finding-log discipline used across sessions and agents.
---
# How this port is developed
## How this port is developed
Most of this port's work is not writing code; it is **diagnosis**: figuring
Most of this port's work is not writing code — it is **diagnosis**: figuring
out which N64 hardware or ABI behavior a piece of unmodified game code is
silently relying on, and satisfying it in the `port/` layer without touching
the game's logic. A lot of that diagnosis is done with LLM coding agents, and
the workflow below is what makes that productive rather than chaotic.
For the project context (the two-model split, the timeline, and an honest
assessment of what did and did not work), see
For the project context — the two-model split, the timeline, and an honest
assessment of what did and did not work — see
[`dev/agentic-development.md`](dev/agentic-development.md).
It is adapted from Chris Lewis's write-up
[*"Decompiling a Nintendo 64 Game in 84 Days"*](https://blog.chrislewis.au/decompiling-a-nintendo-64-game-in-84-days/)
(Snowboard Kids, 2026), an AI-assisted decompilation that ran several times
faster than its predecessor. That project bit-matches compiler output;
this one matches runtime behavior, so the mechanics differ; but the
this one matches runtime behavior, so the mechanics differ — but the
project-structure lessons carry over directly.
### Diagnosis is unrestricted; only the fix is constrained
AGENTS.md non-negotiable #2 restricts *changing* `src/game` behavior. It
has never restricted *investigating* it. An agent that stops short of
naming the exact `src/game` struct, field, or logic responsible for a bug
— because touching that file "feels" off-limits — is doing worse
diagnosis than the rules require, not better compliance with them.
Several of this port's hardest, highest-impact bugs (D209, D210, D255)
were exactly this shape: a `src/game` state variable reading wrong,
root-caused only by tracing fully into the game file that owned it.
Every investigation should end able to say which of three buckets
applies:
- **(a) ABI/layout-class.** The true cause is a struct-layout or
pointer-width misread introduced by the 32→64-bit transition — in a
ROM-serialized record or a live runtime struct/union alike. Fixable
under the narrow exception, entirely inside the game file under
`#ifdef PORT`, no sign-off needed. Check `docs/porting-notes.md` §A1
first (the pattern's tells + worked examples) — this is the most
common shape a "`src/game` behavior looks wrong" report turns out to
have.
- **(b) Port-layer bug that merely manifests through game state.** The
game code is reading state that's correct for what N64 would have
produced; something in `port/` (a shim, fast3d, frame timing, an
uninitialized or mis-ordered port-owned value) is what actually
differs. Fixable entirely in `port/`, no `src/game` touch, no sign-off
needed.
- **(c) Genuine game-logic behavior difference.** The decomp's
byte-identical control flow, given verified-correct inputs, provably
diverges from real N64 behavior. Rare, and requires the rule-2
sign-off procedure (§7 below) before any `src/game` edit.
Bucket (c) is rare by design — most bugs that look like (c) turn out to
be (a) or (b) in disguise — but ruling it in or out requires actually
tracing the state to its source, not stopping at the file boundary.
"This state lives in `src/game`" is a reason to keep tracing, not a
reason to stop.
## 1. Every investigation task gets a visible budget
An open-ended tool; for them the permuter, for us "rebuild and run the game
and stare at a frame dump"; wrecks throughput when an agent will grind it
An open-ended tool — for them the permuter, for us "rebuild and run the game
and stare at a frame dump" — wrecks throughput when an agent will grind it
indefinitely. Giving the agent a **deadline it can see** lets it trade
tool-time against thinking and decide when to give up.
So every investigation brief states:
- a **budget**; "≤ N build → run → inspect cycles" or "~M minutes"; and
- a **budget** — "≤ N build → run → inspect cycles" or "~M minutes"; and
- the **fallback on expiry**: stop, revert any temporary probes, and write up
what was found with an explicit confidence rating. A good write-up of a
half-solved bug is a deliverable, not a failure.
@@ -84,17 +40,16 @@ more room.
Lewis's agents recorded generalisable IDO quirks in a learnings file; later
agents read it and did better. Our equivalent is
[`porting-notes.md`](porting-notes.md); the recurring N64→PC bug classes,
each entry a terse index into the full
[`dev/findings.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/findings.md)
[`porting-notes.md`](porting-notes.md) — the recurring N64→PC bug classes,
each entry a terse 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;
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,
@@ -102,18 +57,6 @@ The classes it currently tracks:
- whole serialized formats are converted by an **offline sidecar** rather than
patched at runtime.
**Two process lessons from the sibling GEVR VR port** (same decomp lineage,
reference-only survey in `scratchpad/GEVR-TRIAGE.md` item D — their corpus
hit both repeatedly enough to name them): (1) **a grep standing in for a
read** — a site list built by grepping one helper name or spelling misses
siblings spelled differently or living in an adjacent file (their own
missed-smoke-scissor-site example); treat a grep-built site list as a draft,
not a census, until it's been read against. (2) **write the falsifier before
the run** — state up front what result would kill the hypothesis ("if X
survives, hypothesis Y is dead"), not just what would confirm it; this fits
the existing budget/findings discipline directly and costs one sentence per
brief.
## 3. Parallelise by file, consolidate often
Lewis ran multiple git worktrees and found the real cost was
@@ -122,7 +65,7 @@ Lewis ran multiple git worktrees and found the real cost was
- **Partition tracks by the files they will touch**, so patches never collide.
A task that can't be cleanly file-partitioned shouldn't be parallelised.
- Each agent reverts its own temporary probes before reporting, leaving only
committed, env-gated, capped diagnostics; and never touches another track's
committed, env-gated, capped diagnostics — and never touches another track's
uncommitted edits.
- One integrator merges each track with its write-up, then the next round
starts from a clean tree. Investigation branches are not allowed to drift.
@@ -136,7 +79,7 @@ Before dispatching an investigation agent:
- capture a baseline frame dump;
- grep the finding log for a prior instance of the same bug class;
- check the [Perfect Dark port](https://github.com/fgsfdsfgs/perfect_dark) for
the analogous code; same engine family.
the analogous code — same engine family.
Then hand the agent the *findings*, not just the problem.
@@ -145,129 +88,19 @@ Then hand the agent the *findings*, not just the problem.
- **The match-percentage loop.** This port doesn't bit-match. "Done" is: no
fault, and visually correct against N64 reference footage.
- **"Understanding the function is the easy part."** True when decompiling;
inverted here; the hard part is identifying the implicit hardware/ABI
inverted here — the hard part is identifying the implicit hardware/ABI
contract the already-readable C depends on.
## 6. Keep PR/git-history noise low
A session produces a lot of small, independently-verifiable units of work;
one Dxx row, one docs pass, one mechanical fix. Left unmanaged that turns
into a pile of small open PRs that's harder to review than the work
justifies. Rules:
- **Squash-merge single-purpose and docs-only PRs.** The repo has squash
merge enabled; use it (`gh pr merge --squash --delete-branch`) for anything
whose in-branch commit history (iteration, probe-add, probe-strip) isn't
itself useful to keep; which is most PRs. Reserve a plain merge commit for
a PR whose individual commits are each a distinct, reviewable unit someone
might want to `git revert` independently (e.g. this session's PR #50: a
fix commit + a separate analysis-only commit).
- **Batch same-session, same-flavor docs updates into one PR/branch**
instead of opening a new PR per finding row. A runtime re-check that
updates one `findings.md` row (like #48) doesn't need its own PR if
another docs-only branch is already open in the same session; fold it in.
Only split into separate PRs when the pieces have genuinely different
review/merge timing (e.g. one needs a human playtest, the other doesn't).
- **A branch takes only the change it's named for.** Don't let unrelated
housekeeping (a stray gitignore fix, a config tweak) ride along on an
active feature/fix branch just because it's the one checked out; cut a
separate branch, even for a one-line change.
- **Stacked PRs declare the dependency in the title/body** ("step 2 of N,
step 1 is #N") and get rebased/merged in order promptly; don't let a
later step sit open so long the earlier step's content drifts under it.
- **Triage the open-PR list at least once a session**, ordered by
time-to-close (docs-only and non-draft first, human-playtest-gated last).
A PR idle long enough that `main` has moved past the finding rows it
touches is **stale, not just old**; check for real textual conflict
(`git merge-tree`), not just calendar age. If `main` already has more
current information than the PR (a bug the PR still lists `OPEN` that's
since been fixed and verified), **close it rather than force a merge or
spend a cycle rebasing it**; re-derive only whatever part of its content
is still true as a fresh, small PR against current `main`. Merging stale
content back over newer information is a regression, not a save.
## 7. The rule-2 sign-off procedure (genuine `src/game` behavior changes)
Use this only for bucket (c) from the diagnosis framing above — never for
the ABI/layout exception (§A1-class fixes need no sign-off, only
documentation) and never for anything expressible in `port/`.
**To request sign-off, the write-up must state:**
1. The exact `src/game` file:line and the specific behavior being
proposed for change.
2. **Proof of divergence.** Given verified-correct inputs — i.e. every
upstream ABI/layout suspect (§A1) and every port-layer suspect has
been checked and ruled out, with the ruling-out cited — the
byte-identical decomp code still produces behavior that differs from
real N64 reference behavior. Name the reference (a capture, documented
prior N64 behavior, a cited external source). "It looks wrong" is not
proof; "it disagrees with `<reference>`, verified by `<method>`" is.
3. Why the fix cannot be expressed in `port/`, and why it does not
qualify under the ABI/layout exception — the two questions that must
be answered "no" before this procedure applies at all.
4. The proposed fix, scoped as tightly as possible to the one named
behavior, plus a same-engine precedent if one exists — the Perfect
Dark port is the standing reference; cite the analogous PD change the
way `docs/dev/WIDESCREEN-FOV-PLAN.md`'s "Rule #2 exception framing"
section did.
**Granting.** The user reviews the above and replies with an explicit
go-ahead in-thread. There is no implicit or inferred sign-off: silence,
"seems reasonable," a prior similar grant, or approval of a *different*
behavior in the same file do not carry over to a new request.
**Recording.** Every sign-off gets its own `docs/dev/findings.md` entry
(next `Dxx` label), tagged **`RULE-2-SIGNOFF`** in its status line
alongside the normal finding classification, and recording: the request
as stated above, the grant (quoted or summarized, with date), and the
resulting diff. Add the label to `findings-index.csv` like any other row,
so `tools_pc/gen_findings_index.py` keeps it discoverable — chat history
is not the record of approval; the finding entry is.
**Constraints that still apply after approval:**
- Still `#ifdef PORT` — the N64 build is untouched, and the `#else` arm
(or the file's un-PORT-guarded default) still matches the original
decomp exactly.
- Still scoped to the one named behavior — a grant is not a standing
license; a different behavior difference in the same file needs its own
request, even if related.
- Still fully documented in the `RULE-2-SIGNOFF` finding entry — that
entry is the permanent record of *why* this specific exception to
non-negotiable #2 exists.
- If the fix would visibly change behavior at *default*
settings/difficulty/region beyond the one bug being fixed, treat that
as a signal the scope drifted mid-implementation — stop and go back for
a fresh sign-off on the wider scope rather than shipping it under the
narrower original grant.
This generalizes the ad hoc precedent in
`docs/dev/WIDESCREEN-FOV-PLAN.md`'s "Rule #2 exception framing" section (a
real, already-granted case, kept as historical record) into a reusable
process; new requests cite this section rather than re-deriving the case
for a process each time.
## 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>.
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: diagnosis may trace as deep into src/game state as the evidence
needs — name the exact struct/field/logic responsible before concluding
which bucket applies (see "Diagnosis is unrestricted" above). The FIX is
still constrained: no src/game behavior changes unless (a) it's an
ABI/layout-class struct-layout or pointer-width misread — check
docs/porting-notes.md §A1 first; #ifdef PORT; documented in the finding
log under §F/D3x — or (b) it's fixable entirely in port/ (no src/game
touch). A genuine game-logic behavior difference (bucket c) is NOT
authorized by this brief — stop and write up a rule-2 sign-off request
per docs/dev-process.md §7 instead of applying it.
VERIFY: <the exact tools_pc/verify.sh (or probe) invocation that proves this done; or, if runtime verification is impossible in this environment, say so and name what a human must run>.
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.
-77
View File
@@ -1,77 +0,0 @@
# Accuracy / code review — N64 fidelity sweep (2026-09-20)
A thorough pass over the PC port with one question: *where does the port diverge from the
N64 original, and what latent failure classes of the D318 kind are still unpriced?*
Method: static review against the decomp ground truth + targeted verification builds.
Findings below; new finding **D320** logged in `findings.md`.
## 1. PRNG — verified bit-exact
`port/src/random.c` re-checked line-by-line against both `src/random.s` and
`src/game/chrObjRandom.s`. The MIPS64 `dsll32`/`dsrl32` semantics (post-D284) are
emulated correctly: the 32-bit LFSR state advances identically, and
`randomGetNext()` returns the same 31-bit values in the same order. No divergence.
## 2. Known bug-class sweeps — all clean or already fixed
| Class | Result |
|---|---|
| **A1** raw-byte/padding aliases (32→64-bit layout shift) | Remaining `padding[]` uses are in pointer-free structs (layout-stable) or already fixed (D115, D209, D312). No live misreads. |
| Shifts by 32 / pointer-width arithmetic in game logic | Only port-layer code (`d318watchdog` print, `DRAM_SIZE`). Game logic clean. |
| Signed-overflow UB | `-fwrapv` enabled in `CMakeLists.txt` — the whole class is neutralized to N64-wrap semantics. |
| `#ifdef PORT` hygiene (N64 path preserved) | 307 hunks audited; 178 are pure additions (diagnostics / struct extensions). No hunk alters the non-PORT control flow. |
| **§B** Gfx/Vtx 16-byte records (raw byte indexing) | `bg.c` uses typed `.words`/`.dma` fields throughout; no remaining raw byte offsets. D312's fix pattern is the last of this class. |
| `osVirtualToPhysical` call sites (88) | All flow through fast3d's `seg_addr()` (D131 fix intact). No site assumes N64 segment RAM layout. |
| Dead code | `objecthandler_2.c` compiles but is not linked on PC (symbols absent from the binary). Harmless; noted for a future source-list prune. |
## 3. Timing layer — intentional, documented
Frame-locked virtual clock (vsync tick), `GE_DETERM` fixed-tick mode, and
`g_GlobalTimerDelta` zero-guarded at every division site. All deliberate port-layer
design, each documented in its finding. No hidden drift source found beyond the
already-fixed D204 tempo work.
## 4. D318-class latent softlock sweep → **D320** (the main new result)
D318's mechanism is generic, not Facility-specific: `actor_fire_or_aim_at_target_update`
(`chraction.c:4882`) retargets any ACT_ATTACK chr with an AIM_ONLY/DONTTURN attacktype and
only moves the model's **endframe** — it never re-inits the animation. If the chr is pinned
on a pre-shoot aim-hold pose (hold endframe H < variant shoot window) and the new endframe
lands above H, the model pins forever and the timer-gated fire events never open; if the
surrounding AI script's only escape is an `if_guard_has_stopped_moving` gate, it can never
pass (`chrHasStoppedOrPatroling` is FALSE for ACT_ATTACK). **Permanent softlock — on N64 too.**
Swept every level's AI scripts (`assets/obseg/setup/*.c`): 17 lists in 8 levels contain an
aim-hold + `_update` pair. Risk-ranked table in the D320 entry. Headlines:
- **Facility ai_19** — HIGH. Same non-self-healing shape as ai_22 (second softlock point in
the same level).
- **Control ai_9** — HIGH. Near-exact ai_22 mirror (pre-aim hold → timer → update → fire
loop gated on the target dying from this guard's shot).
- **Depot ai_12** — MED-HIGH. The kill is scripted, but the objective bit is gated behind a
stop-check; pinned guard → level-complete softlock.
- **Bond-combat loops** (Aztec/Control/Egyptian/Depot/Streets/Bunker/Surface) — likely safe:
sustained fire keeps the model inside the shoot window by design, and every playtest
exercises them without freezes. Inferred, not probe-verified per list.
Classification: **as-authored latent race(s), faithfully reproduced** — same bucket as D318,
not a port bug. Verification path (generalized timeline probe) and the
generalize-the-watchdog recommendation are in D320.
## 5. Fixed this session
- `port/fast3d/gfx_pc.cpp:2417` — the GE_D116 diagnostic printf used `%f` for `uint32_t`
args (`v->u`, `tex_width`) and `%d` for unsigned tile fields; wrong debug output only, no
runtime impact. Casts added; build now warning-free on that file.
## 6. Verification ritual
Full `./build-pc.sh ntsc-final` configure + compile + link: **PASS**, no warnings on
touched files. All new/changed symbols resolve exactly once (link is the sweep). D318
watchdog/timeline symbols confirmed present in the binary.
## 7. Deliberately not committed (release-prep WIP, separate workstream)
`.github/release-notes.md`, `README.md`, `docs/index.md`, `docs/media/goldeneye-gh-preview.gif`,
`docs/dev/agentic-development.md`, `.gitignore` (`/local/` exclude), and non-D318 scratch
artifacts — left in the working tree for their owner.
+343 -170
View File
@@ -1,202 +1,375 @@
# AUDIO-PLAN — Phase 3 / ROADMAP B3 (libaudio → SDL)
# Audio Plan — Phase 3 (libaudio → SDL)
Status: plan of record for the audio track, 2026-09-05. Companion to
`ROADMAP-1.0.md` §B3 ("audio (Phase 3)", "audio track parallel from M-50 →
v0.4.0"). Independent of B1/B2 — can start immediately.
**Status update (M-69):** Phases 0–2 below are DONE — the mixer landed as
D199 (`port/src/mixer.c` macro-swap), D200/D201 fixed the two pointer-width
landmines it exposed, and D204 fixed a permanent ~2 % tempo drift. Phase 3
(verification) is now the active track: D202's sample-correctness question is
closed negative through M-69 (full-corpus per-voice exact match — 286/320
requests CC ≥ 0.85, every allocated voice plays the exact ROM sample at the
exact ROM pitch, no corruption). Remaining: user by-ear pass + N64 A/B on the
door-loop cap (M-66b), then D202 probe removal + close; reverb/mix-balance
tuning is still owed. See findings.md D199–D204.
Status: **draft for evaluation** (written read-only; no code changes made).
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/porting-notes.md`.
---
## Where things stand (verified 2026-09-05)
## Contents
More is already in place than "audio not started" suggests:
1. [Goal and scope](#1-goal-and-scope)
2. [Current state](#2-current-state-verified-by-reading-not-assumption)
3. [The GE pipeline in one paragraph](#3-the-ge-pipeline-in-one-paragraph)
4. [How the PD port does it](#4-how-the-pd-port-does-it-ground-truth-comparison)
5. [Architecture options](#5-architecture-options-evaluate-all-recommendation-marked) — options A/B/C + sub-decisions A1–A3
6. [DMEM layout](#6-dmem-layout-ido-from-srclibultraaudiosynthinternalsh5771)
7. [Shims and ABI fixes required](#7-shims-and-abi-fixes-required) — incl. [7.1 `K0_TO_PHYS` bug](#71-bug-found-k0_to_phys-is-unshimmed-and-corrupts-pointers-on-pc)
8. [Phase 0 — recon](#8-phase-0--recon-do-first-informs-everything)
9. [Open questions](#9-open-questions-decide-before-phase-1)
10. [Phases and milestones](#10-phases-and-milestones)
11. [Verification ritual (audio-specific)](#11-verification-ritual-audio-specific-on-top-of-the-standard-one-in-agentsmd)
12. [Risks / unknowns](#12-risks--unknowns)
13. [Key references](#13-key-references)
- **The whole CPU-side audio engine already compiles and runs.**
`src/audi.c`, `src/snd.c`, `src/music.c` are in `SRC_ENGINE`; IDO libaudio
(`src/libultra/audio/*.c`) + Rare's naudio "New" driver
(`src/libultrare/audio/*.c`: drvrNew/env/reverb) are in the link. The
`amMain` audio thread runs per VI-retrace; D54/D54b already killed its
SIGSEGVs — so synthesis bookkeeping is live (D77: "music runs in code but
no audible output").
- **The scheduler side needs zero changes.** `osSpTaskStartGo`
(`port/src/libultra.c:1237`) already has an M_AUDTASK branch that completes
the task and posts `OS_EVENT_SP`, so `amMain`'s reply-message handshake
works. The branch is currently a no-op — exactly where the audio ucode's
job used to go.
- **The two TODO stubs are the actual gap:** `osAiSetNextBuffer()` /
`osAiGetLength()` in `port/src/libultra.c:852-862` discard everything
(`osAiGetLength` returns 0), and `port/src/audio.c` never opens an SDL
device. `main.c:141-142` already calls `audioInit()` / `mixerInit()`.
- **The PD reference is local** at `C:\Users\james\Source\Repos\pd_port`,
with the full audio stack: `port/src/audio.c` (75 lines, SDL device +
queue), `port/src/mixer.c` (~720 lines, software RSP-audio-ucode), and the
wiring trick in `include/PR/abi.h`.
---
## Architecture — PD's macro-swap trick, adapted
## 1. Goal and scope
On N64, `alAudioFrame()` only *builds* an acmd list; the RSP audio ucode
(`aspMain`) executes it. PD's PC port never runs that ucode: under
`#ifndef PLATFORM_N64`, `include/PR/abi.h` swaps each acmd-building macro
`aXxx(pkt, ...)` for a call to a CPU implementation (`aXxxImpl`) operating on
a software DMEM array. libaudio/naudio compile unmodified; every "command"
executes inline at build time; the M_AUDTASK that reaches the scheduler is an
empty formality (PD's `pdsched.c` audio branch is literally empty).
Make GoldenEye's audio audible on the PC port with 1:1 fidelity to the N64 pipeline:
music (3 cseq tracks) and SFX through the real libaudio synth, out of SDL2.
**Critical catch — GE and PD use different acmd ABIs** (the AGENTS.md
"same family ≠ identical format" case):
In scope:
- Executing the per-frame **Acmd stream** that stock IDO libaudio emits (the part that
ran on the RSP under GE's `aspMain` microcode).
- SDL audio device + queue plumbing (`port/src/audio.c`, AI shims in `libultra.c`).
- One latent ABI fix in the port shim headers (`K0_TO_PHYS`, §7.1).
| | GE (`include/PR/abi.h`) | PD (naudio "New") |
Out of scope:
- MP3 playback (naudio-only, PD-specific; GE never emits it).
- Input (done — D118), saves (Phase 4).
- Any change to game logic. All work lives in `port/` except the narrow, documented
shim-header exception in §7.1 (same class as D78).
## 2. Current state (verified by reading, not assumption)
Working already:
- **The whole CPU side of libaudio runs.** `src/libultra/audio/*` +
`src/libultrare/audio/{drvrNew,env,reverb}.c` compile and execute; the audio thread
(`amMain`, `src/audi.c`) gets retrace messages from the real scheduler (`src/sched.c`,
compiled — Phase 2 decision), calls `alAudioFrame()`, which fills a 3000-word Acmd
list and posts an `M_AUDTASK` to the scheduler. Bank trees, cseq fixups (D54, D36/D37)
are done. Sample DMA from cart works via `amDmaCallback` → `osPiStartDma`
(synchronous ROM-image shim in `libultra.c`).
Dead (the three gaps):
1. **Acmd list never executed.** `port/src/libultra.c:1108` `osSpTaskStartGo()` has an
explicit Phase-3 no-op branch for `M_AUDTASK`. The output buffer (`info->data`) is
never filled.
2. **AI shims are stubs.** `libultra.c:794–810`: `osAiSetNextBuffer` discards the
buffer, `osAiGetLength` returns 0 (so the per-frame sample-count feedback loop in
`amHandleFrameMessage`, `audi.c:527`, runs blind).
3. **No SDL device.** `port/src/audio.c` `audioInit()` is a TODO stub; and nothing
calls `audioEndFrame()` (zero callers repo-wide).
Existing PORT guards to revisit when audio lands:
- **C7 / D127** (`src/snd.c:859`): skips SFX whose resolved `ALSound*` pointer is out of
range — added because the converted bank tree (`romdata.c afFixupInst`) yields fewer/
rearranged `soundArray` slots than the N64 image on some levels (Surface1). May be
masking a real bank-tree defect. See **Q1**.
- `src/audi.c:365`: `#if defined(PORT)` memcpy of `CUSTOM_FX_PARAMS_N` (C++-style
initializer workaround) — harmless, keep.
## 3. The GE pipeline in one paragraph
Audio thread on each retrace: `amHandleFrameMessage()` (`src/audi.c:497`) hands the
*previous* frame's output to AI via `osAiSetNextBuffer(lastInfo->data, …)`, computes
this frame's sample count from `g_FrameSize - osAiGetLength()>>2` (clamped to
`g_MinFrameSize`), calls `alAudioFrame()` which walks the filter chains (decoder →
resampler → envmixer → [FX: GE's custom chorus/reverb] → aux bus → main bus → save)
emitting **Acmd words** into a 3000-word list, then posts an `M_AUDTASK` (ucode =
`aspMain`). The real scheduler (`sched.c:439,485`) queues it in the audio list and runs
it through `osSpTaskLoad/osSpTaskStartGo`. On the N64 the RSP executes the Acmd stream
against 4 KB of DMEM, writing interleaved s16 stereo to `info->data`; AI DMA then plays
it at 22050 Hz. On PC we must replace "RSP executes Acmd stream" — that is the entire
problem.
**Opcode inventory** (grep-verified across `src/libultra/audio/*.c` +
`src/libultrare/audio/*.c` — 15 ops):
| Op | Emitted by | PD reference code? |
|---|---|---|
| `aADPCMdec` | `(pkt, flags, statePtr)` — params from an `aSetBuffer` context + `ADPCM_STATE[16]` | `(pkt, state, flags, count, inofs, outofs)` — inline args |
| `aLoadBuffer` / `aSaveBuffer` | `(pkt, dramAddr)` only — dest/count from the `aSetBuffer` context | explicit dest + count args |
| `aInterleave` | `(pkt, lDmem, rDmem)` | no args |
| Opcodes | 16: adds `aSetBuffer`, `aPan`, `aSegment`; no `aDisable` / `aPlayMP3` | 15 incl. MP3 (PD-specific) |
| `aSetBuffer` (×20) | load, mainbus, resample, save, reverb | yes (`a*Impl` pattern; different arg ABI) |
| `aMix` (×7) | mainbus, reverb | yes — `aMixImpl` |
| `aLoadBuffer` (×7) | load, reverb | yes — `aLoadBufferImpl` |
| `aClearBuffer` (×7) | auxbus, mainbus, load, reverb | yes — `aClearBufferImpl` |
| `aSetVolume` (×5) | env.c (Rare envelope) | yes — `aSetVolumeImpl` |
| `aSaveBuffer` (×4) | save, reverb | yes — `aSaveBufferImpl` |
| `aDMEMMove` (×4) | load, resample, reverb | yes — `aDMEMMoveImpl` |
| `aResample` (×2) | resample.c, reverb.c | yes — `aResampleImpl` (SSE4.1/NEON/scalar) |
| `aLoadADPCM` (×2) | load.c, reverb.c | yes — `aLoadADPCMImpl` |
| `aSetLoop` | load.c:479 | yes — `aSetLoopImpl` |
| `aSegment` | synthesizer.c:231 | n/a (no-op on CPU) |
| `aPoleFilter` | **reverb.c:402 (GE custom FX)** | **NO — PD's stub is empty ("never gets called?")** |
| `aInterleave` | save.c | yes — `aInterleaveImpl` (naudio variant takes no args) |
| `aEnvMixer` | env.c:410/414 | yes — `aEnvMixerImpl` |
| `aADPCMdec` | load.c:483 | yes — `aADPCMdecImpl` (SSE4.1/NEON/scalar) |
GE's context model (from the call sites in `src/libultra/audio/`):
`aSetBuffer(flags, dmemin, dmemout, count)` sets a working-buffer context;
subsequent `aLoadBuffer(dramAddr)` / `aADPCMdec(flags, statePtr)` /
`aResample(flags, pitch, statePtr)` operate within it; the tail is
`aInterleave(AL_MAIN_L_OUT, AL_MAIN_R_OUT)` → `aSaveBuffer(f->dramout)`.
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).
So this is **not** "copy `mixer.c`": port PD's mixer as the *DSP algorithm
reference* (ADPCM decode, linear resample, mix math, env-mixer / pole-filter
are the same Rare/Nintendo DSP blocks) and write GE-ABI wrappers that
maintain the `aSetBuffer` context and read params from the state structs.
## 4. How the PD port does it (ground-truth comparison)
**GE's full emittable opcode set** (static audit of every call site in
`src/libultra/audio/*.c` + `src/libultrare/audio/*.c`): ClearBuffer,
LoadADPCM, DMEMMove, SetBuffer, LoadBuffer, SetLoop, ADPCMdec, Resample,
Interleave, SaveBuffer, Mix, SetVolume, EnvMixer, PoleFilter, Segment — plus
`aPan`, defined in abi.h but apparently uncalled. PD's mixer covers the DSP
core for all of them except the trivial bookkeeping ones: SetBuffer /
LoadBuffer / SaveBuffer are context tracking + memcpys; Segment is a no-op on
PC (`amDmaCallback` already resolves ROM addresses against the mapped image —
`osVirtualToPhysical` is identity, `libultra.c:1295`).
1. naudio is Rare's own library; its `n_abi.h` has a `#ifndef PLATFORM_N64` branch that
redefines every `n_a*` macro as a **direct C call** into `port/src/mixer.c`
(`mixer.h` `#undef`s the macros and redirects to `a*Impl`). All DSP therefore runs
**inline on the audio thread at task-build time**, inside `amgrFrame()`.
2. The `M_AUDTASK` is an empty shell: `pdsched.c:233` receives it with the comment
`// send this into the mixer` and an empty body.
3. PD **replaced the scheduler** (`pdsched.c`) and drives audio from retrace:
`schedAudioFrame()` → `amgrFrame(); audioEndFrame();` (×`diffframe60`). Same thread
for build and flush ⇒ no locking in their SDL handoff.
4. SDL device layer: queue-based, exactly what our scaffold already copied —
`osAiGetLength` = queued **bytes** (`audioGetBytesBuffered()`),
`osAiSetNextBuffer` = stash pointer, `audioEndFrame` = `SDL_QueueAudio` with a
`queueLimit` backpressure drop. Device opened at 22020 Hz (their TODO admits this
"might cause trouble" — it is a silent −9-cent detune vs their 22050 synth rate).
5. DMEM: ~3 KB static buffer, **naudio-specific** offsets (main L/R @0x4E0/0x650,
aux @0x7C0/0x930) — different from IDO's layout (§6).
6. PD-only extras: `aPlayMP3Impl` (minimp3; GE never emits), `aPoleFilterImpl` empty
stub (GE *does* emit it).
## Phases
**Why we can't copy it wholesale:** GE compiles the real `src/sched.c` and stock IDO
libaudio, whose macros only *write words* into the Acmd list — there is no build-time
hook unless we shadow `PR/abi.h`. Something must execute the stream. Two further ABI
facts:
### Phase 0 — Ground truth (research, ~half day, delegable)
- **IDO's implicit "current buffer":** e.g. `aADPCMdec(pkt, f, s)` carries only flags +
a state pointer; in/out DMEM offsets and sample count come from the preceding
`aSetBuffer` (`load.c:482–483`, `resample.c:90–91`). naudio spells everything out per
call. Whatever option we pick, the implementation must model current-buffer state.
- **Addresses in the stream are RAM pointers** (`K0_TO_PHYS(f->state)`,
`osVirtualToPhysical(e->state)`, `f->dramout`) — resolvable on PC *except* for the
`K0_TO_PHYS` bug in §7.1.
1. **Disassemble GE's `aspMain` microcode from the ROM** and document each
opcode's exact semantics in findings.md §F (new Dxx): flag-bit meanings
(`A_FIRST`, `A_CONTINUE`, …), count units (bytes vs samples), where the
interleaved output lands before `aSaveBuffer`, how `aSetLoop` state gets
restored, `ENVMIX_STATE[40]` layout. This is the audio analogue of using
`rsp/graphics/gmain.s` as GBI ground truth — we don't run the ucode, but
it is the spec. (`port/src/ucode.c`'s header already flags this "DECISION
NEEDED in Phase 3"; CPU-bypass means the dummy `aspMainTextStart` /
`aspMainDataStart` markers stay forever — document that decision there.)
2. **Opcode inventory probe** (port-only, zero game-code change): a
`GE_AUDIOTRACE` env var in the existing M_AUDTASK branch of
`osSpTaskStartGo` that walks `t->t.data_ptr` for `data_size/8` words and
histograms `w0 >> 24`. Confirms the static call-site audit and catches
anything emitted dynamically.
## 5. Architecture options (evaluate all; recommendation marked)
### Phase 1 — Device plumbing (small, lead work)
### Option A — CPU-side Acmd interpreter at task time 【RECOMMENDED】
- Implement `port/src/audio.c::audioInit`: SDL device at **22050 Hz** (GE's
`OUTPUT_RATE = 0x5622`, not PD's 22020), `AUDIO_S16SYS`, stereo. Keep the
existing `Audio.BufferSize` / `Audio.QueueLimit` config registrations.
- Wire `osAiSetNextBuffer` → `SDL_QueueAudio` guarded by `queueLimit`;
`osAiGetLength` → `SDL_GetQueuedAudioSize()`. Faithful mapping: N64's
`AI_LEN_REG` ("bytes synthesized but not yet played") ≙ SDL queued-bytes,
and the game's self-tuning `frameSamples = g_FrameSize −
osAiGetLength()>>2` math (`audi.c:527`) keeps the virtual FIFO at ~736
samples (NTSC) with no game-code changes. Queue inline in
`osAiSetNextBuffer` (same thread, once per frame) rather than PD's deferred
`audioEndFrame`. Overflow safety is already self-limiting: a full queue
makes `frameSamples` clamp to `g_MinFrameSize`, and the `queueLimit` check
drops rather than crashes.
- Add a `GE_AUDIODEBUG` probe logging queue depth + synthesized samples every
60 frames; catalog it in `docs/GE-ENV-PROBES.md`.
- **Exit:** links 243/243, `/linkcheck` clean; headless `-level_09` 60 s
crash-free; debug probe shows the queue filling while intro music "plays"
(still silent — acmds don't execute yet).
Decode the 64-bit command words from `t->t.data_ptr` inside the existing `M_AUDTASK`
branch of `osSpTaskStartGo()` (`libultra.c:1108`), dispatching each op to an
implementation in `port/src/mixer.c` operating on a static 4 KB "DMEM".
### Phase 2 — The mixer (core work, ~1–3 days)
- Pros: zero game/header/build changes; treats the stream as data exactly like fast3d
does with GBI (project-consistent); natural logging hook (`GE_DAUDIO=1` dump of op +
args per frame) for debugging and for validating against the microcode disassembly;
execution point matches N64 timing (DSP runs when the RSP would have run it).
- Cons: must write the word decoder (~50 lines, trivial — op is bits 24–31 of w0 per
`include/PR/abi.h`) and the current-buffer state machine; one scheduler tick of extra
latency vs build-time execution (inaudible).
- Port PD's DSP cores into `port/src/mixer.c`: ADPCM decode (coefficient-book
handling via `aLoadADPCM`), linear resampler, stereo mix with `0x8000`
scaling, env-mixer + pole-filter (reverb/chorus driven by
`CUSTOM_FX_PARAMS_N`, `audi.c:169`), set-volume ramps. Add the GE-ABI
context layer: a static `{dmemin, dmemout, count}` updated by
`aSetBuffer`, consumed by LoadBuffer / SaveBuffer / ADPCMdec / Resample; a
shared software DMEM array across Impls. Replace the current 31-line stub
interface (`port/include/mixer.h`) with PD-style `aXxxImpl` declarations.
- Edit `include/PR/abi.h` with an `#ifdef PORT` split (N64 path
byte-identical): PC includes a port `mixer.h` whose macros match **GE's**
arg arities. This is the one non-`port/` edit — same documented
ABI/layout-exception class as D78 / D135, logged in §F with its own label.
- Unknown / unused opcodes (`aPan`, `aSegment`): log-and-no-op under
`GE_AUDIOTRACE`; implement only if Phase 0 shows them live.
- All DSP runs on the existing `amMain` thread inside `alAudioFrame` — no new
locks, no scheduler changes.
- **Exit:** opcode histogram fully covered; headless run clean; queue probe
non-zero.
### Option B — Shadow `PR/abi.h`, redirect macros to direct C calls (the PD trick)
### Phase 3 — Verification & tuning (user-gated listen tests)
Add `port/shim/PR/abi.h`: `#include "include/PR/abi.h"` then `#undef` the 15 `a*`
macros and `#define` them as direct calls into `mixer.c` (PD's `mixer.h` does exactly
this for naudio). DSP then runs inline during `alAudioFrame()` on the audio thread; the
`M_AUDTASK` branch stays a no-op, exactly like PD.
- Listen checklist: menu music → briefing → in-level music transitions →
gunfire / explosions → **positional SFX** (walk around a source) → weapon
one-shots → level changes → **mission-fail fade** (the D152 `s_imLock`
scenario — retest killing Trevelyan in Facility with live audio, since the
fade hammers the event queue; the D152 self-healing lock mitigation is
already in place but must be re-proven under load).
- Compare reverb / chorus character against N64 reference footage (that's
what `CUSTOM_FX_PARAMS_N` is for).
- Regression: `GE_PCDUMP` framediff vs goldens unchanged (audio must not
touch rendering) + 60 s `-level_09` soak.
- Docs: findings §F entry, porting-notes quirk ("classic-libultra vs
naudio-New acmd ABI delta"), ROADMAP B3 status, HANDOFF update.
- Pros: battle-tested pattern (shipped in PD); no decoder; slightly less latency.
- Cons: shadows a core PR header for every TU that includes it (only libaudio uses these
macros on PC, so blast radius is small but nonzero — must verify include-path
ordering in `CMakeLists.txt`); stream becomes invisible at runtime (no logging hook —
you'd instrument inside each Impl); the shim must exactly mirror IDO arg order, which
differs from PD's naudio redirects line-by-line.
## Risks
### Option C — Run the real `aspMain` microcode 【NOT RECOMMENDED】
1. **Ucode-semantics mismatch** (count units, flag bits) → garbled or
wrong-tinted audio, not crashes; mitigated by Phase 0 disassembly +
iterative listen checks. Main schedule risk.
2. **Timing coupling with B1** (D193 family): low — audio frame size is
feedback-driven off `osAiGetLength`, robust to scheduler jitter; re-verify
after B1 lands.
3. **Performance:** ~736 samples/frame of CPU DSP is trivial on x86-64 (PD
shipped exactly this).
4. **D152 lock storm under live audio** — covered by the explicit Phase 3
retest.
Point `aspMainTextStart/DataStart` (`port/src/ucode.c`) at the ROM microcode and execute
it with an RSP emulator. Maximum fidelity by construction, but requires a 16-bit-MIPS
RSP core + DMEM + interrupt model for one component whose entire job is the 15 ops we
can implement directly. Keep the `ucode.c` dummies permanently (their header comment
anticipates this decision). The microcode is still *used* — as a **reference to
disassemble**, not to run (§8, Phase 0).
## Delegation shape & effort
### Sub-decision A1 — where samples hit the SDL queue 【RECOMMENDED: immediate】
Per `docs/dev-process.md`:
- **Immediate:** `osAiSetNextBuffer()` shim calls into `audio.c` and `SDL_QueueAudio`s
right there (on the audio thread), with the existing `queueLimit` backpressure drop.
Lock-free (single writer); matches the moment N64 AI DMA starts; `audioEndFrame()`
disappears entirely. Safe because `NUMBER_OUTPUT_BUFFERS=3` rotation guarantees the
handed-off buffer isn't rewritten before it is queued.
- **Deferred (PD-style):** stash in `osAiSetNextBuffer`, flush from a frame-end hook.
In GE this crosses threads (audio thread sets; scheduler/gfx path would flush) ⇒ needs
a mutex or accepts PD's unlocked pattern, and needs a new `audioEndFrame()` call site
that doesn't exist today. Only worth it if immediate queueing shows a problem.
- Phase 0 disassembly → Claude research subagent (judgment-heavy, read-only +
findings write-up).
- Phase 1 → lead or mechanical worker (build-verifiable).
- Phase 2 DSP-core port → candidate for the local Qwen once the semantics doc
exists (mechanical, diff-checkable against PD), with the ABI mapping and
review kept by the lead.
- Phase 3 listen tests → user.
### Sub-decision A2 — device sample rate 【RECOMMENDED: 22050】
Total ~3–6 focused sessions. Milestone: ROADMAP "audio track (parallel from
M-50) → v0.4.0".
Request exactly `OUTPUT_RATE` (0x5622 = 22050 Hz), stereo s16, as PD's `audio.c`
scaffold already comments. Do **not** copy PD's shipped 22020 (detune). If a platform
rejects 22050, let SDL convert (`SDL_OpenAudioDevice` with allowed-conversion) rather
than changing the requested rate.
## Key file map
### Sub-decision A3 — timing and thread priorities 【RECOMMENDED: inline + host priorities; no worker thread】
| File | Role in this plan |
Execution stays on the scheduler thread (Option A's execution point). If Phase 2/3
milestones show underruns (`audio: ai out of samples` spam — verification step 3) or
frame-time spikes attributable to synthesis cost, the **first lever is host thread
priorities**: `SetThreadPriority` / `pthread_attr_setschedparam` with the scheduler
thread time-critical (it now owns both fast3d and the mixer) and the tick thread
normal — a ~10-line fix in `libultra.c`. A dedicated audio worker thread consuming
`M_AUDTASK`s is explicitly **not** the next step: it adds a cross-thread handoff for a
problem priorities may fully solve, and the kernel's priority semantics are already
fake (D24 — see the backlogged bug-class entry in `docs/BACKLOG.md`), so any deeper
redesign starts there. Do **not** consider rewriting toward PD's single-threaded
inline-`amgrFrame()` model: it works for them only because they adapted game-side lib
code (direct `schedSubmitTask` calls, gutted `pdsched.c`); for GE it means editing
`src/audi.c` / scheduler submission points — a non-negotiable #2 violation. Rationale
recorded in `docs/BACKLOG.md` (Architecture decision).
## 6. DMEM layout (IDO, from `src/libultra/audio/synthInternals.h:57–71`)
```
AL_MAX_RSP_SAMPLES = 160 (per-chunk; alAudioFrame builds the list in ≤160-sample chunks)
offset 0 AL_DECODER_IN / AL_RESAMPLER_OUT / AL_TEMP_0 (320 B)
offset 320 AL_DECODER_OUT / AL_TEMP_1 (320 B)
offset 640 AL_TEMP_2 (320 B)
offset 1088 AL_MAIN_L_OUT (320 B)
offset 1408 AL_MAIN_R_OUT (320 B)
offset 1728 AL_AUX_L_OUT (320 B)
offset 2048 AL_AUX_R_OUT (320 B) → max extent 2368 B
```
Static 4 KB buffer in `mixer.c` (round number, headroom). **Not** PD's naudio offsets.
GE's custom FX (`reverb.c`) uses `AL_TEMP_0/1/2` as scratch plus RAM-backed delay lines
via `aLoadBuffer`/`aSaveBuffer` to `r->base` — all plain host pointers on PC.
## 7. Shims and ABI fixes required
### 7.1 【BUG FOUND】 `K0_TO_PHYS` is unshimmed and corrupts pointers on PC
`port/shim/PR/R4300.h` redefines `PHYS_TO_K0` (identity) but **not** `K0_TO_PHYS`.
Stock: `#define K0_TO_PHYS(x) ((x)&0x1FFFFFFF)` (`include/PR/R4300.h:65`). Game RAM
lives at the V1 view `0x70xxxxxx` (`port/src/dram.c`); masking gives `0x10xxxxxx` — an
invalid host pointer. Hit sites: `load.c:483` (`aADPCMdec(..., K0_TO_PHYS(f->state))`)
and `load.c:479` (`aSetLoop(ptr++, K0_TO_PHYS(f->lstate))`) — the mixer would receive
garbage state addresses for every ADPCM voice. Fix: extend the existing shim with
`#undef K0_TO_PHYS / #define K0_TO_PHYS(x) ((u32)(x))` (identity; V1 addresses are live
host pointers). Inert in the N64 build (no `-DPORT`) — same class as the D78 precedent.
Record as a new Dxx finding when implemented (next free label after D127).
### 7.2 AI shims (`libultra.c:794–810`)
- `osAiSetFrequency(hz)` → return `hz` (already does; 22050 flows into
`alconf->outputRate` and the frame-size math — correct).
- `osAiSetNextBuffer(buf, size_bytes)` → queue to SDL per A1.
- `osAiGetLength()` → **bytes** currently queued (`SDL_GetQueuedAudioSize`); the game
does `>> 2` for frames (`audi.c:527`). PD returns bytes too. This closes the
feedback loop that sizes each frame's synthesis.
- `osAiSetConvert` — not called by GE; leave stub.
### 7.3 Misc
- `port/src/audio.c`: fill in `audioInit()` (SDL_InitSubSystem(AUDIO), open device per
A2, unpause); add the queue function per A1; keep `Audio.BufferSize`/`Audio.QueueLimit`
config entries; add an `Audio.Enabled` toggle (PD's `g_SndDisabled`) so the level
sweep can run silent.
- `port/src/mixer.c`: rewrite as the op implementations + (for Option A) the decoder /
current-buffer state machine. `mixerInit/Destroy` reset DMEM + op state.
- Threading note: mixer runs on the scheduler thread (`sched.c __scMain` →
`osSpTaskStartGo` inline); with A1 there is exactly one shared object (the SDL queue,
internally locked) — no port-side locks needed.
## 8. Phase 0 — recon (do first; informs everything)
1. **Disassemble `bin/aspboot.text.bin`** (+ `.data.bin`; markers in `src/aspboot.s`) —
GE's real ASP microcode, 16-bit MIPS RSP. This is the ground truth for:
- exact `aSetBuffer` current-buffer semantics (which subsequent ops consume it, and
how),
- `aADPCMdec` state layout (what the pointer in w1 points at — IDO `ADPCM_STATE` +
data addressing),
- **`aPoleFilter`** — the one op with no PD reference code; implement from this,
- any DMEM convention not derivable from libaudio usage.
Methodology mirrors how `rsp/graphics/gmain.s` is used for graphics. A small
16-bit-MIPS disassembler script (`tools_pc/`) is sufficient — we decode, not execute.
2. **Acmd stream logger** (`GE_DAUDIO=1`, in the Option-A decoder or a temporary probe
in the `M_AUDTASK` branch): dump op + decoded args per frame for the first N frames.
Confirms the 15-op inventory at runtime, catches surprises (e.g. ops we didn't grep),
and gives before/after artifacts for validating the implementation.
## 9. Open questions (decide before Phase 1)
- **Q1 — C7/D127 bank-tree guard (`src/snd.c:859`):** root-cause the bogus
`ALSound*` resolutions now (likely an `afFixupInst` layout/stride defect in
`port/src/romdata.c`, same family as D37's bank-tree re-layout), or keep the guard as
a documented stopgap and file a separate finding? Keeping it means some SFX stay
silently missing on affected levels (Surface1) even after audio lands.
- **Q2 — Option A vs B** (§5): A is recommended (no header shadowing, stream logging,
project-consistent with fast3d); B is the PD-proven fallback if current-buffer
semantics prove fiddly at task time. Note for the evaluator: this is a **low-stakes,
cheap-to-revisit decision** — both options share 100% of the DSP implementation code
(§6 layout, all op bodies); only the ~50-line front end (word decoder vs macro
redirects) and the execution point differ. If Phase 0's disassembly surfaces anything
that makes B look safer, switching costs an afternoon, not a re-architecture.
- **Q3 — Pole-filter fidelity bar:** implement `aPoleFilter` strictly from the
microcode disassembly (recommended), or accept a devkit-docs approximation and verify
by ear? The chorus is audible in most music; a wrong filter coefficient shows as tonal
difference, not crash.
## 10. Phases and milestones
- **Phase 0 — Recon** (§8): microcode disassembly notes + Acmd log artifacts committed
to `docs/` (or `tools_pc/`). Exit: current-buffer semantics + pole filter documented;
op inventory confirmed at runtime.
- **Phase 1 — Plumbing.** SDL device opens @22050 stereo s16 (A2); A1 queue path live;
real AI shims (§7.2); `K0_TO_PHYS` shim fix (§7.1); `Audio.Enabled` toggle. Mixer still
a no-op is fine. **Milestone: device drains, `osAiGetLength` feedback loop visible in
logs, clean start/stop, silence (not crash).**
- **Phase 2 — Core ops.** Implement `aSetBuffer/aLoadBuffer/aClearBuffer/aDMEMMove/
aADPCMdec/aResample/aEnvMixer/aMix/aSetVolume/aLoadADPCM/aSetLoop/aInterleave/
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/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/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/porting-notes.md`.
## 11. Verification ritual (audio-specific, on top of the standard one in AGENTS.md)
1. Boot → title: menu music audible within a couple of seconds; no click at start.
2. Enter BUNKER1 (`-level_09`): gunfire/door SFX present and roughly level-correct;
level music plays.
3. No `audio: ai out of samples` spam from `amHandleDoneMessage` (`audi.c:596`) — that
print is the built-in underrun detector.
4. Run 2–3 levels incl. one with heavy SFX overlap; watch `SDL_GetQueuedAudioSize`
(log it) staying below `queueLimit` in steady state.
5. PAL build (`pal-final`) spot-check: 50 Hz retrace drives the same path; frame-size
math uses `MAYBE_FRAME_RATE` — confirm no underruns there too.
6. `Audio.Enabled=0` → fully silent, zero audio CPU cost, nothing else affected.
## 12. Risks / unknowns
| Risk | Mitigation |
|---|---|
| `src/audi.c` | Game audio manager — compiles unmodified; source of `OUTPUT_RATE`, frame-size math, `CUSTOM_FX_PARAMS_N` |
| `include/PR/abi.h` | GE's acmd accessors (16 opcodes, context ABI) — the one non-`port/` edit (`#ifdef PORT` split) |
| `src/libultra/audio/*.c`, `src/libultrare/audio/*.c` | CPU synth + naudio New driver — compile unmodified; call sites define the opcode set |
| `port/src/audio.c` | SDL device + queue (implement Phase 1) |
| `port/src/mixer.c` | Software acmd interpreter + software DMEM (implement Phase 2) |
| `port/include/mixer.h` | Stub interface → replace with `aXxxImpl` declarations |
| `port/src/libultra.c:848-863` | `osAi*` shims — wire to audio.c (Phase 1) |
| `port/src/libultra.c:1237` | M_AUDTASK branch — stays a completing no-op; hosts `GE_AUDIOTRACE` |
| `port/src/ucode.c` | Dummy `aspMain*` markers stay forever under CPU-bypass — document decision |
| `C:\Users\james\Source\Repos\pd_port\port\src\{audio,mixer}.c`, `include\PR\abi.h` | PD ground truth: device glue + DSP cores + the macro-swap pattern |
| IDO current-buffer semantics misread → subtle wrong output (no crash) | Phase 0 disassembly is the arbiter; Acmd logger + by-ear checks per phase |
| `aPoleFilter` has no PD code | Disassembly (Q3); it's one small IIR — bounded effort |
| ADPCM state layout differs from assumption → garbage/cratchy samples | Phase 0 documents the struct from `load.c` usage + microcode; scalar fallback path makes debugging easy |
| C7/D127 hides a bank-tree bug that audio makes *audible* (wrong SFX, not just missing) | Q1; keep the guard until root-caused |
| SDL queue backpressure drops under load (slow machines) | `queueLimit` drop is graceful (silence gap, no crash); A3: raise scheduler-thread host priority before buffer tuning; PD shipped the same design |
| Synthesis cost on the scheduler thread starves gfx frame time / causes underruns | A3: host thread priorities first; a dedicated audio worker thread only if priorities provably insufficient (and revisit D24's fake-priority semantics before designing one) |
| Cross-region: PAL sample-count math differs | Verification step 5 |
## 13. Key references
| File | What it gives us |
|---|---|
| `src/audi.c` | audio thread, frame-size feedback loop, task construction (`OUTPUT_RATE` @22) |
| `src/sched.c:439,465,485` | real scheduler's M_AUDTASK path → `osSpTaskStartGo` interception point |
| `include/PR/abi.h:260–407` | IDO Acmd word formats (the decoder's spec) + A_* opcodes |
| `src/libultra/audio/synthInternals.h:57–71` | DMEM layout constants |
| `src/libultrare/audio/reverb.c`, `env.c`, `drvrNew.c` | GE custom FX (`alFxNew`), pole filter + chorus, envelope ops |
| `bin/aspboot.text.bin` / `.data.bin` (+ `src/aspboot.s`) | **ground-truth ASP microcode to disassemble** |
| `pd_port/port/src/mixer.c`, `port/include/mixer.h` | DSP math reference + the Option-B pattern |
| `pd_port/port/src/audio.c`, `pdsched.c:233–260`, `libultra.c:150–175` | PD wiring (queue design, empty AUDTASK branch, AI shims) |
| `port/src/libultra.c:794–810, 1108` | the three dead seams this plan fills |
| `port/src/dram.c`, `port/shim/PR/R4300.h` | address model (V1/V2 views) + the K0_TO_PHYS gap |
-317
View File
@@ -1,317 +0,0 @@
# CUT-CONTENT-BACKLOG — TCRF "cutting room floor" findings vs decomp/port
Research branch: `research/cut-content-backlog`. Source of truth: the local TCRF
export at `scratchpad/backlog-scrape/restore-cut-content-mod/` (gitignored).
> **Superseded by the verified cross-reference.** All six article *bodies* were
> recovered and every major claim was checked against the decomp source and asset
> tables. The full verified findings (with file:line references, corrections to
> TCRF, and updated mod tiers) live in `backlog/cut-content/RESEARCH.md`
> (gitignored; article markdowns in `backlog/cut-content/articles/`). This doc is
> the first pass (image-filename based) — read it for the per-item imagery index,
> but trust RESEARCH.md where they disagree. Key corrections: (1) "dest" is the
> *shipped* Frigate geometry, not an early version (`bg.c` levelinfotable);
> `cryp`/`dish` are shipped Egypt/Temple; (2) Library/Stack share Basement's bg
> file — they have geometry; (3) all 29 "unused items" exist in the ROM object
> table as pickup+prop pairs except heroine; (4) TCRF's 11 unused cheats, music
> track index, MP character table and build dates all verify 1:1 against
> `bondconstants.h`/`cheat.c`/`front.c`/`compiletime.c`.
## 1. Findings
### A. Debug menu & dev tools — fully in the ROM, fully compiled into the PC build
TCRF main page images: `GEDebugMenu`, `GECheatOptions`, `GoldenEye-displayspeed`,
`GoldenEye-moveview`, `GoldenEye-LineMode`, `GoldenEye-testingmanpos`,
`GoldenEye-gunwatchpos`.
All of these exist in decompiled code:
- **Debug menu** — `src/game/debugmenu_handler.c` (1202 lines). Opened by holding
**U + D C-buttons** (`debug_menu_processor`, ~line 451; wired into `boss.c:551`
and the main game loop). Full option list at lines 116–185: `move view`,
`stan view`, `bond view`, `level` / `region` / `scale` (level switching),
`play title`, `bond die`, `select anim`, `gun pos`, `flash colour`,
`hit colour`, `music`, `sfx`, `invincible`, `visible`, `collisions`,
`all guns`, `max ammo`, `display speed`, `background`, `props`,
`stan hit` / `stan region` / `stan problems`, `print man pos`,
`port close` / `port inf` / `port approx`, `pr room loads`,
`show mem use` / `show mem bars`, `grab rgb` / `grab jpeg` / `grab task`
(screenshots), `rnd walk`, **`record ramrom` / `replay ramrom` /
`save ramrom` / `load ramrom`** (input recording/replay!), `auto y aim`,
`auto x aim`, `agent` / `all` / `fast` (unlock flags), `objectives`,
`marg top/bot/left/right/reset`, `screen size`, `screen pos`,
`show patrols`, `intro` / `intro edit` / `intro pos`, `world pos`,
`gun key pos`, `vis cvg`.
- **Free camera** — `src/game/debug_camera.c` (the "move view" option).
- **Load-all-models tool** — `src/game/deb_loadallmodels.c` (used by
`initguards.c`; dumps every model — how the TCRF screenshots of unused
models were likely made).
- **Line mode** — `CHEAT_LINEMODE` in `src/game/cheat.c:685` (wireframe toggle,
cheat-menu unlockable; `front.c:1017`).
- Screenshot options (`grab rgb`/`grab jpeg`) call `indyGrabRgb32bit()` /
`indyGrabJpg32bit()` — N64 VI grabs; on PC these are the natural hook for the
port's existing screenshot QoL (see `feat/qol-mute-screenshot` lineage).
**Status: Tier 0.** Everything compiles into the PC build (`CMakeLists.txt`
`GLOB_RECURSE src/game/*.c`). The debug menu should already work in the PC port
if U+D C-buttons can be produced by `port/src/input.c`. *Verify in-game* (open
menu, toggle `display speed`, use free cam). Mod value: document it; consider a
config/keybind that synthesizes the U+D c-button hold, and mapping `grab rgb`
to the port's screenshot path.
### B. Cut weapons — fully functional in the ROM, grantable via existing cheat codes
TCRF images (main + prerelease pages): `GoldenEye-Taser[PR]`, `GoldenEye-KF7[PR]`
(+`KF7Watch`), `GoldenEye-GrenadeLauncher[PR]`, `GoldenEye-RocketLauncher[PR]`
(+`RocketLauncherWatch`), `GoldenEye-RifleLauncher` (+`RifleLauncherCheat`),
`GE64_Shotgun1`, watch variants `MagnumWatchPR`, `PP7WatchPR`, `KlobbWatchPR`,
`TaserWatch`.
Evidence in code/assets:
- Model + stat files exist for every cut gun under `assets/obseg/gun/`:
`taser/`, `rocketlaunch/`, `grenadelaunch/`, `fnp90/`, `spectre/`,
`autoshot/`, `ak47/`, `m16/`, `skorpion/` … each with
`gunWeaponStat.inc.c` + `ModelFileHeader.inc.c`.
- `assets/obseg/gun/gunWeaponStats.inc.c`: complete `WeaponStats` for
`taser_stats` (line ~365), `rocketlaunch_stats` (~305),
`grenadelaunch_stats` (~295), plus `watchlaser_stats`, `tank_stats`,
`bombcase_stats`, etc. These are data-driven — the weapon engine
(`src/game/gun.c`, `gunfire.c`) will run them.
- First-person view cases exist: `src/game/bondview_r.c:38` handles
`ITEM_GRENADELAUNCH`, `:41` `ITEM_ROCKETLAUNCH`.
- **Cheat codes grant them**: `src/game/cheat.c` — `CHEAT_2X_ROCKET_LAUNCHER`
(line 711), `CHEAT_2X_GRENADE_LAUNCHER` (712), `CHEAT_2X_RCP90` (713),
plus unlock sequences (`CHEAT_UNLOCK_2XGL/2XRL/2XFNP0`, lines ~695–734) and
the grant logic at lines 1277–1310: `bondinvAddInvItem(ITEM_ROCKETLAUNCH)` /
`give_cur_player_ammo(AMMO_ROCKETS, …)` etc. Note the `#if defined(BUGFIX_R1)`
guards — check which region macros the PC build defines (non-negotiable #3).
- Taser SFX exist: `GUN_TASER_SFX` / `GUN_TASER_LOOP_SFX` (`src/bondconstants.h:2271`).
- `ITEM_TASER` is in the item enum and used as an inventory bound in
`bondinv.c:854` (JP-text-trigger branch) — the taser was closest to shipping.
**Status: Tier 0/1.** Rocket launcher, grenade launcher and FN P90 are
playable today via the game's own cheat codes (single-player). Taser/Spectre/
AutoShot have no cheat path but everything needed is in the binary; a
port-layer toggle calling the same `bondinvAddInvItem` +
`give_cur_player_ammo` pair the cheats use would be a clean mod (no game-logic
change — it replicates an existing code path). Watch-mounted variants
(KF7/Magnum/PP7/Taser watch) are *not* in the item enum — likely cut before
data landed; **unverified** whether any model exists beyond what TCRF shows.
### C. "Unused items" page (intact imagery) — objects with full logic, never placed
The 20 intact images correspond exactly to ROM object entries
(`assets/obseg/file_resource_table.inc.c`, 781 entries):
| Image | Resource ID | Notes |
|---|---|---|
| `GE007-AudioTape` | `AUDIOTAPE` / `CHRAUDIOTAPE` (lines 125, 265) | pickup + prop variants |
| `GE007-BombCase` | `BOMBCASE` (129) | has full `bombcase_stats` (throwable explosive) |
| `GE007-Controller` | N64 controller model (`joypad/` dir in gun table) | desk prop |
| `GE007-WatchMagnetRepel` | `ITEM_WATCHMAGNETREPEL` (`bondconstants.h:4190`) | watch function, use-logic present |
| `GEMicroCamera` | `MICROCAMERA` (176) + `AMMO_MICRO_CAMERA` | |
| `GoldenEye-Weapon_Case` | `ITEM_WEAPONCASE` (`gunfire.c:3066` use-case) | |
| `GE007-PolarizedGlasses` | `POLARIZEDGLASSES` (185) + `ITEM_POLARIZEDGLASSES` | `darkglasses/` also present |
| `GoldenEye-Plans` | `PLANS` (183) | `blueprints/` variant too |
| `GoldenEye-Watch_Identifier` | `WATCHIDENTIFIER` (208) + `ITEM_WATCHIDENTIFIER` (`gunfire.c:3076`) | 4th watch function with use-anim, never spawned |
| `GoldenEye-Safecracker` | `SAFECRACKERCASE` (190) | |
| `GoldenEye-Watch_Communicator` / `Watch_Geiger_Counter` | `ITEM_WATCHCOMMUNICATOR` / `ITEM_WATCHGEIGERCOUNTER` (`bondconstants.h:4188-89`) | shown for contrast — these *are* used (Aztec) |
| `GEGasKeyring` | `GASKEYRING` (157) + `ITEM_GASKEYRING` | |
All have `GUN_ANIM_STATE_USE_ITEM` handling in `gunfire.c:3060-3084` — i.e. the
game can *use* them; level data just never places them.
**Status: Tier 1/2.** Granting any of these to Bond is a port-layer item-spawn
(same mechanism as §B). Actually *placing* them in levels requires ROM/level-data
editing (a level editor or hand-patched object tables) — Tier 2, out of scope
for this repo but viable as a data mod.
### D. Cut multiplayer maps — 8 commented-out menu entries with dev notes
`src/game/front.c:556-573` `multi_stage_setups[]`: the shipped MP list is
Random/Temple/Complex/Caves/Library/Basement/Stack (default) + Facility/Bunker2/
Archives/Caverns/Egypt (progression-unlocked). Immediately below, **commented
out with in-source dev annotations**:
```c
//{... LEVELID_CITADEL ...}, //Citadel (old format setup)
//{... LEVELID_FRIGATE ...}, //dest (has xbla setup)
//{... LEVELID_STATUE ...}, //stat (works)
//{... LEVELID_CRADLE ...}, //crad (works)
//{... LEVELID_AZTEC ...}, //azt (needs setup)
//{... LEVELID_RUNWAY ...}, //runway (xbla setup)
//{... LEVELID_DAM ...}, //dam (xbla setup)
//{... LEVELID_DEPOT ...}, //depot (xbla setup)
```
TCRF imagery matches: `GECitadel*` (5 screenshots, main page),
`GEStatueMulti`, `GECradleMulti`, the whole Depot image set (`DepotCrateStack`,
`DepotRockets`, `DepotSafeTable`, `DepotSiloPipe[Ed]`, `DepotTrailers`,
`DepotPhoto`), and `GoldenEye-FrigateMap`.
The port already boots every one of these standalone (`-level_XX`; see
`docs/dev/LEVEL-STATUS.md` — Statue `-level_22`, Train `-level_25`, etc. all
PASS). So the geometry, AI setup and briefings
(`assets/obseg/brief/UbriefdepoZ.c`, `UbrieftraZ.c`, …) are intact; only the MP
menu entry was removed.
**Status: Tier 3 (mod fork).** Re-enabling = editing a game data table in
`src/game/front.c` — that's a game-logic change, disallowed in this repo
(non-negotiable #2). Viable as a separate mod branch/fork ("8 more MP maps"),
or as a documented cheat-menu route (the debug menu's `level`/`region` options
may already jump to any level ID — verify).
### E. Per-level unused props & geometry ("Unused stuff by level" page)
~150 intact screenshots, grouped by level. Cross-referenced against the object
table and level IDs:
- **Streets** (campaign stage 12; `LEVELID_STREETS`): an entire prop kit was
never placed — `StreetsCars`, `StreetsLamp[Base/Big]`, `StreetsSign[2/3]`,
`StreetsTrafficLight`, `StreetsShovel`, `StreetsCrossing`, plus two cut set
pieces `GE64-StreetsArea1/2` and Ourumov's car driving/crashing
(`StreetsOuruGo`, `StreetsOuruCrash`, `GEOurumovsCar`). Suggests a cut
street-chase scene.
- **Silo**: `SiloVentShaft`, `SiloLadder`, `SiloLaunchDoors`,
`SiloWalkway2Door/Guard`, `SiloMissingHandrail` (a bug/oddity screenshot),
`GoldenEye_UnusedStartingPoint`, and prerelease-only `PRSiloElevator`,
`PRSiloLamps`, `SiloCutWalkway`, **`ThirdSiloEditor`** (editor view of a
third silo that didn't ship).
- **Facility**: PR-vs-final wall sets (`FacilityWalls[Final]`,
`PRFacilityWalls`, `ArchivesWalls`), plus cut rooms: `FacilityBathroom`,
`FacilityConveyor`, `FacilityGasTanks`, `FacilityLabTanks`,
`FacilityWalkway`, `FacilityArmedScientistPR`, `FacilitySatBoxTex`,
`GoldenEye007-FacilityPhoto`.
- **Archives**: `ArchivesOverview`, `ArchivesLamp[Scan]`, `ArchivesPhoto`,
`PRArchivesNoWalls` (wall-less build view).
- **Bunker**: `BunkerExtraMons[Side]` (extra monster/guard spawns),
`BunkerPIP`, PR `BunkerLamp`.
- **Caverns**: `CavernsPursueObj` (a cut "pursue" objective marker), PR
`CavernsWalkway`.
- **Dam**: `DamIsland`, `DamPlatform`, `DamDockDoors`, `DamEndDuct`,
`DamTruckDoor`; region-diff shots `DamCompUS/EU`.
- **Frigate**: `FrigateHarpoonScan` (scan of a harpoon model),
`PRTrainTargetGlass`, `Pdest_harpoonZ[2]`, `Ptorpedo_rack`,
`FrigateMoneypennyPR` (a Moneypenny model in the Frigate PR build!),
region-diff `FrigateIntroUS/JP`.
- **Aztec/Temple**: `AztecConsolePads1-4`, `GE-AztScreen2781/2789`,
`AztExtraMainframe`, `AztDronePadsLabel`, `unused-Aztec-Path` (a whole
unplaced path), `EgyptianRed/WhiteClouds` (two skybox variants),
`TempleSkySolo/Multi`, PR: `AztBlueSuitPR` (cut enemy suit),
`AztHeavyFogPR`, `AztTableScreensPR`.
- **Surface**: `SurfaceBookCrate`, `SurfaceObsPad`, `SurfaceTower`,
`SurfaceJerryCan`, `SurfaceHutTable`, `SurfacePropHut`, PR
`SurfaceOverviewPR`, `SurfaceVent[PR]`, `SurfaceDish[PR]`.
- **Train** (campaign stage 14): `GE007-TrainSecretRoom` (a hidden room),
`TrainTargetGlass`.
- **Misc props/vehicles**: `GEMotorbike`, `GERedTruck`, `GESovietBoat`,
`GEHelicopterPilot`, `GEBiker`, `GoldenEye-moonfemale` (`moonfemale/` chr
model exists), `OurumovKey/Briefcase`, `GEDeskMat`, `GoldenEye-Crypt`
(confirmed orphaned map — see note below),
`GE64-BridgeRooms`, `GEJunglePicture`, `Runwaypicture`,
`Controlicon`, `Cactus`.
- **Orphaned level geometry** — `assets/obseg/bg/` holds per-level dirs for
`ame arch arec ark azt cat cave crad cryp dam depo dest dish len oat ref run
sev sevb sevx silo stat tra`. Notable: `cryp` (Crypt) has full geometry +
briefing (`UbriefcrypZ.c`) but **no `LEVELID_CRYPT` entry** in
`bondconstants.h` — fully orphaned. `len` (Lenin statue area, cf.
`StatueLeninPR`) and `dish`/`cat`/`oat`/`arec`/`ame`/`ark`/`ref` are further
level dirs beyond the shipped set. No `citadel` dir — its geometry was
either removed or shared (TCRF's 5 Citadel screenshots show a finished map,
so it existed in some build).
**Status: Tier 2.** All models are in the ROM's object table (spot-checked:
`PchrtesttubeZ`, `Pdest_harpoonZ`, `Pdoor_roller1-4Z`, `PhatfurryZ`,
`Pletter_tray1Z`, `Pmissile_rackZ`, `PsilencerZ`, `Psteel_door2[ b]Z`,
`Ptorpedo_rack`). Placing them needs level-data editing (object spawn tables) —
a data-mod / level-editor project, not a port change. The PR-vs-final texture
pairs (`*PR__1vt.jpg`) are interesting for a "pre-release textures" cosmetic mod
via the `tools_pc` asset pipeline — but the PR ROM itself isn't available here,
so source assets would have to come from TCRF's screenshots (low-res) or a
donated PR dump.
### F. Unused characters & faces
TCRF main-page imagery: `UnusedFace1/2/3` (each with F/S1/S2 views),
`GE64-FemHead1-4`, `KenFaceUnused/Used`, `Rosika[Compare]`, `SciCompare`,
`FemaleSci`, `GEBonds`, `Terrorists`, `TerroristBiker`, `StPetersburgGuard`,
Bond-suit sets named after actors: `Connery*`, `Dalton*`, `Moore*` (each with
HeadFront/Back/Left/Right + Jacket pieces), `GEArmor[our]`.
Cross-reference:
- The MP character table (`front.c:601+` `mp_chr_setup[]`) is huge — ~40 named
entries beyond the 8 selectable, including `ROSIKA`, `KARL`, `MARTIN`,
`MARK`, `DAVE`, `DUNCAN`, … `TERRORIST`, `BIKER`, `STPETERSBURGGUARD`,
`HELICOPTERPILOT` — Brosnan-body + swapped-head combos. The "unused faces"
are almost certainly extra head models in this pool that no body/entry uses.
- Head model dirs exist under `assets/obseg/chr/`: `headken` (the Ken face),
`headbrosnan*` (4 costume variants), `moonfemale`, plus ~30 named heads.
The actor-named suits (Connery/Dalton/Moore) do **not** appear as chr dirs —
likely cut before model export, or embedded in a shared body file.
**Unverified** which exact models the TCRF face shots show.
**Status: Tier 2/3.** Cosmetic mod (extra MP faces) = asset-level work if the
models are in the ROM; verify by dumping `assets/obseg/chr/*/Model.c` headers.
### G. Prerelease / dev-build evidence
Prerelease page (body blocked; filenames + embeds only):
- PR-vs-final comparison pairs: Taser, KF7, GrenadeLauncher, RocketLauncher
(+watch versions), SurfaceVent/Dish, `StatueLeninPR`, Facility walls/rooms
(§E), ArchivesWalls, CavernsWalkway, SiloCutWalkway, ThirdSiloEditor,
PRSiloElevator, BunkerLamp, OutletConsoles. Pattern: the PR build had more
weapons (watch-mounted guns!) and different Facility/Silo/Aztec geometry.
- **Dev-build videos** (YouTube embeds captured in the export):
- "Goldeneye 007 - Shoshinkai/Space World 1996"
- "Nintendo of America E3 1996 Take Away Video (VHS Rip)"
- "Goldeneye Beta WalMart Video"
- "Goldeneye 007 Promotional Trailer 1996"
- "N64 vous n'en reviendrez pas (VHS RIP 1997)"
- "64History - N64 B-Roll (1995)"
- "Escape (Banjo-Kazooie, GoldenEye 007)" — engine sibling footage.
Useful for dating PR features; not actionable in-repo.
### H. Misc oddities (main page, unverified)
- `GoldenEye007MickeyMouse` (by-level page, 2 KB image) — no "mickey" object in
the resource table; likely a texture/model easter egg spotted in a level.
- `Unused_bond_walk.gif` — an unused Bond walk animation (check
`assets/animationtable_entries.c` for orphaned anim IDs).
- `GoldenEye007-MovieLogoUnused` / `-MovieNoLogoUnused` — alternate intro logo
frames (title sequence data; `src/game/brief*`? verify).
- `GE_UnusedAlarm`, `GoldenEye-BloodImpact` / `BoxBlood` / PR `OpaqueBlood` —
blood-effect variants (cf. `src/game/blood_animation.c`).
- `GoldenEye007-BarrelUnused` / `-SmallBarrelUnused` — barrel props never placed.
- `GoldenEye-OddDay`, `Sight[Alt]`, `MountainTex`, `background`, `Pbin1Z`,
`PchrtesttubeZ[Explode]`, `PitonGunBungee`, `Psev_door[3_wind]Z` — props in
the table, placement unverified.
- Bugs & Notes articles: **nothing recoverable from this export** — re-scrape.
## 2. Feasibility summary
| Tier | Item | Effort | Where it lands |
|---|---|---|---|
| **0 — already works** | Debug menu (U+D C-hold), free cam, line mode, ramrom record/replay, cheat-grantable rocket/grenade/P90 | verify in-game; document keybinds | docs + maybe `input.c` keybind for U+D c-buttons |
| **1 — port-layer hook** | Grant taser/spectre/autoshot + unused items (watch identifier, bomb case, …) via config toggle replicating the cheat grant path (`bondinvAddInvItem` + `give_cur_player_ammo`) | small; follow F10-toggles pattern (`feat/f10-game-toggles`) | `port/src/` only |
| **2 — data/asset mod** | Place unused props per level; extra MP faces; PR texture swap | needs level-editor or object-table patching; separate project | data mod, not this repo's code |
| **3 — game-logic change (mod fork)** | Re-enable 8 cut MP maps (`front.c` table), campaign reordering | one-line-per-map table edits + setup fixes ("xbla setup" = unknown editor flag) | separate mod branch/fork only |
## 3. Next steps
1. **In-game verification pass** (PC build): open debug menu; confirm free cam,
`display speed`, level switch; enter the 2X rocket/grenade/P90 cheat codes;
screenshot via `grab rgb`. Record results here + findings.md if anything new
breaks (Dxx).
2. Confirm which region macros the PC build defines vs the `BUGFIX_R1` guards in
the weapon-grant cheats (non-negotiable #3) — the grants may be dead code in
our build.
3. Dump `assets/obseg/chr/` model headers to identify the UnusedFace1-3 /
FemHead1-4 models concretely.
4. ~~Re-scrape TCRF article bodies~~ — **done**; all six bodies recovered into
`backlog/cut-content/articles/` and cross-referenced (see RESEARCH.md §0).
5. If Tier-1 item granting is wanted: spec it as an F10 toggle set, port-layer
only, each entry citing the cheat-code precedent in `cheat.c`.
-170
View File
@@ -1,170 +0,0 @@
# D172 — particle/flares wrong color (magenta/cyan blood): ROOT CAUSE FOUND + FIXED (M-82)
Symptom (user-reported, GRAPHICS-BACKLOG line 31): bullet impacts / flares / blood spatter
render magenta/cyan instead of dark red.
## TL;DR (M-82)
**Root cause:** `gfx_lod_tile_offset()` (`gfx_pc.cpp`) returns `0` unconditionally on
the `!gfx_detail_textures_enabled` path (a D107 blurry-ceiling fix — and
`port/src/video.c:225` sets that flag `false` at startup). Particle draws use
`G_CC_INTERFERENCE` (`TEXEL0 * TEXEL1`) with tile 0 = IA8 smoke @ TMEM 0 and
tile 1 = RGBA16 fire @ TMEM 0x188 — but with the offset pinned to 0, **texunit 1
samples the smoke texture too**. `smoke * smoke` through the rest of the combine
= the magenta/cyan. Confirmed by the `GE_D172` probe on a user Silo firefight:
40/40 particle tris logged `cfg1[tile=1 tmem=392 fmt=RGBA]` but
`SAMPLED1=tile0(tmem=0 fmt=IA)`.
**Fix (`gfx_pc.cpp` `gfx_lod_tile_offset`):** `return rdp.tex_lod ? 0 : i;` —
fold to the base tile only when LOD is actually active (D107's mip case,
`tex_lod=1`, unchanged); a genuine non-LOD 2-texture combine now gets tile `i`.
`bunker1` verify PASS (the D107 repro level). Committed on PR #34. **Owed: an
in-game eyeball that blood/sparks now read dark red.**
The 0xB9 / cycle-type false trails and the probe design are kept below for the
record.
---
Original note (superseded): Static audit done; one strong root-cause candidate
with a cheap in-game verification. **No code changed.**
## 1. How particles get their color state (mechanism)
Particle triangles (`explosion.c`, `gSPVertex` + `G_TRI_FILL`/`G_TRI_SHADE_TXTR`) carry
**no per-primitive material command**. Their CC/tile state comes from replaying ROM-resident
state records via `gSPDisplayList(gdl++, g_ExplosionDisplayLists[var_s2])`
(`explosion.c:894`, 15-record loop, one record + its particles per iteration).
- Records: `globalDL_0x078 … globalDL_0x9a8`, a 17-entry table at image-bank offset
0x28 (`g_pc_gimg_off_globalDL_*`), bank base cart 0x1029D160 (see `image_bank.c:322`).
- Each record is also scanned at load by `texLoadFromDisplayList` (`src/game/image.c:475`)
for its `G_SETTIMG` + ABCD marker word — that part works (textures upload fine).
## 2. Record layout (decoded from ROM)
```
+0x00 E7 C3 FF FE G_RDPPIPESYNC
+0x08 B9 00 03 1D ??? <-- UNHANDLED by fast3d (see §4)
+0x10 <varies> second word of the ??? command
+0x18 FC ... G_SETCOMBINE w0 (rgb c0 | alpha c0 <<16)
+0x20 FF ... G_SETCOMBINE w1 (rgb c1 | alpha c1 <<16)
+0x28 FD ... G_SETTIMG w0 (timg pointer)
+0x30 .. 50 50 50 50 G_SETTIMG w1 (ABCD marker)
```
Dominant particle combine mode (14 of 17 records), decoded with the standard libultra
bit layout:
- cycle 0 RGB: `TEXEL0, 0, TEXEL1, COMB_A`
- cycle 0 ALP: `TEXEL0A, COMB_A, SHADE, COMB_A`
- cycle 1 RGB: `COMBINED, 0, SHADE, COMB_A` ← **2-cycle** modulate
- cycle 1 ALP: same as cycle 0
## 3. fast3d audit (what is right)
- `G_SETCOMBINE` bit extraction (`gfx_pc.cpp:2786`) matches the standard libultra layout
exactly (verified field-by-field against the ROM words). Not the bug.
- Store/decode of `rdp.combine_mode` (`gfx_dp_set_combine_mode` vs `gfx_generate_cc`
key shifts) are consistent. Not the bug.
- The generated GLSL combiner implements the RDP formula with the usual special cases;
`SHADER_COMBINED` correctly resolves to the previous cycle's running value.
## 4. The 0xB9 root-cause candidate is WITHDRAWN (M-82, 2026-09-08)
The earlier audit claimed fast3d has "no case for opcode 0xB9" and that the D146
`default:` guard therefore aborts every particle sub-DL. **That is wrong.**
This build does **not** define `F3DEX_GBI_2` (verified: no `-DF3DEX_GBI_2` in
`CMakeLists.txt`, no `#define` in `include/PR/gbi.h` or anywhere in tree). In the
non-F3DEX2 branch of `gbi.h`, `G_SETOTHERMODE_L = G_IMMFIRST-6 = -71`, and
`(uint8_t)(-71) == 0xB9`. So `gfx_pc.cpp:2731` `case (uint8_t)G_SETOTHERMODE_L:`
**is** `case 0xB9:`.
Decode of the record word `w0 = 0xB900031D`, `w1 = <varies>`:
`gfx_sp_set_other_mode(C0(8,8)=3, C0(0,8)=29, w1)` → sets `other_mode_l` bits
3..31. `sft=3 == G_MDSFT_RENDERMODE`, `len=29` — this is exactly
`gsDPSetRenderMode(w1)`. `w1` values (`0x0C184B50`, `0x00504B50`, …) are
render-mode / blender words. fast3d applies it correctly.
So the particle sub-DL runs to completion on PC: `E7` RDPPIPESYNC (handled),
`B9` SetRenderMode (handled), `FC` SETCOMBINE (handled), `FD` SETTIMG (handled).
**No D146 abort. No stale-state fallthrough.** The wrong-colour bug is elsewhere.
## 5. Cycle-type is ALSO ruled out — the real record, and where the bug is (M-82)
The actual particle records are in **`assets/oddtextures.c`**
(`globalDL_0x078` … , 17 of them). One record verbatim:
```c
gsDPPipeSync(),
gsDPSetCycleType(G_CYC_2CYCLE), // <-- record DOES set 2-cycle
gsDPSetRenderMode(G_RM_PASS, G_RM_ZB_CLD_SURF2),
gsDPSetTextureLOD(G_TL_TILE),
gsDPSetCombineMode(G_CC_INTERFERENCE, G_CC_MODULATEIA2),
gsSPTexture(..., G_TX_RENDERTILE, G_ON),
// tile 0: IMAGE_SMOKE_0 IA16 image -> loaded/rendered as IA8, 56x56
// tile 1: IMAGE_FIRE_0 RGBA16, 16x14, loaded to tmem 0x188
gsDPSetTextureLUT(G_TT_NONE),
gsDPPipeSync(),
gsSPEndDisplayList(),
```
So the record **explicitly sets `G_CYC_2CYCLE`** (opcode `0xBA` =
`G_SETOTHERMODE_H` in this build), and fast3d handles `0xBA` correctly:
`gfx_pc.cpp:2734` → `gfx_sp_set_other_mode(52, 2, w1<<32)` sets `other_mode_h`
cycletype = 2CYC. **Confirmed by probe (M-82):** the `GE_D172=1` SETCOMBINE
probe logged the particle combine `w0=fc26a004` (= `G_CC_INTERFERENCE`/
`G_CC_MODULATEIA2`) with **`cycletype=1 (2CYC)`** every time. The cycle-type
hypothesis is dead too.
**The bug is the `G_CC_INTERFERENCE` two-tile combine / TEXEL1 path.**
`G_CC_INTERFERENCE` cycle-0 = `TEXEL0 * TEXEL1`; the record binds **two
different tiles of two different formats** (tile 0 IA8 smoke, tile 1 RGBA16
fire at tmem 0x188). Magenta/cyan = a TEXEL1 sampling/decode fault: wrong tmem
offset for tile 1, RGBA16 (5551) channel/endian misread, or fast3d treating
tile 1 as an LOD mip of tile 0 under `G_TL_TILE`. This needs a visual
render-iterate pass (headless can't see particle colour) — see §6.
### Probes shipped (M-82, `#ifdef PORT`, `GE_D172=1`, inert when unset)
- `gfx_pc.cpp` G_SETCOMBINE case — logs each distinct combine + active
cycletype (dedup, 64 max). Confirmed the particle combine `fc26a004` runs
at `cycletype=2CYC`.
- `gfx_pc.cpp` `gfx_sp_tri1` (after the texunit loop) — whenever a **2-cycle
triangle actually consumes TEXEL1**, dumps `combine_mode` + both texunits'
`tile / tmem / fmt / siz`. This is the decisive probe: a Silo close-range
guard kill with `GE_D172=1` should log
`T0[... fmt=3(IA) siz=1(8b)] T1[tmem=392 fmt=0(RGBA) siz=2(16b)]`.
If `T1.tmem != 392`, or `T1.fmt/siz` are wrong, the TEXEL1 bind is the bug.
If they're all correct, the fault is in `import_texture_rgba16` for the
second texunit or the `G_CC_INTERFERENCE` CC generation — pair with
`GE_DTEX=1` / `GE_TEXRAW=1` to dump the fire tile's bytes.
## 6. Fix path (next session — needs a display)
1. Get a `-level_XX` capture with visible particle spray (Silo guard kill at
close range per the original report, or any wall-impact spark). Note the
colour.
2. Env-gated probe in `gfx_pc.cpp` `import_texture` / the TEXEL1 bind path:
for tile 1 when combine == the particle key, log format/size/tmem-addr/
line_size and the first few decoded texels. Compare against the RGBA16
`IMAGE_FIRE_0` bytes in the extracted asset.
3. Likely fixes, in order of suspicion: (a) tile-1 tmem address 0x188 not
honoured → TEXEL1 reads tile-0 bytes; (b) RGBA16→RGBA32 decode for the
second texunit; (c) `G_TL_TILE` LOD path picking the wrong tile for TEXEL1.
4. Re-decode `G_CC_INTERFERENCE` / `G_CC_MODULATEIA2` as fast3d generates them
(`gfx_cc.cpp`) and confirm the GLSL matches `TEXEL0*TEXEL1` then
`COMBINED*SHADE`.
## Files
- `src/game/explosion.c` — record table (165), replay loop (894)
- `src/game/image_bank.c:322`, `src/game/image.c:475` — record offsets / load-time scan
- `port/fast3d/gfx_pc.cpp` — SP walker cases (~2600–2930), D146 default (2920),
SETCOMBINE decode (2786), combine store (2270), cc generation (312+), 2CYCLE flag (1523)
- `rsp/graphics/gmain.s` — N64 ucode ground truth for 0xB9 (dispatch table at dmem 0xbc;
ginit.s missing from repo)
Note (M-82): 0xB9 is `G_SETOTHERMODE_L` in this non-F3DEX2 build and is already
handled by fast3d — `gmain.s` / `ginit.s` are NOT needed to resolve D172.
-127
View File
@@ -1,127 +0,0 @@
# D176(a) Sky — consolidated research notes (read-only)
Companion to `GRAPHICS-BACKLOG.md` D176(a) and findings.md M-37/M-47.
Purpose: make the next implementation session mechanical. No code changed by
this doc.
## State of play
- **Root cause (M-37):** `skyRenderTri`/`skyRenderFull` emit geometry only as
`G_RDPHALF_*` immediates; `gfx_pc.cpp:2901-2903` deliberately no-ops them
(same in the PD port) → solid black sky on every cloud-sky level. Clean
headless repro: `-level_22` (Statue), frame ~360.
- **M-47 draft (Path B)** — `scratchpad/sky.patch`: `#ifdef PORT` hooks in
both functions re-emit the sky as ordinary screen-space GBI via
`skyPortRenderPoly` (ortho pixel projection + `gSPVertex`/triangles).
Geometry reaches GL and textures (top-half luma 0.1 → ~174 on `-level_22`).
Two open defects (below). Draft, not shippable.
- **Path A** (decode RDPHALF in fast3d) is the backlog's original suggestion;
the wire format is now fully decoded (next section), so it is a viable
fallback if Path B's units can't be pinned.
## RDPHALF wire format (decoded from `src/game/sky.c` + `include/PR/gbi.h`)
- Opcodes (`gbi.h:102-164,216-223`): `G_RDPHALF_1 = 0xe1`,
`G_RDPHALF_2 = 0xf1` (final word of a stream), `G_RDPHALF_CONT`
(= `G_IMMFIRST-13`). Tri ops: `G_TRI_FILL 0xc8`, `G_TRI_FILL_ZBUFF 0xc9`,
`G_TRI_SHADE_TXTR 0xce`, `G_TRI_SHADE_TXTR_ZBUFF 0xcf`.
- **All payload values are S15.1 fixed point** — converter is
`sub_GAME_7F094298` (`sky.c:218`): `clamp(x, ±32767.9) × 65536`, u32.
- **Packing:** each `(G_RDPHALF_1, G_RDPHALF_CONT)` pair carries four
halfwords = **two S15.1 values**: `valueA = (w1.hi16, w2.lo16)`,
`valueB = (w1.lo16, w2.hi16)`. Multi-value groups are streamed in two
passes — all high halves first, then all low halves (see the explicit
`(x & 0xffff0000) | (y & 0xffff0000) >> 16` packing at `sky.c:1854-1870`).
- **`skyRenderTri` header** (`sky.c:1725-1728`): first word =
`(op << 24) | (winding/sign flag bit23) | lo16(first value)`; the stream
then carries screen-space positions (`unk28×0.25` = pixel X, `unk2c` = Y×4
family) plus per-vertex attributes. The RSP ucode reassembles and DMAs a
real edge-walked RDP tri command (per the M-46 comment in sky.patch).
- **Vertex attribute vectors are 8 components** (`sky.c:1759-1776`):
`(r, g, b, a, S·w′, T·w′, 32767·w′, z)` with perspective weight
`w′ = unk34 × min(unk0c)/2` — i.e. color + perspective-corrected S/T +
depth. `SkyRelated38` fields: `unk28 = screenX×4`, `unk2c = screenY×4`,
`unk30 = z (0..0x7fff)`, `unk34 = 1/w`, `unk20/unk24 = S/T`.
- GE emits RDPHALF **nowhere except sky.c** — the command surface is closed
and small.
## Defect 1 — texcoord shear (clouds smeared into streaks)
`skyPortRenderPoly` uses `tc = unk20 × 32.0f` — a magic number.
### M-82 update — the scale is right, the magnitude is the bug
- **Cloud texture is `s_skywaterimages[0]` = `IMAGE_CLOUDS_GRAYSCALE`,
`0x40 × 0x40` (64×64), `G_IM_FMT_IA` / `G_IM_SIZ_8b`, `G_TX_WRAP`/`G_TX_WRAP`**
(`assets/oddtextures.c:646`). Not a fraction-indexed texture — it *tiles*.
- **GE's own screen-space textured-quad idiom is `tc = width << 5`**
(`src/game/glass2.c:678,689` — `sImageTableEntry.width << 5` /
`.height << 5`). `<< 5` = ×32 = the S10.5 texel fixed-point fast3d expects,
and it divides `tc` by `tex_width` before GL_REPEAT. So the patch's `× 32.0f`
**is the correct scale** — provided `unk20`/`unk24` are already in *texel*
units (they are: the cloud-tiling math in `sub_GAME_7F097388` /
`CloudRepeat(skyheight)=10000` produces texel coords in the thousands, and
M-47 measured `unk20` reaching ±20 000 near the horizon).
- **The bug: `(s16)(20000 * 32)` overflows.** `Vtx.tc` is `s16` (±32767); a
horizon vertex needs `tc` ≈ 640 000. It wraps/saturates → the streak.
- **Fix (no fast3d change, no grid needed for the near/mid sky):** phase-fold
every vertex of *one primitive* by the **same** multiple of the tile period
(64 texels), so inter-vertex deltas are preserved but magnitudes stay in
s16 range:
```c
s32 foldS = (s32)floorf(vmin_unk20 / 64.0f) * 64; // vmin over this prim's verts
s32 foldT = (s32)floorf(vmin_unk24 / 64.0f) * 64;
tc[0] = (s16)((v[i]->unk20 - foldS) * 32.0f);
tc[1] = (s16)((v[i]->unk24 - foldT) * 32.0f);
```
This is exact wherever the S/T *span across a single triangle* is
< ~1000 texels (s16 range ÷ 32). It holds for the corner-quad tessellation
the current patch emits everywhere except triangles that straddle the
horizon line, where the span diverges — those still need Defect-2's
finer/adaptive tessellation. Land the fold first (fixes most of the frame),
keep the grid as the horizon-band follow-up.
## Defect 2 — vertical coverage (band fills only top ~25%)
Leading hypothesis, from reading the N64 path: **the quad is not a plain
corner quad.** The N64 stream's four vertices are *perspective-interpolated*
points computed in the big interpolation block (`sky.c:1797-1813`, the
`sp210/sp290/sp2b0/sp230` vectors), not the raw TL/TR/BL/BR corners that
Path B's hook passes through. If those interpolated y-values differ from the
corner ys (they will, wherever w varies across the quad), the re-emitted
quad lands short.
Next steps (one focused session):
1. Per-call probe on `skyPortRenderPoly`: log nverts + each vertex's
(x, y) + the tri op-header word; A/B against N64 reference footage of
`-level_22`/`-level_36`.
2. If confirmed, either (a) replicate the interpolation block under PORT and
emit the 4 computed vertices, or (b) drop to Path A with the decoder spec
above (the ucode's own reassembly is then irrelevant — we decode the same
values directly).
## Recommendation
Finish **Path B** first — geometry already works; both defects are unit/
vertex-set questions answerable in one render-iterate session. Keep Path A
as fallback; it is now fully specified by this doc if B stalls.
## Verification ask (M-82 — for the next session with a display)
The Defect-1 phase-fold is committed to PR #18 but **unverified visually**
(this session had no display). One check settles it:
1. `./build-pc/ge007.x86_64.exe -level_22` (Statue, night). At the intro the
camera faces the horizon within a few seconds.
2. Look at the upper half of the frame:
- **Black** → Path B still not emitting (regression from the rebase —
check `skyPortRenderPoly` is reached; `GE_D176=1`).
- **Clouds, but smeared into horizontal streaks** → the fold didn't take;
re-check `unk20`/`unk24` really are texels (probe their range).
- **Recognisable tiled cloud texture, correct near/mid, ragged or missing
right at the horizon line** → fold works; that ragged band is Defect 2
→ adaptive tessellation is the next task.
- **Looks right** → close Defect 1, mark PR #18 ready pending a
`-level_36`/`-29`/Dam-exterior spot check.
3. A `GE_PCDUMP="330-390:30"` capture of that run attached to PR #18 is enough
for a remote reviewer to make the same call.
+5 -68
View File
@@ -1,9 +1,7 @@
# GE_* environment probes — consolidated list
Every `getenv("GE_…")` in the tree. The table prose is hand-curated;
**`tools_pc/gen_env_probes.py`** re-greps the live sites and reports drift
(NEW / GONE vars + a fresh file:line map) — run it before trusting the
File:line cells. Last reconciled 2026-09-20 (v0.3.0 release review). All are **env-gated**: unset = zero behavior
Every `getenv("GE_…")` in the tree, from a grep of `src/` + `port/` on
2026-08-31 (master `31d036da`). All are **env-gated**: unset = zero behavior
change, so they are safe to leave in a release build (they only cost a
`getenv` on the guarded path). "Dead" below means *the finding it was built
for is closed* — the probe is strip-candidate scaffolding, not that it does
@@ -24,65 +22,18 @@ Two classes:
| `GE_INPUTLOG` | `port/src/input.c:329`, `port/src/config.c:53` (`configGetInputLog`) | Logs computed pad state (buttons + stick) each poll when non-zero. Also `[Debug] InputLog` ini. Also traces D165 `menuptr est/tgt/eff/stick`. | **live** |
| `GE_STARTMENU` (+ `GE_STARTMENU_PAGE`, `GE_STARTMENU_DIFF`) | `src/game/lv.c:398-402` (`#ifdef PORT`) | Boot straight into a front-end menu id (13=MISSION_COMPLETE, 10=BRIEFING, 7=MISSION_SELECT, 12=MISSION_FAILED, 6=MODE_SELECT); `_PAGE`=folder row (def 1=Dam), `_DIFF`=0..3. Crash-test a screen fast. | **live** |
| `GE_UNLOCK_ALL` | `src/game/debugmenu_handler.c:1098` (`get_debug_enable_all_levels_flag`, `#ifdef PORT`) | Mission-select shows every solo level (playtest jump-to-any-level aid). Cached in a `static int c`. | **live** |
| `GE_DEBUG_UNLOCKALL` | `src/game/file2.c:404` (`#ifdef PORT`) | Forces the "all levels unlocked" flag TRUE regardless of the shipped save — same effect as `GE_UNLOCK_ALL` but at the file2 unlock-query site. Cached in a `static int`. | **live** (playtest aid) |
| `GE_DEBUGMENU` | `src/boss.c:566` (`#ifdef PORT`) | Makes the L+R debug menu respond to live controller input (C-up+C-down open/close, stick + buttons read from pad 0) instead of the `#ifndef DEBUGMENU` path; unset reproduces that path exactly. Cached in a `static int`. | **live** (playtest aid) |
| `GE_OPTIONSOVERLAY` | `port/src/optionsoverlay.c:231` | Force-enable the F10 port-layer options overlay (D184 / PR #10) regardless of ini. `atoi != 0`. | **live** |
| `GE_D204` | `port/src/audio.c` (`audioSetNextBuffer`) | **Audio-health monitor (D204).** One line per 5 s of wall clock: `rt=` produced-audio/wall ratio (1.000 healthy; below 1 = the 30 Hz audio retrace is starving and the device is padding with silence), `q=` SDL queue depth / `queueLimit` (pinning at the limit with `drop=` climbing = overproduction, the "eventually no audio" state), `drop=` blocks discarded, `max=` largest block vs its `g_MaxFrameSize*4` allocation. **Deliberately cheap (~30 clock reads/s) — safe to leave on for a full playtest, unlike `GE_MIXERTRACE`.** The instrument of record for D202. | **live** |
| `GE_D204_OLD` | `port/src/audio.c` (`d204OldMode`) | Restores pre-D204 behaviour (`osAiGetLength()` reports the whole SDL queue depth; `queueLimit` back to 8192) so before/after can be A/B'd in **one binary**, without build-to-build variance. Temporary — remove when D204 closes. | **live** (D204 A/B) |
| `GE_AUDIOTRACE` | `src/snd.c` (`sndPlaySfx`, `sndDeactivate`), `src/game/propobj.c` (door sounds), `src/libultra/audio/load.c` (`alLoadParam`) | Per-`sndPlaySfx` resolution trace to `audiotrace.log`: `soundIndex` → `ALSound*`/keyMap/wavetable/base/len/book, the returned `ALSoundState*`, every `sndDeactivate`, and wavetable bind events. M-65 additions: `[VOICE+]`/`[VOICE-]` pairing every voice acquire/release with sound/state/flags (NB: written from two threads to one unbuffered FILE* — occasional torn lines, parse defensively), `[SLOTWRITE]` door slot ownership, `[RETRIGGER-POST]`, `[WAVELOOP]` (per-sound `ALADPCMloop` presence), `[DOORSND]` every door sound state change incl. an `ORPHAN-both-slots-busy` verdict. M-66b addition: `[EXPIRE]` — posted whenever the port-side ownerless-infinite-loop expiration is scheduled (sound/state/delay/fade); this line is **permanent** (part of the D202 option-C fix), unlike the rest of the probe set which is removable on close. M-67/M-69 additions: `[WIRE] t=µs filter=… <- table=…` (`src/libultra/audio/load.c`, every wavetable→decoder bind, µs-timestamped) and `[VOICEMAP]` (`src/libultra/audio/synthesizer.c`, full PVoice→decoder/resampler/env-state address map at synth init — this is how offline voicedump slot addresses tie to WIRE filter addresses). M-68 additions: `[EVT] t=µs type=N state=P` + `[STOP-EVT] … deltaUs=N` (event types per `src/snd.h`: 1=PLAY, 2=STOP, 8=VOL, 64=DECAY, 128=END). M-71 additions: `[VOL] t=µs state=… rawVol=… playing=…` (`src/snd.c`, per-VOL-event value before the slot-volume law) and `[DISTVOL] pos=(…) player=(…) dist=… vol=…` (`src/game/propobj.c`, the distance-attenuation funnel `sub_GAME_7F053894` — this is how an SFX's final volume is computed from hit position). NB: lines from the multiple file handles interleave — occasional torn/corrupt lines, parse defensively. `dumppos=` gives the exact byte offset into `audiodump.raw` (quantised to the 2880-byte audio block). Low cost. | **live** (D202/D205) |
| `GE_PULLTRACE` | `src/libultra/audio/load.c` (`alAdpcmPull`) | `[PASTEND]` assertion: fires if a voice's `f->sample` exceeds its wave's total samples while `nOver == 0` (i.e. an unstopped voice keeps decoding past the end instead of zero-filling). Rate-limited to 40 lines. M-65: 0 hits — exhausted voices go silent correctly. | **live** (D202) |
| `GE_DMEMWIPE` | `port/src/mixer.c` (`aClearBufferImpl`) | Zeroes the whole software DMEM at the frame boundary (detected as `alMainBusPull`'s clear of `AL_MAIN_L_OUT`, addr 1088). Tests whether audio state survives across frames in DMEM. M-65: no-op on output — ruled out. | **live** (D202) |
| `GE_NOWET` | `port/src/mixer.c` (`aSetVolumeImpl`) | Mutes the reverb send (`wetamt = 0`), isolating the reverb delay lines as a possible source of persistent signal. M-65: drone unchanged — ruled out (NB `aPoleFilterImpl` is still an unimplemented no-op, D199). | **live** (D202) |
| `GE_OBJT` | `src/game/chraction.c` (`chrSetStageFlags`/`chrUnsetStageFlags`), `src/game/propobj.c` (GAS_RELEASING destruction) | D321 stage-flag writer probe: logs every low-16-bit write to `objectiveregisters1` (tick, writing chr, actiontype/aioffset, AI-list ID) plus each toxic-gas prop destruction timestamp — used to map the Facility tanks→combat-bit trigger chain. Capped, read-only, inert unset; post-release removal candidate (M-192 item 7). | **live** (D321) |
| `GE_BANKDUMP` | `port/src/romdata.c` | `BANKSRC` lines: the source ROM-layout bank tree per `soundIndex` (instrument → sound → envelope pointers + `decayTime`). Used to prove sound 203's `decay=-1` is genuine ROM data, not a conversion bug. One-shot at load. | **live** (D202) |
| `GE_FORCEALARM` | `src/game/propobj.c` (`handle_alarm_gas_timer_calldamage`, `#ifdef PORT`) | **D207 diag (temporary).** Forces the in-game alarm klaxon (`alarmActivate`) on a frame counter — on ~2–10 s, off, on again — and emits a per-frame `[D207] f=… active=… ptr_alarm_sfx=… playstate=… locked=…` line to `audiotrace.log`. Repro harness for the "alarm SFX overrides other audio" report when a level's own alarm can't be triggered headlessly. Remove when D207 closes. | **live** (D207) — temporary |
| `GE_KEYMAPDUMP` | `port/src/romdata.c` | `KEYMAP` + `ENVSRC` lines: raw source bytes of every `ALKeyMap` / `ALEnvelope` in the ROM-layout blob, for byte-wise comparison against runtime values. One-shot at load. | **live** (D202) |
| `GE_AUDIODUMP` | `port/src/audio.c` (`audioSetNextBuffer`) | Raw-dumps every mixed s16/stereo/22050 Hz buffer to `audiodump.raw` in the CWD for offline waveform inspection. Length ÷ 88200 = seconds of audio produced. | **live** (D202) |
| `GE_VOICEDUMP` | `port/src/mixer.c` (`aEnvMixerImpl`) | Dumps each voice's resampled mono stream (post-resample/pre-envelope, i.e. the env-mixer input) to `voicedump.raw` as records of `[u32 stateAddr][u32 nSamples][u64 us][s16 × n]`. **The µs stamp is when the block was MIXED — records are rendered ahead, so a record's samples END at its stamp** (M-69: treating stamps as start offsets misplaces content by up to one ~33 ms block). `stateAddr` = the env-mixer state address from `[VOICEMAP]`. Used with `scratchpad/exact_match.py` for full-corpus per-voice sample verification. | **live** (D202) |
| `GE_MIXERTRACE` | `port/src/mixer.c`, `src/audi.c` (`amDmaCallback`), `src/libultra/audio/load.c` (`_decodeChunk`, `alLoadParam`) | Per-opcode mixer trace to `mixertrace.log` (DMEM addresses, ADPCM books, decoded frames, `[DMAREQ]`/`[DMAHIT]`/`[DMAMISS]`/`[BINDTABLE]`). **WARNING: unbuffered per-opcode `fprintf`, ~25 MB/min. It slows the process enough to starve the audio thread — M-63 measured "audio at 13 % of real time" that was entirely this probe's own artifact. NEVER use it for timing-sensitive measurement; use `GE_D204`.** Grep it, never read it whole. | **live** (D202) — timing-unsafe |
| `GE_TEXPITCH` | `port/fast3d/gfx_pc.cpp:924` (`gfx_tex_pitch_fix`, cached) | `=0` disables the D183 defensive de-stride in `import_texture` (default on). RC2/RC3 texture-shear A/B knob. | **live** (RC3 not finished) |
| `GE_TEXRAW` | `port/fast3d/gfx_pc.cpp:1079` | With `GE_TEXDUMP`, also dumps the raw pre-import source bytes (`texdump/rNNN_f…_s…_WxH.bin`) so a decode bug can be told from a source-data bug offline (D183). | **live** (RC3 not finished) |
| `GE_D219RAW` | `src/game/image.c` (`texReadUncompressed`/`texChannelsToPixels`, `#ifdef PORT`) | Diagnostic-only, print-only (no behavior change): logs the runtime texture decompressor's bitstream header (texnum/format/width/height/compmethod) for every non-zlib wide-pixel image, plus a dump of the first row of decoded texels exactly as stored in the pool (post-`PORT_PIXEL16`, i.e. byte-swapped for the 16-bit family) for RGBA16/RGB15/IA16. No texnum filter — lets any suspect texture be cross-referenced against the offline ROM census (`scratch/d219_census.txt`) without narrowing by hand. | **live** (D219, closed — kept for future runtime-decoder triage) |
| `GE_WRAPFIX` | `port/fast3d/gfx_pc.cpp:3166` (`gfx_set_wrap_fix`) | RC3 test override for the `Video.WrapFix` D74/D167 wrap-block path (`atoi != 0` wins over the ini). | **live** (RC3 not finished) |
| `GE_SAVELOG` | `src/game/file.c:46`, `src/game/file2.c:16` (`SAVELOG` macro), `src/boss.c:749`, `src/game/objective_status.c:297,343` | Traces the campaign save/unlock chain (objective completion → `end_of_mission_briefing` → `fileWriteSave` → EEPROM). Used to confirm D157. `g_savelogObjOnce` gates the per-criterion dump to the `bossReturnTitleStage` call. | **live** (strip once a few more level boundaries are playtested) |
| `GE_D160` | `src/boss.c:744`, `src/game/bondview2.c:791`, `src/aicommands.def:10055,10181,10236,10242,10247` (all `#ifdef PORT`) | Dam exit-cutscene (D148/D160) diagnostic: trace points in `bossReturnTitleStage`, `bondviewSetCameraMode`, and AI cmds `EndLevel`/`exit_level`/`CameraLookAtBondFromPad`/`CameraSwitch`. **Investigation in progress** — user owes a `GE_D160=1` Dam run. | **live** |
| `GE_D243` | `src/game/front.c` (`menu_init`, cached `s_d243`) | Per-menu-transition trace with a local per-tick frame counter (D243 stale-DL-burst screen identification). Counter is NOT numerically identical to the port's internal frame count — cross-reference by elapsed offset. | **live** (D243) |
| `GE_D243CAM` | `src/game/bondview2.c` (`bondviewFrozenCameraTick`, three `CAMERAMODE_POSEND` branches) | Dumps eye/look-at positions (+theta/verta on the `gBondViewCutscene` branch, +dt/angle/pad on the orbit branch) per call. **Found M-143**: the `gBondViewCutscene` branch fires and its look-at (`field_3C4/8/C`) sweeps through a filter catch-up after the cutscene warp — the shake mechanism. | **live** (D243) |
| `GE_D243M` | `src/game/bondview2.c` (`solo_char_load`, `bondviewRemovePlayerBody`, `playerTick`; cached `d243mEnabled`) + `src/game/objecthandler.c` (`drawjointlist`, both region variants) + `src/game/chr.c` (`chrTick`, `after_position_update:`) | M-151/M-154/M-159 probe for the Dam-abseil "frozen Bonds + shake": `D243M: CREATE/REMOVE` at both `bodyModel` sites; `D243M: tick` (idx/model/**propptr/chrptr**/cam modes/`prop->pos`/`field_488.pos`/`field_3B8`/`field_3C4-8-C`/**rp** = render_pos pointer, **never dereferenced** — M-154 fix 2: tick-time `render_pos` holds arbitrary stale slot contents until the first draw pass, so the pre-view root translation is logged by `chrdraw` only) while the model exists or POSEND/INTRO is active; `D243M: draw` per on-screen player render submission; `D243M: chrdraw` — M-154 per-frame model-draw census, one line per CHR render submission (Model* + raw root translation) while a scripted camera mode (POSEND/INTRO/SWIRL/FADESWIRL) is active; a second Bond instance = extra lines with *distinct* model pointers at the same frame (same model is drawn twice/frame — two passes — dedupe by (frame, model)); `D243M: onscreen` (M-159/M-162/M-163/M-166, `chr.c`) — one line per `chrTick` call while the probe is active: `vis=` `posIsOnScreen`'s result (M-159's frustum hypothesis, REFUTED by M-160) + the live `prop->pos` + `actiontype=`/`anim=` (M-160/M-164: which of `chrTick`'s branches fired, the attached-animation pointer) + `aidx=`/`animframe=` (M-166: the animation reverse-resolved to its `animation_table_ptrs1` index, GE_D193A's technique, + its current playback phase — tests whether the visible chr's animation *phase* at cutscene-entry, not just its identity, is what differs from N64) + `x2active=`/`x2raw=` (M-162/M-163, gated on `GE_D243X2`: whether the decisive save/restore experiment ran this tick, and the raw pre-restore position it overrode — the whole `prop->pos` vector as of M-163, not just Z). Silent during FPS play, silent on the `GE_D243X2` fields unless that env var is also set. Note: `field_3B8` x/z read at 10× world units in the smoke capture — not plain world position. | **live** (D243) |
| `GE_D243X2` | `src/game/chr.c` (`chrTick`, gated additionally on `d243mProbeActive()`) | **Not a probe — a decisive behavioral experiment, off by default, NOT a fix.** M-162/M-163: saves `prop->pos` right before `chrTick`'s position-update dispatch (`chr.c:2541-2617`) runs, restores it at `after_position_update:` — before the render-matrix computation later in the same tick can see the cycled value (unlike M-161, which restored too late to have any effect). Tests whether `chrTick`'s own animation-position write (M-160/M-164) is what produces the Dam-abseil shake. M-162 was Z-only (confirmed the mechanism on that axis, but the visible glitch persisted because that capture's chr cycled on Y instead); M-163 widened it to the whole vector. **M-163's live result: NEGATIVE for the visible shake** — `prop->pos` genuinely went smooth (log-verified) but the shake was unchanged, because the camera's look-at filter reads a *different* variable (`field_488.pos`, see `GE_D243X3`) that this experiment never touches. Kept in the tree as a real, useful (if ultimately not the visible-symptom fix) data point — always pair with `GE_D243M=1` so the `x2active=`/`x2raw=` fields on `D243M: onscreen` show whether/what it overrode. Do not ship. | **live, test-only** (D243) |
| `GE_D243X3` | `src/game/bondview2.c` (`playerTick`, the `field_488.pos` write site, gated on `g_CameraMode == CAMERAMODE_POSEND`) | **Not a probe — the experiment that actually confirmed the mechanism, off by default, NOT a fix.** M-167: skips the `field_488.pos.x/y/z` assignment (`bondview2.c:10495-10497`) entirely while POSEND is active, freezing it at whatever value it held the instant suppression engaged. `field_488.pos` — not `prop->pos` — is what the cutscene's leaky-integrator look-at filter (M-143, `field_3B8`/`field_3C4-8-C`) actually reads; it comes from `render_pos` (the model's animated-skeleton readback), an independent data path from `prop->pos`. **Live-confirmed positive: user — "yep camera shake is gone now."** Logs a companion `D243M: f488write frame=... idx=... x3active=... x3skip=... epoch=... f488pos=x,y,z` line (always-on when `GE_D243M=1`, independent of whether `GE_D243X3` itself is set) so the freeze is log-verifiable. **M-169: no longer freezes forever** — re-baselines (`x3skip=0`, write allowed through) exactly on the tick a legitimate shot-change teleport fires (`d243GetTeleportEpoch()`, bumped by `chrai.c`'s `AI_TRYTeleportingChrToPad` right after its real `setsuboffset()` call), addressing the M-168 postmortem's "camera looks the wrong way for later shots" incompleteness — untested live yet, next session's first move. **Superseded: D243 shipped its permanent fix (`4ccab22c`, M-170), which removed the X3 suppression block from `playerTick` — no live site remains.** Kept here as a tombstone; the generator flags it GONE. | **dead** (D243 closed — probe removed) |
| `GE_D243X4` | `src/game/chr.c` (`chrTick`, the `render_pos` snapshot/restore, gated on `d243mProbeActive()`) | **Not a probe — the M-168 experiment applying X3's technique to the actual draw-path consumer, off by default, NOT a fix.** Snapshots `model->render_pos[]` after a 2-tick warm-up once POSEND engages, then holds it steady by copying the cached contents back over each subsequent tick's freshly-computed buffer — `subcalcmatrices()` always still runs first (never skipped; the first two attempts skipped it and crashed on a reused-pointer stale `-1` tombstone, see findings.md D243 M-168). Addresses Bond's own rendered-body duplication (the consumer X3 never touched). Crash-free, stable, but same forever-freeze incompleteness as X3 pre-M-169. **M-169: same teleport-epoch re-baseline as X3** — the warm-up counter now also resets (re-snapshots) whenever `d243GetTeleportEpoch()` changes, not only on the active/inactive edge. Untested live yet. Revert once a real root-cause fix replaces it; do not ship as-is. | **live, test-only** (D243) |
| `GE_D243SWIRL` | `src/game/bondview2.c:10480` (`playerTick`, cached `d243SwirlTarget`) | D243 follow-up probe: `GE_D243SWIRL=<chrnum>` logs a per-tick line for that chr while the swirl/fade camera path is active (tick counter + state), to verify post-fix behavior of the swirl transition. Inert unless set to a non-negative chr number. | **live** (D243 verification) |
| `GE_D193` | `src/game/frametiming.c` (`waitForNextFrame`, `#ifdef PORT`) | Per-wall-second line: `render=` rendered frames, `sim=` sim ticks delivered (Σ of the `deltaFrames`/`g_ClockTimer` fed downstream), `clamped=` frames whose catch-up hit `FRAMETIMING_PORT_MAX_CATCHUP`, `maxraw=` largest pre-clamp delta. Healthy steady state = `render=30 sim=60 clamped=0 maxraw=2` (measured M-80, all levels idle). Use to check whether the wall-clock timer under-advances under real AI load (D193). Cheap; safe for a full playtest. | **live** (D193) |
| `GE_D193A` | `src/game/chr.c` (`chrTick` → `d193aSample`, plus `d193aMoved` in `chrUpdateAnim`; `#ifdef PORT`) | Per-wall-second per-ticked-chr locomotion telemetry (D193/D209): `chr=` chrnum, `act=` ACT_TYPE, `speed=` world-units travelled/s, `gtd=` g_GlobalTimerDelta, `tier=` `act_gopos.unk59` SPEED tier (0 walk / 1 run / 2 sprint; `-1` when not ACT_GOPOS), `gspd=` `act_gopos.speed` (**a turn rate, not a travel rate** — 0 means running straight), `typ=` PROP_TYPE (3=CHR, 6=VIEWER), `tick=`/`move=`/`frz=` chrTick entries vs `chrUpdateAnim` calls vs freeze-gated (move « tick = the chr is being visibility-culled out of its own locomotion; `move=0` at high `speed` = WAYMODE_MAGIC), `hid=` CHRFLAG_HIDDEN, then the root-motion scalars `scale` / `xlscale` / `animrate` / `playspeed` / `mspeed` (model->speed) / `endframe` / `anim` ptr / `aidx=` that pointer reverse-resolved to its `animation_table_ptrs1` index (40 walking, 41 sprinting, 42 running, 107 walking_unarmed — map via `src/game/initanitable.c`). `GE_D193A_CHR=<n>` restricts to one chrnum. **This probe found D209**: Cradle showed `tier=1` (RUN) with `aidx=40/107` (walk anims) on every sample. | **live** (D193/D209) |
| `GE_D193B` | `src/game/model.c` (`sub_GAME_7F06D3F4`, `#ifdef PORT`) | Rate-limited (1/97) raw anim root-motion decode trace (D193): `joint=` / `flip=` / `base=` mtx channel index / `frame=` / `tmp=` decoded s16 translation triple from the anim bitstream / `angle=` / `anim` ptr / `bd=`+`bs=` the D32 u32 bitDescriptors/bitStream addresses / `njoints`. M-80 Facility: `base=0`, `angle=0` **always** (root-motion facing delta never non-zero — top lead), `tmp` ≈ (±small, ~1086 hip-height, ±small). Compare `tmp`/`angle` magnitudes against a clean N64 decomp build of the same anim+frame. | **live** (D193) |
| `GE_D250` | `src/boss.c` (`bossMainloop`, wraps `lvlRender()`, `#ifdef PORT`) | Per-wall-second line: `[D250] lvlRender avg=Nus n=M` — average + count of `lvlRender()` (DL-building: culling/animation/scene traversal) calls that wall-second, isolated from `gfx_run()`'s software-RSP interpretation cost (already covered by the existing "frame rendered in Nus" line). Added M-120 to chase D250 (fps stuck ~45-50 on Dam, ruled resolution/MSAA/vsync/build-flags out already) — found `lvlRender` itself is cheap (~0.5-1.5ms avg), so it is NOT the missing ~10-15ms/frame either; the still-unexplained cost must be elsewhere in the per-retrace-message loop (scheduler/thread round-trip, audio, or something not yet isolated by a targeted timer). Cheap; safe for a full playtest. | **live** (D250) |
| `GE_D172` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1` — SETCOMBINE dedup log + non-LOD multitex tri dump; `#ifdef PORT`) | Particle magenta/cyan colour (D172). Logs each distinct SETCOMBINE + active cycletype, and — for a 2-cycle tri that consumes TEXEL1 with `tex_lod=0` and a distinct 2nd tile — the tile fast3d *samples* for each texunit vs what the DL *configured*. **Found D172**: 40/40 particle tris configured tile 1 @ tmem 392 (fire) but sampled tile 0 (smoke) → `smoke*smoke`. FIXED (`gfx_lod_tile_offset` `return rdp.tex_lod ? 0 : i`). Probe kept inert. | **live** (D172 FIXED, probe kept) |
| `GE_D157` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex` + `import_texture`; `#ifdef PORT`) | D219/D252's "rainbow particle" family — bullet-impact spark on Bunker1 door confirmed by screenshot to cycle magenta/green/cyan (M-157). Three sub-probes: original exact-color-match log (`g_BulletSparkColors[]` byte match → geometry_mode/LIGHTING + shaded RGB); **`D157T`** texture-identity-keyed vertex full-path log (tile 0 holds a loaded texture ≤16 KiB in frame window [90,240] → raw `cn[]`, post-shade RGBA, LIGHTING, geometry_mode, combine_mode, tile-0 tmem/fmt/siz/addr/size/line, uv; cap 400 + summary/200); **`D157I`** import census (small textures imported in the window, deduped per source address → addr/fmt/siz/size/tile/tmem). M-158: bug did NOT reproduce in 6 identical dev-box runs — probes left live to capture the full RGBA path on the next occurrence (user Steam Deck run with `GE_D157=1`). | **live** (D219/D252, non-reproducible as of M-158) |
| `GE_DVM` | `src/game/gunfire.c` (`gunUpdateAndFire` gate dump + `gunRenderFirstPersonGunModels` render dump; `#ifdef PORT`) | 1P weapon viewmodel (D215). `[DVM]` line: the `field_87F` show/hide gate terms + `Gun_hand_without_item` sub-terms + `copy_of_body_obj_header` RootNode/Switches/numRecords. `[DVMr]` line: `87F` + `cmds_emitted` = gfx commands the weapon `subdraw()` produced (healthy ≈ 150; the D215 bug showed **1**). Rate-limited ~1/s per hand. **Found + verified D215** (`renderdata.flags` read 0 → all weapon DL nodes gated off). Probe kept inert. | **live** (D215 FIXED, probe kept) |
| `GE_DTEX` | `port/fast3d/gfx_pc.cpp:1001` | Per-`import_texture` param log (dims / line / size / lod / gen_mipmaps). Used to root-cause RC2 and D159. Kept as an inert diagnostic. | **live** (RC2/RC3 not finished) |
| `GE_TEXDUMP` | `port/fast3d/gfx_pc.cpp:1020`, `port/fast3d/gfx_opengl.cpp:700` | PPM dump of every uploaded texture + a `fmt/siz/palfmt/palidx/pal[0..3]` line. Root-caused D161 (Depot ceiling). Kept as an inert diagnostic. | **live** (RC3 not finished) |
| `GE_D116` | `port/fast3d/gfx_pc.cpp:1710,2362`, `src/game/textrelated.c:265,541` (cached `ge_d116`) | HUD/text "X-mirror" (D114/D116) vbo/uv trace. **D114/D116 CLOSED — NOT A BUG (M-33/D168): the captures were upside-down, not mirrored.** Probe is now dead scaffolding — strip on the next cleanup pass. | dead (D114/D116 closed) |
| `GE_D288` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, right after the D233 trivial-reject guard; cached `ge_d288`) | M-152 diag for the Silo intro stray triangle: logs any triangle whose clip-space NDC bbox covers >15% of the screen or goes non-finite, with each vertex's `w` and `any_behind_camera`. **Investigation in progress** — narrowed to two candidates (an unguarded `w`-division NaN leak in the backface-cull test just below, or the D106/D271 portal-BFS family), not yet root-caused; needs a live/headless `-level_20` run to disambiguate. | **live** (D288) |
| `GE_D236RM` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, unconditional getenv, no state kept when unset) | M-155/pass-10 diag for D236: per-frame (`num_dls`) triangle count of the `0x0c184b50` render-mode class (`other_mode_l` exact match), heartbeat-logged every 300 frames even at zero. **REFRAMED D280's "run-to-run dependent instantiation" as ordinary room-based occlusion**, not a CPU-side gating bug — see `## D236` pass-10 writeup. | **live** (D236 OPEN, kept for future passes) |
| `GE_D236ORDER` | `port/fast3d/gfx_pc.cpp:1844` (`gfx_sp_tri1`, cached `s_d236order`) | D236 pass-12 diag: per-DL sequence log of the tree-class triangles (DL index, screen-space depth, noise min/max census) to test the draw-order hypothesis. Zero cost unset; remove once D236 concludes. | **live** (D236 OPEN) |
| `GE_D236ALPHA` | `port/fast3d/gfx_pc.cpp:1896` (`gfx_sp_tri1`, cached `s_d236alpha`) | D236 pass-14 diag: for the `0x0c184b50` tree-class render mode, logs the combine/alpha state of up to 4000 matching triangles (deduped on combine change) so the captured draw can be cross-checked against the source. Zero cost unset; remove once D236 concludes. | **live** (D236 OPEN) |
| `GE_D236ZFIX` | `port/fast3d/gfx_pc.cpp:1970` (`gfx_sp_tri1`, cached `s_d236zfix`) | D236 pass-12 test override: forces `depth_update=true` for the `0x0c184b50` tree-class render mode — a cheap mechanism test before deciding what a real fix looks like. Off by default; remove once D236 concludes. | **live, test-only** (D236 OPEN) |
| `GE_D236RAW` | `src/game/bg.c:2437` (`#ifdef PORT`) | D236 pass-16 diag: one-shot census of the raw vertex table's alpha-byte distribution (5 buckets + min/max), camera-independent, to check whether any vertex in the whole table exceeds the low ceiling. Zero cost unset; remove once D236 concludes. | **live** (D236 OPEN) |
| `GE_D245V` | `src/game/sky.c:962` (`skyPortRenderPoly`-adjacent, `#ifdef PORT`) | D245 water-seam diag: per-call min/max of the recomputed S/T texture-coordinate ranges for the water fan vertices (rate-limited), to check whether the seam tracks a tc-shift boundary. Inert unset. | **live** (D245 OPEN) |
| `GE_D245_FIXEDSHIFT` | `src/game/sky.c:946` (`#ifdef PORT`) | D245 test override: pins the water tc shift `k` to the given integer (0..SKY_TC_MAX_SHIFT) instead of the computed per-vertex value, to A/B the allowShift=TRUE behavior. Off by default (unset = current behavior). | **live, test-only** (D245 OPEN) |
| `GE_D252` | `port/fast3d/gfx_pc.cpp:733` (`import_texture`, cached `s_d252`) | D252 rainbow-particle diag: one-shot per (address,size) import log of the tile state (tmem/fmt/siz/addr/size/line) for small textures so a suspect particle tile can be checked against its source bytes by hand. Remove once D252 is resolved. | **live** (D252 OPEN — non-reproducible) |
## Dxx diagnostic probes — finding closed unless noted
| Env var | File:line | Finding / what it logged | Status |
|---|---|---|---|
| `GE_D51` | `port/src/libultra.c:496`, `src/game/model.c:125,244,544` | msgQ 32-slot overflow watch / ModelSlot layout | dead (D51 closed) |
| `GE_D54` | — (all blocks stripped M-32, commit `49ce620a`) | music seq-table ABI / endianness (`ALMidiHdr`) | dead (D54 closed) — fully removed (kept here as a tombstone; the generator flags it GONE) |
| `GE_D154` | `src/game/bg.c:3418,3447,3632` (capped 64 calls) | bg room-GDL call trace (room / gdlidx / vtxoff / op / raw hdr words) | dead (D154 closed) |
| `GE_D227V` | `src/game/sky.c` (`skyPortRenderPoly`, `#ifdef PORT`) | Sky/water per-vertex dump: `S`/`T`/`w`/`1/w`, screen xy, the chosen tc shift `k`, the baked `tc`, and whether the tile capture succeeded. **Found D227**: one sky quad spans ~30,000 texels in S and T but `Vtx.tc` is S10.5 in an `s16` (+/-1024 texels), so 7 of its 8 texture coordinates overflowed and wrapped -- the starburst/"two sky processes" seam. Root cause was a unit error (`unk20`/`unk24` are already S10.5, not texels; M-82's `*32` was spurious). FIXED M-100b. Probe kept inert. | **live** (D227 FIXED, probe kept; useful for D229 water) |
| `GE_D176` | `src/game/bgfog.c:459,471`, `src/game/sky.c:325,381` (all `#ifdef PORT`) | D176 sky/fog env-match + sky-vert trace (Path B, PR #18) | **live** (D176(a)/(b) OPEN) |
| `GE_D178` | `src/game/front.c:6586,6594` (`#ifdef PORT`) | briefing-data u16 decode trace (blank-objectives, D143/D178) | dead (D178 closed) |
| `GE_D54` | — (all blocks stripped M-32, commit `49ce620a`) | music seq-table ABI / endianness (`ALMidiHdr`) | dead (D54 closed) — fully removed |
| `GE_D56` | `src/game/model.c:227,830` | watch `Model` raw-offset reads | dead (D56 closed) |
| `GE_D60` | `port/src/libultra.c:521,735` | gfx frame msgQ delivery trace | dead (D60 closed) |
| `GE_D61` | `port/src/libultra.c:795` (`s_d61opened`) | opens a one-shot log file | dead (D61 closed) |
@@ -93,8 +44,6 @@ Two classes:
| `GE_D69STAN` | `src/game/stan.c:266,276` | D69 stan-tile trace | dead |
| `GE_D85DUMP` | `src/game/bg.c:2510` | D85 `bgWidenRoomGdl` 8→16B dump | dead (D85 closed) |
| `GE_D85TEX` | `src/game/image.c:2479`, `src/game/tex.c:859,1050` | D85 texpool-full events (Depot analysis) | dead (D85 closed) — was useful for §3 |
| `GE_D217TEX` | `src/game/image.c:234` (`texInflateZlib`) | D217: logs `texnum`/`format`/`numcolours`/`palette[0..1]` straight off the decode bitstream (before any arena/pointer math) — the decode-correctness check that proved the affected texture (grip, `texnum=1608`) decodes byte-identically across levels, ruling out a decode/ROM-data bug. | **live** (D217 open — investigation instrument) |
| `GE_D229` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri` dual-texunit probe + `import_texture`) | D229: per-draw dump of both texunits' tile/tmem/fmt/siz/UV state for 2-cycle draws, plus a one-shot confirm line when the RGBA16-over-CI8 import remap fires (the fix path). Inert by default. | **live** (D229 fixed — probe kept for the Surface 2 / sweep verification) |
| `GE_D86` | `src/game/model.c:6295`, `src/game/objecthandler_2.c:143` | D86 model rwdata trace | dead (D86 closed) |
| `GE_D87` | `src/game/ramromreplay.c:296,363,381` | D87 `ramromfilestructure` endianness | dead (D87 closed) |
| `GE_D88` | `src/game/prop.c:1367`, `src/game/stan.c:3090` | D88 `Usetup*Z` propDef stream | dead (D88 family closed; D88.4 resolved) |
@@ -103,14 +52,6 @@ Two classes:
| `GE_D104` | `src/game/bg.c:654` (rate-limited, `d104c`) | D104 depth-clear trace | dead (D104 closed) |
| `GE_D71LOG` | `port/fast3d/gfx_pc.cpp:645` | D71 fast3d trace | dead (D71 closed) |
| `GE_D75` | `src/game/title.c` `sub_GAME_7F007F30` (capped 8) | D75 Bug 2 gun-barrel model probe: logs `chrModelInstance`/`gunModelInstance` ptr+obj+numMatrices, `render_pos`, `renderData.mtxlist`, `g_GfxMemPos`, `osVirtualToPhysical(render_pos)` | **LIVE** (D75 Bug 2 OPEN — see §F "D75 Bug 2 — RUNTIME PROBE") |
| `GE_D305` | `src/snd.c:420` (`sndHandleEvent` desync branch, `#ifdef PORT`) | D305: logs the sound-preemption desync state when it is detected (after the NULL-guard fix made the crash itself impossible) — confirms whether `Game.AllUnlocked` correlates or was coincidental, and whether it recurs. Inert unset. | **live** (D305 monitoring) |
| `GE_D309` | `src/game/chrai.c:810,3424`, `src/game/chraction.c:10139` (all `#ifdef PORT`) | D309 freeze probe: logs AI label-not-found restarts (`chrai.c`) and the run-to-bond persistent-list step-on path (`chraction.c`, capped 400 lines) — the diagnostics used to root-cause the elusive complete freeze (D309/D310). Inert unset. | **live** (D309/D310 fixed — kept for recurrence) |
| `GE_D314` | `port/src/optionsoverlay.c:681` (cached `s_d314Enabled`) | D314 F10-flicker probe: logs the overlay's viGetY / visible-row count / scroll / selection each poll. Inert unset; strip candidate once D314 is triaged. | **live** (D314 pending triage) |
| `GE_D318R` | `port/src/random.c:69,147` (`#ifdef PORT`) | PRNG golden probe (D284/D318): logs the first 4000 gameplay-PRNG draws with tick + caller return address, and every `randomSeed` set — verifies the bit-exact port of `random.s` and which call sites consume the stream. Inert unset. | **live** (D284 verification) |
| `GE_RSEED` | `port/src/random.c:134` (`#ifdef PORT`) | Test-only boot-seed override: `GE_RSEED=<hex64>` pins `g_randomSeed` at first seed instead of the wall-clock-derived value — for determinism and N64-parity tests. Inert unset. | **live, test-only** |
| `GE_RSEED_LV` | `port/src/d318watchdog.c:232` (`#ifdef PORT`) | One-shot level-start seed log: logs `g_randomSeed` at the first tick after level start (optionally armed for a specific tick via `<hex>@<tick>`) — ties a saved/loaded game's PRNG state to a level for D318-class repro. Inert unset. | **live** (D318 verification) |
| `GE_D318T` | `port/src/d318watchdog.c:270` (`#ifdef PORT`) | D318 watchdog timeline probe: per-tick log of the Facility ai_22 softlock signature window (c78 list/offset/attack state, c67 health) while the watchdog watches. Inert unset. | **live** (D318 verification) |
| `GE_D318W` | `port/src/d318watchdog.c:136` (`#ifdef PORT`) | **Not a probe — the D318 port-side watchdog itself, ON by default.** Detects the Facility ai_22 hold-pose softlock signature (a latent N64 race, see findings.md D318) for 600 consecutive ticks and re-seeds c78's attack via the game's own fresh-attack entry point. `GE_D318W=0` opts out; logs `D318W:` lines when it acts. **Permanent fix, not strip-candidate.** | **live** (D318 fix — permanent) |
## Notes
@@ -121,9 +62,5 @@ Two classes:
- `[Debug]` ini keys (`Debug.FrameDump`, `Debug.InputLog`) mirror
`GE_PCDUMP` / `GE_INPUTLOG` as a fallback (env var wins) — M-26,
`port/src/config.c`.
- Not GE_-prefixed but same category: `GE_DETERM` is **implemented but
experimental — not yet achieving its determinism goal** (fixed-tick
mode, §F D117, M-52 four-pass writeup); `GE_DETERM_TRACE` (M-52 4th
pass) adds instrumented clock-event logging when used alongside it, for
diffing two runs to localize remaining divergence. Both env-gated,
default off, zero cost when unset.
- Not GE_-prefixed but same category: `GE_DETERM` is **designed, not
implemented** (fixed-tick determinism mode, §F D117).
-217
View File
@@ -1,217 +0,0 @@
# GEPD-inspired input overhaul — FINAL PLAN (issues #89, #90)
Status: **plan only** — no code changes yet. Companion to `port/src/input.c`
(current state: D118/D165/D166/D194/D214/D223/D238 lineage).
## 1. The reference (no longer vendored in the public repo)
The GEPD Edition Mouse Injector source was vendored at `reference/mouse-injector/`
and **removed from the public repo on 2026-09-19** (GPLv2 code + a prebuilt
binary inside an MIT project; never compiled here). See
`reference/mouse-injector/README.md` for provenance and how to re-vendor it
locally if you need the original. The file that mattered was
**`games/goldeneye.c`** (~380 lines, the whole GE aim model), plus `device.c`
(raw mouse polling) and `maindll.h` (tickrate — the units every per-tick
constant assumes). The model is quoted inline below and in the D194 findings;
the ported result lives in `port/src/input.c`.
### What the Mouse Injector does (GE)
### What the Mouse Injector actually does (GE)
| Aspect | GEPD implementation (`games/goldeneye.c`) |
|---|---|
| Mouse source | Raw driver deltas via ManyMouse (`device.c`), cursor locked by periodic `SetCursorPos`. No OS pointer accel, no deadzone. |
| **Hipfire look** | **Direct camera write**: `camx += XPOS/10 * (SENS/40) * (fov/basefov)` degrees per tick; `camy` likewise, clamped ±90 (−20 in tank). Linear in px — this is why GEPD "just feels right". |
| **Aim mode (R)** | Crosshair *position* accumulator `crosshairpos += XPOS/10 * SENS/292`, clamped to ±`CROSSHAIRLIMIT` (5.159° = screen edge). Gun/arm pose derived per-weapon from a 33-entry offset table. **View scrolls only past 72% of the way to the edge**: `aimx = (ratio−0.72) * 475 * timestep`, scaled by `fov/basefov`. |
| FOV coupling | Every term carries `(fov/basefov)` — scopes automatically get finer control. `basefov` = min(60, override). |
| Tank | Separate direct write to the tank x-rotation global (`0x80036484`), different scale (`360/TANKXROT * 2.5`); crosshair X locked to 0 when driving with tank equipped. |
| Menus | Mouse moves the menu cursor directly (`menux/y += px/10*SENS*6`, clamped to screen rect); aim button acts as B (back) in menus; wheel = weapon cycle macro. |
| Safety gates | Injection only when `camera==4||0 && menupage==11 && !dead && !watch && !pause && !mproundend`. |
| Extras | Crouch *toggle* mode, dedicated reload key (ROM hack — not portable to us), FOV override + viewmodel repositioning (ROM hacks — we have the clean equivalents in `port/`), intro skip on FIRE+AIM. |
### Already inherited (do not redo)
- **D194 GEPD-mirror aim** (`aimGepdCompute()`): crosshair-position model,
±5.159° clamp, 72% edge scroll at 475/s, fov scaling, per-tick overwrite of
`crosshair_x/y_pos` + `gun_azimuth_angle/turning`. This *is* GEPD's aim mode.
- D223 mouse-wheel weapon cycling (A / A+Z synthesis) — mirrors
`GE_Controller()`'s weapon macro; D214 ini key binds; click-to-lock capture;
F10 options overlay with aim/turn sensitivity sliders; D194(b) dt-decoupled
look; menu pointer via direct `cursor_h/v_pos` writes (mirrors GEPD's
`menux/y` writes).
### Remaining inheritances (this plan)
GEPD's *hipfire* is a direct linear camera write — we still route hipfire
through the N64 stick curve (#89). Also un-inherited: raw-input-by-default,
crouch toggle, GEPD-style safety gates on our always-live look paths, and its
sensitivity/FOV coupling discipline. That's WIs 1–3 + 6 below.
### Reference split (GEPD vs PD port)
Two references, two jobs (decided during #89/#90 triage):
- **GEPD Mouse Injector** (was `reference/mouse-injector/`, removed from the
public repo 2026-09-19 — see its README stub) = *what the mouse does to the
camera*. The only reference with GE-specific knowledge (camera fields,
crosshair limits, tank case, safety gates). Primary for WI-1.
- **PD PC port checkout** (standing reference, see AGENTS.md) = *how a PC game
presents and captures input*: cursor-lock behavior, raw input handling,
sensitivity units/scaling, and the shipped in-game options/keybind UI.
Primary for WI-2 conventions and WI-5's menu design. PD knows nothing about
GE — never use it for camera math.
## 2. Issue #89 — "mouse movement is not uniform; slow motion barely registers"
### Diagnosis (high confidence)
The complaint is the **hipfire** path. Today hipfire converts mouse px into an
*analog stick value* (`sx += (int)(hipEdx * hipSens * MOUSE_TURN_GAIN)`), and
the game then runs that stick through its own N64-era natural-turn response —
a **quadratic-ish curve with a soft floor** (D238: "saturates the game's
quadratic natural-turn curve at ~13 px/poll"). Consequences, exactly as the
reporter describes:
1. Slow mouse motion → small stick values → squared-off tiny turn rates →
"almost unrecognised".
2. Fast motion saturates the curve at full 315°/s — so the usable band is
narrow and the feel is non-uniform (bang-bang-ish).
3. `(int)` truncation of the stick adds a quantized dead zone on top.
GEPD/PD-PC never hit this because **they write the camera angles directly** —
the stick curve is bypassed entirely, and sensitivity is linear in px with
FOV scaling. We already do exactly that in aim mode (D194); hipfire is the one
leftover N64-stick-shaped path.
Secondary contributors worth folding in:
- `Input.MouseRawInput` defaults to **0** — on X11/Wayland (the #90 reporter
is on Linux) OS pointer acceleration still shapes relative deltas; GEPD
reads the raw driver. Windows is largely unaffected.
- Sensitivity is spread over 4 knobs (`MouseSensitivity`, `MouseTurnSpeed`,
`AimModeSens`, legacy `MouseAimSpeed`) — the reporter "tried adjusting
ge007.ini" and couldn't find the one that mattered for hipfire.
### Work items
**WI-1 (core fix): direct camera writes for hipfire look.**
Mirror GEPD's hipfire branch in `input.c`.
*Pre-flight:* skim the PD port's `port/src/input.c` mouse path first and match
its conventions (sensitivity units, dt handling, focus-loss behavior) so the
result feels like PD PC rather than a reinvention. GEPD supplies the GE camera
math; PD supplies the UX shape.
Implementation:
- New path `hipDirectCompute()` next to `aimGepdCompute()`: convert this
poll's dt-scaled px to degrees using the *current* viewport
(deg/px = fovy/viewH, or hfov/viewW — reuse whatever D194 absolute aim used),
scale by `MouseTurnSpeed * MouseSensitivity`, and add straight to
`g_CurrentPlayer->vv_theta` / `vv_verta` (clamp ±90). Same confinement as
today: only when `idx==0`, cursor captured, in a running stage.
- **GEPD's safety gates** must be ported over (aim mode got away without them
because it requires RMB held; hipfire is always live): check the decomp
equivalents of GEPD's `camera==4||0 && menupage==11 && !dead && !watch &&
!pause` before writing. `menupage` → `current_menu` (already have); the rest
are `player`/game globals — read-only, no logic change.
- **Tank**: GEPD writes a separate tank-rotation global with its own scale and
locks crosshair X to 0 when driving with the tank equipped. Find the decomp
symbol for `0x80036484` (tank.c) and mirror it; if too fiddly, phase 2 —
tanks are rare in single-player.
- Keyboard turn (arrows → stick) keeps the existing stick path; only mouse px
goes direct. Escape hatch: `Input.MouseDirectLook=1` default, `0` reverts to
the current stick path (kept as-is).
- This makes hipfire and aim mode share one linear mental model: px → screen
angle, FOV-scaled, dt-normalized. Expect to then simplify or delete the
D166/D238 curve-tuning knobs that become inert (`MouseAimCurve`, `AimBand`,
legacy `MouseAimSpeed`) — *after* playtest, and keep the config keys as
no-op aliases so existing inis don't break.
**WI-2: raw input by default.** Flip `Input.MouseRawInput` default to 1 (SDL
hints already handled in `inputInit`). Verify Windows relative-mode deltas are
unaffected; this mainly fixes Linux feel. Keep the escape hatch.
**WI-3: one honest sensitivity number.** With WI-1, hipfire and aim both scale
off px→degrees, so `MouseSensitivity` becomes genuinely master. Add it to the
F10 overlay (see WI-5), document the knob hierarchy in the generated ini
header comment, and re-calibrate defaults against GEPD's SENS≈40 ≈ 1:1
screen-angle mapping (our current 38/50/100 trio should land close; measure,
don't guess).
**Verification:** `GE_INPUTLOG` linearity check — log px-in vs deg-out at slow
and fast speeds (ratio must be constant); headless script harness for the
stick-fallback path; manual playtest checklist (hipfire slow/fast, aim mode,
scope zoom, tank, menu, death/cutscene safety gates). New finding `Dxxx` in
`docs/dev/findings.md`, index row added.
## 3. Issue #90 — in-game key bindings + "ini binds don't work" + sensitivity menu
### Diagnosis candidates for "binds don't work properly" (repro before fixing)
1. **Not a bug, GE design:** in-game, a fresh A edge *without* Z held is the
weapon-cycle-forward signal (D223 documents `weaponForwardOffset`). The
default Action bind includes E — so "pressing E changes weapon" is the
game's own behavior, and "Escape reloads/activates switches" is the default
Cancel=Escape → B button. The user's mental model (PC: E = interact)
collides with GE's A/B semantics. Address via documentation + possibly a
PC-idiomatic default set (product decision; GEPD itself uses Q=A, E=B).
2. **Ini syntax footgun:** keys must be written as `Input.Bind.Action=E` (or
`Bind.Action=E` under `[Input]`). A bare `Action=E` under `[Input]` resolves
to `Input.Action`, which is unregistered and silently ignored. `configLoad`
also ignores unknown keys with no warning — add a log line for unrecognized
keys so users get feedback.
3. **Key-name format:** binds use SDL scancode names (`"Left Ctrl"`, `"Up"`,
`"Space"`); nothing documents this in the ini. Add per-key comments in
`configSave` output and/or a `# valid names: ...` header.
4. There may still be a real parse bug lurking (`strtok` on the seeded buffer,
constructor ordering vs `configLoad`) — **repro first**: write an ini with
`Input.Bind.Action=E`, launch, confirm via `GE_INPUTLOG` that E alone
produces A. Fix whatever actually breaks; don't fix hypotheticals.
### Work items
**WI-4: keybind audit + ini ergonomics.** Repro (above), fix real bugs, add
unknown-key warnings, document scancode names in the saved ini. Cheap, do first
— it unblocks #90's half regardless of the UI work.
**WI-5: F10 overlay — bindings section + master sensitivity.**
Model the UX on the PD port's shipped in-game options menu (standing reference
checkout) where anything is player-facing. `optionsoverlay.c` already has
row/hit-test/scroll infrastructure and owns all input while open; what's missing:
- A new row kind `ROW_BIND`: label = action, value = first bound key name;
click → "press a key…" state that captures the *next* scancode (keyboard or
mouse button), writes it into `g_bindStr[a]` (replacing or appending —
decide: GEPD/PD use one primary + optional secondary; simplest v1 = replace
single key, keep multi-key only for ini power users), then `inputRebuildBinds()`
+ persist via the existing config save path.
- Expose `Input.MouseSensitivity` (master), and after WI-1 trim the panel to
master + per-mode fine-trim + invert Y (drop inert legacy rows).
- Bind capture must swallow all input while armed (overlay already does this)
and handle ESC = cancel, Backspace/Delete = clear.
**WI-6 (nice-to-have, from GEPD): crouch toggle.** GEPD's `CROUCHTOGGLE` state
machine (`GE_Crouch`) is ~20 lines and pure port-side (stance flag write).
Low priority; include only if WI-1..5 land cleanly.
## 4. Suggested order
1. **WI-4** (keybind repro/fix + ini docs) — small, unblocks #90 partially.
2. **WI-1** (direct hipfire) + **WI-2** (raw input default) — the #89 fix;
playtest-heavy, needs the safety gates done right.
3. **WI-5** (overlay binds + master sens) — completes #90.
4. **WI-3** (sensitivity consolidation/cleanup) — after WI-1 defaults settle.
5. **WI-6** (crouch toggle) — optional garnish.
## 5. Constraints reminders
- All work is `port/`-only: direct struct writes are the established D194
pattern (read/write live game state from the input poll, no logic change).
Any temptation to "fix" the natural-turn curve in `src/game` = rule-2 sign-off
territory — don't.
- Keep every removed/renamed ini key registered as an alias so shipped inis
keep loading; log deprecations at startup.
- Findings: new `Dxxx` entries for WI-1 (hipfire direct look), the #90 bind
repro outcome, and the overlay bind UI; update `findings-index.csv`.
+8 -15
View File
@@ -12,29 +12,22 @@ once the game is playable start-to-finish without crashes.
| ~~**D74 wrap-block**~~ | ~~`gfx_pc.cpp:1546/1551` PORT block is dead code (guard `cms & G_TX_WRAP` always false; indexes per-texunit arrays with the vertex index → OOB if reached).~~ | **RESOLVED (M-30, RC3/D167).** The in-vertex-loop wrap block was reworked and hoisted to a per-texunit pre-wrap window (`gfx_pc.cpp` ~1574, now `cms`-correct and indexed by texunit not vertex), behind the `Video.WrapFix` knob (default OFF). No longer dead code or OOB. See §F "RC3 · D167". |
| **D75** | ~~Nintendo logo mispositioned; gun-barrel Bond model absent~~; cast-roll character models — status unknown. (Rareware logo OK.) | **Shrunk (M-33, upright captures after D168).** The "overlaps D114 shared mirror" note is withdrawn. **Gun-barrel Bond RENDERS correctly** (walk + fire, upright) — the "absent" reports were the flipped capture. **Nintendo logo IS still broken** — renders as two overlapping white ellipsoids shifted left, no wordmark/geometry (a real `logoinst` model transform/geometry bug, not a flip). Cast roll not re-captured — recheck with `GE_PCDUMP="1400-2200:25"`. `findings.md` §F "D75 Bug 2 — M-33 UPDATE". |
| **D76** | 2D disclaimer screen only partially drawn. | §F D76. |
| ~~**D77**~~ | ~~No audible music on PC.~~ | **RESOLVED (M-104, PR #42) — in-level music now plays.** Root cause was a D6-class missing `return` in `sub_GAME_7F0C0BF0` (`mp_music.c`) that zeroed the seqp volume on the only in-level trigger path; menu/intro music bypassed it and was always fine. The remaining audio-*quality* gap (wrong-sounding instruments on some tracks — elevator named specifically — plus occasional garbling) is tracked as **D230**. §F D77/D230. |
| **D77** | No audible music on PC. | Phase 3 (audio). §F D77. |
| ~~**D116 (HUD)**~~ | ~~proportional-font strings + ammo counter mirrored~~ | **CLOSED — NOT A BUG (M-33, §F D168).** Same capture-orientation artifact as D114. |
| **D148** | Level-end cinematics glitch. Dam: the Bond rappelling/bungee cutscene plays but the camera violently shakes and Bond's own model appears to duplicate/glitch. **Cutscenes are buggy in many places across the game** (D173 level-start floating, D292 death-sinking, Cradle spin) — treat D148/D243 as the tracking entry for the whole cutscene-reliability problem, not just Dam. | **2026-09-17 (D243 M-107 through M-168, ~18 session-passes): the mechanism is now understood in real depth, and the camera-shake half is fixed (diagnostically, live-confirmed).** Full history + code citations: `docs/dev/findings.md` `## D243` (long, multi-pass — look up via `findings-index.csv`, read the M-159 through M-168 paragraphs for the current understanding, don't linear-read). Short version for planning purposes: **(1) Root mechanism** — the cutscene teleports the *visible* Bond chr between shot positions correctly (`AI_TRYTeleportingChrToPad`, `chrai.c:4064`), but the actual rappel *animation* commands run on a separate, invisible driver chr (M-145) — the visible chr never gets told to play it, so it just keeps ticking whatever animation it already had equipped before the cutscene grabbed it. Every tick, that stale animation's own root motion gets read back into the chr's render position (`chrUpdateAnim`→`getsuboffset`, `model.c:684`), producing a small repeating cycle that reads as "shaking"/"glitching" on screen. Whether this exact mechanism is itself decomp-faithful (with the real PC/N64 divergence being *which* animation phase Bond happens to inherit at the cutscene boundary, not a code-path bug) is still an open question (M-165/M-166) — not yet answered. **(2) Two independent render paths, not one bug** — the camera's look-at filter reads `field_488.pos` (derived from `render_pos`, the model's animated-skeleton transform); the model's own on-screen draw reads `render_pos` directly (`objecthandler.c`'s `drawjointlist`). Fixing one does not fix the other; two live-test cycles were spent learning this the hard way (M-162/M-163 → M-167). **(3) Camera shake: FIXED at the mechanism level, live-confirmed by the user** ("camera shake is gone now") by freezing `field_488.pos` during the cutscene (`GE_D243X3`, `bondview2.c:10495`) — diagnostic-only, not shippable as-is (see next). **(4) Bond's own model duplication: mechanism-level fix attempted (`GE_D243X4`, `chr.c`, freezing `render_pos`'s contents) — stable after fixing two crashes (model-pointer-reuse hazard, see `porting-notes.md`'s two D11 updates), but freezing unconditionally also breaks the cutscene's *legitimate* per-shot camera/model repositioning** (the camera now looks the wrong direction for later shots; Bond still looks stuck) — not a fix yet. **Next steps for whoever picks this up** (none attempted yet): (a) re-baseline the `GE_D243X3`/`GE_D243X4` freeze on a position-delta threshold (a jump much larger than the observed intra-shot cycling amplitude, already characterized in the M-153/M-160 captures, is very likely a real shot-change teleport, not cycling) instead of freezing forever; or (b) hook the freeze/re-baseline directly to `AI_TRYTeleportingChrToPad` firing, the precise, unambiguous signal for "a legitimate reposition just happened"; (c) separately, find the actual N64-vs-PC divergence point per M-165/M-166 (log `aidx=`/`animframe=`, already wired into `D243M: onscreen`) to determine whether a real, permanent fix is even a `port/`-layer timing correction or needs a rule-2 sign-off for a genuine behavioral difference — this determines which of (a)/(b) is worth turning into a real fix vs. a stopgap. |
| ~~**D149**~~ | ~~Front-end MISSION COMPLETE / mode-select 3D models (dossier, wallets) render as garbled full-screen geometry or not at all.~~ | **CLOSED — user-verified fixed (M-87 playtest).** No longer reproduces; presumably fixed as a side effect of other front-end/D75-family work since M-49. D75 family. §F D146. |
| **D148** | Level-end cinematics don't play. Dam: hitting the exit trigger should show Bond rappelling down the dam (the bungee jump); instead it cuts straight to the mission-complete report. **Broader (M-49 full playthrough): cutscenes are buggy in many places across the game**, not just the Dam ending — the user reports frequent cutscene glitches (skipped, wrong camera, misplaced/hovering actors, timing). Treat D148/D160 as the tracking entry for the whole cutscene-reliability problem, not just Dam. | Scripted cutscene / cinematic-camera + Bond rappel anim. Found M-27 during the first full Dam→Facility playthrough. Progression is unaffected — the report and next briefing load fine. The widespread nature (M-49) points at a systemic cause: the propDef/`sizepropdef` stride desync (D122/D132 family) that misreads `CutsceneRecord` lookups, and/or D75 anim-model + D173 actor-position bugs showing up wherever a cutscene puts a character on screen, and/or the D155/D156/D193 timing family driving cutscene camera/anim pacing. **M-31 (§F D160):** static-traced the Dam end ailists (`ai_24`→`ai_17`, `camera_switch`→`CutsceneRecord` type-46 lookup). Diagnostic `GE_D160=1` shipped (`c95713f5`) — user runs Dam to the exit with it set; the trace pins whether the cutscene AI runs, and whether the propDef-index walk (D122/D132 family) hands `camera_switch` a bogus cutscene record. Leading hypothesis: propDef `sizepropdef` stride desync (medium confidence), not D75 anim-model. |
| **D149** | Front-end MISSION COMPLETE / mode-select 3D models (dossier, wallets) render as garbled full-screen geometry or not at all — one of the compiled sub-DLs (seg5+0x9ee4) is malformed, D146 now skips it instead of aborting. | D75 family. §F D146. |
| **D170** | Named-NPC "flee" scripted behavior looks wrong on PC: on Silo, Ourumov breaks into a full run the instant he alerts ("Ourumov: Kill him!") rather than the N64 walk/retreat; developer reports Trevelyan behaves the same way on Cradle. **M-49 full playthrough: escalated — Cradle (`-level_51`) is "bugged out completely" from Trevelyan's behavior; the final level's scripted progression breaks. Now folded into / superseded by §F D193 (NPC AI locomotion too slow).** | **Observed, not investigated** (M-34, from a Silo playtest capture; user bug report — no fix wanted yet). Could be a genuine ailist / `chrTick` divergence, OR the D155/D156 wall-clock `deltaFrames` locomotion-anim speed-up (a catch-up spike playing the walk cycle at 2–3×). **Verify first:** capture the same beat against N64 reference footage before assuming it diverges — Ourumov may be scripted to flee. If it's real, suspect the frametiming clamp (§F D155/D156) or the Cradle-family AI path (§F D123). |
| **D171** | Silo missile-shaft: a large triangle sweeps across the frame while panning down the shaft. **Promoted to `docs/dev/findings.md` §F D288 (M-141)** with a user-reproduced, precisely-bounded, visually-documented repro (frames ~562-741 of a bare `-level_20` boot) — read that entry for the current state, not this row. | See D288. |
| **D172** | Bullet-impact / blood particle spray renders magenta or cyan instead of dark red (visible on guards hit at close range on Silo). | **M-82: ROOT CAUSE FOUND + FIXED (PR #34).** `GE_D172` probe on a user Silo firefight: 40/40 particle tris configure tile 1 = RGBA16 fire @ tmem 392 but fast3d **samples tile 0 (smoke) for both texunits** — `gfx_lod_tile_offset()` returns 0 unconditionally (D107 blurry-ceiling fix; `video.c:225` disables `gfx_detail_textures_enabled`). `G_CC_INTERFERENCE` = `smoke*smoke` → magenta/cyan. Fix: `return rdp.tex_lod ? 0 : i` — only fold to base tile for real LOD/mip draws. (0xB9 + cycle-type were both false trails, ruled out earlier in M-82.) **Owed: in-game eyeball — blood should be dark red.** Detail: `D172-PARTICLE-COLOR-RESEARCH.md`. |
| ~~**D178**~~ | ~~Objectives not listed correctly on the pre-mission briefing screen (the mission overview shown before starting a level).~~ | **CLOSED — user-verified fixed (M-87 playtest): "main menu text is good now."** No longer reproduces. M-36 triage below kept for reference. **M-36 triage: the objective lines are BLANK, not wrong.** `GE_STARTMENU=10` Dam briefing renders the header fine ("OHMSS / Agent: James Bond / Mission 1: Arkangelsk / Part i: Dam / PRIMARY OBJECTIVES:") then just the bullet "a." with no text and no b/c/d. → **D143/D151 blank-text family** (`langGet()` → NULL → silent blank), *not* a propDef walk / difficulty decode. **Family B** in `docs/dev/M-36-TRIAGE.md` — find which lang bank the front-end briefing flow fails to load. Re-confirm the exact shape in a *real* briefing (the `GE_STARTMENU` boot skips real save state). Close relatives: D151 (watch objective text blank — type-35/23 `text` slot u16→u32 fix, M-28; interactive re-verify was still pending), D143 (briefing/objective text blank via `langGet` NULL), D157 (objective difficulty byte offset — could make difficulty-gated objectives appear/disappear wrongly). The pre-start briefing is a *different surface* from the in-level watch, so D151's fix may not cover it. **Next:** ask user which level + what exactly shows vs expected; compare against `docs/dev/LEVEL-OBJECTIVES.md` (regenerated post-D157); if lines are blank → D143/D151 family (`langGet` NULL guard = silent blank), if wrong objectives listed → propDef type-23 record walk / difficulty decode. |
| **D171** | Silo missile-shaft: a large translucent grey triangle sweeps across the lower frame while panning down the shaft (~3 s of the capture). Looks like a stray poly / near-plane clip fan, same family as D106 (portal near-plane min>max). | **Observed, not investigated** (M-34, Silo capture). Cosmetic, intermittent, tied to camera angle in the open vertical shaft. |
| **D172** | Bullet-impact / blood particle spray renders magenta or cyan instead of dark red (visible on guards hit at close range on Silo). | **Observed, not investigated** (M-34, Silo capture). A channel-order / CC-mode error in the particle-sprite path (magenta = R+B, cyan = G+B) — not the D64/D120 blood-decal family (those are crash/hang + missing decals, not color). Likely a texture-format or env-color decode in `blood_animation.c` / the fast3d sprite path. |
| **D178** | Objectives not listed correctly on the pre-mission briefing screen (the mission overview shown before starting a level). | **M-36 triage: the objective lines are BLANK, not wrong.** `GE_STARTMENU=10` Dam briefing renders the header fine ("OHMSS / Agent: James Bond / Mission 1: Arkangelsk / Part i: Dam / PRIMARY OBJECTIVES:") then just the bullet "a." with no text and no b/c/d. → **D143/D151 blank-text family** (`langGet()` → NULL → silent blank), *not* a propDef walk / difficulty decode. **Family B** in `docs/dev/M-36-TRIAGE.md` — find which lang bank the front-end briefing flow fails to load. Re-confirm the exact shape in a *real* briefing (the `GE_STARTMENU` boot skips real save state). Close relatives: D151 (watch objective text blank — type-35/23 `text` slot u16→u32 fix, M-28; interactive re-verify was still pending), D143 (briefing/objective text blank via `langGet` NULL), D157 (objective difficulty byte offset — could make difficulty-gated objectives appear/disappear wrongly). The pre-start briefing is a *different surface* from the in-level watch, so D151's fix may not cover it. **Next:** ask user which level + what exactly shows vs expected; compare against `docs/dev/LEVEL-OBJECTIVES.md` (regenerated post-D157); if lines are blank → D143/D151 family (`langGet` NULL guard = silent blank), if wrong objectives listed → propDef type-23 record walk / difficulty decode. |
| **D175** | In-game stutter / brief hang during normal play — observed opening a door on Runway (mission 3, `-level_35`) and also on Surface. No backtrace captured (gdb attach missed the window; it self-recovered). | **Observed, not investigated** (user QA report). Matches known transient-hang classes rather than looking new: D155 (post-slow-load catch-up spiral — clamped), D156 (anim NaN loop — guarded, prints one-shot stderr diag if it fires), D134 (SP/DP task-done event swallowed — fixed M-22, Runway was an original victim), D147/D152 (audio `s_imLock` steal = ~2 s hiccup). Door-open specifically = new room first visible → one-off texture-import/frame spike (likely benign). **Next:** reproduce under `tools_pc/debug.ps1` with the box idle; check console for the D156 stderr diag; if it recurs, attach gdb mid-hang and pattern-match `bt` (import_texture = benign slow frame; model.c:3131 spin = D156; pthread_mutex_lock on s_imLock = D147/D152). |
| **D176** | Surface exterior renders wrong (two co-located defects). **(a)** Sky is solid **black** — the skybox / sky gradient does not draw at all (reference: warm sunset cloud gradient). **(b)** The perimeter **tree-line backdrop renders as grey TV-static / comb interlacing** instead of snow-laden pines — the geometry is present and roughly the right silhouette but every tree texture is scrambled noise. Ground, tunnel and the dish/bunker building draw fine. | **Observed, screenshots committed** (M-35, user QA; `docs/img/bugs/d176-surface-exterior-our-build.png` vs `-reference.png`). Level is the shared Surface exterior — user reached it Dam→Surface on Agent so this is **Surface (i) `-level_36`** (confirm it also affects `-level_43`). **(a)** black sky: **M-36 partial (findings.md "D176(a)").** Env data is correct (`Sky.Clouds=1`, `CloudRGB=240,120,30`, `SkyImageId=0`); `skyRender` runs the cloud path and classifies corners correctly (switch case 12), but `skyRenderTri`/`skyRenderFull` emit nothing — not even the `env->RGB` fill shows. Not root-caused: next is probing `sub_GAME_7F097388` projected coords, verifying `skywaterimages[0]` resolves on PC (`image_bank.c` GIMG offset `0xFB4`), and a fast3d sky-tri trace. `GE_D176=1` probe shipped. **M-37: ROOT-CAUSED** — `skyRenderTri`/`skyRenderFull` build geometry *exclusively* as `G_RDPHALF_*` immediates, which `gfx_pc.cpp` deliberately no-ops (identical no-op in the PD port); fix = decode the RDPHALF tri-raster stream in fast3d (~1 session, genuine new code). **M-42: `-level_22` (Statue) confirmed a clean headless repro** — bare boot, frame ~360, upper half pure black; use it to verify the decoder. **M-47 + read-only consolidation:** Path B draft exists (`scratchpad/sky.patch`, `skyPortRenderPoly` PORT hook — geometry+texture reach GL, two unit defects open) and the full RDPHALF wire format is now decoded (S15.1 half-packing, 8-component perspective-interpolated vertex vectors); next session is mechanical per `docs/dev/D176a-SKY-NOTES.md`. `-level_36`/`-43` intro cams face down (not a sky test without `GE_INPUTSCRIPT`). **M-82: PR #18 rebased onto main + Defect-1 fix applied** — cloud tile is `s_skywaterimages[0]` = 64×64 IA8; the patch's `tc = S·32` scale is correct (matches GE's `width<<5` idiom, `glass2.c:678`), the bug was `(s16)` overflow on the ±20k-texel horizon S/T; fix = per-primitive phase-fold to the 64-texel period (`sky.c` `skyPortRenderPoly`, `#ifdef PORT`). Builds clean, no crash (bunker1/statue/surface1 verify PASS). **Unverified visually** — owed: one `-level_22` screenshot (see `D176a-SKY-NOTES.md` "verification ask"). Defect 2 (horizon-band coverage / straddling tris) still open → adaptive tessellation. **(b)** tree/rock static: **M-36 triage — NOT the D159 family.** `texSwapAltRowBytes` is a full `#ifdef PORT` no-op (`image.c:2199`), it cannot cause this. Real cause: `port/fast3d/gfx_pc.cpp` `import_texture_*` assume `line_size_bytes == full_image_line_size_bytes` (`SUPPORT_CHECK`; rgba16's is commented out *"trips ... with a garbage size"* `:658`) → non-PoT / sub-rect tiles get a flat linear read → diagonal row shear = grey static. **Family A** in `docs/dev/M-36-TRIAGE.md`. Fix: honour `line_size_bytes` as the real source pitch. Same defect as D182(2). |
| ~~**D233**~~ | ~~All levels (intermittent): near doors — especially doors you can't see through to the inside — the indoor level geometry vanishes and you can see straight through it into the room, while world objects (computer terminals, furniture) in that space still draw.~~ | **FIXED + VISUALLY VERIFIED (M-107 fix, M-108 verify).** `port/fast3d/gfx_pc.cpp` `gfx_sp_tri1` was trivial-rejecting visible triangles that had a behind-camera (`w<0`) vertex. Status cell here was stale at "not investigated" — corrected M-119. §F D233. |
| ~~**D234**~~ | ~~Dam: the guard towers along the actual dam structure are invisible except for the first one.~~ | **FIXED + VISUALLY VERIFIED (M-108).** Same underlying mechanism as D233. Status cell here was stale at "not investigated" — corrected M-119. §F D234. |
| **D235** | Wrong textures load after dying/restarting a level — clarified: not Facility-specific, seen on multiple levels; trigger is death/restart (level reload), not a general build regression. | **Observed, not investigated** (M-106, user QA report, clarified same session). This is a state-leak on level re-entry, not a one-level texture-decode bug — prime suspect is the texture pool / CI-cache (D217/D228 neighbourhood) not fully resetting between a level teardown and reload (stale arena base, a palette-cache entry keyed by a reused address, or a texpool index not rewound). Needs: confirm it does NOT repro on a fresh level boot (only after death/restart); which surfaces; a screenshot. Next: instrument `texInitPool`/the CI-cache clear path for what does/doesn't reset on reload vs cold boot. |
| **D236** | Surface 1: the 2D billboard trees that appear at the start of the level and around the map don't draw as discrete tree sprites — they render as a wall/sheet of tree texture instead of the proper 2D tree objects. | **OPEN — second pass active (D266–D269); first-pass decode fix was reverted after a negative user re-test (D265)** (M-106, user QA report). The earlier IA16 framing was a misidentification: the cards are flat quads by design (no billboard opcode; byte-matched ROM emission) and bind an opaque IA8 64×16 strip plus an alpha-blended **IA4 128×16** strip (texdump r117) that is a row of ~13 faint conifer silhouettes. The wall came from `import_texture_ia4` decoding the RDP's I2:A2 nibble as I3:A1 — A2=1 canopy pixels went fully opaque, A2=2 dithering vanished (bug inherited verbatim from the PD port). **Second pass (D266–D269) re-framed the bug: the visible wall band is CI8 32×32 gradient/blob cards, not IA at all** — two draw classes per room stream (OPA + XLU), BILERP/TEXTLOD/2-cycle/**FOG-on**; texel-alpha blending proven correct by force-alpha A/B (D268). Current ranked suspects: fog range/application (D218 family) > TEXTLOD/mip sampling > draw density/culling > palette. The I2:A2 decode + AC-aware edge fixes are re-applied in-tree (uncommitted) and still owe a game-wide IA4 regression sweep before commit. |
| **D249** | Streets and Egyptian (the two biggest/most-open levels) show culling problems on far-away geometry at *default* FOV — distant geometry drops/pops incorrectly. Also a QoL ask: improve LOD in general, but gate it behind a toggle so N64-accurate ("old school") LOD stays selectable. | **Observed, not investigated** (M-119, user QA report). Distinct from D222 (high-FOV near-edge culling) and D218 (fog/far-clip tint). Two candidate mechanisms: (a) portal/room-visibility cutting off too aggressively at range on large multi-room layouts (D233/D234 neighbourhood — check for a far-plane/depth-range sibling to D233's w<0 trivial-reject fix), or (b) GE's own LOD/detail-swap system not ported faithfully (mip-tile / simple-model substitution at distance, `gfx_lod_tile_offset` family). First checks: `GE_PCDUMP` a long sightline on Streets/Egyptian where geometry visibly pops; determine which of (a)/(b) is firing before designing the fix or the requested toggle. §F D249. |
| **D217** | 1P weapon viewmodel + character-model CI textures render with a wrong palette on many levels / multiple weapons — PP7 grip + hand render flat green on Facility & Depot, "weird or missing" gun textures elsewhere; Bond's watch face too. Surfaced by D215 (viewmodel now draws). Clean on Bunker1. | **Defect ① FIXED (M-90), pending human playtest eyeball — full detail in `findings.md` §H "D217 — M-90" and §F D217.** Root cause: weapon texpool arena bases land %8==4 on PC (memp bump drift from pointer bloat); `texAlignIndices` absolute 8-byte row padding then shifts every palette 4 bytes early while model-DL `G_LOADTLUT` reads the N64 offset (grip count=1 load lands on next tenant's header slot → `0x1A00` green; larger loads read a 2-entry zero pad — the M-86 tell). Fix: `texInitPool` rounds the base up to 8 under `#ifdef PORT` (no-op on N64); 21/21 sweep PASS. Two defects: ① **visible green = misaligned texpool arena** (fixed as above). ② **CI texture-cache collision** (no palette-content key) — proven, **fix landed on branch `fix/d217-model-texture-palette` `f6fc0399`** (palette FNV-1a hash in the CI cache key), owes a 21-level `verify.sh sweep`; does not fix ①. |
| **D176** | Surface exterior renders wrong (two co-located defects). **(a)** Sky is solid **black** — the skybox / sky gradient does not draw at all (reference: warm sunset cloud gradient). **(b)** The perimeter **tree-line backdrop renders as grey TV-static / comb interlacing** instead of snow-laden pines — the geometry is present and roughly the right silhouette but every tree texture is scrambled noise. Ground, tunnel and the dish/bunker building draw fine. | **Observed, screenshots committed** (M-35, user QA; `docs/img/bugs/d176-surface-exterior-our-build.png` vs `-reference.png`). Level is the shared Surface exterior — user reached it Dam→Surface on Agent so this is **Surface (i) `-level_36`** (confirm it also affects `-level_43`). **(a)** black sky: **M-36 partial (findings.md "D176(a)").** Env data is correct (`Sky.Clouds=1`, `CloudRGB=240,120,30`, `SkyImageId=0`); `skyRender` runs the cloud path and classifies corners correctly (switch case 12), but `skyRenderTri`/`skyRenderFull` emit nothing — not even the `env->RGB` fill shows. Not root-caused: next is probing `sub_GAME_7F097388` projected coords, verifying `skywaterimages[0]` resolves on PC (`image_bank.c` GIMG offset `0xFB4`), and a fast3d sky-tri trace. `GE_D176=1` probe shipped. **M-37: ROOT-CAUSED** — `skyRenderTri`/`skyRenderFull` build geometry *exclusively* as `G_RDPHALF_*` immediates, which `gfx_pc.cpp` deliberately no-ops (identical no-op in the PD port); fix = decode the RDPHALF tri-raster stream in fast3d (~1 session, genuine new code). **M-42: `-level_22` (Statue) confirmed a clean headless repro** — bare boot, frame ~360, upper half pure black; use it to verify the decoder. `-level_36`/`-43` intro cams face down (not a sky test without `GE_INPUTSCRIPT`). **(b)** tree/rock static: **M-36 triage — NOT the D159 family.** `texSwapAltRowBytes` is a full `#ifdef PORT` no-op (`image.c:2199`), it cannot cause this. Real cause: `port/fast3d/gfx_pc.cpp` `import_texture_*` assume `line_size_bytes == full_image_line_size_bytes` (`SUPPORT_CHECK`; rgba16's is commented out *"trips ... with a garbage size"* `:658`) → non-PoT / sub-rect tiles get a flat linear read → diagonal row shear = grey static. **Family A** in `docs/dev/M-36-TRIAGE.md`. Fix: honour `line_size_bytes` as the real source pitch. Same defect as D182(2). |
| **D174** | "No blood effect" — user reports no visible blood when shooting guards / into surfaces. | **User QA report; likely D120, unverified.** Decals: the D120 `PointUsage[]` converter fix (M-30) was never interactively verified and only takes effect after a full sidecar regen (`d43 && d69 && d88 --regen`) — check whether the playtest build had regenerated sidecars before concluding the fix failed. Spray: D172 observed the particle spray *drawing* (wrong color, magenta/cyan) on Silo — if the user sees no spray either, that's new info (different weapon/range, or the D172 decode is worse than "wrong color"). First step: BUNKER1 firefight with regenerated sidecars; if decals still absent, instrument the `chr.c:3328` decal walk. |
| **D173** | Third-person Bond model (the player-representing figure shown at level start) spawns too high — floats well above the ground "a lot of the time" while the actual player spawn is correct. Same symptom on the Cuba end credits: JB stands ~6 ft in the air above Natalya. | **Confirmed still present across the full 21-level playthrough (M-49).** **Observed, not investigated** (user QA report). D75 animated-model family but a *position* defect, not absence — M-33 confirmed the gun-barrel Bond renders fine upright. Suspects: (a) intro/credits puppet chr spawn position vs the model's base transform; (b) anim root-motion accumulation (`modelSetAnimFrame2WithChrStuff`, D156 area) offsetting the rendered joints above the body origin; (c) a `coord3d`/packed-float decode in the intro camera/model setup. "Player spawning is OK" localises it to the render-side model, not `g_CurrentPlayer` state. |
| **D182** | SELECT FILE screen degrades after navigating away and back (main menu → file select, or backing out of file select and re-entering). On the fresh screen the 007 gun-barrel spiral / Bond silhouette renders behind the file boxes; after a round-trip **the Bond image stops drawing entirely** and the whole background gains the **comb / interlacing** artifact (horizontal line scramble). File boxes, text and the Copy/Erase icons still draw. | **Observed, screenshot committed** (M-35, user QA; `docs/img/bugs/d182-file-select-backout-20260902.png`). Two symptoms, likely linked: **(1)** background 3D image stops rendering after re-entry → a front-end model/DL whose `render_pos` / segment binding is only valid on first construction (D149 / D75 `constructor_menuXX` family — the dossier/wallet models have the same "renders once" behavior). **(2)** interlacing = **M-36 triage: Family A**, the `import_texture_*` `line` vs `full_image_line` shear (see D176(b) / `docs/dev/M-36-TRIAGE.md`) — **not** the D159 `texSwapAltRowBytes` path (that is a full PORT no-op). Open question: why only *after* a round-trip — first import may land `line == width`, re-import may not, or the tile descriptor is stale on re-entry. Repro: `GE_STARTMENU` to file-select, back out with ESC/B, re-enter; `GE_DTEX` on the spiral texture across both visits. Shares defect (2) with D176(b). |
| **D197** | Character face/head textures wrap wrong — the face texture repeats around the side of the head "and continues weird" instead of clamping to the front. Seen on **Silo** (M-49 playtest), intermittent ("sometimes"). Reads as **`G_TX_CLAMP` not honoured** for the character-skin tile in fast3d (→ `GL_REPEAT`, so the face UV region tiles around the skull), or wrong `shifts`/`shiftt` / LOD-tile selection on the head texture, or the D167 non-PoT wrap-period issue (head textures are often non-PoT) hitting skinned models. | **Observed, not investigated** (M-49, user bug report — needs a capture + which chr/level). Distinct from D176(b)/D182(2) (those are *bg wall* pitch shear). Related: RC3/D167 (`Video.WrapFix`, non-PoT wrap period) + the `texWriteTileLods` `shifts=shiftt=lod` note in findings §F "D176(b) → 4. candidate 2". First checks: does `gfx_pc.cpp` map N64 `cms/cmt` `G_TX_CLAMP` → `GL_CLAMP_TO_EDGE` for character DLs; is the head tile getting the base tile vs an LOD tile (D107); try `Video.WrapFix=1` on a repro. |
| **D195** | Transparency / alpha-blended surfaces render wrong — reported on **Control** (Severnaya bunker basement — Natalya's console room; heavy on glass partitions and the translucent projected/holographic wall displays). Exact failure mode not yet pinned by the reporter (M-49 playtest): candidates are alpha-blended geometry drawing fully opaque, translucent panels drawing invisible, or draw-order/sort so translucent surfaces occlude what's behind. | **Observed, not investigated** (M-49, user bug report — needs a `GE_PCDUMP` capture + which surfaces). fast3d render-mode / combiner path. Same neighborhood as D161 (CI8 tile + `G_TT_NONE` → garbage), D172 (particle-sprite channel order), D176(b) (texture pitch shear) — but this is a *blend/alpha* defect, not a texture-decode one. Note D128 already touched Control's `specialportalarray` (portal struct stride) — check whether the affected surfaces are portal-adjacent. First checks: does `gfx_pc.cpp` honour `G_RM_*` XLU render modes + `G_AC_THRESHOLD`/`G_CC` alpha for these DLs; is `gDPSetRenderMode` `w1` being parsed (cf. findings §F "zbuf-clear w1 scribble"); is Z-write-vs-blend ordering right for XLU surfaces. Get the reporter to say: opaque-where-should-be-clear, invisible, or wrong edges? |
| **D252** | Particle effects cycle through a rainbow palette — bullet-impact sparks and lingering smoke/explosion residue drift through the full hue wheel over time (~10 shots to sweep it) instead of holding their intended grey/orange palette (M-124, user playtest). Full mechanism + suspect in findings §F **D252**: colour comes from 15 distinct `IMAGE_SMOKE_N`×`IMAGE_FIRE_N` (RGBA16) pairs the particles age through; prime suspect is `import_texture_rgba16` deriving width/height from the tile line size (32×7 instead of 16×14 for the fire tiles), scrambling each stage's sprite into a different wrong hue. | **Documented only — not fixed for v0.2.0-pre** (user instruction). Confirm with one dumped fire tile vs raw `IMAGE_FIRE_N` bytes, then derive width/height from the LOADBLOCK dtx/dlh. D172/M-82 lineage. |
| **1P weapon viewmodel** | **RESOLVED (M-83, D215).** In-level first-person weapon models drew nothing. Root cause: `gunRenderFirstPersonGunModels` built its `ModelRenderData` via `*(ModelRenderData*)&D_80035CC0` — a reinterpret across two separately-declared adjacent globals that only lands right on the N64 linker + 4-byte pointers; on x86-64 `renderdata.flags` read 0, gating every weapon DL node off in `modelRenderNodeGundl`. All 3 ranked suspects in VIEWMODEL-RESEARCH.md were ruled out first by the `GE_DVM=1` probe. Fixed `#ifdef PORT` (explicit field init). `subdraw` 1→153 gfx cmds; owed one BUNKER1 eyeball. |
| **1P weapon viewmodel** | In-level first-person weapon models don't draw at all (no gun visible while playing). | Already documented — **AUDIT-M6 #5** (`docs/dev/AUDIT-M6-player-offsets.md`, "prime suspect") + §F D115 "still open (MED)"; M-8 handoff listed it NOT STARTED with a plan: instrument `gunfire.c:600-661` setup + `field_87F` gating first, then check `weaponModel.render_pos` arena lifetime and `weaponRwPool[192]` sizing. Note M-32b's "render_pos/dynAllocate RULED OUT" probe was the **front-end gun-barrel** path (`title.c`), not this in-level path — candidate #5 still stands. Distinct from the watch-page "dark/faint weapon model" (D75, D141 note). |
## Resolved (for reference)
File diff suppressed because it is too large. Load diff
-12
View File
@@ -7,18 +7,6 @@ auto-injected, D121), `GE_PCDUMP="80-260:40"`, ~24 s watchdog then
0x140000000). "render %" = `tools_pc/pixcount.py` non-clear on the last
captured frame. Binary at `f2beae4b` + this session's build.
**M-79/M-81 catch-up note (added M-91, since these fixes were never given a
LEVEL-STATUS entry of their own).** Both crashers below (D191) and the
AI-pacing bug (D193) that blocked Cradle were subsequently root-caused,
fixed, and playtest-verified: D191 in M-79 (user replayed both trigger
scenes — Bunker ii first-guard kill, Statue post-Trevelyan cutscene — neither
crashes; **21/21 solo levels crash-free**), D193 in M-81 (user-verified on
Cradle itself — Trevelyan and the guards run at correct pace; root cause was
D209). **No known crashing levels as of M-91.** What's still open is a full
21-mission end-to-end re-playthrough confirming completability on the
current build (the "v0.2.0 gate" in `docs/HANDOFF.md`) — the M-49 playtest
below predates both fixes and should not be read as current.
**M-49 (2026-09-04) — full-campaign playtest, v0.1.0 win64 bundle.**
User played every solo mission on the packaged build (fresh save). **19 / 21
completable start to finish.** Only two crashes in the whole game — **Bunker ii
-68
View File
@@ -1,68 +0,0 @@
# QoL Inventory — PD-vs-GE options diff
Status: **partially implemented.** M-83 landed three port-only quick wins —
DisplayFPS (D213), anisotropic filtering (D212), and the FOV slider (D211, via
`WIDESCREEN-FOV-PLAN.md` Phase 4). Rebinding / SkipIntro / per-pad tuning /
window HiDpi still open. Companion to `WIDESCREEN-FOV-PLAN.md` and
`UNLOCKED-FPS-PLAN.md`.
Method: diffed the config surface of both ports — every `configRegister*`
key in `pd_port/port/src/*.c` vs `port/src/config.c` + the F10 overlay
(`port/src/optionsoverlay.c`) + PD's `optionsmenu.c`. Principle applied:
**where PD has a QoL feature that is achievable here, it is the standard;
GE keeps its own extras.**
Classification key — **P** = port-only (no rules issues), **G** = needs a
game-code exception (rule #2 sign-off, per `WIDESCREEN-FOV-PLAN.md` framing).
## GE already has (baseline — keep)
Input: mouse enable/capture/aim+turn speed/invert Y/Y scale/smoothing/raw
input, aim band, hipfire pitch speed, menu pointer speed + mode (GE's menu
cursor is injected entirely in `port/src/input.c` — no game code), pad
deadzone/trigger %. Video: VSync, FpsCap, MSAA, TextureFilter,
FixMipTextures, WrapFix, Fullscreen, Window geometry. Game:
ScreenShakeIntensity. Debug: FrameDump, InputLog.
## PD has, GE lacks (candidates)
| Feature | PD location | Class | Effort | Notes / priority |
|---|---|---|---|---|
| **Key rebinding** _(DONE M-83, D214)_ | `port/src/input.c` `[Input.Bind]` section | P | M | Keyboard only, 12 actions, `Input.Bind.* = Key,Key` (SDL scancode names), file-driven like PD. Defaults = prior FPS layout (verified). Per-controller pad rebinding + a UI still open. |
| **SkipIntro** _(DONE M-83, D216)_ | `Game.SkipIntro` -> `src/game/lv.c` | P | S | Boots to the SELECT FILE menu, skipping the legal screen + Nintendo/Rare/GoldenEye logo attract loop. Reuses the game's own post-intro route (sets `is_first_time_on_main_menu=FALSE` + `menu_update=MENU_FILE_SELECT`); one `#ifdef PORT` line in the existing GE_STARTMENU block. Verified headless (screenshots). |
| **DisplayFPS** _(DONE M-83, D213)_ | config `Video.DisplayFPS` | P | S | Top-right readout, config-only; ~0.5 s sample window. |
| **Anisotropic filtering** _(DONE M-83, D212)_ | `Video.Anisotropy` (1–16, dflt 4) | P | S | fast3d already had the GL hook + a hardcoded 4×; M-83 exposed it as config. MipmapFilter / TextureFilter2D still not surfaced. |
| Per-pad tuning: per-stick deadzones, stick scale, rumble scale, device index, swap sticks, C-button mapping | `input.c` padsCfg block | P | M | Medium. Matters for real-controller users; PD pattern is a config section per pad. **M-87 survey addendum:** PD does *incremental* hot-plug (`SDL_CONTROLLERDEVICEADDED/REMOVED` opens/closes individual pads into the first free slot, remembers `deviceIndex=-1` on removal so a replugged pad returns to its configured seat, `input.c:473-491`); GE's port instead does a full rescan-close-and-reopen-everything (`inputRescanPads()`, `input.c:983-1000`) and the game itself latches the controller mask at boot, so a pad added later merges into controller 0 for play. PD's incremental-with-remembered-seat approach is the natural implementation shape for this row and sidesteps the boot-latch merge — fold into the design when this row gets built, not a separate item. |
| **Rumble → D224 (M-87). DEPRIORITIZED (M-87, user call) — gamepad-only, this release targets mouse/keyboard as primary.** GE's port fully no-ops the N64 Rumble Pak (`port/src/libultra.c:1220-1223` `osMotorInit/Start/Stop` all stubbed "no accessories on the PC") — GE genuinely has a rumble subsystem (`src/motor.c`, `src/joy.c`) that never reaches any output. **PD's PC port wires this straight through to real gamepad rumble**: `inputRumbleSupported()`/`inputRumble()` call `SDL_GameControllerRumble()` when the connected pad reports haptics, `Input.PadN.RumbleScale` config + an options-menu slider, `osMemSize`-style "pretend the accessory is there" pattern applied to a real one instead of faked. | P | M | Concrete PD-port precedent, straightforward port-layer wiring, ready whenever gamepad support is prioritized (no `src/` game-logic touch — GE's `motor.c` calls already exist, just need `osMotorStart/Stop` in `libultra.c` to route to a real `SDL_GameControllerRumble` call the way PD's do). |
| FakeGamepads / FirstGamepadNum / UseHIDAPI | `input.c` | P | S–M | Niche (local MP on PC); defer until MP is actually played. |
| Window polish: DefaultFullscreen/Maximize, CenterWindow, AllowHiDpi, ExclusiveFullscreen | `video.c` vid* block | P | S | HiDpi + center are the useful ones on modern Windows. |
| VSync adaptive (−1..10) | `Video.VSync` range | P | S | Note for FPS plan — GE's 0/1 toggle is fine until then. |
| **CenterHUD** (0/1/2 = left/center/right) | `Game.CenterHUD` → `g_HudAlignModeL/R` | G* | — | Already scoped in `WIDESCREEN-FOV-PLAN.md` Option B (Phase 1 decision). Not a separate item. |
| Per-player FovY + FovAffectsZoom | `Game.Player%d.FovY`, `g_PlayerExtCfg` | G | M | PD does this in game code. GE's port-only RSP FOV slider (widescreen plan Phase 4) covers the global case; per-player is a stretch goal needing the exception class. |
| MemorySize (4–2048 MB emulated RAM) | `Game.MemorySize` → `g_OsMemSizeMb` | P? | S | Check how GE sizes its OS memory heap; if fixed, make it a config knob (stability lever for heavy levels). |
| MaxExplosions / GEMuzzleFlashes / DisableMpDeathMusic | PD decomp edits | G | S | Low priority; rule-#2 cost > benefit. Skip unless asked. |
| GlareBrightness / OverexposureScale / FramebufferEffects | PD post-FX | — | — | Not applicable: GE's fast3d has no framebuffer-effect path. Excluded. |
| **HUD scale → D226 (M-87, new).** Ammo counter + bottom-left pickup/status text + top-of-screen dialogue all funnel through one function, `textRenderOutlined()` (`textrelated.c:688`), from two `bondview2.c` call sites — plain RDP texture-rectangles at native glyph size, no scale factor anywhere. Menus/options/MP UI share the same function but must NOT be scaled by this (different constraint, own layout). | P | M | User-requested. No PD precedent. Scale-about-anchor (bottom stays bottom, top stays top), matching D211's HUD-static principle. F10 slider candidate alongside FovScale. |
| **F10 overlay: mouse-wheel scroll + more settings + category grouping → D237 (M-106, new).** The F10 options overlay (`port/src/optionsoverlay.c`) works but has no wheel-scroll navigation and only a minimal row set. User ask: (1) scroll with the mouse wheel; (2) expose more settings that can easily be surfaced — explicitly `Game.NoHitFlash` (D232, screen-flash setting), plus other cheap candidates from this inventory's baseline list (e.g. `Video.Anisotropy`, `Video.TextureFilter`, `Input.MouseYScale`, `Input.MouseSmoothing`, `Input.AimBand`); (3) organise rows by category — mouse settings together, video together, etc. (group headers over the existing flat registry walk). | P | M | User-requested. No PD precedent needed — pure port-layer overlay work on top of the existing `config.c` registry; wheel event already reaches the port (`inputPostWheel`, D223). Cross-ref `OPTIONS-MENU-PLAN.md` §3. |
| **Mouse sensitivity rework → D238 (M-106, new).** User wants normal PC-shooter mouse settings: (1) a *separate* RMB aim-mode sensitivity; (2) ideally ONE single master sensitivity driving both turn + aim, **linked by default**, with a future option to set them independently (today `Input.MouseAimSpeed` dflt 16 and `Input.MouseTurnSpeed` dflt 100 are two unrelated keys, `input.c:188-190`); (3) up/down should feel mostly the same as left/right — GEPD Mouse Injector cited as the good-feel reference. Implementation shape: one master `Input.MouseSensitivity` feeding both paths with a per-mode scale factor defaulting to equal, plus independent override keys that decouple only when set; audit the Y-axis gain asymmetry (`AIM_GAIN` vs `MOUSE_TURN_GAIN`, `MouseYScale`, smoothing) so vertical tracks horizontal. Lump with the overall mouse/input fixes (D194/D165/D166 lineage; the SOLITAIRE natural-pitch scheme switch noted on D166 may subsume part of this). | P | M | User-requested. No PD precedent — GE-specific feel work in `port/src/input.c` + `config.c`. |
| **Click-to-lock / always-grab parity → D239 (M-106, new).** User: click-to-lock should be the default (it already is — `mouseCaptureMode = 1`, `input.c:185`); "always grab" feels bad on the file-select menu and is not 1:1 with click-to-lock; if parity is too complicated, **drop always-grab and remove the setting**. Re-confirms the M-82 decision to rip out `MouseCaptureMode=0` (`docs/BACKLOG.md` "M-82 playtest feedback"; D192 documents mode 0's unreachable-outer-cells defect). Recommendation: take the fallback — delete mode 0 + the config key, keep click-to-lock only. | P | S | User-requested; closes D192 by deletion rather than fix. |
| **Debug unlock-all → D225 (M-87, new, dev-tooling not player QoL).** One env-gated `#ifdef PORT` short-circuit at `fileGetIsCheatUnlocked()` (`src/game/file2.c:391`) unlocks **both** all-mission-select **and** the full cheat menu in one change — the two gates already share that single function. Bonus: exposes leftover Rare dev cheats (`CHEAT_LINEMODE` wireframe, `CHEAT_BONDPHASE`, `CHEAT_DEBUG_POS/UNK5`) that are otherwise unreachable. | P | S | User-requested testing convenience ("always enable all level access + all cheats... goes a long way"). Default off, pure query-time override, zero save-data/gameplay-logic touch. |
## GE has, PD lacks (keep as-is)
`Video.FixMipTextures`, `Video.WrapFix`, `Input.AimBand`,
`Input.HipfirePitchSpeed`, `Input.MouseSmoothing`, `Debug.FrameDump`,
`Debug.InputLog`. No action.
## Suggested Phase 4 order (after audio; ordering vs widescreen/FPS TBD)
1. **Quick wins (all P, small):** SkipIntro, DisplayFPS, aniso + mipmap
filters, window HiDpi/center.
2. **Rebinding** (P, medium) — the headline Phase 4 item; PD pattern is
config-string driven, no UI required for parity.
3. **Per-pad tuning** (P, medium).
4. Niche: fake gamepads / HIDAPI (when MP gets played); MemorySize knob
(after the heap check).
Everything here is parked behind audio like the other plans; nothing in this
inventory implies an implementation decision yet.
-192
View File
@@ -1,192 +0,0 @@
# Unlocked FPS Plan
Status: **proposal, parked** — documented while audio work (project Phase 3) is
active. Do not start Phase 0 until audio is done; first action when resumed is
the Phase 0 measurement pass.
**M-119 note:** this plan's premise below ("default preset = ~30fps") is now
stale — **D248** (M-118) found and fixed a real port bug (a pointer-width
scheduler ABI bug) that had been silently halving the game to 30fps since the
port's inception; the corrected default is a genuine 60fps. Re-measure Phase 0
against the corrected baseline before resuming this plan. Also see **D250**
(M-119, open): the exposed real 60fps cost now drops to 20-40fps intermittently
on some levels — a performance pass may be a prerequisite for this plan, not
just a baseline re-measurement.
Related: `docs/dev/findings.md` §D117 (GE_DETERM), §D155 (catch-up clamp),
`port/src/libultra.c` (pacemaker), `src/game/frametiming.c`, `src/sched.c`.
## Decisions (agreed)
1. **Default preset = the original pace** — whatever the game currently does
(measured ~30 fps; Phase 0 pins the exact rate). All testing, level
sweeps, determinism runs (`GE_DETERM`), and playtest baselines stay on the
default preset. Its code path must remain byte-for-byte what it is today.
2. **Unlocked FPS (60 / 120 / uncapped) is EXPERIMENTAL.** It ships behind a
clearly-labelled option and may be less polished than the default.
3. **Hard requirement:** raising the presentation rate must never change game
speed. Sim rate is invariant across presets. This is "the main speed-up
issue" and it gates everything else.
4. **Follow the PD PC port where feasible** (`pd_port` checkout): wall-clock
accumulator pacing with remainder carry, vsync + framerate-limit options,
safety cap when vsync is off.
## Non-goals (for now)
- Motion interpolation between sim ticks (the only thing that would make 120
fps *look* like 120 fps). Requires per-entity prev/cur state in the
DL-building game code — rule #2 territory, parked as a separate proposal.
- Changing the sim rate itself (running logic at 120 Hz with halved constants
is explicitly out — game logic).
## Background: why raising FPS today speeds up the game
One clock drives everything:
- `portTickThread` (`port/src/libultra.c`) posts a VI-retrace message on a
free-running grid (60 Hz NTSC / 50 Hz PAL, `g_tickIntervalUs`).
- `src/sched.c` `__scMain` blocks on that retrace; `__scHandleRetrace`
notifies clients → **exactly one sim tick + one render per retrace**.
- `osGetCount()` is wall-clock scaled to 46.5 MHz. `waitForNextFrame()`
(`src/game/frametiming.c`) converts elapsed time to `deltaFrames`, which
becomes `g_ClockTimer` (`src/game/lv.c:1008`) driving the per-tick sim loops.
The quantization is the trap: `nextFrameTime = (elapsed + Q/2) / Q` (Q = one
frame quantum). The `+Q/2` bias **rounds every interval up to at least 1
tick**, and `waitForNextFrame` spins until ≥1 tick is due. Consequence:
**sim speed tracks the retrace rate almost 1:1** — 30 Hz retrace → 30 ticks/s,
120 Hz retrace → ~120 ticks/s (each 8.3 ms interval rounds up to a full
tick). That is exactly the observed "anything faster speeds up the game".
Two useful existing properties:
- GE's game code already handles `g_ClockTimer == 0` and multi-tick deltas
everywhere (`for (i = 0; i < g_ClockTimer; i++)`, explicit `== 0` guards in
bondhead/bondview2/propobj/explosion). The N64 used this for dropped frames.
- D155 already clamps catch-up to 6 ticks after a real-time hitch.
## What the PD port does (reference)
- `src/game/timing.c` `frametimeCalculate()`: wall-clock accumulator with
remainder carry (`lostframetime60t`) → `diffframe60` / `diffframe240` ticks
due **per rendered frame**; render loop runs at display rate.
- Vsync + framerate limit options (`VIDEO_MAX_FPS` 200/240); vsync-off with
no cap forces the safety cap.
- Feasibility mapping to GE:
- *Pacing math (accumulator + carry)* → adoptable **port-only**. ✓
- *Render frames carrying zero sim ticks* → blocked in GE by
`waitForNextFrame`'s ≥1-tick enforcement (`frameDelay`, only settable
under `LEFTOVERDEBUG`). A crafted `osGetCount()` cannot defeat it (the
spin + `+Q/2` bias always yield ≥1). Needs a game-code change → **parked**
(see "Parked" below), and note it buys little without interpolation.
## Phases
### Phase 0 — Baseline & ground truth (port-only)
Goal: know exactly what the default preset does before touching anything.
- [ ] Measure actual sim rate and present rate of the current default build
(speedgraph + a `video.c` counter; add a periodic log line
`sim=..Hz render=..Hz`). Confirm NTSC vs PAL build.
- [ ] Find why the default is ~30 rather than the console's 60 Hz. Candidates:
- tick-thread 4 ms chunked-sleep drift under host load;
- `__scTaskReady` double-buffer check (`sched.c`) stalling a task every
other retrace;
- `__scHandleRetrace`'s client flag read `*((s32*)client + 2)` is
**out-of-bounds of `OSScClient`** (struct is only `next`+`msgQ`; the
decomp even says "im wrong size"). For `gfxClient[0]` it happens to
read `gfxClient[1].next` (zero) — but this is layout luck, worth a
documented finding either way.
- [ ] Record a Dxxx finding with the measured rates and root cause.
- [ ] **Decision gate:** if 30 fps turns out to be a port artifact rather than
original behavior, document it and decide with data whether the default
preset keeps the current pace (test continuity) or is fixed. Default
assumption: keep current behavior as the "original" preset either way;
a fix becomes its own item.
### Phase 1 — PD-style pacing hardening (port-only)
Makes the default preset stable and drift-free; no game-code changes.
- [ ] Replace the free-running tick grid in `portTickThread` with a
wall-clock accumulator + remainder carry (PD `frametimeCalculate` math):
post ticks when real time says they're due, carry the remainder, bounded
catch-up after hitches (D155 philosophy). No drift under load, no burst
of back-to-back ticks.
- [ ] `video.c`: proper frame limiter for fractional caps (spin+sleep hybrid),
triple-buffered swap chain, and the PD safety rule (vsync off + no cap →
force a max cap so "unlocked" can't spin).
- [ ] Config: keep `Video.VSync` / `Video.FpsCap`; add whatever Phase 2 needs.
### Phase 2 — Experimental presentation rates (port-only)
The user-visible "60 / 120 / unlocked" feature. Game speed is invariant **by
construction**: sim + render stay on the original tick clock; only *present*
runs faster by showing each rendered frame more than once.
- [ ] Frame duplication at the present stage (`video.c` / fast3d SDL swap
path, near `gfx_pre_swap_hook`): after a fresh frame is presented,
re-present the last finished frame on extra vsyncs until the next sim
tick's frame is ready. Target rates: 60, 120, uncapped (safety-capped à
la PD `VIDEO_MAX_FPS`).
- [ ] Input at present rate: sample SDL input continuously in
`port/src/input.c`; each sim tick consumes the latest state (compare
against PD `joy.c`). This is the real tangible win of the feature: up
to ~half a frame of latency removed even without interpolation.
- [ ] Options menu entries, clearly labelled **experimental**
(`port/src/optionsoverlay.c`), default preset untouched.
- [ ] Audio check: `src/audi.c` uses real-time `osGetTime()` and its own
thread; libaudio buffer sends ride the sim/retrace clock. Verify nothing
divides by an assumed frame rate; expect no changes.
### Phase 3 — Verification & sign-off
- [ ] `GE_DETERM` regression: default preset simulation path byte-identical to
pre-change (existing determinism diffs keep passing).
- [ ] Preset-invariance playtest: a timed in-game event (e.g. a fixed-length
animation or timer) takes the same real time on default / 60 / 120 /
unlocked. This is the acceptance test for decision #3.
- [ ] Hitch recovery: force a multi-second stall (debugger pause / asset load),
confirm D155 clamp holds and no speed spiral on any preset.
- [ ] `./build-pc.sh` + `/linkcheck` ritual; new findings in §F/§H with index
labels.
## Timing-consumer audit (each gets a line in the finding)
| Consumer | Clock class | Action |
|---|---|---|
| `src/game/frametiming.c` (`waitForNextFrame`, `updateFrameCounters`) | sim-time | unchanged; stays tick-locked |
| `src/boss.c:403,517` (main-loop gate, `MAIN_LOOP_TICK_INTERVAL`) | sim-time | unchanged; re-verify vs new pacemaker |
| `src/usb.c:387,405` (LibDragon flash timeouts) | real-time | fine as-is; document |
| `src/speed_graph.c` | measurement | fine; source of the rate log line |
| `libultrare/audio/env.c`, `reverb.c` `lastCnt` | perf counters only | fine; document |
| `src/audi.c` `g_DeltaTime` etc. (`osGetTime`) | real-time | fine; verify in Phase 2 |
## Parked (needs explicit sign-off, separate proposals)
1. **True decoupled sim/render (PD's zero-tick render frames).** Minimal
change class: relax `waitForNextFrame`'s ≥1-tick enforcement so rendered
frames can carry `deltaFrames = 0` (GE already guards `g_ClockTimer == 0`
everywhere). This is a *behavior-affecting* game-code edit gated off by
default — outside the ABI-only D3x exception, needs its own rules decision.
Perceptual benefit over Phase 2 duplication is small **without**
interpolation, so do this only as a prerequisite for #2.
2. **Motion interpolation** (prev/cur entity state, lerp at render time).
The actual "120 fps looks like 120 fps" feature. Large, invasive, in
DL-building game code. Separate proposal; not implied by this plan.
## Open questions
- Phase 0 outcome: is the ~30 fps default a port artifact? (fix vs keep — see
decision gate)
- PAL builds: default stays 50 Hz pace; experimental present rates identical
option set. Confirm no PAL-specific pacing assumptions in Phase 1 math.
- Naming/caps for "unlocked": adopt PD's safety-cap value (200/240) or expose
the cap as a slider?
**M-87 PD-legacy-survey note (candidate 8):** if this plan stalls and GE stays
fixed-tick, PD's `Game.TickRateDivisor` (0–10, slow-motion/perf lever) +
`Game.ExtraSleep` (`port/src/main.c:39,167-168`, applied `src/game/timing.c:49`)
is a cheap borrowable fallback lever — not otherwise relevant since this plan's
whole direction is decoupling sim from frame rate, the opposite of PD's
fixed-tick-with-a-divisor approach.
-96
View File
@@ -1,96 +0,0 @@
# 1P weapon viewmodel — static research (read-only)
> **RESOLVED 2026-09-09 (M-83, D215).** None of the three ranked suspects below
> was the cause — all were ruled out by the `GE_DVM=1` probe (added this
> session). The bug was `renderdata.flags == 0` in `gunRenderFirstPersonGunModels`:
> the N64 `*(ModelRenderData*)&D_80035CC0` reinterpret-across-adjacent-globals
> breaks under x86-64 pointer widening + no guaranteed global layout. Fixed
> `#ifdef PORT` in `src/game/gunfire.c`. See findings.md D215. This doc is kept
> for the ruled-out analysis only.
Companion to `GRAPHICS-BACKLOG.md` ("1P weapon viewmodel") and findings D115
item #5 / AUDIT-M6 #5. Goal: narrow the five audit suspects to a ranked,
probe-ready list. No code changed by this doc.
## Path map (verified in source)
- **Setup (per frame):** `bondview2.c:9040` → `gunUpdateAndFireBothHands()`
(`gunfire.c:1018`) → `gunUpdateAndFire(handnum)` (`gunfire.c:327`). Inside,
the weapon-model block at `gunfire.c:599-662`: computes `field_87F` (the
show/hide gate), then if set: `dynAllocate(numMatrices × sizeof Mtxf)` →
identity-fill → `modelCalculateRwDataLen` → `modelInit(HAND_WEAPON_MODEL,
&copy_of_body_obj_header[handnum], HAND_WEAPON_RWPOOL)` →
`hand->mtxlist = rwmtx` and (PORT/D102)
`weaponModel.render_pos = (RenderPosView*)rwmtx`.
- **Render (same frame, later):** `gunRenderFirstPersonGunModels` loop at
`gunfire.c:1532-1540`: **`if (handptr->field_87F == 0) continue;`** then
`subdraw(&renderdata, HAND_WEAPON_MODEL(handptr))` (`model.c:5326`) which
does `gSPSegment(gdl, 3, osVirtualToPhysical(mdl->render_pos))` and walks
`mdl->obj->RootNode` — **a NULL RootNode returns silently, drawing
nothing**.
## Statically ruled out (with evidence)
1. **Transient-arena lifetime** (the audit's prime suspect). `dynAllocate`
is a bump allocator on the vtx pool (`src/game/dyn.c:123`), reset only by
`dynSwapBuffers()` at frame end. Setup and render both happen inside one
frame, so `render_pos`/`mtxlist` are valid when consumed — in solo play.
(Split-screen mid-frame swaps would be a separate question; out of scope.)
2. **Word-index / record-size mismatch** (`flashvisptr = RWPOOL +
RwDataIndex`, `gunfire.c:650`). D52 kept `Model.datas` as `u32*` on PC
precisely so N64 word math holds, and `RwDataIndex` values are computed
at runtime by `modelCalculateRwDataIndexes` from the *PC* record sizes —
self-consistent. (Self-consistent ≠ in-bounds; see suspect 2.)
3. **Segment/render_pos mechanism itself.** Every working model in the game
(bodies, props) uses the same `gSPSegment(3, render_pos)` +
`bondviewTransformManyPosToViewMatrix` machinery — and it renders. The
**watch menu draws the same weapon models** via a different path
(`set_enviro_fog_for_items_in_solo_watch_menu`, `gunfire.c:1630+`, local
matrices, no 87F gate, no hand rw-pool) and they appear (albeit faint —
D75/D141 note). So the model assets and headers are very likely fine; the
defect is in-level-path-specific.
## Ranked suspects + probe spec
**#1 — `field_87F` gating (`gunfire.c:599-610`).** The clear-condition
disjunction contains `(Gun_hand_without_item(handnum) == 0)`. That function
(`gun.c:812-816`) returns `hand_invisible[h] > 0 || (hand_item[h] == 0 &&
field_2A44[h] < 0)` — so the term is TRUE (→ gate cleared → gun hidden)
whenever the hand *has* an item and isn't invisible. Read naively, that
hides the gun exactly when you're holding one; since the N64 shows the gun,
the runtime values of `hand_item`/`field_2A44`/`hand_invisible` must differ
from their names suggest — but this is the single most likely place for a
PC-only divergence (e.g. a field the port initializes differently). If 87F
is 0, the render loop `continue`s and the symptom is exactly "no gun, no
crash".
**#2 — rw-pool capacity.** PC records are wider than N64's (pointer fields);
`weaponRwPool[192]` (768 B) was D102's scaled-up budget, but nothing bounds-
checks it. The `#ifdef DEBUG` print at `gunfire.c:620` still tests the *N64*
32-word threshold — stale on PC. If `numRecords` words exceed 192,
`modelInitRwData` overruns the pool into whatever follows it in `struct
hand` → corruption that could kill the draw (or worse, silently).
**#3 — stale `copy_of_body_obj_header`.** Populated only at item load
(`gun.c:924` shallow copy / `gun.c:931-951` `load_object_fill_header`). If a
PC asset-load quirk zeroed it for the in-level path, `RootNode == NULL` and
`subdraw` no-ops silently. Lower priority given the watch menu renders the
same models — but cheap to confirm in the same probe.
**Probe (one session, PORT-only, env-gated `GE_DVM=1`, zero behavior
change):** in the `field_87F != 0` block at `gunfire.c:611+`, print once per
frame while a pistol is held in BUNKER1: `handnum`, `item`, each clear-term
value + final `field_87F`, `&copy_of_body_obj_header[h]`, `numMatrices`,
`numRecords` (words) vs 192, `RootNode`, `rwmtx`. One run triages all three
suspects: 87F==0 → suspect #1 (log which term); numRecords>192 → #2;
RootNode NULL/garbage → #3.
## Fix-shape notes (for when the probe lands)
- If #1: the fix is a `#ifdef PORT` initialization/correction of whichever
hand field diverges — **not** an edit to the gate logic itself (rule #2);
document as Dxxx with the N64-vs-PC value table.
- If #2: enlarge `weaponRwPool` to the measured max + a PORT bounds guard
that prints (mirrors the existing DEBUG print, correct threshold).
- If #3: trace the `load_object_fill_header` call for the in-level weapon
load; likely an asset-offset issue in the port's loader path.
+56 -76
View File
@@ -1,22 +1,19 @@
---
title: Agentic Development
description: A case study in two AI coding agents porting a Nintendo 64 game to PC; the setup, timeline, handoff workflow, and an honest assessment of what worked.
---
# Agentic development: two AI coding agents porting GoldenEye 007
## Agentic development: two AI coding agents porting GoldenEye 007
*How this port was actually built: a local open-weight model (Qwen 3.8 on a
single RTX 5090) and a hosted frontier model (Claude / Claude Code) handing
work back and forth through shared written notes, under one person's
part-time direction. The goal, the setup, the timeline, and an honest read on
what did and didn't work.*
*A case study in AI-agent collaboration on a large, low-level codebase. A
GoldenEye 007 Nintendo 64 → desktop port, taken from "builds a ROM" to
"playable front end" in about two weeks — driven mostly by two coding agents (a
local open-weight model, Qwen 3.8, on a single RTX 5090, and a hosted frontier
model, Claude / Claude Code), handing work back and forth through shared
written notes under one person's part-time direction. This is the goal, the
method, the timeline, and an honest read on what did and didn't work.*
## Contents
- [Why this project exists](#why-this-project-exists)
- [The setup](#the-setup)
- [Timeline](#timeline)
- [By the numbers](#by-the-numbers); [Commit velocity](#commit-velocity) · [Who did what](#who-did-what)
- [By the numbers](#by-the-numbers) — [Commit velocity](#commit-velocity) · [Who did what](#who-did-what)
- [The handoff workflow](#the-handoff-workflow)
- [Assessment](#assessment)
@@ -24,7 +21,7 @@ what did and didn't work.*
The playable port is real, but it is not the primary deliverable. The goal was
to **test how well coding agents hold up on a large, unfamiliar, low-level
codebase**; one with none of the properties that make web-app work easy for
codebase** — one with none of the properties that make web-app work easy for
an LLM:
- ~230 translation units of decompiled Nintendo 64 game C, compiled
@@ -39,7 +36,7 @@ an LLM:
Concretely it set out to validate a **two-agent arrangement**: a
locally-hosted open-weight model doing the bulk of the work on a single
consumer GPU, a hosted frontier model brought in for a collaborative phase,
and, the part that turned out most interesting, the two **handing work back
and — the part that turned out most interesting — the two **handing work back
and forth** through shared written artifacts, directed by one human.
## The setup
@@ -47,17 +44,17 @@ and forth** through shared written artifacts, directed by one human.
| | |
|---|---|
| Local agent | `unsloth/Qwen3.8-27B-GGUF:UD-Q4_K_XL` on a single **NVIDIA RTX 5090**, driven mainly through the **[pi](https://pi.dev/)** coding agent. Unsloth Desktop (Unsloth's local model runtime, which can drive agents such as Claude Code) was also trialed but not used significantly. |
| Hosted agent | **Claude**, via **Claude Code**, on a Claude Pro subscription; mostly **Sonnet 5**, with **Opus 5** used as an escalation tier for the hardest problems and whenever there was subscription budget to spend on it |
| Hosted agent | **Claude**, via **Claude Code**, on a Claude Pro subscription — mostly **Sonnet 5**, with **Opus 5** used as an escalation tier for the hardest problems and whenever there was subscription budget to spend on it |
| Human | one person: direction, work partitioning, integration, and every build / playtest / frame-capture the agents could not run |
| Base | fork of the [GoldenEye 007 decompilation](https://github.com/n64decomp/007) (years of prior work by Larry Ficken ("kholdfuzion") and contributors) |
| Reused | the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark)'s `fast3d` software RSP; same Rare engine family |
| Reused | the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark)'s `fast3d` software RSP — same Rare engine family |
The port is the GoldenEye-specific porting work **on top of** those two
existing bodies of work; it is not a from-scratch reimplementation of either.
In practice the models formed a **three-tier escalation**: the local model
handled well-scoped work, Sonnet 5 took what it stalled on, and Opus 5 was
reserved for the bugs that needed the most reasoning held at once; the same
reserved for the bugs that needed the most reasoning held at once — the same
"escalate when stuck" move applied at every level.
## Timeline
@@ -68,7 +65,7 @@ All dates from this repository's own commit history (August 2026).
gantt
dateFormat YYYY-MM-DD
axisFormat %b %d
title GoldenEye 007 PC port; from fork to v0.3.0
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
@@ -84,76 +81,63 @@ gantt
SDL input layer (kbd/mouse/gamepad) :2026-08-29, 1d
Front-end flow (menu to briefing to start) :m7, 2026-08-30, 1d
File-backed EEPROM saves :2026-08-31, 1d
section Month one - front end to v0.3.0
Audio mixer (music + SFX) :2026-09-02, 7d
Steam Deck hardening + F10 overlay QoL :2026-09-14, 3d
v0.3.0 released (full campaign playtested) :milestone, 2026-09-18, 0d
```
- **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,
- **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.
- **Day 33** (18 Sep): **v0.3.0 released** — the full campaign playtested
end to end at Agent difficulty, audio complete, bundles for Windows,
Linux and Steam Deck.
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". The rest of month one (29 Aug –
18 Sep) took it from there to a public release: the 21-level sweep to
full-campaign completion, the audio mixer, input and Steam Deck polish,
widescreen FOV scaling, and the v0.3.0 bundles — with a set of cosmetic
issues documented rather than outstanding.
through the front end into the early game" — with audio and a set of cosmetic
issues still outstanding.
## By the numbers
| | |
|---|---|
| Calendar time | 34 days (16 Aug – 18 Sep 2026), one person part-time |
| Commits on the port | ~690 |
| Root-caused bugs logged | `D1`–`D311` in [`findings.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/findings.md), 190 tracked entries in the index (some labels later merged or withdrawn) |
| Handoff sessions | 190+ (`M-2` … `M-191`, and counting) |
| Game-source files given `#ifdef PORT` ABI edits | 71 files, ~385 blocks |
| New port-layer / tooling files | ~119 |
| Port layer | ~24,450 lines C/C++ (`port/`) |
| PC asset-conversion tooling | ~7,180 lines Python (`tools_pc/`) |
| Outcome | **v0.3.0 released**: full campaign playable end to end (all 21 missions playtested at Agent difficulty) at a steady 60 fps, full audio (music + SFX), file-backed saves; Windows, Linux and Steam Deck; known cosmetic defects documented in the finding log |
| Calendar time | 16 days (16 Aug – 1 Sep 2026), one person part-time |
| Commits on the port | ~223 |
| Root-caused bugs logged | 162 (`D1`–`D169` in [`findings.md`](findings.md); some later merged or withdrawn) |
| Handoff sessions | ~33 (`M-2` … `M-33`) |
| 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", "9/2", "9/3", "9/4", "9/5", "9/6", "9/7", "9/8", "9/9", "9/10", "9/11", "9/12", "9/13", "9/14", "9/15", "9/16", "9/17", "9/18"]
y-axis "commits" 0 --> 90
bar [1, 2, 2, 6, 9, 2, 4, 7, 57, 39, 39, 46, 11, 52, 39, 8, 8, 2, 8, 34, 17, 30, 20, 30, 57, 4, 84, 38, 22, 13]
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, 9]
```
Days with no commits are omitted (18–19 and 25–26 Aug). The step up from
8/28 onward is the collaborative phase in full swing, including the parallel
multi-agent "bursts"; the 9/15 spike is the Steam Deck playtest-feedback
batch landing at once.
Days with no commits are omitted. The 8/25–8/26 gap is between phases; the
step up from 8/28 onward is the collaborative phase in full swing, including
the parallel multi-agent "bursts" used near the end.
### Who did what
```mermaid
pie showData title "Commits by agent (raw count, snapshot through early September)"
pie showData title "Commits by agent (raw count)"
"Claude" : 161
"Local model (Qwen 3.8 / pi)" : 62
```
(Phase A, the first 26 commits, to 24 Aug, was entirely the local model,
solo; from 27 Aug on the two agents worked in parallel. The raw tally was
not re-run for the final third of the project; treat the milestone-weighted
estimate below as the better number.)
(Phase A — the first 26 commits, to 24 Aug — was entirely the local model,
solo; from 27 Aug on the two agents worked in parallel.)
| Milestone / workstream | Primary agent | Weight |
|---|---|---|
@@ -163,20 +147,20 @@ estimate below as the better number.)
| Offline asset-conversion architecture + first converters (`tools_pc/`) | local model | large |
| Intro rendering (logos → gun-barrel → cast) | local model | medium |
| Stage load unblocked (`D69`–`D87`) | Claude | medium |
| 21-level crash sweep; ~12 crash classes root-caused (`D88`–`D169`) | Claude | large |
| 21-level crash sweep — ~12 crash classes root-caused (`D88`–`D169`) | Claude | large |
| SDL input layer (keyboard / mouse / gamepad, mouse-look) | Claude | medium |
| Front-end flow (menu → briefing → start), EEPROM saves | Claude | medium |
| Parallel struct-layout / converter static audits | local model | medium |
| Continuing tasks after Claude hit a usage limit | local model | small |
| The hardest structural bugs (matrix handedness, format specs) | Claude | n/a |
| Every build, playtest, frame capture, integration, and direction | human | n/a |
| The hardest structural bugs (matrix handedness, format specs) | Claude | — |
| Every build, playtest, frame capture, integration, and direction | human | — |
**Estimated effort split.** The raw commit count above (~28% local / ~72%
Claude) undercounts the local model: Phase A landed the build system and boot
chain in relatively few, large commits, and the local model kept contributing
~15–20% of Phase B in parallel. Weighting by milestone difficulty rather than
raw commits; the foundation is a heavier third of the project than its commit
share suggests; the developer's estimate is roughly:
raw commits — the foundation is a heavier third of the project than its commit
share suggests — the developer's estimate is roughly:
```mermaid
pie showData title "Agent effort, milestone-weighted"
@@ -215,30 +199,28 @@ flowchart LR
Both agents worked against the **same three written artifacts**, which is what
let them substitute for each other:
1. **`HANDOFF.md`**; the current state, the immediate next task, and the
1. **`HANDOFF.md`** — the current state, the immediate next task, and the
environment gotchas. Originally a session-to-session note for one agent, it
became the **interface between the two agents**: when Claude reached a
usage limit mid-problem, the local model picked the task up from the
HANDOFF state and continued; when the local model hit a bug that needed
deeper structural reasoning, it wrote up where it was and Claude took over.
2. **`findings.md`**; the chronological finding log. 311 numbered `D`
entries by v0.3.0 (190 tracked in the index; some labels merged or
withdrawn), 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
2. **`findings.md`** — the chronological finding log. 162 numbered entries,
each a root cause with `file:line` evidence and the fix. New agents (either
model) are pointed at the relevant entries before they start.
3. **`porting-notes.md`** — the append-only "recurring bug classes" file. The
single highest-leverage artifact: it stopped both models from
re-deriving the same class of N64→PC bug over and over.
Around these, the working rules (full detail in
[`../dev-process.md`](../dev-process.md)):
- **file-partitioned tasks**; each agent's task scoped to a disjoint set of
- **file-partitioned tasks** — each agent's task scoped to a disjoint set of
files so patches never collided;
- **visible budgets**; every investigation task carried an explicit
- **visible budgets** — every investigation task carried an explicit
"N build→run cycles" limit and a defined fallback (revert probes, write up
with a confidence rating);
- **the human owns verification**; building, running the game, capturing a
- **the human owns verification** — building, running the game, capturing a
frame, and judging it against N64 reference footage was never delegated.
## Assessment
@@ -247,7 +229,7 @@ Honest notes, for anyone weighing whether this transfers.
**Worked well**
- The **local model carried the groundwork phase**; build system, boot
- The **local model carried the groundwork phase** — build system, boot
chain, OS shims, and the offline-converter architecture. None of it was
rewritten later. A 27B open model on one consumer GPU was genuinely productive
on this.
@@ -256,14 +238,14 @@ Honest notes, for anyone weighing whether this transfers.
because the accumulated notes were good. This is also what made the two
models interchangeable on a given task.
- **Handoff on limit** turned Claude's usage cap from a hard stop into a
slowdown; the local model kept the problem moving.
slowdown — the local model kept the problem moving.
- Bounded behavioural bugs (a truncated pointer, a byte-swap off by one) are a
good fit for an agent given a tight loop and a reference to diff against.
**Worked poorly / needed the human**
- **Anything requiring the running game.** Build, playtest, capture a frame,
decide whether it looks right; that loop was the human's job throughout,
decide whether it looks right — that loop was the human's job throughout,
and it was the bottleneck.
- **Non-deterministic bugs.** Frame-timing stalls and concurrent-build
flakiness repeatedly fooled agents into "fixing" regressions that were not
@@ -273,12 +255,10 @@ Honest notes, for anyone weighing whether this transfers.
rather than a fix, even with the budget raised.
- **The capability gap is a gradient, not a wall.** The local model was strong
on well-scoped work and weaker when a bug needed several interacting facts
held at once; those went to Sonnet 5, and the few that stalled Sonnet went
held at once — those went to Sonnet 5, and the few that stalled Sonnet went
to Opus 5. Each tier earned its place on the problems the tier below it
couldn't close.
**Still outstanding** (the v0.3.0 known-issues list): cosmetic rendering
defects (particle colours, Surface 1 billboard trees, front-end logos, water
seam), stretched-not-native widescreen, PAL/JP support, a controller
rebinding UI, and macOS/ARM builds. See the README's Status section and
[`GRAPHICS-BACKLOG.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/GRAPHICS-BACKLOG.md).
**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).
-200
View File
@@ -1,200 +0,0 @@
# GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
# Grep this to pick which findings.md entries to read; it is not authoritative.
label,bucket,one_liner,status_raw
D24-D30,RESOLVED,green threads / kernel / crash handler,resolved
D31-D42,RESOLVED,"boot ABI: ANIM_DATA, music seq-table, libaudio banks, Globalimagetable rebase, GL context, rsp toggle",resolved
D43-D50,RESOLVED,"model-file loading → offline sidecar ""Plan B"" (d43_emit.py); D50.6 texCopyGdls full-slot copy",resolved
D51-D58,RESOLVED,"font pixeldata, osGetCount tick rate, model RW pools, cseq BE header, synth slots, RLE menu bg, watch raw offsets, gun-barrel DL",resolved
D59-D68,RESOLVED,"intro render: blood-RLE clobber, DMA validate, OSMesgQueue, HEADS/BODIES sentinels, romCopy width, image_entry, Globalimagetable BE→LE",resolved
D70-D74,RESOLVED,"intro-logo pixels: C-array bswap, UV path, sinf/cosf DVAL(), texture-import truncation",resolved
D75,OPEN,front-end 3D model transforms (D77 audio split out below —,"OPEN (parked). M-32 triage (see §F ""D75 ADDENDUM""):"
D76 · D164,FIXED,Legal screen only draws line 1 —,"FIXED — this row and the ## D164 entry below were stale; the proposed one-line #ifdef PORT fix was actually committed 8dfc578f4 (2026-08-31), well before this correction (2026-09-18)."
D159,FIXED,"front-end wallet-Bond photo ""interlaced""/combed (RC1 / D149) —",FIXED (#ifdef PORT no-op the swap in image.c)
D161,FIXED,Depot (-level_30) ceiling = bright-blue speckle + radial rays (B2).,FIXED (port/fast3d/gfx_pc.cpp)
D165 · D166,FIXED,"Input-layer polish (M-31, port-only):",FIXED
D180 · D181,PARTIAL,Native-PC input pass (QoL run):,"LANDED, config-gated, feel-checks owed"
D186,PARTIAL,"Video.FpsCap throttles the sim, not just presentation (M-39 diag, M-42 clamp).","CLAMP LANDED (port/fast3d/gfx_sdl2.cpp, port/src/video.c); real fix owed"
D187,FIXED,"HUD ammo-type/weapon icon vanished at -O2 (M-46 root-cause, M-47 fix + verify).","FIXED (src/game/gunfire.c, #ifdef AVOID_UB); PR #19"
D188,FIXED,Linux -O2 boot crash:,"FIXED (src/libultrare/libc/xprintf.c, src/sprintf.c, #ifdef PORT); PR #22"
D189,FIXED,Linux -fstack-protector aborts on the decomp's latent stack-buffer overruns (M-48) —,"FIXED (CMakeLists.txt, src/game/stan.c #ifdef PORT); PR #23"
D190,CLOSED,"Linux -level_45 SIGSEGV at load in add_ptr_to_objective (objective.c:64) (M-48, open).","CLOSED — not a port bug; level 45 is MP-only and has no solo setup file in the ROM (root-caused M-105, reproduced on Windows)."
D191,FIXED,Windows -O2 release-bundle crash:,FIXED + PLAYTEST-VERIFIED (M-79).…
D193,FIXED,NPC AI locomotion is too slow —,FIXED + user-playtest-verified (M-81) —…
D194,PARTIAL,Mouse input needs another tuning pass —,PARTIAL — M-120 human playtest…
D195,FIXED,"Transparency / alpha surfaces broken on Control (M-49, user playtest) —","FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_vertex) —…"
D197,CLOSED,"Character face/head textures wrap around the head, seen on Silo (M-49, user playtest).",CLOSED — user playtest v0.2.0-pre
D198,FIXED,alAdpcmPull/_decodeChunk used K0_TO_PHYS (an unconditional & 0x1FFFFFFF mask) on 64-bit heap pointers (src/libultra/audio/load.c).,FIXED (src/libultra/audio/load.c) —
D199,PARTIAL,Phase-3 software audio mixer:,"LANDED (port/src/mixer.c, port/include/mixer.h, include/PR/abi.h) —"
D200,FIXED,-level_09 segfault: reverb's ALDelay.input/output are u32 —,FIXED — 5-line cast in reverb.c (this session)
D196,FIXED,"OS mouse cursor stays visible after closing the F10 options overlay in click-to-lock mode (M-49, user playtest).","FIXED (6e758f1d, merged to main) —…"
D192,CLOSED,Front-end mission/level-select grid pointer still can't reach the outer cells —,"CLOSED — the buggy mode 0 was removed entirely in the v0.2.0 prep pass (see D239); click-to-lock is now the only capture mode, so the unreachable-cells path no longer exists"
D221,FIXED,SELECT FILE mouse hit-box offset —,"FIXED (M-91) — explicit {min,max} coord2d pairs under #ifdef PORT; human mouse-playtest confirmed."
D222,FIXED,High Video.FovScale breaks culling —,"FIXED, awaiting visual verification (M-121)."
D223,FIXED,QoL ask: mouse-wheel weapon cycling should be directional like a normal PC shooter —,"FIXED (M-105, port/src/input.c) —…"
D224,?,"QoL gap (M-87, from a PD-PC-port survey):","DEPRIORITIZED (M-87, user call)."
D225,FIXED,"Dev-tooling QoL (M-87, user ask):","FIXED (2026-09-18) — GE_DEBUG_UNLOCKALL=1 short-circuits fileGetIsCheatUnlocked() (src/game/file2.c, #ifdef PORT, cached getenv) to always return TRUE; default off, no save mutation."
D226,OPEN,"QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text.","OPEN — root cause / call sites identified, not designed or implemented."
D227,FIXED,Sky renders as two independently-moving halves —,"FIXED (M-100b, PR #43) —"
D229,FIXED,"IsWater-level water renders green, pulses blue<->green (M-101; Frigate -level_26).","FIXED (port-layer) — ZLIB+CI8 water image now imports via the CI8 palette path (gfx_pc.cpp); Frigate verified live (""looks better""); sweep + Surface 2 by-eye still owed."
D230,FIXED,In-level music quality:,"FIXED (M-121, v0.2.0) —"
D233,FIXED,All levels (intermittent):,"FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_tri1) —…"
D234,FIXED,"Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).",VISUALLY VERIFIED FIXED (M-108) —…
D244,OPEN,Self-hosted CI runner (corpyvt6agent01) link failure:,"OPEN, not yet fixed. Next…"
D228,FIXED,AK47 white missing-texture patches after D217's texpool fix —,"FIXED (port/fast3d/gfx_pc.cpp, palette_to_rgba32) —"
D204,FIXED,PC audio ran ~2 % below real time permanently (AI feedback loop's 3 ms setpoint vs PC jitter).,"FIXED (port/src/audio.c, port/src/libultra.c, port/include/audio.h) —"
D203,CLOSED,"Steam Deck (SteamOS, x86_64 Linux) v0.1.0 release bundle:",CLOSED — root cause + fix in D253.
D201,FIXED,"src/libultra/audio/bnkf.c bank/instrument/sound/wavetable relocation offsets were s32, truncating the real 64-bit base pointer.",FIXED (src/libultra/audio/bnkf.c) —
D202,?,"Silenced PPK ""slap"" + broader real-time mixer corruption (M-52→M-67).",ROOT CAUSE ESTABLISHED (M-66); Disposition C implemented + measured (M-66b) —
D206,FIXED,PC played every SFX one bank slot too high —,"FIXED (M-76, src/snd.c:1001, #ifdef PORT) —"
D207,?,"Alarm klaxon starves combat SFX and doesn't recover after it stops (surfaced by D206, M-77).",MONITORING (M-77c: user reports the alarm is acceptable in play after D206; no fix shipped) —
D208,FIXED,Player weapon-fire sound goes permanently silent mid-firefight (all other SFX unaffected) —,FIXED + user playtest-verified (M-78d) —
D209,FIXED,Every AI character in the game is locked to a *walk* animation regardless of the speed the ailist commands —,FIXED + user-playtest-verified (M-81).
D210,FIXED,chrToPatrol() leaves act_patrol.lastvisible60 uninitialised on PC —,"FIXED (chraction.c:3872, #ifdef PORT → self->act_patrol.lastvisible60 = -1;, N64 line under #else).…"
D205,CLOSED,"PC plays wrong/extra SFX for gameplay events (gunfire slap layer, explosion→scream, armour pickup, melee→Klobb idx 106) —",CLOSED (M-78) — root cause was D206; all four symptoms resolved + user-confirmed by ear.
D152+,FIXED,D152 addendum (M-31):,"FIXED (src/snd.c, port/src/libultra.c, #ifdef PORT) —"
D168,FIXED,GE_PCDUMP / F12 PPM captures were vertically flipped —,FIXED (port/fast3d/gfx_opengl.cpp); D114 / D116 reclassified as capture-orientation artifacts (below).
D169,FIXED,Front-end mouse pointer can't reach the outer cells of the mission/level-select grid —,"FIXED (M-33, port/src/input.c, port-only, no #ifdef PORT)."
D178,FIXED,Pre-mission briefing screen:,FIXED (M-36) — high confidence
D175,OPEN,In-game stutter / brief hang during normal play —,"OPEN — observed, not investigated"
D176,FIXED,"Surface exterior renders wrong (-level_36), two independent defects.","(a) scroll-speed FIXED (fidelity pass; HIGH confidence on root cause, MODERATE on N64 parity); M-93 playtest + M-98 Defect-2 starburst screenshot in the sections."
D177,FIXED,Ladders non-functional —,FIXED (M-36) — high confidence; interactive climb test owed
D172,FIXED,"Bullet-impact / blood / spark particles render magenta or cyan instead of dark red (M-82, ROOT CAUSE FOUND + FIXED).",FIXED (port/fast3d/gfx_pc.cpp gfx_lod_tile_offset)…
D174,OPEN,"""No blood effect"" (user QA report).",OPEN — observed; first step = BUNKER1 firefight with regenerated sidecars
D183,OPEN,"M-36 Family A (""texture line/pitch shear"") is DISPROVEN for the Surface repro (-level_36).",Hypothesis DISPROVEN + diagnostics shipped; D176(b)/D182(2) still OPEN
D173,FIXED,Third-person Bond model spawns too high —,"MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed)."
D179,FIXED,Packaged build crashes after the logos —,"Diagnosed, not fixed —"
D180,PARTIAL,"Native-PC input pass (M-XX QoL run, qol/native-pc-input-menu, port-only).","LANDED, port-only, no #ifdef PORT.…"
D181,?,Game.ScreenShakeIntensity —,"Menu row pulled (v0.2.1, user testing)."
D184,PARTIAL,F10 port-layer options overlay —,"LANDED, port-only, no #ifdef PORT in src/."
D211,PARTIAL,"PC FOV slider — Video.FovScale (M-83 QoL wave, port-only, WIDESCREEN-FOV-PLAN.md Phase 4).",PARTIAL — M-84 refix…
D212,PARTIAL,Anisotropic-filtering config wiring —,"LANDED, port-only. Build clean."
D213,PARTIAL,On-screen FPS readout —,"LANDED, port-only. Build clean; bunker1 no-op confirmed."
D214,PARTIAL,"Keyboard rebinding — [Input.Bind] config section (M-83, port-only, PD parity, Phase 4 debt).",PARTIAL — M-84: code PASS (alt/ctrl); one doc-only fix.
D215,FIXED,1P weapon viewmodel never drew in-level —,"FIXED (src/game/gunfire.c, #ifdef PORT)."
D216,PARTIAL,"Game.SkipIntro — boot straight to the file-select menu (M-83, port + one #ifdef PORT game-code line, PD parity, Phase 4 quick win).",LANDED + VERIFIED this session.
D217,FIXED,1P weapon viewmodel + 3P Bond model textures garbled —,FIXED + merged (PR #47 + PR #49); white residual split out as D228.
D218,PROPOSED,Wide FOV exposes the per-level draw-distance / fog boundary —,"Logged (M-84), fix proposed + parked."
D219,RESOLVED,"PPK explosions render purple on Dam (M-87, user QA, ""not 3D but noting"").","RESOLVED (M-115, 2026-09-12) for the original PPK-purple symptom."
D220,PARTIAL,"1P muzzle flash position shifts upward at higher Video.FovScale (M-87, user QA, Jungle/M16).","ANALYZED (M-91) — the suspected unscaled-viewmodel root does NOT hold; this is correct perspective behavior, likely cosmetic/QoL."
D85,OPEN,Room primary/secondary DL → texLoadFromGdl garbage.,"OPEN (safety-netted; widen/pool fixes landed, geometry now renders)"
D86 · D87,RESOLVED,modelInitRwData truncated ptr · attract-demo BE ramromfilestructure.,resolved
D88.4,RESOLVED,propDefs polymorphic record stream not byteswapped → setupDoor crash.,resolved (d88_propdefs.py); layout audit M-20 → D132
D132,PROPOSED,D88 propDefs layout audit (M-20):,proposed fix (not applied) —
D157,FIXED,"Campaign never unlocks the next level (M-30, user bug —","FIXED (src/bondtypes.h, #ifdef PORT —…"
D160,PARTIAL,Dam level-end cutscene (Bond off the dam / bungee) never plays —,"DIAGNOSTIC SHIPPED, not root-caused (c95713f5, #ifdef PORT + GE_D160=1)…"
D89-D92,RESOLVED,"stage-load→frame: stan zero-fill overrun, portal address trunc, chr/AI spawn ptrs",resolved
D93-D102,RESOLVED,struct-pun / hardcoded-size pass:,resolved (D95 pool bump partial)
D103-D107,RESOLVED,"BUNKER1 viewport height, depth-buffer clear (G_CLEAR_DEPTH_EXT), portal near-plane, LOD mip tile",resolved
D108-D112,RESOLVED,skeletal models: d43_emit.py put_f32 byte-reversal bug,resolved
D113-D116,FIXED,portal BFS ok · matrix chain ok · player raw-offset audit + gunfire.c THROW* · HUD text X-mirror,"D114/D116 CLOSED — NOT A BUG (M-33, see D168):"
D117,OPEN,"Frame nondeterminism root-caused; GE_DETERM attempted, not achieved.","OPEN (mode attempted, not achieved)"
D118,RESOLVED,"SDL input layer; M-24 mouse-look rework (mode-aware map, real INI); D118b/c FIXED.",resolved (D118a residual)
D150,FIXED,watch OBJECTIVES / BRIEF page crash:,FIXED (src/str.c #ifdef PORT); interactive re-verify pending
D260-D265,PARTIAL,"v0.2.1 hotfix batch: D260 wheel direction swap (fixed) · D261 watch-menu nav auto-repeat (fixed, feel-check owed) · D262 watch item-preview model missing → roo…",partial — see rows
D266-D269,OPEN,"D236/D265 wall, second pass:",OPEN (the wall itself) —
D270,?,Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it.,dead-ended into D271/D272; infrastructure kept in tree
D271,FIXED,"D236 wall, third pass —",FIXED-but-insufficient (src/game/bg.c #ifdef PORT) —
D276,OPEN,"D236 wall, eighth pass —","REFRAMED — oml=0xc81049d8/fmt=3 is the primary suspect now, not D269's CI8 pair; 0x0c184b50 unattributed; wall still open, see D236"
D279,?,Backfilled — this label was consumed by an undocumented partial session (2026-09-14 ~23:00) that left artifacts in scratch/ (d279_run.log… —,undocumented — artifacts only
D281,OPEN,"Game.AllUnlocked (D257) still breaks audio, and can also break right-mouse aim, when enabled before any save has ever been written (v0.2.1 user testing… —","OPEN — user-reported regression, not root-caused; mitigation is user-facing guidance only (release notes:"
D282,FIXED,"QoL ask (v0.2.1 user testing, 2026-09-15):","FIXED (port/src/input.c, #ifdef not needed —"
D283,OPEN,"Steam Deck first-run preset (D-unnumbered in port/src/video.c/main.c, videoApplySteamOSDefaults()) did not apply for a user who added the game via Steam… —","OPEN — mechanism understood, root cause (Desktop-vs-Game-Mode STEAMOS env var difference) unverified on real hardware; user-facing workaround documented in README/release notes."
D280,?,"D236 wall, ninth pass —","REFRAMED again — no fix; next steps in detail section, see D236"
D275,?,"D236 wall, eighth pass —","done — probes live, uncommitted; see D276 for what they found"
D274,OPEN,"D236 wall, seventh pass —","d267_slot_addr fix live (always-on); content/decode question CLOSED; mip-presence CLOSED; wall still open, see D236/D276"
D273,OPEN,"D236 wall, fifth pass —","RULED OUT (port/fast3d/gfx_pc.cpp palette_to_rgba32, pre-existing/uncommitted GE_D273) —"
D272,OPEN,"D236 wall, fourth pass —","OPEN — probe GE_D272 added to port/fast3d/gfx_pc.cpp (env-gated, uncommitted); wall still open, see D236"
D137,FIXED,right-mouse (aim-sight) crash:,FIXED (src/game/gunfire.c #ifdef PORT —
D119 · D120,FIXED,guard-attack weapons_held[]->chr pun crash · blood-stain PointUsage[] chain hang,D119 fixed · D120 FIXED (M-30)
D120,FIXED,Blood-decal PointUsage[] chain —,FIXED (tools_pc/d43_emit.py).
D121,RESOLVED,WS1 frictionless per-level boot (bare -level_XX injects its -m* row).,resolved
D122,FIXED,per-level prop/item model-load crash (Dam/Facility/Runway modelLoad/modelInitRwData):,converter fixed; residual chr/fast3d crashes on those levels are separate
D123,FIXED,crash class C1: chrIsNotDeadOrShot NULL deref on 6 levels (Dam/Runway/Frigate/Statue/Streets/Cradle).,"converter fixed (OBJ_ID_WORDS); C1 cleared on all 6, residual crashes are fast3d (C2)"
D125,RESOLVED,crash classes C3+C6 (Aztec/Bunker2/Surface2):,resolved (M-16b) — line 374 → 8 NULs; all 21 sidecars regen'd; Aztec -level_28 now PASSES (was C3 CRASH).
D126,RESOLVED,"crash classes C3r/C4/C6 (Bunker2 -level_27 door7F054FB4 propobj.c:13523, Depot -level_30 prop.c:902, Surface2 -level_43 loadobjectmodel.c:393):",resolved — d88_propdefs.py PROPDEF_PC_BYTES[30/32/33/35]=24 + typed handler; loadobjectmodel.c sizepropdef PORT returns 6.
D124,FIXED,crash class C2: fast3d bad texture pointer.,Jungle fixed (port/src/gimgfixup.c); Facility → D130
D131,RESOLVED,crash class C2m (Jungle -level_37):,"resolved — seg_addr() restores the module high word for any fallthrough w1 in [0x40000000, 0x70000000) (module fixed-based at 0x140000000, no ASLR; DRAM/KSEG0/segmented/phys all handled earlier).…"
D133,RESOLVED,intro render triage (M-20):,"RESOLVED — not a regression, parked (D75/D76, GRAPHICS-BACKLOG.md)"
D134,FIXED,"Frame-2 boot hang / sweep ""NO-FRAMES"" flakiness —",FIXED (port/src/libultra.c)
D135,FIXED,"Firefight crash — bgTestHitOnObj (propobj.c:8446) is an unported N64 GBI parser (M-22, WS6 playtest).","FIXED (src/game/propobj.c, #ifdef PORT —…"
D136,RESOLVED,"Stage-unload crash — objFree obj->prop reads float garbage (M-22, WS6 Facility playthrough).",ROOT-CAUSED → D139 (it was NOT a layout bug —
D139,FIXED,Stage-unload crash root cause + fix (M-23).,"FIXED (src/game/cleanup_objects.c, #ifdef PORT —…"
D140,FIXED,Pause-menu crash (M-23 open → M-27 fixed).,"FIXED (src/game/bondview.h, src/game/bondview2.c; #ifdef PORT)."
D141,FIXED,"Watch-menu item-model crash (M-27), exposed by D140.","FIXED (src/game/gunfire.c, #ifdef PORT)."
D152,?,"Mission-failed / objective-failed → permanent black screen (M-28, user bug report —","MITIGATED (port/src/libultra.c, M-28, #ifdef PORT/port-only).…"
D154,?,"bg.c room-geometry ray/hit-test GBI parser port (M-28, WRITTEN / UNVERIFIED / UNCOMMITTED).",M-30 re-audit + fixes (still playtest-gated for final verify).
D156,PARTIAL,"Facility outro-cutscene ""hang"" —","GUARDED (src/game/model.c, #ifdef PORT)…"
D155,FIXED,"Facility outro-cutscene end → ""hang"" = unclamped deltaFrames spiral (M-29, user bug report).","FIXED (src/game/frametiming.c, #ifdef PORT —…"
D153,RESOLVED,"-level_09 frame ~900–1400 0xc0000005 = D117/D134 load flakiness, NOT a bug (M-28).",NOT A BUG — closed. Run anchors on a quiet machine.
D151,FIXED,Watch data all blank —,"FIXED (src/bondtypes.h, #ifdef PORT —…"
D130,RESOLVED,"crash class C2 (Facility -level_34, Runway -level_35):",resolved — stage all 6 N64 fields in a local f[6] before writing any.
D164,FIXED,"Disclaimer / legal screen only draws the first line (D76 root cause, M-31).",FIXED — corrected 2026-09-18:
D165,FIXED,"Front-end menu cursor doesn't feel like a mouse pointer (M-31, user bug B5 follow-up).","FIXED (port/src/input.c; Input.MenuPointerMode, Input.HipfirePitchSpeed added)."
D166,FIXED,Hipfire mouse pitch is digital while yaw is analog —,FIXED (port/src/input.c).…
D235,OPEN,Wrong textures load after dying/restarting a level —,OPEN — needs: does it repro on a *fresh* level boot (rules out reload-state) vs only after death/restart (confirms it); which surfaces; a screenshot.…
D236,OPEN,"Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).","OPEN, sixteenth pass (2026-09-18) —"
D237,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only
D238,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only (port/src/input.c + config.c)
D239,CLOSED,"QoL ask (M-106, user QA):",CLOSED — executed in the v0.2.0 prep pass (port-only)…
D240,OPEN,"Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report).","OPEN — not root-caused; static call-site tracing only, no runtime verification possible in this environment."
D241,OPEN,Player gunshot SFX goes silent under some conditions —,"OPEN — not newly root-caused; high confidence this is D207 reproduced, not an independent bug."
D242,?,Player-death sequence UX regression vs N64 original:,NOT A PORT-LOGIC BUG as far as static analysis can tell.
D243,FIXED,"Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject):","FIXED, live-confirmed by the user (M-189/M-190, 2026-09-18)."
D245,OPEN,IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) —,"OPEN — REOPENED, M-142 headless probe found the tc-shift k measurably flips during ordinary view rotation (new root-cause evidence); an opt-in GE_D245_FIXEDSHIFT candidate is build-verified but needs…"
D246,FIXED,"Tiny pixel strips at the left and right edges of the screen, ""like overscan"" —","FIXED (2026-09-18 session, port-only, Video.SafeAreaCrop)."
D247,FIXED,Black bars top and bottom of screen —,"FIXED (2026-09-18 session, port-only, Video.SafeAreaCrop)."
D248,FIXED,"Port runs a stable 30 FPS instead of 60 FPS game-wide (M-117, user asked to verify FPS after the D209 timing-keystone fixes).","FIXED, root-caused to a genuine pointer-width ABI bug in src/sched.c (compiled for the PC build, not excluded)."
D249,OPEN,"LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV —","OPEN — M-142 static review ruled out the unscaled-far-clip and LOD-multiplier suspects (both already correct); narrowed to the portal/room-visibility BFS, needs a live screenshot+location, not more c…"
D250,FIXED,"FPS is inconsistent/low on many levels after 60 Hz shipped (M-119, user QA; D248 follow-on).","FIXED — per-call getenv() cost was the bottleneck; rock-steady 60-61 fps after, user re-tested 1440p/4K clean."
D251,FIXED,F10 options overlay QoL pass:,"FIXED (port-only, port/src/optionsoverlay.c)."
D252,PARTIAL,"Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).","PARTIALLY FIXED — a real, distinct manifestation was found and fixed, but the bug family is not fully closed, and is intermittent on BOTH platforms, not Deck-specific (corrected 2026-09-18)."
D253,FIXED,Facility (and any level with a FORCECROUCH/LADDER linked special tile) SIGSEGV on Linux/Steam Deck —,FIXED — Deck-verified:
D254,FIXED,Linux crash handler destroyed its own logs:,FIXED — field-verified.
D255,FIXED,Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle; D253 fix confirmed working).,"FIXED (M-140) — D3x-class 32-bit pointer truncation (vtxstore_fix_refs read Model* through the mistyped ChrRecord.chrflags 4-byte-enum alias instead of ObjectRecord.model), not the destroy-path race…"
D256,FIXED,Saves (and config) silently failed to persist on Linux whenever the game was not launched from the directory containing data/ —,FIXED (path half) — save-flow detail still owed from the user.
D257,FIXED,New Game.AllUnlocked option (F10; default flipped OFF for v0.2.0 —,FIXED — verified by user playtest (PR #79 merged); default flipped OFF for v0.2.0 (D259).
D259,FIXED,Game.AllUnlocked (D257) breaks audio on a fresh save:,FIXED in v0.2.1 —…
D260,FIXED,v0.2.1 (user request):,"FIXED — build green; one-line branch swap, trivially reviewable."
D261,FIXED,v0.2.1 (user request:,FIXED — feel-check received; W/S sensitivity follow-up logged above.
D262,FIXED,v0.2.1 (user report):,"FIXED (primary cause, D264) —"
D264,FIXED,"D262's root cause (ABI/layout class, cf.","FIXED — build green, probes removed; visual confirmation pending."
D263,RESOLVED,v0.2.1 (user report):,"RESOLVED (not a bug), refined (v0.2.1 further user testing, 2026-09-15)."
D265,?,"D236 investigation (port-layer, port/fast3d/gfx_pc.cpp):",PARKED — reverted to baseline; evidence + suspect list above.
D258,OPEN,PAL/JP asset conversion is broken at the source-data level —,"OPEN — repair path verified, not applied (out of v0.2.0 scope)."
D284,FIXED,port/src/random.c's MIPS64 dsll32/dsrl32 shift helpers were backwards —,"FIXED, 21-level sweep clean, no crashes —"
D285,PARTIAL,Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug:,"PARTIALLY FIXED, RECURRED PAST THE FIX (see D305, 2026-09-18)."
D286,OPEN,"User-confirmed real ~8s frame freeze on Caverns, Steam Deck —","OPEN — one confirmed occurrence, no repro trigger identified, not root-caused; distinct from D255/D285."
D287,OPEN,"User-confirmed real, non-self-recovering freeze on Windows (M-141, 2026-09-15/16) after ~15-20 minutes with no input, triggered by clicking back into the… —","OPEN — one confirmed occurrence, real (not self-recovering), not root-caused; gfxFrameMsgQ full is a lead, not a confirmed cause."
D288,OPEN,"Silo intro: a huge, flat-shaded, untextured brown/tan triangle sweeps across the frame during the camera pan down the missile shaft (M-141, user report —… —","OPEN — visually confirmed and precisely bounded (frames ~562-741 of a bare -level_20 boot), root cause not yet identified; promoted from the long-parked GRAP… |"
D289,OPEN,"Dam: a crash in the computer room, not reproducible on a second attempt (M-141, Steam Deck user playtest).","OPEN — needs a human re-test specifically on a build with the D255 fix (this session's release/v0.3.0, not any earlier build) to see if it still happens.…"
D290,OPEN,"Watch's in-game ""controller"" section shows no control-pad graphic (M-141, Steam Deck user playtest).","OPEN — observed, not investigated."
D291,OPEN,"Dam: texture glitches when the truck drives past (M-141, Steam Deck user playtest).","OPEN — observed, not investigated."
D292,FIXED,Death animations are glitched —,"MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed alongside D173) —"
D293,FIXED,"QoL ask: no discoverable way to quit the game from the UI (M-141, Steam Deck user playtest —","FIXED (M-141) — ""Quit to desktop"" row added to the F10 overlay; build-verified Win+Linux, not yet live-confirmed.…"
D294,OPEN,"Statue: ground geometry near the large CCCP/hammer-and-sickle monument briefly renders solid black over a wide area, then self-recovers 10-30 seconds later… —",OPEN — not investigated.…
D295,FIXED,"Campaign-halting crash at the end of Control Center on 00 Agent difficulty, then a crash on every subsequent relaunch/resume (GitHub #87, user campaign playtes…",FIXED (M-149) — M-148 root-caused + reproduced on HEAD; fix landed:
D296,OPEN,"Control Center, 00 Agent:","OPEN — single live report, un-triaged; observed only under a gdb-attached debug-harness launch (tools_pc/debug.ps1), not the packaged release artifact —"
D297,FIXED,v0.3.0 silently wipes ALL pre-fix player saves on first launch (D284 side effect; found during D295/M-148).,FIXED (M-150) — migration shim landed:
D298,?,Full HUD (not just the aim-mode crosshair) missing in a v0.3.0 dev build —,"s — regression reported 2026-09-16 during v0.3.0 work (build ce8427cf); user says ""don't fix, just for context""; not yet investigated —"
D299,FIXED,Game.SkipIntro crashed selecting any save slot except one —,"FIXED — fileValidateSaves() (only called from the skipped legal screen) never ran under SkipIntro, leaving saves[] all-zero; added the missing call to the SkipIntro shortcut (src/game/lv.c, #ifdef PO…"
D300,FIXED,Hipfire look feels non-uniform (issue #89):,"FIXED, user-confirmed live —"
D301,FIXED,"fileGetIsCheatUnlocked() NULL-deref, unguarded on every call site, not just the D299/SkipIntro one (GitHub #87, two reports with an identical crash PC/fault ad…",FIXED locally on release/v0.3.0 (not pushed) —
D302,FIXED,"Render FPS regression to ~40-45fps on release/v0.3.0 (user QA on the local v0.3.0 pre-release build, 2026-09-18) —","FIXED — 4 uncached getenv() calls added by D236 passes 10/12/14/16 directly inside gfx_sp_tri1 (port/fast3d/gfx_pc.cpp, per-triangle) cached with the same static int x = -1 pattern D250 established e…"
D303,OPEN,"Some muzzle flashes (M16 named specifically, ""and some others"") draw wrong —","OPEN — reported only, not yet investigated."
D304,FIXED,"Mouse sensitivity menu overhaul (user request, 2026-09-18):","FIXED — see full entry for both the range/parity pass and the same-day follow-up (collapsed to a single Input.MouseSensitivity menu row + fixed overlayUpdateScroll()'s unconditional bottom-anchor, th…"
D305,FIXED,"Steam Deck SIGSEGV on the v0.3.0 pre-release build (Cavern, early in the level, Game.AllUnlocked previously enabled on the session/save) —",FIXED (defensive guard) —
D306,OPEN,"Dam intro/truck wheels render with odd transparent-looking areas, possibly z-fighting (user QA, 2026-09-18).","OPEN — reported only, not investigated."
D307,OPEN,"In-level security camera entities can be flipped facing the wrong direction (backwards) on some levels, e.g.","OPEN — reported only, not investigated."
D308,OPEN,"Occasional z-fighting confirmed on some levels' geometry, general/broader than the D306 truck-wheel instance (user QA, 2026-09-18).","OPEN — reported/confirmed by the user, not investigated."
D309,FIXED,Complete non-self-recovering freeze (Windows) returning from Caverns to the intro screen —,FIXED via D310 — user-playtest-confirmed 2026-09-18 (Caverns→intro clean; the spin record itself observed resuming correctly in the post-fix probe log).
D310,FIXED,"RULE-2-SIGNOFF: chraiitemsize() mis-sizes 1-byte AI_PRINT records (NUL-scans forward), corrupting AI execution for any list containing an active PRINT —",FIXED — user-playtest-confirmed 2026-09-18 (v2:
D311,PARTIAL,Complete freeze in modelSetAnimFrame2WithChrStuff root-motion loop —,"GUARDED (#ifdef PORT, D156-guard extension in the same file) —"
D312,FIXED,"Throwable items (mines, grenades, etc.) can clip through walls in certain spots (user report, v0.3.0 pre-release playtest, 2026-09-18).",FIXED — bgBuildRoomVtxBounds mis-reads the G_VTX vertex count on PC (Gfx_le.dma.par bit-layout shift):
D313,FIXED,Bullet impacts do not appear on some walls/surfaces —,"FIXED — same root cause as D312, merged diagnosis; same one-line fix."
D314,OPEN,"F10 menu items flicker randomly/intermittently (reported on the main-menu context, user report, v0.3.0 playtest build, 2026-09-18).",OPEN — confirmed still present post-D316 fix (2026-09-18); user deprioritized chasing it further for now.
D315,OPEN,Menu scaling differs between main menu and in-game —,"OPEN — disclosed as a known issue, deferred to v0.3.1 (bigger design question, not a mechanical fix)."
D316,FIXED,"Mouse-clicking some menu items glitches and lands on the item below instead (user report, v0.3.0 playtest build, 2026-09-18).",FIXED — root-caused to two compounding bugs (naive mouse->logical mapping ignoring the safe-area crop; asymmetric row hit-test band) in port/src/optionsoverlay.c + port/fast3d/gfx_pc.cpp.
D317,CLOSED,"Crash when toggling Game.AllUnlocked back and forth in-game (user report, v0.3.0 playtest build, 2026-09-18) —","CLOSED — symptom of D316 (confirmed instant window close, no WER report; the D316 hit-band bias let a click on ""All unlocked"" fall through to the adjacent ""Quit to desktop"" row)."
D318,RESOLVED,Facility (regular play path):,RESOLVED (2026-09-19):
D319,FIXED,"Frame cap slider was mostly a dead zone; replaced with a plain 30/60 toggle (user ask, 2026-09-18).",FIXED — new ROW_FPSCAP cycle (30/60 only) in port/src/optionsoverlay.c; default changed to 60 in port/src/video.c.
D320,OPEN,"D318-class sweep: aim-hold → _update AI lists in 8 levels carry the same as-authored softlock race (static analysis, 2026-09-20).","OPEN — static sweep complete (Facility ai_19, Control ai_9, Depot ai_12 flagged high/med-high; Bond-combat loops likely safe); no live confirmation beyond D318 itself; probe-verify path + generalized…"
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
2 # Grep this to pick which findings.md entries to read; it is not authoritative.
3 label bucket one_liner status_raw
4 D24-D30 RESOLVED green threads / kernel / crash handler resolved
5 D31-D42 RESOLVED boot ABI: ANIM_DATA, music seq-table, libaudio banks, Globalimagetable rebase, GL context, rsp toggle resolved
6 D43-D50 RESOLVED model-file loading → offline sidecar "Plan B" (d43_emit.py); D50.6 texCopyGdls full-slot copy resolved
7 D51-D58 RESOLVED font pixeldata, osGetCount tick rate, model RW pools, cseq BE header, synth slots, RLE menu bg, watch raw offsets, gun-barrel DL resolved
8 D59-D68 RESOLVED intro render: blood-RLE clobber, DMA validate, OSMesgQueue, HEADS/BODIES sentinels, romCopy width, image_entry, Globalimagetable BE→LE resolved
9 D70-D74 RESOLVED intro-logo pixels: C-array bswap, UV path, sinf/cosf DVAL(), texture-import truncation resolved
10 D75 OPEN front-end 3D model transforms (D77 audio split out below — OPEN (parked). M-32 triage (see §F "D75 ADDENDUM"):
11 D76 · D164 FIXED Legal screen only draws line 1 — FIXED — this row and the ## D164 entry below were stale; the proposed one-line #ifdef PORT fix was actually committed 8dfc578f4 (2026-08-31), well before this correction (2026-09-18).
12 D159 FIXED front-end wallet-Bond photo "interlaced"/combed (RC1 / D149) — FIXED (#ifdef PORT no-op the swap in image.c)
13 D161 FIXED Depot (-level_30) ceiling = bright-blue speckle + radial rays (B2). FIXED (port/fast3d/gfx_pc.cpp)
14 D165 · D166 FIXED Input-layer polish (M-31, port-only): FIXED
15 D180 · D181 PARTIAL Native-PC input pass (QoL run): LANDED, config-gated, feel-checks owed
16 D186 PARTIAL Video.FpsCap throttles the sim, not just presentation (M-39 diag, M-42 clamp). CLAMP LANDED (port/fast3d/gfx_sdl2.cpp, port/src/video.c); real fix owed
17 D187 FIXED HUD ammo-type/weapon icon vanished at -O2 (M-46 root-cause, M-47 fix + verify). FIXED (src/game/gunfire.c, #ifdef AVOID_UB); PR #19
18 D188 FIXED Linux -O2 boot crash: FIXED (src/libultrare/libc/xprintf.c, src/sprintf.c, #ifdef PORT); PR #22
19 D189 FIXED Linux -fstack-protector aborts on the decomp's latent stack-buffer overruns (M-48) — FIXED (CMakeLists.txt, src/game/stan.c #ifdef PORT); PR #23
20 D190 CLOSED Linux -level_45 SIGSEGV at load in add_ptr_to_objective (objective.c:64) (M-48, open). CLOSED — not a port bug; level 45 is MP-only and has no solo setup file in the ROM (root-caused M-105, reproduced on Windows).
21 D191 FIXED Windows -O2 release-bundle crash: FIXED + PLAYTEST-VERIFIED (M-79).…
22 D193 FIXED NPC AI locomotion is too slow — FIXED + user-playtest-verified (M-81) —…
23 D194 PARTIAL Mouse input needs another tuning pass — PARTIAL — M-120 human playtest…
24 D195 FIXED Transparency / alpha surfaces broken on Control (M-49, user playtest) — FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_vertex) —…
25 D197 CLOSED Character face/head textures wrap around the head, seen on Silo (M-49, user playtest). CLOSED — user playtest v0.2.0-pre
26 D198 FIXED alAdpcmPull/_decodeChunk used K0_TO_PHYS (an unconditional & 0x1FFFFFFF mask) on 64-bit heap pointers (src/libultra/audio/load.c). FIXED (src/libultra/audio/load.c) —
27 D199 PARTIAL Phase-3 software audio mixer: LANDED (port/src/mixer.c, port/include/mixer.h, include/PR/abi.h) —
28 D200 FIXED -level_09 segfault: reverb's ALDelay.input/output are u32 — FIXED — 5-line cast in reverb.c (this session)
29 D196 FIXED OS mouse cursor stays visible after closing the F10 options overlay in click-to-lock mode (M-49, user playtest). FIXED (6e758f1d, merged to main) —…
30 D192 CLOSED Front-end mission/level-select grid pointer still can't reach the outer cells — CLOSED — the buggy mode 0 was removed entirely in the v0.2.0 prep pass (see D239); click-to-lock is now the only capture mode, so the unreachable-cells path no longer exists
31 D221 FIXED SELECT FILE mouse hit-box offset — FIXED (M-91) — explicit {min,max} coord2d pairs under #ifdef PORT; human mouse-playtest confirmed.
32 D222 FIXED High Video.FovScale breaks culling — FIXED, awaiting visual verification (M-121).
33 D223 FIXED QoL ask: mouse-wheel weapon cycling should be directional like a normal PC shooter — FIXED (M-105, port/src/input.c) —…
34 D224 ? QoL gap (M-87, from a PD-PC-port survey): DEPRIORITIZED (M-87, user call).
35 D225 FIXED Dev-tooling QoL (M-87, user ask): FIXED (2026-09-18) — GE_DEBUG_UNLOCKALL=1 short-circuits fileGetIsCheatUnlocked() (src/game/file2.c, #ifdef PORT, cached getenv) to always return TRUE; default off, no save mutation.
36 D226 OPEN QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text. OPEN — root cause / call sites identified, not designed or implemented.
37 D227 FIXED Sky renders as two independently-moving halves — FIXED (M-100b, PR #43) —
38 D229 FIXED IsWater-level water renders green, pulses blue<->green (M-101; Frigate -level_26). FIXED (port-layer) — ZLIB+CI8 water image now imports via the CI8 palette path (gfx_pc.cpp); Frigate verified live ("looks better"); sweep + Surface 2 by-eye still owed.
39 D230 FIXED In-level music quality: FIXED (M-121, v0.2.0) —
40 D233 FIXED All levels (intermittent): FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_tri1) —…
41 D234 FIXED Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report). VISUALLY VERIFIED FIXED (M-108) —…
42 D244 OPEN Self-hosted CI runner (corpyvt6agent01) link failure: OPEN, not yet fixed. Next…
43 D228 FIXED AK47 white missing-texture patches after D217's texpool fix — FIXED (port/fast3d/gfx_pc.cpp, palette_to_rgba32) —
44 D204 FIXED PC audio ran ~2 % below real time permanently (AI feedback loop's 3 ms setpoint vs PC jitter). FIXED (port/src/audio.c, port/src/libultra.c, port/include/audio.h) —
45 D203 CLOSED Steam Deck (SteamOS, x86_64 Linux) v0.1.0 release bundle: CLOSED — root cause + fix in D253.
46 D201 FIXED src/libultra/audio/bnkf.c bank/instrument/sound/wavetable relocation offsets were s32, truncating the real 64-bit base pointer. FIXED (src/libultra/audio/bnkf.c) —
47 D202 ? Silenced PPK "slap" + broader real-time mixer corruption (M-52→M-67). ROOT CAUSE ESTABLISHED (M-66); Disposition C implemented + measured (M-66b) —
48 D206 FIXED PC played every SFX one bank slot too high — FIXED (M-76, src/snd.c:1001, #ifdef PORT) —
49 D207 ? Alarm klaxon starves combat SFX and doesn't recover after it stops (surfaced by D206, M-77). MONITORING (M-77c: user reports the alarm is acceptable in play after D206; no fix shipped) —
50 D208 FIXED Player weapon-fire sound goes permanently silent mid-firefight (all other SFX unaffected) — FIXED + user playtest-verified (M-78d) —
51 D209 FIXED Every AI character in the game is locked to a *walk* animation regardless of the speed the ailist commands — FIXED + user-playtest-verified (M-81).
52 D210 FIXED chrToPatrol() leaves act_patrol.lastvisible60 uninitialised on PC — FIXED (chraction.c:3872, #ifdef PORT → self->act_patrol.lastvisible60 = -1;, N64 line under #else).…
53 D205 CLOSED PC plays wrong/extra SFX for gameplay events (gunfire slap layer, explosion→scream, armour pickup, melee→Klobb idx 106) — CLOSED (M-78) — root cause was D206; all four symptoms resolved + user-confirmed by ear.
54 D152+ FIXED D152 addendum (M-31): FIXED (src/snd.c, port/src/libultra.c, #ifdef PORT) —
55 D168 FIXED GE_PCDUMP / F12 PPM captures were vertically flipped — FIXED (port/fast3d/gfx_opengl.cpp); D114 / D116 reclassified as capture-orientation artifacts (below).
56 D169 FIXED Front-end mouse pointer can't reach the outer cells of the mission/level-select grid — FIXED (M-33, port/src/input.c, port-only, no #ifdef PORT).
57 D178 FIXED Pre-mission briefing screen: FIXED (M-36) — high confidence
58 D175 OPEN In-game stutter / brief hang during normal play — OPEN — observed, not investigated
59 D176 FIXED Surface exterior renders wrong (-level_36), two independent defects. (a) scroll-speed FIXED (fidelity pass; HIGH confidence on root cause, MODERATE on N64 parity); M-93 playtest + M-98 Defect-2 starburst screenshot in the sections.
60 D177 FIXED Ladders non-functional — FIXED (M-36) — high confidence; interactive climb test owed
61 D172 FIXED Bullet-impact / blood / spark particles render magenta or cyan instead of dark red (M-82, ROOT CAUSE FOUND + FIXED). FIXED (port/fast3d/gfx_pc.cpp gfx_lod_tile_offset)…
62 D174 OPEN "No blood effect" (user QA report). OPEN — observed; first step = BUNKER1 firefight with regenerated sidecars
63 D183 OPEN M-36 Family A ("texture line/pitch shear") is DISPROVEN for the Surface repro (-level_36). Hypothesis DISPROVEN + diagnostics shipped; D176(b)/D182(2) still OPEN
64 D173 FIXED Third-person Bond model spawns too high — MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed).
65 D179 FIXED Packaged build crashes after the logos — Diagnosed, not fixed —
66 D180 PARTIAL Native-PC input pass (M-XX QoL run, qol/native-pc-input-menu, port-only). LANDED, port-only, no #ifdef PORT.…
67 D181 ? Game.ScreenShakeIntensity — Menu row pulled (v0.2.1, user testing).
68 D184 PARTIAL F10 port-layer options overlay — LANDED, port-only, no #ifdef PORT in src/.
69 D211 PARTIAL PC FOV slider — Video.FovScale (M-83 QoL wave, port-only, WIDESCREEN-FOV-PLAN.md Phase 4). PARTIAL — M-84 refix…
70 D212 PARTIAL Anisotropic-filtering config wiring — LANDED, port-only. Build clean.
71 D213 PARTIAL On-screen FPS readout — LANDED, port-only. Build clean; bunker1 no-op confirmed.
72 D214 PARTIAL Keyboard rebinding — [Input.Bind] config section (M-83, port-only, PD parity, Phase 4 debt). PARTIAL — M-84: code PASS (alt/ctrl); one doc-only fix.
73 D215 FIXED 1P weapon viewmodel never drew in-level — FIXED (src/game/gunfire.c, #ifdef PORT).
74 D216 PARTIAL Game.SkipIntro — boot straight to the file-select menu (M-83, port + one #ifdef PORT game-code line, PD parity, Phase 4 quick win). LANDED + VERIFIED this session.
75 D217 FIXED 1P weapon viewmodel + 3P Bond model textures garbled — FIXED + merged (PR #47 + PR #49); white residual split out as D228.
76 D218 PROPOSED Wide FOV exposes the per-level draw-distance / fog boundary — Logged (M-84), fix proposed + parked.
77 D219 RESOLVED PPK explosions render purple on Dam (M-87, user QA, "not 3D but noting"). RESOLVED (M-115, 2026-09-12) for the original PPK-purple symptom.
78 D220 PARTIAL 1P muzzle flash position shifts upward at higher Video.FovScale (M-87, user QA, Jungle/M16). ANALYZED (M-91) — the suspected unscaled-viewmodel root does NOT hold; this is correct perspective behavior, likely cosmetic/QoL.
79 D85 OPEN Room primary/secondary DL → texLoadFromGdl garbage. OPEN (safety-netted; widen/pool fixes landed, geometry now renders)
80 D86 · D87 RESOLVED modelInitRwData truncated ptr · attract-demo BE ramromfilestructure. resolved
81 D88.4 RESOLVED propDefs polymorphic record stream not byteswapped → setupDoor crash. resolved (d88_propdefs.py); layout audit M-20 → D132
82 D132 PROPOSED D88 propDefs layout audit (M-20): proposed fix (not applied) —
83 D157 FIXED Campaign never unlocks the next level (M-30, user bug — FIXED (src/bondtypes.h, #ifdef PORT —…
84 D160 PARTIAL Dam level-end cutscene (Bond off the dam / bungee) never plays — DIAGNOSTIC SHIPPED, not root-caused (c95713f5, #ifdef PORT + GE_D160=1)…
85 D89-D92 RESOLVED stage-load→frame: stan zero-fill overrun, portal address trunc, chr/AI spawn ptrs resolved
86 D93-D102 RESOLVED struct-pun / hardcoded-size pass: resolved (D95 pool bump partial)
87 D103-D107 RESOLVED BUNKER1 viewport height, depth-buffer clear (G_CLEAR_DEPTH_EXT), portal near-plane, LOD mip tile resolved
88 D108-D112 RESOLVED skeletal models: d43_emit.py put_f32 byte-reversal bug resolved
89 D113-D116 FIXED portal BFS ok · matrix chain ok · player raw-offset audit + gunfire.c THROW* · HUD text X-mirror D114/D116 CLOSED — NOT A BUG (M-33, see D168):
90 D117 OPEN Frame nondeterminism root-caused; GE_DETERM attempted, not achieved. OPEN (mode attempted, not achieved)
91 D118 RESOLVED SDL input layer; M-24 mouse-look rework (mode-aware map, real INI); D118b/c FIXED. resolved (D118a residual)
92 D150 FIXED watch OBJECTIVES / BRIEF page crash: FIXED (src/str.c #ifdef PORT); interactive re-verify pending
93 D260-D265 PARTIAL v0.2.1 hotfix batch: D260 wheel direction swap (fixed) · D261 watch-menu nav auto-repeat (fixed, feel-check owed) · D262 watch item-preview model missing → roo… partial — see rows
94 D266-D269 OPEN D236/D265 wall, second pass: OPEN (the wall itself) —
95 D270 ? Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it. dead-ended into D271/D272; infrastructure kept in tree
96 D271 FIXED D236 wall, third pass — FIXED-but-insufficient (src/game/bg.c #ifdef PORT) —
97 D276 OPEN D236 wall, eighth pass — REFRAMED — oml=0xc81049d8/fmt=3 is the primary suspect now, not D269's CI8 pair; 0x0c184b50 unattributed; wall still open, see D236
98 D279 ? Backfilled — this label was consumed by an undocumented partial session (2026-09-14 ~23:00) that left artifacts in scratch/ (d279_run.log… — undocumented — artifacts only
99 D281 OPEN Game.AllUnlocked (D257) still breaks audio, and can also break right-mouse aim, when enabled before any save has ever been written (v0.2.1 user testing… — OPEN — user-reported regression, not root-caused; mitigation is user-facing guidance only (release notes:
100 D282 FIXED QoL ask (v0.2.1 user testing, 2026-09-15): FIXED (port/src/input.c, #ifdef not needed —
101 D283 OPEN Steam Deck first-run preset (D-unnumbered in port/src/video.c/main.c, videoApplySteamOSDefaults()) did not apply for a user who added the game via Steam… — OPEN — mechanism understood, root cause (Desktop-vs-Game-Mode STEAMOS env var difference) unverified on real hardware; user-facing workaround documented in README/release notes.
102 D280 ? D236 wall, ninth pass — REFRAMED again — no fix; next steps in detail section, see D236
103 D275 ? D236 wall, eighth pass — done — probes live, uncommitted; see D276 for what they found
104 D274 OPEN D236 wall, seventh pass — d267_slot_addr fix live (always-on); content/decode question CLOSED; mip-presence CLOSED; wall still open, see D236/D276
105 D273 OPEN D236 wall, fifth pass — RULED OUT (port/fast3d/gfx_pc.cpp palette_to_rgba32, pre-existing/uncommitted GE_D273) —
106 D272 OPEN D236 wall, fourth pass — OPEN — probe GE_D272 added to port/fast3d/gfx_pc.cpp (env-gated, uncommitted); wall still open, see D236
107 D137 FIXED right-mouse (aim-sight) crash: FIXED (src/game/gunfire.c #ifdef PORT —
108 D119 · D120 FIXED guard-attack weapons_held[]->chr pun crash · blood-stain PointUsage[] chain hang D119 fixed · D120 FIXED (M-30)
109 D120 FIXED Blood-decal PointUsage[] chain — FIXED (tools_pc/d43_emit.py).
110 D121 RESOLVED WS1 frictionless per-level boot (bare -level_XX injects its -m* row). resolved
111 D122 FIXED per-level prop/item model-load crash (Dam/Facility/Runway modelLoad/modelInitRwData): converter fixed; residual chr/fast3d crashes on those levels are separate
112 D123 FIXED crash class C1: chrIsNotDeadOrShot NULL deref on 6 levels (Dam/Runway/Frigate/Statue/Streets/Cradle). converter fixed (OBJ_ID_WORDS); C1 cleared on all 6, residual crashes are fast3d (C2)
113 D125 RESOLVED crash classes C3+C6 (Aztec/Bunker2/Surface2): resolved (M-16b) — line 374 → 8 NULs; all 21 sidecars regen'd; Aztec -level_28 now PASSES (was C3 CRASH).
114 D126 RESOLVED crash classes C3r/C4/C6 (Bunker2 -level_27 door7F054FB4 propobj.c:13523, Depot -level_30 prop.c:902, Surface2 -level_43 loadobjectmodel.c:393): resolved — d88_propdefs.py PROPDEF_PC_BYTES[30/32/33/35]=24 + typed handler; loadobjectmodel.c sizepropdef PORT returns 6.
115 D124 FIXED crash class C2: fast3d bad texture pointer. Jungle fixed (port/src/gimgfixup.c); Facility → D130
116 D131 RESOLVED crash class C2m (Jungle -level_37): resolved — seg_addr() restores the module high word for any fallthrough w1 in [0x40000000, 0x70000000) (module fixed-based at 0x140000000, no ASLR; DRAM/KSEG0/segmented/phys all handled earlier).…
117 D133 RESOLVED intro render triage (M-20): RESOLVED — not a regression, parked (D75/D76, GRAPHICS-BACKLOG.md)
118 D134 FIXED Frame-2 boot hang / sweep "NO-FRAMES" flakiness — FIXED (port/src/libultra.c)
119 D135 FIXED Firefight crash — bgTestHitOnObj (propobj.c:8446) is an unported N64 GBI parser (M-22, WS6 playtest). FIXED (src/game/propobj.c, #ifdef PORT —…
120 D136 RESOLVED Stage-unload crash — objFree obj->prop reads float garbage (M-22, WS6 Facility playthrough). ROOT-CAUSED → D139 (it was NOT a layout bug —
121 D139 FIXED Stage-unload crash root cause + fix (M-23). FIXED (src/game/cleanup_objects.c, #ifdef PORT —…
122 D140 FIXED Pause-menu crash (M-23 open → M-27 fixed). FIXED (src/game/bondview.h, src/game/bondview2.c; #ifdef PORT).
123 D141 FIXED Watch-menu item-model crash (M-27), exposed by D140. FIXED (src/game/gunfire.c, #ifdef PORT).
124 D152 ? Mission-failed / objective-failed → permanent black screen (M-28, user bug report — MITIGATED (port/src/libultra.c, M-28, #ifdef PORT/port-only).…
125 D154 ? bg.c room-geometry ray/hit-test GBI parser port (M-28, WRITTEN / UNVERIFIED / UNCOMMITTED). M-30 re-audit + fixes (still playtest-gated for final verify).
126 D156 PARTIAL Facility outro-cutscene "hang" — GUARDED (src/game/model.c, #ifdef PORT)…
127 D155 FIXED Facility outro-cutscene end → "hang" = unclamped deltaFrames spiral (M-29, user bug report). FIXED (src/game/frametiming.c, #ifdef PORT —…
128 D153 RESOLVED -level_09 frame ~900–1400 0xc0000005 = D117/D134 load flakiness, NOT a bug (M-28). NOT A BUG — closed. Run anchors on a quiet machine.
129 D151 FIXED Watch data all blank — FIXED (src/bondtypes.h, #ifdef PORT —…
130 D130 RESOLVED crash class C2 (Facility -level_34, Runway -level_35): resolved — stage all 6 N64 fields in a local f[6] before writing any.
131 D164 FIXED Disclaimer / legal screen only draws the first line (D76 root cause, M-31). FIXED — corrected 2026-09-18:
132 D165 FIXED Front-end menu cursor doesn't feel like a mouse pointer (M-31, user bug B5 follow-up). FIXED (port/src/input.c; Input.MenuPointerMode, Input.HipfirePitchSpeed added).
133 D166 FIXED Hipfire mouse pitch is digital while yaw is analog — FIXED (port/src/input.c).…
134 D235 OPEN Wrong textures load after dying/restarting a level — OPEN — needs: does it repro on a *fresh* level boot (rules out reload-state) vs only after death/restart (confirms it); which surfaces; a screenshot.…
135 D236 OPEN Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report). OPEN, sixteenth pass (2026-09-18) —
136 D237 OPEN QoL ask (M-106, user QA): OPEN — not designed; port-only
137 D238 OPEN QoL ask (M-106, user QA): OPEN — not designed; port-only (port/src/input.c + config.c)
138 D239 CLOSED QoL ask (M-106, user QA): CLOSED — executed in the v0.2.0 prep pass (port-only)…
139 D240 OPEN Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report). OPEN — not root-caused; static call-site tracing only, no runtime verification possible in this environment.
140 D241 OPEN Player gunshot SFX goes silent under some conditions — OPEN — not newly root-caused; high confidence this is D207 reproduced, not an independent bug.
141 D242 ? Player-death sequence UX regression vs N64 original: NOT A PORT-LOGIC BUG as far as static analysis can tell.
142 D243 FIXED Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject): FIXED, live-confirmed by the user (M-189/M-190, 2026-09-18).
143 D245 OPEN IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) — OPEN — REOPENED, M-142 headless probe found the tc-shift k measurably flips during ordinary view rotation (new root-cause evidence); an opt-in GE_D245_FIXEDSHIFT candidate is build-verified but needs…
144 D246 FIXED Tiny pixel strips at the left and right edges of the screen, "like overscan" — FIXED (2026-09-18 session, port-only, Video.SafeAreaCrop).
145 D247 FIXED Black bars top and bottom of screen — FIXED (2026-09-18 session, port-only, Video.SafeAreaCrop).
146 D248 FIXED Port runs a stable 30 FPS instead of 60 FPS game-wide (M-117, user asked to verify FPS after the D209 timing-keystone fixes). FIXED, root-caused to a genuine pointer-width ABI bug in src/sched.c (compiled for the PC build, not excluded).
147 D249 OPEN LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV — OPEN — M-142 static review ruled out the unscaled-far-clip and LOD-multiplier suspects (both already correct); narrowed to the portal/room-visibility BFS, needs a live screenshot+location, not more c…
148 D250 FIXED FPS is inconsistent/low on many levels after 60 Hz shipped (M-119, user QA; D248 follow-on). FIXED — per-call getenv() cost was the bottleneck; rock-steady 60-61 fps after, user re-tested 1440p/4K clean.
149 D251 FIXED F10 options overlay QoL pass: FIXED (port-only, port/src/optionsoverlay.c).
150 D252 PARTIAL Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate). PARTIALLY FIXED — a real, distinct manifestation was found and fixed, but the bug family is not fully closed, and is intermittent on BOTH platforms, not Deck-specific (corrected 2026-09-18).
151 D253 FIXED Facility (and any level with a FORCECROUCH/LADDER linked special tile) SIGSEGV on Linux/Steam Deck — FIXED — Deck-verified:
152 D254 FIXED Linux crash handler destroyed its own logs: FIXED — field-verified.
153 D255 FIXED Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle; D253 fix confirmed working). FIXED (M-140) — D3x-class 32-bit pointer truncation (vtxstore_fix_refs read Model* through the mistyped ChrRecord.chrflags 4-byte-enum alias instead of ObjectRecord.model), not the destroy-path race…
154 D256 FIXED Saves (and config) silently failed to persist on Linux whenever the game was not launched from the directory containing data/ — FIXED (path half) — save-flow detail still owed from the user.
155 D257 FIXED New Game.AllUnlocked option (F10; default flipped OFF for v0.2.0 — FIXED — verified by user playtest (PR #79 merged); default flipped OFF for v0.2.0 (D259).
156 D259 FIXED Game.AllUnlocked (D257) breaks audio on a fresh save: FIXED in v0.2.1 —…
157 D260 FIXED v0.2.1 (user request): FIXED — build green; one-line branch swap, trivially reviewable.
158 D261 FIXED v0.2.1 (user request: FIXED — feel-check received; W/S sensitivity follow-up logged above.
159 D262 FIXED v0.2.1 (user report): FIXED (primary cause, D264) —
160 D264 FIXED D262's root cause (ABI/layout class, cf. FIXED — build green, probes removed; visual confirmation pending.
161 D263 RESOLVED v0.2.1 (user report): RESOLVED (not a bug), refined (v0.2.1 further user testing, 2026-09-15).
162 D265 ? D236 investigation (port-layer, port/fast3d/gfx_pc.cpp): PARKED — reverted to baseline; evidence + suspect list above.
163 D258 OPEN PAL/JP asset conversion is broken at the source-data level — OPEN — repair path verified, not applied (out of v0.2.0 scope).
164 D284 FIXED port/src/random.c's MIPS64 dsll32/dsrl32 shift helpers were backwards — FIXED, 21-level sweep clean, no crashes —
165 D285 PARTIAL Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug: PARTIALLY FIXED, RECURRED PAST THE FIX (see D305, 2026-09-18).
166 D286 OPEN User-confirmed real ~8s frame freeze on Caverns, Steam Deck — OPEN — one confirmed occurrence, no repro trigger identified, not root-caused; distinct from D255/D285.
167 D287 OPEN User-confirmed real, non-self-recovering freeze on Windows (M-141, 2026-09-15/16) after ~15-20 minutes with no input, triggered by clicking back into the… — OPEN — one confirmed occurrence, real (not self-recovering), not root-caused; gfxFrameMsgQ full is a lead, not a confirmed cause.
168 D288 OPEN Silo intro: a huge, flat-shaded, untextured brown/tan triangle sweeps across the frame during the camera pan down the missile shaft (M-141, user report —… — OPEN — visually confirmed and precisely bounded (frames ~562-741 of a bare -level_20 boot), root cause not yet identified; promoted from the long-parked GRAP… |
169 D289 OPEN Dam: a crash in the computer room, not reproducible on a second attempt (M-141, Steam Deck user playtest). OPEN — needs a human re-test specifically on a build with the D255 fix (this session's release/v0.3.0, not any earlier build) to see if it still happens.…
170 D290 OPEN Watch's in-game "controller" section shows no control-pad graphic (M-141, Steam Deck user playtest). OPEN — observed, not investigated.
171 D291 OPEN Dam: texture glitches when the truck drives past (M-141, Steam Deck user playtest). OPEN — observed, not investigated.
172 D292 FIXED Death animations are glitched — MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed alongside D173) —
173 D293 FIXED QoL ask: no discoverable way to quit the game from the UI (M-141, Steam Deck user playtest — FIXED (M-141) — "Quit to desktop" row added to the F10 overlay; build-verified Win+Linux, not yet live-confirmed.…
174 D294 OPEN Statue: ground geometry near the large CCCP/hammer-and-sickle monument briefly renders solid black over a wide area, then self-recovers 10-30 seconds later… — OPEN — not investigated.…
175 D295 FIXED Campaign-halting crash at the end of Control Center on 00 Agent difficulty, then a crash on every subsequent relaunch/resume (GitHub #87, user campaign playtes… FIXED (M-149) — M-148 root-caused + reproduced on HEAD; fix landed:
176 D296 OPEN Control Center, 00 Agent: OPEN — single live report, un-triaged; observed only under a gdb-attached debug-harness launch (tools_pc/debug.ps1), not the packaged release artifact —
177 D297 FIXED v0.3.0 silently wipes ALL pre-fix player saves on first launch (D284 side effect; found during D295/M-148). FIXED (M-150) — migration shim landed:
178 D298 ? Full HUD (not just the aim-mode crosshair) missing in a v0.3.0 dev build — s — regression reported 2026-09-16 during v0.3.0 work (build ce8427cf); user says "don't fix, just for context"; not yet investigated —
179 D299 FIXED Game.SkipIntro crashed selecting any save slot except one — FIXED — fileValidateSaves() (only called from the skipped legal screen) never ran under SkipIntro, leaving saves[] all-zero; added the missing call to the SkipIntro shortcut (src/game/lv.c, #ifdef PO…
180 D300 FIXED Hipfire look feels non-uniform (issue #89): FIXED, user-confirmed live —
181 D301 FIXED fileGetIsCheatUnlocked() NULL-deref, unguarded on every call site, not just the D299/SkipIntro one (GitHub #87, two reports with an identical crash PC/fault ad… FIXED locally on release/v0.3.0 (not pushed) —
182 D302 FIXED Render FPS regression to ~40-45fps on release/v0.3.0 (user QA on the local v0.3.0 pre-release build, 2026-09-18) — FIXED — 4 uncached getenv() calls added by D236 passes 10/12/14/16 directly inside gfx_sp_tri1 (port/fast3d/gfx_pc.cpp, per-triangle) cached with the same static int x = -1 pattern D250 established e…
183 D303 OPEN Some muzzle flashes (M16 named specifically, "and some others") draw wrong — OPEN — reported only, not yet investigated.
184 D304 FIXED Mouse sensitivity menu overhaul (user request, 2026-09-18): FIXED — see full entry for both the range/parity pass and the same-day follow-up (collapsed to a single Input.MouseSensitivity menu row + fixed overlayUpdateScroll()'s unconditional bottom-anchor, th…
185 D305 FIXED Steam Deck SIGSEGV on the v0.3.0 pre-release build (Cavern, early in the level, Game.AllUnlocked previously enabled on the session/save) — FIXED (defensive guard) —
186 D306 OPEN Dam intro/truck wheels render with odd transparent-looking areas, possibly z-fighting (user QA, 2026-09-18). OPEN — reported only, not investigated.
187 D307 OPEN In-level security camera entities can be flipped facing the wrong direction (backwards) on some levels, e.g. OPEN — reported only, not investigated.
188 D308 OPEN Occasional z-fighting confirmed on some levels' geometry, general/broader than the D306 truck-wheel instance (user QA, 2026-09-18). OPEN — reported/confirmed by the user, not investigated.
189 D309 FIXED Complete non-self-recovering freeze (Windows) returning from Caverns to the intro screen — FIXED via D310 — user-playtest-confirmed 2026-09-18 (Caverns→intro clean; the spin record itself observed resuming correctly in the post-fix probe log).
190 D310 FIXED RULE-2-SIGNOFF: chraiitemsize() mis-sizes 1-byte AI_PRINT records (NUL-scans forward), corrupting AI execution for any list containing an active PRINT — FIXED — user-playtest-confirmed 2026-09-18 (v2:
191 D311 PARTIAL Complete freeze in modelSetAnimFrame2WithChrStuff root-motion loop — GUARDED (#ifdef PORT, D156-guard extension in the same file) —
192 D312 FIXED Throwable items (mines, grenades, etc.) can clip through walls in certain spots (user report, v0.3.0 pre-release playtest, 2026-09-18). FIXED — bgBuildRoomVtxBounds mis-reads the G_VTX vertex count on PC (Gfx_le.dma.par bit-layout shift):
193 D313 FIXED Bullet impacts do not appear on some walls/surfaces — FIXED — same root cause as D312, merged diagnosis; same one-line fix.
194 D314 OPEN F10 menu items flicker randomly/intermittently (reported on the main-menu context, user report, v0.3.0 playtest build, 2026-09-18). OPEN — confirmed still present post-D316 fix (2026-09-18); user deprioritized chasing it further for now.
195 D315 OPEN Menu scaling differs between main menu and in-game — OPEN — disclosed as a known issue, deferred to v0.3.1 (bigger design question, not a mechanical fix).
196 D316 FIXED Mouse-clicking some menu items glitches and lands on the item below instead (user report, v0.3.0 playtest build, 2026-09-18). FIXED — root-caused to two compounding bugs (naive mouse->logical mapping ignoring the safe-area crop; asymmetric row hit-test band) in port/src/optionsoverlay.c + port/fast3d/gfx_pc.cpp.
197 D317 CLOSED Crash when toggling Game.AllUnlocked back and forth in-game (user report, v0.3.0 playtest build, 2026-09-18) — CLOSED — symptom of D316 (confirmed instant window close, no WER report; the D316 hit-band bias let a click on "All unlocked" fall through to the adjacent "Quit to desktop" row).
198 D318 RESOLVED Facility (regular play path): RESOLVED (2026-09-19):
199 D319 FIXED Frame cap slider was mostly a dead zone; replaced with a plain 30/60 toggle (user ask, 2026-09-18). FIXED — new ROW_FPSCAP cycle (30/60 only) in port/src/optionsoverlay.c; default changed to 60 in port/src/video.c.
200 D320 OPEN D318-class sweep: aim-hold → _update AI lists in 8 levels carry the same as-authored softlock race (static analysis, 2026-09-20). OPEN — static sweep complete (Facility ai_19, Control ai_9, Depot ai_12 flagged high/med-high; Bond-combat loops likely safe); no live confirmation beyond D318 itself; probe-verify path + generalized…
+74 -7008
View File
File diff suppressed because it is too large. Load diff
-78
View File
@@ -1,78 +0,0 @@
# GitHub preview video — runbook
The README embeds `docs/media/goldeneye-gh-preview.mp4` (the gif of the same
name is too heavy for GitHub; keep it only if someone wants a local copy).
It's a ~32 s, 8-clip montage of gameplay cut from ShadowPlay session
recordings.
## Tooling
`scratchpad/` is gitignored, so `make-preview.py` is a **local** tool (it
hardcodes local video paths anyway) — this doc is the tracked artifact. If it
goes missing, the commands below describe exactly what to rebuild. Run with
plain `python`.
Working files (per-clip mp4s, concat list, palette, `state.json`) go to
`scratchpad/.preview-work/` — disposable, safe to wipe (but then re-run
`build` after any edit).
```sh
python scratchpad/make-preview.py list # montage index -> source session + time
python scratchpad/make-preview.py shift <i> <±sec> # nudge a clip within its source video
python scratchpad/make-preview.py set <i> <sec> # absolute source time
python scratchpad/make-preview.py replace <i> <.10|.12|.18|.19> # new random spot in a session
python scratchpad/make-preview.py regen --seed N # full random redo (same clips/lengths)
python scratchpad/make-preview.py build # re-concat + re-render from state.json
```
Every edit command rebuilds mp4 + gif automatically. A full rebuild takes
~30–50 s; a single-clip shift is the same cost (concat dominates). Budget
one command per step if running under a timeout — cut+concat and the two gif
passes together fit in ~55 s.
## Source material
`C:\Users\james\Videos\NVIDIA\Build-pc\new\` — ShadowPlay recordings
(3072x1728 @ 60 fps, h264), no usable audio track (cut with `-an`). The four
sessions and their tags are hardcoded in `SESSIONS` at the top of the script;
add a new session there (tag, filename suffix, duration) to use it.
## Rules learned from review rounds
- **Never start a clip in the first ~60 s** of a session: desktop / boot /
menus. (`MIN_START`)
- **Bad zones** — source times that show non-gameplay; random picks avoid
them and `replace` should too. Currently known (in `BAD_ZONES`):
- `.10` @ ~395–425 s: file-select menu
- `.10` @ ~1290–1420 s: watch close-up (appears twice in that session)
Add new ones here whenever a review round finds another.
- **Clip length** 4 s is the sweet spot; 5 s starts to drag, <3 s flickers.
- **Same-session spacing**: two clips from one session ≥90 s apart, or they
read as a repeat of the same scene.
- When told "roll forward/back ~N seconds", use N ± a few s and prefer a
high-motion moment in that window if you have keyframe stats (see below);
otherwise a flat offset is fine.
- If a pick lands on a menu/loading screen, step it forward again rather
than picking a new random time — the user's "a bit later/earlier" almost
always means the *same* scene, different part.
## Output spec (don't change casually)
- mp4: 1920x1080 @ 30 fps, libx264 crf 20, `+faststart`, no audio. ~25 MB for 32 s.
- gif: 512 px wide, 12 fps, two-pass palette (`palettegen` then
`paletteuse=dither=bayer:bayer_scale=3`). ~28 MB — local use only.
## Optional: motion data to find "cool" moments
`scratchpad/analyze-sessions.py` dumps per-keyframe YAVG stats for the
session files; the diff between consecutive keyframes is a decent proxy for
"something is happening". If you need to find the coolest moment in a time
window, maximize it — but note plain random picks with spacing constraints
have been preferred by the reviewer over signal-optimized cuts, so use this
only when asked for "something cool".
## ffmpeg notes (Windows)
No system ffmpeg; the script falls back to the `imageio-ffmpeg` static
binary (`pip install imageio-ffmpeg`). Pass paths as forward-slash or bare
filenames with `cwd=` set — drive-letter colons and backslashes break
filter-graph argument parsing.
+9 -70
View File
@@ -220,83 +220,22 @@ offer to create it).
`docs/building.md` says it needs region-specific offsets; the code says
otherwise. Generate `e` and `j` in CI and diff against a
locally-ROM-generated copy to confirm before committing them.
2. **Does `d43_emit.py` really use `scripts/filelist.u.csv` for all three
regions?** — *Resolved: yes, and that is a real bug.* Both emitters
hard-code the NTSC filelist regardless of `REGION`: `FILELIST =
"scripts/filelist.u.csv"` at `tools_pc/d43_emit.py:47` and
`tools_pc/d69_emit.py:56`, so `pal-final`/`jpn-final` runs read NTSC byte
offsets out of the wrong ROM — decompression garbage or `find_row()`
misses. The per-region CSVs already exist in the repo with the identical
shape (`rom_offset,size,asset_path,flag,flag`): `scripts/filelist.e.csv`
(32,843 bytes) and `scripts/filelist.j.csv` (37,947 bytes), just with bare
names instead of NTSC's `.bin`-suffixed ones — and `find_row()`
(`d43_emit.py:89-93`) already tries multiple name variants, so it would
work against any of the three CSVs if pointed at the right one. The fix is
small: pick `scripts/filelist.{u,e,j}.csv` from `REGION` instead of
hard-coding `.u`. **Remaining real unknown:** `d43_emit.py` also reads
every `assets/**/modelFileHeader.inc.c` (512 files, e.g.
`assets/obseg/chr/armourguard/modelFileHeader.inc.c:2`) to get NS/NT — arg
4 and arg 8 of the `MODELFILEHEADER(...)` macro — which it uses in
`build_nodes()` (`d43_emit.py`, ~lines 125-157) to compute the model root
offset `R0 = 4*NS + 12*NT`. Those headers are committed decomp data, one
file per model dir, with no region-suffixed variants; whether NS/NT
actually differ between NTSC/PAL/JP ROMs for the same model cannot be
verified from files in this repo alone (it would need an actual PAL or JP
ROM to check). If they don't differ, the CSV fix above makes `d43`/`d69`
region-correct on its own; if they do, that is a second, currently
invisible bug.
3. **Where does Part B's converter write, so `pcmodels.c` / `pccg.c` find it?**
— *Resolved.* Both loaders resolve the region from the ROM country byte at
offset `0x3E` via the same `Country→region` map (`'E'`→`"ntsc-final"`,
`'P'`→`"pal-final"`, `'J'`→`"jpn-final"`;
`pcmodelsRegionForCountry()` at `port/src/pcmodels.c:66-74`, same pattern
in `port/src/pccg.c:59-67`), then search the prefixes `$S/`, `$E/`, `./`
in order (`pcmodels.c:79`; `pccg.c` similar) through `sysResolvePath()`
(`port/src/system.c:207-259`: on Windows `$S/` → `./data` if it exists,
else `<exedir>/data`; `$E/` → `<exedir>/`). So a native first-run
converter in `port/src/pcconvert/` must write exactly, for region `R`:
`data/pcmodels-R/pcmodels.bin` + `data/pcmodels-R/manifest.csv`
(`pcmodels.c:100,109`) and `data/pccg-R/pccg.bin` +
`data/pccg-R/manifest.csv` (`pccg.c:92,101`). The manifest is a CSV with
header `name,offset,size`, decimal-parsed via `strtol`
(`pcmodels.c:185-186`, `pccg.c:176-177`), row cap 1024 for pcmodels / 256
for pccg.
4. **Launcher vs self-re-exec vs three shortcuts** — pick one UX.
5. **Splash UI for the first-run convert** — reuse the SDL window / a simple
regions?** It hard-codes `FILELIST = "scripts/filelist.u.csv"` even when
`REGION` is `pal-final`/`jpn-final`. Either model file offsets are
US-identical across regions (plausible — models live in obseg which may not
shift) or PAL/JP model conversion is currently untested. Confirm before
promising PAL/JP support in the release.
3. **Launcher vs self-re-exec vs three shortcuts** — pick one UX.
4. **Splash UI for the first-run convert** — reuse the SDL window / a simple
progress line, or a pre-SDL Win32 dialog? ~5 s is short enough that a log
line + busy cursor may suffice.
6. **Non-Windows.** Part B's C converter is portable; the launcher and
5. **Non-Windows.** Part B's C converter is portable; the launcher and
bundling are Windows-only today. Out of scope but the C port keeps the
door open.
7. **Signing.** Unsigned exes + a "reads your ROM" first-run step will trip
6. **Signing.** Unsigned exes + a "reads your ROM" first-run step will trip
SmartScreen. Not blocking, but worth a release-notes note.
8. **How much metadata does Part B's CMake codegen actually have to
generate?** — *Resolved: far less than the original plan assumed.* The
emitters read three kinds of committed metadata:
(a) `scripts/filelist.u.csv` (and now `.e`/`.j` per question 2) — plain CSV,
~33-41 KB each; readable from disk at runtime, no embedding needed.
(b) `assets/obseg/file_resource_table.inc.c` (38,056 bytes, 884 lines) —
used only to extract an ordered filename list via regex; a C port can read
the already-compiled `file_resource_table[]` symbol directly at link time
instead of parsing or embedding it.
(c) `assets/**/modelFileHeader.inc.c`, 512 files — **this is the one real
codegen artifact**: the C port needs a build-time-generated table of 512
entries mapping model symbol → (NS, NT), the two args of the
`MODELFILEHEADER(...)` macro that `d43_emit.py` actually uses. Everything
else the scripts use — the N64_REC/PC_REC record-size tables, ADDR_OPS
marker bytes, `d69_emit.py`'s `TILESIZES[11]` array — are small fixed
constants that can just be hand-transcribed into the C port, not
codegen'd. Bottom line: the CMake codegen step Part B calls for is
narrowly the 512-entry nsnt table, not a general metadata-embedding
system.
## Suggested milestone ordering
1. Part A (3-region matrix + pre-generated `romassets` + launcher) — days.
Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 893 KiB

-27
View File
@@ -1,27 +0,0 @@
new_name,original,video_session,snapshot_time
shot-01.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_02.11.149.jpg,2026.09.13 - 16.05.44.10,02:11:149
shot-02.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_06.00.446.jpg,2026.09.13 - 16.05.44.10,06:00:446
shot-03.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_07.56.664.jpg,2026.09.13 - 16.05.44.10,07:56:664
shot-04.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_08.33.264.jpg,2026.09.13 - 16.05.44.10,08:33:264
shot-05.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_10.16.041.jpg,2026.09.13 - 16.05.44.10,10:16:041
shot-06.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_11.03.430.jpg,2026.09.13 - 16.05.44.10,11:03:430
shot-07.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_02.25.190.jpg,2026.09.13 - 16.54.07.12,02:25:190
shot-08.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_03.05.000.jpg,2026.09.13 - 16.54.07.12,03:05:000
shot-09.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_05.39.999.jpg,2026.09.13 - 16.54.07.12,05:39:999
shot-10.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_05.55.651.jpg,2026.09.13 - 16.54.07.12,05:55:651
shot-11.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_06.25.650.jpg,2026.09.13 - 16.54.07.12,06:25:650
shot-12.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_09.33.997.jpg,2026.09.13 - 16.54.07.12,09:33:997
shot-13.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_12.09.456.jpg,2026.09.13 - 16.54.07.12,12:09:456
shot-14.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_12.59.497.jpg,2026.09.13 - 16.54.07.12,12:59:497
shot-15.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_13.09.496.jpg,2026.09.13 - 16.54.07.12,13:09:496
shot-16.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_15.22.656.jpg,2026.09.13 - 16.54.07.12,15:22:656
shot-17.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_15.39.495.jpg,2026.09.13 - 16.54.07.12,15:39:495
shot-18.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_16.54.496.jpg,2026.09.13 - 16.54.07.12,16:54:496
shot-19.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_17.29.495.jpg,2026.09.13 - 16.54.07.12,17:29:495
shot-20.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_17.39.495.jpg,2026.09.13 - 16.54.07.12,17:39:495
shot-21.jpg,Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_00.21.484.jpg,2026.09.13 - 17.49.35.18,00:21:484
shot-22.jpg,Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_00.31.328.jpg,2026.09.13 - 17.49.35.18,00:31:328
shot-23.jpg,Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_02.03.469.jpg,2026.09.13 - 17.49.35.18,02:03:469
shot-24.jpg,Build-pc 2026.09.13 - 17.54.08.19.mp4_snapshot_00.11.972.jpg,2026.09.13 - 17.54.08.19,00:11:972
shot-25.jpg,Build-pc 2026.09.13 - 17.54.08.19.mp4_snapshot_02.36.532.jpg,2026.09.13 - 17.54.08.19,02:36:532
shot-26.jpg,settings menu.jpg,settings-menu,
1 new_name original video_session snapshot_time
2 shot-01.jpg Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_02.11.149.jpg 2026.09.13 - 16.05.44.10 02:11:149
3 shot-02.jpg Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_06.00.446.jpg 2026.09.13 - 16.05.44.10 06:00:446
4 shot-03.jpg Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_07.56.664.jpg 2026.09.13 - 16.05.44.10 07:56:664
5 shot-04.jpg Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_08.33.264.jpg 2026.09.13 - 16.05.44.10 08:33:264
6 shot-05.jpg Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_10.16.041.jpg 2026.09.13 - 16.05.44.10 10:16:041
7 shot-06.jpg Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_11.03.430.jpg 2026.09.13 - 16.05.44.10 11:03:430
8 shot-07.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_02.25.190.jpg 2026.09.13 - 16.54.07.12 02:25:190
9 shot-08.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_03.05.000.jpg 2026.09.13 - 16.54.07.12 03:05:000
10 shot-09.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_05.39.999.jpg 2026.09.13 - 16.54.07.12 05:39:999
11 shot-10.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_05.55.651.jpg 2026.09.13 - 16.54.07.12 05:55:651
12 shot-11.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_06.25.650.jpg 2026.09.13 - 16.54.07.12 06:25:650
13 shot-12.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_09.33.997.jpg 2026.09.13 - 16.54.07.12 09:33:997
14 shot-13.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_12.09.456.jpg 2026.09.13 - 16.54.07.12 12:09:456
15 shot-14.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_12.59.497.jpg 2026.09.13 - 16.54.07.12 12:59:497
16 shot-15.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_13.09.496.jpg 2026.09.13 - 16.54.07.12 13:09:496
17 shot-16.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_15.22.656.jpg 2026.09.13 - 16.54.07.12 15:22:656
18 shot-17.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_15.39.495.jpg 2026.09.13 - 16.54.07.12 15:39:495
19 shot-18.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_16.54.496.jpg 2026.09.13 - 16.54.07.12 16:54:496
20 shot-19.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_17.29.495.jpg 2026.09.13 - 16.54.07.12 17:29:495
21 shot-20.jpg Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_17.39.495.jpg 2026.09.13 - 16.54.07.12 17:39:495
22 shot-21.jpg Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_00.21.484.jpg 2026.09.13 - 17.49.35.18 00:21:484
23 shot-22.jpg Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_00.31.328.jpg 2026.09.13 - 17.49.35.18 00:31:328
24 shot-23.jpg Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_02.03.469.jpg 2026.09.13 - 17.49.35.18 02:03:469
25 shot-24.jpg Build-pc 2026.09.13 - 17.54.08.19.mp4_snapshot_00.11.972.jpg 2026.09.13 - 17.54.08.19 00:11:972
26 shot-25.jpg Build-pc 2026.09.13 - 17.54.08.19.mp4_snapshot_02.36.532.jpg 2026.09.13 - 17.54.08.19 02:36:532
27 shot-26.jpg settings menu.jpg settings-menu
Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

+23 -224
View File
@@ -1,235 +1,34 @@
---
title: GoldenEye 007 PC Port
# v0.3.0: status bumped from v0.2.1; Honest-status bullets synced with the
# README's v0.3.0 known issues (D255/D282/F10-dup/overscan items removed as
# fixed). Description kept at 133 chars (Bing's 160 limit).
description: >-
A native PC port of the 1997 N64 classic, built from decompiled source with
a software RSP. v0.3.0 for Windows, Linux and Steam Deck.
description: A native PC port of the original Nintendo 64 GoldenEye 007, built from decompiled source — and a case study in AI-agent collaboration on a large low-level codebase.
---
<!-- v0.2.1 (Bing SEO): the "# GoldenEye 007 PC Port" h1 was removed here --
the Cayman masthead already renders the site title as an <h1>, so the page
was emitting two h1 tags. -->
# GoldenEye 007 PC Port
A native PC port of the original 1997 Nintendo 64 _GoldenEye 007_, compiled
from the game's [decompiled source](https://github.com/n64decomp/007) with the
N64's graphics coprocessor (RSP) running in software, the same architecture
as the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark), the
same Rare "Indy" engine family, one hardware generation apart.
A work-in-progress **native PC port of the original 1997 Nintendo 64
_GoldenEye 007_**, compiled from the game's
[decompiled source](https://github.com/n64decomp/007) and running the N64's
graphics coprocessor in software. It follows the architecture of the
[Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark).
**Status: v0.3.0.** The full single-player campaign runs at a steady 60 fps
and is completable end to end (all 21 missions playtested); all 21 missions
load and run clean on Windows, Linux and Steam Deck, and audio (music + SFX)
plays throughout. The known rough edges — mostly cosmetic rendering defects,
stretched-not-native widescreen, and missing features — are listed plainly
under [Honest status](#honest-status).
It is also a **research project on AI-agent collaboration in a large,
unfamiliar, low-level codebase**: two coding agents — a local open-weight model
(Qwen 3.8) on a single RTX 5090, and Claude / Claude Code — driven by one person
part-time through ~230 translation units of unmodified big-endian MIPS game
code, handing work back and forth through shared written notes.
**This is a pre-1.0 release, not a finished product** — v1.0 is the target
for a polished, feature-complete build; expect rough edges and missing
features until then. See the
[README's Roadmap section](https://github.com/jkdansereau/goldeneye-pc-port#roadmap)
for direction (PAL/JP support, real widescreen, LAN multiplayer under
consideration, and more).
**Status:** Phase 2 of 4 (rendering); first alpha **v0.1.0** released. Runs the
full single-player campaign — 19 of 21 missions completable start to finish in
a full-campaign playtest, two levels still crash mid-mission. No audio yet; AI pacing
and cutscenes are rough.
<p align="center">
<img src="media/goldeneye-gh-preview.gif" width="70%"
alt="~12 s gameplay montage from live play sessions">
<br><em>~12 s gameplay montage from live v0.3.0 play sessions, running in the port.</em>
</p>
## Download
| Platform | Bundle | Notes |
|---|---|---|
| **Windows** (x86_64) | [win64.zip](https://github.com/jkdansereau/goldeneye-pc-port/releases) | Engine + runtime DLLs + the one-time asset tool. |
| **Linux** (x86_64) / **Steam Deck** | [linux tarball](https://github.com/jkdansereau/goldeneye-pc-port/releases) | SDL2 is bundled, so it runs as-is on any distro, and sideloads onto a Deck with nothing installed. |
**Bring your own ROM.** Both bundles contain no ROM and no game assets: you
supply your own GoldenEye 007 N64 ROM (the
[Requirements table](https://github.com/jkdansereau/goldeneye-pc-port#requirements)
has the region filenames and SHA-1s). This release supports the NTSC-U (US)
ROM; PAL and JP are on the roadmap. Then: unpack, drop the ROM in `data/`,
and launch; the first run generates the derived assets automatically (no
Python or other tooling needed). The full steps are in the
[Quick start](https://github.com/jkdansereau/goldeneye-pc-port#quick-start);
pre-built releases are legal to distribute precisely because they're useless
without a ROM you already own.
## See it running
<p align="center">
<img src="img/shots/shot-06.jpg" width="45%" alt="Bunker 1, rendered by the port (v0.2.0 playtest)">
<img src="img/shots/shot-01.jpg" width="45%" alt="Dam, rendered by the port (v0.2.0 playtest)">
</p>
<details>
<summary><strong>Full playtest gallery</strong>: 19 in-engine captures from the v0.2.0 Windows playtest, across the campaign</summary>
<p align="center">
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-01.jpg" width="100%" alt="In-engine: Dam (v0.2.0)">
<div><small>Dam</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-02.jpg" width="100%" alt="In-engine: Facility (v0.2.0)">
<div><small>Facility</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-03.jpg" width="100%" alt="In-engine: Runway (v0.2.0)">
<div><small>Runway</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-04.jpg" width="100%" alt="In-engine: Surface (v0.2.0)">
<div><small>Surface</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-05.jpg" width="100%" alt="In-engine: Surface (v0.2.0)">
<div><small>Surface</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-06.jpg" width="100%" alt="In-engine: Bunker 1 (v0.2.0)">
<div><small>Bunker 1</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-07.jpg" width="100%" alt="In-engine: Silo (v0.2.0)">
<div><small>Silo</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-09.jpg" width="100%" alt="In-engine: Frigate (v0.2.0)">
<div><small>Frigate</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-10.jpg" width="100%" alt="In-engine: Frigate (v0.2.0)">
<div><small>Frigate</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-11.jpg" width="100%" alt="In-engine: Surface 2 (v0.2.0)">
<div><small>Surface 2</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-13.jpg" width="100%" alt="In-engine: Bunker 2 (v0.2.0)">
<div><small>Bunker 2</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-16.jpg" width="100%" alt="In-engine: Statue (v0.2.0)">
<div><small>Statue</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-19.jpg" width="100%" alt="In-engine: Statue (v0.2.0)">
<div><small>Statue</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-20.jpg" width="100%" alt="In-engine: Archives (v0.2.0)">
<div><small>Archives</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-21.jpg" width="100%" alt="In-engine: Cradle (v0.2.0)">
<div><small>Cradle</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-23.jpg" width="100%" alt="In-engine: Cradle (v0.2.0)">
<div><small>Cradle</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-24.jpg" width="100%" alt="In-engine: Aztec (v0.2.0)">
<div><small>Aztec</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-25.jpg" width="100%" alt="In-engine: Aztec (v0.2.0)">
<div><small>Aztec</small></div>
</div>
<div style="width:32%;text-align:center;margin:4px">
<img src="img/shots/shot-26.jpg" width="100%" alt="In-engine: Frigate (v0.2.0)">
<div><small>Frigate</small></div>
</div>
</p>
</details>
More captures may land here as playtesting continues.
## Play it, or take it apart
- **Try it yourself**: grab a bundle above, bring your own ROM, and play the
campaign. It's an early cut for exactly this: if something breaks, an
[issue](https://github.com/jkdansereau/goldeneye-pc-port/issues) with what
you were doing is genuinely useful.
- **Read the code**: the game logic in `src/` is unmodified decompilation;
every hardware surface (video, audio, input, save storage, and the
software-RSP renderer) is shimmed in the MIT-licensed `port/` layer.
[Internals](internals.md) is the map; [Porting notes](porting-notes.md) is
the catalogue of N64→PC bug classes hit along the way, a good read even if
you never touch the code.
- **Mod it**: the port layer, build system and `tools_pc/` helpers are MIT
licensed and yours to extend: new video options, input tweaks, your own
asset sidecar. [CONTRIBUTING](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/CONTRIBUTING.md)
has the ground rules that keep it faithful to the original game.
- **AI disclosure**: development here was agentic — Claude Pro plus a local
open-weight model on a single RTX 5090, as of August–September 2026. This
project is as much a study of *that process* as it is a port: whether
current LLMs can carry a codebase like this, and what actually goes wrong
along the way. Game codebases have historically been a rough fit for
LLMs — large, stateful, hardware-adjacent, unforgiving of a subtly wrong
memory layout — more so for older local models, so this is a useful data
point on where that stands now, for better or worse. Judge the result for
yourself. I'm one person doing this in my spare time, not a team. See
[the full write-up](dev/agentic-development.md) for the setup, timeline,
handoff workflow, and an honest assessment of what did and didn't work.
## How it works
The R4300 game code is compiled completely unmodified; the decompilation's
control flow is ground truth. The N64's Reality Signal Processor, the graphics
coprocessor that builds and executes each frame's display list, is emulated in
software: the port interprets the GBI stream the game emits and translates it
to OpenGL, bypassing the RDP entirely. Everything else that would touch N64
hardware (video, audio, input, timers, save storage) is shimmed in a small
dedicated layer, following the architecture of the Perfect Dark PC port from
the same Rare engine family. Full detail: [Internals](internals.md); the bug
catalogue: [Porting notes](porting-notes.md).
## Honest status
- **Particle colours** can still drift through a rainbow palette
intermittently: one genuine cause is fixed this release, a second hasn't
been found yet (D252).
- **Some muzzle flashes draw an extra erroneous long flash** straight up from
the gun, on top of the correct one. Cosmetic only (D303).
- Water levels show a moving seam between two water patterns (D245);
occasional z-fighting on some geometry (D308); in-level security-camera
props can occasionally face backwards (D307). Minor and cosmetic.
- **The front-end Nintendo logo renders as two white blobs** and the Rareware
logo's texture filtering looks off (D75).
- Surface 1's 2D billboard trees render as a solid wall of tree texture
instead of discrete sprites; under active investigation (D236).
- **Widescreen is stretched, not native**: 16:9 stretches the 4:3 frame to
fill the display; automatic FOV scaling keeps the framing comfortable and
gameplay is unaffected, but a distortion-free native widescreen render is
still on the roadmap. Distant geometry can also drop out on the biggest
open levels (D249).
- **Gunshot SFX can sound off during sustained/rapid fire** (D240/D241).
- **`All unlocked` is highly experimental** — don't enable it until you have
at least one save written, or it can break audio and mouse aim
(D257/D259/D281).
- Bond's cutscene positioning is fixed and right the large majority of the
time now; on the rare occasion he's off it's a small drift — no more
floating or spin-glitching (D173/D292/D243).
- No macOS/ARM support; no controller rebinding UI yet.
The full list, with root causes and fix status: the
[README's Status section](https://github.com/jkdansereau/goldeneye-pc-port#status)
and the [finding log](https://github.com/jkdansereau/goldeneye-pc-port/tree/main/docs/dev).
- **[Project repository and README](https://github.com/jkdansereau/goldeneye-pc-port)** — build instructions, requirements, status, how it compares to the Xbox 360 recompilation projects.
## Documentation
- [The two-agent development case study](dev/agentic-development.md): goal, setup, timeline, the handoff workflow, and an honest assessment of what did and didn't work.
- [Development process](dev-process.md): how work was scoped, partitioned, and budgeted across agents; the finding-log discipline.
- [Internals](internals.md): architecture, the software RSP-emulation approach, GoldenEye-vs-Perfect-Dark engine differences, the phased plan.
- [Porting notes](porting-notes.md): the recurring Nintendo 64 → PC bug classes hit during the port, with fixes.
- [Building](building.md): full build and asset-extraction guide.
---
<small>Non-commercial fan preservation/research project. No ROM or game assets
are distributed; you supply a ROM you already own. Not affiliated with or
endorsed by Nintendo, Rare, Microsoft, MGM, Danjaq, or EON Productions.</small>
- **[The two-agent development case study](dev/agentic-development.md)** — goal, setup, timeline, the handoff workflow, and an honest assessment of what did and didn't work.
- **[Development process](dev-process.md)** — how work was scoped, partitioned, and budgeted across agents; the finding-log discipline.
- **[Internals](internals.md)** — architecture, the software RSP-emulation approach, GoldenEye-vs-Perfect-Dark engine differences, the phased plan.
- **[Porting notes](porting-notes.md)** — the recurring Nintendo 64 → PC bug classes hit during the port, with fixes.
- **[Game-behavior reference](dev/game-behavior-reference.md)** — how the retail N64 game is meant to behave: combat/AI model, difficulty scaling, per-level objectives, timers, weapon data, and the original game's known quirks.
- **[Building](building.md)** — full build and asset-extraction guide.
+58 -64
View File
@@ -1,18 +1,12 @@
---
title: Internals
description: Architecture behind the PC port; the software RSP-emulation approach, GoldenEye-vs-Perfect-Dark engine differences, and the phased development plan.
---
## GoldenEye 007 PC Port: architecture & plan
# GoldenEye 007 — PC Port: architecture & plan
> This began as the pre-implementation research note and is kept as the
> architecture reference. Sections 1–9 describe the design; section 10 lists
> external references. The phased plan in section 8 is largely done through
> Phase 2; for current status see the
> Phase 2 — for current status see the
> [README](https://github.com/jkdansereau/goldeneye-pc-port#status) and
> [`dev/LEVEL-STATUS.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/LEVEL-STATUS.md);
> for the blow-by-blow finding log see
> [`dev/findings.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/findings.md).
> [`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
@@ -28,18 +22,18 @@ platforms.
1. [Executive summary](#1-executive-summary)
2. [What the PD port actually does (the reference architecture)](#2-what-the-pd-port-actually-does-the-reference-architecture)
· [2.1 Emulate the RSP, bypass the RDP](#21-the-key-architectural-insight-emulate-the-rsp-bypass-the-rdp)
— [2.1 Emulate the RSP, bypass the RDP](#21-the-key-architectural-insight-emulate-the-rsp-bypass-the-rdp)
· [2.2 How the RSP is invoked](#22-how-the-rsp-is-invoked)
· [2.3 Build system](#23-build-system)
· [2.4 PD-port copy-candidate audit](#24-pd-port-copy-candidate-audit-session-2026-08-22)
3. [GoldenEye decomp: what we're porting](#3-goldeneye-decomp-what-were-porting)
· [3.1 Entry / main loop](#31-entry--main-loop)
— [3.1 Entry / main loop](#31-entry--main-loop)
· [3.2 The two libultra trees](#32-the-two-libultra-trees-srclibultra-and-srclibultrare)
4. [N64 hardware surfaces that must be shimmed](#4-n64-hardware-surfaces-that-must-be-shimmed)
5. [GE-specific graphics differences vs PD (the RSP work)](#5-ge-specific-graphics-differences-vs-pd-the-rsp-work)
6. [Audio](#6-audio)
7. [Input & saves](#7-input--saves)
8. [Recommended plan](#8-recommended-plan); phases 0–4
8. [Recommended plan](#8-recommended-plan) — phases 0–4
9. [Open questions / risks](#9-open-questions--risks)
10. [References](#10-references)
@@ -70,7 +64,7 @@ platforms.
* **Saves** are **EEPROM-based** (controller EEPROM via `osEeprom*`), not
Memory Pak. The PFS/motor code in `joy.c` is only for accessory (Rumble Pak
/ Memory Pak) detection and can be stubbed. Phase 4 is therefore a
file-backed EEPROM plus no-op PFS/motor shims; simpler than a full Memory
file-backed EEPROM plus no-op PFS/motor shims — simpler than a full Memory
Pak emulation.
---
@@ -127,7 +121,7 @@ R4300 builds a Gfx display list
The PD port **emulates the RSP in software** (`fast3d/gfx_pc.cpp`) and
**bypasses the RDP entirely**: the software RSP translates GBI commands
directly into OpenGL calls. The R4300 game code is run natively and is
unaware of the difference; it just builds display lists and "starts the
unaware of the difference — it just builds display lists and "starts the
RSP" as usual.
The frame flow (from `port/src/pdsched.c` + `port/src/video.c`):
@@ -162,7 +156,7 @@ The PC build is **CMake** (the N64 build stays Make + IDO). Dependencies:
* `src/game/*.c` (all of it)
* a curated list of `src/lib/*.c` (the engine helpers that are
platform-independent: mema, memp, model, music, snd, vi, rdp, etc.)
* `src/lib/ultra/audio/*.c` (libaudio; runs fine on the CPU)
* `src/lib/ultra/audio/*.c` (libaudio — runs fine on the CPU)
* `src/lib/ultra/gu/*.c` (gu matrix helpers)
* `port/src/*.c` + `port/src/*.cpp` + `port/fast3d/*.cpp`
@@ -176,38 +170,38 @@ for this project (see CONTRIBUTING.md): same Rare "Indy"/Bond engine family, and
port layer solves the same problem classes we are currently hitting. Audited
against our remaining work items:
**Direct copy candidates (Phase 3/4; our stubs were scaffolded to be replaced
**Direct copy candidates (Phase 3/4 — our stubs were scaffolded to be replaced
by these):**
| PD file | Lines | Replaces our | Notes |
|---|---|---|---|
| `port/src/mixer.c` | 722 | 31-line stub | libaudio→SDL final mixer; same RareAL family. Adapt to GE's `audi.c` wiring (sample rate/buffering) |
| `port/src/input.c` | 1551 | 48-line stub | SDL_GameController backend: hotplug, rumble, keyboard fallback. Remap the button table to GE's scheme (C-buttons/Z-trig differ from PD) |
| `port/src/fs.c` | 294 | 81 lines | File-backed save dir (`--savedir`, `$S` path expansion); foundation for Phase 4 EEPROM/PFS file backing (ours are no-op stubs today) |
| `port/src/fs.c` | 294 | 81 lines | File-backed save dir (`--savedir`, `$S` path expansion) — foundation for Phase 4 EEPROM/PFS file backing (ours are no-op stubs today) |
| `port/src/audio.c` | 75 | 70 lines | Near-identical; diff when Phase 3 starts |
**Reference implementations (Phase 2; the D32/D37/D43 class).** PD's port
**Reference implementations (Phase 2 — the D32/D37/D43 class).** PD's port
layer has a `port/src/preprocess/` module (~4,125 lines): a per-ROM-segment
`preprocessfunc` table in their `romdata.c` that converts N64-layout asset data
to PC-native layout *at load time*; a systematic solution to the problem class
to PC-native layout *at load time* — a systematic solution to the problem class
we have been fixing ad-hoc per asset type.
- `filemodel.c` (1,114); the direct **D43** analogue: same `PROMOTE` idiom,
- `filemodel.c` (1,114) — the direct **D43** analogue: same `PROMOTE` idiom,
**same vma 0x5000000**. NOT a drop-in copy (GE's `ModelRoData` union + opcode
set differ; PD has stargunfire/headspot/gundl node types), but the node-walk
set differ — PD has stargunfire/headspot/gundl node types), but the node-walk
order, placement/alignment choices, and record handling are Rare-validated
against near-identical data: adapt rather than design from scratch.
- `filebg.c` (524); BG tables (an expected D43 follow-on fault).
- `segaudio.c` (362); audio bank-tree conversion; cross-check for our D37
- `filebg.c` (524) — BG tables (an expected D43 follow-on fault).
- `segaudio.c` (362) — audio bank-tree conversion; cross-check for our D37
`romdataFixupAudioBank`.
- `filelang.c`, `filetiles.c`, `filepads.c`, `filesetup.c`, `gbi.c`,
`segfonts.c`; the remaining asset types we will hit.
`segfonts.c` — the remaining asset types we will hit.
**Not copyable:** GE-specific `ModelRoData` record map; fast3d CC/RM
correctness (GE's custom GBI modes vs `gmain.s` do not exist in PD); PD extras
(`config.c`, `optionsmenu.c`, `mpsetups.c`, `mod.c`); options menu, MP setup,
(`config.c`, `optionsmenu.c`, `mpsetups.c`, `mod.c`) — options menu, MP setup,
MOD player; not part of 1:1 fidelity.
**Caveats:** copy **port-layer files only**; their decomp was modified for PC
**Caveats:** copy **port-layer files only** — their decomp was modified for PC
(e.g. `types.h` keeps N64 offset comments while using real pointers); our
non-negotiables are unchanged. Same family ≠ identical format: validate every
copied conversion per-field against GE headers + ROM ground truth (the standing
@@ -221,19 +215,19 @@ D32 procedure).
US/EU/JP ROMs. Game code lives in `src/game/` (~242 files) plus engine
files in `src/`.
* **Engine layout** (same family as PD):
* `src/sched.c`; RCP scheduler (`osCreateScheduler`, `__scExec`,
* `src/sched.c` — RCP scheduler (`osCreateScheduler`, `__scExec`,
`__scYield`). Structurally near-identical to PD's `src/lib/sched.c`.
* `src/game/rsp.c`; builds the Gfx display list, wraps it in an `OSScTask`,
* `src/game/rsp.c` — builds the Gfx display list, wraps it in an `OSScTask`,
and starts it via the scheduler (`rspGfxTaskStart`).
* `src/fr.c`; frame/VI management, video modes (`osViSetMode`).
* `src/audi.c`, `src/snd.c`, `src/music.c`; libaudio-based sound + music.
* `src/joy.c`; controllers, **EEPROM** save handling (via `joyGamePak*`
* `src/fr.c` — frame/VI management, video modes (`osViSetMode`).
* `src/audi.c`, `src/snd.c`, `src/music.c` — libaudio-based sound + music.
* `src/joy.c` — controllers, **EEPROM** save handling (via `joyGamePak*`
-> `osEeprom*`), plus PFS/motor code for accessory (Rumble Pak / Memory
Pak) detection only.
* `src/init.c`; `init()` -> `mainproc()` -> `bossEntry()` (game entry).
* `src/init.c` — `init()` -> `mainproc()` -> `bossEntry()` (game entry).
**Compiled for the PC** (provides `mainproc()`); its N64-only `init()` is
stubbed (see A3).
* **Custom RSP microcode:** `rsp/graphics/gmain.s` (1545 lines); a modified
* **Custom RSP microcode:** `rsp/graphics/gmain.s` (1545 lines) — a modified
fast3d. This is the RSP-side code that consumes the display list. For the PC
port we do **not** run this; we replace it with the software RSP. But it is
the authoritative reference for which GBI commands GE actually emits.
@@ -265,23 +259,23 @@ overlap.
What the PC build must do with each:
* **`src/libultra/audio/*.c`**; standard libaudio (AL). Runs on the CPU;
* **`src/libultra/audio/*.c`** — standard libaudio (AL). Runs on the CPU;
**compile** (the game's `audi.c`/`snd.c`/`music.c` depend on it).
* **`src/libultrare/audio/*.c`** (`drvrNew.c`, `env.c`, `reverb.c`); Rare's
* **`src/libultrare/audio/*.c`** (`drvrNew.c`, `env.c`, `reverb.c`) — Rare's
audio "New" driver. `synthesizer.c` calls `alSaveNew`/`alAuxBusNew`/
`alMainBusNew`/`alLoadNew`/`alResampleNew`/`alEnvmixerNew`/`alEnvmixerParam`/
`alFxParam`, all defined here. Runs on the CPU; **compile**.
* **`src/libultra/gu/*.c`**; matrix helpers (`guMtxF`, `guOrtho`, ...).
* **`src/libultra/gu/*.c`** — matrix helpers (`guMtxF`, `guOrtho`, ...).
**Compile**.
* **`src/libultrare/io/vitbl.c`**; defines `osViModeTable` (a data table),
* **`src/libultrare/io/vitbl.c`** — defines `osViModeTable` (a data table),
referenced ~15× by `fr.c`. **Compile** (it's data, not a driver). This is the
*only* `io/` file we compile.
* **All other `src/libultra/io/*.c` and `src/libultrare/io/*.c`**; hardware
* **All other `src/libultra/io/*.c` and `src/libultrare/io/*.c`** — hardware
drivers (PI, SI, SP, DP, VI, controller, PFS, motor, EEPROM, ...). **Exclude
and shim** in `port/src/libultra.c`. Notably `pfsinit.c`/`pfsisplug.c`
(`osPfsInit`/`osPfsIsPlug`) and `motor.c` (`osMotor*`) are called by
`joy.c` only for accessory detection, so they are shimmed as no-ops (see §7).
* **`src/libultra/os/*.c` + `src/libultrare/os/*.c`**; OS core (threads,
* **`src/libultra/os/*.c` + `src/libultrare/os/*.c`** — OS core (threads,
message queues, timers, cache, TLB). **Exclude and shim** in
`port/src/libultra.c` (+ `n64stubs.c` for the boot/TLB/FPU symbols).
@@ -296,7 +290,7 @@ What the PC build must do with each:
| Subsystem | GE API used | Port strategy (from PD) |
|-----------|-------------|--------------------------|
| RSP (coproc 0) | `osSpTaskLoad/StartGo/Yield`, display lists | Software RSP (`fast3d`); **main work** |
| RSP (coproc 0) | `osSpTaskLoad/StartGo/Yield`, display lists | Software RSP (`fast3d`) — **main work** |
| RDP (coproc 1) | `osDpSetStatus`, `osDpSetNextBuffer`, render modes | Bypassed; OpenGL backend |
| VI (video) | `osViSetMode`, `osViVSyncCallback`, `osViWaitVSync` | SDL2 window + GL context + frame pacing |
| AI (audio) | `osAiSetFrequency/SetNextBuffer/GetLength` | SDL audio device + queue |
@@ -312,32 +306,32 @@ What the PC build must do with each:
GE's `include/gbi_extension.h` defines the custom GBI surface. Compared to the
PD port's `fast3d`, the deltas are:
1. **`G_TRI4`**; draws up to 4 triangles in one command using 4-bit vertex
1. **`G_TRI4`** — draws up to 4 triangles in one command using 4-bit vertex
indices (0–15). A memory-saving optimization (Rare packed 4 tris into the
slot normally used for `G_TRI2`).
* **Status: already supported** by the PD `fast3d` (`gfx_sp_tri4`, and the
PD game itself uses it). ✅
2. **`G_SETTEX`** (`0xc0`); "use texture from bank": selects a texture by
2. **`G_SETTEX`** (`0xc0`) — "use texture from bank": selects a texture by
bank/tile with detail/mipmap type, min-level, detail-id and texture-id.
Emitted only by `gsSPUseTexture` (`include/gbi_extension.h:193`).
* **Status: appears UNUSED.** `gsSPUseTexture` is defined but never called
anywhere in the game code or the RSP ucode (`rsp/graphics/gmain.s`), and
`G_SETTEX` appears nowhere outside its `#define`. The game uses the
standard texture commands (`gSPTexture`, `gSPTextureL`,
`gSPTextureRectangle`, `gSPTextureRectangleFlip`); all already handled
`gSPTextureRectangle`, `gSPTextureRectangleFlip`) — all already handled
by the PD `fast3d`. **If this holds (verify the decomp is complete),
Phase 2 shrinks to verifying the custom CC/RM modes and the PD fast3d
may work largely as-is.** Keep a `G_SETTEX` decode path as a safety net,
but it is no longer the headline task.
3. **Custom color-combiner modes**; `G_CC_MODULATEIFADE`, `G_CC_FADE`,
3. **Custom color-combiner modes** — `G_CC_MODULATEIFADE`, `G_CC_FADE`,
`G_CC_DECALFADE`, `G_CC_BLENDRGBFADEA`, etc. (fade/blend variants).
* **Status: handled generically** by the fast3d color-combiner -> GLSL
shader compiler (`gfx_cc.cpp`), which compiles arbitrary CC equations.
Needs verification that each GE mode produces correct GLSL.
4. **Custom render mode**; `G_RM_CUSTOM_AA_ZB_XLU_SURF` (AA_ZB_XLU with
4. **Custom render mode** — `G_RM_CUSTOM_AA_ZB_XLU_SURF` (AA_ZB_XLU with
`Z_UPD`).
* **Status: handled** by the render-mode -> GL state mapping; verify.
5. **`gDPLoadTLUT06/07`**; custom TLUT load variants.
5. **`gDPLoadTLUT06/07`** — custom TLUT load variants.
* **Status: minor**, map onto the existing `G_LOADTLUT` path.
**Conclusion:** the PD `fast3d` is a strong starting point. `G_SETTEX` appears
@@ -347,7 +341,7 @@ safety net. Everything else (matrices, lights, fog, tri1/tri4, texture load,
sync/flush) is shared.
> Note: GE's on-hardware RSP ucode is `rsp/graphics/gmain.s`. We do not run it
> on PC, but it is the ground truth for the exact command encodings GE emits;
> on PC, but it is the ground truth for the exact command encodings GE emits —
> use it to validate the software RSP's command decoding.
---
@@ -355,7 +349,7 @@ sync/flush) is shared.
## 6. Audio
* GE uses **libaudio (AL)** on the R4300 (`src/audi.c`, `src/snd.c`,
`src/music.c`); identical architecture to PD. Sound synthesis (ADPCM
`src/music.c`) — identical architecture to PD. Sound synthesis (ADPCM
decode, mixing, FX) runs on the CPU; the RSP audio ucode is a thin
pass-through to the AI DMA.
* The PD port runs libaudio natively and routes the mixed output through
@@ -370,7 +364,7 @@ sync/flush) is shared.
* **Controllers:** `src/joy.c` uses `osContInit` / `osContStartReadData`.
The port maps SDL2 keyboard/mouse/gamepad into the N64 `OSError`/controller
structs. PD implements 1964GEPD-style and Xbox-style bindings; a good
structs. PD implements 1964GEPD-style and Xbox-style bindings — a good
default for GE too (fire=Z, aim=R, etc.).
* **Saves:** GE saves are **EEPROM-based**, not Memory Pak. The actual save
I/O (`src/game/file2.c`) uses `joyGamePakProbe/LongRead/LongWrite`, which
@@ -386,35 +380,35 @@ sync/flush) is shared.
## 8. Recommended plan
### Phase 0: Scaffolding (this change)
### Phase 0 — Scaffolding (this change)
* Add `port/` directory structure + headers + stub sources.
* Add a top-level `CMakeLists.txt` (+ `cmake/` modules) for the PC build,
parallel to the N64 `Makefile`.
* This document + `port/README.md`.
### Phase 1: Boot to a window
### Phase 1 — Boot to a window
* Port `system.c`, `fs.c`, `romdata.c`, `config.c`, `main.c`.
* Port `video.c` + a minimal `fast3d` (clear color, present).
* Goal: load `ge007.u.z64`, open a window, draw a clear color.
### Phase 1.5: Boot to first frame (ABI reconciliation)
### Phase 1.5 — Boot to first frame (ABI reconciliation)
* Get mainThread through all of `bossInitMainthreadData()` → `bossEntry()` →
`bossMainloop()` and render frame 1. (Done; D34–D58; see §G.)
* Recurring mechanism: **ROM-data struct ABI/layout fixes** (the D32 pattern);
`bossMainloop()` and render frame 1. (Done — D34–D58; see §G.)
* Recurring mechanism: **ROM-data struct ABI/layout fixes** (the D32 pattern) —
a 32-bit-pointer struct read as a 64-bit-pointer struct misaligns; convert
embedded pointer fields to `u32` + cast at use sites (PD ground truth), then
verify the load-time rebase yields valid DRAM addresses. See §H for the
step-by-step procedure and the standing "non-negotiable #2 ABI exception."
* Goal: title screen / first level actually drawn on the GL surface.
### Phase 2: RSP / rendering
### Phase 2 — RSP / rendering
* Bring in the PD `fast3d` (gfx_pc / gfx_opengl / gfx_cc / gfx_sdl2).
* Verify the custom CC/RM modes against `gmain.s`; add a `G_SETTEX` decode
path only if it turns out to be used (see §5; it appears unused).
path only if it turns out to be used (see §5 — it appears unused).
* Port `pdsched.c` (GE variant) to drive the software RSP.
* Goal: render the title screen / first level.
### Phase 3: Audio + input
### Phase 3 — Audio + input
* Port `audio.c`, `mixer.c`, `mod.c`; wire libaudio output to SDL.
* **Decide the ASP strategy** (see §11 C2): if audio runs the `aspMain`
microcode, wire `aspMainTextStart`/`aspMainDataStart` to the real microcode
@@ -422,7 +416,7 @@ sync/flush) is shared.
* Port `input.c` (keyboard/mouse/gamepad).
* Goal: playable with sound.
### Phase 4: Saves + polish
### Phase 4 — Saves + polish
* File-backed EEPROM (saves); PFS/motor already stubbed as no-ops.
* Widescreen, mouse-look, FPS options, config file, crash handler.
* Goal: feature-parity with the original + QoL extras.
@@ -431,21 +425,21 @@ sync/flush) is shared.
## 9. Open questions / risks
1. **`G_SETTEX` (low priority)**; appears unused (`gsSPUseTexture` is never
1. **`G_SETTEX` (low priority)** — appears unused (`gsSPUseTexture` is never
called). If a future decomp revision or a missed call site shows it is used,
reverse the texture-bank selection (bank -> image, tile, detail/mipmap type)
from `gmain.s` + `tex.c` / `initmttex.c`. Not currently the main unknown.
2. **Custom CC correctness**; the GLSL compiler must handle every fade/blend
2. **Custom CC correctness** — the GLSL compiler must handle every fade/blend
mode GE uses; validate per-mode against the original. This is now the main
rendering unknown.
3. **Scheduler fidelity**; GE's `sched.c` has speed-graph / profiling hooks
3. **Scheduler fidelity** — GE's `sched.c` has speed-graph / profiling hooks
(`speedgraphMarkerHandler`) and a slightly different task model than PD;
the port scheduler must reproduce the gfx/audio interleaving and VSync
pacing.
4. **Decomp completeness**; the decomp is WIP; some functions may still be
4. **Decomp completeness** — the decomp is WIP; some functions may still be
stubs/`unk_`. The PC port inherits whatever state the decomp is in. Track
the upstream `n64decomp/007` and rebase.
5. **Endianness / alignment**; GE, like PD, is big-endian N64 code with
5. **Endianness / alignment** — GE, like PD, is big-endian N64 code with
unaligned accesses; the PD port handles this with careful casts and
`-fno-strict-aliasing`. Reuse those flags.
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 MiB

+120 -856
View File
File diff suppressed because it is too large. Load diff
-102
View File
@@ -1,102 +0,0 @@
# Security & fidelity status
This page answers two questions plainly: *what does installing this put on
your machine*, and *how faithfully does the port actually reproduce the
original N64 game's logic*. Both were reviewed end-to-end on 2026-09-15 and
re-verified for the v0.3.0 release bundles on 2026-09-18 (bundle contents,
no-networking source check, and the asset converter). The full engineering
record behind each item is in the project's finding log
([`docs/dev/findings.md`](dev/findings.md)).
## What a release actually installs
Each release (`goldeneye-pc-port-*-win64.zip` / `-linux-x86_64.tar.gz`)
contains, and only contains: the game engine executable, a fixed set of
standard runtime libraries (SDL2, zlib, and the MinGW/Linux runtime — no
third-party downloads), the one-time ROM-to-PC asset converter, a README,
and license texts. The build scripts hard-fail if a ROM image or an
oversized blob ends up in the bundle. **No ROM, no game assets (textures,
audio, models, levels, text) are ever shipped** — you supply your own
legally-owned copy.
At runtime, the engine:
- has **no networking of any kind** (no sockets, no HTTP, checked directly
against the source),
- has **no telemetry, crash reporting, or analytics** of any kind,
- **never touches the Windows registry** and **never requests elevated
permissions**,
- writes save data, config, and derived ROM assets only inside its own
folder — never to `%APPDATA%`, `Program Files`, or any system location.
The one-time asset converter (`ge007-convert` / `prepare-assets.py`) — the
part of the bundle most likely to draw suspicion, since it's the thing that
reads your ROM and writes new files — has no network access at all,
verifies your ROM against a fixed table of known retail GoldenEye 007
hashes before touching it (an unrecognized file is rejected outright), and
writes only inside the bundle folder. Its full source ships right next to
the compiled version in every release if you'd like to read it yourself.
See [`SECURITY.md`](../.github/SECURITY.md) for why the binaries are
unsigned and what that means in practice (a SmartScreen prompt on first
run, and why some antivirus engines flag the converter specifically).
## Building from source
Cloning and building pulls in only standard, well-known packages: MSYS2
`pacman` packages on Windows, `apt` packages on Linux — no `npm`, no
`cargo`, nothing downloaded at CMake configure time, no git submodules, and
no prebuilt binaries checked into the repository. **One real gap**: the CI
build installs `pyinstaller` from PyPI without pinning a version, which is
used to freeze the release's asset-converter tool. It's a widely-used
packaging tool, and what it packages is entirely in-repo, stdlib-only
Python — but an unpinned dependency means a future PyInstaller release
could change what ships without a corresponding diff in this repository.
Pinning it is a tracked follow-up.
One development-only backdoor exists in source builds: the `GE_DEBUG_UNLOCKALL`
environment variable (`src/game/file2.c`) pre-unlocks all cheat options at boot.
It is off unless explicitly set, changes no shipped behavior, and is documented
here so the env-var surface stays fully accounted for.
## Fidelity to the original N64 game
The port's non-negotiable rule is that decompiled game logic is never
changed — only genuine N64-hardware dependencies get a PC-side
replacement, in the `port/` layer. This was audited directly:
- **Region build macros** exactly mirror the original N64 Makefile's macro
sets for all three regions — verified line-by-line.
- **The audio/library file classification** (which original files compile
vs. get PC-shimmed) exactly matches the project's own ground-truth
manifest — no silent gaps, no silent duplicates.
- **The one shipped game-logic-adjacent change** (a portal-culling
float-precision fix, D271) is `#ifdef PORT`-gated, documented in the
finding log, and moves *toward* matching N64 behavior, not away from it.
Every other deviation from the decompiled source is one of the documented
32→64-bit pointer-width ABI corrections described above.
**One issue, found and fixed before v0.3.0 shipped**: the port's
random-number generator (`port/src/random.c`) was documented as a bit-exact
port of the N64's PRNG, but wasn't — a shift-operation helper didn't match
the real MIPS64 instruction semantics it was meant to mirror. This was
independently confirmed by hand-tracing the assembly against the C port,
tracked as finding D284, and fixed before the release (verified clean
across a full 21-level sweep, no crashes). It affected loot placement, AI
behavior variance, and replay-state determinism from the very first random
draw, in every release before v0.3.0. Because of the fix, RNG-derived
output — including the save-file CRC — changed, which meant existing save
files and any previously recorded replays no longer validate; v0.3.0's
release notes describe the automatic save migration that handles this. We'd
rather say this plainly than let a "verified bit-exact" claim stand
uncorrected, both when it was wrong and now that it's fixed.
Everything else checked (the documented ABI/pointer-width exceptions used
for the 32-to-64-bit transition, a sample of the finding log's own claimed
fixes against the actual current code) matched what's documented.
---
*This page is kept current alongside future releases. Its source review is a
manual, human-reviewed process today — see the project roadmap for plans to
partially automate the mechanical parts of it.*
-12
View File
@@ -269,16 +269,6 @@ typedef short ENVMIX_STATE[40];
* Macros to assemble the audio command list
*/
#ifdef PORT
/* PC port (D199): the N64 RSP audio ucode that would execute this acmd list
* never runs on PC (port/src/ucode.c). Following the Perfect Dark PC port's
* macro-swap pattern (docs/dev/AUDIO-PLAN.md), each aXxx macro below is
* replaced with an immediate call into port/src/mixer.c's software
* implementation instead of packing RSP command words. The N64 branch below
* is untouched. */
#include "mixer.h"
#else /* !PORT */
#define aADPCMdec(pkt, f, s) \
{ \
Acmd *_a = (Acmd *)pkt; \
@@ -412,8 +402,6 @@ typedef short ENVMIX_STATE[40];
_a->words.w1 = (unsigned int) d; \
}
#endif /* PORT */
#endif /* _LANGUAGE_C */
#endif /* !_ABI_H_ */
-7
View File
@@ -48,13 +48,6 @@ void gfx_set_texture_filter(enum FilteringMode mode);
void gfx_set_mipmap_filter(enum MipmapFilteringMode mode);
void gfx_set_fix_mip_textures(int on);
void gfx_set_wrap_fix(int on);
void gfx_set_anisotropy_level(int level); /* 1 = off; clamped to GL max */
void gfx_set_safe_area_crop(int on); /* crop the N64 TV-overscan safe-area margin instead of showing it as black bars */
/* On-window pixel rect (top-left origin) the full VI logical canvas (0,0)-
* (SCREEN_WIDTH, SCREEN_HEIGHT) currently maps to, honoring the safe-area
* crop above -- for inverting a window mouse click into logical 2D UI space
* (see port/src/optionsoverlay.c, D316). */
void gfx_get_ui_screen_rect(int32_t *outX, int32_t *outY, int32_t *outW, int32_t *outH);
void gfx_texture_cache_clear(void);
void gfx_texture_cache_delete(const uint8_t *orig_addr);
void gfx_texture_cache_delete_range(const uint8_t *start, const uint8_t *end);
+7 -40
View File
@@ -510,27 +510,11 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad
}
}
/* D266: CVG_X_ALPHA ("texedge") modelled on the RDP's actual semantics,
* keyed on the alphacompare field. The inherited line quantized to
* opaque/discard at a hardcoded 0.19: it kept texels the N64 discards and
* forced survivors fully opaque, defeating the ALPHA_CVG_SEL blend.
* GE's Surface 1 IA4 tree strip (D236/D265) rendered as a wall because of
* it. Now: AC_DITHER -> dithered discard; AC_THRESHOLD -> discard only
* fully-transparent; AC_NONE -> discard below 0x80 and let survivors keep
* their alpha for blending. (AC_DECAL never sets SHADER_OPT_TEXTURE_EDGE,
* see gfx_pc.cpp.) */
if (cc_features.opt_texture_edge && cc_features.opt_alpha) {
if (cc_features.opt_noise) {
append_line(fs_buf, &fs_len,
" if (texel.a < ((random(vec3(floor(gl_FragCoord.xy * noise_scale), float(frame_count))) + 1.0) / 2.0)) discard;");
} else if (cc_features.opt_alpha_threshold) {
append_line(fs_buf, &fs_len, " if (texel.a < 1.0 / 256.0) discard;");
} else {
append_line(fs_buf, &fs_len, " if (texel.a < 0.5) discard;");
}
append_line(fs_buf, &fs_len, " if (texel.a > 0.19) texel.a = 1.0; else discard;");
}
if (cc_features.opt_alpha && cc_features.opt_noise && !cc_features.opt_texture_edge) {
if (cc_features.opt_alpha && cc_features.opt_noise) {
append_line(fs_buf, &fs_len,
" texel.a *= floor(clamp(random(vec3(floor(gl_FragCoord.xy * noise_scale), float(frame_count))) + "
"texel.a, 0.0, 1.0));");
@@ -712,27 +696,12 @@ static void gfx_opengl_select_texture(int tile, GLuint texture_id, bool linear_f
static void gfx_opengl_upload_texture(const uint8_t* rgba32_buf, uint32_t width, uint32_t height, bool gen_mipmaps) {
#ifdef PORT
/* GE_TEXDUMP: PPM-dump every uploaded texture (first N) for B2/D161 triage.
* D250: this runs on every texture upload -- cache the getenv() like the
* gfx_pc.cpp import_texture() sites (uncached getenv() measured at ~50%
* of the hot render thread's CPU time via a live profile). */
static int ge_texdump_gl = -1;
if (ge_texdump_gl < 0) ge_texdump_gl = getenv("GE_TEXDUMP") != NULL;
if (ge_texdump_gl) {
/* GE_TEXDUMP: PPM-dump every uploaded texture (first N) for B2/D161 triage. */
if (getenv("GE_TEXDUMP")) {
static int td = 0;
static int td_fire = 0;
/* D219: the 400-cap fills up during ordinary level load, long before
* a player-triggered explosion. Never suppress the 16x14 RGBA fire
* particle images (assets/oddtextures.c IMAGE_FIRE_N via
* globalDL_0x078..0x9a8) -- that's the one shape we actually need. */
const bool is_fire_shape = (width == 16 && height == 14);
if ((is_fire_shape || td < 400) && width && height && width < 4096 && height < 4096) {
if (td < 400 && width && height && width < 4096 && height < 4096) {
char nm[128];
if (is_fire_shape) {
snprintf(nm, sizeof nm, "texdump/fire%03d_%ux%u_mip%d.ppm", td_fire++, width, height, (int)gen_mipmaps);
} else {
snprintf(nm, sizeof nm, "texdump/t%03d_%ux%u_mip%d.ppm", td++, width, height, (int)gen_mipmaps);
}
snprintf(nm, sizeof nm, "texdump/t%03d_%ux%u_mip%d.ppm", td++, width, height, (int)gen_mipmaps);
FILE* f = fopen(nm, "wb");
if (f) {
fprintf(f, "P6\n%u %u\n255\n", width, height);
@@ -1107,9 +1076,7 @@ static void gfx_opengl_end_frame(void) {
* the default framebuffer (the window back buffer; the scene renders there
* directly at 1:1 window size) and write a P6 PPM. */
extern "C" bool gfx_opengl_pcdump_enabled(void) {
static int cached = -1;
if (cached < 0) cached = getenv("GE_PCDUMP") != NULL;
return cached != 0;
return getenv("GE_PCDUMP") != NULL;
}
extern "C" bool gfx_opengl_dump_bound_fbo(uint32_t width, uint32_t height, const char* path) {
+53 -787
View File
File diff suppressed because it is too large. Load diff
-9
View File
@@ -29,15 +29,6 @@ struct TextureCacheKey {
// (gDPLoadBlock mip chains vs partial loads); include the size so a
// truncated first import cannot poison later full imports.
uint32_t size_bytes;
// D217: for CI (palettized) textures, a hash of the decoded rdp.palette
// content at import time. GE assembles weapon / character model DLs in
// scratch arena RAM and reissues gDPLoadTLUT with different palette
// *content* from a repeated source address, so
// {texture_addr, palette_addrs, palette_index, size_bytes} alone collides
// across materials/frames -> a CI tile takes a stale cache HIT decoded
// against a different palette. Recomputed only in gfx_dp_load_tlut(), so
// there is no per-texel or per-draw cost. Zero for non-CI textures.
uint32_t palette_hash;
bool operator==(const TextureCacheKey&) const noexcept = default;
+1 -7
View File
@@ -409,13 +409,7 @@ static inline void sync_framerate_with_timer(void) {
// We want to exit a bit early, so we can busy-wait the rest to never miss the deadline
left -= 15000UL;
if (left > 0) {
/* D250: `left` is in 100ns units (qpc_to_100ns); sysSleep() takes
* microseconds. Passing it unconverted slept ~10x too long every
* time this branch fired (e.g. an intended 2ms sleep became a real
* ~20ms one), silently eating the rest of the frame budget whenever
* per-frame work left little slack -- a resolution/GPU-independent
* frame-pacing bug, not a real render cost. */
sysSleep((uint32_t)(left / 10));
sysSleep(left);
}
do {
+3 -5
View File
@@ -23,14 +23,12 @@ void audioDestroy(void);
/* Number of samples (stereo s16 frames) currently queued. */
s32 audioGetSamplesBuffered(void);
/* D204/F1: emulate the N64 AI_LEN_REG -- bytes remaining in the buffer the
* DAC is *currently* playing, NOT the whole queue depth. src/audi.c does
* arithmetic on this value that only holds inside the N64's range. */
u32 audioGetAiLengthBytes(void);
/* Queue the next block of mixed samples (len is in bytes). */
void audioSetNextBuffer(const s16 *buf, u32 len);
/* Called at the end of each frame to flush the queued buffer to the device. */
void audioEndFrame(void);
#ifdef __cplusplus
}
#endif
-17
View File
@@ -1,17 +0,0 @@
/*
* audiotrace.h — serialized trace writer for the GE_AUDIOTRACE / WIRE probes.
*
* D202/M-70 (PORT): the probe sites live in TWO threads — sndPlaySfx runs on
* the game thread, the sndp event handlers ([EVT]/[VOICE+]/[STOP-EVT]) and
* load.c's [PASTEND] run on the audio thread. Each site used to open its own
* unbuffered FILE* on the same path; concurrent fprintf() calls interleaved
* MID-LINE and corrupted records, which M-69's offline matcher misread as
* dropped voice allocations. One FILE* per path plus a spinlock around
* vfprintf makes each line atomic.
*/
#ifndef PORT_AUDIOTRACE_H
#define PORT_AUDIOTRACE_H
void geTracePrintf(const char *path, const char *fmt, ...);
#endif /* PORT_AUDIOTRACE_H */
+7 -10
View File
@@ -44,23 +44,20 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y);
* disabled in config. */
void inputSetMouseGrab(int on);
/* WI-1 click-to-lock cursor capture. The host event pump calls
* inputNotifyClick() when a mouse button goes down inside the game window
* (arms + locks the cursor), and inputReleaseCapture() when ESC is pressed
* (frees it; returns 1 if it consumed the key). */
/* WI-1 click-to-lock cursor capture (Input.MouseCaptureMode = 1). The host
* event pump calls inputNotifyClick() when a mouse button goes down inside the
* game window (arms + locks the cursor), and inputReleaseCapture() when ESC is
* pressed (frees it; returns 1 if it consumed the key). inputMouseCaptureActive()
* is true when capture mode is on and the cursor is currently free -- the pump
* uses it to decide whether a click should be swallowed rather than passed on. */
void inputNotifyClick(void);
int inputReleaseCapture(void);
int inputMouseCaptureActive(void);
/* F10 options overlay: force the OS cursor free + visible while the overlay
* owns the mouse. Safe to call every poll. */
void inputSuspendForOverlay(void);
/* Raw gamepad state for controller idx (the F10 options overlay's gamepad
* navigation). Unaffected by the overlay's pad-swallow, which happens at our
* logic layer, not SDL's. No pad open -> 0. */
int inputPadButton(int idx, SDL_GameControllerButton b);
short inputPadAxis(int idx, SDL_GameControllerAxis a);
/* Queue a mouse-wheel weapon-cycle input (one short A-button press). Sign is
* ignored -- GE only cycles forward on a bare A edge. */
void inputPostWheel(int notches);
+11 -61
View File
@@ -2,21 +2,16 @@
#define PORT_MIXER_H
/*
* Audio mixing — software implementation of GE's RSP audio ucode opcodes.
* Audio mixing.
* Modelled on the PD port's port/include/mixer.h.
*
* include/PR/abi.h's aXxx macros normally pack RSP command words into an
* Acmd list for the audio ucode (aspMain, never run on PC — port/src/ucode.c)
* to execute later. Under #ifdef PORT, abi.h includes this header instead,
* which redefines every aXxx macro to call an Impl function here immediately
* — same macro-swap trick as the Perfect Dark PC port (docs/dev/AUDIO-PLAN.md).
* On the N64 the RSP audio ucode does the final mix / sample-rate handling
* before the AI DMA. Since the RSP is emulated in software here, this module
* performs that final processing on the CPU and hands the result to audio.c.
*
* GE uses the classic IDO libaudio ABI (verified against every call site in
* src/libultra/audio and src/libultrare/audio/{env,reverb}.c — NOT PD's
* differently-shaped "naudio New" ABI). Several opcodes (aADPCMdec,
* aResample, aEnvMixer, aLoadBuffer, aSaveBuffer) take only a state/DRAM
* pointer; their DMEM source/dest addresses and byte counts come from the
* most recent aSetBuffer call(s), tracked here as persistent context —
* see the field comments on MixerCtx in mixer.c.
* NOTE: the exact responsibilities depend on how GE's libaudio output is
* wired (see src/audi.c). This is a placeholder interface to be refined in
* Phase 3 (audio).
*/
#include <PR/ultratypes.h>
@@ -28,54 +23,9 @@ extern "C" {
void mixerInit(void);
void mixerDestroy(void);
/* Software RSP-audio-ucode opcode implementations (port/src/mixer.c). */
void aSetBufferImpl(u32 flags, u16 i, u16 o, u16 c);
void aClearBufferImpl(u16 addr, u32 count);
void aLoadBufferImpl(u32 dramAddr);
void aSaveBufferImpl(u32 dramAddr);
void aDMEMMoveImpl(u16 in, u16 out, u32 count);
void aLoadADPCMImpl(u32 count, u32 dramAddr);
void aSetLoopImpl(u32 stateAddr);
void aADPCMdecImpl(u32 flags, u32 stateAddr);
void aResampleImpl(u32 flags, u16 pitch, u32 stateAddr);
void aInterleaveImpl(u16 l, u16 r);
void aMixImpl(u32 flags, u16 gain, u16 in, u16 out);
void aSetVolumeImpl(u32 flags, u16 v, u16 t, u16 r);
void aEnvMixerImpl(u32 flags, u32 stateAddr);
void aPoleFilterImpl(u32 flags, u16 gain, u32 stateAddr);
void aSegmentImpl(u32 seg, u32 base);
#undef aSetBuffer
#undef aClearBuffer
#undef aLoadBuffer
#undef aSaveBuffer
#undef aDMEMMove
#undef aLoadADPCM
#undef aSetLoop
#undef aADPCMdec
#undef aResample
#undef aInterleave
#undef aMix
#undef aSetVolume
#undef aEnvMixer
#undef aPoleFilter
#undef aSegment
#define aSetBuffer(pkt, f, i, o, c) aSetBufferImpl((u32)(f), (u16)(i), (u16)(o), (u16)(c))
#define aClearBuffer(pkt, d, c) aClearBufferImpl((u16)(d), (u32)(c))
#define aLoadBuffer(pkt, s) aLoadBufferImpl((u32)(s))
#define aSaveBuffer(pkt, s) aSaveBufferImpl((u32)(s))
#define aDMEMMove(pkt, i, o, c) aDMEMMoveImpl((u16)(i), (u16)(o), (u32)(c))
#define aLoadADPCM(pkt, c, d) aLoadADPCMImpl((u32)(c), (u32)(d))
#define aSetLoop(pkt, a) aSetLoopImpl((u32)(a))
#define aADPCMdec(pkt, f, s) aADPCMdecImpl((u32)(f), (u32)(s))
#define aResample(pkt, f, p, s) aResampleImpl((u32)(f), (u16)(p), (u32)(s))
#define aInterleave(pkt, l, r) aInterleaveImpl((u16)(l), (u16)(r))
#define aMix(pkt, f, g, i, o) aMixImpl((u32)(f), (u16)(g), (u16)(i), (u16)(o))
#define aSetVolume(pkt, f, v, t, r) aSetVolumeImpl((u32)(f), (u16)(v), (u16)(t), (u16)(r))
#define aEnvMixer(pkt, f, s) aEnvMixerImpl((u32)(f), (u32)(s))
#define aPoleFilter(pkt, f, g, s) aPoleFilterImpl((u32)(f), (u16)(g), (u32)(s))
#define aSegment(pkt, s, b) aSegmentImpl((u32)(s), (u32)(b))
/* Process one frame of audio. `in` is the libaudio-mixed buffer, `frames`
* the number of stereo frames. Output is queued via audioSetNextBuffer. */
void mixerProcess(const s16 *in, u32 frames);
#ifdef __cplusplus
}
-15
View File
@@ -1,15 +0,0 @@
/* port_math.h -- real system math declarations for port/ code.
*
* The build's -I list puts the game's include/math.h (a constants-only
* header: M_PI & friends, guard _MATH_EXT_H_) ahead of the system one, so a
* plain #include <math.h> in a .c file picks up the game header and leaves
* fabs/tan/atan/pow/... implicitly declared. Including from THIS header lets
* #include_next step past the game header to the real system math.h.
*/
#ifndef PORT_MATH_H
#define PORT_MATH_H
#include <math.h> /* -> include/math.h (game constants) via -I include */
#include_next <math.h> /* -> next match in the search path: system math.h */
#endif /* PORT_MATH_H */
-24
View File
@@ -1,24 +0,0 @@
#ifndef PORT_ROMCONVERT_H
#define PORT_ROMCONVERT_H
/*
* First-run sidecar conversion — see port/src/romconvert.c. Called from
* romdataInit() once a valid ROM image is in hand; spawns the bundled frozen
* converter (ge007-convert) if the pcmodels/pccg sidecars are missing.
*/
#ifdef __cplusplus
extern "C" {
#endif
/* romImg: mapped/read ROM image (country byte at 0x3E selects the region);
* romRelPath: the "$S/..." / "$E/..." / "./..." path the ROM was opened from.
* Returns 1 when both sidecars are present, -1 when they could not be
* produced (caller should abort boot). */
int romConvertEnsureSidecars(const unsigned char *romImg, const char *romRelPath);
#ifdef __cplusplus
}
#endif
#endif /* PORT_ROMCONVERT_H */
-2
View File
@@ -4,8 +4,6 @@
/* Generated by CMake from versioninfo.h.in — do not edit. */
#define GE007_VERSION_HASH "@VERSION_HASH@"
#define GE007_VERSION_CODENAME "@VERSION_CODENAME@"
#define GE007_ORIGIN_URL "@ORIGIN_URL@"
#define GE007_TARGET_PLATFORM "@TARGET_PLATFORM@"
#define GE007_ROMID "@ROMID@"
#define GE007_IS_PAL @VERSION_IS_PAL@
-3
View File
@@ -20,9 +20,6 @@ extern "C" {
#endif
/* Initialize the window + GL context. Returns 0 on success. */
/* Steam Deck first-run preset; must run after the config constructor and
* before configLoad() (see video.c). No-op effect once an ini exists. */
void videoApplySteamOSDefaults(void);
int videoInit(void);
void videoDestroy(void);
+21 -252
View File
@@ -6,102 +6,33 @@
* osAiGetLength (libultra.c) route through here.
*
* Modelled on the PD port's port/src/audio.c (~75 lines).
*
* STATUS: scaffolding stub — implement during Phase 3.
*/
#include <SDL.h>
#include <stdlib.h>
#include <stdio.h>
#include "platform.h"
#include "system.h"
#include "config.h"
#include "audio.h"
#include "audiotrace.h"
static SDL_AudioDeviceID dev = 0;
/* src/audi.c: g_FrameSize + EXTRA_SAMPLES + 0x10, the exact sample count
* info->data is allocated for (audi.c:388). Zero until amCreateAudioManager
* has run. Used by the D204/F4 oversize guard below. */
extern u32 g_MaxFrameSize;
/* D202 diag: GE_AUDIODUMP=1 raw-dumps every mixed s16 stereo buffer at
* 22050Hz to audiodump.raw in the CWD for offline waveform inspection.
* Remove once D202 is root-caused. */
static FILE *s_audioDumpFile = NULL;
static int s_audioDumpChecked = 0;
static u64 s_audioDumpBytesWritten = 0;
/* D202 diag: exact byte offset into audiodump.raw written so far, so a probe
* elsewhere (snd.c sndPlaySfx) can log precisely which sample position a
* request happened at, instead of guessing via an RMS scan. */
u64 audioDumpBytePos(void)
{
return s_audioDumpBytesWritten;
}
static const s16 *nextBuf = NULL;
static u32 nextSize = 0;
static int bufferSize = 512;
/* D204/F3: was 8192 frames (372 ms). GE's AI feedback loop (src/audi.c:531)
* regulates to a very shallow queue -- it only asks for more than
* g_MinFrameSize once the queue is under ~69 frames -- so a 372 ms drop
* threshold is far past anything the game can steer out of, and just
* converts overproduction into silent buffer loss at high latency.
* 2880 frames (130 ms, four of GE's 720-sample blocks) sits comfortably above
* the F5 cushion of AUDIO_TARGET_FRAMES while keeping worst-case latency in a
* range the regulator can still steer out of.
* NB: an existing data/ge007.ini pins QueueLimit, so this default only
* applies to fresh configs -- see docs/dev/findings.md D204. */
static int queueLimit = 2880;
/* D204/F1: size in bytes of the block most recently handed to the DAC. Used
* to bound audioGetAiLengthBytes() to one buffer, like real AI hardware. */
static u32 lastBufferBytes = 0;
/* D204/F5: queue cushion the AI feedback loop steers toward, in stereo
* frames. ~46 ms at 22050 Hz -- enough to absorb OS scheduling jitter and a
* dropped retrace or two, small enough that input->sound latency stays well
* under a frame of the game's own 30 Hz audio cadence. See the long comment
* in audioGetAiLengthBytes(). */
#define AUDIO_TARGET_FRAMES 1024
/* D204/F4: rate-limited reporting for the two ways audio can be lost. */
static u32 dropCount = 0;
static u32 oversizeCount = 0;
/* D204 diag (temporary): GE_D204_OLD=1 restores the pre-fix behaviour --
* osAiGetLength() reports the whole SDL queue depth and queueLimit goes back
* to 8192 -- so before/after can be measured in ONE binary, without
* build-to-build variance. Remove once D204 is closed. */
static int d204OldMode(void)
{
static int cached = -1;
if (cached < 0) {
cached = getenv("GE_D204_OLD") ? 1 : 0;
if (cached) queueLimit = 8192;
}
return cached;
}
static int queueLimit = 8192;
int audioInit(void)
{
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
sysLogPrintf(LOG_ERROR, "audioInit: SDL_InitSubSystem: %s", SDL_GetError());
return -1;
}
SDL_AudioSpec want = {0};
want.freq = 22050; /* GE's OUTPUT_RATE (src/audi.c) */
want.format = AUDIO_S16SYS;
want.channels = 2;
want.samples = (u16)bufferSize;
SDL_AudioSpec have;
dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
if (!dev) {
sysLogPrintf(LOG_ERROR, "audioInit: SDL_OpenAudioDevice: %s", SDL_GetError());
return -1;
}
SDL_PauseAudioDevice(dev, 0);
sysLogPrintf(LOG_INFO, "audioInit: opened SDL audio device at %d Hz", have.freq);
/* TODO(Phase 3):
* - SDL_InitSubSystem(AUDIO)
* - open device: 22020 Hz (match GE's OUTPUT_RATE in src/audi.c),
* AUDIO_S16SYS, 2 channels
* - unpause
*/
sysLogPrintf(LOG_INFO, "audioInit: TODO (Phase 3)");
return 0;
}
@@ -115,182 +46,20 @@ s32 audioGetSamplesBuffered(void)
return dev ? (SDL_GetQueuedAudioSize(dev) / 4) : 0;
}
/*
* D204/F1 -- correct AI_LEN_REG semantics. ROBUSTNESS, not a bug fix:
* measured A/B (M-64) shows this changes no observable behaviour today.
*
* src/audi.c:531 sizes every audio block as
* frameSamples = (u16)((g_FrameSize - (osAiGetLength() >> 2) + 16 + 0x25) & ~0xf)
* clamped below at g_MinFrameSize. On the N64 osAiGetLength() reads
* AI_LEN_REG: the bytes left in the ONE buffer the DAC is currently playing,
* bounded by g_MaxFrameSize*4, so the term acts as a +-64-sample trim.
*
* Reporting SDL's whole queue depth instead lets that subtraction (done in
* u32) wrap once the queue exceeds g_FrameSize + 0x35 frames, which it does
* routinely -- measured high-water 2064 frames over a 5 min run. That wrap
* is HARMLESS in practice and I verified it rather than assuming: the result
* is stored into `s16 frameSamples` (audi.c:145), so it lands negative and
* audi.c's own `(s32)frameSamples < g_MinFrameSize` clamp catches it, giving
* a nominal 720-sample frame. Max block observed stays 3136 bytes against
* the 3156-byte info->data allocation -- there is no heap overrun.
*
* Bounding the report to one buffer is still the right thing: it matches the
* hardware the game was written against, keeps the arithmetic inside its
* designed range instead of relying on a signed-overflow accident, and costs
* one compare. AGENTS.md rule #2: it belongs here in port/, not in audi.c.
*/
u32 audioGetAiLengthBytes(void)
{
u32 queued = dev ? SDL_GetQueuedAudioSize(dev) : 0;
if (d204OldMode()) {
return queued; /* pre-fix behaviour, for A/B measurement only */
}
/* D204/F5 -- shift the regulator's setpoint off zero. THIS is the part
* that measurably changes behaviour.
*
* audi.c only asks for more than g_MinFrameSize (720) once the reported
* length drops under ~69 frames. 720 samples per 30 Hz audio frame is
* 21600/s against a 22050 Hz device, so at the nominal rate the pipeline
* is structurally ~2 % short and relies on those top-up frames to make it
* back. On the N64 a 69-frame setpoint is fine -- AI is double-buffered
* and the VI interrupt is exact. On PC it means the loop does not react
* until the SDL queue is 3 ms from empty, which OS scheduling jitter
* clears easily: measured rt=0.980 sustained with q reaching 0, i.e. the
* device is padding ~2 % of playback with silence, permanently. That lost
* time is unrecoverable, because a queue-depth reading cannot tell the
* regulator it has already fallen behind.
*
* Subtracting a target cushion makes "queue is at AUDIO_TARGET_FRAMES"
* read as "queue is empty", so the loop tops up while there is still
* ~46 ms of slack, and settles just above the cushion instead of
* oscillating into starvation. Purely a port-side setpoint change: audi.c
* still runs its own unmodified control law. */
{
const u32 targetBytes = AUDIO_TARGET_FRAMES * 4u;
queued = (queued > targetBytes) ? (queued - targetBytes) : 0u;
}
if (lastBufferBytes && queued > lastBufferBytes) {
queued = lastBufferBytes;
}
return queued;
}
void audioSetNextBuffer(const s16 *buf, u32 len)
{
if (!s_audioDumpChecked) {
s_audioDumpChecked = 1;
if (getenv("GE_AUDIODUMP")) {
s_audioDumpFile = fopen("audiodump.raw", "wb");
}
}
(void)d204OldMode(); /* D204 diag: ensure the queueLimit override applies */
nextBuf = buf;
nextSize = len;
}
/* D204 audio-health monitor: GE_D204=1 prints one line every 5 s of wall
* clock with everything needed to see the pipeline degrade in real time:
*
* rt= produced audio seconds / wall seconds. 1.00 is healthy; a
* sustained value below 1 means the 30 Hz retrace that drives
* amMain is being starved, and audio is being generated slower
* than the DAC eats it (gaps, stutter).
* q= SDL queue depth in frames. Should sit shallow (GE's regulator
* in audi.c:531 only asks for more than g_MinFrameSize once the
* queue is under ~69 frames). Climbing toward queueLimit means
* overproduction, and once it pins there every new block is
* dropped -- the "eventually no audio except a stuck loop" state.
* drop= blocks discarded because the queue was full (cumulative).
* max= largest block audi.c has handed us, vs its g_MaxFrameSize*4
* allocation.
*
* Deliberately cheap (one clock read + one counter per block, ~30/s) so
* it can be left on for a full playtest -- unlike GE_MIXERTRACE, whose
* unbuffered per-opcode log slows the process enough to starve the audio
* thread on its own (that artifact is what M-63 measured as "13 % of
* real time"; see docs/dev/findings.md D204). Remove once D204 closes. */
if (getenv("GE_D204")) {
static u64 startUs = 0, nextReportUs = 0, produced = 0;
static u32 maxLen = 0;
u64 now = sysGetMicroseconds();
if (!startUs) { startUs = now; nextReportUs = now + 5000000ull; }
produced += len;
if (len > maxLen) maxLen = len;
if (now >= nextReportUs) {
u64 wallUs = now - startUs;
/* produced bytes / 4 = frames; / 22050 = seconds of audio */
u64 rtx1000 = wallUs ? (produced / 4ull) * 1000000ull / 22050ull * 1000ull / wallUs : 0;
sysLogPrintf(LOG_NOTE,
"D204 audio health t=%llus rt=%llu.%03llu q=%d/%d drop=%u max=%u/%u",
(unsigned long long)(wallUs / 1000000ull),
(unsigned long long)(rtx1000 / 1000), (unsigned long long)(rtx1000 % 1000),
(int)audioGetSamplesBuffered(), queueLimit,
(unsigned)dropCount, (unsigned)maxLen,
(unsigned)(g_MaxFrameSize * 4u));
nextReportUs = now + 5000000ull;
}
}
/* D204/F4: invariant guard, believed unreachable. info->data is allocated
* for exactly g_MaxFrameSize samples (audi.c:388), so a longer block means
* alAudioFrame() has already run off the end of it. audi.c's own signed
* clamp should make that impossible (see audioGetAiLengthBytes above), and
* no run has ever tripped this -- but "impossible" is worth one compare
* per block when the alternative is a silent heap overrun. */
if (g_MaxFrameSize && len > g_MaxFrameSize * 4u) {
if (oversizeCount++ < 8) {
sysLogPrintf(LOG_ERROR,
"D204: oversized audio block %u bytes (info->data alloc %u) -- "
"audi.c frameSamples escaped its clamp; heap already overrun",
(unsigned)len, (unsigned)(g_MaxFrameSize * 4u));
}
return;
}
if (s_audioDumpFile && buf && len) {
fwrite(buf, 1, len, s_audioDumpFile);
s_audioDumpBytesWritten += len;
}
/* D77 diag (temporary): GE_AUDIOTRACE=1 already logs csplayer.c
* [MUSICNOTE] note-on events into audiotrace.log. That proves the
* level-music trigger fires and posts notes to the synth, but says
* nothing about whether those notes actually reach the final mixed
* buffer handed to the SDL device below. Interleave a cheap (~1/s)
* peak/RMS reading of *this* buffer into the SAME file/lock
* (geTracePrintf) so the two can be correlated by line order without
* per-line timestamps. Remove once D77 closes. */
if (buf && len && getenv("GE_AUDIOTRACE")) {
static u64 lastReportUs = 0;
u64 now = sysGetMicroseconds();
if (now - lastReportUs >= 1000000ull) {
const s16 *s = buf;
u32 n = len / 2u;
s32 peak = 0;
s64 sumsq = 0;
for (u32 i = 0; i < n; i++) {
s32 v = s[i];
if (v < 0) v = -v;
if (v > peak) peak = v;
sumsq += (s64)s[i] * (s64)s[i];
}
double rms = n ? __builtin_sqrt((double)sumsq / (double)n) : 0.0;
geTracePrintf("audiotrace.log", "[AUDIOLVL] peak=%d rms=%.1f n=%u\n",
(int)peak, rms, (unsigned)n);
lastReportUs = now;
}
}
if (dev && buf && len) {
void audioEndFrame(void)
{
if (nextBuf && nextSize && dev) {
if (audioGetSamplesBuffered() < queueLimit) {
SDL_QueueAudio(dev, buf, len);
lastBufferBytes = len;
} else if (dropCount++ % 128 == 0) {
/* D204/F3: dropping here used to be silent, so overproduction
* surfaced only as unexplained dropouts. */
sysLogPrintf(LOG_NOTE,
"D204: audio queue full (%d/%d frames), dropped block #%u",
(int)audioGetSamplesBuffered(), queueLimit, (unsigned)dropCount);
SDL_QueueAudio(dev, nextBuf, nextSize);
}
nextBuf = NULL;
nextSize = 0;
}
}
+5 -40
View File
@@ -324,7 +324,7 @@ static void *crashGetModuleBase(const void *addr)
return NULL;
}
static void crashStackTrace(char *msg, int sig, void *pc, ucontext_t *ucontext, siginfo_t *siginfo)
static void crashStackTrace(char *msg, int sig, void *pc)
{
unsigned msglen = 0;
void *frames[CRASH_MAX_FRAMES] = { NULL };
@@ -338,26 +338,6 @@ static void crashStackTrace(char *msg, int sig, void *pc, ucontext_t *ucontext,
char **strings = backtrace_symbols(frames, nframes);
CRASH_MSG("SIGNAL: %d\n", sig);
if (siginfo && (sig == SIGSEGV || sig == SIGBUS)) {
CRASH_MSG("FAULT ADDR: %p\n", (void *)siginfo->si_addr);
}
#if defined(PLATFORM_X86_64)
if (ucontext) {
CRASH_MSG("REGS: Rax=%p Rcx=%p Rdx=%p Rsi=%p Rdi=%p R8=%p R9=%p R10=%p R11=%p\n",
(void *)ucontext->uc_mcontext.gregs[REG_RAX],
(void *)ucontext->uc_mcontext.gregs[REG_RCX],
(void *)ucontext->uc_mcontext.gregs[REG_RDX],
(void *)ucontext->uc_mcontext.gregs[REG_RSI],
(void *)ucontext->uc_mcontext.gregs[REG_RDI],
(void *)ucontext->uc_mcontext.gregs[REG_R8],
(void *)ucontext->uc_mcontext.gregs[REG_R9],
(void *)ucontext->uc_mcontext.gregs[REG_R10],
(void *)ucontext->uc_mcontext.gregs[REG_R11]);
CRASH_MSG("Rsp=%p Rbp=%p\n",
(void *)ucontext->uc_mcontext.gregs[REG_RSP],
(void *)ucontext->uc_mcontext.gregs[REG_RBP]);
}
#endif
CRASH_MSG("PC: ");
if (pc) {
CRASH_MSG("%p\n", pc);
@@ -390,26 +370,13 @@ static void crashStackTrace(char *msg, int sig, void *pc, ucontext_t *ucontext,
free(strings);
}
/* D254: re-entrancy guard. The handler used to call sysFatalError() ->
* abort() -> SIGABRT, which re-entered this handler and TRUNCATED the log
* ("wb"), destroying the original SEGV block — the one with the faulting
* PC/registers. Field crash logs then only showed the abort. Guard against
* re-entry and terminate directly instead (the Windows path already did).
*/
static sig_atomic_t inCrashHandler = 0;
static void crashHandler(int sig, siginfo_t *siginfo, void *ctx)
{
if (inCrashHandler) {
_exit(128 + sig);
}
inCrashHandler = 1;
char msg[CRASH_MAX_MSG + 1] = { 0 };
ucontext_t *ucontext = ctx ? (ucontext_t *)ctx : NULL;
void *pc = NULL;
if (ucontext) {
if (ctx) {
ucontext_t *ucontext = (ucontext_t *)ctx;
#ifdef PLATFORM_X86
pc = (void *)ucontext->uc_mcontext.gregs[REG_EIP];
#elif defined(PLATFORM_X86_64)
@@ -422,7 +389,7 @@ static void crashHandler(int sig, siginfo_t *siginfo, void *ctx)
fflush(stderr);
fflush(stdout);
crashStackTrace(msg, sig, pc, ucontext, siginfo);
crashStackTrace(msg, sig, pc);
{
FILE *f = fopen(CRASH_LOG_FNAME, "wb");
@@ -432,9 +399,7 @@ static void crashHandler(int sig, siginfo_t *siginfo, void *ctx)
}
}
/* D254: terminate directly — sysFatalError() raises SIGABRT which used
* to re-enter this handler and truncate the log we just wrote. */
_exit(128 + sig);
sysFatalError("Crash!\n\n%s", msg);
}
/* D38: called by the kernel heartbeat watchdog (port/src/libultra.c) when a
-439
View File
@@ -1,439 +0,0 @@
/* d318watchdog.c -- D318: port-side deadlock recovery for the Facility
* (level_34) Ourumov/Trevelyan execution softlock.
*
* What this is (full writeup: docs/dev/findings.md, D318):
*
* As-authored latent race, faithfully reproduced by the port (NOT a port
* bug -- every link in the chain is byte-matched ROM logic):
*
* 1. Ourumov (chr 78, ai_22) pre-aims at kneeling Trevelyan (chr 67)
* during his monologue: actor_aim_at_actor with TARGET_AIM_ONLY|
* TARGET_DONTTURN (attacktype 0x64). chrlvInitActAttack picks a RANDOM
* variant from the aim-animation group (randomGetNext() % len) and
* modelSetAnimation's it with animlooping=0.
* 2. The aim anim plays to its endframe H (the "hold" pose). With
* animlooping==0, modelTickAnim has no wrap logic; when the frame
* reaches endframe, modelConstrainOrWrapAnimFrame clamps BOTH framea
* and frameb to ceil(H), so framea==frameb, and the tail of
* modelSetAnimFrame2WithChrStuff (if (vb==va) animframe1 = va) then
* discards all sub-frame progress: the model is PINNED at H. That is
* how a hold pose works by design -- normally something else changes
* the attack state next.
* 3. If the gas cascade sets objective bit 0x04 (combat) mid-monologue,
* ai_22 derails to section 0x2a and off=307 runs
* actor_fire_or_aim_at_target_update, which SUCCEEDS while the pre-aim
* is active: attacktype becomes 0x04, entityid becomes Trevelyan, and
* chrlvAttackActionRelated sets a NEW endframe from the variant's
* recoil/shoot start frame -- but it never calls modelSetAnimation.
* 4. Branch (determined by the random variant chosen in step 1):
* - new endframe <= H: chrlvTickAttackCommon's "endframe <= frame"
* block fires next tick and its unk31 path calls modelSetAnimation
* again -> pin broken -> anim advances into the shoot window ->
* c78 live-fires Trevelyan -> he dies -> attack ends -> ACT_STAND
* -> the stop check at ai_22 off=324 passes -> scene continues.
* - new endframe > H: nothing ever calls modelSetAnimation again
* (verified by probe: zero calls in frozen runs). The frame stays
* pinned at H < shoot_start, c78 never fires, Trevelyan never
* dies, and ai_22's off=324 stop check (chrHasStoppedOrPatroling,
* TRUE only for ACT_STAND/ACT_ANIM/ACT_PATROL) can never pass
* while c78 is in ACT_ATTACK -> PERMANENT SOFTLOCK.
*
* The N64 has the identical race and the identical softlock; this watchdog
* is a deliberate port-layer accommodation (same pattern as the D202 M-66b
* port-side expiration). It detects the exact unrecoverable signature and,
* after a long margin, re-initializes c78's attack via sub_GAME_7F025560 --
* the same entry point the game itself uses for a fresh attack (chrlvTickAttack
* type_of_motion==2 calls it with (attacktype, entityid)). That runs
* chrlvInitActAttack: random variant pick + modelSetAnimation(start_frame),
* which breaks the framea==frameb pin. c78 then live-fires Trevelyan exactly
* like the natural escape path (verified: escape runs kill c67 within ~230
* ticks of a fresh init), his attack sequence completes, he reaches
* ACT_STAND, the stop check at off=324 passes, and ai_22 proceeds as
* authored (the off=333 headshot lands on the corpse -- the same no-op the
* game already handles when Trevelyan dies of gas before the execution shot).
*
* NOTE: merely killing Trevelyan (e.g. via handles_shot_actors) is NOT
* enough -- a pinned attack has no internal completion event tied to target
* death, so c78 would stay in ACT_ATTACK and the stop check would still
* never pass (verified by probe). The attack state itself must be re-seeded.
*
* Signature (all must hold for D318_DEADLOCK_TICKS consecutive ticks):
* - chr 78 exists, is on AI list 0x0417 (ai_22), offset in [321, 333)
* (the sleep/stop-check loop; 333 is the headshot command itself),
* - chr 78 actiontype == ACT_ATTACK with act_attack.entityid == 67
* (targeting Trevelyan -- the derailed state; legitimate combat at this
* offset targets Bond, ent=1),
* - chr 67 exists, has a prop, and is not dead.
*
* In every non-deadlocked state the watchdog is inert: the escape path
* kills Trevelyan within ~25-100 ticks (60x under the threshold) and
* legitimate combat never targets chr 67 from this loop. One-shot per
* OCCURRENCE: after firing, it re-arms once the signature has been absent
* for D318_REARM_TICKS consecutive ticks (the deadlock is gone -- recovered
* or the player moved on), so a later replay of Facility in the same
* session (AllUnlocked) is still covered. Logs its detection and
* intervention for playtest audit.
*
* Opt out (research only): GE_D318W=0
*/
#include <stdlib.h>
#include <string.h>
#include "PR/os.h"
#include "bondtypes.h"
#include "chr.h"
#include "chrai.h"
#include "chraction.h"
#include "lv.h"
#include "model.h"
#include "random.h"
#ifdef PORT
/* chrai.c defines these but chrai.h does not declare them. */
extern s32 chraiGetAIListID(AIRecord *AIList, bool *isGlobalAIList);
/* chraction.c internal (not in chraction.h): the fresh-attack entry point.
* Signature from the definition; call form mirrors chrlvTickAttack's own
* re-init at chraction.c:7401. */
extern void sub_GAME_7F025560(ChrRecord *self, s32 attack_type, s32 arg2);
#define D318_LIST_AI22 0x0417 /* Ourumov's execution list (ai_22) */
#define D318_OFF_LOOP_MIN 321 /* label 0x01: sleep/stop-check loop */
#define D318_OFF_LOOP_MAX 333 /* exclusive: the headshot command */
#define D318_OURUMOV_CHR 78
#define D318_TREVELYAN_CHR 67
#define D318_DEADLOCK_TICKS 600 /* 10 s at 60 Hz; escape takes < 2 s */
#define D318_REARM_TICKS 600 /* signature-absent ticks before re-arm */
static ChrRecord *d318wFindChr(s16 chrnum)
{
s32 i;
for (i = 0; i < g_NumChrSlots; i++)
{
if (g_ChrSlots[i].chrnum == chrnum)
{
return &g_ChrSlots[i];
}
}
for (i = 0; i < g_ActiveChrsCount; i++)
{
if (g_ActiveChrs[i].chrnum == chrnum)
{
return &g_ActiveChrs[i];
}
}
return NULL;
}
void d318WatchdogTick(void)
{
static int s_enabled = -1; /* -1 uncached, 0 off (GE_D318W=0), 1 on */
static s32 s_run = 0; /* consecutive ticks the signature has held */
static s32 s_absent = 0; /* consecutive ticks signature absent */
static bool s_fired = FALSE;
ChrRecord *c78;
ChrRecord *c67;
bool g = FALSE;
bool sig;
if (s_enabled < 0)
{
const char *e = getenv("GE_D318W");
s_enabled = (e && e[0] == '0') ? 0 : 1;
}
if (!s_enabled)
{
return;
}
c78 = d318wFindChr(D318_OURUMOV_CHR);
c67 = d318wFindChr(D318_TREVELYAN_CHR);
sig = (c78 && c78->ailist
&& chraiGetAIListID(c78->ailist, &g) == D318_LIST_AI22
&& c78->aioffset >= D318_OFF_LOOP_MIN && c78->aioffset < D318_OFF_LOOP_MAX
&& c78->actiontype == ACT_ATTACK
&& (s32)c78->act_attack.entityid == D318_TREVELYAN_CHR
&& c67 && c67->prop && !chrIsDead(c67));
if (sig)
{
s_absent = 0;
if (!s_fired)
{
if (s_run == 1)
{
osSyncPrintf("D318W: t=%d deadlock signature detected (c78 ai_22 off=%d "
"ACT_ATTACK ent=67, c67 alive) -- starting %d-tick confirmation window\n",
(int)g_GlobalTimer, (int)c78->aioffset, D318_DEADLOCK_TICKS);
}
s_run++;
if (s_run >= D318_DEADLOCK_TICKS)
{
osSyncPrintf("D318W: t=%d D318 execution deadlock confirmed -- re-initializing "
"c78's attack (sub_GAME_7F025560 atk=0x%x ent=%d) to break the anim pin\n",
(int)g_GlobalTimer, (unsigned)c78->act_attack.attacktype,
(int)c78->act_attack.entityid);
sub_GAME_7F025560(c78, (s32)c78->act_attack.attacktype, (s32)c78->act_attack.entityid);
s_fired = TRUE;
}
}
}
else
{
s_run = 0;
/* One-shot per occurrence: once the deadlock signature has been
* clear for a while after a fire, re-arm so a later replay of
* Facility in the same process (AllUnlocked) is still covered. */
if (s_fired && ++s_absent >= D318_REARM_TICKS)
{
s_fired = FALSE;
s_absent = 0;
osSyncPrintf("D318W: t=%d signature clear for %d ticks -- watchdog re-armed\n",
(int)g_GlobalTimer, D318_REARM_TICKS);
}
}
}
/* ------------------------------------------------------------------ */
/* D318T: diagnostic timeline for the "Ourumov shoots Trevelyan the */
/* moment his lines begin" report (2026-09-20). Env-gated, OFF by */
/* default; enable with GE_D318T=1. Port-side read-only: logs state */
/* transitions so a single playtest captures which combat trigger */
/* (gas bit 0x04 vs Bond near-miss flag) derailed ai_22, and how many */
/* ticks after the monologue started it landed. No game state is */
/* touched. */
/* */
/* Background: the ONLY path by which c78 live-fires Trevelyan */
/* (entityid 67) in this scene is section 0x2a -> off=307 */
/* actor_fire_or_aim_at_target_update succeeding while the pre-aim */
/* (ACT_ATTACK + AIM_ONLY|DONTTURN) is active. That requires a combat */
/* trigger inside the monologue loop: gas bit 0x04 (off=75) or */
/* chrIfNearMiss (off=87, CHRFLAG_NEAR_MISS -- set by Bond bullets */
/* passing c78's bounds, cleared on c78's next tick). The freeze */
/* (D318) and the immediate-fire are the two branches of the same */
/* derailed state (random aim-variant endframe vs the hold frame H). */
/* ------------------------------------------------------------------ */
/* Manual hex parse: MinGW's strtoull returns the sign-extended low 32 bits
* for values above LLONG_MAX (verified: "deadbeefcafe0123" ->
* ffffffffcafe0123). Parse by hand; stops at '@' or non-hex. */
static u64 d318wParseHex(const char *s)
{
u64 v = 0;
while (*s && *s != '@')
{
char c = *s++;
v <<= 4;
if (c >= '0' && c <= '9') { v |= (u64)(c - '0'); }
else if (c >= 'a' && c <= 'f') { v |= (u64)(c - 'a' + 10); }
else if (c >= 'A' && c <= 'F') { v |= (u64)(c - 'A' + 10); }
else { break; }
}
return v;
}
void d318TimelineTick(void)
{
/* E-B seed injection: GE_RSEED_LV=<hex64>[@tick] -- set g_randomSeed to
* the value read from N64 RAM (0x80024460) at a canonical moment, so the
* port's stream can be A/B-compared against the original from that point
* on. Test-only; inert without the env var. The DET line logs the running
* seed every 60 ticks for verifying stream parity afterwards. */
{
static int s_seedArmed = 1;
static u64 s_seedVal = 0;
static s32 s_seedTick = 0;
if (s_seedArmed)
{
const char *e = getenv("GE_RSEED_LV");
s_seedArmed = 0;
if (e && e[0])
{
const char *at = strchr(e, '@');
s_seedVal = d318wParseHex(e); /* MinGW strtoull mangles >LLONG_MAX */
s_seedTick = at ? (s32)strtol(at + 1, NULL, 10) : 0;
}
}
if (s_seedVal && (s32)g_GlobalTimer >= s_seedTick)
{
u64 v = s_seedVal;
g_randomSeed = v;
s_seedVal = 0;
osSyncPrintf("D318T: t=%d RSEED_LV -> %016llx\n",
(int)g_GlobalTimer, (unsigned long long)v);
}
}
static int s_on = -1; /* -1 uncached, 0 off, 1 on (GE_D318T=1) */
static s32 s_obj = -1; /* last logged objectiveregisters1 */
static s32 s_off = -1; /* last logged c78 aioffset */
static s32 s_act = -1; /* last logged c78 actiontype */
static s32 s_atk = -1; /* last logged c78 act_attack.attacktype */
static s32 s_ent = -1; /* last logged c78 act_attack.entityid */
static s32 s_nm = -1; /* last logged c78 NEAR_MISS flag state */
static f32 s_dmg = -1.0f;
static bool s_dead = FALSE;
static s32 s_hb = 0;
static s32 s_det = 0; /* E1 determinism fingerprint counter */
ChrRecord *c78;
ChrRecord *c67;
if (s_on < 0)
{
const char *e = getenv("GE_D318T");
s_on = (e && e[0] == '1') ? 1 : 0;
}
if (!s_on)
{
return;
}
c78 = d318wFindChr(D318_OURUMOV_CHR);
c67 = d318wFindChr(D318_TREVELYAN_CHR);
/* Objective bitfield transitions (the derail triggers live here: */
/* 0x04 gas/combat, 0x20 surrender/monologue, 0x40 flee). */
if (s_obj != objectiveregisters1)
{
osSyncPrintf("D318T: t=%d obj bits 0x%08x -> 0x%08x\n",
(int)g_GlobalTimer, (unsigned)s_obj, (unsigned)objectiveregisters1);
s_obj = objectiveregisters1;
}
if (c78)
{
s32 nm = (c78->chrflags & CHRFLAG_NEAR_MISS) != 0;
/* D318 anim-internals line: catches the kneel-attack route */
/* (unk54==0 => sub_GAME_7F0256F0 "chrAttackKneel" re-inits) and the */
/* hold/pin frame values, for the "bending down too far" report. */
{
static s32 s_tom = -1;
static u32 s_u54 = 0xFFFFFFFFu;
static f32 s_endf = -1.0f;
s32 tom = (s32)c78->act_attack.type_of_motion;
u32 u54 = c78->act_attack.unk54;
f32 endf = c78->model ? modelGetAnimEndFrame(c78->model) : -1.0f;
if (s_tom != tom || s_u54 != u54 || s_endf != endf)
{
osSyncPrintf("D318T: t=%d c78 anim mot=%d unk54=%u endframe=%.2f frame1=%.2f (off=%d)\n",
(int)g_GlobalTimer, tom, (unsigned)u54,
(double)endf,
(double)(c78->model ? modelGetAnimFrame(c78->model) : 0.0f),
(int)c78->aioffset);
s_tom = tom;
s_u54 = u54;
s_endf = endf;
}
}
if (s_off != (s32)c78->aioffset || s_act != (s32)c78->actiontype)
{
const char *phase = "?";
if (c78->aioffset >= 38 && c78->aioffset < 44) { phase = "pre-aim(off38)"; }
else if (c78->aioffset >= 44 && c78->aioffset < 69) { phase = "wait-loop(off44)"; }
else if (c78->aioffset >= 69 && c78->aioffset < 272){ phase = "MONOLOGUE(off69+)"; }
else if (c78->aioffset >= 272 && c78->aioffset < 295){ phase = "execution(0x13)"; }
else if (c78->aioffset >= 302 && c78->aioffset < 333){ phase = "COMBAT-0x2a"; }
osSyncPrintf("D318T: t=%d c78 off=%d [%s] act=%d atk=0x%x ent=%d\n",
(int)g_GlobalTimer, (int)c78->aioffset, phase,
(int)c78->actiontype, (unsigned)c78->act_attack.attacktype,
(int)c78->act_attack.entityid);
s_off = (s32)c78->aioffset;
s_act = (s32)c78->actiontype;
}
if (s_atk != (s32)c78->act_attack.attacktype || s_ent != (s32)c78->act_attack.entityid)
{
osSyncPrintf("D318T: t=%d c78 ATTACK CHANGE atk=0x%x ent=%d (off=%d)\n",
(int)g_GlobalTimer, (unsigned)c78->act_attack.attacktype,
(int)c78->act_attack.entityid, (int)c78->aioffset);
s_atk = (s32)c78->act_attack.attacktype;
s_ent = (s32)c78->act_attack.entityid;
}
if (s_nm != nm)
{
osSyncPrintf("D318T: t=%d c78 NEAR_MISS flag %s (Bond bullet passed his bounds)\n",
(int)g_GlobalTimer, nm ? "SET" : "cleared");
s_nm = nm;
}
}
if (c67)
{
bool dead = chrIsDead(c67);
if (s_dmg != c67->damage || s_dead != dead)
{
osSyncPrintf("D318T: t=%d c67(Trevelyan) dmg=%.2f/%.2f %s\n",
(int)g_GlobalTimer, (double)c67->damage, (double)c67->maxdamage,
dead ? "DEAD" : "");
s_dmg = c67->damage;
s_dead = dead;
}
}
/* Heartbeat while c78 is on his execution list, so gaps are visible. */
if (c78 && c78->ailist)
{
bool g = FALSE;
if (chraiGetAIListID(c78->ailist, &g) == D318_LIST_AI22 && (++s_hb >= 60))
{
s_hb = 0;
osSyncPrintf("D318T: t=%d c78 hb off=%d act=%d atk=0x%x ent=%d obj=0x%08x\n",
(int)g_GlobalTimer, (int)c78->aioffset, (int)c78->actiontype,
(unsigned)c78->act_attack.attacktype, (int)c78->act_attack.entityid,
(unsigned)objectiveregisters1);
}
}
/* E1 determinism fingerprint: one line per second over ALL active chrs */
/* + the PRNG seed + objective bits. Two no-input runs that differ here */
/* are non-deterministic in the port (foundational); identical logs */
/* mean run-to-run variation is input-timing-driven. */
if (++s_det >= 60)
{
s32 h = 0x811C9DC5;
s32 i;
s_det = 0;
for (i = 0; i < g_ActiveChrsCount; i++)
{
ChrRecord *c = &g_ActiveChrs[i];
union { f32 f; u32 u; } xf, yf, zf, df;
xf.f = c->prop ? c->prop->pos.x : 0.0f;
yf.f = c->prop ? c->prop->pos.y : 0.0f;
zf.f = c->prop ? c->prop->pos.z : 0.0f;
df.f = c->damage;
h ^= c->chrnum; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= (s32)c->actiontype; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= (s32)c->aioffset; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= xf.u; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= yf.u; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= zf.u; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= df.u; h = (h * 0x01000193) ^ 0x9E3779B9;
}
osSyncPrintf("D318T: DET t=%d seed=%08x%08x obj=0x%08x h=%08x nchrs=%d\n",
(int)g_GlobalTimer,
(unsigned)(u32)(g_randomSeed >> 32), (unsigned)(u32)g_randomSeed,
(unsigned)objectiveregisters1, (unsigned)h, (int)g_ActiveChrsCount);
}
}
#endif /* PORT */
+164 -842
View File
File diff suppressed because it is too large. Load diff
-106
View File
@@ -1,106 +0,0 @@
/*
* port/src/legacycrc.c — D297: one-time migration of pre-D284 save-slot CRCs.
*
* WHY A DELIBERATELY-WRONG PRNG LIVES IN THIS TREE (read before "fixing" it):
*
* v0.2.x shipped with a port-side bug in randomGetNextFrom's dsll32/dsrl32
* semantics (modelled as 32-bit shifts; real MIPS64 shifts by n+32 over the
* full register). D284 (commit 0de99b4d) fixed it. But fileGenerateCRC
* (src/game/crc.c) runs that PRNG off a LOCAL constant seed, so every slot
* checksum stored on disk is a fingerprint of the PRNG implementation that
* wrote it: saves written by v0.2.x validate under the OLD stream and fail
* the NEW one, and fileValidateSaves wipes failing slots (D295/M-148, D297).
*
* This file carries the pre-D284 transform so the eeprom shim can recognize
* a legacy save (old-CRC match) and re-stamp its two checksum words with the
* current-PRNG values before the game validates. No game files are touched;
* the new CRCs come from the game's own fileGenerateCRC — only the OLD PRNG
* is duplicated here, and it must stay bit-exact-WRONG. Do not "fix" the
* shifts below; do not share helpers with port/src/random.c.
*
* When this can be deleted: once no pre-D284 saves are expected in the wild
* (the v0.3.0 release is the last build that meets them un-migrated). Keep
* it at least until then; the cost is ~60 lines of dead-but-harmless code.
*
* Verification: the legacy stream below was re-derived from
* `git show 0de99b4d^:port/src/random.c` and matches tools_pc/d295_crc.py,
* which is bit-exact against compiled C for both PRNGs (see D295 M-148).
*/
#include <ultra64.h>
#include "file.h" /* save_data */
extern void fileGenerateCRC(u8 *addressA, u8 *addressB, save_data *retval);
/* Pre-D284 randomGetNextFrom: verbatim body from 0de99b4d^:port/src/random.c
* with the OLD helper semantics inlined ((x << n) & 0xFFFFFFFF etc.). */
static u32 geLegacyRandomGetNextFrom(u64 *seed)
{
u64 a3 = *seed;
u64 a1, a2;
a2 = (a3 << 0x1f) & 0xFFFFFFFFULL; /* dsll32, pre-D284 semantics */
a1 = a3 << 0x1f; /* dsll */
a2 = a2 >> 0x1f; /* dsrl */
a1 = (a1 >> 0) & 0xFFFFFFFFULL; /* dsrl32, pre-D284 semantics */
a3 = (a3 << 0xc) & 0xFFFFFFFFULL; /* dsll32, pre-D284 semantics */
a2 = a2 | a1;
a3 = (a3 >> 0) & 0xFFFFFFFFULL; /* dsrl32, pre-D284 semantics */
a2 = a2 ^ a3;
a3 = a2 >> 0x14; /* dsrl */
a3 = a3 & 0xfff;
a3 = a3 ^ a2;
*seed = a3;
return (u32)a3;
}
/* Same loop as fileGenerateCRC (src/game/crc.c) stepping the legacy PRNG.
* Keep in lockstep with crc.c if that loop ever changes. */
static void geLegacyGenerateCRC(u8 *addressA, u8 *addressB, save_data *retval)
{
u8 *byte;
s32 shift = 0;
s64 polynormal = 0x8F809F473108B3C1; /* same local constant seed as crc.c */
s32 checksum1 = 0;
s32 checksum2 = 0;
for (byte = addressA; byte < addressB; byte++, shift += 7) {
polynormal += *byte << (shift & 0xF);
checksum1 ^= geLegacyRandomGetNextFrom(&polynormal);
}
for (byte = addressB - 1; byte >= addressA; byte--, shift += 3) {
polynormal += *byte << (shift & 0xF);
checksum2 ^= geLegacyRandomGetNextFrom(&polynormal);
}
retval->chksum1 = checksum1;
retval->chksum2 = checksum2;
}
/* Migrate up to five consecutive 96-byte save slots in place. A slot is
* migrated iff it FAILS the current-PRNG check but PASSES the legacy one;
* re-stamped via the game's own fileGenerateCRC. Corrupt slots (fail both)
* are left alone for the game's own wipe path. Returns the count migrated. */
int geLegacyCrcMaybeMigrateSlots(u8 *slots5)
{
save_data *slots = (save_data *)slots5;
int migrated = 0;
for (int i = 0; i < 5; i++) {
save_data calc = {0}; /* only chksum1/chksum2 are read back */
u8 *a = &slots[i].completion_bitflags; /* slot bytes [8..96) */
u8 *b = (u8 *)&slots[i + 1]; /* end boundary, never dereferenced */
fileGenerateCRC(a, b, &calc);
if (calc.chksum1 == slots[i].chksum1 && calc.chksum2 == slots[i].chksum2)
continue; /* current-PRNG save (or already migrated) — idempotent no-op */
geLegacyGenerateCRC(a, b, &calc);
if (calc.chksum1 != slots[i].chksum1 || calc.chksum2 != slots[i].chksum2)
continue; /* corrupt: game's fileValidateSaves handles it */
fileGenerateCRC(a, b, &slots[i]); /* re-stamp chksum1/chksum2 in place */
migrated++;
}
return migrated;
}
+5 -329
View File
@@ -86,46 +86,8 @@ OSTime osGetTime(void)
return (OSTime)sysGetMicroseconds();
}
/* GE_DETERM=1 — fixed-tick deterministic mode (speed-ups plan Step 7 / R1,
* design in docs/dev/findings.md §D117). Test-only, default OFF: replaces
* the wall-clock osGetCount() below with a virtual clock advanced exactly
* once per virtual VI retrace (generated synchronously in osRecvMesg() —
* see the GE_DETERM block there — when __scMain, the sole consumer of
* g_viRetraceMQ, asks for the next message; NOT paced by an independent
* thread) instead of real elapsed time, so two runs of the same build take
* the same simulation path. Never the shipping timing model — the default
* (GE_DETERM unset) path below is byte-for-byte what it always was. */
static int g_determEnabled = 0;
static u32 g_determQuantum = 775875; /* NTSC; portKernelInit sets 931050 PAL */
static u32 g_determTicks = 0;
/* M-52 3rd attempt: __scMain is the sole consumer of g_viRetraceMQ, but
* before the first real gfx/audio task ever runs (src/boss.c's bounded
* osSetTimer(100ms)-based controller-detection loop -- genuinely real-time,
* out of scope by design), NOTHING is consuming its forwards, so unbounded
* per-ask generation free-spins for that whole real-time window and however
* many ticks accumulate is real-scheduling-dependent (confirmed: 2nd
* attempt's 33/38/14% divergence). waitForNextFrame() (frametiming.c) is a
* tight busy-spin on osGetCount(), not a queue wait -- so FREEZING the
* clock during that window (0 extra ticks) hangs it forever, and letting it
* free-run is exactly the bug. The fix: cap the clock to exactly ONE extra
* quantum beyond the seed before the first task runs -- fixed, identical
* every run, and >=1 quantum is already enough to clear
* waitForNextFrame()'s first-call threshold (see the arithmetic in
* frametiming.c), so it can't hang either. Set true by osSpTaskStartGo. */
static int g_determTaskEverRun = 0;
static int g_determPreTaskTicksGranted = 0;
/* GE_DETERM_TRACE=1 (with GE_DETERM=1): log every virtual-clock advance with a
* monotonic sequence number, for diffing two runs' logs to find the exact
* first point they diverge -- see docs/dev/findings.md §D117 M-52. Test-only,
* both env-gated off by default; zero cost when unset. */
static int g_determTraceEnabled = 0;
static u32 g_determTraceSeq = 0;
u32 osGetCount(void)
{
if (g_determEnabled) {
return g_determTicks;
}
/* The N64 RSP core counter advances at ~OS_CPU_COUNTER (~46.5 MHz), NOT
* microseconds. GE's pacing assumes this rate: waitForNextFrame() in
* frametiming.c waits for 775,875 - 387,937 ticks per NTSC frame (931,050
@@ -222,7 +184,7 @@ static void portHeartbeatCheck(void)
g_lastHeartbeatUs = now;
sysLogPrintf(LOG_ERROR,
"kernel heartbeat: no frame rendered for %llu ms (frames=%d); state:",
(unsigned long long)(now - g_lastFrameUs) / 1000, g_framesRendered);
(unsigned long long)(now - g_lastFrameUs), g_framesRendered);
for (int i = 0; i < PORT_MAX_THREADS; ++i) {
PortThread *t = &g_pt[i];
if (!t->os) continue;
@@ -272,25 +234,6 @@ static void *portTickThread(void *arg)
{
(void)arg;
for (;;) {
/* GE_DETERM (§D117, M-52 2nd attempt): retrace generation lives in
* osRecvMesg() now (see there) -- synchronous with the sole
* consumer asking for the next message, not paced by this thread.
* The M-52 FIRST attempt polled g_viRetraceMQ's empty/non-empty
* state from here on a real-time cadence; that poll's own
* OS-scheduling latency varied run-to-run and leaked directly into
* the tick count (measured: 90%/28%/16% frame divergence between
* two runs, worse than the port's baseline wall-clock
* nondeterminism this mode exists to remove). This thread still
* only does what osGetTime()-based real-time bookkeeping needs
* (timers, hang-heartbeat) -- deliberately NOT touching
* g_viRetraceMQ at all in this mode. */
if (g_determEnabled) {
portServiceTimers();
portHeartbeatCheck();
sysSleep(g_tickIntervalUs);
continue;
}
uint64_t now = sysGetMicroseconds();
if (!g_nextTickUs) g_nextTickUs = now + g_tickIntervalUs;
@@ -329,25 +272,8 @@ void portKernelInit(void)
if (osTvType == OS_TV_MPAL || osTvType == OS_TV_PAL) {
g_tickIntervalUs = 1000000 / 50; /* PAL: 50 frames/s -> 20ms */
g_determQuantum = 931050;
} else {
g_tickIntervalUs = 1000000 / 60; /* NTSC: 60 frames/s -> ~16.7ms */
g_determQuantum = 775875;
}
/* GE_DETERM=1: seed so the first waitForNextFrame() computes exactly
* (quantum + 387937) / quantum == 1 tick, per the §D117 design. */
g_determEnabled = getenv("GE_DETERM") != NULL;
if (g_determEnabled) {
g_determTicks = g_determQuantum;
g_determTraceEnabled = getenv("GE_DETERM_TRACE") != NULL;
sysLogPrintf(LOG_NOTE,
"GE_DETERM=1: fixed-tick deterministic mode (test-only, "
"quantum=%u) -- osGetCount() is now frame-locked, not wall-clock",
g_determQuantum);
if (g_determTraceEnabled) {
sysLogPrintf(LOG_NOTE, "DETERMTRACE seq=0 ticks=%u event=seed", g_determTicks);
}
}
pthread_t th;
@@ -651,42 +577,6 @@ s32 osRecvMesg(OSMesgQueue *mq, OSMesg *msg, s32 flag)
pthread_mutex_unlock(&pq->lock);
return -1;
}
/* GE_DETERM (§D117, M-52 2nd attempt): __scMain (sched.c) is the
* SOLE consumer of g_viRetraceMQ, and everything the rest of the
* boot/frame chain waits on (task execution via osSpTaskStartGo,
* joyPoll() -- which is also what unblocks joyCheckStatusThreadSafe
* -- and the forward to mainThread's gfxFrameMsgQ) happens
* synchronously inside __scMain's handling of THAT message, on this
* same thread, before it comes back to ask for the next one. So
* "the consumer is asking and none exists" is itself the correct,
* purely call-sequenced trigger for the next virtual retrace --
* driven by program order, not by an independent thread polling on
* a wall-clock cadence (the M-52 first attempt's mistake: that
* poll's timing varied run-to-run and leaked into the tick count).
* Enqueue inline (not via osEnqueueMesg -- would re-lock pq->lock,
* which we already hold). */
if (g_determEnabled && mq == g_viRetraceMQ) {
/* Still synthesize the message every time (keeps __scMain/
* joyPoll unblocked -- no deadlock), but only advance the clock
* up to a fixed cap before the first real task runs. See the
* g_determTaskEverRun comment above. */
if (g_determTaskEverRun || g_determPreTaskTicksGranted < 1) {
g_determTicks += g_determQuantum;
if (!g_determTaskEverRun) ++g_determPreTaskTicksGranted;
if (g_determTraceEnabled) {
sysLogPrintf(LOG_NOTE,
"DETERMTRACE seq=%u ticks=%u event=advance taskEverRun=%d",
++g_determTraceSeq, g_determTicks, g_determTaskEverRun);
}
} else if (g_determTraceEnabled) {
sysLogPrintf(LOG_NOTE,
"DETERMTRACE seq=%u ticks=%u event=capped taskEverRun=%d",
++g_determTraceSeq, g_determTicks, g_determTaskEverRun);
}
mq->msg[(mq->first + mq->validCount) % mq->msgCount] = g_viRetraceMsg;
++mq->validCount;
break;
}
/* Log each queue's first blocking call site (the return address is
* the caller); symbolicate with addr2line to find where in game code
* this wait happens. */
@@ -851,17 +741,14 @@ s32 osAiSetFrequency(u32 hz)
}
s32 osAiSetNextBuffer(void *buf, u32 size)
{
audioSetNextBuffer((const s16 *)buf, size);
/* TODO(Phase 3): hand the mixed buffer to audioSetNextBuffer(). */
(void)buf; (void)size;
return 1;
}
u32 osAiGetLength(void)
{
/* D204/F1: AI_LEN_REG is the bytes remaining in the buffer the DAC is
* CURRENTLY playing -- not the whole queue. Reporting the full SDL queue
* depth here pinned src/audi.c:531's frame-size regulator at its lower
* clamp and let its u32 subtraction wrap into a 74x heap overrun. See the
* long comment on audioGetAiLengthBytes() in port/src/audio.c. */
return audioGetAiLengthBytes();
/* TODO(Phase 3): return audioGetSamplesBuffered() in the right units. */
return 0;
}
void osAiSetConvert(u32 convert) { (void)convert; }
@@ -1188,147 +1075,6 @@ static void geEepromStore(void)
}
}
/* D257: Game.AllUnlocked -- mirror of src/game/file.h save_data (96 bytes;
* keep in sync). Two things are patched in at read time:
* - the progression-gated cheat-unlock bits (front.c
* frontCheckIfCheatIsUnlocked) test per-LEVEL bits in
* unlocked_cheats_1/2/3 that normal play sets when each level is
* completed, so a fresh save has an empty cheat menu;
* - every stage/difficulty completion time. When ANY single-player cheat
* is active, g_AppendCheatSinglePlayer makes the mission-select screen
* (front.c get_highest_unlocked_difficulty_for_level) only accept levels
* whose status is STAGESTATUS_COMPLETED (3), not merely UNLOCKED (1) --
* so AllUnlocked must read as a fully-completed campaign or enabling a
* cheat locks every level. Existing (nonzero) player times are kept;
* only empty slots get the max time (0x3FF). */
typedef struct ge_save_slot {
s32 chksum1;
s32 chksum2;
u8 completion_bitflags;
u8 flag_007;
u8 music_vol;
u8 sfx_vol;
u16 options;
u8 unlocked_cheats_1;
u8 unlocked_cheats_2;
u8 unlocked_cheats_3;
u8 padding;
u8 times[76]; /* (SP_LEVEL_MAX-1)*4: 19 levels x 4 difficulties */
} ge_save_slot; /* sizeof == 96 == save_data (file.h) -- enforced below */
/* If this ever fires, the mirror drifted from save_data and the AllUnlocked
* cheat patch silently no-ops (the block-read size check stops matching). */
typedef char ge_save_slot_size_check[(sizeof(ge_save_slot) == 96) ? 1 : -1];
/* Game-side CRC (src/game/crc.c) over [completion_bitflags, next slot),
* stored in the slot's own chksum1/2 -- exactly what fileValidateSaves
* re-checks after the block read. */
extern void fileGenerateCRC(u8 *addressA, u8 *addressB, void *retval);
extern int geLegacyCrcMaybeMigrateSlots(u8 *slots5); /* port/src/legacycrc.c (D297) */
extern s32 portAllUnlocked; /* port/src/video.c */
/* Bit math mirrors fileGetSaveStageDifficultyTime / fileSetDifficultyStageTime
* (src/game/file2.c): 10-bit time fields, offset = (difficulty*20+level)*10,
* difficulties 0..2 (agent/secret/00) only -- 007 times are virtual. */
static u32 geSaveGetTime(const u8 *t, s32 difficulty, s32 level)
{
s32 offset = (difficulty * 20 + level) * 10;
s32 index = offset >> 3;
switch (7 - (offset & 7)) {
case 7: return ((u32)(t[index] & 0xFF) << 2) | ((u32)(t[index + 1] & 0xc0) >> 6);
case 5: return ((u32)(t[index] & 0x3f) << 4) | ((u32)(t[index + 1] & 0xf0) >> 4);
case 3: return ((u32)(t[index] & 0x0f) << 6) | ((u32)(t[index + 1] & 0xfc) >> 2);
case 1: return ((u32)(t[index] & 0x03) << 8) | (u32)(t[index + 1] & 0xFF);
default: return 0;
}
}
static void geSaveSetTime(u8 *t, s32 difficulty, s32 level, u32 newtime)
{
s32 offset = (difficulty * 20 + level) * 10;
s32 index = offset >> 3;
switch (7 - (offset & 7)) {
case 7:
t[index] = (u8)((t[index] & 0x00) | ((newtime >> 2) & 0xFF));
t[index + 1] = (u8)((t[index + 1] & 0x3F) | ((newtime << 6) & 0xC0));
break;
case 5:
t[index] = (u8)((t[index] & 0xC0) | ((newtime >> 4) & 0x3F));
t[index + 1] = (u8)((t[index + 1] & 0x0F) | ((newtime << 4) & 0xF0));
break;
case 3:
t[index] = (u8)((t[index] & 0xF0) | ((newtime >> 6) & 0x0F));
t[index + 1] = (u8)((t[index + 1] & 0x03) | ((newtime << 2) & 0xFC));
break;
case 1:
t[index] = (u8)((t[index] & 0xFC) | ((newtime >> 8) & 0x03));
t[index + 1] = (u8)(newtime & 0xFF);
break;
}
}
static void geEepromPatchAllCheats(u8 *buf)
{
/* buf holds the five save slots read from block 4. The sixth local slot
* is only a CRC end-boundary for the last one (fileGenerateCRC reads
* [A, B), so its contents are irrelevant). */
static ge_save_slot slots[6];
memcpy(slots, buf, sizeof(ge_save_slot) * 5);
memset(&slots[5], 0, sizeof(ge_save_slot));
int changed = 0;
for (int i = 0; i < 5; i++) {
/* D259: a fresh ge007.eep is zero-filled, so every slot reads as
* all-zero. Normally such a slot fails fileValidateSaves' CRC and is
* reset to BLANKSAVEDATA (music_vol/sfx_vol = 0xFF); but this patch
* gives the slot a valid CRC below, so it survives with volume 0
* -> silence. Emulate fileResetSave: seed max volume on all-zero
* slots only -- real saves (incl. a deliberately muted one) keep
* their bytes. */
{
const u8 *raw = (const u8 *)&slots[i];
int allzero = 1;
for (int b = 0; b < (int)sizeof(ge_save_slot); b++)
if (raw[b]) { allzero = 0; break; }
if (allzero && (slots[i].music_vol != 0xFF ||
slots[i].sfx_vol != 0xFF)) {
slots[i].music_vol = 0xFF;
slots[i].sfx_vol = 0xFF;
changed = 1;
}
}
/* Cheat ids are level ids 0..19 (CHEAT_INPUT_BUFFER_SIZE == 20):
* bits 0-7 in _1, 8-15 in _2, 16-19 in the low nibble of _3. */
if (slots[i].unlocked_cheats_1 != 0xFF ||
slots[i].unlocked_cheats_2 != 0xFF ||
(slots[i].unlocked_cheats_3 & 0x0F) != 0x0F) {
slots[i].unlocked_cheats_1 = 0xFF;
slots[i].unlocked_cheats_2 = 0xFF;
slots[i].unlocked_cheats_3 |= 0x0F;
changed = 1;
}
/* Fully-completed campaign: fill empty completion times with the
* max (0x3FF). Needed so mission select still works while any cheat
* is active (see header comment); player records are preserved. */
for (s32 diff = 0; diff < 3; diff++) {
for (s32 lvl = 0; lvl < 20; lvl++) {
if (geSaveGetTime(slots[i].times, diff, lvl) == 0) {
geSaveSetTime(slots[i].times, diff, lvl, 0x3FF);
changed = 1;
}
}
}
}
if (!changed) return;
for (int i = 0; i < 5; i++)
fileGenerateCRC(&slots[i].completion_bitflags, (u8 *)&slots[i + 1],
&slots[i]);
memcpy(buf, slots, sizeof(ge_save_slot) * 5);
}
static s32 geEepromRW(u8 block, u8 *buf, int nbytes, int write)
{
u32 off = (u32)block * 8;
@@ -1339,28 +1085,6 @@ static s32 geEepromRW(u8 block, u8 *buf, int nbytes, int write)
geEepromStore();
} else {
memcpy(buf, s_eeprom + off, nbytes);
/* D297: one-time migration of pre-D284 slot CRCs (port/src/legacycrc.c).
* This IS the read fileValidateSaves performs (block 4, five slots), so
* the re-stamped checksums are exactly what the game validates — no
* ordering hazard. Runs BEFORE geEepromPatchAllCheats so that only
* pristine+migrated bytes are ever persisted; AllUnlocked patches stay
* read-time-only as before. Block-0 smallSave is deliberately untouched
* (factory seal, see D297). */
if (block == 4 && nbytes == (int)(sizeof(ge_save_slot) * 5)) {
int migrated = geLegacyCrcMaybeMigrateSlots(buf);
if (migrated) {
memcpy(s_eeprom + off, buf, nbytes);
geEepromStore();
sysLogPrintf(LOG_INFO,
"eeprom: D297-migrated %d pre-D284 save slot(s) to current CRC",
migrated);
}
}
/* fileValidateSaves' block read: five save slots from block 4. */
if (portAllUnlocked && block == 4 &&
nbytes == (int)(sizeof(ge_save_slot) * 5)) {
geEepromPatchAllCheats(buf);
}
}
return 0;
}
@@ -1394,12 +1118,6 @@ void osSpTaskLoad(OSTask *t) { (void)t; /* nothing to load on the host */ }
void osSpTaskStartGo(OSTask *t)
{
if (g_determTraceEnabled && !g_determTaskEverRun) {
sysLogPrintf(LOG_NOTE,
"DETERMTRACE seq=%u ticks=%u event=first_task_run tasktype=%d",
++g_determTraceSeq, g_determTicks, (int)t->t.type);
}
g_determTaskEverRun = 1; /* GE_DETERM: §D117 M-52 3rd attempt */
if (t->t.type == M_AUDTASK) {
/* Phase 3: execute the audio ucode against t->t.data_ptr (an Acmd
* list). For now the task simply completes; libaudio's amMain still
@@ -1748,45 +1466,3 @@ void viInit(void)
/* VI debug message queue (normally src/vi.c, EXCLUDED). fr.c references it. */
OSMesgQueue vi_c_debug_MQ;
/* ------------------------------------------------------------------------ */
/* D202/M-70: serialized trace writer for the GE_AUDIOTRACE / WIRE probes. */
/* See port/include/audiotrace.h for why (cross-thread mid-line */
/* interleaving corrupted the M-69 corpus). */
/* ------------------------------------------------------------------------ */
#include <stdarg.h>
#include <stdio.h>
static volatile int geTraceLock = 0;
struct geTraceFile { const char *path; FILE *f; };
static struct geTraceFile geTraceFiles[4];
void geTracePrintf(const char *path, const char *fmt, ...)
{
struct geTraceFile *tf = NULL;
va_list ap;
while (__sync_lock_test_and_set(&geTraceLock, 1)) { /* spin */ }
for (int i = 0; i < (int)(sizeof(geTraceFiles) / sizeof(geTraceFiles[0])); i++) {
if (!geTraceFiles[i].path) {
if (!tf) tf = &geTraceFiles[i];
continue;
}
if (strcmp(geTraceFiles[i].path, path) == 0) { tf = &geTraceFiles[i]; break; }
}
if (tf && !tf->f) {
tf->path = path;
tf->f = fopen(path, "a");
if (tf->f) setvbuf(tf->f, NULL, _IONBF, 0);
}
if (tf && tf->f) {
va_start(ap, fmt);
vfprintf(tf->f, fmt, ap);
va_end(ap);
}
__sync_lock_release(&geTraceLock);
}
+4 -90
View File
@@ -33,7 +33,6 @@
#include "mixer.h"
#include "crash.h"
#include "thread_config.h"
#include "game/language.h" /* D295/M-148: JPN glyph-cache types + j_text_trigger */
/* Defined in the game (src/init.c). The port calls into the real game entry. */
extern void mainproc(void *args);
@@ -55,10 +54,8 @@ static void portPrintVersion(void)
printf("GoldenEye 007 PC port\n"
" rom : %s\n"
" platform : %s\n"
" build : %s (%s)\n"
" origin : %s\n",
GE007_ROMID, GE007_TARGET_PLATFORM, GE007_VERSION_HASH,
GE007_VERSION_CODENAME, GE007_ORIGIN_URL);
" build : %s\n",
GE007_ROMID, GE007_TARGET_PLATFORM, GE007_VERSION_HASH);
}
static void portPrintHelp(const char *argv0)
@@ -67,9 +64,6 @@ static void portPrintHelp(const char *argv0)
printf("\nusage: %s [options] [-level_XX]\n\n"
" --help this message\n"
" --version build id only\n"
" -fresh wipe playtest data + config before starting\n"
" (removes ge007.eep save and ge007.ini; the\n"
" ini is re-written with defaults on exit)\n"
" -level_XX boot straight into a solo level (per-level\n"
" memory pools are auto-injected)\n\n"
"config: ge007.ini in the data dir (written on first run).\n\n"
@@ -98,67 +92,15 @@ int main(int argc, char **argv)
}
sysLogPrintf(LOG_INFO, "GoldenEye 007 PC port starting "
"(%s, %s %s) -- %s",
GE007_ROMID, GE007_VERSION_HASH, GE007_VERSION_CODENAME,
GE007_ORIGIN_URL);
"(%s, %s)", GE007_ROMID, GE007_VERSION_HASH);
/* Crash handler first, so any failure below is debuggable. */
crashInit();
/* -fresh: clean-slate run -- drop the file-backed EEPROM save and the
* ini before anything reads them (configLoad below, eeprom's lazy load).
* sysResolvePath returns one static buffer, so copy each path out. */
if (sysArgCheck("-fresh") || sysArgCheck("--fresh")) {
char ini[1024], eep[1024];
strncpy(ini, sysResolvePath("$S/ge007.ini"), sizeof(ini) - 1);
ini[sizeof(ini) - 1] = 0;
strncpy(eep, sysResolvePath("$S/ge007.eep"), sizeof(eep) - 1);
eep[sizeof(eep) - 1] = 0;
if (remove(ini) == 0) sysLogPrintf(LOG_INFO, "fresh: removed %s", ini);
else sysLogPrintf(LOG_NOTE, "fresh: no %s to remove", ini);
if (remove(eep) == 0) sysLogPrintf(LOG_INFO, "fresh: removed %s", eep);
else sysLogPrintf(LOG_NOTE, "fresh: no %s to remove", eep);
}
/* 1. Platform + config + filesystem. Steam Deck / SteamOS: seed the
* first-run preset (native 1280x800 fullscreen, MSAA 4, longer draw/LOD
* distances) before the load so a missing ini saves these values; an
* existing ini always wins. */
if (getenv("STEAMOS")) {
sysLogPrintf(LOG_INFO, "video: SteamOS detected; applying Steam Deck first-run defaults");
videoApplySteamOSDefaults();
}
/* 1. Platform + config + filesystem. */
configLoad();
atexit(portAtExit); /* persist config + window geometry on clean exit */
/* 1a. D257: Game.AllUnlocked (default OFF; F10 'All unlocked' enables)
* -- when set, seed the game's own RAM unlock flags so mission
* select offers every solo level at every
* difficulty plus 007 mode, with no save data required. Both are
* plain s32 globals in src/game/debugmenu_handler.c (compiled because
* the PC build defines LEFTOVERDEBUG); file2.c's
* fileIsStageUnlockedAtDifficulty() and front.c's 007-mode gate OR
* them in ahead of the EEPROM completion bits. Port-layer memory
* writes only -- no game-logic edits (AGENTS rule 2), same class as
* the existing GE_UNLOCK_ALL getenv hook in the getter. No active
* cheats (invincibility / all guns) are enabled; weapons remain
* per-mission pickups as on the N64. Separately, the eep shim
* (libultra.c geEepromPatchAllCheats) sets every progression-gated
* cheat-unlock bit in the save block at read time (per-slot CRC
* recomputed via the game's own fileGenerateCRC), so the cheat
* menu is fully populated without completed levels. */
{
extern s32 portAllUnlocked; /* port/src/video.c */
extern s32 debug_enable_all_levels_flag; /* src/game/debugmenu_handler.c */
extern s32 debug_007_unlock_flag; /* ditto */
if (portAllUnlocked) {
debug_enable_all_levels_flag = 1;
debug_007_unlock_flag = 1;
sysLogPrintf(LOG_INFO, "all-unlocked: RAM unlock flags seeded "
"(Game.AllUnlocked=1)");
}
}
/* 2. Load the ROM and map segments. */
if (romdataInit() != 0) {
sysLogPrintf(LOG_ERROR, "Failed to load ROM (expected a .z64 in the "
@@ -205,34 +147,6 @@ int main(int argc, char **argv)
* audio threads and never returns in practice. */
sysLogPrintf(LOG_INFO, "ROM mapped at 0x%08X (%u bytes); starting game",
(unsigned)0x10000000, romdataGetRomSize());
/* 4a. D295/M-148: non-JP builds never allocate the JPN glyph cache (langInit
* gates it on j_text_trigger), but textRender's high-bit path calls
* langGetJpnCharPixels unconditionally and indexes the NULL global
* (language.c:320). Seed both globals with static port buffers, using the
* exact same construction as langInit's JP path (language.c:247-253), so a
* stray byte >= 0x80 takes the normal cache-miss path (romCopy from the JP
* font segment -> garbage glyph) instead of NULL-deref. Runs before
* osStartThread => no race; langInit (non-JP) never touches these, and if a
* debug -j flag ever flips j_text_trigger, its real allocation supersedes
* this stub harmlessly. */
{
extern struct jpncharpixels *g_JpnCharCachePixels; /* src/game/language.c:27 */
extern struct jpncacheitem *g_JpnCacheCacheItems; /* src/game/language.c:29 */
if (!j_text_trigger) {
static struct jpncharpixels jpnPixels[124 * 8]; /* 0x2E80 bytes, langInit's size */
static struct jpncacheitem jpnItems[124]; /* 124 slots; langInit over-allocates 0x100 (128) but only ever uses 0x7C */
s32 i;
_Static_assert(sizeof(jpnPixels) == 0x2E80,
"JPN pixel stub size must match langInit (language.c:248)");
for (i = 0; i < 124; i++) {
jpnItems[i].ttl = 0;
jpnItems[i].codepoint = -1;
}
g_JpnCharCachePixels = jpnPixels;
g_JpnCacheCacheItems = jpnItems;
}
}
portKernelInit();
osCreateThread(&mainThread, MAIN_THREAD_ID, &mainproc, NULL, NULL,
MAIN_THREAD_PRIORITY);
+14 -518
View File
@@ -1,535 +1,31 @@
/*
* Audio mixing — software implementation of GE's RSP audio ucode opcodes.
* Audio mixing.
*
* On the N64 the RSP audio ucode (aspMain) executes an acmd list built by
* libaudio/naudio to do ADPCM decode, resampling, and the final envelope
* mix. The RSP is never run on PC (port/src/ucode.c); instead every aXxx
* macro in include/PR/abi.h is redefined (port/include/mixer.h, included
* under #ifdef PORT) to call one of the Impl functions below immediately,
* against a small software "DMEM" scratch buffer — the Perfect Dark PC
* port's macro-swap trick (docs/dev/AUDIO-PLAN.md), adapted to GE's ABI.
* On the N64 the RSP audio ucode does final mix / sample handling before the
* AI DMA. With the RSP emulated in software, this runs on the CPU.
*
* GE uses the classic IDO libaudio ABI, not PD's differently-shaped naudio
* "New" ABI — verified against every call site in src/libultra/audio/*.c
* and src/libultrare/audio/{env,reverb}.c. The DSP algorithms below (ADPCM
* decode, linear resample, linear envelope mix) are the same Nintendo/SGI
* ucode math PD's port/src/mixer.c ported (scalar path); only the ABI/
* addressing layer here is GE-specific.
* Modelled on the PD port's port/src/mixer.c (~720 lines).
*
* GE's aSetBuffer(flags, in, out, count) sets a small persistent context
* that several opcodes read instead of taking DMEM addresses/counts
* directly (see MixerCtx below) — reconstructed from reading every call
* site (docs/dev/findings.md D199), not from RSP disassembly.
* STATUS: scaffolding stub — implement during Phase 3. The exact interface
* depends on how GE's libaudio output is wired (see src/audi.c).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <PR/abi.h>
#include <PR/os.h>
#include "system.h"
/* D202 diag (temporary): per-opcode DMEM address/context trace, gated by
* GE_MIXERTRACE=1, to test the M-56 "concurrent voices share/clobber DMEM
* context" hypothesis directly against a live repro. Remove once
* root-caused. */
extern char *getenv(const char *);
static FILE *sMixerTraceFile = NULL;
static int sMixerTraceChecked = 0;
static int mixerTraceOn(void)
{
if (!sMixerTraceChecked) {
sMixerTraceChecked = 1;
if (getenv("GE_MIXERTRACE")) {
sMixerTraceFile = fopen("mixertrace.log", "a");
if (sMixerTraceFile) setvbuf(sMixerTraceFile, NULL, _IONBF, 0);
}
}
return sMixerTraceFile != NULL;
}
#define MTRACE(...) do { if (mixerTraceOn()) fprintf(sMixerTraceFile, __VA_ARGS__); } while (0)
#include "platform.h"
#include "system.h"
#include "mixer.h"
#include "audio.h"
/* Largest DMEM address GE's audio call sites use is AL_AUX_R_OUT (2048) +
* up to AL_MAX_RSP_SAMPLES (160) samples * 2 bytes; round up generously. */
#define DMEM_SIZE 4096
static u8 sDmem[DMEM_SIZE];
#define DMEM_U8(a) (sDmem + (a))
#define DMEM_S16(a) ((s16 *)(sDmem + (a)))
static inline s16 mixerClamp16(s32 v)
{
if (v < -0x8000) return -0x8000;
if (v > 0x7fff) return 0x7fff;
return (s16)v;
}
/*
* Persistent context set by aSetBuffer, read by opcodes that don't carry
* their own DMEM addresses/counts. Reconstructed from call-site pairs:
*
* resample.c: aSetBuffer(0, inp, *outp, outCnt<<1); aResample(...)
* load.c: aSetBuffer(0, dmemAddr, 0, nbytes...); aLoadBuffer(dram)
* reverb.c: aSetBuffer(0, 0, buff, count<<1); aSaveBuffer(dram)
* mainbus.c: aSetBuffer(0, 0, 0, outCount<<1); aMix(...); aMix(...)
* save.c: aSetBuffer(0, 0, 0, outCount<<1); aInterleave(L, R)
* env.c: aSetBuffer(A_MAIN, *inp, AL_MAIN_L_OUT+*outp, outCount<<1);
* aSetBuffer(A_AUX, AL_MAIN_R_OUT+*outp, AL_AUX_L_OUT+*outp,
* AL_AUX_R_OUT+*outp);
* aEnvMixer(...)
*
* i.e. LoadBuffer/ADPCMdec/Resample read `in` as their DMEM source (or, for
* LoadBuffer, as the load destination); Resample/EnvMixer read `out` as a
* destination too; SaveBuffer/Interleave read `out` as their DMEM source;
* Mix/DMEMMove/ClearBuffer take explicit addresses and don't touch this
* context at all. The A_AUX-flagged aSetBuffer call packs three more
* envmixer-only DMEM addresses (dry-R, wet-L, wet-R) — its third argument
* is a DMEM address here, not a byte count, unlike every other opcode's use
* of aSetBuffer's `count` field.
*/
static struct {
u16 in;
u16 out;
u32 count;
u16 dryR;
u16 wetL;
u16 wetR;
} sCtx;
void aSetBufferImpl(u32 flags, u16 i, u16 o, u16 c)
{
if (flags & A_AUX) {
sCtx.dryR = i;
sCtx.wetL = o;
sCtx.wetR = c;
} else {
sCtx.in = i;
sCtx.out = o;
sCtx.count = c;
}
MTRACE("[SETBUF] flags=%u i=%u o=%u c=%u\n", flags, i, o, c);
}
void aClearBufferImpl(u16 addr, u32 count)
{
/* D202/M-65 diag (temporary): alMainBusPull's clear of AL_MAIN_L_OUT is
* the first opcode of every audio frame, so it is an exact frame
* boundary. GE_DMEMWIPE=1 zeroes the scratch region below AL_MAIN_L_OUT
* (AL_TEMP_0/1/2, AL_DECODER_IN, AL_RESAMPLER_OUT) there. Nothing on real
* hardware may read those without writing them first in the same command
* list, so this must be a no-op; if it silences the stuck drone, some
* opcode is reading scratch DMEM left over from the previous frame.
* Remove once root-caused. */
if (addr == 1088 /* AL_MAIN_L_OUT */ && getenv("GE_DMEMWIPE")) {
memset(sDmem, 0, sizeof(sDmem));
}
memset(DMEM_U8(addr), 0, count);
}
void aLoadBufferImpl(u32 dramAddr)
{
if (mixerTraceOn()) {
const u8 *src = (const u8 *)osPhysicalToVirtual(dramAddr);
u32 n = (sCtx.count > 72 ? 72 : sCtx.count);
u32 k;
fprintf(sMixerTraceFile, "[LOADBUF] dram=0x%08x -> in=%u count=%u bytes=", dramAddr, sCtx.in, sCtx.count);
for (k = 0; k < n; k++) fprintf(sMixerTraceFile, "%02x", src[k]);
fprintf(sMixerTraceFile, "\n");
}
memcpy(DMEM_U8(sCtx.in), osPhysicalToVirtual(dramAddr), sCtx.count);
}
void aSaveBufferImpl(u32 dramAddr)
{
memcpy(osPhysicalToVirtual(dramAddr), DMEM_U8(sCtx.out), sCtx.count);
(void)0;
}
void aDMEMMoveImpl(u16 in, u16 out, u32 count)
{
memmove(DMEM_U8(out), DMEM_U8(in), count);
}
void aSegmentImpl(u32 seg, u32 base)
{
/* No-op on PC: GE's audio DMA addresses are already resolved via
* osVirtualToPhysical before reaching us (D199); segment/base never
* factor into DMEM addressing here. */
(void)seg; (void)base;
}
/* ------------------------------------------------------------------------ */
/* ADPCM */
/* ------------------------------------------------------------------------ */
static s16 sAdpcmTable[8][2][8];
static ADPCM_STATE *sAdpcmLoopState;
void aLoadADPCMImpl(u32 count, u32 dramAddr)
{
/* `count` is GE's bookSize in bytes (2 * order * npredictors * 8);
* order=2 always for this format, matching sAdpcmTable's shape. */
u32 n = count;
void *src = osPhysicalToVirtual(dramAddr);
if (n > sizeof(sAdpcmTable)) n = sizeof(sAdpcmTable);
memcpy(sAdpcmTable, src, n);
if (mixerTraceOn()) {
int p, q, r;
fprintf(sMixerTraceFile, "[LOADADPCM] count=%u dram=%p book=", count, src);
for (p = 0; p < 8; p++) for (q = 0; q < 2; q++) for (r = 0; r < 8; r++)
fprintf(sMixerTraceFile, "%d,", sAdpcmTable[p][q][r]);
fprintf(sMixerTraceFile, "\n");
}
}
void aSetLoopImpl(u32 stateAddr)
{
sAdpcmLoopState = (ADPCM_STATE *)osPhysicalToVirtual(stateAddr);
}
void aADPCMdecImpl(u32 flags, u32 stateAddr)
{
ADPCM_STATE *state = (ADPCM_STATE *)osPhysicalToVirtual(stateAddr);
MTRACE("[ADPCMDEC] flags=%u state=%p in=%u out=%u count=%u book0=%d\n",
flags, (void *)state, sCtx.in, sCtx.out, sCtx.count, sAdpcmTable[0][0][0]);
u8 *in = DMEM_U8(sCtx.in);
s16 *out = DMEM_S16(sCtx.out);
s32 nbytes = (s32)((sCtx.count + 31) & ~31u); /* round up to 16-sample (32-byte) chunks */
if (flags & A_INIT) {
memset(out, 0, 16 * sizeof(s16));
} else if (flags & A_LOOP) {
memcpy(out, sAdpcmLoopState, 16 * sizeof(s16));
} else {
memcpy(out, state, 16 * sizeof(s16));
}
out += 16;
while (nbytes > 0) {
int shift = *in >> 4;
int tableIndex = *in++ & 0xf;
s16 (*tbl)[8] = sAdpcmTable[tableIndex];
int i, j, k;
for (i = 0; i < 2; i++) {
s16 ins[8];
s16 prev1 = out[-1];
s16 prev2 = out[-2];
for (j = 0; j < 4; j++) {
ins[j * 2] = (s16)((((*in >> 4) << 28) >> 28) << shift);
ins[j * 2 + 1] = (s16)((((*in++ & 0xf) << 28) >> 28) << shift);
}
for (j = 0; j < 8; j++) {
s32 acc = tbl[0][j] * prev2 + tbl[1][j] * prev1 + ((s32)ins[j] << 11);
for (k = 0; k < j; k++) {
acc += tbl[1][(j - k) - 1] * ins[k];
}
acc >>= 11;
*out++ = mixerClamp16(acc);
}
}
nbytes -= 16 * (s32)sizeof(s16);
}
memcpy(state, out - 16, 16 * sizeof(s16));
if (mixerTraceOn()) {
s16 *dumpOut = DMEM_S16(sCtx.out) + 16; /* skip the 16-sample history/init lead-in */
u32 n = (sCtx.count > 64 ? 64 : sCtx.count);
u32 k;
fprintf(sMixerTraceFile, "[PCMOUT] book0=%d first-decoded-frame[0..%u]=", sAdpcmTable[0][0][0], n / 2 - 1);
for (k = 0; k < n / 2; k++) fprintf(sMixerTraceFile, "%d,", dumpOut[k]);
fprintf(sMixerTraceFile, "\n");
}
}
/* ------------------------------------------------------------------------ */
/* Resample (linear, N64 64-phase table) */
/* ------------------------------------------------------------------------ */
static const s16 sResampleTable[64][4] = {
{0x0c39, 0x66ad, 0x0d46, 0xffdf}, {0x0b39, 0x6696, 0x0e5f, 0xffd8},
{0x0a44, 0x6669, 0x0f83, 0xffd0}, {0x095a, 0x6626, 0x10b4, 0xffc8},
{0x087d, 0x65cd, 0x11f0, 0xffbf}, {0x07ab, 0x655e, 0x1338, 0xffb6},
{0x06e4, 0x64d9, 0x148c, 0xffac}, {0x0628, 0x643f, 0x15eb, 0xffa1},
{0x0577, 0x638f, 0x1756, 0xff96}, {0x04d1, 0x62cb, 0x18cb, 0xff8a},
{0x0435, 0x61f3, 0x1a4c, 0xff7e}, {0x03a4, 0x6106, 0x1bd7, 0xff71},
{0x031c, 0x6007, 0x1d6c, 0xff64}, {0x029f, 0x5ef5, 0x1f0b, 0xff56},
{0x022a, 0x5dd0, 0x20b3, 0xff48}, {0x01be, 0x5c9a, 0x2264, 0xff3a},
{0x015b, 0x5b53, 0x241e, 0xff2c}, {0x0101, 0x59fc, 0x25e0, 0xff1e},
{0x00ae, 0x5896, 0x27a9, 0xff10}, {0x0063, 0x5720, 0x297a, 0xff02},
{0x001f, 0x559d, 0x2b50, 0xfef4}, {0xffe2, 0x540d, 0x2d2c, 0xfee8},
{0xffac, 0x5270, 0x2f0d, 0xfedb}, {0xff7c, 0x50c7, 0x30f3, 0xfed0},
{0xff53, 0x4f14, 0x32dc, 0xfec6}, {0xff2e, 0x4d57, 0x34c8, 0xfebd},
{0xff0f, 0x4b91, 0x36b6, 0xfeb6}, {0xfef5, 0x49c2, 0x38a5, 0xfeb0},
{0xfedf, 0x47ed, 0x3a95, 0xfeac}, {0xfece, 0x4611, 0x3c85, 0xfeab},
{0xfec0, 0x4430, 0x3e74, 0xfeac}, {0xfeb6, 0x424a, 0x4060, 0xfeaf},
{0xfeaf, 0x4060, 0x424a, 0xfeb6}, {0xfeac, 0x3e74, 0x4430, 0xfec0},
{0xfeab, 0x3c85, 0x4611, 0xfece}, {0xfeac, 0x3a95, 0x47ed, 0xfedf},
{0xfeb0, 0x38a5, 0x49c2, 0xfef5}, {0xfeb6, 0x36b6, 0x4b91, 0xff0f},
{0xfebd, 0x34c8, 0x4d57, 0xff2e}, {0xfec6, 0x32dc, 0x4f14, 0xff53},
{0xfed0, 0x30f3, 0x50c7, 0xff7c}, {0xfedb, 0x2f0d, 0x5270, 0xffac},
{0xfee8, 0x2d2c, 0x540d, 0xffe2}, {0xfef4, 0x2b50, 0x559d, 0x001f},
{0xff02, 0x297a, 0x5720, 0x0063}, {0xff10, 0x27a9, 0x5896, 0x00ae},
{0xff1e, 0x25e0, 0x59fc, 0x0101}, {0xff2c, 0x241e, 0x5b53, 0x015b},
{0xff3a, 0x2264, 0x5c9a, 0x01be}, {0xff48, 0x20b3, 0x5dd0, 0x022a},
{0xff56, 0x1f0b, 0x5ef5, 0x029f}, {0xff64, 0x1d6c, 0x6007, 0x031c},
{0xff71, 0x1bd7, 0x6106, 0x03a4}, {0xff7e, 0x1a4c, 0x61f3, 0x0435},
{0xff8a, 0x18cb, 0x62cb, 0x04d1}, {0xff96, 0x1756, 0x638f, 0x0577},
{0xffa1, 0x15eb, 0x643f, 0x0628}, {0xffac, 0x148c, 0x64d9, 0x06e4},
{0xffb6, 0x1338, 0x655e, 0x07ab}, {0xffbf, 0x11f0, 0x65cd, 0x087d},
{0xffc8, 0x10b4, 0x6626, 0x095a}, {0xffd0, 0x0f83, 0x6669, 0x0a44},
{0xffd8, 0x0e5f, 0x6696, 0x0b39}, {0xffdf, 0x0d46, 0x66ad, 0x0c39}
};
void aResampleImpl(u32 flags, u16 pitch, u32 stateAddr)
{
RESAMPLE_STATE *stateBuf = (RESAMPLE_STATE *)osPhysicalToVirtual(stateAddr);
s16 *state = (s16 *)stateBuf;
MTRACE("[RESAMPLE] flags=%u pitch=%u state=%p in=%u out=%u count=%u\n",
flags, pitch, (void *)stateBuf, sCtx.in, sCtx.out, sCtx.count);
s16 tmp[16];
s16 *inInitial = DMEM_S16(sCtx.in);
s16 *in = inInitial;
s16 *out = DMEM_S16(sCtx.out);
s32 nbytes = (s32)((sCtx.count + 15) & ~15u);
u32 pitchAccumulator;
s32 i;
if (flags & A_INIT) {
memset(tmp, 0, 5 * sizeof(s16));
} else {
memcpy(tmp, state, 16 * sizeof(s16));
}
in -= 4;
pitchAccumulator = (u16)tmp[4];
memcpy(in, tmp, 4 * sizeof(s16));
do {
for (i = 0; i < 8; i++) {
const s16 *tbl = sResampleTable[(pitchAccumulator * 64) >> 16];
s32 sample = ((in[0] * tbl[0] + 0x4000) >> 15) +
((in[1] * tbl[1] + 0x4000) >> 15) +
((in[2] * tbl[2] + 0x4000) >> 15) +
((in[3] * tbl[3] + 0x4000) >> 15);
*out++ = mixerClamp16(sample);
pitchAccumulator += (u32)pitch << 1;
in += pitchAccumulator >> 16;
pitchAccumulator %= 0x10000;
}
nbytes -= 8 * (s32)sizeof(s16);
} while (nbytes > 0);
state[4] = (s16)pitchAccumulator;
memcpy(state, in, 4 * sizeof(s16));
i = (s32)((in - inInitial + 4) & 7);
in -= i;
if (i != 0) i = -8 - i;
state[5] = (s16)i;
memcpy(state + 8, in, 8 * sizeof(s16));
}
/* ------------------------------------------------------------------------ */
/* Interleave / DMEM mix */
/* ------------------------------------------------------------------------ */
void aInterleaveImpl(u16 l, u16 r)
{
const s16 *lp = DMEM_S16(l);
const s16 *rp = DMEM_S16(r);
s16 *d = DMEM_S16(sCtx.out);
u32 n = sCtx.count >> 1; /* mono sample count */
u32 i;
for (i = 0; i < n; i++) {
*d++ = *lp++;
*d++ = *rp++;
}
}
void aMixImpl(u32 flags, u16 gain, u16 in, u16 out)
{
const s16 *inp = DMEM_S16(in);
s16 *outp = DMEM_S16(out);
u32 n = sCtx.count >> 1;
u32 i;
(void)flags;
for (i = 0; i < n; i++) {
s32 sample = ((s32)*outp * 0x7fff + (s32)*inp++ * (s16)gain + 0x4000) >> 15;
*outp++ = mixerClamp16(sample);
}
}
/* ------------------------------------------------------------------------ */
/* Volume / envelope mixer */
/* ------------------------------------------------------------------------ */
static struct {
s16 volCur[2];
s16 volTgt[2];
s32 volRate[2];
s16 dryamt, wetamt;
} sVol;
void aSetVolumeImpl(u32 flags, u16 v, u16 t, u16 r)
{
if (flags & A_AUX) {
sVol.dryamt = (s16)v;
sVol.wetamt = (s16)r;
/* D202/M-65 diag (temporary): GE_NOWET=1 stops any signal entering
* the reverb send. The reverb delay lines are the only audio state
* that survives across frames in DRAM, and aPoleFilterImpl -- the
* damping in that feedback path -- is an unimplemented no-op here
* (D199). If muting the send silences the stuck drone, the drone is
* an undamped reverb feedback loop, not a stuck voice.
* Remove once root-caused. */
if (getenv("GE_NOWET")) {
sVol.wetamt = 0;
}
} else if (flags & A_VOL) {
int ch = (flags & A_LEFT) ? 0 : 1;
sVol.volCur[ch] = (s16)v;
} else {
int ch = (flags & A_LEFT) ? 0 : 1;
sVol.volTgt[ch] = (s16)v;
sVol.volRate[ch] = ((u32)t << 16) | (u16)r;
}
}
/* D202/M-67 diag (temporary): GE_VOICEDUMP=1 writes each voice's resampled
* mono stream (the aEnvMixer input, i.e. post-resample/pre-envelope) to
* voicedump.raw as records of [u32 stateAddr][u32 nSamples][u64 us]
* [s16 x nSamples], where us is sysGetMicroseconds() at mix time so records
* can be correlated with the timestamped [WIRE] (load.c) and [AUDIOTRACE]
* (snd.c) lines. Offline, each record is matched against all 261 ROM SFX
* decodes (pitch-resampled per keymap) to prove exactly which sample data a
* voice played, with no music/reverb masking. Remove once D202 closes. */
static FILE *s_voiceDumpFile = NULL;
void aEnvMixerImpl(u32 flags, u32 stateAddr)
{
struct {
s32 t[2];
s32 rate[2];
s16 tgt[2];
s16 voldry;
s16 volwet;
} *saved = (void *)osPhysicalToVirtual(stateAddr);
MTRACE("[ENVMIX] flags=%u state=%p in=%u out=%u dryR=%u wetL=%u wetR=%u count=%u\n",
flags, (void *)saved, sCtx.in, sCtx.out, sCtx.dryR, sCtx.wetL, sCtx.wetR, sCtx.count);
const s16 *in = DMEM_S16(sCtx.in);
if (getenv("GE_VOICEDUMP")) {
if (!s_voiceDumpFile)
s_voiceDumpFile = fopen("voicedump.raw", "wb");
if (s_voiceDumpFile) {
u32 hdr[2] = { stateAddr, (u32)(sCtx.count >> 1) };
u64 us = sysGetMicroseconds();
fwrite(hdr, 4, 2, s_voiceDumpFile);
fwrite(&us, 8, 1, s_voiceDumpFile);
fwrite(in, sizeof(s16), sCtx.count >> 1, s_voiceDumpFile);
}
}
s16 *dry[2] = { DMEM_S16(sCtx.out), DMEM_S16(sCtx.dryR) };
s16 *wet[2] = { DMEM_S16(sCtx.wetL), DMEM_S16(sCtx.wetR) };
u32 nsamples = sCtx.count >> 1;
s32 t[2], tgt[2], rate[2];
s16 voldry, volwet;
u32 i;
int j;
if (flags & A_INIT) {
for (j = 0; j < 2; j++) {
t[j] = sVol.volCur[j] << 16;
rate[j] = sVol.volRate[j] >> 3;
tgt[j] = sVol.volTgt[j] << 16;
}
voldry = sVol.dryamt;
volwet = sVol.wetamt;
} else {
for (j = 0; j < 2; j++) {
t[j] = saved->t[j];
rate[j] = saved->rate[j];
tgt[j] = saved->tgt[j] << 16;
}
voldry = saved->voldry;
volwet = saved->volwet;
}
for (i = 0; i < nsamples; i++) {
s16 gain[4];
s16 vol[2];
s16 insamp = in[i];
for (j = 0; j < 2; j++) {
t[j] += rate[j];
if ((rate[j] <= 0 && t[j] <= tgt[j]) || (rate[j] > 0 && t[j] >= tgt[j])) {
t[j] = tgt[j];
rate[j] = 0;
}
vol[j] = (s16)(t[j] >> 16);
}
gain[0] = mixerClamp16(((s32)vol[0] * voldry + 0x4000) >> 15);
gain[1] = mixerClamp16(((s32)vol[1] * voldry + 0x4000) >> 15);
gain[2] = mixerClamp16(((s32)vol[0] * volwet + 0x4000) >> 15);
gain[3] = mixerClamp16(((s32)vol[1] * volwet + 0x4000) >> 15);
dry[0][i] = mixerClamp16(dry[0][i] + (((s32)insamp * gain[0]) >> 15));
dry[1][i] = mixerClamp16(dry[1][i] + (((s32)insamp * gain[1]) >> 15));
wet[0][i] = mixerClamp16(wet[0][i] + (((s32)insamp * gain[2]) >> 15));
wet[1][i] = mixerClamp16(wet[1][i] + (((s32)insamp * gain[3]) >> 15));
}
for (j = 0; j < 2; j++) {
saved->t[j] = t[j];
saved->rate[j] = rate[j];
saved->tgt[j] = (s16)(tgt[j] >> 16);
}
saved->voldry = voldry;
saved->volwet = volwet;
}
/* ------------------------------------------------------------------------ */
/* Pole filter (reverb/chorus lowpass) */
/* ------------------------------------------------------------------------ */
void aPoleFilterImpl(u32 flags, u16 gain, u32 stateAddr)
{
/* PC port (D199): identity passthrough — reverb/chorus tone-shaping
* (CUSTOM_FX_PARAMS_N) is not implemented. Affects reverb/chorus
* character only, not silence/garbling (docs/dev/AUDIO-PLAN.md risk 1).
* DMEM in-place buffer (sCtx set by the caller's aSetBuffer(buf,buf,..))
* is left untouched, which is the correct no-op for this filter shape. */
(void)flags; (void)gain; (void)stateAddr;
}
/* ------------------------------------------------------------------------ */
void mixerInit(void)
{
memset(sDmem, 0, sizeof(sDmem));
memset(sAdpcmTable, 0, sizeof(sAdpcmTable));
memset(&sCtx, 0, sizeof(sCtx));
memset(&sVol, 0, sizeof(sVol));
sAdpcmLoopState = NULL;
/* TODO(Phase 3) */
}
void mixerDestroy(void)
{
/* TODO(Phase 3) */
}
void mixerProcess(const s16 *in, u32 frames)
{
/* TODO(Phase 3): process + queue via audioSetNextBuffer(). */
(void)in; (void)frames;
}
+67 -451
View File
@@ -6,12 +6,6 @@
* port-owned config.c variables directly. Live knobs apply immediately; the
* two that need an FBO/window rebuild (MSAA, Fullscreen) are tagged "(restart)".
*
* The panel adapts to whatever 2D space it is drawn in (320x240 in-game vs
* 440x330 on front-end screens -- viSetXY differs) and scrolls when the row
* list outgrows the viewport (wheel / arrows at the edges). Cyclic rows
* (MSAA, texture filter, resolution, toggles) wrap in both directions; the
* manual % rows (draw/LOD distance) are hidden while their "auto" toggle is on.
*
* Text + fill helpers are the game's own (textRender / microcode_constructor /
* gDPFillRectangle) reached by extern -- same pattern input.c uses to read
* current_menu / cursor_h_pos. This is a rendering/UI view, not a logic change.
@@ -45,7 +39,6 @@
#include "video.h"
#include "input.h"
#include "optionsoverlay.h"
#include "../fast3d/gfx_api.h"
/* ---- game symbols (rendering/UI only; see input.c for the same pattern) ---- */
struct font;
@@ -63,24 +56,12 @@ extern s16 viGetY(void);
/* ------------------------------------------------------------------------ */
enum { ROW_TOGGLE, ROW_SLIDER, ROW_ENUM, ROW_MSAA, ROW_RES, ROW_ACTION, ROW_FPSCAP };
enum { ROW_TOGGLE, ROW_SLIDER, ROW_ENUM, ROW_MSAA, ROW_RES };
static const char *const kOnOff[] = { "OFF", "ON", NULL };
static const char *const kTexFilter[] = { "NEAREST", "BILINEAR", "3-POINT", NULL };
static const char *const kCapture[] = { "ALWAYS GRAB", "CLICK-TO-LOCK", NULL };
static const int kMsaaSeq[] = { 1, 2, 4, 8 };
/* D186: the sim's own tick pacemaker is hardcoded to the console's native VI
* rate (60Hz NTSC / 50Hz PAL, port/src/libultra.c) -- Video.FpsCap can only
* throttle down from there, never past it, and throttling it below 30
* throttles game logic itself (video.c already force-uncaps anything under
* 30). A free 0-360 slider therefore had a huge dead zone (every value above
* the console rate is a no-op, every value 1-29 silently snaps to 0) with
* only two states that actually do anything. Exposed as a plain 30/60 toggle
* instead (user ask, 2026-09-18). A third "uncapped" (0, skip the port's own
* frame-pacing wait) state exists at the config level and old inis may still
* have it, but it's dropped from the menu: with VSync on (the default) it's
* indistinguishable from 60, and with VSync off it just burns GPU time
* re-presenting the same simulated frame -- confusing for no real benefit. */
static const int kFpsCapSeq[] = { 30, 60 };
/* Windowed-mode resolution presets. Filtered at init to those that fit the
* desktop; the Resolution row cycles the surviving list. */
@@ -110,129 +91,26 @@ struct Row {
int type; /* CONFIG_OPT_* */
void *ptr;
double cfgMin, cfgMax;
/* Hidden while the option named here is nonzero (a manual % row
* disappears while its "auto" toggle is on). Resolved to hidePtr at init. */
const char *hiddenIfOn;
int *hidePtr;
};
static struct Row rows[] = {
{ "Video.Fullscreen", "Fullscreen", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
{ "__Resolution", "Resolution", ROW_RES, 0, NULL, 0, 0, 0, 0,0,0,0,0 },
{ "Video.VSync", "VSync", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
{ "Video.FpsCap", "Frame cap", ROW_FPSCAP, 0, NULL, 0, 0, 0, 0,0,0,0,0 },
{ "Video.FpsCap", "Frame cap", ROW_SLIDER, 10, NULL, 0, 0, 360, 0,0,0,0,0 },
{ "Video.MSAA", "MSAA", ROW_MSAA, 0, NULL, 1, 0, 0, 0,0,0,0,0 },
{ "Video.TextureFilter", "Texture filter", ROW_ENUM, 1, kTexFilter, 0, 0, 0, 0,0,0,0,0 },
{ "Video.Anisotropy", "Anisotropic", ROW_SLIDER, 1, NULL, 0, 0, 0, 0,0,0,0,0 },
{ "Video.FovScale", "FOV scale %", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 },
{ "Video.WidescreenAuto", "Widescreen auto FOV",ROW_TOGGLE,1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
{ "Video.SafeAreaCrop", "Crop overscan bars", ROW_TOGGLE,1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
{ "Video.DrawDistance", "Draw distance %", ROW_SLIDER, 25, NULL, 0, 0, 0, 0,0,0,0,0, "Video.DrawDistanceAutoFov" },
{ "Video.DrawDistanceAutoFov","Draw dist. auto", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
{ "Video.LodDistance", "LOD distance %", ROW_SLIDER, 25, NULL, 0, 0, 0, 0,0,0,0,0, "Video.LodDistanceAutoFov" },
{ "Video.LodDistanceAutoFov", "LOD dist. auto", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
/* Aim row edits Input.AimModeSens -- the knob the default GEPD aim path
* actually uses (Input.MouseAimSpeed only feeds the legacy velocity-stick
* fallback, so it was inert here). D304: both this row and the turn-speed
* row below now use the (0,0) "inherit the registered clamp" sentinel
* instead of hardcoded uiMin/uiMax -- they were previously (1,80) and
* (0,100), two arbitrary, DIFFERENT, and (for turn speed) outright wrong
* ceilings that didn't even match either row's own backing config clamp
* (both are actually registered 1..500 in input.c) -- fixed as an earlier
* part of this same D304 pass. **Follow-up, same day, user feedback after
* trying the widened sliders live:** exposing BOTH per-mode knobs still
* let a player decouple them into a bad, hard-to-diagnose state (e.g. aim
* mode very slow, hipfire very fast, or vice versa) with no indication
* anything was wrong -- worse than a narrow range. Per the user's request,
* these two rows and the "Link aim/turn sens" toggle that tried to paper
* over the same risk are pulled from the menu entirely; only the single
* master `Input.MouseSensitivity` row below remains player-facing, exactly
* mirroring the reference GEPD/PD injector model this input was ported
* from (GEPD-Edition Mouse Injector's goldeneye.c -- source no longer
* vendored in the public repo, see reference/mouse-injector/README.md) --
* ONE user-facing
* sensitivity value, with the two per-mode constants staying fixed at
* their calibrated (38/50) defaults, never independently player-tunable.
* Config vars + SensLink logic stay in `input.c`/`rowSet()` untouched
* (ini power users can still hand-edit them; existing ini files with
* either key keep working) -- only the menu surface changes, same
* established pattern as the D181/D216 pulled rows below. */
/* { "Input.AimModeSens", "Mouse aim speed", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 }, */
/* { "Input.MouseTurnSpeed", "Mouse turn speed", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 }, */
/* { "Input.SensLink", "Link aim/turn sens",ROW_TOGGLE,1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, */
{ "Input.MouseSensitivity", "Mouse sensitivity",ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 },
{ "Input.MouseAimSpeed", "Mouse aim speed", ROW_SLIDER, 1, NULL, 0, 0, 100, 0,0,0,0,0 },
{ "Input.MouseTurnSpeed", "Mouse turn speed", ROW_SLIDER, 1, NULL, 0, 0, 100, 0,0,0,0,0 },
{ "Input.MouseInvertY", "Mouse invert Y", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
/* D181/Game.ScreenShakeIntensity: user testing (v0.2.1) found the slider
* "basically useless" -- viShake() is only called from explosion.c, so it
* scales explosion shake alone; it never touches the always-on walking
* head-bob or any getting-shot reaction, which is what "Screen shake"
* reads as to a player. Pulled from the menu until it covers all
* screen-shake/view-bob sources, not just explosions. Config var + fr.c
* hook stay in place. */
/* D232: the community "no damage flash" toggle (suppresses the red/green
* hit-flash overlay in bondview2). */
{ "Game.NoHitFlash", "No hit flash", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
/* D216/Game.SkipIntro: user report (v0.2.1 testing) that it breaks audio
* -- pulled from the menu until root-caused. Not exposed to players; the
* config var + lv.c hook stay in place (dead unless an existing ini has
* it set, which no menu path can do any more). Do not re-add without
* fixing the underlying issue first. */
/* D257: everything-unlocked goodie (default ON). Consumed at startup by
* main.c -- applies from the next launch. */
{ "Game.AllUnlocked", "All unlocked", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
/* D293: only quit path used to be the OS window-close / Alt+F4 -- no
* discoverable in-game way to exit, a real gap on Deck/controller-only
* setups. Not config-backed (like __Resolution); activating it exits
* the same way video.c's SDL_QUIT/Alt+F4 handlers already do. */
{ "__QuitToDesktop", "Quit to desktop", ROW_ACTION, 0, NULL, 0, 0, 0, 0,0,0,0,0 },
{ "Input.MouseCaptureMode", "Mouse capture", ROW_TOGGLE, 1, kCapture, 0, 0, 0, 0,0,0,0,0 },
{ "Game.ScreenShakeIntensity","Screen shake", ROW_SLIDER, 0.25, NULL, 0, 0, 3, 0,0,0,0,0 },
};
#define NUM_ROWS ((int)(sizeof(rows) / sizeof(rows[0])))
static int s_inited = 0;
static volatile int s_open = 0;
static int s_sel = 0; /* selection, index into s_visIdx (visible list) */
/* Visible-row list: rows whose hiddenIfOn option is nonzero are omitted
* (manual % rows hide while their auto toggle is on). Rebuilt every frame in
* overlayUpdateVisible(); s_scroll is the first visible-list entry drawn. */
static int s_visIdx[NUM_ROWS];
static int s_visN = 0;
static int s_scroll = 0;
/* D213: optional on-screen FPS readout (PD parity: Video.DisplayFPS). Drawn
* top-right whenever enabled, independent of the F10 panel. Config-only knob
* (kept off the 13-row panel, which is at its layout limit) -- matches PD,
* whose DisplayFPS is also file-only. Default 0 => emit path unchanged =>
* golden dumps byte-identical. */
static int s_showFps = 0;
static char s_fpsText[16] = "";
PD_CONSTRUCTOR static void overlayConfigInit(void)
{
configRegisterInt("Video.DisplayFPS", &s_showFps, 0, 1);
}
/* Sampled once per emitted frame; recomputes the string every ~0.5 s. */
static void fpsTick(void)
{
static uint64_t winStartUs = 0;
static int frames = 0;
uint64_t nowUs = sysGetMicroseconds();
if (winStartUs == 0) {
winStartUs = nowUs;
return;
}
frames++;
uint64_t dtUs = nowUs - winStartUs;
if (dtUs >= 500000) {
int fps = (int)((double)frames * 1e6 / (double)dtUs + 0.5);
snprintf(s_fpsText, sizeof(s_fpsText), "%d FPS", fps);
winStartUs = nowUs;
frames = 0;
}
}
static int s_sel = 0;
/* Layout (game 2D pixel space = viGetX() x viGetY(), ~320x240). Shared by the
* emit path and the mouse hit-testing in optionsOverlayHandleInput().
@@ -243,20 +121,7 @@ static void fpsTick(void)
#define OV_TOP 12
#define OV_LINE 15 /* row pitch (13 rows must fit ~240) */
#define OV_HDR 2 /* header rows above row 0 (title+hint) */
/* i is a VISIBLE-POSITION (already scroll-adjusted by the caller). */
#define OV_ROW_Y(i) (OV_TOP + ((i) + OV_HDR) * OV_LINE)
/* How many rows fit between the header and the bottom edge of whatever 2D
* space we are in right now (320x240 in-game, 440x330 on front-end screens --
* viSetXY differs, see src/game/front.c). More rows than this => scroll. */
static int maxVisibleRows(void)
{
int n = (viGetY() - 6 - OV_TOP - OV_HDR * OV_LINE) / OV_LINE;
if (n < 4) {
n = 4;
}
return n;
}
#define OV_RIGHT (viGetX() - OV_X0) /* right edge for right-aligned text */
#define OV_NUM_W 36 /* reserved width for a slider's number */
#define OV_BAR_X 150
@@ -271,95 +136,18 @@ static void sliderBarSpan(s32 *x0, s32 *x1)
}
}
/* Visible position (0..s_visN-1) the given overlay-space y falls in, or -1.
*
* D304 fix: this used to loop over ALL s_visN entries (0..s_visN-1)
* regardless of which ones are actually scrolled into view and drawn this
* frame -- OV_ROW_Y(p - s_scroll) is a plain linear function of p, so a row
* sitting just past the last visibly-drawn one (pLast, see the emit path)
* still produces a geometrically valid, in-range Y band immediately below
* the panel's real bottom edge. A click near that boundary (real-pixel-to-
* virtual-2D-space rounding in the ox/oy scale-up, or simply a slightly low
* click on the last visible row) could therefore resolve to the NEXT,
* invisible, scrolled-off-the-bottom row instead of the one actually drawn
* there. Concretely: with "All unlocked" sitting in the last visible slot,
* this let a click on it silently hit "Quit to desktop" (the very next row)
* instead -- reported as "I clicked unlock all and I think it crashed"
* (user, 2026-09-18): the game didn't crash, __QuitToDesktop's ROW_ACTION
* fired and closed it via the game's normal exit path. Fix: bound the scan
* to the same [s_scroll, pLast] range the draw loop actually renders.
*
* D316 fix: the band used to be [Y-3, Y+12) -- 3 units above OV_ROW_Y but
* 12 below it, a systematic downward bias baked in independent of the
* mouse-mapping bug above. Centered here on OV_ROW_Y so a borderline click
* no longer favors the row below. */
/* Row index the given overlay-space y falls in, or -1. */
static int overlayRowAtY(double oy)
{
int maxV = maxVisibleRows();
int pLast = (s_visN - s_scroll < maxV) ? s_visN - 1 : s_scroll + maxV - 1;
for (int p = s_scroll; p <= pLast; p++) {
double top = OV_ROW_Y(p - s_scroll) - OV_LINE / 2;
for (int i = 0; i < NUM_ROWS; i++) {
double top = OV_ROW_Y(i) - 3;
if (oy >= top && oy < top + OV_LINE) {
return p;
return i;
}
}
return -1;
}
/* Rebuild the visible-row list and keep the selection in range. */
static void overlayUpdateVisible(void)
{
s_visN = 0;
for (int i = 0; i < NUM_ROWS; i++) {
if (!(rows[i].hidePtr && *rows[i].hidePtr != 0)) {
s_visIdx[s_visN++] = i;
}
}
if (s_visN == 0) { /* cannot happen (toggles have no hide source) */
s_visIdx[s_visN++] = 0;
}
if (s_sel < 0) {
s_sel = 0;
}
if (s_sel >= s_visN) {
s_sel = s_visN - 1;
}
}
/* Keep the selected row on screen: shift the window ONLY when the selection
* is actually outside it, by the minimum amount needed.
*
* D304 fix: this used to unconditionally set `s_scroll = s_sel - (maxV-1)`
* -- i.e. bottom-anchor the selected row -- on every single call, including
* every mouse click. A click on a row already visible (anywhere but the very
* last slot) still forced the whole list to re-scroll so that row landed at
* the bottom, shifting every row's on-screen position for the rest of that
* same frame -- so whatever row the user then saw/clicked at that same
* screen position was a DIFFERENT (usually the next, i.e. "below") setting.
* Reported as "the F10 menu keeps jumping to the setting below when I left
* click" (user, 2026-09-18) -- clicking any row not already at the bottom
* slot reproduced it every time. Fix: only move the window when the
* selection is above the top or below the bottom of the current view. */
static void overlayUpdateScroll(void)
{
int maxV = maxVisibleRows();
if (s_visN <= maxV) {
s_scroll = 0;
return;
}
if (s_sel < s_scroll) {
s_scroll = s_sel;
} else if (s_sel > s_scroll + maxV - 1) {
s_scroll = s_sel - (maxV - 1);
}
if (s_scroll < 0) {
s_scroll = 0;
}
if (s_scroll > s_visN - maxV) {
s_scroll = s_visN - maxV;
}
}
/* The close box brackets the title row at the panel's right edge. */
#define OV_CB_X0 (OV_RIGHT - 14)
#define OV_CB_X1 (OV_RIGHT + 7)
@@ -405,8 +193,8 @@ static void overlayInit(void)
configForEachOption(resolveCb, NULL);
for (int i = 0; i < NUM_ROWS; i++) {
if (rows[i].kind == ROW_RES || rows[i].kind == ROW_ACTION) {
rows[i].found = 1; /* not config-backed */
if (rows[i].kind == ROW_RES) {
rows[i].found = 1; /* not config-backed; driven via video.c */
continue;
}
if (!rows[i].found) {
@@ -415,21 +203,6 @@ static void overlayInit(void)
}
}
/* Resolve the hidden-while-on sources (manual % rows vs their auto
* toggles), then build the initial visible list. */
for (int i = 0; i < NUM_ROWS; i++) {
if (!rows[i].hiddenIfOn) {
continue;
}
for (int j = 0; j < NUM_ROWS; j++) {
if (strcmp(rows[j].key, rows[i].hiddenIfOn) == 0 && rows[j].found) {
rows[i].hidePtr = rows[j].ptr;
break;
}
}
}
overlayUpdateVisible();
/* Build the windowed-resolution preset list: presets that fit the desktop,
* plus the current window size snapped to the nearest surviving entry. */
{
@@ -484,18 +257,6 @@ static double rowHi(const struct Row *r)
return (r->uiMin != r->uiMax) ? r->uiMax : r->cfgMax;
}
static struct Row *rowByKey(const char *key)
{
for (int i = 0; i < NUM_ROWS; i++) {
if (strcmp(rows[i].key, key) == 0) {
return &rows[i];
}
}
return NULL;
}
static int s_linkDepth = 0; /* re-entrancy guard for the sens link below */
static void rowSet(struct Row *r, double v)
{
double lo = rowLo(r), hi = rowHi(r);
@@ -517,29 +278,6 @@ static void rowSet(struct Row *r, double v)
} else if (strncmp(r->key, "Video.", 6) == 0 && !r->restart) {
videoRequestLiveConfig();
}
/* Linked aim/turn sensitivity (Input.SensLink, default on): moving either
* knob scales the other to hold the stock default ratio -- AimModeSens 38
* : MouseTurnSpeed 50 (the D194/D238 calibrated defaults). */
if (!s_linkDepth && strcmp(r->key, "Input.SensLink") != 0) {
struct Row *lk = rowByKey("Input.SensLink");
if (lk && lk->found && *(int *)lk->ptr != 0) {
struct Row *o = NULL;
double nv = 0.0;
if (strcmp(r->key, "Input.MouseTurnSpeed") == 0) {
o = rowByKey("Input.AimModeSens");
nv = v * 38.0 / 50.0;
} else if (strcmp(r->key, "Input.AimModeSens") == 0) {
o = rowByKey("Input.MouseTurnSpeed");
nv = v * 50.0 / 38.0;
}
if (o && o->found) {
s_linkDepth = 1;
rowSet(o, nv);
s_linkDepth = 0;
}
}
}
}
static void rowAdjust(struct Row *r, int dir)
@@ -557,9 +295,9 @@ static void rowAdjust(struct Row *r, int dir)
for (int i = 0; i < 4; i++) {
if (kMsaaSeq[i] == (int)lround(v)) idx = i;
}
/* Wrap like a normal settings-menu cycle: OFF->2x->4x->8x->OFF, in
* both directions (left/right click and arrows all roll). */
idx = (idx + dir + 4) % 4;
idx += dir;
if (idx < 0) idx = 0;
if (idx > 3) idx = 3;
rowSet(r, (double)kMsaaSeq[idx]);
break;
}
@@ -571,15 +309,6 @@ static void rowAdjust(struct Row *r, int dir)
rowSet(r, v);
break;
}
case ROW_FPSCAP: {
int idx = 0;
for (int i = 0; i < 2; i++) {
if (kFpsCapSeq[i] == (int)lround(v)) idx = i;
}
idx = (idx + dir + 2) % 2;
rowSet(r, (double)kFpsCapSeq[idx]);
break;
}
case ROW_RES: {
if (s_resFitN <= 0 || videoIsFullscreen()) {
break; /* resolution is windowed-only */
@@ -591,13 +320,6 @@ static void rowAdjust(struct Row *r, int dir)
videoRequestWindowSize(kResList[i][0], kResList[i][1]);
break;
}
case ROW_ACTION:
/* D293: same exit path as SDL_QUIT / Alt+F4 (video.c), just reachable
* without OS window chrome or a keyboard. */
sysLogPrintf(LOG_INFO, "optionsoverlay: quit to desktop requested");
configSave();
exit(0);
break;
default: /* ROW_SLIDER */
rowSet(r, v + dir * r->step);
break;
@@ -626,18 +348,12 @@ int optionsOverlayIsOpen(void)
void optionsOverlayScroll(int dir)
{
if (!s_inited) {
overlayInit();
}
if (!s_open || dir == 0) {
return;
}
overlayUpdateVisible();
s_sel += (dir > 0) ? -1 : 1; /* wheel-up -> move up the list */
/* Clamp at both ends like a normal PC settings list -- wrapping made the
* selection "repeat" from the far edge, which read as a duplicate. */
if (s_sel < 0) s_sel = 0;
if (s_sel >= s_visN) s_sel = s_visN - 1;
if (s_sel < 0) s_sel = NUM_ROWS - 1;
if (s_sel >= NUM_ROWS) s_sel = 0;
}
/* Set a slider row from an overlay-space x inside its value bar, snapped to
@@ -659,15 +375,6 @@ static void sliderSetFromX(struct Row *r, double ox)
rowSet(r, v);
}
/* D314 (findings.md): F10 menu items flicker/mis-land on the file-select
* screen, mechanism unpinned. Ruled out: viGetX/viGetY (stable per-screen,
* set once by front.c) and non-determinism in the display list. This logs
* the remaining suspects -- viGetY(), s_visN, s_scroll, s_sel -- once per
* frame while the overlay is open, to catch whichever one oscillates during
* a file-select repro. Env-gated, cached once (not re-queried per frame --
* see D302: a hot-path getenv() regression cost a real bug before). */
static int s_d314Enabled = -1; /* -1 = not yet resolved */
void optionsOverlayHandleInput(void)
{
static int prevUp, prevDn, prevLf, prevRt, prevLmb, prevRmb;
@@ -679,81 +386,40 @@ void optionsOverlayHandleInput(void)
return;
}
if (s_d314Enabled < 0) {
s_d314Enabled = getenv("GE_D314") ? 1 : 0;
}
if (s_d314Enabled) {
fprintf(stderr, "GE_D314 viGetY=%d visN=%d scroll=%d sel=%d\n",
(int)viGetY(), s_visN, s_scroll, s_sel);
}
/* The overlay owns the mouse while it is open: force the OS cursor free +
* visible (a stage poll would otherwise leave it locked/hidden). */
inputSuspendForOverlay();
overlayUpdateVisible(); /* % rows may have appeared/vanished (auto toggles) */
overlayUpdateScroll();
const Uint8 *ks = SDL_GetKeyboardState(NULL);
int mx = 0, my = 0;
Uint32 mb = SDL_GetMouseState(&mx, &my);
int lmb = (mb & SDL_BUTTON(SDL_BUTTON_LEFT)) ? 1 : 0;
int rmb = (mb & SDL_BUTTON(SDL_BUTTON_RIGHT)) ? 1 : 0;
/* Gamepad navigation (controller-only machines, e.g. Steam Deck): the
* D-pad or left stick moves the selection; A/X step forward (the Enter
* equivalent), B/Y step back; Start closes. OR-ed into the same edge
* logic as the keyboard, so repeat/clamp/scroll behaviour is identical.
* input.c swallows the pad while we are open, so none of this reaches
* the game. (Select also closes -- handled in input.c's toggle, which
* runs before this one.) */
int gUp = inputPadButton(0, SDL_CONTROLLER_BUTTON_DPAD_UP)
|| inputPadAxis(0, SDL_CONTROLLER_AXIS_LEFTY) < -12000;
int gDn = inputPadButton(0, SDL_CONTROLLER_BUTTON_DPAD_DOWN)
|| inputPadAxis(0, SDL_CONTROLLER_AXIS_LEFTY) > 12000;
int up = ks[SDL_SCANCODE_UP] || ks[SDL_SCANCODE_KP_8] || gUp;
int dn = ks[SDL_SCANCODE_DOWN] || ks[SDL_SCANCODE_KP_2] || gDn;
int lf = ks[SDL_SCANCODE_LEFT] || ks[SDL_SCANCODE_KP_4]
|| inputPadButton(0, SDL_CONTROLLER_BUTTON_B)
|| inputPadButton(0, SDL_CONTROLLER_BUTTON_Y);
int up = ks[SDL_SCANCODE_UP] || ks[SDL_SCANCODE_KP_8];
int dn = ks[SDL_SCANCODE_DOWN] || ks[SDL_SCANCODE_KP_2];
int lf = ks[SDL_SCANCODE_LEFT] || ks[SDL_SCANCODE_KP_4];
int rt = ks[SDL_SCANCODE_RIGHT] || ks[SDL_SCANCODE_KP_6]
|| ks[SDL_SCANCODE_RETURN] || ks[SDL_SCANCODE_KP_ENTER]
|| inputPadButton(0, SDL_CONTROLLER_BUTTON_A)
|| inputPadButton(0, SDL_CONTROLLER_BUTTON_X);
|| ks[SDL_SCANCODE_RETURN] || ks[SDL_SCANCODE_KP_ENTER];
static int prevStart = 0; /* not reset while closed: a Start held across
close must not re-close on the next open */
int startNow = inputPadButton(0, SDL_CONTROLLER_BUTTON_START);
if (startNow && !prevStart) optionsOverlayToggle(); /* Start closes */
prevStart = startNow;
/* ---- keyboard / D-pad nav ---- */
if (up && !prevUp) s_sel = (s_sel + NUM_ROWS - 1) % NUM_ROWS;
if (dn && !prevDn) s_sel = (s_sel + 1) % NUM_ROWS;
if (lf && !prevLf) rowAdjust(&rows[s_sel], -1);
if (rt && !prevRt) rowAdjust(&rows[s_sel], +1);
/* ---- keyboard / D-pad nav (clamped at the ends; scroll follows) ---- */
if (up && !prevUp && s_sel > 0) s_sel--;
if (dn && !prevDn && s_sel < s_visN - 1) s_sel++;
overlayUpdateScroll();
if (lf && !prevLf) rowAdjust(&rows[s_visIdx[s_sel]], -1);
if (rt && !prevRt) rowAdjust(&rows[s_visIdx[s_sel]], +1);
/* ---- mouse ----
* D316: mx/my are raw window pixels, but the overlay's own 2D content
* is drawn into whatever on-window rect the safe-area crop currently
* maps the logical (viGetX() x viGetY()) canvas to -- NOT the full
* window whenever that rect is inset (default-on: any in-game "Full"
* viewport insets it). Map through the real forward transform's rect
* (gfx_get_ui_screen_rect) instead of a naive window-size scale so a
* click lands on the same row it visually appears over. */
int32_t rx = 0, ry = 0, rw = 0, rh = 0;
gfx_get_ui_screen_rect(&rx, &ry, &rw, &rh);
if (rw > 0 && rh > 0) {
double ox = (double)(mx - rx) * (double)viGetX() / rw;
double oy = (double)(my - ry) * (double)viGetY() / rh;
int hoverVis = overlayRowAtY(oy);
/* ---- mouse ---- */
int ww = 0, wh = 0;
videoGetWindowSize(&ww, &wh);
if (ww > 0 && wh > 0) {
double ox = (double)mx * (double)viGetX() / ww;
double oy = (double)my * (double)viGetY() / wh;
int hoverRow = overlayRowAtY(oy);
int onClose = overlayInCloseBox(ox, oy);
/* No hover-to-highlight: merely moving the mouse must not move the
* selection or scroll the window (hovering near a list edge fed the
* new row back into the cursor and made the bottom twitch/echo).
* Selection moves only by click, wheel, or arrows. */
if (hoverRow >= 0 && !onClose) {
s_sel = hoverRow; /* hover-to-highlight */
}
/* left press. A click in the label column only focuses the row; a
* click in the value/control column (>= the bar-span start) changes
@@ -765,17 +431,13 @@ void optionsOverlayHandleInput(void)
optionsOverlayToggle(); /* close + configSave */
return;
}
if (hoverVis >= 0) {
s_sel = hoverVis; /* explicit click -> select */
overlayUpdateScroll();
if (ox >= bx0) {
struct Row *r = &rows[s_visIdx[hoverVis]];
if (r->kind == ROW_SLIDER && r->found) {
sliderSetFromX(r, ox);
dragRow = s_visIdx[hoverVis];
} else {
rowAdjust(r, +1); /* toggle / cycle forward (wraps) */
}
if (hoverRow >= 0 && ox >= bx0) {
struct Row *r = &rows[hoverRow];
if (r->kind == ROW_SLIDER && r->found) {
sliderSetFromX(r, ox);
dragRow = hoverRow;
} else {
rowAdjust(r, +1); /* toggle / cycle forward */
}
}
}
@@ -787,10 +449,8 @@ void optionsOverlayHandleInput(void)
dragRow = -1;
}
/* right press in the value column: cycle back / decrement */
if (rmb && !prevRmb && hoverVis >= 0 && !onClose && ox >= bx0) {
s_sel = hoverVis;
overlayUpdateScroll();
rowAdjust(&rows[s_visIdx[hoverVis]], -1);
if (rmb && !prevRmb && hoverRow >= 0 && !onClose && ox >= bx0) {
rowAdjust(&rows[hoverRow], -1);
}
}
@@ -841,18 +501,13 @@ static void valueText(const struct Row *r, char *out, int n)
else snprintf(out, n, "%dx", (int)lround(v));
return;
}
if (r->kind == ROW_ACTION) {
snprintf(out, n, "[ENTER]");
return;
}
if (r->kind == ROW_SLIDER && r->type == CONFIG_OPT_FLOAT) {
snprintf(out, n, "%.2f", v);
return;
}
if (r->kind == ROW_FPSCAP) {
int fps = (int)lround(v);
if (fps <= 0) snprintf(out, n, "Uncapped");
else snprintf(out, n, "%d FPS", fps);
if (r->kind == ROW_SLIDER && (int)lround(v) == 0 &&
strcmp(r->key, "Video.FpsCap") == 0) {
snprintf(out, n, "OFF");
return;
}
snprintf(out, n, "%d", (int)lround(v));
@@ -888,51 +543,15 @@ Gfx *optionsOverlayEmit(void)
if (!s_inited) {
overlayInit();
}
fpsTick();
if (!s_open) {
if (!s_showFps || !s_fpsText[0]) {
return NULL; /* nothing appended -> golden dumps byte-identical */
}
/* D213: FPS-only mini DL (top-right), panel closed. */
const s32 fw = viGetX();
const s32 fh = viGetY();
Gfx *fgdl = s_buf;
gDPPipeSync(fgdl++);
gDPSetCycleType(fgdl++, G_CYC_1CYCLE);
gDPSetTexturePersp(fgdl++, G_TP_NONE);
gDPSetScissor(fgdl++, G_SC_NON_INTERLACE, 0, 0, fw, fh);
fgdl = microcode_constructor(fgdl);
fgdl = drawTextR(fgdl, fw - 6, 6, s_fpsText, 0x40ff60ff);
gDPPipeSync(fgdl++);
gSPEndDisplayList(fgdl++);
return s_buf;
return NULL; /* nothing appended -> golden dumps byte-identical */
}
overlayUpdateVisible();
overlayUpdateScroll();
const s32 W = viGetX();
const s32 H = viGetY();
const s32 right = OV_RIGHT;
const s32 panelTop = OV_TOP - 9;
/* Last visible position actually drawn (window may be shorter than the
* list at small 2D viewports -- the rest is reached by scrolling).
* D304 fix: the "everything fits" branch was `s_visN - s_scroll`, which
* is a COUNT, not the last valid 0-based index -- since that branch only
* runs when s_scroll==0 (overlayUpdateScroll's own invariant), this
* evaluated to `s_visN`, one past the last valid entry, so both draw
* loops below (`for (p = s_scroll; p <= pLast; p++)`) read one row past
* the end of `s_visIdx[]`/`rows[]` on any 2D viewport large enough to fit
* the whole list without scrolling (e.g. the larger front-end 440x330
* screen once a couple of rows are pulled from the menu, as just
* happened above). Should be `s_visN - 1`, matching overlayRowAtY's
* identical fix. */
const int maxV = maxVisibleRows();
const int pLast = (s_visN - s_scroll < maxV) ? s_visN - 1
: s_scroll + maxV - 1;
const s32 panelBottom = OV_ROW_Y(pLast) + OV_LINE / 2 + 3;
const s32 panelBottom = OV_ROW_Y(NUM_ROWS - 1) + OV_LINE / 2 + 3;
s32 bx0, bx1;
sliderBarSpan(&bx0, &bx1);
Gfx *gdl = s_buf;
@@ -949,16 +568,15 @@ Gfx *optionsOverlayEmit(void)
gdl = fillRect(gdl, OV_CB_X0, OV_CB_Y0, OV_CB_X1, OV_CB_Y1,
150, 40, 40, 235); /* close */
for (int p = s_scroll; p <= pLast; p++) {
const struct Row *r = &rows[s_visIdx[p]];
s32 rowY = OV_ROW_Y(p - s_scroll);
if (p == s_sel) {
for (int i = 0; i < NUM_ROWS; i++) {
s32 rowY = OV_ROW_Y(i);
if (i == s_sel) {
gdl = fillRect(gdl, OV_X0 - 4, rowY - 3, W - (OV_X0 - 4),
rowY + OV_LINE - 4, 40, 46, 96, 220);
}
if (r->kind == ROW_SLIDER && r->found) {
double lo = rowLo(r), hi = rowHi(r);
double f = (hi > lo) ? (rowGet(r) - lo) / (hi - lo) : 0.0;
if (rows[i].kind == ROW_SLIDER && rows[i].found) {
double lo = rowLo(&rows[i]), hi = rowHi(&rows[i]);
double f = (hi > lo) ? (rowGet(&rows[i]) - lo) / (hi - lo) : 0.0;
if (f < 0) f = 0; if (f > 1) f = 1;
s32 by = rowY + 3;
gdl = fillRect(gdl, bx0, by, bx1, by + 5, 60, 60, 70, 220);
@@ -971,27 +589,25 @@ Gfx *optionsOverlayEmit(void)
gdl = microcode_constructor(gdl);
gdl = drawText(gdl, OV_X0, OV_TOP, "PC OPTIONS", 0xffe040ff);
gdl = drawText(gdl, OV_X0, OV_TOP + OV_LINE,
"select: scroll/click, change: mouse/arrows",
gdl = drawText(gdl, OV_X0, OV_TOP + OV_LINE, "click value / drag / arrows",
0x8890a0ff); /* hint line */
gdl = drawText(gdl, (OV_CB_X0 + OV_CB_X1) / 2 - measureText("X") / 2,
OV_TOP, "X", 0xffffffff); /* close glyph */
for (int p = s_scroll; p <= pLast; p++) {
const struct Row *r = &rows[s_visIdx[p]];
s32 rowY = OV_ROW_Y(p - s_scroll);
u32 col = (p == s_sel) ? 0xffffffff : 0xc0c0c8ff;
for (int i = 0; i < NUM_ROWS; i++) {
s32 rowY = OV_ROW_Y(i);
u32 col = (i == s_sel) ? 0xffffffff : 0xc0c0c8ff;
char val[48];
gdl = drawText(gdl, OV_LABEL_X, rowY, (char *)r->label,
r->found ? col : 0x808080ff);
if (!r->found) {
gdl = drawText(gdl, OV_LABEL_X, rowY, (char *)rows[i].label,
rows[i].found ? col : 0x808080ff);
if (!rows[i].found) {
gdl = drawTextR(gdl, right, rowY, "(n/a)", 0x808080ff);
continue;
}
valueText(r, val, sizeof(val));
if (r->restart) {
valueText(&rows[i], val, sizeof(val));
if (rows[i].restart) {
/* value left of the bar span, "(restart)" pinned to the edge */
gdl = drawText(gdl, bx0, rowY, val, col);
gdl = drawTextR(gdl, right, rowY, "(restart)", 0x909090ff);
+9 -98
View File
@@ -10,79 +10,32 @@
* Also ports src/game/chrObjRandom.s: the character-AI PRNG (chroid.c),
* which runs the identical transform on its own seed variable.
*
* The seed is stored as a full u64 and the transform genuinely operates on
* all 64 bits of the register (see the dsll32/dsrl32 note below) — the
* stored seed is NOT truncated to 32 bits between calls, matching what the
* real `sd $a0, g_randomSeed` in random.s stores. The initial seed is the
* two .words in random.s: 0xAB8D9F77 (hi) / 0x81280783 (lo). Only the low
* 32 bits are ever returned to callers (`dsll32 $v0,$a0,0; dsra32
* $v0,$v0,0` sign-extends the low half into $v0, of which callers only use
* the u32 truncation).
*
* D284 (M-140): the original port of this file had dsll32/dsrl32 backwards
* — it shifted by the literal immediate `n` and masked to 32 bits, i.e. it
* modelled them as 32-bit shift instructions. Real MIPS64 dsll32/dsrl32 are
* "shift by n+32", operating on the FULL 64-bit register with no masking:
* they exist specifically to encode a 32..63 shift amount in a 5-bit field.
* The bug was bit-exact-but-wrong: every generated value differed from the
* real N64 stream from the very first draw, silently desyncing loot RNG, AI
* variance, and replay determinism in every release to date. Verified two
* independent ways (a forked review agent's numeric trace, and a hand trace
* of random.s against this file, both in the M-138 review session).
* The assembly operates on the low 32 bits of the seed (the high 32 bits are
* never read), but the seed is stored as a full u64. We mirror each
* instruction with explicit masks to guarantee bit-exactness. The initial
* seed is the two .words in random.s: 0xAB8D9F77 (hi) / 0x81280783 (lo).
*/
#include <ultra64.h>
#include <stdlib.h>
#include <random.h>
u64 g_randomSeed = 0xAB8D9F7781280783ULL;
/*
* MIPS64 shift helpers, matching the .s instructions exactly:
* dsll32 rd,rs,n -> (rs << (n+32)) (full 64-bit register, no masking)
* dsrl32 rd,rs,n -> (rs >> (n+32)) (full 64-bit register, unsigned)
* Plain `<<`/`>>` on u64 match dsll/dsrl (full 64-bit shifts, shift amount
* used as-is, 0-31).
* dsll32 rd,rs,n -> (rs << n) & 0xFFFFFFFF (low 32 bits kept, hi zeroed)
* dsrl32 rd,rs,n -> (rs >> n) & 0xFFFFFFFF (low 32 bits kept, hi zeroed)
* Plain `<<`/`>>` on u64 match dsll/dsrl (full 64-bit shifts).
*/
static inline u64 dsll32(u64 x, int n) { return x << (n + 32); }
static inline u64 dsrl32(u64 x, int n) { return x >> (n + 32); }
static inline u64 dsll32(u64 x, int n) { return (x << n) & 0xFFFFFFFFULL; }
static inline u64 dsrl32(u64 x, int n) { return (x >> n) & 0xFFFFFFFFULL; }
/*
* Advance the global seed and return the new low 32 bits.
* Mirrors randomGetNext in random.s line-by-line.
*/
/* D318R: env-gated draw logger (GE_D318R=1). Logs the first N draws with
* tick + caller return address to identify which call sites consume the
* gameplay PRNG and whether their per-run counts are stable. Diagnostic
* only; inert without the env var. */
#ifdef PORT
#include <PR/os.h>
extern s32 g_GlobalTimer;
static void randomDrawLog(void)
{
static int s_on = -1;
static u32 s_n = 0;
const u32 MAXN = 4000;
if (s_on < 0)
{
const char *e = getenv("GE_D318R");
s_on = (e && e[0] == '1') ? 1 : 0;
}
if (s_on && s_n < MAXN)
{
osSyncPrintf("D318R: n=%u t=%d ra=%p\n",
s_n, (int)g_GlobalTimer, __builtin_return_address(0));
s_n++;
}
}
#endif /* PORT */
u32 randomGetNext(void)
{
#ifdef PORT
randomDrawLog();
#endif
u64 a0 = g_randomSeed;
u64 a1, a2;
@@ -103,51 +56,9 @@ u32 randomGetNext(void)
}
/* Set the seed (the .s adds 1 before storing). */
/* Manual hex parse (u64-safe): MinGW's strtoull returns the sign-extended low
* 32 bits for values above LLONG_MAX. Stops at first non-hex char. */
static u64 randomParseHex(const char *s)
{
u64 v = 0;
while (*s)
{
char c = *s++;
v <<= 4;
if (c >= '0' && c <= '9') { v |= (u64)(c - '0'); }
else if (c >= 'a' && c <= 'f') { v |= (u64)(c - 'a' + 10); }
else if (c >= 'A' && c <= 'F') { v |= (u64)(c - 'A' + 10); }
else { break; }
}
return v;
}
void randomSetSeed(u32 seed)
{
#ifdef PORT
/* GE_RSEED=<hex64>: test-only boot-seed override. boss.c seeds the PRNG
* from osGetCount() (wall-clock on PC; RSP timer on N64), so the boot
* seed differs every run -- with or without GE_DETERM, because the
* virtual clock's pre-task advance count is still real-time dependent.
* This pins it for determinism tests (E1) and N64 parity tests (E-B). */
{
const char *e = getenv("GE_RSEED");
if (e && e[0])
{
g_randomSeed = randomParseHex(e);
osSyncPrintf("D318R: SETSEED OVERRIDDEN by GE_RSEED -> %016llx\n",
(unsigned long long)g_randomSeed);
return;
}
}
#endif
g_randomSeed = (u64)seed + 1; /* daddiu $a0,$a0,1; sd $a0, g_randomSeed */
#ifdef PORT
if (getenv("GE_D318R") != NULL)
osSyncPrintf("D318R: SETSEED arg=%08x -> %016llx\n",
seed, (unsigned long long)g_randomSeed);
#endif
}
/*
-203
View File
@@ -1,203 +0,0 @@
/*
* romconvert.c — first-run sidecar conversion ("drop in your ROM and play").
*
* The engine needs two ROM-derived sidecar trees that are NOT shipped with a
* release (they are copyrighted game data derived from the user's own ROM):
*
* $S/pcmodels-<region>/ (pcmodels.bin + manifest.csv) — port/src/pcmodels.c
* $S/pccg-<region>/ (pccg.bin + manifest.csv) — port/src/pccg.c
*
* The release bundle ships a frozen copy of the asset-prep tool
* (prepare-assets.py, PyInstaller --onefile) as
* <exedir>/prepare-assets/ge007-convert[.exe]. When the sidecars are missing
* at boot, romdataInit() calls romConvertEnsureSidecars(), which spawns that
* binary with `--rom <this ROM> --out <exedir>`, waits for it to finish, and
* re-checks. The conversion is deterministic and takes a few seconds; after
* the first run the sidecars exist and this path is a no-op (two fsExists).
*
* Returns 1 when both sidecars are present (already or after converting),
* -1 when they could not be produced (caller should abort boot with a
* message rather than crash later in modelPromoteNodeOffsetsToPointers, D179).
*/
#include <stdlib.h>
#include <string.h>
/* D38: <stdio.h>/<stdlib.h> resolve to the decomp's N64 stubs via the include
* path; declare the host functions this file uses. */
extern void *malloc(unsigned long long size);
extern int snprintf(char *str, size_t maxsize, const char *format, ...);
#include "platform.h"
#if defined(PLATFORM_WINDOWS)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#else
#include <unistd.h>
#include <sys/wait.h>
#endif
#include "system.h"
#include "fs.h"
/* Same country→region map as pcmodels.c / pccg.c (finding: plan Q3). */
static const char *rcRegionForCountry(unsigned char country)
{
switch (country) {
case 'E': return "ntsc-final";
case 'P': return "pal-final";
case 'J': return "jpn-final";
default: return NULL;
}
}
/* sysResolvePath() returns a static buffer — copy before the next call. */
static int rcExistsResolved(const char *rel)
{
const char *p = sysResolvePath(rel);
return fsExists(p);
}
static int rcSidecarsPresent(const char *region)
{
char rel[256];
snprintf(rel, sizeof(rel), "$S/pcmodels-%s/pcmodels.bin", region);
if (!rcExistsResolved(rel))
return 0;
snprintf(rel, sizeof(rel), "$S/pccg-%s/pccg.bin", region);
return rcExistsResolved(rel);
}
#if defined(PLATFORM_WINDOWS)
/* Spawn `exePath --rom "<rom>" --out "<exedir>"` and wait for it.
* Returns the exit code, or -1 if it could not be started. */
static int rcRunConverter(const char *exePath, const char *rom, const char *out)
{
char cmd[4096];
int n = snprintf(cmd, sizeof(cmd), "\"%s\" --rom \"%s\" --out \"%s\"",
exePath, rom, out);
if (n < 0 || (size_t)n >= sizeof(cmd))
return -1; /* paths too long to build a command line */
int wlen = MultiByteToWideChar(CP_UTF8, 0, cmd, -1, NULL, 0);
if (wlen <= 0)
return -1;
WCHAR *wcmd = (WCHAR *)malloc((size_t)wlen * sizeof(WCHAR));
if (!wcmd)
return -1;
MultiByteToWideChar(CP_UTF8, 0, cmd, -1, wcmd, wlen);
STARTUPINFOW si;
PROCESS_INFORMATION pi;
int rc = -1;
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
memset(&pi, 0, sizeof(pi));
if (CreateProcessW(NULL, wcmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD code = 0;
GetExitCodeProcess(pi.hProcess, &code);
rc = (int)code;
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
free(wcmd);
return rc;
}
#else
static int rcRunConverter(const char *exePath, const char *rom, const char *out)
{
pid_t pid = fork();
if (pid < 0)
return -1;
if (pid == 0) {
/* Child: stdio is inherited (console apps see the converter's
* progress lines; GUI builds simply discard them). */
execv(exePath, (char *const[]){ exePath, "--rom", (char *)rom,
"--out", (char *)out, NULL });
_exit(127); /* execv failed */
}
int status = 0;
while (waitpid(pid, &status, 0) < 0)
; /* EINTR */
if (WIFEXITED(status))
return WEXITSTATUS(status);
return -1;
}
#endif
int romConvertEnsureSidecars(const unsigned char *romImg, const char *romRelPath)
{
const char *region = rcRegionForCountry(romImg[0x3E]);
if (!region)
return 1; /* unknown country — romHeaderValid() rejects it anyway */
if (rcSidecarsPresent(region))
return 1;
/* Resolve the ROM path we were given ("$S/...", "$E/..." or "./...") to
* an absolute form for the child's argv. */
char romPath[1024];
snprintf(romPath, sizeof(romPath), "%s", sysResolvePath(romRelPath));
const char *exedir = sysGetExeDir();
char outDir[1024];
snprintf(outDir, sizeof(outDir), "%s", exedir);
/* Converter candidates: bundled under <exedir>/prepare-assets/ (the
* release layout), then the CWD-relative form (running from a source
* tree or an unpacked bundle in the CWD). */
#if defined(PLATFORM_WINDOWS)
static const char *candRel[4] = {
"$E/prepare-assets/ge007-convert.exe",
"./prepare-assets/ge007-convert.exe",
"$E/ge007-convert.exe",
"./ge007-convert.exe",
};
#else
static const char *candRel[4] = {
"$E/prepare-assets/ge007-convert",
"./prepare-assets/ge007-convert",
"$E/ge007-convert",
"./ge007-convert",
};
#endif
char convPath[1024] = "";
for (int i = 0; i < 4; i++) {
if (rcExistsResolved(candRel[i])) {
/* sysResolvePath() returns a static buffer — snapshot it. */
snprintf(convPath, sizeof(convPath), "%s", sysResolvePath(candRel[i]));
break;
}
}
if (!convPath[0]) {
sysLogPrintf(LOG_ERROR,
"romconvert: sidecars for '%s' are missing and no ge007-convert "
"binary was found (looked in <exedir>/prepare-assets/). Put your "
"ROM at data/ge007.%s.z64 and re-run from the bundle folder, or "
"run prepare-assets manually.", region, region);
return -1;
}
sysLogPrintf(LOG_INFO,
"romconvert: sidecars missing — running %s (one-time, a few seconds)…",
convPath);
int code = rcRunConverter(convPath, romPath, outDir);
if (code != 0)
sysLogPrintf(LOG_ERROR,
"romconvert: ge007-convert exited with code %d — see its output "
"above (a non-retail or byte-swapped ROM will fail here)", code);
if (!rcSidecarsPresent(region)) {
sysLogPrintf(LOG_ERROR,
"romconvert: data/pcmodels-%s/ and data/pccg-%s/ are still "
"missing — cannot continue", region, region);
return -1;
}
sysLogPrintf(LOG_INFO, "romconvert: sidecars generated — continuing boot");
return 1;
}
Loaded 100 of 160 files, more files were not shown because too many files have changed in this diff. Show more