43 Commits
Author SHA1 Message Date
James Dansereau 5b86a9bbeb v0.6.0: release notes, ROADMAP and findings for D953-D955 (maintainer Deck-confirmed) 2026-10-13 22:09:22 -04:00
James Dansereau e70fffffcd D953: a wider Field of view no longer pulls model LOD, prop fog fade and fog visibility closer
c_lodscalez is built from the rendered FOV (D222), so Video.FovScale moved every model LOD switch,
the prop fog fade and the fog-range visibility test closer (x0.58 at 150); half-fogged props such
as the Surface windows went fully invisible and flickered while turning. All three now use the
game's own FOV (portLodScaleOwnFov); identity at the default FOV.
2026-10-13 22:09:22 -04:00
James Dansereau a77bd4e03d D955: at 90+ fps, object motion and the sky are blended between ticks
Guards, doors and props moved at the 60 Hz tick inside a 90 Hz picture. Each object's joint
matrices are now blended, keyed by (Model*, joint) from a game-thread snapshot and rebased onto
the new camera; a chr blends together with its held weapons and hat; room-origin changes, failed
gates and split-screen stay exact. The CPU-projected sky is re-projected to the in-between camera.
Destroyed objects (world-space matrices) stay exact. Port layer only; porting-notes D46.
2026-10-13 22:09:22 -04:00
James Dansereau b7280e5386 D954: at 90+ fps, props (guards, doors, objects, effects) follow the blended camera
In-between presents drew props at the tick's camera while rooms used the blended one, so props
jumped against the world whenever the camera moved (doors and guards flickering at 90 fps, never
at 60). The props' plain perspective now gets the room camera blend (P * R^-1 * Rb) from
bgLevelRender up to the first-person gun. Port layer only (gfx_pc.cpp, interpportal.c).
2026-10-13 22:09:22 -04:00
James Dansereau 7d3abbf813 v0.6.0: D952 fix in; macOS black-window known issue removed from notes, ROADMAP, user guide and the macOS download README 2026-10-13 14:08:17 -04:00
James Dansereau a9feb0960f D952: macOS black window when started with the display asleep - VSync through CGL, not SDL3's display-link wait 2026-10-13 14:02:56 -04:00
James Dansereau af5e75ccbf Release notes: one line per paragraph and bullet (GitHub release pages render every newline as a line break) 2026-10-13 13:19:37 -04:00
James Dansereau 971ff2b2bd v0.6.0 docs accuracy pass: credits-crash and exit wording, known-issues table, download README save import and menu paths, D951 2026-10-13 13:14:59 -04:00
James Dansereau ac8b0f38df Download README: frame rates above 60 and the macOS ROM check 2026-10-13 11:25:57 -04:00
James Dansereau 61e45ed085 Release notes: link the user guide and about page at the v0.6.0 tag, not the release branch 2026-10-13 10:57:17 -04:00
James Dansereau c3e64329c4 README: SEO pass (Apple Silicon in the tagline, ultrawide, descriptive alt and link text, Steam Deck and refresh-rate FAQ entries) 2026-10-13 10:53:11 -04:00
James Dansereau 1ca5eb197e Docs: README, release notes and user docs restructured for v0.6.0
- README in the shape of the top decomp/recomp ports: title, one-paragraph
  pitch, latest-release link, a single no-assets line; then Requirements,
  Quick start, Controls, What this port offers, FAQ (new), Status, Roadmap,
  Building, Documentation, Contributing, Credits, Legal. 852 -> ~370 lines.
- Moved, not removed: docs/user-guide.md (full controls, feature list,
  settings and files, ROM check, macOS setup, updating, emulator saves,
  uninstalling, troubleshooting) and docs/about.md (release history, status
  in detail, how it was made with the models/tools table, how it works, the
  comparison with the XBLA projects).
- Release notes: upgrade and code-signing callouts, Highlights, Downloads,
  Getting started, Added/Fixed/Changed without internal finding labels, a
  shorter known-issues list, Tested on, Thanks, Verify (incl. attestation).
- ROADMAP: v0.7.0 rows for the macOS first-launch black window and the pixel
  gate vs the Auto frame-cap default. building.md: ROM table link.
2026-10-13 10:47:31 -04:00
James Dansereau cdf43d0140 v0.6.0: license texts in every download; README/NOTICE wording pass
- The Windows and Linux bundles shipped without the GCC runtime and SDL2
  license texts when the build machine lacked them (CI's MSYS2 has no
  share/licenses/gcc-libs; CI's Linux image has no dpkg). bundle-win.sh,
  bundle-linux.sh and package-macos.sh now fall back to in-tree copies:
  tools_pc/dist/GPL-3.0.txt, GCC-RUNTIME-EXCEPTION.txt, SDL2-LICENSE.txt.
- NOTICE / README: say the repository and downloads carry no ROM data or
  original assets and that the C source is a reconstruction (not "no
  Nintendo code"); describe the license-text sources.
- README: "What this port offers"; split-screen for up to 4 players; the
  few game-code changes keep N64 behavior or are opt-in; neutral wording
  for the comparison with the XBLA recompilation projects (rows about their
  status and motives removed); verification approach stated; release notes
  drop the 165/240 testing caveat.
2026-10-13 09:58:48 -04:00
James Dansereau 162691a3f5 v0.6.0: issue fixes and v0.7.0 roll-ins before release
Issue fixes:
- D643 (#145 b/c, #149): Original control style is a strict virtual N64 pad
  (native buttons in stage/menu/watch/tank, per-seat Input.N64Pad remap,
  Input.N64StickFeel)
- D605 + D645 (#145 a): Invert look reaches the original styles; in Original
  it is the game's own Look up/down, synced with the F10 row
- D606 (#144): optional always-visible mission timer
- D644 (#146): IA4 decodes as the RDP's I3:A1 (file-select crest drew on a
  dark square)
- D581 (#151): open on the real primary monitor (SDL display 0) and centre
  every window placement in the display's global bounds; the earlier
  "display at (0,0)" rule still picked the left monitor
- D580 (#153, #156): runtime check of the propDef type/end-of-list fix
- D646: F10 dropdown popup stays inside the settings box

Frame rate (#148):
- D578: Auto follows the display up to 240 (4 presents per sim tick, the
  renderer's ceiling); 165 and 240 in the Frame rate cap row

Rolled in from release/v0.7.0:
- D632: release the GL context + window before exit (BSOD 0x119)
- D626: Windows per-monitor DPI awareness
- D639: exclusive fullscreen uses the desktop mode
- verify.sh: a frame-size mismatch fails; demobatch/flicker_scan harness

Release gate, licenses, docs:
- NOTICE: macOS packaging and pi/ coverage; runtime libraries bundled in
  the downloads (SDL2/SDL3, zlib, MinGW runtime, macOS GCC runtime)
- macOS package ships SDL3, GCC COPYING.RUNTIME and the GPL-3.0 text
  (tools_pc/dist/GPL-3.0.txt)
- README: release date 2026-10-13, "Why play this port", Legal (Apple,
  runtime licenses), frame-rate text; site date tokens
- tools_pc/release_gate.sh: one-command win/deck/mac release checks
- release notes, findings D578/D580/D581/D643-D646 + index, porting notes
2026-10-13 09:37:19 -04:00
James Dansereau 884b8fb8a5 ROADMAP: v0.6.0 wrap-up accuracy — #95 merged (record merge 903095c9 + f87f4bcd), origin/macos + ci/selfhosted-fork-runners deleted from GitHub, #131 closed (scaffold parked park/ci-selfhosted-sweep-131), #124 tentatively v0.7.0 triage, #108 addressed as D573 2026-10-10 12:36:56 -04:00
James Dansereau c4ba655227 AGENTS: rule 7 — squashes/rebuilds must not erase contributor PRs (record merges, Forgejo backup + tree-diff gate + CI re-run before force-push; PR-base handling) 2026-10-10 12:20:37 -04:00
James Dansereau cd4902dfe6 ROADMAP: v0.7.0 picks (alt-tab, DPI awareness, exclusive fullscreen, Linux parity gate, Deck interp goldens, self-hosted runner) 2026-10-10 12:16:27 -04:00
James Dansereau 5d9ecb495a Docs: alt-tab known issue in the v0.6.0 notes; README macOS bullet (download exists) 2026-10-10 12:16:27 -04:00
James Dansereau 2ff406b9a8 GE-ENV-PROBES: rows for the v0.6.0 interpolation, Deck harness and macOS probes 2026-10-10 12:16:27 -04:00
3f73a18db1 docs: v0.6.0 README, release notes, site pages, social card, version 0.6.0
Co-authored-by: f1zz1ec0ke <220638765+f1zz1ec0ke@users.noreply.github.com>
Co-authored-by: danturn <7792186+danturn@users.noreply.github.com>
Co-authored-by: JunielKatarn <4507319+JunielKatarn@users.noreply.github.com>
Co-authored-by: italoarruda <3868006+italoarruda@users.noreply.github.com>
Co-authored-by: dolent <16023813+dolent@users.noreply.github.com>
2026-10-10 12:16:27 -04:00
James Dansereau 218cb90231 docs: findings D573-D614, porting notes, ROADMAP (v0.5.1 renumbered to v0.6.0, D585 parked, known issues) 2026-10-10 12:16:27 -04:00
James Dansereau 130ebc456b dev tooling: LLM session discipline (dev-process section 8) and the pi/ subagent package 2026-10-10 12:16:27 -04:00
James Dansereau b0d4abbfdf golden: Windows reference set re-baselined (D596) 2026-10-10 12:16:26 -04:00
James Dansereau 62c56cf886 ci+tools: macOS arm64 release package (scripts/package-macos.sh) + macos-build CI job, Mach-O symbol tooling, Deck levelbench/deckrunner harness, verify.sh ini extras 2026-10-10 12:16:26 -04:00
859a777d61 feat+fix: frame interpolation above 60 fps (D578, D583, D584; rework of #137), experimental macOS on Apple Silicon (#95, #88; D586-D604, D607-D613), issue fixes #150 (D579), #151 (D581), #152 (D582), #153 (D580), #108 (D573), ge007.log (D576), update-check fix, HUD-150 sliders, preset scope (D440); interpolation dev switches stripped for release
Co-authored-by: f1zz1ec0ke <220638765+f1zz1ec0ke@users.noreply.github.com>
Co-authored-by: danturn <7792186+danturn@users.noreply.github.com>
Co-authored-by: JunielKatarn <4507319+JunielKatarn@users.noreply.github.com>
Co-authored-by: italoarruda <3868006+italoarruda@users.noreply.github.com>
2026-10-10 12:16:26 -04:00
James Dansereau 903095c9c8 Merge pull request #95 from danturn/macos-arm64-gcc
Experimental macOS on Apple Silicon (record merge).

The content of this PR shipped in v0.6.0 via the macos-arm64 integration
line (D585-D613) and is carried by the commit above the merge in this
history; the release line uses maintainer squashes for its own work, so
this merge records the PR against that history. See the PR for the full
diff, and D600/D601-era notes for the port_addr decision made with the
author. Intel (x86_64) status: see issue #154.
2026-10-10 12:16:13 -04:00
James Dansereau fda76b6983 Merge pull request from jkdansereau/goldeneye-pc-port/docs/post-v050
Post-v0.5.0 docs: Pages markdown fix, credit line, ROADMAP
2026-10-07 12:07:09 -04:00
James Dansereau 0698524e87 Post-v0.5.0 docs: Pages markdown fix, credit line, release-notes template, ROADMAP/D572
- Pages: the two cblock sections render as markdown under GitHub Pages'
  kramdown (markdown="1"); the Download block's unparsed `<file>` had swallowed
  every heading below it. Gallery alt text says 3840x2160.
- Credit line (README "How it was made" + social preview card): several coding
  agents directed by one maintainer, with a handful of contributors; card
  findings count 430.
- Release-notes template: drop the stale header-stills comment and the Aztec
  re-check-owed note.
- ROADMAP: Windows display-scaling (not DPI-aware) item
  D572: CI-tarball verification (glibc floor, X220 + Deck fresh
  install, 21/21 pixel gate on both).
2026-10-07 12:04:04 -04:00
James Dansereau 56b574cb1c Merge pull request from jkdansereau/goldeneye-pc-port/release/v0.5.0
release/v0.5.0: v0.5.0 split-screen, options menu, N64 accuracy and Deck fixes
2026-10-07 10:50:51 -04:00
Dan Turner 7b08a04f54 ci: macOS arm64 build + port_addr self-test on PRs into macos
Adds a GitHub-hosted macos-15 (Apple Silicon) workflow for pushes and PRs
into the macos branch: build with Homebrew GCC, once with PORT_ADDR_STRICT
off and once on, then run the ROM-free address-model self-test.

- main.c: new --selftest flag, handled right after portAddrInit() and
  before crashInit/config/ROM/SDL. Exits 0 if the self-test passed, 1 if
  not. Listed in --help.
- port_addr.c/.h: portAddrSelfTest() now returns its result and
  portAddrSelfTestPassed() exposes it; no change to normal runs.
- macos.yml: contents: read only, no secrets, no ROM, persist-credentials
  off, so it is safe for fork PRs. The strict entry also requires the
  GE_ADDRSTRICT_SELFTEST report, proving the validator is live.
- docs: building.md CI note; plan M4 row no longer lists macOS CI as
  outstanding (ROM sweeps stay with the self-hosted job in #131).
2026-10-04 11:21:37 +01:00
dolent 92986a07a9 docs: D420 findings entry (crouched-rifle firing animation table: linker adjacency)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit d954dfb581)
2026-10-04 11:18:18 +01:00
dolent 82e20c4815 fix: D420 -- crouched-rifle firing animation never resolved on PC (linker adjacency) -> SIGSEGV in MP matches
crouched_rifle_firing_animation_group1 (chr.c) has one entry and no
terminator; on the N64, .data is in source order, so
crouched_rifle_firing_animation_groupA follows it and the single
initResolveAnimGroupTable pass over group1 also resolves groupA (len 2).
GCC places groupA below group1, so groupA[0] kept the raw anim offset
0xB84 (PTR_ANIM_fire_kneel_left_leg). playerTick (bondview2.c:11021) passes
it to modelSetAnimation for a player's crouched rifle fire
(firing_animation_groups[1][5]) -> SIGSEGV in modelConstrainOrWrapAnimFrame
(fault 0xb88). Crouching rifle guards also saw len 1 instead of 2.

#ifdef PORT: one array {right_leg, left_leg, terminator} (the N64 layout),
groupA = &group1[1] (chr.h). ABI/layout exception, porting-notes D5 class
(D164 precedent). Verified: 4-seat repro (M16, Complex, frame 5030) runs
5 min x3 SH-identical, no crash; gdb scan of all firing_animation_groups
entries: 1 bad pointer before, 0 after; P1 corpus (1324 recordings) verdicts
identical; solo/couch frames byte-identical; netplay 2P/4P identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 8595a06edd40ca22ac9ba4955b13a9ce61064475)
(cherry picked from commit 8e5d4bcd5a)
2026-10-04 11:17:57 +01:00
Dan Turner 9e473faa6c macos/arm64: review fixes — NULL-safe portAddrIsInWindow, drop duplicate MSAA clamp and wrong terminator
- port_addr.h: portAddrIsInWindow() now rejects the low 64 KiB of the
  window. With PORT_ADDR_BASE == 0 that range contains NULL; the previous
  version let NULL through on Windows/Linux, regressing langGet()'s
  cleared-bank guard (the old numeric bound rejected < 0x10000). Matches
  the old bounds on every platform. Self-test covers NULL, the low guard,
  DRAM and above-window.
- gfx_opengl.cpp: revert to origin/macos. upstream already clamps MSAA to
  GL_MAX_SAMPLES (D405 / #106); the branch's second clamp and its comments
  were a duplicate. MACOS-MSAA-CLAMP finding removed.
- chr.c: drop the branch's crouched_rifle_firing_animation_group1 {0}
  terminator. GCC places groupA below group1, so it never resolved groupA
  and did not fix the crouched-rifle crash. Replaced by #120 (next commit).
- boss.c: guard the rspReplyMsg re-base with #ifdef PORT (the file has no
  PORT_N64PTR identity fallback for the N64 build).
- findings.md: correct the objDeform vtxslot write-up (host pointers, not
  u32 N64 addresses), give accurate numbers for unguarded src/ edits, and
  record why the terminator was replaced.
2026-10-04 11:17:53 +01:00
Dan Turner aebce4013e macos/arm64: relabel findings to descriptive tags, use portAddrIsInWindow, document src/ edits
Owner review follow-ups on #95:

- Findings labels: replace the branch's D409-D421 (which clash with
  upstream's D-range) with descriptive MACOS-<TOPIC> tags across
  findings.md (section F rows + tail entries), findings-index.csv,
  porting-notes.md, MACOS-ARM64-PLAN.md, CMakeLists.txt, building.md and
  the code comments. Real D-numbers get assigned at merge.
- gen_findings_index.py: accept non-D descriptive tags as labels so the
  generated index stays complete while the branch is unmerged (D-rows
  unchanged).
- language.c: convert the last hardcoded numeric address bound in the
  boot path to portAddrIsInWindow(), per the branch rule (address
  plausibility checks must not use numeric bounds).
- Document the src/ edits the owner flagged as unwritten: the two
  vtxstore_allocate() callers and objDeform() (both the PORT_N64PTR(0)
  -> NULL class, under MACOS-BLOODSTAIN), the crouched-rifle table
  terminator (MACOS-CAST-CENSUS), and an honest note on which widened
  sites are #ifdef PORT and which follow the existing unguarded house
  style.
- Drop the stray .DS_Store from history (it was added in the base commit
  and only deleted at the tip).

No behaviour change: PORT_ADDR_BASE == 0 everywhere but macOS, and the
one logic edit (language.c) is an identity there. Build + window +
port_addr self-test ALL PASS on arm64.
2026-10-04 10:58:12 +01:00
Dan Turner 4d0abb1a39 macos/arm64: renumber findings onto v0.4.0 (D409–D421) + restore docs
After rebasing the macOS/arm64 branch onto v0.4.0 (main was at D408, and had
already consumed D323–D349), the branch's findings collided. Renumber the 13
macOS findings D294–D307 -> D409–D421 (main's own D29x/D30x references left
untouched), and restore the findings.md entries + porting-notes §F section
that the merge dropped.

- findings.md: 13 §F/§H rows (D409–D421) + their tail sections.
- findings-index.csv: regenerated (--check clean).
- porting-notes.md: §F macOS section restored + Darwin/Mach-O bullets renumbered.
- building.md / GE-ENV-PROBES.md / CMakeLists.txt / code comments: D294–D307 -> D409–D421.

Drops the obsolete reticule-jitter finding (D336): v0.4.0 fixed it as D332
(folded from PR #96).
2026-10-04 10:50:01 +01:00
Dan Turner 3785dcad33 macos: fix the pointer-truncation crash family, add PORT_ADDR_STRICT (D301-D308)
Census-driven: rebuilt the compiled set with -Wint-to-pointer-cast
-Wpointer-to-int-cast -Wint-conversion, giving 276 actionable sites, triaged
in three disjoint file groups. Group A (206 warnings) and most of C were
already-correct round trips; these are the real bugs.

- D301: chrCreateBloodStain() crashed on guard gunfire. Two defects —
  portN64ToHost(0) returned base+0 instead of NULL (defeating the decomp's
  `if (p != NULL)` failure checks), and Vertex.CollisionRelatedNode (a u32 vma
  kept 32-bit by PROMOTE32) was read with a bare cast.
- D302: six more truncations. Four are the "twin function" pattern — one of a
  near-duplicate solo/MP pair converted, the other missed. Includes bg.c:5213,
  where a truncated loop bound meant bgRoomCalcBB() never ran its body on
  arm64, leaving EVERY room in every level with an inverted bounding box.
  Silent, not a crash. Possible relevance to open D249.
- D303: texReadUncompressed() aligned its destination through u32. Latent —
  a probe over a full level boot showed the path is unreachable on PC
  (textures come from the pre-converted sidecars).
- D306: MSAA=8 produced a completely black screen. GL_MAX_SAMPLES is 4, and
  nothing clamped the request, so the renderbuffer was never allocated.
  video.c's own comment claimed MSAA "snaps down to the highest supported
  level"; it did not. Also fixed two index-vs-GL-name bugs in the same file.
- D308: the watch's Control page crashed. options.c cast a stack-local table
  pointer to (s32) before passing it as WatchContButtonPositions*, and
  watchRenderControllerOpaque forwarded the truncated value. Verified A/B
  against the user's exact repro — fault address byte-identical.
- D305: PORT_ADDR_STRICT, a debug gate (off by default, compiled out) that
  validates every u32->host conversion against the regions the port maps and
  reports offenders with a backtrace. Zero reports across 6 levels, 5 of them
  under full-control input.

Also records D307 (reticule jitter in aim mode) in the finding log as OPEN,
with the cause identified from live captures and no code change — an attempted
fix was reverted for not working.

Verified: clean build, 21/21 sweep, plus a targeted repro per fix.
2026-10-04 10:50:01 +01:00
Dan Turner 498be832c2 macos: packaging, sweep tooling, and two runtime fixes (D299, D300)
- tools_pc/bundle-mac.sh: a signed, double-clickable GoldenEye.app. Homebrew's
  libstdc++/libgcc_s and SDL2 are not system libraries, so they are bundled
  and rewritten to @executable_path/@loader_path. Note sdl2-compat (Homebrew's
  `sdl2`) finds its SDL3 backend through a RELATIVE rpath baked into the dylib,
  so SDL3 ships too and that rpath is repointed — copy it without that and
  sdl2-compat raises a modal error dialog from its initialiser and the app
  hangs before main(). prepare-assets/ goes in Contents/Resources/: codesign
  treats a directory under Contents/MacOS/ as nested code and fails the bundle.
- tools_pc/level_sweep_mac.sh: per-level crash-log sweep, symbolicated with
  atos, with a rendered-pixel check so "runs but renders nothing" is caught.
- D299: sndPlaySfx() rejected EVERY sound on arm64 — all in-level SFX were
  silent, while music played (separate player). Its guard predated the shifted
  window: `sp >= 0x400000000ULL` (16 GiB) is correct on x86_64 (DRAM at
  0x70000000) and rejects everything on arm64 (DRAM at ~0x1000_707b_5740).
  Fixed with a portAddrIsInWindow() helper rather than absolute bounds.
- D300: cleanupObjects() did `u32 *obj = (u32)g_CurrentSetup.propDefs`,
  truncating a host pointer to 32 bits. Crashed at stage unload — only
  reachable once a level actually ends.

Verified: 21/21 on both the no-input and full-control sweeps; SFX voices
allocate on every level.
2026-10-04 10:50:01 +01:00
Dan Turner d4cc71af0a macos: native Apple Silicon (arm64) support — address model, boot chain, 21/21 levels
Adds native arm64 macOS support on top of #88's Intel macOS layer.

The one structural change arm64 needs is an address model. The port realises
the N64's 32-bit address space *as host addresses* — DRAM at 0x70000000, cart
at 0x10000000, plus ~1700 linker-absolute symbols — which is only sound while
host addresses fit in 32 bits. Native arm64 cannot map below 4 GiB
(__PAGEZERO; a shrunken pagezero is SIGKILLed at exec; PIE is mandatory;
MAP_32BIT fails). So the whole space is relocated to a fixed host base,
PORT_ADDR_BASE (16 TiB), with portN64ToHost / portHostToN64 / PORT_N64PTR as
the conversion chokepoints. PORT_ADDR_BASE is 0 on every other platform, so
all of this is an identity there.

That relocation then surfaces a class of latent 64-bit bugs that are invisible
at base 0 — any place the decomp round-trips a pointer through a 32-bit
integer. The src/ changes here are that class: pointer-width/layout only, no
behaviour change, N64 branch textually unchanged, each documented in
docs/dev/findings.md (D298/M2).

Also: Mach-O absolute-symbol generation at the same base, #pragma weak ->
symbol equates, __x86_64__ -> PLATFORM_64BIT, Darwin crash handler/ucontext,
shm_open DRAM, the AppKit main-thread guard, and the sidecar generation that
was the real boot blocker.

Verified: 21/21 solo levels boot, render and run crash-free.
2026-10-04 10:50:01 +01:00
James Dansereau 39d4f8d088 Merge pull request #132 from jkdansereau/fix/macos-errno-include
romdata: include <errno.h> after ultra64.h (Linux build fix on macos)
2026-09-30 10:06:29 -04:00
James Dansereau 12a814a4e4 romdata: include <errno.h> after ultra64.h (fixes the Linux build on macos)
#88 added <errno.h> to the POSIX include block, ahead of ultra64.h.
glibc defines errno as a macro, (*__errno_location ()), which rewrites
the N64 `u8 errno;` fields in PR/os.h (OSContStatus, OSContPad,
OSContRamIo) and fails with "field '__errno_location' declared as a
function". Include it after ultra64.h/bondtypes.h instead. Nothing in
this file reads those struct fields; errno is only read by the mmap
failure diagnostic. No change on Windows.
2026-09-30 10:02:08 -04:00
James Dansereau c415315797 Merge PR #88 (JunielKatarn: native Intel macOS) into macos
#88's original commit (62b21054) conflicts with current main. The same
change, rebased onto main by danturn with the conflicts resolved, is
carried as ae1884cc (same author) at the base of #95. This merge keeps
that resolved tree exactly, so the tree is identical to ae1884cc, and
brings in #88's original head so the PR is recorded as merged.
2026-09-30 09:36:34 -04:00
Julio C. Rocha ae1884cc05 Implement macOS (Intel) support 2026-09-30 10:09:35 +01:00
Julio C. Rocha 62b2105458 Implement macOS (Intel) support 2026-09-17 00:35:30 -07:00
191 changed files with 14951 additions and 1561 deletions

No files matched your search

+2 -2
View File
@@ -51,12 +51,12 @@ by this repo's CI from the release tag, and nothing else. Verify any
downloaded artifact with:
```sh
gh attestation verify goldeneye-pc-port-0.5.0-win64.zip \
gh attestation verify goldeneye-pc-port-0.6.0-win64.zip \
--repo jkdansereau/goldeneye-pc-port
```
(use the name of the file you downloaded, e.g.
`goldeneye-pc-port-0.5.0-linux-x86_64.tar.gz` on Linux). A passing check confirms both
`goldeneye-pc-port-0.6.0-linux-x86_64.tar.gz` on Linux). A passing check confirms both
the file's digest and its provenance; a tampered or foreign file fails.
Releases before v0.5.0 have no attestations — for those, the per-file
`.sha256` sidecars and a from-source build are the available checks.
+79 -258
View File
@@ -5,295 +5,116 @@
<img src="https://github.com/jkdansereau/goldeneye-pc-port/raw/v<version>/docs/img/shots/feature-options.jpg" width="340" alt="The PC options menu: custom crosshair colour sliders">
</p>
<!-- MAINTAINER: header stills are from the public capture set (docs/img/shots/).
The 15 MB gameplay-montage gif was dropped from the repo on the v0.5.0
docs media pass (C3); the project index now uses a still carousel. -->
> [!NOTE]
> **Updating?** Your progress and settings carry over. Unpack the new version into a new folder and copy your ROM, `ge007.eep` and `ge007.ini` into its `data/` folder. Leave the old `pcmodels-*` and `pccg-*` folders behind; the new version makes its own.
Platforms: **Windows x86-64** and **Linux x86-64 (including Steam Deck)**.
Region: **NTSC-U (US) only**. See [Known issues](#known-issues).
> [!NOTE]
> The downloads aren't code-signed. On Windows, if SmartScreen appears, click **More info**, then **Run anyway**. On macOS, see [Getting started](#getting-started).
## Highlights
This is the first release since v0.4.0. It includes everything that was
staged as v0.4.1, which was never published on its own.
- **Frame rates above 60.** Pick 90, 120, 144, 165 or 240, or **Auto** to match your display (up to 240). The game still runs at its original speed; the extra frames are blended between its ticks, for the camera, the level, the sky, and the guards, doors and objects moving in it. A rework of f1zz1ec0ke's #137.
- **macOS on Apple Silicon (experimental).** A new download for M1 and later Macs, based on danturn's #95. Tested on one M3 Mac so far; reports are welcome.
- **Original controls are now original (#145, #149).** With *Control style* set to Original, your pad behaves as an N64 controller, and *Invert look* works in every original style.
- **Fixes:** geometry vanishing up close (#150), the first launch opening on the wrong monitor (#151), the end-credits crash on Linux and macOS (#152) and a crash when a guard spawns with a hat (#153, #156).
- **Also:** an optional mission timer (#144), the file-select crest fixed (#146), and a fix for a Windows blue screen on exit.
- **2-4 player split-screen.** The game's multiplayer runs with a pad per
player, on every map, with each seat keeping its own controls.
- **One options menu.** The file-select **PC Options** entry and the F10 overlay
are now the same menu, laid out like the Perfect Dark PC port's, with
checkboxes, sliders and dropdowns. The game's own crosshair is the mouse
pointer, long pages scroll, and a tip explains the selected option.
- **The game's N64 control styles, per player.** Pick **Original** to use the
game's own styles 1.1-2.4; each seat keeps its own, saved with the Bond file.
- **Emulator saves load directly.** Copy a Project64, mupen or 1964 save in as
`data/ge007.eep` and it is converted on first launch.
- **Fog and haze closer to the N64.** Ground fog no longer chunks away tile by
tile, the sky fades into fog at the horizon, and the haze around Bond stays
steady as he moves. A new **Fog distance** setting (100-800%, 100% = N64) is separate
from Draw distance, and both apply live.
- **Fixes you can see:** the Dam ending camera swivels onto Bond as on the N64,
and rockets no longer pass
through the ground.
- **New settings:** an optional always-on crosshair, a custom
crosshair colour (0-255 RGB, like the Perfect Dark port), fullscreen mode,
center window, crosshair opacity and crosshair colour by health, a scalable
HUD overlay, and an optional update check (off by default).
Played on Windows and Steam Deck: campaign spot-checks, 2-4 player split-screen
and controller-only sessions.
---
## What's new
### Options menu
- Six sections in the Perfect Dark port's order: Video, Audio, Mouse,
Controller, Key Bindings and Game. **Display mode** is a dropdown; **Backspace** goes back.
- The F10 overlay and FPS counter are the same on-screen size in the front end
and in a level, and **Game.HudScale** scales them with the in-game HUD.
Sliders and arrows are drawn smooth at any window size.
- The OS cursor stays visible over the overlay (no main-menu flicker),
right-click goes back, and the tip follows the hovered row.
- Clearer wording: 27 menu-wording edits, sliders with real units (percent,
frames, seconds, pixels), wrapped tips, one `Key: Action` hint style and
consistent names (Original / Extended, "Original layout"). *Skip intro* and
*All unlocked* are no longer marked experimental, and *All unlocked* applies
live. The game's own *Look up/down* row is gone; use *Invert look* on the
Mouse and Controller pages.
### Controls
- **Control style:** **Ext** (this port's scheme, unchanged) or **Original**
(the game's N64 styles 1.1-2.4), chosen per seat.
- The Xbox-release presets (1.1 Jinx, 1.2 Christmas, 1.3 Frost, 1.4 Elektra)
and Custom rebinding carry over. Preset names refer to the Xbox
release's control styles; this project is not affiliated with Microsoft or Rare.
- The right stick aims and the D-pad strafes, as on the N64; the Xbox
release's "left stick aims the crosshair" and "D-pad copies the left stick"
behaviours are not reproduced.
- New profiles default to Crouch Toggle and Look ahead off (it fights mouse look
on PC). Existing untouched empty save folders get the same on first play;
played or customised profiles are not changed.
### Saves
- An emulator-format `data/ge007.eep` is converted on first launch (the
original is kept as `ge007.eep.emulator.bak`). The converter `eep_convert.py` still does explicit two-way conversion.
(Live playtest owed.)
### Picture and settings
- Fog and near-plane clipping closer to the N64 (details under the technical
section below). Forced aspect ratios letterbox/pillarbox without re-cropping
the play area.
- Draw and LOD distance default to 2.0x (N64 = 1.0x), stick deadzone to 25%,
mouse sensitivity maximum is 4x. **FOV** is vertical degrees, 30-90, with
60 = the N64 view. "Guard AI uses full wide view" is now **Gameplay view
area** (Original / Extended). Old values migrate once; existing `ge007.ini`
files still load.
### Window and updates
- The title bar is static (the FPS readout is still in the F10 overlay) and
the window/taskbar icon is a new project mark.
- **Check for updates** (Game, **off by default**): when on, one HTTPS request
to GitHub per launch and an "Update available" row linking to the releases page.
### Also new since v0.4.0
- **Split-screen multiplayer** for 2-4 players, played live in 2P on every
multiplayer map and in 4P on Temple.
- Controller presets and rebinding, PlayStation and Nintendo button names,
master volume and audio-device selection.
- A steady 60 fps on low-end GPUs (tested on an Intel HD 3000 laptop).
- Fidelity fixes checked against the N64 game: AI visibility at long draw
distances, sniper zoom, turret pacing, Dam/Caverns water, weapons.
- Fixes for reported issues: fire rate (#114), the tank-crush sound loop
(#115), tank movement (#116), weapon sway (#117), recoil on some GPUs (#118)
and light fixtures that didn't react to shots (#119).
---
## Install and upgrade
### Downloads
## Downloads
| File | Platform |
|---|---|
| `goldeneye-pc-port-<version>-win64.zip` | Windows x86-64 |
| `goldeneye-pc-port-<version>-linux-x86_64.tar.gz` | Linux x86-64 (incl. Steam Deck) |
| `goldeneye-pc-port-<version>-win64.zip` | Windows 10/11, 64-bit |
| `goldeneye-pc-port-<version>-linux-x86_64.tar.gz` | Linux x86-64 (glibc 2.31+), including Steam Deck |
| `goldeneye-pc-port-<version>-macos-arm64.tar.gz` | macOS 14+ on Apple Silicon (experimental) |
Each contains the engine executable, a README, license texts, and the
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. The Linux bundle runs on glibc 2.31 or newer (Ubuntu 20.04+,
Debian 11+, SteamOS).
Each download is ready to run: it carries its runtime libraries, a README, the license texts and the one-time asset tool. **No ROM and no game assets are included; you need your own US GoldenEye 007 ROM.**
### Running it
## Getting started
You need your own **GoldenEye 007 N64 ROM** (US version, `.z64`).
1. Unpack the download.
2. Make a `data/` folder next to the executable and put your ROM in it as `ge007.ntsc-final.z64`.
3. Start the game. The first start takes a few extra seconds while it prepares the game files from your ROM. **F10** (controller: **Select**) opens the options menu.
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 start takes a few extra
seconds while it generates its asset files from your ROM.
**Steam Deck:** do the steps on the Deck, then add the executable as a non-Steam game.
**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.
**macOS:** the app isn't notarized, so run this once in Terminal before the first launch, then start `./ge007.aarch64` from the folder.
**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.
```
xattr -dr com.apple.quarantine goldeneye-pc-port-<version>-macos-arm64
```
Full steps, controls and troubleshooting are in the bundled `README.md`.
Or download it in Terminal, which skips that step:
---
```
curl -L https://github.com/jkdansereau/goldeneye-pc-port/releases/download/v<version>/goldeneye-pc-port-<version>-macos-arm64.tar.gz | tar xz
```
Without Terminal: after macOS blocks the first launch, open System Settings → Privacy & Security and click **Open Anyway** next to the `ge007.aarch64` message.
The [user guide](https://github.com/jkdansereau/goldeneye-pc-port/blob/v0.6.0/docs/user-guide.md) covers controls, settings, emulator saves and troubleshooting.
## What's new
### Added
- **Frame rate cap:** Auto, 90, 120, 144, 165 and 240 join 30 and 60 (F10 → *Video*). Auto is the default on fresh installs; an existing `ge007.ini` keeps its value. Above 60 the extra frames are blended between game ticks (no effect on a 60 Hz display): the camera, the level and the sky, and each guard, door and object along its own motion. This adds one tick of latency; a fast camera turn or a big change on screen shows a full frame instead. The *Original N64* display mode keeps 60.
- **macOS Apple Silicon download** (experimental), plus source builds with Homebrew's GNU `gcc`.
- **Original control style as an N64 controller:** every press sends the real N64 button in levels, menus, the watch and the tank. Default layout: South = A, West = B, right trigger = Z, left trigger / RB = R, LB = L, right stick = C-buttons. Each button can be moved per player in `ge007.ini` (`Input.N64Pad.A = a`, ...), and `Input.N64StickFeel` picks the N64's linear stick range.
- **Mission timer** (#144): an optional always-visible timer, off by default.
- **`ge007.log`:** every run is logged next to the ini; the previous run is kept as `ge007.prev.log`.
### Fixed
- Geometry vanishing when you stand against a wall (#150).
- With a wider *Field of view*, objects in fog (such as the windows on Surface) no longer flicker or vanish while you turn, and models keep their detail out to the normal distance.
- The first launch opening on the wrong monitor (#151).
- A crash during the end credits on Linux and macOS (#152).
- macOS: a black window that never started the game when it was launched while the display was asleep.
- A crash when a guard spawns wearing a hat (#153, #156).
- The file-select crest drawing on a dark square (#146).
- *Invert look* now works in every original control style and stays in sync with the watch's *Look up/down* (#145).
- A Windows blue screen (0x119) when the game exits, seen on an NVIDIA GPU.
- The Cradle catwalk shadow flicker, and stale pixels beside the widescreen picture on front-end screens.
- Sound and text pointer checks that rejected valid pointers (#108, with italoarruda).
- The Linux/macOS update check failing on a large response.
- F10 slider labels being cut off at HUD scale 150.
### Changed
- Windows: per-monitor DPI awareness, so fullscreen on a scaled 4K display is native resolution.
- F10 dropdowns stay inside the settings box.
- The crosshair look (colour, size, style) is no longer reset by the *Display mode* presets.
- The old front-end *Velocity* pointer mode is removed.
## Known issues
| Issue | Impact | Workaround |
|---|---|---|
| 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; packaging is still open |
| Changing aspect ratio inside a level can briefly glitch the gun/hand model, rarely | Cosmetic, one-off | Change the ratio from the front-end PC Options, or accept it |
| No macOS or ARM builds | Platform | — |
| Saves from v0.4.0 and earlier can hold fake unlocks from `All unlocked` | Save data | Not repaired automatically. Since v0.5.0 the option never writes the save; keep a backup of `data/ge007.eep` from before you used it |
| The first frame of a level takes a little longer while its textures upload | Brief FPS-counter dip | None needed |
| Far objects almost fully in fog are now hidden as on the N64, except in widescreen where a faint distant building edge can still show (e.g. Surface's dish from the start area); 4:3 matches the N64 | Cosmetic | Higher Draw/LOD distance shows more |
| Issue | Workaround |
|---|---|
| On the Steam Deck at 90 fps, firefights in the centre of Bunker 1 and 2 can drop a few fps, and Jungle can feel slightly less smooth | Set the frame cap to 60 for those levels |
| Cradle's turret explosions may draw wrongly | None yet |
| On Windows, the picture can look hazy for a moment after alt-tabbing back | It settles on its own |
| PAL and JP ROMs aren't supported yet | Use the US (NTSC-U) ROM |
| Saves from v0.4.0 and earlier can hold fake unlocks from *All unlocked* | Keep a backup of `data/ge007.eep` from before you used it |
The full list, with workarounds, is the
[known-issues table](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/ROADMAP.md#known-issues).
The full list is the [known-issues table](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/ROADMAP.md#known-issues).
---
## Tested on
## Fixes and changes, with finding labels
Menu and settings work:
- One options UI: the file-select entry opens the F10 menu (D519). Six
PD-ordered sections with checkboxes, sliders and dropdowns (D504); display
mode dropdown and Backspace-back (D504 follow-up).
- Control styles: Ext / Original with the N64 styles 1.1-2.4 (D513, D516),
chosen per seat and saved with the Bond file (D518, D516).
- Emulator `ge007.eep` import (D514).
- F10 overlay and FPS counter match in size front-end vs level (D510); HudScale
covers them (D512).
- Fullscreen mode, Center window, crosshair opacity and crosshair colour by
health: the four settings the Perfect Dark port had that this port lacked (D511).
- Wording, units, tips and spelling: D505, D506, D507; settings wording pass
and tip wrapping (D554); Look up/down row removed (D564).
- Settings standardised: defaults, FOV in vertical degrees, Gameplay view area (D546).
- Optional update check (D551). All unlocked applies live (D547).
- Crosshair mouse pointer and scrollbar (D555, D556); long tips no longer cut
off inside a level (D558). New defaults: Crouch Toggle, Look ahead off (D556,
D557); same for untouched empty save folders (D559).
- Crosshair always on, opt-in, off by default (D436, #123 by dolent). Crosshair colour is
Original or Custom RGB with 0-255 sliders, as in the Perfect Dark port; a
saved named preset carries over as Custom (D569). Long dropdowns stay on
screen (D568).
- F10 overlay mouse behaviour (D544). Static title bar and new icon (D550).
- Overlay slider wedges, markers and dropdown arrows used to step in whole
canvas pixels and grew visibly at large window sizes; now smooth (D520).
Holding left/right on a Bond settings row no longer drops the FPS counter (D517).
- Watch menu: closing the in-level **Watch** menu saves the watch's own screen
size and ratio, which no longer fight the front-end **Aspect ratio** setting;
the earlier findings-log note was corrected (D349).
Rendering and gameplay fidelity:
- **Dam ending camera:** the first shot of the ending cutscene swivels up onto
Bond again, as on the N64; an old port workaround snapped it straight onto
him (D552).
- **Fog rework (Surface 2, from Deck playtest):** ground fog no longer "chunks"
away tile by tile, and the sky fades into fog toward the horizon as on the
N64 (D540). Large near-camera triangles are now clipped against the near
plane the way the N64's RSP does, with per-vertex fog recomputed on the
clipped edge, so the haze around Bond stays steady as he moves instead of
brightening and darkening (D543, D553; compared against hardware-level
emulation). A new **Fog distance** setting (100% = N64) is independent of
Draw distance, and Draw and Fog distance now apply live instead of on the
next level load.
- **Banded walls near explosions:** in Aztec's dark corridor, walls near rocket
fire turned into black/white/yellow/blue bands (also seen on the Deck); fixed
by clamping colour-combiner inputs to the range the N64 can produce (D548).
Build-verified; a live Aztec re-check is owed.
- **Rocket and thrown-item crashes/clipping:** a player-fired rocket could pass
through the ground when fired near Bond's feet (a stack-layout difference
from the N64, D545), and the same class of bug is fixed in thrown grenades,
knives and objects (D549).
- **Menu double-trigger (Steam Deck):** one A press could advance two menu
screens, and one B could back out two overlay pages. A short release
hold-off on menu A/B/Start and a single back action per press are now in
place. This is a mitigation: the exact cause is unconfirmed (the Deck
re-check passed) (D541).
- **Fog snap:** objects far in fog are hidden as on the N64; in widescreen
the wider view can still show a faint distant building edge (e.g. Surface's
dish from the start area); 4:3 matches the N64 (D503).
- **Aspect ratio:** forced ratios letterbox/pillarbox without re-cropping the
play area (D508); the rare gun/hand model glitch after changing ratio
mid-level is largely fixed, a rare remainder is logged (D509).
Build, tooling and harness:
- A fresh build directory no longer produces an untagged executable name (D515).
- The asset converter's argument quoting and sidecar size check are
tightened; the CI cache actions no longer carry stale ROM paths.
- The reference-frame tooling is non-destructive and drift-free: a verification
sweep used to overwrite your own `data/ge007.ini` and leave the previous
level's save file behind, and now pins both idempotently (your ini and save
are restored whatever the run does); the capture helper reads its frame
windows out of the gate instead of a stale hardcoded copy (D524). The gate's
recipe was re-based: per-level windows in settled gameplay rather than the
intro flyby, and the save file pinned present (D522, D523).
- Debug env vars are documented: `GE_PCDUMP` must be `first-last:step` (a colon
where the dash belongs silently dumps every frame, D527), and a
`GE_STARTMENU` boot skips the EEPROM import, so the imported-save test must
run from a normal boot (D528). A per-triangle draw-state census probe
(`GE_D526`/`GE_D526BOX`/`GE_D526MAX`) is available for transparency/texture
triage (D526; the P13 Dam-ending grate show-through it targeted was confirmed
faithful N64 behaviour on 1964/GEPD, so no change shipped).
## Verification
**Reference-frame gate for this release:** Windows, Linux and Steam Deck each
carry the full 21-level reference-frame set (63 frames each) at the same stems,
re-captured for this release's fog and near-plane changes and confirmed by two
independent capture passes per platform. The full pixel gate is green on all
three against their own goldens on the final build: Windows 21/21, Linux
(Intel HD 3000) 21/21 and Steam Deck 21/21. The recipe pins the run for you: the
save file's CONTENT is pinned (the gate installs its own canonical save per
level, the same way it pins the display ini — D529) and the PRNG seed is
hard-pinned (a stray `GE_RSEED` in your environment warns and is ignored rather
than re-seeding the gate — D532), so a gate run and a capture run see the same
starting state. The cross-platform spread on the shipped sets (0.188-4.553% of
pixels over tol 2 on the 20 non-Cuba levels) is informational only — it is not
a cross-platform pixel-parity claim.
The release downloads themselves, from a fresh folder with only the ROM: all 21 solo levels on Windows (at 120 fps), the Steam Deck (Desktop and Game Mode, at 90), macOS on Apple Silicon, and a 2011 Linux laptop with Intel HD 3000 graphics (at 60).
## Thanks
Outside contributions merged for this release: dolent (PRs #120–#123), MST246
(#125 draw-distance investigation), italoarruda (#109 gamepad-preset ideas),
TenebrusoM (DexDrive / the D514 save-format sample), plus reporter credits on
the issue numbers named above.
f1zz1ec0ke (#137, the frame-interpolation design this release reworks), danturn (#95, macOS), Julio C. Rocha (#88, the first macOS groundwork), italoarruda (#108), and everyone who reported the issues above.
## Verify the download
```
sha256sum -c goldeneye-pc-port-<version>-win64.zip.sha256
sha256sum -c goldeneye-pc-port-<version>-linux-x86_64.tar.gz.sha256
shasum -a 256 -c goldeneye-pc-port-<version>-macos-arm64.tar.gz.sha256 # macOS
gh attestation verify <file> --repo jkdansereau/goldeneye-pc-port # build provenance
```
## Source & docs
---
<https://github.com/jkdansereau/goldeneye-pc-port>, built on the
[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. See the README's
[How it was made](https://github.com/jkdansereau/goldeneye-pc-port#how-it-was-made)
section for the full account.
Built on the [GoldenEye 007 decompilation](https://github.com/n64decomp/007) and modelled on the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark). A non-commercial fan project, not affiliated with any rights holder. Developed with AI coding agents directed by one maintainer; see [About the project](https://github.com/jkdansereau/goldeneye-pc-port/blob/v0.6.0/docs/about.md#how-it-was-made).
+3 -3
View File
@@ -122,12 +122,12 @@
<div class="rep">REPORT:</div>
<div class="rows">
<div class="statusline">Status: <span class="status">RELEASED v0.5.0 &mdash; WINDOWS &middot; LINUX &middot; STEAM DECK</span></div>
<div class="statusline">Status: <span class="status">RELEASED v0.6.0 &mdash; WINDOWS &middot; LINUX &middot; STEAM DECK</span></div>
<div class="stats">
<span class="k">Statistics:</span><span>All 20 solo missions &mdash; full campaign completed in playtesting</span>
<span class="k">Statistics:</span><span>All 20 solo missions &mdash; 60 to 144 fps &middot; macOS (experimental)</span>
<span></span><span>~210 game-code units, compiled unmodified</span>
<span></span><span>~34k lines C/C++ &nbsp;&middot;&nbsp; ~10k Python (port layer + tools)</span>
<span></span><span>407 findings logged &nbsp;&middot;&nbsp; two coding agents, one human, part-time</span>
<span></span><span>465 findings logged &nbsp;&middot;&nbsp; AI coding agents + a handful of contributors</span>
</div>
</div>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 596 KiB

After

Width:  |  Height:  |  Size: 597 KiB

+95 -3
View File
@@ -294,6 +294,93 @@ jobs:
if-no-files-found: error
retention-days: 14
# Experimental macOS (Apple Silicon) build. Compiles + links with Homebrew's
# real GNU GCC (Apple Clang cannot compile the decomp's Plan 9 struct
# extensions) and packages the bundle with scripts/package-macos.sh: the
# non-system dylibs (sdl2-compat + SDL3, libstdc++, libgcc_s) are copied
# next to the exe with @executable_path install names, re-signed ad-hoc.
# The bundle is not notarized; the README inside it gives the one-line
# `xattr -dr com.apple.quarantine .` unblock. Like linux-build this is a
# "compiles + links + packages" signal, not a runtime claim. Not a required
# check. Mirrors the maintainer's local build-mac configuration (Unix
# Makefiles, RelWithDebInfo, newest Homebrew gcc-N).
macos-build:
runs-on: macos-14
env:
CCACHE_MAXSIZE: 500M
HOMEBREW_NO_AUTO_UPDATE: "1"
HOMEBREW_NO_INSTALL_CLEANUP: "1"
HOMEBREW_NO_ANALYTICS: "1"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install dependencies
run: |
echo "CCACHE_DIR=$GITHUB_WORKSPACE/.ccache" >> "$GITHUB_ENV"
brew install gcc cmake sdl2 sdl3 ccache
ls "$(brew --prefix)"/bin/gcc-[0-9]* "$(brew --prefix)"/bin/g++-[0-9]*
- name: Restore ccache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .ccache
key: ccache-macos-arm64-${{ hashFiles('CMakeLists.txt') }}-${{ github.sha }}
restore-keys: |
ccache-macos-arm64-${{ hashFiles('CMakeLists.txt') }}-
ccache-macos-arm64-
- name: Regenerate ROM-asset symbol file (US)
run: python3 scripts/gen_romassets.py u
# Same frozen first-run converter as linux-build. The runner's python3
# is Homebrew's (PEP 668, no system pip installs), so use a venv.
- name: Freeze asset converter (PyInstaller)
run: |
python3 -m venv "$RUNNER_TEMP/venv"
"$RUNNER_TEMP/venv/bin/pip" install --quiet pyinstaller==6.22.3
"$RUNNER_TEMP/venv/bin/python" -m PyInstaller --onefile --noconfirm --clean --name ge007-convert --distpath tools_pc/dist/prepare-assets --workpath build-pyinst --specpath build-pyinst tools_pc/dist/prepare-assets/prepare-assets.py
ls -la tools_pc/dist/prepare-assets/ge007-convert
- name: Configure (ntsc-final)
run: |
GCC="$(ls "$(brew --prefix)"/bin/gcc-[0-9]* | sort -t- -k2 -n | tail -1)"
GXX="$(ls "$(brew --prefix)"/bin/g++-[0-9]* | sort -t- -k2 -n | tail -1)"
echo "using $GCC / $GXX"
cmake -S . -B build-mac -DROMID=ntsc-final -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_C_COMPILER="$GCC" -DCMAKE_CXX_COMPILER="$GXX"
- name: Build (ntsc-final)
run: cmake --build build-mac -j"$(sysctl -n hw.ncpu)"
- name: ccache stats
run: ccache -s
- name: Show binary
run: |
ls -la build-mac/ge007* || true
file build-mac/ge007* || true
otool -L build-mac/ge007.aarch64
- name: Package macOS bundle
run: |
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
V="${GITHUB_REF_NAME}"
else
V="0.0.0-ci.${GITHUB_SHA::7}"
fi
bash scripts/package-macos.sh build-mac dist "$V"
- name: Upload bundle
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: goldeneye-pc-port-macos-arm64
path: |
dist/*.tar.gz
dist/*.tar.gz.sha256
if-no-files-found: error
retention-days: 14
# Real Windows (MSYS2 MINGW64) build — the only platform the port is
# developed and tested on. This compiles and LINKS the full executable.
#
@@ -417,14 +504,14 @@ jobs:
# -level_09 soak, a golden-frame diff) would run on a self-hosted runner
# instead. Not planned.
# Tags only. Attach the packaged Windows + Linux bundles to a DRAFT
# Tags only. Attach the packaged Windows + Linux + macOS bundles to a DRAFT
# pre-release so a human can do the ROM smoke-test on each platform before
# publishing. This job — and only this job — gets contents: write, and it
# is also the only job that attests build provenance (id-token: write +
# attestations: write; attest-build-provenance needs both).
release:
if: github.ref_type == 'tag'
needs: [ changes, validate, windows-build, linux-build ]
needs: [ changes, validate, windows-build, linux-build, macos-build ]
runs-on: ubuntu-22.04
permissions:
contents: write
@@ -454,6 +541,11 @@ jobs:
name: goldeneye-pc-port-linux-x86_64
path: dist
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: goldeneye-pc-port-macos-arm64
path: dist
# SLSA build provenance for the release artifacts (in-toto, signed by
# GitHub's OIDC key for this repo's workflow at the tagged commit).
# Anyone can check a downloaded file:
@@ -474,7 +566,7 @@ jobs:
ASSETS=(dist/*.zip dist/*.zip.sha256 dist/*.tar.gz dist/*.tar.gz.sha256)
NOTES="$(mktemp)"
# Asset names are built from the tag with the leading v stripped
# (bundle-{linux,win}.sh), so substitute the same form -- a raw
# (bundle-{linux,win}.sh, package-macos.sh), so substitute the same form -- a raw
# TAG would make the notes reference goldeneye-pc-port-v*.* assets
# that don't exist.
NOTES_VERSION="${TAG#v}"
+8
View File
@@ -205,3 +205,11 @@ tools_pc/sweep-captures/**/*.ppm
# referenced by no project tooling. Never commit.
/Python/
docs/media/*.mp4
# mod-loader tool build artifacts
/tools_pc/modpatch/modpatch.exe
# demo harness data (tools_pc/demobatch.sh): personal recordings + run output
demos/
demobatch/
levelbench/
+13
View File
@@ -84,6 +84,19 @@ mostly agent-authored.
README Legal section must name every rights holder the game's trademarks
implicate. Rewriting pushed history (squash) needs explicit maintainer
consent, and any such report states the remote + branch it applies to.
7. **Squashes and history rebuilds must not erase contributor PRs.** Release
lines may squash *our own* work, but every contributor PR whose content
shipped gets a zero-diff record merge at its timeline position (merge
commit with the PR head as second parent, tree unchanged) so the PR shows
as merged and the contributor's commits stay reachable in public history
with their authorship. If the PR's base is a stale internal branch, merge
it there via `gh pr merge` so the badge is true; GitHub refuses a base
change to a branch that already contains the head. Superseded/reworked
PRs are never merged — comment + let the author close. Before force-
pushing a rewritten release branch: back up the pushed tip to Forgejo
(`backup/<branch>-pre-rebuild-<date>`), pass a byte-identical tree-diff
gate against the old tip, re-run CI, and note the rebuild in any open
issue that tells people to build the branch.
## Critical files
+232 -22
View File
@@ -25,11 +25,26 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
# name as an inherited one — CCTVRecord's `pad` — see docs/internals.md.)
# An explicit -DCMAKE_C_COMPILER=... always wins.
if(NOT DEFINED CMAKE_C_COMPILER)
find_program(_GE007_GCC NAMES gcc)
if(CMAKE_HOST_APPLE)
# Homebrew installs real GNU GCC with a version suffix; /usr/bin/gcc is
# Apple Clang and does not support the Plan 9 struct extensions required
# by the decomp's `inherits` pattern.
find_program(_GE007_GCC
NAMES gcc-16 gcc-15 gcc-14 gcc-13 gcc-12 gcc-11 gcc)
else()
find_program(_GE007_GCC NAMES gcc)
endif()
if(_GE007_GCC)
set(CMAKE_C_COMPILER "${_GE007_GCC}" CACHE FILEPATH "C compiler" FORCE)
get_filename_component(_GE007_GCC_DIR "${_GE007_GCC}" DIRECTORY)
find_program(_GE007_GXX NAMES g++ PATHS "${_GE007_GCC_DIR}" NO_DEFAULT_PATH)
if(CMAKE_HOST_APPLE)
find_program(_GE007_GXX
NAMES g++-16 g++-15 g++-14 g++-13 g++-12 g++-11 g++
PATHS "${_GE007_GCC_DIR}" NO_DEFAULT_PATH)
else()
find_program(_GE007_GXX NAMES g++
PATHS "${_GE007_GCC_DIR}" NO_DEFAULT_PATH)
endif()
if(_GE007_GXX)
set(CMAKE_CXX_COMPILER "${_GE007_GXX}" CACHE FILEPATH "CXX compiler" FORCE)
endif()
@@ -37,17 +52,48 @@ if(NOT DEFINED CMAKE_C_COMPILER)
endif()
if(APPLE)
# CMAKE_OSX_ARCHITECTURES is normally EMPTY here (the build targets the host
# arch), so an arm64 Mac used to fall into the 10.13 (Intel) floor and ld
# warned that the brew-gcc aarch64 runtime libs and the generated .s objects
# carry a newer floor than the linked one. D609 (macos branch): also consult
# the host machine when no explicit -arch list was requested. CMake's own
# CMAKE_HOST_SYSTEM_PROCESSOR is EMPTY on macOS (its `uname -p` probe
# returns "unknown" there - same class as D515's stripped-env arch probe),
# so ask `uname -m` directly. 11.0 is Big Sur, the first arm64 release.
if(CMAKE_OSX_ARCHITECTURES MATCHES "arm64")
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "" FORCE)
else()
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "" FORCE)
set(_GE007_OSX_FLOOR "11.0")
elseif(CMAKE_OSX_ARCHITECTURES STREQUAL "")
# No /bin/uname on macOS, and CMake's execute_process looks the name up in
# PATH - so pass the bare name.
execute_process(COMMAND uname -m OUTPUT_VARIABLE _GE007_UNAME_M
RESULT_VARIABLE _GE007_UMRC)
if(_GE007_UMRC EQUAL 0)
string(STRIP "${_GE007_UNAME_M}" _GE007_UNAME_M)
if(_GE007_UNAME_M MATCHES "^(aarch64|arm64)")
set(_GE007_OSX_FLOOR "11.0")
else()
set(_GE007_OSX_FLOOR "10.13")
endif()
endif()
endif()
if(NOT DEFINED _GE007_OSX_FLOOR)
set(_GE007_OSX_FLOOR "10.13")
endif()
set(CMAKE_OSX_DEPLOYMENT_TARGET "${_GE007_OSX_FLOOR}" CACHE STRING "" FORCE)
unset(_GE007_OSX_FLOOR)
message(STATUS "macOS deployment target floor: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif()
# ASM is needed for the generated absolute ROM asset symbols
# (port/src/romassets_<region>.s, see scripts/gen_romassets.py).
project(ge007 C CXX ASM)
# Debug gate: validate every u32 -> host-pointer conversion against the regions
# the port actually maps, and report offenders at the conversion site with a
# backtrace. Off by default (zero cost); enable with -DPORT_ADDR_STRICT=ON.
# Catches the MACOS-SFX-BOUND through MACOS-TEXALIGN truncated/never-rebased-pointer family early.
option(PORT_ADDR_STRICT "Validate N64->host address conversions at runtime" OFF)
include(TargetArch)
include(FindPkgConfig)
@@ -93,6 +139,21 @@ else()
endif()
message(STATUS "Target arch: ${TARGET_ARCH} (64bit=${TARGET_IS_64BIT})")
# Host base of the N64 address window (port/include/portaddr.h, D607). Must be
# set BEFORE add_definitions below, and AFTER target_architecture(): D609 showed
# CMAKE_SYSTEM_PROCESSOR/CMAKE_HOST_SYSTEM_PROCESSOR are EMPTY on macOS, so the
# gate uses TARGET_ARCH (the D609 pagezero gate uses the same test).
# 0 on Windows/Linux; 16 TiB on arm64 macOS (overridable with
# -DPORT_ADDR_BASE=... for tooling, e.g. an ASan build whose shadow region
# overlaps 16 TiB). Fed to the compiler (-DPORT_ADDR_BASE) and to
# scripts/gen_macho_syms.py --base for the romassets absolute symbols, so the
# cart pairing (romdata.c CART_HOST vs romassets_*.s values) agrees.
if(APPLE AND TARGET_ARCH MATCHES "^aarch64$")
set(PORT_ADDR_BASE 0x100000000000 CACHE STRING "Host base of the N64 address window (arm64 macOS)")
else()
set(PORT_ADDR_BASE 0x0 CACHE STRING "Host base of the N64 address window")
endif()
if(WIN32)
set(TARGET_OS "windows")
elseif(APPLE)
@@ -149,7 +210,7 @@ endif()
# Provenance, printed by --version and at startup (port/src/main.c): a build
# keeps pointing at this repository no matter who compiles or renames it.
set(VERSION_CODENAME "larkspur")
set(GE007_VERSION "0.5.0") # bump on every release
set(GE007_VERSION "0.6.0") # bump on every release
set(ORIGIN_URL "https://github.com/jkdansereau/goldeneye-pc-port")
configure_file(
@@ -164,7 +225,18 @@ configure_file(
# the toolchain include dir from the compiler location (MinGW/MSYS2 layout:
# bin/../include) and generate an absolute-path include.
get_filename_component(_PORT_GCC_DIR "${CMAKE_C_COMPILER}" DIRECTORY)
get_filename_component(PORT_HOST_STRING_H "${_PORT_GCC_DIR}/../include/string.h" ABSOLUTE)
if(APPLE)
execute_process(
COMMAND xcrun --show-sdk-path
OUTPUT_VARIABLE _PORT_MACOS_SDK OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
if(NOT _PORT_MACOS_SDK)
message(FATAL_ERROR "Cannot locate the macOS SDK; install the Xcode Command Line Tools")
endif()
set(PORT_HOST_STRING_H "${_PORT_MACOS_SDK}/usr/include/string.h")
else()
get_filename_component(PORT_HOST_STRING_H "${_PORT_GCC_DIR}/../include/string.h" ABSOLUTE)
endif()
configure_file(
"${CMAKE_SOURCE_DIR}/port/include/hoststring.h.in"
"${CMAKE_BINARY_DIR}/port/include/hoststring.h" @ONLY)
@@ -173,7 +245,11 @@ configure_file(
# the include path and shadows the system header; libstdc++'s <thread>/gthr
# headers in the fast3d C++ TUs need the real <sched.h> (cpu_set_t,
# sched_yield). Both MinGW and Linux keep <sched.h> at bin/../include.
get_filename_component(PORT_HOST_SCHED_H "${_PORT_GCC_DIR}/../include/sched.h" ABSOLUTE)
if(APPLE)
set(PORT_HOST_SCHED_H "${_PORT_MACOS_SDK}/usr/include/sched.h")
else()
get_filename_component(PORT_HOST_SCHED_H "${_PORT_GCC_DIR}/../include/sched.h" ABSOLUTE)
endif()
configure_file(
"${CMAKE_SOURCE_DIR}/port/include/hostsched.h.in"
"${CMAKE_BINARY_DIR}/port/include/hostsched.h" @ONLY)
@@ -183,7 +259,11 @@ configure_file(
# one C++ TU (fast3d): MinGW's own sec_api/stdlib_s.h does `#include
# <stdlib.h>`, which resolves to the N64 header via -I. The shim routes C++
# TUs to the host header by absolute path (same trick as hoststring.h).
get_filename_component(PORT_HOST_STDLIB_H "${_PORT_GCC_DIR}/../include/stdlib.h" ABSOLUTE)
if(APPLE)
set(PORT_HOST_STDLIB_H "${_PORT_MACOS_SDK}/usr/include/stdlib.h")
else()
get_filename_component(PORT_HOST_STDLIB_H "${_PORT_GCC_DIR}/../include/stdlib.h" ABSOLUTE)
endif()
configure_file(
"${CMAKE_SOURCE_DIR}/port/include/hoststdlib.h.in"
"${CMAKE_BINARY_DIR}/port/include/hoststdlib.h" @ONLY)
@@ -213,8 +293,13 @@ add_definitions(
-D_LANGUAGE_C=1
-DAVOID_UB=1
-DPAL=${VERSION_IS_PAL}
-DPORT_ADDR_BASE=${PORT_ADDR_BASE}
${REGION_DEFS}
)
if(PORT_ADDR_STRICT)
add_definitions(-DPORT_ADDR_STRICT=1)
message(STATUS "PORT_ADDR_STRICT enabled: N64->host conversions are validated at runtime")
endif()
if(TARGET_IS_64BIT)
add_definitions(-DPLATFORM_64BIT=1)
endif()
@@ -238,7 +323,9 @@ add_compile_options(
# runs the same way; the genuinely understood cases are still fixed properly
# under #ifdef PORT (D189). A hardened Linux build would need the whole class
# swept first — tracked in docs/dev/findings.md.
add_compile_options(-fno-stack-protector)
if(NOT GE_STACK_PROTECTOR_ALL)
add_compile_options(-fno-stack-protector)
endif()
# Keep frame pointers: dbghelp's StackWalk64 (crash handler + thread dumps)
# cannot read DWARF unwind info on MinGW, so the EBP chain is the only way to
# get a real backtrace of a hung/crashed game thread.
@@ -288,14 +375,18 @@ add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-Wno-unused-function>)
# automatic base-pointer conversion the idiom relies on). C only — the decomp
# is C; fast3d C++ does not use `inherits`.
if(NOT MINGW)
add_compile_options($<$<COMPILE_LANGUAGE:C>:-fplan9-extensions>)
# GCC 14 promoted several long-standing C leniencies to hard errors
# (int<->pointer for a known prototype, incompatible-pointer arg types,
# implicit int/function). The WIP decomp still trips these (e.g.
# append_text_picked_up's vestigial u8* params in propobj.c). -fpermissive
# downgrades them to warnings — the C counterpart of the -Wno-error lines
# above, and the documented path for building older C against GCC 14+.
add_compile_options($<$<COMPILE_LANGUAGE:C>:-fpermissive>)
# GCC-only flags: the macOS default compiler is Apple clang (via the
# /usr/bin/gcc c99 shim), which rejects both of these.
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
add_compile_options($<$<COMPILE_LANGUAGE:C>:-fplan9-extensions>)
# GCC 14 promoted several long-standing C leniencies to hard errors
# (int<->pointer for a known prototype, incompatible-pointer arg types,
# implicit int/function). The WIP decomp still trips these (e.g.
# append_text_picked_up's vestigial u8* params in propobj.c). -fpermissive
# downgrades them to warnings — the C counterpart of the -Wno-error lines
# above, and the documented path for building older C against GCC 14+.
add_compile_options($<$<COMPILE_LANGUAGE:C>:-fpermissive>)
endif()
endif()
# ASLR must be OFF: dram_syms.s defines absolute symbols (cfb_16 @ 0x70000000,
@@ -329,6 +420,30 @@ elseif(UNIX AND NOT APPLE)
# relocations are fine (unlike a 0x140000000 base, which needs mcmodel=large).
# This is the ELF analogue of the Windows build's fixed 0x140000000 base.
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-Ttext-segment=0x20000000")
elseif(APPLE)
# Mach-O executables normally reserve [0, 4 GiB) as __PAGEZERO. The x86_64
# (Intel) port needs fixed mappings at 0x10000000 (ROM), 0x70000000 (s32-safe
# DRAM) and 0x80000000 (KSEG0 mirror), so shrink the guard to 64 KiB and keep
# null/low-pointer protection only that far (#88).
#
# D609: on arm64 macOS that flag is FATAL AT EXEC, not at link time. The
# Apple-Silicon kernel enforces the 4 GiB guard region (SVE): an image whose
# __PAGEZERO is smaller than 0x100000000 is SIGKILL'd by the kernel during
# exec, before dyld runs - no output, no crash report, no unified-log entry
# at unprivileged levels, and lldb refuses the image ("Malformed Mach-o file").
# Reproduced with a 6-line C program: `gcc probe.c -Wl,-pagezero_size,0x10000`
# exits 137 (128+9); the same program with the default pagezero exits 7. The
# arm64 port does not need the shrink at all - portaddr.h already shifts the
# whole N64 window up by PORT_ADDR_BASE = 16 TiB, so every required mapping
# (ROM, DRAM V1/K0) lives far above the guard; verified with the same minimal
# probe (shm_open + the two MAP_FIXED mmaps at 0x100070000000 / 0x100080000000
# alias correctly and exit 7). So: shrink on x86_64 only, default on arm64.
if(TARGET_ARCH MATCHES "^aarch64$")
message(STATUS "arm64 macOS: keeping the default 4 GiB __PAGEZERO (arena is at PORT_ADDR_BASE)")
else()
set(CMAKE_EXE_LINKER_FLAGS
"${CMAKE_EXE_LINKER_FLAGS} -Wl,-pagezero_size,0x10000")
endif()
endif()
# port/shim first: PC-only shadow headers (bondtypes.h, bondconstants.h,
@@ -343,6 +458,7 @@ include_directories(
"${CMAKE_SOURCE_DIR}/src/game"
"${CMAKE_SOURCE_DIR}/src/libultra"
"${CMAKE_SOURCE_DIR}/src/libultra/audio"
"${CMAKE_SOURCE_DIR}/src/libultra/gu"
"${CMAKE_SOURCE_DIR}/port/include"
"${CMAKE_BINARY_DIR}/port/include"
)
@@ -396,6 +512,8 @@ if(NOT EXTRA_LIBRARIES)
set(EXTRA_LIBRARIES dbghelp -static-libstdc++ pthread)
elseif(WIN32)
set(EXTRA_LIBRARIES dbghelp -static-libstdc++)
elseif(APPLE)
set(EXTRA_LIBRARIES stdc++ m pthread)
else()
set(EXTRA_LIBRARIES stdc++ m dl pthread)
endif()
@@ -415,14 +533,14 @@ file(GLOB_RECURSE SRC_FAST3D "${CMAKE_SOURCE_DIR}/port/fast3d/*.cpp"
# scripts/gen_romassets.py). Defines every obseg/ramrom/music label and the
# ge007.ld segment markers as 0x10000000 + rom_offset, so `&symbol` / romCopy()
# work once romdata.c maps the .z64 at 0x10000000.
set(SRC_ROMASSETS "${CMAKE_SOURCE_DIR}/port/src/romassets_${ASSET_REGION}.s")
if(NOT EXISTS ${SRC_ROMASSETS})
message(FATAL_ERROR "Missing ${SRC_ROMASSETS} — run: python3 scripts/gen_romassets.py ${ASSET_REGION}")
set(SRC_ROMASSETS_INPUT "${CMAKE_SOURCE_DIR}/port/src/romassets_${ASSET_REGION}.s")
if(NOT EXISTS ${SRC_ROMASSETS_INPUT})
message(FATAL_ERROR "Missing ${SRC_ROMASSETS_INPUT} — run: python3 scripts/gen_romassets.py ${ASSET_REGION}")
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_INPUT "${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.
@@ -443,6 +561,64 @@ if(GE_HIGHARENA_BASE)
set(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} ${_d441_arena_def}")
endif()
# Apple: the two absolute-symbol files are converted to Mach-O spelling
# (scripts/gen_macho_syms.py: .global -> .globl with a leading underscore,
# ELF-style directives dropped). dram_syms.S is C-preprocessed first: it
# pulls the arena bases from portaddr.h (PORT_ADDR_BASE + offset; the header
# self-detects the 16 TiB arm64 base from its own __ASSEMBLER__ branch, no
# -D needed), and the Mach-O assembler does not run the C preprocessor on
# .s input. Only the high-arena test flavour passes a -D override.
if(APPLE)
find_package(Python3 REQUIRED COMPONENTS Interpreter)
# The generated .s files land in a build-dir subdir; CMake creates dirs for
# rules with C/C++ outputs only, so make it explicitly (the cc -E -o below
# fails with "No such file or directory" otherwise).
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/port/src")
set(SRC_ROMASSETS "${CMAKE_BINARY_DIR}/port/src/romassets_${ASSET_REGION}_macho.s")
set(SRC_DRAM_SYMS_EXPANDED "${CMAKE_BINARY_DIR}/port/src/dram_syms_expanded.s")
set(SRC_DRAM_SYMS "${CMAKE_BINARY_DIR}/port/src/dram_syms_macho.s")
add_custom_command(
OUTPUT "${SRC_ROMASSETS}"
COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py"
--base "${PORT_ADDR_BASE}"
"${SRC_ROMASSETS_INPUT}" "${SRC_ROMASSETS}"
DEPENDS "${SRC_ROMASSETS_INPUT}"
"${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py"
VERBATIM)
set(_dram_syms_pp_def "")
if(GE_HIGHARENA_BASE)
set(_dram_syms_pp_def "-DPORT_DRAM_V1_BASE=${GE_HIGHARENA_BASE}")
endif()
# D607: the COMMAND list must stay one CMake argument per argv element:
# quoting the whole "cc -E -I..." as ONE string works on Windows (cmd
# parses the command line) but /bin/sh on macOS/Linux treats the entire
# string as a single filename ("No such file or directory").
add_custom_command(
OUTPUT "${SRC_DRAM_SYMS_EXPANDED}"
COMMAND "${CMAKE_C_COMPILER}" "-E"
"-I${CMAKE_SOURCE_DIR}/port/include"
${_dram_syms_pp_def}
"${SRC_DRAM_SYMS_INPUT}"
"-o" "${SRC_DRAM_SYMS_EXPANDED}"
DEPENDS "${SRC_DRAM_SYMS_INPUT}"
"${CMAKE_SOURCE_DIR}/port/include/portaddr.h"
VERBATIM)
add_custom_command(
OUTPUT "${SRC_DRAM_SYMS}"
COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py"
"${SRC_DRAM_SYMS_EXPANDED}" "${SRC_DRAM_SYMS}"
DEPENDS "${SRC_DRAM_SYMS_EXPANDED}"
"${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py"
VERBATIM)
set_source_files_properties(${SRC_ROMASSETS} ${SRC_DRAM_SYMS_EXPANDED} ${SRC_DRAM_SYMS}
PROPERTIES GENERATED TRUE)
else()
set(SRC_ROMASSETS "${SRC_ROMASSETS_INPUT}")
set(SRC_DRAM_SYMS "${SRC_DRAM_SYMS_INPUT}")
endif()
# Game code — compiled unmodified.
file(GLOB_RECURSE SRC_GAME "${CMAKE_SOURCE_DIR}/src/game/*.c")
@@ -472,6 +648,39 @@ file(GLOB SRC_LIBULTRARE_AUDIO "${CMAKE_SOURCE_DIR}/src/libultrare/audio/*.c")
# gu matrix helpers.
file(GLOB SRC_GU "${CMAKE_SOURCE_DIR}/src/libultra/gu/*.c")
# GCC for Darwin crashes while lowering the decomp's ELF-style
# `#pragma weak alias = target` directives. Compile generated copies with
# those directives removed, then provide their aliases in Mach-O assembly.
set(SRC_MACHO_ALIASES "")
if(APPLE)
set(_MACHO_GENERATED_SOURCES "")
set(_MACHO_WEAK_INPUTS
"${CMAKE_SOURCE_DIR}/src/game/lv.c"
"${CMAKE_SOURCE_DIR}/src/game/objective_status.c"
"${CMAKE_SOURCE_DIR}/src/game/spectrum.c"
"${CMAKE_SOURCE_DIR}/src/libultra/gu/sinf.c"
"${CMAKE_SOURCE_DIR}/src/libultra/gu/cosf.c")
foreach(_INPUT IN LISTS _MACHO_WEAK_INPUTS)
get_filename_component(_NAME "${_INPUT}" NAME)
set(_OUTPUT "${CMAKE_BINARY_DIR}/port/src/macho_${_NAME}")
add_custom_command(
OUTPUT "${_OUTPUT}"
COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_SOURCE_DIR}/scripts/strip_weak_pragmas.py"
"${_INPUT}" "${_OUTPUT}"
DEPENDS "${_INPUT}" "${CMAKE_SOURCE_DIR}/scripts/strip_weak_pragmas.py"
VERBATIM)
list(APPEND SRC_MACHO_ALIASES "${_OUTPUT}")
list(APPEND _MACHO_GENERATED_SOURCES "${_OUTPUT}")
list(REMOVE_ITEM SRC_GAME "${_INPUT}")
list(REMOVE_ITEM SRC_GU "${_INPUT}")
endforeach()
list(APPEND SRC_MACHO_ALIASES
"${CMAKE_SOURCE_DIR}/port/src/macho_weak_aliases.s")
set_source_files_properties(${_MACHO_GENERATED_SOURCES}
PROPERTIES GENERATED TRUE)
endif()
# VI mode table (DATA, not a driver): fr.c references osViModeTable ~15x.
# This is the only libultra/io file we compile — the rest are hardware
# drivers that get shimmed in port/src/libultra.c.
@@ -589,6 +798,7 @@ set(SRC
${SRC_LIBULTRARE_LIBC}
${SRC_ASSETS}
${SRC_GU}
${SRC_MACHO_ALIASES}
${SRC_ENGINE}
)
+23 -4
View File
@@ -10,6 +10,9 @@ repository:
* the port layer port/
* the PC build system CMakeLists.txt, cmake/, build-pc.sh
* PC-port tooling tools_pc/
* macOS packaging scripts/package-macos.sh, scripts/gen_macho_syms.py,
scripts/strip_weak_pragmas.py
* agent-development tooling pi/ (not part of the game build)
* documentation docs/, README.md
Third-party code vendored into the port layer keeps its own license:
@@ -18,6 +21,20 @@ Third-party code vendored into the port layer keeps its own license:
libultraship / sm64-port lineage; see port/fast3d/LICENSE.txt.
* port/fast3d/glad/ - generated OpenGL loader (glad), public domain / MIT.
Runtime libraries bundled in the downloads (not in this repository) keep
their own licenses; each license text ships in the download's licenses/ folder:
* SDL2 (all platforms) and SDL3 (macOS, loaded by sdl2-compat) - zlib license.
* zlib - zlib license.
* Windows: the MinGW-w64 runtime (libwinpthread, libgcc, libstdc++).
* macOS: the GCC runtime (libgcc_s, libstdc++) - GPL-3.0 with the GCC
Runtime Library Exception 3.1. The GPL-3.0 text the macOS package ships
is tools_pc/dist/GPL-3.0.txt.
The license texts the downloads carry come from the build environment where
it provides them, otherwise from tools_pc/dist/ (GPL-3.0.txt,
GCC-RUNTIME-EXCEPTION.txt, SDL2-LICENSE.txt).
Vendored build tooling keeps its own license (none of it is compiled or linked
into the PC port; it belongs to the N64 decompilation build):
@@ -47,11 +64,13 @@ It does NOT cover:
assets -- textures, audio, models, level geometry, in-game text -- are
NOT included and are extracted from your own ROM at build time.
NO NINTENDO / RARE CONTENT IS DISTRIBUTED HERE
---------------------------------------------
NO ROM DATA OR ORIGINAL ASSETS ARE DISTRIBUTED HERE
--------------------------------------------------
This repository contains NO copyrighted ROM data, game assets (textures,
models, levels, audio, text), or Nintendo/Rare code. Building a playable
This repository contains NO ROM data and none of the original game assets
(textures, models, levels, audio, text), and no original Nintendo/Rare
binaries. The C source under src/ is the decompilation community's
reconstruction of the game's code (see above). Building a playable
executable requires a GoldenEye 007 Nintendo 64 ROM that you legally own;
the build extracts the required assets from that ROM on your own machine.
+203 -527
View File
@@ -1,143 +1,86 @@
# GoldenEye 007 PC Port
<p align="center">
<img src="docs/img/icon/ge007-icon.png" width="112" alt="GoldenEye 007 PC Port">
</p>
[![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)
<h1 align="center">GoldenEye 007 PC Port</h1>
**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, and not a binary recompilation: the game's own
reconstructed C, rebuilt from the
[GoldenEye 007 decompilation](https://github.com/n64decomp/007), compiled
for your PC and running from your own ROM.
<p align="center">
The original Nintendo 64 GoldenEye 007 (1997) as a native game for Windows,
Linux, Steam Deck and macOS (Apple Silicon).
</p>
**[Download the latest release](https://github.com/jkdansereau/goldeneye-pc-port/releases)**,
then follow the [Quick start](#quick-start).
The GoldenEye 007 PC Port is a native port built from the game's own
reconstructed source code (the [GoldenEye 007 decompilation](https://github.com/n64decomp/007)),
in the same family as the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark).
It is not an emulator: the game is compiled for your PC and runs from your own
ROM, with native widescreen, 60 fps or more (up to 240 on fast displays),
mouse and keyboard or a modern controller, split-screen for up to 4 players,
and an in-game options menu.
[Quick start](#quick-start) · [Controls](#controls) · [Features](#features) · [Settings and files](#settings-and-files) · [Troubleshooting](#troubleshooting) · [Roadmap](#roadmap) · [Building](#building-from-source) · [Legal](#legal)
### [Download the latest release](https://github.com/jkdansereau/goldeneye-pc-port/releases/latest)
> [!IMPORTANT]
> **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.5.0** 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 using AI coding agents (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).
**This repository and its downloads contain no ROM data and none of the
original game assets. You need your own GoldenEye 007 ROM (US version) to play.**
<p align="center">
<img src="docs/media/goldeneye-gh-preview.gif" width="64%"
alt="~29 s gameplay montage from live v0.5.0 play sessions">
<br><em>All in-engine, running in the port: a ~29&nbsp;s montage from live
v0.5.0 play sessions (the Facility intro, auto-aim in the Archives,
the crosshair options and the Dam sniper rifle, Silo, a 2-player rocket kill,
the Facility bathroom, the Dam ending) ·
<a href="docs/index.md">14 stills in the project index</a></em>
alt="GoldenEye 007 running natively on PC: the Facility, the Dam sniper rifle and split-screen multiplayer">
<br><em>Recorded in the port: the Facility intro, the Archives, the Dam
sniper rifle, Silo, a 2-player match and the Dam ending.</em>
</p>
---
## Table of contents
## News
- [Requirements](#requirements)
- [Quick start](#quick-start)
- [Controls](#controls)
- [What this port offers](#what-this-port-offers)
- [FAQ](#faq)
- [Status](#status)
- [Roadmap](#roadmap)
- [Building from source](#building-from-source)
- [Documentation](#documentation)
- [Contributing and forking](#contributing-and-forking)
- [Credits](#credits)
- [Legal](#legal)
- **2026-10-07** — **v0.5.0** ([release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.5.0)), the first release
since v0.4.0; it includes the v0.4.1 work, which was never published on
its own:
- **2–4 player split-screen multiplayer** on every multiplayer map, each
player with their own pad, controls and aim settings.
- **One options menu** for every setting, laid out like the Perfect Dark
port's, with the game's own crosshair as the mouse pointer. Pick the
game's own N64 control styles (1.1–2.4) per player, or the Xbox release's
presets with per-player pad rebinding; PlayStation and Nintendo pads show
their own button names.
- **Closer to the N64:** fog and haze (Surface 2's ground fog, the sky at
the horizon), automatic-weapon fire rate, sniper and camera zoom, Dam and
Caverns water, the Jungle boss fight's timing, guard visibility in
widescreen, and the Dam ending camera.
- **Fewer crashes and glitches:** a multiplayer crash with the crouched
rifle, rockets passing through the ground, and broken geometry around
Aztec's shuttle.
- **A steady 60 fps on low-end GPUs** (tested on an Intel HD 3000 laptop),
with lighter first-launch defaults on Atom/Celeron-class graphics.
- **Emulator saves load directly:** copy a 1964 or Project64 save in as
`data/ge007.eep` and it is converted on first launch.
- Also: more audio, video and control settings (master volume, audio device,
trilinear filtering, draw distance up to 800%, PC-friendly crouch and
look-ahead defaults), and controllers are picked up again when replugged.
- **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).
- **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
per-level stabilization across the campaign.
## Requirements
---
## System requirements
**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).
**Linux / Steam Deck:** 64-bit x86 Linux with glibc 2.31 or newer (Ubuntu
20.04+, Debian 11+, Fedora 32+, SteamOS). SDL2 is bundled, so
the tarball runs as-is; it sideloads onto a Steam Deck with nothing installed.
**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.
**Not yet:** macOS, ARM Linux and Windows 7 are on the [roadmap](#roadmap).
- **Windows:** Windows 10 or 11, 64-bit. Nothing to install: the download
carries its own runtime libraries and the one-time asset tool.
- **Linux and Steam Deck:** 64-bit x86 Linux with glibc 2.31 or newer (Ubuntu
20.04+, Debian 11+, Fedora 32+, SteamOS). SDL2 is bundled.
- **macOS (experimental):** an Apple Silicon Mac (M1 or later) on macOS 14 or
newer.
- **Graphics:** OpenGL 3.0 (practically any PC GPU from the last fifteen
years). A 2011 laptop with Intel HD 3000 graphics holds 60 fps.
- **Input:** keyboard and mouse, or an Xbox, PlayStation, Switch or other
SDL-supported controller; split-screen needs one controller per extra player.
## Quick start
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.
1. Download the build for your system from
[Releases](https://github.com/jkdansereau/goldeneye-pc-port/releases/latest)
and extract it. On Windows, don't 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; on the file-select
screen the *PC Options* entry opens the same 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 → *Video → Resolution*.
---
`ge007.ntsc-final.z64` (US version; [check your ROM](docs/user-guide.md#whats-a-rom)).
3. Launch the game. The first start takes a few extra seconds while it
prepares the game files from your ROM.
- **Windows:** if SmartScreen appears (the exe isn't code-signed), click
**More info**, then **Run anyway**.
- **Steam Deck:** add the executable as a non-Steam game. It starts in
native 1280x800 fullscreen.
- **macOS:** run `xattr -dr com.apple.quarantine <folder>` once in
Terminal, then start `./ge007.aarch64`
(see the [macOS setup details](docs/user-guide.md#macos-apple-silicon-experimental)).
4. Play. **F10** (controller: **Select**) opens the options menu.
## Controls
| Action | Keyboard / mouse | Controller |
|---------------------|--------------------------|---------------|
| Move / strafe | `W` `A` `S` `D` | Left stick (or D-pad) |
| Move / turn | Arrow keys | — |
| Move / turn | Arrow keys | - |
| Aim / look | Mouse | Right stick |
| Fire | Left mouse | Right trigger |
| Aim mode | Right mouse / `LShift` | Left trigger / LB |
@@ -146,421 +89,176 @@ resolution is wrong, set it under F10 → *Video → Resolution*.
| Crouch (toggles) | `LCtrl` | Left or right stick click |
| Cycle owned gadgets | Watch inventory | B |
| Next weapon | Mouse wheel down / `Q` | Y |
| Previous weapon | Mouse wheel up | — |
| Previous weapon | Mouse wheel up | - |
| Start | `Enter` / `Tab` | Start |
| Options overlay | `F10` | Select |
| Options menu | `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
*Controller* (pick a player first, then *Layout preset*). PlayStation and Nintendo controllers
show their own button names in the menus.
Everything is rebindable in the options menu. Rare's Xbox control styles are
presets, and the **Original** control style turns your pad into an N64
controller. The [user guide](docs/user-guide.md#controls) covers control
styles, rebinding and split-screen.
**Control style:** *Controller → Control style* (named after the watch's own
setting, with Perfect Dark's "Ext" for the port's controls) picks how the
game's controller style (the watch's 1.1–1.4 selector) is treated. **Ext**
(the default) keeps the port's feel and fixes the style itself, so the watch's
selector has no effect. **Original** stops the port from touching the style:
pick 1.1, 1.2, 1.3 or 1.4 on the watch (or in the multiplayer setup) and it
behaves as on the N64, and the Controller page then offers the game's own
styles ("Controller style", 1.1 Honey to 2.4 Goodhead, saved in the Bond file
like the watch's) instead of the layout presets, which apply in Ext only. Buttons are passed
through as the N64's buttons, so 1.3 and 1.4 move fire and aim exactly as the
console does. The 2.x dual-controller styles run faithfully too: the game
reads their second stick from the next controller slot, which the port does not
map yet (a second pad assigned to that slot should provide it, untested), so with
one pad pick a 1.x style. Saves made with earlier builds may hold 1.2 (the port wrote its
forced style back into the save), so when you switch to the Original control style, pick
your style on the watch once.
## What this port offers
**Rebinding:** *Key Bindings* in the options overlay rebinds the
keyboard and mouse (one list: movement, then actions). Controller buttons are
rebindable per controller under *Controller* (pick a player first): 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. There is no warning yet when two
actions share a button, and the Xbox release's "left stick aims the
crosshair" and "D-pad copies the left stick" behaviors are not reproduced
(here the right stick aims and the D-pad strafes, as on the N64).
- **Real 60 fps, not a speed hack.** The game's logic counts time in 60 Hz
ticks. On the N64 it only drew a frame every 2–3 ticks (20–30 fps); here it
draws every tick, so you get 60 fps with the game, physics and AI running
at the N64's own speed. There is no CPU overclock or frame-skip patch involved,
and the few spots in the game that behaved differently at one tick per frame
are fixed to match the N64. Above 60 (90 to 240, or Auto), extra
frames are blended between ticks for smoothness, at the cost of one tick of
latency.
- **Aiming that doesn't feel floaty.** *Aim style* picks the behavior.
**Centered** pins the crosshair in the middle, and the mouse or the right
stick turns the camera directly, like a modern shooter. **Original** keeps
the N64's crosshair that moves a little before the camera follows. The stick
has separate horizontal and vertical sensitivity, separate deadzones for each
stick, look smoothing and a trigger threshold. The mouse has raw input, an aim
curve, smoothing, and separate turn and aim-mode sensitivities.
- **Dual analog and mouse and keyboard, built in.** No emulator remapping. The
right stick looks, the triggers fire and aim, and every button is rebindable,
for the keyboard, the mouse and each controller. Rare's Xbox control styles are
presets. If you want the N64 exactly, the **Original** control style turns the
pad into a strict virtual N64 controller.
- **The whole game.** All 20 solo missions and the credits, with every
difficulty, 007 mode and the cheats, plus split-screen multiplayer for up to
4 players. Your emulator saves (1964, Project64) load directly.
- **Faithful by construction.** It is the game's own reconstructed source
compiled natively, not an emulator and not a remake, so levels, guards and
weapons behave the way the N64 cartridge does. The few changes to game code
either keep that N64 behavior where the PC would otherwise differ (frame
rate, widescreen, compiler) or are opt-in extras, and the **Original N64**
display preset switches every visual improvement off at once.
- **Modern display options:** native widescreen and ultrawide at any aspect ratio, high
resolutions, MSAA, adjustable FOV and draw distance, and HUD scaling.
- **Windows, Linux, Steam Deck and (experimental) macOS** from one download
page, with an in-game options menu on F10 or the pad's Select button.
**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.
Bots, online play, mod support and PAL/JP ROMs are on the
[Roadmap](#roadmap).
**Options overlay with a controller** (also how it works on the Deck):
**Select** opens it, the D-pad or left stick moves between rows and
left/right adjusts the selected value, **A** opens a page or toggles a row,
**B** goes back one page (and closes from the top), and **Start** (or Select
again) closes. With a keyboard, use `F10`, the arrow keys, Enter and
Backspace.
The [user guide](docs/user-guide.md#feature-list) has the full feature list.
## Features
## FAQ
- **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 *Display mode* row (F10 → *Video*) switches
between *Modern* and *Original N64* (it reads *Custom* once you edit a setting it covers),
and *Aspect ratio → Original* shows the exact 4:3 frame.
- **A steady 60 fps**, with VSync and a frame cap (30 or 60), including on
low-end GPUs.
- **Graphics options:** resolution, borderless or exclusive fullscreen,
a Center window action while windowed, 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 centered 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 (also scales the in-game options
menu), an optional always-on crosshair, a custom RGB crosshair color
with adjustable opacity and a health-based color, 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; the game's crosshair is the mouse pointer, and
each page has a *Reset to defaults* row.
- **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), and *Check for updates* (off
by default; one request to GitHub per launch when on, see Security).
- **Saves in a plain file** next to the game, so backing up is a file copy;
1964 and Project64 saves load directly ([details](#using-an-emulator-save)).
**Is this an emulator?**
No. The game's code is compiled natively for your PC, the same way the
Perfect Dark PC port works. Graphics go through OpenGL, and the N64 hardware
is replaced by a small port layer.
**Do I need the game?**
Yes. You need a ROM of the US (NTSC-U) version, dumped from a cartridge you
own. PAL and Japanese ROMs are planned.
**Does it play like the original?**
Yes. Levels, guards and weapons run on the game's own logic at its original
speed. The **Original N64** display preset and the **Original** control style
give you the N64 look and controls whenever you want them.
**Does it run on the Steam Deck?**
Yes. The Linux download is the Deck build: add it as a non-Steam game and it
starts in native 1280x800 fullscreen at up to 90 fps, with the options menu
fully usable on the controller.
**Does it support ultrawide and high refresh rates?**
Yes. Native widescreen works at any aspect ratio, including ultrawide, and the
frame rate goes up to 240 fps on fast displays (Auto matches your monitor).
**Is there online play, bots or mod support?**
Not today. LAN play, bots and mod support are on the [roadmap](#roadmap) as
opt-in extras.
**Can I use HD textures?**
Not in this release. A texture-pack loader is planned for 1.0.
**How is this different from the XBLA recompilation projects?**
Those projects statically recompile the unreleased Xbox 360 remaster, a
separate version of the game. This project ports the original N64 game from
its source code. See [About the project](docs/about.md#how-this-differs-from-the-other-goldeneye-pc-projects).
**Can I keep my saves?**
Yes. Your progress is a plain file, `data/ge007.eep`. Saves from 1964 or
Project64 load directly (see [using an emulator save](docs/user-guide.md#using-an-emulator-save)),
and updating keeps your save and settings (see [updating](docs/user-guide.md#updating)).
**Was AI used to build it?**
Yes. The port was developed with AI coding agents directed by one maintainer.
Releases are checked by comparing captured frames from all 21 solo levels on
Windows, crash sweeps on four platforms, and play-testing.
[How it was made](docs/about.md#how-it-was-made) has the full story.
**Something went wrong.**
See [Troubleshooting](docs/user-guide.md#troubleshooting), or
[open an issue](https://github.com/jkdansereau/goldeneye-pc-port/issues/new/choose)
with `ge007.crash.log`, your OS and GPU. Never attach the ROM.
## Status
**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. v0.5.0 was
played on Windows and Steam Deck (campaign spot-checks, 2–4 player
split-screen and controller-only sessions), and every level's reference
frames were compared pixel by pixel on Windows, Linux and Steam Deck. The
full campaign was last played end to end, at Agent difficulty on all three
platforms, for v0.4.0. Split-screen was played live in 2P on every
multiplayer 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: NTSC-U ROMs
only in the release packages, and no macOS/ARM builds yet.
> ***All unlocked* never changes your save** since v0.5.0: it unlocks every
> level and cheat in memory only, and switching it off shows your real
> progress. v0.4.0 and earlier 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`.
What a release installs (no networking unless you turn on the opt-in update check, which is off by default and makes one HTTPS request to GitHub per launch; no telemetry, no ROM or game assets)
and how faithfully the port tracks the original game's logic:
[Security](docs/security.md) and [fidelity](docs/fidelity.md) status.
The binaries are not code-signed; from v0.5.0 on each release artifact also
carries a GitHub build-provenance attestation you can check with
`gh attestation verify <file> --repo jkdansereau/goldeneye-pc-port`
(see [SECURITY.md](.github/SECURITY.md)).
---
## 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, aim
control, sight, look ahead, ammo on screen, screen size, screen ratio, music
and FX volume) are part of your save, as on the N64. Screen size (Full / Wide /
Cinema) and screen ratio (Normal / 16:9) combine with the PC **Aspect ratio**
setting: Aspect ratio owns the window shape, while the watch's two options
only change what the game renders inside it.
**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-<region>/`, `data/pccg-<region>/` | 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. |
**Removing ROM-derived content:** the only files on your machine that come
from the game are the ROM you put there and the two generated asset folders
(`data/pcmodels-<region>/`, `data/pccg-<region>/`). Delete those and every
ROM-derived file is gone; your save (`ge007.eep`) and settings (`ge007.ini`)
are separate files, yours to keep or delete.
**`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 = 200
[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.
## Using an emulator save
Saves from N64 emulators (1964, Project64, …) load directly: with the game
closed, just copy your `.eep` to `data/ge007.eep`. On first launch the game
detects the emulator byte order and converts it in place, keeping a backup of
the original file next to it as `ge007.eep.emulator.bak` (never overwriting an
existing one). Any slot that is corrupt in both formats is left alone and gets
reset by the game, exactly as before. An existing port save is untouched.
To take a save back into an emulator, use the small converter that ships in
the release folder as `tools/eep_convert.py`
(`tools_pc/eep_convert.py` in a source checkout; plain Python 3, no
dependencies):
```sh
python tools/eep_convert.py pc-to-n64 data/ge007.eep back-to-emulator.eep # port -> emulator
python tools/eep_convert.py verify some-file.eep # which format is this?
```
`verify` checks every save checksum and, without `--as n64|pc`, tells you
which convention the file uses.
## 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 → *Video → 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.**
---
**v0.6.0 is the current release.** The whole campaign, the end credits and
split-screen multiplayer are playable on Windows, Linux, Steam Deck and macOS.
The project is pre-1.0, so expect new features and the occasional change
between versions. Known issues and workarounds are in the
[known-issues table](docs/ROADMAP.md#known-issues);
[About the project](docs/about.md#status-in-detail) has the testing details and
the [release history](docs/about.md#release-history).
## Roadmap
The goal is a **feature-complete, faithful** port: every mode and region the
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:
N64 cartridge ships, with modern options on top that can always be switched
back to the original look and feel. Next up:
- **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.
- **1.0 sign-off**: a full campaign playthrough at more than one difficulty
plus a split-screen session, on every platform that ships.
([#85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)).
- **A notarized macOS build**, Intel Macs and ARM Linux.
- **A texture-pack loader** for HD texture packs.
- **The 1.0 sign-off:** a full campaign playthrough and 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, 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
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://github.com/earendil-works/pi) agent) did the groundwork: build, boot chain,
software-RSP integration, asset pipeline, first frames. **Claude Code**
(Sonnet 5 and Opus 5, now 5.5; Opus 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.5.0: 51
days (16 Aug – 6 Oct), 1,537 commits (the public history squashes the
v0.4.1–v0.5.0 work into 17), and 419 findings root-caused and logged
(`D1`–`D564`).
The full write-up (timeline, handoff mechanism, effort breakdown, an honest
"what worked / what didn't"): [`docs/dev/agentic-development.md`](docs/dev/agentic-development.md).
The workflow itself: [`docs/dev-process.md`](docs/dev-process.md). To cite the
project or its findings: [`CITATION.cff`](CITATION.cff) (GitHub's "Cite this
repository" menu).
## How this differs from the other GoldenEye PC projects
This is a native port of the original 1997 Nintendo 64 game, built from its
actual reconstructed source code, the same lineage as the Perfect Dark PC
port. The other well-known "GoldenEye on PC" projects are something
different: they machine-translate the shipped binary of the *unreleased Xbox
360 XBLA remaster*, a different game on a different codebase with no shared
code with this one.
| | This project | The other well-known "GoldenEye on PC" recompilation projects (and their Steam Deck builds) |
|---|---|---|
| **What it ports** | The original **Nintendo 64** game (1997) | The **Xbox 360 XBLA** HD remaster (built ~2007, never released) |
| **How** | Decompilation-based source port: human-reconstructed C, compiled for the host; game logic runs as written | Static binary recompilation: the shipped machine code is auto-translated to C; the source is machine-generated |
| **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** | 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 |
---
After 1.0, opt-in extras that are always off by default: LAN play, bots,
co-op and enhanced visuals. The full tracker is
[`docs/ROADMAP.md`](docs/ROADMAP.md). If something matters to you, open an
issue; it helps prioritize.
## Building from source
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)
You need CMake 3.16+, a C/C++ toolchain, SDL2, zlib, OpenGL and Python 3, plus
a one-time asset extraction from your ROM. On Windows, build in the MSYS2
MINGW64 shell; on macOS, use Homebrew's GNU `gcc`.
```sh
# in the MINGW64 shell
pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-SDL2 \
mingw-w64-x86_64-zlib mingw-w64-x86_64-cmake \
mingw-w64-x86_64-python make git
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: see docs/building.md
```
### Linux
The Linux build is compiled on every push by CI (ubuntu-24.04); the release
bundle additionally bundles SDL2 so no system packages are needed at runtime.
```sh
sudo apt install build-essential cmake python3 libsdl2-dev zlib1g-dev libgl1-mesa-dev
git clone https://github.com/jkdansereau/goldeneye-pc-port.git
cd goldeneye-pc-port
# extract assets (docs/building.md), then:
# extract assets from your ROM first (see docs/building.md), then:
./build-pc.sh ntsc-final
```
The executable is written to `build-pc/ge007.x86_64` (on Windows,
`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 game code in `src/` is the decompilation's, and its control flow is the
reference we preserve. Everything that would touch N64 hardware is redirected into
`port/`: a **software RSP** (`port/fast3d/`, adapted from the Perfect Dark
port) that interprets the graphics command stream (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.
Every port change inside `src/` is gated behind `#ifdef PORT` and
documented. They are few in kind, all narrow in scope: mechanical 32→64-bit
pointer/struct fixes (the pointer-width transition touches many structs, so
there are a lot of them), 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), the opt-in *All unlocked* hook, and env-gated diagnostic
probes tied to the bug catalogue (off by default, no behavior change). Where it diverges from the Perfect Dark port: GoldenEye's N64
serialized asset formats are converted in a separate step rather than
fixed up at load time — on first start for a download, or by the offline
Python converters in `tools_pc/` for a source build. Full detail:
[`docs/internals.md`](docs/internals.md) and
[`docs/porting-notes.md`](docs/porting-notes.md).
```
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); port edits are #ifdef PORT
port/
fast3d/ software RSP -> OpenGL
src/ port layer (main, OS shims, video, audio, input, fs, ...)
include/ port-facing headers
tools/ Makefile the N64 build + asset extraction (from the decomp; do not modify)
tools_pc/ PC-port helper + analysis scripts
docs/ see below
```
[`docs/building.md`](docs/building.md) has the full walkthrough for Windows,
Linux and macOS.
## Documentation
Key docs are also published as a site:
The docs are also published as a site:
<https://jkdansereau.github.io/goldeneye-pc-port/>.
| 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, 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. |
| [User guide](docs/user-guide.md) | Controls, settings and files, ROM check, macOS setup, updating, emulator saves, troubleshooting. |
| [About the project](docs/about.md) | Release history, testing status, how it was made, how it works. |
| [Roadmap](docs/ROADMAP.md) | Open work and the known-issues table. |
| [Building](docs/building.md) | Full build and asset-extraction guide. |
| [Internals](docs/internals.md) | Architecture and the software RSP. |
| [Porting notes](docs/porting-notes.md) | The recurring N64-to-PC bug classes, with fixes. |
| [Security](docs/security.md) and [Fidelity](docs/fidelity.md) | What a release installs, and how closely it tracks the original game's logic. |
| [`docs/dev/`](docs/dev/) | The engineering record: the finding log and development process. |
## 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.
---
the port faithful, what to check before a pull request, and what a fork is
asked to keep.
## Credits
@@ -606,50 +304,26 @@ This port is a thin layer on a large amount of other people's work.
**Contributors**
- **dolent** (#120, #121, #122, #123), **italoarruda** (#107, #109),
**JosephAHK** (#133), and **MST246** (the #125 investigation), plus
everyone who filed issues.
- **dolent** (#120, #121, #122, #123), **italoarruda** (#106, #107, #108, #109),
**danturn** (#95 macOS, #96), **Julio C. Rocha** (#88 macOS Intel groundwork),
**f1zz1ec0ke** (#137 frame interpolation), **JosephAHK** (#133), and
**MST246** (the #125 investigation), plus everyone who filed issues.
**Consultation**
- **f1zz1ec0ke** ([GitHub](https://github.com/f1zz1ec0ke)): LLM consultation on model
tuning, agent harnesses, and agentic strategy throughout the port's development.
tuning, agent harnesses, and agentic strategy throughout the port's development
(and the author of the #137 frame-interpolation design noted above).
**Tools and models used to develop the port**
Development ran for most of its life on a single local model — **Unsloth's
Qwen 3.8 27B** — which did nearly all of the local (groundwork) work on its
own. In the final week that grew into a small menu, and the **current standard
is NInfer + Strata**; tools are chosen per task, with local models carrying
the high-volume work and hosted models the exception. The full record of the
models used, and the harnesses that drove them:
| Model | Ran as | How it was used |
|---|---|---|
| **Qwen 3.8 27B** ([Unsloth](https://huggingface.co/unsloth/Qwen3.8-27B-GGUF)) | `UD-Q4_K_XL`, one RTX 5090 | The primary local workhorse for the bulk of the port's development. |
| **Qwen 3.8 27B** via [NInfer](https://github.com/natpate/ninfer-windows) (Windows) | base, NVFP4 and NVFP4-full artifacts; MTP (DFlash2) spec-decode; one 5090 | Trialed in the final week, now half of the standard; the fast day-to-day local driver. |
| **Qwen 3.8 27B** ([ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-27B-GSQ-RCO-GGUF)) | `IQ3_S` (GSQ-RCO), larger context | Briefly trialed for larger-context workloads; shelved, little use since v0.2.0. |
| **Qwen 3.8 Flash-Next** via [Strata](https://github.com/Niko1221/Strata) | 125B MoE, `IQ1_M` (1-bit), one gaming GPU | The big-context local tier: a model that normally needs a server, run locally for the largest-context passes. |
| **Claude** (Anthropic) | Sonnet 5 / Opus 5, now **Sonnet 5.5 / Opus 5.5** (out in the last few weeks) | The orchestrator in the two-agent phase: held what the 27B couldn't close, took over on limit handoff; the 5.5 tier has driven the most recent work. |
| **GPT-6 (Sol)** (OpenAI), via [OpenRouter](https://openrouter.ai) | a frontier hosted model | Trialed. |
*Harnesses and tooling:*
- **[pi](https://github.com/earendil-works/pi)** — the local coding-agent
harness that drives the local models.
- **[Claude Code](https://github.com/anthropics/claude-code)** (Anthropic) —
the agentic harness that drove the Claude models above; the orchestrator in
the two-agent phase.
- **[claude-code-delegate-local](https://github.com/fegone/claude-code-delegate-local)** —
the MCP bridge that let Claude and the local models talk (delegating Claude's
subagents to the local engines).
The models and tools used to develop the port are listed in
[About the project](docs/about.md#tools-and-models-used-to-develop-the-port).
## 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 is **not affiliated with, endorsed by, or sponsored by** Nintendo,
Rare, Microsoft, Valve, MGM, Danjaq, EON Productions, or any rights holder in
Rare, Microsoft, Valve, Apple, MGM, Danjaq, EON Productions, or any rights holder in
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.
@@ -657,8 +331,10 @@ 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.
- **Runtime libraries:** SDL2 (and SDL3 on macOS) and zlib under the zlib
license, the MinGW runtime on Windows, and the GCC C++ runtime on macOS
(GPL-3.0 with the GCC Runtime Library Exception); 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
@@ -699,4 +375,4 @@ is covered by [`NOTICE`](NOTICE), not by that license.
---
*Last updated 2026-10-07 — v0.5.0 · <a href="https://github.com/jkdansereau/goldeneye-pc-port">GitHub</a> · <a href="https://github.com/jkdansereau/goldeneye-pc-port/releases">Releases</a>*
*v0.6.0 · 2026-10-13 · <a href="https://github.com/jkdansereau/goldeneye-pc-port/releases">Releases</a>*
+5 -3
View File
@@ -197,7 +197,7 @@
// These are compile time extern constants (so not "really" const), but used the same
// as the defines above, which is to index into ptr_animation_table.
#if defined(__x86_64__)
#if defined(__x86_64__) || (defined(__aarch64__) && defined(__APPLE__))
/* D34 (docs/dev/findings.md): on N64 this segment links at VMA 0
* (ge007.ld), so &ANIM_DATA_x is the record's offset within the
* animation_data segment and game code re-bases it with
@@ -207,7 +207,9 @@
* image base, set by romdataInit); (s32)&ANIM_DATA_x then equals
* PTR_ANIM_x exactly. Address-only: the record data itself is romCopy'd
* from ROM into ptr_animation_table, and assets/animationtable_data.c is
* not compiled into the PC image. Never dereference these lvalues. */
* not compiled into the PC image. Never dereference these lvalues.
* D608: the arm64 macOS image loads at the 16 TiB window base
* (PORT_ADDR_BASE) — same re-basing class, mechanical guard widening. */
extern unsigned char *g_pc_animdata_base;
#define ANIM_DATA_empty (g_pc_animdata_base[0x0u])
#define ANIM_DATA_idle (g_pc_animdata_base[0x1cu])
@@ -572,7 +574,7 @@ extern s32 ANIM_DATA_credits_natalya_kissing;
extern s32 ANIM_DATA_helicopter_cradle;
extern s32 ANIM_DATA_plane_runway;
extern s32 ANIM_DATA_helicopter_takeoff;
#endif /* __x86_64__ (D34) */
#endif /* __x86_64__ / arm64 macOS (D34, D608) */
+58 -18
View File
@@ -92,8 +92,11 @@ two stages:
**Stage B: pre-1.0 build → 1.0**
1. PAL/JP (§1). It goes last on purpose, so that region-specific
regressions are easy to isolate and unwind.
2. macOS/ARM: merge the `macos` branch (§4), with the D441 census
(store-to-u32 tagging) and a high-arena test.
2. macOS/ARM: merge the `macos-arm64` branch (§4), with the D441 census
(store-to-u32 tagging) and a high-arena test. (2026-10-08 decision: an
*experimental* arm64 macOS build ships in v0.6.0; the merge-to-main is
still the post-1.0 step here. Scope is Apple Silicon — the x86_64 path is
unverified and out of release scope.)
3. The exit gate on every shipped platform.
4. Remove the temporary scaffolding (§7).
@@ -118,10 +121,14 @@ below).
|---|---|---|---|
| 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 |
| Changing Aspect ratio in a level (mostly Fill window to Original or a forced ratio) can briefly glitch the gun / hand model, rarely | Cosmetic, one-off | Change the ratio from the front-end PC Options, or accept it | D509 (frame hold fixed the common case; the rare remainder is logged, not investigated) |
| No macOS or ARM builds | Platform | — | #88, #95, #101, §4 |
| No Intel macOS or ARM Linux builds (the macOS download is Apple Silicon only, experimental and not notarized) | Platform | — | #88, #95, #101, #154, §4 |
| Steam Deck at 90 fps: centre firefights in Bunker 1/2 drop several fps; Jungle feels slightly less smooth than other levels | Brief FPS drops | Frame cap 60 for those levels | D583, D585 (present thread, v0.7.0), D614 |
| Cradle's turret explosions may draw wrongly | Cosmetic | None yet | Deck per-level pass (2026-10-10), not investigated |
| Windows: the picture can look hazy for a moment after alt-tabbing back; fast repeated alt-tabbing recovers slowly | Cosmetic, temporary | It settles on its own | D578, alt-tab row in §4 (v0.7.0) |
| Saves from v0.4.0 and earlier can hold fake unlocks from `All unlocked` | Save data | Not repaired automatically. Since v0.5.0 the option never writes the save; keep a backup of `data/ge007.eep` from before you used it | D442, D387 |
| 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 in fog are now hidden as on the N64, except in widescreen, where the wider view can still show a faint distant building edge (e.g. Surface's dish from the start area). 4:3 matches the N64 | Cosmetic | None; higher Draw/LOD distance shows more | D503, §5c |
| Windows: an occasional one-off render-thread stall can trip the "no frame rendered for N ms" watchdog (rare; observed once, during heavy `GE_`-debug I/O) | Brief one-off stall / recover | Normal play (no `GE_` dump probes) is unaffected; a flake, not a deterministic hang | D593 (root-cause owed: probe I/O cost vs a genuine render-thread hang) |
---
@@ -132,7 +139,7 @@ below).
| **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 (D496 fixed an uninitialised weapon-text buffer). **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 byte-verified against real PAL and JP ROMs); both play Dam with no crash; NTSC-U unchanged. Build: `BUILD_DIR=build-pal ./build-pc.sh pal-final` / `build-jpn ... jpn-final` with `data/ge007.<region>.z64`. D487, D488 fixed (PAL frame height, TV type). **Open:** full runtime passes per region (levels, menus, audio, saves; PAL 50 Hz feel incl. the D427/D451/D486 gates; EU-only text); packaging (per-region exe names, CI matrix, converter region gate, #6). ROMs: `ge-port-reference/1964_GEPD_Edition/ROMs`. #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. |
| 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. **2026-10-09 (maintainer):** the Tier-2/3 items whose data is already in the ROM — the 8 cut MP maps and the extended MP roster (backlog §D/§F) — are in 1.0 scope as a mod-fork branch (the `front.c` table edits touch `src/game`, non-negotiable #2), shipped through the mod scaffolding (§5a). Prop placement stays out. |
## 2. Accuracy (behaviour differs from the N64)
@@ -143,10 +150,19 @@ below).
| Dam ending cutscene | done (2026-10-06) | D526, D552 | Railing/grate transparency faithful (D526); first-shot swivel restored (D552, maintainer-verified). |
| Title-screen attract demo runs too fast; F10 box rescales while it plays | backlogged (after v0.5.0, maintainer 2026-10-04) | D87 (ramrom file struct); timing: D13, D248, D427, D451, D486 | **Reported 2026-10-04 (live pass):** attract plays visibly too fast; not investigated. Lead: it is a ramrom demo replay whose recordings carry a per-frame tick count, which the port's 60 fps / 1-tick pacing may override. Rule out PC causes, then compare with 1964. Same pass: the F10 box briefly rescaled during the demo (not reproduced; candidate fix = overlay reads fast3d's native viewport, parked locally). |
| 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. |
| Geometry disappears when hugging a wall (dark close-range smear) | verify (fixed 2026-10-09; golden sweep owed) | #150, D579, D543 | **Root cause (2026-10-09):** a v0.5.0 regression from D543, the CPU near-plane clip. It cut at the near plane (`z = -w`), but GL runs with `GL_DEPTH_CLAMP`, so geometry between the eye and the near plane is meant to be drawn (as on the N64; 1964 shows the wall). Hugging a wall put it in that band and the clip deleted it. Confirmed by one `GE_NEARCLIP=0` run (no see-through). **Fix:** the clip now cuts at the camera plane (`w = 0.01`), and clip-created vertices take fog at `max(z/w, -1)` so the D553 near-plane haze is unchanged. Maintainer-verified at the Dam gate in 16:9 with the floor haze intact. The earlier cull/ucode/guard-band investigation was a dead end (see D579 and dev-process §8 "Regression triage first"). **Owed:** one 21-level golden sweep. Diagnostics stripped (last one, the `src/game/bg.c` probe, removed 2026-10-10). The `any_w_neg` no-cull caused the Cradle catwalk flicker; mixed-w triangles now get the homogeneous facing test (D584), Dam gate wall hug re-checked clean. |
| Decal shadows flicker at high FPS (Dam, Cradle catwalks) | closed 2026-10-09 | D578 | Maintainer compared Dam against 1964 (GEPD): the shadow look there is essentially the same, so it is faithful, not a port bug. The flicker seen during the D578 switch testing is not considered an open issue. Reopen if the Cradle catwalk flicker shows up on the shipped default. |
| 120 fps + VSync on G-Sync/VRR: OLED brightness flicker (present cadence); Deck 90 smoothness | PC fixed 2026-10-09; Deck 90 good on most levels (per-level pass 2026-10-10, `feat/d583-deck90`, local); open items in the per-level row below | D583, D578 | PC: continuous present grid snapped to tick/N, 8-slot present ring, draw-time-based ready, present-before-draw (120.0 presents/s, 99%+ gaps 7-9.5 ms, OLED by eye OK). Deck: false turn-detection redraws removed, exact frames drawn once, turn softening applied at frame start, room scissor = union of replayed and game box, projections paired by nearest (draw peak 8-10 -> 3-5 ms, 88-90 presents/s; maintainer by eye OK); no post-swap glFinish under gamescope (present 3.3 -> 0.3 ms); set-change limit 60%. Not yet a smooth 90 in crowded fights (maintainer: "not perfect for the release yet"): set-change holds and turn-softening steps remain; cut detection replaced the matrix set-change check (crowd fights "good now"). Next: re-tune softening (Dam tunnel by-eye), per-level Deck pass, Dam truck brief flicker (Deck, 2026-10-09), Aztec M16 viewmodel flicker, rare dead-body flicker before fade-out, level-start load stutter, gamescope flip-time stats. PC 120 re-checked 2026-10-09 (120.0/s, 1% exact). Owed: VSync-without-G-Sync check, per-level Deck pass + 90 fps performance validation across levels (only Dam exercised). **2026-10-10 Deck Dam run (ac50865d):** 9.5-12 ms gaps 98-99.7% per 10 s window, 0-5 dropped, draw peak 2.1-6.3 ms, late softened (redrawn) 8-56 per 10 s, maintainer by eye: good, no tunnel holes. **Turn-softening re-tune closed as not needed** (the redraw count no longer costs presents). **2026-10-10:** per-level Deck pass done (next row); the older "Next" list above is superseded by it, except the still-unexplained Dam truck / Aztec M16 / dead-body flickers (captures owed), level-start stutter, gamescope flip-time stats and the VSync-without-G-Sync PC check. |
| Deck per-level pass findings (2026-10-10, maintainer, `ac50865d`) | open | D583, D578 | Overall very strong. (1) **Bunker 1/2 centre firefights drop 5-10 fps:** the log shows a ~70-frame run of 2-pass frames drawing 11.2-12.5 ms (~5.6 ms per pass, over the 11.1 ms 90 Hz budget); game side is idle (work ~0.14 ms), so this is draw cost, a real-optimization job (profile the pass), not pacing. **2026-10-10 measured (SLOW line counters):** ~550-800 batches per pass at ~6 us each (~4-5 ms per pass); texture lookups ~1.2 per batch, few misses; a softened restart made fight frames 3 passes. Refuted: same-texture rebinds (reverted). Landed: late-soften budget gate (no restart when n+1 passes exceed 3/4 tick; `nobudget` = old): 3-pass frames mostly gone, but drops remain (2-pass frames to ~13 ms; maintainer: still up to ~10 fps drops). **2026-10-10 late (D583 addendum "present scheduling"):** NOT draw cost: not CPU-bound (perf), not the GPU clock (1600 MHz manual: same), not the VBO upload (ring: within noise). 72% of the long present gaps are a due present stuck behind the single worker's next draw. **2026-10-10 review: present thread (D585) PARKED for v0.7.0** on local `wip/d585-present-thread` (opt-in, offscreen frames black): a second shared GL context + thread on every platform, macOS included, for one Deck scenario was too much risk for a point release. v0.6.0 ships with the Bunker fight drops as a known Deck issue. **2026-10-10 (D614):** texture-cache thrash measured in the fight (CI8 keys hashed leftover palette entries); fixing it cut re-imports 60-70% but did not change pacing: reverted, not the cause. (2) **Jungle/Egyptian read 90 fps but do not feel fully smooth:** motion histogram shows 3-10 hold+jump pairs per 10 s (~0.5-1 visible hitch/s) even with 0 dropped; next step is to log which decision (room-change exact, late exact, pre-soften) precedes each hold. **2026-10-10 hold-cause log (6d7aa851), Jungle:** every hold is an exact fallback (1-7 per 10 s), almost all REACH (a room the previous frame drew and this one dropped); `noreach` removed them with no visible holes in Jungle but did NOT change the feel (maintainer: "not significantly"; Egyptian and other levels feel better). So holds are not the Jungle issue and the pacing stats look like the good levels. Untested lead: content refreshed only at the game tick inside a 90 Hz blended view (the sky is always this frame's; objects not paired), most visible outdoors. Needs a focused look, not more switch rounds. (3) **Clip-through at long range (FIXED 2026-10-10):** Frigate terminal shadow through a wall, Statue Valentin through his crate; above 60 fps only. Cause: the D578 in-between depth-clip widening (`gfx_interp_conservative_clip`, heading-dependent depth remap). Gone on the Deck with `noclip`; now default OFF (`interp_clip_off`; `GE_INTERPCLIP_ON` / `clipon` opts back in). Owed: Dam distant-ridgeline check without it (the reason it was added; maintainer deferred it 2026-10-10). (4) **Cradle catwalk shadow flicker (FIXED 2026-10-10, D584):** a regression from D579 at every frame rate (bisected on the Deck); mixed-w triangles get the homogeneous facing test again. (5) **Cradle turret explosions may draw wrongly** (maintainer, 2026-10-10): noted only, not investigated. |
| Native widescreen: stale watch pixels at the sides of 4:3 menus (mission folder, file-select background, main menu after exiting from the watch) | fixed (2026-10-10, ac50865d; maintainer Deck check OK) | D579, D334 | Not interpolation (present with `allexact`). D579 (#150) stopped the per-frame canvas color clear to match N64 persistent VRAM and clears only the letterbox bars; with Native widescreen the canvas is the full 16:10 window, but 2D menus draw only their 4:3 core, so the canvas sides are never drawn or cleared and keep the watch. Fix direction: keep the no-clear where the game draws, clear the widescreen extension outside the 4:3 core (or on 2D/menu frames). Regression from D579 (this week), not in v0.5.0 releases. **Fix 2026-10-10 (`gfx_frame_body`, D579 follow-up):** outside a running stage (`current_menu != RUN_STAGE`) with native widescreen wider than 4:3, the canvas strips outside the centred 4:3 core are cleared to black before the DL, like the letterbox bars; in-stage frames keep the no-clear. |
| Dam: grate decals vanish behind a sliding edge, wall lighting shifts with view; guard tower window/doorway view of the outside glitches | closed 2026-10-09: faithful | D583 | Present with every present exact (`allexact`) and in v0.5.0; maintainer checked 1964 (GEPD): the exact same behaviour. Original-game portal/culling artefacts, not port bugs. Candidate for the opt-in fixes row below. |
| Opt-in fixes for original-game rendering bugs (QoL) | idea, post-1.0 (maintainer 2026-10-09) | D583 | Maintainer: a nice QoL feature would be optionally fixing artefacts the original game has, e.g. the Dam wall grates cut by a sliding portal edge and the guard-tower window/doorway outside view (D583 row above), and overlapping 2D transparent textures on Cradle sorting wrongly (also in the original). Must stay off by default (fidelity is the default) and live in `port/` or an explicit option; game-logic changes would need Rule-2 sign-off. |
| Exclusive fullscreen picks the wrong resolution with NVIDIA "OpenGL present method = Prefer native" | open (v0.7.0, maintainer 2026-10-10) | D583 | Seen 2026-10-09 while bisecting D583; borderless is fine. Probably exclusive mode switches to the ini Width/Height instead of keeping the desktop mode. Not investigated. |
| Deck golden captures are 1280x800 when interpolation is on | open (v0.7.0, maintainer 2026-10-10) | D578, D283 | With Auto FpsCap on the Deck's 90 Hz screen, GE_PCDUMP writes 1280x800 frames against the 640x480 Deck goldens, so every frame fails to compare (and the sweep prints PASS with "stems don't match", which is not a pass). **2026-10-11:** the false PASS is fixed: `framediff.py` exits 3 on a size mismatch and `verify.sh` reports it as REGRESSION ("not compared"); the capture-size fix itself is still owed. `verify.sh` now pins `Video.DeckPresetApplied = 1` (D283 preset). Fix: dump at the window size, or re-capture the Deck goldens once. |
| 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; AUDIT-M6 `struct player`/`struct hand` raw offsets (porting-notes §A1) | **State (2026-10-03, D441):** complete census (local `scratch/d441/`; many hits are intentional `(s32)&ANIM_DATA_*` offsets, D34); 17 files widened to `uintptr_t` under `#ifdef PORT`; 35 sign-extension sites fixed; CI ratchet `tools_pc/abi_ratchet.py` (baseline 389); arena windows centralised (`port/include/portaddr.h`); tier-1 high-arena build (`GE_HIGHARENA_BASE=0x90000000`) runs the menu and `-level_09` pixel-identical to default. D478: nothing live on the current layout; the rest would bite only on macOS/ARM/ASLR. **Sweep (2026-10-05):** tier-1 all-level sweep done, 21/21 PASS, no fix batches, ratchet unchanged (389), recipe in the D441 entry. **Owed:** tier 2 (above 4 GiB; u32 slots widened); store-to-u32 vs arithmetic-only tag per site (`portHostToN64` under macOS port_addr); `mema.c` widening (D453); `initanitable.c` heap slots; GE_DETERM wall-clock timers (D117 follow-up, blocks using determinism mode for frame gates). **#108:** widening the `sndPlaySfx` bound disables the guard everywhere; prefer a range check. |
| Latent 64-bit ABI hazards | partial | D441, #107, #108; AUDIT-M6 `struct player`/`struct hand` raw offsets (porting-notes §A1) | **State (2026-10-03, D441):** complete census (local `scratch/d441/`; many hits are intentional `(s32)&ANIM_DATA_*` offsets, D34); 17 files widened to `uintptr_t` under `#ifdef PORT`; 35 sign-extension sites fixed; CI ratchet `tools_pc/abi_ratchet.py` (baseline 389); arena windows centralised (`port/include/portaddr.h`); tier-1 high-arena build (`GE_HIGHARENA_BASE=0x90000000`) runs the menu and `-level_09` pixel-identical to default. D478: nothing live on the current layout; the rest would bite only on macOS/ARM/ASLR. **Sweep (2026-10-05):** tier-1 all-level sweep done, 21/21 PASS, no fix batches, ratchet unchanged (389), recipe in the D441 entry. **Owed:** tier 2 (above 4 GiB; u32 slots widened); store-to-u32 vs arithmetic-only tag per site (`portHostToN64` under macOS port_addr); `mema.c` widening (D453); `initanitable.c` heap slots; GE_DETERM wall-clock timers (D117 follow-up, blocks using determinism mode for frame gates). **#108:** widening the `sndPlaySfx` bound disables the guard everywhere; prefer a range check — **done in v0.6.0 as D573** (address-window check). |
| Port-memory vertex/pool bounds hardening | post-1.0 (decided; not a v0.5.0 blocker) | D545, D549 | D545 (rockets through floor, fixed) left one residual: ~1 dropped rocket in ~60 spawns (Archives, akimbo), consistent with the room-pool hypothesis (`bgBuildRoomVtxBounds` allocation failing leaves no bounds) but unproven; D549 (same stack-overflow class in grenade/knife/object throws) is fixed. Both shipped. Proposed hardening: a port-memory bounds table validating vtx/pool pointers against the known arena windows. |
## 3. Verification debt (fixed; a live check is owed)
@@ -175,12 +191,19 @@ A single campaign pass with these on the checklist would clear most of them.
| Item | Status | Refs | Notes |
|---|---|---|---|
| macOS (Intel + Apple Silicon) / ARM64 Linux | open (target 0.9.0/1.0, maintainer 2026-10-02) | PRs #88, #95; #101 | Outside drafts, parked until after 1.0. **Address model decided (2026-09-30): port_addr (#95)**, a `portN64ToHost`/`PORT_N64PTR` chokepoint with a `PORT_ADDR_STRICT` validator. The `macos` integration branch is on origin with #88 (merged), #132 and #95 (open); nothing merges to `main` before the post-1.0 pass. **Open against `macos`:** #131 (self-hosted sweep scaffold, never run on a Mac; needs #95 first). **On the next rebase onto `main`:** take #131's `selfhosted.yml`; drop #95's `crouched_rifle` hunk (our #120/D445 is correct); relabel #95's D29x/D405-D421 labels. Needs the §2 ABI-hazard sweep; the maintainer tests firsthand before merging. |
| Platform parity gate (Linux co-equal with Windows) | open | — | **Decided 2026-09-30:** yes. Every released platform carries the same gate: a 21-level sweep plus a campaign pass. Needs the golden baselines on both platforms first (§7). The last known Linux crash (D190) was closed as not-a-bug. |
| macOS (Apple Silicon) / ARM64 Linux | open (target 0.9.0/1.0, maintainer 2026-10-02; **scope 2026-10-08: Apple Silicon only, experimental in v0.6.0**) | PRs #88, #95; #101 | Outside drafts, parked until after 1.0. **Address model decided (2026-09-30): port_addr (#95)**, a `portN64ToHost`/`PORT_N64PTR` chokepoint with a `PORT_ADDR_STRICT` validator. The integration branch is `macos-arm64` (renamed 2026-10-08 from `macos`; local + Forgejo mirror) with #88 (merged), #132 (merged) and #95 (**merged 2026-10-10** — record merge `903095c9` in the v0.6.0 line + base merge `f87f4bcd`; the stale GitHub `origin/macos` mirror was deleted 2026-10-10, its lineage stays reachable via the record merge); nothing merges to `main` before the post-1.0 pass. **Scope decision (maintainer 2026-10-08): ship Apple Silicon only** — the x86_64 macOS path (#88) stays in-tree (identity at `PORT_ADDR_BASE==0`) but was **never verified on Intel hardware** and is out of release scope; **arm64 macOS is planned as *experimental* in v0.6.0** (the first release carrying a macOS build; owed before the release: D602 diagnosis, D600/D601 live confirmations, and a release-notes experimental marker on the `macos-arm64` artifact). **#131 (self-hosted sweep scaffold) closed 2026-10-10** — its content shipped in the v0.6.0 CI; the fork-friendly rework is parked locally (`park/ci-selfhosted-sweep-131`, `2fb8e52e`) for the v0.7.0 self-hosted runner item. **Status 2026-10-08:** D590 fixed the deterministic Windows `load_bg_file` SIGSEGV (the D588 bug class on the full-width `bg_addr_t` — plain casts, 5/5 Dam runs clean; the D589 A/B's Win side is unblocked); **D589's rev32 is ROOT-CAUSED as D591**: the decode-time pool is 134/134 byte-identical Mac/Win (the writer side is clean — the “pool holds the rev32” line is retired), and the reversal happens at the *import* — `gfx_tex_source_is_c_array` (`port/fast3d/gfx_pc.cpp`) mixes host-space and N64-raw-space in its range test, so on arm64 macOS (`PORT_ADDR_BASE=16 TiB`) every DRAM/cart texture source mis-classifies as a “C-compiled array” and `gfx_tex_normalize_source` bswaps it (a clean rev32 of the whole wide-pixel pool — the D219/M-114 pink-HUD symptom class). Fix in `9c59cbe7` (compare the host pointer directly; port-layer only, no-op at `PORT_ADDR_BASE==0`); **Windows verified byte-identical (400/400 import `r*.bin` + PPM unchanged), Mac A/B now verified 2026-10-08** (clean arm64 build at `25f92e09`: `GE_TEXN carray=0` on all probe lines, Mac `raw8` matches the Win baseline — the rev32 is gone). **D589 is now RESOLVED: the "second cause" (D592) was CLOSED as a non-issue 2026-10-08.** The "rock striping" that D592 tracked turned out to be a **false positive of a crude vertical-high-frequency (VHF) metric flagging natural rock grain**, *not* a GL-layer artifact: a `GE_TEXSTATE` GL-state probe showed the effective sampler state **and** the NPOT mip level counts are **byte-identical Win/Mac** (the Apple M3/Metal backend builds the full NPOT mip chain; the aniso extension is present on both; the NPOT rock texture gets `min=trilinear, mag=linear, aniso=4` on both sides), a 0-pink scan confirmed **D591 held**, and the **maintainer's live Mac look showed the Dam rendering clean** (no visible striping, ammo counter not pink, F10 menu fine). The NPOT `glGenerateMipmap` guard + companion single-level sampler fix (`9bed5904` / `82dbea9c`) were **reverted as dead weight** (`b80dcd08`, which also strips the `GE_TEXSTATE` probe). The genuine cross-platform symptom (the pink HUD) was **D591 (fixed, `9c59cbe7`)**. **D594 (FIXED): macOS partial input** — `GE_KEYLOG` **disproved** the cross-thread-poll suspect (WASD + LMB reach both the main-thread pump and the scheduler-thread poll); the real gap was the `actHeld` mouse gate on `current_menu == RUN_STAGE` (a forced `-level_NN` start leaves `current_menu == INVALID (-1)`, so the mouse was never read) plus the macOS `reconcileGrab` `windowFocused` gate. Fixed by matching PD's input model (in-stage = RUN_STAGE *or* INVALID; macOS grab = PD's MLOCK_ON, no focus gate) — LMB fire now works on the Mac (trackpad: LMB fire, two-finger = RMB/aim, Shift = ADS). **D595 (FIXED): the arm64-only SIGSEGV that firing then exposed** — `initResolveAnimTable` truncated the live `ptr_animation_table` host pointer to `(s32)`, dropping the 16 TiB `PORT_ADDR_BASE` → every AI hit/stagger `struck_anim` resolved to bogus low memory → crash in `modelSetAnimFrame` on the first AI hit-reaction. Fixed by reading the base at full pointer width (§A1; identity on x86_64). Both are the last input-blockers to a playable arm64 macOS build. (maintainer visual-sanity package: `scratch/d589-dam/ab3-mac/{d591_sbs_win_mac.png,d591_diffmap.png,D591-AB-SUMMARY.txt}`. `tools_pc/abi_ratchet.py` baseline refreshed for the branch. **Integration into the v0.6.0 line done (2026-10-10):** #95's `crouched_rifle` hunk and its `D420` label were dropped at integration (our #120/D445 is Line truncated
| macOS first-launch black window (v0.7.0) | open | D581-era sweeps; logs in `docs/dev/notes/mac-first-launch-hang-2026-10-13/` (local) | **2026-10-13, v0.6.0 release download on the M3:** the cold first launch generated assets, reached the title stage (90) and then rendered no frame (`kernel heartbeat: no frame rendered`, frames=0, all game threads blocked); the next launch was clean. 1 hang in 16 launches that day (1 in 10 first launches), 0 in 7 after a reboot; neither a new binary alone nor asset generation alone reproduced it. 3 earlier sightings on 10-12. Lead: one timer mq in the heartbeat dump shows `valid=83746432/0`. Release notes carry a one-line "close and start again" tip. |
| Pixel gate vs the Auto frame-cap default (v0.7.0) | open | D578, `tools_pc/verify.sh` | The goldens (2026-10-02) predate frame blending; with the defaults-only ini the sweep now runs Auto, which blends at 120 on a 120 Hz panel, and 2-9 levels flag at random per run (worst 9.3%). The same levels pass with `VERIFY_INI_EXTRA` set to a `[Video]` block with `FpsCap = 60`. Pin the sweep to 60 or re-capture goldens on the blended path; until then run the release sweep with that extra ini. |
| Platform parity gate (Linux co-equal with Windows) | open (v0.7.0, maintainer 2026-10-10) | — | **Decided 2026-09-30:** yes. Every released platform carries the same gate: a 21-level sweep plus a campaign pass. Needs the golden baselines on both platforms first (§7). The last known Linux crash (D190) was closed as not-a-bug. |
| Low-end GPUs (~30 fps on Celeron/HD 400) | fixed, verify on N3060 class | D339, D481, D482, #92 | **D481 (2026-10-02):** fast3d ran inline on the scheduler thread, delaying retrace delivery so the tick gate skipped frames; the render worker took a Lenovo X220 (HD 3000) from 44-46 to 60.0 fps. **D482:** Atom/Celeron-class GPUs (HD 400-605, UHD 600/605, software renderers) get DrawDistance/LodDistance 100 and MSAA 1 once, where still at defaults. **D473:** uncached per-batch getenv fixed. Direction: real optimizations, not a preset. **Next only if N3060-class hardware still drops:** one `GE_PERFSTAT=1` run (`gpu=` vs `run=`), then a lighter renderer-keyed default (GPU) or fast3d per-batch cuts (CPU; local `PERF-HOTSPOTS.md`). |
| Statue/Cradle hitching (maintainer, 2026-09-30) | parked (not player-visible, 2026-10-02) | `GE_PERFSTAT` spike counters | **Diagnosed (D475, D477, D480):** no sustained load — the ~33 ms frames are the frame limiter skipping a retrace after a late gfx task (`boss.c`, D13-class timing); CPU bursts were CI-palette re-imports (ruled out, D477) and in-play shader compiles (fixed, D480). **Parked 2026-10-02:** a play session with the timeline probe felt no hitching. Reopen only if a visible hitch is reported (lever: pre-warm at level load). |
| Steam Deck preset skipped when first launched from Desktop Mode | verify | D283 | Fixed on `release/v0.4.1`: the Deck is detected by DMI hardware id (Valve Jupiter/Galileo; `STEAMOS` as a second signal) and the preset applies once per ini, only while Fullscreen/Maximized are untouched. Only a Windows build and a Linux `-fsyntax-only` check ran. **Owed: one run on a real Deck** (Desktop-Mode-created ini, then Game Mode; expect fullscreen 1280x800 and the log line `video: Steam Deck (DMI Valve ...)`). The Known-issues row goes when this ships. |
| Windows 7 | open | #98 | **Re-checked 2026-10-02: likely feasible around 1.0.** The exe imports no Windows 8+ functions and the bundled SDL2 2.32.10 still supports Windows 7. The real blocker is the first-run converter `ge007-convert.exe` (PyInstaller, Python 3.9+): the native C converter (#6 Part B) removes it, or a separate Python 3.8 build. Work: one Windows 7 VM test, then manual per-release checks (CI cannot test it). Supersedes the earlier 'won't-fix' call. |
| Alt-tab haze + fast alt-tab recovery | open (v0.7.0, maintainer 2026-10-10) | D578 | Maintainer 2026-10-10 (v0.6.0 build, Windows 120 Hz): alt-tabbing leaves a hazy picture; fast repeated alt-tab is wonky and recovers only after a while or more alt-tabbing. Not new in v0.6.0 (was an owed check). Known issue in the v0.6.0 notes. QoL fix: clean focus-loss/regain path (context/FBO state, present queue reset). |
| Deck: max Field of view feels choppier while turning | fixed in v0.6.0 2026-10-13 (D953 LOD/fade/visibility + D954/D955); maintainer Deck-confirmed (Surface windows) | D953 | At FOV 90 (menu max) turning feels worse than at 60, at 60 and 90 fps. Frame pacing measured identical at both FOVs. Lead: FovScale pulls every model LOD switch 1.73x closer, and depth-based LOD flips off-axis objects while turning (Dam turn probe: 0 flips at 60 deg, 400 at 90 deg, 120 with the fix candidate on `fix/d953-fov-lod`). Pass 2 (10-13 evening): prop fog fade and fog-range visibility fixed the same way; on Surface LOD/fade/visibility do not change while turning, so they are not the feel. Maintainer: objects "update not at the right speed" (object motion is not interpolated at 90, plan D955). **Next:** D955 Stage 0, and one Deck `GE_D193=1` run per FOV for 2-tick game steps. |
| Doors and guards flicker at 90 fps (not at 60) | fixed 2026-10-13 (D954); maintainer Deck-confirmed | D954 | In-between presents drew props (chrs, doors, effects) at the exact camera while rooms used the blended one, so props jumped against the world whenever the camera moved (Surface turn on Windows: 0.4-0.5 deg mean, up to 1.8 deg). Present in v0.6.0 at 90 fps. Fix in `gfx_pc.cpp`: props' plain perspective gets the room camera blend (`P * R^-1 * Rb`) up to the first-person gun. **Owed:** Deck at 90 fps, Surface doors/guards, and the gun must not move while turning. |
| Objects move at 60 Hz steps at 90 fps (props, doors, guards) | fixed 2026-10-13 (D955 + sky pass); maintainer Deck-confirmed (guards, doors); first-person weapon not blended yet | D955 | In-between presents drew objects at their tick pose (only the camera was blended). Now blended per joint, keyed by (Model*, joint) from a game-thread snapshot, rebased onto the new camera; exact on room-origin changes, failed gates, split-screen. Dam on Windows: 99.6% of object joint loads blended. **Owed:** Deck at 90: guards walking, gun in hand, hats, deaths, shadows, doors, Facility stall doors/sinks (the old ghost site), room boundaries. Vehicles later. |
| Windows display scaling: the game is not DPI-aware on scaled displays | fixed 2026-10-10 (D626); maintainer by-eye on a scaled display owed | D626 | Seen 2026-10-07 during the v0.5.0 4K captures: on a 3840x2160 display at 125% Windows gives the game a 3072x1728 surface and stretches it, so the picture is softer than native. `gfx_sdl2.cpp` sets `SDL_HINT_WINDOWS_DPI_AWARENESS` only when `allow_hidpi` is off; setting it (`permonitorv2`) on the HiDPI path too renders at native resolution. Workaround today: environment variable `SDL_WINDOWS_DPI_AWARENESS=permonitorv2`. |
## 5. Modern-remaster layer (port features beyond the N64)
@@ -203,25 +226,38 @@ Background research lives in the maintainer's local notes (gitignored):
### 5a. Before 1.0
**Renumbered 2026-10-10 (maintainer):** the planned v0.5.1 (frame rate above 60 + macOS) ships as **v0.6.0**, and the old v0.6.0 scope is now **v0.7.0**; labels below are updated, findings and commits keep the old names. **Order (maintainer, 2026-10-08; mod-start added 2026-10-09): v0.6.0 = frame rate above 60 (D578) + macOS (#95, local); then custom textures and mod support (the texture work likely shares the surface/geometry findings from D578). v0.7.0 ships the items marked v0.7.0 below (#145 control styles, native N64 pad support, the v0.5.0 issue triage, the README line, **mod support start — maintainer 2026-10-09: mods come sooner, in v0.7.0, not after 1.0**). They are the minimum for 1.0.** Everything marked v0.7.0 is open; none of it is claimed fixed or verified.
| Item | Status | Refs | Notes |
|---|---|---|---|
| Log file (next release) | open | maintainer 2026-10-06 | Write `ge007.log` next to the ini (console output is lost on a normal launch). Crosshair pointer done in v0.5.0 (D555). |
| Log file (next release) | done (v0.6.0) | maintainer 2026-10-06; D576 | **Done 2026-10-07 (D576):** `ge007.log` next to the ini, previous run kept as `ge007.prev.log`, 8 MiB cap. Was: write `ge007.log` next to the ini (console output is lost on a normal launch). Crosshair pointer done in v0.5.0 (D555). |
| F10 / PC Options menu: PD-style dialog refactor + watch-consolidation review | verify | D504-D512, `MENU-PD-ALIGNMENT-PLAN.md` (maintainer's local notes) | **Built 2026-10-04 (D504); follow-ups merged:** D505/D506/D507 (wording, real-unit sliders, descriptions); D508 forced aspect ratios (16:9 / 21:9); D509 2-frame hold after an aspect change (rare gun glitch = known issue); D510 overlay + FPS counter same size front end vs level; D512 `Game.HudScale` scales the F10 overlay. **Owed (live pass, `docs/dev/notes/MENU-LIVE-CHECKLIST.md`, launches A/B/C):** look-and-feel vs PD; mouse, keyboard and pad in a level and on the front end; HudScale 75/100/150; 21:9; display-mode round trip across a restart; split-screen 2P with the overlay open. **Gaps:** background blur (post-1.0, §5c); per-seat control scheme is #139 / D513. Watch consolidation: not cheaply feasible (parked as T5). |
| Defaults/presets consolidation: one `Display mode` switch | verify | D504, D440, `MENU-PD-ALIGNMENT-PLAN.md` §5 (local notes) | **Landed 2026-10-04 (D504):** the derived `__DisplayMode` row shows Modern / Original N64 / Custom (no ini key); F10 dropdown + left/right stepping, front end steps too; one reset row per dialog covering exactly its keys; editing a non-bundled key does not flip Display mode. **Decided 2026-10-03: tri-state**, flipping to Custom when the user edits a bundled key. **Owed (live):** display-mode round trip across a restart; read of the regrouped pages and wording (MENU-LIVE-CHECKLIST.md A1/A2/A4). |
| Control style Original shows the game's own N64 styles | verify | #139 follow-up, D516, D517 | **Landed 2026-10-04 (D516, merged @ 91595597):** "Controller style" row (Original only; the game's own 1.1-2.4 per seat, saved to the Bond file; front end reads "(in a mission)"); D498 presets hidden in Original. **D517:** hold-repeat on any Bond row dropped fps (EEPROM rewrite per detent); now staged like sliders. Live passes 2026-10-04 (maintainer): single player + split-screen 2P with different styles per seat. **Owed:** the 2.x second-pad pass (real second controller). |
| Pad layout preset per player (setting P2's no longer moves P1) | verify | D518, D498 follow-up | **Landed 2026-10-04 (D518):** `Input.PadPreset` kept as seat 1's key + new `Input.PadPreset2/3/4`; the F10/front-end preset row follows the Select-player seat. Headless GE_PADMAPTEST PASS; **live passes 2026-10-04** (P2 change leaves P1 alone; split-screen: each seat ran its own preset). |
| Control scheme selectable: Ext / Original control style | verify | #139, D513, D498, D166 | **Landed 2026-10-04 (D513, merged into `release/v0.4.1`).** `Input.ControlScheme` (row "Control style", Ext default / Original) on the Controller page, both UIs. Original leaves the game's own style alone; D498 presets set layout and stick swap only in Original; 2.x runs faithfully; global in v1, per seat with the Select Player work. **Owed:** pad pass in both modes, watch-selected 1.1-1.4, split-screen 2P with different styles, 2.x with a second pad. |
| 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:** **#120/#121/#122** merged locally (#122 = D450); close on push. **#123**: merged 2026-10-06 into v0.5.0 (D436; presets set it off, tip, Rule-2 signed off; by-eye hipfire / sniper zoom / 2P owed). **#124**: deferred (ignores HudScale, overlaps status text, ultrawide anchoring, split-screen untested). **#133**: duplicates D472; close as superseded on push. **#109** (pad rebinding): decline as-is; supersede with our pad presets (§5a), crediting the author (Co-authored-by). **#107**: folded into D441; close on push. **#108**: belongs with `macos`; ask for a range check. |
| 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:** **#120/#121/#122** merged locally (#122 = D450); close on push. **#123**: merged 2026-10-06 into v0.5.0 (D436; presets set it off, tip, Rule-2 signed off; by-eye hipfire / sniper zoom / 2P owed). **#124**: deferred (ignores HudScale, overlaps status text, ultrawide anchoring, split-screen untested) — **tentatively on the v0.7.0 triage list (commented to the author 2026-10-10; blocker is ours: scope + golden re-baseline, so a rebase onto `release/v0.6.0` would make integration cheaper).** **#133**: duplicates D472; close as superseded on push. **#109** (pad rebinding): decline as-is; supersede with our pad presets (§5a), crediting the author (Co-authored-by). **#107**: folded into D441; close on push. **#108**: range check implemented in v0.6.0 as D573 (commented + credited 2026-10-10; superseded, author to close). |
| Gamepad presets: duplicate-binding warning, Xbox stick behaviours | open (after v0.5.0; documented in the README) | D469, D498, D518, D394 | D469 (pad action table, Jinx 1.1 default, Custom per-seat bindings), **D498** (Xbox 1.2 Christmas / 1.3 Frost / 1.4 Elektra) and the per-seat preset (**D518**) are done and maintainer-verified with pads. Not done (maintainer 2026-10-04: skip in game for v0.5.0, document only): 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. |
| 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. |
| Frame rate above 60 (90/120/144 with interpolation); **v0.6.0** | verify | D578, #137 (f1zz1ec0ke), [`dev/UNLOCKED-FPS-PLAN.md`](dev/UNLOCKED-FPS-PLAN.md) | **Built 2026-10-07 (D578, rework of #137 by f1zz1ec0ke), branch `feat/interp-fps`:** `Video.FpsCap` 90/120/144 draws matrix-blended frames between game frames on displays faster than 60 Hz; exact-frame fallback for turns (default ON); room-set change forces an exact frame; auto (match display) cap default. Latest commit `06ab98af`: port-side portal-chain replay recomputes room scissors for the interpolated camera (verified 0 mismatches over 1,200 exact frames at alpha=1 on Dam; the deep tunnel chain and flicker are untested). **Goal: a fresh Steam Deck install defaults to 90.** **Remaining before merge into the `release/v0.6.0` line (all open):** (1) Dam tunnel holes / pop-in re-test on a Deck with `06ab98af`; (2) VSync-on re-test (74.5 presents/s seen earlier); (3) Windows 120 Hz check plus a default-60 golden regression; (4) alt-tab haze; (5) live 144 Hz and Deck OLED 90 Hz checks. Dam in-between flicker (2026-10-08, NOT FIXED, the blocker for shipping high FPS): eight Deck A/B variants (dclamp, projset, exactproj, clipfix, nodepth, nocull, fullsciss, plus the older noportal/nosciss/noclip/noreach) all still flicker, and the CPU emits the same triangle counts in blended and exact passes; the loss is per-pixel content, not missing triangles. Next: run `holecap` in the Dam tunnel gap (saves in-between vs exact images of a real hole) and read what the hole pixels are. Findings: D578 addenda; handoff: docs/HANDOFF.md. **State 2026-10-10 (supersedes the "Remaining before merge" list and the Dam-flicker note above, both resolved by D578 2026-10-09 / D583):** work lives on `feat/d583-deck90` (local, unpushed, on top of `feat/interp-fps`). PC 120 + VSync on G-Sync fixed; Deck 90 good on most levels. **Merged 2026-10-10 into `land/v051-integration`** (off `release/v0.6.0`, with `macos-arm64`). Known issues for the release notes: Bunker fight drops on the Deck (D585 parked), Jungle feel, Cradle turret explosions (uninvestigated). Open gates: Dam ridgeline check with the clip widening off, default-60 golden regression / 21-level golden sweep, alt-tab haze, 144 Hz live check, VSync-without-G-Sync PC check. Was: decision. Older note: the menu cap was {30, 60} (the ini accepts 0 = uncapped, but the sim ticks at the VI rate); the plan's Phase 0 is resolved by D248 and its Phase 1 audit and Phase 2 slice scope are written (2026-09-30). Must stay compatible with netplay determinism. |
| PD Extended Options parity: settings that need new dev work | done (reviewed 2026-10-04; two rows deferred) | `MENU-PARITY-SONNET-PLAN.md` §3 (local notes); pd_port `port/src/optionsmenu.c` @ 32a1cb9 | **Reviewed 2026-10-04 (maintainer decisions; local notes).** **Done:** D511 fullscreen mode, Center window, crosshair opacity and color by health (live check owed, MENU-LIVE-CHECKLIST.md A4/A5/B3). **Watch Screen size/Ratio:** stay on the watch, no PC rows; the game saves watch settings on watch close (D349 correction) and they don't fight Aspect ratio (README). **Deferred past v0.5.0:** mouse lock mode (Auto/Always/Off; aim lock itself = `Input.AimMode`, D337); per-seat controller device select (needs a stable pad id; MP input layer). **Closed:** edge deadzone = `Input.AimRange` (D338); crosshair sway N/A (GE has none; gun sway is D428); use key reloads, no toggle (Ext keeps D378's GEPD split, Control style Original gives the N64 behaviour); per-seat options/binds (pad binds already per seat, D469/D518; keyboard/mouse is one device); no Detail textures toggle (`Video.DetailBaseTile` is the D236 fix); N/A confirmed: HUD centering (D335b), glare, overexposure, GE64 muzzle flashes, MP death music, radial menu speed, language filter, crosshair speed. **Other rows:** analog movement/stick scale/crouch → #139, T7; tickrate/framerate limit → frame rate above 60. |
| One PC options UI: front-end "PC Options" opens the F10 overlay over the folder graphic | verify | D519, D343, D504, D510, D512 | **Landed 2026-10-04 (D519):** the file-select "PC Options" entry opens the F10 overlay over file select; the `MENU_PC_OPTIONS` screen and four old probes are gone; Bond-file rows and the watch chooser are keyed to file select. Headless checks pass. **Owed:** by-eye mouse, keyboard and pad pass on file select; profile chooser + Controller style with a real profile; footer text overlaps the Copy/Erase bar. |
| Emulator saves load directly in the game (no tool for import) | verify | D514, D492; `port/src/eepimport.c` called from `geEepromLoad`; D297 `legacycrc.c` same pattern | **Landed 2026-10-04 (D514).** Copying a 1964/Project64 `.eep` in as `data/ge007.eep` works: on first launch each region that validates in emulator format and not port format is converted in place (same rules as `eep_convert.py`); regions invalid in both are left to the game's wipe; port saves untouched; the original is backed up to `ge007.eep.emulator.bak`. Export stays with `eep_convert.py`. Headless verify 6/6 byte-identical to the reference. **Owed:** by-eye mission select after importing a real 1964 save. |
| DexDrive / GameFAQs `.n64` saves | idea (easy, post-1.0) | D514; sample `scratch/eepinv/gamefaqs_dexdrive_2001.eep` (local) | **Noted 2026-10-04.** Old GameFAQs GoldenEye saves are DexDrive Controller Pak dumps (`123-456-STD` header, then a 32 KB pak) whose note (game code `3BADD1E5`, 2 pages) holds the 512-byte cartridge EEPROM. Extracting it and zero-padding to 2048 bytes gives a valid emulator-format save (TenebrusoM sample: all 6 CRCs OK). Support = walk the note table, pull the note, pad, hand to the D514 importer (or an `eep_convert.py dexdrive-to-pc` subcommand). Not tested in game. |
| Title bar and window icon (no FPS counter; static "GoldenEye 007" title + GE-crosshair window icon) | done (v0.5.0 polish) | maintainer request 2026-10-17; D550; `port/src/video.c`, `port/fast3d/gfx_sdl2.cpp`, `cmake/icon/ge007.rc`; icon art = `docs/img/icon/ge007-icon.png` (`tools_pc/make_icon.py`) | **DONE 2026-10-17 (D550):** static title bar (live FPS stays on F10), `SDL_SetWindowIcon` from a generated 32x32 header, exe icon via `ge007.rc`. Original generated mark, no game imagery. Build-verified; maintainer live window/taskbar check owed. |
| Opt-in update check (`Game.CheckUpdates`, off by default) | verify | D551; `port/src/updatecheck.c` | **Landed 2026-10-06:** Game-page toggle; one HTTPS GET per launch to `/releases/latest` (no pre-releases); Windows loads `winhttp.dll` at runtime only. Live-verified (Win/Linux/Deck), security-reviewed. **Follow-up PR (2026-10-06):** (1) the Linux/Deck `curl` path reads into a 64 KB buffer and treats `curl`'s exit code as failure, so a `/releases/latest` response over 64 KB (long notes, many assets) silently gives no result even though `tag_name` is in the first ~1.5 KB; raise the cap and accept a full buffer (v0.4.0's response is 27 KB, so v0.5.0 is safe). (2) Validation matrix: offline harness for `versionNewer`/`extractTag` edge cases; throwaway builds with `GE007_VERSION` set to 0.3.0/0.4.0 against the live endpoint, Windows + Deck; a pre-release never prompts; after a publish, an older-version build shows the F10 row and opens the releases page; network-off and setting-off runs. |
| Opt-in update check (`Game.CheckUpdates`, off by default) | verify | D551; `port/src/updatecheck.c` | **Landed 2026-10-06:** Game-page toggle; one HTTPS GET per launch to `/releases/latest` (no pre-releases); Windows loads `winhttp.dll` at runtime only. Live-verified (Win/Linux/Deck), security-reviewed. **Follow-up PR (2026-10-06):** (1) DONE 2026-10-07 (D575; offline harness `tools_pc/updatecheck_test.c` covers the edge-case half of (2); the live half is still owed). Was: the Linux/Deck `curl` path reads into a 64 KB buffer and treats `curl`'s exit code as failure, so a `/releases/latest` response over 64 KB (long notes, many assets) silently gives no result even though `tag_name` is in the first ~1.5 KB; raise the cap and accept a full buffer (v0.4.0's response is 27 KB, so v0.5.0 is safe). (2) Validation matrix: offline harness for `versionNewer`/`extractTag` edge cases; throwaway builds with `GE007_VERSION` set to 0.3.0/0.4.0 against the live endpoint, Windows + Deck; a pre-release never prompts; after a publish, an older-version build shows the F10 row and opens the releases page; network-off and setting-off runs. |
| Control style: Classic/Original gamepad styles do not match the N64 (#145) | open (v0.7.0; reported on v0.5.0) | #145, D516, D498, D469, D518, D394; README gamepad section | **Reported:** (a) Input "Invert look" (`port/src/input.c` `padLookInvertY`, applied only on the right-stick look path, ~lines 2797/2821) does not reach the left stick when an Original style that aims with the left stick (1.2) is active. **Hypothesis from code reading, not yet verified.** (b) Face buttons under "Classic" do not revert to the original roles (original: South = change weapon, West = action/reload; port: South = action, West = reload, North = change weapon). (c) Menu buttons differ (original: South accepts, West goes back; port: West + South accept, East goes back). (d) The in-game watch invert row looks locked (D564 removed the game's own Look up/down row). **Decision owed (maintainer):** should "Original" mean strictly the N64 face-button and menu roles, with the modern roles kept as a separate preset? Port-only fix. Cross-refs the Gamepad presets row above. |
| Native N64 pads (raphnet adapter) | open (v0.7.0; unverified) | v0.5.0 video review; vendored mupen `InputAutoCfg.ini` | Nothing raphnet-specific exists in the repo. The adapter should appear as an SDL joystick, but that is **unverified**. Work: verify with a real adapter or a user report (raw stick range ~+-80, octagonal gate, deadzone, C-buttons as buttons vs axes), then add a port-only N64-pad profile alongside the Xbox 1.2-1.4 styles. **Owed: a real adapter test (maintainer or a tester).** |
| v0.5.0 issue triage: #146 menu emblem, #147 gun sway part 2, #144 mission timer | open (v0.7.0 triage) | #146, #147, #144, D428 | **#146:** main-menu file-folder emblem texture corruption (4K, Fedora KDE); needs a repro and a 1964 comparison before any fix. **#147:** gun slides side to side but does not turn with the look direction (part 2 of the sway report, relates to D428); check against 1964 first. **#144:** optional PD-style mission timer; port-only QoL, small. Parked issues #113 (cheat toggles), #111 (bots), #110 (third-person), #105, #101, #100, #98 are unchanged and already decided/answered above. |
| README "what this is" line | open (v0.7.0, docs) | recurring viewer questions (v0.5.0 video comments) | Add a short, honest line to the README: a decomp-based native port, AI-assisted, with 1:1 fidelity as the goal. Answers the repeated vibe-coding doubts and comparisons with other GoldenEye ports. VR stays post-1.0 (reference row in *Community forks*). |
| Opt-in gameplay behaviour toggles (shoot-through, true rockets) | open (1.0, added 2026-10-09; **not started — no F10 row, no ini key, no port code; today's build is pure vanilla**) | §5b pattern; `gunfire.c`, `chrprop.c`; the 007 Plus source in the reference tree (**semantic reference only — it is a modded decomp, never a copy source**) | Port-only, default off; one batched Rule-2 sign-off (same pattern as the §5b rows). **Semantics (checked 2026-10-09 against the 007 Plus source in the reference tree):** shoot-through = the per-weapon `ObjectsShootThrough` stat (`gun.h:77`; `PROPFLAG2_SHOOTTHROUGH` `bondconstants.h:360`; penetration-limit checks `chrprop.c:2693/2822/2848`) — how many cover objects a shot passes through; mods retune the per-weapon table (e.g. Moonraker 6 vs 2; the dabs weapon-stat notes agree) and the disable toggle restores the vanilla values (RAM override, All-unlocked class). **True rockets — still open (2026-10-09):** the on-disk Plus tree's rocket handling matches vanilla (`propobj.c:4943`: fixed 100.0f player-prop / 2.0f enemy damage, owner-tagged self-damage, `unk8C` wall-bounce), so the scene's "true" semantics are not in that tree, nor in the GEPD bundle or the footstep docs; plausible candidates = AoE radius/splash retune or door-destruction behaviour — **confirm with the scene author before implementing**. **Implementation-risk note (maintainer 2026-10-09):** these are game-adjacent weapon-stat edits — fine-tune, don't guess: implement as minimal RAM-override toggles, diff-check against the vanilla values, verify with 1964 A/B + golden frames, one small commit per toggle, human review of the diff. **State 2026-10-09: nothing implemented. Work order:** (1) one-off stats-table dumper (vanilla + tuned per-weapon values, both ROMs); (2) the RAM-override + F10 row (one small commit, per the risk note); (3) 1964 A/B + golden-frame verify. True rockets waits on the scene-author answer for its semantics. F10 rows; the Original N64 preset keeps both off. |
| Mod/patch support: drag-and-drop mod loading + mod management + mod template scaffolding + a shipped example mod | open (v0.7.0 start — maintainer 2026-10-09: **mods come sooner, in v0.7.0, not after 1.0**; full scope stays the 1.0 floor; added 2026-10-09) | D413 (capacity-only Rule-2 exception, user-approved 2026-09-28 — recorded in the mod-loader worktree's AGENTS.md), `land/modloader-*` worktrees, §5d; dabs `CLAUDE-notes/mods.md` (local reference) | **Progress so far (the Goldfinger 64 campaign, 2026-09-28 → 2026-10-08, worktree `land/modloader-m1-backlog`) — this is a working mod-loader layer, not a starting point; do not undersell it:** the **`rommod` layer is live** — a 25.1 MB GF64 mod ROM loads over the 12.5 MB vanilla base (`rommod: mod 'goldfinger64' requested — 58 raw file rows, 598 sidecar rows` in the run logs; the standard pcmodels/pccg sidecar pools carry the extra content), with C2 ROM-patch decoding of the mod's patches. **M1 done** (`97fda74f`): GF64 **boots, renders (walls fixed), clean-quits** via the loader. **C2 done** (`8c325fc5`): the ROM-patch decoder CLI `tools_pc/modpatch/` — **branch-only, not yet merged to main**. **D413** (user-approved 2026-09-28): the capacity-only Rule-2 exception — raising compiled table/pool capacities (`rommodTextureCap()`, `g_MaxProps`, `STAN_MAX_ROOMS`) under `#ifdef PORT` with a provably-vanilla-identical gate; it is the mechanism that lets a mod exceed vanilla limits. **Asset-prep + verification:** the `modprepare` pipeline (decodes the vanilla RZ file table — 727 rows, all resolved → `base_filelist.csv`/`base_table.json`); a **70-run GF64-vs-vanilla A/B campaign** (per-system: chrs/heads, levels, fog, portals, props, textures, vis/cull/nocull, mem, dyn, shots, clean-quit; vanilla-gate runs proving no-mod identity; GF64 re-verification runs against mainline findings). This row = M1→: content-tier mods (maps, textures, models) loadable by drop onto the exe / a `mods/` folder, with a management list (enable/disable); custom patch support rides on the same path. **Design standard (maintainer 2026-10-09): the dabs mod system** — `mods/<name>/{files,segs,textures}` overlay dirs, single-overlay slot, live file swap (`modListApplySelection` / `romdataResetFiles`), restart path only for `segs/` mods. **Add (maintainer 2026-10-09):** mod-template scaffolding + docs (new mod = copy template into `mods/`) and a first example mod we ship that does something cool. **The starter/test mod is the gun-colour feature (maintainer 2026-10-09 — it does not stand alone as its own feature):** per-weapon tint + a texture swap, demonstrating the scaffold end-to-end (crosshair colour is the separate shipped D511 — don't conflate). **Precedent:** the drop-in ROM converter already loads a Goldfinger 64 ROM — maps work, the mod's custom logic is inert; closing that gap (weapon/script data) is the stretch goal. Acceptance case: GEPD footstep-patch-style mods (bodies staying down, footstep SFX, core tweaks); their feature set maps onto the §5b opt-in toggles. **v0.7.0 slice (maintainer 2026-10-09):** look at mods sooner — merge M1/C2 to main, start the M1→ management + scaffolding (dabs standard) in v0.7.0, with the GF64 boot/render as the acceptance case; template + example mod and custom-patch support complete by 1.0. |
| Cut-content restoration (mod tier): extended MP roster + the 8 cut MP maps | open (1.0; added 2026-10-09, bring sooner if easy) | [`dev/CUT-CONTENT-BACKLOG.md`](dev/CUT-CONTENT-BACKLOG.md) §D/§F | Everything is in the ROM data (backlog, verified): roster — `front.c:601+` `mp_chr_setup[]` holds ~40 named entries beyond the 8 selectable (ROSIKA, KARL, TERRORIST, BIKER, …); maps — `front.c:556-573` `multi_stage_setups[]` has 8 commented-out entries (Citadel/Frigate/Statue/Cradle/Aztec/Runway/Dam/Depot) and every one of those maps already boots standalone (`-level_XX`, geometry, AI setup and briefings intact). Tier 2/3: the table edits touch `src/game` → land on the mod-fork branch (non-negotiable #2) and ship through the mod scaffolding above; Tier-1 config-toggle grants stay port-side (backlog §2). **Maintainer 2026-10-09: in the 1.0 roadmap at minimum; if the `front.c` table edits + setup fixes turn out small, pull in earlier (v0.7.0).** **Assessed 2026-10-10 (read-only, `front.c`):** decision = *not* pulled in for v0.7.0. (a) Roster half is smaller than the row implies: `num_chars_selectable_mp` is already dynamic game logic (`front.c:4240` `unlock_all_mp_chars()` sets 0x40 via the existing debug/cheat unlock-all; `front.c:4986` sets 0x21 on Agent + Cradle-completed, else 8) — so an extended roster may need no `src/game` table edit at all: a Tier-1 port-only config toggle invoking the existing unlock path (live check owed: the 64-char select screen under the cheat) — the one-line "set the default to N" variant is the only `src/game` touch, and that stays mod-fork-only either way. (b) Maps half is the real cost: 8 entries are commented out with per-map state flags (`stat`/`crad` "works"; `azt` "needs setup"; `dest`/`runway`/`dam`/`depot` "xbla setup"; `Citadel` "old format") — uncommenting is trivial, the xbla/old-format/needs-setup fixes are not, and they touch `src/game` data handling → mod-fork branch. (c) v0.7.0 has no mod scaffolding yet (it lands with the 1.0 mod-fork), so there is no v0.7.0 ship path for the content itself; keep the item in 1.0 mod-fork scope and do the (a) live check opportunistically. |
| Motion/gyro aim (controller sensor) | open (1.0, added 2026-10-09) | — | Controller gyro look, opt-in, default off; sensitivity + calibration rows. **Scope note (2026-10-09):** gyro hardware exists only on PS4/PS5 pads (and the rare Switch Pro via adapter) — Xbox/XInput pads have no sensor, so the practical feature set is small; the API is SDL2 `SDL_GameControllerGetSensor` — **verify first on the PS5 DualSense (maintainer has one; the D471 row already used it as a controller test)** before building. Steam-Input-style gyro mapping stays a player-side workaround (the game is non-Steam, custom shortcut only — no Steamworks). |
| Localisation: JP/PAL ROM support first, then community language packs | open (1.0; reframed 2026-10-09) | §1 (PAL/JP row) | **Reframed 2026-10-09 (maintainer):** phase 1 = JP/PAL ROM support, already Stage B (§1). Phase 2 (later 1.0, community-driven) = a port-only text-override layer (`data/languages/<code>` packs overriding ROM strings) so people can add their own language packs if inclined; translation content is community-supplied (open-source-branching model). |
| Custom gun colour (ships as the example/starter mod — see the mod row above) | deferred (2026-10-09) | D511 (crosshair colour already shipped — a different thing) | Maintainer 2026-10-09: not a feature in its own right — it is the starter/test mod that demonstrates the mod scaffolding (per-weapon tint + texture swap); the hook (a tint pass in the model/fast3d stage) is designed when the mod row starts. |
### 5b. GE+ / Dab's-mod gameplay options (opt-in; touch game code)
@@ -245,7 +281,7 @@ needs Rule-2 review.
| 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. |
| HD texture packs: loader + texnum dumper; model packs | post-1.0 | #100. There is a staged design. Step 1 (texture identity registry) touches `src/game`, so it needs Rule-2 framing. **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. |
| HD texture packs: loader + texnum dumper; model packs | open (1.0; loader + dumper bumped 2026-10-09, was post-1.0; model packs stay post-1.0) | #100 | There is a staged design. Step 1 (texture identity registry) touches `src/game`, so it needs Rule-2 framing. **Bumped 2026-10-09:** loader + texnum dumper in 1.0 — the §5a order already puts custom texture work in the v0.6.0 sequence (sharing the D578 surface/geometry findings); the geometry/texture-lining fixes for pack support come first; model packs stay post-1.0; our own opt-in implementation (default OFF; the Original N64 preset restores the N64 look); thepont's fork is reference only. |
| Animation smoothing (keyframe slerp) | 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. |
| F10 background blur + GUI-only texture filtering | post-1.0 | **Backlogged 2026-10-04 (maintainer): visual nice-to-have after 1.0.** Design done (local `MENU-BLUR-GUIFILTER-DESIGN.md`, ready Sonnet prompts in its §4). Blur: fast3d already decodes PD's framebuffer EXT opcodes and has the 16-tap kernel; only an emitter is missing (`port/fast3d/gbiex.h`); capture once on open (live capture too costly on HD 3000, D481). GUI filter: `Video.GuiTextNearest` point-samples texrects only. Port-only, default off. |
@@ -263,9 +299,10 @@ and a stage loader.
| Item | Status | Refs | Notes |
|---|---|---|---|
| LAN netplay | post-1.0 | — | Depends on split-screen. Determinism groundwork is D117 (not achieved). **Decided 2026-10-01:** in scope after 1.0. |
| MP bots | post-1.0 | #111 | Not N64 (GoldenEye's MP is human-only; Simulants are Perfect Dark). birdturtle `simulants` is an outside prototype. **Decided 2026-10-01:** in scope after 1.0 (opt-in; not N64). |
| Co-op campaign | post-1.0 | — | Not N64. thepont `feature/coop` is an outside prototype. Listed under Not planned unless this changes. **Decided 2026-10-01:** in scope after 1.0 (opt-in; not N64). |
| LAN netplay | open (1.0; bumped 2026-10-09, was post-1.0) | — | Depends on split-screen. Determinism groundwork is D117 (not achieved). **Decided 2026-10-01:** in scope after 1.0. **Bumped to 1.0 (2026-10-09):** single-screen view option alongside split-screen, spectator mode, and demo recording (doubles as the §7 determinism harness, §5d). Online (non-LAN) netplay is in scope via the client/server row below. |
| Online netplay: client/server (Zandronum-style) | open (1.0 if viable — tier 1 pre-1.0, narrow tier-2 core as the 1.0 stretch, else post-1.0 per the plan) | local note `dev/notes/ONLINE-CLIENT-SERVER-PLAN.md` (direction agreed 2026-10-07) | **Added 2026-10-09 (maintainer pivot; supersedes the blanket online exclusion in *Not planned*); maintainer 2026-10-09: wants it in 1.0 if viable — the viability gate is D117 determinism + the plan's tier-2 milestones, tier 1 (port-only) goes regardless.** Target: client/server (Quake/QuakeWorld→Source lineage) with a Zandronum-style feature set; shipped as a build variant in the main repo (`GE_ONLINE=ON` → `ge007-online` client + headless `ge007-server`; no forks); game-logic hooks fenced under `#ifdef GE_ONLINE` so faithful builds stay byte-identical (golden frames prove it; a fenced Rule-2 amendment owed, maintainer call). **Tiers:** 1 port-only (lobby + NAT, server config, master server + browser, quick match, rcon — pre-1.0); 2 the `GE_ONLINE` core (authoritative server, prediction/reconciliation, mid-match join, spectators — start narrow: one level, one scenario, 4 players; 1.0 stretch, post-1.0 if the gate slips); 3 later (more players, unlagged hit checks, server demos). References: the PD port `port-net` branch (ENet, host-authoritative — closest analogue, study first); Zandronum/Odamex design-only (GPL — copy design, never code). **The #141 security hold stands** (forensic review before reusing any of it). |
| MP bots | open (1.0; bumped 2026-10-09, was post-1.0; opt-in) | #111 | Not N64 (GoldenEye's MP is human-only; Simulants are Perfect Dark). birdturtle `simulants` is an outside prototype. **Decided 2026-10-01:** in scope after 1.0 (opt-in; not N64). **Bumped to 1.0 (2026-10-09):** opt-in AI/scripted bots for local MP. |
| Co-op campaign | open (1.0; bumped 2026-10-09, was post-1.0; opt-in) | — | Not N64. thepont `feature/coop` is an outside prototype. Listed under Not planned unless this changes. **Decided 2026-10-01:** in scope after 1.0 (opt-in; not N64). **Bumped to 1.0 (2026-10-09):** opt-in split-screen co-op campaign (007 Plus-style). |
| Cheats beyond the N64 cheat menu | post-1.0 | #113 | Only in scope as an opt-in toggle. **Decided 2026-10-01:** in scope after 1.0, opt-in toggles only. |
## 7. Tooling, infrastructure, security
@@ -277,8 +314,9 @@ and a stage loader.
| 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` (the maintainer's cost call). |
| Golden baselines | partial | `tools_pc/verify.sh`, `tools_pc/golden/README.md`, D117/D522/D529/D531 | **2026-10-06: all three sets (win/linux/deck) re-based for D543/D546**, A/B-confirmed; history in the golden README. **Windows gate GREEN (2026-10-05 re-base):** 21/21 PASS honest sequential. The recipe pins the save CONTENT (D529: `verify.sh` installs the canonical in-tree save `tools_pc/golden/ge007.eep` per level) and per-level windows — `900-1500:300` on 14 levels, `1000-1400:200` on Surface 2/Streets/Depot/Cradle/Frigate, `1500-1700:100` on Dam — because the 900 stem sits inside the wall-clock-paced intro flyby (D117; dam measured 1.6–7.2 % run to run). No tol limit raised. **Linux set (D525 → D530 → D531):** D530's re-round ran under the box-local save (`capture_p7.sh`'s re-pin overwrote D529's canonical install); D531 fixed the tool and re-captured all 21 levels under the canonical eep (47/63 frames moved). **Cross-platform spread recomputed 2026-10-05** from the committed pairs (`framediff.py` win-vs-linux at the stems, tol 2): exact **0.380–5.120 %** on the 20 non-Cuba levels (silo's 900 stem is the outlier: cell mean 34.3, phash 10), structural **21/21 clean**, max phash 40 (cradle, at threshold); Cuba 1.357–12.663 % is the structural-tier-only level. **Pixel gate green on all three, final v0.5.0 build (2026-10-06):** win 21/21; linux 21/21 on the X220 (`DISPLAY=:1` makes the GL probe see the real GPU; one Jungle flyby-jitter miss passed twice solo); deck 21/21 (needs `XAUTHORITY` for the probe over ssh). |
| Flicker scan (A/B/A frame alternation) | parked (2026-10-03: tooling, nothing blocked on it; local spec `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. For any old-vs-new pixel diff outside the golden gate, run a same-commit control first. Reference: Birdman64 `tools/flicker_scan.py`. |
| GE_ probe catalog (macos-track diagnostics: keep vs strip) | done 2026-10-08 (strip debt closed; the `GE_TEXDUMP` KEEP family stays in-tree) | [`dev/GE-PROBE-CATALOG.md`](dev/GE-PROBE-CATALOG.md) | The macOS track added env-gated, diagnostic-only `GE_` probes (all off by default, no behavior change when unset). **Keep:** the `GE_TEXDUMP` family (`GE_TEXN`/`GE_TEXDUMPROW`/`GE_TEXRAW` + the GL `.rgba32` upload dump — the standing cross-platform texture-triage tool, cached `getenv`). **Stripped already (2026-10-08, `b80dcd08`):** `GE_NPOT_NOGENMIP` (the D592 NPOT mip-guard, `9bed5904` + companion `82dbea9c`) and `GE_TEXSTATE` (the D592 GL-state probe, `de5a0fea`) — both removed when **D592 closed as a non-issue** (metric false-positive; see the D592 close-out). **Stripped 2026-10-08 (final probe-strip commit; on `macos-arm64`, pushed to Forgejo 2026-10-08):** `GE_TEXP` (the `src/game/image.c` pool-cursor dump — its removal returns that region to the N64 build), `GE_SIZEOF` (the `port/src/main.c` startup ABI-width canary, redundant with the `abi_ratchet.py` CI) and `GE_KEYLOG` (the `video.c`+`input.c` D594 input diagnostic) — 75 pure deletions, behavior-neutral, Mac headless re-verified (2P + solo, 0 crash logs). Stripping is a mechanical, behavior-neutral edit. |
| Release build-provenance attestation | verify | `.github/workflows/ci.yml` release job; `.github/SECURITY.md`; README Status section | **Implemented 2026-10-04:** SHA-pinned `actions/attest-build-provenance` attests `dist/*.zip[.sha256]` + `dist/*.tar.gz[.sha256]` in the tag-only release job (`id-token: write` + `attestations: write` on that job only). Backs SECURITY.md's "unsigned, but reproducible" stance with `gh attestation verify <file> --repo jkdansereau/goldeneye-pc-port`; documented in SECURITY.md and the README. **Owed:** the v0.5.0 tag build is the first real run; confirm the step passes and verification succeeds on a downloaded asset, then delete this row. |
| 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. |
| Self-hosted CI runner link failure | open (v0.7.0, maintainer 2026-10-10) | 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 | Passes 2026-09-30 to 2026-10-03 removed the closed-finding probe suites (registry in `GE-ENV-PROBES.md`, `gen_env_probes.py --check` passing); TEMP probes `GE_TESTSTYLE` / `GE_D516` removed 2026-10-04. **Left:** D104 (kept while D294 is in verify); the M-183/M-185 clamps were **deleted 2026-10-04** (`879f879a`, `model.c` pure decomp again; the M-189 root fix stays) and the cutscene re-check was covered by the 2026-10-05 Deck campaign playthrough on rc1 (level intros, endings incl. Facility and Dam, credits). |
| P12 U5 review of `tools_pc/**` (release-diff, B10) | partial | D531/D532/D533 | Reviewed 2026-10-05 (the unit that owed the P7 merge). **Blocker found + fixed (D531):** `capture_p7.sh`'s per-level re-pin restored the local save (see Golden baselines). **Should-fixes applied as D532 (tooling-only):** `verify.sh`'s `pin_ini_640x480` fails closed like `pin_eep`; `GOLDEN_SEED` hard-pinned with a warning for a stray `GE_RSEED`; `audiodebug.ps1`'s dead `-AB`/`-Old` switches removed. Box pixel verify done 2026-10-06 (see Golden baselines). |
| Findings log and doc size | partial | `tools_pc/docs_budget.py` | Budget checker (per-file and tier-1 token budgets; `--toc FILE`). `findings.md` is still about 2 MB (~500k tokens), so agents must keep using the index. Nothing blocks; trim only when a budget check fails. |
@@ -288,6 +326,7 @@ and a stage loader.
| Agentic-development deep dive (doc) | queued — not yet (2026-10-05) | `docs/dev/agentic-development.md` | Write-up owed for the public case study: per-agent commit/work estimates (manual attribution pass over the session log, incl. D350-style commit-date divergences), a code-quality audit of the trial models (GPT-6 (Sol) et al.), and process feedback / findings. Deferred by the maintainer; nothing here blocks. |
| Docs site: split Security & fidelity -> two pages + v0.5.0 update pass (2026-10-05) | done (local, unpushed) | `docs/security.md`, `docs/fidelity.md` (was `security-and-fidelity-status.md`) | Split on review (the homepage pointer sat near Known issues and read as if there were security *issues*); both pages cross-linked and linked from the hub and index.md's Download section. Update pass 2026-10-05 against the `release/v0.4.1` tree: no network includes/symbols/HTTP literals in compiled `src/`+`port/`; no registry or library-loading calls; converter imports no network module; bundle manifests re-checked (allowlist + ROM/oversized-blob hard-fail); region macros re-compared vs the Makefile (all three regions); library classification re-checked (CMake globs libultra+libultrare audio, gu, `io/vitbl.c` only). Full game-logic audit not re-run; recent `src/game` edits are finding-gated. |
| Docs site: final sanity sweep + anchor fixes (2026-10-05) | done (local, unpushed) | `docs/porting-notes.md` (3 TOC hrefs), `docs/404.html` (permalink) | Audit of the 10-page published set (one h1 per page, no heading skips, in-page anchors resolve, unique titles/descriptions, og:image/canonical, JSON-LD, no local paths, served == built). Three `porting-notes.md` TOC hrefs had a double hyphen where kramdown emits one: corrected at source against the live ids. `docs/404.html` got `permalink: /404.html`. Preview-builder slug rule fixed to kramdown's. |
| LLM-agent dev-velocity workstream (model tiers + subagents + compaction hygiene) | decision — Tier 0/1 landed 2026-10-09; two approvals outstanding | `docs/dev-process.md` §8, `pi/` package, forensics of pi session `01a11ec6` (#150 cull-hunt evening) | Root cause of the #150 loss was model + discipline, not retrieval: a 4-bit-quantized 27B default model fired unvalidated render fixes at the maintainer for A/B (four consecutive failures); the 04:52Z compaction promoted an unvalidated hypothesis to "diagnosed" and post-compaction turns built on it. RAG/GitNexus evaluated and rejected (PolyForm license conflict, auto-writes AGENTS.md, wrong failure class; the repo's `findings-index.csv` is the domain-specific retrieval layer). **Landed per the 2026-10-09 maintainer instruction:** Tier 0 — `~/.pi/agent/settings.json` default = the ninfer backend's model (only one of ninfer/strata runs at a time, and ninfer is the fast one; today that is the coding-launcher 4-bit `qwen3.8-27b-nvfp4full` — the quality launcher `ninfer-quality.bat` serves the groupwise-int `qwen3.8-27b` as the strong local tier, a 30-second launcher restart + new pi session, not a backend switch); §8 rules (model tiers; data-before-edit; context reset after a 2nd failed maintainer A/B or a contested compaction; evidence-ledger compaction prompt); MODEL line in the brief template. Tier 1 — `pi install npm:pi-subagents@0.76.1` (pinned; audit in `docs/dev/notes/pi-subagents-audit.md`: MIT, no eval/telemetry, no install hooks, 310 js files) + tracked `pi/` package: four model-pinned subagent roles on ninfer (`build-runner`/`sweep-watcher`/`doc-sync` on 4-bit; `pairing` pinned `inherit` so the reasoning step follows the parent session's tier) + the `compaction-ledger` extension (evidence-ledger prompt; optional stronger summarizer via `GE_COMPACT_MODEL`). **Awaiting maintainer approval:** (1) `pi-subagents` as a third-party npm dependency; (2) the tier split itself — planning on `openrouter/deepseek/deepseek-v4-pro` (or `strata-coder-hard` with the local backend switched), execution on the standing ninfer backend (default = the model it serves, today the 4-bit `nvfp4full`). **Parked until after v0.7.0:** `pi-plans` trial (228★/2 contributors, exploratory benchmark, self-published evidence), `rpiv-ask-user-question`, Langfuse tracing, `pi-durable-subagents` (1★, unvetted); do NOT stack `billion-context` on the ledger extension (compaction overlap). **Workstream mapping:** first real runs on the v0.6.0 critical path — D578 Dam-tunnel flicker (`feat/interp-fps`; the `holecap` probe is `build-runner`-shaped) and D579 T-step (a)+(c) log pairing (the `pairing` agent's first job); build-runner + sweep-watcher support the v0.7.0 #145/native-pad campaigns. The ledger extension loads at session start, so its verification happens in a fresh session. |
## Community forks (not ours; reference only)
@@ -306,8 +345,9 @@ Nothing else owed right now (outside PRs decided 2026-10-01, see §5a).
## Not planned
Online (non-LAN) multiplayer, matchmaking, leaderboards, anti-cheat,
cross-play · Steamworks, achievements, cloud saves · HDR, frame
Online matchmaking, leaderboards, anti-cheat, cross-play (client/server
netplay is in scope now — the §6 online row) · Steamworks, achievements, cloud
saves · 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) · a
+1 -1
View File
@@ -55,7 +55,7 @@
<footer class="site-footer">
<div class="footer-stamp">
<a href="https://github.com/jkdansereau">maintained by jkdansereau</a>
&nbsp;&middot;&nbsp; last updated: 2026-10-07 &middot; v0.5.0
&nbsp;&middot;&nbsp; last updated: 2026-10-13 &middot; v0.6.0
&nbsp;&middot;&nbsp; <a href="https://github.com/jkdansereau/goldeneye-pc-port">GitHub</a>
&nbsp;&middot;&nbsp; <a href="https://github.com/jkdansereau/goldeneye-pc-port/releases">releases</a>
</div>
+225
View File
@@ -0,0 +1,225 @@
---
title: About the project
description: Release history, how the GoldenEye 007 PC port was made, how it works, and how it differs from the other GoldenEye PC projects.
---
# About the project
- [Release history](#release-history)
- [Status in detail](#status-in-detail)
- [How it was made](#how-it-was-made)
- [How it works](#how-it-works)
- [How this differs from the other GoldenEye PC projects](#how-this-differs-from-the-other-goldeneye-pc-projects)
## Release history
The full notes for each version are on the [Releases page](https://github.com/jkdansereau/goldeneye-pc-port/releases).
- **2026-10-13** — **v0.6.0** ([release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.6.0)):
- **Frame rates above 60.** `Video.FpsCap` now offers 90, 120, 144, 165 and
240, and an **Auto** setting (the new default on fresh installs) that
matches your display's refresh rate, up to 240. Frames between game ticks are blended from the
previous and current frame; the game still simulates at its original
rate. It costs one game tick of extra latency, does nothing on a 60 Hz
display, and the *Original N64* preset caps at 60. This is a rework of
f1zz1ec0ke's #137. I checked 120 Hz with VSync on a G-Sync display
on PC and 90 fps on the Steam Deck.
- **macOS on Apple Silicon (experimental).** There is now a `macos-arm64`
download (or build from source; see [Building](building.md)). Based
on danturn's #95 (with Julio C. Rocha's earlier Intel groundwork in
#88). Tested with a 21-level sweep and live sessions on one M3 Mac (the
one crash seen, in the end credits, is guarded in this release); the download is not notarized, so see [macOS](user-guide.md#macos-apple-silicon-experimental)
for the one-line unblock. Test reports are very welcome.
- **Fixes:** geometry vanishing when you press against a wall (#150),
the first launch opening on the wrong monitor (#151), a crash in the end
credits on Linux and macOS (#152), a crash when a guard spawns with a hat (#153, #156), a
sound/text pointer check that rejected valid pointers (#108), a Cradle
catwalk shadow flicker, stale pixels beside the widescreen picture on
front-end screens, and a black window on macOS when the game was started
with the display asleep.
- **Original controls (#145, #149):** with *Control style* set to Original,
a gamepad now behaves as an N64 controller (South = A, West = B, the game
decides what each does), and *Invert look* works in every original style.
Also an optional mission timer (#144), the file-select crest no longer
drawing on a dark square (#146), and a fix for a Windows blue screen on
exit.
- **Smaller changes:** the legacy front-end Velocity pointer mode is gone,
the Linux/macOS update check no longer fails on a large response, every
run is also logged to `ge007.log` next to the ini, and F10 slider labels
fit at HUD 150.
- **Known issues on the Steam Deck at 90 fps:** firefights in the centre of
Bunker 1 and 2 can drop several fps, and Jungle can feel slightly less
smooth than other levels. Cradle's turret explosions may draw wrongly
(reported, not yet investigated).
- **2026-10-07** — **v0.5.0** ([release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.5.0)), the first release
since v0.4.0; it includes the v0.4.1 work, which was never published on
its own:
- **Split-screen multiplayer for up to 4 players**, each player with their
own pad, controls and aim settings.
- **One options menu** for every setting, laid out like the Perfect Dark
port's, with the game's own crosshair as the mouse pointer. Pick the
game's own N64 control styles (1.1–2.4) per player, or the Xbox release's
presets with per-player pad rebinding; PlayStation and Nintendo pads show
their own button names.
- **Closer to the N64:** fog and haze (Surface 2's ground fog, the sky at
the horizon), automatic-weapon fire rate, sniper and camera zoom, Dam and
Caverns water, the Jungle boss fight's timing, guard visibility in
widescreen, and the Dam ending camera.
- **Fewer crashes and glitches:** a multiplayer crash with the crouched
rifle, rockets passing through the ground, and broken geometry around
Aztec's shuttle.
- **A steady 60 fps on low-end GPUs** (tested on an Intel HD 3000 laptop),
with lighter first-launch defaults on Atom/Celeron-class graphics.
- **Emulator saves load directly:** copy a 1964 or Project64 save in as
`data/ge007.eep` and it is converted on first launch.
- Also: more audio, video and control settings (master volume, audio device,
trilinear filtering, draw distance up to 800%, PC-friendly crouch and
look-ahead defaults), and controllers are picked up again when replugged.
- **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).
- **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
per-level stabilization across the campaign.
## Status in detail
**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. For v0.6.0
the release downloads themselves were tested on Windows, Steam Deck (Desktop
and Game Mode), macOS on Apple Silicon and a 2011 Linux laptop: a fresh
folder with only the ROM, a first launch, then all 21 solo levels. The new frame-rate path and the listed fixes were also checked by
playing on Windows and the Steam Deck. The full campaign was last played end
to end, at Agent difficulty on Windows, Linux and the Deck, for v0.4.0.
Split-screen was played live in 2P on every multiplayer map and in 4P on
Temple.
**Known issues:** see the **[known-issues table](ROADMAP.md#known-issues)**
(what you'll notice, impact, workarounds). The short version: NTSC-U ROMs
only in the release packages, and the Apple Silicon macOS download is
experimental and not notarized. On the Steam Deck at 90 fps, Bunker 1/2
centre firefights can drop several fps and Jungle can feel slightly less smooth
than other levels.
> ***All unlocked* never changes your save** since v0.5.0: it unlocks every
> level and cheat in memory only, and switching it off shows your real
> progress. v0.4.0 and earlier 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`.
What a release installs (no networking unless you turn on the opt-in update check, which is off by default and makes one HTTPS request to GitHub per launch; no telemetry, no ROM or game assets)
and how faithfully the port tracks the original game's logic:
[Security](security.md) and [fidelity](fidelity.md) status.
The binaries are not code-signed; from v0.5.0 on each release artifact also
carries a GitHub build-provenance attestation you can check with
`gh attestation verify <file> --repo jkdansereau/goldeneye-pc-port`
(see [SECURITY.md](../.github/SECURITY.md)).
## How it was made
The port was built by several coding agents directed by one maintainer, with
contributions from a handful of people (credited in the release notes).
A local open-weight model (`unsloth/Qwen3.8-27B-GGUF` on one RTX 5090, via the
[pi](https://github.com/earendil-works/pi) agent) did the groundwork: build, boot chain,
software-RSP integration, asset pipeline, first frames. **Claude Code**
(Sonnet 5 and Opus 5, now 5.5; Opus 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.5.0: 51
days (16 Aug – 6 Oct), 1,537 commits (the public history squashes the
v0.4.1–v0.5.0 work into 17), and 419 findings root-caused and logged
(`D1`–`D564`). Every change is checked by comparing captured frames from all
21 solo levels against reference images, crash sweeps on Windows, Linux,
Steam Deck and macOS, and play-testing; there is no unit-test suite.
The full write-up (timeline, handoff mechanism, effort breakdown, an honest
"what worked / what didn't"): [`docs/dev/agentic-development.md`](dev/agentic-development.md).
The workflow itself: [`docs/dev-process.md`](dev-process.md). To cite the
project or its findings: [`CITATION.cff`](../CITATION.cff) (GitHub's "Cite this
repository" menu).
### Tools and models used to develop the port
Development ran for most of its life on a single local model — **Unsloth's
Qwen 3.8 27B** — which did nearly all of the local (groundwork) work on its
own. In the final week that grew into a small menu, and the **current standard
is NInfer + Strata**; tools are chosen per task, with local models carrying
the high-volume work and hosted models the exception. The full record of the
models used, and the harnesses that drove them:
| Model | Ran as | How it was used |
|---|---|---|
| **Qwen 3.8 27B** ([Unsloth](https://huggingface.co/unsloth/Qwen3.8-27B-GGUF)) | `UD-Q4_K_XL`, one RTX 5090 | The primary local workhorse for the bulk of the port's development. |
| **Qwen 3.8 27B** via [NInfer](https://github.com/natpate/ninfer-windows) (Windows) | base, NVFP4 and NVFP4-full artifacts; MTP (DFlash2) spec-decode; one 5090 | Trialed in the final week, now half of the standard; the fast day-to-day local driver. |
| **Qwen 3.8 27B** ([ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-27B-GSQ-RCO-GGUF)) | `IQ3_S` (GSQ-RCO), larger context | Briefly trialed for larger-context workloads; shelved, little use since v0.2.0. |
| **Qwen 3.8 Flash-Next** via [Strata](https://github.com/Niko1221/Strata) | 125B MoE, `IQ1_M` (1-bit), one gaming GPU | The big-context local tier: a model that normally needs a server, run locally for the largest-context passes. |
| **Claude** (Anthropic) | Sonnet 5 / Opus 5, now **Sonnet 5.5 / Opus 5.5** (out in the last few weeks) | The orchestrator in the two-agent phase: held what the 27B couldn't close, took over on limit handoff; the 5.5 tier has driven the most recent work. |
| **GPT-6 (Sol)** (OpenAI), via [OpenRouter](https://openrouter.ai) | a frontier hosted model | Trialed. |
*Harnesses and tooling:*
- **[pi](https://github.com/earendil-works/pi)** — the local coding-agent
harness that drives the local models.
- **[Claude Code](https://github.com/anthropics/claude-code)** (Anthropic) —
the agentic harness that drove the Claude models above; the orchestrator in
the two-agent phase.
- **[claude-code-delegate-local](https://github.com/fegone/claude-code-delegate-local)** —
the MCP bridge that let Claude and the local models talk (delegating Claude's
subagents to the local engines).
## How it works
The game code in `src/` is the decompilation's, and its control flow is the
reference we preserve. Everything that would touch N64 hardware is redirected into
`port/`: a **software RSP** (`port/fast3d/`, adapted from the Perfect Dark
port) that interprets the graphics command stream (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.
Every port change inside `src/` is gated behind `#ifdef PORT` and
documented. They are few in kind, all narrow in scope: mechanical 32→64-bit
pointer/struct fixes (the pointer-width transition touches many structs, so
there are a lot of them), 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), the opt-in *All unlocked* hook, and env-gated diagnostic
probes tied to the bug catalogue (off by default, no behavior change). Where it diverges from the Perfect Dark port: GoldenEye's N64
serialized asset formats are converted in a separate step rather than
fixed up at load time — on first start for a download, or by the offline
Python converters in `tools_pc/` for a source build. Full detail:
[`docs/internals.md`](internals.md) and
[`docs/porting-notes.md`](porting-notes.md).
```
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); port edits are #ifdef PORT
port/
fast3d/ software RSP -> OpenGL
src/ port layer (main, OS shims, video, audio, input, fs, ...)
include/ port-facing headers
tools/ Makefile the N64 build + asset extraction (from the decomp; do not modify)
tools_pc/ PC-port helper + analysis scripts
docs/ see below
```
## How this differs from the other GoldenEye PC projects
This is a native port of the original 1997 Nintendo 64 game, built from its
reconstructed source code, the same lineage as the Perfect Dark PC port.
Other GoldenEye PC projects take a different approach: they statically
recompile the Xbox 360 XBLA remaster, a separate version of the game with its
own codebase. The two don't share code, and they target different versions
of the game.
| | This project | The XBLA recompilation projects |
|---|---|---|
| **What it ports** | The original **Nintendo 64** game (1997) | The **Xbox 360 XBLA** remaster |
| **How** | Decompilation-based source port: reconstructed C, compiled for the host | Static recompilation of the Xbox 360 executable |
| **Lineage** | [GoldenEye 007 decompilation](https://github.com/n64decomp/007) + [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark) engine family | The Xbox 360 static-recompilation family |
+23 -8
View File
@@ -1,6 +1,6 @@
---
title: Building
description: Full build and asset-extraction guide for the GoldenEye 007 PC port, covering Windows (MSYS2) and Linux.
description: Full build and asset-extraction guide for the GoldenEye 007 PC port on Windows, Linux, and macOS.
---
## Building the PC port
@@ -18,7 +18,7 @@ Stages:
files the port loads at runtime. **Required to run.**
You need a GoldenEye 007 N64 ROM you legally own (`.z64`, big-endian). See the
[Requirements table in the README](https://github.com/jkdansereau/goldeneye-pc-port#requirements)
[ROM table in the user guide](user-guide.md#whats-a-rom)
for accepted versions and hashes.
---
@@ -27,21 +27,36 @@ for accepted versions and hashes.
### Port build
> **The Windows (MSYS2 MINGW64) path is the primary one.** The Linux build is
> compiled by CI on every push and ships in the release bundle (also
> playtested on Steam Deck hardware); the macOS column is best-effort
> guidance and has never been built or run there. Expect to fix build breaks
> yourself on untested platforms.
> **The Windows (MSYS2 MINGW64) path is the primary one.** Linux is compiled
> by CI and ships in the release bundle. **macOS on Apple Silicon is
> experimental as of v0.6.0**: the release has a `macos-arm64` download
> (`scripts/package-macos.sh` makes it on a Mac; CI runs it) that is not
> notarized, so clear the quarantine flag once before the first launch with
> `xattr -dr com.apple.quarantine <folder>`. This guide covers building it from
> source. On an arm64 Mac the build moves the N64 address window to a
> high host base (`PORT_ADDR_BASE`, set automatically); Windows and Linux builds
> are unaffected. The x86_64 macOS path (Julio C. Rocha, #88) is still in the
> tree but was never verified on Intel hardware.
| Need | Windows (MSYS2 MINGW64) | Debian/Ubuntu | macOS (Homebrew) |
|------|------------------------|---------------|------------------|
| toolchain | `mingw-w64-x86_64-toolchain` | `build-essential` | Xcode CLT / `gcc` |
| toolchain | `mingw-w64-x86_64-toolchain` | `build-essential` | Xcode CLT + Homebrew `gcc` |
| CMake | `mingw-w64-x86_64-cmake` | `cmake` | `cmake` |
| SDL2 | `mingw-w64-x86_64-SDL2` | `libsdl2-dev` | `sdl2` |
| zlib | `mingw-w64-x86_64-zlib` | `zlib1g-dev` | `zlib` |
| OpenGL | (in the toolchain) | `libgl1-mesa-dev` | (system) |
| Python 3 | `mingw-w64-x86_64-python` | `python3` | `python3` |
On macOS, install the build dependencies with:
```sh
brew install cmake gcc sdl2 zlib python3
```
The project requires real GNU GCC for the decomp's Plan 9 struct extensions.
Apple's `/usr/bin/gcc` is Clang and is not compatible. CMake automatically
selects Homebrew's versioned `gcc-N`/`g++-N` executables.
### Asset extraction (decompilation toolchain)
The extraction scripts need `binutils-mips-linux-gnu` (or an equivalent MIPS
+58
View File
@@ -255,6 +255,62 @@ real, already-granted case, kept as historical record) into a reusable
process; new requests cite this section rather than re-deriving the case
for a process each time.
## 8. LLM session discipline (model tiers, data-before-edit, context resets)
Standing rules for LLM-agent sessions, fixed 2026-10-09 after the session
forensics on the #150 cull hunt (pi session `01a11ec6`, 2026-10-09): a
27B 4-bit-quantized default model spent an evening firing unvalidated
render fixes at the maintainer for A/B (four consecutive failures), and
the mid-session compaction summary promoted an unvalidated hypothesis to
"diagnosed" — the post-compaction turns then built three more fix
candidates on that false premise. The loss was model + discipline, not
retrieval; the rules below are the zero- and low-cost countermeasures.
- **Model tiers.** Planning, diagnosis, and anything that touches a
contested claim run on the strongest model available — the frontier
`openrouter/deepseek/deepseek-v4-pro` (no local contention) or, with
the local backend switched, `strata-coder-hard`. Only ONE local backend
can run at a time (ninfer ↔ strata), so the **standing backend is ninfer** and the
session default is its model (`qwen3.8-27b-nvfp4full`, the
`ninfer-coding.bat` 4-bit daily driver). The strong *local* tier is
the same backend under `ninfer-quality.bat` (groupwise-int
`qwen3.8-27b`, 200k ctx) — a launcher restart, not a backend
switch; the strata 125B remains the top local option at the cost of a
full switch. Mechanical labor — builds, captures,
log triage, golden sweeps, doc sync — goes to the ninfer models via
the model-pinned subagent roles in the `pi/` package (see
`pi/README.md`): 4-bit for build/capture/triage/doc-sync, and
`pairing` pinned to `inherit` so the reasoning step follows the
parent session’s tier. 4-bit is never a planning or diagnosis model.
- **Data before edit.** If a probe log for the current hypothesis
already exists on disk, the next action is *analyzing it*, not building
another fix candidate. The maintainer gates only the final candidate;
intermediate variants are judged by the golden sweep (regressions are
machine-detected), not by maintainer A/B — one A/B is the gate, not
the test harness.
- **Context resets.** After a second failed maintainer A/B, or after a
compaction whose state is contested, do not continue the session: write
a HANDOFF brief and start a fresh session from it. Compaction summaries
must carry an **evidence ledger** — one line per live hypothesis tagged
`refuted` / `unvalidated` / `validated (<artifact: log, golden frame,
maintainer A/B, commit>)`, and `"diagnosed"` is forbidden for any fix
lacking a maintainer A/B or a passing golden sweep. The
`pi/extensions/compaction-ledger/` extension encodes this prompt (and,
with `GE_COMPACT_MODEL`, summarizes on a stronger model than the
session's labor model).
- **Regression triage first.** Before any probe or theory on a bug,
ask when it first appeared. If a release window is known, list the
commits in `git log <good>..<bad>` that touch the subsystem and run the
off switch each of them shipped with, one run per switch. The #150
wall-hug see-through (D579) spent days on cull/ucode/guard-band probes; a
single `GE_NEARCLIP=0` run named the v0.5.0 feature (D543) and the fix
was one line. Corollaries: when an A/B refutes a layer, leave that layer;
when a probe shows the code behaving correctly, pivot instead of probing
deeper; a delegated decode is a cross-check, never the lead.
- **Briefs are mandatory** (§1): no investigation kickoff without a
BUDGET line and a filled brief template — including the MODEL line
below, so the tier choice is visible in the brief itself.
## Investigation-brief template
```
@@ -263,6 +319,8 @@ READ FIRST: docs/porting-notes.md; docs/dev/findings.md <specific Dxx entries>.
FILES YOU MAY TOUCH: <disjoint from any other in-flight work>.
KNOWN-GOOD / RULED OUT: <list; do not re-investigate>.
PRE-FLIGHT ATTACHED: <probe output, baseline capture, PD-port pointer>.
MODEL: <tier + model id, per §8 — planning/diagnosis on the strong
model; mechanical labor delegated to the pi/ subagent roles>.
BUDGET: <N build->run cycles / ~M min>. On expiry: revert probes, write up with confidence.
CONSTRAINTS: diagnosis may trace as deep into src/game state as the evidence
needs — name the exact struct/field/logic responsible before concluding
+20
View File
@@ -19,6 +19,20 @@ Two classes:
| Env var | File:line | What it does | Status |
|---|---|---|---|
| `GE_INPUTRECORD` | `port/src/input.c:1547` | Records pad input to a file (D583 Deck harness; `levelbench.sh --record`). | live tooling |
| `GE_INPUTREPLAY` | `port/src/input.c:1548` | Replays a `GE_INPUTRECORD` file (`levelbench.sh --replay`; pin `GE_RSEED`). | live tooling |
| `GE_INTERPLOG` | `port/src/libultra.c:1482` | D578 IPLOG pacing trace (`=<n>` caps the lines). | live diag |
| `GE_INTERP_ALPHA1` | `port/src/libultra.c:1489` | D578: every interpolated pass exact (identity check). | live diag |
| `GE_INTERP_FORCE` | `port/src/video.c:1058` | D578: interpolate even when the display refresh is not above the VI rate. | live diag |
| `GE_INTERPCLIP_ON / GE_INTERPCLIP_OFF` | `port/fast3d/gfx_pc.cpp:1816` | D578 in-between depth-clip widening (default off since 2026-10-10). | live switch |
| `GE_INTERPPORTAL_OFF` | `port/fast3d/gfx_pc.cpp:4156` | D578: disable the portal-scissor replay on in-between passes. | live switch |
| `GE_INTERPROOM_OFF` | `port/fast3d/gfx_pc.cpp:6059` | D578: disable the room-change exact fallback. | live switch |
| `GE_INTERPREACH_OFF` | `port/fast3d/gfx_pc.cpp:6128` | D583: disable the REACH exact fallback. | live switch |
| `GE_INTERPOBJ_OFF` | `port/fast3d/gfx_pc.cpp:2626` | D955: in-between presents draw objects at their tick pose again (no per-joint motion blend; counters still run). A group whose joints step more than 120 units in a tick is drawn exact. | live switch |
| `GE_INTERPPROPCAM_OFF` | `port/fast3d/gfx_pc.cpp:2436` | D954: in-between presents draw props at the exact camera again (no room-camera blend on the plain perspective). | live switch |
| `GE_PCDUMP_PRESENTS` | `port/src/video.c:1539` | With `GE_PCDUMP`: one capture per present instead of per game frame. | live tooling |
| `GE_NOTICKLOCK` | `port/src/libultra.c:1321` | D600: disable the display-locked retrace tick. | live switch |
| `GE_ADDRSTRICT_SELFTEST` | `port/src/port_addr.c:122` | D607 (macOS): address-window self-test at startup. | live diag |
| `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: `"<frame>:<tok>,…;…"` (`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** |
@@ -134,6 +148,12 @@ Two classes:
| Env var | File:line | Finding / what it logged | Status |
|---|---|---|---|
| `GE_D589POOL` | `src/game/image.c:339,1202` | D589 (macOS): texture-pool decode trace. | open diag |
| `GE_D590HDR` | `src/game/bg.c:899` | D590: `load_bg_file` header trace. | closed |
| `GE_TEXDUMPROW` | `port/fast3d/gfx_pc.cpp:1480` | D591/D592 (macOS): per-row texture dump. | closed |
| `GE_TEXIMP2` | `port/fast3d/gfx_pc.cpp:1588` | D592 (macOS): import-order trace. | closed |
| `GE_TEXSRC` | `src/game/image.c:1042,1177` | D591 (macOS): texture source-address trace. | closed |
| `GE_ZLIBDUMP` | `src/game/image.c:317` | D611 (macOS): inflate input/output dump. | 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) — removed 2026-10-03 (strip batch 5, `chore/strip-batch5`); tombstone — the generator flags it GONE |
+41
View File
@@ -0,0 +1,41 @@
# GE_ diagnostic-probe catalog (keep vs strip before a `main` merge)
The `macos` track added a set of env-gated, diagnostic-only `GE_` probes to
triage the D589/D591/D592 wide-pixel + GL-layer work. All of them are **off
by default** (a `getenv` returns NULL → no code runs) and **change no game
behavior** when unset. The decision owed before any merge toward `main`
(§4, post-1.0) is, per probe: **keep** (a useful shipping debug tool) or
**strip** (diagnostic-only, no shipping value, or a `src/game` edit that is
not a port-layer fix). This file is that catalog — the tracker row is ROADMAP
§7 "GE_ probe catalog."
## The macOS-track texture / ABI probes
| Probe | Where | Gate | What it does | Disposition |
|---|---|---|---|---|
| `GE_TEXDUMP` | `port/fast3d/gfx_pc.cpp` (import) + `port/fast3d/gfx_opengl.cpp` (GL upload) | master, cached `getenv` | Per-import `GE_TEXI`/`GE_TEXN` log lines; `texdump/rNNN_*` normalized bins; GL-level `tNNN_*.ppm` + `.rgba32` (the exact `glTexImage2D` input, D592 Q1). Zero hot-loop cost when unset (the `getenv` is cached once). | **KEEP** — the standing cross-platform texture-triage tool (predates the macOS track; B2/D161). |
| `GE_TEXDUMPROW` | `port/fast3d/gfx_pc.cpp` | sub-gate (needs `GE_TEXDUMP`) | Dumps the FINAL post-transform source bytes keyed by the stable RAW source address (`texdump/row_<rawaddr>_f<fmt>_s<siz>.bin`) — the D592 cross-platform byte-compare key. | **KEEP** — cheap, off by default, and the raw-addr key is the right way to compare a specific texture across platforms (the positional rNNN index is not a stable key). |
| `GE_TEXRAW` | `port/fast3d/gfx_pc.cpp` | sub-gate (needs `GE_TEXDUMP`) | Writes the RAW source bytes handed to the importer (`texdump/rNNN_*.bin`). | **KEEP** — part of the same triage family. |
| `GE_TEXP` | **`src/game/image.c`** | independent `getenv` | D589 L2: logs pool-cursor state after each pool load. | **STRIPPED 2026-10-08** — `image.c` is back to the N64 build in this region; the pool-cursor dump served the D589 L2 cross-check only. |
| `GE_SIZEOF` | `port/src/main.c` | `#ifdef PORT` + `getenv` | Startup ABI-width canary (`sizeof` of key structs/pointers) for a quick 32/64-bit sanity check on a fresh platform. | **STRIPPED 2026-10-08** — the ongoing ABI-width check is the `tools_pc/abi_ratchet.py` CI ratchet; `main.c` also drops the `game/image.h` include it needed. |
| `GE_NPOT_NOGENMIP` (+ companion single-level sampler fix) | `port/fast3d/gfx_opengl.cpp` | `#ifdef PORT` + `getenv`, default off | The D592 NPOT `glGenerateMipmap` guard (skip mips on NPOT textures) + a companion that forced a single-level min filter under the guard. **REVERTED 2026-10-08 (`b80dcd08`)** when **D592 closed as a non-issue**: the "rock striping" was a VHF-metric false-positive (natural rock grain), and the `GE_TEXSTATE` probe showed the sampler state + NPOT mip level counts are byte-identical Win/Mac, so the guard had nothing to fix. | **REMOVED** — no longer in the tree (was `9bed5904` + companion `82dbea9c`). |
| `GE_TEXSTATE` | `port/fast3d/gfx_opengl.cpp` (upload + set_sampler) | `#ifdef PORT` + `getenv`, default off, needs `--debug-gl` (logs at `LOG_NOTE`) | D592 Q2: logged the **effective sampler state** for the bound texture + per-NPOT-shape mip level counts. Confirmed the sampler state **and** the NPOT mip levels are **byte-identical Win/Mac** (the Metal backend builds the full NPOT mip chain; the aniso extension is present on both), which is the core evidence that closed D592. | **STRIPPED 2026-10-08 (`b80dcd08`)** — one-off D592 GL-state diagnostic, removed with the guard when D592 closed as a non-issue. |
| `GE_KEYLOG` | `port/src/video.c` (`videoPumpEvents`) + `port/src/input.c` (`inputComputePadSlot`) | `getenv`, default off (writes `./ge_keylog.txt`) | D594: logs SDL key/mouse-button **events** on the main (host) thread (`E main KEYDOWN/KEYUP/MOUSEBT-…`) and the scheduler-thread **per-frame poll** (`P sched A/W/S/D/LMB/RMB`, change-only) to `./ge_keylog.txt`. The two streams together answer "do the key events reach the game, and does the *different* scheduler thread ever see them?" (the cross-thread poll is the D594 prime suspect). Validated on Windows (known-good input: poll logs the all-zero baseline; event lines fire on real key/mouse press). | **STRIPPED 2026-10-08** — both helpers and their two call sites removed (`video.c`, `input.c`). The D597 headless fire check is now "no SIGSEGV while firing" only (it no longer logs `LMB=1`). |
## Notes
- **Stripping is a mechanical, behavior-neutral edit**: delete the `getenv`
+ the env-gated block; nothing else references the probe (each is self-
contained). `GE_TEXP`, `GE_SIZEOF`, and `GE_KEYLOG` are the ones still in the
tree with a real "should not ship" character; the `GE_TEXDUMP` family is the
kind of triage tool that earns its place. `GE_NPOT_NOGENMIP` (+ its
companion) and `GE_TEXSTATE` are already gone (`b80dcd08`) with the D592
close-out.
- The broader `GE_` set (the ~70 vars in `port/`: `GE_QUITFRAME`, `GE_PCDUMP`,
`GE_RSEED`, `GE_DETERM`, the `GE_OPTIONSOVERLAY_*` UI probes, the `GE_ZF*`
z-fog family, etc.) is the standing debug-tool set and is **out of scope**
for this decision — it already ships. Only the **macOS-track additions**
above need a keep/strip verdict.
- **Ordering:** strip (or consciously keep) `GE_TEXP` + `GE_SIZEOF` +
`GE_KEYLOG` as part of the §4 post-1.0 macOS merge, *before* the branch goes
near `main`; leave the `GE_TEXDUMP` family in. (`GE_NPOT_NOGENMIP` +
`GE_TEXSTATE` are already stripped with the D592 close-out, `b80dcd08`.)
+21
View File
@@ -0,0 +1,21 @@
# Release gate driver
`tools_pc/release_gate.sh` runs the automatable pre-release checks and prints one
PASS / FAIL / SKIPPED table. It wraps `verify.sh` (pixel gate) and `levelbench.sh`
(scripted run, clean-quit and crash-log detection); every run ends through
`GE_QUITFRAME`, nothing is hard-killed.
```sh
tools_pc/release_gate.sh --dry-run all # print what would run
tools_pc/release_gate.sh win # sweep vs win goldens + fire smoke + exit path
tools_pc/release_gate.sh deck <host> # 21 levels via deckrunner (shortcut must be up)
GE_MAC_DIR=<remote game dir> tools_pc/release_gate.sh mac <host>
```
Options: `--secs N` (per-level run length, default 30), `--out DIR`. Hosts and
paths come only from arguments or env (`GE_WIN_DIR`, `GE_DECK_HOST`,
`GE_MAC_HOST`, `GE_MAC_DIR`, `GE_MAC_EXE`, `GE_MAC_BUILD_CMD`,
`GE_GATE_SSH_OPTS`). The Deck pixel compare is reported SKIPPED (640x480
goldens vs 1280x800 dumps); Mac has no goldens, so it is crash-free only. The
fire smoke is `GE_INPUTSCRIPT` `MLB1`/`MLB0` (D597, the D595 regression).
Remote job, result and script files are deleted after the summaries are pulled.
+64
View File
@@ -315,6 +315,18 @@ D569,?,Crosshair color follows the PD port:,CHANGED 2026-10-06 (maintainer reque
D570,FIXED,Scaled crosshair (Crosshair size != 100%) showed a faint copy of the top bar's tip under the bottom bar (WRAP tile flags) —,FIXED 2026-10-06 (port only:
D571,?,"Project icon restyled to the in-game sight red as pixel art (exe, window, favicon) —",CHANGED 2026-10-06 (maintainer request; make_icon.py; social preview unchanged)
D572,?,Linux tarball glibc floor lowered from 2.38 to 2.31:,CHANGED 2026-10-07 (maintainer request; packaging only; X220 glibc 2.36 fresh install PASS)
D573,FIXED,"sndPlaySfx / langGet pointer guards used an absolute [64 KiB, 16 GiB) literal; replaced by portAddrIsMapped() (DRAM arena views + mapped cart image) —","FIXED 2026-10-07 (from #108, italoarruda; Windows build + golden:"
D574,FIXED,Legacy front-end menu-pointer Velocity mode deleted (Input.MenuPointerMode / Input.MenuPointerSpeed); retired ini keys dropped silently —,FIXED 2026-10-07 (D561 follow-up; build + ini runtime check)
D575,FIXED,Update check (D551) gave no result on Linux/macOS when /releases/latest exceeded 64 KB (curl SIGPIPE); parse helpers moved to a header with an offline test —,FIXED 2026-10-07 (harness 19/19; live Linux/Deck matrix owed)
D576,FIXED,"ge007.log written next to the ini (previous run kept as ge007.prev.log, 8 MiB cap) —",FIXED 2026-10-07 (Windows runtime check)
D577,FIXED,"F10 slider labels ended in "".."" at HUD 150:",FIXED 2026-10-07 (captures HUD 100 unchanged / HUD 150 whole)
D578,OPEN,Frame interpolation above 60 fps (Video.FpsCap 90/120/144):,BUILT 2026-10-07 (Windows:
D579,FIXED,Issue #150: hugging a wall makes geometry vanish (N64 native widescreen does not).,"FIXED 2026-10-09 (maintainer-verified, Dam gate 16:9; golden sweep owed)"
D583,FIXED,120 fps + VSync on G-Sync/VRR:,"FIXED on PC 2026-10-09 (G-Sync OLED, maintainer by eye); Deck 90 good on most levels (per-level pass 2026-10-10); open:"
D584,FIXED,Cradle catwalk shadow flicker at every frame rate (regression from D579).,FIXED 2026-10-10 (maintainer Deck:
D614,OPEN,Deck Bunker fight texture-cache thrash:,"OPEN: measured, fix not shipped (2026-10-10)"
D605,FIXED,"Issue #145 (v0.6.0): F10 ""Invert look"" never reached the look stick in the HONEY role (game styles 1.1/1.3 -- left stick looks, right stick is the digital move…",FIXED 2026-10-09 (v0.6.0 line; port-only:
D606,FIXED,Issue #144 (v0.6.0 triage):,"FIXED 2026-10-09 (v0.6.0 line; port-only in port/src/optionsoverlay.c, src/game zero-diff; off by default so goldens stay byte-identical; smoke-verified:"
D407,CLOSED,Tanks cannot be boarded/exited on PC —,CLOSED (2026-09-28 bookkeeping:
D408,FIXED,Game.SkipIntro skips the post-mission failure dossier:,FIXED 2026-09-30 (SkipIntro hook gated to first boot; live dossier check owed).
D415,FIXED,"Shooting a light fixture does not ""kill"" its flickering light (Bunker, Caverns):",FIXED (2026-09-29 bookkeeping:
@@ -429,3 +441,55 @@ D534,FIXED,Pre-release review of the 2026-10-05 local batch —,FIXED 2026-10-05
D540,FIXED,Fog rework (Deck playtest):,FIXED 2026-10-05 (port:
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…"
D436,FIXED,"Persistent (hipfire) crosshair, opt-in:",FIXED (shipped 2026-09-29; rule-2 sign-off:
D617,OPEN,"The HD pack's ge007.tdb is the emulator's full per-draw key list, not the pack's PNG list —",OPEN — plan written (docs/dev/TEXPACK-RICE-DRAW-PLAN.md); whole-texture recipe confirmed correct by 9 tdb hits with exact ;WxH agreement; PNG-filename and brute-force param searches are dead ends
D618,?,"The community HD pack's non-CI keys are per-draw rects partly unreachable from N64 tile state (440x1 strips), and a rect key cannot land in the whole-texture p…",STOPPED 2026-10-06 — tdb ;WxH are per-draw rects (440x1 ×301 over tmem's 128-byte pitch = recompile-side); ~100 byte-domain variants score 0 hits; content-join does not discriminate; rect-keyed repla…
D619,?,"The community HD pack's remaining 1790 PNGs are blocked by four independent causes (recompile-side rect keys, wrong hash domain, rect-vs-whole-texture format,…",WRITTEN UP 2026-10-06 —
D620,OPEN,The HD pack's non-CI keys hash the pack's own re-encoded content —,MEASURED 2026-10-07 —
D621,PARTIAL,The PD dabs-mod texture-pack notes mine:,PLAN LANDED 2026-10-07 —
D622,?,B0 sprite-bank confirmation —,"PASS 2026-10-07 — two human-driven GE_TEXNUMDUMP+GE_TEXDUMP runs (front end 377 unique texnums / 521 dumps, -level_09 combat 321 / 316; evidence scratch/b0/):"
D623,OPEN,Phase B1–B3 measured:,MEASURED 2026-10-07 —
D624,OPEN,B4 content scorer built + tuned:,"BUILT 2026-10-07 — 5-stage pipeline (geometric prefilter → content rank → GT-tuned auto-accept → vision judge → human queue) per the 2026-10-07 decision (commits 512e6d57, 95d38637)."
D625,?,"texpack A5: downscale-to-GL-max instead of the D615 vanilla fallback, plus first-use decode-stall metering —","BUILT 2026-10-10, not yet A/B-verified —"
D626,FIXED,Windows DPI awareness was never declared:,FIXED 2026-10-10 (gfx_sdl2.cpp:
D627,FIXED,DexDrive / GameFAQs .n64 saves import directly:,"FIXED 2026-10-10 (eepimport.c + eep_convert.py dexdrive-to-pc); byte-identical to the reference extraction, malformed inputs rejected untouched; by-eye mission select owed"
D628,RESOLVED,"The exact-CRC CI half of the GoldenEye-007-HD join is sound (256 texnums, independently re-verified), but its structural half was scored against GE_TEXDUMP cap…","CI HALF ACCEPTED 2026-10-10 (256/256 (crc,fmt,siz) agreement with the rice census, 256 distinct texnums, palette-variant collision resolved by ROM palcrc, builder resolves all 256 sources); structura…"
D632,OPEN,Exit BSOD 0x119 recurred (2026-10-10) despite D344; the first minidump shows it fires in the kernel's process-exit GPU cleanup (DXGPROCESS::Destroy -> DXGCONTE…,FIX BUILT 2026-10-10 (fix/d632-exit-teardown):
D636,FIXED,Non-CI pack textures ARE fingerprint-keyed:,FIXED 2026-10-10 (feat/texpack-fp):
D637,?,Fingerprint fallback in the loader:,BUILT 2026-10-10 (feat/texpack-fp); C CRC == Python on 80 ROM glyphs; not run-verified (game runs on hold)
D638,FIXED,HD-pack ammo icon cut off:,FIXED 2026-10-10 (chore/v070-agentic-batch):
D639,OPEN,Exclusive fullscreen picked the wrong resolution:,FIX BUILT 2026-10-10 (chore/v070-agentic-batch):
D643,FIXED,Issues #145 (b)(c) / #149:,FIXED 2026-10-12 (port-only; GE_PADMAPTEST PASS; maintainer pad pass vs the #145/#149 reports; 1:1 N64-pad stick check owed when an adapter is available)
D644,FIXED,File-select royal crest drew on an opaque dark square (#146):,"FIXED 2026-10-12 (import_texture_ia4; 1964-confirmed symptom, maintainer-verified; Win sweep unchanged)"
D645,FIXED,"Original 1.1: F10 ""Invert look"" did not invert vertical look (#145 a):",FIXED 2026-10-12 (F10 row and watch row synced in Original; maintainer-verified)
D646,FIXED,"F10 dropdown popup drew over the hint/tooltip panel (popup bounded by the screen edge, not the settings box) —",FIXED 2026-10-12 (ddGeom bounded to the box; maintainer-verified)
D951,OPEN,"v0.6.0 release gate: Windows golden sweep flags are timing noise in the test, not rendering changes —",OPEN (test-side; 2026-10-13)
D952,FIXED,"macOS: black window, no frame, when the game starts while the display is asleep (SDL3 Cocoa VSync waits forever on a CVDisplayLink that never started) —","FIXED 2026-10-13 (port-only, gfx_sdl2.cpp; Mac A/B:"
D953,FIXED,Deck: wide Field of view feels choppier while turning —,FIXED 2026-10-13 (v0.6.0; LOD + prop fog fade/visibility follow the game FOV; maintainer Deck-confirmed)
D954,FIXED,"Doors and guards flicker at 90 fps, not at 60:","FIXED 2026-10-13 (port-only, gfx_pc.cpp + interpportal.c; maintainer Deck-confirmed)"
D955,FIXED,"Objects (props, doors, guards) moved at the 60 Hz tick inside a 90 Hz picture:","FIXED 2026-10-13 (port-only, gfx_pc.cpp + interpportal.c; sky pass; maintainer Deck-confirmed)"
D607,OPEN,Address model unified into one header:,DONE 2026-10-07 (macos branch; Windows identity gate:
D608,FIXED,D34 ANIM_DATA_x re-basing guard in assets/animationtable_data.h was __x86_64__-only:,"FIXED 2026-10-07 (macos branch; mechanical guard widening, arm64 link clean)"
D609,FIXED,macOS arm64 SIGKILL at exec:,"FIXED 2026-10-07 (macos branch; probe-confirmed; arm64 keeps the 4 GiB pagezero, floor 11.0)"
D610,FIXED,GE_MEMPOOL_CEILING (port/src/n64stubs.c) hard-coded the D441 base-0 arena top; now #if PORT_ADDR_BASE != 0-keyed so arm64 gets the arena-relative ceiling and b…,FIXED 2026-10-07 (macos branch; base-0 identity by construction)
D611,FIXED,"Mempool arena start is truncated to 32 bits at three carriers (osVirtualToPhysical, PHYS_TO_K0, src/boss.c:168 u32 start) so arm64 macOS banks live at arena-re…","DIAGNOSED 2026-10-07 (macos branch; arena-start leg fixed as D612, rest owed with the #95 re-application)"
D612,PARTIAL,Arena-start leg fixed (src/boss.c:,PARTIAL 2026-10-07 (macos branch:
D613,FIXED,PR #95 (macos branch) re-applied onto release/v0.5.1 —,FIXED 2026-10-07 (macos branch)
D586,FIXED,"Apple gen_macho_syms.py --base parse was dead code (undefined args), the shifted literals emitted bare (assembler ""expected newline""), and the generated .s dir…",FIXED 2026-10-07 (macos branch)
D587,FIXED,macos sweep: 9 of the first 13 levels SIGSEGV at one site —,FIXED 2026-10-07 (macos branch)
D588,FIXED,Windows x86_64 SIGSEGV (deterministic 3/3) in texReadBits under lvlStageLoad:,FIXED 2026-10-07 (macos branch)
D589,RESOLVED,macOS arm64: wide-pixel (32-bit RGBA / 16-bit) textures render as colourful static / confetti (the Surface 1/2 billboard trees) while x86_64 renders them corre…,RESOLVED 2026-10-08 (macos branch; byte order fixed+verified = D591; the SECOND CAUSE D592 is CLOSED as a non-issue —
D590,FIXED,"Windows x86_64 deterministic SIGSEGV in load_bg_file (bg-load worker thread, 4/4 incl.",FIXED 2026-10-08 (macos branch)
D591,FIXED,arm64 macOS fast3d mis-classifies every DRAM/cart texture source as a “C-compiled array” and byte-swaps it —,FIXED + Mac A/B VERIFIED 2026-10-08 (macos branch; 9c59cbe7) —
D592,CLOSED,"arm64 macOS — the ""rock striping"" (the remaining cross-platform frame diff after the D591 byte-order fix) is a VHF-metric FALSE-POSITIVE, not a GL-layer defect.",CLOSED (non-issue) 2026-10-08 (macos branch; VHF-metric false-positive —
D593,OPEN,"Windows x86_64 — an occasional render-thread stall fired the kernel's ""no frame rendered for N ms"" watchdog (observed once, 8001 ms, on an NVIDIA RTX 5090 duri…",OPEN 2026-10-08 (Windows; single observation; repro + root-cause owed)
D594,FIXED,arm64 macOS — partial input:,FIXED 2026-10-08 (macos branch; actHeld menu gate + macOS reconcileGrab focus gate → PD MLOCK_ON; LMB fire verified on the Mac; unblocked D595)
D595,FIXED,arm64 macOS — deterministic SIGSEGV in modelSetAnimFrame when an AI plays a hit/stagger/death animation (fire → triggered_on_shot_hit → modelSetAnimation2); th…,FIXED 2026-10-08 (macos branch; read the base at full pointer width —
D596,FIXED,"the win/ golden set predates D565/D569/D570, so verify.sh on the macos branch (and the main tip) reported a REGRESSION worst_cell=3.106875 that is a stale refe…","FIXED 2026-10-08 (macos branch; win goldens regenerated from e0a2d4b9, 21/21 PASS; recipe-side only, no game-code change; the linux/deck sets need their own re-capture round before any new cross-plat…"
D597,FIXED,"GE_INPUTSCRIPT gains MLB<n>/MRB<n> (sustained mouse-button tokens, slot 0):",FIXED 2026-10-08 (macos branch; Windows menu + forced-level keylog checks; Mac sweep-fire no-crash run + no-env no-op run; the live E1 LMB fire is the struck-animation ground truth; cmd-wrapper gotch…
D598,RESOLVED,"arm64 macOS gameplay is ""a bit choppy"" on heavy levels (Cradle-class) —",RESOLVED 2026-10-08 by D600 (display-locked retrace tick; live playtest confirmation owed)
D599,FIXED,arm64 macOS: SIGBUS (signal 10) the moment a game controller is connected —,FIXED 2026-10-08 (macos branch; Mac M3 re-run with a connected Xbox pad:
D600,FIXED,VSync-on frame pacing:,FIXED + LIVE-CONFIRMED 2026-10-08 (macos branch; Mac M3:
D601,FIXED,"Long-session hang at stage load (macOS playtest, Archives):","FIXED + LIVE-CONFIRMED 2026-10-08 (macos branch; #ifdef PORT guard, D152/D285 class; evidence = live lldb read of the hung process; maintainer full-campaign completion incl."
D602,OPEN,macOS end-credits SIGBUS:,"MITIGATED 2026-10-08, OPEN (macos branch; langGet returns NULL for offset > 1 MiB and logs the bank state once as D602:; needs one natural-flow re-trigger to diagnose)"
D603,FIXED,"arm64 macOS multiplayer (2P, stage 38) SIGSEGV in modelSetAnimFrame via playerTick:","FIXED 2026-10-08, live confirmation owed (macos branch; identity at PORT_ADDR_BASE==0; headless 2/3/4P + solo smoke clean but the death/firing-animation path is not reachable headlessly)"
D604,FIXED,arm64 macOS multiplayer respawn SIGSEGV:,FIXED + LIVE-CONFIRMED 2026-10-08 (macos branch; maintainer 2P respawn OK; identity at PORT_ADDR_BASE==0)
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
315 D570
316 D571
317 D572
318 D573
319 D574
320 D575
321 D576
322 D577
323 D578
324 D579
325 D583
326 D584
327 D614
328 D605
329 D606
330 D407
331 D408
332 D415
441 D540
442 D450
443 D436
444 D617
445 D618
446 D619
447 D620
448 D621
449 D622
450 D623
451 D624
452 D625
453 D626
454 D627
455 D628
456 D632
457 D636
458 D637
459 D638
460 D639
461 D643
462 D644
463 D645
464 D646
465 D951
466 D952
467 D953
468 D954
469 D955
470 D607
471 D608
472 D609
473 D610
474 D611
475 D612
476 D613
477 D586
478 D587
479 D588
480 D589
481 D590
482 D591
483 D592
484 D593
485 D594
486 D595
487 D596
488 D597
489 D598
490 D599
491 D600
492 D601
493 D602
494 D603
495 D604
+3663 -2
View File
File diff suppressed because it is too large. Load diff
+29 -4
View File
@@ -1,14 +1,15 @@
---
title: Framerate and pacing
description: How the GoldenEye 007 PC port holds a steady 60 fps and keeps per-frame game timing correct on a fast CPU.
description: How the GoldenEye 007 PC port holds a steady 60 fps, draws frames above 60 on fast displays, and keeps per-frame game timing correct on a fast CPU.
date: '2026-10-05'
modified_time: '2026-10-05'
modified_time: '2026-10-10'
---
## Framerate and pacing
The port targets a **steady 60 fps** with VSync and an optional 30/60 frame
cap, including on low-end GPUs.
The port targets a **steady 60 fps** with VSync and a frame cap, including on
low-end GPUs. Since v0.6.0 the cap can also go above 60 on a faster display
(see [Above 60 fps](#above-60-fps)).
The N64's game code assumed a **60 Hz vertical-interrupt tick** — it ran a
few ticks per displayed frame. On a PC the CPU is far faster, so left alone
@@ -27,5 +28,29 @@ On very weak integrated GPUs (Atom/Celeron class) the first launch writes
lighter values for draw distance, LOD distance and anti-aliasing into
`ge007.ini` so the game still holds 60 fps.
### Above 60 fps
`Video.FpsCap` (F10, *Frame rate cap*) accepts Auto, 30, 60, 90, 120, 144, 165 and 240.
**Auto** is the default on fresh installs (an existing `ge007.ini` keeps its
value) and matches the display's refresh rate, up to 240 (the renderer draws at most four
frames per game tick); on a 60 Hz display it
is plain 60.
- **The game still simulates at its original 60 Hz (NTSC) tick.** The extra
frames are drawn between two game ticks by blending the previous and current
frame's matrices (camera and objects), so no game state changes.
- **Cost:** one game tick of extra latency.
- **Inert on 60 Hz displays.** The present rate is the lower of the cap and the
display's refresh rate. The *Original N64* display mode caps at 60.
- **Fallbacks:** a fast camera turn or a large change in the visible room set
draws a full (non-blended) frame instead.
- The design comes from f1zz1ec0ke's PR #137; the port reworked it onto the
render worker thread and from a fixed 2x to any present rate, so a 90 Hz
Steam Deck OLED works (D578, D583, D584).
Checked by the maintainer: 120 Hz with VSync on a G-Sync display on PC, and 90
fps on the Steam Deck. Known issue: on the Deck at 90, centre firefights in
Bunker 1/2 can drop several fps, and Jungle can feel slightly less smooth.
*More: [porting notes](porting-notes.md) — the timing-related findings (D13,
D22, D486).*
+37 -12
View File
@@ -1,15 +1,15 @@
---
title: GoldenEye 007 PC Port
# v0.5.0: status bumped from v0.4.0. Description kept short (Bing's 160 limit).
# v0.6.0: status bumped from v0.5.0. Description kept short (Bing's 160 limit).
description: >-
A native PC port of GoldenEye 007 (Nintendo 64, 1997), built from
decompiled source with a software RSP. v0.5.0 for Windows, Linux and
decompiled source with a software RSP. v0.6.0 for Windows, Linux and
Steam Deck.
# Freshness signals for jekyll-seo-tag (article:published_time / modified_time
# and the WebPage JSON-LD dates). Keep these equal to the last real content
# change of THIS page, not to the last release.
date: '2026-10-05'
modified_time: '2026-10-07'
modified_time: '2026-10-13'
locale: en_US
# The masthead is this page's title, so its first content h2 must NOT get the
# amber page-title look (double header). See the .flat-title rule in
@@ -30,12 +30,35 @@ Rare "Indy" engine family, one hardware generation apart.
## Status
<div class="cblock">
<div class="cblock" markdown="1">
### v0.5.0 (2026-10-07) — split-screen multiplayer, one options menu, closer to the N64.
### v0.6.0 (2026-10-13) — frame rates above 60, experimental macOS, fixes.
- **2–4 player split-screen multiplayer** on every multiplayer map, each
player with their own pad, controls and aim settings.
- **Frame rates above 60:** the frame cap offers 90, 120, 144, 165 and 240, and **Auto**
(the default on fresh installs) matches your display. Frames between game
ticks are blended; the game still simulates at its original rate. It costs
one game tick of latency and does nothing on a 60 Hz display. A rework of
f1zz1ec0ke's #137. See [framerate and pacing](framerate-and-pacing.md).
- **macOS on Apple Silicon, experimental:** a `macos-arm64` download (not
notarized; run `xattr -dr com.apple.quarantine <folder>` once before the
first launch) or build from source. Based on danturn's #95.
- **Fixes:** geometry vanishing against a wall (#150), the first launch opening
on the wrong monitor (#151), a Linux end-credits crash (#152), a hat-spawn
crash (#153), the Cradle catwalk shadow flicker, and stale pixels beside the
native-widescreen picture.
- **Known issues:** on the Steam Deck at 90 fps, Bunker 1/2 centre firefights
can drop several fps and Jungle can feel slightly less smooth; Cradle's
turret explosions may draw wrongly (not yet investigated).
</div>
<details>
<summary>v0.5.0 (2026-10-07) — split-screen multiplayer, one options menu, closer to the N64</summary>
<div class="cblock" markdown="1">
- **Split-screen multiplayer for up to 4 players**, each player with their
own pad, controls and aim settings.
- **One options menu** for every setting, laid out like the Perfect Dark
port's, with the game's own crosshair as the mouse pointer. Pick the game's
own N64 control styles (1.1–2.4) per player, or the Xbox release's presets
@@ -57,6 +80,7 @@ Rare "Indy" engine family, one hardware generation apart.
look-ahead defaults), and controllers are picked up again when replugged.
</div>
</details>
What remains is a short list, under [Known issues](#known-issues).
@@ -74,7 +98,7 @@ single tracker for what comes next.
<ul>
<li><strong>NTSC-U ROMs only in the release packages.</strong> PAL and JP ROMs convert, build and run from source, but release packages ship the US (NTSC-U) ROM. On the roadmap.</li>
<li><strong>Changing the aspect ratio in a level can briefly glitch the gun/hand model</strong> (cosmetic, one-off). Change the ratio from the front-end PC Options, or accept it (D509).</li>
<li><strong>No macOS or ARM builds yet.</strong> Today the release ships Windows, Linux and Steam Deck (keyboard, mouse and controller-button rebinding are all supported, defaults in the <a href="https://github.com/jkdansereau/goldeneye-pc-port#controls">README's Controls section</a>); macOS and ARM are on the roadmap.</li>
<li><strong>No ARM Linux build; macOS is experimental.</strong> The release ships Windows, Linux and Steam Deck (keyboard, mouse and controller-button rebinding are all supported, defaults in the <a href="https://github.com/jkdansereau/goldeneye-pc-port#controls">README's Controls section</a>). macOS on Apple Silicon is an experimental, not-notarized download (run <code>xattr -dr com.apple.quarantine &lt;folder&gt;</code> before the first launch) or a source build; ARM Linux is on the roadmap.</li>
<li><strong>Saves from v0.4.0 and earlier can hold fake unlocks from <code>All unlocked</code></strong> — <code>All unlocked</code> has not written your save since v0.5.0, but saves made while it was on in v0.4.0 and earlier can hold fake unlocks and are not repaired; back up <code>data/ge007.eep</code> if you used it on an older build (D442, D387).</li>
<li><strong>The first frame of a level takes a little longer</strong> while its textures upload — a brief FPS-counter dip; nothing to fix (D475, D480).</li>
<li><strong>In widescreen, far objects almost fully in fog can still show a faint distant building edge</strong> (4:3 matches the N64). Cosmetic (D503).</li>
@@ -88,7 +112,7 @@ single tracker for what comes next.
<p class="warn"><strong>Warning:</strong> always use the latest release — earlier builds are kept in the release history for reference only; they lack the features and fixes of newer versions, so don't install an older release.</p>
<ul>
<li><strong>2026-10-07 — v0.5.0</strong>: what it adds is the Status list above. <a href="https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.5.0">Release notes</a>.</li>
<li><strong>2026-10-07 — v0.5.0</strong>: split-screen multiplayer, one options menu, closer-to-N64 fog and fidelity fixes, emulator save import. <a href="https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.5.0">Release notes</a>.</li>
<li><strong>2026-09-28 — v0.4.0</strong>: 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. <a href="https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.4.0">Release notes</a>.</li>
<li><strong>2026-09-20 — v0.3.0</strong>: the first release with the complete campaign playable end to end at 60 fps on Windows, Linux, and Steam Deck. <a href="https://github.com/jkdansereau/goldeneye-pc-port/releases/tag/v0.3.0">Release notes</a>.</li>
<li><strong>2026-09-04 → 2026-09-16 — v0.1.0 – v0.2.2</strong>: the alpha and beta cycle — build chain, software RSP, first rendered frames, front end, and per-level stabilization across the campaign.</li>
@@ -98,13 +122,14 @@ single tracker for what comes next.
## Download
<div class="cblock">
<div class="cblock" markdown="1">
| 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. |
| macOS / ARM Linux | — | Roadmap: not shipped yet. |
| **macOS** (Apple Silicon, experimental) | macos-arm64 tarball | SDL2 and the C++ runtime are bundled. Not notarized: run `xattr -dr com.apple.quarantine <folder>` once before the first launch. Tested on one Mac; reports welcome. |
| ARM Linux | — | No download; on the roadmap. |
**Bring your own ROM.** Both bundles contain no ROM and no game assets: you
supply your own GoldenEye 007 N64 ROM (the
@@ -214,7 +239,7 @@ architecture, the N64→PC bug catalogue, and how the project is developed.
i = (n + slides.length) % slides.length;
img.src = slides[i].src;
img.alt = "GoldenEye 007 PC port — " + slides[i].cap +
", in-engine capture, 3072×1728";
", in-engine capture, 3840×2160";
cap.textContent = slides[i].cap;
idx.textContent = (i + 1) + " / " + slides.length;
}
+136
View File
@@ -296,6 +296,14 @@ When a PC-larger asset gets a bigger reservation inside a shared scratch buffer
**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.
**A1 cross-tag (D588, 2026-10-07): re-basing a LIVE host pointer through `portN64ToHost` mis-maps a native (out-of-window) pointer that *collides* with the D131 image-rel range.** `texSetBitstring` took the compressed-source cursor as an `s32` and re-based it (`portN64ToHost((u32)pos)`) — correct for an in-window (macOS) pointer whose low 32 bits are a genuine N64 offset, but wrong for a **native** pointer (e.g. the Windows libwinpthread worker-thread stack at `0x618d…`–`0x620d…`, whose low 32 bits fall in `[0x40M,0x70M)`): `portN64ToHost` maps it onto the PE image (`imagebase + (low32 − 0x40M)`), landing past the image end → deterministic SIGSEGV in `texReadBits`. Rule: a *stored N64 u32 field / DRAM address* is the legitimate `portN64ToHost` re-base input; a *live host pointer* must be carried at full pointer width (take the parameter as `u8 *` / `void *` under `#ifdef PORT`) so no truncate-then-rebase happens at all. Grep for `portN64ToHost((u32)<live-ptr-param>` — the feeder `rzipGetSomething()` (truncating `s_rz_nextin` to `s32`) was the second site. §F D588.
**A1 cross-tag (D590, 2026-10-08): the same collision, one site up the call chain, and the general diagnostic tell.** `load_bg_file` reads its stack `header` array through `PORT_N64PTR(void, ptr_bg_data)` where `ptr_bg_data` is a full-width host `bg_addr_t` (D457) — the truncate-then-re-base maps the bg-load worker stack onto the PE image and faults at `header[1]`. **Diagnostic tell for the whole class: if the fault address equals `imagebase + (low32(faulting pointer) − 0x40000000)` — i.e. the top 32 bits were dropped and the remainder re-based into the D131 image-rel window — the bug is a live host pointer fed through `portN64ToHost` / `PORT_N64PTR`, not a game-logic fault.** The check is a one-line arithmetic compare against the crash log's FAULT ADDR (Windows: `ge007.crash.log` prints both; `R8`/`RSP` in the same record give the raw pointer) — far cheaper than rebuild-and-run hypothesis testing. `PORT_ADDR_STRICT` does NOT catch this variant: strict only rejects u32s that map to no known N64 region, and a colliding in-window low32 passes it (a clean strict run is path-coverage evidence only). The legitimate `PORT_N64PTR` operand is a *stored N64 u32 field or DRAM address* (ROM-serialized records); a stack array, a `mempAllocBytesInBank` result, or any D457-retyped host pointer must not go through it. §F D590.
**A1 cross-tag (D591, 2026-10-08): host-space vs N64-raw-space mixup in a range *classification* test (not a rebase) — bswaps every in-window source on the `PORT_ADDR_BASE != 0` platform.** `gfx_tex_source_is_c_array` (`port/fast3d/gfx_pc.cpp`) stripped `PORT_ADDR_BASE` first (putting the pointer in N64 raw space, `0x70xxxxxx`), then compared against **host-space** bounds — `PORT_DRAM_V1_BASE`/`PORT_DRAM_K0_BASE` are `PORT_ADDR_BASE + 0x70000000`/`+ 0x80000000` (`port/include/portaddr.h`), i.e. `0x100070000000` on arm64 macOS. At `PORT_ADDR_BASE == 0` (x86_64) the two spaces coincide and the test is accidentally correct; at 16 TiB the raw pool offset is *above* the raw cart window and *below* the host-space DRAM bases, so every DRAM and cart texture source fails all three range checks and falls through to the C-array default — `gfx_tex_normalize_source` then hands fast3d a `PD_BE32`-swapped copy of the whole texture pool (the D589 confetti / D219-M-114 pink-HUD symptom class; a clean rev32 in the import-time dumps while the decode-time pool stays byte-identical). **Diagnostic tell: a texture-format-dependent byte-reversal that appears on *one* platform, with the pool-write side proven clean (decode-time dumps byte-identical) — check the platform-gated *classification* of the source address, not the writer; and on any `PORT_ADDR_BASE != 0` build, grep for range tests that strip the base before comparing to `PORT_DRAM_*` bounds (or, conversely, compare a raw offset to host bases). The test must use one space end to end: the host pointer against host-space bounds, or the raw offset against raw bases.** §F D591.
**A1 cross-tag (D595, 2026-10-08): the simplest member — a plain `(s32)` *read* of a live host pointer as the rebase *base* (no `portN64ToHost`, no classification), silently identity on x86_64 and wrong at `PORT_ADDR_BASE != 0`.** `initResolveAnimTable` (`src/game/initactorpropstuff.c`) resolves the `StruckAnim` reaction tables (`death_stagger`, `g_HitReactionTable[*].{death,flinch}Anims`) as `struck_anim = base + offset`, where `base` is the live host pointer `ptr_animation_table` (`mempAllocBytesInBank(…, MEMPOOL_PERMANENT)`) and `offset` is a small N64 `PTR_ANIM_*` (e.g. `0x1C`). The PORT path read the base as `(*((s32 *)entries))`: on x86_64 the PERMANENT bank is s32-low (`0x70xxxxxx`) so the 4-byte read is identity; on arm64 macOS (`PORT_ADDR_BASE` = 16 TiB, `0x10007xxxxxx`) it drops the high 32 bits, so every `struck_anim` landed in bogus low memory and the first AI hit/stagger/death reaction (`triggered_on_shot_hit → modelSetAnimation2 → modelSetAnimFrame` deref of `anim->unk07`) SIGSEGV'd. **Diagnostic tell: an arm64-only NULL/garbage-pointer fault in an *animation / table-resolution* path where the pointer is `base + small-rom-offset` and `base` is a `mempAllocBytesInBank` result — grep the `(s32)`/`(u32)` reads of such bases (the sibling sites `bondhead.c` / `bondview2.c` already use a full-width `(uintptr_t)&ptr_animation_table->data`; `initResolveAnimTable` was the one still on the `(s32)` form).** Fix: read the base at full width (`(*entries).struck_anim`), add the offset as `u32`; identity on every `PORT_ADDR_BASE == 0` platform. §F D595.
## B. 16-byte PC `Gfx` / `Vtx` vs 8-byte N64
Any buffer reservation, `memcpy` size, slot stride, or pool budget
@@ -589,6 +597,30 @@ through a converter or a runtime bswap fixup reads scrambled.
## C2. Port-layer / SDL shims
- **Darwin assembly uses Mach-O symbol spelling.** GAS absolute symbols that
are `.global foo` / `.set foo, address` on ELF/PE must become `.globl _foo`
/ `.set _foo, address`; Darwin also rejects the ELF `.section .data`
spelling used by these symbol-only files. Generate transformed build-tree
copies rather than changing the cross-platform source files (D294).
- **Darwin GCC cannot lower ELF-style `#pragma weak alias = target`.** It can
ICE in `assemble_alias`; compile generated copies with those pragmas removed
and provide Mach-O indirect symbols (`.set _alias, _target`) separately
(D294).
- **Mach-O's default `__PAGEZERO` reserves the entire low 4 GiB.** A build
can compile and link successfully yet fail every fixed N64 mapping at
runtime (`0x10000000`, `0x70000000`, `0x80000000`). Link with
`-Wl,-pagezero_size,0x10000` so null-page pointers remain protected while
the cartridge and DRAM ranges are available (D294).
- **Darwin has no `MAP_FIXED_NOREPLACE`; a fixed-address hint can relocate.**
Once `__PAGEZERO` is deliberately bounded below the cart range, use
`MAP_FIXED` for the `0x10000000` cartridge mapping. Retaining the advisory
hint silently falls back to a heap ROM and later crashes on direct absolute
ROM-symbol reads (D294).
- **AppKit event polling is main-thread-only.** This port has both a host-main
`videoPumpEvents()` loop and fast3d's render-thread event pump. The latter
must be disabled on macOS or `SDL_PollEvent` raises
`NSInternalInconsistencyException`; the main-thread loop already handles
the complete event set (D294).
- `#include <PR/os.h>` in a port `.c`/`.h` that also sees `<errno.h>`
breaks: `OSContStatus`/`OSContPad` have a `u8 errno;` field vs errno.h's
macro. libultra.c wraps the include in `#pragma push_macro("errno")` /
@@ -644,6 +676,8 @@ through a converter or a runtime bswap fixup reads scrambled.
- **CPU-built RDP triangles need float vertices, not `Vtx` (D245).** Where GE builds RDP edge/texture coefficients on the CPU (sky/water), 32-bit S/T and positions are normal; squeezing them through s16 `tc`/`ob` costs visible precision on horizon-scale geometry. Use `G_FLOATVTX_EXT` (`port/include/floatvtx.h`).
- **A first-run-only preset is skipped forever by any earlier launch that created the ini, and window keys are not an "untouched" signal (D283).** Seeding values *before* `configLoad()` only works when no `ge007.ini` exists; one launch in a different environment (SteamOS Desktop Mode has no `STEAMOS`) writes a plain ini and the preset never runs. Apply presets *after* the load, record a one-shot flag key, and gate on keys the user must change deliberately (`Video.Fullscreen`, `Window.Maximized`) — `Window.Width/Height` are rewritten from the live window on every clean exit (`videoSaveWindowState`), so "still at default" never holds for them.
**SDL3 on macOS: a GL swap with VSync can wait forever (D952).** SDL3's Cocoa backend implements the GL swap interval itself: `SDL_GL_SwapWindow` waits, with no timeout, for a tick from a `CVDisplayLink` that SDL creates and starts once, with the context. If that link never runs (seen when the game starts while the display is asleep), the first swap never returns: black window, zero frames, every other thread waiting behind the render thread. Tells: `sample` shows `Cocoa_GL_SwapWindow -> SDL_WaitCondition*` on the render thread and the process has no `CVDisplayLink` thread. Fix pattern: leave SDL's interval at 0 and set `kCGLCPSwapInterval` on the context with `CGLSetParameter`, which waits in the driver (SDL2's behaviour). Reproduce with `pmset displaysleepnow` before launching.
## D. N64 hardware idioms fast3d does not emulate
- **Appending a port-owned 2D overlay to the game frame: hook inside
@@ -1445,6 +1479,18 @@ Also the read side: D496 (`generate_language_specific_text_for_weapon` strcat'd
`GE_PCDUMP`/`GE_QUITFRAME` step the port's *sim* frame counter (`port/src/video.c:1406` `++frames`, one per `portRenderGfxTask()` at `port/src/libultra.c:1256`), and retrace messages are dropped at the posting site when the game's queue is nearly full (`port/src/libultra.c:753`), so a slow box stretches wall time without advancing `frames` -- frame stems are comparable, wall time is not. What is NOT comparable is any stem inside the intro flyby, whose settle point is wall-clock paced (D117): on a ~54 fps Linux box the flyby was still running at frame 900 (settles ~830-1130 run to run) while the Windows golden at 900 was settled, which reads as a 92.9% "different scene" delta if reported bare. Tells: over-tol2 % >30% at one stem and 1-7% at the next; phash Hamming in the 80s; a dense dump whose consecutive diffs sit at 80-95% and drop to 1-5% once settled. Never compare across platforms with `GE_DETERM=1` (call-sequenced retraces: stable run to run, different frame -> sim mapping). Before calling a residual band a renderer bug, classify it: a constant ~20 delta confined to a screen band (Archives' top 3 grid rows, all three stems) is systematic and needs a same-sim-state capture pair; a uniform d 3-6 over the whole frame with cell means <= 11 (Cuba) is the dither/quantization noise floor. **RESOLVED 2026-10-05 (D525):** both residual bands were *save state*, not renderer — the pair that closed them was a re-round of the whole linux set with `data/ge007.eep` pinned present (Archives 19.956-24.554% -> 3.590-3.859%, worst cell 50.8 -> 2.31; Streets 1200's 16.0-148.4 cell-mean band -> 3.616-3.983%, worst cell 4.43). The diagnostic tell that separates the two: re-capture one side with the save present and the band collapses to the noise floor, while a renderer defect survives identical sim state.
## D26. Never bound a host address with an absolute number (D573)
A plausibility guard written as `p < 0x10000 || p >= 0x400000000ULL` encodes today's layout (DRAM at `0x7000_0000`, cart at `0x1000_0000`). It silently rejects every real pointer once the arena moves (macOS `PORT_ADDR_BASE`, the D441 above-4-GiB tier), and it accepts any unmapped address below the bound. Test the actual windows with `portAddrIsMapped()` (`portaddr.h`: DRAM arena views + mapped cart image). Tells: a feature that goes completely silent or blank (all SFX, all text) on one platform or arena flavour while the rest of the game runs; a guard comment that names the windows next to a literal that does not.
## D27. Rendering one display list twice: per-frame renderer state must not advance per pass (D578)
Frame interpolation (D578) draws each game DL several times. Everything a fresh frame starts from has to be the same for every pass: emulated RSP/RDP state, segment table, viewport caches, and the GL backend's noise seed (`frame_count` in `gfx_opengl.cpp`, which advances in `start_frame` and seeds dither noise; letting it advance per pass changed thousands of pixels on early-level frames). Do NOT restore caches that mirror live GL state (`rendering_state`). Tells: a pass-0 vs pass-N diff (`GE_INTERP_SELFCHECK`) that is large on a few frames (noise, dithered alpha) or 1-4 px on rare frames (a DL relying on the texture left bound by the previous frame). Also: never re-read a game DL after posting SP/DP done; draw every pass before it.
## D29. A "default depends on whether the user chose" needs the load-time `seen` flag, not the value (D578)
`configLoad` writes the registered defaults to a missing ini *before* subsystems init, so by `videoInit` the file already contains every key and its value cannot tell "fresh" from "chosen". Use `configIntKeyWasLoaded(key)` (the per-option `seen` flag from the last load) for fresh-install-only defaults (gamescope VSync off), and re-`configSave()` if the override must persist. Likewise a new sentinel for an existing numeric key (FpsCap Auto = -1) must not reuse 0 when old inis already give 0 a meaning, and the registered min must be widened or the load clamps it away.
## E. Process / method notes
- **Never `exit()` while another thread may be inside the GL driver (D344).**
@@ -1884,3 +1930,93 @@ Also the read side: D496 (`generate_language_specific_text_for_weapon` strcat'd
### The options row table has two UIs; new row kinds need a front-end answer (menu PD alignment)
`port/src/optionsoverlay.c`'s `rows[]` is consumed by the F10 overlay and by `frontoptions.c` through the `optionsRow*` accessors. A row kind that only one UI understands (separator, Back, hub-only actions) must be hidden from the other in `optionsRowIsShown()`, and should be a `ROW_ACTION` with a key prefix so the section-reset and reset-probe loops (which skip `ROW_ACTION`) stay correct. Moving a row between sections silently changes what that section's "Reset to defaults" resets: re-run `GE_WSPROBE_RESET` after any regroup.
### Never put a blocking vsync swap ahead of the draw the game is waiting on (D578)
A render worker that must post "task done" after drawing must not swap (a blocking VSync present) before the draw: under a compositor (gamescope) each swap blocks a full refresh and the game misses retraces while CPU/GPU idle. Draw, post done, then present; derive interpolation alphas from predicted vblanks (swap-return times), not a free-running timer.
## D28. A "fall back to exact" gate on a per-tick rate needs a graded response, not a cliff (D578)
A fallback that turns interpolation off for the whole frame above a low threshold (1.5 deg/tick view turn) fired on ordinary stick turns (9-64% of frames on a 90 Hz Deck). Prefer pulling the blend alphas toward 1 in proportion to threshold/measured (bounded offset, still smooth) and reserve full fallback for genuine cuts. Also: the display list references live game memory, so deferring a draw past SP/DP done is unsafe without copying every referenced buffer.
A second, unscaled cliff hid behind the first: the modelview 6 deg/tick gate set "exact" directly, so softening the projection check changed nothing. When softening a fallback, grep every writer of the fallback flag. Also: a per-pass cost estimate must divide by passes actually drawn (discarded restarts included) and clamp single outliers, or one exact restart collapses the pass budget (D578).
## D30. Detect "the culled set changed" from a stable per-item id in the DL, not from matrix pairing (D578)
When a game builds its visible set and per-item clip rects for the current camera only, an interpolated pass cannot repair a changed set. The cheapest exact detector is a per-item marker already in the DL (GE: `gSPSegment(14, room vertices)` before each room's `gSPDisplayList`; the base is unique per room). Record the sorted unique ids per game frame and compare prev -> cur; do not infer it from matrix-pairing counts (rooms share matrices). Clip rects derived from a recursive portal descent are path-dependent intersections, so they cannot be recomputed per portal in isolation.
## D31. A render worker must not read live game state; snapshot it on the game thread at task submit (D578)
With the D481 render worker the game thread is already building frame N+1 (running `bgDetermineVisibleRooms`, resetting per-frame arrays, replacing matrices) while frame N is being drawn. Any port-side code that re-derives something from game globals at draw time (here: replaying the portal traversal) sees a half-updated mix: counts and arrays disagree mid-compare, matrices point at the next frame's allocation. Capture what the replay needs on the game thread at the one synchronous point per frame (`osSendMesg(sched_cmdQ, gfx task)` in `rspGfxTaskStart`; recognise it by `msg == g_gfxTaskSettingsList`), key it by the display-list pointer, and read only the snapshot (plus truly static level data) on the render side. A probe that compares live game output with a replay will report "extra/missing" noise that is really this race.
## D32. A replay of game code at an interpolated camera needs the camera in the game's own matrix convention (D578)
GE folds the camera lookat into the room projection (`field_10E0 = lookat * persp`) while `field_10CC` is the world-to-view matrix bg.c culls with, in a different (unscaled, un-offset) position space. To get the second at a blended camera from the first: `L = P * persp^-1` (row vectors; `matrix_4x4_multiply(a,b,r)` means `r = b*a`), take rotation and camera position from `L`, convert the position delta back with the world scale (`D_800364CC`), rebuild `field_10CC`. Verify the derivation on exact frames first (derived rotation == `field_10CC` rotation, `(cam - model_pos) * scale == derived position`): both held to 1e-5 / 3e-3, which also proves the multiply-order and layout assumptions before any blended frame is trusted. Accept a projection pair as "the room projection" only when that rotation check passes, and cache the replay per pass: pure-perspective and HUD loads between room draws must not flip a room's scissor between two code paths.
## D33. "Enable depth clamp" is not a fix until you find every far-plane reject, and read the matrix layout before editing a column (D578)
A GL-side `GL_DEPTH_CLAMP` is moot when the software RSP already drops whole triangles on the CPU (`z > w` CLIP_FAR trivial reject): audit CPU rejects before blaming or relying on the GL clip stage, and check whether the clamp is already globally on. Separately, when patching a perspective matrix that GE pre-multiplies by a lookat (`lookat * persp`, row vectors), the z column is `A*L[i][2]` for the basis rows and `A*T.z + B` for the translation row (T is nonzero: the lookat is built from camera-minus-model-origin). Reading row 3 as `k*L[2]` is a column-vector habit; the result is a heading-dependent tilt, not a clean plane shift.
## D34. Diagnose "geometry vanishes" by counting outcomes and capturing the frame, not by switching features off one at a time (D578)
Eight A/B switches on a visual dropout (far clip, depth, cull, scissor, projection lerp ...) all failed to move it; two cheap instruments did what they could not. Per-room triangle fate counters (seen / rejected by plane / culled / eye-crossing / emitted), compared between the suspect pass and the reference pass of the SAME frame, showed the CPU side was equal and eliminated a whole class at once. Two traps: (1) log both directions (or an aggregate), never only the losses, or selection bias manufactures a "systematic loss"; (2) an automatic capture trigger must be insensitive to legitimate change. An interpolated image can only contain what the previous and current exact frames contain, so "differs from BOTH neighbours" isolates real glitches, whereas "differs from the exact frame" fires on every camera turn and burns the capture cap.
## D35. A CPU-side clip must clip where the GPU would, and the GPU runs with depth clamp (D579)
`GL_DEPTH_CLAMP` is on globally (`gfx_opengl.cpp`), so GL never clips at the near plane: geometry between the eye and the near plane is drawn, as on the N64. The D543 CPU clip, added to stop attribute extrapolation across the camera plane, cut at `z = -w` and so deleted exactly that geometry, and a wall the player hugs vanished (#150, a v0.5.0 regression). The cut belongs at the camera plane (`w = eps`), with fog for clip-created vertices evaluated at `max(z/w, -1)` so the near-plane haze is unchanged. Before adding any CPU reject or clip, check which GL clip stages are actually live; see also D33.
## D36. Under VRR the swap is not a clock: pace presents from one continuous grid (D583)
With G-Sync/FreeSync a blocking swap returns when *we* present, so swap-return gaps measure our own cadence; any scheduler that derives its period or phase from them (or rebuilds its slot grid per game tick from the last swap) feeds back and wobbles, and a VRR OLED shows every frame-time change as a brightness change. Present from one continuous grid at the display period, snapped to exactly tick/N when within ~1% (no beat between the game clock and the display clock), re-anchored only on a real stall; never drop a queued present when a new game frame arrives (ring), base the ready time on the measured whole-frame draw (capped, so load frames are not learned), and avoid letting a queued present fall due during the next frame's draw. Measure cadence with a gap histogram, not presents/s alone.
## D37. Interpolation gates: never normalise a tiny measurement by (1 - alpha) near 1, and measure before any widening (D583)
A per-tick rate estimated as offset / (1 - alpha) amplifies anything that is not proportional to (1 - alpha): float rounding in an acos (~0.05 deg) and a fixed offset added by a later step (the far-clip widening read as 0.16 deg) became 1.5-3 deg/tick on passes with alpha just under 1, and every such frame was redrawn from scratch for a change nobody could see. Measure on the value before any post-processing, and only where the denominator is large or the measurement well above its noise. Count what each gate costs (redraws per 10 s), not just how often it fires.
## D38. Pair cameras by nearest, not one-to-one; and an exact fallback should draw once (D583)
GE loads the same camera projection once per room/object (~60 per pass). Pairing each load one-to-one with the previous frame's loads runs out of partners whenever the count changes (a set of rooms entering or leaving view) and pairs the leftover with another kind of projection: one object is drawn at the wrong camera or depth on an in-between present and appears to flicker in step with the room change. Pair a camera with the nearest previous camera even if already claimed, and give identical source matrices an identical result within a pass (memo). Separately: when a frame falls back to exact, draw the exact image once and copy it to the remaining present slots; redrawing n identical passes is pure cost.
## D39. Behind-camera triangles still need a facing test: use det[x y w], not "skip" (D584)
Dropping the cull for triangles with a vertex at w < 0 (because x/w is undefined there) draws both faces of two-faced translucent walls (Cradle catwalks) and reads as flicker at every frame rate. The homogeneous determinant det[x y w] is the correct facing test for any w: for all w > 0 its sign equals the NDC cross, and for mixed w it equals the classic XOR behind-eye flip, with no division. Also: when bisecting on a test box, use builds made from a known commit; a backup's file name is not its provenance (D584 lost one round to a mislabelled binary).
## D40. Deck draw cost: count before optimising, and never redraw past the budget (D583)
On the Deck (Game Mode: stderr is dropped, so instruments must go to ge007.log via sysLogPrintf), a heavy pass is ~6 us per batch, and batches track texture changes about 1:1. A counter at the top of `import_texture` counts lookups, not uploads; split hits, misses and time before concluding anything. "Skip the rebind when the same texture is re-issued" looked like a win on paper and changed nothing: consecutive batches really use different textures. For interpolation, any decision that redraws a whole pass (softened restart) must check the measured pass time against the refresh budget first: one slightly-off in-between image is invisible, a missed present is not.
## D41. A long frame time on one render thread is not draw cost until you line presents up against draws (D583)
Before optimising a "slow" interpolated frame, check what its time contains: with one worker that both draws and presents, a frame's measured draw includes nothing but work, yet the presents queued behind it go out late by up to the whole draw, and the symptom (uneven 8/14 ms gap pairs at full present rate) looks exactly like an over-budget pass. Profile on-CPU time (`perf` works on SteamOS, `perf_event_paranoid` 2 is enough for your own process) and rule out the GPU clock (QAM Manual GPU Clock) before touching batching; then log each draw's start/end next to each present's due/late time (`GE_INTERPLOG=<n>`). On the Deck in Game Mode, gamescope only shows windows Steam launched: a harness started over ssh runs hidden and throttled, so launch through a Steam shortcut (`tools_pc/deckrunner.sh`).
## D42.. Combiner alpha only reaches the framebuffer through the blender: no FORCE_BL, no blend (D414)
fast3d's `use_alpha` (blender inputs CLR_MEM/1MA) is a heuristic for "this is an alpha-blended draw". On the RDP the blender only acts on interior pixels when `FORCE_BL` is set (or via coverage/`CVG_X_ALPHA`); a render mode with XLU-style inputs but no `FORCE_BL` is opaque regardless of combiner alpha. Vanilla GE never noticed because its combiner alpha there is `SHADE` alpha (255). A mod's DL with cycle-2 alpha `(0,0,0,0)` drew fully transparent = black. GL blend is now gated on `FORCE_BL | CVG_X_ALPHA`.
**Tell:** rasterized with correct depth, immune to vertex-shade boosting, correct raw texels, black output. **Bisect fast:** output raw `texVal0/texVal1` in the fragment shader (bypasses combiner + blend); if that is right, the fault is combiner or blend state, not textures.
## D43. A capture taken through the feature under test is not ground truth (D628)
`GE_TEXDUMP` dumps what the GL upload receives, which is the *texture pack's*
image whenever a pack supplies that texnum - so dumps captured with
`Video.TexturePack` set cannot validate a pack join: the hold-out compares the
pack PNG with a dump of itself and scores a perfect 1.000. 52 of 346 dumps in
one such set were pack-substituted, and the false "pack and ROM textures are
the same size" premise read off them then gated the matcher down to nothing
(real packs are 4x-128x upscales). Two habits avoid the whole class: capture
ground truth with the feature OFF (and record in the dump dir which switches
were set), and before trusting a precision number, check that the hold-out's
dims/format still look like the *source* data rather than the replacement.
A 100 % score on a derived metric is a circularity smell, not a result.
**"Faithful" input mode = emit native bits and add nothing (D643).** A port that routes pad buttons to PC *actions* (dedicated use/reload calls, crouch, gadget cycle) cannot also claim to be the N64 pad: the game's own handlers (e.g. B = use-else-reload) only run on the native bit, so a button bound to two actions double-fires, and a fixed menu lockout reinterprets faces. For a strict mode, give the published bind table a native-button layer, zero every port-action mask in it (so side paths that read the action bitset, like a dedicated use/reload hook, go inert without being touched), and make the pure mapper `orig`-flagged and context-free; prove it with the same boot self-test (per-source table across stage/menu/tank). Also stop role-swapping sticks to match the game's selected style: a virtual pad keeps left = analog, right = C.
## D44. RDP IA4 is I3:A1; a decode tweak justified by "hardware layout" needs a texel dump (D644)
IA4 = 3-bit intensity + 1-bit alpha (nibble = I<<1 | A). An I2:A2 decode (D266) turned the A=0 nibbles 2/4/6 into semi-opaque black and drew the file-select crest on a dark square. When a texture's transparent texels render opaque, histogram the raw nibbles/bytes (`GE_TEXRAW` + `GE_D526` for the draw's fmt/oml/comb) before touching blender or alpha-compare code: the importer's alpha expansion is the cheapest suspect.
## D45. Interpolating GE: rooms carry the camera in the projection, props carry it in the modelview (D954)
GE loads two projections per view: `field_10E0` = perspective * lookat for rooms (modelviews are scale*translation), and the plain perspective `projmatrix` for chrs, doors, props, glass, explosions and the gun (camera baked into each modelview; LOD reads `-m[3][2]` as view depth). An interpolator that blends only the projection moves the rooms and leaves every prop at the exact camera, which reads as props flickering against the world at high frame rates only. Give the plain perspective the room camera blend (`P * R^-1 * Rb`, row vectors, `R = L * P`), and stop before anything camera-attached (the gun inherits the loaded projection; its `gSPPerspNormalize` marks the boundary). Check the matrix order at runtime (`R * P^-1` affine) instead of assuming it.
## D46. GE models are matrix-skinned; every view matrix is relative to the current room's origin (D955)
Chrs and objects load per-joint matrices from `model->render_pos` through segment 3 (`gSPSegment(3, render_pos)` in `subdraw`); the vertices are the model file's shared static arrays, so per-object identity lives in the matrix block, not in vertex buffers (interpolate matrices, keyed by (Model*, joint); never by draw ordinal, which shifts with LOD, weapons, hats and decals). Those matrices have the camera baked in, and every view matrix (rooms, props, the lookat in `field_10E0`) is relative to `current_model_pos`, the current ROOM's origin, which jumps at a room change: anything that compares matrices across ticks must rebase by the camera delta and treat a room change as a discontinuity.
+307
View File
@@ -0,0 +1,307 @@
---
title: User guide
description: Controls, settings, files, ROM check, updating, emulator saves, macOS setup and troubleshooting for the GoldenEye 007 PC port.
---
# User guide
Everything beyond the [Quick start](../README.md#quick-start): the full
controls, every setting and file, and fixes for common problems.
- [Controls](#controls)
- [Feature list](#feature-list)
- [Settings and files](#settings-and-files)
- [What's a ROM?](#whats-a-rom)
- [macOS (Apple Silicon, experimental)](#macos-apple-silicon-experimental)
- [Updating](#updating) · [Using an emulator save](#using-an-emulator-save) · [Uninstalling](#uninstalling)
- [Troubleshooting](#troubleshooting)
## 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 (toggles) | `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
*Controller* (pick a player first, then *Layout presets*). PlayStation and Nintendo controllers
show their own button names in the menus.
**Control style:** *Controller → Control style* (named after the watch's own
setting, with **Extended**, after Perfect Dark's "Ext", for the port's controls) picks how the
game's controller style (the watch's 1.1–1.4 selector) is treated. **Extended**
(the default) keeps the port's feel and fixes the style itself, so the watch's
selector has no effect. **Original** stops the port from touching the style:
pick 1.1, 1.2, 1.3 or 1.4 on the watch (or in the multiplayer setup) and it
behaves as on the N64, and the Controller page then offers the game's own
styles ("Controller style", 1.1 Honey to 2.4 Goodhead, saved in the Bond file
like the watch's) instead of the layout presets, which apply in Extended only. Buttons are passed
through as the N64's buttons, so 1.3 and 1.4 move fire and aim exactly as the
console does. The 2.x dual-controller styles run faithfully too: the game
reads their second stick from the next controller slot, which the port does not
map yet (a second pad assigned to that slot should provide it, untested), so with
one pad pick a 1.x style. Saves made with earlier builds may hold 1.2 (the port wrote its
forced style back into the save), so when you switch to the Original control style, pick
your style on the watch once.
In the Original control style the pad is a strict virtual N64 controller in
every context (stage, menus, watch, tank): each press sends the native N64
button and the game decides what it means (so *B* uses a door or reloads, as
on the console). Default layout, remappable per player in the ini
(`Input.N64Pad.<Button>`, `Input.N64Pad2..4` for players 2-4; up to two pad
buttons each) and with `Input.N64StickFeel` (1 = linear +/-80 stick with a
small dead centre, 0 = the Extended deadzone):
| Pad | N64 | Pad | N64 |
|---|---|---|---|
| South (A) | A | Right stick | C buttons |
| West (X) | B | Left stick | Analog stick |
| Right trigger | Z | D-pad | D-pad |
| Left trigger / RB | R | Start | Start |
| LB | L | Back | PC options overlay |
**Rebinding:** *Key Bindings* in the options overlay rebinds the
keyboard and mouse (one list: movement, then actions). Controller buttons are
rebindable per controller under *Controller* (pick a player first): set *Layout presets* 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. There is no warning yet when two
actions share a button, and the Xbox release's "left stick aims the
crosshair" and "D-pad copies the left stick" behaviors are not reproduced
(here the right stick aims and the D-pad strafes, as on the N64).
**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 rows and
left/right adjusts the selected value, **A** opens a page or toggles a row,
**B** goes back one page (and closes from the top), and **Start** (or Select
again) closes. With a keyboard, use `F10`, the arrow keys, Enter and
Backspace.
## Feature list
- **Split-screen multiplayer for up to 4 players**, 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 *Display mode* row (F10 → *Video*) switches
between *Modern* and *Original N64* (it reads *Custom* once you edit a setting it covers),
and *Aspect ratio → Original* shows the exact 4:3 frame.
- **A steady 60 fps**, with VSync and a frame cap (30 or 60), including on
the low-end GPUs I test on.
- **Frame rates above 60:** 90, 120, 144, 165 or 240 fps, or **Auto** to match
your display (up to 240). Extra frames are blended between game ticks (the game still runs at
its original rate), at the cost of one tick of latency. It has no effect on
a 60 Hz display, and the *Original N64* preset keeps 60. See
[framerate and pacing](framerate-and-pacing.md).
- **macOS (Apple Silicon), experimental:** a `macos-arm64` download, not
notarized (one Terminal command to unblock it, see [macOS](#macos-apple-silicon-experimental)),
or build from source. I'd like test reports.
- **Graphics options:** resolution, borderless or exclusive fullscreen,
a Center window action while windowed, 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 centered 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 (also scales the in-game options
menu), an optional always-on crosshair, a custom RGB crosshair color
with adjustable opacity and a health-based color, 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; the game's crosshair is the mouse pointer, and
each page has a *Reset to defaults* row.
- **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), and *Check for updates* (off
by default; one request to GitHub per launch when on, see [Security](security.md)).
- **Saves in a plain file** next to the game, so backing up is a file copy;
1964 and Project64 saves load directly ([details](#using-an-emulator-save)).
## Settings and files
**Options overlay:** press **F10** (pad: **Select**) in game or in the menus;
on the file-select screen the *PC Options* entry opens it too.
Changes are saved automatically. Most settings live in `ge007.ini`; the
ones that come from the game's own watch menu (auto-aim, aim
control, sight, look ahead, ammo on screen, screen size, screen ratio, music
and FX volume) are part of your save, as on the N64. Screen size (Full / Wide /
Cinema) and screen ratio (Normal / 16:9) combine with the PC **Aspect ratio**
setting: Aspect ratio owns the window shape, while the watch's two options
only change what the game renders inside it.
**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-<region>/`, `data/pccg-<region>/` | 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. |
**Removing ROM-derived content:** the only files on your machine that come
from the game are the ROM you put there and the two generated asset folders
(`data/pcmodels-<region>/`, `data/pccg-<region>/`). Delete those and every
ROM-derived file is gone; your save (`ge007.eep`) and settings (`ge007.ini`)
are separate files, yours to keep or delete.
**`ge007.ini`** uses `[Section]` headers and `Key = value` lines. A few
common keys and their defaults:
```ini
[Video]
Fullscreen = 0
VSync = 1
FpsCap = -1
MSAA = 2
TextureFilter = 1
NativeWidescreen = 1
FovScale = 100
DrawDistance = 200
[Audio]
MasterVolume = 100
```
`Fullscreen` 1 is borderless fullscreen; `FpsCap` -1 is Auto (match the display,
up to 240), 0 is uncapped, otherwise a fps cap (30, 60, or 90 to 240 for frame
interpolation on a faster display; an existing ini keeps its value); `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`; on macOS, `shasum ge007.ntsc-final.z64`.
## macOS (Apple Silicon, experimental)
**Download:** `goldeneye-pc-port-<version>-macos-arm64.tar.gz` from the release
page, for Apple Silicon (M1 or later), macOS 14 or newer. It bundles SDL2 and the
C++ runtime, so Homebrew is not needed. The app is experimental and **not
signed with an Apple Developer ID or notarized**, so Gatekeeper blocks it when
it comes from a download. Unpack it, then run `xattr -dr com.apple.quarantine <folder>` in Terminal once, before the first launch:
```sh
xattr -dr com.apple.quarantine goldeneye-pc-port-<version>-macos-arm64
```
**Or skip the quarantine step** by downloading in Terminal instead of a browser
(`curl` does not mark the file, so Gatekeeper never checks it):
```sh
curl -L https://github.com/jkdansereau/goldeneye-pc-port/releases/download/v<version>/goldeneye-pc-port-<version>-macos-arm64.tar.gz | tar xz
```
**Without Terminal:** after macOS blocks the first launch, open System Settings →
Privacy & Security, scroll down to the message about `ge007.aarch64` and click
**Open Anyway** (macOS 15 removed the old right-click → Open bypass).
Then set up `data/` with your ROM as in the package README and run
`./ge007.aarch64` from that folder in Terminal. If you launch it first, the process
stalls before the window appears (it never reached `main` in my test; that is
most likely Gatekeeper, and clearing the flag afterwards did not unstick that
copy), so re-extract the archive and clear the flag before launching. I have run
the package on one M3 Mac only, so test reports (what Mac, what macOS, what
happened) are very welcome.
## 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.
## Using an emulator save
Saves from N64 emulators (1964, Project64, …) load directly: with the game
closed, just copy your `.eep` to `data/ge007.eep`. On first launch the game
detects the emulator byte order and converts it in place, keeping a backup of
the original file next to it as `ge007.eep.emulator.bak` (never overwriting an
existing one). Any slot that is corrupt in both formats is left alone and gets
reset by the game, exactly as before. An existing port save is untouched.
To take a save back into an emulator, use the small converter that ships in
the release folder as `tools/eep_convert.py`
(`tools_pc/eep_convert.py` in a source checkout; plain Python 3, no
dependencies):
```sh
python tools/eep_convert.py pc-to-n64 data/ge007.eep back-to-emulator.eep # port -> emulator
python tools/eep_convert.py verify some-file.eep # which format is this?
```
`verify` checks every save checksum and, without `--as n64|pc`, tells you
which convention the file uses.
## 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 → *Video → 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.**
+64
View File
@@ -0,0 +1,64 @@
# pi/ — agent-development tooling (local, not part of the game build)
This directory is a [pi package](https://pi.dev/packages) for the
LLM-agent workflow of this repo — it is **not** part of the game build
(never add it to `CMakeLists.txt`). It exists because the 2026-10-09
session forensics on the #150 cull hunt (see `docs/dev-process.md` §8
and the `Tooling workstream` row in `docs/ROADMAP.md` §7) showed the
loss mode is model + discipline, not retrieval.
**Install (per machine):**
```sh
pi install ./pi # local package: agents + compaction-ledger
pi install npm:pi-subagents@0.76.1 # the subagent runtime (pinned; audit: docs/dev/notes/)
```
`.pi/` is gitignored, so the install declarations land in machine-local
settings; the reviewable source of truth is this directory, committed.
**What it provides**
- `agents/` — four model-pinned subagent roles (via `pi-subagents`), all on the
standing **ninfer** backend (only one of ninfer/strata runs at a time;
children inherit the running backend, so pins must match it):
- `build-runner` — `./build-pc.sh` + capture + golden sweep, reports
pass/fail. `ninfer-windows/qwen3.8-27b-nvfp4full` (fast 4-bit).
- `pairing` — D579T-style log pairing (E-rows vs C-rows by x/y/w),
names candidate cull rules, read-only. `model: inherit` — the
reasoning step, so it follows the parent session's tier; run the
parent on a strong model when doing D579 diagnosis.
- `sweep-watcher` — watches long golden-sweep runs, triages new vs
regressed frames. `ninfer-windows/qwen3.8-27b-nvfp4full`.
- `doc-sync` — ROADMAP rows, findings-index.csv, findings.md §F index
after a verified change. `ninfer-windows/qwen3.8-27b-nvfp4full`.
Note: NInfer serves one model per launcher — `ninfer-coding.bat`
(4-bit `qwen3.8-27b-nvfp4full`) is the standing default; the
`ninfer-quality.bat` launcher serves the ≈6-bit groupwise-int
`qwen3.8-27b` as the strong local tier (a launcher restart + new pi
session, not a backend switch). If a better/faster artifact appears in
the closed NInfer set, promote the labor-role pins to it.
Model pins live in each agent file's frontmatter (`model:
provider/model`) — keep them here, not in untracked settings, so the
pins are reviewable, and keep them on the standing backend (ninfer).
Planning/diagnosis stays on the parent session's strong model
(`openrouter/deepseek/deepseek-v4-pro`, or `strata-coder-hard` with the
local backend switched); only mechanical labor goes to these agents.
- `extensions/compaction-ledger/` — overrides the compaction summary
with an evidence-ledger prompt (every live hypothesis must be tagged
`refuted` / `unvalidated` / `validated (<artifact>)`; "diagnosed"
without a maintainer A/B or golden sweep is forbidden) and, when
`GE_COMPACT_MODEL` is set (e.g. `openrouter/deepseek/deepseek-v4-pro` or
`strata/strata-coder-hard`), summarizes with that
stronger model instead of the session model. The 01a11ec6 session
lost the evening because a mid-session compaction promoted an
unvalidated hypothesis to "diagnosed" and the post-compaction turns
built on it.
Rules for anything added here: machine paths are forbidden (tree
hygiene), third-party deps are forbidden without a ROADMAP §7 approval
row (like `pi-subagents`), and `src/game` is still read-only ground
truth for every agent.
+27
View File
@@ -0,0 +1,27 @@
---
name: build-runner
description: Runs ./build-pc.sh, verifies the exe, runs capture + golden sweep, reports pass/fail with log tails. Never edits code.
tools: bash, read
model: ninfer-windows/qwen3.8-27b-nvfp4full
---
You are the build-runner for the GoldenEye 007 PC port. Your single job:
build, run, capture, sweep, report. You do NOT edit or write any source
file, and you do NOT diagnose — you report facts.
Procedure (the task gives you the region and the level):
1. `./build-pc.sh <region>` (ntsc-final | pal-final | jpn-final). If the
build fails, capture the last 60 lines of the error and STOP.
2. Run the capture command the task gives you (or the standard
`tools_pc/sweep` invocation) and let it finish.
3. Run the golden sweep / framediff pass the task names; collect the
per-frame verdicts (pass / new-diff / regressed).
4. Report: region, exe timestamp, pass/fail counts, the list of
regressed frames with their diff sizes, and any crash log tail.
End with one line: `VERDICT: PASS` or `VERDICT: FAIL (<count> regressed)`.
House rules: no commits, no pushes, no edits outside capture output
dirs. Machine paths in your report must use the placeholder legend from
`docs/dev/HANDOFF-ARCHIVE.md` (`<repo>`, `<games>`, `<python>`, `<temp>`).
If the game process is locking the exe (link "Permission denied"),
report it — do not kill user processes.
+31
View File
@@ -0,0 +1,31 @@
---
name: doc-sync
description: After a verified change, syncs docs/ROADMAP.md rows, docs/dev/findings-index.csv, and the findings.md §F index. Never invents findings.
tools: read, write, edit, bash
model: ninfer-windows/qwen3.8-27b-nvfp4full
---
You are the doc-sync agent for the GoldenEye 007 PC port. You keep the
documentation layer in sync AFTER a change is verified (golden sweep +
maintainer A/B where applicable). You never invent findings or change
status of work you have not been told about.
Scope per task (the task names the change and its evidence):
1. `docs/dev/findings.md` — if a new Dxx finding was recorded, check
the §F/§H index entry exists; regenerate `docs/dev/findings-index.csv`
with `python tools_pc/gen_findings_index.py` (verify the CSV matches
the labels in findings.md).
2. `docs/ROADMAP.md` — update the row(s) the task names: status
vocabulary only (`open` / `partial` / `verify` / `parked` /
`decision`), refs as Dxx/#issue links, one row per item; delete rows
that are closed (their content moves to findings.md if not already
there). Never create a new backlog file.
3. If the task says so: the README "Status" section (current release
state only) and `docs/dev/LEVEL-STATUS.md` if a level changed.
House rules: batch all doc edits into the commit the task names (one
commit, not one per item); no local machine paths (placeholder legend
in `docs/dev/HANDOFF-ARCHIVE.md`); no contributor emails in prose; pure
site pages under `docs/` (index.md, fidelity.md, etc.) are a
presentation layer — do NOT edit them to match working docs and do not
treat their staleness as a bug.
+29
View File
@@ -0,0 +1,29 @@
---
name: pairing
description: Pairs D579T-style probe logs (E-rows vs C-rows by x/y/w) and names candidate cull rules. Read-only analysis.
tools: bash, read
model: inherit
---
You are the pairing analyst for the GoldenEye 007 PC port's #150 cull
investigation (D579). You pair two probe logs and name candidate cull
rules. You are READ-ONLY: no edits, no builds, no game runs — bash is
for `awk`/`sort`/`join`-style offline processing of log files only.
Method (the task names the two log files, e.g. `GE_X_NOCULL=1` vs
default with `GE_D579T=1`):
1. Parse the E-rows (wall-visible, should-be-drawn) and C-rows
(culled) with their x/y/w fields.
2. Match culled C-objects to wall-visible E-objects by (x, y, w) with a
small tolerance; report the match rate and the unmatched sets.
3. From the geometry of the matched/mismatched sets, state the
candidate cull rule that would explain them (e.g. "C set = E set
minus objects with w < 24 and y < horizon").
4. Output: a table of the paired rows, the match statistics, and the
candidate rule(s) ranked by how many rows they explain. Do not
declare a rule validated — validation is the golden sweep + a
maintainer A/B.
House rules: `src/game` is ground truth and read-only; cite the exact
log lines (line numbers) behind every claim; keep machine paths to the
placeholder convention.
+28
View File
@@ -0,0 +1,28 @@
---
name: sweep-watcher
description: Watches a long golden-sweep run in the background, triages new vs regressed frames against golden baselines, writes a triage table.
tools: bash, read, write
model: ninfer-windows/qwen3.8-27b-nvfp4full
---
You are the sweep-watcher for the GoldenEye 007 PC port. You monitor
long-running golden-sweep / capture jobs and triage their results. You
may write ONE triage file (the task names it); you do not edit source,
run builds, or start game sessions yourself.
Procedure:
1. Poll the sweep's output dir / log the task names until it finishes
(bounded: report and stop if it runs past the task's deadline).
2. Compare each frame against its golden baseline
(`tools_pc/golden/<level>/{win,linux,deck}/`); classify every
non-identical frame as NEW-DIFF (was passing) or REGRESSED (worse
than the previous sweep's triage file, if given).
3. Write the triage table: frame, level, platform, status, diff size,
one-line guess only if obvious from the diff stats (never
"diagnosed" — diagnosis needs a parent session with context).
4. Report: counts by status, the worst 5 frames, and the triage file
path. End with `VERDICT: CLEAN` / `VERDICT: N REGRESSIONS`.
House rules: capture output dirs are gitignored — write the triage file
where the task says (typically `docs/dev/notes/`), use placeholder
paths, and never commit or push anything.
+241
View File
@@ -0,0 +1,241 @@
/**
* Evidence-ledger compaction for the GoldenEye 007 PC port (pi/ package).
* Targets the installed pi 1.0.2 extension API (SessionBeforeCompactResult);
* the first-party examples/ in the npm package target a newer API — do not
* copy from them without re-checking dist/core/extensions/types.d.ts.
*
* What it does:
*
* 1. Overrides the compaction summary with an evidence-ledger prompt: every
* live hypothesis about the bug being worked gets one line tagged
* refuted / unvalidated / validated(<artifact>). "Diagnosed" is
* forbidden without a maintainer A/B or a passing golden sweep.
* Motivated by pi session 01a11ec6 (the #150 cull hunt): the 04:52Z
* compaction promoted an unvalidated hypothesis to "diagnosed" and the
* post-compaction turns built the next three fix candidates on that
* false premise.
*
* 2. When GE_COMPACT_MODEL is set (fully-qualified "provider/model", e.g.
* "strata/strata-coder-hard" or "openrouter/deepseek/deepseek-v4-pro"),
* the summarization runs on that stronger model instead of the session
* model. Unset -> the session model (labor on a fast local model,
* compaction on a strong one).
*
* Failure mode is safe by design: if the custom summarization throws (model
* down, no API key, shape drift after a pi upgrade), the handler returns
* nothing and pi's built-in compaction proceeds unchanged.
*
* GE_COMPACT_LEDGER=0 disables the extension's override entirely.
*/
import { completeSimple } from "@earendil-works/pi-ai/compat";
import type { Model } from "@earendil-works/pi-ai";
import type {
ExtensionAPI,
ExtensionContext,
SessionBeforeCompactEvent,
SessionBeforeCompactResult,
} from "@earendil-works/pi-coding-agent";
const EVIDENCE_LEDGER_PROMPT = [
"The summary MUST start with a section titled `Evidence ledger`. For every",
"live hypothesis or open claim about the bug/work item being pursued, write",
"exactly one line in this form:",
" - <claim> -- refuted | unvalidated | validated (<artifact>)",
"where <artifact> is a concrete check: a named log file, a golden-sweep",
"frame, a maintainer A/B, or a commit. Rules:",
' - A fix that has not passed a maintainer A/B or a golden sweep is',
'"unvalidated", never "diagnosed" or "root-caused".',
" - Carry forward the previous summary's ledger lines that are still live;",
" do not drop a refutation just because it was summarized before.",
"After the ledger, include: the uncommitted-tree state (files, which work",
"item each belongs to), the next concrete step, and -- if the session's",
"brief had a BUDGET line -- the remaining budget.",
].join("\n");
const SUMMARY_SYSTEM = [
"You are writing the context-compaction summary for a coding-agent session.",
"Produce a compact, dense handoff summary that a fresh session can resume",
"work from. Preserve: the active task, refuted hypotheses (so they are not",
"retried), the uncommitted tree state, standing environment rules, and the",
"next concrete step. No filler, no restating this instruction.",
].join("\n");
// ---------------------------------------------------------------- transcript
type Msg = { role?: string; content?: unknown; toolName?: string; [k: string]: unknown };
function contentText(content: unknown): string {
if (typeof content === "string") return content;
if (Array.isArray(content)) {
return content
.map((p) => {
if (p && typeof p === "object") {
const o = p as Record<string, unknown>;
if (o.type === "text") return (o.text as string) ?? "";
if (o.type === "toolCall" || o.type === "tool_use")
return `[tool call: ${(o.name as string) ?? "?"}]`;
if (o.type === "image" || o.type === "image_url") return "[image]";
}
return "";
})
.join(" ");
}
return "";
}
function truncate(s: string, n: number): string {
return s.length <= n ? s : s.slice(0, n) + ` [${s.length - n} chars elided]`;
}
function digestMessages(messages: Msg[], capChars: number): string {
const lines: string[] = [];
let budget = capChars;
for (const m of messages) {
const role = m?.role ?? "unknown";
let text = contentText(m?.content).replace(/\s+/g, " ").trim();
if (role === "toolResult" || m?.toolName) text = truncate(text, 400);
const line = `[${role}] ${text}`.trim();
if (budget - line.length - 1 < 0 && lines.length > 0) {
lines.unshift(`[... ${messages.length - lines.length} earlier messages elided ...]`);
break;
}
budget -= line.length + 1;
lines.push(line);
}
return lines.join("\n");
}
function buildLedgerPrompt(
prep: SessionBeforeCompactEvent["preparation"],
extraInstructions: string | undefined,
): string {
const sections: string[] = [];
if (prep.previousSummary) {
sections.push(
"## Previous compaction summary (update it; carry forward still-live ledger lines)\n" +
prep.previousSummary,
);
}
sections.push(
`## Transcript to summarize (${prep.messagesToSummarize.length} messages)\n` +
digestMessages(prep.messagesToSummarize as Msg[], 24000),
);
if (prep.turnPrefixMessages?.length) {
sections.push(
`## Split-turn prefix (${prep.turnPrefixMessages.length} messages)\n` +
digestMessages(prep.turnPrefixMessages as Msg[], 8000),
);
}
if (extraInstructions) {
sections.push(`## Extra custom instructions\n${extraInstructions}`);
}
sections.push(EVIDENCE_LEDGER_PROMPT);
return sections.join("\n\n");
}
// ------------------------------------------------------------------- models
function pickSummarizer(ctx: ExtensionContext): Model<any> | undefined {
const spec = process.env.GE_COMPACT_MODEL?.trim();
if (spec) {
const i = spec.indexOf("/");
if (i > 0 && i < spec.length - 1) {
const m = ctx.modelRegistry.find(spec.slice(0, i), spec.slice(i + 1));
if (m) return m;
try {
ctx.ui.notify?.(`compaction-ledger: GE_COMPACT_MODEL "${spec}" not found; using session model`, "warning");
} catch {
/* headless */
}
}
}
return ctx.model;
}
function extractSummaryText(resp: unknown): string {
const r = resp as { content?: unknown };
if (Array.isArray(r?.content)) {
return (r.content as Array<Record<string, unknown>>)
.filter((p) => p?.type === "text")
.map((p) => (p.text as string) ?? "")
.join("");
}
return typeof r?.content === "string" ? (r.content as string) : "";
}
// -------------------------------------------------------------------- entry
export default function (pi: ExtensionAPI): void {
pi.registerCommand("compact-ledger", {
description: "Compact now with the evidence-ledger summary prompt (usage: /compact-ledger [extra instruction])",
handler: async (args: string, ctx: ExtensionContext) => {
const instructions = [
EVIDENCE_LEDGER_PROMPT,
args ? `Additional user instruction: ${args}` : "",
]
.filter(Boolean)
.join("\n");
ctx.compact({
customInstructions: instructions,
onError: (err) => {
try {
ctx.ui.notify?.(`compaction failed: ${err.message}`, "error");
} catch {
/* headless */
}
},
});
try {
ctx.ui.notify?.("Compaction started (evidence ledger)", "info");
} catch {
/* headless */
}
},
});
pi.on("session_before_compact", async (event, ctx): Promise<SessionBeforeCompactResult | undefined> => {
if (process.env.GE_COMPACT_LEDGER === "0") return; // opt-out: built-in compaction
const prep = event.preparation;
const model = pickSummarizer(ctx);
if (!model) return; // no model resolvable -> built-in compaction
try {
const prompt = buildLedgerPrompt(prep, event.customInstructions);
const maxTokens = Math.min(8192, Math.max(2048, Math.floor(((model as { maxTokens?: number }).maxTokens ?? 4096) / 2)));
const resp = await completeSimple(
model as never,
{
systemPrompt: SUMMARY_SYSTEM,
messages: [{ role: "user", content: prompt }],
},
{ signal: event.signal, maxTokens, temperature: 0.2 },
);
const summary = extractSummaryText(resp);
if (!summary.trim()) throw new Error("summarizer returned an empty summary");
try {
ctx.ui.notify?.(
`Compaction complete (evidence ledger): ${prep.tokensBefore} -> ~${Math.ceil(summary.length / 4)} tokens`,
"info",
);
} catch {
/* headless */
}
return {
compaction: {
summary,
firstKeptEntryId: prep.firstKeptEntryId,
tokensBefore: prep.tokensBefore,
},
};
} catch (err) {
try {
ctx.ui.notify?.(
`compaction-ledger: custom summarization failed (${(err as Error).message}); using built-in compaction`,
"warning",
);
} catch {
/* headless */
}
return; // undefined -> pi's built-in compaction takes over
}
});
}
+31
View File
@@ -0,0 +1,31 @@
{
"name": "ge007-pi-tooling",
"version": "0.1.0",
"description": "GoldenEye 007 PC port - agent-development tooling: model-pinned subagent roles and evidence-ledger compaction (see pi/README.md)",
"license": "MIT",
"keywords": [
"pi-package"
],
"peerDependencies": {
"@earendil-works/pi-coding-agent": "*",
"@earendil-works/pi-ai": ">=0.86.1"
},
"peerDependenciesMeta": {
"@earendil-works/pi-coding-agent": {
"optional": true
},
"@earendil-works/pi-ai": {
"optional": true
}
},
"pi": {
"extensions": [
"./extensions/compaction-ledger/index.ts"
],
"subagents": {
"agents": [
"./agents"
]
}
}
}
+7
View File
@@ -44,6 +44,13 @@ void gfx_start_frame(void);
void gfx_run(Gfx* commands);
void gfx_end_frame(void);
void gfx_set_target_fps(int);
/* D578: frame interpolation (gfx_pc.cpp), driven by the render worker. */
void gfx_interp_reset(void);
int gfx_interp_tick(Gfx* commands, const float* alphas, int n, int base);
int gfx_interp_last_exact(void);
int gfx_interp_body_runs(void);
void gfx_interp_trigger_counts(unsigned* turn, unsigned* clamp, unsigned* set, unsigned* room);
void gfx_interp_present(int slot, int vsync);
void gfx_set_texture_filter(enum FilteringMode mode);
void gfx_set_mipmap_filter(enum MipmapFilteringMode mode);
void gfx_set_fix_mip_textures(int on);
+117 -3
View File
@@ -20,6 +20,11 @@
#include "gfx_rendering_api.h"
#include "gfx_pc.h"
/* D578 L3: the glad (GL 1.0 base) header lacks this token. */
#ifndef GL_CLEAR_COLOR
#define GL_CLEAR_COLOR 0x0C22
#endif
using namespace std;
struct ShaderProgram {
@@ -757,12 +762,14 @@ static void gfx_opengl_upload_texture(const uint8_t* rgba32_buf, uint32_t width,
* globalDL_0x078..0x9a8) -- that's the one shape we actually need. */
const bool is_fire_shape = (width == 16 && height == 14);
if ((is_fire_shape || td < 400) && width && height && width < 4096 && height < 4096) {
char nm[128];
char base[128], nm[160];
if (is_fire_shape) {
snprintf(nm, sizeof nm, "texdump/fire%03d_%ux%u_mip%d.ppm", td_fire++, width, height, (int)gen_mipmaps);
snprintf(base, sizeof base, "texdump/fire%03d_%ux%u_mip%d", td_fire++, width, height, (int)gen_mipmaps);
} else {
snprintf(nm, sizeof nm, "texdump/t%03d_%ux%u_mip%d.ppm", td++, width, height, (int)gen_mipmaps);
snprintf(base, sizeof base, "texdump/t%03d_%ux%u_mip%d", td++, width, height, (int)gen_mipmaps);
}
/* D592: PPM = the RGB triage view (alpha dropped). */
snprintf(nm, sizeof nm, "%s.ppm", base);
FILE* f = fopen(nm, "wb");
if (f) {
fprintf(f, "P6\n%u %u\n255\n", width, height);
@@ -770,6 +777,15 @@ static void gfx_opengl_upload_texture(const uint8_t* rgba32_buf, uint32_t width,
fwrite(rgba32_buf + 4 * p, 1, 3, f);
fclose(f);
}
/* D592 (Q1): the EXACT 4-byte RGBA32 glTexImage2D input (with
* alpha), so the Mac/Win upload comparison is byte-exact (the PPM
* alone cannot reveal an alpha / channel-order artifact). */
snprintf(nm, sizeof nm, "%s.rgba32", base);
FILE* fb = fopen(nm, "wb");
if (fb) {
fwrite(rgba32_buf, 1, (size_t)width * height * 4, fb);
fclose(fb);
}
}
}
#endif
@@ -1182,6 +1198,12 @@ static int s_gpuq_idx = 0, s_gpuq_valid[2] = { 0, 0 };
static double s_gpuq_ms = 0.0;
static int s_gpuq_n = 0, s_gpuq_frames = 0;
/* D578: an extra interpolation pass of the same game frame must reuse its
* noise seed (frame_count advances once per game frame, as at 60 fps). */
extern "C" void gfx_opengl_frame_count_rewind(void) {
frame_count--;
}
static void gfx_opengl_start_frame(void) {
frame_count++;
if (s_gpuq_on < 0) {
@@ -1264,6 +1286,74 @@ extern "C" bool gfx_opengl_dump_bound_fbo(uint32_t width, uint32_t height, const
static void gfx_opengl_finish_render(void) {
}
/* D578: present queue for frame interpolation (gfx_pc.cpp). Each slot is a
* plain RGBA8 colour FBO at window size; the default framebuffer is single-
* sampled (MSAA lives in the game framebuffer), so a nearest blit each way is
* an exact copy. Bindings are put back the way copy_framebuffer leaves them. */
static GLuint s_islot_fbo[GFX_INTERP_SLOTS], s_islot_tex[GFX_INTERP_SLOTS];
static uint32_t s_islot_w, s_islot_h;
static uint32_t s_islot_tw[GFX_INTERP_SLOTS], s_islot_th[GFX_INTERP_SLOTS]; /* D583: per-slot storage size */
static void gfx_opengl_interp_restore_binding(void) {
glBindFramebuffer(GL_FRAMEBUFFER, current_framebuffer == 0 ? 0 : framebuffers[current_framebuffer].fbo);
glReadBuffer(GL_BACK);
glEnable(GL_SCISSOR_TEST);
}
extern "C" void gfx_opengl_interp_store(int slot, uint32_t width, uint32_t height) {
if (slot < 0 || slot >= GFX_INTERP_SLOTS || width == 0 || height == 0) {
return;
}
if (s_islot_fbo[0] == 0) {
glGenFramebuffers(GFX_INTERP_SLOTS, s_islot_fbo);
glGenTextures(GFX_INTERP_SLOTS, s_islot_tex);
}
if (width != s_islot_tw[slot] || height != s_islot_th[slot]) {
/* D583: each slot is sized on its first store at a new window size, so
* slots a mode never uses (5-8 below 240 Hz) never take VRAM. */
GLint prevTex = 0;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &prevTex); /* fast3d caches its bindings */
glBindTexture(GL_TEXTURE_2D, s_islot_tex[slot]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, (GLsizei)width, (GLsizei)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, s_islot_fbo[slot]);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, s_islot_tex[slot], 0);
glBindTexture(GL_TEXTURE_2D, (GLuint)prevTex);
s_islot_tw[slot] = width;
s_islot_th[slot] = height;
}
s_islot_w = width;
s_islot_h = height;
glDisable(GL_SCISSOR_TEST);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
glReadBuffer(GL_BACK);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, s_islot_fbo[slot]);
glBlitFramebuffer(0, 0, (GLint)width, (GLint)height, 0, 0, (GLint)width, (GLint)height,
GL_COLOR_BUFFER_BIT, GL_NEAREST);
gfx_opengl_interp_restore_binding();
}
/* D578: block until the GL queue (and so a VSync'd swap) completes, so the
* render worker's present clock learns the display's real refresh timing. */
extern "C" void gfx_opengl_interp_sync(void) {
glFinish();
}
extern "C" void gfx_opengl_interp_show(int slot) {
if (slot < 0 || slot >= GFX_INTERP_SLOTS || s_islot_fbo[slot] == 0 || s_islot_tw[slot] == 0) {
return;
}
glDisable(GL_SCISSOR_TEST);
glBindFramebuffer(GL_READ_FRAMEBUFFER, s_islot_fbo[slot]);
glReadBuffer(GL_COLOR_ATTACHMENT0);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
/* The slot's own size: one stored before a window resize can still be pending (D583 ring). */
glBlitFramebuffer(0, 0, (GLint)s_islot_tw[slot], (GLint)s_islot_th[slot], 0, 0, (GLint)s_islot_tw[slot], (GLint)s_islot_th[slot],
GL_COLOR_BUFFER_BIT, GL_NEAREST);
gfx_opengl_interp_restore_binding();
}
static int gfx_opengl_create_framebuffer() {
size_t i = framebuffers.size();
framebuffers.resize(i + 1);
@@ -1405,6 +1495,29 @@ void gfx_opengl_clear_framebuffer(bool clear_color, bool clear_depth) {
glEnable(GL_SCISSOR_TEST);
}
/* D579: scissored clear of one region of the bound target. Same
* bottom-up pixel coordinates as gfx_opengl_set_scissor. Mirrors the
* depth-mask save/restore of gfx_opengl_clear_framebuffer. */
void gfx_opengl_clear_region(bool clear_color, bool clear_depth, int x, int y, int w, int h) {
if (w <= 0 || h <= 0) return;
GLbitfield mask = 0;
if (clear_color) {
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
mask |= GL_COLOR_BUFFER_BIT;
}
if (clear_depth) {
glDepthMask(GL_TRUE);
mask |= GL_DEPTH_BUFFER_BIT;
}
glScissor(x, y, w, h);
glEnable(GL_SCISSOR_TEST);
glClear(mask);
if (clear_depth) {
glDepthMask(current_depth_mask ? GL_TRUE : GL_FALSE);
}
}
void gfx_opengl_resolve_msaa_color_buffer(int fb_id_target, int fb_id_source) {
if (!gfx_framebuffers_enabled) {
return;
@@ -1541,6 +1654,7 @@ struct GfxRenderingAPI gfx_opengl_api = {
gfx_opengl_start_draw_to_framebuffer,
gfx_opengl_copy_framebuffer,
gfx_opengl_clear_framebuffer,
gfx_opengl_clear_region,
gfx_opengl_resolve_msaa_color_buffer,
gfx_opengl_get_framebuffer_texture_id,
gfx_opengl_select_texture_fb,
+6
View File
@@ -14,6 +14,12 @@ const char *gfx_opengl_renderer_string(void);
/* Frame capture (dev tool, env GE_PCDUMP=1) — see gfx_opengl.cpp. */
bool gfx_opengl_pcdump_enabled(void);
bool gfx_opengl_dump_bound_fbo(uint32_t width, uint32_t height, const char* path);
/* D578: frame-interpolation present queue. store copies the finished back
* buffer (width x height) into image `slot`; show copies it back to the back
* buffer just before a swap. Slots are resized on demand. */
void gfx_opengl_interp_store(int slot, uint32_t width, uint32_t height);
void gfx_opengl_interp_show(int slot);
#ifdef __cplusplus
}
#endif
+2292 -114
View File
File diff suppressed because it is too large. Load diff
+8
View File
@@ -16,6 +16,11 @@ struct GfxClipParameters {
enum FilteringMode { FILTER_NONE, FILTER_LINEAR, FILTER_THREE_POINT, FILTER_TRILINEAR };
enum MipmapFilteringMode { MIPMAP_DISABLED, MIPMAP_NEAREST, MIPMAP_LINEAR };
/* D578: frame-interpolation present slots (gfx_opengl.cpp / gfx_pc.cpp), a
* ring: a game frame's passes go after the slots still waiting to be shown
* (D583). 8 = two game frames of 4 presents (240 Hz); allocated on first use. */
#define GFX_INTERP_SLOTS 8
struct GfxRenderingAPI {
const char* (*get_name)(void);
int (*get_max_texture_size)(void);
@@ -48,6 +53,9 @@ struct GfxRenderingAPI {
bool (*start_draw_to_framebuffer)(int fb_id, float noise_scale);
void (*copy_framebuffer)(int fb_dst, int fb_src, int left, int top, bool flip_y, bool use_back);
void (*clear_framebuffer)(bool clear_color, bool clear_depth);
/* D579: scissored clear of one axis-aligned region of the BOUND target
* (x, y in GL bottom-up target pixels, as for set_scissor). */
void (*clear_region)(bool clear_color, bool clear_depth, int x, int y, int w, int h);
void (*resolve_msaa_color_buffer)(int fb_id_target, int fb_id_source);
void* (*get_framebuffer_texture_id)(int fb_id);
void (*select_texture_fb)(int fb_id);
+7
View File
@@ -18,11 +18,18 @@ void gfx_sdl_make_context_current(void);
void gfx_sdl_release_context(void);
/* D344: finish queued GL work and unbind the context (render thread, quit). */
void gfx_sdl_park_for_exit(void);
/* D632: delete the GL context + window (host thread, exit, render parked). */
void gfx_sdl_teardown_for_exit(void);
/* Refresh the cached drawable size after a window resize (called by the
* host thread's event pump). */
void gfx_sdl_update_cached_size(void);
/* D578: display refresh rate (Hz) of the window; poll on the host thread,
* read anywhere. 0 = unknown. */
void gfx_sdl_poll_refresh_rate(void);
int gfx_sdl_get_refresh_rate(void);
#ifdef __cplusplus
}
#endif
+143 -15
View File
@@ -9,6 +9,9 @@
#include "gfx_screen_config.h"
#include "input.h"
#include "ge007_icon.h"
#if defined(__APPLE__)
#include <OpenGL/OpenGL.h> /* D952: CGL swap interval */
#endif
extern "C" void videoRequestQuit(const char *why); // port/src/video.c (D344)
extern "C" void inputRequestRescan(void); // port/src/input.c (D450)
@@ -18,6 +21,34 @@ static SDL_GLContext ctx;
static SDL_Renderer* renderer;
static int sdl_to_lus_table[512];
static bool vsync_enabled = true;
/* D952: macOS swap interval through CGL, not SDL. SDL3's Cocoa GL VSync makes
* SDL_GL_SwapWindow wait, with no timeout, for a tick from the CVDisplayLink it
* starts when the context is created. On some first launches of a never-seen
* binary that display link never starts (no CVDisplayLink thread in the hung
* process), so the first swap blocks forever: no frame, a black window. SDL's
* own interval stays 0 (its swap never waits) and CGL's kCGLCPSwapInterval does
* the VSync inside flushBuffer, as SDL2 did on macOS. Must be called with the
* context current (both callers are: init on the host thread, live config on
* the render thread). */
#if defined(__APPLE__)
static int s_cglSwapInterval = 0;
static bool gfx_sdl_apply_swap_interval(int interval) {
SDL_GL_SetSwapInterval(0);
CGLContextObj cgl = CGLGetCurrentContext();
GLint v = interval ? 1 : 0;
if (!cgl || CGLSetParameter(cgl, kCGLCPSwapInterval, &v) != kCGLNoError) {
return false;
}
s_cglSwapInterval = v;
{
GLint back = -1;
CGLGetParameter(cgl, kCGLCPSwapInterval, &back);
sysLogPrintf(LOG_INFO, "SDL: swap interval %d via CGL (read back %d)", (int)v, (int)back);
}
return true;
}
#endif
// OTRTODO: These are redundant. Info can be queried from SDL.
static int window_width = DESIRED_SCREEN_WIDTH;
static int window_height = DESIRED_SCREEN_HEIGHT;
@@ -61,8 +92,24 @@ static void gfx_sdl_set_fullscreen_flag(int32_t mode) {
}
}
/* D639: set once a resolution is picked explicitly (gfx_sdl_set_closest_resolution). */
static bool exclusive_mode_chosen = false;
static void set_fullscreen(bool on, bool call_callback) {
fullscreen_state = on;
/* D639: exclusive fullscreen switches the monitor to the window's display
* mode; with none set, SDL derives it from the current WINDOW size, so a
* 1440x1080 window asked the monitor for ~1440x1080 instead of its desktop
* resolution (seen with NVIDIA "OpenGL present method = Prefer native").
* Unless a resolution was picked explicitly, use the desktop mode of the
* display the window is on. */
if (on && fullscreen_flag == SDL_WINDOW_FULLSCREEN && !exclusive_mode_chosen) {
SDL_DisplayMode dm;
const int disp = SDL_GetWindowDisplayIndex(wnd);
if (SDL_GetDesktopDisplayMode(disp >= 0 ? disp : 0, &dm) == 0 && dm.w > 0 && dm.h > 0) {
SDL_SetWindowDisplayMode(wnd, &dm);
}
}
SDL_SetWindowFullscreen(wnd, on ? fullscreen_flag : 0);
if (call_callback && on_fullscreen_changed_callback) {
on_fullscreen_changed_callback(on);
@@ -86,6 +133,16 @@ static void gfx_sdl_get_active_window_refresh_rate(uint32_t* refresh_rate) {
*refresh_rate = mode.refresh_rate;
}
// D581 (#151): SDL lists the OS primary display first (X11 RandR primary,
// Windows MONITORINFOF_PRIMARY, macOS main display), so display 0 IS the
// primary. The original bug was centring with display 0's size as absolute
// coordinates, ignoring its position; callers now use its global bounds.
// Not "the display containing (0,0)": on X11 the primary can sit right of a
// secondary at the origin (the #151 layout; verified on KDE X11, 2026-10-13).
static int primary_display_index() {
return 0;
}
static void gfx_sdl_init(const struct GfxWindowInitSettings *set) {
window_width = set->width;
window_height = set->height;
@@ -94,12 +151,16 @@ static void gfx_sdl_init(const struct GfxWindowInitSettings *set) {
if (!set->allow_hidpi) {
// HiDPI control, if available
SDL_SetHint(SDL_HINT_VIDEO_HIGHDPI_DISABLED, "1");
#if defined(PLATFORM_WIN32) && defined(SDL_HINT_WINDOWS_DPI_AWARENESS)
// if HiDPI is disabled, declare ourselves DPI aware to get 1:1 window size on Windows
SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitor");
#endif
}
#endif
#if defined(_WIN32) && defined(SDL_HINT_WINDOWS_DPI_AWARENESS) /* PD used PLATFORM_WIN32, never defined here */
// Always declare per-monitor DPI awareness on Windows so the window and
// fullscreen surfaces are 1:1 with physical pixels. Without it, a scaled
// display (e.g. 3840x2160 at 125%) hands the game a 3072x1728 surface and
// stretches it (soft picture) -- this used to be set only when HiDPI was
// disabled. Width/Height in the ini are physical pixels either way.
SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2");
#endif
// D566: the port never takes text input, so never ask for an on-screen
// keyboard (Steam Deck Game Mode popped one at every launch). Literal
@@ -116,7 +177,7 @@ static void gfx_sdl_init(const struct GfxWindowInitSettings *set) {
// behaviour, and 4:3 so the pointer maps onto the render with no letterbox.
if (window_width <= 0 || window_height <= 0) {
SDL_DisplayMode dm;
if (SDL_GetDesktopDisplayMode(0, &dm) == 0 && dm.w > 0 && dm.h > 0) {
if (SDL_GetDesktopDisplayMode(primary_display_index(), &dm) == 0 && dm.w > 0 && dm.h > 0) {
int maxw = (int)(dm.w * 0.85);
int maxh = (int)(dm.h * 0.85);
if (maxw * 3 > maxh * 4) { window_height = maxh; window_width = maxh * 4 / 3; }
@@ -142,10 +203,15 @@ static void gfx_sdl_init(const struct GfxWindowInitSettings *set) {
}
if (set->centered) {
SDL_DisplayMode mode = {};
SDL_GetCurrentDisplayMode(0, &mode);
posX = mode.w / 2 - window_width / 2;
posY = mode.h / 2 - window_height / 2;
// centre on the primary display, in global desktop coordinates
SDL_Rect b = { 0, 0, 0, 0 };
if (SDL_GetDisplayBounds(primary_display_index(), &b) != 0 || b.w <= 0 || b.h <= 0) {
SDL_DisplayMode mode = {};
SDL_GetCurrentDisplayMode(0, &mode);
b.x = 0; b.y = 0; b.w = mode.w; b.h = mode.h;
}
posX = b.x + b.w / 2 - window_width / 2;
posY = b.y + b.h / 2 - window_height / 2;
}
if (set->fullscreen_is_exclusive) {
@@ -232,7 +298,11 @@ static void gfx_sdl_init(const struct GfxWindowInitSettings *set) {
}
SDL_GL_MakeCurrent(wnd, ctx);
#if defined(__APPLE__)
gfx_sdl_apply_swap_interval(1); /* D952 */
#else
SDL_GL_SetSwapInterval(1);
#endif
// D550: window / taskbar icon = the project mark (generated by
// tools_pc/make_icon.py into ge007_icon.h; R,G,B,A byte order per
@@ -309,16 +379,24 @@ static void get_centered_positions_native(int32_t width, int32_t height, int32_t
const int disp_idx = SDL_GetWindowDisplayIndex(wnd);
SDL_DisplayMode mode = {};
SDL_GetDesktopDisplayMode(disp_idx, &mode);
*posX = mode.w / 2 - width / 2;
*posY = mode.h / 2 - height / 2;
SDL_Rect b = { 0, 0, 0, 0 }; /* D581: the display's desktop offset */
if (disp_idx < 0 || SDL_GetDisplayBounds(disp_idx, &b) != 0) { b.x = 0; b.y = 0; }
*posX = b.x + mode.w / 2 - width / 2;
*posY = b.y + mode.h / 2 - height / 2;
}
static void gfx_sdl_get_centered_positions(int32_t width, int32_t height, int32_t *posX, int32_t *posY) {
const int disp_idx = SDL_GetWindowDisplayIndex(wnd);
SDL_DisplayMode mode = {};
SDL_GetCurrentDisplayMode(disp_idx, &mode);
*posX = mode.w / 2 - width / 2;
*posY = mode.h / 2 - height / 2;
/* D581: centre on the window's display in global coordinates (its bounds
* carry the desktop offset; the mode size alone lands on the leftmost). */
SDL_Rect b = { 0, 0, 0, 0 };
if (disp_idx < 0 || SDL_GetDisplayBounds(disp_idx, &b) != 0 || b.w <= 0 || b.h <= 0) {
SDL_DisplayMode mode = {};
SDL_GetCurrentDisplayMode(disp_idx < 0 ? 0 : disp_idx, &mode);
b.x = 0; b.y = 0; b.w = mode.w; b.h = mode.h;
}
*posX = b.x + b.w / 2 - width / 2;
*posY = b.y + b.h / 2 - height / 2;
}
static void gfx_sdl_set_closest_resolution(int32_t width, int32_t height, bool should_center) {
@@ -326,6 +404,7 @@ static void gfx_sdl_set_closest_resolution(int32_t width, int32_t height, bool s
const int disp_idx = SDL_GetWindowDisplayIndex(wnd);
SDL_DisplayMode closest = {};
if (SDL_GetClosestDisplayMode(disp_idx, &mode, &closest)) {
exclusive_mode_chosen = true; /* D639: an explicit pick wins over the desktop mode */
SDL_SetWindowDisplayMode(wnd, &closest);
SDL_SetWindowSize(wnd, closest.w, closest.h);
if (should_center) {
@@ -348,6 +427,12 @@ static void gfx_sdl_get_dimensions(uint32_t* width, uint32_t* height, int32_t* p
}
static void gfx_sdl_handle_events(void) {
#if defined(__APPLE__)
/* AppKit requires event polling on the process main thread. The host
* loop in videoPumpEvents owns the complete SDL queue, so the scheduler
* render thread must not call SDL_PollEvent on macOS. */
return;
#else
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
@@ -402,6 +487,7 @@ static void gfx_sdl_handle_events(void) {
break;
}
}
#endif
}
/* The window/context are created on the host main thread, but the game's
@@ -431,6 +517,25 @@ extern "C" void gfx_sdl_park_for_exit(void) {
}
}
/* D632: host thread, at exit, after the render thread has parked (context
* unbound everywhere, GPU drained by gfx_sdl_park_for_exit). Delete the GL
* context and the window while the process is still alive, so the driver
* retires its hardware queues through the normal path. Left alone, they are
* destroyed by the kernel inside process teardown (DXGPROCESS::Destroy ->
* DXGCONTEXT::DestroyContext -> VidSchTerminateHwQueue), which is exactly
* where the 0x119 bugcheck of 2026-10-10 fired (minidump, HAGS on). */
extern "C" void gfx_sdl_teardown_for_exit(void) {
if (ctx) {
SDL_GL_MakeCurrent(NULL, NULL); // host never holds it after init; harmless
SDL_GL_DeleteContext(ctx);
ctx = NULL;
}
if (wnd) {
SDL_DestroyWindow(wnd);
wnd = NULL;
}
}
/* Release the GL context from the calling (host main) thread. WGL only allows
* a context to be current on one thread at a time, and while another thread
* still holds it, wglMakeContextCurrent fails with "The requested resource is
@@ -445,6 +550,21 @@ extern "C" void gfx_sdl_release_context(void) {
/* The host thread owns SDL event pumping (see videoPumpEvents); when it sees
* a window resize it calls this to refresh the cached drawable size that
* fast3d uses for scaling. */
/* D578: refresh rate of the window's display, polled on the host thread
* (SDL video calls belong there) and read by the render thread. 0 = unknown. */
static SDL_atomic_t s_refresh_hz;
extern "C" void gfx_sdl_poll_refresh_rate(void) {
SDL_DisplayMode mode;
const int idx = wnd ? SDL_GetWindowDisplayIndex(wnd) : -1;
const int hz = (idx >= 0 && SDL_GetCurrentDisplayMode(idx, &mode) == 0) ? mode.refresh_rate : 0;
SDL_AtomicSet(&s_refresh_hz, hz);
}
extern "C" int gfx_sdl_get_refresh_rate(void) {
return SDL_AtomicGet(&s_refresh_hz);
}
extern "C" void gfx_sdl_update_cached_size(void) {
if (wnd) {
SDL_GL_GetDrawableSize(wnd, &window_width, &window_height);
@@ -546,11 +666,19 @@ static void gfx_sdl_set_window_title(const char *title) {
}
static int gfx_sdl_get_swap_interval(void) {
#if defined(__APPLE__)
return s_cglSwapInterval; /* D952 */
#else
return SDL_GL_GetSwapInterval();
#endif
}
static bool gfx_sdl_set_swap_interval(int interval) {
#if defined(__APPLE__)
const bool success = gfx_sdl_apply_swap_interval(interval); /* D952 */
#else
const bool success = SDL_GL_SetSwapInterval(interval) >= 0;
#endif
vsync_enabled = success && (interval != 0);
if (!success) {
sysLogPrintf(LOG_WARNING, "SDL: failed to set vsync %d: %s", interval, SDL_GetError());
+1
View File
@@ -35,6 +35,7 @@ void configRegisterString(const char *key, char *value, int bufSize);
* a file load). Return 1 if the key exists. Used by the settings presets. */
int configSetValue(const char *key, double v);
int configGetValue(const char *key, double *out);
int configIntKeyWasLoaded(const char *key); /* key present in the ini at last configLoad */
/* [Debug] knobs, env-var-or-ini. GE_PCDUMP / GE_INPUTLOG override the ini. */
const char *configGetFrameDump(void); /* "lo-hi[:step]" or NULL if unset */
+86
View File
@@ -0,0 +1,86 @@
#ifndef _INTERPPORTAL_H_
#define _INTERPPORTAL_H_
#include <stdint.h>
/* D578: port-side replay of bg.c's portal-visibility traversal for an
* interpolated camera, so each drawn room's scissor box can be recomputed for
* the in-between pass instead of using the one the game computed for the
* current camera. Read-only on game data. See port/src/interpportal.c and
* docs/dev/findings.md D578.
*
* Env: GE_INTERPPORTAL_OFF=1 never replace scissors (old matrix correction)
* GE_INTERPPORTAL_UNION=1 union the replayed box with the game's box */
#ifdef __cplusplus
extern "C" {
#endif
/* osSendMesg hook, GAME thread: when `msg` is the frame's graphics task going to
* the scheduler, snapshot everything the replay reads (the render worker draws
* the frame while the game thread is already on the next one). */
void interpPortalOnSend(void *mq, void *msg);
/* Start of a frame body / interpolation pass on the render thread: select the
* snapshot taken for display list `dl` and drop the cached replay. */
void interpPortalPassBegin(const void *dl);
/* Every G_MTX_PROJECTION|G_MTX_LOAD: `raw` is the matrix as the game loaded
* it, `blended` the in-between version (before the far-clip widening); equal
* on exact passes. Row-vector 4x4 floats as the gfx layer holds them. */
void interpPortalNoteProj(const float raw[4][4], const float blended[4][4]);
/* The scissor about to be applied. seg14 = last gSPSegment(14) value (the room
* vertices base), (rl,rt,rr,rb) = the game's scissor in N64 pixels. When it
* returns 1, *ol..*ob is the replacement (N64 pixels, left/top/right/bottom,
* same clamping as bgScissorCurrentPlayerView). 0 = leave the scissor alone
* (not a room scissor, no replay available, or replace == 0). D583: 2 = a room
* scissor whose room the replay at this camera does not reach (the caller
* hides a room that just entered the drawn set). */
int interpPortalScissor(uint32_t seg14, float rl, float rt, float rr, float rb,
int replace, int *ol, int *ot, int *or_, int *ob);
/* D578: replay counters {replaced, replays, fullview, gatefail, noroom, unreached, nocam, nosnap} */
void interpPortalCounts(unsigned out[8]);
/* D578: rooms the replay reached at the interpolated camera that the game did
* not draw (a hole risk); read-and-clear after a pass. interpPortalExtraCount
* = replays that found any. */
/* D583: rooms the last replay reached that the game did not draw for the
* current camera: returns the count; fills up to `max` vertex bases
* (SPSEGMENT_BG_VTX values) and whether the previous snapshot drew each
* (1 / 0 / -1 unknown). */
#define IP_MAXEXTRA 64
int interpPortalTakeExtraRooms(uint32_t *vphys, int *prevdrew, int max);
/* D583: 1 when this pass ran the portal replay at a blended camera (then the
* REACH test above decides holes geometrically). */
int interpPortalReplayedThisPass(void);
unsigned interpPortalExtraCount(void);
/* D954: the drawn frame's plain perspective (props) and perspective*lookat
* (rooms) as fixed-point Mtx words; 0 = unavailable (split-screen, menus). */
int interpPortalCamMatrices(int32_t proj[16], int32_t room[16]);
/* D955: one on-screen model instance of the drawn frame (game-thread snapshot).
* render_pos = the tick's joint matrix block (nmtx fixed-point Mtx, 64 bytes
* each; the DL loads them through segment 3). group = instances that must
* blend or fall back together (a chr, its held weapons and hat). */
typedef struct IpObjInst {
const void *render_pos;
const void *model;
const void *modelobj;
float pos[3];
int nmtx;
int group;
int type; /* PROP_TYPE_* */
int rigid; /* OBJ / DOOR / WEAPON prop (not a chr or its attachment) */
} IpObjInst;
/* D955: the drawn frame's instances; *origin = current_model_pos (the room
* origin every view matrix is relative to), *serial = snapshot serial. 0 =
* none (split-screen, menus, no snapshot). */
int interpPortalObjInstances(const IpObjInst **out, float origin[3], unsigned *serial);
#ifdef __cplusplus
}
#endif
#endif
+13 -2
View File
@@ -28,10 +28,21 @@
* "using typedef-name after 'struct'"), and no C++ TU has an implicit
* declaration needing a fix here. C++ TUs keep their pre-D38 header exposure.
*/
#ifndef _PC_PROTOS_H_
#define _PC_PROTOS_H_
#if defined(PORT) && defined(__x86_64__) && !defined(__cplusplus)
#if defined(__APPLE__)
/* Darwin SDK headers expose these libc calls as fortify function-like
* macros. This legacy declaration list needs their actual symbol names. */
#undef memcpy
#undef snprintf
#undef strcat
#undef strcpy
#undef strncpy
#endif
#if defined(PORT) && defined(PLATFORM_64BIT) && !defined(__cplusplus)
#include <PR/ultratypes.h> /* u8..s32, f32, size_t (host on PC) */
#include <PR/gbi.h> /* Gfx, Mtx, Vtx, Light (shimmed on PC) */
@@ -482,5 +493,5 @@ Gfx * watchRenderControllerOpaque();
u32 weaponLoadProjectileModels();
void zbufSetBuffer();
#endif /* PORT && __x86_64__ && !__cplusplus */
#endif /* PORT && PLATFORM_64BIT && !__cplusplus */
#endif /* _PC_PROTOS_H_ */
+187 -9
View File
@@ -1,23 +1,201 @@
#ifndef PORT_PORTADDR_H
#define PORT_PORTADDR_H
/* Stage B step 1 (STAGEB-ABI-DESIGN, D441 follow-on): the ONE place that
* names the N64-DRAM arena windows. port/src/dram.c maps an 8 MB backing
/*
* portaddr.h — the ONE place that names the N64-DRAM arena windows and
* converts between 32-bit N64 addresses and live host pointers.
*
* D607: unifies D441's arena macros (this file, pre-unification) with the
* #95 macOS address-window model (port/include/port_addr.h), which is
* superseded by this file. One header, one shifted base, one guard helper.
*
* The port realises the N64's 32-bit address space as a range of *host*
* addresses (the [PORT_ADDR_BASE, PORT_ADDR_BASE + PORT_ADDR_WINDOW) window),
* because the game stores addresses in 32-bit fields and 32-bit arithmetic
* and expects `(u32)ptr` to round-trip:
*
* 0x00000000..0x0fffffff segmented DL addresses (never host memory)
* 0x10000000..0x1fffffff cart image + sidecars (CART_BASE)
* 0x40000000..0x6fffffff image-relative encoding (see portN64ToHost)
* 0x70000000..0x707fffff DRAM V1 ("virtual", s32-safe; all RAM symbols)
* 0x80000000..0x807fffff DRAM V2 (KSEG0 mirror of V1)
* 0xa0000000..0xbfffffff game-thread stacks
*
* On Windows/Linux the window starts at host address 0, so `(u32)ptr ==
* offset` and the pointer IS the N64 address. Native arm64 macOS cannot map
* below 4 GiB (__PAGEZONE; docs/dev/MACOS-ARM64-PLAN.md sec 1), so there the
* whole range is shifted up by PORT_ADDR_BASE = 16 TiB. Every game-visible
* 32-bit value is then IDENTICAL to the x86_64 build; only the u32->host
* direction adds the base. PORT_ADDR_BASE is 0 everywhere but arm64 macOS,
* so those builds are bit-identical to the pre-unification literals (the
* identity claim the x86_64 golden gates verify).
*
* Compile-time constants: no codegen change on x86_64.
*/
/*
* The shifted base of the window. #ifndef-guarded so a -D on the command
* line (CMake's GE_HIGHARENA_BASE flavour; the #95 PORT_ADDR_BASE cache var
* for tooling) can override it. 16 TiB is reserved cleanly on Apple
* Silicon, far above libmalloc's regions and the mmap hint area, and well
* below fast3d_ptr_ok()'s 128 TiB bound; an AddressSanitizer build (whose
* shadow region overlaps 16 TiB) uses a LowMem base like 0x4000000000.
*
* The base selection below is shared by every context: preprocessed .S
* input (dram_syms.S via `cc -E`, where only the macros below matter and
* platform.h's C content would not survive the assembler) and C/C++ alike.
* __APPLE__ && __aarch64__ mirrors platform.h's PLATFORM_MACOS &&
* PLATFORM_ARM selection exactly. portaddr.h stays self-contained (no
* platform.h include): it is included from every TU, asm and C++ alike.
*/
#ifndef PORT_ADDR_BASE
#if defined(__APPLE__) && defined(__aarch64__)
#define PORT_ADDR_BASE 0x100000000000ULL
#else
#define PORT_ADDR_BASE 0
#endif
#endif
/* Size of the N64 address space. */
#define PORT_ADDR_WINDOW (1ULL << 32)
/* The low PORT_ADDR_LOW_GUARD bytes of the window are rejected by the
* plausibility checks: nothing is mapped there, and with PORT_ADDR_BASE == 0
* that range contains NULL, which is exactly what the callers' plausibility
* checks exist to catch (e.g. langGet() on a cleared bank). */
#define PORT_ADDR_LOW_GUARD 0x10000u
/* Bound on the executable image's size, for classifying a host pointer as
* image-relative (portHostToN64). The binary is a few MB; 256 MiB is
* comfortably generous. */
#define PORT_IMAGE_MAX (256u << 20)
/*
* The N64-DRAM arena windows (D441). port/src/dram.c maps an 8 MB backing
* store at BOTH views; everything that converts or range-checks an arena
* 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. */
* high-arena test flavour, the arm64 PORT_ADDR_BASE shift) is a change in
* one header.
*
* 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). On x86_64 the
* defaults expand to exactly the pre-unification literals.
*/
#ifndef PORT_DRAM_V1_BASE
#define PORT_DRAM_V1_BASE 0x70000000UL /* s32-safe "virtual" view (game RAM symbols) */
#define PORT_DRAM_V1_BASE (PORT_ADDR_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) */
#define PORT_DRAM_K0_BASE (PORT_ADDR_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
#ifndef __ASSEMBLER__
#include <stdint.h>
/* D573 (#108, italoarruda): the ONE plausibility check for a host pointer
* read out of game data (sndPlaySfx's ALSound*, langGet's text bank). True
* only inside the DRAM arena views above or the mapped cart image (ROM +
* model/bg sidecars, port/src/romdata.c). Replaces the old absolute
* [64 KiB, 16 GiB) literal, which goes wrong as soon as the arena moves
* (the arm64 PORT_ADDR_BASE shift, the D441 above-4-GiB tier) and which
* also accepted any unmapped address in between. The low 64 KiB (NULL-ish)
* is outside every window, so it is rejected.
*
* D607: this is the merged successor of #95's portAddrIsInWindow() -- the
* #95 re-application folds its guard call sites (src/snd.c,
* src/game/language.c: the same two guards) into portAddrIsMapped(), and
* port/src/port_addr.c's self-test uses the window-bound constants above
* (PORT_ADDR_LOW_GUARD / PORT_ADDR_WINDOW) for its boundary cases. */
/* Set by portAddrInit(): the executable's load address, and whether the
* image lies outside the window (so a truncated pointer needs the
* 0x40000000 encoding). False on Linux, true on Windows and macOS.
* Defined by port/src/port_addr.c (lands with the #95 re-application);
* nothing in the current tree references them until then. */
#ifdef __cplusplus
extern "C" {
#endif
extern uintptr_t g_portImageBase;
extern int g_portUseImageRel;
/* D573: exact check, implemented in port/src/romdata.c (shift-aware via
* the arena macros above). */
int portAddrIsMapped(const void *p);
#ifdef PORT_ADDR_STRICT
/* Debug gate (-DPORT_ADDR_STRICT=1). Validates that a u32 being turned into
* a host pointer actually names one of the N64 regions the port maps --
* reports the bad ones AT THE CONVERSION, with a backtrace naming the
* caller. Compiled out entirely by default; costs one call when enabled.
* Defined by port/src/port_addr.c. */
void portAddrStrictCheck(uint32_t a);
#endif
/* Reserve the window (arm64 macOS) and record the image base. Must run
* before any of the port's fixed mappings and before large allocations.
* Defined by port/src/port_addr.c. */
void portAddrInit(void);
/* Result of the ROM-free self-test portAddrInit() ran (1 = all checks
* passed). Used by `--selftest` so CI can gate on it without a ROM. */
int portAddrSelfTestPassed(void);
#ifdef __cplusplus
}
#endif
/* u32 N64/physical address -> live host pointer.
*
* N64 address 0 maps to NULL, NOT to PORT_ADDR_BASE. Offset 0 is never a
* mapped address (the window's lowest live region is the cart at
* 0x1000_0000), so base+0 lands in the reserved PROT_NONE area and faults.
* More importantly, game code routinely signals failure by returning an N64
* address of 0 and then testing the converted pointer for NULL --
* `p = PORT_N64PTR(T, f()); if (p != NULL) *p = ...` (e.g.
* chrCreateBloodStain(), objDeform()). With base+0 that test passes and the
* very next store faults. Returning NULL restores the decomp's intended
* semantics. Identity on x86_64: with PORT_ADDR_BASE == 0 the image-relative
* path (0x40000000 + (ptr - image base), image base 0x140000000 on Windows)
* round-trips the exact truncation the D131 fix relied on. */
static inline void *portN64ToHost(uint32_t a)
{
if (a == 0) {
return (void *)0; /* NULL, without pulling in stddef.h: portaddr.h is included by every TU, asm and C++ alike */
}
#ifdef PORT_ADDR_STRICT
portAddrStrictCheck(a);
#endif
if (g_portUseImageRel && a >= 0x40000000u && a < 0x70000000u) {
return (void *)(g_portImageBase + (uintptr_t)(a - 0x40000000u));
}
return (void *)((uintptr_t)PORT_ADDR_BASE + (uintptr_t)a);
}
/* Host pointer -> u32, the inverse of portN64ToHost for in-window and
* image pointers. Out-of-range pointers fall back to raw truncation (the
* pre-shift behaviour); they should not be stored in 32-bit fields. */
static inline uint32_t portHostToN64(const void *p)
{
uintptr_t v = (uintptr_t)p;
if (v - (uintptr_t)PORT_ADDR_BASE < (uintptr_t)PORT_ADDR_WINDOW) {
return (uint32_t)(v - (uintptr_t)PORT_ADDR_BASE);
}
if (g_portUseImageRel && v >= g_portImageBase
&& v - g_portImageBase < (uintptr_t)PORT_IMAGE_MAX) {
return (uint32_t)(0x40000000u + (v - g_portImageBase));
}
return (uint32_t)v;
}
/* Cast-site helper for game code (the D3x ABI/layout class): reads a 32-bit
* address out of a field/expression and yields a typed pointer. Identity on
* Windows/Linux (PORT_ADDR_BASE == 0). */
#define PORT_N64PTR(T, expr) ((T *)portN64ToHost((uint32_t)(expr)))
#endif /* __ASSEMBLER__ */
#endif /* PORT_PORTADDR_H */
+9
View File
@@ -40,6 +40,15 @@ void sysLogPrintf(enum LogLevel level, const char *fmt, ...)
void sysFatalError(const char *fmt, ...)
__attribute__((format(printf, 1, 2))) __attribute__((noreturn));
/*
* D576: open $S/ge007.log (next to the ini) as a duplicate sink for every
* sysLogPrintf/sysFatalError line -- on a normal (double-click) launch
* stderr is lost. Rolls a previous run's ge007.log to ge007.prev.log, caps
* the file at 8 MiB, and closes it via atexit. Idempotent; if the file
* cannot be opened, logging continues on stderr only.
*/
void sysLogOpenFile(void);
/* --- Paths -------------------------------------------------------------- */
/*
+63
View File
@@ -0,0 +1,63 @@
/*
* Parsing helpers for the opt-in update check (see port/src/updatecheck.c).
* Header-only (static inline) so tools_pc/updatecheck_test.c can exercise
* them offline without pulling in SDL or the rest of the port.
*/
#ifndef UPDATECHECK_PARSE_H
#define UPDATECHECK_PARSE_H
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
/* Parse "vMAJOR.MINOR.PATCH" (missing parts = 0, suffix ignored). */
static inline int parseVer(const char *s, int v[3])
{
v[0] = v[1] = v[2] = 0;
if (*s == 'v' || *s == 'V') s++;
if (*s < '0' || *s > '9') return 0;
for (int i = 0; i < 3; i++) {
if (*s < '0' || *s > '9') break;
v[i] = (int)strtol(s, (char **)&s, 10);
if (*s != '.') break;
s++;
}
return 1;
}
static inline int versionNewer(const char *tag, const char *running)
{
int a[3], b[3];
if (!parseVer(tag, a) || !parseVer(running, b)) return 0;
for (int i = 0; i < 3; i++) {
if (a[i] != b[i]) return a[i] > b[i];
}
return 0;
}
/* Minimal scan for "tag_name": "<value>". */
static inline int extractTag(const char *body, char *out, size_t n)
{
const char *p = strstr(body, "\"tag_name\"");
if (!p) return 0;
p += 10;
while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++;
if (*p != ':') return 0;
p++;
while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++;
if (*p != '"') return 0;
p++;
size_t i = 0;
/* Tag characters only (no control/ANSI bytes reach the log). */
while (*p && *p != '"' && i + 1 < n) {
char c = *p++;
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
c == '.' || c == '_' || c == '-')) return 0;
out[i++] = c;
}
if (*p != '"' || i == 0) return 0;
out[i] = 0;
return 1;
}
#endif
+11
View File
@@ -39,6 +39,12 @@ void videoDestroy(void);
void videoStartFrame(void);
void videoSubmitCommands(Gfx *cmds); /* runs the software RSP on the list */
void videoEndFrame(void);
/* D578: frame interpolation present rate (0 = off) and the worker-side present. */
int videoInterpHz(void);
int videoAutoFpsCap(void); /* D578: what Video.FpsCap=-1 (Auto) resolves to now */
int videoInterpVSync(void); /* D578: swaps block on the display refresh */
void videoInterpInfo(int *refreshHz, int *swapInterval); /* D578 log */
void videoInterpPresent(int slot);
/* Host-thread SDL event pump: keeps the window responsive (Windows only
* dispatches messages to the creating thread) and handles quit. Called in a
@@ -51,6 +57,7 @@ void videoPumpEvents(void);
* could be inside the GL driver (the 0x119 bugchecks). */
void videoRequestQuit(const char *why);
int videoQuitRequested(void);
int videoVSyncOn(void); /* D600 */
/* D443: orderly quit + relaunch (main.c atexit reads videoRestartRequested). */
void videoRequestRestart(const char *why);
int videoRestartRequested(void);
@@ -96,6 +103,10 @@ int videoIsFullscreen(void);
* on a clean exit). No-op if the window isn't up. */
void videoSaveWindowState(void);
/* D632: delete the GL context + window at exit (only if the render thread
* parked). Called from the atexit handler after configSave. */
void videoTeardownForExit(void);
#ifdef __cplusplus
}
#endif
+21 -2
View File
@@ -29,12 +29,31 @@
#include "portaddr.h"
#if defined(PORT)
# if defined(__APPLE__)
/* Darwin exposes errno and sprintf as function-like macros. Suppress them
* while parsing the N64 API's errno fields and sprintf declaration. */
# pragma push_macro("errno")
# pragma push_macro("sprintf")
# undef errno
# undef sprintf
# endif
# include "include/PR/os.h"
# if defined(__APPLE__)
# pragma pop_macro("sprintf")
# pragma pop_macro("errno")
# endif
/* Offset of a DRAM V1 pointer from the V1 base. On macOS the V1 base is
* PORT_ADDR_BASE + 0x70000000 (PORT_DRAM_V1_BASE, portaddr.h — already
* included above), so the base must be subtracted too; the result
* is a small (< 8 MB) offset that fits s32 and that fast3d's seg_addr()
* re-bases into the V2/KSEG0 mirror. Identity behaviour at PORT_ADDR_BASE==0. */
# undef OS_K0_TO_PHYSICAL
# define OS_K0_TO_PHYSICAL(x) ((u32)((char *)(x) - PORT_DRAM_V1_BASE))
# define OS_K0_TO_PHYSICAL(x) ((u32)((uintptr_t)(x) - (uintptr_t)PORT_DRAM_V1_BASE))
# undef OS_PHYSICAL_TO_K0
/* Identity: callers pass either a live V1 pointer (which is preserved with
* its full 64-bit value through the 64-bit GBI words) or a small physical
* offset (which fast3d re-bases). Both are unaffected by the window base. */
# define OS_PHYSICAL_TO_K0(x) ((void *)(x))
#else
+20
View File
@@ -226,6 +226,13 @@ static char *trim(char *s)
static int s_unknownKeys;
static char s_unknownFirst[48];
/* Keys the port used to write and has since retired: dropped silently
* (not counted as unknown) and not written back by configSave. */
static const char *const kRetiredKeys[] = {
"Input.MenuPointerMode", /* D574 */
"Input.MenuPointerSpeed", /* D574 */
};
static void applyKV(const char *dottedKey, const char *val)
{
for (int i = 0; i < numIntOpts; i++) {
@@ -273,6 +280,9 @@ static void applyKV(const char *dottedKey, const char *val)
return;
}
}
for (size_t r = 0; r < sizeof(kRetiredKeys) / sizeof(kRetiredKeys[0]); r++) {
if (ci_eq(kRetiredKeys[r], dottedKey)) return; /* D574 */
}
sysLogPrintf(LOG_NOTE, "config: unknown key '%s' (ignored)", dottedKey);
if (s_unknownKeys++ == 0) { /* D472: surfaced once in the F10 overlay */
strncpy(s_unknownFirst, dottedKey, sizeof(s_unknownFirst) - 1);
@@ -326,6 +336,16 @@ int configSetValue(const char *key, double v)
return 0;
}
/* 1 if the last configLoad read this registered int key from the ini (0 on a
* fresh install or an ini that predates the key). Lets a default depend on
* "the user never chose" (D578 gamescope VSync default). */
int configIntKeyWasLoaded(const char *key)
{
for (int i = 0; i < numIntOpts; i++)
if (ci_eq(intOpts[i].key, key)) return intOpts[i].seen;
return 0;
}
int configGetValue(const char *key, double *out)
{
for (int i = 0; i < numIntOpts; i++)
+41 -7
View File
@@ -11,6 +11,12 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE 1
#endif
#if defined(__APPLE__) && !defined(_XOPEN_SOURCE)
#define _XOPEN_SOURCE 700
#endif
#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE)
#define _DARWIN_C_SOURCE 1
#endif
#include <stdlib.h>
#include <stdio.h>
@@ -347,7 +353,7 @@ static LONG __stdcall crashHandler(PEXCEPTION_POINTERS exinfo)
return EXCEPTION_CONTINUE_EXECUTION;
}
#elif defined(PLATFORM_LINUX)
#elif defined(PLATFORM_LINUX) || defined(PLATFORM_MACOS)
#include <ucontext.h>
#include <signal.h>
@@ -356,6 +362,9 @@ static LONG __stdcall crashHandler(PEXCEPTION_POINTERS exinfo)
#include <ctype.h>
#include <dlfcn.h>
#include <sys/fcntl.h>
#if defined(PLATFORM_MACOS)
#include <mach-o/dyld.h> /* _dyld_get_image_vmaddr_slide (for atos) */
#endif
static struct sigaction prevSigAction;
@@ -385,7 +394,7 @@ static void crashStackTrace(char *msg, int sig, void *pc, ucontext_t *ucontext,
if (siginfo && (sig == SIGSEGV || sig == SIGBUS)) {
CRASH_MSG("FAULT ADDR: %p\n", (void *)siginfo->si_addr);
}
#if defined(PLATFORM_X86_64)
#if defined(PLATFORM_LINUX) && defined(PLATFORM_X86_64)
if (ucontext) {
CRASH_MSG("REGS: Rax=%p Rcx=%p Rdx=%p Rsi=%p Rdi=%p R8=%p R9=%p R10=%p R11=%p\n",
(void *)ucontext->uc_mcontext.gregs[REG_RAX],
@@ -401,6 +410,27 @@ static void crashStackTrace(char *msg, int sig, void *pc, ucontext_t *ucontext,
(void *)ucontext->uc_mcontext.gregs[REG_RSP],
(void *)ucontext->uc_mcontext.gregs[REG_RBP]);
}
#elif defined(PLATFORM_MACOS) && defined(PLATFORM_ARM)
/* Darwin's uc_mcontext is a POINTER to __darwin_mcontext64; the arm64
* thread state is __ss.__x[0..28], __fp, __lr, __sp, __pc. */
if (ucontext) {
CRASH_MSG("REGS: x0=%p x1=%p x2=%p x3=%p x4=%p x5=%p x6=%p x7=%p\n",
(void *)(uintptr_t)ucontext->uc_mcontext->__ss.__x[0],
(void *)(uintptr_t)ucontext->uc_mcontext->__ss.__x[1],
(void *)(uintptr_t)ucontext->uc_mcontext->__ss.__x[2],
(void *)(uintptr_t)ucontext->uc_mcontext->__ss.__x[3],
(void *)(uintptr_t)ucontext->uc_mcontext->__ss.__x[4],
(void *)(uintptr_t)ucontext->uc_mcontext->__ss.__x[5],
(void *)(uintptr_t)ucontext->uc_mcontext->__ss.__x[6],
(void *)(uintptr_t)ucontext->uc_mcontext->__ss.__x[7]);
CRASH_MSG("Pc=%p Lr=%p Sp=%p Fp=%p\n",
(void *)(uintptr_t)ucontext->uc_mcontext->__ss.__pc,
(void *)(uintptr_t)ucontext->uc_mcontext->__ss.__lr,
(void *)(uintptr_t)ucontext->uc_mcontext->__ss.__sp,
(void *)(uintptr_t)ucontext->uc_mcontext->__ss.__fp);
}
CRASH_MSG("IMAGE SLIDE: 0x%lx\n",
(unsigned long)(uintptr_t)_dyld_get_image_vmaddr_slide(0));
#endif
CRASH_MSG("PC: ");
if (pc) {
@@ -454,10 +484,14 @@ static void crashHandler(int sig, siginfo_t *siginfo, void *ctx)
ucontext_t *ucontext = ctx ? (ucontext_t *)ctx : NULL;
void *pc = NULL;
if (ucontext) {
#ifdef PLATFORM_X86
#if defined(PLATFORM_LINUX) && defined(PLATFORM_X86)
pc = (void *)ucontext->uc_mcontext.gregs[REG_EIP];
#elif defined(PLATFORM_X86_64)
#elif defined(PLATFORM_LINUX) && defined(PLATFORM_X86_64)
pc = (void *)ucontext->uc_mcontext.gregs[REG_RIP];
#elif defined(PLATFORM_MACOS) && defined(PLATFORM_X86_64)
pc = (void *)ucontext->uc_mcontext->__ss.__rip;
#elif defined(PLATFORM_MACOS) && defined(PLATFORM_ARM)
pc = (void *)(uintptr_t)ucontext->uc_mcontext->__ss.__pc;
#endif
}
@@ -514,7 +548,7 @@ void crashDumpThreads(const unsigned long *tids, const char **names, int count)
}
}
#endif /* PLATFORM_WINDOWS / PLATFORM_LINUX */
#endif /* PLATFORM_WINDOWS / POSIX */
int g_CrashEnabled = 0;
@@ -549,7 +583,7 @@ void crashInit(void)
SetErrorMode(SEM_FAILCRITICALERRORS);
prevExFilter = SetUnhandledExceptionFilter(crashHandler);
g_CrashEnabled = 1;
#elif defined(PLATFORM_LINUX)
#elif defined(PLATFORM_LINUX) || defined(PLATFORM_MACOS)
struct sigaction sigact = { 0 };
sigact.sa_flags = SA_SIGINFO | SA_ONSTACK;
sigact.sa_sigaction = crashHandler;
@@ -571,7 +605,7 @@ void crashShutdown(void)
if (prevExFilter) {
SetUnhandledExceptionFilter(prevExFilter);
}
#elif defined(PLATFORM_LINUX)
#elif defined(PLATFORM_LINUX) || defined(PLATFORM_MACOS)
sigaction(SIGSEGV, &prevSigAction, NULL);
sigaction(SIGABRT, &prevSigAction, NULL);
sigaction(SIGBUS, &prevSigAction, NULL);
+15 -2
View File
@@ -54,7 +54,10 @@
#include <windows.h>
#else
#define _GNU_SOURCE /* memfd_create — must precede all system headers */
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#endif
@@ -102,10 +105,20 @@ void *dramReserve(void)
CloseHandle(hSec);
return v1;
#else
/* memfd + two MAP_SHARED mmaps = one backing store, two views. */
/* One shared file descriptor + two MAP_SHARED mmaps = one backing store,
* two views. Linux can keep it fully anonymous with memfd; Darwin uses a
* POSIX shared-memory object and unlinks its name immediately. */
#if defined(PLATFORM_MACOS)
char shmName[64];
snprintf(shmName, sizeof(shmName), "/ge007_dram_%ld", (long)getpid());
int fd = shm_open(shmName, O_RDWR | O_CREAT, 0600);
if (fd >= 0)
shm_unlink(shmName);
#else
int fd = memfd_create("ge007_dram", 0);
#endif
if (fd < 0) {
sysFatalError("dram: memfd_create failed");
sysFatalError("dram: could not create shared backing store");
}
if (ftruncate(fd, DRAM_SIZE) != 0) {
sysFatalError("dram: ftruncate failed");
+327 -32
View File
@@ -188,7 +188,6 @@ extern s32 lvlGetCurrentStageToLoad(void);
#define CONTROLLER_CONFIG_HONEY_ 0
#define CONTROLLER_CONFIG_SOLITARE_ 1
#define CONTROLLER_CONFIG_GOODNIGHT_ 3 /* 1.4: SOLITARE movement, swapped button roles (bondview2.c) */
#define MENU_POINTER_GAIN 1.5
#define TRIG_THRESHOLD (30 * 256)
#define RSTICK_THRESHOLD 0x4000
/* Mouse-look tuning. GE's aim model is mode-dependent (bondview2.c
@@ -466,7 +465,33 @@ static const struct { const char *key; const char *label; const char *def; } kPa
[PA_START] = { "Start", "Start / pause", "start" },
};
typedef struct { u32 mask[PA_COUNT]; } PadBinds;
/* D643 (#145 b/c, #149): Original control scheme = a STRICT virtual N64
* controller. Each native N64 button is bound to up to two pad sources
* (Input.N64Pad[N].<Button>), and a press emits exactly that bit in every
* context (stage, menu, watch, tank) -- no port action (dedicated use/reload,
* crouch, gadget cycle, prev-weapon) is injected; the game's own code decides
* what A/B/Z/... mean (B = "use if usable else reload" lives in lv.c).
* Defaults follow the Nintendo Switch Online N64 convention. The right stick
* stays digital C-buttons (fixed, in addition to any C source bound here). */
enum { NB_A, NB_B, NB_Z, NB_R, NB_L, NB_CU, NB_CD, NB_CL, NB_CR, NB_START,
NB_DU, NB_DD, NB_DL, NB_DR, NB_COUNT };
static const struct { const char *key; const char *def; unsigned bit; } kN64Btn[NB_COUNT] = {
[NB_A] = { "A", "a", GE_CONT_A },
[NB_B] = { "B", "x", GE_CONT_B },
[NB_Z] = { "Z", "righttrigger", GE_CONT_G },
[NB_R] = { "R", "lefttrigger,rightshoulder", GE_CONT_R },
[NB_L] = { "L", "leftshoulder", GE_CONT_L },
[NB_CU] = { "CUp", "none", GE_CONT_E },
[NB_CD] = { "CDown", "none", GE_CONT_D },
[NB_CL] = { "CLeft", "none", GE_CONT_C },
[NB_CR] = { "CRight", "none", GE_CONT_F },
[NB_START] = { "Start", "start", GE_CONT_START },
[NB_DU] = { "DUp", "dpup", GE_CONT_UP },
[NB_DD] = { "DDown", "dpdown", GE_CONT_DOWN },
[NB_DL] = { "DLeft", "dpleft", GE_CONT_LEFT },
[NB_DR] = { "DRight", "dpright", GE_CONT_RIGHT },
};
typedef struct { u32 mask[PA_COUNT]; u32 n64[NB_COUNT]; } PadBinds;
typedef struct { PadBinds seat[MAX_PADS]; } PadTable;
static int padPreset[MAX_PADS] = {0}; /* D518: per-seat (Input.PadPreset[2/3/4]) */
@@ -476,10 +501,24 @@ static int padPreset[MAX_PADS] = {0}; /* D518: per-seat (Input.PadPre
* (D498: declared here so the stick-style helpers below can read it.) */
static int naturalPitchMode = 1; /* Input.NaturalPitch */
static int controlScheme = 0; /* D513 Input.ControlScheme: 0 Modern (port forces the style), 1 OG (the game's own selection is honoured) */
/* D513 accessor (#145): watchsettings.c gates D564's pin of the game's own
* "Look up/down" value on this: in the Modern scheme the port's Invert
* look rows own the inversion; in Original the game's watch setting is
* the live control (the game honours it in bondview2's stick-pitch path),
* so it must stay user-settable there. */
int inputControlScheme(void) { return controlScheme; }
/* D645: Original scheme -- F10 "Invert look" mirrors the game's own
* "Look up/down" (watchsettings.c keeps the two in sync both ways). */
static int padLookInvertY = 0; /* Input.PadLookInvertY: 1 = invert the look stick's Y (right stick in SOLITARE roles, left stick in HONEY roles -- #145) */
int inputPadLookInvert(void) { return padLookInvertY; }
void inputSetPadLookInvert(int v) { padLookInvertY = v ? 1 : 0; }
static char padStr[MAX_PADS][PA_COUNT][32]; /* Input.Pad[N].<Action> */
static char padStrKey[MAX_PADS][PA_COUNT][40];
static char n64Str[MAX_PADS][NB_COUNT][32]; /* D643 Input.N64Pad[N].<Button> */
static char n64StrKey[MAX_PADS][NB_COUNT][40];
static int n64StickFeel = 1; /* D643 Input.N64StickFeel (Original only): 1 accurate, 0 the Modern deadzone/rescale */
static PadTable padTabBuf[2];
static SDL_atomic_t padTabPub; /* published PadTable* */
static void *padTabPub; /* published PadTable*; pointer-sized/aligned for SDL_Atomic{Get,Set}Ptr (D599) */
static unsigned padTabSig = 0;
static int padTabValid = 0;
@@ -512,6 +551,21 @@ static void padBuildBinds(PadBinds *b, int seat, int custom, int *bad)
for (int a = 0; a < PA_COUNT; a++)
b->mask[a] = padParseMask(custom ? padStr[seat][a] : kPadAct[a].def, bad);
if (!b->mask[PA_START]) b->mask[PA_START] = PSM(PS_START); /* lockout guard: keep a pause path */
for (int n = 0; n < NB_COUNT; n++) b->n64[n] = 0;
}
/* D643: the Original table. Port actions are zeroed (nothing may inject
* use/reload/crouch/gadget/weapon-cycle); n64[] carries the buttons. Guards:
* Start unbound -> default; A and B both unbound -> both default (menu lockout). */
static void padBuildN64(PadBinds *b, int seat, int *bad)
{
for (int a = 0; a < PA_COUNT; a++) b->mask[a] = 0;
for (int n = 0; n < NB_COUNT; n++) b->n64[n] = padParseMask(n64Str[seat][n], bad);
if (!b->n64[NB_START]) b->n64[NB_START] = padParseMask(kN64Btn[NB_START].def, NULL);
if (!b->n64[NB_A] && !b->n64[NB_B]) {
b->n64[NB_A] = padParseMask(kN64Btn[NB_A].def, NULL);
b->n64[NB_B] = padParseMask(kN64Btn[NB_B].def, NULL);
}
}
/* D498: preset 3 is Custom; 4 = 1.4 Elektra is a fixed mapping, not custom.
@@ -560,6 +614,11 @@ static unsigned padComputeSig(void)
{
int anyCustom = 0;
unsigned h = 2166136261u;
h = (h ^ (unsigned)controlScheme) * 16777619u; /* D643 */
if (controlScheme)
for (int s = 0; s < MAX_PADS; s++)
for (int n = 0; n < NB_COUNT; n++)
for (const char *p = n64Str[s][n]; *p; p++) h = (h ^ (unsigned char)*p) * 16777619u;
for (int s = 0; s < MAX_PADS; s++) {
h ^= (unsigned)padPreset[s] * 16777619u;
if (padPresetCustom(s)) anyCustom = 1;
@@ -585,7 +644,10 @@ static const PadTable *padTabGet(void)
PadTable *cur = (PadTable *)SDL_AtomicGetPtr((void **)&padTabPub);
PadTable *spare = (cur == &padTabBuf[0]) ? &padTabBuf[1] : &padTabBuf[0];
int bad = 0;
for (int s = 0; s < MAX_PADS; s++) padBuildBinds(&spare->seat[s], s, padPresetCustom(s), &bad);
for (int s = 0; s < MAX_PADS; s++) {
padBuildBinds(&spare->seat[s], s, padPresetCustom(s), &bad);
if (controlScheme) padBuildN64(&spare->seat[s], s, &bad); /* D643 */
}
SDL_AtomicSetPtr((void **)&padTabPub, spare);
padTabSig = sig;
padTabValid = 1;
@@ -619,7 +681,7 @@ static u32 padActHeld(const PadBinds *b, u32 raw)
return h;
}
typedef struct { int facePlayable; int slotNative; int southpaw; int tank; } PadMapCtx;
typedef struct { int facePlayable; int slotNative; int southpaw; int tank; int orig; } PadMapCtx; /* orig: D643 */
typedef struct { unsigned button; int crouch; int gadgetEdge; u32 actHeld; } PadMapOut;
/* The pure mapping: raw source bits + bindings + previous action state ->
@@ -630,6 +692,15 @@ static void padMapPure(const PadBinds *b, u32 raw, u32 actPrev, unsigned buttonI
const PadMapCtx *c, PadMapOut *o)
{
unsigned button = buttonIn;
if (c->orig) { /* D643: strict virtual N64 pad; same in every context, no port actions, no southpaw swap */
for (int n = 0; n < NB_COUNT; n++)
if (raw & b->n64[n]) button |= kN64Btn[n].bit;
o->button = button;
o->crouch = 0;
o->gadgetEdge = 0;
o->actHeld = 0;
return;
}
u32 held = padActHeld(b, raw);
if (raw & b->mask[PA_FIRE] & PS_TRIGGERS) button |= GE_CONT_G;
if (raw & b->mask[PA_AIM] & PS_TRIGGERS) button |= GE_CONT_R;
@@ -835,7 +906,14 @@ void inputApplyMouseRequests(void)
* pointer could not reach the outer grid cells. Click-to-lock is the only
* mode now; existing ini files setting the old key are ignored.
* Controller input is entirely independent of all of this. */
/* D594: on macOS the FOCUS_GAINED-driven click-to-lock is unreliable (AppKit
* key-window; no focus events over ssh), so start armed like PD's MLOCK_ON --
* the mouse is locked by default in-stage (see reconcileGrab()). */
#ifdef PLATFORM_MACOS
static int captureArmed = 1; /* D594: PD MLOCK_ON default (see reconcileGrab) */
#else
static int captureArmed = 0; /* user has clicked to lock (capture mode) */
#endif
static int windowFocused = 1;
static int mouseAimSpeed = 16; /* aim-mode sensitivity, percent (B3: 50 -> 25 M-29 -> 16; still overshot at 25) */
static int gepdSens = 38; /* D194 Input.GepdSens: GEPD SENSITIVITY setting, range 1..500
@@ -861,7 +939,6 @@ static int sensLink = 1; /* F10 overlay: keep aim/turn sens at the
static int mouseTurnSpeed = 50; /* hipfire yaw sensitivity, percent
(40 = M-123 match point at GepdSens=30; 50 = the user's
"~20-35% faster" default request, same +25% as gepdSens) */
static int menuPointerSpeed = 100; /* front-end cursor speed, percent */
static int mouseInvertY = 0; /* 1 = mouse-down looks up */
static int mouseYScale = 100; /* extra vertical (pitch) sensitivity, % */
static int mouseSmoothing = 0; /* 0 = raw; 1..90 = low-pass strength (%) */
@@ -1042,7 +1119,7 @@ static int padLookSensY = 100; /* right-stick look: vertical (pit
static int padSouthpaw = 0; /* 1 = swap fire (G) / grenade (R) trigger actions */
static int padLookSmooth = 0; /* right-stick look low-pass strength, 0-10 (0 = off, 10 = max) */
static int padTriggerPct = 25; /* trigger press point, % of travel (~30*256) */
static int padLookInvertY = 0; /* 1 = invert right-stick (look) Y */
/* padLookInvertY: declared with the D513 accessors above (D645). */
static int inputDefaultsRev = 0; /* D546: 1 once the deadzone default migration ran */
/* Per-pad look-smoothing EMA state (Wave A item 5), one X/Y pair per pad. Reset
* on hot-unplug (inputRescanPads) so a smoothed value can't stick after the
@@ -1078,7 +1155,6 @@ static int wheelFwd = 0;
static int wheelBack = 0;
/* Item 1 (D165) — front-end pointer P-controller state. */
static int menuPointerMode = 1; /* 0 = legacy velocity, 1 = 1:1 pointer */
static int hipfirePitchSpeed = 100; /* D166: hipfire pitch pulse rate, percent */
static int menuPrevActive = 0;
static double hipPitchPhase = 0.0; /* D166: hipfire pitch pulse phase 0..1 */
@@ -1250,6 +1326,8 @@ static void inputOpenPads(void)
* SUSTAINED: the stick stays deflected until a later entry changes it; SNONE
* re-centres it. CHOLD/CREL hold/release GEPD crouch (D377);
* UHOLD/UREL and RELOADHOLD/RELOADREL exercise dedicated use/reload (D378).
* MLB<n>/MRB<n> (D597) press/release the sustained mouse buttons (n=1 press,
* 0 release; slot 0 only): headless fire for the D595-class regression.
* "Frame" = count of controller-0 reads since launch (roughly
* 2 per rendered frame -- watch GE_INPUTLOG to calibrate). Unset env => no
* effect; when set it is the ONLY controller-0 input source.
@@ -1279,7 +1357,7 @@ static int scriptMsMode(void)
}
struct scriptEntry { long frame; unsigned mask; int sx, sy; int hasStick;
int hasMouse, mdx, mdy; int hasHold, hold; /* D337 mouse/aim tokens */
int hasMouse, mdx, mdy, mlb, mrb; int hasHold, hold; /* D337/D597 mouse 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 */
@@ -1292,7 +1370,7 @@ struct scriptSlot {
int count; /* -1 = not parsed yet, 0 = parsed empty */
long frame; /* controller reads for this slot since launch */
int curSX, curSY; /* stick set by the last scriptApply() for this slot */
int mouseOn, mdx, mdy; /* sustained scripted mouse (slot 0 only) */
int mouseOn, mdx, mdy, mlb, mrb; /* 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 */
@@ -1352,6 +1430,29 @@ static void scriptApplyToken(struct scriptEntry *e, const char *s, int n,
if (s[2] == 'X' || s[2] == 'x') e->mdx = atoi(num); else e->mdy = atoi(num);
return;
}
/* D597: sustained scripted mouse buttons: MLB<n> MRB<n> (n=1 press,
* 0 release; latest entry wins), slot 0 only. Headless fire for the
* D595-class regression: aim with MDX, MLB1 on an AI -> struck animation
* with no SIGSEGV. A headless window has no real mouse state, so the
* bit ORs into the polled mask (see the D337 block in
* inputComputePadSlot); the game's mouse-button bindings (LMB=IA_FIRE)
* then drive it like a real press. */
if (n > 3 && (SDL_strncasecmp("MLB", s, 3) == 0 || SDL_strncasecmp("MRB", s, 3) == 0)) {
if (slot > 0) { /* no mouse path on slots 1-3: ignore, warn once */
struct scriptSlot *st = &scriptSlots[slot];
if (!st->ignoredWarned) {
st->ignoredWarned = 1;
sysLogPrintf(LOG_WARNING, "%s: mouse tokens are ignored for slots 1-3 ('%.*s')",
scriptTag(slot), n, s);
}
return;
}
char num[16]; int k = n - 3; if (k > 15) k = 15;
memcpy(num, s + 3, k); num[k] = 0;
e->hasMouse = 1;
if (s[1] == 'L') e->mlb = atoi(num) != 0; else e->mrb = atoi(num) != 0; /* MLB/MRB: the L/R is index 1 */
return;
}
if (n == 5 && SDL_strncasecmp("RHOLD", s, 5) == 0) { e->hasHold = 1; e->hold = 1; return; }
if (n == 5 && SDL_strncasecmp("LHOLD", s, 5) == 0) { e->hasHold = 1; e->hold = 2; return; }
if (n == 4 && SDL_strncasecmp("RREL", s, 4) == 0) { e->hasHold = 1; e->hold = 0; return; }
@@ -1417,7 +1518,7 @@ static void scriptParse(int slot)
e->mask = 0;
e->sx = e->sy = 0;
e->hasStick = 0;
e->hasMouse = e->mdx = e->mdy = 0;
e->hasMouse = e->mdx = e->mdy = e->mlb = e->mrb = 0;
e->hasHold = e->hold = 0;
e->hasZHold = e->zhold = 0;
e->hasCHold = 0; e->chold = 0;
@@ -1466,6 +1567,8 @@ static void scriptRefreshHolds(int slot)
st->mouseOn = 1;
st->mdx = st->entries[i].mdx;
st->mdy = st->entries[i].mdy;
st->mlb = st->entries[i].mlb; /* D597 */
st->mrb = st->entries[i].mrb;
}
if (st->entries[i].hasHold && st->entries[i].frame > bestH) {
bestH = st->entries[i].frame;
@@ -1494,6 +1597,75 @@ static void scriptRefreshHolds(int slot)
}
}
/* Input record/replay (test harness, port-only). GE_INPUTRECORD=<file>
* appends controller 0's final button mask + stick per controller read;
* GE_INPUTREPLAY=<file> feeds the same stream back read for read (then
* neutral). Reads are tick-driven, so with -level_XX and a pinned GE_RSEED a
* replay re-plays a human run (tools_pc/levelbench.sh --replay). Pad only:
* mouse aim is not captured; D404 centred right-stick look is (fields 4-6). */
static int s_replayActive; /* GE_INPUTREPLAY stream open */
static int s_recPadDX, s_recPadDY, s_recPadDirect; /* this poll's D404 pad-direct look */
static int padDirectCompute(int dx, int dy);
static FILE *s_rec, *s_rep;
static int s_rrInit, s_repFetched;
static struct { unsigned b; int x, y, pdx, pdy, pd, use, reload; } s_repCur;
static int s_recUse, s_recReload; /* this poll's dedicated use/reload (D378) */
static void inputRecordReplayInit(void)
{
if (s_rrInit) return;
s_rrInit = 1;
const char *r = getenv("GE_INPUTRECORD");
const char *p = getenv("GE_INPUTREPLAY");
if (p && *p) {
s_rep = fopen(p, "r");
s_replayActive = s_rep != NULL;
sysLogPrintf(LOG_NOTE, "GE_INPUTREPLAY %s: %s", p, s_rep ? "playing" : "cannot open");
}
if (r && *r) {
s_rec = fopen(r, "w");
if (s_rec) setvbuf(s_rec, NULL, _IOLBF, 0); /* survive a crash/kill */
sysLogPrintf(LOG_NOTE, "GE_INPUTRECORD %s: %s", r, s_rec ? "recording" : "cannot open");
}
}
/* Read this poll's replay line (once per controller-0 poll). */
static void replayFetch(void)
{
inputRecordReplayInit();
if (!s_rep || s_repFetched) return;
s_repFetched = 1;
char line[128];
memset(&s_repCur, 0, sizeof s_repCur);
if (!fgets(line, sizeof line, s_rep) ||
sscanf(line, "%x %d %d %d %d %d %d %d", &s_repCur.b, &s_repCur.x, &s_repCur.y,
&s_repCur.pdx, &s_repCur.pdy, &s_repCur.pd, &s_repCur.use, &s_repCur.reload) < 3) {
sysLogPrintf(LOG_NOTE, "GE_INPUTREPLAY: end of stream");
fclose(s_rep);
s_rep = NULL;
s_replayActive = 0;
memset(&s_repCur, 0, sizeof s_repCur);
}
}
static void inputRecordReplay(unsigned *button, int *sx, int *sy)
{
replayFetch();
if (s_replayActive || s_repFetched) {
*button = s_repCur.b; *sx = s_repCur.x; *sy = s_repCur.y;
/* D404 centred pad aim turns the camera directly, not via the
* stick channel: re-apply the recorded deflection, same poll. */
if (s_repCur.pd) padDirectCompute(s_repCur.pdx, s_repCur.pdy);
}
s_repFetched = 0;
if (s_rec) {
fprintf(s_rec, "%x %d %d %d %d %d %d %d\n", *button, *sx, *sy,
s_recPadDX, s_recPadDY, s_recPadDirect, s_recUse, s_recReload);
}
s_recPadDX = s_recPadDY = s_recPadDirect = s_recUse = s_recReload = 0;
}
static unsigned scriptApply(int slot, unsigned button)
{
struct scriptSlot *st = &scriptSlots[slot];
@@ -1661,6 +1833,27 @@ void inputUpdate(void)
mouseDY += dy;
}
/* D643 Input.N64StickFeel=1 (Original): the stick as a bare N64 pot. Linear
* 32767 -> STICK_MAX (80, the N64 range), a small centre deadzone and nothing
* else (no deadzone rescale, look sensitivity or smoothing). The ~7.6% centre
* (2500/32767) is an assumption of typical N64 stick dead-centre jitter --
* confirm against 1964 / the N64 manual. */
#define N64_STICK_DZ 2500
static int scaleAxis(int v, int dz);
static int scaleAxisN64(int v)
{
if (v > -N64_STICK_DZ && v < N64_STICK_DZ) return 0;
int out = (int)((long)v * STICK_MAX / 32767);
if (out > STICK_MAX) out = STICK_MAX;
if (out < -STICK_MAX) out = -STICK_MAX;
return out;
}
/* Modern/legacy deadzone scaling, or the accurate N64 response in Original. */
static int padStickAxis(int v, int dz)
{
return (controlScheme && n64StickFeel) ? scaleAxisN64(v) : scaleAxis(v, dz);
}
static int scaleAxis(int v, int dz)
{
if (dz < 0) dz = 0;
@@ -2038,9 +2231,18 @@ static void inputBindingProbe(void)
static int actHeld(const Uint8 *ks, int act)
{
/* Front-end mouse clicks stay fixed UI controls. Click-to-lock while
* playing cannot fire a newly rebound button until the grab succeeds. */
Uint32 mb = mouseEnabled && mouseGrabbed &&
current_menu == GE_MENU_RUN_STAGE && !optionsOverlayIsOpen()
* playing cannot fire a newly rebound button until the grab succeeds.
* D594: the in-stage test must match the file-wide `menuMode` semantics
* (RUN_STAGE *or* INVALID). A forced-level start (-level_NN) leaves
* current_menu at MENU_INVALID, so the old strict ==RUN_STAGE check
* silently disabled every mouse-driven action (fire, aim) in that path --
* the reason mouse look worked but the mouse button never did. PD has no
* such gate; a real front-end menu is a specific MENU_ id (menuMode 1),
* so it still stays a UI control. */
int inStage = (current_menu == GE_MENU_RUN_STAGE ||
current_menu == GE_MENU_INVALID);
Uint32 mb = mouseEnabled && mouseGrabbed && inStage &&
!optionsOverlayIsOpen()
? SDL_GetMouseState(NULL, NULL) : 0;
for (int k = 0; k < BIND_MAX_KEYS; k++) {
int code = g_bind[act][k];
@@ -2245,6 +2447,10 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
mouseDX = (double)scriptSlots[0].mdx;
mouseDY = (double)scriptSlots[0].mdy;
mouseGrabbed = 1;
/* D597: scripted sustained mouse buttons (MLB/MRB) OR into the
* polled mask; a headless window has no real mouse state. */
if (scriptSlots[0].mlb) mb |= SDL_BUTTON(1);
if (scriptSlots[0].mrb) mb |= SDL_BUTTON(2);
}
}
/* D196: the F10 options overlay forces the OS cursor visible via
@@ -2368,11 +2574,14 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
* combined edges prevent a held input from repeatedly using a door.
* In menus/watch/tank the pad keeps native accept/cancel buttons. */
SDL_GameController *slot0Pad = slotPad(0);
u32 slot0Held = slot0Pad ? padActHeld(&padTabGet()->seat[padSeatForSlot(0)], padReadRaw(slot0Pad)) : 0;
u32 slot0Held = (slot0Pad && !controlScheme) ? padActHeld(&padTabGet()->seat[padSeatForSlot(0)], padReadRaw(slot0Pad)) : 0;
int padUse = (slot0Held & (1u << PA_USE)) != 0; /* D469: bound actions (Jinx 1.1: A / X) */
int padReload = (slot0Held & (1u << PA_RELOAD)) != 0;
int useNow = scriptIsActive(0) ? scriptSlots[0].use : (actHeld(ks, IA_CANCEL) || padUse);
int reloadNow = scriptIsActive(0) ? scriptSlots[0].reload : (actHeld(ks, IA_RELOAD) || padReload);
replayFetch();
if (s_replayActive) { useNow = s_repCur.use; reloadNow = s_repCur.reload; }
s_recUse = useNow; s_recReload = reloadNow;
int playable = inputCanUseGameplayActions(menuMode);
/* D407: on the N64 the B bit also drives bondview2.c's tank
* handlers (board when g_BondCanEnterTank, exit while
@@ -2490,14 +2699,7 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
lookDtScale, edx, dyLook);
}
if (menuMode && !menuPointerMode) {
/* Legacy velocity mode (Input.MenuPointerMode = 0): mouse
* velocity -> stick. Kept as a fallback; integrates as
* velocity^2 through front.c (D165). */
double g = MENU_POINTER_GAIN * (menuPointerSpeed / 100.0);
sx += (int)(edx * g);
sy -= (int)(dyLook * g); /* front-end cursor: +sy = up */
} else if (menuMode) {
if (menuMode) {
/* Front-end menu pointer.
*
* front.c frontUpdateControlStickPosition() is the game's cursor
@@ -2771,11 +2973,11 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
if (!padMenuMode && stickSwap(idx)) { int tx = lx, ty = ly; lx = rx; ly = ry; rx = tx; ry = ty; } /* D518 */
if (padMenuMode) {
int px = scaleAxis(lx, padDeadzoneL);
int py = -scaleAxis(ly, padDeadzoneL); /* SDL up = negative -> N64 up = positive */
int px = padStickAxis(lx, padDeadzoneL); /* D643: accurate N64 response in Original */
int py = -padStickAxis(ly, padDeadzoneL); /* SDL up = negative -> N64 up = positive */
if (px) sx = px;
if (py) sy = py;
} else if (styleIsSolitare(idx)) { /* D498, D513, D518 */
} else if (!controlScheme && styleIsSolitare(idx)) { /* D498, D513, D518; D643: Original never swaps roles (left stick = analog stick, right = C) */
/* 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
@@ -2805,7 +3007,10 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
rys = (int)lround(padSmSY[idx]);
}
if (padLookInvertY) rys = -rys;
if (aimModeGet() == AIMMODE_CENTRED && padDirectCompute(rxs, rys)) {
if (idx == 0) { s_recPadDX = rxs; s_recPadDY = rys; s_recPadDirect = 0; }
if (aimModeGet() == AIMMODE_CENTRED && !(idx == 0 && s_replayActive) &&
padDirectCompute(rxs, rys)) {
if (idx == 0) s_recPadDirect = 1;
/* D404 (resolved by implementation, 2026-09-28): pad CENTRED
* aim -- the stick drives the camera directly (crosshair
* pinned at centre), the pad-side twin of the mouse CENTRED
@@ -2822,13 +3027,23 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
if (rys) sy = rys;
}
} else {
int px = scaleAxis(lx, padDeadzoneL);
int py = -scaleAxis(ly, padDeadzoneL); /* SDL up = negative -> N64 up = positive */
int px = padStickAxis(lx, padDeadzoneL); /* D643: accurate N64 response in Original */
int py = -padStickAxis(ly, padDeadzoneL); /* SDL up = negative -> N64 up = positive */
/* #145: in the HONEY role the LEFT stick is the look stick (1.1/
* 1.3 -- and, after the D498/D518 physical swap for 1.3 Frost,
* this channel carries the physical right stick, the 1.3 look
* stick). Inverting it, not the right stick (which is the digital
* movement C-button source in this role), is what makes F10's
* "Invert look" affect in-game vertical aim under the game's own
* original styles. The SOLITARE branch above already inverts its
* (look) right-stick channel. */
/* D645: not in Original -- there the game's own "Look up/down"
* (mirrored from this row by watchsettings.c) inverts natively. */
if (padLookInvertY && !controlScheme) py = -py;
if (px) sx = px;
if (py) sy = py;
if (idx == 0 && (px || py)) s_aimDevMouse = 0; /* D337: pad aim active */
if (padLookInvertY) ry = -ry;
if (rx > RSTICK_THRESHOLD) button |= GE_CONT_F;
if (rx < -RSTICK_THRESHOLD) button |= GE_CONT_C;
if (ry > RSTICK_THRESHOLD) button |= GE_CONT_D;
@@ -2846,6 +3061,7 @@ 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.orig = controlScheme; /* D643 */
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)];
@@ -2964,6 +3180,10 @@ static unsigned inputComputePadSlot(int idx, signed char *stick_x, signed char *
if (sy > STICK_MAX) sy = STICK_MAX;
if (sy < -STICK_MAX) sy = -STICK_MAX;
if (idx == 0) {
inputRecordReplay(&button, &sx, &sy);
}
if (stick_x) *stick_x = (signed char)sx;
if (stick_y) *stick_y = (signed char)sy;
@@ -3013,7 +3233,17 @@ static void applyGrab(int want)
* per controller-0 poll (menuMode known there) and from the notify hooks. */
static void reconcileGrab(int menuMode)
{
#ifdef PLATFORM_MACOS
/* D594: the FOCUS_GAINED-driven `windowFocused` gate is unreliable on
* macOS (AppKit key-window; no focus events over ssh), so a click-to-lock
* that waits on it never arms and the mouse button + look stay dead. PD
* keeps the mouse locked by default (MLOCK_ON); match it -- drop only the
* focus gate. Menus and aim-holds still free the cursor (menuMode /
* s_absAimSuspend, exactly as on every other platform). */
int want = captureArmed && !menuMode;
#else
int want = captureArmed && windowFocused && !menuMode; /* click-to-lock */
#endif
if (s_absAimSuspend) {
want = 0; /* D194: an aim hold wants the free cursor for absolute aim */
}
@@ -3878,6 +4108,64 @@ static void padMapSelfTest(void)
sysLogPrintf(LOG_INFO, "GE_PADMAPTEST: binding API round trip (%d) clear keeps slot (%d) [%s]", c10, c11, padStr[1][PA_FIRE]);
fail += !(c10 && c11);
/* D643: Original = strict virtual N64 pad. Per-source table (identical in
* stage / menu / tank), then remap, per-seat independence, guards. */
{
static const unsigned exp[PS_COUNT] = {
[PS_A] = GE_CONT_A, [PS_B] = 0, [PS_X] = GE_CONT_B, [PS_Y] = 0,
[PS_LB] = GE_CONT_L, [PS_RB] = GE_CONT_R, [PS_LS] = 0, [PS_RS] = 0,
[PS_START] = GE_CONT_START, [PS_DUP] = GE_CONT_UP, [PS_DDOWN] = GE_CONT_DOWN,
[PS_DLEFT] = GE_CONT_LEFT, [PS_DRIGHT] = GE_CONT_RIGHT,
[PS_LT] = GE_CONT_R, [PS_RT] = GE_CONT_G };
char savedN[MAX_PADS][NB_COUNT][32];
int savedCs = controlScheme, savedSouth = padSouthpaw, o1 = 1, o2 = 1, o3 = 1;
memcpy(savedN, n64Str, sizeof(savedN));
controlScheme = 1;
padSouthpaw = 1; /* must be ignored in Original */
for (int s2 = 0; s2 < MAX_PADS; s2++) {
padPreset[s2] = 0;
for (int n = 0; n < NB_COUNT; n++) snprintf(n64Str[s2][n], sizeof(n64Str[0][0]), "%s", kN64Btn[n].def);
}
padTabValid = 0;
const PadTable *ot = padTabGet();
for (int src = 0; src < PS_COUNT; src++)
for (int cx = 0; cx < 3; cx++) { /* playable / menu / tank */
PadMapCtx c = { cx == 0, 1, 1, cx == 2, 1 };
padMapPure(&ot->seat[0], PSM(src), 0, 0, &c, &o);
int ok = o.button == exp[src] && !o.crouch && !o.gadgetEdge &&
padActHeld(&ot->seat[0], PSM(src)) == 0;
o1 &= ok;
sysLogPrintf(ok ? LOG_INFO : LOG_ERROR, "GE_PADMAPTEST: original %-13s %-8s -> %04x (want %04x)",
kPadSrc[src].name, cx == 0 ? "playable" : cx == 1 ? "menu" : "tank", o.button, exp[src]);
}
{ PadMapCtx c = { 1, 0, 0, 0, 1 }; padMapPure(&ot->seat[0], PSM(PS_RT) | PSM(PS_A), 0, 0, &c, &o); o1 &= o.button == (GE_CONT_G | GE_CONT_A); }
/* remap: seat 1 B := y,b and CUp := b; seat 0 untouched */
snprintf(n64Str[1][NB_B], sizeof(n64Str[0][0]), "y,b");
snprintf(n64Str[1][NB_CU], sizeof(n64Str[0][0]), "rightstick");
/* guards: seat 2 Start none; seat 3 A and B both none */
snprintf(n64Str[2][NB_START], sizeof(n64Str[0][0]), "none");
snprintf(n64Str[3][NB_A], sizeof(n64Str[0][0]), "none");
snprintf(n64Str[3][NB_B], sizeof(n64Str[0][0]), "none");
ot = padTabGet();
PadMapCtx oc = { 1, 0, 0, 0, 1 };
padMapPure(&ot->seat[1], PSM(PS_Y), 0, 0, &oc, &o); o2 &= o.button == GE_CONT_B;
padMapPure(&ot->seat[1], PSM(PS_B), 0, 0, &oc, &o); o2 &= o.button == GE_CONT_B;
padMapPure(&ot->seat[1], PSM(PS_X), 0, 0, &oc, &o); o2 &= o.button == 0;
padMapPure(&ot->seat[1], PSM(PS_RS), 0, 0, &oc, &o); o2 &= o.button == GE_CONT_E;
padMapPure(&ot->seat[0], PSM(PS_X), 0, 0, &oc, &o); o2 &= o.button == GE_CONT_B;
padMapPure(&ot->seat[0], PSM(PS_Y), 0, 0, &oc, &o); o2 &= o.button == 0;
padMapPure(&ot->seat[2], PSM(PS_START), 0, 0, &oc, &o); o3 &= o.button == GE_CONT_START;
padMapPure(&ot->seat[3], PSM(PS_A), 0, 0, &oc, &o); o3 &= o.button == GE_CONT_A;
padMapPure(&ot->seat[3], PSM(PS_X), 0, 0, &oc, &o); o3 &= o.button == GE_CONT_B;
int st = scaleAxisN64(2499) == 0 && scaleAxisN64(32767) == STICK_MAX && scaleAxisN64(-32768) == -STICK_MAX &&
scaleAxisN64(16384) == 40 && padStickAxis(5000, 7500) == scaleAxisN64(5000);
sysLogPrintf(LOG_INFO, "GE_PADMAPTEST: original table (%d) remap/seat-independence (%d) unbound-fallback (%d) stick (%d)", o1, o2, o3, st);
fail += !(o1 && o2 && o3 && st);
controlScheme = savedCs;
padSouthpaw = savedSouth;
memcpy(n64Str, savedN, sizeof(savedN));
}
memcpy(padStr, saved, sizeof(saved));
memcpy(padPreset, savedPreset, sizeof(savedPreset));
padTabValid = 0;
@@ -3899,6 +4187,15 @@ PD_CONSTRUCTOR static void inputConfigInit(void)
snprintf(padStr[seat][a], sizeof(padStr[seat][a]), "%s", kPadAct[a].def);
configRegisterString(padStrKey[seat][a], padStr[seat][a], sizeof(padStr[seat][a]));
}
/* D643 (#145/#149): Original-scheme N64 button map, per seat (seat 1 = Input.N64Pad.<B>, seats 2-4 = Input.N64Pad2/3/4.<B>). */
for (int seat = 0; seat < MAX_PADS; seat++)
for (int n = 0; n < NB_COUNT; n++) {
if (seat == 0) snprintf(n64StrKey[seat][n], sizeof(n64StrKey[seat][n]), "Input.N64Pad.%s", kN64Btn[n].key);
else snprintf(n64StrKey[seat][n], sizeof(n64StrKey[seat][n]), "Input.N64Pad%d.%s", seat + 1, kN64Btn[n].key);
snprintf(n64Str[seat][n], sizeof(n64Str[seat][n]), "%s", kN64Btn[n].def);
configRegisterString(n64StrKey[seat][n], n64Str[seat][n], sizeof(n64Str[seat][n]));
}
configRegisterInt("Input.N64StickFeel", &n64StickFeel, 0, 1);
configRegisterInt("Input.MPMode", &mpInputMode, 0, 2); /* D416: 0 Auto, 1 PadsOnly, 2 KbmP1 */
configRegisterInt("Input.MouseEnabled", &mouseEnabled, 0, 1);
configRegisterInt("Input.CrosshairCursor", &cfgCrosshairCursor, 0, 1); /* D555 */
@@ -3929,8 +4226,6 @@ PD_CONSTRUCTOR static void inputConfigInit(void)
configRegisterInt("Input.TankAimScale", &tankAimScale, 10, 300); /* D407 */
configRegisterInt("Input.MouseTurnSpeed", &mouseTurnSpeed, 1, 500);
configRegisterInt("Input.SensLink", &sensLink, 0, 1);
configRegisterInt("Input.MenuPointerSpeed", &menuPointerSpeed, 10, 500);
configRegisterInt("Input.MenuPointerMode", &menuPointerMode, 0, 1);
configRegisterInt("Input.HipfirePitchSpeed", &hipfirePitchSpeed, 10, 500);
configRegisterInt("Input.MouseInvertY", &mouseInvertY, 0, 1);
configRegisterInt("Input.MouseYScale", &mouseYScale, 1, 500);
File diff suppressed because it is too large. Load diff
+729 -13
View File
@@ -27,6 +27,7 @@
#include <string.h>
#include <stdint.h>
#include <time.h>
#include <math.h> /* D583: floor/fabs in the present grid */
#include <SDL.h>
@@ -44,6 +45,7 @@
#include "platform.h"
#include "system.h"
#include "portaddr.h"
#include "crash.h" /* D38: crashDumpThreads() */
#include "video.h"
#include "audio.h"
@@ -59,6 +61,13 @@
/* fast3d (C++): the software RSP entry point. */
extern void gfx_run(Gfx *commands);
/* D578: frame interpolation (port/fast3d/gfx_pc.cpp). */
extern void gfx_interp_reset(void);
extern int gfx_interp_last_exact(void);
extern int gfx_interp_body_runs(void);
extern void gfx_interp_trigger_counts(unsigned* turn, unsigned* clamp, unsigned* set, unsigned* room);
extern int gfx_interp_tick(Gfx *commands, const float *alphas, int n, int base);
extern void gfx_tick_counts(unsigned *batches, unsigned *tris, unsigned *texloads, unsigned *texmiss, unsigned *dynbytes, unsigned *texus); /* D583 SLOW line */
extern void videoSyncSplitScreen(void); /* D416 */
/* ------------------------------------------------------------------------ */
@@ -200,8 +209,31 @@ static OSMesg g_viRetraceMsg = 0;
* frame rate (the N64 VI interrupt fires once per frame; GE calls
* osViSetEvent with NUM_FIELDS=1, i.e. "every retrace"). */
static uint64_t g_nextTickUs = 0;
/* D578: scheduled time of the latest posted retrace (the sim timeline the
* render worker blends against; regular, unlike the posting time). */
static uint64_t g_lastViUs = 0;
static uint32_t g_tickIntervalUs = 1000000 / 60; /* NTSC frame rate */
/* D600: display-locked retrace. The pacemaker below is a free-running wall
* clock (16666 us: 1e6/60 truncated, ~40 ppm fast) while the swap blocks on
* the display's vblank, so with VSync on the tick's phase slides against the
* vblank (~7 min beat on a 60.000 Hz panel). Once the tick lands inside the
* render+swap window every frame is late and boss.c's tick gate skips
* (22-33 ms frames) until the phase slides back out; every launch starts at a
* random phase, so a restart "re-rolls" it. Fix: a first-order phase lock.
* After each swap return (= a vblank) the render side nudges the NEXT tick so
* it fires D600_TARGET_US after the vblank, leaving the rest of the period
* for game logic + render. Only engaged when VSync is on and consecutive swap
* returns are one game frame apart (a 60 Hz-class display); otherwise the
* pacemaker free-runs exactly as before. GE_NOTICKLOCK=1 disables it (A/B). */
#define D600_TARGET_US 1500
#define D600_MAX_NUDGE_US 100
static int64_t g_tickNudgeUs = 0; /* written by the render side, consumed by the pacemaker */
static uint64_t g_vbLastUs = 0;
static int g_tickLockMode = 0; /* 0 undecided, 1 on, -1 off (env / runaway guard) */
static int g_tickLockSame = 0; /* consecutive saturated nudges, runaway guard */
static int g_tickLockLogN = 0;
static PortThread *portFind(OSThread *t)
{
for (int i = 0; i < PORT_MAX_THREADS; ++i) {
@@ -313,7 +345,8 @@ static void *portTickThread(void *arg)
now = sysGetMicroseconds();
if (now >= g_nextTickUs) {
g_nextTickUs += g_tickIntervalUs;
__atomic_store_n(&g_lastViUs, g_nextTickUs, __ATOMIC_RELEASE); /* D578 */
g_nextTickUs += (int64_t)g_tickIntervalUs + __atomic_exchange_n(&g_tickNudgeUs, 0, __ATOMIC_RELAXED); /* D600 */
if (g_nextTickUs <= now) g_nextTickUs = now + g_tickIntervalUs;
portPostVIEvent();
}
@@ -423,13 +456,40 @@ void osCreateThread(OSThread *t, OSId id, void (*entry)(void *), void *arg,
#if !defined(_WIN32)
#include <sys/mman.h>
/* The decomp aligns/compares stack-buffer pointers by truncating to u32
* (e.g. `(u32)compbuffer` in image.c texLoad) — an N64 assumption. glibc
* puts pthread stacks above 4 GiB, so those truncations corrupt. Force each
* game-thread stack into the low 2 GiB with MAP_32BIT so every such idiom
* works exactly as on the console. Windows pthread stacks are already low. */
* (e.g. `(u32)compbuffer` in image.c texLoad) — an N64 assumption. Windows
* pthread stacks are already low, so every such idiom works there.
*
* On macOS the whole N64 window is reserved PROT_NONE by portAddrInit, so each
* game-thread stack is carved from the window's stack region
* [PORT_ADDR_BASE + 0xA0000000, + 0xC0000000), leaving a PROT_NONE guard page
* below it; the stack's host address then truncates to its N64 address exactly
* as on Windows.
*
* On Linux (PORT_ADDR_BASE == 0) the window is not reserved, so keep the
* original MAP_32BIT allocation: addresses below 2 GiB truncate identically. */
#define PORT_STACK_REGION_OFF 0xA0000000ULL
#define PORT_STACK_REGION_SIZE (512ULL << 20)
#define PORT_STACK_GUARD 0x4000ULL
static uintptr_t s_stackNext = 0;
static void *portAllocLowStack(size_t sz)
{
#if defined(MAP_32BIT)
#if PORT_ADDR_BASE != 0
const uintptr_t region = (uintptr_t)PORT_ADDR_BASE + PORT_STACK_REGION_OFF;
if (s_stackNext == 0)
s_stackNext = region;
const uintptr_t base = s_stackNext + PORT_STACK_GUARD;
const uintptr_t end = base + sz;
if (end > region + PORT_STACK_REGION_SIZE)
return NULL;
void *p = mmap((void *)base, sz, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
if (p != (void *)base)
return NULL;
s_stackNext = end;
return p;
#elif defined(MAP_32BIT)
void *p = mmap(NULL, sz, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT
#if defined(MAP_STACK)
@@ -438,8 +498,11 @@ static void *portAllocLowStack(size_t sz)
, -1, 0);
if (p != MAP_FAILED)
return p;
#endif
return NULL;
#else
(void)sz;
return NULL;
#endif
}
#endif
@@ -586,8 +649,11 @@ void osEnqueueMesg(OSMesgQueue *mq, OSMesg msg)
pthread_mutex_unlock(&pq->lock);
}
extern void interpPortalOnSend(void *mq, void *msg); /* D578: port/src/interpportal.c */
s32 osSendMesg(OSMesgQueue *mq, OSMesg msg, s32 flag)
{
interpPortalOnSend(mq, msg); /* D578: game-thread snapshot for the portal-scissor replay */
PortQueue *pq = portQueueGet(mq);
pthread_mutex_lock(&pq->lock);
while (mq->validCount >= mq->msgCount) {
@@ -938,7 +1004,7 @@ static void piServiceDma(s32 direction, u32 srcPA, void *dstVA, u32 size)
sysFatalError("D60: ROM-read target %p + 0x%X not host-mapped "
"(src=0x%08X)", dstVA, size, srcPA);
}
memcpy(dstVA, (const void *)(uintptr_t)srcPA, size);
memcpy(dstVA, portN64ToHost(srcPA), size);
} else {
/* OS_WRITE: the game never writes the cart (saves go to EEPROM via
* osEeprom*, shimmed separately). Log and drop. */
@@ -1247,6 +1313,47 @@ s32 osMotorStop(OSPfs *pfs)
void osSpTaskLoad(OSTask *t) { (void)t; /* nothing to load on the host */ }
/* D600: called by the render side right after videoEndFrame() returns, i.e.
* just after the blocking swap returned at a vblank. */
static void portNoteSwapReturn(void)
{
if (g_tickLockMode == 0) {
g_tickLockMode = (g_determEnabled || getenv("GE_NOTICKLOCK") != NULL) ? -1 : 1;
if (g_tickLockMode < 0) sysLogPrintf(LOG_NOTE, "D600: display-locked tick disabled");
}
if (g_tickLockMode < 0) return;
uint64_t t = sysGetMicroseconds();
uint64_t last = g_vbLastUs;
g_vbLastUs = t;
if (!videoVSyncOn() || last == 0) return;
const int64_t P = (int64_t)g_tickIntervalUs;
const int64_t gap = (int64_t)(t - last);
if (gap < P * 9 / 10 || gap > P * 11 / 10) return; /* not one vblank per game frame: leave it free-running */
int64_t next = (int64_t)__atomic_load_n(&g_nextTickUs, __ATOMIC_RELAXED);
int64_t phase = (next - (int64_t)t) % P;
if (phase < 0) phase += P;
int64_t err = phase - D600_TARGET_US; /* >0: tick is later than target */
if (err >= P / 2) err -= P; else if (err < -P / 2) err += P;
int64_t nudge = -err / 8;
if (nudge > D600_MAX_NUDGE_US) nudge = D600_MAX_NUDGE_US;
if (nudge < -D600_MAX_NUDGE_US) nudge = -D600_MAX_NUDGE_US;
/* Runaway guard (a driver that ignores vsync makes the swap return track
* the render, and the loop would chase it): a lock that stays saturated
* far longer than a full-period slew needs is not converging. */
if (nudge == D600_MAX_NUDGE_US || nudge == -D600_MAX_NUDGE_US) {
if (++g_tickLockSame > 400) {
g_tickLockMode = -1;
sysLogPrintf(LOG_WARNING, "D600: display-locked tick not converging; disabled (swap may not block on vblank)");
return;
}
} else {
g_tickLockSame = 0;
}
__atomic_store_n(&g_tickNudgeUs, nudge, __ATOMIC_RELAXED);
if ((++g_tickLockLogN % 300) == 1)
sysLogPrintf(LOG_NOTE, "D600: tick lock phase=%lld us err=%lld us nudge=%lld us", (long long)phase, (long long)err, (long long)nudge);
}
static void portRenderGfxTask(Gfx *dl)
{
uint64_t t0 = sysGetMicroseconds();
@@ -1254,6 +1361,7 @@ static void portRenderGfxTask(Gfx *dl)
videoSyncSplitScreen();
gfx_run(dl);
videoEndFrame();
portNoteSwapReturn(); /* D600 */
g_lastFrameUs = sysGetMicroseconds();
if (++g_framesRendered <= 5 || (g_framesRendered % 300) == 0)
sysLogPrintf(LOG_NOTE, "frame %d rendered in %llu us",
@@ -1277,12 +1385,549 @@ static void portRenderGfxTask(Gfx *dl)
static pthread_mutex_t s_rwLock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t s_rwCond = PTHREAD_COND_INITIALIZER;
static Gfx *s_rwTask = NULL; /* submitted display list; NULL = worker idle */
static uint64_t s_rwTaskViUs; /* D578: g_lastViUs when it was submitted */
/* D583: when the game submitted the task, and when the worker last posted
* SP/DP done: split a late frame into the game's own work, time the game was
* still waiting for our previous frame, and the worker's pickup delay. */
static int64_t s_rwTaskSubmitUs, s_rwLastDoneUs;
static int s_rwMode = 0; /* 0 = undecided, 1 = worker, -1 = inline */
/* D578: frame interpolation pacing (worker thread only; fast3d does the
* drawing, see gfx_pc.cpp). A game frame is stamped with the scheduled time
* S of the retrace it was submitted after. A present at wall time t shows sim
* time t - L, i.e. the blend alpha = (t - L - S_prev) / (S - S_prev): every
* refresh between two game frames lands at its position on the sim timeline,
* whatever the present rate (90 on a Deck OLED, 120, 144). L follows the
* frame's last present, so that one shows the frame exactly (see below);
* about half a tick of extra latency at 120. All passes for a game frame are drawn when it arrives, before
* SP/DP done (the game reuses its buffers after that); the worker then swaps
* the stored slots on its present clock, waking early for a new frame. */
#define IP_SLOTS 8 /* = GFX_INTERP_SLOTS (port/fast3d/gfx_rendering_api.h) */
static int64_t s_ipPrevS, s_ipCurS;
static double s_ipDelayEma; /* submit time - S, us */
static double s_ipLag; /* present time - sim time shown, us */
static double s_ipPassEma; /* CPU cost of one pass, us */
static double s_ipDrawPk; /* D583: decaying peak of a whole frame's draw time, us */
static double s_ipSwapEma; /* D583: time a present takes (show + swap + finish), us */
static int s_ipWasOn;
/* D583: present slots are a ring. Slots [s_qHead, s_qHead + s_qLen) are
* stored and waiting, in due order; a new game frame's passes go after them,
* so a frame never drops the previous frame's unshown presents (that left
* ~8% of 120 Hz ticks with one present and a 16 ms hole: VRR flicker). */
static int64_t s_ipTimes[IP_SLOTS]; /* due time per ring slot */
static uint8_t s_ipIdx[IP_SLOTS]; /* position within its game frame */
static uint8_t s_ipWhy[IP_SLOTS]; /* D583 hold causes: 0 blended, 1 exact fallback, 2 alpha clamped to 0, 3 clamped to 1 (late/stale) */
static double s_ipSim[IP_SLOTS]; /* D583: sim time each slot shows (prevS + alpha * dS), us */
static int s_qHead, s_qLen;
/* D583: one continuous present grid. s_gNext = the next grid point no frame
* has taken yet; each game frame takes the next round(dS / period) points
* (2 per tick at 120 Hz), so presents are evenly spaced whatever the game
* frame's arrival jitter. Re-anchored only when a frame is ready after its
* first point (a stall) or the period changes. Rebuilding it every tick from
* the last swap return (under VRR, our own present time) wobbled it. */
static double s_gNext, s_gPeriod, s_gAcc;
static double s_gMinSlack; /* least spare time (grid point - ready) this window */
static int s_gWin; /* frames in the window */
/* Swap-driven pacing (VSync on): a blocking swap returns at a vblank, so the
* swap-return times ARE the display clock. s_vbLast = last return, s_vbP =
* measured refresh period (EMA of gaps near one period). */
static int64_t s_vbLast;
static double s_vbP;
static unsigned s_vbOut; /* consecutive off-period gaps */
static double s_vbOutGap[8];
/* 10 s stats window (worker thread only). */
static int64_t s_stStart;
static unsigned s_stFrames, s_stExactFrames, s_stPresents;
/* D578 VRR cadence: present-gap histogram (<7, 7-9.5, 9.5-12, 12-20, >20 ms),
* slots dropped unshown, ticks by slot count (1, 2, 3, 4+). */
static unsigned s_stGap[5], s_stDrop, s_stSlotsN[4], s_stAnchor, s_stPull;
/* D583 motion: sim-time step between consecutive presents, in grid periods:
* hold (< 0.25), short (< 0.75), ok (< 1.25), long (< 1.75), jump. Even
* present timing can still show uneven motion (a frame forced exact shows the
* same image twice: a hold, then a jump). */
static unsigned s_stMot[5];
static unsigned s_stHoldWhy[4]; /* D583: holds by the cause of the second present (s_ipWhy) */
/* D583: present cost (show + swap + finish): longest, and how many took over
* half a refresh (a swap that blocks holds the worker: the next game frame
* starts its draw late). */
static double s_stSwapMax;
static unsigned s_stSwapLong;
/* D583: game-side split (see s_rwTaskSubmitUs): game work after its retrace /
* our previous done, time it was blocked by our previous frame, our pickup. */
static double s_stGameSum, s_stGameMax, s_stBlockSum, s_stBlockMax, s_stPickSum, s_stPickMax;
static unsigned s_stGameLong, s_stBlocked;
static double s_stLastSim;
static int64_t s_stLastPres;
static int portInterpSwapDriven(void)
{
/* D578: the LIVE swap interval decides, not the setting: a driver that
* forces VSync off (swap interval 0) gives non-blocking swaps, and
* swap-driven pacing then measures swap cost as the display period. */
static int64_t s_ivAt;
static int s_iv = 1;
const int64_t now = (int64_t)sysGetMicroseconds();
if (now - s_ivAt > 1000000) { /* re-read ~1/s (render worker thread) */
int rf = 0;
videoInterpInfo(&rf, &s_iv);
s_ivAt = now;
}
return videoInterpVSync() && videoInterpHz() > 0 && s_iv != 0;
}
static int portInterpLog(void)
{
static int v = -1; /* GE_INTERPLOG: pacing trace (first ~2000 lines) */
if (v < 0) { const char *e = getenv("GE_INTERPLOG"); v = e ? (atoi(e) > 1 ? atoi(e) : 2000) : 0; } /* =<n>: line cap */
return v > 0 ? v-- : 0;
}
static int portInterpAlpha1(void)
{
static int v = -1; /* GE_INTERP_ALPHA1: every pass exact (identity gate) */
if (v < 0) v = getenv("GE_INTERP_ALPHA1") != NULL;
return v;
}
static void portRenderGfxTaskInterp(Gfx *dl, uint64_t viUs, int hz)
{
const int64_t tick = (int64_t)g_tickIntervalUs;
const int64_t P = 1000000 / hz;
const int64_t A = (int64_t)sysGetMicroseconds();
const int64_t S = (int64_t)viUs;
float alphas[IP_SLOTS];
int n = 0;
/* D583: game-side split of this frame's submit delay (A - S). */
const int64_t subU = s_rwTaskSubmitUs, doneU = s_rwLastDoneUs;
const int64_t gStart = (doneU > S) ? doneU : S;
const int64_t gameUs = subU > gStart ? subU - gStart : 0;
const int64_t blockUs = doneU > S ? doneU - S : 0;
const int64_t pickUs = A > subU ? A - subU : 0;
s_stGameSum += (double)gameUs; if ((double)gameUs > s_stGameMax) s_stGameMax = (double)gameUs;
s_stBlockSum += (double)blockUs; if ((double)blockUs > s_stBlockMax) s_stBlockMax = (double)blockUs;
s_stPickSum += (double)pickUs; if ((double)pickUs > s_stPickMax) s_stPickMax = (double)pickUs;
if (gameUs > (tick * 3) / 4) s_stGameLong++;
if (blockUs > 0) s_stBlocked++;
const int stale = !s_ipWasOn || s_ipCurS == 0 || S <= s_ipCurS || S - s_ipCurS > 4 * tick;
if (stale) {
gfx_interp_reset();
s_ipPrevS = S - tick;
s_ipDelayEma = (double)(A - S);
} else {
s_ipPrevS = s_ipCurS;
s_ipDelayEma = 0.9 * s_ipDelayEma + 0.1 * (double)(A - S);
}
s_ipCurS = S;
s_ipWasOn = 1;
/* D583: slot times come from one continuous present grid (s_gNext), so
* every present is one display period after the last whatever the game
* frame's arrival jitter. Grid period: the display's reported refresh
* with VSync (not the measured swap gaps: under G-Sync/VRR a swap returns
* when we present, so the "period" is our own cadence fed back), else the
* target rate. */
const int swapDriven = portInterpSwapDriven();
int maxN = IP_SLOTS / 2; /* the ring holds this frame + one still waiting */
/* With the F10 overlay open, one pass per game frame: it is a menu, and
* replaying its text in a second pass measured a few px of difference
* between passes in a level (shimmer). The timeline continues, so
* nothing jumps when it closes. */
{
extern int optionsOverlayIsOpen(void);
if (optionsOverlayIsOpen()) maxN = 1;
}
if (s_ipPassEma > 0 && s_ipPassEma * maxN > 0.8 * (double)tick) {
maxN = (int)(0.8 * (double)tick / s_ipPassEma); /* slow GPU/CPU: fewer passes */
if (maxN < 1) maxN = 1;
}
double Pg = (double)P;
if (swapDriven) {
int rfN = 0, ivN = 0;
videoInterpInfo(&rfN, &ivN);
Pg = (rfN > 0) ? 1e6 / (double)rfN : (s_vbP > 0.0) ? s_vbP : (double)P;
}
{
/* Within 1% of tick/k: use exactly tick/k. A 60 Hz game on a 120 Hz
* grid is then exactly 2 presents per tick, with no beat between the
* two clocks (the reported refresh is an integer, 119 for 119.88). */
const double k = floor((double)tick / Pg + 0.5);
if (k >= 1.0 && fabs((double)tick / k - Pg) < 0.01 * Pg) Pg = (double)tick / k;
}
/* Grid points this frame spans: dS / period, the fraction carried
* (90 Hz: 1.5 -> 2, 1, 2, 1, ...). */
if (stale) s_gAcc = 0.0;
s_gAcc += (double)(S - s_ipPrevS) / Pg;
int want = (int)floor(s_gAcc + 0.5);
if (want < 1) want = 1;
s_gAcc -= (double)want;
if (s_gAcc < -1.0 || s_gAcc > 1.0) s_gAcc = 0.0;
if (want > IP_SLOTS / 2) want = IP_SLOTS / 2; /* a long hitch; the grid re-anchors below */
n = want < maxN ? want : maxN;
/* D583: a frame that arrives a tick late (a stall on the game side) gets
* more grid points (3 at 90 Hz), and with a restart that was 6 passes and a
* 16 ms draw on top of the stall. Two passes keep it short. */
/* D583: a frame that arrives a tick late (a stall on the game side) gets
* more grid points (3 at 90 Hz), and with a restart that was 6 passes and a
* 16 ms draw on top of the stall. Two passes keep it short. */
if ((S - s_ipPrevS) > tick + tick / 2 && n > 2) n = 2;
/* Nothing can be shown before this frame's passes are drawn. D583: a
* frame's real draw time, not passes x pass cost: a turn-softened or
* exact restart redraws its passes (about twice the estimate), and the
* first present then went out late, then the next on time (an uneven
* pair of gaps). The peak decays, so a past spike stops costing latency. */
double drawEst = (double)n * s_ipPassEma;
if (s_ipDrawPk > drawEst) drawEst = s_ipDrawPk;
const int64_t ready = A + (int64_t)drawEst + 500;
{
const double margin = 1000.0; /* arrival jitter the grid absorbs without a re-anchor */
const double slack = s_gNext - (double)ready;
if (stale) {
/* The waiting presents belong to a timeline that is gone. */
s_stDrop += (unsigned)s_qLen;
s_qLen = 0;
}
if (stale || s_gNext == 0.0 || fabs(Pg - s_gPeriod) > 0.002 * Pg || slack < 0.0) {
/* Ready after the next grid point (a stall, a late frame) or a new
* period: start the grid at ready. One uneven gap, then even again. */
if (!stale && s_gNext != 0.0) s_stAnchor++;
s_gNext = (double)ready + margin;
if (s_qLen > 0) {
/* Never ahead of a present still waiting (a period change). */
const double after = (double)s_ipTimes[(s_qHead + s_qLen - 1) % IP_SLOTS] + 0.75 * Pg;
if (s_gNext < after) s_gNext = after;
}
s_gMinSlack = 1e18;
s_gWin = 0;
} else {
/* Latency trim: a late frame re-anchors the grid later, nothing moves
* it back. Once a second, if every frame had spare time, move the
* grid earlier by part of it (at most a quarter period: the gap to
* the waiting presents stays >= 0.75 period). */
if (slack < s_gMinSlack) s_gMinSlack = slack;
/* Ten frames when over a period is spare (a load, a stall that
* left a deep queue), else a second. */
if (++s_gWin >= (s_gMinSlack > margin + Pg ? 10 : 60)) {
if (s_gMinSlack > margin + Pg) {
/* Over a period spare (after a load or a stall): take it
* all back at once and drop the waiting presents it
* overtakes (older images; the timeline stays in order). */
s_gNext -= s_gMinSlack - margin;
s_ipLag -= s_gMinSlack - margin; /* the lag follows (it only falls slowly) */
while (s_qLen > 0 &&
(double)s_ipTimes[(s_qHead + s_qLen - 1) % IP_SLOTS] > s_gNext - 0.75 * Pg) {
s_qLen--;
s_stDrop++;
}
s_stPull++;
} else if (s_gMinSlack > margin + 500.0) {
double d = s_gMinSlack - margin;
if (d > 0.25 * Pg) d = 0.25 * Pg;
s_gNext -= d;
s_ipLag -= d;
s_stPull++;
}
s_gMinSlack = 1e18;
s_gWin = 0;
}
}
s_gPeriod = Pg;
}
int64_t times[IP_SLOTS];
for (int j = 0; j < n; j++) {
/* n < want (slow GPU, overlay): spread over the frame's grid points,
* the last pass on the last point (it shows the frame exactly). */
const int idx = want - 1 - ((n - 1 - j) * want) / n;
times[j] = (int64_t)(s_gNext + (double)idx * Pg);
}
s_gNext += (double)want * Pg;
/* The lag (present time minus the sim time it shows) is set so this
* frame's LAST present shows the frame exactly (alpha 1) and earlier ones
* fall between it and the previous frame. Only matrices are blended;
* the sky, effects and anything not matched are always this frame's, so
* ending each frame on alpha 1 keeps them in step (a fixed one-tick lag
* measured alpha ~0 / 0.5 and the world trailed the rest: ghosting in
* the maintainer's 120 Hz test). Rises at once, falls slowly, so a frame
* with one slot fewer (90 Hz) does not swing it back and forth. */
{
const double wantLag = (double)(times[n - 1] - S);
if (stale || wantLag > s_ipLag) s_ipLag = wantLag;
else s_ipLag += 0.05 * (wantLag - s_ipLag);
}
uint8_t why[IP_SLOTS];
for (int i = 0; i < n; i++) {
double a = ((double)times[i] - s_ipLag - (double)s_ipPrevS) / (double)(S - s_ipPrevS);
why[i] = 0;
if (a < 0.0) { a = 0.0; why[i] = 2; }
if (a > 1.0 + 1e-6 || stale) why[i] = 3;
if (a > 1.0 || stale || portInterpAlpha1()) a = 1.0;
alphas[i] = (float)a;
}
if (portInterpLog()) {
char b[160]; int o = 0;
for (int i = 0; i < n && o < (int)sizeof(b) - 24; i++)
o += snprintf(b + o, sizeof(b) - o, " %.3f@%+lld", alphas[i], (long long)(times[i] - A));
sysLogPrintf(LOG_NOTE, "IPLOG tick S=%lld dS=%lld delay=%lld n=%d:%s", (long long)S,
(long long)(S - s_ipPrevS), (long long)(A - S), n, b);
}
videoStartFrame();
videoSyncSplitScreen();
const uint64_t t0 = sysGetMicroseconds();
/* D583: this frame's passes go after the presents still waiting. */
while (s_qLen > IP_SLOTS - n) {
s_qHead = (s_qHead + 1) % IP_SLOTS;
s_qLen--;
s_stDrop++;
}
const int base = (s_qHead + s_qLen) % IP_SLOTS;
const int filled = gfx_interp_tick(dl, alphas, n, base);
for (int j = 0; j < filled; j++) {
const int slot = (base + j) % IP_SLOTS;
s_ipTimes[slot] = times[j];
s_ipIdx[slot] = (uint8_t)j;
/* An exact fallback inside gfx_interp_tick drew every pass at alpha 1. */
const double aj = gfx_interp_last_exact() ? 1.0 : (double)alphas[j];
s_ipSim[slot] = (double)s_ipPrevS + aj * (double)(S - s_ipPrevS);
s_ipWhy[slot] = gfx_interp_last_exact() ? 1 : why[j];
}
s_qLen += filled;
unsigned tkB = 0, tkT = 0, tkX = 0, tkM = 0, tkD = 0, tkU = 0;
gfx_tick_counts(&tkB, &tkT, &tkX, &tkM, &tkD, &tkU);
if (filled > 0) {
/* Capped: a shader prewarm or level-load frame (50-100 ms) is not a
* draw time, and learning it held ~40 ms of latency for seconds. */
double drew = (double)(sysGetMicroseconds() - t0);
if ((S - s_ipPrevS) > tick + tick / 2 || (A - S) > (tick * 5) / 6 || drew > 0.5 * (double)tick) {
/* D583: a slow game frame (spawn-in drops): late from the game
* (dS > 1 tick, a large submit delay) or slow to draw here. */
static int s_slowLog = 0;
if (s_slowLog++ < 300)
sysLogPrintf(LOG_NOTE, "D583 SLOW frame %d: dS %lld us, submit delay %lld us (game %lld, blocked by us %lld, pickup %lld), draw %.0f us (%d passes, %u batches, %u tris, %u texture lookups: %u misses, %u dyn-hash bytes, %u us in import)",
g_framesRendered + 1, (long long)(S - s_ipPrevS), (long long)(A - S), (long long)gameUs,
(long long)blockUs, (long long)pickUs, drew, gfx_interp_body_runs(), tkB, tkT, tkX, tkM, tkD, tkU);
}
if (drew > 0.4 * (double)tick) drew = 0.4 * (double)tick;
s_ipDrawPk = drew > s_ipDrawPk ? drew : s_ipDrawPk - 0.05 * (s_ipDrawPk - drew);
/* D578 (c): divide by the passes actually drawn (an exact restart
* discards earlier ones) and reject single slow outliers. */
int runs = gfx_interp_body_runs();
if (runs < filled) runs = filled;
double per = (double)(sysGetMicroseconds() - t0) / runs;
if (s_ipPassEma > 0 && per > 1.5 * s_ipPassEma) per = 1.5 * s_ipPassEma;
s_ipPassEma = s_ipPassEma > 0 ? 0.9 * s_ipPassEma + 0.1 * per : per;
}
videoEndFrame();
g_lastFrameUs = sysGetMicroseconds();
if (++g_framesRendered <= 5 || (g_framesRendered % 300) == 0)
sysLogPrintf(LOG_NOTE, "frame %d rendered (%d interp pass(es), %.0f us each, %d Hz presents)",
g_framesRendered, filled, s_ipPassEma, hz);
/* D578: always-on pacing stats, once per 10 s while interpolating. */
{
int allOne = 1;
for (int i = 0; i < n; i++) if (alphas[i] < 1.0f) allOne = 0;
s_stFrames++;
s_stSlotsN[n >= 4 ? 3 : n >= 1 ? n - 1 : 0]++;
if (allOne || gfx_interp_last_exact()) s_stExactFrames++;
const int64_t now = (int64_t)sysGetMicroseconds();
int refresh = 0, swapIv = 0;
if (s_stStart == 0) {
s_stStart = now;
videoInterpInfo(&refresh, &swapIv);
sysLogPrintf(LOG_NOTE, "D578 interp start: target %d Hz, refresh %d Hz, swap interval %d, %s pacing",
hz, refresh, swapIv, swapDriven ? "swap-driven" : "timer");
} else if (now - s_stStart >= 10 * 1000000) {
const double secs = (double)(now - s_stStart) / 1e6;
videoInterpInfo(&refresh, &swapIv);
unsigned trTurn = 0, trClamp = 0, trSet = 0, trRoom = 0;
gfx_interp_trigger_counts(&trTurn, &trClamp, &trSet, &trRoom);
sysLogPrintf(LOG_NOTE,
"D578 interp triggers: %u turn-exact, %u turn-softened, %u set-change-exact, "
"%u room-change-exact",
trTurn, trClamp, trSet, trRoom);
{
extern void interpPortalCounts(unsigned out[8]);
extern unsigned interpPortalExtraCount(void);
unsigned pc[8];
interpPortalCounts(pc);
sysLogPrintf(LOG_NOTE,
"D578 portal replay: replaced %u, replays %u, fullview %u, gatefail %u, noroom %u, "
"unreached %u, nocam %u, nosnap %u, undrawn-room replays %u",
pc[0], pc[1], pc[2], pc[3], pc[4], pc[5], pc[6], pc[7], interpPortalExtraCount());
}
sysLogPrintf(LOG_NOTE,
"D578 interp: %.1f presents/s, target %d Hz, refresh %d Hz, swap interval %d, "
"%s, vblank period %.0f us, %u game frames, %.0f%% exact",
(double)s_stPresents / secs, hz, refresh, swapIv,
swapDriven ? "swap-driven" : "timer", s_vbP, s_stFrames,
s_stFrames ? 100.0 * (double)s_stExactFrames / (double)s_stFrames : 0.0);
sysLogPrintf(LOG_NOTE,
"D578 cadence: gaps <7:%u 7-9.5:%u 9.5-12:%u 12-20:%u >20:%u ms | dropped %u | "
"slots/tick 1:%u 2:%u 3:%u 4+:%u | tick %.0f us | grid %.1f us, re-anchors %u, trims %u, draw peak %.0f us | "
"motion hold:%u (exact %u, a=0 %u, late %u, other %u) short:%u ok:%u long:%u jump:%u | present avg %.0f max %.0f us, %u over half a refresh",
s_stGap[0], s_stGap[1], s_stGap[2], s_stGap[3], s_stGap[4], s_stDrop,
s_stSlotsN[0], s_stSlotsN[1], s_stSlotsN[2], s_stSlotsN[3],
s_stFrames ? secs * 1e6 / (double)s_stFrames : 0.0, s_gPeriod, s_stAnchor, s_stPull, s_ipDrawPk,
s_stMot[0], s_stHoldWhy[1], s_stHoldWhy[2], s_stHoldWhy[3], s_stHoldWhy[0], s_stMot[1], s_stMot[2], s_stMot[3], s_stMot[4], s_ipSwapEma, s_stSwapMax, s_stSwapLong);
s_stSwapMax = 0.0;
s_stSwapLong = 0;
if (s_stFrames > 0) {
sysLogPrintf(LOG_NOTE,
"D583 game side: work avg %.0f max %.0f us (%u over 3/4 tick) | blocked by our previous frame: %u frames, avg %.0f max %.0f us | pickup avg %.0f max %.0f us",
s_stGameSum / s_stFrames, s_stGameMax, s_stGameLong, s_stBlocked, s_stBlockSum / s_stFrames,
s_stBlockMax, s_stPickSum / s_stFrames, s_stPickMax);
}
s_stGameSum = s_stGameMax = s_stBlockSum = s_stBlockMax = s_stPickSum = s_stPickMax = 0.0;
s_stGameLong = s_stBlocked = 0;
s_stAnchor = s_stPull = 0;
memset(s_stMot, 0, sizeof(s_stMot));
memset(s_stHoldWhy, 0, sizeof(s_stHoldWhy));
memset(s_stGap, 0, sizeof(s_stGap));
memset(s_stSlotsN, 0, sizeof(s_stSlotsN));
s_stDrop = 0;
s_stStart = now;
s_stFrames = s_stExactFrames = s_stPresents = 0;
}
}
}
/* Show the next waiting slot; called once it is due. */
static void portInterpPresentNext(void)
{
/* D583: a present whose successor is already due is skipped: the display
* fell a whole period behind (a stall, a fixed refresh a little slower
* than the grid), and showing it would delay every later one. */
const int64_t leadS = portInterpSwapDriven() ? 1000 : 0;
while (s_qLen >= 2 && (int64_t)sysGetMicroseconds() >= s_ipTimes[(s_qHead + 1) % IP_SLOTS] - leadS) {
s_qHead = (s_qHead + 1) % IP_SLOTS;
s_qLen--;
s_stDrop++;
}
const int k = s_qHead;
s_qHead = (s_qHead + 1) % IP_SLOTS;
s_qLen--;
const int64_t due = s_ipTimes[k];
if (portInterpSwapDriven() && s_vbLast != 0) {
/* Swap-driven: no timer, the blocking swap is the clock. Guard only
* against a swap that does not block (no real VSync): never present
* twice within 0.7 of the target period. */
const int hz0 = videoInterpHz();
/* Based on the measured display period, not the target rate. */
int rfG = 0, ivG = 0;
videoInterpInfo(&rfG, &ivG);
const double pG = rfG > 0 ? 1e6 / (double)rfG : s_vbP > 0.0 ? s_vbP : 1000000.0 / (hz0 > 0 ? hz0 : 60);
const int64_t minGap = (int64_t)(0.7 * pG);
for (;;) {
const int64_t left = s_vbLast + minGap - (int64_t)sysGetMicroseconds();
if (left <= 0) break;
if (left > 2000) sysSleep(1000);
else sysCpuRelax();
}
}
const int64_t t0 = (int64_t)sysGetMicroseconds();
videoInterpPresent(k);
const int64_t r = (int64_t)sysGetMicroseconds();
if (portInterpLog())
sysLogPrintf(LOG_NOTE, "IPLOG present k=%d at=%lld late=%lld swap=%lld", k, (long long)t0,
(long long)(t0 - due), (long long)(r - t0));
const int hz = videoInterpHz();
const int64_t P = 1000000 / (hz > 0 ? hz : 60);
s_stPresents++;
s_ipSwapEma = s_ipSwapEma > 0.0 ? 0.95 * s_ipSwapEma + 0.05 * (double)(r - t0) : (double)(r - t0);
if ((double)(r - t0) > s_stSwapMax) s_stSwapMax = (double)(r - t0);
if ((r - t0) > P / 2) s_stSwapLong++;
if (s_stLastPres != 0) {
const int64_t g = r - s_stLastPres;
s_stGap[g < 7000 ? 0 : g < 9500 ? 1 : g < 12000 ? 2 : g < 20000 ? 3 : 4]++;
}
s_stLastPres = r;
if (s_gPeriod > 0.0) {
const double st = (s_ipSim[k] - s_stLastSim) / s_gPeriod;
if (s_stLastSim != 0.0 && st > -1.0 && st < 8.0)
{
s_stMot[st < 0.25 ? 0 : st < 0.75 ? 1 : st < 1.25 ? 2 : st < 1.75 ? 3 : 4]++;
if (st < 0.25) s_stHoldWhy[s_ipWhy[k] & 3]++;
}
s_stLastSim = s_ipSim[k];
}
if (portInterpSwapDriven()) {
/* The swap returned at a vblank: that is the display clock. Track the
* refresh period from gaps near one period (a missed vblank or an idle
* stretch is not a period). */
if (s_vbLast != 0) {
const double gap = (double)(r - s_vbLast);
if (s_vbP <= 0.0) {
/* Seed from the REAL refresh, not the target rate (120 target
* on a 90 Hz Deck, gamescope caps): else every gap is rejected. */
int rf = 0, iv = 0;
videoInterpInfo(&rf, &iv);
s_vbP = (rf > 0) ? 1e6 / (double)rf : (double)P;
}
if (gap > 0.75 * s_vbP && gap < 1.3 * s_vbP) {
s_vbP = 0.95 * s_vbP + 0.05 * gap;
s_vbOut = 0;
} else {
/* Slow re-seed: many consecutive off-period gaps mean the
* estimate is wrong (swap interval change, cap); take the
* median of the recent gaps. */
s_vbOutGap[s_vbOut++ & 7] = gap;
if (s_vbOut >= 16) {
double a[8];
for (int i = 0; i < 8; i++) a[i] = s_vbOutGap[i];
for (int i = 1; i < 8; i++)
for (int j = i; j > 0 && a[j - 1] > a[j]; j--) { double t = a[j]; a[j] = a[j - 1]; a[j - 1] = t; }
s_vbP = 0.5 * (a[3] + a[4]);
s_vbOut = 0;
}
}
{
/* D578: never estimate a display period longer than the real
* refresh's (+10%). A run of missed vblanks or one-present
* ticks re-seeded it to ~16.7 ms on a 120 Hz panel, and the
* 0.7-period gate above then held presents at 60. */
int rf = 0, iv = 0;
videoInterpInfo(&rf, &iv);
if (rf > 0 && s_vbP > 1.1e6 / (double)rf) {
s_vbP = 1e6 / (double)rf;
}
}
}
s_vbLast = r;
}
}
/* D583: a waiting present that falls due while the new frame draws would go
* out late (by up to the draw time), then the next one on time: an uneven
* pair of gaps. Show it first, at its time, when the frame can afford the
* wait: the game is waiting for this frame's SP/DP done, so never when the
* wait + present + draw would run past ~60% of a tick (a compositor whose
* swap blocks a whole refresh, a slow GPU: then draw first, as before). */
static void portInterpPresentBeforeDraw(uint64_t viUs)
{
const int64_t tick = (int64_t)g_tickIntervalUs;
const int64_t lead = portInterpSwapDriven() ? 1000 : 0;
const double drawEst = s_ipDrawPk > 2.0 * s_ipPassEma ? s_ipDrawPk : 2.0 * s_ipPassEma;
while (s_qLen > 0) {
const int64_t now = (int64_t)sysGetMicroseconds();
const int64_t due = s_ipTimes[s_qHead] - lead;
if ((double)due > (double)now + drawEst + 300.0) break; /* falls after the draw */
const int64_t wait = due > now ? due - now : 0;
if ((double)(now - (int64_t)viUs + wait) + s_ipSwapEma + drawEst > 0.6 * (double)tick) break;
while ((int64_t)sysGetMicroseconds() < due) {
if (due - (int64_t)sysGetMicroseconds() > 2000) sysSleep(1000);
else sysCpuRelax();
}
portInterpPresentNext();
}
}
static void *portRenderWorker(void *arg)
{
(void)arg;
for (;;) {
int presentDue = 0;
pthread_mutex_lock(&s_rwLock);
while (s_rwTask == NULL) {
/* Quit while idle: stop taking frames and never touch GL again.
@@ -1294,19 +1939,88 @@ static void *portRenderWorker(void *arg)
pthread_mutex_unlock(&s_rwLock);
for (;;) sysSleep(100000);
}
/* D578: sleep until the next stored present is due, or a frame. */
int64_t waitUs = 100 * 1000;
/* D578: with VSync the slot times are predicted vblanks. Presenting
* back-to-back and letting the blocking swap pace only works on a
* fixed refresh: under G-Sync/VRR a swap does not wait for a beat,
* the slots went out bunched and early, the panel followed that
* uneven cadence (brightness shimmer) and NVIDIA's layered-DXGI
* fullscreen path showed black frames. Present each slot at its
* time minus a small lead; on a fixed refresh the swap still lands
* on the same vblank. */
const int64_t lead = portInterpSwapDriven() ? 1000 : 0;
if (s_qLen > 0) {
waitUs = s_ipTimes[s_qHead] - lead - (int64_t)sysGetMicroseconds();
if (waitUs <= 0) {
presentDue = 1;
break;
}
}
/* An absolute CLOCK_REALTIME deadline is too coarse for this
* (MinGW: off by several ms either way, measured). While a present
* is pending, sleep 1 ms at a time to 2 ms before it, then spin,
* watching for a new frame throughout. */
if (s_qLen > 0) {
const int64_t due = s_ipTimes[s_qHead] - lead;
pthread_mutex_unlock(&s_rwLock);
for (;;) {
const int64_t left = due - (int64_t)sysGetMicroseconds();
if (left <= 0 || __atomic_load_n(&s_rwTask, __ATOMIC_ACQUIRE) != NULL) break;
if (left > 2000) sysSleep(1000);
else sysCpuRelax();
}
pthread_mutex_lock(&s_rwLock);
continue;
}
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_nsec += 100 * 1000 * 1000;
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
uint64_t ns = (uint64_t)ts.tv_nsec + (uint64_t)waitUs * 1000u;
ts.tv_sec += (time_t)(ns / 1000000000u);
ts.tv_nsec = (long)(ns % 1000000000u);
pthread_cond_timedwait(&s_rwCond, &s_rwLock, &ts);
}
if (presentDue) {
pthread_mutex_unlock(&s_rwLock);
portInterpPresentNext();
continue;
}
Gfx *dl = s_rwTask;
const uint64_t viUs = s_rwTaskViUs;
pthread_mutex_unlock(&s_rwLock);
portRenderGfxTask(dl);
/* D578: with VSync nothing is presented here: a swap blocks for a
* refresh, and the game waits for SP/DP done (posted below, after the
* passes are drawn), so a swap before the draw cost the game whole
* retraces (Deck Game Mode: 90 fps unreachable on idle hardware). The
* finished passes live in the present-slot FBOs
* (gfx_opengl_interp_store), so presenting after SP/DP is safe. D583:
* the previous frame's unshown slots stay queued (ring), they are not
* dropped: the new frame's slots come after them on the grid. */
const int hz = videoInterpHz();
if (hz > 0 && viUs != 0) {
portInterpPresentBeforeDraw(viUs);
const int64_t d0 = (int64_t)sysGetMicroseconds();
const int q0 = s_qLen;
portRenderGfxTaskInterp(dl, viUs, hz);
if (portInterpLog()) /* D583: line late presents up against the draw they fell in */
sysLogPrintf(LOG_NOTE, "IPLOG draw at=%lld dur=%lld vi+%lld q %d->%d next_due=%lld",
(long long)d0, (long long)((int64_t)sysGetMicroseconds() - d0),
(long long)(d0 - (int64_t)viUs), q0, s_qLen,
(long long)(s_qLen > 0 ? s_ipTimes[s_qHead] - d0 : -1));
} else {
if (s_ipWasOn) {
s_ipWasOn = 0;
s_vbLast = 0; s_vbP = 0.0; s_vbOut = 0; /* re-measure next time */
s_qLen = 0; s_gNext = 0.0;
gfx_interp_reset();
}
portRenderGfxTask(dl);
}
pthread_mutex_lock(&s_rwLock);
s_rwTask = NULL;
s_rwLastDoneUs = (int64_t)sysGetMicroseconds(); /* D583 */
pthread_cond_broadcast(&s_rwCond);
pthread_mutex_unlock(&s_rwLock);
portPostEventForce(OS_EVENT_SP); /* D134: must not be dropped */
@@ -1373,6 +2087,8 @@ void osSpTaskStartGo(OSTask *t)
}
}
s_rwTask = (Gfx *)t->t.data_ptr;
s_rwTaskViUs = __atomic_load_n(&g_lastViUs, __ATOMIC_ACQUIRE); /* D578 */
s_rwTaskSubmitUs = (int64_t)sysGetMicroseconds(); /* D583 */
pthread_cond_broadcast(&s_rwCond);
pthread_mutex_unlock(&s_rwLock);
return;
@@ -1422,8 +2138,8 @@ void osWritebackDCache(void *addr, int size) { (void)addr; (void)size; }
void osWritebackDCacheAll(void) { }
void osInvalICache(void *addr, int size) { (void)addr; (void)size; }
void osInvalDCache(void *addr, int size) { (void)addr; (void)size; }
u32 osVirtualToPhysical(void *va) { return (u32)(uintptr_t)va; }
void *osPhysicalToVirtual(u32 pa) { return (void *)(uintptr_t)pa; }
u32 osVirtualToPhysical(void *va) { return portHostToN64(va); }
void *osPhysicalToVirtual(u32 pa) { return portN64ToHost(pa); }
/* ------------------------------------------------------------------------ */
/* Misc */
+25
View File
@@ -0,0 +1,25 @@
/* Mach-O equivalents of the decomp's `#pragma weak alias = target`. */
.globl _g_DebugPortalsInputBufferSource1
.set _g_DebugPortalsInputBufferSource1, _g_DebugPortalsInputBuffer1
.globl _g_DebugPortalsInputBufferSource2
.set _g_DebugPortalsInputBufferSource2, _g_DebugPortalsInputBuffer2
.globl _g_DebugPortalsInputBufferSource3
.set _g_DebugPortalsInputBufferSource3, _g_DebugPortalsInputBuffer3
.globl _g_DebugPortalsInputBufferSource4
.set _g_DebugPortalsInputBufferSource4, _g_DebugPortalsInputBuffer4
.globl _objectiveGetStatus_WEAK
.set _objectiveGetStatus_WEAK, _get_status_of_objective
.globl _spec_keyboard_row_caps_z_x_c_v
.set _spec_keyboard_row_caps_z_x_c_v, _spec_keyboard_buffer
.globl _fsin
.set _fsin, ___sinf
.globl _sinf
.set _sinf, ___sinf
.globl _fcos
.set _fcos, ___cosf
.globl _cosf
.set _cosf, ___cosf
+17
View File
@@ -23,6 +23,7 @@
#include "platform.h"
#include "system.h"
#include "portaddr.h"
#include "config.h"
#include "updatecheck.h"
#include "fs.h"
@@ -87,6 +88,10 @@ static void portAtExit(void)
inputRumbleStopAll(); /* D493: no pad left rumbling after exit */
videoSaveWindowState();
configSave();
/* D632: release the GPU context while the process is alive (needs the
* window state saved first; before any relaunch so the GPU init of the
* new instance never overlaps this teardown). */
videoTeardownForExit();
/* D443: Restart game -- runs only on the orderly D344 exit path, after the
* ini is written, so the new instance reads the saved settings. */
if (videoRestartRequested()) {
@@ -133,11 +138,23 @@ int main(int argc, char **argv)
return 0;
}
/* D576: also log to $S/ge007.log (next to the ini) -- on a normal
* double-click launch stderr is lost. $S resolves from the CWD/exe dir
* and needs no prior setup, so open it here and let the banner below
* be the file's first line. */
sysLogOpenFile();
sysLogPrintf(LOG_INFO, "GoldenEye 007 PC port starting "
"(%s, %s %s) -- %s",
GE007_ROMID, GE007_VERSION_HASH, GE007_VERSION_CODENAME,
GE007_ORIGIN_URL);
/* Reserve the N64 address-space window before anything else maps or
* allocates (macOS: 4 GiB PROT_NONE; elsewhere a no-op). Must precede
* crashInit so the reservation is in place for every later stage, and any
* failure here is reported before SDL/GL are involved. */
portAddrInit();
/* Crash handler first, so any failure below is debuggable. */
crashInit();
+19 -2
View File
@@ -23,6 +23,7 @@
#include <PR/ultratypes.h>
#include <PR/os.h>
#include <tlb_manage.h>
#include "portaddr.h"
/* --- Segment start/end getters (normally linker-script symbols) --------- */
/* On the PC the ROM is loaded by romdata.c; these are unused. Return 0. */
@@ -102,8 +103,24 @@ u8 (*tlbmanageGetTlbAllocatedBlock(void))[TLB_BLOCK_SIZE]
* lives up there, at 0x707FFD30). Reclaim ~4 MB of it for the mempool
* area; the extra goes to MEMPOOL_STAGE (boss.c:218 gives STAGE
* everything that isn't the fixed PERMANENT bank). Stays well clear of
* animations_frame_buffer. */
return (u8 (*)[TLB_BLOCK_SIZE])(PORT_DRAM_V1_BASE + 0x700000UL); /* portaddr.h */
* animations_frame_buffer.
*
* D610 (macos branch): #95 (danturn) puts the ceiling at 0x707F0000 -
* ~960 KiB further up, i.e. it reclaims the gap just below
* animations_frame_buffer (0x707FFD30). That is a SILENT BEHAVIOUR CHANGE
* on the base-0 platforms (MEMPOOL_STAGE gets ~960 KiB more, so the
* per-level -m* budget that OOMs at the N64-fidelity ceiling stops OOMing
* at a different point, and any level that did OOM would OOM elsewhere).
* Keep the shipped base-0 value EXACTLY as it is today, and take #95's
* value only where the window is actually shifted (arm64 macOS, where the
* 21/21-level arm64 gate was measured). Re-applying #95 must not replace
* this with an unconditional portN64ToHost(0x707F0000).
*/
#if PORT_ADDR_BASE != 0
return (u8 (*)[TLB_BLOCK_SIZE])(PORT_DRAM_V1_BASE + 0x7FF000UL);
#else
return (u8 (*)[TLB_BLOCK_SIZE])(PORT_DRAM_V1_BASE + 0x700000UL);
#endif
}
/* --- K&R libc helpers (IDO provided these; MinGW's libc does not) -------- */
+112 -22
View File
@@ -53,6 +53,7 @@ extern MENU current_menu;
#include "video.h"
#include "input.h"
#include "front.h" /* selected_folder_num (D356 stage probe) */
#include "lv.h" /* lvlGetCurrentMultiPlayerSec (D606 mission timer) */
#include "bondtypes.h" /* sImageTableEntry (D555 crosshair pointer: typed field reads) */
#include "file.h" /* save_data (D356 reset probe: second-file isolation) */
#include "optionsoverlay.h"
@@ -144,7 +145,12 @@ static const char *const kGameplayView[] = { "Original", "Extended", NULL }; /
* have it, but it's dropped from the menu: with VSync on (the default) it's
* indistinguishable from 60, and with VSync off it just burns GPU time
* re-presenting the same simulated frame -- confusing for no real benefit. */
static const int kFpsCapSeq[] = { 30, 60 };
/* D578: 90/120/144/165/240 draw interpolated frames between the 60 Hz game frames
* (port/fast3d/gfx_pc.cpp); the sim rate is unchanged. */
/* -1 = Auto: match the display refresh (up to 240; the default). 240 is the
* renderer's ceiling: at most IP_SLOTS/2 = 4 presents per 60 Hz tick (libultra.c). */
static const int kFpsCapSeq[] = { -1, 30, 60, 90, 120, 144, 165, 240 };
#define FPSCAP_N ((int)(sizeof(kFpsCapSeq) / sizeof(kFpsCapSeq[0])))
/* Windowed-mode resolution presets. Filtered at init to those that fit the
* desktop; the Resolution row cycles the surviving list. */
@@ -379,10 +385,13 @@ static struct Row rows[] = {
{ .key="__HdrGame", .label="GAME", .kind=ROW_HEADER },
/* Aim control (hold vs toggle the aim button) is a per-file watch row
* like Auto-aim; the port honours the game's toggle. The game's own
* "Look up/down" (Bond.Look) row was removed (D564): the port's mouse and
* centred-pad look write the view directly and never consult it, so the
* Mouse / Controller "Invert look" rows are the only inversion controls;
* watchsettings.c pins the game's value to its factory default. */
* "Look up/down" (Bond.Look) row was removed (D564): in the Modern
* scheme the port's mouse and centred-pad look write the view directly
* and never consult it, so the Mouse / Controller "Invert look" rows
* are the only inversion controls there (watchsettings.c pins the
* game's value to its factory default). #145: in the Original scheme
* the pin is lifted and the in-game watch row is live again -- the
* rows above still apply on top (pad look stick, #145 fix). */
{ .key="Bond.AimControl", .label="Aim control", .kind=ROW_TOGGLE,
.names=kHold, .found=1, .uiMax=1, .cfgMax=1, .saveScoped=1 },
{ .key="Bond.AutoAim", .label="Auto-aim", .kind=ROW_TOGGLE,
@@ -433,6 +442,9 @@ static struct Row rows[] = {
* patched, read or written. Applies live (rowSetCommit). */
{ .key="Game.AllUnlocked", .label="All unlocked",
.kind=ROW_TOGGLE, .step=1, .names=kOnOff },
/* D606 (#144): PD-style always-visible mission timer; drawn top-right
* (under the FPS readout when both are on) while a stage is active. */
{ .key="Game.MissionTimer", .label="Mission timer", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
SEP(G4),
/* D551: opt-in update check; the action row appears only once a newer release is known. */
{ .key="Game.CheckUpdates", .label="Check for updates", .kind=ROW_TOGGLE, .step=1, .names=kOnOff },
@@ -787,9 +799,20 @@ int optionsBindingCaptureTick(void)
static int s_showFps = 0;
static char s_fpsText[16] = "";
/* D606 (#144, v0.6.0): optional PD-style always-visible mission timer.
* Reads the game's own mission clock (lvlGetCurrentMultiPlayerSec: the
* D_80048394 accumulator scaled to region ticks -- the same source the
* watch TIME page and the AI_IFMissionTime conditions use), so it is the
* faithful N64 mission time, not a wall clock. Port-only: a text row like
* the D213 FPS readout; off by default, nothing appended when off, so
* goldens are untouched. */
static int s_missionTimer = 0;
static char s_missionTimeText[24] = "";
PD_CONSTRUCTOR static void overlayConfigInit(void)
{
configRegisterInt("Video.DisplayFPS", &s_showFps, 0, 1);
configRegisterInt("Game.MissionTimer", &s_missionTimer, 0, 1);
}
/* Sampled once per emitted frame; recomputes the string every ~0.5 s. */
@@ -807,12 +830,36 @@ static void fpsTick(void)
uint64_t dtUs = nowUs - winStartUs;
if (dtUs >= 500000) {
int fps = (int)((double)frames * 1e6 / (double)dtUs + 0.5);
/* D578: with frame interpolation the screen gets more frames than the
* overlay is emitted (once per game frame): show what video.c counts. */
if (videoInterpHz() > 0) fps = (int)(videoGetFPS() + 0.5f);
snprintf(s_fpsText, sizeof(s_fpsText), "%d FPS", fps);
winStartUs = nowUs;
frames = 0;
}
}
/* D606: refreshed once per emitted frame, like the FPS window above. The
* clock is stage-paced (lvl.c advances it during a run), so menu frames
* simply carry no text and the row disappears outside a mission. */
static void missionTimerTick(void)
{
if (!s_missionTimer ||
(current_menu != MENU_RUN_STAGE && current_menu != MENU_INVALID)) {
s_missionTimeText[0] = 0;
return;
}
int s = (int)lvlGetCurrentMultiPlayerSec();
if (s < 0) s = 0;
if (s >= 3600) {
snprintf(s_missionTimeText, sizeof(s_missionTimeText), "TIME %d:%02d:%02d",
s / 3600, (s / 60) % 60, s % 60);
} else {
snprintf(s_missionTimeText, sizeof(s_missionTimeText), "TIME %d:%02d",
s / 60, s % 60);
}
}
/* Layout (game 2D pixel space = viGetX() x viGetY(), ~320x240). Shared by the
* emit path and the mouse hit-testing in optionsOverlayHandleInput().
* PD-style dialog (menu PD parity R4): sized to its content and centred, one
@@ -863,6 +910,8 @@ static float ovScale(void)
}
extern void gfx_set_overlay_scale(float s);
static s32 bodyWidth(const char *str); /* D577: used by overlayLayout */
static struct OvLayout overlayLayout(void)
{
struct OvLayout o;
@@ -885,8 +934,32 @@ static struct OvLayout overlayLayout(void)
o.hintY = o.bottom + 3;
o.labelX = o.left + 10;
o.valueR = o.right - 10;
o.barX1 = o.valueR - 54;
o.barX0 = o.barX1 - 66;
{ /* D577: the bar and value column are fixed canvas units, but the
* canvas shrinks with HudScale (D512), so at HUD 150 slider labels
* only had 53 units and ended in "..". When the page's widest slider
* label does not fit, shorten the bar (66 -> 32), then the value
* column (54 -> 40). Measured over the whole page so the bar never
* moves while scrolling; a page that fits (HUD 100) is unchanged. */
s32 valueW = 54, barW = 66, need = 0;
if (s_section >= 0) {
for (int p = 0; p < s_visN; p++) {
const struct Row *r = &rows[s_visIdx[p]];
if (r->kind != ROW_SLIDER || isSepRow(r)) continue;
s32 lw = bodyWidth(r->label) + 4; /* + the 4-unit gap before the bar */
if (lw > need) need = lw;
}
}
s32 over = need - (o.valueR - valueW - barW - o.labelX);
if (over > 0) {
s32 cut = over < barW - 32 ? over : barW - 32;
barW -= cut;
over -= cut;
}
if (over > 0)
valueW -= over < valueW - 40 ? over : valueW - 40;
o.barX1 = o.valueR - valueW;
o.barX0 = o.barX1 - barW;
}
return o;
}
@@ -1656,10 +1729,10 @@ static void rowAdjust(struct Row *r, int dir)
}
case ROW_FPSCAP: {
int idx = 0;
for (int i = 0; i < 2; i++) {
for (int i = 0; i < FPSCAP_N; i++) {
if (kFpsCapSeq[i] == (int)lround(v)) idx = i;
}
idx = (idx + dir + 2) % 2;
idx = (idx + dir + FPSCAP_N) % FPSCAP_N;
rowSetStepWatch(r, (double)kFpsCapSeq[idx]);
break;
}
@@ -1815,12 +1888,13 @@ static void ddBuild(const struct Row *r)
}
s_ddN = MSAA_N;
} else if (r->kind == ROW_FPSCAP) {
for (int i = 0; i < 2; i++) {
for (int i = 0; i < FPSCAP_N; i++) {
s_ddVals[i] = kFpsCapSeq[i];
snprintf(s_ddLab[i], sizeof(s_ddLab[i]), "%d FPS", kFpsCapSeq[i]);
if (kFpsCapSeq[i] < 0) snprintf(s_ddLab[i], sizeof(s_ddLab[i]), "Auto (%d FPS)", videoAutoFpsCap());
else snprintf(s_ddLab[i], sizeof(s_ddLab[i]), "%d FPS", kFpsCapSeq[i]);
if (kFpsCapSeq[i] == cur) s_ddCur = i;
}
s_ddN = 2;
s_ddN = FPSCAP_N;
} else if (r->kind == ROW_RES) {
for (int k = 0; k < s_resFitN && k < DD_MAX; k++) {
int i = s_resFit[k];
@@ -1911,14 +1985,17 @@ static int ddGeom(s32 *x0, s32 *y0, s32 *x1, s32 *y1, int *vis)
s32 h = v * OV_LINE + 2;
*x1 = o.valueR + 3;
*x0 = *x1 - w;
s32 H = ovH();
/* D646: the popup stays inside the settings box -- it used to open
* down over the hint/tooltip panel below the box (its limit was the
* screen edge, not o.bottom). */
s32 lo = o.titleB + 1, hi = o.bottom - 1;
*y0 = rowY + OV_LINE;
if (*y0 + h > H - 4) *y0 = rowY - h; /* no room below: open upward */
if (*y0 + h > hi) *y0 = rowY - h; /* no room below: open upward */
/* D568: no room above either (a long list on a mid-page row): keep the whole
* popup on screen, flush with the bottom margin. A negative top used to
* popup inside the box, flush with its bottom. A negative top used to
* draw the labels above the screen while the fill wrapped down past the
* bottom edge (an empty, misplaced box). */
if (*y0 < 4) *y0 = (H - 4 - h > 4) ? H - 4 - h : 4;
if (*y0 < lo) *y0 = (hi - h > lo) ? hi - h : lo;
*y1 = *y0 + h;
*vis = v;
return 1;
@@ -2018,7 +2095,6 @@ static const struct { const char *key; double def; } kResetDefaults[] = {
{ "Input.PadTriggerPct", 25 }, /* = 25 (was 23; rounded, D508) */
{ "Input.RumbleScale", 0.5 }, /* = gRumbleScale (0.5f), D401 */
{ "Input.CrouchMode", 1 }, /* = 1 (toggle; D556) */
{ "Input.MenuPointerMode", 1 }, /* = 1 (direct pointer); no F10 row since D561 (front-end-only legacy option, ini key kept) */
{ "Input.MouseSmoothing", 0 }, /* = 0 (raw) */
{ "Input.MouseRawInput", 0 }, /* = 0 (off) */
{ "Input.ControlScheme", 0 }, /* = 0 (Modern), D513 */
@@ -2048,6 +2124,7 @@ static const struct { const char *key; double def; } kResetDefaults[] = {
/* GAMEPLAY ini rows (port/src/video.c initializers) */
{ "Game.SkipIntro", 0 }, /* = 0 */
{ "Game.CheckUpdates", 0 }, /* = 0 (off, D551) */
{ "Game.MissionTimer", 0 }, /* = 0 (off, D606) */
{ "Game.NoHitFlash", 0 }, /* = 0 */
{ "Game.AllUnlocked", 0 }, /* = 0 -- D257's "default ON" note was stale */
{ "Game.HudScale", 100 }, /* = 100 (D226) */
@@ -2056,7 +2133,7 @@ static const struct { const char *key; double def; } kResetDefaults[] = {
{ "Video.Fullscreen", 0 }, /* = 0 (windowed) */
{ "Video.FullscreenMode", 0 }, /* = 0 (borderless, D511) */
{ "Video.VSync", 1 }, /* = 1 (on) */
{ "Video.FpsCap", 60 }, /* = 60 */
{ "Video.FpsCap", -1 }, /* = -1 (Auto: match display refresh) */
{ "Video.DisplayFPS", 0 }, /* registered 0 */
/* AUDIO (port/src/audio.c, D470) */
{ "Audio.MasterVolume", 100 }, /* = 100 (bit-identical passthrough) */
@@ -2934,7 +3011,8 @@ static void valueText(int i, char *out, int n)
}
if (r->kind == ROW_FPSCAP) {
int fps = (int)lround(v);
if (fps <= 0) snprintf(out, n, "Uncapped");
if (fps < 0) snprintf(out, n, "Auto (%d FPS)", videoAutoFpsCap());
else if (fps == 0) snprintf(out, n, "Uncapped");
else snprintf(out, n, "%d FPS", fps);
return;
}
@@ -3369,6 +3447,7 @@ static Gfx *overlayEmitLocked(void)
}
fpsTick();
missionTimerTick();
{ /* D558 test hook: GE_OPTIONSOVERLAY_ATFRAME=<n> re-applies the open request at emit n (read once). */
static int s_atFrame = -2, s_emitN = 0;
@@ -3387,10 +3466,12 @@ static Gfx *overlayEmitLocked(void)
}
if (!s_open) {
if (!s_showFps || !s_fpsText[0]) {
if ((!s_showFps || !s_fpsText[0]) && !s_missionTimeText[0]) {
return NULL; /* nothing appended -> golden dumps byte-identical */
}
/* D213: FPS-only mini DL (top-right), panel closed. */
/* D213: FPS-only mini DL (top-right), panel closed. D606: the
* mission timer shares the top-right corner, below the FPS readout
* when both are visible. */
const s32 fw = ovW();
const s32 fh = ovH();
gfx_set_overlay_scale(ovScale()); /* D510 */
@@ -3400,7 +3481,14 @@ static Gfx *overlayEmitLocked(void)
gDPSetTexturePersp(fgdl++, G_TP_NONE);
gDPSetScissor(fgdl++, G_SC_NON_INTERLACE, 0, 0, viGetX(), viGetY());
fgdl = microcode_constructor(fgdl);
fgdl = drawTextR(fgdl, fw - 6, 6, s_fpsText, 0x40ff60ff);
int ty = 6;
if (s_showFps && s_fpsText[0]) {
fgdl = drawTextR(fgdl, fw - 6, 6, s_fpsText, 0x40ff60ff);
ty = 22;
}
if (s_missionTimeText[0]) {
fgdl = drawTextR(fgdl, fw - 6, ty, s_missionTimeText, 0x40ff60ff);
}
gDPPipeSync(fgdl++);
gSPEndDisplayList(fgdl++);
return s_buf;
@@ -3758,6 +3846,7 @@ int optionsRowIsShown(int i)
* resolution, VSync, anti-aliasing, volumes and sensitivities carry no tip). */
static const struct { const char *key, *help; } kRowHelp[] = {
{ "__DisplayMode", "Modern, original or custom graphics presets." },
{ "Video.FpsCap", "Auto matches your display. Above 60 adds blended in-between frames. The game still runs at 60." }, /* D578 */
{ "Video.AspectMode", "Fill window fits the picture to the whole window." }, /* D560: shown only while Fill window is selected (optionsRowHelp) */
{ "Video.CrosshairHide", "When off, the crosshair stays hidden, even with Sight on screen." },
{ "Video.CrosshairPersistent", "Shows the crosshair all the time. The N64 shows it only while aiming. Off by default." },
@@ -3782,6 +3871,7 @@ static const struct { const char *key, *help; } kRowHelp[] = {
{ "Game.SkipIntro", "Skips the intro and opens the file select screen." },
{ "Game.AllUnlocked", "Unlocks all levels, 007 mode and every cheat." },
{ "Game.CheckUpdates", "Notifies you when a new version is available." },
{ "Game.MissionTimer", "Shows the mission elapsed time on screen (top-right, under the FPS readout)." },
};
+215
View File
@@ -0,0 +1,215 @@
/*
* port_addr.c — the N64 address-space window (see port/include/portaddr.h,
* D607's one-header model; the conversion inlines live in that header, this
* file owns the window reservation, the image-base state, and the self-test).
*
* On macOS/arm64 the whole window is reserved PROT_NONE up front so that the
* fixed-address carves (cart, DRAM V1/V2, game-thread stacks, in dram.c /
* romdata.c / libultra.c) land inside a range the process already owns and
* cannot collide with libmalloc or the dyld shared cache. Reserving is cheap:
* PROT_NONE pages are not committed.
*
* On Windows/Linux PORT_ADDR_BASE is 0 and this is a no-op beyond recording
* the image base.
*/
#include <stdint.h>
#include <stdlib.h>
#include <stddef.h>
#include "platform.h"
#include "system.h"
#include "portaddr.h"
#ifdef PORT_ADDR_STRICT
#if defined(PLATFORM_MACOS) || defined(__APPLE__) || defined(__linux__)
/* backtrace support is POSIX-only; the block below is guarded the same way
* (mingw-w64 has no execinfo.h without -static-libgcc; strict checks still
* log the bad address there, just without a native backtrace). */
#include <execinfo.h>
#endif
#endif
#if defined(PLATFORM_MACOS) && defined(PLATFORM_ARM)
#include <mach/mach.h>
#include <mach/mach_vm.h>
#endif
uintptr_t g_portImageBase = 0;
int g_portUseImageRel = 0;
/* Defined by port/src/dram_syms*.s; must equal PORT_ADDR_BASE + 0x70000000. */
extern unsigned char cfb_16[];
/* ROM-free verification of the address model: window round-trips for each
* region, the absolute-symbol base, and the image-relative encoding. Logs
* PASS/FAIL for each so a run without a ROM still exercises the core. */
static int portAddrCheck(const char *what, int ok)
{
sysLogPrintf(ok ? LOG_INFO : LOG_ERROR, "portAddr: %-26s %s",
what, ok ? "PASS" : "FAIL");
return ok;
}
static void portAddrSelfTest(void)
{
const uintptr_t B = (uintptr_t)PORT_ADDR_BASE;
int ok = 1;
ok &= portAddrCheck("host->n64 DRAM V1",
portHostToN64((void *)(B + 0x70001234u)) == 0x70001234u);
ok &= portAddrCheck("n64->host DRAM V1",
portN64ToHost(0x70001234u) == (void *)(B + 0x70001234u));
ok &= portAddrCheck("host->n64 DRAM V2",
portHostToN64((void *)(B + 0x80005678u)) == 0x80005678u);
ok &= portAddrCheck("host->n64 cart",
portHostToN64((void *)(B + 0x10456789u)) == 0x10456789u);
ok &= portAddrCheck("n64->host cart",
portN64ToHost(0x10456789u) == (void *)(B + 0x10456789u));
ok &= portAddrCheck("cfb_16 at base+DRAM_V1",
(uintptr_t)cfb_16 == B + 0x70000000u);
ok &= portAddrCheck("in-window: NULL rejected",
!portAddrIsMapped(0));
ok &= portAddrCheck("in-window: low guard",
!portAddrIsMapped((void *)(B + PORT_ADDR_LOW_GUARD - 1u)));
ok &= portAddrCheck("in-window: DRAM V1",
portAddrIsMapped((void *)(B + 0x70001234u)));
ok &= portAddrCheck("in-window: above window",
!portAddrIsMapped((void *)(B + (uintptr_t)PORT_ADDR_WINDOW)));
if (g_portUseImageRel) {
ok &= portAddrCheck("host->n64 image",
portHostToN64((void *)(g_portImageBase + 0x1234u)) == 0x40001234u);
ok &= portAddrCheck("n64->host image",
portN64ToHost(0x40001234u) == (void *)(g_portImageBase + 0x1234u));
}
sysLogPrintf(ok ? LOG_INFO : LOG_ERROR, "portAddr: selftest %s",
ok ? "ALL PASS" : "FAILURES");
}
#ifdef PORT_ADDR_STRICT
/*
* Strict address validation (-DPORT_ADDR_STRICT=1, off by default).
*
* Every bug in the MACOS-SFX-BOUND through MACOS-TEXALIGN family reaches portN64ToHost() holding a value
* that is not a real N64 address -- either a host pointer truncated to 32 bits
* or an address that was never re-based. The fault then happens later, in
* unrelated code, which is what made these expensive to find. This checks the
* value against the regions the port actually maps and reports the bad ones
* at the point of conversion, naming the caller.
*
* Regions (see port/include/portaddr.h and port/src/dram.c):
* 0x10000000..0x1fffffff cart image + pcmodels/pccg sidecars
* 0x40000000..0x6fffffff image-relative encoding (exe globals)
* 0x70000000..0x707fffff DRAM V1
* 0x80000000..0x807fffff DRAM V2 (KSEG0 mirror of V1)
* 0xa0000000..0xbfffffff game-thread stacks
* Segmented DL addresses (0x00000000..0x0fffffff) never name host memory and
* are resolved by fast3d's seg_addr(), not here -- so they are reported too.
*/
static int portAddrRegionOk(uint32_t a)
{
if (a >= 0x10000000u && a <= 0x1fffffffu) return 1; /* cart + sidecars */
if (a >= 0x40000000u && a <= 0x6fffffffu) return 1; /* image-relative */
if (a >= 0x70000000u && a <= 0x707fffffu) return 1; /* DRAM V1 */
if (a >= 0x80000000u && a <= 0x807fffffu) return 1; /* DRAM V2 */
if (a >= 0xa0000000u && a <= 0xbfffffffu) return 1; /* thread stacks */
return 0;
}
/* Negative self-test: GE_ADDRSTRICT_SELFTEST=1 feeds the validator a value
* that is deliberately not a mapped N64 address, so a run can prove the
* detector is actually live rather than merely silent. */
void portAddrStrictSelfTest(void)
{
if (getenv("GE_ADDRSTRICT_SELFTEST")) {
sysLogPrintf(LOG_INFO, "PORT_ADDR_STRICT: self-test, expect one report below");
portAddrStrictCheck(0x30000000u); /* in the gap between cart and image-rel */
}
}
void portAddrStrictCheck(uint32_t a)
{
/* Report each distinct bad value once -- a bad pointer in a per-frame path
* would otherwise emit thousands of identical lines and bury the first. */
enum { SEEN_MAX = 64 };
static uint32_t seen[SEEN_MAX];
static int seenCount;
int i;
if (portAddrRegionOk(a)) {
return;
}
for (i = 0; i < seenCount; i++) {
if (seen[i] == a) {
return;
}
}
if (seenCount < SEEN_MAX) {
seen[seenCount++] = a;
}
sysLogPrintf(LOG_ERROR,
"PORT_ADDR_STRICT: 0x%08x is not a mapped N64 address "
"(-> %p). Truncated host pointer, or an address that was "
"never re-based (MACOS-SFX-BOUND through MACOS-TEXALIGN class).",
(unsigned)a, (void *)((uintptr_t)PORT_ADDR_BASE + (uintptr_t)a));
#if defined(PLATFORM_MACOS) || defined(__APPLE__) || defined(__linux__)
{
void *bt[24];
int n = backtrace(bt, (int)(sizeof(bt) / sizeof(bt[0])));
/* Skip frame 0 (this function); the caller is what matters. */
if (n > 1) {
backtrace_symbols_fd(bt + 1, n - 1, 2);
}
}
#endif
}
#endif /* PORT_ADDR_STRICT */
void portAddrInit(void)
{
g_portImageBase = sysImageBase();
/* The image needs the 0x40000000 encoding iff it lies outside the window,
* i.e. its address does not survive a u32 truncation. False on Linux
* (image at 0x20000000, inside the window), true on Windows (0x140000000)
* and macOS (0x1_00xxxxxx + slide). */
g_portUseImageRel = (g_portImageBase >= (uintptr_t)PORT_ADDR_WINDOW) ? 1 : 0;
#if PORT_ADDR_BASE != 0
{
/* Non-clobbering: VM_FLAGS_FIXED without VM_FLAGS_OVERWRITE fails with
* KERN_NO_SPACE if the range is already in use, rather than silently
* replacing a live mapping (mmap MAP_FIXED on macOS does the latter). */
mach_vm_address_t addr = (mach_vm_address_t)PORT_ADDR_BASE;
kern_return_t kr = mach_vm_map(mach_task_self(), &addr,
(mach_vm_size_t)PORT_ADDR_WINDOW, 0,
VM_FLAGS_FIXED, MEMORY_OBJECT_NULL, 0, FALSE,
VM_PROT_NONE, VM_PROT_ALL, VM_INHERIT_DEFAULT);
if (kr != KERN_SUCCESS || addr != (mach_vm_address_t)PORT_ADDR_BASE) {
sysFatalError("portAddr: cannot reserve the 4 GiB N64 window at "
"0x%llx (%s). See docs/dev/MACOS-ARM64-PLAN.md sec 2.",
(unsigned long long)PORT_ADDR_BASE, mach_error_string(kr));
}
}
#endif
/* The committed .darwin.s absolute symbols must have been generated with
* the same --base; cfb_16 is a region-independent sentinel for that. */
if ((uintptr_t)cfb_16 != (uintptr_t)PORT_ADDR_BASE + 0x70000000ULL) {
sysFatalError("portAddr: dram_syms base mismatch (cfb_16=%p, expected 0x%llx). "
"Regenerate with: scripts/gen_romassets.py <region> --darwin "
"--base 0x%llx --rom-size <size> --out ... --dram-out ...",
(void *)cfb_16,
(unsigned long long)((uintptr_t)PORT_ADDR_BASE + 0x70000000ULL),
(unsigned long long)PORT_ADDR_BASE);
}
sysLogPrintf(LOG_INFO, "portAddr: window base=0x%llx image=0x%llx image_rel=%d",
(unsigned long long)PORT_ADDR_BASE,
(unsigned long long)g_portImageBase, g_portUseImageRel);
portAddrSelfTest();
#ifdef PORT_ADDR_STRICT
portAddrStrictSelfTest();
#endif
}
+20
View File
@@ -222,6 +222,26 @@ int romConvertEnsureSidecars(const unsigned char *romImg, const char *romRelPath
break;
}
}
if (!convPath[0] && exedir[0]) {
/* Two layouts the "$E/" forms above miss. (1) On POSIX "$E/" resolves
* CWD-relative, not to the real exe dir, so a launch from another CWD
* needs the absolute form. (2) A macOS .app keeps prepare-assets/ in
* Contents/Resources/ — it cannot live in Contents/MacOS/, where
* codesign treats the directory as nested code and refuses to sign. */
static const char *candExe[3] = {
"prepare-assets/ge007-convert",
"../Resources/prepare-assets/ge007-convert",
"ge007-convert",
};
for (size_t i = 0; i < sizeof(candExe) / sizeof(candExe[0]); i++) {
char full[1024];
snprintf(full, sizeof(full), "%s/%s", exedir, candExe[i]);
if (fsExists(full)) {
snprintf(convPath, sizeof(convPath), "%s", full);
break;
}
}
}
if (!convPath[0]) {
sysLogPrintf(LOG_ERROR,
+56 -15
View File
@@ -35,7 +35,7 @@ extern int snprintf(char *str, size_t maxsize, const char *format, ...);
#else
/* POSIX: mmap the ROM at the fixed cart address (mirrors the VirtualAlloc
* path). <sys/mman.h> is a host header the decomp include path does not
* shadow. */
* shadow. <errno.h> is included further down, after ultra64.h. */
#include <sys/mman.h>
#endif
@@ -45,13 +45,27 @@ extern int snprintf(char *str, size_t maxsize, const char *format, ...);
#include "pcmodels.h"
#include "pccg.h"
#include "romconvert.h"
#include "portaddr.h"
/* D50: struct font/fontchar layouts for the font-segment re-layout. Order
* matters (see pcmodels.c): ultra64.h must finish before bondtypes.h. */
#include <ultra64.h>
#include "bondtypes.h"
#define CART_BASE 0x10000000u
#if !defined(PLATFORM_WINDOWS)
/* Must come after ultra64.h: libc defines `errno` as a macro (glibc:
* `(*__errno_location ())`), which would rewrite the N64 `u8 errno;`
* fields in PR/os.h (OSContStatus, OSContPad, ...) into a function
* declaration and break the build ("field '__errno_location' declared as
* a function"). Only the mmap-failure diagnostic below reads errno. */
#include <errno.h>
#endif
#define CART_BASE 0x10000000u /* N64 cart address */
/* Host address the ROM is mapped at (PORT_ADDR_BASE + CART_BASE; identity on
* Windows/Linux). Game-visible 32-bit cart values stay CART_BASE-based; this
* is only for the mapping and the host-side pointers into it. */
#define CART_HOST ((uintptr_t)portN64ToHost(CART_BASE))
static u8 *rom = NULL; /* heap copy (fallback path) */
static u32 romSize = 0;
@@ -235,15 +249,15 @@ static void romdataRaw16Walk(const u8 *ctl, u32 ctlSize, u8 *tbl, u32 tblSize,
static int romdataFinishCartMap(const char *tok, u8 *img,
u32 sideTotal, u32 cgTotal)
{
memcpy((void *)(uintptr_t)CART_BASE, img, romSize);
memcpy((void *)CART_HOST, img, romSize);
free(img);
mappedAtCartBase = 1;
sysLogPrintf(LOG_INFO, "romdataInit: %s (%u bytes) mapped at 0x%08X "
"(cart base)%s%s", tok, romSize, CART_BASE,
sideTotal ? ", + model sidecars" : "",
cgTotal ? ", + bg/stan sidecars" : "");
pcmodelsLoadSidecars(CART_BASE, romSize);
pccgLoadSidecars(CART_BASE + romSize + pcmodelsTotalSize());
pcmodelsLoadSidecars(CART_HOST, romSize);
pccgLoadSidecars(CART_HOST + romSize + pcmodelsTotalSize());
/* D55: the RLE folder-menu background at `unknown2` has a big-endian w/h
* header that rle_expand_8bit() reads little-endian; match the N64 .data
@@ -370,9 +384,9 @@ int romdataInit(void)
{
u32 maplen = romSize + sideTotal + cgTotal;
#if defined(PLATFORM_WINDOWS)
void *at = VirtualAlloc((LPVOID)CART_BASE, maplen,
void *at = VirtualAlloc((LPVOID)CART_HOST, maplen,
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (at == (void *)(uintptr_t)CART_BASE)
if (at == (void *)CART_HOST)
return romdataFinishCartMap(tok, img, sideTotal, cgTotal);
/* D179 tail: the fixed-address map failed. Say exactly why (Win32
* error + what already occupies the range) and stop here: the
@@ -411,22 +425,33 @@ int romdataInit(void)
/* POSIX: anonymous fixed-address mmap. MAP_FIXED_NOREPLACE (Linux
* 4.17+) fails instead of clobbering an existing mapping; where it
* is unavailable the plain hint is advisory and the == check below
* catches a relocated result. */
* catches a relocated result. On macOS portAddrInit has already
* reserved the whole window, so MAP_FIXED lands inside our range. */
int flags = MAP_PRIVATE | MAP_ANONYMOUS;
#ifndef PLATFORM_MACOS
#ifdef MAP_FIXED_NOREPLACE
flags |= MAP_FIXED_NOREPLACE;
#elif defined(PLATFORM_MACOS)
/* __PAGEZERO ends at CART_BASE in the macOS build, so this range
* is deliberately vacant. Darwin has no MAP_FIXED_NOREPLACE and
* otherwise treats CART_BASE only as a hint, relocating it. */
flags |= MAP_FIXED;
#endif
void *at = mmap((void *)(uintptr_t)CART_BASE, maplen,
#else
flags |= MAP_FIXED;
#endif
void *at = mmap((void *)CART_HOST, maplen,
PROT_READ | PROT_WRITE, flags, -1, 0);
if (at != MAP_FAILED && at == (void *)(uintptr_t)CART_BASE) {
int mapErrno = errno;
if (at != MAP_FAILED && at == (void *)CART_HOST) {
mappedLen = maplen;
return romdataFinishCartMap(tok, img, sideTotal, cgTotal);
}
if (at != MAP_FAILED)
munmap(at, maplen);
sysLogPrintf(LOG_WARNING, "romdataInit: could not map 0x%08X "
"(ASLR/kernel refused the fixed address); using heap "
"copy — direct ROM reads will fail", CART_BASE);
"(mmap result=%p errno=%d); using heap copy — direct "
"ROM reads will fail", CART_BASE, at, mapErrno);
#endif
}
@@ -443,9 +468,9 @@ void romdataDestroy(void)
{
if (mappedAtCartBase) {
#if defined(PLATFORM_WINDOWS)
VirtualFree((LPVOID)CART_BASE, 0, MEM_RELEASE);
VirtualFree((LPVOID)CART_HOST, 0, MEM_RELEASE);
#else
munmap((void *)(uintptr_t)CART_BASE, (size_t)mappedLen);
munmap((void *)CART_HOST, (size_t)mappedLen);
mappedLen = 0;
#endif
mappedAtCartBase = 0;
@@ -458,7 +483,7 @@ void romdataDestroy(void)
const u8 *romdataGetRom(void)
{
if (mappedAtCartBase)
return (const u8 *)(uintptr_t)CART_BASE;
return (const u8 *)CART_HOST;
return rom;
}
u32 romdataGetRomSize(void) { return romSize; }
@@ -479,6 +504,22 @@ int romdataCartAddrValid(u32 addr, u32 size)
return off + size <= romSize + pcmodelsTotalSize() + pccgTotalSize();
}
/* D573: see portaddr.h. DRAM views first (the common case), then the cart
* image; widened before the subtraction so a pointer above 4 GiB can never
* truncate into a window. */
int portAddrIsMapped(const void *p)
{
uintptr_t v = (uintptr_t)p;
if (v - (uintptr_t)PORT_DRAM_V1_BASE < (uintptr_t)PORT_DRAM_SIZE)
return 1;
if (v - (uintptr_t)PORT_DRAM_K0_BASE < (uintptr_t)PORT_DRAM_SIZE)
return 1;
if (mappedAtCartBase && v - (uintptr_t)CART_BASE <
(uintptr_t)romSize + pcmodelsTotalSize() + pccgTotalSize())
return 1;
return 0;
}
const void *romdataMapVa(u32 va)
{
/* N64 RDRAM VAs (0x80000000+/0xA0000000+) have no PC meaning; only cart
+6 -2
View File
@@ -60,7 +60,11 @@ u32 decompressdata(u8 *src, u8 *dst, struct huft *huffman_table)
return produced;
}
s32 rzipGetSomething(void)
u8 *rzipGetSomething(void)
{
return (s32)(uintptr_t)s_rz_nextin;
/* D588: return the live cursor at full pointer width (see the header
* guard). The pre-#95 (s32)(uintptr_t) truncation fed the #95
* portN64ToHost() re-base in texSetBitstring and mis-mapped on
* native pointers in the D131 image-relative range. */
return s_rz_nextin;
}
+155 -18
View File
@@ -27,6 +27,9 @@
#include <unistd.h>
#include <time.h>
#include <sys/stat.h>
#if defined(PLATFORM_MACOS)
#include <mach-o/dyld.h> /* _dyld_get_image_header, _NSGetExecutablePath */
#endif
#endif
#include "system.h"
@@ -59,12 +62,46 @@ uintptr_t sysImageBase(void)
{
#if defined(PLATFORM_WINDOWS)
return (uintptr_t)GetModuleHandleW(NULL);
#elif defined(PLATFORM_MACOS)
/* The mach_header of the main image == its load address. Unlike Windows
* there is no fixed base: arm64 is PIE-only (and `-image_base` is ignored),
* so this is 0x1_0000_0000 + a per-run ASLR slide. M1's image-relative
* address rule uses this; get the slide from _dyld_get_image_vmaddr_slide. */
return (uintptr_t)_dyld_get_image_header(0);
#else
/* TODO: parse /proc/self/maps for the first executable mapping. */
return 0;
#endif
}
/* Fill `buf` with the directory containing the executable, or "" on failure.
* Linux reads /proc/self/exe; macOS uses _NSGetExecutablePath (no /proc, and
* _POSIX_C_SOURCE=199309L above hides readlink on Darwin). */
static void getExeDir(char *buf, size_t buflen)
{
buf[0] = 0;
#if defined(PLATFORM_MACOS)
uint32_t n = (uint32_t)buflen;
if (_NSGetExecutablePath(buf, &n) == 0) {
char *slash = strrchr(buf, '/');
if (slash) *slash = 0; else buf[0] = 0;
} else {
buf[0] = 0;
}
#elif defined(PLATFORM_LINUX)
ssize_t n = readlink("/proc/self/exe", buf, buflen - 1);
if (n > 0) {
buf[n] = 0;
char *slash = strrchr(buf, '/');
if (slash) *slash = 0; else buf[0] = 0;
} else {
buf[0] = 0;
}
#else
(void)buf; (void)buflen;
#endif
}
int64_t sysGetTime(void)
{
return (int64_t)time(NULL);
@@ -85,28 +122,130 @@ void sysSleep(uint32_t micros)
/* --- Logging ------------------------------------------------------------ */
/* D576: also send every log line to $S/ge007.log (next to ge007.ini),
* because a normal (double-click) launch loses stderr. One fputs per line
* keeps lines whole when several threads log at once (the CRT locks per
* call); the file is flushed after every line (the game may crash and the
* log must survive; this is not a per-pixel hot path). Capped at 8 MiB
* total (debug env vars can make the log very chatty).
* Plain C stdio + remove()/rename() only, so Windows (mingw) and Linux
* both build this. */
#define SYS_LOG_FILE_CAP (8u * 1024u * 1024u)
static FILE *g_logFile = NULL;
static size_t g_logFileBytes = 0;
static int g_logFileCapped = 0;
static int g_logFileTried = 0;
static void sysLogCloseFile(void)
{
if (g_logFile) {
fclose(g_logFile);
g_logFile = NULL;
}
}
/* Write one already-formatted line (NUL-terminated, ends in '\n') to the
* log file, honouring the size cap. */
static void sysLogWriteFile(const char *line)
{
if (!g_logFile || g_logFileCapped)
return;
size_t len = strlen(line);
if (fwrite(line, 1, len, g_logFile) != len) {
g_logFileCapped = 1; /* unwriteable: stop trying (no cap note) */
return;
}
g_logFileBytes += len;
if (g_logFileBytes >= SYS_LOG_FILE_CAP) {
fputs("[NOTE ] log: size cap reached, further lines go to stderr only\n",
g_logFile);
g_logFileCapped = 1;
}
fflush(g_logFile);
}
void sysLogOpenFile(void)
{
char logpath[1024], prevpath[1024];
if (g_logFile || g_logFileTried)
return; /* idempotent */
g_logFileTried = 1;
/* sysResolvePath returns one static buffer; copy each path out. */
strncpy(logpath, sysResolvePath("$S/ge007.log"), sizeof(logpath) - 1);
logpath[sizeof(logpath) - 1] = 0;
strncpy(prevpath, sysResolvePath("$S/ge007.prev.log"), sizeof(prevpath) - 1);
prevpath[sizeof(prevpath) - 1] = 0;
/* Roll the previous run's log to ge007.prev.log (drop any older
* one first); failures are ignored (std C has no portable stat). */
remove(prevpath);
rename(logpath, prevpath);
g_logFile = fopen(logpath, "w");
if (!g_logFile) {
/* Carry on with stderr only; at most one warn (no error spam). */
sysLogPrintf(LOG_WARNING, "log: cannot open %s, stderr only", logpath);
return;
}
g_logFileBytes = 0;
g_logFileCapped = 0;
atexit(sysLogCloseFile);
}
void sysLogPrintf(enum LogLevel level, const char *fmt, ...)
{
static const char *tags[] = { "ERROR", "WARN ", "NOTE ", "INFO ", "DEBUG" };
char buf[1024];
int off, n;
va_list ap;
if ((int)level >= 5) level = LOG_DEBUG;
fprintf(stderr, "[%s] ", tags[level]);
off = snprintf(buf, sizeof(buf), "[%s] ", tags[level]);
if (off < 0) off = 0;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
n = vsnprintf(buf + off, sizeof(buf) - (size_t)off - 1, fmt, ap);
va_end(ap);
fprintf(stderr, "\n");
if (n > 0)
off += n;
else
off = (int)strlen(buf); /* message truncated: advance past what fit */
if (off > (int)sizeof(buf) - 2) /* vsnprintf returns the untruncated length */
off = (int)sizeof(buf) - 2; /* room for '\n' + NUL */
buf[off++] = '\n';
buf[off] = 0;
fputs(buf, stderr);
sysLogWriteFile(buf);
}
void sysFatalError(const char *fmt, ...)
{
char buf[1024];
int off, n;
va_list ap;
off = snprintf(buf, sizeof(buf), "[FATAL] ");
if (off < 0) off = 0;
va_start(ap, fmt);
n = vsnprintf(buf + off, sizeof(buf) - (size_t)off - 1, fmt, ap);
va_end(ap);
if (n > 0)
off += n;
else
off = (int)strlen(buf);
if (off > (int)sizeof(buf) - 2) /* vsnprintf returns the untruncated length */
off = (int)sizeof(buf) - 2; /* room for '\n' + NUL */
buf[off++] = '\n';
buf[off] = 0;
fflush(stdout);
fflush(stderr);
fprintf(stderr, "[FATAL] ");
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "\n");
fputs(buf, stderr);
if (g_logFile) { /* the fatal line bypasses the size cap: it is the one that matters */
fputs(buf, g_logFile);
fflush(g_logFile);
}
fflush(stderr);
abort();
}
@@ -243,6 +382,13 @@ const char *sysGetExeDir(void)
*slash = 0;
}
}
#else
if (exeDir[0] == '.' && exeDir[1] == 0) {
char d[512];
getExeDir(d, sizeof(d));
if (d[0])
snprintf(exeDir, sizeof(exeDir), "%s", d);
}
#endif
return exeDir;
}
@@ -299,16 +445,7 @@ const char *sysResolvePath(const char *path)
else {
static char sexedir[1024] = "";
if (!sexedir[0]) {
ssize_t n = readlink("/proc/self/exe", sexedir, sizeof(sexedir) - 1);
if (n > 0) {
char *slash;
sexedir[n] = 0;
slash = strrchr(sexedir, '/');
if (slash)
*slash = 0;
else
sexedir[0] = 0;
}
getExeDir(sexedir, sizeof(sexedir));
}
if (sexedir[0])
snprintf(out, sizeof(out), "%s/data/%s", sexedir, path + 3);
+14 -52
View File
@@ -25,6 +25,7 @@
#include "config.h"
#include "versioninfo.h"
#include "updatecheck.h"
#include "updatecheck_parse.h"
#define UC_HOST "api.github.com"
/* /releases/latest deliberately excludes drafts and pre-releases (maintainer
@@ -33,7 +34,7 @@
#define UC_URL "https://" UC_HOST UC_PATH
#define UC_RELEASES "https://github.com/jkdansereau/goldeneye-pc-port/releases"
#define UC_UA "ge007-pc-port"
#define UC_BODY_MAX (64 * 1024)
#define UC_BODY_MAX (256 * 1024)
static int cfgCheckUpdates = 0; /* Game.CheckUpdates, default OFF */
static SDL_atomic_t s_ready; /* 1 once s_tag is published */
@@ -44,56 +45,6 @@ PD_CONSTRUCTOR static void updateCheckConfigInit(void)
configRegisterInt("Game.CheckUpdates", &cfgCheckUpdates, 0, 1);
}
/* Parse "vMAJOR.MINOR.PATCH" (missing parts = 0, suffix ignored). */
static int parseVer(const char *s, int v[3])
{
v[0] = v[1] = v[2] = 0;
if (*s == 'v' || *s == 'V') s++;
if (*s < '0' || *s > '9') return 0;
for (int i = 0; i < 3; i++) {
if (*s < '0' || *s > '9') break;
v[i] = (int)strtol(s, (char **)&s, 10);
if (*s != '.') break;
s++;
}
return 1;
}
static int versionNewer(const char *tag, const char *running)
{
int a[3], b[3];
if (!parseVer(tag, a) || !parseVer(running, b)) return 0;
for (int i = 0; i < 3; i++) {
if (a[i] != b[i]) return a[i] > b[i];
}
return 0;
}
/* Minimal scan for "tag_name": "<value>". */
static int extractTag(const char *body, char *out, size_t n)
{
const char *p = strstr(body, "\"tag_name\"");
if (!p) return 0;
p += 10;
while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++;
if (*p != ':') return 0;
p++;
while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++;
if (*p != '"') return 0;
p++;
size_t i = 0;
/* Tag characters only (no control/ANSI bytes reach the log). */
while (*p && *p != '"' && i + 1 < n) {
char c = *p++;
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
c == '.' || c == '_' || c == '-')) return 0;
out[i++] = c;
}
if (*p != '"' || i == 0) return 0;
out[i] = 0;
return 1;
}
#ifdef _WIN32
/* winhttp.dll is loaded at runtime, only here (after the Game.CheckUpdates
* gate), so the exe's import table never lists it and nothing networking-
@@ -174,9 +125,20 @@ static int fetchLatest(char *buf, size_t cap)
if (!f) return 0;
size_t got;
while (len + 1 < cap && (got = fread(buf + len, 1, cap - 1 - len, f)) > 0) len += got;
int full = 0;
if (len + 1 == cap) {
/* The body is longer than cap: drain the rest (discarded) so curl
* can write to EOF and exit cleanly instead of dying on SIGPIPE. */
full = 1;
char scratch[4096];
while (fread(scratch, 1, sizeof(scratch), f) > 0) { }
}
int rc = pclose(f); /* nonzero if curl is missing (127) or failed */
buf[len] = 0;
return rc == 0 && len > 0;
/* A full buffer is success: the tag field lives in the first bytes of
* the JSON, so a truncated body is still usable. Otherwise curl -f's
* nonzero rc on an HTTP error (with an empty body) must stand. */
return len > 0 && (rc == 0 || full);
#endif
}
+225 -16
View File
@@ -13,6 +13,9 @@
#include <string.h>
#include <math.h>
/* Prevent GCC's implicit declaration warning */
extern void exit(int status);
#if defined(_WIN32)
#include <direct.h>
#define GE_MKDIR(p) _mkdir(p)
@@ -57,7 +60,7 @@ static int initDone = 0;
* at modern resolutions without requiring 4x anti-aliasing.
*/
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 cfgFpsCap = -1; /* frame cap in fps; -1 = Auto (match display refresh, up to 4x the VI rate), 0 = uncapped (vsync); menu: Auto/30/60, and 90/120/144/165/240 = D578 interpolation */
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, 3 = trilinear (Trilinear option, playtest 2026-10-03) */
static int cfgFixMipTex = 1; /* RC2: clip mip-contaminated texture uploads to base height */
@@ -309,6 +312,37 @@ f32 portScaleFovY(f32 fovy, s32 isTitleScreen)
return fovy;
}
/* D953: tan(own/2) / tan(rendered/2), own = the game's fovy (viGetFovY, zoom
* included), rendered = c_perspfovy (what currentPlayerSetCameraScale built
* c_lodscalez from: portScaleFovY's widening, clamps and title exemption).
* c_lodscalez is tan(fovy/2)-proportional, so multiplying it by this gives
* the value the game would compute at its own FOV. Every RENDER consumer of
* c_lodscalez (model LOD, prop fog fade, prop fog-range visibility) uses it,
* so Video.FovScale widens the view without pulling detail swaps, fades and
* pop-in closer (x0.58 at 150) -- they are view-depth tests, so the pulled-in
* band swept over off-axis objects while turning. Frustum culling (D222)
* keeps the rendered value. 1.0f whenever the two are equal (all defaults).
* Cached on the pair (called per model node and per prop). */
f32 portLodScaleOwnFov(void)
{
extern f32 viGetFovY(void);
extern f32 getPlayer_c_perspfovy(void);
static f32 lastOwn = -1.0f, lastEff = -1.0f, lastK = 1.0f;
const f32 own = viGetFovY();
const f32 eff = getPlayer_c_perspfovy();
if (own == lastOwn && eff == lastEff) {
return lastK;
}
lastOwn = own;
lastEff = eff;
lastK = 1.0f;
if (own > 0.0f && eff > 0.0f && own != eff) {
const f32 h = 3.14159265f / 360.0f;
lastK = tanf(own * h) / tanf(eff * h);
}
return lastK;
}
/* D468: the factor portScaleFovY applies to an in-level fovy (clamps
* ignored), so gameplay can recover the game's own FOV from the rendered one.
* 1.0f at the defaults (native widescreen on, FovScale 100). */
@@ -540,7 +574,7 @@ PD_CONSTRUCTOR static void videoConfigInit(void)
configRegisterInt("Video.CrosshairHealthColor", &cfgCrosshairHealth, 0, 1); /* D511 */
configRegisterInt("Game.AllUnlocked", &portAllUnlocked, 0, 1);
configRegisterInt("Video.VSync", &cfgVSync, 0, 1);
configRegisterInt("Video.FpsCap", &cfgFpsCap, 0, 1000);
configRegisterInt("Video.FpsCap", &cfgFpsCap, -1, 1000);
configRegisterInt("Video.MSAA", &cfgMSAA, 1, 16); /* D443: 16x added */
configRegisterInt("Video.TextureFilter", &cfgTexFilter, 0, 3); /* Trilinear option (playtest 2026-10-03) */
configRegisterInt("Video.FixMipTextures", &cfgFixMipTex, 0, 1);
@@ -756,7 +790,9 @@ static void videoApplyLowEndDefaults(void)
* entry: key, N64 value, port default (the C initializer). The full audit,
* including the keys deliberately left OUT (MSAA, VSync, FpsCap, window /
* fullscreen, accuracy fixes, input feel, bindings, volumes, SkipIntro,
* AllUnlocked), is in findings D440.
* AllUnlocked, and the crosshair look -- Color/Size/Style and the D511
* Alpha/HealthColor: user aiming preferences, 2026-10-07), is in findings
* D440. Hide and Persistent stay: they decide WHEN the sight is drawn.
* Culling needs no key of its own: frustum-cull planes, the fog/LOD scale
* (D222) and the room-pool budget (D294, portRoomPoolScale) all derive from
* FovScale / widescreen / draw / LOD distance and are identity when those
@@ -779,9 +815,6 @@ static const struct { const char *key; double n64, port; } kVideoPresets[] = {
{ "Game.NoHitFlash", 0, 0 }, /* D232 */
{ "Video.CrosshairHide", 0, 0 }, /* D373 */
{ "Video.CrosshairPersistent", 0, 0 }, /* D436: aim mode only, as the N64 */
{ "Video.CrosshairColor", 0, 0 }, /* authored red sprite */
{ "Video.CrosshairSize", 100, 100 },
{ "Video.CrosshairStyle", 0, 0 },
{ "Input.AimMode", 0, 0 }, /* N64 aim (D337) */
{ "Input.AimRange", 1, 0 }, /* N64 aim limits (D338) */
};
@@ -803,6 +836,14 @@ int videoApplyPreset(int which)
videoRequestLiveConfigForKey(kVideoPresets[i].key);
changed++;
}
/* D578: Original N64 also drops an interpolated frame rate back to the
* console's (a lower cap such as 30 is left alone). Not a bundle key, so
* picking 120 does not flip Display mode to Custom. */
if (which == VIDEO_PRESET_N64 && (cfgFpsCap > 60 || cfgFpsCap < 0)) {
configSetValue("Video.FpsCap", 60);
if (initDone) videoRequestLiveConfigForKey("Video.FpsCap");
changed++;
}
sysLogPrintf(LOG_INFO, "video: applied %s preset (%d value(s) changed)",
which == VIDEO_PRESET_N64 ? "Original N64" : "port defaults", changed);
return changed;
@@ -1008,8 +1049,111 @@ static double fpsWindowStart = 0.0;
static int fpsNumFrames = 0;
static float vidAvgFPS = 0.f;
/* D578: frame interpolation. A cap above the console VI rate draws extra,
* matrix-blended frames between game frames (port/fast3d/gfx_pc.cpp; the
* render worker in port/src/libultra.c paces them). The sim stays at the VI
* rate. s_interpHz is the present rate the worker uses, 0 = off. Presents
* cannot outrun the display (VSync or a compositor holds the swap): the rate
* is held to the reported refresh, and interpolation is off when that is no
* faster than the VI rate (a 60 Hz panel). s_dumpBias: the worker presents a game frame after
* videoEndFrame counted it, so captures name it one lower. */
extern u32 osTvType; /* port/src/libultra.c */
static SDL_atomic_t s_interpHzAtomic;
static int s_dumpBias = 0;
static int s_dumpSlot = -1; /* D578: GE_PCDUMP_PRESENTS names each present */
/* Auto (Video.FpsCap = -1): match the display. Refresh unknown, or no faster
* than the sim (60 Hz panel, 59.94 reported as 59/60) -> the VI rate, i.e. the
* plain non-interpolated path; otherwise the refresh itself, up to the
* renderer's ceiling of 4 presents per sim tick (IP_SLOTS / 2 in libultra.c:
* the ring holds this frame + one still waiting), i.e. 240 NTSC / 200 PAL. */
int videoAutoFpsCap(void)
{
const int vi = (osTvType == 0) ? 50 : 60;
const int refresh = gfx_sdl_get_refresh_rate();
if (refresh <= vi + 5) return vi;
return refresh > 4 * vi ? 4 * vi : refresh;
}
static void videoApplyFpsCap(void)
{
static int lastHz = -1, lastTarget = -1;
const int vi = (osTvType == 0) ? 50 : 60; /* OS_TV_PAL; MPAL is 60 Hz (D488) */
const int cap = cfgFpsCap < 0 ? videoAutoFpsCap() : cfgFpsCap; /* effective cap */
int hz = 0;
if (cap > vi) {
const int refresh = gfx_sdl_get_refresh_rate();
hz = cap;
/* Presents cannot reach the screen faster than it refreshes: with VSync
* the swap waits for it, and without it a composited window (Windows
* DWM, measured ~8 ms per swap on a 60 Hz panel) still does. */
static int force = -1; /* GE_INTERP_FORCE: dev/test, ignore the refresh rate */
if (force < 0) force = getenv("GE_INTERP_FORCE") != NULL;
if (!force && refresh > 0 && refresh < hz) {
hz = refresh;
}
if (hz <= vi + 5) {
hz = 0; /* nothing to gain: a display no faster than the sim */
}
if (hz != lastHz) {
if (hz > 0) {
sysLogPrintf(LOG_INFO, "video: frame interpolation on: %d fps presents, %d Hz sim (FpsCap %d, display %d Hz, VSync %d)",
hz, vi, cfgFpsCap, refresh, cfgVSync);
} else {
sysLogPrintf(LOG_WARNING, "video: FpsCap=%d needs a display faster than %d Hz "
"(this one reports %d Hz); holding at %d fps", cfgFpsCap, vi, refresh, vi);
}
}
} else if (lastHz > 0) {
sysLogPrintf(LOG_INFO, "video: frame interpolation off (FpsCap %d)", cfgFpsCap);
}
lastHz = hz;
SDL_AtomicSet(&s_interpHzAtomic, hz);
/* Interpolation paces its own presents; otherwise the old cap (a cap
* above the VI rate without interpolation can only mean the VI rate). */
const int target = hz > 0 ? 0 : (cap > vi ? vi : cap);
if (target != lastTarget) {
lastTarget = target;
gfx_set_target_fps(target);
}
}
int videoInterpHz(void)
{
return SDL_AtomicGet(&s_interpHzAtomic);
}
int videoInterpVSync(void)
{
return cfgVSync != 0;
}
/* Render worker (GL thread): display refresh and the live GL swap interval. */
void videoInterpInfo(int *refreshHz, int *swapInterval)
{
*refreshHz = gfx_sdl_get_refresh_rate();
*swapInterval = wmAPI ? wmAPI->get_swap_interval() : -1;
}
int videoInit(void)
{
/* D578: fresh install (no Video.VSync in the ini) under gamescope / Steam
* Deck Game Mode: gamescope already syncs to the panel, and our own VSync
* path tops out ~75 fps there, so default VSync off. An ini that has the
* key (any user choice, or a prior run) is never touched. */
{
const char *gs = getenv("GAMESCOPE_WAYLAND_DISPLAY");
const char *sd = getenv("SteamDeck");
const int gamescope = (gs && *gs) || (sd && !strcmp(sd, "1"));
if (gamescope && !configIntKeyWasLoaded("Video.VSync")) {
cfgVSync = 0;
configSave(); /* configLoad already wrote VSync=1 for this fresh ini */
sysLogPrintf(LOG_NOTE, "video: gamescope/Steam Deck detected, fresh ini: Video.VSync defaulted to 0");
} else if (gamescope) {
sysLogPrintf(LOG_NOTE, "video: gamescope/Steam Deck detected; Video.VSync=%d kept from ini", cfgVSync);
}
}
/* D569: Crosshair color is Original (0) or Custom RGB (8), as in the PD
* port. Inis from v0.5.0 builds before this may hold a named preset 1..7:
* carry its colour into the Custom channels once, so nothing changes. */
@@ -1075,7 +1219,8 @@ int videoInit(void)
sysLogPrintf(LOG_WARNING, "video: Video.FpsCap=%d too low (throttles the sim); using 0 (uncapped)", cfgFpsCap);
cfgFpsCap = 0;
}
gfx_set_target_fps(cfgFpsCap); /* 0 = uncapped */
gfx_sdl_poll_refresh_rate(); /* D578: host thread, window exists */
videoApplyFpsCap(); /* 0 = uncapped; above the VI rate = D578 interpolation */
/* Texture filtering. 1 = bilinear (default, matches prior behaviour),
* 0 = crisp nearest, 2 = N64 3-point emulation + trilinear mips (opt-in;
@@ -1147,6 +1292,8 @@ int videoRestartRequested(void)
return s_restartReq;
}
int videoVSyncOn(void) { return cfgVSync != 0; } /* D600: the tick lock only engages with VSync */
int videoQuitRequested(void)
{
return SDL_AtomicGet(&s_quitReq) != 0;
@@ -1163,6 +1310,24 @@ static void videoRenderPark(void)
}
}
/* D632: host thread, from the atexit handler (after the window state and
* config are saved). Tears the GL context + window down only when the render
* thread is parked: it then holds no context and never touches GL again. On a
* park timeout the render thread may still be inside the driver, so deleting
* its context would be worse than leaving it to the kernel. */
void videoTeardownForExit(void)
{
if (!initDone) {
return;
}
if (!SDL_AtomicGet(&s_parked)) {
sysLogPrintf(LOG_WARNING, "video: render not parked; GL teardown skipped");
return;
}
gfx_sdl_teardown_for_exit();
sysLogPrintf(LOG_INFO, "video: GL context and window destroyed");
}
/* Host thread: exit once the render thread is out of the GL driver. */
static void videoHostExitIfRequested(void)
{
@@ -1170,8 +1335,15 @@ static void videoHostExitIfRequested(void)
return;
}
Uint32 start = SDL_GetTicks();
while (!SDL_AtomicGet(&s_parked) && SDL_AtomicGet(&s_inFrame) &&
SDL_GetTicks() - start < QUIT_WAIT_MS) {
/* D632: wait for the park itself, not just for "outside a frame". A
* quit landing between frames (window close, 2026-10-10: "render idle
* after 0 ms") used to exit at once, before the render thread had
* unbound its context, so the D632 teardown was skipped and the GL
* context fell to the kernel's process-exit cleanup -- the bugcheck
* path. Once s_quitReq is set the next frame start parks instead of
* drawing (see the ordering note above), so this takes about a frame;
* QUIT_WAIT_MS stays the bound if the render thread is stuck. */
while (!SDL_AtomicGet(&s_parked) && SDL_GetTicks() - start < QUIT_WAIT_MS) {
SDL_Delay(2);
}
sysLogPrintf(LOG_INFO, "video: exiting (render %s after %u ms)",
@@ -1210,6 +1382,23 @@ static float videoOutputAspect(void)
}
}
/* Render worker: show one interpolation present slot. Brackets the GL work
* as a frame for the D344 quit protocol (a quit parks here, never mid-swap). */
void videoInterpPresent(int slot)
{
SDL_AtomicSet(&s_inFrame, 1);
if (SDL_AtomicGet(&s_quitReq)) {
videoRenderPark();
}
s_dumpBias = -1;
s_dumpSlot = slot;
gfx_interp_present(slot, cfgVSync);
s_dumpSlot = -1;
s_dumpBias = 0;
SDL_AtomicSet(&s_inFrame, 0);
++fpsNumFrames;
}
void videoStartFrame(void)
{
if (!initDone) {
@@ -1234,7 +1423,6 @@ void videoStartFrame(void)
int dirty = SDL_AtomicSet(&liveCfgDirty, 0);
if (dirty) {
if (dirty & VCFG_VSYNC) wmAPI->set_swap_interval(cfgVSync ? 1 : 0);
if (dirty & VCFG_FPS) gfx_set_target_fps(cfgFpsCap);
if (dirty & VCFG_FILTER) videoApplyTexFilter();
if (dirty & VCFG_FOV) portFovScale = cfgFovScale / 100.0f;
if (dirty & VCFG_ANISO) gfx_set_anisotropy_level(cfgAniso);
@@ -1244,6 +1432,10 @@ void videoStartFrame(void)
dirty, cfgVSync, cfgFpsCap, cfgTexFilter, cfgFovScale, cfgAniso);
}
/* D578: every frame (cheap): FpsCap/VSync edits and a window moved to a
* display with another refresh rate all land here. */
videoApplyFpsCap();
gfx_set_output_aspect(videoOutputAspect());
gfx_start_frame();
}
@@ -1255,11 +1447,16 @@ void videoStartFrame(void)
* CREATED the window pumps them — and every game thread can be blocked on a
* message queue at any time. So the host main thread (which created the
* window in videoInit) must keep pumping; otherwise the window goes
* "Not Responding" and ESC/close never arrive. fast3d's own handle_events
* (which runs during rendering) remains as a backstop.
* "Not Responding" and ESC/close never arrive. On macOS AppKit strictly
* requires this main-thread pump, so fast3d's render-thread event handler is
* disabled there; on other platforms it remains as a backstop.
*/
void videoPumpEvents(void)
{
{ /* D578: keep the display refresh rate current (window moved, mode change) */
static unsigned pumpN = 0;
if ((pumpN++ % 120) == 0) gfx_sdl_poll_refresh_rate();
}
if (!initDone) {
return;
}
@@ -1393,10 +1590,15 @@ static void videoPreSwapCapture(void)
hi = 0x7fffffff;
GE_MKDIR("ppm");
}
if ((int)frames >= lo && (int)frames <= hi &&
((int)frames - lo) % step == 0) {
const int fr = (int)frames + s_dumpBias; /* D578 */
if (fr >= lo && fr <= hi && (fr - lo) % step == 0) {
char path[128];
snprintf(path, sizeof(path), "ppm/frame_%06d.ppm", (int)frames);
static int perPresent = -1; /* D578 dev: every present, not the last per frame */
if (perPresent < 0) perPresent = getenv("GE_PCDUMP_PRESENTS") != NULL;
if (perPresent && s_dumpSlot >= 0)
snprintf(path, sizeof(path), "ppm/frame_%06d_p%d.ppm", fr, s_dumpSlot);
else
snprintf(path, sizeof(path), "ppm/frame_%06d.ppm", fr);
gfx_opengl_dump_bound_fbo((uint32_t)gfx_current_window_dimensions.width,
(uint32_t)gfx_current_window_dimensions.height, path); /* D447: whole window, bars included */
}
@@ -1441,7 +1643,9 @@ void videoEndFrame(void)
SDL_AtomicSet(&s_inFrame, 0);
++frames;
++fpsNumFrames;
if (!SDL_AtomicGet(&s_interpHzAtomic)) {
++fpsNumFrames; /* D578: with interpolation, presents are counted */
}
double now = wmAPI->get_time();
if (fpsWindowStart == 0.0) {
@@ -1451,6 +1655,11 @@ void videoEndFrame(void)
vidAvgFPS = (float)(fpsNumFrames / (now - fpsWindowStart));
fpsNumFrames = 0;
fpsWindowStart = now;
{
static int s_perfOn = -1; /* D578: per-second present rate for stress sweeps */
if (s_perfOn < 0) s_perfOn = getenv("GE_PERFSTAT") != NULL;
if (s_perfOn) fprintf(stderr, "PERFSTAT presents=%.1f/s frame=%u\n", vidAvgFPS, (unsigned)frames);
}
}
}
+31 -6
View File
@@ -27,6 +27,9 @@ extern u16 musicTrack1GetVolume(void);
extern u16 get_mTrack2Vol(void);
extern void fileBuildWriteNewSave(u32 folder);
extern void set_cur_player_look_vertical_inverted(u32 value);
extern int inputControlScheme(void); /* D513: 0 Modern, 1 Original */
extern int inputPadLookInvert(void); /* D645 */
extern void inputSetPadLookInvert(int v);
extern void cur_player_set_aim_control(u32 value);
extern void cur_player_set_lookahead(u32 value);
extern void cur_player_set_sight_onscreen_control(u32 value);
@@ -623,13 +626,35 @@ void watchSettingsGameTick(void)
* during a level/menu transition. Each command's folder is re-checked
* against selected_folder_num at apply time, so a stale cross-file edit
* is still dropped, never misapplied. */
/* D564: the game's own "Look up/down" option is not exposed (the port's
* Mouse / Controller "Invert look" rows are the inversion controls). Pin it
* to its factory value (0, as a blank profile has it) so a value saved by an
* older build or set via the in-game watch menu cannot silently flip the
* pad stick on the game's pitch path. RAM only; one int compare per tick. */
if (stageActive() && g_CurrentPlayer && get_cur_player_look_vertical_inverted() != 0)
/* D564: the game's own "Look up/down" option is not exposed in the
* Modern scheme (the port's Mouse / Controller "Invert look" rows are
* the inversion controls there -- the port writes the view directly on
* those paths). Pin it to its factory value (0, as a blank profile has
* it) so a value saved by an older build or set via the in-game watch
* menu cannot silently flip the pad stick on the game's pitch path.
* #145 (v0.6.0): in the Original scheme the pin is LIFTED -- the game
* honours the setting in bondview2's stick-pitch path
* (moveData.invertPitch), so the in-game watch "Look up/down" row is
* the player's live control again, exactly as on the N64. RAM only;
* one int compare per tick. */
if (stageActive() && g_CurrentPlayer && inputControlScheme() == 0 &&
get_cur_player_look_vertical_inverted() != 0)
set_cur_player_look_vertical_inverted(0);
/* D645 (#145): in Original the F10 "Invert look" row and the watch
* "Look up/down" row are one setting. Whichever changed since the last
* tick wins; the game then applies it natively (C-buttons and the
* aim-mode stick), as on the N64. */
static int lastRow = -1, lastGame = -1;
if (!stageActive()) lastRow = -1; /* re-sync on each stage entry */
if (stageActive() && g_CurrentPlayer && inputControlScheme() == 1) {
int row = inputPadLookInvert(), game = get_cur_player_look_vertical_inverted() ? 1 : 0;
if (lastRow < 0) { /* stage entry: the F10 row (edited in the front end) wins */
if (row != game) set_cur_player_look_vertical_inverted(row);
} else if (row != lastRow && row != game) set_cur_player_look_vertical_inverted(row);
else if (game != lastGame && game != row) inputSetPadLookInvert(game);
lastRow = inputPadLookInvert();
lastGame = get_cur_player_look_vertical_inverted() ? 1 : 0;
}
if (!stageActive() || !g_CurrentPlayer) {
wsStageWasActive = 0;
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Convert ELF/PE GAS absolute symbols to Mach-O spelling.
Also accepts C-preprocessed input (`cc -E` on a .S, the dram_syms path on
Apple): gcc's `# N "file"` line markers pass through as harmless `#`
comments, and C-style integer suffixes in `.set` expressions are stripped
(`0x70000000UL` -> `0x70000000`) so the Mach-O assembler never has to parse
them."""
import pathlib
import re
import sys
def main() -> int:
argv = sys.argv[1:]
base = 0
if argv and argv[0] == "--base":
if len(argv) < 2:
print(f"usage: {sys.argv[0]} [--base N] INPUT OUTPUT", file=sys.stderr)
return 2
base = int(argv[1], 0)
argv = argv[2:]
if len(argv) != 2:
print(f"usage: {sys.argv[0]} [--base N] INPUT OUTPUT", file=sys.stderr)
return 2
source = pathlib.Path(argv[0])
output = pathlib.Path(argv[1])
lines = []
for line in source.read_text(encoding="utf-8").splitlines():
if re.match(r"^\s*\.section\s+\.data\s*$", line):
continue
match = re.match(r"^(\s*)\.global\s+([A-Za-z_]\w*)\s*$", line)
if match:
lines.append(f"{match.group(1)}.globl _{match.group(2)}")
continue
match = re.match(
r"^(\s*)\.set\s+([A-Za-z_]\w*)(\s*,.*)$", line
)
if match:
expr = re.sub(
r"\b(0[xX][0-9a-fA-F]+|\d+)(?:[uU][lL]{1,2}|[uUlL])\b", r"\1", match.group(3)
)
if base:
# Add the window base to the expression's first numeric
# literal (romassets lines are a single literal; dram lines
# never take --base — see the docstring).
expr = re.sub(
r"(0[xX][0-9a-fA-F]+|\d+)",
lambda m, _b=base: f"0x{(int(m.group(1), 0) + _b):X}",
expr,
count=1,
)
lines.append(
f"{match.group(1)}.set _{match.group(2)}{expr}"
)
continue
lines.append(line)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text("\n".join(lines) + "\n", encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env bash
#
# Package a macOS (Apple Silicon) GoldenEye 007 PC port build for distribution.
# Runs ON a Mac, after the build:
#
# scripts/package-macos.sh BUILD_DIR OUT_DIR [VERSION]
#
# BUILD_DIR cmake build directory containing ge007.aarch64
# OUT_DIR where to write the bundle + tarball (created if missing)
# VERSION defaults to `git describe`
#
# Produces, under OUT_DIR:
# goldeneye-pc-port-<VERSION>-macos-arm64/ the unpacked bundle
# goldeneye-pc-port-<VERSION>-macos-arm64.tar.gz + .tar.gz.sha256
#
# Same shape as tools_pc/bundle-linux.sh: engine exe, the non-system dylibs it
# needs (copied next to the exe, install names rewritten to @executable_path /
# @loader_path so nothing resolves into Homebrew), README, license texts, the
# prepare-assets tool and tools/eep_convert.py. NO ROM and NO game data.
# Every touched Mach-O is re-signed ad-hoc (mandatory on arm64 after
# install_name_tool). The bundle is NOT notarized; see the README it writes.
#
# Env: GE_REPO_ROOT overrides the repo root (default: parent of this script).
# GE_ALLOW_NO_CONVERTER=1 permits packaging without the PyInstaller-frozen
# ge007-convert (local testing only; CI always freezes it).
set -euo pipefail
[ "$(uname -s)" = "Darwin" ] || { echo "error: this script runs on macOS only" >&2; exit 1; }
[ $# -ge 2 ] || { echo "usage: $0 BUILD_DIR OUT_DIR [VERSION]" >&2; exit 1; }
BUILD_DIR="$1"; OUT_DIR="$2"
[ -d "$BUILD_DIR" ] || { echo "error: build dir not found: $BUILD_DIR" >&2; exit 1; }
mkdir -p "$OUT_DIR"
BUILD_DIR="$(cd "$BUILD_DIR" && pwd)"; OUT_DIR="$(cd "$OUT_DIR" && pwd)"
REPO_ROOT="${GE_REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
cd "$REPO_ROOT"
VERSION="${3:-$(git describe --tags --always --dirty 2>/dev/null || echo 0.0.0-dev)}"
VERSION="${VERSION#v}"
NAME="goldeneye-pc-port-${VERSION}-macos-arm64"
OUT="${OUT_DIR}/${NAME}"
EXE=""
for cand in "$BUILD_DIR"/ge007.aarch64 "$BUILD_DIR"/ge007*; do
if [ -f "$cand" ] && [ -x "$cand" ]; then EXE="$cand"; break; fi
done
[ -n "$EXE" ] || { echo "error: no ge007 binary in $BUILD_DIR - build first" >&2; exit 1; }
EXE_NAME="$(basename "$EXE")"
file "$EXE" | grep -q arm64 || { echo "error: $EXE is not an arm64 Mach-O" >&2; exit 1; }
echo "==> Bundling $EXE -> $NAME"
rm -rf "$OUT"
mkdir -p "$OUT/licenses"
cp "$EXE" "$OUT/$EXE_NAME"
chmod 755 "$OUT/$EXE_NAME"
# --- dylib bundling ----------------------------------------------------
is_system() { case "$1" in /usr/lib/*|/System/*) return 0 ;; *) return 1 ;; esac; }
deps_of() { otool -L "$1" | tail -n +2 | awk '{print $1}'; }
rpaths_of() { otool -l "$1" | awk '/cmd LC_RPATH/ {f=1} f && /path / {print $2; f=0}'; }
# Resolve an install name to a real file, searching the dependent's rpaths and
# the Homebrew gcc runtime dir (libgcc_s / libstdc++ are @rpath-linked there).
resolve() {
local dep="$1" from="$2" r cand
case "$dep" in
@rpath/*)
for r in $(rpaths_of "$from"); do
cand="${r/@loader_path/$(dirname "$from")}"
cand="${cand/@executable_path/$(dirname "$from")}/${dep#@rpath/}"
[ -f "$cand" ] && { echo "$cand"; return 0; }
done
for cand in /opt/homebrew/opt/gcc/lib/gcc/current/"${dep#@rpath/}" \
/opt/homebrew/lib/"${dep#@rpath/}" /usr/local/lib/"${dep#@rpath/}"; do
[ -f "$cand" ] && { echo "$cand"; return 0; }
done
return 1 ;;
@loader_path/*)
cand="$(dirname "$from")/${dep#@loader_path/}"
[ -f "$cand" ] && { echo "$cand"; return 0; }
return 1 ;;
/*) [ -f "$dep" ] && { echo "$dep"; return 0; }; return 1 ;;
esac
return 1
}
BUNDLED=" "
SIGN_LIST=()
bundle_dep() { # $1 = file (already inside $OUT) whose deps to process; $2 = new-name prefix
local file="$1" prefix="$2" dep real base
while IFS= read -r dep; do
is_system "$dep" && continue
case "$dep" in "@executable_path/"*|"@loader_path/"*) continue ;; esac
real="$(resolve "$dep" "$file")" || { echo "error: cannot resolve $dep (needed by $file)" >&2; exit 1; }
real="$(cd "$(dirname "$real")" && pwd -P)/$(basename "$real")"
base="$(basename "$dep")"
if [[ "$BUNDLED" != *" $base "* ]]; then
BUNDLED+="$base "
cp -L "$real" "$OUT/$base"; chmod 644 "$OUT/$base"
install_name_tool -id "@loader_path/$base" "$OUT/$base" 2>/dev/null
echo " + $base (from $real)"
SIGN_LIST+=("$OUT/$base")
bundle_dep "$OUT/$base" "@loader_path"
fi
install_name_tool -change "$dep" "$prefix/$base" "$file" 2>/dev/null
done < <(deps_of "$file")
}
# Drop any rpath that points outside the bundle (Homebrew, build tree).
strip_rpaths() { local r; for r in $(rpaths_of "$1"); do install_name_tool -delete_rpath "$r" "$1" 2>/dev/null; done; }
# Homebrew's sdl2 is sdl2-compat: libSDL2 dlopen()s @loader_path/libSDL3.dylib at runtime, so it
# is not in otool -L. Bundle it too; sdl2-compat looks next to itself first.
bundle_sdl3_if_needed() {
local sdl2 sdl3="" d
sdl2="$(ls "$OUT"/libSDL2-*.dylib 2>/dev/null | head -1 || true)"
[ -n "$sdl2" ] || return 0
strings -a "$sdl2" | grep -q 'libSDL3' || return 0
for d in /opt/homebrew/opt/sdl3/lib /opt/homebrew/lib /usr/local/lib; do
if [ -f "$d/libSDL3.0.dylib" ]; then sdl3="$d/libSDL3.0.dylib"; break; fi
done
[ -n "$sdl3" ] || { echo "error: sdl2-compat found but libSDL3.0.dylib is not installed" >&2; exit 1; }
cp -L "$sdl3" "$OUT/libSDL3.dylib"; chmod 644 "$OUT/libSDL3.dylib"
install_name_tool -id "@loader_path/libSDL3.dylib" "$OUT/libSDL3.dylib" 2>/dev/null
echo " + libSDL3.dylib (sdl2-compat runtime dependency, from $sdl3)"
SIGN_LIST+=("$OUT/libSDL3.dylib")
bundle_dep "$OUT/libSDL3.dylib" "@loader_path"
}
bundle_dep "$OUT/$EXE_NAME" "@executable_path"
bundle_sdl3_if_needed
strip_rpaths "$OUT/$EXE_NAME"
for f in ${SIGN_LIST[@]+"${SIGN_LIST[@]}"}; do strip_rpaths "$f"; done
# Ad-hoc sign everything we rewrote (libs first, exe last). Without this an
# arm64 binary is killed on launch ("Killed: 9").
for f in ${SIGN_LIST[@]+"${SIGN_LIST[@]}"} "$OUT/$EXE_NAME"; do
codesign --force -s - "$f" 2>&1 | sed 's/^/ /'
done
# Guard: nothing may still point into Homebrew / the build machine.
for f in ${SIGN_LIST[@]+"${SIGN_LIST[@]}"} "$OUT/$EXE_NAME"; do
if otool -L "$f" | tail -n +2 | awk '{print $1}' | grep -Eq '^(/opt/|/usr/local/|/Users/|/private/|@rpath/)'; then
echo "error: $f still references a non-system path:" >&2; otool -L "$f" >&2; exit 1
fi
codesign --verify "$f" || { echo "error: bad signature on $f" >&2; exit 1; }
done
echo " remaining system dependencies:"
otool -L "$OUT/$EXE_NAME" | tail -n +2 | awk '{print " " $1}' | grep -v '@executable_path' || true
# --- docs + licenses ---------------------------------------------------
DEPS_BLOCK=$'## macOS (Apple Silicon, experimental)\n\nThis is an experimental build for Macs with Apple Silicon (M1 or later,\nmacOS @MINOS@ or newer). SDL2 and the C++ runtime are bundled in this folder and\nfound automatically; nothing needs installing, and Homebrew is not required.\n\nThe app is not signed with an Apple Developer ID or notarized, so Gatekeeper\nblocks it when this folder came from a download. After unpacking, open Terminal\nin this folder and run, once:\n\n xattr -dr com.apple.quarantine .\n\nThen run `./@EXE@` from Terminal.\n'
MINOS="$(for f in ${SIGN_LIST[@]+"${SIGN_LIST[@]}"} "$OUT/$EXE_NAME"; do otool -l "$f" | awk '/LC_BUILD_VERSION/ {f=1} f && /minos/ {print $2; exit}'; done | sort -V | tail -1)"
echo " minimum macOS (highest minos of the bundled Mach-Os): $MINOS"
DEPS_BLOCK="${DEPS_BLOCK//@EXE@/$EXE_NAME}"
DEPS_BLOCK="${DEPS_BLOCK//@MINOS@/$MINOS}"
sed -e "s|@VERSION@|${VERSION}|g" \
-e "s|@PLATFORM@|macOS Apple Silicon (arm64, experimental)|g" \
-e "s|@EXE@|${EXE_NAME}|g" \
-e "s|@LICENSE_EXTRA@||g" \
tools_pc/dist/README.md.in > "$OUT/README.md.tmp"
# BSD awk rejects newlines in -v; pass the block via the environment.
DEPS_BLOCK="$DEPS_BLOCK" awk '{ if ($0 == "@DEPS@") print ENVIRON["DEPS_BLOCK"]; else if ($0 == "@DECK@") print ""; else print }' \
"$OUT/README.md.tmp" > "$OUT/README.md"
rm -f "$OUT/README.md.tmp"
cp NOTICE "$OUT/licenses/NOTICE"
cp LICENSE "$OUT/licenses/LICENSE-port-MIT.txt"
[ -f port/fast3d/LICENSE.txt ] && cp port/fast3d/LICENSE.txt "$OUT/licenses/LICENSE-fast3d.txt"
# SDL2 is zlib-licensed; carry its text when the Homebrew prefix has one.
for d in /opt/homebrew/opt/sdl2/LICENSE.txt /opt/homebrew/opt/sdl2-compat/LICENSE.txt /opt/homebrew/opt/sdl2/share/licenses/*/LICENSE.txt; do
if [ -f "$d" ]; then cp "$d" "$OUT/licenses/SDL2.txt"; break; fi
done
# sdl2-compat loads the bundled libSDL3 (zlib); carry its text too.
for d in /opt/homebrew/opt/sdl3/LICENSE.txt /opt/homebrew/opt/sdl3/share/licenses/*/LICENSE.txt; do
if [ -f "$d" ]; then cp "$d" "$OUT/licenses/SDL3.txt"; break; fi
done
# The bundled libgcc_s / libstdc++ are GPL-3.0 with the GCC Runtime Library
# Exception. The Homebrew gcc keg carries only GPL-2.0 as COPYING, so both
# texts ship from the tree.
cp tools_pc/dist/GPL-3.0.txt "$OUT/licenses/gcc-COPYING3.txt"
cp tools_pc/dist/GCC-RUNTIME-EXCEPTION.txt "$OUT/licenses/gcc-COPYING.RUNTIME.txt"
ls "$OUT/licenses"
# --- save converter + asset-prep tool (same assembly as bundle-linux.sh) ---
mkdir -p "$OUT/tools"
cp tools_pc/eep_convert.py "$OUT/tools/"
PREP="$OUT/prepare-assets"
mkdir -p "$PREP/vendor/scripts" "$PREP/vendor/assets/obseg"
cp tools_pc/dist/prepare-assets/prepare-assets.py "$PREP/"
cp tools_pc/d43_emit.py tools_pc/d69_emit.py tools_pc/d88_emit.py tools_pc/d88_propdefs.py "$PREP/"
cp scripts/filelist.u.csv "$PREP/vendor/scripts/"
cp assets/obseg/file_resource_table.inc.c "$PREP/vendor/assets/obseg/"
# BSD cp has no --parents; cpio -pdm recreates the relative paths.
find assets -iname 'modelfileheader.inc.c' -print | cpio -pdm "$PREP/vendor" 2>/dev/null
nmh="$(find "$PREP/vendor/assets" -iname 'modelfileheader.inc.c' | wc -l | tr -d ' ')"
echo " + prepare-assets/ (emit scripts + $nmh model headers)"
[ "$nmh" -gt 400 ] || { echo "error: prepare-assets vendored only $nmh model headers - expected ~512" >&2; exit 1; }
CV="$REPO_ROOT/tools_pc/dist/prepare-assets/ge007-convert"
if [ -f "$CV" ]; then
cp "$CV" "$PREP/"; chmod 755 "$PREP/ge007-convert"
codesign --force -s - "$PREP/ge007-convert" 2>&1 | sed 's/^/ /' || true
echo " + prepare-assets/ge007-convert ($(du -h "$CV" | cut -f1))"
elif [ "${GE_ALLOW_NO_CONVERTER:-0}" = "1" ]; then
echo " ! ge007-convert missing (allowed by GE_ALLOW_NO_CONVERTER=1)"
else
echo "error: $CV not found - freeze it with PyInstaller first (see ci.yml macos-build)" >&2; exit 1
fi
# --- guards ------------------------------------------------------------
if find "$OUT" -type f \( -iname '*.z64' -o -iname '*.n64' -o -iname '*.v64' \) | grep -q .; then
echo "error: bundle contains a ROM image - aborting" >&2; exit 1
fi
KIB="$(du -sk "$OUT" | cut -f1)"
[ "$KIB" -le $((120 * 1024)) ] || { echo "error: bundle is ${KIB} KiB (> 120 MB) - likely contains game data" >&2; exit 1; }
# --- tarball + checksum ------------------------------------------------
# COPYFILE_DISABLE keeps AppleDouble ._* files out of the archive.
( cd "$OUT_DIR" && rm -f "${NAME}.tar.gz" "${NAME}.tar.gz.sha256" \
&& COPYFILE_DISABLE=1 tar -czf "${NAME}.tar.gz" "$NAME" \
&& shasum -a 256 "${NAME}.tar.gz" > "${NAME}.tar.gz.sha256" )
echo "==> ${OUT_DIR}/${NAME}.tar.gz ($(du -h "${OUT_DIR}/${NAME}.tar.gz" | cut -f1))"
cat "${OUT_DIR}/${NAME}.tar.gz.sha256"
( cd "$OUT_DIR" && find "$NAME" -type f | sort )
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
"""Remove ELF-style weak aliases from a C source copied for Mach-O."""
import pathlib
import re
import sys
def main() -> int:
if len(sys.argv) != 3:
print(f"usage: {sys.argv[0]} INPUT OUTPUT", file=sys.stderr)
return 2
source = pathlib.Path(sys.argv[1])
output = pathlib.Path(sys.argv[2])
text = source.read_text(encoding="utf-8")
text = re.sub(r"^[ \t]*#pragma[ \t]+weak[^\n]*\n", "", text,
flags=re.MULTILINE)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(text, encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+3 -3
View File
@@ -7,7 +7,7 @@
#include "speed_graph.h"
#if defined(PORT)
#include <string.h> /* PC port: memcpy for the CUSTOM_FX_PARAMS_N copy in audioInit (docs/dev/findings.md §11) */
#if defined(__x86_64__)
#if defined(PLATFORM_64BIT)
#include <stdio.h>
#include <stdlib.h>
#ifdef PORT
@@ -666,7 +666,7 @@ s32 amDmaCallback(s32 addr, s32 len, void* state)
/* mark it used */
dmaPtr->lastFrame = (s32) g_AudioFrameCount;
freeBuffer = (dmaPtr->ptr + addr) - dmaPtr->startAddr;
#if defined(__x86_64__)
#if defined(PLATFORM_64BIT)
if (GE_ENVFLAG("GE_MIXERTRACE")) {
static FILE *dtf2 = NULL;
if (!dtf2) { dtf2 = fopen("mixertrace.log", "a"); if (dtf2) setvbuf(dtf2, NULL, _IONBF, 0); }
@@ -734,7 +734,7 @@ s32 amDmaCallback(s32 addr, s32 len, void* state)
dmaPtr->lastFrame = g_AudioFrameCount;
osPiStartDma(&g_DmaIOMessageBuffer[g_NextDMa++], OS_MESG_PRI_HIGH, OS_READ, (u32)addr, freeBuffer, AUDIO_DMA_MAX_BUFFER_LENGTH, &g_DmaMessageQueue);
#if defined(__x86_64__)
#if defined(PLATFORM_64BIT)
if (GE_ENVFLAG("GE_MIXERTRACE")) {
static FILE *dtf3 = NULL;
if (!dtf3) { dtf3 = fopen("mixertrace.log", "a"); if (dtf3) setvbuf(dtf3, NULL, _IONBF, 0); }
+9 -3
View File
@@ -745,11 +745,17 @@ typedef union
/* PC port (D43/D45): stored as a raw vma (0x05xxxxxx) instead of a real
* pointer so sizeof(Vertex) stays 16 bytes — the gSPVertex stride is
* hardcoded to 16B and propobj.c copies vertices with sizeof(Vertex).
* modelPromoteNodeOffsetsToPointers rebases it via PROMOTE32; chr.c casts
* back to ModelNode* at the use site. N64 layout unchanged (4B either way). */
* modelPromoteNodeOffsetsToPointers rebases it via PROMOTE32, which is
* `var = (u32)((u32)var + diff)` — note that unlike PROMOTE it keeps the
* field a 32-bit N64 ADDRESS, it does NOT produce a host pointer.
* MACOS-BLOODSTAIN: consumers must therefore re-base it (PORT_N64PTR), never cast it
* straight to a pointer — a bare cast leaves 0x7070_xxxx and faults on
* arm64, where the window is based at PORT_ADDR_BASE.
* N64 layout unchanged (4B either way). */
u32 LinkedTo;
/* Collision vertices only: points to related model node (raw vma). */
/* Collision vertices only: related model node, as a raw vma — same union
* storage as LinkedTo above, so the same PORT_N64PTR rule applies. */
u32 CollisionRelatedNode; /* 0x8 */
#else
struct Vertex *LinkedTo;
+29
View File
@@ -43,6 +43,12 @@
#include "game/player.h"
#include "game/frametiming.h"
#include "PR/R4300.h"
#if defined(PORT)
#include "portaddr.h"
#else
/* N64 build: the port address window is the identity (see port_addr.h). */
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
/**
* @file boss.c
@@ -165,7 +171,18 @@ void bossInitMainthreadData(void)
OSMesg bossmsg;
OSTimer bosstimer;
OSMesgQueue bossmq;
#ifdef PORT
/* D611 (macos branch): the mempool arena start is a live host pointer,
* not a 32-bit address, once PORT_ADDR_BASE != 0 (arm64 macOS: the 4 GiB
* __PAGEZERO forbids the base-0 view, D609). Same class as D453, which
* widened mempCheckMemflagTokens' own parameter to uintptr_t but left
* this caller's carrier truncating. Identity at PORT_ADDR_BASE == 0
* (see the assignment below: the N64 expression is the identity on any
* address below 4 GiB). */
uintptr_t start;
#else
u32 start;
#endif
u32 unused;
s32 i;
@@ -261,7 +278,19 @@ void bossInitMainthreadData(void)
g_CurentMMallocValue = (s32) (strtol(tokenFind(1, "-m"), 0, 0) << 0xa);
}
#ifdef PORT
/* D611: the N64 expression round-trips the address through two 32-bit
* carriers (osVirtualToPhysical returns u32; PHYS_TO_K0 is (u32)(x)),
* which is lossless only while the arena sits below 4 GiB. Take the
* committed address directly - dram_syms.S defines _bssSegmentEnd as
* PORT_DRAM_V1_BASE + 0x50000, i.e. the same value the N64 expression
* yields at PORT_ADDR_BASE == 0. The size argument below keeps its
* (u32) operands: both carriers truncate consistently, so the extent
* (block - start) is base-invariant. */
start = (uintptr_t)&_bssSegmentEnd;
#else
start = (PHYS_TO_K0(osVirtualToPhysical(&_bssSegmentEnd)));
#endif
mempCheckMemflagTokens(start, ((u32)tlbmanageGetTlbAllocatedBlock() - (u32)start));
mempResetBank(MEMPOOL_PERMANENT);
langInit();
+28 -7
View File
@@ -8,6 +8,12 @@
#include <memp.h>
#include "bg.h"
#include "lightfixture.h"
#if defined(PORT)
#include "portaddr.h"
#else
/* N64 build: the port address window is the identity (see port_addr.h). */
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
#include "bondview.h"
#include "chr.h"
#include "debug_camera.h"
@@ -883,6 +889,20 @@ void load_bg_file(LEVEL_INDEX levelid)
lightFixtureInitTables();
ptr_bg_data = (bg_addr_t)header;
#ifdef PORT
/* D590 mechanism check (GE_D590HDR-gated): prove `header` (a stack array on
* the bg-load worker thread) has its low 32 bits inside the D131 image-rel
* range [0x40M,0x70M) — i.e. that the dropped PORT_N64PTR re-base was what
* remapped this stack address onto the PE image (fault = imagebase +
* (low32 - 0x40M)). If low32 is OUTSIDE the window, the D588-class
* diagnosis is wrong — stop and report. */
if (getenv("GE_D590HDR"))
{
uint32_t hdr_lo = (uint32_t)(uintptr_t)header;
osSyncPrintf("D590HDR header=%p low32=0x%08x in-window=%d\n", (void *)header,
(unsigned)hdr_lo, (unsigned)(hdr_lo >= 0x40000000u && hdr_lo < 0x70000000u));
}
#endif
obLoadBGFileBytesAtOffset(levelinfotable[levelentry_index].bg_seg_filename, (u8 *) ptr_bg_data, 0, 0x40);
if (((levelid && ptr_bg_data) && levelentry_index));
@@ -897,8 +917,13 @@ void load_bg_file(LEVEL_INDEX levelid)
gptr_stan = (bg_addr_t) _fileNameLoadToBank(levelinfotable[levelentry_index].bg_stan_filename, 2, 0, 4);
stanDetermineEOF((struct StanPrefixRecord *) gptr_stan, 0, (u8 *) gptr_stan);
stanLoadFile((struct StanPrefixRecord *) gptr_stan);
/* D588-class (ABI/pointer-width, §A1): under PORT, ptr_bg_data/gptr_stan are
* full-width host bg_addr_t (D457) — PORT_N64PTR here truncated the low 32
* bits and re-based them into the image window, so a bg-load worker stack
* (or any bank pointer inside the window) faulted. Plain cast: identity
* under N64, correct under PORT. */
stanDetermineEOF((struct StanPrefixRecord *)gptr_stan, 0, (u8 *)gptr_stan);
stanLoadFile((struct StanPrefixRecord *)gptr_stan);
sub_GAME_7F0B4810(levelinfotable[levelentry_index].levelscale);
setLevelScale(levelinfotable[levelentry_index].levelscale);
@@ -3082,11 +3107,7 @@ void bgBuildRoomVtxBounds(s32 roomID)
numvertices = ((gdl[cmdindex].dma.par >> 4) & 0xf) + 1;
#endif
#ifdef PORT
vtx = (Vtx *)(SEGMENT_OFFSET(gdl[cmdindex].dma.addr) + (uintptr_t)vertices);
#else
vtx = (Vtx *)(SEGMENT_OFFSET(gdl[cmdindex].dma.addr) + (u32)vertices);
#endif
vtx = (Vtx *)PORT_N64PTR(void, SEGMENT_OFFSET(gdl[cmdindex].dma.addr) + (u32)vertices);
#if defined(PORT)
/* TEMP D69 safety net: the room primary/secondary DL binaries
+5
View File
@@ -1,4 +1,9 @@
#include <ultra64.h>
#if defined(PORT)
#include "portaddr.h"
#else
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
#include <limits.h>
#include <assets/animationtable_data.h>
#include <random.h>
+54 -26
View File
@@ -1,4 +1,9 @@
#include <ultra64.h>
#if defined(PORT)
#include "portaddr.h"
#else
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
#ifdef PORT
#include <stdio.h>
#include <stdlib.h>
@@ -93,8 +98,15 @@
#if defined(VERSION_US)
/* MACOS-ADDR-WINDOW/M2: the font-table globals are s32 holding truncated pointers
* (D88 class); re-base on use. Identity at PORT_ADDR_BASE == 0. */
#if defined(PORT)
#define BONDVIEW_2ND_FONTTABLE(_param) PORT_N64PTR(void, copy_2ndfonttable)
#define BONDVIEW_1ST_FONTTABLE(_param) PORT_N64PTR(void, copy_1stfonttable)
#else
#define BONDVIEW_2ND_FONTTABLE(_param) copy_2ndfonttable
#define BONDVIEW_1ST_FONTTABLE(_param) copy_1stfonttable
#endif
#elif defined(VERSION_JP) || defined(VERSION_EU)
#define BONDVIEW_2ND_FONTTABLE(_param) dword_CODE_bss_jp80079CEC[_param]
#define BONDVIEW_1ST_FONTTABLE(_param) dword_CODE_bss_jp80079Cd8[_param]
@@ -643,11 +655,7 @@ 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
something_with_generating_object(self, prop, item, 0, PORT_N64PTR(WeaponObjRecord, helddst), (ItemModelFileRecord *)pitemheader);
}
chrlvMergeKneelToStand(self, 0.0f);
@@ -1145,7 +1153,15 @@ void bondviewCalcIntroSwirlCamera(s32 index, f32 time, coord3d *pos, coord3d *lo
{
struct SetupIntroSwirl *base;
struct SetupIntroSwirl *loopbase;
#if defined(PORT)
/* MACOS-ADDR-WINDOW: written pointbuf[0..11] via `&pointbuf[i*3]` then dst[3..5]
* (i = -1..2), but declared [10] -- a 2-float stack overrun that on arm64
* corrupts the saved frame pointer (Fp=0x374d56eac1a00000 at the fault).
* N64/MinGW absorb it; give it room on the port. */
f32 pointbuf[12];
#else
f32 pointbuf[10];
#endif
struct SetupIntroSwirl *swirl;
f32 frac;
f32 *dst;
@@ -1294,13 +1310,13 @@ void bondviewFrozenCameraTick(u16 buttons, u16 oldbuttons, struct coord3d *pos,
#if defined(VERSION_US)
setFontTables(ptrFontZurichBoldChars, ptrFontZurichBold);
#ifdef PORT
hudmsgBottomShow((char *)(uintptr_t)ptr_random06cam_entry->lang1c.lang_ptr);
hudmsgBottomShow(PORT_N64PTR(char, ptr_random06cam_entry->lang1c.lang_ptr));
#else
hudmsgBottomShow(ptr_random06cam_entry->lang1c.lang_ptr);
#endif
#else
#ifdef PORT
hudmsgBottomShow((char *)(uintptr_t)ptr_random06cam_entry->lang1c.lang_ptr, ptrFontZurichBoldChars, ptrFontZurichBold);
hudmsgBottomShow(PORT_N64PTR(char, ptr_random06cam_entry->lang1c.lang_ptr), ptrFontZurichBoldChars, ptrFontZurichBold);
#else
hudmsgBottomShow(ptr_random06cam_entry->lang1c.lang_ptr, ptrFontZurichBoldChars, ptrFontZurichBold);
#endif
@@ -1313,13 +1329,13 @@ void bondviewFrozenCameraTick(u16 buttons, u16 oldbuttons, struct coord3d *pos,
{
#if defined(VERSION_US)
#ifdef PORT
hudmsgBottomShow((char *)(uintptr_t)ptr_random06cam_entry->lang20.lang_ptr);
hudmsgBottomShow(PORT_N64PTR(char, ptr_random06cam_entry->lang20.lang_ptr));
#else
hudmsgBottomShow(ptr_random06cam_entry->lang20.lang_ptr);
#endif
#else
#ifdef PORT
hudmsgBottomShow((char *)(uintptr_t)ptr_random06cam_entry->lang20.lang_ptr, ptrFontZurichBoldChars, ptrFontZurichBold);
hudmsgBottomShow(PORT_N64PTR(char, ptr_random06cam_entry->lang20.lang_ptr), ptrFontZurichBoldChars, ptrFontZurichBold);
#else
hudmsgBottomShow(ptr_random06cam_entry->lang20.lang_ptr, ptrFontZurichBoldChars, ptrFontZurichBold);
#endif
@@ -7862,6 +7878,7 @@ void bondviewFrozenMoveBond(s8 stick_x, s8 stick_y, u16 buttons, u16 oldbuttons)
return;
}
room_pointer_tile = (struct StandTile *)0;
bondviewFrozenCameraTick(buttons, oldbuttons, &property_pos, &property_pos2, &property_offset, &room_pointer_tile, &stan_walk_start);
currentPlayerSetCameraMode(1);
bondviewSetCurrentPlayerPosition(&property_pos, &property_pos2, &property_offset, room_pointer_tile, &stan_walk_start);
@@ -8102,11 +8119,7 @@ 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
viSetFrameBuf2(PORT_N64PTR(u8, resolution));
}
#ifdef VERSION_EU
@@ -8341,12 +8354,12 @@ void bondviewUpdateCameraMatrices(coord3d* cam_pos, coord3d* cam_look_dir, coord
clpos.x, clpos.y, clpos.z,
cam_up->x, cam_up->y, cam_up->z);
matrix_4x4_set_lookat((Mtxf*) g_CurrentPlayer->field_64,
matrix_4x4_set_lookat(PORT_N64PTR(Mtxf, g_CurrentPlayer->field_64),
cam_pos->x, cam_pos->y, cam_pos->z,
cam_look_dir->x, cam_look_dir->y, cam_look_dir->z,
cam_up->x, cam_up->y, cam_up->z);
matrix_4x4_set_basis_and_position((Mtxf*) g_CurrentPlayer->field_68,
matrix_4x4_set_basis_and_position(PORT_N64PTR(Mtxf, g_CurrentPlayer->field_68),
cam_pos->x, cam_pos->y, cam_pos->z,
cam_look_dir->x, cam_look_dir->y, cam_look_dir->z,
cam_up->x, cam_up->y, cam_up->z);
@@ -8381,13 +8394,13 @@ void bondviewUpdateCameraMatrices(coord3d* cam_pos, coord3d* cam_look_dir, coord
scale = bgGetLevelVisibilityScale();
matrix_scalar_multiply(scale, spC4.m[0]);
guMtxF2L((f32 (*)[4]) &spC4, (Mtx* ) g_CurrentPlayer->field_5C);
sub_GAME_7F059334((s32* ) g_CurrentPlayer->field_5C, (s32* ) g_CurrentPlayer->field_60);
guMtxF2L((f32 (*)[4]) &spC4, PORT_N64PTR(Mtx, g_CurrentPlayer->field_5C));
sub_GAME_7F059334(PORT_N64PTR(s32, g_CurrentPlayer->field_5C), PORT_N64PTR(s32, g_CurrentPlayer->field_60));
currentPlayerSetMatrix10C8((Mtx* ) g_CurrentPlayer->field_5C);
currentPlayerSetMatrix10C4((Mtx* ) g_CurrentPlayer->field_60);
currentPlayerSetMatrix10CC((Mtxf* ) g_CurrentPlayer->field_64);
currentPlayerSetViewToWorldMtxf((Mtxf* ) g_CurrentPlayer->field_68);
currentPlayerSetMatrix10C8(PORT_N64PTR(Mtx, g_CurrentPlayer->field_5C));
currentPlayerSetMatrix10C4(PORT_N64PTR(Mtx, g_CurrentPlayer->field_60));
currentPlayerSetMatrix10CC(PORT_N64PTR(Mtxf, g_CurrentPlayer->field_64));
currentPlayerSetViewToWorldMtxf(PORT_N64PTR(Mtxf, g_CurrentPlayer->field_68));
#ifdef PORT
sub_GAME_7F078464(lookat);
@@ -8850,7 +8863,17 @@ void mp_respawn_handler(void)
{
coord3d start_pos = ZeroCoordSpawnPos;
f32 start_look_angle;
#ifdef PORT
/* MACOS-CAST-CENSUS: PadRecord.stan is a StandTile* host pointer. An s32 local
* truncates it, and every consumer below (bondviewYPositionRelated,
* change_player_pos_to_target, prop->stan) rebuilds it unbased and
* dereferences it. Local only -- no struct/ABI impact -- and the s32 pad
* below keeps the N64 stack layout identical. The solo twin
* (bondview_r.c:102) already declares this StandTile*. */
StandTile *start_stan;
#else
s32 start_stan;
#endif
s32 pad;
f32 stan_height;
s32 var_v0;
@@ -8894,7 +8917,7 @@ void mp_respawn_handler(void)
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 */
stan_height = bondviewYPositionRelated(start_stan, start_pos.x, start_pos.z); /* D604: start_stan is a StandTile* under PORT; the D441 (u32) cast truncated it */
#else
stan_height = bondviewYPositionRelated(start_stan, start_pos.x, start_pos.z);
#endif
@@ -8913,7 +8936,7 @@ void mp_respawn_handler(void)
#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 */
change_player_pos_to_target(&g_CurrentPlayer->field_488, &start_pos, start_stan); /* D604 */
#else
change_player_pos_to_target(&g_CurrentPlayer->field_488, &start_pos, start_stan);
#endif
@@ -8925,7 +8948,7 @@ void mp_respawn_handler(void)
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 */
g_CurrentPlayer->prop->stan = start_stan; /* D604 */
#else
g_CurrentPlayer->prop->stan = start_stan;
#endif
@@ -10941,7 +10964,12 @@ join_768:
{
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 */
/* D603: `anim` is an s32 holding an N64-form address ((s32)ptr_animation_table + a ROM
* offset, or a firing-table word). The D441 zero-extend is only right where the DRAM
* window base is 0 (Windows/Linux); on arm64 macOS the base must be re-added
* (PORT_N64PTR; identity at base 0) or modelSetAnimFrame faults on the bare 0x70xxxxxx
* address (live 2P crash, stage 38, playerTick). §A1 s32-slot class, cf. D587/D595. */
modelSetAnimation(ppointers[index]->bodyModel, PORT_N64PTR(ModelAnimation, (u32)(anim)), 0, startframe, angle, 16.0f);
#else
modelSetAnimation(ppointers[index]->bodyModel, (ModelAnimation *) anim, 0, startframe, angle, 16.0f);
#endif
+7 -1
View File
@@ -1,4 +1,9 @@
#include <ultra64.h>
#if defined(PORT)
#include "portaddr.h"
#else
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
#ifdef PORT
#include <stdio.h>
#include <stdlib.h>
@@ -435,7 +440,8 @@ void bondviewLoadSetupIntroSection(void)
{
rand_camera_index--;
#ifdef PORT
ptr_random06cam_entry = (struct SetupIntroCamera *)(uintptr_t)ptr_random06cam_entry->prev;
/* D88/MACOS-ADDR-WINDOW: prev is a u32 holding the truncated host pointer; re-base. */
ptr_random06cam_entry = PORT_N64PTR(struct SetupIntroCamera, ptr_random06cam_entry->prev);
#else
ptr_random06cam_entry = ptr_random06cam_entry->prev;
#endif
+24 -21
View File
@@ -33,6 +33,16 @@
#include "model.h"
#include "tex.h"
/* MACOS-ADDR-WINDOW/M2: animation_table_ptrs1/2 slots and vtxstore_allocate() results hold
* N64/window addresses as s32; re-base where they become pointers. Identity at
* PORT_ADDR_BASE == 0. */
#if defined(PORT)
#include "portaddr.h"
#else
/* N64 build: the port address window is the identity (see port_addr.h). */
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
#ifdef PORT
#include <stdio.h>
#include <stdlib.h>
@@ -70,7 +80,7 @@ static s32 d193aAnimIndex(void *anim) {
s32 i;
if (anim == NULL) return -1;
for (i = 0; animation_table_ptrs1[i] != 0; i++) {
if ((void *)(uintptr_t)(u32)animation_table_ptrs1[i] == anim) return i;
if (PORT_N64PTR(void, animation_table_ptrs1[i]) == anim) return i;
}
return -1;
}
@@ -2486,23 +2496,11 @@ 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
if (PORT_N64PTR(ModelAnimation, animation_table_ptrs1[g_AnimationTablePointerCountRelated]) != PORT_N64PTR(ModelAnimation, 1))
{
#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
if (objecthandlerGetModelAnim(model) != PORT_N64PTR(ModelAnimation, animation_table_ptrs1[g_AnimationTablePointerCountRelated]))
{
#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
modelSetAnimation(model, PORT_N64PTR(ModelAnimation, animation_table_ptrs1[g_AnimationTablePointerCountRelated]), 0, 0.0f, 0.5f, 0.0f);
}
}
}
@@ -3435,10 +3433,15 @@ after_opcode:
}
#ifdef PORT
/* PC port (D43/D45): CollisionRelatedNode is a raw vma (u32), not a pointer. */
relatednode = (ModelNode *)(uintptr_t)((ModelRoData_DisplayList_CollisionRecord *) node)->CollisionVertices[bestindex].CollisionRelatedNode;
/* PC port (D43/D45): CollisionRelatedNode is a raw vma (u32), not a pointer.
* MACOS-BLOODSTAIN: that vma is an N64 address (the port stores window pointers as
* 32-bit values, and PORT_ADDR_BASE is 4 GiB-aligned, so the low 32 bits
* ARE the N64 address), so it must be re-based like every other
* address-carrying field -- a bare cast leaves it as 0x7070_xxxx and the
* &relatednode->Data deref below faults. Test the re-based pointer. */
relatednode = PORT_N64PTR(ModelNode, ((ModelRoData_DisplayList_CollisionRecord *) node)->CollisionVertices[bestindex].CollisionRelatedNode);
if (((ModelRoData_DisplayList_CollisionRecord *) node)->CollisionVertices[bestindex].CollisionRelatedNode != 0)
if (relatednode != NULL)
#else
relatednode = (ModelNode *) ((ModelRoData_DisplayList_CollisionRecord *) node)->CollisionVertices[bestindex].CollisionRelatedNode;
@@ -3452,7 +3455,7 @@ after_opcode:
if (((ModelRwData_DisplayList_CollisionRecord *) rwdata)->Vertices == ((ModelRoData_DisplayList_CollisionRecord *) node)->Vertices)
{
newvertices = (Vertex *) vtxstore_allocate(((ModelRoData_DisplayList_CollisionRecord *) node)->numVertices, 0xcccc, 0, 0);
newvertices = PORT_N64PTR(Vertex, vtxstore_allocate(((ModelRoData_DisplayList_CollisionRecord *) node)->numVertices, 0xcccc, 0, 0));
if (newvertices != NULL)
{
@@ -3469,7 +3472,7 @@ after_opcode:
if ((relatedrwdata != NULL) && (relatedrwdata->Vertices == relatedrodata->Vertices))
{
newvertices = (Vertex *) vtxstore_allocate(relatedrodata->numVertices, 0xcccc, 0, 0);
newvertices = PORT_N64PTR(Vertex, vtxstore_allocate(relatedrodata->numVertices, 0xcccc, 0, 0));
if (newvertices != NULL)
{
+6 -5
View File
@@ -1,4 +1,9 @@
#include <ultra64.h>
#if defined(PORT)
#include "portaddr.h"
#else
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
#include <bondtypes.h>
#include "cheat.h"
#include "chr.h"
@@ -100,11 +105,7 @@ 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
modelAttachHead(model, PORT_N64PTR(ModelNode, opcode), headHeader);
if ((sunglasses == 0) && ((s32) headHeader->numSwitches > 0))
{
+12 -2
View File
@@ -48,6 +48,16 @@
#define PUN_ATTACK_ITEM(punchr, field) ((s32)(punchr)->field.attack_item)
#endif
/* MACOS-ADDR-WINDOW/M2: `(s32)&ANIM_DATA_x + (s32)&ptr_animation_table->data` is an N64
* address, and animation_table_ptrs1[] slots hold N64 addresses; re-base where
* such a value becomes a pointer. Identity at PORT_ADDR_BASE == 0. */
#if defined(PORT)
#include "portaddr.h"
#else
/* N64 build: the port address window is the identity (see port_addr.h). */
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
point2d D_800309F0 = {0, 0};
// forward declarations
@@ -609,7 +619,7 @@ 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 */
modelSetAnimation(self->model, PORT_N64PTR(void, animation_table_ptrs1[animID]), (bitfield & ANIM_MIRROR) != 0, startframef, phi_f0, (f32)interpol_time60);
#else
modelSetAnimation(self->model, (void *)animation_table_ptrs1[animID], (bitfield & ANIM_MIRROR) != 0, startframef, phi_f0, (f32)interpol_time60);
#endif
@@ -4986,7 +4996,7 @@ 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 */
set_actor_on_path(self, PORT_N64PTR(struct patrol_path, pathid)); /* D587: s32-held DRAM patrol-path slot; PORT_N64PTR rebases on macos (identity at base 0) */
#else
set_actor_on_path(self, pathid);
#endif
+6 -5
View File
@@ -36,6 +36,11 @@
#include <random.h>
#include <snd.h>
#include <ultra64.h>
#if defined(PORT)
#include "portaddr.h"
#else
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
#ifdef PORT
#include <stdlib.h>
#include "envflag.h"
@@ -1138,11 +1143,7 @@ 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
modelSetAnimation(AircraftEntityp->model, PORT_N64PTR(ModelAnimation, animation_table_ptrs2[anim_id]), zero, startframe, 0.5f, (s32)ai->INTERPOL_TIME60);
if (endframe >= 0)
{
modelSetAnimEndFrame(AircraftEntityp->model, endframe);
+12
View File
@@ -23,7 +23,19 @@
void cleanupObjects(s32 stage)
{
#ifdef PORT
/* MACOS-PROPDEFS-TRUNC: the decomp's `(u32)g_CurrentSetup.propDefs` converts the pointer
* to a 32-bit integer and back into a `u32 *` -- a no-op where pointers
* are 32-bit (N64), but on the 64-bit port it TRUNCATES the host pointer
* to its low 32 bits, leaving an unbased N64 address (0x7017_c70f on
* Cuba) that faults on the first CLEANUP_PDTYPE deref. propDefs is a real
* host pointer (prop.c:1283 stores `local_stage + propDefs`), so cast
* pointer-to-pointer and keep `u32 *` for sizepropdef()'s 4-byte-unit
* increment. Same class as D3x; semantics unchanged. */
u32 *obj = (u32 *)g_CurrentSetup.propDefs;
#else
u32 *obj = (u32)g_CurrentSetup.propDefs;
#endif
if (obj)
{
+7
View File
@@ -5,6 +5,13 @@
u32 decompressdata(u8 *src, u8 *dst, struct huft *hlist);
#ifdef PORT
/* D588: full pointer width — returns the live DRAM-window input cursor
* (s_rz_nextin in port/src/rzdecomp.c); callers feed it to
* texSetBitstring(), which takes pointer width. */
u8 *rzipGetSomething(void);
#else
s32 rzipGetSomething(void);
#endif
#endif
+5
View File
@@ -2,6 +2,11 @@
#include "porttick.h" /* D486 */
#endif
#include <ultra64.h>
#if defined(PORT)
#include "portaddr.h"
#else
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
#include <PR/os.h>
#include <PR/gbi.h>
#include <gbi_extension.h>
+18 -26
View File
@@ -51,6 +51,10 @@
#include "model.h"
#ifdef PORT
#include "hudaspect.h" /* D335 front-end pillarbox under native widescreen */
#include "portaddr.h"
#else
/* N64 build: the port address window is the identity (see port_addr.h). */
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
@@ -992,8 +996,8 @@ Gfx *frontPrintText(Gfx *gdl, s32 *x, s32 *y, s8 *text, s32 second_font_table, s
x,
y,
text,
second_font_table,
first_font_table,
PORT_N64PTR(struct fontchar, second_font_table),
PORT_N64PTR(struct font, first_font_table),
arg6,
(textglowR.r << 0x18) | (textglowG.r << 0x10) | (textglowB.r << 8) | textglowA.r,
view_x,
@@ -1008,8 +1012,8 @@ Gfx *frontPrintText(Gfx *gdl, s32 *x, s32 *y, s8 *text, s32 second_font_table, s
x,
y,
text,
second_font_table,
first_font_table,
PORT_N64PTR(struct fontchar, second_font_table),
PORT_N64PTR(struct font, first_font_table),
arg6,
view_x,
view_y,
@@ -1521,7 +1525,7 @@ Gfx *display_aligned_white_text_to_screen(Gfx *dl, s32 arg1, s32 arg2, s32 halig
x = arg1 - ((s32) (halign * sp48) / 2);
y = arg2 - ((s32) (valign * sp4C) / 2);
return textRender(dl, &x, &y, text, arg6, arg7, -1, viGetX(), viGetY(), 0, 0);
return textRender(dl, &x, &y, text, PORT_N64PTR(struct fontchar, arg6), PORT_N64PTR(struct font, arg7), -1, viGetX(), viGetY(), 0, 0);
}
@@ -2153,11 +2157,7 @@ void load_walletbond(void)
b = (struct ModelNode *)mnode;
srecord = b->Data;
#ifdef PORT
arg0 = (Gfx_le *)((uintptr_t)srecord->BaseAddr + ((u32)(uintptr_t)srecord->Primary & 0xffffff));
#else
arg0 = (s32)srecord->BaseAddr + ((s32)srecord->Primary & 0xffffff);
#endif
arg0 = PORT_N64PTR(Gfx, (s32)srecord->BaseAddr + ((s32)srecord->Primary & 0xffffff));
bgApplyDynamicCCRMLUT(arg0, NULL, CCRMLUT_WALLETBOND);
}
}
@@ -7450,11 +7450,7 @@ Gfx *constructor_menu0D_missioncomplete(Gfx *DL)
x = 0x37;
y = 0xDC;
DL = frontPrintText(DL, &x, &y, text, ptrFontZurichBoldChars, ptrFontZurichBold, 0xFF, viGetX(), viGetY(), 0, 0);
#ifdef PORT
strcpy(stagename, (char *)frontGetPlayersFavoriteWeaponInHand(0, 0));
#else
strcpy(stagename, frontGetPlayersFavoriteWeaponInHand(0, 0));
#endif
strcpy(stagename, PORT_N64PTR(char, frontGetPlayersFavoriteWeaponInHand(0, 0)));
if ((array_favweapon[0][0] > 0) && (array_favweapon[0][1] == array_favweapon[0][0]))
{
temp = strlen(stagename)-1;
@@ -7819,16 +7815,16 @@ Gfx *constructor_menu16_nocontrollers(Gfx *DL)
if ((numContCon == 0) || (numContCon == 1) || (numContCon == 2) || (numContCon == 3)) {
text = langGet(getStringID(LTITLE, TITLE_STR_118_NOCONT)); //NO CONTROLLER IN CONTROLLER SOCKET 1
}
textMeasure(&y2, &x2, text, ptrFontZurichBoldChars, ptrFontZurichBold, 0);
textMeasure(&y2, &x2, PORT_N64PTR(char, text), ptrFontZurichBoldChars, ptrFontZurichBold, 0);
x = 0xDC - (x2 >> 1);
y = 0x99 - (y2 >> 1);
#ifdef BUGFIX_R1
if (j_text_trigger) {
DL = textRenderOutlined(DL, &x, &y, text, ptrFontZurichBoldChars, ptrFontZurichBold, -1, 0x8000FF, viGetX(), viGetY(), 0, 0);
DL = textRenderOutlined(DL, &x, &y, PORT_N64PTR(char, text), ptrFontZurichBoldChars, ptrFontZurichBold, -1, 0x8000FF, viGetX(), viGetY(), 0, 0);
}
else {
#endif
DL = textRender(DL, &x, &y, text, ptrFontZurichBoldChars, ptrFontZurichBold, -1, viGetX(), viGetY(), 0, 0);
DL = textRender(DL, &x, &y, PORT_N64PTR(char, text), ptrFontZurichBoldChars, ptrFontZurichBold, -1, viGetX(), viGetY(), 0, 0);
#ifdef BUGFIX_R1
}
#endif
@@ -7837,16 +7833,16 @@ Gfx *constructor_menu16_nocontrollers(Gfx *DL)
if ((numContCon == 0) || (numContCon == 1) || (numContCon == 2) || (numContCon == 3)) {
text = langGet(getStringID(LTITLE, TITLE_STR_119_ATTACHCONT)); //PLEASE POWER OFF AND ATTACH A CONTROLLER
}
textMeasure(&y2, &x2, text, ptrFontZurichBoldChars, ptrFontZurichBold, 0);
textMeasure(&y2, &x2, PORT_N64PTR(char, text), ptrFontZurichBoldChars, ptrFontZurichBold, 0);
x = 0xDC - (x2 >> 1);
y = 0xB1 - (y2 >> 1);
#ifdef BUGFIX_R1
if (j_text_trigger) {
DL = textRenderOutlined(DL, &x, &y, text, ptrFontZurichBoldChars, ptrFontZurichBold, -1, 0x8000FF, viGetX(), viGetY(), 0, 0);
DL = textRenderOutlined(DL, &x, &y, PORT_N64PTR(char, text), ptrFontZurichBoldChars, ptrFontZurichBold, -1, 0x8000FF, viGetX(), viGetY(), 0, 0);
}
else {
#endif
DL = textRender(DL, &x, &y, text, ptrFontZurichBoldChars, ptrFontZurichBold, -1, viGetX(), viGetY(), 0, 0);
DL = textRender(DL, &x, &y, PORT_N64PTR(char, text), ptrFontZurichBoldChars, ptrFontZurichBold, -1, viGetX(), viGetY(), 0, 0);
#ifdef BUGFIX_R1
}
#endif
@@ -8053,11 +8049,7 @@ 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
modelSetAnimation(cast_model, PORT_N64PTR(ModelAnimation, 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);
g_MenuTimer = 0;
cast_camera_dist_start = ((((f32) ((u32) randomGetNext())) * (1.0f / U32_MAX)) * 80.0f) + 70.0f;
+12 -7
View File
@@ -1,4 +1,9 @@
#include <ultra64.h>
#if defined(PORT)
#include "portaddr.h"
#else
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
#include "include/limits.h"
#include <bondconstants.h>
#include <bondtypes.h>
@@ -6335,14 +6340,14 @@ Gfx *generate_ammo_total_microcode(Gfx *gdl)
if (imageoffset_r != 0)
{
imageoffset_r += globalbank_rdram_offset;
gdl = set_rgba_redirect_generate_microcode(gdl, (u8 *)imageoffset_r, (getPlayer_c_screenleft() + getPlayer_c_screenwidth()) - (f32)rightx, -1.0f,
gdl = set_rgba_redirect_generate_microcode(gdl, PORT_N64PTR(u8, imageoffset_r), (getPlayer_c_screenleft() + getPlayer_c_screenwidth()) - (f32)rightx, -1.0f,
#if defined(VERSION_EU)
(viGetViewTop() + viGetViewHeight()) - 30, 0,
#else
(viGetViewTop() + viGetViewHeight()) - 20, 0,
#endif
ammo_related[ammotype].IconYOffset, 1);
textwidth_r = ((u8 *)imageoffset_r)[4];
textwidth_r = (PORT_N64PTR(u8, imageoffset_r))[4];
}
gdl = microcode_constructor(gdl);
@@ -6407,14 +6412,14 @@ Gfx *generate_ammo_total_microcode(Gfx *gdl)
if (imageoffset_l != 0)
{
imageoffset_l += globalbank_rdram_offset;
gdl = set_rgba_redirect_generate_microcode(gdl, (u8 *)imageoffset_l, getPlayer_c_screenleft() + (f32)leftx, -1.0f,
gdl = set_rgba_redirect_generate_microcode(gdl, PORT_N64PTR(u8, imageoffset_l), getPlayer_c_screenleft() + (f32)leftx, -1.0f,
#if defined(VERSION_EU)
(viGetViewTop() + viGetViewHeight()) - 30, 1,
#else
(viGetViewTop() + viGetViewHeight()) - 20, 1,
#endif
ammo_related[ammotype].IconYOffset, 1);
textwidth_l = ((u8 *)imageoffset_l)[4];
textwidth_l = (PORT_N64PTR(u8, imageoffset_l))[4];
}
gdl = microcode_constructor(gdl);
@@ -6509,12 +6514,12 @@ Gfx *gunDrawWatchAmmoDisplay(Gfx *gdl)
// Draw the ammo icon
#if defined(VERSION_EU)
gdl = set_rgba_redirect_generate_microcode(gdl, (u8 *)imageoffset, 200.0f, 208.0f, (viGetViewTop() + viGetViewHeight()) - 30, 0, ammo_related[ammotype].IconYOffset, 1);
gdl = set_rgba_redirect_generate_microcode(gdl, PORT_N64PTR(u8, imageoffset), 200.0f, 208.0f, (viGetViewTop() + viGetViewHeight()) - 30, 0, ammo_related[ammotype].IconYOffset, 1);
#else
gdl = set_rgba_redirect_generate_microcode(gdl, (u8 *)imageoffset, 200.0f, 180.0f, (viGetViewTop() + viGetViewHeight()) - 20, 0, ammo_related[ammotype].IconYOffset, 1);
gdl = set_rgba_redirect_generate_microcode(gdl, PORT_N64PTR(u8, imageoffset), 200.0f, 180.0f, (viGetViewTop() + viGetViewHeight()) - 20, 0, ammo_related[ammotype].IconYOffset, 1);
#endif
textwidth = ((u8 *)imageoffset)[4];
textwidth = (PORT_N64PTR(u8, imageoffset))[4];
}
gdl = microcode_constructor(gdl);
+140 -9
View File
@@ -1,4 +1,7 @@
#include <ultra64.h>
#ifdef PORT
#include <stdio.h> /* D589 D589POOL probe only (decode-time full pool dump) */
#endif
#include "bondconstants.h"
#include "image.h"
#include "image_bank.h"
@@ -300,6 +303,56 @@ s32 texInflateZlib(u8 *src, u8 *dst, s32 arg2, s32 forcenumimages, struct texpoo
decompressdata(img_curpos, &scratch2, (struct huft *)&scratch);
imagebytesout = texAlignIndices(scratch2, width, height, format, &dst[totalbytesout]);
#ifdef PORT
/* D589 pin probe: companion to the non-zlib D589SRC/D589DST dumps --
* for the first ~60 zlib texture files, print the first 32 bytes of
* the DECOMPRESSED scratch and of the POOL just written, so a
* same-seed A/B localizes whether a platform divergence enters at
* the zlib output, the pool write, or after the write (memory view).
* Diagnostic-only, inert unless GE_ZLIBDUMP is set; print only. */
if (j == 0)
{
static int s_zdump = -1;
if (s_zdump < 0)
s_zdump = getenv("GE_ZLIBDUMP") ? 60 : 0;
if (s_zdump-- > 0)
{
int k;
osSyncPrintf("ZDUMP texnum=%d fmt=%d w=%d h=%d scratch=",
(int)arg4->rightpos->texturenum, (int)format, (int)width, (int)height);
for (k = 0; k < 32; k++)
osSyncPrintf(" %02x", (unsigned)scratch2[k]);
osSyncPrintf(" pool=");
for (k = 0; k < 32; k++)
osSyncPrintf(" %02x", (unsigned)dst[totalbytesout + k]);
osSyncPrintf("\n");
}
}
/* D589 pin probe (session D+): FULL decoded-image pool dump at DECODE
* time (zlib path -- post-texAlignIndices, pre-swap), the companion
* to the non-zlib D589POOL dump above. Same env gate, same
* address-keyed naming (texdump/d_z_<texnum>.bin). */
if (j == 0)
{
static int s_d589poolz = -1;
if (s_d589poolz < 0)
s_d589poolz = getenv("GE_D589POOL") ? 300 : 0;
if (s_d589poolz-- > 0)
{
char pn[64];
snprintf(pn, sizeof pn, "texdump/d_z_%d.bin", (int)arg4->rightpos->texturenum);
FILE* pf = fopen(pn, "wb");
if (pf)
{
fwrite(&dst[totalbytesout], 1, (size_t)imagebytesout, pf);
fclose(pf);
}
osSyncPrintf("D589POOL z texnum=%d addr=%p bytes=%d\n",
(int)arg4->rightpos->texturenum, (void *)&dst[totalbytesout],
(int)imagebytesout);
}
}
#endif
texSetBitstring(rzipGetSomething());
if ((arg2 == 1) && (forcenumimages > 0))
@@ -974,6 +1027,30 @@ s32 texInflateNonZlib(u8 *src, u8 *dst, s32 arg2, s32 forcenumimages, struct tex
osSyncPrintf("D219RAW texnum=%d format=%d width=%d height=%d compmethod=%d i=%d\n",
(int)g_TexNumToLoad, (int)format, (int)width, (int)height, (int)compmethod, (int)i);
}
/* D589 next-step (docs/dev/findings.md): diagnostic-only, inert
* unless GE_TEXSRC is set. Log the first 32 SOURCE bytes (payload
* start, pre-decode) of the first image of every non-zlib RZ file,
* so a same-seed A/B can compare the bitstream region on both
* platforms: the pool-side GE_TEXRAW dumps are a clean per-word
* reversal (rev32) on arm64, and this tells us whether the source
* region is already reversed (upstream) or the write is.
* No behavior change -- print only. */
if (i == 0)
{
static int s_d589src = -1;
if (s_d589src < 0)
s_d589src = getenv("GE_TEXSRC") ? 500 : 0;
if (s_d589src-- > 0)
{
const u8 *pp = (const u8 *)img_curpos;
int k;
osSyncPrintf("D589SRC texnum=%d fmt=%d w=%d h=%d cm=%d src=",
(int)g_TexNumToLoad, (int)format, (int)width, (int)height, (int)compmethod);
for (k = 0; k < 32; k++)
osSyncPrintf(" %02x", (unsigned)pp[k]);
osSyncPrintf("\n");
}
}
#endif
switch (compmethod)
@@ -1086,6 +1163,58 @@ s32 texInflateNonZlib(u8 *src, u8 *dst, s32 arg2, s32 forcenumimages, struct tex
osSyncPrintf(" %04x", (unsigned)p16[dbgk]);
osSyncPrintf("\n");
}
/* D589 next-step (docs/dev/findings.md): companion to GE_TEXSRC --
* dump the first 32 POOL bytes (post-decode, pre-swap/shrink) of the
* first image of every non-zlib RZ file, so a same-seed A/B tells us
* which pool writes (if any) diverge between platforms. The source
* bytes (GE_TEXSRC) are byte-identical on both; this shows the
* decoded output. Diagnostic-only, inert unless GE_TEXSRC is set.
* No behavior change -- print only. */
if (i == 0)
{
static int s_d589dst = -1;
if (s_d589dst < 0)
s_d589dst = getenv("GE_TEXSRC") ? 500 : 0;
if (s_d589dst-- > 0)
{
const u8 *oo = &dst[totalbytesout];
int k;
osSyncPrintf("D589DST texnum=%d fmt=%d w=%d h=%d cm=%d dst=",
(int)g_TexNumToLoad, (int)format, (int)width, (int)height, (int)compmethod);
for (k = 0; k < 32; k++)
osSyncPrintf(" %02x", (unsigned)oo[k]);
osSyncPrintf("\n");
}
}
/* D589 pin probe (session D+): FULL decoded-image pool dump at DECODE
* time (non-zlib path), so a same-seed A/B tells a writer-side
* divergence (pool already rev32 the instant the decode writes it)
* from a later clobber (pool correct at decode, rev32 at import time).
* The 32-byte D589DST/ZDUMP windows above are too narrow to settle
* that. Writes texdump/d_nz_<texnum>.bin and logs the pool address
* (host pointer, comparable to GE_TEXI's addr=) so the bin can be
* matched to the import-time rNNN bin by address. Diagnostic-only,
* inert unless GE_D589POOL. */
if (i == 0)
{
static int s_d589pool = -1;
if (s_d589pool < 0)
s_d589pool = getenv("GE_D589POOL") ? 300 : 0;
if (s_d589pool-- > 0)
{
char pn[64];
snprintf(pn, sizeof pn, "texdump/d_nz_%d.bin", (int)g_TexNumToLoad);
FILE* pf = fopen(pn, "wb");
if (pf)
{
fwrite(&dst[totalbytesout], 1, (size_t)imagebytesout, pf);
fclose(pf);
}
osSyncPrintf("D589POOL nz texnum=%d addr=%p bytes=%d\n",
(int)g_TexNumToLoad, (void *)&dst[totalbytesout],
(int)imagebytesout);
}
}
#endif
if (arg2 == 1 && forcenumimages > 0)
@@ -1744,18 +1873,19 @@ void texReadAlphaBits(u8 *image,s32 count)
s32 texReadUncompressed(u8 *dst, s32 width, s32 height, s32 format)
{
#ifdef PORT
u32 *dst32 = (u32 *)(((uintptr_t)dst + 0xf) & ~0xf);
/* MACOS-TEXALIGN: aligning a host pointer through u32 truncates it -- on arm64 dst is
* ~0x1000_70xx_xxxx and these would become unbased 0x70xx_xxxx, then be
* written through. Align at full pointer width instead. Identical result
* where pointers are 32-bit. NOTE: currently unreachable on the PC port --
* textures come from the pre-converted pccg sidecars, and a GE_D303 probe
* over a full level boot recorded zero calls -- so this is latent, fixed
* defensively rather than in response to an observed fault. */
u32 *dst32 = (u32 *)(((uintptr_t)dst + 0xf) & ~(uintptr_t)0xf);
u16 *dst16 = (u16 *)(((uintptr_t)dst + 7) & ~(uintptr_t)7);
u8 *dst8 = (u8 *)(((uintptr_t)dst + 7) & ~(uintptr_t)7);
#else
u32 *dst32 = (u32 *)(((u32)dst + 0xf) & ~0xf);
#endif
#ifdef PORT
u16 *dst16 = (u16 *)(((uintptr_t)dst + 7) & ~7);
#else
u16 *dst16 = (u16 *)(((u32)dst + 7) & ~7);
#endif
#ifdef PORT
u8 *dst8 = (u8 *)(((uintptr_t)dst + 7) & ~7);
#else
u8 *dst8 = (u8 *)(((u32)dst + 7) & ~7);
#endif
s32 x;
@@ -2618,6 +2748,7 @@ void texLoad(s32 *updateword, struct texpool *pool)
}
pool->leftpos += bytesout;
}
texFreeBytesInBuffer(pool);
+24 -6
View File
@@ -4,6 +4,10 @@
#include "image_bank.h"
#ifdef PORT
#include <gimgfixup.h>
#include "portaddr.h"
#else
/* N64 build: the port address window is the identity (see port_addr.h). */
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
// bss
@@ -95,9 +99,23 @@ struct sImageTableEntry *mpstageselimages;
extern u8* _GlobalimagetableSegmentRomStart;
void texSetBitstring(s32 pos) {
#ifdef PORT
img_curpos = (u8 *)(uintptr_t)(u32)(pos); /* D441: zero-extend s32-held DRAM ptr */
void texSetBitstring(u8 *pos)
#else
void texSetBitstring(s32 pos)
#endif
{
#if defined(PORT)
/* D588: the argument is a LIVE host pointer; take it at full pointer
* width. The pre-#95 code zero-extended the s32 (D441); the #95
* re-apply re-based it through portN64ToHost(), which remaps the D131
* image-relative range [0x40000000, 0x70000000) onto the image and
* mis-maps any native (out-of-window) pointer whose low 32 bits fall
* there -- e.g. the Windows game thread's native 8 MB stack
* (0x618Dxxxx..0x620Dxxxx), whose compbuffer pointer faulted in
* texReadBits(). Full width is identity at base 0 (Win/Linux) and
* exact for in-window (macOS) pointers. */
img_curpos = pos;
#else
img_curpos = pos;
#endif
@@ -178,7 +196,7 @@ extern Gfx* globalDL_0x900;
extern Gfx* globalDL_0x9a8;
extern Gfx* globalDL_0xa50;
#if defined(__x86_64__)
#if defined(PLATFORM_64BIT)
/* D39 (docs/dev/findings.md): on N64 all 49 symbols above link inside the
* Globalimagetable segment at physical 0x02xxxxxx (ge007.ld), so
* `globalbank_rdram_offset + (u32)&sym` rebases each onto pGlobalimagetable.
@@ -259,13 +277,13 @@ void texReset(void)
pGlobalimagetable = ((u32)pGlobalimagetable + 0xFFFU) & 0xFFFFF000;
#endif
romCopy(pGlobalimagetable, &_GlobalimagetableSegmentRomStart, size);
romCopy(PORT_N64PTR(void, pGlobalimagetable), &_GlobalimagetableSegmentRomStart, size);
#ifdef PORT
/* D68 (docs/dev/findings.md): the ROM copy is N64 big-endian; convert
* the CPU-interpreted u32 fields (IMAGESEG Gfx w1 words and
* sImageTableEntry.index) to host order before any code reads them. */
gimgFixupGlobalimagetable((u8 *)pGlobalimagetable);
gimgFixupGlobalimagetable((u8 *)PORT_N64PTR(void, pGlobalimagetable));
#endif
#ifdef PORT
@@ -340,6 +358,6 @@ void texReset(void)
/* D68: explosion.c executes the compiled globalDL_0xNNN shadows via
* g_ExplosionDisplayLists[]; copy the IMAGESEG words that texLoad()
* patched in the ROM copy over into those arrays. */
gimgSyncCompiledGlobalDLs((u8 *)pGlobalimagetable);
gimgSyncCompiledGlobalDLs((u8 *)PORT_N64PTR(void, pGlobalimagetable));
#endif
}
+10
View File
@@ -33,6 +33,16 @@ extern s32 globalbank_rdram_offset;
void texReset(void);
u32 texReadBits(s32 bitCount);
#ifdef PORT
/* D588: full pointer width. Callers pass a LIVE host pointer (the
* compressed-source cursor: texLoad's stack compbuffer, or the DRAM
* mempool cursor from rzipGetSomething()); the s32 parameter truncated
* it and the #95 portN64ToHost() re-base then mis-mapped native
* (out-of-window) pointers whose low 32 bits fall in the D131
* image-relative range [0x40000000, 0x70000000). */
void texSetBitstring(u8 *pos);
#else
void texSetBitstring(s32 pos);
#endif
#endif
+5
View File
@@ -1,4 +1,9 @@
#include <ultra64.h>
#if defined(PORT)
#include "portaddr.h"
#else
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
#include <math.h>
#include <bondtypes.h>
#include "chr.h"
+24 -1
View File
@@ -6,6 +6,16 @@
#include "math.h"
#include "math_floor.h"
/* MACOS-ADDR-WINDOW/M2: these tables store animation addresses as N64 values (s32/union
* offset) that are then cast to pointers. Re-base when the pointer is formed;
* identity at PORT_ADDR_BASE == 0. */
#if defined(PORT)
#include "portaddr.h"
#define ANI_ADDR(x) ((void *)portN64ToHost((u32)(x)))
#else
#define ANI_ADDR(x) ((void *)(x))
#endif
/**
* Gets the number of currently allocated heads and bodies
@@ -191,7 +201,20 @@ s32 initResolveAnimTable(struct StruckAnim *entries)
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 */
/* D595 (2026-10-08, §A1 cross-tag): `ptr_animation_table` is a
* live host pointer. The original (s32) read truncated it to its
* low 32 bits: identity on x86_64 (PORT_ADDR_BASE == 0, the
* PERMANENT bank is s32-low at 0x70xxxxxx) but on arm64 macOS
* (PORT_ADDR_BASE = 16 TiB) it drops the high 32 bits, so every
* resolved `struck_anim` (hit / stagger / death reaction) landed
* in bogus low memory. The first time an AI played one --
* triggered_on_shot_hit -> modelSetAnimation2 -- modelSetAnimFrame
* dereferenced the garbage ModelAnimation and SIGSEGV'd. Reached
* only now that mouse fire works (D594). Read the base at full
* pointer width and add the small N64 ROM offset (u32); identical
* to the old value on every PORT_ADDR_BASE == 0 platform. */
entry[-1].struck_anim =
(ModelAnimation *)((uintptr_t)(*entries).struck_anim + (u32)address);
#else
entry[-1].struck_anim = (ModelAnimation *)((*((s32 *)entries)) + (0, address));
#endif
+9 -15
View File
@@ -6,6 +6,12 @@
/* D33: ROM-file endianness fixup for the animation data segment (port layer). */
#include "romdata.h"
#if defined(PORT)
#include "portaddr.h"
#else
/* N64 build: the port address window is the identity (see port_addr.h). */
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
//bss
@@ -256,27 +262,15 @@ 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
PORT_N64PTR(struct anim_entry, *var_v0)->unk08 += (s32)&ptr_animation_table->data;
PORT_N64PTR(struct anim_entry, *var_v0)->unk10 += (s32)&ptr_animation_table->data;
}
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
*PORT_N64PTR(s32, *var_v0) += (s32)&_animation_entriesSegmentRomStart;
}
}
}
+1 -1
View File
@@ -38,7 +38,7 @@ void initModelHitEntryFreeList(void)
entries[i].prev = &entries[i - 1];
}
#if defined(__x86_64__)
#if defined(PLATFORM_64BIT)
// D40: N64 g_ModelHitEntriesPenultimate labels entry 598 of the pool; on PC the
// placeholder symbol is not emitted, so take the address directly.
((ModelHitEntry *)g_ModelHitEntries)[MODELHITENTRIES_LEN - 1].prev = &entries[MODELHITENTRIES_LEN - 2];
+40 -11
View File
@@ -10,7 +10,9 @@
#include "romdata.h"
#ifdef PORT
#include "envflag.h" /* cached getenv for hot-path probes */
#include "system.h" /* sysLogPrintf: D582 / D602 one-shot diagnostics */
#endif
#include "portaddr.h" /* MACOS-ADDR-WINDOW/M2: DRAM window base */
extern resource_lookup_data_entry resource_lookup_data_array[]; /* ob.c */
static void langFixupLoadedBank(char *name, void *p)
@@ -21,6 +23,16 @@ static void langFixupLoadedBank(char *name, void *p)
}
#endif
/* g_LangBanks[] holds N64 DRAM addresses as s32 (they must fit s32, see
* memp.h / MACOS-ADDR-WINDOW). Re-base only where a slot becomes a pointer; identity at
* PORT_ADDR_BASE == 0, so Windows/Linux are unchanged. */
#if defined(PORT)
#include "portaddr.h"
#else
/* N64 build: the port address window is the identity (see port_addr.h). */
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
// bss
//CODE.bss:8008C640
#ifdef PORT
@@ -271,13 +283,13 @@ void langInit(void) {
g_LangBanks[LOPTIONS] = _fileNameLoadToBank(LnameX_lookuptable[LOPTIONS][j_text_trigger], FILELOADMETHOD_DEFAULT, 0x100, MEMPOOL_PERMANENT);
g_LangBanks[LMISC] = _fileNameLoadToBank(LnameX_lookuptable[LMISC][j_text_trigger], FILELOADMETHOD_DEFAULT, 0x100, MEMPOOL_PERMANENT);
#ifdef PORT
langFixupLoadedBank(LnameX_lookuptable[LGUN][j_text_trigger], (void *)g_LangBanks[LGUN]);
langFixupLoadedBank(LnameX_lookuptable[LTITLE][j_text_trigger], (void *)g_LangBanks[LTITLE]);
langFixupLoadedBank(LnameX_lookuptable[LMPMENU][j_text_trigger], (void *)g_LangBanks[LMPMENU]);
langFixupLoadedBank(LnameX_lookuptable[LPROPOBJ][j_text_trigger], (void *)g_LangBanks[LPROPOBJ]);
langFixupLoadedBank(LnameX_lookuptable[LMPWEAPONS][j_text_trigger], (void *)g_LangBanks[LMPWEAPONS]);
langFixupLoadedBank(LnameX_lookuptable[LOPTIONS][j_text_trigger], (void *)g_LangBanks[LOPTIONS]);
langFixupLoadedBank(LnameX_lookuptable[LMISC][j_text_trigger], (void *)g_LangBanks[LMISC]);
langFixupLoadedBank(LnameX_lookuptable[LGUN][j_text_trigger], PORT_N64PTR(void, g_LangBanks[LGUN]));
langFixupLoadedBank(LnameX_lookuptable[LTITLE][j_text_trigger], PORT_N64PTR(void, g_LangBanks[LTITLE]));
langFixupLoadedBank(LnameX_lookuptable[LMPMENU][j_text_trigger], PORT_N64PTR(void, g_LangBanks[LMPMENU]));
langFixupLoadedBank(LnameX_lookuptable[LPROPOBJ][j_text_trigger], PORT_N64PTR(void, g_LangBanks[LPROPOBJ]));
langFixupLoadedBank(LnameX_lookuptable[LMPWEAPONS][j_text_trigger], PORT_N64PTR(void, g_LangBanks[LMPWEAPONS]));
langFixupLoadedBank(LnameX_lookuptable[LOPTIONS][j_text_trigger], PORT_N64PTR(void, g_LangBanks[LOPTIONS]));
langFixupLoadedBank(LnameX_lookuptable[LMISC][j_text_trigger], PORT_N64PTR(void, g_LangBanks[LMISC]));
#endif
}
@@ -385,7 +397,7 @@ void langLoadToAddr(u32 id)
{
g_LangBanks[id] = _fileNameLoadToBank(LnameX_lookuptable[id][j_text_trigger],1,0x100,MEMPOOL_STAGE);
#ifdef PORT
langFixupLoadedBank(LnameX_lookuptable[id][j_text_trigger], (void *)g_LangBanks[id]);
langFixupLoadedBank(LnameX_lookuptable[id][j_text_trigger], PORT_N64PTR(void, g_LangBanks[id]));
#endif
}
@@ -394,7 +406,7 @@ void langLoadToBank(int id,u8 *target,int size)
{
g_LangBanks[id] = _fileNameLoadToAddr(LnameX_lookuptable[id][j_text_trigger],1,target,size);
#ifdef PORT
langFixupLoadedBank(LnameX_lookuptable[id][j_text_trigger], (void *)g_LangBanks[id]);
langFixupLoadedBank(LnameX_lookuptable[id][j_text_trigger], PORT_N64PTR(void, g_LangBanks[id]));
#endif
}
@@ -421,7 +433,7 @@ u8 * langGet(s32 slotID)
return NULL;
}
#endif
u32 * textbank_ptr = g_LangBanks[slotID >> 10]; /* get the text file bank ID index the text ptr table */
u32 * textbank_ptr = PORT_N64PTR(u32, g_LangBanks[slotID >> 10]); /* get the text file bank ID index the text ptr table */
#ifdef PORT
/* D129 cont.: g_LangBanks[] holds pointer-width entries (u32* under PORT) and is only populated for banks the
* current flow has loaded. A bare `-level_XX` boot that reaches the
@@ -429,11 +441,28 @@ u8 * langGet(s32 slotID)
* slot that was never filled -> stale/garbage non-NULL value -> fault on
* the table read below. Reject anything that is not a plausible mapped
* DRAM address. */
if ((uintptr_t)textbank_ptr < 0x10000 || (uintptr_t)textbank_ptr >= 0x400000000ULL) {
if (!portAddrIsMapped(textbank_ptr)) { /* D573: arena views + cart */
return NULL;
}
#endif
u32 textslot_offset = textbank_ptr[slotID & 0x03FF]; /* load the textbank ptr table then get the slot's offset */
#ifdef PORT
/* D582 (#152): a bank pointer that passes the check above can still point
* at memory that no longer holds a language bank (seen: Cuba end credits,
* offset -> 0xe236b67a). Banks are < 64 KiB (romdataFixupLangBank), so a
* larger offset is stale data: log it and return an empty string (callers
* such as textMeasure dereference the result). D602 found the same fault
* on macOS (end credits, bank + 0x721E08F2); this check covers it. */
if (textslot_offset >= 0x10000u) {
static int nlog;
static u8 empty[1];
if (nlog++ < 8) {
sysLogPrintf(LOG_WARNING, "D582: langGet stale bank: slot 0x%x bank %d offset 0x%x (bank ptr %p)",
(unsigned) slotID, (int) (slotID >> 10), (unsigned) textslot_offset, (void *) textbank_ptr);
}
return empty;
}
#endif
#ifdef PORT
/* D456: rebuild the result at pointer width; a u32 here truncates any arena address >= 4 GiB. */
+11 -2
View File
@@ -565,6 +565,15 @@ void setupUpdateObjectRoomPosition(ObjectRecord *obj)
}
#ifdef PORT
/* D580: N64 `(u8)cmd[0]` is the propDef type only on big-endian (header word =
* [u16 extrascale][u8 state][u8 type]); on little-endian it is the low byte of
* extrascale. Read the struct member (offset 3), as sizepropdef does. */
#define REUSE_PDTYPE(c) (((PropDefHeaderRecord *)(c))->type)
#else
#define REUSE_PDTYPE(c) ((u8)(c)[0])
#endif
ObjectRecord *setupFindObjForReuse(s32 wanttype, ObjectRecord **offscreenobjptr, ObjectRecord **anyobjptr, bool musthaveprop, bool musthavemodel, ModelFileHeader *modeldef)
{
ObjectRecord *offscreenobj = NULL;
@@ -574,9 +583,9 @@ ObjectRecord *setupFindObjForReuse(s32 wanttype, ObjectRecord **offscreenobjptr,
if (cmd)
{
while ((u8)cmd[0] != PROPDEF_END)
while (REUSE_PDTYPE(cmd) != PROPDEF_END)
{
if ((wanttype & 0xff) == (u8)cmd[0])
if ((wanttype & 0xff) == REUSE_PDTYPE(cmd))
{
ObjectRecord *obj = (ObjectRecord *)cmd;
+51 -20
View File
@@ -13,6 +13,16 @@
#include "initunk_005520.h"
#include "math_asinfacosf.h"
#include "math_floor.h"
/* MACOS-ADDR-WINDOW/M2: ModelAnimation.bitDescriptors/bitStream are u32 fields holding N64
* addresses (D32: the blob base is added by expand_ani_table_entries). Re-base
* where they become pointers; identity at PORT_ADDR_BASE == 0. */
#if defined(PORT)
#include "portaddr.h"
#else
/* N64 build: the port address window is the identity (see port_addr.h). */
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
#include "math_ceil.h"
#include "math_unk_05A9E0.h"
#include "objecthandler.h"
@@ -78,6 +88,11 @@ Model *modelmgrInstantiateModel(ModelFileHeader *header)
/* PC port (D53.2): this buffer is used as a struct Model via the
* same type-pun as g_ModelSlots; on PC that struct is 0xE8 bytes,
* not the N64 0x20 slot size. */
{
static int fb = 0;
osSyncPrintf("modelmgr: STAGE model fallback #%d (numRecords=%d)\n",
++fb, (int)header->numRecords);
}
model = mempAllocBytesInBank(sizeof(struct Model), MEMPOOL_STAGE);
#else
model = mempAllocBytesInBank(0x20, MEMPOOL_STAGE);
@@ -980,7 +995,7 @@ u16 modelAnimReadRootMotionValue(ModelAnimation *anim, s32 fieldIndex, s32 extra
u8 bitsThisRead;
result = 0;
desc = (ModelAnimBitField *)anim->bitDescriptors + fieldIndex; // D32: u32 -> ptr
desc = PORT_N64PTR(struct ModelAnimBitField, anim->bitDescriptors) + fieldIndex; // D32: u32 -> ptr (MACOS-ADDR-WINDOW/M2 re-base)
bitsRemaining = desc->bitCount;
if (bitsRemaining > 0)
@@ -988,7 +1003,7 @@ u16 modelAnimReadRootMotionValue(ModelAnimation *anim, s32 fieldIndex, s32 extra
totalBitOffset = extraBitOffset + desc->bitOffset;
byteIndex = totalBitOffset >> 3;
totalBitOffset &= 7;
byteptr = (u8 *)anim->bitStream + byteIndex; // D32: u32 -> ptr
byteptr = PORT_N64PTR(u8, anim->bitStream) + byteIndex; // D32: u32 -> ptr (MACOS-ADDR-WINDOW/M2 re-base)
bitsThisRead = 8 - totalBitOffset;
if (bitsRemaining >= bitsThisRead)
@@ -1665,24 +1680,24 @@ void process_02_position(ModelRenderData *arg0, Model *model, ModelNode *node)
rot1 = D_80036094;
sub_GAME_7F06DEC0(jointnum.v, model->gunhand, skeleton, model->anim, model->unk34, &rot1);
sub_GAME_7F06DEC0(jointnum.v, model->gunhand, skeleton, model->anim, PORT_N64PTR(u8, model->unk34), &rot1);
if (model->unk2c != 0.0f)
{
rot2 = D_800360A0;
sub_GAME_7F06DEC0(jointnum.v, model->gunhand, skeleton, model->anim, model->unk38, &rot2);
sub_GAME_7F06DEC0(jointnum.v, model->gunhand, skeleton, model->anim, PORT_N64PTR(u8, model->unk38), &rot2);
sub_GAME_7F06D160(&rot1, &rot2, model->unk2c);
}
if (model->unk84 != 0.0f)
{
rot3 = D_800360AC;
sub_GAME_7F06DEC0(jointnum.v, model->unk25, skeleton, model->anim2, model->unk64, &rot3);
sub_GAME_7F06DEC0(jointnum.v, model->unk25, skeleton, model->anim2, PORT_N64PTR(u8, model->unk64), &rot3);
if (model->unk5c != 0.0f)
{
rot4 = D_800360B8;
sub_GAME_7F06DEC0(jointnum.v, model->unk25, skeleton, model->anim2, model->unk68, &rot4);
sub_GAME_7F06DEC0(jointnum.v, model->unk25, skeleton, model->anim2, PORT_N64PTR(u8, model->unk68), &rot4);
sub_GAME_7F06D160(&rot3, &rot4, model->unk5c);
}
@@ -1861,18 +1876,18 @@ void process_03_unknown(ModelRenderData *renderData, Model *model, ModelNode *no
jointIndex = rodata->JointID;
skeleton = model->obj->Skeleton;
angle = sub_GAME_7F06E540(jointIndex, model->gunhand, skeleton, model->anim, (u8 *)model->unk34);
angle = sub_GAME_7F06E540(jointIndex, model->gunhand, skeleton, model->anim, PORT_N64PTR(u8, model->unk34));
if (model->unk2c != 0.0f) {
tmp = sub_GAME_7F06E540(jointIndex, model->gunhand, skeleton, model->anim, (u8 *)model->unk38);
tmp = sub_GAME_7F06E540(jointIndex, model->gunhand, skeleton, model->anim, PORT_N64PTR(u8, model->unk38));
angle = sub_GAME_7F06D0CC(angle, tmp, model->unk2c);
}
if (model->unk84 != 0.0f) {
tmp = sub_GAME_7F06E540(jointIndex, model->unk25, skeleton, model->anim2, (u8 *)model->unk64);
tmp = sub_GAME_7F06E540(jointIndex, model->unk25, skeleton, model->anim2, PORT_N64PTR(u8, model->unk64));
if (model->unk5c != 0.0f) {
tmp2 = sub_GAME_7F06E540(jointIndex, model->unk25, skeleton, model->anim2, (u8 *)model->unk68);
tmp2 = sub_GAME_7F06E540(jointIndex, model->unk25, skeleton, model->anim2, PORT_N64PTR(u8, model->unk68));
tmp = sub_GAME_7F06D0CC(tmp, tmp2, model->unk5c);
}
@@ -1928,6 +1943,17 @@ void modelUpdateDistanceRelations(Model* model, ModelNode* node)
else
{
distance = -mtx->m[3][2] * getPlayer_c_lodscalez();
#ifdef PORT
/* D953: c_lodscalez is built from the RENDERED fovy (D222), so a
* wider Video.FovScale moved every model's LOD switch closer (x1.7
* at 150) and models swapped detail more often while turning.
* Rescale to the game's own fovy (port/src/video.c
* portLodScaleOwnFov; zoom kept, identity at the defaults). */
{
extern f32 portLodScaleOwnFov(void);
distance *= portLodScaleOwnFov();
}
#endif
if (g_ModelDistanceScale != 1)
{
@@ -5994,7 +6020,12 @@ s32 loadAnimationFrame(ModelAnimation* anim, s32 frame, ModelSkeleton* unused)
s32 ret;
s32 source;
s32 frameSize;
#if defined(PORT)
/* MACOS-ADDR-WINDOW/M2: keep the buffer address at full pointer width (D59 buffer). */
uintptr_t dest;
#else
u32 dest;
#endif
u32 size;
ret = 0;
@@ -6034,7 +6065,7 @@ s32 loadAnimationFrame(ModelAnimation* anim, s32 frame, ModelSkeleton* unused)
size = ((u32) (frameSize + 15) >> 4) * 16;
// This copies one animation frame from ROM to the destination in RAM
romCopy((void* ) dest, (void* ) source, size);
romCopy((void *)dest, (void *)source, size);
// Increment this which serves nothing
D_80036414->uselessPointer += 1;
@@ -6135,7 +6166,7 @@ void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr
{
ModelRoData_DisplayListRecord* rodata = &node->Data->DisplayList;
PROMOTE(rodata->Vertices);
rodata->BaseAddr = (void *)fileramaddr;
rodata->BaseAddr = PORT_N64PTR(void, fileramaddr);
break;
}
@@ -6153,7 +6184,7 @@ void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr
PROMOTE(rodata->CollisionVertices[i].LinkedTo);
#endif
}
rodata->BaseAddr = (void *)fileramaddr;
rodata->BaseAddr = PORT_N64PTR(void, fileramaddr);
break;
}
@@ -6177,7 +6208,7 @@ void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr
PROMOTE(rodata->Children[i].unk04);
}
rodata->BaseAddr = (void *)fileramaddr;
rodata->BaseAddr = PORT_N64PTR(void, fileramaddr);
break;
}
@@ -6196,14 +6227,14 @@ void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr
PROMOTE(rodata->Children[i].unk04);
}
rodata->BaseAddr = (void *)fileramaddr;
rodata->BaseAddr = PORT_N64PTR(void, fileramaddr);
break;
}
case MODELNODE_OPCODE_OP06:
{
ModelRoData_Op06Record* rodata = &node->Data->Op06;
rodata->BaseAddr = (void *)fileramaddr;
rodata->BaseAddr = PORT_N64PTR(void, fileramaddr);
break;
}
@@ -6241,7 +6272,7 @@ void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr
{
ModelRoData_Op11Record* rodata = &node->Data->Op11;
PROMOTE(rodata->unk0c[15]);
rodata->BaseAddr = (void *)fileramaddr;
rodata->BaseAddr = PORT_N64PTR(void, fileramaddr);
break;
}
@@ -6249,7 +6280,7 @@ void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr
{
ModelRoData_GunfireRecord* rodata = &node->Data->Gunfire;
PROMOTE(rodata->Image);
rodata->BaseAddr = (void *)fileramaddr;
rodata->BaseAddr = PORT_N64PTR(void, fileramaddr);
break;
}
@@ -6258,7 +6289,7 @@ void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr
ModelRoData_ShadowRecord* rodata = &node->Data->Shadow;
PROMOTE(rodata->image);
PROMOTE(rodata->Header);
rodata->BaseAddr = (void *)fileramaddr;
rodata->BaseAddr = PORT_N64PTR(void, fileramaddr);
break;
}
@@ -6266,7 +6297,7 @@ void modelPromoteNodeOffsetsToPointers(ModelNode *node, u32 vma, u32 fileramaddr
{
ModelRoData_DisplayListPrimaryRecord* rodata = &node->Data->DisplayListPrimary;
PROMOTE(rodata->Vertices);
rodata->BaseAddr = (void *)fileramaddr;
rodata->BaseAddr = PORT_N64PTR(void, fileramaddr);
break;
}
+11 -11
View File
@@ -17,6 +17,12 @@
#include "mp_music.h"
#include "file.h"
#include "assets/obseg/text/LmpmenuE.h"
#if defined(PORT)
#include "portaddr.h"
#else
/* N64 build: the port address window is the identity (see port_addr.h). */
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
#ifdef REFRESH_PAL
#define MPMENU_YOFF 8 /* PAL: every text row sits 8 pixels lower */
@@ -1421,12 +1427,9 @@ Gfx *mp_watch_menu_display(Gfx *gdl)
// Must remain a comma expression for matching
#ifdef PORT
h2 = (char *) langGet(getStringID(LMPMENU, MPMENU_STR_1D_KILLS)), /* KILLS */
sprintf(rankbuffer, ascii_pnum_KILLS, PORT_N64PTR(char, q), curplayernum + 1, (char *) h2); /* -> "P<n> KILLS" */
#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<n> KILLS" */ /* D441: zero-extend s32-held DRAM ptr */
#else
sprintf(rankbuffer, ascii_pnum_KILLS, (char *) q, curplayernum + 1, (char *) h2); /* -> "P<n> KILLS" */
#endif
@@ -1499,12 +1502,9 @@ Gfx *mp_watch_menu_display(Gfx *gdl)
// Must remain a comma expression for matching.
#ifdef PORT
h2 = (char *) langGet(getStringID(LMPMENU, MPMENU_STR_1E_LOSSES)), /* LOSSES */
sprintf(rankbuffer, ascii_pnum_LOSSES, PORT_N64PTR(char, q), curplayernum + 1, (char *) h2); /* -> "P<n> LOSSES" */
#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<n> LOSSES" */ /* D441: zero-extend s32-held DRAM ptr */
#else
sprintf(rankbuffer, ascii_pnum_LOSSES, (char *) q, curplayernum + 1, (char *) h2); /* -> "P<n> LOSSES" */
#endif
@@ -1622,7 +1622,7 @@ Gfx *mp_watch_menu_display(Gfx *gdl)
y = (viGetViewTop() - (fav_textheight >> 1)) + (37 + MPMENU_YOFF);
viewleft = viGetX(); h1 = viGetY();
gdl = textRender(gdl, &x, &y, text, ptrFontBankGothicChars, ptrFontBankGothic, 0x00ff00b0, viewleft, h1, 0, 0);
text = (char *) frontGetPlayersFavoriteWeaponInHand(curplayernum, 0);
text = PORT_N64PTR(char, frontGetPlayersFavoriteWeaponInHand(curplayernum, 0));
textMeasure(&fav_textheight, &fav_textwidth, text, ptrFontBankGothicChars, ptrFontBankGothic, 0);
x = ((viGetViewLeft() + fav_x_offset) - (fav_textwidth >> 1)) + 80;
x2 = viGetViewTop();
@@ -1644,7 +1644,7 @@ Gfx *mp_watch_menu_display(Gfx *gdl)
if (g_CurrentPlayer->ptr_text_first_mp_award)
{
text = (char *) g_CurrentPlayer->ptr_text_first_mp_award;
text = PORT_N64PTR(char, g_CurrentPlayer->ptr_text_first_mp_award);
textMeasure(&fav_textheight, &fav_textwidth, text, ptrFontBankGothicChars, ptrFontBankGothic, 0);
x = ((viGetViewLeft() + fav_x_offset) - (fav_textwidth >> 1)) + 80;
y = (viGetViewTop() - (fav_textheight >> 1)) + (75 + MPMENU_YOFF);
@@ -1655,7 +1655,7 @@ Gfx *mp_watch_menu_display(Gfx *gdl)
if (g_CurrentPlayer->ptr_text_second_mp_award)
{
text = (char *) g_CurrentPlayer->ptr_text_second_mp_award;
text = PORT_N64PTR(char, g_CurrentPlayer->ptr_text_second_mp_award);
textMeasure(&fav_textheight, &fav_textwidth, text, ptrFontBankGothicChars, ptrFontBankGothic, 0);
x = ((viGetViewLeft() + fav_x_offset) - (fav_textwidth >> 1)) + 80;
y = (viGetViewTop() - (fav_textheight >> 1)) + (88 + MPMENU_YOFF);
+1 -1
View File
@@ -4,7 +4,7 @@
#include "model.h"
// bss
#if defined(__x86_64__)
#if defined(PLATFORM_64BIT)
// D40: the N64 placeholder chain below (g_ModelHitEntries .. g_ModelHitEntriesPenultimate)
// forms one contiguous 600 x 20-byte ModelHitEntry pool in CODE.bss. On x86-64
// ModelHitEntry is 40 bytes (two pointer pairs), so initModelHitEntryFreeList()
+7 -10
View File
@@ -1,4 +1,9 @@
#include <ultra64.h>
#if defined(PORT)
#include "portaddr.h"
#else
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
#include "chrobjdata.h"
#include "image.h"
#include "math_asinfacosf.h"
@@ -60,11 +65,7 @@ void sub_GAME_7F0762E0(ModelFileHeader *objheader, u8 *name, u8 *dst, struct tex
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
texCopyGdls((Gfx *) (((u8 *) objheader->Switches) + (((u32) gdl) & 0x00ffffff)), PORT_N64PTR(Gfx, (romremaining + filedata) - (s32) name), (s32) name);
texLoadFromModelFileHeader(objheader, buffer);
@@ -106,11 +107,7 @@ void sub_GAME_7F0762E0(ModelFileHeader *objheader, u8 *name, u8 *dst, struct tex
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
fileSetSize(filenum, PORT_N64PTR(u8, filedata), (((s32) name + 0xf) & (~0xf)), dst == 0);
}
}
+54 -48
View File
@@ -17,6 +17,12 @@
#include "glass.h"
#include "frametiming.h"
#include "assets/obseg/text/LoptionE.h"
#if defined(PORT)
#include "portaddr.h"
#else
/* N64 build: the port address window is the identity (see port_addr.h). */
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
#define WATCH_BACKGROUND_VERTEX_COUNT 30
@@ -1957,30 +1963,30 @@ Gfx *draw_abort_cancel_confirm(Gfx *gdl)
if (watch_item_is_actively_selected != 0)
{
textMeasure(&sp60, &sp64, sp54, pFontChars, pFontFile, 0);
textMeasure(&sp60, &sp64, PORT_N64PTR(char, sp54), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0);
gdl = textRender(gdl, &sp7C, &sp70, sp54, pFontChars, pFontFile, 0xA0FFA0F0, sp64, sp60, 0, 0);
gdl = textRender(gdl, &sp7C, &sp70, PORT_N64PTR(char, sp54), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0xA0FFA0F0, sp64, sp60, 0, 0);
if (D_800409A4 != 0)
{
gdl = textRenderOutlined(gdl, &sp78, &sp6C, sp50, pFontChars, pFontFile, -1, 0x7000A0, viGetX(), viGetY(), 0, 0);
gdl = textRender(gdl, &sp74, &sp68, sp4C, pFontChars, pFontFile, 0xFF00B0, viGetX(), viGetY(), 0, 0);
gdl = textRenderOutlined(gdl, &sp78, &sp6C, PORT_N64PTR(char, sp50), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), -1, 0x7000A0, viGetX(), viGetY(), 0, 0);
gdl = textRender(gdl, &sp74, &sp68, PORT_N64PTR(char, sp4C), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0xFF00B0, viGetX(), viGetY(), 0, 0);
}
else
{
if (D_800409A4 == 0)
{
gdl = textRender(gdl, &sp78, &sp6C, sp50, pFontChars, pFontFile, 0xFF00B0, viGetX(), viGetY(), 0, 0);
gdl = textRenderOutlined(gdl, &sp74, &sp68, sp4C, pFontChars, pFontFile, -1, 0x7000A0, viGetX(), viGetY(), 0, 0);
gdl = textRender(gdl, &sp78, &sp6C, PORT_N64PTR(char, sp50), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0xFF00B0, viGetX(), viGetY(), 0, 0);
gdl = textRenderOutlined(gdl, &sp74, &sp68, PORT_N64PTR(char, sp4C), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), -1, 0x7000A0, viGetX(), viGetY(), 0, 0);
}
}
}
else
{
textMeasure(&sp60, &sp64, sp54, pFontChars, pFontFile, 0);
gdl = textRender(gdl, &sp7C, &sp70, sp54, pFontChars, pFontFile, 0x800080, sp64, sp60, 0, 0);
gdl = textRender(gdl, &sp78, &sp6C, sp50, pFontChars, pFontFile, 0x800080, viGetX(), viGetY(), 0, 0);
gdl = textRender(gdl, &sp74, &sp68, sp4C, pFontChars, pFontFile, 0x800080, viGetX(), viGetY(), 0, 0);
textMeasure(&sp60, &sp64, PORT_N64PTR(char, sp54), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0);
gdl = textRender(gdl, &sp7C, &sp70, PORT_N64PTR(char, sp54), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0x800080, sp64, sp60, 0, 0);
gdl = textRender(gdl, &sp78, &sp6C, PORT_N64PTR(char, sp50), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0x800080, viGetX(), viGetY(), 0, 0);
gdl = textRender(gdl, &sp74, &sp68, PORT_N64PTR(char, sp4C), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0x800080, viGetX(), viGetY(), 0, 0);
}
return gdl;
@@ -2032,10 +2038,10 @@ Gfx *draw_text_mission_status(Gfx *gdl)
}
gdl = microcode_constructor(gdl);
textMeasure(&sp5C, &sp58, txtptr_1, pFontChars, pFontFile, 0);
textMeasure(&sp5C, &sp58, PORT_N64PTR(char, txtptr_1), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0);
sp64 = 0x51;
sp60 = YOFFSET_MISSIONSTATUS;
gdl = textRender(gdl, &sp64, &sp60, txtptr_1, pFontChars, pFontFile, 0xFF00B0, sp58, sp5C, 0, 0);
gdl = textRender(gdl, &sp64, &sp60, PORT_N64PTR(char, txtptr_1), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0xFF00B0, sp58, sp5C, 0, 0);
if (j_text_trigger)
{
@@ -2048,8 +2054,8 @@ Gfx *draw_text_mission_status(Gfx *gdl)
sp64 = sp64 + sp58 + joffset + 4;
sp60 = sp60 - sp5C;
textMeasure(&sp5C, &sp58, txtptr_2, pFontChars, pFontFile, 0);
gdl = textRender(gdl, &sp64, &sp60, txtptr_2, pFontChars, pFontFile, sp4C, sp58, sp5C, 0, 0);
textMeasure(&sp5C, &sp58, PORT_N64PTR(char, txtptr_2), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0);
gdl = textRender(gdl, &sp64, &sp60, PORT_N64PTR(char, txtptr_2), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), sp4C, sp58, sp5C, 0, 0);
gdl = draw_abort_cancel_confirm(gdl);
return gdl;
@@ -2101,8 +2107,8 @@ Gfx *draw_text_q_watch_v201_beta(Gfx *gdl)
pFontFile = ptrFontBankGothic;
pFontChars = ptrFontBankGothicChars;
gdl = microcode_constructor(gdl);
textMeasure(&sp48, &sp44, txtptr, pFontChars, pFontFile, 0);
gdl = textRender(gdl, &sp50, &sp4C, txtptr, pFontChars, pFontFile, 0xFF00B0, sp44, sp48, 0, 0);
textMeasure(&sp48, &sp44, PORT_N64PTR(char, txtptr), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0);
gdl = textRender(gdl, &sp50, &sp4C, PORT_N64PTR(char, txtptr), PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0xFF00B0, sp44, sp48, 0, 0);
return gdl;
}
@@ -2245,16 +2251,16 @@ Gfx* draw_current_hand_item_and_ammo(Gfx* gdl) {
#else
sp88 = 0xBC;
#endif
textMeasure(&sp84, &sp80, text, sp78, sp7C, 0);
gdl = textRender(gdl, &sp8C, &sp88, text, sp78, sp7C, 0xFF00B0, sp80, sp84, 0, 0);
textMeasure(&sp84, &sp80, text, PORT_N64PTR(struct fontchar, sp78), PORT_N64PTR(struct font, sp7C), 0);
gdl = textRender(gdl, &sp8C, &sp88, text, PORT_N64PTR(struct fontchar, sp78), PORT_N64PTR(struct font, sp7C), 0xFF00B0, sp80, sp84, 0, 0);
#if defined(LEFTOVERDEBUG)
sp88 = 0xAA;
#else
sp88 = 0xC6;
#endif
textMeasure(&sp84, &sp80, text2, sp78, sp7C, 0);
gdl = textRender(gdl, &sp8C, &sp88, text2, sp78, sp7C, 0xFF00B0, sp80, sp84, 0, 0);
textMeasure(&sp84, &sp80, text2, PORT_N64PTR(struct fontchar, sp78), PORT_N64PTR(struct font, sp7C), 0);
gdl = textRender(gdl, &sp8C, &sp88, text2, PORT_N64PTR(struct fontchar, sp78), PORT_N64PTR(struct font, sp7C), 0xFF00B0, sp80, sp84, 0, 0);
return gdl;
}
@@ -2496,11 +2502,11 @@ Gfx *draw_watch_inventory_page(Gfx *gdl, Mtx *param_2)
gdl = microcode_constructor(gdl);
textMeasure(&textheight, &textwidth, string_builder_allocation, pFontChars2, pFontFile2, LINEHEIGHT());
textMeasure(&textheight, &textwidth, string_builder_allocation, PORT_N64PTR(struct fontchar, pFontChars2), PORT_N64PTR(struct font, pFontFile2), LINEHEIGHT());
gdl = microcode_constructor_related_to_menus(gdl, 0x4E, WATCH_INV_BASE_Y(), textwidth + 0x4E, (LINEHEIGHT() * 5) + WATCH_INV_BASE_Y(), 0);
gdl = textRender(gdl, &x1, &y1, string_builder_allocation, pFontChars2, pFontFile2, 0xAA00B0, textwidth + 1, LINEHEIGHT() * 5, watch_inventory_text_y, LINEHEIGHT());
gdl = textRender(gdl, &x1, &y1, string_builder_allocation, PORT_N64PTR(struct fontchar, pFontChars2), PORT_N64PTR(struct font, pFontFile2), 0xAA00B0, textwidth + 1, LINEHEIGHT() * 5, watch_inventory_text_y, LINEHEIGHT());
gdl = microcode_constructor_related_to_menus(gdl, 0x4B, temp_s0_3, textwidth + 0x52, (LINEHEIGHT() + temp_s0_3) - 2, 0x800050);
@@ -2530,22 +2536,22 @@ Gfx *draw_watch_inventory_page(Gfx *gdl, Mtx *param_2)
gdl = microcode_constructor(gdl);
textMeasure(&y2, &x2, formattedString, pFontChars, pFontFile, 0);
textMeasure(&y2, &x2, formattedString, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0);
if (watch_inventory_text_is_settled != 0)
{
textMeasure(&y2, &x2, invItemName, pFontChars, pFontFile, LINEHEIGHT());
textMeasure(&y2, &x2, invItemName, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), LINEHEIGHT());
x1 = 0x4E;
y1 = (LINEHEIGHT() * 2) + WATCH_INV_BASE_Y();
if (D_800409C4 == 0)
{
gdl = textRender(gdl, &x1, &y1, invItemName, pFontChars, pFontFile, 0xA0FFA0F0, x2, 0x64, 0, LINEHEIGHT());
gdl = textRender(gdl, &x1, &y1, invItemName, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0xA0FFA0F0, x2, 0x64, 0, LINEHEIGHT());
}
else
{
gdl = textRenderOutlined(gdl, &x1, &y1, invItemName, pFontChars, pFontFile, -1, 0x7000A0, x2 + 1, 0x64, 0, LINEHEIGHT());
gdl = textRenderOutlined(gdl, &x1, &y1, invItemName, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), -1, 0x7000A0, x2 + 1, 0x64, 0, LINEHEIGHT());
}
sub_GAME_7F0A8378();
@@ -2605,7 +2611,7 @@ Gfx *unused_draw_watch_inventory_page(Gfx *gdl, Mtx *param_2) {
game_options_inventory_navigation();
gdl = microcode_constructor(gdl);
textMeasure(&sp58, &sp54, long_name, pFontChars, pFontFile, 0);
textMeasure(&sp58, &sp54, long_name, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0);
sp70 = ((s32) (0xAA - sp54) / 2) + 0x4B;
temp_2 = sp70;
@@ -2618,16 +2624,16 @@ Gfx *unused_draw_watch_inventory_page(Gfx *gdl, Mtx *param_2) {
sub_GAME_7F0A8378();
if (D_800409C4 == 0)
{
gdl = textRender(gdl, &sp70, &sp6C, long_name, pFontChars, pFontFile, 0xA0FFA0F0, sp54, 0x64, 0, 0);
gdl = textRender(gdl, &sp70, &sp6C, long_name, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0xA0FFA0F0, sp54, 0x64, 0, 0);
}
else
{
gdl = textRenderOutlined(gdl, &sp70, &sp6C, long_name, pFontChars, pFontFile, -1, 0x7000A0, sp54 + 1, 0x64, 0, 0);
gdl = textRenderOutlined(gdl, &sp70, &sp6C, long_name, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), -1, 0x7000A0, sp54 + 1, 0x64, 0, 0);
}
}
else
{
gdl = textRender(gdl, &sp70, &sp6C, long_name, pFontChars, pFontFile, 0xAA00B0, sp54, 0x64, 0, 0);
gdl = textRender(gdl, &sp70, &sp6C, long_name, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0xAA00B0, sp54, 0x64, 0, 0);
}
}
@@ -3348,11 +3354,11 @@ Gfx *display_text_buttons_dual_control(Gfx *gdl)
if (joyGetButtons(PLAYER_1, Z_TRIG))
{
gdl = draw_options_labels(gdl, 0x5A, YOFFSET_5, textptr_aux, -1, 1, 0x7000A0, 0, 0, 0x3000B0, 0);
gdl = draw_options_labels(gdl, 0x5A, YOFFSET_5, PORT_N64PTR(char, textptr_aux), -1, 1, 0x7000A0, 0, 0, 0x3000B0, 0);
}
else
{
gdl = draw_options_labels(gdl, 0x5A, YOFFSET_5, textptr_aux, 0xAA00B0, 0, -1, 0, 0, 0x3000B0, 0);
gdl = draw_options_labels(gdl, 0x5A, YOFFSET_5, PORT_N64PTR(char, textptr_aux), 0xAA00B0, 0, -1, 0, 0, 0x3000B0, 0);
}
if ((g_CurrentPlayer->cur_player_control_type_0 == CONTROLLER_CONFIG_PLENTY) || (g_CurrentPlayer->cur_player_control_type_0 == CONTROLLER_CONFIG_DOMINO))
@@ -3364,7 +3370,7 @@ Gfx *display_text_buttons_dual_control(Gfx *gdl)
textptr_aux = langGet(getStringID(LOPTIONS, OPTION_STR_06_LOOK_LF)); //look
}
gdl = draw_options_labels(gdl, 0x5A, YOFFSET_4, textptr_aux, 0xAA00B0, 0, -1, 0, 0, 0x3000B0, 0);
gdl = draw_options_labels(gdl, 0x5A, YOFFSET_4, PORT_N64PTR(char, textptr_aux), 0xAA00B0, 0, -1, 0, 0, 0x3000B0, 0);
if (joyGetButtons(1, A_BUTTON))
{
@@ -3395,11 +3401,11 @@ Gfx *display_text_buttons_dual_control(Gfx *gdl)
if (joyGetButtons(1, Z_TRIG))
{
gdl = draw_options_labels(gdl, 0xE6, YOFFSET_5, textptr_aux, -1, 1, 0x7000A0, 0, 0, 0x3000B0, 1);
gdl = draw_options_labels(gdl, 0xE6, YOFFSET_5, PORT_N64PTR(char, textptr_aux), -1, 1, 0x7000A0, 0, 0, 0x3000B0, 1);
}
else
{
gdl = draw_options_labels(gdl, 0xE6, YOFFSET_5, textptr_aux, 0xAA00B0, 0, -1, 0, 0, 0x3000B0, 1);
gdl = draw_options_labels(gdl, 0xE6, YOFFSET_5, PORT_N64PTR(char, textptr_aux), 0xAA00B0, 0, -1, 0, 0, 0x3000B0, 1);
}
if ((g_CurrentPlayer->cur_player_control_type_0 == CONTROLLER_CONFIG_PLENTY) || (g_CurrentPlayer->cur_player_control_type_0 == CONTROLLER_CONFIG_DOMINO))
@@ -3411,7 +3417,7 @@ Gfx *display_text_buttons_dual_control(Gfx *gdl)
textptr_aux = langGet(getStringID(LOPTIONS, OPTION_STR_05_MOVE_LF)); //move
}
gdl = draw_options_labels(gdl, 0xE6, YOFFSET_4, textptr_aux, 0xAA00B0, 0, -1, 0, 0, 0x3000B0, 1);
gdl = draw_options_labels(gdl, 0xE6, YOFFSET_4, PORT_N64PTR(char, textptr_aux), 0xAA00B0, 0, -1, 0, 0, 0x3000B0, 1);
return gdl;
}
@@ -3656,14 +3662,14 @@ Gfx *draw_watch_control_options_page(Gfx *gdl, Mtx *param_2) {
}
}
textMeasure(&sp50, &sp54, textptr, pFontChars, pFontFile, 0);
textMeasure(&sp50, &sp54, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0);
if ((watch_item_is_actively_selected != 0) && (controller_options_index == CONTROLLER_OPTIONS_INDEX_STYLE))
{
gdl = textRenderOutlined(gdl, &sp5C, &sp58, textptr, pFontChars, pFontFile, phi_s1, 0x7000A0, sp54 + 1, sp50, 0, 0);
gdl = textRenderOutlined(gdl, &sp5C, &sp58, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), phi_s1, 0x7000A0, sp54 + 1, sp50, 0, 0);
} else
{
gdl = textRender(gdl, &sp5C, &sp58, textptr, pFontChars, pFontFile, phi_s1, sp54, sp50, 0, 0);
gdl = textRender(gdl, &sp5C, &sp58, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), phi_s1, sp54, sp50, 0, 0);
}
gdl = draw_controller_style_text(gdl);
@@ -3687,14 +3693,14 @@ Gfx *draw_watch_control_options_page(Gfx *gdl, Mtx *param_2) {
}
}
textMeasure(&sp50, &sp54, textptr, pFontChars, pFontFile, 0);
textMeasure(&sp50, &sp54, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0);
if ((watch_item_is_actively_selected != 0) && (controller_options_index == CONTROLLER_OPTIONS_INDEX_INPUTS))
{
gdl = textRenderOutlined(gdl, &sp5C, &sp58, textptr, pFontChars, pFontFile, phi_s1, 0x7000A0, sp54 + 1, sp50, 0, 0);
gdl = textRenderOutlined(gdl, &sp5C, &sp58, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), phi_s1, 0x7000A0, sp54 + 1, sp50, 0, 0);
} else
{
gdl = textRender(gdl, &sp5C, &sp58, textptr, pFontChars, pFontFile, phi_s1, sp54, sp50, 0, 0);
gdl = textRender(gdl, &sp5C, &sp58, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), phi_s1, sp54, sp50, 0, 0);
}
}
@@ -3969,15 +3975,15 @@ Gfx *draw_watch_game_options_page(Gfx *gdl, Mtx *param_2) {
}
}
textMeasure(&sp48, &sp4C, textptr, pFontChars, pFontFile, 0);
textMeasure(&sp48, &sp4C, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0);
if ((watch_item_is_actively_selected != 0) && (game_options_index == 0))
{
gdl = textRenderOutlined(gdl, &sp54, &sp50, textptr, pFontChars, pFontFile, sp5C, 0x7000A0, sp4C + 1, sp48, 0, 0);
gdl = textRenderOutlined(gdl, &sp54, &sp50, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), sp5C, 0x7000A0, sp4C + 1, sp48, 0, 0);
}
else
{
gdl = textRender(gdl, &sp54, &sp50, textptr, pFontChars, pFontFile, sp5C, sp4C, sp48, 0, 0);
gdl = textRender(gdl, &sp54, &sp50, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), sp5C, sp4C, sp48, 0, 0);
}
sp5C = 0xFF00B0;
@@ -3996,15 +4002,15 @@ Gfx *draw_watch_game_options_page(Gfx *gdl, Mtx *param_2) {
}
}
textMeasure(&sp48, &sp4C, textptr, pFontChars, pFontFile, 0);
textMeasure(&sp48, &sp4C, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), 0);
if ((watch_item_is_actively_selected != 0) && (game_options_index == 1))
{
gdl = textRenderOutlined(gdl, &sp54, &sp50, textptr, pFontChars, pFontFile, sp5C, 0x7000A0, sp4C + 1, sp48, 0, 0);
gdl = textRenderOutlined(gdl, &sp54, &sp50, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), sp5C, 0x7000A0, sp4C + 1, sp48, 0, 0);
}
else
{
gdl = textRender(gdl, &sp54, &sp50, textptr, pFontChars, pFontFile, sp5C, sp4C, sp48, 0, 0);
gdl = textRender(gdl, &sp54, &sp50, textptr, PORT_N64PTR(struct fontchar, pFontChars), PORT_N64PTR(struct font, pFontFile), sp5C, sp4C, sp48, 0, 0);
}
gdl = draw_toggle_options(gdl);
+6 -1
View File
@@ -1,4 +1,9 @@
#include <ultra64.h>
#if defined(PORT)
#include "portaddr.h"
#else
#define PORT_N64PTR(T, x) ((T *)(x))
#endif
#include <R4300.h>
#include "tex.h"
#include "image.h"
@@ -364,7 +369,7 @@ void texSelect(Gfx **gdlptr, struct sImageTableEntry *tconfig, u32 arg2, s32 arg
texLoad((s32 *)tconfig, NULL);
}
aa = PHYS_TO_K0(tconfig->index);
aa = PORT_N64PTR(u16, tconfig->index); /* MACOS-ADDR-WINDOW/M2: index holds the pool address */
tex = texFindInPool((aa)[-4], NULL);
if (tconfig->level == 0)
+1 -1
View File
@@ -1507,7 +1507,7 @@ void proplvreset2(enum LEVELID stageId)
/** Ugly matching hack.
* TODO: investigate if there's a way to get rid of this.
*/
if ((((u32) local_stage) ^ 0) + ((u32)vol->plink));
if ((((uintptr_t) local_stage) ^ 0) + ((u32)vol->plink));
#ifdef PORT
vol->plink = (void *) (((uintptr_t) local_stage) + ((u32)vol->plink));
Loaded 100 of 191 files, more files were not shown because too many files have changed in this diff. Show more