diff --git a/.github/social-preview.html b/.github/social-preview.html index e09787bf..323885cc 100644 --- a/.github/social-preview.html +++ b/.github/social-preview.html @@ -118,18 +118,18 @@
GOLDENEYE 007
PC PORT
- Mission: Native PC port of GoldenEye, compiled from the N64 decomp project + Mission: Native PC port of GoldenEye 007, compiled from the N64 decomp project
REPORT:
-
Status: ALPHA v0.1.0 — PHASE 2 OF 4
+
Status: RELEASED v0.2.0 — WINDOWS · LINUX · STEAM DECK
- Statistics:19 / 21 solo missions completable in a full playthrough + Statistics:All 21 solo missions — full campaign completed in playtesting ~230 game-code units, compiled unmodified - ~17k lines C/C++  ·  ~6k Python (port layer + tools) - ~190 bugs root-caused  ·  built in ~3 weeks, part-time + ~22k lines C/C++  ·  ~10k Python (port layer + tools) + ~250 bugs root-caused  ·  two coding agents, one human, part-time
diff --git a/.github/social-preview.png b/.github/social-preview.png index 7941d3d8..626aa7af 100644 Binary files a/.github/social-preview.png and b/.github/social-preview.png differ diff --git a/.gitignore b/.gitignore index 3445fb2b..3b832aa7 100644 --- a/.gitignore +++ b/.gitignore @@ -156,6 +156,9 @@ ge007.crash.log # agent scratch space + audio probe artifacts (regenerable dumps/traces) scratchpad/ +# montage work artifact: the tiled contact sheet behind docs/img/shots/ and +# the preview video (regenerable from the same session recordings) +docs/img/shots/CONTACT-SHEET.jpg /audiodump.raw /audiotrace.log /mixertrace.log diff --git a/README.md b/README.md index a5abf1b0..052931b5 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,11 @@ in a dedicated `port/` layer, following the architecture of the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark), the same Rare "Indy" engine family, one hardware generation apart. -**v0.2.0 (pre-release)** is out for Windows and Linux — including Steam Deck, -where the Linux bundle sideloads as-is — and runs the full campaign at a steady -60 fps with known rough edges ([Status](#status)). +**v0.2.0** is out for Windows and Linux — including Steam Deck, where the +Linux bundle sideloads as-is — and runs the full campaign at a steady 60 fps +with known rough edges ([Status](#status)). To our knowledge it's also the +first open-source port of the original N64 game to ship as a public release: +free to download, build on and modify (you bring the ROM). It's also a case study in AI-agent collaboration on a large, low-level codebase: two coding agents, driven by one person part-time, porting ~230 @@ -28,11 +30,10 @@ See [Background](#background).

Bunker 1 intro camera - ~15 s of the port running: mission dossier, Facility, Silo, Jungle, Archives + Dam intro camera
In-engine, running in the port — Bunker 1 and Dam attract views, and a - ~15 s clip (the clip itself has no audio track): mission dossier → - Facility → Silo → Jungle → Archives. + ~32 s gameplay montage from live play sessions (the clip has no audio track).

