From dac4ae7c0c55ba2b18f91fb5a48e261b51b4317c Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Sat, 12 Sep 2026 21:20:51 -0400 Subject: [PATCH] D228: fix white NPC-weapon-texture patches -- IA16 palette intensity/alpha byte-order bug GE_TEXDUMP on Facility (-level_34) found ~14% of LOD-textured CI uploads whose palette is IA16 (intensity-alpha, not RGBA16) rendering solid near-white. A GE_D228 correlation probe ruled out the texpool/RC2-clip hypothesis this row started with -- decoded size exactly matched the base tile every time, so the data reaches import_texture fine. The real bug: palette_to_rgba32's G_TT_IA16 branch read intensity = palentry & 0xff (low byte), alpha = palentry >> 8 (high byte) -- backwards relative to the already-correct direct-IA16 decode (import_texture_ia16) a few lines above it in the same file, which reads intensity from the first byte (high, after gfx_dp_load_tlut's PD_BE16 swap) and alpha from the second. A dim, fully-opaque palette entry (e.g. intensity=0x58, alpha=0xff) decoded backwards as bright and mostly-transparent -- the white-patch symptom. Fix: swap the two lines to match the working convention. User-verified live: AK47 and other metallic guns render correctly. Headless -level_34 run crash-free pre-verification; a full sweep is still owed (the user's own game instance was running concurrently this session, so verify.sh/taskkill were avoided). Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog --- docs/dev/findings-index.csv | 2 +- docs/dev/findings.md | 2 +- docs/porting-notes.md | 18 +++++++++++++++++- port/fast3d/gfx_pc.cpp | 12 ++++++++++-- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 27904c90..a6ea767c 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -40,7 +40,7 @@ D230,OPEN,In-level music quality:,"OPEN — static-analysis pass done (M-108), n D233,FIXED,All levels (intermittent):,"FIXED (port/fast3d/gfx_pc.cpp, gfx_sp_tri1) —" D234,FIXED,"Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).",VISUALLY VERIFIED FIXED (M-108) — D244,OPEN,Self-hosted CI runner (corpyvt6agent01) link failure:,"OPEN, not yet fixed. Next:" -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." +D228,FIXED,AK47 (and other) weapon models still show WHITE missing-texture patches after D217's texpool-alignment fix —,"FIXED (port/fast3d/gfx_pc.cpp, palette_to_rgba32) —" 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 8f92e056..f9315d7d 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -413,7 +413,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D233 | **All levels (intermittent): indoor level geometry invisible near doors — you can see through walls/floors into rooms you can't see into, while world objects (terminals, furniture) in that space still draw (M-106, user QA report; screenshots `docs/img/bugs/20260912-cullingbug-bugged.png` vs `-notbugged.png`).** Root-caused M-107 (session-016TaKy6, port/fast3d only): **fast3d's CPU-side "trivial reject" AND-test in `gfx_sp_tri1` (`port/fast3d/gfx_pc.cpp:1455-1460`) has a latent w-sign bug independent of D222/D106.** The per-vertex outcode bits computed in `gfx_sp_vertex` (`x<-w`/`x>w`/`y<-w`/`y>w`/`z>w`, `gfx_pc.cpp:1372-1386`) are only valid half-space tests when `w>0`; for a vertex that has crossed behind the camera plane (`w<0` — common when the camera is close to large room-structure polygons, e.g. walls/ceiling trusses near a doorway) the comparison sense is flipped, so its outcode can come out wrong. If that spurious bit happens to match the other two (genuinely correct) vertices' bits, `v1->clip_rej & v2->clip_rej & v3->clip_rej` fires and the whole triangle is dropped before it ever reaches OpenGL — even though GL's own homogeneous clipper (which *does* handle `w<0` correctly) would have rendered the visible part of it. Confirmed against the two screenshots: same room/camera/ammo-HUD, one frame is missing the room's ceiling trusses/walls (replaced by flat sky) while the console/terminal props in the same room still draw fully — exactly the "big background polys vanish, small prop models don't" split you'd expect, since large near-camera polygons are far more likely to have a vertex cross behind the eye than small self-contained prop meshes. The backface-cull code 6 lines below (`gfx_pc.cpp:1462-1495`) already special-cases this exact `w<0` hazard for its own cross-product sign (`(v1->w<0)^(v2->w<0)^(v3->w<0)`) — the trivial-reject block above it never got the same treatment. **Independent of D222** (D222 is a game-logic frustum-cull bug in `src/game/fr.c`/`bondview2.c` that only manifests at non-default `Video.FovScale`; this is a GPU-pipeline clip-rejection bug in `port/fast3d/` that fires at any FovScale, including the default 100, whenever the camera gets close to a large polygon — matching the report's "all levels, near doors" framing with no FovScale mentioned). Also independent of D106 (D106 fixed the *game-side* portal-visibility bbox computation for a different z==0 edge case in `bg.c`; this is the *renderer-side* per-triangle clip decision, a different stage of the pipeline entirely) — same neighbourhood/symptom vocabulary ("things vanish near a portal-adjacent boundary"), different bug. | **FIXED (port/fast3d/gfx_pc.cpp, `gfx_sp_tri1`) — high confidence on mechanism, UNVERIFIED visually.** Fix: skip the trivial-reject AND-test entirely whenever any of the triangle's 3 vertices has `w<0`, deferring to GL's clipper instead (mirrors the cull code's existing w-sign handling one block below). Change only ever *adds* triangles that were previously dropped — cannot make anything newly invisible, so it's a safe, narrowly-targeted fix with no game-logic touch (fast3d is PC-only code, no N64 build parity to preserve, no `#ifdef PORT` needed). **Build verified green** (`./build-pc.sh` equivalent — MSYS2 mingw64 cmake+ninja, full `ge007.x86_64.exe` link succeeds, only one pre-existing unrelated `-Wformat` warning in the same file). **VISUALLY VERIFIED (M-108):** user played `-level_26` (Frigate — corrected level id, the old `-level_02` note above predates the `bondconstants.h` numbering check) and confirmed the doorway/ship-corridor geometry no longer flickers or vanishes near room boundaries. Closed. | | D234 | **Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).** Investigated M-107 (session-016TaKy6, port/fast3d scope): **NOT root-caused — no fast3d-side mechanism found that plausibly explains "only the first of N repeated instances renders."** Ruled out: fast3d has no matrix/model instance cache that could explain a first-instance-only bug (the D217-family "keyed by address, not content" cache-bug class doesn't recur here — the *only* content-addressed cache in fast3d is the texture cache, already excluded from this investigation per the M-106 triage note); the modelview matrix stack (`gfx_pc.cpp:105`, depth 11, push properly gated `< 11` at `gfx_pc.cpp:1220`) is correctly bounded and would affect every level's nested-DL geometry, not just Dam's towers specifically; the D233 w<0 clip-rejection bug (above) doesn't fit either — towers are viewed from a distance along the dam, not close enough for a camera-behind-vertex condition, and D233's mechanism is per-triangle/per-frame flicker, not a stable "always the same instances missing" pattern. **Leading hypothesis, NOT verified:** GE's road/structure-instancing for repeated architecture typically goes through the room-graph's bytecode "vis program" (`VISOP_*` opcodes interpreted in `bg.c` ~L4450-4620, e.g. `VISOP_PUSH_IF_ROOM_IN_RANGE`, `VISOP_ADD_VISIBLE_ROOM`) or through per-instance room/portal definitions in the level's compiled BG data — both are level *data*, not code reachable via static analysis without the ROM (this worktree has none extracted). Could equally be a per-instance object/prop transform or cull-volume bug in `src/game/propobj.c`/`chrprop.c` (out of this task's `port/fast3d/` scope, and out of the `src/game/*` model/anim exclusion in the brief regardless). **No fix attempted — do not have enough evidence to touch code.** | **VISUALLY VERIFIED FIXED (M-108) — mechanism still unexplained.** User confirmed on a live Dam playthrough (post-D233 fix, correct `-level_26`/Frigate session's build) that all guard towers along the dam now render, not just the first. No code change was made specifically for D234, and the M-107 investigation's own reasoning argued D233's mechanism doesn't fit this symptom (towers viewed from a distance, not a close-camera w<0 case) — so either that reasoning was wrong, D233's fix had a broader effect than analyzed, or the original symptom was intermittent/state-dependent rather than deterministic. Not closed with high confidence in *why* it's fixed, just closed on live confirmation that it *is*. If it resurfaces, re-open with a fresh `GE_PCDUMP` capture rather than assuming this write-up's ruled-out list still holds. | | D244 | **Self-hosted CI runner (`corpyvt6agent01`) link failure: `undefined reference to 'objectiveGetStatus_WEAK'` (2026-09-12, first-ever build attempt on the reinstalled box, run `34707798707`).** `src/game/objective_status.c:162` uses `#pragma weak objectiveGetStatus_WEAK = get_status_of_objective` (a decomp ground-truth alias, referenced 2-arg from `chrai.c:2538` — comment calls it "horrible hack to get ai matching"). The box's `g++`/`ld` reported as `x86_64-w64-mingw32/16.1.0`; a minimal local repro of the identical `#pragma weak` cross-TU function alias **links cleanly** on this session's own machine (`mingw-w64-x86_64-gcc 16.2.0-3`, `binutils 2.47-3`). Not a game-logic bug (the alias is unmodified decomp code, ground truth) and not a CMakeLists/link-flag issue (no `--gc-sections`/LTO in play) — the leading, unverified explanation is the self-hosted box's MSYS2 packages predate a PE/COFF weak-external fix or behavior change versus current MSYS2, consistent with rollout step 2's earlier finding that the box's mingw64 package set was stale/incomplete at initial setup. | **OPEN, not yet fixed.** Next: on the box, `pacman -Syu` (or at minimum `pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-binutils`) to bring the toolchain current, then re-dispatch `selfhosted.yml`. If updating doesn't clear it, the fallback is a narrow `#ifdef PORT` replacement of the alias with an explicit thin wrapper function in `objective_status.c` (ABI-preserving, no logic change) — but try the toolchain update first since the local repro suggests this is a version/environment gap, not a fundamental PE/COFF limitation. | -| 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. | +| 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. | **FIXED (`port/fast3d/gfx_pc.cpp`, `palette_to_rgba32`) — a real byte-order bug, not a texpool/decode-never-ran issue as originally guessed.** `GE_TEXDUMP` on Facility (`-level_34`) found ~14% of LOD-textured uploads (fmt=CI, `palette_fmt=G_TT_IA16`) rendering solid near-white; a `GE_D228` correlation probe ruled out the texpool/RC2-clip hypothesis (the decoded size exactly matched the tile's base image every time — the data reaches `import_texture` fine, sized correctly). The actual bug: `palette_to_rgba32`'s `G_TT_IA16` branch read `intensity = palentry & 0xff` (low byte) and `alpha = palentry >> 8` (high byte) — backwards relative to the already-correct direct-IA16 decode two functions above it in the same file (`import_texture_ia16`: `intensity = addr[2*i]` (first/high byte after `gfx_dp_load_tlut`'s `PD_BE16` swap), `alpha = addr[2*i+1]`). A dim, fully-opaque palette entry (e.g. intensity=0x58, alpha=0xff) decoded backwards as bright and mostly-transparent (intensity=0xff, alpha=0x58) — exactly the white-patch symptom, on any CI-format material whose TLUT happens to be IA16 rather than RGBA16 (weapon metal/grip materials commonly are, since IA is a natural fit for grayscale metal with a separate opacity channel). Fix: swap the two lines to match the working convention. **User-verified live:** AK47 and other metallic guns render correctly (no more white patches). Verified crash-free with a `-level_34` headless run before the live check; a full 21-level sweep is still owed (not run this session — the user was using their own game instance concurrently, `verify.sh`/`taskkill` would have killed it). D217 · D161 lineage — same general "CI/LUT format-assumption" bug class, distinct specific bug. | | 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 | diff --git a/docs/porting-notes.md b/docs/porting-notes.md index df48b540..c1c1eded 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -1144,4 +1144,20 @@ and turns every such overrun into a fatal `*** stack smashing detected ***` hardware supports and GE's own assets exercise is not "PD-only" just because the first investigation happened to hit it on a draw that shouldn't have taken that path. §F **D195**. - §F **D247**. +- **D228 — when two decode paths exist for the same underlying format, + check them against each other for a swapped byte order, not just against + the spec.** `palette_to_rgba32`'s `G_TT_IA16` branch (CI texture whose + palette is intensity-alpha, not RGBA16) read `intensity = palentry & 0xff`, + `alpha = palentry >> 8` — exactly backwards relative to the *already + correct* direct-IA16 decode (`import_texture_ia16`) a few lines above it + in the same file, which reads `intensity = addr[2*i]` (first/high byte), + `alpha = addr[2*i+1]` (second/low byte), matching what `gfx_dp_load_tlut`'s + `PD_BE16` byte-swap actually produces. A dim, fully-opaque texel (e.g. + intensity=0x58, alpha=0xff) decoded backwards as bright and + mostly-transparent — read as "white missing-texture patches" (AK47 and + other metallic weapon materials, whose grayscale-metal-plus-opacity look + is a natural fit for IA16 palettes). Two importers for the same source + format existing side by side in one file is exactly the situation where + one silently drifts from the other; when investigating a decode bug, diff + the suspect path against its sibling path for the same format before + re-deriving the bit layout from scratch. §F **D228**. diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index 3c1ee8cd..15fd60d7 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -842,8 +842,16 @@ static void import_texture_i8(int tile, const LoadedTexture& loaded_texture, boo static inline void palette_to_rgba32(const uint16_t palentry, uint8_t *rgba32_buf) { if (rdp.palette_fmt == G_TT_IA16) { - const uint8_t intensity = (palentry & 0xff); - const uint8_t alpha = palentry >> 8; + /* D228: intensity/alpha were swapped here relative to the (correct) + * direct IA16 decode in import_texture_ia16() a few lines above -- + * that one reads addr[2*i]=intensity, addr[2*i+1]=alpha, i.e. + * intensity is the first (high, after gfx_dp_load_tlut's PD_BE16 + * swap) byte. This path had them backwards: a dim, fully-opaque + * palette entry (e.g. intensity=0x58, alpha=0xff) decoded as a + * bright, mostly-transparent one (intensity=0xff, alpha=0x58) -- + * the "white missing-texture patches" on AK47/NPC weapon models. */ + const uint8_t intensity = palentry >> 8; + const uint8_t alpha = palentry & 0xff; rgba32_buf[0] = intensity; rgba32_buf[1] = intensity; rgba32_buf[2] = intensity;