Files
BT412/docs/GAUGE_COMPOSITE.md
T
arcattackandClaude Opus 4.8 b526e92cad gauges: all 6 cockpit MFD surfaces in a SEPARATE dev window (Milestone C)
Generalizes the single-'sec'-inset (Milestone B) into a full 6-surface
compositor presented in its own top-level window -- the default under
BT_DEV_GAUGES (BT_DEV_GAUGES_DOCK=1 docks it into the main window instead).
A 960x384 window tiles all six pod instrument screens: Heat (COOLANT/BALANCE/
RES + condenser gauges), Comm (KILLS/DEATHS/SELECT TARGET), Mfd1/2/3 (DISPLAY/
PROGRAM/NEAREST frames), and the color radar (SCALE grid + dials + ARMOR
DAMAGE). Main 800x600 3D view is un-occluded; default DEV un-regressed
(TARGET DESTROYED after 8 hits, 0 crashes).

Design (mapped by the mfd-multisurface-map workflow):
- All 6 surfaces are bit-plane MASKS over the ONE shared SVGA16/pixelBuffer
  (sec=palette low byte; Heat=0x4000 UL, Mfd2=0x0400 UC, Comm=0x8000 UR,
  Mfd1=0x0100 LL, Mfd3=0x1000 LR), so the compositor reaches the SVGA16 once
  and extracts each by mask. No port-name reconcile needed on the dev path --
  fetch the BT names directly; the RP aux* names only matter to the pod's own
  SVGA16::Update demux (a deferred pod-only fallback).
- SVGA16::DrawDevSurface: two kernels -- palette-LUT (sec) + mono bit-plane->
  tint ((word&mask)?tint:0). BTDrawGaugeSurfaces iterates a 6-entry table.
- Separate window = one CreateAdditionalSwapChain on the existing device (no
  2nd D3D device). BTGaugeWindowRenderAndPresent (after the main EndScene):
  SetRenderTarget -> SetDepthStencilSurface(NULL) -> Clear -> BeginScene ->
  6 tiles -> EndScene -> restore -> swap->Present.
- KEY BUG fixed: the main 800x600 depth surface stays bound when rendering to
  the 960x384 gauge backbuffer; a bound depth smaller than the RT is invalid
  -> all draws silently fail (clear color, no geometry). Unbind depth (Z is
  off), restore after.

Pod SVGA16::Update/BuildWindows path byte-unchanged; all gated BT_DEV_GAUGES.
Content is still the authored cockpit frames + base-table gauges -- the live
MFD widgets need the BTL4MethodDescription reconstruction (this window is now
the live viewer for that work). Details: docs/GAUGE_COMPOSITE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 11:43:31 -05:00

19 KiB
Raw Blame History

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 0x01000x8000 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 DONE (2026-07) — THE SECONDARY/RADAR SURFACE COMPOSITES LIVE ON A DEV BOX (opt-in BT_DEV_GAUGES; default DEV + pod paths verified un-regressed). The inset in the bottom-left of the 800×600 window renders the authentic BT cockpit secondary MFD: the radar/tactical grid (SCALE/SECTOR + crosshair), the SPEED / HEADING / MAGNETIC / PROP dials, and the color-coded ARMOR DAMAGE mech schematic (green intact / red damaged) — real content (nzSec=27247 non-zero secondary-plane pixels), not garbage. The composite chain: SVGA16::DrawDevInset(device, secMask, paletteID) (L4VB16.cpp) lazily creates a MANAGED R5G6B5 640×480 texture on the MAIN device, palette-expands the secondary plane into it (mirrors SVGA16::Update case 0), and draws an XYZRHW|TEX1 inset quad; the free entry BTDrawGaugeInset(mDevice) reaches application->GetGaugeRenderer()->GetGraphicsPort("sec")->graphicsDisplay and is called from DPLRenderer::ExecuteImplementation as the LAST draw before EndScene (no state save/restore needed).

