# Gauge / MFD render pipeline — map + dev-composite plan **Status:** pipeline MAPPED (workflow `gauge-pipeline-map`, 2026-07); the dev composite is **planned, not yet built**. This is the execute-ready plan for making the pod's gauges/MFDs render + be VISUALLY testable on a dev box — the "MFD compositing on dev" follow-up the platform profile gates (see §8 / the PLATFORM PROFILE entry in CLAUDE.md). Companion: the platform-profile scaffold (`-platform pod|dev`). ## The draw model — CONFIRMED (the good news) **CPU-raster → texture-upload, DEVICE-INDEPENDENT.** Every gauge widget (the radar included) is a *software rasterizer* that writes 16-bit R5G6B5 pixels into ONE shared CPU buffer (`Video16BitBuffered::pixelBuffer`, a `PixelMap16`, `L4VB16.h:140`). Logical surfaces are packed by **BIT-PLANE** into that one 640×480×16 buffer (each MFD = one bit mask; the radar/secondary = the low byte, palette-indexed). D3D is touched **ONLY** in `SVGA16::Update` (`L4VB16.cpp:4041`): LockRect a `D3DPOOL_SYSTEMMEM` staging texture → a CPU expand loop (palette-LUT for the radar, case 0; bit-plane→RGB-channel demux for the MFDs, case 1/2) → UnlockRect → `UpdateTexture` staging→`D3DPOOL_DEFAULT` → ONE fullscreen textured quad (`TRIANGLEFAN`) → `Present`, **per surface on its own per-adapter device**. So redirecting the content to a texture on the MAIN device is a small change: the entire raster stays byte-identical; only the upload target + the final blit move. ## The 3 gotchas (why it's a multi-step build, not a one-liner) 1. **DORMANT in BT.** `BTL4GaugeRenderer` passes `L4GaugeRenderer(false, NULL,NULL,NULL)` (`btl4grnd.cpp:151`) → `NUMGAUGEWINDOWS=0`; and `MakeGaugeRenderer` only builds anything if `getenv("L4GAUGE")` (`btl4app.cpp:353`). Both must change to wake it. With 0 surfaces, `SVGA16::Update` may index an empty `mSurfaces[]` → guard needed. 2. **RP↔BT PORT-NAME MISMATCH.** `SVGA16::Update` hardcodes the Red-Planet MFD port names `auxUL2/auxC/auxUR2/auxLL/auxLR` (`L4VB16.cpp:4056-4061`), but BT's `content/GAUGE/L4GAUGE.CFG` names them `Heat/Comm/Mfd1/Mfd2/Mfd3` (:4395-4410). `GetGraphicsPort` returns NULL for the RP names → the MFD branch early-outs ("No MFDs to draw"). **ONLY the `sec`/radar surface matches today** — the 5 MFDs render nothing until the names are reconciled. 3. **CROSS-DEVICE + STATE.** Folding onto the main device means the gauge blit shares the main render state (save/restore around it) + an ORDERING concern: is `l4_application->GetVideoRenderer()->GetDevice()` valid when the gauge renderer builds during `Application::Initialize`? Verify build order or defer texture creation. ## Architecture options (dev) - **(A) `SVGA16` dev-composite branch.** In `SVGA16::BuildWindows`/`Update`, when a dev-composite flag is set, use the MAIN device for the staging/default textures, skip the per-adapter device/window/`Present`; the main renderer composites the DEFAULT textures as insets. Reuses the expand loops verbatim; **branches the pod code** (guard on the flag → inert on pod). - **(B) Bypass `SVGA16`.** Leave the `SVGA16` pod path untouched; add a composite pass in the main renderer that reads the shared `pixelBuffer` + palette directly and runs the expand + inset blit itself. **Best protects the pod path**; re-implements the expand + needs `pixelBuffer`/palette accessors on `Video16BitBuffered`/`SVGA16`. - **RECOMMEND (B)** for the beachhead (pod path untouched); fall back to (A) if reusing the expand loops wholesale is preferred. Decide this with the operator before building — it's the main design fork. ## Beachhead (Step 1) — one surface visible on dev Goal: the **secondary/radar** surface composited as an **INSET** in the existing 800×600 window (no 2nd window yet). It's the one surface that works WITHOUT the port-name fix, so it proves the whole chain end-to-end. 1. **Wake** it via a dev-composite opt-in (e.g. `BT_DEV_GAUGES` env, default OFF so DEV/pod defaults are unaffected; or gate on `-platform pod` on a dev box). When on: set `L4GAUGE` (so `MakeGaugeRenderer` builds) + set the composite flag. Guard `SVGA16::Update` for 0 surfaces (no crash). 2. **Raster**: the widgets already raster into `pixelBuffer` (driven by `GaugeRenderer::ProcessOneActiveGauge`). 3. **Composite**: each frame, in the main renderer's present, upload the secondary-expanded pixels to a main-device texture + draw an inset quad (`XYZRHW|TEX1`), main render state saved/restored. Reach the buffer via `GetGaugeRenderer()`. - **Verify**: the radar/secondary inset appears; DEV default (no `BT_DEV_GAUGES`) un-regressed; pod path untouched; 0 crashes / 0 heap. ## Follow-ups (after the beachhead) - **Step 2** — reconcile the MFD port names (rename in `L4GAUGE.CFG` or in `SVGA16::Update`) → the 5 MFDs render. - **Step 3** — the 2-window layout (3D-view window + an instrument-panel window tiling radar + the 5 MFDs). - Confirm the MFD widget CONTENT is actually fed each frame (entity→gauge routing `BTL4GaugeRenderer::NotifyOfNewInterestingEntity` → the `L4Warehouse` gauge-image bin). ## Key files / hooks - `engine/MUNGA_L4/L4VB16.cpp`: `SVGA16::Update:4041` (the CPU→texture→blit crux; expand loops :4124-4191), `SVGA16::BuildWindows:101` (per-surface device/window/texture — the pod path), `Video16BitBuffered::Draw*` (:761/847/1607/1839, the software raster into `pixelBuffer`), `pixelBuffer` `L4VB16.h:140`, RP port names :4056-4061. - `engine/MUNGA_L4/L4GREND.cpp`: `L4GaugeRenderer` ctor:57 (builds `SVGA16` from `L4GAUGE.INI`), `ExecuteBackgroundDisplayUpdate:369` (calls `graphicsDisplay->Update`), `BuildGraphicsPort:485` (every port shares ONE `pixelBuffer`, bit-plane per port). - `engine/MUNGA/GAUGREND.cpp`: `ExecuteForeground:3556` / `ProcessOneActiveGauge:3836` (draw-all-gauges into `pixelBuffer`). - `game/reconstructed/btl4grnd.cpp`: ctor:151 (the un-dormant point — NULL indices), `NotifyOfNewInterestingEntity:250` (routes `GaugeImageStream` type-0x12 entities to moving/static bins). - `game/reconstructed/btl4rdr.cpp`: `MapDisplay` (the radar; draws into the secondary surface). - Main device: `l4_application->GetVideoRenderer()->GetDevice()` (`L4VIDEO.h:376`). - Config: `content/GAUGE/L4GAUGE.CFG` (port→bitmask map, MechInit :4395-4410), `L4GAUGE.INI` (640×480×16 page). ## Surface → content map (for the 2-window layout) - **Surface 0 = SECONDARY / radar** (640×480, palette-indexed low byte `sec` 0x003F + `overlay` 0x00C0): the radar `MapDisplay` + secondary instruments (message board / heading / speed arc / armor-critical-heat maps). - **Surface 1(+2) = the 5 MFDs** (bit-planes 0x0100–0x8000 of the shared buffer, demuxed to R/G/B channels; pod spans them as one 1280×480): `Mfd1`=LL, `Mfd2`=UC, `Mfd3`=LR, `Heat`=UL, `Comm`=UR (+ `Eng1-3`). - `Plasma` (CFG port 10, `L4PLASMA=com2`) is an EXTERNAL serial annunciator — not a D3D surface. ## Progress log **Milestone A DONE (2026-07) — the gauge renderer is woken + boots STABLY on a dev box** (opt-in `BT_DEV_GAUGES`, default OFF; default DEV + pod paths verified un-regressed — DEV combat DESTROYED, 0 crashes). Option **B** chosen. `BT_DEV_GAUGES` sets `L4GAUGE` (so `MakeGaugeRenderer` builds the renderer) and `SVGA16` does NO per-surface D3D (a file-static `DevGaugeComposite()` gate forces the no-surface path in `BuildWindows` + short-circuits `Update`/`Refresh`). ⚠ Reality-check: `FindBestAdapterIndices` hands the gauge renderer **3 non-null indices even on a dev box** (all the primary adapter), so the earlier "passes NULL → 0 surfaces" assumption was wrong — the gate is keyed on `BT_DEV_GAUGES`, not the count. Waking the (never-exercised) gauge subsystem exposed a **cascade of 4 dormant-path bugs**, each guarded: 1. `SVGA16::Update` (L4VB16.cpp) — indexed empty `mSurfaces[]`; guarded `DevGaugeComposite()||NUM<=0`. 2. `SVGA16::Refresh` — same; guarded. 3. `LBE4ControlsManager::MakeLinkedLamp` (L4CTRL.cpp:2057) — `GetGaugeRenderer()->GetLampManager()` returns NULL (recon gap in `BTL4GaugeRenderer`); guarded (skip the linked lamp when null). 4. `L4GaugeRenderer::NotifyOfNewInterestingEntity` (L4GREND.cpp:440) — `warehousePointer->gaugeImageBin` has a garbage internal chain (recon shadow of the warehouse); guarded (skip the per-mech gauge-IMAGE cache under `BT_DEV_GAUGES`). ⚠ **KEY FINDING: the gauge subsystem's RECONSTRUCTION IS INCOMPLETE.** It was dormant/never-exercised, so these latent recon bugs (lamp-manager offset, warehouse init) only surfaced on waking it. It now BOOTS stably, but whether the widgets raster MEANINGFUL content into `pixelBuffer` is UNVERIFIED until the composite (Milestone B) draws it. The per-mech gauge IMAGES (armor diagrams via the warehouse) are currently skipped; the radar/secondary map + instruments read the mech directly and *may* render partially. So the beachhead's real question shifted from "composite a working buffer" to "how complete is the gauge recon." FAITHFUL FOLLOW-UPS: fix the `BTL4GaugeRenderer` lamp-manager + warehouse reconstruction (shadowed members) so per-mech gauges + lamps work (needed for real MFD content anyway). **Milestone B (NEXT) — the composite pass:** a new `SVGA16` method (has access to `pixelBuffer` + the `sec` port + palette) creates a texture on the MAIN device (`l4_application->GetVideoRenderer()->GetDevice()`), runs the secondary palette-expand (L4VB16.cpp:4124-4147) into it, and the main renderer draws it as an inset quad (state save/restore) after the 3D pass. This is what reveals whether the content is real or garbage.