setupCctv orientation path traced end-to-end and every input verified
N64-faithful against the ROM for Bunker1: PcctvZ (not GcameraZ) switch
data bit-for-bit, boundpads match the pccg sidecar once the legitimate
load-time room-scale (pad->pos *= get_room_data_float2(), ~1.8542) is
applied, record config words incl. unkCC match; all matrix/atan2 helpers
are byte-identical src/game code. PC orientation values are what the N64
computes; if the symptom genuinely differs from N64 the remaining suspect
is fast3d's handling of the non-orthonormal lens matrix (unk84 row 0
scaled 0.1x). Blocked on user repro detail.
Also: plan/README/findings refreshed for M-197 (D75 fixed, user-confirmed)
and M-198 (D236 closed); D252 retired; findings index regenerated.
D236 was root-caused and fixed in the previous commit, so its diagnostic
scaffolding comes out:
GE_D236RM / GE_D236ORDER / GE_D236ALPHA / GE_D236ZFIX gfx_sp_tri1
GE_D236RAW bg.c room load
GE_D236BT chr.c + model.c
port_d236bt_dump() host stack backtrace
g_d236_last_vtx_seg / _raw G_VTX trackers
Also drops the <windows.h> include that only the backtrace dumper needed, and
rewords the one D288 comment that cross-referenced GE_D236RM. src/game/bg.c,
chr.c and model.c are back to their pre-probe state -- no game-code diff from
D236 remains. The pass-26 fix in gfx_lod_tile_offset is untouched.
Doc rows removed from GE-ENV-PROBES.md; gen_env_probes.py --check and
gen_findings_index.py --check both pass. Build clean. The Surface 1 spawn
capture is unchanged across the strip (settled frame 560 within threshold;
the intro-pan frames never match run-to-run, D117).
GE's texLoadFromGdl picks a handler per G_NOOP texture reference from w0 & 7.
TEXTURETYPE_LOD/MIPMAP (texHandleType0/Type2) issue ONE load block at TMEM 0,
so tiles 0..n are LOD levels of that one image. TEXTURETYPE_DETAIL
(texHandleType1, src/game/tex.c) issues TWO: the detail texture at TMEM 0 and
the real base image at TMEM texGetSizeInBytes(detail,0) on tiles 1+.
gfx_lod_tile_offset folded every G_TL_LOD draw to tile 0 (D107 -- correct for
the first two, wrong for DETAIL), so the port drew the detail texture and never
imported the base image at all. All four Surface 1 treeline cards
(tex1198-tex1201, RGBA5551 64x17 cut-outs) are DETAIL bindings sharing one
detail texture, tex2465: an opaque 32x32 IA8 noise tile. That is the same
texture D267/D276 measured at 1023/1024 texels opaque and called the mystery
"noise painter" -- hence 20+ passes chasing decode, alpha, mip and geometry
theories for what was a binding bug.
Fix (port layer only): under G_TL_LOD, if tile fi+1 differs from tile fi in
BOTH TMEM address and fmt/siz, sample tile 1. Asymmetric on purpose -- LOD
levels of one image always share a format, so a mip chain can never trip it,
and a detail pair that happened to share a format just keeps the old
behaviour. Config Video.DetailBaseTile, default on.
Verified: Surface 1 idle spawn goes from an opaque tiled noise wall to discrete
snow-covered pines with ground/snow unchanged; Surface 2 shows a discrete night
treeline; Depot renders clean; bunker1 golden framediff is bit-identical with
the fix on and off. Offline scan of every level's bg: DETAIL bindings exist
only in sevx (84), depo (39), sevb (4), sev (4), so every other level is
provably untouched. User play-test confirmed.
Also refuted and removed pass 25's fix direction: cut-out mip suppression
changed nothing, and its alpha census showed the wall is painted by a fully
opaque texture -- retiring the mip / 1-bit-alpha / XLU-smear branch. The
GE_D236P25 probe is gone with it.
tools_pc/gen_env_probes.py: stop counting a "GE_FOO[" log-line TAG in the doc
as a documented env var. GE_TEXDUMP_ADDR prints "GE_TEXA[...]", which is not
the name of any getenv(), so the check reported a permanent phantom GONE.
Generalised as docs/porting-notes.md D12; full trail in findings.md D236 pass
26. Carries the pending D75/M-197 sidecar-converter fix and the env-gated
D236/D75 probes, which the doc gates require to land alongside their rows.
New env-gated GE_D75D probe (gfx_pc.cpp) captures per-frame V/T token stream
+ per-triangle rejection/emission accounting for the front-end logo window.
Findings (bare front-end boot, logo = frames ~244+, nv=93/nt=1018):
- Slot-overwrite (M-195's leading hypothesis) REFUTED: the DL is streamed
(V TTT...VTTT interleaved), max triangle vertex index = 15, so no
cross-batch vertex clobbering can occur.
- Rejection gates are not the cause: of 1018 tris/frame, trivial-reject=0,
backface-cull ~520 (normal half), ~500 reach GL, all on-screen (NDC in
[-1,1]^2). Not the invisible flag; vertex colors [0,255]; depth normal.
- PPM analysis: logo window renders 0-33 content px/frame (the 'tiny
fragment'), vs title quads at 8.36% coverage.
- Narrowed cause: combine mode 0x009ffe4f19ffe4f1 parses to
rgb a=TEXEL0 c=SHADE (texture-modulate); gfx_generate_cc sets
used_textures[0]=1, but the material still renders black-on-black. Next
step: trace where the TEXEL0 sampler is lost between combiner build and
draw (shader_id0 -> GL program / prg[tm] / texture upload).
Port-layer fast3d only; no rule-2 sign-off needed. Probes env-gated, inert
without GE_D75D (verified: 0 lines in a normal run).
The campaign has 20 playable missions (SP_LEVEL_MAX // 20 in
bondconstants.h; mission_folder_setup_entries in front.c has exactly 20
entries). The old '21' counted -level_54 (LEVELID_CUBA), which the front
end jumps to after the final mission for the extended cast display +
credits scroll (do_extended_cast_display / bondviewRenderCredits, D129) -
it is not a selectable mission.
- README, docs/index.md, .github/release-notes.md,
docs/security-and-fidelity-status.md: 21 -> 20 in current-status claims
- AGENTS.md phase status: same correction
- docs/dev/LEVEL-STATUS.md + LEVEL-PLAYTEST.md: relabel the level list;
counting note explains historical '21/21 PASS' sweep results = 21 stage
boots (20 missions + ending sequence)
- social-preview card: stat line 21 -> 20, PNG regenerated (1280x640)
- gfx_sp_vertex probe (env GE_D75V=lo-hi): logs per G_VTX batch the
transformed x/y/z/w min/max, raw first-vertex ob, dest_index, and the
MP translation row. Proves the logo transform is correct and that every
batch encodes v0=0 (slot-overwrite hypothesis).
- videoGetFrameCount() accessor for frame-gated probes.
- findings.md: M-195 entry in the D75 section (emission + transform
eliminated; G_VTX v0=0 overwrite is the leading suspect).
- GE-ENV-PROBES.md: register GE_D75V, bump reconciled line.
D235 (wrong textures after dying/restarting a level) -- mechanism found
+ fix (M-193). M-108's refutation of the game-side CI-cache theory stands,
but the port has its own TextureCacheMap (port/fast3d/gfx_pc.cpp) keyed by
(source address, palette addrs, fmt, siz, size_bytes, palette_hash) with NO
content check. Stage-texture source addresses live in MEMPOOL_STAGE, which
the per-stage while(!done) block in boss.c resets on every transition
(death/restart included). After the reset, entries keyed into that arena are
stale; if the texpool places a different image at an address that held
another one in the previous run of the level, the lookup hits and returns
the old pixels -- 'wrong textures' only on the reload path, never on a cold
boot. The N64 has no such cache (RSP re-reads DRAM every frame), so this is
a pure port-side defect: videoResetTextureCache() (port/src/video.c) existed
with zero callers and is now called in that block after memaReset
(#ifdef PORT). Re-uploads are pixel-identical; cost is one-time during the
loading screen. GE_D235=1 logs the purged entry count per transition
(cold boot = 0, death/restart > 0) as mechanism evidence; new
gfx_texture_cache_count() accessor backs it. Full 21-level golden sweep:
21/21 PASS (pixel-identical). Live death/restart confirmation is a playtest
item.
D311 (upstream poison source, M-194): every write of model->speed funnels
through modelSetAnimSpeed (startframe<=0 path) or modelSetAnimation2 -- both
now log 'D311: ... ANOMALY' when the incoming value or the resident speed is
non-finite/|v|>1e4 (#ifdef PORT, unconditional-on-fire like the D156
guards; silent in healthy play). The line captures in/resident/startframe/
oldspeed/newspeed/timespeed + model ptr so the next Caverns occurrence pins
the injecting call. Static pass: sub_GAME_7F06FE90 callers all pass
arg2=16.0f (denominator safe); the old->new lerp only circulates values;
bheadAdjustAnimation is the one path where a live player-velocity
corruption reaches model->speed.
D320 (Phase 1 item 1): d318TimelineTick (GE_D318T=1) now also runs a
generic D318-class pin DETECTOR for the sweep's flagged lists (Facility
ai_19, Control ai_9, Depot ai_12): any chr in ACT_ATTACK with a live-fire
attacktype (no TARGET_AIM_ONLY bit) whose modelGetAnimFrame is byte-stable
for 600 ticks logs one 'D320T: PIN?' line with AI-list id/offset/atk/ent/
mot/unk54, repeating every further 600 stable ticks. Detection only -- the
recovery watchdog stays hardcoded to ai_22 by design (D320's explicit
instruction: generalize only after a second list actually freezes in play).
Legit states excluded/self-clearing: monologue pre-aims keep the AIM_ONLY
bit; working fire cycles re-init through chrlvTickAttackCommon so their
frame does not stay byte-stable for 10 s. Smoke: 90 s Facility run, zero
false positives, existing D318T timeline unaffected.
D288 (Phase 1 item 3): per M-155's revised next step, the GE_D288 probe
now skips any_behind_camera==1 triangles (the x/w divide is meaningless for
w<=0 -- that subset was pure false-lead noise) and logs the render-mode
word (oml) so a live capture can ID the culprit asset against level tables.
Re-ran bare -level_20: 1965 sane hits in the 550-755 window, still zero
hits for the target color RGB(83,62,44) -- consistent with M-155's working
theory that the bare boot's scripted camera never reaches the bug state;
headless probing is at its limit here, live capture is next.
bg.c getenv caching (D302-class sweep follow-up): GE_D104 (per room pass,
per frame), GE_D63 primary+secondary render logs (per room, per frame), and
the two GE_D154 inner-GDL-walk sites (per bullet hit) now use the cached
static pattern. All #ifdef PORT blocks; zero behavior change when unset.
The D321 stage-flag writer probe (chrSetStageFlags/chrUnsetStageFlags in
chraction.c, GAS_RELEASING destruction timestamp in propobj.c) did its job
(D321 trigger chain mapped + closed); all three blocks were #ifdef PORT
diagnostics with no N64-build or runtime effect when unset. Dropped the
GE-ENV-PROBES.md row too (drift check passes).
getenv hot-path sweep (D302 follow-up, HANDOFF M-192): function-level scan
of all 171 GE_* getenv sites found no uncached calls at frame/triangle rate
-- the only candidates are snd.c's event-rate probe guards (all #ifdef PORT)
and bossReturnTitleStage (once per title return). fast3d has zero raw
'if (getenv(' sites.
User report (v0.3.0 full campaign): audio progressively degrades from Silo;
by Caverns/Cradle the OST is inaudible and SFX come and go; restarting the
game restores it. Logged as D322 with static triage: level teardown audit
clean, mixer stateless per frame, D202's covered leak class is one shape
only (ownerless count==-1 loops) validated single-level only, and D305
already records a pool/list accounting desync. Ranked hypotheses:
voice-pool exhaustion/counter drift > queue starvation > evtq saturation.
Probe: GE_D322=1 prints sfx=n/8 (SFX soft cap), evtq=n/64, pv free/lame/alloc
(24 physical synth voices) + pipeline depth/drops every 5 s of wall clock,
read with GE_D204. sndD322PoolSummary() walks the ALSynth lists under the
D285 lock; NB drvr is a pointer field (first build faulted on &drvr).
Smoke-tested 40 s headless -level_09: pools coherent, no crash.
Also adds the missing D321 row to the findings index (entry existed at file
tail with no table row, so it was absent from the CSV too).
User report (v0.3.0 full campaign): audio progressively degrades from Silo;
by Caverns/Cradle the OST is inaudible and SFX come and go; restarting the
game restores it. Logged as D322 with static triage: level teardown audit
clean, mixer stateless per frame, D202's covered leak class is one shape
only (ownerless count==-1 loops) validated single-level only, and D305
already records a pool/list accounting desync. Ranked hypotheses:
voice-pool exhaustion/counter drift > queue starvation > evtq saturation.
Probe: GE_D322=1 prints sfx=n/8 (SFX soft cap), evtq=n/64, pv free/lame/alloc
(24 physical synth voices) + pipeline depth/drops every 5 s of wall clock,
read with GE_D204. sndD322PoolSummary() walks the ALSynth lists under the
D285 lock; NB drvr is a pointer field (first build faulted on &drvr).
Smoke-tested 40 s headless -level_09: pools coherent, no crash.
Also adds the missing D321 row to the findings index (entry existed at file
tail with no table row, so it was absent from the CSV too).
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.
- 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
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.
- 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).
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.
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.
- 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).
- 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.
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.
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).
- 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.
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.
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)
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).
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.
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).
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.
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.
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).
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.