docs: v0.2.1 known-issues sweep, pre-1.0/roadmap framing, AI disclosure

Batched pass across README/release-notes/Pages (one commit instead of the
scattered per-fix pushes earlier this session, per new commit-cadence
preference):

- New known issues surfaced that weren't documented anywhere: no real
  widescreen (16:9 stretches 4:3 rather than expanding FOV), no controller
  rebinding UI / macOS-ARM builds, culling/LOD dropout on Streets/Egyptian
  (D249), gunshot SFX oddities under sustained fire (D240/D241).
- Fixed a stale claim (in three places) that a wrong-sounding-instrument bug
  was still open; it was fixed in v0.2.0 (D230).
- Softened the D259 "All unlocked" fix to reflect reality: still breaks
  audio/aim if enabled before any save exists; flagged experimental
  (new finding D281).
- Corrected D255's repro to the precise, deterministic trigger found this
  session (Facility's door-opening computer terminals, Linux-wide not
  Deck-specific) instead of the vaguer original combat-crash description.
- Logged D282 (front-end menus need right stick not left on gamepad) and
  D283 (Steam Deck first-run preset skipped by an early Desktop Mode
  launch) with user-facing workarounds.
- Refined D263 (Ourumov/Trevelyan) internally with a position-sensitivity
  caveat; no public bullet since it doesn't block progression.
