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.
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).
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.
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).
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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).
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.
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).
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).
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.
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.
- 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.
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.
- 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.
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.
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.
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.
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.
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.
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.
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
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
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
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
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
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
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
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