Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66e38e27d7 |
No files matched your search
@@ -1,125 +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.2.0/docs/media/goldeneye-gh-preview.gif" width="480"
|
||||
alt="~32 s gameplay montage from live 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.
|
||||
|
||||
### What's new since v0.1.0
|
||||
|
||||
- **Drop-in ROM, no tooling**: unpack the bundle, drop your NTSC-U (US) ROM
|
||||
in `data/`, launch; the first run generates the derived assets
|
||||
automatically. No Python, no installer, nothing to set up.
|
||||
- **Steady 60 fps** in normal play (the software RSP runs off the presentation
|
||||
critical path); `Video.DisplayFPS` in the F10 overlay shows it.
|
||||
- **Audio**: in-level music and sound effects throughout (the alpha was
|
||||
silent). A handful of tracks have wrong-sounding instruments (below).
|
||||
- **Mouse**: click-to-lock capture (click to grab, ESC to release) with a
|
||||
proportional GEPD-style aim mode; sensitivity / Y-inversion / aim-turn split
|
||||
tunable in `ge007.ini` or the F10 overlay. The legacy always-grab mode is
|
||||
gone.
|
||||
- **Rendering**: outdoor skies render correctly; water no longer renders
|
||||
green/pulsing; reflective surfaces (glass, chrome weapon skins) work.
|
||||
- **Crash fixes**: the two v0.1.0-era crashing levels (Bunker ii, Statue) and
|
||||
the AI-pacing bug that broke Cradle are fixed and playtest-verified: all 21
|
||||
solo missions load and run crash-free. Also fixed: the Steam Deck / Linux
|
||||
Facility crash triggered by walking into the crouch-forcing spot at the
|
||||
level's opening (D253); this also revives the auto-crouch and ladder
|
||||
signals, which were dead on PC before.
|
||||
- **QoL**: F10 in-game options overlay (fullscreen, resolution, frame cap,
|
||||
MSAA, texture filtering, FOV/draw distance, sensitivity), F12 screenshot.
|
||||
- **Sharper defaults out of the box**: draw distance and LOD swap distance now
|
||||
default to 150% of the authored N64 values, so props no longer fade in just
|
||||
before they become visible (Dam's alarms and wall switches being the tell);
|
||||
F10 → *Draw distance* / *LOD distance* set back to 100 restores the
|
||||
console-authentic look. MSAA now defaults to 4× instead of off.
|
||||
- **All-unlocked toggle**: F10 → *All unlocked* makes all 21 solo levels
|
||||
selectable at every difficulty from the first launch, adds 007 mode, and
|
||||
fully populates the cheat menu. It is off by default (faithful N64
|
||||
progression). No *active* cheats are enabled either way (weapons remain
|
||||
per-mission pickups, as on N64).
|
||||
- **Steam Deck first-run preset**: on SteamOS the first launch seeds
|
||||
Deck-friendly defaults (native 1280×800 fullscreen, VSync, MSAA 4, 150%
|
||||
draw/LOD distance); an existing `ge007.ini` always wins. Two more F10 rows:
|
||||
*No hit flash* (suppresses the damage-flash overlay) and *Skip intro*.
|
||||
- **Modern dual-stick controller layout** (the scheme used by the console
|
||||
re-releases): left stick move/strafe, right stick look, right trigger fire,
|
||||
left trigger aim, A/X use, B/Y crouch/cancel, **RB/LB cycle weapons**.
|
||||
- **Linux / Steam Deck**: the Linux bundle now ships its own SDL2, so it runs
|
||||
as-is on any distro, and sideloads onto a Steam Deck with nothing
|
||||
installed. Saves and F10 settings now persist no matter which directory you
|
||||
launch from (previously Linux wrote them relative to the launch directory
|
||||
only, so they silently failed elsewhere; D256); the options overlay is
|
||||
fully gamepad-driven on the Deck.
|
||||
|
||||
### Known issues
|
||||
|
||||
- **Cutscenes still glitch, mostly with James Bond**: in scripted sequences
|
||||
Bond is the one who gets misplaced, hovers, or spins; the other actors are
|
||||
fine for the most part now. The Dam level-end cutscene is racy (D243).
|
||||
Still the most visible gap in this release.
|
||||
- **Particle colours cycle through a rainbow palette**: bullet-impact sparks
|
||||
and lingering smoke/explosion residue drift through the hues over time
|
||||
instead of holding their intended grey/orange palette (D252).
|
||||
- Water on `IsWater` levels shows a moving seam between two patterns (D245);
|
||||
thin pixel strips at the left/right screen edges at non-integer window
|
||||
scales (D246).
|
||||
- The front-end **Nintendo logo renders as two white blobs**, and the
|
||||
Rareware logo is close but its texture filtering looks off (D75).
|
||||
- The F10 overlay's **bottom row duplicates whatever item is currently
|
||||
selected** (e.g. the MSAA value appears both on its own row and again at
|
||||
the panel bottom); earlier builds showed it as an intermittent 4K-
|
||||
fullscreen ghost of the top row.
|
||||
- **Steam Deck: one intermittent crash remains**: the v0.1.0-era Facility
|
||||
crash is confirmed fixed on Deck hardware, but a SIGSEGV can occasionally
|
||||
occur during heavy firefights (D255; seen on Bunker and Frigate). The
|
||||
faulting-register capture in `ge007.crash.log` has pinned it: an object
|
||||
record whose model reference is NULL while the record is still active;
|
||||
i.e. something in a mid-firefight destruction leaves the record live one
|
||||
tick too long. We're tracking down the exact destruction path; more Deck
|
||||
playtime across levels is welcome; please report any Deck-specific faults.
|
||||
> ⚠️ **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`.
|
||||
|
||||
@@ -132,7 +58,7 @@ 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
|
||||
|
||||
@@ -118,18 +118,18 @@
|
||||
<div class="h1">GOLDENEYE <span class="r">007</span><br><small>PC 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 — WINDOWS · LINUX · STEAM DECK</span></div>
|
||||
<div class="statusline">Status: <span class="status">ALPHA v0.1.0 — PHASE 2 OF 4</span></div>
|
||||
<div class="stats">
|
||||
<span class="k">Statistics:</span><span>All 21 solo missions — 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++ · ~10k Python (port layer + tools)</span>
|
||||
<span></span><span>~250 bugs root-caused · two coding agents, one human, part-time</span>
|
||||
<span></span><span>~17k lines C/C++ · ~6k Python (port layer + tools)</span>
|
||||
<span></span><span>~190 bugs root-caused · built in ~3 weeks, part-time</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 597 KiB After Width: | Height: | Size: 583 KiB |
@@ -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,12 +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
|
||||
|
||||
# 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
|
||||
@@ -166,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:
|
||||
@@ -181,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
|
||||
@@ -218,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
|
||||
@@ -259,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
|
||||
@@ -396,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
|
||||
@@ -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
|
||||
@@ -142,27 +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/
|
||||
# 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/
|
||||
|
||||
@@ -172,7 +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/
|
||||
@@ -56,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.
|
||||
@@ -54,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.
|
||||
|
||||
@@ -88,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.
|
||||
@@ -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)
|
||||
|
||||
@@ -32,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.
|
||||
|
||||
@@ -1,185 +1,232 @@
|
||||
# GoldenEye 007 PC Port
|
||||
|
||||
[](https://github.com/jkdansereau/goldeneye-pc-port/actions/workflows/ci.yml)
|
||||
-orange)
|
||||

|
||||

|
||||

|
||||
|
||||
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.2.0** is out for Windows and Linux (including Steam Deck, where the
|
||||
Linux bundle sideloads as-is) and runs the full campaign at a steady 60 fps
|
||||
with known rough edges ([Status](#status)). Free to download, build on and
|
||||
modify (you bring the ROM).
|
||||
It 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).
|
||||
|
||||
It's also a case study in AI-agent collaboration on a large, low-level
|
||||
codebase: two coding agents, driven by one person part-time, porting ~230
|
||||
translation units of unmodified big-endian MIPS game code to a 64-bit desktop.
|
||||
See [Background](#background).
|
||||
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="~32 s gameplay montage from live play sessions">
|
||||
<br><em>All in-engine, running in the port, a ~32 s gameplay montage loop
|
||||
from the v0.2.0 playtest.</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 1 and Dam attract views, and a
|
||||
~15 s clip (no audio yet, Phase 3): mission dossier → Facility → Silo →
|
||||
Jungle → Archives.</em>
|
||||
</p>
|
||||
|
||||
## Download
|
||||
## Quick start
|
||||
|
||||
| Platform | Bundle | Notes |
|
||||
|---|---|---|
|
||||
| **Windows** (x86_64) | [win64.zip](/jkdansereau/goldeneye-pc-port/releases) | Engine + runtime DLLs + the one-time asset tool. |
|
||||
| **Linux** (x86_64) / **Steam Deck** | [linux tarball](/jkdansereau/goldeneye-pc-port/releases) | SDL2 is bundled, so it runs as-is on any distro, and sideloads onto a Deck with nothing installed. |
|
||||
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.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](/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](/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.2.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.2.0 - playable, with known rough edges.** The full single-player
|
||||
campaign is completable end to end (all 21 missions, Agent difficulty,
|
||||
playtested), at a steady 60 fps; all 21 solo missions load, render and run
|
||||
crash-free, verified on Windows, Linux and real Steam Deck hardware. Feedback
|
||||
is very welcome.
|
||||
|
||||
**Working:** boot sequence and front end (menu → mission select → briefing →
|
||||
start); all 21 solo missions load, render and are crash-free (full campaign
|
||||
playtested end to end at Agent difficulty); steady 60 fps
|
||||
(software RSP off the presentation critical path); full audio: in-level music and SFX; keyboard + mouse (click-to-lock, proportional aim mode) and a modern
|
||||
dual-stick controller layout; 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.
|
||||
|
||||
**Known issues:**
|
||||
|
||||
- **Cutscenes still glitch, mostly with James Bond**: in scripted sequences
|
||||
Bond is the one who gets misplaced, hovers, or spins; the other actors are
|
||||
fine for the most part now. The Dam level-end cutscene is racy (D243).
|
||||
Still the most visible gap in this release.
|
||||
- Particle colours drift through a rainbow palette instead of holding their
|
||||
grey/orange intent (bullet sparks, lingering smoke/explosion residue) (D252).
|
||||
- Water levels show a moving seam between two water patterns (D245); thin
|
||||
pixel strips at the left/right screen edges at non-integer window scales
|
||||
(D246).
|
||||
- Some front-end 3D models are off: the spinning Nintendo logo renders as two
|
||||
white blobs and the Rareware logo's texture filtering looks wrong (D75).
|
||||
- The F10 overlay's bottom row duplicates whatever item is currently selected
|
||||
(D251).
|
||||
- **Steam Deck:** one intermittent SIGSEGV remains in heavy firefights (D255);
|
||||
current builds capture full faulting registers in `ge007.crash.log`.
|
||||
- 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](/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](/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. 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), verified on the Deck; one intermittent
|
||||
SIGSEGV in heavy firefights remains open (D255).
|
||||
|
||||
**In-game settings on the Deck.** The options overlay is fully gamepad-driven:
|
||||
it opens with **Select**, the D-pad or left stick (up/down) moves between
|
||||
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.
|
||||
|
||||
## 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.2.0 public release; full campaign playable at 60 fps (see [Status](#status)) | Playable full game with online multiplayer |
|
||||
| **Why it exists** | To run the *original* N64 game from source, and as a [case study in AI-agent collaboration](#background) on a hard low-level codebase | To get a playable PC release of the remaster |
|
||||
| **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 |
|
||||
@@ -188,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](/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
|
||||
@@ -216,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
|
||||
@@ -240,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
|
||||
@@ -303,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
|
||||
|
||||
@@ -321,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
|
||||
@@ -354,7 +396,7 @@ This port is a thin layer on a large amount of other people's work.
|
||||
**Methodology**
|
||||
|
||||
- Chris Lewis, [*"Decompiling a Nintendo 64 Game in 84 Days"*](https://blog.chrislewis.au/decompiling-a-nintendo-64-game-in-84-days/) (the Snowboard
|
||||
Kids decompilation write-up), the agent-workflow practices in
|
||||
Kids decompilation write-up) — the agent-workflow practices in
|
||||
[`docs/dev-process.md`](docs/dev-process.md) are adapted from it.
|
||||
|
||||
**Tools and models used to develop the port**
|
||||
@@ -364,7 +406,7 @@ This port is a thin layer on a large amount of other people's work.
|
||||
`UD-Q4_K_XL` quant;
|
||||
[Unsloth](https://unsloth.ai) (the GGUF quantisation and Unsloth Desktop,
|
||||
used as the local model server).
|
||||
- [pi](https://pi.dev/): the local coding-agent harness.
|
||||
- [pi](https://pi.dev/) — the local coding-agent harness.
|
||||
- [Claude / Claude Code](https://claude.com/claude-code) (Anthropic).
|
||||
|
||||
## Legal
|
||||
@@ -373,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
|
||||
@@ -382,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](/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
|
||||
@@ -395,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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -3,8 +3,8 @@
|
||||
title: GoldenEye 007 PC Port
|
||||
description: >-
|
||||
A native PC port of the original Nintendo 64 GoldenEye 007, built from its
|
||||
decompiled source with a software RSP — plus the engineering record:
|
||||
internals, porting notes, and the two-agent development workflow.
|
||||
decompiled source, and a case study in AI-agent collaboration on a large
|
||||
low-level codebase.
|
||||
theme: jekyll-theme-cayman
|
||||
show_downloads: false
|
||||
|
||||
@@ -26,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
|
||||
@@ -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
|
||||
|
||||
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).
|
||||
@@ -1,37 +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
|
||||
|
||||
Most of this port's work is not writing code; it is **diagnosis**: figuring
|
||||
Most of this port's work is not writing code — it is **diagnosis**: figuring
|
||||
out which N64 hardware or ABI behavior a piece of unmodified game code is
|
||||
silently relying on, and satisfying it in the `port/` layer without touching
|
||||
the game's logic. A lot of that diagnosis is done with LLM coding agents, and
|
||||
the workflow below is what makes that productive rather than chaotic.
|
||||
|
||||
For the project context (the two-model split, the timeline, and an honest
|
||||
assessment of what did and did not work), see
|
||||
For the project context — the two-model split, the timeline, and an honest
|
||||
assessment of what did and did not work — see
|
||||
[`dev/agentic-development.md`](dev/agentic-development.md).
|
||||
|
||||
It is adapted from Chris Lewis's write-up
|
||||
[*"Decompiling a Nintendo 64 Game in 84 Days"*](https://blog.chrislewis.au/decompiling-a-nintendo-64-game-in-84-days/)
|
||||
(Snowboard Kids, 2026), an AI-assisted decompilation that ran several times
|
||||
faster than its predecessor. That project bit-matches compiler output;
|
||||
this one matches runtime behavior, so the mechanics differ; but the
|
||||
this one matches runtime behavior, so the mechanics differ — but the
|
||||
project-structure lessons carry over directly.
|
||||
|
||||
## 1. Every investigation task gets a visible budget
|
||||
|
||||
An open-ended tool; for them the permuter, for us "rebuild and run the game
|
||||
and stare at a frame dump"; wrecks throughput when an agent will grind it
|
||||
An open-ended tool — for them the permuter, for us "rebuild and run the game
|
||||
and stare at a frame dump" — wrecks throughput when an agent will grind it
|
||||
indefinitely. Giving the agent a **deadline it can see** lets it trade
|
||||
tool-time against thinking and decide when to give up.
|
||||
|
||||
So every investigation brief states:
|
||||
|
||||
- a **budget**; "≤ N build → run → inspect cycles" or "~M minutes"; and
|
||||
- a **budget** — "≤ N build → run → inspect cycles" or "~M minutes"; and
|
||||
- the **fallback on expiry**: stop, revert any temporary probes, and write up
|
||||
what was found with an explicit confidence rating. A good write-up of a
|
||||
half-solved bug is a deliverable, not a failure.
|
||||
@@ -45,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,
|
||||
@@ -71,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.
|
||||
@@ -85,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.
|
||||
|
||||
@@ -94,58 +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.
|
||||
|
||||
## 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: no game-logic changes; ABI/layout/format only; #ifdef PORT; documented in the finding log.
|
||||
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>.
|
||||
REPORT: (a) root cause + file:line evidence (b) fix diff, or why not
|
||||
(c) probes left in tree (d) confidence.
|
||||
Append any generalisable quirk to docs/porting-notes.md.
|
||||
|
||||
@@ -1,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 |
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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-04 (M-51). 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,31 +22,8 @@ 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_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_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_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_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) |
|
||||
@@ -58,11 +33,7 @@ Two classes:
|
||||
| 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) |
|
||||
@@ -73,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) |
|
||||
@@ -93,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).
|
||||
@@ -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 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. **M-87 user playtest:** Bond floating/hover cutscene glitch (D173) still present; new specific instance — **Bond spins in circles in some cutscenes**, named example the end-of-Cradle Trevelyan-falls cutscene. Likely the same actor-position/anim-root cause manifesting as bad rotation instead of bad height. |
|
||||
| ~~**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 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). | **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. |
|
||||
| **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. | **Observed, not investigated** (M-106, user QA report). Billboards = camera-facing quads; "wall of tree texture" reads as either (a) the billboard orientation/quaternion missing so all quads emit coplanar in world space, or (b) a UV/texture-rect setup error mapping the whole sheet onto each quad. Distinct from D176(b)'s tree-*line* backdrop static (that's the painted perimeter wall; this is discrete 2D tree objects). First checks: `GE_PCDUMP` at the Surface 1 opening; inspect one tree billboard's emitted vertices + texture coords in fast3d; check whether the quads are camera-facing on N64 reference footage. |
|
||||
| **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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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,
|
||||
©_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`, `©_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.
|
||||
@@ -1,287 +0,0 @@
|
||||
# Widescreen & FOV Plan
|
||||
|
||||
Status: **proposal, parked** — like `UNLOCKED-FPS-PLAN.md`, resume after audio
|
||||
work (project Phase 3). Research current as of this writing.
|
||||
|
||||
Scope decision (agreed): **target exactly two display aspects — 4:3 and 16:9.**
|
||||
Arbitrary aspect ratios are TBD and out of scope; the window manager should
|
||||
snap/letterbox to one of the two rather than trying to serve anything else.
|
||||
|
||||
Standard (agreed): **Option B (native 16:9 render, PD-parity) is the goal.**
|
||||
The PD PC port's widescreen architecture is the reference standard for this
|
||||
engine family. Option A (4:3-internal + stretch) remains an acceptable
|
||||
**case-by-case workaround** only where B proves impractical for a specific
|
||||
surface — each such fallback documented individually.
|
||||
|
||||
## Problem
|
||||
|
||||
At high resolutions (e.g. 4K) in a 16:9 window the game is disorienting. Root
|
||||
cause, confirmed in code: the port renders in *window-resolution space*. All
|
||||
320×240 logical screen coordinates are scaled per-axis by
|
||||
`RATIO_X = win_w/320`, `RATIO_Y = win_h/240` (`port/fast3d/gfx_pc.cpp:53-54`).
|
||||
In a 16:9 window everything — world geometry **and** HUD — is stretched 4/3
|
||||
horizontally: circles become ellipses, and the horizontal FOV balloons from
|
||||
~78° to ~91° while vertical stays 60°. That stretch is the disorientation.
|
||||
|
||||
## Research findings
|
||||
|
||||
### 1. The FOV model (where FOV lives)
|
||||
|
||||
- Vertical FOV is a fixed 60° base: `FOV_Y_F` (`src/fr.h:137`); per-player
|
||||
`fovy`/`aspect` fields default to 60 / `DEFAULT_ASPECT`
|
||||
(`src/game/player.c:454-455`).
|
||||
- Setters: `viSetFovY` / `viSetAspect` / `viSetFov(fovx, fovy)`
|
||||
(`src/fr.c:911-934`) → `guPerspectiveF(g_viProjectionMatrixF, …, fovy,
|
||||
aspect, znear, zfar, 1.0f)` (`src/fr.c:718`).
|
||||
- **FOV is dynamic**: aim zoom rewrites fovy every frame via
|
||||
`viSetFovY(g_CurrentPlayer->zoominfovy)` (`src/game/bondview2.c:3123`), and
|
||||
the camera path resets fovy/aspect per frame
|
||||
(`bondviewMovePlayerUpdateViewport`, `src/game/bondview2.c:7967-8034`).
|
||||
⇒ Any FOV option must be a *multiplicative correction composable with zoom*,
|
||||
not an override of a constant.
|
||||
|
||||
### 2. The game ships with a (stretch-based) 16:9 path
|
||||
|
||||
This decomp contains a player-facing widescreen path from the original:
|
||||
options-menu **"Ratio": Normal / 16:9** (`GAME_OPTIONS_INDEX_RATIO`, string
|
||||
`OPTION_STR_23_169_LF`, `src/game/options.c:129`), save bit
|
||||
`OPTION_SCREENRATIO` (`src/game/file2.c:1266-1270, 1323`),
|
||||
`get/set_screen_ratio()` (`src/game/options.c:553-558`). When active, the
|
||||
camera sets `aspect = (vp_w/vp_h) × 0.75 × 16/9` (= viewport ratio × 4/3,
|
||||
`src/game/bondview2.c:8007-8034`) — the pre-distortion for a **4:3 render
|
||||
stretched to 16:9** (N64-out-4:3, TV stretches). HUD compensation exists for
|
||||
crosshair (`gunfire.c:6316-6318`, half-width ×0.75) and rocket aim
|
||||
(`gunfire.c:501`); EU-only extras need an NTSC audit.
|
||||
|
||||
⇒ This path is exactly **Option A's** game-side machinery. It is *not* the
|
||||
PD standard (which renders native 16:9), so under Option B it stays dormant
|
||||
except as fallback.
|
||||
|
||||
### 3. Current port rendering pipeline (what to build on)
|
||||
|
||||
- Render happens at window size; an FBO path already exists and activates when
|
||||
internal dims ≠ window dims or MSAA > 1 (`gfx_pc.cpp:3041-3115+`).
|
||||
- Native-res bookkeeping: `GE_NATIVE_W/H` = 640×480 (NTSC) / 640×400 (PAL)
|
||||
(`port/src/video.c:40-44`); VI size requests clamp to ≤640
|
||||
(`port/src/libultra.c:816`). Auto window defaults to 4:3
|
||||
(`port/fast3d/gfx_sdl2.cpp:107`).
|
||||
|
||||
### 4. fast3d EXT aspect commands — already present in GE's RSP
|
||||
|
||||
GE's software RSP (adapted from PD's) implements Rare's PC-port extension
|
||||
geometry-mode commands `G_ASPECT_{LEFT,RIGHT,WIDE,CENTER}_EXT`
|
||||
(`gfx_pc.cpp:1954-1979`, decoder at `gfx_sp_extra_geometry_mode`), and
|
||||
`port/fast3d/gbiex.h` carries the `gSP…EXT` command macros. **The RSP side of
|
||||
the PD standard already works in this codebase**; only the game-code emission
|
||||
sites are missing.
|
||||
|
||||
### 5. The PD standard (reference architecture — what Option B replicates)
|
||||
|
||||
From the `pd_port` checkout (N64 PD had no widescreen; the port added it):
|
||||
|
||||
1. **Projection aspect comes from the port**: `videoGetAspect()` returns the
|
||||
real window aspect (`gfx_current_dimensions.aspect_ratio`,
|
||||
`port/src/video.c:232-235`). Game code calls `viSetAspect(videoGetAspect())`
|
||||
(e.g. `title.c:2495`; even `#define TITLE_ASPECT (videoGetAspect())`).
|
||||
Native 16:9 projection ⇒ correct perspective, vertical FOV preserved.
|
||||
2. **Per-element screen-space corrections** where an element is positioned in
|
||||
screen coords: `sightGetAdjustedX(x)`, `×(SCREEN_ASPECT / videoGetAspect())`
|
||||
scalings (`sight.c:77,921`, `sky.c:2728+`, `menu.c:2332,4771`,
|
||||
`player.c:3108`).
|
||||
3. **HUD alignment via the EXT commands**: screen-space draw segments wrapped
|
||||
in `gSPSetExtraGeometryModeEXT(gdl, G_ASPECT_CENTER_EXT)` …
|
||||
`gSPClearExtraGeometryModeEXT(…)`, all under `#ifndef PLATFORM_N64`. With
|
||||
the mode active, fast3d scales 4:3-logical screen coords by the *native*
|
||||
aspect and centers (or left/right-aligns) them in the wider window — no
|
||||
distortion. PD scale: **52 emission sites across 10 game files** (sight,
|
||||
hudmsg, bondgun, menu, radar, title, propobj, credits, activemenu,
|
||||
game_1531a0).
|
||||
4. User-facing: vsync/framerate options + HUD-centering option
|
||||
(`g_HudCenter` → `g_HudAlignModeL/R`, `port/src/main.c:77-83`).
|
||||
|
||||
**M-87 PD-legacy-survey notes (`docs/dev/notes/PD-LEGACY-SURVEY.md`):**
|
||||
- **Overscan/safe-frame (candidate 2, deliberate divergence, recorded here):**
|
||||
PD hardcodes its native viewport to 320×220 (`port/src/video.c:77-79`) — the
|
||||
N64-on-a-TV visible area after ~20 lines top/bottom of overscan. **GE's port
|
||||
renders the full 640×480/640×400 frame, no overscan crop** (`port/src/video.c:38-47`)
|
||||
— a deliberate PC-default choice (nothing hidden, no HUD-clipping risk from a
|
||||
TV-only limitation), not an oversight, and not something to "fix" toward PD's
|
||||
behavior. Net effect: GE shows ~9% more vertical content than PD at the
|
||||
equivalent native resolution. Revisit only if a `Video.SafeAreaOverlay` debug
|
||||
knob is ever wanted to visualize the original TV-safe region.
|
||||
- **"Ratio" 4:3/16:9 option (candidate 3) — correction, M-87 review:** PD
|
||||
deleted its N64-era per-player Ratio dropdown on PC (`options.c` forces
|
||||
`SCREENRATIO_NORMAL` outside `PLATFORM_N64`). GE's equivalent (`SCREEN_RATIO_16_9`,
|
||||
`get/set_screen_ratio`, `options.c:553-558`, persisted per-save, consumed by
|
||||
the camera) is a live, functioning code path — but it's an old **anamorphic
|
||||
stretch toggle**, not true wider-FOV rendering, and this doc's own **Open
|
||||
Questions** (below) already flag the tension and lean the *other* way: leave
|
||||
this legacy flag **dormant** and have a **separate, new, port-owned
|
||||
`Video.Aspect` config** be Option B's actual trigger — the same
|
||||
separation-of-concerns choice PD itself made (PD didn't repurpose its legacy
|
||||
flag either). Repurposing the shipped save-flag to drive a brand-new render
|
||||
path risks desync (existing saves silently gaining/missing the modern
|
||||
feature, or the flag and `Video.Aspect` disagreeing). **Not yet decided**
|
||||
("Decide Phase 1" below) — correcting an earlier note here that overstated
|
||||
this as settled.
|
||||
|
||||
## Option B — the standard (native 16:9, PD-parity)
|
||||
|
||||
### Architecture
|
||||
|
||||
Render directly at the 16:9 window resolution. World: projection aspect =
|
||||
window aspect (vertical FOV preserved, no post-stretch). HUD/screen-space:
|
||||
4:3-logical coordinates mapped undistorted via the EXT aspect modes
|
||||
(centered by default; left/right alignment available à la PD). 4:3 windows:
|
||||
byte-identical to today.
|
||||
|
||||
### Change inventory
|
||||
|
||||
**Port layer (no rules issues):**
|
||||
- [ ] `videoGetAspect()` equivalent in `port/src/video.c` (return window
|
||||
aspect; GE already tracks `gfx_current_dimensions.aspect_ratio`).
|
||||
- [ ] Window/aspect policy: `Video.Aspect` {4:3, 16:9}, snap on create/resize;
|
||||
out-of-scope aspects → nearest + note (TBD).
|
||||
- [ ] Make `port/fast3d/gbiex.h` reachable from game TUs (include path or a
|
||||
shim header) — build-system only.
|
||||
- [ ] HUD alignment mode plumbing if we adopt PD's center/wide option
|
||||
(`g_HudAlignModeL/R` pattern) — decide in Phase 1; default = centered.
|
||||
|
||||
**Game code (exception class — see below):**
|
||||
- **(a) Projection aspect path.** `bondviewMovePlayerUpdateViewport`
|
||||
(`bondview2.c:8007-8034`): under PORT + 16:9, use window aspect instead
|
||||
of the ×4/3 stretch formula. Plus the menu/title/briefing call sites
|
||||
`viSetFovY(FOV_Y_F); viSetAspect(ASPECT_RATIO_SD)` (`front.c` — ~7+
|
||||
sites; enumerate in Phase 1) → PD pattern `viSetAspect(videoGetAspect())`.
|
||||
- **(b) HUD / screen-space draw sites.** Audit + wrap with EXT aspect modes
|
||||
and/or per-element corrections, per surface: gameplay HUD (crosshair —
|
||||
replaces the existing ×0.75 compensation; ammo/timer/text via
|
||||
`textrelated.c`), menus (`front.c`), title/briefing/credits, letterbox
|
||||
bars (`fr.c` gDPFillRectangle calls). GE's known screen-space emitters:
|
||||
`front.c`, `textrelated.c`, `gunfire.c`, `bondwalk2.c` (initial grep;
|
||||
Phase 1 completes the audit). Expect PD-comparable scale (~10 files),
|
||||
possibly less.
|
||||
- **(c) Anything the audit surfaces** (e.g. mouse→screen mapping for menus,
|
||||
cutscene camera framing) — each item logged individually.
|
||||
|
||||
### Rule #2 exception framing (needs explicit sign-off to start Phase 2)
|
||||
|
||||
This is a behavior-affecting game-code edit class, outside the ABI-only D3x
|
||||
exception. The case for granting it:
|
||||
|
||||
1. **Direct sister-engine precedent**: PD's port made exactly these edits on
|
||||
the same Rare engine/fast3d; the project designates PD as standing
|
||||
reference for port patterns. We replicate a proven, bounded change class,
|
||||
not inventing one.
|
||||
2. **Bounded and gated**: every edit is `#ifdef PORT` (and where sensible
|
||||
runtime aspect-gated) so the N64 build and the 4:3 default path stay
|
||||
byte-identical; golden/determinism tests unaffected at 4:3.
|
||||
3. **The decomp already contains** both a shipped 16:9 branch and `#ifdef
|
||||
PORT` precedents in game files — the pattern is not foreign to this tree.
|
||||
|
||||
Conditions: explicit user sign-off before Phase 2; each change (or tight
|
||||
cluster) gets a findings.md entry with §F index label citing the PD analogue;
|
||||
4:3 byte-identity verified per phase; any edit that would alter *sim* behavior
|
||||
(not just presentation) stops the work and comes back for review.
|
||||
|
||||
### Fallback (case-by-case, documented)
|
||||
|
||||
If the Phase 1 audit shows a specific surface is disproportionately costly or
|
||||
can't be made clean under B (candidate: complex cutscene framing), that
|
||||
surface may fall back to **Option A** — 4:3-internal render + edge-to-edge
|
||||
stretch using the game's shipped `SCREEN_RATIO_16_9` path (`set_screen_ratio`
|
||||
is an existing API; FBO+blit infra exists) — recorded as a finding with the
|
||||
reason. Whole-feature fallback to A only if B fails verification overall.
|
||||
|
||||
## FOV adjustment design (port-only, applies to both options)
|
||||
|
||||
- **Hook: software RSP projection post-scale.** Wherever the RSP applies the
|
||||
uploaded projection matrix, scale its vertical row by
|
||||
`tan(fov_orig/2) / tan(fov_adj/2)`. Zoom rewrites the matrix every frame, so
|
||||
post-scaling whatever arrives composes with aim zoom for free. No game code.
|
||||
- Rejected: editing `viSetFovY` call sites (game code); writing
|
||||
`g_ViBackData` from the port (racy).
|
||||
- UI: `Video.FovScale` (percent of original vertical FOV, e.g. 50–150),
|
||||
default 100 = byte-identical to today.
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 0 — Verification research (port-only, read-mostly)
|
||||
|
||||
- [ ] Playtest the in-game Ratio option as-is on PC; document + screenshot
|
||||
what the shipped stretch path does under the current pipeline.
|
||||
- [ ] Measure baseline FOVs (4:3 and current 16:9-stretch) from the projection
|
||||
matrix or a calibration render; record numbers.
|
||||
- [ ] Confirm `gbiex.h` EXT commands round-trip through GE's RSP with a test
|
||||
DL (they should — decoder exists).
|
||||
- [ ] Audit EU-only aspect constants (`g_GunSightAspectRatio`,
|
||||
`EU_CAMERA_8003642C_ASPECT`) for NTSC/PAL effect.
|
||||
- [ ] Record a Dxxx finding consolidating baselines.
|
||||
|
||||
### Phase 1 — Port foundation + full change audit (port-only)
|
||||
|
||||
- [ ] `videoGetAspect()`, window/aspect policy, gbiex.h include path.
|
||||
- [ ] Complete the screen-space draw-site audit for inventory item (b): every
|
||||
emitter, its coordinate assumptions, proposed wrap/correction, PD
|
||||
analogue cited. Output = the signed change list for Phase 2/3.
|
||||
- [ ] Decide HUD alignment default (centered vs PD's center/wide option).
|
||||
|
||||
### Phase 2 — Projection aspect (game-code exception class, small wave)
|
||||
|
||||
- [ ] Item (a): camera path + front.c call sites, `#ifdef PORT`-gated.
|
||||
- [ ] Verify: world perspective correct at 16:9 (FOV measurement vs Phase 0),
|
||||
aim zoom composes, 4:3 byte-identical.
|
||||
|
||||
### Phase 3 — HUD alignment wave (game-code exception class)
|
||||
|
||||
- [ ] Item (b), per surface in order: gameplay HUD → menus →
|
||||
title/briefing/credits → letterbox/misc. Each surface: change, visual
|
||||
verification at 4:3 + 16:9, finding entry. Fallback-to-A decisions
|
||||
happen here, per surface, with documented reason.
|
||||
|
||||
### Phase 4 — FOV slider (port-only) — **LANDED (M-83, D211)**
|
||||
|
||||
- [x] RSP projection post-scale (`gfx_apply_fov_scale()` in
|
||||
`port/fast3d/gfx_pc.cpp`, load-path only); `Video.FovScale` config
|
||||
(percent of vertical FOV, 50–150, default 100 = byte-identical) + F10
|
||||
overlay row. Perspective-only (ortho/HUD untouched); columns 0+1 scaled
|
||||
together so aspect is preserved. Build clean, `bunker1` no-op confirmed.
|
||||
- [ ] Owed: interactive eyeball of composition with aim-zoom at scale ≠ 100.
|
||||
|
||||
This phase does **not** depend on the Phase 2/3 game-code exception — it
|
||||
shipped standalone ahead of the rest of the plan.
|
||||
|
||||
### Phase 5 — Verification & sign-off
|
||||
|
||||
- [ ] FOV measurements per mode/scale vs Phase 0 baselines.
|
||||
- [ ] Full HUD pass at 4:3 + 16:9; cosmetics → `GRAPHICS-BACKLOG.md`.
|
||||
- [ ] 4K performance check; MSAA interaction; config persistence; screenshots.
|
||||
- [ ] `./build-pc.sh` + `/linkcheck`; findings complete with §F index labels.
|
||||
|
||||
## Open questions
|
||||
|
||||
- HUD alignment UX: always-centered (simple) vs PD's center/wide option
|
||||
(more faithful, more surface area). Decide Phase 1.
|
||||
- In-game Ratio menu entry under Option B: the shipped option means "stretch
|
||||
mode" — keep it as the *fallback* trigger, hide it, or repurpose? Decide
|
||||
Phase 1 (leaning: leave dormant; port config owns display mode).
|
||||
- Mouse/menu coordinate mapping at 16:9 (PD had `videoGetAspect()`-scaled
|
||||
mouse math in menu.c) — confirm GE's menu input path needs the same.
|
||||
- Ordering vs `UNLOCKED-FPS-PLAN.md`: both parked behind audio; B renders
|
||||
native so the FPS plan's present-stage duplication slots in unchanged.
|
||||
Decide order when audio lands.
|
||||
- PAL: `GE_NATIVE` 640×400 bookkeeping — confirm aspect math per region.
|
||||
- Split-screen viewports under 16:9 — likely out of scope for PC; note and close.
|
||||
- **M-87 QoL ask (user, not critical, backlogged):** expose `Video.FovScale` as
|
||||
a real horizontal-or-vertical **degree** value instead of the current
|
||||
percent-of-original-vertical-FOV scale (D211) — the "50–150%" knob doesn't
|
||||
tell the user what degree FOV they're actually getting. Needs: pick a
|
||||
convention (report vertical, since that's what `frFovY` actually is post-D211
|
||||
refix — `fr.c:737`) and either (a) relabel the F10 row + INI value as degrees
|
||||
directly (`frFovY = clamp(requestedDeg, ~24°, 160°)`, drop the percent
|
||||
multiply), or (b) keep percent internally but show the *computed* degree
|
||||
value live in the F10 overlay/INI comment for transparency. (a) is cleaner
|
||||
and removes a level of indirection; check whether anything else reads
|
||||
`Video.FovScale` as a percent before renaming the key (search besides
|
||||
`fr.c`/`video.c`). Cosmetic/UX only, no render-path change beyond the
|
||||
input unit.
|
||||
@@ -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
|
||||
|
||||
*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 playable front end
|
||||
title GoldenEye 007 PC port — from fork to playable front end
|
||||
section Local model - Qwen 3.8 via pi
|
||||
Fork + PC-port scaffolding :m1, 2026-08-16, 1d
|
||||
Full compile + link (~230 TUs) :m2, 2026-08-20, 1d
|
||||
@@ -90,7 +87,7 @@ gantt
|
||||
- **Day 4** (20 Aug): the entire ~230-TU game + libultra set compiles and
|
||||
links as a host binary.
|
||||
- **Day 6** (22 Aug): first real frames render.
|
||||
- **Day 8** (24 Aug): the whole intro sequence renders; logos, gun-barrel,
|
||||
- **Day 8** (24 Aug): the whole intro sequence renders — logos, gun-barrel,
|
||||
cast roll.
|
||||
- **Day 11** (27 Aug): second agent joins.
|
||||
- **Day 13** (29 Aug): **all 21 solo missions load, render, and survive an
|
||||
@@ -100,22 +97,22 @@ gantt
|
||||
|
||||
So roughly **two weeks**, one person part-time, to take a decompilation from
|
||||
"builds an N64 ROM" to "boots on desktop, renders every solo level, playable
|
||||
through the front end into the early game"; with audio and a set of cosmetic
|
||||
through the front end into the early game" — with audio and a set of cosmetic
|
||||
issues still outstanding.
|
||||
|
||||
## By the numbers
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Calendar time | 26 days (17 Aug – 11 Sep 2026), one person part-time |
|
||||
| Commits on the port | ~430 |
|
||||
| Root-caused bugs logged | `D1`–`D226` in [`findings.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/findings.md), 116 still-listed entries (some labels later merged or withdrawn) |
|
||||
| Handoff sessions | 89+ (`M-2` … `M-89`, and counting) |
|
||||
| Game-source files given `#ifdef PORT` ABI edits | 67 files, ~305 blocks |
|
||||
| New port-layer / tooling files | ~114 |
|
||||
| Port layer | ~21,900 lines C/C++ (`port/`) |
|
||||
| PC asset-conversion tooling | ~7,050 lines Python (`tools_pc/`) |
|
||||
| Outcome | intro + all 21 solo missions render and are crash-free, front end playable, in-level SFX + menu/intro music play; in-level music and full-campaign completion re-confirmation still owed |
|
||||
| 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
|
||||
|
||||
@@ -139,7 +136,7 @@ pie showData title "Commits by agent (raw count)"
|
||||
"Local model (Qwen 3.8 / pi)" : 62
|
||||
```
|
||||
|
||||
(Phase A, the first 26 commits, to 24 Aug, was entirely the local model,
|
||||
(Phase A — the first 26 commits, to 24 Aug — was entirely the local model,
|
||||
solo; from 27 Aug on the two agents worked in parallel.)
|
||||
|
||||
| Milestone / workstream | Primary agent | Weight |
|
||||
@@ -150,20 +147,20 @@ solo; from 27 Aug on the two agents worked in parallel.)
|
||||
| Offline asset-conversion architecture + first converters (`tools_pc/`) | local model | large |
|
||||
| Intro rendering (logos → gun-barrel → cast) | local model | medium |
|
||||
| Stage load unblocked (`D69`–`D87`) | Claude | medium |
|
||||
| 21-level crash sweep; ~12 crash classes root-caused (`D88`–`D169`) | Claude | large |
|
||||
| 21-level crash sweep — ~12 crash classes root-caused (`D88`–`D169`) | Claude | large |
|
||||
| SDL input layer (keyboard / mouse / gamepad, mouse-look) | Claude | medium |
|
||||
| Front-end flow (menu → briefing → start), EEPROM saves | Claude | medium |
|
||||
| Parallel struct-layout / converter static audits | local model | medium |
|
||||
| Continuing tasks after Claude hit a usage limit | local model | small |
|
||||
| The hardest structural bugs (matrix handedness, format specs) | Claude | 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"
|
||||
@@ -202,28 +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. 162 numbered entries,
|
||||
2. **`findings.md`** — the chronological finding log. 162 numbered entries,
|
||||
each a root cause with `file:line` evidence and the fix. New agents (either
|
||||
model) are pointed at the relevant entries before they start.
|
||||
3. **`porting-notes.md`**; the append-only "recurring bug classes" file. The
|
||||
3. **`porting-notes.md`** — the append-only "recurring bug classes" file. The
|
||||
single highest-leverage artifact: it stopped both models from
|
||||
re-deriving the same class of N64→PC bug over and over.
|
||||
|
||||
Around these, the working rules (full detail in
|
||||
[`../dev-process.md`](../dev-process.md)):
|
||||
|
||||
- **file-partitioned tasks**; each agent's task scoped to a disjoint set of
|
||||
- **file-partitioned tasks** — each agent's task scoped to a disjoint set of
|
||||
files so patches never collided;
|
||||
- **visible budgets**; every investigation task carried an explicit
|
||||
- **visible budgets** — every investigation task carried an explicit
|
||||
"N build→run cycles" limit and a defined fallback (revert probes, write up
|
||||
with a confidence rating);
|
||||
- **the human owns verification**; building, running the game, capturing a
|
||||
- **the human owns verification** — building, running the game, capturing a
|
||||
frame, and judging it against N64 reference footage was never delegated.
|
||||
|
||||
## Assessment
|
||||
@@ -232,7 +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.
|
||||
@@ -241,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
|
||||
@@ -258,11 +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** (parked below crash/level work): audio is not
|
||||
implemented; several front-end 3D transforms and some text rendering are
|
||||
wrong. See
|
||||
[`GRAPHICS-BACKLOG.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/GRAPHICS-BACKLOG.md).
|
||||
wrong. See [`GRAPHICS-BACKLOG.md`](GRAPHICS-BACKLOG.md).
|
||||
@@ -1,143 +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,PROPOSED,disclaimer/legal screen only draws line 1 —,fix proposed (not applied —
|
||||
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, port/src/input.c):",FIXED
|
||||
D180 · D181,PARTIAL,"native-PC input pass (QoL run, qol/native-pc-input-menu):","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,?,"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,?,"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,?,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 screen: the mouse hit-box for a file slot is vertically offset from its visible box —,FIXED (M-91) — root cause was NOT a coordinate/origin mismatch:
|
||||
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,FIXED,"QoL gap (M-87, from a PD-PC-port survey):","DEPRIORITIZED (M-87, user call)."
|
||||
D225,OPEN,"Dev-tooling QoL (M-87, user ask):","OPEN — root-caused, not implemented."
|
||||
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,"Water on IsWater levels renders green and pulses blue<->green on an interval (M-101, user playtest, Frigate -level_26; Surface 2 also an IsWater level).",FIXED (port-layer) — root cause:
|
||||
D230,OPEN,In-level music quality:,"OPEN — static-analysis pass done (M-108), not runtime-confirmed."
|
||||
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 (and other) weapon models still show WHITE missing-texture patches after D217's texpool-alignment fix —,"FIXED (port/fast3d/gfx_pc.cpp, palette_to_rgba32) —"
|
||||
D204,FIXED,PC audio ran ~2 % below real time permanently:,"FIXED (port/src/audio.c, port/src/libultra.c, port/include/audio.h) —"
|
||||
D203,?,"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,RESOLVED,"Silenced PPK plays a ""slap"" instead of a gunshot, PLUS user reports broader real-time audio corruption (M-52 → M-59 → REOPENED M-60 → M-61/M-62 closed the data…",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 → soundArray[soundIndex - 1], N64 line verbatim under #else)."
|
||||
D207,PROPOSED,Alarm klaxon starves combat SFX and (per user) doesn't recover after it stops —,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,RESOLVED,PC plays wrong/extra SFX for gameplay events the user can A/B against N64:,CLOSED (M-78) — root cause was D206; all four symptoms resolved + user-confirmed by ear vs N64.
|
||||
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 (user QA report):,"OPEN — observed, not investigated"
|
||||
D176,FIXED,"Surface exterior renders wrong (-level_36), two independent defects.","(a) ""scrolls too fast/tiles too visibly"" ROOT-CAUSED + FIXED (see ""D176(a) —"
|
||||
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,OPEN,Third-person Bond model spawns too high (user QA report):,"OPEN — observed, not investigated."
|
||||
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,PARTIAL,Game.ScreenShakeIntensity —,"LANDED behind config, default = original."
|
||||
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 + third-person Bond model textures render garbled —,FIXED (M-90/M-92/M-97; PR #47 texpool alignment + PR #49 CI-cache palette key) —
|
||||
D218,FIXED,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) —"
|
||||
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 deferred; framediff.py structural.,"OPEN (mode attempted, not achieved)"
|
||||
D118,RESOLVED,SDL input layer (port/src/input.c); M-24 mouse-look rework:,resolved (D118a residual)
|
||||
D150,FIXED,watch OBJECTIVES / BRIEF page crash:,FIXED (src/str.c #ifdef PORT); interactive re-verify pending
|
||||
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:,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,OPEN,"Mission-failed / objective-failed → permanent black screen (M-28, user bug report —","MITIGATED (port/src/libultra.c, M-28, #ifdef PORT/port-only)."
|
||||
D154,FIXED,"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,PROPOSED,"Disclaimer / legal screen only draws the first line (D76 root cause, M-31).","FIX PROPOSED, NOT APPLIED (front.c is owned by another agent this session)."
|
||||
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 — not investigated
|
||||
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,?,"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,OPEN,"Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject):",OPEN — M-109 re-analysis of the same session's raw logs corrects the timing/pattern claims above; the D134/pendingGfx race hypothesis is likely wrong.
|
||||
D245,OPEN,IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) —,"OPEN — REOPENED (M-116 live playtest, same session):"
|
||||
D246,OPEN,"Tiny pixel strips at the left and right edges of the screen, ""like overscan"" —","OPEN — one candidate ruled out (M-116 follow-up), root cause not found."
|
||||
D247,RESOLVED,Black bars top and bottom of screen —,"NOT A BUG — confirmed faithful, unmodified N64 decomp behaviour, no code change made."
|
||||
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 — not investigated.
|
||||
D250,?,FPS is inconsistent/low on many levels since the port started actually running at 60fps —,
|
||||
D251,FIXED,F10 options overlay QoL pass:,"FIXED (port-only, port/src/optionsoverlay.c)."
|
||||
D252,?,"Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).",
|
||||
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,OPEN,"Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle, D253 fix confirmed working —","OPEN — case identified (NULL model on an active OBJ record; destroy-path ordering), reproducible on Deck in combat (Bunker + Frigate); fix pending rule-#2 sign-off."
|
||||
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,OPEN,Game.AllUnlocked (D257) breaks audio on a fresh save:,OPEN — quirk documented; default flipped OFF for v0.2.0 (port/src/video.c); fix deferred.
|
||||
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)."
|
||||
|
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 531 KiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 621 KiB |
|
Before Width: | Height: | Size: 287 KiB |
|
Before Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 286 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 456 KiB |
|
Before Width: | Height: | Size: 893 KiB |
@@ -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,
|
||||
|
|
Before Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 217 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 125 KiB |
@@ -1,206 +1,34 @@
|
||||
---
|
||||
title: GoldenEye 007 PC Port
|
||||
description: >-
|
||||
A native PC port of the original Nintendo 64 GoldenEye 007, built from its
|
||||
decompiled source with a software RSP. v0.2.0 is out for Windows
|
||||
and Linux (including Steam Deck): download it, drop in your own ROM, and
|
||||
play; or dig into the code and the engineering record behind it.
|
||||
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.
|
||||
---
|
||||
|
||||
# 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.2.0.** The full single-player campaign runs at a steady 60 fps
|
||||
and is completable end to end; all 21 missions load and run clean on Windows
|
||||
and Linux (one intermittent Steam Deck crash remains, listed below), and
|
||||
audio (music + SFX) plays throughout. The known
|
||||
rough edges (cutscenes that still glitch, a few music tracks that sound
|
||||
wrong, a handful of cosmetic rendering defects) are listed plainly under
|
||||
[Honest status](#honest-status).
|
||||
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.
|
||||
|
||||
<p align="center">
|
||||
<img src="media/goldeneye-gh-preview.gif" width="70%"
|
||||
alt="~32 s gameplay montage from live play sessions">
|
||||
<br><em>~32 s gameplay montage loop from live play sessions, running in the port.</em>
|
||||
</p>
|
||||
**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.
|
||||
|
||||
## 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.
|
||||
- **The research angle**: this is also a case study in AI-agent collaboration
|
||||
on a large, low-level codebase: two coding agents (a local open-weight model
|
||||
on a single RTX 5090, and Claude / Claude Code), driven by one person
|
||||
part-time through ~230 translation units of unmodified big-endian MIPS game
|
||||
code. [The full write-up](dev/agentic-development.md): setup, timeline, the
|
||||
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
|
||||
|
||||
- **Cutscenes still glitch, mostly with James Bond**: in scripted sequences
|
||||
Bond is the one who gets misplaced, hovers, or spins; the other actors are
|
||||
fine for the most part now. Still the most visible gap in this release.
|
||||
- A few in-level music tracks sound wrong (wrong instruments, occasional garbling).
|
||||
- Particle colours drift through a rainbow palette instead of holding grey/orange.
|
||||
- Water levels show a moving seam; pixel strips at screen edges at non-integer
|
||||
scales; some front-end 3D models mispositioned or absent.
|
||||
- The F10 overlay's bottom row duplicates the selected item; one intermittent
|
||||
Steam Deck SIGSEGV in heavy firefights remains open (D255).
|
||||
- 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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 6.1 MiB |
|
Before Width: | Height: | Size: 27 MiB |
@@ -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_ */
|
||||
@@ -48,7 +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_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);
|
||||
|
||||
@@ -696,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);
|
||||
@@ -1091,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) {
|
||||
|
||||
@@ -150,21 +150,14 @@ struct LoadedTexture {
|
||||
uint32_t line_size_bytes;
|
||||
uint32_t tex_flags;
|
||||
struct RawTexMetadata raw_tex_metadata;
|
||||
/* D229: G_IM_FMT_* of the gDPSetTextureImage active when this slot was
|
||||
* last written by a load command (0xFF = never / unknown). Used to tell
|
||||
* a CI8 index stream apart from real RGBA16 pixels when a later tile
|
||||
* re-declares the same TMEM in another format -- see import_texture. */
|
||||
uint8_t src_fmt = 0xFF;
|
||||
};
|
||||
|
||||
static struct RDP {
|
||||
uint16_t palette[256];
|
||||
const uint8_t* palette_addrs[2];
|
||||
uint32_t palette_fmt;
|
||||
uint32_t palette_hash; /* D217: FNV-1a of palette[], refreshed in gfx_dp_load_tlut */
|
||||
struct {
|
||||
const uint8_t* addr;
|
||||
uint8_t fmt; /* D229: was dropped before; needed to track CI sources */
|
||||
uint8_t siz;
|
||||
uint32_t width;
|
||||
uint32_t tex_flags;
|
||||
@@ -650,13 +643,9 @@ static const uint8_t* gfx_tex_normalize_source(const uint8_t* addr, uint32_t ext
|
||||
uint32_t* dst = (uint32_t*)buf.data();
|
||||
for (uint32_t i = 0; i < n / 4; i++)
|
||||
dst[i] = PD_BE32(src[i]);
|
||||
{
|
||||
static int ge_d71log = -1;
|
||||
if (ge_d71log < 0) ge_d71log = getenv("GE_D71LOG") != NULL;
|
||||
if (ge_d71log)
|
||||
fprintf(stderr, "[D71] normalized C-array texture source %p (%u bytes)\n",
|
||||
(const void*)addr, extent);
|
||||
}
|
||||
if (getenv("GE_D71LOG"))
|
||||
fprintf(stderr, "[D71] normalized C-array texture source %p (%u bytes)\n",
|
||||
(const void*)addr, extent);
|
||||
return s_c_array_tex_norms.emplace(addr, std::move(buf)).first->second.data();
|
||||
}
|
||||
|
||||
@@ -846,16 +835,8 @@ static void import_texture_i8(int tile, const LoadedTexture& loaded_texture, boo
|
||||
|
||||
static inline void palette_to_rgba32(const uint16_t palentry, uint8_t *rgba32_buf) {
|
||||
if (rdp.palette_fmt == G_TT_IA16) {
|
||||
/* D228: intensity/alpha were swapped here relative to the (correct)
|
||||
* direct IA16 decode in import_texture_ia16() a few lines above --
|
||||
* that one reads addr[2*i]=intensity, addr[2*i+1]=alpha, i.e.
|
||||
* intensity is the first (high, after gfx_dp_load_tlut's PD_BE16
|
||||
* swap) byte. This path had them backwards: a dim, fully-opaque
|
||||
* palette entry (e.g. intensity=0x58, alpha=0xff) decoded as a
|
||||
* bright, mostly-transparent one (intensity=0xff, alpha=0x58) --
|
||||
* the "white missing-texture patches" on AK47/NPC weapon models. */
|
||||
const uint8_t intensity = palentry >> 8;
|
||||
const uint8_t alpha = palentry & 0xff;
|
||||
const uint8_t intensity = (palentry & 0xff);
|
||||
const uint8_t alpha = palentry >> 8;
|
||||
rgba32_buf[0] = intensity;
|
||||
rgba32_buf[1] = intensity;
|
||||
rgba32_buf[2] = intensity;
|
||||
@@ -1007,9 +988,9 @@ static void import_texture(int i, int tile, bool importReplacement) {
|
||||
TextureCacheKey key;
|
||||
if (fmt == G_IM_FMT_CI) {
|
||||
key = { orig_addr, { rdp.palette_addrs[0], rdp.palette_addrs[1] }, fmt, siz, palette_index,
|
||||
loaded_texture.size_bytes, rdp.palette_hash }; // D217: key on palette content
|
||||
loaded_texture.size_bytes };
|
||||
} else {
|
||||
key = { orig_addr, {}, fmt, siz, palette_index, loaded_texture.size_bytes, 0u };
|
||||
key = { orig_addr, {}, fmt, siz, palette_index, loaded_texture.size_bytes };
|
||||
}
|
||||
|
||||
if (gfx_texture_cache_lookup(i, key)) {
|
||||
@@ -1058,14 +1039,8 @@ static void import_texture(int i, int tile, bool importReplacement) {
|
||||
* RC2 (mip-chain contamination -> over-tall upload) can be told apart from a
|
||||
* decode/row-swap bug. tile_h = base-tile height from SETTILESIZE; if the
|
||||
* computed upload height is much larger, the excess rows are LOD mip data.
|
||||
* docs/dev/TEXTURE-GLITCH-ANALYSIS.md sec 6b.
|
||||
* D250: import_texture() runs on every texture bind -- an uncached
|
||||
* getenv() here measured at ~50% of the hot render thread's total CPU
|
||||
* time on a texture-heavy level (Dam), via a live WPR/xperf profile.
|
||||
* Cache like every other env-gated probe in this codebase. */
|
||||
static int ge_dtex = -1;
|
||||
if (ge_dtex < 0) ge_dtex = getenv("GE_DTEX") != NULL;
|
||||
if (ge_dtex) {
|
||||
* docs/dev/TEXTURE-GLITCH-ANALYSIS.md sec 6b. */
|
||||
if (getenv("GE_DTEX")) {
|
||||
static int dtexCount = 0;
|
||||
if (dtexCount < 64) {
|
||||
const uint32_t row = rdp.texture_tile[tile].line_size_bytes;
|
||||
@@ -1088,9 +1063,7 @@ static void import_texture(int i, int tile, bool importReplacement) {
|
||||
}
|
||||
}
|
||||
|
||||
static int ge_texdump = -1;
|
||||
if (ge_texdump < 0) ge_texdump = getenv("GE_TEXDUMP") != NULL;
|
||||
if (ge_texdump) {
|
||||
if (getenv("GE_TEXDUMP")) {
|
||||
static int tdc = 0;
|
||||
const uint16_t* pal = (const uint16_t*)rdp.palette;
|
||||
sysLogPrintf(LOG_NOTE,
|
||||
@@ -1103,26 +1076,11 @@ static void import_texture(int i, int tile, bool importReplacement) {
|
||||
/* GE_TEXRAW=1 additionally writes the raw source bytes handed to the
|
||||
* importer (texdump/rNNN_f<fmt>_s<siz>_<w>x<h>.bin) -- lets a decode
|
||||
* bug be told apart from a source-data bug offline (D183). */
|
||||
const int tw = (int)(((rdp.texture_tile[tile].lrs - rdp.texture_tile[tile].uls) >> 2) + 1);
|
||||
const int th = (int)(((rdp.texture_tile[tile].lrt - rdp.texture_tile[tile].ult) >> 2) + 1);
|
||||
/* D219: same cap-exhausted-before-the-explosion problem as the
|
||||
* D172/GE_TEXDUMP probes -- never suppress the fire particle image
|
||||
* dump. NOTE: `tw`/`th` above come from gsDPSetTileSize (the DL sets
|
||||
* the fire tile's logical wrap size to 56x56, unrelated to its real
|
||||
* 16x14 pixel content), so they do NOT identify this texture -- a
|
||||
* first attempt at this exemption keyed on tw/th==16/14 and silently
|
||||
* never matched, burning a whole live-playtest cycle for nothing.
|
||||
* The real, always-correct signature is the load's byte count: 16 *
|
||||
* 14 * 2 bytes/texel (RGBA16) = 448, matching CALC_LRS(16,14,...) in
|
||||
* every one of assets/oddtextures.c's 15 fire DLs and confirmed
|
||||
* against the TEXEL1_bytes=448 field already proven out via the
|
||||
* gfx_sp_tri1 D172/D219 probe. */
|
||||
const bool is_fire_bytes = (loaded_texture.size_bytes == 448);
|
||||
static int ge_texraw = -1;
|
||||
if (ge_texraw < 0) ge_texraw = getenv("GE_TEXRAW") != NULL;
|
||||
if ((is_fire_bytes || tdc <= 400) && ge_texraw) {
|
||||
if (tdc <= 400 && getenv("GE_TEXRAW")) {
|
||||
char nm[160];
|
||||
snprintf(nm, sizeof nm, "texdump/r%03d_f%u_s%u_%ux%u.bin", tdc - 1, fmt, siz, tw, th);
|
||||
snprintf(nm, sizeof nm, "texdump/r%03d_f%u_s%u_%ux%u.bin", tdc - 1, fmt, siz,
|
||||
((rdp.texture_tile[tile].lrs - rdp.texture_tile[tile].uls) >> 2) + 1,
|
||||
((rdp.texture_tile[tile].lrt - rdp.texture_tile[tile].ult) >> 2) + 1);
|
||||
FILE* bf = fopen(nm, "wb");
|
||||
if (bf) { fwrite(loaded_texture.addr, 1, loaded_texture.size_bytes, bf); fclose(bf); }
|
||||
}
|
||||
@@ -1135,68 +1093,40 @@ static void import_texture(int i, int tile, bool importReplacement) {
|
||||
* decoding it against the stale rdp.palette produced the blue-speckle roof
|
||||
* (docs/dev/TEXTURE-GLITCH-ANALYSIS.md, B2). Route CI4/CI8 -> I4/I8 here. */
|
||||
uint8_t fmt_eff = fmt;
|
||||
uint8_t siz_eff = siz;
|
||||
if (fmt == G_IM_FMT_CI && rdp.palette_fmt == G_TT_NONE) {
|
||||
fmt_eff = G_IM_FMT_I;
|
||||
}
|
||||
|
||||
/* D229: green/pulsating sky water. GE's texSelect loads CI8 mipmap chains
|
||||
* with gDPLoadBlock (SetTexImage format = CI; the 16b "size" is only the
|
||||
* fast3d 4KB-per-block convention), then the sky-water draw re-declares
|
||||
* the same TMEM slot as RGBA/16b (sub_GAME_7F09343C). On N64 GE's custom
|
||||
* RSP ucode expands the indices through the TLUT into real 16-bit pixels
|
||||
* in TMEM, so the RGBA16 tile samples blue water. fast3d has no such
|
||||
* expansion: it uploads the raw index bytes as 16-bit texels (g = 2*idx
|
||||
* mod 32 dominates) -> green mottle. Route the import through the CI8
|
||||
* palette path instead -- the port-layer equivalent of the ucode's
|
||||
* expand-at-load. Only fires when the tile format genuinely disagrees
|
||||
* with the loaded source, so real RGBA16 textures are untouched. */
|
||||
if (fmt_eff == G_IM_FMT_RGBA && siz == G_IM_SIZ_16b &&
|
||||
loaded_texture.src_fmt == G_IM_FMT_CI) {
|
||||
#ifdef PORT
|
||||
static int ge_d229_a = -1;
|
||||
if (ge_d229_a < 0) ge_d229_a = getenv("GE_D229") != NULL;
|
||||
if (ge_d229_a) {
|
||||
static int n_ci8r = 0;
|
||||
if (n_ci8r++ < 8)
|
||||
sysLogPrintf(LOG_NOTE, "D229: RGBA16 tile over CI8 source -> ci8 import (addr=%p size=%u palidx=%u)",
|
||||
(const void *)orig_addr, loaded_texture.size_bytes, palette_index);
|
||||
}
|
||||
#endif
|
||||
fmt_eff = G_IM_FMT_CI;
|
||||
siz_eff = G_IM_SIZ_8b;
|
||||
}
|
||||
|
||||
if (fmt_eff == G_IM_FMT_RGBA) {
|
||||
if (siz_eff == G_IM_SIZ_16b) {
|
||||
if (siz == G_IM_SIZ_16b) {
|
||||
import_texture_rgba16(tile, loaded_texture, rdp.tex_lod);
|
||||
} else if (siz_eff == G_IM_SIZ_32b) {
|
||||
} else if (siz == G_IM_SIZ_32b) {
|
||||
import_texture_rgba32(tile, loaded_texture, rdp.tex_lod);
|
||||
} else {
|
||||
sysFatalError("Bad size for RGBA texture in tile %d: %02x", tile, siz);
|
||||
}
|
||||
} else if (fmt_eff == G_IM_FMT_IA) {
|
||||
if (siz_eff == G_IM_SIZ_4b) {
|
||||
if (siz == G_IM_SIZ_4b) {
|
||||
import_texture_ia4(tile, loaded_texture, rdp.tex_lod);
|
||||
} else if (siz_eff == G_IM_SIZ_8b) {
|
||||
} else if (siz == G_IM_SIZ_8b) {
|
||||
import_texture_ia8(tile, loaded_texture, rdp.tex_lod);
|
||||
} else if (siz_eff == G_IM_SIZ_16b) {
|
||||
} else if (siz == G_IM_SIZ_16b) {
|
||||
import_texture_ia16(tile, loaded_texture, rdp.tex_lod);
|
||||
} else {
|
||||
sysFatalError("Bad size for IA texture in tile %d: %02x", tile, siz);
|
||||
}
|
||||
} else if (fmt_eff == G_IM_FMT_CI) {
|
||||
if (siz_eff == G_IM_SIZ_4b) {
|
||||
if (siz == G_IM_SIZ_4b) {
|
||||
import_texture_ci4(tile, loaded_texture, rdp.tex_lod);
|
||||
} else if (siz_eff == G_IM_SIZ_8b) {
|
||||
} else if (siz == G_IM_SIZ_8b) {
|
||||
import_texture_ci8(tile, loaded_texture, rdp.tex_lod);
|
||||
} else {
|
||||
sysFatalError("Bad size for CI texture in tile %d: %02x", tile, siz);
|
||||
}
|
||||
} else if (fmt_eff == G_IM_FMT_I) {
|
||||
if (siz_eff == G_IM_SIZ_4b) {
|
||||
if (siz == G_IM_SIZ_4b) {
|
||||
import_texture_i4(tile, loaded_texture, rdp.tex_lod);
|
||||
} else if (siz_eff == G_IM_SIZ_8b) {
|
||||
} else if (siz == G_IM_SIZ_8b) {
|
||||
import_texture_i8(tile, loaded_texture, rdp.tex_lod);
|
||||
} else {
|
||||
sysFatalError("Bad size for I texture in tile %d: %02x", tile, siz);
|
||||
@@ -1414,55 +1344,19 @@ static void gfx_sp_vertex(size_t n_vertices, size_t dest_index, const Vtx* verti
|
||||
d->color.g = g > 255 ? 255 : g;
|
||||
d->color.b = b > 255 ? 255 : b;
|
||||
|
||||
/* D195/D72 correction: GE DOES use RSP-generated (environment-
|
||||
* mapped) texture coordinates -- shiny gold/silver weapon skins
|
||||
* and Control's reflective console glass all set G_TEXTURE_GEN,
|
||||
* the real GBI bit that gates this on real hardware. D72.1
|
||||
* originally removed this whole PD-inherited block to fix the
|
||||
* Rareware logo (a lit surface that does NOT set G_TEXTURE_GEN,
|
||||
* so it was never supposed to hit this path in the first place)
|
||||
* but over-corrected: it deleted the feature instead of gating
|
||||
* it on the bit that actually distinguishes the two cases. With
|
||||
* no envmap UV generation, every G_TEXTURE_GEN draw sampled its
|
||||
* texture at whatever raw tc[] happened to be authored (usually
|
||||
* ~0), landing on the same corner texel every frame regardless
|
||||
* of view angle -- for a typical dark-edged reflection map, a
|
||||
* solid near-black surface instead of a shiny/reflective one.
|
||||
* Gating on G_TEXTURE_GEN preserves D72.1's actual fix (the logo
|
||||
* never sets this bit, so it's unaffected) while restoring the
|
||||
* envmap effect for the draws that do. */
|
||||
if (rsp.geometry_mode & G_TEXTURE_GEN) {
|
||||
float dotx = 0, doty = 0;
|
||||
if (rsp.lookat_enabled) {
|
||||
dotx += vcn->x * rsp.current_lookat_coeffs[0][0];
|
||||
dotx += vcn->y * rsp.current_lookat_coeffs[0][1];
|
||||
dotx += vcn->z * rsp.current_lookat_coeffs[0][2];
|
||||
doty += vcn->x * rsp.current_lookat_coeffs[1][0];
|
||||
doty += vcn->y * rsp.current_lookat_coeffs[1][1];
|
||||
doty += vcn->z * rsp.current_lookat_coeffs[1][2];
|
||||
dotx /= 127.0f;
|
||||
doty /= 127.0f;
|
||||
} else {
|
||||
float tvcn[3];
|
||||
calculate_normal_dir(vcn, tvcn);
|
||||
dotx = tvcn[0];
|
||||
doty = tvcn[1];
|
||||
}
|
||||
|
||||
dotx = clampf(dotx, -1.0f, 1.0f);
|
||||
doty = clampf(doty, -1.0f, 1.0f);
|
||||
|
||||
if (rsp.geometry_mode & G_TEXTURE_GEN_LINEAR) {
|
||||
dotx = acosf(-dotx) / 4.0f;
|
||||
doty = acosf(-doty) / 4.0f;
|
||||
} else {
|
||||
dotx = (dotx + 1.0f) / 4.0f;
|
||||
doty = (doty + 1.0f) / 4.0f;
|
||||
}
|
||||
|
||||
U = (int32_t)(dotx * rsp.texture_scaling_factor.s);
|
||||
V = (int32_t)(doty * rsp.texture_scaling_factor.t);
|
||||
}
|
||||
/* D72 (docs/internals.md): unlike the PD port, GE never
|
||||
* generates texture coordinates from vertex normals. This PD-
|
||||
* inherited block overwrote the authored tc[] UVs on every
|
||||
* lit+textured surface (lookat_enabled defaults to true here),
|
||||
* which smeared the Rareware-logo quads into a diagonal gold
|
||||
* wedge: their corner UVs (0x0010..0x03F0 = full 32x32 coverage)
|
||||
* were replaced by normal-derived values. GE's ground truth:
|
||||
* every DL that could env-map sets gsDPSetTextureLUT(G_TT_NONE),
|
||||
* stage rendering always has a camera lookat yet stage textures
|
||||
* are surface-fixed on hardware, and the logo vertex batches
|
||||
* carry authored atlas-slice UVs. So U/V stay as computed from
|
||||
* tc[] above. (gSPLookAt state is still tracked — it may matter
|
||||
* for lighting fidelity later.) */
|
||||
} else {
|
||||
d->color.r = vcn->r;
|
||||
d->color.g = vcn->g;
|
||||
@@ -1538,17 +1432,8 @@ static inline int gfx_lod_tile_offset(const int i) {
|
||||
// mip (tile 1), whose single-LOADBLOCK TMEM slot fast3d never registers
|
||||
// -> a magnified crop of the base image (the "blurry blob" ceilings /
|
||||
// wall panels in BUNKER1). GE loads the whole mip chain at TMEM 0, so
|
||||
// for an LOD texture the base render tile is the only correctly-loaded
|
||||
// level: use it.
|
||||
//
|
||||
// D172: but this must NOT collapse a genuine non-LOD two-texture combine.
|
||||
// The explosion/blood/spark particle records (assets/oddtextures.c
|
||||
// globalDL_0x078..) set G_TL_TILE (no LOD) + G_CC_INTERFERENCE
|
||||
// (TEXEL0 * TEXEL1) and bind tile 0 = IA8 smoke @ TMEM 0, tile 1 = RGBA16
|
||||
// fire @ TMEM 0x188. Returning 0 here fed TEXEL1 the smoke texture too
|
||||
// (smoke * smoke) -> the magenta/cyan particle colour. Only fold to the
|
||||
// base tile when LOD is actually active.
|
||||
return rdp.tex_lod ? 0 : i;
|
||||
// the base render tile is the only correctly-loaded level: always use it.
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bool is_rect) {
|
||||
@@ -1558,26 +1443,7 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
|
||||
struct LoadedVertex* v_arr[3] = { v1, v2, v3 };
|
||||
|
||||
if ((rsp.extra_geometry_mode & G_NO_CLIPPING_EXT) == 0) {
|
||||
/* D233: the outcode bits set in gfx_sp_vertex (x<-w / x>w / y<-w /
|
||||
* y>w / z>w) are only valid half-space tests when w>0. A vertex
|
||||
* that has crossed behind the camera plane (w<0) flips the sense of
|
||||
* those comparisons, so its clip_rej bits can come out wrong. If
|
||||
* that spurious bit happens to match the other two (genuinely
|
||||
* correct) vertices' bits, the AND-reduction below fires and drops
|
||||
* the whole triangle before it ever reaches the GPU -- even though
|
||||
* OpenGL's own homogeneous clipper (which handles w<0 correctly)
|
||||
* would have rendered the visible portion of it. This hits large
|
||||
* polygons near the camera (room walls/ceilings near a doorway,
|
||||
* where the camera is close enough for a vertex to sit behind it)
|
||||
* far more than small prop models, matching D233's "room geometry
|
||||
* intermittently vanishes near doors, furniture/terminals still
|
||||
* draw" report. Fix: never trust the trivial-reject AND test when
|
||||
* any vertex has a negative w -- defer to GL's clipper instead,
|
||||
* same as the backface-cull code just below already does for the
|
||||
* same w-sign hazard. Worst case for the (rare) mixed-sign case is
|
||||
* a few extra triangles reaching the GPU; never fewer. */
|
||||
bool any_behind_camera = (v1->w < 0) || (v2->w < 0) || (v3->w < 0);
|
||||
if (!any_behind_camera && (v1->clip_rej & v2->clip_rej & v3->clip_rej)) {
|
||||
if (v1->clip_rej & v2->clip_rej & v3->clip_rej) {
|
||||
// The whole triangle lies outside the visible area
|
||||
return;
|
||||
}
|
||||
@@ -1822,131 +1688,9 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
|
||||
rendering_state.textures[i]->second.cmt = cmt;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef PORT
|
||||
/* D229 probe (env-gated, inert): green/pulsating IsWater water.
|
||||
* The water quad is a 2-cycle LERP(TEXEL1, TEXEL0) draw binding
|
||||
* BOTH tiles to the same TMEM with tile 1 offset by uls/ult --
|
||||
* the D172 probe's `tmem differs` filter never fires for it. Dump
|
||||
* each texunit's full decoded tile state + wrap decision + GL
|
||||
* texture identity so one Frigate capture pins whether TEXEL1
|
||||
* resolves to the same image/region as TEXEL0 or drifts into
|
||||
* other TMEM content (the green). */
|
||||
static int ge_d229_b = -1;
|
||||
if (ge_d229_b < 0) ge_d229_b = getenv("GE_D229") != NULL;
|
||||
if (ge_d229_b && use_2cyc && comb->used_textures[0] && comb->used_textures[1]) {
|
||||
static int d229x = 0;
|
||||
static int d229_total = 0;
|
||||
d229_total++;
|
||||
if (d229x < 12) {
|
||||
d229x++;
|
||||
const uint32_t tmem = rdp.texture_tile[tile].tmem;
|
||||
sysLogPrintf(LOG_NOTE,
|
||||
"D229: texunit%d first=%u lodoff=%u tile=%u tmem=%u fmt=%u siz=%u | "
|
||||
"uls=%u ult=%u lrs=%u lrt=%u masks=%u maskt=%u shifts=%d shiftt=%d cms=%u cmt=%u | "
|
||||
"texw=%u texh=%u texw2=%u texh2=%u | "
|
||||
"wrapS=%d(tw=%.1f,uls=%.1f) wrapT=%d(th=%.1f,ult=%.1f) | "
|
||||
"tmembytes=%u GLaddr=%p texid=%u glbytes=%u same_tmem_as_other=%d",
|
||||
i, rdp.first_tile_index, gfx_lod_tile_offset(i), tile, tmem,
|
||||
rdp.texture_tile[tile].fmt, rdp.texture_tile[tile].siz,
|
||||
rdp.texture_tile[tile].uls, rdp.texture_tile[tile].ult,
|
||||
rdp.texture_tile[tile].lrs, rdp.texture_tile[tile].lrt,
|
||||
rdp.texture_tile[tile].masks, rdp.texture_tile[tile].maskt,
|
||||
(int)rdp.texture_tile[tile].shifts, (int)rdp.texture_tile[tile].shiftt,
|
||||
(unsigned)cms, (unsigned)cmt,
|
||||
tex_width[i], tex_height[i], tex_width2[i], tex_height2[i],
|
||||
(int)wrap_s[i], wrap_tw[i], wrap_uls[i],
|
||||
(int)wrap_t[i], wrap_th[i], wrap_ult[i],
|
||||
rdp.loaded_texture[tmem].orig_size_bytes,
|
||||
rendering_state.textures[i] ? (const void*)rendering_state.textures[i]->first.texture_addr : nullptr,
|
||||
rendering_state.textures[i] ? rendering_state.textures[i]->second.texture_id : 0u,
|
||||
rendering_state.textures[i] ? rendering_state.textures[i]->first.size_bytes : 0u,
|
||||
(int)(rdp.texture_tile[rdp.first_tile_index + gfx_lod_tile_offset(1 - i)].tmem == tmem));
|
||||
} else if (d229_total % 400 == 0) {
|
||||
sysLogPrintf(LOG_NOTE, "D229: (summary) %d dual-texunit 2-cycle tris so far", d229_total);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef PORT
|
||||
/* D172 probe (env-gated, inert): the magenta/cyan blood/spark bug. The
|
||||
* particle records (assets/oddtextures.c globalDL_0x078..) draw a 2-cycle
|
||||
* G_CC_INTERFERENCE combine (TEXEL0*TEXEL1) binding two tiles of two
|
||||
* formats - tile 0 IA8 smoke, tile 1 RGBA16 fire @ tmem 0x188. Dump both
|
||||
* texunits' tile state whenever a 2-cycle tri actually consumes TEXEL1, so
|
||||
* one Silo capture pins whether TEXEL1 resolves to the right tmem/format. */
|
||||
static int ge_d172_a = -1;
|
||||
if (ge_d172_a < 0) ge_d172_a = getenv("GE_D172") != NULL;
|
||||
if (ge_d172_a && use_2cyc && comb->used_textures[1] && !rdp.tex_lod) {
|
||||
const uint32_t fi = rdp.first_tile_index;
|
||||
/* the tile fast3d will actually SAMPLE for each texunit */
|
||||
const uint32_t s0 = fi + gfx_lod_tile_offset(0);
|
||||
const uint32_t s1 = fi + gfx_lod_tile_offset(1);
|
||||
/* the tile the DL actually CONFIGURED for texunit 1 */
|
||||
const uint32_t c1 = fi + 1;
|
||||
/* only interesting when the DL set up a genuinely distinct 2nd tile
|
||||
* (different tmem) -- filters out mip/LOD-bilerp false positives */
|
||||
if (rdp.texture_tile[c1].tmem != rdp.texture_tile[fi].tmem) {
|
||||
static int d172x = 0;
|
||||
static int d172_total = 0;
|
||||
/* D219: the original 40-hit cap (sized for M-90's short scripted
|
||||
* repro) silently went dark for the rest of a real play session
|
||||
* after ~40 ordinary bullet-impact particles, well before any
|
||||
* actual explosion -- and the silence itself was indistinguishable
|
||||
* from "no more particle draws happened at all". Never suppress
|
||||
* the interesting (lit) case, and keep an always-on counter with
|
||||
* periodic summaries so a long session still proves whether this
|
||||
* code path is being hit throughout, not just early on. */
|
||||
const bool lit = (rsp.geometry_mode & G_LIGHTING) != 0;
|
||||
d172_total++;
|
||||
if (!lit && d172x >= 40) {
|
||||
if (d172_total % 200 == 0) {
|
||||
sysLogPrintf(LOG_NOTE, "D172: (summary) %d multitex tris seen so far, still LIGHTING=off", d172_total);
|
||||
}
|
||||
} else {
|
||||
d172x++;
|
||||
sysLogPrintf(LOG_NOTE,
|
||||
"D172: multitex tri combine=%llx tex_lod=%d first=%u | "
|
||||
"cfg1[tile=%u tmem=%u fmt=%u siz=%u] | "
|
||||
"SAMPLED0=tile%u(tmem=%u) SAMPLED1=tile%u(tmem=%u fmt=%u)%s | "
|
||||
/* D219 (docs/dev/findings.md): explosion.c never clears
|
||||
* G_LIGHTING before drawing these particle billboards -
|
||||
* it relies on whatever last set it. If lighting is ON
|
||||
* here, cn[]'s authored RGBA tint is misread as a normal
|
||||
* vector (gfx_pc.cpp:1301-1346) and SHADE becomes a
|
||||
* scene-light color instead of red/orange, which would
|
||||
* explain a state-dependent purple/blue that a synthetic
|
||||
* scripted repro (M-90) might not reproduce. */
|
||||
"geometry_mode=%08x LIGHTING=%s | "
|
||||
/* D219 round 2: G_LIGHTING is ruled out (5000/5000
|
||||
* samples off, live-confirmed still purple/blue). Next
|
||||
* suspect: a D217-style texture-cache collision -- the
|
||||
* cache key is {texture_addr, fmt, siz, size_bytes,
|
||||
* palette_hash} (gfx_pc.h) with no content hash for
|
||||
* non-CI textures, so if two of the 15 FIRE_N images
|
||||
* ever resolve to the same address (Globalimagetable
|
||||
* fixup aliasing) or the cache evicts/reuses a texture_id
|
||||
* without a real re-upload, this tile's GL texture could
|
||||
* be serving stale/wrong-image content while every
|
||||
* bookkeeping field above still looks correct. */
|
||||
"TEXEL1_addr=%p TEXEL1_texid=%u TEXEL1_bytes=%u",
|
||||
(unsigned long long)rdp.combine_mode, (int)rdp.tex_lod, fi,
|
||||
c1, rdp.texture_tile[c1].tmem, rdp.texture_tile[c1].fmt, rdp.texture_tile[c1].siz,
|
||||
s0, rdp.texture_tile[s0].tmem,
|
||||
s1, rdp.texture_tile[s1].tmem, rdp.texture_tile[s1].fmt,
|
||||
(s1 == s0) ? " <-- TEXEL1 == TEXEL0 (BUG)" : "",
|
||||
rsp.geometry_mode,
|
||||
(rsp.geometry_mode & G_LIGHTING) ? "ON <-- D219 SUSPECT" : "off",
|
||||
rendering_state.textures[1] ? (const void*)rendering_state.textures[1]->first.texture_addr : nullptr,
|
||||
rendering_state.textures[1] ? rendering_state.textures[1]->second.texture_id : 0u,
|
||||
rendering_state.textures[1] ? rendering_state.textures[1]->first.size_bytes : 0u);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
struct ShaderProgram* prg = comb->prg[tm];
|
||||
if (prg == NULL) {
|
||||
comb->prg[tm] = prg =
|
||||
@@ -2358,7 +2102,6 @@ static void gfx_dp_set_scissor(uint32_t mode, uint32_t ulx, uint32_t uly, uint32
|
||||
|
||||
static void gfx_dp_set_texture_image(uint32_t format, uint32_t size, uint32_t width, uint32_t tex_flags, const void* addr) {
|
||||
rdp.texture_to_load.addr = (const uint8_t*)addr;
|
||||
rdp.texture_to_load.fmt = (uint8_t)format; /* D229 */
|
||||
rdp.texture_to_load.siz = size;
|
||||
rdp.texture_to_load.width = width;
|
||||
rdp.texture_to_load.tex_flags = tex_flags;
|
||||
@@ -2447,23 +2190,6 @@ static void gfx_dp_load_tlut(uint8_t tile, uint32_t uls, uint32_t ult, uint32_t
|
||||
*dst++ = PD_BE16(*src++);
|
||||
}
|
||||
|
||||
/* D217: refresh the palette-content hash that keys the CI texture cache.
|
||||
* GE reissues gDPLoadTLUT from a repeated scratch source address with
|
||||
* different content between weapon / character model materials; the CI
|
||||
* TextureCacheKey keys on the source *address*, so without a content hash a
|
||||
* later material can take a stale cache HIT decoded against an earlier
|
||||
* palette. gDPLoadTLUT is rare relative to draws, so hashing the whole
|
||||
* 512-byte table here keeps the per-texel path untouched. */
|
||||
{
|
||||
uint32_t h = 2166136261u;
|
||||
const uint8_t *pb = (const uint8_t *)rdp.palette;
|
||||
for (uint32_t k = 0; k < sizeof(rdp.palette); ++k) {
|
||||
h ^= pb[k];
|
||||
h *= 16777619u;
|
||||
}
|
||||
rdp.palette_hash = h;
|
||||
}
|
||||
|
||||
rdp.textures_changed[0] = rdp.textures_changed[1] = true;
|
||||
}
|
||||
|
||||
@@ -2490,7 +2216,6 @@ static void gfx_dp_load_block(uint8_t tile, uint32_t uls, uint32_t ult, uint32_t
|
||||
loaded_texture.tex_flags = rdp.texture_to_load.tex_flags;
|
||||
loaded_texture.raw_tex_metadata = rdp.texture_to_load.raw_tex_metadata;
|
||||
loaded_texture.addr = rdp.texture_to_load.addr;
|
||||
loaded_texture.src_fmt = rdp.texture_to_load.fmt; /* D229 */
|
||||
|
||||
rdp.textures_changed[0] = rdp.textures_changed[1] = true;
|
||||
}
|
||||
@@ -2531,7 +2256,6 @@ static void gfx_dp_load_tile(uint8_t tile, uint32_t uls, uint32_t ult, uint32_t
|
||||
loaded_texture.tex_flags = rdp.texture_to_load.tex_flags;
|
||||
loaded_texture.raw_tex_metadata = rdp.texture_to_load.raw_tex_metadata;
|
||||
loaded_texture.addr = rdp.texture_to_load.addr + start_offset_bytes;
|
||||
loaded_texture.src_fmt = rdp.texture_to_load.fmt; /* D229 */
|
||||
|
||||
rdp.texture_tile[tile].uls = uls;
|
||||
rdp.texture_tile[tile].ult = ult;
|
||||
@@ -3060,30 +2784,6 @@ static void gfx_run_dl(Gfx* cmd) {
|
||||
gfx_dp_set_grayscale_color(C1(24, 8), C1(16, 8), C1(8, 8), C1(0, 8));
|
||||
break;
|
||||
case G_SETCOMBINE:
|
||||
#ifdef PORT
|
||||
/* D172 probe (env-gated, inert): log every SETCOMBINE with the
|
||||
* cycle-type active at that moment. Particle records
|
||||
* (explosion.c g_ExplosionDisplayLists[]) set a 2-cycle
|
||||
* combine but never set cycletype - this tells us what
|
||||
* cycletype fast3d has when they replay. */
|
||||
static int ge_d172_b = -1;
|
||||
if (ge_d172_b < 0) ge_d172_b = getenv("GE_D172") != NULL;
|
||||
if (ge_d172_b) {
|
||||
static uint64_t d172seen[64];
|
||||
static int d172cnt = 0;
|
||||
uint64_t key = ((uint64_t)(uint32_t)cmd->words.w0 << 32) | (uint32_t)cmd->words.w1;
|
||||
bool dup = false;
|
||||
for (int k = 0; k < d172cnt; k++) if (d172seen[k] == key) { dup = true; break; }
|
||||
if (!dup && d172cnt < 64) {
|
||||
d172seen[d172cnt++] = key;
|
||||
uint32_t ct = (rdp.other_mode_h >> G_MDSFT_CYCLETYPE) & 3;
|
||||
sysLogPrintf(LOG_NOTE,
|
||||
"D172: SETCOMBINE w0=%08x w1=%08x cycletype=%u (%s)",
|
||||
(uint32_t)cmd->words.w0, (uint32_t)cmd->words.w1,
|
||||
ct, ct == 0 ? "1CYC" : ct == 1 ? "2CYC" : ct == 2 ? "COPY" : "FILL");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
gfx_dp_set_combine_mode(color_comb(C0(20, 4), C1(28, 4), C0(15, 5), C1(15, 3)),
|
||||
alpha_comb(C0(12, 3), C1(12, 3), C0(9, 3), C1(9, 3)),
|
||||
color_comb(C0(5, 4), C1(24, 4), C0(0, 5), C1(6, 3)),
|
||||
@@ -3462,20 +3162,6 @@ extern "C" void gfx_set_mipmap_filter(enum MipmapFilteringMode mode) {
|
||||
}
|
||||
|
||||
extern "C" void gfx_set_fix_mip_textures(int on) { g_fix_mip_textures = !!on; }
|
||||
|
||||
/* D212: expose the (already-implemented) rendering-API anisotropy hook to the
|
||||
* port layer. Clamp to [1, GL max] so a stale ini value can't feed an invalid
|
||||
* GL_TEXTURE_MAX_ANISOTROPY. 1 = isotropic (driver default). */
|
||||
extern "C" void gfx_set_anisotropy_level(int level) {
|
||||
reset_texture_state();
|
||||
int max = gfx_rapi->get_max_anisotropy_level ? gfx_rapi->get_max_anisotropy_level() : 1;
|
||||
if (max < 1) max = 1;
|
||||
if (level < 1) level = 1;
|
||||
if (level > max) level = max;
|
||||
if (gfx_rapi->set_anisotropy_level) {
|
||||
gfx_rapi->set_anisotropy_level(level);
|
||||
}
|
||||
}
|
||||
extern "C" void gfx_set_wrap_fix(int on) {
|
||||
const char* e = getenv("GE_WRAPFIX"); /* RC3 test override */
|
||||
g_wrap_fix = e ? (atoi(e) != 0) : !!on;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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@
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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,126 +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 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++) {
|
||||
/* 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;
|
||||
@@ -1318,11 +1085,6 @@ static s32 geEepromRW(u8 block, u8 *buf, int nbytes, int write)
|
||||
geEepromStore();
|
||||
} else {
|
||||
memcpy(buf, s_eeprom + off, nbytes);
|
||||
/* fileValidateSaves' block read: five save slots from block 4. */
|
||||
if (portAllUnlocked && block == 4 &&
|
||||
nbytes == (int)(sizeof(ge_save_slot) * 5)) {
|
||||
geEepromPatchAllCheats(buf);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1356,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
|
||||
@@ -1710,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);
|
||||
}
|
||||
@@ -54,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)
|
||||
@@ -66,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"
|
||||
@@ -97,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 "
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
@@ -66,6 +60,7 @@ 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 };
|
||||
|
||||
/* Windowed-mode resolution presets. Filtered at init to those that fit the
|
||||
@@ -96,11 +91,6 @@ 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[] = {
|
||||
@@ -110,76 +100,17 @@ static struct Row rows[] = {
|
||||
{ "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.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). */
|
||||
{ "Input.AimModeSens", "Mouse aim speed", ROW_SLIDER, 1, NULL, 0, 1, 80, 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.SensLink", "Link aim/turn sens",ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
||||
{ "Input.MouseInvertY", "Mouse invert Y", ROW_TOGGLE, 1, kOnOff, 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 },
|
||||
/* 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: read once at title-stage load (lv.c) -- toggling it takes effect
|
||||
* the next time the title screen is reached. */
|
||||
{ "Game.SkipIntro", "Skip intro", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
|
||||
/* 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 },
|
||||
};
|
||||
#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().
|
||||
@@ -190,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
|
||||
@@ -218,55 +136,18 @@ static void sliderBarSpan(s32 *x0, s32 *x1)
|
||||
}
|
||||
}
|
||||
|
||||
/* Visible position (0..s_visN-1) the given overlay-space y falls in, or -1. */
|
||||
/* Row index the given overlay-space y falls in, or -1. */
|
||||
static int overlayRowAtY(double oy)
|
||||
{
|
||||
for (int p = 0; p < s_visN; p++) {
|
||||
double top = OV_ROW_Y(p - s_scroll) - 3;
|
||||
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 when it nears an edge. */
|
||||
static void overlayUpdateScroll(void)
|
||||
{
|
||||
int maxV = maxVisibleRows();
|
||||
if (s_visN <= maxV) {
|
||||
s_scroll = 0;
|
||||
return;
|
||||
}
|
||||
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)
|
||||
@@ -322,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. */
|
||||
{
|
||||
@@ -391,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);
|
||||
@@ -424,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)
|
||||
@@ -464,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;
|
||||
}
|
||||
@@ -517,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
|
||||
@@ -565,48 +390,23 @@ void optionsOverlayHandleInput(void)
|
||||
* 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 (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);
|
||||
/* ---- 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);
|
||||
|
||||
/* ---- mouse ---- */
|
||||
int ww = 0, wh = 0;
|
||||
@@ -614,13 +414,12 @@ void optionsOverlayHandleInput(void)
|
||||
if (ww > 0 && wh > 0) {
|
||||
double ox = (double)mx * (double)viGetX() / ww;
|
||||
double oy = (double)my * (double)viGetY() / wh;
|
||||
int hoverVis = overlayRowAtY(oy);
|
||||
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
|
||||
@@ -632,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 */
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -654,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -750,41 +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). */
|
||||
const int maxV = maxVisibleRows();
|
||||
const int pLast = (s_visN - s_scroll < maxV) ? s_visN - s_scroll
|
||||
: 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;
|
||||
@@ -801,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);
|
||||
@@ -823,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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -44,7 +44,6 @@ extern int snprintf(char *str, size_t maxsize, const char *format, ...);
|
||||
#include "romdata.h"
|
||||
#include "pcmodels.h"
|
||||
#include "pccg.h"
|
||||
#include "romconvert.h"
|
||||
|
||||
/* D50: struct font/fontchar layouts for the font-segment re-layout. Order
|
||||
* matters (see pcmodels.c): ultra64.h must finish before bondtypes.h. */
|
||||
@@ -114,116 +113,6 @@ static int romHeaderValid(const u8 *h, char *err, size_t errsz)
|
||||
|
||||
static u32 romdataBswap32(u32 v); /* defined below; used by the D55 fixup */
|
||||
|
||||
/* D230: RAW16 wavetable samples are stored big-endian s16 in the ROM (the
|
||||
* N64 CPU reads them that way), but the PC mixer's resampler consumes
|
||||
* host-native (little-endian) s16 — so every RAW16 instrument plays as a
|
||||
* byte-swapped buzz. ADPCM nibble data is byte-order agnostic, which is why
|
||||
* only RAW16 ranges are affected. The wavetable sample segments stay in the
|
||||
* cart map (alBnkfNew() points straight at them; only the bank structs are
|
||||
* re-laid out, D37), so bswap16 those ranges in place once the map is live.
|
||||
* The walk mirrors the ALBankFile layout romdataFixupAudioBank() uses. */
|
||||
|
||||
#define RAW16_MAX_RANGES 32
|
||||
|
||||
struct raw16Range { u32 start, end; };
|
||||
|
||||
static void romdataRaw16Swap(u8 *tbl, u32 tblSize, u32 base, s32 len,
|
||||
struct raw16Range *done, int *ndone)
|
||||
{
|
||||
int i;
|
||||
if (len <= 0 || base >= tblSize || (u32)len > tblSize - base) {
|
||||
sysLogPrintf(LOG_ERROR, "romdataInit: D230 RAW16 wavetable out of "
|
||||
"range (base 0x%X len %d, segment 0x%X bytes)",
|
||||
base, len, tblSize);
|
||||
return;
|
||||
}
|
||||
for (i = 0; i < *ndone; i++) {
|
||||
/* Shared sample data (two instruments can point at the same range):
|
||||
* swapping twice would undo the first swap. */
|
||||
if (base >= done[i].start && base + (u32)len <= done[i].end)
|
||||
return;
|
||||
}
|
||||
for (i = 0; i + 1 < len; i += 2) {
|
||||
u8 t = tbl[base + i];
|
||||
tbl[base + i] = tbl[base + i + 1];
|
||||
tbl[base + i + 1] = t;
|
||||
}
|
||||
if (*ndone < RAW16_MAX_RANGES) {
|
||||
done[*ndone].start = base;
|
||||
done[*ndone].end = base + (u32)len;
|
||||
(*ndone)++;
|
||||
}
|
||||
}
|
||||
|
||||
static void romdataRaw16Inst(const u8 *ctl, u32 ctlSize, u8 *tbl, u32 tblSize,
|
||||
struct raw16Range *done, int *ndone, u32 ioff)
|
||||
{
|
||||
s16 scount;
|
||||
u32 k;
|
||||
if (!ioff || ioff + 16 > ctlSize)
|
||||
return;
|
||||
scount = (s16)((ctl[ioff + 14] << 8) | ctl[ioff + 15]); /* soundCount */
|
||||
if (scount < 0)
|
||||
return;
|
||||
for (k = 0; k < (u32)scount; k++) {
|
||||
u32 soff, wt;
|
||||
if (ioff + 16 + 4 * k + 4 > ctlSize)
|
||||
break;
|
||||
soff = ((u32)ctl[ioff + 16 + 4 * k] << 24) |
|
||||
((u32)ctl[ioff + 17 + 4 * k] << 16) |
|
||||
((u32)ctl[ioff + 18 + 4 * k] << 8) |
|
||||
(u32)ctl[ioff + 19 + 4 * k];
|
||||
if (!soff || soff + 15 > ctlSize)
|
||||
continue;
|
||||
wt = ((u32)ctl[soff + 8] << 24) | ((u32)ctl[soff + 9] << 16) |
|
||||
((u32)ctl[soff + 10] << 8) | (u32)ctl[soff + 11];
|
||||
if (!wt || wt + 20 > ctlSize)
|
||||
continue;
|
||||
if (ctl[wt + 8] == 1 /* AL_RAW16_WAVE */) {
|
||||
u32 base = ((u32)ctl[wt] << 24) | ((u32)ctl[wt + 1] << 16) |
|
||||
((u32)ctl[wt + 2] << 8) | (u32)ctl[wt + 3];
|
||||
s32 len = (s32)(((u32)ctl[wt + 4] << 24) |
|
||||
((u32)ctl[wt + 5] << 16) |
|
||||
((u32)ctl[wt + 6] << 8) | (u32)ctl[wt + 7]);
|
||||
romdataRaw16Swap(tbl, tblSize, base, len, done, ndone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void romdataRaw16Walk(const u8 *ctl, u32 ctlSize, u8 *tbl, u32 tblSize,
|
||||
struct raw16Range *done, int *ndone)
|
||||
{
|
||||
s16 i;
|
||||
if (ctlSize < 4 || ((ctl[0] << 8) | ctl[1]) != 0x4231 /* 'B1' */)
|
||||
return;
|
||||
for (i = 0; i < (s16)((ctl[2] << 8) | ctl[3]); i++) {
|
||||
u32 boff, j;
|
||||
s16 icount;
|
||||
if (4 + 4 * (u32)i + 4 > ctlSize)
|
||||
break;
|
||||
boff = ((u32)ctl[4 + 4 * i] << 24) | ((u32)ctl[5 + 4 * i] << 16) |
|
||||
((u32)ctl[6 + 4 * i] << 8) | (u32)ctl[7 + 4 * i];
|
||||
if (!boff || boff + 12 > ctlSize)
|
||||
continue;
|
||||
icount = (s16)((ctl[boff] << 8) | ctl[boff + 1]); /* instCount */
|
||||
if (icount < 0)
|
||||
continue;
|
||||
romdataRaw16Inst(ctl, ctlSize, tbl, tblSize, done, ndone,
|
||||
((u32)ctl[boff + 8] << 24) |
|
||||
((u32)ctl[boff + 9] << 16) |
|
||||
((u32)ctl[boff + 10] << 8) | (u32)ctl[boff + 11]);
|
||||
for (j = 0; j < icount; j++) {
|
||||
if (boff + 12 + 4 * (u32)j + 4 > ctlSize)
|
||||
break;
|
||||
romdataRaw16Inst(ctl, ctlSize, tbl, tblSize, done, ndone,
|
||||
((u32)ctl[boff + 12 + 4 * j] << 24) |
|
||||
((u32)ctl[boff + 13 + 4 * j] << 16) |
|
||||
((u32)ctl[boff + 14 + 4 * j] << 8) |
|
||||
(u32)ctl[boff + 15 + 4 * j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* The caller has placed a writable region of (romSize + sidecars) bytes at
|
||||
* CART_BASE. Populate it, load the sidecars, apply the D55 RLE-header fixup,
|
||||
* and mark the ROM live. Shared by the Windows (VirtualAlloc) and POSIX (mmap)
|
||||
@@ -250,38 +139,6 @@ static int romdataFinishCartMap(const char *tok, u8 *img,
|
||||
if ((u16)*hdr > 512 || (u16)(*hdr >> 16) > 512)
|
||||
*hdr = romdataBswap32(*hdr);
|
||||
}
|
||||
|
||||
/* D230: byte-swap the RAW16 wavetable sample ranges in both bank trees
|
||||
* (BE s16 in the ROM -> host LE for the mixer's resampler). */
|
||||
{
|
||||
extern u32 _sfxctlSegmentRomStart;
|
||||
extern u32 _sfxtblSegmentRomStart;
|
||||
extern u32 _instrumentsctlSegmentRomStart;
|
||||
extern u32 _instrumentstblSegmentRomStart;
|
||||
struct raw16Range done[RAW16_MAX_RANGES];
|
||||
int ndone = 0;
|
||||
|
||||
/* Each tbl segment runs to the end of the ROM; bound the swap range
|
||||
* by that (the real segment is smaller, but base/len are validated
|
||||
* against this anyway). */
|
||||
romdataRaw16Walk((const u8 *)&_sfxctlSegmentRomStart,
|
||||
(u32)&_sfxtblSegmentRomStart -
|
||||
(u32)&_sfxctlSegmentRomStart,
|
||||
(u8 *)&_sfxtblSegmentRomStart,
|
||||
CART_BASE + romSize -
|
||||
(u32)&_sfxtblSegmentRomStart,
|
||||
done, &ndone);
|
||||
romdataRaw16Walk((const u8 *)&_instrumentsctlSegmentRomStart,
|
||||
(u32)&_instrumentstblSegmentRomStart -
|
||||
(u32)&_instrumentsctlSegmentRomStart,
|
||||
(u8 *)&_instrumentstblSegmentRomStart,
|
||||
CART_BASE + romSize -
|
||||
(u32)&_instrumentstblSegmentRomStart,
|
||||
done, &ndone);
|
||||
if (ndone)
|
||||
sysLogPrintf(LOG_INFO, "romdataInit: D230 byte-swapped %d RAW16 "
|
||||
"wavetable range(s) in place", ndone);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -316,11 +173,11 @@ int romdataInit(void)
|
||||
static const char *locs[3] = { "$S/", "$E/", "./" };
|
||||
int li;
|
||||
FSFile *f = NULL;
|
||||
char romRelPath[1024] = ""; /* locs[li] + tok of the ROM we opened */
|
||||
|
||||
for (li = 0; li < 3 && !f; li++) {
|
||||
snprintf(romRelPath, sizeof(romRelPath), "%s%s", locs[li], tok);
|
||||
f = fsOpen(sysResolvePath(romRelPath), "rb");
|
||||
char pathbuf[1024];
|
||||
snprintf(pathbuf, sizeof(pathbuf), "%s%s", locs[li], tok);
|
||||
f = fsOpen(sysResolvePath(pathbuf), "rb");
|
||||
}
|
||||
if (!f)
|
||||
continue;
|
||||
@@ -341,15 +198,6 @@ int romdataInit(void)
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Drop-in ROM support: if the ROM-derived sidecars are missing, run
|
||||
* the bundled frozen converter (ge007-convert) once to generate them.
|
||||
* Without them the game crashes later in
|
||||
* modelPromoteNodeOffsetsToPointers (D179), so abort boot cleanly. */
|
||||
if (!romConvertEnsureSidecars(img, romRelPath)) {
|
||||
free(img);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* D50: reserve room for the PC model sidecar image right after the
|
||||
* ROM ([CART_BASE+romSize, ...)); it is loaded below and patched into
|
||||
* file_resource_table after obInit. */
|
||||
@@ -570,11 +418,8 @@ void romdataFixupMusicSeqTable(u8 *blob, u32 blobSize)
|
||||
u32 count = *(const u16 *)(blob + 0);
|
||||
u32 maxEntries = (blobSize - 4) / 8;
|
||||
if (count > maxEntries) {
|
||||
/* Expected, not an error: music.c first decodes a 0x10-byte header
|
||||
* probe (capacity 1) before re-copying the full table and calling
|
||||
* again. Log at NOTE so it isn't mistaken for broken music data. */
|
||||
sysLogPrintf(LOG_NOTE,
|
||||
"romdataFixupMusicSeqTable: partial decode, seqCount %u > capacity %u (full table follows)",
|
||||
sysLogPrintf(LOG_ERROR,
|
||||
"romdataFixupMusicSeqTable: seqCount %u exceeds blob capacity %u",
|
||||
count, maxEntries);
|
||||
count = maxEntries;
|
||||
}
|
||||
@@ -936,20 +781,6 @@ static void afFixupEnvelope(struct afCtx *c, u32 o)
|
||||
afWr32(c, n + 4 * i, afRd32(c, o + 4 * i));
|
||||
c->dst[n + 12] = c->src[o + 12]; /* u8 volumes: identity */
|
||||
c->dst[n + 13] = c->src[o + 13];
|
||||
/* D202/M-65 diag (temporary): decayTime == -1 is what sets
|
||||
* SOUND_FLAG_LOOPED, and a looped sound never releases its voice on its
|
||||
* own. Print the SOURCE ROM bytes so genuine data can be told from a
|
||||
* conversion artifact. Remove once root-caused. */
|
||||
if (getenv("GE_KEYMAPDUMP")) {
|
||||
sysLogPrintf(LOG_NOTE,
|
||||
"ENVSRC src@0x%06X -> img@0x%06X : raw=%02X%02X%02X%02X %02X%02X%02X%02X %02X%02X%02X%02X "
|
||||
"attack=%d decay=%d release=%d",
|
||||
o, n,
|
||||
c->src[o], c->src[o+1], c->src[o+2], c->src[o+3],
|
||||
c->src[o+4], c->src[o+5], c->src[o+6], c->src[o+7],
|
||||
c->src[o+8], c->src[o+9], c->src[o+10], c->src[o+11],
|
||||
(int)afRd32(c, o), (int)afRd32(c, o + 4), (int)afRd32(c, o + 8));
|
||||
}
|
||||
}
|
||||
|
||||
static void afFixupWaveTable(struct afCtx *c, u32 o)
|
||||
@@ -1017,16 +848,6 @@ static void afFixupSound(struct afCtx *c, u32 o)
|
||||
if (c->write) {
|
||||
c->newOff[km] = kn;
|
||||
memcpy(c->dst + kn, c->src + km, 6); /* ALKeyMap: all u8 */
|
||||
/* D202/M-65 diag (temporary): print the SOURCE ROM bytes of
|
||||
* every ALKeyMap, so "the game sees zeros" can be separated
|
||||
* from "the ROM holds zeros". Remove once root-caused. */
|
||||
if (getenv("GE_KEYMAPDUMP")) {
|
||||
sysLogPrintf(LOG_NOTE,
|
||||
"KEYMAP src@0x%06X -> img@0x%06X : velMin=%u velMax=%u "
|
||||
"keyMin=%u keyMax=0x%02X keyBase=%u detune=%d",
|
||||
km, kn, c->src[km], c->src[km + 1], c->src[km + 2],
|
||||
c->src[km + 3], c->src[km + 4], (int)(s8)c->src[km + 5]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1062,20 +883,6 @@ static void afFixupInst(struct afCtx *c, u32 o)
|
||||
|
||||
for (u32 i = 0; i < (u32)scount; i++) { /* children first */
|
||||
u32 soff = afRd32(c, o + 16 + 4 * i);
|
||||
/* D202/M-65 diag (temporary): dump the SOURCE bank tree per
|
||||
* soundIndex -- instArray[0]->soundArray[i] is exactly how snd.c
|
||||
* resolves a soundIndex. decayTime == -1 is what makes a sound
|
||||
* SOUND_FLAG_LOOPED (and therefore un-releasable without an owner),
|
||||
* so this says whether that flag is genuine ROM data for a given
|
||||
* index or a conversion artifact. Remove once root-caused. */
|
||||
if (getenv("GE_BANKDUMP") && soff && soff + 12 <= c->srcSize) {
|
||||
u32 env = afRd32(c, soff);
|
||||
u32 km = afRd32(c, soff + 4);
|
||||
sysLogPrintf(LOG_NOTE,
|
||||
"BANKSRC inst@0x%06X soundIndex=%u sound@0x%06X env@0x%06X km@0x%06X decay=%d",
|
||||
o, i, soff, env, km,
|
||||
(env && env + 8 <= c->srcSize) ? (int)afRd32(c, env + 4) : 0);
|
||||
}
|
||||
if (soff)
|
||||
afFixupSound(c, soff);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#define _POSIX_C_SOURCE 199309L
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <sys/stat.h>
|
||||
#endif
|
||||
|
||||
#include "system.h"
|
||||
@@ -246,42 +245,10 @@ const char *sysResolvePath(const char *path)
|
||||
else
|
||||
snprintf(out, sizeof(out), "%s\\data\\%s", exedir, path + 3);
|
||||
#else
|
||||
/* D256: mirror the Windows branch. The ROM itself is found via
|
||||
* $S/ OR $E/ (exe dir) by romdataInit, so a launch from any CWD
|
||||
* (Steam shortcut, terminal in ~) runs fine while saves/config —
|
||||
* which only knew the CWD-relative path — silently failed to write.
|
||||
* Prefer CWD data/ (dev/repo workflow), else exe-dir data/, and
|
||||
* create the directory best-effort so first-run writes don't fail. */
|
||||
if (access("data", F_OK) == 0)
|
||||
snprintf(out, sizeof(out), "data/%s", path + 3);
|
||||
else {
|
||||
static char sexedir[1024] = "";
|
||||
if (!sexedir[0]) {
|
||||
ssize_t n = readlink("/proc/self/exe", sexedir, sizeof(sexedir) - 1);
|
||||
if (n > 0) {
|
||||
char *slash;
|
||||
sexedir[n] = 0;
|
||||
slash = strrchr(sexedir, '/');
|
||||
if (slash)
|
||||
*slash = 0;
|
||||
else
|
||||
sexedir[0] = 0;
|
||||
}
|
||||
}
|
||||
if (sexedir[0])
|
||||
snprintf(out, sizeof(out), "%s/data/%s", sexedir, path + 3);
|
||||
else
|
||||
snprintf(out, sizeof(out), "data/%s", path + 3);
|
||||
}
|
||||
{
|
||||
char dirbuf[1024];
|
||||
char *slash;
|
||||
strncpy(dirbuf, out, sizeof(dirbuf) - 1);
|
||||
dirbuf[sizeof(dirbuf) - 1] = 0;
|
||||
slash = strrchr(dirbuf, '/');
|
||||
if (slash && slash != dirbuf)
|
||||
*slash = 0, mkdir(dirbuf, 0755); /* best effort; EEXIST is fine */
|
||||
}
|
||||
else
|
||||
snprintf(out, sizeof(out), "./data/%s", path + 3);
|
||||
#endif
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -54,16 +54,10 @@ static int initDone = 0;
|
||||
*/
|
||||
static int cfgVSync = 1; /* swap interval: 0 = off, 1 = on */
|
||||
static int cfgFpsCap = 0; /* frame cap in fps; 0 = uncapped (vsync) */
|
||||
static int cfgMSAA = 4; /* 1/2/4/8 samples; default 4 (modern ports ship AA on; snaps down to the highest supported level) */
|
||||
static int cfgMSAA = 1; /* 1/2/4/8 samples; 1 = off */
|
||||
static int cfgTexFilter = 1; /* 0 = nearest, 1 = bilinear (default), 2 = N64 3-point + trilinear */
|
||||
static int cfgFixMipTex = 1; /* RC2: clip mip-contaminated texture uploads to base height */
|
||||
static int cfgWrapFix = 0; /* D74 sub-tile UV pre-wrap + RC3/D167 non-PoT mask-period wrap (opt-in; GE_WRAPFIX env overrides) */
|
||||
static int cfgFovScale = 100; /* D211: percent of the original vertical FOV; 100 = unchanged (byte-identical) */
|
||||
static int cfgDrawDistance = 150; /* D218: percent of the level's authored far-clip/fog distance. Default raised 100->150 for v0.2.0: at the authored N64 distance, props visibly fade in just before they become visible on modern displays (Dam alarms / wall switches); 150 is the value the Steam Deck preset playtest-validated. 100 = unchanged N64. */
|
||||
static int cfgDrawDistanceAutoFov = 1; /* D218: couple draw distance to Video.FovScale unless DrawDistance is set explicitly */
|
||||
static int cfgLodDistance = 150; /* D249: percent scale on the geometry/model LOD-swap distance. Default raised 100->150 for v0.2.0 (same pop-in family as DrawDistance: LOD-swapped props like Dam's alarms/wall switches faded in at range); 150 matches the Steam Deck preset. 100 = unchanged N64. */
|
||||
static int cfgLodDistanceAutoFov = 0; /* off by default -- unlike DrawDistance, this is meant as a standalone perf lever, not something that should silently get more expensive as FovScale widens */
|
||||
static int cfgAniso = 4; /* D212: anisotropic filtering samples; 4 = the value fast3d already applied (no visual delta at default) */
|
||||
static int cfgFullscreen = 0; /* 0 = windowed, 1 = borderless fullscreen */
|
||||
|
||||
/*
|
||||
@@ -86,146 +80,15 @@ static int cfgWinMax = 0;
|
||||
*/
|
||||
f32 portScreenShakeScale = 1.0f;
|
||||
|
||||
/* D216: Game.SkipIntro — read once in src/game/lv.c at title-stage load.
|
||||
* 0 (default) = the legal screen + logo attract sequence plays as normal. */
|
||||
s32 portSkipIntro = 0;
|
||||
|
||||
/* D232: Game.NoHitFlash — the community "no damage flash" toggle (route-(b)
|
||||
* hook in src/game/bondview2.c currentPlayerSetFadeColour). 0 (default) =
|
||||
* original damage flash. */
|
||||
s32 portNoHitFlash = 0;
|
||||
|
||||
/* D257: Game.AllUnlocked — everything-unlocked goodie, OFF by default
|
||||
* (faithful N64 progression: levels unlock as you complete them). Consumed
|
||||
* once at startup by main.c, which sets the game's own RAM unlock flags
|
||||
* (debug_enable_all_levels_flag / debug_007_unlock_flag in
|
||||
* src/game/debugmenu_handler.c, live because the PC build defines
|
||||
* LEFTOVERDEBUG) — port-layer memory writes only, no game-code edits.
|
||||
* 1 = every solo level selectable at every difficulty plus 007 mode from
|
||||
* the first launch. F10 'All unlocked' row toggles it; takes effect next
|
||||
* run. Known quirk when ON with a fresh save: audio volumes load as 0
|
||||
* (silence) because the patched save block is CRC-valid and skips the
|
||||
* game's BLANKSAVEDATA reset that normally seeds max volume — see D259. */
|
||||
s32 portAllUnlocked = 0;
|
||||
|
||||
/* D211: Video.FovScale as a multiplier on the render FOV. Applied game-side
|
||||
* at the guPerspectiveF chokepoint (src/fr.c) so it lands BEFORE the CPU
|
||||
* pre-multiplies projection x view into the combined world matrix — the
|
||||
* fast3d-side matrix hack only caught the handful of pure-perspective loads
|
||||
* (pause/watch model, sky) and left the world untouched. 1.0f = original. */
|
||||
f32 portFovScale = 1.0f;
|
||||
|
||||
/* D222: the same widen-FOV math as the fr.c guPerspectiveF chokepoint,
|
||||
* factored out so cull-plane / LOD-scale call sites (currentPlayerSetCameraScale,
|
||||
* via currentPlayerSetPerspective) can feed the SAME effective FOV that is
|
||||
* actually rendered, instead of leaving them on the nominal value. Before this,
|
||||
* high FovScale widened what was drawn but left frustum-cull planes and the
|
||||
* fog/LOD distance scale (c_scalelod/c_lodscalez) calibrated for the narrower
|
||||
* nominal FOV, so on-screen geometry near the edges (and, via c_lodscalez,
|
||||
* the fog-based distance-visibility fade) got culled/faded as if the view
|
||||
* were still narrow. `isTitleScreen` mirrors fr.c's own
|
||||
* `lvlGetCurrentStageToLoad() != LEVELID_TITLE` guard -- the front end's
|
||||
* fixed-FOV 3D must not be touched. 1.0f/identity at FovScale=100 (default),
|
||||
* bit-for-bit no-op. */
|
||||
f32 portScaleFovY(f32 fovy, s32 isTitleScreen)
|
||||
{
|
||||
if (!isTitleScreen && portFovScale > 0.4f && portFovScale < 2.01f && portFovScale != 1.0f) {
|
||||
fovy *= portFovScale;
|
||||
if (fovy > 160.0f) { fovy = 160.0f; }
|
||||
}
|
||||
return fovy;
|
||||
}
|
||||
|
||||
/* D218: Video.DrawDistance -- multiplier applied to a level's authored
|
||||
* Visibility.FarFog (src/game/bgfog.c fogLoadCurrentEnvironment), which is
|
||||
* both the far clip plane and the fog-saturation distance (levels are tuned
|
||||
* for the stock ~60deg FOV), plus the character/prop fog-visibility-fade
|
||||
* cutoff (src/game/propobj.c chrobjFogVisRangeRelated/sub_GAME_7F054C58).
|
||||
* Video.DrawDistanceAutoFov (default on) couples the multiplier to
|
||||
* Video.FovScale so a wider FOV doesn't clip its own newly visible far
|
||||
* geometry -- or fade NPCs out early -- against distances tuned for the
|
||||
* narrower original view (D218's "blue artifacting"; the M-121 live
|
||||
* playtest found a straight 1:1 FovScale coupling still faded guards in
|
||||
* noticeably close on Dam, so the auto coupling is 2x FovScale, not 1x).
|
||||
* An explicit Video.DrawDistance != 100 overrides that coupling outright.
|
||||
* Clamped to <=4.0x -- pushing the far plane much further out risks
|
||||
* far-field z-fighting against the level's original near-plane precision;
|
||||
* raised again (M-121 live playtest: 2x FovScale still showed a "blue
|
||||
* glow" on far Dam tunnel geometry, so auto coupling is now 4x FovScale)
|
||||
* to give headroom (max portFovScale is 1.5 at Video.FovScale's registered
|
||||
* ceiling of 150, so 4x tops out at 6.0x -- ceiling raised to match).
|
||||
* Identity (1.0f) at both defaults, bit-for-bit no-op. NOTE: end-to-end
|
||||
* re-check of fogLoadCurrentEnvironment (bgfog.c) found the fog RAMP
|
||||
* itself (not just the far-clip cutoff) already scales correctly with
|
||||
* this multiplier -- g_ScaledFarFogIntensity/scaled_far_fog_dist both
|
||||
* derive from the same scaled far value. If a visible blue tint at range
|
||||
* persists even at a large multiplier, it may be Dam's tunnel sightline
|
||||
* simply exceeding whatever distance was tried, or a separate visual
|
||||
* element (skybox/backdrop) not gated by Visibility.FarFog at all --
|
||||
* worth a fresh screenshot-driven look before assuming another bug here. */
|
||||
f32 portDrawDistanceMultiplier(void)
|
||||
{
|
||||
f32 mult;
|
||||
if (cfgDrawDistance != 100) {
|
||||
mult = (f32)cfgDrawDistance / 100.0f;
|
||||
} else if (cfgDrawDistanceAutoFov && portFovScale != 1.0f) {
|
||||
mult = portFovScale * 4.0f;
|
||||
} else {
|
||||
return 1.0f;
|
||||
}
|
||||
if (mult > 6.0f) { mult = 6.0f; }
|
||||
if (mult < 1.0f) { mult = 1.0f; }
|
||||
return mult;
|
||||
}
|
||||
|
||||
/* D249: Video.LodDistance -- multiplier on the *distance* term
|
||||
* modelUpdateDistanceRelations() (src/game/model.c) tests against each LOD
|
||||
* node's MinDistance/MaxDistance, composed on top of the game's own
|
||||
* g_ModelDistanceScale rather than replacing it. Smaller distance reads as
|
||||
* "closer", so this function returns the INVERSE of the requested percent:
|
||||
* Video.LodDistance=200 (keep full detail twice as far, more cost) ->
|
||||
* 0.5x on the distance term; =50 (drop to lower detail twice as soon, less
|
||||
* cost -- the perf lever) -> 2.0x. Video.LodDistanceAutoFov (default OFF,
|
||||
* unlike DrawDistanceAutoFov) can couple it to Video.FovScale the same
|
||||
* direction as draw distance if ever wanted; off by default so this stays a
|
||||
* standalone dial and doesn't quietly add cost as FovScale widens. Clamped
|
||||
* to a [0.25, 4.0] distance multiplier (== effective LodDistance 25-400%) --
|
||||
* far outside that band either does nothing visible (LOD never triggers) or
|
||||
* thrashes every frame. Identity (1.0f) at the Video.LodDistance=100
|
||||
* default, bit-for-bit no-op. */
|
||||
f32 portLodDistanceMultiplier(void)
|
||||
{
|
||||
f32 pct;
|
||||
if (cfgLodDistance != 100) {
|
||||
pct = (f32)cfgLodDistance;
|
||||
} else if (cfgLodDistanceAutoFov && portFovScale != 1.0f) {
|
||||
pct = portFovScale * 100.0f;
|
||||
} else {
|
||||
return 1.0f;
|
||||
}
|
||||
if (pct < 25.0f) { pct = 25.0f; }
|
||||
if (pct > 400.0f) { pct = 400.0f; }
|
||||
return 100.0f / pct;
|
||||
}
|
||||
|
||||
PD_CONSTRUCTOR static void videoConfigInit(void)
|
||||
{
|
||||
configRegisterFloat("Game.ScreenShakeIntensity", &portScreenShakeScale, 0.0f, 10.0f);
|
||||
configRegisterInt("Game.SkipIntro", &portSkipIntro, 0, 1);
|
||||
configRegisterInt("Game.NoHitFlash", &portNoHitFlash, 0, 1);
|
||||
configRegisterInt("Game.AllUnlocked", &portAllUnlocked, 0, 1);
|
||||
configRegisterInt("Video.VSync", &cfgVSync, 0, 1);
|
||||
configRegisterInt("Video.FpsCap", &cfgFpsCap, 0, 1000);
|
||||
configRegisterInt("Video.MSAA", &cfgMSAA, 1, 8);
|
||||
configRegisterInt("Video.TextureFilter", &cfgTexFilter, 0, 2);
|
||||
configRegisterInt("Video.FixMipTextures", &cfgFixMipTex, 0, 1);
|
||||
configRegisterInt("Video.WrapFix", &cfgWrapFix, 0, 1);
|
||||
configRegisterInt("Video.FovScale", &cfgFovScale, 50, 150);
|
||||
configRegisterInt("Video.DrawDistance", &cfgDrawDistance, 100, 400);
|
||||
configRegisterInt("Video.DrawDistanceAutoFov", &cfgDrawDistanceAutoFov, 0, 1);
|
||||
configRegisterInt("Video.LodDistance", &cfgLodDistance, 25, 400);
|
||||
configRegisterInt("Video.LodDistanceAutoFov", &cfgLodDistanceAutoFov, 0, 1);
|
||||
configRegisterInt("Video.Anisotropy", &cfgAniso, 1, 16);
|
||||
configRegisterInt("Video.Fullscreen", &cfgFullscreen, 0, 1);
|
||||
configRegisterInt("Window.Width", &cfgWinW, 0, 16384);
|
||||
configRegisterInt("Window.Height", &cfgWinH, 0, 16384);
|
||||
@@ -234,41 +97,10 @@ PD_CONSTRUCTOR static void videoConfigInit(void)
|
||||
configRegisterInt("Window.Maximized", &cfgWinMax, 0, 1);
|
||||
}
|
||||
|
||||
/* Steam Deck / SteamOS first-run preset. Called from main() when STEAMOS is
|
||||
* set, BEFORE configLoad(): if no ge007.ini exists yet, configLoad's
|
||||
* first-run path saves these values to disk and every later launch reads the
|
||||
* file (user changes via F10 win outright); if an ini already exists its
|
||||
* values overwrite everything here. So this is a first-launch preset only.
|
||||
* 1280x800 is the Deck's native panel resolution; MSAA 4 + VSync is
|
||||
* comfortable headroom for the A11 GPU; DrawDistance/LodDistance at 150%
|
||||
* because the authored N64 fade distances read "things pop in just before
|
||||
* you can see them" on a sharp 7" close-up panel (e.g. the Dam lock).
|
||||
* Note: the panel is 16:10 and the game renders 4:3, so this stretches
|
||||
* uniformly like any non-4:3 window today (letterboxing is the parked
|
||||
* WIDESCREEN-FOV-PLAN). */
|
||||
void videoApplySteamOSDefaults(void)
|
||||
{
|
||||
cfgFullscreen = 1;
|
||||
cfgWinW = 1280;
|
||||
cfgWinH = 800;
|
||||
cfgVSync = 1;
|
||||
cfgMSAA = 4;
|
||||
cfgDrawDistance = 150;
|
||||
cfgLodDistance = 150;
|
||||
}
|
||||
|
||||
/* Set by videoRequestLiveConfig() (F10 overlay, host thread); consumed on the
|
||||
* scheduler thread in videoStartFrame() where the GL context is bound. */
|
||||
static volatile int liveCfgDirty = 0;
|
||||
|
||||
/* D211/D212: push the port-only image knobs where they apply. FovScale is a
|
||||
* plain float the game re-reads each frame; anisotropy goes to fast3d. */
|
||||
static void videoApplyImageOptions(void)
|
||||
{
|
||||
portFovScale = (f32)cfgFovScale / 100.0f;
|
||||
gfx_set_anisotropy_level(cfgAniso);
|
||||
}
|
||||
|
||||
static void videoApplyTexFilter(void)
|
||||
{
|
||||
if (cfgTexFilter >= 2) {
|
||||
@@ -439,7 +271,6 @@ int videoInit(void)
|
||||
gfx_set_wrap_fix(cfgWrapFix);
|
||||
|
||||
videoApplyTexFilter();
|
||||
videoApplyImageOptions();
|
||||
|
||||
/* The GL context is currently current on this (host main) thread, but all
|
||||
* rendering happens on the game's scheduler thread. WGL only allows a
|
||||
@@ -480,10 +311,9 @@ void videoStartFrame(void)
|
||||
wmAPI->set_swap_interval(cfgVSync ? 1 : 0);
|
||||
gfx_set_target_fps(cfgFpsCap); /* 0 = uncapped */
|
||||
videoApplyTexFilter();
|
||||
videoApplyImageOptions();
|
||||
sysLogPrintf(LOG_INFO, "video: live config applied "
|
||||
"(vsync=%d fpscap=%d texfilter=%d fov=%d aniso=%d)",
|
||||
cfgVSync, cfgFpsCap, cfgTexFilter, cfgFovScale, cfgAniso);
|
||||
"(vsync=%d fpscap=%d texfilter=%d)",
|
||||
cfgVSync, cfgFpsCap, cfgTexFilter);
|
||||
}
|
||||
|
||||
gfx_start_frame();
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
== census (kind, format) -> count ==
|
||||
('NONZLIB', 'I4'): 363
|
||||
('NONZLIB', 'I8'): 231
|
||||
('NONZLIB', 'IA16'): 1
|
||||
('NONZLIB', 'IA4'): 74
|
||||
('NONZLIB', 'IA8'): 192
|
||||
('NONZLIB', 'RGB15'): 14
|
||||
('NONZLIB', 'RGBA16'): 19
|
||||
('NONZLIB', 'RGBA32'): 60
|
||||
('ZLIB', 'IA16_CI4'): 15
|
||||
('ZLIB', 'IA16_CI8'): 91
|
||||
('ZLIB', 'RGBA16_CI4'): 83
|
||||
('ZLIB', 'RGBA16_CI8'): 1555
|
||||
|
||||
== non-zlib WIDE-pixel images: 94 ==
|
||||
texnum= 0 IMAGE_COPYICON RGBA32 comp=7 32x28 lod=0
|
||||
texnum= 2 IMAGE_DELICON RGBA32 comp=8 32x28 lod=0
|
||||
texnum= 437 _image437_ID RGBA32 comp=8 32x16 lod=6
|
||||
texnum= 439 _image439_ID RGBA32 comp=8 32x16 lod=6
|
||||
texnum= 884 _image884_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=1198 _image1198_ID RGBA16 comp=6 64x17 lod=6
|
||||
texnum=1199 _image1199_ID RGBA16 comp=6 64x17 lod=6
|
||||
texnum=1200 _image1200_ID RGBA16 comp=6 64x17 lod=6
|
||||
texnum=1201 _image1201_ID RGBA16 comp=6 64x17 lod=6
|
||||
texnum=2085 IMAGE_FIRE_0 RGBA16 comp=5 14x14 lod=0
|
||||
texnum=2087 IMAGE_FIRE_1 RGBA16 comp=8 14x14 lod=0
|
||||
texnum=2089 IMAGE_FIRE_2 RGBA16 comp=4 14x14 lod=0
|
||||
texnum=2091 IMAGE_FIRE_3 RGBA16 comp=8 14x14 lod=0
|
||||
texnum=2093 IMAGE_FIRE_4 RGBA16 comp=8 14x14 lod=0
|
||||
texnum=2095 IMAGE_FIRE_5 RGBA16 comp=7 14x14 lod=0
|
||||
texnum=2097 IMAGE_FIRE_6 RGBA16 comp=7 14x14 lod=0
|
||||
texnum=2099 IMAGE_FIRE_7 RGBA16 comp=4 14x14 lod=0
|
||||
texnum=2101 IMAGE_FIRE_8 RGBA16 comp=4 14x14 lod=0
|
||||
texnum=2103 IMAGE_FIRE_9 RGBA16 comp=8 14x14 lod=0
|
||||
texnum=2105 IMAGE_FIRE_10 RGBA16 comp=8 14x14 lod=0
|
||||
texnum=2107 IMAGE_FIRE_11 RGBA16 comp=4 14x14 lod=0
|
||||
texnum=2109 IMAGE_FIRE_12 RGBA16 comp=8 14x14 lod=0
|
||||
texnum=2111 IMAGE_FIRE_13 RGBA16 comp=8 14x14 lod=0
|
||||
texnum=2113 IMAGE_FIRE_14 RGBA16 comp=8 14x14 lod=0
|
||||
texnum=2114 _image2114_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2115 _image2115_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2116 _image2116_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2117 _image2117_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2119 _image2119_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2120 _image2120_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2121 _image2121_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2122 _image2122_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2123 _image2123_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2124 _image2124_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2125 _image2125_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2126 _image2126_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2127 _image2127_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2128 IMAGE_FLAREORANGELINE RGBA32 comp=4 16x32 lod=6
|
||||
texnum=2134 IMAGE_FLAREBLUEROUND RGBA32 comp=8 32x32 lod=0
|
||||
texnum=2135 IMAGE_FLAREBLUELINE RGBA32 comp=8 16x32 lod=6
|
||||
texnum=2137 _image2137_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2138 _image2138_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2139 _image2139_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2140 _image2140_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2141 _image2141_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2142 _image2142_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2143 _image2143_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2144 _image2144_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2145 _image2145_ID RGBA32 comp=8 32x32 lod=0
|
||||
texnum=2146 _image2146_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2147 _image2147_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2148 _image2148_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2149 _image2149_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2150 _image2150_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2151 _image2151_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2152 _image2152_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2153 _image2153_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2154 _image2154_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2155 _image2155_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2156 _image2156_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2157 _image2157_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2158 _image2158_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2159 _image2159_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2160 _image2160_ID RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2161 IMAGE_ROCKETAMMO RGBA32 comp=4 7x22 lod=0
|
||||
texnum=2162 IMAGE_PLAINMINEAMMO RGBA32 comp=4 14x14 lod=0
|
||||
texnum=2163 IMAGE_GRENADEAMMO RGBA32 comp=4 14x18 lod=0
|
||||
texnum=2164 IMAGE_MAGAMMO RGBA32 comp=7 5x15 lod=0
|
||||
texnum=2165 IMAGE_GLAMMO RGBA32 comp=4 8x21 lod=0
|
||||
texnum=2166 IMAGE_KNIFEAMMO RGBA32 comp=4 6x24 lod=0
|
||||
texnum=2167 IMAGE_SHOTAMMO RGBA32 comp=4 6x20 lod=0
|
||||
texnum=2231 IMAGE_9MMAMMO RGBA32 comp=4 5x12 lod=0
|
||||
texnum=2232 IMAGE_RIFLEAMMO RGBA32 comp=4 5x28 lod=0
|
||||
texnum=2233 IMAGE_GGAMMO RGBA32 comp=4 5x12 lod=0
|
||||
texnum=2234 IMAGE_MINEAMMO RGBA32 comp=4 14x14 lod=0
|
||||
texnum=2235 IMAGE_PROXAMMO RGBA32 comp=4 14x14 lod=0
|
||||
texnum=2236 IMAGE_CROSSHAIR1 RGBA32 comp=4 32x32 lod=0
|
||||
texnum=2237 IMAGE_CROSSHAIR2 RGBA32 comp=7 32x32 lod=0
|
||||
texnum=2238 IMAGE_TIMEAMMO RGBA32 comp=4 14x14 lod=0
|
||||
texnum=2430 _image2430_ID IA16 comp=4 64x32 lod=1
|
||||
texnum=2510 _image2510_ID RGB15 comp=7 128x12 lod=2
|
||||
texnum=2511 _image2511_ID RGB15 comp=8 128x12 lod=2
|
||||
texnum=2512 _image2512_ID RGB15 comp=8 128x12 lod=2
|
||||
texnum=2513 _image2513_ID RGB15 comp=6 128x12 lod=2
|
||||
texnum=2514 _image2514_ID RGB15 comp=6 128x12 lod=2
|
||||
texnum=2515 _image2515_ID RGB15 comp=6 128x12 lod=2
|
||||
texnum=2516 _image2516_ID RGB15 comp=8 128x12 lod=2
|
||||
texnum=2517 _image2517_ID RGB15 comp=8 128x12 lod=2
|
||||
texnum=2518 _image2518_ID RGB15 comp=8 128x12 lod=2
|
||||
texnum=2519 _image2519_ID RGB15 comp=8 128x12 lod=2
|
||||
texnum=2520 _image2520_ID RGB15 comp=8 128x12 lod=2
|
||||
texnum=2521 _image2521_ID RGB15 comp=8 128x12 lod=3
|
||||
texnum=2522 _image2522_ID RGB15 comp=8 128x12 lod=3
|
||||
texnum=2523 _image2523_ID RGB15 comp=8 128x12 lod=3
|
||||
@@ -1,136 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""D219 offline probe: parse the non-zlib/zlib texture bitstream headers
|
||||
straight out of the ROM images segment, replicating texLoad/texInflateNonZlib
|
||||
header reads (src/game/image.c) so we can compare TEXCOMPMETHOD/TEXFORMAT for
|
||||
IMAGE_FIRE_N vs muzzle-flash/ammo/flare/impact siblings WITHOUT running the game.
|
||||
|
||||
ROM: data/ge007.ntsc-final.z64
|
||||
Images segment cart start 0x108F7DF0 (port/src/romassets_u.s) -> file offset 0x8F7DF0.
|
||||
Offsets are cumulative sizes from assets/images.def (image_entries_load, no align).
|
||||
"""
|
||||
import re, sys
|
||||
|
||||
SEG = 0x8F7DF0
|
||||
ROM = open('data/ge007.ntsc-final.z64','rb').read()
|
||||
|
||||
# ---- enum name -> index from image_externs.h ----
|
||||
enummap = {}
|
||||
_in_enum = False
|
||||
_v = 0
|
||||
for line in open('assets/image_externs.h', encoding='utf-8', errors='replace'):
|
||||
if 'typedef enum IMAGEIDS' in line:
|
||||
_in_enum = True; continue
|
||||
if _in_enum and line.strip().startswith('}'):
|
||||
break
|
||||
m = re.match(r'\s*([A-Za-z_][A-Za-z0-9_]*)\s*,?\s*(//.*)?$', line)
|
||||
if m and not line.strip().startswith('//'):
|
||||
enummap[m.group(1)] = _v; _v += 1
|
||||
|
||||
# ---- parse images.def: field1 is a bare index or an enum name minus IMAGE_ ----
|
||||
entries = []
|
||||
for line in open('assets/images.def', encoding='utf-8', errors='replace'):
|
||||
m = re.match(r'\s*IMAGE\(\s*([A-Za-z0-9_]+)\s*,\s*(0x[0-9A-Fa-f]+|\d+)\s*,', line)
|
||||
if not m: continue
|
||||
f1, sz = m.group(1), int(m.group(2), 0)
|
||||
if f1.isdigit():
|
||||
idx = int(f1); name = f"_image{idx}_ID"
|
||||
else:
|
||||
key = 'IMAGE_' + f1 if not f1.startswith('IMAGE_') else f1
|
||||
idx = enummap.get(key)
|
||||
name = key
|
||||
if idx is None:
|
||||
# named in images.def but left _imageN_ID in the decomp enum --
|
||||
# trust position
|
||||
idx = len(entries)
|
||||
print(f"# note: {key} not in enum, using positional index {idx}", file=sys.stderr)
|
||||
assert idx == len(entries), f"images.def out of order at {name} ({idx} vs {len(entries)})"
|
||||
entries.append((name, sz))
|
||||
print(f"# {len(entries)} image entries", file=sys.stderr)
|
||||
|
||||
# cumulative offsets (image_entries_load)
|
||||
offs = []
|
||||
o = 0
|
||||
for name, sz in entries:
|
||||
offs.append(o)
|
||||
o += sz
|
||||
total = o
|
||||
print(f"# images segment total size per g_Textures: {total:#x} (segment room to 0xC00000-0x8F7DF0 = {0xC00000-SEG:#x})", file=sys.stderr)
|
||||
|
||||
class Bits:
|
||||
def __init__(self, buf, pos):
|
||||
self.buf = buf; self.pos = pos; self.bit = 0
|
||||
def read(self, n):
|
||||
v = 0
|
||||
for _ in range(n):
|
||||
byte = self.buf[self.pos]
|
||||
v = (v << 1) | ((byte >> (7 - self.bit)) & 1)
|
||||
self.bit += 1
|
||||
if self.bit == 8:
|
||||
self.bit = 0; self.pos += 1
|
||||
return v
|
||||
|
||||
NAMES = {n:i for i,(n,_) in enumerate(entries)}
|
||||
|
||||
def dump(i, name):
|
||||
off = offs[i]
|
||||
base = SEG + off
|
||||
b0 = ROM[base]
|
||||
u, z, lod = (b0>>7)&1, (b0>>6)&1, b0 & 0x3f
|
||||
out = [f"texnum={i:4d} {name:28s} off={off:#08x} byte0={b0:#04x} u={u} z={z} lod={lod}"]
|
||||
if z:
|
||||
# zlib path: format(8), numcolours(8)+1, palette... then per-image w/h
|
||||
b = Bits(ROM, base+1)
|
||||
fmt = b.read(8); nc = b.read(8)+1
|
||||
pal = [b.read(16) for _ in range(nc)]
|
||||
out.append(f" ZLIB format={fmt} numcolours={nc} pal0={pal[0]:#05x}")
|
||||
w = b.read(8); h = b.read(8)
|
||||
out.append(f" image0 {w}x{h} (zlib payload follows -- indices only)")
|
||||
else:
|
||||
b = Bits(ROM, base+1)
|
||||
fmt = b.read(4); w = b.read(8); h = b.read(8); cm = b.read(4)
|
||||
out.append(f" NONZLIB format={fmt} width={w} height={h} compmethod={cm}")
|
||||
return ' '.join(out)
|
||||
|
||||
FMT = {0:'RGBA32',1:'RGBA16',2:'RGB24',3:'RGB15',4:'IA16',5:'IA8',6:'IA4',7:'I8',8:'I4',9:'RGBA16_CI8',10:'RGBA16_CI4',11:'IA16_CI8',12:'IA16_CI4'}
|
||||
CM = {0:'UNCOMPRESSED0',1:'UNCOMPRESSED1',2:'HUFFMAN',3:'HUFFMANPERHCHANNEL',4:'RLE',5:'LOOKUP',6:'HUFFMANLOOKUP',7:'RLELOOKUP',8:'HUFFMANBLUR',9:'RLEBLUR'}
|
||||
|
||||
def dump2(i, name):
|
||||
off = offs[i]
|
||||
base = SEG + off
|
||||
b0 = ROM[base]
|
||||
z = (b0>>6)&1; lod = b0 & 0x3f
|
||||
line = f"texnum={i:4d} {name:28s} off={off:#08x} byte0={b0:#04x} z={z} lod={lod}"
|
||||
if z:
|
||||
b = Bits(ROM, base+1)
|
||||
fmt = b.read(8); nc = b.read(8)+1
|
||||
pal = [b.read(16) for _ in range(nc)]
|
||||
w = b.read(8); h = b.read(8)
|
||||
line += f" ZLIB fmt={FMT.get(fmt,fmt)}({fmt}) ncols={nc} pal0={pal[0]:#05x} img0={w}x{h}"
|
||||
else:
|
||||
b = Bits(ROM, base+1)
|
||||
fmt = b.read(4); w = b.read(8); h = b.read(8); cm = b.read(4)
|
||||
line += f" NONZLIB fmt={FMT.get(fmt,fmt)}({fmt}) {w}x{h} comp={CM.get(cm,cm)}({cm})"
|
||||
return line
|
||||
|
||||
targets = []
|
||||
# fire + smoke particle range
|
||||
for i in range(2084, 2116):
|
||||
targets.append(i)
|
||||
# flares
|
||||
for n in ['IMAGE_FLAREORANGELINE','IMAGE_FLAREBLUEROUND','IMAGE_FLAREBLUELINE','IMAGE_FLAREWHITEROUND']:
|
||||
targets.append(NAMES[n])
|
||||
# ammo HUD icons
|
||||
for n in ['IMAGE_ROCKETAMMO','IMAGE_PLAINMINEAMMO','IMAGE_GRENADEAMMO','IMAGE_MAGAMMO','IMAGE_GLAMMO','IMAGE_KNIFEAMMO','IMAGE_SHOTAMMO']:
|
||||
targets.append(NAMES[n])
|
||||
# impact sprites
|
||||
for n in ['IMAGE_IMPACT1','IMAGE_IMPACT2','IMAGE_IMPACT3','IMAGE_IMPACT4','IMAGE_IMPACTMULTI','IMAGE_IMPACTREDBRICK2','IMAGE_IMPACTBRICK2','IMAGE_IMPACTBRICK3']:
|
||||
targets.append(NAMES[n])
|
||||
# smoke sprites
|
||||
for n in ['IMAGE_SMOKE1','IMAGE_SMOKE2','IMAGE_SMOKEBALLS1']:
|
||||
targets.append(NAMES[n])
|
||||
|
||||
seen = set()
|
||||
for i in targets:
|
||||
if i in seen or i >= len(entries): continue
|
||||
seen.add(i)
|
||||
print(dump2(i, entries[i][0]))
|
||||
@@ -7,10 +7,6 @@
|
||||
#include "speed_graph.h"
|
||||
#if defined(PORT)
|
||||
#include <string.h> /* PC port: memcpy for the CUSTOM_FX_PARAMS_N copy in audioInit (docs/dev/findings.md §11) */
|
||||
#if defined(__x86_64__)
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/**
|
||||
@@ -663,15 +659,6 @@ s32 amDmaCallback(s32 addr, s32 len, void* state)
|
||||
/* mark it used */
|
||||
dmaPtr->lastFrame = (s32) g_AudioFrameCount;
|
||||
freeBuffer = (dmaPtr->ptr + addr) - dmaPtr->startAddr;
|
||||
#if defined(__x86_64__)
|
||||
if (getenv("GE_MIXERTRACE")) {
|
||||
static FILE *dtf2 = NULL;
|
||||
if (!dtf2) { dtf2 = fopen("mixertrace.log", "a"); if (dtf2) setvbuf(dtf2, NULL, _IONBF, 0); }
|
||||
if (dtf2) fprintf(dtf2, "[DMAHIT] addr=0x%08x len=%d bufStart=0x%08x bufPtr=%p ret=%p\n",
|
||||
(unsigned)addr, (int)len, (unsigned)dmaPtr->startAddr,
|
||||
dmaPtr->ptr, freeBuffer);
|
||||
}
|
||||
#endif
|
||||
return osVirtualToPhysical(freeBuffer);
|
||||
}
|
||||
|
||||
@@ -731,15 +718,6 @@ s32 amDmaCallback(s32 addr, s32 len, void* state)
|
||||
dmaPtr->lastFrame = g_AudioFrameCount;
|
||||
|
||||
osPiStartDma(&g_DmaIOMessageBuffer[g_NextDMa++], OS_MESG_PRI_HIGH, OS_READ, (u32)addr, freeBuffer, AUDIO_DMA_MAX_BUFFER_LENGTH, &g_DmaMessageQueue);
|
||||
#if defined(__x86_64__)
|
||||
if (getenv("GE_MIXERTRACE")) {
|
||||
static FILE *dtf3 = NULL;
|
||||
if (!dtf3) { dtf3 = fopen("mixertrace.log", "a"); if (dtf3) setvbuf(dtf3, NULL, _IONBF, 0); }
|
||||
if (dtf3) fprintf(dtf3, "[DMAMISS] addr=0x%08x len=%d bufPtr=%p ret=0x%08x\n",
|
||||
(unsigned)addr, (int)len, freeBuffer,
|
||||
(unsigned)((s32)osVirtualToPhysical(freeBuffer) + delta));
|
||||
}
|
||||
#endif
|
||||
return (s32)osVirtualToPhysical(freeBuffer) + delta;
|
||||
}
|
||||
|
||||
|
||||
@@ -2188,12 +2188,7 @@ typedef union
|
||||
// recalculated, the array replaced with a new one and index set to 0.
|
||||
/* player offset 0x58 */
|
||||
u8 curindex;
|
||||
// SPEED tier for this GOPOS: 0 = walk, 1 = run, 2 = sprint. Set from
|
||||
// the SPEED enum in plot_course_for_actor(); read back by
|
||||
// chrlvApplySpeed() and (via a raw-byte union alias on N64 -- see
|
||||
// D209) by the locomotion-animation selector get_sound_at_range().
|
||||
// NB: the old "guess: room" comment here was wrong.
|
||||
u8 unk59;
|
||||
u8 unk59; // guess: room
|
||||
|
||||
// g_ClockTimer related
|
||||
u16 unk5a;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#include <PR/os.h>
|
||||
#ifdef PORT
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#endif
|
||||
#include "bondview.h"
|
||||
#include <bondconstants.h>
|
||||
@@ -582,41 +581,7 @@ void bossMainloop(void)
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef PORT
|
||||
/* D250: isolate DL-building cost (culling,
|
||||
* animation, scene traversal) from gfx_run's
|
||||
* software-RSP interpretation cost, measured
|
||||
* separately by libultra.c's "frame rendered in
|
||||
* us" (D193 already showed that one stays well
|
||||
* under budget while real fps still sags -- this
|
||||
* finds out whether lvlRender is where the rest
|
||||
* of the missing time goes). Env-gated, no-op
|
||||
* unless GE_D250 is set. */
|
||||
{
|
||||
static int gd250 = -1;
|
||||
static OSTime d250Sum = 0, d250WallStart = 0;
|
||||
static int d250N = 0;
|
||||
OSTime d250T0 = 0;
|
||||
if (gd250 < 0) gd250 = getenv("GE_D250") != NULL;
|
||||
if (gd250) d250T0 = osGetTime();
|
||||
#endif
|
||||
gdl = lvlRender(gdl);
|
||||
#ifdef PORT
|
||||
if (gd250) {
|
||||
OSTime now = osGetTime();
|
||||
d250Sum += now - d250T0;
|
||||
d250N++;
|
||||
if (!d250WallStart) d250WallStart = now;
|
||||
if (now - d250WallStart >= 1000000) {
|
||||
fprintf(stderr,
|
||||
"[D250] lvlRender avg=%lluus n=%d\n",
|
||||
(unsigned long long)(d250Sum / (d250N ? d250N : 1)),
|
||||
d250N);
|
||||
d250Sum = 0; d250N = 0; d250WallStart = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Lets Visualise the Coverage Value used for Scilohete Anti-Ailising (edges)
|
||||
// (done on the VI), also produces a cool looking linemode - providing AA is working.
|
||||
|
||||
@@ -715,29 +715,7 @@ Gfx *viSetupCurrentPlayerView(Gfx *gdl)
|
||||
|
||||
// Create both a floating-point matrix for the CPU side and a fixed-point matrix for the RSP.
|
||||
g_viProjectionMatrix = dynAllocateMatrix();
|
||||
#ifdef PORT
|
||||
/* D211: Video.FovScale — widen the render FOV here, at the one per-frame
|
||||
* chokepoint that feeds both the world (via the CPU-combined proj x view
|
||||
* matrix) and the sky/full-screen layer. fovy is in degrees, so an
|
||||
* angle-proportional scale (portFovScale, default 1.0f) matches the
|
||||
* documented 60deg->75deg at 1.25. Composes with the per-frame aim-zoom
|
||||
* rewrites of fovy for free. Default 1.0f => identical matrix, bit for
|
||||
* bit. D222: the same widen (portScaleFovY, port/src/video.c) now also
|
||||
* feeds currentPlayerSetPerspective at the viSet* call sites below, so
|
||||
* frustum-cull planes and the fog/LOD distance scale stay calibrated to
|
||||
* what's actually rendered instead of the narrower nominal FOV. */
|
||||
{
|
||||
extern f32 portScaleFovY(f32 fovy, s32 isTitleScreen);
|
||||
extern s32 lvlGetCurrentStageToLoad(void);
|
||||
/* Gameplay only: the front end (main menu / file / mission select /
|
||||
* briefing) also renders through this path with stage == LEVELID_TITLE
|
||||
* (lv.c lvlRender), and the menu 3D is authored at a fixed FOV. */
|
||||
f32 frFovY = portScaleFovY(g_ViBackData->fovy, lvlGetCurrentStageToLoad() == LEVELID_TITLE);
|
||||
guPerspectiveF(g_viProjectionMatrixF, &g_viPerspNorm, frFovY, g_ViBackData->aspect, g_ViBackData->znear, g_ViBackData->zfar, 1.0f);
|
||||
}
|
||||
#else
|
||||
guPerspectiveF(g_viProjectionMatrixF, &g_viPerspNorm, g_ViBackData->fovy, g_ViBackData->aspect, g_ViBackData->znear, g_ViBackData->zfar, 1.0f);
|
||||
#endif
|
||||
guMtxF2L(g_viProjectionMatrixF, g_viProjectionMatrix);
|
||||
|
||||
/**
|
||||
@@ -930,44 +908,17 @@ void viSetUseZBuf(s32 usezbuf)
|
||||
g_ViBackData->usezbuf = usezbuf;
|
||||
}
|
||||
|
||||
#ifdef PORT
|
||||
/* D222: currentPlayerSetPerspective feeds c_perspfovy, which
|
||||
* currentPlayerSetCameraScale() (game/bondview.c) uses to derive the
|
||||
* frustum-cull plane normals (c_cameratopnorm/c_cameraleftnorm) and the
|
||||
* fog/LOD distance scale (c_scalelod/c_lodscalez, consumed by
|
||||
* chrobjFogVisRangeRelated() et al. in game/propobj.c). Every viSet* below
|
||||
* was calling it with the nominal g_ViBackData->fovy even though the actual
|
||||
* render projection (this file, viSetupCurrentPlayerView) had already been
|
||||
* widened by Video.FovScale — so at high FovScale, on-screen geometry near
|
||||
* the edges (and the fog-based distance-visibility fade) got culled/faded
|
||||
* as if the view were still narrow. Identity at FovScale=100. */
|
||||
static f32 frCullFovY(void)
|
||||
{
|
||||
extern f32 portScaleFovY(f32 fovy, s32 isTitleScreen);
|
||||
extern s32 lvlGetCurrentStageToLoad(void);
|
||||
return portScaleFovY(g_ViBackData->fovy, lvlGetCurrentStageToLoad() == LEVELID_TITLE);
|
||||
}
|
||||
#endif
|
||||
|
||||
void viSetFovY(f32 fovy)
|
||||
{
|
||||
g_ViBackData->fovy = fovy;
|
||||
#ifdef PORT
|
||||
currentPlayerSetPerspective(g_ViBackData->znear, frCullFovY(), g_ViBackData->aspect);
|
||||
#else
|
||||
currentPlayerSetPerspective(g_ViBackData->znear, g_ViBackData->fovy, g_ViBackData->aspect);
|
||||
#endif
|
||||
currentPlayerSetCameraScale();
|
||||
}
|
||||
|
||||
void viSetAspect(f32 aspect)
|
||||
{
|
||||
g_ViBackData->aspect = aspect;
|
||||
#ifdef PORT
|
||||
currentPlayerSetPerspective(g_ViBackData->znear, frCullFovY(), g_ViBackData->aspect);
|
||||
#else
|
||||
currentPlayerSetPerspective(g_ViBackData->znear, g_ViBackData->fovy, g_ViBackData->aspect);
|
||||
#endif
|
||||
currentPlayerSetCameraScale();
|
||||
}
|
||||
|
||||
@@ -980,11 +931,7 @@ void viSetFov(f32 fovx, f32 fovy)
|
||||
{
|
||||
g_ViBackData->fovy = fovy;
|
||||
g_ViBackData->aspect = (f32) (fovx / fovy);
|
||||
#ifdef PORT
|
||||
currentPlayerSetPerspective(g_ViBackData->znear, frCullFovY(), g_ViBackData->aspect);
|
||||
#else
|
||||
currentPlayerSetPerspective(g_ViBackData->znear, g_ViBackData->fovy, g_ViBackData->aspect);
|
||||
#endif
|
||||
currentPlayerSetCameraScale();
|
||||
}
|
||||
|
||||
@@ -992,11 +939,7 @@ void viSetZRange(f32 near, f32 far)
|
||||
{
|
||||
g_ViBackData->znear = near;
|
||||
g_ViBackData->zfar = far;
|
||||
#ifdef PORT
|
||||
currentPlayerSetPerspective(g_ViBackData->znear, frCullFovY(), g_ViBackData->aspect);
|
||||
#else
|
||||
currentPlayerSetPerspective(g_ViBackData->znear, g_ViBackData->fovy, g_ViBackData->aspect);
|
||||
#endif
|
||||
currentPlayerSetCameraScale();
|
||||
}
|
||||
|
||||
|
||||
@@ -291,9 +291,6 @@ void fogLoadCurrentEnvironment(EnvironmentRecord *arg0)
|
||||
f32 temp_f0;
|
||||
f32 sp20; // 32
|
||||
f32 sp1C; // 28
|
||||
#ifdef PORT
|
||||
f32 scaledFarFog; // D218: Video.DrawDistance -- see fogLoadCurrentEnvironment body
|
||||
#endif
|
||||
#ifdef DEBUG
|
||||
f32 fmin; // hoisted to global
|
||||
f32 fmax; // hoisted to global
|
||||
@@ -305,23 +302,7 @@ void fogLoadCurrentEnvironment(EnvironmentRecord *arg0)
|
||||
assert(fmax <= 1.0F);
|
||||
#endif
|
||||
|
||||
#ifdef PORT
|
||||
/* D218: Video.DrawDistance -- fogLoadCurrentEnvironment is the single
|
||||
* point where a level's authored Visibility.FarFog becomes the actual
|
||||
* runtime far-clip/fog distance (both viSetZRange's far plane and the
|
||||
* fog-blend math just below use it), so scaling it here keeps the two
|
||||
* consistent. Read arg0->Visibility.FarFog into a local instead of
|
||||
* mutating the record -- EnvironmentRecord data is shared/reloaded, not
|
||||
* a per-frame copy. Identity at DrawDistance=100/DrawDistanceAutoFov's
|
||||
* FovScale=100 default. */
|
||||
{
|
||||
extern f32 portDrawDistanceMultiplier(void);
|
||||
scaledFarFog = arg0->Visibility.FarFog * portDrawDistanceMultiplier();
|
||||
}
|
||||
viSetZRange(arg0->Visibility.BlendMultiplier, scaledFarFog);
|
||||
#else
|
||||
viSetZRange(arg0->Visibility.BlendMultiplier, arg0->Visibility.FarFog);
|
||||
#endif
|
||||
viGetZRange(&zrange);
|
||||
|
||||
temp_f0 = bgGetLevelVisibilityScale();
|
||||
@@ -336,13 +317,7 @@ void fogLoadCurrentEnvironment(EnvironmentRecord *arg0)
|
||||
|
||||
g_CurFogDetails.g_CurFogDetails = (arg0->Visibility.BlendMultiplier / temp_f0);
|
||||
pk0 = g_CurFogDetails.g_CurFogDetails;
|
||||
#ifdef PORT
|
||||
/* D218: keep the fog-blend distance consistent with the scaled far
|
||||
* clip plane set above, instead of the level's raw authored FarFog. */
|
||||
g_CurFogDetails.scaled_far_fog_dist = (scaledFarFog / temp_f0);
|
||||
#else
|
||||
g_CurFogDetails.scaled_far_fog_dist = (arg0->Visibility.FarFog / temp_f0);
|
||||
#endif
|
||||
pk4 = g_CurFogDetails.scaled_far_fog_dist;
|
||||
|
||||
// numerator is constant 128.0f
|
||||
|
||||