diff --git a/.github/release-notes.md b/.github/release-notes.md index 5b4ba7cc..6cf631ae 100644 --- a/.github/release-notes.md +++ b/.github/release-notes.md @@ -1,288 +1,194 @@ -# GoldenEye 007 PC Port — v0.4.0 +# GoldenEye 007 PC Port — v

- ~24 s gameplay montage from live v0.4.0 play sessions, opening on the Runway tank (no audio track) + ~24 s gameplay montage from live play sessions, opening on the Runway tank (no audio track)

Platforms: **Windows x86-64** and **Linux x86-64 (including Steam Deck)**. -Region: **NTSC-U (US) only** — see [Caveats](#caveats). +Region: **NTSC-U (US) only**. See [Known issues](#known-issues). -v0.4.0 is a large feature and fidelity release. The full campaign runs at a -steady 60 fps with music and SFX throughout, and has been playtested end to -end (all 20 missions, Agent difficulty) on Windows and on Steam Deck with a -physical controller. This is a pre-1.0 release: the known issues below are -real, and some areas (multiplayer, PAL/JP, HD assets) are still ahead of us. + + +The headline is **2–4 player split-screen multiplayer**, the largest feature +the N64 original had that this port was still missing. Alongside it come +controller presets and rebinding, audio output options, a steady 60 fps on +low-end hardware, and a round of fidelity and stability fixes checked against +the N64 game. --- ## New features -### Native widescreen *(on by default)* +### 2–4 player split-screen multiplayer (#99) -The world now renders natively at your display's aspect ratio instead of -stretched 4:3: geometry is undistorted (wider view, same vertical FOV), the -in-level HUD keeps its shape and anchors to the screen edges, and the 4:3 -front-end menus and ending-credits sequence are pillarboxed. Turn it off -with the **`Native widescreen`** toggle (F10 → Graphics) to restore the old -stretched frame. (D334/D335) +- Local multiplayer on every MP map, with the N64's own setup flow, + pause/watch menus and per-player HUD. +- **Who controls which player:** with one controller, keyboard/mouse is + player 1 and the controller is player 2; with two or more controllers, the + controllers take players 1–4. `Input.MPMode` in `ge007.ini` picks Auto + (default), PadsOnly or KbmP1. +- A controller unplugged and replugged mid-match keeps its player (thanks to + dolent, #122). Each controller's LED shows its player number. -### A true, stable 60 fps +### Controller presets and rebinding (D469, D498) -v0.3.0 could settle at 30 fps game-wide on some setups. The cause was a -pointer-width bug in the game's frame scheduler: a "present every frame" -flag was read back at a hardcoded byte offset that the 32→64-bit transition -silently moved, so the render path ran on the audio client's every-other- -frame cadence. Fixed at the root; the game now holds a rock-stable 60 fps -(measured with the in-game FPS readout) on both platforms. (D248) +F10 → Input → **Controller** (also in the front-end PC Options): -### A complete aim system for mouse *and* controller +- **Layout preset:** the Xbox release's four styles: "1.1 Jinx" (the + default, identical to before), "1.2 Christmas", "1.3 Frost" and + "1.4 Elektra" (they swap which stick moves, looks, strafes and turns), or + **Custom**. +- **Custom:** rebind every action per controller, up to two buttons each. + Tap B or Back to cancel a capture, hold Back to clear. Menus always keep + A/X accept and B/Y cancel, so you can't lock yourself out. +- Builds on ideas from italoarruda's #109 (credited as co-author). +- In the tank, the pad follows your layout too: the weapon button switches + the tank's weapon and the use button gets out (D495). -- **`Aim style`** — **N64** (default): the crosshair deflects and the - camera follows once it reaches the screen edge. **Centred (PC)**: the - crosshair is fixed at screen center and your mouse — or your right stick — - moves the camera directly. In v0.3.0 the centred mode only worked with a - mouse; it now works with a controller too, using the N64 original's own - stick-response curve so full stick matches the original's top turn rate. - (D332/D333/D337/D404) -- **`Aim range`** (shown while Centred is selected) — **PC** (default, - full-screen feel) or **N64** (the original 65% stick limits). (D338) -- **Reticule jitter in aim mode is gone** — mouse aim now runs on the - Perfect Dark PC port's aim model. (D332) -- Per-device sensitivity: `Mouse horizontal/vertical sensitivity`, and for - controllers `X axis / Y axis look sensitivity (controller)` plus - `Look smoothing (controller)`. +### PlayStation and Nintendo button names (D471) -### Rebuilt options (F10 overlay + front-end PC Options) +PlayStation and Switch controllers show their own button names in the +menus (Cross/Circle, ZL/ZR, …). Xbox and Steam Deck text is unchanged. -- Both options screens are reorganized into functional sections — **Input, - Gameplay, HUD, Graphics, Audio, Video** — each with its own *Reset to - defaults*; per-profile settings scoping; and the old "save file" wording - is now **profile**. (D353–D356) -- The **F10 in-game overlay** was refreshed GE-style: category headers, - full controller support (value-adjust with hold-to-repeat), fixed pointer/ - keyboard navigation, and new rows: **Show FPS**, **Skip intro - (EXPERIMENTAL)**, plus pad-only **Invert look / Southpaw / Deadzone / - Trigger threshold**. (D237/D345–D347/D360–D370) -- The front end gained a **PC Options screen** beside the file-select bar, - and save-file **Copy/Erase** moved into the bottom bar. Long sections - (Input) page within the screen — a "Page 1/2" marker plus a bottom hint - on every page — so every row stays on screen and stays reachable; the - mouse wheel and W/S also page the list, and the selection clamps at page - edges. (D343/D406/D407) -- Watch-only settings — **Auto-aim**, **Look ahead** and the rest — are now - editable from the PC options and **persist correctly** (this closes the - "auto-aim won't stay on" report, issue #103). (D330/D350/D352/D354) -- **`HUD scale`** (75–150%, default 100%): scales the ammo counter, pickup - text and subtitle bars for smaller or larger displays. (D226) +### Audio output options (D470) -### Crosshair customization *(defaults leave the N64 crosshair untouched)* +- **Master volume**, applied after the music and FX volumes. +- **Output device:** pick your audio device. A missing or unplugged device + falls back to the system default. -**Show crosshair** (on/off), **Crosshair colour** (default *Original -(red)*, or Green / Red / Blue / Yellow / Cyan / Magenta / White, or custom -RGB), **Crosshair size** (default 100% of the original drawing), and -**Crosshair style** (Original / Thin cross). (D373/D379/D381/D382) +### Presets, filtering and the original look (D440, D447, D499) -### In-game key rebinding + new default layout +- **Original N64 preset** (F10 → Graphics) sets every value the port changes + back to the N64's; **Port defaults preset** restores the port's modern + defaults. +- **Aspect → Original** shows the exact 4:3 frame with bars (16:9 while the + game's own Ratio setting is 16:9). +- **Texture filter → Trilinear**, next to Bilinear: smooths distant + textures that the game has no mipmaps for. -A two-group key editor (**Movement**, **Actions**, each with its own *Reset -to defaults*) lives in both options screens. The new default **Actions** -layout is the GEPD-style preset (e.g. Action/next-weapon on the keys you'd -expect from a modern layout); existing INI bindings are honored and older -ones migrate automatically. (D371/D374/D383/D385) +### Options polish (D443, D472, D497, D502) -### Controller improvements +- **Restart game** row: applies restart-only settings such as anti-aliasing + with one click. +- Clearer values: mouse sensitivity as ×, deadzone, vibration and volumes in + whole %, field of view in horizontal degrees, anti-aliasing up to 16×. +- Front-end PC Options pages like the game's own dossiers: PREVIOUS and + NEXT folder tabs, and B goes back a page. With a controller, the first + item is highlighted when you open a section and you return to where you + were when you leave it. +- The F10 overlay is pillarboxed in widescreen (instead of stretched), + experimental options are shown in red, an unknown `ge007.ini` key shows a + warning (#90), and with several controllers the one that opened the + overlay drives it. -- **Sane pad defaults**: **A = use/interact, X = reload, Y = weapon cycle** - (v0.3.0 mapped two buttons to crouch and had no in-game use button). (D393) -- Crouch uses the engine's real crouch input, with a **`Crouch mode`** - toggle (Hold / Toggle). (D375) -- Xbox-variant controller parity, **Southpaw**, and per-stick - **deadzone** + **trigger threshold** rows. (D237/D394) -- **The N64 Rumble Pak now drives real gamepad haptics** (Steam Deck - included), scaled by a global **`Vibration`** slider (default 50%). (D401) -- The F10 overlay is fully gamepad-driven. (D347/D395/D396) +### `All unlocked` no longer touches your save (D442) -### `All unlocked` ships as a documented EXPERIMENTAL feature - -F10 → Gameplay → **`All unlocked (EXPERIMENTAL)`** opens every mission, 007 -mode and the full cheat menu. On a fresh install it now yields the game's -own fresh-slot layout instead of corrupting state (D281) — but enabling it -can still bake synthetic cheat/completion data into your save that switching -it off does not undo: **complete a level normally first, and back up -`data/ge007.eep` before enabling it.** (D387) - -### Steam Deck / Linux - -The first clean SteamOS/glibc build, plus a first-launch preset for decks -without a config (native 1280×800, 2× MSAA, vsync, 250% draw/LOD -distances). (D402/D283) +It's now a memory-only override: switch it off and your real progress is +back. Fake unlocks written by older builds are not repaired, so back up +`data/ge007.eep` if you used it before. --- -## Bug fixes +## Performance -### Fidelity (verified against the N64 original / era-correct references) - -- **Water** on the `IsWater` levels (Dam, Frigate, Surface 2, …) matches - the original: the moving seam between two patterns and the pattern - "resetting" as you moved are gone. (D245) -- The **particle "rainbow" effect** (intermittently recoloured sparks and - explosion residue) no longer occurs. (D252) -- **Frigate: "every polygon breaks except the gun"** after turning — and - the rare "Bond briefly out of place" quirk — were the same never-written - fields read by the head-bob animation; both are fixed. (D336/D311) -- Surface 1/2: the tree backdrop that rendered as a solid wall of texture - now renders as proper camera-facing tree cards. (D236) -- Ejected **shell casings** render again. (D331) -- The intro **gun-barrel blood drip** draws correctly. (D341) -- The extra erroneous **long muzzle flash** on some guns is gone. (D303) -- Occasional **z-fighting** fixed; **security-camera props** no longer face - backwards; the front-end **Nintendo logo and copyright page** render - correctly. (D308/D307/D403) -- The **train-intro soldier pose** is correct. (D392) -- **Tanks** (Runway, Streets) can be boarded and exited again — the new - use/reload button split had removed the B-button tap the engine's tank - handlers use; B is presented again in tank states, and use keeps its - no-reload-fallback semantics everywhere else. A short input lockout during - the sit-down animation means a double-tap can't cancel boarding, and in-tank - mouse aim has its own **`Tank aim speed`** setting (`Input.TankAimScale`, - default 100) so the turret matches your on-foot feel. (D407) -- File-select background / gun-barrel comb rendering fixed. (D397) -- Stale-texture artifacts after level transitions fixed. (D235) -- Distant-geometry dropout on the biggest open levels (Streets, Egyptian) - substantially improved at default settings. (D249) -- The **Facility execution-scene watchdog** from v0.3.0 stays enabled as a - safeguard; the animation-pinning that motivated it is root-caused (a tick - granularity divergence) and fixed, so the scene plays out as authored. - (D329/D318) - -### Front end & UI - -- Randomly **flickering F10 menu items** are gone. (D314) -- The watch's **controller-page graphic** renders again. (D290) -- File-select **folders and Bond photos** no longer vanish after backing - out of a file. (D342) -- File-select bottom row (SELECT FILE / Copy / Erase / PC Options) aligned. - (D398–D400) - -### Audio - -- **Gunshot SFX** cadence matches the N64 original's rate, and the - intermittent silence of the PP7/AK47 under sustained fire near a looping - sound is gone. (D240/D241) -- The silenced **PPK "slap"** and the broader real-time mixer corruption - behind it are fixed. (D202) -- Windows audio now prefers the **DirectSound** backend over WASAPI (lower, - more robust latency). (D322) -- In-stage music **re-synchronizes** correctly at stage transitions and - level entry. (D355) -- Long-session audio degradation (issue #87): pool hardening shipped, plus - an optional `GE_D322=1` telemetry probe if it ever recurs. - -### Stability & renderer - -- **Orderly game exit**: quitting no longer lands inside the graphics - driver (a class of Windows driver bugchecks seen during testing). (D344) -- A **non-self-recovering Windows freeze** after the window sat idle and - lost focus is fixed (mouse-mode changes now applied on the window - thread). (D287) -- **MSAA above your driver's maximum no longer black-screens** (the request - is clamped to `GL_MAX_SAMPLES`), and two framebuffer-state bugs in the - render/resolve paths were fixed — community contribution, PR #106, - security-reviewed. (D405) -- Port-wide code audit: missing returns in front-end menus, missing - prototypes, undefined-behaviour shifts. +- **A steady 60 fps on low-end hardware (D481, #92).** Rendering ran on the + same thread that delivers the game's 60 Hz timing signal, so slower GPUs + regularly missed their frame slot and dropped to 30–45 fps. Rendering now + runs on its own thread, like the N64's graphics chip. A 2011 laptop (Intel + HD 3000) went from ~45 to a steady 60 fps at every setting. +- **Lighter first-launch defaults on Atom/Celeron-class GPUs (D482, #92).** + On Intel HD 400–605 / UHD 600–605 class GPUs and software renderers, the + first launch lowers draw distance, LOD distance and anti-aliasing. Settings + you've changed yourself are never touched. +- **No more in-play shader stutter (D480):** shaders the game has used are + compiled at startup on the next launch. +- **Render-thread CPU roughly halved in typical scenes (D473),** plus fewer + redundant texture lookups and palette re-imports (D474, D476). --- -## Options reference +## Fidelity fixes (checked against the N64 game) -Every setting in v0.4.0's options screens (front-end **PC Options** and the -in-game **F10 overlay**), with the current default. - -| Setting | Section | Default | -|---|---|---| -| `Aim style` | Input | N64 (N64 / Centred (PC)) | -| `Aim range` | Input | PC (shown while Centred is selected) | -| `Mouse horizontal sensitivity` / `Mouse vertical sensitivity` | Input | 100% (calibrated midpoint) | -| `Invert look (mouse)` / `Invert look (controller)` | Input | Off | -| `Southpaw` | Input | Off | -| `X axis look sensitivity (controller)` / `Y axis look sensitivity (controller)` | Input | 100% (native) | -| `Look smoothing (controller)` | Input | 0 (off) | -| `Tank aim speed` | Input | 100% (`Input.TankAimScale`) | -| `Deadzone (left stick)` / `Deadzone (right stick)` | Input | 70% | -| `Trigger threshold` | Input | 23% | -| `Vibration` | Input | 50% | -| `Crouch mode` | Input | Hold (Hold / Toggle) | -| `Bindings…` (`Movement…` / `Actions…`) | Input | GEPD-style preset | -| `Auto-aim` / `Look ahead` | Gameplay | Off (watch-backed) | -| `Skip intro (EXPERIMENTAL)` / `No hit flash` | Gameplay | Off | -| `All unlocked (EXPERIMENTAL)` | Gameplay | Off | -| `Sight on screen` / `Ammo on screen` | HUD | On (watch-backed) | -| `HUD scale` | HUD | 100% (75–150%) | -| `Show crosshair` | HUD | On | -| `Crosshair colour` | HUD | Original (red) | -| `Crosshair size` | HUD | 100% | -| `Crosshair style` | HUD | Original (Original / Thin cross) | -| `Native widescreen` | Graphics | On | -| `Widescreen auto FOV` | Graphics | On | -| `Crop overscan` | Graphics | On | -| `Anti-aliasing` | Graphics | 2× (1/2/4/8, clamped to driver max) | -| `Texture filter` | Graphics | Bilinear (Nearest / Bilinear / 3-Point) | -| `Anisotropic filtering` | Graphics | 4× (1–16) | -| `FOV scale` | Graphics | 100% | -| `Draw distance` / `LOD distance` | Graphics | 250% (100–400%) | -| `Music volume` / `FX volume` | Audio | 100% (watch-backed) | -| `Fullscreen` | Video | Off (windowed) | -| `Resolution` | Video | Auto windowed preset | -| `VSync` | Video | On | -| `Frame rate cap` | Video | 60 (30 / 60) | -| `Show FPS` | Video | Off | +- **Draw distance no longer changes AI behaviour (D466, #125).** The + extended draw distance (default 250%) let guards "see" you from beyond the + N64's fog, so Xenia's fight on Jungle could start early. AI awareness now + uses the level's original distance; you can still see, shoot and use + everything that's drawn. +- **Ultrawide and high field of view (D468):** the AI keeps the N64's widest + (16:9) view. Turn on *AI sees the wider view* if you prefer the old + behaviour. +- **Sniper rifle (#136):** the scope zooms all the way in again (it was + capped at about a third of the original magnification, D484); zoom speed + matches the N64 (it was about twice as fast at 60 fps, D485); the crouch + key now crouches and stands with it out, as it does in GEPD (D494). +- **Turrets and explosions at 60 fps (D486, #126):** turret fire rate, + tracers, explosion density and screen-shake length now match the N64. + Damage is unchanged. (Explosions that sometimes don't appear in heavy + firefights are the N64's own behaviour: it has six explosion slots.) +- **Weapons (#114–#118):** automatic-fire cadence follows the N64's rules at + any frame rate; grenade launcher recoil is back; the gun sways with mouse + look again; the tank turns with the mouse; the run-over death sound no + longer loops. +- **Light fixtures go dark when shot, with the right impact effects + (#119).** +- **Dam/Caverns water (D465, #127):** the water animation no longer slides + side to side, and it scrolls at the N64's rate. +- **Kneeling guards** use their crouched-rifle firing animation (D445, + thanks dolent, #120). +- **Dam:** the first-tower guard walks his whole patrol instead of a small + circle (D490). +- **The Rareware logo** in the intro renders correctly (D75). +- Props near some doorways no longer count as being in extra rooms, and + ejected casings spin as authored (D491). --- -## Caveats +## Stability & fixes -- **NTSC-U (US) ROMs only.** PAL and JP ROMs are not supported in this - version; asset repair for those regions is on the post-release roadmap. - (D258) -- **The default Actions key layout is new** (GEPD-style). If you preferred - the v0.3.0 defaults, rebind them in *Input → Bindings…* or reset the - group. Existing INI bindings are auto-migrated and still honored. -- **Back up `data/ge007.eep` before using `All unlocked`** (see above). -- Steam Deck: the first launch in Game Mode applies the Deck preset only if - no `ge007.ini` exists yet; F10 changes always win afterwards. +- **Knife-slash crash fixed (D454).** +- **64-bit memory-layout fixes** (D441, D453, D456, D457, D461, D462): two + item/vertex tables were sized with N64 strides, and several pointer fields + were truncated. +- A split-screen zero-size texture crash is guarded (D463); stale + anisotropic filtering on recycled textures is fixed (D446, thanks dolent, + #121). +- *Skip intro* no longer skips the failure dossier after a failed mission + (D408). +- Adjusting the music volume with a controller in F10 no longer drops the + frame rate (D489). +- Steam Deck: the Deck preset now also applies after a first launch in + Desktop Mode (D283). +- Aztec: the area by the shuttle with the four turrets no longer breaks + into stray triangles. The per-frame vertex buffer, sized for what the + N64 could see, could overflow with the port's wider view (D500). +- In 3- and 4-player games, weapon pickup messages no longer show stray + characters (D496). +- A controller no longer keeps vibrating after you quit mid-firefight + (D493). +- *Aspect → Original* no longer shows a one-pixel strip of flickering + pixels at the left and right edges (D493). +- Crash logs now include resolved stack frames, which makes reports far + easier to act on. + +--- ## Known issues -- On Facility, if gas leaks during Ourumov's monologue he can pause for up - to ~10 s before resuming the scripted shootout — a latent race that exists - in the N64 original too (where it softlocks permanently); the port detects - and auto-recovers it. (D318) -- With *Native widescreen* on, the F10 options overlay stretches with the - window instead of pillarboxing like the front-end menus (legible; - cosmetic). (D335b) -- The front-end Rareware logo shows a subtle texture-filtering artifact - (the Nintendo logo and legal page are clean). Cosmetic only. (D75) -- **`All unlocked` is experimental** (see above); the fresh-install - corruption case is fixed, but saves made while it is on are not - guaranteed recoverable by switching it off. (D387) -- **`Skip intro` is experimental — not recommended for regular use yet.** - With it on, a failed or aborted mission skips the post-mission failure - report screen and returns straight to the menus (the mission still records - correctly). An older audio quirk on some saves is also still being chased. - (D408/D216) - -## Roadmap since v0.3.0 (research, not yet features) - -- ROM-level **mod support** investigated and found viable (the game's file - table lives in the ROM); HD texture packs and the drop-in-ROM flow are - tracked post-1.0. (D325–D328) -- A measured CPU budget on low-end hardware (issue #92) reshaped the - performance investigation toward the GPU. (D339) -- Controller options wave 2 (presets, pad reassignment UI) and PAL/JP - support are planned post-v0.4.0. +- **NTSC-U (US) ROMs only.** PAL and JP ROMs convert, build and boot from + source on this release's code, but the release packages don't support them + yet (#85). +- **No macOS or ARM builds yet.** +- The first frame of a level takes a little longer while its textures + upload: a brief dip in the FPS counter. +- **`Skip intro` and `All unlocked` are still experimental.** +- Far objects almost hidden in fog can show as a faint outline with + straight edges (e.g. the satellite dish seen from Surface's start area). + The N64's 16-bit colour hid it (D503). +- The full list, with workarounds, is the + [known-issues table](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/ROADMAP.md#known-issues). --- @@ -290,58 +196,57 @@ in-game **F10 overlay**), with the current default. | File | Platform | |---|---| -| `goldeneye-pc-port-0.4.0-win64.zip` | Windows x86-64 | -| `goldeneye-pc-port-0.4.0-linux-x86_64.tar.gz` | Linux x86-64 (incl. Steam Deck) | +| `goldeneye-pc-port--win64.zip` | Windows x86-64 | +| `goldeneye-pc-port--linux-x86_64.tar.gz` | Linux x86-64 (incl. Steam Deck) | Each contains the engine executable, a README, license texts, and the -`prepare-assets` tool. **No ROM, no game assets.** The Windows bundle -carries its runtime DLLs; the Linux bundle carries SDL2, so on both -platforms nothing needs to be installed first. +one-time asset tool. **No ROM, no game assets.** The Windows bundle carries +its runtime DLLs and the Linux bundle carries SDL2, so nothing needs to be +installed first. ## Running it -You supply your own **GoldenEye 007 N64 ROM** (`.z64`, big-endian) that you -legally own (NTSC-U only, as noted above). No Python, no toolchain, nothing -to install. +You need your own **GoldenEye 007 N64 ROM** (US version, `.z64`). 1. Unpack the archive. 2. Make a `data/` folder next to the executable and put the ROM in it, named `ge007.ntsc-final.z64`. -3. Run the executable **from that folder**. The first run takes a few extra - seconds: it detects your ROM, generates the two derived asset folders - (`data/pcmodels-ntsc-final/`, `data/pccg-ntsc-final/`) once, and saves - them for every future run. (The generator is - `prepare-assets/ge007-convert` inside the bundle; you can also run it - manually; it prints what it's doing.) +3. Run the executable from that folder. The first start takes a few extra + seconds while it generates its asset files from your ROM. -**Steam Deck:** sideload the unpacked folder (SFTP, USB or a file manager), do -steps 2–3, then add the executable to Games → *Add Game* as a non-Steam -game. The first launch in Game Mode picks up the Deck preset (native -1280×800, 2× MSAA, vsync) automatically if no config exists yet. +**Updating from an earlier version:** unpack into a new folder and copy your +ROM, `ge007.eep` (your progress) and `ge007.ini` (your settings) into its +`data/` folder. Don't copy the old `pcmodels-*` / `pccg-*` folders: the new +version makes its own. -**In-game settings on the Deck:** the options overlay is fully -gamepad-driven: it opens with **Select**, the D-pad or left stick (up/down) -moves between options, **A** steps the selected option forward, **B** steps -it back, and **Start** (or Select again) closes. No keyboard needed. +**Steam Deck:** do the steps above on the Deck, then add the executable as a +non-Steam game. The options overlay opens with **Select** and is fully +controller-driven. -Full steps are in the bundled `README.md`. +Full steps, controls and troubleshooting are in the bundled `README.md`. ## Verify the download ``` -sha256sum -c goldeneye-pc-port-0.4.0-win64.zip.sha256 -sha256sum -c goldeneye-pc-port-0.4.0-linux-x86_64.tar.gz.sha256 +sha256sum -c goldeneye-pc-port--win64.zip.sha256 +sha256sum -c goldeneye-pc-port--linux-x86_64.tar.gz.sha256 ``` +## Thanks + +dolent (#120, #121, #122), italoarruda (#107, #109), MST246 (the #125 +investigation and handoff used for the fix), JosephAHK (#133), MistaEcho +(#119 via #87), and everyone who filed #92, #114–#119, #125, #126, #127, +#129, #130 and #136. + ## Source & docs , built on the -[GoldenEye 007 decompilation](https://github.com/n64decomp/007), -architecture after the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark). +[GoldenEye 007 decompilation](https://github.com/n64decomp/007), with its +architecture modelled on the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark). Non-commercial fan preservation/research project; not affiliated with any rights holder. **AI disclosure:** built through agentic AI coding (Claude Code + a local open-weight model), directed by one person in their spare -time — as much a study of what agentic development gets wrong on a -game-sized codebase as it is a port. See the README's -[Background section](https://github.com/jkdansereau/goldeneye-pc-port#background) -for the full account. +time. See the README's +[How it was made](https://github.com/jkdansereau/goldeneye-pc-port#how-it-was-made) +section for the full account. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29148736..b567f845 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -349,6 +349,12 @@ jobs: - name: Build (ntsc-final) run: ./build-pc.sh ntsc-final + # D441 ratchet: fail if the per-file count of 64-bit pointer/int ABI + # warnings rises relative to tools_pc/abi_baseline.json. Re-baseline + # locally with `python tools_pc/abi_ratchet.py build-pc --update`. + - name: ABI warning ratchet (D441) + run: python tools_pc/abi_ratchet.py build-pc + - name: ccache stats run: ccache -s diff --git a/CMakeLists.txt b/CMakeLists.txt index fe0c9500..59ed90a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -421,7 +421,26 @@ endif() # Absolute symbols inside the reserved N64-DRAM region (port/src/dram.c): # cfb_16 (replaces src/cfb.c) and _bssSegmentEnd (mempool start marker). -set(SRC_DRAM_SYMS "${CMAKE_SOURCE_DIR}/port/src/dram_syms.s") +set(SRC_DRAM_SYMS "${CMAKE_SOURCE_DIR}/port/src/dram_syms.S") + +# D441 high-arena test flavour: relocate the DRAM arena for test builds +# (tier 1 = 0x90000000). Build option only — the default build is unchanged. +# GE_HIGHARENA_BASE=0x90000000 ./build-pc.sh ntsc-final +# or -DGE_HIGHARENA_BASE=0x90000000 at configure time. The value must be a +# C/asm integer literal; it overrides PORT_DRAM_V1_BASE in portaddr.h for +# C, CXX and ASM (dram.c's mapping and the dram_syms.S absolute symbols both +# follow it). +set(GE_HIGHARENA_BASE "" CACHE STRING "Override PORT_DRAM_V1_BASE for test builds (high-arena flavour)") +if(NOT GE_HIGHARENA_BASE) + set(GE_HIGHARENA_BASE "$ENV{GE_HIGHARENA_BASE}") +endif() +if(GE_HIGHARENA_BASE) + message(STATUS "GE_HIGHARENA_BASE = ${GE_HIGHARENA_BASE}") + set(_d441_arena_def "-DPORT_DRAM_V1_BASE=${GE_HIGHARENA_BASE}") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${_d441_arena_def}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${_d441_arena_def}") + set(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} ${_d441_arena_def}") +endif() # Game code — compiled unmodified. file(GLOB_RECURSE SRC_GAME "${CMAKE_SOURCE_DIR}/src/game/*.c") diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5237893d..975e9d14 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,6 +32,25 @@ keep the port faithful to the original game. clean configure + build for `ntsc-final` and a crash-free run of one level (`./build-pc/ge007.x86_64 -level_09`). +5. **No game data, ever.** Never commit, upload or attach a ROM or anything + extracted from one (textures, audio, models, level data, in-game text, + the generated `data/pcmodels-*` / `data/pccg-*` folders) in a pull + request or an issue. A small cropped screenshot of a glitch is fine. + +## Bug reports + +Open an [issue](https://github.com/jkdansereau/goldeneye-pc-port/issues/new/choose) +with: + +- `ge007.crash.log` if the game crashed (in the folder you launched from), +- the output of `ge007.x86_64 --version` (or the release version you + downloaded), your OS, and your GPU and driver version, +- your ROM's SHA-1 (Windows: `certutil -hashfile SHA1`, Linux: + `sha1sum `), +- what you did, what happened, and what you expected. + +**Never attach the ROM itself.** + ## Style - C/C++ formatting follows `.clang-format` and `.editorconfig` in the repo diff --git a/README.md b/README.md index 9c51c3fb..4c36bc82 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,43 @@ # GoldenEye 007 PC Port [![CI](https://github.com/jkdansereau/goldeneye-pc-port/actions/workflows/ci.yml/badge.svg)](https://github.com/jkdansereau/goldeneye-pc-port/actions/workflows/ci.yml) +[![Latest release](https://img.shields.io/github/v/release/jkdansereau/goldeneye-pc-port?include_prereleases&label=download)](https://github.com/jkdansereau/goldeneye-pc-port/releases) +![Platforms](https://img.shields.io/badge/platforms-Windows%20%7C%20Linux%20%7C%20Steam%20Deck-blue) ![license](https://img.shields.io/badge/license-MIT-ffb454) -

GoldenEye 007 (Nintendo 64, 1997) on the PC — -decompiled, ported, and playable at 60 fps.

+**GoldenEye 007 (Nintendo 64, 1997) as a native PC game for Windows, Linux +and Steam Deck.** Native widescreen, a steady 60 fps, 2–4 player +split-screen, mouse and keyboard or a modern controller, rebindable controls +and an in-game options menu. No +emulator: the original game's own code, rebuilt from the +[GoldenEye 007 decompilation](https://github.com/n64decomp/007), running from +your own ROM. -[Download](#download) · [News](#news) · [Status](#status) · [Roadmap](#roadmap) · [Building](#building) · [Docs](#documentation) · [Legal](#legal) +**[Download the latest release](https://github.com/jkdansereau/goldeneye-pc-port/releases)**, +then follow the [Quick start](#quick-start). -A native PC port of _GoldenEye 007_ (Rare, 1997, Nintendo 64), compiled from -the [GoldenEye 007 decompilation](https://github.com/n64decomp/007): the -original N64 game running from reconstructed source, not the Xbox 360 -remaster. The N64's graphics coprocessor (RSP) is emulated in software; every -other hardware surface (video, audio, input, timers, save storage) is shimmed -in a dedicated `port/` layer, following the architecture of the -[Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark), the same -Rare "Indy" engine family, one hardware generation apart. - -**v0.4.0** (released 2026-09-28) is out for Windows and Linux (including -Steam Deck, where the Linux bundle sideloads as-is). It is the most complete -release to date: the full campaign runs at a steady 60 fps with the known -issues now few and mostly cosmetic ([Status](#status)). Free to download, -build on and modify (you bring the ROM). - -**This is a pre-1.0 release, not a finished product.** v1.0 is the target -for a polished, feature-complete build; until then, expect missing features -and the occasional breaking change between versions. See [Status](#status) -for what works today and [Roadmap](#roadmap) for where this is headed. - -**AI disclosure:** development here was agentic - Claude Pro plus a local -open-weight model on a single RTX 5090, as of August–September 2026. This -project is as much a study of *that process* as it is a port: whether -current LLMs can carry a codebase like this, and what actually goes wrong -along the way. Judge the result for yourself. I'm one person doing this in my -spare time, not a team. See [Background](#background) for the full setup, -timeline, and an honest account of what worked and what didn't. +[Quick start](#quick-start) · [Controls](#controls) · [Features](#features) · [Settings and files](#settings-and-files) · [Troubleshooting](#troubleshooting) · [Roadmap](#roadmap) · [Building](#building-from-source) · [Legal](#legal) > [!IMPORTANT] -> **You must supply your own GoldenEye 007 ROM.** This repository contains no -> Nintendo code or assets, and no ROM. Nothing here is distributable as a -> playable game: see [Requirements](#requirements) and [Legal](#legal). +> **You must supply your own GoldenEye 007 ROM.** This repository and its +> downloads contain no Nintendo code or assets, and no ROM. See +> [What's a ROM?](#whats-a-rom) and [Legal](#legal). + +**v0.4.1** is the current release. It is a **pre-1.0 release, not a +finished product**: the campaign and split-screen multiplayer play end to +end, but expect missing pieces (PAL/JP ROMs, macOS) and the occasional +breaking change between versions. v1.0 is the target for a feature-complete +build. + +**AI disclosure:** this port was developed agentically, with Claude Code and a +local open-weight model, directed by one person in their spare time. The +project is as much a study of that process as it is a port. See +[How it was made](#how-it-was-made).

~24 s gameplay montage from live play sessions -
All in-engine, running in the port — a ~24 s gameplay montage +
All in-engine, running in the port: a ~24 s gameplay montage from live v0.4.0 play sessions, opening on the Runway tank · 12 level stills in the project index

@@ -53,177 +46,305 @@ timeline, and an honest account of what worked and what didn't. ## News +- **v0.4.1**: 2–4 player split-screen multiplayer, controller presets and + rebinding, PlayStation/Nintendo button names, master volume and audio + device selection, a steady 60 fps on low-end GPUs, and fidelity fixes + checked against the N64 game (AI visibility at long draw distances, sniper + zoom, turret pacing, Dam/Caverns water, weapons). + [Release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases). - **2026-09-28** — **v0.4.0**: native widescreen, a complete aim system for mouse and controller, in-game key rebinding, crosshair customization, rumble-pak haptics, a rebuilt options overlay, and a broad fidelity-fix pass (water, particles, billboard trees, front-end logo, gunshot SFX, a - true stable 60 fps). [Release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.4.0) · - [downloads](#download). + true stable 60 fps). [Release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.4.0). - **2026-09-20** — **v0.3.0**: the first release with the complete campaign playable end to end at 60 fps on Windows, Linux, and Steam Deck. [Release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.3.0). - **2026-09-04 → 2026-09-16** — **v0.1.0 – v0.2.2**: the alpha and beta - cycle — build chain, software RSP, first rendered frames, front end, and + cycle: build chain, software RSP, first rendered frames, front end, and per-level stabilization across the campaign. --- -## Download +## System requirements -| Platform | Bundle | Notes | -|---|---|---| -| **Windows** (x86_64) | [win64.zip](https://github.com/jkdansereau/goldeneye-pc-port/releases) | Engine + runtime DLLs + the one-time asset tool. | -| **Linux** (x86_64) / **Steam Deck** | [linux tarball](https://github.com/jkdansereau/goldeneye-pc-port/releases) | SDL2 is bundled, so it runs as-is on any distro, and sideloads onto a Deck with nothing installed. | +**Windows:** Windows 10 or 11, 64-bit. Nothing else to install: the +download carries its own runtime libraries and the one-time asset tool (no +Python, no emulator). -Both bundles contain **no ROM and no game assets**: you supply your own -(see [Requirements](#requirements)), which keeps the release legal to -distribute. Earlier builds: v0.3.0, v0.2.2, v0.2.1, v0.2.0, and v0.1.0 -alpha, same page. You can also build it yourself; see [Building](#building). +**Linux / Steam Deck:** 64-bit x86 Linux. SDL2 is bundled, so the tarball +runs as-is; it sideloads onto a Steam Deck with nothing installed. -### Quick start +**Both:** a GPU with OpenGL 3.0 drivers (practically any PC GPU from the last +fifteen years; keep the driver up to date). A 2011 laptop with Intel HD +3000 graphics holds 60 fps; on very weak integrated GPUs (Atom/Celeron class) +the first launch lowers draw distance and anti-aliasing to keep up. A +keyboard and mouse, or a controller (Xbox, PlayStation, Switch and most +SDL-supported pads); split-screen needs one controller per extra player. -You need a GoldenEye 007 N64 ROM (`.z64`, big-endian). This release supports -the **NTSC-U (US)** version; PAL and JP are on the roadmap ([issue -#85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)). No ROM or game asset is -included or distributed. Then: +**Not yet:** macOS, ARM Linux and Windows 7 are on the [roadmap](#roadmap). -1. Download the Windows or Linux bundle from [Releases](https://github.com/jkdansereau/goldeneye-pc-port/releases) and unpack it. -2. Make a `data/` folder next to the executable and drop the ROM in as `ge007.ntsc-final.z64`. -3. Launch the executable from that folder. The first run takes a few extra seconds: it detects the ROM and generates the derived asset folders once (no Python or other tooling needed). +## Quick start -Read the [Status](#status) section first: the small number of known issues -in this release are listed plainly there. +1. From [Releases](https://github.com/jkdansereau/goldeneye-pc-port/releases), + download the **win64.zip** (Windows) or the **linux tarball** (Linux and + Steam Deck), and extract it. On Windows, never run the exe from inside the + zip. +2. Make a `data/` folder next to the executable and put your ROM in it as + `ge007.ntsc-final.z64`. Only the **US (NTSC-U)** ROM works today; see + [What's a ROM?](#whats-a-rom) to check yours. +3. Launch the executable from that folder. Windows may show "Windows + protected your PC" (SmartScreen, because the exe is not code-signed): + click **More info**, then **Run anyway**. +4. The first start takes a few extra seconds: the port reads your ROM and + generates its asset files once. After that it starts normally. +5. Play. **F10** (pad: **Select**) opens the options overlay; the + [Controls](#controls) table lists the default keys. + +**Steam Deck:** the Linux tarball is the Deck build. Download it on the Deck +(or copy it over from your PC), extract it, add your ROM to `data/`, launch +it once, and add the executable as a non-Steam game. On first start it +applies Deck-friendly display defaults (native 1280×800 fullscreen); if the +resolution is wrong, set it under F10 → *Resolution*. --- +## Controls + +| Action | Keyboard / mouse | Controller | +|---------------------|--------------------------|---------------| +| Move / strafe | `W` `A` `S` `D` | Left stick (or D-pad) | +| Move / turn | Arrow keys | — | +| Aim / look | Mouse | Right stick | +| Fire | Left mouse | Right trigger | +| Aim mode | Right mouse / `LShift` | Left trigger / LB | +| Use / interact | `E` | A | +| Reload | `R` | X | +| Crouch | `LCtrl` | Left or right stick click | +| Cycle owned gadgets | Watch inventory | B | +| Next weapon | Mouse wheel down / `Q` | Y | +| Previous weapon | Mouse wheel up | — | +| Start | `Enter` / `Tab` | Start | +| Options overlay | `F10` | Select | + +The keyboard defaults are a GEPD-style layout. The controller layout matches +Rare's Xbox 1.1 (Jinx) button roles; the other Xbox styles (1.2 Christmas, +1.3 Frost, 1.4 Elektra, which swap the stick roles) are selectable under +*Input → Controller → Layout preset*. PlayStation and Nintendo controllers +show their own button names in the menus. + +**Rebinding:** *Input → Bindings…* in the options overlay rebinds the +keyboard and mouse (*Movement…* and *Actions…*). Controller buttons are +rebindable per controller under *Input → Controller*: set *Layout preset* to +*Custom*, select an action and press a pad button (tap B or Esc to cancel, +hold B to bind B itself, hold Back to clear; Y clears the selected slot). +Each action takes up to two buttons. Menus always keep A/X accept and B/Y +cancel, so you can't lock yourself out. + +**Split-screen:** pick Multiplayer from the main menu. With one controller, +keyboard/mouse is player 1 and the controller is player 2; with two or more +controllers, the controllers take players 1–4 (`Input.MPMode` in `ge007.ini` +changes this). A controller unplugged mid-match keeps its player when +plugged back in. + +**Options overlay with a controller** (also how it works on the Deck): +**Select** opens it, the D-pad or left stick moves between options, **A** +steps the selected option forward, **B** steps it back, and **Start** (or +Select again) closes. With a keyboard, use `F10` and the arrow keys/Enter. + +## Features + +- **2–4 player split-screen multiplayer** on every MP map, with the N64's own + setup flow and per-player HUD. +- **Native widescreen** at any aspect ratio: the world is undistorted, the HUD + sits at the screen edges, and 4:3 menus are pillarboxed. FOV and draw/LOD + distance are adjustable, and AI behaviour stays as on the N64 whatever you + set them to. +- **The original look in one click:** the *Original N64 preset* restores the + N64's settings, *Aspect → Original* shows the exact 4:3 frame, and the + *Port defaults preset* goes back. +- **A steady 60 fps**, with VSync and a frame cap (30 or 60), including on + low-end GPUs. +- **Graphics options:** resolution, borderless fullscreen, MSAA (up to 16×), + anisotropic filtering, and nearest, bilinear, trilinear or the N64's own + 3-point texture filter. +- **Mouse aim** in your choice of style (the N64's, or centred FPS-style) with + per-device sensitivity, smoothing and Y-inversion. +- **Modern controller support:** dual-stick layout, deadzones, sensitivity, + southpaw, trigger thresholds, rumble-pak vibration, and PlayStation/Nintendo + button names. +- **Rebindable controls** for keyboard, mouse and each controller. +- **HUD and crosshair options:** HUD scale, an optional customizable + crosshair, and a no-hit-flash option. +- **Full audio:** in-level music and sound effects, with music, FX and master + volume and a choice of output device. +- **An in-game options overlay** (F10 / pad Select) for all of the above, + saved automatically. +- **Faithful N64 progression by default.** Opt-in extras: *Skip intro* and + *All unlocked* (every level, 007 mode and the full cheat menu, without + touching your save; both experimental). +- **Saves in a plain file** next to the game, so backing up is a file copy. + ## Status -**v0.4.0 - fully playable, with a small set of known caveats.** The full -single-player campaign is completable end to end (all 20 missions, Agent -difficulty, playtested), at a steady 60 fps; all 20 solo missions — plus -the end-of-campaign credits sequence — load, render and run crash-free, -verified on Windows, Linux and real Steam Deck hardware. The most common -defect classes from earlier releases — particle colour drift, water seams, -z-fighting, billboard trees, muzzle flashes, the front-end Nintendo logo, -gunshot SFX — are fixed in this version; what remains is a short list, -below. Feedback is very welcome. - -What a release actually installs (no networking, no telemetry, no ROM or -game assets shipped) and how faithfully the port tracks the original N64 -game's logic: [Security & fidelity status](docs/security-and-fidelity-status.md). - -**Working:** boot sequence and front end (menu → mission select → briefing → -start), front-end menu navigation on the left stick to match the F10 overlay -(D282); all 20 solo missions load, render and are crash-free (full campaign -playtested end to end at Agent difficulty, including the ending sequence); steady 60 fps -(software RSP off the presentation critical path); full audio: in-level music and SFX; keyboard + mouse (click-to-lock, an aim style of your -choice — N64 or centred FPS-style — with per-device sensitivity) and a modern -dual-stick controller layout (use/reload/weapon-cycle on A/X/Y, rumble-pak -vibration on supported pads); native widescreen at any aspect ratio -(undistorted world, HUD anchored to the screen edges, 4:3 menus pillarboxed); -in-game key rebinding (keyboard/mouse) with a GEPD-style default layout; -crosshair customization (opt-in); Bond is fixed in cutscenes (no more -floating or spin-glitching) and his third-person model positioning generally -is right the large majority of the time now, at most a small drift when off; file-backed saves; faithful N64 progression -by default (F10 → *All unlocked* opens every level, 007 mode and the full -cheat menu); F10 in-game options overlay (video, input, gameplay, HUD, -graphics, audio; frame cap, MSAA, filtering, FOV, sensitivity, key rebinding, -crosshair, vibration); Windows and -Linux, including Steam Deck. +**Fully playable, with a small set of known caveats.** All 20 solo missions +and the end-of-campaign credits load, render and run crash-free; the full +campaign was playtested end to end at Agent difficulty on Windows, Linux and +real Steam Deck hardware for v0.4.0. Split-screen multiplayer was played live +in 2P on every MP map and in 4P on Temple. + **Known issues:** see the **[known-issues table](docs/ROADMAP.md#known-issues)** -(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. +(what you'll notice, impact, workarounds). The short version: NTSC-U ROMs +only in the release packages, and no macOS/ARM builds yet. -> **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. +> ***All unlocked* never changes your save** since v0.4.1: it unlocks every +> level and cheat in memory only, and switching it off shows your real +> progress. Builds before v0.4.1 could write fake unlocks into the save, and +> those are not repaired; if you used it on an older build, keep a backup of +> `data/ge007.eep`. -### Steam Deck +What a release installs (no networking, no telemetry, no ROM or game assets) +and how faithfully the port tracks the original game's logic: +[Security & fidelity status](docs/security-and-fidelity-status.md). -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. 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 -identified and fixed (D253), and a separate intermittent SIGSEGV in heavy -firefights/terminal destruction (D255) and an audio-thread crash (D305) are -both fixed and live-verified on real hardware as of this release. +--- -**In-game settings on the Deck.** The options overlay is fully gamepad-driven: -it opens with **Select**, the D-pad or left stick (up/down) moves between -options, **A** steps the selected option forward, **B** steps it back, and -**Start** (or Select again) closes. Toggles flip, resolution / MSAA / -filtering cycle, sliders step in increments. With a keyboard attached the same -overlay is `F10` + arrows/Enter. +## Settings and files + +**Options overlay:** press **F10** (pad: **Select**) in game or in the menus. +Changes are saved automatically. Most settings live in `ge007.ini`; the +ones that come from the game's own watch menu (auto-aim, look ahead, sight +and ammo on screen, music and FX volume) are part of your save, as on the +N64. + +**Where files live:** everything stays inside the game's own folder. The port +never writes to `%APPDATA%`, the registry or any system location. + +| File | What it is | +|---|---| +| `data/ge007.ntsc-final.z64` | Your ROM (you put it there). | +| `data/ge007.eep` | Your game progress (the N64 save). | +| `data/ge007.ini` | Settings, written on first start. | +| `data/pcmodels-/`, `data/pccg-/` | Asset files generated from your ROM on first start. Safe to delete: they are made again on the next start. | +| `data/ge007.shaders` | A list of the graphics shaders the game used, compiled at startup to avoid stutter. Safe to delete. | +| `ge007.crash.log` | Written in the folder you launched from if the game crashes. | + +**`ge007.ini`** uses `[Section]` headers and `Key = value` lines. A few +common keys and their defaults: + +```ini +[Video] +Fullscreen = 0 +VSync = 1 +FpsCap = 60 +MSAA = 2 +TextureFilter = 1 +NativeWidescreen = 1 +FovScale = 100 +DrawDistance = 250 + +[Audio] +MasterVolume = 100 +``` + +`Fullscreen` 1 is borderless fullscreen; `TextureFilter` is 0 nearest, +1 bilinear, 2 the N64 3-point filter, 3 trilinear; `FovScale` and `DrawDistance` are +percentages of the original (the options overlay shows the field of view in +degrees). On Atom/Celeron-class GPUs the first launch writes lighter values +for draw distance, LOD distance and `MSAA`. Edit the file with the game +closed: it is rewritten on a clean exit, and comments are dropped. + +**Launch options:** `-fresh` wipes the save and settings before starting (a +clean-slate run). `--version` prints the build id. + +## What's a ROM? + +A ROM is a copy of the game cartridge's data, made with a cartridge dumper +from a cartridge you own. This project can't provide or link one. The port +needs the big-endian `.z64` format, and supports the US version today: + +| Region | ROM filename (in `data/`) | SHA-1 | Supported | +|--------|---------------------------|-------|-----------| +| NTSC-U (US) | `ge007.ntsc-final.z64` | `abe01e4aeb033b6c0836819f549c791b26cfde83` | yes | +| PAL (EU) | `ge007.pal-final.z64` | `167c3c433dec1f1eb921736f7d53fac8cb45ee31` | not yet ([#85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)) | +| NTSC-J (JP) | `ge007.jpn-final.z64` | `2a5dade32f7fad6c73c659d2026994632c1b3174` | not yet ([#85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)) | + +To check which file you have, compute its SHA-1: on Windows, +`certutil -hashfile ge007.ntsc-final.z64 SHA1`; on Linux, +`sha1sum ge007.ntsc-final.z64`. + +## Updating + +Extract the new release into a new folder, then copy your ROM, `ge007.eep` +and `ge007.ini` from the old `data/` folder into the new one. Don't copy the +generated `pcmodels-*` and `pccg-*` folders: the new version makes its own on first start, and +stale ones from an older version can cause glitches. + +## Uninstalling + +Delete the folder. The port writes nothing anywhere else, so that removes the +game, your save and your settings; back up `data/ge007.eep` first if you +want to keep your progress. + +## Troubleshooting + +- **The game says the ROM is missing or wrong:** check it is in `data/`, named + `ge007.ntsc-final.z64`, in `.z64` byte order, and that its SHA-1 matches the + US row in [What's a ROM?](#whats-a-rom). PAL and JP ROMs are not supported + yet. +- **Windows shows a SmartScreen warning:** the exe is not code-signed. Click + **More info**, then **Run anyway**. +- **Black screen or no window:** update your graphics driver; the port needs + OpenGL 3.0. +- **Wrong resolution on the Steam Deck:** F10 → *Resolution*. +- **Where are my saves?** `data/ge007.eep`. Copy it somewhere safe while the + game is not running to back it up. +- **The game crashes:** open an + [issue](https://github.com/jkdansereau/goldeneye-pc-port/issues/new/choose) + and attach `ge007.crash.log` (in the folder you launched from), the output of + `--version`, your OS, your GPU and your ROM's SHA-1. **Never attach the ROM + itself.** --- ## Roadmap The goal is a **feature-complete, faithful** port: every mode and region the -N64 cartridge ships, behaving 1:1 with the original. The full list of open -items, and which ones are waiting on a decision, lives in one place: -[`docs/ROADMAP.md`](docs/ROADMAP.md). In brief: +N64 cartridge ships, behaving 1:1 with the original, with modern options on +top that can always be switched back to the original look and feel. The full +list of open items, and which ones are waiting on a decision, lives in one +place: [`docs/ROADMAP.md`](docs/ROADMAP.md). In brief: -- **Split-screen multiplayer** (2–4 players), the largest missing N64 feature - ([#99](https://github.com/jkdansereau/goldeneye-pc-port/issues/99)). -- **PAL and JP ROM support**. NTSC-U is the only supported region today. -- The remaining small accuracy differences (audio timbre, a few cosmetic - rendering details). -- **macOS/ARM builds**. -- **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. +- **PAL and JP ROM support** in the release packages + ([#85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)). Both + regions already convert, build and boot from source. +- **macOS and ARM builds**. +- The remaining small accuracy differences, each checked against the N64 + game in an emulator. +- **Emulator saves**: a converter so saves from 1964, Project64 and other N64 + emulators can be moved to and from the port (they need converting today; a + straight copy is wiped). +- **1.0 sign-off**: a full campaign playthrough at more than one difficulty + plus a split-screen session, on every platform that ships. 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. -Not currently planned: co-op or other modes GE never shipped, online -multiplayer, ray tracing. If any of these matter to you, open an issue — it -helps prioritize. +game (LAN play, bots, co-op, HD textures, enhanced visuals, frame rates above +60) come after 1.0, and always off by default. Not planned: online +multiplayer over the internet, achievements or cloud saves, remake-scope +assets (new models, music or voice), and new movement mechanics. If any of +these matter to you, open an issue — it helps prioritize. ---- +## How it was made -## Beyond playing - -- **Tweak it**: `ge007.ini` and the F10 in-game overlay expose resolution, - frame cap, MSAA, texture filtering, FOV/draw distance, mouse feel, key - rebinding, crosshair and vibration; launch with `-fresh` for a clean-slate - run. -- **Read it**: [`docs/internals.md`](docs/internals.md) maps the - architecture and the software RSP; [`docs/porting-notes.md`](docs/porting-notes.md) - is the catalogue of N64→PC bug classes hit along the way. Game logic in - `src/` is unmodified decompilation; every hardware surface lives in the - MIT-licensed `port/` layer. -- **Mod it**: the port layer, build system and `tools_pc/` are yours to - extend (see [License](#license)); [`CONTRIBUTING.md`](CONTRIBUTING.md) has - the ground rules for getting changes in, and [`docs/dev/`](docs/dev/) is - the raw engineering record behind every fix. - ---- - -## Background - -The port was built by two coding agents, a local open-weight model -(`unsloth/Qwen3.8-27B-GGUF` on one RTX 5090, via the [pi](https://pi.dev/) -agent) doing the groundwork (build, boot chain, software-RSP integration, -asset pipeline, first frames), and **Claude Code** (Sonnet 5, Opus 5 for the -hardest bugs) joining for the collaborative phase (the 21-level sweep, the -ABI finding catalog, SDL input, front end), handing work back and forth -through shared written notes, directed by one person part-time. In short: -43 days (16 Aug – 28 Sep), 1005 commits, 286 findings root-caused and logged +The port was built by two coding agents, directed by one person part-time. +A local open-weight model (`unsloth/Qwen3.8-27B-GGUF` on one RTX 5090, via the +[pi](https://pi.dev/) agent) did the groundwork: build, boot chain, +software-RSP integration, asset pipeline, first frames. **Claude Code** +(Sonnet 5, Opus 5 for the hardest bugs) joined for the collaborative phase: +the 21-level sweep, the ABI finding catalog, SDL input, front end. The two +handed work back and forth through shared written notes. Up to v0.4.0: 43 +days (16 Aug – 28 Sep), 1005 commits, 286 findings root-caused and logged (`D1`–`D408`). The full write-up (timeline, handoff mechanism, effort breakdown, an honest @@ -232,8 +353,6 @@ The workflow itself: [`docs/dev-process.md`](docs/dev-process.md). To cite the project or its findings: [`CITATION.cff`](CITATION.cff) (GitHub's "Cite this repository" menu). ---- - ## How this differs from the other GoldenEye PC projects This is a native port of the original 1997 Nintendo 64 game, built from its @@ -249,43 +368,18 @@ 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.4.0 public release; full campaign playable at 60 fps (see [Status](#status)) | Playable full game | -| **Why it exists** | To run the *original* N64 game from source, and as a [case study in AI-agent collaboration](#background) on a hard low-level codebase | To get a playable PC release of the remaster | - -They answer a different question: how to get the *remaster* onto PC by -machine translation. This project answers how to get the *original 1997 -game* onto PC, running from its reconstructed source. +| **Status** | Pre-1.0 releases; full campaign and split-screen playable at 60 fps (see [Status](#status)) | Playable full game | +| **Why it exists** | To run the *original* N64 game from source, and as a [case study in AI-agent collaboration](#how-it-was-made) on a hard low-level codebase | To get a playable PC release of the remaster | --- -## Requirements +## Building from source -You need a GoldenEye 007 (Nintendo 64) ROM that you legally own, in -big-endian (`.z64`) format, matching one of: - -| Region | ROMID | ROM filename (in `data/`) | SHA-1 | -|--------|-------|---------------------------|-------| -| NTSC-U (US) | `ntsc-final` | `ge007.ntsc-final.z64` | `abe01e4aeb033b6c0836819f549c791b26cfde83` | -| PAL (EU) | `pal-final` | `ge007.pal-final.z64` | `167c3c433dec1f1eb921736f7d53fac8cb45ee31` | -| NTSC-J (JP) | `jpn-final` | `ge007.jpn-final.z64` | `2a5dade32f7fad6c73c659d2026994632c1b3174` | - -This release supports the US (NTSC-U) version; PAL and JP ROMs are -recognised by region but not yet supported (on the roadmap, [issue -#85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)). - -The port also relies on the decompilation's asset-extraction step, which pulls -the level, model, texture and music data out of your ROM at build time. That -step, too, requires your ROM and is part of [Building](#building). - ---- - -## Building - -Prerequisites: CMake >= 3.16, a C/C++ toolchain, SDL2, zlib, OpenGL, Python 3, -plus the decompilation's own build dependencies (an IRIX MIPS toolchain via -`qemu-irix`, used only for the one-time asset extraction). See -[`docs/building.md`](docs/building.md) for the full walkthrough and the -asset-extraction details. +Prerequisites: CMake >= 3.16, a C/C++ toolchain, SDL2, zlib, OpenGL and +Python 3, plus MIPS binutils, `make` and `git` for the one-time asset +extraction (no IDO/IRIX toolchain is involved; on Windows the extraction is +easiest under WSL). See [`docs/building.md`](docs/building.md) for the full +walkthrough and the asset-extraction details. ### Windows (MSYS2) @@ -299,7 +393,7 @@ git clone https://github.com/jkdansereau/goldeneye-pc-port.git cd goldeneye-pc-port # 1. extract assets from your ROM (see docs/building.md) # 2. build the port -./build-pc.sh ntsc-final # NTSC-U; PAL/JP engine builds compile, but their asset sidecars are not yet producible (issue #85) +./build-pc.sh ntsc-final # NTSC-U; PAL/JP: see docs/building.md ``` ### Linux @@ -316,71 +410,25 @@ cd goldeneye-pc-port ``` The executable is written to `build-pc/ge007.x86_64` (on Windows, -`build-pc/ge007.x86_64.exe`). - ---- - -## Running - -1. Create a `data/` directory in the repo root. -2. Put your ROM in it, named as in the table above - (e.g. `data/ge007.ntsc-final.z64`). -3. Run the executable from the repo root: - `./build-pc/ge007.x86_64`. - -Configuration is written to `ge007.ini` on first run; game progress -lives in `ge007.eep`. Launch with `-fresh` to wipe both before starting -(a clean-slate run for playtesting). - -### Default controls - -| Action | Keyboard / mouse | Controller | -|---------------------|--------------------------|---------------| -| Move / strafe | `W` `A` `S` `D` / arrows | Left stick (or D-pad) | -| Aim / look | Mouse | Right stick | -| Fire | Left mouse | Right trigger | -| Aim mode | Right mouse / `LShift` | Left trigger / LB | -| Use / interact | `E` | A | -| Reload | `R` | X | -| Crouch | `LCtrl` | Left or right stick click | -| Cycle owned gadgets | Watch inventory | B | -| Next weapon | Mouse wheel down / `Q` | Y | -| Previous weapon | Mouse wheel up | — | -| Start | `Enter` / `Tab` | Start | -| Options overlay | `F10` | Select (A toggles/steps; B backs; D-pad/stick adjusts sliders; Start closes) | - -The default layout is the GEPD-style preset on the keyboard and the standard -dual-stick scheme on controllers (left stick move, right stick look, -triggers fire/aim, A use, X reload, B gadget cycle, Y weapon cycle, stick -click crouch). The watch gadget cycle selects the next owned gadget in the -N64 inventory. The controller layout matches Rare's Xbox 1.1 (Jinx) button -roles (user-tested on a physical pad; D394), and the remaining Xbox schemes -(1.2 Christmas, 1.3 Frost, 1.4 Elektra) are planned as selectable presets -and not yet available. Keyboard → Bindings edits **keyboard/mouse only**. -Controller buttons are rebindable per controller under *Input → Controller* -(F10 overlay or the front-end PC Options): pick *Custom* and bind each -action by pressing a pad button (tap B or Back to cancel, hold Back to -clear, hold B to bind B). Menus always keep A/X accept and B/Y cancel. - -Mouse sensitivity, Y-inversion and the aim/turn split are tunable in the -F10 overlay (*Keyboard → Sensitivity*) or the `[Input]` section of -`ge007.ini`. - ---- +`build-pc/ge007.x86_64.exe`). To run a source build, create `data/` in the +repo root, put your ROM in it as in [What's a ROM?](#whats-a-rom), and run +`./build-pc/ge007.x86_64` from the repo root. ## How it works -The R4300 game code in `src/` is compiled completely unmodified; the -decompilation's control flow is ground truth. Everything that would touch N64 -hardware is redirected into `port/`: a **software RSP** (`port/fast3d/`, -adapted from the Perfect Dark port) that interprets the GBI display list the -game builds each frame and emits OpenGL, bypassing the RDP; libultra OS shims -(`port/src/libultra.c`, game threads as real host threads) that let the -game's own scheduler run and drive the software RSP inline; and SDL2/OpenGL/filesystem backends -for video, audio, input and storage. The 32→64-bit transition forces a small, -cataloged class of mechanical ABI-only edits to ROM-serialized structs -(pointer-width reconciliation); these change no behavior and are documented -individually. Where it diverges from the Perfect Dark port: GoldenEye's N64 +The game code in `src/` is the decompilation's, and its control flow is +ground truth. Everything that would touch N64 hardware is redirected into +`port/`: a **software RSP** (`port/fast3d/`, adapted from the Perfect Dark +port) that interprets the GBI display list the game builds each frame and +emits OpenGL, bypassing the RDP; libultra OS shims (`port/src/libultra.c`, +game threads as real host threads) that let the game's own scheduler run, +with the software RSP on its own render thread as the RSP was its own chip; +and SDL2/OpenGL/filesystem backends for video, audio, input and storage. +Edits inside `src/` are rare, marked `#ifdef PORT`, and each one is +documented: mostly mechanical 64-bit fixes (a struct or pointer field that +the 32→64-bit move misaligned or truncated), plus a few maintainer-approved +timing fixes where per-frame N64 code ran too fast at the PC's 60 fps (for +example sniper zoom and turret fire), and the opt-in *All unlocked* hook. Where it diverges from the Perfect Dark port: GoldenEye's N64 serialized asset formats are converted offline by Python "sidecar" converters in `tools_pc/` rather than fixed up at load time. Full detail: [`docs/internals.md`](docs/internals.md) and @@ -389,7 +437,7 @@ in `tools_pc/` rather than fixed up at load time. Full detail: ``` CMakeLists.txt PC build (parallel to the decomp's Makefile, which is untouched) build-pc.sh configure + build helper -src/ include/ the decompilation (game + libultra) - compiled unmodified +src/ include/ the decompilation (game + libultra); port edits are #ifdef PORT port/ fast3d/ software RSP -> OpenGL src/ port layer (main, OS shims, video, audio, input, fs, ...) @@ -399,8 +447,6 @@ tools_pc/ PC-port helper + analysis scripts docs/ see below ``` ---- - ## Documentation Key docs are also published as a site: @@ -408,14 +454,23 @@ Key docs are also published as a site: | Doc | What's in it | |---|---| +| [`docs/ROADMAP.md`](docs/ROADMAP.md) | The single tracker of open work and known issues. | | [`docs/building.md`](docs/building.md) | Full build + asset-extraction guide. | | [`docs/internals.md`](docs/internals.md) | Architecture, the RSP-emulation approach, GE-vs-PD engine differences, the phased plan. | | [`docs/porting-notes.md`](docs/porting-notes.md) | The recurring N64→PC bug classes hit during the port, with fixes. | | [`docs/dev/agentic-development.md`](docs/dev/agentic-development.md) | The research angle: the two-agent setup, timeline, handoff workflow, and an assessment of what did and didn't work. | | [`docs/dev-process.md`](docs/dev-process.md) | The investigation workflow in detail: budgets, file partitioning, the finding-log discipline. | -| [`docs/dev/`](docs/dev/) | The raw engineering record: the full finding log, per-level status, graphics backlog, playtest matrices, and [`docs/dev/game-behavior-reference.md`](docs/dev/game-behavior-reference.md) (a secondary-sourced playtest reference for how the retail game is meant to behave; repo-only, code is ground truth). | +| [`docs/dev/`](docs/dev/) | The raw engineering record: the full finding log, per-level status, playtest matrices, and [`docs/dev/game-behavior-reference.md`](docs/dev/game-behavior-reference.md) (a secondary-sourced playtest reference for how the retail game is meant to behave; repo-only, code is ground truth). | | [`docs/SetupGuide.md`](docs/SetupGuide.md), [`docs/StyleGuide.md`](docs/StyleGuide.md) | Inherited from the decompilation this repo forks. | +## Contributing and forking + +Issues and pull requests are welcome, and so are forks: the port layer, build +system and `tools_pc/` are MIT-licensed and exist to be extended. Read +[`CONTRIBUTING.md`](CONTRIBUTING.md) first: it has the ground rules that keep +the port faithful (game logic stays unmodified), what to verify before a +pull request, and what a fork is asked to keep. + --- ## Credits @@ -460,6 +515,12 @@ This port is a thin layer on a large amount of other people's work. Kids decompilation write-up), the agent-workflow practices in [`docs/dev-process.md`](docs/dev-process.md) are adapted from it. +**Contributors** + +- **dolent** (#120, #121, #122), **italoarruda** (#107, #109), + **JosephAHK** (#133), and **MST246** (the #125 investigation), plus + everyone who filed issues. + **Consultation** - **f1zz1ec0ke** ([GitHub](https://github.com/f1zz1ec0ke)): LLM consultation on model @@ -475,33 +536,47 @@ This port is a thin layer on a large amount of other people's work. - [pi](https://pi.dev/): the local coding-agent harness. - [Claude / Claude Code](https://claude.com/claude-code) (Anthropic). ---- - ## Legal This is a non-commercial fan preservation/research project, in the same category as the many other N64 decompilation and native-port repositories on -GitHub. It follows the same conventions they do: - -- **No ROM and no game assets are distributed**: not in this repository and - not in any release. Textures, audio, models, level data and in-game text are - extracted from a ROM *you already own*, on *your* machine, at build time. -- The repository is a fork of the public - [GoldenEye 007 decompilation](https://github.com/n64decomp/007) and inherits - its contents unmodified (see [`NOTICE`](NOTICE) for what that includes). -- No official logos, box art, or marketing assets are used. "GoldenEye 007", - "007", "James Bond" and related marks belong to their respective owners - (Nintendo, Microsoft/Rare, MGM, Danjaq, EON Productions). -- Pre-built binaries published under [Releases](https://github.com/jkdansereau/goldeneye-pc-port/releases) contain - only the engine (the `port/` layer plus the compiled decompilation, with no - game data of any kind), bundled with permissively-licensed runtime libraries - (SDL2, zlib, the MinGW runtime; their licenses travel in the download). Any - build, yours or ours, is useless without a ROM you supply. - -This project is **not affiliated with, endorsed by, or sponsored by** Nintendo, +GitHub. It is **not affiliated with, endorsed by, or sponsored by** Nintendo, Rare, Microsoft, MGM, Danjaq, EON Productions, or any rights holder in -GoldenEye or James Bond. If you are a rights holder with a concern, open an -issue and it will be addressed. +GoldenEye or James Bond. "GoldenEye 007", "007", "James Bond" and related +marks belong to their respective owners; they are used here only to say which +game this port runs. No official logos, box art, or marketing assets are used. + +### What the download contains + +- **The engine:** the `port/` layer plus the compiled decompilation. +- **Runtime libraries** under permissive licenses (SDL2, zlib, the MinGW + runtime); their licenses travel in the download. +- **The one-time asset tool**, which reads your ROM on your machine. + +It contains **no ROM and no game assets of any kind**. Any build, yours or a +release, is useless without a ROM you supply. + +### What happens on your computer + +Textures, audio, models, level data and in-game text are extracted from a ROM +*you already own*, on *your* machine, the first time you start the game. The +port has no network access and never uploads anything. + +### This repository + +The repository is a fork of the public +[GoldenEye 007 decompilation](https://github.com/n64decomp/007) and inherits +its contents (see [`NOTICE`](NOTICE) for what that includes). The few +port-specific edits inside the decompiled code are marked `#ifdef PORT`. + +### Your part + +- Only use a ROM that you dumped from a cartridge you own. +- Never ask for, link to or share ROMs or extracted game files in issues, + discussions or pull requests. + +If you are a rights holder with a concern, open an issue and it will be +addressed. ## License @@ -512,4 +587,4 @@ is covered by [`NOTICE`](NOTICE), not by that license. --- -*Last updated 2026-09-28 — v0.4.0 · GitHub · Releases* +*Last updated 2026-10-03 — v0.4.1 · GitHub · Releases* diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 3252a3d6..e1789b61 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -93,6 +93,19 @@ Work continues on `release/v0.4.1`. When to cut it, and what to call it 3. The exit gate on every shipped platform. 4. Remove the temporary scaffolding (§7). +**Next (decided 2026-10-03, after the Birdman64 comparison;** findings and +lessons in the maintainer's local note `BIRDMAN64-LESSONS.md`**):** +1. D441: finish the census as one complete, lead-verified list, compiler + pass in both directions plus the compiler-invisible classes (§2). + **Done 2026-10-03.** +2. Fix the listed sites as mechanical, build-checked batches on a cheap + worker in its own worktree; the lead reviews every diff (§7). + **Batches 1-5 done 2026-10-03** (35 sites, driven by the tier-1 + high-arena build's crash order). Next: the tier-1 sweep over every + level with a seeded frame compare, then further batches from it. +3. Scripted-playthrough harness: a feasibility spike first. Aiming, + shooting, guards and timed events may block open-loop scripts (§7). + ## Known issues What a player will notice in the current release. **This table is the @@ -103,16 +116,12 @@ below). | Issue | Impact | Workaround | Tracking | |---|---|---|---| -| Multiplayer isn't available yet | Missing feature | **Fixed on `release/v0.4.1`** (2–4 player split-screen, merged 2026-10-01); delete this row when it ships | #99, §1 | -| PAL and JP ROMs aren't supported | NTSC-U only | Use an NTSC-U ROM | D258, §1 | -| `All unlocked` may already have written fake unlocks into a save from an older build | Save corruption | **Fixed on `release/v0.4.1`**: `All unlocked` is now a RAM-only override (D442) and never writes the save. Fakes an older build already wrote are not repaired; back up `data/ge007.eep`. Delete this row when it ships. | D442, D387, §5a | -| `Game.SkipIntro` also skips the post-mission failure dossier | Minor | **Fixed on `release/v0.4.1`** (D408, maintainer-verified 2026-10-02); delete this row when it ships | D408 | -| F10 overlay stretches in native widescreen (menus pillarbox correctly) | Cosmetic | **Fixed on `release/v0.4.1`** (D472 pillarboxes the overlay card); delete this row when it ships | D335b, D472, §5a | -| Rareware logo shows a subtle texture-filtering artifact | Cosmetic | **Fixed on `release/v0.4.1`** (D75: the RAREWARE text was scrambled by a missing RDP odd-row TMEM swap; maintainer-verified 2026-10-02); delete this row when it ships | D75 | -| 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 | **Fixed on `release/v0.4.1`** (D481 render worker; an Intel HD 3000 laptop went from ~45 to 60 fps; D482 lowers draw/LOD distance and MSAA once on Atom/Celeron-class GPUs). Not yet confirmed on N3060-class hardware. Delete this row when it ships | D339, D481, D482, #92, §4 | +| PAL and JP ROMs aren't supported in release packages | NTSC-U only | Use an NTSC-U ROM. Both regions convert, build and boot from source (D258, D487, D488; see `docs/building.md`); packaging (per-region exe, CI matrix, converter region check) is still open | D258, #85, §1 | | No macOS or ARM builds | Platform | — | #88, #95, #101, §4 | -| Controller buttons can't be rebound in-game (keyboard/mouse can) | Missing feature | **Fixed on `release/v0.4.1`** (D469, F10 → Input → Controller); delete this row when it ships | D469, #109, §5a | +| Saves from builds before v0.4.1 can hold fake unlocks from `All unlocked` | Save data | Not repaired automatically. Since v0.4.1 the option never writes the save; keep a backup of `data/ge007.eep` from before you used it | D442, D387 | +| `Skip intro` and `All unlocked` are experimental | Opt-in options | Leave them off for a normal playthrough | D408, D442 | +| The first frame of a level takes a little longer while its textures upload | Brief FPS-counter dip | None needed | D475, D480 | +| Far objects almost fully hidden in fog can show as a faint silhouette with straight vertical edges (e.g. Surface's satellite dish from the start area). The N64's 16-bit colour hid the 2-5 level difference; the port's 32-bit output shows it | Cosmetic, any settings | None; raising Draw/LOD distance makes more such objects visible | D503, §5c | --- @@ -120,7 +129,7 @@ below). | Item | Status | Refs | Notes | |---|---|---|---| -| **2–4 player split-screen multiplayer** | verify | #99, D416–D423, merged into `release/v0.4.1` 2026-10-01 (from `mp/virtual-seats`) | Merged 2026-10-01. D417 Rule-2 approved. Maintainer live test: every MP map in 2P (2–4 kills per map, each player scored), 4P Temple, two Xbox pads mapped P1/P2. Headless 2P/3P/4P pause, watch-menu pages, exit-confirm and F10-overlay runs clean. D420 fixed (Scores overrun), D421 closed (moot: no MP gadgets), D423 closed (faithful shadow). Still owed: 3P/4P with real pads, pads-only mode, MP audio, the full N64 MP setup screens (scenarios, handicaps, teams), D422 centred aim live on P2+ (`AimMode = 1`), D428 sway on P2+; the harness-only missing ammo counter was the `GE_STARTMP` shortcut skipping file-select's `selected_folder_num = -1` (fixed, D479). Per-pad seats (§5) done (D448/D450). | +| **2–4 player split-screen multiplayer** | verify | #99, D416–D423, D496 | Merged 2026-10-01. D417 Rule-2 approved. **Maintainer playtest 2026-10-03:** 2P and 3P with real Xbox and PS5 pads, handicaps, teams and game modes all good; the short 3P/4P pickup text is the game's own (D496 fixed an uninitialised buffer behind the weapon text). Earlier: every MP map in 2P, 4P Temple. **Still owed:** 4P with real pads, pads-only mode, MP audio, D422 centred aim live on P2+ (`AimMode = 1`), D428 sway on P2+. | | **PAL and JP ROM support** | partial | D258, D487, #85 | **2026-10-02: both regions convert, build and boot** (D258 fixed and byte-verified against real PAL and JP ROMs; region-aware emitters and ROM detection; PAL link fix). Both play Dam with no crash; NTSC-U unchanged (`verify.sh dam` PASS). Build: `BUILD_DIR=build-pal ./build-pc.sh pal-final` / `build-jpn ... jpn-final`, with `data/ge007..z64` and the emitters run for that region. **D487 fixed 2026-10-02** (PAL frame height decoded with the NTSC rule cut the bottom ~12% of every PAL frame). **D488 fixed 2026-10-02** (PAL build now reports the PAL TV type, not Brazil's MPAL). **Open:** full runtime passes per region (all levels, menus, audio, saves; PAL 50 Hz feel incl. the D427/D451/D486 2-ticks/frame gates; EU-only text paths); packaging (per-region exe names, CI matrix, drop-in converter region gate, #6). ROMs: `ge-port-reference/1964_GEPD_Edition/ROMs` (JP .n64 = v64, PAL `.z64`, both hash-verified). #85 needs reopening or a fresh tracker when this ships. | | 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. | @@ -130,14 +139,14 @@ below). | Item | Status | Refs | Notes | |---|---|---|---| | Third-person Bond offset residual | partial; intro puppet explained (2026-10-02) | D173, D292 | Mostly fixed. On the level-start puppet the variance is tick/PRNG-dependent idle timing (D13 class, faithful, no sign-off requested); one 1-frame first-tick Y transient is untraced. If the residual is seen elsewhere (cutscene body model), the maintainer should note where. | -| Turret fire rate, explosion density and shake at 60 fps | verify | #126, D486 | **Fixed 2026-10-02 (D486, RULE-2):** autogun shots/tracers, explosion-part spawning and shake decay were counted per frame (2-3x the N64 at 60 fps); now gated to the N64 2-ticks/frame pace (`portN64FrameStep`), damage per second unchanged; headless 60-vs-30 fps A/B identical. The dropped explosions themselves are faithful (1964/GEPD drops them the same way), so #126 closes as matching the N64. **Owed:** a turret fight at 60 fps (Cradle/Aztec) feels like 1964: fire rate, tracers, sound, shake; damage taken unchanged. | | Dam ending cutscene: Bond placement; railing transparency | open | D173, D292; maintainer 2026-10-02 | **Maintainer to re-check the end of Dam** (the last place a Bond offset was seen; the D173 level-start probe found no defect). Also seen in the Dam cutscene: an edge railing texture (a 2D element, maintainer believes) with odd transparency; possibly already fixed by the alpha pass. Capture a screenshot of each if still present. | -| Gameplay visibility vs modern view options (FOV, ultrawide) | verify | D466, D468, D334, D222; maintainer's local policy note | **Policy approved 2026-10-01 and implemented:** AI awareness may see what an N64 player could have seen in some configuration the cartridge supports. D466: the AI uses the authored draw distance (the player can still shoot, use and photograph whatever is drawn). Widescreen up to 16:9 is faithful (the cartridge's own Ratio 16:9 mode). **D468 (merged 2026-10-01):** under ultrawide or `Video.FovScale` > 100 the AI's on-screen view is clamped to the 16:9 / game-FOV view; option `Game.AIWideView` (F10 Gameplay, default off) gives the AI the full view. **Owed:** by-eye check on an ultrawide window or at a high FOV. D466 follow-up (2026-10-01, maintainer decision): movement/locomotion LOD follows the render, only awareness uses the N64 distance. | -| Sniper rifle: can't crouch while using it; zoom feels slow | verify | #136, D483, D484, D485 | **Fixed 2026-10-02 on `release/v0.4.1`.** Crouch: partly faithful (the sniper's DISABLE_CROUCH flag blocks stance changes on the N64); the port's divergence (the PC crouch key stood you up on switching to the sniper) is fixed by **D483**. Zoom level: the port's FOV floor capped full zoom at 20 degrees instead of 7, fixed by **D484**. Zoom speed: per-frame stepping made the port zoom 2x faster than the N64; now tick-scaled (**D485**, Rule-2 approved). Owed: 1964 A/B of zoom level/speed and crouch-then-switch. Keep #136 open until the maintainer's 1964 check. | +| Dam: first-tower guard walks a small circle (glass collision polygon) | done | D490 | **Fixed 2026-10-02, merged into `release/v0.4.1` 2026-10-03:** a stack overflow in `sub_GAME_7F03ECC0` (flat-bbox objects, i.e. glass) corrupted the collision polygon; `rem[8]` under `AVOID_UB`. Headless probe: the guard now walks his whole patrol loop. Maintainer by-eye check in Dam passed 2026-10-02. The porting-notes D24 audit ran 2026-10-03 and found D491 (below); no other overflow of this kind. | +| Struct reads across adjacent globals: portal bbox seeds put props in extra rooms; casing spin seeded from trigger state | done | D491 | **Fixed 2026-10-03, merged into `release/v0.4.1`:** `bg.c` / `gunfire.c` read three separate scalar globals as one coord3d; mingw reverses their order. `#ifdef PORT` reads the named globals. Builds clean, 900-frame `-level_09` smoke clean. Nothing by eye is expected. Delete this row once released. | +| Sniper rifle: zoom level and speed vs 1964 | verify | #136, D484, D485, D494 | Crouch: **D494** (2026-10-03) makes the PC crouch key crouch/stand freely with the sniper, as GEPD does (superseding D483's freeze); maintainer-verified. Zoom level (D484, 20 -> 7 degrees) and speed (D485, tick-scaled, Rule-2) fixed 2026-10-02. **Owed:** 1964 A/B of zoom level and speed; close #136 after it. | | Crash on save slots 1/3/4, plus a host BSOD (fltmgr.sys) | decided | #94 | Needs a repro. Two maintainer retest requests (v0.3.0, v0.4.0) got no reply. Likely D299/D301 (save-slot crash, fixed) and D344 (BSOD on exit, fixed). **Decided 2026-10-01: close as stale when the release ships**, pointing at D299/D301/D344; no GitHub activity before then. | | Authored mip chains not sampled for explicit-LOD textures | post-1.0 | D323 | Distant textures use driver mips instead of Rare's hand-authored chain (heaviest on Statue/Aztec). Parked as a fidelity enhancement. **Decided 2026-10-01:** after 1.0, together with the LOD-fraction approximation as one "real RDP LOD" project. | | Approximations in the software RSP | post-1.0 | `gfx_pc.cpp` `G_CCMUX_LOD_FRACTION`; D323 | The LOD-fraction CC input is an eyeballed approximation, bundled with D323 as one "real RDP LOD" project after 1.0 (2026-09-30 triage). **WrapFix settled 2026-10-02:** the maintainer checked the Depot building ceilings against 1964 with the default `Video.WrapFix = 0` and they look right, so the default stays off (the opt-in knob remains; RC3/D167). | -| 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. **2026-10-01:** D457(c)(d) merged (player matrix pointers, Model/PROMOTE/vtxstore pointer width); #107's two sites fixed (D441); read-side census part 2 (403 `-Wint-conversion`/`-Wint-to-pointer-cast` warnings, triaged by file) in the local note `ABI-CENSUS-D441-PART2.md`; model.c rows need a re-run after D457(d). **Stage B plan (2026-10-01, maintainer's local design note):** (1) centralise the hard-coded arena address windows: **done 2026-10-02** (`port/include/portaddr.h`: `PORT_DRAM_V1_BASE` / `PORT_DRAM_K0_BASE` / `PORT_DRAM_SIZE`, used by `OS_K0_TO_PHYSICAL`, `dram.c`, `libultra.c` bases, `n64stubs.c`, the fast3d window checks; compile-time constants, no codegen change; menu / Bunker / Statue smoke clean). Left: the three link-time symbols in `port/src/dram_syms.s` (needs a preprocessed `.S` or `--defsym`), and a runtime-derived base for macOS; (2) widen `mema.c` (`memaspace.addr` s32 with `0xffffffff` sentinels, `memaRealloc`/`_memaFree` s32 params; the deferred D453 item); (3) build the high-arena test flavour (`GE_HIGHARENA_BASE`, tier 1 at 0x90000000, tier 2 above 4 GiB) and iterate on the first failures; (4) the heap-backed animation-table slots (`initanitable.c`); converges with #95's port_addr chokepoint. **2026-10-02 (D478):** read-side census re-run (319; 129 outside `chr.c` D34) triaged site by site: nothing live on the current layout; the remaining items are arena pointers in 32-bit slots and stack buffers through `s32` (safe only while stacks are low: `MAP_32BIT`, no ASLR on Windows), which is the Stage B / macOS list in the D478 entry. | +| 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) | **Next (2026-10-03): one complete census list** replacing parts 1/2 (part 3 was fabricated): re-run `scratch/d441p2/run.py` with `-Wpointer-to-int-cast -Wint-to-pointer-cast -Wint-conversion` (mingw is LLP64, so this already matches Birdman64's "long forced to 32 bits" pass), add the classes no compiler flags (raw `sizeof` copies of structs with pointer slots, union/raw-offset aliases, u32 addresses in display lists, bit-31/K0 tests, `&p[-u32]`, adjacent-global reads (D491), N64-stride allocations, s32 sentinels, endianness of serialized records), one row per site with provenance, store-to-u32 vs arithmetic-only, status and verifier. 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. **2026-10-01:** D457(c)(d) merged (player matrix pointers, Model/PROMOTE/vtxstore pointer width); #107's two sites fixed (D441); read-side census part 2 (403 `-Wint-conversion`/`-Wint-to-pointer-cast` warnings, triaged by file) in the local note `ABI-CENSUS-D441-PART2.md`; model.c rows need a re-run after D457(d). **Stage B plan (2026-10-01, maintainer's local design note):** (1) centralise the hard-coded arena address windows: **done 2026-10-02** (`port/include/portaddr.h`: `PORT_DRAM_V1_BASE` / `PORT_DRAM_K0_BASE` / `PORT_DRAM_SIZE`, used by `OS_K0_TO_PHYSICAL`, `dram.c`, `libultra.c` bases, `n64stubs.c`, the fast3d window checks; compile-time constants, no codegen change; menu / Bunker / Statue smoke clean). Left: the three link-time symbols in `port/src/dram_syms.s` (needs a preprocessed `.S` or `--defsym`), and a runtime-derived base for macOS; (2) widen `mema.c` (`memaspace.addr` s32 with `0xffffffff` sentinels, `memaRealloc`/`_memaFree` s32 params; the deferred D453 item); (3) build the high-arena test flavour (`GE_HIGHARENA_BASE`, tier 1 at 0x90000000, tier 2 above 4 GiB) and iterate on the first failures; (4) the heap-backed animation-table slots (`initanitable.c`); converges with #95's port_addr chokepoint. **2026-10-02 (D478):** read-side census re-run (319; 129 outside `chr.c` D34) triaged site by site: nothing live on the current layout; the remaining items are arena pointers in 32-bit slots and stack buffers through `s32` (safe only while stacks are low: `MAP_32BIT`, no ASLR on Windows), which is the Stage B / macOS list in the D478 entry. **2026-10-03 (D441):** complete census (424 compiler sites + 460 compiler-invisible grep hits, local `scratch/d441/`); Stage B item (3) tier 1 built (`GE_HIGHARENA_BASE=0x90000000`, `dram_syms.S`) and item (1)'s leftover link-time symbols done; 35 sign-extension sites fixed in 5 batches plus a fast3d `0x90000000` range literal; CI ratchet `tools_pc/abi_ratchet.py` (baseline 389). Tier 1 runs the menu and `-level_09` pixel-identical to the default build. **Still owed:** the tier-1 all-level sweep, tier 2 (above 4 GiB, needs the u32 slots widened, not zero-extension), the store-to-u32 tagging for macOS. | ## 3. Verification debt (fixed; a live check is owed) @@ -194,19 +203,16 @@ Background research lives in the maintainer's local notes (gitignored): | Item | Status | Refs | Notes | |---|---|---|---| -| "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 | verify | D442, D387, D259 | **Landed 2026-09-30 (D442)** on `release/v0.4.1`: query-time hooks in `file2.c` (`fileGetIsCheatUnlocked`, `fileIsStageUnlockedAtDifficulty`), RULE-2-SIGNOFF recorded; the EEPROM patch and the D387 merge are removed. **Owed:** every level at every difficulty with a cheat on; Magnum/Laser/Golden Gun rows; MP characters; OFF restores real progress. | -| Settings units and values cleanup | verify | D357, D443 | **Landed 2026-09-30 (D443):** mouse ×, whole-% deadzone and volume, FOV in horizontal degrees, MSAA 16×. FPS presets above 60 are deferred to the frame-rate-above-60 work (the sim ticks at 60 Hz). **Owed:** the new values in both settings UIs. | +| `All unlocked` rework: RAM override instead of save patching | done | D442, D387, D259 | **Landed 2026-09-30 (D442)** on `release/v0.4.1`: query-time hooks in `file2.c` (`fileGetIsCheatUnlocked`, `fileIsStageUnlockedAtDifficulty`), RULE-2-SIGNOFF recorded; the EEPROM patch and the D387 merge are removed. **Verified 2026-10-03 (headless, frame dumps):** ON = all 20 levels in mission select, every difficulty ticked (real menu path), cheat menu fully populated (all 23 incl. Magnum/Laser/Golden Gun); OFF = only the save's real progress (Dam); `ge007.eep` byte-identical across all runs. MP character list not captured (same COMPLETED query path). Delete this row once released. | +| Settings units and values cleanup | done | D357, D443 | **Landed 2026-09-30 (D443):** mouse ×, whole-% deadzone and volume, FOV in horizontal degrees, MSAA 16×. FPS presets above 60 are deferred to the frame-rate-above-60 work (the sim ticks at 60 Hz). **Verified 2026-10-03 (headless frame dumps of Input/Graphics/Audio in F10 and front PC Options):** both UIs show the same values in the new units (mouse 1.0x, deadzones 23%, volumes %, FOV 75 deg, MSAA list up to 16x). Vibration now shows whole % too (was a raw 0.50). The `N/100` sliders (mouse vertical, look sensitivity, distances, trigger) are the calibrated-default scheme (50 = default), kept. Delete this row once released. | | Restart button in the settings menus | done | D443 | **Landed 2026-09-30 (D443):** a "Restart game" row (orderly D344 quit, then relaunch). **Owed:** click it once on Windows (no BSOD); the Linux relaunch path has never been compiled. | -| Front-end PC Options: Previous/Next paging for mouse users | verify | D444 | **Landed 2026-09-30 (D444):** Previous/Next page buttons; hover no longer snaps the wheel selection. **Owed:** a mouse pass through Input and Graphics. | | Outside PRs #107–#109, #122–#124 | decided | #122 hot-plugged pads never reopen, #123 persistent crosshair (`gunfire.c`), #124 compact health/armour bars (`bondview2.c`); #107/#108/#109 (italoarruda) | **Decided 2026-10-01 after design reviews (notes: `docs/dev/notes/PR-REVIEW-109.md`, `PR-REVIEW-123-124.md`, local):** **#120/#121/#122** merged locally (#122 = D450, with split-screen); close all three when the branch is pushed. **#123** (persistent crosshair): accept-with-changes, **deferred**; later, either amend their PR or do the work ourselves (add to the Original N64 preset, relabel D436, our Rule-2 line, 2P + sniper-zoom check). **#124** (compact health/armour bars): **deferred** (pre- or post-1.0); too many issues to merge now: ignores HudScale, overlaps the bottom-left status text, 4:3-edge anchoring on ultrawide, draws extra commands when off, mixed apparent/real values, a test knob in shipping code, not in the Original N64 preset, split-screen untested. Either we take it on or ask the author to address the list when GitHub activity resumes. **#133** (F10 ultrawide aspect, JosephAHK, 2026-10-01): duplicates D472 (overlay card pillarboxed + mouse mapping, already on `release/v0.4.1`); close as superseded with thanks when the branch is pushed. **#109** (pad rebinding): **decline as-is**. It conflicts in all 4 files, would revert the D394 Jinx defaults, uses global (not per-seat) bindings, and its cancel is keyboard-only. Supersede with our own pad presets + rebinding (§5a row below), crediting the author's two-names format, menu lockout guard and release-before-capture arming (Co-authored-by); close with thanks and the change list when GitHub activity resumes. **#107**: folded into D441 on 2026-10-01 (both sites fixed with the author's `uintptr_t` arithmetic, Co-authored-by); close it on push. **#108**: belongs with `macos`; ask for a range check instead of widening the bound. Relabel any outside D-labels on merge. | -| Gamepad rebinding + modern controller presets | verify | D469 (supersedes PR #109), D394 | **Landed 2026-10-01 (D469):** data-driven pad action table; `Input.PadPreset` (0 = Jinx 1.1, default, proven identical to the old mapping over 20,971,520 synthetic cases; 3 = Custom); per-seat `Input.Pad[N].` bindings; F10 / front-end "Controller..." page with pad capture (B/Back cancel, hold Back / Y clear); menu lockout guard; credited to italoarruda (#109). **Maintainer live test 2026-10-01:** bind, cancel, clear, 2P per-seat tables, hot-plug and relaunch persistence pass; the Restart row (D443) works. **Still owed:** more play with the default preset ("plays as before") and longer controller-only use. **Not done:** Jinx 1.2/1.3 tables (D394 has no reference tables; presets 1/2 hidden), a per-seat preset, a warning when two actions share a button. | -| Per-pad tuning (device index, hot-plug seats) | verify | D448, D450 (#122) | Pad seats keyed by SDL instance id, reserved in a match; hot-plug events reach the input layer. Merged with split-screen 2026-10-01. **Owed:** unplug and replug a pad mid-match. | -| Full-gamepad menu navigation audit, Xbox/PS button glyphs | verify | D471; local note `PAD-MENU-AUDIT.md` | **Audit done 2026-10-01:** a controller-only player is never trapped in the PC menus. **Glyphs: not wanted** (maintainer); instead D471 shows family-specific button **names** (PlayStation/Nintendo) in the help lines and binding column, Xbox text unchanged, plus player-index LEDs (PD-port parity). **Audit G1-G3 fixed by D472** (owner-pad input). **Owed:** a real PS/Nintendo pad check. | +| Gamepad presets: per-seat preset, duplicate-binding warning | open | D469, D498, D394 | D469 (pad action table, Jinx 1.1 default, Custom per-seat bindings) and **D498** (Xbox 1.2 Christmas / 1.3 Frost / 1.4 Elektra) are done and maintainer-verified with pads (2026-10-01, 2026-10-03). Not done: a per-seat preset; a warning when two actions share a button; the Xbox "left stick aims the crosshair" and "D-pad copies the left stick" behaviours (D394). | +| Pad button names on a Nintendo pad | verify | D471; local note `PAD-MENU-AUDIT.md` | Audit done 2026-10-01 (no controller-only trap; D472 fixed G1-G3). D471 family button names + player LEDs: **PS5 DualSense verified 2026-10-03.** **Owed:** a Nintendo (Switch) pad check; the maintainer has no such pad, so this waits for one or for a user report. | | 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 | verify | D472, D335b, #90 | **Landed 2026-10-01 (D472):** overlay pillarboxed to the centred 4:3 card in widescreen (mouse mapped through the same region); EXPERIMENTAL rows in red; unknown `ge007.ini` keys shown as a non-blocking "N UNKNOWN INI KEY(S) - SEE LOG" in the title bar (#90's promise); the overlay is driven by the pad that opened it (audit G1-G3). **Owed:** 2-pad ownership live check and mouse hit-testing at 16:9/21:9 (checklist in D472). | -| Master volume, audio output device select | verify | D470 | **Landed 2026-10-01 (D470):** `Audio.MasterVolume` (0-100, default 100 = byte-identical output; gain applied after the game's music/FX volumes, live) and `Audio.Device` (default = system default; switch on the audio producer thread; missing device or removal falls back to default). Rows at the top of F10 / front-end AUDIO. **Owed:** listening checklist in the D470 finding (master 100 unchanged, slider live, device switch, persistence, unplug, bogus name). | +| FPS tanks while adjusting F10 music volume with a controller | done | D489 | Maintainer, 2026-10-02 (seen on PAL; general, also on v0.4.0). Cause: every pad/arrow slider step saved to the EEPROM (3 joy-poll handshakes + file rewrite on the game thread). Fixed: steps stage, save once on release; merged into `release/v0.4.1` 2026-10-03. Maintainer pad check passed 2026-10-03. Delete this row once released. | | 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. | +| Swapping saves with emulators | decided (after the D441 census) | D492 | **Investigated 2026-10-03 (D492):** the port stores the save checksum words and `options` little-endian, so an unconverted 1964 save fails validation and all five slots are wiped on first boot. Converting (swap `options`, recompute the checksum pair) was proven in a headless A/B: all slots OK, all 20 levels unlocked. **Decided:** `tools_pc/eep_convert.py` (both directions, `--verify`) plus a README "Using an emulator save" section; the on-disk format stays as is. Seed: `scratch/eepinv/convert.py`. | ### 5b. GE+ / Dab's-mod gameplay options (opt-in; touch game code) @@ -226,6 +232,7 @@ needs Rule-2 review. | Item | Status | Notes | |---|---|---| | Post-process chain: FXAA/SMAA, bloom, depth of field, colour grade/LUT, CRT filter, brightness/gamma, colourblind filters, SSAO | post-1.0 | Build the framebuffer pass once. Every effect defaults OFF. **Decided 2026-10-01:** after 1.0; our own opt-in implementation (default OFF; the Original N64 preset restores the N64 look); thepont's fork is reference only. | +| Near-full fog reveal (D503): far objects about 97% fogged show as faint silhouettes with straight room-clip edges on 32-bit output (e.g. Surface's dish) | triage | Two fix options, both opt-in or by-eye gated: (a) snap fog at or above ~97% to full fog (port-only, fast3d vertex fog; small, but changes fog everywhere slightly, needs a by-eye pass across levels); (b) an N64 16-bit colour/dither output mode (5 bits per channel, as the console framebuffer), a natural fit for the Original N64 preset and the "authentic 320x240" item below. Draw/LOD distance cannot help: 100 is already the N64 value. Measurements: HANDOFF 2026-10-03 / findings D503 (owed). | | Render scale / supersampling, integer scaling, "authentic 320×240" mode | post-1.0 | **Decided 2026-10-01:** after 1.0; our own opt-in implementation (default OFF; the Original N64 preset restores the N64 look); thepont's fork is reference only. | | Dynamic/coloured lighting, dynamic shadows, water reflections | post-1.0 | Large. Consult the PD port first. #105 is about the fork's version of reflections, not ours. **Decided 2026-10-01:** after 1.0; our own opt-in implementation (default OFF; the Original N64 preset restores the N64 look); thepont's fork is reference only. | | Path-traced render mode (opt-in, RTGL1) | post-1.0 | Experimental, for fun (maintainer, 2026-10-01): **deprioritised to after 1.0**. Plan in the maintainer's local notes: vendor MIT RTGL1 behind `port/rt/`, default OFF; fast3d stays the default backend and the fidelity ground truth; Phase 0 is a toolchain spike (RTGL1 under MSYS2/MINGW64). Nothing starts before 1.0. | @@ -256,11 +263,15 @@ and a stage loader. | Item | Status | Refs | Notes | |---|---|---|---| | 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. | +| 1964-vs-port comparison harness | partial | `tools_pc/abpad/` | **2026-10-02:** one millisecond timeline drives 1964 (virtual XInput pad through the maintainer's own NRage profile) and the port (`GE_INPUTSCRIPT_MS`), with both windows captured on one clock and side-by-side sheets; verified through intro skip, file select and folder open. Not frame-identical (1964 has no seed pin or frame dump), so it's a timing/behaviour comparison, not a pixel diff. **Left:** in-level timelines (load a level on both sides, e.g. the D486 turret check); the two sides use different save files. **2026-10-03:** scripted menu navigation dropped (the free cursor lands on different cells on each side); replaced by `live_ab.ps1` (1964 then port after `-PortDelayMs`, 1280x720 each, position-only) with one real pad (`GE_PAD_BACKGROUND=1`). C-button hold tokens (`CUPHOLD`...`CNONE`) added for 1.2 walking in scripts. | +| Bond seems to move faster on the port than on 1964 (live A/B) | parked | — | Maintainer, 2026-10-03, live one-pad A/B (`tools_pc/abpad/live_ab.ps1`). Not investigated. Likely input mapping, not game speed: the port forces 1.2 SOLITARE and turns the left stick into digital C-buttons past a threshold (`port/src/input.c` ~2717), so a slightly off-centre push strafe-runs; 1964 is probably 1.1 analog through NRage (speed depends on Range/deadzone). Next step: match control styles (1964 save on 1.2, NRage left stick as C-buttons), push straight forward, then compare on a fixed route. | +| Scripted-playthrough harness | feasibility spike (after D441 and the worker tasks) | `GE_INPUTSCRIPT`, D117 | Input script + `GE_PCDUMP` + crash-log check, for unattended regression runs. **Maintainer concern (2026-10-03):** aiming/shooting, PRNG guards and timed events may block open-loop scripts, and runs aren't deterministic (D117). Levers: the game's own Invincibility + Invisibility cheats, closed-loop waypoint steering from Bond's position (port hook), a pinned seed (`GE_RSEED`), and coverage routes (load, walk, doors) instead of objectives. Spike: one Dam route run 5 times, measure end-position spread, then go/no-go. | +| Cheap worker for mechanical, build-checked tasks | open | `delegate-local` MCP; memory of past Qwen issues | Birdman64's fast worker model wrote 44% of its commits while the lead planned and reviewed. Ours: pattern-applied edits (D441 fix batches, scaffolding strips, small tools), each gated by a clean build + a specific check + the lead's diff review, in the worker's own worktree. Local Qwen for edit-only tasks (it fabricates on reading tasks), or a hosted fast model via `delegate_to_provider` (cost is the maintainer's call). | | Golden baselines | partial | `tools_pc/verify.sh`, `tools_pc/golden/README.md`, D117 | **Windows done 2026-10-02:** all 21 levels re-based (`tools_pc/golden//win/`) with a near-deterministic recipe (pinned seed, START skips the intro flyby, frames 900-1500 of settled gameplay; Cuba captures its ending); two-tier gate (structural + per-pixel with per-level limits) catches a global texture-filter change (22% vs <= 2.3% noise). verify.sh now quits via `GE_QUITFRAME` and never kills other game instances. **Left:** Linux baselines (needs a real-GPU Linux box: `GE_PCDUMP` reads black on llvmpipe/WSLg), then the parity gate per platform. `GE_DETERM` stays experimental (not needed for this recipe). | +| Flicker scan (A/B/A frame alternation) | parked (2026-10-03: tooling, nothing blocked on it; spec in the local investigation report `scratch/birdman64-rows-investigation.md` §4) | `GE_PCDUMP`, `GE_INPUTSCRIPT` | Dump **every** frame of a scripted run at low resolution and flag frame n when it differs from n-1 but matches n-2. Dumping every Nth frame hides this kind of flicker by design. Cheap once the scripted-playthrough harness exists. For any old-vs-new pixel diff outside the golden gate, run a same-commit control first to measure the noise floor, as the golden gate already does. Reference: Birdman64 `tools/flicker_scan.py`, `docs/notes/sweep.md`. | +| Release build-provenance attestation | decided | `.github/workflows/ci.yml` release job; `.github/SECURITY.md` | **Decided 2026-10-03:** add SHA-pinned `actions/attest-build-provenance` to the tag-only release job, with `id-token: write` on that job only. It backs SECURITY.md's "unsigned, but reproducible from this repository" stance with a check anyone can run (`gh attestation verify --repo jkdansereau/goldeneye-pc-port`); document that in SECURITY.md and the README. Verifiable only on the next tag. Dropped: a combined `SHA256SUMS` (no security gain over the per-file `.sha256` from the same page) and `READ ME FIRST.txt` (duplicates the bundled README). | | 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. | -| 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 (**removed 2026-10-02 (batch 4, `chore/strip-batch4`):** `GE_D235_OFF`/`_RELOAD`/`_NEXT` (D235 closed; the fix and the `GE_D235` log stay), `GE_D245V`, `GE_D245_FIXEDSHIFT` and the inert sky tile-capture/shift code in `sky.c`; registry drift fixed and `gen_env_probes.py --check` passing again; **removed 2026-10-01:** `GE_D306C`, `GE_D318T` (batch 2); the whole D243 diagnostic set -- `GE_D243X2`, `GE_D243X4`, `GE_D243M` (+ its `d243mProbeActive`/frame-counter plumbing and all `D243M:` log lines in `bondview2.c`/`chr.c`/`chraction.c`/`chrai.c`/`model.c`/`objecthandler.c`), `GE_D243CAM`, `GE_D243SWIRL`, `GE_D243` (front.c menu trace) (batch 3, `chore/strip-d243-probes`; the shipped fix and the M-183/M-185 clamps are kept); `GE_DYNTEXHASH_OFF`, `GE_FORCEALARM`, `GE_D204_OLD`, `GE_D252`, `GE_RSEED_LV` were already gone); and the M-183/M-185 clamps, which need a full cutscene re-check first. | +| 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. **2026-10-03 (batch 5, `chore/strip-batch5`):** removed the dead registry probes D116, D51, D56, D60-D63, D69/D69BB/D69STAN, D71LOG, D85DUMP, D86-D88, D90, D96, D154, D178 (logging only; ~500 lines; build clean, intro + `-level_09` smokes clean). **Left:** D104 (kept while D294 is in verify); closed-finding TEMPs (**removed 2026-10-02 (batch 4, `chore/strip-batch4`):** `GE_D235_OFF`/`_RELOAD`/`_NEXT` (D235 closed; the fix and the `GE_D235` log stay), `GE_D245V`, `GE_D245_FIXEDSHIFT` and the inert sky tile-capture/shift code in `sky.c`; registry drift fixed and `gen_env_probes.py --check` passing again; **removed 2026-10-01:** `GE_D306C`, `GE_D318T` (batch 2); the whole D243 diagnostic set -- `GE_D243X2`, `GE_D243X4`, `GE_D243M` (+ its `d243mProbeActive`/frame-counter plumbing and all `D243M:` log lines in `bondview2.c`/`chr.c`/`chraction.c`/`chrai.c`/`model.c`/`objecthandler.c`), `GE_D243CAM`, `GE_D243SWIRL`, `GE_D243` (front.c menu trace) (batch 3, `chore/strip-d243-probes`; the shipped fix and the M-183/M-185 clamps are kept); `GE_DYNTEXHASH_OFF`, `GE_FORCEALARM`, `GE_D204_OLD`, `GE_D252`, `GE_RSEED_LV` were already gone); 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. | @@ -290,4 +301,8 @@ cross-play · Steamworks, achievements, cloud saves (no storefront) · HDR, frame generation, stereo 3D (ray tracing only as the opt-in post-1.0 experiment in §5c) · remake-scope assets (new models, music or voice) · new movement mechanics (jump, roll, melee) · the -007 Plus ROM patch as such (#110). +007 Plus ROM patch as such (#110) · a per-user save folder (decided +2026-10-03: saves stay next to the game, which keeps the security doc's +"never writes to `%APPDATA%`" promise; README *Updating* covers moving them) · +a first-launch controls card and controller toasts (decided 2026-10-03: new +UI for little gain; F10 is documented in both READMEs). diff --git a/docs/building.md b/docs/building.md index b06d9139..57588c4a 100644 --- a/docs/building.md +++ b/docs/building.md @@ -139,8 +139,12 @@ python3 tools_pc/d69_emit.py ntsc-final # -> data/pccg-ntsc-final/{pccg python3 tools_pc/d88_emit.py ntsc-final --regen # appends the 21 solo Usetup*Z + 13 multiplayer Ump_setup*Z stage-setup files -> ~3.6 MB ``` -**PAL / JP note:** sidecar generation for these regions is currently broken at -the source-data level (finding D258); use an NTSC-U ROM until issue #85 lands. +**PAL / JP note:** both regions convert, build and boot from source (finding +D258, fixed 2026-10-02; D487/D488 fixed the PAL frame height and TV type). +Run the three emit passes with `pal-final` or `jpn-final`, and build into a +separate directory, for example `BUILD_DIR=build-pal ./build-pc.sh pal-final`. +Release packages are still NTSC-U only until per-region packaging lands +(issue #85). These are **pure-stdlib Python 3** (no MIPS toolchain, independent of the step-2 asset extraction) and read only the ROM plus files already committed to diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index f75cb921..8c816a37 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -21,6 +21,8 @@ Two classes: |---|---|---|---| | `GE_PCDUMP` | `port/fast3d/gfx_opengl.cpp:1079` (`is_pcdump`), `gfx_opengl.cpp:700`, `port/src/video.c:188`, `port/src/config.c:46` (`configGetFrameDump`) | Per-frame framebuffer PPM dump to `$PCDUMP` dir (verification-ritual golden captures, `tools_pc/framediff.py`/`pixcount.py`). Also settable via `[Debug] FrameDump` ini (env wins). **M-33/D168: the writer now emits rows top-to-bottom — captures before that fix are vertically flipped.** | **live** | | `GE_INPUTSCRIPT` | `port/src/input.c:306` | Headless scripted controller-0 input: `":,…;…"` (`SUP/SDOWN/SLEFT/SRIGHT/SNONE` sustain, buttons pulse; `ZHOLD`/`ZREL` sustain fire, D207 M-201). Sole input source when set. | **live** | +| `GE_INPUTSCRIPT_MS` | `port/src/input.c:1194` | abpad: entry times in `GE_INPUTSCRIPT` are milliseconds since SDL init instead of controller reads (`tools_pc/abpad`). C-button holds `CUPHOLD`/`CDOWNHOLD`/`CLEFTHOLD`/`CRIGHTHOLD`/`CNONE` (1.2 walk/strafe) also live in the script parser. | **live** | +| `GE_PAD_BACKGROUND` | `port/src/input.c:1472` | `=1` sets `SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS` so the port reads pads while unfocused: one real pad drives the port and 1964 side by side (`tools_pc/abpad/live_ab.ps1`). | **live** | | `GE_INPUTLOG` | `port/src/input.c:329`, `port/src/config.c:53` (`configGetInputLog`) | Logs computed pad state (buttons + stick) each poll when non-zero. Also `[Debug] InputLog` ini. Also traces D165 `menuptr est/tgt/eff/stick`. | **live** | | `GE_STARTMENU` (+ `GE_STARTMENU_PAGE`, `GE_STARTMENU_DIFF`) | `src/game/lv.c:398-402` (`#ifdef PORT`) | Boot straight into a front-end menu id (13=MISSION_COMPLETE, 10=BRIEFING, 7=MISSION_SELECT, 12=MISSION_FAILED, 6=MODE_SELECT); `_PAGE`=folder row (def 1=Dam), `_DIFF`=0..3. Crash-test a screen fast. | **live** | | `GE_STARTMP` | `src/game/lv.c:403` (`#ifdef PORT`) | D416: `GE_STARTMP=[,]` starts a local multiplayer match with the menu's default options (as MP options -> START does); pair with `GE_MPVIRT=` for the controller count. Unattended split-screen testing. | **live** (D416) | @@ -66,7 +68,7 @@ Two classes: | `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_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) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | | `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. | **dead** (D245 fixed) — removed 2026-10-02 (strip batch 4, `chore/strip-batch4`); tombstone | | `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). | **dead** (D245 fixed) — removed 2026-10-02 with the inert sky tile-capture/shift code (strip batch 4); tombstone | @@ -124,31 +126,31 @@ Two classes: | Env var | File:line | Finding / what it logged | Status | |---|---|---|---| -| `GE_D51` | `port/src/libultra.c:496`, `src/game/model.c:125,244,544` | msgQ 32-slot overflow watch / ModelSlot layout | dead (D51 closed) | +| `GE_D51` | `port/src/libultra.c:496`, `src/game/model.c:125,244,544` | msgQ 32-slot overflow watch / ModelSlot layout | dead (D51 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | | `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_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) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | | `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) | -| `GE_D60` | `port/src/libultra.c:521,735` | gfx frame msgQ delivery trace | dead (D60 closed) | -| `GE_D61` | `port/src/libultra.c:795` (`s_d61opened`) | opens a one-shot log file | dead (D61 closed) | -| `GE_D62` | `port/src/libultra.c:477` | osRecvMesg trace | dead (D62 closed) | -| `GE_D63` | remaining: `src/game/bg.c:2525,2908,2953`, `front.c:1410,1416,7814,7889,8083,8091,8099,8366`, `image.c:2453`, `language.c:431`, `model.c:4340`, `title.c:607` | gun-barrel sub-DL clobber hunt (D63/D64/D65/D66 — the labels drift within the blocks). Stripped M-30 (`d0789358`, `gfx_pc.cpp` trail). Stripped M-32 (`49ce620a`): `memp.c`, `bg.c:2880` (incl. the bare non-PORT `d63bgprimarycount` static + entry log), `blood_animation.c`, `dyn.c`, `rsp.c`. **Remaining blocks are all `#ifdef PORT` + getenv-guarded and inert — strip candidates for a later pass.** | dead (D63 closed) — partially removed | -| `GE_D69` | `src/game/ob.c:165,227` | stage-load (D69) object trace | dead (D69 closed) | -| `GE_D69BB` | `src/game/bg.c:2460,2479,3020` | D69 bg-binary layout trace | dead | -| `GE_D69STAN` | `src/game/stan.c:266,276` | D69 stan-tile trace | dead | -| `GE_D85DUMP` | `src/game/bg.c:2510` | D85 `bgWidenRoomGdl` 8→16B dump | dead (D85 closed) | +| `GE_D178` | `src/game/front.c:6586,6594` (`#ifdef PORT`) | briefing-data u16 decode trace (blank-objectives, D143/D178) | dead (D178 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | +| `GE_D56` | `src/game/model.c:227,830` | watch `Model` raw-offset reads | dead (D56 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | +| `GE_D60` | `port/src/libultra.c:521,735` | gfx frame msgQ delivery trace | dead (D60 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | +| `GE_D61` | `port/src/libultra.c:795` (`s_d61opened`) | opens a one-shot log file | dead (D61 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | +| `GE_D62` | `port/src/libultra.c:477` | osRecvMesg trace | dead (D62 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | +| `GE_D63` | remaining: `src/game/bg.c:2525,2908,2953`, `front.c:1410,1416,7814,7889,8083,8091,8099,8366`, `image.c:2453`, `language.c:431`, `model.c:4340`, `title.c:607` | gun-barrel sub-DL clobber hunt (D63/D64/D65/D66 — the labels drift within the blocks). Stripped M-30 (`d0789358`, `gfx_pc.cpp` trail). Stripped M-32 (`49ce620a`): `memp.c`, `bg.c:2880` (incl. the bare non-PORT `d63bgprimarycount` static + entry log), `blood_animation.c`, `dyn.c`, `rsp.c`. **Remaining blocks are all `#ifdef PORT` + getenv-guarded and inert — strip candidates for a later pass.** | dead (D63 closed) — partially removed — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | +| `GE_D69` | `src/game/ob.c:165,227` | stage-load (D69) object trace | dead (D69 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | +| `GE_D69BB` | `src/game/bg.c:2460,2479,3020` | D69 bg-binary layout trace | dead — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | +| `GE_D69STAN` | `src/game/stan.c:266,276` | D69 stan-tile trace | dead — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | +| `GE_D85DUMP` | `src/game/bg.c:2510` | D85 `bgWidenRoomGdl` 8→16B dump | dead (D85 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | | `GE_D85TEX` | `src/game/image.c:2479`, `src/game/tex.c:859,1050` | D85 texpool-full events (Depot analysis) | dead (D85 closed) — was useful for §3 | | `GE_D217TEX` | `src/game/image.c:234` (`texInflateZlib`) | D217: logs `texnum`/`format`/`numcolours`/`palette[0..1]` straight off the decode bitstream (before any arena/pointer math) — the decode-correctness check that proved the affected texture (grip, `texnum=1608`) decodes byte-identically across levels, ruling out a decode/ROM-data bug. | **live** (D217 open — investigation instrument) | | `GE_D229` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri` dual-texunit probe + `import_texture`) | D229: per-draw dump of both texunits' tile/tmem/fmt/siz/UV state for 2-cycle draws, plus a one-shot confirm line when the RGBA16-over-CI8 import remap fires (the fix path). Inert by default. | **live** (D229 fixed — probe kept for the Surface 2 / sweep verification) | -| `GE_D86` | `src/game/model.c:6295`, `src/game/objecthandler_2.c:143` | D86 model rwdata trace | dead (D86 closed) | -| `GE_D87` | `src/game/ramromreplay.c:296,363,381` | D87 `ramromfilestructure` endianness | dead (D87 closed) | -| `GE_D88` | `src/game/prop.c:1367`, `src/game/stan.c:3090` | D88 `Usetup*Z` propDef stream | dead (D88 family closed; D88.4 resolved) | -| `GE_D90` | remaining: `src/game/bondview2.c:2146`, `src/game/prop.c:1391` (bondview_r.c blocks stripped M-32, `49ce620a`) | D90 bondview / prop NULL trace | dead (D90 closed) — partially removed | -| `GE_D96` | `src/game/chrprop.c:436,507` (cached `probe`) | D96 chrprop trace | dead (D96 closed) | +| `GE_D86` | `src/game/model.c:6295`, `src/game/objecthandler_2.c:143` | D86 model rwdata trace | dead (D86 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | +| `GE_D87` | `src/game/ramromreplay.c:296,363,381` | D87 `ramromfilestructure` endianness | dead (D87 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | +| `GE_D88` | `src/game/prop.c:1367`, `src/game/stan.c:3090` | D88 `Usetup*Z` propDef stream | dead (D88 family closed; D88.4 resolved) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | +| `GE_D90` | remaining: `src/game/bondview2.c:2146`, `src/game/prop.c:1391` (bondview_r.c blocks stripped M-32, `49ce620a`) | D90 bondview / prop NULL trace | dead (D90 closed) — partially removed — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | +| `GE_D96` | `src/game/chrprop.c:436,507` (cached `probe`) | D96 chrprop trace | dead (D96 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | | `GE_D104` | `src/game/bg.c:654` (rate-limited, `d104c`) | D104 depth-clear trace | dead (D104 closed) | -| `GE_D71LOG` | `port/fast3d/gfx_pc.cpp:645` | D71 fast3d trace | dead (D71 closed) | +| `GE_D71LOG` | `port/fast3d/gfx_pc.cpp:645` | D71 fast3d trace | dead (D71 closed) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE | | `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. | **dead** (D309/D310 fixed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE | diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index bbbe6bbb..e764f8cc 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -172,7 +172,7 @@ D290,FIXED,"Watch's in-game ""controller"" section shows no control-pad graphic D291,CLOSED,"Dam: texture glitches when the truck drives past (M-141, Steam Deck user playtest).","CLOSED — duplicate of D306, faithful (M-201, 2026-09-26:" D292,FIXED,Death animations are glitched —,"MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed alongside D173) —" D293,FIXED,"QoL ask: no discoverable way to quit the game from the UI (M-141, Steam Deck user playtest —","FIXED (M-141) — ""Quit to desktop"" row added to the F10 overlay; build-verified Win+Linux, not yet live-confirmed.…" -D294,PARTIAL,"Statue: ground geometry near the large CCCP/hammer-and-sickle monument briefly renders solid black over a wide area, then self-recovers 10-30 seconds later… —",PARTIAL -> fix landed pending by-eye check (2026-09-30: +D294,PARTIAL,"Statue: ground geometry near the large CCCP/hammer-and-sickle monument briefly renders solid black over a wide area, then self-recovers 10-30 seconds later… —",PARTIAL -> fix landed pending by-eye check (2026-10-03: D295,FIXED,"Campaign-halting crash at the end of Control Center on 00 Agent difficulty, then a crash on every subsequent relaunch/resume (GitHub #87, user campaign playtes…",FIXED (M-149) — M-148 root-caused + reproduced on HEAD; fix landed: D296,CLOSED,"Control Center, 00 Agent:","CLOSED (2026-09-26, user-approved:" D297,FIXED,v0.3.0 silently wipes ALL pre-fix player saves on first launch (D284 side effect; found during D295/M-148).,FIXED (M-150) — migration shim landed: @@ -306,8 +306,8 @@ D432,FIXED,"Gunshot sounds ""off"" after D427:",FIXED in tree (gate offset round 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). +D441,PARTIAL,64-bit pointer-width sweep (part 1):,"PARTIAL (census + GE_HIGHARENA_BASE tier-1 build + 35 sites fixed + CI ratchet, 2026-10-03; tier-1 menu and -level_09 pixel-identical; owed tier-1 all-level sweep and the above-4-GiB tier)." +D442,FIXED,Game.AllUnlocked reworked into a pure RAM/query-time override:,"FIXED; verified 2026-10-03 headless (mission select all 20 / only real progress when OFF, full cheat menu, eep byte-identical)." 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). D454,FIXED,Knife slash crashed on Windows:,FIXED (verified by maintainer live: D461,FIXED,Item-slot allocation overran into the next stage-bank block:,FIXED (maintainer-verified: @@ -345,4 +345,22 @@ D482,FIXED,Lighter first-launch defaults on Atom/Celeron-class GPUs (#92 follow- D483,FIXED,#136: the PC crouch key stood Bond up when he switched to the sniper rifle; the N64 keeps the stance (the weapon's DISABLE_CROUCH flag blocks both crouch-down…,"FIXED 2026-10-02 (local, fix/d483-sniper); scripted crouch -> switch -> release keeps SQUAT" D484,FIXED,"#136: portScaleFovY's 20-degree floor raised the game's own zoom FOVs, capping sniper/camera magnification at 20 degrees instead of the N64's 7 (about a third…","FIXED 2026-10-02 (local, fix/d483-sniper, maintainer-approved 2026-10-02); full zoom renders at 7 degrees, normal views pixel-identical" D485,FIXED,"#136: sniper/camera zoom stepped once per game frame with no tick scaling, so at the port's 1 tick/frame it zoomed 2x faster in real time than the N64 at 2 tic…","FIXED 2026-10-02 (local, fix/d483-sniper); 15 -> 7 takes 16 frames at 60 fps (was 8) = the N64 at 30 fps" +D486,FIXED,#126: explosions dropped in heavy firefights (turrets on Cradle/Egypt/Aztec):,"FIXED (a)(c)(d); drops + (b) faithful, closed." +D487,FIXED,"PAL: the in-level ammo counter was not drawn (only a small dot where the ""7 / 93"" counter should be) —",FIXED; maintainer PAL play check 2026-10-02: +D488,FIXED,PAL build reported the Brazilian MPAL TV type instead of PAL (osTvType) —,FIXED +D489,FIXED,FPS dropped sharply while adjusting F10 watch sliders (music/FX) with a pad or arrow keys:,"FIXED 2026-10-02 (port-only UI layer, fix/d489-pad-slider-commit, merged into release/v0.4.1 2026-10-03); maintainer pad check passed 2026-10-03" +D490,FIXED,Dam first-tower guard walked a small circle:,"FIXED 2026-10-02 (fix/d490-glass-hull, merged into release/v0.4.1 2026-10-03); maintainer by-eye Dam check passed 2026-10-02" +D491,FIXED,"Struct reads across separately-declared adjacent globals (the D5/D215 class), found by the D24 overflow audit:","FIXED 2026-10-03 (fix/d491-adjacent-global-reads, local); builds clean, 900-frame -level_09 smoke clean; no by-eye effect expected beyond room-membership-driven behaviour" +D492,OPEN,Emulator .eep saves are not drop-in compatible:,OPEN (investigated 2026-10-03; fix decided: +D493,FIXED,Pad rumble kept going after quitting the game:,"FIXED 2026-10-03, maintainer-verified in play (DualSense; Original-mode edges incl." +D494,FIXED,"Sniper rifle: the PC crouch key now crouches/stands freely with a disable-crouch weapon (GEPD parity), never injecting C-down for it; supersedes D483's stance…","FIXED 2026-10-03, maintainer-verified in play" +D495,FIXED,"Tank: with the default pad preset X changed the tank weapon and Y left it, because in-tank faces fell back to the fixed menu faces; PadMapCtx.tank routes Next/…","FIXED 2026-10-03, maintainer-verified in play" +D496,FIXED,3P/4P split-screen: weapon-pickup messages garbled and oversized —,"FIXED 2026-10-03 (condensed 3+P text is faithful, maintainer-confirmed)" +D497,FIXED,PC Options paging now matches the game's dossiers:,"FIXED 2026-10-03, maintainer-verified in play" +D498,FIXED,Xbox 1.2 Christmas / 1.3 Frost / 1.4 Elektra gamepad presets:,"FIXED 2026-10-03, maintainer-verified with a pad" +D499,FIXED,Trilinear texture filter option:,"FIXED 2026-10-03, maintainer-checked (no visible difference; kept)" +D500,FIXED,"Aztec shuttle area: polygons ""fell apart""; the per-frame vtx pool (dyn.c, no bounds check, Aztec -mvtx40) could overrun into the half the render worker draws;…","FIXED 2026-10-03, maintainer-verified at the Aztec spot" +D501,CLOSED,Ultrawide 16:9 pillarbox for cutscenes and the watch:,CLOSED (reverted 31ade91f) +D502,FIXED,PC Options pad navigation:,"FIXED 2026-10-03, maintainer-verified with a pad" +D503,?,Surface far dish shows as a faint silhouette with straight edges:,KNOWN ISSUE D450,FIXED,"Unplugging and replugging a controller was not picked up (user, GameSir-G7 Pro; 2026-09-29).","FIXED — port-only: the render thread's gfx_sdl_handle_events drains the same SDL queue as video.c's host pump but had no case for SDL_CONTROLLERDEVICEADDED/REMOVED, so it usually swallowed them and i…" diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 064aace7..5027b9ca 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -580,7 +580,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D291 | **Dam: texture glitches when the truck drives past (M-141, Steam Deck user playtest).** — full `## D291` entry at file tail | CLOSED — duplicate of D306, faithful (M-201, 2026-09-26: user-captured, Dam intro camera 5 only). Earlier: OPEN — observed, not investigated. Needs a live repro with the truck's exact position/timing and, ideally, a screenshot. | | D292 | **Death animations are glitched — Bond is often sunk halfway into the floor (M-141, Steam Deck user playtest).** — full `## D292` entry at file tail | **MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed alongside D173) — same fix, same outcome: right 9/10 times, small residual offset only when off, no more full sinking/glitching.** See D173 for the user's exact confirmation wording; not separately re-tested, reported together. | | D293 | **QoL ask: no discoverable way to quit the game from the UI (M-141, Steam Deck user playtest — "couldn't see a way to exit the game. Maybe build this into the…** — full `## D293` entry at file tail | FIXED (M-141) — "Quit to desktop" row added to the F10 overlay; build-verified Win+Linux, not yet live-confirmed.… | -| D294 | **Statue: ground geometry near the large CCCP/hammer-and-sickle monument briefly renders solid black over a wide area, then self-recovers 10-30 seconds later…** — full `## D294` entry at file tail | PARTIAL -> fix landed pending by-eye check (2026-09-30: pool cap 2.0 -> 2.5, DD/Lod terms powf 0.66, quarter-step round-up; ma 450560 -> 563200 at max settings; see D294 entry tail). Prior: PARTIAL (2026-09-29 bookkeeping: main symptom FIXED — room-model pool; user playtests 2026-09-22/25 no longer reproduce the black ground; the residual corner flicker at max FOV reported 2026-09-22 was never re-checked, tracked in docs/ROADMAP.md §2). Earlier: **FIXED, user playtest confirmed (2026-09-25: not reproduced)** — root cause: room-model POOL exhaustion (authored `-ma220`=220 KB < ~250 KB monument-plaza demand at FovScale 130 + max DD/Lod); `memaAlloc` NULL → silent skip → undrawn room shows the night-sky clear colour. Stochastic (wall-clock seed → load order). Fix: `portRoomPoolScale()` (video.c, identity at N64-faithful settings) applied in `bossMainloop`; verified headless (faithful→exact authored size; user settings→0 failures; `GE_ROOMPOOL=0.25`→exhaustion on demand). Playtest: main patch gone; residual corner flicker traced to full-level residency (487 KB = 2.2× authored) starving the 2× pool; the 3×/linear fix REGRESSED the renderer on the menu-driven path (D156 NaN anim state, not a bank overflow) → reverted to sqrt + 2.0 cap (ma=450560). Residual open; probes in place for next capture. High-DD horizon edge on night levels = setting-inherent, documented only.… | +| D294 | **Statue: ground geometry near the large CCCP/hammer-and-sickle monument briefly renders solid black over a wide area, then self-recovers 10-30 seconds later…** — full `## D294` entry at file tail | PARTIAL -> fix landed pending by-eye check (2026-10-03: spawn check clean, 0 `D104 SKIP`/load FAIL on Statue/Streets/Jungle at default and max FOV/draw/LOD; 2026-09-30: pool cap 2.0 -> 2.5, DD/Lod terms powf 0.66, quarter-step round-up; ma 450560 -> 563200 at max settings; see D294 entry tail). Prior: PARTIAL (2026-09-29 bookkeeping: main symptom FIXED — room-model pool; user playtests 2026-09-22/25 no longer reproduce the black ground; the residual corner flicker at max FOV reported 2026-09-22 was never re-checked, tracked in docs/ROADMAP.md §2). Earlier: **FIXED, user playtest confirmed (2026-09-25: not reproduced)** — root cause: room-model POOL exhaustion (authored `-ma220`=220 KB < ~250 KB monument-plaza demand at FovScale 130 + max DD/Lod); `memaAlloc` NULL → silent skip → undrawn room shows the night-sky clear colour. Stochastic (wall-clock seed → load order). Fix: `portRoomPoolScale()` (video.c, identity at N64-faithful settings) applied in `bossMainloop`; verified headless (faithful→exact authored size; user settings→0 failures; `GE_ROOMPOOL=0.25`→exhaustion on demand). Playtest: main patch gone; residual corner flicker traced to full-level residency (487 KB = 2.2× authored) starving the 2× pool; the 3×/linear fix REGRESSED the renderer on the menu-driven path (D156 NaN anim state, not a bank overflow) → reverted to sqrt + 2.0 cap (ma=450560). Residual open; probes in place for next capture. High-DD horizon edge on night levels = setting-inherent, documented only.… | | D295 | **Campaign-halting crash at the end of Control Center on 00 Agent difficulty, then a crash on every subsequent relaunch/resume (GitHub #87, user campaign playtest).** — full `## D295` entry at file tail | FIXED (M-149) — M-148 root-caused + reproduced on HEAD; fix landed: port-side JPN-cache seed (`port/src/main.c`) + `#ifdef PORT` buffer widen in `front.c` (D8 class). Verified: previously-crashing input now clean, 21/21 sweep PASS. Companion save-wipe regression → D297. History — M-148 fully root-caused + **reproduced on current HEAD**: `difficultytext[4]` (`front.c:2616`) overflow — `strcpy("00 Agent")`/`strcat("\n")` spills onto `textpos.p[0]` (y, ≥0x80 in normal geometries) → high-bit byte → `langGetJpnCharPixels` NULL deref (`language.c:320`, `g_JpnCacheCacheItems` NULL outside JPN mode — likewise unallocated on N64 non-JP builds, crash potential is decomp-native). The user's save is a *valid pre-D284* save (all 5 slot CRCs bit-exact under the old PRNG); post-fix builds wipe it at validation, which is why the unmodified artifact no longer triggers it — NOT a fix. No OOB exists (M-147 guess retracted: 007 difficulty has no stored times). Fix options flagged for sign-off (port-side JPN-cache init preferred; `difficultytext` enlarge = D8 class). Side finding: v0.3.0 silently wipes all pre-fix saves on first launch (D284 side effect — own D entry owed). Separate still-open issue found: unconditional `[EVTQ-DROP]` audio-pool-exhaustion logging, `src/libultra/audio/event.c:92`.… | | D296 | **Control Center, 00 Agent: noticeable stutter, user's own read is "maybe from the drone guns" (M-146, same D295 investigation session).** — full `## D296` entry at file tail | CLOSED (2026-09-26, user-approved: stutter seen only under gdb (debug.ps1); D302, fixed two days later, removed a per-triangle uncached getenv costing ~15 fps; reopen if seen on a normal build). Earlier: OPEN — single live report, un-triaged; observed only under a gdb-attached debug-harness launch (`tools_pc/debug.ps1`), not the packaged release artifact — user flagged that harness overhead has caused false stutter before. Needs a plain-exe / real-release repro check before any perf tracing.… | | D297 | **v0.3.0 silently wipes ALL pre-fix player saves on first launch (D284 side effect; found during D295/M-148).** — full `## D297` entry at file tail | FIXED (M-150) — migration shim landed: `port/src/legacycrc.c` (isolated, deliberately-wrong pre-D284 PRNG, reviewer-verified bit-exact against `0de99b4d^`) + eeprom-shim read-path hook in `libultra.c` (block 4 only; persists pristine+migrated bytes before the read-time-only AllUnlocked patch). Verified: user's #87 artifact → `D297-migrated 5`, all slots OK, idempotent on relaunch; native post-fix save untouched; corrupt slot left to the game's own wipe path; fresh eep untouched; 21/21 sweep PASS. Block-0 header needed no migration (factory seal, verified). | @@ -714,8 +714,8 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | 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). | +| 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 (census + `GE_HIGHARENA_BASE` tier-1 build + 35 sites fixed + CI ratchet, 2026-10-03; tier-1 menu and `-level_09` pixel-identical; owed tier-1 all-level sweep and the above-4-GiB tier). | +| 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; verified 2026-10-03 headless (mission select all 20 / only real progress when OFF, full cheat menu, eep byte-identical). | | 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). | | D454 | **Knife slash crashed on Windows: `var_a0_2` in `gunTickHandState` was `u32`, truncating the exe-data address of `D_80034CA4/E0C` (PE image base 0x140000000) before `gunSample1PTransform` (ABI/layout exception, §A1).** Full `## D454` entry at file tail | FIXED (verified by maintainer live: knife slash no longer crashes). | | D461 | **Item-slot allocation overran into the next stage-bank block: `alloc_additional_item_slots` used the N64 0x14 stride for a 32-byte `InvItem` (the code comment tags it D458b); `bondinvReinitInv` wrote `p_itemcur[i].type = -1` at the C stride, scribbling 0xffffffff into the first prop model file (TT33 root Child) and crashing 2P respawn / pickups later (ABI/layout, section A1).** Full `## D461` entry at file tail | FIXED (maintainer-verified: ~10 min of 2P on Caves, Bunker and Complex, no crash). | @@ -753,6 +753,24 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D483 | **#136: the PC crouch key stood Bond up when he switched to the sniper rifle; the N64 keeps the stance (the weapon's DISABLE_CROUCH flag blocks both crouch-down and crouch-up)** — full `## D483` entry at file tail | FIXED 2026-10-02 (local, `fix/d483-sniper`); scripted crouch -> switch -> release keeps SQUAT | | D484 | **#136: `portScaleFovY`'s 20-degree floor raised the game's own zoom FOVs, capping sniper/camera magnification at 20 degrees instead of the N64's 7 (about a third of the zoom)** — full `## D484` entry at file tail | FIXED 2026-10-02 (local, `fix/d483-sniper`, maintainer-approved 2026-10-02); full zoom renders at 7 degrees, normal views pixel-identical | | D485 | **#136: sniper/camera zoom stepped once per game frame with no tick scaling, so at the port's 1 tick/frame it zoomed 2x faster in real time than the N64 at 2 ticks/frame; steps now scale to base^(ticks/2) (RULE-2, maintainer-approved)** — full `## D485` entry at file tail | FIXED 2026-10-02 (local, `fix/d483-sniper`); 15 -> 7 takes 16 frames at 60 fps (was 8) = the N64 at 30 fps | +| D486 | **#126: explosions dropped in heavy firefights (turrets on Cradle/Egypt/Aztec): the 6-slot explosion buffer filled with the player's own bullet-impact puffs** — full `## D486` entry at file tail | FIXED (a)(c)(d); drops + (b) faithful, closed. Maintainer turret-fight feel check owed | +| D487 | **PAL: the in-level ammo counter was not drawn (only a small dot where the "7 / 93" counter should be)** — full `## D487` entry | FIXED; maintainer PAL play check 2026-10-02: "PAL seems fine" | +| D488 | **PAL build reported the Brazilian MPAL TV type instead of PAL (`osTvType`)** — full `## D488` entry at file tail | FIXED | +| D489 | **FPS dropped sharply while adjusting F10 watch sliders (music/FX) with a pad or arrow keys: every step was a full EEPROM persist on the game thread** — full `## D489` entry at file tail | FIXED 2026-10-02 (port-only UI layer, `fix/d489-pad-slider-commit`, merged into `release/v0.4.1` 2026-10-03); maintainer pad check passed 2026-10-03 | +| D490 | **Dam first-tower guard walked a small circle: `sub_GAME_7F03ECC0` overflowed `s32 rem[4]` on flat bboxes (glass/windows), corrupting the collision polygon; `rem[8]` under `AVOID_UB` (UB/stack-layout class, porting-notes D24)** — full `## D490` entry at file tail | FIXED 2026-10-02 (`fix/d490-glass-hull`, merged into `release/v0.4.1` 2026-10-03); maintainer by-eye Dam check passed 2026-10-02 | +| D491 | **Struct reads across separately-declared adjacent globals (the D5/D215 class), found by the D24 overflow audit: `bg.c` `sub_GAME_7F0BA2D4` (portal bbox seeds, so every portal's Y/Z max stayed FLT_MAX and props joined extra rooms in `chrpropUpdateRoomList`) and both `gunfire.c` casing `rot` reads (y/z from `g_ZeroTriggerState`); `#ifdef PORT` reads the named globals** — full `## D491` entry at file tail | FIXED 2026-10-03 (`fix/d491-adjacent-global-reads`, local); builds clean, 900-frame `-level_09` smoke clean; no by-eye effect expected beyond room-membership-driven behaviour | +| D492 | **Emulator `.eep` saves are not drop-in compatible: the port stores the save checksum words and `options` little-endian, so an unconverted 1964 save fails `fileValidateSaves` and all five slots are wiped on first boot** — full `## D492` entry at file tail | OPEN (investigated 2026-10-03; fix decided: `tools_pc/eep_convert.py` converter + README section, on-disk format unchanged; queued after the D441 census) | +| D493 | **Pad rumble kept going after quitting the game: the clean exit never runs `inputDestroy`, so `portAtExit` now calls `inputRumbleStopAll()`; same commit fixes D493b, a 1-unit live-pixel strip at the pillarbox edges in Original mode** — full `## D493` entry at file tail (D493b is a sub-section) | FIXED 2026-10-03, maintainer-verified in play (DualSense; Original-mode edges incl. ultrawide) | +| D494 | **Sniper rifle: the PC crouch key now crouches/stands freely with a disable-crouch weapon (GEPD parity), never injecting C-down for it; supersedes D483's stance freeze** — full `## D494` entry at file tail | FIXED 2026-10-03, maintainer-verified in play | +| D495 | **Tank: with the default pad preset X changed the tank weapon and Y left it, because in-tank faces fell back to the fixed menu faces; `PadMapCtx.tank` routes Next/Prev weapon to N64 A and aim/fire to R/Z** — full `## D495` entry at file tail | FIXED 2026-10-03, maintainer-verified in play | +| D496 | **3P/4P split-screen: weapon-pickup messages garbled and oversized — `generate_language_specific_text_for_weapon` strcat'd onto an uninitialised stack buffer with 3+ players; `finalstring[0] = '\0'` under `AVOID_UB` (porting-notes D24 class)** — full `## D496` entry at file tail | FIXED 2026-10-03 (condensed 3+P text is faithful, maintainer-confirmed) | +| D497 | **PC Options paging now matches the game's dossiers: PREVIOUS tab/B turn back a page or leave, NEXT folder tab while a later page exists; D444's bottom-row page links removed** — full `## D497` entry at file tail | FIXED 2026-10-03, maintainer-verified in play | +| D498 | **Xbox 1.2 Christmas / 1.3 Frost / 1.4 Elektra gamepad presets: `Input.PadPreset` gains 1/2/4 (stick roles per D394, buttons unchanged); Jinx and Custom keep `Input.NaturalPitch`** — full `## D498` entry at file tail | FIXED 2026-10-03, maintainer-verified with a pad | +| D499 | **Trilinear texture filter option: `Video.TextureFilter = 3`, every texture gets generated mips at upload and samples `GL_LINEAR_MIPMAP_LINEAR`** — full `## D499` entry at file tail | FIXED 2026-10-03, maintainer-checked (no visible difference; kept) | +| D500 | **Aztec shuttle area: polygons "fell apart"; the per-frame vtx pool (`dyn.c`, no bounds check, Aztec `-mvtx40`) could overrun into the half the render worker draws; moved to 2x384 KB at DRAM +0x700000 with a budget log** — full `## D500` entry at file tail | FIXED 2026-10-03, maintainer-verified at the Aztec spot | +| D501 | **Ultrawide 16:9 pillarbox for cutscenes and the watch: built, then reverted (original framing, not breakage)** — full `## D501` entry at file tail | CLOSED (reverted 31ade91f) | +| D502 | **PC Options pad navigation: highlight on enter/back/page, back lands on the origin section, crosshair snap moved left; Trilinear listed after Bilinear** — full `## D502` entry at file tail | FIXED 2026-10-03, maintainer-verified with a pad | +| D503 | **Surface far dish shows as a faint silhouette with straight edges: ~97% fogged, the N64's 16-bit colour hid it, 32-bit output reveals it (any settings)** — full `## D503` entry at file tail | KNOWN ISSUE | | D407 | **Tanks cannot be boarded/exited on PC — the v0.4.0 use/reload split (D378/D393) removed the B-button tap the engine's tank handlers in `bondview2.c` consume (user report 2026-09-28, Runway/Streets)** — full `## D407` entry at file tail | PARTIAL (fix landed in v0.4.0, port-only): present `GE_CONT_B` on the use (E / pad A) rising edge only while `g_PlayerIsInTank == 1` or `g_BondCanEnterTank != 0`; E keeps its D378 no-reload-fallback semantics elsewhere, N64 layout unaffected. D407(b) same cycle: front PC Options page-edge highlight clamp + mouse-wheel / W-S paging (wheel queue consumed on the menu, D223); its wheel mapping shipped inverted and was fixed to match W/S (wheel up = step up). D407(c): board-animation lockout, menu-accept B gate in tank states, in-tank aim routed through the legacy velocity stick + `Input.TankAimScale` knob. RESOLVED 2026-09-28: all port-only (src/game zero-diff), 10 TANKDBG probes stripped, release binary verified clean, user live tank drive signed off (board + exit OK, aim feel good). | | D408 | **`Game.SkipIntro` skips the post-mission failure dossier: with it on, entering a level then aborting (watch Z+A) or dying (KIA) returns straight to the menus — no REPORT / "Mission status: KILLED IN ACTION / ABORTED" screen** (user report 2026-09-28; save/AllUnlocked ruled out) — full `## D408` entry at file tail | OPEN (cosmetic, not root-caused past the handoff site). v0.4.0 decision: SkipIntro stays EXPERIMENTAL / not recommended for regular users — F10 row relabelled "Skip intro (EXPERIMENTAL)". | | D450 | **Unplugging and replugging a controller was not picked up (user, GameSir-G7 Pro; 2026-09-29).** — full `## D450` entry at file tail | FIXED — port-only: the render thread's `gfx_sdl_handle_events` drains the same SDL queue as video.c's host pump but had no case for `SDL_CONTROLLERDEVICEADDED/REMOVED`, so it usually swallowed them and `inputRescanPads` never ran. Both pumps now post a request; the rescan runs at the top of `inputUpdate`, on the thread that reads the pads. | @@ -11904,7 +11922,7 @@ Found by the D258 PAL bring-up (2026-10-02): `-level_33` Dam on the PAL build (` **Fix (port-only, `#ifdef VERSION_EU`):** decode with the EU rule (smallest height whose EU encoding reproduces `yScale`; fallback 272 = `fr.h` `SCREEN_HEIGHT_272`), keyed on the build. `video.c` `GE_NATIVE_H` for PAL (pre-VI seed only) corrected 400 -> 538 with the TCRF facts. NTSC-U/JP code unchanged. **Verified:** PAL Dam frame 1500 shows the full "7 | 93" ammo counter; the PAL legal screen shows its last line ("Used by permission of EMI Unart Catalog Inc."); NTSC-U `verify.sh dam` PASS; JP build/run unchanged. **Open question (resolved by D488, 2026-10-02):** the port set `osTvType = OS_TV_MPAL` for EU (libultra.c, comment says "PAL consoles report MPAL"). A real PAL N64 reports `OS_TV_PAL` (0). GE's `sched.c:287` and `init.c:237` test only for MPAL (else NTSC LAN1), so on real PAL hardware those take the NTSC branch while the port takes MPAL; the EU `video_related_8` then installs its own PAL-table modes (`osViModeTable[16]/[20]`), so the visible effect may be nil. Needs a 1964 PAL comparison before changing (it would also move the port's 50 Hz tick selection, which keys on MPAL||PAL). -**Status:** FIXED (maintainer by-eye check on PAL owed). +**Status:** FIXED; maintainer PAL play check 2026-10-02: "PAL seems fine". ## D279 — Backfilled — this label was consumed by an undocumented partial session (2026-09-14 ~23:00) that left artifacts in `scratch/` (`d279_run.log`… @@ -12060,6 +12078,8 @@ Found by the D258 PAL bring-up (2026-10-02): `-level_33` Dam on the PAL build (` **Maintainer check 2026-10-02:** max FOV + max draw distance, fast strafe past the Statue plaza/park corners: no flicker. VERIFIED, closed. +**Spawn check 2026-10-03 (headless, `GE_D104=1`, 900 frames, 640x480):** Statue (22), Streets (29), Jungle (23), each at default and at max (`FovScale 150`, `DrawDistance 400`, `LodDistance 400`): 0 `D104 SKIP`, 0 `D104 LOAD ... FAIL`; at max, Statue's spawn draws 8 rooms (3 at default), all loaded. So the 2.5x pool has headroom at spawn; the by-eye monument check (walk to the CCCP monument at max FOV) is still the maintainer's. + ## D211 — PC FOV slider — `Video.FovScale` (M-83 QoL wave, port-only, `WIDESCREEN-FOV-PLAN.md` Phase 4). **PC FOV slider — `Video.FovScale` (M-83 QoL wave, port-only, `WIDESCREEN-FOV-PLAN.md` Phase 4).** fast3d applies a multiplicative half-angle scale to every *perspective* projection matrix as it is loaded (`gfx_apply_fov_scale()` in `gfx_pc.cpp gfx_sp_matrix`, on the `G_MTX_PROJECTION | G_MTX_LOAD` path only — GE always reloads its view projection via `guPerspectiveF` at `fr.c:725`, so a relative `G_MTX_MUL` onto an already-scaled matrix cannot compound). Columns 0 and 1 are scaled by the same factor `tan(half_orig)/tan(half_adj)` → H and V FOV widen uniformly, aspect ratio exactly preserved (no stretch), and because the game rewrites fovy every frame for aim-zoom the scale composes with zoom for free. Ortho matrices (`P[3][3]==1`) are left untouched so HUD/menu 2D is unaffected. `Video.FovScale` = percent of the original vertical FOV, 50–150, **default 100 ⇒ `g_fov_scale == 1.0f` ⇒ guarded early-return, zero FP ops on the matrix ⇒ golden dumps byte-identical**. F10 overlay row "FOV scale %". Math checked offline (`s=1.25 → fovy 60°→75°`, aspect held at 1.3333). @@ -15293,6 +15313,62 @@ Reconstructed stub — the original full entry was lost when `findings.md` was t **Status: PARTIAL (port 1.1 gameplay button roles user-tested; exact gadget-category parity and other selectable presets outstanding; RB HD toggle unavailable)**: user supplied the Xbox 1.1–1.4 control table; B is gadgets, stick clicks crouch, LB aim, RB HD toggle (not weapon cycling). +**Reference table restored (maintainer, 2026-10-03, from StrategyWiki's GoldenEye 007 controls page, "Xbox One/Xbox Series X|S" section).** The four Xbox styles share every button; only the sticks and D-pad differ. + +| Xbox control | All four styles | +|---|---| +| RT | Fire | +| LT, LB | Crosshair aim | +| A | Action | +| X | Reload | +| B | Cycle gadgets | +| Y | Cycle weapons | +| L / R stick click | Crouch | +| RB | Toggle updated graphics (no port equivalent) | +| Start | Menu / pause | +| Back (View) | View scores (the port uses Back for the F10 overlay) | + +Stick cells on the source read "while aiming / up-down axis / left-right axis". The D-pad copies the left stick in every style. + +| Style | Left stick (aiming / up-down / left-right) | Right stick (aiming / up-down / left-right) | +|---|---|---| +| 1.1 Jinx | crosshair / move / strafe | strafe / look / turn | +| 1.2 Christmas | crosshair / move / turn | strafe / look / strafe | +| 1.3 Frost | crosshair / look / strafe | strafe / move / turn | +| 1.4 Elektra | crosshair / look / turn | strafe / move / strafe | + +**Mapping onto the port (input.c stick block, 2026-10-03):** `naturalPitchMode` on (SOLITARE) = left stick to C-buttons (move/strafe), right stick to the N64 stick (look/turn) = **1.1 Jinx** (today's default). `naturalPitchMode` off (HONEY) = left stick to the N64 stick (move/turn), right stick to C-buttons (look/strafe) = **1.2 Christmas**. **1.3 Frost** = HONEY with the sticks swapped; **1.4 Elektra** = SOLITARE with the sticks swapped (neither exists yet). Divergence in all styles: on Xbox the LEFT stick moves the crosshair while aiming and the right stick strafes; the port aims with whichever stick feeds the N64 stick (the right stick under Jinx). The D-pad currently sends N64 D-pad bits, not a left-stick copy. + +**Original N64 control styles (historic reference; same StrategyWiki page, "Nintendo 64" section, maintainer 2026-10-03).** Stick cells read "while aiming / up-down axis"; the stick's left-right axis always turns. Enum: `CONTROLLER_CONFIG_*` (`src/bondconstants.h`); names `front.c` ~732; watch control-screen labels `options.c` ~149. + +Single controller: + +| N64 control | 1.1 Honey | 1.2 Solitaire | 1.3 Kissy | 1.4 Goodnight | +|---|---|---|---|---| +| Control stick | crosshair / move (left-right turns) | crosshair / look (left-right turns) | crosshair / move | crosshair / look | +| Z | Fire | Fire | Aim | Aim | +| B | Reload / action | Reload / action | Reload / action | Reload / action | +| A | Change weapon | Change weapon | Fire | Fire | +| L or R | Aim | Aim | Change weapon | Change weapon | +| C left/right | Strafe | Strafe | Strafe | Strafe | +| C up/down | Look up/down | Move forward/back | Look up/down | Move forward/back | +| D-pad | Same as C | Same as C | Same as C | Same as C | +| Start | Pause | Pause | Pause | Pause | + +Two controllers (one player holds both): + +| N64 control | 2.1 Plenty | 2.2 Galore | 2.3 Domino | 2.4 Goodhead | +|---|---|---|---|---| +| Controller 1 stick | crosshair / move | crosshair / look | crosshair / move | crosshair / look | +| Controller 1 Z | Fire | Fire | Aim | Aim | +| Controller 2 stick | strafe / look | strafe / move | strafe / look | strafe / move | +| Controller 2 Z | Aim | Aim | Fire | Fire | +| B | Reload / action | Reload / action | Reload / action | Reload / action | +| A | Change weapon | Change weapon | Change weapon | Change weapon | +| Start | Pause | Pause | Pause | Pause | + +How the Xbox styles relate: 1.1 Jinx is Solitaire's movement split onto two sticks (left = C-buttons move/strafe, right = N64 stick look/turn), which is why the port drives Jinx through `CONTROLLER_CONFIG_SOLITARE` (D194/D238); 1.2 Christmas is Honey split the same way (left = N64 stick move/turn, right = C look/strafe). 1.3/1.4 swap the sticks of 1.2/1.1. Xbox drops the Kissy/Goodnight button layouts and the 2.x two-pad styles. + ## D395 — Controller trapped in keyboard/mouse-only binding capture (2026-09-27). Reconstructed stub — the original full entry was lost when `findings.md` was truncated to its section table (recovery, 2026-09-28); this entry is rebuilt from the section-table row and the `findings-index.csv` one-liner. Original root-cause detail is not recoverable. @@ -15937,7 +16013,16 @@ Under PORT `struct image_entry` puts `dataoffset:24` in bits 0-23 (D67), so byte **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). +**Status:** PARTIAL (part 1 landed; census and high-arena test build done 2026-10-03; full-game tier-1 sweep and the above-4-GiB tier owed). + +**2026-10-03: complete census + high-arena test build (batches 1-5).** Lead-planned, the mechanical edits applied by the local Qwen worker one prompt at a time, each lead-verified (diff, rebuild, runs). +- **Census:** one machine-readable list replacing parts 1/2: 424 compiler sites (`-Wpointer-to-int-cast` 276, `-Wint-conversion` 102, `-Wint-to-pointer-cast` 46; prior tags carried forward for 385) plus 460 raw grep hits for the classes no compiler flags (sizeof copies, raw-offset aliases, address conversions, bit-31 tests, negated indexes, stride allocs, s32 sentinels, raw struct I/O). Local artefacts: `scratch/d441/` (sites_tagged.csv, invisible_hits.csv). +- **High-arena test build:** `GE_HIGHARENA_BASE=0x90000000 ./build-pc.sh` (CMake option, env fallback) overrides `PORT_DRAM_V1_BASE`; `dram_syms.s` became a preprocessed `dram_syms.S`, so the three link-time symbols follow it. Default build unchanged (`nm`: 0x70000000 / 0x70050000 / 0x707ffd30). With bit 31 set, every `s32`-held arena pointer read back as a pointer sign-extends to 0xffffffff9xxxxxxx and faults, so the crash order is the work list. +- **Fixed (35 sites, all `(T *)(uintptr_t)(u32)(EXPR)` under `#ifdef PORT`, `#else` byte-identical; identical values at the default arena, correct for any arena below 4 GiB):** batch 1, animation tables (`initanitable.c` x3, `chr.c` x3, `chraction.c`, `chrai.c`, `propobj.c`, `bondview2.c`, `initactorpropstuff.c`); batch 2, model-GDL copy `sub_GAME_7F0762E0` (`objecthandler_2.c` x6), `mema.c` allocator return, `rsp.c` RDP output buffer x2; batch 3, implicit conversions (`chr_b.c` `s32 opcode` passed as `ModelNode *` (gdb-confirmed: `Switches[4]`=0x9022ad42 arrived as 0xffffffff9022ad42), `front.c` x2, `title.c`, `bondview2.c` `start_stan` x3, `image_bank.c` `texSetBitstring`, `mema.c` `memaRealloc`); batch 4, `chraction.c` `pathid` (an `s32` that carries a `PathRecord *`), `chrprop.c` hit `unk44`; batch 5, `bondview2.c` held weapon `helddst` and camera buffer `resolution`, `mpmenu.c` rank strings x2. Every `-Wint-to-pointer-cast` row is now fixed or ruled out (u32 sources, ROM offsets, literals, audio params, dead code). +- **Port-layer fix found by the test build:** `gfx_tex_source_is_c_array()` (`gfx_pc.cpp`) bounded the DRAM range with a `0x90000000` literal; with the arena there the range was empty, every DRAM texture was bswapped as a C array, and the legal screen drew with mirrored texel pairs (no crash). Now the two mapped views from `portaddr.h`. Porting-notes §A-buf cross-tag. +- **Ratchet:** `tools_pc/abi_ratchet.py` + `abi_baseline.json` + a windows-build CI step fail if any file's count of the three warnings rises; re-baselined 424 -> 389 after the batches. +- **Verified:** each batch: default build, `-level_09` 900 frames rc=0; `GE_PCDUMP` menu frames 60-480 identical between the default and tier-1 builds (pixel-exact). After batch 5 the tier-1 build runs `-level_09` to `GE_QUITFRAME`, and seeded runs (`GE_RSEED=0x0123456789abcdef`, `GE_INPUTSCRIPT=20:START`) give pixel-exact identical level frames (unseeded level runs differ run-to-run, so an unseeded frame diff is not evidence). +- **Owed:** the tier-1 sweep over every level (seeded frame compare); the above-4-GiB tier, which needs the `u32` storage itself widened (or `portHostToN64` under the macOS port_addr model), not zero-extension; the remaining `-Wint-conversion` and `-Wpointer-to-int-cast` rows (most are truncating stores that are harmless below 4 GiB). ### D387 Fix (2026-09-30) @@ -15980,6 +16065,8 @@ Side effects beyond unlocks (read from code): `fileGetHighestStageUnlockedForFol **Confidence:** high on the removal and the eep-diff result; medium on the UI outcome until eyeballed. +**Verified 2026-10-03 (headless frame dumps, `scratch/pinrun_au.sh`):** ON: mission select shows all 20 levels; the real menu path ticks every difficulty; the cheat menu lists all 23 cheats (Magnum, Laser, Golden Gun included). OFF: mission select shows only the save's real progress (Dam). `data/ge007.eep` byte-identical before and after every run. `GE_STARTMENU=8`/`=21` direct boots are not a valid check (they skip folder/page setup: an empty cheat page, a mission-header difficulty page). + ## 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. @@ -16565,6 +16652,7 @@ Whichever loop runs `SDL_PollEvent` first gets the event, and the render loop us **Port divergence:** the PC crouch key (`port/src/input.c`, the free-crouch block) treated a disable-crouch weapon like "crouch not allowed" and called `inputDropCrouch()`, which stood a port-crouched Bond up as soon as he switched to the sniper. **Fix (port layer):** while a disable-crouch weapon is out, the free-crouch block does nothing: no new crouch, no stand-up, and no injected C-down (which zooms out with this weapon). Switching back resumes the normal rules. `GE_INPUTLOG` logs `free crouch frozen (weapon N disables crouch, crouchpos=, key=)` once per entry. **Verified 2026-10-02 (Dam, `GE_STARTWEAPON=17`, scripted CHOLD / weapon switches / CREL, `GE_INPUTLOG`):** crouch key held with the sniper out while standing: frozen, no crouch (`crouchpos=2`); switched to another weapon: crouch applied; switched back to the sniper: frozen while crouched (`crouchpos=0`, SQUAT); key released: no stand-up. Matches the N64 rules above. +**Superseded 2026-10-03 by D494** (GEPD lets the crouch key work with the sniper; the freeze is gone). ## D484 — #136: the port's FOV floor capped sniper/camera zoom at 20 degrees @@ -16608,5 +16696,155 @@ Whichever loop runs `SDL_PollEvent` first gets the event, and the render loop us `port/src/libultra.c` hard-codes `osTvType` (no hardware to read). Since the first scaffolding commit (`cd6cc158`, 2026-08-17) the PAL build set `OS_TV_MPAL` (2), with the comment "PAL consoles report MPAL" -- an inference from GE's shared code, which only ever tests for MPAL (`sched.c:287`, `init.c:237`, `fr.c` non-EU: MPAL LAN1 mode, else NTSC LAN1). Not copied from the PD reference, which hard-codes `OS_TV_NTSC` for every build and whose own game code tests `OS_TV_PAL` separately. A European N64 reports `OS_TV_PAL` (0); MPAL is Brazil's 60 Hz PAL-M console. So on real PAL hardware GE starts the scheduler in NTSC LAN1 and the EU `video_related_8` (`fr.c`, `#ifdef VERSION_EU`) then installs the PAL-table modes; the port took the Brazil-only MPAL branches instead. Never noticed because the PAL build was first compiled on 2026-10-02 (D258). **Fix (port-only, maintainer go-ahead 2026-10-02):** `osTvType = OS_TV_PAL` for `REFRESH_PAL`, comment corrected; the port's 50 Hz tick now keys on `OS_TV_PAL` only (MPAL is 60 Hz; no build sets it). Not game logic (the value a console reports), so no Rule-2. **Verified (headless):** PAL Dam frames 900/1200/1500 identical to the pre-change build (3/3); the boot sequence (legal, Nintendo, Rareware, gun barrel) identical to the post-D487 capture on every shared frame, timing unchanged; front-end mode select and mission select render, mode select identical to NTSC-U (the stamp running off the bottom is the shared 440x325 menu layout). US/JP unaffected (the change is under `REFRESH_PAL`). -**Owed:** a PAL 50 Hz feel check against 1964 when the maintainer plays PAL. +**Maintainer PAL play check 2026-10-02:** "PAL seems fine" (no issues seen). **Status:** FIXED. + +## D489 — FPS drops sharply while adjusting the F10 music volume with a controller (maintainer, 2026-10-02) + +Seen during the 2026-10-02 PAL play session: holding/stepping the F10 overlay's music volume row with a pad made the frame rate "tank extremely". Possibly a general regression (not PAL-specific); not yet reproduced or investigated. Suspects to check first: per-step work on the music path while the value repeats (volume applied every repeat tick, a config save or audio re-init per step), the D470 master/music gain path, or the pad repeat rate in the overlay. +**Root cause (static, 2026-10-02):** every D-pad/stick/arrow step on a watch-setting slider (music, FX) went through `rowSet` -> `rowSetCommit(..., commit=1)` (`port/src/optionsoverlay.c`), so each held-repeat detent (every 4 frames after an 18-frame delay) was a full persist on the game thread: `fileGamePakProbe` + `fileWriteSave` -> three `joyDisablePoll`/`joyEnablePoll` handshakes (each blocks until `joyPoll` services it, `src/joy.c:745-817`) plus a 2 KB `ge007.eep` rewrite (`geEepromStore`) and a log line. A mouse drag stages with commit=0 and saves once on release, so only pad/keyboard stepping stalled. Not PAL-specific; the same path is on `main` (v0.4.0) and in the front PC Options screen (`frontoptions.c`, game thread). +**Fix (port-only, UI layer):** watch-slider steps in `rowAdjust` stage (commit=0) and record the field in `s_adjWatchField`; `optionsAdjustCommitPending()` saves once when left/right is released (F10 input loop, front options screen), when another watch slider is stepped, and when F10 closes. Toggle rows and non-watch sliders are unchanged. +**Status:** FIXED; merged into `release/v0.4.1` 2026-10-03. Maintainer pad check passed 2026-10-03 (held left/right on F10 music volume; FPS held). + +## D490 — Dam: the first-tower guard walks a small circle instead of patrolling around the tower (maintainer, 2026-10-02) + +Spotted in the first in-level 1964-vs-port A/B harness run (`tools_pc/abpad`): on 1964/GEPD the guard circles the outside of the first guard tower; on the port he walks a tight circle. Same at a 30 fps cap, so not the tick-rate class (D486). +**Diagnosis (temporary probes in `chrlvTickPatrol`/`chrlvTravelTick`/`sub_GAME_7F03ECC0`, removed):** the guard is chr 44 on patrol path 5. He reaches pad 26, then never reaches pad 25 (step 9): every straight-line test to it fails for `CDTYPE_PATHBLOCKER` only, so the travel code cycles its side-step modes around the pad forever. The blocker is the tower window (`PROPDEF_GLASS`, `PROP_WINDOW`, bound pad 92, prop at (16906, 445, 19472)). Its floor-plane collision polygon had a phantom vertex at (prop centre x, pane z), adding an edge across the walkway. Setup data ruled out first: Dam's patrol paths, 205 waypoints, 367 pads and 95 bound pads match the ROM byte-for-byte after the D88 conversion. +**Root cause:** `sub_GAME_7F03ECC0` (`src/game/chrprop.c`) builds an object's collision polygon from the 8 projected corners of its bbox. Its filter loop stores every corner that is not one of the four extremes into `s32 rem[4]`. The window's bbox is flat (z = [0, 0]), so the extremes coincide (minxi = maxzi = 2, maxxi = minzi = 4) and the loop writes 6 entries. On the PC, `rem[4..5]` (the indices 6 and 7) overwrote `pts[0][0]` (a tiny double, logged 0.0000 instead of -85.04); that corrupted corner became the phantom vertex. On the N64 the same overflow lands in the decomp's `f64 pad[1]` stack slot and is harmless, so N64 behaviour is the uncorrupted hull. Any object with a zero-thickness bbox (glass, windows) on any level got a wrong polygon on the port. +**Fix:** under `AVOID_UB` (defined in the PC build), `s32 rem[8]`; `rem[4]` kept under `#else`. The loops still read only `rem[0..3]`, so the result equals the N64's. No logic change (UB/stack-layout class). Implemented by the local Qwen worker from a lead brief; diff checked by the lead. +**Verified (headless, probe build):** `pts[0]` = (-85.04, 113.85), polygon = the pane line only; every collision class clear to pad 25; chr 44 walks steps 8 -> 14 -> 0 (the whole loop) instead of sticking at 9. Maintainer by-eye check in Dam owed. +**Status:** FIXED; merged into `release/v0.4.1` 2026-10-03. Maintainer by-eye check in Dam passed 2026-10-02. The broader audit for other D24-class overflows is still open (ROADMAP). + +## D491 — Struct reads across adjacent scalar globals: portal bbox seeds (bg.c) and casing rotation (gunfire.c) (D24 audit, 2026-10-03) + +Found by the porting-notes D24 audit (a `-Warray-bounds=2 -Wstringop-overflow=4` recompile of `src/`, plus `-fanalyzer` on `src/game`, which gave up on the large decomp functions and found nothing useful). Same class as porting-notes D5 and D215: the decomp reads a whole struct starting at one of several separately-declared globals, relying on N64 source-order layout. +**Evidence (`nm` on the built exe):** mingw lays the trios out in **reverse** order. `D_80044918..D_80044904` sit at `...d9f0..da04` (4-byte steps down), so `*(coord3d *)&D_80044904` read `D_80044904` then 8 bytes of `D_8004489C`; `*(coord3d *)&D_80044910` read `{D_80044910, D_8004490C, D_80044908}` = `{-FLT_MAX, FLT_MAX, FLT_MAX}`. `D_80035EA4` (gun.c, bss) is followed by `g_ZeroTriggerState`, not `D_80035EA8/AC`. +**Effect:** (1) `sub_GAME_7F0BA2D4` (`bg.c`, called from `chrpropUpdateRoomList` with a cap of 7 rooms) grows a portal's bbox from `portal_min`/`portal_max` seeds; with the Y/Z max seeded at +FLT_MAX it never shrinks, so `bgIsBboxOverlapping` passes whenever X overlaps and props/chrs are added to neighbouring rooms they do not touch (more per-room work; room-based checks see them in the wrong rooms). (2) Ejected casings (`gunfire.c` two sites) started from a rotation whose y/z were trigger-state bytes instead of 0. +**Fix:** `#ifdef PORT` reads the six/three named globals as `f32` (`*(f32 *)&D_...`); `#else` keeps the original cast. `gunfire.c` gains `extern` declarations for `D_80035EA8`/`D_80035EAC` under `#ifdef PORT`. Layout only (D5 class), no logic change. +**Audit results, rest (no fix needed):** `chrprop.c` `sub_GAME_7F03ECC0` writes up to 8 points into `rect4f.points[4]`: the obj/door storage is `collision_data.polygon[8]`, and the tank's spill past `TankRecord.rect` hits the same s32 fields on N64 and PC (writer and the only reader, `bondview2.c:2674`, agree). `gunfire.c` `ModelHeader` locals passed as `Model *` to `modelInit`: field offsets match `Model` on PC (probe: obj 16, render_pos 24, datas 32, scale 40, attachedto 48, attachedto_objinst 56; local is 64 B), so writes stay in bounds. `ALSoundState` warnings in `gunfire.c`: `gunGetFreeSfxState` returns a pointer-slot address typed as `ALSoundState *`; `link.next` is at offset 0, so the read is the slot itself (works at 64-bit). `snd.c` `sndCountAllocList`: already documented. `os.h` `OS_K0_TO_PHYSICAL`: constant-address false positive. A scan for the D490 append shape (`a[v] = ...; v++` into a fixed local) found only bounded or already-fixed sites (`stan.c` `tileStack[64]`, `bg.c` `addToByteSetMaxSize15`, `textrelated.c` `curword[32]` bounded by ROM word lengths). +**Status:** FIXED (local, `fix/d491-adjacent-global-reads`); builds clean; 900-frame `-level_09` smoke clean. + +## D492 — Emulator `.eep` saves are not drop-in compatible: stored checksum/options words are host-endian (investigated 2026-10-03) + +**Symptom (measured, isolated stage `scratch/eepinv/stage/`, repo `data/` untouched):** 1964 GEPD's `save/GOLDENEYE-usa.eep` (a full clear) copied verbatim as `data/ge007.eep` → `GE_SAVELOG`: `fileValidateSaves slot 0: bitflags=08 stored_crc=3bd4c57e,9014a5f6 calc_crc=7ec5d43b,f6a51490 -> BAD -> RESET (progress wiped)`, the same for slots 1-4; the wipe is written back to disk. +**Cause:** the save is byte-identical in layout (2048 B; block 0 `smallSave`, five 96-byte `save_data` slots from block 4, `src/game/file.h`), but `geEepromRW` (`port/src/libultra.c`) copies the structs raw, so the multi-byte fields are stored in host order: per slot `chksum1`/`chksum2` (s32 @0/@4) and `options` (u16 @12), plus block 0's checksum pair. Every other field is a byte or the byte-addressed `times[]` bitfield. `fileGenerateCRC` (`src/game/crc.c`) hashes raw bytes, so the CRC value is endian-independent; validation fails only because the stored words are read in host order (`calc_crc` above is exactly the big-endian reading of the stored words). +**Trap:** `options` lies inside the slot CRC range `[8..96)`, so a plain word swap still fails. Conversion per slot = swap `options`, then recompute `chksum1/2` over `[8..96)` and store them little-endian; block 0 = plain swap of its checksum pair (no multi-byte field in its CRC range). 58 of 2048 bytes change. +**Verified (headless A/B):** converted save → all five slots `-> OK`, `wsfront` reads its music byte, mission select shows all 20 levels; blank control → only Dam. `AllUnlocked = 0` in the stage ini (checked). The investigation's Python CRC replica (`scratch/eepinv/eepinv_analyze.py`, from `fileGenerateCRC` + `src/random.s`) matches the binary on both files. +**Decision (maintainer, 2026-10-03):** converter tool (`tools_pc/eep_convert.py`, both directions, `--verify`) + a README "Using an emulator save" section; the on-disk format stays as is (no save-path or game-code change). Queued after the D441 census. Seed: `scratch/eepinv/convert.py`. +**Class:** §A1-adjacent (serialized multi-byte fields are host-endian in this port; raw-byte CRCs hide it until the stored words are compared). Cross-ref D297 (validation-failure wipe class). +**Status:** OPEN (converter not written yet). + +## D493 — Pad rumble kept going after quitting the game (2026-10-03) + +**Report:** maintainer, DualSense, quitting mid-firefight left the pad rumbling. +**Cause:** `SDL_GameControllerRumble` runs on the pad for its duration; the pad-close path (`inputDestroy`) never runs because the host loop exits via `exit(0)` from the clean-exit path (only the atexit handler in `main.c` then runs). +**Fix (port only, commit 44728eb1):** `inputRumbleStopAll()` (`port/src/input.c`, declared in `port/include/input.h`) sends a zero rumble to every open pad and sets `s_rumbleStopped`, which makes `inputRumble` a no-op afterwards (the game thread is still running during atexit); called first in `main.c` `portAtExit`. +**Verified:** build only. +**Owed:** none. +**Status:** FIXED, maintainer-verified 2026-10-03. + +**D493b — Original N64 pillarbox showed live pixels in a 1-unit strip at the left/right edges (maintainer playtest of D447).** +**Cause:** the Original preset sets `Video.SafeAreaCrop = 0`, which also turns off D246's 1-unit horizontal trim, so D246's gap (unexplained; see D246) showed scene/background pixels. D447 had listed it as a known cosmetic. +**Fix (port only, same commit 44728eb1):** `gfx_dp_set_scissor` (`port/fast3d/gfx_pc.cpp`) clamps the RDP scissor to logical x [1, 319] when the crop is off and not split-screen; those columns show the frame clear (black). Geometry is not rescaled; crop-on (the default) is unchanged by construction. +**Verified:** headless `-level_09`, 1280x720, AspectMode=1, SafeAreaCrop=0, MSAA 1: output columns 160-162 and 1117-1119 (1 unit at 3x) are black, run exited cleanly. +**Owed:** none (maintainer-verified 2026-10-03). +**Status:** FIXED. +**Maintainer play check 2026-10-03:** rumble stops on quit (DualSense); Original-mode edges clean, also at ultrawide. + +## D494 — Sniper rifle: the PC crouch key now crouches/stands freely with a disable-crouch weapon (2026-10-03) + +#136 follow-up; supersedes D483's freeze. +**Report:** maintainer, controller and keyboard, hold and toggle crouch; 1964 GEPD lets Bond crouch and stand freely with the sniper. +**Cause:** D483 froze the port's free-crouch stance while a `WEAPONSTATBITFLAG_DISABLE_CROUCH` weapon is out, matching the N64 pad (`bondview2.c` turns aim-mode C-up/down into zoom for that weapon). GEPD's crouch key (the mouse injector, the reference for this preset) writes the stance directly and ignores the flag. +**Fix (port only, commit 0f24f407):** the free-crouch block (`port/src/input.c`) now crouches/stands for every weapon; for a disable-crouch weapon it never injects C-down (that would zoom out) and the "keep native C-down crouch" release exception does not apply. The N64 pad rules are untouched. +**Verified:** build only. +**Owed:** none. +**Status:** FIXED, maintainer-verified 2026-10-03. +**Maintainer play check 2026-10-03:** crouch and stand with the sniper fixed. + +## D495 — Tank: pad Next/Prev weapon changed the tank weapon / left the tank instead of following the bindings (2026-10-03) + +**Report:** maintainer: with the default pad preset, X changed the tank weapon and Y left the tank; on foot Y changes weapon. +**Cause:** in the tank `inputCanUseGameplayActions` is false, so `padMapPure` (`port/src/input.c`) used the fixed menu faces (A/X -> N64 A = change weapon, B/Y -> N64 B = exit). +**Fix (port only, commit a69cc144):** `PadMapCtx.tank`; while seated in the tank (slot 0, stage running, not paused, outside the watch) the pad's Next/Prev weapon bindings send N64 A, digital aim/fire bindings send R/Z, and the raw face buttons send nothing else; exit stays on the bound Use action via the existing D407 locked-B path. Preset 0 outside the tank is unchanged (GE_PADMAPTEST contexts leave `tank = 0`). +**Verified:** build only. +**Owed:** none. +**Status:** FIXED, maintainer-verified 2026-10-03. +**Maintainer play check 2026-10-03:** works, "feels good now". + +## D496 — 3P/4P split-screen: weapon-pickup messages garbled and oversized (2026-10-03) + +**Report:** maintainer, 3-player team game on Basement, natural weapon pickups. +**Cause:** `generate_language_specific_text_for_weapon` (`src/game/propobj.c`, US/EU path) writes `finalstring` only when fewer than 3 players, then strcat's onto it; the caller's buffer (`display_text_for_weapon_in_lower_left_corner`, `char acStack100[100]`) is an uninitialised stack array, so stack leftovers were drawn. Ammo pickups clear their buffer first and were fine. The N64 build has the same code; its stack contents happened to hide it. +**Fix (commit ba8c3942):** `finalstring[0] = '\0'` at the top of the function under `#ifdef AVOID_UB` (D490 / porting-notes D24 pattern), no other change. The same commit adds an optional third field to the GE_STARTMP harness (`src/game/lv.c`): ",,", e.g. 3,5,7 = Basement 2v1. +**Class:** porting-notes D24 (UB the N64 stack layout absorbed). +**Verified:** build only. +**Owed:** none. +**Status:** FIXED 2026-10-03 (uninitialised buffer); the condensed 3+P text itself is faithful. +**Maintainer play check 2026-10-03:** the condensed pickup text with 3+ players is the game's own: `prepare_ammo_type_collection_text` and this function drop "Picked up" when `getPlayerCount() >= 3`, so short 3P text is faithful (maintainer: fine). The uninitialised-buffer fix stands on its own. + +## D497 — PC Options paging did not match the game's dossiers (2026-10-03) + +**Report:** maintainer: only "previous" was a folder tab, and a redundant "Previous page / Next page" line sat under the rows (e.g. Graphics). +**Fix (port only, commit 31153ab3):** `frontoptions.c`, as the briefing screen (`front.c` `interface_menu0A_briefing`): the folder's PREVIOUS tab and B turn back a page, or leave the section/screen from its first page (`goPrevious`); a NEXT folder tab (`frontAddNextTabText` / `frontCheckCursorOnNextTab`) shows while a later page exists; D444's bottom-row links and `pageCtlAt` were removed. Up/Down still page. +**Verified:** build only. +**Owed:** none. +**Status:** FIXED, maintainer-verified 2026-10-03. +**Maintainer play check 2026-10-03:** PREVIOUS/NEXT tabs and B correct. Pad highlight/back-navigation issues found in the same check: D502. + +## D498 — Xbox 1.2 Christmas / 1.3 Frost / 1.4 Elektra gamepad presets (2026-10-03) + +ROADMAP 5a "Gamepad rebinding + modern controller presets"; reference table in `## D394`. +**Change (port only; commits e94621b5 by Qwen, merge fb8ed152, follow-up c12fc99e):** `Input.PadPreset` 0 = 1.1 Jinx, 1 = 1.2 Christmas, 2 = 1.3 Frost, 3 = Custom (unchanged value), 4 = 1.4 Elektra; menu order Jinx, Christmas, Frost, Elektra, Custom (`inputPadPresetStep`, `port/src/input.c`). Buttons are identical for all non-Custom presets. `stickSolitare()`: Christmas/Frost force HONEY, Elektra forces SOLITARE; Jinx and Custom keep `Input.NaturalPitch` (c12fc99e fixed the first version, which forced SOLITARE for Jinx too and broke the NaturalPitch=0 escape hatch). `stickSwap()`: Frost and Elektra swap the physical sticks in-stage (never in menus). +**Known limits:** the forced style applies to mouse and keyboard too (one control style per player), so a mouse user on Christmas/Frost gets the legacy D166 pitch pulses; after a swap the deadzone/sensitivity settings follow the function (look settings apply to whichever stick looks), not the physical stick; the Xbox "left stick aims the crosshair" behaviour and the D-pad-copies-left-stick behaviour are not reproduced (see D394). +**Verified:** build only. +**Owed:** none. +**Status:** FIXED, maintainer-verified 2026-10-03. +**Maintainer pad check 2026-10-03:** 1.2 Christmas, 1.3 Frost and 1.4 Elektra work as described. + +## D499 — Trilinear texture filter option (2026-10-03) + +**Report:** maintainer playtest request ("add trilinear in addition to bilinear"). +**Change (port only; commit d796b3dc by Qwen, merge 02786004):** `Video.TextureFilter = 3` "Trilinear" (F10 / PC Options Texture filter row). fast3d `FILTER_TRILINEAR` (`port/fast3d/gfx_opengl.cpp`, `port/fast3d/gfx_rendering_api.h`): every texture gets `glGenerateMipmap` at upload and is sampled `GL_LINEAR_MIPMAP_LINEAR` (min) / `GL_LINEAR` (mag). Bilinear (1) keeps mips only where the game supplies LOD tiles; 3-Point (2) unchanged. +**Note:** 3-Point already uses generated mips for minification (B1 shimmer fix), which is softer at distance than GLideN64/1964 (no generated mips). +**Verified:** build only. +**Owed:** none. +**Status:** FIXED 2026-10-03 (kept; imperceptible on the dev machine). +**Maintainer play check 2026-10-03:** no visible difference on the dev machine (2644x875, aniso 16x: most textures are magnified, and Bilinear already blends between game-supplied mips). Kept; listed after Bilinear for A/B (D502 commit: menu order 0, 1, 3, 2). + +## D500 — Aztec, by the shuttle (4 turrets + glass): polygons "fell apart" before the glass was shot (2026-10-03) + +**Report:** maintainer playtest (D486 turret check, 60 fps): random triangles across the area in front of the glass between two turrets; recovered when walking on, and after the glass was shot. +**Cause (code-established, not confirmed live):** `src/game/dyn.c`'s per-frame "vtx" pool (`dynAllocateVertices`, `dynAllocateMatrix`, `dynAllocateLights`, `dynAllocate`) is a bump allocator with no bounds check, sized per level by `src/boss.c` `memallocstringtable` `-mvtx` (Aztec and Streets 40 KB per half, most levels 50, MP maps 100). The port's wider FOV / draw distance / LOD (porting-notes D18) puts more models and matrices in view than that budget assumed, so a busy frame can run past its half into the other half, which the D481 render worker may still be drawing. Code map: Qwen Q4 (glass shards, autogun beams, bullet sparks and model matrices all draw from this pool; spot-checked). +**Fix (`#ifdef PORT`, commit f7868308; N64 branch unchanged):** `g_VtxBuffers` point at a fixed pool of two 384 KB halves at `PORT_DRAM_V1_BASE + 0x700000` (free emulated DRAM above the D95 mempool end, below `animations_frame_buffer` at +0x7FFD30), so addresses stay 0x70xxxxxx and survive every s32 path. A first version placed it in the executable's .bss (above 4 GB); rejected before commit for that reason. The stage-bank allocation is no longer made. `dynSwapBuffers` logs once per level `D500: vtx pool: frame used N bytes, N64 budget B` when a frame exceeds the N64 budget, and `D500: vtx pool OVERRUN` past the port pool. +**Class:** porting-notes D18; cross-ref D95 (gfx pool scaled for 16-byte Gfx). +**Verified:** build; `-level_09` intro renders (Bond model) and exits cleanly. Maintainer sessions afterwards (Aztec, Cradle, ultrawide): no `D500` line logged, no crash. +**Owed:** none. +**Status:** FIXED, maintainer-verified 2026-10-03. +**Maintainer check 2026-10-03:** the Aztec shuttle spot (glass intact) no longer breaks up. + +## D501 — Ultrawide: 16:9 pillarbox for cutscenes and the watch, REVERTED (2026-10-03) + +**Report:** at > 16:9, cutscenes show past their framing (guards at the edges) and the watch raise/lower shows the end of Bond's arm model for a frame or two. +**Analysis (Qwen Q5, lead-verified):** native widescreen projects at the window aspect (`bondview2.c` ~8157 `portNativeAspect`, `fr.c:735`); scripted cameras and the watch zoom were authored for 4:3/16:9; the watch face has its own fixed-aspect projection (`bondview2.c` ~8671/8673). +**Change, then revert:** commit 6f1f49bd pillarboxed > 16:9 windows to 16:9 during scripted cameras and the watch (`Video.UltrawideCinematic`); reverted in 31ade91f. Maintainer: the switching is clunky; these are the original game's framing shown at 21:9, not breakage. A toggle can come later if wanted. +**Related:** ultrawide performance checked by the maintainer (Aztec, Cradle, 21:9 and wider): good; `GE_PERFSTAT` showed steady 60 fps at 2644x875, FOV 150. +**Status:** CLOSED (reverted). + +## D502 — PC Options pad navigation: no highlight on entering, wrong section on leaving (2026-10-03) + +**Report:** maintainer (pad): entering a section highlights nothing; leaving can land on another section (leave Input, land on Graphics); the snapped crosshair overlaps the row text. +**Cause (`port/src/frontoptions.c`):** `goBack` snapped the cursor to the origin section but left `s_hl` at the row index of the sub-page (row 1 inside Input -> section 1, Graphics); the snap also updates the "previous cursor", so hover never corrected it. Entering set `s_hl = -1` for every input device. +**Fix (port only, commit fa0adbf2):** `navHighlight(k)`: pad/keyboard (pointer not live) get item k highlighted on enter, back and page turn; with a live mouse the pointer still owns the selection. Back lands on the origin section/row. Crosshair snap x 44 -> 32, `ROW_HIT_X0` 40 -> 24. Same commit: the Texture filter row cycles Nearest, Bilinear, Trilinear, 3-Point (stored 0, 1, 3, 2) (D499). +**Verified:** build only. +**Owed:** none. +**Status:** FIXED, maintainer-verified 2026-10-03. +**Maintainer pad check 2026-10-03:** first item highlighted on entering, back lands on the origin section, crosshair clear of the text. + +## D503 — Surface: far satellite dish shows as a faint silhouette with straight vertical edges (2026-10-03) + +**Report:** maintainer, Surface start area: the dish (and the edge of the tree band) looks cut off in the distance, at every Draw/LOD distance setting; 1964 GEPD at the same spot shows nothing there (`docs/img/surface radar visibility.png`, untracked). +**Measured (port capture, `build-pc/ppm/shot_000.ppm`):** dish pixels (93-95, 93-95, 124-127); Surface fog colour (96, 96, 128) (`bgfog.c:138`); sky/haze around (96-100, 96-98, 128-131). The dish is about 97% fogged. +**Mechanism:** Surface fog is `gSPFogPosition(996, 1000)` with near 2 / far 2500, a ramp from about 417 to 2456 units; the dish sits near its end. fast3d's vertex fog is the standard `z/w * fm + fo` (same as the PD port), and the Draw/LOD multipliers are exactly 1.0 at 100 (`video.c` `portDrawDistanceMultiplier` / `portLodDistanceMultiplier`; 100 is the slider minimum). The N64's 16-bit framebuffer (5 bits per channel) plus the full-fog clamp make a 2-5 level difference vanish; the port's 32-bit output keeps it, so the nearly fogged dish and the straight edge of GE's room-rect scissor (`propobj.c:7510` `getPropCombinedRoomsBBox2D` -> `bgScissorCurrentPlayerViewF`) become visible. Raised Draw/LOD distance makes more such objects visible. D491 (props filed under fewer, correct rooms) is not the cause. Same "reveal, not regression" pattern as D227/D249. +**Options (ROADMAP §5c row):** snap fog at or above ~97% to full (port-only, global by-eye pass); an N64 16-bit colour/dither output mode for the Original preset. +**Status:** KNOWN ISSUE (ROADMAP Known issues). diff --git a/docs/img/rareware logo - 1964 gepd.png b/docs/img/rareware logo - 1964 gepd.png new file mode 100644 index 00000000..22891689 Binary files /dev/null and b/docs/img/rareware logo - 1964 gepd.png differ diff --git a/docs/porting-notes.md b/docs/porting-notes.md index e3852ca6..7c09580f 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -294,6 +294,8 @@ When a PC-larger asset gets a bigger reservation inside a shared scratch buffer **A1 cross-tag (D457, 2026-09-30): a pure pointer-width retype can change behaviour by shifting the allocation layout.** Widening `struct player` (+32 B) / `struct Model` (+16 B, inline x4 in player) flipped the default-boot file-select menu to an all-black frame (no crash, identical log) while `-level_09` was fine; allocating the player block at its old size restored it. The menu depends on the MEMPOOL_STAGE layout (player block precedes `g_GfxBuffers[0]`, D98) through something address-sensitive. After any struct widening, run the default-boot menu with a `GE_PCDUMP` frame (not only a level) and check the frame extrema, not just rc. Also: a hardcoded record stride (`vtxstore.c` `n * 0x14` for a record that is 0x18 bytes on x86-64) is a silent overrun that widening makes worse; grep literals near allocs of retyped structs. +**A1 cross-tag (D441, 2026-10-03): the read side, `s32` back to a pointer, sign-extends.** An arena pointer stored in an `s32` (a local, a global, a field, a function parameter named like an id: `pathid`, `opcode`, `resolution`, `helddst`) truncates harmlessly below 4 GiB, but `(T *)s32` and the implicit `s32` -> `T *` conversion (a `-Wint-conversion` "makes pointer from integer", no cast to grep for) sign-extend bit 31. Invisible while the arena sits below 0x80000000; the tier-1 test build (`GE_HIGHARENA_BASE=0x90000000`) turns each one into a 0xffffffff9xxxxxxx fault, in crash order. Fix: `(T *)(uintptr_t)(u32)(EXPR)` under `#ifdef PORT`. Also grep the port layer for address-range **upper bounds**, not only base literals: fast3d's DRAM-vs-C-array texture test ended at a `0x90000000` literal, so with the arena moved there every DRAM texture was bswapped (mirrored texel pairs, no crash). Ranges must come from `portaddr.h` (base + `PORT_DRAM_SIZE`). And pin `GE_RSEED` + `GE_INPUTSCRIPT` before diffing level frames between builds: unseeded runs differ every frame. + ## B. 16-byte PC `Gfx` / `Vtx` vs 8-byte N64 Any buffer reservation, `memcpy` size, slot stride, or pool budget @@ -1109,6 +1111,13 @@ past it (walk off the end). links 0x60 bytes *before* `legalpage_text_array` → only line 1 renders (== the D76 "disclaimer half-drawn" bug; it was never an image-table issue). Fix: `#ifdef PORT` uses `array + ARRAY_COUNT(array)`. +- Instance: **D491**; the same assumption in struct reads, not loop bounds: + `*(coord3d *)&D_80044904` (bg.c portal bbox seeds) and + `*(coord3d *)&D_80035EA4` (gunfire.c casing rot) read three separately + declared scalars as one struct. mingw emits such runs in **reverse** order, + so the read picks up a different neighbour. `-Warray-bounds` flags these + ("partly outside array bounds of 's32[1]'"); confirm with `nm -n` on the + exe. Fix: `#ifdef PORT` reads each named global. - Grep for `= &` / `(TYPE *)&` on the RHS of a loop-terminator compare, and any `for`/`while`/`do` whose end pointer is the address of a *different* symbol than the one being iterated. @@ -1404,6 +1413,8 @@ A port stub that forces a value to `-1` (D154 `texnum`, D135) can hide latent la - **Techniques that worked:** run the original traversal twice with a port flag that pins the single far read (a static read, never `viSetZRange`) and snapshot the result; keep the "gameplay verdict" in an unused bit of a u8 flag byte (`prop->flags & 0x80`) written by every setter of the dual-use flag; store the authored fog values at load time instead of recomputing from the live multiplier. - **Triage trap:** check whether a `PROPFLAG_*` hit is on `prop->flags` (runtime u8) or on `obj->flags` / pdef flags (u32 definition bits) before treating it as a reader (`propobj.c:8417`, `prop.c` 233-467 are the latter). - **Remaining class member:** FOV/widescreen widening of `camIsPosInScreen` and `c_lodscalez` (D222). +- **Fixed-size per-frame budgets are readers too (D500):** the dyn.c vtx pool (`-mvtx`, as low as 40 KB) is bump-allocated with no bounds check and sized for what the N64 could see; widened views overran it (Aztec triangles). Look for unchecked per-frame pools sized by per-level tokens when a widening feature shows corruption only in busy scenes. +- **Reveal, not regression (D503):** the N64's 16-bit framebuffer hid 2-5 level differences (near-full fog against haze); 32-bit output shows them, plus the room-scissor edges of nearly fogged objects. Measure pixel values against the fog colour before blaming draw distance. ## D19. A "click after the shot" report needs the trigger-hold duration, not just the state machine (D433/D467) State-machine audits of a gun's fire/dry-fire path (GE: `gunTickHandState`, CLICKY weapons) are tick-driven and match the console at any tick/frame ratio; an extra click/sound that appears "with the shot" is usually the trigger still being down when the recoil state ends (about 14 ticks for the Golden Gun). Measure with `GE_STARTWEAPON` + `GE_INPUTSCRIPT` holds of 4/9/16 frames and a `state/mag/88C/890/clk` log at the sound-play sites before suspecting the audio layer; and remember `GE_QUITFRAME` counts frames from boot, the level's `g_GlobalTimer` starts ~270 later. @@ -1423,6 +1434,12 @@ Decomp code that advances a counter once per *call* (per rendered frame) instead PAL/JP had never been built. What broke was all port-side: (1) asset emitters hard-coded `scripts/filelist.u.csv` (so on a PAL/JP ROM they would use US offsets with no error); (2) `romdata.c` treated "not PAL" as NTSC-U (rejecting a JP ROM's country byte); (3) port code referenced globals that exist only under a region-specific define (`LEFTOVERDEBUG` is US/JP-only), which surfaces as a PAL-only link error; (4) the decomp's PAL/JP filelists were incomplete and mislabelled. With a real ROM, prove every filelist row by bytes (same-name US file comparison, constant-shift search, header-walk for variable files) instead of trusting labels. Whenever port code touches a symbol inside a `#if` region block, check all three `REGION_DEFS` sets. +## D24. Decomp out-of-bounds writes that the N64 stack layout absorbed (D490) + +Some decomp functions write past a local array in edge cases. On the N64, IDO's stack layout put a padding local (often a `pad[]` the decomp keeps for matching) right after the array, so the overflow was harmless. GCC lays the stack out differently, so the same write corrupts a live neighbour. D490: `sub_GAME_7F03ECC0` writes up to 6 entries into `s32 rem[4]` when an object's bbox is flat, overwriting `pts[0]` and giving every glass pane a phantom collision vertex (guards circled pads they could not reach). Tells: a bug that only appears for degenerate inputs (flat or zero-size boxes, coincident points); a value that is exactly 0 or a tiny denormal where a real number belongs; a `pad[]` local next to a fixed-size array in the decomp. Fix pattern: enlarge the array under `#ifdef AVOID_UB`, keep the original under `#else`, change no reads, so the result matches the N64's uncorrupted path. + +Also the read side: D496 (`generate_language_specific_text_for_weapon` strcat'd onto an uninitialised caller stack buffer for 3+ players; init under `AVOID_UB`). + ## E. Process / method notes - **Never `exit()` while another thread may be inside the GL driver (D344).** @@ -1848,6 +1865,8 @@ PAL/JP had never been built. What broke was all port-side: (1) asset emitters ha `inputComputePad` runs on the scheduler thread (`joyPoll`), a real pthread, not the game thread. Any save/swap/restore of `g_CurrentPlayer`/`player_num`/`g_playerPerm` from there races the game tick's own `set_cur_player`: the restore can leave the game thread on the wrong player and it then spins in list walks that compare against `g_CurrentPlayer->...` (seen: `bondinvCycleForward` infinite loop after a split-screen respawn). Resolve the owning player explicitly (`g_playerPointers[slot]`) and touch its fields directly; for game functions that take no player argument, present a native button bit instead of calling them. +**A1 cross-tag (D492, 2026-10-03): serialized multi-byte fields are host-endian in this port.** Structs copied raw to a file (the EEPROM save via `geEepromRW`) store s32/u16 fields little-endian, while N64 images are big-endian. A raw-byte CRC over the region gives the same value on both, which hides the difference until the stored checksum words themselves are compared. Tell: data that is intact but fails validation, with `calc_crc` equal to the big-endian reading of the stored word. Any field inside the CRC range must be swapped *before* recomputing the checksum. + **A1 cross-tag (D455, 2026-09-30):** an exe-static address (array/global/function in `.data`/`.rodata`/`.text`) held in a `u32`/`s32` truncates on Windows (image base `0x140000000`; Linux 0x20000000 hides it). Binary tell: `mov r64,[rip+.refptr.X]` ... `mov r32,r32` before the pointer is used or passed. The address load may precede a `jmp` join, so an adjacent-instruction scan misses it; follow jumps. A census of the whole exe found only the D454 site; `_*Segment*` linker symbols are ROM constants (`.set` in `romassets_u.s`), not exe addresses, and are benign in `(u32)` casts. **`GE_MEMPREDZONE=1` (D464): catch allocator overruns deterministically.** Set it before any run to pad every `mempAlloc*` block with a 64-byte pattern trailer and check at level unload, every 60 frames and on orderly quit. A hit prints `MEMPREDZONE: HIT ... caller=0x`; resolve with `addr2line -f -C -i -e build-pc/ge007.x86_64.exe 0x` to the allocation site, then compare its size expression with `sizeof` of how the buffer is indexed (an N64 literal stride vs a widened struct is the D461/D462 class). Zero hits is only "no overrun reached the trailer"; whole-bank allocations are unpadded until shrunk. Unset = one cached branch per allocation. diff --git a/port/fast3d/gfx_opengl.cpp b/port/fast3d/gfx_opengl.cpp index 4b33b375..e16b386f 100644 --- a/port/fast3d/gfx_opengl.cpp +++ b/port/fast3d/gfx_opengl.cpp @@ -744,7 +744,8 @@ static void gfx_opengl_upload_texture(const uint8_t* rgba32_buf, uint32_t width, } #endif glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba32_buf); - if (gen_mipmaps || current_filter_mode == FILTER_THREE_POINT) { + /* Trilinear option (playtest 2026-10-03): force mips for TRILINEAR too. */ + if (gen_mipmaps || current_filter_mode == FILTER_THREE_POINT || current_filter_mode == FILTER_TRILINEAR) { glGenerateMipmap(GL_TEXTURE_2D); } } @@ -764,24 +765,28 @@ static uint32_t gfx_cm_to_opengl(uint32_t val) { } static void gfx_opengl_set_sampler_parameters(int tile, bool linear_filter, uint32_t cms, uint32_t cmt, bool mipmaps) { - const GLint min_filters[3][3] = { + const GLint min_filters[4][3] = { // MIPMAP_DISABLED MIPMAP_NEAREST MIPMAP_LINEAR { GL_NEAREST, GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST_MIPMAP_LINEAR }, // FILTER_NONE { GL_LINEAR, GL_LINEAR_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_LINEAR }, // FILTER_BILINEAR { GL_NEAREST, GL_LINEAR_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_LINEAR }, // FILTER_THREE_POINT + { GL_LINEAR, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_LINEAR }, // FILTER_TRILINEAR (Trilinear option, playtest 2026-10-03) }; // B1: the shader does 3-point on magnification; for minification let the // hardware do trilinear mips. THREE_POINT force-generates mips at upload // (see gfx_opengl_upload_texture) so it can always sample them here -- this // is what kills the grazing-angle shimmer (Depot roof, docs/BRIEF-B2). - if (current_filter_mode == FILTER_THREE_POINT) { + /* Trilinear option (playtest 2026-10-03): TRILINEAR force-generates mips + * at upload too, so it can always sample them here. */ + if (current_filter_mode == FILTER_THREE_POINT || current_filter_mode == FILTER_TRILINEAR) { mipmaps = true; } mipmaps = mipmaps && (current_mipmap_filter_mode != MIPMAP_DISABLED); const int mip_idx = mipmaps ? current_mipmap_filter_mode : 0; const GLint min_filter = linear_filter ? min_filters[current_filter_mode][mip_idx] : GL_NEAREST; - const GLint max_filter = linear_filter && (current_filter_mode == FILTER_LINEAR) ? GL_LINEAR : GL_NEAREST; + /* Trilinear option (playtest 2026-10-03): TRILINEAR magnifies LINEAR too. */ + const GLint max_filter = linear_filter && (current_filter_mode == FILTER_LINEAR || current_filter_mode == FILTER_TRILINEAR) ? GL_LINEAR : GL_NEAREST; glActiveTexture(GL_TEXTURE0 + tile); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter); diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index a46ff540..753ed3fc 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -810,7 +810,11 @@ void gfx_texture_cache_delete_range(const uint8_t* start, const uint8_t* end) { static bool gfx_tex_source_is_c_array(const uint8_t* addr) { const uintptr_t a = (uintptr_t)addr; if (a >= 0x10000000u && a < 0x20000000u) return false; // cart map + sidecar - if (a >= PORT_DRAM_V1_BASE && a < 0x90000000u) return false; // V1 dram + KSEG0 mirror (portaddr.h) + // V1 dram + KSEG0 mirror: the two mapped views (port/src/dram.c). D441: + // was `a >= V1 && a < 0x90000000`, which went empty with the arena at + // 0x90000000 and bswapped every DRAM texture (mirrored texel pairs). + if (a >= PORT_DRAM_V1_BASE && a < PORT_DRAM_V1_BASE + PORT_DRAM_SIZE) return false; + if (a >= PORT_DRAM_K0_BASE && a < PORT_DRAM_K0_BASE + PORT_DRAM_SIZE) return false; return true; // exe image: C-compiled array } @@ -830,13 +834,6 @@ static const uint8_t* gfx_tex_normalize_source(const uint8_t* addr, uint32_t ext uint32_t* dst = (uint32_t*)buf.data(); for (uint32_t i = 0; i < n / 4; i++) dst[i] = PD_BE32(src[i]); - { - static int ge_d71log = -1; - if (ge_d71log < 0) ge_d71log = getenv("GE_D71LOG") != NULL; - if (ge_d71log) - fprintf(stderr, "[D71] normalized C-array texture source %p (%u bytes)\n", - (const void*)addr, extent); - } return s_c_array_tex_norms.emplace(addr, std::move(buf)).first->second.data(); } @@ -2800,23 +2797,6 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo buf_vbo[buf_vbo_len++] = u / tex_width[t]; buf_vbo[buf_vbo_len++] = v / tex_height[t]; -#ifdef PORT - { - static int ge_d116 = -1; - if (ge_d116 < 0) ge_d116 = getenv("GE_D116") ? 1 : 0; - if (ge_d116 && is_rect && t == 0 && tex_width[t] > 0 && tex_width[t] <= 32) { - fprintf(stderr, - "[D116/vbo] rect vtx%d x=%.4f y=%.4f u=%.4f v=%.4f " - "(raw v->u=%.1f texw=%.0f texw2=%u tile.uls=%d lrs=%d cms=%d)\n", - i, v_arr[i]->x, v_arr[i]->y, u / tex_width[t], v / tex_height[t], - (double)v_arr[i]->u, (double)tex_width[t], (unsigned)tex_width2[i], - (int)rdp.texture_tile[rdp.first_tile_index + tile].uls, - (int)rdp.texture_tile[rdp.first_tile_index + tile].lrs, - (int)rdp.texture_tile[rdp.first_tile_index + tile].cms); - } - } -#endif - bool clampS = tm & (1 << 2 * t); bool clampT = tm & (1 << (2 * t + 1)); @@ -3157,6 +3137,25 @@ static void gfx_dp_set_scissor(uint32_t mode, uint32_t ulx, uint32_t uly, uint32 float width = (lrx - ulx) / 4.0f; float height = (lry - uly) / 4.0f; + // D493b: with the overscan crop off (the Original N64 preset turns it + // off) D246's 1-unit trim is off too, and its gap at the left/right + // edges showed live scene/background pixels. Keep the geometry unscaled + // and scissor those two logical columns out instead, so they show the + // frame clear (black) like the rest of the overscan border. + if (!g_safe_area_crop_enabled && !g_split_screen) { + const float edge = 1.0f; + if (x < edge) { + width -= edge - x; + x = edge; + } + if (x + width > (float)SCREEN_WIDTH - edge) { + width = (float)SCREEN_WIDTH - edge - x; + } + if (width < 0.0f) { + width = 0.0f; + } + } + rdp.scissor.x = x; rdp.scissor.y = y; rdp.scissor.width = width; @@ -3606,21 +3605,6 @@ static void gfx_dp_texture_rectangle(int32_t ulx, int32_t uly, int32_t lrx, int3 ur->v = lrt; } - { - static int ge_d116 = -1; - if (ge_d116 < 0) ge_d116 = getenv("GE_D116") ? 1 : 0; - if (ge_d116) { - const auto& tt = rdp.texture_tile[tile]; - fprintf(stderr, - "[D116/f3d] tile=%d flip=%d ul(%d,%d) lr(%d,%d) uls=%d ult=%d dsdx=%d dtdy=%d " - "-> lrs=%.2f lrt=%.2f ul.u=%.2f lr.u=%.2f | TILE siz=%d fmt=%d line_bytes=%d " - "uls=%d lrs=%d width=%d height=%d cms=%d\n", - tile, (int)flip, ulx, uly, lrx, lry, (int)uls, (int)ult, (int)dsdx, (int)dtdy, - lrs, lrt, ul->u, lr->u, - tt.siz, tt.fmt, tt.line_size_bytes, tt.uls, tt.lrs, tt.width, tt.height, tt.cms); - } - } - uint8_t saved_tile = rdp.first_tile_index; if (saved_tile != tile) { rdp.textures_changed[0] = true; diff --git a/port/fast3d/gfx_rendering_api.h b/port/fast3d/gfx_rendering_api.h index d21b3f75..dedf24cd 100644 --- a/port/fast3d/gfx_rendering_api.h +++ b/port/fast3d/gfx_rendering_api.h @@ -12,7 +12,8 @@ struct GfxClipParameters { bool invert_y; }; -enum FilteringMode { FILTER_NONE, FILTER_LINEAR, FILTER_THREE_POINT }; +/* Trilinear option (playtest 2026-10-03) */ +enum FilteringMode { FILTER_NONE, FILTER_LINEAR, FILTER_THREE_POINT, FILTER_TRILINEAR }; enum MipmapFilteringMode { MIPMAP_DISABLED, MIPMAP_NEAREST, MIPMAP_LINEAR }; struct GfxRenderingAPI { diff --git a/port/include/input.h b/port/include/input.h index 8f297811..da00694a 100644 --- a/port/include/input.h +++ b/port/include/input.h @@ -21,6 +21,8 @@ extern "C" { /* Initialize SDL input. Returns 0 on success. */ int inputInit(void); void inputDestroy(void); +/* D493: stop all pad rumble and block new rumble (clean exit). */ +void inputRumbleStopAll(void); /* Poll once per frame; updates the connected-controller state that * osContStartReadData / osContRead will return. */ diff --git a/port/include/optionsoverlay.h b/port/include/optionsoverlay.h index ef3f8d7d..947b0cc3 100644 --- a/port/include/optionsoverlay.h +++ b/port/include/optionsoverlay.h @@ -86,6 +86,7 @@ void optionsRowSetFraction(int i, double f); void optionsRowCommit(int i); /* slider release; watch rows persist once */ void optionsRowValueText(int i, char *out, int n); void optionsRowAdjust(int i, int dir); +void optionsAdjustCommitPending(void); /* D489: save a stepped watch slider on release */ /* D356: per-section "Reset to defaults" rows (ROW_ACTION, keys * "__Reset*"). Activation is edge-triggered and two-step (arm -> confirm diff --git a/port/include/portaddr.h b/port/include/portaddr.h index b2ba151d..492ea65c 100644 --- a/port/include/portaddr.h +++ b/port/include/portaddr.h @@ -7,8 +7,17 @@ * address uses these instead of a literal, so moving the arena later (the * high-arena test flavour, macOS/ARM port_addr) is a change in one header. * Compile-time constants: no codegen change from the old literals. */ +/* Each define is #ifndef-guarded so a -D on the command line can override it + * (the D441 high-arena test flavour: GE_HIGHARENA_BASE=0x90000000 adds + * -DPORT_DRAM_V1_BASE=0x90000000 for C, CXX and ASM). Values unchanged. */ +#ifndef PORT_DRAM_V1_BASE #define PORT_DRAM_V1_BASE 0x70000000UL /* s32-safe "virtual" view (game RAM symbols) */ +#endif +#ifndef PORT_DRAM_K0_BASE #define PORT_DRAM_K0_BASE 0x80000000UL /* KSEG0 mirror of V1 (same pages) */ +#endif +#ifndef PORT_DRAM_SIZE #define PORT_DRAM_SIZE 0x00800000UL /* 8 MB, like an 8-MB-RAM N64 */ +#endif #endif /* PORT_PORTADDR_H */ diff --git a/port/src/dram_syms.s b/port/src/dram_syms.S similarity index 62% rename from port/src/dram_syms.s rename to port/src/dram_syms.S index 5765ad7f..55e86452 100644 --- a/port/src/dram_syms.s +++ b/port/src/dram_syms.S @@ -1,7 +1,10 @@ /* - * dram_syms.s — absolute symbols inside the "virtual" (s32-safe) DRAM view. - * See port/src/dram.c: one 8 MB backing store is mapped at BOTH 0x70000000 - * (V1, where these live) and 0x80000000 (V2, the KSEG0 mirror). These are + * dram_syms.S — absolute symbols inside the "virtual" (s32-safe) DRAM view. + * Preprocessed by gcc (.S): the base comes from port/include/portaddr.h, so + * the high-arena test flavour (-DPORT_DRAM_V1_BASE=..., GE_HIGHARENA_BASE) + * moves these symbols along with dram.c's mapping. + * See port/src/dram.c: one 8 MB backing store is mapped at BOTH PORT_DRAM_V1_BASE + * (V1, where these live) and PORT_DRAM_K0_BASE (V2, the KSEG0 mirror). These are * committed host addresses, so they are live pointers on the PC. * * cfb_16 replaces src/cfb.c (excluded from the build): the two @@ -9,13 +12,15 @@ * _bssSegmentEnd end-of-.bss marker; boss.c:217 starts the mempools at * PHYS_TO_K0(osVirtualToPhysical(&_bssSegmentEnd)). */ +#include "portaddr.h" + .section .data .global cfb_16 -.set cfb_16, 0x70000000 +.set cfb_16, PORT_DRAM_V1_BASE + 0x0 .global _bssSegmentEnd -.set _bssSegmentEnd, 0x70050000 +.set _bssSegmentEnd, PORT_DRAM_V1_BASE + 0x50000 /* D59: animation-frame scratch buffer (src/game/initanitable.c). The game * addresses it through s32 fields (Model.unk34/38/64/68) and loadAnimationFrame @@ -23,4 +28,4 @@ * the BSS copy at 0x140xxxxxx truncates to wild 0x40xxxxxx. Parked at the top * of DRAM V1 (above the mempool sentinel at +0x2F4400). */ .global animations_frame_buffer -.set animations_frame_buffer, 0x707FFD30 +.set animations_frame_buffer, PORT_DRAM_V1_BASE + 0x7FFD30 diff --git a/port/src/frontoptions.c b/port/src/frontoptions.c index 5b59613c..4195b989 100644 --- a/port/src/frontoptions.c +++ b/port/src/frontoptions.c @@ -52,6 +52,8 @@ extern Gfx *frontSetupMenuBackground(Gfx *DL); extern Gfx *frontAddPreviousTabText(Gfx *DL); extern s32 frontCheckCursorOnPreviousTab(void); +extern Gfx *frontAddNextTabText(Gfx *DL); +extern u32 frontCheckCursorOnNextTab(void); extern Gfx *frontDrawCursor(Gfx *DL); extern Gfx *frontPrintText(Gfx *gdl, s32 *x, s32 *y, s8 *text, struct fontchar *second_font_table, struct font *first_font_table, s32 arg6, s32 view_x, s32 view_y, @@ -83,7 +85,7 @@ extern struct rectbbox folder_option_ERASE_bound; /* front.c:439 */ #define VAL_R 372 /* values right-aligned here */ #define BAR_X0 232 #define BAR_X1 316 -#define ROW_HIT_X0 40.0f +#define ROW_HIT_X0 24.0f /* D502: covers the pad crosshair snap at x = 32 */ #define ROW_HIT_X1 385.0f /* ---- file-select label: right end of the Select / Copy / Erase bar ---- */ @@ -245,12 +247,12 @@ static void cursorToItemRaw(int k) { /* D345(g): snap into the left gutter (x=44), not onto the row text * (the old ROW_X+20 centred the sprite over the label/highlight box). - * 44 stays inside [ROW_HIT_X0..1] so the highlight hit-test still + * 32 (D502; was 44) stays inside [ROW_HIT_X0..1] so the hit-test still * resolves row k, and clears the number column (NUM_X=55) / highlight * boxes (from x=53/73). The PREVIOUS-tab hitbox (x>390 && y>223) is * far away. Mouse users are unaffected: the pointer sits under the * real mouse, never at this snap position. */ - cursor_h_pos = 44.0f; + cursor_h_pos = 32.0f; /* D502: was 44; the sprite overlapped the "1." / row text */ 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; @@ -267,6 +269,14 @@ static void cursorToItem(int k) cursorToItemRaw(k); } +/* D502: highlight for a screen change. Pad/keyboard users get item k + * highlighted (the crosshair was just snapped onto it); with a live mouse the + * pointer owns the selection, so start unselected and let hover pick. */ +static int navHighlight(int k) +{ + return inputMenuPointerLive() ? -1 : k; +} + /* ------------------------------------------------------------------------ */ /* MENU_PC_OPTIONS: init / update / interface / constructor (front.c cases) */ @@ -358,7 +368,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_lastHover = -2; + s_hl = navHighlight(selected); s_lastHover = -2; /* D502 */ sysLogPrintf(LOG_INFO, "frontoptions: back to %s", optionsRowLabel(activeHeader())); return; } @@ -366,6 +376,10 @@ static void goBack(void) s_level = 0; s_pageno = 0; /* D406 */ cursorToItem(s_page); + /* D502: the highlight is an item index of the level being shown; + * leaving it at the section's row index landed pad users on another + * section (row 1 inside Input -> "Graphics"). */ + s_hl = navHighlight(s_page); s_lastHover = -2; sysLogPrintf(LOG_INFO, "frontoptions: back to section list"); return; } @@ -374,22 +388,32 @@ 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) +/* D497: paging mirrors the mission dossiers (front.c briefing screen): the + * folder's PREVIOUS tab (and B) turns back a page, or leaves the section from + * its first page; a NEXT tab is shown while a later page exists. This replaced + * D444's "Previous page / Next page" links under the rows. */ +static int hasNextPage(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; + return s_level >= 1 && s_pageno * ROWS_PER_PAGE + s_rowN < s_rowTotal; +} + +static void turnPage(int dir) +{ + playSfx(DOOR_METAL_CLOSE2_SFX); + if (s_dragRow >= 0) optionsRowCommit(s_dragRow); + s_dragRow = -1; + optionsResetClear(); + s_pageno += dir; + buildPages(); + cursorToItem(dir > 0 ? 1 : 0); + s_hl = navHighlight(dir > 0 ? 1 : 0); s_lastHover = -2; /* D502 */ +} + +/* PREVIOUS tab / B: previous page, else back out. */ +static void goPrevious(void) +{ + if (s_level >= 1 && s_pageno > 0) turnPage(-1); + else goBack(); } void frontOptionsMenuInterface(void) @@ -414,8 +438,11 @@ void frontOptionsMenuInterface(void) int hit = -1; int moved = (cursor_h_pos != s_prevH || cursor_v_pos != s_prevV); tab_prev_highlight = FALSE; + tab_next_highlight = FALSE; if (frontCheckCursorOnPreviousTab()) { tab_prev_highlight = TRUE; + } else if (hasNextPage() && frontCheckCursorOnNextTab()) { + tab_next_highlight = TRUE; /* D497 */ } 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) && @@ -445,13 +472,9 @@ void frontOptionsMenuInterface(void) s_prevV = cursor_v_pos; } - /* 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 */ + /* D497: NEXT folder tab (mouse click; Up/Down still page too). */ + if (tab_next_highlight && joyGetButtonsPressedThisFrame(PLAYER_1, A_BUTTON | Z_TRIG)) { + turnPage(+1); } 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 @@ -459,7 +482,7 @@ void frontOptionsMenuInterface(void) if (!tab_prev_highlight && joyGetButtonsPressedThisFrame(PLAYER_1, A_BUTTON | Z_TRIG)) s_hl = s_hoverHit; if (tab_prev_highlight) { - goBack(); + goPrevious(); /* D497 */ } else if (s_hl >= 0 && s_level == 0) { playSfx(DOOR_METAL_CLOSE2_SFX); s_page = s_hl; @@ -468,7 +491,7 @@ void frontOptionsMenuInterface(void) sysLogPrintf(LOG_INFO, "frontoptions: section %d", s_page + 1); buildPages(); cursorToItem(0); - s_hl = -1; s_lastHover = -2; + s_hl = navHighlight(0); s_lastHover = -2; /* D502 */ } 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). */ @@ -493,7 +516,7 @@ void frontOptionsMenuInterface(void) sysLogPrintf(LOG_INFO, "frontoptions: opened %s (depth %d)", optionsRowLabel(child), s_level); cursorToItem(0); - s_hl = -1; s_lastHover = -2; + s_hl = navHighlight(0); s_lastHover = -2; /* D502 */ } 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)); @@ -517,7 +540,7 @@ void frontOptionsMenuInterface(void) } } } else if (joyGetButtonsPressedThisFrame(PLAYER_1, B_BUTTON)) { - goBack(); + goPrevious(); /* D497: as the dossiers, B turns back a page first */ } /* Drag a slider while A is held. */ @@ -564,8 +587,10 @@ void frontOptionsMenuInterface(void) } } s_repeatDir = dir; + if (dir == 0) optionsAdjustCommitPending(); /* D489: save once on release */ } else { s_repeatDir = 0; + optionsAdjustCommitPending(); } /* D356: reset arm state -- the screen's selected rows[] index (s_hl 0 is @@ -794,31 +819,11 @@ Gfx *frontOptionsMenuDraw(Gfx *DL) ? "Press pad button tap {B} cancel hold Back clear\n" : "{A}: bind Left/Right: slot {Y}: clear {B}: back\n"); DL = ink(DL, ROW_X, rowY(s_rowN + 2), d471buf1, INK_DIM); - } else if (s_rowTotal > ROWS_PER_PAGE) { - /* D406b: bottom hint on every page of a multi-page section (11 - * rows per page leaves the last line of the paper for it). */ - int last = s_pageno * ROWS_PER_PAGE + s_rowN >= s_rowTotal; - int first = s_pageno == 0; - /* 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. */ - 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); - } } } DL = frontAddPreviousTabText(DL); + if (hasNextPage()) DL = frontAddNextTabText(DL); /* D497 */ DL = frontDrawCursor(DL); return DL; } diff --git a/port/src/input.c b/port/src/input.c index 04be7597..344300f3 100644 --- a/port/src/input.c +++ b/port/src/input.c @@ -403,9 +403,10 @@ int inputOverlayOwnerPad(void) * Sources are SDL controller buttons plus the two analog triggers treated as * digital (padTriggerPct). Each action is bound to up to two sources, written * in the ini as SDL names joined by a comma ("a,x", "righttrigger", "none"). - * Input.PadPreset: 0 = Jinx 1.1 (the pre-D469 hard-wired mapping; default), - * 1/2 = Jinx 1.2/1.3 (NOT defined by D394: reserved, resolve to 1.1), - * 3 = Custom (the per-seat Input.Pad.* strings). Seat 0 keys are + * Input.PadPreset: 0 = 1.1 Jinx (the pre-D469 hard-wired mapping; default), + * 1 = 1.2 Christmas, 2 = 1.3 Frost (D498: D394 stick roles, same buttons), + * 3 = Custom (the per-seat Input.Pad.* strings), 4 = 1.4 Elektra (D498). + * Seat 0 keys are * Input.Pad.; seats 1..3 are Input.Pad2/3/4.. Seat = pads[] * index (D448), not the controller slot. * @@ -458,6 +459,11 @@ typedef struct { u32 mask[PA_COUNT]; } PadBinds; typedef struct { PadBinds seat[MAX_PADS]; } PadTable; static int padPreset = 0; /* Input.PadPreset */ +/* D194/D238: 1 = force GE's own 1.2/SOLITARE + * control style for continuous analog pitch; + * 0 = legacy 1.1/HONEY + D166 digital pulse. + * (D498: declared here so the stick-style helpers below can read it.) */ +static int naturalPitchMode = 1; /* Input.NaturalPitch */ static char padStr[MAX_PADS][PA_COUNT][32]; /* Input.Pad[N]. */ static char padStrKey[MAX_PADS][PA_COUNT][40]; static PadTable padTabBuf[2]; @@ -496,7 +502,21 @@ static void padBuildBinds(PadBinds *b, int seat, int custom, int *bad) if (!b->mask[PA_START]) b->mask[PA_START] = PSM(PS_START); /* lockout guard: keep a pause path */ } -static int padPresetCustom(void) { return padPreset >= 3; } +/* D498: preset 3 is Custom; 4 = 1.4 Elektra is a fixed mapping, not custom. */ +static int padPresetCustom(void) { return padPreset == 3; } + +/* D498: stick style for the active preset. 1.2 Christmas / 1.3 Frost force + * HONEY and 1.4 Elektra forces SOLITARE (for mouse and keyboard too, since + * the game runs one control style per player). 1.1 Jinx (the default) and + * Custom keep Input.NaturalPitch, so the NaturalPitch=0 escape hatch still + * works exactly as before D498. */ +static int stickSolitare(void) { + if (padPreset == 1 || padPreset == 2) return 0; + if (padPreset == 4) return 1; + return naturalPitchMode; +} +/* D498: 1.3 Frost / 1.4 Elektra swap the two sticks (never in menus). */ +static int stickSwap(void) { return padPreset == 2 || padPreset == 4; } /* Signature of everything the table depends on (preset only unless Custom). */ static unsigned padComputeSig(void) @@ -527,9 +547,8 @@ static const PadTable *padTabGet(void) SDL_AtomicSetPtr((void **)&padTabPub, spare); padTabSig = sig; padTabValid = 1; - sysLogPrintf(LOG_INFO, "input: pad table rebuilt (preset %d%s%s, %d unknown name(s))", - padPreset, padPresetCustom() ? " custom" : "", - padPreset == 1 || padPreset == 2 ? " [1.2/1.3 undefined -> 1.1]" : "", bad); + sysLogPrintf(LOG_INFO, "input: pad table rebuilt (preset %d%s, %d unknown name(s))", + padPreset, padPresetCustom() ? " custom" : "", bad); /* D498 */ SDL_AtomicUnlock(&rebuildLock); return spare; } @@ -557,7 +576,7 @@ static u32 padActHeld(const PadBinds *b, u32 raw) return h; } -typedef struct { int facePlayable; int slotNative; int southpaw; } PadMapCtx; +typedef struct { int facePlayable; int slotNative; int southpaw; int tank; } PadMapCtx; typedef struct { unsigned button; int crouch; int gadgetEdge; u32 actHeld; } PadMapOut; /* The pure mapping: raw source bits + bindings + previous action state -> @@ -588,6 +607,15 @@ static void padMapPure(const PadBinds *b, u32 raw, u32 actPrev, unsigned buttonI if (edge & (1u << PA_PREVWPN)) button |= GE_CONT_A | GE_CONT_G; /* same backward-cycle idiom as the mouse wheel */ if (raw & b->mask[PA_AIM] & ~PS_TRIGGERS) button |= GE_CONT_R; /* digital sources (LB aim): playable only, after the swap */ if (raw & b->mask[PA_FIRE] & ~PS_TRIGGERS) button |= GE_CONT_G; + } else if (c->tank) { + /* D495: driving the tank. The native A (change weapon) and B (exit, + * sent by the locked use path in inputComputePadSlot) follow the + * player's bindings like on foot, instead of the fixed menu faces + * (A/X = A, B/Y = B): with the default preset X switched the tank + * weapon and Y left the tank. */ + if (edge & ((1u << PA_NEXTWPN) | (1u << PA_PREVWPN))) button |= GE_CONT_A; + if (raw & b->mask[PA_AIM] & ~PS_TRIGGERS) button |= GE_CONT_R; + if (raw & b->mask[PA_FIRE] & ~PS_TRIGGERS) button |= GE_CONT_G; } else { if (raw & (PSM(PS_A) | PSM(PS_X))) button |= GE_CONT_A; /* menu lockout guard: fixed native accept/cancel */ if (raw & (PSM(PS_B) | PSM(PS_Y))) button |= GE_CONT_B; @@ -940,9 +968,6 @@ static int padDirectCompute(int dx, int dy); /* D404: pad twin of the above */ static int s_absAimSuspend = 0; static int aimAbsCompute(double dxPx, double dyPx, int *outSx, int *outSy); /* D194 */ -static int naturalPitchMode = 1; /* D194/D238: 1 = force GE's own 1.2/SOLITARE - * control style for continuous analog pitch; - * 0 = legacy 1.1/HONEY + D166 digital pulse. */ static Uint64 s_lastLookPollCounter = 0; /* D194(b): monotonic clock, gameplay-look drain only */ /* Gamepad tuning -- defaults reproduce the old hardcoded constants exactly. @@ -1178,13 +1203,27 @@ static void inputOpenPads(void) * Same entry syntax; "frame" = count of THAT controller's reads since * launch. Slots 1-3 have no mouse path, so mouse tokens (MDX/MDY) and * CHOLD/CREL are ignored there (one warning per script if they appear). - * Unset => the seat stays idle exactly as before. */ -#define INPUTSCRIPT_MAX 64 -#define INPUTSCRIPT_PULSE 6 + * Unset => the seat stays idle exactly as before. + * + * GE_INPUTSCRIPT_MS=1 (abpad, tools_pc/abpad): every slot's "frame" is + * instead milliseconds since SDL init (SDL_GetTicks), and buttons pulse for + * INPUTSCRIPT_PULSE_MS. Read rates vary across the intro and menus, so a + * read count can't be lined up with a wall-clock reference (1964). */ +#define INPUTSCRIPT_MAX 64 +#define INPUTSCRIPT_PULSE 6 +#define INPUTSCRIPT_PULSE_MS 50 + +static int scriptMsMode(void) +{ + static int ms = -1; + if (ms < 0) ms = GE_ENVFLAG("GE_INPUTSCRIPT_MS") ? 1 : 0; + return ms; +} struct scriptEntry { long frame; unsigned mask; int sx, sy; int hasStick; int hasMouse, mdx, mdy; int hasHold, hold; /* D337 mouse/aim tokens */ int hasZHold, zhold; /* D207: sustained fire */ + int hasCHold; unsigned chold; /* abpad: sustained C buttons (1.2 walk/strafe) */ int hasCrouch, crouch; /* D377: free-crouch QA */ int hasUse, use, hasReload, reload; }; /* D378: split-action QA */ /* Per-slot state. Slot 0 is the original GE_INPUTSCRIPT harness (byte-for- @@ -1197,6 +1236,7 @@ struct scriptSlot { int curSX, curSY; /* stick set by the last scriptApply() for this slot */ int mouseOn, mdx, mdy; /* sustained scripted mouse (slot 0 only) */ int hold, zhold, crouch, use, reload; /* latest sustained holds */ + unsigned chold; /* latest sustained C-button mask */ int ignoredWarned; /* P1..P3: warned once about mouse/crouch tokens */ }; static struct scriptSlot scriptSlots[4] = { @@ -1261,6 +1301,13 @@ static void scriptApplyToken(struct scriptEntry *e, const char *s, int n, * independent of RHOLD so aim + fire can be held together. */ if (n == 5 && SDL_strncasecmp("ZHOLD", s, 5) == 0) { e->hasZHold = 1; e->zhold = 1; return; } if (n == 4 && SDL_strncasecmp("ZREL", s, 4) == 0) { e->hasZHold = 1; e->zhold = 0; return; } + /* abpad: CUPHOLD/CDOWNHOLD/CLEFTHOLD/CRIGHTHOLD hold C buttons until CNONE + * (the port forces 1.2 SOLITARE, where C-up/down walk and C-left/right strafe). */ + if (n == 7 && SDL_strncasecmp("CUPHOLD", s, 7) == 0) { e->hasCHold = 1; e->chold |= GE_CONT_E; return; } + if (n == 9 && SDL_strncasecmp("CDOWNHOLD", s, 9) == 0) { e->hasCHold = 1; e->chold |= GE_CONT_D; return; } + if (n == 9 && SDL_strncasecmp("CLEFTHOLD", s, 9) == 0) { e->hasCHold = 1; e->chold |= GE_CONT_C; return; } + if (n == 10 && SDL_strncasecmp("CRIGHTHOLD", s, 10) == 0) { e->hasCHold = 1; e->chold |= GE_CONT_F; return; } + if (n == 5 && SDL_strncasecmp("CNONE", s, 5) == 0) { e->hasCHold = 1; e->chold = 0; return; } if ((n == 5 && SDL_strncasecmp("CHOLD", s, 5) == 0) || (n == 4 && SDL_strncasecmp("CREL", s, 4) == 0)) { if (slot > 0) { /* no crouch path on slots 1-3: ignore, warn once */ @@ -1315,6 +1362,7 @@ static void scriptParse(int slot) e->hasMouse = e->mdx = e->mdy = 0; e->hasHold = e->hold = 0; e->hasZHold = e->zhold = 0; + e->hasCHold = 0; e->chold = 0; e->hasCrouch = e->crouch = 0; e->hasUse = e->use = e->hasReload = e->reload = 0; while (*p && *p != ';') { @@ -1351,7 +1399,7 @@ static int scriptIsActive(int slot) static void scriptRefreshHolds(int slot) { struct scriptSlot *st = &scriptSlots[slot]; - long bestM = -1, bestH = -1, bestZ = -1, bestC = -1, bestU = -1, bestR = -1; + long bestM = -1, bestH = -1, bestZ = -1, bestC = -1, bestU = -1, bestR = -1, bestCH = -1; for (int i = 0; i < st->count; ++i) { long d = st->frame - st->entries[i].frame; if (d < 0) continue; @@ -1369,6 +1417,10 @@ static void scriptRefreshHolds(int slot) bestZ = st->entries[i].frame; st->zhold = st->entries[i].zhold; } + if (st->entries[i].hasCHold && st->entries[i].frame > bestCH) { + bestCH = st->entries[i].frame; + st->chold = st->entries[i].chold; + } if (st->entries[i].hasCrouch && st->entries[i].frame > bestC) { bestC = st->entries[i].frame; st->crouch = st->entries[i].crouch; @@ -1390,12 +1442,15 @@ static unsigned scriptApply(int slot, unsigned button) if (!scriptIsActive(slot)) { return button; } + int ms = scriptMsMode(); + if (ms) st->frame = (long)SDL_GetTicks(); scriptRefreshHolds(slot); /* D207: refresh sustained holds even off the mouse path (idempotent) */ unsigned m = 0; long bestStickFrame = -1; + long pulse = ms ? INPUTSCRIPT_PULSE_MS : INPUTSCRIPT_PULSE; for (int i = 0; i < st->count; ++i) { long d = st->frame - st->entries[i].frame; - if (d >= 0 && d < INPUTSCRIPT_PULSE) { + if (d >= 0 && d < pulse) { m |= st->entries[i].mask; } /* sustained stick: the latest-starting entry that carried a stick token */ @@ -1408,7 +1463,8 @@ static unsigned scriptApply(int slot, unsigned button) if (st->hold == 1) m |= GE_CONT_R; /* D337 RHOLD */ if (st->hold == 2) m |= GE_CONT_L; /* D337 LHOLD (Q / "LeanLeft") */ if (st->zhold) m |= GE_CONT_G; /* D207 ZHOLD (fire) */ - st->frame++; + m |= st->chold; /* abpad C-button holds */ + if (!ms) st->frame++; return m; } @@ -1435,6 +1491,11 @@ int inputInit(void) * be set before the pads below are opened. */ SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1"); + /* abpad live A/B: GE_PAD_BACKGROUND=1 keeps pad input flowing while the + * window is unfocused, so one real pad can drive the port and 1964 at once. + * Off by default (SDL ignores pads in unfocused windows). */ + if (getenv("GE_PAD_BACKGROUND") && getenv("GE_PAD_BACKGROUND")[0] == '1') + SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); for (int i = 0; i < MAX_PADS; ++i) { pads[i] = NULL; @@ -2026,7 +2087,7 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char * * anything else ever calls the setter (menu, save load) in between. */ if ((idx == 0 || inputMpSplit()) && slotPlayer() != NULL && (idx == 0 || idx < getPlayerCount())) { /* D416: each slot rewrites only its own player's style */ - int wantSolitare = naturalPitchMode ? CONTROLLER_CONFIG_SOLITARE_ : CONTROLLER_CONFIG_HONEY_; + int wantSolitare = stickSolitare() ? CONTROLLER_CONFIG_SOLITARE_ : CONTROLLER_CONFIG_HONEY_; /* D498 */ struct player *sp = slotPlayer(); if (!inputMpSplit()) { if (cur_player_get_control_type() != wantSolitare) @@ -2162,7 +2223,7 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char * * front-end screen (file select, cheat, MENU_PC_OPTIONS) can be * driven vertically from the keyboard. Watch/pause keeps the C-button * mapping: it is current_menu==RUN_STAGE (menuMode 0), N64-style. */ - if (naturalPitchMode && !menuMode) { + if (stickSolitare() && !menuMode) { /* D498 */ if (actHeld(ks, IA_FORWARD)) button |= GE_CONT_E; /* C-up = forward */ if (actHeld(ks, IA_BACK)) button |= GE_CONT_D; /* C-down = back */ } else { @@ -2580,7 +2641,7 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char * sx += (int)(hipEdx * hipSens * MOUSE_TURN_GAIN * (inTankHip ? tankAimScale / 100.0 : 1.0)); if (inTankHip) { /* no pitch/drive from the mouse in a tank */ - } else if (naturalPitchMode) { + } else if (stickSolitare()) { /* D498 */ /* D194/D238: SOLITARE gives hipfire pitch the same * continuous analog stick treatment as yaw -- same * formula as the sx line above, so X and Y are, by @@ -2666,12 +2727,15 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char * int padMenuMode = (current_menu != GE_MENU_RUN_STAGE && current_menu != GE_MENU_INVALID); + /* D498: 1.3 Frost / 1.4 Elektra swap the two sticks; menus never swap. */ + if (!padMenuMode && stickSwap()) { int tx = lx, ty = ly; lx = rx; ly = ry; rx = tx; ry = ty; } + if (padMenuMode) { int px = scaleAxis(lx, padDeadzoneL); int py = -scaleAxis(ly, padDeadzoneL); /* SDL up = negative -> N64 up = positive */ if (px) sx = px; if (py) sy = py; - } else if (naturalPitchMode) { + } else if (stickSolitare()) { /* D498 */ /* D194/D238: SOLITARE swaps stick roles -- left stick becomes * digital-step movement (same analog-for-movement tradeoff as * the keyboard remap above), right stick becomes continuous @@ -2742,6 +2806,8 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char * pc.facePlayable = (idx == 0 || inputMpSplit()) && inputCanUseGameplayActions(padMenuMode); pc.slotNative = (idx > 0 || inputMpPadsOwnP1()); pc.southpaw = padSouthpaw; + pc.tank = !pc.facePlayable && idx == 0 && !padMenuMode && g_PlayerIsInTank == 1 && + slotPlayer() && slotPlayer()->outside_watch_menu && !slotPlayer()->pause_state; const PadBinds *pb = &padTabGet()->seat[padSeatForSlot(idx)]; padMapPure(pb, padReadRaw(pad), padActPrev[idx], button, &pc, &po); button = po.button; @@ -2785,37 +2851,30 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char * !p->pause_state && !p->mpmenuon && !g_PlayerIsInTank && !lvlGetControlsLockedFlag() && !gameScriptedCameraActive(); + /* D483/D494 (#136): a weapon with DISABLE_CROUCH (the sniper rifle) + * makes the native code turn aim-mode C-up/C-down into zoom, so the + * N64 pad cannot change stance with it out. GEPD's crouch key (the + * mouse injector, the reference for this preset) writes the stance + * directly and ignores the flag: Bond crouches and stands freely with + * the sniper (maintainer A/B on 1964 GEPD, 2026-10-03). Do the same, + * but never feed C-down for such a weapon (it would zoom out). */ int weaponBlocks = 0; - if (canCrouch && (crouchNow || s_crouchApplied)) { /* weapon lookup only when it can matter */ + if (canCrouch && (crouchNow || s_crouchApplied)) /* weapon lookup only when it can matter */ weaponBlocks = bondwalkItemCheckBitflags(p->hands[GUNRIGHT].weaponnum, WEAPONSTATBITFLAG_DISABLE_CROUCH); - canCrouch = !weaponBlocks; - } - static int frozenLogged = 0; - if (!weaponBlocks) frozenLogged = 0; - if (weaponBlocks) { - /* D483 (#136): a weapon with DISABLE_CROUCH (the sniper rifle) - * blocks the native crouch-down AND crouch-up (bondview2.c), so - * on the N64 Bond keeps whatever stance he had when he switched - * to it. Freeze the port stance the same way: don't apply a new - * crouch, don't stand up, and never feed C-down (it zooms out - * with this weapon). Switching back resumes the normal rules. */ - if (!frozenLogged && configGetInputLog()) - sysLogPrintf(LOG_NOTE, "GE_INPUTLOG free crouch frozen (weapon %d disables crouch, crouchpos=%d, key=%d)", - (int)p->hands[GUNRIGHT].weaponnum, (int)p->crouchpos, crouchNow); - frozenLogged = 1; - } else if (canCrouch && crouchNow) { + if (canCrouch && crouchNow) { /* In aim mode the native crouchUp branch runs every tick without * C-down, undoing the port stance by one step. Feed the native * axis ONLY while aiming; hipfire still crouches by stance alone. */ - if (p->insightaimmode || (button & (GE_CONT_R | GE_CONT_L))) + if (!weaponBlocks && (p->insightaimmode || (button & (GE_CONT_R | GE_CONT_L)))) button |= GE_CONT_D; p->crouchpos = CROUCH_SQUAT; if (!s_crouchApplied && configGetInputLog()) - sysLogPrintf(LOG_NOTE, "GE_INPUTLOG free crouch applied (aim=%d)", p->insightaimmode); + sysLogPrintf(LOG_NOTE, "GE_INPUTLOG free crouch applied (aim=%d, weapon disables crouch=%d)", + p->insightaimmode, weaponBlocks); s_crouchApplied = 1; s_crouchPlayer = p; - } else if (!(canCrouch && (button & GE_CONT_D) && p->insightaimmode)) { + } else if (!(canCrouch && !weaponBlocks && (button & GE_CONT_D) && p->insightaimmode)) { /* If native C-down is still held in aim, let the game retain its * own crouch on this tick rather than force a stand. */ if (s_crouchApplied && configGetInputLog()) @@ -3092,8 +3151,13 @@ int inputRumbleSupported(int idx) return padRumbleOn[idx]; } +/* D493: set once inputRumbleStopAll has run at exit, so the still-running + * game thread cannot start a new rumble after the pads were silenced. */ +static SDL_atomic_t s_rumbleStopped; + void inputRumble(int idx, f32 strength, f32 time) { + if (SDL_AtomicGet(&s_rumbleStopped)) return; if (idx < 0 || idx >= MAX_PADS) return; SDL_GameController *rpad = slotPad(idx); const int rIdx = (inputMpSplit() && inputMpKbmP1()) ? idx - 1 : idx; @@ -3112,6 +3176,21 @@ void inputRumble(int idx, f32 strength, f32 time) } } +/* D493: silence every open pad at a clean exit. SDL_GameControllerRumble + * runs for its duration on the pad itself, so a rumble started just before + * the window closed kept going after the process ended (seen on a DualSense + * when quitting mid-firefight). inputDestroy never runs: the host loop exits + * through exit(0), so this is called from main.c's atexit handler. */ +void inputRumbleStopAll(void) +{ + SDL_AtomicSet(&s_rumbleStopped, 1); + for (int i = 0; i < MAX_PADS; ++i) { + if (pads[i] && padRumbleOn[i]) { + SDL_GameControllerRumble(pads[i], 0, 0, 0); + } + } +} + f32 inputRumbleGetScale(void) { return gRumbleScale; @@ -3520,8 +3599,11 @@ unsigned inputPadHeldSources(int seat) } int inputPadPresetStep(int cur, int dir) { - (void)dir; /* only Jinx 1.1 (0) and Custom (3) are defined; 1/2 are reserved (D394 does not define 1.2/1.3) */ - return cur == 0 ? 3 : 0; + /* D498: display order Jinx, Christmas, Frost, Elektra, Custom (stored 0,1,2,4,3). */ + static const int order[5] = { 0, 1, 2, 4, 3 }; + for (int i = 0; i < 5; i++) + if (order[i] == cur) return order[(i + (dir > 0 ? 1 : 4)) % 5]; + return 0; /* unknown stored value -> Jinx */ } /* "a,x" -> src[0]=A, src[1]=X; empty/none/unknown -> -1 (positional). */ @@ -3687,7 +3769,7 @@ static void padMapSelfTest(void) PD_CONSTRUCTOR static void inputConfigInit(void) { - configRegisterInt("Input.PadPreset", &padPreset, 0, 3); /* D469: 0 Jinx 1.1, 1/2 reserved (= 1.1), 3 Custom */ + configRegisterInt("Input.PadPreset", &padPreset, 0, 4); /* D498: 0 Jinx 1.1, 1 Christmas 1.2, 2 Frost 1.3, 3 Custom, 4 Elektra 1.4 */ for (int seat = 0; seat < MAX_PADS; seat++) for (int a = 0; a < PA_COUNT; a++) { if (seat == 0) snprintf(padStrKey[seat][a], sizeof(padStrKey[seat][a]), "Input.Pad.%s", kPadAct[a].key); diff --git a/port/src/libultra.c b/port/src/libultra.c index d335f72a..64709f4d 100644 --- a/port/src/libultra.c +++ b/port/src/libultra.c @@ -586,39 +586,9 @@ void osEnqueueMesg(OSMesgQueue *mq, OSMesg msg) pthread_mutex_unlock(&pq->lock); } -/* TEMP D62: full message-flow trace (env GE_D62=1 -> d62mesg.log). */ -static FILE *s_d62log = NULL; -static int s_d62opened = 0; -static pthread_mutex_t s_d62lock = PTHREAD_MUTEX_INITIALIZER; -static void d62log(const char *op, OSMesgQueue *mq, OSMesg msg) -{ - if (!s_d62opened) { - s_d62opened = 1; - if (getenv("GE_D62")) - s_d62log = fopen("d62mesg.log", "w"); - } - if (s_d62log) { - pthread_mutex_lock(&s_d62lock); - fprintf(s_d62log, "%s mq=%p msg=%p valid=%d/%d first=%d\n", - op, (void *)mq, (void *)msg, mq->validCount, - mq->msgCount, mq->first); - fflush(s_d62log); - pthread_mutex_unlock(&s_d62lock); - } -} - - 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) */ - 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); - } pthread_mutex_lock(&pq->lock); while (mq->validCount >= mq->msgCount) { if (flag != OS_MESG_BLOCK) { @@ -634,25 +604,11 @@ s32 osSendMesg(OSMesgQueue *mq, OSMesg msg, s32 flag) return 0; } -/* TEMP D60: gfxFrameMsgQ receive watch (env GE_D60=1). The main loop only - * acts on msg type 1 (retrace) / 4 (RSP done); anything else means the - * message object's .type field was clobbered. */ extern OSMesgQueue gfxFrameMsgQ; /* src/init.c */ -static void d60logRecv(OSMesgQueue *mq, OSMesg m) { - 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)) - sysLogPrintf(LOG_NOTE, "D60 recv #%llu mq=%p msg=%p type=%u", - (unsigned long long)n, (void *)mq, (void *)m, type); - ++n; -} s32 osRecvMesg(OSMesgQueue *mq, OSMesg *msg, s32 flag) { PortQueue *pq = portQueueGet(mq); - d62log("RECV", mq, 0); /* TEMP D62 */ pthread_mutex_lock(&pq->lock); while (mq->validCount == 0) { if (flag != OS_MESG_BLOCK) { @@ -711,7 +667,6 @@ s32 osRecvMesg(OSMesgQueue *mq, OSMesg *msg, s32 flag) mq->first = (mq->first + 1) % mq->msgCount; --mq->validCount; if (msg) *msg = m; - d60logRecv(mq, m); /* TEMP D60 */ pthread_cond_signal(&pq->cond); pthread_mutex_unlock(&pq->lock); /* gfxFrameMsgQ is consumed only by boss.c's game thread. Apply queued @@ -789,8 +744,6 @@ void osViSetEvent(OSMesgQueue *mq, OSMesg msg, u32 retraceCount) g_viRetraceMsg = msg; } -/* TEMP D51: instrument the retrace pacemaker */ -static uint64_t g_viPostCount = 0; static void portPostVIEvent(void) { if (g_viRetraceMQ) { @@ -798,17 +751,9 @@ static void portPostVIEvent(void) * end of a synchronous fast3d frame. A retrace posted into a backlog * is stale anyway -- sched has not drained the previous one yet. */ if (g_viRetraceMQ->validCount >= g_viRetraceMQ->msgCount - 2) return; - s32 r = osSendMesg(g_viRetraceMQ, g_viRetraceMsg, OS_MESG_NOBLOCK); - if (++g_viPostCount % 60 == 1 || r != 0) { - sysLogPrintf(r != 0 ? LOG_ERROR : LOG_NOTE, - "D51 vi post #%llu mq=%p msg=%d valid=%d/%d ret=%d", - (unsigned long long)g_viPostCount, (void *)g_viRetraceMQ, - (int)g_viRetraceMsg, g_viRetraceMQ->validCount, - g_viRetraceMQ->msgCount, r); - } + osSendMesg(g_viRetraceMQ, g_viRetraceMsg, OS_MESG_NOBLOCK); } } -/* END TEMP D51 */ void osViSetMode(OSViMode *vm) { @@ -910,35 +855,9 @@ void osAiSetConvert(u32 convert) { (void)convert; } * completion; osPiStartDma() below replicates that, because the game blocks * in romReceiveMesg()/osRecvMesg() after every romCopy(). */ -/* TEMP D60: identify sidecar model reads (env GE_D60=1). srcPA-base is the - * manifest offset; cross-reference data/pcmodels-/manifest.csv for - * the file name. */ -extern uintptr_t pcmodelsSidecarBase(void); -extern uint32_t pcmodelsTotalSize(void); -/* D69: same diagnostic for the bg/stan sidecar image. */ -extern uintptr_t pccgSidecarBase(void); -extern uint32_t pccgTotalSize(void); -static void d60logSidecarRead(u32 srcPA, void *dstVA, u32 size) { - 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", - (unsigned long long)(srcPA - base), size, dstVA); - return; - } - uintptr_t cgBase = pccgSidecarBase(); - if (cgBase && srcPA >= cgBase && srcPA < cgBase + pccgTotalSize()) - sysLogPrintf(LOG_NOTE, "D69 pccg sidecar read off=%llu size=0x%X dst=%p", - (unsigned long long)(srcPA - cgBase), size, dstVA); -} - -/* TEMP D60/D61: a ROM-read DMA target must land in the game DRAM views +/* A ROM-read DMA target must land in the game DRAM views * (V1/V2) or the current thread's stack (texLoad's compbuffer). Anything * else is unmapped host memory on PC. */ -static FILE *s_d61log = NULL; -static int s_d61opened = 0; - static int dramHostAddrValid(uintptr_t addr, u32 size) { static const uintptr_t bases[2] = { PORT_DRAM_V1_BASE, PORT_DRAM_K0_BASE }; /* portaddr.h */ @@ -974,7 +893,6 @@ static void piServiceDma(s32 direction, u32 srcPA, void *dstVA, u32 size) if (size == 0) return; if (direction == OS_READ) { - d60logSidecarRead(srcPA, dstVA, size); /* TEMP D60 */ if (!romdataCartAddrValid(srcPA, size)) { sysLogPrintf(LOG_WARNING, "osPiStartDma: ROM read out of range " @@ -982,23 +900,8 @@ static void piServiceDma(s32 direction, u32 srcPA, void *dstVA, u32 size) srcPA, size); return; } - /* TEMP D60: validate the DMA target too. The N64 PI happily DMAs to - * any KSEG address; on PC an unmapped target is a wild memcpy. - * Log the whole call chain (romCopy <- doRomCopy <- osPiStartDma) so - * 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. */ - 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; - } - if (s_d61log) { - static int d61count = 0; - fprintf(s_d61log, "D61 %06d dst=%p src=0x%08X size=0x%X\n", - ++d61count, dstVA, srcPA, size); - fflush(s_d61log); - } + /* Validate the DMA target too. The N64 PI happily DMAs to any KSEG + * address; on PC an unmapped target is a wild memcpy. */ if (!dramHostAddrValid((uintptr_t)dstVA, size)) { /* No __builtin_return_address here: the caller's frame chain is * not always walkable (it faulted). Log the raw stack window diff --git a/port/src/main.c b/port/src/main.c index 663f44a1..5e12e863 100644 --- a/port/src/main.c +++ b/port/src/main.c @@ -83,6 +83,7 @@ static void portAtExit(void) { /* Clean-exit only (exit(0) from videoPumpEvents). Crash/fatal paths call * abort(), which does not run atexit handlers. */ + inputRumbleStopAll(); /* D493: no pad left rumbling after exit */ videoSaveWindowState(); configSave(); /* D443: Restart game -- runs only on the orderly D344 exit path, after the @@ -146,11 +147,10 @@ int main(int argc, char **argv) * writes only -- no game-logic edits (AGENTS rule 2), same class as * the existing GE_UNLOCK_ALL getenv hook in the getter. No active * cheats (invincibility / all guns) are enabled; weapons remain - * per-mission pickups as on the N64. Separately, the eep shim - * (libultra.c geEepromPatchAllCheats) sets every progression-gated - * cheat-unlock bit in the save block at read time (per-slot CRC - * recomputed via the game's own fileGenerateCRC), so the cheat - * menu is fully populated without completed levels. */ + * per-mission pickups as on the N64. D442: the cheat menu and every + * COMPLETED check come from the query-time hooks in file2.c + * (fileGetIsCheatUnlocked, fileIsStageUnlockedAtDifficulty); the + * save is never patched. */ #if defined(LEFTOVERDEBUG) /* the debug unlock flags only exist with LEFTOVERDEBUG (not PAL); D442's query-time hooks cover All unlocked everywhere */ { extern s32 portAllUnlocked; /* port/src/video.c */ diff --git a/port/src/optionsoverlay.c b/port/src/optionsoverlay.c index 2aec54e5..d0226451 100644 --- a/port/src/optionsoverlay.c +++ b/port/src/optionsoverlay.c @@ -93,8 +93,8 @@ 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 kPadPreset[] = { "Jinx 1.1", "Jinx 1.2 (n/a)", "Jinx 1.3 (n/a)", "Custom", NULL }; /* D469: 1.2/1.3 undefined by D394 -> behave as 1.1 */ +static const char *const kTexFilter[] = { "Nearest", "Bilinear", "3-Point", "Trilinear", NULL }; /* Trilinear option (playtest 2026-10-03) */ +static const char *const kPadPreset[] = { "1.1 Jinx", "1.2 Christmas", "1.3 Frost", "Custom", "1.4 Elektra", NULL }; /* D498: index = stored value; inputPadPresetStep gives the display order */ static int s_padSeat = 0; /* D469: seat edited by the Controller page rows */ static const char *const kAimMode[] = { "N64", "Centred (PC)", NULL }; /* D337 */ static const char *const kAimRange[] = { "PC", "N64", NULL }; /* D338 */ @@ -146,6 +146,12 @@ static int s_resFitN = 0; static int s_resSel = 0; /* index into s_resFit */ static int s_dragRow = -1; /* scheduler-thread mouse drag */ static SDL_atomic_t s_dragWatchField; /* field+1, host may close F10 mid-drag */ +/* D489: field+1 of a watch slider stepped by D-pad/stick/arrows. Each commit + * is three joyDisablePoll/joyEnablePoll handshakes plus an EEPROM file + * rewrite on the game thread, so committing every held-repeat detent stalled + * frames. Steps stage like a mouse drag; optionsAdjustCommitPending() saves + * once when the direction is released. */ +static SDL_atomic_t s_adjWatchField; struct Row { const char *key; @@ -1386,10 +1392,21 @@ static void rowAdjust(struct Row *r, int dir) } case ROW_ENUM: { double lo = r->cfgMin, hi = r->cfgMax; - if (!strcmp(r->key, "Input.PadPreset")) { /* D469: only Jinx 1.1 and Custom are defined */ + if (!strcmp(r->key, "Input.PadPreset")) { /* D498: step through the preset display order (0,1,2,4,3) */ rowSet(r, (double)inputPadPresetStep((int)lround(v), dir)); break; } + if (!strcmp(r->key, "Video.TextureFilter")) { + /* D499: menu order Nearest, Bilinear, Trilinear, 3-Point (stored + * 0, 1, 3, 2: 2 stays 3-Point for existing inis and the Original + * N64 preset), so Bilinear and Trilinear sit side by side for A/B. */ + static const int order[4] = { 0, 1, 3, 2 }; + int cur = (int)lround(v), i = 0; + while (i < 4 && order[i] != cur) i++; + if (i == 4) i = 0; + rowSet(r, (double)order[(i + (dir > 0 ? 1 : 3)) % 4]); + break; + } v += dir; if (v < lo) v = hi; if (v > hi) v = lo; @@ -1468,11 +1485,27 @@ static void rowAdjust(struct Row *r, int dir) rowSet(r, (double)p); break; } + { + int field = watchSettingsFieldForKey(r->key); + if (field >= 0) { + /* D489: stage only; a different pending field saves first. */ + int prev = SDL_AtomicSet(&s_adjWatchField, field + 1); + if (prev > 0 && prev != field + 1) watchSettingsCommit(prev - 1); + rowSetCommit(r, v + dir * r->step, 0); + break; + } + } rowSet(r, v + dir * r->step); break; } } +void optionsAdjustCommitPending(void) +{ + int pending = SDL_AtomicSet(&s_adjWatchField, 0); + if (pending > 0) watchSettingsCommit(pending - 1); +} + /* ------------------------------------------------------------------------ */ /* D356: per-section "Reset to defaults" (plan Gate E, made concrete). */ /* The activation contract (plan §5.4): edge-triggered, two-step */ @@ -1721,6 +1754,7 @@ void optionsOverlayToggle(void) if (optionsBindingCaptureActive()) return; /* Escape cancels modal */ int pending = SDL_AtomicSet(&s_dragWatchField, 0); if (pending > 0) watchSettingsCommit(pending - 1); + optionsAdjustCommitPending(); /* D489 */ } s_open = !s_open; if (s_open) { @@ -1833,6 +1867,7 @@ static void overlayHandleInputLocked(void) prevUp = prevDn = prevLf = prevRt = prevLmb = prevRmb = 0; s_dragRow = -1; SDL_AtomicSet(&s_dragWatchField, 0); + optionsAdjustCommitPending(); /* D489: closed by the host mid-hold */ navDir = adjDir = 0; SDL_AtomicSet(&s_wheelPending, 0); return; @@ -2015,6 +2050,9 @@ static void overlayHandleInputLocked(void) } else { adjDir = 0; } + /* D489: save a stepped watch slider once, on release (also covers + * mouse click/right-click steps, which never hold left/right). */ + if (!(rt || lf)) optionsAdjustCommitPending(); } /* ---- mouse ---- @@ -2260,6 +2298,10 @@ static void valueText(int i, char *out, int n) snprintf(out, n, "%.1fx", v / 100.0); return; } + if (!strcmp(r->key, "Input.RumbleScale")) { /* D357: whole %, like deadzone/volume; storage stays 0..1 */ + snprintf(out, n, "%d%%", (int)lround(v * 100.0)); + return; + } if (r->kind == ROW_SLIDER && r->type == CONFIG_OPT_FLOAT) { snprintf(out, n, "%.2f", v); return; diff --git a/port/src/video.c b/port/src/video.c index 48280e2f..56447e69 100644 --- a/port/src/video.c +++ b/port/src/video.c @@ -60,7 +60,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/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 cfgTexFilter = 1; /* 0 = nearest, 1 = bilinear (default), 2 = N64 3-point + trilinear, 3 = trilinear (Trilinear option, playtest 2026-10-03) */ 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 */ static int cfgWrapFix = 0; /* D74 sub-tile UV pre-wrap + RC3/D167 non-PoT mask-period wrap (opt-in; GE_WRAPFIX env overrides) */ @@ -489,7 +489,7 @@ PD_CONSTRUCTOR static void videoConfigInit(void) configRegisterInt("Video.VSync", &cfgVSync, 0, 1); configRegisterInt("Video.FpsCap", &cfgFpsCap, 0, 1000); configRegisterInt("Video.MSAA", &cfgMSAA, 1, 16); /* D443: 16x added */ - configRegisterInt("Video.TextureFilter", &cfgTexFilter, 0, 2); + configRegisterInt("Video.TextureFilter", &cfgTexFilter, 0, 3); /* Trilinear option (playtest 2026-10-03) */ configRegisterInt("Video.FixMipTextures", &cfgFixMipTex, 0, 1); configRegisterInt("Video.DetailBaseTile", &cfgDetailBaseTile, 0, 1); configRegisterInt("Video.WrapFix", &cfgWrapFix, 0, 1); @@ -762,7 +762,11 @@ static void videoApplyImageOptions(void) static void videoApplyTexFilter(void) { - if (cfgTexFilter >= 2) { + /* Trilinear option (playtest 2026-10-03): value 3 = bilinear + generated mips. */ + if (cfgTexFilter == 3) { + gfx_set_texture_filter(FILTER_TRILINEAR); + gfx_set_mipmap_filter(MIPMAP_LINEAR); + } else if (cfgTexFilter >= 2) { gfx_set_texture_filter(FILTER_THREE_POINT); gfx_set_mipmap_filter(MIPMAP_LINEAR); } else if (cfgTexFilter == 1) { diff --git a/src/game/bg.c b/src/game/bg.c index 7661550c..cfde7b37 100644 --- a/src/game/bg.c +++ b/src/game/bg.c @@ -2502,15 +2502,6 @@ s32 bgLoadRoomPrimaryGdl(s32 roomnum, u8 *dst, s32 allocsize) obLoadBGFileBytesAtOffset(levelinfotable[levelentry_index].bg_seg_filename, scratch, fileoffset, size); -#if defined(PORT) - if (getenv("GE_D69BB")) { - osSyncPrintf("D69bb primGdl room=%d fileoffset=%08x size=%d " - "compbytes=%02x%02x%02x%02x%02x%02x%02x%02x\n", - roomnum, (unsigned)fileoffset, (int)size, - scratch[0], scratch[1], scratch[2], scratch[3], - scratch[4], scratch[5], scratch[6], scratch[7]); - } -#endif // Decompress from the end-of-buffer location at dst. expanded_size = bgDecompress(scratch, dst); @@ -2521,15 +2512,6 @@ s32 bgLoadRoomPrimaryGdl(s32 roomnum, u8 *dst, s32 allocsize) expanded_size = bgWidenRoomGdl(dst, expanded_size); #endif -#if defined(PORT) - if (getenv("GE_D69BB")) { - osSyncPrintf("D69bb primGdl room=%d expanded=%d dstbytes=" - "%02x%02x%02x%02x%02x%02x%02x%02x\n", - roomnum, (int)expanded_size, - dst[0], dst[1], dst[2], dst[3], - dst[4], dst[5], dst[6], dst[7]); - } -#endif /** * Copy the decompressed GDL back to the end of the buffer as scratch. @@ -2551,30 +2533,7 @@ s32 bgLoadRoomPrimaryGdl(s32 roomnum, u8 *dst, s32 allocsize) roominfo->ptr_expanded_mapping_info = dst; roominfo->usize_primary_DL_binary = expanded_size; -#if defined(PORT) - /* TEMP D85: dump the post-texLoadFromGdl primary GDL command stream. */ - if (getenv("GE_D85DUMP")) { - Gfx *g = (Gfx *)dst; - s32 k; - osSyncPrintf("D85DUMP room=%d dst=%p size=%d\n", roomnum, (void *)dst, (int)size); - for (k = 0; k < 400; k++) { - u32 w0 = (u32)g[k].words.w0; - u32 w1 = (u32)g[k].words.w1; - osSyncPrintf("D85DUMP [%3d] op=%02x w0=%08x w1=%08x\n", k, (u8)(w0 >> 24), w0, w1); - if ((u8)(w0 >> 24) == 0xB8) break; /* G_ENDDL */ - } - } -#endif -#if defined(PORT) - /* TEMP D63: log primary GDL load result (env GE_D63=1) */ - if (getenv("GE_D63")) { - u32 *w = (u32 *)dst; - osSyncPrintf("D63 bgLoadRoomPrimaryGdl room=%d dst=%p expanded=%d final=%d w0..3=(%08x,%08x,%08x,%08x)\n", - roomnum, (void *)dst, (int)expanded_size, (int)size, - w[0], w[1], w[2], w[3]); - } -#endif // Return the uncompressed data size. return expanded_size; @@ -2970,18 +2929,6 @@ Gfx *bgRenderRoomPrimary(Gfx *gdl, s32 room_index) { gdl = applyRoomMatrixToDisplayList(gdl, room_index); -#if defined(PORT) - /* TEMP D63: log primary GDL at render time (env GE_D63=1) */ - static int d63on_p = -1; /* D302-class cache: per room, per frame */ - if (d63on_p < 0) d63on_p = getenv("GE_D63") != NULL; - if (d63on_p) { - u32 *w = (u32 *)g_BgRoomInfo[room_index].ptr_expanded_mapping_info; - osSyncPrintf("D63 bgRenderRoomPrimary room=%d ptr=%p usize=%d w0..3=(%08x,%08x,%08x,%08x)\n", - room_index, (void *)g_BgRoomInfo[room_index].ptr_expanded_mapping_info, - (int)g_BgRoomInfo[room_index].usize_primary_DL_binary, - w[0], w[1], w[2], w[3]); - } -#endif gSPSegment(gdl++, SPSEGMENT_BG_VTX, OS_K0_TO_PHYSICAL(g_BgRoomInfo[room_index].vertices)); gSPDisplayList(gdl++, OS_K0_TO_PHYSICAL(g_BgRoomInfo[room_index].ptr_expanded_mapping_info)); @@ -3017,18 +2964,6 @@ Gfx *bgRenderRoomSecondary(Gfx *gdl, s32 room_index) { gdl = applyRoomMatrixToDisplayList(gdl, room_index); -#if defined(PORT) - /* TEMP D63: log secondary GDL at render time (env GE_D63=1) */ - static int d63on_s = -1; /* D302-class cache: per room, per frame */ - if (d63on_s < 0) d63on_s = getenv("GE_D63") != NULL; - if (d63on_s) { - u32 *w = (u32 *)g_BgRoomInfo[room_index].ptr_secondary_expanded_mapping_info; - osSyncPrintf("D63 bgRenderRoomSecondary room=%d ptr=%p usize=%d w0..3=(%08x,%08x,%08x,%08x)\n", - room_index, (void *)g_BgRoomInfo[room_index].ptr_secondary_expanded_mapping_info, - (int)g_BgRoomInfo[room_index].usize_secondary_DL_binary, - w[0], w[1], w[2], w[3]); - } -#endif gSPSegment(gdl++, SPSEGMENT_BG_VTX, OS_K0_TO_PHYSICAL(g_BgRoomInfo[room_index].vertices)); gSPDisplayList(gdl++, OS_K0_TO_PHYSICAL(g_BgRoomInfo[room_index].ptr_secondary_expanded_mapping_info)); @@ -3086,21 +3021,6 @@ void bgBuildRoomVtxBounds(s32 roomID) cmdindex = 0; numpoints = 0; -#if defined(PORT) - /* TEMP D69: log the vtx-bounds walk (env GE_D69BB=1) */ - if (getenv("GE_D69BB")) { - osSyncPrintf("D69bb room=%d gdl=%p vertices=%p usizePrim=%d usizePts=%d\n", - roomID, (void *)gdl, (void *)vertices, - (int)g_BgRoomInfo[roomID].usize_primary_DL_binary, - (int)g_BgRoomInfo[roomID].usize_point_index_binary); - for (cmdindex = 0; cmdindex < 8; cmdindex++) { - osSyncPrintf("D69bb cmd[%d] cmd=%02x par=%08x addr=%08x\n", - cmdindex, gdl[cmdindex].dma.cmd, - (unsigned)gdl[cmdindex].dma.par, - (unsigned)gdl[cmdindex].dma.addr); - } - } -#endif while (gdl[cmdindex].dma.cmd != G_ENDDL) { @@ -3508,11 +3428,6 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV { static s32 d154_calls = 0; d154_calls++; - if (d154_calls <= 64 && getenv("GE_D154")) { - osSyncPrintf("D154 call#%d room=%d gdlidx=%d vtxoff=%d op=%d hdr w0=%08x w1=%08x\n", - d154_calls, (int)roomnum, (int)point->gdlindex, (int)vtxoff, - (int)op, (unsigned)(u32)gdl[-1].words.w0, (unsigned)(u32)gdl[-1].words.w1); - } } #endif @@ -3535,13 +3450,6 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV idx[0] = (((u8 *) gdl)[5] / 10) - vtxoff; idx[1] = (((u8 *) gdl)[6] / 10) - vtxoff; idx[2] = (((u8 *) gdl)[7] / 10) - vtxoff; -#endif -#ifdef PORT - static int d154on = -1; /* D302-class cache: inner GDL walk, fires per bullet hit */ - if (d154on < 0) d154on = getenv("GE_D154") != NULL; - if (d154on) - osSyncPrintf("D154 TRI1 w1=%08x idx=%d,%d,%d\n", - (unsigned)(u32)gdl->words.w1, (int)idx[0], (int)idx[1], (int)idx[2]); #endif i = 0; @@ -3728,14 +3636,6 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV idx2[2] = (((u32) ((u16 *) gdl)[1]) >> 12) - vtxoff; } #endif -#ifdef PORT - static int d154on4 = -1; /* D302-class cache: inner GDL walk, fires per bullet hit */ - if (d154on4 < 0) d154on4 = getenv("GE_D154") != NULL; - if (d154on4) - osSyncPrintf("D154 TRI4 s2=%d w0=%08x w1=%08x idx2=%d,%d,%d\n", - (int)s2, (unsigned)(u32)gdl->words.w0, (unsigned)(u32)gdl->words.w1, - (int)idx2[0], (int)idx2[1], (int)idx2[2]); -#endif i = 0; @@ -6038,8 +5938,23 @@ void sub_GAME_7F0BA2D4(coord3d *bbmin, coord3d *bbmax, s32 *room_list, s32 *coun goto next_portal; } +#ifdef PORT + /* D491: the N64 read each coord3d across three separately-declared + * adjacent s32 globals (D_80044904..0C = FLT_MAX bits, D_80044910..18 = + * -FLT_MAX bits). mingw lays them out in reverse order, so the reads + * picked up neighbouring data: portal_max started as {-FLT_MAX, FLT_MAX, + * FLT_MAX} and every portal's Y/Z max stuck at FLT_MAX. Read the named + * globals instead. Layout only (porting-notes D5), no logic change. */ + portal_min.x = *(f32 *) &D_80044904; + portal_min.y = *(f32 *) &D_80044908; + portal_min.z = *(f32 *) &D_8004490C; + portal_max.x = *(f32 *) &D_80044910; + portal_max.y = *(f32 *) &D_80044914; + portal_max.z = *(f32 *) &D_80044918; +#else portal_min = *(coord3d *) &D_80044904; portal_max = *(coord3d *) &D_80044910; +#endif portal_pts = g_BgPortals[portal_idx].offset_portal; for (j = 0; j < portal_pts->numPoints; j++) diff --git a/src/game/bondview2.c b/src/game/bondview2.c index 241de776..f9202539 100644 --- a/src/game/bondview2.c +++ b/src/game/bondview2.c @@ -652,7 +652,11 @@ void solo_char_load(void) pitemheader = NULL; } +#ifdef PORT + something_with_generating_object(self, prop, item, 0, (WeaponObjRecord *)(uintptr_t)(u32)(helddst), (ItemModelFileRecord *)pitemheader); /* D441: zero-extend s32-held DRAM ptr */ +#else something_with_generating_object(self, prop, item, 0, (WeaponObjRecord *)helddst, (ItemModelFileRecord *)pitemheader); +#endif } chrlvMergeKneelToStand(self, 0.0f); @@ -2210,18 +2214,6 @@ s32 bondviewTryMoveToStan(struct coord3d *arg0, StandTile **stan) { sp90 = g_CurrentPlayer->field_488.current_tile_ptr; -#ifdef PORT - if (sp90 == NULL && getenv("GE_D90")) { - static int d90n = 0; - if (d90n++ < 8) - fprintf(stderr, "D90 bondviewTryMoveToStan: current_tile_ptr NULL " - "(prop=%p prop->stan=%p ctp4p=%p pos=%.1f,%.1f,%.1f)\n", - (void *)g_CurrentPlayer->prop, - (void *)(g_CurrentPlayer->prop ? g_CurrentPlayer->prop->stan : NULL), - (void *)g_CurrentPlayer->field_488.current_tile_ptr_for_portals, - arg0->f[0], arg0->f[1], arg0->f[2]); - } -#endif if (obj_collision_flag) { @@ -8119,7 +8111,11 @@ void bondviewMovePlayerUpdateViewport(s8 stick_x, s8 stick_y, u16 buttons) if ((cameraBufferToggle != 0) && (viGetFrameBuf2() == (u8*)(cfb_16[1]))) { +#ifdef PORT + viSetFrameBuf2((u8 *)(uintptr_t)(u32)(resolution)); /* D441: zero-extend s32-held DRAM ptr */ +#else viSetFrameBuf2((u8 *) resolution); +#endif } #ifdef VERSION_EU @@ -8906,7 +8902,11 @@ void mp_respawn_handler(void) start_pos.z = g_Startpad[var_v1]->pos.z; start_stan = g_Startpad[var_v1]->stan; +#ifdef PORT + stan_height = bondviewYPositionRelated((StandTile *)(uintptr_t)(u32)(start_stan), start_pos.x, start_pos.z); /* D441: zero-extend s32-held DRAM ptr */ +#else stan_height = bondviewYPositionRelated(start_stan, start_pos.x, start_pos.z); +#endif start_pos.y = g_CurrentPlayer->eyeheight + stan_height; g_CurrentPlayer->field_70 = stan_height; @@ -8921,7 +8921,11 @@ void mp_respawn_handler(void) g_CurrentPlayer->field_6C = (f32) (stan_height / 0.17000002f); #endif +#ifdef PORT + change_player_pos_to_target(&g_CurrentPlayer->field_488, &start_pos, (StandTile *)(uintptr_t)(u32)(start_stan)); /* D441: zero-extend s32-held DRAM ptr */ +#else change_player_pos_to_target(&g_CurrentPlayer->field_488, &start_pos, start_stan); +#endif g_CurrentPlayer->field_488.theta_transform.x = -sinf(start_look_angle); g_CurrentPlayer->field_488.theta_transform.y = 0.0f; @@ -8929,7 +8933,11 @@ void mp_respawn_handler(void) g_CurrentPlayer->prop->pos.x = g_CurrentPlayer->bondprevpos.x = start_pos.f[0]; g_CurrentPlayer->prop->pos.y = g_CurrentPlayer->bondprevpos.y = start_pos.f[1]; g_CurrentPlayer->prop->pos.z = g_CurrentPlayer->bondprevpos.z = start_pos.f[2]; +#ifdef PORT + g_CurrentPlayer->prop->stan = (StandTile *)(uintptr_t)(u32)(start_stan); /* D441: zero-extend s32-held DRAM ptr */ +#else g_CurrentPlayer->prop->stan = start_stan; +#endif #if defined(VERSION_EU) g_CurrentPlayer->field_3B8.x = (f32) (g_CurrentPlayer->field_488.pos.x / 0.118799984f); g_CurrentPlayer->field_3B8.y = (f32) (g_CurrentPlayer->field_488.pos.y / 0.118799984f); @@ -10989,7 +10997,11 @@ join_768: if (ppointers[index]->bodyModel->anim2 == NULL) { startframe = (0.0f <= frame) ? (frame) : (0.0f); +#ifdef PORT + modelSetAnimation(ppointers[index]->bodyModel, (ModelAnimation *)(uintptr_t)(u32)(anim), 0, startframe, angle, 16.0f); /* D441: zero-extend s32-held DRAM ptr */ +#else modelSetAnimation(ppointers[index]->bodyModel, (ModelAnimation *) anim, 0, startframe, angle, 16.0f); +#endif ppointers[index]->players_cur_animation = anim; ppointers[index]->field_1288 = angle; diff --git a/src/game/chr.c b/src/game/chr.c index adaae032..eb411d0a 100644 --- a/src/game/chr.c +++ b/src/game/chr.c @@ -2486,11 +2486,23 @@ s32 chrTick(PropRecord *prop) { if (D_8002C904) { +#ifdef PORT + if (((ModelAnimation *)(uintptr_t)(u32)(animation_table_ptrs1[g_AnimationTablePointerCountRelated])) != ((ModelAnimation *)1)) /* D441: zero-extend s32-held DRAM ptr */ +#else if (((ModelAnimation *)animation_table_ptrs1[g_AnimationTablePointerCountRelated]) != ((ModelAnimation *)1)) +#endif { +#ifdef PORT + if (objecthandlerGetModelAnim(model) != ((ModelAnimation *)(uintptr_t)(u32)(animation_table_ptrs1[g_AnimationTablePointerCountRelated]))) /* D441: zero-extend s32-held DRAM ptr */ +#else if (objecthandlerGetModelAnim(model) != ((ModelAnimation *)animation_table_ptrs1[g_AnimationTablePointerCountRelated])) +#endif { +#ifdef PORT + modelSetAnimation(model, (ModelAnimation *)(uintptr_t)(u32)(animation_table_ptrs1[g_AnimationTablePointerCountRelated]), 0, 0.0f, 0.5f, 0.0f); /* D441: zero-extend s32-held DRAM ptr */ +#else modelSetAnimation(model, (ModelAnimation *)animation_table_ptrs1[g_AnimationTablePointerCountRelated], 0, 0.0f, 0.5f, 0.0f); +#endif } } } diff --git a/src/game/chr_b.c b/src/game/chr_b.c index f8fe0ebd..74a6fa68 100644 --- a/src/game/chr_b.c +++ b/src/game/chr_b.c @@ -100,7 +100,11 @@ struct Model *makeonebody(s32 body, s32 head, struct ModelFileHeader *bodyHeader if ((headHeader != 0) && (c_item_entries[body].hasHead == 0)) { bodyHeader->numRecords -= headHeader->numRecords; +#ifdef PORT + modelAttachHead(model, (ModelNode *)(uintptr_t)(u32)(opcode), headHeader); /* D441: zero-extend s32-held DRAM ptr */ +#else modelAttachHead(model, opcode, headHeader); +#endif if ((sunglasses == 0) && ((s32) headHeader->numSwitches > 0)) { diff --git a/src/game/chraction.c b/src/game/chraction.c index d1e84587..cdc33743 100644 --- a/src/game/chraction.c +++ b/src/game/chraction.c @@ -608,7 +608,11 @@ void chrlvPerformAnimationForActor(ChrRecord *self, s32 animID, s32 startframe, chrStopFiring(self); +#ifdef PORT + modelSetAnimation(self->model, (void *)(uintptr_t)(u32)(animation_table_ptrs1[animID]), (bitfield & ANIM_MIRROR) != 0, startframef, phi_f0, (f32)interpol_time60); /* D441: zero-extend s32-held DRAM ptr */ +#else modelSetAnimation(self->model, (void *)animation_table_ptrs1[animID], (bitfield & ANIM_MIRROR) != 0, startframef, phi_f0, (f32)interpol_time60); +#endif if (endframe >= 0) { @@ -4981,7 +4985,11 @@ bool if_actor_able_set_on_path(ChrRecord *self, s32 pathid) { if (pathid && chrIsNotDeadOrShot(self)) { +#ifdef PORT + set_actor_on_path(self, (struct patrol_path *)(uintptr_t)(u32)(pathid)); /* D441: zero-extend s32-held DRAM ptr */ +#else set_actor_on_path(self, pathid); +#endif return TRUE; } diff --git a/src/game/chrai.c b/src/game/chrai.c index 0faf0a98..7128c132 100644 --- a/src/game/chrai.c +++ b/src/game/chrai.c @@ -1138,7 +1138,11 @@ void ai(PropDefHeaderRecord *Entityp, PROP_TYPE EntityType) { zero = 0; // debug value maybe? /* D32/D33: table holds s32 offsets; cast at use site. */ +#ifdef PORT + modelSetAnimation(AircraftEntityp->model, (ModelAnimation *)(uintptr_t)(u32)(animation_table_ptrs2[anim_id]), zero, startframe, 0.5f, (s32)ai->INTERPOL_TIME60); /* D441: zero-extend s32-held DRAM ptr */ +#else modelSetAnimation(AircraftEntityp->model, (ModelAnimation *)animation_table_ptrs2[anim_id], zero, startframe, 0.5f, (s32)ai->INTERPOL_TIME60); +#endif if (endframe >= 0) { modelSetAnimEndFrame(AircraftEntityp->model, endframe); diff --git a/src/game/chrprop.c b/src/game/chrprop.c index 99cfec0d..2e8b348f 100644 --- a/src/game/chrprop.c +++ b/src/game/chrprop.c @@ -434,10 +434,6 @@ void chrpropDetach(PropRecord* prop) { Gfx *chrpropRender(Gfx * gdl, PropRecord *prop, s32 withalpha) { u8 type; -#if defined(PORT) - Gfx *gdl_in = gdl; - int probe = GE_ENVFLAG("GE_D96"); -#endif type = prop->type; @@ -462,18 +458,6 @@ Gfx *chrpropRender(Gfx * gdl, PropRecord *prop, s32 withalpha) gdl = bondviewRenderProp(prop, gdl, withalpha); } -#if defined(PORT) - if (probe) { - long long d = (long long)((char*)gdl - (char*)gdl_in); - if (d > 0x4000 || d < 0) { - osSyncPrintf("D96 chrpropRender RUNAWAY prop=%p type=%d alpha=%d flags=%08x chr=%p obj=%p model=%p delta=0x%llx gdl %p->%p\n", - (void*)prop, (int)type, (int)withalpha, (unsigned)prop->flags, - (void*)prop->chr, (void*)prop->obj, - (void*)(prop->chr ? prop->chr->model : NULL), - (unsigned long long)d, (void*)gdl_in, (void*)gdl); - } - } -#endif return gdl; } @@ -506,15 +490,6 @@ Gfx *chrpropsRenderPass(Gfx *gdl, s32 roomid, s32 renderpass) } } -#if defined(PORT) - if (GE_ENVFLAG("GE_D96")) { - s32 n = (s32)((PropRecord **)g_LastOnScreenProp - g_OnScreenPropList); - osSyncPrintf("D96 chrpropsRenderPass room=%d pass=%d gdl=%p list=[%p..%p) n=%d cap=%d\n", - (int)roomid, (int)renderpass, (void*)gdl, - (void*)g_OnScreenPropList, (void*)g_LastOnScreenProp, (int)n, - (int)ONSCREEN_PROP_LIST_LEN); - } -#endif if ((renderpass == 0) || (renderpass == 2)) { @@ -1332,7 +1307,11 @@ void chrpropAddBulletHit(struct ShotData *shotdata, PropRecord *prop, f32 dist, shotdata->hits[i].node = node; shotdata->hits[i].hit = *hitthing; shotdata->hits[i].room = room; +#ifdef PORT + shotdata->hits[i].unk44 = (struct ModelNode *)(uintptr_t)(u32)(unk44); /* D441: zero-extend s32-held DRAM ptr */ +#else shotdata->hits[i].unk44 = unk44; +#endif shotdata->hits[i].model = model; shotdata->hits[i].countsAsPenetration = countsAsPenetration; break; @@ -3528,7 +3507,12 @@ void sub_GAME_7F03ECC0(f32 x1, f32 x2, f32 y1, f32 y2, f32 z1, f32 z2, Mtxf *m, s32 maxxi = 0; s32 minzi; s32 maxzi = 0; +#ifdef AVOID_UB + s32 rem[8]; /* D490: filterloop writes up to 6 when extremes coincide (flat bbox); + on N64 the overflow landed in `pad`, on PC it corrupted pts[0] */ +#else s32 rem[4]; +#endif s32 cnt; f64 x1d = x1; f64 x2d = x2; diff --git a/src/game/dyn.c b/src/game/dyn.c index 8bc67d9b..decf08df 100644 --- a/src/game/dyn.c +++ b/src/game/dyn.c @@ -37,6 +37,18 @@ s32 D_800482E0 = 0; s32 g_GfxSizesByPlayerCount[] = {0x10000, 0x18000, 0x20000, 0x28000}; s32 g_VtxSizesByPlayerCount[] = {0x10000, 0x18000, 0x20000, 0x28000}; +#ifdef PORT +/* D500: see dynInitMemory. Two 384 KB halves (~10x Aztec's budget, ~4x the + * multiplayer maps') in the unused top of emulated DRAM: [+0x700000, + * +0x7C0000), above the mempool end (D95, n64stubs.c) and below + * animations_frame_buffer (+0x7FFD30, dram_syms.S). */ +#include +#define PORT_VTX_HALF 0x60000 +#define PORT_VTX_POOL ((u8 *)(uintptr_t)(PORT_DRAM_V1_BASE + 0x700000UL)) +static s32 s_portVtxN64Budget; +static s32 s_portVtxWarned; /* 1 = past the N64 budget logged, 2 = past the port pool logged */ +#endif + char membars_string1[] = ">>>>>>>>>>>>>>>>>>>>>>>>>"; char membars_string2[] = "========================="; char membars_string3[] = "-------------------------"; @@ -74,9 +86,27 @@ void dynInitMemory(void) { g_GfxBuffers[2] = (g_GfxBuffers[1] + g_GfxSizesByPlayerCount[getPlayerCount() - 1]); #endif +#ifdef PORT + /* D500: the vtx pool (per-frame vertices, matrices, lights; bump-allocated + * with no bounds check) is sized per level for what the N64 could see, + * as low as -mvtx40 (Aztec, Streets). The port's wider FOV, draw distance + * and LOD put more models in view, so a busy frame could run past its + * half into the other half, which the render worker may still be drawing + * (triangles "falling apart", Aztec shuttle room, 2026-10-03). Use a + * fixed, much larger pool in the free top of emulated DRAM instead of + * the stage bank (tight, see D294). The addresses stay 0x70xxxxxx, so + * they survive every s32 path and osVirtualToPhysical like any other + * game RAM pointer. */ + g_VtxBuffers[0] = PORT_VTX_POOL; + g_VtxBuffers[1] = (g_VtxBuffers[0] + PORT_VTX_HALF); + g_VtxBuffers[2] = (g_VtxBuffers[1] + PORT_VTX_HALF); + s_portVtxN64Budget = g_VtxSizesByPlayerCount[getPlayerCount() - 1]; + s_portVtxWarned = 0; +#else g_VtxBuffers[0] = mempAllocBytesInBank(g_VtxSizesByPlayerCount[getPlayerCount() - 1] * 2, MEMPOOL_STAGE); g_VtxBuffers[1] = (g_VtxBuffers[0] + g_VtxSizesByPlayerCount[getPlayerCount() - 1]); g_VtxBuffers[2] = (g_VtxBuffers[1] + g_VtxSizesByPlayerCount[getPlayerCount() - 1]); +#endif g_GfxActiveBufferIndex = 0; g_GfxRequestedDisplayList = FALSE; @@ -128,6 +158,21 @@ void *dynAllocate(s32 size) { } void dynSwapBuffers(void) { +#ifdef PORT + { + /* D500: report (once per level each) a frame that used more than the + * N64 budget, and one that overran even the port pool. */ + s32 used = (s32)(g_GfxMemPos - g_VtxBuffers[g_GfxActiveBufferIndex]); + if (used > PORT_VTX_HALF && s_portVtxWarned < 2) { + s_portVtxWarned = 2; + osSyncPrintf("D500: vtx pool OVERRUN: frame used %d bytes, port pool %d\n", used, PORT_VTX_HALF); + } else if (used > s_portVtxN64Budget && s_portVtxWarned < 1) { + s_portVtxWarned = 1; + osSyncPrintf("D500: vtx pool: frame used %d bytes, N64 budget %d (port pool %d)\n", + used, s_portVtxN64Budget, PORT_VTX_HALF); + } + } +#endif g_GfxActiveBufferIndex = (g_GfxActiveBufferIndex ^ 1); g_GfxRequestedDisplayList = FALSE; g_GfxMemPos = g_VtxBuffers[g_GfxActiveBufferIndex]; diff --git a/src/game/front.c b/src/game/front.c index 14353cef..5d62fe48 100644 --- a/src/game/front.c +++ b/src/game/front.c @@ -1425,39 +1425,17 @@ u32 frontCheckCursorOnNextTab(void) //******************************************************************************************************** //LEGAL SCREEN //******************************************************************************************************** -#ifdef PORT -/* TEMP D63: log each intro-menu init that reuses ptr_logo_and_walletbond_DL, - * with the word at base+0x200 (the gun-barrel sub-DL slot) before/after. */ -static void d63MenuProbe(const char *menu) -{ - if (!getenv("GE_D63") || !ptr_logo_and_walletbond_DL) return; - u32 before = *(u32 *)(ptr_logo_and_walletbond_DL + 0x200); - osSyncPrintf("D63 menu init %s: base=%p word@+0x200 before=%08x\n", menu, (void *)ptr_logo_and_walletbond_DL, before); -} -static void d63MenuProbeAfter(const char *menu) -{ - if (!getenv("GE_D63") || !ptr_logo_and_walletbond_DL) return; - u32 after = *(u32 *)(ptr_logo_and_walletbond_DL + 0x200); - osSyncPrintf("D63 menu init %s: word@+0x200 after=%08x\n", menu, after); -} -#endif void init_menu00_legalscreen(void) { s32 padding; struct coord3d pos; -#ifdef PORT - d63MenuProbe("legalscreen"); -#endif musicTrack1Stop(); maybe_is_in_menu = TRUE; g_MenuTimer = 0; pos = legalpage_pos; load_object_fill_header(PitemZ_entries[PROP_LEGALPAGE].header, PitemZ_entries[PROP_LEGALPAGE].filename, ptr_logo_and_walletbond_DL, 0x3c000, 0); -#ifdef PORT - d63MenuProbeAfter("legalscreen"); -#endif modelCalculateRwDataLen(PitemZ_entries[PROP_LEGALPAGE].header); logoinst = modelmgrInstantiateModel(PitemZ_entries[PROP_LEGALPAGE].header); @@ -1669,13 +1647,7 @@ void init_menu01_nintendo(void) g_MenuTimer = 0; pos = nintendologo_pos; -#ifdef PORT - d63MenuProbe("nintendologo"); -#endif load_object_fill_header(PitemZ_entries[PROP_NINTENDOLOGO].header, PitemZ_entries[PROP_NINTENDOLOGO].filename, ptr_logo_and_walletbond_DL, 0x3c000, 0); -#ifdef PORT - d63MenuProbeAfter("nintendologo"); -#endif modelCalculateRwDataLen(PitemZ_entries[PROP_NINTENDOLOGO].header); logoinst = modelmgrInstantiateModel(PitemZ_entries[PROP_NINTENDOLOGO].header); modelSetScale(logoinst, 1.0f); @@ -1856,13 +1828,7 @@ Gfx *constructor_menu01_nintendo(Gfx *DL) */ void init_menu02_rarelogo(void) { -#ifdef PORT - d63MenuProbe("rarelogo"); -#endif setupRarewareLogoData(ptr_logo_and_walletbond_DL, 0x78000); -#ifdef PORT - d63MenuProbeAfter("rarelogo"); -#endif sndPlaySfx(g_musicSfxBufferPtr, RARELOGO_SFX, 0); } @@ -1908,9 +1874,6 @@ Gfx * constructor_menu02_rareware(Gfx * DL) { //GUNBARREL //******************************************************************************************************** void init_menu03_gunbarrel(void) { -#ifdef PORT - d63MenuProbe("gunbarrel"); -#endif initializeGunBarrelIntro(ptr_logo_and_walletbond_DL, 0x78000); musicTrack1Play(M_INTRO); maybe_is_in_menu = TRUE; @@ -1955,13 +1918,7 @@ void init_menu04_goldeneyelogo(void) g_MenuTimer = 0; pos = goldeneyelogo_pos; -#ifdef PORT - d63MenuProbe("goldeneyelogo"); -#endif load_object_fill_header(PitemZ_entries[PROP_GOLDENEYELOGO].header, PitemZ_entries[PROP_GOLDENEYELOGO].filename, ptr_logo_and_walletbond_DL, 0x3c000, 0); -#ifdef PORT - d63MenuProbeAfter("goldeneyelogo"); -#endif modelCalculateRwDataLen(PitemZ_entries[PROP_GOLDENEYELOGO].header); logoinst = modelmgrInstantiateModel(PitemZ_entries[PROP_GOLDENEYELOGO].header); #ifdef DEBUG @@ -6696,20 +6653,7 @@ void load_briefing_text_for_stage(void) * and the objective/briefing text renders blank (D143). Decode in * place, mirroring langFixupLoadedBank() in language.c. */ if (ptrbriefingdata) { - if (getenv("GE_D178")) { - const u16 *w = (const u16 *)ptrbriefingdata; - fprintf(stderr, "D178 %s raw brief=%04x,%04x,%04x,%04x obj0=%04x/%04x obj3=%04x/%04x seldiff=%d\n", - (const char *)mission_folder_setup_entries[briefingpage].briefing_name_ptr, - w[0], w[1], w[2], w[3], w[4], w[5], w[10], w[11], - (int)selected_difficulty); - } romdataFixupBriefing((u8 *)ptrbriefingdata); - if (getenv("GE_D178")) { - const u16 *w = (const u16 *)ptrbriefingdata; - fprintf(stderr, "D178 %s fix brief=%04x,%04x,%04x,%04x obj0=%04x/%04x obj3=%04x/%04x\n", - (const char *)mission_folder_setup_entries[briefingpage].briefing_name_ptr, - w[0], w[1], w[2], w[3], w[4], w[5], w[10], w[11]); - } } #endif @@ -7965,15 +7909,6 @@ void init_menu18_displaycast(void) randomly_selected_intro_animation = randomGetNext() % ((u32) intro_animation_count); body = intro_char_table[intro_character_index].body; head = intro_char_table[intro_character_index].head; -#ifdef PORT - /* TEMP D64: dump the live cast-table entry (suspected .data clobber). */ - if (getenv("GE_D63")) { - const u32 *e = (const u32 *)&intro_char_table[intro_character_index]; - osSyncPrintf("D64 cast-table idx=%d body=%d head=%08x raw=[%08x %08x %08x %08x %08x]\n", - (int)intro_character_index, (int)body, (unsigned)head, - e[0], e[1], e[2], e[3], e[4]); - } -#endif cameraPreset = intro_animation_table[randomly_selected_intro_animation].camera_preset; if (body == BODY_Special_Operations_Uniform) @@ -8040,15 +7975,6 @@ void init_menu18_displaycast(void) bufferRemaining -= allocSize; bufferPtr += allocSize; -#ifdef PORT - /* TEMP D64: dump head/body/index state right before the wild access. */ - if (getenv("GE_D63")) { - extern u32 num_male_heads, num_female_heads; - osSyncPrintf("D64 pre-citem idx=%d body=%d head=%08x nmh=%u nfh=%u\n", - (int)intro_character_index, (int)body, (unsigned)head, - num_male_heads, num_female_heads); - } -#endif if (head >= 0) { headHeader = c_item_entries[head].header; @@ -8127,7 +8053,11 @@ void init_menu18_displaycast(void) #else modelSetAnimPlaySpeed(cast_model, 0.5f, 0); #endif +#ifdef PORT + modelSetAnimation(cast_model, (ModelAnimation *)(uintptr_t)(u32)(animation_table_ptrs1[intro_animation_table[randomly_selected_intro_animation].animID]), flip, intro_animation_table[randomly_selected_intro_animation].startframeoffset, intro_animation_table[randomly_selected_intro_animation].playback_speed, 0.0f); /* D441: zero-extend s32-held DRAM ptr */ +#else modelSetAnimation(cast_model, animation_table_ptrs1[intro_animation_table[randomly_selected_intro_animation].animID], flip, intro_animation_table[randomly_selected_intro_animation].startframeoffset, intro_animation_table[randomly_selected_intro_animation].playback_speed, 0.0f); +#endif g_MenuTimer = 0; cast_camera_dist_start = ((((f32) ((u32) randomGetNext())) * (1.0f / U32_MAX)) * 80.0f) + 70.0f; @@ -8234,27 +8164,14 @@ void interface_menu18_displaycast(void) } f = intro_character_index; -#ifdef PORT - /* TEMP D65: trace the cast-end decision. */ - 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) { intro_character_index = 0; f = 0; -#ifdef PORT - if (GE_ENVFLAG("GE_D63")) - osSyncPrintf("D65 cast-end RESET idx=0\n"); -#endif } if (intro_character_index > 0) { -#ifdef PORT - if (GE_ENVFLAG("GE_D63")) - osSyncPrintf("D65 cast-end frontChangeMenu idx=%d\n", (int)intro_character_index); -#endif frontChangeMenu(MENU_DISPLAY_CAST, 1); } else if (full_actor_intro != 0) @@ -8517,18 +8434,6 @@ Gfx *constructor_menu18_displaycast(Gfx *DL) DL = microcode_constructor(DL); -#ifdef PORT - /* TEMP D65: dump index state at the crashing langGet. */ - if (getenv("GE_D63")) { - int i65 = (int)intro_character_index; - osSyncPrintf("D65 ctor idx=%d full=%d\n", i65, (int)full_actor_intro); - if (i65 >= 0 && i65 <= 40) { - const u32 *e = (const u32 *)&intro_char_table[i65]; - osSyncPrintf("D65 ctor raw=[%08x %08x %08x %08x %08x]\n", - e[0], e[1], e[2], e[3], e[4]); - } - } -#endif if (full_actor_intro == FALSE) { text = langGet( (u16)intro_char_table[intro_character_index].text1); @@ -8719,7 +8624,11 @@ void menu_init(void) { if (viGetFrameBuf2() == (cfb_16[1])) { +#ifdef PORT + viSetFrameBuf2((u8 *)(uintptr_t)(u32)(ptr_menu_videobuffer)); /* D441: zero-extend s32-held DRAM ptr */ +#else viSetFrameBuf2(ptr_menu_videobuffer); +#endif } viSetAspect(MENU_INIT_ASPECT_440); diff --git a/src/game/gunfire.c b/src/game/gunfire.c index d5b838a4..03b7f442 100644 --- a/src/game/gunfire.c +++ b/src/game/gunfire.c @@ -87,6 +87,10 @@ extern u32 D_80035CC0; extern u32 D_80035D00; extern u32 D_80035D04[]; extern u32 D_80035EA4; +#ifdef PORT +extern u32 D_80035EA8; +extern u32 D_80035EAC; +#endif extern u32 watchControllerButtonBases[]; extern GunModelFileRecord gitem_structs[]; extern struct gun_trigger_state g_ZeroTriggerState; @@ -5346,7 +5350,16 @@ void sub_GAME_7F068508(GUNHAND handnum, f32 floor_y_pos) return; } +#ifdef PORT + /* D491: N64 reads rot across the adjacent globals D_80035EA4/A8/AC (gun.c, + * all zero); mingw lays them out in reverse, so y/z came from + * g_ZeroTriggerState. Read the named globals. Layout only (porting-notes D5). */ + rot.x = *(f32 *) &D_80035EA4; + rot.y = *(f32 *) &D_80035EA8; + rot.z = *(f32 *) &D_80035EAC; +#else rot = *((coord3d *) (&D_80035EA4)); +#endif casing->floor_y_pos = floor_y_pos; if (((((weaponid == ITEM_WPPK) || (weaponid == ITEM_WPPKSIL)) || (weaponid == ITEM_TT33)) || (weaponid == ITEM_SILVERWPPK)) || (weaponid == ITEM_GOLDWPPK)) @@ -5663,7 +5676,16 @@ void sub_GAME_7F068508(GUNHAND handnum, f32 floor_y_pos) return; } +#ifdef PORT + /* D491: N64 reads rot across the adjacent globals D_80035EA4/A8/AC (gun.c, + * all zero); mingw lays them out in reverse, so y/z came from + * g_ZeroTriggerState. Read the named globals. Layout only (porting-notes D5). */ + rot.x = *(f32 *) &D_80035EA4; + rot.y = *(f32 *) &D_80035EA8; + rot.z = *(f32 *) &D_80035EAC; +#else rot = *((coord3d *) (&D_80035EA4)); +#endif casing->floor_y_pos = floor_y_pos; if (((((weaponid == ITEM_WPPK) || (weaponid == ITEM_WPPKSIL)) || (weaponid == ITEM_TT33)) || (weaponid == ITEM_SILVERWPPK)) || (weaponid == ITEM_GOLDWPPK)) diff --git a/src/game/image.c b/src/game/image.c index e321b123..3b3702a7 100644 --- a/src/game/image.c +++ b/src/game/image.c @@ -2571,14 +2571,6 @@ void texLoad(s32 *updateword, struct texpool *pool) if (TRUE) { -#ifdef PORT - /* TEMP D66: catch oversized texLoad ROM reads. */ - if (getenv("GE_D63")) - osSyncPrintf("D66 texLoad texnum=%d thisoff=%08x nextoff=%08x size=%08x compbuf=%p\n", - g_TexNumToLoad, thisoffset, nextoffset, - (unsigned)(((u32)(nextoffset - thisoffset) + 0x1f) >> 4 << 4), - (void *)alignedcompbuffer); -#endif // Copy the compressed texture to RAM romCopy(alignedcompbuffer, (u32) &_imagesSegmentRomStart + (thisoffset & 0xfffffff8), diff --git a/src/game/image_bank.c b/src/game/image_bank.c index 8b067191..b979844e 100644 --- a/src/game/image_bank.c +++ b/src/game/image_bank.c @@ -96,7 +96,11 @@ extern u8* _GlobalimagetableSegmentRomStart; void texSetBitstring(s32 pos) { +#ifdef PORT + img_curpos = (u8 *)(uintptr_t)(u32)(pos); /* D441: zero-extend s32-held DRAM ptr */ +#else img_curpos = pos; +#endif img_curdatatable = 0; img_bitcount = 0; } diff --git a/src/game/initactorpropstuff.c b/src/game/initactorpropstuff.c index b67c52be..dc4c3e64 100644 --- a/src/game/initactorpropstuff.c +++ b/src/game/initactorpropstuff.c @@ -190,7 +190,11 @@ s32 initResolveAnimTable(struct StruckAnim *entries) count++; entry++; ptr_animation_table_addr = (struct StruckAnim *)(&ptr_animation_table); +#ifdef PORT + entry[-1].struck_anim = (ModelAnimation *)(uintptr_t)(u32)((*((s32 *)entries)) + (0, address)); /* D441: zero-extend s32-held DRAM ptr */ +#else entry[-1].struck_anim = (ModelAnimation *)((*((s32 *)entries)) + (0, address)); +#endif } while (entry->struck_anim != 0); } diff --git a/src/game/initanitable.c b/src/game/initanitable.c index edf2fd19..7295fcb6 100644 --- a/src/game/initanitable.c +++ b/src/game/initanitable.c @@ -256,15 +256,27 @@ void expand_ani_table_entries(s32** arg0) while (*var_v0 != 0) { if (*var_v0 != 1) { *var_v0 = (s32)((s32)*var_v0 + (s32)(&ptr_animation_table->data)); +#ifdef PORT + ((struct anim_entry *)(uintptr_t)(u32)(*var_v0))->unk08 += (s32)&ptr_animation_table->data; /* D441: zero-extend s32-held DRAM ptr */ +#else ((struct anim_entry *)*var_v0)->unk08 += (s32)&ptr_animation_table->data; +#endif +#ifdef PORT + ((struct anim_entry *)(uintptr_t)(u32)(*var_v0))->unk10 += (s32)&ptr_animation_table->data; /* D441: zero-extend s32-held DRAM ptr */ +#else ((struct anim_entry *)*var_v0)->unk10 += (s32)&ptr_animation_table->data; +#endif } var_v0++; } for (var_v0 = (s32 *)arg0; *var_v0 != 0; var_v0++) { if (*var_v0 != 1) { +#ifdef PORT + *(s32 *)(uintptr_t)(u32)(*var_v0) += (s32)&_animation_entriesSegmentRomStart; /* D441: zero-extend s32-held DRAM ptr */ +#else *(s32 *)*var_v0 += (s32)&_animation_entriesSegmentRomStart; +#endif } } } diff --git a/src/game/language.c b/src/game/language.c index daa75ad3..979cd8af 100644 --- a/src/game/language.c +++ b/src/game/language.c @@ -432,20 +432,6 @@ u8 * langGet(s32 slotID) if ((uintptr_t)textbank_ptr < 0x10000 || (uintptr_t)textbank_ptr >= 0x400000000ULL) { return NULL; } -#endif -#ifdef PORT - /* TEMP D65: log NULL-bank derefs (cast screen langGet crash). */ - if (GE_ENVFLAG("GE_D63")) { - static int d65first = 1; - if (d65first) { - d65first = 0; - for (int b = 0; b < 45; b++) - if (g_LangBanks[b]) - osSyncPrintf("D65 bank %d = %p\n", b, (void *)g_LangBanks[b]); - } - if (!textbank_ptr) - osSyncPrintf("D65 langGet slotID=0x%08x bank=%d ptr=NULL\n", (unsigned)slotID, slotID >> 10); - } #endif u32 textslot_offset = textbank_ptr[slotID & 0x03FF]; /* load the textbank ptr table then get the slot's offset */ diff --git a/src/game/lv.c b/src/game/lv.c index 28ed76e4..31a62db0 100644 --- a/src/game/lv.c +++ b/src/game/lv.c @@ -397,7 +397,7 @@ void lvlStageLoad(s32 stage) * Not compiled without PORT; no effect unless the env var is set. */ { const char *sm = getenv("GE_STARTMENU"); - /* D416 harness: GE_STARTMP="[,]" starts a local + /* D416 harness: GE_STARTMP="[,[,]]" starts a local * multiplayer match with the menu's own default options (what * MP options -> START does), so split-screen can be tested with no * one at the menu. Needs GE_MPVIRT= for the controller count. */ @@ -421,6 +421,13 @@ void lvlStageLoad(s32 stage) selected_folder_num = -1; init_mp_options_for_scenario(np); if (comma) MP_stage_selected = (s32)strtol(comma + 1, NULL, 0); + /* optional third field: scenario (MPSCENARIOS, e.g. 7 = 2v1) */ + if (comma && strchr(comma + 1, ',')) { + extern void reset_mp_options_for_scenario(MPSCENARIOS scenarioid); + extern s32 scenario; + reset_mp_options_for_scenario((MPSCENARIOS)strtol(strchr(comma + 1, ',') + 1, NULL, 0)); + osSyncPrintf("GE_STARTMP: scenario=%d\n", scenario); + } selected_stage = multi_stage_setups[MP_stage_selected].stage_id; briefingpage = -1; prev_keypresses = TRUE; diff --git a/src/game/model.c b/src/game/model.c index 3fd8d76f..9d64fcac 100644 --- a/src/game/model.c +++ b/src/game/model.c @@ -124,15 +124,6 @@ Model *modelmgrInstantiateModel(ModelFileHeader *header) if (model != NULL) { -#ifdef PORT /* TEMP D51 */ - if (getenv("GE_D51")) { - static FILE *f = NULL; - if (!f) { f = fopen("d52rw.log", "a"); setvbuf(f, NULL, _IONBF, 0); } - fprintf(f, "INST model=%p header=%p numRecords=%d rwdata=%p rwdatalen=%d lvreset=%d\n", - (void *)model, (void *)header, header->numRecords, - (void *)rwdata, rwdatalen, g_ModelIsLvResetting); - } -#endif modelInit(model, header, rwdata); ((struct ModelSlot *)model)->unk02 = rwdatalen; } @@ -225,33 +216,11 @@ Model *modelmgrInstantiateModelWithAnim(ModelFileHeader *modelFileHeader) rwdatalen = requiredRwdatalen; } } -#endif -#ifdef PORT /* TEMP D56: log slot-scan outcome incl. failure */ - if (getenv("GE_D56")) { - fprintf(stderr, "[D56] animInstantiate header=%p numRecords=%d maxslots=%d newModel=%p\n", - (void *)modelFileHeader, modelFileHeader->numRecords, - g_MaxAnimModelSlots, (void *)newModel); - if (newModel == NULL) { - for (i2 = 0; i2 < g_MaxAnimModelSlots; i2++) - fprintf(stderr, "[D56] slot%d unk08=%d unk10=%p unk02=%d\n", i2, - g_AnimModelSlots[i2].unk08, g_AnimModelSlots[i2].unk10, - g_AnimModelSlots[i2].unk02); - } - } #endif } if (newModel != NULL) { -#ifdef PORT /* TEMP D51 */ - if (getenv("GE_D51")) { - static FILE *f = NULL; - if (!f) { f = fopen("d52rw.log", "a"); setvbuf(f, NULL, _IONBF, 0); } - fprintf(f, "INSTA model=%p header=%p numRecords=%d rwdatas=%p rwdatalen=%d lvreset=%d\n", - (void *)newModel, (void *)modelFileHeader, modelFileHeader->numRecords, - (void *)rwdatas, rwdatalen, g_ModelIsLvResetting); - } -#endif animInit(newModel, modelFileHeader, rwdatas); newModel->rwdatalen = rwdatalen; } @@ -543,15 +512,6 @@ union ModelRwData* modelGetNodeRwData(Model *Objinst, ModelNode *root) } #ifdef PORT - /* TEMP D51: trace rwdata pool addressing */ - if (GE_ENVFLAG("GE_D51")) { - static FILE *f = NULL; - if (!f) { f = fopen("d52rw.log", "a"); setvbuf(f, NULL, _IONBF, 0); } - fprintf(f, "GND obj=%p datas=%p idx=%d rwdatalen=%d op=%d data=%p res=%p\n", - (void *)Objinst, (void *)data, index, Objinst->rwdatalen, - root->Opcode & 0xff, (void *)data, - (void *)&data[index]); - } return (union ModelRwData *)&data[index]; #else return &data[index]; @@ -828,14 +788,6 @@ void setsubroty(Model *model, f32 angle) void modelSetScale(Model *objinst, f32 scale) { -#ifdef PORT /* TEMP D56: identify the crashing caller */ - static int d56count = 0; - if (getenv("GE_D56")) { - fprintf(stderr, "[D56] modelSetScale #%d objinst=%p scale=%.4f ret=%p\n", - d56count++, (void *)objinst, scale, - __builtin_return_address(0)); - } -#endif objinst->scale = scale; } @@ -4568,13 +4520,6 @@ void modelRenderNodeGundl(ModelRenderData* renderdata, ModelNode* arg1) modelApplyRenderModeType2(renderdata); } -#if defined(PORT) - /* TEMP D63: log Primary/Secondary before emit (env GE_D63=1) */ - if (GE_ENVFLAG("GE_D63") && ((renderdata->flags & 1) && rodata->Primary)) - osSyncPrintf("D63 modelRenderNodeGundl Primary=%p Secondary=%p BaseAddr=%p ModelType=%d\n", - (void *)rodata->Primary, (void *)rodata->Secondary, - (void *)rodata->BaseAddr, (int)rodata->ModelType); -#endif gSPDisplayList(renderdata->gdl++, rodata->Primary); if ((rodata->ModelType == 3) && rodata->Secondary) @@ -6574,13 +6519,6 @@ void modelInitRwData(Model *model, ModelNode *startnode) while (node) { -#if defined(PORT) /* TEMP D86: trace node walk to catch the bad pointer before deref */ - if (getenv("GE_D86")) { - fprintf(stderr, "[D86] modelInitRwData model=%p header=%p node=%p\n", - (void *)model, (void *)(model ? model->obj : NULL), (void *)node); - fflush(stderr); - } -#endif u32 type = node->Opcode & 0xFF; switch (type) diff --git a/src/game/mpmenu.c b/src/game/mpmenu.c index 642cd7cc..dab71709 100644 --- a/src/game/mpmenu.c +++ b/src/game/mpmenu.c @@ -1424,7 +1424,11 @@ Gfx *mp_watch_menu_display(Gfx *gdl) #else h2 = (s32) langGet(getStringID(LMPMENU, MPMENU_STR_1D_KILLS)), /* KILLS */ #endif +#ifdef PORT + sprintf(rankbuffer, ascii_pnum_KILLS, (char *)(uintptr_t)(u32)(q), curplayernum + 1, (char *) h2); /* -> "P KILLS" */ /* D441: zero-extend s32-held DRAM ptr */ +#else sprintf(rankbuffer, ascii_pnum_KILLS, (char *) q, curplayernum + 1, (char *) h2); /* -> "P KILLS" */ +#endif textMeasure(&textheight, &textwidth, rankbuffer, ptrFontBankGothicChars, ptrFontBankGothic, 0); x = ((viGetViewLeft() + two_player_x_offset) - (textwidth >> 1)) + 80; @@ -1498,7 +1502,11 @@ Gfx *mp_watch_menu_display(Gfx *gdl) #else h2 = (s32) langGet(getStringID(LMPMENU, MPMENU_STR_1E_LOSSES)), /* LOSSES */ #endif +#ifdef PORT + sprintf(rankbuffer, ascii_pnum_LOSSES, (char *)(uintptr_t)(u32)(q), curplayernum + 1, (char *) h2); /* -> "P LOSSES" */ /* D441: zero-extend s32-held DRAM ptr */ +#else sprintf(rankbuffer, ascii_pnum_LOSSES, (char *) q, curplayernum + 1, (char *) h2); /* -> "P LOSSES" */ +#endif textMeasure(&textheight, &textwidth, rankbuffer, ptrFontBankGothicChars, ptrFontBankGothic, 0); x = ((viGetViewLeft() + two_player_x_offset) - (textwidth >> 1)) + 80; diff --git a/src/game/ob.c b/src/game/ob.c index 2483def4..2139af7d 100644 --- a/src/game/ob.c +++ b/src/game/ob.c @@ -1,8 +1,5 @@ #include #ifdef PORT -/* TEMP D69: probe below uses fprintf/getenv. */ -#include -#include #include "pcmodels.h" /* D50: PC-layout model sidecars */ #include "pccg.h" /* D69: PC-layout bg/stan sidecars */ #endif @@ -160,15 +157,6 @@ void obLoadBGFileBytesAtOffset(u8 *bgname, u8 *target, s32 offset, s32 len) index = fileGetIndex(bgname); fileentry = &file_resource_table[index]; -#ifdef PORT - /* TEMP D69: trace BG-file loads (env GE_D69=1). */ - if (getenv("GE_D69")) - fprintf(stderr, "D69 obLoadBGFile %s idx=%d rom_size=0x%X hw=0x%08X off=0x%X len=0x%X\n", - (const char *)bgname, index, - (unsigned)resource_lookup_data_array[index].rom_size, - (unsigned)(u32)fileentry->hw_address, (unsigned)offset, (unsigned)len); -#endif - if (resource_lookup_data_array[index].rom_size != 0) { //if the size of offset data would exceed file size, loop forever @@ -222,15 +210,6 @@ void obLoadBGFileBytesAtOffset(u8 *bgname, u8 *target, s32 offset, s32 len) index = fileGetIndex(bgname); fileentry = &file_resource_table[index]; -#ifdef PORT - /* TEMP D69: trace BG-file loads (env GE_D69=1). */ - if (getenv("GE_D69")) - fprintf(stderr, "D69 obLoadBGFile %s idx=%d rom_size=0x%X hw=0x%08X off=0x%X len=0x%X\n", - (const char *)bgname, index, - (unsigned)resource_lookup_data_array[index].rom_size, - (unsigned)(u32)fileentry->hw_address, (unsigned)offset, (unsigned)len); -#endif - if (resource_lookup_data_array[index].rom_size != 0) { //if the size of offset data would exceed file size, loop forever diff --git a/src/game/objecthandler_2.c b/src/game/objecthandler_2.c index ae8cb4a4..e6278a6c 100644 --- a/src/game/objecthandler_2.c +++ b/src/game/objecthandler_2.c @@ -49,14 +49,22 @@ void sub_GAME_7F0762E0(ModelFileHeader *objheader, u8 *name, u8 *dst, struct tex if (gdl != 0) { +#ifdef PORT + name = (u8 *)(uintptr_t)(u32)((pcremaining - ((s32) (((u8 *) objheader->Switches) + (((u32) gdl) & 0x00ffffff)))) + ((s32) filedata)); /* D441: zero-extend s32-held DRAM ptr */ +#else name = (u8 *) ((pcremaining - ((s32) (((u8 *) objheader->Switches) + (((u32) gdl) & 0x00ffffff)))) + ((s32) filedata)); +#endif /* The signed lvalue cast is required for the compiler to choose the target registers. */ replacementgdl = (u32)*(s32 *)&gdl; delta = ((s32) ((romremaining + filedata) - (s32) name)) - ((s32) (((u8 *) objheader->Switches) + (((u32) gdl) & 0x00ffffff))); +#ifdef PORT + texCopyGdls((Gfx *) (((u8 *) objheader->Switches) + (((u32) gdl) & 0x00ffffff)), (Gfx *)(uintptr_t)(u32)((romremaining + filedata) - (s32) name), (s32) name); /* D441: zero-extend s32-held DRAM ptr */ +#else texCopyGdls((Gfx *) (((u8 *) objheader->Switches) + (((u32) gdl) & 0x00ffffff)), (Gfx *) ((romremaining + filedata) - (s32) name), (s32) name); +#endif texLoadFromModelFileHeader(objheader, buffer); @@ -70,11 +78,19 @@ void sub_GAME_7F0762E0(ModelFileHeader *objheader, u8 *name, u8 *dst, struct tex if (gdl != 0) { +#ifdef PORT + name = (u8 *)(uintptr_t)(u32)(((s32) gdl) - ((s32) curgdl)); /* D441: zero-extend s32-held DRAM ptr */ +#else name = (u8 *) (((s32) gdl) - ((s32) curgdl)); +#endif } else { +#ifdef PORT + name = (u8 *)(uintptr_t)(u32)(((((s32) (filedata + pcremaining)) - ((s32) objheader->Switches)) - (((u32) curgdl) & 0x00ffffff))); /* D441: zero-extend s32-held DRAM ptr */ +#else name = (u8 *) ((((s32) (filedata + pcremaining)) - ((s32) objheader->Switches)) - (((u32) curgdl) & 0x00ffffff)); +#endif } modelNodeReplaceGdl((u32) objheader, curnode, curgdl, (Gfx *) replacementgdl); @@ -84,9 +100,17 @@ void sub_GAME_7F0762E0(ModelFileHeader *objheader, u8 *name, u8 *dst, struct tex while (node != 0); } +#ifdef PORT + name = (u8 *)(uintptr_t)(u32)(((s32) (((u8 *) objheader->Switches) + (replacementgdl & 0x00ffffff))) - filedata); /* D441: zero-extend s32-held DRAM ptr */ +#else name = (u8 *) (((s32) (((u8 *) objheader->Switches) + (replacementgdl & 0x00ffffff))) - filedata); +#endif +#ifdef PORT + fileSetSize(filenum, (u8 *)(uintptr_t)(u32)(filedata), (((s32) name + 0xf) & (~0xf)), dst == 0); /* D441: zero-extend s32-held DRAM ptr */ +#else fileSetSize(filenum, (u8 *) filedata, (((s32) name + 0xf) & (~0xf)), dst == 0); +#endif } } @@ -139,14 +163,6 @@ void load_object_fill_header(struct ModelFileHeader *objheader, u8 *name, u8* ds objheader->RootNode = (struct ModelNode *)&objheader->Textures[objheader->numtextures]; -#if defined(PORT) /* TEMP D86: correlate header identity with the model name at load time */ - if (getenv("GE_D86")) { - fprintf(stderr, "[D86] load_object_fill_header name=%s objheader=%p filedata=%p RootNode=%p numSwitches=%d numtextures=%d\n", - (const char *)name, (void *)objheader, (void *)filedata, (void *)objheader->RootNode, - objheader->numSwitches, objheader->numtextures); - fflush(stderr); - } -#endif #ifdef PORT sub_GAME_7F075A90(objheader, 0x5000000, (uintptr_t)filedata); #else diff --git a/src/game/prop.c b/src/game/prop.c index 392d719c..936b0530 100644 --- a/src/game/prop.c +++ b/src/game/prop.c @@ -1474,14 +1474,6 @@ void proplvreset2(enum LEVELID stageId) pad->pos.f[1] *= roompos_1; pad->pos.f[2] *= roompos_1; -#if defined(PORT) && !defined(DEBUG) - if (getenv("GE_D88")) { - fprintf(stderr, "D88 pad idx=%d plink=%p str=%s pos=%f,%f,%f\n", - (s32)(pad - g_CurrentSetup.pads), (void *)pad->plink, - pad->plink ? pad->plink : "(null)", - pad->pos.f[0], pad->pos.f[1], pad->pos.f[2]); - } -#endif #ifdef DEBUG { @@ -1498,15 +1490,6 @@ void proplvreset2(enum LEVELID stageId) #else init_pathtable_something(pad, pad->plink, &pad->stan); #endif -#ifdef PORT - if (getenv("GE_D90")) { - fprintf(stderr, "D90 pad idx=%d plink=%s pos=%.1f,%.1f,%.1f stan=%p%s\n", - (s32)(pad - g_CurrentSetup.pads), - pad->plink ? (char *)pad->plink : "(null)", - pad->pos.f[0], pad->pos.f[1], pad->pos.f[2], - (void *)pad->stan, pad->stan ? "" : " <-- NULL"); - } -#endif if (1); } diff --git a/src/game/propobj.c b/src/game/propobj.c index 1b9d2f55..d40d1197 100644 --- a/src/game/propobj.c +++ b/src/game/propobj.c @@ -5741,7 +5741,11 @@ s32 objTick(struct PropRecord *prop) #endif temp_s0_6 = render_pad2F4->model; +#ifdef PORT + if (temp_s0_6->anim == (ModelAnimation *)(uintptr_t)(u32)(animation_table_ptrs2[1])) /* D32/D33 */ /* D441: zero-extend s32-held DRAM ptr */ +#else if (temp_s0_6->anim == (ModelAnimation *)animation_table_ptrs2[1]) /* D32/D33 */ +#endif { modelSetAnimTranslationScale(temp_s0_6, 10.438f); setsubroty(render_pad2F4->model, M_PI_F); @@ -10436,6 +10440,14 @@ void generate_language_specific_text_for_weapon(u8 *finalstring, ITEM_IDS itemty morethan2players = FALSE; +#ifdef AVOID_UB + /* D496: with 3+ players the non-JP path below never writes finalstring + * before strcat'ing onto it, and the caller's buffer is an uninitialised + * stack array, so leftover stack bytes were drawn as the pickup message + * (garbled, oversized weapon-pickup text in 3P/4P split-screen). */ + finalstring[0] = '\0'; +#endif + if (j_text_trigger != 0) { strcpy(finalstring,""); diff --git a/src/game/ramromreplay.c b/src/game/ramromreplay.c index adfb7b9e..8fd4183f 100644 --- a/src/game/ramromreplay.c +++ b/src/game/ramromreplay.c @@ -296,14 +296,6 @@ s32 ramrom_replay_handler(struct contsample *arg0, s32 arg1) temp_a2 = (s32) ptr_active_demofile->size_cmds; temp_t2 = ramrom_blkbuf_2->count; -#if defined(PORT) /* TEMP D87: trace consumer before the crash-site deref */ - if (getenv("GE_D87")) { - fprintf(stderr, "[D87] replay_handler: ramrom_blkbuf_3=%p temp_a2=%d temp_t2=%d arg1=%d\n", - (void *)ramrom_blkbuf_3, temp_a2, temp_t2, arg1); - fflush(stderr); - } -#endif - for (var_a3 = 0; var_a3 < temp_t2; var_a3++) { arg1 = (s32) (arg1 + 1) % CONTSAMPLE_LEN; @@ -363,17 +355,6 @@ void iterate_ramrom_entries_handle_camera_out(void) ramrom_blkbuf_2 = romCopyAligned(ramrom_data_target + 0x1F8, address_demo_loaded, sizeof(struct ramrom_seed)); -#if defined(PORT) /* TEMP D87: trace ramrom block setup */ - if (getenv("GE_D87")) { - fprintf(stderr, "[D87] iterate: address_demo_loaded=%p ramrom_blkbuf_2=%p count=%d speedframes=%d size_cmds=%d\n", - (void *)address_demo_loaded, (void *)ramrom_blkbuf_2, - ramrom_blkbuf_2 ? ramrom_blkbuf_2->count : -1, - ramrom_blkbuf_2 ? ramrom_blkbuf_2->speedframes : -1, - ptr_active_demofile ? ptr_active_demofile->size_cmds : -1); - fflush(stderr); - } -#endif - var_a3 = ramrom_blkbuf_2->count; if (var_a3 > 0) { @@ -381,13 +362,6 @@ void iterate_ramrom_entries_handle_camera_out(void) ramrom_data_target + 0x21E, address_demo_loaded + 4, ptr_active_demofile->size_cmds * sizeof(struct ramrom_blockbuf) * ramrom_blkbuf_2->count); -#if defined(PORT) /* TEMP D87 */ - if (getenv("GE_D87")) { - fprintf(stderr, "[D87] iterate: ramrom_blkbuf_3=%p len=%d\n", (void *)ramrom_blkbuf_3, - (int)(ptr_active_demofile->size_cmds * sizeof(struct ramrom_blockbuf) * ramrom_blkbuf_2->count)); - fflush(stderr); - } -#endif } var_a3 = ramrom_blkbuf_2->count; diff --git a/src/game/rsp.c b/src/game/rsp.c index 4eab881a..398e522e 100644 --- a/src/game/rsp.c +++ b/src/game/rsp.c @@ -247,8 +247,16 @@ void rspGfxTaskStart(Gfx *firstGdl, Gfx *gdl, s32 arg2, OSMesg rspReplyMsg) // task->ucode_data_size = SP_UCODE_DATA_SIZE, set in bss constructor // task->dram_stack = &g_gfxDramStack, set in bss constructor // task->dram_stack_size = SP_DRAM_STACK_SIZE8, set in bss constructor +#ifdef PORT + task->t.output_buff = (u64 *)(uintptr_t)(u32)(g_gfxRdpOutputBufferStart); /* D441: zero-extend s32-held DRAM ptr */ +#else task->t.output_buff = (u64 *)g_gfxRdpOutputBufferStart; +#endif +#ifdef PORT + task->t.output_buff_size = (u64 *)(uintptr_t)(u32)(g_gfxRdpOutputBufferEnd); /* D441: zero-extend s32-held DRAM ptr */ +#else task->t.output_buff_size = (u64 *)g_gfxRdpOutputBufferEnd; +#endif task->t.data_ptr = (u64 *) firstGdl; task->t.data_size = (s32) ((gdl - firstGdl) * sizeof (Gfx)); // u64 pointers, generates sra t3,t2,0x3 task->t.yield_data_ptr = (u64 *)&g_gfxYieldBuf; diff --git a/src/game/stan.c b/src/game/stan.c index 5254eeaf..6f13e32a 100644 --- a/src/game/stan.c +++ b/src/game/stan.c @@ -277,30 +277,9 @@ void stanBuildRoomData(void) tile = stan_prefix->ptr_firstroom; -#if defined(PORT) - /* TEMP D69: log the tile walk (env GE_D69STAN=1) */ - if (getenv("GE_D69STAN")) { - osSyncPrintf("D69stan start tile=%p firstroom_raw=%p\n", - (void *)tile, (void *)stan_prefix->ptr_firstroom); - } -#endif while (*(u32 *)tile) { -#if defined(PORT) - /* TEMP D69: log the tile walk (env GE_D69STAN=1) */ - if (getenv("GE_D69STAN")) { - static s32 s_n = 0; - s32 pc = tile->tail.hdrTail.pointCount & 0xf; - if (s_n < 20 || pc == 0 || list_of_tilesizes[pc] == 0) - osSyncPrintf("D69stan n=%d tile=%p room=%d mid=%04x " - "tail=%04x pc=%d sz=%d\n", - (int)s_n, (void *)tile, tile->room, - (u16)tile->mid.half, (u16)tile->tail.half, - (int)pc, (int)list_of_tilesizes[pc]); - s_n++; - } -#endif if (tile->room != lastRoom) { lastRoom = tile->room; @@ -3150,37 +3129,6 @@ struct StandTilePoint *stanMatchTileName(char *id) tile = stan_prefix->ptr_firstroom; -#ifdef PORT - if (getenv("GE_D88")) { - s32 iters = 0; - StandTilePoint *t0 = tile; - fprintf(stderr, "D88 stanMatchTileName id=%s hi=%04x lo=%02x stan_prefix=%p firstroom=%p\n", - id, stanIdHi, stanIdLo, (void *)stan_prefix, (void *)tile); - while (*(u32 *)tile != 0) { - if ((u16)tile->x == stanIdHi) { - if (*((u8 *)&tile->y) == stanIdLo) { - fprintf(stderr, "D88 matched after %d iters tile=%p\n", iters, (void *)tile); - return tile; - } - } - tmp = tile->link; - tile = (StandTilePoint *)((u8 *)tile + - list_of_tilesizes[(tmp >> 12) & 0xf]); - iters++; - if (iters < 20 || (iters % 5000) == 0) { - fprintf(stderr, "D88 iter=%d tile=%p tmp(link)=%04x stride=%d delta_from_start=%lld\n", - iters, (void *)tile, (u16)tmp, list_of_tilesizes[(tmp >> 12) & 0xf], - (long long)((u8 *)tile - (u8 *)t0)); - } - if (iters > 200000) { - fprintf(stderr, "D88 ABORT runaway walk\n"); - return NULL; - } - } - fprintf(stderr, "D88 no match, %d iters\n", iters); - return NULL; - } -#endif while (*(u32 *)tile != 0) { if ((u16)tile->x == stanIdHi) { diff --git a/src/game/textrelated.c b/src/game/textrelated.c index 46c26352..87cc055b 100644 --- a/src/game/textrelated.c +++ b/src/game/textrelated.c @@ -259,28 +259,6 @@ Gfx *textRenderGlyph(Gfx *gdl, s32 *x, s32 *y, struct fontchar *curchar, struct gDPSetTileSize(gdl++, G_TX_RENDERTILE, 0, 0, (curchar->unkc - 1) << 2, (curchar->height - 1) << 2); */ gDPLoadTextureBlock(gdl++, curchar->pixeldata, G_IM_FMT_I, G_IM_SIZ_8b, ((curchar->width + 7) & 0xF8), curchar->height, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, 0, 0); -#ifdef PORT - { - static int ge_d116 = -1; - if (ge_d116 < 0) ge_d116 = getenv("GE_D116") ? 1 : 0; - if (ge_d116) { - const unsigned char *pd = (const unsigned char *)curchar->pixeldata; - int c = curchar->index; - fprintf(stderr, - "[D116] glyph '%c' idx=%d w=%d loadw=%d h=%d *x=%d s=%d t=%d pd=%p row0:", - (c >= 32 && c < 127) ? c : '?', c, curchar->width, - (curchar->width + 7) & 0xF8, curchar->height, - (int)*x, (int)text_s, (int)text_t, (void *)pd); - if (pd) { - int n = ((curchar->width + 7) & 0xF8) * 2; - int i; - if (n > 32) n = 32; - for (i = 0; i < n; i++) fprintf(stderr, " %02x", pd[i]); - } - fprintf(stderr, "\n"); - } - } -#endif } else { @@ -535,28 +513,6 @@ Gfx *textRenderGlyphOutlined(Gfx *gdl, s32 *x, s32 *y, struct fontchar *curchar, if (curchar->index < 0x80) { gDPSetTextureLUT(gdl++, G_TT_NONE); gDPLoadTextureBlock(gdl++, curchar->pixeldata, G_IM_FMT_I, G_IM_SIZ_8b, ((curchar->width + 7) & 0xF8), curchar->height, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, 0, 0); -#ifdef PORT - { - static int ge_d116 = -1; - if (ge_d116 < 0) ge_d116 = getenv("GE_D116") ? 1 : 0; - if (ge_d116) { - const unsigned char *pd = (const unsigned char *)curchar->pixeldata; - int c = curchar->index; - int loadw = (curchar->width + 7) & 0xF8; - int r; - fprintf(stderr, "[D116] O-glyph '%c' idx=%d w=%d loadw=%d h=%d *x=%d pd=%p\n", - (c >= 32 && c < 127) ? c : '?', c, curchar->width, loadw, - curchar->height, (int)*x, (void *)pd); - if (pd) for (r = 0; r < curchar->height && r < 16; r++) { - int i; - fprintf(stderr, " row%2d:", r); - for (i = 0; i < loadw && i < 24; i++) - fprintf(stderr, " %02x", pd[r * loadw + i]); - fprintf(stderr, "\n"); - } - } - } -#endif } else { gDPPipeSync(gdl++); gDPSetTextureLUT(gdl++, G_TT_IA16); diff --git a/src/game/title.c b/src/game/title.c index 2c038ece..ce673e6c 100644 --- a/src/game/title.c +++ b/src/game/title.c @@ -492,7 +492,11 @@ void initializeGunBarrelIntro(u8 *gfxBuffer, s32 bufferSize) bufferSize -= 0x200; gfxBuffer += 0x200; +#ifdef PORT + createGunbarrelRenderHole((struct s_display_list_something *)(uintptr_t)(u32)(barrelDisplayListPtr), 0x1E); /* D441: zero-extend s32-held DRAM ptr */ +#else createGunbarrelRenderHole(barrelDisplayListPtr, 0x1E); +#endif gunbarrelgfxListPointer = (Gfx*)gfxBuffer; #ifdef PORT @@ -632,14 +636,6 @@ void clearChrGunModelInstances(void) * Address: 0x7F009254 */ Gfx *renderGunbarrelEyeIntroSequence (Gfx *gdl) { -#ifdef PORT - if (GE_ENVFLAG("GE_D63")) { - static int n = 0; - if ((++n % 200) == 1) - osSyncPrintf("D63 gb-render call #%d mode=%d slot=%08x\n", - n, (int)gunbarrel_mode, *(const u32 *)0x7012EC38); - } -#endif D_8002A7D0 = (1 - D_8002A7D0); switch (gunbarrel_mode - 2) { diff --git a/src/mema.c b/src/mema.c index e50583ac..3b0f634d 100644 --- a/src/mema.c +++ b/src/mema.c @@ -314,7 +314,11 @@ void *memaAlloc(u32 amount) { best->addr = 0; } +#ifdef PORT + return (void*)(uintptr_t)(u32)(addr); /* D441: zero-extend s32-held DRAM ptr */ +#else return (void*)addr; +#endif } // Find the memaspace of the given address and reduce its size by the given @@ -569,7 +573,11 @@ s32 memaRealloc(s32 addr, u32 oldsize, u32 newsize) if ((oldsize > newsize)) { +#ifdef PORT + memaFree((void *)(uintptr_t)(u32)(addr + newsize), oldsize - newsize); /* D441: zero-extend s32-held DRAM ptr */ +#else memaFree(addr + newsize, oldsize - newsize); +#endif } return 1; diff --git a/tools_pc/abi_baseline.json b/tools_pc/abi_baseline.json new file mode 100644 index 00000000..68268318 --- /dev/null +++ b/tools_pc/abi_baseline.json @@ -0,0 +1,202 @@ +{ + "assets/animationtable_data.h": { + "-Wint-conversion": 25 + }, + "port/shim/PR/os.h": { + "-Wint-conversion": 3, + "-Wint-to-pointer-cast": 2, + "-Wpointer-to-int-cast": 1 + }, + "port/src/romdata.c": { + "-Wpointer-to-int-cast": 7 + }, + "src/audi.c": { + "-Wint-conversion": 1, + "-Wint-to-pointer-cast": 1, + "-Wpointer-to-int-cast": 4 + }, + "src/boss.c": { + "-Wpointer-to-int-cast": 1 + }, + "src/debugmenu.c": { + "-Wpointer-to-int-cast": 2 + }, + "src/fr.c": { + "-Wint-conversion": 2 + }, + "src/game/bg.c": { + "-Wint-conversion": 3, + "-Wpointer-to-int-cast": 6 + }, + "src/game/blood_decrypt.c": { + "-Wint-conversion": 2, + "-Wpointer-to-int-cast": 2 + }, + "src/game/bondview2.c": { + "-Wint-conversion": 1, + "-Wint-to-pointer-cast": 2, + "-Wpointer-to-int-cast": 8 + }, + "src/game/bondview_r.c": { + "-Wpointer-to-int-cast": 1 + }, + "src/game/chr.c": { + "-Wint-conversion": 1 + }, + "src/game/chr_b.c": { + "-Wint-conversion": 1 + }, + "src/game/chraction.c": { + "-Wpointer-to-int-cast": 118 + }, + "src/game/chrai.c": { + "-Wint-conversion": 2 + }, + "src/game/cleanup_objects.c": { + "-Wint-conversion": 1, + "-Wpointer-to-int-cast": 1 + }, + "src/game/glass.c": { + "-Wpointer-to-int-cast": 2 + }, + "src/game/gunfire.c": { + "-Wint-conversion": 2 + }, + "src/game/image.c": { + "-Wint-conversion": 3, + "-Wpointer-to-int-cast": 3 + }, + "src/game/image_bank.c": { + "-Wpointer-to-int-cast": 2 + }, + "src/game/indy_commands.c": { + "-Wint-conversion": 2, + "-Wpointer-to-int-cast": 4 + }, + "src/game/indy_comms.c": { + "-Wint-conversion": 1, + "-Wpointer-to-int-cast": 1 + }, + "src/game/initBondDATAdefaults.c": { + "-Wpointer-to-int-cast": 1 + }, + "src/game/initactorpropstuff.c": { + "-Wint-conversion": 1, + "-Wpointer-to-int-cast": 1 + }, + "src/game/initanitable.c": { + "-Wpointer-to-int-cast": 6 + }, + "src/game/initmenus.c": { + "-Wint-conversion": 1 + }, + "src/game/initunk_0009E0.c": { + "-Wint-conversion": 1 + }, + "src/game/language.c": { + "-Wint-conversion": 2, + "-Wpointer-to-int-cast": 2 + }, + "src/game/lightfixture.c": { + "-Wint-conversion": 1, + "-Wpointer-to-int-cast": 5 + }, + "src/game/lv.c": { + "-Wint-conversion": 2, + "-Wpointer-to-int-cast": 2 + }, + "src/game/model.c": { + "-Wint-conversion": 9, + "-Wint-to-pointer-cast": 3, + "-Wpointer-to-int-cast": 3 + }, + "src/game/mpmenu.c": { + "-Wpointer-to-int-cast": 2 + }, + "src/game/objecthandler_2.c": { + "-Wint-to-pointer-cast": 1, + "-Wpointer-to-int-cast": 18 + }, + "src/game/othermodemicrocode.c": { + "-Wint-conversion": 1 + }, + "src/game/prop.c": { + "-Wint-conversion": 2, + "-Wpointer-to-int-cast": 21 + }, + "src/game/propobj.c": { + "-Wint-conversion": 1, + "-Wpointer-to-int-cast": 9 + }, + "src/game/ramromreplay.c": { + "-Wpointer-to-int-cast": 2 + }, + "src/game/ramromreplay.h": { + "-Wint-conversion": 1 + }, + "src/game/rsp.c": { + "-Wint-conversion": 1, + "-Wint-to-pointer-cast": 1, + "-Wpointer-to-int-cast": 2 + }, + "src/game/stan.c": { + "-Wint-conversion": 3, + "-Wpointer-to-int-cast": 1 + }, + "src/game/tex.c": { + "-Wpointer-to-int-cast": 2 + }, + "src/game/textrelated.c": { + "-Wpointer-to-int-cast": 4 + }, + "src/game/title.c": { + "-Wint-conversion": 6, + "-Wint-to-pointer-cast": 2, + "-Wpointer-to-int-cast": 7 + }, + "src/game/viewport.c": { + "-Wint-conversion": 1 + }, + "src/game/vtxstore.c": { + "-Wpointer-to-int-cast": 2 + }, + "src/init.c": { + "-Wint-conversion": 6, + "-Wint-to-pointer-cast": 3, + "-Wpointer-to-int-cast": 2 + }, + "src/libultra/audio/heapinit.c": { + "-Wpointer-to-int-cast": 1 + }, + "src/libultra/audio/load.c": { + "-Wpointer-to-int-cast": 7 + }, + "src/libultra/audio/resample.c": { + "-Wpointer-to-int-cast": 1 + }, + "src/libultra/audio/save.c": { + "-Wpointer-to-int-cast": 1 + }, + "src/libultrare/audio/env.c": { + "-Wint-to-pointer-cast": 2 + }, + "src/libultrare/libc/xprintf.c": { + "-Wpointer-to-int-cast": 1 + }, + "src/macro.h": { + "-Wpointer-to-int-cast": 1 + }, + "src/music.c": { + "-Wint-to-pointer-cast": 5, + "-Wpointer-to-int-cast": 6 + }, + "src/ramrom.c": { + "-Wint-conversion": 2 + }, + "src/sched.c": { + "-Wpointer-to-int-cast": 2 + }, + "src/snd.c": { + "-Wpointer-to-int-cast": 1 + } +} diff --git a/tools_pc/abi_ratchet.py b/tools_pc/abi_ratchet.py new file mode 100644 index 00000000..2313c6e4 --- /dev/null +++ b/tools_pc/abi_ratchet.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +"""D441 P4: ABI warning ratchet for CI. + +Re-runs every .c/.cpp entry of BUILD_DIR/compile_commands.json with +-c -> -fsyntax-only, -o removed, -Werror dropped, and +-Wpointer-to-int-cast -Wint-to-pointer-cast -Wint-conversion -Wno-error +appended (same method as scratch/d441/collect.py), deduped the same way +on (file, line, col, flag, msg). Counts per (repo-relative file, flag) +and compares against the baseline JSON {"file": {"flag": count}}: + + python tools_pc/abi_ratchet.py build-pc # fail if any count rises + python tools_pc/abi_ratchet.py build-pc --update # rewrite the baseline + python tools_pc/abi_ratchet.py build-pc --baseline other.json + +Exit codes: 0 = OK (every count flat or lower), 1 = ratchet violation +(a count rose, or a new file/flag appeared with count > 0), 2 = usage or +environment error (missing compile_commands.json / baseline). Stdlib only. +""" +import argparse +import json +import os +import re +import shlex +import subprocess +import sys +import time +from concurrent.futures import ThreadPoolExecutor + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +ROOT_FW = ROOT.replace('\\', '/') +DEFAULT_BASELINE = os.path.join(HERE, 'abi_baseline.json') + +FLAGS = ['-Wpointer-to-int-cast', '-Wint-to-pointer-cast', '-Wint-conversion'] +FLAG_RE = re.compile( + r'^(?P.+?):(?P\d+):(?P\d+): warning: (?P.*?) ' + r'\[(?P-W(?:pointer-to-int-cast|int-to-pointer-cast|int-conversion))\]\s*$') + +if os.name == 'nt': + ENV = dict(os.environ) + ENV['PATH'] = r'C:\msys64\mingw64\bin;' + ENV.get('PATH', '') + if not ENV.get('ComSpec'): + ENV['ComSpec'] = r'C:\Windows\system32\cmd.exe' + if not ENV.get('PATHEXT'): + ENV['PATHEXT'] = '.COM;.EXE;.BAT;.CMD' + if not ENV.get('TMP'): + ENV['TMP'] = ENV.get('TEMP', os.environ.get('SystemRoot', r'C:\Windows')) + r'\Temp' + ENV['TEMP'] = ENV['TMP'] +else: + ENV = None # inherit + + +def transform(entry): + """-o removed, -c -> -fsyntax-only, -Werror dropped, ratchet flags appended.""" + toks = shlex.split(entry['command'], posix=False) + new, skip = [], False + for t in toks: + if skip: + skip = False + continue + if t == '-o': + skip = True + continue + if t == '-c': + new.append('-fsyntax-only') + continue + if t == '-Werror' or t.startswith('-Werror='): + continue + new.append(t) + new += FLAGS + ['-Wno-error'] + return new + + +def norm_path(p, tu_file, cwd): + """Repo-relative forward-slash path (same rules as collect.py).""" + p = p.replace('\\', '/') + if p.startswith(ROOT_FW + '/'): + return p[len(ROOT_FW) + 1:] + if os.path.isabs(p): + return p + bases = [] + for b in (os.path.dirname(tu_file), cwd): + if b not in bases: + bases.append(b) + fallback = None + for b in bases: + cand = os.path.normpath(os.path.join(b, p)).replace('\\', '/') + if not cand.startswith(ROOT_FW + '/'): + continue + rel = cand[len(ROOT_FW) + 1:] + if fallback is None: + fallback = rel + if os.path.exists(os.path.join(ROOT, *rel.split('/'))): + return rel + return fallback if fallback is not None else p + + +def run_tu(entry): + """Returns (tu_file, rows, error); rows are (file, line, col, flag, msg).""" + tu = entry['file'] + cwd = entry.get('directory', ROOT) + cmd = transform(entry) + env = ENV + if env is not None and os.path.isabs(cmd[0]): + # Put the compiler's own bin dir first so cc1 loads its matching DLLs. + # On GitHub-hosted runners setup-msys2 installs under D:\a\_temp while + # the image also ships an unrelated C:\msys64. + env = dict(env) + env['PATH'] = os.path.dirname(cmd[0]) + ';' + env['PATH'] + try: + p = subprocess.run(cmd, cwd=cwd, capture_output=True, + text=True, env=env, timeout=600) + except Exception as ex: # noqa: BLE001 - report and continue + return tu, [], repr(ex) + rows = [] + for line in (p.stderr or '').splitlines(): + m = FLAG_RE.match(line) + if m: + rows.append((norm_path(m.group('path'), tu, cwd), + int(m.group('line')), int(m.group('col')), + m.group('flag'), m.group('msg'))) + err = 'rc=%d' % p.returncode if p.returncode != 0 else None + return tu, rows, err + + +def collect(build_dir): + cc_json = os.path.join(build_dir, 'compile_commands.json') + if not os.path.isfile(cc_json): + print('ERROR: %s not found — run ./build-pc.sh ntsc-final first ' + '(CMake exports compile_commands.json at configure time).' + % cc_json, file=sys.stderr) + sys.exit(2) + db = json.load(open(cc_json, encoding='utf-8')) + entries = [e for e in db if e['file'].endswith(('.c', '.cpp'))] + print('compile_commands entries: %d total, %d .c/.cpp selected' + % (len(db), len(entries))) + + t0 = time.time() + results = [] + with ThreadPoolExecutor(max_workers=4) as ex: + for r in ex.map(run_tu, entries): + results.append(r) + dt = time.time() - t0 + + seen = set() + errors = [] + for tu, rows, err in results: + if err: + errors.append((tu, err)) + seen.update(rows) + + counts = {} + for (f, _ln, _col, flag, _msg) in seen: + counts.setdefault(f, {}) + counts[f][flag] = counts[f].get(flag, 0) + 1 + + print('elapsed: %.1fs' % dt) + if errors: + print('TU ERRORS (%d):' % len(errors), file=sys.stderr) + for tu, err in errors: + try: + rel = os.path.relpath(tu, ROOT) + except ValueError: + rel = tu + print(' %s %s' % (rel, err), file=sys.stderr) + sys.exit(2) + return counts + + +def main(): + ap = argparse.ArgumentParser(description='ABI warning ratchet (D441)') + ap.add_argument('build_dir', help='directory containing compile_commands.json') + ap.add_argument('--baseline', default=DEFAULT_BASELINE, + help='baseline JSON path (default: %s)' % DEFAULT_BASELINE) + ap.add_argument('--update', action='store_true', + help='rewrite the baseline from the current tree and exit 0') + args = ap.parse_args() + + counts = collect(args.build_dir) + total_now = sum(c for fl in counts.values() for c in fl.values()) + + if args.update: + with open(args.baseline, 'w', encoding='utf-8', newline='\n') as fh: + json.dump(counts, fh, indent=2, sort_keys=True) + fh.write('\n') + print('baseline updated: %s (%d files, %d warnings)' + % (args.baseline, len(counts), total_now)) + return + + if not os.path.isfile(args.baseline): + print('ERROR: baseline %s not found — run with --update first.' + % args.baseline, file=sys.stderr) + sys.exit(2) + base = json.load(open(args.baseline, encoding='utf-8')) + total_base = sum(c for fl in base.values() for c in fl.values()) + + violations = [] + lowered = 0 + for f in sorted(set(counts) | set(base)): + for flag in sorted(set(counts.get(f, {})) | set(base.get(f, {}))): + old = base.get(f, {}).get(flag, 0) + new = counts.get(f, {}).get(flag, 0) + if new > old: + violations.append((f, flag, old, new)) + elif new < old: + lowered += 1 + + if violations: + print('ABI RATCHET VIOLATION: %d file/flag count(s) rose' % len(violations)) + for f, flag, old, new in violations: + print(' %s %s: %d -> %d' % (f, flag, old, new)) + sys.exit(1) + + if lowered: + print('abi ratchet OK: %d warnings now vs %d baseline ' + '(%d file/flag count(s) lower; run --update to ratchet down)' + % (total_now, total_base, lowered)) + else: + print('abi ratchet OK: %d warnings now vs %d baseline' + % (total_now, total_base)) + + +if __name__ == '__main__': + main() diff --git a/tools_pc/abpad/README.md b/tools_pc/abpad/README.md new file mode 100644 index 00000000..7a3364cc --- /dev/null +++ b/tools_pc/abpad/README.md @@ -0,0 +1,130 @@ +# abpad — 1964 vs PC port A/B input harness + +One millisecond timeline drives both sides of a faithful-vs-port comparison: + +- **1964** reads a ViGEmBus virtual Xbox 360 pad (`vgamepad`) through NRage + 2.3c in XInput mode. abpad reads NRage's own mapping file and presses + whatever Xbox input your profile maps to each N64 input, so you can keep + your normal play profile. +- **The port** is launched with the same timeline converted to its own + `GE_INPUTSCRIPT` (exact N64 bits, works unfocused, reproducible). It does + not read the virtual pad. Its default pad layout (Jinx 1.1) maps buttons + by context: in a level Y = N64 A and pad A = N64 B, while in menus A/X = + N64 A. A single NRage mapping can't follow that switch. + +Runs are **not frame-identical**: 1964 has no seed pin or frame dump, and the +port's script counts controller reads, not milliseconds. Use it to compare +timing and feel (fire rates, AI reactions, menus), not pixels. + +## One-time setup + +1. ViGEmBus 1.22 installed (it is: Sunshine's driver). `pip install vgamepad` + is done. Note that a plain `pip install vgamepad` runs its bundled + ViGEmBus 1.17 MSI if it doesn't recognise the installed bus. On + 2026-10-02 that downgraded the bus and had to be reverted. +2. 1964 → Plugins → Input = `NRage_Input_V2.dll` (2.3c), Controller 1 with + **XInput** ticked. Any button layout works. abpad inverts + `plugin/XInput Controller 1 Config.xcc` (`--nrage-dir`, default the + `ge-port-reference` 1964 copy) and errors if a timeline needs an N64 + input that the profile leaves unmapped. File format, from NRage's + `XInputController.cpp` ([libertyernie/nrage-input](https://github.com/libertyernie/nrage-input)): + - Button keys (`A=`, `Z=`, `DUp=`, ...) are XInput button bitmasks. + - `LeftTrigger=`/`RightTrigger=` are N64 button codes (Z = 0x20, + L = 0x2000, ...). + - Each `*Axis=` is either the N64 analog axis (0x4000 X, 0x8000 Y) or + `(positive-direction code << 16) | negative-direction code` for a + stick used as C or D buttons. +3. **The virtual pad must be XInput slot 0.** NRage reads N64 controller 1 + from XInput slot 0 only, and a real pad that is already connected holds + it. Switch the real pad off, or unplug it, before a run. abpad probes + slot 0 at startup and refuses to run otherwise. +4. Stick range is handled automatically. The port's script deflects the + stick to ±80. abpad computes the thumb value that NRage turns into 80 + after its `PadDeadZone` (21248 at the default 5%); `--stick` changes the + target. +5. `1964.cfg`: `PauseWhenInactive 0` (set 2026-10-02). Only one window + has focus, and 1964 pauses when it's in the background. + +## Timeline + +`:[,...]` entries separated by `;` or newlines; `#` starts a +comment. Times are from harness start. + +| Tokens | Meaning | +|---|---| +| `A B Z START L R UP DOWN LEFT RIGHT CUP CDOWN CLEFT CRIGHT` | 50 ms press (the port's `INPUTSCRIPT_PULSE_MS`) | +| `SUP SDOWN SLEFT SRIGHT` (combinable) / `SNONE` | analog stick, held until changed / recentre | +| `ZHOLD` / `ZREL` | hold / release fire | +| `RHOLD` / `LHOLD` / `RREL` | hold R or L (one aim hold) / release | + +Port-only `GE_INPUTSCRIPT` tokens (`MDX`/`MDY`, `CHOLD`, `UHOLD`, +`RELOADHOLD`, ...) are rejected. The port caps a script at 64 entries. + +## Running + +The full side-by-side run (PowerShell; the real pad must be off): + +```powershell +tools_pc\abpad\run_ab.ps1 -Timeline "19000:START;25000:SRIGHT;25300:SNONE;27000:SLEFT;27300:SNONE;29000:A" ` + -QuitFrame 2400 -Seconds 36 -Step 0.5 +python tools_pc\abpad\sheet.py $env:TEMP\abpad-run 16 36 1 # side-by-side contact sheet +``` + +`run_ab.ps1` starts abpad (which launches the port), then 1964 with +`-c NRage_Input_V2.dll -g ` 1.2 s later, and screenshots both windows +every `-Step` seconds on one clock (`capture.ps1`, DPI-aware PrintWindow, so +overlapping windows are fine). 1964's `-g` takes a file name inside its +configured ROM directory and can't contain spaces, so keep a copy such as +`GoldenEyeUSA.n64` there. The port exits by itself through `GE_QUITFRAME`; +the script closes 1964 normally. + +abpad on its own: + +```sh +python tools_pc/abpad/abpad.py t.txt --print-script # the port's GE_INPUTSCRIPT +python tools_pc/abpad/abpad.py t.txt --quitframe 2400 # both sides +python tools_pc/abpad/abpad.py t.txt --no-port # 1964 only +python tools_pc/abpad/abpad.py t.txt --no-pad # port only +``` + +- **Port timing:** abpad sets `GE_INPUTSCRIPT_MS=1`, so the port reads entry + times as milliseconds since its SDL init instead of controller reads. Read + rates vary through the intro and menus, so read counts can't be lined up + with 1964. `--reads-per-ms` converts to a read-count script instead, for + exes that don't have the millisecond mode. +- **1964 lags by ~2 s** (measured 2026-10-02): the 1.2 s launch gap plus + 1964's slower startup. `run_ab.ps1` defaults `-PadOffset 2000`, which + delivers each input at the same game state on both sides. The screenshots + then show 1964 about 2 s behind the port throughout. +- **Menus use the analog stick** (`SUP`/`SDOWN`/`SLEFT`/`SRIGHT` + `SNONE`), + not the D-pad. +- The port is `build-pc/ge007.x86_64.exe`, launched from the repo root, so + it uses the repo's `data/` (`SkipIntro = 0`). `build-pc/data` has the + maintainer's play settings and skips the intro. + +Verified 2026-10-02: START during the gun-barrel dots skipped to the title, +file select came up, stick right and left moved the folder cursor, and A +opened the folder. Both sides went through the same sequence. + +## Checks before trusting a run + +1. With `--no-port` and a long `0:ZHOLD` timeline, NRage's XInput dialog + (or 1964 firing in a level) should show the profile's Z input held. +2. The port side needs no pad check. `GE_INPUTSCRIPT` is the port's sole + controller-0 source when set, focused or not. +3. The two sides use different save files (the port's `data/ge007.eep`, + 1964's `save/`), so file-select contents can differ. + +## Live A/B (one real pad) + +`live_ab.ps1` launches 1964 GEPD and the port side by side at 1280x720 each (1964: GLideN64 windowed size; port: `data/ge007.ini` [Window]; the script only moves windows, never resizes), driven by ONE +real gamepad: the port reads it unfocused (`GE_PAD_BACKGROUND=1`), 1964 gets +it on click. 1964 starts ~0.8 s slower, so it launches first and the port +follows after `-PortDelayMs` (tune if the intros drift). + +``` +powershell -ExecutionPolicy Bypass -File tools_pc\abpad\live_ab.ps1 [-PortDelayMs 800] [-NoArrange] +``` + +The real pad must be ON (no virtual pad), and the NRage profile should match +the port's pad layout. `-DryRun` prints the launch lines without launching. diff --git a/tools_pc/abpad/abpad.py b/tools_pc/abpad/abpad.py new file mode 100644 index 00000000..a9b6cc02 --- /dev/null +++ b/tools_pc/abpad/abpad.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python3 +"""A/B fidelity harness: one millisecond timeline drives 1964 and the PC port. + +1964 gets the input from a ViGEmBus virtual Xbox 360 pad (vgamepad), read by +NRage 2.3c in XInput mode. The harness reads NRage's own mapping file +(`plugin/XInput Controller 1 Config.xcc`) and presses whatever Xbox input +that profile maps to each N64 input, so any profile works. The port does +NOT read that pad: its default Jinx 1.1 pad layout is context-dependent (in a +level Y = N64 A, pad A = N64 B; in menus A/X = N64 A), so no single NRage +mapping could line up with it. The same timeline is converted to the port's +own GE_INPUTSCRIPT (exact N64 bits, focus-independent) and the port is +launched with it. + +Timeline: `:[,...];...` (also one entry per line; `#` comments). +Tokens are the subset of GE_INPUTSCRIPT that 1964 can reproduce: + buttons (pulsed 50 ms = the port's INPUTSCRIPT_PULSE_MS): + A B Z START L R UP DOWN LEFT RIGHT CUP CDOWN CLEFT CRIGHT + analog stick (sustained until changed): SUP SDOWN SLEFT SRIGHT SNONE + holds: ZHOLD ZREL (fire), RHOLD LHOLD RREL (one aim hold: R or L), + CUPHOLD CDOWNHOLD CLEFTHOLD CRIGHTHOLD (combinable) / CNONE (C buttons) +Port-only tokens (MDX/MDY, CHOLD/CREL, UHOLD/UREL, RELOADHOLD/RELOADREL) +are rejected: 1964 has no equivalent. + +Examples: + python tools_pc/abpad/abpad.py t.txt --print-script + python tools_pc/abpad/abpad.py t.txt --quitframe 3600 + python tools_pc/abpad/abpad.py "2000:START;4000:A" --no-port +""" +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +import time +from dataclasses import dataclass, field +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent.parent +DEFAULT_NRAGE = ROOT.parent / "ge-port-reference" / "1964_GEPD_Edition" / "1964" / "plugin" + +PULSE_MS = 50 # port: INPUTSCRIPT_PULSE_MS (GE_INPUTSCRIPT_MS mode) +PORT_SCRIPT_MAX = 64 # port: INPUTSCRIPT_MAX +TICK_HZ = 240 # pad update loop rate + +BUTTONS = ("A", "B", "Z", "START", "L", "R", "UP", "DOWN", "LEFT", "RIGHT", + "CUP", "CDOWN", "CLEFT", "CRIGHT") +STICK = {"SUP": (0, 1), "SDOWN": (0, -1), "SLEFT": (-1, 0), "SRIGHT": (1, 0)} +HOLDS = ("ZHOLD", "ZREL", "RHOLD", "LHOLD", "RREL") +# C-button holds (the port forces 1.2 SOLITARE: C-up/down walk, C-left/right strafe) +CHOLDS = {"CUPHOLD": "CUP", "CDOWNHOLD": "CDOWN", "CLEFTHOLD": "CLEFT", "CRIGHTHOLD": "CRIGHT"} +PORT_ONLY = ("CHOLD", "CREL", "UHOLD", "UREL", "RELOADHOLD", "RELOADREL") + + +@dataclass +class Entry: + ms: int + toks: list[str] + buttons: set[str] = field(default_factory=set) + stick: tuple[int, int] | None = None # (x, y) in -1..1, None = unchanged + zhold: bool | None = None + aim: int | None = None # 0 none, 1 R, 2 L + chold: frozenset | None = None # held C buttons, None = unchanged + + +def parse_timeline(text: str) -> list[Entry]: + entries: list[Entry] = [] + raw = [] + for line in text.splitlines(): + line = line.split("#", 1)[0] + raw.extend(p for p in line.split(";") if p.strip()) + for part in raw: + if ":" not in part: + raise SystemExit(f"abpad: entry without ':' -> {part.strip()!r}") + ms_s, toks_s = part.split(":", 1) + try: + ms = int(ms_s.strip()) + except ValueError: + raise SystemExit(f"abpad: bad ms {ms_s.strip()!r}") + toks = [t.strip().upper() for t in toks_s.split(",") if t.strip()] + e = Entry(ms, toks) + sx = sy = 0 + has_stick = False + for t in toks: + if t in BUTTONS: + e.buttons.add(t) + elif t in STICK: + dx, dy = STICK[t] + sx, sy = sx or dx, sy or dy + has_stick = True + elif t == "SNONE": + has_stick = True + elif t == "ZHOLD": + e.zhold = True + elif t == "ZREL": + e.zhold = False + elif t in CHOLDS: + e.chold = (e.chold or frozenset()) | {CHOLDS[t]} + elif t == "CNONE": + e.chold = frozenset() + elif t in ("RHOLD", "LHOLD", "RREL"): + e.aim = {"RHOLD": 1, "LHOLD": 2, "RREL": 0}[t] + elif t in PORT_ONLY or t.startswith(("MDX", "MDY")): + raise SystemExit(f"abpad: {t} is port-only (no 1964 equivalent)") + else: + raise SystemExit(f"abpad: unknown token {t!r}") + if has_stick: + e.stick = (sx, sy) + entries.append(e) + entries.sort(key=lambda e: e.ms) + return entries + + +def to_inputscript(entries: list[Entry], reads_per_ms: float | None, offset_ms: int) -> str: + """reads_per_ms None = GE_INPUTSCRIPT_MS mode (times stay in ms).""" + if len(entries) > PORT_SCRIPT_MAX: + raise SystemExit(f"abpad: {len(entries)} entries > port limit {PORT_SCRIPT_MAX}") + out, last = [], -1 + for e in entries: + t = e.ms + offset_ms + frame = max(0, round(t if reads_per_ms is None else t * reads_per_ms)) + if frame <= last: # two entries collapsed onto one read + frame = last + 1 + last = frame + out.append(f"{frame}:{','.join(e.toks)}") + return ";".join(out) + + +def pad_state(entries: list[Entry], t_ms: float): + """(buttons, stick, zhold, aim, chold) at t_ms, mirroring the port's script semantics.""" + buttons: set[str] = set() + stick, zhold, aim, chold = (0, 0), False, 0, frozenset() + for e in entries: + if e.ms > t_ms: + break + if t_ms < e.ms + PULSE_MS: + buttons |= e.buttons + if e.stick is not None: + stick = e.stick + if e.zhold is not None: + zhold = e.zhold + if e.aim is not None: + aim = e.aim + if e.chold is not None: + chold = e.chold + return buttons, stick, zhold, aim, chold + + +# NRage 2.3c XInputController.h N64_BUTTONS codes (libertyernie/nrage-input). +N64_CODE = {"A": 0x0080, "B": 0x0040, "Z": 0x0020, "R": 0x1000, "L": 0x2000, + "START": 0x0010, "UP": 0x0008, "DOWN": 0x0004, "LEFT": 0x0002, + "RIGHT": 0x0001, "CUP": 0x0800, "CDOWN": 0x0400, "CLEFT": 0x0200, + "CRIGHT": 0x0100} +N64_XAXIS, N64_YAXIS = 0x4000, 0x8000 +XCC_BUTTON_KEY = {"A": "A", "B": "B", "Z": "Z", "R": "R", "L": "L", "START": "Start", + "UP": "DUp", "DOWN": "DDown", "LEFT": "DLeft", "RIGHT": "DRight", + "CUP": "CUp", "CDOWN": "CDown", "CLEFT": "CLeft", "CRIGHT": "CRight"} +XCC_AXES = {"LeftXAxis": ("L", 0), "LeftYAxis": ("L", 1), + "RightXAxis": ("R", 0), "RightYAxis": ("R", 1)} + + +def load_nrage(plugin_dir: Path): + """Invert NRage's XInput mapping: N64 input -> the Xbox input that makes it. + + Returns (sources, stick_axes, deadzone_pct). sources[name] is one of + ("btn", xusb_bit), ("trig", "L"|"R"), ("axis", "L"|"R", axis, +1|-1). + stick_axes = {0: (stick, axis) driving N64 X, 1: ... N64 Y}. + """ + xcc = plugin_dir / "XInput Controller 1 Config.xcc" + if not xcc.is_file(): + raise SystemExit(f"abpad: no NRage XInput config at {xcc} (--nrage-dir)") + cfg = {} + for line in xcc.read_text(encoding="ascii", errors="replace").splitlines(): + if "=" in line: + k, v = line.split("=", 1) + cfg[k.strip()] = int(v.strip() or 0) + sources, stick_axes = {}, {} + for name, code in N64_CODE.items(): + mask = cfg.get(XCC_BUTTON_KEY[name], 0) + if mask: + sources[name] = ("btn", mask & -mask) # lowest bound Xbox button + elif cfg.get("LeftTrigger") == code: + sources[name] = ("trig", "L") + elif cfg.get("RightTrigger") == code: + sources[name] = ("trig", "R") + else: + for key, (stick, axis) in XCC_AXES.items(): + v = cfg.get(key, 0) + if v in (N64_XAXIS, N64_YAXIS): + continue + if (v >> 16) & code: # positive deflection + sources[name] = ("axis", stick, axis, +1) + break + if v & 0xFFFF & code: # negative deflection + sources[name] = ("axis", stick, axis, -1) + break + for key, (stick, axis) in XCC_AXES.items(): + if cfg.get(key) == N64_XAXIS: + stick_axes.setdefault(0, (stick, axis)) + elif cfg.get(key) == N64_YAXIS: + stick_axes.setdefault(1, (stick, axis)) + deadzone = 0 + ini = plugin_dir / "NRage.ini" + if ini.is_file(): + sect = None + for line in ini.read_text(encoding="ascii", errors="replace").splitlines(): + line = line.strip() + if line.startswith("["): + sect = line + elif sect == "[Controller 1]" and line.startswith("PadDeadZone="): + deadzone = int(line.split("=", 1)[1]) + return sources, stick_axes, deadzone + + +def nrage_stick_value(n64: int, deadzone_pct: int) -> int: + """Smallest XInput thumb value NRage turns into n64 (AxisDeadzone, then *127/32767).""" + if n64 <= 0: + return 0 + dz = deadzone_pct * 32767 // 100 + rel = 32767.0 / (32767 - dz) if dz else 1.0 + for raw in range(1, 32768): + v = raw + if dz: + v = 0 if raw < dz else min(32767.0, (raw - dz) * rel) + if int(v) * 127 // 32767 >= n64: + return raw + return 32767 + + +def xinput_slot0_rx() -> int | None: + import ctypes + + class GP(ctypes.Structure): + _fields_ = [("w", ctypes.c_ushort), ("lt", ctypes.c_ubyte), ("rt", ctypes.c_ubyte), + ("lx", ctypes.c_short), ("ly", ctypes.c_short), + ("rx", ctypes.c_short), ("ry", ctypes.c_short)] + + class ST(ctypes.Structure): + _fields_ = [("pkt", ctypes.c_uint), ("gp", GP)] + + st = ST() + if ctypes.WinDLL("xinput1_4").XInputGetState(0, ctypes.byref(st)) != 0: + return None + return st.gp.rx + + +def run_pad(entries: list[Entry], t0: float, offset_ms: int, linger_ms: int, + plugin_dir: Path, stick_n64: int) -> None: + import vgamepad as vg # imported late so --print-script works without it + + sources, stick_axes, deadzone = load_nrage(plugin_dir) + used = set() + for e in entries: + used |= e.buttons + if e.zhold: + used.add("Z") + if e.aim: + used.add("R" if e.aim == 1 else "L") + if e.chold: + used |= e.chold + missing = sorted(n for n in used if n not in sources) + if missing: + raise SystemExit(f"abpad: the NRage profile maps nothing to {', '.join(missing)}") + if any(e.stick not in (None, (0, 0)) for e in entries) and len(stick_axes) < 2: + raise SystemExit("abpad: the NRage profile maps no Xbox stick to the N64 analog stick") + full = nrage_stick_value(stick_n64, deadzone) + + pad = vg.VX360Gamepad() + # NRage reads N64 controller 1 from XInput slot 0. Probe with an + # odd right-stick X value inside NRage's deadzone (no N64 effect) to + # confirm the virtual pad got slot 0 rather than a real pad. + probe = 777 + # A new pad takes ~0.3 s to enumerate and drops reports sent before that + # (it then reads back uninitialised stick values), so keep re-sending. + deadline = time.perf_counter() + 3.0 + slot0 = None + while time.perf_counter() < deadline and slot0 != probe: + pad.right_joystick(x_value=probe, y_value=0) + pad.update() + time.sleep(0.05) + slot0 = xinput_slot0_rx() + pad.reset() + pad.update() + if slot0 != probe: + del pad + raise SystemExit("abpad: the virtual pad is not XInput slot 0 (a real pad holds it). " + "NRage reads N64 controller 1 from slot 0: disconnect the real pad " + "(or start abpad before plugging it in) and retry.") + end_ms = (entries[-1].ms + PULSE_MS if entries else 0) + linger_ms + prev, last_send = None, 0.0 + try: + while True: + t_ms = (time.perf_counter() - t0) * 1000.0 - offset_ms + if t_ms > end_ms: + break + buttons, stick, zhold, aim, chold = pad_state(entries, t_ms) + state = (frozenset(buttons), stick, zhold, aim, chold) + now = time.perf_counter() + if state != prev or now - last_send > 0.1: # periodic resend: dropped reports + held = set(buttons) | chold + if zhold: + held.add("Z") + if aim: + held.add("R" if aim == 1 else "L") + wbuttons, trig = 0, {"L": 0, "R": 0} + axes = {("L", 0): 0, ("L", 1): 0, ("R", 0): 0, ("R", 1): 0} + for i, d in enumerate(stick): + if d: + axes[stick_axes[i]] = d * full + for n in held: + src = sources[n] + if src[0] == "btn": + wbuttons |= src[1] + elif src[0] == "trig": + trig[src[1]] = 255 + else: + axes[(src[1], src[2])] = src[3] * 32767 + pad.report.wButtons = wbuttons + pad.left_trigger(value=trig["L"]) + pad.right_trigger(value=trig["R"]) + pad.left_joystick(x_value=axes[("L", 0)], y_value=axes[("L", 1)]) + pad.right_joystick(x_value=axes[("R", 0)], y_value=axes[("R", 1)]) + pad.update() + prev, last_send = state, now + time.sleep(1.0 / TICK_HZ) + finally: + pad.reset() + pad.update() + del pad + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + ap.add_argument("timeline", help="timeline file, or an inline timeline string") + ap.add_argument("--print-script", action="store_true", + help="print the port GE_INPUTSCRIPT and exit") + ap.add_argument("--no-port", action="store_true", help="drive the pad (1964) only") + ap.add_argument("--no-pad", action="store_true", help="launch the port only") + ap.add_argument("--port-exe", default=str(ROOT / "build-pc" / "ge007.x86_64.exe")) + ap.add_argument("--port-cwd", default=None, help="default: the exe's directory") + ap.add_argument("--port-offset-ms", type=int, default=0, + help="shift the port's timeline (e.g. to absorb a boot-time difference)") + ap.add_argument("--pad-offset-ms", type=int, default=0, + help="shift the 1964 pad timeline") + ap.add_argument("--reads-per-ms", type=float, default=None, + help="convert to the port's read-count script instead of GE_INPUTSCRIPT_MS " + "(older exes; ~0.12 in a level, lower in the intro)") + ap.add_argument("--quitframe", type=int, default=None, + help="GE_QUITFRAME for the port (always set one for unattended runs)") + ap.add_argument("--nrage-dir", default=str(DEFAULT_NRAGE), + help="1964 plugin dir holding NRage.ini and 'XInput Controller 1 Config.xcc'") + ap.add_argument("--stick", type=int, default=80, + help="N64 stick deflection for SUP/SDOWN/SLEFT/SRIGHT (the port script uses 80)") + ap.add_argument("--linger-ms", type=int, default=1000, + help="keep the virtual pad plugged this long after the last entry") + args = ap.parse_args() + + p = Path(args.timeline) + text = p.read_text(encoding="utf-8") if p.is_file() else args.timeline + entries = parse_timeline(text) + if not entries: + raise SystemExit("abpad: empty timeline") + script = to_inputscript(entries, args.reads_per_ms, args.port_offset_ms) + if args.print_script: + print(script) + return 0 + + proc = None + t0 = time.perf_counter() + if not args.no_port: + env = dict(os.environ, GE_INPUTSCRIPT=script) + if args.reads_per_ms is None: + env["GE_INPUTSCRIPT_MS"] = "1" # port times are ms since its SDL init + if args.quitframe is not None: + env["GE_QUITFRAME"] = str(args.quitframe) + exe = Path(args.port_exe) + proc = subprocess.Popen([str(exe)], cwd=args.port_cwd or str(exe.parent), env=env) + print(f"abpad: port pid {proc.pid}, GE_INPUTSCRIPT={script}") + if not args.no_pad: + print(f"abpad: driving virtual pad, {len(entries)} entries, last at {entries[-1].ms} ms") + run_pad(entries, t0, args.pad_offset_ms, args.linger_ms, + Path(args.nrage_dir), args.stick) + print("abpad: pad timeline done") + if proc is not None: + return proc.wait() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools_pc/abpad/capture.ps1 b/tools_pc/abpad/capture.ps1 new file mode 100644 index 00000000..63ab404b --- /dev/null +++ b/tools_pc/abpad/capture.ps1 @@ -0,0 +1,45 @@ +param([string]$OutDir, [int]$Seconds = 24, [double]$Step = 1.0) +Add-Type -AssemblyName System.Drawing +Add-Type @" +using System; +using System.Runtime.InteropServices; +public class W { + [StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; } + [DllImport("user32.dll")] public static extern bool SetProcessDPIAware(); + [DllImport("user32.dll")] public static extern bool GetClientRect(IntPtr h, out RECT r); + [DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr h, IntPtr hdc, uint flags); +} +"@ +[W]::SetProcessDPIAware() | Out-Null +New-Item -ItemType Directory -Force -Path $OutDir | Out-Null +$t0 = Get-Date +$log = Join-Path $OutDir "cap.log" +"start $($t0.ToString('HH:mm:ss.fff'))" | Out-File $log -Encoding ascii +function Find-Win($name) { + $p = Get-Process $name -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne [IntPtr]::Zero } | Select-Object -First 1 + if ($p) { return $p.MainWindowHandle } else { return [IntPtr]::Zero } +} +function Snap($h, $path) { + if ($h -eq [IntPtr]::Zero) { return $false } + $r = New-Object W+RECT; [W]::GetClientRect($h, [ref]$r) | Out-Null + $w = $r.R - $r.L; $hh = $r.B - $r.T + if ($w -le 0 -or $hh -le 0) { return $false } + $bmp = New-Object System.Drawing.Bitmap $w, $hh + $g = [System.Drawing.Graphics]::FromImage($bmp) + $hdc = $g.GetHdc() + [W]::PrintWindow($h, $hdc, 3) | Out-Null # PW_CLIENTONLY | PW_RENDERFULLCONTENT + $g.ReleaseHdc($hdc) + $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) + $g.Dispose(); $bmp.Dispose() + return $true +} +$n = [int]([math]::Floor($Seconds / $Step)) +for ($i = 1; $i -le $n; $i++) { + $t = $i * $Step + while (((Get-Date) - $t0).TotalSeconds -lt $t) { Start-Sleep -Milliseconds 5 } + $tag = "{0:D5}" -f [int]($t * 1000) + $a = Snap (Find-Win '1964') (Join-Path $OutDir "1964_$tag.png") + $b = Snap (Find-Win 'ge007.x86_64') (Join-Path $OutDir "port_$tag.png") + "t=$t 1964=$a port=$b" | Out-File $log -Append -Encoding ascii +} +"done" | Out-File $log -Append -Encoding ascii diff --git a/tools_pc/abpad/live_ab.ps1 b/tools_pc/abpad/live_ab.ps1 new file mode 100644 index 00000000..7ecca3eb --- /dev/null +++ b/tools_pc/abpad/live_ab.ps1 @@ -0,0 +1,87 @@ +# Live side-by-side A/B: 1964 GEPD (left) and the GE007 PC port (right), both +# driven by ONE real gamepad. The port reads the pad even unfocused via +# GE_PAD_BACKGROUND=1; 1964 gets focus when you click it (PauseWhenInactive 0 +# is already set in 1964.cfg). +# +# 1964 starts ~0.8 s slower than the port (measured 2026-10-02: 1964 launched +# 1.2 s after the port ran ~2 s behind), so 1964 launches first and the port +# PortDelayMs later. Tune -PortDelayMs if the intros drift. +# +# The real pad must be ON (no virtual pad / abpad). The NRage profile should +# match the port's pad layout. Does not wait for the games to exit. +param([int]$PortDelayMs = 800, + [string]$Emu = (Join-Path $PSScriptRoot '..\..\..\ge-port-reference\1964_GEPD_Edition\1964'), + [string]$Rom = 'GoldenEyeUSA.n64', + [string]$PortExe = (Join-Path $PSScriptRoot '..\..\build-pc\ge007.x86_64.exe'), + [int]$HoldSec = 30, # keep re-applying the positions this long (both games move themselves at startup) + [switch]$NoArrange, [switch]$DryRun) + +$repo = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +$emu = (Resolve-Path $Emu).Path +$PortExe = (Resolve-Path $PortExe).Path +if (-not (Test-Path "$emu\1964.exe")) { Write-Error "1964 not found: $emu\1964.exe"; exit 1 } +if (-not (Test-Path $PortExe)) { Write-Error "port exe not found: $PortExe"; exit 1 } + +$emuLine = "Start-Process -FilePath `"$emu\1964.exe`" -ArgumentList @('-c','NRage_Input_V2.dll','-g','$Rom') -WorkingDirectory '$emu'" +$portLine = "Start-Process -FilePath '$PortExe' -WorkingDirectory '$repo' (env GE_PAD_BACKGROUND=1)" + +if ($DryRun) { + Write-Output "1964 : $emuLine" + Write-Output "port : $portLine" + Write-Output "delay: port launches ${PortDelayMs} ms after 1964" + Write-Output "arrange: $(if ($NoArrange) {'off'} else {'on'})" + exit 0 +} + +$emuProc = Start-Process -FilePath "$emu\1964.exe" -ArgumentList @('-c','NRage_Input_V2.dll','-g',$Rom) ` + -WorkingDirectory $emu -PassThru +Start-Sleep -Milliseconds $PortDelayMs +$env:GE_PAD_BACKGROUND = '1' +$portProc = Start-Process -FilePath $PortExe -WorkingDirectory $repo -PassThru + +# Wait up to 15 s (200 ms polls) for both windows to appear. +$deadline = (Get-Date).AddSeconds(15) +do { + $emuProc.Refresh(); $portProc.Refresh() + Start-Sleep -Milliseconds 200 +} while (((($emuProc.MainWindowHandle -eq 0) -or ($portProc.MainWindowHandle -eq 0)) -and (Get-Date) -lt $deadline)) +$emuProc.Refresh(); $portProc.Refresh() +if (($emuProc.MainWindowHandle -eq 0) -or ($portProc.MainWindowHandle -eq 0)) { + Write-Warning "one or both windows not up after 15 s; continuing" +} + +if (-not $NoArrange) { + Add-Type -AssemblyName System.Windows.Forms + Add-Type -Namespace Win32 -Name User32 -MemberDefinition @' +[StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; } +[DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr after, int X, int Y, int W, int H, uint flags); +[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT r); +'@ + # Position only, never resize: GLideN64 renders 1964 at its own windowed + # size (plugin/GLideN64.ini video\windowedWidth/Height = 1280x720) and draws + # into a corner if the window is resized; the port takes its size from + # data/ge007.ini [Window] (1280x720). 1964 goes top-left, the port to its + # right. Re-applied for HoldSec since both reposition themselves at startup. + $wa = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea + $SWP = 0x0001 -bor 0x0004 # NOSIZE | NOZORDER + $end = (Get-Date).AddSeconds($HoldSec) + Write-Output "READY: start pad input once both show the same screen (holding positions for $HoldSec s)." + while ((Get-Date) -lt $end) { + $emuProc.Refresh(); $portProc.Refresh() + $x = $wa.X + foreach ($p in @($emuProc, $portProc)) { + if ($p.HasExited -or $p.MainWindowHandle -eq 0) { continue } + $r = New-Object Win32.User32+RECT + [Win32.User32]::GetWindowRect($p.MainWindowHandle, [ref]$r) | Out-Null + if ($r.L -ne $x -or $r.T -ne $wa.Y) { + [Win32.User32]::SetWindowPos($p.MainWindowHandle, [IntPtr]::Zero, $x, $wa.Y, 0, 0, $SWP) | Out-Null + } + $x += ($r.R - $r.L) + } + Start-Sleep -Milliseconds 250 + } +} + +if ($NoArrange) { Write-Output "READY: start pad input once both show the same screen." } +Write-Output "1964 pid : $($emuProc.Id)" +Write-Output "port pid : $($portProc.Id)" diff --git a/tools_pc/abpad/run_ab.ps1 b/tools_pc/abpad/run_ab.ps1 new file mode 100644 index 00000000..c16d9a87 --- /dev/null +++ b/tools_pc/abpad/run_ab.ps1 @@ -0,0 +1,30 @@ +# Boot 1964 and the port together, drive both from one abpad timeline, and +# screenshot both windows on one clock (capture.ps1). Compare with sheet.py. +# Needs: the real pad off (abpad must own XInput slot 0), a port exe with +# GE_INPUTSCRIPT_MS, and a ROM name without spaces in 1964's ROM directory. +param([string]$Timeline, [int]$QuitFrame = 1500, [int]$Seconds = 24, [double]$Step = 1.0, + [int]$PadOffset = 2000, + [string]$Emu = (Join-Path $PSScriptRoot '..\..\..\ge-port-reference\1964_GEPD_Edition\1964'), + [string]$Rom = 'GoldenEyeUSA.n64', + [string]$Out = (Join-Path $env:TEMP 'abpad-run')) +$repo = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +$emu = (Resolve-Path $Emu).Path +Remove-Item -Recurse -Force $Out -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $Out | Out-Null +Remove-Item Env:GE_PCDUMP -ErrorAction SilentlyContinue +$ab = Start-Process -FilePath python -ArgumentList @("$repo\tools_pc\abpad\abpad.py", "`"$Timeline`"", + '--quitframe', "$QuitFrame", '--port-cwd', $repo, '--linger-ms', '6000', '--pad-offset-ms', "$PadOffset") ` + -WorkingDirectory $repo -PassThru -NoNewWindow ` + -RedirectStandardOutput "$Out\abpad.out" -RedirectStandardError "$Out\abpad.err" +Start-Sleep -Milliseconds 1200 # the virtual pad exists before NRage initialises +$emuProc = Start-Process -FilePath "$emu\1964.exe" -ArgumentList @('-c', 'NRage_Input_V2.dll', '-g', $Rom) ` + -WorkingDirectory $emu -PassThru +& powershell -NoProfile -File "$PSScriptRoot\capture.ps1" -OutDir $Out -Seconds $Seconds -Step $Step +$ab.WaitForExit(60000) | Out-Null +"abpad exit: $($ab.ExitCode)" | Out-File "$Out\run.log" -Encoding ascii +if (-not $emuProc.HasExited) { + $emuProc.CloseMainWindow() | Out-Null + if (-not $emuProc.WaitForExit(8000)) { "1964 did not close; left running" | Out-File "$Out\run.log" -Append -Encoding ascii } + else { "1964 closed" | Out-File "$Out\run.log" -Append -Encoding ascii } +} +"output: $Out" | Write-Output diff --git a/tools_pc/abpad/sheet.py b/tools_pc/abpad/sheet.py new file mode 100644 index 00000000..de67bedf --- /dev/null +++ b/tools_pc/abpad/sheet.py @@ -0,0 +1,25 @@ +import sys +from pathlib import Path +from PIL import Image, ImageDraw + +D = Path(sys.argv[1]) # run dir: 1964_.png + port_.png (capture.ps1) +t0, t1, step = float(sys.argv[2]), float(sys.argv[3]), float(sys.argv[4]) +W, H = 320, 240 +rows = [] +t = t0 +while t <= t1 + 1e-6: + tag = f"{int(round(t * 1000)):05d}" + row = Image.new("RGB", (W * 2 + 70, H), "black") + for i, side in enumerate(("1964", "port")): + f = D / f"{side}_{tag}.png" + if f.exists(): + row.paste(Image.open(f).convert("RGB").resize((W, H)), (70 + i * W, 0)) + ImageDraw.Draw(row).text((5, H // 2), f"{t:.1f}s", fill="white") + rows.append(row) + t += step +sheet = Image.new("RGB", (W * 2 + 70, H * len(rows)), "black") +for i, r in enumerate(rows): + sheet.paste(r, (0, i * H)) +out = D / f"sheet2_{t0:04.1f}-{t1:04.1f}.png" +sheet.save(out) +print(out) diff --git a/tools_pc/bundle-linux.sh b/tools_pc/bundle-linux.sh index 342c8d73..53f665a9 100755 --- a/tools_pc/bundle-linux.sh +++ b/tools_pc/bundle-linux.sh @@ -70,11 +70,13 @@ if command -v ldd >/dev/null 2>&1; then fi # --- docs + licenses -------------------------------------------------- -DEPS_BLOCK=$'## 1a. Runtime libraries\n\nSDL2 is bundled in this folder and found automatically (the executable\npoints at its own directory first). zlib and OpenGL come from your system —\npreinstalled on every desktop distro and on SteamOS / Steam Deck.\n\n**Steam Deck:** sideload this folder (USB or a file manager), do steps 2–4\nbelow, then add `@EXE@` to Games → *Add Game* as a non-Steam game.\n' +DEPS_BLOCK=$'## Linux and Steam Deck\n\nSDL2 is bundled in this folder and found automatically (the executable\npoints at its own directory first). zlib and OpenGL come from your system —\npreinstalled on every desktop distro and on SteamOS / Steam Deck.\n\n**Steam Deck:** copy this folder over (USB or a file manager) or download it\non the Deck, do steps 2 and 3 below, then add `@EXE@` to Games → *Add Game*\nas a non-Steam game. The first launch applies Deck-friendly display settings\n(native 1280×800 fullscreen).\n' +# The sed pass below runs before the awk block insert, so expand @EXE@ here. +DEPS_BLOCK="${DEPS_BLOCK//@EXE@/$EXE_NAME}" # @DECK@: the in-game settings (F10 overlay) controller-mapping guide — only # meaningful where Steam input mapping exists (Deck / Linux); Windows bundles # substitute it with nothing. -DECK_BLOCK=$'## 4a. Steam Deck — in-game settings (options overlay)\n\nThe options overlay is fully gamepad-driven: it opens with **Select**, the\nD-pad or left stick (up/down) moves between options, **A** steps the selected\noption forward, **B** steps it back, and **Start** (or Select again) closes.\nToggles flip, resolution / MSAA / filtering cycle, sliders step in\nincrements. With a keyboard attached the same overlay is `F10` + arrows/Enter.\n\n' +DECK_BLOCK=$'## Steam Deck: in-game settings (options overlay)\n\nThe options overlay is fully gamepad-driven: it opens with **Select**, the\nD-pad or left stick (up/down) moves between options, **A** steps the selected\noption forward, **B** steps it back, and **Start** (or Select again) closes.\nToggles flip, resolution / MSAA / filtering cycle, sliders step in\nincrements. With a keyboard attached the same overlay is `F10` + arrows/Enter.\n\n' sed -e "s|@VERSION@|${VERSION}|g" \ -e "s|@PLATFORM@|Linux x86-64|g" \ -e "s|@EXE@|${EXE_NAME}|g" \ diff --git a/tools_pc/dist/README.md.in b/tools_pc/dist/README.md.in index f01dcfd6..cfb49822 100644 --- a/tools_pc/dist/README.md.in +++ b/tools_pc/dist/README.md.in @@ -1,17 +1,13 @@ # GoldenEye 007 PC Port - @VERSION@ A native PC port of the original Nintendo 64 *GoldenEye 007*, compiled from the -[GoldenEye 007 decompilation](https://github.com/n64decomp/007). +[GoldenEye 007 decompilation](https://github.com/n64decomp/007). No emulator. -> **Playable, with known rough edges.** -> The full single-player campaign runs at a steady 60 fps with no known -> crashes, and in-level music + sound effects play throughout. Known issues: -> cutscenes still glitch, mostly with James Bond (the Dam level-end in -> particular), a few music tracks have wrong-sounding instruments, some -> front-end 3D models are broken (the spinning Nintendo logo, the MISSION -> COMPLETE / mode-select models), and assorted cosmetic rendering defects -> remain. It's an early public cut: report what you find, feedback is very -> welcome. +The full single-player campaign runs at a steady 60 fps, and 2-4 player +split-screen multiplayer works on every multiplayer map. This is a pre-1.0 +release: the known issues are listed at +. +Feedback is very welcome. Project & source: @@ -22,38 +18,38 @@ This is the **@PLATFORM@** build. The executable is `@EXE@`. ## 1. You must supply your own ROM This package contains **no Nintendo code and no game data**. You need a -*GoldenEye 007* Nintendo 64 ROM that **you legally own**, big-endian `.z64`, -NTSC-U (US): +*GoldenEye 007* Nintendo 64 ROM that **you legally own**, US (NTSC-U) +version, big-endian `.z64`: | Region | ROM filename | SHA-1 | |---|---|---| | NTSC-U (US) | `ge007.ntsc-final.z64` | `abe01e4aeb033b6c0836819f549c791b26cfde83` | -| PAL (EU) | `ge007.pal-final.z64` | `167c3c433dec1f1eb921736f7d53fac8cb45ee31` | -| NTSC-J (JP) | `ge007.jpn-final.z64` | `2a5dade32f7fad6c73c659d2026994632c1b3174` | -This release supports the **NTSC-U (US)** ROM. PAL and JP ROMs are recognised -by their SHA-1 but not yet supported; they are on the roadmap. +PAL (EU) and JP ROMs are not supported by this package yet. + +To check your file: on Windows, `certutil -hashfile ge007.ntsc-final.z64 SHA1`; +on Linux, `sha1sum ge007.ntsc-final.z64`. @DEPS@ ## 2. Set up (one time) -1. Unzip this folder anywhere. +1. Unpack this folder anywhere (on Windows, not inside the zip). 2. Make a `data` folder next to `@EXE@`. 3. Copy your ROM into it, named exactly: `data/ge007.ntsc-final.z64` -4. That's all the setup. On first launch the game detects the ROM and - generates the derived data (`data/pcmodels-ntsc-final/` and - `data/pccg-ntsc-final/`) automatically; that first run takes a few extra - seconds. No Python, no compiler, no N64 toolchain: nothing to install. - If you replace the ROM later, delete those two folders and the game will - regenerate them on the next launch. + +That's all. On first launch the game generates its asset files from the ROM +(`data/pcmodels-ntsc-final/` and `data/pccg-ntsc-final/`); that first run +takes a few extra seconds. No Python, no compiler, nothing to install. If +you replace the ROM later, delete those two folders and they are made again. ## 3. Run -Run `@EXE@` **from this folder**. Configuration is written to `ge007.ini` on -first run. +Run `@EXE@` **from this folder**. On Windows, SmartScreen may say "Windows +protected your PC" because the exe is not code-signed: click **More info**, +then **Run anyway**. -> If the game exits immediately reporting missing data, run the generator -> manually from this folder (`prepare-assets/ge007-convert`; on Windows, +> If the game exits right away reporting missing data, run the generator +> yourself from this folder (`prepare-assets/ge007-convert`; on Windows, > `prepare-assets\ge007-convert.exe`). On success it prints one line per pass > and ends with `prepare-assets: done.` @@ -61,21 +57,55 @@ first run. | Action | Keyboard / mouse | Controller | |---|---|---| -| Move / strafe | `W` `A` `S` `D` / arrows | Left stick (or D-pad) | -| Aim / look | mouse | Right stick | -| Fire | left mouse / `LCtrl` | Right trigger | -| Aim mode | right mouse / `LShift` | Left trigger | -| Use / accept | `Space` / `E` / `X` | A / X | -| Reload / cancel | `R` / `F` | B / Y | -| Next / previous weapon | Mouse wheel up / down | RB / LB | +| Move / strafe | `W` `A` `S` `D` | Left stick (or D-pad) | +| Move / turn | Arrow keys | - | +| Aim / look | Mouse | Right stick | +| Fire | Left mouse | Right trigger | +| Aim mode | Right mouse / `LShift` | Left trigger / LB | +| Use / interact | `E` | A | +| Reload | `R` | X | +| Crouch | `LCtrl` | Left or right stick click | +| Cycle owned gadgets | Watch inventory | B | +| Next weapon | Mouse wheel down / `Q` | Y | +| Previous weapon | Mouse wheel up | - | | Start | `Enter` / `Tab` | Start | -| Options overlay | `F10` | Select (D-pad/stick + A/B navigate, Start closes) | +| Options overlay | `F10` | Select | -Mouse sensitivity (master and aim-mode), Y-inversion and the other input -settings are in the `[Input]` section of `ge007.ini`, or in the F10 overlay. +Everything is rebindable in the options overlay: *Input -> Bindings...* for +keyboard and mouse, *Input -> Controller* for each controller. + +**Split-screen:** pick Multiplayer from the main menu. With one controller, +keyboard/mouse is player 1 and the controller is player 2; with two or more +controllers, the controllers take players 1-4. @DECK@ -## 5. Legal +## 5. Your files + +Everything stays inside this folder; nothing is written anywhere else. + +| File | What it is | +|---|---| +| `data/ge007.eep` | Your game progress. Copy it somewhere safe (with the game closed) to back it up. | +| `data/ge007.ini` | Your settings. | +| `data/pcmodels-*/`, `data/pccg-*/`, `data/ge007.shaders` | Generated files. Safe to delete; they are made again. | +| `ge007.crash.log` | Written in the folder you launched from if the game crashes. | + +## 6. Updating to a newer version + +Unpack the new version into a new folder, then copy your ROM, `ge007.eep` +and `ge007.ini` into its `data` folder. Don't copy the generated +`pcmodels-*` / `pccg-*` folders: the new version makes its own. + +## 7. Problems? + +- **Black screen or no window:** update your graphics driver (OpenGL 3.0 is + needed). +- **The game crashes:** open an issue at + and attach + `ge007.crash.log`, the output of `@EXE@ --version`, your OS, your GPU and + your ROM's SHA-1. **Never attach the ROM itself.** + +## 8. Legal Non-commercial fan preservation / research project. **Not** affiliated with, endorsed by, or sponsored by Nintendo, Rare, Microsoft, MGM, Danjaq, or EON