diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index bc0f3d2f..1a07d5f5 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -76,9 +76,9 @@ D217,FIXED,1P weapon viewmodel + third-person Bond model textures render garbled D218,PROPOSED,Wide FOV exposes the per-level draw-distance / fog boundary —,"Logged (M-84), fix proposed + parked." D219,RESOLVED,"PPK explosions render purple on Dam (M-87, user QA, ""not 3D but noting"").","RESOLVED (M-115, 2026-09-12) —…" D220,PARTIAL,"1P muzzle flash position shifts upward at higher Video.FovScale (M-87, user QA, Jungle/M16).","ANALYZED (M-91) — the suspected unscaled-viewmodel root does NOT hold; this is correct perspective behavior, likely cosmetic/QoL." -D85,OPEN,room primary/secondary DL → texLoadFromGdl garbage,"OPEN (safety-netted; widen/pool fixes landed, geometry now renders)" -D86 · D87,RESOLVED,modelInitRwData truncated ptr · attract-demo BE ramromfilestructure,resolved -D88.4,RESOLVED,propDefs polymorphic record stream not byteswapped → setupDoor crash,resolved (d88_propdefs.py); layout audit M-20 → D132 +D85,OPEN,Room primary/secondary DL → texLoadFromGdl garbage.,"OPEN (safety-netted; widen/pool fixes landed, geometry now renders)" +D86 · D87,RESOLVED,modelInitRwData truncated ptr · attract-demo BE ramromfilestructure.,resolved +D88.4,RESOLVED,propDefs polymorphic record stream not byteswapped → setupDoor crash.,resolved (d88_propdefs.py); layout audit M-20 → D132 D132,PROPOSED,D88 propDefs layout audit (M-20):,proposed fix (not applied) — D157,FIXED,"Campaign never unlocks the next level (M-30, user bug —","FIXED (src/bondtypes.h, #ifdef PORT —…" D160,PARTIAL,Dam level-end cutscene (Bond off the dam / bungee) never plays —,"DIAGNOSTIC SHIPPED, not root-caused (c95713f5, #ifdef PORT + GE_D160=1)…" @@ -87,8 +87,8 @@ D93-D102,RESOLVED,struct-pun / hardcoded-size pass:,resolved (D95 pool bump part D103-D107,RESOLVED,"BUNKER1 viewport height, depth-buffer clear (G_CLEAR_DEPTH_EXT), portal near-plane, LOD mip tile",resolved D108-D112,RESOLVED,skeletal models: d43_emit.py put_f32 byte-reversal bug,resolved D113-D116,FIXED,portal BFS ok · matrix chain ok · player raw-offset audit + gunfire.c THROW* · HUD text X-mirror,"D114/D116 CLOSED — NOT A BUG (M-33, see D168):" -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) +D117,OPEN,"Frame nondeterminism root-caused; GE_DETERM attempted, not achieved.","OPEN (mode attempted, not achieved)" +D118,RESOLVED,"SDL input layer; M-24 mouse-look rework (mode-aware map, real INI); D118b/c FIXED.",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" @@ -107,7 +107,7 @@ D272,OPEN,"D236 wall, fourth pass —","OPEN — probe GE_D272 added to port/fas 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). -D121,RESOLVED,WS1 frictionless per-level boot:,resolved +D121,RESOLVED,WS1 frictionless per-level boot (bare -level_XX injects its -m* row).,resolved D122,FIXED,per-level prop/item model-load crash (Dam/Facility/Runway modelLoad/modelInitRwData):,converter fixed; residual chr/fast3d crashes on those levels are separate D123,FIXED,crash class C1: chrIsNotDeadOrShot NULL deref on 6 levels (Dam/Runway/Frigate/Statue/Streets/Cradle).,"converter fixed (OBJ_ID_WORDS); C1 cleared on all 6, residual crashes are fast3d (C2)" D125,RESOLVED,crash classes C3+C6 (Aztec/Bunker2/Surface2):,resolved (M-16b) — line 374 → 8 NULs; all 21 sidecars regen'd; Aztec -level_28 now PASSES (was C3 CRASH). diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 554b5136..9b405ec7 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -473,10 +473,10 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). 3. **Fix (`src/game/image.c`, `#ifdef PORT`/`#else` macro, no `src/` logic changed):** added `PORT_PIXEL16`/`PORT_PIXEL32` — `__builtin_bswap16`/`32` under `#ifdef PORT`, identity under `#else` (so the N64 build is byte-for-byte unaffected, verified by inspection: the macro is a no-op expansion, not a code path change) — and wrapped every wide-pixel (`RGBA32`/`RGB24`/`RGBA16`/`IA16`/`RGB15`) store in the five functions listed above. CI/I4/I8/IA4/IA8 branches (single-byte, no byte order) are untouched. This is the D3x/AGENTS.md#2 narrow ABI/byte-layout exception class: mechanical, semantics-preserving (same numeric color, corrected byte order for the LE host), scoped to exactly the format cases that need it. 4. **Verified, numerically and visually:** `GE_TEXDUMP=1` PPM capture of the 15 `IMAGE_FIRE_N` textures post-fix — average RGB flipped from B-dominant (M-111: `fire000 (102,67,138)`) to R-dominant (post-fix `fire000 (141,127,53)`, `fire003 (147,81,24)`, `fire005 (47,10,2)`); a 10x-upscaled render of one frame and a 6-frame animation strip (frames 000-006, session scratchpad) show a clean, unambiguous fire-sprite animation — bright yellow-white core cooling through orange to red/black ash, exactly the expected in-game fire look, no purple/blue anywhere. Sanity-checked a montage of 12 ordinary (non-fire) world textures from the same Dam capture (clouds, hazard stripes, pipes, grating, concrete) — all render correctly, no regression from the format-gated change. `-level_09` (BUNKER1) and `-level_33` (Dam) both boot and render crash-free post-fix (10s+ each, no `ge007.crash.log`). **Not yet done:** a full 21-level `verify.sh sweep` and a genuine live human playtest confirming the fix in real (non-scripted) gameplay — the scripted-fire + texdump evidence here is strong but the row should stay open until a human eyeballs a real firefight on this build. `docs/porting-notes.md` gets a new entry for this bug class (runtime-decoded multi-byte pixel formats need explicit byte-order handling; static C-array textures already get this via D71/`gfx_tex_normalize_source`, runtime-decompressed ones did not). **M-114 — REGRESSION, reverted.** User playtest after M-113 landed reported 1P muzzle flash rendering blue and the ammo HUD counter rendering pink — colors inverted on textures that were fine before. Root cause: M-113's `PORT_PIXEL16`/`PORT_PIXEL32` swap is applied unconditionally to every call site in `texReadUncompressed`, `texChannelsToPixels`, `texInflateLookup`, and `texInflateLookupFromBuffer` — i.e. to every RGBA16/RGB15/IA16/RGBA32/RGB24 texture decoded through these shared functions, not just `IMAGE_FIRE_N`. This is the exact failure mode already logged above against the sibling function ("do NOT attempt a blanket R/B swap in `import_texture_rgba16` — it's shared by the whole game and would break every correctly-rendering RGBA16 texture"), just repeated one file over. Muzzle flash and the ammo HUD icon evidently decode through this same shared path without needing the swap. **Fix applied:** `PORT_PIXEL16`/`PORT_PIXEL32` reverted to identity (no-op) in `src/game/image.c`; the macro/call-site wrapping is left in place as scaffolding. **D219 is OPEN again** — the fire-particle color bug is unfixed. **Next, concrete, not yet done:** find the actual property that distinguishes `IMAGE_FIRE_N`'s decode from a working same-format sibling (M-112 already narrowed one candidate: FIRE is baked directly into a DL rather than going through the `sImageTableEntry` table that flare/ammo/impact sprites use — confirm whether that DL-embedded path is what needs the swap, or whether it's specifically `compmethod`, per M-112's still-open next step) and scope the macro to that property, not to a hardcoded texnum range. Not runtime-verified this session (static revert only, done to stop an active regression fast) — a human should confirm muzzle flash/ammo colors are back to normal on this build. **M-115 (2026-09-12, delegated session, 3 build/run cycles): fix RE-APPLIED, scoped by GBI pixel format — the M-114 regression resolved by construction, fire fixed again.** Per M-114's brief, found the property that distinguishes `IMAGE_FIRE_N`'s decode from working same-format siblings and scoped the macros to it. (1) **The discriminator is the decoded pixel FORMAT, not compmethod or texnum.** Full offline census of all 2698 ROM image-table entries (`scratch/d219_headers.py`, parsing real bitstream headers with the same MSB-first `texReadBits` contract as `src/game/image_bank.c`; zlib images skipped after the 2-byte `rz` preamble per `decompressdata`): only 94 non-zlib wide-pixel images exist — RGBA16=19, RGB15=14, IA16=1, RGBA32=60. The fire frames span compmethods RLE(4)/LOOKUP(5)/RLELOOKUP(7)/HUFFMANBLUR(8) (confirmed offline AND at runtime), so compmethod cannot scope the fix; a texnum range is a hardcoded landmine. Format IS structural: every 16-bit GBI-size format (RGBA16/RGB15/IA16) is consumed by importers that read the pool as **manually big-endian bytes** (`import_texture_rgba16`: `col16=(addr[0]<<8)|addr[1]`; `import_texture_ia16`: `intensity=addr[0], alpha=addr[1]`), while every 32-bit format (RGBA32/RGB24) is consumed by `import_texture_rgba32`, which does `PD_BE32()` on a **native** u32 load — it expects host-order words. So bswap-at-store is required for exactly the 16-bit family and must be identity for the 32-bit family; M-113's blanket swap hit both (M-114), M-114's full revert broke the 16-bit family again. (2) **Change (`src/game/image.c`, `#ifdef PORT` only, no call-site edits):** `PORT_PIXEL16(x)` = `__builtin_bswap16((unsigned short)(x))`; `PORT_PIXEL32(x)` = `(x)`. Every existing call site already routes by `TEXFORMAT_*` to the right macro, so M-114's scaffolding was already format-scoped per site — only the macro definitions were wrong (both bswapped). 30-line comment at the definition records the importer contract + census. (3) **Runtime verification (Dam `-level_33`, scripted Z-pulse fire via `GE_INPUTSCRIPT`, `GE_D219RAW=1` + `GE_TEXDUMP`/`GE_TEXRAW`):** all 15 fire frames decode at runtime as format=1 (RGBA16) with compmethods 4/5/7/8 exactly matching the offline census (texnum 2085 is genuinely an nc=1 LOOKUP transparent first frame — A=0 everywhere; the visible frames are 2087–2113); the raw pool dumps decode as warm RGBA5551 sprites — mean RGB over opaque texels **(255,238,139)**, R-dominant, correct vertical-blob shape (rows 3–10 of the 16×14 tile); e.g. texnum 2111's pool word is now `0xc2f1`, which the importer reads back as `0xf1c2` = R30,G3,B1 — the true decoded value M-113 recorded pre-fix, i.e. exactly the pool state whose rendered PPMs M-113 proved correct (mean 141,127,53, clean animation). (4) **Regression safety is structural, not empirical:** RGBA32/RGB24 pool bytes are bit-identical to the M-114-revert build (`PORT_PIXEL32` = identity in both), so muzzle flash / ammo HUD / flares / crosshairs — all RGBA32 — render exactly as in the state the user last confirmed fine; no 32-bit path was touched at all. The other non-zlib 16-bit images (texnum 1198–1201 RGBA16, 2430 IA16, 2510–2523 RGB15) were equally byte-reversed before this change and are now corrected by the same importer contract; they do not load during a Dam run, so their on-screen state is owed to the next human playtest in whatever level uses them. (5) **Probes left in tree** (env-gated, inert): `GE_D219RAW` generalized in `texInflateNonZlib` — header line for every non-zlib wide-pixel decode (`format <= TEXFORMAT_IA16`), first-row pixel dump for RGBA16/RGB15/IA16, no texnum filter; plus the pre-existing D172/D219 and `GE_TEXDUMP` fire-dumper probes. **Status: RESOLVED (2026-09-12)** — user playtested this build and confirmed it works (fire correct, no muzzle-flash/ammo-HUD regression); D219 closed. | | D220 | **1P muzzle flash position shifts upward at higher `Video.FovScale` (M-87, user QA, Jungle/M16).** — full `## D220` entry at file tail | **ANALYZED (M-91) — the suspected unscaled-viewmodel root does NOT hold; this is correct perspective behavior, likely cosmetic/QoL.** The 1P hand + flash are model nodes in ONE gun DL (`gunfire.c` builds `rwmtx[1..4]` for hammer/flash/billboard nodes) drawn through the single per-frame projection from `viSetupCurrentPlayerView` —… | -| D85 | room primary/secondary DL → `texLoadFromGdl` garbage | OPEN (safety-netted; widen/pool fixes landed, geometry now renders) | -| D86 · D87 | modelInitRwData truncated ptr · attract-demo BE `ramromfilestructure` | resolved | +| D85 | **Room primary/secondary DL → `texLoadFromGdl` garbage.** — see §H (D85 entries: root cause + widen/texture-pool fixes) | OPEN (safety-netted; widen/pool fixes landed, geometry now renders) | +| D86 · D87 | **modelInitRwData truncated ptr · attract-demo BE `ramromfilestructure`.** — see §H (D86, D87) | resolved | | D88.1–D88.3 · D88.5–D88.6 | `Usetup*Z` header + sub-table width/endian conversion (`d88_emit.py`) | resolved | -| D88.4 | `propDefs` polymorphic record stream not byteswapped → `setupDoor` crash | resolved (`d88_propdefs.py`); layout audit M-20 → **D132** | +| D88.4 | **`propDefs` polymorphic record stream not byteswapped → `setupDoor` crash.** — see §H (D88.4) + `## D132` | resolved (`d88_propdefs.py`); layout audit M-20 → **D132** | | D132 | D88 propDefs layout audit (M-20): converter cursor confirmed vs real PC struct layout for all types the 21 levels emit. DOOR/OBJECT-prefix/VEHICHLE/AIRCRAFT/TANK/AUTOGUN/AMMO/TINTED_GLASS/objective sub-records all MATCH. **Divergence found:** types 14 LINK / 19 SWITCH / 38 LOCK_DOOR / 44 SAFE_ITEM — each has `s32 IndexN` fields sharing a union with a pointer (`LinkRecord.first`/`Index1`), so on PC the field sits in an 8-byte, 8-aligned union slot, but the converter emits it at N64 tight-4-byte-word offsets → `pdef->Index1` reads `Index2`'s value, `Index2` reads 0 → switch-doors / dual-weapons / locked-doors / safes silently fail their validity guard (non-crash; guard failure also prevents the under-sized-record `->next` overflow). | proposed fix (not applied) — see below | | D157 | **Campaign never unlocks the next level (M-30, user bug — "complete Dam on Agent, Facility stays locked, no save").** — full `## D157` entry at file tail | FIXED (`src/bondtypes.h`, `#ifdef PORT` —… | | D160 | **Dam level-end cutscene (Bond off the dam / bungee) never plays — cuts straight to the MISSION COMPLETE report (M-30/M-31, user bug D148).** — full `## D160` entry at file tail | DIAGNOSTIC SHIPPED, not root-caused (`c95713f5`, `#ifdef PORT` + `GE_D160=1`)… | @@ -486,8 +486,8 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D103–D107 | BUNKER1 viewport height, depth-buffer clear (`G_CLEAR_DEPTH_EXT`), portal near-plane, LOD mip tile | resolved | | D108–D112 | skeletal models: `d43_emit.py put_f32` byte-reversal bug | resolved | | D113–D116 | portal BFS ok · matrix chain ok · player raw-offset audit + `gunfire.c` THROW* · HUD text X-mirror | **D114/D116 CLOSED — NOT A BUG (M-33, see D168): the "X-mirror" was an upside-down `GE_PCDUMP` capture misread as mirrored. PPM writer fixed; the game renders correctly on hardware.** | -| 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) | +| D117 | **Frame nondeterminism root-caused; `GE_DETERM` attempted, not achieved.** — see §H (D117 M-8 root cause + M-52 addenda) | OPEN (mode attempted, not achieved) | +| D118 | **SDL input layer; M-24 mouse-look rework (mode-aware map, real INI); D118b/c FIXED.** — see §H (D118 + M-24 entries) | 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 | @@ -505,8 +505,8 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | 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:** — full `## D137` entry at file tail | 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. | -| D121 | WS1 frictionless per-level boot: bare `-level_XX` injects its `memallocstringtable[]` `-m*` row (`#ifdef PORT` in `boss.c`) | resolved | +| D120 | **Blood-decal `PointUsage[]` chain — converter now emits it (M-30).** — full `## D120` entry at file tail | FIXED (`tools_pc/d43_emit.py`). Interactive verify pending — blood decals on a BUNKER1 firefight should now appear + no hang. | +| D121 | **WS1 frictionless per-level boot (bare `-level_XX` injects its `-m*` row).** — see §H (D121) | resolved | | D122 | per-level prop/item model-load crash (Dam/Facility/Runway `modelLoad`/`modelInitRwData`): `d88_propdefs.py` had no handler for 6 ObjectRecord-derived propDef types (47/39/40/45/13/20) → generic arm half-swapped the `[s16 obj][s16 pad]` word → OOB `PitemZ_entries[]` | converter fixed; residual chr/fast3d crashes on those levels are separate | | D123 | **crash class C1: `chrIsNotDeadOrShot` NULL deref on 6 levels (Dam/Runway/Frigate/Statue/Streets/Cradle).** — full `## D123` entry at file tail | converter fixed (`OBJ_ID_WORDS`); C1 cleared on all 6, residual crashes are fast3d (C2) | | D125 | **crash classes C3+C6 (Aztec/Bunker2/Surface2):** — full `## D125` entry at file tail | **resolved (M-16b)** — line 374 → 8 NULs; all 21 sidecars regen'd; **Aztec `-level_28` now PASSES** (was C3 CRASH). Bunker2 falls through to a separate DOOR-tail `linkedDoor` layout bug (C3 residual) | @@ -10931,3 +10931,9 @@ crash class C2 (Facility `-level_34`, Runway `-level_35`): `import_texture_i8` A **Status (from §F table):** **VISUALLY VERIFIED FIXED (M-108) — mechanism still unexplained.** User confirmed on a live Dam playthrough (post-D233 fix, correct `-level_26`/Frigate session's build) that all guard towers along the dam now render, not just the first. No code change was made specifically for D234, and the M-107 investigation's own reasoning argued D233's mechanism doesn't fit this symptom (towers viewed from a distance, not a close-camera w<0 case) — so either that reasoning was wrong, D233's fix had a broader effect than analyzed, or the original symptom was intermittent/state-dependent rather than deterministic. Not closed with high confidence in *why* it's fixed, just closed on live confirmation that it *is*. If it resurfaces, re-open with a fresh `GE_PCDUMP` capture rather than assuming this write-up's ruled-out list still holds. > Earlier triage pass, merged 2026-09-15 (ctx-opt Bucket B): "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`." — status then: VISUALLY VERIFIED FIXED (M-108), mechanism unexplained. + +## D120 — Blood-decal `PointUsage[]` chain — converter now emits it (M-30). + +**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). + +**Status (from §F table):** FIXED (`tools_pc/d43_emit.py`). Interactive verify pending — blood decals on a BUNKER1 firefight should now appear + no hang.