## Download @@ -58,34 +59,46 @@ Then: 3. Run the one-time asset step: `python3 prepare-assets/prepare-assets.py` (Python 3.8+, stdlib only). 4. Launch the executable from that folder. -Read the [Status](#status) caveats first — this is a pre-release. +Read the [Status](#status) caveats first — v0.2.0 has known rough edges, +listed plainly there. ## Status -**v0.2.0 (pre-release) — playable, with known rough edges.** It runs the full -single-player campaign at a steady 60 fps with no known crashes; the point of -the pre-release is an end-to-end playtest that all 21 missions are completable -start to finish on this build — feedback is welcome. +**v0.2.0 — playable, with known rough edges.** The full single-player +campaign is completable end to end (all 21 missions, Agent difficulty — +playtested), at a steady 60 fps; all 21 solo missions load, render and run +crash-free, verified on Windows, Linux and real Steam Deck hardware. Feedback +is very welcome. **Working:** boot sequence and front end (menu → mission select → briefing → -start); all 21 solo missions load, render and are crash-free; steady 60 fps +start); all 21 solo missions load, render and are crash-free (full campaign +playtested end to end at Agent difficulty); steady 60 fps (software RSP off the presentation critical path); full audio — in-level music and SFX; keyboard + mouse (click-to-lock, proportional aim mode) and a modern -dual-stick controller layout; file-backed saves; F10 in-game options overlay -(resolution, frame cap, MSAA, filtering, FOV, sensitivity); Windows and Linux. +dual-stick controller layout; file-backed saves; all levels, 007 mode and the full cheat menu unlocked by +default (F10 → *All unlocked* restores faithful N64 progression); F10 in-game +options overlay (resolution, frame cap, MSAA, filtering, FOV, sensitivity); +Windows and Linux. **Known issues:** - **Cutscenes glitch frequently** — skipped beats, wrong camera, misplaced or - hovering actors, wrong timing. The most visible gap in this release. -- A few in-level music tracks sound wrong (wrong instruments, occasional - garbling). + hovering actors, wrong timing; the Dam level-end cutscene is racy (D243). + The most visible gap in this release. +- A few in-level music tracks sound wrong — a wrong-sounding bass instrument + on some elevator/level tracks (D230). - Particle colours drift through a rainbow palette instead of holding their - grey/orange intent (bullet sparks, lingering smoke/explosion residue). -- Water levels show a moving seam between two water patterns; pixel strips at - the left/right screen edges at non-integer scales; some front-end 3D models - (spinning Nintendo logo, MISSION COMPLETE / mode-select) mispositioned or - absent. Assorted further cosmetic defects are tracked in + grey/orange intent (bullet sparks, lingering smoke/explosion residue) (D252). +- Water levels show a moving seam between two water patterns (D245); thin + pixel strips at the left/right screen edges at non-integer window scales + (D246). +- Some front-end 3D models are off — the spinning Nintendo logo renders as two + white blobs and the Rareware logo's texture filtering looks wrong (D75). +- The F10 overlay's bottom row duplicates whatever item is currently selected + (D251). +- **Steam Deck:** one intermittent SIGSEGV remains in heavy firefights (D255); + current builds capture full faulting registers in `ge007.crash.log`. +- Assorted further cosmetic defects are tracked in [`docs/dev/GRAPHICS-BACKLOG.md`](docs/dev/GRAPHICS-BACKLOG.md). - No macOS or ARM support; no controller rebinding UI. @@ -102,9 +115,10 @@ installing. On SteamOS the first launch seeds `ge007.ini` with Deck-friendly defaults — native 1280×800 fullscreen, VSync, MSAA 4, and 150% draw/LOD distance (the authored N64 fade distances read short on the close-up panel); everything is changeable in the options overlay and persists afterwards. The renderer is CPU-bound (software RSP); expect original N64-era -performance at 60 fps rather than more. A v0.1.0-era crash on the Deck in -Facility was never reproduced and its prime suspect has since been fixed, but -this release has not yet been verified on real Deck hardware. +performance at 60 fps rather than more. This release was playtested on real +Deck hardware — the v0.1.0-era Facility crash was never reproduced there, and +its prime suspect (D253) is fixed; one intermittent SIGSEGV in heavy +firefights remains open (D255). **In-game settings on the Deck.** The options overlay is fully gamepad-driven: it opens with **Select**, the D-pad or left stick (up/down) moves between @@ -160,7 +174,7 @@ code with this one. | **How** | Decompilation-based source port — human-reconstructed C, compiled for the host; game logic runs as written | Static binary recompilation — the shipped machine code is auto-translated to C; no source-level understanding | | **Lineage** | [GoldenEye 007 decompilation](https://github.com/n64decomp/007) + [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark) engine family | Xbox 360 "…Recompiled" static-recompilation family | | **Renderer** | Software RSP → OpenGL | Hardware (Vulkan) | -| **Status** | v0.2.0 pre-release; see [Status](#status) | Playable full game, higher frame rates, online multiplayer | +| **Status** | v0.2.0; see [Status](#status) | Playable full game, higher frame rates, online multiplayer | | **Why it exists** | A [case study in AI-agent collaboration](#background) on a hard low-level codebase | A polished, playable PC release of the remaster | If you just want to play GoldenEye on PC today, use one of the recompilation diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 70be3625..461b7bc3 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -135,6 +135,7 @@ D250,?,FPS is inconsistent/low on many levels since the port started actually ru D251,FIXED,F10 options overlay QoL pass:,"FIXED (port-only, port/src/optionsoverlay.c)." D252,?,"Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).", D253,FIXED,Facility (and any level with a FORCECROUCH/LADDER linked special tile) SIGSEGV on Linux/Steam Deck —,FIXED — Deck-verified: -D254,FIXED,Linux crash handler destroyed its own logs:,FIXED — needs one Deck crash to prove the new format in the field. -D255,OPEN,"Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle, D253 fix confirmed working —",OPEN — awaiting register capture from the Deck. +D254,FIXED,Linux crash handler destroyed its own logs:,FIXED — field-verified. +D255,OPEN,"Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle, D253 fix confirmed working —","OPEN — case identified (NULL model on an active OBJ record; destroy-path ordering), reproducible on Deck in combat (Bunker + Frigate); fix pending rule-#2 sign-off." D256,FIXED,Saves (and config) silently failed to persist on Linux whenever the game was not launched from the directory containing data/ —,FIXED (path half) — save-flow detail still owed from the user. +D257,FIXED,"New Game.AllUnlocked option (F10, default ON for this cut):",FIXED — verified by user playtest; PR open. diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 9e4487f3..2f5fde8e 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -531,8 +531,8 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D251 | **F10 options overlay QoL pass: (a) bottom rows drawn off-screen in the in-game 2D space — the layout assumed a ~320×240 field and hard-fit all rows, but `viSetXY` gives 440×330 on front-end screens (`src/game/front.c:8618`) vs 320×240 in-game (`front.c:8628`), so with 17 rows × 15u pitch + 2 header rows the last rows land at y≈282, off-screen whenever the panel is opened in-level; (b) MSAA row only cycled forward — `rowAdjust()`'s `ROW_MSAA` case clamped its index to [0,3] instead of wrapping, so left-click stuck it at 8x with no way down via clicks; (c) manual % rows (`Video.DrawDistance`/`Video.LodDistance`) showed unconditionally even while their `*AutoFov` toggles were on, where the percent is inert/misleading. | **FIXED (port-only, `port/src/optionsoverlay.c`).** (a) The panel now computes how many rows fit the *actual* current 2D height (`maxVisibleRows()` from `viGetY()`) and scrolls: a visible-row window (`s_scroll`) follows the selection, driven by wheel (`optionsOverlayScroll`) and arrow/D-pad at the list edges; layout, mouse hit-testing (`overlayRowAtY` now returns a visible position) and keyboard nav all index the visible list (`s_visIdx`/`s_visN`, `s_sel` is now a visible-list index), so it fits both 240- and 330-tall spaces with no per-space tuning. (b) `ROW_MSAA` wraps `(idx + dir + 4) % 4` — OFF→2x→4x→8x→OFF in both directions, matching the already-wrapping `ROW_ENUM`/`ROW_RES`; sliders keep clamping (standard for drag-able values). (c) New `Row.hiddenIfOn`/`hidePtr` fields: a row is hidden while the named option is nonzero — `Draw distance %` hides under `Video.DrawDistanceAutoFov`, `LOD distance %` under `Video.LodDistanceAutoFov`; rebuilt every frame in `overlayUpdateVisible()` (selection clamped into range when its row vanishes). Hint line updated to "click / drag / arrows / wheel". **Verified:** compiles clean in the full 241-target build; final link of this session was blocked only by a running game instance holding the exe (re-link pending). Not yet eyeballed live: scroll feel at 320×240, hover/scroll interaction, and the auto/% row appear-vanish transitions. Cross-ref D213 (FPS readout in the same file), D218/D249 (the auto-coupling these rows expose). **Follow-up after live test:** (d) list navigation (wheel + arrows) wrapped at both ends, so scrolling past the bottom made the highlight "repeat" from the top — now clamped at both edges like a normal PC settings list; (e) new `Input.SensLink` toggle (default ON, registered in `port/src/input.c`) links the aim/turn sensitivity knobs: moving either rescales the other to hold the stock 38 (`Input.AimModeSens`) : 50 (`Input.MouseTurnSpeed`) ratio, via a re-entrancy-guarded pair-write in `rowSet()`; (f) the "Mouse aim speed" row now edits `Input.AimModeSens` (the knob the default GEPD aim path actually uses — it previously edited `Input.MouseAimSpeed`, which only feeds the legacy velocity-stick fallback and was inert under default settings). **Second follow-up:** hover-to-highlight removed entirely -- moving the mouse over the panel no longer moves the selection or scrolls; near a list edge the hover->select->scroll-shift->new-row-under-cursor feedback loop made the bottom of the list twitch/echo (the persistent "duplicate row" report). Selection now moves only by explicit click (which also selects the clicked row), wheel, or arrows. **Reassessment of the 4K-fullscreen ghost-row bug:** the presentation-UV-wrap theory is DEAD — `gfxFramebuffer` has zero readers (the textured-quad present never happens); presentation is a full-frame `glBlitFramebuffer` from the same-size MSAA FBO to the default framebuffer (fb index 0 is reserved for the window, `gfx_opengl_init`), which has no UVs and cannot wrap. The overlay DL draws each visible row exactly once, in-bounds, with its own scissor. Local repro (maximized 16:9 window + MSAA=4 + `GE_OPTIONSOVERLAY=1` + `GE_PCDUMP`) shows NO ghost — the FBO/MSAA path alone is not the trigger; the remaining variable is true fullscreen on the user's 3072×1728 display (driver/display-level suspect). Next: user bisection — Fullscreen ON + MSAA OFF (restart) → does the ghost persist? **User follow-up (logged, not fixed):** the 4K-fullscreen ghost is *intermittent* across launches, and the user suspects the `hiddenIfOn` appear/vanish transitions of the Draw/LOD distance % rows (visible-list resize while the panel is open) — unverified; note `overlayUpdateVisible()`+`overlayUpdateScroll()` are re-run together in both emit and input each frame, so a stale-state window would have to come from elsewhere (e.g. the one frame a row count changes). MSAA-off bisection still pending. User: in the sessions seen so far the ghost is present on *first open* of F10 (not only after an auto-toggle flip) — weakens but does not kill the appear/vanish theory (toggles had flipped in earlier sessions before a restart). **v0.2.0-pre playtest (user, latest build):** the symptom has changed — the panel's *bottom row now deterministically duplicates whatever item is currently selected* (e.g. MSAA appears both on its own row and again at the bottom), not just an intermittent 4K/MSAA ghost of the top row; suggests the visible-row window can contain the selected row twice (off-by-one in `overlayUpdateScroll`/`maxVisibleRows` bookkeeping) — investigate post-release. **QoL follow-up:** make the auto-linked % rows visually subordinate to their toggle (indent and/or dimmer value colour) so the pairing is obvious in the panel; hint line also reworded to "select: scroll/click, change: mouse/arrows". | | D252 | **Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).** Shooting repeatedly makes the bullet-impact "sparks" slowly progress through the full hue wheel (~10 shots to sweep it); other lingering particle effects — smoke / explosion residue — show the same rainbow colouration instead of their intended grey/orange palette. **Mechanism context (verified this session):** particle tris carry no per-primitive material; their colour comes entirely from the ROM-resident state records `globalDL_0x078 … globalDL_0xa50` (`assets/oddtextures.c`, 15 of them) replayed per particle batch (`explosion.c:894`). Each record binds a **distinct** texture pair — `IMAGE_SMOKE_N` (IA, tile 0 @ TMEM 0) × `IMAGE_FIRE_N` (RGBA16, tile 1 @ TMEM 0x188) under `G_CC_INTERFERENCE` (TEXEL0·TEXEL1), and `explosionRenderPart` uses a fixed all-white default vertex (`explosion.c:215`) — so the intended per-stage colour is whatever those 15 fire/smoke pairs encode, and particles **age through the records** (`var_s2` 14→0 at `frame/flareanimspeed`), so a per-texture decode error surfaces exactly as a hue sweep over time / across shots. **Ruled out this session:** (a) the M-82/D172 tile-offset fix is correctly configured for this path — `gsDPSetTextureLOD(G_TL_TILE)` does NOT set the `G_TL_LOD` bit, so `rdp.tex_lod` stays false (`gfx_pc.cpp:2860`) and `gfx_lod_tile_offset` returns `i`, i.e. TEXEL1 genuinely samples tile 1; (b) the RGBA16/5551 texel decode in `import_texture_rgba16` is correct (big-endian, R=bits15-11 G=10-6 B=5-1 A=bit0). **Prime suspect: the width/height derivation in `import_texture_rgba16`** — `width = rdp.texture_tile[tile].line_size_bytes / 2`, `height = size_bytes / line_size_bytes`. The fire tiles are 16 px wide with a MASK_16 TMEM line (64 bytes), so `width` computes 32 (true: 16) and, with `size_bytes` = the LOADBLOCK's 16×14×2 = 448, `height` computes 7 (true: 14) — the GL texture would be built from the right pixels at the wrong dimensions, scrambling each of the 15 fire sprites into a different wrong-hued smear; that fits "slow rainbow progression" precisely (each aging stage = a different scrambled sprite). **Not yet confirmed** — needs one imported fire tile dumped and compared against the raw `IMAGE_FIRE_N` bytes (`GE_TEXDUMP`/`GE_PCDUMP`, or a targeted probe at the `upload_texture` call). **Per user instruction: documented only, NOT fixed for v0.2.0-pre** — ships as a known issue; pick up afterwards (cheap if the suspect holds: derive width/height from the LOADBLOCK's dtx/dlh rather than the tile line size, or special-case non-32-texel-wide RGBA16 tiles). D172/M-82 lineage. | | D253 | **Facility (and any level with a FORCECROUCH/LADDER linked special tile) SIGSEGV on Linux/Steam Deck — `stanCheckLinkedSpecialTile` stores to a garbage `outFlags` pointer (D203 root cause).** The B-callback typedef `standTileLocusCallback_B_t` (`bondtypes.h:573`) declares the middle three params as `f32 f32 f32`, so on x86-64 SysV the caller (`sub_GAME_7F0B1DDC`, `stan.c:2228`) passes the 6th arg (`record`) in **`%rdx`** (3rd integer slot; the floats take xmm0–2). But the actual implementation `stanCheckLinkedSpecialTile` was decompiled with an all-`s32` prototype, so it reads `outFlags` from **`%r9`** (6th integer slot) — a stale scratch value left by earlier callees. On the N64 (MIPS o32) both prototypes agree: everything flows through GPRs a0–a5 in order, 6th arg = a5 either way, so the original is fine and the decompilation looked consistent. The mismatch stays dormant until a tile actually hits the FORCECROUCH or LADDER branch and *stores* through `outFlags` → SEGV; that's why it was level-specific (Facility's opening crouch tile) and input-dependent, and why idle repros were negative. The sibling B-callback `stanLocusEdgeIsAboveY` already had the correct float prototype, which is why only this path was booby-trapped. **Fix (ABI-class, semantics-preserving):** declare the three middle params of `stanCheckLinkedSpecialTile` as `f32` in both the forward decl and definition (`stan.c:152`, `stan.c:2407`) to match the typedef; the body never uses them, and on N64 o32 f32 args occupy the same GPRs, so N64 codegen is unaffected. **Verified:** gdb run on WSL (gcc 15) — pre-fix, SIGSEGV at `stan.c:2441` with `r9=0x3` and valid `record=0x409fea00` in the caller frame; post-fix, the same walk route completes with no crash. Side benefit: the FORCECROUCH/LADDER branches were effectively dead-on-arrival on PC (any hit = crash); they now function. **Also observed post-fix (separate, WSLg-only):** a segfault *inside libgallium* during `gfx_opengl_upload_texture` (32×48 RGBA + mipmaps) while walking further — GL-driver-side under WSLg's Mesa; not reproducible on Windows; park as a new finding if it recurs on real hardware. | FIXED — Deck-verified: user got past the crouch tile with the re-cut bundle (a *new*, later crash is D255). | -| D254 | **Linux crash handler destroyed its own logs: `ge007.crash.log` only ever contained the SIGABRT block, never the original SEGV.** The POSIX `crashHandler` (`port/src/crash.c`) finished by calling `sysFatalError()` → `abort()` → SIGABRT, which re-entered the *same* handler (SIGABRT was one of the installed signals); the second pass reopened the log with `"wb"` and overwrote the first pass's SEGV block — faulting PC, signal, everything. The Deck firefight crash (D255) came back as a bare `SIGNAL: 6` abort record for exactly this reason, forcing offline disassembly of the CI binary to recover what the log should have said. **Fix (port-only):** re-entrancy guard (`inCrashHandler`, second pass `_exit(128+sig)` immediately) + terminate directly with `_exit()` instead of `sysFatalError()` (the Windows path already terminated directly for the same reason) + the Linux block now dumps the faulting context: `FAULT ADDR` (`si_addr`) and the full general register set (Rax/Rcx/Rdx/Rsi/Rdi/R8–R11, Rsp/Rbp) from the `ucontext`, so a field crash now names the bad pointer on its face. | FIXED — needs one Deck crash to prove the new format in the field. | -| D255 | **Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle, D253 fix confirmed working — user got past the opening crouch tile).** Crash during gunplay; abort-unwind backtrace points at `sub_GAME_7F09BAC4` (`vtxstore.c:158`) ← `sub_GAME_7F09BBBC` (`:220`) ← `lvlManageMpGame` (`lv.c:1302`) ← `bossMainloop` (`boss.c:567`). Disassembling the exact CI binary at the fault PC (`0x200db135`) gives `mov 0x14(%rax),%edi; mov 0x10(%rdi),%rax`: the function walks the active PropRecord list keeping `type == 1` — which is **PROP_TYPE_OBJ, not CHR** (the decompiler cast to the union's first member and misnamed the field `ChrRecord.chrflags`; it is actually `ObjectRecord.model` at `0x14`) — then dereferences `model->obj`. Two port-relevant wrinkles: (a) the load is **32-bit zero-extended** because the decomp types the field through the `CHRFLAG` enum, so a model pointer ≥ 4 GB would truncate to garbage; (b) the loop skips records whose `model->obj` is NULL but *dereferences* `model` unconditionally, so an active object with a NULL/freed `model` faults — plausible in a firefight where destructibles (crates etc.) die. Not reproduced on WSL after equivalent play. **Next:** the D254 handler now captures registers at fault time (`rax` = the ObjectRecord, `rdi` = its model value) — one more Deck crash identifies which case: tiny/zero `rdi` → destroy-path ordering; plausible-but-wrong `rdi` → stale/reused record or truncation. | OPEN — awaiting register capture from the Deck. | +| D254 | **Linux crash handler destroyed its own logs: `ge007.crash.log` only ever contained the SIGABRT block, never the original SEGV.** The POSIX `crashHandler` (`port/src/crash.c`) finished by calling `sysFatalError()` → `abort()` → SIGABRT, which re-entered the *same* handler (SIGABRT was one of the installed signals); the second pass reopened the log with `"wb"` and overwrote the first pass's SEGV block — faulting PC, signal, everything. The Deck firefight crash (D255) came back as a bare `SIGNAL: 6` abort record for exactly this reason, forcing offline disassembly of the CI binary to recover what the log should have said. **Fix (port-only):** re-entrancy guard (`inCrashHandler`, second pass `_exit(128+sig)` immediately) + terminate directly with `_exit()` instead of `sysFatalError()` (the Windows path already terminated directly for the same reason) + the Linux block now dumps the faulting context: `FAULT ADDR` (`si_addr`) and the full general register set (Rax/Rcx/Rdx/Rsi/Rdi/R8–R11, Rsp/Rbp) from the `ucontext`, so a field crash now names the bad pointer on its face. **Field-verified 2026-09-13:** the D255 Deck crash came back as `SIGNAL: 11` + `FAULT ADDR: 0x10` + full register set — exactly the format designed here; it directly identified the NULL-model case (see D255). | FIXED — field-verified. | +| D255 | **Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle, D253 fix confirmed working — user got past the opening crouch tile).** Crash during gunplay; abort-unwind backtrace points at `sub_GAME_7F09BAC4` (`vtxstore.c:158`) ← `sub_GAME_7F09BBBC` (`:220`) ← `lvlManageMpGame` (`lv.c:1302`) ← `bossMainloop` (`boss.c:567`). Disassembling the exact CI binary at the fault PC (`0x200db135`) gives `mov 0x14(%rax),%edi; mov 0x10(%rdi),%rax`: the function walks the active PropRecord list keeping `type == 1` — which is **PROP_TYPE_OBJ, not CHR** (the decompiler cast to the union's first member and misnamed the field `ChrRecord.chrflags`; it is actually `ObjectRecord.model` at `0x14`) — then dereferences `model->obj`. Two port-relevant wrinkles: (a) the load is **32-bit zero-extended** because the decomp types the field through the `CHRFLAG` enum, so a model pointer ≥ 4 GB would truncate to garbage; (b) the loop skips records whose `model->obj` is NULL but *dereferences* `model` unconditionally, so an active object with a NULL/freed `model` faults — plausible in a firefight where destructibles (crates etc.) die. Not reproduced on WSL after equivalent play. **Register capture (D254 format, 2026-09-13):** two more Deck crashes — Bunker mid-firefight, then Frigate with ~3 NPCs alive; log: `SIGNAL: 11 / FAULT ADDR: 0x10 / Rax=0x701e5f54 Rdi=(nil) / PC 0x200db135`. `Rdi` = the record's model value = **exactly NULL** → case (b) confirmed: an active PROP_TYPE_OBJ record with a NULL `model`, dereferenced by the unconditional `mov 0x10(%rdi),%rax`; truncation (a) and stale/garbage pointers are ruled out. Faulting PC matches the recorded disassembly (`sub_GAME_7F09BAC4` = `vtxstore_fix_refs`). Grep-verified: nothing in game code ever writes NULL into `ObjectRecord.model` — `clear_model_obj()` nulls `model->obj` only, and `objInit`/spawn paths only activate a record once model ≠ NULL — so the NULL comes from either a fresh/uninitialized record slot being activated or a destroy-path ordering race (model freed/nulled while the record is still active this tick). **Fix constraint:** any guard lands in game code (`vtxstore.c`) → needs rule-#2 sign-off; `port/` cannot intercept this dereference. Next: (1) decision on whether a NULL-model skip in `vtxstore_fix_refs` qualifies as an ABI-class exception (semantics-preserving defensive guard, PD analogue check); (2) if yes, add it plus a `#ifdef PORT` trace at the deactivation/model-free sites to name the path that leaves the record model-less. | OPEN — case identified (NULL model on an active OBJ record; destroy-path ordering), reproducible on Deck in combat (Bunker + Frigate); fix pending rule-#2 sign-off. | | D256 | **Saves (and config) silently failed to persist on Linux whenever the game was not launched from the directory containing `data/` — reported as "saves do not work at all" on Steam Deck.** `romdataInit` finds the ROM in `$S/` (CWD `data/`) *or* `$E/` (exe dir) *or* `./`, but `sysResolvePath`'s POSIX `$S` branch (used by `ge007.eep` and `ge007.ini`) only ever produced a CWD-relative path — its "fallback" was `./data/`, identical to the primary. Launch from any other CWD (Steam non-steam-game shortcut, terminal in `~`): ROM loads via the exe-dir candidate, game runs, but every EEPROM/config write hits `fopen(..., "wb")` on a nonexistent `./data/` and fails with a stderr-only "cannot write" — saves never persist, and **every launch also starts with default settings** (F10 changes don't stick), which presents as "draw distance/LOD is low" on levels like Dam where the user had raised `Video.DrawDistance` on another machine. The Windows branch already did CWD-else-exedir; the POSIX branch now mirrors it (`readlink("/proc/self/exe")`) and creates the directory best-effort so first-run writes don't fail. **Verified on WSL:** with exe+`data/` in `/tmp/fb2` and CWD=`/`, config/eeprom/ROM all resolve into `/tmp/fb2/data/` and a 25 s run completes clean; the pre-fix binary in the same layout fails to find the ROM at all. **Caveat:** if the user's Deck launch *was* from the bundle dir (CWD had `data/`), this fix does not explain their save failure — need the save-flow detail (does the memory-card screen appear? slot confirm? "saved" message?) before closing the report. Side note found while testing: a `data/` without the generated sidecars (`pccg-*/`, `pcmodels-*/` from `tools_pc/d69_emit.py`/`d43_emit.py`) starts fine but faults in `load_bg_file` (`bg.c:865`) on level load — the reserve functions warn "stage loads will fault" and return 0; expected degraded mode, not a new bug. | FIXED (path half) — save-flow detail still owed from the user. | | D257 | **New `Game.AllUnlocked` option (F10, default ON for this cut): skip N64 progression entirely — all 21 solo levels selectable at every difficulty, 007 mode available, and the cheat menu fully populated. Port-layer only; no game-code edits.** Three mechanisms, all in `port/`: (1) `main.c` seeds the two live `LEFTOVERDEBUG` RAM flags after `configLoad()` — `debug_enable_all_levels_flag` (all levels) and `debug_007_unlock_flag` (007 mode; both in `src/game/debugmenu_handler.c`, same class as the existing `GE_UNLOCK_ALL` getenv hook in the getter); (2) the eep shim (`libultra.c geEepromPatchAllCheats`) patches the save block at read time — special-casing `fileValidateSaves`'s exact pattern (block 4, 5×96 bytes) — setting all 20 progression-gated cheat-unlock bits per slot (`unlocked_cheats_1/2/3`; the cheat menu row on mode-select only renders when ≥1 is set, `front.c interface_menu06_modesel`) **and** filling every *empty* stage/difficulty completion-time field with the max time 0x3FF (existing player times preserved); both are then CRC'd per slot with the game's own `fileGenerateCRC` or `fileValidateSaves` would wipe the slots. (3) `optionsoverlay.c` row so users can toggle OFF for faithful progression. **Why the completion-time half is required (found by user repro):** when ANY single-player cheat is active, `g_AppendCheatSinglePlayer` makes mission-select (`get_highest_unlocked_difficulty_for_level`, front.c) accept only levels with status `STAGESTATUS_COMPLETED (3)`, not merely `UNLOCKED (1)` — AllUnlocked's debug-flag path returns 1, so enabling a cheat locked every level ("could only do Silo"). Reading the save as a fully-completed campaign satisfies both branches. Time bit-math mirrors `fileGet/SetSaveStageDifficultyTime` verbatim (10-bit fields, difficulties 0–2; 007 times are virtual). **Pitfall hit during implementation:** the first mirror of `save_data` used `times[80]` instead of `(SP_LEVEL_MAX-1)*4 = 76`, making `sizeof` 100≠96 so the block-read size check silently never matched — now enforced by a compile-time `ge_save_slot_size_check`. **Semantics:** unlocked cheats/completions persist to `ge007.eep` on any subsequent save write (self-consistent; mirrors N64 where completing a level permanently unlocks its cheat). No *active* cheats are enabled (invincibility/all-guns stay OFF); weapons remain per-mission pickups. **Verified:** fresh eep — all 5 slots validate `stored==calc` under `GE_SAVELOG=1`; user playtest: all levels selectable at each difficulty, 007 mode present, cheat menu populated, cheats toggle and work (time-modifying cheats untested by user), mission select intact with all cheats ON. | FIXED — verified by user playtest; PR open. | diff --git a/docs/dev/preview-video.md b/docs/dev/preview-video.md new file mode 100644 index 00000000..83ea7d67 --- /dev/null +++ b/docs/dev/preview-video.md @@ -0,0 +1,78 @@ +# GitHub preview video — runbook + +The README embeds `docs/media/goldeneye-gh-preview.mp4` (the gif of the same +name is too heavy for GitHub; keep it only if someone wants a local copy). +It's a ~32 s, 8-clip montage of gameplay cut from ShadowPlay session +recordings. + +## Tooling + +`scratchpad/` is gitignored, so `make-preview.py` is a **local** tool (it +hardcodes local video paths anyway) — this doc is the tracked artifact. If it +goes missing, the commands below describe exactly what to rebuild. Run with +plain `python`. +Working files (per-clip mp4s, concat list, palette, `state.json`) go to +`scratchpad/.preview-work/` — disposable, safe to wipe (but then re-run +`build` after any edit). + +```sh +python scratchpad/make-preview.py list # montage index -> source session + time +python scratchpad/make-preview.py shift <±sec> # nudge a clip within its source video +python scratchpad/make-preview.py set # absolute source time +python scratchpad/make-preview.py replace <.10|.12|.18|.19> # new random spot in a session +python scratchpad/make-preview.py regen --seed N # full random redo (same clips/lengths) +python scratchpad/make-preview.py build # re-concat + re-render from state.json +``` + +Every edit command rebuilds mp4 + gif automatically. A full rebuild takes +~30–50 s; a single-clip shift is the same cost (concat dominates). Budget +one command per step if running under a timeout — cut+concat and the two gif +passes together fit in ~55 s. + +## Source material + +`C:\Users\james\Videos\NVIDIA\Build-pc\new\` — ShadowPlay recordings +(3072x1728 @ 60 fps, h264), no usable audio track (cut with `-an`). The four +sessions and their tags are hardcoded in `SESSIONS` at the top of the script; +add a new session there (tag, filename suffix, duration) to use it. + +## Rules learned from review rounds + +- **Never start a clip in the first ~60 s** of a session: desktop / boot / + menus. (`MIN_START`) +- **Bad zones** — source times that show non-gameplay; random picks avoid + them and `replace` should too. Currently known (in `BAD_ZONES`): + - `.10` @ ~395–425 s: file-select menu + - `.10` @ ~1290–1420 s: watch close-up (appears twice in that session) + Add new ones here whenever a review round finds another. +- **Clip length** 4 s is the sweet spot; 5 s starts to drag, <3 s flickers. +- **Same-session spacing**: two clips from one session ≥90 s apart, or they + read as a repeat of the same scene. +- When told "roll forward/back ~N seconds", use N ± a few s and prefer a + high-motion moment in that window if you have keyframe stats (see below); + otherwise a flat offset is fine. +- If a pick lands on a menu/loading screen, step it forward again rather + than picking a new random time — the user's "a bit later/earlier" almost + always means the *same* scene, different part. + +## Output spec (don't change casually) + +- mp4: 1920x1080 @ 30 fps, libx264 crf 20, `+faststart`, no audio. ~25 MB for 32 s. +- gif: 512 px wide, 12 fps, two-pass palette (`palettegen` then + `paletteuse=dither=bayer:bayer_scale=3`). ~28 MB — local use only. + +## Optional: motion data to find "cool" moments + +`scratchpad/analyze-sessions.py` dumps per-keyframe YAVG stats for the +session files; the diff between consecutive keyframes is a decent proxy for +"something is happening". If you need to find the coolest moment in a time +window, maximize it — but note plain random picks with spacing constraints +have been preferred by the reviewer over signal-optimized cuts, so use this +only when asked for "something cool". + +## ffmpeg notes (Windows) + +No system ffmpeg; the script falls back to the `imageio-ffmpeg` static +binary (`pip install imageio-ffmpeg`). Pass paths as forward-slash or bare +filenames with `cwd=` set — drive-letter colons and backslashes break +filter-graph argument parsing. diff --git a/docs/img/shots/manifest.csv b/docs/img/shots/manifest.csv new file mode 100644 index 00000000..3334d020 --- /dev/null +++ b/docs/img/shots/manifest.csv @@ -0,0 +1,27 @@ +new_name,original,video_session,snapshot_time +shot-01.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_02.11.149.jpg,2026.09.13 - 16.05.44.10,02:11:149 +shot-02.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_06.00.446.jpg,2026.09.13 - 16.05.44.10,06:00:446 +shot-03.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_07.56.664.jpg,2026.09.13 - 16.05.44.10,07:56:664 +shot-04.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_08.33.264.jpg,2026.09.13 - 16.05.44.10,08:33:264 +shot-05.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_10.16.041.jpg,2026.09.13 - 16.05.44.10,10:16:041 +shot-06.jpg,Build-pc 2026.09.13 - 16.05.44.10.mp4_snapshot_11.03.430.jpg,2026.09.13 - 16.05.44.10,11:03:430 +shot-07.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_02.25.190.jpg,2026.09.13 - 16.54.07.12,02:25:190 +shot-08.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_03.05.000.jpg,2026.09.13 - 16.54.07.12,03:05:000 +shot-09.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_05.39.999.jpg,2026.09.13 - 16.54.07.12,05:39:999 +shot-10.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_05.55.651.jpg,2026.09.13 - 16.54.07.12,05:55:651 +shot-11.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_06.25.650.jpg,2026.09.13 - 16.54.07.12,06:25:650 +shot-12.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_09.33.997.jpg,2026.09.13 - 16.54.07.12,09:33:997 +shot-13.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_12.09.456.jpg,2026.09.13 - 16.54.07.12,12:09:456 +shot-14.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_12.59.497.jpg,2026.09.13 - 16.54.07.12,12:59:497 +shot-15.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_13.09.496.jpg,2026.09.13 - 16.54.07.12,13:09:496 +shot-16.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_15.22.656.jpg,2026.09.13 - 16.54.07.12,15:22:656 +shot-17.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_15.39.495.jpg,2026.09.13 - 16.54.07.12,15:39:495 +shot-18.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_16.54.496.jpg,2026.09.13 - 16.54.07.12,16:54:496 +shot-19.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_17.29.495.jpg,2026.09.13 - 16.54.07.12,17:29:495 +shot-20.jpg,Build-pc 2026.09.13 - 16.54.07.12.mp4_snapshot_17.39.495.jpg,2026.09.13 - 16.54.07.12,17:39:495 +shot-21.jpg,Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_00.21.484.jpg,2026.09.13 - 17.49.35.18,00:21:484 +shot-22.jpg,Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_00.31.328.jpg,2026.09.13 - 17.49.35.18,00:31:328 +shot-23.jpg,Build-pc 2026.09.13 - 17.49.35.18.mp4_snapshot_02.03.469.jpg,2026.09.13 - 17.49.35.18,02:03:469 +shot-24.jpg,Build-pc 2026.09.13 - 17.54.08.19.mp4_snapshot_00.11.972.jpg,2026.09.13 - 17.54.08.19,00:11:972 +shot-25.jpg,Build-pc 2026.09.13 - 17.54.08.19.mp4_snapshot_02.36.532.jpg,2026.09.13 - 17.54.08.19,02:36:532 +shot-26.jpg,settings menu.jpg,settings-menu, diff --git a/docs/index.md b/docs/index.md index d407d849..2d2d0767 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,7 +2,7 @@ title: GoldenEye 007 PC Port description: >- A native PC port of the original Nintendo 64 GoldenEye 007, built from its - decompiled source with a software RSP. v0.2.0 pre-release is out for Windows + decompiled source with a software RSP. v0.2.0 is out for Windows and Linux (including Steam Deck) — download it, drop in your own ROM, and play; or dig into the code and the engineering record behind it. --- @@ -15,15 +15,17 @@ N64's graphics coprocessor (RSP) running in software — the same architecture as the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark), the same Rare "Indy" engine family, one hardware generation apart. -**Status: v0.2.0 pre-release.** The full single-player campaign runs at a -steady 60 fps with no known crashes, and audio (music + SFX) plays throughout. +**Status: v0.2.0** — to our knowledge, the first open-source port of the +original N64 game to ship as a public release. The full single-player +campaign runs at a steady 60 fps with no known crashes, and audio (music + +SFX) plays throughout. Known rough edges — cutscenes still glitch, a few music tracks sound wrong, a handful of cosmetic rendering defects remain — are listed plainly under [Honest status](#honest-status).