- Added pre-1.0 framing and a new README Roadmap section (known-issues
  cleanup, PAL/JP, real widescreen, rebinding, macOS/ARM, LAN multiplayer
  explicitly "under consideration, no ETA", plus an explicit "not currently
  planned" list) across README/release-notes/Pages.
- Added an AI-development disclosure (agentic development as a genuine
  study of the process, not just a port) across the same three docs.
- Pinned the release-notes hero GIF to the v0.2.1 tag instead of v0.2.0.

Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
This commit is contained in:
James Dansereau committed 2026-09-15 12:53:57 -04:00
1 parent 4b51ded7d1
commit e7fee0d7a4
5 files changed
+172 -44

No files matched your search

+5 -2
View File
@@ -96,6 +96,9 @@ D270,?,Backfilled (M-132) — this label was never given its own row; only code
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
D281,OPEN,"Game.AllUnlocked (D257) still breaks audio, and can also break right-mouse aim, when enabled before any save has ever been written (v0.2.1 user testing, 2026-0…","OPEN — user-reported regression, not root-caused; mitigation is user-facing guidance only (release notes:"
D282,OPEN,"QoL ask (v0.2.1 user testing, 2026-09-15):","OPEN — not designed; QoL, port-only."
D283,OPEN,"Steam Deck first-run preset (D-unnumbered in port/src/video.c/main.c, videoApplySteamOSDefaults()) did not apply for a user who added the game via Steam in Des…","OPEN — mechanism understood, root cause (Desktop-vs-Game-Mode STEAMOS env var difference) unverified on real hardware; user-facing workaround documented in README/release notes."
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"
@@ -147,7 +150,7 @@ D251,FIXED,F10 options overlay QoL pass:,"FIXED (port-only, port/src/optionsover
D252,?,"Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).",
D253,FIXED,Facility (and any level with a FORCECROUCH/LADDER linked special tile) SIGSEGV on Linux/Steam Deck —,FIXED — Deck-verified:
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."
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); reliable deterministic repro is Facility's door-opening computer terminals on activation (Linux/Steam Deck); earlie…
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,FIXED,Game.AllUnlocked (D257) breaks audio on a fresh save:,FIXED in v0.2.1 — the deferred fix was applied:
@@ -155,6 +158,6 @@ D260,FIXED,v0.2.1 (user request):,"FIXED — build green; one-line branch swap,
D261,FIXED,v0.2.1 (user request:,FIXED — feel-check received; W/S sensitivity follow-up logged above.
D262,FIXED,v0.2.1 (user report):,"FIXED (primary cause, D264) —"
D264,FIXED,"D262's root cause (ABI/layout class, cf.","FIXED — build green, probes removed; visual confirmation pending."
D263,RESOLVED,v0.2.1 (user report):,RESOLVED (not a bug) —
D263,RESOLVED,v0.2.1 (user report):,"RESOLVED (not a bug), but refined (v0.2.1 further user testing, 2026-09-15) —"
D265,?,"D236 investigation (port-layer, port/fast3d/gfx_pc.cpp):",PARKED — reverted to baseline; evidence + suspect list above.
D258,OPEN,PAL/JP asset conversion is broken at the source-data level —,"OPEN — repair path verified, not applied (out of v0.2.0 scope)."
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
96 D271
97 D276
98 D279
99 D281
100 D282
101 D283
102 D280
103 D275
104 D274
150 D252
151 D253
152 D254
153 D255
154 D256
155 D257
156 D259
158 D261
159 D262
160 D264
161 D263
162 D265
163 D258
+5 -2
View File
@@ -484,6 +484,9 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| 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 |
| D281 | **`Game.AllUnlocked` (D257) still breaks audio, and can also break right-mouse aim, when enabled before any save has ever been written (v0.2.1 user testing, 2026-09-15) — a distinct regression from D259, which only fixed the all-zero-slot volume case.** User's empirical workaround: complete a level normally first (e.g. Dam on Agent, which writes a real save), THEN enable `All unlocked`; enabling it on a completely fresh install (no save yet) still reproduces both symptoms. Not root-caused this session — D259's `geEepromPatchAllCheats` all-zero-slot seeding was verified against exactly the "fresh eep" case, so either that fix doesn't cover every code path a truly-first-launch toggle can hit (e.g. a race between the F10 toggle taking effect and the very first save-slot creation/read), or there's a second, independent mechanism. The right-mouse-aim symptom is new information with no prior lead — cross-ref D211/D194 (aim-mode sensitivity/mode family) for where a save-state-dependent aim regression could originate. | OPEN — user-reported regression, not root-caused; mitigation is user-facing guidance only (release notes: don't enable until a save exists, treat as experimental). |
| D282 | **QoL ask (v0.2.1 user testing, 2026-09-15): front-end menu navigation (main menu, file select, mission-select map) currently requires the RIGHT analog stick — matches N64 default control schemes but is unintuitive on a modern controller/Steam Deck, where the left stick is the expected primary-navigation input.** Distinct from the F10 options overlay, which is already left-stick/D-pad navigable (`port/src/optionsoverlay.c`). Scope: front-end menu input in `src/game/front.c` (or wherever N64 stick reads are gated by the active `controldef`) — likely wants a port-layer input remap for these menu contexts specifically, not a general control-scheme change. | OPEN — not designed; QoL, port-only. |
| D283 | **Steam Deck first-run preset (D-unnumbered in `port/src/video.c`/`main.c`, `videoApplySteamOSDefaults()`) did not apply for a user who added the game via Steam in Desktop Mode and switched to Game Mode afterward — resolution stayed at some non-native value instead of the Deck's native 1280×800 (v0.2.1 user testing, 2026-09-15).** Root mechanism, confirmed by code inspection: the preset only applies when `getenv("STEAMOS")` is truthy AND no `ge007.ini` exists yet (`main.c:126-129`, before `configLoad()`); once any ini exists, it always wins over the preset on every future launch (by design, so user F10 changes persist). **Leading hypothesis (not verified on real hardware in this environment):** SteamOS's Desktop Mode (a normal Plasma/X11 session) likely does not export the same `STEAMOS` env var that the Game Mode/gamescope session does, so a Desktop Mode test-launch (a natural step when adding a non-Steam game via its file browser) creates `ge007.ini` with ordinary Linux defaults *before* the user ever reaches Game Mode — permanently skipping the preset for that install, since the ini already exists on the first real Game Mode launch. This exact caveat was already known internally (`docs/HANDOFF.md`: "a Deck that already has an ini needs `-fresh` or F10 to pick the preset up") but was never surfaced in user-facing docs (README/release notes) — same class of gap as the stale D230 release-notes reference. **Workaround (lighter than `-fresh`, which also wipes the save):** F10 → Resolution → 1280×800 manually; takes seconds, no data lost. | OPEN — mechanism understood, root cause (Desktop-vs-Game-Mode `STEAMOS` env var difference) unverified on real hardware; user-facing workaround documented in README/release notes. |
| 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 |
@@ -543,7 +546,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D252 | **Particle effects cycle through a rainbow palette (M-124, user playtest of the v0.2.0-pre candidate).** Shooting repeatedly makes the bullet-impact "sparks" slowly progress through the full hue wheel (~10 shots to sweep it); other lingering particle effects — smoke / explosion residue — show the same rainbow colouration instead of their intended grey/orange palette. **Mechanism context (verified this session):** particle tris carry no per-primitive material; their colour comes entirely from the ROM-resident state records `globalDL_0x078 … globalDL_0xa50` (`assets/oddtextures.c`, 15 of them) replayed per particle batch (`explosion.c:894`). Each record binds a **distinct** texture pair — `IMAGE_SMOKE_N` (IA, tile 0 @ TMEM 0) × `IMAGE_FIRE_N` (RGBA16, tile 1 @ TMEM 0x188) under `G_CC_INTERFERENCE` (TEXEL0·TEXEL1), and `explosionRenderPart` uses a fixed all-white default vertex (`explosion.c:215`) — so the intended per-stage colour is whatever those 15 fire/smoke pairs encode, and particles **age through the records** (`var_s2` 14→0 at `frame/flareanimspeed`), so a per-texture decode error surfaces exactly as a hue sweep over time / across shots. **Ruled out this session:** (a) the M-82/D172 tile-offset fix is correctly configured for this path — `gsDPSetTextureLOD(G_TL_TILE)` does NOT set the `G_TL_LOD` bit, so `rdp.tex_lod` stays false (`gfx_pc.cpp:2860`) and `gfx_lod_tile_offset` returns `i`, i.e. TEXEL1 genuinely samples tile 1; (b) the RGBA16/5551 texel decode in `import_texture_rgba16` is correct (big-endian, R=bits15-11 G=10-6 B=5-1 A=bit0). **Prime suspect: the width/height derivation in `import_texture_rgba16`** — `width = rdp.texture_tile[tile].line_size_bytes / 2`, `height = size_bytes / line_size_bytes`. The fire tiles are 16 px wide with a MASK_16 TMEM line (64 bytes), so `width` computes 32 (true: 16) and, with `size_bytes` = the LOADBLOCK's 16×14×2 = 448, `height` computes 7 (true: 14) — the GL texture would be built from the right pixels at the wrong dimensions, scrambling each of the 15 fire sprites into a different wrong-hued smear; that fits "slow rainbow progression" precisely (each aging stage = a different scrambled sprite). **Not yet confirmed** — needs one imported fire tile dumped and compared against the raw `IMAGE_FIRE_N` bytes (`GE_TEXDUMP`/`GE_PCDUMP`, or a targeted probe at the `upload_texture` call). **Per user instruction: documented only, NOT fixed for v0.2.0-pre** — ships as a known issue; pick up afterwards (cheap if the suspect holds: derive width/height from the LOADBLOCK's dtx/dlh rather than the tile line size, or special-case non-32-texel-wide RGBA16 tiles). D172/M-82 lineage. |
| D253 | **Facility (and any level with a FORCECROUCH/LADDER linked special tile) SIGSEGV on Linux/Steam Deck — `stanCheckLinkedSpecialTile` stores to a garbage `outFlags` pointer (D203 root cause).** The B-callback typedef `standTileLocusCallback_B_t` (`bondtypes.h:573`) declares the middle three params as `f32 f32 f32`, so on x86-64 SysV the caller (`sub_GAME_7F0B1DDC`, `stan.c:2228`) passes the 6th arg (`record`) in **`%rdx`** (3rd integer slot; the floats take xmm0–2). But the actual implementation `stanCheckLinkedSpecialTile` was decompiled with an all-`s32` prototype, so it reads `outFlags` from **`%r9`** (6th integer slot) — a stale scratch value left by earlier callees. On the N64 (MIPS o32) both prototypes agree: everything flows through GPRs a0–a5 in order, 6th arg = a5 either way, so the original is fine and the decompilation looked consistent. The mismatch stays dormant until a tile actually hits the FORCECROUCH or LADDER branch and *stores* through `outFlags` → SEGV; that's why it was level-specific (Facility's opening crouch tile) and input-dependent, and why idle repros were negative. The sibling B-callback `stanLocusEdgeIsAboveY` already had the correct float prototype, which is why only this path was booby-trapped. **Fix (ABI-class, semantics-preserving):** declare the three middle params of `stanCheckLinkedSpecialTile` as `f32` in both the forward decl and definition (`stan.c:152`, `stan.c:2407`) to match the typedef; the body never uses them, and on N64 o32 f32 args occupy the same GPRs, so N64 codegen is unaffected. **Verified:** gdb run on WSL (gcc 15) — pre-fix, SIGSEGV at `stan.c:2441` with `r9=0x3` and valid `record=0x409fea00` in the caller frame; post-fix, the same walk route completes with no crash. Side benefit: the FORCECROUCH/LADDER branches were effectively dead-on-arrival on PC (any hit = crash); they now function. **Also observed post-fix (separate, WSLg-only):** a segfault *inside libgallium* during `gfx_opengl_upload_texture` (32×48 RGBA + mipmaps) while walking further — GL-driver-side under WSLg's Mesa; not reproducible on Windows; park as a new finding if it recurs on real hardware. | FIXED — Deck-verified: user got past the crouch tile with the re-cut bundle (a *new*, later crash is D255). |
| D254 | **Linux crash handler destroyed its own logs: `ge007.crash.log` only ever contained the SIGABRT block, never the original SEGV.** The POSIX `crashHandler` (`port/src/crash.c`) finished by calling `sysFatalError()` → `abort()` → SIGABRT, which re-entered the *same* handler (SIGABRT was one of the installed signals); the second pass reopened the log with `"wb"` and overwrote the first pass's SEGV block — faulting PC, signal, everything. The Deck firefight crash (D255) came back as a bare `SIGNAL: 6` abort record for exactly this reason, forcing offline disassembly of the CI binary to recover what the log should have said. **Fix (port-only):** re-entrancy guard (`inCrashHandler`, second pass `_exit(128+sig)` immediately) + terminate directly with `_exit()` instead of `sysFatalError()` (the Windows path already terminated directly for the same reason) + the Linux block now dumps the faulting context: `FAULT ADDR` (`si_addr`) and the full general register set (Rax/Rcx/Rdx/Rsi/Rdi/R8–R11, Rsp/Rbp) from the `ucontext`, so a field crash now names the bad pointer on its face. **Field-verified 2026-09-13:** the D255 Deck crash came back as `SIGNAL: 11` + `FAULT ADDR: 0x10` + full register set — exactly the format designed here; it directly identified the NULL-model case (see D255). | FIXED — field-verified. |
| 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. |
| 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. **Corrected, deterministic repro (v0.2.1 user testing, 2026-09-15, superseding the initial report): the trigger is Facility's computer terminals — the ones you ACTIVATE (interact with, not shoot/destroy) to open a door for level progression.** User confirms this is consistent/reliably reproducible on those specific terminals, on Steam Deck **and** Linux generally (not Deck-hardware-specific). This is a much stronger lead than the original "sometimes during firefights" — a terminal activation deactivating/freeing its associated object model while the record briefly stays active this tick is fully consistent with the destroy-path-ordering theory above, just via a scripted trigger instead of combat destruction, and gives a deterministic repro instead of a probabilistic one. **Next session should reproduce via terminal activation specifically**, not generic combat — much cheaper to instrument. | OPEN — case identified (NULL model on an active OBJ record; destroy-path ordering); reliable deterministic repro is Facility's door-opening computer terminals on activation (Linux/Steam Deck); earlier combat-related reports (Bunker, Frigate) may be the same mechanism via a different trigger; 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. | **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). |
@@ -551,7 +554,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D261 | **v0.2.1 (user request: "fix watch menu sensitivity for wasd/controller buttons"): holding W/S (or a fully-pressed stick axis) on the watch inventory page 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. |
| 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), but refined (v0.2.1 further user testing, 2026-09-15) — beat verified firing end-to-end on PC incl. execution shot; **user playtest confirmed the sequence plays normally.** **Refinement: the exact trigger behavior is significantly sensitive to Bond's precise location and does not fire 1:1 identically to the N64 original across different approach paths/positions** — consistent with the beat's own documented sensitivity (range/facing/LOS/same-stand-tile + a ≈1/49-per-tick random gate, see the mapped mechanism above), so this is likely the same finicky-trigger behavior rather than a new bug, but not confirmed byte-identical to N64 for every approach. **User confirms this does NOT block level progression** either way — logged for awareness, not tracked as an open defect. 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). |