169 Commits
Author SHA1 Message Date
James Dansereau 4827bc868a release-notes/readme: cutscene fix covers all cutscenes (not just Dam); drop never-shipped locked-camera bullet; merge hipfire + F10 sensitivity into one mouse/input bullet 2026-09-20 17:35:16 -04:00
James Dansereau 92dbd015b5 Register GE_OBJT in the env-probe index (CI drift check) 2026-09-20 17:24:16 -04:00
James Dansereau 75e5902717 D321: map the D318 trigger chain (tanks -> chr 254 gas cascade -> combat bit, ~3.5s authored delay; derail same-tick; PC faithful to N64) + GE_OBJT probe extensions (AI-list ID, GAS prop timestamps) 2026-09-20 17:21:24 -04:00
James Dansereau b3b0ff74db v0.3.0 release review: watchdog re-arm, rule-2 sign-offs, GPL source removal, repo hygiene
Per the 2026-09-19 adversarial pre-ship review (findings.md M-192):

- d318watchdog.c: one-shot per OCCURRENCE, not per process -- re-arm after
  600 ticks of signature absence so a Facility replay in the same session
  (AllUnlocked) is still covered.
- model.c: RULE-2-SIGNOFF records for the retained M-183/M-185 and D311
  defensive clamps (user-approved retention as the stability-maximizing
  option; stale 'Threshold: 50.0' comment corrected to 10.0f; both marked
  post-release removal candidates pending full cutscene re-verification).
- reference/mouse-injector/: removed from the public repo (GPLv2 source +
  prebuilt discord-rpc.lib in an MIT project; never compiled here).
  Provenance stub README remains; path gitignored for local re-vendoring.
  AGENTS.md / GEPD-INPUT-PLAN.md / optionsoverlay.c refs updated.
- scratch/: untracked + gitignored (19 session logs/probe sources were
  committed in the release window).
- findings-index.csv: D317 (and 5 other stale rows) now CLOSED -- added
  CLOSED to gen_findings_index.py's verdict vocabulary.
- docs: D310 perf gauge (no change warranted), GE_DEBUG_UNLOCKALL noted in
  security-and-fidelity-status.md, release notes gain the missing D318
  watchdog bullet + FOV-clamp clarification.

Build verified: ./build-pc.sh ntsc-final clean; gen_findings_index.py
--check and gen_env_probes.py --check pass.
2026-09-20 10:51:12 -04:00
James Dansereau 63315030f8 docs: v0.3.0 release prep — final review pass
- GE-ENV-PROBES.md reconciled (18 new vars documented, GE_D243X3 tombstoned
  after the M-170 permanent fix removed it) — CI gen_env_probes.py --check passes
- release notes gif alt text: ~32 s -> ~12 s (matches README/index captions)
- agentic-development.md stats regenerated for the month-one milestone
- docs/index.md status bumped to v0.3.0, honest-status synced with README
- .gitignore: local/ working-junk dir
2026-09-20 10:09:33 -04:00
James Dansereau 448444ed38 D320 second pass: re-verify mechanism + high-risk rows against source; settle AI command-ID mapping
Review of the D320 entry itself. All mechanism claims confirmed against
source (chrHasStoppedOrPatroling ACT_ATTACK exclusion; update fn only moves
endframe via chrlvAttackActionRelated, never modelSetAnimation; ai_9/ai_12
structures re-read in full). Two precision corrections:
- ai_19 row: the ONLY progression out of the label 0x34 loop is own-flag
  bit 0x01 -> list 0x1504; the stop-check path re-aims at 0x08 (loops).
  Nothing in the list sets that bit (level's only chr_bitfield_set_on(,0x01)
  targets chr 0x43).
- ai_12 row: the update lands mid-hold by construction (aim cmd jumps to it
  on success); the 180-range-flag wait between is unreachable on this path.

Also settled the AI command-ID mapping for future reference: bondaicommands.h
IDs == cmdbuilder enum values from aicommands.def order (naive CMDNAME count
is off by 2; cross-checked on 4 known commands). if_guard_bitfield_is_set_on
= 0x96 = AI_IFMyFlags2Has (executing guard's OWN flags2); the per-chr family
(chr_bitfield_*) is 0x97-0x99.

Also audited d318watchdog.c in this pass: chraiGetAIListID and
sub_GAME_7F025560 signatures match; call form mirrors chraction.c:7396;
dead c78 can never satisfy the signature (ACT_ATTACK vs ACT_DIE/ACT_DEAD
disjoint); g_randomSeed is u64 as written. No changes needed.
2026-09-19 09:01:01 -04:00
James Dansereau cc169874ce D318: port-side watchdog + timeline probe for the Facility ai_22 softlock; D320: D318-class sweep of all level AI scripts; gfx_pc GE_D116 printf format fix
- D318 (RESOLVED): as-authored latent race — gas bit mid-monologue derails
  c78's pre-aim hold into a permanent ACT_ATTACK anim pin
  (actor_fire_or_aim_at_target_update moves only the endframe, never
  re-inits; N64-identical). NOT a port bug. Port-side watchdog
  port/src/d318watchdog.c (hooked in lv.c after g_GlobalTimer += ) detects
  the pinned state and re-inits the attack on confirm; GE_D318T=1 timeline
  diagnostic included. random.c gains an env-gated probe (GE_RANDPROBE) for
  bit-exact PRNG verification. Scratch evidence (dis/seqcmp scripts, addr
  dumps, session handoff) parked per house practice.
- D320 (OPEN): swept every level's AI scripts for the same aim-hold ->
  _update shape: 17 lists in 8 levels. Facility ai_19 and Control ai_9 are
  HIGH (non-self-healing, same as ai_22); Depot ai_12 MED-HIGH (objective
  bit gated behind a stop-check). Bond-combat loops likely safe (sustained
  fire stays inside the shoot window by design; exercised every playtest).
  Classified as-authored latent race(s), faithfully reproduced. Probe-verify
  path + generalize-the-watchdog recommendation documented in findings.md.
- gfx_pc.cpp: GE_D116 debug printf used %f for uint32_t args and %d for
  unsigned tile fields; casts added, build warning-free.

Full ntsc-final configure+compile+link PASS. Accuracy sweep write-up in
docs/dev/ACCURACY-AUDIT-2026-09-20.md (PRNG bit-exact; A1/shift32/wrapv/
PORT-hygiene/Gfx-Vtx/osVirtualToPhysical sweeps all clean or already fixed).
2026-09-19 00:38:14 -04:00
James Dansereau 276d0124dc D312/D313: fix bgBuildRoomVtxBounds G_VTX vertex-count misread on PC (Gfx_le dma.par bit shift) — n=16 batches collapsed to single-point bounds, failing the per-batch AABB pretest in bgTestBulletHitBackground; projectiles passed through those walls and bullets showed no impacts. One-line #ifdef PORT fix (recover params byte from words.w0 bits 16-23); N64 path untouched. User-repro verified (Runaway: mines collide, bullet impacts land on wall plane). 2026-09-18 18:12:14 -04:00
James Dansereau 340d6a595f D316/D317: fix F10 overlay mouse click mapping + row hit-band; D319: FpsCap 30/60 toggle
D316: optionsOverlayHandleInput() mapped clicks with a naive window-size
scale that ignored the safe-area crop, and overlayRowAtY()'s hit-band was
asymmetric (biased downward) -- together these could land a click on the
row below the one clicked. Fixed by adding gfx_get_ui_screen_rect() (reuses
the real forward transform from gfx_adjust_viewport_or_scissor) to invert
clicks correctly, and centering the row hit-band.

D317 (reported as a crash toggling Game.AllUnlocked): confirmed instant
close, not a hang -- closed as a symptom of D316 (the hit-band bias let a
click on "All unlocked" fall through to the adjacent "Quit to desktop" row).

D314 (F10 flicker on main menu): mechanism still unpinned; landed an
env-gated diagnostic probe (GE_D314=1) for a future repro run. Confirmed
still present after the D316 fix (different bug class) -- deprioritized
per user call.

D315 (menu scaling main-menu vs in-game): disclosed as a known issue,
deferred to v0.3.1.

D319: Video.FpsCap's free 0-360 slider was mostly a dead zone (D186: sim
tick ceiling caps it at 60/50, and values under 30 already got force-
uncapped) -- replaced with a plain 30/60 cycle (ROW_FPSCAP, mirrors the
existing ROW_MSAA pattern); default changed to 60 so a fresh config shows
a reachable value. Uncapped stays valid at the config level for old inis
but is no longer offered in the menu.
2026-09-18 16:47:48 -04:00
James Dansereau 69cc9790ac README: credit f1zz1ec0ke for LLM consultation (v0.3.0) 2026-09-18 13:27:32 -04:00
James Dansereau ce05ae7187 Park D309/D310/D311 session evidence logs + referenced diagnostics in scratch/
- Probe logs preserved as evidence: pre-fix standing-still guard lines,
  v1 Trevelyan regression (local-list embedded PRINT strings), clean
  post-fix Caverns run (global PRINTs size=1/global=1 hit once each incl.
  the D309 spin record; local PRINTs NUL-scanned with valid landings; zero
  LABEL-NOT-FOUND / D156 / watchdog lines), D311 freeze watchdog+stack dump.
- D309/D310 diagnostic files referenced by findings.md: frozen thread
  stacks, live m_RunToBondPersistent byte dump, AI-list decoders (original
  NUL-scan + fixed global-only sizing), command name table.
2026-09-18 13:22:50 -04:00
James Dansereau c67479866c D310 v2: scope PRINT sizing fix to global AI lists; D311: close D156 guard hole
D310 (RULE-2-SIGNOFF, user-confirmed fixed): the uniform 1-byte AI_PRINT
size broke level-local lists, which embed a NUL-terminated string after
the PRINT byte (Caverns Trevelyan ran circles with glitched anims).
chraiitemsize is back to byte-identical decomp; new PORT-only
d310ItemSize() applies the 1-byte size only for lists confirmed global
via chraiGetAIListID, used at ai()'s AI_PRINT step and chraiGoToLabel's
label scan. Local-list behavior unchanged from the original.

D311 (new, guarded): live freeze in Caverns diagnosed via GDB — Bond's
own anim tick spun ~3.1e9 iterations in modelSetAnimFrame2WithChrStuff:
animframe1 poisoned to -1.67e31 upstream, and the D156 guard validated
only frameb, so 'frameb = framea' passed both garbage values through and
floorFloatToInt saturated them to far-apart ints. Extend the guard to
validate both framea/frameb (snap bad one to the valid other, else 0)
and heal model->animframe1/2 before the fallback re-reads them. Same
file/pattern as the accepted D156 guards, #ifdef PORT only. Upstream
poison source (oldspeed=-1.75e18 at transition start; timespeed=0,
unkb0=0) recorded open in the D311 entry.

Post-fix Caverns playtest: guards dash-pause-resume, Trevelyan pathing
normal, Caverns->intro clean. Probe log: global PRINTs size=1/global=1
(hit once each), local PRINTs NUL-scanned/valid landings, zero
LABEL-NOT-FOUND, zero D156 fires, zero watchdog freezes.
2026-09-18 13:18:16 -04:00
James Dansereau 0c700852e2 D310 (RULE-2-SIGNOFF): size AI_PRINT records at their true 1-byte width in chraiitemsize (#ifdef PORT) — fixes D309 spin + Caverns standing-still guards 2026-09-18 12:55:06 -04:00
James Dansereau 0f8bd90aaf D309: extend GE_D309 probe to AI_PRINT step-ons and label-not-found restarts (diagnosis only) 2026-09-18 12:43:48 -04:00
James Dansereau 8be34ba1f6 D309: add GE_D309 diagnostic probe at chrGoToBond failure branches (diagnosis only, #ifdef PORT) 2026-09-18 12:31:46 -04:00
James Dansereau ede16415f2 docs: D309 diagnosed — Caverns->intro complete freeze, phantom GotoNext(lblZero) via AI_PRINT NUL-scan mis-size 2026-09-18 12:25:33 -04:00
James Dansereau 3085775d52 Merge branch 'main' into release/v0.3.0
# Conflicts:
#	.github/release-notes.md
#	README.md
#	docs/dev/findings-index.csv
#	docs/dev/findings.md
#	src/snd.c
2026-09-18 10:55:16 -04:00
James Dansereau 444b172cb8 docs: v0.3.0 release prep — D251 closed, security page published, notes cleanup
- release-notes: drop the internal 'carried forward' meta paragraph; move the
  D251 bottom-row duplication out of Known Issues into the F10 fix bullet
  (no longer occurring per user live playtest of the D304 build)
- findings: close D251's last symptom; mark D304 live F10 playtest done;
  de-stale D266/D271 'uncommitted' cells (landed in 532092c7); regen index CSV
- README: link the security & fidelity status page
- docs/security-and-fidelity-status.md: publish — re-verified for v0.3.0 on
  2026-09-18 (bundle contents, no-networking source check, asset converter);
  D271 now landed; D284 RNG history reworded as found-and-fixed pre-v0.3.0

Verified: build-pc.sh ntsc-final clean (243/243); tools_pc/verify.sh sweep
21/21 PASS on this tree (scratch/verify_sweep_v030_final.json).
2026-09-18 10:54:00 -04:00
James Dansereau 416396e504 D302/D304/D225/D164/D252/D305: perf regression, F10 menu overhaul, small fixes pass
- D302: fix a reintroduced D250-class perf regression -- 4 uncached getenv()
  calls added by D236 diagnostic passes 10/12/14/16 were running on every
  triangle draw in gfx_sp_tri1; cached with the established pattern.
- D304: F10 mouse-sensitivity menu overhaul -- widened Input.AimModeSens'
  clamp to match Input.MouseTurnSpeed, then collapsed both plus the
  aim/turn link toggle into a single Input.MouseSensitivity control per
  user request (avoids decoupling into a confusing state). Fixed two real
  overlay bugs found along the way: overlayUpdateScroll() bottom-anchored
  the clicked row on every click (not just when off-screen), and
  overlayRowAtY()'s hit-test scanned rows past the last one actually drawn,
  letting a click near a scroll boundary silently hit the wrong row
  (reproduced as "All unlocked" triggering "Quit to desktop" instead).
- D225: GE_DEBUG_UNLOCKALL=1 dev-tooling toggle (file2.c), default off.
- D164/D76: docs-only correction -- the legal-screen one-line fix was
  already committed 8dfc578f4; findings.md just never caught up.
- D252: root-caused and fixed a 6+-session-old rainbow-particle bug --
  explosionRenderPart() wrote 5 vertices into a 4-vertex allocation,
  corrupting the next particle/effect's data in the shared vertex pool.
  User-confirmed live: "Rainbow went away."
- D305: Steam Deck SIGSEGV recurrence past D285's own fix -- sndHandleEvent's
  preempt-scan dereferenced its list-walk pointer before checking it for
  NULL. Added the missing guard. User-verified crash-free on the Deck
  across multiple sessions, including toggling Game.AllUnlocked off/on.

D236 pass-16 diagnostic (GE_D236RAW, bg.c) carried along from a prior
session's in-progress investigation; unchanged, env-gated, zero-cost unset.
2026-09-18 09:56:23 -04:00
James Dansereau e7c76cf8eb D236 pass 13: live capture demotes draw-order theory, redirects to tree-card fog
User played live with GE_D236ORDER=1, walked toward Surface1's tree line;
wall bug still reproduced. Tree-card count reached 121 tris (vs 2-4 in
deterministic boot), and draw order flips favorably at high counts (tree
drawn last, as pass 12's theory needed) -- but the bug persisted anyway in
that same zone, demoting draw-order as the dominant cause.

Redirects to a lead D280 flagged but never closed: the tree class's own
fog computation (bimodal 25/255 per vertex, never verified against N64,
unlike the noise class's fog which D280 already confirmed correct). Not
attempted this pass -- next session's starting point.

Not v0.3.0-blocking, parked again per standing user priority.
2026-09-18 02:36:49 -04:00
James Dansereau db823d0261 D243 M-189/M-190: fix Bond cutscene root cause, camera tracking, mouse-lock; D236 pass 12 diagnostics
D243/D173/D292 (Bond cutscene family) -- root cause found and fixed:
bondview2.c's cutscene body-model buffer layout reserved space using a
hardcoded 32-bit sizeof(Model) literal that never accounted for 64-bit
pointer widening (real size 232 vs reserved ~188 bytes), so the model's
own animation-data buffer overlapped its tail fields (playspeed/animrate/
unkac/unkb0/unkb4), corrupting them on every cutscene body-model creation.
Fixed by computing the real sizeof(Model), matching the identical idiom
already used one line above for ModelFileHeader. User-confirmed live.

Follow-up camera-tracking fix: replaced the old shake-workaround freeze
(which also blocked legitimate per-shot tracking) with a write-every-tick
scheme plus an instant re-seed of the look-at filter's accumulator at each
teleport, eliminating the old catch-up shake without freezing anything.

Second bug found in the same review: mouse-look was never gated during
scripted/no-control camera states (an already-documented but never
implemented safety gate in port/src/input.c's hipDirectCompute). Fixed by
adding the missing check, reusing gameScriptedCameraActive() (renamed from
D243's d243mCutsceneActive, since it's no longer D243-specific -- also
used by model.c's M-183/M-185 clamps, which were separately found to be
dead code for real players (gated on the diagnostic env var, not just
camera mode) and fixed to be unconditionally active.

D236 (Surface tree wall) pass 12: new mechanism found (both competing
texture classes are Z_UPD=0 decal-mode draws, so on-screen precedence is
decided by draw order, not depth) and a headless capture finally landed on
the actual bugged view -- but that view has zero tree-class triangles
present, so the mechanism couldn't be tested there. Two new env-gated
diagnostic probes (GE_D236ORDER, GE_D236ZFIX) left in tree for the next
pass; not a fix, not v0.3.0-blocking.

All findings.md summary-table rows touched this session cross-checked
against their full entries and corrected where stale (D219/D252/D246/D247
had been fixed in a prior session but their index rows still said
OPEN/NOT-A-BUG).
2026-09-18 02:30:17 -04:00
James Dansereau 643785c2c5 Widescreen FOV Phase 4, D246/D247 overscan crop, D219 rainbow-spark fix
- Widescreen/FOV Phase 4: automatic vertical-FOV scale by window aspect
  ratio (Video.WidescreenAuto, default on), composed before the existing
  manual Video.FovScale multiplier. New symmetric FOV floor (20 deg)
  alongside the existing 160 deg ceiling.
- D247/D246: the game's own TV-overscan safe-area viewport inset (real on
  N64, hidden by CRT overscan, visible as black bars on PC with no crop)
  now gets cropped instead of shown -- gfx_adjust_viewport_or_scissor
  remaps against the actual captured gameplay viewport bounds (vertical)
  plus a measured, resolution-independent 1-logical-unit trim (horizontal)
  instead of the fixed VI canvas size. New Video.SafeAreaCrop toggle,
  default on.
- D219 (rainbow bullet-spark colors), a real 32->64-bit ABI bug:
  bullet_spark_render (glass2.c) read the spark's stored RGBA color via a
  hardcoded raw byte offset instead of the named s_bullet_spark struct
  field -- on 64-bit PC an earlier void* field's widening + alignment
  padding shifts the true offset by 4 bytes, so the read landed in
  unrelated f32 velocity fields instead. Fixed by reading the named
  fields. Also fixed an adjacent UB (a 16-byte Vtx read from a 4-byte
  global) in the same function. This turned out to be the actual root
  cause behind the D252 "lingering particle sprites after an explosion"
  report as well (same render path via objHit and friends) -- closed as
  a duplicate.
- Moved docs/dev/WIDESCREEN-FOV-PLAN.md to docs/dev/notes/ (gitignored
  local notes), matching this project's convention of keeping
  in-progress planning docs out of the tracked tree.

User live-verified all of the above interactively this session.
2026-09-18 01:10:27 -04:00
James Dansereau 814ecf5fce D243 M-183/M-185: playspeed/endframe clamp workarounds for Bond animation issue
Added two #ifdef PORT workarounds in modelTickAnim() to address D243's
Bond position/animation issue during scripted camera modes:

M-183: Clamp playspeed > 10.0 to 1.0 (prevents rapid advancement ~97 frames/tick)
M-185: Clamp corrupted endframe values (< 0.0 or > 1000.0) to 100.0

Both are env-gated on GE_D243M and apply only during scripted camera modes.
The clamp fixes prevent the rapid animation advancement but Bond's animation
still 'repeats' — he can't complete his animation during the Dam abseil cutscene.

Root cause analysis: playspeed/endframe corruption is consistent with the
D100/D140 struct layout/punning family (32→64-bit pointer widening causes
fields to be read from wrong memory locations). Playspeed observed at 388.82
(expected ~1.0), endframe as huge negative values like -604462909807314587353088.0.

Updated findings.md §F table entry for D243 to reflect partial fix status.
2026-09-17 23:10:59 -04:00
James Dansereau 4ccab22ce2 D243: ship permanent fix for Dam abseil camera shake (M-170)
Convert the GE_D243X3 experiment into an always-on fix by removing
the env-gating. The fix freezes field_488.pos (the camera's look-at
target derived from the model's animated-skeleton transform) during
POSEND camera mode, re-baselining only when a legitimate shot-change
teleport fires (detected via the d243TeleportEpoch counter incremented
by d243NotifyTeleport() in chrai.c's AI_TRYTeleportingChrToPad handler).

This eliminates the camera shake while allowing legitimate inter-shot
repositioning. The fix is decomp-faithful - it targets the camera's
reaction to the animation cycle rather than suppressing Bond's own
animation, which was found to be playing correctly.

Also includes:
- GE_DEBUGMENU diagnostic in boss.c (prior session)
- GE_D243SWIRL probe in bondview2.c (prior session)
- M-178 finding documentation (rapid animation advancement analysis)
- D243 biganim probe in model.c (for future investigation)
2026-09-17 22:38:52 -04:00
James Dansereau 6938d5644d docs: add vendored GEPD Mouse Injector reference + input-overhaul plan
Both were already documented as standing repo content (AGENTS.md's
critical-files table, several findings.md entries citing D194/D300's
ground truth) but never actually committed -- add them so the repo
matches what's already written about it. GPLv2, read-only reference,
never compiled as part of this build (see reference/mouse-injector/README.md
for provenance).
2026-09-17 19:31:33 -04:00
James Dansereau ee4ea96258 docs(D300): correct stale status -- user already live-confirmed hipfire fix
findings.md and the index still said "pending manual playtest" / "pending
build-verify", but HANDOFF.md records the user played it live this session
and confirmed better feel. Regenerated the index via gen_findings_index.py.
2026-09-17 19:30:38 -04:00
James Dansereau 2fe0e1c83e fix(D301): NULL-guard fileGetIsCheatUnlocked() against unvalidated saves (GitHub #87)
Closes the unguarded save deref at its single choke point instead of
patching each call site (front.c mode-select, file.c briefing check).
Build-verified, crash-free smoke test; live confirmation against the
exact #87 repro still owed (see finding for detail).
2026-09-17 19:30:05 -04:00
James Dansereau 52a3e9d938 D173/D243: add CONSOLIDATED NEXT STEPS block for D173 (M-87..M-176); correct a self-introduced level-34 doubt
D173 gets the same "read this one block, not the whole history" treatment
D243 already has -- established facts, what's ruled out, and the concrete
next step (a D252-style raw ROM byte dump of the extending_left_hand clip
at frame 95) in one place.

Also retracts an unfounded doubt raised earlier this session about whether
-level_34 is really Facility -- a hand-count of setup_text_pointers[] alone
suggested otherwise, but multiple already-established findings in this same
file (D130, D203, D217, D255, and a full independent static decode of
UsetuparkZ.c during the Trevelyan/Ourumov investigation) confirm it is;
"ark" is just Facility's internal decomp codename, not Park/Archives.
Checked before letting the doubt stand.
2026-09-17 16:20:28 -04:00
James Dansereau f6c3cfe421 D173 M-176: user's live N64 comparison confirms genuine divergence, narrows to an asset-decode bug
User directly compared this port's level-start sequence against real N64
from memory: N64 shows Bond grounded, arms at sides, for 1-2s before he
draws his gun and surveils left-right (~0.25-0.5s later) as the camera
moves to first-person. This port shows Bond floating from the very first
frame his model appears -- never briefly grounded.

Traced the second phase too: chrlvTickBondIntro (chraction.c:7663, also
decomp-verbatim) is a watchdog that switches Bond from the held
"extending_left_hand" pose to fire_standing_draw_one_handed_weapon_fast
once the first pose wraps -- confirmed firing live. The two-phase structure
matches the user's description exactly; only the first phase's visual
(grounded vs floating) diverges.

Checked one more candidate: c_item_entries[BODY_Formal_Wear].pov = 1.058,
a small plausible-looking scale constant, not an obvious corruption --
doesn't distinguish good data from bad on its own.

With theory (b) (faithful N64 behavior) now ruled out by direct comparison,
and the selection/scale code confirmed clean, this narrows to bucket (a):
the extending_left_hand clip's own keyframe data being mis-decoded for this
port around frame 95. Next step is a D252-style raw ROM byte dump, not more
source tracing.
2026-09-17 16:17:30 -04:00
James Dansereau ca01cb9ba9 D173 M-174/M-175: root-caused the intro puppet's animation to a decomp-verbatim table entry
M-174: tried firing_animation_groups[group][sub] as the source of the
puppet's initial animation -- zero hits, ruled out (negative result logged
so it isn't re-guessed next time).

M-175: instrumented modelSetAnimation itself (the one choke point every
caller funnels through) with a __builtin_return_address probe filtered to
the player's own bodyModel, then resolved the logged addresses with
addr2line. Found the exact call site on the first try: bondviewSetCameraMode
(bondview2.c:936-942), inside CAMERAMODE_SWIRL's intro-fly-in setup. It
reads stage_intro_anim_table[] (bondview.c:355), a decomp ground-truth
table with zero #ifdef PORT in the whole call chain -- entry [0] is
{PTR_ANIM_extending_left_hand, 95.0, -1.0, 0.02}, and PTR_ANIM_extending_
left_hand is confirmed index 61 in animation_table_ptrs1[], an exact match
to the aidx=61/animframe~96 measured live on both Dam and Facility in M-172.

Conclusion: the selection logic is fully decomp-verbatim, not a port bug.
If the puppet genuinely floats while playing this held pose, the remaining
candidates are narrowed to exactly two: (a) this specific clip's keyframe
data is mis-decoded for this port at/around frame 95 (asset-pipeline bug,
D252-style raw-byte dump would confirm), or (b) this is real N64 behavior
and something else (camera framing, or just how it always looked) is the
actual source of the "floating" reports. Distinguishing the two needs N64
reference footage or a raw asset dump, not more source tracing -- this
thread is static-exhausted for this session. Captures archived at
scratch/d173/ (gitignored, local).
2026-09-17 15:48:59 -04:00
James Dansereau 1b48577367 D173: correct M-173's write-up before it goes stale -- chrTick is shared, not a separate player draw path
Caught this while writing the next-step paragraph, before committing to a
wrong claim: the render-position computation (subcalcmatrices, chr.c:2777)
runs inside chrTick, which is called identically for the player's own prop
and every guard -- there is no separate "player draw path" for it to skip a
step in. The real divergence is which INPUT feeds that shared machinery:
bondview2.c's firing_animation_groups[group][sub] table, reached only by
the player's own per-tick animation-select call, versus guards' own
AI-driven animation selection. Retargets the next step at comparing that
specific table entry's keyframe data against a comparable guard-idle entry,
rather than a code-path diff that doesn't actually exist.
2026-09-17 14:52:11 -04:00
James Dansereau 9b75d75934 D173 M-173: control experiment confirms bug is isolated to player-puppet draw path, not generic chr rendering
Added a before/after prop->pos probe spanning all of chrTick (not just the
player), gated on the existing d243mProbeActive(). Checked four background
guards on Dam with different actiontypes/animations/positions: all show
~zero before/after gap, nothing like the player puppet's ~328-unit float
measured in M-172. Rules out "every chr's root joint sits above prop->pos by
design, normal rendering just hides it" -- normal chr rendering is correctly
grounded. Narrows the remaining lead to the player-specific bodyModel draw
call (bondview2.c, near the existing D243M: draw/f488write probes) versus
the generic chrRenderProp path every other chr uses. Diagnosis only, no
game-code changes. Capture archived at scratch/d173/ (gitignored, local).
2026-09-17 14:51:28 -04:00
James Dansereau 447ef22162 D173 M-172 cont'd: cross-level replication (Facility) + ruled out two more candidates
Facility (-level_34) reproduces an almost identical ~329-unit float vs Dam's
~328, despite completely different ground heights/positions -- strong
evidence of a fixed constant rather than a level- or animation-scale-driven
effect. Dispatched a local Qwen triage pass on solo_char_load; it raised
modelSetAnimTranslationScale/pov as a candidate but correctly self-qualified
it as inconsistent with the now-cross-level-confirmed near-constant offset.
Own follow-up trace ruled out two more candidates: the puppet's position
setup matches the normal guard init path exactly (setsuboffset after
init_GUARDdata_with_set_values), and the puppet's first animation's nonzero
start frame (~96) is a legitimate firing_animation_groups table constant,
not carried-over garbage state. Leading theory now: the ~328-unit elevation
is baked into that animation's own keyframe data (root/pelvis joint height),
and whatever normally compensates for a chr's third-person render staying
grounded isn't reached by this puppet's draw path. Next step (compare a
normal guard's raw root-joint gap the same way) is static/no-live-access,
logged in findings.md.
2026-09-17 14:47:49 -04:00
James Dansereau 6f604c14cf D243 M-170/M-171 + D173 M-172: playanim probe refutes empty-driver theory; first quantified D173 measurement
D243: added a GE_D243M-gated probe on AI_PlayAnimation. Live data shows the
abseil's scripted fall animation (decoded anim_id 0xb1) lands on Bond's own
real player chr/model (isPlayerChr=1), directly refuting the standing M-145
"empty, model-less driver chr" theory. The animation plays and loops
correctly (aidx stable, animframe cycling normally) -- this reframes the
whole bug: the render-position "cycling" this session's GE_D243X3/X4 freeze
experiments were built to suppress is very likely the animation's own
legitimate looping root motion, not corruption. GE_D243X4 (Bond's own
render_pos freeze) is flagged as actively wrong and should not ship;
GE_D243X3 (camera field_488.pos) is on firmer ground but should ideally
damp on loop-wrap, not freeze-forever. Next step (camera-shake vs loop-wrap
correlation) needs a fresh unfiltered capture, logged as open.

D173: used the same probes to capture the intro/pre-control puppet cutscene
autonomously (no player input needed to progress it) on Dam. First-ever
quantified measurement: prop->pos.y is rock-steady at the correct ground
level (60.3) for the puppet's whole on-screen lifetime, while its rendered
root sits ~324-328 units higher throughout -- and the gap barely moves
across three different attached animations, arguing against an
animation-root-motion cause and pointing at a fixed root-joint/base-transform
offset in the puppet's spawn setup (solo_char_load) instead. Measurement
only, no code change; capture archived at scratch/d173/ (gitignored, local).
2026-09-17 14:42:48 -04:00
James Dansereau 8ba1be14f2 D243 M-169: real teleport signal wired in, root mechanism confirmed decomp-faithful
Implements next-steps (b) and (c) from the D243 CONSOLIDATED NEXT STEPS
block. (c): static analysis shows the render_pos-cycling mechanism (chrTick's
cutscene-unaware animation dispatch) is unmodified decomp code with no gate
on either platform, so it's decomp-faithful by construction -- clears the
way to ship a port-layer fix without a rule-2 sign-off. (b): chrai.c's
AI_TRYTeleportingChrToPad now notifies a new epoch counter right after its
real setsuboffset() call; GE_D243X3/GE_D243X4 re-baseline on that signal
instead of freezing forever, targeting the M-168 "camera looks wrong way for
later shots" / "Bond still looks stuck" incompleteness.

Build/link/crash-safety verified headlessly (two 25-30s -level_09 smoke
runs, both crash-free). Live Dam-to-abseil playtest still owed before this
can replace the diagnostic gating with a shipped default.
2026-09-17 13:59:47 -04:00
James Dansereau b69cdbe6af D243/D148: consolidated next-steps synthesis for the cutscene bug family
Adds a single "read this, not the whole M-107..M-168 saga" block to the
D243 findings.md entry: what's established (two independent render
paths, the stale-animation mechanism, the wrong-chr animation
targeting, camera shake fixed diagnostically, model duplication fix
incomplete because it breaks legitimate per-shot repositioning) and
three concrete, not-yet-attempted next steps (delta-threshold
re-baseline, hook AI_TRYTeleportingChrToPad directly, determine the
actual N64-vs-PC divergence point before committing to either).

GRAPHICS-BACKLOG.md's D148 row (last meaningfully updated ~M-87,
long stale) rewritten to match -- it was the only other place a future
session would look for cutscene-bug status and didn't reflect any of
this session's work.
2026-09-17 13:46:10 -04:00
James Dansereau d2caaa09bc D243 M-168: render_pos freeze -- 2 crashes fixed, stable but incomplete
Extended M-167's freeze technique to render_pos (Bond's own draw
position), aiming at the model-duplication symptom M-167 didn't fix.
Two live crashes along the way, both from the same root cause: model
pointers get reused by the allocator across different logical model
instances, so any attempt to skip subcalcmatrices() based on inspecting
render_pos's value or caching pointer identity can be fooled into
treating a brand-new, uninitialized instance as already-validated --
leaving render_pos at its fresh tombstone value for a later consumer
(sub_GAME_7F06C768, computing zDepth) to crash on.

Fixed by never skipping subcalcmatrices() at all -- it always runs now,
guaranteeing render_pos is never left invalid regardless of model
lifecycle. The freeze is purely a contents copy on top of a
now-guaranteed-valid buffer. Live-playtest stable (no crash, full
session).

But the result is informative-negative: freezing unconditionally also
suppresses the LEGITIMATE per-shot camera/model repositioning
(AI_TRYTeleportingChrToPad moves Bond between several distinct shots
during the cutscene), not just the illegitimate intra-shot cycling --
so the camera now looks the wrong direction for later shots and Bond
still appears stuck. Both GE_D243X3/GE_D243X4 remain diagnostic
experiments, not real fixes. Documented two next-step candidates
(delta-threshold re-baseline, or hooking the teleport call directly)
for distinguishing legitimate repositioning from the cycling bug.

porting-notes.md gets the generalized lesson: freezing a value and
freezing a pointer target are different operations -- the latter needs
the call that keeps the pointer valid to always run, with the freeze
applied to contents afterward, not to whether the call happens.
2026-09-17 13:43:08 -04:00
James Dansereau 53219c18ae D243 M-167: camera shake mechanism confirmed live -- first positive result
M-163's live playtest smoothed prop->pos completely (log-verified) but
the visible camera shake was unchanged -- the camera's look-at filter
doesn't read prop->pos at all. It reads field_488.pos, a separate
variable computed from render_pos (the model's own animated-skeleton
transform), independent of prop->pos even though both happened to show
the same period-4 cycling symptom through a probe that only logged one
of them.

Retargeted the suppression at field_488.pos's actual write site
(bondview2.c:10495-10497, GE_D243X3-gated, freezes it while POSEND is
active). User's live playtest: "yep camera shake is gone now" -- the
first positive result across the whole D243 investigation.

Not a real fix -- GE_D243X3 unconditionally freezes state rather than
correcting why the model's skeleton animation cycles in the first
place. User also reports Bond's own rendered body still visibly
duplicates/glitches, expected since this only freezes the camera-feed
copy, not the render_pos draw-path read -- likely the same underlying
mechanism observed through a different consumer, not yet isolated.

porting-notes.md D11 gets the generalized lesson: confirm the actual
consumer reads the field you fixed before spending a live-test cycle
on it -- two fields can show the same symptom from one shared root
cause while being genuinely independent data paths.
2026-09-17 13:06:57 -04:00
James Dansereau e4a870484b D173/D292: cross-reference D243's chrUpdateAnim mechanism finding
The Dam abseil cutscene investigation (D243 M-160/M-164/M-165) traced a
general pattern likely shared by the whole "third-person world model
transform/lifecycle" bug family already flagged at M-145: chrUpdateAnim
reads a puppet's animated root-joint position back into its logical
position, so a transition that moves the puppet without also updating
its animation state produces a position that visibly lags/diverges,
tracking the stale animation instead of the intended spot. Plausible
explanation for D173's level-start float and D292's death-sink, not
confirmed for either -- flagged as the thing to test with a live capture
using the same aidx=/animframe= technique D243 M-166 just added, rather
than re-deriving a new mechanism from scratch next time either is
investigated.
2026-09-17 12:50:42 -04:00
James Dansereau d9c7705654 D243 M-162..M-166: corrected decisive experiment + deep root-cause tracing
Two third-party reviews caught a real flaw in the earlier M-161 negative
experiment: it restored prop->pos AFTER chrTick() returned, but
render_pos/field_488.pos are computed INSIDE that same call -- so M-161's
"no change" result never actually tested M-160's overwrite hypothesis.
Verified this directly against the capture data (a clean period-4
same-frame before/after-chrTick offset proves the lane-advance happens
entirely inside chrTick) before accepting the review.

M-162: corrected save/restore placement (chr.c), Z-only. Live playtest
confirmed the mechanism -- Z went smooth in the log -- but the visible
glitch persisted because that capture's chr cycled on Y instead.

M-163: widened the experiment to the whole prop->pos vector. Built and
smoke-tested; not yet live-playtested (exe was busy/user stepped away) --
this is the ready-to-test build for the next session.

M-164/M-165/M-166 (static tracing, no build needed): chrUpdateAnim reads
the model's root-joint position back into prop->pos after advancing
whatever animation is attached (getsuboffset) -- not an arbitrary
overwrite. The cutscene's teleport (AI_TRYTeleportingChrToPad) re-anchors
the root joint but never touches animation state; the real rappel
animation commands target a separate invisible driver chr (M-145), not
the visible one. Traced the Dam exit script (UsetupdamZ.c) directly and
found no animation-set command for Bond anywhere in it -- his head
animation gets forced back to an idle/walk loop by
bondviewFrozenMoveBond's own tick, but the *phase* within that loop is
inherited, not reset. This raises a real possibility that the mechanism
is decomp-faithful and the actual PC/N64 divergence is upstream of the
cutscene code entirely (which animation phase Bond happens to be on when
the cutscene grabs him). Extended D243M: onscreen with aidx=/animframe=
(reusing GE_D193A's reverse-resolve technique) so the next live capture
can test this directly.

Docs updated throughout (findings.md, findings-index.csv,
GE-ENV-PROBES.md); HANDOFF.md refreshed with full session state (local,
gitignored).
2026-09-17 12:49:19 -04:00
James Dansereau fec396f53b D300: hipfire direct camera look (WI-1); D243 M-159/160/161 investigation
D300: new hipDirectCompute() in port/src/input.c writes vv_theta/vv_verta
directly for hipfire mouse-look, mirroring GEPD's linear px->degree model
and the existing D194 aim-mode pattern -- replaces the N64 stick-curve
routing that saturated at ~13 px/poll (issue #89). Ported GEPD's
dead/watch/pause safety gates. Input.MouseDirectLook=1 default, escape
hatch to the legacy path. User-confirmed better feel in a live playtest.

D243: added a permanent onscreen-visibility probe (D243M: onscreen,
chr.c) to test whether chrTick's posIsOnScreen gate explains the
abseil cutscene's frozen render position (M-159). A live capture
refuted that and found the real mechanism instead: chrTick overwrites
prop->pos with a stale value via its own chrUpdateAnim/modelTickAnim
animation-position logic, racing the AI script's scripted write
(M-160). A decisive throwaway experiment neutralizing that overwrite
produced no visible change in a live playtest (M-161) -- the mechanism
is real but isn't the dominant visible cause. Experiment reverted;
D243 parked pending a new lead after ~15 session-passes without a fix.

Docs: findings.md/findings-index.csv/porting-notes.md/GE-ENV-PROBES.md
updated throughout.
2026-09-17 10:56:41 -04:00
James Dansereau 7b405abc63 fix(D299): Game.SkipIntro crash selecting most save slots
fileValidateSaves() -- the only place saves[] is populated from EEPROM
-- is called exclusively from the legal screen's init, which
Game.SkipIntro's boot-to-file-select shortcut skips entirely. saves[]
was left all-zero, so fileGetSaveForFoldernum() returned NULL for
every folder except whichever one a zeroed save_data happened to
decode to, crashing file-select's unguarded fileGetIsCheatUnlocked()
for the rest. Call fileValidateSaves() from the shortcut too
(port-layer completeness fix, zero game-logic change).

Live-verified by user: all 4 save slots now selectable with
SkipIntro=1, intro still correctly skipped.
2026-09-17 09:43:44 -04:00
James Dansereau 85a165d74b porting-notes: findings-index.csv is generator-derived — edit the §F table row, not the CSV 2026-09-17 08:50:48 -04:00
James Dansereau f70284b5be D219/D252 M-158: fix §F table status cells (index CSV is generator-derived — update the source row, not the CSV) 2026-09-17 08:50:33 -04:00
James Dansereau 30dcbea085 D219/D252 M-158: rainbow bug not reproducible in 6 identical dev-box runs; classified flaky/environment-dependent; probes left live for next occurrence 2026-09-16 23:15:45 -04:00
James Dansereau 6d34a880b3 D219 M-158: GE_D157 gains D157T (texture-identity-keyed vertex full-path) + D157I (import census) probes 2026-09-16 23:15:45 -04:00
James Dansereau 145d3f46ac D219 M-157 correction: measured G_LIGHTING for glass2.c sparks, theory doesn't hold up
Went back and actually tested the G_LIGHTING hypothesis from the
previous commit instead of leaving it as an untested code-reading
inference. Two things prompted this: (1) noticed a code comment near
the existing GE_D172 probe documenting a live result never written up
in findings.md -- "G_LIGHTING is ruled out (5000/5000 samples off)" for
explosion.c's particles specifically, meaning the sibling case this
session's theory leaned on was already empirically disproven, not
merely unconfirmed; (2) that's exactly the kind of unchecked assumption
this session's own rule-2 taxonomy work was written to prevent.

Added GE_D157 (gfx_pc.cpp, gfx_sp_vertex, env-gated, cached getenv):
logs geometry_mode/LIGHTING + resulting shaded RGB whenever a vertex's
raw color bytes match one of glass2.c's g_BulletSparkColors[] entries.
Re-ran the identical Bunker1 repro that produced the magenta/green/cyan
screenshots -- only 2 hits, both LIGHTING=off, both unaffected white,
no hit at all for the pale-yellow value the door-spark path should
produce. Reading: G_LIGHTING likely isn't the mechanism here either
(consistent with the sibling case), though the probe may simply not be
catching the real spark vertices yet (exact-byte-match is fragile).

Corrected the D219 finding to flag the "cross-validates M-110" framing
from the previous commit as superseded, not delete it -- the reasoning
trail (including the mistake) stays visible so a future session doesn't
repeat it. Root cause of the visually-confirmed rainbow bug remains
open; next steps revised to a texture-identity-keyed probe variant.

Added GE_D157 to GE-ENV-PROBES.md. No src/game changes. findings-index
regen was a no-op.
2026-09-16 22:25:39 -04:00
James Dansereau c58797c2da D219 M-157: first visual capture of the rainbow-particle bug, cross-validates M-110's G_LIGHTING lead
Chasing D252 further, got the first-ever screenshot proof of this bug
family: a plain bullet-impact spark on a Bunker1 door cycling magenta ->
purple -> green -> teal -> purple across consecutive shots, captured
headlessly via GE_INPUTSCRIPT (Z fire pulses) + GE_PCDUMP together, then
reading the resulting PPMs directly (PIL) instead of only grepping a
probe log.

Traced the call chain (glass2.c's bullet_spark_render, a different code
path from both D219's explosion.c particles and D252's IMAGE_FIRE_N
system) and ruled out three mechanisms with actual code evidence before
finding one that fits:
- Not D252's fire-tile system (GE_D252 got zero hits on this repro).
- Not a struct-layout/ABI bug (sImageTableEntry is 12 bytes on both
  32- and 64-bit, no pointer members -- checked before assuming, per
  this session's own rule-2 bucket-(a) discipline).
- Not the primitive color (g_BulletSparkColors[1], a hardcoded literal
  for world-prop hits -- always pale yellow).
- The texture itself (explosion_smokeimages, IA8, verified via the
  asset table) has no color channels and decodes correctly.

That leaves M-110's G_LIGHTING-not-cleared mechanism (already proposed,
unconfirmed, for explosion.c) as the one that fits: bullet_spark_render
never clears G_LIGHTING either (only ORs in G_CULL_BACK), so if lighting
is on, fast3d reinterprets the vertex color bytes as a lighting normal
and computes a scene-light SHADE color instead -- independently
converging on the same mechanism at a second call site, for a visually
distinct symptom (bullet sparks vs explosions). Flagged honestly as
unresolved bucket (b) vs (c): bullet_spark_render's missing lighting
clear is decomp-verified N64 ground truth, so a src/game fix needs the
port-layer alternative ruled out first, per docs/dev-process.md's own
new taxonomy -- not assumed just because the mechanism fits.

Also documented the headless-visual-capture technique itself
(GE_INPUTSCRIPT + GE_PCDUMP together, then actually opening the PPMs)
in porting-notes.md as a reusable method -- several other long-open
visual findings (D290/D291/D294) have been stalled purely for lack of
eyes on the pixels, not for lack of a working probe.

No src/game or port/ changes -- diagnosis only, screenshots are
session-local (scratch/, gitignored). findings-index.csv regen was a
no-op.
2026-09-16 22:20:49 -04:00
James Dansereau 1e1041c6ba D252: note GE_D172 re-run on the same repro found nothing (no combiner data) 2026-09-16 18:18:05 -04:00
James Dansereau 46b72b76fa D252 M-156: GE_D252 probe finally fired, decode confirmed correct by hand
Reused the D219/M-90 scripted-fire technique (GE_INPUTSCRIPT Z pulses) on
a bare -level_33 boot to get bullet-impact sparks headlessly -- got 14
distinct fire-stage texture dumps (scratch/d252_raw_*.bin, gitignored).
Hand-decoded all 14 as RGBA5551 big-endian and averaged the opaque
texels per stage: a perfect, smooth white-hot -> yellow -> orange ->
deep-red -> fade-out progression, zero off-hue content anywhere. This
independently reconfirms M-141's width/height/byte-order conclusion and
goes further -- it's the first time this investigation actually looked
at the decoded numeric pixel values rather than reasoning about the
decode formula, and it rules out the source asset data and the decoder
entirely. The bug is confirmed downstream: GL upload, the texture
cache, or the G_CC_INTERFERENCE combiner.

Also checked and refuted M-141's texture-cache-collision lead: all 14
captured stages have distinct texture_addr values, so TextureCacheKey
(gfx_pc.h) can't be colliding between stages the way that theory
required. Recorded a concrete next step (correlate a live GE_PCDUMP
rainbow frame against which of the now-known-correct 14 stages was
bound) for the next session.

No src/game or port/ changes -- diagnosis only. findings-index.csv
regen was a no-op (no new Dxx label).
2026-09-16 18:16:33 -04:00
James Dansereau d5fd08f40c D288: frame-gate the GE_D288 probe, re-run, still inconclusive
Gated GE_D288 to num_dls 550-755 (the reported bug window, same pattern
GE_D236RM already uses) and raised the bbox threshold from 0.15 to 0.5.
Volume dropped 116k -> 28,883, but that's still not the isolated signal
-- traced the remaining noise to a bad assumption in the probe itself:
bbox_frac from a raw x/w divide is meaningless once any_behind_camera=1
(w<0 or near-zero), which is exactly why the surrounding D233 code already
defers to GL's own clipper for that case rather than trusting the divide.
Restricting to any_behind_camera=0 (2,128 hits) still found no match for
the target color (83,62,44); the persistent hits across the window look
like static level geometry, not the transient bug triangle.

Working theory: the bare -level_20 boot's scripted intro camera may not
reach the same state the original visual bug report came from -- the same
"deterministic boot flag != deterministic camera path" pattern this
session's D236 passes independently hit on a different level. Recorded a
revised next-step list in findings.md; a live human repro is now more
promising than further headless probing of this specific approach.

No src/game changes. findings-index.csv regen was a no-op.
2026-09-16 18:10:43 -04:00
James Dansereau 08865bedeb D288 M-155: ran the GE_D288 probe, result inconclusive -- needs frame-gating
Ran the M-152 next-step probe (GE_D288=1 GE_PCDUMP=560-745:5, backgrounded,
25s bare -level_20 boot). Two problems surfaced with the probe itself, not
the underlying bug: it fires 116k times in ~1500-2000 frames (the
bbox_frac>0.15 threshold catches ordinary near-camera geometry throughout
the whole run, not just the reported ~562-741 window), and the target
color signature (83,62,44) never appears in the log. No GE_PCDUMP PPM
output was produced either, so no visual cross-check was available.
Confidence unchanged from M-152; this is a probe-refinement finding, not
a root-cause finding. Recorded the concrete next step (frame-gate GE_D288
on num_dls the way GE_D236RM already does) in the D288 entry.

Also caught and documented a real tooling gotcha: a backgrounded
ge007.x86_64.exe launch from this Bash tool doesn't reliably die to
`pkill -f` (process shows as bare `ge007.x86_64` in ps, no .exe suffix) --
a stray instance ran ~90s past its intended capture window before being
caught via `taskkill //F //IM ge007.x86_64.exe`. Added to porting-notes.md
so the next session uses taskkill from the start.

findings-index.csv regen was a no-op (no new Dxx label).
2026-09-16 18:07:47 -04:00
James Dansereau 7e81fb5e55 D236 pass 11: GE_INPUTSCRIPT nav attempt, no working path found, one new data point
Tried to keep the -level_36 deterministic boot's camera in the outdoor
room past its scripted indoor transition, to re-run GE_D236RM facing the
tree line directly (the concrete next step pass 10 named). Three
GE_INPUTSCRIPT variants (sustained SDOWN, sustained SUP, a START-skip +
delayed SUP) each changed the pre-transition room (curRoom 7/12/13) but
none held the camera outdoor past the transition, which fires at roughly
the same real time regardless of direction and earlier when START skips
faster -- timer/event-driven, not position-driven.

One new data point: the START-skip run's very first two GE_D236RM
samples hit count=10 then 6 (vs pass 10's flat max of 4) before
collapsing back to 2 then 0 within ~100 draw-list calls. Still far short
of D280's ~720 free-run figure -- best current explanation is that
D280's original number came from a genuinely different, later, closer
vantage than anything reachable in this boot's opening seconds, not a
CPU-side instantiation bug at this specific vantage.

No src/game or port/ file touched (GE_D236RM unmodified from pass 10).
Added the "opening cutscene isn't fully input-inert, but its transition
may still be time-gated not position-gated" quirk to porting-notes.md.
findings-index.csv regen was a no-op (no new Dxx label).
2026-09-16 18:04:08 -04:00
James Dansereau b73742ca2c fix: cache the GE_AUDIOTRACE getenv() check in alEvtqPostEvent (D250)
The D202/M-71 [EVTQ-DROP] diagnostic probe (gated at 56d2dfd6 after
being found unconditional) called getenv() uncached on every dropped
event post -- a real per-item hot path during audio event-pool
exhaustion, unlike the sibling GE_AUDIOTRACE probes in
synthesizer.c/csplayer.c/load.c which only run at one-shot init/load
call sites. Cached with the standard static-int pattern (D250, M-120),
matching the dozens of existing GE_* probe checks already using it.

Not yet re-linked -- build-pc is occupied by a concurrent investigation
this session; verify with a clean build + /linkcheck before the next
push.
2026-09-16 17:59:45 -04:00
James Dansereau 23da815dda D236 pass 10: refute "nondeterministic instantiation", it's room occlusion
Per the rule-2-clarification pass-10 brief in findings.md, applied the new
three-bucket taxonomy to D280's leading theory that the 0x0c184b50
discrete-tree-card render-mode class instantiates inconsistently across
runs of the deterministic -level_36 boot (~4 tris vs ~720).

Added a small env-gated diagnostic (GE_D236RM, port/fast3d/gfx_pc.cpp
gfx_sp_tri1) counting that class's per-frame triangle count, heartbeat-
logged every 300 frames even at zero. Three independent -level_36
launches (one with sustained GE_INPUTSCRIPT camera rotation) produced
byte-identical per-frame counts, falsifying the "run-to-run dependent"
premise outright. Cross-referencing with the existing GE_D104 room-census
probe in the same run shows the count drops to zero at the exact frame
the player's curRoom transitions from 12 (17-20 rooms drawn, an outdoor
vista) to 18 (1-5 rooms, an enclosed indoor space) via the boot's own
scripted intro camera — ordinary portal/room occlusion, not a bug. None
of buckets (a)/(b)/(c) apply; no src/game or port/ fix made or warranted.

The original "wall not discrete sprites" symptom is untouched by this
pass — narrowed the real next step to getting a camera into room 12
facing the tree line directly, in docs/dev/findings.md.

Added the generalisable "deterministic boot flag != deterministic camera
path" quirk to porting-notes.md §E. findings-index.csv regenerated.
2026-09-16 17:58:01 -04:00
James Dansereau 1c60da6f46 docs: clarify rule #2's diagnostic scope + define rule-2 sign-off process
Widens the written scope of AGENTS.md non-negotiable #2's ABI exception to
match established practice (runtime struct/union misreads, not just
ROM-serialized structs — the D209/D210/D255 pattern), states explicitly
that diagnosis into src/game is never restricted (only the fix is), and
defines a real three-bucket taxonomy + sign-off procedure (dev-process.md
§7) for the rare genuine-behavior-difference case, replacing the
undefined ad hoc "ask the user" gesture that's been referenced without
being defined across ~10 places in the docs.

Mirrors the same framing into CONTRIBUTING.md and the PR template for
external contributors. Extends porting-notes.md §A1 with D255 as a third
worked example and a tell-tale-signs checklist, explicitly distinguishing
it from D284 (same epistemic shape, different mechanism, no rule-2
question at all). Appends a ready-to-dispatch pass-10 brief to the D236
entry applying the new taxonomy to its actual next step.

Docs-only; no code/build changes. Per docs/dev/notes/RULE-2-CLARIFICATION-PLAN.md.
2026-09-16 17:45:58 -04:00
James Dansereau deac53f892 docs: D243 M-154 fix 2 (second probe crash + final deref-free tick path) 2026-09-16 17:09:36 -04:00
James Dansereau 14fc657412 D243 M-154 fix 2: tick probe never dereferences render_pos (second abseil crash)
The NULL/-1 sentinel guard from 85aa1a74 was not sufficient: the second
live run crashed at the identical PC/fault address, now PAST the guard
(disassembly confirms it compiled correctly). Between CREATE and the
first draw pass, render_pos holds arbitrary stale AnimModelSlot contents
-- run 1 drew the 0xFF tombstone (-1), run 2 drew some other bad value.
Sentinel-whack-a-mole is the wrong shape for a probe.

Final fix: drop the raw= read from the tick line entirely (rp=%p stays --
logging a pointer costs no deref, and the -1 -> real transition frame is
still captured). No data lost: the pre-view root translation is already
logged by the D243M chrdraw census in drawjointlist, where validity is
guaranteed (the game's own gSPSegment runs moments later) and draw-time
is the more accurate sample anyway (tick-time would read the previous
frame's matrix buffer). chrdraw probe gains the same -1 rejection as
belt-and-braces. Smoke: 30 s bare boot, 4,051 chrdraw lines, no crash.
2026-09-16 17:09:19 -04:00
James Dansereau 2a1f1b2472 findings: D243 M-154 crash-fix note + D298 corrected (full HUD, suspected debug-launch artifact)
- D243: M-154 fix paragraph — probe-side AV at the abseil CREATE
  (render_pos = 0xFF tombstone until instcalcmatrices runs during the
  draw pass); guarded; transition frame now logged.
- D298: user corrected the symptom — the FULL HUD is missing, not just
  the aim-mode crosshair; suspected debug-launch artifact; per explicit
  instruction, do not raise until seen in a regular release-targeted
  build. Entry re-scoped and parked accordingly.
2026-09-16 16:57:40 -04:00
James Dansereau 85aa1a74e9 D243 M-154 fix: guard tick-probe render_pos read against the -1 tombstone
First live abseil run crashed inside the new probe (AV, fault addr
0xFFFFFFFFFFFFFFFF, PC in playerTick bondview2.c:10419) on the first
tick after CREATE: bodyModel->render_pos is assigned by
instcalcmatrices() during the draw pass (model.c:2453), i.e. AFTER the
probe point, so on tick 1 post-CREATE it still holds the reused
AnimModelSlot's stale 0xFF tombstone (-1). The game only dereferences
it post-chrTick, so it is legitimately unset here; the M-153-era
fields never touched it, which is why the earlier capture survived.

Guard the raw= read against NULL and (RenderPosView*)-1; rp=%p stays
unconditional so the -1 -> real transition frame is captured (a D243
data point in itself). Probe-only; zero game-code impact. Smoke: 30 s
bare boot clean, 4,059 chrdraw lines.
2026-09-16 16:57:39 -04:00
James Dansereau 90c87fa227 findings: D298 filed — aim-mode crosshair missing (regression, parked per user)
User-reported 2026-09-16 during the M-154 re-run build: the aim-mode
crosshair no longer renders, suspected to come from earlier v0.3.0 work.
Explicitly parked ('don't fix, just for context'). Entry records the
symptom, rules out the probe-only M-154 commits and D295's menu-buffer
widen, points at the crosshair draw path (crosshairimage / bondview.h
show-hide flag / gunfire.c D215 renderdata family) and a bisect plan.
2026-09-16 16:50:59 -04:00
James Dansereau ce8427cff2 docs: D243 M-154 — user confirmed double Bond (one frozen copy per shot); corrected log reading; census probe documented
- findings.md ## D243: M-154 entry. User symptom sharpened (one new
  frozen Bond per camera shot; old copies persist, never animate).
  Corrects M-153: the POSENT window has three phases and in phase 3
  prop->pos ITSELF cycles period-4 through three z-lanes while y falls
  smoothly — the rendered model tracks it (per-lane pose offsets), so
  it teleports every frame rather than freezing. H1 vs H2 named;
  probe design + smoke results recorded. §F table cell updated.
- GE-ENV-PROBES.md: GE_D243M row updated (chrdraw census, new tick
  fields, dedupe note).

HANDOFF.md (gitignored) got the matching LATEST block with the
re-capture command.
2026-09-16 16:47:31 -04:00
James Dansereau 1b3476a401 D243 M-154: chrdraw census probe + extended tick fields (GE_D243M)
Discriminates the two remaining hypotheses for the Dam-abseil frozen
Bonds (H1 multiple Model instances vs H2 framebuffer accumulation):

- objecthandler.c drawjointlist (both region variants): new
  'D243M: chrdraw' line per CHR render submission while a scripted
  camera mode is active (POSEND/INTRO/SWIRL/FADESWIRL), logging Model*
  + raw root-matrix translation. A second Bond instance = extra lines
  with distinct model pointers at the same frame. All CHR draws funnel
  through here (chrRenderProp; player path bondview2.c:10883, generic
  chrprop.c:443).
- bondview2.c tick probe extended: propptr/chrptr (phase-3 A,A,B,C
  position cycle could be pp->prop rotating among three PropRecords),
  rp= render_pos pointer (change = buffer reassignment/aliasing),
  raw= pre-view-transform root translation (model-matrix vs
  view-matrix origin of the cycle).
- bondview2.c: d243mProbeActive()/d243mGetFrameCounter() accessors;
  camera gate folded into active() so objecthandler.c needs no
  bondview.h include. D250 cached getenv kept.

#ifdef PORT only; silent when GE_D243M unset or during FPS play.
Smoke: 45s bare -level_33 boot -> 4,053 chrdraw + 1,066 tick lines,
clean link, no behaviour change.
2026-09-16 16:47:31 -04:00
James Dansereau 8d42ac5ee2 D287 M-152: correct false evidence citation, static architecture trace
scratch/d287_freeze_gdb.txt, cited as this finding's freeze-time
evidence, does not contain it -- grepped for every specific number the
write-up quotes (frame 17616, the three heartbeat stall times) and got
zero hits. The file on disk is an unrelated clean normal-exit capture.
No real freeze-time thread state has actually been analyzed for this bug
yet; corrected the entry so a future session doesn't treat it as
narrowed-and-confirmed.

Also traced the consumer/producer architecture statically (mainThread's
only OS_MESG_BLOCK send is rsp.c:271's osSendMesg into the scheduler's
cmdQ; GE_D60 doesn't cover sends), and found portHeartbeatCheck hard-caps
at 3 reports/process -- "heartbeat stopped logging" in the original
write-up is that cap, not new evidence. Both quirks added to
porting-notes.md. No code change -- diagnostic-only pass, needs a real
live capture next.
2026-09-16 16:26:39 -04:00
James Dansereau 695ff7ec09 docs: D243 M-153 — live GE_D243M capture analyzed; double-instance refuted, period-4 render cycle named
The user's interactive Dam->abseil run with GE_D243M=1 GE_D160=1 landed
(351 KB log, 2991 probe lines; preserved locally at scratch/d243/).
Findings (## D243 M-153 + §F table cell):

- Single Bond model throughout (one idx/model pair, one draw/frame); no
  REMOVE/CREATE window around the abseil — M-145's double-instance and
  bodyModel==0 re-seed-window hypotheses are refuted on PC.
- Two divergent position streams: prop->pos runs the smooth scripted
  void sweep; the model's rendered position (field_488.pos = render
  readback, the only writer under POSEND) freezes on dam-surface points
  then locks into a strict period-4 cycle through four fixed world
  points. The lagged filter (field_3C4, starting at the exact -22832
  M-143 observed) chasing that cycle is the observed camera shake.
- Next: code dive chrTick->render_pos for what produces the four fixed
  points; D173/D292 are the same family.
2026-09-16 16:25:08 -04:00
James Dansereau a9f370bdac cleanup: strip dead GE_D255 correlation probe from objFree
D255 was root-caused and fixed for real at M-140 (a 32-bit pointer
truncation in vtxstore_fix_refs, not a destroy-path race). This
objFree/getenv("GE_D255") trace was a correlation aid for the
now-superseded race hypothesis and never fires against the real fix;
HANDOFF already flagged it as strip-candidate scaffolding. The
vtxstore.c comment documenting the actual fix is untouched.
2026-09-16 16:20:25 -04:00
James Dansereau 190141ff53 docs: D230 status was stale OPEN despite M-121 fix in the same entry
The §F summary table row and the entry's own inline Status line still
said "OPEN -- static-analysis pass done (M-108)" even though the same
paragraph documents the RAW16-wavetable byte-swap fix landed at M-121
(v0.2.0). Left a future session likely to re-investigate an already-fixed
bug. Corrected both to FIXED, noting the still-owed by-ear pass.
2026-09-16 16:17:53 -04:00
James Dansereau 895118c205 D290 M-152: static investigation, decomp side ruled out
Traced the watch controller-graphic render path end to end: page gating,
model/item wiring, ROM resource-table entry, and the generic equip/load
pipeline all check out byte-matched or generic-and-proven-elsewhere.
Rules out src/game/ with high confidence. Best-narrowed hypothesis (not
confirmed): a fast3d translucent-blend edge case for the one-off
PROP_TYPE_PLAYER draw used during the watch's fade-in, matching this
project's recurring one-off-combiner-mode bug class. Falsifier for the
next live session recorded in the finding. No code change.
2026-09-16 16:15:30 -04:00
James Dansereau fbc2a533ae D288 M-152: static investigation, add GE_D288 near-plane probe
Read gfx_sp_tri1 against D233/D106/D271's existing guards. D233's
trivial-reject fix is ruled out as a direct cause -- GL clips well-formed
clip-space geometry correctly downstream of it. Found a new, previously
uncatalogued unguarded site: the G_CULL_BOTH backface-cull cross-product
divides raw vertex w with no epsilon guard, which can NaN/Inf near w=0
and silently skip both cull branches. Not yet confirmed against a live
repro. Added GE_D288 (cached-getenv, D250-safe) to catch the culprit
triangle's w/bbox at draw time on the next -level_20 run.

No game-logic change; port/fast3d/ diagnostic only, inert unless
GE_D288 is set.
2026-09-16 16:13:33 -04:00
James Dansereau 56d2dfd672 fix: gate EVTQ-DROP audio diagnostic behind GE_AUDIOTRACE
The D202/M-71 evtq-exhaustion probe in alEvtqPostEvent was left
unconditional, so shipped builds wrote an unbounded log line on every
dropped event -- found reading GitHub #87's session logs, which showed
real sustained event-pool exhaustion. Gate it on GE_AUDIOTRACE, the same
switch the sibling probes in synthesizer.c/csplayer.c/load.c already use.
2026-09-16 16:06:44 -04:00
James Dansereau fd065071cc findings: D243 M-151 probe landed; GE-ENV-PROBES + index updated 2026-09-16 15:58:37 -04:00
James Dansereau 4a6fb1b940 D243 M-151: GE_D243M model-lifecycle probe for the Dam-abseil two-Bonds/shake
#ifdef PORT, cached getenv, zero behaviour change when unset. Three probes
in bondview2.c:
- D243M: CREATE/REMOVE at both bodyModel sites (solo_char_load /
  bondviewRemovePlayerBody) with camera mode at the transition;
- D243M: tick per playerTick() call while the third-person model exists or
  POSEND/INTRO is active (idx, model ptr, cam modes, prop->pos,
  field_488.pos, filter fields field_3B8 + field_3C4/8/C);
- D243M: draw per on-screen render_pos submission (two lines at the same
  frame number = second Bond instance).

Smoke-tested on a bare -level_33 boot: level-intro cutscene exercises the
full lifecycle (CREATE frame 534 cam=SWIRL -> per-frame tick+draw -> REMOVE
frame 1127), FPS play silent, no crash. Next: user's interactive Dam run to
the abseil with GE_D243M=1 GE_D160=1.
2026-09-16 15:57:51 -04:00
James Dansereau 5071185f8c findings: D295 M-148/M-149 + D297 filed/closed (M-150); porting-notes D8/D9
- D295: full M-148 root-cause/repro record (difficultytext overflow ->
  high-bit byte -> langGetJpnCharPixels NULL deref; OOB theory retracted;
  pre-D284 save fingerprint identified), verdict FIXED (M-149).
- D297: new entry — v0.3.0 wipes all pre-fix saves (D284 side effect);
  scope verified (5 slots only, block-0 is a factory seal), FIXED (M-150).
- D284: correct the false 'savegames are unaffected' claim.
- porting-notes: D8 instance (difficultytext[4]); new D9 — changing a
  PRNG/CRC implementation invalidates persisted derived state; sweep
  rewrites data/ge007.eep gotcha in E.
2026-09-16 15:28:44 -04:00
James Dansereau f1e2c7cbb8 D297 M-150: eeprom-shim migration of pre-D284 save CRCs
v0.3.0 silently wiped all v0.2.x saves on first launch: stored slot CRCs
are fingerprints of the PRNG that wrote them, and D284 changed the PRNG.
One-time migration in the eeprom shim's read path (block 4 only): a slot
that fails the current-CRC check but validates under the legacy PRNG is
re-stamped with current-PRNG checksums before fileValidateSaves sees it,
then persisted once. Runs before geEepromPatchAllCheats so cheat patches
never reach the file. Corrupt/fresh slots untouched (game's own wipe path).

- port/src/legacycrc.c: isolated file carrying the deliberately-wrong
  pre-D284 PRNG (verbatim from 0de99b4d^, reviewer-verified bit-exact) +
  lockstep copy of the crc.c loop; header says when it may be deleted.
- tools_pc/d295_crc.py: reference CRC model, bit-exact vs compiled C for
  both PRNGs (used to verify migrated files).

Verified: user's #87 artifact migrates 5/5 slots with progress intact and
is idempotent on relaunch; native post-fix saves, corrupt slots, and fresh
eeps all behave per design; 21/21 sweep PASS.
2026-09-16 15:28:37 -04:00
James Dansereau 4fc54293f9 D295 M-149: fix #87 crash — port-side JPN-cache seed + PORT buffer widen
langGetJpnCharPixels NULL deref (language.c:320): any rendered string byte
>= 0x80 in non-JP mode indexed the unallocated JPN glyph cache. Two
signed-off fixes:
- port/src/main.c: seed g_JpnCacheCacheItems/g_JpnCharCachePixels from
  static port buffers before osStartThread (exact langInit construction,
  _Static_assert on pixel-stub size) so a stray high-bit byte takes the
  normal cache-miss path instead of crashing.
- src/game/front.c: widen difficultytext[4]->[32], missiontext[18]->[32]
  under #ifdef PORT (D8 class; '00 Agent' + newline overflowed the decomp
  buffer into textpos.p[0], the direct trigger). N64 build untouched.

Verified: previously-crashing input runs clean, 21/21 sweep PASS.
2026-09-16 15:28:33 -04:00
James Dansereau 6f28156956 findings: D243 M-145 - two-Bonds observation confirms third-person model lifecycle bug
Static trace confirms user's hypothesis architecturally: the Dam exit
script runs on a model-less BG driver chr, while the visible Bond is the
player's own third-person model teleported via chr_try_teleporting_to_pad
(CHR_BOND_CINEMA). Ties the abseil camera shake (M-143/M-144) and the
double-Bond image to one model-lifecycle mechanism; cross-refs D173/D292.
2026-09-16 09:38:02 -04:00
James Dansereau 08dbd581e9 findings: D285 live-Deck verification (Archives, dual grenade launchers, rocket-cheat guards, v0.2.2) 2026-09-16 09:37:02 -04:00
James Dansereau 768ac8d837 release-notes: drop stale per-release changelog re-dumps
.github/release-notes.md re-included the full "What's new since v0.2.0"
and "since v0.1.0" sections in every release body, even though that
content already lives permanently on those releases' own GitHub pages.
Keep only the delta since the immediately previous release, with a link
to the Releases page for full history.
2026-09-16 09:21:40 -04:00
James Dansereau 2e8511539b v0.2.2 hotfix: README + release notes + D285 live-verification
Documents the four hotfix-batch changes since v0.2.1 (D255, D285, D282,
D293) in README Status and .github/release-notes.md, and folds in
2026-09-16 real-Deck confirmation that D285's audio-thread lock holds
under sustained overlapping-explosion combat (Archives, dual grenade
launchers + rocket-launcher-cheat guards).
2026-09-16 09:13:43 -04:00
James Dansereau 2c19689c62 findings: D243 M-144 — user confirms real N64 does NOT show the abseil shake
Closes the open question M-143 ended on: this is a genuine PC-only
divergence, not an original-game quirk. Since bondviewUpdatePlayerCollisionPositionFields's
smoothing-filter math is decomp-exact and already g_ClockTimer-scaled, the
bug must be upstream of the filter -- either the position warp itself is
wrong/larger on PC, or a call-ordering desync around the CAMERAMODE_INTRO
transition. Narrows the next step to a single probe: log raw field_488.pos
and field_3B8 (pre-filter) around the warp tick, instead of the open-ended
"needs N64 hardware" framing M-143 left off on.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-16 00:17:08 -04:00
James Dansereau 91ba14d2e6 findings: D243 M-143 — name the shake mechanism (unreset position-smoothing filter)
Live Dam-abseil capture with the new lookatpad/gBondViewCutscene GE_D243CAM
probes (added this session) fires the gBondViewCutscene branch. Its pos2
read is g_CurrentPlayer->field_3C4/8/C, a leaky-integrator low-pass filter
of Bond's real position (bondviewUpdatePlayerCollisionPositionFields) that
is never reset when a scripted cutscene warps his position out from under
it -- the filter takes ~1s to catch up, and a fixed eye with a look-at
target sweeping through that catch-up is the reported shake. The filter's
own update is g_ClockTimer-scaled (frame-rate-correct by the project's
usual standard), so this looks decomp-faithful rather than a port-specific
timing bug -- open question is whether real N64 hardware shows the same
transient, which would make this not a port defect at all.

D146 stays fully decoupled (zero hits on a third consecutive live run).

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-16 00:13:30 -04:00
James Dansereau 57b6c6d67c D243: instrument the two untested CAMERAMODE_POSEND branches (lookatpad, gBondViewCutscene)
M-141/M-142 ruled out the pad-orbit branch (GE_D243CAM: zero hits across two
live Dam-abseil captures). The Dam cutscene must reach one of the other two
CAMERAMODE_POSEND sub-paths instead. Adds the same GE_D243CAM-gated
osSyncPrintf to both, right before their early returns: pos/pos2 for the
look-at-pad branch, plus theta/verta (the spherical angles driving pos2's
offset) for the gBondViewCutscene branch, since a jump/jitter in either is
the leading candidate for the reported shake.

Build-verified (release/v0.3.0, 11/11 changed objects relink clean); all
three D243CAM log lines confirmed present in the built binary. Awaiting the
next live Dam-abseil repro (Phase 1 in docs/HANDOFF.md) to see which branch
actually fires and what the logged values show.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-16 00:00:42 -04:00
James Dansereau 09fdd86eef optionsoverlay: add D293 quit-to-desktop row to the F10 overlay
No discoverable in-UI way to quit (Deck/controller-only setups had only
window-close/Alt+F4). Adds a ROW_ACTION row kind (not config-backed, same
pattern as the existing __Resolution row) that triggers the identical
configSave()+exit(0) path SDL_QUIT already uses. Build-verified on Windows
and Linux; verify.sh dam PASS, no regression.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 23:52:50 -04:00
James Dansereau 1970ab3732 fix(D285): lock sndHandleEvent's preempt-scan against sndSetupSound's writer
sndHandleEvent's pool-exhaustion preempt-scan read/walked the shared
D_800243E4 active-ALSoundState list with no osSetIntMask lock, while
sndSetupSound (the list's insert path, reached from sndPlaySfx calls
across 15 game files) already locks it -- an asymmetric-locking gap,
the same D4/D147/D152 "N64 interrupts-off isn't a real lock on PC"
class. Wraps the scan in the identical lock/unlock pair sndSetupSound
already uses, unconditionally (no #ifdef PORT, matching that function's
own style -- osSetIntMask is genuine N64 API and the port's shim is a
process-wide recursive lock, safe to nest). Audited the rest of the
function: the scan is the only D_800243E4 touch point in it.

Build green; -level_09/-level_20 unregressed (1400+ frames, no crash,
no D152 lock-stuck warnings). Not yet gdb/live-confirmed against a
real reproduced crash -- a GE_INPUTSCRIPT-driven stress attempt this
session didn't visibly activate the scripted-input harness for a
reason not root-caused; the honest bar met here is "correct by D4/D152
precedent, build/sweep-clean," not "reproduced-then-fixed."

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 23:49:30 -04:00
James Dansereau 97678b2980 fix(D282): front-end menu nav always uses left stick, matching F10-overlay convention
Menus read the same stick_x/stick_y channel as in-level gameplay, and
naturalPitchMode (SOLITARE-style, default-on) routes that channel to
the right stick for continuous look. Since front.c's menu nav shares
the channel, it silently inherited the right-stick routing, which is
unintuitive on a modern pad/Deck outside a running stage. The gamepad
poll already computed an unused padMenuMode flag for shoulder-button
gating; reused it to always source stick_x/stick_y from the left
stick outside RUN_STAGE, regardless of naturalPitchMode. Port-layer
only, inert during gameplay -- verified via -level_09/-level_20
unregressed; in-menu feel needs a human controller playtest.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 23:48:20 -04:00
James Dansereau 1ba83ba4b0 fix(D255): correct 32-bit pointer truncation in vtxstore_fix_refs
Root-caused via a live Steam Deck session over SSH: sub_GAME_7F09BAC4
(vtxstore_fix_refs) read an active OBJ record's Model* through
((Model*)((ChrRecord*)prop->chr)->chrflags) - a union pun onto
ChrRecord.chrflags, which is typed CHRFLAG, a 4-byte enum. On 32-bit
N64 this is bit-exact (the enum and a pointer are both 4 bytes, so
casting the enum's value to Model* reproduces the real stored bytes).
On 64-bit PC, Model* is 8 bytes but the enum read is still only 4 -
the cast reads just the pointer's low 32 bits as an int and
zero-extends it back to a 64-bit address, discarding the real high
bits. This is the D3x pointer-width ABI class from AGENTS.md - a
struct field misread across the 32-to-64-bit pointer-width transition.

This was actually flagged as suspect (a) in the very first D255
investigation, then wrongly deprioritized in favor of a "destroy-path
race" theory once a register capture happened to show Rdi=(nil) and
looked like it confirmed a different, unrelated NULL-model theory. It
didn't: (nil) is exactly what a 32-bit-truncated read produces
whenever the real pointer's low 32 bits happen to be zero - a
coincidence that pointed two sessions' worth of investigation at the
wrong field. Proof: reading the same record through the correctly
64-bit-typed ObjectRecord.model field (this session's diagnostic
probe) was always a valid, non-NULL pointer - the object's model was
never actually missing.

Fix: read the pointer through ObjectRecord.model (the same union
memory, correctly typed) instead of through the mistyped
ChrRecord.chrflags alias, at both use sites in the function. Identical
bytes and identical result on N64; correct on 64-bit PC.

Verified live on the user's own Steam Deck hardware over SSH, running
their own prepared ROM assets: held through the entire Facility level
including repeated terminal destruction (the most consistent prior
repro) and through Caverns' radio-objective terminals (the second
confirmed level), zero crashes. Windows rebuild + full 21-level
verify.sh sweep also clean.

The separate audio-thread crash (D285, caught during this same live
session) is unrelated and unaffected by this fix.
2026-09-15 23:48:17 -04:00
James Dansereau 24fb22ca80 D243: live playthrough decouples shake from D146; add GE_D243CAM probe
Live Dam abseil cutscene captures (2 runs, both diagnostic probes on):
the visible camera shake reproduces every time, but the D146 stale-memory
burst fired on neither run -- the two symptoms M-107/M-109 treated as
related are actually independent. Added an env-gated GE_D243CAM probe to
bondviewFrozenCameraTick's pad-orbit CAMERAMODE_POSEND branch to check for
g_GlobalTimerDelta jitter; zero hits rules out that branch entirely. The
Dam cutscene uses one of the other two CAMERAMODE_POSEND sub-paths
(g_CameraLookAtBondPad or gBondViewCutscene), neither instrumented yet --
next session's concrete next step per docs/HANDOFF.md.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 23:44:07 -04:00
James Dansereau 8c28453517 findings: fix D255's stale OPEN status (was FIXED+verified, never updated)
The M-140 fix writeup existed in two places -- an orphaned paragraph right
after the table row, and the pre-fix narrative in the tail ## D255 section
-- but neither ever updated the table row's own status cell, so
findings-index.csv (generated from the table) reported a real, fixed,
live-verified crash as still OPEN. Consolidated into one writeup in the
tail section (which findings-index.csv derives its verdict from per this
file's own ctx-opt convention) and fixed the row's status cell to match.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 23:25:23 -04:00
James Dansereau e978e526be findings: D249 static review rules out two suspects, narrows to portal BFS
Confirmed correct level IDs (Streets=29, Egyptian=32, not the finding's
original level_20 guess). Source review clears both the unscaled-far-clip
and LOD-distance-multiplier hypotheses (bgfog.c/video.c both already scale
consistently). Narrowed to bg.c's portal/room-visibility BFS as the one
remaining suspect -- could be a real bug or just N64 behavior newly visible
on PC's sharp display; needs a live screenshot + location before touching
this decomp-verbatim system, not more static tracing.

Headless GE_PCDUMP long-sightline attempts on Streets/Statue/Egyptian did
not transfer from the D245 technique -- none of them have a scriptable path
to an open sightline without real level navigation.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 23:20:44 -04:00
James Dansereau 7412a9818d sky.c: D245 headless probe + opt-in fixed-shift A/B candidate
Adds a GE_D245V env-gated probe dumping the water fan's raw S/T span each
call, and a GE_D245_FIXEDSHIFT env var (skyPortForceFanShift) that forces a
constant tc-shift k instead of the per-frame adaptive pick. Both are
diagnostic/opt-in only -- default behavior is unchanged (verify.sh
frigate/"surface 2" PASS with the env vars unset).

Headless GE_INPUTSCRIPT rotation on Frigate found k genuinely flips (kT
4->3) during ordinary view turning, which the original single-frame D227V
trace never caught -- new evidence for why the M-116 allowShift=TRUE fix
didn't hold up live. The fixed-k candidate is NOT the default; it needs a
live A/B (unset vs 3/4/5) to know if it actually looks better, not another
blind ship.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 23:06:47 -04:00
James Dansereau e108d6bf2e findings: log D295, campaign-halting Control Center crash (GitHub #87)
00 Agent campaign crashes at end of Control Center, then crashes on every
subsequent relaunch — points at corrupted persisted state (save/EEPROM),
not a one-off frame bug. Possibly-related audio degradation reported from
Streets onward, unconfirmed. Not investigated; needs the user's crash log
and save file before further work.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 22:52:11 -04:00
James Dansereau a3637c40e1 optionsoverlay: add D293 quit-to-desktop row to the F10 overlay
No discoverable in-UI way to quit (Deck/controller-only setups had only
window-close/Alt+F4). Adds a ROW_ACTION row kind (not config-backed, same
pattern as the existing __Resolution row) that triggers the identical
configSave()+exit(0) path SDL_QUIT already uses. Build-verified on Windows
and Linux; verify.sh dam PASS, no regression.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 22:49:42 -04:00
James Dansereau acf80a874f agents: findings table row now points at the CSV lookup path (consistency with preflight bullet) 2026-09-15 22:39:21 -04:00
James Dansereau 5d7b18a035 Merge docs/llm-context-optimization: LLM context optimization (P0+P1+P2)
Independently validated (fresh-session audit from git + snapshot); all
findings fixed. Docs-only: findings-index.csv lookup path, HANDOFF split,
~127 fat §F cells migrated verbatim to ## Dxx tail sections, CI size
budgets. Preflight ~123k -> ~6.5k tok.
2026-09-15 22:38:00 -04:00
James Dansereau 6cb430c4a9 claude.md: point findings lookup at findings-index.csv (was stale in-file §F index; validator-relevant for Claude Code sessions) 2026-09-15 22:30:24 -04:00
James Dansereau 44ea596411 findings: validation fixes — D252 derived status now reflects M-141 (both hypotheses refuted, root cause open); normalize D221/D227 append flags to 'appended 2026-09-15 ctx-opt' 2026-09-15 22:24:48 -04:00
James Dansereau 81cb276ed5 hygiene: untrack docs/dev/notes/ctx-opt/consolidate_ab.py (dir is gitignored; validator finding) 2026-09-15 22:24:48 -04:00
James Dansereau 5684d30bd4 agents: caveat for large multi-pass ## Dxx sections (grep-within / offset-limit, don't slurp) 2026-09-15 21:55:31 -04:00
James Dansereau c2b42db275 ci: recalibrate §F size budget to 180 KB (table ~55 KB + legacy narrative ~110 KB)
The 150 KB threshold assumed the §F region was table-only, but it also
holds ~92 KB of pre-ctx-opt D24–D33 bullet narrative plus the M-111/M-113
write-ups. The regression signal is fat ROWS re-accumulating (table is now
~55 KB), so the budget sits ~15 KB above today's 166 KB total.
2026-09-15 21:52:07 -04:00
James Dansereau 093e428639 findings.md: has-section dedup, rows 20-27 (D280 D275 D274 D273 D272 D284 D285 D286)
Same protocol: topics were compressions of their own §H entries (verified
by probe), statuses (the verdicts) kept verbatim. Skipped as already-thin:
D240-D243, D247 (one-liner + short status each).
§F table region: 226 KB -> 166 KB.
2026-09-15 21:51:22 -04:00
James Dansereau e9bb415488 findings.md: has-section dedup, rows 16-19 (D217 D271 D276 + skips)
- D217: 7.5 KB status was a digest of the §H M-86→M-97 entries (each
  'See full write-up: §H ...'); only the closing verdict was missing —
  appended it (PR #47 + PR #49 merged, white residual → D228, M-105
  stale-status correction).
- D271/D276: topics were compressions of their own §H entries; statuses
  (the verdicts) kept verbatim.
- Skipped as already-thin/substantive: D220, D132 (compact audit summary),
  D183, D179, D178, D182, D181.
2026-09-15 21:50:34 -04:00
James Dansereau 2a84927e3f findings.md: finish D227 shrink (status cell was missed in the batch-2 pass)
The earlier edit replaced only D227's topic cell; its 11.9 KB status
survived. That status was a digest of the full write-ups, which exist:
§H '## D227' entries M-94/M-95/M-96/M-98/M-100/M-100b (in findings.md) and
M-99 (full entry in HANDOFF-ARCHIVE.md). Digest wording survives in git
history + snapshot.
2026-09-15 21:49:31 -04:00
James Dansereau e85175639c findings.md: has-section dedup, rows 11-15 (D206 D207 D205 D176 D177)
Same protocol. Three sections ended before their final disposition — the
newer verdicts were appended verbatim to the section first (flagged):
- D206: M-77c user by-ear A/B = PASS (section still listed it as 'still owed')
- D207: M-77c MONITORING disposition
- D205: M-78 CLOSED (root cause was D206)
D176's defect-(b) summary (grey diagonal static, D183 0/166) has no section
home — kept as the row's substantive topic instead of a bare pointer.
2026-09-15 21:48:28 -04:00
James Dansereau 45427eaf0e findings.md: has-section dedup, rows 1-10 (D159 D161 D186 D200 D221 D227 D229 D228 D204 D202)
Each cell checked line-by-line against its ## Dxx section(s); cells were
compressed summaries of the sections, so rows shrink to one-liner + pointer
and status keeps its verdict. Two cells carried facts NEWER than their
sections — appended verbatim to the sections first (flagged in-section):
- D221: human mouse-playtest confirmation (section still said 'eyeball owed')
- D227: M-100b human play-test confirmed (section ended 'still owed')
D228's status was the post-split FIX write-up, absent from its pre-fix
M-97 section — appended as a second ## D228 pass-section.
Exact pre-shrink wording survives in git history + notes/snapshot-pre-ctx-opt/.
2026-09-15 21:45:46 -04:00
James Dansereau 00a877cc53 findings.md: D70–D121 §H cross-check — shrink covered rows to pointers, migrate D120
Semantic coverage check of each §F row labeled D70–D121 against the §H
narrative (done directly, not delegated):
- D85, D86·D87, D88.4, D121: thin rows, fully covered by §H entries —
  topic reworded as one-liner + 'see §H' pointer, status kept verbatim
- D117, D118: fat cells whose content (M-52 addenda; M-24 mouse-look
  rework incl. D118b/c) is fully in §H — shrunk to one-liner + pointer
- D120: fat cell with NO section and no §H coverage (sole record of the
  M-30 PointUsage fix) — pure verbatim move to a new ## D120 tail section,
  row becomes a pointer; verified byte-identical vs pre-migration snapshot
- D75: LEFT AS-IS — its status cell holds a v0.2.0-pre playtest result not
  present in §H; shrinking would lose data
- D119·D120: left as-is — sole record of D119 (§H mentions it only in passing)
Range rows (D70–D74, D89–D92, …) stay as-is per plan.
2026-09-15 21:41:50 -04:00
James Dansereau 14eb1f8330 findings.md: shrink the 3 multi-label rows with per-sublabel sections to pointers
D76·D164, D165·D166, D180·D181 each point at existing ## Dxx sections that
fully cover the cell content (verified line-by-line; cells were compressed
summaries of the sections). Status cells kept verbatim. The remaining two
multi-label rows (D86·D87, D119·D120) are thin §H-era summary rows with no
sections — deferred to the D70–D121 §H cross-check.
2026-09-15 21:40:23 -04:00
James Dansereau e2b8ec3432 findings.md: consolidate the (a)/(b) duplicate-label sections (D152+, D233, D234)
Each label had a full writeup + a short 'Duplicate row' triage pass.
Keep the full writeup; append the short pass's genuinely-unique content
verbatim under a dated merge note (D233/D234: GRAPHICS-BACKLOG.md logging +
triage hypotheses; D152+: fully subsumed — noted as such). Both §F table
rows stay (cell 1 frozen) and both now point at the single merged section.
Nothing deleted: earlier text in git history + notes/snapshot-pre-ctx-opt/.
2026-09-15 21:39:38 -04:00
James Dansereau 8687846bee findings.md: header documents the ctx-opt migration + CSV lookup path + 4 known index gaps
Cell 1 of the §F table is frozen, so four rows (D69·D78–D84, RC3·D167,
the D88.x range row, D118d) are invisible to findings-index.csv — LABEL_RE
can't parse their label format. Documented in the file header alongside
the lookup convention and a record of what the 2026-09-15 migration did
(including the one non-verbatim step: D250/D252/D255 derived statuses).
2026-09-15 21:39:33 -04:00
James Dansereau 9c9d4a0997 docs(findings): migrate D250/D252/D255 to tail sections (ctx-opt P1, manual)
These three rows were fat topic with NO status cell (2-column rows). Topic
moved verbatim to new ## Dxx sections; table rows get a substantive one-liner
+ a status DERIVED from each entry's own verdict text (D250 FIXED / D252 OPEN
known-issue / D255 OPEN pending rule-#2 sign-off) — the derivation is flagged
in-section for auditability. CSV regenerated, label set unchanged (171); all
three topics verbatim-checked against the snapshot.
2026-09-15 21:30:27 -04:00
James Dansereau 9a8f1c9162 docs(findings): migrate D211/D219 to tail sections (ctx-opt P1, structurally-odd follow-up)
Pure moves following the 6208276d formatting repair: topic/status cells moved
verbatim to new ## Dxx sections; table rows now carry one-line summary +
pointer. CSV regenerated, label set unchanged (171); both sections
verbatim-checked against the snapshot.
2026-09-15 21:29:31 -04:00
James Dansereau 6208276d2b docs(findings): repair pre-existing table formatting in D211/D219/D255 (NOT a content edit)
Formatting fixes only — text preserved verbatim, no content added/removed:
- D211: the HUD ammo display '7 | 93' contained a literal unescaped pipe,
  splitting its status cell in two; wrapped that token in backticks.
- D219: a call-chain used pipes as separators between separate code spans
  (5 cells instead of 3); merged into one continuous code span — same text,
  backtick positions moved.
- D255: row lacked the trailing pipe its neighbours have; added it so the
  table is well-formed 3-column (status cell empty, as in D250/D252).

These were pre-existing CSV corruption (garbled/truncated status_raw), not
migration artifacts. CSV regenerated; label set unchanged (171).
2026-09-15 21:29:20 -04:00
James Dansereau 29dd9ac882 ci: warn on dev-docs size budgets (ctx-opt P2)
Warn-only (non-fatal): findings.md §F table region > 150 KB means fat rows
are accumulating again (cell content belongs in ## Dxx sections; lookup path
is findings-index.csv); porting-notes.md > 120 KB. Lit until Bucket B of the
ctx-opt migration lands (~266 KB today). HANDOFF.md is gitignored so its
> 30 KB budget stays a local check.
2026-09-15 21:27:58 -04:00
James Dansereau dd2e95e229 docs(findings): repair D233/D234 disambiguation markers (were inserted before '##')
The previous commit's (a)/(b) insertion landed at the line start, producing
' (a)## D233 — …' lines that no longer parse as headings. Moved the markers to
after the label: '## D233 (a) — …'. 117/117 migrated sections now have valid
headings; CSV check + label set still green.
2026-09-15 21:24:21 -04:00
James Dansereau 1726221030 docs(findings): disambiguate duplicate D233/D234 section headings (a)/(b)
Two table rows each carry the same label+topic with a status progression
(earlier vs updated pass). Both migrated verbatim in batch 6; heading markers
are formatting only. Flagged for Bucket B consolidation.
2026-09-15 21:23:47 -04:00
James Dansereau 60b1e4ff61 docs(findings): migrate fat §F rows D271-D294 to tail sections (ctx-opt P1, batch 8/8)
12 rows: topic/status cells moved verbatim to new '## Dxx' sections at file
tail; table rows now carry a one-line summary + pointer. CSV regenerated, label
set unchanged (171). Nothing deleted.
2026-09-15 21:23:27 -04:00
James Dansereau 63b4d79ce6 docs(findings): migrate fat §F rows D244-D270 to tail sections (ctx-opt P1, batch 7/8)
20 rows: topic/status cells moved verbatim to new '## Dxx' sections at file
tail; table rows now carry a one-line summary + pointer. CSV regenerated, label
set unchanged (171). Nothing deleted.
2026-09-15 21:23:27 -04:00
James Dansereau ce4ec5a387 docs(findings): migrate fat §F rows D218-D243 to tail sections (ctx-opt P1, batch 6/8)
21 rows: topic/status cells moved verbatim to new '## Dxx' sections at file
tail; table rows now carry a one-line summary + pointer. CSV regenerated, label
set unchanged (171). Nothing deleted.
2026-09-15 21:23:26 -04:00
James Dansereau 5446cc295b docs(findings): migrate fat §F rows D201-D216 to tail sections (ctx-opt P1, batch 5/8)
10 rows: topic/status cells moved verbatim to new '## Dxx' sections at file
tail; table rows now carry a one-line summary + pointer. CSV regenerated, label
set unchanged (171). Nothing deleted.
2026-09-15 21:23:20 -04:00
James Dansereau 4ea303e25e docs(findings): migrate fat §F rows D184-D199 to tail sections (ctx-opt P1, batch 4/8)
Also disambiguates the two '## D152+' headings from batch 2 as (a)/(b)
(formatting only; content untouched — flagged for Bucket B consolidation).

14 rows: topic/status cells moved verbatim to new '## Dxx' sections at file
tail; table rows now carry a one-line summary + pointer. CSV regenerated, label
set unchanged (171). Nothing deleted.
2026-09-15 21:23:19 -04:00
James Dansereau 2d50dd2319 docs(findings): migrate fat §F rows D169-D181 to tail sections (ctx-opt P1, batch 3/8)
9 rows: topic/status cells moved verbatim to new '## Dxx' sections at file
tail; table rows now carry a one-line summary + pointer. CSV regenerated, label
set unchanged (171). Nothing deleted.
2026-09-15 21:22:48 -04:00
James Dansereau 415abb4438 docs(findings): migrate fat §F rows D152-D168 to tail sections (ctx-opt P1, batch 2/8)
15 rows: topic/status cells moved verbatim to new '## Dxx' sections at file
tail; table rows now carry a one-line summary + pointer. CSV regenerated, label
set unchanged (171). Nothing deleted.
2026-09-15 21:22:47 -04:00
James Dansereau cd6f0b5e83 docs(findings): migrate fat §F rows D123-D151 to tail sections (ctx-opt P1, batch 1/8)
16 rows: topic/status cells moved verbatim to new '## Dxx' sections under a
provenance divider at file tail; table rows now carry a one-line summary +
pointer. CSV regenerated, label set unchanged (171). Nothing deleted.
2026-09-15 21:22:39 -04:00
James Dansereau d7d84c09f5 docs: HANDOFF archive receives M-140 -> ~M-12 block (ctx-opt P0)
Companion to the local split of docs/HANDOFF.md (gitignored rolling
working file — that half of the move is working-tree-only, backed up in
the pre-edit snapshot). Appends lines 137-5764 of the pre-split HANDOFF
(### ARCHIVE — M-140 onward, ~444 KB) after the existing M-2/M-3 brief,
with a divider documenting coverage and the newest-first ordering of
the moved block. Content verbatim; CRLF->LF normalised to match this
file and .gitattributes (eol=lf). Verified: live head + moved chunk
rejoin byte-identical to the original (CR-stripped); appended region
cmp-clean against the snapshot.

Archive intro corrected: it claimed 'through session M-11' but held only
M-2/M-3; the M-4-M-11 gap is pre-existing, noted as such.
2026-09-15 21:11:53 -04:00
James Dansereau eb184b6df1 docs: point auto-loaded context at findings-index.csv, not the §F fat table (ctx-opt P1.5)
AGENTS.md findings bullet + HANDOFF-ARCHIVE.md intro pointer now steer
cold-starting agents to docs/dev/findings-index.csv (grep this, then read
the specific ## Dxx entry) instead of linear-reading the ~100k-token §F
table. Base: release/v0.3.0 @ bf24bef8.
2026-09-15 21:11:00 -04:00
James Dansereau bf24bef8a9 docs(D294): correct trigger condition - ground level, not elevated vantage
User clarified the black-geometry drop-out happened at normal ground
level during normal progression near the statue, not from the
elevated ramp with wide sightlines as the first write-up assumed.
Retracted the "widened FOV frustum from elevation" framing of the
room-visibility hypothesis; kept the underlying room-transition/
streaming-timing guess since it doesn't depend on elevation.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:38:43 -04:00
James Dansereau a001f1c137 docs(D294): new finding - Statue ground briefly renders black near the elevated CCCP-monument ramp, self-recovers
User report at Video.FovScale~130 + max draw distance/LOD: ground
geometry near an elevated wide-visibility vantage point drops out to
solid black, then recovers 10-30s later on its own. Doesn't cleanly
match D222 (NPC/object culling, FIXED), D249 (default-FOV far
geometry, doesn't self-recover), or D218 (fog/tint boundary, not
missing geometry) -- logged as its own finding with those three as
neighbors, not a confirmed duplicate of any. Leading unconfirmed
guess: a portal/room-visibility BFS not accounting for the widened
FOV frustum.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:37:28 -04:00
James Dansereau b6ed6b1d36 docs: fold D289 into D255 (likely same terminal crash), consolidate D173/D292 as one Bond-positioning thread
User corrections on the playtest batch: D289 (Dam computer-room
crash) is most likely the same computer-terminal SIGSEGV D255 already
fixed this session, not a separate mystery -- needs a re-test on the
post-fix build before treating as new. D292 (Bond sunk into floor on
death) is confirmed the same "Bond's world model positioned wrong"
family as D173/D148, not a separate bug -- cross-referenced both
directions so the split-across-two-files writeup (GRAPHICS-BACKLOG.md
D148 + findings.md D173) is easier to find as one thread covering
level-start floating, cutscene spin, and now death-pose sinking.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:35:52 -04:00
James Dansereau 4c2643c239 docs: log Steam Deck playtest feedback batch (D289-D293 new, cross-refs to D173/D219/D243/D246)
User completed a Dam-through-Runway Steam Deck playtest and reported
9 items. Four corroborate already-open findings (D173 third-person
Bond floating at level start, D246 fullscreen edge strips -- Deck's
own default preset sets MSAA=4, exactly this row's leading suspect,
D219 purple/blue particle colors widened to blood decals, D243 Dam
end-cutscene camera glitch -- second independent confirmation). Five
are new: D289 (Dam computer-room crash, non-reproducing), D290 (watch
controller-page missing pad graphic), D291 (Dam truck texture
glitches), D292 (death-pose Bond sunk into floor, possibly D173-
adjacent), D293 (no way to quit the game from the UI -- confirmed in
code, SDL_QUIT via window-close/Alt+F4 is the only path; a QoL gap on
Deck/controller-only setups).

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:33:07 -04:00
James Dansereau d475884456 docs(D288): promote D171 (Silo stray triangle) with user-reproduced, visually-documented repro
User reported a stray triangle during the Silo missile-shaft camera
pan (previously D171, parked since M-34 as "observed, not
investigated"). Reproduced headlessly, precisely bounded to frames
~562-741 of a bare -level_20 boot: a large, flat-shaded, untextured
brown triangle (uniform RGB(83,62,44)) grows then shrinks across the
pan, consistent with a real world-space triangle mis-transformed near
the camera plane rather than a HUD element or garbage vertex-color
read. Points at the D106/D233 near-plane/w-clip neighborhood in
port/fast3d/gfx_pc.cpp, not yet confirmed at the code level. Evidence
saved to docs/img/bugs/. Also noted: foreground `timeout N ...` launches
of the exe from this session's Bash tool produce zero GE_PCDUMP output;
backgrounding the launch works reliably -- worth remembering next time.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:27:54 -04:00
James Dansereau a2b028737b docs: fix Ourumov spelling in D263 quoted report title
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:19:28 -04:00
James Dansereau 791bf6b7c0 docs(D263): confirm Ourumov/Trevelyan standoff timeout fallback is by design, not a bug
User reproduced the "stuck standoff" 3-4 times; traced to a hard
timeout in ai_22 (label 0x13, UsetuparkZ.c) that fires the execution
unconditionally after ~2.5 min if no other trigger fires first. User
waited it out and confirmed it resolves normally with dialogue -- not
a freeze, not a new bug. Documented so it isn't re-flagged.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:17:17 -04:00
James Dansereau 269de57941 Merge docs/d287-freeze-finding: new D287 freeze finding + D252 bullet-impact confirmation
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:00:49 -04:00
James Dansereau 86fdcc1db6 docs(D287): new finding - real non-recovering freeze after long idle + refocus; D252 confirmed on bullet-impact sparks too
D287: user hit a live freeze under debug.ps1 after ~15-20 min idle,
refocus-triggered, non-self-recovering (distinct from D286's single
fire-then-recover shape). Evidence preserved at
scratch/d287_freeze_gdb.txt (untracked, matches this repo's existing
scratch/ convention). Leading lead: gfxFrameMsgQ (32-slot scheduler
client queue) full while the VI-retrace pacemaker keeps ticking,
pointing at the main/render thread's consumer stalling rather than
the scheduler. Not root-caused; GE_D60 diagnostic already exists and
is the recommended next probe.

D252: user confirmed live the rainbow-hue bug still reproduces on
plain bullet-impact sparks, not just explosions -- a much cheaper
repro for the next GE_D252 capture.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 20:00:43 -04:00
James Dansereau 79d33a0a07 Merge fix/d285-audio-thread-race: D285 audio-thread SIGSEGV fixed via D4/D152-class lock
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:35:26 -04:00
James Dansereau 07b25f4b45 fix(D285): lock sndHandleEvent's preempt-scan against sndSetupSound's writer
sndHandleEvent's pool-exhaustion preempt-scan read/walked the shared
D_800243E4 active-ALSoundState list with no osSetIntMask lock, while
sndSetupSound (the list's insert path, reached from sndPlaySfx calls
across 15 game files) already locks it -- an asymmetric-locking gap,
the same D4/D147/D152 "N64 interrupts-off isn't a real lock on PC"
class. Wraps the scan in the identical lock/unlock pair sndSetupSound
already uses, unconditionally (no #ifdef PORT, matching that function's
own style -- osSetIntMask is genuine N64 API and the port's shim is a
process-wide recursive lock, safe to nest). Audited the rest of the
function: the scan is the only D_800243E4 touch point in it.

Build green; -level_09/-level_20 unregressed (1400+ frames, no crash,
no D152 lock-stuck warnings). Not yet gdb/live-confirmed against a
real reproduced crash -- a GE_INPUTSCRIPT-driven stress attempt this
session didn't visibly activate the scripted-input harness for a
reason not root-caused; the honest bar met here is "correct by D4/D152
precedent, build/sweep-clean," not "reproduced-then-fixed."

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:35:22 -04:00
James Dansereau 6f278ad4d4 Merge investigate/d243-fresh: D243 trace ported, build-verified, D250-fixed; still needs interactive playthrough
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:31:02 -04:00
James Dansereau 556f4434f3 D243: port GE_D243 front-end MENU_* trace onto release/v0.3.0, build-verify, fix uncached getenv
Cherry-picks the GE_D243 diagnostic (764a1f26, previously stuck on an
unmerged, un-build-verified branch) onto release/v0.3.0. The prior
session's "toolchain blocker" was this session's own TMP/TEMP-vs-Bash-tool
gotcha (documented in porting-notes.md this session), not a real
CMake/ninja issue -- now explained and worked around. Also fixes an
uncached getenv("GE_D243") per menu_init() call (D250's own rule: this
runs ~once/frame during front-end states) to a single cached check.
Build-verified; smoke-tested via a bare -level_33 boot (no crash, no
D243 lines, as expected since bare -level_XX skips the front end).
Still needs a real interactive playthrough to actually correlate
against the D146 stale-DL burst -- documented as the next step in
findings.md D243 (M-141), including an unconfirmed D173-connection
hypothesis raised by the user.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:30:58 -04:00
James Dansereau beef5a37f8 Merge feat/d282-menu-nav-leftstick: D282 front-end menu nav now defaults to left stick
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:28:07 -04:00
James Dansereau bb1e658ea5 fix(D282): front-end menu nav always uses left stick, matching F10-overlay convention
Menus read the same stick_x/stick_y channel as in-level gameplay, and
naturalPitchMode (SOLITARE-style, default-on) routes that channel to
the right stick for continuous look. Since front.c's menu nav shares
the channel, it silently inherited the right-stick routing, which is
unintuitive on a modern pad/Deck outside a running stage. The gamepad
poll already computed an unused padMenuMode flag for shoulder-button
gating; reused it to always source stick_x/stick_y from the left
stick outside RUN_STAGE, regardless of naturalPitchMode. Port-layer
only, inert during gameplay -- verified via -level_09/-level_20
unregressed; in-menu feel needs a human controller playtest.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:28:03 -04:00
James Dansereau e4211cced7 Merge fix/d252-rainbow-particles: D252 width/height suspect refuted, diag probe added; D283 doc confirmed accurate
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:25:26 -04:00
James Dansereau 4b1d9bc21c docs(D252): refute width/height suspect via authored asset data; add GE_D252 diag probe
The prior "prime suspect" (import_texture_rgba16 width/height derivation)
assumed line_size_bytes comes from the tile's MASK_16 wrap period, but
assets/oddtextures.c's actual gsDPSetTile for the fire render tile sets
line=4 explicitly, giving the correct 16x14 dimensions -- matching the
pre-existing is_fire_shape==16x14 filter already hardcoded into the
GE_TEXDUMP dumper. Neither named D252 hypothesis (width/height, GEVR's
byte-group-order alternative) survives; root cause stays open. Adds an
env-gated GE_D252 diagnostic (dims + raw texel dump) for whoever next
gets an interactive session to trigger the actual fire draw.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:25:15 -04:00
James Dansereau e0167c8266 Merge docs/gevr-triage-adopt: cheap GEVR-triage doc adoptions
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:18:17 -04:00
James Dansereau 8df8ad3c35 docs: adopt cheap GEVR-triage items (crash heuristic, frame-rate notes, process lesson, widescreen pre-flight)
Folds in the zero-code-risk ADOPT items from scratchpad/GEVR-TRIAGE.md
(a prior survey of the sibling GEVR VR port, same n64decomp/007
lineage, reference-only): a crash-triage heuristic for non-canonical
x86-64 pointer faults, two frame-rate-assumption notes that pre-empt a
future uncapped/high-refresh rabbit hole, a process lesson about
grep-built site lists standing in for a real read, and the
portal-scissor site map as pre-flight for whenever widescreen work
resumes.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 19:18:07 -04:00
James Dansereau eb0928889e Merge docs/d286-caverns-freeze: D286 Caverns freeze finding + log-unit fix 2026-09-15 18:55:32 -04:00
James Dansereau 65728e9f0e docs(D286): new finding - user-confirmed real freeze on Caverns, Steam Deck
Caught during the same live Deck session used to fix D255: the game's
own hang watchdog (portHeartbeatCheck) fired once, no frame rendered
for ~8s at frames=46404, dumped all-thread state, then rendering
resumed and the session continued to a clean close - not a crash, a
self-recovered stall. User confirmed by direct observation this was a
real, visible freeze (not a Deck-side system hiccup), happening early
in a Caverns playthrough, not tied to an explosion or terminal (rules
out D255/D285 as the cause).

Not root-caused - no distinguishing log line precedes it. Logged as
open with no repro trigger identified rather than guessed at further.

Also fixes a cosmetic units bug spotted while reading the watchdog
code: the heartbeat log printed a raw microsecond delta with a "ms"
label (port/src/libultra.c, no /1000 conversion) - now divided
correctly. No behavior change, log output only.
2026-09-15 18:55:26 -04:00
James Dansereau 2ec57eb40d Merge fix/d255-vtxstore-pointer-truncation: root-caused and fixed D255 2026-09-15 18:42:14 -04:00
James Dansereau 7ef0395a3f fix(D255): correct 32-bit pointer truncation in vtxstore_fix_refs
Root-caused via a live Steam Deck session over SSH: sub_GAME_7F09BAC4
(vtxstore_fix_refs) read an active OBJ record's Model* through
((Model*)((ChrRecord*)prop->chr)->chrflags) - a union pun onto
ChrRecord.chrflags, which is typed CHRFLAG, a 4-byte enum. On 32-bit
N64 this is bit-exact (the enum and a pointer are both 4 bytes, so
casting the enum's value to Model* reproduces the real stored bytes).
On 64-bit PC, Model* is 8 bytes but the enum read is still only 4 -
the cast reads just the pointer's low 32 bits as an int and
zero-extends it back to a 64-bit address, discarding the real high
bits. This is the D3x pointer-width ABI class from AGENTS.md - a
struct field misread across the 32-to-64-bit pointer-width transition.

This was actually flagged as suspect (a) in the very first D255
investigation, then wrongly deprioritized in favor of a "destroy-path
race" theory once a register capture happened to show Rdi=(nil) and
looked like it confirmed a different, unrelated NULL-model theory. It
didn't: (nil) is exactly what a 32-bit-truncated read produces
whenever the real pointer's low 32 bits happen to be zero - a
coincidence that pointed two sessions' worth of investigation at the
wrong field. Proof: reading the same record through the correctly
64-bit-typed ObjectRecord.model field (this session's diagnostic
probe) was always a valid, non-NULL pointer - the object's model was
never actually missing.

Fix: read the pointer through ObjectRecord.model (the same union
memory, correctly typed) instead of through the mistyped
ChrRecord.chrflags alias, at both use sites in the function. Identical
bytes and identical result on N64; correct on 64-bit PC.

Verified live on the user's own Steam Deck hardware over SSH, running
their own prepared ROM assets: held through the entire Facility level
including repeated terminal destruction (the most consistent prior
repro) and through Caverns' radio-objective terminals (the second
confirmed level), zero crashes. Windows rebuild + full 21-level
verify.sh sweep also clean.

The separate audio-thread crash (D285, caught during this same live
session) is unrelated and unaffected by this fix.
2026-09-15 18:42:08 -04:00
James Dansereau 49f18ad1e6 docs(D285): new finding - audio-thread SIGSEGV, separate from D255
Caught a real crash on the user's Steam Deck during D255 live-test
setup: a fresh ge007.crash.log from the published v0.2.1 binary,
inside the user's active testing window. Symbolized directly on-device
with addr2line against the crashing binary's own debug symbols.

FAULT ADDR 0x62 inside sndHandleEvent (src/snd.c:366), on the audio
thread (amMain/portThreadWrapper) - not D255's 0x10/vtxstore_fix_refs
on the render thread. Different bug, different subsystem, likely
conflated with D255 in reports since both surface during the same
explosion/combat testing.

Root cause (static, code-read only): sndHandleEvent's pool-exhaustion
preempt-scan walks the shared D_800243E4 ALSoundState list with no
osSetIntMask lock anywhere in the function, while the list's insert
path (sndSetupSound, reached from 15 different game files' direct
sndPlaySfx calls) does lock it - an asymmetric-locking gap matching
the established D4/D147/D152 "N64 interrupts-off isn't a real lock on
PC" race class. Fits the repro pattern (explosion SFX bursts = high
thread-contention window).

Not fixed - flagged two open questions before it should be: whether
the missing lock is a genuine decomp-fidelity gap (N64 assembly may
have had it) vs a legitimate new PC-only lock, and it needs gdb
confirmation on a live crash, not just this static read.
2026-09-15 18:12:30 -04:00
James Dansereau edcfb1f163 docs(D255): consolidate real-Deck evidence - destroy-via-explosion, not activation
Further Steam Deck testing on the published v0.2.1 release reconciles
the two competing D255 repro theories: the trigger is an object dying
via explosion, not terminal activation. Confirmed on four levels:

- Facility: every terminal crashes consistently when destroyed
- Caverns: terminals near the radio objective crash when they explode
- Archives: a similar crash when a guard's grenade detonates (no
  terminal involved at all - narrows the real trigger to "object dies
  via explosion damage" in general)
- Frigate: negative control - shooting a room of terminals there did
  not reproduce it, so it isn't simply "any destructible OBJ record
  exploding" either

Updated the D255 finding with a static lead for the next session (the
objFree destroy path never explicitly nulls ObjectRecord.model, so the
NULL likely comes from a freed-slot-reuse or list-modify-during-iterate
race rather than a direct field write - untested, needs a live Deck
capture with the GE_D255 probes already in tree). Updated
.github/release-notes.md's D255 bullet to match.
2026-09-15 17:58:25 -04:00
James Dansereau b322b55a70 docs(D255): log user's real-hardware Deck report (destroy-trigger, not just activation)
User reproduced on real Steam Deck hardware, v0.2.1 published release:
Facility crashes consistently when terminals are DESTROYED (shot), not
only on activation as the M-139 "corrected" framing concluded. Frigate
crashed once in the same pass, specifics not recorded.

Logging as a dated, evidence-stamped addition rather than overwriting
the existing activation-repro entry - both triggers are plausible
routes into the same underlying destroy-path-ordering race and are not
necessarily contradictory; neither has been cross-verified against the
other yet. Next Linux/Deck session should test both triggers with
GE_D255=1 before revising the "reliable repro" framing again.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 17:35:51 -04:00
James Dansereau 0c209dedd4 Merge diag/d255-vtxstore-null-model-probe: D255 diagnostic probes
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 17:12:41 -04:00
James Dansereau 126fe75d7f diag(D255): env-gated NULL-model probe for the Facility terminal crash
Adds a live-repro diagnostic for D255 (Facility door-opening terminal
SIGSEGV, Linux/Steam-Deck-only): vtxstore.c's vtxstore_fix_refs
(sub_GAME_7F09BAC4) skips-and-logs instead of dereferencing when it
finds a NULL model on an active PROP_TYPE_OBJ record, logging the
record's identity/state; propobj.c's objFree logs every free call so
the two logs can be correlated by prop/obj pointer. Both gated behind
GE_D255 (off by default, getenv-cached per the D250 lesson) - zero
behavior change unless set.

Attempted a live Windows repro this session (5 terminal activations
across 3 direct -level_34 launches): zero probe hits, zero crashes.
Combined with the existing WSL-negative result, this is now two
negatives against one Linux/Steam-Deck positive - the bug does not
reproduce on Windows, likely a genuine allocator/timing difference.
Landing the probes anyway since they're inert by default and are the
fastest path to a real trace whenever a Linux/Deck session is
available; no more Windows time should go toward a live repro here.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 17:12:35 -04:00
James Dansereau 78bca455cf Merge fix/d284-prng-mips64-shift: D284 MIPS64 PRNG shift fix
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 16:40:24 -04:00
James Dansereau 0de99b4db4 fix(prng): correct MIPS64 dsll32/dsrl32 semantics in port/src/random.c (D284)
random.c's dsll32/dsrl32 helpers modelled the real MIPS64 dsll32/dsrl32
instructions as ordinary 32-bit shift-by-n operations, masked to 32
bits. Real MIPS64 dsll32/dsrl32 shift the FULL 64-bit register by
n+32 with no masking - they exist specifically to encode a 32-63 shift
amount in a 5-bit immediate field. The bug was bit-exact-looking but
wrong: it produced a deterministic, self-consistent stream that was
never the N64's actual stream, from the very first draw.

Surfaced by M-138's security/decomp-fidelity review (a forked review
agent's numeric trace + a hand trace of random.s both found the same
divergence); fix decided over disclose-as-known-issue per M-139b.

Fix: dsll32(x,n) = x << (n+32), dsrl32(x,n) = x >> (n+32), no masking.
No other line changes - every call site (randomGetNext,
randomGetNextFrom, chrObjRandomGetNext) already mirrors random.s/
chrObjRandom.s line-by-line through these two helpers plus plain <</>>
(which already matched dsll/dsrl correctly).

Verified against a standalone C reproduction outside the game (first
five draws from random.s's real initial seed diverge completely
between the buggy and corrected helpers, confirming the fix actually
changes the output stream). 242/242 clean build; full 21-level
verify.sh sweep PASS, no crashes.

This silently changes every RNG-derived sequence (loot, AI variance,
anything seeded from randomGetNext/randomGetNextFrom) from the first
draw in every build going forward. Savegames are unaffected (seed
isn't save state); existing recorded replays will diverge against a
post-fix build. Called out in v0.3.0 release notes per M-139c.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 16:40:19 -04:00
James Dansereau 6807555c17 Merge fix/d266-d271-extraction: D266 IA4/texedge + D271 portal-culling fixes
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 16:34:33 -04:00
James Dansereau 532092c732 fix(render): land D266 IA4/texedge decode + D271 portal-culling fixes (M-139 R2)
R2 extraction from the parked D236 investigation tree
(park/d236-probes-m139): pulls out only the two verified, behavior-real
fixes and drops every D267-D280 debug probe (census counters, PPM
dumpers, per-tri/per-portal logging) that had accumulated alongside them.

- D266: import_texture_ia4 decodes RDP IA4 as I2:A2 (was I3:A1, an
  inherited PD assumption); gfx_opengl.cpp's CVG_X_ALPHA "texedge" path
  now keys off the real alphacompare field (dither/threshold/none)
  instead of a hardcoded 0.19 cutoff, and gfx_pc.cpp excludes AC_DECAL
  tiles from the texedge path entirely (it never discards on real
  hardware).
- D271: the D106 near-plane portal-straddling guard's `lim` constant
  widens from 1e5 to 1e38 so finite out-of-range bounds flow through to
  bgRectIntersect and clamp/cull exactly as they do on N64, instead of
  being force-mapped to full-screen (was drawing whole rooms N64 culls).

Checked D271 against D249 (distant-geometry drop-out on Streets/Egyptian)
before landing per M-139's overlap concern: no overlap, different code
paths (near-plane portal guard vs. far-plane/LOD symptom); D249 stays
open.

Verified: 242/242 clean build; full 21-level verify.sh sweep clean (one
dam NO-FRAMES on the sweep pass reproduced PASS on an immediate solo
re-run - a load-sensitivity flake, not a regression).

The D267-D280 probe code and the rest of the D236 tree-wall
investigation stay parked on park/d236-probes-m139 for whenever that
thread resumes (still no root cause for the tree wall itself - see
D280's negative outcome).

Also documents a build-environment gotcha hit this session: this Bash
tool's shell reports MINGW64 (uname/$MSYSTEM) but is actually
Git-for-Windows Bash, missing cmake/ninja/gcc from PATH until
/c/msys64/mingw64/bin and /c/msys64/usr/bin are prepended explicitly
(docs/porting-notes.md SS E).

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 16:34:28 -04:00
James Dansereau e7fee0d7a4 docs: v0.2.1 known-issues sweep, pre-1.0/roadmap framing, AI disclosure
Batched pass across README/release-notes/Pages (one commit instead of the
scattered per-fix pushes earlier this session, per new commit-cadence
preference):

- New known issues surfaced that weren't documented anywhere: no real
  widescreen (16:9 stretches 4:3 rather than expanding FOV), no controller
  rebinding UI / macOS-ARM builds, culling/LOD dropout on Streets/Egyptian
  (D249), gunshot SFX oddities under sustained fire (D240/D241).
- Fixed a stale claim (in three places) that a wrong-sounding-instrument bug
  was still open; it was fixed in v0.2.0 (D230).
- Softened the D259 "All unlocked" fix to reflect reality: still breaks
  audio/aim if enabled before any save exists; flagged experimental
  (new finding D281).
- Corrected D255's repro to the precise, deterministic trigger found this
  session (Facility's door-opening computer terminals, Linux-wide not
  Deck-specific) instead of the vaguer original combat-crash description.
- Logged D282 (front-end menus need right stick not left on gamepad) and
  D283 (Steam Deck first-run preset skipped by an early Desktop Mode
  launch) with user-facing workarounds.
- Refined D263 (Ourumov/Trevelyan) internally with a position-sensitivity
  caveat; no public bullet since it doesn't block progression.
- Added pre-1.0 framing and a new README Roadmap section (known-issues
  cleanup, PAL/JP, real widescreen, rebinding, macOS/ARM, LAN multiplayer
  explicitly "under consideration, no ETA", plus an explicit "not currently
  planned" list) across README/release-notes/Pages.
- Added an AI-development disclosure (agentic development as a genuine
  study of the process, not just a port) across the same three docs.
- Pinned the release-notes hero GIF to the v0.2.1 tag instead of v0.2.0.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 12:53:57 -04:00
James Dansereau 4b51ded7d1 docs: SEO round 2 (Bing) - demote per-page h1 to h2 site-wide, re-trim meta description, bump README/Pages to v0.2.1
Bing Webmaster re-flagged the site after the first SEO pass (32f193a0):
- Every Jekyll-processed doc page (building.md, dev-process.md, internals.md,
  porting-notes.md, dev/agentic-development.md, plus index.md caught last
  time) opens with its own "# Heading" on top of the Cayman theme's masthead
  <h1 class="project-name">, so all of them emitted two h1 tags, not just the
  homepage. Demoted each page's opening heading to h2 -- anchor slugs are
  unaffected (kramdown ids don't depend on heading level).
- index.md's meta description was 161 chars (1 over Bing's 160 limit) and
  still said v0.2.0; re-trimmed to 133 chars and bumped to v0.2.1.
- README.md and docs/index.md's Status lines/comparison table bumped from
  v0.2.0 to v0.2.1 to match the actual current release.

Verified against a live, cache-busted fetch of the deployed Pages site
(the first Bing report was reading a stale CDN edge cache from before the
32f193a0 deploy, per its own Last-Modified header) that the underlying fix
was already live; this pass addresses what was still genuinely wrong.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 10:04:02 -04:00
James Dansereau ff677da74d fix(ci): regenerate findings-index.csv (stale after D216/D181 edits)
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 09:36:44 -04:00
James Dansereau 795c8f176f release-notes: document ScreenShakeIntensity menu removal (D181)
Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 09:29:21 -04:00
James Dansereau 1e11e1a5fd v0.2.1 polish: pull Game.ScreenShakeIntensity from the F10 menu (D181)
User testing found the slider "basically useless" -- viShake() only scales
explosion/effect shake (explosion.c), never the always-on walking head-bob
or a getting-shot reaction, which is what "Screen shake" implies to a
player. Removed the menu row until it covers all screen-shake/view-bob
sources, not just explosions. Config var + fr.c hook untouched.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 09:28:06 -04:00
James Dansereau 99f4f38e1c v0.2.1 polish: pull Game.SkipIntro from the F10 menu (breaks audio)
User spot-check of the v0.2.1 draft found that enabling Skip intro breaks
audio. It was already off by default (D216); removing the menu row so
players can't enable it until the audio interaction is root-caused. The
config var and lv.c hook are untouched (dead unless an existing ini already
has it set).

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 09:18:40 -04:00
James Dansereau 06b675b5e8 fix(ci): revert GE-ENV-PROBES.md rows for uncommitted D236 probes
The v0.2.1 docs commit (572ab7bf) added GE-ENV-PROBES.md rows for the
D266-D280 tree-investigation probes, but that probe code itself was
deliberately left uncommitted (still mid-investigation, unswept). CI's
tools_pc/gen_env_probes.py --check caught the mismatch: 13 documented
vars with no live getenv() site in the committed tree. Reverting the
doc rows to match what's actually committed; they'll return together
with the probe code once the D236 investigation lands.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 08:59:51 -04:00
James Dansereau 572ab7bf5b docs: findings/backlog through D280 (v0.2.1 batch + D236 tree-wall passes 2-9)
Records the v0.2.1 hotfix batch (D259-D264, this commit's code) and the
ongoing D236 Surface-1 tree-wall investigation (D265-D280): still open as of
D280 -- fog math verified N64-faithful, the noise class confirmed as the
weave's painter, the portal-culling over-draw fix (D271) real but
insufficient, no quick win identified. The D266-D280 probe code and the
D266 IA4 decode / portal-culling fixes themselves stay uncommitted pending
the game-wide regression sweep GRAPHICS-BACKLOG.md flags.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 08:21:56 -04:00
James Dansereau 32f193a02f docs: trim GitHub Pages descriptions for SEO, drop duplicate h1
_config.yml's description was ~230 chars once jekyll-seo-tag spliced it
into the masthead tagline and home <title>; index.md's page description
was ~277 chars for the same og/meta tag. Also removed index.md's manual
"# GoldenEye 007 PC Port" h1 -- the Cayman theme already renders the site
title as an h1, so the page was emitting two.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 08:21:42 -04:00
James Dansereau ffb786bad8 v0.2.1 hotfix batch: D259 audio silence, D260 wheel direction, D261 watch-nav repeat, D263 script order, D264 watch preview model
User playtest reports against v0.2.0:
- D259: geEepromPatchAllCheats left an all-zero fresh save at volume 0
  (silence) instead of seeding max volume like fileResetSave would.
- D260: swap wheel-up/wheel-down to match the PC wheel-menu's on-screen
  order (was inverted vs. user expectation).
- D261: watch-menu stick-held navigation had no repeat past the smooth-scroll
  band, so holding W/S or a pegged stick crawled one item per tap; added a
  bounded auto-repeat.
- D263: GE_INPUTSCRIPT stick tokens were applied after the stick was already
  written out and silently discarded (test-harness fix, kept from the D263
  Facility/Ourumov investigation, which itself found no port bug).
- D264: watch item-preview ModelRenderData template read across two globals
  that no longer sit adjacent on PC, zeroing renderdata.flags and gating all
  geometry emission in the preview's subdraw pass.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
2026-09-15 08:21:32 -04:00
James Dansereau b0dc9d1257 README: absolute URLs for Releases/issue links (leading-slash forms 404 on GitHub) 2026-09-14 01:04:24 -04:00
James Dansereau de6c875bb8 docs: cut-content research — article bodies recovered, claims verified vs decomp
All six TCRF article bodies were extracted (backlog/cut-content/articles/,
gitignored) and cross-referenced against src/ + assets/obseg/. Full verified
findings with file:line refs land in backlog/cut-content/RESEARCH.md.

Verified 1:1: the 11 unused cheats (CHEAT_ID enum vs cheat.c grant table),
music track index (MUSIC_TRACKS enum vs Notes-page GS index), MP character
table (~40 entries, 1.0446 eye height), all unused text strings (LmiscE/LgunE/
LpropobjE/LtitleE), build dates (compiletime.c), Spectrum emulator plumbing
(front.c MENU_SPECTRUM_EMU — only the transition is missing).

Corrections to TCRF/first pass: bg_dest is shipped Frigate geometry (not an
early version); cryp/dish are shipped Egypt/Temple; Library/Stack share
Basement's bg_ame; 28 of 29 unused items exist in the object table.

Tracked doc updated to point at the research file; backlog/ gitignored.
2026-09-14 01:02:07 -04:00
James Dansereau 395c616fc5 docs: cut-content research backlog from TCRF export (first pass)
Cross-reference the local TCRF 'cutting room floor' export
(scratchpad/backlog-scrape/restore-cut-content-mod, gitignored) against the
decomp source and extracted asset tables. Article bodies were lost to a
Cloudflare challenge at scrape time; findings rest on the ~170 intact article
images + all image filenames, each mapped to ROM object/level evidence:

- A: full debug menu (U+D C-hold) decompiled in debugmenu_handler.c — free cam,
  level switch, ramrom record/replay, screenshot grabs; all compiled into PC build
- B: cut weapons (taser/rocket/grenade/P90/spectre/autoshot) have models + stats
  + SFX; rocket/grenade/P90 grantable via existing cheat codes (cheat.c)
- C: 'unused items' page maps 1:1 to file_resource_table entries with use-logic
- D: 8 commented-out MP map entries in front.c with dev notes (Citadel/Statue/
  Cradle/Aztec/Runway/Dam/Depot/Frigate)
- E: per-level unused props + orphaned level geometry (Crypt has bg+briefing but
  no LEVELID; len/dish/cat/oat/arec/ame/ark/ref dirs beyond shipped set)
- F/G/H: unused faces, prerelease comparison pairs, dev-build video embeds

Feasibility tiers 0-3 with the port-layer-only constraint called out.
2026-09-14 00:48:30 -04:00
64 changed files with 13090 additions and 1043 deletions

No files matched your search

+8 -3
View File
@@ -13,9 +13,14 @@ rules there are non-negotiable.
## Scope check
- [ ] No changes under `src/` or `include/` — **or** the only changes are the
narrow `#ifdef PORT` ABI exception (CONTRIBUTING.md rule 2), and each is
documented in `docs/porting-notes.md` / `docs/dev/findings.md`.
- [ ] No changes under `src/` or `include/` — **or** the only changes are
the narrow `#ifdef PORT` ABI exception (CONTRIBUTING.md rule 2),
checked against `docs/porting-notes.md` §A1, and documented in
`docs/dev/findings.md` (§F/D3x).
- [ ] If this PR contains a genuine `src/game` behavior change (not an
ABI/layout fix): an explicit rule-2 sign-off was granted
(`docs/dev-process.md` §7) and is recorded in a
`RULE-2-SIGNOFF`-tagged `docs/dev/findings.md` entry linked here.
- [ ] `Makefile`, `tools/`, `rsp/`, `ld/` untouched (N64 build).
- [ ] If `CMakeLists.txt` `REGION_DEFS` changed, it still matches the N64
`Makefile` per-region macro set exactly.
+153 -71
View File
@@ -1,8 +1,8 @@
## GoldenEye 007 PC Port <version>
<p align="center">
<img src="https://github.com/jkdansereau/goldeneye-pc-port/raw/v0.2.0/docs/media/goldeneye-gh-preview.gif" width="480"
alt="~32 s gameplay montage from live play sessions (no audio track)">
<img src="https://github.com/jkdansereau/goldeneye-pc-port/raw/v0.3.0/docs/media/goldeneye-gh-preview.gif" width="480"
alt="~12 s gameplay montage from live v0.3.0 play sessions (no audio track)">
</p>
> The full single-player campaign runs at a steady 60 fps with audio (music +
@@ -10,81 +10,158 @@
> is completable end to end: the whole campaign has been playtested through
> all 21 missions (Agent difficulty). It's an early public cut: all 21 solo
> missions load and run crash-free, but the known issues below are real;
> feedback is very welcome.
> feedback is very welcome. **This is a pre-1.0 release, not a finished
> product** — expect rough edges and missing features until v1.0; see the
> [README's Roadmap section](https://github.com/jkdansereau/goldeneye-pc-port#roadmap)
> for where this is headed.
### What's new since v0.1.0
### What's new since v0.2.2
- **Drop-in ROM, no tooling**: unpack the bundle, drop your NTSC-U (US) ROM
in `data/`, launch; the first run generates the derived assets
automatically. No Python, no installer, nothing to set up.
- **Steady 60 fps** in normal play (the software RSP runs off the presentation
critical path); `Video.DisplayFPS` in the F10 overlay shows it.
- **Audio**: in-level music and sound effects throughout (the alpha was
silent). A handful of tracks have wrong-sounding instruments (below).
- **Mouse**: click-to-lock capture (click to grab, ESC to release) with a
proportional GEPD-style aim mode; sensitivity / Y-inversion / aim-turn split
tunable in `ge007.ini` or the F10 overlay. The legacy always-grab mode is
gone.
- **Rendering**: outdoor skies render correctly; water no longer renders
green/pulsing; reflective surfaces (glass, chrome weapon skins) work.
- **Crash fixes**: the two v0.1.0-era crashing levels (Bunker ii, Statue) and
the AI-pacing bug that broke Cradle are fixed and playtest-verified: all 21
solo missions load and run crash-free. Also fixed: the Steam Deck / Linux
Facility crash triggered by walking into the crouch-forcing spot at the
level's opening (D253); this also revives the auto-crouch and ladder
signals, which were dead on PC before.
- **QoL**: F10 in-game options overlay (fullscreen, resolution, frame cap,
MSAA, texture filtering, FOV/draw distance, sensitivity), F12 screenshot.
- **Sharper defaults out of the box**: draw distance and LOD swap distance now
default to 150% of the authored N64 values, so props no longer fade in just
before they become visible (Dam's alarms and wall switches being the tell);
F10 → *Draw distance* / *LOD distance* set back to 100 restores the
console-authentic look. MSAA now defaults to 4× instead of off.
- **All-unlocked toggle**: F10 → *All unlocked* makes all 21 solo levels
selectable at every difficulty from the first launch, adds 007 mode, and
fully populates the cheat menu. It is off by default (faithful N64
progression). No *active* cheats are enabled either way (weapons remain
per-mission pickups, as on N64).
- **Steam Deck first-run preset**: on SteamOS the first launch seeds
Deck-friendly defaults (native 1280×800 fullscreen, VSync, MSAA 4, 150%
draw/LOD distance); an existing `ge007.ini` always wins. Two more F10 rows:
*No hit flash* (suppresses the damage-flash overlay) and *Skip intro*.
- **Modern dual-stick controller layout** (the scheme used by the console
re-releases): left stick move/strafe, right stick look, right trigger fire,
left trigger aim, A/X use, B/Y crouch/cancel, **RB/LB cycle weapons**.
- **Linux / Steam Deck**: the Linux bundle now ships its own SDL2, so it runs
as-is on any distro, and sideloads onto a Steam Deck with nothing
installed. Saves and F10 settings now persist no matter which directory you
launch from (previously Linux wrote them relative to the launch directory
only, so they silently failed elsewhere; D256); the options overlay is
fully gamepad-driven on the Deck.
- **Bond is fixed in cutscenes** — the most visible remaining glitch in
prior releases: Bond no longer floats or glitches out during cutscenes,
his animation no longer breaks or repeats, and the camera correctly
tracks him through scripted sequences including all three Dam end-of-level
abseil shots (D243). Root cause: a 32-bit-era struct-size constant in the
cutscene body-model setup never accounted for 64-bit pointer widening,
corrupting the model's own animation-timing fields on creation. Fixing
that also fixed the associated camera shake, which had previously been
worked around with a camera-freeze hack that's no longer needed. **Two
related reports also cleared up as a side effect**: Bond's third-person
model floating above the ground at level start, and death animations
sinking him into the floor, both shared an underlying mechanism with this
bug — Bond now ends up in the right place the large majority of the time,
with only a small positional drift on the rare occasion he's off (D173/
D292).
- **Particle colors mostly fixed**: bullet-impact sparks and lingering
smoke/explosion residue no longer cycle through a rainbow palette (D219) —
root cause was a hardcoded raw-memory-offset read into the spark's stored
color that landed on the wrong bytes once the containing struct grew from
64-bit pointer widening. A second, separate cause was found and fixed in
explosion rendering itself (an out-of-bounds vertex-buffer write
corrupting adjacent particle data). This fix is real and stays in, but the
rainbow effect can still occur intermittently on any platform, not every
time and not on every level — a second cause hasn't been found yet; see
Known Issues.
- **Automatic widescreen FOV scaling**: the game now scales its field of
view to match your display's aspect ratio automatically, instead of
requiring the old manual FOV-scale slider. Toggle off in the F10 overlay
(`Widescreen Auto`) if you prefer the original 4:3 framing. The scaled
vertical FOV is clamped to a 20°–160° range as a guard against degenerate
values on unusual aspect ratios. (This is FOV scaling, not a native 16:9
world/HUD re-render — see Known Issues.)
- **TV overscan artifacts removed**: the black bars top/bottom and the thin
pixel strips left/right of the gameplay view are gone (D247/D246) — both
were the original N64 TV-safe-area margin, invisible on a CRT under real
overscan but visible on a PC monitor. New `Video.SafeAreaCrop` toggle
(default on) if you want the original framing back.
- **Two more crash fixes**: a campaign-halting crash at the end of Control
Center on 00 Agent difficulty, which would then crash on every subsequent
relaunch until the save was reset (D295); and a crash selecting most save
slots with `Skip Intro` enabled (D299).
- **The elusive complete freeze is root-caused and fixed.** Players had hit
a total, non-self-recovering lockup (most recently when returning from
Caverns to the intro screen) that was real but never reproduced on demand.
It turned out to be a latent defect in the original game's own AI command
stream — a record type the engine mis-measures, which our level-teardown
timing can trigger and the N64 never presents. Fixed with a minimal change
under the project's game-code exception process (D309/D310); a second,
unrelated freeze mechanism found during the same playtest is now guarded
as well (D311).
- **A NULL-pointer guard was added** around a save-data lookup that two
players hit with an identical crash address (GitHub #87, D301) — this
should close that crash, though we weren't able to reproduce the exact
trigger condition ourselves to confirm it end-to-end.
- **Facility's execution softlock is caught and recovered from**: a rare
race in the original game's own AI script (present on the N64 too) could
leave Ourumov frozen mid-execution after Objective C, with no way out short
of reloading. A port-side watchdog now detects the exact unrecoverable
state and re-seeds the attack so the scene plays out as authored; it
re-arms once the scene has cleared, so replaying Facility later in the
same session is still covered (D318).
- **A Steam Deck crash is fixed**: an audio-thread bug could crash the game
early in a level (`sndHandleEvent`'s sound-preemption scan could
dereference a pointer without checking it was valid first). Live-verified
crash-free on real Steam Deck hardware, including with `All unlocked`
toggled on.
- **Mouse input and F10 controls fixed**: mouse input was reworked to
follow the Perfect Dark PC port's architecture — slow mouse motion is no
longer barely recognized and fast motion no longer saturates almost
immediately (D300, `Input.MouseDirectLook`, on by default). The separate
aim-speed and turn-speed sliders — which could be set to inconsistent
values against each other — are now a single `Mouse sensitivity` control
with a wider range. Also fixed three F10 menu bugs found along the way:
clicking a row could unexpectedly scroll the whole list, a click near
certain rows could silently activate a different option than the one you
clicked (most noticeably, clicking `All unlocked` could instead trigger
`Quit to desktop`), and the panel's bottom row no longer duplicates
whatever item is currently selected (D251).
- **Random-number generation now matches the N64 original exactly** (D284) —
a shift-operation bug in the PRNG had desynced the PC's random stream from
N64's since the very first release, affecting loot placement, AI variance,
and other randomized elements. **This changes the random sequence from
every prior build**, so old input recordings/replays will diverge, but
your save files are unaffected: **a migration shim automatically upgrades
any save written by an older build the first time you load it** (D297) —
no manual action needed.
- **Assorted texture/rendering fixes**: an incorrect IA4 texture-format
decode and an overly-tight near-plane portal-culling guard were both
corrected (D266/D271) — reduces some texture-edge and visibility artifacts
on affected levels.
### Known issues
- **Cutscenes still glitch, mostly with James Bond**: in scripted sequences
Bond is the one who gets misplaced, hovers, or spins; the other actors are
fine for the most part now. The Dam level-end cutscene is racy (D243).
Still the most visible gap in this release.
- **Particle colours cycle through a rainbow palette**: bullet-impact sparks
and lingering smoke/explosion residue drift through the hues over time
instead of holding their intended grey/orange palette (D252).
- Water on `IsWater` levels shows a moving seam between two patterns (D245);
thin pixel strips at the left/right screen edges at non-integer window
scales (D246).
- The front-end **Nintendo logo renders as two white blobs**, and the
Rareware logo is close but its texture filtering looks off (D75).
- The F10 overlay's **bottom row duplicates whatever item is currently
selected** (e.g. the MSAA value appears both on its own row and again at
the panel bottom); earlier builds showed it as an intermittent 4K-
fullscreen ghost of the top row.
- **Steam Deck: one intermittent crash remains**: the v0.1.0-era Facility
crash is confirmed fixed on Deck hardware, but a SIGSEGV can occasionally
occur during heavy firefights (D255; seen on Bunker and Frigate). The
faulting-register capture in `ge007.crash.log` has pinned it: an object
record whose model reference is NULL while the record is still active;
i.e. something in a mid-firefight destruction leaves the record live one
tick too long. We're tracking down the exact destruction path; more Deck
playtime across levels is welcome; please report any Deck-specific faults.
- **Particle colors: a real fix landed this release, but the rainbow effect
can still occur intermittently.** One genuine, reproducible cause (an
out-of-bounds vertex-buffer write in explosion rendering) is fixed. A
second, still-unidentified cause remains, on both Windows and Steam Deck —
it doesn't reproduce every time or on every level, so treat this as
improved, not fully resolved (D252).
- **Some muzzle flashes draw an extra, erroneous long flash straight up from
the gun** (seen on the M16, among others), on top of the normal, correctly
drawn flash. Cosmetic only (D303).
- **Surface 1: the 2D billboard trees near the start render as a solid wall
of tree texture** instead of discrete sprites (D236). Under active
investigation across ten+ passes; no fix yet.
- **No native widescreen render**: v0.3.0's automatic FOV scaling (above)
expands the *field of view* for 16:9+ displays, but the world geometry and
HUD are still authored/laid out for 4:3 — this is not yet a true
edge-to-edge 16:9 render.
- **No controller rebinding UI, and no macOS or ARM builds.**
- **Distant geometry can drop out on the biggest open levels** (Streets,
Egyptian) at default FOV — a culling/LOD issue that sometimes
self-corrects as you keep moving (D249).
- **Gunshot SFX can sound off during sustained/rapid fire**: cadence can
drift from the N64 original's rate, and PP7/AK47 fire can occasionally go
silent under heavy automatic fire near another looping sound (e.g. an
alarm klaxon) (D240/D241).
- **`All unlocked` is highly experimental — do not enable it until you have
at least one save written.** Complete a level normally first (e.g. Dam on
Agent), then turn the toggle on. Enabling it on a brand-new install with
no prior save can still cause silent audio and odd right-mouse-aim
behavior (D259/D257). (The Steam Deck crash fix above addresses a
separate, now-fixed symptom that had also been associated with `All
unlocked` use; this audio/aim caveat is unrelated and still open.)
- Water on `IsWater` levels shows a moving seam between two patterns (D245).
- **Occasional z-fighting on some levels' geometry** (D308) — a Dam intro/
truck-wheel instance showing odd transparent-looking areas has also been
seen and may be related (D306). Minor, cosmetic; not investigated for
this release.
- **In-level security camera props can occasionally end up facing backwards**
(the wrong direction) on some levels, seen on Bunker — this is the
gameplay security-camera object, not the player's own view (D307). Not
investigated for this release.
- **A rare, self-clearing visual quirk**: on an occasional animation
transition, Bond's model can briefly render out of place. It has no effect
on gameplay or your save, and it clears on its own or by re-entering the
level; a safeguard added this release guarantees it can never affect game
stability (D311).
- **Two collision oddities found in final playtesting**: throwable items
(mines, grenades) can occasionally clip through walls in certain spots
(D312), and bullet impacts don't appear on some surfaces — doors and
windows are unaffected (D313). Both are queued for the next release; if
you hit either, an issue with the level and spot helps a lot.
### Downloads
@@ -136,4 +213,9 @@ sha256sum -c goldeneye-pc-port-<version>-linux-x86_64.tar.gz.sha256
[GoldenEye 007 decompilation](https://github.com/n64decomp/007), architecture
after the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark).
Non-commercial fan preservation/research project; not affiliated with any
rights holder.
rights holder. **AI disclosure:** built through agentic AI coding (Claude
Code + a local open-weight model), directed by one person in their spare
time — as much a study of what agentic development gets wrong on a
game-sized codebase as it is a port. See the README's
[Background section](https://github.com/jkdansereau/goldeneye-pc-port#background)
for the full account.
+14
View File
@@ -138,6 +138,20 @@ jobs:
python3 tools_pc/gen_findings_index.py --check
python3 tools_pc/gen_env_probes.py --check
# Docs budget guardrail (ctx-opt P2): warn, don't fail. The §F index
# table should stay one-liners; fat cell content belongs in `## Dxx`
# sections at the file tail (lookup path: docs/dev/findings-index.csv).
- name: Check dev-docs size budgets (warn only)
run: |
# Budget = ~55 KB of table rows + ~110 KB of legacy D24–D33 / M-xxx
# narrative that predates the ctx-opt migration. The signal is fat
# ROWS re-accumulating, so the threshold sits ~15 KB above today's
# 166 KB total.
f=$(awk '/^### F\./{f=1;next} /^### G\./{f=0} f' docs/dev/findings.md | wc -c)
[ "$f" -le 184320 ] || echo "WARNING: findings.md §F region is $f bytes (> 180 KB) — fat rows accumulating; migrate cell content to ## Dxx sections at the file tail"
p=$(wc -c < docs/porting-notes.md)
[ "$p" -le 122880 ] || echo "WARNING: porting-notes.md is $p bytes (> 120 KB) — consider splitting or adding a TOC block"
# US only. gen_romassets.py resolves every symbol for the US region from
# the committed CSVs with no ROM present; PAL/JP each need a handful of
# region-specific prop/font offsets that only come from that region's ROM
+12
View File
@@ -156,6 +156,14 @@ ge007.crash.log
# agent scratch space + audio probe artifacts (regenerable dumps/traces)
scratchpad/
# session logs / investigation scratch — local only, never in the public repo
# (untracked 2026-09-19 during the v0.3.0 release review)
/scratch/
# re-vendored GPL reference source (see reference/mouse-injector/README.md)
/reference/mouse-injector/*
!reference/mouse-injector/README.md
# local research/backlog working docs (TCRF cut-content cross-reference etc.)
backlog/
# montage work artifact: the tiled contact sheet behind docs/img/shots/ and
# the preview video (regenerable from the same session recordings)
docs/img/shots/CONTACT-SHEET.jpg
@@ -176,3 +184,7 @@ gdb-hang.txt
# frozen asset converter (built by CI via PyInstaller; local freezes land here)
tools_pc/dist/prepare-assets/ge007-convert*
build-pyinst/
# Local working junk (frame captures, timerun state, PPM dumps) - moved here
# from repo root / scratch on 2026-09-18; never commit.
/local/
+4 -3
View File
@@ -4,14 +4,14 @@
- `n64decomp/007`: WIP decompilation of GoldenEye 007 (N64), byte-matches US/EU/JP ROMs.
- Active work: **PC port** modelled on the Perfect Dark PC port (same Rare "Indy" engine family).
- **Reference docs:** `docs/internals.md` — architecture, GE-specific RSP deltas, phased plan (§1–§10). `docs/dev/findings.md` — the `Dxx` finding log (§F + §H, indexed at the top of §F); read the entry you need, don't linear-read. `docs/porting-notes.md` — the recurring N64→PC bug classes (dense; skim the headers, read what's relevant).
- **Reference docs:** `docs/internals.md` — architecture, GE-specific RSP deltas, phased plan (§1–§10). `docs/dev/findings.md` — the `Dxx` finding log (§F + §H). **Look up findings via `docs/dev/findings-index.csv` (label, one-liner, status; regenerate with `tools_pc/gen_findings_index.py`), then read only the specific `## Dxx` entry (multi-pass labels like D202/D176(a) have large sections — grep within the section or read with offset/limit, don't slurp it whole). Never linear-read.** `docs/porting-notes.md` — the recurring N64→PC bug classes (dense; skim the headers, read what's relevant).
- **Current status:** the README "Status" section, and `docs/dev/LEVEL-STATUS.md` for the per-level sweep. Current task + environment: `docs/HANDOFF.md` (a rolling local working file — may be absent in a fresh clone; fall back to the README "Status" section).
- **Dispatching subagents?** `docs/dev-process.md` — task budgets/deadlines, file partitioning, pre-flight, the standard brief template. Every investigation subagent reads `docs/porting-notes.md` first and appends to it.
## Non-negotiables
1. **N64 build untouched.** `Makefile`, `tools/`, `rsp/`, `ld/` belong to the N64 build. Never modify them for the PC port.
2. **Game logic is unmodified.** The decomp's control flow and behavior are ground truth for 1:1 fidelity — never change them. All N64 *hardware* dependencies are satisfied by the `port/` layer; if a game file seems to need a behavioral change, stop — the fix belongs in `port/`. **Narrow exception (ABI/layout only):** the 32→64-bit transition forces a small class of mechanical, semantics-preserving edits that cannot be isolated in `port/` — chiefly pointer-width reconciliation in ROM-serialized structs (a struct with a 32-bit-pointer field misaligns when read as 64-bit). These follow the PD ground-truth pattern (store the embedded address as `u32`, cast to a real pointer at the use site), change no logic or behavior, and are each documented in `docs/dev/findings.md` §F/D3x. No other game-code edits are permitted.
2. **Game logic is unmodified.** The decomp's control flow and behavior are ground truth for 1:1 fidelity — never change them. All N64 *hardware* dependencies are satisfied by the `port/` layer; if a game file seems to need a behavioral change, stop and check the exception below before assuming the fix belongs in `port/`. **Diagnosis is never restricted — only the fix.** Trace a bug as deep into `src/game` state as the evidence leads, and name the exact struct/field/logic responsible, before deciding which of three buckets it falls in (full framing: `docs/dev-process.md`). **Narrow exception (ABI/layout only):** the 32→64-bit pointer-width transition forces a small class of mechanical, semantics-preserving edits that cannot be isolated in `port/` — **any struct-layout or pointer-width-driven misread caused by 32→64-bit widening**, whether in a ROM-serialized record (a struct with a 32-bit-pointer field misaligns when read as 64-bit) or a **live runtime struct/union** (a raw-byte offset alias into a union arm whose true field shifted because an earlier member in the same union widened — the D209/D210/D255 pattern; see `docs/porting-notes.md` §A1 for the full catalogue and its diagnostic tells). These follow the PD ground-truth pattern (store the embedded address as `u32` and cast to a real pointer at the use site; or read the correctly-named/typed field instead of a raw-offset/mistyped alias), change no logic or behavior, and are each documented in `docs/dev/findings.md` §F/D3x, cross-tagged to §A1 where that pattern applies. **A genuine behavioral difference** — the decomp's byte-identical code, given verified-correct inputs, still diverging from real N64 behavior — is not covered by this exception and requires the rule-2 sign-off procedure (`docs/dev-process.md`) before any `src/game` edit. No other game-code edits are permitted.
3. **Region macros mirror the Makefile.** `CMakeLists.txt` `REGION_DEFS` must match the N64 Makefile's per-region macro set exactly (finding A1). Divergence = silent branch divergence + link failures.
4. **`src/libultrare/Makefile.libultrare` is ground truth** for original-vs-Rare libultra files (finding B3). The PC build compiles: `libultra/audio`, `libultrare/audio` (drvrNew/env/reverb), `libultra/gu`, and `libultrare/io/vitbl.c` only. All other `io/` + `os/` files are excluded and shimmed in `port/src/libultra.c`.
5. **`rsp/graphics/gmain.s` is the RSP ground truth** — the authoritative reference for which GBI commands GE emits (modified fast3d, 1545 lines). We do not run it on PC; `port/fast3d/` replaces it. Use it to validate the software RSP's command decoding and the custom CC/RM modes.
@@ -21,11 +21,12 @@
| File | Role |
|---|---|
| `docs/internals.md` | Architecture + RSP deltas + phased plan (§1–§10). Reference, not a linear read. |
| `docs/dev/findings.md` | The `Dxx` finding log (§F/§H, indexed at top of §F). |
| `docs/dev/findings.md` | The `Dxx` finding log (§F/§H); lookups via `docs/dev/findings-index.csv`. |
| `CMakeLists.txt` | PC build (parallel to the N64 Makefile). Source list + `REGION_DEFS` live here. |
| `port/src/` | Shims: `libultra.c` (OS API), `gesched.c` (scheduler), `n64stubs.c` (boot/TLB/FPU/rmon), `random.c` (PRNG ported verbatim from `random.s`), `ucode.c` (microcode segment markers), `main.c`, `video.c`, … |
| `port/fast3d/` | Software RSP (adapted from the PD port). The main Phase 2 work. |
| `rsp/graphics/gmain.s` | GE's RSP ucode — ground truth for GBI/CC/RM. |
| `reference/mouse-injector/README.md` | **Stub only** — the vendored GEPD-Edition Mouse Injector source (GPLv2) was removed from the public repo 2026-09-19 (license hygiene: GPL code + prebuilt binary in an MIT project; it was never compiled here). The stub records provenance + how to re-vendor locally (gitignored). The ported mouse-aim model lives in `port/src/input.c` (D194 lineage); design record: `docs/dev/GEPD-INPUT-PLAN.md`. |
| A local **Perfect Dark PC port** checkout ([fgsfdsfgs/perfect_dark](https://github.com/fgsfdsfgs/perfect_dark)) | **Standing reference** — consult it whenever a work item has a PD analogue (same Rare engine family): port-layer ground truth (`port/fast3d/`, crash/system/video), plus copy candidates `port/src/preprocess/` (N64→PC asset conversion; `filemodel.c` is the D43 near-analogue) and `mixer.c`/`input.c`/`fs.c`. Port-layer files only; same family ≠ identical format — validate per field. Full audit: `docs/internals.md` §2.4. |
## Build
+4 -3
View File
@@ -21,9 +21,10 @@ Context is scarce. Load by tier; do not blind-read whole files.
- **Tier 2 — on demand only, do NOT read start-to-finish:**
- `docs/internals.md` — architecture / RSP deltas / phased plan (§1–§10).
Read the section you need.
- `docs/dev/findings.md` — the `Dxx` finding log (§F + §H).
**Jump to a specific `Dxx` via the index at the top of §F.** Never
linear-read.
- Findings: look up via `docs/dev/findings-index.csv` (label, one-liner,
status), then read only the specific `## Dxx` entry in
`docs/dev/findings.md` (multi-pass labels like D202/D176(a) have large
sections — grep within or offset/limit). Never linear-read.
- `docs/dev/HANDOFF-ARCHIVE.md` — prior-session narrative (M-2…). Only
when tracing the history of one fix.
- `docs/dev-process.md` — before dispatching a subagent (also Tier 1
+13 -6
View File
@@ -11,12 +11,19 @@ keep the port faithful to the original game.
belong to the N64 build and are never touched for PC work. If PC code seems
to need a behavioural change in the game, the fix belongs in `port/`.
2. **Narrow ABI exception.** The 32→64-bit transition forces a small class of
mechanical, semantics-preserving edits to ROM-serialized structs (storing an
embedded 32-bit pointer as `u32` and casting at the use site). These are
allowed, must be guarded with `#ifdef PORT`, must change no behaviour, and
must be documented. See `docs/porting-notes.md` for the catalogue of
patterns.
2. **Narrow ABI exception; diagnosis is always allowed.** Trace a bug as
deep into the decomp's state as the evidence leads — that's never
restricted. Only the *fix* is constrained: the 32→64-bit transition
forces a small class of mechanical, semantics-preserving edits — any
struct-layout or pointer-width-driven misread caused by 32→64-bit
widening, in a ROM-serialized record or a live runtime struct/union alike
(see `docs/porting-notes.md` §A1 for the pattern and its tells). These
are allowed, must be guarded with `#ifdef PORT`, must change no
behaviour, and must be documented. A genuine behavioral difference from
real N64 behavior is a separate, much rarer case that needs explicit
maintainer sign-off first — see `docs/dev-process.md`'s rule-2 sign-off
procedure; it is not something a contributor can self-approve or infer
from this exception.
3. **Region macros mirror the Makefile.** `CMakeLists.txt` `REGION_DEFS` must
match the N64 `Makefile`'s per-region macro set exactly.
+126 -47
View File
@@ -12,15 +12,25 @@ in a dedicated `port/` layer, following the architecture of the
[Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark), the same
Rare "Indy" engine family, one hardware generation apart.
**v0.2.0** is out for Windows and Linux (including Steam Deck, where the
**v0.3.0** is out for Windows and Linux (including Steam Deck, where the
Linux bundle sideloads as-is) and runs the full campaign at a steady 60 fps
with known rough edges ([Status](#status)). Free to download, build on and
modify (you bring the ROM).
It's also a case study in AI-agent collaboration on a large, low-level
codebase: two coding agents, driven by one person part-time, porting ~230
translation units of unmodified big-endian MIPS game code to a 64-bit desktop.
See [Background](#background).
**This is a pre-1.0 release, not a finished product.** v1.0 is the target
for a polished, feature-complete build; until then, expect rough edges,
missing features, and breaking changes between versions. See
[Status](#status) for what works today and [Roadmap](#roadmap) for where
this is headed.
**AI disclosure:** development here was agentic - Claude Pro plus a local
open-weight model on a single RTX 5090, as of August–September 2026. This
project is as much a study of *that process* as it is a port: whether
current LLMs can carry a codebase like this, and what actually goes wrong
along the way. Judge the result for yourself.
I'm one person doing this in my spare time, not a team. See
[Background](#background) for the full setup, timeline, and an honest
account of what worked and what didn't.
> [!IMPORTANT]
> **You must supply your own GoldenEye 007 ROM.** This repository contains no
@@ -29,92 +39,134 @@ See [Background](#background).
<p align="center">
<img src="docs/media/goldeneye-gh-preview.gif" width="64%"
alt="~32 s gameplay montage from live play sessions">
<br><em>All in-engine, running in the port, a ~32&nbsp;s gameplay montage loop
from the v0.2.0 playtest.</em>
alt="~12 s gameplay montage from live play sessions">
<br><em>All in-engine, running in the port — a ~12&nbsp;s gameplay montage
from live v0.3.0 play sessions.</em>
</p>
## Download
| Platform | Bundle | Notes |
|---|---|---|
| **Windows** (x86_64) | [win64.zip](/jkdansereau/goldeneye-pc-port/releases) | Engine + runtime DLLs + the one-time asset tool. |
| **Linux** (x86_64) / **Steam Deck** | [linux tarball](/jkdansereau/goldeneye-pc-port/releases) | SDL2 is bundled, so it runs as-is on any distro, and sideloads onto a Deck with nothing installed. |
| **Windows** (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. |
Both bundles contain **no ROM and no game assets**: you supply your own
(see [Requirements](#requirements)), which keeps the release legal to
distribute. Earlier builds: v0.1.0 alpha, same page. You can also build it
yourself; see [Building](#building).
distribute. Earlier builds: v0.2.2, v0.2.1, v0.2.0, and v0.1.0 alpha, same
page. You can also build it yourself; see [Building](#building).
### Quick start
You need a GoldenEye 007 N64 ROM (`.z64`, big-endian). This release supports
the **NTSC-U (US)** version; PAL and JP are on the roadmap ([issue
#85](/jkdansereau/goldeneye-pc-port/issues/85)). No ROM or game asset is
#85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)). No ROM or game asset is
included or distributed. Then:
1. Download the Windows or Linux bundle from [Releases](/jkdansereau/goldeneye-pc-port/releases) and unpack it.
1. Download the Windows or Linux bundle from [Releases](https://github.com/jkdansereau/goldeneye-pc-port/releases) and unpack it.
2. Make a `data/` folder next to the executable and drop the ROM in as `ge007.ntsc-final.z64`.
3. Launch the executable from that folder. The first run takes a few extra seconds: it detects the ROM and generates the derived asset folders once (no Python or other tooling needed).
Read the [Status](#status) caveats first: v0.2.0 has known rough edges,
Read the [Status](#status) caveats first: v0.3.0 has known rough edges,
listed plainly there.
## Status
**v0.2.0 - playable, with known rough edges.** The full single-player
**v0.3.0 - playable, with known rough edges.** The full single-player
campaign is completable end to end (all 21 missions, Agent difficulty,
playtested), at a steady 60 fps; all 21 solo missions load, render and run
crash-free, verified on Windows, Linux and real Steam Deck hardware. Feedback
is very welcome.
What a release actually installs (no networking, no telemetry, no ROM or
game assets shipped) and how faithfully the port tracks the original N64
game's logic: [Security & fidelity status](docs/security-and-fidelity-status.md).
**Working:** boot sequence and front end (menu → mission select → briefing →
start); all 21 solo missions load, render and are crash-free (full campaign
start), front-end menu navigation on the left stick to match the F10 overlay
(D282); all 21 solo missions load, render and are crash-free (full campaign
playtested end to end at Agent difficulty); steady 60 fps
(software RSP off the presentation critical path); full audio: in-level music and SFX; keyboard + mouse (click-to-lock, proportional aim mode) and a modern
dual-stick controller layout; file-backed saves; faithful N64 progression by default
(F10 → *All unlocked* opens every level, 007 mode and the full cheat menu); F10 in-game
options overlay (resolution, frame cap, MSAA, filtering, FOV, sensitivity);
Windows and Linux.
(software RSP off the presentation critical path); full audio: in-level music and SFX; keyboard + mouse (click-to-lock, proportional aim mode, a single
simplified sensitivity control) and a modern dual-stick controller layout;
automatic widescreen FOV scaling; Bond is fixed in cutscenes (no more
floating or spin-glitching) and his third-person model positioning generally
is right the large majority of the time now, at most a small drift when off; file-backed saves; faithful N64 progression
by default (F10 → *All unlocked* opens every level, 007 mode and the full
cheat menu); F10 in-game options overlay (resolution, frame cap, MSAA,
filtering, FOV, sensitivity); Windows and Linux, including Steam Deck.
**Known issues:**
- **Cutscenes still glitch, mostly with James Bond**: in scripted sequences
Bond is the one who gets misplaced, hovers, or spins; the other actors are
fine for the most part now. The Dam level-end cutscene is racy (D243).
Still the most visible gap in this release.
- Particle colours drift through a rainbow palette instead of holding their
grey/orange intent (bullet sparks, lingering smoke/explosion residue) (D252).
- Water levels show a moving seam between two water patterns (D245); thin
pixel strips at the left/right screen edges at non-integer window scales
(D246).
- Particle colours: a real fix landed (a corrupted-vertex-buffer bug), but
the rainbow effect can still occur intermittently on either platform, not
every time or on every level — a second cause hasn't been found yet
(D252).
- Some muzzle flashes draw an extra, erroneous long flash straight up from
the gun (seen on the M16, among others), overlaid on the normal, correctly
drawn flash. Cosmetic only (D303).
- Water levels show a moving seam between two water patterns (D245).
- Occasional z-fighting on some levels' geometry (D308) — a Dam intro/
truck-wheel instance showing odd transparent-looking areas may be related
(D306). Minor and cosmetic.
- In-level security camera props (not the player's own view) can
occasionally end up facing backwards on some levels, seen on Bunker
(D307).
- Rarely, Bond's model can briefly render out of place after an animation
transition — purely visual: no effect on gameplay or saves, and it clears
on its own or by re-entering the level (D311).
- On Facility, if gas leaks during Ourumov's monologue he can pause for up to
~10 s before resuming the scripted shootout — a latent race that exists in
the N64 original too (where it softlocks permanently); the port detects and
auto-recovers it (D318).
- Some front-end 3D models are off: the spinning Nintendo logo renders as two
white blobs and the Rareware logo's texture filtering looks wrong (D75).
- The F10 overlay's bottom row duplicates whatever item is currently selected
(D251).
- **Steam Deck:** one intermittent SIGSEGV remains in heavy firefights (D255);
current builds capture full faulting registers in `ge007.crash.log`.
- Surface 1's 2D billboard trees render as a solid wall of tree texture
instead of discrete sprites (D236). Under active investigation.
- **Widescreen is stretched, not native**: on non-4:3 windows the whole
frame (world and HUD) is stretched horizontally to fill your display —
about 33% wider than original at 16:9, like a 4:3 video in a TV's "stretch"
mode. Automatic FOV scaling keeps the framing comfortable and gameplay is
completely unaffected (all game logic runs in world space), but shapes are
subtly wider than on the N64 — most visible on round objects (barrels, the
radar). A distortion-free native widescreen render is on the roadmap.
- Distant geometry can drop out on the biggest open levels (Streets,
Egyptian) at default FOV — a culling/LOD issue that sometimes
self-corrects as you keep moving (D249).
- Gunshot SFX can sound off during sustained/rapid fire: cadence can drift
from the N64 original's rate, and PP7/AK47 fire can occasionally go silent
under heavy automatic fire near another looping sound (D240/D241).
- **`All unlocked` is highly experimental — don't enable it until you have
at least one save written** (complete a level normally first, e.g. Dam on
Agent). Enabling it on a brand-new install with no prior save can still
cause silent audio and odd right-mouse-aim behavior (D257/D259/D281).
- Assorted further cosmetic defects are tracked in
[`docs/dev/GRAPHICS-BACKLOG.md`](docs/dev/GRAPHICS-BACKLOG.md).
- No macOS or ARM support; no controller rebinding UI.
Root causes and fix status for every item: the [release notes](/jkdansereau/goldeneye-pc-port/releases)
Root causes and fix status for every item: the [release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases)
and the finding log in [`docs/dev/findings.md`](docs/dev/findings.md).
### Steam Deck
The Linux bundle is the Deck build. SFTP it over from your PC, or download
it straight from the [releases page](/jkdansereau/goldeneye-pc-port/releases) on the Deck itself:
it straight from the [releases page](https://github.com/jkdansereau/goldeneye-pc-port/releases) on the Deck itself:
unzip, drop your ROM in `data/`, launch it once (the first run generates the
derived assets), and add the executable as a non-Steam game. SDL2 is bundled, so no dependencies need
installing. On SteamOS the first launch seeds `ge007.ini` with Deck-friendly
defaults: native 1280×800 fullscreen, VSync, MSAA 4, and 150% draw/LOD
distance (the authored N64 fade distances read short on the close-up panel);
everything is changeable in the options overlay and persists afterwards. The renderer is CPU-bound (software RSP); expect original N64-era
everything is changeable in the options overlay and persists afterwards.
**Do that first launch in Game Mode, not Desktop Mode** — an ini created by
an earlier Desktop Mode launch (e.g. while testing before adding it as a
Steam shortcut) permanently skips the Deck preset, since any existing ini
always wins over it (D283). If your resolution isn't 1280×800 on first
Game Mode boot, just set it manually: F10 → *Resolution*. The renderer is
CPU-bound (software RSP); expect original N64-era
performance at 60 fps rather than more. This release was playtested on real
Deck hardware; the v0.1.0-era Facility crash (D203) did not recur: its root
cause was identified and fixed (D253), verified on the Deck; one intermittent
SIGSEGV in heavy firefights remains open (D255).
cause was identified and fixed (D253), and a separate intermittent SIGSEGV
in heavy firefights/terminal destruction (D255) and an audio-thread crash
(D305) are both fixed and live-verified on real hardware as of this release.
**In-game settings on the Deck.** The options overlay is fully gamepad-driven:
it opens with **Select**, the D-pad or left stick (up/down) moves between
@@ -123,6 +175,28 @@ options, **A** steps the selected option forward, **B** steps it back, and
filtering cycle, sliders step in increments. With a keyboard attached the same
overlay is `F10` + arrows/Enter.
## Roadmap
No fixed timeline or committed feature list — this is spare-time work — but
directionally, on the way to v1.0:
- Working through the [known issues](#status) above and the fuller list in
[`docs/dev/findings.md`](docs/dev/findings.md).
- **PAL and JP ROM support** ([issue #85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)); NTSC-U is the only supported region today.
- **Native widescreen** — a distortion-free render at your display's aspect
(correct-aspect projection and a wide-frame HUD), replacing today's
stretched 4:3 frame + FOV compensation.
- **Controller rebinding UI**, and macOS/ARM builds.
- **LAN multiplayer**: reviving GoldenEye's original split-screen/deathmatch
netplay across multiple PCs on a local network. Genuinely under
consideration, but early and not started; no ETA.
- General polish: performance, remaining rendering/audio defects, save/config
robustness.
Not currently planned: new game modes GE never shipped (e.g. co-op), online (non-LAN)
multiplayer, ray tracing. If any of these matter to you, open an issue —
it helps prioritize.
## Beyond playing
- **Tweak it**: `ge007.ini` and the F10 in-game overlay expose resolution,
@@ -170,7 +244,7 @@ code with this one.
| **How** | Decompilation-based source port: human-reconstructed C, compiled for the host; game logic runs as written | Static binary recompilation: the shipped machine code is auto-translated to C; no source-level understanding |
| **Lineage** | [GoldenEye 007 decompilation](https://github.com/n64decomp/007) + [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark) engine family | Xbox 360 "…Recompiled" static-recompilation family |
| **Renderer** | Software RSP → OpenGL | Hardware (Vulkan) |
| **Status** | v0.2.0 public release; full campaign playable at 60 fps (see [Status](#status)) | Playable full game with online multiplayer |
| **Status** | v0.3.0 public release; full campaign playable at 60 fps (see [Status](#status)) | Playable full game |
| **Why it exists** | To run the *original* N64 game from source, and as a [case study in AI-agent collaboration](#background) on a hard low-level codebase | To get a playable PC release of the remaster |
They answer a different question: how to get the *remaster* onto PC by
@@ -190,7 +264,7 @@ big-endian (`.z64`) format, matching one of:
This release supports the US (NTSC-U) version; PAL and JP ROMs are
recognised by region but not yet supported (on the roadmap, [issue
#85](/jkdansereau/goldeneye-pc-port/issues/85)).
#85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)).
The port also relies on the decompilation's asset-extraction step, which pulls
the level, model, texture and music data out of your ROM at build time. That
@@ -357,13 +431,18 @@ This port is a thin layer on a large amount of other people's work.
Kids decompilation write-up), the agent-workflow practices in
[`docs/dev-process.md`](docs/dev-process.md) are adapted from it.
**Consultation**
- **f1zz1ec0ke** ([GitHub](https://github.com/f1zz1ec0ke)): LLM consultation on model
tuning, agent harnesses, and agentic strategy throughout the port's development.
**Tools and models used to develop the port**
- Qwen 3.8 (Alibaba Qwen team), run locally as the
- Qwen 3.8 (Alibaba Qwen team), run locally from the
[`unsloth/Qwen3.8-27B-GGUF`](https://huggingface.co/unsloth/Qwen3.8-27B-GGUF)
`UD-Q4_K_XL` quant;
[Unsloth](https://unsloth.ai) (the GGUF quantisation and Unsloth Desktop,
used as the local model server).
repo — `UD-Q4_K_XL` as the primary quant, with other quants used along the
way; [Unsloth](https://unsloth.ai) (the GGUF quantisation and Unsloth
Desktop) as the local model server.
- [pi](https://pi.dev/): the local coding-agent harness.
- [Claude / Claude Code](https://claude.com/claude-code) (Anthropic).
@@ -382,7 +461,7 @@ GitHub. It follows the same conventions they do:
- No official logos, box art, or marketing assets are used. "GoldenEye 007",
"007", "James Bond" and related marks belong to their respective owners
(Nintendo, Microsoft/Rare, MGM, Danjaq, EON Productions).
- Pre-built binaries published under [Releases](/jkdansereau/goldeneye-pc-port/releases) contain
- Pre-built binaries published under [Releases](https://github.com/jkdansereau/goldeneye-pc-port/releases) contain
only the engine (the `port/` layer plus the compiled decompilation, with no
game data of any kind), bundled with permissively-licensed runtime libraries
(SDL2, zlib, the MinGW runtime; their licenses travel in the download). Any
+6 -4
View File
@@ -1,10 +1,12 @@
# GitHub Pages config. Site is served from the /docs folder on the default
# branch (Settings -> Pages -> Deploy from a branch -> main / docs).
title: GoldenEye 007 PC Port
description: >-
A native PC port of the original Nintendo 64 GoldenEye 007, built from its
decompiled source with a software RSP — plus the engineering record:
internals, porting notes, and the two-agent development workflow.
# v0.2.1 (Bing SEO): keep this SHORT -- jekyll-seo-tag splices it into the
# home page <title> whenever the page title equals the site title, and the
# long version made a ~230-char title. It also renders as the masthead
# tagline on every page. Per-page meta/og descriptions come from each page's
# front matter `description:` (see index.md), not from here.
description: N64 GoldenEye 007, decompiled for PC.
theme: jekyll-theme-cayman
show_downloads: false
+1 -1
View File
@@ -3,7 +3,7 @@ title: Building
description: Full build and asset-extraction guide for the GoldenEye 007 PC port, covering Windows (MSYS2) and Linux.
---
# Building the PC port
## Building the PC port
Stages:
+124 -2
View File
@@ -3,7 +3,7 @@ title: Development Process
description: The investigation workflow behind the port; task budgets, file partitioning, and the finding-log discipline used across sessions and agents.
---
# How this port is developed
## How this port is developed
Most of this port's work is not writing code; it is **diagnosis**: figuring
out which N64 hardware or ABI behavior a piece of unmodified game code is
@@ -22,6 +22,45 @@ faster than its predecessor. That project bit-matches compiler output;
this one matches runtime behavior, so the mechanics differ; but the
project-structure lessons carry over directly.
### Diagnosis is unrestricted; only the fix is constrained
AGENTS.md non-negotiable #2 restricts *changing* `src/game` behavior. It
has never restricted *investigating* it. An agent that stops short of
naming the exact `src/game` struct, field, or logic responsible for a bug
— because touching that file "feels" off-limits — is doing worse
diagnosis than the rules require, not better compliance with them.
Several of this port's hardest, highest-impact bugs (D209, D210, D255)
were exactly this shape: a `src/game` state variable reading wrong,
root-caused only by tracing fully into the game file that owned it.
Every investigation should end able to say which of three buckets
applies:
- **(a) ABI/layout-class.** The true cause is a struct-layout or
pointer-width misread introduced by the 32→64-bit transition — in a
ROM-serialized record or a live runtime struct/union alike. Fixable
under the narrow exception, entirely inside the game file under
`#ifdef PORT`, no sign-off needed. Check `docs/porting-notes.md` §A1
first (the pattern's tells + worked examples) — this is the most
common shape a "`src/game` behavior looks wrong" report turns out to
have.
- **(b) Port-layer bug that merely manifests through game state.** The
game code is reading state that's correct for what N64 would have
produced; something in `port/` (a shim, fast3d, frame timing, an
uninitialized or mis-ordered port-owned value) is what actually
differs. Fixable entirely in `port/`, no `src/game` touch, no sign-off
needed.
- **(c) Genuine game-logic behavior difference.** The decomp's
byte-identical control flow, given verified-correct inputs, provably
diverges from real N64 behavior. Rare, and requires the rule-2
sign-off procedure (§7 below) before any `src/game` edit.
Bucket (c) is rare by design — most bugs that look like (c) turn out to
be (a) or (b) in disguise — but ruling it in or out requires actually
tracing the state to its source, not stopping at the file boundary.
"This state lives in `src/game`" is a reason to keep tracing, not a
reason to stop.
## 1. Every investigation task gets a visible budget
An open-ended tool; for them the permuter, for us "rebuild and run the game
@@ -63,6 +102,18 @@ The classes it currently tracks:
- whole serialized formats are converted by an **offline sidecar** rather than
patched at runtime.
**Two process lessons from the sibling GEVR VR port** (same decomp lineage,
reference-only survey in `scratchpad/GEVR-TRIAGE.md` item D — their corpus
hit both repeatedly enough to name them): (1) **a grep standing in for a
read** — a site list built by grepping one helper name or spelling misses
siblings spelled differently or living in an adjacent file (their own
missed-smoke-scissor-site example); treat a grep-built site list as a draft,
not a census, until it's been read against. (2) **write the falsifier before
the run** — state up front what result would kill the hypothesis ("if X
survives, hypothesis Y is dead"), not just what would confirm it; this fits
the existing budget/findings discipline directly and costs one sentence per
brief.
## 3. Parallelise by file, consolidate often
Lewis ran multiple git worktrees and found the real cost was
@@ -135,6 +186,68 @@ justifies. Rules:
is still true as a fresh, small PR against current `main`. Merging stale
content back over newer information is a regression, not a save.
## 7. The rule-2 sign-off procedure (genuine `src/game` behavior changes)
Use this only for bucket (c) from the diagnosis framing above — never for
the ABI/layout exception (§A1-class fixes need no sign-off, only
documentation) and never for anything expressible in `port/`.
**To request sign-off, the write-up must state:**
1. The exact `src/game` file:line and the specific behavior being
proposed for change.
2. **Proof of divergence.** Given verified-correct inputs — i.e. every
upstream ABI/layout suspect (§A1) and every port-layer suspect has
been checked and ruled out, with the ruling-out cited — the
byte-identical decomp code still produces behavior that differs from
real N64 reference behavior. Name the reference (a capture, documented
prior N64 behavior, a cited external source). "It looks wrong" is not
proof; "it disagrees with `<reference>`, verified by `<method>`" is.
3. Why the fix cannot be expressed in `port/`, and why it does not
qualify under the ABI/layout exception — the two questions that must
be answered "no" before this procedure applies at all.
4. The proposed fix, scoped as tightly as possible to the one named
behavior, plus a same-engine precedent if one exists — the Perfect
Dark port is the standing reference; cite the analogous PD change the
way `docs/dev/WIDESCREEN-FOV-PLAN.md`'s "Rule #2 exception framing"
section did.
**Granting.** The user reviews the above and replies with an explicit
go-ahead in-thread. There is no implicit or inferred sign-off: silence,
"seems reasonable," a prior similar grant, or approval of a *different*
behavior in the same file do not carry over to a new request.
**Recording.** Every sign-off gets its own `docs/dev/findings.md` entry
(next `Dxx` label), tagged **`RULE-2-SIGNOFF`** in its status line
alongside the normal finding classification, and recording: the request
as stated above, the grant (quoted or summarized, with date), and the
resulting diff. Add the label to `findings-index.csv` like any other row,
so `tools_pc/gen_findings_index.py` keeps it discoverable — chat history
is not the record of approval; the finding entry is.
**Constraints that still apply after approval:**
- Still `#ifdef PORT` — the N64 build is untouched, and the `#else` arm
(or the file's un-PORT-guarded default) still matches the original
decomp exactly.
- Still scoped to the one named behavior — a grant is not a standing
license; a different behavior difference in the same file needs its own
request, even if related.
- Still fully documented in the `RULE-2-SIGNOFF` finding entry — that
entry is the permanent record of *why* this specific exception to
non-negotiable #2 exists.
- If the fix would visibly change behavior at *default*
settings/difficulty/region beyond the one bug being fixed, treat that
as a signal the scope drifted mid-implementation — stop and go back for
a fresh sign-off on the wider scope rather than shipping it under the
narrower original grant.
This generalizes the ad hoc precedent in
`docs/dev/WIDESCREEN-FOV-PLAN.md`'s "Rule #2 exception framing" section (a
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.
## Investigation-brief template
```
@@ -144,7 +257,16 @@ 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>.
BUDGET: <N build->run cycles / ~M min>. On expiry: revert probes, write up with confidence.
CONSTRAINTS: no game-logic changes; ABI/layout/format only; #ifdef PORT; documented in the finding log.
CONSTRAINTS: diagnosis may trace as deep into src/game state as the evidence
needs — name the exact struct/field/logic responsible before concluding
which bucket applies (see "Diagnosis is unrestricted" above). The FIX is
still constrained: no src/game behavior changes unless (a) it's an
ABI/layout-class struct-layout or pointer-width misread — check
docs/porting-notes.md §A1 first; #ifdef PORT; documented in the finding
log under §F/D3x — or (b) it's fixable entirely in port/ (no src/game
touch). A genuine game-logic behavior difference (bucket c) is NOT
authorized by this brief — stop and write up a rule-2 sign-off request
per docs/dev-process.md §7 instead of applying it.
VERIFY: <the exact tools_pc/verify.sh (or probe) invocation that proves this done; or, if runtime verification is impossible in this environment, say so and name what a human must run>.
REPORT: (a) root cause + file:line evidence (b) fix diff, or why not
(c) probes left in tree (d) confidence.
+77
View File
@@ -0,0 +1,77 @@
# Accuracy / code review — N64 fidelity sweep (2026-09-20)
A thorough pass over the PC port with one question: *where does the port diverge from the
N64 original, and what latent failure classes of the D318 kind are still unpriced?*
Method: static review against the decomp ground truth + targeted verification builds.
Findings below; new finding **D320** logged in `findings.md`.
## 1. PRNG — verified bit-exact
`port/src/random.c` re-checked line-by-line against both `src/random.s` and
`src/game/chrObjRandom.s`. The MIPS64 `dsll32`/`dsrl32` semantics (post-D284) are
emulated correctly: the 32-bit LFSR state advances identically, and
`randomGetNext()` returns the same 31-bit values in the same order. No divergence.
## 2. Known bug-class sweeps — all clean or already fixed
| Class | Result |
|---|---|
| **A1** raw-byte/padding aliases (32→64-bit layout shift) | Remaining `padding[]` uses are in pointer-free structs (layout-stable) or already fixed (D115, D209, D312). No live misreads. |
| Shifts by 32 / pointer-width arithmetic in game logic | Only port-layer code (`d318watchdog` print, `DRAM_SIZE`). Game logic clean. |
| Signed-overflow UB | `-fwrapv` enabled in `CMakeLists.txt` — the whole class is neutralized to N64-wrap semantics. |
| `#ifdef PORT` hygiene (N64 path preserved) | 307 hunks audited; 178 are pure additions (diagnostics / struct extensions). No hunk alters the non-PORT control flow. |
| **§B** Gfx/Vtx 16-byte records (raw byte indexing) | `bg.c` uses typed `.words`/`.dma` fields throughout; no remaining raw byte offsets. D312's fix pattern is the last of this class. |
| `osVirtualToPhysical` call sites (88) | All flow through fast3d's `seg_addr()` (D131 fix intact). No site assumes N64 segment RAM layout. |
| Dead code | `objecthandler_2.c` compiles but is not linked on PC (symbols absent from the binary). Harmless; noted for a future source-list prune. |
## 3. Timing layer — intentional, documented
Frame-locked virtual clock (vsync tick), `GE_DETERM` fixed-tick mode, and
`g_GlobalTimerDelta` zero-guarded at every division site. All deliberate port-layer
design, each documented in its finding. No hidden drift source found beyond the
already-fixed D204 tempo work.
## 4. D318-class latent softlock sweep → **D320** (the main new result)
D318's mechanism is generic, not Facility-specific: `actor_fire_or_aim_at_target_update`
(`chraction.c:4882`) retargets any ACT_ATTACK chr with an AIM_ONLY/DONTTURN attacktype and
only moves the model's **endframe** — it never re-inits the animation. If the chr is pinned
on a pre-shoot aim-hold pose (hold endframe H < variant shoot window) and the new endframe
lands above H, the model pins forever and the timer-gated fire events never open; if the
surrounding AI script's only escape is an `if_guard_has_stopped_moving` gate, it can never
pass (`chrHasStoppedOrPatroling` is FALSE for ACT_ATTACK). **Permanent softlock — on N64 too.**
Swept every level's AI scripts (`assets/obseg/setup/*.c`): 17 lists in 8 levels contain an
aim-hold + `_update` pair. Risk-ranked table in the D320 entry. Headlines:
- **Facility ai_19** — HIGH. Same non-self-healing shape as ai_22 (second softlock point in
the same level).
- **Control ai_9** — HIGH. Near-exact ai_22 mirror (pre-aim hold → timer → update → fire
loop gated on the target dying from this guard's shot).
- **Depot ai_12** — MED-HIGH. The kill is scripted, but the objective bit is gated behind a
stop-check; pinned guard → level-complete softlock.
- **Bond-combat loops** (Aztec/Control/Egyptian/Depot/Streets/Bunker/Surface) — likely safe:
sustained fire keeps the model inside the shoot window by design, and every playtest
exercises them without freezes. Inferred, not probe-verified per list.
Classification: **as-authored latent race(s), faithfully reproduced** — same bucket as D318,
not a port bug. Verification path (generalized timeline probe) and the
generalize-the-watchdog recommendation are in D320.
## 5. Fixed this session
- `port/fast3d/gfx_pc.cpp:2417` — the GE_D116 diagnostic printf used `%f` for `uint32_t`
args (`v->u`, `tex_width`) and `%d` for unsigned tile fields; wrong debug output only, no
runtime impact. Casts added; build now warning-free on that file.
## 6. Verification ritual
Full `./build-pc.sh ntsc-final` configure + compile + link: **PASS**, no warnings on
touched files. All new/changed symbols resolve exactly once (link is the sweep). D318
watchdog/timeline symbols confirmed present in the binary.
## 7. Deliberately not committed (release-prep WIP, separate workstream)
`.github/release-notes.md`, `README.md`, `docs/index.md`, `docs/media/goldeneye-gh-preview.gif`,
`docs/dev/agentic-development.md`, `.gitignore` (`/local/` exclude), and non-D318 scratch
artifacts — left in the working tree for their owner.
+317
View File
@@ -0,0 +1,317 @@
# CUT-CONTENT-BACKLOG — TCRF "cutting room floor" findings vs decomp/port
Research branch: `research/cut-content-backlog`. Source of truth: the local TCRF
export at `scratchpad/backlog-scrape/restore-cut-content-mod/` (gitignored).
> **Superseded by the verified cross-reference.** All six article *bodies* were
> recovered and every major claim was checked against the decomp source and asset
> tables. The full verified findings (with file:line references, corrections to
> TCRF, and updated mod tiers) live in `backlog/cut-content/RESEARCH.md`
> (gitignored; article markdowns in `backlog/cut-content/articles/`). This doc is
> the first pass (image-filename based) — read it for the per-item imagery index,
> but trust RESEARCH.md where they disagree. Key corrections: (1) "dest" is the
> *shipped* Frigate geometry, not an early version (`bg.c` levelinfotable);
> `cryp`/`dish` are shipped Egypt/Temple; (2) Library/Stack share Basement's bg
> file — they have geometry; (3) all 29 "unused items" exist in the ROM object
> table as pickup+prop pairs except heroine; (4) TCRF's 11 unused cheats, music
> track index, MP character table and build dates all verify 1:1 against
> `bondconstants.h`/`cheat.c`/`front.c`/`compiletime.c`.
## 1. Findings
### A. Debug menu & dev tools — fully in the ROM, fully compiled into the PC build
TCRF main page images: `GEDebugMenu`, `GECheatOptions`, `GoldenEye-displayspeed`,
`GoldenEye-moveview`, `GoldenEye-LineMode`, `GoldenEye-testingmanpos`,
`GoldenEye-gunwatchpos`.
All of these exist in decompiled code:
- **Debug menu** — `src/game/debugmenu_handler.c` (1202 lines). Opened by holding
**U + D C-buttons** (`debug_menu_processor`, ~line 451; wired into `boss.c:551`
and the main game loop). Full option list at lines 116–185: `move view`,
`stan view`, `bond view`, `level` / `region` / `scale` (level switching),
`play title`, `bond die`, `select anim`, `gun pos`, `flash colour`,
`hit colour`, `music`, `sfx`, `invincible`, `visible`, `collisions`,
`all guns`, `max ammo`, `display speed`, `background`, `props`,
`stan hit` / `stan region` / `stan problems`, `print man pos`,
`port close` / `port inf` / `port approx`, `pr room loads`,
`show mem use` / `show mem bars`, `grab rgb` / `grab jpeg` / `grab task`
(screenshots), `rnd walk`, **`record ramrom` / `replay ramrom` /
`save ramrom` / `load ramrom`** (input recording/replay!), `auto y aim`,
`auto x aim`, `agent` / `all` / `fast` (unlock flags), `objectives`,
`marg top/bot/left/right/reset`, `screen size`, `screen pos`,
`show patrols`, `intro` / `intro edit` / `intro pos`, `world pos`,
`gun key pos`, `vis cvg`.
- **Free camera** — `src/game/debug_camera.c` (the "move view" option).
- **Load-all-models tool** — `src/game/deb_loadallmodels.c` (used by
`initguards.c`; dumps every model — how the TCRF screenshots of unused
models were likely made).
- **Line mode** — `CHEAT_LINEMODE` in `src/game/cheat.c:685` (wireframe toggle,
cheat-menu unlockable; `front.c:1017`).
- Screenshot options (`grab rgb`/`grab jpeg`) call `indyGrabRgb32bit()` /
`indyGrabJpg32bit()` — N64 VI grabs; on PC these are the natural hook for the
port's existing screenshot QoL (see `feat/qol-mute-screenshot` lineage).
**Status: Tier 0.** Everything compiles into the PC build (`CMakeLists.txt`
`GLOB_RECURSE src/game/*.c`). The debug menu should already work in the PC port
if U+D C-buttons can be produced by `port/src/input.c`. *Verify in-game* (open
menu, toggle `display speed`, use free cam). Mod value: document it; consider a
config/keybind that synthesizes the U+D c-button hold, and mapping `grab rgb`
to the port's screenshot path.
### B. Cut weapons — fully functional in the ROM, grantable via existing cheat codes
TCRF images (main + prerelease pages): `GoldenEye-Taser[PR]`, `GoldenEye-KF7[PR]`
(+`KF7Watch`), `GoldenEye-GrenadeLauncher[PR]`, `GoldenEye-RocketLauncher[PR]`
(+`RocketLauncherWatch`), `GoldenEye-RifleLauncher` (+`RifleLauncherCheat`),
`GE64_Shotgun1`, watch variants `MagnumWatchPR`, `PP7WatchPR`, `KlobbWatchPR`,
`TaserWatch`.
Evidence in code/assets:
- Model + stat files exist for every cut gun under `assets/obseg/gun/`:
`taser/`, `rocketlaunch/`, `grenadelaunch/`, `fnp90/`, `spectre/`,
`autoshot/`, `ak47/`, `m16/`, `skorpion/` … each with
`gunWeaponStat.inc.c` + `ModelFileHeader.inc.c`.
- `assets/obseg/gun/gunWeaponStats.inc.c`: complete `WeaponStats` for
`taser_stats` (line ~365), `rocketlaunch_stats` (~305),
`grenadelaunch_stats` (~295), plus `watchlaser_stats`, `tank_stats`,
`bombcase_stats`, etc. These are data-driven — the weapon engine
(`src/game/gun.c`, `gunfire.c`) will run them.
- First-person view cases exist: `src/game/bondview_r.c:38` handles
`ITEM_GRENADELAUNCH`, `:41` `ITEM_ROCKETLAUNCH`.
- **Cheat codes grant them**: `src/game/cheat.c` — `CHEAT_2X_ROCKET_LAUNCHER`
(line 711), `CHEAT_2X_GRENADE_LAUNCHER` (712), `CHEAT_2X_RCP90` (713),
plus unlock sequences (`CHEAT_UNLOCK_2XGL/2XRL/2XFNP0`, lines ~695–734) and
the grant logic at lines 1277–1310: `bondinvAddInvItem(ITEM_ROCKETLAUNCH)` /
`give_cur_player_ammo(AMMO_ROCKETS, …)` etc. Note the `#if defined(BUGFIX_R1)`
guards — check which region macros the PC build defines (non-negotiable #3).
- Taser SFX exist: `GUN_TASER_SFX` / `GUN_TASER_LOOP_SFX` (`src/bondconstants.h:2271`).
- `ITEM_TASER` is in the item enum and used as an inventory bound in
`bondinv.c:854` (JP-text-trigger branch) — the taser was closest to shipping.
**Status: Tier 0/1.** Rocket launcher, grenade launcher and FN P90 are
playable today via the game's own cheat codes (single-player). Taser/Spectre/
AutoShot have no cheat path but everything needed is in the binary; a
port-layer toggle calling the same `bondinvAddInvItem` +
`give_cur_player_ammo` pair the cheats use would be a clean mod (no game-logic
change — it replicates an existing code path). Watch-mounted variants
(KF7/Magnum/PP7/Taser watch) are *not* in the item enum — likely cut before
data landed; **unverified** whether any model exists beyond what TCRF shows.
### C. "Unused items" page (intact imagery) — objects with full logic, never placed
The 20 intact images correspond exactly to ROM object entries
(`assets/obseg/file_resource_table.inc.c`, 781 entries):
| Image | Resource ID | Notes |
|---|---|---|
| `GE007-AudioTape` | `AUDIOTAPE` / `CHRAUDIOTAPE` (lines 125, 265) | pickup + prop variants |
| `GE007-BombCase` | `BOMBCASE` (129) | has full `bombcase_stats` (throwable explosive) |
| `GE007-Controller` | N64 controller model (`joypad/` dir in gun table) | desk prop |
| `GE007-WatchMagnetRepel` | `ITEM_WATCHMAGNETREPEL` (`bondconstants.h:4190`) | watch function, use-logic present |
| `GEMicroCamera` | `MICROCAMERA` (176) + `AMMO_MICRO_CAMERA` | |
| `GoldenEye-Weapon_Case` | `ITEM_WEAPONCASE` (`gunfire.c:3066` use-case) | |
| `GE007-PolarizedGlasses` | `POLARIZEDGLASSES` (185) + `ITEM_POLARIZEDGLASSES` | `darkglasses/` also present |
| `GoldenEye-Plans` | `PLANS` (183) | `blueprints/` variant too |
| `GoldenEye-Watch_Identifier` | `WATCHIDENTIFIER` (208) + `ITEM_WATCHIDENTIFIER` (`gunfire.c:3076`) | 4th watch function with use-anim, never spawned |
| `GoldenEye-Safecracker` | `SAFECRACKERCASE` (190) | |
| `GoldenEye-Watch_Communicator` / `Watch_Geiger_Counter` | `ITEM_WATCHCOMMUNICATOR` / `ITEM_WATCHGEIGERCOUNTER` (`bondconstants.h:4188-89`) | shown for contrast — these *are* used (Aztec) |
| `GEGasKeyring` | `GASKEYRING` (157) + `ITEM_GASKEYRING` | |
All have `GUN_ANIM_STATE_USE_ITEM` handling in `gunfire.c:3060-3084` — i.e. the
game can *use* them; level data just never places them.
**Status: Tier 1/2.** Granting any of these to Bond is a port-layer item-spawn
(same mechanism as §B). Actually *placing* them in levels requires ROM/level-data
editing (a level editor or hand-patched object tables) — Tier 2, out of scope
for this repo but viable as a data mod.
### D. Cut multiplayer maps — 8 commented-out menu entries with dev notes
`src/game/front.c:556-573` `multi_stage_setups[]`: the shipped MP list is
Random/Temple/Complex/Caves/Library/Basement/Stack (default) + Facility/Bunker2/
Archives/Caverns/Egypt (progression-unlocked). Immediately below, **commented
out with in-source dev annotations**:
```c
//{... LEVELID_CITADEL ...}, //Citadel (old format setup)
//{... LEVELID_FRIGATE ...}, //dest (has xbla setup)
//{... LEVELID_STATUE ...}, //stat (works)
//{... LEVELID_CRADLE ...}, //crad (works)
//{... LEVELID_AZTEC ...}, //azt (needs setup)
//{... LEVELID_RUNWAY ...}, //runway (xbla setup)
//{... LEVELID_DAM ...}, //dam (xbla setup)
//{... LEVELID_DEPOT ...}, //depot (xbla setup)
```
TCRF imagery matches: `GECitadel*` (5 screenshots, main page),
`GEStatueMulti`, `GECradleMulti`, the whole Depot image set (`DepotCrateStack`,
`DepotRockets`, `DepotSafeTable`, `DepotSiloPipe[Ed]`, `DepotTrailers`,
`DepotPhoto`), and `GoldenEye-FrigateMap`.
The port already boots every one of these standalone (`-level_XX`; see
`docs/dev/LEVEL-STATUS.md` — Statue `-level_22`, Train `-level_25`, etc. all
PASS). So the geometry, AI setup and briefings
(`assets/obseg/brief/UbriefdepoZ.c`, `UbrieftraZ.c`, …) are intact; only the MP
menu entry was removed.
**Status: Tier 3 (mod fork).** Re-enabling = editing a game data table in
`src/game/front.c` — that's a game-logic change, disallowed in this repo
(non-negotiable #2). Viable as a separate mod branch/fork ("8 more MP maps"),
or as a documented cheat-menu route (the debug menu's `level`/`region` options
may already jump to any level ID — verify).
### E. Per-level unused props & geometry ("Unused stuff by level" page)
~150 intact screenshots, grouped by level. Cross-referenced against the object
table and level IDs:
- **Streets** (campaign stage 12; `LEVELID_STREETS`): an entire prop kit was
never placed — `StreetsCars`, `StreetsLamp[Base/Big]`, `StreetsSign[2/3]`,
`StreetsTrafficLight`, `StreetsShovel`, `StreetsCrossing`, plus two cut set
pieces `GE64-StreetsArea1/2` and Ourumov's car driving/crashing
(`StreetsOuruGo`, `StreetsOuruCrash`, `GEOurumovsCar`). Suggests a cut
street-chase scene.
- **Silo**: `SiloVentShaft`, `SiloLadder`, `SiloLaunchDoors`,
`SiloWalkway2Door/Guard`, `SiloMissingHandrail` (a bug/oddity screenshot),
`GoldenEye_UnusedStartingPoint`, and prerelease-only `PRSiloElevator`,
`PRSiloLamps`, `SiloCutWalkway`, **`ThirdSiloEditor`** (editor view of a
third silo that didn't ship).
- **Facility**: PR-vs-final wall sets (`FacilityWalls[Final]`,
`PRFacilityWalls`, `ArchivesWalls`), plus cut rooms: `FacilityBathroom`,
`FacilityConveyor`, `FacilityGasTanks`, `FacilityLabTanks`,
`FacilityWalkway`, `FacilityArmedScientistPR`, `FacilitySatBoxTex`,
`GoldenEye007-FacilityPhoto`.
- **Archives**: `ArchivesOverview`, `ArchivesLamp[Scan]`, `ArchivesPhoto`,
`PRArchivesNoWalls` (wall-less build view).
- **Bunker**: `BunkerExtraMons[Side]` (extra monster/guard spawns),
`BunkerPIP`, PR `BunkerLamp`.
- **Caverns**: `CavernsPursueObj` (a cut "pursue" objective marker), PR
`CavernsWalkway`.
- **Dam**: `DamIsland`, `DamPlatform`, `DamDockDoors`, `DamEndDuct`,
`DamTruckDoor`; region-diff shots `DamCompUS/EU`.
- **Frigate**: `FrigateHarpoonScan` (scan of a harpoon model),
`PRTrainTargetGlass`, `Pdest_harpoonZ[2]`, `Ptorpedo_rack`,
`FrigateMoneypennyPR` (a Moneypenny model in the Frigate PR build!),
region-diff `FrigateIntroUS/JP`.
- **Aztec/Temple**: `AztecConsolePads1-4`, `GE-AztScreen2781/2789`,
`AztExtraMainframe`, `AztDronePadsLabel`, `unused-Aztec-Path` (a whole
unplaced path), `EgyptianRed/WhiteClouds` (two skybox variants),
`TempleSkySolo/Multi`, PR: `AztBlueSuitPR` (cut enemy suit),
`AztHeavyFogPR`, `AztTableScreensPR`.
- **Surface**: `SurfaceBookCrate`, `SurfaceObsPad`, `SurfaceTower`,
`SurfaceJerryCan`, `SurfaceHutTable`, `SurfacePropHut`, PR
`SurfaceOverviewPR`, `SurfaceVent[PR]`, `SurfaceDish[PR]`.
- **Train** (campaign stage 14): `GE007-TrainSecretRoom` (a hidden room),
`TrainTargetGlass`.
- **Misc props/vehicles**: `GEMotorbike`, `GERedTruck`, `GESovietBoat`,
`GEHelicopterPilot`, `GEBiker`, `GoldenEye-moonfemale` (`moonfemale/` chr
model exists), `OurumovKey/Briefcase`, `GEDeskMat`, `GoldenEye-Crypt`
(confirmed orphaned map — see note below),
`GE64-BridgeRooms`, `GEJunglePicture`, `Runwaypicture`,
`Controlicon`, `Cactus`.
- **Orphaned level geometry** — `assets/obseg/bg/` holds per-level dirs for
`ame arch arec ark azt cat cave crad cryp dam depo dest dish len oat ref run
sev sevb sevx silo stat tra`. Notable: `cryp` (Crypt) has full geometry +
briefing (`UbriefcrypZ.c`) but **no `LEVELID_CRYPT` entry** in
`bondconstants.h` — fully orphaned. `len` (Lenin statue area, cf.
`StatueLeninPR`) and `dish`/`cat`/`oat`/`arec`/`ame`/`ark`/`ref` are further
level dirs beyond the shipped set. No `citadel` dir — its geometry was
either removed or shared (TCRF's 5 Citadel screenshots show a finished map,
so it existed in some build).
**Status: Tier 2.** All models are in the ROM's object table (spot-checked:
`PchrtesttubeZ`, `Pdest_harpoonZ`, `Pdoor_roller1-4Z`, `PhatfurryZ`,
`Pletter_tray1Z`, `Pmissile_rackZ`, `PsilencerZ`, `Psteel_door2[ b]Z`,
`Ptorpedo_rack`). Placing them needs level-data editing (object spawn tables) —
a data-mod / level-editor project, not a port change. The PR-vs-final texture
pairs (`*PR__1vt.jpg`) are interesting for a "pre-release textures" cosmetic mod
via the `tools_pc` asset pipeline — but the PR ROM itself isn't available here,
so source assets would have to come from TCRF's screenshots (low-res) or a
donated PR dump.
### F. Unused characters & faces
TCRF main-page imagery: `UnusedFace1/2/3` (each with F/S1/S2 views),
`GE64-FemHead1-4`, `KenFaceUnused/Used`, `Rosika[Compare]`, `SciCompare`,
`FemaleSci`, `GEBonds`, `Terrorists`, `TerroristBiker`, `StPetersburgGuard`,
Bond-suit sets named after actors: `Connery*`, `Dalton*`, `Moore*` (each with
HeadFront/Back/Left/Right + Jacket pieces), `GEArmor[our]`.
Cross-reference:
- The MP character table (`front.c:601+` `mp_chr_setup[]`) is huge — ~40 named
entries beyond the 8 selectable, including `ROSIKA`, `KARL`, `MARTIN`,
`MARK`, `DAVE`, `DUNCAN`, … `TERRORIST`, `BIKER`, `STPETERSBURGGUARD`,
`HELICOPTERPILOT` — Brosnan-body + swapped-head combos. The "unused faces"
are almost certainly extra head models in this pool that no body/entry uses.
- Head model dirs exist under `assets/obseg/chr/`: `headken` (the Ken face),
`headbrosnan*` (4 costume variants), `moonfemale`, plus ~30 named heads.
The actor-named suits (Connery/Dalton/Moore) do **not** appear as chr dirs —
likely cut before model export, or embedded in a shared body file.
**Unverified** which exact models the TCRF face shots show.
**Status: Tier 2/3.** Cosmetic mod (extra MP faces) = asset-level work if the
models are in the ROM; verify by dumping `assets/obseg/chr/*/Model.c` headers.
### G. Prerelease / dev-build evidence
Prerelease page (body blocked; filenames + embeds only):
- PR-vs-final comparison pairs: Taser, KF7, GrenadeLauncher, RocketLauncher
(+watch versions), SurfaceVent/Dish, `StatueLeninPR`, Facility walls/rooms
(§E), ArchivesWalls, CavernsWalkway, SiloCutWalkway, ThirdSiloEditor,
PRSiloElevator, BunkerLamp, OutletConsoles. Pattern: the PR build had more
weapons (watch-mounted guns!) and different Facility/Silo/Aztec geometry.
- **Dev-build videos** (YouTube embeds captured in the export):
- "Goldeneye 007 - Shoshinkai/Space World 1996"
- "Nintendo of America E3 1996 Take Away Video (VHS Rip)"
- "Goldeneye Beta WalMart Video"
- "Goldeneye 007 Promotional Trailer 1996"
- "N64 vous n'en reviendrez pas (VHS RIP 1997)"
- "64History - N64 B-Roll (1995)"
- "Escape (Banjo-Kazooie, GoldenEye 007)" — engine sibling footage.
Useful for dating PR features; not actionable in-repo.
### H. Misc oddities (main page, unverified)
- `GoldenEye007MickeyMouse` (by-level page, 2 KB image) — no "mickey" object in
the resource table; likely a texture/model easter egg spotted in a level.
- `Unused_bond_walk.gif` — an unused Bond walk animation (check
`assets/animationtable_entries.c` for orphaned anim IDs).
- `GoldenEye007-MovieLogoUnused` / `-MovieNoLogoUnused` — alternate intro logo
frames (title sequence data; `src/game/brief*`? verify).
- `GE_UnusedAlarm`, `GoldenEye-BloodImpact` / `BoxBlood` / PR `OpaqueBlood` —
blood-effect variants (cf. `src/game/blood_animation.c`).
- `GoldenEye007-BarrelUnused` / `-SmallBarrelUnused` — barrel props never placed.
- `GoldenEye-OddDay`, `Sight[Alt]`, `MountainTex`, `background`, `Pbin1Z`,
`PchrtesttubeZ[Explode]`, `PitonGunBungee`, `Psev_door[3_wind]Z` — props in
the table, placement unverified.
- Bugs & Notes articles: **nothing recoverable from this export** — re-scrape.
## 2. Feasibility summary
| Tier | Item | Effort | Where it lands |
|---|---|---|---|
| **0 — already works** | Debug menu (U+D C-hold), free cam, line mode, ramrom record/replay, cheat-grantable rocket/grenade/P90 | verify in-game; document keybinds | docs + maybe `input.c` keybind for U+D c-buttons |
| **1 — port-layer hook** | Grant taser/spectre/autoshot + unused items (watch identifier, bomb case, …) via config toggle replicating the cheat grant path (`bondinvAddInvItem` + `give_cur_player_ammo`) | small; follow F10-toggles pattern (`feat/f10-game-toggles`) | `port/src/` only |
| **2 — data/asset mod** | Place unused props per level; extra MP faces; PR texture swap | needs level-editor or object-table patching; separate project | data mod, not this repo's code |
| **3 — game-logic change (mod fork)** | Re-enable 8 cut MP maps (`front.c` table), campaign reordering | one-line-per-map table edits + setup fixes ("xbla setup" = unknown editor flag) | separate mod branch/fork only |
## 3. Next steps
1. **In-game verification pass** (PC build): open debug menu; confirm free cam,
`display speed`, level switch; enter the 2X rocket/grenade/P90 cheat codes;
screenshot via `grab rgb`. Record results here + findings.md if anything new
breaks (Dxx).
2. Confirm which region macros the PC build defines vs the `BUGFIX_R1` guards in
the weapon-grant cheats (non-negotiable #3) — the grants may be dead code in
our build.
3. Dump `assets/obseg/chr/` model headers to identify the UnusedFace1-3 /
FemHead1-4 models concretely.
4. ~~Re-scrape TCRF article bodies~~ — **done**; all six bodies recovered into
`backlog/cut-content/articles/` and cross-referenced (see RESEARCH.md §0).
5. If Tier-1 item granting is wanted: spec it as an F10 toggle set, port-layer
only, each entry citing the cheat-code precedent in `cheat.c`.
+29 -1
View File
@@ -3,7 +3,7 @@
Every `getenv("GE_…")` in the tree. The table prose is hand-curated;
**`tools_pc/gen_env_probes.py`** re-greps the live sites and reports drift
(NEW / GONE vars + a fresh file:line map) — run it before trusting the
File:line cells. Last reconciled 2026-09-04 (M-51). All are **env-gated**: unset = zero behavior
File:line cells. Last reconciled 2026-09-20 (v0.3.0 release review). All are **env-gated**: unset = zero behavior
change, so they are safe to leave in a release build (they only cost a
`getenv` on the guarded path). "Dead" below means *the finding it was built
for is closed* — the probe is strip-candidate scaffolding, not that it does
@@ -24,6 +24,8 @@ Two classes:
| `GE_INPUTLOG` | `port/src/input.c:329`, `port/src/config.c:53` (`configGetInputLog`) | Logs computed pad state (buttons + stick) each poll when non-zero. Also `[Debug] InputLog` ini. Also traces D165 `menuptr est/tgt/eff/stick`. | **live** |
| `GE_STARTMENU` (+ `GE_STARTMENU_PAGE`, `GE_STARTMENU_DIFF`) | `src/game/lv.c:398-402` (`#ifdef PORT`) | Boot straight into a front-end menu id (13=MISSION_COMPLETE, 10=BRIEFING, 7=MISSION_SELECT, 12=MISSION_FAILED, 6=MODE_SELECT); `_PAGE`=folder row (def 1=Dam), `_DIFF`=0..3. Crash-test a screen fast. | **live** |
| `GE_UNLOCK_ALL` | `src/game/debugmenu_handler.c:1098` (`get_debug_enable_all_levels_flag`, `#ifdef PORT`) | Mission-select shows every solo level (playtest jump-to-any-level aid). Cached in a `static int c`. | **live** |
| `GE_DEBUG_UNLOCKALL` | `src/game/file2.c:404` (`#ifdef PORT`) | Forces the "all levels unlocked" flag TRUE regardless of the shipped save — same effect as `GE_UNLOCK_ALL` but at the file2 unlock-query site. Cached in a `static int`. | **live** (playtest aid) |
| `GE_DEBUGMENU` | `src/boss.c:566` (`#ifdef PORT`) | Makes the L+R debug menu respond to live controller input (C-up+C-down open/close, stick + buttons read from pad 0) instead of the `#ifndef DEBUGMENU` path; unset reproduces that path exactly. Cached in a `static int`. | **live** (playtest aid) |
| `GE_OPTIONSOVERLAY` | `port/src/optionsoverlay.c:231` | Force-enable the F10 port-layer options overlay (D184 / PR #10) regardless of ini. `atoi != 0`. | **live** |
| `GE_D204` | `port/src/audio.c` (`audioSetNextBuffer`) | **Audio-health monitor (D204).** One line per 5 s of wall clock: `rt=` produced-audio/wall ratio (1.000 healthy; below 1 = the 30 Hz audio retrace is starving and the device is padding with silence), `q=` SDL queue depth / `queueLimit` (pinning at the limit with `drop=` climbing = overproduction, the "eventually no audio" state), `drop=` blocks discarded, `max=` largest block vs its `g_MaxFrameSize*4` allocation. **Deliberately cheap (~30 clock reads/s) — safe to leave on for a full playtest, unlike `GE_MIXERTRACE`.** The instrument of record for D202. | **live** |
| `GE_D204_OLD` | `port/src/audio.c` (`d204OldMode`) | Restores pre-D204 behaviour (`osAiGetLength()` reports the whole SDL queue depth; `queueLimit` back to 8192) so before/after can be A/B'd in **one binary**, without build-to-build variance. Temporary — remove when D204 closes. | **live** (D204 A/B) |
@@ -31,6 +33,7 @@ Two classes:
| `GE_PULLTRACE` | `src/libultra/audio/load.c` (`alAdpcmPull`) | `[PASTEND]` assertion: fires if a voice's `f->sample` exceeds its wave's total samples while `nOver == 0` (i.e. an unstopped voice keeps decoding past the end instead of zero-filling). Rate-limited to 40 lines. M-65: 0 hits — exhausted voices go silent correctly. | **live** (D202) |
| `GE_DMEMWIPE` | `port/src/mixer.c` (`aClearBufferImpl`) | Zeroes the whole software DMEM at the frame boundary (detected as `alMainBusPull`'s clear of `AL_MAIN_L_OUT`, addr 1088). Tests whether audio state survives across frames in DMEM. M-65: no-op on output — ruled out. | **live** (D202) |
| `GE_NOWET` | `port/src/mixer.c` (`aSetVolumeImpl`) | Mutes the reverb send (`wetamt = 0`), isolating the reverb delay lines as a possible source of persistent signal. M-65: drone unchanged — ruled out (NB `aPoleFilterImpl` is still an unimplemented no-op, D199). | **live** (D202) |
| `GE_OBJT` | `src/game/chraction.c` (`chrSetStageFlags`/`chrUnsetStageFlags`), `src/game/propobj.c` (GAS_RELEASING destruction) | D321 stage-flag writer probe: logs every low-16-bit write to `objectiveregisters1` (tick, writing chr, actiontype/aioffset, AI-list ID) plus each toxic-gas prop destruction timestamp — used to map the Facility tanks→combat-bit trigger chain. Capped, read-only, inert unset; post-release removal candidate (M-192 item 7). | **live** (D321) |
| `GE_BANKDUMP` | `port/src/romdata.c` | `BANKSRC` lines: the source ROM-layout bank tree per `soundIndex` (instrument → sound → envelope pointers + `decayTime`). Used to prove sound 203's `decay=-1` is genuine ROM data, not a conversion bug. One-shot at load. | **live** (D202) |
| `GE_FORCEALARM` | `src/game/propobj.c` (`handle_alarm_gas_timer_calldamage`, `#ifdef PORT`) | **D207 diag (temporary).** Forces the in-game alarm klaxon (`alarmActivate`) on a frame counter — on ~2–10 s, off, on again — and emits a per-frame `[D207] f=… active=… ptr_alarm_sfx=… playstate=… locked=…` line to `audiotrace.log`. Repro harness for the "alarm SFX overrides other audio" report when a level's own alarm can't be triggered headlessly. Remove when D207 closes. | **live** (D207) — temporary |
| `GE_KEYMAPDUMP` | `port/src/romdata.c` | `KEYMAP` + `ENVSRC` lines: raw source bytes of every `ALKeyMap` / `ALEnvelope` in the ROM-layout blob, for byte-wise comparison against runtime values. One-shot at load. | **live** (D202) |
@@ -43,15 +46,32 @@ Two classes:
| `GE_WRAPFIX` | `port/fast3d/gfx_pc.cpp:3166` (`gfx_set_wrap_fix`) | RC3 test override for the `Video.WrapFix` D74/D167 wrap-block path (`atoi != 0` wins over the ini). | **live** (RC3 not finished) |
| `GE_SAVELOG` | `src/game/file.c:46`, `src/game/file2.c:16` (`SAVELOG` macro), `src/boss.c:749`, `src/game/objective_status.c:297,343` | Traces the campaign save/unlock chain (objective completion → `end_of_mission_briefing` → `fileWriteSave` → EEPROM). Used to confirm D157. `g_savelogObjOnce` gates the per-criterion dump to the `bossReturnTitleStage` call. | **live** (strip once a few more level boundaries are playtested) |
| `GE_D160` | `src/boss.c:744`, `src/game/bondview2.c:791`, `src/aicommands.def:10055,10181,10236,10242,10247` (all `#ifdef PORT`) | Dam exit-cutscene (D148/D160) diagnostic: trace points in `bossReturnTitleStage`, `bondviewSetCameraMode`, and AI cmds `EndLevel`/`exit_level`/`CameraLookAtBondFromPad`/`CameraSwitch`. **Investigation in progress** — user owes a `GE_D160=1` Dam run. | **live** |
| `GE_D243` | `src/game/front.c` (`menu_init`, cached `s_d243`) | Per-menu-transition trace with a local per-tick frame counter (D243 stale-DL-burst screen identification). Counter is NOT numerically identical to the port's internal frame count — cross-reference by elapsed offset. | **live** (D243) |
| `GE_D243CAM` | `src/game/bondview2.c` (`bondviewFrozenCameraTick`, three `CAMERAMODE_POSEND` branches) | Dumps eye/look-at positions (+theta/verta on the `gBondViewCutscene` branch, +dt/angle/pad on the orbit branch) per call. **Found M-143**: the `gBondViewCutscene` branch fires and its look-at (`field_3C4/8/C`) sweeps through a filter catch-up after the cutscene warp — the shake mechanism. | **live** (D243) |
| `GE_D243M` | `src/game/bondview2.c` (`solo_char_load`, `bondviewRemovePlayerBody`, `playerTick`; cached `d243mEnabled`) + `src/game/objecthandler.c` (`drawjointlist`, both region variants) + `src/game/chr.c` (`chrTick`, `after_position_update:`) | M-151/M-154/M-159 probe for the Dam-abseil "frozen Bonds + shake": `D243M: CREATE/REMOVE` at both `bodyModel` sites; `D243M: tick` (idx/model/**propptr/chrptr**/cam modes/`prop->pos`/`field_488.pos`/`field_3B8`/`field_3C4-8-C`/**rp** = render_pos pointer, **never dereferenced** — M-154 fix 2: tick-time `render_pos` holds arbitrary stale slot contents until the first draw pass, so the pre-view root translation is logged by `chrdraw` only) while the model exists or POSEND/INTRO is active; `D243M: draw` per on-screen player render submission; `D243M: chrdraw` — M-154 per-frame model-draw census, one line per CHR render submission (Model* + raw root translation) while a scripted camera mode (POSEND/INTRO/SWIRL/FADESWIRL) is active; a second Bond instance = extra lines with *distinct* model pointers at the same frame (same model is drawn twice/frame — two passes — dedupe by (frame, model)); `D243M: onscreen` (M-159/M-162/M-163/M-166, `chr.c`) — one line per `chrTick` call while the probe is active: `vis=` `posIsOnScreen`'s result (M-159's frustum hypothesis, REFUTED by M-160) + the live `prop->pos` + `actiontype=`/`anim=` (M-160/M-164: which of `chrTick`'s branches fired, the attached-animation pointer) + `aidx=`/`animframe=` (M-166: the animation reverse-resolved to its `animation_table_ptrs1` index, GE_D193A's technique, + its current playback phase — tests whether the visible chr's animation *phase* at cutscene-entry, not just its identity, is what differs from N64) + `x2active=`/`x2raw=` (M-162/M-163, gated on `GE_D243X2`: whether the decisive save/restore experiment ran this tick, and the raw pre-restore position it overrode — the whole `prop->pos` vector as of M-163, not just Z). Silent during FPS play, silent on the `GE_D243X2` fields unless that env var is also set. Note: `field_3B8` x/z read at 10× world units in the smoke capture — not plain world position. | **live** (D243) |
| `GE_D243X2` | `src/game/chr.c` (`chrTick`, gated additionally on `d243mProbeActive()`) | **Not a probe — a decisive behavioral experiment, off by default, NOT a fix.** M-162/M-163: saves `prop->pos` right before `chrTick`'s position-update dispatch (`chr.c:2541-2617`) runs, restores it at `after_position_update:` — before the render-matrix computation later in the same tick can see the cycled value (unlike M-161, which restored too late to have any effect). Tests whether `chrTick`'s own animation-position write (M-160/M-164) is what produces the Dam-abseil shake. M-162 was Z-only (confirmed the mechanism on that axis, but the visible glitch persisted because that capture's chr cycled on Y instead); M-163 widened it to the whole vector. **M-163's live result: NEGATIVE for the visible shake** — `prop->pos` genuinely went smooth (log-verified) but the shake was unchanged, because the camera's look-at filter reads a *different* variable (`field_488.pos`, see `GE_D243X3`) that this experiment never touches. Kept in the tree as a real, useful (if ultimately not the visible-symptom fix) data point — always pair with `GE_D243M=1` so the `x2active=`/`x2raw=` fields on `D243M: onscreen` show whether/what it overrode. Do not ship. | **live, test-only** (D243) |
| `GE_D243X3` | `src/game/bondview2.c` (`playerTick`, the `field_488.pos` write site, gated on `g_CameraMode == CAMERAMODE_POSEND`) | **Not a probe — the experiment that actually confirmed the mechanism, off by default, NOT a fix.** M-167: skips the `field_488.pos.x/y/z` assignment (`bondview2.c:10495-10497`) entirely while POSEND is active, freezing it at whatever value it held the instant suppression engaged. `field_488.pos` — not `prop->pos` — is what the cutscene's leaky-integrator look-at filter (M-143, `field_3B8`/`field_3C4-8-C`) actually reads; it comes from `render_pos` (the model's animated-skeleton readback), an independent data path from `prop->pos`. **Live-confirmed positive: user — "yep camera shake is gone now."** Logs a companion `D243M: f488write frame=... idx=... x3active=... x3skip=... epoch=... f488pos=x,y,z` line (always-on when `GE_D243M=1`, independent of whether `GE_D243X3` itself is set) so the freeze is log-verifiable. **M-169: no longer freezes forever** — re-baselines (`x3skip=0`, write allowed through) exactly on the tick a legitimate shot-change teleport fires (`d243GetTeleportEpoch()`, bumped by `chrai.c`'s `AI_TRYTeleportingChrToPad` right after its real `setsuboffset()` call), addressing the M-168 postmortem's "camera looks the wrong way for later shots" incompleteness — untested live yet, next session's first move. **Superseded: D243 shipped its permanent fix (`4ccab22c`, M-170), which removed the X3 suppression block from `playerTick` — no live site remains.** Kept here as a tombstone; the generator flags it GONE. | **dead** (D243 closed — probe removed) |
| `GE_D243X4` | `src/game/chr.c` (`chrTick`, the `render_pos` snapshot/restore, gated on `d243mProbeActive()`) | **Not a probe — the M-168 experiment applying X3's technique to the actual draw-path consumer, off by default, NOT a fix.** Snapshots `model->render_pos[]` after a 2-tick warm-up once POSEND engages, then holds it steady by copying the cached contents back over each subsequent tick's freshly-computed buffer — `subcalcmatrices()` always still runs first (never skipped; the first two attempts skipped it and crashed on a reused-pointer stale `-1` tombstone, see findings.md D243 M-168). Addresses Bond's own rendered-body duplication (the consumer X3 never touched). Crash-free, stable, but same forever-freeze incompleteness as X3 pre-M-169. **M-169: same teleport-epoch re-baseline as X3** — the warm-up counter now also resets (re-snapshots) whenever `d243GetTeleportEpoch()` changes, not only on the active/inactive edge. Untested live yet. Revert once a real root-cause fix replaces it; do not ship as-is. | **live, test-only** (D243) |
| `GE_D243SWIRL` | `src/game/bondview2.c:10480` (`playerTick`, cached `d243SwirlTarget`) | D243 follow-up probe: `GE_D243SWIRL=<chrnum>` logs a per-tick line for that chr while the swirl/fade camera path is active (tick counter + state), to verify post-fix behavior of the swirl transition. Inert unless set to a non-negative chr number. | **live** (D243 verification) |
| `GE_D193` | `src/game/frametiming.c` (`waitForNextFrame`, `#ifdef PORT`) | Per-wall-second line: `render=` rendered frames, `sim=` sim ticks delivered (Σ of the `deltaFrames`/`g_ClockTimer` fed downstream), `clamped=` frames whose catch-up hit `FRAMETIMING_PORT_MAX_CATCHUP`, `maxraw=` largest pre-clamp delta. Healthy steady state = `render=30 sim=60 clamped=0 maxraw=2` (measured M-80, all levels idle). Use to check whether the wall-clock timer under-advances under real AI load (D193). Cheap; safe for a full playtest. | **live** (D193) |
| `GE_D193A` | `src/game/chr.c` (`chrTick` → `d193aSample`, plus `d193aMoved` in `chrUpdateAnim`; `#ifdef PORT`) | Per-wall-second per-ticked-chr locomotion telemetry (D193/D209): `chr=` chrnum, `act=` ACT_TYPE, `speed=` world-units travelled/s, `gtd=` g_GlobalTimerDelta, `tier=` `act_gopos.unk59` SPEED tier (0 walk / 1 run / 2 sprint; `-1` when not ACT_GOPOS), `gspd=` `act_gopos.speed` (**a turn rate, not a travel rate** — 0 means running straight), `typ=` PROP_TYPE (3=CHR, 6=VIEWER), `tick=`/`move=`/`frz=` chrTick entries vs `chrUpdateAnim` calls vs freeze-gated (move « tick = the chr is being visibility-culled out of its own locomotion; `move=0` at high `speed` = WAYMODE_MAGIC), `hid=` CHRFLAG_HIDDEN, then the root-motion scalars `scale` / `xlscale` / `animrate` / `playspeed` / `mspeed` (model->speed) / `endframe` / `anim` ptr / `aidx=` that pointer reverse-resolved to its `animation_table_ptrs1` index (40 walking, 41 sprinting, 42 running, 107 walking_unarmed — map via `src/game/initanitable.c`). `GE_D193A_CHR=<n>` restricts to one chrnum. **This probe found D209**: Cradle showed `tier=1` (RUN) with `aidx=40/107` (walk anims) on every sample. | **live** (D193/D209) |
| `GE_D193B` | `src/game/model.c` (`sub_GAME_7F06D3F4`, `#ifdef PORT`) | Rate-limited (1/97) raw anim root-motion decode trace (D193): `joint=` / `flip=` / `base=` mtx channel index / `frame=` / `tmp=` decoded s16 translation triple from the anim bitstream / `angle=` / `anim` ptr / `bd=`+`bs=` the D32 u32 bitDescriptors/bitStream addresses / `njoints`. M-80 Facility: `base=0`, `angle=0` **always** (root-motion facing delta never non-zero — top lead), `tmp` ≈ (±small, ~1086 hip-height, ±small). Compare `tmp`/`angle` magnitudes against a clean N64 decomp build of the same anim+frame. | **live** (D193) |
| `GE_D250` | `src/boss.c` (`bossMainloop`, wraps `lvlRender()`, `#ifdef PORT`) | Per-wall-second line: `[D250] lvlRender avg=Nus n=M` — average + count of `lvlRender()` (DL-building: culling/animation/scene traversal) calls that wall-second, isolated from `gfx_run()`'s software-RSP interpretation cost (already covered by the existing "frame rendered in Nus" line). Added M-120 to chase D250 (fps stuck ~45-50 on Dam, ruled resolution/MSAA/vsync/build-flags out already) — found `lvlRender` itself is cheap (~0.5-1.5ms avg), so it is NOT the missing ~10-15ms/frame either; the still-unexplained cost must be elsewhere in the per-retrace-message loop (scheduler/thread round-trip, audio, or something not yet isolated by a targeted timer). Cheap; safe for a full playtest. | **live** (D250) |
| `GE_D172` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1` — SETCOMBINE dedup log + non-LOD multitex tri dump; `#ifdef PORT`) | Particle magenta/cyan colour (D172). Logs each distinct SETCOMBINE + active cycletype, and — for a 2-cycle tri that consumes TEXEL1 with `tex_lod=0` and a distinct 2nd tile — the tile fast3d *samples* for each texunit vs what the DL *configured*. **Found D172**: 40/40 particle tris configured tile 1 @ tmem 392 (fire) but sampled tile 0 (smoke) → `smoke*smoke`. FIXED (`gfx_lod_tile_offset` `return rdp.tex_lod ? 0 : i`). Probe kept inert. | **live** (D172 FIXED, probe kept) |
| `GE_D157` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex` + `import_texture`; `#ifdef PORT`) | D219/D252's "rainbow particle" family — bullet-impact spark on Bunker1 door confirmed by screenshot to cycle magenta/green/cyan (M-157). Three sub-probes: original exact-color-match log (`g_BulletSparkColors[]` byte match → geometry_mode/LIGHTING + shaded RGB); **`D157T`** texture-identity-keyed vertex full-path log (tile 0 holds a loaded texture ≤16 KiB in frame window [90,240] → raw `cn[]`, post-shade RGBA, LIGHTING, geometry_mode, combine_mode, tile-0 tmem/fmt/siz/addr/size/line, uv; cap 400 + summary/200); **`D157I`** import census (small textures imported in the window, deduped per source address → addr/fmt/siz/size/tile/tmem). M-158: bug did NOT reproduce in 6 identical dev-box runs — probes left live to capture the full RGBA path on the next occurrence (user Steam Deck run with `GE_D157=1`). | **live** (D219/D252, non-reproducible as of M-158) |
| `GE_DVM` | `src/game/gunfire.c` (`gunUpdateAndFire` gate dump + `gunRenderFirstPersonGunModels` render dump; `#ifdef PORT`) | 1P weapon viewmodel (D215). `[DVM]` line: the `field_87F` show/hide gate terms + `Gun_hand_without_item` sub-terms + `copy_of_body_obj_header` RootNode/Switches/numRecords. `[DVMr]` line: `87F` + `cmds_emitted` = gfx commands the weapon `subdraw()` produced (healthy ≈ 150; the D215 bug showed **1**). Rate-limited ~1/s per hand. **Found + verified D215** (`renderdata.flags` read 0 → all weapon DL nodes gated off). Probe kept inert. | **live** (D215 FIXED, probe kept) |
| `GE_DTEX` | `port/fast3d/gfx_pc.cpp:1001` | Per-`import_texture` param log (dims / line / size / lod / gen_mipmaps). Used to root-cause RC2 and D159. Kept as an inert diagnostic. | **live** (RC2/RC3 not finished) |
| `GE_TEXDUMP` | `port/fast3d/gfx_pc.cpp:1020`, `port/fast3d/gfx_opengl.cpp:700` | PPM dump of every uploaded texture + a `fmt/siz/palfmt/palidx/pal[0..3]` line. Root-caused D161 (Depot ceiling). Kept as an inert diagnostic. | **live** (RC3 not finished) |
| `GE_D116` | `port/fast3d/gfx_pc.cpp:1710,2362`, `src/game/textrelated.c:265,541` (cached `ge_d116`) | HUD/text "X-mirror" (D114/D116) vbo/uv trace. **D114/D116 CLOSED — NOT A BUG (M-33/D168): the captures were upside-down, not mirrored.** Probe is now dead scaffolding — strip on the next cleanup pass. | dead (D114/D116 closed) |
| `GE_D288` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, right after the D233 trivial-reject guard; cached `ge_d288`) | M-152 diag for the Silo intro stray triangle: logs any triangle whose clip-space NDC bbox covers >15% of the screen or goes non-finite, with each vertex's `w` and `any_behind_camera`. **Investigation in progress** — narrowed to two candidates (an unguarded `w`-division NaN leak in the backface-cull test just below, or the D106/D271 portal-BFS family), not yet root-caused; needs a live/headless `-level_20` run to disambiguate. | **live** (D288) |
| `GE_D236RM` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, unconditional getenv, no state kept when unset) | M-155/pass-10 diag for D236: per-frame (`num_dls`) triangle count of the `0x0c184b50` render-mode class (`other_mode_l` exact match), heartbeat-logged every 300 frames even at zero. **REFRAMED D280's "run-to-run dependent instantiation" as ordinary room-based occlusion**, not a CPU-side gating bug — see `## D236` pass-10 writeup. | **live** (D236 OPEN, kept for future passes) |
| `GE_D236ORDER` | `port/fast3d/gfx_pc.cpp:1844` (`gfx_sp_tri1`, cached `s_d236order`) | D236 pass-12 diag: per-DL sequence log of the tree-class triangles (DL index, screen-space depth, noise min/max census) to test the draw-order hypothesis. Zero cost unset; remove once D236 concludes. | **live** (D236 OPEN) |
| `GE_D236ALPHA` | `port/fast3d/gfx_pc.cpp:1896` (`gfx_sp_tri1`, cached `s_d236alpha`) | D236 pass-14 diag: for the `0x0c184b50` tree-class render mode, logs the combine/alpha state of up to 4000 matching triangles (deduped on combine change) so the captured draw can be cross-checked against the source. Zero cost unset; remove once D236 concludes. | **live** (D236 OPEN) |
| `GE_D236ZFIX` | `port/fast3d/gfx_pc.cpp:1970` (`gfx_sp_tri1`, cached `s_d236zfix`) | D236 pass-12 test override: forces `depth_update=true` for the `0x0c184b50` tree-class render mode — a cheap mechanism test before deciding what a real fix looks like. Off by default; remove once D236 concludes. | **live, test-only** (D236 OPEN) |
| `GE_D236RAW` | `src/game/bg.c:2437` (`#ifdef PORT`) | D236 pass-16 diag: one-shot census of the raw vertex table's alpha-byte distribution (5 buckets + min/max), camera-independent, to check whether any vertex in the whole table exceeds the low ceiling. Zero cost unset; remove once D236 concludes. | **live** (D236 OPEN) |
| `GE_D245V` | `src/game/sky.c:962` (`skyPortRenderPoly`-adjacent, `#ifdef PORT`) | D245 water-seam diag: per-call min/max of the recomputed S/T texture-coordinate ranges for the water fan vertices (rate-limited), to check whether the seam tracks a tc-shift boundary. Inert unset. | **live** (D245 OPEN) |
| `GE_D245_FIXEDSHIFT` | `src/game/sky.c:946` (`#ifdef PORT`) | D245 test override: pins the water tc shift `k` to the given integer (0..SKY_TC_MAX_SHIFT) instead of the computed per-vertex value, to A/B the allowShift=TRUE behavior. Off by default (unset = current behavior). | **live, test-only** (D245 OPEN) |
| `GE_D252` | `port/fast3d/gfx_pc.cpp:733` (`import_texture`, cached `s_d252`) | D252 rainbow-particle diag: one-shot per (address,size) import log of the tile state (tmem/fmt/siz/addr/size/line) for small textures so a suspect particle tile can be checked against its source bytes by hand. Remove once D252 is resolved. | **live** (D252 OPEN — non-reproducible) |
## Dxx diagnostic probes — finding closed unless noted
@@ -83,6 +103,14 @@ Two classes:
| `GE_D104` | `src/game/bg.c:654` (rate-limited, `d104c`) | D104 depth-clear trace | dead (D104 closed) |
| `GE_D71LOG` | `port/fast3d/gfx_pc.cpp:645` | D71 fast3d trace | dead (D71 closed) |
| `GE_D75` | `src/game/title.c` `sub_GAME_7F007F30` (capped 8) | D75 Bug 2 gun-barrel model probe: logs `chrModelInstance`/`gunModelInstance` ptr+obj+numMatrices, `render_pos`, `renderData.mtxlist`, `g_GfxMemPos`, `osVirtualToPhysical(render_pos)` | **LIVE** (D75 Bug 2 OPEN — see §F "D75 Bug 2 — RUNTIME PROBE") |
| `GE_D305` | `src/snd.c:420` (`sndHandleEvent` desync branch, `#ifdef PORT`) | D305: logs the sound-preemption desync state when it is detected (after the NULL-guard fix made the crash itself impossible) — confirms whether `Game.AllUnlocked` correlates or was coincidental, and whether it recurs. Inert unset. | **live** (D305 monitoring) |
| `GE_D309` | `src/game/chrai.c:810,3424`, `src/game/chraction.c:10139` (all `#ifdef PORT`) | D309 freeze probe: logs AI label-not-found restarts (`chrai.c`) and the run-to-bond persistent-list step-on path (`chraction.c`, capped 400 lines) — the diagnostics used to root-cause the elusive complete freeze (D309/D310). Inert unset. | **live** (D309/D310 fixed — kept for recurrence) |
| `GE_D314` | `port/src/optionsoverlay.c:681` (cached `s_d314Enabled`) | D314 F10-flicker probe: logs the overlay's viGetY / visible-row count / scroll / selection each poll. Inert unset; strip candidate once D314 is triaged. | **live** (D314 pending triage) |
| `GE_D318R` | `port/src/random.c:69,147` (`#ifdef PORT`) | PRNG golden probe (D284/D318): logs the first 4000 gameplay-PRNG draws with tick + caller return address, and every `randomSeed` set — verifies the bit-exact port of `random.s` and which call sites consume the stream. Inert unset. | **live** (D284 verification) |
| `GE_RSEED` | `port/src/random.c:134` (`#ifdef PORT`) | Test-only boot-seed override: `GE_RSEED=<hex64>` pins `g_randomSeed` at first seed instead of the wall-clock-derived value — for determinism and N64-parity tests. Inert unset. | **live, test-only** |
| `GE_RSEED_LV` | `port/src/d318watchdog.c:232` (`#ifdef PORT`) | One-shot level-start seed log: logs `g_randomSeed` at the first tick after level start (optionally armed for a specific tick via `<hex>@<tick>`) — ties a saved/loaded game's PRNG state to a level for D318-class repro. Inert unset. | **live** (D318 verification) |
| `GE_D318T` | `port/src/d318watchdog.c:270` (`#ifdef PORT`) | D318 watchdog timeline probe: per-tick log of the Facility ai_22 softlock signature window (c78 list/offset/attack state, c67 health) while the watchdog watches. Inert unset. | **live** (D318 verification) |
| `GE_D318W` | `port/src/d318watchdog.c:136` (`#ifdef PORT`) | **Not a probe — the D318 port-side watchdog itself, ON by default.** Detects the Facility ai_22 hold-pose softlock signature (a latent N64 race, see findings.md D318) for 600 consecutive ticks and re-seeds c78's attack via the game's own fresh-attack entry point. `GE_D318W=0` opts out; logs `D318W:` lines when it acts. **Permanent fix, not strip-candidate.** | **live** (D318 fix — permanent) |
## Notes
+217
View File
@@ -0,0 +1,217 @@
# GEPD-inspired input overhaul — FINAL PLAN (issues #89, #90)
Status: **plan only** — no code changes yet. Companion to `port/src/input.c`
(current state: D118/D165/D166/D194/D214/D223/D238 lineage).
## 1. The reference (no longer vendored in the public repo)
The GEPD Edition Mouse Injector source was vendored at `reference/mouse-injector/`
and **removed from the public repo on 2026-09-19** (GPLv2 code + a prebuilt
binary inside an MIT project; never compiled here). See
`reference/mouse-injector/README.md` for provenance and how to re-vendor it
locally if you need the original. The file that mattered was
**`games/goldeneye.c`** (~380 lines, the whole GE aim model), plus `device.c`
(raw mouse polling) and `maindll.h` (tickrate — the units every per-tick
constant assumes). The model is quoted inline below and in the D194 findings;
the ported result lives in `port/src/input.c`.
### What the Mouse Injector does (GE)
### What the Mouse Injector actually does (GE)
| Aspect | GEPD implementation (`games/goldeneye.c`) |
|---|---|
| Mouse source | Raw driver deltas via ManyMouse (`device.c`), cursor locked by periodic `SetCursorPos`. No OS pointer accel, no deadzone. |
| **Hipfire look** | **Direct camera write**: `camx += XPOS/10 * (SENS/40) * (fov/basefov)` degrees per tick; `camy` likewise, clamped ±90 (−20 in tank). Linear in px — this is why GEPD "just feels right". |
| **Aim mode (R)** | Crosshair *position* accumulator `crosshairpos += XPOS/10 * SENS/292`, clamped to ±`CROSSHAIRLIMIT` (5.159° = screen edge). Gun/arm pose derived per-weapon from a 33-entry offset table. **View scrolls only past 72% of the way to the edge**: `aimx = (ratio−0.72) * 475 * timestep`, scaled by `fov/basefov`. |
| FOV coupling | Every term carries `(fov/basefov)` — scopes automatically get finer control. `basefov` = min(60, override). |
| Tank | Separate direct write to the tank x-rotation global (`0x80036484`), different scale (`360/TANKXROT * 2.5`); crosshair X locked to 0 when driving with tank equipped. |
| Menus | Mouse moves the menu cursor directly (`menux/y += px/10*SENS*6`, clamped to screen rect); aim button acts as B (back) in menus; wheel = weapon cycle macro. |
| Safety gates | Injection only when `camera==4||0 && menupage==11 && !dead && !watch && !pause && !mproundend`. |
| Extras | Crouch *toggle* mode, dedicated reload key (ROM hack — not portable to us), FOV override + viewmodel repositioning (ROM hacks — we have the clean equivalents in `port/`), intro skip on FIRE+AIM. |
### Already inherited (do not redo)
- **D194 GEPD-mirror aim** (`aimGepdCompute()`): crosshair-position model,
±5.159° clamp, 72% edge scroll at 475/s, fov scaling, per-tick overwrite of
`crosshair_x/y_pos` + `gun_azimuth_angle/turning`. This *is* GEPD's aim mode.
- D223 mouse-wheel weapon cycling (A / A+Z synthesis) — mirrors
`GE_Controller()`'s weapon macro; D214 ini key binds; click-to-lock capture;
F10 options overlay with aim/turn sensitivity sliders; D194(b) dt-decoupled
look; menu pointer via direct `cursor_h/v_pos` writes (mirrors GEPD's
`menux/y` writes).
### Remaining inheritances (this plan)
GEPD's *hipfire* is a direct linear camera write — we still route hipfire
through the N64 stick curve (#89). Also un-inherited: raw-input-by-default,
crouch toggle, GEPD-style safety gates on our always-live look paths, and its
sensitivity/FOV coupling discipline. That's WIs 1–3 + 6 below.
### Reference split (GEPD vs PD port)
Two references, two jobs (decided during #89/#90 triage):
- **GEPD Mouse Injector** (was `reference/mouse-injector/`, removed from the
public repo 2026-09-19 — see its README stub) = *what the mouse does to the
camera*. The only reference with GE-specific knowledge (camera fields,
crosshair limits, tank case, safety gates). Primary for WI-1.
- **PD PC port checkout** (standing reference, see AGENTS.md) = *how a PC game
presents and captures input*: cursor-lock behavior, raw input handling,
sensitivity units/scaling, and the shipped in-game options/keybind UI.
Primary for WI-2 conventions and WI-5's menu design. PD knows nothing about
GE — never use it for camera math.
## 2. Issue #89 — "mouse movement is not uniform; slow motion barely registers"
### Diagnosis (high confidence)
The complaint is the **hipfire** path. Today hipfire converts mouse px into an
*analog stick value* (`sx += (int)(hipEdx * hipSens * MOUSE_TURN_GAIN)`), and
the game then runs that stick through its own N64-era natural-turn response —
a **quadratic-ish curve with a soft floor** (D238: "saturates the game's
quadratic natural-turn curve at ~13 px/poll"). Consequences, exactly as the
reporter describes:
1. Slow mouse motion → small stick values → squared-off tiny turn rates →
"almost unrecognised".
2. Fast motion saturates the curve at full 315°/s — so the usable band is
narrow and the feel is non-uniform (bang-bang-ish).
3. `(int)` truncation of the stick adds a quantized dead zone on top.
GEPD/PD-PC never hit this because **they write the camera angles directly** —
the stick curve is bypassed entirely, and sensitivity is linear in px with
FOV scaling. We already do exactly that in aim mode (D194); hipfire is the one
leftover N64-stick-shaped path.
Secondary contributors worth folding in:
- `Input.MouseRawInput` defaults to **0** — on X11/Wayland (the #90 reporter
is on Linux) OS pointer acceleration still shapes relative deltas; GEPD
reads the raw driver. Windows is largely unaffected.
- Sensitivity is spread over 4 knobs (`MouseSensitivity`, `MouseTurnSpeed`,
`AimModeSens`, legacy `MouseAimSpeed`) — the reporter "tried adjusting
ge007.ini" and couldn't find the one that mattered for hipfire.
### Work items
**WI-1 (core fix): direct camera writes for hipfire look.**
Mirror GEPD's hipfire branch in `input.c`.
*Pre-flight:* skim the PD port's `port/src/input.c` mouse path first and match
its conventions (sensitivity units, dt handling, focus-loss behavior) so the
result feels like PD PC rather than a reinvention. GEPD supplies the GE camera
math; PD supplies the UX shape.
Implementation:
- New path `hipDirectCompute()` next to `aimGepdCompute()`: convert this
poll's dt-scaled px to degrees using the *current* viewport
(deg/px = fovy/viewH, or hfov/viewW — reuse whatever D194 absolute aim used),
scale by `MouseTurnSpeed * MouseSensitivity`, and add straight to
`g_CurrentPlayer->vv_theta` / `vv_verta` (clamp ±90). Same confinement as
today: only when `idx==0`, cursor captured, in a running stage.
- **GEPD's safety gates** must be ported over (aim mode got away without them
because it requires RMB held; hipfire is always live): check the decomp
equivalents of GEPD's `camera==4||0 && menupage==11 && !dead && !watch &&
!pause` before writing. `menupage` → `current_menu` (already have); the rest
are `player`/game globals — read-only, no logic change.
- **Tank**: GEPD writes a separate tank-rotation global with its own scale and
locks crosshair X to 0 when driving with the tank equipped. Find the decomp
symbol for `0x80036484` (tank.c) and mirror it; if too fiddly, phase 2 —
tanks are rare in single-player.
- Keyboard turn (arrows → stick) keeps the existing stick path; only mouse px
goes direct. Escape hatch: `Input.MouseDirectLook=1` default, `0` reverts to
the current stick path (kept as-is).
- This makes hipfire and aim mode share one linear mental model: px → screen
angle, FOV-scaled, dt-normalized. Expect to then simplify or delete the
D166/D238 curve-tuning knobs that become inert (`MouseAimCurve`, `AimBand`,
legacy `MouseAimSpeed`) — *after* playtest, and keep the config keys as
no-op aliases so existing inis don't break.
**WI-2: raw input by default.** Flip `Input.MouseRawInput` default to 1 (SDL
hints already handled in `inputInit`). Verify Windows relative-mode deltas are
unaffected; this mainly fixes Linux feel. Keep the escape hatch.
**WI-3: one honest sensitivity number.** With WI-1, hipfire and aim both scale
off px→degrees, so `MouseSensitivity` becomes genuinely master. Add it to the
F10 overlay (see WI-5), document the knob hierarchy in the generated ini
header comment, and re-calibrate defaults against GEPD's SENS≈40 ≈ 1:1
screen-angle mapping (our current 38/50/100 trio should land close; measure,
don't guess).
**Verification:** `GE_INPUTLOG` linearity check — log px-in vs deg-out at slow
and fast speeds (ratio must be constant); headless script harness for the
stick-fallback path; manual playtest checklist (hipfire slow/fast, aim mode,
scope zoom, tank, menu, death/cutscene safety gates). New finding `Dxxx` in
`docs/dev/findings.md`, index row added.
## 3. Issue #90 — in-game key bindings + "ini binds don't work" + sensitivity menu
### Diagnosis candidates for "binds don't work properly" (repro before fixing)
1. **Not a bug, GE design:** in-game, a fresh A edge *without* Z held is the
weapon-cycle-forward signal (D223 documents `weaponForwardOffset`). The
default Action bind includes E — so "pressing E changes weapon" is the
game's own behavior, and "Escape reloads/activates switches" is the default
Cancel=Escape → B button. The user's mental model (PC: E = interact)
collides with GE's A/B semantics. Address via documentation + possibly a
PC-idiomatic default set (product decision; GEPD itself uses Q=A, E=B).
2. **Ini syntax footgun:** keys must be written as `Input.Bind.Action=E` (or
`Bind.Action=E` under `[Input]`). A bare `Action=E` under `[Input]` resolves
to `Input.Action`, which is unregistered and silently ignored. `configLoad`
also ignores unknown keys with no warning — add a log line for unrecognized
keys so users get feedback.
3. **Key-name format:** binds use SDL scancode names (`"Left Ctrl"`, `"Up"`,
`"Space"`); nothing documents this in the ini. Add per-key comments in
`configSave` output and/or a `# valid names: ...` header.
4. There may still be a real parse bug lurking (`strtok` on the seeded buffer,
constructor ordering vs `configLoad`) — **repro first**: write an ini with
`Input.Bind.Action=E`, launch, confirm via `GE_INPUTLOG` that E alone
produces A. Fix whatever actually breaks; don't fix hypotheticals.
### Work items
**WI-4: keybind audit + ini ergonomics.** Repro (above), fix real bugs, add
unknown-key warnings, document scancode names in the saved ini. Cheap, do first
— it unblocks #90's half regardless of the UI work.
**WI-5: F10 overlay — bindings section + master sensitivity.**
Model the UX on the PD port's shipped in-game options menu (standing reference
checkout) where anything is player-facing. `optionsoverlay.c` already has
row/hit-test/scroll infrastructure and owns all input while open; what's missing:
- A new row kind `ROW_BIND`: label = action, value = first bound key name;
click → "press a key…" state that captures the *next* scancode (keyboard or
mouse button), writes it into `g_bindStr[a]` (replacing or appending —
decide: GEPD/PD use one primary + optional secondary; simplest v1 = replace
single key, keep multi-key only for ini power users), then `inputRebuildBinds()`
+ persist via the existing config save path.
- Expose `Input.MouseSensitivity` (master), and after WI-1 trim the panel to
master + per-mode fine-trim + invert Y (drop inert legacy rows).
- Bind capture must swallow all input while armed (overlay already does this)
and handle ESC = cancel, Backspace/Delete = clear.
**WI-6 (nice-to-have, from GEPD): crouch toggle.** GEPD's `CROUCHTOGGLE` state
machine (`GE_Crouch`) is ~20 lines and pure port-side (stance flag write).
Low priority; include only if WI-1..5 land cleanly.
## 4. Suggested order
1. **WI-4** (keybind repro/fix + ini docs) — small, unblocks #90 partially.
2. **WI-1** (direct hipfire) + **WI-2** (raw input default) — the #89 fix;
playtest-heavy, needs the safety gates done right.
3. **WI-5** (overlay binds + master sens) — completes #90.
4. **WI-3** (sensitivity consolidation/cleanup) — after WI-1 defaults settle.
5. **WI-6** (crouch toggle) — optional garnish.
## 5. Constraints reminders
- All work is `port/`-only: direct struct writes are the established D194
pattern (read/write live game state from the input poll, no logic change).
Any temptation to "fix" the natural-turn curve in `src/game` = rule-2 sign-off
territory — don't.
- Keep every removed/renamed ini key registered as an alias so shipped inis
keep loading; log deprecations at startup.
- Findings: new `Dxxx` entries for WI-1 (hipfire direct look), the #90 bind
repro outcome, and the overlay bind UI; update `findings-index.csv`.
+3 -3
View File
@@ -14,10 +14,10 @@ once the game is playable start-to-finish without crashes.
| **D76** | 2D disclaimer screen only partially drawn. | §F D76. |
| ~~**D77**~~ | ~~No audible music on PC.~~ | **RESOLVED (M-104, PR #42) — in-level music now plays.** Root cause was a D6-class missing `return` in `sub_GAME_7F0C0BF0` (`mp_music.c`) that zeroed the seqp volume on the only in-level trigger path; menu/intro music bypassed it and was always fine. The remaining audio-*quality* gap (wrong-sounding instruments on some tracks — elevator named specifically — plus occasional garbling) is tracked as **D230**. §F D77/D230. |
| ~~**D116 (HUD)**~~ | ~~proportional-font strings + ammo counter mirrored~~ | **CLOSED — NOT A BUG (M-33, §F D168).** Same capture-orientation artifact as D114. |
| **D148** | Level-end cinematics don't play. Dam: hitting the exit trigger should show Bond rappelling down the dam (the bungee jump); instead it cuts straight to the mission-complete report. **Broader (M-49 full playthrough): cutscenes are buggy in many places across the game**, not just the Dam ending — the user reports frequent cutscene glitches (skipped, wrong camera, misplaced/hovering actors, timing). Treat D148/D160 as the tracking entry for the whole cutscene-reliability problem, not just Dam. | Scripted cutscene / cinematic-camera + Bond rappel anim. Found M-27 during the first full Dam→Facility playthrough. Progression is unaffected — the report and next briefing load fine. The widespread nature (M-49) points at a systemic cause: the propDef/`sizepropdef` stride desync (D122/D132 family) that misreads `CutsceneRecord` lookups, and/or D75 anim-model + D173 actor-position bugs showing up wherever a cutscene puts a character on screen, and/or the D155/D156/D193 timing family driving cutscene camera/anim pacing. **M-31 (§F D160):** static-traced the Dam end ailists (`ai_24`→`ai_17`, `camera_switch`→`CutsceneRecord` type-46 lookup). Diagnostic `GE_D160=1` shipped (`c95713f5`) — user runs Dam to the exit with it set; the trace pins whether the cutscene AI runs, and whether the propDef-index walk (D122/D132 family) hands `camera_switch` a bogus cutscene record. Leading hypothesis: propDef `sizepropdef` stride desync (medium confidence), not D75 anim-model. **M-87 user playtest:** Bond floating/hover cutscene glitch (D173) still present; new specific instance — **Bond spins in circles in some cutscenes**, named example the end-of-Cradle Trevelyan-falls cutscene. Likely the same actor-position/anim-root cause manifesting as bad rotation instead of bad height. |
| **D148** | Level-end cinematics glitch. Dam: the Bond rappelling/bungee cutscene plays but the camera violently shakes and Bond's own model appears to duplicate/glitch. **Cutscenes are buggy in many places across the game** (D173 level-start floating, D292 death-sinking, Cradle spin) — treat D148/D243 as the tracking entry for the whole cutscene-reliability problem, not just Dam. | **2026-09-17 (D243 M-107 through M-168, ~18 session-passes): the mechanism is now understood in real depth, and the camera-shake half is fixed (diagnostically, live-confirmed).** Full history + code citations: `docs/dev/findings.md` `## D243` (long, multi-pass — look up via `findings-index.csv`, read the M-159 through M-168 paragraphs for the current understanding, don't linear-read). Short version for planning purposes: **(1) Root mechanism** — the cutscene teleports the *visible* Bond chr between shot positions correctly (`AI_TRYTeleportingChrToPad`, `chrai.c:4064`), but the actual rappel *animation* commands run on a separate, invisible driver chr (M-145) — the visible chr never gets told to play it, so it just keeps ticking whatever animation it already had equipped before the cutscene grabbed it. Every tick, that stale animation's own root motion gets read back into the chr's render position (`chrUpdateAnim`→`getsuboffset`, `model.c:684`), producing a small repeating cycle that reads as "shaking"/"glitching" on screen. Whether this exact mechanism is itself decomp-faithful (with the real PC/N64 divergence being *which* animation phase Bond happens to inherit at the cutscene boundary, not a code-path bug) is still an open question (M-165/M-166) — not yet answered. **(2) Two independent render paths, not one bug** — the camera's look-at filter reads `field_488.pos` (derived from `render_pos`, the model's animated-skeleton transform); the model's own on-screen draw reads `render_pos` directly (`objecthandler.c`'s `drawjointlist`). Fixing one does not fix the other; two live-test cycles were spent learning this the hard way (M-162/M-163 → M-167). **(3) Camera shake: FIXED at the mechanism level, live-confirmed by the user** ("camera shake is gone now") by freezing `field_488.pos` during the cutscene (`GE_D243X3`, `bondview2.c:10495`) — diagnostic-only, not shippable as-is (see next). **(4) Bond's own model duplication: mechanism-level fix attempted (`GE_D243X4`, `chr.c`, freezing `render_pos`'s contents) — stable after fixing two crashes (model-pointer-reuse hazard, see `porting-notes.md`'s two D11 updates), but freezing unconditionally also breaks the cutscene's *legitimate* per-shot camera/model repositioning** (the camera now looks the wrong direction for later shots; Bond still looks stuck) — not a fix yet. **Next steps for whoever picks this up** (none attempted yet): (a) re-baseline the `GE_D243X3`/`GE_D243X4` freeze on a position-delta threshold (a jump much larger than the observed intra-shot cycling amplitude, already characterized in the M-153/M-160 captures, is very likely a real shot-change teleport, not cycling) instead of freezing forever; or (b) hook the freeze/re-baseline directly to `AI_TRYTeleportingChrToPad` firing, the precise, unambiguous signal for "a legitimate reposition just happened"; (c) separately, find the actual N64-vs-PC divergence point per M-165/M-166 (log `aidx=`/`animframe=`, already wired into `D243M: onscreen`) to determine whether a real, permanent fix is even a `port/`-layer timing correction or needs a rule-2 sign-off for a genuine behavioral difference — this determines which of (a)/(b) is worth turning into a real fix vs. a stopgap. |
| ~~**D149**~~ | ~~Front-end MISSION COMPLETE / mode-select 3D models (dossier, wallets) render as garbled full-screen geometry or not at all.~~ | **CLOSED — user-verified fixed (M-87 playtest).** No longer reproduces; presumably fixed as a side effect of other front-end/D75-family work since M-49. D75 family. §F D146. |
| **D170** | Named-NPC "flee" scripted behavior looks wrong on PC: on Silo, Ourumov breaks into a full run the instant he alerts ("Ourumov: Kill him!") rather than the N64 walk/retreat; developer reports Trevelyan behaves the same way on Cradle. **M-49 full playthrough: escalated — Cradle (`-level_51`) is "bugged out completely" from Trevelyan's behavior; the final level's scripted progression breaks. Now folded into / superseded by §F D193 (NPC AI locomotion too slow).** | **Observed, not investigated** (M-34, from a Silo playtest capture; user bug report — no fix wanted yet). Could be a genuine ailist / `chrTick` divergence, OR the D155/D156 wall-clock `deltaFrames` locomotion-anim speed-up (a catch-up spike playing the walk cycle at 2–3×). **Verify first:** capture the same beat against N64 reference footage before assuming it diverges — Ourumov may be scripted to flee. If it's real, suspect the frametiming clamp (§F D155/D156) or the Cradle-family AI path (§F D123). |
| **D171** | Silo missile-shaft: a large translucent grey triangle sweeps across the lower frame while panning down the shaft (~3 s of the capture). Looks like a stray poly / near-plane clip fan, same family as D106 (portal near-plane min>max). | **Observed, not investigated** (M-34, Silo capture). Cosmetic, intermittent, tied to camera angle in the open vertical shaft. |
| **D171** | Silo missile-shaft: a large triangle sweeps across the frame while panning down the shaft. **Promoted to `docs/dev/findings.md` §F D288 (M-141)** with a user-reproduced, precisely-bounded, visually-documented repro (frames ~562-741 of a bare `-level_20` boot) — read that entry for the current state, not this row. | See D288. |
| **D172** | Bullet-impact / blood particle spray renders magenta or cyan instead of dark red (visible on guards hit at close range on Silo). | **M-82: ROOT CAUSE FOUND + FIXED (PR #34).** `GE_D172` probe on a user Silo firefight: 40/40 particle tris configure tile 1 = RGBA16 fire @ tmem 392 but fast3d **samples tile 0 (smoke) for both texunits** — `gfx_lod_tile_offset()` returns 0 unconditionally (D107 blurry-ceiling fix; `video.c:225` disables `gfx_detail_textures_enabled`). `G_CC_INTERFERENCE` = `smoke*smoke` → magenta/cyan. Fix: `return rdp.tex_lod ? 0 : i` — only fold to base tile for real LOD/mip draws. (0xB9 + cycle-type were both false trails, ruled out earlier in M-82.) **Owed: in-game eyeball — blood should be dark red.** Detail: `D172-PARTICLE-COLOR-RESEARCH.md`. |
| ~~**D178**~~ | ~~Objectives not listed correctly on the pre-mission briefing screen (the mission overview shown before starting a level).~~ | **CLOSED — user-verified fixed (M-87 playtest): "main menu text is good now."** No longer reproduces. M-36 triage below kept for reference. **M-36 triage: the objective lines are BLANK, not wrong.** `GE_STARTMENU=10` Dam briefing renders the header fine ("OHMSS / Agent: James Bond / Mission 1: Arkangelsk / Part i: Dam / PRIMARY OBJECTIVES:") then just the bullet "a." with no text and no b/c/d. → **D143/D151 blank-text family** (`langGet()` → NULL → silent blank), *not* a propDef walk / difficulty decode. **Family B** in `docs/dev/M-36-TRIAGE.md` — find which lang bank the front-end briefing flow fails to load. Re-confirm the exact shape in a *real* briefing (the `GE_STARTMENU` boot skips real save state). Close relatives: D151 (watch objective text blank — type-35/23 `text` slot u16→u32 fix, M-28; interactive re-verify was still pending), D143 (briefing/objective text blank via `langGet` NULL), D157 (objective difficulty byte offset — could make difficulty-gated objectives appear/disappear wrongly). The pre-start briefing is a *different surface* from the in-level watch, so D151's fix may not cover it. **Next:** ask user which level + what exactly shows vs expected; compare against `docs/dev/LEVEL-OBJECTIVES.md` (regenerated post-D157); if lines are blank → D143/D151 family (`langGet` NULL guard = silent blank), if wrong objectives listed → propDef type-23 record walk / difficulty decode. |
| **D175** | In-game stutter / brief hang during normal play — observed opening a door on Runway (mission 3, `-level_35`) and also on Surface. No backtrace captured (gdb attach missed the window; it self-recovered). | **Observed, not investigated** (user QA report). Matches known transient-hang classes rather than looking new: D155 (post-slow-load catch-up spiral — clamped), D156 (anim NaN loop — guarded, prints one-shot stderr diag if it fires), D134 (SP/DP task-done event swallowed — fixed M-22, Runway was an original victim), D147/D152 (audio `s_imLock` steal = ~2 s hiccup). Door-open specifically = new room first visible → one-off texture-import/frame spike (likely benign). **Next:** reproduce under `tools_pc/debug.ps1` with the box idle; check console for the D156 stderr diag; if it recurs, attach gdb mid-hang and pattern-match `bt` (import_texture = benign slow frame; model.c:3131 spin = D156; pthread_mutex_lock on s_imLock = D147/D152). |
@@ -25,7 +25,7 @@ once the game is playable start-to-finish without crashes.
| ~~**D233**~~ | ~~All levels (intermittent): near doors — especially doors you can't see through to the inside — the indoor level geometry vanishes and you can see straight through it into the room, while world objects (computer terminals, furniture) in that space still draw.~~ | **FIXED + VISUALLY VERIFIED (M-107 fix, M-108 verify).** `port/fast3d/gfx_pc.cpp` `gfx_sp_tri1` was trivial-rejecting visible triangles that had a behind-camera (`w<0`) vertex. Status cell here was stale at "not investigated" — corrected M-119. §F D233. |
| ~~**D234**~~ | ~~Dam: the guard towers along the actual dam structure are invisible except for the first one.~~ | **FIXED + VISUALLY VERIFIED (M-108).** Same underlying mechanism as D233. Status cell here was stale at "not investigated" — corrected M-119. §F D234. |
| **D235** | Wrong textures load after dying/restarting a level — clarified: not Facility-specific, seen on multiple levels; trigger is death/restart (level reload), not a general build regression. | **Observed, not investigated** (M-106, user QA report, clarified same session). This is a state-leak on level re-entry, not a one-level texture-decode bug — prime suspect is the texture pool / CI-cache (D217/D228 neighbourhood) not fully resetting between a level teardown and reload (stale arena base, a palette-cache entry keyed by a reused address, or a texpool index not rewound). Needs: confirm it does NOT repro on a fresh level boot (only after death/restart); which surfaces; a screenshot. Next: instrument `texInitPool`/the CI-cache clear path for what does/doesn't reset on reload vs cold boot. |
| **D236** | Surface 1: the 2D billboard trees that appear at the start of the level and around the map don't draw as discrete tree sprites — they render as a wall/sheet of tree texture instead of the proper 2D tree objects. | **Observed, not investigated** (M-106, user QA report). Billboards = camera-facing quads; "wall of tree texture" reads as either (a) the billboard orientation/quaternion missing so all quads emit coplanar in world space, or (b) a UV/texture-rect setup error mapping the whole sheet onto each quad. Distinct from D176(b)'s tree-*line* backdrop static (that's the painted perimeter wall; this is discrete 2D tree objects). First checks: `GE_PCDUMP` at the Surface 1 opening; inspect one tree billboard's emitted vertices + texture coords in fast3d; check whether the quads are camera-facing on N64 reference footage. |
| **D236** | Surface 1: the 2D billboard trees that appear at the start of the level and around the map don't draw as discrete tree sprites — they render as a wall/sheet of tree texture instead of the proper 2D tree objects. | **OPEN — second pass active (D266–D269); first-pass decode fix was reverted after a negative user re-test (D265)** (M-106, user QA report). The earlier IA16 framing was a misidentification: the cards are flat quads by design (no billboard opcode; byte-matched ROM emission) and bind an opaque IA8 64×16 strip plus an alpha-blended **IA4 128×16** strip (texdump r117) that is a row of ~13 faint conifer silhouettes. The wall came from `import_texture_ia4` decoding the RDP's I2:A2 nibble as I3:A1 — A2=1 canopy pixels went fully opaque, A2=2 dithering vanished (bug inherited verbatim from the PD port). **Second pass (D266–D269) re-framed the bug: the visible wall band is CI8 32×32 gradient/blob cards, not IA at all** — two draw classes per room stream (OPA + XLU), BILERP/TEXTLOD/2-cycle/**FOG-on**; texel-alpha blending proven correct by force-alpha A/B (D268). Current ranked suspects: fog range/application (D218 family) > TEXTLOD/mip sampling > draw density/culling > palette. The I2:A2 decode + AC-aware edge fixes are re-applied in-tree (uncommitted) and still owe a game-wide IA4 regression sweep before commit. |
| **D249** | Streets and Egyptian (the two biggest/most-open levels) show culling problems on far-away geometry at *default* FOV — distant geometry drops/pops incorrectly. Also a QoL ask: improve LOD in general, but gate it behind a toggle so N64-accurate ("old school") LOD stays selectable. | **Observed, not investigated** (M-119, user QA report). Distinct from D222 (high-FOV near-edge culling) and D218 (fog/far-clip tint). Two candidate mechanisms: (a) portal/room-visibility cutting off too aggressively at range on large multi-room layouts (D233/D234 neighbourhood — check for a far-plane/depth-range sibling to D233's w<0 trivial-reject fix), or (b) GE's own LOD/detail-swap system not ported faithfully (mip-tile / simple-model substitution at distance, `gfx_lod_tile_offset` family). First checks: `GE_PCDUMP` a long sightline on Streets/Egyptian where geometry visibly pops; determine which of (a)/(b) is firing before designing the fix or the requested toggle. §F D249. |
| **D217** | 1P weapon viewmodel + character-model CI textures render with a wrong palette on many levels / multiple weapons — PP7 grip + hand render flat green on Facility & Depot, "weird or missing" gun textures elsewhere; Bond's watch face too. Surfaced by D215 (viewmodel now draws). Clean on Bunker1. | **Defect ① FIXED (M-90), pending human playtest eyeball — full detail in `findings.md` §H "D217 — M-90" and §F D217.** Root cause: weapon texpool arena bases land %8==4 on PC (memp bump drift from pointer bloat); `texAlignIndices` absolute 8-byte row padding then shifts every palette 4 bytes early while model-DL `G_LOADTLUT` reads the N64 offset (grip count=1 load lands on next tenant's header slot → `0x1A00` green; larger loads read a 2-entry zero pad — the M-86 tell). Fix: `texInitPool` rounds the base up to 8 under `#ifdef PORT` (no-op on N64); 21/21 sweep PASS. Two defects: ① **visible green = misaligned texpool arena** (fixed as above). ② **CI texture-cache collision** (no palette-content key) — proven, **fix landed on branch `fix/d217-model-texture-palette` `f6fc0399`** (palette FNV-1a hash in the CI cache key), owes a 21-level `verify.sh sweep`; does not fix ①. |
| **D174** | "No blood effect" — user reports no visible blood when shooting guards / into surfaces. | **User QA report; likely D120, unverified.** Decals: the D120 `PointUsage[]` converter fix (M-30) was never interactively verified and only takes effect after a full sidecar regen (`d43 && d69 && d88 --regen`) — check whether the playtest build had regenerated sidecars before concluding the fix failed. Spray: D172 observed the particle spray *drawing* (wrong color, magenta/cyan) on Silo — if the user sees no spray either, that's new info (different weapon/range, or the D172 decode is worse than "wrong color"). First step: BUNKER1 firefight with regenerated sidecars; if decals still absent, instrument the `chr.c:3328` decal walk. |
File diff suppressed because it is too large. Load diff
-287
View File
@@ -1,287 +0,0 @@
# Widescreen & FOV Plan
Status: **proposal, parked** — like `UNLOCKED-FPS-PLAN.md`, resume after audio
work (project Phase 3). Research current as of this writing.
Scope decision (agreed): **target exactly two display aspects — 4:3 and 16:9.**
Arbitrary aspect ratios are TBD and out of scope; the window manager should
snap/letterbox to one of the two rather than trying to serve anything else.
Standard (agreed): **Option B (native 16:9 render, PD-parity) is the goal.**
The PD PC port's widescreen architecture is the reference standard for this
engine family. Option A (4:3-internal + stretch) remains an acceptable
**case-by-case workaround** only where B proves impractical for a specific
surface — each such fallback documented individually.
## Problem
At high resolutions (e.g. 4K) in a 16:9 window the game is disorienting. Root
cause, confirmed in code: the port renders in *window-resolution space*. All
320×240 logical screen coordinates are scaled per-axis by
`RATIO_X = win_w/320`, `RATIO_Y = win_h/240` (`port/fast3d/gfx_pc.cpp:53-54`).
In a 16:9 window everything — world geometry **and** HUD — is stretched 4/3
horizontally: circles become ellipses, and the horizontal FOV balloons from
~78° to ~91° while vertical stays 60°. That stretch is the disorientation.
## Research findings
### 1. The FOV model (where FOV lives)
- Vertical FOV is a fixed 60° base: `FOV_Y_F` (`src/fr.h:137`); per-player
`fovy`/`aspect` fields default to 60 / `DEFAULT_ASPECT`
(`src/game/player.c:454-455`).
- Setters: `viSetFovY` / `viSetAspect` / `viSetFov(fovx, fovy)`
(`src/fr.c:911-934`) → `guPerspectiveF(g_viProjectionMatrixF, …, fovy,
aspect, znear, zfar, 1.0f)` (`src/fr.c:718`).
- **FOV is dynamic**: aim zoom rewrites fovy every frame via
`viSetFovY(g_CurrentPlayer->zoominfovy)` (`src/game/bondview2.c:3123`), and
the camera path resets fovy/aspect per frame
(`bondviewMovePlayerUpdateViewport`, `src/game/bondview2.c:7967-8034`).
⇒ Any FOV option must be a *multiplicative correction composable with zoom*,
not an override of a constant.
### 2. The game ships with a (stretch-based) 16:9 path
This decomp contains a player-facing widescreen path from the original:
options-menu **"Ratio": Normal / 16:9** (`GAME_OPTIONS_INDEX_RATIO`, string
`OPTION_STR_23_169_LF`, `src/game/options.c:129`), save bit
`OPTION_SCREENRATIO` (`src/game/file2.c:1266-1270, 1323`),
`get/set_screen_ratio()` (`src/game/options.c:553-558`). When active, the
camera sets `aspect = (vp_w/vp_h) × 0.75 × 16/9` (= viewport ratio × 4/3,
`src/game/bondview2.c:8007-8034`) — the pre-distortion for a **4:3 render
stretched to 16:9** (N64-out-4:3, TV stretches). HUD compensation exists for
crosshair (`gunfire.c:6316-6318`, half-width ×0.75) and rocket aim
(`gunfire.c:501`); EU-only extras need an NTSC audit.
⇒ This path is exactly **Option A's** game-side machinery. It is *not* the
PD standard (which renders native 16:9), so under Option B it stays dormant
except as fallback.
### 3. Current port rendering pipeline (what to build on)
- Render happens at window size; an FBO path already exists and activates when
internal dims ≠ window dims or MSAA > 1 (`gfx_pc.cpp:3041-3115+`).
- Native-res bookkeeping: `GE_NATIVE_W/H` = 640×480 (NTSC) / 640×400 (PAL)
(`port/src/video.c:40-44`); VI size requests clamp to ≤640
(`port/src/libultra.c:816`). Auto window defaults to 4:3
(`port/fast3d/gfx_sdl2.cpp:107`).
### 4. fast3d EXT aspect commands — already present in GE's RSP
GE's software RSP (adapted from PD's) implements Rare's PC-port extension
geometry-mode commands `G_ASPECT_{LEFT,RIGHT,WIDE,CENTER}_EXT`
(`gfx_pc.cpp:1954-1979`, decoder at `gfx_sp_extra_geometry_mode`), and
`port/fast3d/gbiex.h` carries the `gSP…EXT` command macros. **The RSP side of
the PD standard already works in this codebase**; only the game-code emission
sites are missing.
### 5. The PD standard (reference architecture — what Option B replicates)
From the `pd_port` checkout (N64 PD had no widescreen; the port added it):
1. **Projection aspect comes from the port**: `videoGetAspect()` returns the
real window aspect (`gfx_current_dimensions.aspect_ratio`,
`port/src/video.c:232-235`). Game code calls `viSetAspect(videoGetAspect())`
(e.g. `title.c:2495`; even `#define TITLE_ASPECT (videoGetAspect())`).
Native 16:9 projection ⇒ correct perspective, vertical FOV preserved.
2. **Per-element screen-space corrections** where an element is positioned in
screen coords: `sightGetAdjustedX(x)`, `×(SCREEN_ASPECT / videoGetAspect())`
scalings (`sight.c:77,921`, `sky.c:2728+`, `menu.c:2332,4771`,
`player.c:3108`).
3. **HUD alignment via the EXT commands**: screen-space draw segments wrapped
in `gSPSetExtraGeometryModeEXT(gdl, G_ASPECT_CENTER_EXT)` …
`gSPClearExtraGeometryModeEXT(…)`, all under `#ifndef PLATFORM_N64`. With
the mode active, fast3d scales 4:3-logical screen coords by the *native*
aspect and centers (or left/right-aligns) them in the wider window — no
distortion. PD scale: **52 emission sites across 10 game files** (sight,
hudmsg, bondgun, menu, radar, title, propobj, credits, activemenu,
game_1531a0).
4. User-facing: vsync/framerate options + HUD-centering option
(`g_HudCenter` → `g_HudAlignModeL/R`, `port/src/main.c:77-83`).
**M-87 PD-legacy-survey notes (`docs/dev/notes/PD-LEGACY-SURVEY.md`):**
- **Overscan/safe-frame (candidate 2, deliberate divergence, recorded here):**
PD hardcodes its native viewport to 320×220 (`port/src/video.c:77-79`) — the
N64-on-a-TV visible area after ~20 lines top/bottom of overscan. **GE's port
renders the full 640×480/640×400 frame, no overscan crop** (`port/src/video.c:38-47`)
— a deliberate PC-default choice (nothing hidden, no HUD-clipping risk from a
TV-only limitation), not an oversight, and not something to "fix" toward PD's
behavior. Net effect: GE shows ~9% more vertical content than PD at the
equivalent native resolution. Revisit only if a `Video.SafeAreaOverlay` debug
knob is ever wanted to visualize the original TV-safe region.
- **"Ratio" 4:3/16:9 option (candidate 3) — correction, M-87 review:** PD
deleted its N64-era per-player Ratio dropdown on PC (`options.c` forces
`SCREENRATIO_NORMAL` outside `PLATFORM_N64`). GE's equivalent (`SCREEN_RATIO_16_9`,
`get/set_screen_ratio`, `options.c:553-558`, persisted per-save, consumed by
the camera) is a live, functioning code path — but it's an old **anamorphic
stretch toggle**, not true wider-FOV rendering, and this doc's own **Open
Questions** (below) already flag the tension and lean the *other* way: leave
this legacy flag **dormant** and have a **separate, new, port-owned
`Video.Aspect` config** be Option B's actual trigger — the same
separation-of-concerns choice PD itself made (PD didn't repurpose its legacy
flag either). Repurposing the shipped save-flag to drive a brand-new render
path risks desync (existing saves silently gaining/missing the modern
feature, or the flag and `Video.Aspect` disagreeing). **Not yet decided**
("Decide Phase 1" below) — correcting an earlier note here that overstated
this as settled.
## Option B — the standard (native 16:9, PD-parity)
### Architecture
Render directly at the 16:9 window resolution. World: projection aspect =
window aspect (vertical FOV preserved, no post-stretch). HUD/screen-space:
4:3-logical coordinates mapped undistorted via the EXT aspect modes
(centered by default; left/right alignment available à la PD). 4:3 windows:
byte-identical to today.
### Change inventory
**Port layer (no rules issues):**
- [ ] `videoGetAspect()` equivalent in `port/src/video.c` (return window
aspect; GE already tracks `gfx_current_dimensions.aspect_ratio`).
- [ ] Window/aspect policy: `Video.Aspect` {4:3, 16:9}, snap on create/resize;
out-of-scope aspects → nearest + note (TBD).
- [ ] Make `port/fast3d/gbiex.h` reachable from game TUs (include path or a
shim header) — build-system only.
- [ ] HUD alignment mode plumbing if we adopt PD's center/wide option
(`g_HudAlignModeL/R` pattern) — decide in Phase 1; default = centered.
**Game code (exception class — see below):**
- **(a) Projection aspect path.** `bondviewMovePlayerUpdateViewport`
(`bondview2.c:8007-8034`): under PORT + 16:9, use window aspect instead
of the ×4/3 stretch formula. Plus the menu/title/briefing call sites
`viSetFovY(FOV_Y_F); viSetAspect(ASPECT_RATIO_SD)` (`front.c` — ~7+
sites; enumerate in Phase 1) → PD pattern `viSetAspect(videoGetAspect())`.
- **(b) HUD / screen-space draw sites.** Audit + wrap with EXT aspect modes
and/or per-element corrections, per surface: gameplay HUD (crosshair —
replaces the existing ×0.75 compensation; ammo/timer/text via
`textrelated.c`), menus (`front.c`), title/briefing/credits, letterbox
bars (`fr.c` gDPFillRectangle calls). GE's known screen-space emitters:
`front.c`, `textrelated.c`, `gunfire.c`, `bondwalk2.c` (initial grep;
Phase 1 completes the audit). Expect PD-comparable scale (~10 files),
possibly less.
- **(c) Anything the audit surfaces** (e.g. mouse→screen mapping for menus,
cutscene camera framing) — each item logged individually.
### Rule #2 exception framing (needs explicit sign-off to start Phase 2)
This is a behavior-affecting game-code edit class, outside the ABI-only D3x
exception. The case for granting it:
1. **Direct sister-engine precedent**: PD's port made exactly these edits on
the same Rare engine/fast3d; the project designates PD as standing
reference for port patterns. We replicate a proven, bounded change class,
not inventing one.
2. **Bounded and gated**: every edit is `#ifdef PORT` (and where sensible
runtime aspect-gated) so the N64 build and the 4:3 default path stay
byte-identical; golden/determinism tests unaffected at 4:3.
3. **The decomp already contains** both a shipped 16:9 branch and `#ifdef
PORT` precedents in game files — the pattern is not foreign to this tree.
Conditions: explicit user sign-off before Phase 2; each change (or tight
cluster) gets a findings.md entry with §F index label citing the PD analogue;
4:3 byte-identity verified per phase; any edit that would alter *sim* behavior
(not just presentation) stops the work and comes back for review.
### Fallback (case-by-case, documented)
If the Phase 1 audit shows a specific surface is disproportionately costly or
can't be made clean under B (candidate: complex cutscene framing), that
surface may fall back to **Option A** — 4:3-internal render + edge-to-edge
stretch using the game's shipped `SCREEN_RATIO_16_9` path (`set_screen_ratio`
is an existing API; FBO+blit infra exists) — recorded as a finding with the
reason. Whole-feature fallback to A only if B fails verification overall.
## FOV adjustment design (port-only, applies to both options)
- **Hook: software RSP projection post-scale.** Wherever the RSP applies the
uploaded projection matrix, scale its vertical row by
`tan(fov_orig/2) / tan(fov_adj/2)`. Zoom rewrites the matrix every frame, so
post-scaling whatever arrives composes with aim zoom for free. No game code.
- Rejected: editing `viSetFovY` call sites (game code); writing
`g_ViBackData` from the port (racy).
- UI: `Video.FovScale` (percent of original vertical FOV, e.g. 50–150),
default 100 = byte-identical to today.
## Phases
### Phase 0 — Verification research (port-only, read-mostly)
- [ ] Playtest the in-game Ratio option as-is on PC; document + screenshot
what the shipped stretch path does under the current pipeline.
- [ ] Measure baseline FOVs (4:3 and current 16:9-stretch) from the projection
matrix or a calibration render; record numbers.
- [ ] Confirm `gbiex.h` EXT commands round-trip through GE's RSP with a test
DL (they should — decoder exists).
- [ ] Audit EU-only aspect constants (`g_GunSightAspectRatio`,
`EU_CAMERA_8003642C_ASPECT`) for NTSC/PAL effect.
- [ ] Record a Dxxx finding consolidating baselines.
### Phase 1 — Port foundation + full change audit (port-only)
- [ ] `videoGetAspect()`, window/aspect policy, gbiex.h include path.
- [ ] Complete the screen-space draw-site audit for inventory item (b): every
emitter, its coordinate assumptions, proposed wrap/correction, PD
analogue cited. Output = the signed change list for Phase 2/3.
- [ ] Decide HUD alignment default (centered vs PD's center/wide option).
### Phase 2 — Projection aspect (game-code exception class, small wave)
- [ ] Item (a): camera path + front.c call sites, `#ifdef PORT`-gated.
- [ ] Verify: world perspective correct at 16:9 (FOV measurement vs Phase 0),
aim zoom composes, 4:3 byte-identical.
### Phase 3 — HUD alignment wave (game-code exception class)
- [ ] Item (b), per surface in order: gameplay HUD → menus →
title/briefing/credits → letterbox/misc. Each surface: change, visual
verification at 4:3 + 16:9, finding entry. Fallback-to-A decisions
happen here, per surface, with documented reason.
### Phase 4 — FOV slider (port-only) — **LANDED (M-83, D211)**
- [x] RSP projection post-scale (`gfx_apply_fov_scale()` in
`port/fast3d/gfx_pc.cpp`, load-path only); `Video.FovScale` config
(percent of vertical FOV, 50–150, default 100 = byte-identical) + F10
overlay row. Perspective-only (ortho/HUD untouched); columns 0+1 scaled
together so aspect is preserved. Build clean, `bunker1` no-op confirmed.
- [ ] Owed: interactive eyeball of composition with aim-zoom at scale ≠ 100.
This phase does **not** depend on the Phase 2/3 game-code exception — it
shipped standalone ahead of the rest of the plan.
### Phase 5 — Verification & sign-off
- [ ] FOV measurements per mode/scale vs Phase 0 baselines.
- [ ] Full HUD pass at 4:3 + 16:9; cosmetics → `GRAPHICS-BACKLOG.md`.
- [ ] 4K performance check; MSAA interaction; config persistence; screenshots.
- [ ] `./build-pc.sh` + `/linkcheck`; findings complete with §F index labels.
## Open questions
- HUD alignment UX: always-centered (simple) vs PD's center/wide option
(more faithful, more surface area). Decide Phase 1.
- In-game Ratio menu entry under Option B: the shipped option means "stretch
mode" — keep it as the *fallback* trigger, hide it, or repurpose? Decide
Phase 1 (leaning: leave dormant; port config owns display mode).
- Mouse/menu coordinate mapping at 16:9 (PD had `videoGetAspect()`-scaled
mouse math in menu.c) — confirm GE's menu input path needs the same.
- Ordering vs `UNLOCKED-FPS-PLAN.md`: both parked behind audio; B renders
native so the FPS plan's present-stage duplication slots in unchanged.
Decide order when audio lands.
- PAL: `GE_NATIVE` 640×400 bookkeeping — confirm aspect math per region.
- Split-screen viewports under 16:9 — likely out of scope for PC; note and close.
- **M-87 QoL ask (user, not critical, backlogged):** expose `Video.FovScale` as
a real horizontal-or-vertical **degree** value instead of the current
percent-of-original-vertical-FOV scale (D211) — the "50–150%" knob doesn't
tell the user what degree FOV they're actually getting. Needs: pick a
convention (report vertical, since that's what `frFovY` actually is post-D211
refix — `fr.c:737`) and either (a) relabel the F10 row + INI value as degrees
directly (`frFovY = clamp(requestedDeg, ~24°, 160°)`, drop the percent
multiply), or (b) keep percent internally but show the *computed* degree
value live in the F10 overlay/INI comment for transparency. (a) is cleaner
and removes a level of indirection; check whether anything else reads
`Video.FovScale` as a percent before renaming the key (search besides
`fr.c`/`video.c`). Cosmetic/UX only, no render-path change beyond the
input unit.
+43 -27
View File
@@ -3,7 +3,7 @@ title: Agentic Development
description: A case study in two AI coding agents porting a Nintendo 64 game to PC; the setup, timeline, handoff workflow, and an honest assessment of what worked.
---
# Agentic development: two AI coding agents porting GoldenEye 007
## Agentic development: two AI coding agents porting GoldenEye 007
*How this port was actually built: a local open-weight model (Qwen 3.8 on a
single RTX 5090) and a hosted frontier model (Claude / Claude Code) handing
@@ -68,7 +68,7 @@ All dates from this repository's own commit history (August 2026).
gantt
dateFormat YYYY-MM-DD
axisFormat %b %d
title GoldenEye 007 PC port; from fork to playable front end
title GoldenEye 007 PC port; from fork to v0.3.0
section Local model - Qwen 3.8 via pi
Fork + PC-port scaffolding :m1, 2026-08-16, 1d
Full compile + link (~230 TUs) :m2, 2026-08-20, 1d
@@ -84,6 +84,10 @@ gantt
SDL input layer (kbd/mouse/gamepad) :2026-08-29, 1d
Front-end flow (menu to briefing to start) :m7, 2026-08-30, 1d
File-backed EEPROM saves :2026-08-31, 1d
section Month one - front end to v0.3.0
Audio mixer (music + SFX) :2026-09-02, 7d
Steam Deck hardening + F10 overlay QoL :2026-09-14, 3d
v0.3.0 released (full campaign playtested) :milestone, 2026-09-18, 0d
```
- **Day 0** (16 Aug): repository forked, PC-port scaffolding added.
@@ -97,50 +101,59 @@ gantt
unattended play window without crashing.**
- **Day 14** (30–31 Aug): front end playable end to end (menu → mission
select → difficulty → briefing → start); file-backed saves.
- **Day 33** (18 Sep): **v0.3.0 released** — the full campaign playtested
end to end at Agent difficulty, audio complete, bundles for Windows,
Linux and Steam Deck.
So roughly **two weeks**, one person part-time, to take a decompilation from
"builds an N64 ROM" to "boots on desktop, renders every solo level, playable
through the front end into the early game"; with audio and a set of cosmetic
issues still outstanding.
through the front end into the early game". The rest of month one (29 Aug –
18 Sep) took it from there to a public release: the 21-level sweep to
full-campaign completion, the audio mixer, input and Steam Deck polish,
widescreen FOV scaling, and the v0.3.0 bundles — with a set of cosmetic
issues documented rather than outstanding.
## By the numbers
| | |
|---|---|
| Calendar time | 26 days (17 Aug – 11 Sep 2026), one person part-time |
| Commits on the port | ~430 |
| Root-caused bugs logged | `D1`–`D226` in [`findings.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/findings.md), 116 still-listed entries (some labels later merged or withdrawn) |
| Handoff sessions | 89+ (`M-2` … `M-89`, and counting) |
| Game-source files given `#ifdef PORT` ABI edits | 67 files, ~305 blocks |
| New port-layer / tooling files | ~114 |
| Port layer | ~21,900 lines C/C++ (`port/`) |
| PC asset-conversion tooling | ~7,050 lines Python (`tools_pc/`) |
| Outcome | intro + all 21 solo missions render and are crash-free, front end playable, in-level SFX + menu/intro music play; in-level music and full-campaign completion re-confirmation still owed |
| Calendar time | 34 days (16 Aug – 18 Sep 2026), one person part-time |
| Commits on the port | ~690 |
| Root-caused bugs logged | `D1`–`D311` in [`findings.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/findings.md), 190 tracked entries in the index (some labels later merged or withdrawn) |
| Handoff sessions | 190+ (`M-2` … `M-191`, and counting) |
| Game-source files given `#ifdef PORT` ABI edits | 71 files, ~385 blocks |
| New port-layer / tooling files | ~119 |
| Port layer | ~24,450 lines C/C++ (`port/`) |
| PC asset-conversion tooling | ~7,180 lines Python (`tools_pc/`) |
| Outcome | **v0.3.0 released**: full campaign playable end to end (all 21 missions playtested at Agent difficulty) at a steady 60 fps, full audio (music + SFX), file-backed saves; Windows, Linux and Steam Deck; known cosmetic defects documented in the finding log |
### Commit velocity
```mermaid
xychart-beta
title "Commits per day"
x-axis ["8/16", "8/17", "8/20", "8/21", "8/22", "8/23", "8/24", "8/27", "8/28", "8/29", "8/30", "8/31", "9/1"]
y-axis "commits" 0 --> 60
bar [1, 2, 2, 6, 9, 2, 4, 7, 57, 39, 39, 46, 9]
x-axis ["8/16", "8/17", "8/20", "8/21", "8/22", "8/23", "8/24", "8/27", "8/28", "8/29", "8/30", "8/31", "9/1", "9/2", "9/3", "9/4", "9/5", "9/6", "9/7", "9/8", "9/9", "9/10", "9/11", "9/12", "9/13", "9/14", "9/15", "9/16", "9/17", "9/18"]
y-axis "commits" 0 --> 90
bar [1, 2, 2, 6, 9, 2, 4, 7, 57, 39, 39, 46, 11, 52, 39, 8, 8, 2, 8, 34, 17, 30, 20, 30, 57, 4, 84, 38, 22, 13]
```
Days with no commits are omitted. The 8/25–8/26 gap is between phases; the
step up from 8/28 onward is the collaborative phase in full swing, including
the parallel multi-agent "bursts" used near the end.
Days with no commits are omitted (18–19 and 25–26 Aug). The step up from
8/28 onward is the collaborative phase in full swing, including the parallel
multi-agent "bursts"; the 9/15 spike is the Steam Deck playtest-feedback
batch landing at once.
### Who did what
```mermaid
pie showData title "Commits by agent (raw count)"
pie showData title "Commits by agent (raw count, snapshot through early September)"
"Claude" : 161
"Local model (Qwen 3.8 / pi)" : 62
```
(Phase A, the first 26 commits, to 24 Aug, was entirely the local model,
solo; from 27 Aug on the two agents worked in parallel.)
solo; from 27 Aug on the two agents worked in parallel. The raw tally was
not re-run for the final third of the project; treat the milestone-weighted
estimate below as the better number.)
| Milestone / workstream | Primary agent | Weight |
|---|---|---|
@@ -208,9 +221,11 @@ let them substitute for each other:
usage limit mid-problem, the local model picked the task up from the
HANDOFF state and continued; when the local model hit a bug that needed
deeper structural reasoning, it wrote up where it was and Claude took over.
2. **`findings.md`**; the chronological finding log. 162 numbered entries,
each a root cause with `file:line` evidence and the fix. New agents (either
model) are pointed at the relevant entries before they start.
2. **`findings.md`**; the chronological finding log. 311 numbered `D`
entries by v0.3.0 (190 tracked in the index; some labels merged or
withdrawn), each a root cause with `file:line` evidence and the fix. New
agents (either model) are pointed at the relevant entries before they
start.
3. **`porting-notes.md`**; the append-only "recurring bug classes" file. The
single highest-leverage artifact: it stopped both models from
re-deriving the same class of N64→PC bug over and over.
@@ -262,7 +277,8 @@ Honest notes, for anyone weighing whether this transfers.
to Opus 5. Each tier earned its place on the problems the tier below it
couldn't close.
**Still outstanding** (parked below crash/level work): audio is not
implemented; several front-end 3D transforms and some text rendering are
wrong. See
**Still outstanding** (the v0.3.0 known-issues list): cosmetic rendering
defects (particle colours, Surface 1 billboard trees, front-end logos, water
seam), stretched-not-native widescreen, PAL/JP support, a controller
rebinding UI, and macOS/ARM builds. See the README's Status section and
[`GRAPHICS-BACKLOG.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/GRAPHICS-BACKLOG.md).
+125 -68
View File
@@ -8,136 +8,193 @@ D51-D58,RESOLVED,"font pixeldata, osGetCount tick rate, model RW pools, cseq BE
D59-D68,RESOLVED,"intro render: blood-RLE clobber, DMA validate, OSMesgQueue, HEADS/BODIES sentinels, romCopy width, image_entry, Globalimagetable BE→LE",resolved
D70-D74,RESOLVED,"intro-logo pixels: C-array bswap, UV path, sinf/cosf DVAL(), texture-import truncation",resolved
D75,OPEN,front-end 3D model transforms (D77 audio split out below —,"OPEN (parked). M-32 triage (see §F ""D75 ADDENDUM""):"
D76 · D164,PROPOSED,disclaimer/legal screen only draws line 1 —,fix proposed (not applied —
D76 · D164,FIXED,Legal screen only draws line 1 —,"FIXED — this row and the ## D164 entry below were stale; the proposed one-line #ifdef PORT fix was actually committed 8dfc578f4 (2026-08-31), well before this correction (2026-09-18)."
D159,FIXED,"front-end wallet-Bond photo ""interlaced""/combed (RC1 / D149) —",FIXED (#ifdef PORT no-op the swap in image.c)
D161,FIXED,Depot (-level_30) ceiling = bright-blue speckle + radial rays (B2).,FIXED (port/fast3d/gfx_pc.cpp)
D165 · D166,FIXED,"input-layer polish (M-31, port-only, port/src/input.c):",FIXED
D180 · D181,PARTIAL,"native-PC input pass (QoL run, qol/native-pc-input-menu):","LANDED, config-gated, feel-checks owed"
D165 · D166,FIXED,"Input-layer polish (M-31, port-only):",FIXED
D180 · D181,PARTIAL,Native-PC input pass (QoL run):,"LANDED, config-gated, feel-checks owed"
D186,PARTIAL,"Video.FpsCap throttles the sim, not just presentation (M-39 diag, M-42 clamp).","CLAMP LANDED (port/fast3d/gfx_sdl2.cpp, port/src/video.c); real fix owed"
D187,FIXED,"HUD ammo-type/weapon icon vanished at -O2 (M-46 root-cause, M-47 fix + verify).","FIXED (src/game/gunfire.c, #ifdef AVOID_UB); PR #19"
D188,FIXED,Linux -O2 boot crash:,"FIXED (src/libultrare/libc/xprintf.c, src/sprintf.c, #ifdef PORT); PR #22"
D189,FIXED,Linux -fstack-protector aborts on the decomp's latent stack-buffer overruns (M-48) —,"FIXED (CMakeLists.txt, src/game/stan.c #ifdef PORT); PR #23"
D190,?,"Linux -level_45 SIGSEGV at load in add_ptr_to_objective (objective.c:64) (M-48, open).","CLOSED — not a port bug; level 45 is MP-only and has no solo setup file in the ROM (root-caused M-105, reproduced on Windows)."
D191,FIXED,Windows -O2 release-bundle crash:,FIXED + PLAYTEST-VERIFIED (M-79).
D193,FIXED,NPC AI locomotion is too slow —,FIXED + user-playtest-verified (M-81) —
D194,PARTIAL,Mouse input needs another tuning pass —,PARTIAL — M-120 human playtest:
D195,FIXED,"Transparency / alpha surfaces broken on Control (M-49, user playtest) —","FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_vertex) —"
D197,?,"Character face/head textures wrap around the head, seen on Silo (M-49, user playtest).",CLOSED — user playtest v0.2.0-pre
D190,CLOSED,"Linux -level_45 SIGSEGV at load in add_ptr_to_objective (objective.c:64) (M-48, open).","CLOSED — not a port bug; level 45 is MP-only and has no solo setup file in the ROM (root-caused M-105, reproduced on Windows)."
D191,FIXED,Windows -O2 release-bundle crash:,FIXED + PLAYTEST-VERIFIED (M-79).…
D193,FIXED,NPC AI locomotion is too slow —,FIXED + user-playtest-verified (M-81) —…
D194,PARTIAL,Mouse input needs another tuning pass —,PARTIAL — M-120 human playtest…
D195,FIXED,"Transparency / alpha surfaces broken on Control (M-49, user playtest) —","FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_vertex) —…"
D197,CLOSED,"Character face/head textures wrap around the head, seen on Silo (M-49, user playtest).",CLOSED — user playtest v0.2.0-pre
D198,FIXED,alAdpcmPull/_decodeChunk used K0_TO_PHYS (an unconditional & 0x1FFFFFFF mask) on 64-bit heap pointers (src/libultra/audio/load.c).,FIXED (src/libultra/audio/load.c) —
D199,PARTIAL,Phase-3 software audio mixer:,"LANDED (port/src/mixer.c, port/include/mixer.h, include/PR/abi.h) —"
D200,FIXED,-level_09 segfault: reverb's ALDelay.input/output are u32 —,FIXED — 5-line cast in reverb.c (this session)
D196,FIXED,"OS mouse cursor stays visible after closing the F10 options overlay in click-to-lock mode (M-49, user playtest).","FIXED (6e758f1d, merged to main) —"
D192,?,Front-end mission/level-select grid pointer still can't reach the outer cells —,"CLOSED — the buggy mode 0 was removed entirely in the v0.2.0 prep pass (see D239); click-to-lock is now the only capture mode, so the unreachable-cells path no longer exists"
D221,FIXED,SELECT FILE screen: the mouse hit-box for a file slot is vertically offset from its visible box —,FIXED (M-91) — root cause was NOT a coordinate/origin mismatch:
D196,FIXED,"OS mouse cursor stays visible after closing the F10 options overlay in click-to-lock mode (M-49, user playtest).","FIXED (6e758f1d, merged to main) —…"
D192,CLOSED,Front-end mission/level-select grid pointer still can't reach the outer cells —,"CLOSED — the buggy mode 0 was removed entirely in the v0.2.0 prep pass (see D239); click-to-lock is now the only capture mode, so the unreachable-cells path no longer exists"
D221,FIXED,SELECT FILE mouse hit-box offset —,"FIXED (M-91) — explicit {min,max} coord2d pairs under #ifdef PORT; human mouse-playtest confirmed."
D222,FIXED,High Video.FovScale breaks culling —,"FIXED, awaiting visual verification (M-121)."
D223,FIXED,QoL ask: mouse-wheel weapon cycling should be directional like a normal PC shooter —,"FIXED (M-105, port/src/input.c) —"
D224,FIXED,"QoL gap (M-87, from a PD-PC-port survey):","DEPRIORITIZED (M-87, user call)."
D225,OPEN,"Dev-tooling QoL (M-87, user ask):","OPEN — root-caused, not implemented."
D223,FIXED,QoL ask: mouse-wheel weapon cycling should be directional like a normal PC shooter —,"FIXED (M-105, port/src/input.c) —…"
D224,?,"QoL gap (M-87, from a PD-PC-port survey):","DEPRIORITIZED (M-87, user call)."
D225,FIXED,"Dev-tooling QoL (M-87, user ask):","FIXED (2026-09-18) — GE_DEBUG_UNLOCKALL=1 short-circuits fileGetIsCheatUnlocked() (src/game/file2.c, #ifdef PORT, cached getenv) to always return TRUE; default off, no save mutation."
D226,OPEN,"QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text.","OPEN — root cause / call sites identified, not designed or implemented."
D227,FIXED,Sky renders as two independently-moving halves —,"FIXED (M-100b, PR #43) —"
D229,FIXED,"Water on IsWater levels renders green and pulses blue<->green on an interval (M-101, user playtest, Frigate -level_26; Surface 2 also an IsWater level).",FIXED (port-layer) — root cause:
D230,OPEN,In-level music quality:,"OPEN — static-analysis pass done (M-108), not runtime-confirmed."
D233,FIXED,All levels (intermittent):,"FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_tri1) —"
D234,FIXED,"Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).",VISUALLY VERIFIED FIXED (M-108) —
D244,OPEN,Self-hosted CI runner (corpyvt6agent01) link failure:,"OPEN, not yet fixed. Next:"
D228,FIXED,AK47 (and other) weapon models still show WHITE missing-texture patches after D217's texpool-alignment fix —,"FIXED (port/fast3d/gfx_pc.cpp, palette_to_rgba32) —"
D204,FIXED,PC audio ran ~2 % below real time permanently:,"FIXED (port/src/audio.c, port/src/libultra.c, port/include/audio.h) —"
D203,?,"Steam Deck (SteamOS, x86_64 Linux) v0.1.0 release bundle:",CLOSED — root cause + fix in D253.
D229,FIXED,"IsWater-level water renders green, pulses blue<->green (M-101; Frigate -level_26).","FIXED (port-layer) — ZLIB+CI8 water image now imports via the CI8 palette path (gfx_pc.cpp); Frigate verified live (""looks better""); sweep + Surface 2 by-eye still owed."
D230,FIXED,In-level music quality:,"FIXED (M-121, v0.2.0) —"
D233,FIXED,All levels (intermittent):,"FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_tri1) —…"
D234,FIXED,"Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).",VISUALLY VERIFIED FIXED (M-108) —…
D244,OPEN,Self-hosted CI runner (corpyvt6agent01) link failure:,"OPEN, not yet fixed. Next…"
D228,FIXED,AK47 white missing-texture patches after D217's texpool fix —,"FIXED (port/fast3d/gfx_pc.cpp, palette_to_rgba32) —"
D204,FIXED,PC audio ran ~2 % below real time permanently (AI feedback loop's 3 ms setpoint vs PC jitter).,"FIXED (port/src/audio.c, port/src/libultra.c, port/include/audio.h) —"
D203,CLOSED,"Steam Deck (SteamOS, x86_64 Linux) v0.1.0 release bundle:",CLOSED — root cause + fix in D253.
D201,FIXED,"src/libultra/audio/bnkf.c bank/instrument/sound/wavetable relocation offsets were s32, truncating the real 64-bit base pointer.",FIXED (src/libultra/audio/bnkf.c) —
D202,RESOLVED,"Silenced PPK plays a ""slap"" instead of a gunshot, PLUS user reports broader real-time audio corruption (M-52 → M-59 → REOPENED M-60 → M-61/M-62 closed the data…",ROOT CAUSE ESTABLISHED (M-66); DISPOSITION C IMPLEMENTED + MEASURED (M-66b) —
D206,FIXED,PC played every SFX one bank slot too high —,"FIXED (M-76, src/snd.c:1001, #ifdef PORT → soundArray[soundIndex - 1], N64 line verbatim under #else)."
D207,PROPOSED,Alarm klaxon starves combat SFX and (per user) doesn't recover after it stops —,MONITORING (M-77c: user reports the alarm is acceptable in play after D206; no fix shipped) —
D202,?,"Silenced PPK ""slap"" + broader real-time mixer corruption (M-52→M-67).",ROOT CAUSE ESTABLISHED (M-66); Disposition C implemented + measured (M-66b) —
D206,FIXED,PC played every SFX one bank slot too high —,"FIXED (M-76, src/snd.c:1001, #ifdef PORT) —"
D207,?,"Alarm klaxon starves combat SFX and doesn't recover after it stops (surfaced by D206, M-77).",MONITORING (M-77c: user reports the alarm is acceptable in play after D206; no fix shipped) —
D208,FIXED,Player weapon-fire sound goes permanently silent mid-firefight (all other SFX unaffected) —,FIXED + user playtest-verified (M-78d) —
D209,FIXED,Every AI character in the game is locked to a *walk* animation regardless of the speed the ailist commands —,FIXED + user-playtest-verified (M-81).
D210,FIXED,chrToPatrol() leaves act_patrol.lastvisible60 uninitialised on PC —,"FIXED (chraction.c:3872, #ifdef PORT → self->act_patrol.lastvisible60 = -1;, N64 line under #else)."
D205,RESOLVED,PC plays wrong/extra SFX for gameplay events the user can A/B against N64:,CLOSED (M-78) — root cause was D206; all four symptoms resolved + user-confirmed by ear vs N64.
D210,FIXED,chrToPatrol() leaves act_patrol.lastvisible60 uninitialised on PC —,"FIXED (chraction.c:3872, #ifdef PORT → self->act_patrol.lastvisible60 = -1;, N64 line under #else).…"
D205,CLOSED,"PC plays wrong/extra SFX for gameplay events (gunfire slap layer, explosion→scream, armour pickup, melee→Klobb idx 106) —",CLOSED (M-78) — root cause was D206; all four symptoms resolved + user-confirmed by ear.
D152+,FIXED,D152 addendum (M-31):,"FIXED (src/snd.c, port/src/libultra.c, #ifdef PORT) —"
D168,FIXED,GE_PCDUMP / F12 PPM captures were vertically flipped —,FIXED (port/fast3d/gfx_opengl.cpp); D114 / D116 reclassified as capture-orientation artifacts (below).
D169,FIXED,Front-end mouse pointer can't reach the outer cells of the mission/level-select grid —,"FIXED (M-33, port/src/input.c, port-only, no #ifdef PORT)."
D178,FIXED,Pre-mission briefing screen:,FIXED (M-36) — high confidence
D175,OPEN,In-game stutter / brief hang during normal play (user QA report):,"OPEN — observed, not investigated"
D176,FIXED,"Surface exterior renders wrong (-level_36), two independent defects.","(a) ""scrolls too fast/tiles too visibly"" ROOT-CAUSED + FIXED (see ""D176(a) —"
D175,OPEN,In-game stutter / brief hang during normal play —,"OPEN — observed, not investigated"
D176,FIXED,"Surface exterior renders wrong (-level_36), two independent defects.","(a) scroll-speed FIXED (fidelity pass; HIGH confidence on root cause, MODERATE on N64 parity); M-93 playtest + M-98 Defect-2 starburst screenshot in the sections."
D177,FIXED,Ladders non-functional —,FIXED (M-36) — high confidence; interactive climb test owed
D172,FIXED,"Bullet-impact / blood / spark particles render magenta or cyan instead of dark red (M-82, ROOT CAUSE FOUND + FIXED).",FIXED (port/fast3d/gfx_pc.cpp gfx_lod_tile_offset):
D172,FIXED,"Bullet-impact / blood / spark particles render magenta or cyan instead of dark red (M-82, ROOT CAUSE FOUND + FIXED).",FIXED (port/fast3d/gfx_pc.cpp gfx_lod_tile_offset)…
D174,OPEN,"""No blood effect"" (user QA report).",OPEN — observed; first step = BUNKER1 firefight with regenerated sidecars
D183,OPEN,"M-36 Family A (""texture line/pitch shear"") is DISPROVEN for the Surface repro (-level_36).",Hypothesis DISPROVEN + diagnostics shipped; D176(b)/D182(2) still OPEN
D173,OPEN,Third-person Bond model spawns too high (user QA report):,"OPEN — observed, not investigated."
D173,FIXED,Third-person Bond model spawns too high —,"MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed)."
D179,FIXED,Packaged build crashes after the logos —,"Diagnosed, not fixed —"
D180,PARTIAL,"Native-PC input pass (M-XX QoL run, qol/native-pc-input-menu, port-only).","LANDED, port-only, no #ifdef PORT."
D181,PARTIAL,Game.ScreenShakeIntensity —,"LANDED behind config, default = original."
D180,PARTIAL,"Native-PC input pass (M-XX QoL run, qol/native-pc-input-menu, port-only).","LANDED, port-only, no #ifdef PORT.…"
D181,?,Game.ScreenShakeIntensity —,"Menu row pulled (v0.2.1, user testing)."
D184,PARTIAL,F10 port-layer options overlay —,"LANDED, port-only, no #ifdef PORT in src/."
D211,PARTIAL,"PC FOV slider — Video.FovScale (M-83 QoL wave, port-only, WIDESCREEN-FOV-PLAN.md Phase 4).",PARTIAL — M-84 refix:
D211,PARTIAL,"PC FOV slider — Video.FovScale (M-83 QoL wave, port-only, WIDESCREEN-FOV-PLAN.md Phase 4).",PARTIAL — M-84 refix…
D212,PARTIAL,Anisotropic-filtering config wiring —,"LANDED, port-only. Build clean."
D213,PARTIAL,On-screen FPS readout —,"LANDED, port-only. Build clean; bunker1 no-op confirmed."
D214,PARTIAL,"Keyboard rebinding — [Input.Bind] config section (M-83, port-only, PD parity, Phase 4 debt).",PARTIAL — M-84: code PASS (alt/ctrl); one doc-only fix.
D215,FIXED,1P weapon viewmodel never drew in-level —,"FIXED (src/game/gunfire.c, #ifdef PORT)."
D216,PARTIAL,"Game.SkipIntro — boot straight to the file-select menu (M-83, port + one #ifdef PORT game-code line, PD parity, Phase 4 quick win).",LANDED + VERIFIED this session.
D217,FIXED,1P weapon viewmodel + third-person Bond model textures render garbled —,FIXED (M-90/M-92/M-97; PR #47 texpool alignment + PR #49 CI-cache palette key) —
D218,FIXED,Wide FOV exposes the per-level draw-distance / fog boundary —,"Logged (M-84), fix proposed + parked."
D219,RESOLVED,"PPK explosions render purple on Dam (M-87, user QA, ""not 3D but noting"").","RESOLVED (M-115, 2026-09-12) —"
D217,FIXED,1P weapon viewmodel + 3P Bond model textures garbled —,FIXED + merged (PR #47 + PR #49); white residual split out as D228.
D218,PROPOSED,Wide FOV exposes the per-level draw-distance / fog boundary —,"Logged (M-84), fix proposed + parked."
D219,RESOLVED,"PPK explosions render purple on Dam (M-87, user QA, ""not 3D but noting"").","RESOLVED (M-115, 2026-09-12) for the original PPK-purple symptom."
D220,PARTIAL,"1P muzzle flash position shifts upward at higher Video.FovScale (M-87, user QA, Jungle/M16).","ANALYZED (M-91) — the suspected unscaled-viewmodel root does NOT hold; this is correct perspective behavior, likely cosmetic/QoL."
D85,OPEN,room primary/secondary DL → texLoadFromGdl garbage,"OPEN (safety-netted; widen/pool fixes landed, geometry now renders)"
D86 · D87,RESOLVED,modelInitRwData truncated ptr · attract-demo BE ramromfilestructure,resolved
D88.4,RESOLVED,propDefs polymorphic record stream not byteswapped → setupDoor crash,resolved (d88_propdefs.py); layout audit M-20 → D132
D85,OPEN,Room primary/secondary DL → texLoadFromGdl garbage.,"OPEN (safety-netted; widen/pool fixes landed, geometry now renders)"
D86 · D87,RESOLVED,modelInitRwData truncated ptr · attract-demo BE ramromfilestructure.,resolved
D88.4,RESOLVED,propDefs polymorphic record stream not byteswapped → setupDoor crash.,resolved (d88_propdefs.py); layout audit M-20 → D132
D132,PROPOSED,D88 propDefs layout audit (M-20):,proposed fix (not applied) —
D157,FIXED,"Campaign never unlocks the next level (M-30, user bug —","FIXED (src/bondtypes.h, #ifdef PORT —"
D160,PARTIAL,Dam level-end cutscene (Bond off the dam / bungee) never plays —,"DIAGNOSTIC SHIPPED, not root-caused (c95713f5, #ifdef PORT + GE_D160=1):"
D157,FIXED,"Campaign never unlocks the next level (M-30, user bug —","FIXED (src/bondtypes.h, #ifdef PORT —…"
D160,PARTIAL,Dam level-end cutscene (Bond off the dam / bungee) never plays —,"DIAGNOSTIC SHIPPED, not root-caused (c95713f5, #ifdef PORT + GE_D160=1)…"
D89-D92,RESOLVED,"stage-load→frame: stan zero-fill overrun, portal address trunc, chr/AI spawn ptrs",resolved
D93-D102,RESOLVED,struct-pun / hardcoded-size pass:,resolved (D95 pool bump partial)
D103-D107,RESOLVED,"BUNKER1 viewport height, depth-buffer clear (G_CLEAR_DEPTH_EXT), portal near-plane, LOD mip tile",resolved
D108-D112,RESOLVED,skeletal models: d43_emit.py put_f32 byte-reversal bug,resolved
D113-D116,FIXED,portal BFS ok · matrix chain ok · player raw-offset audit + gunfire.c THROW* · HUD text X-mirror,"D114/D116 CLOSED — NOT A BUG (M-33, see D168):"
D117,OPEN,frame nondeterminism root-caused; GE_DETERM deferred; framediff.py structural.,"OPEN (mode attempted, not achieved)"
D118,RESOLVED,SDL input layer (port/src/input.c); M-24 mouse-look rework:,resolved (D118a residual)
D117,OPEN,"Frame nondeterminism root-caused; GE_DETERM attempted, not achieved.","OPEN (mode attempted, not achieved)"
D118,RESOLVED,"SDL input layer; M-24 mouse-look rework (mode-aware map, real INI); D118b/c FIXED.",resolved (D118a residual)
D150,FIXED,watch OBJECTIVES / BRIEF page crash:,FIXED (src/str.c #ifdef PORT); interactive re-verify pending
D260-D265,PARTIAL,"v0.2.1 hotfix batch: D260 wheel direction swap (fixed) · D261 watch-menu nav auto-repeat (fixed, feel-check owed) · D262 watch item-preview model missing → roo…",partial — see rows
D266-D269,OPEN,"D236/D265 wall, second pass:",OPEN (the wall itself) —
D270,?,Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it.,dead-ended into D271/D272; infrastructure kept in tree
D271,FIXED,"D236 wall, third pass —",FIXED-but-insufficient (src/game/bg.c #ifdef PORT) —
D276,OPEN,"D236 wall, eighth pass —","REFRAMED — oml=0xc81049d8/fmt=3 is the primary suspect now, not D269's CI8 pair; 0x0c184b50 unattributed; wall still open, see D236"
D279,?,Backfilled — this label was consumed by an undocumented partial session (2026-09-14 ~23:00) that left artifacts in scratch/ (d279_run.log… —,undocumented — artifacts only
D281,OPEN,"Game.AllUnlocked (D257) still breaks audio, and can also break right-mouse aim, when enabled before any save has ever been written (v0.2.1 user testing… —","OPEN — user-reported regression, not root-caused; mitigation is user-facing guidance only (release notes:"
D282,FIXED,"QoL ask (v0.2.1 user testing, 2026-09-15):","FIXED (port/src/input.c, #ifdef not needed —"
D283,OPEN,"Steam Deck first-run preset (D-unnumbered in port/src/video.c/main.c, videoApplySteamOSDefaults()) did not apply for a user who added the game via Steam… —","OPEN — mechanism understood, root cause (Desktop-vs-Game-Mode STEAMOS env var difference) unverified on real hardware; user-facing workaround documented in README/release notes."
D280,?,"D236 wall, ninth pass —","REFRAMED again — no fix; next steps in detail section, see D236"
D275,?,"D236 wall, eighth pass —","done — probes live, uncommitted; see D276 for what they found"
D274,OPEN,"D236 wall, seventh pass —","d267_slot_addr fix live (always-on); content/decode question CLOSED; mip-presence CLOSED; wall still open, see D236/D276"
D273,OPEN,"D236 wall, fifth pass —","RULED OUT (port/fast3d/gfx_pc.cpp palette_to_rgba32, pre-existing/uncommitted GE_D273) —"
D272,OPEN,"D236 wall, fourth pass —","OPEN — probe GE_D272 added to port/fast3d/gfx_pc.cpp (env-gated, uncommitted); wall still open, see D236"
D137,FIXED,right-mouse (aim-sight) crash:,FIXED (src/game/gunfire.c #ifdef PORT —
D119 · D120,FIXED,guard-attack weapons_held[]->chr pun crash · blood-stain PointUsage[] chain hang,D119 fixed · D120 FIXED (M-30)
D120,FIXED,Blood-decal PointUsage[] chain —,FIXED (tools_pc/d43_emit.py).
D121,RESOLVED,WS1 frictionless per-level boot:,resolved
D121,RESOLVED,WS1 frictionless per-level boot (bare -level_XX injects its -m* row).,resolved
D122,FIXED,per-level prop/item model-load crash (Dam/Facility/Runway modelLoad/modelInitRwData):,converter fixed; residual chr/fast3d crashes on those levels are separate
D123,FIXED,crash class C1: chrIsNotDeadOrShot NULL deref on 6 levels (Dam/Runway/Frigate/Statue/Streets/Cradle).,"converter fixed (OBJ_ID_WORDS); C1 cleared on all 6, residual crashes are fast3d (C2)"
D125,RESOLVED,crash classes C3+C6 (Aztec/Bunker2/Surface2):,resolved (M-16b) — line 374 → 8 NULs; all 21 sidecars regen'd; Aztec -level_28 now PASSES (was C3 CRASH).
D126,RESOLVED,"crash classes C3r/C4/C6 (Bunker2 -level_27 door7F054FB4 propobj.c:13523, Depot -level_30 prop.c:902, Surface2 -level_43 loadobjectmodel.c:393):",resolved — d88_propdefs.py PROPDEF_PC_BYTES[30/32/33/35]=24 + typed handler; loadobjectmodel.c sizepropdef PORT returns 6.
D124,FIXED,crash class C2: fast3d bad texture pointer.,Jungle fixed (port/src/gimgfixup.c); Facility → D130
D131,RESOLVED,crash class C2m (Jungle -level_37):,"resolved — seg_addr() restores the module high word for any fallthrough w1 in [0x40000000, 0x70000000) (module fixed-based at 0x140000000, no ASLR; DRAM/KSEG0/segmented/phys all handled earlier)."
D131,RESOLVED,crash class C2m (Jungle -level_37):,"resolved — seg_addr() restores the module high word for any fallthrough w1 in [0x40000000, 0x70000000) (module fixed-based at 0x140000000, no ASLR; DRAM/KSEG0/segmented/phys all handled earlier).…"
D133,RESOLVED,intro render triage (M-20):,"RESOLVED — not a regression, parked (D75/D76, GRAPHICS-BACKLOG.md)"
D134,FIXED,"Frame-2 boot hang / sweep ""NO-FRAMES"" flakiness —",FIXED (port/src/libultra.c)
D135,FIXED,"Firefight crash — bgTestHitOnObj (propobj.c:8446) is an unported N64 GBI parser (M-22, WS6 playtest).","FIXED (src/game/propobj.c, #ifdef PORT —"
D135,FIXED,"Firefight crash — bgTestHitOnObj (propobj.c:8446) is an unported N64 GBI parser (M-22, WS6 playtest).","FIXED (src/game/propobj.c, #ifdef PORT —…"
D136,RESOLVED,"Stage-unload crash — objFree obj->prop reads float garbage (M-22, WS6 Facility playthrough).",ROOT-CAUSED → D139 (it was NOT a layout bug —
D139,FIXED,Stage-unload crash root cause + fix (M-23).,"FIXED (src/game/cleanup_objects.c, #ifdef PORT —"
D139,FIXED,Stage-unload crash root cause + fix (M-23).,"FIXED (src/game/cleanup_objects.c, #ifdef PORT —…"
D140,FIXED,Pause-menu crash (M-23 open → M-27 fixed).,"FIXED (src/game/bondview.h, src/game/bondview2.c; #ifdef PORT)."
D141,FIXED,"Watch-menu item-model crash (M-27), exposed by D140.","FIXED (src/game/gunfire.c, #ifdef PORT)."
D152,OPEN,"Mission-failed / objective-failed → permanent black screen (M-28, user bug report —","MITIGATED (port/src/libultra.c, M-28, #ifdef PORT/port-only)."
D154,FIXED,"bg.c room-geometry ray/hit-test GBI parser port (M-28, WRITTEN / UNVERIFIED / UNCOMMITTED).",M-30 re-audit + fixes (still playtest-gated for final verify).
D156,PARTIAL,"Facility outro-cutscene ""hang"" —","GUARDED (src/game/model.c, #ifdef PORT):"
D155,FIXED,"Facility outro-cutscene end → ""hang"" = unclamped deltaFrames spiral (M-29, user bug report).","FIXED (src/game/frametiming.c, #ifdef PORT —"
D152,?,"Mission-failed / objective-failed → permanent black screen (M-28, user bug report —","MITIGATED (port/src/libultra.c, M-28, #ifdef PORT/port-only).…"
D154,?,"bg.c room-geometry ray/hit-test GBI parser port (M-28, WRITTEN / UNVERIFIED / UNCOMMITTED).",M-30 re-audit + fixes (still playtest-gated for final verify).
D156,PARTIAL,"Facility outro-cutscene ""hang"" —","GUARDED (src/game/model.c, #ifdef PORT)…"
D155,FIXED,"Facility outro-cutscene end → ""hang"" = unclamped deltaFrames spiral (M-29, user bug report).","FIXED (src/game/frametiming.c, #ifdef PORT —…"
D153,RESOLVED,"-level_09 frame ~900–1400 0xc0000005 = D117/D134 load flakiness, NOT a bug (M-28).",NOT A BUG — closed. Run anchors on a quiet machine.
D151,FIXED,Watch data all blank —,"FIXED (src/bondtypes.h, #ifdef PORT —"
D151,FIXED,Watch data all blank —,"FIXED (src/bondtypes.h, #ifdef PORT —…"
D130,RESOLVED,"crash class C2 (Facility -level_34, Runway -level_35):",resolved — stage all 6 N64 fields in a local f[6] before writing any.
D164,PROPOSED,"Disclaimer / legal screen only draws the first line (D76 root cause, M-31).","FIX PROPOSED, NOT APPLIED (front.c is owned by another agent this session)."
D164,FIXED,"Disclaimer / legal screen only draws the first line (D76 root cause, M-31).",FIXED — corrected 2026-09-18:
D165,FIXED,"Front-end menu cursor doesn't feel like a mouse pointer (M-31, user bug B5 follow-up).","FIXED (port/src/input.c; Input.MenuPointerMode, Input.HipfirePitchSpeed added)."
D166,FIXED,Hipfire mouse pitch is digital while yaw is analog —,FIXED (port/src/input.c).
D235,OPEN,Wrong textures load after dying/restarting a level —,OPEN — needs: does it repro on a *fresh* level boot (rules out reload-state) vs only after death/restart (confirms it); which surfaces; a screenshot.
D236,OPEN,"Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).",OPEN — not investigated
D166,FIXED,Hipfire mouse pitch is digital while yaw is analog —,FIXED (port/src/input.c).…
D235,OPEN,Wrong textures load after dying/restarting a level —,OPEN — needs: does it repro on a *fresh* level boot (rules out reload-state) vs only after death/restart (confirms it); which surfaces; a screenshot.…
D236,OPEN,"Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).","OPEN, sixteenth pass (2026-09-18) —"
D237,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only
D238,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only (port/src/input.c + config.c)
D239,?,"QoL ask (M-106, user QA):",CLOSED — executed in the v0.2.0 prep pass (port-only):
D239,CLOSED,"QoL ask (M-106, user QA):",CLOSED — executed in the v0.2.0 prep pass (port-only)…
D240,OPEN,"Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report).","OPEN — not root-caused; static call-site tracing only, no runtime verification possible in this environment."
D241,OPEN,Player gunshot SFX goes silent under some conditions —,"OPEN — not newly root-caused; high confidence this is D207 reproduced, not an independent bug."
D242,?,Player-death sequence UX regression vs N64 original:,NOT A PORT-LOGIC BUG as far as static analysis can tell.
D243,OPEN,"Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject):",OPEN — M-109 re-analysis of the same session's raw logs corrects the timing/pattern claims above; the D134/pendingGfx race hypothesis is likely wrong.
D245,OPEN,IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) —,"OPEN — REOPENED (M-116 live playtest, same session):"
D246,OPEN,"Tiny pixel strips at the left and right edges of the screen, ""like overscan"" —","OPEN — one candidate ruled out (M-116 follow-up), root cause not found."
D247,RESOLVED,Black bars top and bottom of screen —,"NOT A BUG — confirmed faithful, unmodified N64 decomp behaviour, no code change made."
D243,FIXED,"Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject):","FIXED, live-confirmed by the user (M-189/M-190, 2026-09-18)."
D245,OPEN,IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) —,"OPEN — REOPENED, M-142 headless probe found the tc-shift k measurably flips during ordinary view rotation (new root-cause evidence); an opt-in GE_D245_FIXEDSHIFT candidate is build-verified but needs…"
D246,FIXED,"Tiny pixel strips at the left and right edges of the screen, ""like overscan"" —","FIXED (2026-09-18 session, port-only, Video.SafeAreaCrop)."
D247,FIXED,Black bars top and bottom of screen —,"FIXED (2026-09-18 session, port-only, Video.SafeAreaCrop)."
D248,FIXED,"Port runs a stable 30 FPS instead of 60 FPS game-wide (M-117, user asked to verify FPS after the D209 timing-keystone fixes).","FIXED, root-caused to a genuine pointer-width ABI bug in src/sched.c (compiled for the PC build, not excluded)."
D249,OPEN,"LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV —",OPEN — not investigated.
D250,?,FPS is inconsistent/low on many levels since the port started actually running at 60fps —,
D249,OPEN,"LOD/culling breaks down on large open levels (Streets, Egyptian named specifically) at *default* FOV —","OPEN — M-142 static review ruled out the unscaled-far-clip and LOD-multiplier suspects (both already correct); narrowed to the portal/room-visibility BFS, needs a live screenshot+location, not more c…"
D250,FIXED,"FPS is inconsistent/low on many levels after 60 Hz shipped (M-119, user QA; D248 follow-on).","FIXED — per-call getenv() cost was the bottleneck; rock-steady 60-61 fps after, user re-tested 1440p/4K clean."
D251,FIXED,F10 options overlay QoL pass:,"FIXED (port-only, port/src/optionsoverlay.c)."
D252,?,"Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).",
D252,PARTIAL,"Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).","PARTIALLY FIXED — a real, distinct manifestation was found and fixed, but the bug family is not fully closed, and is intermittent on BOTH platforms, not Deck-specific (corrected 2026-09-18)."
D253,FIXED,Facility (and any level with a FORCECROUCH/LADDER linked special tile) SIGSEGV on Linux/Steam Deck —,FIXED — Deck-verified:
D254,FIXED,Linux crash handler destroyed its own logs:,FIXED — field-verified.
D255,OPEN,"Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle, D253 fix confirmed working —","OPEN — case identified (NULL model on an active OBJ record; destroy-path ordering), reproducible on Deck in combat (Bunker + Frigate); fix pending rule-#2 sign-off."
D255,FIXED,Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle; D253 fix confirmed working).,"FIXED (M-140) — D3x-class 32-bit pointer truncation (vtxstore_fix_refs read Model* through the mistyped ChrRecord.chrflags 4-byte-enum alias instead of ObjectRecord.model), not the destroy-path race…"
D256,FIXED,Saves (and config) silently failed to persist on Linux whenever the game was not launched from the directory containing data/ —,FIXED (path half) — save-flow detail still owed from the user.
D257,FIXED,New Game.AllUnlocked option (F10; default flipped OFF for v0.2.0 —,FIXED — verified by user playtest (PR #79 merged); default flipped OFF for v0.2.0 (D259).
D259,OPEN,Game.AllUnlocked (D257) breaks audio on a fresh save:,OPEN — quirk documented; default flipped OFF for v0.2.0 (port/src/video.c); fix deferred.
D259,FIXED,Game.AllUnlocked (D257) breaks audio on a fresh save:,FIXED in v0.2.1 —…
D260,FIXED,v0.2.1 (user request):,"FIXED — build green; one-line branch swap, trivially reviewable."
D261,FIXED,v0.2.1 (user request:,FIXED — feel-check received; W/S sensitivity follow-up logged above.
D262,FIXED,v0.2.1 (user report):,"FIXED (primary cause, D264) —"
D264,FIXED,"D262's root cause (ABI/layout class, cf.","FIXED — build green, probes removed; visual confirmation pending."
D263,RESOLVED,v0.2.1 (user report):,"RESOLVED (not a bug), refined (v0.2.1 further user testing, 2026-09-15)."
D265,?,"D236 investigation (port-layer, port/fast3d/gfx_pc.cpp):",PARKED — reverted to baseline; evidence + suspect list above.
D258,OPEN,PAL/JP asset conversion is broken at the source-data level —,"OPEN — repair path verified, not applied (out of v0.2.0 scope)."
D284,FIXED,port/src/random.c's MIPS64 dsll32/dsrl32 shift helpers were backwards —,"FIXED, 21-level sweep clean, no crashes —"
D285,PARTIAL,Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug:,"PARTIALLY FIXED, RECURRED PAST THE FIX (see D305, 2026-09-18)."
D286,OPEN,"User-confirmed real ~8s frame freeze on Caverns, Steam Deck —","OPEN — one confirmed occurrence, no repro trigger identified, not root-caused; distinct from D255/D285."
D287,OPEN,"User-confirmed real, non-self-recovering freeze on Windows (M-141, 2026-09-15/16) after ~15-20 minutes with no input, triggered by clicking back into the… —","OPEN — one confirmed occurrence, real (not self-recovering), not root-caused; gfxFrameMsgQ full is a lead, not a confirmed cause."
D288,OPEN,"Silo intro: a huge, flat-shaded, untextured brown/tan triangle sweeps across the frame during the camera pan down the missile shaft (M-141, user report —… —","OPEN — visually confirmed and precisely bounded (frames ~562-741 of a bare -level_20 boot), root cause not yet identified; promoted from the long-parked GRAP… |"
D289,OPEN,"Dam: a crash in the computer room, not reproducible on a second attempt (M-141, Steam Deck user playtest).","OPEN — needs a human re-test specifically on a build with the D255 fix (this session's release/v0.3.0, not any earlier build) to see if it still happens.…"
D290,OPEN,"Watch's in-game ""controller"" section shows no control-pad graphic (M-141, Steam Deck user playtest).","OPEN — observed, not investigated."
D291,OPEN,"Dam: texture glitches when the truck drives past (M-141, Steam Deck user playtest).","OPEN — observed, not investigated."
D292,FIXED,Death animations are glitched —,"MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed alongside D173) —"
D293,FIXED,"QoL ask: no discoverable way to quit the game from the UI (M-141, Steam Deck user playtest —","FIXED (M-141) — ""Quit to desktop"" row added to the F10 overlay; build-verified Win+Linux, not yet live-confirmed.…"
D294,OPEN,"Statue: ground geometry near the large CCCP/hammer-and-sickle monument briefly renders solid black over a wide area, then self-recovers 10-30 seconds later… —",OPEN — not investigated.…
D295,FIXED,"Campaign-halting crash at the end of Control Center on 00 Agent difficulty, then a crash on every subsequent relaunch/resume (GitHub #87, user campaign playtes…",FIXED (M-149) — M-148 root-caused + reproduced on HEAD; fix landed:
D296,OPEN,"Control Center, 00 Agent:","OPEN — single live report, un-triaged; observed only under a gdb-attached debug-harness launch (tools_pc/debug.ps1), not the packaged release artifact —"
D297,FIXED,v0.3.0 silently wipes ALL pre-fix player saves on first launch (D284 side effect; found during D295/M-148).,FIXED (M-150) — migration shim landed:
D298,?,Full HUD (not just the aim-mode crosshair) missing in a v0.3.0 dev build —,"s — regression reported 2026-09-16 during v0.3.0 work (build ce8427cf); user says ""don't fix, just for context""; not yet investigated —"
D299,FIXED,Game.SkipIntro crashed selecting any save slot except one —,"FIXED — fileValidateSaves() (only called from the skipped legal screen) never ran under SkipIntro, leaving saves[] all-zero; added the missing call to the SkipIntro shortcut (src/game/lv.c, #ifdef PO…"
D300,FIXED,Hipfire look feels non-uniform (issue #89):,"FIXED, user-confirmed live —"
D301,FIXED,"fileGetIsCheatUnlocked() NULL-deref, unguarded on every call site, not just the D299/SkipIntro one (GitHub #87, two reports with an identical crash PC/fault ad…",FIXED locally on release/v0.3.0 (not pushed) —
D302,FIXED,"Render FPS regression to ~40-45fps on release/v0.3.0 (user QA on the local v0.3.0 pre-release build, 2026-09-18) —","FIXED — 4 uncached getenv() calls added by D236 passes 10/12/14/16 directly inside gfx_sp_tri1 (port/fast3d/gfx_pc.cpp, per-triangle) cached with the same static int x = -1 pattern D250 established e…"
D303,OPEN,"Some muzzle flashes (M16 named specifically, ""and some others"") draw wrong —","OPEN — reported only, not yet investigated."
D304,FIXED,"Mouse sensitivity menu overhaul (user request, 2026-09-18):","FIXED — see full entry for both the range/parity pass and the same-day follow-up (collapsed to a single Input.MouseSensitivity menu row + fixed overlayUpdateScroll()'s unconditional bottom-anchor, th…"
D305,FIXED,"Steam Deck SIGSEGV on the v0.3.0 pre-release build (Cavern, early in the level, Game.AllUnlocked previously enabled on the session/save) —",FIXED (defensive guard) —
D306,OPEN,"Dam intro/truck wheels render with odd transparent-looking areas, possibly z-fighting (user QA, 2026-09-18).","OPEN — reported only, not investigated."
D307,OPEN,"In-level security camera entities can be flipped facing the wrong direction (backwards) on some levels, e.g.","OPEN — reported only, not investigated."
D308,OPEN,"Occasional z-fighting confirmed on some levels' geometry, general/broader than the D306 truck-wheel instance (user QA, 2026-09-18).","OPEN — reported/confirmed by the user, not investigated."
D309,FIXED,Complete non-self-recovering freeze (Windows) returning from Caverns to the intro screen —,FIXED via D310 — user-playtest-confirmed 2026-09-18 (Caverns→intro clean; the spin record itself observed resuming correctly in the post-fix probe log).
D310,FIXED,"RULE-2-SIGNOFF: chraiitemsize() mis-sizes 1-byte AI_PRINT records (NUL-scans forward), corrupting AI execution for any list containing an active PRINT —",FIXED — user-playtest-confirmed 2026-09-18 (v2:
D311,PARTIAL,Complete freeze in modelSetAnimFrame2WithChrStuff root-motion loop —,"GUARDED (#ifdef PORT, D156-guard extension in the same file) —"
D312,FIXED,"Throwable items (mines, grenades, etc.) can clip through walls in certain spots (user report, v0.3.0 pre-release playtest, 2026-09-18).",FIXED — bgBuildRoomVtxBounds mis-reads the G_VTX vertex count on PC (Gfx_le.dma.par bit-layout shift):
D313,FIXED,Bullet impacts do not appear on some walls/surfaces —,"FIXED — same root cause as D312, merged diagnosis; same one-line fix."
D314,OPEN,"F10 menu items flicker randomly/intermittently (reported on the main-menu context, user report, v0.3.0 playtest build, 2026-09-18).",OPEN — confirmed still present post-D316 fix (2026-09-18); user deprioritized chasing it further for now.
D315,OPEN,Menu scaling differs between main menu and in-game —,"OPEN — disclosed as a known issue, deferred to v0.3.1 (bigger design question, not a mechanical fix)."
D316,FIXED,"Mouse-clicking some menu items glitches and lands on the item below instead (user report, v0.3.0 playtest build, 2026-09-18).",FIXED — root-caused to two compounding bugs (naive mouse->logical mapping ignoring the safe-area crop; asymmetric row hit-test band) in port/src/optionsoverlay.c + port/fast3d/gfx_pc.cpp.
D317,CLOSED,"Crash when toggling Game.AllUnlocked back and forth in-game (user report, v0.3.0 playtest build, 2026-09-18) —","CLOSED — symptom of D316 (confirmed instant window close, no WER report; the D316 hit-band bias let a click on ""All unlocked"" fall through to the adjacent ""Quit to desktop"" row)."
D318,RESOLVED,Facility (regular play path):,RESOLVED (2026-09-19):
D319,FIXED,"Frame cap slider was mostly a dead zone; replaced with a plain 30/60 toggle (user ask, 2026-09-18).",FIXED — new ROW_FPSCAP cycle (30/60 only) in port/src/optionsoverlay.c; default changed to 60 in port/src/video.c.
D320,OPEN,"D318-class sweep: aim-hold → _update AI lists in 8 levels carry the same as-authored softlock race (static analysis, 2026-09-20).","OPEN — static sweep complete (Facility ai_19, Control ai_9, Depot ai_12 flagged high/med-high; Bond-combat loops likely safe); no live confirmation beyond D318 itself; probe-verify path + generalized…"
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
8 D59-D68
9 D70-D74
10 D75
11 D76 · D164
12 D159
13 D161
14 D165 · D166
15 D180 · D181
16 D186
17 D187
18 D188
19 D189
20 D190
21 D191
22 D193
23 D194
24 D195
25 D197
26 D198
27 D199
28 D200
29 D196
30 D192
31 D221
32 D222
33 D223
34 D224
35 D225
36 D226
37 D227
38 D229
39 D230
40 D233
41 D234
42 D244
43 D228
44 D204
45 D203
46 D201
47 D202
48 D206
49 D207
50 D208
51 D209
52 D210
53 D205
54 D152+
55 D168
56 D169
57 D178
58 D175
59 D176
60 D177
61 D172
62 D174
63 D183
64 D173
65 D179
66 D180
67 D181
68 D184
69 D211
70 D212
71 D213
72 D214
73 D215
74 D216
75 D217
76 D218
77 D219
78 D220
79 D85
80 D86 · D87
81 D88.4
82 D132
83 D157
84 D160
85 D89-D92
86 D93-D102
87 D103-D107
88 D108-D112
89 D113-D116
90 D117
91 D118
92 D150
93 D260-D265
94 D266-D269
95 D270
96 D271
97 D276
98 D279
99 D281
100 D282
101 D283
102 D280
103 D275
104 D274
105 D273
106 D272
107 D137
108 D119 · D120
109 D120
110 D121
111 D122
112 D123
113 D125
114 D126
115 D124
116 D131
117 D133
118 D134
119 D135
120 D136
121 D139
122 D140
123 D141
124 D152
125 D154
126 D156
127 D155
128 D153
129 D151
130 D130
131 D164
132 D165
133 D166
134 D235
135 D236
136 D237
137 D238
138 D239
139 D240
140 D241
141 D242
142 D243
143 D245
144 D246
145 D247
146 D248
147 D249
148 D250
149 D251
150 D252
151 D253
152 D254
153 D255
154 D256
155 D257
156 D259
157 D260
158 D261
159 D262
160 D264
161 D263
162 D265
163 D258
164 D284
165 D285
166 D286
167 D287
168 D288
169 D289
170 D290
171 D291
172 D292
173 D293
174 D294
175 D295
176 D296
177 D297
178 D298
179 D299
180 D300
181 D301
182 D302
183 D303
184 D304
185 D305
186 D306
187 D307
188 D308
189 D309
190 D310
191 D311
192 D312
193 D313
194 D314
195 D315
196 D316
197 D317
198 D318
199 D319
200 D320
+2424 -128
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 KiB

+57 -28
View File
@@ -1,13 +1,16 @@
---
title: GoldenEye 007 PC Port
# v0.3.0: status bumped from v0.2.1; Honest-status bullets synced with the
# README's v0.3.0 known issues (D255/D282/F10-dup/overscan items removed as
# fixed). Description kept at 133 chars (Bing's 160 limit).
description: >-
A native PC port of the original Nintendo 64 GoldenEye 007, built from its
decompiled source with a software RSP. v0.2.0 is out for Windows
and Linux (including Steam Deck): download it, drop in your own ROM, and
play; or dig into the code and the engineering record behind it.
A native PC port of the 1997 N64 classic, built from decompiled source with
a software RSP. v0.3.0 for Windows, Linux and Steam Deck.
---
# GoldenEye 007 PC Port
<!-- v0.2.1 (Bing SEO): the "# GoldenEye 007 PC Port" h1 was removed here --
the Cayman masthead already renders the site title as an <h1>, so the page
was emitting two h1 tags. -->
A native PC port of the original 1997 Nintendo 64 _GoldenEye 007_, compiled
from the game's [decompiled source](https://github.com/n64decomp/007) with the
@@ -15,18 +18,24 @@ N64's graphics coprocessor (RSP) running in software, the same architecture
as the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark), the
same Rare "Indy" engine family, one hardware generation apart.
**Status: v0.2.0.** The full single-player campaign runs at a steady 60 fps
and is completable end to end; all 21 missions load and run clean on Windows
and Linux (one intermittent Steam Deck crash remains, listed below), and
audio (music + SFX) plays throughout. The known
rough edges (cutscenes that still glitch, a few music tracks that sound
wrong, a handful of cosmetic rendering defects) are listed plainly under
[Honest status](#honest-status).
**Status: v0.3.0.** The full single-player campaign runs at a steady 60 fps
and is completable end to end (all 21 missions playtested); all 21 missions
load and run clean on Windows, Linux and Steam Deck, and audio (music + SFX)
plays throughout. The known rough edges — mostly cosmetic rendering defects,
stretched-not-native widescreen, and missing features — are listed plainly
under [Honest status](#honest-status).
**This is a pre-1.0 release, not a finished product** — v1.0 is the target
for a polished, feature-complete build; expect rough edges and missing
features until then. See the
[README's Roadmap section](https://github.com/jkdansereau/goldeneye-pc-port#roadmap)
for direction (PAL/JP support, real widescreen, LAN multiplayer under
consideration, and more).
<p align="center">
<img src="media/goldeneye-gh-preview.gif" width="70%"
alt="~32 s gameplay montage from live play sessions">
<br><em>~32 s gameplay montage loop from live play sessions, running in the port.</em>
alt="~12 s gameplay montage from live play sessions">
<br><em>~12 s gameplay montage from live v0.3.0 play sessions, running in the port.</em>
</p>
## Download
@@ -155,11 +164,16 @@ More captures may land here as playtesting continues.
licensed and yours to extend: new video options, input tweaks, your own
asset sidecar. [CONTRIBUTING](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/CONTRIBUTING.md)
has the ground rules that keep it faithful to the original game.
- **The research angle**: this is also a case study in AI-agent collaboration
on a large, low-level codebase: two coding agents (a local open-weight model
on a single RTX 5090, and Claude / Claude Code), driven by one person
part-time through ~230 translation units of unmodified big-endian MIPS game
code. [The full write-up](dev/agentic-development.md): setup, timeline, the
- **AI disclosure**: development here was agentic — Claude Pro plus a local
open-weight model on a single RTX 5090, as of August–September 2026. This
project is as much a study of *that process* as it is a port: whether
current LLMs can carry a codebase like this, and what actually goes wrong
along the way. Game codebases have historically been a rough fit for
LLMs — large, stateful, hardware-adjacent, unforgiving of a subtly wrong
memory layout — more so for older local models, so this is a useful data
point on where that stands now, for better or worse. Judge the result for
yourself. I'm one person doing this in my spare time, not a team. See
[the full write-up](dev/agentic-development.md) for the setup, timeline,
handoff workflow, and an honest assessment of what did and didn't work.
## How it works
@@ -176,15 +190,30 @@ catalogue: [Porting notes](porting-notes.md).
## Honest status
- **Cutscenes still glitch, mostly with James Bond**: in scripted sequences
Bond is the one who gets misplaced, hovers, or spins; the other actors are
fine for the most part now. Still the most visible gap in this release.
- A few in-level music tracks sound wrong (wrong instruments, occasional garbling).
- Particle colours drift through a rainbow palette instead of holding grey/orange.
- Water levels show a moving seam; pixel strips at screen edges at non-integer
scales; some front-end 3D models mispositioned or absent.
- The F10 overlay's bottom row duplicates the selected item; one intermittent
Steam Deck SIGSEGV in heavy firefights remains open (D255).
- **Particle colours** can still drift through a rainbow palette
intermittently: one genuine cause is fixed this release, a second hasn't
been found yet (D252).
- **Some muzzle flashes draw an extra erroneous long flash** straight up from
the gun, on top of the correct one. Cosmetic only (D303).
- Water levels show a moving seam between two water patterns (D245);
occasional z-fighting on some geometry (D308); in-level security-camera
props can occasionally face backwards (D307). Minor and cosmetic.
- **The front-end Nintendo logo renders as two white blobs** and the Rareware
logo's texture filtering looks off (D75).
- Surface 1's 2D billboard trees render as a solid wall of tree texture
instead of discrete sprites; under active investigation (D236).
- **Widescreen is stretched, not native**: 16:9 stretches the 4:3 frame to
fill the display; automatic FOV scaling keeps the framing comfortable and
gameplay is unaffected, but a distortion-free native widescreen render is
still on the roadmap. Distant geometry can also drop out on the biggest
open levels (D249).
- **Gunshot SFX can sound off during sustained/rapid fire** (D240/D241).
- **`All unlocked` is highly experimental** — don't enable it until you have
at least one save written, or it can break audio and mouse aim
(D257/D259/D281).
- Bond's cutscene positioning is fixed and right the large majority of the
time now; on the rare occasion he's off it's a small drift — no more
floating or spin-glitching (D173/D292/D243).
- No macOS/ARM support; no controller rebinding UI yet.
The full list, with root causes and fix status: the
+1 -1
View File
@@ -3,7 +3,7 @@ title: Internals
description: Architecture behind the PC port; the software RSP-emulation approach, GoldenEye-vs-Perfect-Dark engine differences, and the phased development plan.
---
# GoldenEye 007 PC Port: architecture & plan
## GoldenEye 007 PC Port: architecture & plan
> This began as the pre-implementation research note and is kept as the
> architecture reference. Sections 1–9 describe the design; section 10 lists
Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 MiB

After

Width:  |  Height:  |  Size: 22 MiB

+301 -11
View File
@@ -3,7 +3,7 @@ title: Porting Notes
description: A field guide to the recurring N64-to-PC bug classes hit while running unmodified big-endian game code on a 64-bit host, each with its symptom and fix.
---
# Porting notes: recurring N64→PC bug classes
## Porting notes: recurring N64→PC bug classes
A field guide to the bug classes that keep recurring when running big-endian
32-bit N64 game code, unmodified, on a little-endian 64-bit host. Terse by
@@ -55,8 +55,9 @@ state.
`.act_*.attack_item`; aliased `WeaponObjRecord.weaponnum` at 0x80 on
N64 via act-union@0x2C+84; act union moves to ~0x38 on PC).
**A1. Raw-byte aliases into a union (`act_ubytes.padding[N]`); the silent
variant.** The nastiest members of this class do not crash: a literal byte
**A1. Raw-byte or mistyped aliases into a union/struct whose true field
shifted because a sibling member widened; the silent variant.** The nastiest
members of this class do not crash: a literal byte
index into a union that overlays pointer-bearing structs still *reads*, it
just reads the wrong byte, so the game runs and merely behaves wrong. Grep
for hardcoded indices into any `padding[]`/`u8[]` union arm.
@@ -86,13 +87,60 @@ for hardcoded indices into any `padding[]`/`u8[]` union arm.
named writes of the *same* arm. The rest of the `padding[N]` sweep
(`chrlvTickAnim`/`chrlvTickDead` → `act_anim`/`act_dead`, both pointer-free)
is layout-stable.
- **D255** (a mistyped-field variant, not a raw-offset variant — same
mechanism, different syntax): `vtxstore_fix_refs` read a record's
`Model*` through `((Model*)((ChrRecord*)prop->chr)->chrflags)` —
`ChrRecord.chrflags` is typed `CHRFLAG`, a **4-byte enum**. On 32-bit
N64 the cast is bit-exact; on 64-bit PC it reads only the pointer's
low 32 bits and zero-extends, discarding the real high bits. The
truncated result is `(nil)` whenever the real pointer's low 32 bits
happen to be zero — which read exactly like a legitimate NULL-model
case, and cost two prior investigation sessions chasing a
"destroy-path race" theory before the field itself was the suspect.
**Tell that generalizes past D209/D210's `padding[N]` shape:** it
doesn't have to be a raw numeric offset — a field that is *correctly
named* but *undersized/mistyped relative to a pointer it's secretly
meant to hold* (an enum, a `u32`, a `s32` doing double duty as a
pointer slot) is the same bug through a different syntax. Fix: read
through the correctly-typed sibling field (`ObjectRecord.model`)
instead, `#ifdef PORT`, N64 line/cast kept verbatim under `#else`.
§F **D255** (M-140).
**Lesson.** When a value reads as a clean constant (0, 1) rather than
garbage, a pointer byte is a prime suspect; the high bytes of a low-4GB
heap pointer are all zero, so a misaligned read looks like a legitimate
"feature off" value. Prefer the named field under `#ifdef PORT`; the alias
is only correct at 32-bit pointer width. Also: verify that every caller
reaches the site with the union arm you are naming actually active.
**Lesson.** When a value reads as a clean constant (0, 1, NULL) rather
than garbage, a pointer byte is a prime suspect; the high bytes of a
low-4GB heap pointer are all zero, so a misaligned or truncated read
looks like a legitimate "feature off" / "not set" value. **Tell-tale
signs of this pattern, any one of which should trigger an A1-first
check before assuming a bug is genuine game-logic behavior:**
- The observed value is **byte-exact-looking but wrong from the very
first call/frame** — not intermittent corruption, not "usually right
but occasionally wrong."
- A **raw/literal offset or byte-width read** into a union or struct
that has **any pointer member**, anywhere in that union/struct's
layout (not just the field being read) — the read doesn't need to
touch the pointer itself; any member *after* one that widened is at
risk.
- A field that is **correctly named but too narrow to hold what it's
actually storing** (an enum/`u32`/`s32` reused as a truncated pointer
slot — the D255 shape, distinct from D209/D210's literal-offset shape
but the same underlying cause).
- The resulting behavior is **deterministic and internally
self-consistent, but demonstrably not what the N64 original would
produce** — the code isn't crashing or producing garbage, it's
confidently doing the wrong (but stable) thing.
- Prefer the named field (or a correctly-typed sibling field) under
`#ifdef PORT`; the alias/pun is only correct at 32-bit pointer width.
Verify every caller reaches the site with the union/struct arm you're
naming actually active.
**Distinguish from D284** (`port/src/random.c`'s MIPS64
shift-emulation bug): same *epistemic* shape — deterministic,
self-consistent, silently not-N64 — but a different *mechanism*
(instruction semantics, not struct layout/pointer width) and,
critically, **entirely inside `port/`, no `src/game` touch at all**.
D284 was never a rule-2 question in the first place; don't fold it
into this catalogue's "looks like game logic, is really ABI"
framing — it never looked like game logic to begin with.
- **Open landmine:** raw hardcoded-offset accessors into `struct player`
/ `struct hand`; see `docs/dev/AUDIT-M6-player-offsets.md`.
- **D126 corollary; a trailing runtime list pointer in a ROM-serialized
@@ -235,8 +283,14 @@ expressed in N64 `Gfx`/`Vtx` units is **half-size** on PC.
N64-semantics `.dma.par` becomes `((u32)gdl->words.w0 >> 16) & 0xff` on PC;
**`gdl->dma.par` gives bits 0-23 (length), and `& 0xf` on it is always 0**
because `len == 16*n`. The original D154 port had exactly this bug (vtxoff
forced to 0). Same trap latent in `bgBuildRoomVtxBounds` (`gdl.dma.par>>4&0xf`
reads PC bits 4-7, not N64 bits 20-23; currently tolerated). Rule: for
forced to 0). Same trap in `bgBuildRoomVtxBounds` (`gdl.dma.par>>4&0xf`
reads PC bits 4-7 = `n&0xf`, not N64 bits 20-23 = `n-1`): for n ≤ 15 it
happened to read the whole block (+1 vertex), but for n = 16 (the common
authored size) it collapsed to `numvertices=1` → single-point batch bounds
→ the per-batch AABB pre-test in `bgTestBulletHitBackground` rejected the
batch → its wall triangles were never ray-tested: D312 projectile
pass-through + D313 missing bullet impacts (FIXED 2026-09-18, recover the
params byte via `((u32)gdl->words.w0 >> 20) & 0xf`). Rule: for
anything but the opcode, extract the bit-field explicitly from
`(u32)gdl->words.w0` / `.w1`, don't trust the named `.dma.*` sub-fields.
Also: a `words.w0 << k >> m` bit-extract idiom that relied on 32-bit
@@ -680,6 +734,41 @@ through a converter or a runtime bswap fixup reads scrambled.
backface culling already has, or it'll silently eat near-camera geometry
exactly where a port is most likely to get scrutinized (doorways,
tight corridors).
- `gfx_sp_tri1`'s `G_CULL_BOTH` backface-cull cross-product (`gfx_pc.cpp`,
right below the D233 trivial-reject guard) divides raw vertex `w` with
no epsilon guard. A vertex at/near `w≈0` during a near-plane crossing
can NaN/Inf the cross product, which silently fails both the
`cross<=0` and `cross>=0` cull branches and lets a triangle draw that
should have been backface-culled — a candidate for D288 (Silo intro
stray triangle), not yet confirmed against a live repro (M-152). Unlike
D233's fix, this one hasn't been shown to actually fire yet; it's a
code-review lead, not a confirmed mechanism.
- **When chasing a missing UI element, split "game logic decides to draw
it" from "fast3d actually rasterizes it."** This codebase's decomp side
for menu/watch item rendering is consistently byte-matched and rarely
at fault; the recurring bug class is a combiner/blend-mode or
texture-format edge case in `port/fast3d/gfx_pc.cpp` for a one-off
translucent/`PROP_TYPE_PLAYER` draw that doesn't share a code path with
the common in-level rendering (D172 particle color, D217/D228 CI/IA16
palette, D229 water blend, D233 invisible-through-doors, and D290's
leading hypothesis — the watch controller graphic's fade-in uses a
low-alpha `PROP_TYPE_PLAYER` combiner state nothing else exercises).
Confirm the `src/game/` side is clean first (item ID, resource-table
entry, model header, generic equip/gate pipeline) before spending time
in fast3d — but once it is, look there, not back at the decomp logic.
- **`portHeartbeatCheck` (`port/src/libultra.c:218`) hard-caps at 3
reports total per process lifetime, by design** ("stop after a few
reports"). When chasing a long-idle hang (D287), "the heartbeat stopped
logging" is that cap firing, not evidence the hang resolved, worsened,
or changed mechanism — don't read silence from this diagnostic as a
signal. Gate a raised/unlimited report count behind its own env var for
these investigations instead of trusting the default cap's silence.
- **Before citing a `scratch/*.log`/`.txt` capture as "evidence" in a
finding, grep it for the specific numbers/strings the write-up claims**
(D287): a file with the right name on disk is not proof it holds the
claimed capture — this one turned out to be an unrelated normal-exit
run, not the freeze it was cited for, and the mistake propagated
through the entry unnoticed until a later static pass checked.
- **D74 wrap-block is DEAD CODE** (`gfx_pc.cpp:1546/1551`): guard is
`cms & G_TX_WRAP` but `G_TX_WRAP == 0`, so always false (line 1887 does
it right with `cms == G_TX_WRAP`). And if it did run it indexes
@@ -980,6 +1069,15 @@ and turns every such overrun into a fatal `*** stack smashing detected ***`
(bail is `if (cat >= 41)`, checked once per outer loop → writes `[40..]`);
`bondview2.c` `bondviewCalcIntroSwirlCamera` `f32 pointbuf[10]` written
`[-3 .. 11]`.
- Instance (D295/M-148): `front.c:2616` `char difficultytext[4]` receives
`strcpy("00 Agent")`/`strcat("\n")` — the NUL lands exactly on the adjacent
`textpos.p[0]` (y-coord), which is then re-stored; when y ≥ 0x80 the string
carries a high-bit byte into `textRender` → JPN-glyph path → NULL deref of
`g_JpnCacheCacheItems` (`language.c:320`). Same class, different fatality:
no canary involved — the clobbered *neighbour* changes control flow. On N64
the identical overflow is cosmetic (the JPN-cache pointer is valid memory in
any language); the crash severity is a PC artifact of that global being NULL
outside Japanese mode.
- Fix policy: **`-fno-stack-protector` globally** (`CMakeLists.txt`) so Linux
matches the N64 and MinGW builds; the code is byte-matched to the ROM and
cannot be "fixed" per site without diverging. Where a case is cleanly
@@ -992,6 +1090,74 @@ and turns every such overrun into a fatal `*** stack smashing detected ***`
- Distinct from D7 (`va_list` UB) and D6 (missing `return`) but the same
"works on N64 + MinGW, fatal on hardened Linux" shape.
## D9. Changing a PRNG/CRC implementation invalidates persisted derived state — save checksums are de facto file format
D284 (M-139b) fixed `port/src/random.c`'s `dsll32`/`dsrl32` semantics and the
entry claimed "savegames unaffected (seed isn't save state)". It was wrong:
`fileGenerateCRC` (`src/game/crc.c`) — the per-slot validation checksum checked
by `fileValidateSaves` (`file2.c:544`) — runs `randomGetNextFrom` off its own
**local constant seed**, so every stored slot CRC is a fingerprint of the PRNG
implementation that wrote it. Verified against a real v0.2.x save (GitHub #87):
pre-fix PRNG reproduces all five stored CRCs bit-exact, post-fix matches zero →
first launch of any post-fix build **silently wipes every existing player's
entire campaign** (D295 M-148). Generalizable rule for this port: before
changing anything that feeds persisted derived state (PRNG, CRC/hash routines,
endian/byte-order helpers used by serializers), audit *all* consumers whose
output is stored on disk — not just the in-memory streams. A "port-only" fix to
a checksum primitive is a save-file-format break unless old and new outputs are
made mutually acceptable (dual validation / one-time migration).
## D10. A "skip to state X" port shortcut must replicate every side effect of the screens it skips, not just the terminal flags
D216's `Game.SkipIntro` jumps straight from boot to `MENU_FILE_SELECT`,
skipping the legal screen + Nintendo/Rare/GoldenEye logo attract loop, by
directly setting the handful of state-machine flags (`is_first_time_on_main_menu`,
`prev_keypresses`, `maybe_is_in_menu`, `menu_update`) that the normal intro
path would have arrived at. Its original comment claimed this "reuses the
game's own post-intro route" — true for the menu *flags*, but the skipped
`init_menu00_legalscreen()` also has a one-time side effect with no flag of
its own: it calls `fileValidateSaves()`, the only place `saves[]` is ever
populated from EEPROM. Skipping it left `saves[]` all-zero, and file-select's
unguarded `fileGetIsCheatUnlocked()` crashed selecting any folder except the
one a zeroed `save_data` happened to decode to (D299). Generalizable rule:
when adding a port shortcut that jumps over N64 screens/states, enumerate
every side effect those screens' `init_menuXX_*()`/`update_menuXX_*()`
functions perform — not just the menu-transition flags — and replicate the
non-flag ones (data loads, one-time validation, subsystem init) explicitly.
Grepping the skipped `init_*`/`update_*` functions for calls with no
matching state flag is the concrete check.
## D11. A chr's own per-tick animation-position logic can silently overwrite an AI script's scripted position in the same frame (D243)
`chrTick` (`src/game/chr.c:2442`) always runs one of three `chr->actiontype`-
gated branches (`ACT_ANIM`/`ACT_STAND`/else, `chr.c:2578-2616`) that call
`chrUpdateAnim`/`modelTickAnim` — ordinary per-chr animation/locomotion
logic with no check for whether the chr is currently being driven by a
scripted cutscene camera/AI-script position write instead. If something
else (an AI script's `GOTO`/teleport, a cinema-pad move, D243's
`CHR_BOND_CINEMA` teleport) writes `prop->pos` earlier in the same frame,
`chrTick`'s own animation-position write can silently overwrite it before
the model transform is computed — two writers racing for the same field
within one tick, no error, no crash, just whichever runs last wins. This
produced D243's "frozen render position that doesn't track the smooth
scripted sweep" symptom: a first pass (M-159) wrongly attributed it to
the on-screen/frustum-visibility gate (`posIsOnScreen`) skipping the
render-transform refresh on off-camera ticks — REFUTED by a live capture
that logged BOTH the pre-`chrTick` position (from the AI script, smoothly
sweeping) and the post-`chrTick` position (frozen) for the same frame
numbers, with the visibility gate returning true throughout. **The
generalizable check:** when a rendered position looks "frozen" or
lagging behind a scripted/AI-driven `prop->pos`, don't assume a
visibility/culling gate is skipping an update — log the same field both
immediately before and immediately after the suspect tick function to
see whether something *inside* that function is overwriting it. A
visibility gate stopping updates and a competing writer stomping them
produce the same visual symptom but need opposite fixes.
**Update (same D243 investigation, later in the same project's life): fixing the field you found the bug in doesn't mean you've fixed what's rendered — check which field the CONSUMER actually reads.** Having found `chrTick` overwrites `prop->pos`, two live-playtest cycles were spent neutralizing exactly that overwrite (save/restore `prop->pos` around `chrTick`) — it worked completely (confirmed in logs: the field went smooth) and yet the visible symptom (camera shake) was completely unchanged. The reason: the camera's look-at filter doesn't read `prop->pos` at all — it reads `field_488.pos`, a *different* field written from a *different* data path (`render_pos`, the model's animated-skeleton transform, computed independently of `prop->pos` inside the same function). Both fields happened to show the same "period-4 cycling" symptom, which made them look like one bug through a probe that only logged `prop->pos` — they were two independent outputs of the same underlying animation-tick call, and only one of them was what actually got rendered. **The generalizable check, sharpened:** once you've confirmed a field is being overwritten, don't assume that field is what the visible symptom's consumer (a camera filter, a draw call, a collision check) actually reads — trace the consumer's own read site explicitly and confirm it's the same field before spending a live-test cycle suppressing the wrong one. A quick static check (grep the consumer's exact read expression) is far cheaper than a live playtest.
**Update 2 (same investigation, immediately after): freezing a *value* and freezing a *pointer target* are different operations — treating them the same crashed twice.** Extending the same freeze technique to `render_pos` (a `Model*` field, unlike `field_488.pos` a plain value) by skipping the call that reassigns it (`subcalcmatrices`) seemed equivalent to M-167's "skip the write" but wasn't: `render_pos` is a pointer into a per-tick dynamically-reallocated arena, and skipping its reassignment only works if the pointer it's left holding is still valid — which depends on model lifecycle, not just on this tick's logic. Two fixes in a row tried to detect "is it safe to skip" by inspecting either the pointer's *value* (a sentinel check, `!= NULL && != -1`) or a *cached identity* (comparing the pointer against a remembered "already validated" pointer) — both crashed, because **the allocator reuses model-pointer addresses across genuinely different logical model instances**: a freshly-`CREATE`d model can land at the exact address a previously-removed model occupied, so both the sentinel check and the identity check wrongly conclude "this is already valid" for an instance whose `render_pos` is still an uninitialized tombstone. This is the same "don't guess validity from a value that can coincidentally look right" trap as the paragraph above, encountered a second time from a different angle (a *pointer's* target identity, not a *field's* content). **The robust fix:** never skip the call that establishes the pointer's validity — let it always run — and apply the freeze as a *contents* copy afterward, on top of a buffer that's now unconditionally guaranteed valid. If you must "hold something steady" and that something is reached through a pointer that some other code reassigns, freeze the *pointee's bytes*, not the *act of assignment* — the assignment is usually also the thing keeping the pointer sane.
## E. Process / method notes
- Investigation loop is: reproduce → env-gated capped probe → root-cause
@@ -1003,6 +1169,14 @@ and turns every such overrun into a fatal `*** stack smashing detected ***`
writing anything new; same Rare engine family.
- Don't re-investigate a closed §F finding or re-derive a format spec
that already has a converter.
- **`docs/dev/findings-index.csv` is generator-derived** (`tools_pc/gen_findings_index.py`): the status cell is normalised from the `## Dxx` entry's §F summary-table row in `findings.md`, so hand-editing the CSV is silently reverted on the next regen (hit live in the D219/D252 M-158 session). Update the table row, then run the generator.
- **Every game launch rewrites `data/ge007.eep`** (the eeprom shim stores on
every write; validation resets, cheat patches and normal saves all write).
When restoring a specific save artifact for a controlled test (e.g. an issue
reproduction), restore it *after* any sweep/verify run — or re-hash after
each run — not before: `tools_pc/level_sweep.sh` alone boots the game 21
times against `data/` and will clobber a pre-run restore (caught live in the
D295 M-149 session; "verified" must mean checked at report time).
- **The port is NOT frame-deterministic** (D117): `osGetCount()` is
wall-clock on PC, and GE is a variable-timestep sim
(`frametiming.c waitForNextFrame` → `deltaFrames` = 60 Hz ticks of real
@@ -1013,6 +1187,38 @@ and turns every such overrun into a fatal `*** stack smashing detected ***`
fixed-tick mode was assessed not-narrow (redesigns retrace/tick
semantics); design is in §F D117 if someone picks it up, after which
`framediff.py --exact` becomes usable.
- **A "deterministic boot" flag (`-level_XX`) pins the level and starting
state, but NOT the camera's later path through it — don't assume two
captures from the same boot flag are comparing the same view.** D236
pass 10 (M-155) found a render-mode-class triangle count that looked
"run-to-run dependent" across separate captures using the same
`-level_36` boot; three independent re-launches (including one with a
full `GE_INPUTSCRIPT` sustained-rotation sweep) actually produced
byte-identical, fully deterministic per-frame counts — the real
variance was the boot's own scripted intro camera walking from an
outdoor room into an indoor one at a fixed frame, and the two captures
being compared had sampled before vs. after that walk (or against an
unrelated live/free-play session entirely). **Before concluding a
count/state is nondeterministic across runs of the same boot flag,
correlate against a room/position probe (`GE_D104`-style) in the SAME
run** to rule out "the camera simply isn't where you assumed it was
yet" before chasing an instantiation or streaming bug.
- **A level's opening cutscene is not necessarily input-inert, and
`GE_INPUTSCRIPT` stick holds during it are not necessarily useful.**
D236 pass 11 (M-155): holding `SUP`/`SDOWN` from frame 0 of the
`-level_36` deterministic boot changed which room the player occupied
at the very first probe sample (`curRoom` 12/7/13 depending on the held
direction) — the pre-transition window is NOT purely camera-scripted
with input discarded, as pass 10 might suggest. But none of forward,
backward, or a `START`-skip attempt could keep the camera in the
outdoor room past the scripted indoor transition, which fired at
roughly the same real time regardless of held direction and *earlier*
when `START` was pressed to skip faster — i.e. the transition looks
timer/event-driven, not distance-to-a-trigger-driven. **Don't assume a
scripted sequence's transition point is walkable-around just because
input has *some* visible effect during it** — test whether the specific
transition you care about is time-gated vs. position-gated before
investing in a nav script to avoid it.
- **Anything blocking on the scheduler thread throttles the whole sim**
(D186, cf. D134). The gfx task runs synchronously on `src/sched.c`'s
thread, and that same thread delivers VI-retrace events; any `sysSleep` /
@@ -1203,3 +1409,87 @@ and turns every such overrun into a fatal `*** stack smashing detected ***`
one silently drifts from the other; when investigating a decode bug, diff
the suspect path against its sibling path for the same format before
re-deriving the bit layout from scratch. §F **D228**.
- **A Claude Code session's default `Bash` tool is NOT the MSYS2 MINGW64
shell the build needs, even when `uname -a`/`$MSYSTEM` claim it is (M-140).**
On this box the tool's shell is Git-for-Windows Bash (root mounted from
`C:/Program Files/Git`), which sets `MSYSTEM=MINGW64` and reports
`MINGW64_NT-...` from `uname` for compatibility — both of AGENTS.md's own
“are you in the right shell” checks pass while `cmake`/`ninja`/`gcc` are
still missing from `PATH` (they resolve at `/c/msys64/mingw64/bin` and
`/c/msys64/usr/bin`, not `/mingw64/bin` — that path exists but is a
Git-Bash stub, not the project's real MSYS2 install). Symptom if you build
anyway with only Git-Bash's own `gcc`/toolchain state: `cmake: command not
found`, or (if some `gcc.exe` further down PATH resolves) every compile
fails with `Cannot create temporary file in C:\Windows\: Permission
denied` — a native Windows exe's `GetTempPath` falling back to `C:\Windows`
because the child process doesn't actually see a usable `TMP`/`TEMP` the
way it does when launched from a real MSYS2 shell window. Fix: explicitly
`export PATH="/c/msys64/mingw64/bin:/c/msys64/usr/bin:$PATH"` (adjust if
MSYS2 is installed elsewhere) before `./build-pc.sh`/`ninja`/`cmake` in any
Claude Code Bash-tool session; do this once per session, first thing, not
reactively after a mystery failure. Also: `cd` with a *backslash* Windows
path (`cd C:\Users\...`) is unreliable in this same shell and can silently
mis-parse to a mangled path on a later command in the same tool call —
prefer `cd /c/Users/...` (forward slashes) for the build working directory.
**The PATH fix alone can still leave the *link* step broken (M-141):**
compiling succeeds but `ld.exe` fails with `cannot find @C:\Windows\Temp\
ccXXXX: Invalid argument` if `TMP`/`TEMP` are unset or point at a
non-writable/non-real path like a literal `C:\Windows\Temp` string — the
response-file `ld` uses for the link command has to land somewhere this
shell's child process can actually write and re-read. Fix: `export
TMP=... TEMP=...` to a real writable per-session directory (the Claude
Code scratchpad path works well) before `ninja`/`build-pc.sh`, not just any
Windows-looking path string.
- **A backgrounded `ge007.x86_64.exe` launch from this Bash tool does not
reliably die to `pkill -f ge007.x86_64.exe`** (M-155, D288 session): the
process appears in `ps aux` as bare `ge007.x86_64`, no `.exe` suffix, so
the `-f` pattern match on the full name silently fails and the game
window (and its log-writing) keeps running well past the intended
`sleep N` capture window — one session let a stray instance run ~90s
longer than intended before it was caught. Use `taskkill //F //IM
ge007.x86_64.exe` instead to reliably terminate it from this shell, and
`ps aux | grep -i ge007` to confirm before moving on — don't trust a
`kill`/`pkill` exit code alone.
- **Headless visual verification of a rendering bug is possible without a
live human** (M-156/M-157, D252/D219 sessions) — combine `GE_INPUTSCRIPT`
(scripted controller input, e.g. `Z` pulses to fire a weapon) with
`GE_PCDUMP="lo-hi:step"` (writes `./ppm/frame_NNNNNN.ppm` — NOT a separate
configurable output dir, just `./ppm` relative to cwd; `GE_MKDIR` creates
it) in the *same* run, then actually open the resulting PPMs (Python
`PIL.Image.open(...).convert('RGB')` reads them fine, no format quirks)
instead of only grepping a probe's text log. A text-only probe proves a
code path fired; it does not prove what ended up on screen — several
long-OPEN findings (D290/D291/D294, and D252/D219's whole "rainbow
particle" family before this session) were stalled for lack of eyes on
the actual pixels, not for lack of a working probe. Build a low-res
contact-sheet montage first (tile many frames small, e.g. 160×120, to
scan for the moment of interest cheaply) before zooming into individual
full-res frames — full Dam/Bunker1 frames are 3072×1728, too large to
eyeball one at a time across a whole capture window.
- **A crash fault address of `0xffffffffffffffff` on x86-64 usually means a
non-canonical #GP from a garbage-high-bits pointer (bits 63:48 not a sign
extension of bit 47), not a computed `-1` sentinel** (cross-validated
against the sibling GEVR VR port, same decomp lineage — see
`scratchpad/GEVR-TRIAGE.md` item C; we independently hit the same register
shape in D191). Two companion habits worth keeping: re-read map anchors
from the freshly rebuilt map after every rebuild (a stale map silently
resolves the wrong symbol), and confirm the fault PC actually disassembles
on an instruction boundary before trusting a resolved symbol.
- **Frame-rate assumptions — read before proposing any non-60 Hz mode**
(cross-validated against GEVR, `scratchpad/GEVR-TRIAGE.md` item B). GE's
logic is delta-scaled through `g_GlobalTimerDelta` (~216 call sites); the
PAL build (×1.2) is a shipping proof that delta-scaled code tolerates
other rates. But: **Bond's walk is frame-rate dependent by construction**
— `MoveBond` (`bondview2.c`) is an exponential approach toward a target,
advanced once per frame with no timestep, so convergence is measured in
frames, not seconds (GEVR measured ~1.487× walk speed at 90 Hz vs 60 Hz,
≈90/60 exactly). Turn/aim *is* delta-scaled; walk is not. High-refresh or
uncapped presentation modes are therefore out of scope without a
game-logic change, which is forbidden — we tick at fixed 60 Hz (D52
pacing) by design, not as a limitation to fix. Also: `g_ClockTimer` must
stay integer (24 iteration sites depend on it); of 44
`/g_GlobalTimerDelta` divide sites, 40 are guarded (three different
spellings) and 4 are not (front-end cast camera `front.c:8271-8273`,
`propobj.c:~2393`) — those 4 rely on div-by-zero yielding NaN/inf without
trapping on both MIPS and x86-64 SSE, matching retail behavior; do not
"fix" them.
+102
View File
@@ -0,0 +1,102 @@
# Security & fidelity status
This page answers two questions plainly: *what does installing this put on
your machine*, and *how faithfully does the port actually reproduce the
original N64 game's logic*. Both were reviewed end-to-end on 2026-09-15 and
re-verified for the v0.3.0 release bundles on 2026-09-18 (bundle contents,
no-networking source check, and the asset converter). The full engineering
record behind each item is in the project's finding log
([`docs/dev/findings.md`](dev/findings.md)).
## What a release actually installs
Each release (`goldeneye-pc-port-*-win64.zip` / `-linux-x86_64.tar.gz`)
contains, and only contains: the game engine executable, a fixed set of
standard runtime libraries (SDL2, zlib, and the MinGW/Linux runtime — no
third-party downloads), the one-time ROM-to-PC asset converter, a README,
and license texts. The build scripts hard-fail if a ROM image or an
oversized blob ends up in the bundle. **No ROM, no game assets (textures,
audio, models, levels, text) are ever shipped** — you supply your own
legally-owned copy.
At runtime, the engine:
- has **no networking of any kind** (no sockets, no HTTP, checked directly
against the source),
- has **no telemetry, crash reporting, or analytics** of any kind,
- **never touches the Windows registry** and **never requests elevated
permissions**,
- writes save data, config, and derived ROM assets only inside its own
folder — never to `%APPDATA%`, `Program Files`, or any system location.
The one-time asset converter (`ge007-convert` / `prepare-assets.py`) — the
part of the bundle most likely to draw suspicion, since it's the thing that
reads your ROM and writes new files — has no network access at all,
verifies your ROM against a fixed table of known retail GoldenEye 007
hashes before touching it (an unrecognized file is rejected outright), and
writes only inside the bundle folder. Its full source ships right next to
the compiled version in every release if you'd like to read it yourself.
See [`SECURITY.md`](../.github/SECURITY.md) for why the binaries are
unsigned and what that means in practice (a SmartScreen prompt on first
run, and why some antivirus engines flag the converter specifically).
## Building from source
Cloning and building pulls in only standard, well-known packages: MSYS2
`pacman` packages on Windows, `apt` packages on Linux — no `npm`, no
`cargo`, nothing downloaded at CMake configure time, no git submodules, and
no prebuilt binaries checked into the repository. **One real gap**: the CI
build installs `pyinstaller` from PyPI without pinning a version, which is
used to freeze the release's asset-converter tool. It's a widely-used
packaging tool, and what it packages is entirely in-repo, stdlib-only
Python — but an unpinned dependency means a future PyInstaller release
could change what ships without a corresponding diff in this repository.
Pinning it is a tracked follow-up.
One development-only backdoor exists in source builds: the `GE_DEBUG_UNLOCKALL`
environment variable (`src/game/file2.c`) pre-unlocks all cheat options at boot.
It is off unless explicitly set, changes no shipped behavior, and is documented
here so the env-var surface stays fully accounted for.
## Fidelity to the original N64 game
The port's non-negotiable rule is that decompiled game logic is never
changed — only genuine N64-hardware dependencies get a PC-side
replacement, in the `port/` layer. This was audited directly:
- **Region build macros** exactly mirror the original N64 Makefile's macro
sets for all three regions — verified line-by-line.
- **The audio/library file classification** (which original files compile
vs. get PC-shimmed) exactly matches the project's own ground-truth
manifest — no silent gaps, no silent duplicates.
- **The one shipped game-logic-adjacent change** (a portal-culling
float-precision fix, D271) is `#ifdef PORT`-gated, documented in the
finding log, and moves *toward* matching N64 behavior, not away from it.
Every other deviation from the decompiled source is one of the documented
32→64-bit pointer-width ABI corrections described above.
**One issue, found and fixed before v0.3.0 shipped**: the port's
random-number generator (`port/src/random.c`) was documented as a bit-exact
port of the N64's PRNG, but wasn't — a shift-operation helper didn't match
the real MIPS64 instruction semantics it was meant to mirror. This was
independently confirmed by hand-tracing the assembly against the C port,
tracked as finding D284, and fixed before the release (verified clean
across a full 21-level sweep, no crashes). It affected loot placement, AI
behavior variance, and replay-state determinism from the very first random
draw, in every release before v0.3.0. Because of the fix, RNG-derived
output — including the save-file CRC — changed, which meant existing save
files and any previously recorded replays no longer validate; v0.3.0's
release notes describe the automatic save migration that handles this. We'd
rather say this plainly than let a "verified bit-exact" claim stand
uncorrected, both when it was wrong and now that it's fixed.
Everything else checked (the documented ABI/pointer-width exceptions used
for the 32-to-64-bit transition, a sample of the finding log's own claimed
fixes against the actual current code) matched what's documented.
---
*This page is kept current alongside future releases. Its source review is a
manual, human-reviewed process today — see the project roadmap for plans to
partially automate the mechanical parts of it.*
+6
View File
@@ -49,6 +49,12 @@ void gfx_set_mipmap_filter(enum MipmapFilteringMode mode);
void gfx_set_fix_mip_textures(int on);
void gfx_set_wrap_fix(int on);
void gfx_set_anisotropy_level(int level); /* 1 = off; clamped to GL max */
void gfx_set_safe_area_crop(int on); /* crop the N64 TV-overscan safe-area margin instead of showing it as black bars */
/* On-window pixel rect (top-left origin) the full VI logical canvas (0,0)-
* (SCREEN_WIDTH, SCREEN_HEIGHT) currently maps to, honoring the safe-area
* crop above -- for inverting a window mouse click into logical 2D UI space
* (see port/src/optionsoverlay.c, D316). */
void gfx_get_ui_screen_rect(int32_t *outX, int32_t *outY, int32_t *outW, int32_t *outH);
void gfx_texture_cache_clear(void);
void gfx_texture_cache_delete(const uint8_t *orig_addr);
void gfx_texture_cache_delete_range(const uint8_t *start, const uint8_t *end);
+18 -2
View File
@@ -510,11 +510,27 @@ static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shad
}
}
/* D266: CVG_X_ALPHA ("texedge") modelled on the RDP's actual semantics,
* keyed on the alphacompare field. The inherited line quantized to
* opaque/discard at a hardcoded 0.19: it kept texels the N64 discards and
* forced survivors fully opaque, defeating the ALPHA_CVG_SEL blend.
* GE's Surface 1 IA4 tree strip (D236/D265) rendered as a wall because of
* it. Now: AC_DITHER -> dithered discard; AC_THRESHOLD -> discard only
* fully-transparent; AC_NONE -> discard below 0x80 and let survivors keep
* their alpha for blending. (AC_DECAL never sets SHADER_OPT_TEXTURE_EDGE,
* see gfx_pc.cpp.) */
if (cc_features.opt_texture_edge && cc_features.opt_alpha) {
append_line(fs_buf, &fs_len, " if (texel.a > 0.19) texel.a = 1.0; else discard;");
if (cc_features.opt_noise) {
append_line(fs_buf, &fs_len,
" if (texel.a < ((random(vec3(floor(gl_FragCoord.xy * noise_scale), float(frame_count))) + 1.0) / 2.0)) discard;");
} else if (cc_features.opt_alpha_threshold) {
append_line(fs_buf, &fs_len, " if (texel.a < 1.0 / 256.0) discard;");
} else {
append_line(fs_buf, &fs_len, " if (texel.a < 0.5) discard;");
}
}
if (cc_features.opt_alpha && cc_features.opt_noise) {
if (cc_features.opt_alpha && cc_features.opt_noise && !cc_features.opt_texture_edge) {
append_line(fs_buf, &fs_len,
" texel.a *= floor(clamp(random(vec3(floor(gl_FragCoord.xy * noise_scale), float(frame_count))) + "
"texel.a, 0.0, 1.0));");
+434 -14
View File
@@ -44,6 +44,9 @@ uintptr_t gfxFramebuffer;
#define SCALE_4_8(VAL_) ((VAL_)*0x11)
#define SCALE_8_4(VAL_) ((VAL_) / 0x11)
#define SCALE_3_8(VAL_) ((VAL_)*0x24)
/* D266: RDP IA4 is I2:A2 (2-bit intensity, 2-bit alpha), not the I3:A1 the
* PD-derived importer assumed. */
#define SCALE_2_8(VAL_) ((VAL_)*0x55)
#define SCALE_8_3(VAL_) ((VAL_) / 0x24)
// SCREEN_WIDTH and SCREEN_HEIGHT are defined in the headerfile
@@ -136,6 +139,12 @@ static struct RSP {
const struct NormalColor *vertex_colors; //[MAX_VERTEX_COLORS];
} rsp;
/* D236 pass 16 (TEMP): segment byte (top byte of the raw segmented address)
* of the most recent G_VTX load, so a later triangle-time probe can report
* which segment the tree class's vertices actually came from. See the
* G_VTX case comment below for why. Remove once D236 pass 16 concludes. */
static uint8_t g_d236_last_vtx_seg = 0xFF;
struct RawTexMetadata {
uint16_t width, height;
float h_byte_scale = 1, v_pixel_scale = 1;
@@ -230,6 +239,34 @@ static bool game_renders_to_framebuffer;
static int game_framebuffer;
static int game_framebuffer_msaa_resolved;
/* Safe-area (TV-overscan) crop. GE's own N64 game code insets its normal
* single-player "Full" gameplay viewport a fixed margin from the true VI
* framebuffer edges (src/fr.h: VIEWPORT_HEIGHT_DEFAULT_NTSC=220 of a
* 240-line frame -- ~8% top+bottom; PAL's equivalent is already full-height,
* no margin) and separately fills that margin with black rectangles
* (src/fr.c viSetupScreensForNumPlayers). On a real CRT that margin falls
* in the invisible overscan region; this port displays the full VI frame,
* so the margin shows up as literal top/bottom black bars.
*
* g_gpSafeTop/g_gpSafeHeight cache the most recently set SP viewport's raw
* (pre window-scale) Y bounds, in the same native-VI-Y units gfx_pc.cpp
* uses elsewhere (see gfx_calc_and_set_viewport). gfx_adjust_viewport_or_
* scissor remaps every screen-space Y (the 3D viewport itself AND RDP
* fill-rect/texture-rect draws, which deliberately bypass the current SP
* viewport and use the full VI canvas -- see gfx_draw_rectangle's
* default_viewport) against this cached region instead of the full VI
* canvas, so it fills the window edge to edge. Self-gating: front-end/menus
* and PAL "Full" never inset their viewport (top=0, full height), so this
* is a no-op there; -1 sentinel means "no viewport captured yet",
* passthrough (identical to original behavior). */
static float g_gpSafeTop = 0.0f;
static float g_gpSafeHeight = -1.0f;
static bool g_safe_area_crop_enabled = true;
extern "C" void gfx_set_safe_area_crop(int on) {
g_safe_area_crop_enabled = !!on;
}
uint32_t gfx_msaa_level = 1;
static bool dropped_frame;
@@ -686,6 +723,32 @@ static void import_texture_rgba16(int tile, const LoadedTexture& loaded_texture,
const uint32_t width = rdp.texture_tile[tile].line_size_bytes / 2;
const uint32_t height = size_bytes / rdp.texture_tile[tile].line_size_bytes;
#ifdef PORT
/* D252 diag (temporary): compare the line_size_bytes-derived width/height
* against the SETTILESIZE-derived rdp.texture_tile[tile].width/height, and
* dump the raw texel data once per distinct (addr,size) pair so a fire
* tile can be checked against the source bytes by hand. Remove once
* D252 is resolved. */
static int s_d252 = -1;
if (s_d252 < 0) s_d252 = getenv("GE_D252") != NULL;
if (s_d252) {
static std::set<std::pair<const void*, uint32_t>> s_d252_seen;
if (s_d252_seen.emplace((const void*)addr, size_bytes).second) {
const auto& t = rdp.texture_tile[tile];
fprintf(stderr,
"[D252] tile=%d addr=%p size_bytes=%u line_size_bytes=%u "
"computed(w=%u h=%u) settilesize(w=%u h=%u) uls=%u ult=%u lrs=%u lrt=%u "
"masks=%u maskt=%u tmem=%u\n",
tile, (const void*)addr, size_bytes, line_size_bytes, width, height,
t.width, t.height, t.uls, t.ult, t.lrs, t.lrt, t.masks, t.maskt, t.tmem);
char rawpath[256];
snprintf(rawpath, sizeof(rawpath), "scratch/d252_raw_%p_%u.bin", (const void*)addr, size_bytes);
FILE* rf = fopen(rawpath, "wb");
if (rf) { fwrite(addr, 1, size_bytes, rf); fclose(rf); }
}
}
#endif
gfx_rapi->upload_texture(tex_upload_buffer, width, height, gen_mipmaps);
// DumpTexture(loaded_texture.otr_path, rgba32_buf, width, height);
}
@@ -724,13 +787,16 @@ static void import_texture_ia4(int tile, const LoadedTexture& loaded_texture, bo
for (uint32_t i = 0; i < size_bytes * 2; i++, dest += 4) {
const uint8_t byte = addr[i / 2];
const uint8_t part = (byte >> (4 - (i % 2) * 4)) & 0xf;
const uint8_t intensity = part >> 1;
const uint8_t alpha = part & 1;
const uint8_t c = SCALE_3_8(intensity);
/* D266: I2:A2 -- the hardware IA4 layout. The old I3:A1 read made the
* A2=1 canopy pixels fully opaque and the A2=2 edge dither invisible
* (D236/D265 tree "wall"). */
const uint8_t intensity = part >> 2;
const uint8_t alpha = part & 3;
const uint8_t c = SCALE_2_8(intensity);
dest[0] = c;
dest[1] = c;
dest[2] = c;
dest[3] = alpha ? 255 : 0;
dest[3] = SCALE_2_8(alpha);
}
const uint32_t width = rdp.texture_tile[tile].line_size_bytes * 2;
@@ -1088,6 +1154,36 @@ static void import_texture(int i, int tile, bool importReplacement) {
}
}
#ifdef PORT
/* D157-I (M-158): import census for the D219/D252 rainbow-spark repro.
* Logs every small-texture (<= 16 KiB) import inside the known-bad frame
* window of the Bunker1 -level_09 repro, deduped per source address (first
* 3 hits each). Paired with the D157T vertex probe: the census names the
* exact source addresses bound during the spark frames (the smoke/flare/
* scattered family should appear in lockstep with the scripted shots),
* which the vertex lines can then be grepped by. Remove once root-caused.
* D250: cached getenv, inert unless GE_D157 is set. */
static int ge_d157i = -1;
if (ge_d157i < 0) ge_d157i = getenv("GE_D157") != NULL;
if (ge_d157i) {
extern uint32_t num_dls;
/* 2026-09-18: dropped the old num_dls in [90,240] gate here too (see
* the matching D157T comment) -- per-address dedup below already
* bounds this to 3 lines per distinct texture for the whole session. */
if (loaded_texture.size_bytes <= 16384) {
static std::map<const void*, int> d157i_seen;
int& n = d157i_seen[(const void*)orig_addr];
if (++n <= 3) {
sysLogPrintf(LOG_NOTE,
"D157I: frame=%u hit#%d addr=%p fmt=%u siz=%u size=%u line=%u tile=%u tmem=%u",
num_dls, n, (const void*)orig_addr, fmt, siz,
loaded_texture.size_bytes, loaded_texture.line_size_bytes,
tile, rdp.texture_tile[tile].tmem);
}
}
}
#endif
static int ge_texdump = -1;
if (ge_texdump < 0) ge_texdump = getenv("GE_TEXDUMP") != NULL;
if (ge_texdump) {
@@ -1469,6 +1565,89 @@ static void gfx_sp_vertex(size_t n_vertices, size_t dest_index, const Vtx* verti
d->color.b = vcn->b;
}
#ifdef PORT
/* D157 diag (temporary, session 2026-09-16/17): does bullet_spark_render
* (glass2.c) actually draw with G_LIGHTING on? The M-110 mechanism
* (this vertex's v.cn[]/n.n[] union reinterpretation above) was
* already ruled out for explosion.c's particles (5000/5000 samples
* measured LIGHTING=off, see the D219 comment a few hundred lines
* below) -- don't assume it holds for glass2.c's bullet sparks
* without measuring. Key on the ORIGINAL authored bytes
* (v->v.cn[], readable regardless of which branch fired above,
* since n.n[]/cn[] are the same union storage) matching one of
* glass2.c's g_BulletSparkColors[] entries exactly. */
static int ge_d157 = -1;
if (ge_d157 < 0) ge_d157 = getenv("GE_D157") != NULL;
if (ge_d157) {
static const uint8_t known[][4] = {
{0xFF,0xFF,0xFF,0xFF}, {0xFF,0xFF,0xC8,0xFF}, {0xFF,0x00,0x00,0xFF},
};
for (const auto& k : known) {
if (v->v.cn[0] == k[0] && v->v.cn[1] == k[1] && v->v.cn[2] == k[2] && v->v.cn[3] == k[3]) {
sysLogPrintf(LOG_NOTE,
"D157: bulletspark-color-match cn=(%d,%d,%d,%d) geometry_mode=%08x LIGHTING=%s -> shaded=(%d,%d,%d)",
v->v.cn[0], v->v.cn[1], v->v.cn[2], v->v.cn[3], rsp.geometry_mode,
(rsp.geometry_mode & G_LIGHTING) ? "ON" : "off",
(int)d->color.r, (int)d->color.g, (int)d->color.b);
break;
}
}
/* D157-B (M-158): the exact-color-match key above NEVER caught the
* pale-yellow (255,255,200) door-spark vertices in the Bunker1
* rainbow repro (only 2 white hits, both LIGHTING=off), so per
* D219 M-157's revised next step this sibling block keys on
* TEXTURE IDENTITY instead: any vertex whose render tile (tile 0)
* holds a small loaded texture (size <= 16 KiB -- the spark
* family is IA 64x64, promoted 8b->16b by texSelect) inside the
* known-bad frame window of that repro (shots at frames 60-350,
* visible rainbow frames 108-220). Logs the full RGBA path per
* vertex: authored cn, shaded result, LIGHTING, geometry_mode,
* combine_mode (texSelect sets G_CC_MODULATEIA for these -- a
* different value here is a combiner-state leak), and the tile's
* source identity (addr/fmt/siz/size = the TextureCacheKey for
* non-CI textures, so cache-collision reasoning works offline).
* Frame window + size key are repro-specific; widen if re-running
* elsewhere. Remove once D219/D252 is root-caused. */
{
extern uint32_t num_dls;
/* 2026-09-18: dropped the old num_dls in [90,240] gate -- that
* window only fit the original short GE_INPUTSCRIPT repro; a
* real interactive session runs for thousands of frames before
* the player actually triggers the bug, so the gate silently
* discarded 100% of useful data in live play. The existing
* per-run hit caps below already bound log growth without it. */
{
const uint32_t tile0 = rdp.first_tile_index;
LoadedTexture& lt0 = rdp.loaded_texture[rdp.texture_tile[tile0].tmem];
if (lt0.addr && lt0.size_bytes <= 16384) {
static int d157b_n = 0;
d157b_n++;
/* Raised from 400/1-in-200 (sized for a ~250-frame
* scripted repro) to a much larger continuous window
* so a real, minutes-long play session doesn't drop
* to sparse 1-in-200 sampling before the player
* actually triggers the bug. */
if (d157b_n <= 20000 || (d157b_n % 50) == 0) {
sysLogPrintf(LOG_NOTE,
"D157T: frame=%u cn=(%d,%d,%d,%d) shaded=(%d,%d,%d) LIGHTING=%s geom=%08x combine=%llx | "
"tile0=%u tmem=%u fmt=%u siz=%u addr=%p size=%u line=%u | uv=(%d,%d)",
num_dls,
v->v.cn[0], v->v.cn[1], v->v.cn[2], v->v.cn[3],
(int)d->color.r, (int)d->color.g, (int)d->color.b,
(rsp.geometry_mode & G_LIGHTING) ? "ON" : "off", rsp.geometry_mode,
(unsigned long long)rdp.combine_mode,
tile0, rdp.texture_tile[tile0].tmem,
rdp.texture_tile[tile0].fmt, rdp.texture_tile[tile0].siz,
(const void*)lt0.addr, lt0.size_bytes, lt0.line_size_bytes,
(int)U, (int)V);
}
}
}
}
}
#endif
d->u = U;
d->v = V;
@@ -1581,6 +1760,158 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
// The whole triangle lies outside the visible area
return;
}
/* D288 diag (M-152, frame-gated M-155): the silo intro's
* screen-filling stray triangle (bounded to level_20 frames
* ~562-741) isn't explained by this function's existing D233/D106
* guards on static review -- log any triangle whose clip-space NDC
* bbox covers an unreasonable chunk of the screen, or whose bbox
* math goes non-finite, together with each vertex's w and
* any_behind_camera, to catch it at draw time. M-155: the first
* ungated run fired 116k times in ~1500-2000 frames -- bbox_frac
* >0.15 alone is a common occurrence for ordinary near-camera
* geometry, not a distinguishing signature. Gated to the reported
* frame window (num_dls, same pattern as GE_D236RM below) and the
* threshold raised well above the ~0.15 "normal" baseline M-155
* established, so a hit here is actually rare. Remove once D288 is
* root-caused. */
static int ge_d288 = -1;
if (ge_d288 < 0) ge_d288 = getenv("GE_D288") != NULL;
if (ge_d288) {
extern uint32_t num_dls;
if (num_dls >= 550 && num_dls <= 755) {
float minx = std::fmin(std::fmin(v1->x / v1->w, v2->x / v2->w), v3->x / v3->w);
float maxx = std::fmax(std::fmax(v1->x / v1->w, v2->x / v2->w), v3->x / v3->w);
float miny = std::fmin(std::fmin(v1->y / v1->w, v2->y / v2->w), v3->y / v3->w);
float maxy = std::fmax(std::fmax(v1->y / v1->w, v2->y / v2->w), v3->y / v3->w);
float bbox_frac = (maxx - minx) * (maxy - miny) / 4.0f; // NDC quad is [-1,1]^2
if (!std::isfinite(bbox_frac) || bbox_frac > 0.5f) {
fprintf(stderr, "D288: dl=%u tri w=(%.6f,%.6f,%.6f) bbox_frac=%.3f any_behind=%d color=(%d,%d,%d)\n",
num_dls, v1->w, v2->w, v3->w, bbox_frac, (int)any_behind_camera,
v1->color.r, v1->color.g, v1->color.b);
}
}
}
}
/* D236 pass 10 (M-155, TEMP): does the 0x0c184b50 render-mode class's
* per-frame triangle count actually vary frame-to-frame WITHIN one
* deterministic -level_36 boot, or is D280's "run-to-run dependent"
* observation only visible ACROSS separate process launches (which
* would point at room-streaming state that isn't reset/seeded the
* same way twice, not at true per-frame nondeterminism)? Log the
* count once per display list (frame) while it's nonzero. Remove
* once D236 pass 10 concludes. */
static int s_d236rm = -1;
if (s_d236rm < 0) s_d236rm = getenv("GE_D236RM") != NULL;
if (s_d236rm) {
static uint32_t d236rm_last_dl = 0xFFFFFFFFu;
static uint32_t d236rm_count = 0;
extern uint32_t num_dls;
if (num_dls != d236rm_last_dl) {
if (d236rm_count != 0 || (num_dls % 300) == 0) {
fprintf(stderr, "D236RM dl=%u count=%u\n", d236rm_last_dl, d236rm_count);
}
d236rm_last_dl = num_dls;
d236rm_count = 0;
}
if (rdp.other_mode_l == 0x0c184b50u) {
d236rm_count++;
}
}
/* D236 pass 12 (M-191, TEMP): new angle, not tried by passes 1-11 (all of
* which focused on the tree-card class's z/w and instantiation counts).
* Both the noise "wall" (oml=0xc81049d8) and the discrete tree-card class
* (oml=0x0c184b50) decode to Z_CMP=1/Z_UPD=0/ZMODE=DEC (checked offline
* against both full oml words) -- NEITHER writes the depth buffer. Two
* no-z-write decals drawn against the same static (z-writing) background
* never actually depth-test against EACH OTHER: each is tested only
* against the background's z, so on-screen precedence between the two
* decals is decided purely by which one is submitted LAST in the frame's
* draw list, not by which one's own z/w is smaller (the z/w comparisons
* all 11 prior passes made are therefore not conclusive either way about
* visual winner). This probe tests that directly: track a per-frame
* submission-order counter (reset whenever num_dls changes, i.e. once
* per frame) and log the min/max order index seen for each of the two
* classes whenever num_dls changes and at least one of them appeared
* that frame -- if the noise class's order index is consistently HIGHER
* (drawn later) than the tree class's in frames where both appear, that
* directly explains "wall painted over trees" regardless of geometric
* depth, and points at a draw-order (not depth) bug. Remove once D236
* pass 12 concludes. */
static int s_d236order = -1;
if (s_d236order < 0) s_d236order = getenv("GE_D236ORDER") != NULL;
if (s_d236order) {
static uint32_t d236o_last_dl = 0xFFFFFFFFu;
static uint32_t d236o_seq = 0;
static uint32_t d236o_noise_min = 0, d236o_noise_max = 0, d236o_noise_n = 0;
static uint32_t d236o_tree_min = 0, d236o_tree_max = 0, d236o_tree_n = 0;
extern uint32_t num_dls;
if (num_dls != d236o_last_dl) {
if (d236o_noise_n != 0 || d236o_tree_n != 0) {
fprintf(stderr,
"D236ORDER dl=%u noise_n=%u noise_order=[%u,%u] tree_n=%u tree_order=[%u,%u] "
"tree_last_after_noise_last=%d\n",
d236o_last_dl, d236o_noise_n, d236o_noise_min, d236o_noise_max,
d236o_tree_n, d236o_tree_min, d236o_tree_max,
(d236o_tree_n != 0 && d236o_noise_n != 0) ? (int) (d236o_tree_max > d236o_noise_max) : -1);
}
d236o_last_dl = num_dls;
d236o_seq = 0;
d236o_noise_min = d236o_noise_max = d236o_noise_n = 0;
d236o_tree_min = d236o_tree_max = d236o_tree_n = 0;
}
if (rdp.other_mode_l == 0xc81049d8u) {
if (d236o_noise_n == 0) d236o_noise_min = d236o_seq;
d236o_noise_max = d236o_seq;
d236o_noise_n++;
} else if (rdp.other_mode_l == 0x0c184b50u) {
if (d236o_tree_n == 0) d236o_tree_min = d236o_seq;
d236o_tree_max = d236o_seq;
d236o_tree_n++;
}
d236o_seq++;
}
/* D236 pass 14 (M-19x, TEMP): pass 13's live capture ruled out draw
* order as the dominant cause even in the favorable (tree-after-noise,
* high tri count) zone -- redirecting to whether the tree class's own
* per-vertex alpha (D280's "gfog=0, CPU-baked bimodal 25/255" census)
* is actually LOW (near-transparent) for the close/high-count trees the
* live capture walked through. This render mode's blend equation is a
* standard alpha-blend decal (FORCE_BL, GBL c1/c2 = CLR_IN,A_IN ->
* CLR_MEM,1-A_IN -- see include/PR/gbi.h RM_AA_ZB_XLU_DECAL), so if the
* combiner's alpha output tracks vertex/SHADE alpha directly, a card
* baked near 25/255 (~10%) would blend almost invisibly over whatever
* was drawn under it -- looking exactly like "the noise wall shows
* through" even with correct geometry and correct draw order. Logs,
* once per triangle of this class: each vertex's raw color.a, the
* combine_mode word (to see which alpha slot actually feeds the
* blend), and 1/w (a cheap camera-distance proxy) so alpha can be
* correlated against "close" vs "far" the same way pass 13's live
* capture was read. Zero cost unset. Remove once D236 pass 14
* concludes. */
static int s_d236alpha = -1;
if (s_d236alpha < 0) s_d236alpha = getenv("GE_D236ALPHA") != NULL;
if (s_d236alpha && rdp.other_mode_l == 0x0c184b50u) {
static uint32_t d236a_hits = 0;
static uint64_t d236a_last_combine = 0xFFFFFFFFFFFFFFFFull;
if (d236a_hits < 4000) {
if (rdp.combine_mode != d236a_last_combine) {
fprintf(stderr, "D236ALPHA combine_mode=0x%016llx\n",
(unsigned long long)rdp.combine_mode);
d236a_last_combine = rdp.combine_mode;
}
fprintf(stderr,
"D236ALPHA a=(%u,%u,%u) invw=(%.4f,%.4f,%.4f) vtxseg=0x%02x\n",
v1->color.a, v2->color.a, v3->color.a,
(v1->w != 0.f) ? 1.f / v1->w : 0.f,
(v2->w != 0.f) ? 1.f / v2->w : 0.f,
(v3->w != 0.f) ? 1.f / v3->w : 0.f,
g_d236_last_vtx_seg);
d236a_hits++;
}
}
if ((rsp.geometry_mode & G_CULL_BOTH) != 0) {
@@ -1621,6 +1952,25 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
bool depth_test = ((rsp.geometry_mode & G_ZBUFFER) == G_ZBUFFER || (rdp.other_mode_l & G_ZS_PRIM) == G_ZS_PRIM) &&
((rdp.other_mode_h & G_CYC_1CYCLE) == G_CYC_1CYCLE || (rdp.other_mode_h & G_CYC_2CYCLE) == G_CYC_2CYCLE);
bool depth_update = (rdp.other_mode_l & Z_UPD) == Z_UPD;
/* D236 pass 12 (M-191, TEMP, diagnostic-only): both the noise "wall"
* (oml=0xc81049d8) and the discrete tree-card class (oml=0x0c184b50) are
* Z_CMP=1/Z_UPD=0 decals, and GE_D236ORDER above shows the noise class is
* ALWAYS submitted after the tree class in every sampled frame -- with
* neither writing depth, later-submitted always wins the color buffer
* regardless of which is geometrically closer. This experiment forces
* depth_update=true for the tree-card class ONLY, so it writes real
* depth; if the noise class (submitted later, same depth_compare=LEQUAL)
* now correctly fails its depth test against the tree cards' nearer z
* instead of overwriting them, that confirms draw-order+no-z-write is
* the actual visual-precedence mechanism. NOT a proposed fix (forcing
* z-write for a decal-mode class is not decomp-faithful either) -- purely
* to test the mechanism cheaply before deciding what a real fix looks
* like. Remove once D236 pass 12 concludes. */
static int s_d236zfix = -1;
if (s_d236zfix < 0) s_d236zfix = getenv("GE_D236ZFIX") != NULL;
if (s_d236zfix && rdp.other_mode_l == 0x0c184b50u) {
depth_update = true;
}
bool depth_compare = (rdp.other_mode_l & Z_CMP) == Z_CMP;
bool depth_source_prim = (rdp.other_mode_l & G_ZS_PRIM) == G_ZS_PRIM /* && gDP.primDepth.z == 1.0f */;
uint16_t zmode = rdp.other_mode_l & ZMODE_DEC;
@@ -1650,7 +2000,11 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
bool use_alpha =
(rdp.other_mode_l & (3 << 20)) == (G_BL_CLR_MEM << 20) && (rdp.other_mode_l & (3 << 16)) == (G_BL_1MA << 16);
const bool use_fog = ((rdp.other_mode_l >> 30) == G_BL_CLR_FOG) || ((rdp.other_mode_l >> 26) == G_BL_A_FOG);
const bool texture_edge = (rdp.other_mode_l & CVG_X_ALPHA) == CVG_X_ALPHA;
/* D266: G_AC_DECAL (alphacompare == 2) never discards, so it must not
* take the texedge path at all; NONE/THRESHOLD/DITHER are distinguished
* in gfx_opengl.cpp via the existing options. */
const bool ac_decal = (rdp.other_mode_l & (3U << G_MDSFT_ALPHACOMPARE)) == (2U << G_MDSFT_ALPHACOMPARE);
const bool texture_edge = (rdp.other_mode_l & CVG_X_ALPHA) == CVG_X_ALPHA && !ac_decal;
const bool use_noise = (rdp.other_mode_l & (3U << G_MDSFT_ALPHACOMPARE)) == G_AC_DITHER;
const bool use_2cyc = (rdp.other_mode_h & (3U << G_MDSFT_CYCLETYPE)) == G_CYC_2CYCLE;
const bool alpha_threshold = (rdp.other_mode_l & (3U << G_MDSFT_ALPHACOMPARE)) == G_AC_THRESHOLD;
@@ -1659,7 +2013,7 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
const bool use_modulate = use_alpha && (rsp.extra_geometry_mode & G_MODULATE_EXT) != 0;
const bool use_blur = (rdp.other_mode_h & (3U << G_MDSFT_TEXTFILT)) == G_TF_BLUR_EXT;
if (texture_edge) {
if ((rdp.other_mode_l & CVG_X_ALPHA) == CVG_X_ALPHA) {
use_alpha = true;
}
@@ -2062,10 +2416,10 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
"[D116/vbo] rect vtx%d x=%.4f y=%.4f u=%.4f v=%.4f "
"(raw v->u=%.1f texw=%.0f texw2=%u tile.uls=%d lrs=%d cms=%d)\n",
i, v_arr[i]->x, v_arr[i]->y, u / tex_width[t], v / tex_height[t],
v_arr[i]->u, tex_width[t], tex_width2[i],
rdp.texture_tile[rdp.first_tile_index + tile].uls,
rdp.texture_tile[rdp.first_tile_index + tile].lrs,
rdp.texture_tile[rdp.first_tile_index + tile].cms);
(double)v_arr[i]->u, (double)tex_width[t], (unsigned)tex_width2[i],
(int)rdp.texture_tile[rdp.first_tile_index + tile].uls,
(int)rdp.texture_tile[rdp.first_tile_index + tile].lrs,
(int)rdp.texture_tile[rdp.first_tile_index + tile].cms);
}
}
#endif
@@ -2239,10 +2593,35 @@ static void gfx_adjust_viewport_or_scissor(XYWidthHeight* area, bool preserve_as
// HACK: assume all target framebuffers have the same aspect
// Use floor/ceil to ensure scissor fully contains the logical region
// and prevents sub-pixel gaps at viewport edges
float x1 = area->x * RATIO_X;
float y1 = (SCREEN_HEIGHT - area->y) * RATIO_Y;
float x2 = (area->x + area->width) * RATIO_X;
float y2 = (SCREEN_HEIGHT - area->y + area->height) * RATIO_Y;
// g_gpSafeTop already plays the exact role SCREEN_HEIGHT plays below
// (both are the bottom-up Y value of the mapped region's TOP edge) --
// this reduces to the untouched original formula when crop is off.
const bool crop = g_safe_area_crop_enabled && g_gpSafeHeight > 0.0f;
const float safeTop = crop ? g_gpSafeTop : (float)SCREEN_HEIGHT;
const float safeHeight = crop ? g_gpSafeHeight : (float)SCREEN_HEIGHT;
const float ratioY = gfx_current_dimensions.height / safeHeight;
// D246 (findings.md): a consistent, exactly-1-logical-unit gap at the
// left AND right screen edges (measured empirically: 2px/3px/4px at
// RATIO_X 2/3/4 -- always exactly 1 unit of the 320-wide logical space,
// on both edges, symmetric, same underlying cause on every level tested)
// reveals whatever's drawn behind the foreground scene there (varies by
// level, e.g. sky/ambient colour) instead of scene content. Root cause
// not isolated to a specific game-code or fast3d call site despite a
// deep pass (viewport/scissor math here is provably exact at integer
// RATIO_X, ruling out a floor/ceil rounding bug) -- treat this as the
// same class of issue as the vertical safe-area crop above (D247/the
// TV-overscan margin) and fold it into the same toggle: trim the same
// fixed 1-unit margin from both edges rather than trying to force
// content to reach a boundary it may never actually be drawn to.
const float safeLeft = g_safe_area_crop_enabled ? 1.0f : 0.0f;
const float safeWidth = g_safe_area_crop_enabled ? (float)SCREEN_WIDTH - 2.0f : (float)SCREEN_WIDTH;
const float ratioX = gfx_current_dimensions.width / safeWidth;
float x1 = (area->x - safeLeft) * ratioX;
float y1 = (safeTop - area->y) * ratioY;
float x2 = (area->x + area->width - safeLeft) * ratioX;
float y2 = (safeTop - area->y + area->height) * ratioY;
area->x = std::floor(x1);
area->y = std::floor(y1);
@@ -2267,6 +2646,27 @@ static void gfx_adjust_viewport_or_scissor(XYWidthHeight* area, bool preserve_as
}
}
/* D316: the on-window pixel rect the full VI canvas (0,0)-(SCREEN_WIDTH,
* SCREEN_HEIGHT) currently maps to, i.e. exactly the rect gfx_draw_rectangle's
* default_viewport (below) resolves to once the safe-area crop above is
* applied. Reuses gfx_adjust_viewport_or_scissor itself (not a hand-derived
* inverse) so this can never drift from the real forward transform: port-side
* mouse-to-logical-2D-space mapping (port/src/optionsoverlay.c) needs this to
* invert clicks correctly when the crop shrinks that mapped rect below the
* full window (D316 -- the overlay's own click math previously assumed the
* logical canvas always fills the whole window, which is false whenever the
* last-set gameplay viewport was inset, e.g. NTSC "Full" removes ~8% top and
* bottom). Top-left origin, window pixel units -- matches SDL mouse coords
* and gfx_current_game_window_viewport's documented convention. */
extern "C" void gfx_get_ui_screen_rect(int32_t *outX, int32_t *outY, int32_t *outW, int32_t *outH) {
struct XYWidthHeight area = { 0, (int16_t)SCREEN_HEIGHT, (uint32_t)SCREEN_WIDTH, (uint32_t)SCREEN_HEIGHT };
gfx_adjust_viewport_or_scissor(&area, false);
*outX = area.x;
*outY = area.y;
*outW = (int32_t)area.width;
*outH = (int32_t)area.height;
}
static void gfx_calc_and_set_viewport(const Vp_t* viewport) {
// 2 bits fraction
float width = 2.0f * viewport->vscale[0] / 4.0f;
@@ -2279,6 +2679,14 @@ static void gfx_calc_and_set_viewport(const Vp_t* viewport) {
rdp.viewport.width = width;
rdp.viewport.height = height;
/* Cache the raw (pre window-scale) viewport bounds for the safe-area
* crop above -- guard against a degenerate/zero-height viewport so a
* later divide can't ever see one. */
if (height > 1.0f) {
g_gpSafeTop = y;
g_gpSafeHeight = height;
}
gfx_adjust_viewport_or_scissor(&rdp.viewport);
rdp.viewport_or_scissor_changed = true;
@@ -2971,6 +3379,18 @@ static void gfx_run_dl(Gfx* cmd) {
gfx_sp_texture(C1(16, 16), C1(0, 16), C0(11, 3), C0(8, 3), C0(0, 8));
break;
case G_VTX:
/* D236 pass 16 (TEMP): pass 15's GE_D236RAW dump proved every
* room-background Vtx (g_BgRoomInfo[].vertices) is alpha=255
* on load -- so the tree class's <=36/255 alpha measured at
* draw time (GE_D236ALPHA) can't be coming from that static
* table. Record which segment (top byte of the raw segmented
* address) the most recent G_VTX load came from, so the next
* triangle-time probe can report it -- distinguishes "still
* room background, something else touches it after load" from
* "not room background at all" (a different segment, e.g. a
* model/CPU-built-quad source) without guessing from source
* review alone. Remove once D236 pass 16 concludes. */
g_d236_last_vtx_seg = (uint8_t)(cmd->words.w1 >> 24);
gfx_sp_vertex(C0(0, 16) / sizeof(Vtx), C0(16, 4), (const Vtx*)seg_addr(cmd->words.w1));
break;
case G_DL: {
+439
View File
@@ -0,0 +1,439 @@
/* d318watchdog.c -- D318: port-side deadlock recovery for the Facility
* (level_34) Ourumov/Trevelyan execution softlock.
*
* What this is (full writeup: docs/dev/findings.md, D318):
*
* As-authored latent race, faithfully reproduced by the port (NOT a port
* bug -- every link in the chain is byte-matched ROM logic):
*
* 1. Ourumov (chr 78, ai_22) pre-aims at kneeling Trevelyan (chr 67)
* during his monologue: actor_aim_at_actor with TARGET_AIM_ONLY|
* TARGET_DONTTURN (attacktype 0x64). chrlvInitActAttack picks a RANDOM
* variant from the aim-animation group (randomGetNext() % len) and
* modelSetAnimation's it with animlooping=0.
* 2. The aim anim plays to its endframe H (the "hold" pose). With
* animlooping==0, modelTickAnim has no wrap logic; when the frame
* reaches endframe, modelConstrainOrWrapAnimFrame clamps BOTH framea
* and frameb to ceil(H), so framea==frameb, and the tail of
* modelSetAnimFrame2WithChrStuff (if (vb==va) animframe1 = va) then
* discards all sub-frame progress: the model is PINNED at H. That is
* how a hold pose works by design -- normally something else changes
* the attack state next.
* 3. If the gas cascade sets objective bit 0x04 (combat) mid-monologue,
* ai_22 derails to section 0x2a and off=307 runs
* actor_fire_or_aim_at_target_update, which SUCCEEDS while the pre-aim
* is active: attacktype becomes 0x04, entityid becomes Trevelyan, and
* chrlvAttackActionRelated sets a NEW endframe from the variant's
* recoil/shoot start frame -- but it never calls modelSetAnimation.
* 4. Branch (determined by the random variant chosen in step 1):
* - new endframe <= H: chrlvTickAttackCommon's "endframe <= frame"
* block fires next tick and its unk31 path calls modelSetAnimation
* again -> pin broken -> anim advances into the shoot window ->
* c78 live-fires Trevelyan -> he dies -> attack ends -> ACT_STAND
* -> the stop check at ai_22 off=324 passes -> scene continues.
* - new endframe > H: nothing ever calls modelSetAnimation again
* (verified by probe: zero calls in frozen runs). The frame stays
* pinned at H < shoot_start, c78 never fires, Trevelyan never
* dies, and ai_22's off=324 stop check (chrHasStoppedOrPatroling,
* TRUE only for ACT_STAND/ACT_ANIM/ACT_PATROL) can never pass
* while c78 is in ACT_ATTACK -> PERMANENT SOFTLOCK.
*
* The N64 has the identical race and the identical softlock; this watchdog
* is a deliberate port-layer accommodation (same pattern as the D202 M-66b
* port-side expiration). It detects the exact unrecoverable signature and,
* after a long margin, re-initializes c78's attack via sub_GAME_7F025560 --
* the same entry point the game itself uses for a fresh attack (chrlvTickAttack
* type_of_motion==2 calls it with (attacktype, entityid)). That runs
* chrlvInitActAttack: random variant pick + modelSetAnimation(start_frame),
* which breaks the framea==frameb pin. c78 then live-fires Trevelyan exactly
* like the natural escape path (verified: escape runs kill c67 within ~230
* ticks of a fresh init), his attack sequence completes, he reaches
* ACT_STAND, the stop check at off=324 passes, and ai_22 proceeds as
* authored (the off=333 headshot lands on the corpse -- the same no-op the
* game already handles when Trevelyan dies of gas before the execution shot).
*
* NOTE: merely killing Trevelyan (e.g. via handles_shot_actors) is NOT
* enough -- a pinned attack has no internal completion event tied to target
* death, so c78 would stay in ACT_ATTACK and the stop check would still
* never pass (verified by probe). The attack state itself must be re-seeded.
*
* Signature (all must hold for D318_DEADLOCK_TICKS consecutive ticks):
* - chr 78 exists, is on AI list 0x0417 (ai_22), offset in [321, 333)
* (the sleep/stop-check loop; 333 is the headshot command itself),
* - chr 78 actiontype == ACT_ATTACK with act_attack.entityid == 67
* (targeting Trevelyan -- the derailed state; legitimate combat at this
* offset targets Bond, ent=1),
* - chr 67 exists, has a prop, and is not dead.
*
* In every non-deadlocked state the watchdog is inert: the escape path
* kills Trevelyan within ~25-100 ticks (60x under the threshold) and
* legitimate combat never targets chr 67 from this loop. One-shot per
* OCCURRENCE: after firing, it re-arms once the signature has been absent
* for D318_REARM_TICKS consecutive ticks (the deadlock is gone -- recovered
* or the player moved on), so a later replay of Facility in the same
* session (AllUnlocked) is still covered. Logs its detection and
* intervention for playtest audit.
*
* Opt out (research only): GE_D318W=0
*/
#include <stdlib.h>
#include <string.h>
#include "PR/os.h"
#include "bondtypes.h"
#include "chr.h"
#include "chrai.h"
#include "chraction.h"
#include "lv.h"
#include "model.h"
#include "random.h"
#ifdef PORT
/* chrai.c defines these but chrai.h does not declare them. */
extern s32 chraiGetAIListID(AIRecord *AIList, bool *isGlobalAIList);
/* chraction.c internal (not in chraction.h): the fresh-attack entry point.
* Signature from the definition; call form mirrors chrlvTickAttack's own
* re-init at chraction.c:7401. */
extern void sub_GAME_7F025560(ChrRecord *self, s32 attack_type, s32 arg2);
#define D318_LIST_AI22 0x0417 /* Ourumov's execution list (ai_22) */
#define D318_OFF_LOOP_MIN 321 /* label 0x01: sleep/stop-check loop */
#define D318_OFF_LOOP_MAX 333 /* exclusive: the headshot command */
#define D318_OURUMOV_CHR 78
#define D318_TREVELYAN_CHR 67
#define D318_DEADLOCK_TICKS 600 /* 10 s at 60 Hz; escape takes < 2 s */
#define D318_REARM_TICKS 600 /* signature-absent ticks before re-arm */
static ChrRecord *d318wFindChr(s16 chrnum)
{
s32 i;
for (i = 0; i < g_NumChrSlots; i++)
{
if (g_ChrSlots[i].chrnum == chrnum)
{
return &g_ChrSlots[i];
}
}
for (i = 0; i < g_ActiveChrsCount; i++)
{
if (g_ActiveChrs[i].chrnum == chrnum)
{
return &g_ActiveChrs[i];
}
}
return NULL;
}
void d318WatchdogTick(void)
{
static int s_enabled = -1; /* -1 uncached, 0 off (GE_D318W=0), 1 on */
static s32 s_run = 0; /* consecutive ticks the signature has held */
static s32 s_absent = 0; /* consecutive ticks signature absent */
static bool s_fired = FALSE;
ChrRecord *c78;
ChrRecord *c67;
bool g = FALSE;
bool sig;
if (s_enabled < 0)
{
const char *e = getenv("GE_D318W");
s_enabled = (e && e[0] == '0') ? 0 : 1;
}
if (!s_enabled)
{
return;
}
c78 = d318wFindChr(D318_OURUMOV_CHR);
c67 = d318wFindChr(D318_TREVELYAN_CHR);
sig = (c78 && c78->ailist
&& chraiGetAIListID(c78->ailist, &g) == D318_LIST_AI22
&& c78->aioffset >= D318_OFF_LOOP_MIN && c78->aioffset < D318_OFF_LOOP_MAX
&& c78->actiontype == ACT_ATTACK
&& (s32)c78->act_attack.entityid == D318_TREVELYAN_CHR
&& c67 && c67->prop && !chrIsDead(c67));
if (sig)
{
s_absent = 0;
if (!s_fired)
{
if (s_run == 1)
{
osSyncPrintf("D318W: t=%d deadlock signature detected (c78 ai_22 off=%d "
"ACT_ATTACK ent=67, c67 alive) -- starting %d-tick confirmation window\n",
(int)g_GlobalTimer, (int)c78->aioffset, D318_DEADLOCK_TICKS);
}
s_run++;
if (s_run >= D318_DEADLOCK_TICKS)
{
osSyncPrintf("D318W: t=%d D318 execution deadlock confirmed -- re-initializing "
"c78's attack (sub_GAME_7F025560 atk=0x%x ent=%d) to break the anim pin\n",
(int)g_GlobalTimer, (unsigned)c78->act_attack.attacktype,
(int)c78->act_attack.entityid);
sub_GAME_7F025560(c78, (s32)c78->act_attack.attacktype, (s32)c78->act_attack.entityid);
s_fired = TRUE;
}
}
}
else
{
s_run = 0;
/* One-shot per occurrence: once the deadlock signature has been
* clear for a while after a fire, re-arm so a later replay of
* Facility in the same process (AllUnlocked) is still covered. */
if (s_fired && ++s_absent >= D318_REARM_TICKS)
{
s_fired = FALSE;
s_absent = 0;
osSyncPrintf("D318W: t=%d signature clear for %d ticks -- watchdog re-armed\n",
(int)g_GlobalTimer, D318_REARM_TICKS);
}
}
}
/* ------------------------------------------------------------------ */
/* D318T: diagnostic timeline for the "Ourumov shoots Trevelyan the */
/* moment his lines begin" report (2026-09-20). Env-gated, OFF by */
/* default; enable with GE_D318T=1. Port-side read-only: logs state */
/* transitions so a single playtest captures which combat trigger */
/* (gas bit 0x04 vs Bond near-miss flag) derailed ai_22, and how many */
/* ticks after the monologue started it landed. No game state is */
/* touched. */
/* */
/* Background: the ONLY path by which c78 live-fires Trevelyan */
/* (entityid 67) in this scene is section 0x2a -> off=307 */
/* actor_fire_or_aim_at_target_update succeeding while the pre-aim */
/* (ACT_ATTACK + AIM_ONLY|DONTTURN) is active. That requires a combat */
/* trigger inside the monologue loop: gas bit 0x04 (off=75) or */
/* chrIfNearMiss (off=87, CHRFLAG_NEAR_MISS -- set by Bond bullets */
/* passing c78's bounds, cleared on c78's next tick). The freeze */
/* (D318) and the immediate-fire are the two branches of the same */
/* derailed state (random aim-variant endframe vs the hold frame H). */
/* ------------------------------------------------------------------ */
/* Manual hex parse: MinGW's strtoull returns the sign-extended low 32 bits
* for values above LLONG_MAX (verified: "deadbeefcafe0123" ->
* ffffffffcafe0123). Parse by hand; stops at '@' or non-hex. */
static u64 d318wParseHex(const char *s)
{
u64 v = 0;
while (*s && *s != '@')
{
char c = *s++;
v <<= 4;
if (c >= '0' && c <= '9') { v |= (u64)(c - '0'); }
else if (c >= 'a' && c <= 'f') { v |= (u64)(c - 'a' + 10); }
else if (c >= 'A' && c <= 'F') { v |= (u64)(c - 'A' + 10); }
else { break; }
}
return v;
}
void d318TimelineTick(void)
{
/* E-B seed injection: GE_RSEED_LV=<hex64>[@tick] -- set g_randomSeed to
* the value read from N64 RAM (0x80024460) at a canonical moment, so the
* port's stream can be A/B-compared against the original from that point
* on. Test-only; inert without the env var. The DET line logs the running
* seed every 60 ticks for verifying stream parity afterwards. */
{
static int s_seedArmed = 1;
static u64 s_seedVal = 0;
static s32 s_seedTick = 0;
if (s_seedArmed)
{
const char *e = getenv("GE_RSEED_LV");
s_seedArmed = 0;
if (e && e[0])
{
const char *at = strchr(e, '@');
s_seedVal = d318wParseHex(e); /* MinGW strtoull mangles >LLONG_MAX */
s_seedTick = at ? (s32)strtol(at + 1, NULL, 10) : 0;
}
}
if (s_seedVal && (s32)g_GlobalTimer >= s_seedTick)
{
u64 v = s_seedVal;
g_randomSeed = v;
s_seedVal = 0;
osSyncPrintf("D318T: t=%d RSEED_LV -> %016llx\n",
(int)g_GlobalTimer, (unsigned long long)v);
}
}
static int s_on = -1; /* -1 uncached, 0 off, 1 on (GE_D318T=1) */
static s32 s_obj = -1; /* last logged objectiveregisters1 */
static s32 s_off = -1; /* last logged c78 aioffset */
static s32 s_act = -1; /* last logged c78 actiontype */
static s32 s_atk = -1; /* last logged c78 act_attack.attacktype */
static s32 s_ent = -1; /* last logged c78 act_attack.entityid */
static s32 s_nm = -1; /* last logged c78 NEAR_MISS flag state */
static f32 s_dmg = -1.0f;
static bool s_dead = FALSE;
static s32 s_hb = 0;
static s32 s_det = 0; /* E1 determinism fingerprint counter */
ChrRecord *c78;
ChrRecord *c67;
if (s_on < 0)
{
const char *e = getenv("GE_D318T");
s_on = (e && e[0] == '1') ? 1 : 0;
}
if (!s_on)
{
return;
}
c78 = d318wFindChr(D318_OURUMOV_CHR);
c67 = d318wFindChr(D318_TREVELYAN_CHR);
/* Objective bitfield transitions (the derail triggers live here: */
/* 0x04 gas/combat, 0x20 surrender/monologue, 0x40 flee). */
if (s_obj != objectiveregisters1)
{
osSyncPrintf("D318T: t=%d obj bits 0x%08x -> 0x%08x\n",
(int)g_GlobalTimer, (unsigned)s_obj, (unsigned)objectiveregisters1);
s_obj = objectiveregisters1;
}
if (c78)
{
s32 nm = (c78->chrflags & CHRFLAG_NEAR_MISS) != 0;
/* D318 anim-internals line: catches the kneel-attack route */
/* (unk54==0 => sub_GAME_7F0256F0 "chrAttackKneel" re-inits) and the */
/* hold/pin frame values, for the "bending down too far" report. */
{
static s32 s_tom = -1;
static u32 s_u54 = 0xFFFFFFFFu;
static f32 s_endf = -1.0f;
s32 tom = (s32)c78->act_attack.type_of_motion;
u32 u54 = c78->act_attack.unk54;
f32 endf = c78->model ? modelGetAnimEndFrame(c78->model) : -1.0f;
if (s_tom != tom || s_u54 != u54 || s_endf != endf)
{
osSyncPrintf("D318T: t=%d c78 anim mot=%d unk54=%u endframe=%.2f frame1=%.2f (off=%d)\n",
(int)g_GlobalTimer, tom, (unsigned)u54,
(double)endf,
(double)(c78->model ? modelGetAnimFrame(c78->model) : 0.0f),
(int)c78->aioffset);
s_tom = tom;
s_u54 = u54;
s_endf = endf;
}
}
if (s_off != (s32)c78->aioffset || s_act != (s32)c78->actiontype)
{
const char *phase = "?";
if (c78->aioffset >= 38 && c78->aioffset < 44) { phase = "pre-aim(off38)"; }
else if (c78->aioffset >= 44 && c78->aioffset < 69) { phase = "wait-loop(off44)"; }
else if (c78->aioffset >= 69 && c78->aioffset < 272){ phase = "MONOLOGUE(off69+)"; }
else if (c78->aioffset >= 272 && c78->aioffset < 295){ phase = "execution(0x13)"; }
else if (c78->aioffset >= 302 && c78->aioffset < 333){ phase = "COMBAT-0x2a"; }
osSyncPrintf("D318T: t=%d c78 off=%d [%s] act=%d atk=0x%x ent=%d\n",
(int)g_GlobalTimer, (int)c78->aioffset, phase,
(int)c78->actiontype, (unsigned)c78->act_attack.attacktype,
(int)c78->act_attack.entityid);
s_off = (s32)c78->aioffset;
s_act = (s32)c78->actiontype;
}
if (s_atk != (s32)c78->act_attack.attacktype || s_ent != (s32)c78->act_attack.entityid)
{
osSyncPrintf("D318T: t=%d c78 ATTACK CHANGE atk=0x%x ent=%d (off=%d)\n",
(int)g_GlobalTimer, (unsigned)c78->act_attack.attacktype,
(int)c78->act_attack.entityid, (int)c78->aioffset);
s_atk = (s32)c78->act_attack.attacktype;
s_ent = (s32)c78->act_attack.entityid;
}
if (s_nm != nm)
{
osSyncPrintf("D318T: t=%d c78 NEAR_MISS flag %s (Bond bullet passed his bounds)\n",
(int)g_GlobalTimer, nm ? "SET" : "cleared");
s_nm = nm;
}
}
if (c67)
{
bool dead = chrIsDead(c67);
if (s_dmg != c67->damage || s_dead != dead)
{
osSyncPrintf("D318T: t=%d c67(Trevelyan) dmg=%.2f/%.2f %s\n",
(int)g_GlobalTimer, (double)c67->damage, (double)c67->maxdamage,
dead ? "DEAD" : "");
s_dmg = c67->damage;
s_dead = dead;
}
}
/* Heartbeat while c78 is on his execution list, so gaps are visible. */
if (c78 && c78->ailist)
{
bool g = FALSE;
if (chraiGetAIListID(c78->ailist, &g) == D318_LIST_AI22 && (++s_hb >= 60))
{
s_hb = 0;
osSyncPrintf("D318T: t=%d c78 hb off=%d act=%d atk=0x%x ent=%d obj=0x%08x\n",
(int)g_GlobalTimer, (int)c78->aioffset, (int)c78->actiontype,
(unsigned)c78->act_attack.attacktype, (int)c78->act_attack.entityid,
(unsigned)objectiveregisters1);
}
}
/* E1 determinism fingerprint: one line per second over ALL active chrs */
/* + the PRNG seed + objective bits. Two no-input runs that differ here */
/* are non-deterministic in the port (foundational); identical logs */
/* mean run-to-run variation is input-timing-driven. */
if (++s_det >= 60)
{
s32 h = 0x811C9DC5;
s32 i;
s_det = 0;
for (i = 0; i < g_ActiveChrsCount; i++)
{
ChrRecord *c = &g_ActiveChrs[i];
union { f32 f; u32 u; } xf, yf, zf, df;
xf.f = c->prop ? c->prop->pos.x : 0.0f;
yf.f = c->prop ? c->prop->pos.y : 0.0f;
zf.f = c->prop ? c->prop->pos.z : 0.0f;
df.f = c->damage;
h ^= c->chrnum; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= (s32)c->actiontype; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= (s32)c->aioffset; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= xf.u; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= yf.u; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= zf.u; h = (h * 0x01000193) ^ 0x9E3779B9;
h ^= df.u; h = (h * 0x01000193) ^ 0x9E3779B9;
}
osSyncPrintf("D318T: DET t=%d seed=%08x%08x obj=0x%08x h=%08x nchrs=%d\n",
(int)g_GlobalTimer,
(unsigned)(u32)(g_randomSeed >> 32), (unsigned)(u32)g_randomSeed,
(unsigned)objectiveregisters1, (unsigned)h, (int)g_ActiveChrsCount);
}
}
#endif /* PORT */
+143 -17
View File
@@ -292,7 +292,8 @@ static int mouseGrabbed = 1; /* released while the window is unfocused */
static int captureArmed = 0; /* user has clicked to lock (capture mode) */
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..80
static int gepdSens = 38; /* D194 Input.GepdSens: GEPD SENSITIVITY setting, range 1..500
(D304: widened from 1..80 to match Input.MouseTurnSpeed)
(20 -> 25 "a bit slow" -> 30 user-calibrated match point
-> 38: user asked defaults ~20-35% faster than that) */
static int aimBand = 20; /* aim mode: usable stick range above the 60 gate */
@@ -329,6 +330,11 @@ static int aimAbsolute = 1; /* D194: 1 = GEPD-style aim while RMB is hel
* crosshair locked at centre, mouse MOVEMENT
* rotates the view 1:1 in screen angle; 0 =
* legacy velocity stick from mouse delta. */
static int mouseDirectLook = 1; /* WI-1 (GEPD-INPUT-PLAN.md #89): 1 = hipfire
* writes vv_theta/vv_verta directly (see
* hipDirectCompute), same linear-in-px model
* as aim mode; 0 = legacy stick-curve path
* (MOUSE_TURN_GAIN), kept as an escape hatch. */
/* D194 GEPD-aim state. Touched ONLY in inputComputePad (game thread), the
* same confinement as every other static here. The per-poll grabbed delta
@@ -343,6 +349,7 @@ static int aimAbsolute = 1; /* D194: 1 = GEPD-style aim while RMB is hel
static double s_gepdCrossX = 0.0, s_gepdCrossY = 0.0;
static int s_gepdHeldPrev = 0; /* aim held last tick -> adopt on entry */
static int aimGepdCompute(double dxPx, double dyLook);
static int hipDirectCompute(double dxPx, double dyLook);
/* D194: bondview2's "look-ahead" pitch centreing (docentreupdown) arms during
* hip-fire walking whenever the pitch strays from the horizon target, and --
* once armed -- keeps pulling vv_verta back to it even in aim mode, EXCEPT
@@ -1044,6 +1051,19 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
} else {
double hipEdx = edx * lookDtScale, hipDyLook = dyLook * lookDtScale;
double hipSens = (mouseTurnSpeed / 100.0) * (mouseSensitivity / 100.0);
/* WI-1: direct camera write, same linear px->degree model as
* aim mode (GEPD's hipfire branch) -- bypasses the N64 stick's
* quadratic natural-turn curve entirely (D238/#89: that curve
* saturated at ~13 px/poll, making slow motion "almost
* unrecognised" and fast motion bang-bang). Falls through to
* the legacy stick path below when it declines (disabled, no
* player, or a safety gate is closed). */
if (mouseDirectLook && hipDirectCompute(hipEdx, hipDyLook)) {
/* Pitch handled inside hipDirectCompute too; nothing left
* to do for yaw/pitch this poll. Digital pitch-pulse
* (naturalPitchMode==0) still applies below only in the
* legacy path, so skip both branches here. */
} else {
sx += (int)(hipEdx * hipSens * MOUSE_TURN_GAIN);
if (naturalPitchMode) {
/* D194/D238: SOLITARE gives hipfire pitch the same
@@ -1071,6 +1091,7 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
hipPitchPhase = 0.0;
}
}
} /* end mouseDirectLook fallback (WI-1) */
}
}
@@ -1094,7 +1115,25 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
int rx = SDL_GameControllerGetAxis(pad, SDL_CONTROLLER_AXIS_RIGHTX);
int ry = SDL_GameControllerGetAxis(pad, SDL_CONTROLLER_AXIS_RIGHTY);
if (naturalPitchMode) {
/* D282: front-end menus (main menu, file select, mission-select map)
* share this same stick_x/stick_y channel with in-game look/movement.
* naturalPitchMode (SOLITARE, default-on) routes that channel to the
* RIGHT stick for continuous look -- fine in a level, unintuitive in
* a menu, where the left stick is the expected primary-navigation
* input on a modern pad/Deck (matches the F10 overlay's own
* left-stick nav, optionsoverlay.c). Outside a running stage, always
* source stick_x/stick_y from the left stick, regardless of
* naturalPitchMode -- a menu-context input remap only, no game-logic
* change and no effect on in-level control feel. */
int padMenuMode = (current_menu != GE_MENU_RUN_STAGE &&
current_menu != GE_MENU_INVALID);
if (padMenuMode) {
int px = scaleAxis(lx);
int py = -scaleAxis(ly); /* SDL up = negative -> N64 up = positive */
if (px) sx = px;
if (py) sy = py;
} else if (naturalPitchMode) {
/* 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
@@ -1142,8 +1181,6 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
if (SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_B) ||
SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_Y))
button |= GE_CONT_B;
int padMenuMode = (current_menu != GE_MENU_RUN_STAGE &&
current_menu != GE_MENU_INVALID);
{
int lbNow = SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_LEFTSHOULDER);
int rbNow = SDL_GameControllerGetButton(pad, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER);
@@ -1177,6 +1214,14 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
}
}
if (idx == 0 && scriptIsActive()) {
/* D263: apply BEFORE the stick is written out -- scripted stick
* tokens (SUP/SDOWN/SLEFT/SRIGHT) used to be discarded. */
button = scriptApply(button);
sx = scriptCurSX;
sy = scriptCurSY;
}
if (sx > STICK_MAX) sx = STICK_MAX;
if (sx < -STICK_MAX) sx = -STICK_MAX;
if (sy > STICK_MAX) sy = STICK_MAX;
@@ -1185,12 +1230,6 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
if (stick_x) *stick_x = (signed char)sx;
if (stick_y) *stick_y = (signed char)sy;
if (idx == 0 && scriptIsActive()) {
button = scriptApply(button);
sx = scriptCurSX;
sy = scriptCurSY;
}
if (configGetInputLog() && (button || sx || sy)) {
sysLogPrintf(LOG_NOTE, "GE_INPUTLOG cont%d: btn=%04x stick=(%d,%d)",
idx, button, sx, sy);
@@ -1297,11 +1336,13 @@ void inputSuspendForOverlay(void)
void inputPostWheel(int notches)
{
/* D223: keep the direction -- up cycles to the next weapon, down to the
* previous one. A burst of same-direction notches just re-arms the same
* pulse; a direction change mid-sequence restarts it (last wins). */
if (notches > 0) { wheelFwd = WHEEL_FWD_POLLS; wheelBack = 0; }
else if (notches < 0) { wheelBack = 2; wheelFwd = 0; }
/* D223: keep the direction. v0.2.1: swapped per user request -- up now
* cycles to the PREVIOUS weapon, down to the NEXT one (PC convention
* "scroll down = advance list"). A burst of same-direction notches just
* re-arms the same pulse; a direction change mid-sequence restarts it
* (last wins). */
if (notches > 0) { wheelBack = 2; wheelFwd = 0; }
else if (notches < 0) { wheelFwd = WHEEL_FWD_POLLS; wheelBack = 0; }
}
/* Called from the host event pump on SDL_CONTROLLERDEVICEADDED/REMOVED.
@@ -1461,17 +1502,102 @@ static int aimGepdCompute(double dxPx, double dyLook)
return 1;
}
/* WI-1 (GEPD-INPUT-PLAN.md #89): hipfire direct camera write.
*
* Mirrors GEPD's hipfire branch (games/goldeneye.c): `camx += XPOS/10 *
* (SENS/40) * (fov/basefov)` degrees this poll -- linear in px, FOV-scaled,
* no stick curve in between. We fold GEPD's SENS/40 into our own
* MouseTurnSpeed*MouseSensitivity product (both already 100% at defaults,
* same "in line" convention D238 established for aimGepdCompute's GepdSens):
* at defaults this reduces to exactly GEPD's px/10 baseline.
*
* dxPx/dyLook are this poll's dt-scaled px (same convention as
* aimGepdCompute -- callers pre-multiply by lookDtScale).
*
* GEPD's safety gates (`camera==4||0 && menupage==11 && !dead && !watch &&
* !pause`) matter here in a way they didn't for aim mode: aim mode requires
* RMB held, so it naturally can't fire during a death/cutscene/pause state a
* player would be holding RMB through; hipfire looks are always live, so a
* frozen or scripted-camera state (POSEND/INTRO cutscenes, death cam, pause)
* must be checked explicitly or this would fight the game's own camera
* control during those states. `current_menu` already gates menupage==11
* (checked by the caller's menuMode branch, same as aim mode); the rest are
* read here directly -- read-only, no logic change.
*
* Returns 1 if it handled this poll (caller must not also run the legacy
* stick path); 0 to fall back (disabled, no player, or a safety gate is
* closed -- e.g. mid-death or mid-cutscene, matching GEPD's !dead/!watch).
*/
static int hipDirectCompute(double dxPx, double dyLook)
{
struct player *p = g_CurrentPlayer;
if (!mouseDirectLook || !mouseGrabbed || p == NULL)
return 0;
/* GEPD: !dead && !watch && !pause. bonddead/outside_watch_menu/
* pause_state are the decomp-canonical equivalents (bondview.h). */
if (p->bonddead || !p->outside_watch_menu || p->pause_state != 0)
return 0;
/* M-190: the "frozen or scripted-camera state (POSEND/INTRO cutscenes...)
* must be checked explicitly" gate this function's own comment above has
* always documented, but never actually implemented -- found live: the
* mouse could still freely move the camera during the Dam intro flyover
* and the abseil-jump cutscene, both CAMERAMODE_INTRO/POSEND, which are
* supposed to be camera-locked. bondviewFrozenMoveBond (bondview2.c:7947)
* already calls bondviewProcessInput(0,0,0,0) to correctly ignore stick
* input during these modes, but that gate lives entirely inside the
* stick-based dispatch (MoveBond vs bondviewFrozenMoveBond,
* bondview2.c:8268) -- this function writes p->vv_theta/vv_verta
* directly, bypassing that dispatch entirely. Reuses the same
* scripted-camera test D243's clamps use (bondview2.c, gameScriptedCameraActive,
* formerly d243mCutsceneActive -- renamed since this isn't D243-specific). */
extern int gameScriptedCameraActive(void);
if (gameScriptedCameraActive())
return 0;
f32 fov = viGetFovY();
f32 scale = (fov > 0.0f) ? fov / GEPD_BASE_FOV : 1.0f;
double sens = (mouseTurnSpeed / 100.0) * (mouseSensitivity / 100.0);
/* dyLook already carries MouseInvertY + MouseYScale (applied by the
* caller before dt-scaling, same as every other consumer of dyLook) --
* do not re-apply either here. */
p->vv_theta += (f32) (dxPx * 0.1 * sens * scale);
p->vv_verta -= (f32) (dyLook * 0.1 * sens * scale);
if (p->vv_verta > 90.0f) p->vv_verta = 90.0f;
if (p->vv_verta < -90.0f) p->vv_verta = -90.0f;
if (configGetInputLog()) {
sysLogPrintf(LOG_NOTE,
"GE_INPUTLOG hipdirect d=(%.1f,%.1f) cam=(%.1f,%.1f)",
dxPx, dyLook, (double)p->vv_theta, (double)p->vv_verta);
}
return 1;
}
PD_CONSTRUCTOR static void inputConfigInit(void)
{
configRegisterInt("Input.MouseEnabled", &mouseEnabled, 0, 1);
configRegisterInt("Input.MouseAimSpeed", &mouseAimSpeed, 1, 500);
configRegisterInt("Input.AimAbsolute", &aimAbsolute, 0, 1); /* D194 */
configRegisterInt("Input.MouseDirectLook", &mouseDirectLook, 0, 1); /* WI-1 */
/* D194: renamed Input.GepdSens -> Input.AimModeSens (community name for
* the RMB aim mode; "GEPD" is internal provenance jargon). The old key
* stays registered against the same variable as a deprecated alias --
* if both appear in an ini, the later line wins. */
configRegisterInt("Input.AimModeSens", &gepdSens, 1, 80);
configRegisterInt("Input.GepdSens", &gepdSens, 1, 80); /* deprecated alias */
/* D304: widened from the old (1,80) clamp to match Input.MouseTurnSpeed's
* (1,500) range exactly -- both are the same underlying "reference SENS
* passthrough" unit (see aimGepdCompute/hipDirectCompute), consumed by
* two different, intentional per-mode divisor constants (2920 vs the
* turn-speed path's /100*0.1), so a shared range lets the two sliders be
* directly comparable ("1:1" in the menu) instead of arbitrarily capped
* at different, mismatched ceilings. Old 80 cap was well below even this
* value's own reference-model native ceiling (100 raw = 500% in the
* source GEPD injector's own display convention). */
configRegisterInt("Input.AimModeSens", &gepdSens, 1, 500);
configRegisterInt("Input.GepdSens", &gepdSens, 1, 500); /* deprecated alias */
configRegisterInt("Input.AimBand", &aimBand, 5, 40);
configRegisterInt("Input.MouseTurnSpeed", &mouseTurnSpeed, 1, 500);
configRegisterInt("Input.SensLink", &sensLink, 0, 1);
+106
View File
@@ -0,0 +1,106 @@
/*
* port/src/legacycrc.c — D297: one-time migration of pre-D284 save-slot CRCs.
*
* WHY A DELIBERATELY-WRONG PRNG LIVES IN THIS TREE (read before "fixing" it):
*
* v0.2.x shipped with a port-side bug in randomGetNextFrom's dsll32/dsrl32
* semantics (modelled as 32-bit shifts; real MIPS64 shifts by n+32 over the
* full register). D284 (commit 0de99b4d) fixed it. But fileGenerateCRC
* (src/game/crc.c) runs that PRNG off a LOCAL constant seed, so every slot
* checksum stored on disk is a fingerprint of the PRNG implementation that
* wrote it: saves written by v0.2.x validate under the OLD stream and fail
* the NEW one, and fileValidateSaves wipes failing slots (D295/M-148, D297).
*
* This file carries the pre-D284 transform so the eeprom shim can recognize
* a legacy save (old-CRC match) and re-stamp its two checksum words with the
* current-PRNG values before the game validates. No game files are touched;
* the new CRCs come from the game's own fileGenerateCRC — only the OLD PRNG
* is duplicated here, and it must stay bit-exact-WRONG. Do not "fix" the
* shifts below; do not share helpers with port/src/random.c.
*
* When this can be deleted: once no pre-D284 saves are expected in the wild
* (the v0.3.0 release is the last build that meets them un-migrated). Keep
* it at least until then; the cost is ~60 lines of dead-but-harmless code.
*
* Verification: the legacy stream below was re-derived from
* `git show 0de99b4d^:port/src/random.c` and matches tools_pc/d295_crc.py,
* which is bit-exact against compiled C for both PRNGs (see D295 M-148).
*/
#include <ultra64.h>
#include "file.h" /* save_data */
extern void fileGenerateCRC(u8 *addressA, u8 *addressB, save_data *retval);
/* Pre-D284 randomGetNextFrom: verbatim body from 0de99b4d^:port/src/random.c
* with the OLD helper semantics inlined ((x << n) & 0xFFFFFFFF etc.). */
static u32 geLegacyRandomGetNextFrom(u64 *seed)
{
u64 a3 = *seed;
u64 a1, a2;
a2 = (a3 << 0x1f) & 0xFFFFFFFFULL; /* dsll32, pre-D284 semantics */
a1 = a3 << 0x1f; /* dsll */
a2 = a2 >> 0x1f; /* dsrl */
a1 = (a1 >> 0) & 0xFFFFFFFFULL; /* dsrl32, pre-D284 semantics */
a3 = (a3 << 0xc) & 0xFFFFFFFFULL; /* dsll32, pre-D284 semantics */
a2 = a2 | a1;
a3 = (a3 >> 0) & 0xFFFFFFFFULL; /* dsrl32, pre-D284 semantics */
a2 = a2 ^ a3;
a3 = a2 >> 0x14; /* dsrl */
a3 = a3 & 0xfff;
a3 = a3 ^ a2;
*seed = a3;
return (u32)a3;
}
/* Same loop as fileGenerateCRC (src/game/crc.c) stepping the legacy PRNG.
* Keep in lockstep with crc.c if that loop ever changes. */
static void geLegacyGenerateCRC(u8 *addressA, u8 *addressB, save_data *retval)
{
u8 *byte;
s32 shift = 0;
s64 polynormal = 0x8F809F473108B3C1; /* same local constant seed as crc.c */
s32 checksum1 = 0;
s32 checksum2 = 0;
for (byte = addressA; byte < addressB; byte++, shift += 7) {
polynormal += *byte << (shift & 0xF);
checksum1 ^= geLegacyRandomGetNextFrom(&polynormal);
}
for (byte = addressB - 1; byte >= addressA; byte--, shift += 3) {
polynormal += *byte << (shift & 0xF);
checksum2 ^= geLegacyRandomGetNextFrom(&polynormal);
}
retval->chksum1 = checksum1;
retval->chksum2 = checksum2;
}
/* Migrate up to five consecutive 96-byte save slots in place. A slot is
* migrated iff it FAILS the current-PRNG check but PASSES the legacy one;
* re-stamped via the game's own fileGenerateCRC. Corrupt slots (fail both)
* are left alone for the game's own wipe path. Returns the count migrated. */
int geLegacyCrcMaybeMigrateSlots(u8 *slots5)
{
save_data *slots = (save_data *)slots5;
int migrated = 0;
for (int i = 0; i < 5; i++) {
save_data calc = {0}; /* only chksum1/chksum2 are read back */
u8 *a = &slots[i].completion_bitflags; /* slot bytes [8..96) */
u8 *b = (u8 *)&slots[i + 1]; /* end boundary, never dereferenced */
fileGenerateCRC(a, b, &calc);
if (calc.chksum1 == slots[i].chksum1 && calc.chksum2 == slots[i].chksum2)
continue; /* current-PRNG save (or already migrated) — idempotent no-op */
geLegacyGenerateCRC(a, b, &calc);
if (calc.chksum1 != slots[i].chksum1 || calc.chksum2 != slots[i].chksum2)
continue; /* corrupt: game's fileValidateSaves handles it */
fileGenerateCRC(a, b, &slots[i]); /* re-stamp chksum1/chksum2 in place */
migrated++;
}
return migrated;
}
+39 -1
View File
@@ -222,7 +222,7 @@ static void portHeartbeatCheck(void)
g_lastHeartbeatUs = now;
sysLogPrintf(LOG_ERROR,
"kernel heartbeat: no frame rendered for %llu ms (frames=%d); state:",
(unsigned long long)(now - g_lastFrameUs), g_framesRendered);
(unsigned long long)(now - g_lastFrameUs) / 1000, g_framesRendered);
for (int i = 0; i < PORT_MAX_THREADS; ++i) {
PortThread *t = &g_pt[i];
if (!t->os) continue;
@@ -1225,6 +1225,8 @@ typedef char ge_save_slot_size_check[(sizeof(ge_save_slot) == 96) ? 1 : -1];
* re-checks after the block read. */
extern void fileGenerateCRC(u8 *addressA, u8 *addressB, void *retval);
extern int geLegacyCrcMaybeMigrateSlots(u8 *slots5); /* port/src/legacycrc.c (D297) */
extern s32 portAllUnlocked; /* port/src/video.c */
/* Bit math mirrors fileGetSaveStageDifficultyTime / fileSetDifficultyStageTime
@@ -1278,6 +1280,25 @@ static void geEepromPatchAllCheats(u8 *buf)
int changed = 0;
for (int i = 0; i < 5; i++) {
/* D259: a fresh ge007.eep is zero-filled, so every slot reads as
* all-zero. Normally such a slot fails fileValidateSaves' CRC and is
* reset to BLANKSAVEDATA (music_vol/sfx_vol = 0xFF); but this patch
* gives the slot a valid CRC below, so it survives with volume 0
* -> silence. Emulate fileResetSave: seed max volume on all-zero
* slots only -- real saves (incl. a deliberately muted one) keep
* their bytes. */
{
const u8 *raw = (const u8 *)&slots[i];
int allzero = 1;
for (int b = 0; b < (int)sizeof(ge_save_slot); b++)
if (raw[b]) { allzero = 0; break; }
if (allzero && (slots[i].music_vol != 0xFF ||
slots[i].sfx_vol != 0xFF)) {
slots[i].music_vol = 0xFF;
slots[i].sfx_vol = 0xFF;
changed = 1;
}
}
/* Cheat ids are level ids 0..19 (CHEAT_INPUT_BUFFER_SIZE == 20):
* bits 0-7 in _1, 8-15 in _2, 16-19 in the low nibble of _3. */
if (slots[i].unlocked_cheats_1 != 0xFF ||
@@ -1318,6 +1339,23 @@ static s32 geEepromRW(u8 block, u8 *buf, int nbytes, int write)
geEepromStore();
} else {
memcpy(buf, s_eeprom + off, nbytes);
/* D297: one-time migration of pre-D284 slot CRCs (port/src/legacycrc.c).
* This IS the read fileValidateSaves performs (block 4, five slots), so
* the re-stamped checksums are exactly what the game validates — no
* ordering hazard. Runs BEFORE geEepromPatchAllCheats so that only
* pristine+migrated bytes are ever persisted; AllUnlocked patches stay
* read-time-only as before. Block-0 smallSave is deliberately untouched
* (factory seal, see D297). */
if (block == 4 && nbytes == (int)(sizeof(ge_save_slot) * 5)) {
int migrated = geLegacyCrcMaybeMigrateSlots(buf);
if (migrated) {
memcpy(s_eeprom + off, buf, nbytes);
geEepromStore();
sysLogPrintf(LOG_INFO,
"eeprom: D297-migrated %d pre-D284 save slot(s) to current CRC",
migrated);
}
}
/* fileValidateSaves' block read: five save slots from block 4. */
if (portAllUnlocked && block == 4 &&
nbytes == (int)(sizeof(ge_save_slot) * 5)) {
+29
View File
@@ -33,6 +33,7 @@
#include "mixer.h"
#include "crash.h"
#include "thread_config.h"
#include "game/language.h" /* D295/M-148: JPN glyph-cache types + j_text_trigger */
/* Defined in the game (src/init.c). The port calls into the real game entry. */
extern void mainproc(void *args);
@@ -204,6 +205,34 @@ int main(int argc, char **argv)
* audio threads and never returns in practice. */
sysLogPrintf(LOG_INFO, "ROM mapped at 0x%08X (%u bytes); starting game",
(unsigned)0x10000000, romdataGetRomSize());
/* 4a. D295/M-148: non-JP builds never allocate the JPN glyph cache (langInit
* gates it on j_text_trigger), but textRender's high-bit path calls
* langGetJpnCharPixels unconditionally and indexes the NULL global
* (language.c:320). Seed both globals with static port buffers, using the
* exact same construction as langInit's JP path (language.c:247-253), so a
* stray byte >= 0x80 takes the normal cache-miss path (romCopy from the JP
* font segment -> garbage glyph) instead of NULL-deref. Runs before
* osStartThread => no race; langInit (non-JP) never touches these, and if a
* debug -j flag ever flips j_text_trigger, its real allocation supersedes
* this stub harmlessly. */
{
extern struct jpncharpixels *g_JpnCharCachePixels; /* src/game/language.c:27 */
extern struct jpncacheitem *g_JpnCacheCacheItems; /* src/game/language.c:29 */
if (!j_text_trigger) {
static struct jpncharpixels jpnPixels[124 * 8]; /* 0x2E80 bytes, langInit's size */
static struct jpncacheitem jpnItems[124]; /* 124 slots; langInit over-allocates 0x100 (128) but only ever uses 0x7C */
s32 i;
_Static_assert(sizeof(jpnPixels) == 0x2E80,
"JPN pixel stub size must match langInit (language.c:248)");
for (i = 0; i < 124; i++) {
jpnItems[i].ttl = 0;
jpnItems[i].codepoint = -1;
}
g_JpnCharCachePixels = jpnPixels;
g_JpnCacheCacheItems = jpnItems;
}
}
portKernelInit();
osCreateThread(&mainThread, MAIN_THREAD_ID, &mainproc, NULL, NULL,
MAIN_THREAD_PRIORITY);
+176 -28
View File
@@ -45,6 +45,7 @@
#include "video.h"
#include "input.h"
#include "optionsoverlay.h"
#include "../fast3d/gfx_api.h"
/* ---- game symbols (rendering/UI only; see input.c for the same pattern) ---- */
struct font;
@@ -62,11 +63,24 @@ extern s16 viGetY(void);
/* ------------------------------------------------------------------------ */
enum { ROW_TOGGLE, ROW_SLIDER, ROW_ENUM, ROW_MSAA, ROW_RES };
enum { ROW_TOGGLE, ROW_SLIDER, ROW_ENUM, ROW_MSAA, ROW_RES, ROW_ACTION, ROW_FPSCAP };
static const char *const kOnOff[] = { "OFF", "ON", NULL };
static const char *const kTexFilter[] = { "NEAREST", "BILINEAR", "3-POINT", NULL };
static const int kMsaaSeq[] = { 1, 2, 4, 8 };
/* D186: the sim's own tick pacemaker is hardcoded to the console's native VI
* rate (60Hz NTSC / 50Hz PAL, port/src/libultra.c) -- Video.FpsCap can only
* throttle down from there, never past it, and throttling it below 30
* throttles game logic itself (video.c already force-uncaps anything under
* 30). A free 0-360 slider therefore had a huge dead zone (every value above
* the console rate is a no-op, every value 1-29 silently snaps to 0) with
* only two states that actually do anything. Exposed as a plain 30/60 toggle
* instead (user ask, 2026-09-18). A third "uncapped" (0, skip the port's own
* frame-pacing wait) state exists at the config level and old inis may still
* 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 };
/* Windowed-mode resolution presets. Filtered at init to those that fit the
* desktop; the Resolution row cycles the surviving list. */
@@ -107,32 +121,71 @@ static struct Row rows[] = {
{ "Video.Fullscreen", "Fullscreen", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
{ "__Resolution", "Resolution", ROW_RES, 0, NULL, 0, 0, 0, 0,0,0,0,0 },
{ "Video.VSync", "VSync", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
{ "Video.FpsCap", "Frame cap", ROW_SLIDER, 10, NULL, 0, 0, 360, 0,0,0,0,0 },
{ "Video.FpsCap", "Frame cap", ROW_FPSCAP, 0, NULL, 0, 0, 0, 0,0,0,0,0 },
{ "Video.MSAA", "MSAA", ROW_MSAA, 0, NULL, 1, 0, 0, 0,0,0,0,0 },
{ "Video.TextureFilter", "Texture filter", ROW_ENUM, 1, kTexFilter, 0, 0, 0, 0,0,0,0,0 },
{ "Video.Anisotropy", "Anisotropic", ROW_SLIDER, 1, NULL, 0, 0, 0, 0,0,0,0,0 },
{ "Video.FovScale", "FOV scale %", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 },
{ "Video.WidescreenAuto", "Widescreen auto FOV",ROW_TOGGLE,1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
{ "Video.SafeAreaCrop", "Crop overscan bars", ROW_TOGGLE,1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
{ "Video.DrawDistance", "Draw distance %", ROW_SLIDER, 25, NULL, 0, 0, 0, 0,0,0,0,0, "Video.DrawDistanceAutoFov" },
{ "Video.DrawDistanceAutoFov","Draw dist. auto", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
{ "Video.LodDistance", "LOD distance %", ROW_SLIDER, 25, NULL, 0, 0, 0, 0,0,0,0,0, "Video.LodDistanceAutoFov" },
{ "Video.LodDistanceAutoFov", "LOD dist. auto", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
/* Aim row edits Input.AimModeSens -- the knob the default GEPD aim path
* actually uses (Input.MouseAimSpeed only feeds the legacy velocity-stick
* fallback, so it was inert here). */
{ "Input.AimModeSens", "Mouse aim speed", ROW_SLIDER, 1, NULL, 0, 1, 80, 0,0,0,0,0 },
{ "Input.MouseTurnSpeed", "Mouse turn speed", ROW_SLIDER, 1, NULL, 0, 0, 100, 0,0,0,0,0 },
{ "Input.SensLink", "Link aim/turn sens",ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
* fallback, so it was inert here). D304: both this row and the turn-speed
* row below now use the (0,0) "inherit the registered clamp" sentinel
* instead of hardcoded uiMin/uiMax -- they were previously (1,80) and
* (0,100), two arbitrary, DIFFERENT, and (for turn speed) outright wrong
* ceilings that didn't even match either row's own backing config clamp
* (both are actually registered 1..500 in input.c) -- fixed as an earlier
* part of this same D304 pass. **Follow-up, same day, user feedback after
* trying the widened sliders live:** exposing BOTH per-mode knobs still
* let a player decouple them into a bad, hard-to-diagnose state (e.g. aim
* mode very slow, hipfire very fast, or vice versa) with no indication
* anything was wrong -- worse than a narrow range. Per the user's request,
* these two rows and the "Link aim/turn sens" toggle that tried to paper
* over the same risk are pulled from the menu entirely; only the single
* master `Input.MouseSensitivity` row below remains player-facing, exactly
* mirroring the reference GEPD/PD injector model this input was ported
* from (GEPD-Edition Mouse Injector's goldeneye.c -- source no longer
* vendored in the public repo, see reference/mouse-injector/README.md) --
* ONE user-facing
* sensitivity value, with the two per-mode constants staying fixed at
* their calibrated (38/50) defaults, never independently player-tunable.
* Config vars + SensLink logic stay in `input.c`/`rowSet()` untouched
* (ini power users can still hand-edit them; existing ini files with
* either key keep working) -- only the menu surface changes, same
* established pattern as the D181/D216 pulled rows below. */
/* { "Input.AimModeSens", "Mouse aim speed", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 }, */
/* { "Input.MouseTurnSpeed", "Mouse turn speed", ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 }, */
/* { "Input.SensLink", "Link aim/turn sens",ROW_TOGGLE,1, kOnOff, 0, 0, 0, 0,0,0,0,0 }, */
{ "Input.MouseSensitivity", "Mouse sensitivity",ROW_SLIDER, 5, NULL, 0, 0, 0, 0,0,0,0,0 },
{ "Input.MouseInvertY", "Mouse invert Y", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
{ "Game.ScreenShakeIntensity","Screen shake", ROW_SLIDER, 0.25, NULL, 0, 0, 3, 0,0,0,0,0 },
/* D181/Game.ScreenShakeIntensity: user testing (v0.2.1) found the slider
* "basically useless" -- viShake() is only called from explosion.c, so it
* scales explosion shake alone; it never touches the always-on walking
* head-bob or any getting-shot reaction, which is what "Screen shake"
* reads as to a player. Pulled from the menu until it covers all
* screen-shake/view-bob sources, not just explosions. Config var + fr.c
* hook stay in place. */
/* D232: the community "no damage flash" toggle (suppresses the red/green
* hit-flash overlay in bondview2). */
{ "Game.NoHitFlash", "No hit flash", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
/* D216: read once at title-stage load (lv.c) -- toggling it takes effect
* the next time the title screen is reached. */
{ "Game.SkipIntro", "Skip intro", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
/* D216/Game.SkipIntro: user report (v0.2.1 testing) that it breaks audio
* -- pulled from the menu until root-caused. Not exposed to players; the
* config var + lv.c hook stay in place (dead unless an existing ini has
* it set, which no menu path can do any more). Do not re-add without
* fixing the underlying issue first. */
/* D257: everything-unlocked goodie (default ON). Consumed at startup by
* main.c -- applies from the next launch. */
{ "Game.AllUnlocked", "All unlocked", ROW_TOGGLE, 1, kOnOff, 0, 0, 0, 0,0,0,0,0 },
/* D293: only quit path used to be the OS window-close / Alt+F4 -- no
* discoverable in-game way to exit, a real gap on Deck/controller-only
* setups. Not config-backed (like __Resolution); activating it exits
* the same way video.c's SDL_QUIT/Alt+F4 handlers already do. */
{ "__QuitToDesktop", "Quit to desktop", ROW_ACTION, 0, NULL, 0, 0, 0, 0,0,0,0,0 },
};
#define NUM_ROWS ((int)(sizeof(rows) / sizeof(rows[0])))
@@ -218,11 +271,34 @@ static void sliderBarSpan(s32 *x0, s32 *x1)
}
}
/* Visible position (0..s_visN-1) the given overlay-space y falls in, or -1. */
/* Visible position (0..s_visN-1) the given overlay-space y falls in, or -1.
*
* D304 fix: this used to loop over ALL s_visN entries (0..s_visN-1)
* regardless of which ones are actually scrolled into view and drawn this
* frame -- OV_ROW_Y(p - s_scroll) is a plain linear function of p, so a row
* sitting just past the last visibly-drawn one (pLast, see the emit path)
* still produces a geometrically valid, in-range Y band immediately below
* the panel's real bottom edge. A click near that boundary (real-pixel-to-
* virtual-2D-space rounding in the ox/oy scale-up, or simply a slightly low
* click on the last visible row) could therefore resolve to the NEXT,
* invisible, scrolled-off-the-bottom row instead of the one actually drawn
* there. Concretely: with "All unlocked" sitting in the last visible slot,
* this let a click on it silently hit "Quit to desktop" (the very next row)
* instead -- reported as "I clicked unlock all and I think it crashed"
* (user, 2026-09-18): the game didn't crash, __QuitToDesktop's ROW_ACTION
* fired and closed it via the game's normal exit path. Fix: bound the scan
* to the same [s_scroll, pLast] range the draw loop actually renders.
*
* D316 fix: the band used to be [Y-3, Y+12) -- 3 units above OV_ROW_Y but
* 12 below it, a systematic downward bias baked in independent of the
* mouse-mapping bug above. Centered here on OV_ROW_Y so a borderline click
* no longer favors the row below. */
static int overlayRowAtY(double oy)
{
for (int p = 0; p < s_visN; p++) {
double top = OV_ROW_Y(p - s_scroll) - 3;
int maxV = maxVisibleRows();
int pLast = (s_visN - s_scroll < maxV) ? s_visN - 1 : s_scroll + maxV - 1;
for (int p = s_scroll; p <= pLast; p++) {
double top = OV_ROW_Y(p - s_scroll) - OV_LINE / 2;
if (oy >= top && oy < top + OV_LINE) {
return p;
}
@@ -250,7 +326,20 @@ static void overlayUpdateVisible(void)
}
}
/* Keep the selected row on screen: shift the window when it nears an edge. */
/* Keep the selected row on screen: shift the window ONLY when the selection
* is actually outside it, by the minimum amount needed.
*
* D304 fix: this used to unconditionally set `s_scroll = s_sel - (maxV-1)`
* -- i.e. bottom-anchor the selected row -- on every single call, including
* every mouse click. A click on a row already visible (anywhere but the very
* last slot) still forced the whole list to re-scroll so that row landed at
* the bottom, shifting every row's on-screen position for the rest of that
* same frame -- so whatever row the user then saw/clicked at that same
* screen position was a DIFFERENT (usually the next, i.e. "below") setting.
* Reported as "the F10 menu keeps jumping to the setting below when I left
* click" (user, 2026-09-18) -- clicking any row not already at the bottom
* slot reproduced it every time. Fix: only move the window when the
* selection is above the top or below the bottom of the current view. */
static void overlayUpdateScroll(void)
{
int maxV = maxVisibleRows();
@@ -258,7 +347,11 @@ static void overlayUpdateScroll(void)
s_scroll = 0;
return;
}
s_scroll = s_sel - (maxV - 1);
if (s_sel < s_scroll) {
s_scroll = s_sel;
} else if (s_sel > s_scroll + maxV - 1) {
s_scroll = s_sel - (maxV - 1);
}
if (s_scroll < 0) {
s_scroll = 0;
}
@@ -312,8 +405,8 @@ static void overlayInit(void)
configForEachOption(resolveCb, NULL);
for (int i = 0; i < NUM_ROWS; i++) {
if (rows[i].kind == ROW_RES) {
rows[i].found = 1; /* not config-backed; driven via video.c */
if (rows[i].kind == ROW_RES || rows[i].kind == ROW_ACTION) {
rows[i].found = 1; /* not config-backed */
continue;
}
if (!rows[i].found) {
@@ -478,6 +571,15 @@ static void rowAdjust(struct Row *r, int dir)
rowSet(r, v);
break;
}
case ROW_FPSCAP: {
int idx = 0;
for (int i = 0; i < 2; i++) {
if (kFpsCapSeq[i] == (int)lround(v)) idx = i;
}
idx = (idx + dir + 2) % 2;
rowSet(r, (double)kFpsCapSeq[idx]);
break;
}
case ROW_RES: {
if (s_resFitN <= 0 || videoIsFullscreen()) {
break; /* resolution is windowed-only */
@@ -489,6 +591,13 @@ static void rowAdjust(struct Row *r, int dir)
videoRequestWindowSize(kResList[i][0], kResList[i][1]);
break;
}
case ROW_ACTION:
/* D293: same exit path as SDL_QUIT / Alt+F4 (video.c), just reachable
* without OS window chrome or a keyboard. */
sysLogPrintf(LOG_INFO, "optionsoverlay: quit to desktop requested");
configSave();
exit(0);
break;
default: /* ROW_SLIDER */
rowSet(r, v + dir * r->step);
break;
@@ -550,6 +659,15 @@ static void sliderSetFromX(struct Row *r, double ox)
rowSet(r, v);
}
/* D314 (findings.md): F10 menu items flicker/mis-land on the file-select
* screen, mechanism unpinned. Ruled out: viGetX/viGetY (stable per-screen,
* set once by front.c) and non-determinism in the display list. This logs
* the remaining suspects -- viGetY(), s_visN, s_scroll, s_sel -- once per
* frame while the overlay is open, to catch whichever one oscillates during
* a file-select repro. Env-gated, cached once (not re-queried per frame --
* see D302: a hot-path getenv() regression cost a real bug before). */
static int s_d314Enabled = -1; /* -1 = not yet resolved */
void optionsOverlayHandleInput(void)
{
static int prevUp, prevDn, prevLf, prevRt, prevLmb, prevRmb;
@@ -561,6 +679,14 @@ void optionsOverlayHandleInput(void)
return;
}
if (s_d314Enabled < 0) {
s_d314Enabled = getenv("GE_D314") ? 1 : 0;
}
if (s_d314Enabled) {
fprintf(stderr, "GE_D314 viGetY=%d visN=%d scroll=%d sel=%d\n",
(int)viGetY(), s_visN, s_scroll, s_sel);
}
/* The overlay owns the mouse while it is open: force the OS cursor free +
* visible (a stage poll would otherwise leave it locked/hidden). */
inputSuspendForOverlay();
@@ -608,12 +734,19 @@ void optionsOverlayHandleInput(void)
if (lf && !prevLf) rowAdjust(&rows[s_visIdx[s_sel]], -1);
if (rt && !prevRt) rowAdjust(&rows[s_visIdx[s_sel]], +1);
/* ---- mouse ---- */
int ww = 0, wh = 0;
videoGetWindowSize(&ww, &wh);
if (ww > 0 && wh > 0) {
double ox = (double)mx * (double)viGetX() / ww;
double oy = (double)my * (double)viGetY() / wh;
/* ---- mouse ----
* D316: mx/my are raw window pixels, but the overlay's own 2D content
* is drawn into whatever on-window rect the safe-area crop currently
* maps the logical (viGetX() x viGetY()) canvas to -- NOT the full
* window whenever that rect is inset (default-on: any in-game "Full"
* viewport insets it). Map through the real forward transform's rect
* (gfx_get_ui_screen_rect) instead of a naive window-size scale so a
* click lands on the same row it visually appears over. */
int32_t rx = 0, ry = 0, rw = 0, rh = 0;
gfx_get_ui_screen_rect(&rx, &ry, &rw, &rh);
if (rw > 0 && rh > 0) {
double ox = (double)(mx - rx) * (double)viGetX() / rw;
double oy = (double)(my - ry) * (double)viGetY() / rh;
int hoverVis = overlayRowAtY(oy);
int onClose = overlayInCloseBox(ox, oy);
@@ -708,13 +841,18 @@ static void valueText(const struct Row *r, char *out, int n)
else snprintf(out, n, "%dx", (int)lround(v));
return;
}
if (r->kind == ROW_ACTION) {
snprintf(out, n, "[ENTER]");
return;
}
if (r->kind == ROW_SLIDER && r->type == CONFIG_OPT_FLOAT) {
snprintf(out, n, "%.2f", v);
return;
}
if (r->kind == ROW_SLIDER && (int)lround(v) == 0 &&
strcmp(r->key, "Video.FpsCap") == 0) {
snprintf(out, n, "OFF");
if (r->kind == ROW_FPSCAP) {
int fps = (int)lround(v);
if (fps <= 0) snprintf(out, n, "Uncapped");
else snprintf(out, n, "%d FPS", fps);
return;
}
snprintf(out, n, "%d", (int)lround(v));
@@ -780,9 +918,19 @@ Gfx *optionsOverlayEmit(void)
const s32 right = OV_RIGHT;
const s32 panelTop = OV_TOP - 9;
/* Last visible position actually drawn (window may be shorter than the
* list at small 2D viewports -- the rest is reached by scrolling). */
* list at small 2D viewports -- the rest is reached by scrolling).
* D304 fix: the "everything fits" branch was `s_visN - s_scroll`, which
* is a COUNT, not the last valid 0-based index -- since that branch only
* runs when s_scroll==0 (overlayUpdateScroll's own invariant), this
* evaluated to `s_visN`, one past the last valid entry, so both draw
* loops below (`for (p = s_scroll; p <= pLast; p++)`) read one row past
* the end of `s_visIdx[]`/`rows[]` on any 2D viewport large enough to fit
* the whole list without scrolling (e.g. the larger front-end 440x330
* screen once a couple of rows are pulled from the menu, as just
* happened above). Should be `s_visN - 1`, matching overlayRowAtY's
* identical fix. */
const int maxV = maxVisibleRows();
const int pLast = (s_visN - s_scroll < maxV) ? s_visN - s_scroll
const int pLast = (s_visN - s_scroll < maxV) ? s_visN - 1
: s_scroll + maxV - 1;
const s32 panelBottom = OV_ROW_Y(pLast) + OV_LINE / 2 + 3;
s32 bx0, bx1;
+98 -9
View File
@@ -10,32 +10,79 @@
* Also ports src/game/chrObjRandom.s: the character-AI PRNG (chroid.c),
* which runs the identical transform on its own seed variable.
*
* The assembly operates on the low 32 bits of the seed (the high 32 bits are
* never read), but the seed is stored as a full u64. We mirror each
* instruction with explicit masks to guarantee bit-exactness. The initial
* seed is the two .words in random.s: 0xAB8D9F77 (hi) / 0x81280783 (lo).
* The seed is stored as a full u64 and the transform genuinely operates on
* all 64 bits of the register (see the dsll32/dsrl32 note below) — the
* stored seed is NOT truncated to 32 bits between calls, matching what the
* real `sd $a0, g_randomSeed` in random.s stores. The initial seed is the
* two .words in random.s: 0xAB8D9F77 (hi) / 0x81280783 (lo). Only the low
* 32 bits are ever returned to callers (`dsll32 $v0,$a0,0; dsra32
* $v0,$v0,0` sign-extends the low half into $v0, of which callers only use
* the u32 truncation).
*
* D284 (M-140): the original port of this file had dsll32/dsrl32 backwards
* — it shifted by the literal immediate `n` and masked to 32 bits, i.e. it
* modelled them as 32-bit shift instructions. Real MIPS64 dsll32/dsrl32 are
* "shift by n+32", operating on the FULL 64-bit register with no masking:
* they exist specifically to encode a 32..63 shift amount in a 5-bit field.
* The bug was bit-exact-but-wrong: every generated value differed from the
* real N64 stream from the very first draw, silently desyncing loot RNG, AI
* variance, and replay determinism in every release to date. Verified two
* independent ways (a forked review agent's numeric trace, and a hand trace
* of random.s against this file, both in the M-138 review session).
*/
#include <ultra64.h>
#include <stdlib.h>
#include <random.h>
u64 g_randomSeed = 0xAB8D9F7781280783ULL;
/*
* MIPS64 shift helpers, matching the .s instructions exactly:
* dsll32 rd,rs,n -> (rs << n) & 0xFFFFFFFF (low 32 bits kept, hi zeroed)
* dsrl32 rd,rs,n -> (rs >> n) & 0xFFFFFFFF (low 32 bits kept, hi zeroed)
* Plain `<<`/`>>` on u64 match dsll/dsrl (full 64-bit shifts).
* dsll32 rd,rs,n -> (rs << (n+32)) (full 64-bit register, no masking)
* dsrl32 rd,rs,n -> (rs >> (n+32)) (full 64-bit register, unsigned)
* Plain `<<`/`>>` on u64 match dsll/dsrl (full 64-bit shifts, shift amount
* used as-is, 0-31).
*/
static inline u64 dsll32(u64 x, int n) { return (x << n) & 0xFFFFFFFFULL; }
static inline u64 dsrl32(u64 x, int n) { return (x >> n) & 0xFFFFFFFFULL; }
static inline u64 dsll32(u64 x, int n) { return x << (n + 32); }
static inline u64 dsrl32(u64 x, int n) { return x >> (n + 32); }
/*
* Advance the global seed and return the new low 32 bits.
* Mirrors randomGetNext in random.s line-by-line.
*/
/* D318R: env-gated draw logger (GE_D318R=1). Logs the first N draws with
* tick + caller return address to identify which call sites consume the
* gameplay PRNG and whether their per-run counts are stable. Diagnostic
* only; inert without the env var. */
#ifdef PORT
#include <PR/os.h>
extern s32 g_GlobalTimer;
static void randomDrawLog(void)
{
static int s_on = -1;
static u32 s_n = 0;
const u32 MAXN = 4000;
if (s_on < 0)
{
const char *e = getenv("GE_D318R");
s_on = (e && e[0] == '1') ? 1 : 0;
}
if (s_on && s_n < MAXN)
{
osSyncPrintf("D318R: n=%u t=%d ra=%p\n",
s_n, (int)g_GlobalTimer, __builtin_return_address(0));
s_n++;
}
}
#endif /* PORT */
u32 randomGetNext(void)
{
#ifdef PORT
randomDrawLog();
#endif
u64 a0 = g_randomSeed;
u64 a1, a2;
@@ -56,9 +103,51 @@ u32 randomGetNext(void)
}
/* Set the seed (the .s adds 1 before storing). */
/* Manual hex parse (u64-safe): MinGW's strtoull returns the sign-extended low
* 32 bits for values above LLONG_MAX. Stops at first non-hex char. */
static u64 randomParseHex(const char *s)
{
u64 v = 0;
while (*s)
{
char c = *s++;
v <<= 4;
if (c >= '0' && c <= '9') { v |= (u64)(c - '0'); }
else if (c >= 'a' && c <= 'f') { v |= (u64)(c - 'a' + 10); }
else if (c >= 'A' && c <= 'F') { v |= (u64)(c - 'A' + 10); }
else { break; }
}
return v;
}
void randomSetSeed(u32 seed)
{
#ifdef PORT
/* GE_RSEED=<hex64>: test-only boot-seed override. boss.c seeds the PRNG
* from osGetCount() (wall-clock on PC; RSP timer on N64), so the boot
* seed differs every run -- with or without GE_DETERM, because the
* virtual clock's pre-task advance count is still real-time dependent.
* This pins it for determinism tests (E1) and N64 parity tests (E-B). */
{
const char *e = getenv("GE_RSEED");
if (e && e[0])
{
g_randomSeed = randomParseHex(e);
osSyncPrintf("D318R: SETSEED OVERRIDDEN by GE_RSEED -> %016llx\n",
(unsigned long long)g_randomSeed);
return;
}
}
#endif
g_randomSeed = (u64)seed + 1; /* daddiu $a0,$a0,1; sd $a0, g_randomSeed */
#ifdef PORT
if (getenv("GE_D318R") != NULL)
osSyncPrintf("D318R: SETSEED arg=%08x -> %016llx\n",
seed, (unsigned long long)g_randomSeed);
#endif
}
/*
+29 -5
View File
@@ -11,6 +11,7 @@
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#if defined(_WIN32)
#include <direct.h>
@@ -53,17 +54,19 @@ static int initDone = 0;
* behaviour, so a fresh config or a missing [Video] section changes nothing.
*/
static int cfgVSync = 1; /* swap interval: 0 = off, 1 = on */
static int cfgFpsCap = 0; /* frame cap in fps; 0 = uncapped (vsync) */
static int cfgFpsCap = 60; /* frame cap in fps; 0 = uncapped (vsync); menu only exposes 30/60 */
static int cfgMSAA = 4; /* 1/2/4/8 samples; default 4 (modern ports ship AA on; snaps down to the highest supported level) */
static int cfgTexFilter = 1; /* 0 = nearest, 1 = bilinear (default), 2 = N64 3-point + trilinear */
static int cfgFixMipTex = 1; /* RC2: clip mip-contaminated texture uploads to base height */
static int cfgWrapFix = 0; /* D74 sub-tile UV pre-wrap + RC3/D167 non-PoT mask-period wrap (opt-in; GE_WRAPFIX env overrides) */
static int cfgFovScale = 100; /* D211: percent of the original vertical FOV; 100 = unchanged (byte-identical) */
static int cfgWidescreenAuto = 1; /* WIDESCREEN-FOV-PLAN Phase 4: auto-scale vertical FOV by window aspect ratio; on by default, no-op at 4:3 */
static int cfgDrawDistance = 150; /* D218: percent of the level's authored far-clip/fog distance. Default raised 100->150 for v0.2.0: at the authored N64 distance, props visibly fade in just before they become visible on modern displays (Dam alarms / wall switches); 150 is the value the Steam Deck preset playtest-validated. 100 = unchanged N64. */
static int cfgDrawDistanceAutoFov = 1; /* D218: couple draw distance to Video.FovScale unless DrawDistance is set explicitly */
static int cfgLodDistance = 150; /* D249: percent scale on the geometry/model LOD-swap distance. Default raised 100->150 for v0.2.0 (same pop-in family as DrawDistance: LOD-swapped props like Dam's alarms/wall switches faded in at range); 150 matches the Steam Deck preset. 100 = unchanged N64. */
static int cfgLodDistanceAutoFov = 0; /* off by default -- unlike DrawDistance, this is meant as a standalone perf lever, not something that should silently get more expensive as FovScale widens */
static int cfgAniso = 4; /* D212: anisotropic filtering samples; 4 = the value fast3d already applied (no visual delta at default) */
static int cfgSafeAreaCrop = 1; /* crop the N64 TV-overscan safe-area margin (visible as black top/bottom bars on PC) instead of showing it; on by default */
static int cfgFullscreen = 0; /* 0 = windowed, 1 = borderless fullscreen */
/*
@@ -126,13 +129,31 @@ f32 portFovScale = 1.0f;
* were still narrow. `isTitleScreen` mirrors fr.c's own
* `lvlGetCurrentStageToLoad() != LEVELID_TITLE` guard -- the front end's
* fixed-FOV 3D must not be touched. 1.0f/identity at FovScale=100 (default),
* bit-for-bit no-op. */
* bit-for-bit no-op.
*
* WIDESCREEN-FOV-PLAN Phase 4: Video.WidescreenAuto (default on) applies an
* automatic aspect-ratio-aware scale BEFORE the manual Video.FovScale
* multiplier above, so the two compose rather than fight. Formula:
* sqrt(aspect / 4:3) -- identity at 4:3 (the game's native aspect), widens
* smoothly for 16:9/21:9/etc. Uses gfx_current_dimensions.aspect_ratio,
* already tracked and updated on window resize (port/fast3d/gfx_pc.cpp).
* The final clamp gained a symmetric floor (20 deg) alongside the existing
* 160 deg ceiling -- covers narrow/portrait-ish window resizes, which the
* auto-scale formula can otherwise degenerate toward as aspect -> 0; this
* resolves WIDESCREEN-FOV-PLAN.md's open "horizontal extreme-aspect sanity
* clamp" question. */
f32 portScaleFovY(f32 fovy, s32 isTitleScreen)
{
if (!isTitleScreen && portFovScale > 0.4f && portFovScale < 2.01f && portFovScale != 1.0f) {
fovy *= portFovScale;
if (fovy > 160.0f) { fovy = 160.0f; }
if (!isTitleScreen) {
if (cfgWidescreenAuto && gfx_current_dimensions.aspect_ratio > 0.01f) {
fovy *= sqrtf(gfx_current_dimensions.aspect_ratio / (4.0f / 3.0f));
}
if (portFovScale > 0.4f && portFovScale < 2.01f && portFovScale != 1.0f) {
fovy *= portFovScale;
}
}
if (fovy > 160.0f) { fovy = 160.0f; }
if (fovy < 20.0f) { fovy = 20.0f; }
return fovy;
}
@@ -221,11 +242,13 @@ PD_CONSTRUCTOR static void videoConfigInit(void)
configRegisterInt("Video.FixMipTextures", &cfgFixMipTex, 0, 1);
configRegisterInt("Video.WrapFix", &cfgWrapFix, 0, 1);
configRegisterInt("Video.FovScale", &cfgFovScale, 50, 150);
configRegisterInt("Video.WidescreenAuto", &cfgWidescreenAuto, 0, 1);
configRegisterInt("Video.DrawDistance", &cfgDrawDistance, 100, 400);
configRegisterInt("Video.DrawDistanceAutoFov", &cfgDrawDistanceAutoFov, 0, 1);
configRegisterInt("Video.LodDistance", &cfgLodDistance, 25, 400);
configRegisterInt("Video.LodDistanceAutoFov", &cfgLodDistanceAutoFov, 0, 1);
configRegisterInt("Video.Anisotropy", &cfgAniso, 1, 16);
configRegisterInt("Video.SafeAreaCrop", &cfgSafeAreaCrop, 0, 1);
configRegisterInt("Video.Fullscreen", &cfgFullscreen, 0, 1);
configRegisterInt("Window.Width", &cfgWinW, 0, 16384);
configRegisterInt("Window.Height", &cfgWinH, 0, 16384);
@@ -267,6 +290,7 @@ static void videoApplyImageOptions(void)
{
portFovScale = (f32)cfgFovScale / 100.0f;
gfx_set_anisotropy_level(cfgAniso);
gfx_set_safe_area_crop(cfgSafeAreaCrop);
}
static void videoApplyTexFilter(void)
+28
View File
@@ -0,0 +1,28 @@
# mouse-injector (removed from public repo)
The GEPD-Edition Mouse Injector source (GPLv2, ~1.5 MB including a prebuilt
`discord-rpc.lib`) was vendored here as a read-only reference for the PC
port's mouse-aim model, and **removed from the public repo on 2026-09-19**
during the v0.3.0 release review: it is GPL-licensed code (plus a binary)
inside an MIT-licensed project, and it is not compiled or linked by anything
in this tree — it was consulted by developers only.
## Provenance (if you need to re-vendor it locally)
- Project: "GEPD-Edition Mouse Injector" (GoldenEye 007 / Perfect Dark N64
mouse-aim injection for emulators), GPLv2.
- The file that mattered was `games/goldeneye.c` (~380 lines — the whole GE
aim/hipfire/menu model), plus `device.c` (raw mouse polling) and
`maindll.h` (tickrate units).
## What replaced it in this repo
- The ported, calibrated result lives in `port/src/input.c` (the D194
lineage; see `docs/dev/GEPD-INPUT-PLAN.md` for the design record, which
quotes the model inline).
- Historical findings that cite `reference/mouse-injector/...` paths
(`docs/dev/findings.md`, D194/D214/etc.) remain accurate as written; the
cited file content is preserved in those write-ups.
If you want the original source again for reference work, clone it locally
into `reference/mouse-injector/` (it is gitignored now) — do not commit it.
-109
View File
@@ -1,109 +0,0 @@
== census (kind, format) -> count ==
('NONZLIB', 'I4'): 363
('NONZLIB', 'I8'): 231
('NONZLIB', 'IA16'): 1
('NONZLIB', 'IA4'): 74
('NONZLIB', 'IA8'): 192
('NONZLIB', 'RGB15'): 14
('NONZLIB', 'RGBA16'): 19
('NONZLIB', 'RGBA32'): 60
('ZLIB', 'IA16_CI4'): 15
('ZLIB', 'IA16_CI8'): 91
('ZLIB', 'RGBA16_CI4'): 83
('ZLIB', 'RGBA16_CI8'): 1555
== non-zlib WIDE-pixel images: 94 ==
texnum= 0 IMAGE_COPYICON RGBA32 comp=7 32x28 lod=0
texnum= 2 IMAGE_DELICON RGBA32 comp=8 32x28 lod=0
texnum= 437 _image437_ID RGBA32 comp=8 32x16 lod=6
texnum= 439 _image439_ID RGBA32 comp=8 32x16 lod=6
texnum= 884 _image884_ID RGBA32 comp=4 32x32 lod=0
texnum=1198 _image1198_ID RGBA16 comp=6 64x17 lod=6
texnum=1199 _image1199_ID RGBA16 comp=6 64x17 lod=6
texnum=1200 _image1200_ID RGBA16 comp=6 64x17 lod=6
texnum=1201 _image1201_ID RGBA16 comp=6 64x17 lod=6
texnum=2085 IMAGE_FIRE_0 RGBA16 comp=5 14x14 lod=0
texnum=2087 IMAGE_FIRE_1 RGBA16 comp=8 14x14 lod=0
texnum=2089 IMAGE_FIRE_2 RGBA16 comp=4 14x14 lod=0
texnum=2091 IMAGE_FIRE_3 RGBA16 comp=8 14x14 lod=0
texnum=2093 IMAGE_FIRE_4 RGBA16 comp=8 14x14 lod=0
texnum=2095 IMAGE_FIRE_5 RGBA16 comp=7 14x14 lod=0
texnum=2097 IMAGE_FIRE_6 RGBA16 comp=7 14x14 lod=0
texnum=2099 IMAGE_FIRE_7 RGBA16 comp=4 14x14 lod=0
texnum=2101 IMAGE_FIRE_8 RGBA16 comp=4 14x14 lod=0
texnum=2103 IMAGE_FIRE_9 RGBA16 comp=8 14x14 lod=0
texnum=2105 IMAGE_FIRE_10 RGBA16 comp=8 14x14 lod=0
texnum=2107 IMAGE_FIRE_11 RGBA16 comp=4 14x14 lod=0
texnum=2109 IMAGE_FIRE_12 RGBA16 comp=8 14x14 lod=0
texnum=2111 IMAGE_FIRE_13 RGBA16 comp=8 14x14 lod=0
texnum=2113 IMAGE_FIRE_14 RGBA16 comp=8 14x14 lod=0
texnum=2114 _image2114_ID RGBA32 comp=4 32x32 lod=0
texnum=2115 _image2115_ID RGBA32 comp=4 32x32 lod=0
texnum=2116 _image2116_ID RGBA32 comp=4 32x32 lod=0
texnum=2117 _image2117_ID RGBA32 comp=4 32x32 lod=0
texnum=2119 _image2119_ID RGBA32 comp=4 32x32 lod=0
texnum=2120 _image2120_ID RGBA32 comp=4 32x32 lod=0
texnum=2121 _image2121_ID RGBA32 comp=4 32x32 lod=0
texnum=2122 _image2122_ID RGBA32 comp=4 32x32 lod=0
texnum=2123 _image2123_ID RGBA32 comp=4 32x32 lod=0
texnum=2124 _image2124_ID RGBA32 comp=4 32x32 lod=0
texnum=2125 _image2125_ID RGBA32 comp=4 32x32 lod=0
texnum=2126 _image2126_ID RGBA32 comp=4 32x32 lod=0
texnum=2127 _image2127_ID RGBA32 comp=4 32x32 lod=0
texnum=2128 IMAGE_FLAREORANGELINE RGBA32 comp=4 16x32 lod=6
texnum=2134 IMAGE_FLAREBLUEROUND RGBA32 comp=8 32x32 lod=0
texnum=2135 IMAGE_FLAREBLUELINE RGBA32 comp=8 16x32 lod=6
texnum=2137 _image2137_ID RGBA32 comp=4 32x32 lod=0
texnum=2138 _image2138_ID RGBA32 comp=4 32x32 lod=0
texnum=2139 _image2139_ID RGBA32 comp=4 32x32 lod=0
texnum=2140 _image2140_ID RGBA32 comp=4 32x32 lod=0
texnum=2141 _image2141_ID RGBA32 comp=4 32x32 lod=0
texnum=2142 _image2142_ID RGBA32 comp=4 32x32 lod=0
texnum=2143 _image2143_ID RGBA32 comp=4 32x32 lod=0
texnum=2144 _image2144_ID RGBA32 comp=4 32x32 lod=0
texnum=2145 _image2145_ID RGBA32 comp=8 32x32 lod=0
texnum=2146 _image2146_ID RGBA32 comp=4 32x32 lod=0
texnum=2147 _image2147_ID RGBA32 comp=4 32x32 lod=0
texnum=2148 _image2148_ID RGBA32 comp=4 32x32 lod=0
texnum=2149 _image2149_ID RGBA32 comp=4 32x32 lod=0
texnum=2150 _image2150_ID RGBA32 comp=4 32x32 lod=0
texnum=2151 _image2151_ID RGBA32 comp=4 32x32 lod=0
texnum=2152 _image2152_ID RGBA32 comp=4 32x32 lod=0
texnum=2153 _image2153_ID RGBA32 comp=4 32x32 lod=0
texnum=2154 _image2154_ID RGBA32 comp=4 32x32 lod=0
texnum=2155 _image2155_ID RGBA32 comp=4 32x32 lod=0
texnum=2156 _image2156_ID RGBA32 comp=4 32x32 lod=0
texnum=2157 _image2157_ID RGBA32 comp=4 32x32 lod=0
texnum=2158 _image2158_ID RGBA32 comp=4 32x32 lod=0
texnum=2159 _image2159_ID RGBA32 comp=4 32x32 lod=0
texnum=2160 _image2160_ID RGBA32 comp=4 32x32 lod=0
texnum=2161 IMAGE_ROCKETAMMO RGBA32 comp=4 7x22 lod=0
texnum=2162 IMAGE_PLAINMINEAMMO RGBA32 comp=4 14x14 lod=0
texnum=2163 IMAGE_GRENADEAMMO RGBA32 comp=4 14x18 lod=0
texnum=2164 IMAGE_MAGAMMO RGBA32 comp=7 5x15 lod=0
texnum=2165 IMAGE_GLAMMO RGBA32 comp=4 8x21 lod=0
texnum=2166 IMAGE_KNIFEAMMO RGBA32 comp=4 6x24 lod=0
texnum=2167 IMAGE_SHOTAMMO RGBA32 comp=4 6x20 lod=0
texnum=2231 IMAGE_9MMAMMO RGBA32 comp=4 5x12 lod=0
texnum=2232 IMAGE_RIFLEAMMO RGBA32 comp=4 5x28 lod=0
texnum=2233 IMAGE_GGAMMO RGBA32 comp=4 5x12 lod=0
texnum=2234 IMAGE_MINEAMMO RGBA32 comp=4 14x14 lod=0
texnum=2235 IMAGE_PROXAMMO RGBA32 comp=4 14x14 lod=0
texnum=2236 IMAGE_CROSSHAIR1 RGBA32 comp=4 32x32 lod=0
texnum=2237 IMAGE_CROSSHAIR2 RGBA32 comp=7 32x32 lod=0
texnum=2238 IMAGE_TIMEAMMO RGBA32 comp=4 14x14 lod=0
texnum=2430 _image2430_ID IA16 comp=4 64x32 lod=1
texnum=2510 _image2510_ID RGB15 comp=7 128x12 lod=2
texnum=2511 _image2511_ID RGB15 comp=8 128x12 lod=2
texnum=2512 _image2512_ID RGB15 comp=8 128x12 lod=2
texnum=2513 _image2513_ID RGB15 comp=6 128x12 lod=2
texnum=2514 _image2514_ID RGB15 comp=6 128x12 lod=2
texnum=2515 _image2515_ID RGB15 comp=6 128x12 lod=2
texnum=2516 _image2516_ID RGB15 comp=8 128x12 lod=2
texnum=2517 _image2517_ID RGB15 comp=8 128x12 lod=2
texnum=2518 _image2518_ID RGB15 comp=8 128x12 lod=2
texnum=2519 _image2519_ID RGB15 comp=8 128x12 lod=2
texnum=2520 _image2520_ID RGB15 comp=8 128x12 lod=2
texnum=2521 _image2521_ID RGB15 comp=8 128x12 lod=3
texnum=2522 _image2522_ID RGB15 comp=8 128x12 lod=3
texnum=2523 _image2523_ID RGB15 comp=8 128x12 lod=3
-136
View File
@@ -1,136 +0,0 @@
#!/usr/bin/env python3
"""D219 offline probe: parse the non-zlib/zlib texture bitstream headers
straight out of the ROM images segment, replicating texLoad/texInflateNonZlib
header reads (src/game/image.c) so we can compare TEXCOMPMETHOD/TEXFORMAT for
IMAGE_FIRE_N vs muzzle-flash/ammo/flare/impact siblings WITHOUT running the game.
ROM: data/ge007.ntsc-final.z64
Images segment cart start 0x108F7DF0 (port/src/romassets_u.s) -> file offset 0x8F7DF0.
Offsets are cumulative sizes from assets/images.def (image_entries_load, no align).
"""
import re, sys
SEG = 0x8F7DF0
ROM = open('data/ge007.ntsc-final.z64','rb').read()
# ---- enum name -> index from image_externs.h ----
enummap = {}
_in_enum = False
_v = 0
for line in open('assets/image_externs.h', encoding='utf-8', errors='replace'):
if 'typedef enum IMAGEIDS' in line:
_in_enum = True; continue
if _in_enum and line.strip().startswith('}'):
break
m = re.match(r'\s*([A-Za-z_][A-Za-z0-9_]*)\s*,?\s*(//.*)?$', line)
if m and not line.strip().startswith('//'):
enummap[m.group(1)] = _v; _v += 1
# ---- parse images.def: field1 is a bare index or an enum name minus IMAGE_ ----
entries = []
for line in open('assets/images.def', encoding='utf-8', errors='replace'):
m = re.match(r'\s*IMAGE\(\s*([A-Za-z0-9_]+)\s*,\s*(0x[0-9A-Fa-f]+|\d+)\s*,', line)
if not m: continue
f1, sz = m.group(1), int(m.group(2), 0)
if f1.isdigit():
idx = int(f1); name = f"_image{idx}_ID"
else:
key = 'IMAGE_' + f1 if not f1.startswith('IMAGE_') else f1
idx = enummap.get(key)
name = key
if idx is None:
# named in images.def but left _imageN_ID in the decomp enum --
# trust position
idx = len(entries)
print(f"# note: {key} not in enum, using positional index {idx}", file=sys.stderr)
assert idx == len(entries), f"images.def out of order at {name} ({idx} vs {len(entries)})"
entries.append((name, sz))
print(f"# {len(entries)} image entries", file=sys.stderr)
# cumulative offsets (image_entries_load)
offs = []
o = 0
for name, sz in entries:
offs.append(o)
o += sz
total = o
print(f"# images segment total size per g_Textures: {total:#x} (segment room to 0xC00000-0x8F7DF0 = {0xC00000-SEG:#x})", file=sys.stderr)
class Bits:
def __init__(self, buf, pos):
self.buf = buf; self.pos = pos; self.bit = 0
def read(self, n):
v = 0
for _ in range(n):
byte = self.buf[self.pos]
v = (v << 1) | ((byte >> (7 - self.bit)) & 1)
self.bit += 1
if self.bit == 8:
self.bit = 0; self.pos += 1
return v
NAMES = {n:i for i,(n,_) in enumerate(entries)}
def dump(i, name):
off = offs[i]
base = SEG + off
b0 = ROM[base]
u, z, lod = (b0>>7)&1, (b0>>6)&1, b0 & 0x3f
out = [f"texnum={i:4d} {name:28s} off={off:#08x} byte0={b0:#04x} u={u} z={z} lod={lod}"]
if z:
# zlib path: format(8), numcolours(8)+1, palette... then per-image w/h
b = Bits(ROM, base+1)
fmt = b.read(8); nc = b.read(8)+1
pal = [b.read(16) for _ in range(nc)]
out.append(f" ZLIB format={fmt} numcolours={nc} pal0={pal[0]:#05x}")
w = b.read(8); h = b.read(8)
out.append(f" image0 {w}x{h} (zlib payload follows -- indices only)")
else:
b = Bits(ROM, base+1)
fmt = b.read(4); w = b.read(8); h = b.read(8); cm = b.read(4)
out.append(f" NONZLIB format={fmt} width={w} height={h} compmethod={cm}")
return ' '.join(out)
FMT = {0:'RGBA32',1:'RGBA16',2:'RGB24',3:'RGB15',4:'IA16',5:'IA8',6:'IA4',7:'I8',8:'I4',9:'RGBA16_CI8',10:'RGBA16_CI4',11:'IA16_CI8',12:'IA16_CI4'}
CM = {0:'UNCOMPRESSED0',1:'UNCOMPRESSED1',2:'HUFFMAN',3:'HUFFMANPERHCHANNEL',4:'RLE',5:'LOOKUP',6:'HUFFMANLOOKUP',7:'RLELOOKUP',8:'HUFFMANBLUR',9:'RLEBLUR'}
def dump2(i, name):
off = offs[i]
base = SEG + off
b0 = ROM[base]
z = (b0>>6)&1; lod = b0 & 0x3f
line = f"texnum={i:4d} {name:28s} off={off:#08x} byte0={b0:#04x} z={z} lod={lod}"
if z:
b = Bits(ROM, base+1)
fmt = b.read(8); nc = b.read(8)+1
pal = [b.read(16) for _ in range(nc)]
w = b.read(8); h = b.read(8)
line += f" ZLIB fmt={FMT.get(fmt,fmt)}({fmt}) ncols={nc} pal0={pal[0]:#05x} img0={w}x{h}"
else:
b = Bits(ROM, base+1)
fmt = b.read(4); w = b.read(8); h = b.read(8); cm = b.read(4)
line += f" NONZLIB fmt={FMT.get(fmt,fmt)}({fmt}) {w}x{h} comp={CM.get(cm,cm)}({cm})"
return line
targets = []
# fire + smoke particle range
for i in range(2084, 2116):
targets.append(i)
# flares
for n in ['IMAGE_FLAREORANGELINE','IMAGE_FLAREBLUEROUND','IMAGE_FLAREBLUELINE','IMAGE_FLAREWHITEROUND']:
targets.append(NAMES[n])
# ammo HUD icons
for n in ['IMAGE_ROCKETAMMO','IMAGE_PLAINMINEAMMO','IMAGE_GRENADEAMMO','IMAGE_MAGAMMO','IMAGE_GLAMMO','IMAGE_KNIFEAMMO','IMAGE_SHOTAMMO']:
targets.append(NAMES[n])
# impact sprites
for n in ['IMAGE_IMPACT1','IMAGE_IMPACT2','IMAGE_IMPACT3','IMAGE_IMPACT4','IMAGE_IMPACTMULTI','IMAGE_IMPACTREDBRICK2','IMAGE_IMPACTBRICK2','IMAGE_IMPACTBRICK3']:
targets.append(NAMES[n])
# smoke sprites
for n in ['IMAGE_SMOKE1','IMAGE_SMOKE2','IMAGE_SMOKEBALLS1']:
targets.append(NAMES[n])
seen = set()
for i in targets:
if i in seen or i >= len(entries): continue
seen.add(i)
print(dump2(i, entries[i][0]))
+35 -5
View File
@@ -541,7 +541,7 @@ void bossMainloop(void)
gdl = firstGdl = dynGetMasterDisplayList();
#ifdef DEBUGMENU
#if defined(DEBUGMENU)
//ported from pd beta, official way to open debug menu
// If menu is open (?) or player has pressed C down + C up
if (g_BossIsDebugMenuOpen || joyGetButtons(0, U_CBUTTONS | D_CBUTTONS) == (U_CBUTTONS | D_CBUTTONS)) {
@@ -552,16 +552,46 @@ void bossMainloop(void)
} else if (joyGetButtons(0, START_BUTTON) == 0) {
g_DebugMode = g_DebugHighlightedOption;
} else
#endif
#ifndef DEBUGMENU
if (g_BossIsDebugMenuOpen)
#endif
{
joyStickXPos = joyGetStickX(0);
joyStickYPos = joyGetStickY(0);
joyButtons = joyGetButtons(0, ANY_BUTTON);
g_BossIsDebugMenuOpen = debug_menu_processor(joyStickXPos, joyStickYPos, joyButtons, joyGetButtonsPressedThisFrame(0, ANY_BUTTON));
}
#elif defined(PORT)
// GE_DEBUGMENU=1 -- diagnostic-only: enable leftover debug-menu open
// path (D243). Unset (default) reproduces the #ifndef DEBUGMENU
// behavior below exactly -- no behavior change.
static int ge_debugmenu = -1;
if (ge_debugmenu < 0) ge_debugmenu = getenv("GE_DEBUGMENU") != NULL;
if (ge_debugmenu && (g_BossIsDebugMenuOpen || joyGetButtons(0, U_CBUTTONS | D_CBUTTONS) == (U_CBUTTONS | D_CBUTTONS))) {
joyStickXPos = joyGetStickX(0);
joyStickYPos = joyGetStickY(0);
joyButtons = joyGetButtons(0, ANY_BUTTON);
g_BossIsDebugMenuOpen = debug_menu_processor(joyStickXPos, joyStickYPos, joyButtons, joyGetButtonsPressedThisFrame(0, ANY_BUTTON));
} else if (ge_debugmenu && joyGetButtons(0, START_BUTTON) == 0) {
g_DebugMode = g_DebugHighlightedOption;
} else if (ge_debugmenu) {
joyStickXPos = joyGetStickX(0);
joyStickYPos = joyGetStickY(0);
joyButtons = joyGetButtons(0, ANY_BUTTON);
g_BossIsDebugMenuOpen = debug_menu_processor(joyStickXPos, joyStickYPos, joyButtons, joyGetButtonsPressedThisFrame(0, ANY_BUTTON));
} else if (g_BossIsDebugMenuOpen)
{
joyStickXPos = joyGetStickX(0);
joyStickYPos = joyGetStickY(0);
joyButtons = joyGetButtons(0, ANY_BUTTON);
g_BossIsDebugMenuOpen = debug_menu_processor(joyStickXPos, joyStickYPos, joyButtons, joyGetButtonsPressedThisFrame(0, ANY_BUTTON));
}
#else
if (g_BossIsDebugMenuOpen)
{
joyStickXPos = joyGetStickX(0);
joyStickYPos = joyGetStickY(0);
joyButtons = joyGetButtons(0, ANY_BUTTON);
g_BossIsDebugMenuOpen = debug_menu_processor(joyStickXPos, joyStickYPos, joyButtons, joyGetButtonsPressedThisFrame(0, ANY_BUTTON));
}
#endif
lvlManageMpGame();
shuffle_player_ids();
+74 -10
View File
@@ -1722,16 +1722,25 @@ s32 sub_GAME_7F0B5864(s32 portalnum, bbox2d *bbox)
{
s32 degenerate = (bounds.max.x <= bounds.min.x) || (bounds.max.y <= bounds.min.y);
#ifdef PORT
/* D106: a portal straddling the camera near-plane feeds z==0 clip
* points into transform3Dto2DWithZScaling, whose inv_z = -1e20 sends
* them to +/-1e20-scale screen coords. On N64 those bracket the view
* symmetrically so the box just clamps to full screen downstream;
* on PC the exact garbage can come back min>max on only one axis, or
* non-finite, slipping past the check above and culling the room the
* player is walking toward. Any wildly out-of-range / non-finite
* bound means "portal fills the view" -> full screensize. */
/* D106 (revised by D271): a portal straddling the camera near-plane
* feeds z==0 clip points into transform3Dto2DWithZScaling, whose
* inv_z = -1e20 sends them to +/-1e20-scale screen coords. On N64
* those huge FINITE values flow straight into bgRectIntersect (pure
* comparisons), which either clamps them to the parent/screen box
* (portal covers the view -> room kept) or yields an empty box
* (the portal's visible projection falls off-screen -> room CULLED).
* The original D106 guard mapped ANY out-of-range bound to full
* screen, which over-included the second case: at Surface 1's boot
* view, 9 of 11 near-plane-straddling portals produce garbage that
* clamps to an empty box (N64 culls the room) but was forced
* full-screen here, drawing whole rooms N64 never submits. Only
* NON-FINITE bounds are unsafe to let through (NaN poisons the
* comparisons downstream); this projection can reach ~1e26 at most,
* so lim=1e38 catches NaN/inf and nothing finite. Finite min>max is
* already caught by the `degenerate` check above, which is the N64
* code path itself. */
{
const f32 lim = 100000.0f;
const f32 lim = 1e38f;
if (!(bounds.min.x > -lim && bounds.min.x < lim) ||
!(bounds.min.y > -lim && bounds.min.y < lim) ||
!(bounds.max.x > -lim && bounds.max.x < lim) ||
@@ -2404,6 +2413,48 @@ s32 bgLoadRoomVtxData(s32 roomnum, u8 *dst, s32 len)
#ifdef PORT
/* D85: byte-swap the raw N64 Vtx table (16-byte struct, no widening). */
bgSwapRoomVtx((Vtx *)dst, result);
/* D236 pass 16 (TEMP): pass 15's live GE_D236ALPHA capture found the
* Surface1 tree-card render class's vertex alpha capped at <=36/255
* (<=14% opacity) across a full far->close walk to the tree line, and
* confirmed (via the combine_mode decode) that this vertex alpha
* directly multiplies the texture's own alpha in the blend equation --
* so a capped-low vertex alpha caps every tree pixel's final opacity
* outright. bgSwapRoomVtx (just above) explicitly leaves colour bytes
* untouched, so whatever is in cn[] here is exactly what bgDecompress
* produced -- this is the earliest point after decompression the raw
* per-room vertex colour data can be inspected, independent of which
* camera path a live player happens to walk. Unlike GE_D236ALPHA (which
* only sees whatever triangles are actually drawn from wherever the
* player is standing), this dumps a histogram of EVERY vertex's alpha
* byte (cn[3]) for the WHOLE room's table the moment it loads --
* settling whether the low ceiling pass 15 measured is a sampling
* artifact (the walked path just never reached a vertex with a high
* baked alpha) or a genuine property of this room's authored data (no
* vertex in the whole table goes above the low ceiling, regardless of
* where the camera is). Zero cost unset. Remove once D236 pass 16
* concludes. */
if (getenv("GE_D236RAW")) {
s32 vtxCount = result / (s32)sizeof(Vtx);
s32 vi;
u32 buckets[5] = {0, 0, 0, 0, 0}; /* 0 / 1-36 / 37-100 / 101-200 / 201-255 */
u8 amin = 255, amax = 0;
Vtx *vtxArr = (Vtx *)dst;
for (vi = 0; vi < vtxCount; vi++) {
u8 a = vtxArr[vi].v.cn[3];
if (a < amin) amin = a;
if (a > amax) amax = a;
if (a == 0) buckets[0]++;
else if (a <= 36) buckets[1]++;
else if (a <= 100) buckets[2]++;
else if (a <= 200) buckets[3]++;
else buckets[4]++;
}
osSyncPrintf(
"D236RAW room=%d n=%d amin=%u amax=%u buckets[0,1-36,37-100,101-200,201-255]=%u,%u,%u,%u,%u\n",
(int)roomnum, (int)vtxCount, amin, amax,
buckets[0], buckets[1], buckets[2], buckets[3], buckets[4]);
}
#endif
room->vertices = (Vtx *)dst;
@@ -3071,7 +3122,21 @@ void bgBuildRoomVtxBounds(s32 roomID)
points[numpoints].max[i] = -0x8000;
}
#ifdef PORT
/* D312: on N64 `dma.par` IS the G_VTX params byte ((n-1)<<4|v0),
* but the PC Gfx_le shim packs w0 = (cmd<<24)|(par<<16)|len and
* Gdma_le.par spans bits 0-23 -- so `(dma.par>>4)&0xf` reads the
* low nibble of len>>4 (= n&0xf), not the (n-1) field. Batches
* with exactly 16 vertices (the common case) collapse to
* numvertices=1: the batch's bounds shrink to a single vertex and
* bgTestBulletHitBackground's per-batch AABB pretest rejects the
* batch for nearly every ray, so its triangles are never tested
* (D312 projectile pass-through / D313 missing bullet impacts).
* Recover the params byte from bits 16-23 of words.w0. */
numvertices = (((u32)gdl[cmdindex].words.w0 >> 20) & 0xf) + 1;
#else
numvertices = ((gdl[cmdindex].dma.par >> 4) & 0xf) + 1;
#endif
vtx = (Vtx *)(SEGMENT_OFFSET(gdl[cmdindex].dma.addr) + (u32)vertices);
@@ -3762,7 +3827,6 @@ bool bgTestRayIntersectionInRoom(coord3d *from, coord3d *to, coord3d *dir, RoomV
return found;
}
/**
* In order to make bgTestBulletHitBackground match, 4 bytes had to dropped from the regular HitThing struct.
* There might be an alternate way of matching this that doesn't require the sub-struct.
+368 -1
View File
@@ -335,6 +335,85 @@ f32 g_MpSwirlDistance;
#define ALIGN64_V3(val) (((val) | 0x3f) ^ 0x3f)
#ifdef PORT
/* D243 (M-145): model-lifecycle probe for the Dam-abseil "two Bonds + camera
* shake" (M-143/M-145: the gBondViewCutscene POSEND look-at reads
* field_3C4/8/C, a leaky-integrator filter of Bond's real position that is
* never reset when a cutscene warps him; M-145 ties both symptoms to a
* suspected model-remove/recreate window around the CHR_BOND_CINEMA pad
* teleports). GE_D243M=1 logs:
* D243M: CREATE/REMOVE -- every bodyModel assignment/removal (the only two
* sites in this file), with camera mode at the moment it happened;
* D243M: tick -- once per playerTick() call while the third-person model
* exists OR a cutscene camera mode (POSEND/INTRO) is active: index, model
* pointer, g_CameraMode, dword_CODE_bss_80079A18, prop->pos,
* field_488.pos, and the filter fields field_3B8 + field_3C4/8/C;
* D243M: draw -- once per on-screen render_pos submission, so a second
* Bond instance shows up as two draw lines (different idx or model
* pointer) at the same frame number.
* The frame counter increments once per playerTick() call (solo play: once
* per frame); it is NOT necessarily numerically identical to port's internal
* g_framesRendered -- cross-reference by elapsed offset, same convention as
* front.c's GE_D243 trace. D250: getenv cached. Diagnosis only; zero
* behaviour change when unset.
*/
static int d243mEnabled(void)
{
static int s = -1;
if (s < 0) { s = getenv("GE_D243M") != NULL; }
return s;
}
static int g_d243mFrameCounter = 0;
/* M-154: accessors for the D243M: chrdraw census probe in objecthandler.c's
* drawjointlist (which does not include bondview.h). ProbeActive() folds in
* the scripted-camera gate (POSEND/INTRO/SWIRL/FADESWIRL) so callers need no
* camera-mode knowledge; silent during FPS play. */
int d243mProbeActive(void)
{
return d243mEnabled() &&
((g_CameraMode == CAMERAMODE_POSEND) || (g_CameraMode == CAMERAMODE_INTRO) ||
(g_CameraMode == CAMERAMODE_SWIRL) || (g_CameraMode == CAMERAMODE_FADESWIRL));
}
int d243mGetFrameCounter(void) { return g_d243mFrameCounter; }
/* D243 M-187: the scripted-camera-mode test alone, WITHOUT the GE_D243M
* getenv gate -- originally added for the always-on M-183/M-185 sanity
* clamps in model.c (playspeed/endframe), which must fire for every real
* player regardless of whether the diagnostic env var happens to be set
* (before this, both clamps were mistakenly gated on d243mProbeActive(),
* env-var AND camera-mode, so they only ever fired during a diagnostic
* capture). Renamed from d243mCutsceneActive (M-190): the same "is the
* game in a scripted/no-control camera state" test is also the missing
* gate port/src/input.c's hipDirectCompute() documented but never
* implemented -- see that function's comment and M-190 in findings.md. */
int gameScriptedCameraActive(void)
{
return (g_CameraMode == CAMERAMODE_POSEND) || (g_CameraMode == CAMERAMODE_INTRO) ||
(g_CameraMode == CAMERAMODE_SWIRL) || (g_CameraMode == CAMERAMODE_FADESWIRL);
}
/* D243 M-169: the "real signal" named as next-step (b) in findings.md's
* D243 CONSOLIDATED NEXT STEPS -- a monotonic epoch counter bumped by
* chrai.c's AI_TRYTeleportingChrToPad case (the exact decomp call, chrai.c
* ~4064-4119, that legitimately re-anchors a chr's model root joint via
* setsuboffset() for a real cutscene shot-change) right after that call.
* chr.c's GE_D243X4 (render_pos freeze) and this file's GE_D243X3
* (field_488.pos freeze) both poll this to re-baseline their frozen
* snapshot exactly on a legitimate shot-change instead of either freezing
* forever (losing real repositioning) or guessing a position-delta
* threshold (next-step (a), not attempted). Global, not per-chr: sufficient
* for this cutscene's scope (one chr teleported at a time); revisit if a
* future cutscene needs per-chr tracking. Diagnosis/experiment plumbing
* only -- zero cost and zero behaviour change when GE_D243M is unset, since
* nothing reads the epoch unless the X3/X4 experiments are also enabled. */
static u32 g_d243TeleportEpoch = 0;
void d243NotifyTeleport(void) { g_d243TeleportEpoch++; }
u32 d243GetTeleportEpoch(void) { return g_d243TeleportEpoch; }
#endif
void solo_char_load(void)
{
f32 yaw;
@@ -519,7 +598,36 @@ void solo_char_load(void)
load_object_fill_header(headheader, (u8 *)c_item_entries[head].filename, weaponbuf0 + cursor, size0 - cursor, &pool);
cursor = ALIGN64_V3(get_pc_buffer_remaining_value((u8 *)c_item_entries[head].filename) + cursor + 0x3f);
model = (Model *)(weaponbuf0 + cursor);
#ifdef PORT
/* D243 M-188: root cause of the Bond cutscene/positioning
* family. The literal 0xfb below is 0xbc+0x3f -- the exact
* same "sizeof(X) + 0x3f, then ALIGN64_V3" idiom used one
* line up for ModelFileHeader (line 591), just pre-computed
* as a constant for N64's 32-bit sizeof(Model) (0xbc = 188
* bytes). On PC, widened pointer fields (attachedto,
* attachedto_objinst, anim, anim2, plus alignment padding)
* grow sizeof(Model) to 232 (0xe8) bytes -- confirmed by a
* standalone compile with the real build flags, see
* scratch/sizeof_model.c. The stale 0xfb under-reserves by
* ~40 bytes, so `animdata` below gets placed INSIDE the
* tail of the just-allocated Model struct instead of after
* it -- corrupting exactly playspeed/animrate/unkac/unkb0/
* unkb4 (this Model's own last ~5 fields) with whatever
* animInit() and the animation-node data first write there.
* This is a live match for the D243 investigation's own
* captured symptom (findings.md D243 M-187/M-188): a freshly
* created third-person body Model's playspeed/animrate read
* back as this exact chr's own prop->pos.y/pos.z the moment
* modelSetAnimPlaySpeed first touches it. Fix: use the real
* sizeof(Model) instead of the stale 32-bit-derived literal,
* completing the same idiom the surrounding code already
* uses -- ABI/layout-only, no logic change, no game-code
* behavior difference on N64 (this branch didn't exist
* there). */
cursor = ALIGN64_V3(cursor + sizeof(Model) + 0x3f);
#else
cursor = ALIGN64_V3(cursor + 0xfb);
#endif
modelCalculateRwDataLen(bodyheader);
modelCalculateRwDataLen(headheader);
@@ -570,6 +678,20 @@ void solo_char_load(void)
self->chrflags |= CHRFLAG_INIT;
setsuboffset((*pp)->bodyModel, &(*pp)->prop->pos);
setsubroty(g_CurrentPlayer->bodyModel, yaw);
#ifdef PORT
if (d243mEnabled()) {
osSyncPrintf("D243M: CREATE frame=%d model=%p prop=%p pos=%.1f,%.1f,%.1f cam=%d subcam=%d "
"sizeofModel=%zu\n",
g_d243mFrameCounter,
(void *) g_CurrentPlayer->bodyModel,
(void *) g_CurrentPlayer->prop,
(double) g_CurrentPlayer->prop->pos.f[0],
(double) g_CurrentPlayer->prop->pos.f[1],
(double) g_CurrentPlayer->prop->pos.f[2],
(int) g_CameraMode, (int) dword_CODE_bss_80079A18,
sizeof(Model));
}
#endif
#ifndef VERSION_US
self->headnum = head;
self->bodynum = body;
@@ -622,6 +744,15 @@ void solo_char_load(void)
*/
void bondviewRemovePlayerBody(void)
{
#ifdef PORT
if (d243mEnabled()) {
osSyncPrintf("D243M: REMOVE frame=%d had_model=%d chr=%p cam=%d subcam=%d\n",
g_d243mFrameCounter,
(int)(g_CurrentPlayer->bodyModel != 0),
(void *) g_CurrentPlayer->prop->chr,
(int) g_CameraMode, (int) dword_CODE_bss_80079A18);
}
#endif
if ((g_CurrentPlayer->prop->chr) && (getPlayerCount() == 1))
{
chrpropCleanupForRemoval(g_CurrentPlayer->prop);
@@ -1552,6 +1683,19 @@ void bondviewFrozenCameraTick(u16 buttons, u16 oldbuttons, struct coord3d *pos,
arg6->f[1] = g_CameraLookAtBondPad->pos.f[1];
arg6->f[2] = g_CameraLookAtBondPad->pos.f[2];
#ifdef PORT
/* D243 (M-142): sibling probe to GE_D243CAM below, for the
* look-at-pad branch of CAMERAMODE_POSEND (untested as of
* M-141/M-142 -- the pad-orbit branch below was ruled out with
* zero hits, so the Dam cutscene must be reaching this branch
* or the gBondViewCutscene branch further down instead). */
if (getenv("GE_D243CAM")) {
osSyncPrintf("D243CAM lookatpad: pos=%.2f,%.2f,%.2f pos2=%.2f,%.2f,%.2f\n",
(double) pos->f[0], (double) pos->f[1], (double) pos->f[2],
(double) pos2->f[0], (double) pos2->f[1], (double) pos2->f[2]);
}
#endif
return;
}
@@ -1590,6 +1734,22 @@ void bondviewFrozenCameraTick(u16 buttons, u16 oldbuttons, struct coord3d *pos,
pos2->f[2] = pos->f[2] - (cosf(gBondViewCutscene->verta) * cosf(gBondViewCutscene->theta));
}
#ifdef PORT
/* D243 (M-142): sibling probe to GE_D243CAM below, for the
* gBondViewCutscene spherical-angle branch of CAMERAMODE_POSEND
* -- the other untested candidate for the Dam abseil cutscene's
* camera path (the pad-orbit branch below was ruled out with
* zero hits in M-141/M-142). theta/verta are the spherical
* angles driving pos2's look-at offset from pos; a jump/jitter
* in either between calls would show up as the reported shake. */
if (getenv("GE_D243CAM")) {
osSyncPrintf("D243CAM cutscene: pos=%.2f,%.2f,%.2f pos2=%.2f,%.2f,%.2f theta=%.4f verta=%.4f\n",
(double) pos->f[0], (double) pos->f[1], (double) pos->f[2],
(double) pos2->f[0], (double) pos2->f[1], (double) pos2->f[2],
(double) gBondViewCutscene->theta, (double) gBondViewCutscene->verta);
}
#endif
return;
}
@@ -1616,6 +1776,27 @@ void bondviewFrozenCameraTick(u16 buttons, u16 oldbuttons, struct coord3d *pos,
pos->f[1] = setupPad->pos.f[1] + flt_CODE_bss_80079A10 + flt_CODE_bss_80079A0C;
pos->f[2] = setupPad->pos.f[2] + (cosf(flt_CODE_bss_80079A00) * flt_CODE_bss_80079A08) + sinf(flt_CODE_bss_80079A00) * 0.0f;
#ifdef PORT
/* D243 (M-142): the abseil/end-of-Dam cutscene's camera is this
* orbit -- a fixed look-at (pos2, above setupPad) with the eye (pos)
* circling it at radius flt_CODE_bss_80079A08, angle accumulated by
* flt_CODE_bss_80079A04 * g_GlobalTimerDelta each tick. User-confirmed
* live (M-142): the visible "violent shake" reproduces even on a run
* where the separate D146 stale-memory burst never fires at all --
* decouples the two symptoms M-107 had been treating as related.
* This dumps the angle delta and computed eye position every call so
* a live capture can show whether g_GlobalTimerDelta (the D155/D193
* wall-clock sim-tick count, normally clamped to <=6 but not
* necessarily steady at 1) is jittering frame-to-frame in a way that
* would show up as this exact judder. */
if (getenv("GE_D243CAM")) {
osSyncPrintf("D243CAM: dt=%.4f angle=%.4f pos=%.2f,%.2f,%.2f pad=%d\n",
(double) g_GlobalTimerDelta, (double) flt_CODE_bss_80079A00,
(double) pos->f[0], (double) pos->f[1], (double) pos->f[2],
(int) dword_CODE_bss_80079A14);
}
#endif
flt_CODE_bss_80079A00 += flt_CODE_bss_80079A04 * g_GlobalTimerDelta;
while (flt_CODE_bss_80079A00 >= M_TAU_F)
@@ -10284,6 +10465,85 @@ s32 playerTick(PropRecord *prop)
index = getPlayerPointerIndex(prop);
chr = prop->chr;
#ifdef PORT
/* GE_D243SWIRL=<tick> -- diagnostic-only: force CAMERAMODE_SWIRL once, at the
* given tick of this per-player function (called once/frame while playing),
* to make ai_25's AI_IFCameraIsInBondSwirl poll jump into ai_17 (the Dam
* abseil cutscene) without navigating to the ledge (D243). Independent of
* GE_D243M/d243mEnabled() so it works with just this one flag set. No-op,
* zero cost, when unset. */
{
static int d243SwirlTarget = -2; /* -2 = uncached, -1 = disabled */
static int d243SwirlTick = 0;
if (d243SwirlTarget == -2) {
const char *e = getenv("GE_D243SWIRL");
d243SwirlTarget = e ? atoi(e) : -1;
}
if (d243SwirlTarget >= 0) {
d243SwirlTick++;
if (d243SwirlTick == d243SwirlTarget) {
osSyncPrintf("D243SWIRL: forcing CAMERAMODE_SWIRL at tick=%d\n", d243SwirlTick);
bondviewSetCameraMode(CAMERAMODE_SWIRL);
}
}
}
if (d243mEnabled()) {
struct player *pp = g_playerPointers[index];
int cutsceneCam = ((g_CameraMode == CAMERAMODE_POSEND) || (g_CameraMode == CAMERAMODE_INTRO));
g_d243mFrameCounter++;
if (cutsceneCam || (pp->bodyModel != NULL)) {
/* M-154: rp = the render_pos pointer itself (a change between
* phases would mean the Model's matrix buffer is being
* reassigned/aliased). The raw pre-view translation used to be
* logged here too, but see M-154 fix 2 below -- it is captured
* by the chrdraw census at draw time instead. */
RenderPosView *d243mrp = (pp->bodyModel != NULL) ? pp->bodyModel->render_pos : NULL;
/* M-154 fix 2 (second abseil crash, same PC/fault addr): the
* NULL/-1 guard was NOT sufficient -- render_pos is assigned by
* instcalcmatrices() during the draw pass (model.c:2453), i.e.
* AFTER this probe point, so between CREATE and the first draw
* it holds whatever the reused AnimModelSlot last had: observed
* as the 0xFF tombstone (-1) on run 1, and a different bad
* value that passed the sentinel guard on run 2 (fault addr was
* still exactly -1). Sentinel-whack-a-mole is the wrong shape
for a probe: NEVER dereference render_pos at tick time. The raw
* root translation is captured by the D243M chrdraw census in
* drawjointlist instead, where the game itself dereferences
* render_pos moments later (gSPSegment), so validity is
* guaranteed there -- and draw-time is the more accurate sample
* anyway (tick-time would read the previous frame's buffer).
* rp=%p stays: logging the pointer value costs no deref. */
/* M-154b: propptr/chrptr -- the phase-3 A,A,B,C position cycle
* with smoothly falling y could mean pp->prop itself rotates
* among three PropRecords per frame; a changing pointer proves
* it, a constant one points at a cycling x/z source (script /
* anim table). */
osSyncPrintf("D243M: tick frame=%d idx=%d model=%p propptr=%p chrptr=%p "
"cam=%d subcam=%d "
"prop=%.1f,%.1f,%.1f f488pos=%.1f,%.1f,%.1f "
"f3B8=%.1f,%.1f,%.1f f3C4=%.1f,%.1f,%.1f "
"rp=%p\n",
g_d243mFrameCounter, index,
(void *) pp->bodyModel,
(void *) pp->prop, (void *) pp->prop->chr,
(int) g_CameraMode, (int) dword_CODE_bss_80079A18,
(double) pp->prop->pos.f[0],
(double) pp->prop->pos.f[1],
(double) pp->prop->pos.f[2],
(double) pp->field_488.pos.f[0],
(double) pp->field_488.pos.f[1],
(double) pp->field_488.pos.f[2],
(double) pp->field_3B8.f[0],
(double) pp->field_3B8.f[1],
(double) pp->field_3B8.f[2],
(double) pp->field_3C4,
(double) pp->field_3C8,
(double) pp->field_3CC,
(void *) d243mrp);
}
}
#endif
if (chr != NULL)
{
@@ -10300,7 +10560,7 @@ s32 playerTick(PropRecord *prop)
g_playerPointers[index]->field_AC = 0;
ret = chrTick(prop);
g_playerPointers[index]->field_AC = 1;
g_playerPointers[index]->field_488.collision_position.x = g_playerPointers[index]->prop->pos.x;
g_playerPointers[index]->field_488.collision_position.y = g_playerPointers[index]->prop->pos.y;
g_playerPointers[index]->field_488.collision_position.z = g_playerPointers[index]->prop->pos.z;
@@ -10309,11 +10569,102 @@ s32 playerTick(PropRecord *prop)
if (prop->flags & PROPFLAG_ONSCREEN)
{
#ifdef PORT
if (d243mEnabled()) {
osSyncPrintf("D243M: draw frame=%d idx=%d model=%p\n",
g_d243mFrameCounter, index,
(void *) g_playerPointers[index]->bodyModel);
}
#endif
RenderPosView *rp = g_playerPointers[index]->bodyModel->render_pos;
matrix_4x4_multiply_homogeneous(currentPlayerGetViewToWorldMtxf(), (Mtxf *) rp, (Mtxf *) mtx);
#ifdef PORT
/* D243 M-167: M-162/M-163 smoothed prop->pos (chr.c) but the
* visible shake was UNCHANGED -- because field_488.pos, which
* THIS site writes and which is what the camera's look-at
* filter (M-143's field_3B8/field_3C4-8-C) actually chases,
* comes from render_pos here -- a completely separate data
* path from prop->pos, driven by the model's own animated
* skeleton (subcalcmatrices/instcalcmatrices, computed inside
* chrTick's chrUpdateAnim -> modelTickAnim/subcalcpos calls,
* upstream of and independent from prop->pos). M-163's live
* capture confirmed this directly: field_488.pos kept cycling
* every tick (228.8/229.3/79.3/-1214.7 repeating) while
* prop->pos in the same capture was smooth. This experiment
* targets the actual variable the filter reads: skip this
* write entirely while active, freezing field_488.pos at
* whatever it was the moment suppression engaged. Gated on
* GE_D243X3 (separate from GE_D243X2 so results aren't
* conflated) + the same POSEND-active gate. THIS IS A TEST,
* NOT A FIX -- revert once it reports (see docs/dev/findings.md
* D243 M-167). */
/* D243 M-170 (superseded by M-190 below): the original "fix"
* unconditionally froze field_488.pos during POSEND, only
* letting a write through on the tick a shot-change teleport
* fired. That killed the shake but also killed legitimate
* camera tracking of Bond's real per-tick motion within a
* shot -- the M-189 buffer-overlap fix that landed this same
* session removed the actual cause of the shake (corrupted
* render-skeleton data), so freezing is no longer needed and
* was actively wrong (user-reported: "camera angles do not
* follow bond"). */
/* D243 M-190: write field_488.pos every tick unconditionally
* (real tracking restored), and instead re-seed the look-at
* filter's leaky-integrator accumulator (field_3B8, read by
* bondviewUpdatePlayerCollisionPositionFields) the instant a
* legitimate shot-change teleport fires, rather than letting
* it slowly converge over ~20 ticks (M-143's original shake
* mechanism, confirmed PC-only vs N64 in M-144). Detected via
* the same d243TeleportEpoch counter M-170 already used
* (bumped by d243NotifyTeleport() in chrai.c's
* AI_TRYTeleportingChrToPad handler) -- only the response to
* an epoch change is different now: reset the filter to the
* new position instead of freezing the input to it. */
extern u32 d243GetTeleportEpoch(void);
static u32 s_d243LastEpoch = 0;
static int s_d243HaveEpoch = 0;
u32 epoch;
int justTeleported;
epoch = d243GetTeleportEpoch();
justTeleported = (!s_d243HaveEpoch) || (epoch != s_d243LastEpoch);
s_d243LastEpoch = epoch;
s_d243HaveEpoch = 1;
#endif
g_playerPointers[index]->field_488.pos.x = mtx[12] + (mtx[4] * 7.0f);
g_playerPointers[index]->field_488.pos.y = mtx[13] + (mtx[5] * 7.0f);
g_playerPointers[index]->field_488.pos.z = mtx[14] + (mtx[6] * 7.0f);
#ifdef PORT
if ((g_CameraMode == CAMERAMODE_POSEND) && justTeleported)
{
f32 fx = g_playerPointers[index]->field_488.pos.x;
f32 fy = g_playerPointers[index]->field_488.pos.y;
f32 fz = g_playerPointers[index]->field_488.pos.z;
/* Re-seed so the filter's steady state (field_3C4/8/C =
* field_3B8 * FACTOR_2) already equals the new position,
* instead of a fresh ~20-tick geometric decay toward it
* (bondviewUpdatePlayerCollisionPositionFields, the
* FACTOR_1/FACTOR_2 pair). field_3B8 is the PRE-scale
* accumulator, so it must be seeded at pos/FACTOR_2, not
* pos itself -- field_3C4/8/C (what the camera actually
* reads) are set directly to the real position so it's
* correct even before the next tick's filter update. */
g_playerPointers[index]->field_3B8.f[0] = fx / S7F081478_FACTOR_2;
g_playerPointers[index]->field_3B8.f[1] = fy / S7F081478_FACTOR_2;
g_playerPointers[index]->field_3B8.f[2] = fz / S7F081478_FACTOR_2;
g_playerPointers[index]->field_3C4 = fx;
g_playerPointers[index]->field_3C8 = fy;
g_playerPointers[index]->field_3CC = fz;
}
if (d243mEnabled()) {
osSyncPrintf("D243M: f488write frame=%d idx=%d cam=%d reseed=%d epoch=%u f488pos=%.1f,%.1f,%.1f\n",
g_d243mFrameCounter, index,
(int) g_CameraMode, (int) (justTeleported && g_CameraMode == CAMERAMODE_POSEND),
(unsigned) epoch,
(double) g_playerPointers[index]->field_488.pos.x,
(double) g_playerPointers[index]->field_488.pos.y,
(double) g_playerPointers[index]->field_488.pos.z);
}
#endif
}
return ret;
@@ -10588,6 +10939,22 @@ lean_return_to_centre:
angle *= fa->x;
local90 = fa->z;
frame = fa->y;
#ifdef PORT
/* D173 M-174: name the exact firing_animation_groups[group][sub]
* entry the puppet's per-tick animation selector lands on, so
* the ~328-unit float (M-172/M-173) can be traced to a specific
* table row instead of guessed from a static read (the group/sub
* selection logic above is too branchy to trust a static guess
* -- already tried and got it wrong once this session). Fires
* whenever this table lookup runs, gated on the existing
* d243mEnabled(); zero cost when GE_D243M is unset. */
if (d243mEnabled()) {
osSyncPrintf("D243M: firinganim frame=%d idx=%d group=%d sub=%d fa_y=%.1f fa_z=%.1f fa_x=%.2f\n",
g_d243mFrameCounter, index, (int) group, (int) sub,
(double) fa->y, (double) fa->z, (double) fa->x);
}
#endif
}
cur = ppointers[index]->players_cur_animation;
+200
View File
@@ -2447,6 +2447,11 @@ s32 chrTick(PropRecord *prop)
s32 headSwitchVisible;
s32 headVisible;
s32 tickamount;
#ifdef PORT
int d243x2Active = 0; /* D243 M-162/M-163, see the CHRFLAG_HIDDEN else block below */
coord3d d243x2SavedPos = {0};
coord3d d243x2CycledPos = {0};
#endif
renderdata = D_8002CC6C;
chr = prop->chr;
@@ -2456,6 +2461,30 @@ s32 chrTick(PropRecord *prop)
#ifdef PORT
d193aSample(chr, model);
/* D173 M-173: does a normal (non-player-puppet) chr's prop->pos, read
* BEFORE this tick's position-update dispatch runs, differ from what
* the existing D243M: onscreen probe logs AFTER it (chr.c:~2705, at
* after_position_update:)? For the player's own third-person puppet,
* M-172 (findings.md D173) measured a fixed ~328-unit gap between
* prop->pos and the rendered root that holds across levels and
* animations. This applies the exact same before/after comparison to
* EVERY ticked chr (not just the player), gated on the existing
* d243mProbeActive() (fires during INTRO/SWIRL/POSEND, silent in normal
* FPS play) -- if ordinary guards show the same before/after gap, the
* compensation a normal chr's render is missing is universal (a
* port-layer/render-path gap); if guards show ~zero gap, the puppet's
* animation/model selection itself is what differs. Diagnosis only. */
{
extern int d243mProbeActive(void);
extern int d243mGetFrameCounter(void);
if (d243mProbeActive())
{
osSyncPrintf("D243M: prebefore frame=%d chr=%p pos=%.1f,%.1f,%.1f\n",
d243mGetFrameCounter(), (void *) chr,
(double) prop->pos.f[0], (double) prop->pos.f[1], (double) prop->pos.f[2]);
}
}
#endif
if ((!(chr->chrflags & CHRFLAG_HIDDEN)) || (chr->chrflags & CHRFLAG_00040000))
@@ -2522,6 +2551,47 @@ s32 chrTick(PropRecord *prop)
}
else
{
#ifdef PORT
/* D243 M-162/M-163: corrected decisive experiment. M-161 restored
* prop->pos AFTER this chrTick() call returned -- but render_pos/
* field_488.pos are computed INSIDE this same call (subcalcmatrices,
* further down this function), from whatever prop->pos is at THAT
* point, so M-161 never actually prevented the stale value from
* being rendered -- its "no change" result was inconclusive, not a
* real falsification (see docs/dev/findings.md D243 M-162).
*
* Save pos right here, before the position-update dispatch below
* runs (chr.c:2541-2617, the ACT_PATROL/ACT_ANIM/ACT_STAND/else
* chain M-160 traced the overwrite to), and restore it at
* after_position_update -- before anything later in this tick
* (including the render-matrix computation) can see the cycled
* value.
*
* M-162 (Z-only) confirmed the mechanism -- Z stopped cycling and
* went smooth -- but the visible glitch was UNCHANGED, because this
* capture's chr (actiontype=3, different from M-160's capture) turned
* out to cycle Y instead of Z, and Y was left untouched. M-163: the
* whole vector, to see if suppressing every axis finally stops the
* visible shake. This DOES also freeze whatever legitimate motion
* exists on any axis (e.g. a real descent) -- acceptable for a
* decisive test, not for a fix.
*
* Gated on GE_D243X2 + the existing d243mProbeActive() cutscene-mode
* gate (POSEND/INTRO/SWIRL/FADESWIRL) -- g_CameraMode was verified
* POSEND for the entire abseil capture (GE_D243M data), so this is
* scoped correctly without needing a new g_CameraMode extern here.
*
* THIS IS A TEST, NOT A FIX. If it confirms the cycling stops: the
* real root cause is still open -- this is decomp-faithful game
* code, so find WHY it emits cycling values on PC before touching
* anything for real (rule #2's diagnosis-before-fix discipline).
* Revert this block once the experiment reports. */
extern int d243mProbeActive(void);
static int s_d243x2 = -1;
if (s_d243x2 < 0) { s_d243x2 = getenv("GE_D243X2") != NULL; }
d243x2Active = s_d243x2 && d243mProbeActive();
d243x2SavedPos = prop->pos;
#endif
if (((prop->type == PROP_TYPE_VIEWER) && (g_playerPointers[getPlayerPointerIndex(prop)]->cameramode == 1)) || (chr->chrflags & CHRFLAG_CULL_USING_HITBOX))
{
headSwitchVisible = 1;
@@ -2617,6 +2687,56 @@ s32 chrTick(PropRecord *prop)
}
after_position_update:
#ifdef PORT
/* D243 M-163: apply the save/restore decided above, BEFORE anything else
* in this tick (including the D243M: onscreen log right below, so it
* reports what will actually be used downstream -- and before the
* render-matrix computation further down this function) can see the
* cycled position. Whole vector this pass (M-162 was Z-only; see the
* comment at the save site for why). */
{
d243x2CycledPos = prop->pos;
if (d243x2Active) { prop->pos = d243x2SavedPos; }
}
/* D243 M-159/M-160: does render_pos only refresh on ticks that pass the
* on-screen visibility gate? Log the gate's result + the live prop
* position every chrTick call while a scripted cutscene camera is
* active (reuses bondview2.c's GE_D243M accessors -- gate already folds
* in POSEND/INTRO/SWIRL/FADESWIRL, so this is silent during FPS play).
* If the freeze/teleport boundaries line up with headSwitchVisible
* flipping 0->1, this confirms the mechanism named in findings.md D243
* M-159. M-162/M-163: actiontype= and anim= (current attached animation
* pointer) added -- M-160 flagged "which of chrTick's branches fires"
* as the open next step toward the actual root cause once the
* suppression experiment reports; x2active=/x2raw= show whether the
* suppression ran this tick and what raw position it overrode, so a
* positive result can be read straight from this log instead of by eye.
* Diagnosis only; zero behaviour change when GE_D243M/GE_D243X2 unset. */
{
extern int d243mProbeActive(void);
extern int d243mGetFrameCounter(void);
if (d243mProbeActive())
{
/* M-166: aidx=/animframe= reuse the GE_D193A reverse-resolve
* technique (d193aAnimIndex, chr.c:67) + model->animframe1 (the
* actual current playback phase, bondtypes.h:1578) -- named per
* M-165/M-166's open question: is the visible chr's animation
* IDENTITY deterministic but its PHASE inherited from whatever
* gameplay preceded the cutscene (not guaranteed identical
* cross-platform)? A live capture with this field can now
* answer that directly instead of needing another round-trip. */
osSyncPrintf("D243M: onscreen frame=%d chr=%p vis=%d pos=%.1f,%.1f,%.1f "
"actiontype=%d anim=%p aidx=%d animframe=%.2f x2active=%d x2raw=%.1f,%.1f,%.1f\n",
d243mGetFrameCounter(), (void *) chr, (int) headSwitchVisible,
(double) prop->pos.f[0], (double) prop->pos.f[1], (double) prop->pos.f[2],
(int) chr->actiontype, (void *) model->anim,
d193aAnimIndex((void *) model->anim), (double) model->animframe1,
(int) d243x2Active,
(double) d243x2CycledPos.f[0], (double) d243x2CycledPos.f[1], (double) d243x2CycledPos.f[2]);
}
}
#endif
if (((chr->actiontype != ACT_STAND) || (model->anim2 != NULL)) || (prop->type == PROP_TYPE_VIEWER))
{
chr->hidden |= CHRHIDDEN_BACKGROUND_AI;
@@ -2680,6 +2800,86 @@ after_position_update:
subcalcmatrices(&renderdata, model);
#ifdef PORT
/* D243 M-168 (rewritten after two live crashes -- see findings.md
* D243 M-168 for the full postmortem): same technique as M-167
* (field_488.pos freeze), aimed at the actual draw-path consumer of
* the cycling skeleton state -- render_pos itself. M-167 only froze
* the camera's copy (field_488.pos) and fixed the shake, but the
* user still saw Bond's own rendered body glitch/duplicate --
* expected, since the draw call reads render_pos directly
* (objecthandler.c's drawjointlist, via gSPSegment), a path M-167
* never touched.
*
* The first two attempts tried to SKIP this subcalcmatrices() call
* entirely once a model "looked already validated" -- both crashed,
* because a model POINTER can be reused by the allocator for a
* genuinely new logical model instance (a new CREATE at the same
* freed address), and there is no way to tell "is this the same
* instance" from the pointer value or from render_pos's own content
* (M-154 already learned the render_pos sentinel-guessing lesson
* once; this is the same trap from a different angle). Skipping the
* call left a brand-new instance's render_pos at its fresh,
* uninitialized -1 tombstone, and sub_GAME_7F06C768() (model.c:459,
* called moments later at chr.c:2822 to compute zDepth) dereferenced
* it and crashed at fault address -1.
*
* This version NEVER skips the call -- subcalcmatrices() always
* runs above, unconditionally, so render_pos is always left valid.
* The freeze is purely a CONTENTS overwrite on top of that
* guaranteed-valid buffer: cache the buffer for a couple of ticks
* after the gate first engages (letting the pose settle from
* whatever it was mid-transition), then hold it steady by copying
* the cached contents back over each subsequent tick's freshly
* (and validly) computed buffer. Gated on GE_D243X4 +
* d243mProbeActive(); the active/inactive edge resets the warm-up
* counter so a later, different model under the same gate re-warms
* instead of inheriting a stale snapshot. Capped at 128 matrices
* (generous; GE character models are well under this). THIS IS A
* TEST, NOT A FIX -- revert once it reports. */
{
extern int d243mProbeActive(void);
extern u32 d243GetTeleportEpoch(void);
static int s_d243x4 = -1;
static int s_d243x4WasActive = 0;
static int s_d243x4WarmTicks = 0;
static RenderPosView s_d243x4Snap[128];
static s16 s_d243x4Count = 0;
static u32 s_d243x4LastEpoch = 0;
if (s_d243x4 < 0) { s_d243x4 = getenv("GE_D243X4") != NULL; }
{
int active = s_d243x4 && d243mProbeActive();
u32 epoch = d243GetTeleportEpoch();
if (active && !s_d243x4WasActive) { s_d243x4WarmTicks = 0; s_d243x4LastEpoch = epoch; }
/* M-169: a legitimate shot-change teleport fired since the
* last snapshot -- re-warm so the freeze re-baselines to the
* post-teleport pose instead of holding the pre-teleport
* one (which produced the "camera looks the wrong way for
* later shots" incompleteness noted in the M-168 postmortem). */
if (active && (epoch != s_d243x4LastEpoch)) { s_d243x4WarmTicks = 0; s_d243x4LastEpoch = epoch; }
s_d243x4WasActive = active;
if (active && (model->render_pos != NULL) && (model->obj != NULL))
{
s16 n = model->obj->numMatrices;
if (n > 0 && n <= 128)
{
s32 i;
if (s_d243x4WarmTicks < 2)
{
for (i = 0; i < n; i++) { s_d243x4Snap[i] = model->render_pos[i]; }
s_d243x4Count = n;
s_d243x4WarmTicks++;
}
else if (n == s_d243x4Count)
{
for (i = 0; i < n; i++) { model->render_pos[i] = s_d243x4Snap[i]; }
}
}
}
}
}
#endif
g_ModelJointPositionedFunc = NULL;
modelSetDistanceScale(1.0f);
+133
View File
@@ -600,6 +600,31 @@ void chrlvPerformAnimationForActor(ChrRecord *self, s32 animID, s32 startframe,
}
chrStopFiring(self);
#ifdef PORT
/* D243 M-187: log animation-restart entry state before modelSetAnimation()
* touches anything, to test whether unkb0/unkb4/unkac/animrate (which
* modelSetAnimation2, model.c:2720, never resets on a scripted restart --
* only modelSetAnimPlaySpeed touches them) are already stale/nonzero at
* this moment -- the open question from findings.md's D243 entry (M-178
* onward). Env-gated via bondview2.c's d243mProbeActive() (folds in the
* POSEND/INTRO/SWIRL/FADESWIRL camera-mode gate already used by the rest
* of the D243M probe family); zero cost/behaviour change when unset. */
{
extern int d243mProbeActive(void);
extern int d243mGetFrameCounter(void);
if (d243mProbeActive() && self->model != NULL)
{
osSyncPrintf("D243M: animrestart frame=%d chr=%d animID=%d startframe=%d "
"unkb0=%.3f unkb4=%.3f unkac=%.3f animrate=%.3f playspeed=%.3f\n",
d243mGetFrameCounter(), self->chrnum, (int) animID, (int) startframe,
(double) self->model->unkb0, (double) self->model->unkb4,
(double) self->model->unkac, (double) self->model->animrate,
(double) self->model->playspeed);
}
}
#endif
modelSetAnimation(self->model, (void *)animation_table_ptrs1[animID], (bitfield & ANIM_MIRROR) != 0, startframef, phi_f0, (f32)interpol_time60);
if (endframe >= 0)
@@ -9969,6 +9994,35 @@ bool chrHasFlags2ById(ChrRecord *self, s32 chrNum, u8 flags2)
*/
void chrSetStageFlags(ChrRecord *self, s32 arg1)
{
#ifdef PORT
/* D318-followup (diagnosis only, GE_OBJT=1): attribute writes to the LOW
* bits of objectiveregisters1 (the Facility execution derail triggers:
* 0x04 gas/combat, 0x20 surrender/monologue, 0x40 flee -- none of which
* the level setup script ever sets; only character-action scripts do).
* Logs the writing chr + its AI position so the writer's script state is
* correlatable against a simultaneous D318T capture. Read-only, capped,
* no behavior change; N64 build unaffected. */
extern char *getenv(const char *);
extern s32 chraiGetAIListID(AIRecord *AIList, bool *isGlobalAIList); /* chrai.c */
static int s_objt = -1;
static int s_objtn = 0;
int objtlog;
s32 aid;
bool aig;
if (s_objt < 0) { s_objt = getenv("GE_OBJT") != NULL; }
objtlog = s_objt && (arg1 & 0x0000FFFF) && (s_objtn < 500);
if (objtlog)
{
aid = self->ailist ? chraiGetAIListID(self->ailist, &aig) : -1;
osSyncPrintf("OBJT: t=%d SET mask=0x%08x by chr %d (act=%d off=%d aiid=0x%04x%s ailist=%p) reg=0x%08x\n",
(int)g_GlobalTimer, (unsigned)arg1, (int)self->chrnum,
(int)self->actiontype, (int)self->aioffset, (unsigned)aid,
aig ? "G" : "", (void *)self->ailist,
(unsigned)objectiveregisters1);
s_objtn++;
}
#endif
objectiveregisters1 |= arg1;
}
@@ -9978,6 +10032,28 @@ void chrSetStageFlags(ChrRecord *self, s32 arg1)
*/
void chrUnsetStageFlags(ChrRecord *self, u32 flags)
{
#ifdef PORT
extern char *getenv(const char *);
extern s32 chraiGetAIListID(AIRecord *AIList, bool *isGlobalAIList); /* chrai.c */
static int s_objtu = -1;
static int s_objtun = 0;
int objtulog;
s32 aid;
bool aig;
if (s_objtu < 0) { s_objtu = getenv("GE_OBJT") != NULL; }
objtulog = s_objtu && (flags & 0x0000FFFF) && (s_objtun < 500);
if (objtulog)
{
aid = self->ailist ? chraiGetAIListID(self->ailist, &aig) : -1;
osSyncPrintf("OBJT: t=%d UNS mask=0x%08x by chr %d (act=%d off=%d aiid=0x%04x%s ailist=%p) reg=0x%08x\n",
(int)g_GlobalTimer, (unsigned)flags, (int)self->chrnum,
(int)self->actiontype, (int)self->aioffset, (unsigned)aid,
aig ? "G" : "", (void *)self->ailist,
(unsigned)objectiveregisters1);
s_objtun++;
}
#endif
objectiveregisters1 = ~flags & objectiveregisters1; //shorthand does not match
}
@@ -10091,6 +10167,30 @@ bool chrGoToBond(ChrRecord *self, SPEED speed)
{
PropRecord *bondprop;
#ifdef PORT
/* D309 (diagnosis only, GE_D309=1): the Caverns->intro complete freeze
* spins in ai() on m_RunToBondPersistent the first time TRYRunToBond
* FAILS for a guard on that list (the 1-byte AI_PRINT record is then
* mis-sized by chraiitemsize's NUL scan -> phantom GotoNext(0) ->
* chraiGoToLabel returns 0 -> tight restart loop, see findings.md D309).
* The code+data are byte-matched ground truth, so the open question is
* WHICH failure branch fires and in what game state at Caverns' end --
* dead/shot gate, seen-count gate, or plot_course (whose three sub-
* conditions are logged here: own stan-path, Bond's stan-path, route).
* Read-only probe: chrlvStanPathRelated/waypointFindRoute are pure
* lookups. No behavior change; N64 build unaffected. */
extern char *getenv(const char *);
extern u8 m_RunToBondPersistent[];
extern s32 g_MainStageNum;
extern LEVELID bossGetStageNum(void);
static int s_d309 = -1;
static int s_d309n = 0;
int d309log;
if (s_d309 < 0) { s_d309 = getenv("GE_D309") != NULL; }
d309log = s_d309 && (self->ailist == (AIRecord *)m_RunToBondPersistent) && (s_d309n < 400);
#endif
if (chrIsNotDeadOrShot(self) && (g_SeenBondRecentlyGuardCount < 10))
{
bondprop = getCurrentPlayerProp();
@@ -10099,12 +10199,45 @@ bool chrGoToBond(ChrRecord *self, SPEED speed)
{
return TRUE;
}
#ifdef PORT
if (d309log)
{
waypoint *wpSelf = chrlvStanPathRelated(&self->prop->pos, self->prop->stan);
waypoint *wpBond = chrlvStanPathRelated(&bondprop->pos, bondprop->stan);
s32 route = -1;
if (wpSelf && wpBond)
{
waypoint sp44[MAX_CHRWAYPOINTS];
route = waypointFindRoute(wpSelf, wpBond, sp44, MAX_CHRWAYPOINTS);
}
osSyncPrintf("D309: FAIL-plot chr=%d act=%d flags2=0x%x stage=%d curload=%d mainstage=%d cammode=%d stoptime=%d seen=%d bondprop=%p bondstan=%p wpSelf=%p wpBond=%p route=%d\n",
(int)self->chrnum, (int)self->actiontype, (int)self->chrflags & 0xffff,
(int)bossGetStageNum(), (int)lvlGetCurrentStageToLoad(), (int)g_MainStageNum,
(int)g_CameraMode, (int)stop_time_flag, (int)g_SeenBondRecentlyGuardCount,
(void *)bondprop, (void *)bondprop->stan, (void *)wpSelf, (void *)wpBond, (int)route);
s_d309n++;
}
#endif
}
#ifdef PORT
else if (d309log)
{
osSyncPrintf("D309: FAIL-gate chr=%d act=%d deadOrShot=%d seen=%d stage=%d curload=%d mainstage=%d cammode=%d stoptime=%d\n",
(int)self->chrnum, (int)self->actiontype, (int)(!chrIsNotDeadOrShot(self)),
(int)g_SeenBondRecentlyGuardCount,
(int)bossGetStageNum(), (int)lvlGetCurrentStageToLoad(), (int)g_MainStageNum,
(int)g_CameraMode, (int)stop_time_flag);
s_d309n++;
}
#endif
return FALSE;
}
/**
* Address 0x7F03350C0.
*/
+154 -1
View File
@@ -681,6 +681,13 @@ s32 chraiitemsize(u8 *AIList, s32 offset)
return sizeof(AiObjectRocketLaunchRecord);
case AI_PRINT:
{
/* D310: for GLOBAL lists the PRINT record is really 1 byte (the
* macro discards its string at list-build time) and this NUL-scan
* mis-skips into later records -- see d310ItemSize() below, which
* applies the 1-byte size on PORT for global lists only. LOCAL
* (level-setup) lists embed a NUL-terminated string after the
* command byte, so the scan is correct for them and must stay.
* This function itself is left byte-identical to the decomp. */
s32 pos = offset + 1;
while (AIList[pos] != 0)
{
@@ -696,6 +703,37 @@ s32 chraiitemsize(u8 *AIList, s32 offset)
}
}
#ifdef PORT
/* D310 / RULE-2-SIGNOFF (2026-09-18, grant: "I want to proceed with your
* implementation plan here"): AI_PRINT records differ by list origin.
* Global lists (g_GlobalAILists, macro-built in chraidata.c) discard the
* PRINT string at build time -> the record is 1 byte and chraiitemsize's
* NUL-scan mis-skips ~20 bytes into later records: m_RunToBondPersistent's
* "no go!" lands on 00 00 (phantom GotoNext(0) -> label-not-found -> PC
* restart -> infinite spin, D309's Caverns->intro freeze) and its "wait"
* lands on arg bytes reading Label+EndList (ai() returns without saving the
* offset -> Stop re-issued every tick -> guards stand still forever).
* Level-local lists (g_CurrentSetup.ailists) embed a NUL-terminated string
* after the command byte (e.g. AD 'D' '\n' 00 in Caverns' Trevelyan list),
* so the NUL-scan is CORRECT for them -- sizing those at 1 lands mid-string
* and corrupts execution (Trevelyan ran circles with glitched anims when a
* uniform 1-byte size was tried, live-observed 2026-09-18). Use this
* instead of chraiitemsize at AI record-walk sites; it applies the 1-byte
* size only for lists confirmed global via chraiGetAIListID. Full-list
* decode of m_RunToBondPersistent with the fix walks clean to EndList and
* matches chraidata.c line-for-line. Same-engine precedent: PD removed
* PRINT and reassigned slot 0xAD (aiChrCopyProperties). See findings D310. */
static s32 d310ItemSize(u8 *AIList, s32 offset, bool isGlobalAIList)
{
if (isGlobalAIList && AIList[offset] == AI_PRINT)
{
return 1;
}
return chraiitemsize(AIList, offset);
}
#endif
/**
* Get ID of AIList
* @param AIList: Ailist to get ID of
@@ -739,8 +777,13 @@ s32 chraiGoToLabel(AIRecord *AIList, s32 Offset, u8 LabelNum)
{
s32 listID;
char *debAIListTypeString;
bool isGlobalAIList;
bool isGlobalAIList = FALSE;
#ifdef PORT
/* D310: the record walk below must size PRINT per list origin (global =
* 1 byte, local = embedded string); resolve it once up front. */
(void)chraiGetAIListID(AIList, &isGlobalAIList);
#endif
for (;;)
{
if (AIList[Offset].cmd == AI_Label)
@@ -753,6 +796,26 @@ s32 chraiGoToLabel(AIRecord *AIList, s32 Offset, u8 LabelNum)
}
else if (AIList[Offset].cmd == AI_EndList)
{
#ifdef PORT
/* D309 (diagnosis only, GE_D309=1): label scan ran off the end of
* the list -- the decomp's own comment says the restart-PC-to-0
* return "causes infinite loop outside of debug". This is the
* spin signature; logging it catches any AI list (not just
* m_RunToBondPersistent) whose mis-sized skip produced a phantom
* GotoNext to a nonexistent label. */
{
static int s_d309g = -1;
static int s_d309gn = 0;
if (s_d309g < 0) { s_d309g = getenv("GE_D309") != NULL; }
if (s_d309g && s_d309gn < 400)
{
osSyncPrintf("D309: LABEL-NOT-FOUND list=%p off=%d label=%d\n",
(void *)AIList, (int)Offset, (int)LabelNum);
s_d309gn++;
}
}
#endif
// restart ai list PC if next label not found - causes infinite loop outside of debug
listID = chraiGetAIListID(AIList, &isGlobalAIList);
#ifdef DEBUG
@@ -769,7 +832,11 @@ s32 chraiGoToLabel(AIRecord *AIList, s32 Offset, u8 LabelNum)
return 0;
}
#ifdef PORT
Offset += d310ItemSize((u8 *)AIList, Offset, isGlobalAIList);
#else
Offset += chraiitemsize(AIList, Offset);
#endif
}
}
@@ -1079,6 +1146,44 @@ void ai(PropDefHeaderRecord *Entityp, PROP_TYPE EntityType)
endframe = -1;
}
#ifdef PORT
/* D243 M-170: which chr does a scripted guard_play_animation
* actually land on during the Dam abseil (anim_id 0xb100)?
* M-145 (static-only) claimed this targets an empty,
* model-less "false GUARD" driver chr -- but that theory's
* own stated binding rule (ailist ID >= 0x1000) doesn't
* apply to ai_17's registered ID (0x412), and the user's
* live memory of the real N64 cutscene has Bond visibly
* jumping/falling with the camera tracking him, which is
* hard to reconcile with "no model to animate." One-shot
* empirical check: log ChrEntityp's chrnum + whether it
* has a model + whether it IS the visible player chr,
* every time this specific anim_id fires. Env-gated
* (GE_D243M, already cached elsewhere), essentially free
* when unset. Diagnosis only. */
if (getenv("GE_D243M") != NULL)
{
/* M-170 correction: the first version of this probe
* filtered on anim_id==0xb100 and got ZERO hits
* across a full Dam-abseil capture that otherwise
* clearly exercised ai_17 (3 successful teleports,
* GE_D243X3/X4's epoch counter went 0->3). Rather
* than guess why, log every AI_PlayAnimation call
* unfiltered so the next capture shows what anim_id
* values (if any) actually arrive at this case
* during the cutscene -- confirms/refutes whether
* the filter itself was wrong (byte-order, wrong
* literal) or the case is never reached at all for
* this cutscene's actor. */
osSyncPrintf("D243M: playanim anim=0x%x chr=%p chrnum=%d model=%p "
"isPlayerChr=%d\n",
(unsigned) anim_id, (void *) ChrEntityp,
ChrEntityp ? (int) ChrEntityp->chrnum : -1,
ChrEntityp ? (void *) ChrEntityp->model : NULL,
(int) (ChrEntityp && g_CurrentPlayer && g_CurrentPlayer->prop
&& (void *) ChrEntityp == (void *) g_CurrentPlayer->prop->chr));
}
#endif
if (ChrEntityp)
{
check_if_able_to_then_perform_animation(ChrEntityp, anim_id, startframe, endframe, ai->BITFIELD, ai->INTERPOL_TIME60);
@@ -3300,6 +3405,36 @@ void ai(PropDefHeaderRecord *Entityp, PROP_TYPE EntityType)
}
case AI_PRINT:
{
#ifdef PORT
bool d310global = FALSE;
/* D310: size the PRINT per list origin (see d310ItemSize). */
(void)chraiGetAIListID(AiListp, &d310global);
/* D309 (diagnosis only, GE_D309=1): log each step on a
* PRINT record with the size actually used and the
* command byte we land on. A repeating line (same
* chr/list/off) means ai() re-executes the same path every
* tick without advancing -- the standing-still signature;
* landcmd==0x00 (AI_GotoNext) is the D309 spin variant.
* Capped so a stuck loop can't flood stderr. */
{
static int s_d309p = -1;
static int s_d309pn = 0;
if (s_d309p < 0) { s_d309p = getenv("GE_D309") != NULL; }
if (s_d309p && s_d309pn < 400)
{
s32 sz = d310ItemSize(AiListp, Offset, d310global);
osSyncPrintf("D309: PRINT chr=%d list=%p off=%d size=%d landcmd=0x%02x global=%d\n",
ChrEntityp ? (int)ChrEntityp->chrnum : -1,
(void *)AiListp, (int)Offset, (int)sz,
(int)(AiListp[Offset + sz].cmd & 0xff),
(int)d310global);
s_d309pn++;
}
}
#endif
#ifdef ENABLE_LOG
AIRecord *ai = AiListp + Offset;
osSyncPrintf("AI_PRINT: %s\n", ai->val);
@@ -3314,7 +3449,11 @@ void ai(PropDefHeaderRecord *Entityp, PROP_TYPE EntityType)
}
#endif
#endif
#ifdef PORT
Offset += d310ItemSize(AiListp, Offset, d310global);
#else
Offset += chraiitemsize(AiListp, Offset);
#endif
break;
}
case AI_MyTimerStart:
@@ -4104,6 +4243,20 @@ void ai(PropDefHeaderRecord *Entityp, PROP_TYPE EntityType)
chr->chrflags = chr->chrflags | CHRFLAG_INIT;
setsubroty(chr->model, FacingDirection);
setsuboffset(chr->model, &pos);
#ifdef PORT
/* D243 M-169: signal the real shot-change moment
* to the X3/X4 render-pos freeze experiments --
* see the d243NotifyTeleport() definition
* (bondview2.c) for the full rationale. This is
* the exact decomp call (setsuboffset, line
* above, unmodified) that a legitimate cutscene
* teleport already performs; the notify is a
* side-channel observer only, no logic change. */
{
extern void d243NotifyTeleport(void);
d243NotifyTeleport();
}
#endif
chrDetectRooms(chr);
if (chr->prop == g_CurrentPlayer->prop)
{
+12 -1
View File
@@ -993,7 +993,18 @@ Gfx *explosionRenderPart(struct ExplosionPart *arg0, Gfx *gdl, struct coord3d *c
vertices[1] = spA0;
vertices[2] = spA0;
vertices[3] = spA0;
vertices[4] = spA0;
/* D252 (2026-09-18): dropped a stray vertices[4] = spA0 write here -- this
* function only allocates 4 Vtx slots (dynAllocateVertices(4) above) and
* only ever uses 4 (gSPVertex(..., 4, 0) / gSP2Triangles(0,1,2,...,2,3,0)
* below), so vertices[4] was never part of this quad's own draw -- it was
* a genuine one-Vtx-sized out-of-bounds write into whatever the next
* dynAllocate* call in the same frame claims from the shared bump-
* allocated pool (dyn.c's g_GfxMemPos), corrupting unrelated particle/
* matrix/light data. Removing it changes zero bytes of what this
* function itself draws. Prime suspect for D252's rainbow-particle
* corruption; see docs/dev/findings.md D252 for the full mechanism and
* why this required the project owner's explicit go-ahead to touch
* (decomp-verified original code, not an ABI/pointer-width artifact). */
sp8C.f[0] = sp9C->m[0][0] * sp54;
sp8C.f[1] = sp9C->m[0][1] * sp54;
+28
View File
@@ -392,6 +392,34 @@ bool fileGetIsCheatUnlocked(save_data *save, s32 cheat)
{
s32 bits;
#ifdef PORT
/* D225 (dev-tooling QoL): both mission-select gating (file.c) and the
* cheat-menu availability check (front.c frontCheckIfCheatIsUnlocked)
* route through this single, side-effect-free bitfield read -- a debug
* env toggle here unlocks both in one place. Default off, no save-file
* mutation (query-time override only, cannot corrupt or leak into a
* shipped save). */
{
static int s_unlockAll = -1;
if (s_unlockAll < 0) s_unlockAll = getenv("GE_DEBUG_UNLOCKALL") != NULL;
if (s_unlockAll) return TRUE;
}
/* D301 (github #87): callers (front.c mode-select cheat rows, file.c
* briefing cheat check) pass fileGetSaveForFoldernum(selected_folder_num)
* straight through with no NULL check. On real N64 that lookup can
* never miss for the currently-selected folder; on PC it can, whenever
* the folder's save slot fails fileValidateSaves()'s CRC check or was
* never populated for the active folder (D299's SkipIntro case was one
* such route; this guards every route, not just that one). Returning
* "not unlocked" here is the same safe default the CRC-fail path
* already produces for other cheat state. No N64 behavior change --
* this branch is unreachable on real hardware. */
if (save == NULL)
{
return FALSE;
}
#endif
if (cheat >= 0 && cheat < CHEAT_INPUT_BUFFER_SIZE)
{
bits = save->unlocked_cheats_1 | save->unlocked_cheats_3 << 0x18 | save->unlocked_cheats_3 << 0x10 | save->unlocked_cheats_2 << 8;
+93
View File
@@ -2613,8 +2613,20 @@ Gfx *constructor_menu05_fileselect(Gfx *DL)
for (foldernum = FOLDER1; foldernum < MAX_FOLDER_COUNT; foldernum++)
{
#ifdef PORT
// D295/M-148: decomp sizes are too small for what this code writes.
// difficultytext[4]: "00 Agent\n" is 9 bytes incl. NUL, "Secret Agent\n"
// 14 — the overflow ran into textpos.p[0], and a y>=0x80 re-store injected
// a high-bit byte into textRender -> JPN glyph path -> NULL deref (issue #87).
// missiontext[18]: verified worst case is ~14 ("Mission 9.iv\n"); widened to
// the same size defensively per D8 policy, not from a verified worst-case.
// Pure local stack scratch, zero ABI role; N64 build keeps decomp sizes.
char difficultytext[32]; // spD0 (decomp: [4])
char missiontext[32]; // spBC (decomp: [18]): "Mission <chapter>.<part>\n"
#else
char difficultytext[4]; // spD0
char missiontext[18]; // spBC: "Mission <chapter>.<part>\n"
#endif
s32 padding3;
struct coord3d * folderworldpos;
@@ -8578,10 +8590,78 @@ MENU get_currentmenu(void)
}
#ifdef PORT
/* D243: fast3d's D146 catch-all ("unknown GBI opcode ... ending DL") fires a
* freed/uninitialized-memory-pattern read burst ~1600-3900 frames *after*
* bossReturnTitleStage()/the Dam abseil cutscene's camera_switch calls, not
* during them (M-109 re-analysis) -- i.e. tens of seconds into whatever
* front-end screen(s) the mission-complete -> briefing/file-select chain
* passes through. This probe does not hook bossReturnTitleStage() directly
* (out of this investigation's file scope, boss.c) -- instead it logs every
* MENU_* state transition this state machine (menu_init(), called once per
* front-end tick from lv.c) makes, tagged with a local frame counter, so a
* GE_D146 burst's frame number can be cross-referenced against whichever
* MENU_* was active at that point. NOTE: this counter is local to front.c's
* per-tick calls (menu_init() runs once per LEVELID_TITLE tick) -- it is not
* necessarily numerically identical to port/src/libultra.c's internal
* g_framesRendered (that counter is `static`, no accessor exists, and it is
* out of this investigation's file scope to add one) -- cross-reference by
* elapsed count/offset from bossReturnTitleStage's own GE_D160 trace line,
* not by assuming exact frame-number equality between the two logs. */
static int g_d243FrameCounter = 0;
static const char *d243MenuName(MENU m)
{
switch (m) {
case MENU_INVALID: return "MENU_INVALID";
case MENU_LEGAL_SCREEN: return "MENU_LEGAL_SCREEN";
case MENU_SWITCH_SCREENS: return "MENU_SWITCH_SCREENS";
case MENU_NINTENDO_LOGO: return "MENU_NINTENDO_LOGO";
case MENU_RAREWARE_LOGO: return "MENU_RAREWARE_LOGO";
case MENU_EYE_INTRO: return "MENU_EYE_INTRO";
case MENU_GOLDENEYE_LOGO: return "MENU_GOLDENEYE_LOGO";
case MENU_FILE_SELECT: return "MENU_FILE_SELECT";
case MENU_MODE_SELECT: return "MENU_MODE_SELECT";
case MENU_MISSION_SELECT: return "MENU_MISSION_SELECT";
case MENU_DIFFICULTY: return "MENU_DIFFICULTY";
case MENU_007_OPTIONS: return "MENU_007_OPTIONS";
case MENU_BRIEFING: return "MENU_BRIEFING";
case MENU_RUN_STAGE: return "MENU_RUN_STAGE";
case MENU_MISSION_FAILED: return "MENU_MISSION_FAILED";
case MENU_MISSION_COMPLETE: return "MENU_MISSION_COMPLETE";
case MENU_MP_OPTIONS: return "MENU_MP_OPTIONS";
case MENU_MP_SCENARIO_SELECT: return "MENU_MP_SCENARIO_SELECT";
case MENU_MP_CHAR_SELECT: return "MENU_MP_CHAR_SELECT";
case MENU_MP_TEAMS: return "MENU_MP_TEAMS";
case MENU_MP_HANDICAP: return "MENU_MP_HANDICAP";
case MENU_MP_CONTROL_STYLE: return "MENU_MP_CONTROL_STYLE";
case MENU_MP_STAGE_SELECT: return "MENU_MP_STAGE_SELECT";
case MENU_CHEAT: return "MENU_CHEAT";
case MENU_NO_CONTROLLERS: return "MENU_NO_CONTROLLERS";
case MENU_DISPLAY_CAST: return "MENU_DISPLAY_CAST";
case MENU_SPECTRUM_EMU: return "MENU_SPECTRUM_EMU";
default: return "MENU_<unknown>";
}
}
#endif
void menu_init(void)
{
s32 var_v0;
#ifdef PORT
/* D243: one call per front-end tick (see g_d243FrameCounter comment
* above for how this correlates against the D146/GE_D160 logs).
* D250: menu_init() runs ~once per frame during front-end states --
* cache the getenv() rather than re-checking it every call. */
static int s_d243 = -1;
if (s_d243 < 0) s_d243 = getenv("GE_D243") != NULL;
if (s_d243) {
g_d243FrameCounter++;
}
#endif
if (current_menu == MENU_SWITCH_SCREENS)
{
if (spectrum_related_flag != 0)
@@ -8671,6 +8751,12 @@ void menu_init(void)
{
current_menu = MENU_SWITCH_SCREENS;
reset_menutimer();
#ifdef PORT
if (s_d243) {
fprintf(stderr, "D243: frame=%d -> MENU_SWITCH_SCREENS (pending=%s)\n",
g_d243FrameCounter, d243MenuName(menu_update));
}
#endif
}
}
@@ -8679,6 +8765,13 @@ void menu_init(void)
current_menu = maybe_prev_menu;
maybe_prev_menu = MENU_INVALID;
#ifdef PORT
if (s_d243) {
fprintf(stderr, "D243: frame=%d -> %s\n",
g_d243FrameCounter, d243MenuName(current_menu));
}
#endif
switch(current_menu) {
case MENU_LEGAL_SCREEN: init_menu00_legalscreen(); break;
case MENU_NINTENDO_LOGO: init_menu01_nintendo(); break;
+33
View File
@@ -635,16 +635,49 @@ void bullet_spark_render(s_bullet_spark *thing, Gfx *gdlarg, s32 zbufferMode)
return;
}
#ifdef PORT
/* D219/D3x: the same class of bug as the cn[] fix below, one step
* earlier -- D_80040980 is declared (correctly, per its actual N64 use
* as a zero-initialized template) as a plain u32, but this line reread
* it as a full Vtx (16 bytes) to get a "blank" starting vertex. Every
* field this sets (ob/tc/cn) is unconditionally overwritten later in
* this function; only v.flag survives from here, and D_80040980's own
* initializer (=0) shows the N64-side intent was a zeroed vertex. Zero
* the local directly instead of reading 12 bytes past a 4-byte global
* (harmless in practice so far, but real UB the PC compiler already
* flags with -Warray-bounds). */
memset(&vtx, 0, sizeof(vtx));
#else
vtx = *((Vtx *) (&D_80040980));
#endif
mtx = currentPlayerGetViewToWorldMtxf();
gdl = *((Gfx **) gdlarg);
vertices = dynAllocateVertices(4);
room = thing->unk06;
roompos = getRoomPositionByIndex(room);
#ifdef PORT
/* D219/D3x: on 32-bit N64, s_bullet_spark's void* unk0C field is 4 bytes,
* keeping unk28/29/2A/2B (the spark's stored RGBA colour, set from
* g_BulletSparkColors[] in bullet_sparks_init) at raw byte offset 0x28 --
* this function's original hardcoded ((u8*)thing)[0x28..0x2b] read relied
* on that. On 64-bit PC the pointer widens to 8 bytes (plus alignment
* padding before it), pushing the real compiler-computed offset of
* unk28 to 0x30 -- the old hardcoded 0x28 read instead lands inside the
* unk1c/unk20 f32 fields (the spark's random cosf/sinf-derived velocity
* components), reinterpreting arbitrary float bytes as the vertex colour.
* That is the root cause of the "rainbow" bullet-spark colour bug
* (D219): read the correctly-named/typed fields instead, which the
* compiler places at the right offset on any pointer width. */
vtx.v.cn[0] = thing->unk28;
vtx.v.cn[1] = thing->unk29;
vtx.v.cn[2] = thing->unk2A;
vtx.v.cn[3] = thing->unk2B;
#else
vtx.v.cn[0] = ((u8 *) thing)[0x28];
vtx.v.cn[1] = ((u8 *) thing)[0x29];
vtx.v.cn[2] = ((u8 *) thing)[0x2a];
vtx.v.cn[3] = ((u8 *) thing)[0x2b];
#endif
frame = (s32) (((f32) thing->age) * (*(&thing->unk08)));
x = *((f32 *) (&thing->unk10));
+14
View File
@@ -1721,7 +1721,21 @@ void gunRenderFirstPersonGunModels(Gfx **gdlptr)
Gfx *set_enviro_fog_for_items_in_solo_watch_menu(Gfx *gdl, ITEM_IDS itemid, Mtxf *mtx, s32 arg3, s32 arg4)
{
#ifdef PORT
/* D264 (ABI/layout class, cf. D140): on N64 this 64-byte copy spans two
* adjacent globals — D_80035D00 (zero) and D_80035D04 {1, 3, 0..} — i.e.
* the ModelRenderData template documented above D_80035D00 in gun.c
* (zbufferenabled=TRUE, flags=3, rest zero; CULLMODE_BOTH is 0). The PC
* link puts them in separate .bss/.data sections, so the read was all
* zeros: flags==0 gates every geometry node in subdraw() and the watch
* item preview emitted nothing. Use the explicit template (identical
* values to the N64 ROM bytes). */
ModelRenderData renderdata = {0};
renderdata.zbufferenabled = TRUE;
renderdata.flags = 3;
#else
ModelRenderData renderdata = *((ModelRenderData *) (&D_80035D00));
#endif
ModelHeader model;
u8 spb8[0x80];
s32 padb4;
+23 -1
View File
@@ -419,9 +419,23 @@ void lvlStageLoad(s32 stage)
* logo attract loop. Reuses the game's own post-intro route:
* every intro stage already jumps to MENU_FILE_SELECT once
* is_first_time_on_main_menu is FALSE (front.c). Port config
* only; default 0 = unchanged. */
* only; default 0 = unchanged.
*
* D3xx (found this session): the skipped legal screen's own
* init (front.c init_menu00_legalscreen) is what calls
* fileValidateSaves() -- the only place saves[] is ever
* populated from EEPROM. Skipping straight to file-select
* left saves[] all-zero, so fileGetSaveForFoldernum()
* returned NULL for every folder except whichever one a
* zeroed save_data happens to decode to, and file-select's
* unguarded fileGetIsCheatUnlocked() (file2.c:397) crashed
* dereferencing NULL for the other three. Call it here too
* so saves[] matches what the legal screen would have
* produced. */
extern s32 portSkipIntro;
if (portSkipIntro) {
extern void fileValidateSaves(void);
fileValidateSaves();
is_first_time_on_main_menu = FALSE;
prev_keypresses = TRUE;
maybe_is_in_menu = TRUE;
@@ -1035,6 +1049,14 @@ void lvlManageMpGame(void)
#endif
#endif
g_GlobalTimer += g_ClockTimer;
#ifdef PORT
{
extern void d318WatchdogTick(void);
extern void d318TimelineTick(void);
d318WatchdogTick(); /* D318 permanent port-side deadlock recovery (findings D318) */
d318TimelineTick(); /* D318T env-gated diagnostic timeline (GE_D318T=1) */
}
#endif
if ((g_CurrentStageToLoad != LEVELID_TITLE) && (D_80048394 == 0) && (g_ClockTimer > 0))
{
if (g_AppendCheatSinglePlayer != 0)
+211 -1
View File
@@ -2855,6 +2855,24 @@ void modelSetAnimationWithMerge(Model *model, ModelAnimation *modelAnimation, s3
void modelSetAnimation(Model *model, ModelAnimation *modelAnimation, s32 flip, f32 startframe, f32 speed, f32 merge) {
#ifdef PORT
/* D173 M-175: identify the actual caller that sets the intro puppet's
* first animation (M-174's firing_animation_groups probe got zero hits,
* ruling that path out -- rather than keep guessing which function it
* is, catch it at the one place all callers funnel through and resolve
* the caller with addr2line against the built exe afterward. Filters
* on g_CurrentPlayer->bodyModel specifically so normal chr/guard
* animation-sets (which fire constantly) don't spam the log. */
extern int d243mProbeActive(void);
extern int d243mGetFrameCounter(void);
extern struct player *g_CurrentPlayer;
if (d243mProbeActive() && g_CurrentPlayer && (model == g_CurrentPlayer->bodyModel))
{
osSyncPrintf("D243M: setanim_caller frame=%d model=%p startframe=%.1f speed=%.2f merge=%.1f retaddr=%p\n",
d243mGetFrameCounter(), (void *) model, (double) startframe,
(double) speed, (double) merge, __builtin_return_address(0));
}
#endif
modelCopyAnimForMerge(model, merge);
modelSetAnimation2(model, modelAnimation, flip, startframe, speed, merge);
}
@@ -2934,6 +2952,21 @@ void sub_GAME_7F06FE44(Model *model, s32 arg1) {
}
void modelSetAnimSpeed(Model *model, f32 anim_speed, f32 startframe) {
#ifdef PORT
/* D243 M-180: log all modelSetAnimSpeed calls during scripted camera modes
* to identify if unusual speed values are being set during cutscenes.
* Env-gated on GE_D243M (already cached elsewhere). */
extern int d243mProbeActive(void);
extern int d243mGetFrameCounter(void);
if (d243mProbeActive() && model != NULL) {
osSyncPrintf("D243M: setanimspeed frame=%d model=%p speed=%.3f startframe=%.1f "
"timespeed=%.1f oldspeed=%.3f newspeed=%.3f elapsespeed=%.1f\n",
d243mGetFrameCounter(), (void *) model, (double) anim_speed,
(double) startframe, (double) model->timespeed,
(double) model->speed, (double) model->newspeed,
(double) model->elapsespeed);
}
#endif
if (startframe > 0.0f) {
model->timespeed = startframe;
@@ -2976,6 +3009,20 @@ void sub_GAME_7F06FE90(Model *model, f32 arg1, f32 arg2)
}
void modelSetAnimPlaySpeed(Model *model, f32 animation_rate, f32 startframe) {
#ifdef PORT
/* D243 M-181: log all modelSetAnimPlaySpeed calls during scripted camera
* modes to identify if unusual playspeed values are being set during
* cutscenes. Env-gated on GE_D243M (already cached elsewhere). */
extern int d243mProbeActive(void);
extern int d243mGetFrameCounter(void);
if (d243mProbeActive() && model != NULL) {
osSyncPrintf("D243M: setanimplayspeed frame=%d model=%p rate=%.3f startframe=%.1f "
"unkb0=%.1f playspeed=%.3f animrate=%.3f\n",
d243mGetFrameCounter(), (void *) model, (double) animation_rate,
(double) startframe, (double) model->unkb0,
(double) model->playspeed, (double) model->animrate);
}
#endif
if (startframe > 0.0f) {
model->unkb0 = startframe;
model->animrate = animation_rate;
@@ -3178,7 +3225,26 @@ void modelSetAnimFrame2WithChrStuff(Model *model, f32 framea, f32 frameb, f32 fr
* NaN. */
if (!(frameb > -1.0e6f && frameb < 1.0e6f))
{
frameb = framea;
frameb = (framea > -1.0e6f && framea < 1.0e6f) ? framea : 0.0f;
}
/* D311 (RULE-2-SIGNOFF 2026-09-19, release review: user approved
* retention as the stability-maximizing option; classified as a
* defensive guard against non-finite state in the same class as
* the accepted D156 guards -- it only fires on poison values that
* indicate an already-broken state, and snaps to a valid frame
* rather than altering any legitimate animation):
* the original guard only validated frameb. If framea
* (model->animframe1) is ALREADY poisoned, "frameb = framea" left
* both garbage and floorFloatToInt saturated the two near-equal
* huge floats to far-apart ints -- observed live in Caverns:
* framea=frameb=-1.67e31 -> curframe=-966612173, endframe=INT_MAX,
* ~3.1e9 loop iterations = permanent hang (D156 guard had fired
* upstream but passed the poison through). Validate framea too;
* with both snapped to 0 the loop does ~0 iterations and the
* animframe1 rewrite at :3447 heals the stored state next tick. */
if (!(framea > -1.0e6f && framea < 1.0e6f))
{
framea = (frameb > -1.0e6f && frameb < 1.0e6f) ? frameb : 0.0f;
}
#endif
@@ -3475,6 +3541,28 @@ void modelTickAnim(struct Model *model, s32 numticks, s32 update_chrstuff)
f32 frame2;
f32 animlast;
#ifdef PORT
/* D243 M-182: log playspeed at the start of every tick during scripted
* camera modes to identify when and how it changes. Env-gated on
* GE_D243M (already cached elsewhere). */
extern int d243mProbeActive(void);
extern int d243mGetFrameCounter(void);
if (d243mProbeActive() && model != NULL)
{
/* D243 M-187: numticks added -- this is the D193 multi-tick
* catch-up burst size for this call; >1 here at the moment of a
* scripted restart is the leading PC-specific candidate for why
* unkb0/unkb4 could be stale in a way N64's single-tick-per-frame
* execution would never produce (see findings.md D243). */
osSyncPrintf("D243M: tickstart frame=%d model=%p numticks=%d playspeed=%.3f animrate=%.3f "
"unkb0=%.1f unkb4=%.1f unkac=%.3f endframe=%.1f\n",
d243mGetFrameCounter(), (void *) model, (int) numticks,
(double) model->playspeed, (double) model->animrate,
(double) model->unkb0, (double) model->unkb4,
(double) model->unkac, (double) model->endframe);
}
#endif
frame = model->animframe1;
frame2 = model->animframe2;
@@ -3552,8 +3640,86 @@ void modelTickAnim(struct Model *model, s32 numticks, s32 update_chrstuff)
}
speed = model->speed;
#ifdef PORT
/* D243 M-183: clamp playspeed to a sane range during scripted
* camera modes when it exceeds a threshold. M-186 correction:
* this is NOT a D100/D140 struct-punning/pointer-widening issue
* -- Model is accessed everywhere by named field, no raw-offset
* alias exists here. The actual mechanism (still not fully root-
* caused, see findings.md D243): modelSetAnimation2 (line ~2720,
* unmodified decomp) never resets unkb0/unkb4/unkac/animrate on
* a scripted animation restart; a restart landing while unkb0 is
* stale-nonzero divides by it using a mismatched unkac/animrate
* pair from the previous animation, producing huge playspeed
* spikes (~388-410, vs. a legitimate max of ~2.0).
* M-187: was mistakenly gated on d243mProbeActive() (requires
* GE_D243M=1), so it never fired for a real player -- only during
* a diagnostic capture. Fixed to gate on the camera-mode test
* alone (gameScriptedCameraActive()) so it's actually active by
* default. Logging stays separately gated on d243mProbeActive().
* M-189 (2026-09-18) found and fixed the ACTUAL root cause (a
* stale hardcoded 32-bit sizeof(Model) literal in bondview2.c's
* model-carving buffer, user-verified live): this clamp no longer
* fires in practice. It is retained as defense-in-depth against
* any future out-of-range playspeed reaching a scripted camera.
* RULE-2-SIGNOFF (2026-09-19, release review): ungated
* behavior-modifying guard in src/game, approved for retention
* by the user as the stability-maximizing option for v0.3.0;
* candidate for removal post-release once all cutscenes are
* re-verified clean without it (findings.md D243 / M-192).
* Threshold: 10.0 is way above any legitimate playspeed (normal
* is ~1.0, max observed in gameplay is ~2.0). */
extern int d243mProbeActive(void);
extern int gameScriptedCameraActive(void);
if (gameScriptedCameraActive() && playspeed > 10.0f)
{
if (d243mProbeActive())
{
osSyncPrintf("D243M: clamp playspeed %.3f → 1.0\n", (double) playspeed);
}
playspeed = 1.0f;
}
#endif
frame += playspeed * speed;
#ifdef PORT
/* M-184: log frame progression after advancement */
extern int d243mProbeActive(void);
if (d243mProbeActive() && model != NULL)
{
osSyncPrintf("D243M: postadv frame=%.2f endframe=%.1f\n",
(double) frame, (double) endframe);
}
#endif
#ifdef PORT
/* D243 M-179: log large per-tick animation advancements to identify
* whether rapid cutscene anim advancement is due to playspeed, speed,
* or numticks. Fires when a single tick advances >5 frames (normal
* is ~0.5-3). Env-gated on GE_D243M (already cached elsewhere). */
{
extern int d243mProbeActive(void);
extern int d243mGetFrameCounter(void);
if (d243mProbeActive())
{
f32 advancement = playspeed * speed;
if (advancement > 5.0f)
{
osSyncPrintf("D243M: biganim frame=%d model=%p adv=%.2f "
"playspeed=%.2f speed=%.2f numticks=%d "
"unkb0=%.1f unk88=%.1f timespeed=%.1f\n",
d243mGetFrameCounter(), (void *) model,
(double) advancement, (double) playspeed,
(double) speed, numticks,
(double) model->unkb0, (double) model->unk88,
(double) model->timespeed);
}
}
}
#endif
if (model->anim2 != NULL)
{
if (model->unk7c > 0.0f)
@@ -3583,6 +3749,37 @@ void modelTickAnim(struct Model *model, s32 numticks, s32 update_chrstuff)
animlast = model->anim->unk04 - 1;
endframe = model->endframe;
#ifdef PORT
/* D243 M-185: clamp corrupted endframe values during scripted
* camera modes, producing huge negative values like
* -604462909807314587353088.0 that cause the animation to loop
* indefinitely or behave unexpectedly. Clamp to a reasonable
* range (0-1000) to prevent this.
* M-186 correction: NOT a D100/D140 struct-punning/pointer-
* widening issue -- see the M-183 comment above for the
* corrected mechanism and for M-189's actual root-cause fix
* (stale 32-bit sizeof(Model) literal in bondview2.c); with that
* fix in place this clamp no longer fires in practice and is
* retained as defense-in-depth. RULE-2-SIGNOFF (2026-09-19,
* release review): approved for retention by the user for
* v0.3.0; candidate for removal post-release (findings.md D243 /
* M-192).
* M-187: was mistakenly gated on d243mProbeActive() (requires
* GE_D243M=1), so it never fired for a real player. Fixed to
* gate on the camera-mode test alone (gameScriptedCameraActive()).
* Logging stays separately gated on d243mProbeActive(). */
extern int d243mProbeActive(void);
extern int gameScriptedCameraActive(void);
if (gameScriptedCameraActive() && (endframe < 0.0f || endframe > 1000.0f))
{
if (d243mProbeActive())
{
osSyncPrintf("D243M: clamp endframe %.1f → 100.0\n", (double) endframe);
}
endframe = 100.0f;
}
#endif
if (endframe);
if (speed >= 0.0f)
@@ -3685,8 +3882,21 @@ void modelTickAnim(struct Model *model, s32 numticks, s32 update_chrstuff)
(void *)model->anim);
fflush(stderr);
}
/* D311: heal the stored state BEFORE falling back to it -- if
* animframe1 is the poison source, "frame = animframe1" would just
* re-derive garbage every tick and pin the anim forever. Snapping
* it to 0 (a valid frame; modelConstrainOrWrapAnimFrame clamps
* further) lets normal accumulation resume next tick. */
if (!(model->animframe1 > -1.0e6f && model->animframe1 < 1.0e6f))
{
model->animframe1 = 0.0f;
}
frame = model->animframe1;
}
if (!(model->animframe2 > -1.0e6f && model->animframe2 < 1.0e6f))
{
model->animframe2 = 0.0f;
}
if (!(frame2 > -1.0e6f && frame2 < 1.0e6f))
{
frame2 = model->animframe2;
+37
View File
@@ -887,6 +887,29 @@ void drawjointlist(ModelRenderData *data, ModelHitEntry *entry)
Gfx *gdl;
s32 opcode;
#ifdef PORT
/* D243 M-154: per-frame model-draw census. One line per drawjointlist
* call (i.e. per CHR render submission) while a scripted camera mode is
* active, logging the Model* pointer and render_pos[0]'s raw translation
* BEFORE the view transform. A second Bond instance shows up as extra
* lines with distinct model pointers at the same frame; a shared/aliased
* matrix buffer shows up as one pointer whose raw position cycles.
* Diagnosis only; silent when GE_D243M is unset or during FPS play.
* M-154 fix 2: render_pos is also rejected at the -1 tombstone value
* (belt-and-braces -- at draw time instcalcmatrices has already run,
* so this should never trigger; if it did, the game's own gSPSegment
* moments later would crash first anyway). */
extern int d243mProbeActive(void);
extern int d243mGetFrameCounter(void);
if (d243mProbeActive() && (entry != NULL) && (entry->model != NULL)
&& (entry->model->render_pos != NULL) && (entry->model->render_pos != (RenderPosView *) -1)) {
Mtxf *d243mraw = (Mtxf *) entry->model->render_pos;
osSyncPrintf("D243M: chrdraw frame=%d model=%p raw=%.1f,%.1f,%.1f\n",
d243mGetFrameCounter(), (void *) entry->model,
(double) d243mraw->m[3][0], (double) d243mraw->m[3][1], (double) d243mraw->m[3][2]);
}
#endif
if (data->gdl == NULL)
{
osSyncPrintf("drawjointlist: no gfxlist!\n");
@@ -1014,6 +1037,20 @@ void drawjointlist(ModelRenderData *data, ModelHitEntry *entry)
Gfx *gdl;
s32 opcode;
#ifdef PORT
/* D243 M-154: per-frame model-draw census (EU variant) -- see the US/JP
* copy above for the full rationale (incl. the M-154 fix 2 -1 guard). */
extern int d243mProbeActive(void);
extern int d243mGetFrameCounter(void);
if (d243mProbeActive() && (entry != NULL) && (entry->model != NULL)
&& (entry->model->render_pos != NULL) && (entry->model->render_pos != (RenderPosView *) -1)) {
Mtxf *d243mraw = (Mtxf *) entry->model->render_pos;
osSyncPrintf("D243M: chrdraw frame=%d model=%p raw=%.1f,%.1f,%.1f\n",
d243mGetFrameCounter(), (void *) entry->model,
(double) d243mraw->m[3][0], (double) d243mraw->m[3][1], (double) d243mraw->m[3][2]);
}
#endif
while (entry != NULL)
{
root = entry->rootnode;
+51
View File
@@ -67,6 +67,44 @@
#define GE_WATCH_STICK_FASTDOWN(pad) (joyGetStickY(pad) < -0x46)
#endif
#ifdef PORT
/* v0.2.1 (user report: "watch menu sensitivity for wasd/controller"): D118d
* removed the N64 slam-the-stick fast scroll because digital sources peg
* past its threshold -- but that left |stickY| >= 0x46 with NO repeat at all
* (the latched path steps once per press), so holding W/S or a full stick
* press crawled one item per tap through the list. Restore fast navigation
* as a bounded auto-repeat: while the stick is held past the smooth-scroll
* band (0x1f..0x45, untouched), step one extra item every GE_WATCH_NAV_RATE
* frames after an initial GE_WATCH_NAV_DELAY hold. The first press still goes
* through the latched single-step path, so a tap behaves exactly as before.
* Returns -1 (up) / +1 (down) on repeat frames, 0 otherwise. */
#define GE_WATCH_NAV_DELAY 15
#define GE_WATCH_NAV_RATE 6
static int s_watchNavHoldDir = 0; /* -1 up, +1 down, 0 none */
static int s_watchNavHoldFrames = 0;
static s32 geWatchStickFastStep(void)
{
int dir = 0;
if (joyGetStickY(PLAYER_1) >= 0x46) dir = -1;
else if (joyGetStickY(PLAYER_1) <= -0x45) dir = 1;
if (dir != s_watchNavHoldDir) {
s_watchNavHoldDir = dir;
s_watchNavHoldFrames = 0;
return 0; /* new direction: the latched press path already stepped */
}
if (dir == 0)
return 0;
if (++s_watchNavHoldFrames > GE_WATCH_NAV_DELAY &&
s_watchNavHoldFrames % GE_WATCH_NAV_RATE == 0)
return dir;
return 0;
}
#else
static s32 geWatchStickFastStep(void) { return 0; }
#endif
// bss
Mtx gfx_background_8007B0A0;
Mtx gfx_background_8007B0E0;
@@ -1107,6 +1145,19 @@ after_updown:
watch_inventory_cursor_pos += 1.0f;
}
#ifdef PORT
/* v0.2.1: bounded auto-repeat for a stick held past the smooth band
* (see geWatchStickFastStep). */
{
s32 fast = geWatchStickFastStep();
if (fast < 0 && g_curWatchItemIndex > 0 && !watch_item_is_actively_selected)
watch_inventory_cursor_pos += (f32) fast;
else if (fast > 0 && (s32) watch_inventory_cursor_pos < count - 1
&& !watch_item_is_actively_selected)
watch_inventory_cursor_pos += (f32) fast;
}
#endif
if (is_holding_less_than_10_up_on_stick() || is_holding_less_than_10_down_on_stick())
{
watch_stick_y_prev_active = 1;
+18
View File
@@ -9427,6 +9427,24 @@ apply_damage:
{
if (objGetDestroyedLevel(obj) == 1)
{
#ifdef PORT
/* D318-followup (diagnosis only, GE_OBJT=1): timestamp each
* GAS_RELEASING prop destruction so the tank-blow tick is
* correlatable against the OBJT stage-flag writes. Read-only,
* capped, no behavior change; N64 build unaffected. */
extern char *getenv(const char *);
static int s_gas = -1;
static int s_gasn = 0;
if (s_gas < 0) { s_gas = getenv("GE_OBJT") != NULL; }
if (s_gas && (s_gasn < 200))
{
osSyncPrintf("OBJT: t=%d GAS prop destroyed at (%.0f,%.0f,%.0f) [gas effect armed]\n",
(int)g_GlobalTimer, (double)obj->runtime_pos.x,
(double)obj->runtime_pos.y, (double)obj->runtime_pos.z);
s_gasn++;
}
#endif
init_trigger_toxic_gas_effect(&obj->runtime_pos);
}
}
+68 -1
View File
@@ -26,8 +26,16 @@
* See the comment above skyPortBeginFan()'s definition. */
static void skyPortBeginFan(SkyRelated38 *v, s32 n, bool allowShift);
static void skyPortEndFan(void);
/* D245 (M-142): same reason -- the water call site (further down) needs to
* override the adaptive shift picked by skyPortBeginFan() before the
* definition itself. */
static void skyPortForceFanShift(s32 kS, s32 kT);
/* D227 (M-100): same reason -- the texSelect call sites precede the definition. */
static void skyPortCaptureTile(Gfx *start, Gfx *end);
/* D227 (M-100): relocated up here (was just above skyPortPickShift, further
* down) so the D245 water call site's GE_D245_FIXEDSHIFT clamp can use it --
* same forward-declare reason as the functions above. */
#define SKY_TC_MAX_SHIFT 5
#endif
// bss
@@ -930,6 +938,43 @@ Gfx *skyRender(Gfx *gdl)
* comment above skyPortBeginFan() for why this doesn't flatten
* the cross-fade. */
skyPortBeginFan(sp274, s1, TRUE); /* water: see skyPortBeginFan */
/* D245 (M-142): opt-in A/B candidate -- see skyPortForceFanShift's
* comment. Off by default (unset env => identical to the current
* allowShift=TRUE behaviour this row is still reopened against). */
{
const char *fs = getenv("GE_D245_FIXEDSHIFT");
if (fs) {
s32 kFixed = atoi(fs);
if (kFixed < 0) kFixed = 0;
if (kFixed > SKY_TC_MAX_SHIFT) kFixed = SKY_TC_MAX_SHIFT;
skyPortForceFanShift(kFixed, kFixed);
}
}
/* D245 (M-142, reopened): dumps the water quad's raw S/T span
* every call, so a scripted multi-frame capture can show whether
* the span crosses skyPortPickShift's ~30719-unit threshold (and
* therefore the tc-shift k) as the camera rotates -- the
* still-open question after allowShift=TRUE didn't fix the live
* symptom. Recomputed locally rather than reading skyPortBeginFan's
* statics (defined later in this TU, not forward-declared). */
if (getenv("GE_D245V")) {
static int callN = 0;
f32 minS = sp274[0].unk20, maxS = sp274[0].unk20;
f32 minT = sp274[0].unk24, maxT = sp274[0].unk24;
s32 vi;
for (vi = 1; vi < s1; vi++) {
if (sp274[vi].unk20 < minS) minS = sp274[vi].unk20;
if (sp274[vi].unk20 > maxS) maxS = sp274[vi].unk20;
if (sp274[vi].unk24 < minT) minT = sp274[vi].unk24;
if (sp274[vi].unk24 > maxT) maxT = sp274[vi].unk24;
}
fprintf(stderr, "D245V call=%d s1=%d minS=%.1f maxS=%.1f minT=%.1f maxT=%.1f "
"spanS=%.1f spanT=%.1f\n", callN++, s1,
(double) minS, (double) maxS, (double) minT, (double) maxT,
(double) (maxS - minS), (double) (maxT - minT));
}
#endif
if (s1 == 4)
{
@@ -1626,7 +1671,6 @@ static bool s_skyFanFoldSet = FALSE;
* Defect-2 tessellation; skyPortPickShift clamps rather than overflowing, so
* the failure mode stays graceful.
*/
#define SKY_TC_MAX_SHIFT 5
/* One tile period expressed in unk20/unk24's units: 64 texels * 32 = 2048.
* The fold must be a whole number of these or the rebase shifts the texture. */
@@ -1795,6 +1839,29 @@ static void skyPortEndFan(void)
s_skyFanShiftT = 0;
}
/* D245 (M-142): overrides the shift skyPortBeginFan() just picked. A GE_D245V
* capture (scripted camera turn, Frigate, headless) proved the water quad's
* raw S/T span sits in the ~290k-390k unit range -- roughly 10x sky's typical
* span -- and, critically, that skyPortPickShift's adaptive k genuinely
* CHANGES during ordinary view rotation (observed kT flipping 4->3 as spanT
* crossed skyPortPickShift's per-k breakpoint mid-turn). That contradicts the
* "normally k==0, this is a rare safety valve" assumption the mechanism was
* verified against (a single static frame, one camera angle) -- every k
* transition re-quantizes the shared vertex tc at a different granularity,
* which is a plausible mechanism for "glitches, goes back and forth... changes
* depending on your view" even with both texunits' relative offset preserved
* (D245's already-verified allowShift=TRUE reasoning). Untested candidate,
* NOT the default: forcing a single fixed k for the whole water draw removes
* the transition, at the cost of coarser (whole-k5-step) tc quantization
* always, which could itself look worse -- needs a live A/B, not another
* blind ship (see the finding's own "don't repeat the same mistake" note).
* GE_D245_FIXEDSHIFT=<k> (0..SKY_TC_MAX_SHIFT) opts in; unset changes nothing. */
static void skyPortForceFanShift(s32 kS, s32 kT)
{
s_skyFanShiftS = kS;
s_skyFanShiftT = kT;
}
/*
* D176(a) Path B (M-46) -- black-sky fix. Full rationale in
* docs/dev/findings.md section F, "D176(a)".
+39
View File
@@ -4,6 +4,10 @@
#include "vtxstore.h"
#include "propobj.h"
#include "model.h"
#ifdef PORT
#include <stdio.h>
#include <stdlib.h>
#endif
// unsure if these structs are defined as something else, elsewhere
struct unk_09B7A0_struct_parent {
@@ -143,7 +147,33 @@ void sub_GAME_7F09B820(void)
*/
void sub_GAME_7F09BAC4(s32 find, s32 replacement) {
PropRecord* var_s1;
#ifdef PORT
/* D255 (M-140): the original decomp read this record's Model* through
* `((Model*)((ChrRecord*)prop->chr)->chrflags)` -- a union pun onto
* ChrRecord.chrflags, which is typed `CHRFLAG` (a 4-byte enum). On
* 32-bit N64 that's bit-exact: both the enum and a pointer are 4
* bytes, so casting the enum's VALUE to Model* reproduces the real
* bytes stored there. On 64-bit PC, Model* is 8 bytes but the enum
* read is still only 4 -- the cast reads just the pointer's low 32
* bits as an int and zero-extends it back out to a 64-bit address,
* discarding the real high bits entirely. The result is either a wild
* pointer or (as observed live on a Steam Deck, 2026-09-15: FAULT ADDR
* 0x10, PC in this function's `var_v1->RootNode` dereference) exactly
* NULL, whenever the true pointer's low 32 bits happen to be zero.
* Root-caused by comparing this truncated read against
* `var_s1->obj->model` (ObjectRecord.model, correctly typed as
* `Model*`) on the same live capture: the ObjectRecord-typed read was
* always a valid, non-NULL pointer -- confirming the record's model
* was never actually missing, only misread. Fix: read the pointer
* through its real 64-bit-wide field (`ObjectRecord.model`, the same
* union memory, just correctly typed) instead of through the
* mistyped `ChrRecord.chrflags` alias. Semantics-preserving on N64
* (identical bytes, identical result) and the D3x pointer-width ABI
* class documented in AGENTS.md / docs/dev/findings.md. */
ObjectRecord* var_v0;
#else
ChrRecord* var_v0;
#endif
Model* temp_a0;
s32* temp_v0_2;
ModelNode* var_a1;
@@ -153,13 +183,22 @@ void sub_GAME_7F09BAC4(s32 find, s32 replacement) {
var_s1 = chrpropGetActiveTail();
while (var_s1 != NULL) {
if (var_s1->type == 1) {
#ifdef PORT
var_v0 = var_s1->obj;
var_v1 = ((Model*)var_v0->model)->obj;
#else
var_v0 = var_s1->chr;
var_v1 = ((Model*)var_v0->chrflags)->obj;
#endif
var_a1 = var_v1->RootNode;
while (var_a1 != NULL) {
val = var_a1->Opcode & 0xFF;
if (val == 0x18) {
#ifdef PORT
temp_v0_2 = modelGetNodeRwData((Model*)var_v0->model, var_a1);
#else
temp_v0_2 = modelGetNodeRwData((Model*)var_v0->chrflags, var_a1);
#endif
if (find == *temp_v0_2) {
*temp_v0_2 = replacement;
}
+16 -4
View File
@@ -88,10 +88,22 @@ void alEvtqPostEvent(ALEventQueue *evtq, ALEvent *evt, ALMicroTime delta)
if (!item) {
#ifdef PORT
/* D202/M-71 diag: silent drop is the prime suspect for missing
* [VOICE+]; log every exhausted-queue post. Remove with probe set. */
geTracePrintf("audiotrace.log", "[EVTQ-DROP] evtq=%p type=%d state=%p delta=%d\n",
(void *)evtq, (int)evt->type,
(void *)((u8 *)evt + 2), (int)delta);
* [VOICE+]. Gated on GE_AUDIOTRACE (same switch as the sibling
* probes in synthesizer.c/csplayer.c/load.c) -- this was left
* unconditional, so shipped builds wrote an unbounded log on every
* exhausted-queue post; found via GitHub #87's session logs, which
* showed sustained pool exhaustion (a real pool-sizing question,
* tracked separately) but should never write to disk by default.
* getenv() is cached (D250, M-120): this runs on every dropped
* event post, a real per-item hot path, unlike the sibling probes'
* one-shot init/load call sites. */
static int s_audiotrace = -1;
if (s_audiotrace < 0) s_audiotrace = getenv("GE_AUDIOTRACE") != NULL;
if (s_audiotrace) {
geTracePrintf("audiotrace.log", "[EVTQ-DROP] evtq=%p type=%d state=%p delta=%d\n",
(void *)evtq, (int)evt->type,
(void *)((u8 *)evt + 2), (int)delta);
}
#endif
osSetIntMask(mask);
#ifdef _DEBUG
+46
View File
@@ -344,8 +344,44 @@ void sndHandleEvent(ALSndPlayer *sndp, ALSndpEvent *event) {
// Not a looped or retriggered sound, and all retries have been exhausted.
if (limitReached) {
// Check if we can preempt a lower-priority sound.
/* D285: this scan reads/walks D_800243E4 without the
* osSetIntMask lock sndSetupSound (above) already takes
* around every mutation of the same list -- an
* asymmetric-locking gap (writer locked, reader not) in
* the D4/D152 class ("N64's interrupt-disable trick is a
* no-op on a real PC thread"). Locking here changes
* nothing observable on N64 (this function's own caller,
* AL_SNDP_PLAY_EVT, already runs with interrupts masked
* on real hardware per the audio ISR's normal execution
* context); it closes the PC race where a burst of
* sndPlaySfx calls (explosions) from the main thread can
* mutate this list while the audio thread is mid-walk. */
OSIntMask d285Mask = osSetIntMask(OS_IM_NONE);
ALSoundState *iterState = (ALSoundState *) D_800243E4.node.prev;
/* D305 (Steam Deck SIGSEGV, v0.3.0 pre-release, FAULT ADDR 0x62 --
* same exact signature as D285/M-140, reproduced AFTER the M-141
* lock fix was already live-verified crash-free on real hardware
* (v0.2.2 hotfix). That rules out D285's own "unprotected
* concurrent mutation" theory as the sole cause: the lock above
* (d285Mask) prevents another thread from emptying this list
* mid-scan, but does nothing if the list is ALREADY empty
* (D_800243E4.node.prev == NULL) the moment this single-threaded
* scan starts -- which the do-while below dereferences
* unconditionally before its own `iterState != NULL` check ever
* runs (a do-while always executes its body once). The scan is
* gated on `limitReached` (the 8-voice pool believed exhausted),
* which should imply at least one live tracked node -- a NULL
* here means the pool-exhaustion accounting and this list have
* desynced by some still-unidentified mechanism (not proven to be
* the user's "Game.AllUnlocked" report specifically; logged as a
* correlation, not a confirmed cause -- see docs/dev/findings.md
* D305/D285). Same defensive-guard shape as D255's
* skip-and-log-instead-of-dereferencing fix: this only prevents a
* crash in a state that was never valid to scan in the first
* place, and changes nothing when the list is non-empty (the
* normal, intended case). */
if (iterState != NULL)
do {
/* D202/M-65 (PORT, experimental): the stock scan refuses to
* preempt any looped or retriggering voice (0x12). That is
@@ -381,6 +417,16 @@ void sndHandleEvent(ALSndPlayer *sndp, ALSndpEvent *event) {
}
iterState = (ALSoundState *) iterState->link.prev;
} while (limitReached && iterState != NULL);
else if (getenv("GE_D305")) {
/* D305: log the desync state for a future occurrence --
* confirms whether Game.AllUnlocked correlates or was
* coincidental, and whether it recurs at all now that the
* crash itself can't happen. */
fprintf(stderr,
"D305: sndHandleEvent preempt-scan found D_800243E4 empty "
"while limitReached was set -- pool/list desync, scan skipped\n");
}
osSetIntMask(d285Mask);
if (!limitReached) {
// Retry the sound that was preempted.
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""D295/M-148: reproduce fileGenerateCRC (src/game/crc.c) with both the pre-D284
(buggy, shipped in v0.2.x) and post-D284 (HEAD) PRNG semantics, verify against the
user's saved slot CRCs, and emit a patched eep whose slots carry POST-FIX CRCs so
a HEAD build accepts them (proves the difficultytext overflow is still live).
Save layout in ge007.eep (verified M-148):
bytes 0..31 : smallSave header (block 0)
slot i : file offset 32 + i*96, i = 0..5 (sizeof(save_data)=96, padded)
slot fields : c1(s32) c2(s32) bitflags(1) flag_007(1) music(1) sfx(1)
options(2) cheats(3) pad(1) times[76]
CRC region : &completion_bitflags .. save+1 => slot bytes [8..96), 88 bytes.
"""
import struct, sys
M64 = (1 << 64) - 1
def make_rng(buggy: bool):
def dsll32(x, n):
if buggy: return (x << n) & 0xFFFFFFFF
return (x << (n + 32)) & M64
def dsrl32(x, n):
if buggy: return (x >> n) & 0xFFFFFFFF
return x >> (n + 32)
def step(seed: int) -> int:
# Mirrors randomGetNextFrom line-by-line. NOTE the plain dsrl/dsll lines
# (a2 >>= 31, a1 = a3 << 31, a2 >>= 20) are NOT the *32 helpers.
a3 = seed & M64
a2 = dsll32(a3, 0x1F) >> 0x1F # dsll32 then plain dsrl 31
a1 = dsrl32((a3 << 0x1F) & M64, 0) # plain dsll 31 then dsrl32 0
a3 = dsrl32(dsll32(a3, 0xC), 0) # dsll32 then dsrl32 0
a2 = (a2 | a1) ^ a3
a3 = ((a2 >> 0x14) & 0xFFF) ^ a2 # plain dsrl 20, andi, xor
return a3 & M64
return step
def file_generate_crc(data: bytes, buggy: bool):
"""Mirror crc.c exactly: shift is NOT reset between the two passes."""
step = make_rng(buggy)
poly = 0x8F809F473108B3C1 & M64
c1 = 0; c2 = 0; shift = 0
for b in data: # forward pass, +7/byte
poly = (poly + ((b << (shift & 0xF)) & M64)) & M64
shift += 7
poly = step(poly) # randomGetNextFrom updates the seed IN PLACE
c1 ^= poly & 0xFFFFFFFF
for b in reversed(data): # backward pass, +3/byte, shift continues
poly = (poly + ((b << (shift & 0xF)) & M64)) & M64
shift += 3
poly = step(poly)
c2 ^= poly & 0xFFFFFFFF
return c1 & 0xFFFFFFFF, c2 & 0xFFFFFFFF
def main():
src = sys.argv[1] if len(sys.argv) > 1 else r"C:\Users\james\Downloads\ge-issue87-artifacts\ge007.eep"
dst = sys.argv[2] if len(sys.argv) > 2 else None
eep = bytearray(open(src, "rb").read())
assert len(eep) == 2048, len(eep)
print("slot | stored c1,c2 | oldPRNG c1,c2 | newPRNG c1,c2 | old? new?")
ok_old = ok_new = True
for i in range(5):
off = 32 + i * 96
sc1, sc2 = struct.unpack_from("<II", eep, off)
region = bytes(eep[off + 8: off + 96])
oc1, oc2 = file_generate_crc(region, buggy=True)
nc1, nc2 = file_generate_crc(region, buggy=False)
o = (oc1, oc2) == (sc1, sc2); n = (nc1, nc2) == (sc1, sc2)
ok_old &= o; ok_new &= n
print(f" {i} | {sc1:08x},{sc2:08x} | {oc1:08x},{oc2:08x} | {nc1:08x},{nc2:08x} | {o=} {n=}")
print(f"old PRNG matches all 5 stored CRCs: {ok_old}")
print(f"new PRNG matches all 5 stored CRCs: {ok_new}")
if dst:
for i in range(5):
off = 32 + i * 96
region = bytes(eep[off + 8: off + 96])
nc1, nc2 = file_generate_crc(region, buggy=False)
struct.pack_into("<II", eep, off, nc1, nc2)
open(dst, "wb").write(eep)
print(f"patched (post-fix CRCs, slots 0-4) -> {dst}")
if __name__ == "__main__":
main()
+1 -1
View File
@@ -54,7 +54,7 @@ def one_liner(cell: str, limit: int = 160) -> str:
LEAD_RE = re.compile(
r"^\**\s*(OPEN|RESOLVED|FIXED|PROPOSED|PARTIAL|LANDED|CLAMP|DISPROVEN"
r"|ROOT-CAUSED|GUARDED|ANALYZED|DIAGNOSTIC SHIPPED|NOT A BUG)",
r"|ROOT-CAUSED|GUARDED|ANALYZED|DIAGNOSTIC SHIPPED|NOT A BUG|CLOSED)",
re.I,
)