From 8f615bde1483a18f3e7e86c78d441dbbdf96cccd Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:18:48 -0400 Subject: [PATCH] =?UTF-8?q?docs(D223):=20new=20=E2=80=94=20directional=20m?= =?UTF-8?q?ouse-wheel=20weapon=20cycling,=20root-caused?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QoL ask: wheel up = next weapon, wheel down = previous, instead of both directions cycling forward. Root-caused: GE already computes separate weaponForwardOffset/weaponBackOffset signals from invButtons(A)+shootButtons(Z) held/edge combos (bondview2.c:5338-5347, the N64 'hold A tap Z' backward-cycle trick); triggerOn is suppressed while invButtons is held so this never accidentally fires. Port's inputPostWheel() discards wheel direction and only ever synthesizes the forward (bare A pulse) case. Fix is port-only (input.c), synthesize the A-held+G-edge combo for wheel-down instead - no game-logic change. Open question: which N64 controldef preset is actually active in port saves, since invButtons/shootButtons differ per preset. Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG --- docs/dev/findings-index.csv | 1 + docs/dev/findings.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index e7d52730..38784cdb 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -30,6 +30,7 @@ D196,OPEN,"OS mouse cursor stays visible after closing the F10 options overlay i D192,OPEN,Front-end mission/level-select grid pointer still can't reach the outer cells —,"OPEN — root-caused, not fixed (M-49)" D221,OPEN,SELECT FILE screen: the mouse hit-box for a file slot is vertically offset from its visible box —,"OPEN — observed, not investigated." D222,OPEN,High Video.FovScale breaks culling —,"OPEN — root cause known, not fixed." +D223,OPEN,QoL ask: mouse-wheel weapon cycling should be directional like a normal PC shooter —,"OPEN — root cause + fix mechanism known, not implemented." D204,FIXED,PC audio ran ~2 % below real time permanently:,"FIXED (port/src/audio.c, port/src/libultra.c, port/include/audio.h) —" D203,OPEN,"Steam Deck (SteamOS, x86_64 Linux) v0.1.0 release bundle:",OPEN — negative idle repros only; not yet reproduced with input. D201,FIXED,"src/libultra/audio/bnkf.c bank/instrument/sound/wavetable relocation offsets were s32, truncating the real 64-bit base pointer.",FIXED (src/libultra/audio/bnkf.c) — diff --git a/docs/dev/findings.md b/docs/dev/findings.md index aad15cce..192030f9 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -393,7 +393,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D190 | **Linux `-level_45` SIGSEGV at load in `add_ptr_to_objective` (`objective.c:64`) (M-48, open).** `objective_ptrs[objective->menu] = objective;` faults — `objective` wild or `objective->menu` a garbage index → OOB write. Almost certainly the D122/D126/D132 propDef-stride / ROM-serialized-struct-widening family (`struct objective_entry` / the type-23/type-1E setup-text record chain read at the wrong stride on PC). Found in the M-48 Linux level sweep: **5/6 spot-checked levels (`-level_09/20/27/34/37`) load + render fine; `-level_45` is the one failure.** Not a v0.1.0-alpha blocker (alpha is explicitly "expect crashes"). Windows: `-level_45` load status not re-checked this session. | OPEN — not investigated (M-48) | | D191 | **Windows `-O2` release-bundle crash: SIGSEGV in `modelGetNodeRwData` (`src/game/model.c:478`) — the `root->Parent` node-tree walk (M-49, user playtests of the v0.1.0 win64 bundle).** **Two instances, identical fault PC `0x1400801b1`:** (1) fresh save, killed the first guard right after escaping the cell in Bunker ii; (2) Statue (`-level_49`?), right after the Trevelyan meeting cutscene. `ge007.crash.log` (auto-dump; symbols intact in the shipped exe, `addr2line` resolves): PC = `modelGetNodeRwData` at `model.c:478` — the `switch (root->Opcode & 0xff)`, i.e. `root` (a `ModelNode *`) is bad; `addr2line` puts it in the inlined self-recursion at `:532`, so the `while (root->Parent) { root = root->Parent; … }` walk (`model.c:527-540`) handed a garbage node into the recursive call. Instance 1: `Rdx = 0x70267bc000000000` — a `~0x70266xxx` heap address in the **high 32 bits, zero low half** = 32-bit pointer field read as 64-bit (or a byte-swapped `u32` pair). Instance 2: `Rdx = 0`, FAULT ADDR `0x0` — `root` (or `root->Parent`) resolved to NULL. Both: `Rcx=Rdi` = the `~0x7008xxxx`/`~0x7007xxxx` object ptr, `R8 = 0x1d` (29). So the `Parent` chain in some model contains a **truncated or NULL link** — pointer-width / byte-swap family (D3x, D119 `weapons_held[]->chr` type-pun, D122/D126/D132 propDef-stride). NOT weapon-fire-specific — common factor is `modelGetNodeRwData` running on a character/cutscene model whose node tree has a bad `Parent`. Crash logs saved: `scratchpad/crashlogs/`. **Repro aid:** shipped exe has full symbols; `debug-crash.ps1` in the bundle dir runs it under gdb, `GE_D51=1` → `d52rw.log` rwdata-pool trace. The crash.c EBP backtrace breaks immediately (`#01: 0x1d`) — need a real `bt` + `p *root` + `p *Objinst` at the fault. Not yet reproduced under a debugger; no fix. | FIXED + PLAYTEST-VERIFIED (M-79). User replayed Bunker ii (first-guard kill) and Statue (post-Trevelyan cutscene) on the fix build — **both no longer crash** → 21/21 solo levels crash-free (v0.2.0 gate met; Cradle's remaining issue is D193, not a crash). Build clean; bunker1 golden framediff 3/3. **Real gdb backtrace (M-79):** the fault is NOT in the `root->Parent` walk — `root` arrives *already* garbage (`0x70267a9000000000` = a half-word-swapped `u32` `0x70267a90`) from frame #1 `bondviewSelectCuff(model=0x70076808, header=0x700763a0, switchindex=29)`, called from `gunUpdateAndFire(GUNRIGHT)` on the frame the player fires. `bondviewSelectCuff` byte-indexes `header->Switches` (a `ModelNode *` array — **8-byte stride on PC**, widened per D43/D45) with `offset = switchindex << 2` (N64 4-byte stride), so every `base[N]` deref lands mid-slot and yields a misaligned half-pointer that is non-NULL by luck → `modelGetNodeRwData(root=garbage)`. **Exactly the D140 bug class** — D140 fixed the identical pattern in `gunfire.c:1740`; this call site was missed. Fix: PORT-only `offset = switchindex * (s32) sizeof(ModelNode *)`; the `base = (u8*)switches + offset` math and the `#else` N64 line are unchanged. ABI/pointer-width only (D3x class), no game-logic change. The Statue instance (`Rdx=0`, NULL fault addr) is the same site landing on a slot whose bytes read as 0. | | D193 | **NPC AI locomotion is too slow — characters don't move to their destinations at the original game's rate (M-49, user playtest; clarified after the FULL 21-level playthrough — "the biggest overall flaw").** Not (primarily) an animation-playback bug — the *travel speed* of scripted/AI movement is wrong: guards, named NPCs (Trevelyan, Ourumov) and friendlies (Natalya) all take noticeably longer to run/walk to their goal positions than on N64. General, not chr-specific. **Leading hypothesis — the D117/D134/D155/D156 wall-clock-timing family:** `chr.c` / `chrai.c` integrate movement as `pos += speed * g_GlobalTimerDelta` and step sim loops `while (i < g_ClockTimer)` / `-= g_ClockTimer` (`chr.c:1437,1446,1491-1502,1862,2359,2569`; `chrai.c:76`). On the port `osGetCount()` is free-running wall-clock (D117), `g_GlobalTimerDelta`/`g_ClockTimer` are derived from it, and D155's `FRAMETIMING_PORT_MAX_CATCHUP = 6` clamps `nextFrameTime`. If the effective per-second sum of `g_GlobalTimerDelta` (or the `g_ClockTimer` tick count) runs low — quantization, the catch-up clamp biting during normal play, or a units mismatch (`g_JP_GlobalTimerDelta` vs `g_GlobalTimerDelta`, `chr.c:1435/1437`) — every delta-scaled AI motion integrates slow while **player** movement (input-driven per frame) still feels right, which is exactly the reported split. Related: **D170** (Ourumov/Trevelyan "flee" looks wrong — flagged there as possibly the same locomotion-speed issue rather than an AI divergence). Also possible: `chrGoToBond/Chr(SPEED_RUN/WALK/SPRINT)` speed enum → path-follow step, or pathfinding advancing ≤1 waypoint per frame-quantized tick. **Instances seen (M-49 full playthrough):** (a) Facility — the post-scripted-scene beat with Ourumov + the soldier squad: the soldiers **walk in super slowly and take a long time to path into firing positions** before they start shooting (this is regular squad combat AI, not a named-NPC script — good evidence the slowdown is general delta-scaled `chr` locomotion / path-following, not one ailist); (b) the named-NPC flee behaviour on Silo (Ourumov) and Cradle (Trevelyan) — see D170; (c) **Natalya (the escort NPC) always walks really slow** — on every escort level (Bunker ii, Statue, Control, Archives) she trails far behind, forcing the player to wait. Escort follow-AI is the same `chrGoToChr`/path-follow path as (a). **WORST CASE — Cradle (`-level_51`) is "bugged out completely" due to Trevelyan's behaviour (M-49).** The final level is Trevelyan-fleeing-and-scripted end to end (he runs the antenna cradle, climbs, drops to the cradle floor, the helicopter beat); if his locomotion timing / path-following is off he never hits the trigger points and the level's scripted progression stalls or goes haywire. This makes D193 effectively a **can't-properly-finish-the-game** bug, not just cosmetic sluggishness. **Next:** log `g_GlobalTimerDelta` + `g_ClockTimer` per frame during normal play and check the 1-second sum vs the N64's ~60; time a guard's run across a known distance vs reference footage; then watch Trevelyan's chr state / current AI command on Cradle. | **FIXED + user-playtest-verified (M-81) — root cause is D209**: `act_ubytes.padding[45]`, a raw-byte union alias for `act_gopos.unk59` (the SPEED tier), lands on offset 45 which is `unk59` on N64 but is byte 5 of `waypoints[1]` at 64-bit — always `0x00`. So the locomotion-animation selector `get_sound_at_range()` always received tier 0 and bound `ANIM_DATA_walking`/`walking_unarmed` for **every AI chr in the game**, and since travel is anim-root-motion driven they all moved at walk pace no matter what the ailist commanded. User verified on Cradle: Trevelyan and the guards run. Full elimination trail below — **steady-state timing keystone MEASURED and the "sim sums low" hypothesis is CONTRADICTED (M-80).** Added a `GE_D193=1` per-wall-second probe in `waitForNextFrame()` (`src/game/frametiming.c`, `#ifdef PORT`, env-gated): logs render frames, sim ticks delivered (= sum of the `deltaFrames` fed to `updateFrameCounters` = sum of `g_ClockTimer`), catch-up clamp hits, and max raw pre-clamp delta. Built ntsc-final, ran `-level_09 / _20 / _27 / _34` idle-after-load, ~20 s each. **Every level, rock-steady: render = 30.0 fps, sim = 60.0 ticks/s, clamped = 0, maxraw = 2.** i.e. each rendered frame carries `g_ClockTimer = 2` and the per-second sum is *exactly* the N64's ~60 — the wall-clock timer is NOT running low in steady state, and `FRAMETIMING_PORT_MAX_CATCHUP` never bites in normal play. VSync on/off made no difference (still 30/60). So the B1 premise ("`g_GlobalTimerDelta`/`g_ClockTimer` sum low over a real second") is wrong for the common case; D193 must come from elsewhere. **Residual quantization risk (not yet observed):** `nextFrameTime = (dt_counts + 387937) / 775875` is integer-rounded per frame with no remainder carry, so a scene that renders at a *steady* rate between 30 and 60 that is not an integer divisor of 60 (e.g. a solid 50 fps → dt≈20 ms → rounds to 1 → sim 50/s, ~17 % slow) would under-advance; the fix there is a fractional accumulator in the `#ifdef PORT` path. But the port renders a hard 30 (software RSP ≈ 60 µs/frame — not GPU-bound; the cap is the `pendingGfx < 2` double-buffer + sched round-trip), so this only matters if a real AI-heavy scripted scene drops into the 20–30 band. **Next (needs input, interactive session):** drive Bond into the Facility Ourumov/squad beat (D193a) and Cradle Trevelyan run with `GE_D193=1` — confirm render/sim hold at 30/60 under real AI load; if they do, pivot off timing entirely and instrument a single guard's world position vs. distance/time (anim root-motion rate, `chrGoToChr` SPEED_* enum → step size, path-node spacing) — the travel-speed bug is likely in the locomotion/anim path, not the frame clock. `GE_D193` catalogued in `docs/dev/GE-ENV-PROBES.md`. **M-80 cont. — LIVE-LOAD CONFIRMED, pivot is final.** User ran Cradle (`GE_D193=1`, `-level_41` — see the level-id note below) and drove Bond through the Trevelyan sequence while Trevelyan was visibly "walking really slow". Probe throughout: **`render=30 sim=60 clamped=0 maxraw=2`, dead steady** — no dips, no clamp hits, no `[D156]` guard fires. So under the *exact* failing scene the sim clock delivers a perfect 60 ticks/s. **D193 is NOT a timing/scheduler bug — it is in the character locomotion/animation path.** Best current hypothesis: GE character *travel* is animation-root-motion driven — `modelTickAnim` advances `frame += playspeed*speed` per tick (numticks = g_ClockTimer, correct), then `modelSetAnimFrame2WithChrStuff` (`model.c:3060+`) extracts per-anim-frame translation via `sub_GAME_7F06D3F4(...&pos)` and applies `scale = model->scale * model->anim_translation_scale`. The **player does NOT use anim root motion** (explicit speedgo/speedstrafe physics in bondview2.c), which explains the "player fine / all AI slow" split precisely. Suspects, in order: (1) the anim translation keyframe data is decoded with wrong units/scale by its sidecar converter (D43/model family); (2) `model->anim_translation_scale` or `model->scale` misread (ABI/struct-stride — D3x/D99 class); (3) `model->animrate`/`playspeed`/`speed` wrong. **M-80 cont. — port side instrumented (`GE_D193A` in `chrTick`, `GE_D193B` in `sub_GAME_7F06D3F4`; both `#ifdef PORT`, catalogued in GE-ENV-PROBES.md). Findings:** (i) `g_GlobalTimerDelta = 2.0`, `model->playspeed = 1.0`, `model->scale = 0.1`, `model->anim_translation_scale = 1.0` for every ticked chr on Silo + Facility — the scalars that feed the root-motion accumulation are all nominal, matching the identical N64 C (no `#ifdef PORT` in this hot path bar the non-firing D156 NaN guards). `model->animrate = 0.0` universally but that is the default — only written by `modelSetAnimPlaySpeed` on a blended transition (`startframe > 0`), so not the bug. (ii) Patrolling guards (`act=14 ACT_PATROL`, `model->speed=0.5`) travel **~60–99 world-u/s** on the port — no N64 number to compare against yet. (iii) `GE_D193B` (Facility, walk anim `706af4d8`): the decoded root-motion triple `tmp` ≈ (±0-3, ~1080-1094 hip height, ±0-50) with **`base = 0` and `angle = 0` on every call** (could be legit for a straight-line walk — joint 0 channel A at index 0, no per-frame facing change — or could be a mis-selected/zeroed channel; can't tell without the N64 side). **Bottom line: every port-side input to the locomotion math reads nominal**, so the divergence is either (a) subtle wrong magnitudes in the decoded anim bitstream values themselves (`tmp` triple / the descriptor decode), or (b) something in the `pos34` accumulation / `header->unk*` state — neither visible without a reference. `ModelSkeleton` **ruled out** — the skeletons are statically defined in C (`New_ModelSkeleton`/`MODELSKELETON` macros, `chrobjdata.c` etc.), not ROM-serialized, so `Joints` is a native x86-64 pointer and `ModelJoint` is 6 B on both platforms; `base` is correct. That leaves the ROM-serialized `ModelAnimation` bitstream (`bitDescriptors`/`bitStream`, D32-widened) and the anim-table load path as where a magnitude error could enter. **M-80 cont. — anim-data endianness (D33) audited, looks complete for this path.** `ptr_animation_table` is `romCopy`'d then byte-swapped by `romdataFixupAnimationData` (`port/src/romdata.c`, D33): per 20-byte record it bswaps `+0x00/+0x04/+0x08/+0x0C/+0x0E/+0x10` and per 6-byte `ModelAnimBitField` descriptor bswaps `+0 bitOffset` / `+4 valueOffset` (bitCount/pad identity); the bit*stream* is left raw (correct — `modelAnimReadRootMotionValue` walks it byte-by-byte, MSB-first, endianness-neutral, same as PD's `anim_read_bits`). `PTR_ANIM_walking/running/sprinting` **are** in `animation_table_ptrs1` so guard-locomotion records do get fixed up; `_ptrs2` is only 3 helicopter/plane anims (no overlap → no double-swap). No `bad descriptor range` / `bad record offset` errors in any run. Every `anim->unkNN` the locomotion path reads (`unk04`, `unk06`, `unk07`, `unk0C`) is within the fixed-up 0x14 bytes. `sizeof(ModelAnimation)` = 64 B both platforms (D32 kept the embedded ptrs as `u32`); `sizeof(ModelAnimBitField)` = 6 B both. So the anim decode is structurally sound as far as static review goes — **a magnitude error, if any, needs the N64 reference to see.** **Next — two viable paths (needs a fresh session; toolchain not present here — no `mips-linux-gnu-*`, no `qemu-irix`, no emulator):** (A) set up the MIPS toolchain + IDO-recomp + an N64 emulator with `osSyncPrintf` capture, build the ROM with `#ifndef PORT` `GE_D193B`-equivalent logging, navigate to the same scene, diff `tmp`/`base`/`angle`/travel-speed against the port capture; (B) user records N64 footage (real HW or their emulator) of one guard traversing a measured distance, frame-count it → gives the port-vs-N64 speed ratio (a clean 2× would point at a per-frame vs per-tick doubling; a fractional ratio at a scale/data error). Recommend (B) first — cheap, and the ratio narrows (A) massively. **Highest-impact playtest finding — breaks Cradle.** **Level-id note:** the earlier "Cradle = `-level_51`" in this row and HANDOFF was WRONG — `LEVELID_CRADLE = 41` (`src/bondconstants.h:1662`; STATUE=22, so CRADLE = 22+19). `-level_51` = `LEVELID_EAR` (unused slot) and SIGSEGVs at load in `stanBuildRoomData` (`stan.c:281`, NULL deref) — a non-issue (bad level id), noted here only so it isn't re-investigated as a real crash. **M-81 — SOLVED, and none of the M-80 suspects were the cause.** Re-verified the whole timing chain independently (`waitForNextFrame` -> `speedgraphframes` -> `g_ClockTimer` -> `g_GlobalTimerDelta`; `g_GlobalTimer += g_ClockTimer` at `lv.c:1022` is **tick**-based, so the ~75 AI timeout comparisons against it are frame-rate independent) and re-checked the D33 fixup *widths* against the real structs (they match exactly, and `(bs-bd) % 6 == 0` passing is independent evidence the two embedded pointers decode correctly). Root-motion accumulation is self-consistent (`pos24 = pos34 + rotate(pos)` at `model.c:3322`, so `pos` is a per-frame delta in x/z and an absolute hip height in y — matching the observed `tmp` shape). Extending `GE_D193A` with the visibility-gate counters (`tick`/`move`, i.e. chrTick entries vs `chrUpdateAnim` calls) killed the culling hypothesis too — chr 0 is `31/31` every second. The tier was then shown correct (`tier=1 SPEED_RUN`) and the *animation* wrong (`aidx=40/107`, never 42/41), which is **D209**. Note for future readers: `chrlvApplySpeed`'s `speedPtr` out-param is a **turn** rate, not a travel rate (`chraction.c:8070` passes `&act_runpos.turnspeed` into it), so `act_gopos.speed == 0` just means "running in a straight line" — it is not evidence of a stalled character. | -| D194 | **Mouse input needs another tuning pass — RMB aim over-sensitive + sensitivity couples to frame/sim rate (M-49, user playtest).** Two parts. **(a) Aim mode (RMB) is near bang-bang, not proportional** (`port/src/input.c:697-707`): any per-poll delta past `AIM_MOVE_THRESH` emits stick `m = clamp(61 + |Δ|·(MouseAimSpeed/100)·AIM_GAIN, 61, 60+AimBand)` = `[61,80]` — with the M-29 defaults (`MouseAimSpeed=16`, `AIM_GAIN=4.0`, `AimBand=20`) `m` is already 61 (fast) at ~1.5 px/poll and saturates at 80 by ~30 px/poll, so the usable proportional band is tiny and sits near the top of GE's aim range → "way too sensitive," turn rate barely tracks how fast you actually move the mouse. Needs a real response curve (low floor, wider proportional band, maybe a gamma), not a threshold+clamp. **(b) Overall sensitivity correlates with movement/frame rate** (user's words: "correlated with movement speed, does not feel good"): `input.c` emits raw stick counts **per controller poll** with no `g_GlobalTimerDelta` / polls-per-frame normalization (hipfire yaw `sx += edx·(MouseTurnSpeed/100)·6.0` at `:709`; aim as above). GE integrates the stick scaled by its sim delta, and the port's poll cadence vs render/sim cadence drifts with scene load — so effective sensitivity shifts when the framerate does (which reads as "when I'm moving"). Fix direction: accumulate mouse delta and convert to stick using a fixed reference dt (or feed the game a dt-consistent value), so a given hand motion = a given view rotation regardless of fps. Lineage: D118a (hipfire pitch digital vs analog), D165 (menu pointer), D166 (hipfire pitch pulses), D180-B3 (`MouseAimSpeed` 25→16). Config knobs exist (`Input.MouseAimSpeed/AimBand/MouseTurnSpeed/HipfirePitchSpeed`) but the *shape* of the mapping is the problem. **M-87 design-goal note (user):** overall mouse feel should move toward **Quake Remaster / the Perfect Dark PC port** — raw, direct, responsive 1:1 mouse-look — while explicitly **not losing GE's original aiming nuance** (the analog-stick aim-mode vs digital-hipfire split this whole D118/D194 lineage exists to preserve). Reframes the fix direction from "just widen the proportional band" to: normalize by a fixed reference dt first (part (b), the frame-rate coupling — this alone should fix most of "feels unresponsive"), then re-derive the aim-mode response curve, and treat raw hipfire mouse-look (PD/Quake-style, bypassing the N64 digital-stick emulation entirely when the player is *not* in aim mode) as a real design option to evaluate against "keep GE's aim-mode feel," not assume the N64 stick-emulation path is mandatory everywhere. | OPEN — root-caused, not fixed (M-49) | +| D194 | **Mouse input needs another tuning pass — RMB aim over-sensitive + sensitivity couples to frame/sim rate (M-49, user playtest).** Two parts. **(a) Aim mode (RMB) is near bang-bang, not proportional** (`port/src/input.c:697-707`): any per-poll delta past `AIM_MOVE_THRESH` emits stick `m = clamp(61 + |Δ|·(MouseAimSpeed/100)·AIM_GAIN, 61, 60+AimBand)` = `[61,80]` — with the M-29 defaults (`MouseAimSpeed=16`, `AIM_GAIN=4.0`, `AimBand=20`) `m` is already 61 (fast) at ~1.5 px/poll and saturates at 80 by ~30 px/poll, so the usable proportional band is tiny and sits near the top of GE's aim range → "way too sensitive," turn rate barely tracks how fast you actually move the mouse. Needs a real response curve (low floor, wider proportional band, maybe a gamma), not a threshold+clamp. **(b) Overall sensitivity correlates with movement/frame rate** (user's words: "correlated with movement speed, does not feel good"): `input.c` emits raw stick counts **per controller poll** with no `g_GlobalTimerDelta` / polls-per-frame normalization (hipfire yaw `sx += edx·(MouseTurnSpeed/100)·6.0` at `:709`; aim as above). GE integrates the stick scaled by its sim delta, and the port's poll cadence vs render/sim cadence drifts with scene load — so effective sensitivity shifts when the framerate does (which reads as "when I'm moving"). Fix direction: accumulate mouse delta and convert to stick using a fixed reference dt (or feed the game a dt-consistent value), so a given hand motion = a given view rotation regardless of fps. Lineage: D118a (hipfire pitch digital vs analog), D165 (menu pointer), D166 (hipfire pitch pulses), D180-B3 (`MouseAimSpeed` 25→16). New adjacent QoL ask, root-caused → **D223** (mouse-wheel direction should reverse weapon cycle direction, currently both directions cycle forward). Config knobs exist (`Input.MouseAimSpeed/AimBand/MouseTurnSpeed/HipfirePitchSpeed`) but the *shape* of the mapping is the problem. **M-87 design-goal note (user):** overall mouse feel should move toward **Quake Remaster / the Perfect Dark PC port** — raw, direct, responsive 1:1 mouse-look — while explicitly **not losing GE's original aiming nuance** (the analog-stick aim-mode vs digital-hipfire split this whole D118/D194 lineage exists to preserve). Reframes the fix direction from "just widen the proportional band" to: normalize by a fixed reference dt first (part (b), the frame-rate coupling — this alone should fix most of "feels unresponsive"), then re-derive the aim-mode response curve, and treat raw hipfire mouse-look (PD/Quake-style, bypassing the N64 digital-stick emulation entirely when the player is *not* in aim mode) as a real design option to evaluate against "keep GE's aim-mode feel," not assume the N64 stick-emulation path is mandatory everywhere. | OPEN — root-caused, not fixed (M-49) | | D195 | **Transparency / alpha surfaces broken on Control (M-49, user playtest)** — Natalya's console room, the glass partitions + translucent projected wall displays. Failure mode not yet specified by the reporter (no `GE_PCDUMP` capture taken). Likely a fast3d render-mode / alpha-blend path defect, not a decode one — neighbours D161 (CI8/LUT decode), D172, D176(b) (both blend-adjacent) but those are texture-format bugs, this reads as a blend-state one; check D128 portal adjacency in case the glass geometry is portal-culled wrong instead. Logged in `GRAPHICS-BACKLOG.md`. **Next:** `GE_PCDUMP` capture on Control near the console room, compare RDP render-mode words against N64 `gmain.s` for the glass/display materials. | OPEN — not investigated (M-49), needs a capture | | D197 | **Character face/head textures wrap around the head, seen on Silo (M-49, user playtest).** Reads as `G_TX_CLAMP` not honoured (or a non-power-of-two wrap period) on a head/face texture tile — the texture repeats around the mesh instead of clamping at the UV edge. Not yet isolated to a specific character or texture; no `GE_PCDUMP`/`GE_TEXDUMP` capture taken. Likely fast3d texture-tile setup (clamp/wrap mode bits from the tile descriptor not round-tripped) rather than a data bug, given it's cosmetic and geometry-independent. **Next:** `GE_TEXDUMP` the offending head texture's tile descriptor on Silo, diff clamp/mirror bits against `gmain.s`'s expectations for that material. | OPEN — not investigated (M-49), needs a capture | | D198 | **`alAdpcmPull`/`_decodeChunk` used `K0_TO_PHYS` (an unconditional `& 0x1FFFFFFF` mask) on 64-bit heap pointers (`src/libultra/audio/load.c`).** `K0_TO_PHYS` is the N64 kseg0→physical mask, valid only because N64 pointers fit in 32 bits; on PC's 64-bit heap it truncates any pointer whose value exceeds 512MB, corrupting the `aLoadADPCM`/`aSetLoop`/`aADPCMdec` DRAM addresses at 3 call sites (`load.c:71`, `:468`, `:474`). Every sibling call site in `env.c`/`resample.c`/`reverb.c` already resolves state pointers via `osVirtualToPhysical` (the port shim, `port/src/libultra.c`), which is bit-identical to `K0_TO_PHYS` for real N64 kseg0 addresses but correctly PC-shimmed. **Fix (ABI-layer, `#ifdef`-free — same call resolves correctly on both platforms):** replace the 3 `K0_TO_PHYS(...)` calls with `osVirtualToPhysical(...)`. No behavior change on N64 (identical result for kseg0 pointers); fixes PC. Surfaced while building the Phase-3 software mixer (D199) — the acmd list these opcodes feed now actually executes on PC instead of being ignored. | FIXED (`src/libultra/audio/load.c`) — part of the Phase-3 mixer landing, not yet build/runtime-verified in isolation | @@ -403,6 +403,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D192 | **Front-end mission/level-select grid pointer still can't reach the outer cells — but only under `Input.MouseCaptureMode=0` (legacy always-grab) (M-49, user playtest).** D169 was marked FIXED (M-33) by replacing the hard-coded 320×240 pointer clamp with the live `getPlayer_c_screenwidth/height/left/top()` rect; the feel-check was flagged "owed" on both D169 and D180 and never done. It now reproduces: with `MouseCaptureMode=0` the menu pointer runs the relative-delta P-controller path (`port/src/input.c:659-690`), which clamps target+estimate to `getPlayer_c_screen*` — but that accessor returns `g_CurrentPlayer->c_screenwidth` (`src/game/bondview.c:882`), which in the front end is the ~320×240 stage viewport, **not** the front end's actual 440×330 (`front.c:8570` `viSetViewSize(440,330)`). So the effective clamp is still ~300/220 while the mission-select grid's outer split points sit at 317 / 235.5 (`front.c:516/519/3180/3194`) — outer columns + bottom row unreachable, P-controller winds up and never settles. `MouseCaptureMode=1` (the shipped default, click-to-lock) writes `cursor_h_pos`/`cursor_v_pos` directly from the absolute OS cursor (`input.c:629-657`) and *feels* fine, though it shares the bad `hiH` so the last few px at the right/bottom edge may still be short. **Not a v0.1.0 blocker** — default mode is acceptable, always-grab is opt-in via INI, front-end roughness is already in the release notes. **Fix (port-only, small):** in the `menuMode` branch, when `current_menu` is a front-end menu use the real front-end rect (hard-code `front.c`'s cursor clamp `[20,420]×[20,310]`, or read the front-end viewport global) instead of routing through `g_CurrentPlayer`. Confirm with one `GE_INPUTLOG=1` capture in mission-select — sweep the mouse to each corner, watch where `cursor=(x,y)` plateaus. D165 · D169 · D180 lineage. | OPEN — root-caused, not fixed (M-49) | | D221 | **SELECT FILE screen: the mouse hit-box for a file slot is vertically offset from its visible box — you have to click *below* a file to select it, hovering directly over it doesn't (M-87, user QA, default `MouseCaptureMode=1`).** Different symptom from D192 (D192 is an unreachable-outer-cells *clamp range* bug under legacy mode 0; this is a per-row hit-test/highlight *offset* bug under the shipped default mode 1) — likely adjacent, same front-end grid-coordinate family. | **OPEN — observed, not investigated.** Next: find the SELECT FILE slot hit-test rects (`front.c`, mission/file-select grid split points, same neighborhood as D192's `front.c:516/519/3180/3194`) vs. the rects used to draw the highlight/box; compare row-height/origin assumptions — a hit-test rect keyed to a baseline/bottom origin while the drawn box uses a top origin would produce exactly "click below to select." | | D222 | **High `Video.FovScale` breaks culling — NPCs and some world objects stop rendering near the screen edges (M-87, user QA).** Split out of D211's noted residual: `currentPlayerSetCameraScale()` / the game's frustum-cull and screen↔world math still run at the **nominal, unscaled** fovy while the actual render projection is widened by `portFovScale` (`fr.c:737`) — so objects that are on-screen in the widened view but outside the nominal-FOV frustum get culled before they reach the renderer. Confirmed **not cosmetic** — a real, noticeable functional regression at high FovScale, not just "culls a hair early." | **OPEN — root cause known, not fixed.** Fix direction: find every frustum-cull / visibility-test call site that derives its frustum from the nominal fovy (`currentPlayerSetCameraScale` and friends, `bondview2.c`/`fr.c`) and feed it `frFovY` (the `portFovScale`-adjusted value) instead, gated the same way as the D211 render-path change (`!= LEVELID_TITLE`, `#ifdef PORT`). Must not change cull behavior at `FovScale=100` (no-op parity). Risk: some of these paths may double as gameplay-affecting AI visibility/awareness checks, not just render culling — audit which before changing (portFovScale must never leak into NPC AI *behavior*, only rendering-adjacent visibility). | +| D223 | **QoL ask: mouse-wheel weapon cycling should be directional like a normal PC shooter — wheel up = next weapon, wheel down = previous — instead of both directions cycling forward (M-87, user QA).** **Root-caused, port-only fix, no game-logic change needed** — GE's engine already has separate forward/backward weapon-cycle signals: `bondview2.c` computes `moveData.weaponForwardOffset = edge(invButtons) && !(shootButtons held)` and `moveData.weaponBackOffset = (invButtons held) && edge(shootButtons)` (`bondview2.c:5338-5347`, the classic N64 "hold A, tap Z to cycle backward" trick). Default control scheme (the `else` branch used unless a Domino/Goodhead/Galore/Plenty preset is active, `bondview2.c:5177-5179`): `invButtons = A_BUTTON` (= port `GE_CONT_A`, `input.c:86`), `shootButtons = Z_TRIG` (= port `GE_CONT_G`, `input.c:88`, the fire button). `moveData.triggerOn` is explicitly suppressed while `invButtons` is held, so holding A never causes an accidental shot when the Z edge lands. The port's `inputPostWheel()` (`input.c:977`) currently does `if (notches < 0) notches = -notches` — **discards wheel direction** — and only ever synthesizes a bare `GE_CONT_A` pulse (`wheelPulse`, `input.c:631-634`), i.e. always "forward." | **OPEN — root cause + fix mechanism known, not implemented.** Plan: keep `wheelPulse` (or a new `wheelPulseFwd`) for wheel-up as today; add a `wheelPulseBack` path for wheel-down that instead holds `GE_CONT_A` across ≥2 polls while presenting a fresh `GE_CONT_G` edge partway through (mirroring the invButtons-held + shootButtons-edge combo) — sequence matters (A must already read "held" `oldbuttons`-wise before G's edge, per `weaponBackOffset`'s check order). Must confirm which N64 control preset (`controldef`) the port's save data actually carries — `invButtons`/`shootButtons` differ under KISSY/GOODNIGHT/SOLITARE/etc; if the port doesn't force one, the synthesized combo needs to match whatever's active, or the port should just force the default (HONEY-equivalent) preset since its own `Input.Bind` system already replaces N64-style scheme selection. Port-only, `#ifdef PORT` in `input.c`, no `src/` game-logic touch. | | D204 | **PC audio ran ~2 % below real time permanently: GE's AI feedback loop (`src/audi.c:531`) has a ~3 ms setpoint that PC scheduling jitter clears, so the SDL queue starves and the device pads playback with silence (FIXED, measured).** `amMain` wakes at 30 Hz (`sched.c:334` forwards every 2nd retrace to the audio client) and asks for `g_MinFrameSize`=720 samples per block = 21600/s against a 22050 Hz device — structurally 2 % short, relying on 784-sample top-up blocks that `audi.c` only requests once the reported AI length falls under ~69 frames (3 ms). Fine on N64 (double-buffered AI, exact VI interrupt); on PC the queue empties before the loop reacts, and the padded silence is unrecoverable time. **Fix (F5, port-only):** `audioGetAiLengthBytes()` subtracts a 1024-frame (~46 ms) cushion before reporting, so the loop tops up while slack remains; `audi.c`'s control law untouched (rule #2 clean). Measured A/B in one binary via `GE_D204_OLD`: **rt 0.980 → 1.000, q min 0 → 368, drop 0.** Also: F1 correct AI_LEN_REG single-buffer semantics (robustness, measures as a no-op), F3 `Audio.QueueLimit` 8192→2880 + non-silent drops (NB: an existing `ge007.ini` pins it), F4 oversize invariant guard, and `GE_D204=1` cheap audio-health monitor (`rt`/`q`/`drop`/`max`, safe to leave on for a full playtest). **Falsified in-session, do not re-open:** the u32 wrap at `audi.c:531` is real (high-water 2064 frames > the 789 threshold) but harmless — `frameSamples` is `s16` (`audi.c:145`), so it lands negative and audi.c's own lower clamp catches it; max block stays 3136 B vs the 3156 B allocation, no heap overrun. **Corrects two M-63 claims:** `soundIndex=109` is guards' return fire (count was double-logged; the "32.6 ms cadence" is just the 2880-byte block quantum every `dumppos` is rounded to) — closed negative; and M-63's "13 % of real time" was `GE_MIXERTRACE`'s own 25 MB unbuffered log starving the audio thread (same repro without it: 58.0 s / 60 s). Does NOT explain D202. | FIXED (`port/src/audio.c`, `port/src/libultra.c`, `port/include/audio.h`) — measured; a by-ear pass on a real playthrough still owed | | D203 | **Steam Deck (SteamOS, x86_64 Linux) v0.1.0 release bundle: Facility (`-level_34`) crashes "after loading in as James Bond" (user bug report).** Idle repro attempts on WSL Ubuntu with the actual `dist/goldeneye-pc-port-0.1.0-linux-x86_64.tar.gz` are **negative**: 120 s idle → frame 3300+ clean exit (the intro auto-advances, so "sitting as Bond" is covered), and a ~4-min stick-forward-only run → frame 6900+ also survived. So the trigger needs real gameplay input (fire/action) or is Deck-environment-specific. **Leading suspect: the D191 family** — both known v0.1.0 field crashes are SIGSEGVs in `modelGetNodeRwData` (`model.c:478`) over a truncated/NULL `root->Parent` node link, and both were triggered by *gameplay state* (first guard kill on Bunker ii; post-cutscene on Statue), never by idling; Facility opens into the Ourumov + soldier-squad scripted beat (D193a instance), which fits the trigger profile. Alternatives: Deck GL driver path (RADV/zink vs llvmpipe) or 40/50/60 Hz timing (D193 family). **Next:** (1) WSL run under gdb with a walk+fire `GE_INPUTSCRIPT` — generate the script into a file first (earlier attempts were blocked by a `bash -lc` quoting quirk that emptied the loop var, so no fire buttons ever went out); on SIGSEGV compare the fault PC against `modelGetNodeRwData`; (2) if not reproducible on WSL, get `ge007.crash.log` from the Deck (the Linux build writes it — proven by a stale one in the smoke tree) + ask what Bond was doing at crash time; optionally a gdb run from the SteamOS terminal. | OPEN — negative idle repros only; not yet reproduced with input. | | D201 | **`src/libultra/audio/bnkf.c` bank/instrument/sound/wavetable relocation offsets were `s32`, truncating the real 64-bit base pointer.** `alSeqFileNew`/`alBnkfNew` compute `offset = (s32) base` from a heap pointer and thread it through `_bnkfPatchBank/Inst/Sound/WaveTable` as `s32 offset, table` parameters, used to rebase every embedded pointer field in a loaded bank/inst/sound/wavetable/book/loop record. On N64 (32-bit pointers) the truncation is a no-op; on PC, sign-extension of the truncated `s32` during the rebase arithmetic reconstructs the wrong 64-bit address whenever the base allocation's low 32 bits have the top bit set (~50% of allocations, allocator-dependent) — corrupting every relocated pointer in the bank file, a plausible contributor to bad model/audio pointers surfacing downstream (D191-family symptoms are a node-tree walk, not this, but the class is the same). **Fix (ABI-layer, no logic change):** widen `offset`/`table`/`woffset` and all four `_bnkfPatch*` parameters from `s32` to `uintptr_t` (``) — already this codebase's idiom for pointer-safe integers in `src/game/*.c`. No behavior change on the 32-bit N64 build. | FIXED (`src/libultra/audio/bnkf.c`) — part of the Phase-3 mixer landing, not yet build/runtime-verified in isolation |