From 50d73c519ccc05c199e0270d58aeae899f17ecd2 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:06:03 -0400 Subject: [PATCH] =?UTF-8?q?D217=20(M-90):=20align=20texpool=20arena=20base?= =?UTF-8?q?s=20to=208=20bytes=20on=20PC=20=E2=80=94=20fixes=20solid=20wron?= =?UTF-8?q?g-colour=20weapon/model=20CI=20tiles=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * D217 (M-90): align texpool arena bases to 8 bytes on PC — fixes solid wrong-colour weapon/model CI tiles Root cause: texAlignIndices pads CI index rows to ABSOLUTE 8-byte boundaries, and model-DL G_LOADTLUT palette offsets are a formula that only matches the real layout when the texpool base is 8-aligned. On N64 the memp bump allocations feeding these pools happened to be 8-multiples; PC pointer bloat (D36/D37-class struct growth) made some STAGE-bank sizes not, so weapon-pool bases land %8==4 (e.g. FACILITY). Every palette in a misaligned pool then sits 4 bytes early inside its allocation while the draw reads the N64 offset: the PP7 grip's count=1 TLUT lands on the next tenant's 8-byte header slot (texnum=26 -> LE s16 -> 0x1A00 = the green), and larger loads read a 2-entry zero pad then the real palette (the M-86 'leading zero pad' tell). BUNKER1 was clean only because its bases happened to be aligned. Fix: texInitPool rounds the arena base up to 8 under #ifdef PORT; end unchanged (pool loses <=7 bytes). No-op on N64 where bases were already aligned. ABI/layout-class edit per AGENTS.md rule 2 narrow exception — no game logic touched. Verified: FACILITY 188/188 decodes %8==0, draw reads pal0=0001 (was 1a00), 0 green pixels in frames 250-450 (pre-fix ~1300); BUNKER1 + DEPOT still clean; full verify.sh sweep 21/21 PASS. Pending: human playtest eyeball of the M-87 weapon matrix (closes defect 1). Defect 2 (CI cache content-hash key, fix/d217-model-texture-palette) remains separate/parked. findings.md: new section H 'D217 - M-90' + D217 row update; GRAPHICS-BACKLOG D217 status. * docs(D217): M-92 - human playtest of PR #47 confirms PP7/PPK/watch fixed; AK47 + NPC-held world-model weapons are a distinct, still-open white-texture residual Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG * docs(D217/D228): M-97 human playtest - green FIXED, AK47 white patches split out as D228 Second independent playtest (after M-92) of PR #47 on FACILITY, this time with the branch merged up to date with main. Confirms the same split the M-90 write-up predicted: PP7/PPK grip+hammer, watch face and world-model green are all fixed by the texpool alignment; NPC-held AK47 white missing-texture patches are untouched. White is a different signature from wrong-colour -- a wrong palette binds a real TLUT with wrong contents, white means the texture was never decoded/uploaded at all -- so this is a separate mechanism, not an incomplete fix. Split out as D228 so D217 can close on the green mechanism it actually describes instead of carrying an unrelated open failure mode indefinitely. --- docs/dev/GRAPHICS-BACKLOG.md | 2 +- docs/dev/findings-index.csv | 1 + docs/dev/findings.md | 189 ++++++++++++++++++++++++++++++++++- src/game/image.c | 20 +++- 4 files changed, 208 insertions(+), 4 deletions(-) diff --git a/docs/dev/GRAPHICS-BACKLOG.md b/docs/dev/GRAPHICS-BACKLOG.md index 4c8df485..1d8b321b 100644 --- a/docs/dev/GRAPHICS-BACKLOG.md +++ b/docs/dev/GRAPHICS-BACKLOG.md @@ -22,7 +22,7 @@ once the game is playable start-to-finish without crashes. | ~~**D178**~~ | ~~Objectives not listed correctly on the pre-mission briefing screen (the mission overview shown before starting a level).~~ | **CLOSED — user-verified fixed (M-87 playtest): "main menu text is good now."** No longer reproduces. M-36 triage below kept for reference. **M-36 triage: the objective lines are BLANK, not wrong.** `GE_STARTMENU=10` Dam briefing renders the header fine ("OHMSS / Agent: James Bond / Mission 1: Arkangelsk / Part i: Dam / PRIMARY OBJECTIVES:") then just the bullet "a." with no text and no b/c/d. → **D143/D151 blank-text family** (`langGet()` → NULL → silent blank), *not* a propDef walk / difficulty decode. **Family B** in `docs/dev/M-36-TRIAGE.md` — find which lang bank the front-end briefing flow fails to load. Re-confirm the exact shape in a *real* briefing (the `GE_STARTMENU` boot skips real save state). Close relatives: D151 (watch objective text blank — type-35/23 `text` slot u16→u32 fix, M-28; interactive re-verify was still pending), D143 (briefing/objective text blank via `langGet` NULL), D157 (objective difficulty byte offset — could make difficulty-gated objectives appear/disappear wrongly). The pre-start briefing is a *different surface* from the in-level watch, so D151's fix may not cover it. **Next:** ask user which level + what exactly shows vs expected; compare against `docs/dev/LEVEL-OBJECTIVES.md` (regenerated post-D157); if lines are blank → D143/D151 family (`langGet` NULL guard = silent blank), if wrong objectives listed → propDef type-23 record walk / difficulty decode. | | **D175** | In-game stutter / brief hang during normal play — observed opening a door on Runway (mission 3, `-level_35`) and also on Surface. No backtrace captured (gdb attach missed the window; it self-recovered). | **Observed, not investigated** (user QA report). Matches known transient-hang classes rather than looking new: D155 (post-slow-load catch-up spiral — clamped), D156 (anim NaN loop — guarded, prints one-shot stderr diag if it fires), D134 (SP/DP task-done event swallowed — fixed M-22, Runway was an original victim), D147/D152 (audio `s_imLock` steal = ~2 s hiccup). Door-open specifically = new room first visible → one-off texture-import/frame spike (likely benign). **Next:** reproduce under `tools_pc/debug.ps1` with the box idle; check console for the D156 stderr diag; if it recurs, attach gdb mid-hang and pattern-match `bt` (import_texture = benign slow frame; model.c:3131 spin = D156; pthread_mutex_lock on s_imLock = D147/D152). | | **D176** | Surface exterior renders wrong (two co-located defects). **(a)** Sky is solid **black** — the skybox / sky gradient does not draw at all (reference: warm sunset cloud gradient). **(b)** The perimeter **tree-line backdrop renders as grey TV-static / comb interlacing** instead of snow-laden pines — the geometry is present and roughly the right silhouette but every tree texture is scrambled noise. Ground, tunnel and the dish/bunker building draw fine. | **Observed, screenshots committed** (M-35, user QA; `docs/img/bugs/d176-surface-exterior-our-build.png` vs `-reference.png`). Level is the shared Surface exterior — user reached it Dam→Surface on Agent so this is **Surface (i) `-level_36`** (confirm it also affects `-level_43`). **(a)** black sky: **M-36 partial (findings.md "D176(a)").** Env data is correct (`Sky.Clouds=1`, `CloudRGB=240,120,30`, `SkyImageId=0`); `skyRender` runs the cloud path and classifies corners correctly (switch case 12), but `skyRenderTri`/`skyRenderFull` emit nothing — not even the `env->RGB` fill shows. Not root-caused: next is probing `sub_GAME_7F097388` projected coords, verifying `skywaterimages[0]` resolves on PC (`image_bank.c` GIMG offset `0xFB4`), and a fast3d sky-tri trace. `GE_D176=1` probe shipped. **M-37: ROOT-CAUSED** — `skyRenderTri`/`skyRenderFull` build geometry *exclusively* as `G_RDPHALF_*` immediates, which `gfx_pc.cpp` deliberately no-ops (identical no-op in the PD port); fix = decode the RDPHALF tri-raster stream in fast3d (~1 session, genuine new code). **M-42: `-level_22` (Statue) confirmed a clean headless repro** — bare boot, frame ~360, upper half pure black; use it to verify the decoder. **M-47 + read-only consolidation:** Path B draft exists (`scratchpad/sky.patch`, `skyPortRenderPoly` PORT hook — geometry+texture reach GL, two unit defects open) and the full RDPHALF wire format is now decoded (S15.1 half-packing, 8-component perspective-interpolated vertex vectors); next session is mechanical per `docs/dev/D176a-SKY-NOTES.md`. `-level_36`/`-43` intro cams face down (not a sky test without `GE_INPUTSCRIPT`). **M-82: PR #18 rebased onto main + Defect-1 fix applied** — cloud tile is `s_skywaterimages[0]` = 64×64 IA8; the patch's `tc = S·32` scale is correct (matches GE's `width<<5` idiom, `glass2.c:678`), the bug was `(s16)` overflow on the ±20k-texel horizon S/T; fix = per-primitive phase-fold to the 64-texel period (`sky.c` `skyPortRenderPoly`, `#ifdef PORT`). Builds clean, no crash (bunker1/statue/surface1 verify PASS). **Unverified visually** — owed: one `-level_22` screenshot (see `D176a-SKY-NOTES.md` "verification ask"). Defect 2 (horizon-band coverage / straddling tris) still open → adaptive tessellation. **(b)** tree/rock static: **M-36 triage — NOT the D159 family.** `texSwapAltRowBytes` is a full `#ifdef PORT` no-op (`image.c:2199`), it cannot cause this. Real cause: `port/fast3d/gfx_pc.cpp` `import_texture_*` assume `line_size_bytes == full_image_line_size_bytes` (`SUPPORT_CHECK`; rgba16's is commented out *"trips ... with a garbage size"* `:658`) → non-PoT / sub-rect tiles get a flat linear read → diagonal row shear = grey static. **Family A** in `docs/dev/M-36-TRIAGE.md`. Fix: honour `line_size_bytes` as the real source pitch. Same defect as D182(2). | -| **D217** | 1P weapon viewmodel + character-model CI textures render with a wrong palette on many levels / multiple weapons — PP7 grip + hand render flat green on Facility & Depot, "weird or missing" gun textures elsewhere; Bond's watch face too. Surfaced by D215 (viewmodel now draws). Clean on Bunker1. | **Root cause characterised (M-86), needs an upstream/asset fix — full detail in `findings.md` §F D217.** Two defects: ① **visible green = wrong palette bytes arriving from the model-DL / segment path** (the grip's `count=1` `G_LOADTLUT` reads `1a 00`→`0x1A00` green on Facility vs `00 01`→black on Bunker; every larger model TLUT has a 2-entry leading zero pad ⇒ port's model-TLUT source pointer likely 4 bytes low, or a `d43_emit.py` blob-placement offset — fast3d byteswap itself is correct). ② **CI texture-cache collision** (no palette-content key) — proven, **fix landed on branch `fix/d217-model-texture-palette` `f6fc0399`** (palette FNV-1a hash in the CI cache key), owes a 21-level `verify.sh sweep`; does not fix ①. | +| **D217** | 1P weapon viewmodel + character-model CI textures render with a wrong palette on many levels / multiple weapons — PP7 grip + hand render flat green on Facility & Depot, "weird or missing" gun textures elsewhere; Bond's watch face too. Surfaced by D215 (viewmodel now draws). Clean on Bunker1. | **Defect ① FIXED (M-90), pending human playtest eyeball — full detail in `findings.md` §H "D217 — M-90" and §F D217.** Root cause: weapon texpool arena bases land %8==4 on PC (memp bump drift from pointer bloat); `texAlignIndices` absolute 8-byte row padding then shifts every palette 4 bytes early while model-DL `G_LOADTLUT` reads the N64 offset (grip count=1 load lands on next tenant's header slot → `0x1A00` green; larger loads read a 2-entry zero pad — the M-86 tell). Fix: `texInitPool` rounds the base up to 8 under `#ifdef PORT` (no-op on N64); 21/21 sweep PASS. Two defects: ① **visible green = misaligned texpool arena** (fixed as above). ② **CI texture-cache collision** (no palette-content key) — proven, **fix landed on branch `fix/d217-model-texture-palette` `f6fc0399`** (palette FNV-1a hash in the CI cache key), owes a 21-level `verify.sh sweep`; does not fix ①. | | **D174** | "No blood effect" — user reports no visible blood when shooting guards / into surfaces. | **User QA report; likely D120, unverified.** Decals: the D120 `PointUsage[]` converter fix (M-30) was never interactively verified and only takes effect after a full sidecar regen (`d43 && d69 && d88 --regen`) — check whether the playtest build had regenerated sidecars before concluding the fix failed. Spray: D172 observed the particle spray *drawing* (wrong color, magenta/cyan) on Silo — if the user sees no spray either, that's new info (different weapon/range, or the D172 decode is worse than "wrong color"). First step: BUNKER1 firefight with regenerated sidecars; if decals still absent, instrument the `chr.c:3328` decal walk. | | **D173** | Third-person Bond model (the player-representing figure shown at level start) spawns too high — floats well above the ground "a lot of the time" while the actual player spawn is correct. Same symptom on the Cuba end credits: JB stands ~6 ft in the air above Natalya. | **Confirmed still present across the full 21-level playthrough (M-49).** **Observed, not investigated** (user QA report). D75 animated-model family but a *position* defect, not absence — M-33 confirmed the gun-barrel Bond renders fine upright. Suspects: (a) intro/credits puppet chr spawn position vs the model's base transform; (b) anim root-motion accumulation (`modelSetAnimFrame2WithChrStuff`, D156 area) offsetting the rendered joints above the body origin; (c) a `coord3d`/packed-float decode in the intro camera/model setup. "Player spawning is OK" localises it to the render-side model, not `g_CurrentPlayer` state. | | **D182** | SELECT FILE screen degrades after navigating away and back (main menu → file select, or backing out of file select and re-entering). On the fresh screen the 007 gun-barrel spiral / Bond silhouette renders behind the file boxes; after a round-trip **the Bond image stops drawing entirely** and the whole background gains the **comb / interlacing** artifact (horizontal line scramble). File boxes, text and the Copy/Erase icons still draw. | **Observed, screenshot committed** (M-35, user QA; `docs/img/bugs/d182-file-select-backout-20260902.png`). Two symptoms, likely linked: **(1)** background 3D image stops rendering after re-entry → a front-end model/DL whose `render_pos` / segment binding is only valid on first construction (D149 / D75 `constructor_menuXX` family — the dossier/wallet models have the same "renders once" behavior). **(2)** interlacing = **M-36 triage: Family A**, the `import_texture_*` `line` vs `full_image_line` shear (see D176(b) / `docs/dev/M-36-TRIAGE.md`) — **not** the D159 `texSwapAltRowBytes` path (that is a full PORT no-op). Open question: why only *after* a round-trip — first import may land `line == width`, re-import may not, or the tile descriptor is stale on re-entry. Repro: `GE_STARTMENU` to file-select, back out with ESC/B, re-enter; `GE_DTEX` on the spiral texture across both visits. Shares defect (2) with D176(b). | diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 1034871a..630da0fb 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -34,6 +34,7 @@ D223,OPEN,QoL ask: mouse-wheel weapon cycling should be directional like a norma D224,FIXED,"QoL gap (M-87, from a PD-PC-port survey):","DEPRIORITIZED (M-87, user call)." D225,OPEN,"Dev-tooling QoL (M-87, user ask):","OPEN — root-caused, not implemented." D226,OPEN,"QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text.","OPEN — root cause / call sites identified, not designed or implemented." +D228,OPEN,AK47 (and other) weapon models still show WHITE missing-texture patches after D217's texpool-alignment fix —,"OPEN — newly split out, not investigated." 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 7f93b67f..20778202 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -407,6 +407,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D224 | **QoL gap (M-87, from a PD-PC-port survey): the N64 Rumble Pak is fully dropped instead of modernized.** GE genuinely has a rumble subsystem (`src/motor.c`, `src/joy.c`) but the port's `osMotorInit/Start/Stop` (`port/src/libultra.c:1220-1223`) are all hard stubs — "no accessories on the PC," `osMotorStart`/`osMotorStop` return `-1` unconditionally, `osPfsFindFile`-family returns `PFS_ERR_NOPACK`. So every in-game rumble event (weapon recoil, explosions, etc.) computes and calls into a dead path. **PD's PC port wires the equivalent straight through to real gamepad rumble:** `inputRumbleSupported(idx)` checks `SDL_GameControllerHasRumble`/`SDL_JoystickIsHaptic` (with a Windows fallback: some pads report no haptics but rumble anyway), `inputRumble(idx, strength, time)` calls `SDL_GameControllerRumble()` scaled by a per-pad `RumbleScale` config value (options-menu slider, `Input.PadN.RumbleScale`), and `osPfsFindFile` reports a "Rumble Pak" present exactly when a real pad supports rumble (`libultra.c` — same "pretend the N64 accessory is there" pattern GE's port already uses for `osMemSize`/Expansion Pak, just applied to a real capability instead of faked). Cross-ref `QOL-INVENTORY.md` (folded into the existing "per-pad tuning" row). | **DEPRIORITIZED (M-87, user call).** Gamepad-only — irrelevant to mouse/keyboard, which is the primary target for this next release; no keyboard/mouse play is affected by NOT doing this. Root cause + fix still valid whenever gamepad support becomes a priority (no `src/` game-logic touch: GE's `motor.c`/`joy.c` call sites already exist and already call the `osMotor*` shims; the fix is entirely in `port/src/libultra.c` + `input.c` — add `inputRumbleSupported`/`inputRumble` per the PD shape, wire `osMotorStart`/`osMotorStop` to them, add `Input.PadN.RumbleScale` config). **M-87 PD-legacy-survey addendum (`docs/dev/notes/PD-LEGACY-SURVEY.md` candidate 1, high confidence) — the D224 fix as originally stated is NOT sufficient by itself.** Two more upstream blockers sit before the stubbed `osMotorInit/Start/Stop` ever gets reached: (1) `port/src/libultra.c:1096-1098` sets `g_contStatus[i].status = 0` for every pad — `CONT_CARD_ON` is never set, and `src/joy.c:184` gates the *entire* rumble-pak init path on that bit, so `joyRumblePakInit()` never even calls `osPfsInit`. (2) `src/joy.c:186-190` only attempts `osMotorInit` if `osPfsInit`'s return is `PFS_ERR_ID_FATAL` or `PFS_ERR_DEVICE` — but the port's stub (`libultra.c:1214`) returns `PFS_ERR_NOPACK`, so motor init is never attempted even once (1) is fixed. PD's exact trick (`libultra.c:213-227,349-351`): set `CONT_CARD_ON` for connected pads, and make `osPfsInitPak()` return `PFS_ERR_DEVICE` specifically when the pad supports rumble. **Whenever D224 gets implemented, both of these must land alongside the `SDL_GameControllerRumble` wiring** or the game's state machine parks at `RUMBLEPAKINITSTATE_NOT_READY` forever and nothing audibly changes despite looking wired up correctly. | | D225 | **Dev-tooling QoL (M-87, user ask): a debug-build/env toggle that unlocks all missions + all cheats, for testing.** Root-caused a clean single choke point — **both** gates route through the same function: `bool fileGetIsCheatUnlocked(save_data *save, s32 cheat)` (`src/game/file2.c:391`) reads the save's `unlocked_cheats_1/2/3` bitfields and returns whether bit `cheat` is set. Mission-select lock (`src/game/file.c:59`, gates whether a briefing page's mission is selectable) and the cheat-menu availability check (`frontCheckIfCheatIsUnlocked`, `front.c:1009`, itself calling `fileGetIsCheatUnlocked` for the per-level `SP_LEVEL_*` cheat IDs) **both** call it — it's a pure, side-effect-free bitfield read (no save-data mutation), so short-circuiting it to always-true under a debug env var unlocks mission-select *and* the cheat menu in one change, matching exactly what was asked ("those two go a long way"). **Bonus, free once cheats unlock:** the ROM already contains real Rare-era developer cheats that are otherwise unreachable in normal play — `CHEAT_LINEMODE` (wireframe render — would make D222-style culling bugs trivial to see), `CHEAT_BONDPHASE`, `CHEAT_DEBUG_POS`, `CHEAT_DEBUG_UNK5` (`cheat.c` ~L687-707, `CHEAT_MASK_GLOBAL`) — worth trying once this lands, may be useful dev tools in their own right. | **OPEN — root-caused, not implemented.** Plan: `#ifdef PORT`, env-gated (e.g. `GE_DEBUG_UNLOCKALL=1`, catalogue in `GE-ENV-PROBES.md` once added) short-circuit at the top of `fileGetIsCheatUnlocked` — `if (getenv-cached-flag) return TRUE;` before the bitfield read. Default off ⇒ zero behavior change for normal play; when set, purely a query-time override (save file itself is never touched, so it can't corrupt a save or leak into the shipped default). No `src/` logic change beyond the guarded early-return (same class of edit as other `#ifdef PORT` dev-convenience hooks, e.g. `GE_STARTMENU`). | | D226 | **QoL ask: player-adjustable HUD scale for the ammo counter, bottom-left pickup/item status text, and top-of-screen dialogue/subtitle text.** All three route through the same primitive, `textRenderOutlined()` (`src/game/textrelated.c:688`, doc comment: *"Used for ammo counter, bottom left HUD messages, countdown timers"*), called from two specific in-game (not menu) sites in `bondview2.c`: **ammo + bottom-left pickup/status** at `bondview2.c:10005` (`stringbuffer_lowerleft[...]`, anchored at `view_left`/`view_vert`), and **top-of-screen dialogue** at `bondview2.c:10163`/`10165` (`stringbuffer_top[...]`, anchored at `msg.x`/`msg.y`). `textRenderOutlined` → `textDrawGlyphQuad` (`textrelated.c:615`) emits each glyph as a plain RDP `gSPTextureRectangle` at the glyph's native font-metric pixel size, `dsdx`/`dsdy` = `0x400` (1:1 texel:pixel, no scale factor anywhere in the call chain). Same function is also used by menus/options/multiplayer UI (`front.c`, `options.c`, `mpmenu.c`) — **do not scale generically**, those have their own layout at fixed screen fractions and D211 already established HUD anchors must stay put; scale only the 3 in-game HUD call sites named above, about their existing anchor (so bottom-left stays bottom-left, top stays top — same anchor-preserving principle as D211's HUD-static check). | **OPEN — root cause / call sites identified, not designed or implemented.** Two viable directions: (a) a `#ifdef PORT` wrapper at just these 2-3 `bondview2.c` call sites that scales the emitted `gSPTextureRectangle` geometry (`xl/yl/xh/yh`) by a new `f32 portHudScale` (default 1.0 ⇒ byte-identical) while inversely scaling `dsdx/dsdy` to keep the same source glyph texels mapped onto the larger/smaller rect, re-anchored so the opposite edge (bottom for the lower-left block, top for dialogue) doesn't move; or (b) thread a scale parameter into `textRenderOutlined`/`textDrawGlyphQuad` itself, defaulted to 1.0 at every other call site. (a) is more surgical (matches the "don't touch shared menu text" constraint) but duplicates a little geometry math; (b) is more central but touches a much more widely shared function. No PD precedent found (PD's port has no equivalent HUD-scale option). F10 overlay slider candidate once implemented, alongside `Video.FovScale`. | +| D228 | **AK47 (and other) weapon models still show WHITE missing-texture patches after D217's texpool-alignment fix — a separate failure mode from the green wrong-palette defect (M-97, user playtest of PR #47, `-level_34` FACILITY).** Split out of D217 defect ①, exactly as the M-90 write-up anticipated ("do not close D217 defect ① on this merge — reopen/retitle as the AK47 + NPC-world-model white-texture residual"). **Playtest result that motivates the split:** with PR #47's `texInitPool` 8-byte arena alignment in, the *green* symptom is GONE — PP7/PPK grip and hammer render correct, Bond's watch face correct, no green cast on hands or the 3rd-person world model. But **NPC-held AK47s still render with white patches**, i.e. tiles sampling a missing/undecoded texture rather than a wrong-but-present palette. White ≠ wrong colour: a wrong palette binds a real TLUT with wrong contents (D217's mechanism, now fixed), whereas white is the signature of a texture that never got decoded/uploaded at all, or a tile descriptor pointing at an empty/cleared texpool slot. The M-87 matrix described these as distinct from the start ("AK47 viewmodel has white (missing-texture) patches on the magazine + barrel" vs. the green/blue colour corruption) — this playtest confirms they have distinct root causes, since fixing one left the other untouched. | **OPEN — newly split out, not investigated.** Next: `-level_34` with `GE_D217TEX=1`/`GE_TEXDUMP` while an AK47-carrying guard is on screen; check whether the AK47's affected tiles ever reach `import_texture` at all (a decode that never ran → white) versus reaching it with empty/zeroed source bytes (a decode that ran on a cleared arena slot). The texpool two-ended allocator (`image.c` `texLoad`, `leftpos`/`rightpos`) and the per-level weapon-model texture load path are the obvious suspects — an AK47 is not a level-start weapon on Facility, so its textures load later than the PP7's and may be hitting an exhausted or differently-based pool. Compare against a level where the AK47 IS a starting/early weapon. D217 · D161 lineage. | | 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 | @@ -439,7 +440,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D214 | **Keyboard rebinding — `[Input.Bind]` config section (M-83, port-only, PD parity, Phase 4 debt).** `port/src/input.c` gains 12 gameplay actions (Forward/Back/StrafeLeft/StrafeRight/TurnLeft/TurnRight/Fire/Aim/Action/Cancel/LeanLeft/Start), each a comma-separated list of up to 4 SDL scancode names (`Input.Bind.Forward = W,Up` …). `inputRebuildBinds()` (called from `inputInit()`, after `configLoad()`) parses them via `SDL_GetScancodeFromName`; unknown names log a warning and are skipped. The old hardcoded `keyDown(ks, SDL_SCANCODE_*)` block in `inputComputePad` is replaced by `actHeld(ks, IA_*)`. Mouse buttons (fire = LMB, aim = RMB) stay hardwired; the mouse-wheel weapon-cycle A-pulse is untouched. **Defaults reproduce the previous FPS layout exactly — verified: all 20 default key names round-trip to the identical `SDL_Scancode` the old code used (`SDL_GetScancodeFromName` test, 0 mismatches).** Config-file only (no in-overlay text entry — matches PD, whose rebinding is also file-driven); a fresh `ge007.ini` gets the full `[Input.Bind]` block with the defaults visible. | **PARTIAL — M-84: code PASS (alt/ctrl); one doc-only fix.** LANDED, port-only, no `#ifdef PORT`; build clean. One doc bug fixed (no code change): `config.c` groups keys by the substring *before the last `.`*, so the section header is **`[Input.Bind]`**, not `[Bind]` — earlier finding/commit/QOL text was wrong. Working syntax: put `Fire = Left Alt` under `[Input.Bind]`, or write a fully-qualified `Input.Bind.Fire = Left Alt` line anywhere (`config.c:307`). Separately: the pause→**watch menu** does not wire Esc→back (still Enter to open/close/back) — pre-existing, `options.c`, minor. | | D215 | **1P weapon viewmodel never drew in-level — `gunRenderFirstPersonGunModels` builds its `ModelRenderData` from a reinterpret across separately-declared adjacent globals that only works on N64 (M-83, D3x/ABI class; retires VIEWMODEL-RESEARCH.md).** The N64 line `renderdata = *(ModelRenderData *)&D_80035CC0;` (`gunfire.c`) casts the lone `u32 D_80035CC0 = 0;` to a whole `ModelRenderData` and reads on into the next declared global, `u32 D_80035CC4[] = {1, 3, 0, 0, …}` (`gun.c`) — yielding `{ basemtx=0, zbufferenabled=1, flags=3, rest=0 }` **only because the N64 linker lays the two globals out contiguously in declaration order and pointers are 4 B**. On x86-64 it breaks twice: `ModelRenderData`'s three pointer members (`basemtx`/`gdl`/`mtxlist`) widen 4→8 B so `flags` is no longer at struct offset 8, and C guarantees nothing about the two globals' relative placement. Measured (`GE_DVM=1`, BUNKER1): `renderdata.flags == 0`, so **every** weapon-model DL node was gated off at `modelRenderNodeGundl` (`(renderdata->flags & 1)`), and `subdraw()` for the weapon emitted just its `gSPSegment` — 1 gfx command instead of ~150. All three VIEWMODEL-RESEARCH.md ranked suspects were ruled out by the same probe first: `field_87F==1` (gate open), `RootNode` valid, `numRecords 22 < 192` (pool fine), node walk visits 24+ valid `DL`/`SWITCH`/`BSP` nodes. **Fix:** `#ifdef PORT` → set the three fields explicitly (`zbufferenabled=TRUE; flags=3;`), N64 line verbatim under `#else`. ABI/layout only, no logic change. | **FIXED (`src/game/gunfire.c`, `#ifdef PORT`).** Build clean. `GE_DVM=1` post-fix: `flags=0x3`, weapon `subdraw()` now emits **153** gfx commands (was 1). Regression sweep crash-free. `GE_DVM` gate+render probes left in, env-gated (cf. `GE_D193*`). **Owed:** one BUNKER1 eyeball that the gun model is actually visible + correctly posed (headless can't see pixels; the DL is now provably generated). The committed `bunker1` golden will need re-baselining — the gun now renders in-frame. | | D216 | **`Game.SkipIntro` — boot straight to the file-select menu (M-83, port + one `#ifdef PORT` game-code line, PD parity, Phase 4 quick win).** New `s32 portSkipIntro` (`port/src/video.c`, `configRegisterInt("Game.SkipIntro", 0, 1)`), read once in `src/game/lv.c` at `LEVELID_TITLE` stage load — in the `else` of the existing `GE_STARTMENU` `#ifdef PORT` block. When set it does what every intro stage's own `!is_first_time_on_main_menu` branch does (`front.c`): `is_first_time_on_main_menu = FALSE; prev_keypresses = TRUE; maybe_is_in_menu = TRUE; menu_update = MENU_FILE_SELECT;` — i.e. it reuses the game's own post-intro route rather than inventing one, so the legal screen + Nintendo/Rare/GoldenEye logo attract loop is skipped. `GE_STARTMENU` still wins when both are set. Default 0 ⇒ untouched. | **LANDED + VERIFIED this session.** Build clean. Headless: default boot frame 90 = the TWYCROSS classification screen (10% non-black), `SkipIntro=1` frame 90 = the **SELECT FILE menu** (folders/photos/cursor all render, 89% non-black), no crash across repeated boots. Screenshots captured + eyeballed. **Owed:** nothing critical — a human boot to confirm the menu is fully interactive (cursor moves, a save slot opens). | -| D217 | **1P weapon viewmodel + third-person Bond model textures render garbled — wrong/stale palette on model-DL CI textures (M-84, user QA; surfaced by D215 now that the viewmodel draws).** BUNKER1/Facility: the PP7 viewmodel has yellow+blue scrambled texels and a solid **green** patch by the hammer; Bond's in-game **watch face** shows the same green mismatch; user reports the **third-person Bond world model** does this "sometimes" too. Signature matches the **D161 family** — a CI-format tile drawn without the expected TLUT state, so fast3d's `import_texture` indexes a **stale `rdp.palette`**. D161 fixed the explicit `G_TT_NONE` case; this looks like model DLs not emitting `gDPLoadTLUT` through the port path, or the palette segment not resolving for weapon/character geometry. **Not a D215 regression** — D215 only un-gated the DL. | **OPEN — root cause characterised (M-86); needs an upstream/asset-pipeline fix.** Headless repro: `-level_34` FACILITY ~frame 300–400, PP7 **grip = flat green block** + green ring at the barrel joint + green-cast hand; barrel/slide upper correct black. Also green on `-level_30` DEPOT. **Clean on `-level_09` BUNKER1** (A/B control — same PP7, identical DL). **Two separate defects found:** ① **Wrong palette bound (the visible green — upstream of fast3d).** The grip is a CI4 solid-fill of palette index 0. Green `(24,65,0)` == RGBA5551 `0x1A00` == `rdp.palette[0]` at grip-import time. The grip's `count=1` model `G_LOADTLUT` is fed **different source bytes per level** for the same tile: BUNKER `00 01`→`PD_BE16`→`0x0001` (black ✓), FACILITY `1a 00`→`0x1A00` (green ✗). fast3d byteswaps + stores faithfully (proven correct by BUNKER) — the wrong bytes *arrive* from the model-DL / segment-address path. Strong tell: every *larger* model TLUT has `raw[0]==raw[1]==0x0000` with real colour from index 2 ⇒ GE's model-TLUT payload likely carries a **4-byte (2-entry) leading zero pad**, OR the port's model-TLUT source pointer is **4 bytes / 2 entries too low** (then the `count=1` grip load reads the tail of the preceding CI blob — BUNKER lucky, FACILITY not; fits "widespread across levels + weapons"). Ruled out this session: not tail-overflow (index 0 is in-range; matches M-85 `GE_D217Z`), not the fast3d byteswap. **Next:** dump the grip TLUT `G_SETTIMG` target vs the ROM/`pcmodels` sidecar PP7-grip material bytes; confirm the 4-byte offset vs N64; fix in model-DL segment resolution or `tools_pc/d43_emit.py` blob placement (sidecar regen). ② **CI texture-cache collision (proven, real, separate; fix landed).** `gfx_texture_cache_lookup` (`gfx_pc.cpp:522`) keyed CI textures on `{addr, palette_addrs[0/1], palidx, size_bytes}` with **no palette-content check**; GE reissues `gDPLoadTLUT` from repeated scratch-arena addresses with different content, so a later material could take a stale HIT and sample another material's decoded palette. **Fix on branch `fix/d217-model-texture-palette` (commit `f6fc0399`):** FNV-1a hash of the 512-byte `rdp.palette` computed only in `gfx_dp_load_tlut()`, added to the CI cache key (`port/fast3d/gfx_pc.{cpp,h}`, fast3d-internal, no `#ifdef`). Cheap, cannot worsen a decode; **owes a full 21-level `verify.sh sweep`** before merge (touches every palettized world texture's import path — cache-churn risk only). Does **not** fix defect ①. Screenshots: session scratchpad `d217/`, `fac_gun_crop.png`. Cross-ref `docs/dev/GRAPHICS-BACKLOG.md`. **User playtest matrix (M-87, raw notes — not yet triaged):** DAM/PPK: watch still green; AK47 viewmodel has white (missing-texture) patches on the magazine + barrel. FACILITY: PPK garbled **green AND blue**; 3rd-person Bond world model shows greenness on level load-in; AK47 has "weird blue / glitched" textures. JUNGLE: M16 viewmodel is orange in spots. Confirms multi-weapon (PP7/AK47/M16), multi-color (green/blue/white/orange) — consistent with the upstream model-TLUT-source theory (different trailing garbage per weapon/material) rather than one single fixed offset. **Scope clarified (M-87):** not just Bond's 1P viewmodel / 3P world model — **NPC-held weapon models (other characters' guns in third person) show the same glitched-color/white-missing texture symptom.** So the defect is in the shared weapon-model-DL/TLUT path used by *any* character holding that weapon, not something specific to Bond's model or the 1P viewmodel rig — widens the likely fix surface (weapon model loading in general) beyond "Bond's gun." **M-87 clarification:** not uniform across levels — severity and glitch *type* both vary per level (some levels show it lightly, some not at all in a given session, and the specific wrong-color/missing pattern differs), consistent with the per-material trailing-garbage theory rather than one fixed global offset. **M-88 update: defect ① is NOT a `tools_pc/d43_*.py` sidecar bug — the theory in this row is refuted; the model converter never touches texture/palette bytes at all.** Root cause narrowed to the runtime `texWriteLoadToTmemAddr`/`gfx_dp_load_tlut` TLUT-address path (`src/game/tex.c` + `port/fast3d/gfx_pc.cpp`, both out of this session's file scope). See full write-up: §H "D217 — M-88 defect ① root-cause narrowing". **M-89 update: the `gfx_dp_load_tlut` pitch/width=1 hypothesis is REFUTED** (hand-derived: it already computes `pitch=1` for every model-DL TLUT call via the GBI `width-1` packing — a proposed `pitch=1` patch would be a no-op, do not apply it) — **the addressing math is correct by inspection**. New leads, neither yet probed: (a) does `g_Textures[texturenum]`/`_imagesSegmentRomStart` actually resolve to identical ROM bytes for this weapon's grip texture on BUNKER1 vs FACILITY (an image-table/asset-resolution path distinct from the already-ruled-out per-model `TextureID`/`d43_emit.py`/`Globalimagetable`/D68 path), or (b) does the shared `texpool` arena (`image.c` `texLoad`, two-ended `leftpos`/`rightpos` allocator) serve a stale/aliased `tex->data` for what should be a fresh per-level decode. See full write-up: §H "D217 — M-89: the `gfx_dp_load_tlut` width=1/pitch hypothesis is REFUTED". | +| D217 | **1P weapon viewmodel + third-person Bond model textures render garbled — wrong/stale palette on model-DL CI textures (M-84, user QA; surfaced by D215 now that the viewmodel draws).** BUNKER1/Facility: the PP7 viewmodel has yellow+blue scrambled texels and a solid **green** patch by the hammer; Bond's in-game **watch face** shows the same green mismatch; user reports the **third-person Bond world model** does this "sometimes" too. Signature matches the **D161 family** — a CI-format tile drawn without the expected TLUT state, so fast3d's `import_texture` indexes a **stale `rdp.palette`**. D161 fixed the explicit `G_TT_NONE` case; this looks like model DLs not emitting `gDPLoadTLUT` through the port path, or the palette segment not resolving for weapon/character geometry. **Not a D215 regression** — D215 only un-gated the DL. | **OPEN — root cause characterised (M-86); needs an upstream/asset-pipeline fix.** Headless repro: `-level_34` FACILITY ~frame 300–400, PP7 **grip = flat green block** + green ring at the barrel joint + green-cast hand; barrel/slide upper correct black. Also green on `-level_30` DEPOT. **Clean on `-level_09` BUNKER1** (A/B control — same PP7, identical DL). **Two separate defects found:** ① **Wrong palette bound (the visible green — upstream of fast3d).** The grip is a CI4 solid-fill of palette index 0. Green `(24,65,0)` == RGBA5551 `0x1A00` == `rdp.palette[0]` at grip-import time. The grip's `count=1` model `G_LOADTLUT` is fed **different source bytes per level** for the same tile: BUNKER `00 01`→`PD_BE16`→`0x0001` (black ✓), FACILITY `1a 00`→`0x1A00` (green ✗). fast3d byteswaps + stores faithfully (proven correct by BUNKER) — the wrong bytes *arrive* from the model-DL / segment-address path. Strong tell: every *larger* model TLUT has `raw[0]==raw[1]==0x0000` with real colour from index 2 ⇒ GE's model-TLUT payload likely carries a **4-byte (2-entry) leading zero pad**, OR the port's model-TLUT source pointer is **4 bytes / 2 entries too low** (then the `count=1` grip load reads the tail of the preceding CI blob — BUNKER lucky, FACILITY not; fits "widespread across levels + weapons"). Ruled out this session: not tail-overflow (index 0 is in-range; matches M-85 `GE_D217Z`), not the fast3d byteswap. **Next:** dump the grip TLUT `G_SETTIMG` target vs the ROM/`pcmodels` sidecar PP7-grip material bytes; confirm the 4-byte offset vs N64; fix in model-DL segment resolution or `tools_pc/d43_emit.py` blob placement (sidecar regen). ② **CI texture-cache collision (proven, real, separate; fix landed).** `gfx_texture_cache_lookup` (`gfx_pc.cpp:522`) keyed CI textures on `{addr, palette_addrs[0/1], palidx, size_bytes}` with **no palette-content check**; GE reissues `gDPLoadTLUT` from repeated scratch-arena addresses with different content, so a later material could take a stale HIT and sample another material's decoded palette. **Fix on branch `fix/d217-model-texture-palette` (commit `f6fc0399`):** FNV-1a hash of the 512-byte `rdp.palette` computed only in `gfx_dp_load_tlut()`, added to the CI cache key (`port/fast3d/gfx_pc.{cpp,h}`, fast3d-internal, no `#ifdef`). Cheap, cannot worsen a decode; **owes a full 21-level `verify.sh sweep`** before merge (touches every palettized world texture's import path — cache-churn risk only). Does **not** fix defect ①. Screenshots: session scratchpad `d217/`, `fac_gun_crop.png`. Cross-ref `docs/dev/GRAPHICS-BACKLOG.md`. **User playtest matrix (M-87, raw notes — not yet triaged):** DAM/PPK: watch still green; AK47 viewmodel has white (missing-texture) patches on the magazine + barrel. FACILITY: PPK garbled **green AND blue**; 3rd-person Bond world model shows greenness on level load-in; AK47 has "weird blue / glitched" textures. JUNGLE: M16 viewmodel is orange in spots. Confirms multi-weapon (PP7/AK47/M16), multi-color (green/blue/white/orange) — consistent with the upstream model-TLUT-source theory (different trailing garbage per weapon/material) rather than one single fixed offset. **Scope clarified (M-87):** not just Bond's 1P viewmodel / 3P world model — **NPC-held weapon models (other characters' guns in third person) show the same glitched-color/white-missing texture symptom.** So the defect is in the shared weapon-model-DL/TLUT path used by *any* character holding that weapon, not something specific to Bond's model or the 1P viewmodel rig — widens the likely fix surface (weapon model loading in general) beyond "Bond's gun." **M-87 clarification:** not uniform across levels — severity and glitch *type* both vary per level (some levels show it lightly, some not at all in a given session, and the specific wrong-color/missing pattern differs), consistent with the per-material trailing-garbage theory rather than one fixed global offset. **M-88 update: defect ① is NOT a `tools_pc/d43_*.py` sidecar bug — the theory in this row is refuted; the model converter never touches texture/palette bytes at all.** Root cause narrowed to the runtime `texWriteLoadToTmemAddr`/`gfx_dp_load_tlut` TLUT-address path (`src/game/tex.c` + `port/fast3d/gfx_pc.cpp`, both out of this session's file scope). See full write-up: §H "D217 — M-88 defect ① root-cause narrowing". **M-89 update: the `gfx_dp_load_tlut` pitch/width=1 hypothesis is REFUTED** (hand-derived: it already computes `pitch=1` for every model-DL TLUT call via the GBI `width-1` packing — a proposed `pitch=1` patch would be a no-op, do not apply it) — **the addressing math is correct by inspection**. New leads, neither yet probed: (a) does `g_Textures[texturenum]`/`_imagesSegmentRomStart` actually resolve to identical ROM bytes for this weapon's grip texture on BUNKER1 vs FACILITY (an image-table/asset-resolution path distinct from the already-ruled-out per-model `TextureID`/`d43_emit.py`/`Globalimagetable`/D68 path), or (b) does the shared `texpool` arena (`image.c` `texLoad`, two-ended `leftpos`/`rightpos` allocator) serve a stale/aliased `tex->data` for what should be a fresh per-level decode. See full write-up: §H "D217 — M-89: the `gfx_dp_load_tlut` width=1/pitch hypothesis is REFUTED". **M-90: defect ① ROOT CAUSE FOUND + FIXED** — neither lead (a)/(b) was it: the texpool arena base for the weapon pools lands **%8==4 on PC** (memp bump drift from pointer-bloat allocation sizes), and `texAlignIndices` pads CI rows to *absolute* 8-byte boundaries, so every palette in a misaligned pool sits **4 bytes early** while the model-DL `G_LOADTLUT` still reads the N64 offset — the grip's count=1 load lands on the next tenant's header slot (`texnum=26` → LE s16 → `0x1A00` = the green), and larger loads read two zero-pad entries then the real palette (this is M-86's unexplained "leading zero pad" tell). **Fix:** `texInitPool` rounds the arena base up to 8 under `#ifdef PORT` (no-op on N64; ≤7-byte pool shrink) — ABI/layout-class edit, no logic touched. Verified: FACILITY 188/188 decodes aligned + draw reads `pal0=0001` (was `1a00`) + 0 green pixels in frames 250–450 (was ~1300); BUNKER1 + DEPOT still clean. **Pending:** full 21-level sweep result + human playtest eyeball of the M-87 weapon matrix. See §H "D217 — M-90". **M-92: sweep 21/21 PASS + human eyeball done — PP7/PPK/watch CONFIRMED FIXED by this alignment fix. AK47 (viewmodel + world model) and NPC-held world-model weapons (white/missing-texture patches, e.g. magazine/receiver) are a DISTINCT, still-OPEN residual — not fixed by this PR, not yet root-caused. PR #47 should still merge (real, confirmed partial fix); reopen defect ① scoped to the AK47/NPC-world-model white-texture failure mode. See §H "D217 — M-92". **M-97: independently re-confirmed by a second human playtest** (this branch merged up to date with `main`, fresh build, `-level_34` FACILITY): PP7/PPK grip+hammer clean, watch face clean, no green anywhere; **NPC-held AK47 still shows white missing-texture patches.** Two independent playtests, same split — the green defect is fixed, the white defect is not. The white residual is now split out under its own label, **D228** (rather than leaving defect ① ambiguously half-open), so this row can close on the green mechanism it actually describes. | | D218 | **Wide FOV exposes the per-level draw-distance / fog boundary — "blue artifacting" down long peripheral sightlines (M-84, user QA on the D211 FOV slider).** BUNKER→**Dam main tunnel** at `FovScale` ~150: geometry / backdrop past the level's fog-end distance shows at the widened frustum edges (blue = Dam's fog tint). Not a render bug — `fogLoadCurrentEnvironment` (`bgfog.c:305`) does `viSetZRange(Visibility.BlendMultiplier, Visibility.FarFog)`, so **`Visibility.FarFog` is both the far clip plane and the fog-saturation distance**; levels tune it for the stock ~60° FOV. | **Logged (M-84), fix proposed + parked.** Plan: `Video.DrawDistance` (percent, 100–400, default 100 ⇒ no-op) `#ifdef PORT` multiplier on `FarFog` at `bgfog.c:305` (+ the direct `arg0->Visibility.FarFog` reads just below, so fog stays consistent) plus `Video.DrawDistanceAutoFov` (0/1, default 1 — couples draw distance to `FovScale` unless `DrawDistance` is set explicitly). Clamp effective multiplier ≲2.5× (small znear ⇒ far-field z-fighting risk). **M-87: user confirms still occurring** at high FOV, and — reversing the earlier "not implemented by user request" — **now wants `Video.DrawDistance` implemented as a real QoL feature**, plus scope it to cover **LOD/detail-distance in general**, not just the fog/far-clip boundary this row root-caused. Open question for that broader ask: does GE have a *separate* geometry/object LOD-swap or draw-culling distance mechanism (distinct from `Visibility.FarFog`) worth exposing too, or is fog-distance the only meaningful "how far can you see" knob in this engine? Check for a per-object visibility-radius / simple-model-swap system before assuming `FarFog` is the whole story. | | D219 | **PPK explosions render purple on Dam (M-87, user QA, "not 3D but noting").** Effect-color defect, not a texture-decode one — likely the D172 particle-color family (`gfx_lod_tile_offset` / `G_CC_INTERFERENCE` channel-order class) recurring on a different particle type/weapon than the D172 fix covered, or a fresh instance of the same class. | **OPEN — observed, not investigated.** Next: `GE_PCDUMP`/`GE_TEXDUMP` a Dam PPK explosion; check whether it's the same `gfx_lod_tile_offset` tile-sampling defect D172 fixed for blood, on an explosion DL D172's fix didn't touch. **Status: re-checked at runtime M-90 — mechanism covered by the D172 fix on current `main`; close pending a human by-eye pass (folds into PR #47 playtest).** **M-89 triage (local-Qwen, static analysis only, not yet runtime-confirmed):** the explosion DLs (`assets/oddtextures.c` `globalDL_0x078/0x120/0x1c8/0x270…`) use the same `G_CYC_2CYCLE`+`G_CC_INTERFERENCE,G_CC_MODULATEIA2`+`G_TL_TILE`, tile0=IA smoke/tile1=RGBA fire mechanism as the already-fixed D172 blood/spark records — no separate/unfixed code path found in `explosion.c` or the asset DLs. If still purple after D172's fix (which the user's Dam report postdates — needs re-confirming on current `main`), the next suspect is the GLSL combiner implementation of `G_CC_INTERFERENCE`+`G_CC_MODULATEIA2` for cycle 2 specifically (`gfx_generate_cc`, `port/fast3d/gfx_pc.cpp:312-494`), not the tile-offset fix. **Next, concrete:** run Dam (`-level_33`) with `GE_D172=1` while triggering a PPK/grenade explosion and check for "BUG" lines in the log (silence = D172's fix already covers this DL and the row can likely close on a re-test; needs a scripted-fire repro, not yet built). **M-90 runtime re-check DONE (scripted-fire repro built with `GE_INPUTSCRIPT`): the D172 fix demonstrably covers Dam's particle DLs — row can close on a human by-eye pass.** Ran `-level_33` headless with `GE_D172=1`, 64 scripted Z-pulses (`GE_INPUTSCRIPT="200:Z;215:Z;..."`, ~15 controller-reads apart) and a `GE_PCDUMP` window over the same span. Result: **40/40 particle multitex tris log `SAMPLED1=tile1 (tmem=392)`** — the correct RGBA fire tile, i.e. exactly the post-D172-fix behaviour (pre-fix these read `SAMPLED1 = tile0 (tmem=0 fmt=IA)`, the smoke texture); **zero** "BUG"/unknown-GBI/D146-abort lines; **0** purple/magenta pixels (`r>120, b>120, g<90, r-g>50, b-g>50`) across all 28 captured frames. Caveat: the user's M-87 "purple" report may predate the D172 fix on their binary (playtests run on whatever build was current); this re-check proves the sampling path is correct on current `main`, which is as far as a headless pass can take it — a final by-eye look at a live Dam PPK firefight is the honest closer (cheap for the user, folds into the D217 PR #47 playtest). | | D220 | **1P muzzle flash position shifts upward at higher `Video.FovScale` (M-87, user QA, Jungle/M16).** Likely shares D211's noted residual: the viewmodel draws through `c_perspfovy`/unscaled fovy while the world view is FovScale-scaled (`fr.c:737`), so a flash sprite anchored to viewmodel-space vs. screen-space math can drift as the two diverge more at higher scale. | **ANALYZED (M-91) — the suspected unscaled-viewmodel root does NOT hold; this is correct perspective behavior, likely cosmetic/QoL.** The 1P hand + flash are model nodes in ONE gun DL (`gunfire.c` builds `rwmtx[1..4]` for hammer/flash/billboard nodes) drawn through the single per-frame projection from `viSetupCurrentPlayerView` — which IS FovScale-scaled on PC. So the flash and the muzzle it attaches to share projection and matrix chain: their relative position is geometrically preserved. What moves is the whole camera-anchored viewmodel: under perspective, screen offset from center ∝ 1/tan(fovy/2), so widening the FOV migrates every camera-space point (gun body included) toward screen center — a muzzle sitting slightly below/right of center visibly drifts up-left at FovScale 150. Same family as the M-85 accepted "viewmodel scales down, stays bottom-right anchored". `c_perspfovy`/`currentPlayerSetCameraScale` do stay unscaled (the D211 note) but that feeds frustum-cull/screen↔world math (→ D222), not the flash's render path. **Disposition: human call** — if it bothers at high FovScale, the fix is a QoL viewmodel re-anchor/scale-at-FOV (VIEWMODEL-RESEARCH.md family), not a fidelity bug; no game-logic change either way. Optional headless confirm: Jungle M16 fire at FovScale 100 vs 150, measure flash centroid vs muzzle in PPM pairs (expected: both shift toward center together). | @@ -8729,6 +8730,135 @@ no regression from the (inert-by-default) probe. `-level_09`/`-level_34` the parked defect ② fix directly against `-level_34` frame 380 — still green, screenshot evidence in this session's scratchpad. +## D217 — M-90: defect ① ROOT CAUSE FOUND + FIXED — texpool arena base misaligned on PC; the palette sits 4 bytes early inside every weapon-pool allocation while LoadTLUT still reads the N64 offset + +**Verdict: the stale-arena-pointer theory (M-89 cont.) is REFUTED, and the +real root cause is found, fixed, and verified.** Arena bump allocations are +never overwritten in place (`leftpos` only advances), so a drawn address's +content cannot "change" after its decode. What actually happens: the palette +**was never at the address the draw reads**. + +**The mechanism (all hand-derived, then probe-confirmed):** + +1. `texAlignIndices` (`src/game/image.c`) pads each CI index row to an + **absolute** 8-byte boundary: `outptr = (u8 *)(((u32)outptr + 7) & ~7)`. + The palette is then written at `dst + imagebytesout`, and the whole + allocation's size (`texInflateZlib`'s return) depends on where `dst` sits + in memory. +2. Model-DL TLUT loads address the palette at `tex->data + len*2` bytes + (`texWriteLoadToTmemAddr`: `uls = len` from `texGetDepthAndSize`, and the + port's `gfx_dp_load_tlut` computes `base = settimg_addr + pitch*ult + uls` + in 16-bit units — correct RDP semantics, re-verified). `len` is a + **formula** that only matches the real layout when rows start 8-aligned. +3. The invariant that makes (1) and (2) agree: **`leftpos` stays 8-aligned + for life** — pool base 8-aligned, header `+= 8`, every inflate size a + multiple of 8 (checked: zlib aligns at return; non-zlib per-image). On + N64 the memp bump allocations feeding these pools happened to be + 8-multiples, so bases were aligned and absolute ≡ relative row padding. +4. **PC broke the invariant.** `mempAllocBytesInBank` (`src/memp.c`) is a + plain bump allocator with no per-allocation alignment; PC pointer bloat + (D36/D37-class struct growth) means some STAGE-bank allocation sizes are + no longer 8-multiples, so by the time `initBondDATA.c:238` allocates the + weapon buffers, the pool position is off. Probe-confirmed: on FACILITY + both weapon-pool bases land **%8==4** (stage pool aligned), and *every* + decode in the misaligned pool has `dst%8==4`. + +**The smoking gun (probe data, `GE_D217TEX` extended with a +`D217TEX-SIZE` line at `texInflateZlib`'s return):** pre-fix FACILITY, +`texnum=1608` (PP7 grip, 1x1 CI4, `numcolours=1`, `pal0=0001`) decodes to +**size=8**: image = 1 byte + absolute-align padding = **4 bytes**, palette +at `dst+4..5`. The draw's count=1 TLUT reads `dst+8` (the N64 layout: image +8, palette at +8) — which is exactly the **next tenant's 8-byte header slot**: +`texnum=26` stored as an LE s16 = bytes `1a 00` → `PD_BE16` → **`0x1A00` = +the green `(24,65,0)`**. Permanent (arena never rewrites it), per-level and +per-weapon (the "next tenant" texnum differs → any RGBA5551 value: the green, +blue, white, orange of the M-87 playtest matrix). + +**This single mechanism also explains M-86's long-unexplained tell:** every +*larger* model TLUT showed `raw[0]==raw[1]==0x0000` with real colour from +index 2. In a %8==4 pool the whole palette sits **4 bytes (2 entries) early**, +so a load at the N64 offset reads two zero-pad entries then the real palette. +The "leading zero pad / pointer 4 bytes too low" suspicion was literally +true — just on the *decode* side, not the draw side. + +**Fix (landed):** `texInitPool` (`src/game/image.c`, `#ifdef PORT`): round +the arena base up to 8 bytes before publishing `start`/`leftpos`; `end` +stays at the original buffer limit (pool loses ≤ 7 bytes). A no-op on N64 +(bases were already aligned — the game works there, which is itself the +proof the invariant held). Chosen over aligning inside +`mempAllocBytesInBank` (would shift *every* allocation — far larger blast +radius, and D36/D37 note the STAGE bank is nearly full). This is a +semantics-preserving ABI/layout edit in the AGENTS.md narrow-exception class +(32→64-bit transition broke an implicit layout invariant); no game logic +touched. + +**Verification:** +- `./build-pc.sh ntsc-final` clean; no new symbols (local var + expression). +- FACILITY `-level_34`: all 188 decodes now `%8==0`; `texnum=1608` size=16, + palette at +8; draw probe reads `addr= pal0=0001 count=1` (black, + the decoded value) — zero `pal0=1a00` events. PPM frames 250–450: **0** + exact-green `(24,65,0)` pixels (pre-fix: ~1300 in frames 300–410) and 0 + strong-green (`g>90, r<70, b<70`) across all 21 frames. +- BUNKER1 `-level_09` (the clean control): still clean — 183/183 decodes + aligned, 0 CHANGED draw events, 0 green pixels in the capture window. +- DEPOT `-level_30` (had green pre-fix): 229/229 aligned, 0 exact/strong + green in the capture window. +- Full 21-level `verify.sh sweep`: **21/21 PASS**, no CRASH/REGRESSION/ + STALLED (`sweep_d217.log`, repo-root scratch). + +**Status change:** defect ① = **FIXED, pending the sweep + a human +playtest eyeball** (the M-87 matrix weapons: PP7/PPK/AK47/M16 grip+body, +watch face, NPC-held guns). Defect ② (CI texture-cache content-hash key, +branch `fix/d217-model-texture-palette` @ `f6fc0399`) remains a real, +separate, parked fix — still owes its own sweep before merge; M-89 already +proved it does not fix the visible symptom. + +**Probes:** stripped before commit — the fix ships clean. The committed +M-89 decode probe (`GE_D217TEX` in `texInflateZlib`) remains as left by that +session; if the playtest eyeball surfaces a residual, re-add the +`D217TEX-SIZE`/`D217TEX-DRAW` probes exactly as documented above. + +## D217 — M-92: human playtest eyeball of PR #47 (debug build) — PP7/PPK/watch CONFIRMED FIXED; AK47 and NPC/world-model weapons have a DISTINCT, still-open residual + +**Method:** user's own debug build (`build-linux-dbg`, their preferred manual +test flow, not the headless `verify.sh` capture), `fix/d217-texpool-alignment` +checked out. Sampled Dam, Facility, Jungle, Frigate. + +**Confirmed fixed by this PR:** PP7/PPK (grip + body) clean on every level +tested; Bond's watch face clean where it was previously green. Matches the +probe/pixel-count evidence in M-90 — the %8==4 texpool-alignment fix is +real and closes the exact symptom it targeted. + +**NOT fixed — residual, still open:** +- **AK47** — both **1P viewmodel and 3rd-person world model** still show + white (missing-texture) patches. +- **NPC-held weapon world models** — white patches on parts that vary per + gun (magazine, receiver, etc. depending on weapon), **consistent across + guns/levels** (not intermittent). Frigate was called out as especially bad. + +**Reading this against the M-87 raw notes above:** the M-87 matrix already +recorded AK47 white-patch symptoms (viewmodel) pre-fix; M-92 shows the +texpool-base-alignment fix (M-90) did not touch that mechanism at all — it +only fixed the PP7/PPK/watch "wrong solid colour from a misread palette +slot" failure mode. AK47 + NPC-world-model "white/missing" is a different +failure signature (white usually means "texture never bound / fell back to +a default," not "bound the wrong palette entry" — cf. the D161 family this +row already flagged). **Likely a second, distinct D217 mechanism** — not +yet root-caused. Candidates to check first, cheapest to most expensive: +whether the AK47/NPC weapon model DLs route through a *different* texpool +(not the one `texInitPool`'s 8-align fix touches — note `gun.c:930-950` +hand multiple distinct offset/pool pairs per weapon, and `bondview2.c:504`/ +`initmttex.c:15` are separate `texInitPool` call sites entirely from the +per-weapon ones); or a genuinely separate "texture never loaded for this +DL" bug (D161-class G_TT_NONE / missing-TLUT-emission) rather than a +misaligned-read one. + +**Status:** PR #47 fix is real and should still merge — it closes a +confirmed sub-defect (PP7/PPK/watch) with no regressions. **Do not close +D217 defect ① on this merge** — reopen/retitle as the AK47 + NPC-world-model +white-texture residual once root-caused, since the M-87 matrix already +described it as a separate colour-vs-white failure mode from the start. + ## D221 — M-91: SELECT FILE mouse hit-box offset — root cause is a compiler stack-layout dependency, not a coordinate/origin mismatch; fixed by passing explicit `{min,max}` pairs (`#ifdef PORT`) **Symptom (M-87 user QA):** on the SELECT FILE screen (default `MouseCaptureMode=1`, free OS cursor), clicking directly over a visible @@ -8787,3 +8917,60 @@ eyeball owed: normal mouse play through file select (hover highlight, select/copy/delete options) — the headless test proves the hit-test fires where the visible wallet is, but the highlight/selection *feel* is a human check. D192 (mode-0 clamp range) is a separate bug and remains open. + +## D228 — M-97: AK47 white missing-texture patches survive D217's texpool-alignment fix — split out as a distinct defect (white ≠ wrong-colour) + +**Method.** PR #47's branch (`fix/d217-texpool-alignment`) merged up to date +with `main`, rebuilt clean, launched directly into FACILITY (`-level_34`) — +the level the D217 log names as the sharpest repro. Human eyeball across the +M-87 weapon matrix. + +**Result — a clean split:** + +| Symptom (M-87 matrix) | After PR #47 | +|---|---| +| PP7/PPK grip + hammer green | **FIXED** — correct colour | +| Bond watch face green | **FIXED** | +| Green cast on hands / 3rd-person world model | **FIXED** — none seen | +| **NPC-held AK47 white patches (magazine/receiver)** | **STILL PRESENT** | +| General world/HUD textures | fine, no regressions, no crash | + +This is the second independent human playtest to land on exactly this split +(M-92 was the first) — and it is the split M-90's write-up predicted in +advance, which is good evidence it is a real boundary between two mechanisms +rather than an artifact of one incomplete fix. + +**Why these are different mechanisms, not degrees of one.** D217's root +cause (M-90) is *a wrong palette being bound*: the texpool arena base landed +`%8==4`, so every palette in the affected pools sat 4 bytes early and +`G_LOADTLUT` read the neighbouring tenant's header bytes as colour — the +tile decodes fine, it just decodes against the wrong 512-byte TLUT, which +necessarily produces a **wrong colour** (the documented green `0x1A00`). +A **white** patch is a different signature entirely: it means the tile is +sampling a texture that is missing, never uploaded, or cleared — not one +that is present with wrong contents. Aligning the arena base cannot fix a +texture that was never decoded into the arena in the first place, which is +precisely what the playtest shows. + +**Why the label split.** Leaving this inside D217 would mean the row can +never close cleanly: its own stated mechanism *is* fixed and sweep-verified, +and carrying an unrelated open failure mode under the same label is how a +finding log rots (cf. the D170→D193 subsumption that had to be untangled +later). D228 takes the white-texture residual; D217 closes on the green +mechanism it actually describes. + +**Next (not started).** Run `-level_34` with `GE_D217TEX=1` / `GE_TEXDUMP` +while an AK47-carrying guard is on screen and determine which of two cases +holds: + +1. the AK47's affected tiles **never reach `import_texture`** — a decode + that never ran, pointing at the weapon-model texture *load* path rather + than anything palette-related; or +2. they reach it with **empty/zeroed source bytes** — a decode that ran + against a cleared or exhausted texpool slot, pointing back at the + two-ended `leftpos`/`rightpos` allocator in `image.c` `texLoad`. + +Useful control: the AK47 is not a level-start weapon on Facility, so its +textures load later than the PP7's — compare against a level where the AK47 +*is* an early/starting weapon. If it renders clean there, load ordering / +pool exhaustion is implicated directly. D217 · D161 lineage. diff --git a/src/game/image.c b/src/game/image.c index 998c0761..78a4c8ec 100644 --- a/src/game/image.c +++ b/src/game/image.c @@ -2322,10 +2322,26 @@ void texBlur(u8 *pixels, s32 width, s32 height, s32 method, s32 chansize) void texInitPool(struct texpool *arg0, u8 *arg1, s32 arg2) { + u8 *endbuf = arg1 + arg2; +#ifdef PORT + /* D217 (M-90): the whole texture-layout scheme assumes the arena base is + * 8-byte aligned. texAlignIndices pads each index row to an ABSOLUTE + * 8-byte boundary, and the model-DL TLUT loads address the palette at + * tex->data + len (texGetDepthAndSize), where len is a formula that only + * matches the real layout when rows start 8-aligned -- i.e. when leftpos + * stays 8-aligned for life (header += 8, every inflate size is 8-multiple). + * On N64 the memp bump allocations feeding these pools happened to be + * 8-multiples so bases were aligned; PC pointer bloat broke that (e.g. + * weapon buffers land %8==4), shifting the palette inside the allocation + * while LoadTLUT still reads the N64 offset -- solid wrong-colour tiles + * (the D217 green grip). Round the base up to 8; a no-op on N64. The end + * stays at the original buffer limit (the pool loses <= 7 bytes). */ + arg1 = (u8 *)(((uintptr_t)arg1 + 7) & ~(uintptr_t)7); +#endif arg0->start = arg1; - arg0->end = (struct tex *)(arg1 + arg2); + arg0->end = (struct tex *)endbuf; arg0->leftpos = arg1; - arg0->rightpos = (struct tex *)(arg1 + arg2); + arg0->rightpos = (struct tex *)endbuf; }