forked from Ports/goldeneye-pc-port
v0.4.1 sync: build and CI fixes, settings presets D440-D447, outside PRs
build-pc.sh Windows temp-dir re-exec fixes; CI pin actions and no-cache ROM handling; original-N64 and Deck presets; D440 preset labelling; D442 All unlocked as a RAM/query-time override; D443 settings values (MSAA 16x, Restart game); D444 front-end Previous/Next paging; D447 Original-aspect pillarbox/letterbox. Outside contributions by dolent: PRs #120 and #121 (relabelled D445, D446), merged as dolent's original commits. Refs: D440 D441 D442 D443 D444 D445 D446 D447 D387 D283 #120 #121
This commit is contained in:
commit
ad3856892a
72 files changed
+2228
-2080
No files matched your search
+32
-2
@@ -1,12 +1,40 @@
|
||||
# Security Policy
|
||||
|
||||
This project is a non-commercial, fan-made research port. It has no server
|
||||
component and ships no binaries. The realistic security surface is:
|
||||
component, no networking of any kind, and no telemetry. Each release ships
|
||||
prebuilt Windows and Linux binaries, but never a ROM or any game asset — you
|
||||
supply those yourself from a copy you legally own. The realistic security
|
||||
surface is:
|
||||
|
||||
- the PC port executable parsing your own ROM and asset files at load time
|
||||
(a malformed ROM/asset could in principle crash it or worse), and
|
||||
- the build/extraction scripts and CI workflow.
|
||||
|
||||
A full walkthrough of what a release actually installs on your machine, and
|
||||
what a from-source build pulls in, is in
|
||||
[`docs/security-and-fidelity-status.md`](../docs/security-and-fidelity-status.md).
|
||||
|
||||
### Why the binaries are unsigned
|
||||
|
||||
The Windows `.exe` and the Linux build are **not code-signed** — this is a
|
||||
free, single-developer, non-commercial hobby project with no code-signing
|
||||
budget, not a sign of anything wrong with the binary. Two things you may
|
||||
run into as a result:
|
||||
|
||||
- **Windows SmartScreen** may show "Windows protected your PC" on first
|
||||
run. Click "More info" → "Run anyway."
|
||||
- Some antivirus engines flag `ge007-convert.exe` (the one-time ROM-asset
|
||||
converter bundled in the release) specifically, because it's packaged
|
||||
with PyInstaller's `--onefile` mode — a well-known trigger for heuristic
|
||||
AV engines industry-wide, unrelated to what this project's code actually
|
||||
does. The game launches this converter itself on the first run, when the
|
||||
derived asset folders are missing, so an AV prompt can appear at that
|
||||
moment rather than when you unpack the release. If you want to verify what
|
||||
it does, its full source is `prepare-assets.py`, shipped right next to it in
|
||||
the same bundle folder. The build pins the PyInstaller version and the CI
|
||||
actions it uses (by commit SHA), so what ships is reproducible from this
|
||||
repository.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Please **do not** open a public issue for a security problem.
|
||||
@@ -38,4 +66,6 @@ errors, and anything requiring a ROM or assets we do not distribute.
|
||||
|
||||
## Supported versions
|
||||
|
||||
Only the tip of the default branch is supported. There are no releases yet.
|
||||
Only the tip of the default branch and the
|
||||
[latest release](https://github.com/jkdansereau/goldeneye-pc-port/releases/latest)
|
||||
are supported.
|
||||
+17
-17
@@ -75,7 +75,7 @@ jobs:
|
||||
# self-hosted per the 2026-09-12 standing preference.
|
||||
windows_selfhosted: ${{ steps.runner.outputs.selfhosted }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -187,7 +187,7 @@ jobs:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_MAXSIZE: 500M
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -198,7 +198,7 @@ jobs:
|
||||
python3-pip libsdl2-dev zlib1g-dev libgl1-mesa-dev ccache patchelf
|
||||
|
||||
- name: Restore ccache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ env.CCACHE_DIR }}
|
||||
key: ccache-linux-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }}
|
||||
@@ -216,8 +216,8 @@ jobs:
|
||||
# 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 -c "import PyInstaller, sys; sys.exit(PyInstaller.__version__ != '6.22.3')" 2>/dev/null \
|
||||
|| python3 -m pip install --quiet --break-system-packages pyinstaller==6.22.3
|
||||
python3 -m PyInstaller --onefile --noconfirm --clean \
|
||||
--name ge007-convert \
|
||||
--distpath tools_pc/dist/prepare-assets \
|
||||
@@ -254,7 +254,7 @@ jobs:
|
||||
tools_pc/bundle-linux.sh "$V"
|
||||
|
||||
- name: Upload bundle
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: goldeneye-pc-port-linux-x86_64
|
||||
path: |
|
||||
@@ -288,7 +288,7 @@ jobs:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_MAXSIZE: 500M
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -300,7 +300,7 @@ jobs:
|
||||
# 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
|
||||
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2.33.0
|
||||
with:
|
||||
msystem: MINGW64
|
||||
release: ${{ needs.changes.outputs.windows_selfhosted != 'true' }}
|
||||
@@ -316,7 +316,7 @@ jobs:
|
||||
run: tools_pc/check-selfhosted-deps.sh
|
||||
|
||||
- name: Restore ccache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ env.CCACHE_DIR }}
|
||||
key: ccache-windows-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }}
|
||||
@@ -335,9 +335,9 @@ jobs:
|
||||
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 -c "import PyInstaller, sys; sys.exit(PyInstaller.__version__ != '6.22.3')" 2>/dev/null || {
|
||||
pacman -S --noconfirm --needed mingw-w64-x86_64-python-pip
|
||||
python -m pip install --quiet --break-system-packages pyinstaller==6.22.3
|
||||
}
|
||||
python -m PyInstaller --onefile --noconfirm --clean \
|
||||
--name ge007-convert \
|
||||
@@ -366,7 +366,7 @@ jobs:
|
||||
tools_pc/bundle-win.sh "$V"
|
||||
|
||||
- name: Upload bundle
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: goldeneye-pc-port-win64
|
||||
path: |
|
||||
@@ -390,16 +390,16 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: goldeneye-pc-port-win64
|
||||
path: dist
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: goldeneye-pc-port-linux-x86_64
|
||||
path: dist
|
||||
|
||||
@@ -9,16 +9,19 @@
|
||||
# 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).
|
||||
# - ROM -> copied every run from a seed dir on the runner:
|
||||
# {runner_home}/_rom-seed/, or the repo variable
|
||||
# GE_ROM_SEED_DIR. Bootstrap: put ge007.ntsc-final.z64,
|
||||
# pcmodels-ntsc-final/ and pccg-ntsc-final/ there once.
|
||||
# Integrity is re-verified every run against the committed
|
||||
# ge007.u.sha1 (repo-tracked US ROM hash).
|
||||
# The ROM is deliberately NOT stored in actions/cache (it
|
||||
# used to be, key ge-rom-data-ntsc-v1). Cache entries live on
|
||||
# GitHub's side, and pull_request workflows, including ci.yml
|
||||
# runs from fork PRs, can restore caches created on the base
|
||||
# branch, so a PR could read the ROM out of the cache. (The
|
||||
# old save never actually persisted on this box, so no entry
|
||||
# was ever created; checked 2026-09-30.)
|
||||
# - 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
|
||||
@@ -37,8 +40,8 @@
|
||||
# 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/.
|
||||
# - The ROM + sidecars live only in the runner's local seed dir (never in
|
||||
# actions/cache); 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.
|
||||
|
||||
@@ -67,7 +70,7 @@ concurrency:
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
ROM_CACHE_KEY: ge-rom-data-ntsc-v1
|
||||
GE_ROM_SEED_DIR: ${{ vars.GE_ROM_SEED_DIR }}
|
||||
|
||||
jobs:
|
||||
build-and-sweep:
|
||||
@@ -79,7 +82,7 @@ jobs:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_MAXSIZE: 500M
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -90,7 +93,7 @@ jobs:
|
||||
# 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
|
||||
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2.33.0
|
||||
with:
|
||||
msystem: MINGW64
|
||||
release: false
|
||||
@@ -101,71 +104,26 @@ jobs:
|
||||
- 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"
|
||||
|
||||
# --- ROM + sidecars: runner-local seed dir (never actions/cache) ------
|
||||
# {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'
|
||||
# The seed dir is the only copy the workflow uses; it is never deleted.
|
||||
- name: Stage ROM + sidecars (runner seed dir)
|
||||
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
|
||||
}
|
||||
seed="${GE_ROM_SEED_DIR:-$(dirname "$(dirname "$RUNNER_WORKSPACE")")/_rom-seed}"
|
||||
for f in ge007.ntsc-final.z64 pcmodels-ntsc-final pccg-ntsc-final; do
|
||||
[ -e "$seed/$f" ] || {
|
||||
echo "missing $seed/$f" >&2
|
||||
echo "One-time bootstrap: copy ge007.ntsc-final.z64, pcmodels-ntsc-final/" >&2
|
||||
echo "and pccg-ntsc-final/ into $seed (or set the repo variable GE_ROM_SEED_DIR)." >&2
|
||||
exit 2
|
||||
}
|
||||
done
|
||||
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 }}
|
||||
echo "staged ROM store from $seed"
|
||||
|
||||
- name: Verify ROM integrity (committed US sha1)
|
||||
run: |
|
||||
@@ -179,7 +137,7 @@ jobs:
|
||||
# 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
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ env.CCACHE_DIR }}
|
||||
key: ccache-selfhosted-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }}
|
||||
@@ -197,8 +155,16 @@ jobs:
|
||||
# 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)
|
||||
env:
|
||||
INPUT_LEVELS: ${{ inputs.levels }}
|
||||
run: |
|
||||
[ -n "${{ inputs.levels }}" ] && export SWEEP_LEVELS='${{ inputs.levels }}'
|
||||
if [ -n "$INPUT_LEVELS" ]; then
|
||||
if ! printf '%s' "$INPUT_LEVELS" | grep -Eq '^[A-Za-z0-9]+:[0-9]+( [A-Za-z0-9]+:[0-9]+)*$'; then
|
||||
echo "levels input must be space-separated Name:NN entries, e.g. 'Dam:33 Facility:34'" >&2
|
||||
exit 1
|
||||
fi
|
||||
export SWEEP_LEVELS="$INPUT_LEVELS"
|
||||
fi
|
||||
tools_pc/verify.sh sweep --json > sweep.log 2>&1
|
||||
rc=$?
|
||||
cat sweep.log
|
||||
@@ -223,7 +189,7 @@ jobs:
|
||||
# Console logs + verdict only — no frames, no ROM bytes (leak rules).
|
||||
- name: Upload sweep report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: selfhosted-sweep-${{ github.run_number }}
|
||||
path: |
|
||||
|
||||
@@ -68,20 +68,31 @@ Needs CMake + SDL2 + zlib + OpenGL, and must run from the MSYS2 MINGW64 shell
|
||||
In `.ps1`, `-DROMID=$Var` is a literal (bare tokens don't expand) — it
|
||||
must be `"-DROMID=$Var"`. Launch powershell plainly — `env -i` before it
|
||||
breaks the nested cmake launch (verified 2026-09-28).
|
||||
**OPEN (next session):** the guard still fails *inside* `build-pc.sh`
|
||||
(cmake "did not execute": the `.ps1` runs, but `$LASTEXITCODE` stays
|
||||
empty) while the *identical* standalone invocation succeeds (cmake runs
|
||||
and the build completes — that is how the D401 builds were made). First
|
||||
test: diff the heredoc-generated `.ps1` against a hand-extracted copy
|
||||
(suspect: heredoc line-ending/whitespace corruption).
|
||||
**Working workaround:** extract the heredoc `.ps1` and run
|
||||
`powershell -NoProfile -ExecutionPolicy Bypass -File <ps1> "C:\msys64\usr\bin" "C:\msys64\mingw64\bin" C:/msys64 <repo-win-path> build-pc <romid>`.
|
||||
Re-confirmed 2026-09-28: in-script re-exec fails again (cmake configure
|
||||
dies inside the ps1; re-exec logs left stale) while the identical
|
||||
standalone invocation reconfigured and built cleanly (extract with
|
||||
`awk "/<<'PS1'\$/{f=1;next} /^PS1\$/{f=0} f" build-pc.sh > /tmp/ps1`
|
||||
— mind the leading whitespace, and `export PATH` to include mingw/bin
|
||||
first, failure mode 3).
|
||||
**FIXED 2026-09-30 (`fix/build-pc-tmp-reexec`):** the heredoc `.ps1`
|
||||
was byte-identical to a hand-extracted copy — not line endings. Three
|
||||
real bugs: (a) the TMP probe's inline `cmd //c "... \"path\" ..."` never
|
||||
ran (msys re-escapes embedded quotes as `\"`, which cmd rejects), so
|
||||
*every* build took the re-exec, and it only tested that TMP existed;
|
||||
(b) the `.ps1`'s toolchain dirs were derived from `command -v cmake`,
|
||||
which in Git Bash/agent shells is a pip cmake (no MSYS2 toolchain) —
|
||||
the hand-run worked because it passed explicit `C:\msys64\...` paths;
|
||||
(c) `/tmp` + `cygpath` disagree when Git Bash and MSYS2 `usr/bin` are
|
||||
both on PATH, so the `.ps1` could be written to one `/tmp` and
|
||||
powershell pointed at the other (it then exits 0 having done nothing).
|
||||
Now: the MSYS2 `mingw64/bin` is located and validated (override:
|
||||
`GE_MSYS2_ROOT`) and prepended to PATH; the probe is a `.cmd` file that
|
||||
tests TMP *writability*; all native-facing files live in the build dir
|
||||
with paths from bash's own `pwd -W`. Verified from an agent Git Bash
|
||||
shell: fresh + incremental builds, both on the direct path and with a
|
||||
forced unwritable TMP (`TMP=C:\Windows\`) through the re-exec.
|
||||
**FIXED 2026-09-30 (`fix/build-pc-reexec-noop`): the re-exec exited rc=2
|
||||
with the diag log ending `cmake ran LASTEXITCODE=[]`.** From a shell with
|
||||
MSYS2 `usr/bin` first on PATH, powershell inherits a stripped ~13-var env
|
||||
with `PATHEXT=.CPL` and no ComSpec/TMP/TEMP, so `& cmake.exe` silently
|
||||
launches nothing. The `.ps1` now restores PATHEXT and ComSpec. In that
|
||||
shell the re-exec itself is legitimately needed (no TMP), so the probe
|
||||
is correct. Verified: MSYS2-PATH shell, forced `TMP=C:\Windows\`, and a
|
||||
no-change incremental run all exit 0 with a fresh exe.
|
||||
2. **`cannot open output file ge007.x86_64.exe: Permission denied`.** A
|
||||
**running** `ge007.x86_64.exe` locks the output file (Windows rule; you
|
||||
can't relink over a live PE). Check with `Get-Process | Where-Object {
|
||||
|
||||
@@ -135,8 +135,10 @@ Linux, including Steam Deck.
|
||||
(what you'll notice, impact, workarounds). The short version: no multiplayer
|
||||
yet, NTSC-U ROMs only, no macOS/ARM builds, and a couple of cosmetic defects.
|
||||
|
||||
> **Before enabling `All unlocked`, back up `data/ge007.eep`.** Saving with it
|
||||
> on can permanently write fake unlocks into your save (D387).
|
||||
> **Before first using `All unlocked`, back up `data/ge007.eep`.** Saves made
|
||||
> with it on no longer write the fake unlocks to disk (D387), but a save that an
|
||||
> earlier build already polluted stays polluted, and cheats you earn while it is
|
||||
> on are not kept. It applies at the next launch.
|
||||
|
||||
### Steam Deck
|
||||
|
||||
@@ -144,15 +146,15 @@ The Linux bundle is the Deck build. SFTP it over from your PC, or download
|
||||
it straight from the [releases page](https://github.com/jkdansereau/goldeneye-pc-port/releases) on the Deck itself:
|
||||
extract, 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, 2× MSAA, and 250% draw/LOD
|
||||
distance (the N64-authored fade distances read short on the close-up 7"
|
||||
panel); everything is changeable in the options overlay and persists
|
||||
afterwards. **Do that first launch in Game Mode, not Desktop Mode** — an ini
|
||||
created by an earlier Desktop Mode launch (e.g. while testing before adding
|
||||
it as a Steam shortcut) permanently skips the Deck preset, since any
|
||||
existing ini always wins over it (D283). If your resolution isn't 1280×800
|
||||
on first Game Mode boot, just set it manually: F10 → *Resolution*. The
|
||||
installing. The port detects a Steam Deck by its hardware id (Linux DMI, or
|
||||
`STEAMOS`) and, once per `ge007.ini`, applies Deck-friendly display defaults:
|
||||
native 1280×800 fullscreen. VSync, 2× MSAA and 250% draw/LOD distance are
|
||||
already the port's defaults (the N64-authored fade distances read short on the
|
||||
close-up 7" panel). The preset only applies while the display settings are still
|
||||
untouched, so an ini created earlier (for example by a Desktop Mode test launch)
|
||||
no longer skips it for good (D283); everything is changeable in the options
|
||||
overlay and persists afterwards. If your resolution isn't 1280×800 on first
|
||||
boot, just set it manually: F10 → *Resolution*. The
|
||||
renderer is CPU-bound (software RSP); expect original N64-era performance at
|
||||
60 fps rather than more. This release was playtested on real Deck hardware;
|
||||
the v0.1.0-era Facility crash (D203) did not recur: its root cause was
|
||||
@@ -182,6 +184,9 @@ items, and which ones are waiting on a decision, lives in one place:
|
||||
- The remaining small accuracy differences (audio timbre, a few cosmetic
|
||||
rendering details).
|
||||
- **macOS/ARM builds** and controller-button rebinding.
|
||||
- **Ongoing security/fidelity review**: a review pass after each release to keep
|
||||
the [security policy](.github/SECURITY.md) and the known-issues list current,
|
||||
moving the mechanical parts (dependency/secret scanning) toward automation.
|
||||
|
||||
This is spare-time work, so there's no timeline. Opt-in extras beyond the N64
|
||||
game (LAN play, bots, HD textures) come after feature-complete, if at all.
|
||||
|
||||
+87
-14
@@ -41,10 +41,68 @@ BUILD_DIR="${BUILD_DIR:-build-pc}"
|
||||
# ninja) passes env vars through intact.
|
||||
# GE_PC_BUILD_VIA_NATIVE=1 prevents a re-exec loop.
|
||||
# ---------------------------------------------------------------------------
|
||||
# Locate the MSYS2 MinGW64 toolchain dir (cmake.exe + gcc.exe, inside an
|
||||
# MSYS2 install). Whatever `cmake`/`gcc` happen to be first on PATH is not
|
||||
# trustworthy: Git Bash's /mingw64 has no toolchain, and a pip-installed
|
||||
# cmake in a Python Scripts dir can shadow the MSYS2 one. GE_MSYS2_ROOT
|
||||
# (e.g. D:\M\msys64) overrides the search.
|
||||
_ge_find_mingw_bin() {
|
||||
local c root
|
||||
root="${GE_MSYS2_ROOT:-}"
|
||||
[ -n "${root}" ] && root="$(cygpath -u "${root}" 2>/dev/null || echo "${root}")"
|
||||
for c in \
|
||||
${root:+"${root}/mingw64/bin"} \
|
||||
"$(dirname "$(command -v gcc 2>/dev/null || echo /nonexistent/gcc)")" \
|
||||
"$(dirname "$(command -v cmake 2>/dev/null || echo /nonexistent/cmake)")" \
|
||||
"/mingw64/bin" \
|
||||
"/c/msys64/mingw64/bin"; do
|
||||
if [ -f "${c}/cmake.exe" ] && [ -f "${c}/gcc.exe" ] && [ -f "${c}/../../usr/bin/msys-2.0.dll" ]; then
|
||||
(cd "${c}" && pwd)
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if [ -n "${MSYSTEM:-}" ] && [ -z "${GE_PC_BUILD_VIA_NATIVE:-}" ]; then
|
||||
_probe="$(mktemp -d "${TMPDIR:-/tmp}/gebuildprobe.XXXXXX")"
|
||||
_probe_win="$(cygpath -w "${_probe}" 2>/dev/null || echo 'C:\\msys64\\tmp')"
|
||||
/c/Windows/system32/cmd.exe //c "if exist \"%TMP%\" (echo ok > \"${_probe_win}/ok.txt\") else (echo bad > \"${_probe_win}/bad.txt\")" >/dev/null 2>&1 || true
|
||||
_mingw_bin="$(_ge_find_mingw_bin)" || {
|
||||
echo "ERROR: could not find the MSYS2 MinGW64 toolchain (mingw64/bin with cmake.exe + gcc.exe)." >&2
|
||||
echo "Install it (see docs/building.md) or point GE_MSYS2_ROOT at the MSYS2 root, e.g. GE_MSYS2_ROOT='D:\M\msys64'." >&2
|
||||
exit 1
|
||||
}
|
||||
# Failure mode 3 (AGENTS.md): the toolchain's DLL dir must be on PATH,
|
||||
# and ahead of any other cmake/gcc. Only mingw64/bin — NOT usr/bin:
|
||||
# from a Git Bash shell that would swap in MSYS2's cygpath/mktemp,
|
||||
# whose /tmp is a different Windows dir than this bash's /tmp.
|
||||
export PATH="${_mingw_bin}:${PATH}"
|
||||
# Every path that crosses to a native process lives in the build dir and
|
||||
# is converted with bash's own `pwd -W` — never /tmp + cygpath, which
|
||||
# silently disagree when two msys runtimes are on PATH (Git Bash + MSYS2:
|
||||
# the .ps1 was written to one /tmp and powershell was pointed at the
|
||||
# other; the TMP probe likewise always read "unusable").
|
||||
mkdir -p "${BUILD_DIR}"
|
||||
_bd_win="$(cd "${BUILD_DIR}" && pwd -W)"
|
||||
_bd_win="${_bd_win//\//\\}"
|
||||
_probe="${BUILD_DIR}/.ge007-tmpprobe"
|
||||
rm -rf "${_probe}"; mkdir -p "${_probe}"
|
||||
# The probe is a .cmd FILE: an inline `cmd //c "... \"path\" ..."` never
|
||||
# worked — the msys argv conversion re-escapes embedded quotes as \"
|
||||
# (which cmd does not understand), so neither ok.txt nor bad.txt was
|
||||
# ever written and every build took the re-exec. It also tests that TMP
|
||||
# is WRITABLE, not merely that it exists (the failure mode is TMP =
|
||||
# C:\Windows\, which exists). %~dp0 = the probe's own dir: no paths
|
||||
# cross the boundary. CRLF: cmd's goto/label scan is unreliable on LF.
|
||||
sed 's/$/\r/' > "${_probe}/probe.cmd" <<'CMD'
|
||||
@echo off
|
||||
if "%TMP%"=="" goto bad
|
||||
(echo x> "%TMP%\ge007-tmpprobe.tmp") 2>nul || goto bad
|
||||
del "%TMP%\ge007-tmpprobe.tmp" 2>nul
|
||||
echo ok> "%~dp0ok.txt"
|
||||
exit /b 0
|
||||
:bad
|
||||
echo bad> "%~dp0bad.txt"
|
||||
CMD
|
||||
/c/Windows/system32/cmd.exe //c "${_bd_win}\\.ge007-tmpprobe\\probe.cmd" >/dev/null 2>&1 || true
|
||||
if [ ! -s "${_probe}/ok.txt" ]; then
|
||||
command -v powershell >/dev/null 2>&1 || {
|
||||
echo "ERROR: native TMP is unusable from this shell and powershell.exe was not found for the fallback." >&2
|
||||
@@ -54,25 +112,30 @@ if [ -n "${MSYSTEM:-}" ] && [ -z "${GE_PC_BUILD_VIA_NATIVE:-}" ]; then
|
||||
}
|
||||
echo "==> native TMP unusable from this shell; re-running cmake+build via PowerShell"
|
||||
export GE_PC_BUILD_VIA_NATIVE=1
|
||||
_here="$(pwd -W 2>/dev/null || cygpath -w "$(pwd)")"
|
||||
# Resolve the Windows-side toolchain paths from what msys found (no
|
||||
# hardcoded install dir: C:\msys64 on this box, D:\M\msys64
|
||||
# elsewhere). usr/bin first: mingw gcc.exe needs msys-2.0.dll from
|
||||
# there.
|
||||
_cmake_root="$(dirname "$(command -v cmake)")" # .../mingw64/bin
|
||||
_msys_root="$(cd "${_cmake_root}/../.." && pwd -W 2>/dev/null || cygpath -w "$(cd "${_cmake_root}/../.." && pwd)")" # msys install root (.. twice: bin -> mingw64 -> root)
|
||||
_bin_w="$(cygpath -w "${_cmake_root}")"
|
||||
_usrbin_w="$(cygpath -w "${_msys_root}/usr/bin")"
|
||||
_here="$(pwd -W)"
|
||||
# Windows-side toolchain paths: from the validated MSYS2 mingw64/bin
|
||||
# found above (NOT blindly from `command -v cmake`, which in a Git
|
||||
# Bash / agent shell resolves to e.g. a pip-installed cmake and fed
|
||||
# the .ps1 a bin dir with no MSYS2 toolchain — the "re-exec fails
|
||||
# in-script, works by hand with explicit paths" bug). usr/bin first
|
||||
# on the .ps1's PATH: mingw tools may need msys-2.0.dll from there.
|
||||
_msys_root="$(cd "${_mingw_bin}/../.." && pwd -W)" # msys install root (bin -> mingw64 -> root)
|
||||
_bin_w="$(cd "${_mingw_bin}" && pwd -W)"
|
||||
_bin_w="${_bin_w//\//\\}"
|
||||
_usrbin_w="${_msys_root//\//\\}\\usr\\bin"
|
||||
# The PowerShell script is written to a .ps1 FILE, not passed via
|
||||
# -Command: the msys->native argv conversion mangles $-bearing
|
||||
# strings (a '\$LASTEXITCODE' check arrived as an empty token and a
|
||||
# failed cmake would have exited 0 silently). A file write is
|
||||
# conversion-free; only plain path/word arguments cross the boundary.
|
||||
_ps1="$(mktemp "${TMPDIR:-/tmp}/ge007-rerun.XXXXXX.ps1")"
|
||||
_ps1="${BUILD_DIR}/ge007-native-reexec.ps1"
|
||||
_ps1_win="${_bd_win}\\ge007-native-reexec.ps1"
|
||||
cat > "${_ps1}" <<'PS1'
|
||||
param([string]$UsrBin, [string]$Bin, [string]$MsysRoot, [string]$Here, [string]$BuildDir, [string]$RomId)
|
||||
# Self-logging: a headless shell loses this process's console output, so
|
||||
# every fact that matters goes to diag/log files in the build dir.
|
||||
# Resolve BuildDir against the repo root, not the inherited cwd.
|
||||
Set-Location $Here
|
||||
$diag = Join-Path $BuildDir "ge007-native-reexec-diag.log"
|
||||
$cmakeLog = Join-Path $BuildDir "ge007-native-reexec-cmake.log"
|
||||
$buildLog = Join-Path $BuildDir "ge007-native-reexec-build.log"
|
||||
@@ -81,6 +144,13 @@ $buildLog = Join-Path $BuildDir "ge007-native-reexec-build.log"
|
||||
# msys bin dirs plus the usual system dirs. (usr/bin first: mingw gcc/cmake
|
||||
# need msys-2.0.dll from there.)
|
||||
$env:PATH = "$UsrBin;$Bin;C:\Windows\System32;C:\Windows;C:\Windows\System32\Wbem"
|
||||
# A PowerShell launched from an MSYS2 bash (the agent-shell case: MSYS2's
|
||||
# usr/bin first on PATH, so `env bash` is MSYS2's bash) inherits a stripped
|
||||
# ~13-var env with PATHEXT=".CPL" and no ComSpec. With that PATHEXT,
|
||||
# `& cmake.exe` silently does NOTHING: no process, no output, $LASTEXITCODE
|
||||
# stays $null (the rc=2 "did not execute" regression). Restore them.
|
||||
$env:PATHEXT = '.COM;.EXE;.BAT;.CMD'
|
||||
if (-not $env:ComSpec) { $env:ComSpec = 'C:\Windows\System32\cmd.exe' }
|
||||
# Writable temp for the native toolchain. [System.IO.Path]::GetTempPath()
|
||||
# CANNOT be used here: it honours the inherited (broken) TMP/TEMP env vars
|
||||
# and returned C:\Windows\. Pick the first writable candidate instead.
|
||||
@@ -101,6 +171,9 @@ $env:TEMP = $tmpdir
|
||||
"start UsrBin=[$UsrBin] Bin=[$Bin] MsysRoot=[$MsysRoot] Here=[$Here] BuildDir=[$BuildDir] RomId=[$RomId] tmp=[$tmpdir]" | Out-File $diag -Encoding ascii
|
||||
$exe = "$Bin\cmake.exe"
|
||||
"exe=[$exe] exists=[$(Test-Path -LiteralPath $exe)]" | Out-File $diag -Append -Encoding ascii
|
||||
# A missing exe makes `& $exe` throw (caught below) and leaves LASTEXITCODE
|
||||
# null, which used to surface only as an opaque "did not execute".
|
||||
if (-not (Test-Path -LiteralPath $exe)) { Write-Host "cmake.exe not found at [$exe] (diag: $diag)"; exit 5 }
|
||||
Set-Location $Here
|
||||
"cwd=[$([System.IO.Directory]::GetCurrentDirectory())]" | Out-File $diag -Append -Encoding ascii
|
||||
try {
|
||||
@@ -128,7 +201,7 @@ if ($LASTEXITCODE -ne 0) { Write-Host "build failed (rc=$LASTEXITCODE; log: $bui
|
||||
"done" | Out-File $diag -Append -Encoding ascii
|
||||
Write-Host "cmake + build ok (native re-exec, TMP=$($env:TMP))"
|
||||
PS1
|
||||
_ps1_win="$(cygpath -w "${_ps1}")"
|
||||
rm -rf "${_probe}"
|
||||
rc=0
|
||||
# NOTE: do NOT launch powershell via `env -i` (tried 2026-09-28): a
|
||||
# native child started with a scrubbed msys env cannot launch
|
||||
|
||||
+63
-30
@@ -37,7 +37,7 @@ checked against an accurate emulator (1964/GEPD) or real hardware. On top
|
||||
of that sits a **modern-remaster layer** (the bar set by Nightdive's
|
||||
Quake/Turok remasters, the XBLA remaster and GE+/Dab's mod). It must never
|
||||
change game logic by default, and it must always be possible to get back
|
||||
to the original look and feel (see the *Original N64 preset* in §5).
|
||||
to the original look and feel (see the *Original N64 preset*, D440).
|
||||
|
||||
The port is **feature complete** when all of these hold:
|
||||
|
||||
@@ -63,6 +63,35 @@ verification debt cleared.
|
||||
Where things stand (2026-09-29): 1–7 hold for NTSC-U, with the small
|
||||
accuracy gaps listed in §2. 8 and 9 are open.
|
||||
|
||||
**Parity rule (decided 2026-09-30):** every platform we release for
|
||||
carries the same sign-off gate: a full 21-level sweep plus a campaign
|
||||
pass. Windows and Linux now; macOS when it joins.
|
||||
|
||||
## Order of work (decided 2026-09-30)
|
||||
|
||||
Work continues on `release/v0.4.1`. When to cut it, and what to call it
|
||||
(v0.5.0 or later), is decided as the work lands. There are two stages:
|
||||
|
||||
**Stage A: now → the pre-1.0 build**
|
||||
1. Clear the §3 verification debt.
|
||||
2. Split-screen (§1): keep working and testing until it is good.
|
||||
3. `All unlocked` rework (§5a).
|
||||
4. D357 settings values (§5a), plus the restart button.
|
||||
5. Outside PRs (§5a), in the proposed order once the maintainer approves it.
|
||||
6. Pillarbox with the exact original aspect for the Original N64 preset (§5a).
|
||||
7. Platform parity: re-base the golden baselines and extend them to both
|
||||
platforms (§7), then run the gate on each platform.
|
||||
8. Investigate and triage every §2 fidelity gap to fix or accept. Do not
|
||||
leave any of them as "open, unexamined".
|
||||
|
||||
**Stage B: pre-1.0 build → 1.0**
|
||||
1. PAL/JP (§1). It goes last on purpose, so that region-specific
|
||||
regressions are easy to isolate and unwind.
|
||||
2. macOS/ARM: merge the `macos` branch (§4), with the D441 census
|
||||
(store-to-u32 tagging) and a high-arena test.
|
||||
3. The exit gate on every shipped platform.
|
||||
4. Remove the temporary scaffolding (§7).
|
||||
|
||||
## Known issues
|
||||
|
||||
What a player will notice in the current release. **This table is the
|
||||
@@ -75,11 +104,11 @@ below).
|
||||
|---|---|---|---|
|
||||
| Multiplayer isn't available yet | Missing feature | — | #99, §1 |
|
||||
| PAL and JP ROMs aren't supported | NTSC-U only | Use an NTSC-U ROM | D258, §1 |
|
||||
| **`All unlocked` can permanently write fake unlocks into your save** | Save corruption | Back up `data/ge007.eep` before enabling it; don't use it on a save you care about | D387, §5a |
|
||||
| `All unlocked` may already have written fake unlocks into a save from an older build | Save corruption | **Fixed on `release/v0.4.1` for new saves** (D387). Back up `data/ge007.eep` before first use; fakes an older build wrote are not repaired; cheats earned while ON are not kept. Delete this row when it ships. | D387, §5a |
|
||||
| `Game.SkipIntro` also skips the post-mission failure dossier | Minor | Leave SkipIntro off | D408, §2 |
|
||||
| F10 overlay stretches in native widescreen (menus pillarbox correctly) | Cosmetic | — | D335b, §5a |
|
||||
| Rareware logo shows a subtle texture-filtering artifact | Cosmetic | — | D75, §2 |
|
||||
| Steam Deck: a first launch in Desktop Mode skips the Deck preset | Settings | First launch in Game Mode, or delete `ge007.ini` and relaunch | D283, §4 |
|
||||
| Steam Deck: a first launch in Desktop Mode skips the Deck preset | Settings | **Fixed on `release/v0.4.1`** (real-Deck check owed); delete this row when it ships. Until then: first launch in Game Mode, or delete `ge007.ini` and relaunch | D283, §4 |
|
||||
| ~30 fps on very low-end GPUs (e.g. Intel HD 400) | Performance | Set `Video.LowEndMode=1` in `ge007.ini` | D339, #92, §4 |
|
||||
| No macOS or ARM builds | Platform | — | #88, #95, #101, §4 |
|
||||
| Controller buttons can't be rebound in-game (keyboard/mouse can) | Missing feature | — | #109, §5a |
|
||||
@@ -91,7 +120,7 @@ below).
|
||||
| Item | Status | Refs | Notes |
|
||||
|---|---|---|---|
|
||||
| **2–4 player split-screen multiplayer** | parked | #99, D416–D423, branch `feat/mp-splitscreen-99` | The largest missing N64 feature. 2P has been live-tested (M&K + pad, respawn, 16:9 HUD). Before merge: Rule-2 review of `PROP__PORT_SIGNED` (D417) and a golden gate run on the maintainer's console. Open on the branch: D420 (stats screen shows a large number), D421 (gadget cycling off; needs a Rule-2 game-thread hook), D422 (no centred aim for P2+), D423 (black arrowhead in P2 view), and D428's sway accumulator is player-0-only. Untested: 3P/4P, pads-only mode, pause/watch/F10 during a match, MP audio, most stages, and the full N64 MP setup screens (scenarios, handicaps, teams). Needs per-pad seats (§5). An outside restore exists for reference: birdturtle `feature/local-mp-input`. |
|
||||
| **PAL and JP ROM support** | decision | D258, D75 (pal/jpn sidecar regen), #85 | Conversion is broken at the source-data level (filelist naming). A repair path has been verified but not applied. Needs PAL/JP ROMs to test. #85 was closed as "retired for now", not won't-do, so public users currently have no tracker. Reopen it or file a fresh one when work starts. |
|
||||
| **PAL and JP ROM support** | open | D258, D75 (pal/jpn sidecar regen), #85 | **Scheduled for Stage B (decided 2026-09-30):** done just before 1.0 so that region-specific issues can be unwound in isolation. Conversion is broken at the source-data level (filelist naming). A repair path has been verified but not applied. Needs PAL/JP ROMs to test. #85 was closed as "retired for now", not won't-do, so public users currently have no tracker. Reopen it or file a fresh one when work starts. |
|
||||
| Drop-in ROM converter, remaining parts | decision | #6, [`dev/release-dropin-rom-plan.md`](dev/release-dropin-rom-plan.md) | NTSC-U drop-in shipped. Left: per-region output names and re-exec guard (with PAL/JP), native C emitters to replace the PyInstaller converter. |
|
||||
| Cut content (TCRF weapons/items) | decision | [`dev/CUT-CONTENT-BACKLOG.md`](dev/CUT-CONTENT-BACKLOG.md) | Not part of the shipped game. Tier 0 already works via the debug/cheat menus. Tier 1 (config-toggle grants) is small and port-only. Tier 2/3 (placing props, re-enabling unused MP maps) need game data changes and are out of scope for this repo. |
|
||||
|
||||
@@ -109,7 +138,7 @@ below).
|
||||
| Front-end: Rareware logo filtering, cast-roll models | partial | D75 | Cosmetic. The other front-end logos are fixed. The cast-roll character models have never been verified. |
|
||||
| Authored mip chains not sampled for explicit-LOD textures | parked | D323 | Distant textures use driver mips instead of Rare's hand-authored chain (heaviest on Statue/Aztec). Parked as a fidelity enhancement. |
|
||||
| Approximations in the software RSP | open | `gfx_pc.cpp` `G_CCMUX_LOD_FRACTION`; `Video.WrapFix` (§F row `RC3 · D167`, D74 family) | The LOD-fraction CC input is an eyeballed approximation. The non-power-of-two wrap fix is still opt-in (default off), with unknown visible impact. Both need a comparison against 1964. |
|
||||
| Latent 64-bit ABI hazards | open | #107, #108; ~396 `-Wpointer-to-int-cast` sites (count from a local review note; re-count with a `-Wpointer-to-int-cast` build); AUDIT-M6 `struct player`/`struct hand` raw offsets (porting-notes §A1) | Harmless today, because the arena sits low in the address space. They would bite on macOS/ARM/ASLR. The fixes fall under the AGENTS.md ABI/layout exception. **#108 concern:** widening the `sndPlaySfx` bound disables the guard on every platform; prefer a range check. Check #107 against D430/D431/D434, which touch the same `lightFindVertexBaseForTri` region. A read-only provenance pass over the 396 sites is a good delegate task. |
|
||||
| Latent 64-bit ABI hazards | partial | D441, #107, #108; ~396 `-Wpointer-to-int-cast` sites at the start of the sweep (185 remain in the 17 touched files; many are intentional `(s32)&ANIM_DATA_*` offsets, D34); AUDIT-M6 `struct player`/`struct hand` raw offsets (porting-notes §A1) | Part 1 landed (D441): 17 files widened to `uintptr_t` under `#ifdef PORT`, build and Bunker/Silo runs verified. **Still owed:** the full census with per-site provenance (H/R/I/?, table not yet written) plus a **store-to-u32 vs arithmetic-only** tag per site (a `uintptr_t` widening is enough only for arithmetic; sites that store back into a u32 field need `portHostToN64` under the macOS port_addr model), an env-gated high-arena test mode, and a check under an arena above 4 GB. Harmless today because the arena sits low. They would bite on macOS/ARM/ASLR. **#108 concern:** widening the `sndPlaySfx` bound disables the guard on every platform; prefer a range check. **#107 will now conflict** with D441 in `bg.c` (`bgRoomCalcBB`), so rebase or close it during the outside-PR review. A read-only provenance pass over the remaining sites is a good local-Qwen delegate task. |
|
||||
| Controller preset parity (Xbox "Jinx" 1.1, 1.2/1.3 selector) | partial | D394 | The gadget category and pad-initiated binding capture are still open. |
|
||||
|
||||
## 3. Verification debt (fixed; a live check is owed)
|
||||
@@ -118,6 +147,9 @@ A single campaign pass with these on the checklist would clear most of them.
|
||||
|
||||
| Item | Refs | Check |
|
||||
|---|---|---|
|
||||
| Presets: Original N64 / Port defaults | D440 | Press both rows in F10 and in the front-end PC Options with a mouse and a pad; draw distance applies from the next level load. `GE_OPTIONTREEPROBE` already fails on the base commit (separate look). |
|
||||
| Probe strip (2026-09-30) | D245, D302, D318 | Frigate or Dam water/sky A/B against the previous build (the s16 sky path was removed); one Facility playthrough to confirm the D318 watchdog stays silent. |
|
||||
| Infra fixes | D179, build-pc.sh, crash.c, CI | The 2026-09-30 re-exec no-op (PATHEXT=.CPL in the inherited env) is fixed and verified from the agent shell (see AGENTS.md). `./build-pc.sh` from a genuine MSYS2 login shell; the next CI run (pinned actions, workflow input check). `GE_CRASHTEST=6` shows resolved frames. The romdata `VirtualAlloc` failure path now fails boot instead of falling back to the heap copy: confirm you want that. |
|
||||
| Light-fixture hit-type reads (impact sound + sparks) | D434, #119 | Shoot fixtures on Bunker and Caverns. |
|
||||
| v0.4.1 batch (unreleased; fixes live on `release/v0.4.1`) | D424–D432; #114 fire rate, #115 tank-crush audio loop, #116 tank movement, #117 sway, #118 GL recoil, #119 light fixtures | All user-verified against GEPD/1964 on 2026-09-29. Close the issues when it ships. |
|
||||
| FOV-scale edge culling | D222 | Max FOV, pan across NPCs at the screen edge. |
|
||||
@@ -132,10 +164,10 @@ A single campaign pass with these on the checklist would clear most of them.
|
||||
|
||||
| Item | Status | Refs | Notes |
|
||||
|---|---|---|---|
|
||||
| macOS (Intel + Apple Silicon) / ARM64 Linux | open | PRs #88, #95; #101 | Outside drafts. The maintainer tests firsthand before merging. Needs the ABI-hazard sweep in §2. |
|
||||
| Linux as a co-equal platform | decision | — | Does Linux carry the same sign-off gate as Windows (a full sweep of all 21 levels)? The last known Linux crash (D190) was closed as not-a-bug. |
|
||||
| macOS (Intel + Apple Silicon) / ARM64 Linux | open | PRs #88, #95; #101 | Outside drafts, parked for `main` until after 1.0. **Address model decided (2026-09-30): port_addr (#95)**, i.e. a single `portN64ToHost`/`PORT_N64PTR` chokepoint with a `PORT_ADDR_STRICT` validator, over the dram-scoped alternative. The `macos` integration branch **exists on origin** (2026-09-30). It was cut from `origin/main` `0e8c2ce2`, and it takes #88 (merged as `c4153157`, using #95's conflict-resolved copy `ae1884cc`), #132 (Linux build fix, `39d4f8d0`: #88's `romdata.c` included `<errno.h>` before `ultra64.h`, and glibc's `errno` macro broke the `errno` fields in `PR/os.h`; #95 needs it on rebase) and #95 (retargeted to it, still open). italoarruda's work also goes there. Nothing merges from it to `main` before the post-1.0 pass. **Open against `macos`:** #131, the fork-friendly self-hosted sweep. It adds `platform` and `runner_labels` inputs, a macOS job running `level_sweep_mac.sh` with a JSON verdict, seed-dir-only ROM staging, and `docs/selfhosted-sweep.md`. It is a scaffold that has not been run on a Mac, and it needs #95 in `macos` first. **When `macos` is next rebased onto `main`** (after the v0.4.1 push), `selfhosted.yml` will conflict with the v0.4.1 ROM-cache fix. Take #131's version, which already includes that fix. The crouched-rifle table fix (`chr.c`), found three times (#95, italoarruda, #120), landed on `release/v0.4.1` via #120 (D445) on 2026-09-30. #120's shape is the correct one (it restores the N64 table length of 2; #95's terminator-only version leaves length 1), so **drop #95's `crouched_rifle` hunk** when `macos` is rebased. #95's finding labels (D29x, then D405–D421) clash with ours, so relabel on merge. The maintainer tests firsthand before merging. Needs the ABI-hazard sweep in §2 (store-to-u32 tagging). |
|
||||
| Platform parity gate (Linux co-equal with Windows) | open | — | **Decided 2026-09-30:** yes. Every released platform carries the same gate: a 21-level sweep plus a campaign pass. Needs the golden baselines on both platforms first (§7). The last known Linux crash (D190) was closed as not-a-bug. |
|
||||
| Low-end GPUs (~30 fps on Celeron/HD 400) | open | D339, #92 | Needs `GE_PERFSTAT` data from that class of hardware. The D372 low-end preset is the mitigation. |
|
||||
| Steam Deck preset skipped when first launched from Desktop Mode | open | D283 | Workaround is documented in the README. Make the fix not depend on the env var. |
|
||||
| Steam Deck preset skipped when first launched from Desktop Mode | verify | D283 | Fixed on `release/v0.4.1`: the Deck is detected by DMI hardware id (Valve Jupiter/Galileo; `STEAMOS` kept as a second signal) and the preset applies once per ini, only while Fullscreen/Maximized are untouched. Only the Windows build and a Linux `-fsyntax-only` check ran. **Owed: one run on a real Deck** (Desktop-Mode-created ini, then Game Mode; expect fullscreen 1280x800 and the log line `video: Steam Deck (DMI Valve ...)`). The Known-issues row above goes when this ships. |
|
||||
| Windows 7 | decision | #98 | Recommend won't-fix (toolchain). |
|
||||
|
||||
## 5. Modern-remaster layer (port features beyond the N64)
|
||||
@@ -147,7 +179,7 @@ ultrawide, FOV, MSAA/anisotropic/texture-filter options (including N64
|
||||
keyboard/mouse rebinding with a GEPD-style layout, pad deadzone,
|
||||
sensitivity, smoothing, southpaw and triggers, rumble, crosshair and HUD
|
||||
customisation, HUD scale, music/FX volume, no-hit-flash, SkipIntro
|
||||
(experimental), All unlocked (unsafe, see D387), and the F10 overlay.
|
||||
(experimental), All unlocked (experimental, see D387), and the F10 overlay.
|
||||
|
||||
The modern defaults (native widescreen, overscan crop, 250% draw/LOD
|
||||
distance, bilinear filtering) are **deliberate**. They are not a fidelity
|
||||
@@ -161,16 +193,19 @@ Background research lives in the maintainer's local notes (gitignored):
|
||||
|
||||
| Item | Status | Refs | Notes |
|
||||
|---|---|---|---|
|
||||
| **"Original N64" preset toggle** | open | — | One switch that snaps every value the port has changed back to N64 values: draw/LOD distance, culling, texture filter (3-point), 4:3, no overscan crop, stock FOV, N64 aim. It's cheap, because each is already a config key. This is the port's answer to the XBLA/Nightdive "original vs enhanced" switch. |
|
||||
| `All unlocked` writes fake unlocks into the real save | open | D387 | **The only data-integrity bug.** Fix it or gate the option harder. Until then, the README warns users to back up the EEPROM. |
|
||||
| Settings units and values cleanup | decision | D357 ([plan](dev/D357-SETTINGS-VALUES-PLAN.md)) | Awaiting sign-off. Its "Now" column is partly stale: the deadzone is already split L/R, and draw/LOD defaults are 250, not 150. Covers FOV in degrees, sensitivity shown as ×, MSAA 16×, filter naming, volume step and the FPS preset grid. |
|
||||
| "Original N64" preset: pillarbox at the original aspect | verify | D440, D447 | Landed 2026-09-30 (D447): `Video.AspectMode` (Window/Original), set by the Original N64 preset. Exact 4:3, or 16:9 while the game's watch Ratio is 16:9. Letterboxes in taller windows. `Crop overscan` no longer eats the game's Wide/Cinema letterbox. Measured: 160 px bars at 1280x720. Window-mode frame diff is within the run-to-run noise floor (dmean 0.93 vs base-vs-base 0.85). **Owed:** compare 4:3 and 16:9 against 1964; Wide/Cinema with crop on; menu mouse alignment in Original mode; MSAA 4/8/16, fullscreen toggle, Linux build. F10 full-window is deferred to an F10 polish pass. Known: with crop off, the D246 one-unit edge line shows at the rect edges. |
|
||||
| `All unlocked` rework: RAM override instead of save patching | open | D387, D259 | **Approved 2026-09-30.** Levels are already RAM-only (the game's `debug_enable_all_levels_flag`/`debug_007_unlock_flag`, `port/src/main.c`). Replace the cheat half, a block-4 EEPROM read patch plus the D387 write merge in `port/src/libultra.c`, with one opt-in `#ifdef PORT` hook at the cheat-unlocked check (`src/game/file2.c` `fileGetIsCheatUnlocked`, and confirm every caller including `front.c` `frontCheckIfCheatIsUnlocked`). The save is then never touched: earned cheats and times persist normally, OFF restores real progress, and the D259 silent-volume quirk goes away. This is a `src/game` edit for an opt-in port feature, so document it as such (maintainer-approved). Then remove the D387 merge machinery. Already-persisted fakes from older builds stay unrepaired. |
|
||||
| Settings units and values cleanup | open | D357 ([plan](dev/D357-SETTINGS-VALUES-PLAN.md)) | **Approved 2026-09-30 with changes** (recorded in the plan's §0): FOV in degrees; FPS cap presets 30/60/90/120/144/240/Uncapped **plus** a free custom value; MSAA adds 16×; mouse invert stays a separate toggle; the texture filter keeps "N64 3-point" (the plan's rename rests on a wrong premise). Refresh the plan's stale "Now" column first (the deadzone is split L/R; draw/LOD defaults are 250). |
|
||||
| Restart button in the settings menus | open | — | QoL (2026-09-30): next to Quit to desktop, a "Restart game" action so that restart-required changes (MSAA, etc.) apply in one click. Relaunch the same exe with the same args after an orderly quit (the D344 path, since hard exits have BSOD'd). Offer it wherever a changed row is flagged "restart". |
|
||||
| Front-end PC Options: Previous/Next paging for mouse users | open | — | QoL (2026-09-30): add clickable Previous/Next (page) controls to the main-menu settings screen, so mouse users don't have to rely on wheel scrolling. Today the wheel scroll competes with mouse hover: any small mouse nudge re-selects a row and snaps the scroll back. Fix that interaction too. Hover must not reset the scroll position, and it should only change the selection once the pointer moves onto a different row. Owner: the D357/D443 track (same file, `port/src/frontoptions.c`). |
|
||||
| Outside PRs #107–#109, #122–#124 | open | #122 hot-plugged pads never reopen, #123 persistent crosshair (`gunfire.c`), #124 compact health/armour bars (`bondview2.c`); #107/#108/#109 (italoarruda) | **#120 and #121 merged locally into `release/v0.4.1` on 2026-09-30** (dolent's original commits kept for credit; relabelled D445/D446; the review is in `docs/dev/notes/PR-REVIEW-120-121.md`). Close both on GitHub when the branch is pushed. **Owed live:** kneeling rifle guards (#120), and the 2P watch-menu text (#121, on the MP branch). **Remaining order (proposed; approval owed):** #122 (then rebase split-screen on it; #122/#109/MP all touch `input.c`) → #107 (fold remaining sites into the D441 census, then close) → #108 (ask for a range check; belongs with `macos`) → #109 (after split-screen settles `input.c`) → #123/#124. **Policy:** any PR that adds a *feature* (#109, #123, #124, …) gets a **design review before any work**: compare against other PC ports and remasters (PD port, Nightdive, GE+/XBLA), then approve, or send suggestions back and defer until the submitter revises. **Their finding labels clash with ours:** relabel on merge. `src/game` touches (#120/#123/#124) each need a Rule-2 or ABI-exception check. |
|
||||
| Gamepad button rebinding UI | open | PR #109, D394 | Keyboard/mouse rebinding has shipped. Review #109 under the outside-PR policy. |
|
||||
| Per-pad tuning (device index, hot-plug seats) | open | — | Needed for split-screen anyway. |
|
||||
| Full-gamepad menu navigation audit, Xbox/PS button glyphs | open | — | Menus are only partly audited for pad-only use. There are no glyphs yet. |
|
||||
| Mute and auto-pause on focus loss; PNG screenshots to `screenshots/` | parked | D231, PR #56 (closed unmerged), `docs/dev/notes/parked/0001-QoL-*.patch` (local) | Deprioritised by the maintainer on 2026-09-25. F12 today is the dev PPM dump. |
|
||||
| F10 overlay polish | open | D335b, #90 | Pillarbox the overlay in widescreen; colour experimental rows red; warn on unknown `ge007.ini` keys (promised in #90; today they are only logged). |
|
||||
| Master volume, audio output device select | open | — | |
|
||||
| Frame rate above 60 (120/144/uncapped with interpolation) | decision | [`dev/UNLOCKED-FPS-PLAN.md`](dev/UNLOCKED-FPS-PLAN.md) | The menu cap is {30, 60} (the ini accepts 0 = uncapped, but the sim ticks at the VI rate). The plan's premise predates the D248 real-60 fix, so re-measure first. Must stay compatible with netplay determinism. |
|
||||
| Frame rate above 60 (120/144/uncapped with interpolation) | decision | [`dev/UNLOCKED-FPS-PLAN.md`](dev/UNLOCKED-FPS-PLAN.md) | The menu cap is {30, 60} (the ini accepts 0 = uncapped, but the sim ticks at the VI rate). The plan's Phase 0 is resolved by D248 (the default was a port bug; it is a real 60 fps) and its Phase 1 audit and Phase 2 slice scope are written (landed 2026-09-30), but the header still reads parked. Next step is a standing sim/render-rate measurement, not the old 30 fps investigation. Must stay compatible with netplay determinism. |
|
||||
|
||||
### 5b. GE+ / Dab's-mod gameplay options (opt-in; touch game code)
|
||||
|
||||
@@ -218,17 +253,14 @@ and a stage loader.
|
||||
|
||||
| Item | Status | Refs | Notes |
|
||||
|---|---|---|---|
|
||||
| Security review follow-ups | open | maintainer's local security review | Four open hardening items: one Medium (self-hosted CI workflow input handling), the rest Low (local-input only). Details are kept out of this public file. Also: pin Actions by SHA, pin `pyinstaller`, take the Dependabot action bumps (#1/#2). |
|
||||
| Security review follow-ups | open | maintainer's local security review | Three Low hardening items remain (local-input only; details are kept out of this public file). Done on `release/v0.4.1`: the Medium item (self-hosted workflow input handling), actions pinned by SHA, `pyinstaller` pinned to 6.22.3, and the ROM no longer stored in `actions/cache` by `selfhosted.yml` (fork PR workflows can restore base-branch caches; no cache entry ever existed, checked 2026-09-30; it is now staged from the runner's `_rom-seed` dir only). The Dependabot action bumps (#1/#2) would now need to land as SHA pins, not tag bumps. |
|
||||
| Deterministic 1964-vs-port comparison harness | open | — | Would replace by-eye/by-ear fidelity checks. Highest-leverage tooling item. |
|
||||
| Scripted-playthrough harness | open | `GE_INPUTSCRIPT` | Input script + `GE_PCDUMP` + crash-log check, for unattended regression runs. The tool exists; the harness doesn't. |
|
||||
| Golden baselines | open | `tools_pc/verify.sh`, D117 | bunker1 has been noisy since M-84, so the per-patch gate is unreliable. Re-base it, extend to all 21 levels × both platforms, and finish `GE_DETERM`. |
|
||||
| Self-hosted CI runner link failure | open | D244 | Needs a `pacman -Syu` on the runner (maintainer-only access). Release tag builds use GitHub runners, so it doesn't block a release. |
|
||||
| `build-pc.sh` TMP self-heal fails in-script | open | AGENTS.md | A standalone `.ps1` workaround is documented. |
|
||||
| Remove temporary scaffolding before a release | open | [`dev/GE-ENV-PROBES.md`](dev/GE-ENV-PROBES.md), D302 | Probes: D207S, D157, D252POOL, D245V, D236 (RAW/RM/ORDER/ALPHA/ZFIX/BT), D288, D309, D318B/D320 repro, `GE_OBJT`. Tools: `tools_pc/scan_op12.py`, `scan_op13.py`. Also demote the D318 watchdog to detect-and-log (D329 removed the cause), remove the M-183/M-185 clamps after a full cutscene re-check, and grep hot paths for `getenv` (D302 repeat offender). |
|
||||
| `crash.c` backtraces | open | — | The EBP walk dies at frame 1. Switch to `CaptureStackBackTrace`/`StackWalk64`. |
|
||||
| `romdata.c` VirtualAlloc fallback hardening | open | D179 tail | Minor. |
|
||||
| Findings log and doc size | open | — | `findings.md` is about 2 MB and growing. It needs the deferred porting-notes TOC and docs-budget script (context cost for every session). |
|
||||
| Branch and PR housekeeping | open | — | Push local `main` (5cccbcef, post-v0.4.0 bookkeeping). Prune the ~20 squash-merged branches: keep `feat/mp-splitscreen-99`, `land/modloader-*` and `research/cut-content-backlog`, and first confirm the single unique commits on `feat/unlocked-fps-phase1-audit` and `docs/m138-security-disclosure` landed. PR #62 (mouse dt-decouple) stays held until mouse feel is fundamentally better. |
|
||||
| Remove temporary scaffolding before a release | partial | [`dev/GE-ENV-PROBES.md`](dev/GE-ENV-PROBES.md), D302 | The 2026-09-30 pass removed D207S, D157, D252POOL, D245V, D288, D309, the D318B suite, the D320 repro harness and `scan_op12/13.py`, demoted the D318 watchdog to detect-and-log, and cached the per-frame `getenv` sites. **Left:** dead registry rows (D116, D51, D56, D60-D63, D69*, D71LOG, D85DUMP, D86-D88, D90, D96, D104, D154, D178); closed-finding TEMPs (`GE_D306C`, `GE_DYNTEXHASH_OFF`, `GE_FORCEALARM`, `GE_D204_OLD`, `GE_D252`, `GE_D243X2/X4`, `GE_D318T`, `GE_RSEED_LV`, `GE_D235_*` once D235 closes); the now-inert sky tile-capture/shift machinery in `sky.c` (`skyPortCaptureTile`, `skyPortPickShift`, `GE_D245_FIXEDSHIFT`); and the M-183/M-185 clamps, which need a full cutscene re-check first. |
|
||||
| Findings log and doc size | partial | `tools_pc/docs_budget.py` | The budget checker exists (per-file and tier-1 token budgets; `--toc FILE`), and everything is within budget. `findings.md` is still about 2 MB (~500k tokens), so agents must keep using the index. Remaining: nothing blocks; trim only if the budget check starts failing. |
|
||||
| Branch and PR housekeeping | open | — | 30 landed local branches pruned 2026-09-30 (tips kept in the notes archive, `prune-branches-2026-09-30.txt`). `docs/m138-security-disclosure` landed on `release/v0.4.1` with a stale-fact pass (SECURITY.md, the security-and-fidelity page, README roadmap bullet). `feat/unlocked-fps-phase1-audit`'s commit (Phase 0 resolved by D248, Phase 1 audit) landed as `docs/unlocked-fps-audit`. **On hold by the maintainer (2026-09-30, do not prune yet):** probe-only leftovers `d243-verify-local`, `investigate/d243-cutscene-race`, `park/d236-probes-m139`. Local `main` and `release/v0.4.1` are deliberately not pushed. **Pushed on 2026-09-30:** the `macos` integration branch; `ci/selfhosted-fork-runners` (PR #131 into `macos`, open); `fix/macos-errno-include` (PR #132, merged into `macos`). Pruned after merging, 2026-09-30: `fix/selfhosted-rom-no-cache`, `docs/macos-ci-tracking` and `fix/macos-errno-include` (local and remote), plus the `review/pr88` and `review/pr95` read-only refs. PR #62 (mouse dt-decouple) stays held until mouse feel is fundamentally better. |
|
||||
| Docs site: Web 1.0 phase 2 | decision | — | Phase 1 shipped. Phase 2 (centred serif page) is plan-only. |
|
||||
|
||||
## Community forks (not ours; reference only)
|
||||
@@ -242,16 +274,17 @@ and a stage loader.
|
||||
|
||||
## Decisions owed (maintainer)
|
||||
|
||||
1. **PAL/JP:** pursue it (needs those ROMs; also unblocks drop-in Part B), or stay NTSC-only?
|
||||
2. **Split-screen:** approve the D417 Rule-2 change, then merge `feat/mp-splitscreen-99`.
|
||||
3. **D387:** fix `All unlocked`'s save leak, or restrict the option?
|
||||
4. **D357:** sign off the settings units/values plan (after refreshing its stale table).
|
||||
5. **GE+ gameplay options (§5b)** and a port-native third-person camera: in or out?
|
||||
6. **Enhanced visuals (§5c):** build our own, adopt parts of thepont's stack, or skip?
|
||||
7. **Beyond-N64 multiplayer (§6):** netplay, bots, co-op, cheats. In scope after feature-complete, or not?
|
||||
8. **Linux co-equal status** and **release cadence / tag policy**.
|
||||
9. **Outside PRs:** #107 and #108 (with the guard concern), #109, #88/#95. Review order.
|
||||
10. **D433 gunshot timbre, D323 authored mips:** worth chasing, or accept?
|
||||
Decided 2026-09-30 (moved into the rows above): PAL/JP goes in Stage B;
|
||||
`All unlocked` becomes a RAM override; D357 approved with changes;
|
||||
platform parity gate; feature PRs get a design review first; the
|
||||
#88/#95 address model is port_addr. Still owed:
|
||||
|
||||
1. **Split-screen:** approve the D417 Rule-2 change (`PROP__PORT_SIGNED`) when the branch is ready to merge.
|
||||
2. **Outside PR review order:** final approval of the order proposed in §5a.
|
||||
3. **GE+ gameplay options (§5b)** and a port-native third-person camera: in or out?
|
||||
4. **Enhanced visuals (§5c):** build our own, adopt parts of thepont's stack, or skip?
|
||||
5. **Beyond-N64 multiplayer (§6):** netplay, bots, co-op, cheats. In scope after feature-complete, or not?
|
||||
6. **D433 gunshot timbre, D323 authored mips:** decided per gap during the Stage A fidelity triage.
|
||||
|
||||
## Not planned
|
||||
|
||||
|
||||
@@ -93,6 +93,14 @@ cmake -S . -B build-pc -DROMID=ntsc-final
|
||||
cmake --build build-pc -j
|
||||
```
|
||||
|
||||
On Windows, `build-pc.sh` also locates the MSYS2 MinGW64 toolchain itself
|
||||
(so it works from Git Bash or a non-login shell too, even with another
|
||||
`cmake` earlier on PATH) and, if native child processes would get an
|
||||
unwritable `TMP`, re-runs the configure+build under PowerShell with a
|
||||
writable one. If MSYS2 is not installed at `C:\msys64`, set
|
||||
`GE_MSYS2_ROOT` (e.g. `GE_MSYS2_ROOT='D:\M\msys64'`). Re-exec logs:
|
||||
`build-pc/ge007-native-reexec-*.log`.
|
||||
|
||||
For PAL/JP you must first generate that region's ROM-asset symbol file
|
||||
(the US one is committed):
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# D357 — Settings thresholds & values: align display units/defaults with the PD port + Turok PC
|
||||
|
||||
Status: **PROPOSAL — awaiting sign-off** (2026-09-27). Baselines: the PD port
|
||||
Status: **IMPLEMENTED 2026-09-30 (findings D443) for mouse sens ×, deadzone whole-%, volume step, FOV degrees, MSAA 16×, Restart game; texture filter unchanged by decision; frame-rate cap deferred (§0 presets + custom = separate project). Approved with changes; see §0.** Proposal dated 2026-09-27. Baselines: the PD port
|
||||
checkout (`C:\Users\james\Source\Repos\pd_port`, `port/src/optionsmenu.c` +
|
||||
`input.c`/`video.c` config registrations), the Turok PC (Nightdive) list from
|
||||
the maintainer's notes (`docs/dev/notes/OPTIONS-MENU-PLAN.md` §6 — *taken from
|
||||
@@ -14,6 +14,23 @@ front top row reads **Profile**, the per-file row tag reads
|
||||
**"(per profile)"** and the F10 unavailable value reads **"Select a
|
||||
profile"** (§4).
|
||||
|
||||
## 0. Sign-off (2026-09-30) — these override §3 where they differ
|
||||
|
||||
- **FOV:** display in **degrees** (the PC standard). Storage stays unchanged.
|
||||
- **Frame rate cap:** presets **30 / 60 / 90 / 120 / 144 / 240 / Uncapped**,
|
||||
**plus** an arbitrary custom value. This keeps the ini's free range and
|
||||
adds a custom entry in the menu, instead of the plan's presets-only grid.
|
||||
- **MSAA:** add **16×**.
|
||||
- **Mouse invert:** stays a separate On/Off toggle.
|
||||
- **Texture filter: do NOT rename.** The §3 premise ("3-point *is*
|
||||
trilinear") is wrong. 3-point is the N64's own 3-sample bilinear
|
||||
approximation, and "Linear" and "Bilinear" name the same thing. Keep
|
||||
"N64 3-point" and check the current enum strings before touching them.
|
||||
- **Refresh §3's "Now" column before implementing.** The stick deadzone is
|
||||
already split L/R, and the draw/LOD defaults are 250, not 150.
|
||||
- Related QoL, tracked separately in ROADMAP §5a: a **Restart game** action
|
||||
next to Quit to desktop, for restart-flagged rows such as MSAA.
|
||||
|
||||
## 1. Problem
|
||||
|
||||
The D356 regroup fixed the *layout*; the *values* still mix three unit
|
||||
@@ -65,6 +82,23 @@ volume trio (master still M3-backlog).
|
||||
Current state from `optionsoverlay.c` `rows[]` + the `configRegister*`
|
||||
ranges (2026-09-27). "Display" = what the value column shows today.
|
||||
|
||||
### Implemented 2026-09-30 (D443) -- refreshed "Now" vs. result
|
||||
|
||||
The per-row tables below keep the 2026-09-27 audit text for history. Actual
|
||||
pre-change state and what shipped (all port-only; no default or storage change,
|
||||
so `kResetDefaults` is untouched and `GE_WSPROBE_RESET` stays failures=0):
|
||||
|
||||
| Row | Actual "Now" (pre-D443) | Shipped |
|
||||
|---|---|---|
|
||||
| Mouse horizontal sensitivity | 1-500 · step 5 · shown as calibrated `N/100` (50/100 at raw 100) | UI range 10-300, step 10, shown `%.1fx` (raw/100), linear bar; left the calibrated mapping |
|
||||
| Stick deadzone L / R | already split `Input.PadDeadzoneL/R` 0-30000 · step 500 · calibrated `N/100` | step 300 + `dispDiv=300` `%` (whole-% grid, no key migration); left the calibrated mapping |
|
||||
| Music / FX volume | step 128, `/328` | step 328 (1 display-%) |
|
||||
| FOV | `Video.FovScale` 50-150 · step 5 · `%` | row "Field of view", horizontal degrees (see D443 formula); key/storage unchanged, step walks the displayed degree |
|
||||
| Anti-aliasing | 1/2/4/8 · default 2 (C initialiser) | + 16x; range 1-16 (GL clamps to `GL_MAX_SAMPLES`) |
|
||||
| Texture filter | Nearest / Bilinear / 3-Point | unchanged (decision §0) |
|
||||
| Frame-rate cap | 30/60 toggle (sim ticks at 60 Hz) | unchanged this round |
|
||||
| Restart game | -- | new action row after Quit to desktop (both UIs, shared table) |
|
||||
|
||||
### INPUT
|
||||
|
||||
| Row | Now (default · range · step · display) | PD port | Turok (notes) | Proposal |
|
||||
|
||||
+15
-12
@@ -3,7 +3,7 @@
|
||||
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-28 (v0.4.0 release sweep: rows added for probes introduced since 09-21 — D383 bind-capture smoke, D388 option-tree walk, D406 front-options diagnostics, D354/D344 watch-settings & quit-frame harnesses, D318B boundary suite, z-fight grid; the drift checker now also sees the cached env-flag wrapper reads from `port/include/envflag.h`). All are **env-gated**: unset = zero behavior
|
||||
File:line cells. Last reconciled 2026-09-30 (ROADMAP §7 strip pass: D157/D157T/D157I/D157TB, D207S, D227V, D245_OLDVTX/OLDTEX, D252POOL, D288, D309, D318B suite, D320R removed and marked as tombstones; D318W demoted to detect-only). Previously 2026-09-28 (v0.4.0 release sweep: rows added for probes introduced since 09-21 — D383 bind-capture smoke, D388 option-tree walk, D406 front-options diagnostics, D354/D344 watch-settings & quit-frame harnesses, D318B boundary suite, z-fight grid; the drift checker now also sees the cached env-flag wrapper reads from `port/include/envflag.h`). 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
|
||||
@@ -26,6 +26,7 @@ Two classes:
|
||||
| `GE_UNLOCK_ALL` | `src/game/debugmenu_handler.c:1098` (`get_debug_enable_all_levels_flag`, `#ifdef PORT`) | Mission-select shows every solo level (playtest jump-to-any-level aid). Cached in a `static int c`. | **live** |
|
||||
| `GE_DEBUG_UNLOCKALL` | `src/game/file2.c:404` (`#ifdef PORT`) | Forces the "all levels unlocked" flag TRUE regardless of the shipped save — same effect as `GE_UNLOCK_ALL` but at the file2 unlock-query site. Cached in a `static int`. | **live** (playtest aid) |
|
||||
| `GE_DEBUGMENU` | `src/boss.c:566` (`#ifdef PORT`) | Makes the L+R debug menu respond to live controller input (C-up+C-down open/close, stick + buttons read from pad 0) instead of the `#ifndef DEBUGMENU` path; unset reproduces that path exactly. Cached in a `static int`. | **live** (playtest aid) |
|
||||
| `GE_FAKE_DECK` | `port/src/video.c:490` (`videoDetectSteamDeck`) | D283: `=1` forces the Steam Deck path on any host (Windows/Linux desktop) so the once-per-ini Deck preset logic (`Video.DeckPresetApplied`, applied only while `Video.Fullscreen=0` and `Window.Maximized=0`) can be tested; `=0` forces it off on a real Deck (skips DMI + `STEAMOS`). Unset = real detection (DMI `Valve` + `Jupiter`/`Galileo`, then `STEAMOS`). **Writes the ini** (preset + flag) — use a throwaway data dir. | **live** (test hook) |
|
||||
| `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) |
|
||||
@@ -58,19 +59,19 @@ Two classes:
|
||||
| `GE_D193B` | `src/game/model.c` (`sub_GAME_7F06D3F4`, `#ifdef PORT`) | Rate-limited (1/97) raw anim root-motion decode trace (D193): `joint=` / `flip=` / `base=` mtx channel index / `frame=` / `tmp=` decoded s16 translation triple from the anim bitstream / `angle=` / `anim` ptr / `bd=`+`bs=` the D32 u32 bitDescriptors/bitStream addresses / `njoints`. M-80 Facility: `base=0`, `angle=0` **always** (root-motion facing delta never non-zero — top lead), `tmp` ≈ (±small, ~1086 hip-height, ±small). Compare `tmp`/`angle` magnitudes against a clean N64 decomp build of the same anim+frame. | **live** (D193) |
|
||||
| `GE_D250` | `src/boss.c` (`bossMainloop`, wraps `lvlRender()`, `#ifdef PORT`) | Per-wall-second line: `[D250] lvlRender avg=Nus n=M` — average + count of `lvlRender()` (DL-building: culling/animation/scene traversal) calls that wall-second, isolated from `gfx_run()`'s software-RSP interpretation cost (already covered by the existing "frame rendered in Nus" line). Added M-120 to chase D250 (fps stuck ~45-50 on Dam, ruled resolution/MSAA/vsync/build-flags out already) — found `lvlRender` itself is cheap (~0.5-1.5ms avg), so it is NOT the missing ~10-15ms/frame either; the still-unexplained cost must be elsewhere in the per-retrace-message loop (scheduler/thread round-trip, audio, or something not yet isolated by a targeted timer). Cheap; safe for a full playtest. | **live** (D250) |
|
||||
| `GE_D172` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1` — SETCOMBINE dedup log + non-LOD multitex tri dump; `#ifdef PORT`) | Particle magenta/cyan colour (D172). Logs each distinct SETCOMBINE + active cycletype, and — for a 2-cycle tri that consumes TEXEL1 with `tex_lod=0` and a distinct 2nd tile — the tile fast3d *samples* for each texunit vs what the DL *configured*. **Found D172**: 40/40 particle tris configured tile 1 @ tmem 392 (fire) but sampled tile 0 (smoke) → `smoke*smoke`. FIXED (`gfx_lod_tile_offset` `return rdp.tex_lod ? 0 : i`). Probe kept inert. | **live** (D172 FIXED, probe kept) |
|
||||
| `GE_D157` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex` + `import_texture`; `#ifdef PORT`) | D219/D252's "rainbow particle" family — bullet-impact spark on Bunker1 door confirmed by screenshot to cycle magenta/green/cyan (M-157). Three sub-probes: original exact-color-match log (`g_BulletSparkColors[]` byte match → geometry_mode/LIGHTING + shaded RGB); **`D157T`** texture-identity-keyed vertex full-path log (tile 0 holds a loaded texture ≤16 KiB in frame window [90,240] → raw `cn[]`, post-shade RGBA, LIGHTING, geometry_mode, combine_mode, tile-0 tmem/fmt/siz/addr/size/line, uv; cap 400 + summary/200); **`D157I`** import census (small textures imported in the window, deduped per source address → addr/fmt/siz/size/tile/tmem). M-158: bug did NOT reproduce in 6 identical dev-box runs — probes left live to capture the full RGBA path on the next occurrence (user Steam Deck run with `GE_D157=1`). **M-201:** F12 opens a ~90-frame full-detail `D157TB` burst; continuous logging cut to a 2000-line baseline + 500 colour-match hits (the uncapped white match ran live play at 4–6 fps). | **TEMP** (D252 fixed M-201; remove after release) |
|
||||
| `GE_D157` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex` + `import_texture`; `#ifdef PORT`) | D219/D252's "rainbow particle" family — bullet-impact spark on Bunker1 door confirmed by screenshot to cycle magenta/green/cyan (M-157). Three sub-probes: original exact-color-match log (`g_BulletSparkColors[]` byte match → geometry_mode/LIGHTING + shaded RGB); **`D157T`** texture-identity-keyed vertex full-path log (tile 0 holds a loaded texture ≤16 KiB in frame window [90,240] → raw `cn[]`, post-shade RGBA, LIGHTING, geometry_mode, combine_mode, tile-0 tmem/fmt/siz/addr/size/line, uv; cap 400 + summary/200); **`D157I`** import census (small textures imported in the window, deduped per source address → addr/fmt/siz/size/tile/tmem). M-158: bug did NOT reproduce in 6 identical dev-box runs — probes left live to capture the full RGBA path on the next occurrence (user Steam Deck run with `GE_D157=1`). **M-201:** F12 opens a ~90-frame full-detail `D157TB` burst; continuous logging cut to a 2000-line baseline + 500 colour-match hits (the uncapped white match ran live play at 4–6 fps). | **dead** (D252 fixed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
|
||||
| `GE_DVM` | `src/game/gunfire.c` (`gunUpdateAndFire` gate dump + `gunRenderFirstPersonGunModels` render dump; `#ifdef PORT`) | 1P weapon viewmodel (D215). `[DVM]` line: the `field_87F` show/hide gate terms + `Gun_hand_without_item` sub-terms + `copy_of_body_obj_header` RootNode/Switches/numRecords. `[DVMr]` line: `87F` + `cmds_emitted` = gfx commands the weapon `subdraw()` produced (healthy ≈ 150; the D215 bug showed **1**). Rate-limited ~1/s per hand. **Found + verified D215** (`renderdata.flags` read 0 → all weapon DL nodes gated off). Probe kept inert. | **live** (D215 FIXED, probe kept) |
|
||||
| `GE_DTEX` | `port/fast3d/gfx_pc.cpp:1001` | Per-`import_texture` param log (dims / line / size / lod / gen_mipmaps). Used to root-cause RC2 and D159. Kept as an inert diagnostic. | **live** (RC2/RC3 not finished) |
|
||||
| `GE_TEXDUMP` | `port/fast3d/gfx_pc.cpp:1020`, `port/fast3d/gfx_opengl.cpp:700` | PPM dump of every uploaded texture + a `fmt/siz/palfmt/palidx/pal[0..3]` line. Root-caused D161 (Depot ceiling). Kept as an inert diagnostic. | **live** (RC3 not finished) |
|
||||
| `GE_D116` | `port/fast3d/gfx_pc.cpp:1710,2362`, `src/game/textrelated.c:265,541` (cached `ge_d116`) | HUD/text "X-mirror" (D114/D116) vbo/uv trace. **D114/D116 CLOSED — NOT A BUG (M-33/D168): the captures were upside-down, not mirrored.** Probe is now dead scaffolding — strip on the next cleanup pass. | dead (D114/D116 closed) |
|
||||
| `GE_D288` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, right after the D233 trivial-reject guard; cached `ge_d288`) | M-152 diag for the Silo intro stray triangle: frame-gated to `num_dls` 550–755 (M-155), now **skips `any_behind_camera==1` triangles** (M-155 follow-up — the x/w divide is meaningless for w≤0; that subset was pure false-lead noise) and logs the render-mode word (`oml`) so a live capture can ID the culprit asset against level tables. Logs any triangle whose clip-space NDC bbox covers >50% of the screen or goes non-finite, with each vertex's `w`. Bare `-level_20` re-run: 1965 sane hits in the window, still zero for the target color RGB(83,62,44) — consistent with M-155's working theory that the bare boot's scripted camera never reaches the bug state; **headless probing is at its limit, live capture is next**. | **live** (D288) |
|
||||
| `GE_D288` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, right after the D233 trivial-reject guard; cached `ge_d288`) | M-152 diag for the Silo intro stray triangle: frame-gated to `num_dls` 550–755 (M-155), now **skips `any_behind_camera==1` triangles** (M-155 follow-up — the x/w divide is meaningless for w≤0; that subset was pure false-lead noise) and logs the render-mode word (`oml`) so a live capture can ID the culprit asset against level tables. Logs any triangle whose clip-space NDC bbox covers >50% of the screen or goes non-finite, with each vertex's `w`. Bare `-level_20` re-run: 1965 sane hits in the window, still zero for the target color RGB(83,62,44) — consistent with M-155's working theory that the bare boot's scripted camera never reaches the bug state; **headless probing is at its limit, live capture is next**. | **dead** (D288 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
|
||||
| `GE_D245V` | `src/game/sky.c:962` (`skyPortRenderPoly`-adjacent, `#ifdef PORT`) | D245 water-seam diag: per-call min/max of the recomputed S/T texture-coordinate ranges for the water fan vertices (rate-limited), to check whether the seam tracks a tc-shift boundary. Inert unset. | **live** (D245 OPEN) |
|
||||
| `GE_D245_FIXEDSHIFT` | `src/game/sky.c:946` (`#ifdef PORT`) | D245 test override: pins the water tc shift `k` to the given integer (0..SKY_TC_MAX_SHIFT) instead of the computed per-vertex value, to A/B the allowShift=TRUE behavior. Off by default (unset = current behavior). | **live, test-only** (D245 OPEN) |
|
||||
| `GE_D245_FIXEDSHIFT` | `src/game/sky.c:946` (`#ifdef PORT`) | D245 test override: pins the water tc shift `k` to the given integer (0..SKY_TC_MAX_SHIFT) instead of the computed per-vertex value, to A/B the allowShift=TRUE behavior. Off by default (unset = current behavior). | **inert** (D245 fixed) — since the s16 sky path was removed 2026-09-30 nothing consumes the forced shift; strip candidate |
|
||||
| `GE_D252` | `port/fast3d/gfx_pc.cpp:733` (`import_texture`, cached `s_d252`) | D252 rainbow-particle diag: one-shot per (address,size) import log of the tile state (tmem/fmt/siz/addr/size/line) for small textures so a suspect particle tile can be checked against its source bytes by hand. Remove once D252 is resolved. | **live** (D252 OPEN — non-reproducible) |
|
||||
| `GE_D252POOL` | `src/game/image.c` (`texLoad`), `src/game/initmttex.c`, `port/src/gimgfixup.c` | **TEMP** (M-201): one line per new texture load (pool main/other, free bytes) + `POOL-FULL` events + stage pool range; `D252SYNC: N slots re-synced, M changed` per stage load (the D252 fix at work: M>0 from the 2nd stage on). Load-time only, no fps cost. | **TEMP** (D252 fixed; remove after release) |
|
||||
| `GE_D245_OLDVTX=1` / `GE_D245_OLDTEX=1` | `src/game/sky.c` (`skyPortRenderPoly`) / `port/fast3d/gfx_pc.cpp` (CI8 import + tri-path dims) | **A/B** (M-201): restore the pre-fix s16 sky/water vertex path / the pre-fix 32x43, 16-wide water texture geometry. | **TEMP** (D245 fixed; remove post-release) |
|
||||
| `GE_D252POOL` | `src/game/image.c` (`texLoad`), `src/game/initmttex.c`, `port/src/gimgfixup.c` | **TEMP** (M-201): one line per new texture load (pool main/other, free bytes) + `POOL-FULL` events + stage pool range; `D252SYNC: N slots re-synced, M changed` per stage load (the D252 fix at work: M>0 from the 2nd stage on). Load-time only, no fps cost. | **dead** (D252 fixed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
|
||||
| `GE_D245_OLDVTX=1` / `GE_D245_OLDTEX=1` | `src/game/sky.c` (`skyPortRenderPoly`) / `port/fast3d/gfx_pc.cpp` (CI8 import + tri-path dims) | **A/B** (M-201): restore the pre-fix s16 sky/water vertex path / the pre-fix 32x43, 16-wide water texture geometry. | **dead** (D245 fixed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE. The pre-fix s16 sky/water vertex path (`skyPortRenderPoly` tail + `skyPortEmitTileShift`) went with it; the float path is now unconditional |
|
||||
| `GE_DYNTEXHASH_OFF=1` | `port/fast3d/gfx_pc.cpp` (`import_texture` cache key) | **A/B** (M-201, D341): disable the content-hashed cache key for dyn-pool textures (reverts to the stale address-only key). | **TEMP** (A/B switch) |
|
||||
| `GE_D207S` | `src/game/gunfire.c` (player shot-sound block, `#ifdef PORT`) | **TEMP** (M-201): one line per player shot-sound decision: `D207S t=<g_GlobalTimer> hand item rate -> H1|H2|SKIP` (SKIP = both per-hand sound handles still held). Read-only, uncapped, cheap; use instead of `GE_AUDIOTRACE` for live captures (that probe does file I/O on the audio path). | **TEMP** (D207/D241) |
|
||||
| `GE_D207S` | `src/game/gunfire.c` (player shot-sound block, `#ifdef PORT`) | **TEMP** (M-201): one line per player shot-sound decision: `D207S t=<g_GlobalTimer> hand item rate -> H1|H2|SKIP` (SKIP = both per-hand sound handles still held). Read-only, uncapped, cheap; use instead of `GE_AUDIOTRACE` for live captures (that probe does file I/O on the audio path). | **dead** (D207/D241 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
|
||||
| `GE_D306C="f0 f1 [wmax]"` | `port/fast3d/gfx_pc.cpp` (after `zf_note_emit`) | **TEMP** (M-201): depth-state census of every emitted triangle in frames f0..f1 (GL depth mode, geometry_mode, other_mode_l, tile-0 texture, NDC centroid, z/w range, min w); optional `wmax` keeps near geometry only. Cap 60000 lines. | **TEMP** (D306 closed; remove post-release) |
|
||||
| `GE_INTROCAM=<n>` | `src/game/bondview_r.c` (`#ifdef PORT`, intro camera pick) | **TEMP/test-only**: force the level intro's first camera index (Dam truck shot = 5). The PRNG draw is still consumed. Logs `D306: intro camera index` when set (or with `GE_D306C`). | **TEMP** (useful repro tool) |
|
||||
| `GE_STARTWEAPON=<ITEM_IDS>` | `src/game/bondview_r.c` (`#ifdef PORT`, after the start-inventory setup) | **TEMP/test-only** (M-201, D303): adds the item + 400 rounds of its ammo and makes it the starting right-hand weapon (M16 = 13). Pair with `GE_INPUTSCRIPT` `ZHOLD`. | **TEMP** (useful repro tool) |
|
||||
@@ -78,7 +79,7 @@ Two classes:
|
||||
| `GE_D235_OFF` | `src/boss.c` (same block) | **TEMP** A/B switch: skips the per-stage `videoResetTextureCache()`, i.e. reverts the D235 fix for one run. For comparing a restart with and without the fix. | **TEMP** (D235; remove when D235 closes) |
|
||||
| `GE_D235_RELOAD=<frames>` | `src/boss.c` (same block + `bossMainloop` retrace branch) | **TEMP** reload harness: once the title stage loads after the first level (e.g. after a scripted watch Abort), wait `<frames>` retraces, then re-enter that level via `bossSetLoadedStage()`, the front end's own mission-start call. A headless death/restart without the pointer-menu walk. Pair with `GE_INPUTSCRIPT="800:START;1200:A;1300:RIGHT;1400:A"` (abort) on `-level_XX`. | **TEMP** (D235; remove when D235 closes) |
|
||||
| `GE_D235_NEXT=<stage id>` | `src/boss.c` (with `GE_D235_RELOAD`) | **TEMP**: re-enter a *different* stage after the abort (level A → title → level B), used for D336 (Runway → Frigate). `GE_D85TEX=1` now also logs each stage's texture-pool size and address range (`src/game/initmttex.c`). | **TEMP** |
|
||||
| `GE_D320R="tick:action;..."` | `port/src/d320repro.c` (hooked in `src/game/lv.c` per-tick PORT block) | **TEMP** forced-repro harness keyed on `g_GlobalTimer`. Actions: `roster`, `jump=<chr>,list=<hexid>,off=<n>`, `bit=<hex>`, `trace=<chr>`, `rename=<chr>,<num>`, `seed=<hi>,<lo>` (D320); `explode=<type>,<dist>` (explosionCreate ahead of Bond) and `cheat=gl|rl|inv|<id>` (game cheat activation) for D252 headless explosion/debris captures. | **TEMP** (D320/D252) |
|
||||
| `GE_D320R="tick:action;..."` | `port/src/d320repro.c` (hooked in `src/game/lv.c` per-tick PORT block) | **TEMP** forced-repro harness keyed on `g_GlobalTimer`. Actions: `roster`, `jump=<chr>,list=<hexid>,off=<n>`, `bit=<hex>`, `trace=<chr>`, `rename=<chr>,<num>`, `seed=<hi>,<lo>` (D320); `explode=<type>,<dist>` (explosionCreate ahead of Bond) and `cheat=gl|rl|inv|<id>` (game cheat activation) for D252 headless explosion/debris captures. | **dead** (D320 fixed via D329) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE; `port/src/d320repro.c` + its `lv.c` hook deleted |
|
||||
| `GE_AIMLOG=1` (+ `GE_INPUTSCRIPT` tokens `MDX<n>` `MDY<n>` `RHOLD` `LHOLD` `RREL`) | `port/src/input.c` | D337 aim-model harness: scripted sustained mouse deltas (treated as a grabbed mouse) and aim holds, plus a per-poll line with aim state, active device, the game's drawn crosshair x, `crosshair_x/y_pos` and camera angles. Measures range, edge-scroll and jitter headlessly. | **live** |
|
||||
| `GE_PERFSTAT=1` | `port/fast3d/gfx_pc.cpp` (`gfx_run`/`gfx_end_frame`), `port/fast3d/gfx_opengl.cpp` (start/end_frame), `src/boss.c` (main loop) | #92/D339 perf probe: per-300-frame averages of render phases (pre / display-list / post / swap+pace), tris and draw batches per frame, game-thread busy time per scheduler message, and a GL_TIME_ELAPSED GPU timer (meaningful only when GPU-bound). | **live** |
|
||||
| `GE_D75V` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex`, end of the copy/transform loop; cached env check) | D75 (Nintendo logo) per-batch vertex probe: for each `G_VTX` batch in frames `lo..hi`, logs frame#, source pointer, n_vertices, original dest_index, raw ob[0..2] of the first vertex, min/max of the transformed x/y/z/w across the batch, and the MP translation row. Used to prove the transform is correct (M-195) and that every batch encodes v0=0 (slot-overwrite hypothesis). Value form `"lo-hi"` (e.g. `300-900` = the logo screen); unset = off. | **live** (D75) |
|
||||
@@ -86,12 +87,14 @@ Two classes:
|
||||
|
||||
| `GE_PDMOUSEAIM` | `port/src/input.c:487` (`pdMouseAimEnabled`) | Force on/off the PD-style mouse-aim model (D194 lineage); unset = default (on in N64 aim mode unless legacy GEPD aim is selected). | **live** |
|
||||
| `GE_QUITFRAME` | `port/src/video.c:966` | D344 harness clean self-quit: exits after frame `<n>` completes and swaps (replaces `timeout` kills; the host never exits under an in-flight frame). Cached. | **live** (harness) |
|
||||
| `GE_CRASHTEST` | `port/src/crash.c` (`crashTestMaybe`, called at the end of `crashInit`) | Deliberate-crash hook for verifying the crash handler: `GE_CRASHTEST=<N>` recurses N frames deep (capped at 64) right after the handler is installed and faults with a null write. `ge007.crash.log` should then show `#00`..`#N-1` in `crashTestRecurse`, then `SDL_main`, CRT and system frames; each frame line carries `va=0x…` for `addr2line -f -p -e build-pc/ge007.x86_64.exe <va>`. Unset/0 = no effect. | **live** (harness) |
|
||||
| `GE_ROOMPOOL` | `port/src/video.c:371` (`portRoomPoolScale`) | Scales the port's room pool by a float factor — before/after captures, and a user opt-out if the larger pool ever causes trouble on a level. | **live** |
|
||||
| `GE_BINDPROBE` | `port/src/input.c:885` | One-shot dump of the resolved PC bindings after setup (D383 bind-capture verification). | **live** (diagnostic) |
|
||||
| `GE_BINDCAPTUREPROBE` (+ `GE_BINDCAPTUREACTION`) | `port/src/optionsoverlay.c:1000,1005` | D383 isolated host→menu smoke probe: when the overlay force-opens, injects a scripted bind-capture keypress (ESC/DEL/F2). Test data dir must be private — capture persists the key. | **live** (diagnostic) |
|
||||
| `GE_OPTIONSOVERLAY_SECTION` | `port/src/optionsoverlay.c:989` | Isolated screenshot/probe of a nested F10 page by row key (opens that header; never a menu mode). | **live** (diagnostic) |
|
||||
| `GE_OPTIONSOVERLAY_SELECTBACK` | `port/src/optionsoverlay.c:995` | Screenshot-only: pins the selection to the Back row when the overlay auto-opens, to check it against a normally selected setting on the same page. | **live** (diagnostic) |
|
||||
| `GE_OPTIONTREEPROBE` | `port/src/optionsoverlay.c:888` | D388: opt-in in-memory walk of the real F10 page tree; writes no settings, binds or profile bytes; restores UI state afterward. | **live** (diagnostic) |
|
||||
| `GE_PRESETPROBE` | `port/src/optionsoverlay.c:957` (`presetProbe`) | D440: one-shot at overlay init — drives the "Original N64" and "Port defaults" preset rows through the real arm -> confirm activation and checks the "Active" state; logs `GE_PRESETPROBE: PASS/FAIL`. **Writes the live config** (persisted on exit) — use a throwaway data dir. | **live** (diagnostic) |
|
||||
| `GE_OVPOINTERMAP` | `port/src/optionsoverlay.c:1726` | D391 pointer-map diagnostic: on LMB-down (and every 120 samples) logs SDL mouse coords vs GL drawable pixel coords + the hover row, to separate hit-band/scroll errors from coordinate-space mismatch. | **live** (diagnostic) |
|
||||
| `GE_FOOTERPROBE` | `port/src/optionsoverlay.c:2232` | One-shot log of the overlay footer/help-line metrics (x extents). | **live** (diagnostic) |
|
||||
| `GE_FRONTOPTIONS_SECTION` | `port/src/frontoptions.c:295` | Isolated headless screenshot of a nested front-end options page by key (changes navigation only; never edits profile or config values). | **live** (diagnostic) |
|
||||
@@ -108,7 +111,7 @@ Two classes:
|
||||
| `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_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. | **dead** (D227 fixed) — the probe lived in the pre-D245 s16 sky path, unreachable since D245 M-201; removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
|
||||
| `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_D56` | `src/game/model.c:227,830` | watch `Model` raw-offset reads | dead (D56 closed) |
|
||||
@@ -132,19 +135,19 @@ Two classes:
|
||||
| `GE_D71LOG` | `port/fast3d/gfx_pc.cpp:645` | D71 fast3d trace | dead (D71 closed) |
|
||||
| `GE_D75` | `src/game/title.c` `sub_GAME_7F007F30` (capped 8) | D75 Bug 2 gun-barrel model probe: logs `chrModelInstance`/`gunModelInstance` ptr+obj+numMatrices, `render_pos`, `renderData.mtxlist`, `g_GfxMemPos`, `osVirtualToPhysical(render_pos)` | **LIVE** (D75 Bug 2 OPEN — see §F "D75 Bug 2 — RUNTIME PROBE") |
|
||||
| `GE_D305` | `src/snd.c:420` (`sndHandleEvent` desync branch, `#ifdef PORT`) | D305: logs the sound-preemption desync state when it is detected (after the NULL-guard fix made the crash itself impossible) — confirms whether `Game.AllUnlocked` correlates or was coincidental, and whether it recurs. Inert unset. | **live** (D305 monitoring) |
|
||||
| `GE_D309` | `src/game/chrai.c:810,3424`, `src/game/chraction.c:10139` (all `#ifdef PORT`) | D309 freeze probe: logs AI label-not-found restarts (`chrai.c`) and the run-to-bond persistent-list step-on path (`chraction.c`, capped 400 lines) — the diagnostics used to root-cause the elusive complete freeze (D309/D310). Inert unset. | **live** (D309/D310 fixed — kept for recurrence) |
|
||||
| `GE_D309` | `src/game/chrai.c:810,3424`, `src/game/chraction.c:10139` (all `#ifdef PORT`) | D309 freeze probe: logs AI label-not-found restarts (`chrai.c`) and the run-to-bond persistent-list step-on path (`chraction.c`, capped 400 lines) — the diagnostics used to root-cause the elusive complete freeze (D309/D310). Inert unset. | **dead** (D309/D310 fixed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE |
|
||||
| `GE_D314` | `port/src/optionsoverlay.c:681` (cached `s_d314Enabled`) | D314 F10-flicker probe: logs the overlay's viGetY / visible-row count / scroll / selection each poll. Inert unset; strip candidate once D314 is triaged. | **live** (D314 pending triage) |
|
||||
| `GE_D318R` | `port/src/random.c:69,147` (`#ifdef PORT`) | PRNG golden probe (D284/D318): logs the first 4000 gameplay-PRNG draws with tick + caller return address, and every `randomSeed` set — verifies the bit-exact port of `random.s` and which call sites consume the stream. Inert unset. | **live** (D284 verification) |
|
||||
| `GE_RSEED` | `port/src/random.c:134` (`#ifdef PORT`) | Test-only boot-seed override: `GE_RSEED=<hex64>` pins `g_randomSeed` at first seed instead of the wall-clock-derived value — for determinism and N64-parity tests. Inert unset. | **live, test-only** |
|
||||
| `GE_RSEED_LV` | `port/src/d318watchdog.c:232` (`#ifdef PORT`) | One-shot level-start seed log: logs `g_randomSeed` at the first tick after level start (optionally armed for a specific tick via `<hex>@<tick>`) — ties a saved/loaded game's PRNG state to a level for D318-class repro. Inert unset. | **live** (D318 verification) |
|
||||
| `GE_D318T` | `port/src/d318watchdog.c:270` (`#ifdef PORT`) | D318 watchdog timeline probe: per-tick log of the Facility ai_22 softlock signature window (c78 list/offset/attack state, c67 health) while the watchdog watches. Inert unset. | **live** (D318 verification) |
|
||||
| `GE_D322` | `port/src/audio.c` (`audioSetNextBuffer`), `src/snd.c` (`sndD322PoolSummary`) | D322 pool-telemetry probe: one line per 5 s of wall clock with the three pool layers a long session can exhaust — `sfx=count/max` (8-voice SFX soft cap), `evtq=n/64` (event queue occupancy; full = silent post drops, M-71), `pv free/lame/alloc` (24 physical synth voices shared by SFX + music; `-1`s before sndInit), plus the D204 pipeline fields. Cheap (~30 reads/s), campaign-safe; read together with `GE_D204=1`. | **live** (D322 capture) |
|
||||
| `GE_D318W` | `port/src/d318watchdog.c:136` (`#ifdef PORT`) | **Not a probe — the D318 port-side watchdog itself, ON by default.** Detects the Facility ai_22 hold-pose softlock signature (a latent N64 race, see findings.md D318) for 600 consecutive ticks and re-seeds c78's attack via the game's own fresh-attack entry point. `GE_D318W=0` opts out; logs `D318W:` lines when it acts. **Permanent fix, not strip-candidate.** | **live** (D318 fix — permanent) |
|
||||
| `GE_D318W` | `port/src/d318watchdog.c` (`d318WatchdogTick`, `#ifdef PORT`) | **The D318/D320 pin detector, ON by default, DETECT-AND-LOG ONLY since 2026-09-30.** Watches the Facility ai_22 / ai_19 and Control ai_9 hold-pose pin signatures (findings D318/D320) and logs `D318W:` lines (first detection, confirmation after 600 ticks, periodic reminders while held). It used to re-seed the attack via `sub_GAME_7F025560`; that recovery was removed because D329 fixed the cause, so a hit now means a D329 regression. `GE_D318W=0` silences it. | **live** (D329 regression detector) |
|
||||
| `GE_D294` | `src/boss.c:498` (`#ifdef PORT`) | One line per stage reset: stage number + current MA malloc value (D294 pool investigation). | **live** (D294) |
|
||||
| `GE_D294BANK` | `src/game/bg.c:605` | D294: restarts the bank-window counter on stage change (pairs with the `GE_D104` windowing). | **live** (D294) |
|
||||
| `GE_D303` | `port/fast3d/gfx_pc.cpp:1717` | D303: logs up to 200 tri emits with per-vertex bbox + w-sum (degenerate-triangle hunt). | **live** (D303) |
|
||||
| `GE_D307` | `src/game/prop.c:709` | D307: load-time dump of every CCTV record's inputs + derived orientation, for word-for-word PC-vs-ROM comparison. | **live** (D307) |
|
||||
| `GE_D318B` (+ `GE_D318B_ALL`, `GE_D318B_CLK`) | `port/src/d318bprobe.c:91,97,210` | D318/D320 boundary probe suite (M-199): per-tick ground truth of the aim-hold/derail/anim-pin state machine for env-selected chrs (`"5,9"`, max 4); `_ALL` logs every ACT_ATTACK tick; `_CLK=N` locksteps N sim ticks per rendered frame. Read-only. **TEMP — remove when D318/D320 conclude.** | **TEMP** (D318/D320) |
|
||||
| `GE_D318B` (+ `GE_D318B_ALL`, `GE_D318B_CLK`) | `port/src/d318bprobe.c:91,97,210` | D318/D320 boundary probe suite (M-199): per-tick ground truth of the aim-hold/derail/anim-pin state machine for env-selected chrs (`"5,9"`, max 4); `_ALL` logs every ACT_ATTACK tick; `_CLK=N` locksteps N sim ticks per rendered frame. Read-only. **TEMP — remove when D318/D320 conclude.** | **dead** (D318/D320 fixed via D329) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE; `port/src/d318bprobe.c` + `port/include/d318bprobe.h` deleted. NB `GE_D318B_CLK` (sim-ticks-per-frame lockstep, used for the D329 pin repro) went with the suite |
|
||||
| `GE_D329` (+ `GE_D329LOG`) | `src/game/model.c:3516,3519` (`#ifdef PORT`) | **The D329 anim-pin fix gate — ON by default** (unlike the other probes here): re-derives a stale pinned frameb when the endframe no longer blocks it. `GE_D329=0` A/B-disables; `GE_D329LOG` logs each trigger (cap 2000). Cached. | **live** (D329 fix) |
|
||||
|
||||
## Notes
|
||||
|
||||
+112
-28
@@ -83,40 +83,124 @@ Two useful existing properties:
|
||||
|
||||
### Phase 0 — Baseline & ground truth (port-only)
|
||||
|
||||
Goal: know exactly what the default preset does before touching anything.
|
||||
**RESOLVED (2026-09-12, commit 8f2f9e1d, D248).** The ~30 fps default was
|
||||
confirmed a **port artifact**, not original N64/GE behavior: `sched.c`'s
|
||||
`__scHandleRetrace` read a per-client "every retrace vs. every other
|
||||
retrace" flag via a hardcoded `*((s32*)client + 2)` byte offset (`sched.c`
|
||||
~line 350) that only lands on the right field when `OSScClient` is 8 bytes
|
||||
(32-bit pointers, true on N64). On this 64-bit port the struct is 16 bytes,
|
||||
so the read landed inside the gfx client's own `msgQ` pointer (always
|
||||
non-NULL), silently collapsing the gfx client onto the audio client's 30Hz
|
||||
path even though the VI-retrace pacemaker itself always ticked at a correct
|
||||
60Hz. Fix (`#ifdef PORT`, `sched.c` ~line 348): index the struct
|
||||
(`client[1].next`) instead of the hand-rolled offset, so it scales with
|
||||
pointer width. Verified live via the `Video.DisplayFPS` overlay: rock-stable
|
||||
30 FPS pre-fix, 60 FPS post-fix, same build/scene. Same bug class as
|
||||
D122/D126/D132/D209 (pointer-width struct growth), just in the scheduler.
|
||||
|
||||
- [ ] 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.
|
||||
**Decision-gate answer:** the default preset is now the console's real
|
||||
60Hz(NTSC)/50Hz(PAL) rate — decision #1 above ("default preset = the
|
||||
original pace") is satisfied by this fix, not by preserving the halved
|
||||
30fps. All prior playtest/determinism baselines predate this fix and should
|
||||
be treated as pre-D248.
|
||||
|
||||
- [x] Find why the default was ~30 rather than the console's 60 Hz — root
|
||||
cause above; §F **D248**.
|
||||
- [x] Record a Dxxx finding with the measured rates and root cause — D248.
|
||||
- [ ] Measure actual sim rate and present rate with a dedicated periodic log
|
||||
line (`sim=..Hz render=..Hz`) rather than the one-off `DisplayFPS`
|
||||
overlay reading used to verify D248. Not yet built — still open if a
|
||||
standing measurement (vs. a spot-check) is wanted before Phase 1/2
|
||||
work lands.
|
||||
- [x] **Decision gate** — resolved above.
|
||||
|
||||
### 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.
|
||||
**Audit (2026-09-12, post-D248):** more of this than the plan assumed
|
||||
already exists. Verified by reading the code directly (not just the
|
||||
delegate's first pass, which mis-read one of these — see below):
|
||||
|
||||
- [x] **Accumulator + remainder carry, tick thread.** `portTickThread`
|
||||
(`port/src/libultra.c` ~294-313) already schedules off `g_nextTickUs +=
|
||||
g_tickIntervalUs` — the *scheduled* time, not `now` — so it doesn't
|
||||
drift under load. It resyncs to `now + interval` only when a tick is
|
||||
already overdue (`g_nextTickUs <= now`), which is a bounded-catch-up
|
||||
behavior, not a naive reset. This already matches the PD
|
||||
`frametimeCalculate` accumulator pattern.
|
||||
- [x] **Accumulator + remainder carry, present-side limiter.**
|
||||
`sync_framerate_with_timer` (`port/fast3d/gfx_sdl2.cpp` ~403-428) uses
|
||||
the same shape: `next = previous_time + interval` (schedule-based, not
|
||||
wall-clock-based), a sleep-then-busy-wait hybrid to hit the deadline
|
||||
precisely, and only snaps `previous_time` to the real wake time `t`
|
||||
when the overshoot exceeds ~1ms (`t - next < 10000` in its 100ns units)
|
||||
— i.e. it already carries the remainder in the normal case and only
|
||||
gives it up after a real hitch. (An earlier audit pass mis-read this as
|
||||
"no remainder carry, resets every frame" — it does not; re-verified by
|
||||
reading `gfx_sdl2.cpp:403-428` directly.)
|
||||
- [ ] **Triple-buffered swap chain.** Not present — presentation uses a
|
||||
standard SDL/GL double-buffer swap (`SDL_GL_SwapWindow`,
|
||||
`gfx_sdl_swap_buffers_begin`, `gfx_sdl2.cpp` ~436-444). Not started.
|
||||
- [ ] **PD safety rule (vsync off + no cap → force a max cap).** Not present.
|
||||
The only existing floor/ceiling logic is the D186 guard in
|
||||
`gfx_sdl_set_target_fps` / `video.c` (~289-292, ~464-467), which goes
|
||||
the *other* direction — it refuses caps below 30 (because pacing still
|
||||
runs inline on the sim thread, see Phase 2 note below) — there is no
|
||||
"vsync off AND cap==0" case that forces a ceiling. Not started.
|
||||
- [x] Config: `Video.VSync` / `Video.FpsCap` already registered
|
||||
(`video.c:106-107`) and live-reapplied (`video.c` ~340-349).
|
||||
|
||||
**Net:** the hard part of Phase 1 (drift-free accumulator scheduling) is
|
||||
already done, on both the sim-tick and present-side clocks. What's left is
|
||||
narrow: a safety-cap rule and, if wanted, triple buffering — plus whatever
|
||||
Phase 2 needs on top.
|
||||
|
||||
**Phase 2 first slice, scoped (2026-09-12).** The real gap for a 120fps
|
||||
*presentation* preset isn't pacing math — it's that `gfx_run()`
|
||||
(`gfx_pc.cpp:3179-3232`) composites straight into the default framebuffer
|
||||
(fb 0) once per sim tick and then calls `gfx_wapi->swap_buffers_begin()`
|
||||
once; there's no mechanism to show that same completed frame again on an
|
||||
extra vsync between sim ticks. The existing `GfxRenderingAPI` already has
|
||||
everything needed to do this **without any new GL primitives or a second
|
||||
thread/context**:
|
||||
- `copy_framebuffer(fb_dst, fb_src, left, top, flip_y, use_back)`
|
||||
(`gfx_opengl.cpp:1270`) already blits the real window backbuffer
|
||||
(`fb_src == 0` + `use_back = true` reads `GL_BACK`, confirmed
|
||||
`gfx_opengl.cpp:1309-1311`) into an arbitrary allocated FBO/texture, and
|
||||
can blit back the other way (`fb_dst == 0` writes straight into the real
|
||||
default framebuffer, `gfx_opengl.cpp:1214`'s `fb_id == 0 -> bind 0`
|
||||
convention).
|
||||
- `gfx_pc.cpp` already keeps a couple of always-allocated internal
|
||||
framebuffers this same way (`game_framebuffer`,
|
||||
`game_framebuffer_msaa_resolved`, created at `gfx_pc.cpp:3069-3070`) — a
|
||||
new "last presented frame" capture FBO is the same pattern, one more
|
||||
entry.
|
||||
- The tick clock (`portTickThread`) runs on its own pthread, independent of
|
||||
the render/sim thread that calls `gfx_run`/`swap_buffers_begin` — so
|
||||
spending extra wall-clock time *inside* `swap_buffers_begin` presenting a
|
||||
duplicate frame does not touch the tick schedule at all; it only changes
|
||||
how much idle time the render thread burns before the next tick message
|
||||
is waiting for it. Sim rate stays exactly invariant by construction, per
|
||||
decision #3.
|
||||
|
||||
**Smallest concrete slice:** capture the just-finished frame into a new
|
||||
internal FBO right before the real `swap_buffers_begin()` call
|
||||
(`gfx_pc.cpp:3230-3231`, via `gfx_rapi->copy_framebuffer(captureFb, 0, -1,
|
||||
-1, false, true)`); when an experimental `Video.PresentRate` config knob
|
||||
requests double-rate (2x — e.g. 120 from a 60Hz NTSC sim, 100 from 50Hz
|
||||
PAL), have `gfx_sdl_swap_buffers_begin` (`gfx_sdl2.cpp`) do the normal swap,
|
||||
then — using the same schedule-accumulator shape as
|
||||
`sync_framerate_with_timer` — sleep to the halfway point of the tick
|
||||
interval, blit the capture FBO back into fb 0
|
||||
(`gfx_rapi->copy_framebuffer(0, captureFb, -1, -1, false, false)`), and
|
||||
`SDL_GL_SwapWindow` again. Default (`Video.PresentRate = 0`) must take none
|
||||
of these code paths, so the default preset stays byte-identical to today.
|
||||
Restrict the first slice to exactly {0 = off, 2 = double}; don't generalize
|
||||
to arbitrary/uncapped multipliers yet — that needs the vsync-off safety cap
|
||||
from the Phase 1 list above first. No options-menu UI in this slice
|
||||
(config-file-only); that's a fast follow once the mechanism is playtest-
|
||||
verified not to tear/stutter.
|
||||
|
||||
### Phase 2 — Experimental presentation rates (port-only)
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ D276,CLOSED,"D236 wall, eighth pass —",CLOSED (2026-09-26 bookkeeping:
|
||||
D279,CLOSED,Backfilled — this label was consumed by an undocumented partial session (2026-09-14 ~23:00) that left artifacts in scratch/ (d279_run.log… —,CLOSED (2026-09-26 bookkeeping:
|
||||
D281,FIXED,"Game.AllUnlocked (D257) still breaks audio, and can also break right-mouse aim, when enabled before any save has ever been written (v0.2.1 user testing… —","FIXED (2026-09-23, port/src/libultra.c):"
|
||||
D282,FIXED,"QoL ask (v0.2.1 user testing, 2026-09-15):","FIXED (port/src/input.c, #ifdef not needed —"
|
||||
D283,OPEN,"Steam Deck first-run preset (D-unnumbered in port/src/video.c/main.c, videoApplySteamOSDefaults()) did not apply for a user who added the game via Steam… —","OPEN — mechanism understood, root cause (Desktop-vs-Game-Mode STEAMOS env var difference) unverified on real hardware; user-facing workaround documented in README/release notes."
|
||||
D283,FIXED,"Steam Deck first-run preset (D-unnumbered in port/src/video.c/main.c, videoApplySteamOSDefaults()) did not apply for a user who added the game via Steam… —","FIXED (2026-09-30, port-only) —"
|
||||
D280,CLOSED,"D236 wall, ninth pass —",CLOSED (2026-09-26 bookkeeping:
|
||||
D275,CLOSED,"D236 wall, eighth pass —",CLOSED (2026-09-26 bookkeeping:
|
||||
D274,CLOSED,"D236 wall, seventh pass —",CLOSED (2026-09-26 bookkeeping:
|
||||
@@ -263,7 +263,7 @@ D383,CLOSED,PD-style keyboard rebinding in both options UIs (2026-09-27).,CLOSED
|
||||
D384,CLOSED,Front PC Options key-bind navigation feedback + two-slot UI (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
|
||||
D385,CLOSED,Real mouse 1–5 action rebinding in the two-slot editor (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
|
||||
D386,CLOSED,Correct Aim default from SDL middle (Mouse 2) to right (Mouse 3) (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
|
||||
D387,OPEN,M7 AllUnlocked's read-time fake unlocks leak into saved EEPROM (2026-09-27).,OPEN (isolated user-data-copy save/untoggle repro; no fix):
|
||||
D387,FIXED,M7 AllUnlocked's read-time fake unlocks leak into saved EEPROM (2026-09-27).,"FIXED 2026-09-30 (port-only per-slot raw/seen merge on EEPROM write, libultra.c geD387MergeSlotWrite; isolated ON-vs-OFF byte-identity matrix passed)."
|
||||
D388,CLOSED,Nest functional binding/HUD menus and remove redundant M5 toggle (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
|
||||
D389,CLOSED,F10 footer clipping and ambiguous submenu links (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
|
||||
D390,CLOSED,F10 pinned Back selection did not visibly change (2026-09-27).,CLOSED (2026-09-28). Shipped in v0.4.0; accepted in the user's final review / campaign playtest.
|
||||
@@ -297,3 +297,11 @@ D431,FIXED,Light fixtures never reached lightFixtureBreak on PC:,FIXED in tree (
|
||||
D432,FIXED,"Gunshot sounds ""off"" after D427:",FIXED in tree (gate offset rounded to odd at 1 tick/frame); measured shot/sound cadence identical to N64 rules; residual audible difference vs 1964 tracked as D433.
|
||||
D433,OPEN,"Slight audible difference in automatic-gun shot sound vs GEPD/1964 after cadence was matched (user by-ear, Caverns).",OPEN (not a timing issue; audio layer suspected).
|
||||
D434,FIXED,Raw ((u8*)&g_Textures[n])[0] & 0xf hit-type reads are wrong on PC; latent OOB once D431 returned real texture ids (found by sub-agent review).,"FIXED (.hitTexture at 3 sites, port-guarded); live check owed."
|
||||
D440,FIXED,"""Original N64"" / ""Port defaults"" one-press presets in the F10 overlay and front-end PC Options (+ -n64preset/-portpreset).",FIXED (port-only; headless-verified; user live accept owed).
|
||||
D441,PARTIAL,64-bit pointer-width sweep (part 1):,"PARTIAL (mechanical widening landed, build/run verified on Bunker and Silo; census/provenance table for the remaining sites still owed)."
|
||||
D442,FIXED,Game.AllUnlocked reworked into a pure RAM/query-time override:,FIXED (build and eep-diff verified; level-select/cheat-menu live check owed).
|
||||
D443,FIXED,"D357 settings-display cleanup (mouse ×, whole-% deadzone and volume steps, FOV in horizontal degrees, MSAA 16×) plus a ""Restart game"" action (orderly D344 quit…",FIXED (port-only; reset probes pass; both-UI visual check and one Restart click owed; Linux relaunch path not compiled).
|
||||
D444,FIXED,Front-end PC Options:,FIXED (build-verified; human mouse test owed)
|
||||
D447,FIXED,Pillarbox output mode (Video.AspectMode = Original) for the Original N64 preset; crop overscan no longer cancels Wide/Cinema.,"FIXED (build + 3 captures verified; Wide/Cinema, 16:9 Ratio, mouse and 1964 comparison owed to a human)"
|
||||
D445,FIXED,Crouched-rifle firing animation (crouched_rifle_firing_animation_groupA) never resolved on PC -> SIGSEGV in modelConstrainOrWrapAnimFrame when an MP player cro…,"FIXED — layout only (§D5 linker adjacency, D164 class):"
|
||||
D446,FIXED,Couch 2P watch-menu text (the MP pause page) differs by +-1-2 per channel between builds whose only difference is data layout (2026-09-29).,FIXED — port-only (fast3d):
|
||||
|
+260
-12
@@ -448,7 +448,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D174 | **"No blood effect" (user QA report).** — full `## D174` entry at file tail | CLOSED — faithful (2026-09-26, user side-by-side on 1964: blood matches N64). Earlier: Blood renders (user, 2026-09-26); OPEN only for an N64-accuracy comparison on 1964. Earlier: OPEN — observed; first step = BUNKER1 firefight with regenerated sidecars |
|
||||
| D183 | **M-36 Family A ("texture line/pitch shear") is DISPROVEN for the Surface repro (`-level_36`).** The importers' pitch assumption is real but never fires there; the Surface cliff "grey static" is not a shear. Full evidence + what the walls actually are: §F "D183" below. A defensive, provably-no-op de-stride landed in `import_texture()` anyway (covers the `gfx_dp_load_tile` case the `SUPPORT_CHECK`s assert against), plus `GE_DTEX` STRIDED marker + `GE_TEXRAW` raw-source dump. | CLOSED (2026-09-26, user-approved: disproven hypothesis; the Surface sky issue it served was fixed under D176). Earlier: Hypothesis DISPROVEN + diagnostics shipped; D176(b)/D182(2) still OPEN |
|
||||
| D173 | **Third-person Bond model spawns too high** — full `## D173` entry at file tail | PARTIAL (2026-09-29 bookkeeping: mostly fixed as a side effect of D243, user-confirmed; a small ~1-in-10 offset residual remains, tracked in docs/ROADMAP.md §2). Earlier: **MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed).** User: "9/10 times he's in the right place now, if he's off it's only by a small amount, no more floating in midair or glitching out spinning like before." The M-160/M-165 shared-mechanism hypothesis (D243's animation-state/position desync class) holds for this bug too, confirmed by outcome even though the exact code path was never separately traced for D173 specifically. Small positional-offset residual remains, not investigated further — no more full floating/spin. |
|
||||
| D179 | **Packaged build crashes after the logos — the D43/D69 model + bg sidecars are ROM-derived and absent from any build that has no ROM (M-34, alpha-release QA).** — full `## D179` entry at file tail | Diagnosed, not fixed — release packaging gap + latent `romdata.c` fallback |
|
||||
| D179 | **Packaged build crashes after the logos — the D43/D69 model + bg sidecars are ROM-derived and absent from any build that has no ROM (M-34, alpha-release QA).** — full `## D179` entry at file tail | Diagnosed, not fixed — release packaging gap + latent `romdata.c` fallback. **Fallback tail hardened (2026-09-30, `chore/romdata-valloc`):** on Windows a failed fixed-address `VirtualAlloc` now logs `GetLastError` + a `VirtualQuery` of whatever occupies `0x10000000` and aborts boot cleanly instead of limping into the heap copy and a later AV; success path unchanged; POSIX `mmap` fallback untouched (still degrades) |
|
||||
| D180 | **Native-PC input pass (M-XX QoL run, `qol/native-pc-input-menu`, port-only).** — full `## D180` entry at file tail | FIXED (2026-09-26 bookkeeping: shipped; user reports it feels good in v0.4.0 play). Earlier: LANDED, port-only, no `#ifdef PORT`.… |
|
||||
| D181 | **`Game.ScreenShakeIntensity` — first route-(b) `src/` gameplay-cosmetic hook (M-XX QoL run).** — full `## D181` entry at file tail | FIXED (2026-09-26 bookkeeping: shipped; user reports it feels good in v0.4.0 play). Earlier: **Menu row pulled (v0.2.1, user testing).** The hook is correct on its own narrow terms —… |
|
||||
| D184 | **F10 port-layer options overlay — the approach-(C) surface from `OPTIONS-MENU-PLAN.md` (M-37).** — full `## D184` entry at file tail | FIXED (2026-09-26 bookkeeping: shipped; user reports it feels good in v0.4.0 play). Earlier: **LANDED, port-only, no `#ifdef PORT` in `src/`.** Build links 241/241.… |
|
||||
@@ -498,7 +498,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D279 | **Backfilled — this label was consumed by an undocumented partial session (2026-09-14 ~23:00) that left artifacts in `scratch/` (`d279_run.log`…** — full `## D279` entry at file tail | CLOSED (2026-09-26 bookkeeping: sub-pass of D236, which closed in M-198). Earlier: undocumented — artifacts only |
|
||||
| D281 | **`Game.AllUnlocked` (D257) still breaks audio, and can also break right-mouse aim, when enabled before any save has ever been written (v0.2.1 user testing…** — full `## D281` entry at file tail | FIXED (2026-09-23, port/src/libultra.c): the AllUnlocked patch now reproduces the game's fresh-slot result (folder saves with DEFAULT_OPTIONS + one free slot) instead of five folder-1 slots with options 0 (no aim sight). Verified by fresh-eep SAVELOG; audio half **confirmed 2026-09-28 (campaign, user: good)**. v0.4.0 decision (user-delegated, 2026-09-28): keep the shipped mitigation + README disclosure; the zero-saves root cause (sndHandleEvent/aim interaction) is v0.5.0. |
|
||||
| D282 | **QoL ask (v0.2.1 user testing, 2026-09-15): front-end menu navigation (main menu, file select, mission-select map) currently requires the RIGHT analog stick —…** — full `## D282` entry at file tail | FIXED (`port/src/input.c`, `#ifdef` not needed — port-layer-only file), feel unconfirmed. |
|
||||
| D283 | **Steam Deck first-run preset (D-unnumbered in `port/src/video.c`/`main.c`, `videoApplySteamOSDefaults()`) did not apply for a user who added the game via Steam…** — full `## D283` entry at file tail | OPEN — mechanism understood, root cause (Desktop-vs-Game-Mode `STEAMOS` env var difference) unverified on real hardware; user-facing workaround documented in README/release notes. |
|
||||
| D283 | **Steam Deck first-run preset (D-unnumbered in `port/src/video.c`/`main.c`, `videoApplySteamOSDefaults()`) did not apply for a user who added the game via Steam…** — full `## D283` entry at file tail | FIXED (2026-09-30, port-only) — hardware (DMI) detection + `STEAMOS`, applied after `configLoad()` once per ini (`Video.DeckPresetApplied`) while the display keys are untouched; `GE_FAKE_DECK`-verified on Windows, real-Deck check owed. (Was OPEN: first-run-only preset skipped by a Desktop Mode launch.) |
|
||||
| D280 | **D236 wall, ninth pass — fog math VERIFIED N64-faithful; the IA8 noise class VISUALLY confirmed as the weave's painter (M-135).** No quick win for trees. — full `## D280` entry at file tail | CLOSED (2026-09-26 bookkeeping: sub-pass of D236, which closed in M-198). Earlier: REFRAMED again — no fix; next steps in detail section, see D236 |
|
||||
| D275 | **D236 wall, eighth pass — probe upgrades (`GE_D270PIX` list, `GE_D270KEEPOML` counter) (M-133).** — full `## D275` entry at file tail | CLOSED (2026-09-26 bookkeeping: sub-pass of D236, which closed in M-198). Earlier: done — probes live, uncommitted; see D276 for what they found |
|
||||
| D274 | **D236 wall, seventh pass — probe identity (`seq`) unreliable; per-slot address stamp. Content-vs-decode question was a bug in our own offline analysis (M-132).** — full `## D274` entry at file tail | CLOSED (2026-09-26 bookkeeping: sub-pass of D236, which closed in M-198). Earlier: `d267_slot_addr` fix live (always-on); content/decode question CLOSED; mip-presence CLOSED; wall still open, see D236/D276 |
|
||||
@@ -671,7 +671,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D384 | **Front PC Options key-bind navigation feedback + two-slot UI (2026-09-27).** full `## D384` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. |
|
||||
| D385 | **Real mouse 1–5 action rebinding in the two-slot editor (2026-09-27).** full `## D385` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. |
|
||||
| D386 | **Correct Aim default from SDL middle (Mouse 2) to right (Mouse 3) (2026-09-27).** full `## D386` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. |
|
||||
| D387 | **M7 AllUnlocked's read-time fake unlocks leak into saved EEPROM (2026-09-27).** full `## D387` entry at file tail | OPEN (isolated user-data-copy save/untoggle repro; no fix): a profile-settings save persists fake cheats/0x3FF times; OFF does not undo them. Rotating writes and cheat-award suppression make simple write masking unsafe. **v0.4.0 (2026-09-28): shipped as a loudly-documented EXPERIMENTAL / irreversible option** (row = "All unlocked (EXPERIMENTAL)", ON shows "ON - UNSAFE", README backs it up); safe architecture deferred to v0.5.0. **User-confirmed 2026-09-28: the warning is sufficient -- ship.** |
|
||||
| D387 | **M7 AllUnlocked's read-time fake unlocks leak into saved EEPROM (2026-09-27).** full `## D387` entry (fix section `### D387 Fix` at file tail) | FIXED 2026-09-30 (port-only per-slot raw/seen merge on EEPROM write, `libultra.c` `geD387MergeSlotWrite`; isolated ON-vs-OFF byte-identity matrix passed). Not repaired: fakes already persisted by older builds. Limitation: cheats earned while ON are not persisted. Live accept owed. |
|
||||
| D388 | **Nest functional binding/HUD menus and remove redundant M5 toggle (2026-09-27).** full `## D388` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. |
|
||||
| D389 | **F10 footer clipping and ambiguous submenu links (2026-09-27).** full `## D389` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. |
|
||||
| D390 | **F10 pinned Back selection did not visibly change (2026-09-27).** full `## D390` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. |
|
||||
@@ -705,6 +705,14 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D432 | **Gunshot sounds "off" after D427: sound-trigger gate (`SoundTriggerRate`) is per-tick on PC but frame-granular on the N64 (user: Cavern, Mac-10 run).** — full `## D432` entry at file tail | FIXED in tree (gate offset rounded to odd at 1 tick/frame); measured shot/sound cadence identical to N64 rules; residual audible difference vs 1964 tracked as D433. |
|
||||
| D433 | **Slight audible difference in automatic-gun shot sound vs GEPD/1964 after cadence was matched (user by-ear, Caverns).** — full `## D433` entry at file tail | OPEN (not a timing issue; audio layer suspected). |
|
||||
| D434 | **Raw `((u8*)&g_Textures[n])[0] & 0xf` hit-type reads are wrong on PC; latent OOB once D431 returned real texture ids (found by sub-agent review).** — full `## D434` entry at file tail | FIXED (`.hitTexture` at 3 sites, port-guarded); live check owed. |
|
||||
| D440 | **"Original N64" / "Port defaults" one-press presets in the F10 overlay and front-end PC Options (+ `-n64preset`/`-portpreset`).** — full `## D440` entry at file tail | FIXED (port-only; headless-verified; user live accept owed). Follow-up: preset stretches 4:3 in non-4:3 windows (no pillarbox mode). |
|
||||
| D441 | **64-bit pointer-width sweep (part 1): 17 files widened `(u32)`/`(s32)` pointer casts to `uintptr_t` under `#ifdef PORT` (ABI/layout exception; ROADMAP §2 "Latent 64-bit ABI hazards").** — full `## D441` entry at file tail | PARTIAL (mechanical widening landed, build/run verified on Bunker and Silo; census/provenance table for the remaining sites still owed). |
|
||||
| D442 | **`Game.AllUnlocked` reworked into a pure RAM/query-time override: #ifdef PORT hooks in `fileGetIsCheatUnlocked` and `fileIsStageUnlockedAtDifficulty` (every stage reports COMPLETED); the EEPROM patch and the D387 merge are removed (RULE-2-SIGNOFF, maintainer opt-in feature).** Full `## D442` entry at file tail | FIXED (build and eep-diff verified; level-select/cheat-menu live check owed). |
|
||||
| D443 | **D357 settings-display cleanup (mouse ×, whole-% deadzone and volume steps, FOV in horizontal degrees, MSAA 16×) plus a "Restart game" action (orderly D344 quit, then relaunch).** Full `## D443` entry at file tail | FIXED (port-only; reset probes pass; both-UI visual check and one Restart click owed; Linux relaunch path not compiled). |
|
||||
| D444 | **Front-end PC Options: clickable Previous/Next page controls; hover no longer overrides the wheel/page selection; stale All-unlocked comments refreshed.** — full `## D444` entry at file tail | FIXED (build-verified; human mouse test owed) |
|
||||
| D447 | **Pillarbox output mode (`Video.AspectMode` = Original) for the Original N64 preset; crop overscan no longer cancels Wide/Cinema.** — full `## D447` entry at file tail | FIXED (build + 3 captures verified; Wide/Cinema, 16:9 Ratio, mouse and 1964 comparison owed to a human) |
|
||||
| D445 | **Crouched-rifle firing animation (`crouched_rifle_firing_animation_groupA`) never resolved on PC -> SIGSEGV in modelConstrainOrWrapAnimFrame when an MP player crouch-fires a rifle (2026-09-28).** — full `## D445` entry at file tail | FIXED — layout only (§D5 linker adjacency, D164 class): `crouched_rifle_firing_animation_group1` has no zero terminator and on N64 falls through into `...groupA` (source-order .data); GCC places groupA first, so groupA[0] kept the raw anim offset 0xB84. `#ifdef PORT` one array {right, left, terminator} + `groupA` alias (chr.c/chr.h); N64 resolution restored. |
|
||||
| D446 | **Couch 2P watch-menu text (the MP pause page) differs by +-1-2 per channel between builds whose only difference is data layout (2026-09-29).** — full `## D446` entry at file tail | FIXED — port-only (fast3d): `GL_TEXTURE_MAX_ANISOTROPY` is per-texture-object state that `gfx_opengl_set_sampler_parameters` set only when mipmapped and never reset; a GL texture id freed by `gfx_texture_cache_delete_range` (unordered_map order = texture address order) and reused for a non-mipmapped glyph kept 4x. Now always written (level or 1). Render-only; sim never affected. |
|
||||
|
||||
|
||||
Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS
|
||||
@@ -10622,7 +10630,7 @@ D243 (cutscene shake/duplication, its own CONSOLIDATED NEXT STEPS block appears
|
||||
|
||||
**Packaged build crashes after the logos — the D43/D69 model + bg sidecars are ROM-derived and absent from any build that has no ROM (M-34, alpha-release QA).** The first `goldeneye-pc-port-*-win64.zip` from CI (bundle-win.sh) shows the Rare/Nintendo logos (compiled-in `assets/rarewarelogo.c`) then AVs at `0x6b157a88`/`0x70157a88` — symbolised: `modelPromoteNodeOffsetsToPointers` (`model.c`) ← `load_object_fill_header` (`objecthandler_2.c`) ← first prop/item model load. Root cause is **not** a code regression: the port loads PC-layout model geometry from `data/pcmodels-<region>/{pcmodels.bin,manifest.csv}` and stage bg/stan from `data/pccg-<region>/{pccg.bin,manifest.csv}`, both produced offline by `tools_pc/d43_emit.py` / `d69_emit.py` from the ROM (see `port/src/pcmodels.c` / `pccg.c`; `pcmodelsReserveSize` logs *"pcmodels.bin not found — model loads will fail"* and returns 0). CI has no ROM so it never runs the emit scripts, and the two `data/` dirs are (correctly) gitignored ROM-derived game data — **cannot be committed or shipped** (converted Nintendo/Rare geometry, DLs, collision, stan nav; = distributing assets). A local build with those dirs present runs clean (verified M-34: 1741 frames, `romdataInit ... mapped at 0x10000000`, `pcmodels: 512 sidecars`, `pccg: 73 sidecars`, no crash — so `0x10000000` is *not* the problem here). The `docs/building.md` sidecar-gen step was also missing entirely. **Fix = release-side, backlogged** (`docs/BACKLOG.md` → "Alpha release"): bundle the emit scripts + their committed inputs as a pure-stdlib-Python asset-prep tool the user runs once against their own ROM (~5 MB output, no MIPS toolchain). Secondary/latent: the fixed-address `VirtualAlloc((LPVOID)0x10000000)` in `romdata.c` has no working fallback (`"using heap copy — direct ROM reads will fail"` then limps into the same crash) — didn't bite M-34 but will on a machine where something occupies `0x10000000`; harden separately (reserve earliest in `main`, or retry low bases and derive all segment math from the base obtained).
|
||||
|
||||
**Status (from §F table):** Diagnosed, not fixed — release packaging gap + latent `romdata.c` fallback
|
||||
**Status (from §F table):** Diagnosed, not fixed — release packaging gap + latent `romdata.c` fallback. **Fallback tail hardened (2026-09-30, `chore/romdata-valloc`):** on Windows a failed fixed-address `VirtualAlloc` now logs `GetLastError` + a `VirtualQuery` of whatever occupies `0x10000000` and aborts boot cleanly instead of limping into the heap copy and a later AV; success path unchanged; POSIX `mmap` fallback untouched (still degrades)
|
||||
|
||||
## D180 — Native-PC input pass (M-XX QoL run, `qol/native-pc-input-menu`, port-only).
|
||||
|
||||
@@ -11133,7 +11141,7 @@ User played live with `GE_D236ORDER=1` set, walked toward Surface1's tree line,
|
||||
4. **The port's `use_alpha` gate ACCEPTS `0x0c184b50` — pass 18's prime suspect is refuted by static decode.** Against this repo's gbi.h blender layout (`include/PR/gbi.h:720-723`: `GBL_c2` puts c2-modifier at bits 16-17, c2-secondary-colour at bits 20-21 of other_mode_l): the gate (`gfx_pc.cpp` ~line 2386) requires bits 16-17 == `G_BL_1MA`(0) and bits 20-21 == `G_BL_CLR_MEM`(1); the word has byte2=`0x18` → bits 16-17=0 ✓, bits 20-21=1 ✓ → `use_alpha=true`. Full decode: ALPHACOMPARE=NONE, no CVG_X_ALPHA/ALPHA_CVG_SEL, FORCE_BL set, c1=(IN, A=0), c2=(IN, A_IN over MEM, 1MA) = standard src-over alpha blend with A from the combiner (`texSelect` mode-4 2-cycle sets `G_CC_TRILERP`/`G_CC_MODULATEIA2` for IA → A = texel.a × shade.a). No port-side opaque-draw gate explains the wall.
|
||||
5. **Implication / reframed picture:** with the tree class alpha-blended correctly in the port, the consistent story across D276 (visible weave painted by the `0xc81049d8` room-bg noise class), D280 (tree cards in front yet not visibly winning) and pass 15 (tree-class vertex alpha capped ≤36/255 ≈ 14%, bimodal 25/255) is that the tree cards blend at ~10% opacity over the opaque noise wall — nearly invisible, exactly "the wall shows through". The remaining open questions are (a) whether that low alpha is ROM-faithful (doshadow's `cn[3] = envcolour.a × D_800363F0/255`, `D_800363F0` set per-chr from shade alpha at `chr.c:3319`, init 0x50; dogfnegx uses template `D_800363E0`) or a port-fed wrong input, and (b) whether the discrete billboards (`dogfnegx`) actually instantiate on PC at all — its gate inputs (`renderdata->flags & 2`, `rwdata->Gunfire.visible`) are unverified.
|
||||
|
||||
**Next steps (fresh session):** (a) ROM-holding session: run new `tools_pc/scan_op12.py` (added this pass; counts op-12/op-13 nodes per model file, dumps GunfireRecord Offset/Size/Image) to identify which models own Surface 1's tree billboards/shadows and cross-ref against pass 17's D236SH live-record data (32×32 fmt=0 shadow texture); (b) runtime: `GE_D236ALPHA` + `GE_D236RM` on `-level_36`, correlate the tree class's per-vertex alpha with `D_800363F0`/envcolour.a, and compare against N64 reference footage (faint vs solid sprites decides faithful-faint vs port bug); (c) verify `dogfnegx`'s gate inputs on PC (`renderdata->flags & 2`, `rwdata->Gunfire.visible`) — if billboards never fire, the discrete sprites are missing entirely (rule-2 bucket (a)/(b) question); (d) on conclusion remove TEMP probes: `GE_D236RM/ORDER/ALPHA/ZFIX/BT` in `gfx_pc.cpp`, `GE_D236BT` + D236SH dump in `model.c:doshadow`, `port_d236bt_dump()`, `tools_pc/scan_op1{2,3}.py`. Scratch tools: `scratch/d236/{lutdump.c,lutbuild.bat,lutvals.txt,scan_bg_rm.py,rmvals.txt,rmdump.c,rmbuild.bat,rmnames.txt}`.
|
||||
**Next steps (fresh session):** (a) ROM-holding session: run new `tools_pc/scan_op12.py` (added this pass; counts op-12/op-13 nodes per model file, dumps GunfireRecord Offset/Size/Image) to identify which models own Surface 1's tree billboards/shadows and cross-ref against pass 17's D236SH live-record data (32×32 fmt=0 shadow texture); (b) runtime: `GE_D236ALPHA` + `GE_D236RM` on `-level_36`, correlate the tree class's per-vertex alpha with `D_800363F0`/envcolour.a, and compare against N64 reference footage (faint vs solid sprites decides faithful-faint vs port bug); (c) verify `dogfnegx`'s gate inputs on PC (`renderdata->flags & 2`, `rwdata->Gunfire.visible`) — if billboards never fire, the discrete sprites are missing entirely (rule-2 bucket (a)/(b) question); (d) on conclusion remove TEMP probes: `GE_D236RM/ORDER/ALPHA/ZFIX/BT` in `gfx_pc.cpp`, `GE_D236BT` + D236SH dump in `model.c:doshadow`, `port_d236bt_dump()`, `tools_pc/scan_op1{2,3}.py`. (Probes stripped `0503d335`; the two scan tools removed 2026-09-30, strip pass.) Scratch tools: `scratch/d236/{lutdump.c,lutbuild.bat,lutvals.txt,scan_bg_rm.py,rmvals.txt,rmdump.c,rmbuild.bat,rmnames.txt}`.
|
||||
|
||||
**Pass 20 (2026-09-22, ROM-holding session) — pass-19 step (a) executed: full 512-file `scan_op12.py` run; the `dogfnegx`=trees identification is REFUTED by the asset data.**
|
||||
|
||||
@@ -11709,7 +11717,7 @@ User vs 1964: PC water jittered with player movement, "reset" even standing stil
|
||||
1. **Texture geometry (the resets + speed).** The IsWater image is a 32x32 CI8 texture (mask 5 = 32-texel period) loaded with its mip chain (1400 B) and drawn through an RGBA16 tile (the D229 case). fast3d imported it CI8 as **32x43** (mip rows included) while the triangle path normalised UVs with the RGBA16 reading, **width 16** (line bytes / 2) and height 43 (`GE_D229` probe: `texw=16 texh=43`). So S ran at 2x frequency and T repeated every 43 rows, not 32; the port's per-fan S/T fold (multiples of 64 texels) is not a multiple of 43, so every fold step jumped the pattern. Fix (`gfx_pc.cpp`, D229 case only): import only the `line_size * (1<<maskt)` base level, and in the tri path use CI8 geometry (width = line bytes, height = mask period). Headless still-camera A/B (300 frames): old 71 jump spikes, new 0 and a steady per-frame change.
|
||||
2. **s16 vertex quantisation (the jitter).** The N64 draws sky/water as CPU-built RDP triangles with 32-bit S/T; the port routed them through `Vtx` (s16 `tc` needing a 2^k shift, k=4 for water = half-texel steps; s16 `ob` scaled by a fan-wide `wScale` ≈ 7, ~8 px position steps near the camera). New port-only `G_FLOATVTX_EXT` (0x47, `port/include/floatvtx.h`, loader `gfx_sp_vertex_float` in `gfx_pc.cpp`) passes clip-space positions and folded S/T as floats; `skyPortRenderPoly` (`sky.c`, `#ifdef PORT`) uses it for every sky/water fan, so no tile shift or wScale.
|
||||
|
||||
Ruled out on the way: the animation phases advance by `g_ClockTimer` (rate-independent); `PRIM_LOD_FRAC` and per-tile `uls/ult` offsets are handled by fast3d; `WrapFix` pre-wrap is off by default. A/B switches kept: `GE_D245_OLDVTX=1`, `GE_D245_OLDTEX=1`. Golden gate 3/3; Windows + Linux builds clean.
|
||||
Ruled out on the way: the animation phases advance by `g_ClockTimer` (rate-independent); `PRIM_LOD_FRAC` and per-tile `uls/ult` offsets are handled by fast3d; `WrapFix` pre-wrap is off by default. A/B switches kept: `GE_D245_OLDVTX=1`, `GE_D245_OLDTEX=1`. Golden gate 3/3; Windows + Linux builds clean. **2026-09-30: removed** (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`). Both A/B switches and the pre-fix s16 sky vertex path they selected (plus its `GE_D227V` probe) are gone; the float path is unconditional.
|
||||
|
||||
**Follow-up (2026-09-27): coloured dashes.** User playtest found rows of yellow/green/magenta dashes on Frigate's water, looking down near the horizon. The base-level crop in fix 1 had never run: its test used `loaded_texture.line_size_bytes`, which for a LoadBlock is the whole 1400-byte block, so the upload stayed 32x43 while the tri path normalised V by 32. The mip-chain rows (32-42) were sampled as garbage texels. Crop test now uses the tile's line size (the importer's row width): the dumped import is a clean 32x32 tile (indices 0-23). User-verified, golden gate 3/3.
|
||||
|
||||
@@ -11847,7 +11855,18 @@ Superseded history below (the original OPEN investigation, root cause not found
|
||||
|
||||
**Steam Deck first-run preset (D-unnumbered in `port/src/video.c`/`main.c`, `videoApplySteamOSDefaults()`) did not apply for a user who added the game via Steam in Desktop Mode and switched to Game Mode afterward — resolution stayed at some non-native value instead of the Deck's native 1280×800 (v0.2.1 user testing, 2026-09-15).** Root mechanism, confirmed by code inspection: the preset only applies when `getenv("STEAMOS")` is truthy AND no `ge007.ini` exists yet (`main.c:126-129`, before `configLoad()`); once any ini exists, it always wins over the preset on every future launch (by design, so user F10 changes persist). **Leading hypothesis (not verified on real hardware in this environment):** SteamOS's Desktop Mode (a normal Plasma/X11 session) likely does not export the same `STEAMOS` env var that the Game Mode/gamescope session does, so a Desktop Mode test-launch (a natural step when adding a non-Steam game via its file browser) creates `ge007.ini` with ordinary Linux defaults *before* the user ever reaches Game Mode — permanently skipping the preset for that install, since the ini already exists on the first real Game Mode launch. This exact caveat was already known internally (`docs/HANDOFF.md`: "a Deck that already has an ini needs `-fresh` or F10 to pick the preset up") but was never surfaced in user-facing docs (README/release notes) — same class of gap as the stale D230 release-notes reference. **Workaround (lighter than `-fresh`, which also wipes the save):** F10 → Resolution → 1280×800 manually; takes seconds, no data lost.
|
||||
|
||||
**Status (from §F table):** OPEN — mechanism understood, root cause (Desktop-vs-Game-Mode `STEAMOS` env var difference) unverified on real hardware; user-facing workaround documented in README/release notes.
|
||||
**Status (from §F table, before the fix):** OPEN — mechanism understood, root cause (Desktop-vs-Game-Mode `STEAMOS` env var difference) unverified on real hardware; user-facing workaround documented in README/release notes.
|
||||
|
||||
**2026-09-30 fix (port-only; `port/src/video.c`, `port/src/main.c`, `port/include/video.h`):** the preset no longer depends on `STEAMOS` or on the ini being absent.
|
||||
- **Detection by hardware:** `videoDetectSteamDeck()` reads `/sys/class/dmi/id/{board_vendor,sys_vendor,product_name}` (`#if defined(__linux__)`): vendor `Valve` + product `Jupiter` (LCD) or `Galileo` (OLED). `STEAMOS` stays as an extra signal. `GE_FAKE_DECK=1` forces the Deck path on any host, `=0` forces it off (docs/dev/GE-ENV-PROBES.md).
|
||||
- **Runs after `configLoad()`** (`videoApplySteamDeckPreset()`, replacing the pre-load `videoApplySteamOSDefaults()`), so an ini created by an earlier Desktop Mode launch is seen and handled.
|
||||
- **Once per ini, without clobbering choices:** new key `Video.DeckPresetApplied` (0/1, default 0). On a detected Deck with the flag 0: if `Video.Fullscreen=0` and `Window.Maximized=0` (the generic windowed state — `Window.Width/Height` can't be used as an "untouched" test, `videoSaveWindowState` rewrites them from the live window on every clean exit) it sets fullscreen + 1280x800; otherwise it leaves the display alone. Either way the flag becomes 1 and the ini is saved, so later F10 choices always win. A Desktop Mode user who deliberately ran windowed gets fullscreen once and can switch back in F10 (it then sticks).
|
||||
- **Preset narrowed to display keys.** The old preset also wrote VSync 1 / MSAA 2 / draw+LOD 250; those are the generic defaults now, so writing them again on an existing ini could only clobber a user's choice. Dropped.
|
||||
- Non-Deck hosts: behaviour unchanged except the new key is added to the ini once (the normal "newly-registered keys" rewrite).
|
||||
|
||||
**Verified (Windows, `GE_FAKE_DECK`, throwaway data dir, `GE_QUITFRAME=60`):** (1) windowed 960x540 ini, no env → no Deck log, `DeckPresetApplied = 0`; (2) same ini + `GE_FAKE_DECK=1` → `applying Deck preset (fullscreen, 1280x800)`, ini `Fullscreen = 1`, `Width = 1280`, `Height = 800`, `DeckPresetApplied = 1`; (3) then `Fullscreen = 0` hand-set + `GE_FAKE_DECK=1` → `preset already considered`, stays windowed; (4) ini with `Fullscreen = 1`, 960x540, no flag + `GE_FAKE_DECK=1` → `display already user-set ... preset skipped`, size kept, flag set. Windows build clean; the `__linux__` DMI path syntax-checked with WSL Ubuntu-24.04 gcc (`-fsyntax-only`, `video.c` + `main.c`, no new warnings). **Not verified:** a real Linux build/run and real Deck hardware (DMI strings, Desktop→Game Mode flow). Human check on a Deck: delete `Video.DeckPresetApplied` from an existing windowed ini (or start from a Desktop-Mode-created one), launch in Game Mode, expect fullscreen and the `video: Steam Deck (DMI Valve Jupiter/Galileo)` log line. README "Steam Deck" section still describes the old first-run-only behaviour (lead to update).
|
||||
|
||||
**Status:** FIXED (port-only; `GE_FAKE_DECK`-verified on Windows; real-Deck check owed).
|
||||
|
||||
## D287 — User-confirmed real, non-self-recovering freeze on Windows (M-141, 2026-09-15/16) after ~15-20 minutes with no input, triggered by clicking back into the…
|
||||
|
||||
@@ -11886,7 +11905,7 @@ Superseded history below (the original OPEN investigation, root cause not found
|
||||
|
||||
**Status (from §F table):** OPEN — visually confirmed and precisely bounded (frames ~562-741 of a bare `-level_20` boot); M-152 narrowed to two live candidates (fast3d backface-cull NaN leak, or the D106/D271 portal-BFS family) via static review; two M-155 probe runs (ungated, then frame-gated) were both inconclusive — the `any_behind_camera=1` case's bbox math is a false lead, and the `any_behind_camera=0` subset found no match for the target color. Root cause still not identified; a live human repro is now the more promising next step over further headless probing. Promoted from the long-parked `GRAPHICS-BACKLOG.md` D171 entry with real evidence.
|
||||
|
||||
**v0.4.0 re-check (2026-09-23, native Windows, current `release/v0.4.0` build): does NOT reproduce.** Bare `-level_20` at 640x480 with a defaults ini, `GE_PCDUMP` 400–1200 every 25 frames, then 500–950 every 5 (91 frames covering the whole rocket pan and the cut to first person). A pixel scan for the reported flat `RGB(83,62,44)` ±6 finds only frames 825–840, and those are Bond's own head fading out as the camera pushes into first person (normal GE behaviour, eyeballed). No stray triangle anywhere in the window. **D271 A/B:** temporarily restoring the pre-D271 portal-bounds limit (1e5 instead of 1e38, `bg.c`) gives the identical result, so the D271 portal-culling fix is not what removed it. The M-155 probe runs (2026-09-16) had already missed the colour, so it has not reproduced headlessly since it was written up. Either it was fixed incidentally before 09-16 (not identified), or it is intermittent, which fits the user's "randomly". **Next:** the playtest brief item (play Silo normally, F12 if seen). If it isn't seen, close as not reproducible.
|
||||
**v0.4.0 re-check (2026-09-23, native Windows, current `release/v0.4.0` build): does NOT reproduce.** Bare `-level_20` at 640x480 with a defaults ini, `GE_PCDUMP` 400–1200 every 25 frames, then 500–950 every 5 (91 frames covering the whole rocket pan and the cut to first person). A pixel scan for the reported flat `RGB(83,62,44)` ±6 finds only frames 825–840, and those are Bond's own head fading out as the camera pushes into first person (normal GE behaviour, eyeballed). No stray triangle anywhere in the window. **D271 A/B:** temporarily restoring the pre-D271 portal-bounds limit (1e5 instead of 1e38, `bg.c`) gives the identical result, so the D271 portal-culling fix is not what removed it. The M-155 probe runs (2026-09-16) had already missed the colour, so it has not reproduced headlessly since it was written up. Either it was fixed incidentally before 09-16 (not identified), or it is intermittent, which fits the user's "randomly". **Next:** the playtest brief item (play Silo normally, F12 if seen). If it isn't seen, close as not reproducible. The `GE_D288` probe was**removed 2026-09-30** (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`).
|
||||
|
||||
**2026-09-25 playtest (user, Windows, one ~46-min session Silo → Caverns, `GE_D322=1 GE_D204=1`, build `7baf4900`):** user reports the Silo intro clean. Closed; the specific fixing commit was never isolated.
|
||||
|
||||
@@ -12078,7 +12097,7 @@ This is a **perfect, smooth white-hot → yellow → orange → deep-red → fad
|
||||
- **Fix:** learn which compiled `G_SETTIMG` slots are IMAGESEG references on the first sync (marker still present), store them in `s_imgslot[][]`, and re-copy those slots on every call. Semantics now match N64 (fresh pointers per stage). Port-only; no game code touched.
|
||||
- **Verified:** user playtest, boot → title → Facility → past the lockers, grenades/mines/tanks all correct ("yeah that fixed it"). `D252SYNC` (TEMP, `GE_D252POOL`) logged `32 texture slots re-synced, 32 changed` at the Facility load, i.e. all 32 were stale before the fix. Golden gate 3/3.
|
||||
- **Scope:** covers every D252-family report that goes through these 17 global DLs (explosion parts, smoke, debris; `globalDL_0x000` too). The M-124/M-157 bullet-impact spark sightings are likely the same stale-pointer effect but were not separately re-tested.
|
||||
- **TEMP probes to remove when the release settles:** `GE_D252POOL` (`image.c`, `initmttex.c`, `D252SYNC` in `gimgfixup.c`); `GE_D157` F12 burst (`gfx_pc.cpp`, `video.c`); `GE_D320R explode=/cheat=`.
|
||||
- **TEMP probes to remove when the release settles:** `GE_D252POOL` (`image.c`, `initmttex.c`, `D252SYNC` in `gimgfixup.c`); `GE_D157` F12 burst (`gfx_pc.cpp`, `video.c`); `GE_D320R explode=/cheat=`. **2026-09-30: removed** (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`). All three, incl. the whole D157/D157T/D157I/D157TB family.
|
||||
|
||||
**Status: FIXED (M-201, user-verified 2026-09-26).**
|
||||
|
||||
@@ -12604,7 +12623,7 @@ This confirms the M-160/M-164/M-165 hypothesis (`## D173`'s own consolidated-nex
|
||||
|
||||
**Scope note (§7 drift check):** v1's sign-off scope ("exactly one AI list affected") was wrong in one respect — PRINT records also exist in level-local lists, but with a different shape (embedded string), where the original NUL-scan is already correct. v2's *behavioral* change is still confined to global-list PRINTs: `m_RunToBondPersistent`'s two records. Local-list behavior is byte-identical to the original decomp — Trevelyan reverts to his pre-fix (working) pathing. Drift disclosed to user in-thread when the regression was reported; same sign-off intent (fix global PRINT sizing, no other behavior change).
|
||||
|
||||
**Cross-refs:** D309 (this fix neutralizes its spin mechanism — the teardown-ordering open question in D309's "why PC and not N64" remains, but reaching that state is now harmless); GE_D309 probes (`chrai.c` `ai()`/`chraiGoToLabel`, `chraction.c` `chrGoToBond`) remain as diagnostic tools.
|
||||
**Cross-refs:** D309 (this fix neutralizes its spin mechanism — the teardown-ordering open question in D309's "why PC and not N64" remains, but reaching that state is now harmless); GE_D309 probes (`chrai.c` `ai()`/`chraiGoToLabel`, `chraction.c` `chrGoToBond`) remain as diagnostic tools. **2026-09-30: removed** (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`).
|
||||
|
||||
**Perf gauge (2026-09-19, release review — M-192 item 6):** v2's up-front `chraiGetAIListID()` call in `chraiGoToLabel` (added so the sizer knows global-vs-local) runs on EVERY label jump; originally that lookup ran only in the rare EndList-not-found branch. Cost per call: ≤ (level-local lists + ~20-entry `g_GlobalAILists`) pointer comparisons ≈ tens of ns. Worst case for a heavy level (Train/Archives, ~50 active guards each executing 1–2 goto-family records per tick): ~1000 calls/tick × ~40 comparisons ≈ 40 µs/tick — under 0.3% of a 16.7 ms frame, and two orders of magnitude below the D250 `getenv` anti-pattern (which was per-TRIANGLE, tens of thousands of calls per frame). **No change warranted; re-measure only if an AI-heavy level shows a regression.**
|
||||
|
||||
@@ -13488,7 +13507,7 @@ Logs: `scratch/d318b_{fac,ctrl}_clk{1,2}.log`. Summariser: `scratch/d318b_summ.p
|
||||
|
||||
**Fix applied (2026-09-23, RULE-2-SIGNOFF: user approved option (c)).** `src/game/model.c` `modelSetAnimFrame2WithChrStuff`, `#ifdef PORT`: when the per-frame loop did not run, progress was requested, and `vb == va`, re-derive `frameb = constrain(va±1)` against the CURRENT endframe (as `modelSetAnimFrame` does); applied only if that next frame is unclamped. Genuine holds, anim ends and looping wraps are untouched. Env: `GE_D329=0` disables (A/B), `GE_D329LOG=1` logs triggers (cached getenv). **Verified (WSL, lockstep):** clk=1 with watchdog off (`GE_D318W=0`): Facility ai_19 and Control ai_9 no longer pin (frame 19→19.5→20…, 0.5/tick = the N64 clk=2 trajectory; aim→fire→re-aim cycle ≈192 ticks vs ≈195 at clk=2/3); authored holds intact. Trigger census: clk=1 Facility, 9 triggers, all on the repro guard, one per attack update; **clk=2: 0 triggers** over ~5300 ticks (Facility) and ~3600 (Control), so the fix is inert in the N64 regime. **Windows-native validation (2026-09-23):** Facility forced repro, native exe, `GE_D318B_CLK=1`, watchdog off: 48 post-update fire episodes, **0 pinned**, 23 D329 triggers. Bunker 1 golden (640x480 pinned, direct launch): REGRESSION with the fix ON (worst dmean 113.6) and equally with it OFF (`GE_D329=0`, 126.4), so it predates this fix (stale golden; re-baseline already owed). **Interactive playtest (2026-09-23, user, native Windows, shipping config + GE_D329LOG):** Ourumov reacts with N64-matching timing (user-confirmed on screen). Log: 4 D329 triggers (four guards, same tick, stale-pin state, released at 0.5/tick), 3 watchdog detections that cleared on their own, 0 watchdog rescues, no crash (`scratch/d329_playtest.log`).
|
||||
|
||||
**Status:** FIXED + playtest-verified (2026-09-23). Next: demote the D318 watchdog to detect-and-log post-release (M-192 item 7), then remove the TEMP probes (d318bprobe, d320repro). Watchdog stays shipped on `release/v0.4.0`; plan to demote it to detect-and-log after playtests. Cross-ref: D318, D320, D321, M-199, D155, D186/D193.
|
||||
**Status:** FIXED + playtest-verified (2026-09-23). Next: demote the D318 watchdog to detect-and-log post-release (M-192 item 7), then remove the TEMP probes (d318bprobe, d320repro). **2026-09-30: done** (strip pass, `chore/strip-temp-probes`): `d318watchdog.c` is now detect-and-log only (no `sub_GAME_7F025560` re-seed); `d318bprobe.c` (incl. `GE_D318B_CLK`) and `d320repro.c` + the `lv.c` hook deleted. Watchdog stays shipped on `release/v0.4.0`; plan to demote it to detect-and-log after playtests. Cross-ref: D318, D320, D321, M-199, D155, D186/D193.
|
||||
|
||||
## D330 — #103 (v0.3.0, Win 11): Auto-Aim / Look Ahead "won't stay on" in the in-game watch options (2026-09-23)
|
||||
|
||||
@@ -15082,7 +15101,8 @@ not edited for the probe. Live RMB/MMB test and release gate remain open.
|
||||
|
||||
## D387 — M7 AllUnlocked read-time patches leak into persistent saves (2026-09-27)
|
||||
|
||||
**Status: OPEN; M7 cannot be signed off as a preservation-only playtest.**
|
||||
**Superseded by D442 (2026-09-30): the save patch and this merge machinery were removed entirely; AllUnlocked is now a pure RAM/query-time override.**
|
||||
**Status: FIXED 2026-09-30 for saves made from now on (see "D387 Fix" at the end of this file); fakes persisted by earlier builds are not repaired. Original diagnosis follows unchanged.**
|
||||
User offered `data/` for investigation. Original `data/ge007.ini` and
|
||||
`data/ge007.eep` were **read only**, checksums unchanged after all runs.
|
||||
Private isolated test folders live under
|
||||
@@ -15663,3 +15683,231 @@ Probe (`GE_D428`) showed the hook firing with the input clamped at 0.7 on most f
|
||||
Under PORT `struct image_entry` puts `dataoffset:24` in bits 0-23 (D67), so byte 0 is the LOW byte of the data offset. On the N64 (big-endian) byte 0 is the MSB of word 0, the hit type. `chrprop.c` (impact_sounds ~1154, the explosion skip test ~1187) and `propobj.c` (~9685) used the raw byte, so on PC they indexed `g_HitTypeSounds[]` with an arbitrary nibble 0..15 (table has 13 entries; 13-15 read past it) and mis-tested the 5/6 spark skip. Harmless while `bghit.texturenum` was always -1 (D154); D431 now returns real ids for light fixtures, activating the path. (The Caverns playtest passed, so the light images' nibbles were presumably in range: luck, not design.) The comment in `image.h` claiming the N64 does the same was wrong and is corrected.
|
||||
**Fix (`#ifdef PORT`, layout-driven, semantics-preserving):** use the `.hitTexture` bitfield at the three sites, as `chr.c` already does. **General rule (for porting-notes):** un-stubbing a `-1` sentinel (D154/D135/D431) re-activates every dormant consumer of the value -- grep all readers and check each for raw-layout reads and table-bounds assumptions first.
|
||||
**Status:** FIXED; built. Live check owed (shoot light fixtures across levels; impact sound/sparks on lights).
|
||||
|
||||
## D440 — "Original N64" preset + "Port defaults" inverse (ROADMAP §5a, 2026-09-30)
|
||||
|
||||
**What:** two one-press action rows, "Original N64 preset" and "Port defaults preset", at the end of the GRAPHICS section (before its Reset row). They are rows in the shared `optionsoverlay.c` row table, so they appear in BOTH the F10 overlay and the front-end PC Options screen (`frontoptions.c` renders the same table; GRAPHICS now paginates to a second page there, the D406 paging). Activation reuses the D356 reset-row contract (edge-triggered arm -> "Confirm" -> apply within 3 s; held-repeat never fires it). The value column shows "Active" while every preset key already holds that preset's value. **An action, not a mode:** it writes the existing config keys (live-applied through `videoRequestLiveConfigForKey`, persisted on overlay close / clean exit like any F10 edit); no new ini key, so existing inis behave exactly as before unless a preset is pressed. D372's `Video.LowEndMode` flag no longer exists in tree (only the tree probe's "no M5" check references it), so there was no preset plumbing to reuse; the new generic hook is `configSetValue`/`configGetValue` (`port/src/config.c`, clamped like a file load).
|
||||
**CLI route:** `-n64preset` / `-portpreset` apply the preset once at the top of `videoInit` (after `configLoad`, before any value reaches fast3d) and persist via the atexit save. Not added to `--help` (the help text lives outside this change's file partition). An "ini route" is just the keys below.
|
||||
|
||||
**Key audit** (every registered key; table lives in `port/src/video.c` `kVideoPresets`):
|
||||
|
||||
| Key | Port default | N64 value | In preset |
|
||||
|---|---|---|---|
|
||||
| Video.TextureFilter | 1 bilinear | 2 (3-point) | yes |
|
||||
| Video.Anisotropy | 4 | 1 (RDP has none) | yes |
|
||||
| Video.NativeWidescreen | 1 | 0 (4:3 projection) | yes |
|
||||
| Video.WidescreenAuto | 1 | 0 (stock vertical FOV) | yes |
|
||||
| Video.SafeAreaCrop | 1 | 0 (see below) | yes |
|
||||
| Video.DrawDistance / LodDistance | 250 / 250 | 100 / 100 | yes |
|
||||
| Video.DrawDistanceAutoFov / LodDistanceAutoFov | 0 / 0 | 0 / 0 | yes (identity guard) |
|
||||
| Video.FovScale, Game.HudScale | 100, 100 | 100, 100 | yes (identity) |
|
||||
| Game.ScreenShakeIntensity, Game.NoHitFlash | 1.0, 0 | 1.0, 0 | yes (identity) |
|
||||
| Video.CrosshairHide/Color/Size/Style | 0/0/100/0 | same | yes (identity) |
|
||||
| Input.AimMode | 0 (N64) | 0 | yes (identity) |
|
||||
| Input.AimRange | 0 (PC) | 1 (N64 limits, D338) | yes |
|
||||
| Culling | — | — | no key: frustum planes / fog-LOD scale (D222) and the room pool (D294 `portRoomPoolScale`) derive from FOV, widescreen, draw and LOD distance and are identity at the values above |
|
||||
| Video.MSAA, VSync, FpsCap | 2, 1, 60 | n/a | **no** — performance/pacing; FpsCap 30 throttles the sim (D186), not faithful |
|
||||
| Video.Fullscreen, Window.* | — | n/a | **no** — display choice |
|
||||
| Video.FixMipTextures, DetailBaseTile, WrapFix | 1, 1, 0 | — | **no** — accuracy fixes that move output toward N64 |
|
||||
| Video.CrosshairRed/Green/Blue, DisplayFPS | — | — | no (only used with Custom colour / overlay) |
|
||||
| Game.SkipIntro, Game.AllUnlocked | 0, 0 | 0, 0 | **no** — progression/convenience, not presentation (AllUnlocked has save implications, D387) |
|
||||
| Input.* feel (sensitivities, deadzones, smoothing, mouse model AimAbsolute/MouseDirectLook/NaturalPitch, CrouchMode), Input.Bind.*, Bond.* (per-save), Audio.*, Debug.* | — | — | **no** — spec: bindings/volumes untouched; mouse modes have no N64 counterpart |
|
||||
|
||||
**Overscan — which is faithful:** `SafeAreaCrop=0`. The N64 VI scans out the whole 640x480 framebuffer including GE's black safe-area border; a CRT's overscan hid part of it but by a set-dependent amount, and emulators/1964 show the full frame. The crop (`gfx_pc.cpp` `gfx_adjust_viewport_or_scissor`) is a port-side remap that stretches the inset gameplay viewport to the window (and trims the D246 1-unit side margin), so 0 = the signal the console output.
|
||||
**4:3 limitation:** the preset gives a 4:3 *projection*; in a non-4:3 window it is stretched to the window (as the N64 image is on a 16:9 TV set to stretch). A pillarboxed gameplay mode does not exist in fast3d today — follow-up for the ROADMAP (lead).
|
||||
|
||||
**Verification (Windows, NTSC, 960x540 window, `-level_09`):**
|
||||
- Port-default ini, `GE_PCDUMP=880-880` vs same with `-n64preset`: log `video: applied Original N64 preset (8 value(s) changed)`; the N64 frame shows the black safe-area bars + D246 side margin, the 4:3 projection stretched (narrower horizontal FOV than the Hor+ port frame) and 3-point-filtered textures.
|
||||
- ini round-trip: after the `-n64preset` run the ini differs from the port-default ini in exactly the 8 keys (TextureFilter 2, WidescreenAuto 0, NativeWidescreen 0, DrawDistance 100, LodDistance 100, Anisotropy 1, SafeAreaCrop 0, AimRange 1); a following `-portpreset` run loaded those values (8 changed) and wrote an ini byte-identical to the original.
|
||||
- UI dispatch: `GE_PRESETPROBE=1 GE_OPTIONSOVERLAY=4` drives both rows through the real arm -> confirm path: PASS (value "Confirm" when armed, "Active" after, other preset inactive).
|
||||
- `GE_OPTIONTREEPROBE` reports FAIL both with and without this change (baseline build from the same commit) — pre-existing, not caused by the new rows (GRAPHICS is not in its page list).
|
||||
**Not verified:** live mouse/pad press in either UI, draw-distance effect on an open level (DrawDistance is read at fog load, so it applies from the next level load), PAL/JP, Linux build.
|
||||
|
||||
**Status:** FIXED (port-only; headless-verified; user live accept owed).
|
||||
|
||||
## D441 — 64-bit pointer-width sweep, part 1 (ABI/layout exception, 2026-09-30)
|
||||
|
||||
**Why:** harmless today because the arena sits low (`0x10000000`) and the PE image base is `0x140000000` with a 4 GiB-aligned `g_pc_animdata_base` (D34), but each `(u32)ptr`/`(s32)ptr` cast truncates a host pointer and would fault once the heap or arena is above 4 GB (macOS/ARM64 or ASLR; #88/#95/#101). Same class as porting-notes §A1 / D255; permitted by the AGENTS.md ABI/layout exception.
|
||||
|
||||
**What (recovered from an agent worktree after a rate-limit crash, then reviewed hunk by hunk):** 17 files, +337 lines, every hunk `#ifdef PORT ... (uintptr_t) ... #else <original, byte-identical> #endif`: `bg.c` (`bgBuildRoomVtxBounds`, `bgTestRayIntersectionInRoom` x2, `bgRoomCalcBB` x2), `bondhead.c`, `bondview2.c` (anim table bases, `mp_respawn_handler` intro-record walk, `hudmsgBottomRender` blackbox call), `bondview_r.c` (intro-record walk), `chraction.c` (animation table bases), `explosion.c`, `front.c` (`load_walletbond`), `image.c`, `image_bank.c`, `initBondDATAdefaults.c`, `initactorpropstuff.c`, `prop.c` (`proplvreset2` setup-file rebase, 20 sites), `ramromreplay.c`, `stan.c` (`stanDetermineEOF` with `intptr_t delta`), `title.c`, `libultra/audio/cseq.c`, `music.c`.
|
||||
|
||||
**Review notes (checked, not just trusted):**
|
||||
- `(s32)&ANIM_DATA_x + (uintptr_t)&ptr_animation_table->data`: the `(s32)` is intentional. D34 defines `ANIM_DATA_x` as an lvalue at a 4 GiB-aligned base + offset, so its low 32 bits ARE the segment offset. Only the base was widened; the offset is non-negative so the sign extension is harmless.
|
||||
- `bondview2.c` `draw_blackbox_to_screen(arg0, &view_left, ...)`: the prototype takes `s32 *` and the four locals are `s32`, so passing the real pointers replaces a truncating int-to-pointer conversion.
|
||||
- `explosion.c`: `g_FlyingParticlesBuffer + i` on a `struct FlyingParticles *` is the same scaled pointer arithmetic as the original (`(struct FlyingParticles *)(u32)ptr + i`).
|
||||
- `front.c`: keeps the original low-24-bit trick on `srecord->Primary`; semantics unchanged (not independently verified beyond that).
|
||||
- `prop.c`, `stan.c`, `bg.c`: rebase arithmetic on the RAM copy (`local_stage`, `newBase`, `ptr_bgdata_room_fileposition_list`) now done in `uintptr_t`. The `bg.c` sites `bgRoomCalcBB` and `lightFindVertexBaseForTri`-adjacent code overlap outside PR #107; this landed version is ours, so #107 will conflict and needs a rebase or closure.
|
||||
|
||||
**Verified:** clean build; pointer-to-int-cast warning sites in the 17 files 279 -> 185 (94 removed); `-level_09` and `-level_20` short runs (`GE_QUITFRAME=420`, `GE_PCDUMP` frame 400) exit 0, no crash lines, frames render correctly (Bunker intro camera; Silo guards + intro caption). No frame diff was run (the intro camera is non-deterministic, D117).
|
||||
|
||||
**NOT done (owed):** the full ~396-site census with per-site provenance classification (H real hazard / R ROM-address 32-bit / I integer-only / ?), the audit table `docs/dev/ABI-PTRCAST-AUDIT.md` (the agent that started it died before writing it), the `GE_HIGHARENA` high-address test mode, and review of #107/#108 (still the #108 range-check concern in ROADMAP §2). 185 pointer-cast warning sites remain in the touched files; many are intentional (`(s32)&ANIM_DATA_*` offsets, D34). Nothing was tested with an arena above 4 GB.
|
||||
|
||||
**Status:** PARTIAL (part 1 landed; census owed).
|
||||
|
||||
### D387 Fix (2026-09-30)
|
||||
|
||||
**The mechanism stays in the game, not in the file.** `geEepromRW` (port/src/libultra.c) still hands the game the AllUnlocked view at the block-4 read, but now remembers per slot the RAW bytes (`s_d387Raw`, what is on disk) and the SEEN bytes (`s_d387Seen`, what the game got). On every whole-slot write (`block = 4 + 12*slot`, 96 bytes) `geD387MergeSlotWrite` persists, for each cheat byte and each 10-bit completion time, the RAW value when the game left the field equal to what it saw, and the game's value when it changed it (a new record, a folder reset or delete). All other bytes come from the write. The game CRC is regenerated over the merged slot (the same `fileGenerateCRC` call the patch uses). The merge keys off "a patched read happened this process" (`s_d387Valid`), not the live flag, because the game keeps the patched view in RAM after the option is toggled off; a read with the option off clears it.
|
||||
|
||||
**Slot relocation (found by the test, fixed):** the game moves saves between slots (a folder's first completion relocates its data to a lower slot; copy-folder duplicates one). Comparing a write to the shadow of its own slot index dropped an earned cheat when slot 4's data was written to slot 0. The merge now picks the source shadow the write differs from least (own slot wins ties) and logs `eeprom: D387 slot N write descends from slot M's view`. A delete or reset differs from every view about equally, and every changed field is persisted as written anyway, so the choice does not matter there.
|
||||
|
||||
**Verification** (isolated dirs under the scratchpad, never the real `data/`; seed = a copy of `data/ge007 copy.eep`, where slot 4 is folder 0 with two real times and one earned cheat; a scratch-only probe, never committed, drove the game's own save APIs from the front-end tick):
|
||||
|
||||
| Scenario | Result |
|
||||
|---|---|
|
||||
| settings write (`GE_WSPROBE_FRONT`), ON vs OFF | EEPROM byte-identical (before the fix, ON persisted `ffff0f` cheats and 0x3FF fills) |
|
||||
| genuine record via `fileUnlockStageInFolderAtDifficulty` (level 3, diff 0, 300 s) with the slot 4 to 0 relocation, ON vs OFF | byte-identical after the relocation fix (before it, ON lost the earned cheat `040000`) |
|
||||
| delete folder 0, ON vs OFF | byte-identical |
|
||||
| copy folder 0, ON vs OFF | not comparable: with ON every folder reads as completed (synthetic 0x3FF), so `fileCopyFolderToFirstFree` finds no empty folder and writes nothing. This is AllUnlocked's own behaviour, not a save leak. |
|
||||
| ON result, then OFF restart | all five slots validate `OK`, the EEPROM is unchanged by the restart, no 0x3FF fakes |
|
||||
| fresh (no EEPROM) settings write, ON vs OFF | no fakes either way. ON leaves the four untouched slots zero-filled, the normal state of a new file; an OFF boot then normalises them to the same bytes as a plain OFF run. |
|
||||
|
||||
**Known limitations:** (1) a cheat earned while ON is not persisted: its bit is already set in the synthetic view, so a write cannot show it changed. It is re-earned while OFF. (2) A first completion at or above 1023 s (the synthetic 0x3FF value) is not seen as a change. (3) If the game ever writes the destination slot after the source slot of a relocation, the source view would already be gone (the observed order is destination first). (4) Copy-folder is a no-op while ON. (5) Toggling the option takes effect at the next launch either way (the save is patched at read time), so the row now reads "ON (restart)". (6) Fakes persisted by earlier builds have no provenance and are not repaired: back up `data/ge007.eep` before first use; if a save already contains them, the only clean reset is the in-game file delete for that folder.
|
||||
|
||||
**Prior art:** the D387 agent that died in the 2026-09-30 rate-limit crash left a 302-line staged design in its worktree (`d387-agent-staged-WIP.patch` in the notes archive) that additionally replays the game's completion and cheat-award rules to persist cheats earned while ON, and normalises invalid slots. It was never built or tested. If limitation (1) matters, start there; this fix deliberately stays smaller and testable.
|
||||
|
||||
**Status:** FIXED (port-only; matrix passed); user live accept owed (play a mission ON, then OFF).
|
||||
|
||||
## D442 — Game.AllUnlocked reworked to a pure RAM override; save patch + D387 merge removed (2026-09-30)
|
||||
|
||||
**Status: RULE-2-SIGNOFF** (maintainer-granted opt-in port feature, edit confined to `#ifdef PORT` in `src/game/file2.c`). Maintainer, 2026-09-30: "All unlock - want that to allow for all levels/cheats to be unlocked, don't really care how we do it if debug/overrides are better than what we currently have" and, on the proposed hook design, "new plan sounds great". Runtime-verified (falsifier passed, first hook); second hook build-verified only, maintainer to check level select by eye.
|
||||
|
||||
**Change.** `fileGetIsCheatUnlocked` (`src/game/file2.c`, the D225 `#ifdef PORT` block) now also returns TRUE when `portAllUnlocked` is nonzero. Mission-select cheat gating (file.c) and the cheat menu (front.c `frontCheckIfCheatIsUnlocked`) both route through it, so no save byte is read or written for the option. Levels and 007 mode stay on the game's own debug flags seeded in `port/src/main.c`. In `port/src/libultra.c` removed `geEepromPatchAllCheats`, `geSaveGet/SetTime`, the `ge_save_slot` mirror, and the D387 `s_d387Raw/Seen/Valid` + `geD387MergeSlotWrite` machinery; `geEepromRW` now passes raw bytes both ways (D297 legacy-CRC migration kept). Fixes D387 (leak), the cheats-earned-while-ON limitation, and D259/D281 (the fresh-save zero-slot handling lived only in the patch; the game's own BLANKSAVEDATA reset now runs as on a stock build).
|
||||
|
||||
**Verification.** AllUnlocked=1, `GE_WSPROBE_FRONT=600 GE_QUITFRAME=780`, slot 0 with real data: the eep diff is only the slot checksum (bytes 32-39) and music_vol (byte 42, the probed change); cheat bytes and all completion-time bytes identical. Log shows `all-unlocked: RAM unlock flags seeded`. Not verified by eye: cheat menu population.
|
||||
|
||||
**Second hook (added same day, maintainer-approved).** Maintainer, 2026-09-30: "Decision - yes this is a requirement for the feature to be feature complete. Imagine the normal cheat menu / all unlocked which lets you select any level obviously." `fileIsStageUnlockedAtDifficulty` (`file2.c`, top of function, `#ifdef PORT`) returns STAGESTATUS_COMPLETED for every in-range level/difficulty when `portAllUnlocked` is set (query only; save untouched). Reason: the removed patch's 0x3FF times made every stage COMPLETED, and the debug flags only give UNLOCKED, while several gates compare against COMPLETED: `front.c:3182-3200` (level select with an SP cheat active), `front.c:5015` (extra MP characters), `cheat.c:1415` (level-unlock cheat counting), and via `file2.c:1003-1028` the Magnum / Laser / Golden Gun cheat rows (`front.c:1134-1140`). Callers checked: front.c 3164, 3189, 4226, 5015; cheat.c 1415; file2.c `fileGetHighestStageUnlockedForFolder`, the Cradle/Aztec/Egypt completed helpers and their AnyFolder variants. No gate bypasses the hook for these purposes; the direct `fileGetSaveStageCompletedForDifficulty` users (`fileGetHighestStageAndDifficultyCompleted`, `fileIs007ModeUnlocked` file2.c ~1486) are not needed (007 mode comes from `get_debug_007_unlock_flag`, front.c:3182).
|
||||
Side effects beyond unlocks (read from code): `fileGetHighestStageUnlockedForFolder` now returns Egypt; the AnyFolder helpers (`front.c:8056,8074,8197,8203`) now report Cradle/Egypt completed, so the intro cinematic may pick the Cougar Magnum, Oddjob and Baron Samedi as if the campaign were finished. The mission best-time text is unaffected (it reads the raw time getter, which shows blank for 0). No save write.
|
||||
|
||||
**Stale references** (not touched, outside allowed files): `port/src/optionsoverlay.c:97-103` comment, `port/src/watchsettings.c:216,546` comments still cite the removed patch. Fakes already persisted by earlier builds are not repaired.
|
||||
|
||||
**Confidence:** high on the removal and the eep-diff result; medium on the UI outcome until eyeballed.
|
||||
|
||||
|
||||
## D443 — D357 settings-display cleanup + "Restart game" action (2026-09-30)
|
||||
|
||||
**Scope (port-only, no `src/game` change, no config storage/default change):** the shared row table in `port/src/optionsoverlay.c` drives BOTH the F10 overlay and the front-end PC Options screen (`frontoptions.c` reads the same `rows[]`), so every change below lands in both UIs once.
|
||||
|
||||
- **Mouse sensitivity:** `Input.MouseSensitivity` shown as `%.1fx` (raw/100, 100 = 1.0x), UI range 10-300 step 10. Removed from `calibratedDefault()` (the Wave A "50/100" piecewise display) so the bar is linear and the number is a real unit. Ini values above 300 still load and display (e.g. 5.0x); the UI clamps on the next edit.
|
||||
- **Stick deadzone:** already split L/R (`Input.PadDeadzoneL/R`, raw 0-30000). Done by display/step only: step 300 raw + `dispDiv=300` `%` = exactly 1 display-% per step; NO new key / migration (principle 3 not triggered). Default 7000 shows 23% (off the 300-grid by 100 raw, invisible). Also removed from `calibratedDefault()`. `Input.PadTriggerPct`, `MouseYScale`, `PadLookSens*` keep the calibrated display (out of scope).
|
||||
- **Volumes:** `Bond.Music`/`Bond.FX` step 128 -> 328 (1 display-%).
|
||||
- **FOV in degrees (`portFovHorizDegrees`, `port/src/video.c`):** vfov = 60 (`FOV_Y_F`) x [sqrt(aspect/(4/3)) only when WidescreenAuto && !NativeWidescreen] x FovScale/100, clamped 20..160 (mirrors `portScaleFovY`, the `src/fr.c` guPerspectiveF chokepoint). Projection aspect = window aspect under native widescreen (`portNativeAspect`, matches bondview2.c `faspect` for a full-screen viewport), else 4:3. `hfov = 2*atan(tan(vfov/2)*aspect)`, shown as `N deg`, i.e. HORIZONTAL FOV at the CURRENT window aspect (100% = 75 deg at 4:3, ~91.5 deg at 16:9). Convention/ambiguity: the value is aspect-dependent (it changes when the window is resized while the stored % does not); with native widescreen OFF and auto FOV ON the game projects at 4:3 with a boosted vfov, and we show that projection's hfov, not the stretched on-screen angle. Label changed "FOV scale" -> "Field of view". Storage steps are 1% (0.7-1.3 deg), so a keyboard step walks the stored % until the rounded degree changes (1-2 deg per press; not every whole degree is reachable); drag rounds to 1%. Vertical/aim-zoom rewrites of fovy are not reflected (menu shows the hip-fire value).
|
||||
- **MSAA 16x:** `kMsaaSeq` + `Video.MSAA` range 1-16 + `gfx_msaa_level` snap in `video.c`. `gfx_opengl.cpp` already clamps to `GL_MAX_SAMPLES`; no SDL multisample attribute is set (FBO-based MSAA), so nothing else clamped at 8. Default remains 2 (C initialiser).
|
||||
- **Texture filter / frame-rate cap:** no change (sign-off §0; 30/60 toggle stays, sim is 60 Hz).
|
||||
- **Restart game:** new `__RestartGame` action row after `__QuitToDesktop` (label "Restart game (for restart options)"). It saves config, calls `videoRequestRestart()` -> the same D344 orderly quit (render thread parks, host `exit(0)`); `main.c`'s `portAtExit` (after `videoSaveWindowState()` + `configSave()`) then calls `sysRelaunchSelf()` (`port/src/system.c`): Windows `CreateProcessA` with `GetModuleFileNameA` + the original `GetCommandLineA()` (cwd inherited); POSIX `fork`+`execv` of `/proc/self/exe` with the saved argv (`_POSIX_C_SOURCE` raised to 200112L for fork/readlink). Relaunching only after the config write avoids the new instance reading a half-written ini. Not click-tested (would spawn a second window).
|
||||
|
||||
**Verified:** build green (ninja); `GE_WSPROBE_RESET=1` front boot: 8 sections all `failures=0`; `-level_33`: 8 stage sections all `failures=0`, saved bytes OK. `GE_OPTIONTREEPROBE` reports FAIL both as flagged pre-existing on the base commit; not compared to a base run here (launch budget) -- the Gameplay page stays under its 14-row cap (Restart adds one row). Linux compile of the POSIX branch of `sysRelaunchSelf` NOT done (no Linux toolchain in this shell).
|
||||
|
||||
**Owed (human):** eyeball both UIs (values: `1.0x`, `23%`, `75 deg`, `16x`; Restart row fits its width); click Restart once (Windows) and confirm the window relaunches with the saved settings and no BSOD/hang; confirm 16x on the target GPU.
|
||||
|
||||
**Confidence:** medium-high for display/step/MSAA/reset-table (probe-verified); medium for FOV convention (formula derived from code, not measured on screen) and for Restart (code-reviewed only).
|
||||
|
||||
## D444 — Front PC Options paging controls + hover/scroll fight (2026-09-30)
|
||||
|
||||
**Scope:** port-only, `port/src/frontoptions.c` (the front-end screen has its own page model: `ROWS_PER_PAGE`=11, wheel/Up/Down step the highlight and turn the page at the ends). F10 (`optionsoverlay.c`) is NOT affected: it already has "no hover-to-highlight" (its hover code only hit-tests clicks) and scrolls its own window.
|
||||
|
||||
- **Bug:** every non-A-held frame the highlight `s_hl` was recomputed from the pointer position, so after the wheel/keys moved the selection any small mouse nudge re-selected the row under the pointer and discarded it.
|
||||
- **Hover rule now:** `s_hl` is persistent. The pointer (cursor_h/v_pos differing from the previous frame, excluding our own keyboard/wheel snap in `cursorToItemRaw`) re-selects ONLY when it lands on a different row than the last hovered row (`s_lastHover`; reset on level changes). Hover never changes `s_pageno`. A click (A press) acts on the row actually under the pointer (`s_hoverHit`), not a stale wheel selection.
|
||||
- **Paging controls:** on the bottom hint row of a multi-page section, "Previous page" (left, hidden on page 1) and "Next page" (right, hidden on the last page), highlight box on hover, click via `pageCtlAt()`. Up/Down still page at the ends (the old text hint "Up: previous page / Down: next page" is replaced by the buttons). Controller: unchanged nav (D-pad down/up crosses pages); the buttons are mouse-only. Named "Previous page" to avoid confusion with the game's bottom-left Previous (back) tab.
|
||||
- **Comments:** stale D257/D259/D281/D387 text in `optionsoverlay.c` (kAllUnlocked) and `watchsettings.c` (two places) updated to D442 (pure query-time override; the boot-time BLANKSAVEDATA patch is gone, so a fresh eeprom now reaches the blank-file-1 fallback).
|
||||
|
||||
**Verified:** build green. No probe exercises mouse hover/clicks, so no game launch; a human must test with a mouse.
|
||||
|
||||
**Owed (human):** in a >11-row section (Input, Graphics): click Next/Previous; wheel down then nudge the mouse slightly (selection and page must stay); move onto another row (selects it); click a row after wheeling (acts on the clicked row); confirm the new hint row layout and that controls hide at first/last page.
|
||||
|
||||
**Confidence:** medium (logic reviewed, not exercised live; the pointer-motion detection assumes cursor_h/v_pos is delta-driven and only changes on real motion or our snap).
|
||||
|
||||
|
||||
## D447 - Pillarbox output mode for the Original N64 preset (port-only)
|
||||
|
||||
Design: `docs/dev/notes/PILLARBOX-DESIGN.md` (local). Maintainer decisions 2026-09-30: exact 4:3 (full 320x240 frame); 16:9 while the game's watch-menu Ratio is 16:9; Crop overscan must never remove the player's Wide/Cinema bars; F10 full-window deferred.
|
||||
|
||||
**Done (steps 1-5, 7 partly).**
|
||||
- `port/fast3d/gfx_pc.cpp`: `gfx_set_output_aspect()` / `gfx_get_output_rect()` (declared in `gfx_api.h`). `gfx_start_frame` computes a centred, even-sized rect for the target aspect; `gfx_current_dimensions` and `gfx_current_game_window_viewport` both become the rect, so `different_size` stays false and the inherited PD offset path in `gfx_adjust_viewport_or_scissor` places every viewport/scissor/rect inside it (also per-player viewports: nothing is hard-coded to one viewport). Bars are the frame clear. Aspect 0 = bit-identical path. Also: mode-0 `aspect_scale` uses `gfx_current_dimensions.aspect_ratio`; autoresize fullscreen FBs and `gfx_copy_framebuffer` use window size / rect offsets. MSAA FB stays window-sized (same-size branch).
|
||||
- `port/src/video.c`: `Video.AspectMode` (0 Window, 1 Original), read every frame in `videoStartFrame`; target = `get_screen_ratio()==SCREEN_RATIO_16_9 ? 16:9 : 4:3` (read only, no `src/game` edit). Preset row (N64 = 1, port = 0). GE_PCDUMP / screenshots now capture the whole window (bars included).
|
||||
- `port/src/optionsoverlay.c`: "Aspect" row (Window/Original) and `kResetDefaults` entry (0). Front PC Options reuses the overlay row table, so it gets the row and pages automatically.
|
||||
- Crop gating: the safe-area cache only adopts SP viewports >= 90% of the canvas height. Wide/Cinema (and split-screen half viewports) no longer define the crop band, so their letterbox survives `SafeAreaCrop=1`. If no full viewport has been seen yet, a centred 220-line band is seeded. Port-only.
|
||||
- Mouse: `input.c` front-end pointer maps across the output rect (`videoGetOutputRectFrac`); the F10 overlay mouse already inverts through `gfx_get_ui_screen_rect`, which includes the rect offset. F10 stays drawn inside the rect (step 6 deferred).
|
||||
|
||||
**Measured (1280x720 window, `-level_09`, AspectMode=1, SafeAreaCrop=0, NativeWidescreen=0, MSAA 2):** columns 160..1119, i.e. 160/160 px side bars (expected 160). Top/bottom 30/30 px = the game's own 10/240 overscan fill at 720 px. 800x800 window, MSAA 1: full width, 125/125 px top/bottom = 100 letterbox + 25 game fill. Base smoke (AspectMode=0, frame 300, self-quit) OK.
|
||||
|
||||
**Not done / owed:** runtime check of Wide/Cinema crop gating and of Ratio 16:9 (code path only); mouse mapping not exercised; golden bunker1 at Window mode not run (identical by construction); MSAA 4/8/16, fullscreen toggle, PAL, Linux CI and the MP branch not run; step 6 (F10 full window) deferred by decision. Known cosmetic: with crop off, D246's 1-unit dark-blue edge line is visible at the rect's left/right edges. Human: compare against 1964.
|
||||
|
||||
Confidence: high for the rect and letterbox geometry (measured), medium for crop gating and Ratio 16:9 (unverified at runtime).
|
||||
|
||||
## D445 — crouched-rifle firing animation never resolved on PC (linker adjacency) -> MP crash (2026-09-28)
|
||||
|
||||
**Symptom.** SIGSEGV in `modelConstrainOrWrapAnimFrame` from `playerTick`
|
||||
(bondview2.c ~11021) with `anim = 0xb84`, when a multiplayer player
|
||||
crouch-fires a rifle (found by the 5PLUS bot runs: Complex, 4 seats, M16,
|
||||
frame 5030; present in the 0.9.x friend bundles). Found on
|
||||
`exp/5plus-multiplayer` as "D341" (renamed D420 in PR #120, then D445 on merge: D341 and D420 are already used here).
|
||||
|
||||
**Root cause (layout, porting-notes §D5 "linker adjacency", D164 class).**
|
||||
`firing_animation_groups[1][5]` points at `crouched_rifle_firing_animation_groupA`
|
||||
(chr.c). The tables hold anim *offsets*; `initWeaponAnimGroups` ->
|
||||
`initResolveAnimGroupTable` rebases each table up to its zero terminator.
|
||||
`crouched_rifle_firing_animation_group1` has ONE entry and **no terminator**:
|
||||
on N64 .data is laid out in source order, so resolving group1 walks on into
|
||||
groupA (resolving it too, len 2). GCC placed groupA *below* group1, so
|
||||
groupA[0] kept the raw offset `PTR_ANIM_fire_kneel_left_leg` (0xB84) and
|
||||
`modelSetAnimation` received it as a `ModelAnimation*`. Side effect before
|
||||
the fix: crouching rifle guards saw len 1 instead of the N64's 2.
|
||||
|
||||
**Not covered upstream:** upstream D392 (chraction.c firing-table fields)
|
||||
and D336 (bondhead.c) are different sites; chr.c/chr.h were unchanged on
|
||||
09278407.
|
||||
|
||||
**Fix (8595a06e, `#ifdef PORT`, `#else` = decomp):** chr.c one array
|
||||
`{kneel_right_leg, kneel_left_leg, terminator}` (exactly the N64 layout);
|
||||
chr.h `#define crouched_rifle_firing_animation_groupA
|
||||
(&crouched_rifle_firing_animation_group1[1])`. Resolution and len are the
|
||||
N64's; no logic change. Upstream candidate (a real port bug).
|
||||
|
||||
**Verified (programmer, 5plus + main):** repro (4 seats, bots, Complex,
|
||||
pinned seed, 18 078 frames x3) no crash, SH identical across runs; 8 seats
|
||||
remote mines no crash; every firing/struck anim table in chr.c scanned --
|
||||
this was the only unterminated one; gdb scan of all 24
|
||||
`firing_animation_groups` entries at run time: 1 bad pointer before, 0 after
|
||||
(also 0 on the main-line cherry-pick); P1 gate (1 324 recordings, frame
|
||||
dumps) unchanged; netplay 2P/4P identical.
|
||||
|
||||
## D446 — couch 2P watch-menu text differs between builds that only differ in data layout: stale per-texture anisotropy (2026-09-29)
|
||||
|
||||
**Status:** FIXED, port-only (`port/fast3d/gfx_opengl.cpp`, `#ifdef PORT`). Render-only; the sim was never involved (state hashes identical throughout).
|
||||
|
||||
**Symptom.** The couch 2P script (MULTIPLAYER-DEV §6), frame 2400 (the MP pause/watch menu, "PLAY / RANK: 1ST / SCORES") is not byte-identical between two builds that differ only in data layout:
|
||||
- 8b080caa vs 8b080caa + an unused 3 KB array in `expinput.c`;
|
||||
- the controller-pages merge;
|
||||
- R1's front.c/front.h array widening (programmer 1's bisect).
|
||||
|
||||
About 55-922 pixels differ, a few glyphs only, each by +-1-2 (mostly green). Every build is deterministic against itself. A 4 KB array at the end of `expinput.c` did not reproduce it, and neither did the pre-sync base with R1: only some layouts trigger it.
|
||||
|
||||
**Method (probes in fast3d, removed).** A (6c8915d7) vs B (A + the 3 KB array), frames 2398-2401:
|
||||
1. Every texture import (format, size, content hash): **identical** sequences in A and B.
|
||||
2. Every `gfx_flush` batch (VBO hash, depth mode, blend, viewport, scissor, combine mode, other modes, and for both texture slots the GL texture id, filter, clamp, content hash): **identical except the GL texture ids.** The same content is bound under different ids in A and B.
|
||||
3. So the difference lives in GL texture-object state.
|
||||
|
||||
**Root cause.**
|
||||
- `gfx_opengl_set_sampler_parameters` (`gfx_opengl.cpp`, the `if (mipmaps) glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, current_anisotropy_level);` line) sets anisotropy only for mipmapped samplers and never resets it. Anisotropy is per-texture-object state; `Video.Anisotropy` defaults to 4.
|
||||
- GL ids are recycled: `gfx_texture_cache_delete_range` (`gfx_pc.cpp`) pushes freed ids onto `free_texture_ids` while walking the `std::unordered_map` in hash-bucket order. The key hash is the texture **address**, so the recycling order depends on where textures live.
|
||||
- A glyph texture (bilinear, no mipmaps) that inherits an id last used by a mipmapped world texture keeps `MAX_ANISOTROPY = 4`. llvmpipe then filters it slightly differently: the +-1-2 values.
|
||||
- A layout shift changes which glyph inherits which id, hence which frames differ.
|
||||
- Nothing in `src/game` is involved: no uninitialised read, no out-of-bounds read, no stale texture content (all ruled out by the probes and the earlier UBSan run).
|
||||
|
||||
**Fix.** Always write the anisotropy when sampler parameters are set: the level when mipmapped, 1.0 (off) otherwise, so a recycled id carries nothing over. This is identity for any texture whose id never had a mipmapped owner, which is why the base frames are unchanged.
|
||||
|
||||
**Verified (headless, silent, llvmpipe, scratch HOME, mp-sandbox data; 6c8915d7 base):**
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| Couch 2P frames 2300-2500 (step 10), fix vs fix + 3 KB array (the reproducing variant) | **0 of 21 frames differ** (before the fix: frames 2300-2420 differed) |
|
||||
| Couch 2P frames 2300-2500, base vs fix | 0 of 21 differ |
|
||||
| Solo `-level_09` frame 1800, base vs fix | byte-identical |
|
||||
| Couch 4P frame 2400, base vs fix | byte-identical |
|
||||
| Netplay 2P (local relay, 60 s) | SH identical over 2 671 ticks |
|
||||
|
||||
**Residual risk (not fixed, noted).** Other per-object state can still follow a recycled id:
|
||||
- mip levels 1..n of a previous owner, if a texture uploaded without mipmaps is later sampled with a mipmapped min filter;
|
||||
- the free-id order is still address-dependent.
|
||||
|
||||
The general fix would reset the whole object on reuse, or sort the ids that `delete_range` frees. Neither is needed for this symptom. **Confidence: [H]** for the cause and the fix (A/B reproduced, fixed, bisected to the one GL parameter).
|
||||
@@ -290,6 +290,8 @@ The offline model converter sizes each record's trailing arrays from its count f
|
||||
|
||||
When a PC-larger asset gets a bigger reservation inside a shared scratch buffer (D45: the wallet model 0xA000 -> 0x17000 in `ptr_logo_and_walletbond_DL`), grep for every other user of that buffer that computes a fixed offset (`base + 4096*10`). Those still point into the old boundary and silently overwrite the enlarged asset; the corruption only shows when the asset is not reloaded.
|
||||
|
||||
**A1 cross-tag (D441, 2026-09-30):** the 32→64 widening of `(u32)`/`(s32)` *pointer casts* (rebase arithmetic on RAM copies, segment/anim-table bases) is the same class. Pattern: `#ifdef PORT` `(uintptr_t)` arm + untouched `#else`. Exception: `(s32)&ANIM_DATA_x` is an intentional offset (D34: 4 GiB-aligned base, low 32 bits = segment offset) — widen only the *base* it is added to, never the offset. Census of the remaining ~185 sites in the touched files is still owed (D441).
|
||||
|
||||
## B. 16-byte PC `Gfx` / `Vtx` vs 8-byte N64
|
||||
|
||||
Any buffer reservation, `memcpy` size, slot stride, or pool budget
|
||||
@@ -633,6 +635,7 @@ through a converter or a runtime bswap fixup reads scrambled.
|
||||
|
||||
- **A texture re-declared in another format must be normalised with the geometry it was IMPORTED with (D245).** GE's sky water loads a CI8 image and draws it through an RGBA16 tile; fast3d imports it as CI8 (D229) but its triangle path computed the UV divisor from the RGBA16 tile (half the width) and the whole mip-chain height (43 rows, not the 32-row mask period). Anything that changes the importer's format/extent must change the tri-path `tex_width/tex_height` with it, and wrap must equal the N64 mask period.
|
||||
- **CPU-built RDP triangles need float vertices, not `Vtx` (D245).** Where GE builds RDP edge/texture coefficients on the CPU (sky/water), 32-bit S/T and positions are normal; squeezing them through s16 `tc`/`ob` costs visible precision on horizon-scale geometry. Use `G_FLOATVTX_EXT` (`port/include/floatvtx.h`).
|
||||
- **A first-run-only preset is skipped forever by any earlier launch that created the ini, and window keys are not an "untouched" signal (D283).** Seeding values *before* `configLoad()` only works when no `ge007.ini` exists; one launch in a different environment (SteamOS Desktop Mode has no `STEAMOS`) writes a plain ini and the preset never runs. Apply presets *after* the load, record a one-shot flag key, and gate on keys the user must change deliberately (`Video.Fullscreen`, `Window.Maximized`) — `Window.Width/Height` are rewritten from the live window on every clean exit (`videoSaveWindowState`), so "still at default" never holds for them.
|
||||
|
||||
## D. N64 hardware idioms fast3d does not emulate
|
||||
|
||||
@@ -1747,3 +1750,21 @@ A port stub that forces a value to `-1` (D154 `texnum`, D135) can hide latent la
|
||||
`propobj.c:~2393`) — those 4 rely on div-by-zero yielding NaN/inf without
|
||||
trapping on both MIPS and x86-64 SSE, matching retail behavior; do not
|
||||
"fix" them.
|
||||
|
||||
- **x64 Windows backtraces: unwind with the PE's .pdata, never walk RBP.**
|
||||
`-fno-omit-frame-pointer` on MinGW-w64 GCC does not give an RBP chain:
|
||||
most prologues set `RBP = RSP + N` (Win64 allows an offset frame
|
||||
pointer), so `[RBP]` is not the saved RBP and a manual walk dies at
|
||||
frame 1. `RtlLookupFunctionEntry` + `RtlVirtualUnwind` from the
|
||||
exception CONTEXT is reliable and needs no dbghelp/symbols (crash.c,
|
||||
2026-09-30). Print `va = preferred ImageBase + RVA` per frame so
|
||||
`addr2line -e ge007.x86_64.exe` resolves it even if the image was
|
||||
relocated. Test with `GE_CRASHTEST=<N>`.
|
||||
- **Crossing msys -> native: pass files, not quoted strings; get paths from
|
||||
bash's own `pwd -W`.** The msys argv conversion re-escapes embedded `"`
|
||||
as `\"`, which `cmd.exe` does not understand, so an inline
|
||||
`cmd //c "... \"C:\path\" ..."` silently never runs (build-pc.sh's TMP
|
||||
probe was dead this way). And with Git Bash plus MSYS2 `usr/bin` on one
|
||||
PATH, `/tmp`, `mktemp` and `cygpath` can belong to different runtimes
|
||||
(different Windows dirs). Never trust `command -v cmake` for the MSYS2
|
||||
toolchain either: a pip-installed cmake can shadow it (2026-09-30).
|
||||
@@ -10,7 +10,11 @@ This page answers two questions plainly: *what does installing this put on
|
||||
your machine*, and *how faithfully does the port actually reproduce the
|
||||
original N64 game's logic*. Both were reviewed end-to-end on 2026-09-15 and
|
||||
re-verified for the v0.3.0 release bundles on 2026-09-18 (bundle contents,
|
||||
no-networking source check, and the asset converter). The full engineering
|
||||
no-networking source check, and the asset converter). The no-networking source
|
||||
check was repeated against the `release/v0.4.1` tree on 2026-09-30 (no socket,
|
||||
HTTP or name-resolution calls in `src/` or `port/`; neither the engine nor the
|
||||
bundled `SDL2.dll` imports a networking DLL; no registry calls in the port's
|
||||
own code); the bundle contents were not re-checked then. The full engineering
|
||||
record behind each item is in the project's finding log
|
||||
([`docs/dev/findings.md`](dev/findings.md)).
|
||||
|
||||
@@ -41,7 +45,10 @@ reads your ROM and writes new files — has no network access at all,
|
||||
verifies your ROM against a fixed table of known retail GoldenEye 007
|
||||
hashes before touching it (an unrecognized file is rejected outright), and
|
||||
writes only inside the bundle folder. Its full source ships right next to
|
||||
the compiled version in every release if you'd like to read it yourself.
|
||||
the compiled version in every release if you'd like to read it yourself. The
|
||||
engine starts it automatically on the first run, when the two derived asset
|
||||
folders (`data/pcmodels-*`, `data/pccg-*`) are missing, passing it the ROM it
|
||||
found and the output folder; after that first run it is not launched again.
|
||||
|
||||
See [`SECURITY.md`](../.github/SECURITY.md) for why the binaries are
|
||||
unsigned and what that means in practice (a SmartScreen prompt on first
|
||||
@@ -52,13 +59,12 @@ run, and why some antivirus engines flag the converter specifically).
|
||||
Cloning and building pulls in only standard, well-known packages: MSYS2
|
||||
`pacman` packages on Windows, `apt` packages on Linux — no `npm`, no
|
||||
`cargo`, nothing downloaded at CMake configure time, no git submodules, and
|
||||
no prebuilt binaries checked into the repository. **One real gap**: the CI
|
||||
build installs `pyinstaller` from PyPI without pinning a version, which is
|
||||
used to freeze the release's asset-converter tool. It's a widely-used
|
||||
packaging tool, and what it packages is entirely in-repo, stdlib-only
|
||||
Python — but an unpinned dependency means a future PyInstaller release
|
||||
could change what ships without a corresponding diff in this repository.
|
||||
Pinning it is a tracked follow-up.
|
||||
no prebuilt binaries checked into the repository. The CI build freezes the release's asset-converter tool with `pyinstaller`,
|
||||
which is pinned to an exact version, and every third-party GitHub Action the
|
||||
workflows use is pinned to a full commit SHA (both from the release after
|
||||
v0.4.0; earlier releases installed an unpinned `pyinstaller`). What
|
||||
PyInstaller packages is entirely in-repo, stdlib-only Python, so a release is
|
||||
reproducible from a diff of this repository.
|
||||
|
||||
One development-only backdoor exists in source builds: the `GE_DEBUG_UNLOCKALL`
|
||||
environment variable (`src/game/file2.c`) pre-unlocks all cheat options at boot.
|
||||
|
||||
@@ -56,6 +56,10 @@ void gfx_set_safe_area_crop(int on); /* crop the N64 TV-overscan safe-area
|
||||
* crop above -- for inverting a window mouse click into logical 2D UI space
|
||||
* (see port/src/optionsoverlay.c, D316). */
|
||||
void gfx_get_ui_screen_rect(int32_t *outX, int32_t *outY, int32_t *outW, int32_t *outH);
|
||||
/* D447: 0 = fill the window; >0 = pillar/letterbox the output to this aspect (call before gfx_start_frame). */
|
||||
void gfx_set_output_aspect(float aspect);
|
||||
/* Current output rect in window pixels, top-left origin (the whole window when no aspect is forced). */
|
||||
void gfx_get_output_rect(int32_t *outX, int32_t *outY, int32_t *outW, int32_t *outH);
|
||||
void gfx_texture_cache_clear(void);
|
||||
int gfx_texture_cache_count(void); /* D235 */
|
||||
void gfx_texture_cache_delete(const uint8_t *orig_addr);
|
||||
|
||||
@@ -787,9 +787,21 @@ static void gfx_opengl_set_sampler_parameters(int tile, bool linear_filter, uint
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, max_filter);
|
||||
|
||||
#ifdef PORT
|
||||
/* D446: anisotropy is per-texture-object state. It was only ever SET
|
||||
* (when mipmapped), never reset, so a GL texture id freed by
|
||||
* gfx_texture_cache_delete_range and reused for a non-mipmapped texture
|
||||
* (HUD / watch text glyphs) kept the previous owner's 4x -- llvmpipe then
|
||||
* filters that glyph differently (+-1..2 per channel). Which id a texture
|
||||
* inherits follows the unordered_map iteration order, i.e. the texture
|
||||
* ADDRESSES, so the frame changed with the binary's data layout. Always
|
||||
* write it: the level when mipmapped, 1 (off) otherwise. */
|
||||
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, mipmaps ? (float)current_anisotropy_level : 1.0f);
|
||||
#else
|
||||
if (mipmaps) {
|
||||
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, current_anisotropy_level);
|
||||
}
|
||||
#endif
|
||||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, gfx_cm_to_opengl(cms));
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, gfx_cm_to_opengl(cmt));
|
||||
|
||||
+68
-197
@@ -263,6 +263,24 @@ extern "C" void gfx_set_safe_area_crop(int on) {
|
||||
g_safe_area_crop_enabled = !!on;
|
||||
}
|
||||
|
||||
/* D447: output rect (Video.AspectMode = Original). 0 = fill the window (the
|
||||
* historical behaviour, bit-identical). >0 = letter/pillarbox the frame to
|
||||
* exactly this aspect, centred; set once per frame by the port layer before
|
||||
* gfx_start_frame. g_output_rect is top-left-origin window pixels. */
|
||||
static float g_output_aspect = 0.0f;
|
||||
static struct XYWidthHeight g_output_rect = { 0, 0, 0, 0 };
|
||||
|
||||
extern "C" void gfx_set_output_aspect(float aspect) {
|
||||
g_output_aspect = aspect > 0.1f ? aspect : 0.0f;
|
||||
}
|
||||
|
||||
extern "C" void gfx_get_output_rect(int32_t *outX, int32_t *outY, int32_t *outW, int32_t *outH) {
|
||||
*outX = g_output_rect.x;
|
||||
*outY = g_output_rect.y;
|
||||
*outW = (int32_t)g_output_rect.width;
|
||||
*outH = (int32_t)g_output_rect.height;
|
||||
}
|
||||
|
||||
uint32_t gfx_msaa_level = 1;
|
||||
|
||||
static bool dropped_frame;
|
||||
@@ -1209,36 +1227,6 @@ static void import_texture(int i, int tile, bool importReplacement) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef PORT
|
||||
/* D157-I (M-158): import census for the D219/D252 rainbow-spark repro.
|
||||
* Logs every small-texture (<= 16 KiB) import inside the known-bad frame
|
||||
* window of the Bunker1 -level_09 repro, deduped per source address (first
|
||||
* 3 hits each). Paired with the D157T vertex probe: the census names the
|
||||
* exact source addresses bound during the spark frames (the smoke/flare/
|
||||
* scattered family should appear in lockstep with the scripted shots),
|
||||
* which the vertex lines can then be grepped by. Remove once root-caused.
|
||||
* D250: cached getenv, inert unless GE_D157 is set. */
|
||||
static int ge_d157i = -1;
|
||||
if (ge_d157i < 0) ge_d157i = getenv("GE_D157") != NULL;
|
||||
if (ge_d157i) {
|
||||
extern uint32_t num_dls;
|
||||
/* 2026-09-18: dropped the old num_dls in [90,240] gate here too (see
|
||||
* the matching D157T comment) -- per-address dedup below already
|
||||
* bounds this to 3 lines per distinct texture for the whole session. */
|
||||
if (loaded_texture.size_bytes <= 16384) {
|
||||
static std::map<const void*, int> d157i_seen;
|
||||
int& n = d157i_seen[(const void*)orig_addr];
|
||||
if (++n <= 3) {
|
||||
sysLogPrintf(LOG_NOTE,
|
||||
"D157I: frame=%u hit#%d addr=%p fmt=%u siz=%u size=%u line=%u tile=%u tmem=%u",
|
||||
num_dls, n, (const void*)orig_addr, fmt, siz,
|
||||
loaded_texture.size_bytes, loaded_texture.line_size_bytes,
|
||||
tile, rdp.texture_tile[tile].tmem);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static int ge_texdump = -1;
|
||||
if (ge_texdump < 0) ge_texdump = getenv("GE_TEXDUMP") != NULL;
|
||||
if (ge_texdump) {
|
||||
@@ -1298,7 +1286,7 @@ static void import_texture(int i, int tile, bool importReplacement) {
|
||||
snprintf(nm2, sizeof nm2, "texdump/a%05d_%ux%u.bin", tdc - 1, tw, th);
|
||||
FILE* bf2 = fopen(nm2, "wb");
|
||||
if (bf2) { fwrite(loaded_texture.addr, 1, loaded_texture.size_bytes, bf2); fclose(bf2); }
|
||||
extern uint32_t num_dls; // same counter the D157I probe uses
|
||||
extern uint32_t num_dls;
|
||||
sysLogPrintf(LOG_NOTE,
|
||||
"GE_TEXA[%d] dls=%u addr=%p fmt=%u siz=%u size=%u min=%u max=%u nonzero=%u/%u file=%s",
|
||||
tdc - 1, num_dls, (void*)orig_addr, fmt, siz,
|
||||
@@ -1372,15 +1360,13 @@ static void import_texture(int i, int tile, bool importReplacement) {
|
||||
* carries the whole mip chain (32x32 base + mips = 1400 B), which
|
||||
* made a 32x43 image whose GL REPEAT period (43 rows) differs from
|
||||
* the N64's mask period (32). See the tri-path counterpart. */
|
||||
static int d245_oldtex = -1;
|
||||
if (d245_oldtex < 0) d245_oldtex = getenv("GE_D245_OLDTEX") != NULL;
|
||||
const uint8_t maskt = rdp.texture_tile[tile].maskt;
|
||||
/* Crop by the TILE line size (the importer's row width): for a
|
||||
* LoadBlock, loaded_texture.line_size_bytes is the whole block, so
|
||||
* testing against it never cropped (M-201 follow-up: the mip rows
|
||||
* then showed as coloured dashes on Frigate's water). */
|
||||
const uint32_t d245_row = rdp.texture_tile[tile].line_size_bytes;
|
||||
if (!d245_oldtex && fmt == G_IM_FMT_RGBA && siz == G_IM_SIZ_16b && maskt > 0 && maskt < 12 &&
|
||||
if (fmt == G_IM_FMT_RGBA && siz == G_IM_SIZ_16b && maskt > 0 && maskt < 12 &&
|
||||
d245_row > 0 && loaded_texture.size_bytes > d245_row * (1u << maskt)) {
|
||||
LoadedTexture lt = loaded_texture;
|
||||
lt.size_bytes = d245_row * (1u << maskt);
|
||||
@@ -1822,12 +1808,6 @@ static void d75d_note_emit_z(uint32_t f, struct LoadedVertex* const* v_arr, int
|
||||
d75d_env[0]=rdp.env_color.r; d75d_env[1]=rdp.env_color.g; d75d_env[2]=rdp.env_color.b; d75d_env[3]=rdp.env_color.a;
|
||||
}
|
||||
|
||||
/* D252 TEMP (2026-09-26): F12 with GE_D157 set opens a full-detail D157T
|
||||
* burst (every small-texture vertex, no sampling) for ~90 frames, so a live
|
||||
* sighting is captured even after the continuous cap has dropped to 1-in-50.
|
||||
* Armed from videoEndFrame's screenshot path via gfxD157Burst(). */
|
||||
static volatile uint32_t s_d157BurstUntil = 0;
|
||||
|
||||
static void gfx_sp_vertex(size_t n_vertices, size_t dest_index, const Vtx* vertices) {
|
||||
const size_t d75_di0 = dest_index; /* D75 probe: loop below mutates dest_index */
|
||||
SUPPORT_CHECK(n_vertices <= MAX_VERTICES);
|
||||
@@ -1957,102 +1937,6 @@ static void gfx_sp_vertex(size_t n_vertices, size_t dest_index, const Vtx* verti
|
||||
d->color.b = vcn->b;
|
||||
}
|
||||
|
||||
#ifdef PORT
|
||||
/* D157 diag (temporary, session 2026-09-16/17): does bullet_spark_render
|
||||
* (glass2.c) actually draw with G_LIGHTING on? The M-110 mechanism
|
||||
* (this vertex's v.cn[]/n.n[] union reinterpretation above) was
|
||||
* already ruled out for explosion.c's particles (5000/5000 samples
|
||||
* measured LIGHTING=off, see the D219 comment a few hundred lines
|
||||
* below) -- don't assume it holds for glass2.c's bullet sparks
|
||||
* without measuring. Key on the ORIGINAL authored bytes
|
||||
* (v->v.cn[], readable regardless of which branch fired above,
|
||||
* since n.n[]/cn[] are the same union storage) matching one of
|
||||
* glass2.c's g_BulletSparkColors[] entries exactly. */
|
||||
static int ge_d157 = -1;
|
||||
if (ge_d157 < 0) ge_d157 = getenv("GE_D157") != NULL;
|
||||
if (ge_d157) {
|
||||
static const uint8_t known[][4] = {
|
||||
{0xFF,0xFF,0xFF,0xFF}, {0xFF,0xFF,0xC8,0xFF}, {0xFF,0x00,0x00,0xFF},
|
||||
};
|
||||
/* 2026-09-26: this match was uncapped and white (255,255,255,255)
|
||||
* hits every HUD/text/effect vertex -> ~200k lines in 18 s, 4-6 fps
|
||||
* in live play. Now: first 500 hits, then only inside an F12 burst. */
|
||||
extern uint32_t num_dls;
|
||||
static int d157m_n = 0;
|
||||
const bool d157m_on = d157m_n < 500 || num_dls <= s_d157BurstUntil;
|
||||
if (d157m_on) for (const auto& k : known) {
|
||||
if (v->v.cn[0] == k[0] && v->v.cn[1] == k[1] && v->v.cn[2] == k[2] && v->v.cn[3] == k[3]) {
|
||||
d157m_n++;
|
||||
sysLogPrintf(LOG_NOTE,
|
||||
"D157: bulletspark-color-match cn=(%d,%d,%d,%d) geometry_mode=%08x LIGHTING=%s -> shaded=(%d,%d,%d)",
|
||||
v->v.cn[0], v->v.cn[1], v->v.cn[2], v->v.cn[3], rsp.geometry_mode,
|
||||
(rsp.geometry_mode & G_LIGHTING) ? "ON" : "off",
|
||||
(int)d->color.r, (int)d->color.g, (int)d->color.b);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* D157-B (M-158): the exact-color-match key above NEVER caught the
|
||||
* pale-yellow (255,255,200) door-spark vertices in the Bunker1
|
||||
* rainbow repro (only 2 white hits, both LIGHTING=off), so per
|
||||
* D219 M-157's revised next step this sibling block keys on
|
||||
* TEXTURE IDENTITY instead: any vertex whose render tile (tile 0)
|
||||
* holds a small loaded texture (size <= 16 KiB -- the spark
|
||||
* family is IA 64x64, promoted 8b->16b by texSelect) inside the
|
||||
* known-bad frame window of that repro (shots at frames 60-350,
|
||||
* visible rainbow frames 108-220). Logs the full RGBA path per
|
||||
* vertex: authored cn, shaded result, LIGHTING, geometry_mode,
|
||||
* combine_mode (texSelect sets G_CC_MODULATEIA for these -- a
|
||||
* different value here is a combiner-state leak), and the tile's
|
||||
* source identity (addr/fmt/siz/size = the TextureCacheKey for
|
||||
* non-CI textures, so cache-collision reasoning works offline).
|
||||
* Frame window + size key are repro-specific; widen if re-running
|
||||
* elsewhere. Remove once D219/D252 is root-caused. */
|
||||
{
|
||||
extern uint32_t num_dls;
|
||||
/* 2026-09-18: dropped the old num_dls in [90,240] gate -- that
|
||||
* window only fit the original short GE_INPUTSCRIPT repro; a
|
||||
* real interactive session runs for thousands of frames before
|
||||
* the player actually triggers the bug, so the gate silently
|
||||
* discarded 100% of useful data in live play. The existing
|
||||
* per-run hit caps below already bound log growth without it. */
|
||||
{
|
||||
const uint32_t tile0 = rdp.first_tile_index;
|
||||
LoadedTexture& lt0 = rdp.loaded_texture[rdp.texture_tile[tile0].tmem];
|
||||
if (lt0.addr && lt0.size_bytes <= 16384) {
|
||||
static int d157b_n = 0;
|
||||
d157b_n++;
|
||||
/* Raised from 400/1-in-200 (sized for a ~250-frame
|
||||
* scripted repro) to a much larger continuous window
|
||||
* so a real, minutes-long play session doesn't drop
|
||||
* to sparse 1-in-200 sampling before the player
|
||||
* actually triggers the bug. */
|
||||
static int d157burst_n = 0;
|
||||
const bool burst = num_dls <= s_d157BurstUntil && d157burst_n < 60000;
|
||||
if (burst) d157burst_n++;
|
||||
/* 2026-09-26: continuous logging cut to a 2000-line baseline;
|
||||
* the 20000 + 1-in-50 tail was a big share of the
|
||||
* live-play slowdown. Full detail now comes from F12. */
|
||||
if (burst || d157b_n <= 2000) {
|
||||
sysLogPrintf(LOG_NOTE,
|
||||
"D157T%s: frame=%u cn=(%d,%d,%d,%d) shaded=(%d,%d,%d) LIGHTING=%s geom=%08x combine=%llx | "
|
||||
"tile0=%u tmem=%u fmt=%u siz=%u addr=%p size=%u line=%u | uv=(%d,%d)",
|
||||
burst ? "B" : "", num_dls,
|
||||
v->v.cn[0], v->v.cn[1], v->v.cn[2], v->v.cn[3],
|
||||
(int)d->color.r, (int)d->color.g, (int)d->color.b,
|
||||
(rsp.geometry_mode & G_LIGHTING) ? "ON" : "off", rsp.geometry_mode,
|
||||
(unsigned long long)rdp.combine_mode,
|
||||
tile0, rdp.texture_tile[tile0].tmem,
|
||||
rdp.texture_tile[tile0].fmt, rdp.texture_tile[tile0].siz,
|
||||
(const void*)lt0.addr, lt0.size_bytes, lt0.line_size_bytes,
|
||||
(int)U, (int)V);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
d->u = U;
|
||||
d->v = V;
|
||||
|
||||
@@ -2256,49 +2140,6 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
|
||||
d75d_note_rej(videoGetFrameCount(), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* D288 diag (M-152, frame-gated M-155, any_behind-restricted
|
||||
* M-155-follow-up): the silo intro's screen-filling stray triangle
|
||||
* (bounded to level_20 frames ~562-741) isn't explained by this
|
||||
* function's existing D233/D106 guards on static review -- log any
|
||||
* triangle whose clip-space NDC bbox covers an unreasonable chunk
|
||||
* of the screen, together with each vertex's w and the render-mode
|
||||
* word (oml), to catch it at draw time. M-155: the first ungated
|
||||
* run fired 116k times in ~1500-2000 frames -- bbox_frac >0.15
|
||||
* alone is a common occurrence for ordinary near-camera geometry,
|
||||
* not a distinguishing signature. Gated to the reported frame
|
||||
* window (num_dls, the pattern the since-removed GE_D236RM
|
||||
* probe also used) and the
|
||||
* threshold raised well above the ~0.15 "normal" baseline M-155
|
||||
* established, so a hit here is actually rare. M-155 follow-up:
|
||||
* for any_behind_camera==1 triangles the x/w perspective divide is
|
||||
* meaningless (w near zero or negative -> arbitrarily large
|
||||
* "bbox_frac" for perfectly ordinary geometry), which is exactly
|
||||
* why the code above defers those to GL's own clipper -- so the
|
||||
* probe now skips them entirely; a hit here has comfortably
|
||||
* positive w's and is a genuine large-NDC-bbox triangle. oml is
|
||||
* logged so the culprit's material/model can be identified against
|
||||
* the level's asset tables (the flat RGB(83,62,44) color grep
|
||||
* found nothing in the .c sources -- binary model blobs). Remove
|
||||
* once D288 is root-caused. */
|
||||
static int ge_d288 = -1;
|
||||
if (ge_d288 < 0) ge_d288 = getenv("GE_D288") != NULL;
|
||||
if (ge_d288 && !any_behind_camera) {
|
||||
extern uint32_t num_dls;
|
||||
if (num_dls >= 550 && num_dls <= 755) {
|
||||
float minx = std::fmin(std::fmin(v1->x / v1->w, v2->x / v2->w), v3->x / v3->w);
|
||||
float maxx = std::fmax(std::fmax(v1->x / v1->w, v2->x / v2->w), v3->x / v3->w);
|
||||
float miny = std::fmin(std::fmin(v1->y / v1->w, v2->y / v2->w), v3->y / v3->w);
|
||||
float maxy = std::fmax(std::fmax(v1->y / v1->w, v2->y / v2->w), v3->y / v3->w);
|
||||
float bbox_frac = (maxx - minx) * (maxy - miny) / 4.0f; // NDC quad is [-1,1]^2
|
||||
if (!std::isfinite(bbox_frac) || bbox_frac > 0.5f) {
|
||||
fprintf(stderr, "D288: dl=%u tri w=(%.6f,%.6f,%.6f) bbox_frac=%.3f oml=0x%08x color=(%d,%d,%d)\n",
|
||||
num_dls, v1->w, v2->w, v3->w, bbox_frac,
|
||||
(unsigned)rdp.other_mode_l,
|
||||
v1->color.r, v1->color.g, v1->color.b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((rsp.geometry_mode & G_CULL_BOTH) != 0) {
|
||||
@@ -2483,10 +2324,8 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
|
||||
* (32, not 32/2 = 16, which doubled the S frequency) and height =
|
||||
* the mask period (32, not 1400/32 = 43 rows of mip chain). */
|
||||
{
|
||||
static int d245_oldtex2 = -1;
|
||||
if (d245_oldtex2 < 0) d245_oldtex2 = getenv("GE_D245_OLDTEX") != NULL;
|
||||
const auto& ltx = rdp.loaded_texture[rdp.texture_tile[tile].tmem];
|
||||
if (!d245_oldtex2 && rdp.texture_tile[tile].fmt == G_IM_FMT_RGBA &&
|
||||
if (rdp.texture_tile[tile].fmt == G_IM_FMT_RGBA &&
|
||||
rdp.texture_tile[tile].siz == G_IM_SIZ_16b && ltx.src_fmt == G_IM_FMT_CI) {
|
||||
const uint8_t mkt = rdp.texture_tile[tile].maskt;
|
||||
tex_width[i] = rdp.texture_tile[tile].line_size_bytes;
|
||||
@@ -2989,7 +2828,7 @@ static void gfx_sp_geometry_mode(uint32_t clear, uint32_t set) {
|
||||
static inline void gfx_update_aspect_mode(void) {
|
||||
const uint32_t side = rsp.aspect_mode & G_ASPECT_CENTER_EXT;
|
||||
|
||||
rsp.aspect_scale = rsp.aspect_mode ? gfx_current_native_aspect : gfx_current_window_dimensions.aspect_ratio;
|
||||
rsp.aspect_scale = rsp.aspect_mode ? gfx_current_native_aspect : gfx_current_dimensions.aspect_ratio; /* D447: == window aspect unless the output rect is active */
|
||||
|
||||
if (side == G_ASPECT_LEFT_EXT) {
|
||||
rsp.aspect_ofs = 1.f - gfx_current_dimensions.aspect_ratio / gfx_current_native_aspect;
|
||||
@@ -3107,9 +2946,19 @@ static void gfx_calc_and_set_viewport(const Vp_t* viewport) {
|
||||
/* Cache the raw (pre window-scale) viewport bounds for the safe-area
|
||||
* crop above -- guard against a degenerate/zero-height viewport so a
|
||||
* later divide can't ever see one. */
|
||||
if (height > 1.0f) {
|
||||
if (height >= 0.9f * (float)SCREEN_HEIGHT) {
|
||||
g_gpSafeTop = y;
|
||||
g_gpSafeHeight = height;
|
||||
} else if (height > 1.0f && g_gpSafeHeight <= 0.0f) {
|
||||
/* D447: Wide/Cinema (and split-screen) viewports are the player's own
|
||||
* letterbox, never the overscan margin: don't let them define the crop
|
||||
* band, or the crop would stretch them to fill the window. If no full
|
||||
* viewport has been seen yet, seed the standard centred 220-line band
|
||||
* (VIEWPORT_HEIGHT_DEFAULT_NTSC) around this viewport's centre. */
|
||||
const float centre = y - height * 0.5f;
|
||||
const float bandH = 220.0f * (float)SCREEN_HEIGHT / 240.0f;
|
||||
g_gpSafeTop = centre + bandH * 0.5f;
|
||||
g_gpSafeHeight = bandH;
|
||||
}
|
||||
|
||||
gfx_adjust_viewport_or_scissor(&rdp.viewport);
|
||||
@@ -4243,8 +4092,37 @@ extern "C" void gfx_start_frame(void) {
|
||||
|
||||
gfx_current_dimensions = gfx_current_window_dimensions;
|
||||
|
||||
gfx_current_game_window_viewport.x = 0;
|
||||
gfx_current_game_window_viewport.y = 0;
|
||||
gfx_current_game_window_viewport.width = gfx_current_dimensions.width;
|
||||
gfx_current_game_window_viewport.height = gfx_current_dimensions.height;
|
||||
g_output_rect = { 0, 0, gfx_current_window_dimensions.width, gfx_current_window_dimensions.height };
|
||||
|
||||
if (g_output_aspect > 0.0f) {
|
||||
/* D447: pillar/letterbox to the exact target aspect, centred, even
|
||||
* sizes. dims == game viewport size, so different_size stays false and
|
||||
* gfx_adjust_viewport_or_scissor's existing offset path places every
|
||||
* canvas rect inside the rect. */
|
||||
const uint32_t ww = gfx_current_window_dimensions.width;
|
||||
const uint32_t wh = gfx_current_window_dimensions.height;
|
||||
uint32_t rw = ww, rh = wh;
|
||||
if ((float)ww / (float)wh > g_output_aspect) {
|
||||
rw = (uint32_t)std::lround((float)wh * g_output_aspect);
|
||||
} else {
|
||||
rh = (uint32_t)std::lround((float)ww / g_output_aspect);
|
||||
}
|
||||
rw &= ~1u;
|
||||
rh &= ~1u;
|
||||
if (rw < 2) rw = 2;
|
||||
if (rh < 2) rh = 2;
|
||||
if (rw > ww) rw = ww;
|
||||
if (rh > wh) rh = wh;
|
||||
g_output_rect = { (int16_t)((ww - rw) / 2), (int16_t)((wh - rh) / 2), rw, rh };
|
||||
gfx_current_dimensions.width = rw;
|
||||
gfx_current_dimensions.height = rh;
|
||||
gfx_current_dimensions.aspect_ratio = g_output_aspect;
|
||||
gfx_current_game_window_viewport = g_output_rect;
|
||||
}
|
||||
|
||||
if (gfx_current_dimensions.height != gfx_prev_dimensions.height) {
|
||||
for (auto& fb : framebuffers) {
|
||||
@@ -4256,8 +4134,8 @@ extern "C" void gfx_start_frame(void) {
|
||||
gfx_adjust_width_height_for_scale(width, height);
|
||||
} else {
|
||||
// assume this is a fullscreen fb
|
||||
width = gfx_current_dimensions.width;
|
||||
height = gfx_current_dimensions.height;
|
||||
width = gfx_current_window_dimensions.width;
|
||||
height = gfx_current_window_dimensions.height;
|
||||
}
|
||||
if (width != fb.second.applied_width || height != fb.second.applied_height) {
|
||||
gfx_rapi->update_framebuffer_parameters(fb.first, width, height, 1, true, true, true, true);
|
||||
@@ -4300,16 +4178,6 @@ extern "C" void gfx_start_frame(void) {
|
||||
|
||||
uint32_t num_dls = 0;
|
||||
|
||||
/* D252 TEMP: see s_d157BurstUntil. No-op unless GE_D157 is set. */
|
||||
extern "C" void gfxD157Burst(void) {
|
||||
static int on = -1;
|
||||
if (on < 0) on = getenv("GE_D157") != NULL;
|
||||
if (!on) return;
|
||||
s_d157BurstUntil = num_dls + 90;
|
||||
sysLogPrintf(LOG_NOTE, "D157 burst: F12 at frame=%u, full logging to frame=%u",
|
||||
num_dls, (uint32_t)s_d157BurstUntil);
|
||||
}
|
||||
|
||||
/* F10 port-layer options overlay (port/src/optionsoverlay.c). Returns a
|
||||
* self-contained 2D display list to draw on top of the game's frame, or NULL
|
||||
* when the overlay is closed -- in which case nothing is appended and the
|
||||
@@ -4513,8 +4381,11 @@ extern "C" void gfx_copy_framebuffer(int fb_dst, int fb_src, int left, int top,
|
||||
// upscale the position
|
||||
left = left * gfx_current_dimensions.width / gfx_current_native_viewport.width;
|
||||
top = top * gfx_current_dimensions.height / gfx_current_native_viewport.height;
|
||||
// D447: offset into the output rect (0,0 when the rect fills the window)
|
||||
left += gfx_current_game_window_viewport.x;
|
||||
top += gfx_current_game_window_viewport.y;
|
||||
// flip Y
|
||||
top = gfx_current_dimensions.height - top - 1;
|
||||
top = gfx_current_window_dimensions.height - top - 1;
|
||||
}
|
||||
if (use_back && gfx_msaa_level > 1) {
|
||||
// read from the framebuffer we've been rendering to
|
||||
|
||||
@@ -29,6 +29,11 @@ void configRegisterFloat(const char *key, float *value, float min, float max);
|
||||
/* Register a string option (buf must live for the program's lifetime). */
|
||||
void configRegisterString(const char *key, char *value, int bufSize);
|
||||
|
||||
/* Set / read a registered int/uint/float option by dotted key (clamped like
|
||||
* a file load). Return 1 if the key exists. Used by the settings presets. */
|
||||
int configSetValue(const char *key, double v);
|
||||
int configGetValue(const char *key, double *out);
|
||||
|
||||
/* [Debug] knobs, env-var-or-ini. GE_PCDUMP / GE_INPUTLOG override the ini. */
|
||||
const char *configGetFrameDump(void); /* "lo-hi[:step]" or NULL if unset */
|
||||
int configGetInputLog(void);
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
/* d318bprobe.h -- D318/D320 boundary probe (M-199 step 2).
|
||||
*
|
||||
* Env-gated per-tick ground-truth logger for the aim-hold / derail /
|
||||
* anim-pin state machine of chrlvTickAttack + chrlvTickAttackCommon.
|
||||
* Read-only; inert without GE_D318B. TEMP: remove when D318/D320
|
||||
* conclude (same class as d320repro.c).
|
||||
*/
|
||||
#ifndef D318BPROBE_H
|
||||
#define D318BPROBE_H
|
||||
|
||||
void d318bProbeTick(void);
|
||||
|
||||
#endif /* D318BPROBE_H */
|
||||
@@ -55,6 +55,8 @@ const char *sysGetExeDir(void);
|
||||
|
||||
/* Store the host argv (call once from main). */
|
||||
void sysSetArgs(int argc, char **argv);
|
||||
/* D443: relaunch this executable (same argv/cwd); atexit-time only. 0 = ok. */
|
||||
int sysRelaunchSelf(void);
|
||||
/* Nonzero if the given argument (e.g. "--debug-gl") was passed. */
|
||||
int sysArgCheck(const char *arg);
|
||||
/* Value following the given argument on the command line, or NULL. */
|
||||
|
||||
+20
-3
@@ -20,9 +20,17 @@ 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);
|
||||
/* D283 Steam Deck preset (hardware-detected; GE_FAKE_DECK overrides); runs
|
||||
* right after configLoad(), at most once per ini (see video.c). */
|
||||
void videoApplySteamDeckPreset(void);
|
||||
/* D440: "Original N64" / "Port defaults" settings presets. An
|
||||
* action, not a mode: writes the existing config keys (see the table in
|
||||
* video.c), queues the live-apply for Video.* keys, and returns how many
|
||||
* values changed. videoPresetIsActive reports whether every key currently
|
||||
* holds that preset's value. */
|
||||
enum { VIDEO_PRESET_PORT = 0, VIDEO_PRESET_N64 = 1 };
|
||||
int videoApplyPreset(int which);
|
||||
int videoPresetIsActive(int which);
|
||||
int videoInit(void);
|
||||
void videoDestroy(void);
|
||||
|
||||
@@ -43,6 +51,12 @@ void videoPumpEvents(void);
|
||||
* could be inside the GL driver (the 0x119 bugchecks). */
|
||||
void videoRequestQuit(const char *why);
|
||||
int videoQuitRequested(void);
|
||||
/* D443: orderly quit + relaunch (main.c atexit reads videoRestartRequested). */
|
||||
void videoRequestRestart(const char *why);
|
||||
int videoRestartRequested(void);
|
||||
/* D443: horizontal FOV in degrees for a Video.FovScale percent, at the
|
||||
* projection aspect the game currently uses (menu display only). */
|
||||
f32 portFovHorizDegrees(s32 pct);
|
||||
|
||||
/* The game's native video mode (NTSC 640x480, PAL 640x400). fast3d scales
|
||||
* N64 screen coordinates into window pixels using this. */
|
||||
@@ -70,6 +84,9 @@ void videoRequestLiveConfigForKey(const char *key);
|
||||
void videoRequestWindowSize(int w, int h);
|
||||
void videoRequestFullscreen(int on);
|
||||
void videoGetWindowSize(int *w, int *h);
|
||||
/* D447: the output rect as fractions of the window (0..1, top-left origin);
|
||||
* (0,0,1,1) unless Video.AspectMode = Original is letter/pillarboxing. */
|
||||
void videoGetOutputRectFrac(double *x0, double *y0, double *x1, double *y1);
|
||||
void videoGetDesktopSize(int *w, int *h);
|
||||
int videoIsFullscreen(void);
|
||||
|
||||
|
||||
+2
-2
@@ -225,7 +225,7 @@ void audioSetNextBuffer(const s16 *buf, u32 len)
|
||||
* 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")) {
|
||||
if (GE_ENVFLAG("GE_D204")) {
|
||||
static u64 startUs = 0, nextReportUs = 0, produced = 0;
|
||||
static u32 maxLen = 0;
|
||||
u64 now = sysGetMicroseconds();
|
||||
@@ -276,7 +276,7 @@ void audioSetNextBuffer(const s16 *buf, u32 len)
|
||||
* 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")) {
|
||||
if (buf && len && GE_ENVFLAG("GE_AUDIOTRACE")) {
|
||||
static u64 lastReportUs = 0;
|
||||
u64 now = sysGetMicroseconds();
|
||||
if (now - lastReportUs >= 1000000ull) {
|
||||
|
||||
@@ -272,6 +272,60 @@ static void applyKV(const char *dottedKey, const char *val)
|
||||
sysLogPrintf(LOG_NOTE, "config: unknown key '%s' (ignored)", dottedKey);
|
||||
}
|
||||
|
||||
/* Preset plumbing (Original-N64 / port-defaults presets, D440;
|
||||
* the D283 Deck preset): set / read a registered numeric option by its
|
||||
* dotted key, with the same clamp a file load applies. Returns 1 if the key
|
||||
* is registered as an int/uint/float option, 0 otherwise (strings are not
|
||||
* preset material). */
|
||||
int configSetValue(const char *key, double v)
|
||||
{
|
||||
for (int i = 0; i < numIntOpts; i++) {
|
||||
if (ci_eq(intOpts[i].key, key)) {
|
||||
int iv = (int)(v < 0 ? v - 0.5 : v + 0.5);
|
||||
if (intOpts[i].min != intOpts[i].max) {
|
||||
if (iv < intOpts[i].min) iv = intOpts[i].min;
|
||||
if (iv > intOpts[i].max) iv = intOpts[i].max;
|
||||
}
|
||||
*intOpts[i].value = iv;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < numUIntOpts; i++) {
|
||||
if (ci_eq(uintOpts[i].key, key)) {
|
||||
unsigned int uv = (v <= 0) ? 0u : (unsigned int)(v + 0.5);
|
||||
if (uintOpts[i].min != uintOpts[i].max) {
|
||||
if (uv < uintOpts[i].min) uv = uintOpts[i].min;
|
||||
if (uv > uintOpts[i].max) uv = uintOpts[i].max;
|
||||
}
|
||||
*uintOpts[i].value = uv;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < numFloatOpts; i++) {
|
||||
if (ci_eq(floatOpts[i].key, key)) {
|
||||
float fv = (float)v;
|
||||
if (floatOpts[i].min != floatOpts[i].max) {
|
||||
if (fv < floatOpts[i].min) fv = floatOpts[i].min;
|
||||
if (fv > floatOpts[i].max) fv = floatOpts[i].max;
|
||||
}
|
||||
*floatOpts[i].value = fv;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int configGetValue(const char *key, double *out)
|
||||
{
|
||||
for (int i = 0; i < numIntOpts; i++)
|
||||
if (ci_eq(intOpts[i].key, key)) { *out = *intOpts[i].value; return 1; }
|
||||
for (int i = 0; i < numUIntOpts; i++)
|
||||
if (ci_eq(uintOpts[i].key, key)) { *out = *uintOpts[i].value; return 1; }
|
||||
for (int i = 0; i < numFloatOpts; i++)
|
||||
if (ci_eq(floatOpts[i].key, key)) { *out = *floatOpts[i].value; return 1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
void configLoad(void)
|
||||
{
|
||||
const char *path = sysResolvePath(INI_PATH);
|
||||
|
||||
+99
-29
@@ -105,13 +105,48 @@ static void crashStackTraceRaw(char *msg, PEXCEPTION_POINTERS exinfo)
|
||||
}
|
||||
|
||||
/*
|
||||
* Phase 2 — backtrace by walking the EBP chain manually. The build keeps
|
||||
* frame pointers (-fno-omit-frame-pointer), so every frame is a pair of
|
||||
* qwords at RBP: [saved RBP, return address]. No dbghelp, no allocation:
|
||||
* each pointer is validated against the thread's stack bounds BEFORE being
|
||||
* dereferenced, and the chain must strictly ascend (x64 stacks grow down),
|
||||
* so a corrupted chain terminates instead of re-faulting. Addresses are
|
||||
* symbolicated offline with addr2line when needed.
|
||||
* Module info for one code address: load base, module basename, and the
|
||||
* image's *preferred* ImageBase from its PE header. addr2line wants the
|
||||
* link-time VA (preferred base + RVA), not the ASLR-relocated runtime
|
||||
* address, so each frame prints both. Reads only the loaded image's own
|
||||
* headers (always mapped while the module is loaded) — no allocation.
|
||||
*/
|
||||
static uintptr_t crashModuleInfo(uintptr_t addr, const char **name, uintptr_t *prefbase)
|
||||
{
|
||||
static char path[MAX_PATH];
|
||||
const uintptr_t base = (uintptr_t)crashGetModuleBase((const void *)addr);
|
||||
*name = "?";
|
||||
*prefbase = 0;
|
||||
if (!base) return 0;
|
||||
if (GetModuleFileNameA((HMODULE)base, path, sizeof(path))) {
|
||||
const char *slash = strrchr(path, '\\');
|
||||
*name = slash ? slash + 1 : path;
|
||||
}
|
||||
const IMAGE_DOS_HEADER *dos = (const IMAGE_DOS_HEADER *)base;
|
||||
if (dos->e_magic == IMAGE_DOS_SIGNATURE) {
|
||||
const IMAGE_NT_HEADERS *nt = (const IMAGE_NT_HEADERS *)(base + dos->e_lfanew);
|
||||
if (nt->Signature == IMAGE_NT_SIGNATURE) {
|
||||
*prefbase = (uintptr_t)nt->OptionalHeader.ImageBase;
|
||||
}
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
/*
|
||||
* Phase 2 — backtrace by x64 table-based unwinding from the exception
|
||||
* CONTEXT (RtlLookupFunctionEntry + RtlVirtualUnwind over the image's
|
||||
* .pdata/.xdata, which MinGW-w64 always emits for SEH). This replaces the
|
||||
* old RBP-chain walk, which died at frame 1: with -fno-omit-frame-pointer
|
||||
* MinGW-w64 GCC still mostly emits `lea N(%rsp),%rbp` (RBP = an offset
|
||||
* inside the frame, as the Win64 ABI allows) rather than `mov %rsp,%rbp`
|
||||
* (1386 vs 565 functions in the 2026-09-30 build), so [RBP] is usually not
|
||||
* a [saved RBP, return address] pair and the chain breaks one hop out. No dbghelp, no allocation; RSP is
|
||||
* validated against the thread's stack bounds before every unwind step so
|
||||
* a corrupted stack terminates the walk instead of re-faulting.
|
||||
*
|
||||
* Per frame: runtime address, [module base]+RVA, module name, and
|
||||
* va=<preferred ImageBase + RVA> — feed that to
|
||||
* `addr2line -f -e ge007.x86_64.exe <va>` offline.
|
||||
*/
|
||||
static void crashStackTraceSym(char *msg, PEXCEPTION_POINTERS exinfo)
|
||||
{
|
||||
@@ -125,32 +160,41 @@ static void crashStackTraceSym(char *msg, PEXCEPTION_POINTERS exinfo)
|
||||
return;
|
||||
}
|
||||
|
||||
uintptr_t fp = context.Rbp;
|
||||
int i = 0;
|
||||
CRASH_MSG("\nBACKTRACE:\n#00: %p (crash PC)\n", exinfo->ExceptionRecord->ExceptionAddress);
|
||||
int i;
|
||||
CRASH_MSG("\nBACKTRACE:\n");
|
||||
for (i = 0; i < CRASH_MAX_FRAMES; ++i) {
|
||||
const uintptr_t pc = (uintptr_t)context.Rip;
|
||||
if (!pc) break;
|
||||
|
||||
while (i < CRASH_MAX_FRAMES - 1) {
|
||||
/* The frame record must live inside the committed stack. */
|
||||
if (fp < (uintptr_t)low || fp > (uintptr_t)high) break;
|
||||
|
||||
const uintptr_t *frame = (const uintptr_t *)fp;
|
||||
const uintptr_t saved_fp = frame[0];
|
||||
const uintptr_t ret_addr = frame[1];
|
||||
|
||||
CRASH_MSG("#%02d: %p", i + 1, (void *)ret_addr);
|
||||
const uintptr_t modbase = (uintptr_t)crashGetModuleBase((void *)ret_addr);
|
||||
const char *modname;
|
||||
uintptr_t prefbase;
|
||||
const uintptr_t modbase = crashModuleInfo(pc, &modname, &prefbase);
|
||||
CRASH_MSG("#%02d: %p", i, (void *)pc);
|
||||
if (modbase) {
|
||||
CRASH_MSG(" [%p]+%llx", (void *)modbase,
|
||||
(unsigned long long)(ret_addr - modbase));
|
||||
CRASH_MSG(" [%p]+%llx %s va=0x%llx", (void *)modbase,
|
||||
(unsigned long long)(pc - modbase), modname,
|
||||
(unsigned long long)(prefbase + (pc - modbase)));
|
||||
}
|
||||
CRASH_MSG("\n");
|
||||
CRASH_MSG("%s\n", i == 0 ? " (crash PC)" : "");
|
||||
|
||||
/* Chain must ascend; stop on the first frame that doesn't. */
|
||||
if (saved_fp <= fp || saved_fp > (uintptr_t)high) break;
|
||||
fp = saved_fp;
|
||||
++i;
|
||||
/* The unwinder reads the frame at RSP; it must be on this stack. */
|
||||
if (context.Rsp < (DWORD64)(uintptr_t)low || context.Rsp >= (DWORD64)(uintptr_t)high) break;
|
||||
|
||||
DWORD64 imgbase = 0;
|
||||
PRUNTIME_FUNCTION fn = RtlLookupFunctionEntry(context.Rip, &imgbase, NULL);
|
||||
if (fn) {
|
||||
PVOID handlerdata = NULL;
|
||||
DWORD64 establisher = 0;
|
||||
RtlVirtualUnwind(UNW_FLAG_NHANDLER, imgbase, context.Rip, fn, &context,
|
||||
&handlerdata, &establisher, NULL);
|
||||
} else {
|
||||
/* Leaf function (no unwind info): return address is at RSP. */
|
||||
context.Rip = *(const DWORD64 *)(uintptr_t)context.Rsp;
|
||||
context.Rsp += 8;
|
||||
}
|
||||
if (context.Rsp < (DWORD64)(uintptr_t)low || context.Rsp > (DWORD64)(uintptr_t)high) break;
|
||||
}
|
||||
if (i == CRASH_MAX_FRAMES - 1) {
|
||||
if (i == CRASH_MAX_FRAMES) {
|
||||
CRASH_MSG("...\n");
|
||||
}
|
||||
#else
|
||||
@@ -286,7 +330,7 @@ static LONG __stdcall crashHandler(PEXCEPTION_POINTERS exinfo)
|
||||
fflush(stdout);
|
||||
crashWriteLog(msg, 0);
|
||||
|
||||
/* Phase 2: EBP-chain backtrace (validated reads only — see
|
||||
/* Phase 2: unwind-table backtrace (validated reads only — see
|
||||
* crashStackTraceSym). The raw data is already on disk either way. */
|
||||
{
|
||||
char sym[CRASH_MAX_MSG + 1] = { 0 };
|
||||
@@ -474,6 +518,31 @@ void crashDumpThreads(const unsigned long *tids, const char **names, int count)
|
||||
|
||||
int g_CrashEnabled = 0;
|
||||
|
||||
/*
|
||||
* GE_CRASHTEST=<N>: deliberate-crash hook for verifying the handler. Right
|
||||
* after the handler is installed, recurse N frames deep and fault with a
|
||||
* null write, so the log should show N crashTestRecurse frames + crashInit
|
||||
* + main + CRT frames. Unset/0 = no effect. Not a game-logic change.
|
||||
*/
|
||||
static volatile int *crashTestNull;
|
||||
|
||||
static __attribute__((noinline)) int crashTestRecurse(int depth)
|
||||
{
|
||||
volatile int guard = depth; /* keeps each frame alive (no tail call) */
|
||||
int r = (depth <= 1) ? (*crashTestNull = depth) : crashTestRecurse(depth - 1);
|
||||
return r + guard;
|
||||
}
|
||||
|
||||
static void crashTestMaybe(void)
|
||||
{
|
||||
const char *env = getenv("GE_CRASHTEST");
|
||||
const int depth = env ? atoi(env) : 0;
|
||||
if (depth > 0) {
|
||||
sysLogPrintf(LOG_WARNING, "GE_CRASHTEST=%d: faulting deliberately", depth);
|
||||
crashTestRecurse(depth > 64 ? 64 : depth);
|
||||
}
|
||||
}
|
||||
|
||||
void crashInit(void)
|
||||
{
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
@@ -490,6 +559,7 @@ void crashInit(void)
|
||||
sigaction(SIGILL, &sigact, &prevSigAction);
|
||||
g_CrashEnabled = 1;
|
||||
#endif
|
||||
crashTestMaybe();
|
||||
}
|
||||
|
||||
void crashShutdown(void)
|
||||
|
||||
@@ -1,377 +0,0 @@
|
||||
/* d318bprobe.c -- D318/D320 boundary probe (M-199 step 2).
|
||||
*
|
||||
* Purpose: capture per-tick ground truth of the aim-hold / derail /
|
||||
* anim-pin state machine so the D318/D320/D321 writeups can be rewritten
|
||||
* on verified evidence and each watchdog entry judged for excision on its
|
||||
* own data. Static analysis (this session) established:
|
||||
*
|
||||
* - The hold-pose pin is topologically stable, not a float knife-edge:
|
||||
* once modelConstrainOrWrapAnimFrame clamps both framea and frameb to
|
||||
* ceil(endframe), the tail of modelSetAnimFrame2WithChrStuff re-emits
|
||||
* the stale va==vb pair every tick (the per-frame loop only runs when
|
||||
* floor(frame) crosses an integer boundary, which a pinned animframe1
|
||||
* never does). All comparisons in the frozen state are exact integers.
|
||||
* - Post-derail, escape requires a modelSetAnimation re-init from one of
|
||||
* exactly three paths: chrlvTickAttack's unk36==0 branch (auto
|
||||
* weapons, endframe<=frame), chrlvTickAttackCommon re-init block
|
||||
* branch 1 (unk37!=0, i.e. auto weapon -> kneel-stop) or branch 3
|
||||
* (unk31 armed by the fire gate while frame sits in the recoil/shoot
|
||||
* window). None is reachable in the observed pins (auto weapons,
|
||||
* endframe unbounded) -- BUT the pin itself only persists at 1 sim
|
||||
* tick per rendered frame: a >=2-tick frame advances >=1.0 and
|
||||
* crosses the integer boundary, so no escape path is needed (D329,
|
||||
* lockstep A/B via GE_D318B_CLK below). Not a platform-invariant lock.
|
||||
*
|
||||
* This probe logs, for env-selected chrs, every operand those decisions
|
||||
* read: the drawn variant (identified by pointer against all six firing-
|
||||
* group sets), its frame fields, the unk30..unk44 gate bytes, the weapon
|
||||
* auto-firing rates, full-precision model frame/endframe/speed state, and
|
||||
* each branch condition recomputed from the same fields. Read-only;
|
||||
* inert without GE_D318B.
|
||||
*
|
||||
* Usage: GE_D318B="5,9" (comma-separated chrnums, max 4)
|
||||
* GE_D318B_ALL=1 (log every ACT_ATTACK tick, not just changes)
|
||||
* GE_D318B_CLK=N (lockstep: exactly N sim ticks per rendered frame)
|
||||
* Combined with the D320R forced repro, e.g.:
|
||||
* GE_D320R="900:jump=5,list=0x0414,off=341" GE_D318B="5"
|
||||
*
|
||||
* TEMP: investigation probe, remove when D318/D320 conclude.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "PR/os.h"
|
||||
#include "bondtypes.h"
|
||||
#include "bondaicommands.h"
|
||||
#include "chr.h"
|
||||
#include "chraction.h"
|
||||
#include "gun.h"
|
||||
#include "lv.h"
|
||||
#include "model.h"
|
||||
|
||||
#ifdef PORT
|
||||
|
||||
#define D318B_MAX_CHRS 4
|
||||
#define D318B_LINE_CAP 6000
|
||||
|
||||
typedef struct
|
||||
{
|
||||
s16 chr;
|
||||
/* last-logged state (change detection) */
|
||||
int act;
|
||||
u32 atk;
|
||||
u32 ent;
|
||||
char animid[8];
|
||||
u32 framebits;
|
||||
s32 stable; /* consecutive ticks with unchanged frame bits */
|
||||
s32 hb; /* ticks since last heartbeat log */
|
||||
} D318BState;
|
||||
|
||||
static int s_on = -1;
|
||||
static int s_all = 0; /* GE_D318B_ALL=1: log every attack tick */
|
||||
static int s_n = 0;
|
||||
static D318BState s_s[D318B_MAX_CHRS];
|
||||
static long s_lines = 0;
|
||||
static int s_capped = 0;
|
||||
|
||||
#define d318bLog osSyncPrintf
|
||||
|
||||
static void d318bInit(void)
|
||||
{
|
||||
const char *e;
|
||||
char buf[128];
|
||||
char *p, *save;
|
||||
|
||||
if (s_on != -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
s_on = 0;
|
||||
e = getenv("GE_D318B");
|
||||
if (!e || !e[0])
|
||||
{
|
||||
return;
|
||||
}
|
||||
s_on = 1;
|
||||
s_all = (getenv("GE_D318B_ALL") != NULL);
|
||||
strncpy(buf, e, sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = 0;
|
||||
p = buf;
|
||||
while (p && *p && s_n < D318B_MAX_CHRS)
|
||||
{
|
||||
save = strchr(p, ',');
|
||||
if (save)
|
||||
{
|
||||
*save = 0;
|
||||
}
|
||||
memset(&s_s[s_n], 0, sizeof(D318BState));
|
||||
s_s[s_n].chr = (s16)strtol(p, NULL, 0);
|
||||
s_s[s_n].act = -1;
|
||||
s_n++;
|
||||
p = save ? save + 1 : NULL;
|
||||
}
|
||||
d318bLog("D318B: probe active for %d chr(s): %s\n", s_n, buf);
|
||||
}
|
||||
|
||||
static ChrRecord *d318bFindChr(s16 chrnum)
|
||||
{
|
||||
s32 i;
|
||||
|
||||
for (i = 0; i < g_NumChrSlots; i++)
|
||||
{
|
||||
if (g_ChrSlots[i].chrnum == chrnum)
|
||||
{
|
||||
return &g_ChrSlots[i];
|
||||
}
|
||||
}
|
||||
for (i = 0; i < g_ActiveChrsCount; i++)
|
||||
{
|
||||
if (g_ActiveChrs[i].chrnum == chrnum)
|
||||
{
|
||||
return &g_ActiveChrs[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Identify act_attack.animfloats by pointer against all six firing-group
|
||||
* sets. Returns 1 and fills id (e.g. "P3.2" = pistol set, bearing group
|
||||
* index 3, variant row 2) on success. */
|
||||
static int d318bIdentifyAnim(struct weapon_firing_animation_table *row, char *id, int idsz)
|
||||
{
|
||||
static const struct
|
||||
{
|
||||
const char tag;
|
||||
struct anim_group_info *const *set;
|
||||
} sets[] = {
|
||||
{ 'P', ptr_pistol_firing_animation_groups },
|
||||
{ 'R', ptr_rifle_firing_animation_groups },
|
||||
{ 'D', ptr_doubles_firing_animation_groups },
|
||||
{ 'C', ptr_crouched_rifle_firing_animation_groups },
|
||||
{ 'K', ptr_crouched_pistol_firing_animation_groups },
|
||||
{ 'L', ptr_crouched_doubles_firing_animation_groups },
|
||||
};
|
||||
s32 si, gi, ri;
|
||||
|
||||
for (si = 0; si < 6; si++)
|
||||
{
|
||||
for (gi = 0; gi < 32; gi++)
|
||||
{
|
||||
struct anim_group_info *g = sets[si].set[gi];
|
||||
|
||||
if (g == NULL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (ri = 0; ri < g->len; ri++)
|
||||
{
|
||||
if (&(*g->table)[ri] == row)
|
||||
{
|
||||
snprintf(id, idsz, "%c%d.%d", sets[si].tag, gi, ri);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
snprintf(id, idsz, "-");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static s32 d318bAutoRate(ChrRecord *self, GUNHAND hand)
|
||||
{
|
||||
PropRecord *prop;
|
||||
s32 item;
|
||||
|
||||
prop = chrGetEquippedWeaponPropWithCheck(self, hand);
|
||||
if (prop == NULL || prop->chr == NULL)
|
||||
{
|
||||
return -99;
|
||||
}
|
||||
item = (s32)prop->chr->act_attack.attack_item;
|
||||
if (item < 0)
|
||||
{
|
||||
return -98;
|
||||
}
|
||||
return (s32)bondwalkItemGetAutomaticFiringRate((ITEM_IDS)item);
|
||||
}
|
||||
|
||||
/* GE_D318B_CLK=N (test-only): force exactly N sim ticks per rendered frame,
|
||||
* independent of host render speed -- a lockstep model of a console frame
|
||||
* rate of 60/N fps. Rewrites the three values lvlManageMpGame just derived
|
||||
* from speedgraphframes (the hook runs after them, before any chr tick).
|
||||
* Pause/lock frames (g_ClockTimer == 0) are left alone. */
|
||||
static void d318bClockLock(void)
|
||||
{
|
||||
static s32 s_clk = -1;
|
||||
|
||||
if (s_clk < 0)
|
||||
{
|
||||
const char *e = getenv("GE_D318B_CLK");
|
||||
|
||||
s_clk = (e && strtol(e, NULL, 10) > 0) ? (s32)strtol(e, NULL, 10) : 0;
|
||||
if (s_clk)
|
||||
{
|
||||
d318bLog("D318B: clock lock active, %d tick(s)/frame\n", s_clk);
|
||||
}
|
||||
}
|
||||
if (s_clk && g_ClockTimer > 0 && g_ClockTimer != s_clk)
|
||||
{
|
||||
g_GlobalTimer += s_clk - g_ClockTimer;
|
||||
g_ClockTimer = s_clk;
|
||||
g_GlobalTimerDelta = (f32)s_clk;
|
||||
}
|
||||
}
|
||||
|
||||
void d318bProbeTick(void)
|
||||
{
|
||||
s32 i;
|
||||
|
||||
d318bClockLock();
|
||||
d318bInit();
|
||||
if (!s_on || s_capped)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < s_n; i++)
|
||||
{
|
||||
D318BState *st = &s_s[i];
|
||||
ChrRecord *c;
|
||||
Model *m;
|
||||
struct act_attack *a;
|
||||
struct weapon_firing_animation_table *f;
|
||||
f32 fr, ef;
|
||||
u32 framebits;
|
||||
int chg, atpin, dolog, gate = 0, k;
|
||||
char animid[8];
|
||||
|
||||
c = d318bFindChr(st->chr);
|
||||
if (c == NULL || c->model == NULL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
m = c->model;
|
||||
a = &c->act_attack;
|
||||
f = a->animfloats;
|
||||
|
||||
if (c->actiontype != ACT_ATTACK && c->actiontype != ACT_ATTACKROLL)
|
||||
{
|
||||
/* reset change-detection so the next attack logs its entry */
|
||||
st->act = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
fr = m->animframe1;
|
||||
ef = m->endframe;
|
||||
memcpy(&framebits, &fr, sizeof(framebits));
|
||||
|
||||
d318bIdentifyAnim(f, animid, sizeof(animid));
|
||||
|
||||
chg = (st->act != (int)c->actiontype) || (st->atk != a->attacktype)
|
||||
|| (st->ent != a->entityid) || (strcmp(st->animid, animid) != 0);
|
||||
|
||||
if (framebits == st->framebits)
|
||||
{
|
||||
st->stable++;
|
||||
}
|
||||
else
|
||||
{
|
||||
st->stable = 0;
|
||||
}
|
||||
st->hb++;
|
||||
|
||||
atpin = ((ef >= 0.0f) && (ef <= fr + 1e-6f));
|
||||
dolog = s_all || chg || atpin || (st->stable >= 20) || (st->hb >= 60);
|
||||
if (!dolog)
|
||||
{
|
||||
goto update;
|
||||
}
|
||||
if (++s_lines >= D318B_LINE_CAP)
|
||||
{
|
||||
s_capped = 1;
|
||||
d318bLog("D318B: line cap (%ld) reached, probe silent\n",
|
||||
(long) D318B_LINE_CAP);
|
||||
return;
|
||||
}
|
||||
|
||||
/* per-hand fire-loop outcome in chrlvTickAttackCommon (source
|
||||
* order): bit0/1 = unk3a[k]==0 arm, frame in shoot window
|
||||
* [ss,se) -> ToggleHidden(1) (gun fires) + unk44=now;
|
||||
* bit4/5 = unk3a[k]!=0 arm, unk31 re-arm conditions met
|
||||
* (unk31==0, unk32 turn, frame in recoil window or >=ss). */
|
||||
for (k = 0; k < 2; k++)
|
||||
{
|
||||
if (a->unk38[k] == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (a->unk3a[k] == 0)
|
||||
{
|
||||
if (f->shoot_start_frame <= fr && fr < f->shoot_end_frame)
|
||||
{
|
||||
gate |= (1 << k);
|
||||
}
|
||||
}
|
||||
else if (a->unk31 == 0
|
||||
&& (k == a->unk32 || a->unk3a[(s32)a->unk32] == 0)
|
||||
&& ((f->recoil_start_frame >= 0.0f && f->recoil_start_frame <= fr
|
||||
&& fr <= f->recoil_end_frame)
|
||||
|| (f->recoil_start_frame < 0.0f && f->shoot_start_frame <= fr)))
|
||||
{
|
||||
gate |= (0x10 << k);
|
||||
}
|
||||
}
|
||||
|
||||
d318bLog(
|
||||
"D318B: t=%d clk=%d c%d off=%d act=%d atk=0x%x ent=%u toM=%d | v=%s "
|
||||
"st=%.9g end=%.9g ss=%.9g se=%.9g rs=%.9g re=%.9g as=%.9g ae=%.9g | "
|
||||
"31=%d 32=%d 33=%d 34=%d 36=%d 37=%d 38=%d/%d 3a=%d/%d 40=%u 44=%d atkT=%d | "
|
||||
"fr=%.9g ef=%.9g fa=%d fb=%d spd=%.9g ps=%.9g nn=%.9g oo=%.9g ts=%.9g es=%.9g "
|
||||
"b0=%.9g b4=%.9g ac=%.9g loop=%d a2=%d a2len=%u wr=%d wl=%d | "
|
||||
"aim=%d dnt=%d efle=%d trm=%d b1=%d b2=%d b3=%d gate=%d cf1=%d\n",
|
||||
(int)g_GlobalTimer, (int)g_ClockTimer, st->chr, (int)c->aioffset, (int)c->actiontype,
|
||||
(unsigned)a->attacktype, (unsigned)a->entityid, (int)a->type_of_motion,
|
||||
animid,
|
||||
(double)f->start_frame, (double)f->end_frame,
|
||||
(double)f->shoot_start_frame, (double)f->shoot_end_frame,
|
||||
(double)f->recoil_start_frame, (double)f->recoil_end_frame,
|
||||
(double)f->aim_start_frame, (double)f->aim_end_frame,
|
||||
(int)a->unk31, (int)a->unk32, (int)a->unk33, (int)a->unk34,
|
||||
(int)a->unk36, (int)a->unk37, (int)a->unk38[0], (int)a->unk38[1],
|
||||
(int)a->unk3a[0], (int)a->unk3a[1], (unsigned)a->unk40,
|
||||
(int)a->unk44, (int)a->attack_time,
|
||||
(double)fr, (double)ef, (int)m->framea, (int)m->frameb,
|
||||
(double)m->speed, (double)m->playspeed, (double)m->newspeed,
|
||||
(double)m->oldspeed, (double)m->timespeed, (double)m->elapsespeed,
|
||||
(double)m->unkb0, (double)m->unkb4, (double)m->unkac,
|
||||
(int)m->animlooping, (int)(m->anim2 != NULL),
|
||||
m->anim2 ? (unsigned)m->anim2->unk04 : 0u,
|
||||
d318bAutoRate(c, GUNRIGHT), d318bAutoRate(c, GUNLEFT),
|
||||
(int)((a->attacktype & TARGET_AIM_ONLY) != 0),
|
||||
(int)((a->attacktype & TARGET_DONTTURN) != 0),
|
||||
(int)((ef >= 0.0f) && (ef <= fr)),
|
||||
(int)(a->unk36 == 0 && f->recoil_end_frame > 0.0f && fr <= f->recoil_end_frame
|
||||
&& (ef >= 0.0f) && (ef <= fr)),
|
||||
(int)(a->unk37 != 0 || a->unk34 < a->unk33),
|
||||
(int)(a->unk33 == a->unk34),
|
||||
(int)(a->unk31 != 0),
|
||||
gate,
|
||||
(int)(a->attack_time < a->unk44 - 30 && m->anim2 == NULL
|
||||
&& f->shoot_start_frame + 10.0f < fr && fr < f->shoot_end_frame));
|
||||
|
||||
update:
|
||||
st->act = (int)c->actiontype;
|
||||
st->atk = a->attacktype;
|
||||
st->ent = a->entityid;
|
||||
strncpy(st->animid, animid, sizeof(st->animid) - 1);
|
||||
st->animid[sizeof(st->animid) - 1] = 0;
|
||||
st->framebits = framebits;
|
||||
if (dolog)
|
||||
{
|
||||
st->hb = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* PORT */
|
||||
+17
-18
@@ -1,5 +1,13 @@
|
||||
/* d318watchdog.c -- D318: port-side deadlock recovery for the Facility
|
||||
* (level_34) Ourumov/Trevelyan execution softlock.
|
||||
/* d318watchdog.c -- D318: port-side pin DETECTOR for the Facility
|
||||
* (level_34) Ourumov/Trevelyan execution softlock and its D320 siblings.
|
||||
*
|
||||
* DETECT-AND-LOG ONLY (strip-temp-probes pass, 2026-09-30): D329 fixed the
|
||||
* cause (a tick-granularity anim-pin divergence in model.c, Rule-2), so the
|
||||
* pin no longer occurs. The recovery action described below (re-seeding the
|
||||
* attack via sub_GAME_7F025560) has been REMOVED; the watchdog now only logs
|
||||
* `D318W:` lines if the signature ever reappears, so a regression of D329 is
|
||||
* visible in a playtest log instead of being silently papered over. The
|
||||
* history below is kept for context.
|
||||
*
|
||||
* What this is (full writeup: docs/dev/findings.md, D318):
|
||||
*
|
||||
@@ -113,7 +121,7 @@
|
||||
* action breaks the state. Full re-arm only when the signature has been
|
||||
* absent for D318_REARM_TICKS.
|
||||
*
|
||||
* Opt out (research only): GE_D318W=0
|
||||
* Opt out (silences the detector): GE_D318W=0
|
||||
*/
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -136,11 +144,6 @@ extern s32 chraiGetAIListID(AIRecord *AIList, bool *isGlobalAIList);
|
||||
* the AI-command header just for one bit constant. */
|
||||
#define D320_TARGET_AIM_ONLY 0x0020 /* "Aim at target instead of firing" */
|
||||
|
||||
/* chraction.c internal (not in chraction.h): the fresh-attack entry point.
|
||||
* Signature from the definition; call form mirrors chrlvTickAttack's own
|
||||
* re-init at chraction.c:7401. */
|
||||
extern void sub_GAME_7F025560(ChrRecord *self, s32 attack_type, s32 arg2);
|
||||
|
||||
#define D318_LIST_AI22 0x0417 /* Ourumov's execution list (ai_22) */
|
||||
#define D318_OURUMOV_CHR 78
|
||||
#define D318_TREVELYAN_CHR 67
|
||||
@@ -232,7 +235,7 @@ void d318WatchdogTick(void)
|
||||
static s32 s_run[D318W_NENTRIES]; /* sig-held ticks per entry */
|
||||
static s32 s_absent[D318W_NENTRIES];/* sig-absent ticks per entry */
|
||||
static s32 s_cool[D318W_NENTRIES]; /* ticks since last fire (sig up) */
|
||||
static s32 s_fires[D318W_NENTRIES]; /* rescues this occurrence */
|
||||
static s32 s_fires[D318W_NENTRIES]; /* reports this occurrence */
|
||||
static bool s_fired[D318W_NENTRIES];
|
||||
s32 e, i;
|
||||
|
||||
@@ -286,13 +289,11 @@ void d318WatchdogTick(void)
|
||||
s_run[e]++;
|
||||
if (s_run[e] >= D318_DEADLOCK_TICKS)
|
||||
{
|
||||
osSyncPrintf("D318W: t=%d [%s] anim pin confirmed -- re-initializing "
|
||||
"c%d's attack (sub_GAME_7F025560 atk=0x%x ent=%d) to break it\n",
|
||||
osSyncPrintf("D318W: t=%d [%s] anim pin CONFIRMED on c%d (atk=0x%x ent=%d) "
|
||||
"-- detect-only, no recovery; D329 should prevent this, please report\n",
|
||||
(int)g_GlobalTimer, en->tag, (int)hit->chrnum,
|
||||
(unsigned)hit->act_attack.attacktype,
|
||||
(int)hit->act_attack.entityid);
|
||||
sub_GAME_7F025560(hit, (s32)hit->act_attack.attacktype,
|
||||
(s32)hit->act_attack.entityid);
|
||||
s_fired[e] = TRUE;
|
||||
s_fires[e] = 1;
|
||||
s_cool[e] = 0;
|
||||
@@ -300,16 +301,14 @@ void d318WatchdogTick(void)
|
||||
}
|
||||
else if (++s_cool[e] >= D318W_REFIRE_TICKS)
|
||||
{
|
||||
/* The beat re-rolled and re-pinned (see header). No cap: */
|
||||
/* Signature still held: periodic reminder, detect-only. */
|
||||
s_cool[e] = 0;
|
||||
osSyncPrintf("D318W: t=%d [%s] re-pin rescued (%d) -- c%d off=%d "
|
||||
osSyncPrintf("D318W: t=%d [%s] pin still held (report %d) -- c%d off=%d "
|
||||
"atk=0x%x ent=%d\n",
|
||||
(int)g_GlobalTimer, en->tag, s_fires[e] + 1,
|
||||
(int)hit->chrnum, (int)hit->aioffset,
|
||||
(unsigned)hit->act_attack.attacktype,
|
||||
(int)hit->act_attack.entityid);
|
||||
sub_GAME_7F025560(hit, (s32)hit->act_attack.attacktype,
|
||||
(s32)hit->act_attack.entityid);
|
||||
s_fires[e]++;
|
||||
}
|
||||
}
|
||||
@@ -317,7 +316,7 @@ void d318WatchdogTick(void)
|
||||
{
|
||||
s_run[e] = 0;
|
||||
s_cool[e] = 0;
|
||||
/* Once the signature has been clear for a while after a fire,
|
||||
/* Once the signature has been clear for a while after a report,
|
||||
* re-arm so a later replay of the level in the same process
|
||||
* (AllUnlocked) is still covered. */
|
||||
if (s_fired[e] && ++s_absent[e] >= D318_REARM_TICKS)
|
||||
|
||||
@@ -1,480 +0,0 @@
|
||||
/* d320repro.c -- D320: forced-repro harness for the three high-risk
|
||||
* D318-class aim-hold softlock lists (Facility ai_19, Control ai_9,
|
||||
* Depot ai_12; findings D320).
|
||||
*
|
||||
* These beats are scripted cutscene moments that do not play in a bare
|
||||
* headless boot (the scene is static with no input), so -- exactly like
|
||||
* D318's mode-5 repro -- the probe forces them: at chosen ticks it can
|
||||
* dump the chr roster, hijack a guard's AI pointer to a loaded list at a
|
||||
* given byte offset, and/or set objective-register bits. The in-tree
|
||||
* D320T pin detector (d318watchdog.c, GE_D318T=1) then reports any
|
||||
* live-fire attack whose anim frame stays byte-stable for 600 ticks --
|
||||
* the exact frozen shape.
|
||||
*
|
||||
* TEMP: investigation probe, remove when D320 concludes. Inert without
|
||||
* the env var (zero cost).
|
||||
*
|
||||
* Usage: GE_D320R="tick:action;tick:action;..." where action is one of
|
||||
* roster -- dump every active chr (num, act,
|
||||
* list id, offset, alive)
|
||||
* jump=<chr>,list=<hexid>,off=<n> -- set the chr's ailist to
|
||||
* ailistFindById(hexid) at byte
|
||||
* offset n, sleep=0
|
||||
* bit=<hex> -- objectiveregisters1 |= hex
|
||||
* rename=<chr>,<newnum> -- reassign a chr's chrnum (used to
|
||||
* materialize an absent target, e.g.
|
||||
* Control ai_9 aims at 0xFC which is
|
||||
* not spawned in a bare boot)
|
||||
* seed=<hi32>,<lo32> -- overwrite g_randomSeed (the boot
|
||||
* seed is deterministic, so vary this
|
||||
* between runs to sample different
|
||||
* aim-variant branches)
|
||||
* explode=<type>,<dist> -- D252: explosionCreate(<type>) <dist>
|
||||
* units in front of Bond (headless
|
||||
* explosion-particle capture)
|
||||
* cheat=gl|rl|inv|<id> -- D252: cheatButtonHandleCheatsTurnedOn
|
||||
* trace=<chr> -- from this tick on, log the chr's
|
||||
* off/act/atk/ent/frame/endframe on
|
||||
* any change + a 120-tick heartbeat
|
||||
* while it is in ACT_ATTACK
|
||||
*
|
||||
* Example (Facility ai_19 hold block):
|
||||
* GE_D320R="60:roster;900:jump=5,list=0x0414,off=341;900:trace=5"
|
||||
*/
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "PR/os.h"
|
||||
#include "bondtypes.h"
|
||||
#include "chr.h"
|
||||
#include "chrai.h"
|
||||
#include "chraction.h"
|
||||
#include "lv.h"
|
||||
#include "model.h"
|
||||
#include "explosion.h"
|
||||
#include "player.h"
|
||||
#include "cheat.h"
|
||||
extern void cheatButtonHandleCheatsTurnedOn(CHEAT_ID cheat_id);
|
||||
|
||||
#ifdef PORT
|
||||
|
||||
/* chrai.c defines these but chrai.h does not declare them. */
|
||||
extern s32 chraiGetAIListID(AIRecord *AIList, bool *isGlobalAIList);
|
||||
|
||||
/* port/src/random.c; not declared in a shared header. */
|
||||
extern u64 g_randomSeed;
|
||||
|
||||
#define D320R_MAX_ACTIONS 16
|
||||
#define D320R_MAX_TRACES 4
|
||||
|
||||
typedef struct
|
||||
{
|
||||
s32 tick;
|
||||
int kind; /* 0 roster, 1 jump, 2 bit, 3 trace */
|
||||
s16 chr;
|
||||
s32 listid;
|
||||
u16 off;
|
||||
u32 bit; /* also: new chrnum for rename */
|
||||
int fired;
|
||||
} D320RAction;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
s16 chr;
|
||||
s32 off;
|
||||
s32 act;
|
||||
s32 atk;
|
||||
s32 ent;
|
||||
u32 framebits;
|
||||
s32 hb;
|
||||
} D320RTrace;
|
||||
|
||||
static ChrRecord *d320rFindChr(s16 chrnum)
|
||||
{
|
||||
s32 i;
|
||||
|
||||
for (i = 0; i <g_NumChrSlots; i++)
|
||||
{
|
||||
if (g_ChrSlots[i].chrnum == chrnum)
|
||||
{
|
||||
return &g_ChrSlots[i];
|
||||
}
|
||||
}
|
||||
for (i = 0; i < g_ActiveChrsCount; i++)
|
||||
{
|
||||
if (g_ActiveChrs[i].chrnum == chrnum)
|
||||
{
|
||||
return &g_ActiveChrs[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void d320rDumpRoster(void)
|
||||
{
|
||||
s32 i;
|
||||
|
||||
osSyncPrintf("D320R: t=%d roster slots=%d bgchrs=%d\n", (int)g_GlobalTimer,
|
||||
(int)g_NumChrSlots, (int)g_ActiveChrsCount);
|
||||
for (i = 0; i < g_NumChrSlots; i++)
|
||||
{
|
||||
ChrRecord *c = &g_ChrSlots[i];
|
||||
bool g = FALSE;
|
||||
s32 aid;
|
||||
|
||||
if (c->model == NULL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
aid = c->ailist ? chraiGetAIListID(c->ailist, &g) : -1;
|
||||
osSyncPrintf("D320R: S c%d act=%d aiid=0x%04x%s off=%d atk=0x%x ent=%d %s\n",
|
||||
(int)c->chrnum, (int)c->actiontype, (unsigned)aid,
|
||||
g ? "G" : "", (int)c->aioffset,
|
||||
(unsigned)c->act_attack.attacktype,
|
||||
(int)c->act_attack.entityid,
|
||||
chrIsDead(c) ? "DEAD" : "");
|
||||
}
|
||||
for (i = 0; i < g_ActiveChrsCount; i++)
|
||||
{
|
||||
ChrRecord *c = &g_ActiveChrs[i];
|
||||
bool g = FALSE;
|
||||
s32 aid = c->ailist ? chraiGetAIListID(c->ailist, &g) : -1;
|
||||
|
||||
osSyncPrintf("D320R: B c%d act=%d aiid=0x%04x%s off=%d\n",
|
||||
(int)c->chrnum, (int)c->actiontype, (unsigned)aid,
|
||||
g ? "G" : "", (int)c->aioffset);
|
||||
}
|
||||
}
|
||||
|
||||
void d320ReproTick(void)
|
||||
{
|
||||
static int s_on = -1; /* -1 uncached, 0 off, 1 on */
|
||||
static D320RAction s_a[D320R_MAX_ACTIONS];
|
||||
static int s_na = 0;
|
||||
static D320RTrace s_tr[D320R_MAX_TRACES];
|
||||
static int s_nt = 0;
|
||||
s32 i;
|
||||
|
||||
if (s_on < 0)
|
||||
{
|
||||
const char *e = getenv("GE_D320R");
|
||||
|
||||
s_on = (e && e[0]) ? 1 : 0;
|
||||
if (s_on)
|
||||
{
|
||||
/* Parse "tick:action;..." -- one pass, fixed budget. */
|
||||
char buf[512];
|
||||
char *p, *save;
|
||||
|
||||
strncpy(buf, e, sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = 0;
|
||||
p = buf;
|
||||
while (p && *p && s_na < D320R_MAX_ACTIONS)
|
||||
{
|
||||
char *semi = strchr(p, ';');
|
||||
char *colon;
|
||||
D320RAction *a = &s_a[s_na];
|
||||
|
||||
if (semi)
|
||||
{
|
||||
*semi = 0;
|
||||
}
|
||||
colon = strchr(p, ':');
|
||||
if (!colon)
|
||||
{
|
||||
break;
|
||||
}
|
||||
*colon = 0;
|
||||
a->tick = (s32)strtol(p, NULL, 10);
|
||||
a->kind = -1;
|
||||
a->chr = -1;
|
||||
a->listid = 0;
|
||||
a->off = 0;
|
||||
a->bit = 0;
|
||||
a->fired = 0;
|
||||
|
||||
if (strcmp(colon + 1, "roster") == 0)
|
||||
{
|
||||
a->kind = 0;
|
||||
}
|
||||
else if (strncmp(colon + 1, "jump=", 5) == 0)
|
||||
{
|
||||
const char *q = colon + 6;
|
||||
|
||||
/* documented form "jump=<chr>,list=..": a leading bare
|
||||
* number (no '=' before the first ',') is the chr */
|
||||
{
|
||||
char *eq0 = strchr(q, '=');
|
||||
char *cm0 = strchr(q, ',');
|
||||
|
||||
if (cm0 && (!eq0 || cm0 < eq0))
|
||||
{
|
||||
*cm0 = 0;
|
||||
a->chr = (s16)strtol(q, NULL, 0);
|
||||
q = cm0 + 1;
|
||||
}
|
||||
}
|
||||
|
||||
while (q && *q)
|
||||
{
|
||||
char *eq = strchr(q, '=');
|
||||
char *cm = strchr(q, ',');
|
||||
const char *key;
|
||||
|
||||
if (!eq)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (cm && cm < eq)
|
||||
{
|
||||
break; /* malformed */
|
||||
}
|
||||
*eq = 0;
|
||||
key = q; /* const-cast: parsed in place below */
|
||||
if (strcmp(key, "chr") == 0) { a->chr = (s16)strtol(eq + 1, NULL, 0); }
|
||||
else if (strcmp(key, "list") == 0){ a->listid = (s32)strtol(eq + 1, NULL, 0); }
|
||||
else if (strcmp(key, "off") == 0) { a->off = (u16)strtol(eq + 1, NULL, 0); }
|
||||
q = cm ? cm + 1 : NULL;
|
||||
}
|
||||
if (a->chr >= 0 && a->listid)
|
||||
{
|
||||
a->kind = 1;
|
||||
}
|
||||
}
|
||||
else if (strncmp(colon + 1, "bit=", 4) == 0)
|
||||
{
|
||||
a->bit = (u32)strtol(colon + 5, NULL, 0);
|
||||
a->kind = 2;
|
||||
}
|
||||
else if (strncmp(colon + 1, "trace=", 6) == 0)
|
||||
{
|
||||
a->chr = (s16)strtol(colon + 7, NULL, 0);
|
||||
a->kind = 3;
|
||||
}
|
||||
else if (strncmp(colon + 1, "rename=", 7) == 0)
|
||||
{
|
||||
const char *q = colon + 8;
|
||||
char *cm = strchr(q, ',');
|
||||
|
||||
if (cm)
|
||||
{
|
||||
*cm = 0;
|
||||
a->chr = (s16)strtol(q, NULL, 0);
|
||||
a->bit = (u32)strtol(cm + 1, NULL, 0);
|
||||
a->kind = 4;
|
||||
}
|
||||
}
|
||||
else if (strncmp(colon + 1, "cheat=", 6) == 0)
|
||||
{
|
||||
const char *q = colon + 7;
|
||||
|
||||
a->listid = (strcmp(q, "gl") == 0) ? CHEAT_2X_GRENADE_LAUNCHER
|
||||
: (strcmp(q, "rl") == 0) ? CHEAT_2X_ROCKET_LAUNCHER
|
||||
: (strcmp(q, "inv") == 0) ? CHEAT_INVINCIBILITY
|
||||
: (s32)strtol(q, NULL, 0);
|
||||
a->kind = 7;
|
||||
}
|
||||
else if (strncmp(colon + 1, "explode=", 8) == 0)
|
||||
{
|
||||
const char *q = colon + 9;
|
||||
char *cm = strchr(q, ',');
|
||||
|
||||
a->listid = (s32)strtol(q, NULL, 0); /* type */
|
||||
a->bit = cm ? (u32)strtol(cm + 1, NULL, 0) : 400; /* dist */
|
||||
a->kind = 6;
|
||||
}
|
||||
else if (strncmp(colon + 1, "seed=", 5) == 0)
|
||||
{
|
||||
const char *q = colon + 6;
|
||||
char *cm = strchr(q, ',');
|
||||
|
||||
if (cm)
|
||||
{
|
||||
*cm = 0;
|
||||
a->listid = (s32)(u32)strtol(q, NULL, 0); /* hi */
|
||||
a->bit = (u32)strtol(cm + 1, NULL, 0); /* lo */
|
||||
a->kind = 5;
|
||||
}
|
||||
}
|
||||
if (a->kind >= 0)
|
||||
{
|
||||
s_na++;
|
||||
}
|
||||
p = semi ? semi + 1 : NULL;
|
||||
}
|
||||
osSyncPrintf("D320R: armed %d actions\n", s_na);
|
||||
}
|
||||
}
|
||||
if (!s_on)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < s_na; i++)
|
||||
{
|
||||
D320RAction *a = &s_a[i];
|
||||
|
||||
if (a->fired || (s32)g_GlobalTimer < a->tick)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
a->fired = 1;
|
||||
|
||||
switch (a->kind)
|
||||
{
|
||||
case 0: /* roster */
|
||||
d320rDumpRoster();
|
||||
break;
|
||||
case 1: /* jump */
|
||||
{
|
||||
ChrRecord *c = d320rFindChr(a->chr);
|
||||
|
||||
if (c)
|
||||
{
|
||||
AIRecord *list = ailistFindById(a->listid);
|
||||
|
||||
if (list)
|
||||
{
|
||||
c->ailist = list;
|
||||
c->aioffset = a->off;
|
||||
c->sleep = 0;
|
||||
osSyncPrintf("D320R: t=%d JUMP c%d -> aiid=0x%04x off=%d\n",
|
||||
(int)g_GlobalTimer, (int)c->chrnum,
|
||||
(unsigned)a->listid, (int)a->off);
|
||||
}
|
||||
else
|
||||
{
|
||||
osSyncPrintf("D320R: t=%d JUMP FAILED c%d aiid=0x%04x not found\n",
|
||||
(int)g_GlobalTimer, (int)c->chrnum,
|
||||
(unsigned)a->listid);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
osSyncPrintf("D320R: t=%d JUMP FAILED c%d not active\n",
|
||||
(int)g_GlobalTimer, (int)a->chr);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 7: /* cheat (D252) */
|
||||
cheatButtonHandleCheatsTurnedOn((CHEAT_ID)a->listid);
|
||||
osSyncPrintf("D320R: t=%d CHEAT %d\n",(int)g_GlobalTimer, (int)a->listid);
|
||||
break;
|
||||
case 6: /* explode (D252) */
|
||||
if (g_CurrentPlayer && g_CurrentPlayer->prop)
|
||||
{
|
||||
struct coord3d pos = g_CurrentPlayer->prop->pos;
|
||||
|
||||
pos.f[0] += -g_CurrentPlayer->vv_sintheta * (f32)a->bit;
|
||||
pos.f[2] += g_CurrentPlayer->vv_costheta * (f32)a->bit;
|
||||
explosionCreate(0, &pos, g_CurrentPlayer->prop->stan, (s16)a->listid,
|
||||
0, 0, g_CurrentPlayer->prop->rooms, 0);
|
||||
osSyncPrintf("D320R: t=%d EXPLODE type=%d dist=%u\n",
|
||||
(int)g_GlobalTimer, (int)a->listid, (unsigned)a->bit);
|
||||
}
|
||||
break;
|
||||
case 2: /* bit */
|
||||
objectiveregisters1 |= a->bit;
|
||||
osSyncPrintf("D320R: t=%d BIT |= 0x%08x (now 0x%08x)\n",
|
||||
(int)g_GlobalTimer, (unsigned)a->bit,
|
||||
(unsigned)objectiveregisters1);
|
||||
break;
|
||||
case 3: /* trace */
|
||||
if (s_nt < D320R_MAX_TRACES)
|
||||
{
|
||||
s_tr[s_nt].chr = a->chr;
|
||||
s_tr[s_nt].off = -1;
|
||||
s_tr[s_nt].act = -1;
|
||||
s_tr[s_nt].atk = -1;
|
||||
s_tr[s_nt].ent = -1;
|
||||
s_tr[s_nt].framebits = 0;
|
||||
s_tr[s_nt].hb = 0;
|
||||
s_nt++;
|
||||
osSyncPrintf("D320R: t=%d TRACE c%d armed\n",
|
||||
(int)g_GlobalTimer, (int)a->chr);
|
||||
}
|
||||
break;
|
||||
case 4: /* rename */
|
||||
{
|
||||
ChrRecord *c = d320rFindChr(a->chr);
|
||||
|
||||
if (c)
|
||||
{
|
||||
osSyncPrintf("D320R: t=%d RENAME c%d -> c%d\n",
|
||||
(int)g_GlobalTimer, (int)c->chrnum,
|
||||
(int)a->bit);
|
||||
c->chrnum = (s16)a->bit;
|
||||
}
|
||||
else
|
||||
{
|
||||
osSyncPrintf("D320R: t=%d RENAME FAILED c%d not found\n",
|
||||
(int)g_GlobalTimer, (int)a->chr);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 5: /* seed */
|
||||
g_randomSeed = ((u64)(u32)a->listid << 32) | (u32)a->bit;
|
||||
osSyncPrintf("D320R: t=%d SEED := %08x%08x\n",
|
||||
(int)g_GlobalTimer, (unsigned)(u32)(g_randomSeed >> 32),
|
||||
(unsigned)(u32)g_randomSeed);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < s_nt; i++)
|
||||
{
|
||||
D320RTrace *t = &s_tr[i];
|
||||
ChrRecord *c = d320rFindChr(t->chr);
|
||||
u32 fb = 0;
|
||||
f32 fr = 0.0f, ef = -1.0f;
|
||||
|
||||
if (!c)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (c->model)
|
||||
{
|
||||
union { f32 f; u32 u; } x;
|
||||
|
||||
x.f = modelGetAnimFrame(c->model);
|
||||
fb = x.u;
|
||||
fr = x.f;
|
||||
ef = modelGetAnimEndFrame(c->model);
|
||||
}
|
||||
|
||||
if (t->off != (s32)c->aioffset || t->act != (s32)c->actiontype
|
||||
|| t->atk != (s32)c->act_attack.attacktype
|
||||
|| t->ent != (s32)c->act_attack.entityid)
|
||||
{
|
||||
osSyncPrintf("D320R: t=%d c%d off=%d act=%d atk=0x%x ent=%d frame=%.2f endf=%.2f\n",
|
||||
(int)g_GlobalTimer, (int)c->chrnum, (int)c->aioffset,
|
||||
(int)c->actiontype, (unsigned)c->act_attack.attacktype,
|
||||
(int)c->act_attack.entityid, (double)fr, (double)ef);
|
||||
t->off = (s32)c->aioffset;
|
||||
t->act = (s32)c->actiontype;
|
||||
t->atk = (s32)c->act_attack.attacktype;
|
||||
t->ent = (s32)c->act_attack.entityid;
|
||||
}
|
||||
else if (c->actiontype == ACT_ATTACK && fb != t->framebits)
|
||||
{
|
||||
/* frame is moving: note the range so a later stall is visible */
|
||||
osSyncPrintf("D320R: t=%d c%d frame MOVED to %.2f (endf=%.2f off=%d)\n",
|
||||
(int)g_GlobalTimer, (int)c->chrnum, (double)fr,
|
||||
(double)ef, (int)c->aioffset);
|
||||
}
|
||||
t->framebits = fb;
|
||||
|
||||
if (c->actiontype == ACT_ATTACK && ++t->hb >= 120)
|
||||
{
|
||||
t->hb = 0;
|
||||
osSyncPrintf("D320R: t=%d c%d hb off=%d act=%d atk=0x%x ent=%d frame=%.2f endf=%.2f\n",
|
||||
(int)g_GlobalTimer, (int)c->chrnum, (int)c->aioffset,
|
||||
(int)c->actiontype, (unsigned)c->act_attack.attacktype,
|
||||
(int)c->act_attack.entityid, (double)fr, (double)ef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* PORT */
|
||||
+77
-18
@@ -117,6 +117,13 @@ static int s_pageno = 0; /* D406: page within the active section */
|
||||
static int s_rowTotal = 0; /* D406: visible rows in the active section */
|
||||
static int s_subHeader = -1; /* rows[] header index for the nested page */
|
||||
static int s_hl = -1; /* highlighted row / section, -1 = none */
|
||||
/* D444: hover model. s_hl is now PERSISTENT (set by hover, wheel/keys or
|
||||
* paging); hover only re-selects when the pointer MOVES onto a DIFFERENT row
|
||||
* than the last row it hovered (s_lastHover), and never touches the page. The
|
||||
* keyboard/wheel snap (cursorToItemRaw) is not pointer motion (s_prevH/V). */
|
||||
static int s_hoverHit = -1; /* row under the pointer this frame, -1 = none */
|
||||
static int s_lastHover = -2; /* last hovered row (-2 = none yet) */
|
||||
static f32 s_prevH = -1e9f, s_prevV = -1e9f; /* cursor at the previous frame */
|
||||
static int s_dragRow = -1; /* global row index being dragged */
|
||||
static int s_repeatDir = 0, s_repeatTimer = 0;
|
||||
/* D345(e): hold-to-repeat state for vertical row stepping (separate from
|
||||
@@ -245,6 +252,8 @@ static void cursorToItemRaw(int k)
|
||||
* real mouse, never at this snap position. */
|
||||
cursor_h_pos = 44.0f;
|
||||
cursor_v_pos = (f32)(rowY(k) + 6);
|
||||
s_prevH = cursor_h_pos; /* D444: our own snap is not pointer motion */
|
||||
s_prevV = cursor_v_pos;
|
||||
}
|
||||
|
||||
/* Put the crosshair on item k (entering a page / going back). */
|
||||
@@ -286,6 +295,9 @@ void frontOptionsMenuInit(void)
|
||||
s_pageno = 0; /* D406 */
|
||||
s_subHeader = -1;
|
||||
s_hl = -1;
|
||||
s_hoverHit = -1;
|
||||
s_lastHover = -2;
|
||||
s_prevH = s_prevV = -1e9f;
|
||||
s_dragRow = -1;
|
||||
s_repeatDir = 0;
|
||||
s_vrepeatDir = 0;
|
||||
@@ -346,7 +358,7 @@ static void goBack(void)
|
||||
for (int k = 0; k < s_rowN; k++)
|
||||
if (optionsRowChildHeader(s_rowIdx[k]) == child) selected = k + 1;
|
||||
cursorToItem(selected);
|
||||
s_hl = -1;
|
||||
s_hl = -1; s_lastHover = -2;
|
||||
sysLogPrintf(LOG_INFO, "frontoptions: back to %s", optionsRowLabel(activeHeader()));
|
||||
return;
|
||||
}
|
||||
@@ -362,6 +374,24 @@ static void goBack(void)
|
||||
frontChangeMenu(MENU_FILE_SELECT, FALSE);
|
||||
}
|
||||
|
||||
/* D444: Previous/Next page controls on the bottom hint row of a multi-page
|
||||
* section. Returns -1 (previous), +1 (next) or 0 (none / not over an enabled
|
||||
* control). Same geometry the draw code uses. */
|
||||
static const char kPrevPage[] = "Previous page";
|
||||
static const char kNextPage[] = "Next page";
|
||||
static int pageCtlAt(void)
|
||||
{
|
||||
if (s_level < 1 || s_rowTotal <= ROWS_PER_PAGE || tab_prev_highlight) return 0;
|
||||
if (cursor_v_pos < (f32)(rowY(s_rowN + 1) - 3) ||
|
||||
cursor_v_pos >= (f32)(rowY(s_rowN + 1) + ROW_DY - 3)) return 0;
|
||||
int last = s_pageno * ROWS_PER_PAGE + s_rowN >= s_rowTotal;
|
||||
if (s_pageno > 0 && cursor_h_pos >= ROW_X - 2 &&
|
||||
cursor_h_pos <= ROW_X + measureW(kPrevPage) + 4) return -1;
|
||||
if (!last && cursor_h_pos <= VAL_R + 2 &&
|
||||
cursor_h_pos >= VAL_R - measureW(kNextPage) - 4) return +1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void frontOptionsMenuInterface(void)
|
||||
{
|
||||
viSetFovY(FOV_Y_F);
|
||||
@@ -381,32 +411,53 @@ void frontOptionsMenuInterface(void)
|
||||
|
||||
/* Highlight: as the cheat screen, recomputed while A is not held. */
|
||||
if (joyGetButtons(PLAYER_1, A_BUTTON | Z_TRIG) == 0) {
|
||||
int hit = -1;
|
||||
int moved = (cursor_h_pos != s_prevH || cursor_v_pos != s_prevV);
|
||||
tab_prev_highlight = FALSE;
|
||||
s_hl = -1;
|
||||
if (frontCheckCursorOnPreviousTab()) {
|
||||
tab_prev_highlight = TRUE;
|
||||
} else if (cursor_h_pos >= ROW_HIT_X0 && cursor_h_pos <= ROW_HIT_X1) {
|
||||
for (int k = itemCount() - 1; k >= 0; k--) {
|
||||
if (cursor_v_pos >= (f32)(rowY(k) - 3) &&
|
||||
cursor_v_pos < (f32)(rowY(k) + ROW_DY - 3)) {
|
||||
s_hl = k;
|
||||
hit = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* D407(b): past the top/bottom edge of the visible rows, clamp
|
||||
* the highlight to the edge row instead of dropping it (a
|
||||
* mouse-only user keeps a row selected at the page edge; wheel
|
||||
* / S then turns the page). */
|
||||
if (s_hl < 0 && itemCount() > 0) {
|
||||
* the hit to the edge row instead of dropping it. */
|
||||
if (hit < 0 && itemCount() > 0) {
|
||||
if (cursor_v_pos < (f32)rowY(0))
|
||||
s_hl = 0;
|
||||
hit = 0;
|
||||
else if (cursor_v_pos <= (f32)(rowY(itemCount() - 1) + ROW_DY))
|
||||
s_hl = itemCount() - 1;
|
||||
hit = itemCount() - 1;
|
||||
}
|
||||
}
|
||||
s_hoverHit = hit;
|
||||
/* D444: hover selects only when the pointer moved onto a DIFFERENT
|
||||
* row; a small nudge inside the same row (or none at all) leaves the
|
||||
* wheel/key/page-chosen selection alone. */
|
||||
if (moved && hit != s_lastHover) {
|
||||
s_lastHover = hit;
|
||||
s_hl = hit;
|
||||
}
|
||||
s_prevH = cursor_h_pos;
|
||||
s_prevV = cursor_v_pos;
|
||||
}
|
||||
|
||||
if (joyGetButtonsPressedThisFrame(PLAYER_1, A_BUTTON | Z_TRIG | START_BUTTON)) {
|
||||
/* D444: Previous/Next page controls (mouse click; Up/Down keep paging). */
|
||||
int pageCtl = pageCtlAt();
|
||||
if (pageCtl != 0 && joyGetButtonsPressedThisFrame(PLAYER_1, A_BUTTON | Z_TRIG)) {
|
||||
playSfx(DOOR_LOCK_SFX);
|
||||
s_pageno += pageCtl;
|
||||
buildPages();
|
||||
s_hl = -1; /* wheel/keys/hover pick a row afresh; no snap */
|
||||
} else if (joyGetButtonsPressedThisFrame(PLAYER_1, A_BUTTON | Z_TRIG | START_BUTTON)) {
|
||||
/* D444: a click acts on the row under the pointer, not on a stale
|
||||
* wheel/key selection (keyboard Enter: the snap put the pointer on
|
||||
* s_hl, so the hit equals it). */
|
||||
if (!tab_prev_highlight && joyGetButtonsPressedThisFrame(PLAYER_1, A_BUTTON | Z_TRIG))
|
||||
s_hl = s_hoverHit;
|
||||
if (tab_prev_highlight) {
|
||||
goBack();
|
||||
} else if (s_hl >= 0 && s_level == 0) {
|
||||
@@ -417,7 +468,7 @@ void frontOptionsMenuInterface(void)
|
||||
sysLogPrintf(LOG_INFO, "frontoptions: section %d", s_page + 1);
|
||||
buildPages();
|
||||
cursorToItem(0);
|
||||
s_hl = -1;
|
||||
s_hl = -1; s_lastHover = -2;
|
||||
} else if (s_hl == 0 && s_level >= 1) {
|
||||
/* D356: the top save-file row -- A steps to the next file (the
|
||||
* D352 chooser; folders only, "none" retired, plan §5.5). */
|
||||
@@ -442,7 +493,7 @@ void frontOptionsMenuInterface(void)
|
||||
sysLogPrintf(LOG_INFO, "frontoptions: opened %s (depth %d)",
|
||||
optionsRowLabel(child), s_level);
|
||||
cursorToItem(0);
|
||||
s_hl = -1;
|
||||
s_hl = -1; s_lastHover = -2;
|
||||
} else if (optionsRowIsSlider(i) && bx0 <= BAR_X1 - 12 &&
|
||||
cursor_h_pos >= bx0 - 4 && cursor_h_pos <= BAR_X1 + 4) {
|
||||
optionsRowSetFraction(i, ((double)cursor_h_pos - bx0) / (BAR_X1 - bx0));
|
||||
@@ -724,7 +775,7 @@ Gfx *frontOptionsMenuDraw(Gfx *DL)
|
||||
if (buf[0]) {
|
||||
u32 col = (buf[0] == 'O' && buf[1] == 'N') ? INK_ON : INK;
|
||||
/* D346: kOnOff is title-case; M7 (D387) tags the
|
||||
* experimental AllUnlocked ON value "ON - UNSAFE". */
|
||||
* experimental AllUnlocked ON value "ON (restart)". */
|
||||
strcat(buf, "\n");
|
||||
DL = inkR(DL, VAL_R, y, buf, col);
|
||||
}
|
||||
@@ -743,11 +794,19 @@ Gfx *frontOptionsMenuDraw(Gfx *DL)
|
||||
/* D406d: one row below the last content row (rowY(s_rowN + 1))
|
||||
* -- pitch-aligned with the list; +2 sat ~26px clear of it and
|
||||
* looked orphaned at the paper's bottom edge. */
|
||||
DL = ink(DL, ROW_X, rowY(s_rowN + 1),
|
||||
first ? "Down: next page\n"
|
||||
: last ? "Up: previous page\n"
|
||||
: "Up: previous page Down: next page\n",
|
||||
INK_DIM);
|
||||
int hov = pageCtlAt(), hy = rowY(s_rowN + 1);
|
||||
if (!first) {
|
||||
if (hov < 0)
|
||||
DL = microcode_constructor_related_to_menus(DL, ROW_X - 2, hy - 1,
|
||||
ROW_X + measureW(kPrevPage) + 4, hy + 0xE, HILITE);
|
||||
DL = ink(DL, ROW_X, hy, "Previous page\n", INK);
|
||||
}
|
||||
if (!last) {
|
||||
if (hov > 0)
|
||||
DL = microcode_constructor_related_to_menus(DL, VAL_R - measureW(kNextPage) - 4, hy - 1,
|
||||
VAL_R + 2, hy + 0xE, HILITE);
|
||||
DL = inkR(DL, VAL_R, hy, "Next page\n", INK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
#include <PR/ultratypes.h>
|
||||
#include <PR/gbi.h>
|
||||
#include "gimgfixup.h"
|
||||
extern char *getenv(const char *name); /* D324: no CRT proto via GE stdlib.h */
|
||||
#include "envflag.h"
|
||||
extern void osSyncPrintf(const char *fmt, ...);
|
||||
|
||||
#define GIMG_REGION_SIZE 0x13F8u
|
||||
|
||||
@@ -112,7 +109,6 @@ static int s_imgslot_learned = 0;
|
||||
void gimgSyncCompiledGlobalDLs(u8 *base)
|
||||
{
|
||||
int i;
|
||||
int nslots = 0, nchanged = 0; /* D252 TEMP verify counters */
|
||||
u8 *end = base + GIMG_REGION_SIZE;
|
||||
|
||||
for (i = 0; i < N_DLS; i++)
|
||||
@@ -147,9 +143,6 @@ void gimgSyncCompiledGlobalDLs(u8 *base)
|
||||
}
|
||||
if (s_imgslot[i][j])
|
||||
{
|
||||
nslots++;
|
||||
if ((u32)dst[j].words.w1 != *(u32 *)(p + 4))
|
||||
nchanged++;
|
||||
dst[j].words.w1 = *(u32 *)(p + 4);
|
||||
}
|
||||
p += 8;
|
||||
@@ -157,8 +150,4 @@ void gimgSyncCompiledGlobalDLs(u8 *base)
|
||||
}
|
||||
}
|
||||
s_imgslot_learned = 1;
|
||||
/* D252 TEMP: before the fix, every stage after the first left
|
||||
* `nchanged` explosion texture pointers stale. */
|
||||
if (GE_ENVFLAG("GE_D252POOL"))
|
||||
osSyncPrintf("D252SYNC: %d texture slots re-synced, %d changed\n", nslots, nchanged);
|
||||
}
|
||||
+12
-1
@@ -83,6 +83,7 @@
|
||||
#include "envflag.h"
|
||||
#include "optionsoverlay.h"
|
||||
#include "frontoptions.h"
|
||||
#include "video.h"
|
||||
/* D194 absolute aim: read-only access to the live camera (struct player).
|
||||
* Game header pulled in through the same shim path every other compiled game
|
||||
* file uses; we only READ vv_theta/vv_verta/speedtheta/speedverta/aspect. */
|
||||
@@ -1802,6 +1803,17 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
|
||||
if ((mx != lastMenuMouseX || my != lastMenuMouseY) ||
|
||||
(menuPointerTransitionFrames > 0 && mouseRecent)) {
|
||||
double fx = (double)mx / (double)ww;
|
||||
double fy = (double)my / (double)wh;
|
||||
/* D447: Video.AspectMode = Original draws into a
|
||||
* letter/pillarboxed rect; map across that rect. */
|
||||
{
|
||||
double rx0, ry0, rx1, ry1;
|
||||
videoGetOutputRectFrac(&rx0, &ry0, &rx1, &ry1);
|
||||
if (rx1 > rx0 && ry1 > ry0) {
|
||||
fx = (fx - rx0) / (rx1 - rx0);
|
||||
fy = (fy - ry0) / (ry1 - ry0);
|
||||
}
|
||||
}
|
||||
/* D335: under native widescreen the front end is
|
||||
* pillarboxed to a centred 4:3 region
|
||||
* (front.c menu_jump_constructor_handler), so map
|
||||
@@ -1814,7 +1826,6 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
|
||||
fx = (fx - (1.0 - vis) * 0.5) / vis;
|
||||
}
|
||||
}
|
||||
double fy = (double)my / (double)wh;
|
||||
if (fx < 0.0) fx = 0.0; else if (fx > 1.0) fx = 1.0;
|
||||
if (fy < 0.0) fy = 0.0; else if (fy > 1.0) fy = 1.0;
|
||||
cursor_h_pos = (float)(loH + fx * (hiH - loH));
|
||||
|
||||
+16
-181
@@ -608,7 +608,8 @@ s32 osSendMesg(OSMesgQueue *mq, OSMesg msg, s32 flag)
|
||||
PortQueue *pq = portQueueGet(mq);
|
||||
d62log("SEND", mq, msg); /* TEMP D62 */
|
||||
/* TEMP D51: watch sends to the 32-slot queues (sched cmdQ + client Qs) */
|
||||
if (getenv("GE_D51") && mq->msgCount == 32 && !(g_viRetraceMQ && mq == g_viRetraceMQ)) {
|
||||
static int s_d51 = -1; if (s_d51 < 0) s_d51 = getenv("GE_D51") != NULL; /* D302: per-message path */
|
||||
if (s_d51 && mq->msgCount == 32 && !(g_viRetraceMQ && mq == g_viRetraceMQ)) {
|
||||
void *ra = __builtin_return_address(0);
|
||||
sysLogPrintf(LOG_NOTE, "D51 send32 mq=%p from %p msg=%p valid=%d/%d",
|
||||
(void *)mq, ra, (void *)msg, mq->validCount, mq->msgCount);
|
||||
@@ -633,7 +634,8 @@ s32 osSendMesg(OSMesgQueue *mq, OSMesg msg, s32 flag)
|
||||
* message object's .type field was clobbered. */
|
||||
extern OSMesgQueue gfxFrameMsgQ; /* src/init.c */
|
||||
static void d60logRecv(OSMesgQueue *mq, OSMesg m) {
|
||||
if (mq != &gfxFrameMsgQ || !getenv("GE_D60")) return;
|
||||
static int s_d60r = -1; if (s_d60r < 0) s_d60r = getenv("GE_D60") != NULL; /* D302 */
|
||||
if (mq != &gfxFrameMsgQ || !s_d60r) return;
|
||||
static uint64_t n = 0;
|
||||
const u16 type = *(const u16 *)m; /* OSScMsg.type */
|
||||
if (n < 20 || (n % 300) == 0 || (type != 1 && type != 4))
|
||||
@@ -890,7 +892,8 @@ extern uint32_t pcmodelsTotalSize(void);
|
||||
extern uintptr_t pccgSidecarBase(void);
|
||||
extern uint32_t pccgTotalSize(void);
|
||||
static void d60logSidecarRead(u32 srcPA, void *dstVA, u32 size) {
|
||||
if (!getenv("GE_D60")) return;
|
||||
static int s_d60s = -1; if (s_d60s < 0) s_d60s = getenv("GE_D60") != NULL; /* D302 */
|
||||
if (!s_d60s) return;
|
||||
uintptr_t base = pcmodelsSidecarBase();
|
||||
if (base && srcPA >= base && srcPA < base + pcmodelsTotalSize()) {
|
||||
sysLogPrintf(LOG_NOTE, "D60 sidecar read off=%llu size=0x%X dst=%p",
|
||||
@@ -958,7 +961,8 @@ static void piServiceDma(s32 direction, u32 srcPA, void *dstVA, u32 size)
|
||||
* the game-side caller can be symbolicated offline. */
|
||||
/* TEMP D61: log every ROM read (dst/src/size). The last line before
|
||||
* a crash identifies the culprit; dst symbolizes offline via nm. */
|
||||
if (!s_d61opened && getenv("GE_D61")) {
|
||||
static int s_d61env = -1; if (s_d61env < 0) s_d61env = getenv("GE_D61") != NULL; /* D302 */
|
||||
if (!s_d61opened && s_d61env) {
|
||||
s_d61log = fopen("d61dma.log", "w");
|
||||
s_d61opened = 1;
|
||||
}
|
||||
@@ -1196,175 +1200,13 @@ static void geEepromStore(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* D257: Game.AllUnlocked -- mirror of src/game/file.h save_data (96 bytes;
|
||||
* keep in sync). Two things are patched in at read time:
|
||||
* - the progression-gated cheat-unlock bits (front.c
|
||||
* frontCheckIfCheatIsUnlocked) test per-LEVEL bits in
|
||||
* unlocked_cheats_1/2/3 that normal play sets when each level is
|
||||
* completed, so a fresh save has an empty cheat menu;
|
||||
* - every stage/difficulty completion time. When ANY single-player cheat
|
||||
* is active, g_AppendCheatSinglePlayer makes the mission-select screen
|
||||
* (front.c get_highest_unlocked_difficulty_for_level) only accept levels
|
||||
* whose status is STAGESTATUS_COMPLETED (3), not merely UNLOCKED (1) --
|
||||
* so AllUnlocked must read as a fully-completed campaign or enabling a
|
||||
* cheat locks every level. Existing (nonzero) player times are kept;
|
||||
* only empty slots get the max time (0x3FF). */
|
||||
typedef struct ge_save_slot {
|
||||
s32 chksum1;
|
||||
s32 chksum2;
|
||||
u8 completion_bitflags;
|
||||
u8 flag_007;
|
||||
u8 music_vol;
|
||||
u8 sfx_vol;
|
||||
u16 options;
|
||||
u8 unlocked_cheats_1;
|
||||
u8 unlocked_cheats_2;
|
||||
u8 unlocked_cheats_3;
|
||||
u8 padding;
|
||||
u8 times[76]; /* (SP_LEVEL_MAX-1)*4: 19 levels x 4 difficulties */
|
||||
} ge_save_slot; /* sizeof == 96 == save_data (file.h) -- enforced below */
|
||||
|
||||
/* If this ever fires, the mirror drifted from save_data and the AllUnlocked
|
||||
* cheat patch silently no-ops (the block-read size check stops matching). */
|
||||
typedef char ge_save_slot_size_check[(sizeof(ge_save_slot) == 96) ? 1 : -1];
|
||||
|
||||
/* Game-side CRC (src/game/crc.c) over [completion_bitflags, next slot),
|
||||
* stored in the slot's own chksum1/2 -- exactly what fileValidateSaves
|
||||
* re-checks after the block read. */
|
||||
extern void fileGenerateCRC(u8 *addressA, u8 *addressB, void *retval);
|
||||
|
||||
extern int geLegacyCrcMaybeMigrateSlots(u8 *slots5); /* port/src/legacycrc.c (D297) */
|
||||
|
||||
extern s32 portAllUnlocked; /* port/src/video.c */
|
||||
|
||||
/* Bit math mirrors fileGetSaveStageDifficultyTime / fileSetDifficultyStageTime
|
||||
* (src/game/file2.c): 10-bit time fields, offset = (difficulty*20+level)*10,
|
||||
* difficulties 0..2 (agent/secret/00) only -- 007 times are virtual. */
|
||||
static u32 geSaveGetTime(const u8 *t, s32 difficulty, s32 level)
|
||||
{
|
||||
s32 offset = (difficulty * 20 + level) * 10;
|
||||
s32 index = offset >> 3;
|
||||
switch (7 - (offset & 7)) {
|
||||
case 7: return ((u32)(t[index] & 0xFF) << 2) | ((u32)(t[index + 1] & 0xc0) >> 6);
|
||||
case 5: return ((u32)(t[index] & 0x3f) << 4) | ((u32)(t[index + 1] & 0xf0) >> 4);
|
||||
case 3: return ((u32)(t[index] & 0x0f) << 6) | ((u32)(t[index + 1] & 0xfc) >> 2);
|
||||
case 1: return ((u32)(t[index] & 0x03) << 8) | (u32)(t[index + 1] & 0xFF);
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void geSaveSetTime(u8 *t, s32 difficulty, s32 level, u32 newtime)
|
||||
{
|
||||
s32 offset = (difficulty * 20 + level) * 10;
|
||||
s32 index = offset >> 3;
|
||||
switch (7 - (offset & 7)) {
|
||||
case 7:
|
||||
t[index] = (u8)((t[index] & 0x00) | ((newtime >> 2) & 0xFF));
|
||||
t[index + 1] = (u8)((t[index + 1] & 0x3F) | ((newtime << 6) & 0xC0));
|
||||
break;
|
||||
case 5:
|
||||
t[index] = (u8)((t[index] & 0xC0) | ((newtime >> 4) & 0x3F));
|
||||
t[index + 1] = (u8)((t[index + 1] & 0x0F) | ((newtime << 4) & 0xF0));
|
||||
break;
|
||||
case 3:
|
||||
t[index] = (u8)((t[index] & 0xF0) | ((newtime >> 6) & 0x0F));
|
||||
t[index + 1] = (u8)((t[index + 1] & 0x03) | ((newtime << 2) & 0xFC));
|
||||
break;
|
||||
case 1:
|
||||
t[index] = (u8)((t[index] & 0xFC) | ((newtime >> 8) & 0x03));
|
||||
t[index + 1] = (u8)(newtime & 0xFF);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void geEepromPatchAllCheats(u8 *buf)
|
||||
{
|
||||
/* buf holds the five save slots read from block 4. The sixth local slot
|
||||
* is only a CRC end-boundary for the last one (fileGenerateCRC reads
|
||||
* [A, B), so its contents are irrelevant). */
|
||||
static ge_save_slot slots[6];
|
||||
memcpy(slots, buf, sizeof(ge_save_slot) * 5);
|
||||
memset(&slots[5], 0, sizeof(ge_save_slot));
|
||||
|
||||
int changed = 0;
|
||||
|
||||
/* D259 + D281: a fresh ge007.eep is zero-filled, so every slot reads as
|
||||
* all-zero. Without this patch such a slot fails fileValidateSaves' CRC
|
||||
* and becomes a free BLANKSAVEDATA slot (fileResetSave), after which
|
||||
* fileBuildWriteNewSave fills the first free slot for each folder with
|
||||
* no save, in folder order. This patch gives every slot a valid CRC
|
||||
* below, so an all-zero slot would instead SURVIVE as-is: five slots all
|
||||
* claiming folder 1 with volume 0 (D259: silence) and options 0 (D281:
|
||||
* sight-on-screen, auto-aim, look-ahead and ammo display all off, which
|
||||
* reads as "RMB aim broken"). D259 seeded only the volumes. Reproduce
|
||||
* the game's full result instead: all-zero slots take the folders that
|
||||
* no real slot holds, in order, built exactly as fileBuildWriteNewSave
|
||||
* builds them (BLANKSAVEDATA + folder + not-free + bond); any left over
|
||||
* become free BLANKSAVEDATA slots. Real saves keep their bytes. */
|
||||
{
|
||||
extern void fileSetSaveFoldernum(void *save, u32 folder);
|
||||
extern void fileSetSaveFlagDoReset(void *save, s32 enable);
|
||||
extern void fileSetSelectedBond(void *save, s32 bond);
|
||||
static const ge_save_slot blank = {
|
||||
0, 0, 0x80 /* SAVEFLAGS_SET(0,0,BOND_BROSNAN,1): free */, 0x00,
|
||||
0xFF, 0xFF, 0x3A /* DEFAULT_OPTIONS */, 0, 0, 0, 0, {0}
|
||||
};
|
||||
int allzero[5], present[4] = {0, 0, 0, 0};
|
||||
for (int i = 0; i < 5; i++) {
|
||||
const u8 *raw = (const u8 *)&slots[i];
|
||||
allzero[i] = 1;
|
||||
for (int b = 0; b < (int)sizeof(ge_save_slot); b++)
|
||||
if (raw[b]) { allzero[i] = 0; break; }
|
||||
if (!allzero[i] && !(slots[i].completion_bitflags & 0x80 /* DORESET */)) {
|
||||
int f = slots[i].completion_bitflags & 0x7; /* SAVEFLAG_FOLDER */
|
||||
if (f < 4) present[f] = 1; /* MAX_FOLDER_COUNT */
|
||||
}
|
||||
}
|
||||
int folder = 0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (!allzero[i]) continue;
|
||||
slots[i] = blank;
|
||||
while (folder < 4 && present[folder]) folder++;
|
||||
if (folder < 4) {
|
||||
fileSetSaveFoldernum(&slots[i], (u32)folder);
|
||||
fileSetSaveFlagDoReset(&slots[i], 0);
|
||||
fileSetSelectedBond(&slots[i], folder);
|
||||
present[folder] = 1;
|
||||
}
|
||||
changed = 1;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
/* D442: Game.AllUnlocked is a pure RAM/query-time override (file2.c
|
||||
* fileGetIsCheatUnlocked + the debug flags seeded in main.c). The EEPROM
|
||||
* bytes the game reads and writes are always the raw disk bytes -- the old
|
||||
* read-time save patch (D257/D259/D281) and its write-time merge (D387) are
|
||||
* gone. */
|
||||
|
||||
static s32 geEepromRW(u8 block, u8 *buf, int nbytes, int write)
|
||||
{
|
||||
@@ -1379,11 +1221,9 @@ static s32 geEepromRW(u8 block, u8 *buf, int nbytes, int write)
|
||||
/* D297: one-time migration of pre-D284 slot CRCs (port/src/legacycrc.c).
|
||||
* This IS the read fileValidateSaves performs (block 4, five slots), so
|
||||
* the re-stamped checksums are exactly what the game validates — no
|
||||
* ordering hazard. Runs BEFORE geEepromPatchAllCheats so that only
|
||||
* pristine+migrated bytes are ever persisted; AllUnlocked patches stay
|
||||
* read-time-only as before. Block-0 smallSave is deliberately untouched
|
||||
* (factory seal, see D297). */
|
||||
if (block == 4 && nbytes == (int)(sizeof(ge_save_slot) * 5)) {
|
||||
* ordering hazard. Only pristine+migrated bytes are ever persisted.
|
||||
* Block-0 smallSave is deliberately untouched (factory seal, D297). */
|
||||
if (block == 4 && nbytes == (int)(96 * 5) /* 5 x save_data, file.h */) {
|
||||
int migrated = geLegacyCrcMaybeMigrateSlots(buf);
|
||||
if (migrated) {
|
||||
memcpy(s_eeprom + off, buf, nbytes);
|
||||
@@ -1393,11 +1233,6 @@ static s32 geEepromRW(u8 block, u8 *buf, int nbytes, int write)
|
||||
migrated);
|
||||
}
|
||||
}
|
||||
/* fileValidateSaves' block read: five save slots from block 4. */
|
||||
if (portAllUnlocked && block == 4 &&
|
||||
nbytes == (int)(sizeof(ge_save_slot) * 5)) {
|
||||
geEepromPatchAllCheats(buf);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
+15
-10
@@ -85,6 +85,13 @@ static void portAtExit(void)
|
||||
* abort(), which does not run atexit handlers. */
|
||||
videoSaveWindowState();
|
||||
configSave();
|
||||
/* D443: Restart game -- runs only on the orderly D344 exit path, after the
|
||||
* ini is written, so the new instance reads the saved settings. */
|
||||
if (videoRestartRequested()) {
|
||||
if (sysRelaunchSelf() != 0) {
|
||||
sysLogPrintf(LOG_ERROR, "restart: relaunch failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
@@ -120,15 +127,12 @@ int main(int argc, char **argv)
|
||||
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 2, midpoint 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. D283: the Steam Deck preset runs
|
||||
* AFTER the load (hardware-detected, once per ini, only while the display
|
||||
* keys are untouched), so a first launch from Desktop Mode no longer
|
||||
* skips it for good. */
|
||||
configLoad();
|
||||
videoApplySteamDeckPreset();
|
||||
atexit(portAtExit); /* persist config + window geometry on clean exit */
|
||||
|
||||
/* 1a. D257: Game.AllUnlocked (default OFF; F10 'All unlocked' enables)
|
||||
@@ -161,8 +165,9 @@ int main(int argc, char **argv)
|
||||
|
||||
/* 2. Load the ROM and map segments. */
|
||||
if (romdataInit() != 0) {
|
||||
sysLogPrintf(LOG_ERROR, "Failed to load ROM (expected a .z64 in the "
|
||||
"data/ dir, see README)");
|
||||
sysLogPrintf(LOG_ERROR, "Failed to load the ROM (no .z64 in the data/ dir, "
|
||||
"asset conversion failed, or it could not be mapped): "
|
||||
"see the earlier log lines for the cause");
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -54,6 +54,7 @@ static int mixerTraceOn(void)
|
||||
#include "system.h"
|
||||
#include "mixer.h"
|
||||
#include "audio.h"
|
||||
#include "envflag.h" /* cached getenv for per-opcode probes (D302) */
|
||||
|
||||
/* 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. */
|
||||
@@ -127,7 +128,7 @@ void aClearBufferImpl(u16 addr, u32 count)
|
||||
* 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")) {
|
||||
if (addr == 1088 /* AL_MAIN_L_OUT */ && GE_ENVFLAG("GE_DMEMWIPE")) {
|
||||
memset(sDmem, 0, sizeof(sDmem));
|
||||
}
|
||||
memset(DMEM_U8(addr), 0, count);
|
||||
@@ -395,7 +396,7 @@ void aSetVolumeImpl(u32 flags, u16 v, u16 t, u16 r)
|
||||
* (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")) {
|
||||
if (GE_ENVFLAG("GE_NOWET")) {
|
||||
sVol.wetamt = 0;
|
||||
}
|
||||
} else if (flags & A_VOL) {
|
||||
@@ -432,7 +433,7 @@ void aEnvMixerImpl(u32 flags, u32 stateAddr)
|
||||
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 (GE_ENVFLAG("GE_VOICEDUMP")) {
|
||||
if (!s_voiceDumpFile)
|
||||
s_voiceDumpFile = fopen("voicedump.raw", "wb");
|
||||
if (s_voiceDumpFile) {
|
||||
|
||||
+139
-30
@@ -87,20 +87,22 @@ enum { ROW_TOGGLE, ROW_SLIDER, ROW_ENUM, ROW_MSAA, ROW_RES, ROW_ACTION, ROW_FPSC
|
||||
static const char *const kOnOff[] = { "Off", "On", NULL };
|
||||
static const char *const kReverse[] = { "Reverse", "Upright", NULL };
|
||||
static const char *const kHold[] = { "Hold", "Toggle", NULL };
|
||||
static const char *const kAspectMode[] = { "Window", "Original", NULL }; /* D447: Original = 4:3 (16:9 with the game Ratio) bars */
|
||||
static const char *const kTexFilter[] = { "Nearest", "Bilinear", "3-Point", NULL };
|
||||
static const char *const kAimMode[] = { "N64", "Centred (PC)", NULL }; /* D337 */
|
||||
static const char *const kAimRange[] = { "PC", "N64", NULL }; /* D338 */
|
||||
static const int kMsaaSeq[] = { 1, 2, 4, 8 };
|
||||
static const int kMsaaSeq[] = { 1, 2, 4, 8, 16 }; /* D443: 16x added */
|
||||
#define MSAA_N ((int)(sizeof(kMsaaSeq) / sizeof(kMsaaSeq[0])))
|
||||
/* v0.4.0 modern options wave: value names for the new rows (they land
|
||||
* in the functional sections -- Turok standard, D356 -- not a bucket). */
|
||||
static const char *const kOnOffRev[] = { "On", "Off", NULL }; /* 0 = On */
|
||||
/* M7 (v0.4.0, D387): AllUnlocked ships as a loudly-documented EXPERIMENTAL,
|
||||
* irreversible option. Enabling it and then saving (even a profile-settings
|
||||
* change) bakes synthetic cheat/completion times into the EEPROM that
|
||||
* switching OFF does not undo; it also breaks audio/aim on a fresh save with
|
||||
* no real save yet (D257/D259/D281). Back up data/ge007.eep before enabling.
|
||||
/* D442: AllUnlocked is a pure query-time/RAM override (file2.c
|
||||
* fileGetIsCheatUnlocked + fileIsStageUnlockedAtDifficulty, plus the debug
|
||||
* flags seeded in main.c at startup): no save bytes are patched, read or
|
||||
* written, so the old D259 silent-volume quirk is gone. It applies from the
|
||||
* next launch. Saves polluted by builds before D442 are not repaired.
|
||||
* Display-only value names; config still stores 0/1. */
|
||||
static const char *const kAllUnlocked[] = { "Off", "ON - UNSAFE", NULL };
|
||||
static const char *const kAllUnlocked[] = { "Off", "ON (restart)", NULL };
|
||||
/* D379: the authored N64 sprite is red. Original is the identity path;
|
||||
* White at index 7 uses the same alpha-mask combiner as other true hues. */
|
||||
static const char *const kCrosshairColor[] = {
|
||||
@@ -192,7 +194,8 @@ static struct Row rows[] = {
|
||||
* watchSettingsActiveFolder(). All rows use designated initializers
|
||||
* (D351 class). */
|
||||
{ .key="__HdrInput", .label="INPUT", .kind=ROW_HEADER },
|
||||
{ .key="Input.MouseSensitivity", .label="Mouse horizontal sensitivity", .kind=ROW_SLIDER, .step=5 }, /* calibrated UI midpoint = raw 100 */
|
||||
{ .key="Input.MouseSensitivity", .label="Mouse horizontal sensitivity", .kind=ROW_SLIDER, .step=10,
|
||||
.uiMin=10, .uiMax=300 }, /* D443/D357: shown as a multiplier, raw 100 = 1.0x; storage unchanged */
|
||||
/* Wave A (v0.5.0, CONTROLLER-INPUT-PLAN item 6): the pre-existing
|
||||
* Input.MouseYScale key (extra vertical/pitch sensitivity, %) finally gets
|
||||
* a row so the asymmetric X/Y mouse aim is tunable without editing the ini.
|
||||
@@ -227,8 +230,8 @@ static struct Row rows[] = {
|
||||
* (left = movement, right = look) replaces the single "Stick deadzone" row;
|
||||
* the look-sensitivity / look-smoothing / southpaw knobs are right-stick
|
||||
* (natural-pitch) feel options, identity at their defaults. All port-layer. */
|
||||
{ .key="Input.PadDeadzoneL", .label="Deadzone (left stick)", .kind=ROW_SLIDER, .step=500 },
|
||||
{ .key="Input.PadDeadzoneR", .label="Deadzone (right stick)", .kind=ROW_SLIDER, .step=500 },
|
||||
{ .key="Input.PadDeadzoneL", .label="Deadzone (left stick)", .kind=ROW_SLIDER, .step=300, .unit="%", .dispDiv=300 },
|
||||
{ .key="Input.PadDeadzoneR", .label="Deadzone (right stick)", .kind=ROW_SLIDER, .step=300, .unit="%", .dispDiv=300 },
|
||||
{ .key="Input.PadLookSensX", .label="X axis look sensitivity (controller)", .kind=ROW_SLIDER, .step=5 },
|
||||
{ .key="Input.PadLookSensY", .label="Y axis look sensitivity (controller)", .kind=ROW_SLIDER, .step=5 },
|
||||
{ .key="Input.PadLookSmooth", .label="Look smoothing (controller)", .kind=ROW_SLIDER, .step=1, .dispMax=10 },
|
||||
@@ -320,6 +323,9 @@ static struct Row rows[] = {
|
||||
* setups. Not config-backed (like __Resolution); activating it exits
|
||||
* the same way video.c's SDL_QUIT/Alt+F4 handlers already do. */
|
||||
{ .key="__QuitToDesktop", .label="Quit to desktop", .kind=ROW_ACTION },
|
||||
/* D443: orderly quit (D344) + relaunch, so "(restart)" rows like
|
||||
* Anti-aliasing can be applied without leaving the game by hand. */
|
||||
{ .key="__RestartGame", .label="Restart game (for restart options)", .kind=ROW_ACTION },
|
||||
|
||||
/* D388: Gameplay -> HUD collects both profile HUD visibility and global
|
||||
* crosshair/scale controls. Its reset honours each row's own scope. */
|
||||
@@ -343,10 +349,13 @@ static struct Row rows[] = {
|
||||
{ .key="Video.MSAA", .label="Anti-aliasing", .kind=ROW_MSAA, .restart=1 },
|
||||
{ .key="Video.TextureFilter", .label="Texture filter", .kind=ROW_ENUM, .step=1, .names=kTexFilter },
|
||||
{ .key="Video.Anisotropy", .label="Anisotropic filtering", .kind=ROW_SLIDER, .step=1, .unit="x" },
|
||||
{ .key="Video.FovScale", .label="FOV scale", .kind=ROW_SLIDER, .step=5, .unit="%" },
|
||||
{ .key="Video.FovScale", .label="Field of view", .kind=ROW_SLIDER, .step=1 } /* D443/D357: shown as horizontal degrees; storage stays Video.FovScale % */,
|
||||
/* D334: native widescreen (world projected at the window aspect, Hor+).
|
||||
* While on, "Widescreen auto FOV" has no effect (it was the stretch-era
|
||||
* vertical-FOV compensation). */
|
||||
/* D447: Original letter/pillarboxes to the console aspect (4:3, or 16:9 while
|
||||
* the watch-menu Ratio is 16:9); Window fills the window. */
|
||||
{ .key="Video.AspectMode", .label="Aspect", .kind=ROW_ENUM, .step=1, .names=kAspectMode },
|
||||
{ .key="Video.NativeWidescreen", .label="Native widescreen", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
|
||||
{ .key="Video.WidescreenAuto", .label="Widescreen auto FOV", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
|
||||
{ .key="Video.SafeAreaCrop", .label="Crop overscan", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
|
||||
@@ -370,6 +379,14 @@ static struct Row rows[] = {
|
||||
* reads as to a player. Pulled from the menu until it covers all
|
||||
* screen-shake/view-bob sources, not just explosions. Config var + fr.c
|
||||
* hook stay in place. */
|
||||
/* D440: one-press presets (arm -> confirm, like the reset
|
||||
* rows). "Original N64" snaps every value the port changed from the N64
|
||||
* back (3-point filter, 4:3 projection, no overscan crop, 100% draw/LOD,
|
||||
* stock FOV, N64 aim range, ...); "Port defaults" is its inverse. The
|
||||
* key table lives in video.c (videoApplyPreset). Not a mode: nothing is
|
||||
* remembered, the keys themselves are written. */
|
||||
{ .key="__PresetN64", .label="Original N64 preset", .kind=ROW_ACTION },
|
||||
{ .key="__PresetPort", .label="Port defaults preset", .kind=ROW_ACTION },
|
||||
{ .key="__ResetGraphics", .label="Reset to defaults", .kind=ROW_ACTION },
|
||||
|
||||
{ .key="__HdrAudio", .label="AUDIO", .kind=ROW_HEADER },
|
||||
@@ -377,9 +394,9 @@ static struct Row rows[] = {
|
||||
* "AUDIO (BOND FILE)" header was retired with the BOND FILE section).
|
||||
* A port-level master volume (M3) will join here as Turok's
|
||||
* Master/Sound/Music trio. */
|
||||
{ .key="Bond.Music", .label="Music volume", .kind=ROW_SLIDER, .step=128,
|
||||
{ .key="Bond.Music", .label="Music volume", .kind=ROW_SLIDER, .step=328,
|
||||
.uiMax=32767, .cfgMax=32767, .found=1, .unit="%", .dispDiv=328, .saveScoped=1 },
|
||||
{ .key="Bond.FX", .label="FX volume", .kind=ROW_SLIDER, .step=128,
|
||||
{ .key="Bond.FX", .label="FX volume", .kind=ROW_SLIDER, .step=328,
|
||||
.uiMax=32767, .cfgMax=32767, .found=1, .unit="%", .dispDiv=328, .saveScoped=1 },
|
||||
{ .key="__ResetAudio", .label="Reset to defaults", .kind=ROW_ACTION },
|
||||
|
||||
@@ -829,6 +846,8 @@ static void resolveCb(const char *key, int type, void *ptr, double min, double m
|
||||
}
|
||||
}
|
||||
|
||||
static void presetProbe(void); /* D440 GE_PRESETPROBE, below */
|
||||
|
||||
static void overlayInit(void)
|
||||
{
|
||||
if (s_inited) {
|
||||
@@ -944,6 +963,8 @@ static void overlayInit(void)
|
||||
bad ? "FAIL" : "PASS");
|
||||
}
|
||||
|
||||
if (getenv("GE_PRESETPROBE")) presetProbe();
|
||||
|
||||
/* Build the windowed-resolution preset list: presets that fit the desktop,
|
||||
* plus the current window size snapped to the nearest surviving entry. */
|
||||
{
|
||||
@@ -1062,10 +1083,10 @@ static double rowHi(const struct Row *r)
|
||||
* piecewise mapping, so the shown number always matches the filled bar. */
|
||||
static double calibratedDefault(const struct Row *r)
|
||||
{
|
||||
if (!strcmp(r->key, "Input.MouseSensitivity")) return 100.0;
|
||||
if (!strcmp(r->key, "Input.MouseYScale")) return 100.0; /* Wave A item 6: native */
|
||||
if (!strcmp(r->key, "Input.PadDeadzoneL")) return 7000.0; /* Wave A: = STICK_DEADZONE */
|
||||
if (!strcmp(r->key, "Input.PadDeadzoneR")) return 7000.0; /* Wave A: = STICK_DEADZONE */
|
||||
/* D443/D357: MouseSensitivity (x multiplier) and PadDeadzoneL/R (whole %)
|
||||
* left the calibrated 50/100 mapping -- they now show real units on a
|
||||
* linear bar (storage unchanged). */
|
||||
if (!strcmp(r->key, "Input.PadLookSensX")) return 100.0; /* Wave A: native */
|
||||
if (!strcmp(r->key, "Input.PadLookSensY")) return 100.0; /* Wave A: native */
|
||||
if (!strcmp(r->key, "Input.PadTriggerPct")) return 23.0;
|
||||
@@ -1177,6 +1198,7 @@ static void rowSet(struct Row *r, double v) { rowSetCommit(r, v, 1); }
|
||||
/* D356 reset rows (defined below, in the reset block). */
|
||||
static struct Row *rowAt(int i);
|
||||
static int isResetRow(const struct Row *r);
|
||||
static int isArmRow(const struct Row *r);
|
||||
static void rowActivateReset(struct Row *r);
|
||||
|
||||
static void rowAdjust(struct Row *r, int dir)
|
||||
@@ -1196,12 +1218,12 @@ static void rowAdjust(struct Row *r, int dir)
|
||||
break;
|
||||
case ROW_MSAA: {
|
||||
int idx = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int i = 0; i < MSAA_N; 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;
|
||||
/* Wrap like a normal settings-menu cycle: OFF->2x->4x->8x->16x->OFF,
|
||||
* in both directions (left/right click and arrows all roll). */
|
||||
idx = (idx + dir + MSAA_N) % MSAA_N;
|
||||
rowSet(r, (double)kMsaaSeq[idx]);
|
||||
break;
|
||||
}
|
||||
@@ -1242,14 +1264,34 @@ static void rowAdjust(struct Row *r, int dir)
|
||||
* contract (edge-triggered by their callers: fresh key press / fresh
|
||||
* click / A-press -- the held-repeat paths below never re-fire them).
|
||||
* The quit row is the only non-reset ROW_ACTION left. */
|
||||
if (isResetRow(r)) { rowActivateReset(r); break; }
|
||||
if (isArmRow(r)) { rowActivateReset(r); break; }
|
||||
/* D293: same exit path as SDL_QUIT / Alt+F4 (video.c), just reachable
|
||||
* without OS window chrome or a keyboard. */
|
||||
if (!strcmp(r->key, "__RestartGame")) {
|
||||
/* D443: same orderly D344 quit; main.c's atexit handler saves
|
||||
* config again and then relaunches (sysRelaunchSelf). */
|
||||
sysLogPrintf(LOG_INFO, "optionsoverlay: restart game requested");
|
||||
configSave();
|
||||
videoRequestRestart("Restart game");
|
||||
break;
|
||||
}
|
||||
sysLogPrintf(LOG_INFO, "optionsoverlay: quit to desktop requested");
|
||||
configSave();
|
||||
videoRequestQuit("Quit to desktop"); /* D344: never exit() off the host thread */
|
||||
break;
|
||||
default: /* ROW_SLIDER */
|
||||
if (!strcmp(r->key, "Video.FovScale")) {
|
||||
/* D443: step the DISPLAYED horizontal degree, not the raw %:
|
||||
* walk the stored % until the rounded degree value changes. */
|
||||
int cur = (int)lround(portFovHorizDegrees((s32)lround(v)));
|
||||
int p = (int)lround(v);
|
||||
for (int k = 0; k < 20; k++) {
|
||||
p += (dir >= 0) ? 1 : -1;
|
||||
if ((int)lround(portFovHorizDegrees((s32)p)) != cur) break;
|
||||
}
|
||||
rowSet(r, (double)p);
|
||||
break;
|
||||
}
|
||||
rowSet(r, v + dir * r->step);
|
||||
break;
|
||||
}
|
||||
@@ -1274,10 +1316,23 @@ static int isResetRow(const struct Row *r)
|
||||
return r->kind == ROW_ACTION && strncmp(r->key, "__Reset", 7) == 0;
|
||||
}
|
||||
|
||||
/* D440: the preset rows share the reset rows' two-step arm ->
|
||||
* confirm activation (and every edge-trigger guard that comes with it), but
|
||||
* are NOT section resets (the GE_WSPROBE_RESET probe keeps isResetRow). */
|
||||
static int isPresetRow(const struct Row *r)
|
||||
{
|
||||
return r->kind == ROW_ACTION && strncmp(r->key, "__Preset", 8) == 0;
|
||||
}
|
||||
|
||||
static int isArmRow(const struct Row *r)
|
||||
{
|
||||
return isResetRow(r) || isPresetRow(r);
|
||||
}
|
||||
|
||||
int optionsRowIsReset(int i)
|
||||
{
|
||||
struct Row *r = rowAt(i);
|
||||
return r && isResetRow(r);
|
||||
return r && isArmRow(r);
|
||||
}
|
||||
|
||||
/* ini rows reset to the port's C initializers (verified at implementation
|
||||
@@ -1313,6 +1368,7 @@ static const struct { const char *key; double def; } kResetDefaults[] = {
|
||||
{ "Video.NativeWidescreen", 1 }, /* = 1 */
|
||||
{ "Video.WidescreenAuto", 1 }, /* = 1 */
|
||||
{ "Video.SafeAreaCrop", 1 }, /* = 1 */
|
||||
{ "Video.AspectMode", 0 }, /* = 0 (Window), D447 */
|
||||
{ "Video.DrawDistance", 250 }, /* midpoint: 50/100 */
|
||||
{ "Video.LodDistance", 250 }, /* midpoint: 50/100 */
|
||||
{ "Video.CrosshairHide", 0 }, /* = 0 (on, N64) */
|
||||
@@ -1394,11 +1450,19 @@ static void rowResetSection(int iReset)
|
||||
* paths; the held-repeat branches skip reset rows entirely. */
|
||||
static void rowActivateReset(struct Row *r)
|
||||
{
|
||||
if (!isResetRow(r)) return;
|
||||
if (!isArmRow(r)) return;
|
||||
uint64_t now = sysGetMicroseconds();
|
||||
int i = (int)(r - rows);
|
||||
if (s_resetArmedRow == i && now - s_resetArmedUs <= RESET_ARM_US) {
|
||||
s_resetArmedRow = -1;
|
||||
if (isPresetRow(r)) {
|
||||
/* D440: ini keys only (live-applied; persisted on
|
||||
* close like any ini edit). Never window/resolution, bindings,
|
||||
* volumes or save-file rows. */
|
||||
videoApplyPreset(strcmp(r->key, "__PresetN64") == 0
|
||||
? VIDEO_PRESET_N64 : VIDEO_PRESET_PORT);
|
||||
return;
|
||||
}
|
||||
rowResetSection(i); /* exactly one commit per confirmed activation */
|
||||
return;
|
||||
}
|
||||
@@ -1406,8 +1470,11 @@ static void rowActivateReset(struct Row *r)
|
||||
s_resetArmedUs = now;
|
||||
int h = i - 1;
|
||||
while (h >= 0 && rows[h].kind != ROW_HEADER) h--;
|
||||
sysLogPrintf(LOG_INFO, "optionsoverlay: section reset '%s' armed (confirm within 3 s)",
|
||||
h >= 0 ? rows[h].label : "?");
|
||||
if (isPresetRow(r))
|
||||
sysLogPrintf(LOG_INFO, "optionsoverlay: '%s' armed (confirm within 3 s)", r->label);
|
||||
else
|
||||
sysLogPrintf(LOG_INFO, "optionsoverlay: section reset '%s' armed (confirm within 3 s)",
|
||||
h >= 0 ? rows[h].label : "?");
|
||||
}
|
||||
|
||||
void optionsRowActivateReset(int i)
|
||||
@@ -1431,6 +1498,34 @@ void optionsResetClear(void)
|
||||
s_resetArmedRow = -1;
|
||||
}
|
||||
|
||||
/* D440 GE_PRESETPROBE (dev, opt-in, one-shot at overlay init):
|
||||
* drives both preset rows through the real arm -> confirm activation
|
||||
* (rowActivateReset, the path F10 and the front screen use) and checks that
|
||||
* the row is "Active" afterwards and the other preset is not. Mutates the
|
||||
* live config like a real press -- use a throwaway data dir. */
|
||||
static void valueText(int i, char *out, int n);
|
||||
static void presetProbe(void)
|
||||
{
|
||||
static const struct { const char *key; int which; } kRows[] = {
|
||||
{ "__PresetN64", VIDEO_PRESET_N64 }, { "__PresetPort", VIDEO_PRESET_PORT },
|
||||
};
|
||||
int bad = 0;
|
||||
for (int k = 0; k < 2; k++) {
|
||||
int i = rowIndexByKey(kRows[k].key);
|
||||
char val[32] = "";
|
||||
if (i < 0 || !optionsRowIsReset(i)) { bad++; continue; }
|
||||
optionsRowActivateReset(i); /* arm */
|
||||
valueText(i, val, sizeof(val));
|
||||
if (strcmp(val, "Confirm") != 0) bad++;
|
||||
optionsRowActivateReset(i); /* confirm -> apply */
|
||||
valueText(i, val, sizeof(val));
|
||||
if (!videoPresetIsActive(kRows[k].which) || strcmp(val, "Active") != 0) bad++;
|
||||
if (videoPresetIsActive(!kRows[k].which)) bad++;
|
||||
sysLogPrintf(LOG_INFO, "GE_PRESETPROBE: %s -> value '%s', bad=%d", kRows[k].key, val, bad);
|
||||
}
|
||||
sysLogPrintf(bad ? LOG_ERROR : LOG_INFO, "GE_PRESETPROBE: %s", bad ? "FAIL" : "PASS");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
void optionsOverlayToggle(void)
|
||||
@@ -1698,7 +1793,7 @@ void optionsOverlayHandleInput(void)
|
||||
/* D356: reset actions are edge-triggered -- the held-repeat
|
||||
* re-fires adjust rows but must never re-fire a reset (a held
|
||||
* key could arm AND confirm, or commit repeatedly). */
|
||||
if (s_section >= 0 && s_sel != 0 && !isResetRow(&rows[s_visIdx[s_sel]]))
|
||||
if (s_section >= 0 && s_sel != 0 && !isArmRow(&rows[s_visIdx[s_sel]]))
|
||||
rowAdjust(&rows[s_visIdx[s_sel]], adjDir);
|
||||
}
|
||||
} else {
|
||||
@@ -1797,7 +1892,7 @@ void optionsOverlayHandleInput(void)
|
||||
if (rmb && !prevRmb && hoverVis >= 0 && !onClose && s_section >= 0) {
|
||||
int i = s_visIdx[hoverVis];
|
||||
s32 x0, x1;
|
||||
if (!isResetRow(&rows[i]) && overlayControlSpan(i, &x0, &x1) &&
|
||||
if (!isArmRow(&rows[i]) && overlayControlSpan(i, &x0, &x1) &&
|
||||
ox >= x0 && ox < x1) {
|
||||
s_sel = hoverVis;
|
||||
overlayUpdateScroll();
|
||||
@@ -1860,6 +1955,12 @@ static void valueText(int i, char *out, int n)
|
||||
/* D356: the label is the button; the value column shows the
|
||||
* armed state so the confirm step is discoverable. */
|
||||
snprintf(out, n, s_resetArmedRow == i ? "Confirm" : "");
|
||||
} else if (isPresetRow(r)) {
|
||||
/* D440: same arm/confirm cue, plus "Active" while every
|
||||
* preset key already holds that preset's value. */
|
||||
int which = strcmp(r->key, "__PresetN64") == 0 ? VIDEO_PRESET_N64 : VIDEO_PRESET_PORT;
|
||||
snprintf(out, n, "%s", s_resetArmedRow == i ? "Confirm" :
|
||||
videoPresetIsActive(which) ? "Active" : "");
|
||||
} else {
|
||||
snprintf(out, n, "[ENTER]");
|
||||
}
|
||||
@@ -1893,6 +1994,14 @@ static void valueText(int i, char *out, int n)
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!strcmp(r->key, "Video.FovScale")) { /* D443/D357: horizontal degrees */
|
||||
snprintf(out, n, "%d deg", (int)lround(portFovHorizDegrees((s32)lround(v))));
|
||||
return;
|
||||
}
|
||||
if (!strcmp(r->key, "Input.MouseSensitivity")) { /* D443/D357: raw 100 = 1.0x */
|
||||
snprintf(out, n, "%.1fx", v / 100.0);
|
||||
return;
|
||||
}
|
||||
if (r->kind == ROW_SLIDER && r->type == CONFIG_OPT_FLOAT) {
|
||||
snprintf(out, n, "%.2f", v);
|
||||
return;
|
||||
@@ -1996,8 +2105,8 @@ static int overlayControlSpan(int i, s32 *x0, s32 *x1)
|
||||
if (r->kind == ROW_HEADER || (r->kind == ROW_SLIDER && !r->found))
|
||||
return 0;
|
||||
*x1 = o.right - 7;
|
||||
if (isResetRow(r)) {
|
||||
*x0 = o.left + 7; /* reset is an action, not a setting name */
|
||||
if (isArmRow(r)) {
|
||||
*x0 = o.left + 7; /* reset/preset is an action, not a setting name */
|
||||
} else {
|
||||
valueText(i, val, sizeof(val));
|
||||
s32 limit = r->kind == ROW_SLIDER ? o.barX0 :
|
||||
@@ -2203,7 +2312,7 @@ Gfx *optionsOverlayEmit(void)
|
||||
gdl = drawBodyR(gdl, o.valueR, rowY, "(restart)", 0x498053ff);
|
||||
} else {
|
||||
u32 valueInk = (strncmp(val, "ON", 2) == 0 ||
|
||||
(isResetRow(r) && strcmp(val, "Confirm") == 0))
|
||||
(isArmRow(r) && strcmp(val, "Confirm") == 0))
|
||||
? 0xa0ffa0ff : ink;
|
||||
gdl = drawBodyR(gdl, o.valueR, rowY, val, valueInk);
|
||||
}
|
||||
|
||||
+36
-7
@@ -359,8 +359,9 @@ int romdataInit(void)
|
||||
u32 cgTotal = pccgReserveSize(img);
|
||||
|
||||
/* Map at the cart address so absolute asset symbols are live. On
|
||||
* failure both paths fall through to the heap copy below (degraded:
|
||||
* anything that dereferences a cart address directly, rather than via
|
||||
* failure Windows aborts boot with a diagnosis (D179 tail); POSIX
|
||||
* still falls through to the heap copy below (degraded: anything
|
||||
* that dereferences a cart address directly, rather than via
|
||||
* romdataGetRom()/the PI shims, will read wrong memory). */
|
||||
{
|
||||
u32 maplen = romSize + sideTotal + cgTotal;
|
||||
@@ -369,11 +370,39 @@ int romdataInit(void)
|
||||
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
|
||||
if (at == (void *)(uintptr_t)CART_BASE)
|
||||
return romdataFinishCartMap(tok, img, sideTotal, cgTotal);
|
||||
if (at)
|
||||
VirtualFree(at, 0, MEM_RELEASE);
|
||||
sysLogPrintf(LOG_WARNING, "romdataInit: could not reserve 0x%08X; "
|
||||
"using heap copy — direct ROM reads will fail",
|
||||
CART_BASE);
|
||||
/* D179 tail: the fixed-address map failed. Say exactly why (Win32
|
||||
* error + what already occupies the range) and stop here: the
|
||||
* heap-copy fallback cannot run the game (absolute asset symbols
|
||||
* dereference CART_BASE directly), so continuing only turned
|
||||
* this into an unexplained AV later, in model loading. */
|
||||
{
|
||||
const DWORD gle = GetLastError();
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
if (at)
|
||||
VirtualFree(at, 0, MEM_RELEASE);
|
||||
sysLogPrintf(LOG_ERROR, "romdataInit: VirtualAlloc(0x%08X, %u "
|
||||
"bytes) %s (GetLastError=%lu)", CART_BASE, maplen,
|
||||
at ? "returned a different address" : "failed",
|
||||
(unsigned long)gle);
|
||||
if (VirtualQuery((LPCVOID)(uintptr_t)CART_BASE, &mbi, sizeof(mbi))) {
|
||||
sysLogPrintf(LOG_ERROR, "romdataInit: 0x%08X is %s "
|
||||
"(allocation base %p, region %p+0x%llx, type 0x%lx)",
|
||||
CART_BASE,
|
||||
mbi.State == MEM_FREE ? "free (range too small?)" :
|
||||
mbi.State == MEM_RESERVE ? "reserved by another allocation" :
|
||||
"committed by another allocation",
|
||||
mbi.AllocationBase, mbi.BaseAddress,
|
||||
(unsigned long long)mbi.RegionSize,
|
||||
(unsigned long)mbi.Type);
|
||||
}
|
||||
sysLogPrintf(LOG_ERROR, "romdataInit: the game needs the ROM "
|
||||
"mapped at 0x%08X and cannot run from a heap copy; "
|
||||
"aborting boot (something in this process, often "
|
||||
"an injected overlay/hook DLL, occupies that "
|
||||
"address)", CART_BASE);
|
||||
free(img);
|
||||
return -1;
|
||||
}
|
||||
#else
|
||||
/* POSIX: anonymous fixed-address mmap. MAP_FIXED_NOREPLACE (Linux
|
||||
* 4.17+) fails instead of clobbering an existing mapping; where it
|
||||
|
||||
+43
-1
@@ -23,7 +23,7 @@
|
||||
extern void abort(void);
|
||||
extern void exit(int status);
|
||||
#else
|
||||
#define _POSIX_C_SOURCE 199309L
|
||||
#define _POSIX_C_SOURCE 200112L
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <sys/stat.h>
|
||||
@@ -122,6 +122,48 @@ void sysSetArgs(int argc, char **argv)
|
||||
g_argv = argv;
|
||||
}
|
||||
|
||||
/* D443: relaunch this executable with the original argv (same cwd -- a child
|
||||
* inherits it). Called ONLY from the atexit handler, i.e. after the D344
|
||||
* orderly quit has parked the render thread and the config/eeprom are saved.
|
||||
* Returns 0 on success. */
|
||||
int sysRelaunchSelf(void)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
char exe[MAX_PATH * 2];
|
||||
DWORD n = GetModuleFileNameA(NULL, exe, (DWORD)sizeof(exe));
|
||||
if (n == 0 || n >= sizeof(exe)) return -1;
|
||||
/* GetCommandLineA is the exact original (already-quoted) command line. */
|
||||
const char *cl = GetCommandLineA();
|
||||
if (!cl) return -1;
|
||||
char *cmd = _strdup(cl);
|
||||
if (!cmd) return -1;
|
||||
STARTUPINFOA si;
|
||||
PROCESS_INFORMATION pi;
|
||||
memset(&si, 0, sizeof(si));
|
||||
memset(&pi, 0, sizeof(pi));
|
||||
si.cb = sizeof(si);
|
||||
BOOL ok = CreateProcessA(exe, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
|
||||
free(cmd);
|
||||
if (!ok) return -1;
|
||||
CloseHandle(pi.hThread);
|
||||
CloseHandle(pi.hProcess);
|
||||
return 0;
|
||||
#else
|
||||
if (!g_argv || g_argc <= 0) return -1;
|
||||
char exe[4096];
|
||||
ssize_t n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
|
||||
const char *path = g_argv[0];
|
||||
if (n > 0) { exe[n] = 0; path = exe; }
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return -1;
|
||||
if (pid == 0) {
|
||||
execv(path, g_argv);
|
||||
_exit(127);
|
||||
}
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
int sysArgCheck(const char *arg)
|
||||
{
|
||||
for (int i = 1; i < g_argc; ++i) {
|
||||
|
||||
+239
-31
@@ -56,7 +56,7 @@ static int initDone = 0;
|
||||
*/
|
||||
static int cfgVSync = 1; /* swap interval: 0 = off, 1 = on */
|
||||
static int cfgFpsCap = 60; /* frame cap in fps; 0 = uncapped (vsync); menu only exposes 30/60 */
|
||||
static int cfgMSAA = 2; /* 1/2/4/8 samples; 2x default is lighter on low-end GPUs */
|
||||
static int cfgMSAA = 2; /* 1/2/4/8/16 samples; 2x default is lighter on low-end GPUs */
|
||||
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 cfgDetailBaseTile = 1; /* D236: TEXTURETYPE_DETAIL -> sample the base image, not the detail tile */
|
||||
@@ -71,7 +71,9 @@ static int cfgLodDistance = 250; /* % of authored geometry LOD distance;
|
||||
static int cfgLodDistanceAutoFov = 0; /* legacy ini option, no longer exposed in the menu */
|
||||
static int cfgAniso = 4; /* D212: anisotropic filtering samples; 4 = the value fast3d already applied (no visual delta at default) */
|
||||
static int cfgSafeAreaCrop = 1; /* crop the N64 TV-overscan safe-area margin (visible as black top/bottom bars on PC) instead of showing it; on by default */
|
||||
static int cfgAspectMode = 0; /* D447: 0 = Window (fill the window), 1 = Original (pillar/letterbox to the console aspect: 4:3, or 16:9 while the game's Ratio option is 16:9) */
|
||||
static int cfgFullscreen = 0; /* 0 = windowed, 1 = borderless fullscreen */
|
||||
static int cfgDeckPresetApplied = 0; /* D283: 1 once the Steam Deck preset has been considered */
|
||||
|
||||
/*
|
||||
* [Window] persistence. W/H = 0 -> auto (gfx_sdl2 fits a 4:3 window into ~85%
|
||||
@@ -177,9 +179,8 @@ void portCrosshairPreview(s32 *r, s32 *g, s32 *b)
|
||||
* 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. */
|
||||
* run. Cheats are unlocked at query time via fileGetIsCheatUnlocked
|
||||
* (src/game/file2.c, D442) -- the save file is never patched. */
|
||||
s32 portAllUnlocked = 0;
|
||||
|
||||
/* D211: Video.FovScale as a multiplier on the render FOV. Applied game-side
|
||||
@@ -262,6 +263,32 @@ f32 portScaleFovY(f32 fovy, s32 isTitleScreen)
|
||||
return fovy;
|
||||
}
|
||||
|
||||
/* D443 (D357): horizontal FOV in degrees for a Video.FovScale percent, for the
|
||||
* settings display only. Mirrors portScaleFovY + the gameplay projection:
|
||||
* vertical FOV = FOV_Y_F (60) * [WidescreenAuto stretch-era boost] * pct/100,
|
||||
* clamped 20..160; the projection aspect is the window aspect under native
|
||||
* widescreen (portNativeAspect) or 4:3 otherwise (bondview2.c faspect for a
|
||||
* full-screen viewport). hfov = 2*atan(tan(vfov/2)*aspect). 100% at 4:3 = 75.0
|
||||
* deg, at 16:9 = 91.5 deg. */
|
||||
f32 portFovHorizDegrees(s32 pct)
|
||||
{
|
||||
const double kRad = 3.14159265358979323846 / 180.0;
|
||||
double v = 60.0;
|
||||
double asp = (double)portNativeAspect();
|
||||
if (asp <= 0.0) {
|
||||
asp = 4.0 / 3.0;
|
||||
if (cfgWidescreenAuto && gfx_current_dimensions.aspect_ratio > 0.01f) {
|
||||
v *= sqrt((double)gfx_current_dimensions.aspect_ratio / (4.0 / 3.0));
|
||||
}
|
||||
}
|
||||
if (pct > 40 && pct < 201) {
|
||||
v *= (double)pct / 100.0;
|
||||
}
|
||||
if (v > 160.0) v = 160.0;
|
||||
if (v < 20.0) v = 20.0;
|
||||
return (f32)(2.0 * atan(tan(v * 0.5 * kRad) * asp) / kRad);
|
||||
}
|
||||
|
||||
/* 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
|
||||
@@ -422,7 +449,7 @@ PD_CONSTRUCTOR static void videoConfigInit(void)
|
||||
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.MSAA", &cfgMSAA, 1, 16); /* D443: 16x added */
|
||||
configRegisterInt("Video.TextureFilter", &cfgTexFilter, 0, 2);
|
||||
configRegisterInt("Video.FixMipTextures", &cfgFixMipTex, 0, 1);
|
||||
configRegisterInt("Video.DetailBaseTile", &cfgDetailBaseTile, 0, 1);
|
||||
@@ -437,34 +464,168 @@ PD_CONSTRUCTOR static void videoConfigInit(void)
|
||||
configRegisterInt("Video.LodDistanceAutoFov", &cfgLodDistanceAutoFov, 0, 1);
|
||||
configRegisterInt("Video.Anisotropy", &cfgAniso, 1, 16);
|
||||
configRegisterInt("Video.SafeAreaCrop", &cfgSafeAreaCrop, 0, 1);
|
||||
configRegisterInt("Video.AspectMode", &cfgAspectMode, 0, 1); /* D447 */
|
||||
configRegisterInt("Video.Fullscreen", &cfgFullscreen, 0, 1);
|
||||
configRegisterInt("Window.Width", &cfgWinW, 0, 16384);
|
||||
configRegisterInt("Window.Height", &cfgWinH, 0, 16384);
|
||||
configRegisterInt("Window.X", &cfgWinX, -1, 16384);
|
||||
configRegisterInt("Window.Y", &cfgWinY, -1, 16384);
|
||||
configRegisterInt("Window.Maximized", &cfgWinMax, 0, 1);
|
||||
configRegisterInt("Video.DeckPresetApplied", &cfgDeckPresetApplied, 0, 1); /* D283 */
|
||||
}
|
||||
|
||||
/* 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; 2x MSAA + VSync keep
|
||||
* GPU cost modest. Draw/LOD distances use the standard midpoint (250%)
|
||||
* to avoid obvious N64-era pop-in on modern displays.
|
||||
* 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)
|
||||
/* D283: Steam Deck preset. Called from main() right AFTER configLoad(), so
|
||||
* it sees the ini the user already has (the original preset only ran when no
|
||||
* ini existed and only on STEAMOS, so a first launch from Desktop Mode --
|
||||
* which does not export STEAMOS -- created a plain-Linux ini and the preset
|
||||
* never applied, D283).
|
||||
*
|
||||
* Detection is by hardware: DMI board/sys vendor "Valve" + product
|
||||
* "Jupiter" (LCD) or "Galileo" (OLED), read from /sys (Linux only); the
|
||||
* STEAMOS env var is kept as an extra signal. GE_FAKE_DECK=1 forces the
|
||||
* Deck path on any host (testing), GE_FAKE_DECK=0 forces it off.
|
||||
*
|
||||
* Applied at most once per ini (Video.DeckPresetApplied records it) and only
|
||||
* while the display keys are still at the generic windowed state
|
||||
* (Video.Fullscreen=0, Window.Maximized=0). Window.Width/Height are NOT a
|
||||
* usable "untouched" signal: videoSaveWindowState() rewrites them from the
|
||||
* live window on every clean exit. A user who already picked fullscreen keeps
|
||||
* their settings; the flag is still set, so later F10 choices always win.
|
||||
* The preset is display-only (borderless fullscreen, 1280x800 = the Deck
|
||||
* panel as the windowed size). VSync 1 / MSAA 2 / draw+LOD 250 were part of
|
||||
* the old first-run preset but are the generic defaults anyway, so they are
|
||||
* no longer written (an existing ini's choices there are never clobbered). */
|
||||
#if defined(__linux__)
|
||||
static int videoReadDmi(const char *name, char *out, int n)
|
||||
{
|
||||
cfgFullscreen = 1;
|
||||
cfgWinW = 1280;
|
||||
cfgWinH = 800;
|
||||
cfgVSync = 1;
|
||||
cfgMSAA = 2;
|
||||
cfgDrawDistance = 250;
|
||||
cfgLodDistance = 250;
|
||||
char path[128];
|
||||
FILE *f;
|
||||
snprintf(path, sizeof(path), "/sys/class/dmi/id/%s", name);
|
||||
out[0] = 0;
|
||||
f = fopen(path, "r");
|
||||
if (!f) return 0;
|
||||
if (!fgets(out, n, f)) out[0] = 0;
|
||||
fclose(f);
|
||||
for (int i = (int)strlen(out) - 1; i >= 0 && (out[i] == '\n' || out[i] == '\r' || out[i] == ' '); i--)
|
||||
out[i] = 0;
|
||||
return out[0] != 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static int videoDetectSteamDeck(const char **why)
|
||||
{
|
||||
const char *fake = getenv("GE_FAKE_DECK");
|
||||
if (fake && *fake) {
|
||||
*why = "GE_FAKE_DECK";
|
||||
return atoi(fake) != 0;
|
||||
}
|
||||
#if defined(__linux__)
|
||||
{
|
||||
char vendor[64], sysVendor[64], product[64];
|
||||
videoReadDmi("board_vendor", vendor, sizeof(vendor));
|
||||
videoReadDmi("sys_vendor", sysVendor, sizeof(sysVendor));
|
||||
videoReadDmi("product_name", product, sizeof(product));
|
||||
if ((!strncmp(vendor, "Valve", 5) || !strncmp(sysVendor, "Valve", 5)) &&
|
||||
(!strcmp(product, "Jupiter") || !strcmp(product, "Galileo"))) {
|
||||
*why = "DMI Valve Jupiter/Galileo";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (getenv("STEAMOS")) {
|
||||
*why = "STEAMOS";
|
||||
return 1;
|
||||
}
|
||||
*why = "";
|
||||
return 0;
|
||||
}
|
||||
|
||||
void videoApplySteamDeckPreset(void)
|
||||
{
|
||||
const char *why = "";
|
||||
if (!videoDetectSteamDeck(&why)) return;
|
||||
if (cfgDeckPresetApplied) {
|
||||
sysLogPrintf(LOG_INFO, "video: Steam Deck (%s); preset already considered for this ini", why);
|
||||
return;
|
||||
}
|
||||
if (!cfgFullscreen && !cfgWinMax) {
|
||||
cfgFullscreen = 1;
|
||||
cfgWinW = 1280;
|
||||
cfgWinH = 800;
|
||||
sysLogPrintf(LOG_INFO, "video: Steam Deck (%s); applying Deck preset (fullscreen, 1280x800)", why);
|
||||
} else {
|
||||
sysLogPrintf(LOG_INFO, "video: Steam Deck (%s); display already user-set (fullscreen=%d maximized=%d), preset skipped",
|
||||
why, cfgFullscreen, cfgWinMax);
|
||||
}
|
||||
cfgDeckPresetApplied = 1;
|
||||
configSave();
|
||||
}
|
||||
|
||||
/* D440: "Original N64" preset + its inverse ("Port defaults").
|
||||
* One row per config key whose port default differs from what the N64
|
||||
* showed, plus the fidelity-identity keys a user may have moved away from
|
||||
* (so the N64 preset really snaps every presentation value back). Each
|
||||
* entry: key, N64 value, port default (the C initializer). The full audit,
|
||||
* including the keys deliberately left OUT (MSAA, VSync, FpsCap, window /
|
||||
* fullscreen, accuracy fixes, input feel, bindings, volumes, SkipIntro,
|
||||
* AllUnlocked), is in findings D440.
|
||||
* Culling needs no key of its own: frustum-cull planes, the fog/LOD scale
|
||||
* (D222) and the room-pool budget (D294, portRoomPoolScale) all derive from
|
||||
* FovScale / widescreen / draw / LOD distance and are identity when those
|
||||
* are at the N64 values below. */
|
||||
static const struct { const char *key; double n64, port; } kVideoPresets[] = {
|
||||
{ "Video.TextureFilter", 2, 1 }, /* N64 3-point vs bilinear */
|
||||
{ "Video.Anisotropy", 1, 4 }, /* the RDP has no anisotropic filtering */
|
||||
{ "Video.NativeWidescreen", 0, 1 }, /* 4:3 projection (D334) */
|
||||
{ "Video.WidescreenAuto", 0, 1 }, /* stock vertical FOV at any window aspect */
|
||||
{ "Video.SafeAreaCrop", 0, 1 }, /* show the VI frame as output, borders included */
|
||||
{ "Video.AspectMode", 1, 0 }, /* D447: exact console aspect, bars around it */
|
||||
{ "Video.DrawDistance", 100, 250 }, /* authored far clip / fog (D218) */
|
||||
{ "Video.LodDistance", 100, 250 }, /* authored LOD switch distances (D249) */
|
||||
{ "Video.DrawDistanceAutoFov", 0, 0 }, /* legacy coupling: off = identity */
|
||||
{ "Video.LodDistanceAutoFov", 0, 0 },
|
||||
{ "Video.FovScale", 100, 100 }, /* stock FOV (D211) */
|
||||
{ "Game.HudScale", 100, 100 }, /* D226 */
|
||||
{ "Game.ScreenShakeIntensity", 1, 1 }, /* D181 */
|
||||
{ "Game.NoHitFlash", 0, 0 }, /* D232 */
|
||||
{ "Video.CrosshairHide", 0, 0 }, /* D373 */
|
||||
{ "Video.CrosshairColor", 0, 0 }, /* authored red sprite */
|
||||
{ "Video.CrosshairSize", 100, 100 },
|
||||
{ "Video.CrosshairStyle", 0, 0 },
|
||||
{ "Input.AimMode", 0, 0 }, /* N64 aim (D337) */
|
||||
{ "Input.AimRange", 1, 0 }, /* N64 aim limits (D338) */
|
||||
};
|
||||
#define NUM_VIDEO_PRESETS ((int)(sizeof(kVideoPresets) / sizeof(kVideoPresets[0])))
|
||||
|
||||
int videoApplyPreset(int which)
|
||||
{
|
||||
int changed = 0;
|
||||
for (int i = 0; i < NUM_VIDEO_PRESETS; i++) {
|
||||
double want = which == VIDEO_PRESET_N64 ? kVideoPresets[i].n64 : kVideoPresets[i].port;
|
||||
double cur = 0.0;
|
||||
if (!configGetValue(kVideoPresets[i].key, &cur)) {
|
||||
sysLogPrintf(LOG_WARNING, "video: preset key %s not registered", kVideoPresets[i].key);
|
||||
continue;
|
||||
}
|
||||
if (cur == want) continue;
|
||||
configSetValue(kVideoPresets[i].key, want);
|
||||
if (initDone && !strncmp(kVideoPresets[i].key, "Video.", 6))
|
||||
videoRequestLiveConfigForKey(kVideoPresets[i].key);
|
||||
changed++;
|
||||
}
|
||||
sysLogPrintf(LOG_INFO, "video: applied %s preset (%d value(s) changed)",
|
||||
which == VIDEO_PRESET_N64 ? "Original N64" : "port defaults", changed);
|
||||
return changed;
|
||||
}
|
||||
|
||||
int videoPresetIsActive(int which)
|
||||
{
|
||||
for (int i = 0; i < NUM_VIDEO_PRESETS; i++) {
|
||||
double want = which == VIDEO_PRESET_N64 ? kVideoPresets[i].n64 : kVideoPresets[i].port;
|
||||
double cur = 0.0;
|
||||
if (!configGetValue(kVideoPresets[i].key, &cur) || cur != want) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* D382: record precisely which live video setting changed. Reticle/FOV/
|
||||
@@ -509,6 +670,7 @@ void videoRequestLiveConfigForKey(const char *key)
|
||||
else if (!strcmp(key, "Video.FovScale")) mask = VCFG_FOV;
|
||||
else if (!strcmp(key, "Video.Anisotropy")) mask = VCFG_ANISO;
|
||||
else if (!strcmp(key, "Video.SafeAreaCrop")) mask = VCFG_CROP;
|
||||
/* Video.AspectMode is read every frame in videoStartFrame: no dirty bit. */
|
||||
if (mask) {
|
||||
int old;
|
||||
do {
|
||||
@@ -535,6 +697,20 @@ void videoRequestFullscreen(int on)
|
||||
winReqA = on ? 1 : 0; winReqKind = 2;
|
||||
}
|
||||
|
||||
void videoGetOutputRectFrac(double *x0, double *y0, double *x1, double *y1)
|
||||
{
|
||||
int32_t rx = 0, ry = 0, rw = 0, rh = 0;
|
||||
double W = (double)gfx_current_window_dimensions.width;
|
||||
double H = (double)gfx_current_window_dimensions.height;
|
||||
gfx_get_output_rect(&rx, &ry, &rw, &rh);
|
||||
if (W < 1.0 || H < 1.0 || rw <= 0 || rh <= 0) {
|
||||
*x0 = 0.0; *y0 = 0.0; *x1 = 1.0; *y1 = 1.0;
|
||||
return;
|
||||
}
|
||||
*x0 = rx / W; *y0 = ry / H;
|
||||
*x1 = (rx + rw) / W; *y1 = (ry + rh) / H;
|
||||
}
|
||||
|
||||
void videoGetWindowSize(int *w, int *h)
|
||||
{
|
||||
uint32_t ww = 0, hh = 0; int32_t x = 0, y = 0;
|
||||
@@ -610,6 +786,15 @@ static float vidAvgFPS = 0.f;
|
||||
|
||||
int videoInit(void)
|
||||
{
|
||||
/* D440 CLI route: -n64preset / -portpreset apply the preset
|
||||
* once at startup (after configLoad, before any value reaches fast3d) and
|
||||
* persist like an F10 press (the atexit configSave). */
|
||||
if (sysArgCheck("-n64preset") || sysArgCheck("--n64preset")) {
|
||||
videoApplyPreset(VIDEO_PRESET_N64);
|
||||
} else if (sysArgCheck("-portpreset") || sysArgCheck("--portpreset")) {
|
||||
videoApplyPreset(VIDEO_PRESET_PORT);
|
||||
}
|
||||
|
||||
wmAPI = &gfx_sdl;
|
||||
renderingAPI = &gfx_opengl_api;
|
||||
|
||||
@@ -620,7 +805,7 @@ int videoInit(void)
|
||||
gfx_detail_textures_enabled = false;
|
||||
|
||||
/* MSAA: snap the requested sample count down to a supported power of two. */
|
||||
gfx_msaa_level = cfgMSAA >= 8 ? 8 : cfgMSAA >= 4 ? 4 : cfgMSAA >= 2 ? 2 : 1;
|
||||
gfx_msaa_level = cfgMSAA >= 16 ? 16 : cfgMSAA >= 8 ? 8 : cfgMSAA >= 4 ? 4 : cfgMSAA >= 2 ? 2 : 1;
|
||||
|
||||
int winW = cfgWinW > 0 ? cfgWinW : 0; /* 0 -> gfx_sdl2 auto-fits to the desktop */
|
||||
int winH = cfgWinH > 0 ? cfgWinH : 0;
|
||||
@@ -714,6 +899,19 @@ void videoRequestQuit(const char *why)
|
||||
}
|
||||
}
|
||||
|
||||
/* D443: "Restart game" = the same orderly quit, plus a flag main.c's atexit
|
||||
* handler reads (after config/window state are saved) to relaunch. */
|
||||
static int s_restartReq = 0;
|
||||
void videoRequestRestart(const char *why)
|
||||
{
|
||||
s_restartReq = 1;
|
||||
videoRequestQuit(why);
|
||||
}
|
||||
int videoRestartRequested(void)
|
||||
{
|
||||
return s_restartReq;
|
||||
}
|
||||
|
||||
int videoQuitRequested(void)
|
||||
{
|
||||
return SDL_AtomicGet(&s_quitReq) != 0;
|
||||
@@ -756,6 +954,17 @@ void videoDestroy(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* D447: target aspect of the output rect for the current frame. Original mode
|
||||
* does what the console did: 4:3, or 16:9 while the watch-menu Ratio option is
|
||||
* 16:9 (the game pre-squashes for a stretching widescreen TV). get_screen_ratio()
|
||||
* is read only, never written. Returns 0 for Window mode (fill the window). */
|
||||
extern int get_screen_ratio(void);
|
||||
static float videoOutputAspect(void)
|
||||
{
|
||||
if (cfgAspectMode != 1) return 0.0f;
|
||||
return get_screen_ratio() == 1 /* SCREEN_RATIO_16_9 */ ? (16.0f / 9.0f) : (4.0f / 3.0f);
|
||||
}
|
||||
|
||||
void videoStartFrame(void)
|
||||
{
|
||||
if (!initDone) {
|
||||
@@ -784,6 +993,7 @@ void videoStartFrame(void)
|
||||
dirty, cfgVSync, cfgFpsCap, cfgTexFilter, cfgFovScale, cfgAniso);
|
||||
}
|
||||
|
||||
gfx_set_output_aspect(videoOutputAspect());
|
||||
gfx_start_frame();
|
||||
}
|
||||
|
||||
@@ -929,21 +1139,19 @@ static void videoPreSwapCapture(void)
|
||||
((int)frames - lo) % step == 0) {
|
||||
char path[128];
|
||||
snprintf(path, sizeof(path), "ppm/frame_%06d.ppm", (int)frames);
|
||||
gfx_opengl_dump_bound_fbo((uint32_t)gfx_current_dimensions.width,
|
||||
(uint32_t)gfx_current_dimensions.height, path);
|
||||
gfx_opengl_dump_bound_fbo((uint32_t)gfx_current_window_dimensions.width,
|
||||
(uint32_t)gfx_current_window_dimensions.height, path); /* D447: whole window, bars included */
|
||||
}
|
||||
}
|
||||
|
||||
if (screenshotReq) {
|
||||
screenshotReq = 0;
|
||||
extern void gfxD157Burst(void); /* D252 TEMP, inert without GE_D157 */
|
||||
gfxD157Burst();
|
||||
static int shotNum = 0;
|
||||
char path[128];
|
||||
GE_MKDIR("ppm");
|
||||
snprintf(path, sizeof(path), "ppm/shot_%03d.ppm", shotNum++);
|
||||
if (gfx_opengl_dump_bound_fbo((uint32_t)gfx_current_dimensions.width,
|
||||
(uint32_t)gfx_current_dimensions.height, path)) {
|
||||
if (gfx_opengl_dump_bound_fbo((uint32_t)gfx_current_window_dimensions.width,
|
||||
(uint32_t)gfx_current_window_dimensions.height, path)) {
|
||||
sysLogPrintf(LOG_INFO, "video: screenshot -> %s "
|
||||
"(view with tools_pc/ppm2bmp.py)", path);
|
||||
} else {
|
||||
|
||||
@@ -212,10 +212,10 @@ static int stageActive(void)
|
||||
* player is viewing (selected_folder_num, set by file select) > the first
|
||||
* valid folder. The first two are pure reads of stable menu state, safe from
|
||||
* the F10 input thread (D350 gate: no GE writes from it; only chosen is
|
||||
* written, under uiLock). A fresh PC eeprom is booted with a BLANKSAVEDATA
|
||||
* slot per folder (libultra.c D259/D281 patch), so a default almost always
|
||||
* resolves; the game-thread fallback in watchSettingsGameTick builds one if
|
||||
* it does not. */
|
||||
* written, under uiLock). D442 removed the boot-time BLANKSAVEDATA slot patch:
|
||||
* a fresh PC eeprom now has no valid slot until the player creates a profile,
|
||||
* so the default resolves only once one exists; the game-thread fallback in
|
||||
* watchSettingsGameTick builds a blank file 1 when none does. */
|
||||
static int frontFolder(void)
|
||||
{
|
||||
SDL_AtomicLock(&uiLock);
|
||||
@@ -540,10 +540,10 @@ void watchSettingsGameTick(void)
|
||||
if (!stageActive() || !g_CurrentPlayer) {
|
||||
wsStageWasActive = 0;
|
||||
/* D354: one-shot fallback -- if not a single folder holds a valid
|
||||
* save (wiped/corrupt eeprom), build the game's own blank save for
|
||||
* save (wiped/corrupt/fresh eeprom), build the game's own blank save for
|
||||
* file 1 (game thread, so the EEPROM write is on the owning thread)
|
||||
* and default to it. A fresh PC eeprom never reaches this: boot
|
||||
* patches in a BLANKSAVEDATA slot per folder (libultra.c D259/D281).
|
||||
* and default to it. (D442 removed the old boot-time BLANKSAVEDATA
|
||||
* patch, so a fresh eeprom does reach this.)
|
||||
* frontFolder() re-checks, so an explicit chooser pick is respected. */
|
||||
if (!stageActive() && frontFolder() < FOLDER1) {
|
||||
static int wsAutoInitTried = 0;
|
||||
|
||||
@@ -3150,7 +3150,11 @@ void bgBuildRoomVtxBounds(s32 roomID)
|
||||
numvertices = ((gdl[cmdindex].dma.par >> 4) & 0xf) + 1;
|
||||
#endif
|
||||
|
||||
#ifdef PORT
|
||||
vtx = (Vtx *)(SEGMENT_OFFSET(gdl[cmdindex].dma.addr) + (uintptr_t)vertices);
|
||||
#else
|
||||
vtx = (Vtx *)(SEGMENT_OFFSET(gdl[cmdindex].dma.addr) + (u32)vertices);
|
||||
#endif
|
||||
|
||||
#if defined(PORT)
|
||||
/* TEMP D69 safety net: the room primary/secondary DL binaries
|
||||
@@ -3574,7 +3578,11 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV
|
||||
|
||||
if (bgTestRayIntersectsBbox(from, dir, (s32 *) (&bboxMin), (s32 *) (&bboxMax)))
|
||||
{
|
||||
#ifdef PORT
|
||||
if (intersectRayTriangle((Vertex *)((uintptr_t)vtxbase - (0 - (idx[0] << 4))), (Vertex *)((uintptr_t)vtxbase - (0 - (idx[1] << 4))), (Vertex *)((uintptr_t)vtxbase - (0 - (idx[2] << 4))), (coord3d *) (((roomnum * 24) + ((uintptr_t) ptr_bgdata_room_fileposition_list)) + 12), from, to, dir, &hitbuf))
|
||||
#else
|
||||
if (intersectRayTriangle((Vertex *)((s32)vtxbase - (0 - (idx[0] << 4))), (Vertex *)((s32)vtxbase - (0 - (idx[1] << 4))), (Vertex *)((s32)vtxbase - (0 - (idx[2] << 4))), (coord3d *) (((roomnum * 24) + ((s32) ptr_bgdata_room_fileposition_list)) + 12), from, to, dir, &hitbuf))
|
||||
#endif
|
||||
{
|
||||
tcmd = gdl;
|
||||
dx = ((s32) hitbuf.hitpos.x) - ((s32) from->x);
|
||||
@@ -3760,7 +3768,11 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV
|
||||
|
||||
if (bgTestRayIntersectsBbox(from, dir, (s32 *) (&bboxMin2), (s32 *) (&bboxMax2)))
|
||||
{
|
||||
#ifdef PORT
|
||||
if (intersectRayTriangle((Vertex *)((uintptr_t)vtxbase - (0 - (idx2[0] << 4))), (Vertex *)((uintptr_t)vtxbase - (0 - (idx2[1] << 4))), (Vertex *)((uintptr_t)vtxbase - (0 - (idx2[2] << 4))), (coord3d *) (((roomnum * 24) + ((uintptr_t) ptr_bgdata_room_fileposition_list)) + 12), from, to, dir, &hitbuf))
|
||||
#else
|
||||
if (intersectRayTriangle((Vertex *)((s32)vtxbase - (0 - (idx2[0] << 4))), (Vertex *)((s32)vtxbase - (0 - (idx2[1] << 4))), (Vertex *)((s32)vtxbase - (0 - (idx2[2] << 4))), (coord3d *) (((roomnum * 24) + ((s32) ptr_bgdata_room_fileposition_list)) + 12), from, to, dir, &hitbuf))
|
||||
#endif
|
||||
{
|
||||
tcmd = gdl;
|
||||
dx = ((s32) hitbuf.hitpos.x) - ((s32) from->x);
|
||||
@@ -5244,7 +5256,11 @@ void bgRoomCalcBB(s32 room) // canonical name
|
||||
StanRoomBounds limits;
|
||||
u8 wasloaded;
|
||||
|
||||
#ifdef PORT
|
||||
roomdata = (bg_room_data *) ((uintptr_t) ptr_bgdata_room_fileposition_list + room * 24);
|
||||
#else
|
||||
roomdata = (bg_room_data *) ((s32) ptr_bgdata_room_fileposition_list + room * 24);
|
||||
#endif
|
||||
|
||||
if (roomdata->pPointTableBin == NULL)
|
||||
{
|
||||
@@ -5277,7 +5293,11 @@ void bgRoomCalcBB(s32 room) // canonical name
|
||||
}
|
||||
|
||||
vertices = g_BgRoomInfo[room].vertices;
|
||||
#ifdef PORT
|
||||
roomdata = (bg_room_data *) ((uintptr_t) ptr_bgdata_room_fileposition_list + room * 24);
|
||||
#else
|
||||
roomdata = (bg_room_data *) ((s32) ptr_bgdata_room_fileposition_list + room * 24);
|
||||
#endif
|
||||
|
||||
limits.minX = 0x7fff;
|
||||
limits.minY = 0x7fff;
|
||||
|
||||
@@ -438,7 +438,11 @@ void bheadAdjustAnimation(f32 speed)
|
||||
modelSetAnimation(
|
||||
&g_CurrentPlayer->model,
|
||||
// match hack: addu address backwards
|
||||
#ifdef PORT
|
||||
(struct ModelAnimation *) ((s32)g_BondMoveAnimationSetup[i].anim_id + (uintptr_t)&ptr_animation_table->data),
|
||||
#else
|
||||
(struct ModelAnimation *) ((s32)g_BondMoveAnimationSetup[i].anim_id + (s32)&ptr_animation_table->data),
|
||||
#endif
|
||||
(s32) BHEAD_FLIPFLAG,
|
||||
startframe,
|
||||
0.5f,
|
||||
|
||||
@@ -981,7 +981,11 @@ void bondviewSetCameraMode(s32 arg0)
|
||||
solo_char_load();
|
||||
|
||||
// HACK: ptr_animation_table->data regalloc is backwards
|
||||
#ifdef PORT
|
||||
sp38 = (struct ModelAnimation *)((s32)stage_intro_anim_table[g_IntroAnimationIndex].anonymous_0 + (uintptr_t)&ptr_animation_table->data);
|
||||
#else
|
||||
sp38 = (struct ModelAnimation *)((s32)stage_intro_anim_table[g_IntroAnimationIndex].anonymous_0 + (s32)&ptr_animation_table->data);
|
||||
#endif
|
||||
sp78 = stage_intro_anim_table[g_IntroAnimationIndex].anonymous_2;
|
||||
ftemp_1 = stage_intro_anim_table[g_IntroAnimationIndex].anonymous_1;
|
||||
ftemp_3 = stage_intro_anim_table[g_IntroAnimationIndex].anonymous_3;
|
||||
@@ -1317,7 +1321,11 @@ void bondviewCalcIntroSwirlCamera(s32 index, f32 time, coord3d *pos, coord3d *lo
|
||||
lookat->y = g_CurrentPlayer->field_3C8;
|
||||
lookat->z = g_CurrentPlayer->field_3CC;
|
||||
|
||||
#ifdef PORT
|
||||
swirl = (void *)(((uintptr_t) g_IntroSwirl) + (uintptr_t) base);
|
||||
#else
|
||||
swirl = (void *)(((u32) g_IntroSwirl) + (u32) base);
|
||||
#endif
|
||||
|
||||
if (!(swirl->bitflags & 4))
|
||||
{
|
||||
@@ -4572,7 +4580,11 @@ void bondviewMoveAnimationTick(f32 speed, f32 speedforwards, f32 speedsideways)
|
||||
// HACK: ptr_animation_table dereference addition is backwards.
|
||||
// this should be:
|
||||
// ptr_animation_table->data[g_bondviewBondDeathAnimations[((u32) randomGetNext() % (u32) g_bondviewBondDeathAnimationsCount)]]
|
||||
#ifdef PORT
|
||||
bheadStartDeathAnimation((struct ModelAnimation *) ((s32)g_bondviewBondDeathAnimations[((u32) randomGetNext() % (u32) g_bondviewBondDeathAnimationsCount)] + (uintptr_t)&ptr_animation_table->data[0]), randomGetNext() & 1, 0.0f, 1.0f);
|
||||
#else
|
||||
bheadStartDeathAnimation((struct ModelAnimation *) ((s32)g_bondviewBondDeathAnimations[((u32) randomGetNext() % (u32) g_bondviewBondDeathAnimationsCount)] + (s32)&ptr_animation_table->data[0]), randomGetNext() & 1, 0.0f, 1.0f);
|
||||
#endif
|
||||
g_CurrentPlayer->startnewbonddie = FALSE;
|
||||
}
|
||||
|
||||
@@ -9057,7 +9069,11 @@ void mp_respawn_handler(void)
|
||||
switch (intro_record->type)
|
||||
{
|
||||
case 0: // INTROTYPE_SPAWN
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroSpawn));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroSpawn));
|
||||
#endif
|
||||
break;
|
||||
case 1: // INTROTYPE_ITEM
|
||||
if (check_ramrom_flags() == ((struct SetupIntroAmmo*)intro_record)->is_demo_playback) {
|
||||
@@ -9067,28 +9083,56 @@ void mp_respawn_handler(void)
|
||||
bondinvAddInvItem(((struct SetupIntroItem*)intro_record)->item_right);
|
||||
}
|
||||
}
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroItem));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroItem));
|
||||
#endif
|
||||
break;
|
||||
case 2: // INTROTYPE_AMMO
|
||||
if (check_ramrom_flags() == ((struct SetupIntroAmmo*)intro_record)->is_demo_playback) {
|
||||
give_cur_player_ammo(((struct SetupIntroAmmo*)intro_record)->ammo_type, ((struct SetupIntroAmmo*)intro_record)->ammo_amount);
|
||||
}
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroAmmo));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroAmmo));
|
||||
#endif
|
||||
break;
|
||||
case 3: // INTROTYPE_SWIRL
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroSwirl));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroSwirl));
|
||||
#endif
|
||||
break;
|
||||
case 4: // INTROTYPE_ANIM
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroAnim));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroAnim));
|
||||
#endif
|
||||
break;
|
||||
case 5: // INTROTYPE_CUFF
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroCuff));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroCuff));
|
||||
#endif
|
||||
break;
|
||||
case 6: // INTROTYPE_CAMERA
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroCamera));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroCamera));
|
||||
#endif
|
||||
break;
|
||||
default: // INTROTYPE_WATCH, INTROTYPE_CREDITS
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroEmpty));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroEmpty));
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
#ifdef DEBUG
|
||||
@@ -10399,7 +10443,11 @@ Gfx* hudmsgBottomRender(Gfx* arg0)
|
||||
}
|
||||
|
||||
view_vert = view_top - view_top_offset;
|
||||
#ifdef PORT
|
||||
arg0 = draw_blackbox_to_screen(arg0, &view_left, &view_vert, &view_horiz, &view_top);
|
||||
#else
|
||||
arg0 = draw_blackbox_to_screen(arg0, (s32) &view_left, (s32) &view_vert, (s32) &view_horiz, (s32) &view_top);
|
||||
#endif
|
||||
arg0 = combiner_bayer_lod_perspective(textRenderOutlined(arg0, &view_left, &view_vert, stringbuffer_lowerleft[status_bar_text_buffer_index], BONDVIEW_2ND_FONTTABLE(status_bar_text_buffer_index), BONDVIEW_1ST_FONTTABLE(status_bar_text_buffer_index), -1, 0x646464FFU, (s16) (s32) viGetX(), (s16) viGetY(), 0, 0));
|
||||
}
|
||||
}
|
||||
@@ -11412,7 +11460,11 @@ void sub_GAME_7F08BEEC(Mtxf *matrices, s32 count)
|
||||
|
||||
for (i = 0, j = 0; i < count; i++, j += sizeof(Mtxf))
|
||||
{
|
||||
#ifdef PORT
|
||||
matrix_4x4_multiply_homogeneous(currentPlayerGetViewToWorldMtxf(), (Mtxf *)((uintptr_t)matrices + j), &sp40);
|
||||
#else
|
||||
matrix_4x4_multiply_homogeneous(currentPlayerGetViewToWorldMtxf(), (Mtxf *)((u32)matrices + j), &sp40);
|
||||
#endif
|
||||
|
||||
sp40.m[3][0] -= g_CurrentPlayer->current_model_pos.f[0];
|
||||
sp40.m[3][1] -= g_CurrentPlayer->current_model_pos.f[1];
|
||||
|
||||
+45
-2
@@ -189,7 +189,11 @@ void bondviewLoadSetupIntroSection(void)
|
||||
startpadcount++;
|
||||
}
|
||||
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroSpawn));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroSpawn));
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -226,7 +230,11 @@ void bondviewLoadSetupIntroSection(void)
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroItem));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroItem));
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -237,7 +245,11 @@ void bondviewLoadSetupIntroSection(void)
|
||||
give_cur_player_ammo(((struct SetupIntroAmmo*)intro_record)->ammo_type, ((struct SetupIntroAmmo*)intro_record)->ammo_amount);
|
||||
}
|
||||
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroAmmo));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroAmmo));
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -256,7 +268,11 @@ void bondviewLoadSetupIntroSection(void)
|
||||
intro_swirl->unk14.fval = intro_swirl->unk14.ival / M_U16_MAX_VALUE_F;
|
||||
intro_swirl->unk18.fval = intro_swirl->unk18.ival / M_U16_MAX_VALUE_F;
|
||||
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroSwirl));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroSwirl));
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -264,7 +280,11 @@ void bondviewLoadSetupIntroSection(void)
|
||||
{
|
||||
g_IntroAnimationIndex = ((struct SetupIntroAnim*)intro_record)->intro_anim;
|
||||
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroAnim));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroAnim));
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -272,7 +292,11 @@ void bondviewLoadSetupIntroSection(void)
|
||||
{
|
||||
g_CurrentPlayer->bondtype = ((struct SetupIntroCuff*)intro_record)->bondtype;
|
||||
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroCuff));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroCuff));
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -311,7 +335,11 @@ void bondviewLoadSetupIntroSection(void)
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroCamera));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroCamera));
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -333,7 +361,11 @@ void bondviewLoadSetupIntroSection(void)
|
||||
|
||||
if (watch_time_0);
|
||||
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroWatch));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroWatch));
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -342,7 +374,11 @@ void bondviewLoadSetupIntroSection(void)
|
||||
intro_credits = (struct SetupIntroCredits*)intro_record;
|
||||
|
||||
// hack: bad address math
|
||||
#ifdef PORT
|
||||
credits = (CreditsEntry*)((uintptr_t)g_ptrStageSetupFile + (s32)intro_credits->unk04);
|
||||
#else
|
||||
credits = (CreditsEntry*)((s32)g_ptrStageSetupFile + (s32)intro_credits->unk04);
|
||||
#endif
|
||||
credits_pointer = credits;
|
||||
|
||||
// what is the point of this?
|
||||
@@ -351,7 +387,11 @@ void bondviewLoadSetupIntroSection(void)
|
||||
credits++;
|
||||
}
|
||||
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroCredits));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroCredits));
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -360,7 +400,11 @@ void bondviewLoadSetupIntroSection(void)
|
||||
#ifdef DEBUG
|
||||
ossyncprintf("unknown bondstart type %d!\n",intro_record->type);
|
||||
#endif
|
||||
#ifdef PORT
|
||||
intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroEmpty));
|
||||
#else
|
||||
intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroEmpty));
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -399,8 +443,7 @@ void bondviewLoadSetupIntroSection(void)
|
||||
bondinvAddInvItem(ITEM_FIST);
|
||||
#ifdef PORT
|
||||
/* TEMP D303 (M-201): test-only start weapon, GE_STARTWEAPON=<ITEM_IDS>,
|
||||
* so headless repros can hold a specific gun (pair with GE_D320R
|
||||
* cheat=3 for ammo). Inert unset. */
|
||||
* so headless repros can hold a specific gun. Inert unset. */
|
||||
{
|
||||
extern char *getenv(const char *name);
|
||||
extern int atoi(const char *s);
|
||||
|
||||
@@ -884,6 +884,25 @@ struct anim_group_info *ptr_doubles_firing_animation_groups[] = {
|
||||
&ptr_doubles_firing_animation_group1
|
||||
};
|
||||
|
||||
#ifdef PORT
|
||||
/* D445 (ABI/layout, porting-notes §D5 linker adjacency): on the N64,
|
||||
* crouched_rifle_firing_animation_group1 has NO terminator and the linker
|
||||
* places crouched_rifle_firing_animation_groupA right after it, so the one
|
||||
* resolver pass over group1 (initResolveAnimGroupTable, via
|
||||
* ptr_crouched_rifle_firing_animation_groups) walks into groupA and turns its
|
||||
* anim offset into a pointer too; the table len is 2. GCC does not keep the
|
||||
* two arrays adjacent (x86-64: groupA BELOW group1), so groupA[0] kept the raw
|
||||
* offset 0xB84 (PTR_ANIM_fire_kneel_left_leg) and playerTick handed it to
|
||||
* modelSetAnimation as a ModelAnimation* (SIGSEGV, crouched rifle fire), and
|
||||
* crouching rifle guards saw len 1 instead of 2. One array restores the N64
|
||||
* layout; groupA is its second entry (chr.h). Same data, same order. */
|
||||
struct weapon_firing_animation_table crouched_rifle_firing_animation_group1[] = {
|
||||
{ PTR_ANIM_fire_kneel_right_leg, 27.0, 0, 0, 0, -1.0, 35.0, 75.0, -1.0, -1.0, 31.0, 75.0, 0.87266463, -0.69813174, 0.90757126, -0.69813174, 1.5, 1.5 },
|
||||
/* = crouched_rifle_firing_animation_groupA[0] */
|
||||
{ PTR_ANIM_fire_kneel_left_leg, 24.0, 0, 0, 0, -1.0, 46.0, 98.0, -1.0, -1.0, 41.0, 98.0, 0.87266463, -0.52359879, 1.134464, -0.69813174, 1.6, 1.6 },
|
||||
{0, 0.0, 0, 0, 0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}
|
||||
};
|
||||
#else
|
||||
struct weapon_firing_animation_table crouched_rifle_firing_animation_group1[] = {
|
||||
{ PTR_ANIM_fire_kneel_right_leg, 27.0, 0, 0, 0, -1.0, 35.0, 75.0, -1.0, -1.0, 31.0, 75.0, 0.87266463, -0.69813174, 0.90757126, -0.69813174, 1.5, 1.5 },
|
||||
};
|
||||
@@ -892,6 +911,7 @@ struct weapon_firing_animation_table crouched_rifle_firing_animation_groupA[] =
|
||||
{ PTR_ANIM_fire_kneel_left_leg, 24.0, 0, 0, 0, -1.0, 46.0, 98.0, -1.0, -1.0, 41.0, 98.0, 0.87266463, -0.52359879, 1.134464, -0.69813174, 1.6, 1.6 },
|
||||
{0, 0.0, 0, 0, 0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}
|
||||
};
|
||||
#endif
|
||||
|
||||
struct anim_group_info ptr_crouched_rifle_firing_animation_group1 = { &crouched_rifle_firing_animation_group1, -1 };
|
||||
|
||||
|
||||
@@ -181,7 +181,12 @@ extern struct weapon_firing_animation_table doubles_firing_animation_group1[];
|
||||
extern struct weapon_firing_animation_table doubles_firing_animation_group2[];
|
||||
extern struct weapon_firing_animation_table doubles_firing_animation_group3[];
|
||||
extern struct weapon_firing_animation_table crouched_rifle_firing_animation_group1[];
|
||||
#ifdef PORT
|
||||
/* D445: groupA is the second entry of group1 (N64 adjacency, see chr.c). */
|
||||
#define crouched_rifle_firing_animation_groupA (&crouched_rifle_firing_animation_group1[1])
|
||||
#else
|
||||
extern struct weapon_firing_animation_table crouched_rifle_firing_animation_groupA[];
|
||||
#endif
|
||||
extern struct weapon_firing_animation_table crouched_rifle_firing_animation_group2[];
|
||||
extern struct weapon_firing_animation_table crouched_rifle_firing_animation_group3[];
|
||||
extern struct weapon_firing_animation_table crouched_pistol_firing_animation_group1[];
|
||||
|
||||
+44
-56
@@ -507,7 +507,11 @@ void chrlvKneelingAnimationRelated(ChrRecord *self)
|
||||
|
||||
if ((s32)objecthandlerGetModelAnim(self->model) == (s32)&ANIM_DATA_fire_kneel_forward_one_handed_weapon_slow + (s32)&ptr_animation_table->data)
|
||||
{
|
||||
#ifdef PORT
|
||||
modelSetAnimation(self->model, (struct ModelAnimation*)((s32)&ANIM_DATA_fire_kneel_forward_one_handed_weapon_slow + (uintptr_t)&ptr_animation_table->data), (s32) self->model->gunhand, 109.0f, chrlvGetGuard007SpeedRating(self, 0.5f, 0.8f), 16.0f);
|
||||
#else
|
||||
modelSetAnimation(self->model, (struct ModelAnimation*)((s32)&ANIM_DATA_fire_kneel_forward_one_handed_weapon_slow + (s32)&ptr_animation_table->data), (s32) self->model->gunhand, 109.0f, chrlvGetGuard007SpeedRating(self, 0.5f, 0.8f), 16.0f);
|
||||
#endif
|
||||
modelSetAnimEndFrame(self->model, 140.0f);
|
||||
}
|
||||
else
|
||||
@@ -2822,7 +2826,11 @@ s32 chrlvExplosionDamage(ChrRecord *self, coord3d *arg1, f32 damage, s32 arg3)
|
||||
|
||||
modelSetAnimation(
|
||||
self_model,
|
||||
#ifdef PORT
|
||||
(struct ModelAnimation *) ((s32)sp38->anonymous_0 + (uintptr_t)&ptr_animation_table->data),
|
||||
#else
|
||||
(struct ModelAnimation *) ((s32)sp38->anonymous_0 + (s32)&ptr_animation_table->data),
|
||||
#endif
|
||||
sp38->anonymous_1,
|
||||
sp38->anonymous_3,
|
||||
sp38->anonymous_2,
|
||||
@@ -5075,7 +5083,11 @@ void chrlvTickStand(ChrRecord *self)
|
||||
modelSetAnimation(
|
||||
self->model,
|
||||
// awkward fix: addu instruction is backwards
|
||||
#ifdef PORT
|
||||
(struct ModelAnimation *)((s32)&ANIM_DATA_walking_unarmed + (uintptr_t)&ptr_animation_table->data),
|
||||
#else
|
||||
(struct ModelAnimation *)((s32)&ANIM_DATA_walking_unarmed + (s32)&ptr_animation_table->data),
|
||||
#endif
|
||||
i,
|
||||
0.0f,
|
||||
0.5f,
|
||||
@@ -5083,14 +5095,22 @@ void chrlvTickStand(ChrRecord *self)
|
||||
|
||||
modelSetAnimEndFrame(
|
||||
self->model,
|
||||
#ifdef PORT
|
||||
(((u16*)((s32)&ANIM_DATA_walking_unarmed + (uintptr_t)&ptr_animation_table->data))[2] - 1));
|
||||
#else
|
||||
(((u16*)((s32)&ANIM_DATA_walking_unarmed + (s32)&ptr_animation_table->data))[2] - 1));
|
||||
#endif
|
||||
}
|
||||
else if ((right != NULL) || (left != NULL))
|
||||
{
|
||||
modelSetAnimation(
|
||||
self->model,
|
||||
// awkward fix: addu instruction is backwards
|
||||
#ifdef PORT
|
||||
(struct ModelAnimation *)((s32)&ANIM_DATA_walking + (uintptr_t)&ptr_animation_table->data),
|
||||
#else
|
||||
(struct ModelAnimation *)((s32)&ANIM_DATA_walking + (s32)&ptr_animation_table->data),
|
||||
#endif
|
||||
left != NULL,
|
||||
0.0f,
|
||||
0.5f,
|
||||
@@ -5098,7 +5118,11 @@ void chrlvTickStand(ChrRecord *self)
|
||||
|
||||
modelSetAnimEndFrame(
|
||||
self->model,
|
||||
#ifdef PORT
|
||||
(((u16*)((s32)&ANIM_DATA_walking + (uintptr_t)&ptr_animation_table->data))[2] - 1));
|
||||
#else
|
||||
(((u16*)((s32)&ANIM_DATA_walking + (s32)&ptr_animation_table->data))[2] - 1));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else if (self->act_stand.face_entitytype & 0x10)
|
||||
@@ -5450,13 +5474,25 @@ void chrlvTickDie(ChrRecord *self)
|
||||
{
|
||||
modelSetAnimation(
|
||||
model,
|
||||
#ifdef PORT
|
||||
(void*)((s32)&ANIM_DATA_jump_backwards + (uintptr_t)&ptr_animation_table->data),
|
||||
#else
|
||||
(void*)((s32)&ANIM_DATA_jump_backwards + (s32)&ptr_animation_table->data),
|
||||
#endif
|
||||
objecthandlerGetModelGunhand(model) == 0,
|
||||
50.0f,
|
||||
0.3f,
|
||||
#ifdef PORT
|
||||
(((u16*)((s32)&ANIM_DATA_jump_backwards + (uintptr_t)&ptr_animation_table->data))[2] - 1.0f) - 50.0f);
|
||||
#else
|
||||
(((u16*)((s32)&ANIM_DATA_jump_backwards + (s32)&ptr_animation_table->data))[2] - 1.0f) - 50.0f);
|
||||
#endif
|
||||
|
||||
#ifdef PORT
|
||||
modelSetAnimSpeed(model, 0.5f, (((u16*)((s32)&ANIM_DATA_jump_backwards + (uintptr_t)&ptr_animation_table->data))[2] - 1.0f) - 50.0f);
|
||||
#else
|
||||
modelSetAnimSpeed(model, 0.5f, (((u16*)((s32)&ANIM_DATA_jump_backwards + (s32)&ptr_animation_table->data))[2] - 1.0f) - 50.0f);
|
||||
#endif
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -9160,8 +9196,16 @@ void chrlvTravelTick(ChrRecord *self, coord3d *arg1, StandTile *arg2, struct way
|
||||
|
||||
if ((phi_s3 == NULL) || ((self->hidden & CHRHIDDEN_OFFSCREEN_PATROL) != 0))
|
||||
{
|
||||
#ifdef PORT
|
||||
if ((objecthandlerGetModelAnim(self->model) == (struct ModelAnimation *)((s32)&ANIM_DATA_idle_unarmed + (uintptr_t)&ptr_animation_table->data))
|
||||
#else
|
||||
if ((objecthandlerGetModelAnim(self->model) == (struct ModelAnimation *)((s32)&ANIM_DATA_idle_unarmed + (s32)&ptr_animation_table->data))
|
||||
#endif
|
||||
#ifdef PORT
|
||||
|| (objecthandlerGetModelAnim(self->model) == (struct ModelAnimation *)((s32)&ANIM_DATA_idle + (uintptr_t)&ptr_animation_table->data)))
|
||||
#else
|
||||
|| (objecthandlerGetModelAnim(self->model) == (struct ModelAnimation *)((s32)&ANIM_DATA_idle + (s32)&ptr_animation_table->data)))
|
||||
#endif
|
||||
{
|
||||
if (self->actiontype == ACT_PATROL)
|
||||
{
|
||||
@@ -10144,30 +10188,6 @@ bool chrGoToBond(ChrRecord *self, SPEED speed)
|
||||
{
|
||||
PropRecord *bondprop;
|
||||
|
||||
#ifdef PORT
|
||||
/* D309 (diagnosis only, GE_D309=1): the Caverns->intro complete freeze
|
||||
* spins in ai() on m_RunToBondPersistent the first time TRYRunToBond
|
||||
* FAILS for a guard on that list (the 1-byte AI_PRINT record is then
|
||||
* mis-sized by chraiitemsize's NUL scan -> phantom GotoNext(0) ->
|
||||
* chraiGoToLabel returns 0 -> tight restart loop, see findings.md D309).
|
||||
* The code+data are byte-matched ground truth, so the open question is
|
||||
* WHICH failure branch fires and in what game state at Caverns' end --
|
||||
* dead/shot gate, seen-count gate, or plot_course (whose three sub-
|
||||
* conditions are logged here: own stan-path, Bond's stan-path, route).
|
||||
* Read-only probe: chrlvStanPathRelated/waypointFindRoute are pure
|
||||
* lookups. No behavior change; N64 build unaffected. */
|
||||
extern char *getenv(const char *);
|
||||
extern u8 m_RunToBondPersistent[];
|
||||
extern s32 g_MainStageNum;
|
||||
extern LEVELID bossGetStageNum(void);
|
||||
static int s_d309 = -1;
|
||||
static int s_d309n = 0;
|
||||
int d309log;
|
||||
|
||||
if (s_d309 < 0) { s_d309 = getenv("GE_D309") != NULL; }
|
||||
d309log = s_d309 && (self->ailist == (AIRecord *)m_RunToBondPersistent) && (s_d309n < 400);
|
||||
#endif
|
||||
|
||||
if (chrIsNotDeadOrShot(self) && (g_SeenBondRecentlyGuardCount < 10))
|
||||
{
|
||||
bondprop = getCurrentPlayerProp();
|
||||
@@ -10176,39 +10196,7 @@ bool chrGoToBond(ChrRecord *self, SPEED speed)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
#ifdef PORT
|
||||
if (d309log)
|
||||
{
|
||||
waypoint *wpSelf = chrlvStanPathRelated(&self->prop->pos, self->prop->stan);
|
||||
waypoint *wpBond = chrlvStanPathRelated(&bondprop->pos, bondprop->stan);
|
||||
s32 route = -1;
|
||||
|
||||
if (wpSelf && wpBond)
|
||||
{
|
||||
waypoint sp44[MAX_CHRWAYPOINTS];
|
||||
|
||||
route = waypointFindRoute(wpSelf, wpBond, sp44, MAX_CHRWAYPOINTS);
|
||||
}
|
||||
osSyncPrintf("D309: FAIL-plot chr=%d act=%d flags2=0x%x stage=%d curload=%d mainstage=%d cammode=%d stoptime=%d seen=%d bondprop=%p bondstan=%p wpSelf=%p wpBond=%p route=%d\n",
|
||||
(int)self->chrnum, (int)self->actiontype, (int)self->chrflags & 0xffff,
|
||||
(int)bossGetStageNum(), (int)lvlGetCurrentStageToLoad(), (int)g_MainStageNum,
|
||||
(int)g_CameraMode, (int)stop_time_flag, (int)g_SeenBondRecentlyGuardCount,
|
||||
(void *)bondprop, (void *)bondprop->stan, (void *)wpSelf, (void *)wpBond, (int)route);
|
||||
s_d309n++;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#ifdef PORT
|
||||
else if (d309log)
|
||||
{
|
||||
osSyncPrintf("D309: FAIL-gate chr=%d act=%d deadOrShot=%d seen=%d stage=%d curload=%d mainstage=%d cammode=%d stoptime=%d\n",
|
||||
(int)self->chrnum, (int)self->actiontype, (int)(!chrIsNotDeadOrShot(self)),
|
||||
(int)g_SeenBondRecentlyGuardCount,
|
||||
(int)bossGetStageNum(), (int)lvlGetCurrentStageToLoad(), (int)g_MainStageNum,
|
||||
(int)g_CameraMode, (int)stop_time_flag);
|
||||
s_d309n++;
|
||||
}
|
||||
#endif
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
+2
-45
@@ -35,6 +35,7 @@
|
||||
#include <ultra64.h>
|
||||
#ifdef PORT
|
||||
#include <stdlib.h>
|
||||
#include "envflag.h"
|
||||
#endif
|
||||
|
||||
// hack? used to match as called with 2 args, but decompiled code takes 1
|
||||
@@ -796,26 +797,6 @@ s32 chraiGoToLabel(AIRecord *AIList, s32 Offset, u8 LabelNum)
|
||||
}
|
||||
else if (AIList[Offset].cmd == AI_EndList)
|
||||
{
|
||||
#ifdef PORT
|
||||
/* D309 (diagnosis only, GE_D309=1): label scan ran off the end of
|
||||
* the list -- the decomp's own comment says the restart-PC-to-0
|
||||
* return "causes infinite loop outside of debug". This is the
|
||||
* spin signature; logging it catches any AI list (not just
|
||||
* m_RunToBondPersistent) whose mis-sized skip produced a phantom
|
||||
* GotoNext to a nonexistent label. */
|
||||
{
|
||||
static int s_d309g = -1;
|
||||
static int s_d309gn = 0;
|
||||
|
||||
if (s_d309g < 0) { s_d309g = getenv("GE_D309") != NULL; }
|
||||
if (s_d309g && s_d309gn < 400)
|
||||
{
|
||||
osSyncPrintf("D309: LABEL-NOT-FOUND list=%p off=%d label=%d\n",
|
||||
(void *)AIList, (int)Offset, (int)LabelNum);
|
||||
s_d309gn++;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// restart ai list PC if next label not found - causes infinite loop outside of debug
|
||||
listID = chraiGetAIListID(AIList, &isGlobalAIList);
|
||||
#ifdef DEBUG
|
||||
@@ -1161,7 +1142,7 @@ void ai(PropDefHeaderRecord *Entityp, PROP_TYPE EntityType)
|
||||
* every time this specific anim_id fires. Env-gated
|
||||
* (GE_D243M, already cached elsewhere), essentially free
|
||||
* when unset. Diagnosis only. */
|
||||
if (getenv("GE_D243M") != NULL)
|
||||
if (GE_ENVFLAG("GE_D243M"))
|
||||
{
|
||||
/* M-170 correction: the first version of this probe
|
||||
* filtered on anim_id==0xb100 and got ZERO hits
|
||||
@@ -3410,30 +3391,6 @@ void ai(PropDefHeaderRecord *Entityp, PROP_TYPE EntityType)
|
||||
|
||||
/* D310: size the PRINT per list origin (see d310ItemSize). */
|
||||
(void)chraiGetAIListID(AiListp, &d310global);
|
||||
/* D309 (diagnosis only, GE_D309=1): log each step on a
|
||||
* PRINT record with the size actually used and the
|
||||
* command byte we land on. A repeating line (same
|
||||
* chr/list/off) means ai() re-executes the same path every
|
||||
* tick without advancing -- the standing-still signature;
|
||||
* landcmd==0x00 (AI_GotoNext) is the D309 spin variant.
|
||||
* Capped so a stuck loop can't flood stderr. */
|
||||
{
|
||||
static int s_d309p = -1;
|
||||
static int s_d309pn = 0;
|
||||
|
||||
if (s_d309p < 0) { s_d309p = getenv("GE_D309") != NULL; }
|
||||
if (s_d309p && s_d309pn < 400)
|
||||
{
|
||||
s32 sz = d310ItemSize(AiListp, Offset, d310global);
|
||||
|
||||
osSyncPrintf("D309: PRINT chr=%d list=%p off=%d size=%d landcmd=0x%02x global=%d\n",
|
||||
ChrEntityp ? (int)ChrEntityp->chrnum : -1,
|
||||
(void *)AiListp, (int)Offset, (int)sz,
|
||||
(int)(AiListp[Offset + sz].cmd & 0xff),
|
||||
(int)d310global);
|
||||
s_d309pn++;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef ENABLE_LOG
|
||||
AIRecord *ai = AiListp + Offset;
|
||||
|
||||
@@ -1734,7 +1734,11 @@ Gfx *explosionRenderFlyingParticles(Gfx *gdl)
|
||||
for (i = 0; i < max_particles; i++)
|
||||
{
|
||||
// HACK: regalloc has instructions backwards.
|
||||
#ifdef PORT
|
||||
particles = g_FlyingParticlesBuffer + i;
|
||||
#else
|
||||
particles = (struct FlyingParticles *)(u32)g_FlyingParticlesBuffer + i;
|
||||
#endif
|
||||
|
||||
if (particles->unk00 > 0)
|
||||
{
|
||||
|
||||
@@ -404,6 +404,16 @@ bool fileGetIsCheatUnlocked(save_data *save, s32 cheat)
|
||||
if (s_unlockAll < 0) s_unlockAll = getenv("GE_DEBUG_UNLOCKALL") != NULL;
|
||||
if (s_unlockAll) return TRUE;
|
||||
}
|
||||
/* D442 (RULE-2-SIGNOFF, maintainer opt-in port feature): the
|
||||
* Game.AllUnlocked setting is a pure query-time override -- the save is
|
||||
* never read or mutated for it, so nothing leaks into ge007.eep (D387)
|
||||
* and cheats earned while it is on persist normally. portAllUnlocked is
|
||||
* defined in port/src/video.c; levels/007 mode come from the game's own
|
||||
* debug flags seeded in port/src/main.c. */
|
||||
{
|
||||
extern s32 portAllUnlocked;
|
||||
if (portAllUnlocked) return TRUE;
|
||||
}
|
||||
/* D301 (github #87): callers (front.c mode-select cheat rows, file.c
|
||||
* briefing cheat check) pass fileGetSaveForFoldernum(selected_folder_num)
|
||||
* straight through with no NULL check. On real N64 that lookup can
|
||||
@@ -682,6 +692,22 @@ STAGESTATUS fileIsStageUnlockedAtDifficulty(s32 foldernum, LEVEL_SOLO_SEQUENCE l
|
||||
save_data* save;
|
||||
s32 i;
|
||||
|
||||
#ifdef PORT
|
||||
/* D442 (RULE-2-SIGNOFF, maintainer opt-in port feature): Game.AllUnlocked
|
||||
* reports every stage/difficulty as COMPLETED, query-time only -- the save
|
||||
* is never read or mutated for it. Needed because g_AppendCheatSinglePlayer
|
||||
* mission-select (front.c) accepts only COMPLETED, and the MP-character,
|
||||
* Magnum/Laser/Golden Gun and cheat.c level-unlock checks compare against
|
||||
* COMPLETED. Same bounds as the real path below. */
|
||||
{
|
||||
extern s32 portAllUnlocked;
|
||||
if (portAllUnlocked &&
|
||||
(levelid >= SP_LEVEL_DAM && levelid < SP_LEVEL_MAX) &&
|
||||
(difficulty >= DIFFICULTY_AGENT && difficulty < DIFFICULTY_MAX))
|
||||
return STAGESTATUS_COMPLETED;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (( fileIsFolderValid(foldernum)) &&
|
||||
(levelid >= SP_LEVEL_DAM && levelid < SP_LEVEL_MAX) &&
|
||||
(difficulty >= DIFFICULTY_AGENT && difficulty < DIFFICULTY_MAX))
|
||||
|
||||
+8
-3
@@ -959,6 +959,7 @@ Gfx *constructor_menu14_mpteams(Gfx *DL);
|
||||
Gfx *constructor_menu15_cheat(Gfx *DL);
|
||||
#ifdef PORT
|
||||
#include "frontoptions.h" /* D343: MENU_PC_OPTIONS lives in port/src/frontoptions.c */
|
||||
#include "envflag.h"
|
||||
#endif
|
||||
Gfx *constructor_menu16_nocontrollers(Gfx *DL);
|
||||
Gfx *constructor_menu17_switchscreens(Gfx *DL);
|
||||
@@ -2179,7 +2180,11 @@ void load_walletbond(void)
|
||||
b = (struct ModelNode *)mnode;
|
||||
srecord = b->Data;
|
||||
|
||||
#ifdef PORT
|
||||
arg0 = (Gfx_le *)((uintptr_t)srecord->BaseAddr + ((u32)(uintptr_t)srecord->Primary & 0xffffff));
|
||||
#else
|
||||
arg0 = (s32)srecord->BaseAddr + ((s32)srecord->Primary & 0xffffff);
|
||||
#endif
|
||||
bgApplyDynamicCCRMLUT(arg0, NULL, CCRMLUT_WALLETBOND);
|
||||
}
|
||||
}
|
||||
@@ -8207,7 +8212,7 @@ void interface_menu18_displaycast(void)
|
||||
f = intro_character_index;
|
||||
#ifdef PORT
|
||||
/* TEMP D65: trace the cast-end decision. */
|
||||
if (getenv("GE_D63"))
|
||||
if (GE_ENVFLAG("GE_D63"))
|
||||
osSyncPrintf("D65 cast-end f=%d body=%d\n", (int)f, (int)intro_char_table[f].body);
|
||||
#endif
|
||||
if (intro_char_table[f].body < 0)
|
||||
@@ -8215,7 +8220,7 @@ void interface_menu18_displaycast(void)
|
||||
intro_character_index = 0;
|
||||
f = 0;
|
||||
#ifdef PORT
|
||||
if (getenv("GE_D63"))
|
||||
if (GE_ENVFLAG("GE_D63"))
|
||||
osSyncPrintf("D65 cast-end RESET idx=0\n");
|
||||
#endif
|
||||
}
|
||||
@@ -8223,7 +8228,7 @@ void interface_menu18_displaycast(void)
|
||||
if (intro_character_index > 0)
|
||||
{
|
||||
#ifdef PORT
|
||||
if (getenv("GE_D63"))
|
||||
if (GE_ENVFLAG("GE_D63"))
|
||||
osSyncPrintf("D65 cast-end frontChangeMenu idx=%d\n", (int)intro_character_index);
|
||||
#endif
|
||||
frontChangeMenu(MENU_DISPLAY_CAST, 1);
|
||||
|
||||
@@ -3351,23 +3351,6 @@ void gunTickHandState(enum GUNHAND hand, s32 triggerOn)
|
||||
sndDeactivate((struct ALSoundState *) handptr->field_A48);
|
||||
}
|
||||
|
||||
#ifdef PORT
|
||||
/* TEMP D207/D241 (M-201): classify every shot-sound
|
||||
* attempt: into handle 1, handle 2, or SKIPPED because
|
||||
* both per-hand handles are still held (the stop above
|
||||
* is async). GE_D207S=1; one line per attempt. */
|
||||
if (bondwalkItemGetSound(var_s1) != 0)
|
||||
{
|
||||
extern char *getenv(const char *name);
|
||||
static int d207s = -1;
|
||||
if (d207s < 0) d207s = getenv("GE_D207S") != NULL;
|
||||
if (d207s)
|
||||
osSyncPrintf("D207S t=%d hand=%d item=%d rate=%d -> %s\n", (int)g_GlobalTimer, (int)hand,
|
||||
(int)var_s1, (int)bondwalkItemGetSoundTriggerRate(var_s1),
|
||||
handptr->audioHandle == NULL ? "H1"
|
||||
: (struct ALSoundState *)handptr->field_A48 == 0 ? "H2" : "SKIP");
|
||||
}
|
||||
#endif
|
||||
if (bondwalkItemGetSound(var_s1) != 0)
|
||||
{
|
||||
if (handptr->audioHandle == NULL)
|
||||
|
||||
+20
-19
@@ -414,7 +414,11 @@ s32 texAlignIndices(u8 *src, s32 width, s32 height, s32 format, u8 *dst)
|
||||
src++;
|
||||
}
|
||||
|
||||
#ifdef PORT
|
||||
outptr = (u8 *)(((uintptr_t)outptr + 7) & ~7);
|
||||
#else
|
||||
outptr = (u8 *)(((u32)outptr + 7) & ~7);
|
||||
#endif
|
||||
}
|
||||
|
||||
return outptr - dst;
|
||||
@@ -1739,9 +1743,21 @@ void texReadAlphaBits(u8 *image,s32 count)
|
||||
*/
|
||||
s32 texReadUncompressed(u8 *dst, s32 width, s32 height, s32 format)
|
||||
{
|
||||
#ifdef PORT
|
||||
u32 *dst32 = (u32 *)(((uintptr_t)dst + 0xf) & ~0xf);
|
||||
#else
|
||||
u32 *dst32 = (u32 *)(((u32)dst + 0xf) & ~0xf);
|
||||
#endif
|
||||
#ifdef PORT
|
||||
u16 *dst16 = (u16 *)(((uintptr_t)dst + 7) & ~7);
|
||||
#else
|
||||
u16 *dst16 = (u16 *)(((u32)dst + 7) & ~7);
|
||||
#endif
|
||||
#ifdef PORT
|
||||
u8 *dst8 = (u8 *)(((uintptr_t)dst + 7) & ~7);
|
||||
#else
|
||||
u8 *dst8 = (u8 *)(((u32)dst + 7) & ~7);
|
||||
#endif
|
||||
s32 x;
|
||||
s32 y;
|
||||
|
||||
@@ -2462,7 +2478,11 @@ void texLoadFromDisplayList(Gfx *gdl, struct texpool *arg1)
|
||||
if (bytes[0] == G_SETTIMG && bytes[4] == 0xab && bytes[5] == 0xcd)
|
||||
#endif
|
||||
{
|
||||
#ifdef PORT
|
||||
texLoad((u32 *)((uintptr_t)bytes + 4), arg1);
|
||||
#else
|
||||
texLoad((u32 *)((s32)bytes + 4), arg1);
|
||||
#endif
|
||||
}
|
||||
|
||||
bytes += 8;
|
||||
@@ -2575,27 +2595,8 @@ void texLoad(s32 *updateword, struct texpool *pool)
|
||||
// pointer to the start of the pool. It'll be garbage data but the
|
||||
// only other option is a crash. GBI commands contain texture IDs
|
||||
// instead of pointers, and they must be replaced with pointers.
|
||||
#if defined(PORT)
|
||||
/* D252 TEMP (2026-09-26): Facility explosions turn rainbow after the
|
||||
* player passes into the middle of the level (user). Log every new
|
||||
* texture load with pool identity + free bytes, and pool-full
|
||||
* events, to test the pool-exhaustion hypothesis. Load-time only. */
|
||||
{
|
||||
static int d252pool = -1;
|
||||
if (d252pool < 0) d252pool = getenv("GE_D252POOL") != NULL;
|
||||
if (d252pool)
|
||||
osSyncPrintf("D252POOL load texnum=%d pool=%s free=%d zlib=%d\n",
|
||||
(int)g_TexNumToLoad,
|
||||
pool == (struct texpool *)&ptr_texture_alloc_start ? "main" : "other",
|
||||
(int)texFreeBytesInBuffer(pool), (int)iszlib);
|
||||
}
|
||||
#endif
|
||||
if ((!iszlib && (texFreeBytesInBuffer(pool) < 0x10CC)) || (iszlib && texFreeBytesInBuffer(pool) < 0xA28)) {
|
||||
#if defined(PORT)
|
||||
if (getenv("GE_D252POOL"))
|
||||
osSyncPrintf("D252POOL POOL-FULL texnum=%d pool=%s free=%d\n", (int)g_TexNumToLoad,
|
||||
pool == (struct texpool *)&ptr_texture_alloc_start ? "main" : "other",
|
||||
(int)texFreeBytesInBuffer(pool));
|
||||
/* D85: env-gated -- a room rendering with placeholder textures
|
||||
* usually means the stage pool filled up here. */
|
||||
if (getenv("GE_D85TEX"))
|
||||
|
||||
@@ -242,7 +242,11 @@ void texReset(void)
|
||||
|
||||
size = (u32)&_GlobalimagetableSegmentEnd - (u32)&_GlobalimagetableSegmentStart;
|
||||
pGlobalimagetable = mempAllocBytesInBank(size + 0x1000, MEMPOOL_STAGE);
|
||||
#ifdef PORT
|
||||
pGlobalimagetable = (s32 *)(((uintptr_t)pGlobalimagetable + 0xFFFU) & ~(uintptr_t)0xFFF);
|
||||
#else
|
||||
pGlobalimagetable = ((u32)pGlobalimagetable + 0xFFFU) & 0xFFFFF000;
|
||||
#endif
|
||||
|
||||
romCopy(pGlobalimagetable, &_GlobalimagetableSegmentRomStart, size);
|
||||
|
||||
|
||||
@@ -166,7 +166,11 @@ void sets_a_bunch_of_BONDdata_values_to_default(void)
|
||||
{
|
||||
sub_GAME_7F0062C0(
|
||||
// match hack: addu address calculated backwards
|
||||
#ifdef PORT
|
||||
(void*)((s32)g_BondMoveAnimationSetup[i].anim_id + (uintptr_t)&ptr_animation_table->data),
|
||||
#else
|
||||
(void*)((s32)g_BondMoveAnimationSetup[i].anim_id + (s32)&ptr_animation_table->data),
|
||||
#endif
|
||||
(s32)g_BondMoveAnimationSetup[i].loopframe,
|
||||
(s32)g_BondMoveAnimationSetup[i].endframe,
|
||||
&spD0);
|
||||
@@ -194,7 +198,11 @@ void sets_a_bunch_of_BONDdata_values_to_default(void)
|
||||
modelSetAnimation(
|
||||
&g_CurrentPlayer->model,
|
||||
// match hack: addu address calculated backwards
|
||||
#ifdef PORT
|
||||
(struct ModelAnimation *) ((s32)g_BondMoveAnimationSetup[g_CurrentPlayer->headanim].anim_id + (uintptr_t)&ptr_animation_table->data),
|
||||
#else
|
||||
(struct ModelAnimation *) ((s32)g_BondMoveAnimationSetup[g_CurrentPlayer->headanim].anim_id + (s32)&ptr_animation_table->data),
|
||||
#endif
|
||||
0,
|
||||
g_BondMoveAnimationSetup[g_CurrentPlayer->headanim].loopframe,
|
||||
0.5f,
|
||||
|
||||
@@ -108,7 +108,11 @@ s32 initResolveAnimGroupTable(struct weapon_firing_animation_table *animconfig)
|
||||
|
||||
do
|
||||
{
|
||||
#ifdef PORT
|
||||
config->anim.anim = (struct ModelAnimation *)(((0, animoffset)) + ((uintptr_t)ptr_animation_table));
|
||||
#else
|
||||
config->anim.anim = (struct ModelAnimation *)(((0, animoffset)) + ((s32)ptr_animation_table));
|
||||
#endif
|
||||
endframe = floorFloatToInt(config->unk04);
|
||||
angle16 = sub_GAME_7F0001F0(config->anim.anim, 0, endframe) & 0xffff;
|
||||
duration = config->unk04;
|
||||
@@ -195,8 +199,13 @@ s32 initResolveAnimTable(struct StruckAnim *entries)
|
||||
}
|
||||
|
||||
|
||||
#ifdef PORT
|
||||
#define ANIM_PTR(anim) \
|
||||
((ModelAnimation *)((s32)&anim + ((uintptr_t)ptr_animation_table)))
|
||||
#else
|
||||
#define ANIM_PTR(anim) \
|
||||
((ModelAnimation *)((s32)&anim + ((s32)ptr_animation_table)))
|
||||
#endif
|
||||
|
||||
#define ANIM_FRAC(anim) \
|
||||
((((f32)sub_GAME_7F000290(ANIM_PTR(anim), 0, ANIM_PTR(anim)->unk04 - 1)) * 0.10000001f) / \
|
||||
|
||||
@@ -22,7 +22,7 @@ void set_mt_tex_alloc(void)
|
||||
* e.g. rainbow Frigate water) -- see GE_D85TEX's pool-full line. */
|
||||
{
|
||||
extern s32 g_StageNum;
|
||||
if (getenv("GE_D85TEX") || getenv("GE_D252POOL"))
|
||||
if (getenv("GE_D85TEX"))
|
||||
osSyncPrintf("D85TEX stage=%d texpool bytes=%d (-mt token %s) range=[%p..%p)\n", (int)g_StageNum,
|
||||
(int)bytes, tokenFind(1, "-mt") ? "present" : "ABSENT",
|
||||
(void *)ptr_texture_alloc_start.start, (void *)ptr_texture_alloc_start.end);
|
||||
|
||||
+1
-5
@@ -1053,12 +1053,8 @@ void lvlManageMpGame(void)
|
||||
{
|
||||
extern void d318WatchdogTick(void);
|
||||
extern void d318TimelineTick(void);
|
||||
extern void d320ReproTick(void);
|
||||
extern void d318bProbeTick(void);
|
||||
d318WatchdogTick(); /* D318 permanent port-side deadlock recovery (findings D318) */
|
||||
d318WatchdogTick(); /* D318/D320 pin detector, log-only since D329 (findings D318) */
|
||||
d318TimelineTick(); /* D318T env-gated diagnostic timeline (GE_D318T=1) */
|
||||
d320ReproTick(); /* D320R env-gated forced-repro harness (GE_D320R, TEMP) */
|
||||
d318bProbeTick(); /* D318B env-gated attack-state boundary probe (GE_D318B, TEMP) */
|
||||
}
|
||||
#endif
|
||||
if ((g_CurrentStageToLoad != LEVELID_TITLE) && (D_80048394 == 0) && (g_ClockTimer > 0))
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@
|
||||
#ifdef PORT
|
||||
#include <stdlib.h>
|
||||
#include "audiotrace.h"
|
||||
#include "envflag.h"
|
||||
#endif
|
||||
#include <music.h>
|
||||
#include "music_0D2720.h"
|
||||
@@ -74,7 +75,7 @@ void set_missionstate(MISSION_STATE_ID arg0)
|
||||
s32 old_mission_state = mission_state;
|
||||
|
||||
#ifdef PORT
|
||||
if (getenv("GE_AUDIOTRACE") && old_mission_state != (s32)arg0)
|
||||
if (GE_ENVFLAG("GE_AUDIOTRACE") && old_mission_state != (s32)arg0)
|
||||
geTracePrintf("audiotrace.log", "[MISSIONSTATE] %d -> %d\n", (int)old_mission_state, (int)arg0);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -1324,19 +1324,55 @@ void proplvreset2(enum LEVELID stageId)
|
||||
* The setup file stores every internal reference as a byte offset from the start of the file,
|
||||
* so rebase them all onto the RAM copy at local_stage.
|
||||
*/
|
||||
#ifdef PORT
|
||||
g_CurrentSetup.pathwaypoints = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->pathwaypoints));
|
||||
#else
|
||||
g_CurrentSetup.pathwaypoints = (void *) (((u32) local_stage) + ((u32) local_stage->pathwaypoints));
|
||||
#endif
|
||||
#ifdef PORT
|
||||
g_CurrentSetup.waypointgroups = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->waypointgroups));
|
||||
#else
|
||||
g_CurrentSetup.waypointgroups = (void *) (((u32) local_stage) + ((u32) local_stage->waypointgroups));
|
||||
#endif
|
||||
#ifdef PORT
|
||||
g_CurrentSetup.intro = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->intro));
|
||||
#else
|
||||
g_CurrentSetup.intro = (void *) (((u32) local_stage) + ((u32) local_stage->intro));
|
||||
#endif
|
||||
#ifdef PORT
|
||||
g_CurrentSetup.propDefs = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->propDefs));
|
||||
#else
|
||||
g_CurrentSetup.propDefs = (void *) (((u32) local_stage) + ((u32) local_stage->propDefs));
|
||||
#endif
|
||||
#ifdef PORT
|
||||
g_CurrentSetup.patrolpaths = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->patrolpaths));
|
||||
#else
|
||||
g_CurrentSetup.patrolpaths = (void *) (((u32) local_stage) + ((u32) local_stage->patrolpaths));
|
||||
#endif
|
||||
#ifdef PORT
|
||||
g_CurrentSetup.ailists = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->ailists));
|
||||
#else
|
||||
g_CurrentSetup.ailists = (void *) (((u32) local_stage) + ((u32) local_stage->ailists));
|
||||
#endif
|
||||
#ifdef PORT
|
||||
g_CurrentSetup.pads = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->pads));
|
||||
#else
|
||||
g_CurrentSetup.pads = (void *) (((u32) local_stage) + ((u32) local_stage->pads));
|
||||
#endif
|
||||
#ifdef PORT
|
||||
g_CurrentSetup.boundpads = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->boundpads));
|
||||
#else
|
||||
g_CurrentSetup.boundpads = (void *) (((u32) local_stage) + ((u32) local_stage->boundpads));
|
||||
#endif
|
||||
|
||||
// Pad names and bound names are optional. An offset of 0 means absent.
|
||||
if (local_stage->padnames != 0)
|
||||
{
|
||||
#ifdef PORT
|
||||
g_CurrentSetup.padnames = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->padnames));
|
||||
#else
|
||||
g_CurrentSetup.padnames = (void *) (((u32) local_stage) + ((u32) local_stage->padnames));
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1345,7 +1381,11 @@ void proplvreset2(enum LEVELID stageId)
|
||||
|
||||
if (local_stage->boundpadnames != 0)
|
||||
{
|
||||
#ifdef PORT
|
||||
g_CurrentSetup.boundpadnames = (void *) (((uintptr_t) local_stage) + ((u32) local_stage->boundpadnames));
|
||||
#else
|
||||
g_CurrentSetup.boundpadnames = (void *) (((u32) local_stage) + ((u32) local_stage->boundpadnames));
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1356,7 +1396,11 @@ void proplvreset2(enum LEVELID stageId)
|
||||
{
|
||||
for (i1 = 0; g_CurrentSetup.pathwaypoints[i1].padID >= 0; i1++)
|
||||
{
|
||||
#ifdef PORT
|
||||
g_CurrentSetup.pathwaypoints[i1].neighbours = (void *) (((u32) g_CurrentSetup.pathwaypoints[i1].neighbours) + ((uintptr_t) local_stage));
|
||||
#else
|
||||
g_CurrentSetup.pathwaypoints[i1].neighbours = (void *) (((u32) g_CurrentSetup.pathwaypoints[i1].neighbours) + ((u32) local_stage));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1364,8 +1408,16 @@ void proplvreset2(enum LEVELID stageId)
|
||||
{
|
||||
for (i2 = 0; g_CurrentSetup.waypointgroups[i2].neighbours; i2++)
|
||||
{
|
||||
#ifdef PORT
|
||||
g_CurrentSetup.waypointgroups[i2].neighbours = (void *) (((u32) g_CurrentSetup.waypointgroups[i2].neighbours) + ((uintptr_t) local_stage));
|
||||
#else
|
||||
g_CurrentSetup.waypointgroups[i2].neighbours = (void *) (((u32) g_CurrentSetup.waypointgroups[i2].neighbours) + ((u32) local_stage));
|
||||
#endif
|
||||
#ifdef PORT
|
||||
g_CurrentSetup.waypointgroups[i2].waypoints = (void *) (((u32) g_CurrentSetup.waypointgroups[i2].waypoints) + ((uintptr_t) local_stage));
|
||||
#else
|
||||
g_CurrentSetup.waypointgroups[i2].waypoints = (void *) (((u32) g_CurrentSetup.waypointgroups[i2].waypoints) + ((u32) local_stage));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1376,7 +1428,11 @@ void proplvreset2(enum LEVELID stageId)
|
||||
{
|
||||
for (i3 = 0; g_CurrentSetup.ailists[i3].ailist != 0; i3++)
|
||||
{
|
||||
#ifdef PORT
|
||||
g_CurrentSetup.ailists[i3].ailist = (void *) (((u32) g_CurrentSetup.ailists[i3].ailist) + ((uintptr_t) local_stage));
|
||||
#else
|
||||
g_CurrentSetup.ailists[i3].ailist = (void *) (((u32) g_CurrentSetup.ailists[i3].ailist) + ((u32) local_stage));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1385,7 +1441,11 @@ void proplvreset2(enum LEVELID stageId)
|
||||
{
|
||||
for (i3 = 0; g_CurrentSetup.patrolpaths[i3].waypoints != NULL; i3++)
|
||||
{
|
||||
#ifdef PORT
|
||||
g_CurrentSetup.patrolpaths[i3].waypoints = (void *) (((u32) g_CurrentSetup.patrolpaths[i3].waypoints) + ((uintptr_t) local_stage));
|
||||
#else
|
||||
g_CurrentSetup.patrolpaths[i3].waypoints = (void *) (((u32) g_CurrentSetup.patrolpaths[i3].waypoints) + ((u32) local_stage));
|
||||
#endif
|
||||
|
||||
for (i5 = 0; g_CurrentSetup.patrolpaths[i3].waypoints[i5] >= 0; i5++)
|
||||
{
|
||||
@@ -1405,7 +1465,11 @@ void proplvreset2(enum LEVELID stageId)
|
||||
|
||||
for (; pad->plink != NULL; pad++)
|
||||
{
|
||||
#ifdef PORT
|
||||
pad->plink = (void *) (((uintptr_t) local_stage) + ((u32) pad->plink));
|
||||
#else
|
||||
pad->plink = (void *) (((u32) local_stage) + ((u32) pad->plink));
|
||||
#endif
|
||||
pad->pos.f[0] *= roompos_1;
|
||||
pad->pos.f[1] *= roompos_1;
|
||||
pad->pos.f[2] *= roompos_1;
|
||||
@@ -1462,7 +1526,11 @@ void proplvreset2(enum LEVELID stageId)
|
||||
*/
|
||||
if ((((u32) local_stage) ^ 0) + ((u32)vol->plink));
|
||||
|
||||
#ifdef PORT
|
||||
vol->plink = (void *) (((uintptr_t) local_stage) + ((u32)vol->plink));
|
||||
#else
|
||||
vol->plink = (void *) (((u32) local_stage) + ((u32)vol->plink));
|
||||
#endif
|
||||
vol->pos.f[0] *= roompos_2;
|
||||
vol->pos.f[1] *= roompos_2;
|
||||
vol->pos.f[2] *= roompos_2;
|
||||
@@ -1497,7 +1565,11 @@ void proplvreset2(enum LEVELID stageId)
|
||||
{
|
||||
for (i1 = 0; g_CurrentSetup.padnames[i1].p; i1++)
|
||||
{
|
||||
#ifdef PORT
|
||||
g_CurrentSetup.padnames[i1].p = (void *) (((u32) g_CurrentSetup.padnames[i1].p) + ((uintptr_t) local_stage));
|
||||
#else
|
||||
g_CurrentSetup.padnames[i1].p = (void *) (((u32) g_CurrentSetup.padnames[i1].p) + ((u32) local_stage));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1508,7 +1580,11 @@ void proplvreset2(enum LEVELID stageId)
|
||||
|
||||
for (i1 = 0; g_CurrentSetup.boundpadnames[i1].p; i1++)
|
||||
{
|
||||
#ifdef PORT
|
||||
g_CurrentSetup.boundpadnames[i1].p = (void *) (((u32) g_CurrentSetup.boundpadnames[i1].p) + ((uintptr_t) local_stage));
|
||||
#else
|
||||
g_CurrentSetup.boundpadnames[i1].p = (void *) (((u32) g_CurrentSetup.boundpadnames[i1].p) + ((u32) local_stage));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-3
@@ -56,6 +56,9 @@
|
||||
#include "tex.h"
|
||||
#include "textrelated.h"
|
||||
#include "vtxstore.h"
|
||||
#ifdef PORT
|
||||
#include "envflag.h" /* cached getenv for per-tick probes (D302) */
|
||||
#endif
|
||||
|
||||
|
||||
#if defined(VERSION_JP) || defined(VERSION_EU)
|
||||
@@ -12782,7 +12785,7 @@ s32 sub_GAME_7F053894(coord3d *pos, f32 low, f32 high)
|
||||
* Log the hit pos, nearest player pos, distance and resulting vol so a
|
||||
* "loud impact that N64 plays silent" can be told apart as wrong-HIT vs
|
||||
* wrong-DISTANCE. Remove with probe set. */
|
||||
if (getenv("GE_AUDIOTRACE")) {
|
||||
if (GE_ENVFLAG("GE_AUDIOTRACE")) {
|
||||
geTracePrintf("audiotrace.log",
|
||||
"[DISTVOL] pos=(%.0f,%.0f,%.0f) player=(%.0f,%.0f,%.0f) dist=%.1f vol=%d\n",
|
||||
(double)pos->x, (double)pos->y, (double)pos->z,
|
||||
@@ -12869,7 +12872,7 @@ void sub_GAME_7F053A3C(DoorRecord* arg0)
|
||||
void doorSndProbe(const char *where, DoorRecord *door, void *pendingState)
|
||||
{
|
||||
static FILE *fd = NULL;
|
||||
if (!getenv("GE_AUDIOTRACE"))
|
||||
if (!GE_ENVFLAG("GE_AUDIOTRACE"))
|
||||
return;
|
||||
if (!fd) {
|
||||
fd = fopen("audiotrace.log", "a");
|
||||
@@ -14485,7 +14488,7 @@ void handle_alarm_gas_timer_calldamage(void)
|
||||
* root-cause "alarm SFX starves all other audio and never recovers".
|
||||
* GE_FORCEALARM pins alarm_timer so alarmIsActive() stays true. Remove
|
||||
* once root-caused. */
|
||||
if (getenv("GE_FORCEALARM")) {
|
||||
if (GE_ENVFLAG("GE_FORCEALARM")) {
|
||||
static int fa_frames;
|
||||
++fa_frames;
|
||||
if (fa_frames == 120 || fa_frames == 900) alarmActivate();
|
||||
|
||||
@@ -136,7 +136,11 @@ void finalize_ramrom_on_hw(void)
|
||||
u8 *p;
|
||||
void *a1;
|
||||
|
||||
#ifdef PORT
|
||||
p = (u8 *)ALIGN16_a((uintptr_t)buffer);
|
||||
#else
|
||||
p = ALIGN16_a((s32)buffer);
|
||||
#endif
|
||||
p[0] = 0;
|
||||
p[1] = 0;
|
||||
|
||||
|
||||
+5
-159
@@ -1740,34 +1740,6 @@ static void skyPortCaptureTile(Gfx *start, Gfx *end)
|
||||
}
|
||||
}
|
||||
|
||||
/* Re-emit every captured tile with shifts/shiftt replaced. k == 0 on both axes
|
||||
* restores the original commands, which is how the sky hands the tiles back
|
||||
* unshifted for whatever draws next. */
|
||||
static Gfx *skyPortEmitTileShift(Gfx *gdl, s32 kS, s32 kT)
|
||||
{
|
||||
s32 i;
|
||||
|
||||
for (i = 0; i < SKY_NUM_CAPTURED_TILES; i++)
|
||||
{
|
||||
Gfx cmd;
|
||||
|
||||
if (!s_skyTileCmdOk[i])
|
||||
continue;
|
||||
|
||||
cmd = s_skyTileCmd[i];
|
||||
cmd.words.w1 &= ~((u32) SKY_SETTILE_FIELD_MASK << SKY_SETTILE_SHIFTT_SHIFT);
|
||||
cmd.words.w1 &= ~((u32) SKY_SETTILE_FIELD_MASK);
|
||||
if (kT > 0)
|
||||
cmd.words.w1 |= (u32) (16 - kT) << SKY_SETTILE_SHIFTT_SHIFT;
|
||||
if (kS > 0)
|
||||
cmd.words.w1 |= (u32) (16 - kS);
|
||||
|
||||
*gdl++ = cmd;
|
||||
}
|
||||
|
||||
return gdl;
|
||||
}
|
||||
|
||||
/* Smallest k in [0, SKY_TC_MAX_SHIFT] for which the whole span still fits an
|
||||
* s16 once divided by 2^k. `span` is the fan's S or T extent in S10.5 units;
|
||||
* the +SKY_TC_WRAP covers the fold's rebase to a tile boundary. Normally
|
||||
@@ -1895,22 +1867,11 @@ static void skyPortForceFanShift(s32 kS, s32 kT)
|
||||
*/
|
||||
static Gfx *skyPortRenderPoly(Gfx *gdl, SkyRelated38 **v, s32 nverts)
|
||||
{
|
||||
Vtx *vtx = dynAllocateVertices(nverts < 3 ? 3 : nverts);
|
||||
Mtxf projf;
|
||||
Mtx *proj = dynAllocateMatrix();
|
||||
Mtx *mv = dynAllocateMatrix();
|
||||
f32 l = getPlayer_c_screenleft();
|
||||
f32 t = getPlayer_c_screentop();
|
||||
f32 r = l + getPlayer_c_screenwidth();
|
||||
f32 b = t + getPlayer_c_screenheight();
|
||||
f32 maxAbsW, wScale;
|
||||
s32 i;
|
||||
/* D227 (M-100). Fan-wide when skyPortBeginFan() ran (the only callers
|
||||
* today); a lone primitive falls back to sizing the shift off its own
|
||||
* span, which is self-consistent because k never changes the decoded
|
||||
* coordinate -- only its granularity. */
|
||||
s32 tcShiftS = s_skyFanShiftS;
|
||||
s32 tcShiftT = s_skyFanShiftT;
|
||||
|
||||
/* D176(a) Defect 1 (M-82), as corrected by D227 (M-100): unk20/unk24 are
|
||||
* S/T for the 64x64 cloud tile (s_skywaterimages[0]) in Vtx.tc's own S10.5
|
||||
@@ -1934,19 +1895,13 @@ static Gfx *skyPortRenderPoly(Gfx *gdl, SkyRelated38 **v, s32 nverts)
|
||||
}
|
||||
else
|
||||
{
|
||||
f32 maxS, maxT;
|
||||
|
||||
foldS = maxS = v[0]->unk20;
|
||||
foldT = maxT = v[0]->unk24;
|
||||
foldS = v[0]->unk20;
|
||||
foldT = v[0]->unk24;
|
||||
for (i = 1; i < nverts; i++)
|
||||
{
|
||||
if (v[i]->unk20 < foldS) foldS = v[i]->unk20;
|
||||
if (v[i]->unk24 < foldT) foldT = v[i]->unk24;
|
||||
if (v[i]->unk20 > maxS) maxS = v[i]->unk20;
|
||||
if (v[i]->unk24 > maxT) maxT = v[i]->unk24;
|
||||
}
|
||||
tcShiftS = skyPortPickShift(maxS - foldS);
|
||||
tcShiftT = skyPortPickShift(maxT - foldT);
|
||||
foldS = (f32) ((s32) floorf(foldS / (f32) SKY_TC_WRAP) * SKY_TC_WRAP);
|
||||
foldT = (f32) ((s32) floorf(foldT / (f32) SKY_TC_WRAP) * SKY_TC_WRAP);
|
||||
}
|
||||
@@ -1971,43 +1926,15 @@ static Gfx *skyPortRenderPoly(Gfx *gdl, SkyRelated38 **v, s32 nverts)
|
||||
* horizon edge. Fix: build a real perspective (w != 1) transform so the
|
||||
* existing fast3d vertex/triangle pipeline (same one every other
|
||||
* textured draw in the game already relies on) does the perspective
|
||||
* divide for us, exactly like the RDP would.
|
||||
*
|
||||
* ob[] is s16 so we can't store camera-space w (unk0c, up to ~2e5 near
|
||||
* the horizon) directly; instead each vertex's NDC xy is pre-multiplied
|
||||
* by its own w/wScale (wScale keeps the product in s16 range) and a
|
||||
* custom projection matrix multiplies that by wScale again for clip.x/y
|
||||
* and routes ob[2] (=w/wScale) through to clip.w -- see the matrix
|
||||
* comment below. Z is unused for the sky (draws first, into a cleared
|
||||
* buffer -- M-46) so clip.z is left 0.
|
||||
* divide for us, exactly like the RDP would. Z is unused for the sky
|
||||
* (draws first, into a cleared buffer -- M-46) so clip.z is left 0.
|
||||
*/
|
||||
/* D227 (M-95): prefer the whole-fan wScale set by skyPortBeginFan() so
|
||||
* every triangle in a multi-call fan quantizes its shared vertices
|
||||
* against the same scale (see the comment above skyPortBeginFan()).
|
||||
* Falls back to the old per-call-local computation if unset. */
|
||||
if (s_skyFanWScale > 0.0f)
|
||||
{
|
||||
wScale = s_skyFanWScale;
|
||||
}
|
||||
else
|
||||
{
|
||||
maxAbsW = 0.0f;
|
||||
for (i = 0; i < nverts; i++)
|
||||
{
|
||||
f32 aw = SKYABS(v[i]->unk0c);
|
||||
if (aw > maxAbsW) maxAbsW = aw;
|
||||
}
|
||||
wScale = (maxAbsW > 30000.0f) ? (maxAbsW / 30000.0f) : 1.0f;
|
||||
}
|
||||
|
||||
/* D245 (M-201): full-precision path. Hand fast3d the clip-space position
|
||||
* (ndcX*w, ndcY*w, 0, w) and the folded S/T as floats (G_FLOATVTX_EXT,
|
||||
* port/include/floatvtx.h) instead of squeezing them through s16 Vtx
|
||||
* ob/tc: no wScale position quantisation, no 2^k tc shift. The fold is
|
||||
* still applied (a multiple of the repeat period, exact under wrapping)
|
||||
* to keep the float magnitudes small. GE_D245_OLDVTX=1 restores the old
|
||||
* s16 route for A/B. */
|
||||
if (!GE_ENVFLAG("GE_D245_OLDVTX"))
|
||||
* to keep the float magnitudes small. */
|
||||
{
|
||||
PortFloatVtx *fv = (PortFloatVtx *) dynAllocateVertices(2 * (nverts < 3 ? 3 : nverts));
|
||||
|
||||
@@ -2043,89 +1970,8 @@ static Gfx *skyPortRenderPoly(Gfx *gdl, SkyRelated38 **v, s32 nverts)
|
||||
{
|
||||
gSP1Triangle(gdl++, 0, 1, 2, 0);
|
||||
}
|
||||
|
||||
return gdl;
|
||||
}
|
||||
|
||||
guMtxIdentF(projf.m);
|
||||
/* row = input axis (matches this codebase's row-vector * M convention,
|
||||
* e.g. sub_GAME_7F097388 / gfx_pc.cpp's own MP_matrix use):
|
||||
* clip.x = ob[0]*wScale = (ndcX*w/wScale)*wScale = ndcX*w
|
||||
* clip.y = ob[1]*wScale = ndcY*w
|
||||
* clip.z = 0
|
||||
* clip.w = ob[2]*wScale = (w/wScale)*wScale = w
|
||||
* so the GPU's normal perspective divide recovers ndcX/ndcY at each
|
||||
* vertex exactly (screen position unchanged) while interpolating
|
||||
* everything else -- tc included -- with real 1/w weighting. */
|
||||
projf.m[0][0] = wScale;
|
||||
projf.m[1][1] = wScale;
|
||||
projf.m[2][2] = 0.0f;
|
||||
projf.m[2][3] = wScale;
|
||||
projf.m[3][3] = 0.0f;
|
||||
guMtxF2L(projf.m, proj);
|
||||
guMtxIdent(mv);
|
||||
|
||||
for (i = 0; i < nverts; i++)
|
||||
{
|
||||
f32 screenX = v[i]->unk28 * 0.25f;
|
||||
f32 screenY = v[i]->unk2c * 0.25f;
|
||||
f32 ndcX = 2.0f * ((screenX - l) / (r - l)) - 1.0f;
|
||||
f32 ndcY = 1.0f - 2.0f * ((screenY - t) / (b - t));
|
||||
f32 wv = v[i]->unk0c / wScale;
|
||||
|
||||
vtx[i].v.ob[0] = (s16) (ndcX * wv);
|
||||
vtx[i].v.ob[1] = (s16) (ndcY * wv);
|
||||
vtx[i].v.ob[2] = (s16) wv;
|
||||
vtx[i].v.flag = 0;
|
||||
/* D227 (M-100): unk20/unk24 are ALREADY in tc's own S10.5 units, so
|
||||
* they go through 1:1 -- see the unit note above skyPortBeginFan().
|
||||
* The 2^k divide is the overflow safety valve, normally k == 0; the
|
||||
* tile shift emitted below multiplies it back out. */
|
||||
vtx[i].v.tc[0] = (s16) ((v[i]->unk20 - foldS) / (f32) (1 << tcShiftS));
|
||||
vtx[i].v.tc[1] = (s16) ((v[i]->unk24 - foldT) / (f32) (1 << tcShiftT));
|
||||
vtx[i].v.cn[0] = (u8) v[i]->r;
|
||||
vtx[i].v.cn[1] = (u8) v[i]->g;
|
||||
vtx[i].v.cn[2] = (u8) v[i]->b;
|
||||
vtx[i].v.cn[3] = (u8) v[i]->a;
|
||||
|
||||
/* D227 (M-100) probe: dumps the per-vertex S/T/w actually handed to
|
||||
* the rasteriser. This is what proved the seam is s16 tc overflow --
|
||||
* one sky quad spans ~30,000 texels in S and T, but tc is S10.5, so
|
||||
* (S - foldS) * 32 only holds +/-1024 texels. Same env-gate style as
|
||||
* the GE_D176 probes above. */
|
||||
if (GE_ENVFLAG("GE_D227V")) {
|
||||
static int n = 0;
|
||||
if (n++ < 200)
|
||||
fprintf(stderr, "D227V poly nv=%d i=%d w=%.1f 1/w=%.6g sx=%.1f sy=%.1f S=%.1f T=%.1f "
|
||||
"kS=%d kT=%d tc=%d,%d tileOk=%d\n",
|
||||
nverts, i, (double) v[i]->unk0c, (double) v[i]->unk34,
|
||||
(double) screenX, (double) screenY,
|
||||
(double) v[i]->unk20, (double) v[i]->unk24,
|
||||
tcShiftS, tcShiftT, vtx[i].v.tc[0], vtx[i].v.tc[1],
|
||||
(s32) s_skyTileCmdOk);
|
||||
}
|
||||
}
|
||||
|
||||
gSPMatrix(gdl++, osVirtualToPhysical(proj), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_PROJECTION);
|
||||
gSPMatrix(gdl++, osVirtualToPhysical(mv), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW);
|
||||
gSPClearGeometryMode(gdl++, G_LIGHTING | G_CULL_BOTH | G_FOG);
|
||||
gSPSetGeometryMode(gdl++, G_SHADE | G_SHADING_SMOOTH);
|
||||
gdl = skyPortEmitTileShift(gdl, tcShiftS, tcShiftT);
|
||||
gSPVertex(gdl++, osVirtualToPhysical(vtx), nverts, 0);
|
||||
|
||||
if (nverts >= 4)
|
||||
{
|
||||
gSP2Triangles(gdl++, 0, 1, 3, 0, 3, 2, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
gSP1Triangle(gdl++, 0, 1, 2, 0);
|
||||
}
|
||||
|
||||
/* Hand the tile back unshifted so nothing downstream inherits it. */
|
||||
if (tcShiftS > 0 || tcShiftT > 0)
|
||||
gdl = skyPortEmitTileShift(gdl, 0, 0);
|
||||
|
||||
return gdl;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -3227,19 +3227,31 @@ void sub_GAME_7F0B2F00(StandTilePoint** arg0) {
|
||||
|
||||
void stanDetermineEOF(struct StanPrefixRecord *file /* canonically r */, s32 origBase, u8 *newBase)
|
||||
{
|
||||
#ifdef PORT
|
||||
intptr_t delta;
|
||||
#else
|
||||
s32 delta;
|
||||
#endif
|
||||
void **roomPtr;
|
||||
StandTile *tile;
|
||||
u8 *tileSizes;
|
||||
|
||||
#ifdef PORT
|
||||
delta = ((intptr_t) newBase) - origBase;
|
||||
#else
|
||||
delta = ((s32) newBase) - origBase;
|
||||
#endif
|
||||
stan_prefix = file;
|
||||
|
||||
#ifdef DEBUG
|
||||
assert(*r==0);
|
||||
#endif
|
||||
|
||||
#ifdef PORT
|
||||
standTileStart = (StandTile *)(((uintptr_t)file->ptr_firstroom + delta) - 0x80);
|
||||
#else
|
||||
standTileStart = (StandTile *)(((s32)file->ptr_firstroom + delta) - 0x80);
|
||||
#endif
|
||||
ptr_firstroom_0 = (s32)file->ptr_firstroom + delta;
|
||||
|
||||
newBase = list_of_tilesizes;
|
||||
@@ -3249,7 +3261,11 @@ void stanDetermineEOF(struct StanPrefixRecord *file /* canonically r */, s32 ori
|
||||
{
|
||||
do
|
||||
{
|
||||
#ifdef PORT
|
||||
*roomPtr = (void *) ((uintptr_t) (*roomPtr) + delta);
|
||||
#else
|
||||
*roomPtr = (void *) ((s32) (*roomPtr) + delta);
|
||||
#endif
|
||||
roomPtr++;
|
||||
}
|
||||
while (*roomPtr != NULL);
|
||||
@@ -3266,7 +3282,11 @@ void stanDetermineEOF(struct StanPrefixRecord *file /* canonically r */, s32 ori
|
||||
// Fake but required for matching.
|
||||
if (tile->tail.half);
|
||||
|
||||
#ifdef PORT
|
||||
tile = (StandTile *)((uintptr_t)tile
|
||||
#else
|
||||
tile = (StandTile *)((s32)tile
|
||||
#endif
|
||||
+ (tileSizes = newBase)[(tile->tail.half >> 0xc) & 0xf]);
|
||||
}
|
||||
while (*(s32 *) tile != 0);
|
||||
|
||||
+10
-1
@@ -28,6 +28,7 @@
|
||||
|
||||
#ifdef PORT
|
||||
#include <stdlib.h>
|
||||
#include "envflag.h"
|
||||
#endif
|
||||
|
||||
|
||||
@@ -221,7 +222,11 @@ Gfx *sub_GAME_7F007F30(Gfx *gdl, s32 count, Mtxf *matrix)
|
||||
|
||||
if (gunbarrelTimer == BOND_EYE_ANIM_START)
|
||||
{
|
||||
#ifdef PORT
|
||||
modelSetAnimation(chrModelInstance, (struct ModelAnimation *) ((s32) &ANIM_DATA_bond_eye_fire + (uintptr_t) &ptr_animation_table->data), 0, 2.0f, 0.910000026f, 16.0f);
|
||||
#else
|
||||
modelSetAnimation(chrModelInstance, (struct ModelAnimation *) ((s32) &ANIM_DATA_bond_eye_fire + (s32) &ptr_animation_table->data), 0, 2.0f, 0.910000026f, 16.0f);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (gunbarrelTimer == BOND_EYE_ANIM_SPEEDUP)
|
||||
@@ -559,7 +564,11 @@ void initializeGunBarrelIntro(u8 *gfxBuffer, s32 bufferSize)
|
||||
modelSetAnimPlaySpeed(chrModelInstance, S_7F008E80_ANIM_SPEED, 0.0f);
|
||||
#undef S_7F008E80_ANIM_SPEED
|
||||
|
||||
#ifdef PORT
|
||||
animation = (struct ModelAnimation*)((uintptr_t)ptr_animation_table + (s32)&ANIM_DATA_bond_eye_walk);
|
||||
#else
|
||||
animation = (struct ModelAnimation*)((s32)ptr_animation_table + (s32)&ANIM_DATA_bond_eye_walk);
|
||||
#endif
|
||||
startframe = animation->unk04 - 0x44;
|
||||
while (startframe < 0)
|
||||
{
|
||||
@@ -624,7 +633,7 @@ void clearChrGunModelInstances(void)
|
||||
*/
|
||||
Gfx *renderGunbarrelEyeIntroSequence (Gfx *gdl) {
|
||||
#ifdef PORT
|
||||
if (getenv("GE_D63")) {
|
||||
if (GE_ENVFLAG("GE_D63")) {
|
||||
static int n = 0;
|
||||
if ((++n % 200) == 1)
|
||||
osSyncPrintf("D63 gb-render call #%d mode=%d slot=%08x\n",
|
||||
|
||||
@@ -49,7 +49,11 @@ void alCSeqNew(ALCSeq *seq, u8 *ptr)
|
||||
{
|
||||
flagTmp = 1 << i;
|
||||
seq->validTracks |= flagTmp;
|
||||
#ifdef PORT
|
||||
seq->curLoc[i] = (u8*)((uintptr_t)ptr + tmpOff);
|
||||
#else
|
||||
seq->curLoc[i] = (u8*)((u32)ptr + tmpOff);
|
||||
#endif
|
||||
seq->evtDeltaTicks[i] = __readVarLen(seq,i);
|
||||
/*__alCSeqGetTrackEvent(seq,i); prime the event buffers */
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ Acmd *alAdpcmPull(void *filter, s16 *outp, s32 outCount, s32 sampleOffset, Acmd
|
||||
* instead of going silent -- which is what "the door sound loops until
|
||||
* you quit" would actually sound like. Assert it directly, rate-limited.
|
||||
* Remove once root-caused. */
|
||||
if (getenv("GE_PULLTRACE")) {
|
||||
if (GE_ENVFLAG("GE_PULLTRACE")) {
|
||||
static s32 reported = 0;
|
||||
s32 totalSamples = (f->table->len / ADPCMFBYTES) * ADPCMFSIZE;
|
||||
if (f->sample > totalSamples + ADPCMFSIZE && nOver == 0 && reported < 40) {
|
||||
|
||||
+12
@@ -876,7 +876,11 @@ void musicTrack1Play(s32 track)
|
||||
t3 = ALIGN16_a(g_musicTrackLength[g_musicXTrack1CurrentTrackNum]) + ALIGN16_a(NUM_MUSIC_TRACKS);
|
||||
trackSizeBytes = ALIGN16_a(g_musicTrackCompressedLength[g_musicXTrack1CurrentTrackNum]);
|
||||
thing.seqData = g_musicXTrack1SeqData;
|
||||
#ifdef PORT
|
||||
temp_a0 = (u8*)((t3 + (uintptr_t)thing.seqData) - trackSizeBytes);
|
||||
#else
|
||||
temp_a0 = (u8*)((t3 + (s32)thing.seqData) - trackSizeBytes);
|
||||
#endif
|
||||
|
||||
romCopy(temp_a0, romAddress, trackSizeBytes);
|
||||
decompressdata(temp_a0, thing.seqData, &hlist);
|
||||
@@ -1070,7 +1074,11 @@ void musicTrack2Play(s32 track)
|
||||
t3 = ALIGN16_a(g_musicTrackLength[g_musicXTrack2CurrentTrackNum]) + ALIGN16_a(NUM_MUSIC_TRACKS);
|
||||
trackSizeBytes = ALIGN16_a(g_musicTrackCompressedLength[g_musicXTrack2CurrentTrackNum]);
|
||||
thing.seqData = g_musicXTrack2SeqData;
|
||||
#ifdef PORT
|
||||
temp_a0 = (u8*)((t3 + (uintptr_t)thing.seqData) - trackSizeBytes);
|
||||
#else
|
||||
temp_a0 = (u8*)((t3 + (s32)thing.seqData) - trackSizeBytes);
|
||||
#endif
|
||||
|
||||
romCopy(temp_a0, romAddress, trackSizeBytes);
|
||||
decompressdata(temp_a0, thing.seqData, &hlist);
|
||||
@@ -1263,7 +1271,11 @@ void musicTrack3Play(s32 track)
|
||||
t3 = ALIGN16_a(g_musicTrackLength[g_musicXTrack3CurrentTrackNum]) + ALIGN16_a(NUM_MUSIC_TRACKS);
|
||||
trackSizeBytes = ALIGN16_a(g_musicTrackCompressedLength[g_musicXTrack3CurrentTrackNum]);
|
||||
thing.seqData = g_musicXTrack3SeqData;
|
||||
#ifdef PORT
|
||||
temp_a0 = (u8*)((t3 + (uintptr_t)thing.seqData) - trackSizeBytes);
|
||||
#else
|
||||
temp_a0 = (u8*)((t3 + (s32)thing.seqData) - trackSizeBytes);
|
||||
#endif
|
||||
|
||||
romCopy(temp_a0, romAddress, trackSizeBytes);
|
||||
decompressdata(temp_a0, thing.seqData, &hlist);
|
||||
|
||||
@@ -300,7 +300,7 @@ void sndHandleEvent(ALSndPlayer *sndp, ALSndpEvent *event) {
|
||||
/* D202/M-67 diag (temporary): log every event the player processes,
|
||||
* so each VOICE- can be attributed to the exact killing event.
|
||||
* Remove once root-caused. */
|
||||
if (getenv("GE_AUDIOTRACE")) {
|
||||
if (GE_ENVFLAG("GE_AUDIOTRACE")) {
|
||||
extern uint64_t sysGetMicroseconds(void);
|
||||
geTracePrintf("audiotrace.log", "[EVT] t=%llu type=%d state=%p\n",
|
||||
(unsigned long long)sysGetMicroseconds(),
|
||||
@@ -437,7 +437,7 @@ void sndHandleEvent(ALSndPlayer *sndp, ALSndpEvent *event) {
|
||||
alEvtqPostEvent(&sndp->evtq, (ALEvent *) event, DELTA_1_MS + 1);
|
||||
} else {
|
||||
// No lower-priority sound to preempt, so stop the sound.
|
||||
if (getenv("GE_AUDIOTRACE")) { /* D207 diag: SFX actually dropped (pool full, nothing preemptable); remove with the D202 probe set */
|
||||
if (GE_ENVFLAG("GE_AUDIOTRACE")) { /* D207 diag: SFX actually dropped (pool full, nothing preemptable); remove with the D202 probe set */
|
||||
geTracePrintf("audiotrace.log",
|
||||
"[D207-DROP] site=preempt-scan state=%p prio=%d flags=%d count=%d/%d\n",
|
||||
soundState, soundState->priority, soundState->unk3e,
|
||||
@@ -451,7 +451,7 @@ void sndHandleEvent(ALSndPlayer *sndp, ALSndpEvent *event) {
|
||||
// Perhaps it would be better to look for a sound to preempt, just like when the limit is
|
||||
// reached. It's strange that we only check for sounds to preempt when the limit is reached,
|
||||
// but not when it hasn't been.
|
||||
if (getenv("GE_AUDIOTRACE")) { /* D207 diag: dropped on the "limit not reached" path; remove with the D202 probe set */
|
||||
if (GE_ENVFLAG("GE_AUDIOTRACE")) { /* D207 diag: dropped on the "limit not reached" path; remove with the D202 probe set */
|
||||
geTracePrintf("audiotrace.log",
|
||||
"[D207-DROP] site=no-limit state=%p prio=%d flags=%d count=%d/%d\n",
|
||||
soundState, soundState->priority, soundState->unk3e,
|
||||
@@ -472,7 +472,7 @@ void sndHandleEvent(ALSndPlayer *sndp, ALSndpEvent *event) {
|
||||
/* D202/M-65 diag (temporary): pair every voice acquire with
|
||||
* its release to find which sounds hold the 8 voices for
|
||||
* good. Remove once root-caused. */
|
||||
if (getenv("GE_AUDIOTRACE")) {
|
||||
if (GE_ENVFLAG("GE_AUDIOTRACE")) {
|
||||
geTracePrintf("audiotrace.log", "[VOICE+] sound=%p state=%p flags=%d count=%d\n",
|
||||
(void *)sound, (void *)soundState, (int)soundState->unk3e,
|
||||
(int)g_sndAllocatedVoicesCount);
|
||||
@@ -559,7 +559,7 @@ void sndHandleEvent(ALSndPlayer *sndp, ALSndpEvent *event) {
|
||||
/* D202/M-67 diag (temporary): log the release ramp a
|
||||
* STOP/DEACTIVATE computes; delta==0 means the voice
|
||||
* is disposed immediately. Remove once root-caused. */
|
||||
if (getenv("GE_AUDIOTRACE")) {
|
||||
if (GE_ENVFLAG("GE_AUDIOTRACE")) {
|
||||
geTracePrintf("audiotrace.log", "[STOP-EVT] type=%d state=%p playingState=%d deltaUs=%d\n",
|
||||
(int)event->common.type, (void *)soundState,
|
||||
(int)soundState->playingState, (int)delta);
|
||||
@@ -614,7 +614,7 @@ void sndHandleEvent(ALSndPlayer *sndp, ALSndpEvent *event) {
|
||||
case AL_SNDP_VOL_EVT:
|
||||
soundState->vol = event->vol.vol;
|
||||
#ifdef PORT
|
||||
if (getenv("GE_AUDIOTRACE")) { /* D202 diag: distance-vol value per VOL event; remove with the other probes */
|
||||
if (GE_ENVFLAG("GE_AUDIOTRACE")) { /* D202 diag: distance-vol value per VOL event; remove with the other probes */
|
||||
extern uint64_t sysGetMicroseconds(void);
|
||||
geTracePrintf("audiotrace.log", "[VOL] t=%llu state=%p rawVol=%d playing=%d\n",
|
||||
(unsigned long long)sysGetMicroseconds(), (void *)soundState,
|
||||
@@ -944,7 +944,7 @@ void sndUnlinkClearSound(ALSoundState *state)
|
||||
{
|
||||
g_sndAllocatedVoicesCount--;
|
||||
#ifdef PORT
|
||||
if (getenv("GE_AUDIOTRACE")) { /* D202/M-65 diag; remove once root-caused */
|
||||
if (GE_ENVFLAG("GE_AUDIOTRACE")) { /* D202/M-65 diag; remove once root-caused */
|
||||
geTracePrintf("audiotrace.log", "[VOICE-] sound=%p state=%p flags=%d count=%d\n",
|
||||
(void *)state->sound, (void *)state, (int)state->unk3e,
|
||||
(int)g_sndAllocatedVoicesCount);
|
||||
@@ -1082,7 +1082,7 @@ ALSoundState *sndPlaySfx(struct ALBankAlt_s *soundBank, s16 soundIndex, ALSoundS
|
||||
/* D202 diag probe (temporary): trace which soundIndex resolves to
|
||||
* which ALSound*, to root-cause the M-52 "wrong sample plays"
|
||||
* playtest report. Remove once root-caused. */
|
||||
if (getenv("GE_AUDIOTRACE")) {
|
||||
if (GE_ENVFLAG("GE_AUDIOTRACE")) {
|
||||
extern unsigned long long audioDumpBytePos(void);
|
||||
geTracePrintf("audiotrace.log", "[AUDIOTRACE] dumppos=%llu sndPlaySfx: bank=%p soundIndex=%d -> sound=%p keyMap=%p wavetable=%p base=%p len=%d type=%d flags=%d book=%p\n",
|
||||
(unsigned long long)audioDumpBytePos(),
|
||||
@@ -1120,7 +1120,7 @@ ALSoundState *sndPlaySfx(struct ALBankAlt_s *soundBank, s16 soundIndex, ALSoundS
|
||||
/* D202 diag probe (temporary): log the ALSoundState* handed back per
|
||||
* soundIndex so a later sndDeactivate trace can be correlated back
|
||||
* to "which chain link was this". Remove once root-caused. */
|
||||
if (getenv("GE_AUDIOTRACE")) {
|
||||
if (GE_ENVFLAG("GE_AUDIOTRACE")) {
|
||||
ALKeyMap *kmp = sound ? sound->keyMap : NULL;
|
||||
/* D202/M-65: the retrigger machinery is what the user's three
|
||||
* symptoms all route through, and none of it was traced before.
|
||||
@@ -1256,7 +1256,7 @@ ALSoundState *sndPlaySfx(struct ALBankAlt_s *soundBank, s16 soundIndex, ALSoundS
|
||||
* forever, until sndDeactivate clears bit 0x10. A stuck looping
|
||||
* sound IS this event never stopping; a "piling up" mix is this
|
||||
* period being far too short. Remove once root-caused. */
|
||||
if (getenv("GE_AUDIOTRACE")) {
|
||||
if (GE_ENVFLAG("GE_AUDIOTRACE")) {
|
||||
geTracePrintf("audiotrace.log", "[AUDIOTRACE] RETRIGGER-POST: state=%p soundIndex=%d delayUs=%d (%.1f ms)\n",
|
||||
(void *)nextState, (int)eventSoundIndex, (int)playSfxDelta,
|
||||
(double)playSfxDelta / 1000.0);
|
||||
@@ -1275,7 +1275,7 @@ ALSoundState *sndPlaySfx(struct ALBankAlt_s *soundBank, s16 soundIndex, ALSoundS
|
||||
* write IS how a looping SFX gets an owner that can later stop it.
|
||||
* Log it so "slot never written" can be told apart from "slot
|
||||
* written then cleared". Remove once root-caused. */
|
||||
if (getenv("GE_AUDIOTRACE")) {
|
||||
if (GE_ENVFLAG("GE_AUDIOTRACE")) {
|
||||
geTracePrintf("audiotrace.log", "[SLOTWRITE] slot=%p <- state=%p (was %p)\n",
|
||||
(void *)pendingState, (void *)nextState,
|
||||
(void *)pendingState->link.next);
|
||||
@@ -1303,7 +1303,7 @@ void sndDeactivate(ALSoundState *state)
|
||||
/* D202 diag probe (temporary): does this ever fire for the stuck
|
||||
* door-loop voice? Correlate against the sndPlaySfx newState= trace.
|
||||
* Remove once root-caused. */
|
||||
if (getenv("GE_AUDIOTRACE")) {
|
||||
if (GE_ENVFLAG("GE_AUDIOTRACE")) {
|
||||
geTracePrintf("audiotrace.log", "[AUDIOTRACE] sndDeactivate: state=%p (%s)\n",
|
||||
(void *)state, state ? "non-null" : "NULL-noop");
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ investigation artifacts.
|
||||
| `disasm.py` | Minimal MIPS disassembler for the BE ROM (RAM-addr → file offset). |
|
||||
| `ppm2bmp.py` | PPM → 24-bit BMP, no deps — eyeball `GE_PCDUMP` frames without PIL. |
|
||||
| `gen_findings_index.py` | Regenerate `docs/dev/findings-index.csv` (grep-before-you-read aid for the 200 KB finding log). `--check` in CI-style use. |
|
||||
| `docs_budget.py` | Context cost (approx tokens) of the docs every agent session loads, against per-file and tier-1 budgets; exits 1 when over. `--toc FILE` prints a heading TOC, `--json` for scripting. |
|
||||
| `gen_env_probes.py` | Drift check for `docs/dev/GE-ENV-PROBES.md` — re-greps live `getenv("GE_*")` sites, reports NEW/GONE. |
|
||||
|
||||
## Living — compiler-verified layout probes (kept: re-run when structs change)
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Report the context cost of the docs that every agent session loads.
|
||||
|
||||
Usage:
|
||||
python tools_pc/docs_budget.py # table of per-file budget rows + tier-1 total
|
||||
python tools_pc/docs_budget.py --json # same data as JSON
|
||||
python tools_pc/docs_budget.py --toc FILE # markdown TOC (## / ### headings) for FILE
|
||||
|
||||
For each tracked doc it prints path, bytes, lines, approx_tokens (bytes // 4),
|
||||
the per-file budget in approx tokens, and OK/OVER. Missing files are reported
|
||||
as "missing" and are not an error (docs/HANDOFF.md is optional). It then sums
|
||||
the "tier 1" set (CLAUDE.md, AGENTS.md, docs/HANDOFF.md, docs/ROADMAP.md)
|
||||
against TIER1_BUDGET. Exit code is 1 if any file or the tier-1 total is OVER,
|
||||
else 0.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
# Approximate-token budgets for each tracked doc.
|
||||
BUDGETS = {
|
||||
"CLAUDE.md": 4000,
|
||||
"AGENTS.md": 6000,
|
||||
"docs/HANDOFF.md": 3000,
|
||||
"docs/ROADMAP.md": 12000,
|
||||
"docs/porting-notes.md": 60000,
|
||||
"docs/dev/findings.md": 600000,
|
||||
"docs/dev/findings-index.csv": 40000,
|
||||
}
|
||||
|
||||
# Files whose approx tokens are summed for the tier-1 total.
|
||||
TIER1_FILES = ["CLAUDE.md", "AGENTS.md", "docs/HANDOFF.md", "docs/ROADMAP.md"]
|
||||
TIER1_BUDGET = 25000
|
||||
|
||||
|
||||
def repo_root():
|
||||
"""Repo root = parent dir of the script's directory."""
|
||||
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def read_bytes(path):
|
||||
"""Return (bytes, lines) for a file read as UTF-8 with errors='replace',
|
||||
or None if the file is missing."""
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
||||
text = f.read()
|
||||
except OSError:
|
||||
return None
|
||||
return len(text.encode("utf-8")), len(text.splitlines())
|
||||
|
||||
|
||||
def collect_rows():
|
||||
"""Build the list of row dicts for every tracked doc."""
|
||||
root = repo_root()
|
||||
rows = []
|
||||
for rel in BUDGETS:
|
||||
full = os.path.join(root, *rel.split("/"))
|
||||
data = read_bytes(full)
|
||||
if data is None:
|
||||
rows.append({
|
||||
"path": rel,
|
||||
"bytes": None,
|
||||
"lines": None,
|
||||
"approx_tokens": None,
|
||||
"budget_tokens": BUDGETS[rel],
|
||||
"status": "missing",
|
||||
})
|
||||
else:
|
||||
nbytes, nlines = data
|
||||
tokens = nbytes // 4
|
||||
budget = BUDGETS[rel]
|
||||
rows.append({
|
||||
"path": rel,
|
||||
"bytes": nbytes,
|
||||
"lines": nlines,
|
||||
"approx_tokens": tokens,
|
||||
"budget_tokens": budget,
|
||||
"status": "OK" if tokens <= budget else "OVER",
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def tier1_total(rows):
|
||||
total = 0
|
||||
for row in rows:
|
||||
if row["path"] in TIER1_FILES and row["approx_tokens"] is not None:
|
||||
total += row["approx_tokens"]
|
||||
return total
|
||||
|
||||
|
||||
def print_table(rows, t1):
|
||||
header = f"{'path':<28} {'bytes':>10} {'lines':>8} {'tokens':>9} {'budget':>9} status"
|
||||
print(header)
|
||||
for row in rows:
|
||||
if row["status"] == "missing":
|
||||
print(f"{row['path']:<28} missing")
|
||||
else:
|
||||
print(
|
||||
f"{row['path']:<28} {row['bytes']:>10} {row['lines']:>8} "
|
||||
f"{row['approx_tokens']:>9} {row['budget_tokens']:>9} {row['status']}"
|
||||
)
|
||||
t1_status = "OK" if t1 <= TIER1_BUDGET else "OVER"
|
||||
print(f"{'TIER1 TOTAL':<28} {'':>10} {'':>8} {t1:>9} {TIER1_BUDGET:>9} {t1_status}")
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Report the context cost of agent-session docs and fail when over budget."
|
||||
)
|
||||
parser.add_argument("--toc", metavar="FILE", help="print a markdown TOC for FILE")
|
||||
parser.add_argument("--json", action="store_true", help="print JSON instead of the table")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.toc:
|
||||
return print_toc(args.toc)
|
||||
|
||||
rows = collect_rows()
|
||||
t1 = tier1_total(rows)
|
||||
|
||||
if args.json:
|
||||
payload = {
|
||||
"rows": rows,
|
||||
"tier1": {"total_tokens": t1, "budget_tokens": TIER1_BUDGET,
|
||||
"status": "OK" if t1 <= TIER1_BUDGET else "OVER"},
|
||||
}
|
||||
print(json.dumps(payload, indent=2))
|
||||
else:
|
||||
print_table(rows, t1)
|
||||
|
||||
over = any(r["status"] == "OVER" for r in rows) or t1 > TIER1_BUDGET
|
||||
return 1 if over else 0
|
||||
|
||||
|
||||
def github_anchor(heading):
|
||||
"""GitHub-style anchor: lowercase, strip non-alnum/space/hyphen, spaces -> hyphens."""
|
||||
text = heading.lower()
|
||||
text = re.sub(r"[^a-z0-9 \-]", "", text)
|
||||
return text.replace(" ", "-")
|
||||
|
||||
|
||||
def print_toc(path):
|
||||
"""Print a markdown TOC of '## ' / '### ' headings, skipping fenced code blocks."""
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
||||
lines = f.read().splitlines()
|
||||
except OSError as e:
|
||||
print(f"error: cannot read {path}: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
in_fence = False
|
||||
for line in lines:
|
||||
if line.lstrip().startswith("```"):
|
||||
in_fence = not in_fence
|
||||
continue
|
||||
if in_fence:
|
||||
continue
|
||||
if line.startswith("### "):
|
||||
heading = line[4:].strip()
|
||||
print(f" - [{heading}](#{github_anchor(heading)})")
|
||||
elif line.startswith("## "):
|
||||
heading = line[3:].strip()
|
||||
print(f"- [{heading}](#{github_anchor(heading)})")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Headings contain non-ASCII (arrows, em dashes); a cp1252 console can't print them.
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.exit(main())
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""D236 pass 19 (TEMP): count op-12 (GunfireRecord / dogfnegx billboard) and
|
||||
op-13 (ShadowRecord / doshadow) nodes per model file, and dump op-12 record
|
||||
fields (Offset/Size/Image) for files that contain them. Remove once D236 concludes."""
|
||||
import struct, sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.argv = [sys.argv[0], "ntsc-final"] if len(sys.argv) < 2 else sys.argv
|
||||
import d43_emit as m
|
||||
|
||||
def plaus_vma(v):
|
||||
return v == 0 or (v >> 24) == 5
|
||||
|
||||
counts12, counts13 = {}, {}
|
||||
for name in m.model_names:
|
||||
key, row = m.find_row(name)
|
||||
if not key:
|
||||
continue
|
||||
addr, size = row
|
||||
src = __import__("zlib").decompress(m.rom[addr:addr + size][2:], -15)
|
||||
D = len(src)
|
||||
hkey = None
|
||||
for cand in (key, key[1:], key[:-1], key[1:-1]):
|
||||
if cand in m.nsnt:
|
||||
hkey = cand
|
||||
break
|
||||
if not hkey:
|
||||
continue
|
||||
NS, NT = m.nsnt[hkey]
|
||||
nodes, R0 = m.build_nodes(src, D, NS, NT)
|
||||
if not nodes:
|
||||
continue
|
||||
placed = m.placement_order(nodes, src, R0)
|
||||
n12 = n13 = 0
|
||||
for (no, op) in placed:
|
||||
if op == 12:
|
||||
n12 += 1
|
||||
d = m.be32o(src, no + 4)
|
||||
raw = src[d:d + 0x28]
|
||||
ox, oy, oz = struct.unpack_from(">fff", raw, 0)
|
||||
sx, sy, sz = struct.unpack_from(">fff", raw, 12)
|
||||
img = struct.unpack_from(">I", raw, 24)[0] & 0xFFFFFF
|
||||
print(f"{name}: op12 node={no:#x} rec={d:#x} offset=({ox:.1f},{oy:.1f},{oz:.1f}) size=({sx:.1f},{sy:.1f},{sz:.1f}) img={img:#08x} vma?{plaus_vma(img)}")
|
||||
elif op == 13:
|
||||
n13 += 1
|
||||
if n12 or n13:
|
||||
print(f"== {name}: op12={n12} op13={n13}")
|
||||
counts12[name] = n12
|
||||
counts13[name] = n13
|
||||
|
||||
print(f"\ntotal files with op12: {len(counts12)}, total op12 nodes: {sum(counts12.values())}")
|
||||
print(f"total files with op13: {len(counts13)}, total op13 nodes: {sum(counts13.values())}")
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""D236 pass 17c (TEMP): dump every op-13 (ShadowRecord) record's raw bytes
|
||||
from the ROM model files, to settle the field-layout question (does the ROM
|
||||
put HeaderNode at +0x14 as the C struct says, or +0x18 as d43_emit.py's emit
|
||||
assumes?). Prints per file: node offset, record offset, and the 16 bytes at
|
||||
d+0x10..d+0x20 with both interpretations annotated. Remove once D236 concludes."""
|
||||
import struct, sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.argv = [sys.argv[0], "ntsc-final"] if len(sys.argv) < 2 else sys.argv
|
||||
import d43_emit as m
|
||||
|
||||
def plaus_vma(v):
|
||||
# file-relative seg-5 vma: 0x05xxxxxx, or null
|
||||
return v == 0 or (v >> 24) == 5
|
||||
|
||||
def plaus_f32(b):
|
||||
f = struct.unpack_from(">f", b)[0]
|
||||
return abs(f) < 1e6 and f == f # finite, sane magnitude
|
||||
|
||||
n_found = 0
|
||||
for name in m.model_names:
|
||||
key, row = m.find_row(name)
|
||||
if not key:
|
||||
continue
|
||||
addr, size = row
|
||||
src = __import__("zlib").decompress(m.rom[addr:addr + size][2:], -15)
|
||||
D = len(src)
|
||||
hkey = None
|
||||
for cand in (key, key[1:], key[:-1], key[1:-1]):
|
||||
if cand in m.nsnt:
|
||||
hkey = cand
|
||||
break
|
||||
if not hkey:
|
||||
continue
|
||||
NS, NT = m.nsnt[hkey]
|
||||
nodes, R0 = m.build_nodes(src, D, NS, NT)
|
||||
if not nodes:
|
||||
continue
|
||||
placed = m.placement_order(nodes, src, R0)
|
||||
for (no, op) in placed:
|
||||
if op != 13:
|
||||
continue
|
||||
n_found += 1
|
||||
d = m.be32o(src, no + 4)
|
||||
raw = src[d + 0x10:d + 0x20]
|
||||
v_10 = struct.unpack_from(">I", raw, 0)[0] & 0xFFFFFF
|
||||
v_14 = struct.unpack_from(">I", raw, 4)[0] & 0xFFFFFF
|
||||
v_18 = struct.unpack_from(">I", raw, 8)[0] & 0xFFFFFF
|
||||
v_1c = struct.unpack_from(">I", raw, 12)[0] & 0xFFFFFF
|
||||
f_18 = struct.unpack_from(">f", raw, 8)[0]
|
||||
f_1c = struct.unpack_from(">f", raw, 12)[0]
|
||||
print(f"{name}: node={no:#x} rec={d:#x}")
|
||||
print(f" +0x10 image = {v_10:#08x} vma?{plaus_vma(v_10)}")
|
||||
print(f" +0x14 (A:Header) = {v_14:#08x} vma?{plaus_vma(v_14)}")
|
||||
print(f" +0x18 (B:Header) = {v_18:#08x} vma?{plaus_vma(v_18)} as f32={f_18!r}")
|
||||
print(f" +0x1C (A:Scale) = as f32={f_1c!r} raw={v_1c:#08x} vma?{plaus_vma(v_1c)}")
|
||||
print(f"\ntotal op13 records: {n_found}")
|
||||
Reference in new issue
Block a user