THREE reconstruction bugs had to be fixed to get from "woken but empty" to "real content" (each a genuine faithful fix or a clearly-gated dev accommodation):

  1. MakeGaugeRenderer OVERRIDE SIGNATURE MISMATCH (real bug — FIXED). The reconstructed BTL4Application::MakeGaugeRenderer() took NO args, but the 2007 WinTesla engine had WIDENED the base virtual to MakeGaugeRenderer(int* secondaryIndex, int* aux1, int* aux2) (called from Application:: Initialize, APP.cpp:382). A no-arg method only HIDES the virtual — it never overrides it — so the engine ran L4Application::MakeGaugeRenderer instead and built a base L4GaugeRenderer whose ctor never calls BuildConfigurationFilegauge\l4gauge.cfg was NEVER PARSED → empty symbol table → the observed GaugeInterpreter: undefined label 'bhk1Init' → no ports, no gauges. FIX: match the 3-arg signature so it truly overrides (args ignored — BT is fullscreen/no-aux, the BTL4GaugeRenderer ctor hardcodes false,NULL,NULL,NULL). Same class of bug as the BTL4GaugeRenderer(false,NULL,NULL,NULL) ctor fix: reconstruction written against the ORIGINAL signature, compiled against the WIDER 2007 engine virtual. (btl4app.cpp / btl4app.hpp.) Label facts: the label = mission->GetGameModel() + "Init" = bhk1Init (lowercase, no capitalization — the binary FUN_0046ff64 does a plain copy + Str_Cat); the CFG defines Bhk1Init but the symbol-table lookup is stricmp (case-insensitive), so the case doesn't matter — the only reason it was "undefined" was the empty table. MechInit (CFG:4390) is the shared macro every <Mech>Init calls that builds the sec/overlay/Mfd*/Heat/Comm PORTS via the base configure primitive.
  2. Gauge-config parse HUNG on undefined primitives (dev accommodation, gated). The BT-specific gauge primitive table BTL4MethodDescription (btl4grnd.cpp) is still a STUB (only the chain-to-base link; the BT gauge widget classes like PlayerStatus aren't reconstructed), so GaugeInterpreter::GetProcedureBody hits an unknown primitive → ReportParsingErrorFail() → a MODAL assert dialog that FREEZES the game mid-parse (cdb confirmed: MessageBoxWabortReportParsingErrorGetProcedureBody). FIX (GAUGREND.cpp, gated BT_DEV_GAUGES): when a primitive doesn't resolve, SKIP its parameter list (paren depth walk + UngetPreviousToken) and continue — the parse then registers ALL labels (Bhk1InitMechInit) and runs the base configure primitives that build the PORTS. The base configure (MakeConfigMethodDescription) IS in L4MethodDescription, so the sec/radar SURFACE builds even though the BT widgets are skipped. Pod keeps the strict Fail (it needs a complete real table).
  3. Gauge widgets AV on NULL data bindings (dev accommodation, gated). Two flavors: (a) value-bound gauges (NumericDisplayScalar) get value_pointer = parameterList[8].data.attributePointer = NULL (the mech data binding isn't reconstructed; the source even comments "(Scalar *) is VERY dangerous!") → the GaugeConnectionDirectOf<float> ctor derefs NULL (gauge.h:198). FIX: the ctor binds a NULL source to a static zero (gated). (b) game-state gauges (RankAndScore::Execute) directly deref unreconstructed mission/player data. FIX: Gauge::Update runs Execute() under a SEH wrapper Gauge::GuardedExecute() (a separate fn — __try/__except can't share a frame with the ChainIteratorOf unwinding local); a gauge that faults once is Disable(True)d (rate=0) so it never retries. Both gated BT_DEV_GAUGES.

⚠ KEY TAKEAWAY: the gauge subsystem is now PROVEN end-to-end on dev (renderer→parse→port config→raster→ palette-expand→composite), and the gauges whose data binds correctly (radar/armor/dials) show REAL content. The remaining work is the BT gauge WIDGET reconstruction — the BTL4MethodDescription method table (the BT gauge classes: PlayerStatus + the ~16 recovered methodDescription entries) AND the gauge→game-state DATA BINDINGS (why RankAndScore / some NumericDisplayScalar resolve NULL). Those are what the dev accommodations (skip/guard) stand in for. Files touched: L4VB16.{h,cpp} (DrawDevInset/BTDrawGaugeInset), L4VIDEO.cpp (the EndScene hook), btl4app.{cpp,hpp} (the override fix), GAUGREND.cpp (tolerant skip), gauge.h + GAUGE.{h,cpp} (NULL-source + SEH guards).

Milestone C DONE (2026-07) — ALL SIX instrument surfaces in a SEPARATE dev window

The beachhead's single-sec-inset is now a full 6-surface compositor in its own top-level window (the default under BT_DEV_GAUGES; BT_DEV_GAUGES_DOCK=1 docks the panel into the main window instead). The 960×384 gauge window tiles all six pod instrument screens with authentic content: Heat (COOLANT/BALANCE/ RES + condenser & temp-leak gauges), Comm (KILLS/DEATHS/SELECT TARGET scoreboard), Mfd1/Mfd2/Mfd3 (DISPLAY/PROGRAM/NEAREST/TEMP-STATUS frames), and the color radar (SCALE grid + speed/heading dials + ARMOR DAMAGE schematic). Verified live: gauge window renders all 6; main 800×600 3D view is un-occluded; default DEV (no gauges) un-regressed (TARGET DESTROYED after 8 hits, 0 crashes).

Architecture (mapped by the mfd-multisurface-map workflow, then implemented):

  • One buffer, six bit-plane views. All gauge ports share ONE SVGA16/pixelBuffer; a "surface" is a MASK over that one 640×480×16 buffer. The compositor reaches the SVGA16 once (via any port) and extracts each surface by its own plane mask. BT plane map (L4GAUGE.CFG MechInit @4395): sec=radar (palette, low byte) · Heat=UL (0x4000) · Mfd2=UC (0x0400) · Comm=UR (0x8000) · Mfd1=LL (0x0100) · Mfd3=LR (0x1000). Eng1/2/3 are the engineering-mode alt planes of UC/LL/LR, not extra monitors.
  • No port-name reconcile needed on the dev path. The RP names auxUL2/auxC/… hardcoded in SVGA16::Update only matter to the POD demux (a deferred pod-only follow-up); our compositor fetches the BT names (Heat/Comm/Mfd1/Mfd2/Mfd3) directly via GetGraphicsPort (stricmp).
  • Two extract kernels in SVGA16::DrawDevSurface: palette-LUT (sec/radar, == Update case 0) and mono bit-plane → tint ((word & mask) ? tint : 0, the reduced core of Update cases 1/2; MFD masks are single bits so no DWORD SIMD/pack). BTDrawGaugeSurfaces iterates a {name,tint,cellRect} table over all 6.
  • Separate window = one ADDITIONAL SWAP CHAIN on the existing device (no 2nd D3D device — textures/verts are shared). BTGaugeWindowRenderAndPresent (called AFTER the main EndScene, before the main Present): GetBackBufferSetRenderTargetSetDepthStencilSurface(NULL) → Clear → BeginScene → 6 tiles → EndScene → restore RT+DS → swap->Present. The pod's own per-surface BuildWindows/Update path is byte-unchanged (dev mode already NULLs those objects).
  • ⚠ THE KEY BUG (cost a cycle): depth-stencil size mismatch. The main 800×600 depth surface stays bound when you SetRenderTarget to the 960×384 gauge backbuffer; a bound depth surface SMALLER than the render target is INVALID → every draw silently fails (window showed the Clear color but no tiles). Fix: SetDepthStencilSurface(NULL) before the gauge draws (Z is disabled anyway), restore after. Symptom to remember: render-to-a-second-swap-chain shows the clear color but no geometry ⇒ check the bound depth size.

Content reality (unchanged from Milestone B): the surfaces show the authored cockpit FRAME art + a few base-table gauges (heat numerics, radar) — the live animated MFD widgets are BT-specific gauge classes not yet reconstructed (parse-skipped). The separate window is now the live viewer for that widget-recon work.

Follow-ups (after Milestone C)

  • Widget recon (the real fix, the big remaining workstream): populate BTL4MethodDescription (give each BT gauge class a methodDescription) + wire the gauge→game-state Execute() data bindings, so the skipped/guarded widgets (map/GeneratorCluster/vehicleSubSystems/pilotList/cmArmor/heat clusters) render live data. The Milestone-C window shows them coming online as they're reconstructed.
  • 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 (one lock/upload); device-reset recreate hardening for the additional swap chain.