The D318/D320 guard freeze is not a platform-invariant lock. modelTickAnim
folds numticks into one modelSetAnimFrame2WithChrStuff call, which only
advances framea/frameb when floor(frame) crosses an integer. At 1 tick/frame
(port at 60 fps) and attack speed 0.5-0.8, a pair left equal by the aim-hold
endframe clamp is re-emitted stale after the endframe is raised, so the
model pins forever. At >=2 ticks/frame (N64's normal regime) it cannot.
- model.c (#ifdef PORT, Rule-2 sign-off): when the loop did not run and the
current endframe no longer blocks the next frame, re-derive frameb as
modelSetAnimFrame does. GE_D329=0 disables, GE_D329LOG=1 logs triggers.
- Verified: lockstep clk=1 no longer pins (Facility ai_19, Control ai_9,
WSL + native Windows, watchdog off); 0 triggers at clk=2; user playtest
confirms N64-matching Ourumov timing. Bunker1 golden failure predates it
(identical with the fix off).
- d318bprobe (TEMP, env-gated): attack-state probe + GE_D318B_CLK lockstep.
- d320repro: fix jump=<chr>,... parser (documented form never parsed).
- findings D329 + D318/D320/D321 corrections, porting-notes D13.
@@ -195,10 +195,10 @@ D314,OPEN,"F10 menu items flicker randomly/intermittently (reported on the main-
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):
D318,OPEN,Facility (regular play path):,"OPEN — re-classified 2026-09-23 by D329 (tick-granularity divergence at 1 tick/frame, not a platform-invariant lock); fix decision pending, watchdog stays shipped."
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,RESOLVED,"D318-class sweep: aim-hold → _update AI lists in 8 levels carry the same as-authored softlock race (static analysis, 2026-09-20).",RESOLVED (2026-09-22):
D321,CLOSED,D318 trigger chain fully mapped:,CLOSED — trigger chain mapped and faithful; D318 watchdog validated in live play.
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 — re-classified 2026-09-23 by D329 (tick-granularity divergence at 1 tick/frame, not a platform-invariant lock); fix decision pending, watchdog stays shipped."
D321,OPEN,D318 trigger chain fully mapped:,"OPEN — re-classified 2026-09-23 by D329 (tick-granularity divergence at 1 tick/frame, not a platform-invariant lock); fix decision pending, watchdog stays shipped."
D323,OPEN,G_TL_LOD bindings of explicit-LOD textures:,"OPEN / parked — mechanism confirmed in code; census (tools_pc/bg_lod_audit.py) shows 15 levels with explicit-LOD textures bound under TEXTLOD-on classes (stat 378, azt 806 binds heaviest); overwhelmi…"
D324,FIXED,"GE's <stdlib.h> stub declares nothing, so TUs that include it get NO CRT prototypes —",FIXED (2026-09-22) — proven impact:
@@ -206,3 +206,4 @@ D325,CLOSED,ROM-mod (xdelta) compatibility:,CLOSED as a question —
D326,CLOSED,Random Eye randomizer repacks the ROM too —,CLOSED as a question —
D327,RESOLVED,"Mod support, resolved plan:",RESOLVED — superseded by D328; approach (a) is NOT ruled out (these scans missed the 1172-compressed data segment).
D328,CLOSED,"Mod support is viable and D327's ""ruled out"" was wrong:",CLOSED as a question —
D329,FIXED,The D318/D320 guard anim pin is a tick-granularity divergence:,"FIXED — Rule-2 high-FPS fix in model.c; clk=1 no longer pins (WSL + native Windows), 0 triggers at clk=2; bunker1 golden failure predates it (on/off A/B); user playtest confirms N64-matching Ourumov…"
1
# GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
@@ -603,10 +603,10 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D315 | **Menu scaling differs between main menu and in-game — main menu scales OK, in-game menu "seems kinda big sometimes" (user report, v0.3.0 playtest build, 2026-09-18).** — full `## D315` entry at file tail | OPEN — disclosed as a known issue, deferred to v0.3.1 (bigger design question, not a mechanical fix). |
| D316 | **Mouse-clicking some menu items glitches and lands on the item below instead (user report, v0.3.0 playtest build, 2026-09-18).** — full `## D316` entry at file tail | 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`. Build-verified (golden diff PASS, worst_cell=0); interactive click verification owed. |
| D317 | **Crash when toggling Game.AllUnlocked back and forth in-game (user report, v0.3.0 playtest build, 2026-09-18) — no dump left behind.** — full `## D317` entry at file tail | 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). Same fix resolves it. |
| D318 | **Facility (regular play path): after Objective C (destroying the bottling tanks) completes, Ourumov freezes and does not shoot Trevelyan as he should (user report, 2026-09-18).** — full `## D318` entry at file tail | RESOLVED (2026-09-19): as-authored latent race (gas bit mid-monologue derails c78's pre-aim hold into a permanent ACT_ATTACK anim pin), NOT a port bug; port-side watchdog `port/src/d318watchdog.c` re-inits the attack on confirm. D263 mechanism map stands (different failure mode). **N64 manifestation unverified** (2026-09-22 correction: user's empirical prior is no such N64 freeze; upstream-port-divergence vs unobserved — A/B vs N64 RAM capture is the discriminating test). |
| D318 | **Facility (regular play path): after Objective C (destroying the bottling tanks) completes, Ourumov freezes and does not shoot Trevelyan as he should (user report, 2026-09-18).** — full `## D318` entry at file tail | OPEN — re-classified 2026-09-23 by D329 (tick-granularity divergence at 1 tick/frame, not a platform-invariant lock); fix decision pending, watchdog stays shipped. Earlier resolution text kept in the full entry. |
| D319 | **Frame cap slider was mostly a dead zone; replaced with a plain 30/60 toggle (user ask, 2026-09-18).** — full `## D319` entry at file tail | FIXED — new `ROW_FPSCAP` cycle (30/60 only) in `port/src/optionsoverlay.c`; default changed to 60 in `port/src/video.c`. Root cause was D186 (sim tick ceiling + low-cap guard); this just fixes the menu's exposure of it. Build-verified; interactive confirmation owed. |
| D320 | **D318-class sweep: aim-hold → `_update` AI lists in 8 levels carry the same as-authored softlock race (static analysis, 2026-09-20).** — full `## D320` entry at file tail | RESOLVED (2026-09-22): forced repro confirms the pin on Facility ai_19 (off=483) and Control ai_9 (off=69, ~2/3 variants); Depot ai_12 clean 3/3 (scripted kill progresses — not covered). D318T detector fixed to scan `g_ChrSlots` as well as `g_ActiveChrs`; watchdog generalized to a table with uncapped repeat rescue. Open threads: N64-fidelity A/B via unthrottled emulator (per D318 correction), single Depot ai_10/TREED pin observation, ai_13/14/15 pad lists. |
| D321 | **D318 trigger chain fully mapped: tanks → combat bit is an authored ~3.5 s gas-cascade delay (chr 254 script); derail lands same-tick; PC behavior confirmed faithful to N64 (probe captures, 2026-09-20).** — full `## D321` entry at file tail | CLOSED — trigger chain mapped and faithful; D318 watchdog validated in live play. Optional deferred tuning: `D318_DEADLOCK_TICKS` 600→300 (user's call). |
| D320 | **D318-class sweep: aim-hold → `_update` AI lists in 8 levels carry the same as-authored softlock race (static analysis, 2026-09-20).** — full `## D320` entry at file tail | OPEN — re-classified 2026-09-23 by D329 (tick-granularity divergence at 1 tick/frame, not a platform-invariant lock); fix decision pending, watchdog stays shipped. Earlier resolution text kept in the full entry. |
| D321 | **D318 trigger chain fully mapped: tanks → combat bit is an authored ~3.5 s gas-cascade delay (chr 254 script); derail lands same-tick; PC behavior confirmed faithful to N64 (probe captures, 2026-09-20).** — full `## D321` entry at file tail | OPEN — re-classified 2026-09-23 by D329 (tick-granularity divergence at 1 tick/frame, not a platform-invariant lock); fix decision pending, watchdog stays shipped. Earlier resolution text kept in the full entry. |
| D322 | **Long-session audio degradation: full campaign on v0.3.0 — audio progressively worsens from Silo, by Caverns/Cradle the OST is inaudible and SFX "come and go"; restarting the game restores it (issue #87, user report, 2026-09-20).** — full `## D322` entry at file tail | OPEN — static triage done (teardown audit, mixer statelessness, D202-coverage check); ranked hypotheses: voice-pool exhaustion/counter drift > queue starvation > evtq saturation. `GE_D322` pool-telemetry probe shipped; needs a campaign capture with `GE_D322=1 GE_D204=1`. |
| D323 | **G_TL_LOD bindings of explicit-LOD textures: PC samples driver mips from LOD0, N64 samples the authored mip chain (static census, 2026-09-21).** — full `## D323` entry at file tail | OPEN / parked — mechanism confirmed in code; census (`tools_pc/bg_lod_audit.py`) shows 15 levels with explicit-LOD textures bound under TEXTLOD-on classes (stat 378, azt 806 binds heaviest); overwhelmingly CI8/IA8 (subtle), the only RGBA5551 cut-outs are the D236 set (already fixed). No reported artifact; fix path documented (upload GL mip chains for TEXTLOD-on bindings) but not worth doing speculatively. Also documents GE's OMH field-write encoding (op 0xBA, value at absolute bit position in w1). |
| D324 | **GE's <stdlib.h> stub declares nothing, so TUs that include it get NO CRT prototypes — implicit `int f()` declarations silently corrupt non-int returns: `atof` garbage in every float ini parse (D38 class).** — full `## D324` entry at file tail | FIXED (2026-09-22) — proven impact: `Game.ScreenShakeIntensity` (the only float ini option) silently parsed to garbage whenever a user set it; caught via the D294 `GE_ROOMPOOL` hook. Fix: `port/shim/stdlib.h` now declares atof/strtod/strtol/atexit/getenv unconditionally (K&R form); pc_protos.h gains atof/strtod for game TUs; glass2.c PORT block uses `__builtin_memset`. Residual harmless implicit decls (atoi/floorf/sqrtf/memset builtins, lround) documented. |
@@ -614,6 +614,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D326 | **Random Eye randomizer repacks the ROM too — the hypothesised "in-place data-only" mod tier is empty in practice; the in-ROM file table is the single keystone for any mod support (2026-09-22).** — full `## D326` entry at file tail | CLOSED as a question — identical ROM size but 96.5% of bytes and 803/803 file slices differ (content preserved, shifted +7216/+7392/−229888); same `d43_emit` zlib failure. D325's cheap tier disproven. Two bounded scans failed to find the in-ROM `fileentry` table (likely inside the compressed code image); that recovery question gates everything. Backlogged (local notes §5, extends T3.6). |
| D327 | **Mod support, resolved plan: reading the file table out of the ROM is ruled out by measurement; content-addressed offset remapping (gap inference over contiguously-packed files) is the viable route (1964GEPD fork review, 2026-09-22).** — full `## D327` entry at file tail | RESOLVED — superseded by D328; approach (a) is NOT ruled out (these scans missed the 1172-compressed data segment). `hw_address` absent from the ROM in all 4 encodings tested (0 hits for the segment-relative form); `file_resource_table.inc.c` is a decomp reconstruction. Viable route: locate unchanged files by content, bound changed ones by neighbour gaps (`obInit` differencing proves contiguity). Verified primitive: GE's `0x7F000000` segment maps uncompressed from ROM `0x34b30`, uniquely signature-findable. 1964 fork is GPL-2.0 — facts only, no code reuse. |
| D328 | **Mod support is viable and D327's "ruled out" was wrong: GE's file table IS in the ROM, inside the 1172-compressed data segment, and locating it needs no hardcoded offsets (PD dabs fork review + verification on vanilla + 2 mods, 2026-09-22).** — full `## D328` entry at file tail | CLOSED as a question — table at `0x252c4` inside the segment inflated from ROM `0x21990` (727 rows; 681 exact offset+size matches vs filelist, 0 mismatches). Hardcode-free scan reads Random Eye's own table -> **704/726 files byte-identical** (vs 0/803 at vanilla offsets in D326) and GF64's 24 MB table correctly. PD's architecture is an offline importer + mod-dir override, not a runtime ROM loader; its MIT `rompatch.c` removes the GPL xdelta3 constraint. Remaining work is the mod-dir override layer. |
| D329 | **The D318/D320 guard anim pin is a tick-granularity divergence: it exists only at 1 sim tick per rendered frame (port at 60 fps); at 2+ ticks/frame (GE's normal N64 regime) it cannot persist (lockstep A/B, 2026-09-23).** — full `## D329` entry at file tail | FIXED — Rule-2 high-FPS fix in model.c; clk=1 no longer pins (WSL + native Windows), 0 triggers at clk=2; bunker1 golden failure predates it (on/off A/B); user playtest confirms N64-matching Ourumov timing; watchdog stays shipped for now. |
Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS
threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software
@@ -12460,6 +12461,8 @@ Same fix as D316 resolves this (no separate code change). Verification: toggle `
## D318 — Facility (regular play path): after Objective C (destroying the bottling tanks) completes, Ourumov freezes and does not shoot Trevelyan as he should (user report, 2026-09-18).
**2026-09-23 correction (D329):** the pin is a tick-granularity divergence, not a platform-invariant lock. It persists only at 1 sim tick per rendered frame (the port at 60 fps); a single frame of 2 or more ticks (GE's normal N64 regime) breaks it. Lockstep A/B (GE_D318B_CLK) on ai_19 and ai_9: clk=1 pins every time, clk=2 never confirms. Earlier WSL wall-clock pin rates are uncalibrated (mixed clk 1-6). See D329.
**Reported (user, 2026-09-18):** on Facility (`-level_34`), following the **regular play path**, Ourumov **freezes up and will not shoot Trevelyan when Objective C — destroying the bottling tanks — is completed**. The user states this is **definitely a bug** despite prior agent conclusions to the contrary.
**Contradicts D263's resolution.** D263 (v0.2.1) closed the same surface symptom as "not a port bug — proximity/LOS-triggered exactly as on N64", verified via a *teleport probe* that dropped Bond 65 units from Trevelyan and confirmed the full chain incl. the execution shot firing. But that verification was **synthetic** (teleport, not the authored play path), and D263's own refinement already noted the trigger is "significantly sensitive to Bond's precise location" and "not confirmed byte-identical to N64 for every approach". This report reaches the beat via the **authored objective sequence** (Objective C = bottling tanks destroyed) and sees Ourumov frozen with no shot — a materially different entry point than D263 tested. **Do not re-assert the "not a bug / finicky trigger" conclusion from D263 without fresh evidence from this specific path.**
## D320 — D318-class sweep: aim-hold → `_update` AI lists in 8 levels carry the same as-authored softlock race (static analysis, 2026-09-20).
**2026-09-23 correction (D329):** the pin is a tick-granularity divergence, not a platform-invariant lock. It persists only at 1 sim tick per rendered frame (the port at 60 fps); a single frame of 2 or more ticks (GE's normal N64 regime) breaks it. Lockstep A/B (GE_D318B_CLK) on ai_19 and ai_9: clk=1 pins every time, clk=2 never confirms. Earlier WSL wall-clock pin rates are uncalibrated (mixed clk 1-6). See D329.
**Context.** D318 (Facility ai_22) root-caused a permanent softlock to a *generic* game-code mechanism, not to anything Facility-specific: `actor_fire_or_aim_at_target_update` (`chraction.c:4882`) succeeds whenever the chr is in ACT_ATTACK with `attacktype & (TARGET_AIM_ONLY | TARGET_DONTTURN)`, retargets, and calls `chrlvAttackActionRelated` — which only changes the model's **endframe** and **never calls `modelSetAnimation`**. If the chr is pinned on a non-looping aim-hold pose whose endframe H sits *before* the variant's shoot window (`shoot_start_frame`), and the new endframe E lands above H, nothing ever re-inits the animation (the `chrlvTickAttackCommon` "endframe <= frame" block's re-init sub-branches are all gated out: `unk31==0`, `unk33 != unk34`) → the model stays pinned below the shoot window → the timer/frame-gated fire events (`shoot_start <= frame < shoot_end`) never open → the chr never fires. If the surrounding AI script's only escape is an `if_guard_has_stopped_moving` gate, it can never pass: `chrHasStoppedOrPatroling` (`chraction.c:4291`) returns FALSE for ACT_ATTACK. **Permanent softlock.** Every link is byte-matched ROM logic. (N64 manifestation unverified — see D318's 2026-09-22 correction: the user's empirical prior is that no such N64 freeze exists; the same upstream-divergence-vs-unobserved question applies to every list in this sweep.)
**Sweep.** All 21 levels' AI script sources (`assets/obseg/setup/*.c`) scanned for lists containing both an aim command (`guard_try_fire_or_aim_at_target[_kneel]`, BITFIELD byte with `TARGET_AIM_ONLY` 0x20 set — the hold pose) and a `guard_try_fire_or_aim_at_target_update` reachable while the hold is active. 17 lists in 8 levels match; control-flow checked per list:
## D321 — D318 trigger chain fully mapped: tanks → combat bit is an authored ~3.5 s gas-cascade delay (chr 254 script); derail lands same-tick; PC behavior confirmed faithful to N64 (probe captures, 2026-09-20).
**2026-09-23 correction (D329):** the pin is a tick-granularity divergence, not a platform-invariant lock. It persists only at 1 sim tick per rendered frame (the port at 60 fps); a single frame of 2 or more ticks (GE's normal N64 regime) breaks it. Lockstep A/B (GE_D318B_CLK) on ai_19 and ai_9: clk=1 pins every time, clk=2 never confirms. Earlier WSL wall-clock pin rates are uncalibrated (mixed clk 1-6). See D329.
**Question.** User's N64 memory: Ourumov shoots Trevelyan the moment the tanks are blown up. PC playtests looked like he waited for the countdown to finish, then paused ~10 s, then shot. D318 covered the pin + watchdog; this entry closes the upstream question — *when and why does the combat bit `0x04` land, and is the PC trigger chain faithful?*
@@ -13082,3 +13089,84 @@ to end on two real mods, no research blocker remains. Amends **D327** (approach
updated in the local notes backlog §5. Cross-ref: D325 (GF64 probe + taxonomy),
D326 (Random Eye repack measurement this re-reads correctly), D327 (the scans
whose conclusion this corrects), D43/D69/D88 (converters), D179.
---
### M-199 (2026-09-22): adversarial review of the proposed D318/D320 fix plan (frame-data audit → runtime trace → conformance sweep → watchdog excision). **Verdict: SOUND-WITH-CORRECTIONS** — direction and constraint posture are right, but step 1 mischaracterizes the data path and is ~80% already done, the plan's "unhandled branch" is actually the *expected* outcome of step 1 (with no decision rule for it), and step 2 as written hunts for a mechanism that cannot exist in byte-matched code. No code changed; review only.
**Pre-flight (mechanism re-verified against source this session, all links hold):**
- Re-init block (chraction.c ~7126+): `modelGetAnimEndFrame <= frame` gates everything; the unk31 path (~7209) is the only `modelSetAnimation` re-init and is unreachable while E > H. ✓
- `modelSetAnimEndFrame` (model.c:2936): accepts `endframe` only if `endframe < ModelAnimation->unk04 − 1`, else stores −1 (unbounded). Named fields throughout. ✓
**New finding 1 — there is no conversion step for this data; step 1's premise is wrong.** The firing-animation groups are byte-matched decompiled C in `src/game/chr.c` (`pistol_firing_animation_groupN[]`, named-field access → A1-immune; `len = −1` resolved at runtime by `initResolveAnimGroups`/`initResolveAnimGroupTable`, initactorpropstuff.c:82–158, counting to the sentinel row). The `ModelAnimation` records come from **raw ROM via `romCopy`** (D34, port/src/romdata.c; `g_pc_animdata_base`) — the PC reads the *same bytes* the N64 does, and `ModelAnimation` has no pointer members (bondtypes.h:629; D32 already u32-ized `bitDescriptors`/`bitStream`) so the layout is stable. "Decode from raw ROM and compare against the port's converted assets" compares two things that are identical by construction; no ROM-decode tooling is needed or in `tools_pc/`.
**New finding 2 — the audit was effectively pre-run, and its outcome is knowable now.** Applying `chrlvAttackActionRelated`'s exact selection logic to `pistol_firing_animation_group1` (the group matching both probe signatures):
| variant | start | shoot_start | recoil_start | H (pre-aim) | E (post-derail) | outcome |
Group1 is **3/4 pin-capable** — consistent with the observed ~2/3–3/4 forced-repro rate. Other pistol groups: g2 1/2, g3 3/4, g4 1/1, g5 1/1, g6 3/4 — **no all-escape group exists**. Consequence: byte-matched code + ROM-identical data ⇒ under identical ticks the N64 should pin on ≥½ (typically ¾) of derails. The user's spec (0 pins in ~30 y / all 1964 trials escape) is therefore **in direct conflict with the byte-matched data** — this is the plan's "unhandled branch," and it is the *expected* outcome of step 1, not a corner case. The plan has no decision rule for it.
**New finding 3 — the pin sits exactly ON the fire-window boundary; D318/D320's stated condition is imprecise.** In every E>H variant above, **H == shoot_start exactly** (19/25/58) — not "H < shoot_start" as D318/D320 state. The pinned frame therefore sits at the edge of the fire gate `shoot_start <= frame < shoot_end` (chrlvTickAttackCommon), and whether c78 fires from that state hinges on **sub-frame settling**: `modelTickAnim`'s s32 `framea/frameb` clamp (`modelConstrainOrWrapAnimFrame`) + the `if (vb == va) animframe1 = va` sub-progress discard. The D318 probe printed `%.2f` ("19.00") — 18.99x would display identically. Working hypothesis: on N64 the frame settles at exactly shoot_start → gate opens → every "pin" variant fires (N64 pin rate = 0%, matches spec); on PC it settles just below (or a downstream gate in the fire loop — `unk38[i]` weapon slots, `unk44`/`attack_time` priming — stays closed) → ¾ pin (matches observation). **A single deterministic per-derail divergence explains the full 0%-vs-~66% rate gap with no PRNG argument** — which also confirms the brief's claim that a stream difference cannot explain it. Note this makes "find what re-inits the animation on N64 that the port misses" (plan step 2) misdirected: there is **no** re-init path for E>H in byte-matched code, so if data+code are identical, nothing can re-init on N64 either — the N64 escape must come from the fire gate opening at the boundary (or from the derail never landing), not from a missed re-init.
**Verdict per attack surface:**
1. **Candidate completeness.** {data, runtime} is exhaustive only if "runtime" is expanded to include (a) fire-gate boundary settling in `modelTickAnim`/`modelConstrainOrWrapAnimFrame` (new finding 3 — the prime suspect) and (b) the upstream derail window (hypothesis i). The §A1 concern (E/H misread at runtime from correct data) is *ruled out by construction* for this chain — compiled named-field access + raw-ROM `ModelAnimation` with no pointer members — but the plan should record that ruling explicitly instead of running a ROM decode.
2. **The unhandled branch.** Expected outcome, not corner case (finding 2). "The user's spec is wrong" is **not** an acceptable first conclusion: byte-matched code + identical data vs a 30-year community record means one link in the pin chain carries an unverified platform assumption; hunt runtime/reachability first (boundary settling is cheap and port-probeable). If the boundary hypothesis fails, one *bounded* emulator artifact — a single `g_randomSeed` memory read feeding `GE_RSEED_LV`, or one trial with frame-precision observation — is a different cost class from the 10-trial series the user ruled out; needs user sign-off before use.
3. **Ordering/feasibility.** Data-audit-first is right but ~80% complete after this review (finding 2). Remainder: extend the H/E table to the ai_19/ai_9 groups and confirm which group index c78 actually draws (`anim_index` from `chrlvDistanceToChrRelated`, distance-bucketed over the 32-entry `ptr_pistol_firing_animation_groups[]`). Effort: ~1–2 h, no new tooling.
4. **ai_19 / ai_9.** Different anim groups (kneel/crouched tables; probes pinned at frames 30.00/23.00 — neither matches a group1 row). Fixing ai_22's root cause implies theirs only if the cause is shared (boundary settling would be; per-group data is not). Step 4's "re-verify first" must be an explicit **per-entry H/E table + boundary check + forced-repro sweep**, and each watchdog entry excised on its own evidence — not on ai_22's result. Depot ai_12 (clean) and the untested ai_13/14/15 (LOW-MED) stay out of scope for excision.
5. **Emulator A/B.** Cutting the trial series is correct (given identical data it can only re-test "does the derail happen on N64," which the spec's +3.5 s fire signature already answers — the monologue-completion path takes ~22 s, so a +3.5 s shot is itself evidence the derail landed). Preserve `GE_RSEED_LV` + DET logging armed as the fallback oracle for an upstream stream hunt (cost: one memory-viewer read + an env var); do not burn it until the boundary hypothesis fails.
6. **Constraint compliance.** All steps port-side; no Rule-2 exposure *as written*. Two cautions: (a) watchdog excision changes shipped v0.4.0 behavior mid-release — keep the watchdog on `release/v0.4.0` until step 3 passes and land the excision post-release (M-192 item 7 precedent); (b) pre-decide the fix bucket for the boundary root cause: if it turns out to be FMA contraction / float-rounding in compiled game code, a **build-flag** change (CMakeLists, e.g. `-ffp-contract=off`) is port-side, but any arithmetic edit in `src/game/model.c` requires Rule-2 sign-off.
**Corrected plan sketch:** (1) finish the static H/E table for all three confirmed entries' groups + c78's group index — ~1–2 h; (2) env-gated probe: full-precision (`%f`/bit-exact) `animframe1` + fire-gate operands (`shoot_start`, `phi_f20`, `unk38[i]`, `unk3a[i]`, `unk44`) in a frozen forced-repro run, plus an A1/float pass over `modelTickAnim`/`modelConstrainOrWrapAnimFrame` — ~half a day; (3) decision gate: boundary divergence found → port-side fix + per-entry conformance (static table + existing ai_19/ai_9 forced sweeps; c78 headless materialization is NOT available today — d320repro cannot spawn the proximity-spawned c78, so a "headless forced-derail seed sweep" of the Trevelyan beat as written in plan step 3 is infeasible without a ~1-day harness extension; cost it or drop it) → excision post-release; boundary hypothesis dead → upstream hunt with `GE_RSEED_LV` fallback (user sign-off for the single bounded emulator artifact); (4) D318/D320/D321 rewrite only on pass, per-entry.
**Status:** review recorded; plan amended per above. No code changed. Cross-ref: D318 (mechanism + 2026-09-22 correction + probe evidence), D320 (sweep + forced repros), D321 (trigger chain), D34 (animationtable ROM-copy data path), D32 (ModelAnimation u32 fields), D94 (firing-table pointer arithmetic), porting-notes §A1 (ruled out for this chain by construction).
## D329 — The D318/D320 guard "anim pin" is a tick-granularity divergence, not a platform-invariant lock: it exists only at 1 sim tick per rendered frame (the port at 60 fps); at ≥2 ticks/frame (GE's normal N64 regime) it cannot persist (lockstep A/B, 2026-09-23)
**Question (M-199 step 2 decision gate).** Is the pinned ACT_ATTACK state behind D318 (Facility ai_22), D320 ai_19 (Facility) and ai_9 (Control) a lock that would also happen on N64, or a platform divergence? M-199 expected the former. It is the latter.
**Mechanism (source, model.c).** `modelTickAnim` accumulates `frame += playspeed * speed` once per tick inside its `numticks` loop, then calls `modelSetAnimFrame2WithChrStuff(model, animframe1, frame, …)`**once per rendered frame**. That function only writes `framea`/`frameb` inside its per-frame loop, which runs only when `floor(frame)` crosses an integer boundary; its tail re-emits the stored pair and, when `vb == va`, sets `animframe1 = va` and discards all sub-frame progress. During the aim hold the endframe clamp (`modelConstrainOrWrapAnimFrame`: `frame = ceil(endframe)`) leaves `framea == frameb == H`. When `actor_fire_or_aim_at_target_update` then raises the endframe (`chrlvAttackActionRelated`, endframe only, no `modelSetAnimation`), the model can move again **only if one call advances by ≥ 1.0 frame**. Guard attack speed is 0.5–0.8 (`chrlvGetGuard007SpeedRating(0.5, 0.8)`; the fire gate also re-sets 0.5 via `modelSetAnimSpeed`). So:
- `numticks` = `g_ClockTimer` = 1: advance 0.5–0.8 < 1, no crossing, stale pair re-emitted, pinned for ever.
- `numticks` ≥ 2: advance ≥ 1.0, crossing, loop runs, `frameb` becomes `framea+1`, the anim plays on. **A single ≥2-tick frame breaks any such pin.**
`g_ClockTimer = speedgraphframes` = 60 Hz ticks elapsed since the last rendered frame (`lvlManageMpGame`, `waitForNextFrame`). The port renders at 60 fps → 1 (D193 telemetry's "healthy" 60 render / 60 sim). GE on N64 runs gameplay at roughly 20–30 fps → 2–3. For the pin to survive on N64 the console would have to hold a steady 60 fps on every frame from the update onward, with guards on screen. This matches the user's spec (no such freeze on N64) without any PRNG, variant-lottery or reachability argument.
**Evidence (lockstep A/B, WSL Linux build).** Harness: `GE_D318B_CLK=N` in the TEMP probe `port/src/d318bprobe.c` rewrites `g_ClockTimer`/`g_GlobalTimer`/`g_GlobalTimerDelta` in the existing lv.c hook (before any chr tick), so every rendered frame is exactly N ticks regardless of host speed. The probe log's `clk` field confirms it: 100% `clk=N` in every run. Same forced repros as D320 (`GE_D320R`), same PRNG path (the first variant drawn matches across N).
| Run | ticks/frame | Post-update state | Watchdog |
|---|---|---|---|
| Facility ai_19 (c5) | 1 | `fr=19` exact, `fa=fb=19`, `ef=-1`, `spd=0.5`, frozen for ever | confirmed t=1561, then re-pin rescue every ~600 ticks (6×) |
1. **Not "clamp at endframe".** After the update the endframe is often *unbounded* (`ef=-1`: rifle variants with `recoil_start = end_frame = -1`). The pin is purely the stale `va == vb` pair from the pre-update clamp, which only a ≥1.0-frame step (or a `modelSetAnimation` re-init) clears.
2. **Weapon class does not decide it.** Both pinned guards carry an **automatic** weapon (`wr=3`). While pinned at frame == `shoot_start` the per-hand fire gate (`shoot_start <= frame < shoot_end`) is open, so the gun **fires every tick** (`unk44` = current tick). The guard "fires for ever and never finishes the animation"; it is not "never fires". The escape conditions all read false (`efle=0`: endframe unbounded; `trm=0`: `recoil_end < 0`; unk31 re-arm path unreachable). "Semi-auto with E>H = deterministic lock on any platform" (M-199 plan) is refuted as the explanation. At ≥2 ticks/frame no escape path is needed, because the model just plays on.
3. **The M-199 "sub-frame settling at the fire-gate boundary" suspect is also refuted** (as the probe header predicted). Frozen values are exact integers and the gate is open, not closed.
4. **Earlier WSL wall-clock repros are not calibrated evidence.** WSLg renders this game at ~5 fps, so wall-clock runs mix clk 1–6. Whether they pinned depended on host speed. D320's "PIN CONFIRMED" and ai_9's "~2/3 of seed variants" rates came from such runs. The first run of this session (42 detections, zero pins) is the same artifact in the other direction. `GE_DETERM=1` does **not** hold 1 tick/frame either (clock advances per VI-retrace request; measured clk 1–6), so use `GE_D318B_CLK` for any granularity-sensitive repro.
5. **D318's variant split still holds, but only at clk=1.** E == H variants escape via the endframe re-init; E > H variants pin. That is where the ~2/3 PC rate comes from. At clk ≥ 2 every variant escapes (e.g. ai_22 hip-slow H=19/E=24 at speed 0.68: 19 + 1.36 crosses, plays to 24, normal re-init fires).
**Float / A1 pass (M-199 task 3).** Nothing in the pin path is precision-sensitive: `floorFloatToInt`/`ceilFloatToInt` are truncation-based on both targets; the stored pair and endframe comparisons are integer-valued floats; the build uses no `-march`/`-ffast-math` (x86-64 baseline SSE2 single precision, no FMA contraction), and the R4300 has no fused multiply-add either. `Model`, `act_attack` and the firing tables are accessed by named fields (A1 ruled out, M-199). The divergence is purely the number of ticks folded into one `modelTickAnim` call.
**Scope (implication, not yet swept).** The trigger is generic: any chr whose non-looping anim is clamped (`framea == frameb`) at an endframe that is then raised without a `modelSetAnimation`, at speed < 1. D320's "combat loops are likely safe" inference and the Depot ai_10 one-off freeze (c0 pinned at 33.00, atk=0x1) should be re-read in this light. The three-entry watchdog covers only the known scripted cases.
**Classification.** A genuine port behavioural divergence caused by frame pacing (the port sim runs finer tick granularity per render than the N64 ever did), acting on byte-matched, non-associative game code. Not a data or ABI issue, not in the game logic's inputs. The N64-side claim "GE runs 2–3 ticks/frame in these scenes" rests on GE's well-known frame rate, not an in-session measurement. It is not needed for the PC-side A/B, but it is what connects the result to the user's spec.
- (a) **Keep the table-driven watchdog** (ships in v0.4.0). Correct for the three known entries; blind to other instances.
- (b) **Generic port-side pin breaker**: detect a chr model in the stale state (`framea == frameb`, non-looping, raised/unbounded endframe above the frame, speed > 0) and nudge it the way a 2-tick frame would. Port-side, but it writes game state from a hook (watchdog class).
- (c) **Rule-2 edit in `modelSetAnimFrame2WithChrStuff`**: keep sub-frame progress when the per-frame loop does not run. This is the standard "high-frame-rate fix" pattern. It changes behaviour only where N64 would have taken ≥2 ticks. Needs sign-off.
- Workaround available now (expected, not verified): `Video.FpsCap=30` gives 2 ticks/frame, which by the above makes the pin impossible.
**Fix applied (2026-09-23, RULE-2-SIGNOFF: user approved option (c)).** `src/game/model.c``modelSetAnimFrame2WithChrStuff`, `#ifdef PORT`: when the per-frame loop did not run, progress was requested, and `vb == va`, re-derive `frameb = constrain(va±1)` against the CURRENT endframe (as `modelSetAnimFrame` does); applied only if that next frame is unclamped. Genuine holds, anim ends and looping wraps are untouched. Env: `GE_D329=0` disables (A/B), `GE_D329LOG=1` logs triggers (cached getenv). **Verified (WSL, lockstep):** clk=1 with watchdog off (`GE_D318W=0`): Facility ai_19 and Control ai_9 no longer pin (frame 19→19.5→20…, 0.5/tick = the N64 clk=2 trajectory; aim→fire→re-aim cycle ≈192 ticks vs ≈195 at clk=2/3); authored holds intact. Trigger census: clk=1 Facility, 9 triggers, all on the repro guard, one per attack update; **clk=2: 0 triggers** over ~5300 ticks (Facility) and ~3600 (Control), so the fix is inert in the N64 regime. **Windows-native validation (2026-09-23):** Facility forced repro, native exe, `GE_D318B_CLK=1`, watchdog off: 48 post-update fire episodes, **0 pinned**, 23 D329 triggers. Bunker 1 golden (640x480 pinned, direct launch): REGRESSION with the fix ON (worst dmean 113.6) and equally with it OFF (`GE_D329=0`, 126.4), so it predates this fix (stale golden; re-baseline already owed). **Interactive playtest (2026-09-23, user, native Windows, shipping config + GE_D329LOG):** Ourumov reacts with N64-matching timing (user-confirmed on screen). Log: 4 D329 triggers (four guards, same tick, stale-pin state, released at 0.5/tick), 3 watchdog detections that cleared on their own, 0 watchdog rescues, no crash (`scratch/d329_playtest.log`).
**Status:** FIXED + playtest-verified (2026-09-23). Next: demote the D318 watchdog to detect-and-log post-release (M-192 item 7), then remove the TEMP probes (d318bprobe, d320repro). Watchdog stays shipped on `release/v0.4.0`; plan to demote it to detect-and-log after playtests. Cross-ref: D318, D320, D321, M-199, D155, D186/D193.
@@ -29,6 +29,7 @@ good that you are looking at one of these.
- [D4. N64 "interrupts off" must be a real lock on PC](#d4-n64-interrupts-off-is-not-free-on-pc--it-must-be-a-real-lock)
- [D5. Loop bounds that assume linker adjacency of two globals](#d5-loop-bounds-that-assume-linker-adjacency-of-two-file-scope-globals)
- [D12. Collapsing an LOD binding to tile 0 breaks a DETAIL binding](#d12-collapse-an-lod-binding-to-tile-0-is-wrong-for-a-two-texture-detail-binding-d236)
- [D13. Per-render accumulation is not associative (1 vs 2–3 ticks/frame)](#d13-per-render-accumulation-is-not-associative-the-ports-1-tickframe-can-reach-states-the-n64s-23-ticksframe-never-did-d329)
- [E. Process / method notes](#e-process--method-notes)
## A. Pointer-width struct growth (32→64): the dominant class
@@ -1207,6 +1208,14 @@ When a user reports z-fighting, do NOT assume the software renderer has less dep
**Probe technique that worked (vs. three variants that didn't):** detecting z-fighting from emitted triangles requires filtering out (a) tessellation gradients — adjacent tris of one surface have near-equal depths by construction — and (b) camera-motion coincidences — as the camera moves, unrelated world points pass through the same screen cell and their depths happen to sit close. What survives both: a coarse screen-cell grid keeping a short per-cell depth history, flagging only **non-monotonic** (a-b-a) oscillation within a small tolerance that **recurs many times at the same cell**. Monotonic drift = gradient; one-off close pairs = motion artifact; sustained recurrence = a real fight. `GE_ZF` in `port/fast3d/gfx_pc.cpp` implements this (`GE_ZFTOL` tolerance, `GE_ZFBOX` region filter).
## D13. Per-render accumulation is not associative: the port's 1 tick/frame can reach states the N64's 2–3 ticks/frame never did (D329)
GE on N64 renders gameplay at ~20–30 fps, so `g_ClockTimer` (= `speedgraphframes`, 60 Hz ticks per rendered frame) is normally 2–3. The port at 60 fps gives 1. Code that folds `numticks` ticks into **one** downstream call is not equivalent at different granularities. `modelTickAnim` sums `playspeed*speed` over the ticks, then makes one `modelSetAnimFrame2WithChrStuff` call, which only moves `framea`/`frameb` when `floor(frame)` crosses an integer. At speed < 1 and 1 tick/frame a clamped (`framea == frameb`) non-looping anim never crosses, so it stays pinned even after its endframe is raised. At 2+ ticks it crosses on the next frame. Result: permanent guard freezes on PC (D318/D320) that the console could not sustain.
- **Tell:** a state that holds forever at 60 fps, is frame-exact (integer frame, `fa == fb`), and has a per-tick step < 1.0 unit.
- **Repro correctly:** force the granularity with `GE_D318B_CLK=N` (TEMP probe, lockstep N ticks/frame) and A/B N=1 vs N=2. Wall-clock runs on a slow host (WSLg renders ~5 fps → mixed clk 1–6) and `GE_DETERM=1` (advances per VI-retrace request, also mixed clk) both give uncalibrated results.
- **Where else to look:** any `for/while (numticks)` accumulator followed by a single threshold/floor consumer; D193 (AI locomotion rate) and D243 (cutscene `numticks` bursts) touched the same axis from the other side.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.