diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 878938b2..c62be64f 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -11140,6 +11140,34 @@ if that's recorded anywhere in the D280 write-up above. **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. **M-141 update: the width/height suspect is REFUTED by the authored asset data, not just re-tested — the prior "prime suspect" reasoning assumed `line_size_bytes` derives from the tile's `MASK_16` wrap period (64 bytes ⇒ 32 texels), but `assets/oddtextures.c`'s actual `gsDPSetTile` for the RGBA16 **render** tile (tile 1, e.g. `globalDL_0x078:50`) sets `line=4` explicitly (independent of the mask), giving `line_size_bytes = 4*8 = 32` bytes ⇒ `width = 32/2 = 16` texels — and `size_bytes` from the paired `gsDPLoadBlock(...CALC_LRS(16,14,G_IM_SIZ_16b)...)` is 16×14×2 = 448 bytes ⇒ `height = 448/32 = 14`. `import_texture_rgba16`'s existing `width = line_size_bytes/2` / `height = size_bytes/line_size_bytes` formula therefore computes the CORRECT 16×14 for every fire tile, cross-confirmed by the pre-existing `is_fire_shape = (width==16 && height==14)` filter already hardcoded into `gfx_opengl.cpp`'s `GE_TEXDUMP` dumper (D219-era) — that filter only makes sense if 16×14 is what the importer actually produces at runtime. **Do not chase width/height again.** Also checked and rejected: the render tile's `gsDPSetTileSize(1,...,CALC_TILESIZE(56),CALC_TILESIZE(56))` sets a 56×56 texel-coordinate WINDOW for UV-wrapping the 16×14 content against the co-rendered 56×56 smoke texture (`G_CC_INTERFERENCE`, TEXEL0=smoke/TEXEL1=fire) — this is not a usable content-dimension source and switching to it (the naive "use SETTILESIZE instead of line_size_bytes" fix) would have made the bug worse, not better. **GEVR-TRIAGE item E (byte-group-order hypothesis) also does not apply new information** — the original session's decode-order check (still correct: `addr[2i]<<8|addr[2i+1]`, matches the already-fixed D228 IA16 sibling) stands; no runtime capture this session found a byte-order anomaly either (see below). **Root cause is now genuinely open, not "unconfirmed suspect"** — both named hypotheses are dead. Next investigator: the TMEM slot the fire render tile always loads into (`tmem=0x188`/392, `rdp.loaded_texture[]` indexed by tmem, hard-coded identically across all 15 `globalDL_0x078..0x9a8` records) is a plausible next avenue — 15 *different* ROM source images share one fixed TMEM address and one GL-cache key derived from `{texture_addr, fmt, siz, size_bytes, ...}` (`gfx_pc.cpp:~1962`); worth checking whether rapid record-to-record aging ever reuses a stale GL texture object keyed wrong, though this is an unverified new lead, not a confirmed mechanism. **Not runtime-confirmed this session** — added an env-gated diagnostic (`GE_D252=1` in `import_texture_rgba16`, `port/fast3d/gfx_pc.cpp`) that logs computed vs. SETTILESIZE dimensions and dumps raw texel bytes per distinct upload to `scratch/d252_raw_*.bin`, but two 35s/90s headless `-level_37` (Jungle) runs never triggered an explosion/fire draw (no player input available in this environment) — needs an interactive session or a longer/different-level run to actually fire the probe. **User-confirmed live, same session: still reproduces on plain bullet-impact sparks**, not just explosions — this is a much cheaper repro than an explosion (any gunshot against a wall/surface) and should be the target for the next `GE_D252=1` capture, not a scripted explosion. +**M-156 (2026-09-16, this session): the `GE_D252` probe finally fired, and the decode is now proven correct by direct byte inspection — rule out asset/decode entirely, the bug is confirmed downstream.** Reused the D219/M-90 scripted-fire technique (`GE_INPUTSCRIPT` with `Z` pulses, `Z`=`GE_CONT_G`=fire) on a bare `-level_33` (Dam) boot facing spawn geometry, 40 pulses over 30s — got **14 distinct fire-stage textures dumped** to `scratch/d252_raw_*.bin` (the probe dedupes by `(addr, size_bytes)`, so 14 unique ROM source addresses fired, each `tile=1 size_bytes=448 computed(w=16 h=14) settilesize(w=56 h=56) tmem=392`, matching M-141's already-established correct computed dimensions). + +**Decoded all 14 raw dumps by hand** (Python, RGBA5551 big-endian: `r=(val>>11)&0x1F, g=(val>>6)&0x1F, b=(val>>1)&0x1F, a=val&1`, matching `import_texture_rgba16`'s own documented decode) and averaged the opaque (`a=1`) texels per stage: + +``` +stage n_opaque avg_rgb (0-255) + 0 39 (248, 245, 155) pale yellow-white (hot) + 1 54 (248, 234, 140) + 2 65 (247, 227, 124) + 3 69 (246, 220, 105) + 4 81 (244, 192, 73) + 5 83 (235, 148, 54) + 6 84 (232, 134, 49) + 7 85 (234, 142, 53) + 8 86 (225, 125, 46) + 9 78 (191, 81, 28) + 10 61 (135, 48, 15) + 11 20 (89, 22, 4) dark red, embers + 12 0 (fully transparent) + 13 0 (fully transparent) +``` + +This is a **perfect, smooth white-hot → yellow → orange → deep-red → fade-out fire progression** — exactly the intended palette, zero rainbow/off-hue content anywhere in the source data. This is the first time this session's (or any prior session's) D252 investigation has actually decoded and eyeballed the numeric pixel values rather than reasoning about dimensions/formulas — and it settles the question definitively: **the ROM asset data is correct, the byte-order/width/height decode is correct (independently reconfirms M-141), and the ordering of stages is a correct hot→cold sequence.** Whatever produces the on-screen rainbow effect is **entirely downstream of this point** — GL upload (`upload_texture`), the texture cache (`gfx_texture_cache_lookup`, `port/fast3d/gfx_pc.h`'s `TextureCacheKey`), or the `G_CC_INTERFERENCE` combiner/shader path — not the source data, not `import_texture_rgba16`'s math. + +**Checked the texture-cache-key collision lead from M-141 and it does NOT hold as stated:** `TextureCacheKey` is `{texture_addr, palette_addrs, fmt, siz, palette_index, size_bytes, palette_hash}` (`gfx_pc.h:23-42`) — all 14 fire stages captured this session have **distinct `texture_addr` values** (14 different ROM addresses, confirmed in the probe log), so a same-address stale-cache-hit cannot explain cross-stage color bleed; each stage's texture is a genuinely separate cache entry. The shared `tmem=392` is an RDP TMEM slot number (where the RSP's interpretation of the texture lives during the *current* draw call), not a cache key component — irrelevant to GL-side caching. This specific theory is now ruled out; the M-141 paragraph proposing it should be read as superseded by this correction. + +**Next, concrete:** since source-side decode is now proven clean, trace forward from `import_texture_rgba16`'s `gfx_rapi->upload_texture(tex_upload_buffer, width, height, gen_mipmaps)` call (`gfx_pc.cpp:718`) through to the actual GL texture object and the `G_CC_INTERFERENCE` (TEXEL0×TEXEL1) shader combiner — the smoke (tile 0, IA) × fire (tile 1, RGBA16) multiply is the last place color assembly happens before the pixel hits the framebuffer. A live capture with `GE_PCDUMP` alongside `GE_D252` during the same kind of repro (bullet impact against a wall) would let a future session correlate a specific on-screen rainbow frame against which of the 14 (now known-correct) source textures was bound at that moment — if the bound texture is stage N but the on-screen color doesn't match stage N's known-correct palette above, the bug is confirmed to be in upload/sampling/combiner, not stage-selection; if the *wrong* stage is bound (e.g. game logic's `var_s2` aging index reads garbage), that's a `src/game` question requiring the bucket-(a)/(b)/(c) check (`docs/dev-process.md`'s new taxonomy) before any fix. Raw dumps kept at `scratch/d252_raw_*.bin` (gitignored) for a follow-up session to reuse without re-capturing. + > The §F row had no status cell; the table row's status is derived from this entry's verdict text (ctx-opt Bucket B, 2026-09-15). ## D255 — Facility firefight SIGSEGV on Steam Deck (v0.2.0-pre CI bundle; D253 fix confirmed working).