Gitea #48 ROOT-CAUSED + FIXED: MFD "stray blocks / misplaced lamps" = uninitialized translation-table entries leaking pixels into other displays' bit-planes

The conviction was empirical, not theoretical: a new write-site trap
(BT_PLANE_AUDIT) in the seven L4VB16 drawing primitives logs any draw whose
color carries bits outside its port's plane mask -- the exact cross-display
corruption condition (Replace ORs an unmasked color; Or/Xor ignore the mask
entirely; And clears foreign planes).  A quiet solo session produced 15-30
leaks per minute:

    [plane] PORT 'sec' pixmap draw at(0,0) idx 217 entry=0xffffff00 mask=0x3f
    [plane] LEAK DrawPixelMap8[table] at(639,0) color=0xffffff00 mask=0x3f leak=0xff00

THE DEFECT: L4GraphicsPort::translationTable[256] is never initialized in the
ctor, and BuildSecondaryTranslation fills only the entries its BitWrangler
reaches -- 2^numberOfBits: 64 for the sec plane (mask 0x3F), 4 for the overlay
(0xC0).  Entries above that stay heap garbage.  Every draw resolves color
through this table, and the PIXMAP path indexes it with raw pixel values
0..255: the 480x640 radar background carries pixel index 217, whose garbage
entry's high bits (0xFF00 = ALL EIGHT MFD planes) were stamped into the shared
640x480 buffer -- invisible on the culprit page (the in-plane bits happened
dark) and visible as bright fragments at the same coordinates on every OTHER
display.  That is the operator's screenshot exactly: the same-position blips
on Mfd1+Mfd2 and the Heat-display block.

FIX (engine, both layers):
  * zero translationTable in the L4GraphicsPort ctor (plane-neutral default);
  * BuildSecondaryTranslation now cycles the in-plane pattern across entries
    [2^bits..255] -- high-index art degrades to its (index mod 2^bits) colour
    IN-PLANE and can never leak.  The 1995 binary ships the SAME 64-entry fill
    and survived on 6-bit art discipline; garbage is not a preservable
    behaviour, so the cycle-fill is a guarded PORT deviation (documented).

A/B PROOF: same probe, 60s -- 0 leaks after the fix (15-30/min before).
sim3 3-pod regression: zero crashes.

SECOND real defect found + fixed en route: sessions configure the CAMERA seat
first ("cameraInit" -- which includes the MISSION-REVIEW context:
configure(0, sec, 0, 0x00FF, native, rgb, mrpal.pcc), a DirectColor context
whose translation tables legitimately hold full RGB565 values spanning all 16
plane bits), and the viewpoint swap to the mech re-configured WITHOUT tearing
that tree down (btl4app's s_gaugeTreeBuilt latch treats the mech build as the
first).  The orphaned review gauges kept executing through stale DirectColor
ports.  BTL4GaugeRenderer::ConfigureForModel now overrides (ConfigureForModel
made virtual in L4GREND.h) and tears the prior entity-bound tree down before
every rebuild -- safe on first build, and the review screen rebuilds the same
way when the seat returns to the camera.

Also logged (open-questions): the review-screen PlayerStatus panels read the
compiled player at RAW BINARY OFFSETS (+0x1FC vehicle / +0x1C8 score /
+0x1C4 alive-dead box) -- the databinding trap, dormant until the review
screen runs; bridge before enabling the review.

Ruled out along the way (with evidence): the pilot-list label/erase geometry
(erase box 128x32 covers the 64x16 name rasters), the radar name labels
(view-level ClipImage clips them), PlayerStatus in-game execution ([ps] probe:
never runs in-game), and the PNAME bitmap dimensions (egg generator emits
64x16 exactly).

