From d0e29a4d97730356377397b87585a01469757f6f Mon Sep 17 00:00:00 2001 From: Cyd Date: Thu, 13 Aug 2026 07:18:43 -0500 Subject: [PATCH] radar: composite the OVERLAY plane -- SECTOR/SCALE were blank on every desktop path Playtester DOS-binary comparison: our radar showed blank SECTOR:/SCALE: values. Root cause [T1]: the secondary CRT's low byte is authored as TWO ports (L4GAUGE.CFG:4395-4396) -- sec (0x3F, btspal) + overlay (0xC0, btopal, TransparentZero) carrying the btsec1ov graticule, the SCALE numeral (RadarRange) and the SECTOR numerals (sectorDisplay). Both numeral writers are reconstructed and RUN -- their pixels were in the shared buffer's 0xC0 bits all along -- but every desktop expand indexed the LUT with pixel & 0x3F, stripping them. The arcade DAC indexed the FULL low byte. Key mechanism: clut0 ALREADY holds the DAC's combined 256-entry table -- BuildSecondaryPalette (AllChannels, sec) fills all 256 indices and the overlay's TransparentZero pass overwrites groups 0x40/0x80/0xC0 -- so the composite is exactly a mask change, no blending code: - BTSecCompositeMask(): sec->GetBitMask() | overlay's (NULL-guarded, same display), used by BTDrawGaugeSurfaces + BTDrawCockpitPanels (surround), SVGA16::Update case 0 (pod parity), and the glass BlitSurface palette expand. - GlassWindowToken: the palette-expanded radar window's dirty token now includes the overlay port -- the SECTOR numerals tick on that plane, and a pixel-only 0x3F token would never repaint them. The overlay's ColorMapper re-stomp over combined indexes is authentic (the shipped machine ran the identical palette code) and is left alone. Verified live (glass panels): SECTOR: 497.503 ticking with movement, SCALE: 1000 at spawn (radarRange init 250*2^2 -- the DOS reference's 4000 is max zoom, not a delta), and the btsec1ov graticule rules now draw. KB: GAUGE_COMPOSITE row 28 + polish list updated. Co-Authored-By: Claude Opus 4.8 --- docs/GAUGE_COMPOSITE.md | 4 ++-- engine/MUNGA_L4/L4GLASSWIN.cpp | 19 ++++++++++++++++++ engine/MUNGA_L4/L4VB16.cpp | 35 +++++++++++++++++++++++++++++++--- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/docs/GAUGE_COMPOSITE.md b/docs/GAUGE_COMPOSITE.md index fc484dd..e59e8c5 100644 --- a/docs/GAUGE_COMPOSITE.md +++ b/docs/GAUGE_COMPOSITE.md @@ -217,7 +217,7 @@ yet reconstructed (parse-skipped). The separate window is now the **live viewer* - **Pod MFD port-name reconcile (pod-only):** a ~5-line positional dual-name fallback in `SVGA16::Update` (`UL = GetGraphicsPort("auxUL2"); if(!UL) UL = GetGraphicsPort("Heat"); …`) so BT MFDs reach the pod's real monitors. NOT a CFG rename (the CFG uses BT names pervasively + `MUNGA_L4` is shared with RP's aux names). -- **Polish:** overlay-plane compositing over `sec`; a pod-accurate RGB-packing toggle; texture atlas +- **Polish:** ~~overlay-plane compositing over `sec`~~ (DONE 2026-08-13, row 28); a pod-accurate RGB-packing toggle; texture atlas (one lock/upload); device-reset recreate hardening for the additional swap chain. ## Widget reconstruction — the real fix (in progress; mapped by `bt-gauge-widget-recon-map`) @@ -691,7 +691,7 @@ reconfigure/externalConfigure); parse-skip list EMPTY ([gskip]=0), all 50 attr b | 25 | sec: headingPointer needle + numeric | YawPitchRoll decomposition (engine-convention port of euler[0]) | LIVE: 089→120 while turning under BT_GOTO | T2 | CORRECT | | 26 | sec: numericSpeed | LinearSpeed (abs(adv)/dt) | LIVE: 0 parked → 176-182 walking | T2 | CORRECT (units question stays open) | | 27 | sec: digitalClock MISSION TIME | engine mission clock (format enum arg) | LIVE: 09:19→06:04 counting | T0/T2 | CORRECT | -| 28 | sec: sectorDisplay (overlay) | localOrigin → Round(−Z·0.01)+500 / Round(X·0.01)+500 | commit 4a4ec68 [BT_SECTOR_LOG live]; NOTE: overlay plane not composited into the dev sec cell (polish item) — data verified by log | T2 | CORRECT | +| 28 | sec: sectorDisplay (overlay) | localOrigin → Round(−Z·0.01)+500 / Round(X·0.01)+500 | commit 4a4ec68 [BT_SECTOR_LOG live]; **overlay plane COMPOSITED 2026-08-13** (branch sec-overlay-composite): every sec expand (BTDrawGaugeSurfaces, BTDrawCockpitPanels, SVGA16::Update case 0, glass BlitSurface) now uses sec\|overlay = 0xFF — clut0 already held the arcade DAC's combined table (BuildSecondaryPalette spreads each port over the other's bit combos), so the fix is mask-only; the glass dirty token gained the overlay port so the numerals repaint. Verified live: SECTOR ticks + SCALE:1000 at spawn (1000 = radarRange init; the DOS shot's 4000 is max zoom) + the btsec1ov graticule now draws | T2 | CORRECT | | 29 | sec: schematic ARMOR view (cmArmor/multiArmor, dama.pcc) | zone damageLevel ×100 → adpal ramp | incr.3/4; all-green pristine LIVE | T2 | CORRECT (AUTH-STATIC all-green solo) | | 30 | sec: schematic CRITICAL view (cmCrit) | subsystem simulationState/damage | LIVE this audit: N-cycle shows the full subsystem list (GEN A-D, LOOP 1-6, HUD, SENSORS, GYRO, TORSO, weapons) | T2 | CORRECT | | 31 | sec: schematic HEAT view (cmHeat) | subsystem currentTemperature tint | #6 pixel-verified; re-cycled this audit (mask 0x450421→0x490421→0x510421) | T2 | CORRECT | diff --git a/engine/MUNGA_L4/L4GLASSWIN.cpp b/engine/MUNGA_L4/L4GLASSWIN.cpp index a36ceef..1a9f992 100644 --- a/engine/MUNGA_L4/L4GLASSWIN.cpp +++ b/engine/MUNGA_L4/L4GLASSWIN.cpp @@ -1313,6 +1313,20 @@ static void int mask = port->GetBitMask(); int palId = port->paletteID; int tint = w->monoTint; + // OVERLAY COMPOSITE (2026-08-13): the radar's low byte is TWO ports -- sec + // (0x3F) + overlay (0xC0: the graticule art, the SCALE numeral and the + // SECTOR numerals). The arcade DAC indexed the full low byte and clut0 + // already holds the combined 256-entry table (BuildSecondaryPalette spreads + // each port over the other's bit combinations), so the palette-expanded + // window composites both planes with just the OR'd mask -- previously the + // 0x3F mask left SECTOR:/SCALE: blank while their writers were live. + if (tint < 0) + { + L4GraphicsPort *ov = + static_cast(gr->GetGraphicsPort("overlay")); + if (ov != NULL && ov->graphicsDisplay == port->graphicsDisplay) + mask |= ov->GetBitMask(); + } static int sMfdPal = -1; if (sMfdPal < 0) sMfdPal = getenv("BT_GLASS_MFD_PAL") ? 1 : 0; if (sMfdPal && tint >= 0) @@ -2110,6 +2124,11 @@ static unsigned long const char *ports[8]; int np = 0; ports[np++] = w->portPrimary; + if (w->monoTint < 0) + ports[np++] = "overlay"; // composited into the palette-expanded + // radar (2026-08-13) -- the SECTOR/SCALE + // numerals tick on THIS plane, so their + // churn must mark the window dirty if (w->portAlt != NULL) ports[np++] = w->portAlt; for (int gi = 0; gi < w->groupCount && np < 7; ++gi) { diff --git a/engine/MUNGA_L4/L4VB16.cpp b/engine/MUNGA_L4/L4VB16.cpp index 4628f72..4becc98 100644 --- a/engine/MUNGA_L4/L4VB16.cpp +++ b/engine/MUNGA_L4/L4VB16.cpp @@ -860,6 +860,31 @@ static const float kBTPanelH = 480.0f; // channel-enabled and which is BlankColor; the draw loop below skips the // blanked plane, so the dev cell shows exactly what the pod monitor shows. // The Eng entries share their sibling's cell rect and FOLLOW it in the list. +// +// OVERLAY COMPOSITE (2026-08-13). The secondary CRT's low byte is authored as +// TWO ports -- sec (bits 0x3F, btspal) and overlay (bits 0xC0, btopal, +// TransparentZero) carrying the graticule art, the SCALE numeral (RadarRange) +// and the SECTOR numerals (sectorDisplay). The arcade DAC indexed the FULL low +// byte, blending both planes; every port expand used to mask to sec's 0x3F, +// which is why SECTOR:/SCALE: read blank on desktop while their reconstructed +// writers were live in the buffer the whole time. clut0 already holds the +// DAC's combined 256-entry table (BuildSecondaryPalette spreads each port's +// colors across the other's bit combinations), so the composite is exactly +// this mask -- no blending code. +// +static int BTSecCompositeMask(GaugeRenderer *gr, L4GraphicsPort *sec_port) +{ + int mask = sec_port->GetBitMask(); + if (gr != NULL) + { + L4GraphicsPort *ov = + static_cast(gr->GetGraphicsPort("overlay")); + if (ov != NULL && ov->graphicsDisplay == sec_port->graphicsDisplay) + mask |= ov->GetBitMask(); + } + return mask; +} + static const BTGaugeSurfaceDesc kBTGaugeSurfaces[9] = { { "Heat", 0xFFFF, 0.0f / kBTPanelW, 0.0f, 320.0f / kBTPanelW, 0.5f, 0 }, @@ -946,7 +971,9 @@ void BTDrawGaugeSurfaces(LPDIRECT3DDEVICE9 device, float px, float py, float pw, rot = s_secRot; } svga->DrawDevSurface( - device, i, port->GetBitMask(), port->paletteID, d.monoTint, + device, i, + d.monoTint < 0 ? BTSecCompositeMask(gr, port) : port->GetBitMask(), + port->paletteID, d.monoTint, px + d.cellX * pw, py + d.cellY * ph, d.cellW * pw, d.cellH * ph, rot); } @@ -1322,7 +1349,9 @@ void BTDrawCockpitPanels(LPDIRECT3DDEVICE9 device) } int surf = ckSlotOf[i]; int useTint = (d.monoTint < 0) ? -1 : tint; - svga->DrawDevSurface(device, i, port->GetBitMask(), port->paletteID, useTint, + int useMask = (d.monoTint < 0) ? BTSecCompositeMask(gr, port) + : port->GetBitMask(); + svga->DrawDevSurface(device, i, useMask, port->paletteID, useTint, (float)L.surfX[surf], (float)L.surfY[surf], (float)L.surfW[surf], (float)L.surfH[surf], rot); } } @@ -5833,7 +5862,7 @@ Logical SVGA16::Update(Logical forceAll) //Drawing secondary, make sure we got secondary if (secPort != NULL) { - secMask = secPort->GetBitMask(); + secMask = BTSecCompositeMask(renderer, secPort); // sec | overlay (DAC parity) secPalette = &((SVGA16 *) secPort->graphicsDisplay)->palette[secPort->paletteID]; } else {