BT410 5.3.82: the MFD bleed -- mechanism found (uninitialised translation table)

Operator report: artifacts bleed from the colour/radar display into the MFDs,
present in BT411 too, absent in the original.  Almost certainly the same
"intermittent plane-write artifact" that survived seven earlier hypotheses.

The chain, every link read from the code:

1. The colour head is SIX BITS (L4GAUGE.CFG:4395, sec mask 0x003F), and the
   MFDs are bits 8-15 of the same 16-bit words -- one plane-packed
   framebuffer, which is why a colour-head defect surfaces as MFD garbage.
2. BuildSecondaryTranslation (L4VB16.CPP:5419) fills exactly 64 entries of
   int translationTable[256]; entries 64..255 are never initialised.
3. DrawPixelMap8 (L4VB16.CPP:2708) indexes that table with an 8-BIT source
   pixel -- no clamp.
4. DrawPoint/Replace (L4VB16.CPP:637) ORs the colour in UNMASKED; the writer
   assumes the caller already confined it to the port's bits.
5. The sec port's own art supplies out-of-range indices, and they are
   SENTINELS not colour data: AVACRIT.PCC is 94% <= 63 with 6% at exactly
   231; BTSEC1.PCX is 99.8% <= 63 with 0.2% at 254.  Transparent they are
   skipped; OPAQUE they index the uninitialised tail.

Why the original is clean: translationTable lives in a heap-allocated port,
and on the pod that allocation was almost certainly zeroed -- garbage reads
as colour 0, black, invisible.  Our heap has different history.  That also
explains the intermittency that defeated the earlier hypotheses: it depends
on heap contents, not on drawing logic.

Narrowed to three opaque call sites (BTL4GAU2.CPP:916/1159,
BTL4GAUG.CPP:1953).  The fix belongs in our tree via the source410-shadows-
CODE rule -- zero the table's tail after BuildSecondaryTranslation -- which
is worth doing regardless of which site is guilty, since it turns a
heap-dependent artifact into a black pixel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-29 21:06:22 -05:00
co-authored by Claude Fable 5
parent ed218483ec
commit b366da6f1e
@@ -1969,3 +1969,75 @@ free, since every limb hinge has the same problem.
So the reconstruction's articulation work is DONE and correct as of 5.3.80;
what remains is a decoder gap in the viewing tool, which is a different
component with a different owner.
--------------------------------------------------------------------------------
THE MFD BLEED (operator report): MECHANISM FOUND -- AN UNINITIALISED TRANSLATION
TABLE INDEXED BY 8-BIT ART ON A 6-BIT PORT
--------------------------------------------------------------------------------
Operator, 2026-07-29: artifacts bleed from the radar/colour display into the
MFDs -- present in the BT411 builds too, ABSENT in the original. That last
clause is the key: whatever it is, both reconstructions share it and the
shipped game does not. This is almost certainly the same "intermittent
plane-write artifact" that has survived seven earlier hypotheses in this file.
THE CHAIN, every link read from the code:
1. L4GAUGE.CFG:4395 configure(0,sec,270,0x003F,clut0,rgb,btspal.pcc)
The colour head is SIX BITS. The MFDs are bits 8-15 of the SAME 16-bit
words (0x0100..0x8000), which is why a colour-head defect can only ever
show up as MFD garbage -- one framebuffer, ten plane-packed ports.
2. L4VB16.CPP:5419 L4GraphicsPort::BuildSecondaryTranslation()
BitWrangler wrangler(bitMask & 0xFF, 8);
do { *destination++ = wrangler.Value; } while (wrangler.IncrementActive());
For mask 0x3F this writes exactly 64 entries of `int translationTable[256]`
(L4VB16.HPP:487). Entries 64..255 are NEVER INITIALISED.
3. L4VB16.CPP:2708 DrawPixelMap8
color = (Word) translation_table[source_data];
An 8-BIT source pixel indexes that table directly. No clamp, no mask.
4. L4VB16.CPP:637 DrawPoint / Replace
bitmask = ~bitmask;
*dest = (Word)((*dest & bitmask) | color);
The colour is OR'd in UNMASKED. The writer's contract is "caller passes a
colour already confined to this port's bits" -- which holds for entries
0..63 and is violated by whatever garbage sits at 64..255.
5. The art supplies the out-of-range indices. Decoding the sec port's own
bitmaps (the CFG scopes widgets with `port = <name>;`):
AVACRIT.PCC 172x217 94%% of pixels <= 63, 6%% at exactly 231
BTSEC1.PCX 480x640 99.8%% <= 63, 0.2%% at exactly 254
ADPAL.PCC 2x5 all 10 pixels = 99 (a palette STRIP, not art)
The high values are SENTINELS -- transparency keys and ramp markers --
not general colour data. Drawn transparent they are skipped and never
touch the table; drawn OPAQUE they index the uninitialised tail.
WHY THE ORIGINAL IS CLEAN. translationTable lives in a heap-allocated
L4GraphicsPort. On the pod the allocation was almost certainly fresh zeroed
memory, so entries 64..255 read 0 -> colour 0 -> black -> invisible against a
black head. Our build (and BT411's) has different heap history, so the same
reads return junk with bits in the MFD planes. That also explains the
intermittency that defeated the earlier hypotheses: the artifact depends on
heap CONTENTS, not on any drawing logic, which is exactly why it never
correlated with anything in the widget code.
SUSPECTS, narrowed to three. Only the OPAQUE path can push a sentinel through
the table; our reconstruction has exactly three such call sites:
BTL4GAU2.CPP:916 localView.DrawBitMapOpaque(0, 0, base)
BTL4GAU2.CPP:1159 view->DrawBitMapOpaque(0, 0, bmp)
BTL4GAUG.CPP:1953 localView.DrawBitMapOpaque(foregroundColor, 0, bit_map, ...)
Next step is to check DrawBitMapOpaque's semantics -- whether it SUBSTITUTES
the background colour for key pixels (safe) or maps every pixel through the
table (the bug) -- and then which of the three draws sec-port art containing
231/254.
THE FIX, when it comes, goes in OUR tree, not the archive. L4GREND.CPP:543
constructs the ports, and translationTable/BuildSecondaryTranslation are
protected, so a subclass cannot be slipped in from outside. The established
mechanism is the build's source410-shadows-CODE rule: bring the TU into
restoration/source410/MUNGA_L4/ and zero the tail there
(`for (i = 1 << numberOfBits; i < 256; ++i) translationTable[i] = 0;`), which
makes our behaviour deterministic AND matches what the original actually did
at runtime. Worth doing regardless of which call site is guilty -- it turns a
heap-dependent artifact into a black pixel.