D219 M-157: first visual capture of the rainbow-particle bug, cross-validates M-110's G_LIGHTING lead

Chasing D252 further, got the first-ever screenshot proof of this bug
family: a plain bullet-impact spark on a Bunker1 door cycling magenta ->
purple -> green -> teal -> purple across consecutive shots, captured
headlessly via GE_INPUTSCRIPT (Z fire pulses) + GE_PCDUMP together, then
reading the resulting PPMs directly (PIL) instead of only grepping a
probe log.

Traced the call chain (glass2.c's bullet_spark_render, a different code
path from both D219's explosion.c particles and D252's IMAGE_FIRE_N
system) and ruled out three mechanisms with actual code evidence before
finding one that fits:
- Not D252's fire-tile system (GE_D252 got zero hits on this repro).
- Not a struct-layout/ABI bug (sImageTableEntry is 12 bytes on both
  32- and 64-bit, no pointer members -- checked before assuming, per
  this session's own rule-2 bucket-(a) discipline).
- Not the primitive color (g_BulletSparkColors[1], a hardcoded literal
  for world-prop hits -- always pale yellow).
- The texture itself (explosion_smokeimages, IA8, verified via the
  asset table) has no color channels and decodes correctly.

That leaves M-110's G_LIGHTING-not-cleared mechanism (already proposed,
unconfirmed, for explosion.c) as the one that fits: bullet_spark_render
never clears G_LIGHTING either (only ORs in G_CULL_BACK), so if lighting
is on, fast3d reinterprets the vertex color bytes as a lighting normal
and computes a scene-light SHADE color instead -- independently
converging on the same mechanism at a second call site, for a visually
distinct symptom (bullet sparks vs explosions). Flagged honestly as
unresolved bucket (b) vs (c): bullet_spark_render's missing lighting
clear is decomp-verified N64 ground truth, so a src/game fix needs the
port-layer alternative ruled out first, per docs/dev-process.md's own
new taxonomy -- not assumed just because the mechanism fits.

Also documented the headless-visual-capture technique itself
(GE_INPUTSCRIPT + GE_PCDUMP together, then actually opening the PPMs)
in porting-notes.md as a reusable method -- several other long-open
visual findings (D290/D291/D294) have been stalled purely for lack of
eyes on the pixels, not for lack of a working probe.

No src/game or port/ changes -- diagnosis only, screenshots are
session-local (scratch/, gitignored). findings-index.csv regen was a
no-op.
This commit is contained in:
James Dansereau committed 2026-09-16 22:20:49 -04:00
1 parent 1e1041c6ba
commit c58797c2da
2 files changed
+33 -1

No files matched your search

+16
View File
@@ -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