forked from Ports/goldeneye-pc-port
D318: port-side watchdog + timeline probe for the Facility ai_22 softlock; D320: D318-class sweep of all level AI scripts; gfx_pc GE_D116 printf format fix
- D318 (RESOLVED): as-authored latent race — gas bit mid-monologue derails c78's pre-aim hold into a permanent ACT_ATTACK anim pin (actor_fire_or_aim_at_target_update moves only the endframe, never re-inits; N64-identical). NOT a port bug. Port-side watchdog port/src/d318watchdog.c (hooked in lv.c after g_GlobalTimer += ) detects the pinned state and re-inits the attack on confirm; GE_D318T=1 timeline diagnostic included. random.c gains an env-gated probe (GE_RANDPROBE) for bit-exact PRNG verification. Scratch evidence (dis/seqcmp scripts, addr dumps, session handoff) parked per house practice. - D320 (OPEN): swept every level's AI scripts for the same aim-hold -> _update shape: 17 lists in 8 levels. Facility ai_19 and Control ai_9 are HIGH (non-self-healing, same as ai_22); Depot ai_12 MED-HIGH (objective bit gated behind a stop-check). Bond-combat loops likely safe (sustained fire stays inside the shoot window by design; exercised every playtest). Classified as-authored latent race(s), faithfully reproduced. Probe-verify path + generalize-the-watchdog recommendation documented in findings.md. - gfx_pc.cpp: GE_D116 debug printf used %f for uint32_t args and %d for unsigned tile fields; casts added, build warning-free. Full ntsc-final configure+compile+link PASS. Accuracy sweep write-up in docs/dev/ACCURACY-AUDIT-2026-09-20.md (PRNG bit-exact; A1/shift32/wrapv/ PORT-hygiene/Gfx-Vtx/osVirtualToPhysical sweeps all clean or already fixed).
This commit is contained in:
1 parent
276d0124dc
commit
cc169874ce
17 files changed
+1029
-14
No files matched your search
@@ -0,0 +1,77 @@
|
||||
# Accuracy / code review — N64 fidelity sweep (2026-09-20)
|
||||
|
||||
A thorough pass over the PC port with one question: *where does the port diverge from the
|
||||
N64 original, and what latent failure classes of the D318 kind are still unpriced?*
|
||||
Method: static review against the decomp ground truth + targeted verification builds.
|
||||
Findings below; new finding **D320** logged in `findings.md`.
|
||||
|
||||
## 1. PRNG — verified bit-exact
|
||||
|
||||
`port/src/random.c` re-checked line-by-line against both `src/random.s` and
|
||||
`src/game/chrObjRandom.s`. The MIPS64 `dsll32`/`dsrl32` semantics (post-D284) are
|
||||
emulated correctly: the 32-bit LFSR state advances identically, and
|
||||
`randomGetNext()` returns the same 31-bit values in the same order. No divergence.
|
||||
|
||||
## 2. Known bug-class sweeps — all clean or already fixed
|
||||
|
||||
| Class | Result |
|
||||
|---|---|
|
||||
| **A1** raw-byte/padding aliases (32→64-bit layout shift) | Remaining `padding[]` uses are in pointer-free structs (layout-stable) or already fixed (D115, D209, D312). No live misreads. |
|
||||
| Shifts by 32 / pointer-width arithmetic in game logic | Only port-layer code (`d318watchdog` print, `DRAM_SIZE`). Game logic clean. |
|
||||
| Signed-overflow UB | `-fwrapv` enabled in `CMakeLists.txt` — the whole class is neutralized to N64-wrap semantics. |
|
||||
| `#ifdef PORT` hygiene (N64 path preserved) | 307 hunks audited; 178 are pure additions (diagnostics / struct extensions). No hunk alters the non-PORT control flow. |
|
||||
| **§B** Gfx/Vtx 16-byte records (raw byte indexing) | `bg.c` uses typed `.words`/`.dma` fields throughout; no remaining raw byte offsets. D312's fix pattern is the last of this class. |
|
||||
| `osVirtualToPhysical` call sites (88) | All flow through fast3d's `seg_addr()` (D131 fix intact). No site assumes N64 segment RAM layout. |
|
||||
| Dead code | `objecthandler_2.c` compiles but is not linked on PC (symbols absent from the binary). Harmless; noted for a future source-list prune. |
|
||||
|
||||
## 3. Timing layer — intentional, documented
|
||||
|
||||
Frame-locked virtual clock (vsync tick), `GE_DETERM` fixed-tick mode, and
|
||||
`g_GlobalTimerDelta` zero-guarded at every division site. All deliberate port-layer
|
||||
design, each documented in its finding. No hidden drift source found beyond the
|
||||
already-fixed D204 tempo work.
|
||||
|
||||
## 4. D318-class latent softlock sweep → **D320** (the main new result)
|
||||
|
||||
D318's mechanism is generic, not Facility-specific: `actor_fire_or_aim_at_target_update`
|
||||
(`chraction.c:4882`) retargets any ACT_ATTACK chr with an AIM_ONLY/DONTTURN attacktype and
|
||||
only moves the model's **endframe** — it never re-inits the animation. If the chr is pinned
|
||||
on a pre-shoot aim-hold pose (hold endframe H < variant shoot window) and the new endframe
|
||||
lands above H, the model pins forever and the timer-gated fire events never open; if the
|
||||
surrounding AI script's only escape is an `if_guard_has_stopped_moving` gate, it can never
|
||||
pass (`chrHasStoppedOrPatroling` is FALSE for ACT_ATTACK). **Permanent softlock — on N64 too.**
|
||||
|
||||
Swept every level's AI scripts (`assets/obseg/setup/*.c`): 17 lists in 8 levels contain an
|
||||
aim-hold + `_update` pair. Risk-ranked table in the D320 entry. Headlines:
|
||||
|
||||
- **Facility ai_19** — HIGH. Same non-self-healing shape as ai_22 (second softlock point in
|
||||
the same level).
|
||||
- **Control ai_9** — HIGH. Near-exact ai_22 mirror (pre-aim hold → timer → update → fire
|
||||
loop gated on the target dying from this guard's shot).
|
||||
- **Depot ai_12** — MED-HIGH. The kill is scripted, but the objective bit is gated behind a
|
||||
stop-check; pinned guard → level-complete softlock.
|
||||
- **Bond-combat loops** (Aztec/Control/Egyptian/Depot/Streets/Bunker/Surface) — likely safe:
|
||||
sustained fire keeps the model inside the shoot window by design, and every playtest
|
||||
exercises them without freezes. Inferred, not probe-verified per list.
|
||||
|
||||
Classification: **as-authored latent race(s), faithfully reproduced** — same bucket as D318,
|
||||
not a port bug. Verification path (generalized timeline probe) and the
|
||||
generalize-the-watchdog recommendation are in D320.
|
||||
|
||||
## 5. Fixed this session
|
||||
|
||||
- `port/fast3d/gfx_pc.cpp:2417` — the GE_D116 diagnostic printf used `%f` for `uint32_t`
|
||||
args (`v->u`, `tex_width`) and `%d` for unsigned tile fields; wrong debug output only, no
|
||||
runtime impact. Casts added; build now warning-free on that file.
|
||||
|
||||
## 6. Verification ritual
|
||||
|
||||
Full `./build-pc.sh ntsc-final` configure + compile + link: **PASS**, no warnings on
|
||||
touched files. All new/changed symbols resolve exactly once (link is the sweep). D318
|
||||
watchdog/timeline symbols confirmed present in the binary.
|
||||
|
||||
## 7. Deliberately not committed (release-prep WIP, separate workstream)
|
||||
|
||||
`.github/release-notes.md`, `README.md`, `docs/index.md`, `docs/media/goldeneye-gh-preview.gif`,
|
||||
`docs/dev/agentic-development.md`, `.gitignore` (`/local/` exclude), and non-D318 scratch
|
||||
artifacts — left in the working tree for their owner.
|
||||
@@ -158,7 +158,7 @@ D260,FIXED,v0.2.1 (user request):,"FIXED — build green; one-line branch swap,
|
||||
D261,FIXED,v0.2.1 (user request:,FIXED — feel-check received; W/S sensitivity follow-up logged above.
|
||||
D262,FIXED,v0.2.1 (user report):,"FIXED (primary cause, D264) —"
|
||||
D264,FIXED,"D262's root cause (ABI/layout class, cf.","FIXED — build green, probes removed; visual confirmation pending."
|
||||
D263,RESOLVED,v0.2.1 (user report):,"RESOLVED (not a bug), but refined (v0.2.1 further user testing, 2026-09-15) —… ⚠ RE-OPENED via D318 (2026-09-18):"
|
||||
D263,RESOLVED,v0.2.1 (user report):,"RESOLVED (not a bug), refined (v0.2.1 further user testing, 2026-09-15)."
|
||||
D265,?,"D236 investigation (port-layer, port/fast3d/gfx_pc.cpp):",PARKED — reverted to baseline; evidence + suspect list above.
|
||||
D258,OPEN,PAL/JP asset conversion is broken at the source-data level —,"OPEN — repair path verified, not applied (out of v0.2.0 scope)."
|
||||
D284,FIXED,port/src/random.c's MIPS64 dsll32/dsrl32 shift helpers were backwards —,"FIXED, 21-level sweep clean, no crashes —"
|
||||
@@ -195,5 +195,6 @@ 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,?,"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,OPEN,Facility (regular play path):,"OPEN — reported only, not investigated."
|
||||
D318,RESOLVED,Facility (regular play path):,RESOLVED (2026-09-19):
|
||||
D319,FIXED,"Frame cap slider was mostly a dead zone; replaced with a plain 30/60 toggle (user ask, 2026-09-18).",FIXED — new ROW_FPSCAP cycle (30/60 only) in port/src/optionsoverlay.c; default changed to 60 in port/src/video.c.
|
||||
D320,OPEN,"D318-class sweep: aim-hold → _update AI lists in 8 levels carry the same as-authored softlock race (static analysis, 2026-09-20).","OPEN — static sweep complete (Facility ai_19, Control ai_9, Depot ai_12 flagged high/med-high; Bond-combat loops likely safe); no live confirmation beyond D318 itself; probe-verify path + generalized…"
|
||||
|
+76
-8
@@ -566,7 +566,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D261 | **v0.2.1 (user request: "fix watch menu sensitivity for wasd/controller buttons"): holding W/S (or a fully-pressed stick axis) on the watch inventory page…** — full `## D261` entry at file tail | FIXED — feel-check received; W/S sensitivity follow-up logged above. |
|
||||
| D262 | **v0.2.1 (user report): "3D models missing on watch menu screen item select" — the selected-item 3D preview does not appear on the watch inventory page.** — full `## D262` entry at file tail | FIXED (primary cause, D264) — pixel-level confirmation pending user run. |
|
||||
| D264 | **D262's root cause (ABI/layout class, cf. D140/D52): the watch item preview's `ModelRenderData` template is read across two adjacent globals that no longer sit…** — full `## D264` entry at file tail | FIXED — build green, probes removed; visual confirmation pending. |
|
||||
| D263 | **v0.2.1 (user report): Facility — "Ourumov not shooting Trevelyan"; the scripted opening beat never triggers. RESOLVED: not a port bug — the beat is…** — full `## D263` entry at file tail | RESOLVED (not a bug), but refined (v0.2.1 further user testing, 2026-09-15) —… **⚠ RE-OPENED via D318 (2026-09-18): user re-reported the beat failing on the regular play path after Objective C; treat "not a bug" as unproven for the authored path.** |
|
||||
| D263 | **v0.2.1 (user report): Facility — "Ourumov not shooting Trevelyan"; the scripted opening beat never triggers. RESOLVED: not a port bug — the beat is…** — full `## D263` entry at file tail | RESOLVED (not a bug), refined (v0.2.1 further user testing, 2026-09-15). The D318 re-report (2026-09-18) turned out to be a **different** failure mode (derailment race → softlock, see D318 RESOLVED) — D263's proximity-trigger mechanism map stands. |
|
||||
| D265 | **D236 investigation (port-layer, `port/fast3d/gfx_pc.cpp`): the tree cards are IA8/IA4, not IA16 — and `import_texture_ia4` decoded the RDP's I2:A2 texel as…** — full `## D265` entry at file tail | PARKED — reverted to baseline; evidence + suspect list above. |
|
||||
| D258 | **PAL/JP asset conversion is broken at the source-data level — `scripts/filelist.e.csv` and `filelist.j.csv` carry naming defects, so `gen_romassets.py e/j` (and…** — full `## D258` entry at file tail | OPEN — repair path verified, not applied (out of v0.2.0 scope). |
|
||||
| D284 | **`port/src/random.c`'s MIPS64 `dsll32`/`dsrl32` shift helpers were backwards — PC PRNG desynced from the N64's from draw 1, in every release to date (M-138 review; fix M-139b).** — full `## D284` entry at file tail | FIXED, 21-level sweep clean, no crashes — RNG stream changes from every prior build; existing replays will diverge. **"Savegames unaffected" was FALSE (M-148): `fileGenerateCRC` runs off a local constant seed via `randomGetNextFrom`, so the fix invalidates every stored slot CRC — v0.3.0 silently wipes all pre-fix saves on first launch (release-blocker candidate, see D295 M-148)** |
|
||||
@@ -603,8 +603,9 @@ 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 | OPEN — reported only, not investigated. Supersedes D263's "not a bug" closure for this repro path (D263 verified via teleport probe, not the authored path). Rule out interaction with same-day D310/D311 AI-executor changes first. |
|
||||
| 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; N64-identical), NOT a port bug; port-side watchdog `port/src/d318watchdog.c` re-inits the attack on confirm. D263 mechanism map stands (different failure mode). |
|
||||
| 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 | OPEN — static sweep complete (Facility ai_19, Control ai_9, Depot ai_12 flagged high/med-high; Bond-combat loops likely safe); no live confirmation beyond D318 itself; probe-verify path + generalized-watchdog recommendation documented. |
|
||||
|
||||
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
|
||||
@@ -12068,13 +12069,48 @@ Same fix as D316 resolves this (no separate code change). Verification: toggle `
|
||||
|
||||
**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.**
|
||||
|
||||
**Not investigated yet.** Pointers only:
|
||||
- The D263 static decode of `UsetuparkZ.c` maps the beat: Trevelyan (chr 67, ai_18) vision-gated → objective bit → BG list ai_41 spawns Ourumov (body 7, pad 88, ai_23→ai_22) + 6-soldier squad; ai_22 runs to pad 123, waits for Trevelyan within 0x1400, dialogue cascade, then `HitChrWithItem` execution shot. Check whether **Objective C completion** is one of the objective bits in ai_22's OR-gate early-exit conditions or in the spawn gate — i.e., does the *authored* path hand the beat to a different state than the teleport path did?
|
||||
- "Freezes up" (as opposed to "never spawns" or "shoots late") suggests Ourumov **did** spawn and reach ai_22 but is stuck on a wait condition — prime suspects: the Trevelyan-within-0x1400-of-pad-123 gate (Trevelyan missing his pad 124 walk, cf. D193-family locomotion history), or an objective-bit state that the teleport path set implicitly but the authored path leaves unset.
|
||||
- Recent AI-executor changes in this exact area: **D310** (`chraiitemsize()` PRINT sizing) and **D311** (root-motion freeze guard) — both 2026-09-18, both touched AI/animation execution; a regression interaction is worth ruling out first since the report lands on the same day.
|
||||
- D263's probes were removed; re-instrument ai_22's gate conditions + the objective bits under `GE_INPUTSCRIPT` or a scripted run of the regular path to see which condition Ourumov stalls on.
|
||||
**Investigated (2026-09-18/19): RESOLVED — as-authored latent race, faithfully reproduced by the port (NOT a port bug); port-side deadlock watchdog added (`port/src/d318watchdog.c`).** D310/D311 ruled out (no interaction). D263's mechanism map stands; this is a *different* failure mode than D263 tested — a derailment race, not the proximity trigger.
|
||||
|
||||
**Status:** OPEN — reported only, not investigated. Supersedes D263's "not a bug" closure for this repro path; D263 remains valid as the mechanism map.
|
||||
### Scene graph (byte disasm of ai_22, 384 bytes; `assets/obseg/setup/UsetuparkZ.c`)
|
||||
|
||||
Ourumov = chr 78 on list 0x0417 (ai_22). Key offsets: off=38 pre-aim at Trevelyan (`actor_aim_at_actor`, TARGET_AIM_ONLY|TARGET_DONTTURN → attacktype 0x64); off=44 gate-B wait loop (gas bit 0x04 → combat, 0x40 → flee, Bond-miss → combat, surrender 0x20 → dialogue cascade); off=72 dialogue cascade (~1320-tick timers) → off=272 label 0x13 = the execution shot `chr_hit_body_part_with_item_damage(0x43, 0x08, 0x06)` (headshot on chr 67); **off=302 label 0x2a** (combat section, reached when a gas/combat bit lands mid-monologue): off=304 Trevelyan-exists? → **off=307 `actor_fire_or_aim_at_target_update`** (succeeds only while ACT_ATTACK + AIM_ONLY|DONTTURN: sets attacktype/entityid + endframe, returns TRUE → off=321) → else falls to off=315 aim/fire at BOND → **off=321 label 0x01**: sleep → off=324 `if_guard_has_stopped_moving` (→ off=328 headshot on pass) → else loops back to 321.
|
||||
|
||||
### Root cause chain (all links byte-matched ROM logic)
|
||||
|
||||
1. **Pre-aim hold.** During the monologue c78 pre-aims at kneeling Trevelyan (atk=0x64). `chrlvInitActAttack` picks a RANDOM variant from the aim-animation group (`randomGetNext() % len`) and `modelSetAnimation`s it with `animlooping=0`.
|
||||
2. **The hold pose is a permanent anim pin.** The aim anim plays to its endframe H (the hold frame). With `animlooping==0`, `modelTickAnim`'s wrap logic is inactive; when the frame reaches endframe, `modelConstrainOrWrapAnimFrame` clamps **both** `framea` and `frameb` to `ceil(H)` (`if (0 <= endframe < frame) frame = ceilFloatToInt(endframe)`), so `framea == frameb`, and the tail of `modelSetAnimFrame2WithChrStuff` (`if (vb == va) { animframe1 = va; }`) then discards all sub-frame progress: the model is pinned at H forever. That is how a hold pose works by design — normally something else changes the attack state next.
|
||||
3. **The derail.** Objective C (bottling tanks) leaks gas → ai_42 sets obj bit 0x04 (~300 ticks after the leak). If that lands while c78 is in the pre-aim hold, ai_22 derails to section 0x2a and off=307's update command SUCCEEDS: attacktype becomes 0x04, entityid becomes Trevelyan (67), and `chrlvAttackActionRelated` sets a NEW endframe from the variant's recoil/shoot start — **but it never calls `modelSetAnimation`**. The model stays pinned on the aim-hold pose.
|
||||
4. **The branch (set by the random variant from step 1):**
|
||||
- new endframe ≤ H: next tick, `chrlvTickAttackCommon`'s `endframe <= frame` block fires and its unk31 path (`chraction.c:7209`) calls `modelSetAnimation` again → pin broken → anim advances into the shoot window → c78 live-fires Trevelyan → he dies (~230 ticks) → attack sequence completes → ACT_STAND → the off=324 stop check passes → scene continues (combat state).
|
||||
- new endframe > H: **nothing ever calls `modelSetAnimation` again** (probe-verified: zero calls in frozen runs). Frame stays pinned at H < shoot_start, c78 never fires, Trevelyan never dies, and the off=324 stop check (`chrHasStoppedOrPatroling`, TRUE only for ACT_STAND/ACT_ANIM/ACT_PATROL) can never pass while c78 is in ACT_ATTACK → **PERMANENT SOFTLOCK**.
|
||||
|
||||
### Evidence (probe runs, `scratch/d318_*.log`)
|
||||
|
||||
- `modelTickAnim` per-call trace: frozen state ticks every frame (`numticks=1, speed=0.68`) but `animframe1` is byte-identical tick to tick (e.g. 19.00 → 19.00) — the pin is in the model layer, not the AI/attack state machine.
|
||||
- `modelSetAnimation` funnel trace: frozen runs have **zero** calls post-derail; escape runs' unpin call resolves via addr2line to `chrlvTickAttackCommon` (chraction.c:7209, the unk31 path).
|
||||
- Pre/post-derail contrast: frozen run pinned at 19.00/end=19.00 → post-derail end=24.00 (24 > 19 → never crosses); escape run pinned at 22.00/end=22.00 → post-derail the unk31 path re-inits at startframe=22 (end ≤ frame) → advances 24→27→44, c67 takes damage, dies t≈2930-2960.
|
||||
- Non-determinism is by design: the variant pick in step 1 is `randomGetNext()`; forced-bit repros froze ~2/3 of runs. The organic path (user's report; playtest2) hit the frozen branch with no artificial forcing.
|
||||
|
||||
### Classification
|
||||
|
||||
**As-authored latent race, not a port bug.** Every link — ai_22 flow, the update command's missing re-init, the `framea==frameb` pin, the ACT_ATTACK-gated stop loop — is byte-matched ROM logic; the N64 has the identical race and the identical unrecoverable softlock. The trigger (gas bit landing during the monologue while c78 is in pre-aim hold) is organic level state on both platforms.
|
||||
|
||||
### Fix (port-side, `port/src/d318watchdog.c` — same pattern as D202 M-66b)
|
||||
|
||||
Per-tick check (hooked at the `g_GlobalTimer += g_ClockTimer` site in lv.c). Signature, all held for 600 consecutive ticks (10 s; the escape path resolves in < 2 s): chr 78 on list 0x0417 with offset in [321, 333), `actiontype == ACT_ATTACK`, `act_attack.entityid == 67` (targeting Trevelyan — legitimate combat at this offset targets Bond, ent=1), and chr 67 alive. On confirm: **re-initialize c78's attack via `sub_GAME_7F025560(c78, attacktype, entityid)`** — the same entry point the game itself uses for a fresh attack (chrlvTickAttack type_of_motion==2, chraction.c:7401). That runs `chrlvInitActAttack` → random variant + `modelSetAnimation(start_frame)`, breaking the pin; c78 then live-fires Trevelyan exactly like the natural escape path. One-shot; logs `D318W:` lines for playtest audit; opt-out `GE_D318W=0`.
|
||||
|
||||
**Why not just kill Trevelyan:** first attempt applied the scripted headshot's own damage call (`handles_shot_actors(c67, 0x08, &zero, 0x06, FALSE)` — ai_22 off=333). c67 died, but a pinned attack has **no completion event tied to target death** — c78 stayed in ACT_ATTACK and the stop check still never passed (probe-verified: act=8 in 100% of samples post-kill vs 32/264 ACT_STAND samples in natural-escape runs). The attack state itself must be re-seeded.
|
||||
|
||||
**Why not an in-src fix:** making `actor_fire_or_aim_at_target_update` call `modelSetAnimation` would change behavior relative to N64 ground truth in *normal* play (Rule 2, no sign-off basis — there is no port divergence to correct). The watchdog is inert in every non-deadlocked state.
|
||||
|
||||
### Verification
|
||||
|
||||
- Forced-bit repros with the final watchdog: escape runs stay silent (c67 dies t≈2930-2960, 60× under threshold); a frozen run armed at t=2702, fired at t=3300, c67 died t=3570 from c78's own gunfire, and c78's post-recovery action profile (ACT_STAND/ATTACK/ATTACKWALK mix, moving) matches natural-escape runs.
|
||||
- Plain headless Facility run (no env): zero D318 lines, no errors, clean shutdown.
|
||||
- Interactive playtest (`scratch/d318_playtest3.log`, 2026-09-19): organic derail occurred; the run drew the escape variant, so c78 broke the pin on his own and killed Trevelyan within seconds. The watchdog detected the signature twice (t=7496, t=9270) but never confirmed — both windows cleared before 600 consecutive ticks → zero false positives in live play.
|
||||
- Diagnostic probes removed (`port/src/d318probe.c`, `port/include/d318probe.h`, and the `#ifdef PORT` hooks in chrai.c/chraction.c/lv.c/model.c); only the permanent watchdog hook remains (lv.c).
|
||||
|
||||
**Status:** RESOLVED (2026-09-19). As-authored latent race documented; port-side watchdog ships by default. Cross-ref: D263 (mechanism map — stands), D202 M-66b (port-side expiration precedent).
|
||||
|
||||
## D319 — Frame cap slider was mostly a dead zone; replaced with a plain 30/60 toggle (user ask, 2026-09-18).
|
||||
|
||||
@@ -12091,3 +12127,35 @@ Only 30, 60, and 0 (uncapped: skip the port's frame-pacing wait entirely) ever d
|
||||
Verified: build clean (MSYS2 MINGW64/ninja); `tools_pc/verify.sh bunker1` — PASS, `worst_cell=0`. Interactive check (toggle Frame cap in the F10 menu, confirm it reads 30/60 and only those two) still owed.
|
||||
|
||||
**Status:** FIXED (pending interactive confirmation). Cross-ref: D186 (root cause of both dead zones, this just fixes the UI's exposure of it).
|
||||
|
||||
## D320 — D318-class sweep: aim-hold → `_update` AI lists in 8 levels carry the same as-authored softlock race (static analysis, 2026-09-20).
|
||||
|
||||
**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; the N64 has the identical race (the random aim-variant pick in `chrlvInitActAttack`, `randomGetNext() % len`, differs per run on both platforms).
|
||||
|
||||
**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:
|
||||
|
||||
| List | Level | Hold target | Update | Escape gate after update | Assessment |
|
||||
|---|---|---|---|---|---|
|
||||
| ai_22 | Facility | chr 0x43 (Trevelyan) | 0x04→0x43 ×2 | stop-check loop (off=321–333) | **D318 — confirmed live, watchdog shipped** |
|
||||
| ai_19 | Facility | chrs 0x44–0x48 / pad 0x6d (kneel-aim cascade) | 0x04→chr / 0x08→pad | label 0x34: guard bitfield 0x01 **or** `if_guard_has_stopped_moving` → re-aim at 0x08 | **HIGH — same non-self-healing shape.** Long monologue (text prints + local timers) precedes the hold; if pinned, ACT_ATTACK fails the stop check and nothing else sets the escape bitfield from this loop. Second softlock point in the same level as D318. |
|
||||
| ai_9 | Control | chr 0xFC (aim or kneel-aim, random pick) | 0x04→0xFC | label 0x0b: chr 0xFC dying/dead (i.e. *killed by this guard's shot*) **or** stop-check → re-aim at 0x04 | **HIGH — near-exact ai_22 mirror**: pre-aim hold → local-timer wait (`if_local_timer_greater_than(0x780000)`) → update → fire loop gated on the target dying. Pinned guard never fires → target never dies → stop check (ACT_ATTACK) never passes → softlock before `jump_to_ai_list(0xfd, 0x0d00)`. |
|
||||
| ai_12 | Depot | chrs 0x0F then 0x0E (aim / kneel-aim) | 0x04→chr ×2 | after update: scripted `chr_hit_body_part_with_item_damage` kills the target, but the **objective bit** (`objective_bitfield_set_on(0x00080000)`) is only set after an `if_guard_has_stopped_moving` gate | **MED-HIGH.** The kill itself is scripted (unlike ai_22/ai_9), so the scene visibly "progresses" — but if pinned, the guard wedges in ACT_ATTACK at the stop-check loop and the objective bit never lands → level-complete softlock. Note the pre-update gate `if_guard_is_firing_and_using_180_range_flag` passes *during* an AIM_ONLY\|DONTTURN hold (it checks ACT_ATTACK + DONTTURN, not "actually firing"), so the update can land mid-hold exactly like D318. |
|
||||
| ai_13/14/15 | Depot | pad 0x1601 (AIM_ONLY) | 0x08→pad | after update: infinite sleep loop (`goto_first(0x06)`), no progression dependency in-list | **LOW-MED.** No in-list gate, but if the level elsewhere requires these guards' shots to hit pad 0x1601 (destructible/trigger), a pinned guard blocks it. Verify against the Depot objective graph before dismissing. |
|
||||
| ai_34 | Facility | Bond (0x21) ×4 | 0x01→Bond | re-aim every cycle (label 0x05 → fresh AIM command) | **Likely safe — see below.** |
|
||||
| ai_0 | Aztec | Bond (0x21)/(0x01) | 0x01→Bond | combat loop, re-aims each cycle | Likely safe. |
|
||||
| ai_11 | Control | Bond (0x21) ×2 | 0x01→Bond ×2 | combat loop | Likely safe. |
|
||||
| ai_3 / ai_14 | Egyptian | Bond (0x21)/(0x01) | 0x01→Bond | combat loop | Likely safe. |
|
||||
| ai_10 | Depot | Bond (0x21) | 0x01→Bond | combat loop | Likely safe. |
|
||||
| ai_17 | Streets | Bond (0x21) ×2 | 0x01→Bond ×2 | combat loop | Likely safe. |
|
||||
| ai_20 | Bunker | Bond (0x21) | 0x01→Bond | combat loop | Likely safe. |
|
||||
| ai_24 / ai_32 | Surface | Bond (0x21) | 0x01→Bond | combat loop | Likely safe. |
|
||||
|
||||
**Why the Bond-combat loops are probably not affected.** In normal sustained fire the aim-hold endframe sits *at/inside* the shoot window (the hold pose is the recoil pose; shots fire from the `attack_time`/`unk44` timer machinery in `chrlvTickAttackCommon`, not from frame progression past endframe), so a `_update` 0x21→0x01 keeps the model pinned *inside* the firing window and the guard keeps shooting — that is how continuous fire works by design. The D318 danger requires the hold pose to be a distinct pre-shoot pose (H < shoot_start) — which is what "aim at X but don't fire yet" scripts produce. These combat lists are also exercised on every playtest of every level with no observed freezes, which is strong empirical evidence. This distinction is inferred from code reading + playtest history, **not** probe-verified per list.
|
||||
|
||||
**Classification.** As-authored latent race(s), faithfully reproduced — NOT a port bug (same classification as D318). No `src/game` fix is warranted or permitted absent Rule-2 sign-off; the N64 carries the identical risk on the identical random-variant branch.
|
||||
|
||||
**Verification path (owed).** The pin branch depends on the per-variant animation-table fields (H vs E), which are data, not code — static analysis cannot price the risk per list. Use the D318T-style timeline probe (`port/src/d318watchdog.c` `d318TimelineTick`, generalized to log any chr in ACT_ATTACK with an AIM_ONLY attacktype whose `modelGetAnimFrame` is byte-stable across ticks while `aioffset` sits in a stop-check loop) during playtests of: Facility (the ai_19 beat — the guard who monologues at the captive scientists before shooting), Control (the ai_9 beat), Depot (the ai_12 execution beat). A frozen run shows the exact D318 signature: stable `animframe1`, zero `modelSetAnimation` calls post-`_update`.
|
||||
|
||||
**If confirmed in live play:** generalize the D318 watchdog instead of writing per-list copies — parameterize (AI list ID, offset range, chr number, target entityid, confirmation tick count) as a small table; the detection signature and the `sub_GAME_7F025560` re-init are already generic. The current `d318WatchdogTick` is hardcoded to ai_22/c78/c67 by design (one-shot, zero false-positive surface); keep it that way until a second list actually freezes in play.
|
||||
|
||||
**Status:** OPEN — static sweep complete, no live confirmation yet for any list beyond D318 itself. Cross-ref: D318 (mechanism + watchdog), D263 (ai_22 mechanism map).
|
||||
@@ -2416,10 +2416,10 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo
|
||||
"[D116/vbo] rect vtx%d x=%.4f y=%.4f u=%.4f v=%.4f "
|
||||
"(raw v->u=%.1f texw=%.0f texw2=%u tile.uls=%d lrs=%d cms=%d)\n",
|
||||
i, v_arr[i]->x, v_arr[i]->y, u / tex_width[t], v / tex_height[t],
|
||||
v_arr[i]->u, tex_width[t], tex_width2[i],
|
||||
rdp.texture_tile[rdp.first_tile_index + tile].uls,
|
||||
rdp.texture_tile[rdp.first_tile_index + tile].lrs,
|
||||
rdp.texture_tile[rdp.first_tile_index + tile].cms);
|
||||
(double)v_arr[i]->u, (double)tex_width[t], (unsigned)tex_width2[i],
|
||||
(int)rdp.texture_tile[rdp.first_tile_index + tile].uls,
|
||||
(int)rdp.texture_tile[rdp.first_tile_index + tile].lrs,
|
||||
(int)rdp.texture_tile[rdp.first_tile_index + tile].cms);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
/* d318watchdog.c -- D318: port-side deadlock recovery for the Facility
|
||||
* (level_34) Ourumov/Trevelyan execution softlock.
|
||||
*
|
||||
* What this is (full writeup: docs/dev/findings.md, D318):
|
||||
*
|
||||
* As-authored latent race, faithfully reproduced by the port (NOT a port
|
||||
* bug -- every link in the chain is byte-matched ROM logic):
|
||||
*
|
||||
* 1. Ourumov (chr 78, ai_22) pre-aims at kneeling Trevelyan (chr 67)
|
||||
* during his monologue: actor_aim_at_actor with TARGET_AIM_ONLY|
|
||||
* TARGET_DONTTURN (attacktype 0x64). chrlvInitActAttack picks a RANDOM
|
||||
* variant from the aim-animation group (randomGetNext() % len) and
|
||||
* modelSetAnimation's it with animlooping=0.
|
||||
* 2. The aim anim plays to its endframe H (the "hold" pose). With
|
||||
* animlooping==0, modelTickAnim has no wrap logic; when the frame
|
||||
* reaches endframe, modelConstrainOrWrapAnimFrame clamps BOTH framea
|
||||
* and frameb to ceil(H), so framea==frameb, and the tail of
|
||||
* modelSetAnimFrame2WithChrStuff (if (vb==va) animframe1 = va) then
|
||||
* discards all sub-frame progress: the model is PINNED at H. That is
|
||||
* how a hold pose works by design -- normally something else changes
|
||||
* the attack state next.
|
||||
* 3. If the gas cascade sets objective bit 0x04 (combat) mid-monologue,
|
||||
* ai_22 derails to section 0x2a and off=307 runs
|
||||
* actor_fire_or_aim_at_target_update, which SUCCEEDS while the pre-aim
|
||||
* is active: attacktype becomes 0x04, entityid becomes Trevelyan, and
|
||||
* chrlvAttackActionRelated sets a NEW endframe from the variant's
|
||||
* recoil/shoot start frame -- but it never calls modelSetAnimation.
|
||||
* 4. Branch (determined by the random variant chosen in step 1):
|
||||
* - new endframe <= H: chrlvTickAttackCommon's "endframe <= frame"
|
||||
* block fires next tick and its unk31 path calls modelSetAnimation
|
||||
* again -> pin broken -> anim advances into the shoot window ->
|
||||
* c78 live-fires Trevelyan -> he dies -> attack ends -> ACT_STAND
|
||||
* -> the stop check at ai_22 off=324 passes -> scene continues.
|
||||
* - new endframe > H: nothing ever calls modelSetAnimation again
|
||||
* (verified by probe: zero calls in frozen runs). The frame stays
|
||||
* pinned at H < shoot_start, c78 never fires, Trevelyan never
|
||||
* dies, and ai_22's off=324 stop check (chrHasStoppedOrPatroling,
|
||||
* TRUE only for ACT_STAND/ACT_ANIM/ACT_PATROL) can never pass
|
||||
* while c78 is in ACT_ATTACK -> PERMANENT SOFTLOCK.
|
||||
*
|
||||
* The N64 has the identical race and the identical softlock; this watchdog
|
||||
* is a deliberate port-layer accommodation (same pattern as the D202 M-66b
|
||||
* port-side expiration). It detects the exact unrecoverable signature and,
|
||||
* after a long margin, re-initializes c78's attack via sub_GAME_7F025560 --
|
||||
* the same entry point the game itself uses for a fresh attack (chrlvTickAttack
|
||||
* type_of_motion==2 calls it with (attacktype, entityid)). That runs
|
||||
* chrlvInitActAttack: random variant pick + modelSetAnimation(start_frame),
|
||||
* which breaks the framea==frameb pin. c78 then live-fires Trevelyan exactly
|
||||
* like the natural escape path (verified: escape runs kill c67 within ~230
|
||||
* ticks of a fresh init), his attack sequence completes, he reaches
|
||||
* ACT_STAND, the stop check at off=324 passes, and ai_22 proceeds as
|
||||
* authored (the off=333 headshot lands on the corpse -- the same no-op the
|
||||
* game already handles when Trevelyan dies of gas before the execution shot).
|
||||
*
|
||||
* NOTE: merely killing Trevelyan (e.g. via handles_shot_actors) is NOT
|
||||
* enough -- a pinned attack has no internal completion event tied to target
|
||||
* death, so c78 would stay in ACT_ATTACK and the stop check would still
|
||||
* never pass (verified by probe). The attack state itself must be re-seeded.
|
||||
*
|
||||
* Signature (all must hold for D318_DEADLOCK_TICKS consecutive ticks):
|
||||
* - chr 78 exists, is on AI list 0x0417 (ai_22), offset in [321, 333)
|
||||
* (the sleep/stop-check loop; 333 is the headshot command itself),
|
||||
* - chr 78 actiontype == ACT_ATTACK with act_attack.entityid == 67
|
||||
* (targeting Trevelyan -- the derailed state; legitimate combat at this
|
||||
* offset targets Bond, ent=1),
|
||||
* - chr 67 exists, has a prop, and is not dead.
|
||||
*
|
||||
* In every non-deadlocked state the watchdog is inert: the escape path
|
||||
* kills Trevelyan within ~25-100 ticks (60x under the threshold) and
|
||||
* legitimate combat never targets chr 67 from this loop. One-shot per
|
||||
* process; logs its detection and intervention for playtest audit.
|
||||
*
|
||||
* Opt out (research only): GE_D318W=0
|
||||
*/
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "PR/os.h"
|
||||
#include "bondtypes.h"
|
||||
#include "chr.h"
|
||||
#include "chrai.h"
|
||||
#include "chraction.h"
|
||||
#include "lv.h"
|
||||
#include "model.h"
|
||||
#include "random.h"
|
||||
|
||||
#ifdef PORT
|
||||
|
||||
/* chrai.c defines these but chrai.h does not declare them. */
|
||||
extern s32 chraiGetAIListID(AIRecord *AIList, bool *isGlobalAIList);
|
||||
|
||||
/* chraction.c internal (not in chraction.h): the fresh-attack entry point.
|
||||
* Signature from the definition; call form mirrors chrlvTickAttack's own
|
||||
* re-init at chraction.c:7401. */
|
||||
extern void sub_GAME_7F025560(ChrRecord *self, s32 attack_type, s32 arg2);
|
||||
|
||||
#define D318_LIST_AI22 0x0417 /* Ourumov's execution list (ai_22) */
|
||||
#define D318_OFF_LOOP_MIN 321 /* label 0x01: sleep/stop-check loop */
|
||||
#define D318_OFF_LOOP_MAX 333 /* exclusive: the headshot command */
|
||||
#define D318_OURUMOV_CHR 78
|
||||
#define D318_TREVELYAN_CHR 67
|
||||
#define D318_DEADLOCK_TICKS 600 /* 10 s at 60 Hz; escape takes < 2 s */
|
||||
|
||||
static ChrRecord *d318wFindChr(s16 chrnum)
|
||||
{
|
||||
s32 i;
|
||||
|
||||
for (i = 0; i < g_NumChrSlots; i++)
|
||||
{
|
||||
if (g_ChrSlots[i].chrnum == chrnum)
|
||||
{
|
||||
return &g_ChrSlots[i];
|
||||
}
|
||||
}
|
||||
for (i = 0; i < g_ActiveChrsCount; i++)
|
||||
{
|
||||
if (g_ActiveChrs[i].chrnum == chrnum)
|
||||
{
|
||||
return &g_ActiveChrs[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void d318WatchdogTick(void)
|
||||
{
|
||||
static int s_enabled = -1; /* -1 uncached, 0 off (GE_D318W=0), 1 on */
|
||||
static s32 s_run = 0; /* consecutive ticks the signature has held */
|
||||
static bool s_fired = FALSE;
|
||||
ChrRecord *c78;
|
||||
ChrRecord *c67;
|
||||
bool g = FALSE;
|
||||
|
||||
if (s_enabled < 0)
|
||||
{
|
||||
const char *e = getenv("GE_D318W");
|
||||
|
||||
s_enabled = (e && e[0] == '0') ? 0 : 1;
|
||||
}
|
||||
if (!s_enabled || s_fired)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
c78 = d318wFindChr(D318_OURUMOV_CHR);
|
||||
c67 = d318wFindChr(D318_TREVELYAN_CHR);
|
||||
|
||||
if (c78 && c78->ailist
|
||||
&& chraiGetAIListID(c78->ailist, &g) == D318_LIST_AI22
|
||||
&& c78->aioffset >= D318_OFF_LOOP_MIN && c78->aioffset < D318_OFF_LOOP_MAX
|
||||
&& c78->actiontype == ACT_ATTACK
|
||||
&& (s32)c78->act_attack.entityid == D318_TREVELYAN_CHR
|
||||
&& c67 && c67->prop && !chrIsDead(c67))
|
||||
{
|
||||
if (s_run == 1)
|
||||
{
|
||||
osSyncPrintf("D318W: t=%d deadlock signature detected (c78 ai_22 off=%d "
|
||||
"ACT_ATTACK ent=67, c67 alive) -- starting %d-tick confirmation window\n",
|
||||
(int)g_GlobalTimer, (int)c78->aioffset, D318_DEADLOCK_TICKS);
|
||||
}
|
||||
s_run++;
|
||||
if (s_run >= D318_DEADLOCK_TICKS)
|
||||
{
|
||||
osSyncPrintf("D318W: t=%d D318 execution deadlock confirmed -- re-initializing "
|
||||
"c78's attack (sub_GAME_7F025560 atk=0x%x ent=%d) to break the anim pin\n",
|
||||
(int)g_GlobalTimer, (unsigned)c78->act_attack.attacktype,
|
||||
(int)c78->act_attack.entityid);
|
||||
sub_GAME_7F025560(c78, (s32)c78->act_attack.attacktype, (s32)c78->act_attack.entityid);
|
||||
s_fired = TRUE;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
s_run = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* D318T: diagnostic timeline for the "Ourumov shoots Trevelyan the */
|
||||
/* moment his lines begin" report (2026-09-20). Env-gated, OFF by */
|
||||
/* default; enable with GE_D318T=1. Port-side read-only: logs state */
|
||||
/* transitions so a single playtest captures which combat trigger */
|
||||
/* (gas bit 0x04 vs Bond near-miss flag) derailed ai_22, and how many */
|
||||
/* ticks after the monologue started it landed. No game state is */
|
||||
/* touched. */
|
||||
/* */
|
||||
/* Background: the ONLY path by which c78 live-fires Trevelyan */
|
||||
/* (entityid 67) in this scene is section 0x2a -> off=307 */
|
||||
/* actor_fire_or_aim_at_target_update succeeding while the pre-aim */
|
||||
/* (ACT_ATTACK + AIM_ONLY|DONTTURN) is active. That requires a combat */
|
||||
/* trigger inside the monologue loop: gas bit 0x04 (off=75) or */
|
||||
/* chrIfNearMiss (off=87, CHRFLAG_NEAR_MISS -- set by Bond bullets */
|
||||
/* passing c78's bounds, cleared on c78's next tick). The freeze */
|
||||
/* (D318) and the immediate-fire are the two branches of the same */
|
||||
/* derailed state (random aim-variant endframe vs the hold frame H). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Manual hex parse: MinGW's strtoull returns the sign-extended low 32 bits
|
||||
* for values above LLONG_MAX (verified: "deadbeefcafe0123" ->
|
||||
* ffffffffcafe0123). Parse by hand; stops at '@' or non-hex. */
|
||||
static u64 d318wParseHex(const char *s)
|
||||
{
|
||||
u64 v = 0;
|
||||
|
||||
while (*s && *s != '@')
|
||||
{
|
||||
char c = *s++;
|
||||
|
||||
v <<= 4;
|
||||
if (c >= '0' && c <= '9') { v |= (u64)(c - '0'); }
|
||||
else if (c >= 'a' && c <= 'f') { v |= (u64)(c - 'a' + 10); }
|
||||
else if (c >= 'A' && c <= 'F') { v |= (u64)(c - 'A' + 10); }
|
||||
else { break; }
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
void d318TimelineTick(void)
|
||||
{
|
||||
/* E-B seed injection: GE_RSEED_LV=<hex64>[@tick] -- set g_randomSeed to
|
||||
* the value read from N64 RAM (0x80024460) at a canonical moment, so the
|
||||
* port's stream can be A/B-compared against the original from that point
|
||||
* on. Test-only; inert without the env var. The DET line logs the running
|
||||
* seed every 60 ticks for verifying stream parity afterwards. */
|
||||
{
|
||||
static int s_seedArmed = 1;
|
||||
static u64 s_seedVal = 0;
|
||||
static s32 s_seedTick = 0;
|
||||
|
||||
if (s_seedArmed)
|
||||
{
|
||||
const char *e = getenv("GE_RSEED_LV");
|
||||
|
||||
s_seedArmed = 0;
|
||||
if (e && e[0])
|
||||
{
|
||||
const char *at = strchr(e, '@');
|
||||
|
||||
s_seedVal = d318wParseHex(e); /* MinGW strtoull mangles >LLONG_MAX */
|
||||
s_seedTick = at ? (s32)strtol(at + 1, NULL, 10) : 0;
|
||||
}
|
||||
}
|
||||
if (s_seedVal && (s32)g_GlobalTimer >= s_seedTick)
|
||||
{
|
||||
u64 v = s_seedVal;
|
||||
|
||||
g_randomSeed = v;
|
||||
s_seedVal = 0;
|
||||
osSyncPrintf("D318T: t=%d RSEED_LV -> %016llx\n",
|
||||
(int)g_GlobalTimer, (unsigned long long)v);
|
||||
}
|
||||
}
|
||||
|
||||
static int s_on = -1; /* -1 uncached, 0 off, 1 on (GE_D318T=1) */
|
||||
static s32 s_obj = -1; /* last logged objectiveregisters1 */
|
||||
static s32 s_off = -1; /* last logged c78 aioffset */
|
||||
static s32 s_act = -1; /* last logged c78 actiontype */
|
||||
static s32 s_atk = -1; /* last logged c78 act_attack.attacktype */
|
||||
static s32 s_ent = -1; /* last logged c78 act_attack.entityid */
|
||||
static s32 s_nm = -1; /* last logged c78 NEAR_MISS flag state */
|
||||
static f32 s_dmg = -1.0f;
|
||||
static bool s_dead = FALSE;
|
||||
static s32 s_hb = 0;
|
||||
static s32 s_det = 0; /* E1 determinism fingerprint counter */
|
||||
ChrRecord *c78;
|
||||
ChrRecord *c67;
|
||||
|
||||
if (s_on < 0)
|
||||
{
|
||||
const char *e = getenv("GE_D318T");
|
||||
|
||||
s_on = (e && e[0] == '1') ? 1 : 0;
|
||||
}
|
||||
if (!s_on)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
c78 = d318wFindChr(D318_OURUMOV_CHR);
|
||||
c67 = d318wFindChr(D318_TREVELYAN_CHR);
|
||||
|
||||
/* Objective bitfield transitions (the derail triggers live here: */
|
||||
/* 0x04 gas/combat, 0x20 surrender/monologue, 0x40 flee). */
|
||||
if (s_obj != objectiveregisters1)
|
||||
{
|
||||
osSyncPrintf("D318T: t=%d obj bits 0x%08x -> 0x%08x\n",
|
||||
(int)g_GlobalTimer, (unsigned)s_obj, (unsigned)objectiveregisters1);
|
||||
s_obj = objectiveregisters1;
|
||||
}
|
||||
|
||||
if (c78)
|
||||
{
|
||||
s32 nm = (c78->chrflags & CHRFLAG_NEAR_MISS) != 0;
|
||||
|
||||
/* D318 anim-internals line: catches the kneel-attack route */
|
||||
/* (unk54==0 => sub_GAME_7F0256F0 "chrAttackKneel" re-inits) and the */
|
||||
/* hold/pin frame values, for the "bending down too far" report. */
|
||||
{
|
||||
static s32 s_tom = -1;
|
||||
static u32 s_u54 = 0xFFFFFFFFu;
|
||||
static f32 s_endf = -1.0f;
|
||||
|
||||
s32 tom = (s32)c78->act_attack.type_of_motion;
|
||||
u32 u54 = c78->act_attack.unk54;
|
||||
f32 endf = c78->model ? modelGetAnimEndFrame(c78->model) : -1.0f;
|
||||
|
||||
if (s_tom != tom || s_u54 != u54 || s_endf != endf)
|
||||
{
|
||||
osSyncPrintf("D318T: t=%d c78 anim mot=%d unk54=%u endframe=%.2f frame1=%.2f (off=%d)\n",
|
||||
(int)g_GlobalTimer, tom, (unsigned)u54,
|
||||
(double)endf,
|
||||
(double)(c78->model ? modelGetAnimFrame(c78->model) : 0.0f),
|
||||
(int)c78->aioffset);
|
||||
s_tom = tom;
|
||||
s_u54 = u54;
|
||||
s_endf = endf;
|
||||
}
|
||||
}
|
||||
|
||||
if (s_off != (s32)c78->aioffset || s_act != (s32)c78->actiontype)
|
||||
{
|
||||
const char *phase = "?";
|
||||
|
||||
if (c78->aioffset >= 38 && c78->aioffset < 44) { phase = "pre-aim(off38)"; }
|
||||
else if (c78->aioffset >= 44 && c78->aioffset < 69) { phase = "wait-loop(off44)"; }
|
||||
else if (c78->aioffset >= 69 && c78->aioffset < 272){ phase = "MONOLOGUE(off69+)"; }
|
||||
else if (c78->aioffset >= 272 && c78->aioffset < 295){ phase = "execution(0x13)"; }
|
||||
else if (c78->aioffset >= 302 && c78->aioffset < 333){ phase = "COMBAT-0x2a"; }
|
||||
|
||||
osSyncPrintf("D318T: t=%d c78 off=%d [%s] act=%d atk=0x%x ent=%d\n",
|
||||
(int)g_GlobalTimer, (int)c78->aioffset, phase,
|
||||
(int)c78->actiontype, (unsigned)c78->act_attack.attacktype,
|
||||
(int)c78->act_attack.entityid);
|
||||
s_off = (s32)c78->aioffset;
|
||||
s_act = (s32)c78->actiontype;
|
||||
}
|
||||
if (s_atk != (s32)c78->act_attack.attacktype || s_ent != (s32)c78->act_attack.entityid)
|
||||
{
|
||||
osSyncPrintf("D318T: t=%d c78 ATTACK CHANGE atk=0x%x ent=%d (off=%d)\n",
|
||||
(int)g_GlobalTimer, (unsigned)c78->act_attack.attacktype,
|
||||
(int)c78->act_attack.entityid, (int)c78->aioffset);
|
||||
s_atk = (s32)c78->act_attack.attacktype;
|
||||
s_ent = (s32)c78->act_attack.entityid;
|
||||
}
|
||||
if (s_nm != nm)
|
||||
{
|
||||
osSyncPrintf("D318T: t=%d c78 NEAR_MISS flag %s (Bond bullet passed his bounds)\n",
|
||||
(int)g_GlobalTimer, nm ? "SET" : "cleared");
|
||||
s_nm = nm;
|
||||
}
|
||||
}
|
||||
|
||||
if (c67)
|
||||
{
|
||||
bool dead = chrIsDead(c67);
|
||||
|
||||
if (s_dmg != c67->damage || s_dead != dead)
|
||||
{
|
||||
osSyncPrintf("D318T: t=%d c67(Trevelyan) dmg=%.2f/%.2f %s\n",
|
||||
(int)g_GlobalTimer, (double)c67->damage, (double)c67->maxdamage,
|
||||
dead ? "DEAD" : "");
|
||||
s_dmg = c67->damage;
|
||||
s_dead = dead;
|
||||
}
|
||||
}
|
||||
|
||||
/* Heartbeat while c78 is on his execution list, so gaps are visible. */
|
||||
if (c78 && c78->ailist)
|
||||
{
|
||||
bool g = FALSE;
|
||||
|
||||
if (chraiGetAIListID(c78->ailist, &g) == D318_LIST_AI22 && (++s_hb >= 60))
|
||||
{
|
||||
s_hb = 0;
|
||||
osSyncPrintf("D318T: t=%d c78 hb off=%d act=%d atk=0x%x ent=%d obj=0x%08x\n",
|
||||
(int)g_GlobalTimer, (int)c78->aioffset, (int)c78->actiontype,
|
||||
(unsigned)c78->act_attack.attacktype, (int)c78->act_attack.entityid,
|
||||
(unsigned)objectiveregisters1);
|
||||
}
|
||||
}
|
||||
|
||||
/* E1 determinism fingerprint: one line per second over ALL active chrs */
|
||||
/* + the PRNG seed + objective bits. Two no-input runs that differ here */
|
||||
/* are non-deterministic in the port (foundational); identical logs */
|
||||
/* mean run-to-run variation is input-timing-driven. */
|
||||
if (++s_det >= 60)
|
||||
{
|
||||
s32 h = 0x811C9DC5;
|
||||
s32 i;
|
||||
|
||||
s_det = 0;
|
||||
for (i = 0; i < g_ActiveChrsCount; i++)
|
||||
{
|
||||
ChrRecord *c = &g_ActiveChrs[i];
|
||||
union { f32 f; u32 u; } xf, yf, zf, df;
|
||||
|
||||
xf.f = c->prop ? c->prop->pos.x : 0.0f;
|
||||
yf.f = c->prop ? c->prop->pos.y : 0.0f;
|
||||
zf.f = c->prop ? c->prop->pos.z : 0.0f;
|
||||
df.f = c->damage;
|
||||
h ^= c->chrnum; h = (h * 0x01000193) ^ 0x9E3779B9;
|
||||
h ^= (s32)c->actiontype; h = (h * 0x01000193) ^ 0x9E3779B9;
|
||||
h ^= (s32)c->aioffset; h = (h * 0x01000193) ^ 0x9E3779B9;
|
||||
h ^= xf.u; h = (h * 0x01000193) ^ 0x9E3779B9;
|
||||
h ^= yf.u; h = (h * 0x01000193) ^ 0x9E3779B9;
|
||||
h ^= zf.u; h = (h * 0x01000193) ^ 0x9E3779B9;
|
||||
h ^= df.u; h = (h * 0x01000193) ^ 0x9E3779B9;
|
||||
}
|
||||
osSyncPrintf("D318T: DET t=%d seed=%08x%08x obj=0x%08x h=%08x nchrs=%d\n",
|
||||
(int)g_GlobalTimer,
|
||||
(unsigned)(u32)(g_randomSeed >> 32), (unsigned)(u32)g_randomSeed,
|
||||
(unsigned)objectiveregisters1, (unsigned)h, (int)g_ActiveChrsCount);
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* PORT */
|
||||
@@ -32,6 +32,7 @@
|
||||
*/
|
||||
|
||||
#include <ultra64.h>
|
||||
#include <stdlib.h>
|
||||
#include <random.h>
|
||||
|
||||
u64 g_randomSeed = 0xAB8D9F7781280783ULL;
|
||||
@@ -50,8 +51,38 @@ static inline u64 dsrl32(u64 x, int n) { return x >> (n + 32); }
|
||||
* Advance the global seed and return the new low 32 bits.
|
||||
* Mirrors randomGetNext in random.s line-by-line.
|
||||
*/
|
||||
/* D318R: env-gated draw logger (GE_D318R=1). Logs the first N draws with
|
||||
* tick + caller return address to identify which call sites consume the
|
||||
* gameplay PRNG and whether their per-run counts are stable. Diagnostic
|
||||
* only; inert without the env var. */
|
||||
#ifdef PORT
|
||||
#include <PR/os.h>
|
||||
extern s32 g_GlobalTimer;
|
||||
static void randomDrawLog(void)
|
||||
{
|
||||
static int s_on = -1;
|
||||
static u32 s_n = 0;
|
||||
const u32 MAXN = 4000;
|
||||
|
||||
if (s_on < 0)
|
||||
{
|
||||
const char *e = getenv("GE_D318R");
|
||||
s_on = (e && e[0] == '1') ? 1 : 0;
|
||||
}
|
||||
if (s_on && s_n < MAXN)
|
||||
{
|
||||
osSyncPrintf("D318R: n=%u t=%d ra=%p\n",
|
||||
s_n, (int)g_GlobalTimer, __builtin_return_address(0));
|
||||
s_n++;
|
||||
}
|
||||
}
|
||||
#endif /* PORT */
|
||||
|
||||
u32 randomGetNext(void)
|
||||
{
|
||||
#ifdef PORT
|
||||
randomDrawLog();
|
||||
#endif
|
||||
u64 a0 = g_randomSeed;
|
||||
u64 a1, a2;
|
||||
|
||||
@@ -72,9 +103,51 @@ u32 randomGetNext(void)
|
||||
}
|
||||
|
||||
/* Set the seed (the .s adds 1 before storing). */
|
||||
/* Manual hex parse (u64-safe): MinGW's strtoull returns the sign-extended low
|
||||
* 32 bits for values above LLONG_MAX. Stops at first non-hex char. */
|
||||
static u64 randomParseHex(const char *s)
|
||||
{
|
||||
u64 v = 0;
|
||||
|
||||
while (*s)
|
||||
{
|
||||
char c = *s++;
|
||||
|
||||
v <<= 4;
|
||||
if (c >= '0' && c <= '9') { v |= (u64)(c - '0'); }
|
||||
else if (c >= 'a' && c <= 'f') { v |= (u64)(c - 'a' + 10); }
|
||||
else if (c >= 'A' && c <= 'F') { v |= (u64)(c - 'A' + 10); }
|
||||
else { break; }
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
void randomSetSeed(u32 seed)
|
||||
{
|
||||
#ifdef PORT
|
||||
/* GE_RSEED=<hex64>: test-only boot-seed override. boss.c seeds the PRNG
|
||||
* from osGetCount() (wall-clock on PC; RSP timer on N64), so the boot
|
||||
* seed differs every run -- with or without GE_DETERM, because the
|
||||
* virtual clock's pre-task advance count is still real-time dependent.
|
||||
* This pins it for determinism tests (E1) and N64 parity tests (E-B). */
|
||||
{
|
||||
const char *e = getenv("GE_RSEED");
|
||||
|
||||
if (e && e[0])
|
||||
{
|
||||
g_randomSeed = randomParseHex(e);
|
||||
osSyncPrintf("D318R: SETSEED OVERRIDDEN by GE_RSEED -> %016llx\n",
|
||||
(unsigned long long)g_randomSeed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
g_randomSeed = (u64)seed + 1; /* daddiu $a0,$a0,1; sd $a0, g_randomSeed */
|
||||
#ifdef PORT
|
||||
if (getenv("GE_D318R") != NULL)
|
||||
osSyncPrintf("D318R: SETSEED arg=%08x -> %016llx\n",
|
||||
seed, (unsigned long long)g_randomSeed);
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# D318 Session Handoff — PRNG investigation (Facility / level_34, Ourumov-Trevelyan sequence)
|
||||
|
||||
**Status: mid-investigation. No verdict change yet. All work uncommitted.**
|
||||
Previous context: `docs/HANDOFF.md`, README "Status", findings D318 in `docs/dev/findings.md`
|
||||
(look up via `docs/dev/findings-index.csv`). This file supersedes the old "next steps" list.
|
||||
|
||||
## 1. The problem (user-reported symptoms)
|
||||
|
||||
N64 ground truth (GEPD bundle emulator, user has it at
|
||||
`C:\Users\james\Games\Emulators\Nintendo 64\1964_GEPD_Edition\1964`): in the Facility
|
||||
tank-room sequence, guards line up and HOLD FIRE during Ourumov's (c78) monologue/countdown
|
||||
while Trevelyan (c67) kneels; no shooting until tanks detonate; ~1s later Ourumov executes
|
||||
Trevelyan; then guards open fire on Bond.
|
||||
|
||||
Port symptoms:
|
||||
- **Intermittent**: sometimes guards start shooting immediately (before/instead of the hold).
|
||||
- **Ourumov "bends down a bit too far"** vs N64 even in "normal" runs — animation suspect:
|
||||
possible misroute into the kneel-attack path `sub_GAME_7F0256F0` (selected by
|
||||
`act_attack.unk54`: 1=standing `sub_GAME_7F025560`, 0=kneeling). The standing path is what
|
||||
ai_22's AI_TRYFireOrAimAtTarget uses, so `unk54==0` during the countdown would be a bug.
|
||||
- Sequence triggers spottily vs original; user suspects a **foundational** issue (PRNG stream
|
||||
parity / tick pacing / objective register state) affecting other areas too.
|
||||
|
||||
## 2. What this session established (findings F1–F5)
|
||||
|
||||
- **F1 — Boot seed is wall-clock BY DESIGN, faithful to N64.** `src/boss.c:404`:
|
||||
`randomSetSeed(osGetCount())`. N64 RSP timer varies per power-on; PC shim (`port/src/libultra.c`
|
||||
`osGetCount`) feeds wall-clock scaled to 46.5 MHz. Per-run PRNG variation is original behavior,
|
||||
NOT a port bug. D284 (M-140) transform fix stands.
|
||||
- **F2 — Guard AI consumes the global PRNG heavily per tick.** Measured (15s headless
|
||||
`-level_34`, `GE_D318R=1`): top callers `chrCheckTargetInSight` (~7.7 draws/tick),
|
||||
`chrlvTickStand`, `ai`, `shuffle_player_ids` (3 draws EVERY main-loop iteration, even solo —
|
||||
`src/boss.c:597`). Guard behavior is strongly seed-dependent by design ⇒ N64 holding fire
|
||||
reliably under random per-boot seeds implies the **hold-fire gate is deterministic
|
||||
(objective-register-driven), not RNG-driven**. This reframes the "guards fire immediately"
|
||||
symptom toward objective-register/gate state rather than raw PRNG parity.
|
||||
- **F3 — Coarse sim state was run-to-run identical** in no-input runs (chr hash `h` matched at
|
||||
all samples) — weak check in an idle scene, not a determinism proof.
|
||||
- **F4 — Residual run-to-run divergence remains even with pinned seed + GE_DETERM=1.** Draw
|
||||
caller sequence diverges by draw ~346 (t≈15); 3548/4000 tick stamps differ. Suspect:
|
||||
**tick pacing, not the PRNG** — `waitForNextFrame()` (`src/game/frametiming.c`) derives
|
||||
`deltaFrames` from wall-clock elapsed time; `g_GlobalTimer += g_ClockTimer` (lv.c:1051) thus
|
||||
advances a jittery 0/1/2 per rendered frame. Under GE_DETERM the virtual clock advances on the
|
||||
scheduler thread and drifts against the main loop. N64 was single-threaded + VI-locked ⇒
|
||||
exactly one tick/frame. **This is the foundational issue: it exists in every level.**
|
||||
NOTE: not yet a clean verdict — GE_DETERM itself has pacing non-determinism (harness flaw).
|
||||
- **F5 — MinGW `strtoull` mangles hex values > LLONG_MAX** (returns sign-extended low 32 bits;
|
||||
`"deadbeefcafe0123"` → `ffffffffcafe0123`). Worked around with manual hex parsers in both new
|
||||
files. Remember for any future 64-bit hex env parsing.
|
||||
|
||||
Also: N64 `g_randomSeed` RAM address = **0x80024460** (u64, big-endian; found by
|
||||
instruction-pattern search). Initial constant 0xAB8D9F7781280783, but boss.c reseeds at boot.
|
||||
|
||||
## 3. Instrumentation added this session (all test-only, env-gated, port-layer)
|
||||
|
||||
| Env var | Behavior | File |
|
||||
|---|---|---|
|
||||
| `GE_D318T=1` | Timeline probe: obj-bit transitions, c78 phase/action/attack/entity changes, NEAR_MISS, c67 damage/death, **c78 anim internals on change** (`type_of_motion`, `unk54`, `endframe`, `frame1` — answers "bending down": `unk54==0` ⇒ kneel-route misfire), DET fingerprint line every 60 ticks (FNV-1a over all active chrs + full u64 seed + obj bits) | `port/src/d318watchdog.c` `d318TimelineTick()` (hooked in `src/game/lv.c` after `g_GlobalTimer += g_ClockTimer`) |
|
||||
| `GE_D318R=1` | Per-draw PRNG logger: first 4000 `randomGetNext()` calls as `D318R: n=<i> t=<tick> ra=<retaddr>`; also logs every `randomSetSeed` (`SETSEED arg=... -> ...`) and GE_RSEED overrides | `port/src/random.c` `randomDrawLog()` |
|
||||
| `GE_RSEED=<hex64>` | Pins the boot seed (overrides wall-clock) for determinism tests | `port/src/random.c` `randomSetSeed()` |
|
||||
| `GE_RSEED_LV=<hex64>[@tick]` | Injects a seed at a chosen tick (default 0) — A/B parity vs N64 RAM read; DET lines then verify stream parity | `port/src/d318watchdog.c` top of `d318TimelineTick()` |
|
||||
|
||||
Resolving `ra=` addresses: `nm build-pc/ge007.x86_64.exe` (has symbols, no line info) → nearest
|
||||
symbol below. Helper scripts in scratch: `d318r_cmp.py` (per-caller counts),
|
||||
`d318r_seqcmp2.py` / `d318r_seqcmp3.py` (sequence divergence; seqcmp3 compares caller-only vs
|
||||
tick stamps). Symbol dump: `scratch/syms.txt` (regenerate: `nm build-pc/ge007.x86_64.exe | awk '$2 ~ /[tTwW]/ {print $1, $3}'`).
|
||||
|
||||
Build is clean (`cmake --build build-pc`). Probe is silent without env vars (smoke-tested).
|
||||
|
||||
## 4. Experiments run (logs in scratch/)
|
||||
|
||||
- `d318t_det1/2.log`: two no-input runs, GE_D318T=1 → identical chr hash, divergent seeds
|
||||
(explained by F1: different wall-clock boot seeds).
|
||||
- `d318r_draws*.log` + `detC..F.log`: draw-caller analysis; `detE/F` = GE_DETERM=1 +
|
||||
GE_RSEED=0123456789abcdef → caller sequence diverges by draw 346 (F4).
|
||||
|
||||
## 5. Next steps (prioritized)
|
||||
|
||||
1. **E2 — USER playtest (needs user):** one BAD + one GOOD tank-room run with `GE_D318T=1`,
|
||||
capturing console: `ge007.x86_64.exe > scratch/d318t_playtest.log 2>&1` (MSYS2 MINGW64 shell,
|
||||
build via `./build-pc.sh ntsc-final`). The obj-bit timeline discriminates "gate opened early"
|
||||
(bit 0x04 set before it should) from "guards bypassing gate"; c78 anim-internals lines answer
|
||||
the bending-down question (`unk54`/`type_of_motion`/`endframe`).
|
||||
2. **E3 — USER reads N64 seed (needs user):** in 1964 debugger, read u64 at RAM **0x80024460**
|
||||
when Facility gameplay starts; report value. Then run port with `GE_RSEED_LV=<value>` and
|
||||
compare guard behavior + verify stream parity: DET lines log the running seed every 60 ticks;
|
||||
a second emulator read a few seconds later should match the port's DET seed if streams are
|
||||
in lockstep from the injection point. (Caveat: `-level_34` skips menu draws — inject at
|
||||
gameplay start, not boot.)
|
||||
3. **E1b — ME:** fix GE_DETERM main-loop/tick lock (or build single-threaded fixed-tick headless
|
||||
harness) for a clean determinism verdict on F4. Lower priority until E2/E3 data lands.
|
||||
4. If PRNG pacing (F4) confirmed as the behavioral culprit: it's a `port/`-layer fix candidate
|
||||
(frametiming/pacing), NOT a game-code edit — but diagnose fully first; document in findings.md.
|
||||
|
||||
## 6. Open questions / holds
|
||||
|
||||
- **D318 verdict is in question.** The committed-in-working-tree framing ("as-authored latent
|
||||
race, N64 has identical softlock") may be wrong: N64 reliably executes ~1s after detonation.
|
||||
The derail path (section 0x2a → off=307) IS the normal execution flow on N64. If N64 never
|
||||
freezes, the freeze branch is likely PC-specific — re-open D318 findings if E2/E3 confirm.
|
||||
- **HOLD the D318 commit** (working tree only, uncommitted): if the verdict flips, the watchdog
|
||||
(`port/src/d318watchdog.c` `d318WatchdogTick`, opt-out `GE_D318W=0`) should go opt-in or be
|
||||
replaced by the real fix before the README auto-recovery claim ships.
|
||||
- Regression-window suspect for guard behavior: D312/D313 commit (bullet-wall collision,
|
||||
last gameplay-affecting change before mostly-working Sep 18 playtest3).
|
||||
|
||||
## 7. Git status (uncommitted)
|
||||
|
||||
Modified: `.github/release-notes.md`, `.gitignore`, `README.md`, `docs/dev/agentic-development.md`,
|
||||
`docs/dev/findings-index.csv`, `docs/dev/findings.md`, `docs/index.md`,
|
||||
`docs/media/goldeneye-gh-preview.gif`, `src/game/lv.c` (D318 hooks), **`port/src/random.c`**
|
||||
(PRNG logging + GE_RSEED, this session).
|
||||
Untracked: **`port/src/d318watchdog.c`** (watchdog + timeline probe + seed injection),
|
||||
various `scratch/*` logs/scripts.
|
||||
|
||||
## 8. Key code map (verified this session)
|
||||
|
||||
- `src/boss.c:404-405` — boot PRNG reseed from osGetCount; `:596-597` — per-main-iteration
|
||||
`lvlManageMpGame()` + `shuffle_player_ids()`.
|
||||
- `src/game/lv.c:1023+` `lvlManageMpGame()` — g_ClockTimer (pause→0, else speedgraphframes),
|
||||
`g_GlobalTimer += g_ClockTimer` at :1051, D318 hooks right after.
|
||||
- `src/game/frametiming.c` — `waitForNextFrame()` wall-clock deltaFrames + D155 clamp
|
||||
(max 6) + D193 telemetry (`GE_D193=1`).
|
||||
- `src/game/model.c:4800+` `sub_GAME_7F072C10` — render-path function doing 3 PRNG draws per
|
||||
vertex (dorottex/rotating-texture effect); also model.c:5251/5310. Render-path PRNG
|
||||
consumption is a secondary parity concern (per presented frame, not per tick).
|
||||
- `src/game/player.c:637` `shuffle_player_ids()` — 3 draws/call.
|
||||
- `port/src/libultra.c:90+` — osGetCount wall-clock shim + GE_DETERM virtual clock (D117/M-52;
|
||||
known pre-task-window non-determinism).
|
||||
- `port/src/random.c` — PRNG transform (D284-verified) + this session's logging/overrides.
|
||||
- ai_22 offsets (from prior sessions): off=38 pre-aim, 44 wait loop, 69+ monologue cascade,
|
||||
272 execution label, 295 sets bit 0x04, 302 combat section 0x2a, 307 update, 321 sleep/stop.
|
||||
- CHRFLAG_NEAR_MISS = 0x4 (bondconstants.h), set chr.c:3806 (Bond bullet bounds), cleared
|
||||
per-tick (one-frame window).
|
||||
- N64 emulator: `C:\Users\james\Games\Emulators\Nintendo 64\1964_GEPD_Edition\1964` (user-provided).
|
||||
@@ -0,0 +1,23 @@
|
||||
import re, sys
|
||||
src = open('assets/obseg/setup/UsetuparkZ.c', encoding='utf-8', errors='replace').read()
|
||||
hdr = open('src/bondaicommands.h', encoding='utf-8', errors='replace').read()
|
||||
lens = dict(re.findall(r'#define (\w+)_LENGTH (0x[0-9a-f]+)', hdr))
|
||||
def dis(name):
|
||||
m = re.search(r'u8 %s\[\] = \{(.*?)\n\};' % name, src, re.S)
|
||||
body = m.group(1)
|
||||
off = 0; out=[]
|
||||
for line in body.splitlines():
|
||||
line=line.strip().rstrip(',')
|
||||
if not line: continue
|
||||
nm = line.split('(')[0].split()[0]
|
||||
L = lens.get(nm)
|
||||
if L is None:
|
||||
out.append(f"{off:5d} ?? {line[:60]}"); continue
|
||||
L=int(L,16)
|
||||
out.append(f"{off:5d} +{L:<2d} {line[:70]}")
|
||||
off+=L
|
||||
return out, off
|
||||
for name in sys.argv[1:]:
|
||||
lines,total = dis(name)
|
||||
print(f"== {name} (total {total} bytes) ==")
|
||||
for l in lines: print(l)
|
||||
@@ -0,0 +1,28 @@
|
||||
1785 ra=000000014002bcc5
|
||||
761 ra=00000001400359a5
|
||||
690 ra=000000014009be4a
|
||||
557 ra=000000014003c870
|
||||
64 ra=000000014002c9b1
|
||||
63 ra=0000000140028c85
|
||||
14 ra=0000000140028d7a
|
||||
9 ra=0000000140030848
|
||||
6 ra=000000014006091a
|
||||
6 ra=00000001400608e8
|
||||
6 ra=00000001400608b5
|
||||
6 ra=0000000140060881
|
||||
6 ra=0000000140060843
|
||||
6 ra=00000001400607fe
|
||||
6 ra=00000001400607b2
|
||||
2 ra=000000014000a4c9
|
||||
2 ra=000000014000a474
|
||||
1 ra=000000014009bb15
|
||||
1 ra=0000000140077391
|
||||
1 ra=0000000140077378
|
||||
1 ra=000000014007735f
|
||||
1 ra=000000014003041d
|
||||
1 ra=000000014002b145
|
||||
1 ra=00000001400220b1
|
||||
1 ra=000000014000a5d8
|
||||
1 ra=000000014000a5a5
|
||||
1 ra=000000014000a54f
|
||||
1 ra=000000014000a51c
|
||||
@@ -0,0 +1,28 @@
|
||||
1785 ra=000000014002bcc5
|
||||
761 ra=00000001400359a5
|
||||
690 ra=000000014009be4a
|
||||
557 ra=000000014003c870
|
||||
64 ra=000000014002c9b1
|
||||
63 ra=0000000140028c85
|
||||
14 ra=0000000140028d7a
|
||||
9 ra=0000000140030848
|
||||
6 ra=000000014006091a
|
||||
6 ra=00000001400608e8
|
||||
6 ra=00000001400608b5
|
||||
6 ra=0000000140060881
|
||||
6 ra=0000000140060843
|
||||
6 ra=00000001400607fe
|
||||
6 ra=00000001400607b2
|
||||
2 ra=000000014000a4c9
|
||||
2 ra=000000014000a474
|
||||
1 ra=000000014009bb15
|
||||
1 ra=0000000140077391
|
||||
1 ra=0000000140077378
|
||||
1 ra=000000014007735f
|
||||
1 ra=000000014003041d
|
||||
1 ra=000000014002b145
|
||||
1 ra=00000001400220b1
|
||||
1 ra=000000014000a5d8
|
||||
1 ra=000000014000a5a5
|
||||
1 ra=000000014000a54f
|
||||
1 ra=000000014000a51c
|
||||
@@ -0,0 +1,29 @@
|
||||
1789 ra=000000014002bcc5
|
||||
753 ra=00000001400359a5
|
||||
693 ra=000000014009be4a
|
||||
547 ra=000000014003c870
|
||||
64 ra=000000014002c9b1
|
||||
63 ra=0000000140028c85
|
||||
14 ra=0000000140028d7a
|
||||
11 ra=00000001400346c4
|
||||
9 ra=0000000140030848
|
||||
6 ra=000000014006091a
|
||||
6 ra=00000001400608e8
|
||||
6 ra=00000001400608b5
|
||||
6 ra=0000000140060881
|
||||
6 ra=0000000140060843
|
||||
6 ra=00000001400607fe
|
||||
6 ra=00000001400607b2
|
||||
2 ra=000000014000a4c9
|
||||
2 ra=000000014000a474
|
||||
1 ra=000000014009bb15
|
||||
1 ra=0000000140077391
|
||||
1 ra=0000000140077378
|
||||
1 ra=000000014007735f
|
||||
1 ra=000000014003041d
|
||||
1 ra=000000014002b145
|
||||
1 ra=00000001400220b1
|
||||
1 ra=000000014000a5d8
|
||||
1 ra=000000014000a5a5
|
||||
1 ra=000000014000a54f
|
||||
1 ra=000000014000a51c
|
||||
@@ -0,0 +1,37 @@
|
||||
import bisect, sys
|
||||
|
||||
syms = []
|
||||
for line in open("scratch/syms.txt"):
|
||||
p = line.split()
|
||||
if len(p) >= 2:
|
||||
try:
|
||||
syms.append((int(p[0], 16), p[1]))
|
||||
except ValueError:
|
||||
pass
|
||||
syms.sort()
|
||||
keys = [s[0] for s in syms]
|
||||
|
||||
def resolve(ra):
|
||||
a = int(ra, 16)
|
||||
i = bisect.bisect_right(keys, a) - 1
|
||||
if i < 0:
|
||||
return "??"
|
||||
b, n = syms[i]
|
||||
return "%s+%x" % (n, a - b)
|
||||
|
||||
def counts(p):
|
||||
d = {}
|
||||
for line in open(p):
|
||||
q = line.split()
|
||||
if len(q) >= 2:
|
||||
d[resolve(q[1].replace("ra=", ""))] = int(q[0])
|
||||
return d
|
||||
|
||||
a = counts("scratch/d318r_addrs1.txt")
|
||||
b = counts("scratch/d318r_addrs2.txt")
|
||||
allk = sorted(set(a) | set(b), key=lambda k: -(a.get(k, 0) + b.get(k, 0)))
|
||||
print("%-45s %5s %5s" % ("caller", "run1", "run2"))
|
||||
for k in allk[:18]:
|
||||
mark = " <== DIFFERS" if a.get(k, 0) != b.get(k, 0) else ""
|
||||
print("%-45s %5d %5d%s" % (k, a.get(k, 0), b.get(k, 0), mark))
|
||||
print("total draws:", sum(a.values()), sum(b.values()))
|
||||
@@ -0,0 +1,30 @@
|
||||
import re, sys
|
||||
|
||||
def seq(p):
|
||||
out = []
|
||||
for line in open(p, encoding="utf8", errors="replace"):
|
||||
m = re.search(r"n=(\d+) t=(\d+) ra=([0-9a-f]+)", line)
|
||||
if m:
|
||||
out.append((int(m.group(1)), int(m.group(2)), m.group(3)))
|
||||
return out
|
||||
|
||||
a = seq("scratch/detA.log")
|
||||
b = seq("scratch/detB.log")
|
||||
print("draws A:", len(a), " B:", len(b))
|
||||
|
||||
# find first divergence in (t, ra) sequence
|
||||
n = min(len(a), len(b))
|
||||
first = None
|
||||
for i in range(n):
|
||||
if a[i][1:] != b[i][1:]:
|
||||
first = i
|
||||
break
|
||||
if first is None:
|
||||
print("sequences identical over", n, "draws; length diff only")
|
||||
else:
|
||||
print("first divergence at draw index", first)
|
||||
for j in range(max(0, first - 3), min(n, first + 4)):
|
||||
mark = " <==" if j == first else ""
|
||||
sa = a[j]; sb = b[j]
|
||||
same = "same" if sa[1:] == sb[1:] else "DIFF"
|
||||
print(f" [{j}] A: t={sa[1]:4d} ra={sa[2]} B: t={sb[1]:4d} ra={sb[2]} {same}{mark}")
|
||||
@@ -0,0 +1,27 @@
|
||||
import re, sys
|
||||
|
||||
def seq(p):
|
||||
out = []
|
||||
for line in open(p, encoding="utf8", errors="replace"):
|
||||
m = re.search(r"n=(\d+) t=(\d+) ra=([0-9a-f]+)", line)
|
||||
if m:
|
||||
out.append((int(m.group(2)), m.group(3)))
|
||||
return out
|
||||
|
||||
a = seq(sys.argv[1])
|
||||
b = seq(sys.argv[2])
|
||||
print("draws:", len(a), len(b))
|
||||
n = min(len(a), len(b))
|
||||
first = None
|
||||
for i in range(n):
|
||||
if a[i] != b[i]:
|
||||
first = i
|
||||
break
|
||||
if first is None:
|
||||
print("FULL SEQUENCE IDENTICAL over", n, "draws (tick, caller)")
|
||||
else:
|
||||
print("first divergence at draw index", first)
|
||||
for j in range(max(0, first - 2), min(n, first + 3)):
|
||||
tag = "same" if a[j] == b[j] else "DIFF <=="
|
||||
print(" [%d] A t=%4d ra=%s B t=%4d ra=%s %s"
|
||||
% (j, a[j][0], a[j][1], b[j][0], b[j][1], tag))
|
||||
@@ -0,0 +1,35 @@
|
||||
import re, sys
|
||||
|
||||
def draws(p):
|
||||
out = []
|
||||
for line in open(p, encoding="utf8", errors="replace"):
|
||||
m = re.search(r"n=(\d+) t=(\d+) ra=([0-9a-f]+)", line)
|
||||
if m:
|
||||
out.append((int(m.group(2)), m.group(3)))
|
||||
return out
|
||||
|
||||
a = draws(sys.argv[1])
|
||||
b = draws(sys.argv[2])
|
||||
print("draws:", len(a), len(b))
|
||||
n = min(len(a), len(b))
|
||||
|
||||
# compare caller sequence only (ignore tick stamps)
|
||||
first_ra = None
|
||||
for i in range(n):
|
||||
if a[i][1] != b[i][1]:
|
||||
first_ra = i
|
||||
break
|
||||
if first_ra is None:
|
||||
print("CALLER SEQUENCE IDENTICAL over", n, "draws")
|
||||
else:
|
||||
print("first caller divergence at draw", first_ra)
|
||||
for j in range(max(0, first_ra - 3), min(n, first_ra + 4)):
|
||||
tag = "same" if a[j][1] == b[j][1] else "DIFF <=="
|
||||
print(" [%d] A t=%4d ra=%s B t=%4d ra=%s %s"
|
||||
% (j, a[j][0], a[j][1], b[j][0], b[j][1], tag))
|
||||
|
||||
# tick-stamp divergence count (pacing jitter)
|
||||
t_diff = sum(1 for i in range(n) if a[i][0] != b[i][0])
|
||||
print("draws with differing tick stamps:", t_diff, "of", n)
|
||||
if n:
|
||||
print("final tick: A t=%d B t=%d" % (a[-1][0], b[-1][0]))
|
||||
Whitespace-only changes.
@@ -1049,6 +1049,14 @@ void lvlManageMpGame(void)
|
||||
#endif
|
||||
#endif
|
||||
g_GlobalTimer += g_ClockTimer;
|
||||
#ifdef PORT
|
||||
{
|
||||
extern void d318WatchdogTick(void);
|
||||
extern void d318TimelineTick(void);
|
||||
d318WatchdogTick(); /* D318 permanent port-side deadlock recovery (findings D318) */
|
||||
d318TimelineTick(); /* D318T env-gated diagnostic timeline (GE_D318T=1) */
|
||||
}
|
||||
#endif
|
||||
if ((g_CurrentStageToLoad != LEVELID_TITLE) && (D_80048394 == 0) && (g_ClockTimer > 0))
|
||||
{
|
||||
if (g_AppendCheatSinglePlayer != 0)
|
||||
|
||||
Reference in new issue
Block a user