docs: findings/backlog through D280 (v0.2.1 batch + D236 tree-wall passes 2-9)

Records the v0.2.1 hotfix batch (D259-D264, this commit's code) and the
ongoing D236 Surface-1 tree-wall investigation (D265-D280): still open as of
D280 -- fog math verified N64-faithful, the noise class confirmed as the
weave's painter, the portal-culling over-draw fix (D271) real but
insufficient, no quick win identified. The D266-D280 probe code and the
D266 IA4 decode / portal-culling fixes themselves stay uncommitted pending
the game-wide regression sweep GRAPHICS-BACKLOG.md flags.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
This commit is contained in:
James Dansereau committed 2026-09-15 08:21:56 -04:00
1 parent 32f193a02f
commit 572ab7bf5b
4 files changed
+235 -7

No files matched your search

+14 -2
View File
@@ -38,7 +38,11 @@ Two classes:
| `GE_VOICEDUMP` | `port/src/mixer.c` (`aEnvMixerImpl`) | Dumps each voice's resampled mono stream (post-resample/pre-envelope, i.e. the env-mixer input) to `voicedump.raw` as records of `[u32 stateAddr][u32 nSamples][u64 us][s16 × n]`. **The µs stamp is when the block was MIXED — records are rendered ahead, so a record's samples END at its stamp** (M-69: treating stamps as start offsets misplaces content by up to one ~33 ms block). `stateAddr` = the env-mixer state address from `[VOICEMAP]`. Used with `scratchpad/exact_match.py` for full-corpus per-voice sample verification. | **live** (D202) |
| `GE_MIXERTRACE` | `port/src/mixer.c`, `src/audi.c` (`amDmaCallback`), `src/libultra/audio/load.c` (`_decodeChunk`, `alLoadParam`) | Per-opcode mixer trace to `mixertrace.log` (DMEM addresses, ADPCM books, decoded frames, `[DMAREQ]`/`[DMAHIT]`/`[DMAMISS]`/`[BINDTABLE]`). **WARNING: unbuffered per-opcode `fprintf`, ~25 MB/min. It slows the process enough to starve the audio thread — M-63 measured "audio at 13 % of real time" that was entirely this probe's own artifact. NEVER use it for timing-sensitive measurement; use `GE_D204`.** Grep it, never read it whole. | **live** (D202) — timing-unsafe |
| `GE_TEXPITCH` | `port/fast3d/gfx_pc.cpp:924` (`gfx_tex_pitch_fix`, cached) | `=0` disables the D183 defensive de-stride in `import_texture` (default on). RC2/RC3 texture-shear A/B knob. | **live** (RC3 not finished) |
| `GE_TEXRAW` | `port/fast3d/gfx_pc.cpp:1079` | With `GE_TEXDUMP`, also dumps the raw pre-import source bytes (`texdump/rNNN_f…_s…_WxH.bin`) so a decode bug can be told from a source-data bug offline (D183). | **live** (RC3 not finished) |
| `GE_TEXRAW` | `port/fast3d/gfx_pc.cpp:1079` | With `GE_TEXDUMP`, also dumps the raw pre-import source bytes (`texdump/rNNN_f…_s…_WxH.bin`) so a decode bug can be told from a source-data bug offline (D183). M-128 addition: for CI formats it also writes `texdump/rNNN_….pal` (512 B = 256 RGB555 entries) so CI images can be decoded to RGB offline. | **live** (RC3 not finished; D269) |
| `GE_D266` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, `#ifdef PORT`) | D236/D265 wall, 2nd pass: tri-time census of alpha-bearing draws (tile fmt / line-width / RM class). Found zero IA4 draws from the Surface 1 boot camera over 1.5M tris — killed the D265 class-(b) attribution at that viewpoint. | **live** (D269 open — investigation instrument) |
| `GE_D266W` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, `#ifdef PORT`) | D269 band-gated all-format tracker: every draw class whose projected vertices fall in the top 3–9% of the viewport (the measured stripe band), logging fmt/line-width/oml/omh/zmode/cyc/blender/imgseq + first-vertex screen position. Identified the wall as CI8 32×32, two classes per room stream (OPA oml=0xc4112078 / XLU oml=0xc41049d8). Caveat: imgseq can be stale on texture-cache hits (stamp is bypassed — see D267); only the first vertex's position is logged (projected quad extent still owed). | **live** (D269 open — investigation instrument) |
| `GE_D267` | `port/fast3d/gfx_pc.cpp` (`import_texture_ia8`, `#ifdef PORT`) | D267: PPM dump + alpha histogram for 32px-wide IA8 strips, plus per-tile-slot upload-sequence tracking (`d267_seq` / `d267_slot_img[128]`, stamped in the `import_texture()` dispatch) so tri-time probes can name the uploaded image a draw uses. Dumps to `scratch/d267_ia8_tile0_seq*.ppm`. | **live** (D269 open — investigation instrument) |
| `GE_D268_FORCEA` | `port/fast3d/gfx_pc.cpp` (`import_texture_ia8`, `#ifdef PORT`) | D268 A/B knob: forces every IA8 texel alpha to the given value (0 or 15). Proved the texel-alpha blend pipeline works end-to-end (band vanishes at 0, solidifies at F) — the wall is not a broken blend path. | **live** (D269 open — A/B knob) |
| `GE_D219RAW` | `src/game/image.c` (`texReadUncompressed`/`texChannelsToPixels`, `#ifdef PORT`) | Diagnostic-only, print-only (no behavior change): logs the runtime texture decompressor's bitstream header (texnum/format/width/height/compmethod) for every non-zlib wide-pixel image, plus a dump of the first row of decoded texels exactly as stored in the pool (post-`PORT_PIXEL16`, i.e. byte-swapped for the 16-bit family) for RGBA16/RGB15/IA16. No texnum filter — lets any suspect texture be cross-referenced against the offline ROM census (`scratch/d219_census.txt`) without narrowing by hand. | **live** (D219, closed — kept for future runtime-decoder triage) |
| `GE_WRAPFIX` | `port/fast3d/gfx_pc.cpp:3166` (`gfx_set_wrap_fix`) | RC3 test override for the `Video.WrapFix` D74/D167 wrap-block path (`atoi != 0` wins over the ini). | **live** (RC3 not finished) |
| `GE_SAVELOG` | `src/game/file.c:46`, `src/game/file2.c:16` (`SAVELOG` macro), `src/boss.c:749`, `src/game/objective_status.c:297,343` | Traces the campaign save/unlock chain (objective completion → `end_of_mission_briefing` → `fileWriteSave` → EEPROM). Used to confirm D157. `g_savelogObjOnce` gates the per-criterion dump to the `bossReturnTitleStage` call. | **live** (strip once a few more level boundaries are playtested) |
@@ -80,7 +84,15 @@ Two classes:
| `GE_D88` | `src/game/prop.c:1367`, `src/game/stan.c:3090` | D88 `Usetup*Z` propDef stream | dead (D88 family closed; D88.4 resolved) |
| `GE_D90` | remaining: `src/game/bondview2.c:2146`, `src/game/prop.c:1391` (bondview_r.c blocks stripped M-32, `49ce620a`) | D90 bondview / prop NULL trace | dead (D90 closed) — partially removed |
| `GE_D96` | `src/game/chrprop.c:436,507` (cached `probe`) | D96 chrprop trace | dead (D96 closed) |
| `GE_D104` | `src/game/bg.c:654` (rate-limited, `d104c`) | D104 depth-clear trace | dead (D104 closed) |
| `GE_D104` | `src/game/bg.c:~663` (rate-limited, `d104c`) | Room-render-pass visibility probe: `curRoom`/`roomsDrawn`/`budget`/per-room bboxes. **Revived 2026-09-14 for D271** (added `hasPortals` + the two D271 degenerate-bounds counters to the same print line); original D104 depth-clear investigation itself is closed. | **live** (D271 fix applied, uncommitted — user visual check owed) |
| `GE_D271` | `src/game/bg.c` (`sub_GAME_7F0B5864`, portal-bounds builder + degenerate check) | D271: per-point z/projection capture in the portal-bounds builder; counts "ordinary" (min>max, N64's own degenerate path) vs D106-fallback (now non-finite-only, `lim=1e38`) hits separately; dumps portal/rooms/points for the first 3 fallback hits (`D271 d106 …` / `D271 ptN …`); logs finite out-of-range bounds that now flow through to `bgRectIntersect` (`D271 oor-finite …`, budget 8). Post-fix state: `d106Fallback=0` on Surface 1 + BUNKER1; the oor-finite dumps are how the per-portal N64-clamp check was done (every culled portal's finite portion is far off-screen). | **live** (D271 fix applied, uncommitted — keep until user visual check lands) |
| `GE_D272` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1` + `import_texture_ci8`) | D272: per-card S/T audit for D236's two wall-card classes only (oml low-24 `0x112078`/`0x1049d8`, **now also gated on `fmt==CI/siz==8b`, M-132** — the oml match alone lets an unrelated fmt=3 class into the budget). Dumps per-tri tile fmt/siz/wh/mask/shift/UV-window + all three vertices' UV+screen+z/w (texel/NDC units) for the first 200 matching band tris (`D272[...]`); a one-shot-per-seq sampler-state line (`D272S`: cms/cmt/tex_lod/window); every CI8 upload's genmip/dims (`D272U`) and, for 32×32 uploads, the raw TLUT (`D272P`). All four lines now also carry `addr=%p` (M-132, see the `d267_slot_addr` row below) — the reliable identity field, not `seq`. Proved distinct card instances carry distinct (not identical) UV, the XLU class wraps its tile ~4.5×, mips are generated (`genmip=1`) and enabled (`tex_lod=1`) for the real wall-card tile with `GL_REPEAT` wrap (`cms=cmt=0`). | **live** (D236 open — investigation instrument) |
| *(no env var — always-on when `#ifdef PORT`)* | `port/fast3d/gfx_pc.cpp` (`import_texture`, `d267_slot_addr[128]`) | D274 (M-132): companion to `d267_slot_img`/D267 — stamped **unconditionally**, before the texture-cache-hit early return, so it always holds the real source address of whatever is currently bound to a TMEM slot (D267's `seq` stamp is stale on a cache hit; confirmed with hard evidence: same `seq` → two different `addr` within one run). Read by the `GE_D272`-family logs' `addr=` field. Not itself env-gated; negligible cost (one pointer store). | **live** |
| `GE_D270PIX` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, pixel-attribution probe) | D270 (backfilled)/D275: `"x0,y0[;x1,y1;...]"` (normalized 0..1, top-left origin; **up to 8 points as of D275/M-133, was single-point**) — logs every triangle covering any listed point (`D270P[ptN][i] pix=(x,y) oml=... fmt=... z/w=(...) fog=(...) addr=...`, budget 2048/point). Offline: group by point + `oml`, the min `z/w` (excluding the sky quad's construction-mandated `z/w=0`) is the front-most painter. Used by D276 to identify the weave's real painter class. | **live** (D236 investigation instrument) |
| `GE_D270KEEPOML` / `GE_D270SKIPOML` / `GE_D270SKIP` / `GE_D270KILLALL` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, A/B isolation hooks) | D270 (backfilled): drop-by-`oml`(low-24-bit)/`fmt` A/B rules for isolating a draw class. `GE_D270KEEPOML` **gained a "kept N tris" counter (D275/M-133)** — was silent, so a black capture couldn't be told apart from a genuine render bug; now logs `D270KEEPOML: kept tri #N` for the first 4 + every 5000th. **Caveat: never combine with `GE_D266W` in the same run — documented heartbeat hang.** Class-isolation A/B attempted in D274 (inconclusive, pre-counter) — re-run with the counter once a target class is confirmed (D276). | **live** (D236 investigation instrument) |
| `GE_D270FOG` | `port/fast3d/gfx_pc.cpp` (near frame-counter decl) | D270 (backfilled): logs every `G_SETFOGCOLOR` change with frame number. Used in the archived third pass to prove no bright fog color exists in a ~1300-frame Surface 1 run (all dark blue-gray/black). | **live** |
| `GE_TEXRAW` cap | `port/fast3d/gfx_pc.cpp` (`import_texture`, raw-dump gate) | D270 (backfilled): the `tdc<=600` raw-dump cap (was 400) — raised to reach the wall cards' upload index within a normal boot-camera capture. Fire-particle images (`is_fire_bytes`) are exempt regardless of the cap (D219). | **live** |
| `GE_D273` | `port/fast3d/gfx_pc.cpp` (`palette_to_rgba32`) | D273: A/B switch for CI4/CI8 palette-entry decode — standard N64 ARGB1555 (`a=15,r=14:10,g=9:5,b=4:0`) vs. the inherited PD-shifted layout (`a=bit0,r=15:11,g=10:6,b=5:1`), applied unconditionally regardless of `palette_fmt`. Pre-existing/uncommitted, undocumented until M-131; tested against a user-supplied N64 reference screenshot and **ruled out** for D236 (wall pattern unchanged; corrupts unrelated CI8 textures instead). Do not enable. | **live** (probe only; theory ruled out for D236, kept for reference) |
| `GE_D71LOG` | `port/fast3d/gfx_pc.cpp:645` | D71 fast3d trace | dead (D71 closed) |
| `GE_D75` | `src/game/title.c` `sub_GAME_7F007F30` (capped 8) | D75 Bug 2 gun-barrel model probe: logs `chrModelInstance`/`gunModelInstance` ptr+obj+numMatrices, `render_pos`, `renderData.mtxlist`, `g_GfxMemPos`, `osVirtualToPhysical(render_pos)` | **LIVE** (D75 Bug 2 OPEN — see §F "D75 Bug 2 — RUNTIME PROBE") |
+1 -1
View File
@@ -25,7 +25,7 @@ once the game is playable start-to-finish without crashes.
| ~~**D233**~~ | ~~All levels (intermittent): near doors — especially doors you can't see through to the inside — the indoor level geometry vanishes and you can see straight through it into the room, while world objects (computer terminals, furniture) in that space still draw.~~ | **FIXED + VISUALLY VERIFIED (M-107 fix, M-108 verify).** `port/fast3d/gfx_pc.cpp` `gfx_sp_tri1` was trivial-rejecting visible triangles that had a behind-camera (`w<0`) vertex. Status cell here was stale at "not investigated" — corrected M-119. §F D233. |
| ~~**D234**~~ | ~~Dam: the guard towers along the actual dam structure are invisible except for the first one.~~ | **FIXED + VISUALLY VERIFIED (M-108).** Same underlying mechanism as D233. Status cell here was stale at "not investigated" — corrected M-119. §F D234. |
| **D235** | Wrong textures load after dying/restarting a level — clarified: not Facility-specific, seen on multiple levels; trigger is death/restart (level reload), not a general build regression. | **Observed, not investigated** (M-106, user QA report, clarified same session). This is a state-leak on level re-entry, not a one-level texture-decode bug — prime suspect is the texture pool / CI-cache (D217/D228 neighbourhood) not fully resetting between a level teardown and reload (stale arena base, a palette-cache entry keyed by a reused address, or a texpool index not rewound). Needs: confirm it does NOT repro on a fresh level boot (only after death/restart); which surfaces; a screenshot. Next: instrument `texInitPool`/the CI-cache clear path for what does/doesn't reset on reload vs cold boot. |
| **D236** | Surface 1: the 2D billboard trees that appear at the start of the level and around the map don't draw as discrete tree sprites — they render as a wall/sheet of tree texture instead of the proper 2D tree objects. | **Observed, not investigated** (M-106, user QA report). Billboards = camera-facing quads; "wall of tree texture" reads as either (a) the billboard orientation/quaternion missing so all quads emit coplanar in world space, or (b) a UV/texture-rect setup error mapping the whole sheet onto each quad. Distinct from D176(b)'s tree-*line* backdrop static (that's the painted perimeter wall; this is discrete 2D tree objects). First checks: `GE_PCDUMP` at the Surface 1 opening; inspect one tree billboard's emitted vertices + texture coords in fast3d; check whether the quads are camera-facing on N64 reference footage. |
| **D236** | Surface 1: the 2D billboard trees that appear at the start of the level and around the map don't draw as discrete tree sprites — they render as a wall/sheet of tree texture instead of the proper 2D tree objects. | **OPEN — second pass active (D266–D269); first-pass decode fix was reverted after a negative user re-test (D265)** (M-106, user QA report). The earlier IA16 framing was a misidentification: the cards are flat quads by design (no billboard opcode; byte-matched ROM emission) and bind an opaque IA8 64×16 strip plus an alpha-blended **IA4 128×16** strip (texdump r117) that is a row of ~13 faint conifer silhouettes. The wall came from `import_texture_ia4` decoding the RDP's I2:A2 nibble as I3:A1 — A2=1 canopy pixels went fully opaque, A2=2 dithering vanished (bug inherited verbatim from the PD port). **Second pass (D266–D269) re-framed the bug: the visible wall band is CI8 32×32 gradient/blob cards, not IA at all** — two draw classes per room stream (OPA + XLU), BILERP/TEXTLOD/2-cycle/**FOG-on**; texel-alpha blending proven correct by force-alpha A/B (D268). Current ranked suspects: fog range/application (D218 family) > TEXTLOD/mip sampling > draw density/culling > palette. The I2:A2 decode + AC-aware edge fixes are re-applied in-tree (uncommitted) and still owe a game-wide IA4 regression sweep before commit. |
| **D249** | Streets and Egyptian (the two biggest/most-open levels) show culling problems on far-away geometry at *default* FOV — distant geometry drops/pops incorrectly. Also a QoL ask: improve LOD in general, but gate it behind a toggle so N64-accurate ("old school") LOD stays selectable. | **Observed, not investigated** (M-119, user QA report). Distinct from D222 (high-FOV near-edge culling) and D218 (fog/far-clip tint). Two candidate mechanisms: (a) portal/room-visibility cutting off too aggressively at range on large multi-room layouts (D233/D234 neighbourhood — check for a far-plane/depth-range sibling to D233's w<0 trivial-reject fix), or (b) GE's own LOD/detail-swap system not ported faithfully (mip-tile / simple-model substitution at distance, `gfx_lod_tile_offset` family). First checks: `GE_PCDUMP` a long sightline on Streets/Egyptian where geometry visibly pops; determine which of (a)/(b) is firing before designing the fix or the requested toggle. §F D249. |
| **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. |
+19 -2
View File
@@ -90,6 +90,17 @@ D113-D116,FIXED,portal BFS ok · matrix chain ok · player raw-offset audit + gu
D117,OPEN,frame nondeterminism root-caused; GE_DETERM deferred; framediff.py structural.,"OPEN (mode attempted, not achieved)"
D118,RESOLVED,SDL input layer (port/src/input.c); M-24 mouse-look rework:,resolved (D118a residual)
D150,FIXED,watch OBJECTIVES / BRIEF page crash:,FIXED (src/str.c #ifdef PORT); interactive re-verify pending
D260-D265,PARTIAL,"v0.2.1 hotfix batch: D260 wheel direction swap (fixed) · D261 watch-menu nav auto-repeat (fixed, feel-check owed) · D262 watch item-preview model missing → roo…",partial — see rows
D266-D269,OPEN,"D236/D265 wall, second pass:","OPEN — probes live in tree (GE_D266/GE_D266W/GE_D267/GE_D268_FORCEA), fixes uncommitted"
D270,?,Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it.,dead-ended into D271/D272; infrastructure kept in tree
D271,FIXED,"D236 wall, third pass —","FIXED-but-insufficient (src/game/bg.c #ifdef PORT, uncommitted) —"
D276,OPEN,"D236 wall, eighth pass —","REFRAMED — oml=0xc81049d8/fmt=3 is the primary suspect now, not D269's CI8 pair; 0x0c184b50 unattributed; wall still open, see D236"
D279,?,"Backfilled — this label was consumed by an undocumented partial session (2026-09-14 ~23:00) that left artifacts in scratch/ (d279_run.log, d279_imgmap_run.log,…",undocumented — artifacts only
D280,?,"D236 wall, ninth pass —","REFRAMED again — no fix; next steps in detail section, see D236"
D275,?,"D236 wall, eighth pass —","done — probes live, uncommitted; see D276 for what they found"
D274,OPEN,"D236 wall, seventh pass —","d267_slot_addr fix live (always-on); content/decode question CLOSED; mip-presence CLOSED; wall still open, see D236/D276"
D273,OPEN,"D236 wall, fifth pass —","RULED OUT (port/fast3d/gfx_pc.cpp palette_to_rgba32, pre-existing/uncommitted GE_D273) —"
D272,OPEN,"D236 wall, fourth pass —","OPEN — probe GE_D272 added to port/fast3d/gfx_pc.cpp (env-gated, uncommitted); wall still open, see D236"
D137,FIXED,right-mouse (aim-sight) crash:,FIXED (src/game/gunfire.c #ifdef PORT —
D119 · D120,FIXED,guard-attack weapons_held[]->chr pun crash · blood-stain PointUsage[] chain hang,D119 fixed · D120 FIXED (M-30)
D120,FIXED,Blood-decal PointUsage[] chain —,FIXED (tools_pc/d43_emit.py).
@@ -118,7 +129,7 @@ D164,PROPOSED,"Disclaimer / legal screen only draws the first line (D76 root cau
D165,FIXED,"Front-end menu cursor doesn't feel like a mouse pointer (M-31, user bug B5 follow-up).","FIXED (port/src/input.c; Input.MenuPointerMode, Input.HipfirePitchSpeed added)."
D166,FIXED,Hipfire mouse pitch is digital while yaw is analog —,FIXED (port/src/input.c).
D235,OPEN,Wrong textures load after dying/restarting a level —,OPEN — needs: does it repro on a *fresh* level boot (rules out reload-state) vs only after death/restart (confirms it); which surfaces; a screenshot.
D236,OPEN,"Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).",OPEN — not investigated
D236,OPEN,"Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).",OPEN — ninth pass closed with no fix; see D276/D280
D237,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only
D238,OPEN,"QoL ask (M-106, user QA):",OPEN — not designed; port-only (port/src/input.c + config.c)
D239,?,"QoL ask (M-106, user QA):",CLOSED — executed in the v0.2.0 prep pass (port-only):
@@ -139,5 +150,11 @@ D254,FIXED,Linux crash handler destroyed its own logs:,FIXED — field-verified.
D255,OPEN,"Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle, D253 fix confirmed working —","OPEN — case identified (NULL model on an active OBJ record; destroy-path ordering), reproducible on Deck in combat (Bunker + Frigate); fix pending rule-#2 sign-off."
D256,FIXED,Saves (and config) silently failed to persist on Linux whenever the game was not launched from the directory containing data/ —,FIXED (path half) — save-flow detail still owed from the user.
D257,FIXED,New Game.AllUnlocked option (F10; default flipped OFF for v0.2.0 —,FIXED — verified by user playtest (PR #79 merged); default flipped OFF for v0.2.0 (D259).
D259,OPEN,Game.AllUnlocked (D257) breaks audio on a fresh save:,OPEN — quirk documented; default flipped OFF for v0.2.0 (port/src/video.c); fix deferred.
D259,FIXED,Game.AllUnlocked (D257) breaks audio on a fresh save:,FIXED in v0.2.1 — the deferred fix was applied:
D260,FIXED,v0.2.1 (user request):,"FIXED — build green; one-line branch swap, trivially reviewable."
D261,FIXED,v0.2.1 (user request:,FIXED — feel-check received; W/S sensitivity follow-up logged above.
D262,FIXED,v0.2.1 (user report):,"FIXED (primary cause, D264) —"
D264,FIXED,"D262's root cause (ABI/layout class, cf.","FIXED — build green, probes removed; visual confirmation pending."
D263,RESOLVED,v0.2.1 (user report):,RESOLVED (not a bug) —
D265,?,"D236 investigation (port-layer, port/fast3d/gfx_pc.cpp):",PARKED — reverted to baseline; evidence + suspect list above.
D258,OPEN,PAL/JP asset conversion is broken at the source-data level —,"OPEN — repair path verified, not applied (out of v0.2.0 scope)."
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
90 D117
91 D118
92 D150
93 D260-D265
94 D266-D269
95 D270
96 D271
97 D276
98 D279
99 D280
100 D275
101 D274
102 D273
103 D272
104 D137
105 D119 · D120
106 D120
129 D165
130 D166
131 D235
132 D236
133 D237
134 D238
135 D239
150 D255
151 D256
152 D257
153 D259
154 D260
155 D261
156 D262
157 D264
158 D263
159 D265
160 D258
+201 -2
View File
@@ -478,6 +478,17 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D117 | frame nondeterminism root-caused; `GE_DETERM` deferred; `framediff.py` structural. **M-52: `GE_DETERM=1` attempted — boot deadlock fixed, determinism NOT achieved (load-phase fallback still real-time-dependent). See addendum below.** | OPEN (mode attempted, not achieved) |
| D118 | SDL input layer (`port/src/input.c`); M-24 mouse-look rework: mode-aware map (aim mode = analog stick, hipfire = digital C-pitch), `config.c` INI now real. D118b/c FIXED; D118a residual (hipfire pitch only) | resolved (D118a residual) |
| D150 | watch OBJECTIVES / BRIEF page crash: `strcat(buf, langGet(id))` with `langGet` → NULL (unloaded bank) → NULL deref; `str.c` str* builtins elide a plain param NULL check, so guard via asm-laundered `GE_IS_NULL` | FIXED (`src/str.c` `#ifdef PORT`); interactive re-verify pending |
| D260–D265 | v0.2.1 hotfix batch: D260 wheel direction swap (fixed) · D261 watch-menu nav auto-repeat (fixed, feel-check owed) · D262 watch item-preview model missing → root-caused to D264 (fixed at Gfx level, visual check pending) · D263 Facility Ourumov/Trevelyan beat — **resolved: not a bug**, proximity/LOS-triggered as on N64; full chain incl. execution shot verified firing on PC (teleport probe + user playtest) · D264 watch preview renderdata.flags zeroed by cross-global template read on PC (ABI/layout class; fixed) · D236/D265 Surface 1 tree "wall": IA16 importer dimensions transposed (fixed); decode attempt reverted after raw-byte proof that byte-pair is the authored image; root cause = `import_texture_ia4` decoding RDP I2:A2 as I3:A1, inverting the tree strip's alpha ramp → opaque repeating silhouettes; IA4 I3:A1→I2:A2 decode fix applied and user re-tested — **still a wall; reverted, parked** with evidence + suspect list in D265 | partial — see rows |
| D266–D269 | D236/D265 wall, second pass: **the wall is CI8 32×32, not IA** (D269) — band-gated probe finds two draw classes per room stream (OPA oml=0xc4112078 + XLU oml=0xc41049d8), both BILERP/TEXTLOD/2-cycle/**FOG-on**; cards are soft gradient blobs, stripe period ≈ one card per visible room. D266: re-applied the reverted I2:A2 IA4 decode + replaced the hardcoded 0.19 edge quantize with AC-aware handling (uncommitted; census shows zero IA4 draws at boot). D267: IA8 dump infra — the three IA8 images are noise/mast sprites, not the wall. D268: force-alpha A/B proves the blend pipeline works. Ranked suspects now: fog range/application (D218 family) > TEXTLOD/mip sampling > draw density/culling > palette | OPEN — probes live in tree (`GE_D266`/`GE_D266W`/`GE_D267`/`GE_D268_FORCEA`), fixes uncommitted |
| D270 | **Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it.** D236 wall, between-pass instrumentation toolkit for the fog hypothesis (D269 ranked suspect #1) and general A/B isolation: `gfx_sp_tri1` per-vertex fog min/max attribution; `GE_D270SKIP`/`GE_D270SKIPOML`/`GE_D270KEEPOML`/`GE_D270KILLALL` (drop-by-oml/fmt A/B rules); `GE_D270PIX` (single-pixel draw-order attribution); `GE_D270FOG` (per-frame fog-color-change log); and the `GE_TEXRAW` dump cap raise 400→600 to reach the wall cards' upload index. No independent conclusion of its own — superseded by D271's culling fix and D272's later per-card audit, both of which reused this toolkit's skip/keep hooks. | dead-ended into D271/D272; infrastructure kept in tree |
| D271 | **D236 wall, third pass — suspect #3 (draw density/culling) CONFIRMED as a real over-draw, and the D106 guard was the cause.** At Surface 1's boot camera, 11 portals around room 13 straddle the near plane every frame; their z==0 clip points project to ±1e20–1e26-scale FINITE screen coords. The original D106 guard (`lim=1e5`) mapped any such bound to a full-screen box — but on N64 there is no guard: those finite values flow into `bgRectIntersect` (pure comparisons), which either clamps to the parent box (portal covers view → keep) or yields an empty box (portal projects off-screen → **cull**). 9 of the 11 portals clamp empty on N64; PC was force-drawing those rooms. Fix: guard `lim` 1e5→1e38 (catches only NaN/inf, which this projection can't reach — max ~1e26); finite min>max still hits the original `degenerate` check (N64's own path, D106's real regression case unchanged). Verified: `d106Fallback` 396–781/600 ticks → **0** (Surface 1 + BUNKER1); roomsDrawn 17–18→10–13 at the boot view; per-portal oor-finite dumps confirm every culled portal's finite portion is far off-screen (N64 clamps identically); BUNKER1 room drops traced to the same N64-matching empty clamp. No crashes in long unattended runs. **USER RE-TEST (2026-09-14): NEGATIVE — trees still render as a big wall; user's read: "rather than the texture being mapped correctly"** → suspect #3 was a real over-draw but NOT the wall's root cause; fix kept for N64 fidelity. D236 thread continues — top suspect now per-card S/T / UV windowing (D265 note: all captured cards sampled the same u/v window) | FIXED-but-insufficient (`src/game/bg.c` `#ifdef PORT`, uncommitted) — probes live (`GE_D271`, extended `GE_D104`); wall still open, see D236 |
| D276 | **D236 wall, eighth pass — the true painter of the visible weave, at confirmed weave pixels, is a fully-opaque IA8 blocky-noise texture sampled with a fixed, identical 2×-wrapped UV window and low fog — NOT either of D269's named CI8 classes.** Multi-point pixel attribution + per-vertex fog (D275's `GE_D270PIX` list extension) at 4 pixels picked directly off the reproduced weave screenshot found the D269 OPA/XLU classes absent at 2 of them; the dominant front-most class there (`oml` low bits coincide with the XLU class but `fmt=3`/IA, not CI8) decodes to blocky gray static, matches D267's already-dumped-but-dismissed "seq 113" texture, and reproduces D265's original "all cards sample u:0..64/v:0..16" observation to the exact texel — that lead was reframed away from prematurely in D269's second pass. A third class (`0x0c184b50`, CI8) is present everywhere with erratic z/w (`-0.04..0.99`) and high fog when it does land normally — flagged as the possible real "tree" layer, with its own likely occlusion/billboard bug, meaning D236 may have two separate root causes, not one. | REFRAMED — `oml=0xc81049d8`/`fmt=3` is the primary suspect now, not D269's CI8 pair; `0x0c184b50` unattributed; wall still open, see D236 |
| D279 | **Backfilled — this label was consumed by an undocumented partial session (2026-09-14 ~23:00) that left artifacts in `scratch/` (`d279_run.log`, `d279_imgmap_run.log`, `d279_g110.png`, `d279_g220*.png`, `d279_gray_1500.png`) but no written conclusion; the logs carry no D279-tagged lines, so its exact probe/finding cannot be reconstructed from the tree.** Next available label after it: D280. | undocumented — artifacts only |
| D280 | **D236 wall, ninth pass — fog math VERIFIED N64-faithful (±2 truncation over all 2505 verts of `0xc81049d8`; D276 owed step #1 closed), the IA8 noise class VISUALLY confirmed as the weave's painter (`GE_D278C` collapse: band HF 14.8→2.9, no trees appear without it — first unconfounded test), full census at both confirmed weave points (only 3 real classes; two log entries are probe artifacts), D276's depth-race read INVERTED (`0x0c184b50` tree cards are in FRONT of the noise quad, z/w smaller=closer), `0xc8102078` ruled out as trees (terrain, 1.4M tris), and `0x0c184b50` shown to barely instantiate under deterministic `-level_36` boot (~4 tris vs 720 free-run) with CPU-baked bimodal fog 25/255. **No quick win for trees** — remaining work is CPU-side instantiation/room-streaming, not a port fix. | REFRAMED again — no fix; next steps in detail section, see D236 |
| D275 | **D236 wall, eighth pass — probe upgrades:** `GE_D270PIX` extended from a single point to a semicolon-separated list (each line now tagged `pix=(x,y)` + D274's reliable `addr=`); `GE_D270KEEPOML` gained a "kept N tris" counter so a black A/B capture is diagnosable (tick-misaligned vs. a genuine render-interaction bug). Mechanical groundwork for D276; the class-isolation A/B itself is still owed. | done — probes live, uncommitted; see D276 for what they found |
| D274 | **D236 wall, seventh pass — probe identity (`seq`) proven unreliable both mid-run (cache-hit staleness) and cross-run (shared runtime-arena address reuse); fixed with an unconditional per-slot address stamp.** Resolved the prior pass's open "content vs. decode" question as a bug in this session's own offline analysis (wrong palette byte order — native/little-endian, not big-endian), not a port bug: the port's existing CI palette decode, applied correctly, reproduces a plausible soft gradient on the real, address-confirmed wall-card texture. Confirmed mips are generated (`genmip=1`) and enabled (`tex_lod=1`) for that texture, with correct `GL_REPEAT` wrap — ruling out "mips absent" as the mechanism. Attempted class-isolation A/B was inconclusive (black frame). | `d267_slot_addr` fix live (always-on); content/decode question CLOSED; mip-presence CLOSED; wall still open, see D236/D276 |
| D273 | **D236 wall, fifth pass — user-supplied N64 reference screenshot obtained (discrete tree silhouettes, not a sheet); a pre-existing, undocumented `GE_D273` palette-decode A/B probe found in the uncommitted tree was tested against it and RULED OUT.** Standard-ARGB1555 vs. inherited-shifted CI palette decode: wall pattern visually unchanged either way; unrelated CI8 textures (snow path, viewmodel skin) corrupted by the swap. Not the wall's cause; `GE_D273` stays default-off. Flags a process gap: a live investigative lead sat in the working tree undocumented across a handoff boundary. | RULED OUT (`port/fast3d/gfx_pc.cpp` `palette_to_rgba32`, pre-existing/uncommitted `GE_D273`) — wall still open, see D236/D272 |
| D272 | **D236 wall, fourth pass — per-card S/T audit (HANDOFF next-step #1) RULES OUT "identical UV window" and "converter bug"; the wall cards genuinely sample a wide, per-instance-varying UV range, verified authored in ROM, not injected by `d43_emit.py`.** New `GE_D272` raw per-vertex UV+tile probe (`gfx_sp_tri1`), filtered to D269's exact two oml classes. Both are confirmed CI8 (fmt=2 siz=1, matches D269). Findings: (1) per-instance UV is **not identical** across cards sharing the same bound texture (`seq`) — small (~0.3 texel) jitter for the OPA class, larger (~1.6 texel) jitter for the XLU class — consistent with genuinely distinct authored per-vertex S/T, not a converter collapse; D265's "identical u:0..64/v:0..16" observation traces to a different (non-wall) draw class caught by the old unfiltered band gate, not these cards. (2) The XLU class's U range spans **~143–145 texels — ~4.5× the tile's 32-texel wrap period** (`mask=5,5`); the OPA class stays within roughly one tile width. (3) Read `tools_pc/d43_emit.py`'s vertex emit path (`emit_main_vtx`): `tc[0]/tc[1]` (offsets 8/10 of the 16-byte `Vtx`) go through a plain `be16` byteswap with no scale/offset — **no converter bug possible here**; the ROM must already encode this ~4.5× wrap. Reframes the leading hypothesis: this heavy horizontal wrap is by design (Rare tiling one texture across a long tree-line quad), so the "wall" comes from *how* the port renders that wrap, not from wrong UV data — TEXTLOD/mip generation and GL wrap-boundary sampling for a highly-minified, wide-wrapped CI8 tile is now the top suspect again (D269 hypothesis #2), ahead of fog/palette. | OPEN — probe `GE_D272` added to `port/fast3d/gfx_pc.cpp` (env-gated, uncommitted); wall still open, see D236 |
| D137 | right-mouse (aim-sight) crash: `gunDrawSight` (`gunfire.c:6235`) `s32 sp54` holds a `Gfx*` the whole function (`sp54 = *gdl`; `texSelect(&sp54,…)`/`display_image_at_position(&sp54,…)` take `Gfx**`). As N64 `s32` it is 4 B, so `*(Gfx**)&sp54` reads 4 B of the adjacent stack float as the pointer high word → `_g = 0x03e4fca0_70081220` → wild DL write in `texSetRenderMode` (`othermodemicrocode.c:177`) the moment the crosshair raises. §A. | FIXED (`src/game/gunfire.c` `#ifdef PORT` — `sp54` is `Gfx *`) |
| D119 · D120 | guard-attack `weapons_held[]->chr` pun crash · blood-stain `PointUsage[]` chain hang | D119 fixed · **D120 FIXED (M-30)** |
| D120 | **Blood-decal `PointUsage[]` chain — converter now emits it (M-30).** `d43_emit.py` reserved the opcode-0x18 `PointUsage` region in the layout pass (`add_region(puo, 2*nv)`) but the **emit pass never wrote it** — every entry stayed zero. `chr.c`'s decal walk `index = PointUsage[index]` then cycled 0→PointUsage[0]→0 forever (capped by the `#ifdef PORT` guard at `chr.c:3328`, so decals were missing/wrong rather than a true hang). Fix: `op24_pointusage[puo] = nv` in layout + a byteswap loop (`put_s16(remap(puo)+2k, be16(src, puo+2k))` for `nv` = `numVertices` entries — a negative-terminated `s16` index chain, no remap) in the emit pass. Full regen chain re-run, round-trip validation passes. The `chr.c` guard stays as a safety net (SMALL-FIXES B3). | FIXED (`tools_pc/d43_emit.py`). Interactive verify pending — blood decals on a BUNKER1 firefight should now appear + no hang. |
@@ -514,7 +525,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 the room still draw (M-106, user QA report; screenshots `docs/img/bugs/20260912-cullingbug-bugged.png` vs `-notbugged.png`).** The geometry-vs-object split localises it to room/portal background visibility or frustum culling in fast3d, not textures/models. Logged in `GRAPHICS-BACKLOG.md`. **Duplicate row — see the full root-cause writeup above (`gfx_sp_tri1` w<0 trivial-reject bug, `port/fast3d/gfx_pc.cpp:1455-1460`).** | **FIXED + VISUALLY VERIFIED (M-107 fix, M-108 verify).** See the other D233 row for full detail. |
| D234 | **Dam: guard towers along the dam structure invisible except the first one (M-106, user QA report).** Repeated instances → per-instance transform decode, shared model/texture resource bound only by the first instance, or cull-volume issue. Same neighbourhood as D233. Logged in `GRAPHICS-BACKLOG.md`. **Duplicate row — see the full entry above for the M-107 investigation + M-108 verification.** | **VISUALLY VERIFIED FIXED (M-108), mechanism unexplained.** See the other D234 row for detail. |
| D235 | **Wrong textures load after dying/restarting a level — not Facility-specific, seen on multiple levels (M-106, user QA report; clarified same session).** Original report ("Facility: wrong textures now") read as a build regression; user clarified the actual trigger is **death/restart** (or a similar level-reload path), and it has recurred on other levels too — this is a state-leak on level re-entry, not a one-level texture-decode bug and likely not caused by #47/#49 at all. Prime suspect: the texture pool / CI-cache (D217/D228 neighbourhood) not being fully reset between a level teardown and its reload — a stale arena base, a leftover palette-cache entry keyed by an address that gets reused with different content, or a texpool index not rewound — surfacing as "wrong texture" only after the reload path runs, never on a fresh level boot. Logged in `GRAPHICS-BACKLOG.md`. | OPEN — needs: does it repro on a *fresh* level boot (rules out reload-state) vs only after death/restart (confirms it); which surfaces; a screenshot. **M-108 static-analysis pass: the leading "stale CI-cache not reset on restart" hypothesis is likely REFUTED, don't re-propose it as-is.** The theory was that `g_TexCacheItems`/`g_TexCacheCount` (`src/game/image.c:38-40`, cleared only in `set_mt_tex_alloc()`, `src/game/initmttex.c:8`) survives a death/restart because the restart path skips `set_mt_tex_alloc`. But tracing the actual call graph: `set_mt_tex_alloc()` runs inside `lvlStageLoad()` (`src/game/lv.c:369`), which has exactly **one** call site (`src/boss.c:485`) inside `bossMainloop`'s outer `while (!done)` loop; that loop re-runs on *every* stage transition, driven by `g_StageNum = g_MainStageNum` (`boss.c:693`) which is set by `bossSetLoadedStage()` (`boss.c:722`) — the same function the front-end's level-select/continue path calls (`front.c:6947`). Nothing in this call graph is level-select-specific vs. restart-specific; both look like they go through the identical full reload (including the `set_mt_tex_alloc` cache reset). Have not yet found the actual death/restart trigger's call site to be 100% certain it doesn't have its own bypass, so this isn't fully closed out — but the single-reload-path structure makes the "cache survives restart" mechanism unlikely as stated. Next: find the actual death-handler call site (search for where a life/continue/retry decision is made, likely `chr.c`/a game-state machine, not yet located) and confirm whether it calls `bossSetLoadedStage`/goes through `bossMainloop`'s reload path same as normal transitions, or has a distinct lighter-weight path — if it's the same path, the real D235 mechanism is still unknown and this CI-cache theory should be dropped. |
| D236 | **Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).** Suspects: missing billboard orientation (quads coplanar in world space) or UV/texture-rect setup mapping the whole sheet per quad. Distinct from D176(b)'s painted tree-line backdrop. Logged in `GRAPHICS-BACKLOG.md`. | OPEN — not investigated |
| D236 | **Surface 1: 2D billboard trees render as a wall/sheet of tree texture instead of discrete camera-facing tree sprites (M-106, user QA report).** Eight passes so far (D265, D266–D270, D271, D272, D273, D274, D275–D276). Ruled out: IA4/IA8 decode (as originally scoped), the blend pipeline, fog combine (as originally scoped), portal-culling over-draw (D271, real bug, fixed, not the cause), converter/UV-conversion bugs (D272, code-read clean), the CI palette bit-layout (D273/D274, tested twice against real content), "mips absent" (D274, confirmed present/enabled), "identical UV window" *for the CI8 classes* (D272). **Current leading picture (D276, M-134): the actual visible weave at confirmed weave pixels is painted by a different, previously-dismissed class — an IA8 blocky-noise texture (`oml=0xc81049d8`/`fmt=3`) sampled with a fixed, byte-identical 2×-wrapped UV window and low fog, reproducing D265's original "same window" observation exactly (D269's CI8-classes framing had reframed away from that lead prematurely).** A third, unattributed CI8 class (`0x0c184b50`) may be the reference's actual discrete-tree layer and shows an erratic z/w suggesting its own occlusion/billboard bug — D236 may split into two root causes. **Ninth pass (D280, M-135): fog math verified N64-faithful (±2 truncation over 2505 verts), the IA8 noise class visually confirmed as the weave's painter (HF 14.8→2.9 when collapsed), `0xc8102078` ruled out (terrain, 1.4M tris), and the depth-race read inverted — `0x0c184b50` tree cards are IN FRONT of the noise quad (z/w smaller=closer) yet still don't visibly win; under deterministic `-level_36` boot they barely instantiate at all (~4 tris vs 720 in a free run). No quick win: remaining work is CPU-side instantiation/room-streaming, not a port fix.** Distinct from D176(b)'s painted tree-line backdrop. Logged in `GRAPHICS-BACKLOG.md`. | OPEN — ninth pass closed with no fix; see D276/D280 |
| D237 | **QoL ask (M-106, user QA): F10 options overlay — make it scrollable with the mouse wheel, expose more settings into it (e.g. screen-flash / no-hit-flash `Game.NoHitFlash`, and other easily-exposed config keys), and organise rows by category (mouse settings together, video together, etc.).** The overlay (`port/src/optionsoverlay.c`) already renders over the registered `config.c` option list; what's missing is wheel-scroll nav, a broader row set, and grouping headers. Cross-ref `OPTIONS-MENU-PLAN.md` §3 (v1 minimum set) + QOL-INVENTORY "PD has, GE lacks". | OPEN — not designed; port-only |
| D238 | **QoL ask (M-106, user QA): mouse sensitivity rework toward normal PC-shooter feel.** Three parts: (1) a *separate* RMB aim-mode sensitivity in addition to hipfire turn; (2) ideally ONE single master mouse-sensitivity setting driving both, **linked by default**, with an option to set them independently later (today `Input.MouseAimSpeed` and `Input.MouseTurnSpeed` are two independent keys, defaults 16/100 — see `input.c:188-190`); (3) up/down should feel mostly the same as left/right (`Input.MouseYScale` exists but the axis asymmetry in the gain constants `AIM_GAIN`/`MOUSE_TURN_GAIN` + smoothing path is the likely culprit). User's reference for "feels better": GEPD Mouse Injector. Lump with the overall mouse/input fixes (D194/D165/D166 lineage; SOLITAIRE natural-pitch scheme switch may subsume part of this — see D166 M-87 note). | OPEN — not designed; port-only (`port/src/input.c` + `config.c`) |
| D239 | **QoL ask (M-106, user QA): click-to-lock vs always-grab parity.** User: make click-to-lock the default *if it isn't already* — it IS: `mouseCaptureMode = 1` is the shipped default (`input.c:185`). The real content: "always grab" feels bad on the file-select menu, not 1:1 with click-to-lock; if making them feel identical is overly complicated, **drop always-grab entirely and remove the setting**. This re-confirms the existing M-82 user decision to rip out `MouseCaptureMode=0` (see `docs/BACKLOG.md` "M-82 playtest feedback" + D192: mode 0's P-controller path can't reach the outer front-end grid cells — a known mode-0-only defect). Recommendation on this new report: take the user's fallback — remove mode 0 and the `Input.MouseCaptureMode` key, keep click-to-lock only. | CLOSED — executed in the v0.2.0 prep pass (port-only): `mouseCaptureMode` + the config key, `reconcileGrab`'s legacy branch, the `!haveAbs` relative menu path and its now-dead P-controller state (`menuEst/TgtH/V`, `menuCursorStep`, `MENU_P_GAIN`/`MENU_EST_DELTA`/`MENU_MOUSE_TO_PX`), the F10 "Mouse capture" row, and the unused `inputMouseCaptureActive()` all removed from `port/src/input.c` / `optionsoverlay.c` / `port/include/input.h`. Click-to-lock is the only mode; existing ini files setting the old key are ignored. Closes D192 with it |
@@ -535,7 +546,13 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D255 | **Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle, D253 fix confirmed working — user got past the opening crouch tile).** Crash during gunplay; abort-unwind backtrace points at `sub_GAME_7F09BAC4` (`vtxstore.c:158`) ← `sub_GAME_7F09BBBC` (`:220`) ← `lvlManageMpGame` (`lv.c:1302`) ← `bossMainloop` (`boss.c:567`). Disassembling the exact CI binary at the fault PC (`0x200db135`) gives `mov 0x14(%rax),%edi; mov 0x10(%rdi),%rax`: the function walks the active PropRecord list keeping `type == 1` — which is **PROP_TYPE_OBJ, not CHR** (the decompiler cast to the union's first member and misnamed the field `ChrRecord.chrflags`; it is actually `ObjectRecord.model` at `0x14`) — then dereferences `model->obj`. Two port-relevant wrinkles: (a) the load is **32-bit zero-extended** because the decomp types the field through the `CHRFLAG` enum, so a model pointer ≥ 4 GB would truncate to garbage; (b) the loop skips records whose `model->obj` is NULL but *dereferences* `model` unconditionally, so an active object with a NULL/freed `model` faults — plausible in a firefight where destructibles (crates etc.) die. Not reproduced on WSL after equivalent play. **Register capture (D254 format, 2026-09-13):** two more Deck crashes — Bunker mid-firefight, then Frigate with ~3 NPCs alive; log: `SIGNAL: 11 / FAULT ADDR: 0x10 / Rax=0x701e5f54 Rdi=(nil) / PC 0x200db135`. `Rdi` = the record's model value = **exactly NULL** → case (b) confirmed: an active PROP_TYPE_OBJ record with a NULL `model`, dereferenced by the unconditional `mov 0x10(%rdi),%rax`; truncation (a) and stale/garbage pointers are ruled out. Faulting PC matches the recorded disassembly (`sub_GAME_7F09BAC4` = `vtxstore_fix_refs`). Grep-verified: nothing in game code ever writes NULL into `ObjectRecord.model` — `clear_model_obj()` nulls `model->obj` only, and `objInit`/spawn paths only activate a record once model ≠ NULL — so the NULL comes from either a fresh/uninitialized record slot being activated or a destroy-path ordering race (model freed/nulled while the record is still active this tick). **Fix constraint:** any guard lands in game code (`vtxstore.c`) → needs rule-#2 sign-off; `port/` cannot intercept this dereference. Next: (1) decision on whether a NULL-model skip in `vtxstore_fix_refs` qualifies as an ABI-class exception (semantics-preserving defensive guard, PD analogue check); (2) if yes, add it plus a `#ifdef PORT` trace at the deactivation/model-free sites to name the path that leaves the record model-less. | OPEN — case identified (NULL model on an active OBJ record; destroy-path ordering), reproducible on Deck in combat (Bunker + Frigate); fix pending rule-#2 sign-off. |
| D256 | **Saves (and config) silently failed to persist on Linux whenever the game was not launched from the directory containing `data/` — reported as "saves do not work at all" on Steam Deck.** `romdataInit` finds the ROM in `$S/` (CWD `data/`) *or* `$E/` (exe dir) *or* `./`, but `sysResolvePath`'s POSIX `$S` branch (used by `ge007.eep` and `ge007.ini`) only ever produced a CWD-relative path — its "fallback" was `./data/`, identical to the primary. Launch from any other CWD (Steam non-steam-game shortcut, terminal in `~`): ROM loads via the exe-dir candidate, game runs, but every EEPROM/config write hits `fopen(..., "wb")` on a nonexistent `./data/` and fails with a stderr-only "cannot write" — saves never persist, and **every launch also starts with default settings** (F10 changes don't stick), which presents as "draw distance/LOD is low" on levels like Dam where the user had raised `Video.DrawDistance` on another machine. The Windows branch already did CWD-else-exedir; the POSIX branch now mirrors it (`readlink("/proc/self/exe")`) and creates the directory best-effort so first-run writes don't fail. **Verified on WSL:** with exe+`data/` in `/tmp/fb2` and CWD=`/`, config/eeprom/ROM all resolve into `/tmp/fb2/data/` and a 25 s run completes clean; the pre-fix binary in the same layout fails to find the ROM at all. **Caveat:** if the user's Deck launch *was* from the bundle dir (CWD had `data/`), this fix does not explain their save failure — need the save-flow detail (does the memory-card screen appear? slot confirm? "saved" message?) before closing the report. Side note found while testing: a `data/` without the generated sidecars (`pccg-*/`, `pcmodels-*/` from `tools_pc/d69_emit.py`/`d43_emit.py`) starts fine but faults in `load_bg_file` (`bg.c:865`) on level load — the reserve functions warn "stage loads will fault" and return 0; expected degraded mode, not a new bug. | FIXED (path half) — save-flow detail still owed from the user. |
| D257 | **New `Game.AllUnlocked` option (F10; default flipped OFF for v0.2.0 — see D259): skip N64 progression entirely — all 21 solo levels selectable at every difficulty, 007 mode available, and the cheat menu fully populated. Port-layer only; no game-code edits.** Three mechanisms, all in `port/`: (1) `main.c` seeds the two live `LEFTOVERDEBUG` RAM flags after `configLoad()` — `debug_enable_all_levels_flag` (all levels) and `debug_007_unlock_flag` (007 mode; both in `src/game/debugmenu_handler.c`, same class as the existing `GE_UNLOCK_ALL` getenv hook in the getter); (2) the eep shim (`libultra.c geEepromPatchAllCheats`) patches the save block at read time — special-casing `fileValidateSaves`'s exact pattern (block 4, 5×96 bytes) — setting all 20 progression-gated cheat-unlock bits per slot (`unlocked_cheats_1/2/3`; the cheat menu row on mode-select only renders when ≥1 is set, `front.c interface_menu06_modesel`) **and** filling every *empty* stage/difficulty completion-time field with the max time 0x3FF (existing player times preserved); both are then CRC'd per slot with the game's own `fileGenerateCRC` or `fileValidateSaves` would wipe the slots. (3) `optionsoverlay.c` row so users can toggle OFF for faithful progression. **Why the completion-time half is required (found by user repro):** when ANY single-player cheat is active, `g_AppendCheatSinglePlayer` makes mission-select (`get_highest_unlocked_difficulty_for_level`, front.c) accept only levels with status `STAGESTATUS_COMPLETED (3)`, not merely `UNLOCKED (1)` — AllUnlocked's debug-flag path returns 1, so enabling a cheat locked every level ("could only do Silo"). Reading the save as a fully-completed campaign satisfies both branches. Time bit-math mirrors `fileGet/SetSaveStageDifficultyTime` verbatim (10-bit fields, difficulties 0–2; 007 times are virtual). **Pitfall hit during implementation:** the first mirror of `save_data` used `times[80]` instead of `(SP_LEVEL_MAX-1)*4 = 76`, making `sizeof` 100≠96 so the block-read size check silently never matched — now enforced by a compile-time `ge_save_slot_size_check`. **Semantics:** unlocked cheats/completions persist to `ge007.eep` on any subsequent save write (self-consistent; mirrors N64 where completing a level permanently unlocks its cheat). No *active* cheats are enabled (invincibility/all-guns stay OFF); weapons remain per-mission pickups. **Verified:** fresh eep — all 5 slots validate `stored==calc` under `GE_SAVELOG=1`; user playtest: all levels selectable at each difficulty, 007 mode present, cheat menu populated, cheats toggle and work (time-modifying cheats untested by user), mission select intact with all cheats ON. | FIXED — verified by user playtest (PR #79 merged); default flipped OFF for v0.2.0 (D259). |
| D259 | **`Game.AllUnlocked` (D257) breaks audio on a fresh save: with it ON, music and SFX load at volume 0 (silence). v0.2.0 ships with `AllUnlocked` default OFF.** User-observed during v0.2.0 artifact testing ("all unlocked seems to break sound"). **Mechanism:** a fresh `ge007.eep` is zero-filled, so every save slot is all-zero. On N64 such a slot fails `fileValidateSaves`' CRC check and is rewritten as `BLANKSAVEDATA` (`src/bondconstants.h:2148`), whose `music_vol`/`sfx_vol` are `0xFF` (max). With AllUnlocked ON, `geEepromPatchAllCheats` (`port/src/libultra.c`) patches the zeroed slots (cheat bits + completion times) and recomputes a **valid** per-slot CRC, so `fileValidateSaves` accepts them as-is and never calls `fileResetSave`. The volume bytes therefore stay 0, and `fileLoadSettingsForFolder` (`src/game/file2.c:1289-1290`) loads them verbatim: `set_mTrack2Vol((save->music_vol << 7) | (save->music_vol >> 1))` = 0 → silence. With AllUnlocked OFF the zeroed slots fail CRC and reset to `BLANKSAVEDATA`, which is why sound works in the default configuration. **Disposition for v0.2.0:** flip `portAllUnlocked` default to 0 (`port/src/video.c`; `SkipIntro` was already 0) — faithful N64 progression out of the box, F10 *All unlocked* still enables the goodie. The quirk then only affects users who deliberately toggle it ON with a fresh save (recoverable: raising the in-game volume writes a nonzero slot, after which the patch leaves volumes alone). **Deferred fix candidate (port-only, ~6 lines):** in `geEepromPatchAllCheats`, detect all-zero slots and seed `music_vol`/`sfx_vol = 0xFF` before the CRC recompute, emulating what `fileResetSave` would have done; real (nonzero) saves incl. a user-muted one stay untouched. | OPEN — quirk documented; default flipped OFF for v0.2.0 (`port/src/video.c`); fix deferred. |
| D259 | **`Game.AllUnlocked` (D257) breaks audio on a fresh save: with it ON, music and SFX load at volume 0 (silence). v0.2.0 ships with `AllUnlocked` default OFF.** User-observed during v0.2.0 artifact testing ("all unlocked seems to break sound"). **Mechanism:** a fresh `ge007.eep` is zero-filled, so every save slot is all-zero. On N64 such a slot fails `fileValidateSaves`' CRC check and is rewritten as `BLANKSAVEDATA` (`src/bondconstants.h:2148`), whose `music_vol`/`sfx_vol` are `0xFF` (max). With AllUnlocked ON, `geEepromPatchAllCheats` (`port/src/libultra.c`) patches the zeroed slots (cheat bits + completion times) and recomputes a **valid** per-slot CRC, so `fileValidateSaves` accepts them as-is and never calls `fileResetSave`. The volume bytes therefore stay 0, and `fileLoadSettingsForFolder` (`src/game/file2.c:1289-1290`) loads them verbatim: `set_mTrack2Vol((save->music_vol << 7) | (save->music_vol >> 1))` = 0 → silence. With AllUnlocked OFF the zeroed slots fail CRC and reset to `BLANKSAVEDATA`, which is why sound works in the default configuration. **Disposition for v0.2.0:** flip `portAllUnlocked` default to 0 (`port/src/video.c`; `SkipIntro` was already 0) — faithful N64 progression out of the box, F10 *All unlocked* still enables the goodie. The quirk then only affects users who deliberately toggle it ON with a fresh save (recoverable: raising the in-game volume writes a nonzero slot, after which the patch leaves volumes alone). **Deferred fix candidate (port-only, ~6 lines):** in `geEepromPatchAllCheats`, detect all-zero slots and seed `music_vol`/`sfx_vol = 0xFF` before the CRC recompute, emulating what `fileResetSave` would have done; real (nonzero) saves incl. a user-muted one stay untouched. | **FIXED in v0.2.1** — the deferred fix was applied: `geEepromPatchAllCheats` now detects all-zero slots and seeds `music_vol`/`sfx_vol = 0xFF` before the CRC recompute, emulating `fileResetSave`; real saves (incl. a deliberately muted one) stay untouched. **By-ear confirmed by the user** (v0.2.1 test pass: fresh save, AllUnlocked ON — audio plays correctly). |
| D260 | **v0.2.1 (user request): mouse-wheel weapon-select direction swapped.** Wheel-up now selects the *previous* weapon and wheel-down the *next*, matching the in-game wheel menu's on-screen layout (the port had them inverted vs the user's expectation). Port-only: `inputPostWheel()` (`port/src/input.c`) — the notches>0 / notches<0 branches were swapped so scroll-up emits the back-notch and scroll-down the forward-notch. N64 build untouched; no game-code edits. | FIXED — build green; one-line branch swap, trivially reviewable. |
| D261 | **v0.2.1 (user request: "fix watch menu sensitivity for wasd/controller buttons"): holding W/S (or a fully-pressed stick axis) on the watch inventory page crawled one item per tap — no repeat.** Root cause is the interaction of two existing behaviours: keyboard W/S map to `stickY = ±STICK_MAX (0x50)` and SDL pads peg at 0x80, both *outside* the smooth-scroll band (0x1f..0x45) that D118d intentionally left for the port — so a held press only ever hit the latched single-step path (`watch_stick_y_pressed_*`, one step per press edge, no repeat). On N64 the same stick level was the *fast-scroll* band (one step per frame), which D118d removed because digital sources peg there and it became runaway scroll. **Fix (port-only, `#ifdef PORT` in `src/game/options.c`):** a bounded auto-repeat helper `geWatchStickFastStep()` — while the stick is held past the smooth band (≥ 0x46 up / ≤ −0x45 down), step one extra item every `GE_WATCH_NAV_RATE` (6) frames after an initial `GE_WATCH_NAV_DELAY` (15)-frame hold; the first press still goes through the untouched latched path, so a tap behaves exactly as before, and the smooth band + D-pad/C-button edge-repeat are unchanged. Called from `game_options_inventory_navigation()` only. **Verified:** build green; headless smoke (`GE_INPUTSCRIPT` long-W-hold on the inventory page) runs clean. Feel-check (repeat rate) owed to the user — both constants are at the top of the helper for a one-number tune. **User feel-check (v0.2.1 test pass): repeat "feels fine for now", but W/S is still too sensitive overall — desired behaviour is tactile one-press-one-item with no risk of scrolling the whole list on an over-held key; candidate tweaks (not yet done): longer `GE_WATCH_NAV_DELAY`, slower `GE_WATCH_NAV_RATE`, or gating the auto-repeat off for keyboard taps entirely (repeat only for analogue stick). Logged, no code change. | FIXED — feel-check received; W/S sensitivity follow-up logged above. |
| D262 | **v0.2.1 (user report): "3D models missing on watch menu screen item select" — the selected-item 3D preview does not appear on the watch inventory page.** User visually confirmed this session: no 3D model on the item-select page where one should be (default selection is *unarmed*, which has no model — a weapon must be scrolled to with S/DOWN). Headless repro (`GE_INPUTSCRIPT` pulse train + `GE_PCDUMP`, Dam `-level_33`): watch can't open during the intro freeze (`bondviewFrozenMoveBond` zeroes input while `g_CameraMode == CAMERAMODE_INTRO`; freeze ends ~frame 1400–1500 on Dam, and controller reads run faster than sim ticks, so scripts are timed by trial); once open + paged to INVENTORY (R_JPAD), the watch UI renders but the centre of the face is empty for every item. **Root cause found and fixed as D264** (`renderdata.flags == 0` in the watch subdraw pass — the cross-global ModelRenderData template read zeroes on PC; `subdraw()` then gated *every* geometry node, emitting exactly 1 word instead of the model's commands). Residual: with flags restored, the preview emits matrix/light-setup words (71 for item 5) but dump frames still show no visible centre geometry — a possible second layer (LOD/distance gating or fast3d rasterisation of the sub-DLs) remains unverified; user visual re-check pending. | FIXED (primary cause, D264) — pixel-level confirmation pending user run. |
| D264 | **D262's root cause (ABI/layout class, cf. D140/D52): the watch item preview's `ModelRenderData` template is read across two adjacent globals that no longer sit adjacent on PC.** `set_enviro_fog_for_items_in_solo_watch_menu()` (`src/game/gunfire.c`) starts with `ModelRenderData renderdata = *((ModelRenderData *) (&D_80035D00));` — a 64-byte copy that on N64 spans `D_80035D00` (a zeroed u32, .bss) *and* `D_80035D04[] = {1, 3, 0…}` (.data): the effective template is the one documented in the comment above `D_80035D00` in `gun.c` — `basemtx=NULL, zbufferenabled=TRUE, flags=3, rest zero` (`CULLMODE_BOTH` is 0). The PC link places .bss and .data in separate sections, so the read is all zeros: **`flags==0`**, and every geometry emitter in the subdraw node dispatch gates on `renderdata->flags & 1/2` (e.g. `modelRenderNodeDl`) — so `subdraw()` emitted exactly one word (`gSPSegment`) per preview, no matter the item. Probes this session: entry gates all pass (hand has item, model header non-NULL), node tree present (~400 nodes, isLoaded=1), world-path geometry nodes carry valid built DLs — but in the watch pass `flags=0` on every node. **Fix (`#ifdef PORT`, values identical to the N64 ROM bytes):** explicit template — `ModelRenderData renderdata = {0}; renderdata.zbufferenabled = TRUE; renderdata.flags = 3;`. Verified at Gfx level: preview emission went 1 → 71 words (item 5) / 81 (mid-scroll) / 9 (item 47, knife-class). Note for follow-up: the 71-word dump is a repeating {matrix/segment/light} unit with no `gSPDisplayList` in the first 24 words — if the user's re-check still shows no visible model, next suspects are LOD/distance gating of the local watch-space matrices (`modelUpdateDistanceRelations`, cf. 7c48f664) or fast3d handling of the preview sub-DLs (segment 3 / render modes). | FIXED — build green, probes removed; visual confirmation pending. |
| D263 | **v0.2.1 (user report): Facility — "ouromov not shooting Trevelyan"; the scripted opening beat never triggers. RESOLVED: not a port bug — the beat is proximity/LOS-triggered exactly as on N64, and the full chain (incl. the execution shot) was verified firing end-to-end on PC.** The triage suspects (tick pacing, camera-mode gate, silent anim lookup) were all ruled out at runtime. **The beat, fully mapped** (static decode of `UsetuparkZ.c` — remember every DBYTE/TBYTE in the dump is bswap16/32'd relative to game interpretation): Trevelyan = guard chr 67 (body 9 = Trevelyan_006, pad 131) on ai_18: walks to pad 124, wanders, then loops a sees-Bond check; **only when Bond is in his vision** (range <10k, facing ±110°, fog, random gate ≈1/49/tick, LOS + *same stand tile* via `chrCanSeeBond`) does he jump to ai_19: runs at Bond, alarm + dialogue timers, sets an objective bit; BG list ai_41 (ID 0x1000, auto-started) polls that bit and spawns **Ourumov (body 7) at pad 88 with ai_23** plus a 6-soldier squad; ai_23 renumbers him to chrnum 78 and arms him → ai_22: runs to pad 123, waits for Trevelyan within 0x1400 of it, ~95 s dialogue cascade, then the **execution shot** (`HitChrWithItem` from 78 on 67). It is a multi-minute sequence that *starts* only when the player walks Bond west and down a floor into Trevelyan's room — Bond spawns 8900 units away on the upper floor with LOS blocked (probed directly: `stanTestLineUnobstructed` = obstructed, different stan tiles), identical geometry to N64. **Runtime verification** (`-level_34`, `#ifdef PORT` probes in the AI executor/vision path, all since removed): all 15 BG lists + guards start; Trevelyan alive on ai_18; every vision gate passes except the geometric ones at spawn distance; player movement confirmed working (note for future headless tests: under the port's default SOLITARE/natural-pitch scheme, walking is C-button/D-pad fed — W→C-up — and the analog stick carries pitch, so `GE_INPUTSCRIPT` SUP/SDOWN tokens do *not* walk Bond there; use UP/DOWN pulses). **Decisive test:** a temporary PORT-only teleport probe dropped Bond 65 units from Trevelyan at t≈12 s → seconds later: vision fires (chr 67 → ai_19), objective bit set, `SpawnChr body=7 pad=88` + squad, chrnum→78 on ai_23; ~2½ min later **`HitChrWithItem from_chr=78 target=67 part=8 item=6`** — Ourumov executes Trevelyan. Most likely explanation of the report: the tester never walked Bond into Trevelyan's sightline (or pre-D253 crash masked it). No game-code or port-code change required. **Kept from this session (test-harness only):** `port/src/input.c` — `GE_INPUTSCRIPT` analog-stick tokens were applied *after* the stick was already written out, so SUP/SDOWN/SLEFT/SRIGHT were silently discarded; the script override now runs before the clamp/write. | RESOLVED (not a bug) — beat verified firing end-to-end on PC incl. execution shot; **user playtest confirmed the sequence plays normally**; probes removed; input-script stick fix kept. |
| D265 | **D236 investigation (port-layer, `port/fast3d/gfx_pc.cpp`): the tree cards are IA8/IA4, not IA16 — and `import_texture_ia4` decoded the RDP's I2:A2 texel as I3:A1, inverting the alpha ramp. Fixed; awaiting user visual re-test.** The earlier sessions misidentified the format: a live tri-time tile census (probe in `gfx_sp_tri1`, since removed) shows level_36's tree-card classes are (a) **IA8, line=64B → 64×16** (texdump r107), drawn OPA/bilerp/1-cycle/AC_NONE — *genuinely fully opaque* (A nibble F on 1023/1024 texels; opaque on N64 too) — and (b) **IA4, line=64B → 128×16** (texdump r117), drawn ZMODE_XLU/point/2-cycle with m2a=CLR_MEM/m2b=1MA, i.e. texel-alpha blended. There are no 32-wide IA tiles at tri time; the prior "32×32 tile" reading was wrong, and the whole earlier IA16 byte-pair/I11:A5 debate (r107) was analyzing an IA8 image as byte-pairs. **The wall's root cause:** r117 decoded as I2:A2 is a strip of ~13 discrete conifer silhouettes (trunk+canopy shapes visible in the ASCII render, `scratch/r117_ia4_alpha.png`) with a 3-level alpha ramp {0: 95% of texels, 85, 170} — faint dithered distant trees on N64. But `import_texture_ia4` read each nibble as intensity=`part>>1` (3-bit, SCALE_3_8) + alpha=`part&1`: A2=1 canopy pixels became **fully opaque**, A2=2 edge dithering became **invisible** → opaque repeating tree silhouettes = the "wall". The bug is inherited verbatim from the PD port (`pd_port/port/fast3d/gfx_pc.cpp` has the identical I3:A1 read — latent there, fatal for GE's tree strip). **Fix (applied):** `intensity = part>>2`, `alpha = part&3`, both via new `SCALE_2_8`; this is the hardware-correct RDP IA4 layout. Also cleaned up: the ia16 importer comment now states plain I8:A8 (the byte-pair read *is* correct IA16; the old "r107 proof" was misidentified data), and the earlier part-1 dimension fix stays (harmless/correct for real IA16 tiles). **Caveats/owed:** (1) user visual re-test of Surface 1 trees — headless A/B was in flight when work halted; a cross-run region diff was inconclusive (camera drift between captures); (2) the decode change affects *every* IA4 tile game-wide — regression-sweep other levels for dithered/alpha IA4 art that now renders with correct 3-level alpha; (3) if a hard repeating band remains after the fix, it is the opaque IA8 strip (a), which matches hardware and D236 needs re-scoping. **OUTCOME (user re-test): NEGATIVE — trees still render as the wall with the I2:A2 decode applied; the change was fully reverted (`git checkout` of `port/fast3d/gfx_pc.cpp`, back to the original PD-derived importers) and work is parked.** The census/RM/texture data above stands as evidence for the next attempt: the decode was *necessary* (I2:A2 is the hardware layout and r117 is unambiguously an authored tree strip under it) but *not sufficient* — remaining suspects, in order: (i) fast3d's blending of these XLU/2-cycle cards (texel alpha reaching the GL blend at all — the `texture_edge`/`SHADER_OPT_TEXTURE_EDGE` path forces `a>0.19 ? 1 : discard`, which for a 3-level ramp quantizes to opaque/discard, i.e. still hard-edged), (ii) per-card UV windowing/placement (all captured cards sampled the same u:0..64/v:0..16 window — confirm against N64), (iii) fog/distance. Note for the next session: verify what the N64 actually shows in that region before more port-side changes (the opaque IA8 strip is faithful to hardware either way). | PARKED — reverted to baseline; evidence + suspect list above. |
| D258 | **PAL/JP asset conversion is broken at the source-data level — `scripts/filelist.e.csv` and `filelist.j.csv` carry naming defects, so `gen_romassets.py e/j` (and the d43/d69/d88 emitters' `find_row`) cannot resolve PAL/JP builds. v0.2.0's drop-in ROM feature therefore ships NTSC-U only.** Discovered while scoping issue #6 (drop-in ROM). **PAL:** 31 of the 512 model rows in `filelist.e.csv` have truncated/renamed basenames (an extraction artifact — e.g. `Pbridge_console` ×6 where the resource table says `Pbridge_console1aZ…3bZ`, `Pchrgrenadelauncher` for `PchrgrenadelaunchZ`). **JP:** one font row is mislabeled — `fontBankGothic_fontchartable.bin` @3044276 is named `fontZurichBold_fontchartable.bin` (a duplicate label). Both were proven without a PAL/JP ROM: (a) greedy two-pointer sequence alignment of the 512-row `file_resource_table.inc.c` model order against each CSV's model rows gives an exact 1:1 match (481 by-name + 31 positional for PAL), offsets strictly non-decreasing, and zero size anomalies vs US (all within [0.5×, 2×]); (b) JP font row proven by the contiguity invariant — Gothic kerning @3043600+676 = 3044276 exactly hits the mislabeled row, whose size (8716) equals the US Gothic chartable, while Zurich's own chain is contiguous one row later. **Consequence:** no PAL or JP PC exe has ever been buildable from this tree (latent — only NTSC was ever exercised); the CSV `file` column is a label only (emitters look up by basename, never read the path), so the repair is a pure data fix: rename the 31 PAL model rows + 1 JP font row to the exact table symbols. **Scope decision (v0.2.0):** ship drop-in ROM support for NTSC-U only — `prepare-assets.py`/the frozen `ge007-convert` gate non-US ROMs with a clear message after SHA-1 region detection; the CSV repair + multi-region matrix + per-region exes are deferred to the Part B follow-up. | OPEN — repair path verified, not applied (out of v0.2.0 scope). |
Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS
@@ -9900,3 +9917,185 @@ 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.
## D266 — M-125: re-applied the two reverted D265 port fixes (I2:A2 IA4 decode + AC-aware edge handling); new tri-time census shows **zero IA4 draws from the boot camera** — the D265 "IA4 tree strip" class does not reproduce at the wall
Re-applied, on top of the committed baseline: (a) `import_texture_ia4` I3:A1→I2:A2 decode (`intensity = part>>2`, `alpha = part&3`, new `SCALE_2_8`) — hardware-correct RDP IA4 layout, necessary per D265; (b) `gfx_opengl.cpp` edge path: the inherited hardcoded `if (texel.a > 0.19) texel.a = 1.0; else discard;` replaced with alphacompare-keyed semantics — AC_DITHER (`opt_noise`) → noise-dithered discard; AC_THRESHOLD → discard only fully-transparent; AC_NONE → discard below 0.5, survivors keep alpha for blending (the old line forced every survivor opaque, defeating ALPHA_CVG_SEL); the plain `opt_alpha && opt_noise` dither line is now gated off when edge is also set. `gfx_pc.cpp`: `texture_edge` no longer set for AC_DECAL tiles. Both are port-only; identical latent lines exist in the PD port (PD's art never exercises them — PD has **no billboards at all**, full audit this session).
New env-gated census probe (`GE_D266`, in `gfx_sp_tri1`) logging per-draw tile fmt/line-width/RM class for alpha-bearing draws. **Decisive result: over 1.5M tris from the Surface 1 boot camera, ZERO IA4 draws.** The dominant alpha classes are IA8 line=32B/64B XLU 2-cycle cards. D265's census (class (b) = "IA4 128×16 tree strip") either came from a different camera position or was misattributed — the visible wall band is not drawn by IA4 at boot (see D269 for what actually draws it).
**State: both fixes remain in-tree, UNCOMMITTED, with the probes.** They are necessary-but-unverified for the wall (the wall turned out to be a different texture class); the I2:A2 decode still owes a game-wide IA4 regression sweep before any commit. Nothing user-visible changed at the wall — consistent with D265's negative re-test.
## D267 — M-126: IA8 dump probe + per-tile-slot upload-sequence tracking — the three IA8 images in play are an opaque noise card and two sparse mast sprites; none of them is the wall band
Probe infra (`GE_D267`, `gfx_pc.cpp`): PPM dumper + alpha histogram for 32px-wide IA8 strips in `import_texture_ia8`; per-tile-slot upload sequence tracking (`d267_seq` counter + `d267_slot_img[128]` stamped in the `import_texture()` dispatch) so tri-time probes can name which uploaded image a draw uses. Caveat discovered: `gfx_texture_cache_lookup()` returns early **before** the stamp on cache hits (key = fmt+addr+size+pal), so a draw's logged imgseq can be stale — the actual image is the earlier import that populated the cache (traceable via the GE_TEXI census log).
Results (level_36, 15 s): three distinct IA8 images — seq 113 (32×32, **fully opaque**, 1023/1024 texels at A=F; blocky noise pattern), seq 165 (32×64, sparse mast/pole sprite: 1344 transparent / 592 opaque / ~112 mid-alpha), seq 170 (32×64, another sparse sprite). Dumps in `scratch/d267_ia8_tile0_seq*.ppm`. None of these matches the horizon stripe band → the wall is not an IA8 card either. Combined with D266: **the entire IA-focused framing of D236/D265 was examining the wrong texture class.**
## D268 — M-127: force-alpha A/B test proves the texel-alpha blend pipeline works end-to-end — the wall is NOT a broken blend path
`GE_D268_FORCEA=0|15` in `import_texture_ia8` forces every IA8 texel alpha to 0 (transparent) or F (opaque). Three captures at frame 730 (`scratch/d268/{normal,alpha0,alphaF}_730.ppm`): the horizon band region vanishes with alpha=0 and becomes a solid bright block with alpha=15. The GL blend state, texel-alpha path, and shader generation all composite correctly — whatever renders the wall does so with *correct* blending of *something*. (Side note: the user's 9/14 BSOD 0x119 was HWiNFO64/nvml.dll, not the game — game log clean.)
## D269 — M-128: **the wall is CI8 32×32, not IA at all** — band-gated all-format probe identifies two draw classes per room stream (OPA + XLU), both BILERP/LOD/2-cycle/**FOG-on**; the cards are soft gradient blobs, and the stripe period ≈ one card per visible room
`GE_D266W` v4 (`gfx_sp_tri1`, env-gated): tracks **all** draw classes whose projected vertices fall in the top 3–9% of the viewport (the measured stripe band), logging fmt/line-width/oml/omh/zmode/cyc/blender/imgseq + first-vertex screen position. Results per room stream (imgseq cycling ~174→179→264→269→282…, shared ROM addresses 0x70173840 / 0x70173de8):
- **Class A (OPA):** CI8 32px, oml=0xc4112078, omh=0x0099ac20, first vertex screen(1.30, 0.07) — off-screen right
- **Class B (XLU):** CI8 32px, oml=0xc41049d8 (m1=CLR_IN·A_IN / m2=CLR_MEM·1MA = standard straight-alpha), omh=0x0099ac60, zmode=XLU, first vertex screen(0.61, 0.08)
**omh decode (GE gbi.h field layout: TEXTFILT@12, TEXTLUT@14, TEXTLOD@16, TEXTPERSP@19, CYCLETYPE@20):** both classes are **BILERP + TEXTLOD-on + perspective-corrected + 2-cycle**, and **bit 13 = FOG enabled**. The band is ~19 repeats across a 1957px frame (period ≈101px) ≈ one card per visible room in the row.
Texture content (`GE_TEXDUMP`/`GE_TEXRAW` + offline decode of `texdump/r*.bin`): the CI8 cards are **soft vertical gradients / sparse blobs** (index histograms concentrated at low indices, e.g. idx 6–14; ASCII renders show gradients and small clumps, not tree silhouettes). NCC correlation of every dumped card against the frame-730 capture at scales 1/2/4: nothing above 0.35 — the cards are not visible as rigid copies (consistent with bilerp magnification + fog wash).
**Reframed hypotheses, ranked:**
1. **Fog.** Both classes have FOG on. On N64 these distant cards are presumably washed toward the sky colour into a smooth pale band; if the PC's fog range (`rsp.fog_mul`/`fog_offset`; per-vertex fog computed at `gfx_pc.cpp:1591`, `fog_z = z*winv*rsp.fog_mul + rsp.fog_offset`) or its shader application differs, the raw dark gradient cards show through as hard stripes. D218 family ("fog/far-clip tint"). **Cheapest test:** extend the band probe to log per-vertex fog for class A/B tris (if vFog≈1 but screen shows dark → fog not applied in shader; if vFog small → range mismatch), or env-gate a fog disable and compare.
2. **TEXTLOD / mipmap sampling.** N64 samples mip levels when minifying; check whether the port generates GL mipmaps for these tiles (tex_lod path) and which min/mag filter is used — and first measure the *projected quad extent* (the probe only logs one vertex; if cards are minified rather than magnified, mips matter directly).
3. **Draw density / portal culling.** PC may draw cards from rooms N64 culls → denser wall. Compare total band-tri count vs expected visible-room count.
4. **Palette (D217 family).** GE_TEXRAW now also writes `.pal` (512 B = 256 RGB555) for CI formats; the aborted run never produced them. Decode class A/B cards as RGB and check colours against the N64 ref (`docs/img/bugs/d227-surface-emulator-ref.png`: smooth bright band + organic clusters centred x≈0.35–0.75).
**Owed next (concrete):** (1) re-run `rm -rf texdump && mkdir texdump && GE_TEXDUMP=1 GE_TEXRAW=1 GE_D267=1 timeout 15 ./build-pc/ge007.x86_64.exe -level_36` and confirm `.pal` files land; (2) decode `texdump/r173..r282_f2_*` CI8 cards + palettes offline to RGB PNGs for the user's eyeball (this model cannot view images — programmatic analysis only; a vision-capable session could do the N64-ref comparison directly); (3) add projected quad extent (min/max screen x over the 3 vertices) + per-vertex fog to the `GE_D266W` band probe — one more run decides hypothesis 1 vs 2.
## D271 — M-129: **the wall, third pass — suspect #3 (draw density / portal culling) confirmed as a real over-draw; the D106 guard was the cause.** Near-plane-straddling portals produce huge *finite* bounds that N64 clamps/culls in `bgRectIntersect`; the PC guard mapped them all to full screen, force-drawing whole rooms N64 never submits. Guard narrowed to non-finite-only; culling now matches N64
**Context.** D269's ranked suspects for the Surface 1 tree wall ended with "#3 draw density / portal culling — PC may draw cards from rooms N64 culls → denser wall". This pass tests exactly that, using the boot camera (`-level_36`), where the wall is most prominent and the scene is (nearly) static.
**Instrumentation (`#ifdef PORT`, `src/game/bg.c`, all env-gated, kept in tree).**
- `GE_D271` in `sub_GAME_7F0B5864` (the portal-bounds builder): per-point capture of every clip point's `z` and its `transform3Dto2DWithZScaling` projection; on each D106-guard hit, dumps the portal number, connected rooms, flags, scale, and all points (`D271 d106 …` / `D271 ptN …` lines). A second probe logs **finite** out-of-range bounds that now flow through to `bgRectIntersect` (`D271 oor-finite …`, budget 8) so their clamp outcome can be checked against N64 semantics.
- `GE_D104` print line extended with `hasPortals=… ordDegen=… d106Fallback=…` — the two counters separate the *ordinary* degenerate case (genuinely edge-on portal, min>max on a real axis — expected, N64 does the same) from the D106-fallback case.
**Findings.**
1. It is not one portal: at the Surface 1 boot camera, **11 different portals around room 13** straddle the camera near plane every frame (a clip point lands exactly at z==0; `transform3Dto2DWithZScaling`'s `inv_z = -1e20f` sends it to ±1e20–1e26-scale screen coords). The D106 guard (`lim=1e5`) fired on **every frame** (~396–781 hits per 600-tick print window) — occlusion culling was effectively defeated for that viewpoint.
2. **The N64 semantics the guard got wrong:** there is no such guard in the N64 code. The huge values are *finite*, and `bgRectIntersect` is pure min/max comparisons: a bound pair that brackets the screen clamps to the parent/screen box (portal covers the view → room kept); a pair whose finite portion falls off-screen yields an **empty** box (room culled). The original guard collapsed both cases to "full screen" — correct for the first, **over-inclusive for the second**.
3. Per-portal check of the over-included case (`D271 oor-finite`): every culled portal's finite portion is far off-screen — e.g. Surface 1 p54 (r22) min.x=997 (>319, entirely right), p68 (r8) max=(-282,-391) (entirely left/below), p73 (r14) min.x=500; BUNKER1 p28 (r25) y-range 339.85..2.9e24 (entirely below the 10..230 screen). With identical f32 values N64 produces the identical empty clamp → **N64 culls these rooms from these viewpoints; the old guard was drawing them.**
**Fix (one constant, `src/game/bg.c` `sub_GAME_7F0B5864`, `#ifdef PORT`).** D106 guard `lim`: `100000.0f` → `1e38f`. Rationale: this projection can reach ~1e26 at most, so 1e38 catches only non-finite values (NaN/inf), which are genuinely unsafe to let through (NaN poisons the downstream comparisons). Finite out-of-range bounds now flow into `bgRectIntersect` exactly as on N64. The finite min>max case is untouched — it still hits the original `degenerate` check (the N64 code path itself), so D106's actual regression (BUNKER1 sky-fill voids from *discarded* portals) stays fixed.
**Verification.**
- `d106Fallback`: ~396–781/600 ticks → **0** on both Surface 1 (`-level_36`) and BUNKER1 (`-level_09`); `ordDegen` unchanged (ordinary edge-on portals still map to full screen, as on N64).
- roomsDrawn at the Surface 1 boot view: **17–18 → 10–13**; per-room boxes now narrowed correctly (e.g. r8 = right sliver `246,10-319,230`, r12 full-screen via a genuine bracketing clamp) instead of ~18 full-screen boxes.
- BUNKER1: roomsDrawn 4→3 (r6 state, lost r5) and 3→1–2 (r29 state, lost r25/r28). Traced per portal above — the losses are the N64-matching empty clamps, i.e. moves *toward* N64 fidelity, not regressions; the D106 void case (discarded min>max portals) is untouched.
- Long unattended runs (`timeout 90+`): no crashes on either level; intro camera progresses normally to later states (curRoom=18, roomsDrawn=4).
- **Pixel-level A/B — inconclusive, owed to the user.** Frame captures at matched `curRoom` state (frame 1500, room-13 state: `scratch/d271_1500_sbs.png`, baseline left / fixed right) show a 14.7% changed-pixel difference — but a same-build/same-state pair differs by 14.8% with an *identical* spatial grid (top-heavy band = animation/camera micro-motion at this viewpoint, not the fix). The boot view is not static enough for programmatic A/B; the user's eyeball on the tree wall is the deciding check.
**OUTCOME (user re-test 2026-09-14): NEGATIVE.** With the fix in place the trees still render as one big wall — and the user's read is that **the texture itself isn't being mapped correctly**, not merely an over-draw/alpha problem. So suspect #3 was a real N64-divergent over-draw (kept — it is a fidelity fix, do not revert) but it is NOT the wall's root cause. The D265 note gains weight: *"all captured cards sampled the same u:0..64/v:0..16 window — confirm against N64"* — if every card samples the same region of a shared texture, the result is exactly a repeating sheet. Top suspect for the next pass: **per-card S/T coordinates / UV windowing** (model DL vertex S/T, tile setup foldS/foldT/tcShift, or `d43_emit.py`'s S/T conversion), then TEXTLOD/mip sampling, then palette.
**Owed / next.**
1. **Per-card S/T audit** — dump the actual vertex S/T + tile for each wall card at draw time (extend `GE_D266W`) and check whether distinct cards carry distinct S/T windows; compare against the N64 ref (`docs/img/bugs/d227-surface-emulator-ref.png`: smooth bright band + sparse organic clusters, not a continuous sheet).
2. Regression sweep of other levels' room counts is cheap insurance (`GE_D104=1` spot-checks); BUNKER1 already re-verified above.
3. Probes are budget-limited and env-gated; keep until the user check lands, then remove with the D266–D269 probe set if the wall closes.
## D272 — M-130: **the wall, fourth pass — per-card S/T audit (D271's owed next-step #1).** Distinct cards carry distinct (not identical) UV windows; the XLU class wraps its CI8 tile ~4.5× horizontally; traced to genuinely-authored ROM vertex data, not a `d43_emit.py` conversion bug — hypothesis shifts back to TEXTLOD/mip handling of a heavily-wrapped, highly-minified tile
**Context.** D271's fix (portal-culling guard) was verified but the user's re-test was negative: "trees are a big wall rather than the texture being mapped correctly." HANDOFF's next-steps for this thread ranked **per-card S/T / UV windowing** as the top suspect, driven by a D265 note that "all captured cards sampled the same u:0..64/v:0..16 window." This pass builds the audit HANDOFF asked for and answers the decisive question it posed: do distinct card instances carry distinct S/T windows, or identical ones?
**Instrumentation (`#ifdef PORT`, `port/fast3d/gfx_pc.cpp`, `gfx_sp_tri1`, env-gated `GE_D272`, budget 200 lines, kept in tree).** A new raw per-triangle dump, filtered to D269's exact two wall-card classes by low-24-bit `other_mode_l` (`0x112078` OPA / `0x1049d8` XLU — matching regardless of the top-byte pipeline bits, which vary between otherwise-identical-looking draws sharing the same blend equation). For each matching tri inside the measured wall band, logs: `oml`/`omh`, the bound texture's `seq` (image identity), tile `fmt`/`siz`/`line`/`width`×`height`/`mask`/`shift`, the tile's own UV window (U10.2 → texels, `/4`), and all three vertices' UV (vertex ST is s10.5 → texels, `/32`).
**Findings.**
1. **Both classes confirmed CI8** (`fmt=2 siz=1`), matching D269 exactly — the earlier unfiltered run of this same probe (before the oml filter was narrowed) picked up an unrelated `fmt=3` (IA) class sharing the same low-24-bit blend bits under a different top byte; that class is NOT one of D269's wall cards and is excluded by construction once the filter checks the full pair of known `oml` values.
2. **UV windows are NOT identical across instances of the same class/texture.** OPA class (`seq=4`, 8 samples): first-vertex U ranges 5.25→5.56 (spread ≈0.3 texel), consistent quad shape (u≈0..32, v≈0..11) each time but never byte-identical. XLU class (`seq=168/169`, multiple samples): U range spans ~143.16–144.81 (spread ≈1.6 texel) across different physical card instances. This is genuine per-vertex variation, not a converter collapsing every card onto one hardcoded window — **D265's "identical window" observation did not come from these cards**; it most likely came from a different draw class caught by the old unfiltered/undifferentiated band probe (D266W has no oml filter and this screen band is crossed by several unrelated classes — see the `fmt=3`/`oml=0x0f0a4000`/`0x00504340` families logged in this session's first (unfiltered) D272 run, several of which *do* show identical `(0,0)` UV across every instance — degenerate/filler geometry, not wall cards).
3. **The XLU class's U range is ~143–145 texels wide — ~4.5× its own tile's 32-texel wrap period** (`mask=5` ⇒ `1<<5=32`). The OPA class stays within roughly one tile width (u≈0..32). This is a large, consistent multi-wrap pattern, not noise.
4. **Traced to `tools_pc/d43_emit.py`'s vertex emit path — no converter bug found.** `emit_main_vtx` (the function that writes every model's `Vtx` array) copies `tc[0]`/`tc[1]` (the S/T fields, offsets 8/10 of the 16-byte N64 `Vtx`) via `put_s16(q+8, be16(src, o+8))` / `put_s16(q+10, be16(src, o+0xA))` — a plain big-endian→little-endian 16-bit byteswap, **no scale, no offset, no reinterpretation**. Every other field in the same struct (`ob[3]`, `flag`, `cn[4]`) goes through the identical plain-copy pattern. There is no code path here that could inflate, rescale, or corrupt a small authored S/T into a ~4.5×-wrap value — the ~144-texel figure must already exist in the ROM's ilbytes at that vertex. **This rules out the converter as the source of the wide wrap** (cf. the D108–D112 `put_f32` byte-reversal precedent this thread was checking against — that bug class is not present here).
**Reframed hypothesis (supersedes D271's "top suspect").** Since the wide UV wrap is real, ROM-authored data (Rare's own design: tile one CI8 texture ~4.5× across a long background quad), the wall is not a windowing/converter bug. The candidate mechanism shifts to *how* the port renders a heavily-wrapped, heavily-minified (TEXTLOD-on) tile: GL's `GL_REPEAT` + whatever mip level(s) OpenGL selects/generates for this tile vs. the RDP's own per-scanline TMEM address wrap + LOD blending. D269's hypothesis #2 (TEXTLOD/mip sampling) is now the leading suspect again, ahead of fog range/application and palette.
**Owed / next.**
1. Measure the actual on-screen minification factor for these cards (projected quad extent vs. tile texel count) — if the quad is only a handful of screen pixels wide while sampling 144+ texels, GL's automatic mip selection is likely picking a coarse level, and any difference from the RDP's own LOD blend range would show up exactly as "hard repeating pattern" vs. "soft distant band."
2. Check whether `glGenerateMipmap` (`gfx_opengl.cpp:748`) is actually invoked for these specific CI8+`GL_REPEAT` tiles, and whether the generated mip chain wraps correctly at the tile's `1<<mask` period rather than at the raw 32×32 upload (should be identical for a power-of-two tile, but confirm — this is the one open question RC3/D167 didn't fully close).
3. Cross-check against the N64 reference (`docs/img/bugs/d227-surface-emulator-ref.png` / `mupen64/`) to confirm whether the intended look really is "soft blended repeats" (mip-driven) rather than a different UV entirely — do not assume without a direct look.
4. Probe is budget-limited (200 lines) and env-gated (`GE_D272`); keep until the wall closes, then remove with the D266–D272 probe set.
## D273 — M-131: **N64 ground-truth screenshot obtained; palette-decode A/B (pre-existing, undocumented `GE_D273` probe found in the uncommitted tree) tested against it and RULED OUT** — the standard ARGB1555 palette layout does not change the wall's appearance at all, but corrupts unrelated CI8 textures (path, viewmodel skin tone)
**Context.** The user supplied two reference screenshots (`docs/img/screenshots/gepd-surface.PNG` — 1964 N64 emulator, GEPD edition ROM, same Surface 1 corridor; `docs/img/screenshots/port-surface.PNG` — our PC port, same location) for a direct side-by-side the thread had been missing (D271/D272 both flagged "no direct N64 look yet" as an open risk). **N64 reference: discrete conifer silhouettes (trunk+canopy visible) lining both sides of a snow corridor, blending softly into a dusk sky — sparse, not a sheet.** **Our port: a continuous, uniform, fine-grained diagonal herringbone/houndstooth weave covering the same wall area, flat-topped, no tree silhouettes at all** (`scratch/d273_base_1500.png`, `-level_36` frame 1500, matches the user's `port-surface.PNG` framing).
**Also found this session (not written up by whoever added it): an existing, uncommitted `GE_D273` A/B probe already in `port/fast3d/gfx_pc.cpp`'s `palette_to_rgba32`** (env-gated, default off, part of the pre-session diff — not authored this session, and not mentioned in HANDOFF.md or findings.md before now). Its comment hypothesizes the inherited PD `RGBA16`/CI-palette decode (`a=bit0, r=15:11, g=10:6, b=5:1`) is one bit off the N64 hardware `ARGB1555` TLUT layout (`a=15, r=14:10, g=9:5, b=4:0`) that GE's RSP ucode actually uses for CI palette expansion, and that for one sampled wall-card palette entry (`0x0011`) this flips the AC_NONE alpha-test outcome from discard (sparse, N64-correct) to keep (opaque sheet, the wall bug) — a plausible, concrete mechanism, matching the HANDOFF's long-parked "side suspect: palette" line, but never actually tested against a live frame.
**Test performed this session.** Captured matched-tick frames (`GE_PCDUMP=1500-1500`, `-level_36`) with and without `GE_D273=1`.
- **Wall region: visually unchanged.** Same herringbone pattern, same density, same silhouette (none). A pixel diff of the wall crop shows a non-trivial mean delta (~28/255), but this is consistent with D271's already-documented run-to-run boot-camera/room-streaming jitter (~14–15% baseline noise between two *identical*-build runs at this same viewpoint) — not a content change; eyeballing both frames side by side shows the identical weave.
- **Everything else got worse.** The snow path (also CI8-textured) turned into a garish cyan/magenta moiré, and the first-person hand/viewmodel skin tone rendered teal-green instead of flesh tone — both regressions exactly where you'd expect if the "standard" ARGB1555 reading is in fact the wrong one for these particular palette sources, or the reinterpretation is simply not selective enough (the probe applies unconditionally to every CI4/CI8 palette lookup, `ignoring palette_fmt` per its own comment, which is a blunt instrument for testing one specific texture's TLUT).
**Conclusion.** The palette-bit-layout swap is **not** the wall's root cause: it does not visibly touch the wall at all, while breaking unrelated known-good CI8 content. `GE_D273` should stay default-off; do not enable it or pursue this specific decode-layout change further for the wall. The underlying question the probe's comment raised (is GE's RSP CI-palette expansion really "standard" ARGB1555 regardless of `palette_fmt`, per `gmain.s`) may still be worth resolving someday for its own sake, but it is decoupled from D236 now.
**Process note.** This probe existed in the working tree with a full, specific, plausible root-cause theory and was never mentioned in `docs/HANDOFF.md`'s CURRENT TASK or `docs/dev/findings.md` — a live investigative lead sat undocumented across at least one handoff boundary. Whenever picking up an in-flight investigation, `git diff` the suspect files (not just what the day's handoff narrates) before assuming the handoff is a complete inventory of uncommitted work.
**Owed / next.**
1. Get an actual reference capture from `mupen64/` (this session used a third-party 1964-based build the user had on hand, GEPD edition — a second, known-clean N64 emulator source would strengthen the ground truth) for a pixel-level comparison, not just an eyeball read.
2. D272's TEXTLOD/mip hypothesis is still the live lead — this session only closed off the palette branch. Proceed with D272's owed next-steps (minification measurement, confirm `glGenerateMipmap` actually fires for this tile).
3. Given the N64 reference shows genuinely discrete tree shapes (not merely "softer" versions of the same repeating pattern), also keep open the possibility that the port is sampling entirely wrong texture content for this tile (not just filtering it differently) — the herringbone weave does not resemble D269's offline-decoded "soft gradient / sparse blob" content at all, which is itself a discrepancy worth reconciling before committing further to the mip-filtering theory.
## D274 — M-132: **seq/addr identity is unreliable across runs (both are runtime-arena, not stable ROM, identifiers) — fixed with an unconditional per-slot address stamp; the "content vs. decode" discrepancy D273 flagged is RESOLVED as an offline-analysis bug in this session, not a port bug — mips confirmed generated + enabled for the real wall-card tile, narrowing the live suspect to mip-blend *quality*, not presence**
**Context.** An external review of the D273 write-up (this session) flagged that all three passes analyzing "the wall cards" (D269, D272, and a `gfx_pc.cpp` code comment) reported different `seq` ranges, and correctly identified the cause: `d267_slot_img[tile]` (D267) is stamped in `import_texture()` *after* the texture-cache-hit early return (`gfx_texture_cache_lookup`, line 1131) — on a cache hit the stamp is never refreshed, so it can read back the identity of whatever last triggered a real upload into that TMEM slot, not the texture actually bound for the current draw. The review recommended anchoring identity on the ROM source address instead.
**Fix (`port/fast3d/gfx_pc.cpp`, `#ifdef PORT`).** Added `d267_slot_addr[128]`, a companion array stamped **unconditionally**, immediately after `orig_addr` is known and *before* the cache-lookup early return — so it always reflects the real source address of whatever is currently bound to that tile slot, cache hit or miss. Threaded into every D272-family log line (`D272[...]`, `D272S`, `D272U`, `D272P`) as `addr=%p`.
**This fix immediately surfaced a second, independent identity hazard: the runtime address itself is not a stable cross-run identifier either.** Two hard confirmations from live runs against the same build/level:
1. **Same `seq`, different `addr`, same run.** Isolating the XLU wall class alone (`GE_D270KEEPOML=1049d8 GE_D272=1`), `seq=65` was logged against **two different addresses** (`0x701587c8` ×161, `0x70155738` ×17) in one 15 s capture — direct proof the seq stamp is stale mid-run, not just across runs.
2. **Same `addr`, different declared format, different run.** `0x70155738` (one of the two addresses `seq=65` produced) showed up in a *separate* run (with `GE_TEXDUMP=1`) as `GE_TEXI[4] fmt=3 siz=1 tile=54x54` (IA8) — a completely different format/size than the CI8 32×32 the tri probe recorded for it moments earlier in a different run. GE's texture/model pipeline decodes into a shared runtime arena (the M-88 lesson already in `docs/porting-notes.md`: "GE's model and image pipelines resolve almost everything through small numeric IDs into shared, runtime-decoded pools"); load-order timing (already documented as non-deterministic across runs by D271) evidently reuses the same arena address for different content run-to-run. **Neither `seq` nor the raw runtime `addr` is a stable identifier across separate process runs — only within a single run, and even then only outside a cache-hit window for `seq`.** D269's recorded "shared ROM addresses 0x70173840/0x70173de8" should be read as *that run's* addresses, not persistent identifiers to search for in a fresh capture.
**Practical consequence for this pass:** identify and decode the real wall-card texture **within one run**, correlating `D272U`'s `addr=` (confirmed CI8, confirmed actively drawn as a wall card in that same run) against that same run's `GE_TEXI[N]`/`texdump/rNNN_*.bin(.pal)` dump by address match, not by `seq`. Done: `GE_TEXDUMP=1 GE_TEXRAW=1 GE_D270KEEPOML=1049d8 GE_D272=1` in one combined pass (per the review's efficiency suggestion) → `GE_TEXI[11] addr=0x701587c8 fmt=2 siz=1 tile=32x32`, matching `D272U`'s live ci8-upload log for the same address in the same run → `texdump/r011_f2_s1_32x32.bin(.pal)` is unambiguously the real, currently-drawn wall-card texture.
**Decoding it exposed a bug in this session's own offline analysis, not a port bug.** First decode attempt (`struct.unpack('>256H', ...)` — big-endian) produced multi-hued rainbow noise under *both* candidate palette layouts (port's shifted decode and the standard ARGB1555 from D273) — which looked like a genuine new "wrong content" finding until traced back: `rdp.palette` is an in-process `uint16_t[256]` array, and `GE_TEXRAW`'s `.pal` dump (`fwrite(rdp.palette, 1, 512, pf)`) writes it as **raw native-endian bytes** — little-endian on x86-64, not the big-endian the ROM's own on-disk TLUT would be. Re-decoding with `'<256H'` (little-endian) immediately produced a coherent image under the port's existing default (shifted) decode: a smooth gray/brown diagonal gradient wedge, closely matching D269's original "soft vertical gradients" description. The **same ARGB1555 alternate that D273 already ruled out for the live scene** also decodes this specific real texture into a broken, speckled result (mostly-transparent with scattered noise along the edges) — a second, independent confirmation that D273's conclusion was correct and this session's earlier "content vs. decode" open question (from the D273/D236-sixth-pass HANDOFF entry) is now **resolved: there is no decode discrepancy.** The port's existing palette decode is correct for this texture; D269's "soft gradient" description was accurate.
**Mip/LOD state confirmed nominal for the real tile** (closes D272's owed next-step #2 for this specific texture): `D272U[4] ci8 upload 32x32 genmip=1` (mip generation *is* requested at upload) and `D272S ... addr=0x701587c8 ... cms=0 cmt=0 tex_lod=1` (LOD/mip sampling *is* enabled at draw time; `cms=cmt=0` = `G_TX_NOMIRROR|G_TX_WRAP` = `GL_REPEAT`, as expected for the ~4.5× wrap D272 measured). **Rules out "mips are simply missing/disabled" as the mechanism.** The remaining candidate narrows to *quality*, not *presence*: whether GL's automatic trilinear mip chain, sampling a small (32×32) diagonal-gradient tile repeated ~4.5× within a small screen-space quad, actually reproduces the RDP's explicit `TEXTLOD`/lod-frac blend closely enough — a smooth gradient wedge tiled many times, if under-blended at the wrap seams, is a plausible mechanism for a regular diagonal herringbone/houndstooth artifact (the wedge's own diagonal edge, repeated, is exactly that shape).
**Attempted, inconclusive: the two-class moiré A/B** the review also asked for (render with `GE_D270KEEPOML` isolating first the OPA class, then the XLU class, to see whether the weave is a single-class tiling artifact or an interference beat between the two overlapping classes). `GE_D270KEEPOML=112078` at the boot-camera frame 1500 produced a **fully black frame** — not just "wall cards missing," *nothing* drew, including geometry that should be unrelated to the wall (this needs its own triage: either the low-24 match genuinely excludes everything in view at this exact tick, or the keep-list interacts with something upstream of the wall class in a way that blanks the frame). Not investigated further this pass; flagged as owed, not blocking — the palette/mip findings above stand independent of it.
**Owed / next.**
1. **Debug the `GE_D270KEEPOML` all-black result** before relying on it — log which `oml` values actually cross the screen at the target tick (the existing unfiltered `GE_D266W` can do this) and confirm `112078`/`1049d8` are really present, then redo the class-isolation A/B.
2. **Mip-blend quality, not presence, is now the question.** Compare a forced-coarse-mip render (LOD bias, or temporarily forcing `GL_NEAREST_MIPMAP_NEAREST`) against the current trilinear result at the wall — if a coarser/blurrier mip visibly smooths the weave into something closer to the N64 reference's soft band, that confirms the mechanism and points at LOD selection/bias tuning as the fix; if it doesn't change materially, mips are a dead end and the remaining candidates are GL_REPEAT-wrap seam continuity across mip levels, or something in how the two overlapping (OPA+XLU) classes composite.
3. Regenerate `docs/dev/findings-index.csv` (`tools_pc/gen_findings_index.py`) — done this pass, but re-run again after any further findings.md edits.
4. **Process note, separate from the palette-decode-byte-order bug above**: `D270` (the skip/keep-oml hooks, the `D270P`/`D270KEEPOML`/`D270SKIPOML` machinery, and the `tdc` cap raise 400→600) has never had its own findings.md row — only code comments and HANDOFF-archive mentions. Same undocumented-lead class D273 flagged. A one-line backfill row is owed whenever someone next touches that thread; not done here to stay focused on D236. **Now backfilled as its own index row, see below.**
## D275 — M-133: probe upgrades for the eighth pass — `GE_D270PIX` extended from one point to a list, `GE_D270KEEPOML` gained a "kept N tris" counter (an external review's step 1/2 groundwork)
**Context.** A second external review of the D274 write-up proposed a four-step eighth pass: (1) multi-point pixel attribution + fog state inside the weave to name the actual front-most painter and close D269's never-done "log per-vertex fog" step; (2) an aligned class-isolation A/B with a visible "kept" counter so a black capture can be told apart from a genuine render bug; (3) an LOD-quality A/B; (4) stay alert to the framing risk that the confirmed wall-card content (a gradient wedge, D274) cannot by itself explain the N64 reference's discrete conifer silhouettes — a second, unattributed draw class is likely responsible for those, and D236 may split into two separate defects. This entry covers the mechanical probe work for steps 1–2; D276 covers what step 1 actually found.
**Changes (`port/fast3d/gfx_pc.cpp`, `#ifdef PORT`, both still env-gated/opt-in).**
- `GE_D270PIX="x0,y0[;x1,y1;...]"` (was single-point `"x,y"`): up to 8 normalized screen points, semicolon-separated. Each `D270P[ptN][...]` line now also carries `pix=(x,y)` (which point) and `addr=%p` (D274's reliable per-slot address, not `seq`).
- `GE_D270KEEPOML`: was silent. Now logs `D270KEEPOML: kept tri #N (oml24=0x...)` for the first 4 kept tris and every 5000th thereafter — a black capture with a nonzero kept-count is a genuine render-interaction bug; a black capture with `kept==0` at that tick is tick misalignment or an empty keep-set, not a probe failure.
**Offline groundwork before the live run:** picked 4 candidate weave pixels directly from `scratch/d273_base_1500.png` (the confirmed reproduction of the user's reported wall) by eyeball + sampled RGB to confirm they land in the gray weave band, not the snow floor or the black letterbox bars: `(0.05,0.10)`, `(0.25,0.15)`, `(0.85,0.12)`, `(0.50,0.06)` (the last one turned out to be sky, not wall — a miss, harmless, see D276).
**Attempted, still open:** the class-isolation A/B itself (review's step 2) was not completed this pass — D276's step-1 finding reframed the whole "which class is the wall" question first, which the review itself said should gate everything else ("step 1 first because it's the only step that can invalidate the current framing"). Re-run step 2 with the *correct* class identified in D276, not the D269-named OPA/XLU pair.
## D276 — M-134: **the true painter of the visible weave, at the confirmed weave pixels, is a fully-opaque IA8 blocky-noise/static texture (`oml=0xc81049d8` low bits, `fmt=3 siz=1`) sampled with a fixed, identical UV window and comparatively low fog — NOT either of D269's named CI8 classes, which don't even appear at two of the four sampled points.** D265's very first "all cards sample the same u:0..64/v:0..16 window" observation matches this class exactly; D269 reframed away from it prematurely.
**Method.** Ran the D275-extended `GE_D270PIX` at the 4 confirmed weave pixels with tick-aligned captures (`GE_PCDUMP=1300-1700:25`, 17 frames, all visually confirmed non-black/weave-consistent — ruling out tick misalignment as an explanation this time), logging every triangle covering each point across the whole 35 s run, then grouped by `oml` and summarized `z/w` and fog ranges per point offline.
**Findings.**
1. **At the two points confirmed inside the weave band** (`(0.05,0.10)`, `(0.25,0.15)`), **neither D269-named class (`0xc4112078` OPA, `0xc41049d8` XLU) appears at all.** The dominant, front-most (`z/w≈0.98–0.998`, excluding the sky quad's construction-mandated `z/w=0`), consistently-drawn class is `oml=0xc81049d8` (note: **low bits `0x1049d8` match the XLU class's low-24 bits, but the full oml/top-byte and — critically — `fmt=3` (IA) differ**; this is the same false-friend class D272/M-130 already had to filter out of its budget) with **average fog 39.5–63.3 out of 255** — mostly-unfogged, so its raw texel color reaches the screen largely unmodified.
2. **Decoded that exact texture within the same run** (`GE_TEXI[106] addr=0x70118d68 fmt=3 siz=1 tile=32x32` → `texdump/r106_f3_s1_32x32.bin`, IA8: high nibble intensity, low nibble alpha per `import_texture_ia8`): **a fully-opaque (alpha nibble mostly 0xf), high-frequency blocky gray static/noise pattern** — visually nothing like D274's gradient wedge, and nothing like a tree. This is almost certainly the same texture D267 already dumped and dismissed ("seq 113, 32×32, fully opaque, 1023/1024 texels at A=F, blocky noise pattern... none of these matches the horizon stripe band") — D267's dismissal was a single-instance visual judgement; it did not test what this content looks like *tiled*.
3. **Every single instance of this class, across every screen position sampled in an earlier unfiltered capture (`scratch/d274_run.log`), uses the byte-identical UV window** `u:0..64, v:0..16` (a 2× horizontal wrap of the 32-wide tile, sampling only its top half vertically) — **this is D265's original "all captured cards sampled the same u:0..64/v:0..16 window" observation, verified to the exact texel, and it belongs to this IA8 noise class, not the CI8 classes D269 and D272 spent three passes on.** D269's second-pass reframing away from D265's own lead was premature: it correctly proved the CI8 classes it found don't share one window, but never established those CI8 classes were the ones D265 was describing in the first place.
4. **A third class, `oml=0x0c184b50` (CI8, `fmt=2 siz=1`) — the "C-cards" the reviewing pass explicitly flagged as unattributed and a candidate for the N64 reference's actual discrete tree silhouettes — is present at every sampled point, with `z/w` ranging wildly from `-0.0381` to `0.9940`** (most other classes stay in a tight 0.96–1.00 band). Where it does land in the normal distant-geometry range, its fog is **high** (up to 255, i.e. fully fog-washed — plausible tree-fading-into-haze behavior) — but the erratic near-camera/behind-camera excursions suggest it is not reliably reaching the screen where it should, consistent with the review's flagged risk: a second, separate class may be responsible for the reference's actual tree shapes, and it may be missing or occluded rather than merely mis-filtered.
**Reframed conclusion.** The visible "wall/weave" the user reported is most directly explained by: a tiled, high-frequency, mostly-opaque IA8 noise texture (2× UV wrap, fixed window, low fog) reaching the screen essentially raw. D274's gradient-wedge CI8 texture was real and correctly analyzed, but is very likely a secondary/different layer, not the one producing the dominant visual artifact. The `0x0c184b50` CI8 class remains an open, unattributed thread that may be the reference's actual "trees" and may itself have a missing/occlusion bug — do not assume D236 has one root cause.
**Not yet done:** confirm this IA8 class's own UV-wrap-vs-fog behavior against the N64 reference directly (does N64 also wrap this noise 2× with low fog, or should the fog be much higher here, matching the `0c184b50` class's 193–255 range, which would wash it to near-invisible as intended); the class-isolation A/B (D275's still-owed step 2) should now isolate `0xc81049d8`/`fmt=3` vs. `0x0c184b50`, not the D269 CI8 pair. The mip-blend-quality experiment (D274's owed next-step, this pass's proposed step 3) is now lower priority — the newly-identified content (blocky noise, not a smooth gradient) makes a filtering-quality explanation less necessary than it seemed against D274's wedge; a wrong/insufficient fog blend is now at least as plausible a single-mechanism explanation and is cheaper to test (it's already logged, no new probe needed).
**Owed / next.**
1. **Fog-correctness check for the `0xc81049d8`/IA8 class**, now that D269's owed "log per-vertex fog" step is finally done: is `39.5–63.3` average fog the *correct* N64 value for this vertex position (compute what `fog_mul`/`fog_offset`/world-z *should* give and compare), or is it a computation bug specific to this draw path? If N64's own fog math would also produce a low value here, the wall is arguably N64-faithful in isolation and the real bug is elsewhere (e.g. the `0c184b50` occluder not painting over it); if the port's fog is wrong, that is a direct, cheap fix.
2. **Attribute and fix-scope the `0x0c184b50` CI8 class's erratic `z/w`** (`-0.0381` to `0.9940` at a single screen point) — likely candidate for a billboard/orientation bug (camera-facing quad computed with a bad or momentarily-invalid basis), separate from the noise-texture fog question. If this class is meant to draw the discrete tree shapes, understand why it doesn't reliably reach these pixels before concluding it's simply "the wrong layer."
3. Re-run the class-isolation A/B (D275) on the newly-identified classes, with the D275 kept-counter, at the D275-established tick-aligned window (`GE_PCDUMP=1300-1700:25`) so a black frame is diagnosable.
4. Regenerate `docs/dev/findings-index.csv` after this edit (done).
## D280 — M-135: **D236 wall, ninth pass — fog math VERIFIED N64-faithful (owed step #1 closed), the noise class VISUALLY confirmed as the weave's painter, and the "trees" question reframed: `0x0c184b50` is in FRONT of the noise quad (depth-race read inverted), its fog is CPU-baked bimodal 25/255, and it barely draws at all under deterministic `-level_36` boot — no quick win for trees; the remaining work is CPU-side instantiation/room-streaming, not a port fix**
1. **Fog math verified (D276 owed step #1, closed).** Computed the expected per-vertex fog from the game's own constants: RSP fog ramp 0..154 → `fmul=32000`, `foff=-31744`; the noise class's z/w ∈ [0.992, 1.0] maps to expected fog ≈ 26–28. Compared against the port's actual per-vertex fog for all **2505 vertices** of `0xc81049d8`: mean abs deviation ±2 (integer truncation only). D276's "low fog" is therefore the *correct* N64 value — on hardware this quad would also reach the screen largely unfogged. **Fog is not a bug; no port-side fix exists here.**
2. **A/B visual confirmation of the painter (D276 owed step #3, closed).** `GE_D278C=1` (collapse `0xc81049d8` to a point) + `GE_PCDUMP=1500-1500:1`: the weave band's high-frequency energy drops **14.8 → 2.9**, mean 108 → 121; the band becomes a smooth bright haze (the fully-fogged CI8 cards behind it). The pixel-attribution-only evidence from D276 is now corroborated visually — `0xc81049d8`/IA8 is definitively what paints the visible weave. **No trees appeared with it removed** — the first clean test of that question, unconfounded by the noise quad (all prior A/Bs, D268/D273, were behind it).
3. **Full class census at the confirmed weave pixels** (`(0.05,0.10)`, `(0.25,0.15)`): only three real classes reach either point — `0xc81049d8` fmt=3 IA8 (the noise painter), `0x0c184b50` fmt=2 CI8 (720 tris; fog bimodal 25/255, **gfog=0** → CPU-baked per-vertex fog), and `0xc8102078` fmt=2 CI8 (69 tris). Two further "classes" in the logs are probe artifacts: `0x00552048` (z/w=0, dl=1 placeholder) and `0x00504340` (z/w=-1, behind camera).
4. **Depth-race correction — D276's "occluder" framing was inverted.** N64 z/w: *smaller = closer*. At pt0, **278/356** `0x0c184b50` tris are CLOSER than the nearest noise tri (z/w < 0.9908); at pt1, 310/356. The tree cards are IN FRONT of the noise quad, not behind it. The open question is therefore "why don't the closer textured cards show?", not "does the noise win the depth race?".
5. **`0xc8102078` ruled out as a tree layer.** `-level_36` + `GE_D270KEEPOML=c8102078`: **1.4M triangles** fill the lower half of the screen (y ≥ 0.35) — terrain/ground, not conifers.
6. **The tree class barely draws under deterministic boot.** `-level_36` + `GE_D270KEEPOML=0c184b50` + `GE_TEXRAW`: ~3–4 tris/frame at tick 1500, zero visible pixels, empty `texdump/` (no CI8 upload). Meanwhile the non-`-level_36` fog run logged 720 covering tris. **`0x0c184b50`'s instantiation is strongly run-to-run dependent and `-level_36` does not pin it** — the room-streaming state that feeds this class varies even with the boot flag.
**Conclusion: no quick win for trees.** The visible weave is the IA8 noise class (N64-faithful fog, confirmed painter — arguably N64-faithful *in isolation*). The discrete conifers of the reference come from `0x0c184b50`, which (a) barely instantiates in deterministic runs and (b), when it does instantiate, sits in front of the noise yet still doesn't visibly win. Making trees appear requires understanding this class's CPU-side placement/instantiation (room streaming / billboard emission) — multi-step game-logic investigation, not a port-layer fix.
**Owed / next.**
1. Single instrumented run: `-level_36` + `GE_D270PIX` (both weave points) + `GE_TEXRAW` + `GE_PCDUMP=1400-1600:1`; correlate per tick — does a fog=25 `0x0c184b50` tri cover the point AND is the pixel dark?
2. Log `curRoom`/`roomsDrawn` (D271 infra) alongside `GE_D270PIX` to correlate `0x0c184b50` presence with room-streaming state.
3. Decode `0x0c184b50`'s CI8 texture offline — needs a run in which it actually uploads (TEXRAW has not captured it yet).