diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index 6153c373..bea907aa 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -51,6 +51,7 @@ Two classes: | `GE_D193B` | `src/game/model.c` (`sub_GAME_7F06D3F4`, `#ifdef PORT`) | Rate-limited (1/97) raw anim root-motion decode trace (D193): `joint=` / `flip=` / `base=` mtx channel index / `frame=` / `tmp=` decoded s16 translation triple from the anim bitstream / `angle=` / `anim` ptr / `bd=`+`bs=` the D32 u32 bitDescriptors/bitStream addresses / `njoints`. M-80 Facility: `base=0`, `angle=0` **always** (root-motion facing delta never non-zero — top lead), `tmp` ≈ (±small, ~1086 hip-height, ±small). Compare `tmp`/`angle` magnitudes against a clean N64 decomp build of the same anim+frame. | **live** (D193) | | `GE_D250` | `src/boss.c` (`bossMainloop`, wraps `lvlRender()`, `#ifdef PORT`) | Per-wall-second line: `[D250] lvlRender avg=Nus n=M` — average + count of `lvlRender()` (DL-building: culling/animation/scene traversal) calls that wall-second, isolated from `gfx_run()`'s software-RSP interpretation cost (already covered by the existing "frame rendered in Nus" line). Added M-120 to chase D250 (fps stuck ~45-50 on Dam, ruled resolution/MSAA/vsync/build-flags out already) — found `lvlRender` itself is cheap (~0.5-1.5ms avg), so it is NOT the missing ~10-15ms/frame either; the still-unexplained cost must be elsewhere in the per-retrace-message loop (scheduler/thread round-trip, audio, or something not yet isolated by a targeted timer). Cheap; safe for a full playtest. | **live** (D250) | | `GE_D172` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1` — SETCOMBINE dedup log + non-LOD multitex tri dump; `#ifdef PORT`) | Particle magenta/cyan colour (D172). Logs each distinct SETCOMBINE + active cycletype, and — for a 2-cycle tri that consumes TEXEL1 with `tex_lod=0` and a distinct 2nd tile — the tile fast3d *samples* for each texunit vs what the DL *configured*. **Found D172**: 40/40 particle tris configured tile 1 @ tmem 392 (fire) but sampled tile 0 (smoke) → `smoke*smoke`. FIXED (`gfx_lod_tile_offset` `return rdp.tex_lod ? 0 : i`). Probe kept inert. | **live** (D172 FIXED, probe kept) | +| `GE_D157` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex`, right after the vertex color/lighting branch; `#ifdef PORT`) | D219/D252's "rainbow particle" family — bullet-impact spark on Bunker1 door confirmed by screenshot to cycle magenta/green/cyan (M-157). Keyed on the vertex's raw `v.cn[]` bytes exactly matching one of `glass2.c`'s `g_BulletSparkColors[]` entries; logs `geometry_mode`/LIGHTING state + resulting shaded RGB, to test (and, on the first run, refute) the M-110 G_LIGHTING-reinterpreted-as-normal theory for this call site. Only 2 hits so far (both `LIGHTING=off`, unaffected white) — likely not catching the actual door-spark vertices yet; needs a texture-identity-keyed variant, see D219 M-157's "Next, concrete" list. | **live** (D219/D252, still investigating) | | `GE_DVM` | `src/game/gunfire.c` (`gunUpdateAndFire` gate dump + `gunRenderFirstPersonGunModels` render dump; `#ifdef PORT`) | 1P weapon viewmodel (D215). `[DVM]` line: the `field_87F` show/hide gate terms + `Gun_hand_without_item` sub-terms + `copy_of_body_obj_header` RootNode/Switches/numRecords. `[DVMr]` line: `87F` + `cmds_emitted` = gfx commands the weapon `subdraw()` produced (healthy ≈ 150; the D215 bug showed **1**). Rate-limited ~1/s per hand. **Found + verified D215** (`renderdata.flags` read 0 → all weapon DL nodes gated off). Probe kept inert. | **live** (D215 FIXED, probe kept) | | `GE_DTEX` | `port/fast3d/gfx_pc.cpp:1001` | Per-`import_texture` param log (dims / line / size / lod / gen_mipmaps). Used to root-cause RC2 and D159. Kept as an inert diagnostic. | **live** (RC2/RC3 not finished) | | `GE_TEXDUMP` | `port/fast3d/gfx_pc.cpp:1020`, `port/fast3d/gfx_opengl.cpp:700` | PPM dump of every uploaded texture + a `fmt/siz/palfmt/palidx/pal[0..3]` line. Root-caused D161 (Depot ceiling). Kept as an inert diagnostic. | **live** (RC3 not finished) | diff --git a/docs/dev/findings.md b/docs/dev/findings.md index ddda485c..37ebf49c 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -11129,7 +11129,7 @@ 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. +**M-157 (2026-09-16/17, this session): first real visual capture of this bug family — but the G_LIGHTING theory below turned out to be a plausible-looking dead end, corrected at the end of this entry; read the whole thing, not just the first paragraph.** 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. @@ -11145,6 +11145,12 @@ if that's recorded anywhere in the D280 write-up above. 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. +**Correction, same session, right after writing the paragraphs above: went back and actually measured G_LIGHTING instead of trusting the pattern-match to M-110.** Missed on first pass that the code comment at `gfx_pc.cpp` (right next to the D219 `GE_D172` probe, ~line 2015) already documents a live result never written up in this findings.md file: **"D219 round 2: G_LIGHTING is ruled out (5000/5000 samples off, live-confirmed still purple/blue)"** for `explosion.c`'s particles specifically — the sibling case the M-157 write-up above leaned on had *already* been empirically disproven, not merely unconfirmed. That should have been caught before asserting a "cross-validation" for `glass2.c` on code-reading alone. + +Added a small, targeted diagnostic (`GE_D157`, `port/fast3d/gfx_pc.cpp`, right after the vertex color/lighting branch in `gfx_sp_vertex`, `#ifdef PORT`, cached getenv) that logs `geometry_mode`/`LIGHTING` state and the resulting shaded RGB whenever a vertex's raw `v.cn[]` bytes exactly match one of `glass2.c`'s `g_BulletSparkColors[]` entries (`{0xFF,0xFF,0xFF,0xFF}`, `{0xFF,0xFF,0xC8,0xFF}`, `{0xFF,0x00,0x00,0xFF}`). Re-ran the identical Bunker1 repro that produced the magenta/green/cyan screenshots: **only 2 hits, both `LIGHTING=off`, both pure white `(255,255,255)` unaffected** — no hit at all for the pale-yellow `(255,255,200)` value `objHit`'s door-spark call path should produce via `g_BulletSparkColors[1]`. Two possible readings, both worth recording: **(a)** most likely, **G_LIGHTING genuinely is off here too, and this mechanism does NOT explain the bullet-spark rainbow** — consistent with the sibling explosion.c case, and the two white hits this probe did catch (probably the correctly-rendering muzzle flash, `arg2==4`/`6`'s `flareimage2` branch) support that lighting is off for this whole particle family in this scene; **(b)** the probe's exact-byte-match approach may simply not be catching the actual spark vertices at all — vertex color could be getting modified before reaching this function by something not yet traced (Gouraud interpolation across the quad, a different `gfx_sp_vertex` call site, or `arg2`'s value at runtime differing from the `1` literal read in the static source). **Confidence: LOW-MEDIUM that G_LIGHTING is not the mechanism; the real root cause of the visually-confirmed rainbow cycling is OPEN AGAIN, not narrowed.** Do not carry the "cross-validates M-110" framing forward — that phrase in the first paragraph of this entry is superseded by this correction, kept only so the reasoning trail (and the mistake) stays visible. + +**Next, concrete, revised:** (1) confirm whether `GE_D157` actually fires at all during the visible magenta/green frames by cross-referencing frame numbers (`num_dls`) against the known-bad frame range (108-220 in the repro above) — if it never fires in that window, the probe is looking at the wrong vertices entirely and needs a texture-identity-based trigger instead (key on `explosion_smokeimages`/`flareimage2`/`scattered_explosions`'s actual runtime texture address, the way D252's probe keyed on `IMAGE_FIRE_N`'s addresses) rather than an exact vertex-color match. (2) Once the probe reliably fires on the actual bad frames, log the full RGBA path in the frame the screenshot shows magenta/green, not just LIGHTING state — the D217-style texture-cache-collision lead already proposed for `explosion.c` (see the code comment cited above) applies here too and hasn't been checked for this call path at all. (3) `GE_D157` is left in tree, env-gated, zero cost unset — extend it rather than replacing it. + ## D250 — FPS is inconsistent/low on many levels after 60 Hz shipped (M-119, user QA; D248 follow-on). diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index b3c932bf..41ca139c 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -1501,6 +1501,36 @@ static void gfx_sp_vertex(size_t n_vertices, size_t dest_index, const Vtx* verti d->color.b = vcn->b; } +#ifdef PORT + /* D157 diag (temporary, session 2026-09-16/17): does bullet_spark_render + * (glass2.c) actually draw with G_LIGHTING on? The M-110 mechanism + * (this vertex's v.cn[]/n.n[] union reinterpretation above) was + * already ruled out for explosion.c's particles (5000/5000 samples + * measured LIGHTING=off, see the D219 comment a few hundred lines + * below) -- don't assume it holds for glass2.c's bullet sparks + * without measuring. Key on the ORIGINAL authored bytes + * (v->v.cn[], readable regardless of which branch fired above, + * since n.n[]/cn[] are the same union storage) matching one of + * glass2.c's g_BulletSparkColors[] entries exactly. */ + static int ge_d157 = -1; + if (ge_d157 < 0) ge_d157 = getenv("GE_D157") != NULL; + if (ge_d157) { + static const uint8_t known[][4] = { + {0xFF,0xFF,0xFF,0xFF}, {0xFF,0xFF,0xC8,0xFF}, {0xFF,0x00,0x00,0xFF}, + }; + for (const auto& k : known) { + if (v->v.cn[0] == k[0] && v->v.cn[1] == k[1] && v->v.cn[2] == k[2] && v->v.cn[3] == k[3]) { + sysLogPrintf(LOG_NOTE, + "D157: bulletspark-color-match cn=(%d,%d,%d,%d) geometry_mode=%08x LIGHTING=%s -> shaded=(%d,%d,%d)", + v->v.cn[0], v->v.cn[1], v->v.cn[2], v->v.cn[3], rsp.geometry_mode, + (rsp.geometry_mode & G_LIGHTING) ? "ON" : "off", + (int)d->color.r, (int)d->color.g, (int)d->color.b); + break; + } + } + } +#endif + d->u = U; d->v = V;