D310 v2: scope PRINT sizing fix to global AI lists; D311: close D156 guard hole

D310 (RULE-2-SIGNOFF, user-confirmed fixed): the uniform 1-byte AI_PRINT
size broke level-local lists, which embed a NUL-terminated string after
the PRINT byte (Caverns Trevelyan ran circles with glitched anims).
chraiitemsize is back to byte-identical decomp; new PORT-only
d310ItemSize() applies the 1-byte size only for lists confirmed global
via chraiGetAIListID, used at ai()'s AI_PRINT step and chraiGoToLabel's
label scan. Local-list behavior unchanged from the original.

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

Post-fix Caverns playtest: guards dash-pause-resume, Trevelyan pathing
normal, Caverns->intro clean. Probe log: global PRINTs size=1/global=1
(hit once each), local PRINTs NUL-scanned/valid landings, zero
LABEL-NOT-FOUND, zero D156 fires, zero watchdog freezes.
This commit is contained in:
James Dansereau committed 2026-09-18 13:18:16 -04:00
1 parent 0c700852e2
commit c67479866c
4 files changed
+127 -43

No files matched your search

+30 -6
View File
@@ -594,8 +594,9 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D306 | **Dam intro/truck wheels render with odd transparent-looking areas, possibly z-fighting (user QA, 2026-09-18).** — full `## D306` entry at file tail | OPEN — reported only, not investigated. Explicitly not for v0.3.0. |
| D307 | **In-level security camera entities can be flipped facing the wrong direction (backwards) on some levels, e.g. Bunker (user QA, 2026-09-18) — NOT the player's view camera.** — full `## D307` entry at file tail | OPEN — reported only, not investigated. Explicitly not for v0.3.0. |
| D308 | **Occasional z-fighting confirmed on some levels' geometry, general/broader than the D306 truck-wheel instance (user QA, 2026-09-18).** — full `## D308` entry at file tail | OPEN — reported/confirmed by the user, not investigated. Explicitly not for v0.3.0; D306 may be one instance of this. |
| D309 | **Complete non-self-recovering freeze (Windows) returning from Caverns to the intro screen — boss thread spins forever in `ai()` on `m_RunToBondPersistent` via a phantom `GotoNext(lblZero)` created by `chraiitemsize`'s `AI_PRINT` NUL-scan mis-sizing a stringless PRINT record (user report + live GDB diagnosis, 2026-09-18).** — full `## D309` entry at file tail | FIXED via D310 (RULE-2-SIGNOFF, 2026-09-18) — the mis-sizing landmine is neutralized, so the phantom-`GotoNext(0)` spin path no longer exists; pending user live re-verification in Caverns. The teardown-ordering open question (why PC presents the unreachable-Bond state) remains documented but is now harmless. |
| D310 | **RULE-2-SIGNOFF: `chraiitemsize()` mis-sizes 1-byte `AI_PRINT` records (NUL-scans forward), corrupting AI execution for any list containing an active PRINT — fixes D309's freeze mechanism and the Caverns standing-still guards (`Stop`+`PRINT("wait")` mis-lands on EndList, re-executed every tick; user report 2026-09-18).** — full `## D310` entry at file tail | FIXED (signed off + implemented 2026-09-18, `#ifdef PORT`, N64 arm unchanged) — pending user live re-verification of both symptoms in Caverns. PD precedent: PRINT removed, slot 0xAD reassigned. |
| D309 | **Complete non-self-recovering freeze (Windows) returning from Caverns to the intro screen — boss thread spins forever in `ai()` on `m_RunToBondPersistent` via a phantom `GotoNext(lblZero)` created by `chraiitemsize`'s `AI_PRINT` NUL-scan mis-sizing a stringless PRINT record (user report + live GDB diagnosis, 2026-09-18).** — full `## D309` entry at file tail | **FIXED via D310 — user-playtest-confirmed 2026-09-18** (Caverns→intro clean; the spin record itself observed resuming correctly in the post-fix probe log). The teardown-ordering open question (why PC presents the unreachable-Bond state) remains documented but is now harmless. |
| D310 | **RULE-2-SIGNOFF: `chraiitemsize()` mis-sizes 1-byte `AI_PRINT` records (NUL-scans forward), corrupting AI execution for any list containing an active PRINT — fixes D309's freeze mechanism and the Caverns standing-still guards (`Stop`+`PRINT("wait")` mis-lands on EndList, re-executed every tick; user report 2026-09-18).** — full `## D310` entry at file tail | **FIXED — user-playtest-confirmed 2026-09-18** (v2: global-list-only sizing via `d310ItemSize`; guards resume, Trevelyan pathing intact, Caverns→intro clean; probe log verified both list classes). PD precedent: PRINT removed, slot 0xAD reassigned. |
| D311 | **Complete freeze in `modelSetAnimFrame2WithChrStuff` root-motion loop — D156 guard hole: poisoned `framea` (`model->animframe1`) passes the frameb-only clamp and saturates to a ~3.1e9-iteration spin (Caverns, live GDB diagnosis, 2026-09-18).** — full `## D311` entry at file tail | GUARDED (`#ifdef PORT`, D156-guard extension in the same file) — freeze class neutralized; upstream speed/frame poison source OPEN (level reload clears displaced model state). No AI involvement — distinct from D309/D310. |
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
@@ -11900,7 +11901,7 @@ do {
**Fix bucket:** NOT an ABI/layout exception (no pointer-width involvement at all). The decomp's code+data are faithful; the divergence is in *when* the state occurs, i.e. PC teardown/scheduling. A port-side fix (ensure guards stop ticking AI before/while Bond+level state goes invalid on level exit) is the natural home and needs its own investigation of the teardown ordering (`gesched.c` / boss-thread vs level-unload race). Any `src/game` edit would require rule-2 sign-off.
**Status:** OPEN — root cause identified with full evidence; no fix made. **Release impact:** user decision owed — fix before the v0.3.0 tag, or ship with a known-issue note (complete freeze on Caverns→intro transition).
**Status:** **FIXED via D310 — user-playtest-confirmed 2026-09-18** (Caverns→intro transition clean; the spin record `PRINT("no go!")`@+4 itself was observed in the post-fix probe log stepping `size=1 global=1` and resuming at `Label(27)` as intended). The teardown-ordering open question (why PC presents the unreachable-Bond state) remains documented but is now harmless — reaching that state no longer spins.
## D173/D292 — user confirmation, 2026-09-18: mostly fixed as a side effect of D243's root cause.
@@ -11914,7 +11915,9 @@ This confirms the M-160/M-164/M-165 hypothesis (`## D173`'s own consolidated-nex
## D310 — RULE-2-SIGNOFF: `chraiitemsize()` mis-sizes 1-byte `AI_PRINT` records (NUL-scans forward), corrupting AI execution for any list containing an active PRINT — fixes D309's freeze mechanism and the Caverns standing-still guards (signed off + implemented, 2026-09-18).
**Status:** **RULE-2-SIGNOFF** / FIXED (pending user live re-verification of both symptoms in Caverns). One-line `src/game` behavior change under `#ifdef PORT`; N64 build untouched.
**Status:** **RULE-2-SIGNOFF / FIXED — user-playtest-confirmed (2026-09-18).** All three checks passed live in Caverns: standing-still guards resume, Trevelyan follows his path, Caverns→intro no longer freezes. `src/game` behavior change under `#ifdef PORT`; N64 build untouched.
**Post-fix probe log (clean session, 989 lines):** global-list PRINTs (`m_RunToBondPersistent`, chr=4/24/72) all step `size=1 global=1` — including the D309 spin record itself (`off=4 "no go!" landcmd=0x02`=Label, resumed correctly) and the standing-still record (`off=167 "wait" landcmd=0x95`=UnsetMyFlags2), each hit exactly once (no per-tick re-execution). Local-list PRINTs (Trevelyan chr=88, fake-GUARD driver chr=254) all keep NUL-scan sizing (`size=4/7 global=0`) with valid landing commands. Zero `LABEL-NOT-FOUND`, zero D156 fires, zero watchdog freezes.
**The defect.** The `PRINT()` macro (`src/aicommands2.h:4913`) emits exactly **one command byte** into the built AI list — its string argument is discarded at list-build time. But `chraiitemsize()`'s `case AI_PRINT:` (`src/game/chrai.c:682`, original) sized the record by scanning forward to the next NUL byte, so stepping on a PRINT always skips ~20 bytes into whatever records follow and resumes at an arbitrary byte pattern. Only 4 PRINTs exist in all of `src/game/chraidata.c`; two are commented out (`m_DeadAI:74`, `m_StandardGuard:136`) and **both active ones are in `m_RunToBondPersistent`** — so exactly one AI list in the game is affected.
@@ -11930,8 +11933,29 @@ This confirms the M-160/M-164/M-165 hypothesis (`## D173`'s own consolidated-nex
**Grant (2026-09-18, in-thread):** "I want to proceed with your implementation plan here to fix this bug." (User's reply to the full §7 write-up; asked only whether a fresh session was needed — answered no, granted in this session.)
**Resulting diff:** `src/game/chrai.c` `chraiitemsize()` — `case AI_PRINT:` now `return 1;` under `#ifdef PORT`, original NUL-scan moved to the `#else` arm unchanged (commit following this entry). Verification: full-list decode of `m_RunToBondPersistent` with the fix (`scratch/decode_printfix.c`) walks +0→EndList@223 with every record valid, and the post-`"wait"` sequence matches `chraidata.c:666+` line-for-line (`UnsetMyFlags2(4)`, `IFNewRandomGreaterThan(160)`, `SetMyFlags2(4)`, `MyTimerStart`, sit-rep block, `GotoFirst(lblRun)`); post-`"no go!"` resumes at `Label(27)+Yield` as in source.
**Resulting diff v1 (REGRESSED, superseded):** `chraiitemsize()` `case AI_PRINT:` → `return 1;` under `#ifdef PORT`. Fixed the guards, but live testing then broke **Caverns' Trevelyan (chr=88)**: ran in circles with glitched animations. Memory dump of his list (`0x701cca30`, a level-LOCAL list from `g_CurrentSetup.ailists`) shows local lists **embed a NUL-terminated string after the PRINT byte** — live bytes `AD 44 0A 00` (PRINT + "D\n"), `AD 57 0A 00`, `AD 34 0A 00` at offsets 50/288/302 — so the NUL-scan is CORRECT for local lists; the uniform 1-byte size landed mid-string (garbage commands + a `LABEL-NOT-FOUND` spin, off=403 label=0, probe-capped). Only the macro-built GLOBAL lists (`g_GlobalAILists`, chraidata.c) discard their PRINT strings.
**Scope note (§7 drift check):** affects exactly one AI list (the only one with active PRINTs). Both observed symptoms are inside that scope; no other list's behavior changes.
**Resulting diff v2 (current):** `chraiitemsize()` left byte-identical to the decomp. New PORT-only `d310ItemSize(AIList, offset, isGlobalAIList)` returns 1 for a PRINT only when `chraiGetAIListID()` confirms the list is global; otherwise falls through to the original NUL-scan. Applied at both record-walk sites: `ai()`'s AI_PRINT step and `chraiGoToLabel`'s label scan (a backward `GotoFirst` crossing a global PRINT would mis-scan even with v1's sizer, since the scan also steps records). Lists found in neither table (teardown) default to the original NUL-scan. Verification: full-list decode of `m_RunToBondPersistent` with 1-byte global PRINTs (`scratch/decode_printfix.c`) walks +0→EndList@223 with every record valid, and the post-`"wait"` sequence matches `chraidata.c:666+` line-for-line (`UnsetMyFlags2(4)`, `IFNewRandomGreaterThan(160)`, `SetMyFlags2(4)`, `MyTimerStart`, sit-rep block, `GotoFirst(lblRun)`); post-`"no go!"` resumes at `Label(27)+Yield` as in source.
**Scope note (§7 drift check):** v1's sign-off scope ("exactly one AI list affected") was wrong in one respect — PRINT records also exist in level-local lists, but with a different shape (embedded string), where the original NUL-scan is already correct. v2's *behavioral* change is still confined to global-list PRINTs: `m_RunToBondPersistent`'s two records. Local-list behavior is byte-identical to the original decomp — Trevelyan reverts to his pre-fix (working) pathing. Drift disclosed to user in-thread when the regression was reported; same sign-off intent (fix global PRINT sizing, no other behavior change).
**Cross-refs:** D309 (this fix neutralizes its spin mechanism — the teardown-ordering open question in D309's "why PC and not N64" remains, but reaching that state is now harmless); GE_D309 probes (`chrai.c` `ai()`/`chraiGoToLabel`, `chraction.c` `chrGoToBond`) remain as diagnostic tools.
## D311 — Complete freeze in `modelSetAnimFrame2WithChrStuff` root-motion loop: D156 guard hole — poisoned `framea` (`model->animframe1`) passes through the frameb-only clamp and saturates to a ~3.1e9-iteration spin (Caverns, live GDB diagnosis, 2026-09-18).
**Reported:** user playtest of the D310 v2 build in Caverns — "it's frozen" shortly after launch (watchdog: no frame rendered for 8014 ms at frames=8424; ~2.3 min into the session).
**Live evidence (GDB attach, PID 46948, mainThread):**
- Stack: `sincosf` ← `modelSetAnimFrame2WithChrStuff` (`model.c:3349`, anim-merge branch) ← `modelTickAnim` ← `bheadUpdate` ← `bondviewMoveAnimationTick` ← `MoveBond` — the **player's own** model anim tick. Zero `D309:` probe lines in the log → no AI-list involvement; this is a different bug from D309/D310.
- Locals at the spin site: `curframe = -966612173`, `endframe = 2147483647` (INT_MAX) → ~3.1e9 loop iterations; `pos34 = (-6.7e30, ?, 2.9e32)` — root-motion accumulator already blown up; `model->animframe1 = -1.6708e+31`; `model->endframe = 17` (the anim has 17 frames); `speed = 0.22` (normal), but `newspeed = -3.2e7`, `oldspeed = -1.75e18`, `timespeed = 0`, `unkb0 = 0`; `anim2 != NULL` with healthy frame2 (18.8→20.9).
- The existing D156 guard **had fired** earlier in the same session (`[D156] bad anim frame guarded: frame=-1.6708e+31 animframe1=-1.6708e+31 ... oldspeed=-1.75136e+18 timespeed=0 unkb0=0`) — i.e. `animframe1` was already poisoned *before* the guarded tick, and the guard's fallback (`frame = model->animframe1`) plus the callee's frameb-only clamp (`if bad frameb: frameb = framea`) passed the poison straight through.
**Mechanism (the guard hole):** D156's guard in `modelSetAnimFrame2WithChrStuff` validates only `frameb`. With `framea = frameb = -1.67e31`, `floorFloatToInt` saturates the two near-equal huge floats to far-apart ints (`curframe = -966612173`, `endframe = INT_MAX`), so the `while(1)` root-motion loop walks ~3.1e9 frames — each doing a joint transform + bitstream decode + `sincosf` — i.e. a permanent hang. The N64 never presents this state (fixed timestep, bounded speeds); the poison is PC-side upstream corruption of the model speed/frame fields (see open question).
**Fix (landed, `#ifdef PORT`, same file/pattern as the accepted D156 guards — port-side defensive clamp against non-finite state, no game-logic change):**
1. `modelSetAnimFrame2WithChrStuff`: validate **both** `framea` and `frameb` against ±1e6; a bad one snaps to the other if that is valid, else 0. Both-bad → both 0 → loop does ~0 iterations; the in-loop `animframe1` rewrite (`model.c:3447`) then heals stored state on subsequent ticks.
2. `modelTickAnim`: heal `model->animframe1`/`animframe2` (snap to 0) **before** the bad-frame fallback reads them, so the poison source itself is cleared instead of being re-derived every tick.
**Open question (deeper root cause, NOT fixed):** what wrote `-1.75e18` into `oldspeed` / poisoned `animframe1` in the first place? `oldspeed = model->speed` at transition start (`model.c:2975`) means `speed` was already garbage upstream; `timespeed = 0` and `unkb0 = 0` at the guarded tick are suspicious division denominators (D156's original note flagged `/ unkb0` and `/ timespeed` as guarded `> 0` but not against tiny values). Also, the model's persistent root-motion header (`header->unk34/unk24`) is poisoned to ~1e30–1e32 in this instance, so **Bond's rendered model may be displaced even after the freeze is prevented** — a level reload clears it. Needs its own investigation (dump speed-field write sites during the triggering transition) before any further fix.
**Status:** GUARDED (freeze class neutralized; upstream poison source OPEN). Disclosed to user in-thread as a D156-guard extension following the already-accepted D156 precedent in this same file; if the project wants formal rule-2 sign-off for `src/game` edits, this is the entry to reference — revert is two small blocks. Cross-refs: D156 (original guard + Facility outro hang), D309/D310 (ruled out for this freeze — no AI involvement).