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
+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();
}