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:
co-authored by
Claude Fable 5
parent
6f5a264835
commit
6bb03aed0b
@@ -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 |
|
||||
|
||||
@@ -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.]
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user