Why it "started with the comms feature" (#43): that wave registered the new
gauge classes with the config interpreter, letting more of the authored page
furniture parse and draw than before -- the leaking high-index pixmaps rode in
with it.  [T3 correlation detail; the leak + fix are T2 live-verified.]

Diagnostics kept (env-gated): BT_PLANE_AUDIT (the leak trap, now a permanent
regression tripwire), BT_PS_LOG, the port-level pixmap identity trap.

KB: gauges-hud.md (#48 section), decomp-reference.md (BT_PLANE_AUDIT),
open-questions.md (PlayerStatus databinding entry).  checkctx CLEAN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-25 17:20:19 -05:00
co-authored by Claude Fable 5
parent 6f5a264835
commit 6bb03aed0b
7 changed files with 195 additions and 1 deletions
+1
View File
@@ -419,6 +419,7 @@ default-ON (`'0'` disables).
| `BT_GAUGE_SKIP_LOG` | `[gskip] <primitive>` per unregistered gauge widget the parse skips |
| `BT_VALVE_LOG` | condenser valve flow distribution |
| `BT_BAYTEST=<frame>` | #46 rig: send message 1 (crit-induced cook-off arm) to the first AmmoBin at the given sim frame — the 10 s fuse then runs live (`scratchpad/baytest.py` / `baypurge.py`) |
| `BT_PLANE_AUDIT` | #48 forensics: log every gauge draw whose color carries bits outside its port's plane mask (the cross-display leak trap; `[plane] LEAK/PORT` lines) |
| `BT_LOOP_AUDIT` | `[loop-audit]` per-sound `sample-flag × source-render-type -> AL_LOOPING` (Gitea #51; rig `scratchpad/loopaudit.py`) |
| `BT_LOOP_LEGACY=1` | restore the OLD always-loop rule (`AL_LOOPING = sample != ForceStatic`) for a field A/B — see the loop-flag note in [[wintesla-port]] |
| `BT_AUDIO_DUMP` | once-a-second `[playing]` dump of every playing AL source + gain/pitch/**loop** — catches a stuck looping source red-handed |
+41
View File
@@ -639,3 +639,44 @@ BITMAPS from the egg, not text — format, renderers, and the operator-console g
- Full history: `docs/GAUGE_COMPOSITE.md`; reticle recovery: `phases/phase-02-dpl2d-reticle.md`.
- Uses: [[attribute-pointer]] + [[reconstruction-gotchas]]; reads [[subsystems]] state.
- Renders on: [[pod-hardware]] MFD surfaces.
## #48 MFD ARTIFACTS -- ROOT-CAUSED + FIXED (2026-07-25) [T2 live-convicted]
The night-3 "stray blocks + misaligned lamps" on Heat/Mfd/sec: **uninitialized
translation-table entries leaking pixels into other displays' bit-planes.**
`L4GraphicsPort::translationTable[256]` was never ctor-initialized, and
`BuildSecondaryTranslation` fills only the entries its BitWrangler reaches --
**2^numberOfBits: 64 for the sec plane (mask 0x3F), 4 for the overlay (0xC0)**
-- so entries above that stayed heap garbage. Every draw resolves color
through this table, and the pixmap path indexes it with RAW PIXEL VALUES
(0..255): the 480x640 radar background carries pixel index 217 -> its garbage
entry's high bits (e.g. 0xFF00 = ALL EIGHT MFD planes) were written into the
shared 640x480 buffer at that position -- invisible on the culprit page (the
in-plane low bits happened dark), visible as bright fragments at the same
coordinates on EVERY OTHER display. Convicted empirically with the new
**`BT_PLANE_AUDIT`** write-site trap (L4VB16 primitives: a draw whose color
carries bits outside its port's plane mask logs primitive/port/position/mask;
Or/Xor ignore the mask entirely and And clears foreign planes -- all audited):
15-30 leaks/minute in a quiet solo session, `PORT 'sec' ... idx 217
entry=0xffffff00 mask=0x3f`, at exactly the artifact positions in the
operator's screenshot. FIX: zero the table in the ctor + cycle the in-plane
pattern across entries [2^bits..255] (high-index art degrades to its
index-mod-2^bits colour IN-PLANE, can never leak). The 1995 binary ships the
SAME 64-entry fill and relied on 6-bit art discipline -- garbage is not a
preservable behaviour, so the cycle-fill is a guarded PORT deviation.
Post-fix: **0 leaks over 60s** on the same probe; sim3 3-pod regression clean.
SECOND real defect fixed en route: sessions configure the CAMERA seat first
(`cameraInit` -- which builds the MISSION-REVIEW context: `configure(0, sec,
0, 0x00FF, native, rgb, mrpal.pcc)`, a DirectColor context whose table holds
full RGB565 values), and the swap to the mech re-configured WITHOUT tearing
that tree down (the btl4app latch treated the mech build as "first") -- the
orphaned review gauges kept executing through stale DirectColor ports.
`BTL4GaugeRenderer::ConfigureForModel` now tears down before every rebuild
(`ConfigureForModel` made virtual in L4GREND.h). Note the review-screen
`PlayerStatus` gauges also read the compiled player at RAW BINARY OFFSETS
(+0x1FC vehicle / +0x1C8 score / +0x1C4 alive-dead -- the databinding trap,
dormant until review runs); logged in [[open-questions]].
Why "it started with the comms feature": the #43 wave registered the new gauge
classes (PlayerStatus/pilotList/...) with the config interpreter, which let
more of the authored page furniture parse + draw than before -- the leaking
high-index pixmaps rode in with that. [T3 for this correlation detail; the
leak itself and its fix are T2 live-verified.]
+7
View File
@@ -247,6 +247,13 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
lamps for the aux/eng bezel buttons (LAMP.cpp/L4LAMP.cpp are T0-compiled; the glass panels
+ pod RIO both consume `RIO::SetLamp`, so the flash lands on-screen AND on the pod).
Decode the type-31 stream format first — it names which conditions flash which lamps.
- **PlayerStatus (the mission-review panels) reads the compiled player at RAW BINARY OFFSETS
(2026-07-25, found via #48) [T1 code-read].** btl4gau3.cpp `PlayerStatus::Execute`:
`player+0x1FC` (vehicle), `+0x1C8` (score, drawn as a numeric), `+0x1C4` (alive/dead -- a
state flip draws a 160x240 filled box) -- all 1995 offsets, garbage on our compiled layout
(the databinding trap). DORMANT in-game (the review context no longer survives the viewpoint
swap -- the #48 teardown fix) but LIVE the moment the mission-review screen actually runs at
round end. Bridge all three through btplayer.cpp accessors before enabling the review screen.
- **Factory capability-roster loops 2-4 are STILL DEAD (task #57 discovery).** mech.cpp's
post-roster loops add to `heatableSubsystems`(0x51155c)/`weaponRoster`(0x511830)/
`damageableSubsystems`(0x50e4fc) through the local `SubProxy` stub whose `IsDerivedFrom`
+6 -1
View File
@@ -87,7 +87,12 @@ public:
// Configuration
//--------------------------------------------------------------------
//
void
// VIRTUAL (Gitea #48): BTL4GaugeRenderer overrides this to tear the
// previous viewpoint's gauge tree down before rebuilding -- the camera
// seat's cameraInit (the MISSION-REVIEW ports/gauges, DirectColor over
// sec+overlay) otherwise survives the swap to the mech and its per-frame
// draws stamp full-16-bit pixels across EVERY display plane.
virtual void
ConfigureForModel(
const char *configuration_name,
Entity *entity
+106
View File
@@ -1843,6 +1843,50 @@ void
//
// Inputs are in DISPLAY COORDINATES, i.e., (0,0) in top left corner!
//===========================================================================//
// BT_PLANE_AUDIT (Gitea #48 forensics) -- the shared-buffer PLANE-LEAK trap.
//
// Every gauge surface owns specific bit-planes of the ONE 640x480 Word buffer
// (configure() masks, L4GAUGE.CFG:4395-4408). A draw is plane-safe only if
// its color stays inside its port's bitmask:
// Replace: *dest = (*dest & ~bitmask) | color -> color & ~bitmask LEAKS
// Or/Xor: *dest |= / ^= color (NO mask at all) -> color & ~bitmask LEAKS
// And: *dest &= color (no mask) -> (~color) & ~bitmask LEAKS
// A leaked bit sets pixels in ANOTHER display's plane at this position = the
// "stray blocks / misplaced lamps" artifact class (#48). This trap convicts
// the exact draw: primitive, position, color, mask, operation. Env-gated,
// capped, zero cost when BT_PLANE_AUDIT is unset.
//===========================================================================//
static void BTPlaneAudit(const char *prim, int color, int bitmask,
Enumeration operation, int x, int y)
{
static int s_on = -1;
if (s_on < 0) s_on = (getenv("BT_PLANE_AUDIT") != NULL) ? 1 : 0;
if (!s_on) return;
int leak;
switch (operation)
{
case GraphicsDisplay::And:
leak = (~color) & ~bitmask & 0xFFFF; // clears other planes' bits
break;
default: // Replace / Or / Xor
leak = color & ~bitmask & 0xFFFF; // sets other planes' bits
break;
}
if (leak == 0) return;
static int s_count = 0;
++s_count;
if (s_count <= 200 || (s_count % 500) == 0)
DEBUG_STREAM << "[plane] LEAK #" << s_count << " " << prim
<< " at(" << x << "," << y << ")"
<< std::hex << " color=0x" << color << " mask=0x" << bitmask
<< " leak=0x" << leak << std::dec
<< " op=" << (int)operation << "\n" << std::flush;
}
//
void
Video16BitBuffered::MarkChangedLines(
@@ -1894,6 +1938,7 @@ void
int x, int y
)
{
BTPlaneAudit("DrawPoint", color, bitmask, operation, x, y);
Check(this);
Verify(x >= bounds.bottomLeft.x);
Verify(x <= bounds.topRight.x);
@@ -1982,6 +2027,7 @@ void
Logical include_last_pixel
)
{
BTPlaneAudit("DrawLine", color, bitmask, operation, x1, y1);
# if defined(DEBUG)
Tell(
"Video16BitBuffered::DrawLine(" << color <<
@@ -2741,6 +2787,7 @@ void
int x2, int y2
)
{
BTPlaneAudit("DrawFilledRectangle", color, bitmask, operation, x1, y1);
# if defined(DEBUG)
Tell("Video16BitBuffered::DrawFilledRectangle(" <<
color << ", " <<
@@ -2975,6 +3022,7 @@ void
int sLeft, int sBottom, int sRight, int sTop
)
{
BTPlaneAudit("DrawBitMap", color, bitmask, operation, x, y);
# if defined(DEBUG)
Tell(
"Video16BitBuffered::DrawBitMap(<" <<
@@ -3397,6 +3445,8 @@ void
int sLeft, int sBottom, int sRight, int sTop
)
{
BTPlaneAudit("DrawBitMapOpaque(fg)", foreground, bitmask, operation, x, y);
BTPlaneAudit("DrawBitMapOpaque(bg)", background, bitmask, operation, x, y);
Check(this);
Diag_on;
@@ -3869,6 +3919,17 @@ void
int sLeft, int sBottom, int sRight, int sTop
)
{
{
static int s_paOn = -1;
if (s_paOn < 0) s_paOn = (getenv("BT_PLANE_AUDIT") != NULL) ? 1 : 0;
if (s_paOn && translation_table != NULL)
for (int _pa = 0; _pa < 256; ++_pa)
if ((translation_table[_pa] & ~bitmask & 0xFFFF) != 0)
{
BTPlaneAudit("DrawPixelMap8[table]", translation_table[_pa], bitmask, operation, x, y);
break;
}
}
# if defined(DEBUG)
Tell(
"Video16BitBuffered::DrawPixelMap8(<" <<
@@ -5987,6 +6048,12 @@ L4GraphicsPort::L4GraphicsPort(
for(int j=0; j<256; ++j)
{
myColor[j] = NULL;
// (#48) plane-safety: the table was NEVER initialized here, and the
// secondary/auxiliary builders only fill the entries their bit-
// wrangler reaches (64 for a 6-bit plane) -- the rest stayed heap
// garbage whose high bits leaked into OTHER displays' planes on any
// high-index pixmap pixel. Zero = plane-neutral.
translationTable[j] = 0;
}
Check_Fpu();
@@ -6377,6 +6444,27 @@ void
int sLeft, int sBottom, int sRight, int sTop
)
{
// #48 forensics: name the PORT + pixmap for any leaking table entries.
{
static int s_paOn = -1;
if (s_paOn < 0) s_paOn = (getenv("BT_PLANE_AUDIT") != NULL) ? 1 : 0;
if (s_paOn)
for (int _pa = 0; _pa < 256; ++_pa)
if ((translationTable[_pa] & ~bitMask & 0xFFFF) != 0)
{
static int s_paN = 0;
if (++s_paN <= 60)
DEBUG_STREAM << "[plane] PORT '" << name << "' pixmap draw at("
<< x << "," << y << ") idx " << _pa << std::hex
<< " entry=0x" << translationTable[_pa]
<< " mask=0x" << bitMask << std::dec
<< " pix=" << (void*)pixelmap
<< " sz=" << (pixelmap ? pixelmap->Data.Size.x : -1)
<< "x" << (pixelmap ? pixelmap->Data.Size.y : -1)
<< "\n" << std::flush;
break;
}
}
Check(this);
if (graphicsDisplay == NULL)
@@ -7028,6 +7116,24 @@ void
*destination++ = wrangler.Value;
}
while (wrangler.IncrementActive());
// (#48) the wrangler covers only 2^numberOfBits entries (64 for the
// 6-bit sec plane, 4 for the 2-bit overlay); indices past that were
// LEFT AS HEAP GARBAGE and any pixmap pixel >= that count wrote the
// garbage's high bits into other displays' planes (the #48 stray
// blocks; convicted live by BT_PLANE_AUDIT -- the 480x640 radar
// background carries index 217). Cycle the in-plane pattern across
// the remainder: high-index art degrades to its (index mod 2^bits)
// colour IN-PLANE, and can never leak. (The 1995 binary shipped the
// same 64-entry fill and relied on art discipline; garbage is not a
// preservable behaviour, so this is a guarded PORT deviation.)
{
int filled = 1;
for (int b = bitMask & 0xFF; b != 0; b &= (b - 1))
filled <<= 1;
for (int i = filled; i < 256; ++i)
translationTable[i] = translationTable[i & (filled - 1)];
}
Check_Fpu();
}
+18
View File
@@ -420,3 +420,21 @@ void
//------------------------------------------------------
L4GaugeRenderer::NotifyOfBecomingUninterestingEntity(entity); // FUN_0046fda0
}
//
// Gitea #48 -- the MFD-artifact root cause fix. See btl4grnd.hpp for the full
// conviction chain (cameraInit's mission-review DirectColor context surviving
// the viewpoint swap and stamping all 16 planes). Every configure now tears
// the prior entity-bound tree down first; safe on the first build (empty tree),
// and the review screen itself rebuilds the same way when the seat returns to
// the camera at round end.
//
void
BTL4GaugeRenderer::ConfigureForModel(
const char *configuration_name,
Entity *entity
)
{
TearDownForViewpointRestream();
L4GaugeRenderer::ConfigureForModel(configuration_name, entity);
}
+16
View File
@@ -71,6 +71,22 @@
//
void
TearDownForViewpointRestream();
// Gitea #48 (THE MFD-ARTIFACT ROOT CAUSE): tear the previous
// configuration's gauge tree down on EVERY viewpoint configure, then
// build. The camera seat configures first (`cameraInit` -- which
// includes the MISSION-REVIEW ports: `configure(0, sec, 0, 0x00FF,
// native, rgb, mrpal.pcc)` -- a DirectColor context whose translation
// tables hold full RGB565 values spanning ALL 16 plane bits); the swap
// to the mech re-configures WITHOUT tearing that tree down (the
// btl4app latch treats the mech build as "first"), so the review
// gauges kept executing through their orphaned DirectColor ports and
// every draw stamped pixels into EVERY display's plane -- the "stray
// blocks / misplaced lamps" on Heat/Mfd/sec. Convicted live by the
// BT_PLANE_AUDIT write trap: pixmap draws with mask 0x3F/0xC0 leaking
// 0xFF00/0x3F at the review furniture positions.
virtual void
ConfigureForModel(const char *configuration_name, Entity *entity);
};