Fixes:
- D565: AI awareness no longer follows two player render settings. Fog
distance is floored at 100 (below it the shortened clip hid guards from
AI on-screen checks); the D466 gameplay fog cull is rescaled to the game's
own FOV (identity at defaults).
- D566: Steam Deck Game Mode no longer shows the on-screen keyboard at
launch (SDL text input stopped; screen-keyboard hint off).
- D567: opening PC Options from the front end with a pad no longer drags
the game's crosshair to the top-left corner.
Docs and site:
- README News/Status/Features/controls brought up to date; the unpublished
v0.4.1 work is folded into v0.5.0 everywhere; stats to v0.5.0.
- Release notes reordered: player-facing highlights first, finding labels
and verification at the bottom.
- Site status rewritten; favicon is now the exe icon (make_icon.py writes
docs/favicon.ico).
- ROADMAP: AI review results, update-check follow-up, stale rows closed.
Verified: Windows + Linux builds; Windows pixel gate Surface 1 PASS;
Deck build deployed for the maintainer's Game Mode checks.
Deck/Windows playtest fixes: fog rework (D540) and RSP-style CPU near-plane
clip with per-vertex N64 fog (D543), with fog recomputed on near-plane-clipped vertices as the RSP does,
so the haze around the player stays steady (D553); colour-combiner clamp for banded walls
(D548); rocket and thrown-item stack overflows (D545, D549); menu
double-trigger mitigation (D541); Dam ending first-shot camera swivel
restored by removing an old port look-at workaround (D552); settings standardisation (D546), live
All unlocked (D547), F10 mouse (D544); static window title and the project
mark (the in-game crosshair shape, site gold) as window/taskbar icon and
favicon (D550). Reference frames re-based
on Windows, Linux and Steam Deck (A/B-confirmed); capture_p7.sh runs on
Windows. Opt-in update check, off by default (D551: one HTTPS request to
GitHub per launch when enabled; winhttp.dll loaded only then; the tag CI
job checks the tag against GE007_VERSION). Release notes, ROADMAP,
internals and security docs updated; tree-hygiene pass.
Refs: D540 D541 D543 D544 D545 D546 D547 D548 D549 D550 D551 D552 D553 D554 D555 D556 D557 D558 D559 D560 D561 D562 D563 D564
Public-copy language pass, homepage restructure and Documentation rename,
404 page, dead-anchor and permalink fixes, release-gate sign-off, and the
pre-release review fixes of the 2026-10-05 batch (probe-doc drift,
build-path hygiene, verify.sh and capture safety).
Refs: D533 D534
Linux reference-frame set for all 21 levels and the cross-platform golden
rule (D521); golden tooling made non-destructive (D524); the gate pins a
canonical in-tree save (D529) and the PRNG seed (D532); the capture tool's
per-level re-pin bug fixed and the Linux set re-captured under the
canonical save (D530, D531); no-arg verify.sh exits non-zero. README and
docs visual/consolidation passes; the 15 MB preview gif replaced by stills.
Refs: D521 D523 D524 D525 D526 D527 D528 D529 D530 D531 D532
PD-style F10 menu with six groups, checkbox/slider/dropdown controls (D504),
wording pass and real-unit sliders (D505-D509), overlay/FPS-counter window
scaling (D510, D512), small settings (D511), control scheme Ext/Original
(D513) with the game's own N64 styles (D516-D518), emulator-format save
import on first launch (D514), build arch tag (D515), converter argv quoting
and sidecar size check, one PC options UI (D519) with by-eye follow-ups,
antialiased overlay sliders (D520), README settings paths, ROADMAP trim,
D441 tier-1 all-level sweep (21/21 pass).
Refs: D503-D520 D349 D441
Virtual seats, per-seat camera and input routing, aim modes, hot-plugged
controllers reopening via a coalesced rescan (D450, dolent's PR #122,
merged as the original commit). Rule-2
sign-off for D417.
Refs: #99 D416 D417 D418 D419 D422 D428 D448 D450 #122
Quit (quit event, window close, Alt+F4, from either SDL pump) used to
exit(0) immediately while the render thread could be mid-frame in the
driver: the likely trigger of the host's 0x119 bugchecks. Now quits are
requested; the render thread glFinish()es, unbinds the context and parks
at a frame boundary, and the host exits after (max 2 s). GE_QUITFRAME=<n>
gives the harness a clean self-quit; golden_gate.sh uses it.
The op-22 DisplayListPrimaryRecord count is the number of star-flash
arms; dorottex (and PD's modelRenderNodeStarGunfire) read count*4
vertices. d43_emit/d43_convert sized the array as count vertices, so
3/4 of every muzzle star came from neighbouring sidecar bytes and drew
as long spikes (M16 "shooting up"). Size it 16*4*count. Emitter
round-trip checks all pass; M16 renders a clean star headless.
Also TEMP GE_STARTWEAPON (bondview_r.c) for weapon repros. Re-run
tools_pc/d43_emit.py to regenerate data/pcmodels-*.
A no-game SDL drain monitor (new tools_pc/sdl_drain_monitor.c, opens SDL
exactly like port/src/audio.c) shows the maintainer's Focusrite USB interface
draining at 88-95% for ~15 s every ~312 s, matching the in-game overflow
bursts' period. Level loads, combat, the SDL pull pattern and a
pacemaker-vs-vsync beat were ruled out. Remaining D322 items: the 8-voice SFX
cap (D207/D241) and #87's progressive decay. porting-notes: measure the host
device before chasing periodic audio glitches.
Pins data/ge007.ini to a defaults-only 640x480 window (restored on any exit),
captures Bunker 1 at 640-1120:240 and runs framediff.py against the golden
baseline. --level/--dump give a capture-only smoke run (exit 0 = frames
captured and no crash). Full framediff output goes to scratch/.
The 2026-09-09 set was captured at 30 fps (2 sim ticks/frame). Since D248
the port renders at 60 fps (1 tick/frame), so frames 200/320/440 landed in
the intro cutscene (nondeterministic camera, D117) and the golden failed on
every build, with or without D329. Moved the window to 400-880:240 (the same
sim ticks the original set sampled, now in settled gameplay); two independent
native-Windows captures agree 3/3.
verify.sh's ini pin rewrote only Width/Height, which kept personal
FovScale/DrawDistance/MSAA settings and failed the gate. It now writes a
defaults-only 640x480 ini (and pins/removes one when none exists).
GE's texLoadFromGdl picks a handler per G_NOOP texture reference from w0 & 7.
TEXTURETYPE_LOD/MIPMAP (texHandleType0/Type2) issue ONE load block at TMEM 0,
so tiles 0..n are LOD levels of that one image. TEXTURETYPE_DETAIL
(texHandleType1, src/game/tex.c) issues TWO: the detail texture at TMEM 0 and
the real base image at TMEM texGetSizeInBytes(detail,0) on tiles 1+.
gfx_lod_tile_offset folded every G_TL_LOD draw to tile 0 (D107 -- correct for
the first two, wrong for DETAIL), so the port drew the detail texture and never
imported the base image at all. All four Surface 1 treeline cards
(tex1198-tex1201, RGBA5551 64x17 cut-outs) are DETAIL bindings sharing one
detail texture, tex2465: an opaque 32x32 IA8 noise tile. That is the same
texture D267/D276 measured at 1023/1024 texels opaque and called the mystery
"noise painter" -- hence 20+ passes chasing decode, alpha, mip and geometry
theories for what was a binding bug.
Fix (port layer only): under G_TL_LOD, if tile fi+1 differs from tile fi in
BOTH TMEM address and fmt/siz, sample tile 1. Asymmetric on purpose -- LOD
levels of one image always share a format, so a mip chain can never trip it,
and a detail pair that happened to share a format just keeps the old
behaviour. Config Video.DetailBaseTile, default on.
Verified: Surface 1 idle spawn goes from an opaque tiled noise wall to discrete
snow-covered pines with ground/snow unchanged; Surface 2 shows a discrete night
treeline; Depot renders clean; bunker1 golden framediff is bit-identical with
the fix on and off. Offline scan of every level's bg: DETAIL bindings exist
only in sevx (84), depo (39), sevb (4), sev (4), so every other level is
provably untouched. User play-test confirmed.
Also refuted and removed pass 25's fix direction: cut-out mip suppression
changed nothing, and its alpha census showed the wall is painted by a fully
opaque texture -- retiring the mip / 1-bit-alpha / XLU-smear branch. The
GE_D236P25 probe is gone with it.
tools_pc/gen_env_probes.py: stop counting a "GE_FOO[" log-line TAG in the doc
as a documented env var. GE_TEXDUMP_ADDR prints "GE_TEXA[...]", which is not
the name of any getenv(), so the check reported a permanent phantom GONE.
Generalised as docs/porting-notes.md D12; full trail in findings.md D236 pass
26. Carries the pending D75/M-197 sidecar-converter fix and the env-gated
D236/D75 probes, which the doc gates require to land alongside their rows.
Per the 2026-09-19 adversarial pre-ship review (findings.md M-192):
- d318watchdog.c: one-shot per OCCURRENCE, not per process -- re-arm after
600 ticks of signature absence so a Facility replay in the same session
(AllUnlocked) is still covered.
- model.c: RULE-2-SIGNOFF records for the retained M-183/M-185 and D311
defensive clamps (user-approved retention as the stability-maximizing
option; stale 'Threshold: 50.0' comment corrected to 10.0f; both marked
post-release removal candidates pending full cutscene re-verification).
- reference/mouse-injector/: removed from the public repo (GPLv2 source +
prebuilt discord-rpc.lib in an MIT project; never compiled here).
Provenance stub README remains; path gitignored for local re-vendoring.
AGENTS.md / GEPD-INPUT-PLAN.md / optionsoverlay.c refs updated.
- scratch/: untracked + gitignored (19 session logs/probe sources were
committed in the release window).
- findings-index.csv: D317 (and 5 other stale rows) now CLOSED -- added
CLOSED to gen_findings_index.py's verdict vocabulary.
- docs: D310 perf gauge (no change warranted), GE_DEBUG_UNLOCKALL noted in
security-and-fidelity-status.md, release notes gain the missing D318
watchdog bullet + FOV-clamp clarification.
Build verified: ./build-pc.sh ntsc-final clean; gen_findings_index.py
--check and gen_env_probes.py --check pass.
v0.3.0 silently wiped all v0.2.x saves on first launch: stored slot CRCs
are fingerprints of the PRNG that wrote them, and D284 changed the PRNG.
One-time migration in the eeprom shim's read path (block 4 only): a slot
that fails the current-CRC check but validates under the legacy PRNG is
re-stamped with current-PRNG checksums before fileValidateSaves sees it,
then persisted once. Runs before geEepromPatchAllCheats so cheat patches
never reach the file. Corrupt/fresh slots untouched (game's own wipe path).
- port/src/legacycrc.c: isolated file carrying the deliberately-wrong
pre-D284 PRNG (verbatim from 0de99b4d^, reviewer-verified bit-exact) +
lockstep copy of the crc.c loop; header says when it may be deleted.
- tools_pc/d295_crc.py: reference CRC model, bit-exact vs compiled C for
both PRNGs (used to verify migrated files).
Verified: user's #87 artifact migrates 5/5 slots with progress intact and
is idempotent on relaunch; native post-fix saves, corrupt slots, and fresh
eeps all behave per design; 21/21 sweep PASS.
- Drop-in ROM wording everywhere (README quick start, Pages landing,
release notes, bundled README): first launch auto-generates the
derived assets via the frozen converter; no Python/toolchain needed.
PAL/JP marked not-yet-supported with issue #85 pointers.
- building.md: Linux build is CI-compiled and ships in the bundle
(was 'never built or run on Linux'); bundle note now describes the
frozen ge007-convert auto-spawn; PAL/JP sidecar caveat (D258).
- Status rewording across README/Pages/release notes: drop the
'first open-source port to our knowledge' claim; cutscene glitch
description aligned to 'mostly with James Bond'; index.md status
no longer contradicts the open D255 Deck crash.
- Em-dash cleanup across all GitHub Pages docs (index, building,
internals, porting-notes, dev-process, agentic-development,
release notes); internal dev logs untouched.
* Drop-in ROM: first-run sidecar conversion, no Python (issue #6, Part A + interim)
The release bundle now turns a dropped-in NTSC-U ROM into a playable game
with zero tooling: when data/pcmodels-* / data/pccg-* are missing at boot,
the engine spawns the frozen prepare-assets converter shipped in the bundle
(prepare-assets/ge007-convert[.exe], PyInstaller --onefile), waits for it,
and re-checks before continuing (port/src/romconvert.c, called from
romdataInit after ROM validation). Failure aborts boot with a message
instead of the later D179-class crash.
- prepare-assets.py: runs in-process via runpy when frozen (no user Python
to exec); NTSC-U-only gate with a clear message for PAL/JP ROMs; d88's
sibling import needs an explicit sys.path entry (run_path does not set
one). Verified: frozen exe output byte-identical to the unfrozen path and
to the shipped sidecars.
- CI (linux + windows): PyInstaller freeze step before bundling
(ensurepip / --break-system-packages for MSYS2's pip-less python).
- bundle scripts: copy ge007-convert into prepare-assets/ (hard-fail if not
frozen), size guard 60 -> 120 MB.
- Release notes: US-only drop-in instructions, no Python required.
- D258: PAL/JP filelist naming defects documented; multi-region deferred to
the Part B follow-up.
* CI: install pip via pacman on Windows (MSYS2 ensurepip is disabled); regen findings index for D258
- README: manual Contents TOC dropped (GitHub auto-generates); Download
table moved to the top with Quick start under it; Status compressed to
a Working paragraph + tight known-issue list (D-numbers live in the
release notes / finding log now); How-it-works tightened.
- Pages (docs/index.md): demo gif is the hero; download table up top;
new How-it-works and Honest-status sections; legal footer; the 19-shot
playtest gallery unchanged.
- Options overlay is now gamepad-driven: README Steam Deck section, the
bundle README (new @DECK@ block on Linux, controller column added to
the controls table), and the release-notes template all document
Select / D-pad / A / B / Start instead of a Steam key-mapping workaround.
The $ORIGIN verification compared ldd's absolute resolved path against the
relative $OUT, so it always failed even though the bundled libSDL2 was the
copy being resolved (CI 34778363397 showed exactly that). Match both forms;
the remaining-deps listing now filters by bundle dir name for the same
reason.
bundle-linux.sh now copies the exact libSDL2 the exe was linked against next
to it and sets the rpath to $ORIGIN via patchelf, with an ldd assertion that
the bundled copy is the one resolved. zlib/libGL remain system deps
(preinstalled on desktop distros and SteamOS). CI installs patchelf; the
generated in-bundle README gains a Steam Deck sideload note and the SDL2
zlib-license copyright file is carried when the distro ships one.
Also refreshes the release-facing docs that ship with / describe the cut:
- .github/release-notes.md: v0.1.0-alpha body -> <version> pre-release body
(what's new + known issues incl. D243/D230/D245/D246/D197/D75, the D251
4K ghost row, and 'not yet verified on real Deck hardware')
- tools_pc/dist/README.md.in: alpha claims (no audio / black sky / US-only)
-> pre-release status; ROM table now lists all three regions (the asset
step auto-detects by SHA-1)
* D250: fix sysSleep unit-conversion bug in frame-pacing; add GE_D250 lvlRender probe
port/fast3d/gfx_sdl2.cpp sync_framerate_with_timer() computed the
remaining frame budget in 100ns units (qpc_to_100ns) and passed it
straight into sysSleep(), which takes microseconds -- a real 10x
oversleep whenever that branch fires. Genuine bug, but only active
when target_fps is nonzero (tied to Input/Video FpsCap); confirmed via
live testing that it is NOT the active cause of the user's reported
~45-50fps with FpsCap=0/VSync=0, where this pacing path never runs.
Added GE_D250 (src/boss.c, #ifdef PORT) to isolate lvlRender()'s
DL-building cost (culling/animation/scene traversal) from gfx_run's
already-measured software-RSP interpretation cost. Live test on Dam
shows lvlRender averaging ~0.5-1.5ms -- also not the bottleneck.
Documented in docs/dev/GE-ENV-PROBES.md (GE_D250 row). findings.md
D250 update to follow after rebase.
Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog
* docs(D250): live A/B session -- MSAA/aniso/FOV/resolution/vsync all ruled out
Live session with the user driving the game directly, confirmed the
sim-healthy/render-sagging pattern from earlier idle testing reproduces
under real play on Dam. One-at-a-time A/B via the F10 overlay ruled out
MSAA, anisotropy, FOV, resolution (1280x960 fullscreen vs 640x480
windowed), and VSync (render never exceeds ~60 even uncapped) as the
cause -- the cost is resolution/GPU-setting-independent.
gfx_run (~6-10ms) + lvlRender (~0.5-1.5ms, via the new GE_D250 probe)
together don't account for the real ~20-22ms/frame. Leading remaining
suspect: the emulated N64 thread-handoff/scheduler round-trip
(gesched.c/sched.c) -- real OS context-switch/wake latency standing in
for what was free hardware handoff on N64. Needs a real profiler
(Visual Studio / WPR) attached during a live bad-fps moment to confirm;
ad-hoc per-call timers have now ruled out the two most obvious CPU call
sites without finding the cost.
Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog
* D250: fix uncached getenv() in fast3d hot paths -- root cause of the fps drop
A live WPR/xperf CPU trace (tools_pc/perf.ps1, new sister script to
debug.ps1) with Microsoft public symbols resolved found the dominant
render thread spending only 5.2% of its CPU time in the game's own
code -- 43% was in msvcrt.dll, and getenv()/_getenv_helper_nolock
alone accounted for ~50% of the thread's total CPU time.
Root cause: port/fast3d/gfx_pc.cpp's import_texture() (runs on every
texture bind) and several per-triangle/per-G_SETCOMBINE debug-probe
checks (GE_DTEX, GE_TEXDUMP, GE_TEXRAW, GE_D229 x2, GE_D172 x2, plus
GE_TEXDUMP/GE_PCDUMP in gfx_opengl.cpp) called getenv() uncached on
every call, unlike the safe static-cached pattern already used
elsewhere in this codebase. On a texture-heavy scene that's tens of
thousands of locked, locale-aware Windows CRT environment lookups per
second -- invisible to both of the timers added earlier this session
(gfx_run's own cost measurement and the new GE_D250 lvlRender probe),
since neither isolates CRT/library time from the game's own code.
Fix: cache every getenv() site in gfx_pc.cpp/gfx_opengl.cpp with the
established static-int pattern. Narrow, mechanical, no logic change.
Verified: the exact idle-Dam test that was pinned at 45-50fps before
the fix now holds a rock-steady 60-61fps, zero dips. Also explains why
the earlier MSAA/aniso/FOV/resolution/vsync A/B pass all showed "no
change" -- the getenv() cost was a large per-call constant that
swamped any real, smaller resolution-dependent GPU cost at every
setting tried.
Residual, separate issue found once this was fixed: still runs badly
at 1440p+ (640x480 now holds a clean 60fps) -- likely the real GPU
fill-rate cost the getenv() bug was masking, now exposed on its own.
Not yet investigated.
docs/dev/findings.md D250 updated with the full root-cause writeup.
docs/porting-notes.md gets a new lesson under section E (cache
getenv() in any hot-path probe). tools_pc/perf.ps1 automates the
trace-and-analyze workflow for next time.
Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog
* docs(D250): correct the 1440p+ follow-up -- it was a stale-build false alarm
User re-tested 1440p/4K on the actual getenv-fix build and confirmed
it holds up fine too. The "still runs badly at 1440p+" report was
from before the fix was live, same stale-build class as the DLL-load
false alarm earlier in the session. D250 is fully resolved across all
resolutions tested, not just 640x480.
Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog
* docs: add missing GE_D219RAW row to GE-ENV-PROBES.md (CI validate fix)
Pre-existing gap unrelated to this branch's own changes -- gen_env_probes.py
--check caught it now since it runs on every PR. GE_D219RAW has had live
getenv() sites in src/game/image.c since the D219 investigation but was
never added to the probe doc.
* ci: route windows-build to self-hosted runner (G2); land ROM-gated sweep workflow (G1)
G2: windows-build now runs on the self-hosted box (corpyvt6agent01) for
push/workflow_dispatch, using local ccache instead of the actions/cache
round-trip and skipping msys2/setup-msys2's install once packages are
already present. pull_request always stays on windows-latest — fork PRs
must never reach a box that will hold a ROM.
G1: adds .github/workflows/selfhosted.yml, a dispatch-only workflow that
builds + runs the 21-level headless sweep (tools_pc/verify.sh sweep --json)
against a runner-cached ROM. Inert until manually dispatched.
Still pending (manual, physical steps on the box — not done by this
commit, see docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md rollout table):
- register corpyvt6agent01 as a runner against this repo
- confirm MSYS2 at C:\msys64 with the required mingw64 package set
- one-time ROM seed into {runner_home}/_rom-seed/
- first workflow_dispatch sweep + parity check against a local run
- flip G1 from workflow_dispatch to push:[main] once green
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* fix(selfhosted.yml): remove invalid concurrency.max-parallel key
Found by testing: dispatching the workflow failed with a hard parse
error (HTTP 422, "Unexpected value 'max-parallel'"). GitHub Actions'
workflow-level `concurrency` block only supports `group` and
`cancel-in-progress` — `max-parallel` is a job `strategy:` key for
matrix jobs, not a concurrency-block key. Serialization was already
achieved via group + cancel-in-progress:false; the stray key just
blocked every dispatch.
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* fix(selfhosted.yml): register the msys2 shell wrapper (setup-msys2 step)
Found by testing: the first fixed dispatch failed every msys2-shell step
with "msys2: command not found". Unlike ci.yml's windows-build, this
workflow used `shell: msys2 {0}` without ever running msys2/setup-msys2,
which is what registers that shell alias for the runner — MSYS2 being
pre-installed on the box doesn't help if Actions doesn't know the shell
exists. Adds the same setup-msys2 step ci.yml uses, with update:false
since the packages are expected already present (fast no-op per plan).
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* ci: stop reinstalling MSYS2 per run on the self-hosted box
The self-hosted paths (selfhosted.yml; ci.yml windows-build's push/
dispatch route) no longer call msys2/setup-msys2 or pacman -S at all.
MSYS2 + the mingw64 package set are now assumed hand-maintained on
corpyvt6agent01 (docs/dev/notes/SELFHOSTED-RUNNER-PLAN.md), invoked by
calling its bash directly (`C:\msys64\usr\bin\bash.exe -leo pipefail
"{0}"`, the same invocation setup-msys2's generated wrapper uses) with
MSYSTEM=MINGW64 so /etc/profile still puts mingw64/bin on PATH.
Adds tools_pc/check-selfhosted-deps.sh: a fast, install-free check that
exits 0 immediately when the required packages (toolchain, cmake,
SDL2, zlib, python, ccache, zip, unzip) are present, or fails with the
exact `pacman -S --needed ...` line to run otherwise — so a missing
package on the box surfaces immediately with a clear fix, instead of
as a confusing failure deeper in the build.
ci.yml's windows-latest (PR) path is unchanged: ephemeral runners still
get a full msys2/setup-msys2 install every time, since there's nothing
to persist there.
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* fix: use msys2/setup-msys2 release:false instead of a raw bash-path shell
The previous attempt (invoking C:\msys64\usr\bin\bash.exe directly as a
custom `shell:` command) failed on the actual runner with "Second path
fragment must not be a drive or UNC name" — a real limitation, not a
typo; it failed identically in both files, including the one with a
static (non-templated) shell string, so it wasn't the ${{ }} expression
either.
Root-caused by reading msys2/setup-msys2's actual source: `release:
false` makes it reuse the existing C:\msys64 as-is (hardcoded path in
that branch, confirmed in main.js) instead of downloading+installing a
fresh copy, and — verified in the same source — omitting `update` and
`install` skips every pacman call entirely (just one `uname -a` + a
pacman.conf sed tweak, no network). This is the officially documented
mechanism for self-hosted runners with a pre-existing MSYS2 install
(see the action's README, "release" section).
ci.yml's windows-build now runs setup-msys2 unconditionally but with
release/update/install all keyed off github.event_name: full install on
windows-latest (PR, ephemeral), reuse-only on the self-hosted box
(push/dispatch). selfhosted.yml always uses release:false. Both then
run tools_pc/check-selfhosted-deps.sh to verify the box's hand-
maintained packages are actually present.
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* fix(selfhosted.yml): cp -r for the ROM sidecar directories
Found by testing with the ROM seed actually in place: pcmodels-ntsc-final
and pccg-ntsc-final are directories (not single files, unlike
ge007.ntsc-final.z64), so a plain `cp` failed with "omitting directory"
and left the seed step failing on every run even with the seed populated.
Claude-Session: https://claude.ai/code/session_01J5PZ3sTHiuW6MdQnepasSU
* ci: drive MSYS2 MINGW64 directly via ci-shell.cmd, drop msys2 shell shim
The self-hosted runs were dying with 'msys2: command not found' because the
msys2/setup-msys2 action's shell shim was never registered on the box. Replace
the dependency on that shim with a small committed launcher (tools_pc/ci-shell.cmd)
that headlessly reproduces the local 'MSYS2 MINGW64' terminal:
- MSYSTEM=MINGW64 + /mingw64/bin on PATH
- NON-login bash, so it keeps the workspace cwd (a login shell cd's to $HOME
and would break every relative path in the workflow)
- -e -o pipefail for GitHub-style error propagation
Both workflows now set defaults.run.shell to C:\msys64\ci-shell.cmd {0}; a
bootstrap step (shell: cmd) refreshes the launcher from the repo into C:\msys64
each run. selfhosted.yml drops setup-msys2 entirely; ci.yml keeps it only for
the ephemeral windows-latest PR path (which installs MSYS2 at C:\msys64).
.gitattributes: force CRLF for *.cmd so cmd.exe parses the launcher correctly.
* Revert "ci: drive MSYS2 MINGW64 directly via ci-shell.cmd, drop msys2 shell shim"
This reverts commit a976b2589c.
* docs(D244): log self-hosted-runner link failure — undefined reference to objectiveGetStatus_WEAK
First real build attempt on the reinstalled corpyvt6agent01 box (run
34707798707) fails at link with an unresolved #pragma weak alias. A local
repro of the identical alias pattern links fine on current MSYS2
(gcc 16.2.0/binutils 2.47); the box reports gcc 16.1.0 — likely a stale
toolchain, consistent with rollout step 2's earlier package-hygiene gap.
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* fix(verify.sh): surface run.log tail on NO-FRAMES instead of silently discarding it
sweep mode's per-level run.log (the game process's own stdout/stderr, where
an SDL/GL init failure would show up) was captured into a mktemp capdir but
unconditionally rm -rf'd with no diagnostic ever printed for the NO-FRAMES
(no-crash) case -- every level failed this way on the self-hosted runner's
first real sweep attempt with zero clue why.
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* fix(selfhosted.yml): stop deleting the ROM seed dir -- cache save silently fails on this box
actions/cache/save has been failing to persist the ROM cache entry on
corpyvt6agent01 (WARNING annotation, tar.exe exit code 2) but doesn't fail
the step -- by design, a cache-save failure is non-fatal, so the workflow
carried on and the now-unconditional "Delete one-time seed dir" step wiped
the only copy of the ROM every single run. Every dispatch since has failed
at "Seed ROM cache" with an empty seed dir and an empty cache -- exactly
the user-reported "it wipes out the ROM data each run".
Fix: don't delete the seed dir. It's cheap disk (three files, a few hundred
MB) and the existing restore-then-seed-if-missing logic already treats it as
the fallback source whenever the cache comes back empty, so leaving it in
place is safe and makes the workflow tolerant of the cache-save bug until
that's root-caused separately.
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* fix(verify.sh): sweep --json never actually produced a verdict array; fix NO-FRAMES diagnostic clipping it further
Two bugs, found together while chasing the self-hosted runner's first real
sweep attempt:
1. sweep mode's `out=$(verify_level ...); echo "$out" | head -1` always
discarded emit_verdict's second (--json) output line -- RESULTS_JSON was
declared for assembling a verdict array but never populated or printed,
so `sweep --json` has never produced anything but `[]` for the
workflow's "Extract JSON verdict" step. Now builds and prints a real
`[{...}, {...}]` array before the RC exit.
2. My own D244-session addition (printing a NO-FRAMES level's run.log tail)
wrote to stdout, which the same `out=$(...)/head -1` capture turned into
the *new* first line, clipping the real verdict summary. Moved to stderr,
which bypasses that capture.
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* docs: regenerate findings-index.csv for D244
Claude-Session: https://claude.ai/code/session_01CUFSnpgrAyQSDVHoQQ6Sx3
* docs(M-102): fix findings-index false-positive, release <version> bug, land parked site cleanup
Docs triage pass (docs/dev/notes/PROMPT-docs-triage-session.md).
- gen_findings_index.py: D194's row was silently bucketed FIXED (an open
design item) because it had no distinct status cell, so the generator's
fallback keyword search matched "fixed" somewhere inside 6,500 chars of
narrative -- the same misleading-index failure mode that cost a session
on D227 (see docs/dev/findings.md SS H "D227 -- M-98"/"M-99"). Gave D194
a real status cell. Also extended LEAD_RE/bucket() with 5 verdict tokens
already in use in the doc (ANALYZED, GUARDED, DIAGNOSTIC SHIPPED,
ROOT-CAUSED, NOT A BUG) and added a leading verdict word to 4 more rows
that had a status cell but no recognized token -- fixes all 10 rows the
generator previously bucketed "?" (D175 D174 D173 D220 D160 D133 D136
D156 D153, plus D194). findings-index.csv regenerated; --check passes;
diff reviewed, only these 10 rows moved.
- ci.yml: release-notes.md's <version> placeholders were never substituted
before `gh release create --notes-file`, so every published release body
(including the live v0.1.0 release) ships the literal text. Now sed'd to
the tag first.
- Landed docs/dev/notes/UNLANDED-docs-site-cleanup-steps3-5.{md,patch}
(parked M-101, never pushed, branch deleted): deletes the orphaned
docs/StructureGuide.md (0 tracked references), adds title/description
frontmatter to 5 published Pages docs, rewrites docs/index.md's stale
"Phase 2 of 4 / no audio yet / 19-of-21" status text to match the
current README. The 2 files whose hunks no longer applied via `git am`
(docs/index.md, docs/dev/agentic-development.md) were redone by hand
against current main.
- docs/BACKLOG.md (local, gitignored): retargeted a dead
docs/PORT-LEARNINGS.md reference to docs/porting-notes.md.
Larger triage items (findings.md monster-row convention, full
status-accuracy sweep, duplicate-defect clustering, dev-notes plan
triage, HANDOFF rotation policy) scoped but not started -- see
docs/dev/notes/FOLLOWUP-docs-triage-session.md (gitignored).
Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
* docs/index.md: fix stale in-level-music claim before merge
This PR predates the M-102c/M-105 correction (already landed in the
README via PR #55): in-level music is NOT absent, it plays with some
tracks having wrong-sounding instruments / occasional garbling (D77
fixed, D230 filed for the residual). This branch's own rewritten Status
paragraph still said music 'doesn't yet' play -- fixing it to match
before merge so main doesn't briefly regress to a stale claim this
session already corrected elsewhere.
run_one always idled out the full watchdog (45s/level sweep, 90s/level
single) even though the game only needs to render its intro pan +
spawn-in once -- everything after the last requested frame was wasted
wall-clock. Poll for the last frame's PPM (or a crash log) every 0.2s and
kill immediately; the watchdog stays as the hang/load-flake safety net
for a level that never gets there. Also trims the pre-launch settle
1s -> 0.3s.
Falls back to the old timeout-wrapped launch if the --dump spec doesn't
parse as lo-hi[:step] integers.
Measured: bunker1 (9 frames, 80-400:40) 45s -> 18s; dam (3 frames, default
200-440:120) 90s -> 17s. A full 21-level sweep should drop from ~16 min
toward ~6 min.
Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
D215 (#37, merged 73768a03) un-gated the 1P weapon-model DL nodes, so the
PP7 + hand now render in the -level_09 intro-pan capture window. The
committed golden predated that, making `verify.sh bunker1` report
REGRESSION (worst_cell ~28) on every run.
Captured on main @ 3fa1ba53, clean default ini pinned to 640x480,
GE_PCDUMP=200-440:120. Gun eyeball-confirmed visible + correctly posed +
clean (no D217 garble in headless capture). Post-rebaseline
`verify.sh bunker1` = PASS worst_cell 12.3.
Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
Env-gated diagnostics for the D202 per-voice sample-correctness work
(M-67/M-68/M-69), all inert unless the env var is set:
- GE_VOICEDUMP (mixer.c aEnvMixerImpl): per-voice resampled mono stream
to voicedump.raw, records [stateAddr][nSamples][us][s16 x n]. NB the
us stamp is mix time — records render ahead, samples END at the stamp.
- [VOICEMAP] (synthesizer.c, under GE_AUDIOTRACE): full PVoice ->
decoder/resampler/env-state address map at synth init, tying voicedump
slot addresses to WIRE filter addresses.
- [WIRE] (load.c) now carries a µs timestamp; sndPlaySfx trace gains
t=µs + wavetable pointer.
- [EVT]/[STOP-EVT] (snd.c): every ALSndpEvent the player processes, plus
the release ramp a STOP/DEACTIVATE computes (deltaUs).
- audiodebug.ps1: -VoiceDump switch.
With scratchpad/exact_match.py these prove every allocated voice plays
the exact ROM sample at the exact ROM pitch (286/320 CC >= 0.85 on the
M-69 full-corpus run; remaining failures are dropped allocations + wire
attribution, not corruption). Remove once D202 closes.
verify.sh wraps the manual render-verify recipe (build-if-stale, 640x480
ini pin/restore, headless GE_PCDUMP capture, pixcount, framediff, crash
symbolication) into one command with a machine-readable PASS/CRASH/
NO-FRAMES/REGRESSION verdict + --json, for a single level or a sweep
subset. Live-tested on Windows: single-level and subset-sweep runs both
produce correct verdicts and leave the tree clean.
crash_brief.py turns a ge007.crash.log into a symbolicated, dispatch-ready
investigation brief: greps porting-notes.md/findings-index.csv for
sections covering the crashing function/file, and flags known
parked-crash signatures (D188 va_list, D189 stack overrun) so agents stop
re-investigating them. verify.sh calls it automatically on a CRASH
verdict. Live-tested against a synthetic-but-real crash log built from an
actual built symbol; caught and fixed 3 bugs before trusting it (addr2line
unresolvable under native Windows Python's PATH handling, a substring
keyword-match false-positive, a Windows drive-letter path-split bug).
Claude-Session: https://claude.ai/code/session_01P3WrUHAFQZUuhrHhiJgTUr
Static/tooling slice of docs/dev/notes/AGENTIC-SPEEDUPS-PLAN.md — no build or
runtime environment needed to verify any of it.
- Step 6 / N7: add a VERIFY: line to the investigation-brief template
(docs/dev-process.md) and the CLAUDE.md dispatch-preflight checklist — the
brief must now name the exact probe/verify invocation that proves the task
done, or state that runtime verification is impossible here.
- Step 4 / N2: tools_pc/README.md — indexes the ~60 scripts with a
living vs artifact marker so closed-finding artifacts can be archived.
- Step 4 / R8: tools_pc/gen_findings_index.py -> docs/dev/findings-index.csv
(label, bucket, one_liner, status_raw) — grep-before-you-read aid for the
~200 KB finding log. Heuristic parser of the §F/§H tables.
- Step 4 / R9: tools_pc/gen_env_probes.py — re-greps live getenv("GE_*")
sites and reports NEW/GONE drift vs docs/dev/GE-ENV-PROBES.md (keeps the
curated prose, only checks the site map). Doc reconciled: 7 new probes
added, GE_D54 kept as a tombstone.
- CI: validate job now runs both generators with --check.
Step 1 / N1 (HANDOFF preflight) applied in the local-only docs/HANDOFF.md.
Step 2 (verify.sh) and the HANDOFF/HANDOFF-ARCHIVE trim are left for a
session with a build environment.
The HUD weapon/ammo-type icon (D187, PR #19) and blank briefing objective
text (D178, cd1ed574) are both fixed and on main. Remove them from the
release-notes and bundled-README known-issues blurbs so a v0.1.0 tag ships
an accurate list. Black sky and the front-end 3D models (Nintendo logo,
MISSION COMPLETE / mode-select) are still open and stay listed.
A stock MSYS2 MINGW64 shell ships neither `zip` nor `unzip`, so
`tools_pc/bundle-win.sh` failed at the packaging step for a local release
build (the M-45 handoff's documented v0.1.0 cut path). CI is unaffected
(it `pacman -S zip unzip`, so the `zip` branch is still taken).
Fallback order: zip -> 7z on PATH -> "C:\Program Files\7-Zip\7z.exe" ->
powershell Compress-Archive. Verified end-to-end: bundle builds, unpacks
to a clean dir, prepare-assets runs, ge007.x86_64.exe boots -level_09 to
frame 2100+ crash-free.
- tools_pc/bundle-linux.sh: package a Linux build (exe + README + licenses +
prepare-assets) as a .tar.gz. Mirrors bundle-win.sh's ROM / size guards and
prepare-assets assembly. No shared libs bundled — the generated README tells
the user to install SDL2 / zlib / libGL from their distro.
- tools_pc/dist/README.md.in: made OS-neutral via @PLATFORM@ / @EXE@ / @DEPS@ /
@LICENSE_EXTRA@ tokens; refreshed the known-issues list (dropped the stale
"ladders don't work" line — D177 fixed M-36; added black sky + missing HUD
weapon icon).
- .github/workflows/ci.yml: linux-build now packages + uploads the Linux
bundle; the release job attaches both platforms to the draft pre-release.
- .github/release-notes.md: rewritten for the two-platform NTSC-U alpha with
the prepare-assets step and current known issues.
Alpha scope: NTSC-U only, one-time prepare-assets step (Part A launcher and
Part B native converter from release-dropin-rom-plan.md remain follow-ups).
Claude-Session: https://claude.ai/code/session_01V4pK3pT83YnPsBp3Ey4RP2
d88_emit.py appends the 21 Usetup*Z stage-setup files (object placement, AI
scripts, pads/nav) to the same data/pccg-<region>/ sidecar d69 produces.
Without it the port has no setup data for any level and segfaults on load
before frame 1 (this was mis-filed as D185 'd69 regression' — it was a
missing pipeline step). Vendor d88_emit.py + its one local import
d88_propdefs.py; run d43 -> d69 -> d88 --regen.
Verified: fresh d43+d69+d88 from an assembled bundle produces pccg.bin
(3604378 B, 74 rows) and pcmodels.bin byte-identical to the known-good
sidecars; -level_09 and -level_20 boot crash-free against them.
The Windows bundle from bundle-win.sh / CI was unrunnable on its own (D179):
the port loads two ROM-derived directories (data/pcmodels-<region>/ and
data/pccg-<region>/) that are gitignored and cannot be shipped.
Add tools_pc/dist/prepare-assets/prepare-assets.py — a stdlib-only wrapper
(no compiler, no N64 toolchain, no network) that:
- auto-detects the ROM near the bundle, verifies SHA-1 -> region
(NTSC-U / PAL / NTSC-J), rejects unrecognised / byte-swapped dumps
- runs d43_emit.py + d69_emit.py against a vendored input tree
- writes data/pcmodels-<region>/ and data/pccg-<region>/ next to the exe
bundle-win.sh assembles prepare-assets/ fresh at package time (emit scripts
from tools_pc/, filelist.u.csv + file_resource_table.inc.c + the 512
modelFileHeader.inc.c files as vendored inputs) so it can never drift from
the scripts. Vendored inputs are decomp layout/symbol metadata, not game
assets. bundled README gets a one-time "prepare-assets" setup step.
Verified end-to-end: from a bundle containing only exe + DLLs + a US ROM,
prepare-assets.py produces all four output files byte-identical to
d43_emit.py / d69_emit.py run from the repo root. Bundle 14.1 MB (< 60 MB
guard). Error paths (no ROM / unknown SHA-1) tested.
Resolves BACKLOG "Alpha release — asset-prep tool" steps 1-3; step 4
(clean-machine smoke test with the game actually running) still owed.
- tools_pc/bundle-win.sh: package a Windows build for distribution — exe +
MinGW/SDL2/zlib runtime DLLs + README + license texts, into
dist/goldeneye-pc-port-<ver>-win64/{,.zip,.zip.sha256}. Hard-fails if a
ROM image or an oversized blob lands in the bundle; verifies the DLL
closure with ldd.
- tools_pc/dist/README.md.in: the bundled README template (ROM-supply
instructions, run steps, controls, legal), @VERSION@-substituted.
- CI: build tag trigger (v*); windows-build now packages the bundle every
run and uploads it; new release job (tags only, contents: write scoped
to it) attaches the bundle to a DRAFT pre-release for a human to
smoke-test and publish.
- README: Download section + Legal reworded to cover pre-built binaries
(engine only, no game data, permissive runtime libs bundled with their
licenses).
- .gitignore: /dist/ (root bundle output; tools_pc/dist/ template stays
tracked).
Cutting a release: tag vX.Y.Z-alpha, push; CI builds + drafts the
pre-release; smoke-test with a ROM, then Publish.
Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
The exe links MSYS2 mingw64 DLLs (SDL2, libgcc, libstdc++, libwinpthread,
zlib). Run from a normal PowerShell those aren't on PATH, so gdb reported
"During startup program exited with code 0xc0000135" and nothing ran.
Prepend the mingw64 bin dir (derived from -Gdb) to PATH before launching.
Also relax ErrorActionPreference around the gdb call so its stderr
progress lines don't raise NativeCommandError noise mid-run.
Claude-Session: https://claude.ai/code/session_01UpTM1AcBRJuTetoZNcCD5V
Built the migrated tree (MSYS2 mingw64 + ROM + sidecar regen) and ran
-level_09 with GE_PCDUMP: HUD text and the exit arrow now read
right-side-up. D114/D116 confirmed as capture artifacts.
- tools_pc/golden/*.png regenerated from a fresh correctly-oriented run
(replaces the stopgap in-place flip); stray v*.png removed
- D75 shrunk: with upright captures the gun-barrel Bond RENDERS fine
(walk + fire) -- the "absent" reports were the flipped capture. The
Nintendo logo is genuinely broken (renders as 2 white blobs shifted
left -- a real logoinst transform bug, not a flip). Cast roll not
re-captured.
- D169 entry -> FIXED with the build/verify note
Claude-Session: https://claude.ai/code/session_01UpTM1AcBRJuTetoZNcCD5V
- Commit CLAUDE.md + AGENTS.md so a fresh clone has the operating manual and
the ABI/no-game-logic rules (trimmed of stale phase status; the personal
pd_port path removed; points at the split docs).
- Sweep every stale doc reference across the tree after the restructure:
PCPortResearch.md -> internals.md / dev/findings.md,
PORT-LEARNINGS.md -> porting-notes.md,
AGENT-WORKFLOW.md -> dev-process.md,
docs/{GRAPHICS-BACKLOG,HANDOFF-ARCHIVE,GE-ENV-PROBES,LEVEL-*,TEXTURE-GLITCH-ANALYSIS,AUDIT-M6-*,AUDIO-PLAN}.md -> docs/dev/.
- Drop the superseded docs/PLAN-linear-level-sweep.md.
- docs/HANDOFF.md + docs/BACKLOG.md stay local-only (rolling working files);
CLAUDE.md now handles their absence in a fresh clone.
gfx_opengl_dump_bound_fbo wrote glReadPixels output (GL bottom-left
origin) straight into a top-row-first P6 PPM, so every GE_PCDUMP / F12
capture was upside-down. This is the entire source of the D114/D116
"HUD/text X-mirror" - four sessions chasing a fast3d mirror that never
existed, misreading inverted screenshots of asymmetric content. The
developer confirms the game renders correctly on real hardware.
- writer now emits rows top-to-bottom
- tools_pc/golden/*.png flipped in place to match + golden/README.md
- D114/D116 reclassified as capture-orientation artifacts (not deleted);
D75 "logo mispositioned" / RC1 "photo 180 deg" flagged for re-judging
- annotations in findings.md, GRAPHICS-BACKLOG.md,
TEXTURE-GLITCH-ANALYSIS.md, porting-notes.md D2, GE-ENV-PROBES.md
Not runtime-verified (migrated tree has no ROM/toolchain) - needs a
fresh capture to confirm text reads normally.
Claude-Session: https://claude.ai/code/session_01UpTM1AcBRJuTetoZNcCD5V
- README, LICENSE (MIT for the port layer), NOTICE, CONTRIBUTING.
- Restructure docs: internals.md, porting-notes.md, dev-process.md +
dev/agentic-development.md (the research write-up), dev/ (raw record).
- Replace decomp status-badge CI with a PC-port validate workflow.
- Remove session/agent scaffolding and stale planning docs from the tree.
- Add .gitattributes (normalize line endings to LF); add screenshots.
The pre-2026-08 history is the GoldenEye 007 decompilation by Larry Ficken
(KholdFuzion) and contributors, preserved unchanged. The PC-port work from
2026-08-17 on is mine.