diff --git a/context/decomp-reference.md b/context/decomp-reference.md index 27cddf6..f3e8447 100644 --- a/context/decomp-reference.md +++ b/context/decomp-reference.md @@ -419,6 +419,7 @@ default-ON (`'0'` disables). | `BT_GAUGE_SKIP_LOG` | `[gskip] ` per unregistered gauge widget the parse skips | | `BT_VALVE_LOG` | condenser valve flow distribution | | `BT_BAYTEST=` | #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 | diff --git a/context/gauges-hud.md b/context/gauges-hud.md index fa01f91..1a809db 100644 --- a/context/gauges-hud.md +++ b/context/gauges-hud.md @@ -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.] diff --git a/context/open-questions.md b/context/open-questions.md index 0cc60cf..c0055ac 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -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` diff --git a/engine/MUNGA_L4/L4GREND.h b/engine/MUNGA_L4/L4GREND.h index 2b3646c..59281bb 100644 --- a/engine/MUNGA_L4/L4GREND.h +++ b/engine/MUNGA_L4/L4GREND.h @@ -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 diff --git a/engine/MUNGA_L4/L4VB16.cpp b/engine/MUNGA_L4/L4VB16.cpp index 0d93feb..f81fc83 100644 --- a/engine/MUNGA_L4/L4VB16.cpp +++ b/engine/MUNGA_L4/L4VB16.cpp @@ -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(); } diff --git a/game/reconstructed/btl4grnd.cpp b/game/reconstructed/btl4grnd.cpp index 7370835..560c507 100644 --- a/game/reconstructed/btl4grnd.cpp +++ b/game/reconstructed/btl4grnd.cpp @@ -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); +} diff --git a/game/reconstructed/btl4grnd.hpp b/game/reconstructed/btl4grnd.hpp index d32dd82..1815667 100644 --- a/game/reconstructed/btl4grnd.hpp +++ b/game/reconstructed/btl4grnd.hpp @@ -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); };