diff --git a/docs/dev/findings.md b/docs/dev/findings.md index c5ba1074..ddda485c 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -11129,6 +11129,22 @@ if that's recorded anywhere in the D280 write-up above. **Status (from §F table):** **RESOLVED (M-115, 2026-09-12)** — byte-order fix in `src/game/image.c` scoped by pixel format (16-bit family bswap16-at-store, 32-bit identity); user playtest-confirmed. Superseded history below, which began: **OPEN — observed, not investigated.** Next: `GE_PCDUMP`/`GE_TEXDUMP` a Dam PPK explosion; check whether it's the same `gfx_lod_tile_offset` tile-sampling defect D172 fixed for blood, on an explosion DL D172's fix didn't touch. **Status: re-checked at runtime M-90 — mechanism covered by the D172 fix on current `main`; close pending a human by-eye pass (folds into PR #47 playtest).** **M-89 triage (local-Qwen, static analysis only, not yet runtime-confirmed):** the explosion DLs (`assets/oddtextures.c` `globalDL_0x078/0x120/0x1c8/0x270…`) use the same `G_CYC_2CYCLE`+`G_CC_INTERFERENCE,G_CC_MODULATEIA2`+`G_TL_TILE`, tile0=IA smoke/tile1=RGBA fire mechanism as the already-fixed D172 blood/spark records — no separate/unfixed code path found in `explosion.c` or the asset DLs. If still purple after D172's fix (which the user's Dam report postdates — needs re-confirming on current `main`), the next suspect is the GLSL combiner implementation of `G_CC_INTERFERENCE`+`G_CC_MODULATEIA2` for cycle 2 specifically (`gfx_generate_cc`, `port/fast3d/gfx_pc.cpp:312-494`), not the tile-offset fix. **Next, concrete:** run Dam (`-level_33`) with `GE_D172=1` while triggering a PPK/grenade explosion and check for "BUG" lines in the log (silence = D172's fix already covers this DL and the row can likely close on a re-test; needs a scripted-fire repro, not yet built). **M-90 runtime re-check DONE (scripted-fire repro built with `GE_INPUTSCRIPT`): the D172 fix demonstrably covers Dam's particle DLs — row can close on a human by-eye pass.** Ran `-level_33` headless with `GE_D172=1`, 64 scripted Z-pulses (`GE_INPUTSCRIPT="200:Z;215:Z;..."`, ~15 controller-reads apart) and a `GE_PCDUMP` window over the same span. Result: **40/40 particle multitex tris log `SAMPLED1=tile1 (tmem=392)`** — the correct RGBA fire tile, i.e. exactly the post-D172-fix behaviour (pre-fix these read `SAMPLED1 = tile0 (tmem=0 fmt=IA)`, the smoke texture); **zero** "BUG"/unknown-GBI/D146-abort lines; **0** purple/magenta pixels (`r>120, b>120, g<90, r-g>50, b-g>50`) across all 28 captured frames. Caveat: the user's M-87 "purple" report may predate the D172 fix on their binary (playtests run on whatever build was current); this re-check proves the sampling path is correct on current `main`, which is as far as a headless pass can take it — a final by-eye look at a live Dam PPK firefight is the honest closer (cheap for the user, folds into the D217 PR #47 playtest). **M-108 — REOPENED, the by-eye closer FAILED.** User played a live Dam (`-level_33`) PPK firefight on the current `main` build (post-D233/D234 fixes, same session as D243) and confirmed explosions are **still blue/purple**, directly contradicting M-90's clean scripted-repro result (40/40 correct-tile samples, 0 purple pixels, 28 frames). This means the M-90 `GE_INPUTSCRIPT` scripted-fire repro was very likely **not exercising the same explosion instance/DL the live firefight hits** — plausible gaps: a different explosion variant (grenade vs. bullet-impact spark vs. enemy-death vs. PPK-specific muzzle/hit effect) using a DL the scripted test's Z-pulses didn't trigger, or a state/lighting/prim-color condition (M-89's alternate suspect: the `G_CC_INTERFERENCE`+`G_CC_MODULATEIA2` cycle-2 GLSL combiner implementation, `gfx_generate_cc`, `port/fast3d/gfx_pc.cpp:312-494`) that only manifests under conditions the synthetic repro didn't reproduce. **Do not trust the M-90 all-clear for this finding going forward** — it proved one particle path clean, not that Dam's live-fire purple is fixed. Next: a live `GE_PCDUMP`/`GE_D172=1` capture during an actual purple explosion in real play (not a scripted approximation) is now the only way forward — need to pin down exactly what triggers it (PPK gunshots hitting a surface/enemy? a thrown/tossed explosive? which). **User has now confirmed (2026-09-12) this is NOT Dam-exclusive — game-wide, not just suspected.** Treat as confirmed scope, not pending a second-level check. **User wants this actively fixed, not just triaged** — next session should prioritize a live `GE_PCDUMP`/`GE_D172=1` capture during a real explosion (any level) over further static analysis, since the M-89/M-90 static passes are exhausted on this finding without producing a fix. **New HIGH-interest lead (M-110): `explosion.c` never clears `G_LIGHTING` before drawing any of the 15 particle DLs** (`globalDL_0x078` through `0x9a8` in `assets/oddtextures.c`, all structurally identical, all `G_CC_INTERFERENCE`+`G_CC_MODULATEIA2`) — grepped every particle-draw call site in `explosion.c` (`gSPClearGeometryMode` calls at lines 883/1495/1718/2227 only ever clear `G_CULL_BOTH`/`G_FOG`/`G_CULL_FRONT`, never `G_LIGHTING`) and confirmed `G_CC_INTERFERENCE,G_CC_MODULATEIA2` is used nowhere else in the codebase, so it's safe to key a probe on it. The port's vertex path (`port/fast3d/gfx_pc.cpp:1301-1346`) branches hard on `rsp.geometry_mode & G_LIGHTING`: if lighting is on when these particles draw, the authored RGBA tint in `v->v.cn[]` gets reinterpreted as a normal vector, and the resulting SHADE color (fed into `G_CC_MODULATEIA2`'s `COMBINED*SHADE`) becomes a scene-light color instead of the DL's actual red/orange fire tint — a mechanism that would plausibly read as "wrong color, state-dependent, game-wide" exactly as reported, and would explain why a synthetic scripted-fire repro (M-90, no nearby lit models) came back clean while live firefights (guards/lit models on screen) don't. **Not yet confirmed** — this depends on whatever last touched `geometry_mode` before the particle draw in a given frame, which the game's own N64-accurate command order controls; it is not yet established whether this is a genuine port bug (fast3d misordering/dropping a `G_GEOMETRYMODE` command relative to real hardware) or something that would reproduce identically on N64 (in which case it isn't actually the D219 mechanism, since N64 doesn't show purple explosions). **Instrumented, not fixed:** added a `#ifdef PORT`/`GE_D172=1`-gated log line to the existing D172 multitex-tri probe (`port/fast3d/gfx_pc.cpp` ~line 1740, `gfx_sp_tri1`) that now also prints `geometry_mode` and flags `LIGHTING=ON <-- D219 SUSPECT` whenever one of these particle triangles draws with lighting on. Sanity-checked against the known-clean M-90 repro (headless `-level_33`, 40 scripted Z-pulses): probe fires exactly 40/40 as before, all show `LIGHTING=off` — consistent with the already-known-clean case, confirming the probe works and doesn't false-positive. **Next, concrete:** run `build-pc/d172_silo.bat` (or equivalent, `GE_D172=1`) and trigger a real explosion (any level, any explosive source) near visible character models; grep the log for `D219 SUSPECT`. A hit confirms the mechanism and turns this into a real fix (clear `G_LIGHTING` in the port's particle-draw path or in fast3d's geometry-mode tracking, whichever turns out to be the actual divergence from N64); a miss across a real purple-explosion capture rules this whole lead out. **M-112 (this session): decode-chain code review + two new inert diagnostic probes; runtime capture NOT obtained (environment limitation, see below). Confidence: LOW-MEDIUM on a narrowed next step, no fix applied.** Read the full N64 texture decompression ground truth for these assets end to end: `texLoadFromDisplayList`→`texLoad`→`texInflateNonZlib`/`texInflateZlib`→(`texReadUncompressed | texInflateHuffman/texInflateRle+texChannelsToPixels | texBuildLookup+texInflateLookup/texInflateLookupFromBuffer`) (`src/game/image.c`), plus `gimgFixupGlobalimagetable`/`gimgSyncCompiledGlobalDLs` (`port/src/gimgfixup.c`) and `import_texture_rgba16` (`port/fast3d/gfx_pc.cpp:653-681`). Findings: (1) `IMAGE_FIRE_0..14` are **not** `sImageTableEntry`-table images (unlike `IMAGE_IMPACTMULTI`/`IMAGE_IMPACTREDBRICK*`/flare/ammo images, which live in the 32 `sImageTableEntry` arrays at segment offset 0xAC8+ and get their `G_SETTIMG` marker synthesized at runtime by `texSelect`, `src/game/othermodemicrocode.c:318`) — FIRE_N's `gsDPSetTextureImage(...IMAGESEG(IMAGE_FIRE_N))` is baked directly into the compiled `globalDL_0x078..0x9a8` display lists (segment offset 0x000-0xA50) alongside the interleaved, correctly-rendering `IMAGE_SMOKE_0..14` (IA16). Both FIRE's and SMOKE's markers go through the *same* `gimgFixupGlobalimagetable` part-1 DL-bswap step and the *same* `texLoadFromDisplayList`→`texLoad` resolution — no separate/divergent table-offset math applies to FIRE_N specifically (this narrows away part of the open brief's option (a): there is no `sImageTableEntry`/`s_tbl_offs[]` bounds bug possible here, because FIRE_N never goes through that array class at all). (2) `texChannelsToPixels`'s `TEXFORMAT_RGBA16` case and `texReadUncompressed`'s `TEXFORMAT_RGBA16` case are decomp ground truth (byte-exact, `#ifdef PORT` free) and are format-generic, not texture-specific — a bug there would require a same-format sibling to also be wrong, and IMPACT/FLARE RGBA16 sprites (same `G_IM_FMT_RGBA,G_IM_SIZ_16b`, same underlying `texLoad`) are the only same-format comparison point on hand, **but this session did not runtime-confirm whether those actually render correct colors in game** — flagging as an unverified assumption inherited from earlier sessions' "shared decoder is proven correct" framing, not re-proven here. (3) `texSwapAltRowBytes` is already an unconditional no-op on `PORT` (D159, `src/game/image.c:2219`, `return;` before any format switch) so it cannot be format- or texture-selective on the port — ruled out as a contributor for FIRE specifically (it's equally a no-op for every other texture). **New, added, NOT yet exercised:** two matching `GE_D219RAW`-gated diagnostic probes (inert unless the env var is set, `#ifdef PORT`, no behavior change): (a) in `texInflateNonZlib` (`src/game/image.c`, right after the per-image header read, and again right after decode) — logs `texnum`, `format`, `width`, `height`, `compmethod` from the bitstream header, then the first row of decoded RGBA5551 pixel words, for every image shaped 16x14 (the FIRE tile shape); (b) in `import_texture_rgba16` (`port/fast3d/gfx_pc.cpp`) — logs the same pixel words as read back at upload time, so a session can diff "as decoded" vs "as consumed" to rule pool corruption/mis-addressing in or out. **Environment blocker hit this session:** could not get the built `ge007.x86_64.exe` to produce *any* stderr/log output when launched from this delegated agent's shell, across four different invocation strategies (direct foreground exec, backgrounded `&`/`nohup`, `cmd /c start foo.bat` detached, `cmd /c foo.bat` synchronous with a hard timeout) — the process either exits or is killed before a single `osSyncPrintf`/`sysLogPrintf` line reaches the redirected log file, even though every print call in this codebase does an explicit `fflush(stderr)`. A plain GUI sanity check (launching `notepad.exe` the same way) worked fine, ruling out a blanket "no GUI" sandbox restriction, so the failure is specific to this binary/environment combination and its cause was not identified. Per `env-no-runtime-verify` (session memory): runtime verification is session-dependent; treat this session's empty captures as inconclusive, not as evidence of anything. **Next, concrete (for a session that CAN capture runtime output, interactive or otherwise):** (i) run any level with `GE_D219RAW=1` during an explosion/PPK firefight (or simply on level load, since these are likely eagerly cached) and read the logged `compmethod` for `IMAGE_FIRE_N` — cross-reference against a `GE_D219RAW`-instrumented capture of a *confirmed-correct-on-screen* RGBA16 sprite from the sibling `sImageTableEntry` families (e.g. `IMAGE_IMPACTMULTI`) to see whether FIRE uses a different `TEXCOMPMETHOD_*` (the compressed-bitstream compression method, not the pixel format) than a texture confirmed to render correctly — `TEXCOMPMETHOD_LOOKUP`/`_HUFFMANLOOKUP`/`_RLELOOKUP` (`texBuildLookup`+`texInflateLookup*`) build/consume a packed-color lookup table with a completely different bit-layout code path than `_HUFFMAN`/`_HUFFMANPERHCHANNEL`/`_RLE` (`texChannelsToPixels`, per-plane R/G/B/A order); if FIRE's method differs from the working comparison texture's, that pinpoints exactly which decode function to scrutinize next, rather than continuing to treat "the RGBA16 decoder" as one monolithic suspect. (ii) if the compmethod matches, the bug is genuinely upstream of decode (ROM/asset-extraction data itself, not the decompression code) — at that point compare the decompressed byte stream against a reference N64 emulator's texture-dump of the same asset (this repo has no such reference on hand) to establish definitively whether the ROM bytes for FIRE really do decode to blue/purple on real hardware too (in which case the bug is actually downstream in the color-combiner path after all, reopening the D172/G_CC_INTERFERENCE lead this session did not have budget to re-examine) or whether the extraction into this repo's `assets/oddtextures.c`-adjacent ROM data diverges from the real cart image for just these 15 offsets. Probes left in tree, both inert by default: `GE_D219RAW` (`src/game/image.c` `texInflateNonZlib`, `port/fast3d/gfx_pc.cpp` `import_texture_rgba16`). **M-141: Steam Deck playtest confirmation, widened symptom scope** — user reports "blood is purple, shots on body are blue/green" (general, not tied to a specific level in the report). Blood/hit-decal color is a new specific instance beyond this row's tracked "PPK explosions"/"particle effects" framing — same RGBA16/particle-color neighborhood as D172/D252, worth keeping in mind together rather than as three unrelated color bugs; whichever gets a real runtime capture first (this row's own `GE_D219RAW` probe, or D252's `GE_D252`) should also check blood decals specifically. +**M-157 (2026-09-16/17, this session): the M-110 `G_LIGHTING` mechanism is now CROSS-VALIDATED at a second, independent particle-draw call site — and this one has a real visual capture.** While chasing D252 (see that entry's M-156), reused the `GE_INPUTSCRIPT` Z-pulse technique on a bare `-level_09` (Bunker1) boot with `GE_PCDUMP` running, aimed at a plain metal door. **Got the first-ever visual capture of this bug family**, and it is dramatic: the same bullet-impact spark, hitting the same spot on the same door across consecutive shots, renders as bright **magenta** (frame 108), fading pale-purple (114), **green** (129), **teal/cyan** (144), purple again (198) — a literal hue-wheel cycle, screenshots in `scratch/d252_impact_zoom_*.png` (session-local, not committed — small enough to re-capture on demand with the command below). This is a plain rifle/pistol bullet hitting a static door, nowhere near an explosion — a different, previously uninvestigated code path from both D219's `explosion.c` particles and D252's `IMAGE_FIRE_N` aging system. + +**Traced the actual call chain and ruled out three plausible mechanisms with code evidence before finding a fourth that fits:** +1. **Not the D252 fire-tile system.** `GE_D252=1` (hooked in `import_texture_rgba16`) got **zero hits** on this exact repro — this bug fires through a different texture-loading path entirely. +2. **Not a struct-layout/ABI bug.** The spark's texture-frame lookup (`glass2.c:678` etc.) does raw pointer arithmetic — `((struct sImageTableEntry *)((u8*)thing->unk0C + frame*12))->width` — which looks exactly like the D3x/A1 "hardcoded stride into a struct that grew" pattern this session's rule-2 work catalogued. **Checked and refuted**: `sImageTableEntry` (`src/bondtypes.h:4085`) is `{u32 index; u8 width,height,level,format,depth,flagsS,flagsT,pad;}` — no pointer members, `sizeof == 12` on both 32- and 64-bit. The hardcoded `12` is correct on this platform; bucket (a) does not apply here. +3. **Not the fixed primitive-color path.** `bullet_spark_render` sets vertex color (`vtx.v.cn[]`) from `g_BulletSparkColors[arg2]` (`glass2.c:20-27`); for a world-prop hit (`propobj.c:9633`, `objHit`) `arg2` is a hardcoded literal `1`, giving a fixed pale-yellow `{0xFF,0xFF,0xC8,0xFF}` every time — cannot itself produce magenta/green/cyan. +4. **The actual texture (`explosion_smokeimages`, `assets/oddtextures.c:484`) is genuinely IA8** (`G_IM_FMT_IA, G_IM_SIZ_8b`, 6 frames, no color channels at all) — confirmed fast3d's format dispatch (`gfx_pc.cpp:~1200`) correctly routes `fmt=IA,siz=16b` (after `texSelect`'s legitimate 8b→16b RDP-upload promotion, `othermodemicrocode.c:~400`) to `import_texture_ia16`, which decodes it as plain intensity+alpha with no byte-order bug (unlike D219's RGBA16 case) — so the texture itself cannot be the color source either. + +**With texture and primitive color both ruled out as direct color sources, the M-110 `G_LIGHTING` mechanism is the one that fits everything: `bullet_spark_render` (`glass2.c:606-702`) never clears `G_LIGHTING`** — its only geometry-mode call is `gSPSetGeometryMode(gdl++, G_CULL_BACK)` (line 696), which only sets a bit, never clears one. If `G_LIGHTING` is on (inherited from whatever level geometry drew immediately before this overlay), fast3d's vertex path reinterprets the authored `vtx.v.cn[]` color bytes as a lighting normal vector and computes a scene-light-derived SHADE color instead — exactly the mechanism M-110 proposed for `explosion.c`'s particles, now found completely independently in a second file, for a completely different visual symptom (bullet sparks, not explosions), both showing the same "wrong, state-dependent, cycling" color family. This is strong convergent evidence for the G_LIGHTING theory generally, even though neither call site has a *confirmed* fix yet. + +**Bucket classification, per this session's own dev-process.md §taxonomy — genuinely unresolved, flagging honestly rather than guessing:** `bullet_spark_render` carries a real decomp address comment (`// Address: 7F0A3F04`), i.e. its lack of a lighting-clear call is decomp-verified N64 ground truth, not a decompilation error — so if this were simply "add `gSPClearGeometryMode(G_LIGHTING)` to glass2.c," that would be changing verified-original game code, squarely bucket (c) territory needing a rule-2 sign-off with proof N64 hardware doesn't show this (not yet gathered). But it's equally plausible this is bucket (b): a port-layer bug in *which* lights are considered, or in geometry-mode tracking/restoration between draws, that N64's actual per-DL state machine would never have produced — fast3d's vertex-lighting implementation (`gfx_pc.cpp:~1301-1346`) hasn't been audited for a normal-vs-reference-implementation divergence in this session. **Do not add a lighting-clear call to `glass2.c` or `explosion.c` without first ruling out (b)** — that's the two-question gate `docs/dev-process.md`'s §7 requires before any bucket-(c) fix. + +**Next, concrete:** (1) audit `port/fast3d/gfx_pc.cpp`'s vertex-lighting path against the real RSP lighting equation for a divergence that would explain why this manifests as literal rainbow hues rather than a plausible (if wrong) lit color — a genuine implementation bug there would be bucket (b), fixable entirely in `port/`, no sign-off needed, and should be checked FIRST. (2) If that's clean, trace what actually leaves `G_LIGHTING` on at the moment these overlays draw and whether N64's real command stream would have the same state (needs either real hardware or a from-scratch RSP command trace, not available this session) before treating this as a genuine bucket-(c) case. (3) A cheap falsifier for a future session with build access: force `G_LIGHTING` off immediately before `bullet_spark_render`'s draw call under a temporary `#ifdef PORT`/env-gated probe (not a permanent fix) and see if the spark holds a stable pale-yellow — a clean result narrows the mechanism definitively without committing to a fix path yet. + +Repro for future sessions (headless, no game code touched): `GE_PCDUMP="0-1200:3" GE_INPUTSCRIPT="60:Z;70:Z;80:Z;...;350:Z" ./build-pc/ge007.x86_64.exe -level_09`, then inspect `ppm/frame_0001*.ppm` visually (or via any PPM viewer) near the door directly ahead of Bunker1's spawn — the spark reproduces on nearly every shot. This headless-visual-capture technique (`GE_INPUTSCRIPT` + `GE_PCDUMP` together, then inspecting the resulting PPMs directly) is itself a reusable finding for future "does this actually look wrong" investigations in this project — no live human needed to get eyes on a rendering bug, which several other OPEN findings (D290, D291, D294) have been stalled on for exactly that reason. + ## D250 — FPS is inconsistent/low on many levels after 60 Hz shipped (M-119, user QA; D248 follow-on). @@ -11166,7 +11182,7 @@ This is a **perfect, smooth white-hot → yellow → orange → deep-red → fad **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. **Tried, no data:** re-ran the identical Z-pulse script with `GE_D172=1` instead (the existing multitex-tile probe that checks whether TEXEL1 actually samples the tile the DL configured) hoping to catch a stale-tile-binding bug directly — **zero hits**, meaning the probe's own gate condition (`use_2cyc && comb->used_textures[1] && !rdp.tex_lod`, tmem mismatch between configured/sampled tiles) never matched during this repro. Not investigated why; could mean this particular repro's particles don't take the 2-cycle dual-texunit path the probe expects, or that path genuinely isn't where the bug lives. Don't re-run `GE_D172` on this exact repro expecting different results without first checking why it stayed silent. +**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. **Tried, no data:** re-ran the identical Z-pulse script with `GE_D172=1` instead (the existing multitex-tile probe that checks whether TEXEL1 actually samples the tile the DL configured) hoping to catch a stale-tile-binding bug directly — **zero hits**, meaning the probe's own gate condition (`use_2cyc && comb->used_textures[1] && !rdp.tex_lod`, tmem mismatch between configured/sampled tiles) never matched during this repro. Not investigated why; could mean this particular repro's particles don't take the 2-cycle dual-texunit path the probe expects, or that path genuinely isn't where the bug lives. Don't re-run `GE_D172` on this exact repro expecting different results without first checking why it stayed silent. **M-157 update (same session, see `## D219`):** found and visually captured the rainbow-cycling symptom in a completely different code path — `glass2.c`'s `bullet_spark_render` (plain bullet-impact sparks on walls/doors, not fire/explosion particles at all) — and traced it to the same `G_LIGHTING`-not-cleared mechanism M-110 already proposed for `explosion.c`. That entry now has the strongest lead and the only real screenshots for this whole particle-color bug family; read it before continuing D252 specifically, since the two may turn out to share one root cause once the `G_LIGHTING` mechanism is confirmed or refuted. > 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). diff --git a/docs/porting-notes.md b/docs/porting-notes.md index 06fbcdff..1d39ff9a 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -1392,6 +1392,22 @@ made mutually acceptable (dual validation / one-time migration). ge007.x86_64.exe` instead to reliably terminate it from this shell, and `ps aux | grep -i ge007` to confirm before moving on — don't trust a `kill`/`pkill` exit code alone. +- **Headless visual verification of a rendering bug is possible without a + live human** (M-156/M-157, D252/D219 sessions) — combine `GE_INPUTSCRIPT` + (scripted controller input, e.g. `Z` pulses to fire a weapon) with + `GE_PCDUMP="lo-hi:step"` (writes `./ppm/frame_NNNNNN.ppm` — NOT a separate + configurable output dir, just `./ppm` relative to cwd; `GE_MKDIR` creates + it) in the *same* run, then actually open the resulting PPMs (Python + `PIL.Image.open(...).convert('RGB')` reads them fine, no format quirks) + instead of only grepping a probe's text log. A text-only probe proves a + code path fired; it does not prove what ended up on screen — several + long-OPEN findings (D290/D291/D294, and D252/D219's whole "rainbow + particle" family before this session) were stalled for lack of eyes on + the actual pixels, not for lack of a working probe. Build a low-res + contact-sheet montage first (tile many frames small, e.g. 160×120, to + scan for the moment of interest cheaply) before zooming into individual + full-res frames — full Dam/Bunker1 frames are 3072×1728, too large to + eyeball one at a time across a whole capture window. - **A crash fault address of `0xffffffffffffffff` on x86-64 usually means a non-canonical #GP from a garbage-high-bits pointer (bits 63:48 not a sign extension of bit 47), not a computed `-1` sentinel** (cross-validated