- ~15 s of the port running: mission dossier, Facility, Silo, Jungle, Archives -
~15 s of the port running: mission dossier → Facility → Silo → Jungle → Archives (the clip has no audio track). + +
~32 s gameplay montage from live play sessions, running in the port (the clip has no audio track).

## Download @@ -132,12 +134,12 @@ without a ROM you already own.

-More captures land here as playtesting continues (Steam Deck session next). +More captures may land here as playtesting continues. ## Play it — or take it apart - **Try it yourself** — grab a bundle above, bring your own ROM, and play the - campaign. It's a pre-release cut for exactly this: if something breaks, an + campaign. It's an early cut for exactly this: if something breaks, an [issue](https://github.com/jkdansereau/goldeneye-pc-port/issues) with what you were doing is genuinely useful. - **Read the code** — the game logic in `src/` is unmodified decompilation; @@ -177,6 +179,8 @@ catalogue: [Porting notes](porting-notes.md). - Particle colours drift through a rainbow palette instead of holding grey/orange. - Water levels show a moving seam; pixel strips at screen edges at non-integer scales; some front-end 3D models mispositioned or absent. +- The F10 overlay's bottom row duplicates the selected item; one intermittent + Steam Deck SIGSEGV in heavy firefights remains open (D255). - No macOS/ARM support; no controller rebinding UI yet. The full list, with root causes and fix status: the diff --git a/docs/media/goldeneye-demo.gif b/docs/media/goldeneye-demo.gif deleted file mode 100644 index b5a6237a..00000000 Binary files a/docs/media/goldeneye-demo.gif and /dev/null differ diff --git a/docs/media/goldeneye-gh-preview.gif b/docs/media/goldeneye-gh-preview.gif new file mode 100644 index 00000000..ed49f978 Binary files /dev/null and b/docs/media/goldeneye-gh-preview.gif differ