# Gauge / MFD render pipeline — map + dev-composite plan **Status (updated 2026-07-12):** the dev composite is **BUILT AND LIVE** (Milestones A/B/C done — BT_DEV_GAUGES / BT_DEV_GAUGES_DOCK; all 6 MFD surfaces composite; 50/50 attr bindings, 0 parse-skips). The header below preserved the original PLAN framing; see the increments + Phase logs for what landed. ## 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 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 `BuildConfigurationFile` → `gauge\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 `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 → `ReportParsingError` → `Fail()` → a MODAL assert dialog that FREEZES the game mid-parse (cdb confirmed: `MessageBoxW` ← `abort` ← `ReportParsingError` ← `GetProcedureBody`). 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 (`Bhk1Init`→ `MechInit`) 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` 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): `GetBackBuffer` → `SetRenderTarget` → **`SetDepthStencilSurface(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. ## Widget reconstruction — the real fix (in progress; mapped by `bt-gauge-widget-recon-map`) **Why the surfaces show only frames:** the BT gauge method table `BTL4MethodDescription[]` (btl4grnd.cpp) was a chain-only STUB, and **no BT gauge class registered a `methodDescription`**, so every BT-specific gauge keyword (`cmHeat`/`headingPointer`/`map`/`vertBar`/`GeneratorCluster`/…) failed table lookup and was parse-skipped. The binary's real table @0x51c910 has 23 gauge entries; 19 keywords are actually invoked by this CFG. The reconstructed classes exist in `btl4gaug/gau2/gau3.cpp` but most have prose-only (undefined) ctors/Execute/connection-feeds — only a few chains are fully real code. **The registration recipe (per widget, mirrors the engine — `NumericDisplayScalar`, L4GAUGE.cpp:542):** 1. Add `static MethodDescription methodDescription;` to the class in its `.hpp`. 2. Define `::methodDescription = { "cfgKeyword", ::Make, { {type,NULL}…, PARAMETER_DESCRIPTION_END } }` in the class's `.cpp`. **The param-type list MUST match the binary's `.data` exactly** — the parser reads one token per entry to the `typeEmpty` terminator (GAUGREND.cpp:1458); a wrong count desyncs the parse. 3. Rewire `Make` to read `methodDescription.parameterList[]` (the interpreter restores each instance's parsed params there before calling Make) instead of placeholder statics; the runtime port arg (`display_port_index`) is the graphics port, NOT a param slot. 4. Register `&::methodDescription` in `BTL4MethodDescription[]` (btl4grnd.cpp), **chain link LAST**. **⚠ THE /FORCE TRAP (the load-bearing gotcha): register a widget ONLY when its whole Make→ctor→feed→every- vtable-slot chain is REAL code.** A registered class instantiates its vtable; any prose-only/undefined virtual (dtor, `BecameActive`, a connection ctor) is `/FORCE`-stubbed to the image base and AVs the moment it's called. The build "succeeds" (exit 0) and lies — **grep the link log for `unresolved external` on the class's symbols** (filter out the known dead `mech3.obj` `DefaultData`/`CreateStreamedSubsystem` offline-factory externals). **✅ Increment 1 — `cmHeat` (ColorMapperHeat) DONE (commit pending).** The first registered BT gauge widget. Pure wiring proved the whole pipeline: added the `methodDescription` (`R,Md,C,St,St,St`, keyword `cmHeat`), rewired `Make` to `parameterList[]`, registered it. Registering it EXPOSED two `/FORCE`-stubbed real functions in the chain (the map had missed them) — reconstructed both from the decomp: `ColorMapper::BecameActive` (@004c395c — invalidates the cached colour index + last RGB so the next Execute re-pushes the palette) and `ColorMapperHeat::~ColorMapperHeat` (empty — base chain releases the HeatConnection + palettes). Verified: register→parse→Make→ctor→`FindSubsystem("GeneratorA")` (resolves — no "does not exist")→HeatConnection→ Execute→palette-push runs end-to-end; combat un-regressed (TARGET DESTROYED, 0 crashes); the sec/heat schematic zones are now tinted by the REAL subsystem `currentTemperature` (stable tint since temp is stable — the visible-animation win is `headingPointer` next). **Method established for every remaining widget.** **✅ Increment 2 — `headingPointer` (HeadingPointer) DONE — the rotating compass + live heading readout.** The visible money-shot: a green needle that rotates with the mech's facing + a numeric heading in whole degrees (render-verified: needle swung down-left→right and the number went 247°→182° as the mech turned). Full class reconstruction from the binary (Make/ctor/dtor/TestInstance/ShowInstance/BecameActive/Execute) — the Ghidra pseudo-C for Execute@004c5914 + ctor@004c562c had the x87 endpoint math dropped, so it was **recovered by disassembling BTL4OPT.EXE** (capstone, `scratchpad/disas_hp.py`). Key facts learned: - **The header ctor arity was wrong (12 vs the binary's 14).** Widened it; fields `@0x98`/`@0x9C` are the needle's inner/outer RADIUS (Execute does `fild [@0x98] ; fmul sin`), not "color/spacing" as named. The needle is a radial line innerRadius(20)..outerRadius(39) at the heading angle; endpoints round half-up (`(int)(r*trig + 0.5f)` == the binary's `fadd 0.5 ; _ftol`). The readout = `round(360 - normalize(deg))`. - **⭐ ENGINE-CONVENTION FIX (empirical, as the map's data-binding agent flagged): the binary read the heading from `EulerAngles` index [0] (`.pitch`), but the WinTesla MUNGA `EulerAngles(Quaternion)` decomposition is AMBIGUOUS for a yawing mech** — the quaternion double-cover flips it to a `pitch=roll=π` branch as the yaw sweeps past ±π, so `euler.yaw` jumps discontinuously (needle spins erratically). Switched to **`YawPitchRoll`** (yaw applied first → `.yaw` is the clean continuous heading, `pitch=roll≈0` throughout). This is a legitimate port of the binary's INTENT, not a stand-in: the original engine's euler put the yaw in [0]; WinTesla's puts it in a different, ambiguous slot, so read the unambiguous decomposition. - `renderer->GetLinkedEntity()` (Renderer::GetLinkedEntity, RENDERER.h:374) resolves the viewpoint mech (the entitySocket IS wired — the map's NULL-socket worry didn't materialize; a `GetViewpointEntity()` fallback is kept belt-and-braces). Deps (`NumericDisplay`, `GraphicsViewRecord`, `GraphicGauge`) are all real engine classes, so no compounding `/FORCE` risk. Verified: no unresolved `HeadingPointer` externals, no parse desync, combat un-regressed (TARGET DESTROYED, 0 crashes). **TECHNIQUE (reusable): recover an x87 float computation Ghidra dropped by PE-parsing + capstone-disassembling the fn (`scratchpad/disas_hp.py`) — read the `fild/fmul/fadd` stream + the `.data` float pool.** **✅ Increment 3 — `cmArmor` (ColorMapperArmor) DONE — the ARMOR DAMAGE schematic, per-zone.** Each of the mech's armor zones on the cockpit schematic is now tinted by that zone's LIVE damage. Render-verified: the schematic shows the Blackhawk silhouette all-green (all zones `damageLevel`=0 at spawn) — which REPLACED the static green+red the schematic showed before cmArmor was registered (i.e. cmArmor now owns those zone colors, showing the real undamaged state; damaged zones shift toward red via the decomp-verified path). Reuses the ColorMapper base (done in incr.1) + adds two pieces reconstructed from the binary + a disassembly of the x87 Ghidra dropped (`scratchpad/disas_hp.py`): - **`ArmorZoneConnection`** (@004c33a4 ctor / @004c3430 Transfer): resolves ONE zone from the owner's inherited `Entity::damageZones[zone_index]`; per-frame feed = `zone==NULL ? 100 : Round(zone->damageLevel @0x158 * 100)` (the damage ratio 0..1 → 0..100 percentage → the colour index ColorMapper::Execute pushes). - **`ColorMapperArmor::Make`/ctor** (@004c3aa4/@004c3b98): `Make` resolves the CFG zone NAME (`dz_ltorso` etc., the 6th param) to an index via `Entity::GetDamageZoneIndex(CString)` — which IS the binary's `FUN_0042076c` (scans `damageZones[]` matching each zone's name @0x15c); the ctor wires the ArmorZoneConnection. - **DECOMP FACT (corrects the cmHeat note): the ColorMapper base ctor `FUN_004c37dc` takes NINE args** — there is an `owner_ID` between `renderer` and `graphics_port_number` (`Gauge(rate,mode,renderer,owner_ID,id)` + `colorSlot=param_7`, `graphicsPort=GetGraphicsPort(param_6)`). Our 8-arg `ColorMapper::ColorMapper` folds `owner_ID=0` in (gauges have owner 0), so it's equivalent — cmArmor's mapping matches cmHeat (gpn=port, colorSlot=p[2], palettes=p[3]/p[4]) plus the zone name (p[5]). Verified: parses, builds, **all `dz_*` zones resolve (0 "not found")**, no `/FORCE` unresolved, combat un-regressed (TARGET DESTROYED, 0 crashes). NOTE: the dynamic red-on-damage needs the PLAYER to take damage — the passive `BT_SPAWN_ENEMY` dummy never hits back, so a live demo of a zone reddening needs a player-damage path (real MP combat, or a test hook). ⚠ The sibling `colorMapperMultiArmor` (worst-of-8-zones, MultiArmorConnection @004c346c) + `cmCrit` (ColorMapperCritical, CriticalConnection @004c3598) are the same shape — easy follow-ups now the pattern + the ColorMapper base facts are pinned. **✅ Increment 4 — `colorMapperMultiArmor` DONE — worst-of-N-zones tint.** Tints one schematic colour (a whole torso SECTION) by the worst of up to 8 damage zones. Reconstructed `MultiArmorConnection` (@004c346c ctor / @004c34f4 Transfer: scan 8 zones via `entity->damageZones[idx]`, keep max `damageLevel`, `count==0 ? 100 : Round(worst*100)`) + `ColorMapperMultiArmor::Make`/ctor (@004c3c48/@004c3d60 — resolves 8 zone names to indices via `GetDamageZoneIndex`, 13-param methodDescription). Verified: parses, builds, all zones resolve, combat un-regressed (TARGET DESTROYED, 0 crashes). **⭐⭐ THE `interpreterTable` OVERFLOW — a latent heap-corruption fixed (every future widget needed this).** Registering colorMapperMultiArmor CRASHED — but not in the gauge code: a heap corruption surfaced later in mission bitmap loading (`ObjectNameList::AddEntry` → malloc AV). ROOT CAUSE: `GaugeInterpreter`'s bytecode buffer is a FIXED `interpreterTableSize = 46864` (GAUGREND.h) tuned for RP's config, and its `Insert()` bounds guard is a `Verify()` that COMPILES OUT at DEBUG_LEVEL 0 (the same dead-`Verify` class as the BNDGBOX/heat bugs). As each BT gauge widget is registered, more of BT's (larger) L4GAUGE.CFG resolves into bytecode instead of being parse-skipped; cmHeat+headingPointer+cmArmor got close, colorMapperMultiArmor (13 params × ~hundreds of calls) tipped it past 46864 → silent overflow past the `char[]` → heap smash detected at the next big alloc. FIX: `interpreterTableSize` → 262144 (sized for BT's full config). ⚠ This was going to block EVERY further widget — the table is cumulative across all registered gauges. **LESSON (add to the checklist): a heap corruption that surfaces in an INNOCENT later alloc, right after registering a gauge, is the interpreterTable overflow — the Insert bounds-`Verify` is dead at DEBUG_LEVEL 0.** **✅ Increment 5 — `cmCrit` (ColorMapperCritical) DONE — the ColorMapper family is COMPLETE.** Tints a schematic colour by a subsystem's operational state (the "critical" secondary display mode). Reconstructed `CriticalConnection` (@004c3598/@004c3610: `src==0 → 0`; `src->simulationState==1` [DestroyedState] `→ 100`; else the subsystem's own damage-zone `damageLevel × 100`) + `ColorMapperCritical::Make`/ctor (@004c3ddc/ @004c3e40 — `FindSubsystem` by name + wire the CriticalConnection). **Resolved the subsystem `damageZone@0xE0` shadow:** the recon declared `MechSubsystem::damageZone` as a `ReconDamageZone*`, but the assignment (`mechsub.cpp: damageZone = (ReconDamageZone*)new DamageZone(...)`) shows the pointer IS a real engine `DamageZone` — so cast it back and read `damageLevel`. Added two public accessors to `MechSubsystem` (`GetSimulationState()`, `GetDamageZoneProxy()`) since those fields are protected (mirrors how HeatConnection reads the public `currentTemperature`). Verified: parses, builds, all subsystems resolve (0 warnings), no `/FORCE` unresolved, combat un-regressed (TARGET DESTROYED, 0 crashes), stable. **Next increments (priority order from the map):** the `ArmorZoneConnection`/`MultiArmorConnection` classes reconstructed); then the attribute-table wave (`vertBar`/ `segmentArcRatio` speed/`GeneratorCluster` — need `AttributePointers[]` on Mech/HeatableSubsystem, a separate pass); the XL items (`map`/`vehicleSubSystems`/`PlayerStatus`) last. The POD path needs the FULL 23-entry table with real ctors (the dev tolerant-skip is the on-ramp, not the finish). ## Attribute wave DONE (2026-07) — the cockpit numeric/bar/arc gauges read LIVE data The gauge widgets bind to game state by NAME through the engine attribute-pointer system, mapped by the `attr-wave-decomp-map` workflow and implemented as 4 committed increments. **This is the mechanism every data-driven gauge depends on — read it before reconstructing any further widget.** ### How a gauge binding resolves (engine, already complete) The config binds either `Subsystem/Attribute` (e.g. `HeatSink/CurrentTemperature`) or a bare `Attribute` (e.g. `LinearSpeed`). `GAUGREND.cpp ParseAttribute` (~2130) splits on `/`: - `Subsystem/Attr` → `entity->FindSubsystem(subName)` (ENTITY.cpp:631 — `stricmp` on each subsystem's `GetName()`) → `subsystem->GetAttributePointer(attrName)`. - bare `Attr` → `entity->GetAttributePointer(name)`. `Simulation::GetAttributePointer(name)` (SIMULATE.cpp:427) = `GetSharedData()->activeAttributeIndex-> Find(name)` then `&(this->*attr)`. ### What a class must publish (the per-class work) 1. An `AttributeID` enum chained from the parent (`= Parent::NextAttributeID … NextAttributeID`). 2. `static const IndexEntry AttributePointers[]` via `ATTRIBUTE_ENTRY(Class,Name,member)` (SIMULATE.h:284). 3. `static AttributeIndexSet& GetAttributeIndex()` chained to the parent (pattern: MOVER.cpp:83). 4. The class's `DefaultData`/`SharedData` ctor must PASS `GetAttributeIndex()` (MOVER.cpp:28-35) — this sets `activeAttributeIndex`. Bind `ATTRIBUTE_ENTRY` to a REAL named member (compiled ptr-to-member; NEVER a raw binary offset — the compiled layout != binary). The engine `Mover`/`JointedMover`/`Entity` tables are dense and real; chain to them. ### ⚠ DENSE-TABLE HAZARD (systemic-bug class — verified in SIMULATE.cpp:565/663) `AttributeIndexSet::Build` sizes the merged index to `max(entryID)`, copies the inherited slots, places own entries at `[id-1]`, and **leaves gap slots uninitialized** (`new IndexEntry[n]` on a POD = garbage `entryName`). `Find(name)` then `strcmp`s EVERY slot → a gap between the parent's `NextAttributeID` and the highest id you publish = strcmp on a garbage pointer = AV. So a published table MUST be a **dense prefix** from the parent's `NextAttributeID` up to the max id it includes. You cannot ship just the ids you need: fill the gaps (bind not-yet-needed ids to a shared read-only pad member). ### Base engine primitives vs BT-specific widgets (don't reconstruct the base ones) - **Engine base primitives (already registered + drawing; only need the DATA):** `numeric`, `numericSpeed`, `digitalClock`, `rankAndScore`, `bgPixelMap`, `bgBitMap` (L4GAUGE.cpp). `digitalClock`'s `time` arg is a FORMAT enum, NOT an attribute — the value comes from the mission clock inside the widget. - **BT-specific (need reconstruction + registration in `BTL4MethodDescription[]`):** `vertBar`, `segmentArcRatio`, `oneOfSeveralPixInt`, `GeneratorCluster`, `map` (radar), `PlayerStatus`, `vehicleSubSystems`, plus the done ColorMapper family + headingPointer. ### Committed increments (each verified live in the dev gauge window; combat un-regressed; heap-clean) 1. **HeatSink attribute table** (heat.{hpp,cpp}) — `CoolantMass`→coolantLevel, `CoolantCapacity`→ thermalCapacity, `CurrentTemperature`→currentTemperature. Condenser/Reservoir inherit it (both derive from HeatSink). → Heat surface temp readout `S` now shows **77** (was 0). 2. **Mech attribute table** (mech.{hpp,cpp}, mech4.cpp) — full enum 0x15..0x38 declared; DENSE PREFIX 0x15..0x21 published (gap ids share `attrPad`; `CurrentSpeed`→legCycleSpeed, `MaxRunSpeed`→ reverseStrideLength, `LinearSpeed`→new `linearSpeed` member set to |adv|/dt per frame). Chained to `JointedMover::GetAttributeIndex()`. → radar SPEED readout now shows **~225** (was 0). 3. **vertBar** (VertTwoPartBar, btl4gaug) — the COOLANT bars. x87 pixel math recovered by disassembly (`FUN_004dcd94`=`(int)ROUND(ST0)`): `warnPix/valPix = clamp(round(height*low_or_value/high + 0.5),0, height)`; tile-blit `[0,warnPix)`, fill `[warnPix,valPix)`, clear `[valPix,height)`. 4. **segmentArcRatio** (SegmentArcRatio, btl4gaug) — the SPEED arc. Thin subclass of engine `SegmentArc`; Execute override (`FUN_004dcd00`=fabs): `currentValue = clamp(|num/den * segmentSpan|,0,1)` then `SegmentArc::Execute()`. `segmentSpan = (Scalar)(|n|/(|n|-1))*0.75f` (int division → 0.75 for 36 segs). 5. **oneOfSeveralPixInt** (OneOfSeveral base + PixInt, btl4gaug) — the button-state LAMPS. Reconstructed the whole OneOfSeveral family (strip selector: `col=selected%columns, row=selected/columns` → blit that sub-rect via `DrawPixelMap8`(pixmap)/`DrawBitMapOpaque`(bitmap); strip resource via `warehouse->pixelMap8Bin`). PixInt forces the PixMap path + one `GaugeConnectionDirectOf` → the frame index; added the missing `OneOfSeveral::foregroundColor`@0x9C. Config binds duck/searchlight/ display-mode/piloting-mode buttons; `ControlsMapper/DisplayMode` resolves live, the rest degrade to frame 0 until their tables land. 6. **map** (MapDisplay, btl4rdr) — the RADAR / tactical display (the marquee). The renderer was already reconstructed (Execute phases: bounds → gather static/moving entities → DrawViewWedge/DrawStatic/ DrawMoving/DrawNames); only the registration glue + data were missing. Extended the Mech table to the full 0x15..0x38 (RadarRange/RadarLinearPosition[Point3D*]/RadarAngularPosition[Quaternion*]/DuckState; `AttributePointer`=`int Simulation::*` so the ATTRIBUTE_ENTRY reinterpret binds pointer members), published the Sensor("Avionics") RadarPercent table, and reconstructed `MapDisplay::methodDescription`+ `Make` from the config (the "center"/"bottom" enum keyword typed as a STRING + converted in Make → no ModeManager named-constant needed). Renders the view wedge (mech FOV cone) live. THREE pre-existing stubs the map was the first consumer of, each fixed: Sensor `radarPercent` went negative (un-normalized `heatEnergy` in the not-byte-exact heat-leaf → guarded to no-penalty); `ResolveOperatorEntity` returned NULL (→ viewpoint fallback); `MapName::ExtractFromEntity` didn't NULL-guard the entity. ⚠ Debug via `BT_MAP_LOG` (phase/draw trace). **CONTACTS NOW RENDER** (2026-07): two more fixes landed the enemy blip inside the FOV wedge. (a) The gauge renderer's entity grid was NEVER populated — the port dropped `ExecuteImplementation`'s InterestingEntity feed; added `GaugeRenderer::RebuildEntityGrid()` (engine) that fills `movingEntities` from the world's DynamicMaster+Replicant entities (the vehicles; AllEntity floods it with ~311 cls-0x5E props), called from the map's guarded Execute; DrawMoving draws a cross+box blip for cls-0xBB9 mechs (the pip table is stubbed). (b) The blip PROJECTION was wrong — `worldToView` is a camera matrix whose baked scale is wrong for a point transform (a 120u contact projected to 167px not 44px); fixed by projecting `delta = entity - viewpoint` directly (rotate by −heading, scale by pixelsPerMeter). Verified: the 120u enemy (dsq=14400, cls 0xBB9, own=0) plots at (0,44) inside the wedge. FOLLOW-UPS: the authentic pip symbol/name/video-object infrastructure (`GetVideoObject`/`LookUpPip`/`GetNameID`, still stubbed → the cross-blip is the stand-in); the blip-vs-wedge rotation convention (reads ahead/in-wedge, correct); `SetTargetRange` (radar zoom, 1km default). ### Reconstruction-technique additions (durable) - **x87 float math Ghidra drops** (`FUN_004dcd94()`/`FUN_004dcd00()` show no args = FPU-stack operands): disassemble the fn with `tools/disas2.py ` (capstone + PE parse; annotates `.data` float constants + known call targets). `FUN_004dcd94`=round-to-int, `FUN_004dcd00`=fabs. - **Find a vtable override the assert-anchored decomp didn't export** (e.g. SegmentArcRatio::Execute wasn't in `part_*.c`): dump the class vtable with `tools/vtdump.py `; a slot pointing into the module's own code range (btl4gaug = 0x4c2f94..0x4c6771) is the override → disassemble it. - **GraphicsView method vtable map** (GRAPH2D.h): +0x08 SetPositionWithinPort, +0x10 SetOrigin, +0x18 SetColor, +0x24 MoveToAbsolute, +0x38 DrawThickLineToAbsolute, +0x48 DrawFilledRectangleToAbsolute. Resource fetch = `renderer->warehousePointer->bitMapBin.Get/Release(name)` (FUN_00442aec/00442c12). ### Deferred / follow-ups - ✅ `HeatSink/AmbientTemperature` — LANDED (AggregateHeatSink 0xBBE reconstructed, commit 16f5f54; cleared the last NULL binding; the bank is also the ambient radiator since task #9). - ✅ Reservoir shadow — LANDED (heatfamily_reslice.cpp now writes thermalCapacity from the resource coolantCapacity; the shadow member is gone). - ✅ **oneOfSeveralPixInt** DONE (increment 5). The Mech table now reaches 0x37 so `DuckState` resolves; `Searchlight/LightOn` still needs a Searchlight `LightOn` table to make that button dynamic. - ✅ **map** DONE (increment 6) — view wedge + live contact blips; ✅ `SetTargetRange` LANDED (radar zoom). REMAINING: the stubbed pip/name/video-object symbology (contacts draw the cross-blip stand-in, no labels) — under reconstruction (task #17, 2026-07-12). - ✅ Remaining widgets — BOTH LANDED: `PlayerStatus` (increment 7, commit b691863) and `vehicleSubSystems` (see docs/VEHICLE_SUBSYSTEMS.md Phase 2). ### Remaining widgets — scope (assessed 2026-07; NOT quick registration wins) - ✅ **PlayerStatus** DONE (increment 7; commit b691863) — reconstructed the whole family (mapped by the `playerstatus-decomp-map` workflow): methodDescription + rewired Make (fixed the port/x/y bug), ctor/dtor/ BecameActive/TestInstance/Execute, PlayerStatusMappingGroup (28 ColorMapperArmor zone tints, reusing the EXISTING 10-arg ColorMapperArmor ctor), + ColorMapper/Armor::SetColor. **KEY FINDING: PlayerStatus lives in the config's `cameraInit` block (the MISSION-REVIEW / spectator camera cockpit), NOT `MechInit`** — so it is NOT part of the mech cockpit and does not build during normal mech play (Execute never runs, mech cockpit un-regressed). It is the post-mission scoreboard rendered by the BTCameraDirector game model. Registered so `cameraInit` builds it. Build/link-safe + mech-cockpit un-regressed, but NOT runtime- verified (a normal `vehicle=` egg never triggers the camera model). BRING-UP STUB: CreateMutant Pixelmap8 returns NULL (the mech-outline recolor needs the DynamicMemoryStream read API + Pixmap pixel-copy mapped) → the score/name-box/status-box render but not the recoloured mech schematic. Player resolve uses `application->GetMissionPlayer()` (WinTesla-clean) instead of the binary's raw App+0x24 "Players"-node walk. - ✅ **vehicleSubSystems** — LANDED (docs/VEHICLE_SUBSYSTEMS.md: 7 authentic engineering panels with real aux-screen positions via the BTGetSubsystemAuxScreen bridge). The from-scratch scoping note above is historical. --- ## Gauge data-binding wave (2026-07) — publish attributes + reconstruct unbuilt widgets Goal (user directive: "all of this needs to work as the game originally played. no shortcuts"): make every cockpit gauge read + reflect LIVE game state exactly as the 1995 pod. The `gauge-databinding-map` workflow (5 surface maps + 2 adversarial verifies) mapped every binding → subsystem/member → published-vs-missing → authentic value → fix. **[SUPERSEDED 2026-07-11, tasks #9/#10: the reframe below was computed at the DEGENERATE bring-up heat scale. At the authentic 1e7-unit economy the Heat MFD is FULLY DYNAMIC — weapons 77→2000, condensers/generators 100-1400, bank ~600; see context/gauges-hud.md.]** The original (superseded) reframe read: the Heat MFD is authentically NEAR-STATIC — `currentTemperature = heatEnergy/thermalMass`; ThermalMass=1.39e6, so heatEnergy_init = 1.07e8 whose float32 ULP is 8.0 — a 3.5-heat laser shot ROUNDS TO ZERO (reaching Degradation 5000 needs ~2 billion shots). Coolant is DAMAGE-gated (not fire), the valve moves only on player toggle, AmbientTemperature is a hardcoded 300. So the wave is **"publish attributes + reconstruct unbuilt widgets + wire the 2 damage-driven formulas + the valve", NOT "revive a dynamic heat model"**. Diagnostic infra: `BT_GAUGE_ATTR_LOG` (GAUGREND ParseAttribute logs `[attr] OK/NULL` per config binding). ### Phase 1 — PUBLISHING (DONE; commits 070af40, 64975ec) Most gauges resolved NULL because the reconstructed subsystems published only a fraction of the bound attributes. All ids kept a dense prefix from each parent's `NextAttributeID` (AttributeIndexSet::Build strcmps every slot — a gap AVs). Done: - **HeatSink table** dense-append: DegradationTemperature@0x118 / FailureTemperature@0x11C (the condenser temp-bar warn/max endpoints — were NULL so the two-part bars couldn't scale), NormalizedPressure→heatLoad, DegradationPressure→coolantEfficiency, CoolantMassLeakRate→coolantDraw, HeatSink→linkedSinks. Condenser/ Reservoir inherit it → all 6 condenser temp bars resolve. - **PoweredSubsystem::GetAttributeIndex()** (new) publishes InputVoltage→voltageSource@0x1D0 — the cluster power-branch gate (SubsystemCluster ctor skips the power-lamp/generator-voltage/state-lamp sub-branch when it resolves NULL). Flows to Sensor/Myomers/weapons automatically (they chain to it). - **MechWeapon::GetAttributeIndex()** (new) publishes OutputVoltage/PercentDone→rechargeLevel@0x320; Emitter/PPC/ProjectileWeapon/MissileLauncher/GaussRifle DefaultData re-pointed at it (they carried an EMPTY default-constructed index → resolved NOTHING). **VERIFIED: the ER-MED-LASER/PPC/STREAK weapon clusters now render live recharge dials (SegmentArc270) — were blank TEMP/STATUS.** - **Searchlight** publishes "LightOn"→lightState@0x1D8 (config binds the NAME "LightOn"; enum renamed LightOnAttributeID). **Mech::SetTargetRange** un-stubbed (radarRange=range) → radar map scale + range readout track the mapper's zoom. - **FIXED a genuine recon bug** (heat.cpp UpdateCoolant + heatfamily_reslice RefrigerationSimulation): both read `linkedSinks.Resolve()->heatEnergy` where the binary reads `*(this[0x38]+0x158)` = the subsystem's OWN DamageZone.damageLevel (@0xE0). The misread would SLAM the live CoolantMass bars to empty (coolantDraw = master.heatEnergy~1.3e7 * heatLoad) and clamp massScale permanently to 1.0. Now reads `this->Subsystem::damageZone->damageLevel` (qualified past the MechSubsystem shadow) → 0 undamaged → no leak / massScale 3.0 = authentic pristine. **After P1 every config-string binding resolves OK except HeatSink/AmbientTemperature** (needs the aggregate bank, P3). ### Phase 2 — WIDGET RECONSTRUCTION (COMPLETE — every config gauge primitive registered; [gskip]=0) These config keywords were UNREGISTERED in `BTL4MethodDescription[]` (btl4grnd.cpp) so their lines parse-skipped and the gauge never built (even after publishing). The `gauge-widget-decode` workflow reconstructed each from the binary; registered widgets need a REAL Make/ctor/Execute/dtor (the /FORCE trap: a prose-only vtable slot AVs on first call). - ✅ **LeakGauge** (== BitMapInverseWipe @4c5b7c, config @6) — the class body already existed; added the Make factory + methodDescription("LeakGauge") + registration. The heat panel's LEAK column now builds (empty undamaged = authentic). - ✅ **VertNormalSlider** (condenser valve slider @2; vtable PTR_FUN_00518c34) — reconstructed all 7 fns (part_013.c:14051-14175; Make @004c4b08 by disassembly), fixed the jumbled header layout, published ValveSetting→coolantFlowScale@0x15C. The SET valve indicator renders (row=Round(span*value)). - ✅ **PilotList** (Comm KILLS/DEATHS roster; part_014.c:3156-3434, DAT_0051af88 8×3 layout PE-parsed) — the Comm surface now renders the live local-pilot row (KILLS 0 DEATHS 0 + name box). Feed = new `BTResolveRosterPilot` bridge (mechmppr.cpp) → mapper roster. DEATHS redirected from the dead pad_0x280 to the real deathCount@0x200. The scoring feed that moves KILLS/DEATHS is P3. - ✅ **GeneratorCluster** (4 generator panels, buttons 9-12) — the 4 panels (A/B/C/D) render with blue OutputVoltage bars (the @004c72ac ScalarBarGauge Scalar* variant + the Generator OutputVoltage table), labels, and status lamps. **Root cause of the initial abort (cdb-attach to the frozen dialog):** the engine base `Gauge::Execute` is `Fail("not overridden")`→abort (GAUGE.cpp:598), so an ACTIVE container that doesn't override Execute aborts by design (GuardedExecute's SEH can't catch an `abort()`). The decode's "overrides ONLY the dtor / renders via children" was wrong — the 1995 base Gauge::Execute was a no-op, the 2007 engine's aborts. FIX: `BecameActive()` (blit the background pixmap + don't self-inactivate) + `Execute()` (no-op; the 5 self-registered children draw the dynamic content), mirroring the sibling SubsystemCluster. **⚠ LESSON: a reconstructed container gauge MUST override Execute (+ a non-inactivating BecameActive) — the 2007 `Gauge::Execute` base aborts.** - ✅ **SectorDisplay** (radar SECTOR X/Z read-out, Secondary overlay; commit 4a4ec68) — was PROSE-ONLY (placeholder Make). Reconstructed byte-verified from the disassembly (ctor @4c9e10, Execute @4ca07c, methodDescription PE-parse): `SectorDisplay : GraphicGauge` (0xC4), overrides LinkToEntity(9)/BecameActive(3)/ Execute(16). Execute reads the linked mech's `localOrigin` → two 100-unit sector numerics `numericA=Round(-Z*0.01)+500`, `numericB=Round(X*0.01)+500` (Round=FUN_004dcd94, corrected from a "truncate" claim; 0.01 + -Z/+X asm-verified). **VERIFIED LIVE (BT_SECTOR_LOG): Make port=1 (overlay), Execute -Z=960/X=362 → sectorA=510 sectorB=504.** - ✅ **PrepEngrScreen** (per-engineering-screen static label overlay, 12 CFG calls; commit c0d2eb0) — was a MISLABELED stub (base Gauge/Execute/mech+screenNumber swapped). Corrected (vtables.tsv proof): base `GraphicGaugeBackground` (paint-on-activation, only dtor+BecameActive overridden — NO Execute virtual, so the Fail→abort hazard doesn't apply), screenNumber@0x6C/mech@0x70. BecameActive walks the roster (safe accessors + BTGetSubsystemAuxScreen), finds the subsystem whose auxScreenNumber==this screen (1..12), paints its labels dispatched on GetClassID. **All 12 build; composite un-regressed.** - ✅ **MessageBoard** (secondary-MFD comm/status ticker; commit 7fc4acb) — was PROSE-ONLY; the LAST parse-skip. Reconstructed (Make @4cb678 … Execute @4cb82c, 0xA4); fixed 3 header mislabels (enabled→trackedMech; previousMessageId/previousNameId swapped). **DEFERRED/EMPTY by design (authentic for bring-up):** the source is never bound (SetSource has no caller) AND the status queue (StatusMessagePool, btstubs.cpp:62) is a NULL stub → no messages → Execute early-returns (safe no-op). Data via a `BTResolveMessageBoard` /FORCE-safe bridge (btplayer.cpp). **After this, the parse-skip list is EMPTY.** **⭐ GAUGE SYSTEM COMPLETE (registration + binding):** all 50 config attribute bindings resolve (`[attr]` 0 NULL — AggregateHeatSink cleared the last, `HeatSink/AmbientTemperature`), and every config gauge primitive is registered + built (`[gskip]`=0). Diagnostic `BT_GAUGE_SKIP_LOG` (GAUGREND.cpp, gated) logs each unregistered primitive the dev-parse skips — the tool that pinned the last 3 (⚠ the gauge renderer builds LAZILY, so verification runs must wait long enough for BuildConfigurationFile; a too-early kill shows [gparse]=0 and looks like "not built"). Remaining gauge work is DATA FEEDS, not widgets — [UPDATE 2026-07-12: the 0xBD3 untangle landed (task #7) and the valve CONTROL landed (task #13, live MoveValve via the rookie-role gate); only the message half remains]: the status-message queue (StatusMessagePool) — deferred systems, cleanly marked. [UPDATE 2026-07-19 Gitea #11: `SeekVoltageGraph` (@004c6934, a cluster-child not config-called) is now FULLY reconstructed and its Seek* attributes published on Emitter+Myomers — see the AUDIT finding A addendum below.] ### Phase 3 — SIM/MODEL - ✅ **Aggregate HeatSink-bank** (0xBBE) — DONE (commit 16f5f54). Reconstructed `AggregateHeatSink : HeatSink` (ctor @4ae8d0, GUID 0x50e590), byte-exact + static_assert-locked (heatSinkCount@0x1D0, ambientTemperature @0x1D4=300, helper@0x1D8, sizeof 0x1E4). Publishes HeatSinkCount + AmbientTemperature via a dense-prefix table chained to HeatSink::NextAttributeID. CreateHeatSinkBankSubsystem moved to heatfamily_reslice.cpp, builds the real class at 0xBBE. **DELIBERATE DEVIATION:** keep the base HeatSinkSimulation; do NOT install the authentic Performance @4ae73c (it derefs a raw self+0xE0→[+0x158] that doesn't map in our layout → AV/NaN for EVERY mech; ambientTemperature is a frozen constant so the gauge reads 300 either way). Clears the LAST config-binding NULL: **`[attr] HeatSink/AmbientTemperature OK`, all 50 bindings resolve (0 NULL).** - ✅ **Condenser valve gauge** — DONE (commits b91057a, aae3ce2). Two coupled fixes: (1) **Dedup** — heat.cpp's STUB Condenser ctor/dtor/TestClass/TestInstance/CreateStreamedSubsystem ODR-duplicated the REAL reslice bodies and WON under /FORCE → the real ctor (valveState=1) was shadowed, valveState=0xCDCDCDCD. #if 0'd the heat.cpp stubs (kept DefaultData/GetClassDerivations/ResetToInitialState). (2) **RecomputeCondenserValves** (FUN_0049f788) — `FinishConstruction()` at the Mech ctor tail was a no-op stub → coolantFlowScale never written (stayed 0). Reconstructed it (coolantFlowScale_i = valveState_i / ΣvalveState + condenserAlarm change-pulse), wired at the ctor post-init pass. **Verified: [valve] the BLH's 6 condensers each read flow=0.166667 (=1/6).** ValveSetting→coolantFlowScale now authentic. (3) **MoveValve** — the reconstructed @4afbe0 body was a MISLABEL (MechControlsMapper mode cycler); disassembled the real handler @4ae464 (valveState cycle 1→5→50→0 + RecomputeCondenserValves) and replaced it. [UPDATE 2026-07-12: LIVE (task #13) — the guard was never the 0xBD3 messmgr; FUN_004ac9c8 is the owning player's ROOKIE-ROLE lockout (roleClassIndex@0x274==0, task #12 bridge). MoveValve is registered (id 4, table @0x50E52C), verified valveState 1→5 / flow 1/6→5/10; 'C' key + BT_VALVE_SLOT.] - ✅ **Combat SCORING feed** (KILLS/SCORE) — DONE (commit 34aaa7d; scoring-feed-decode workflow). The scoreboard read 0 for 4 reasons (none was handler logic — the ScoreMessage/VehicleDead/ScoreInflicted handlers were already reconstructed): (1) no producer — added the posts (per-hit ScoreInflicted at the beam/projectile dispatch; KillScore at the TARGET-DESTROYED edge; senderMechID = the VICTIM so the local player is credited via the !=our-mech branch; dispatched to GetMissionPlayer()); (2) cross-family Mech offsets → GetPlayerLink() + tonnage/bias stubs; (3) NULL scenarioRole → CalcInflictedScore returns the neutral (damage+bias); (4) ownerless-dummy null-guards. Plus the **databinding**: the PilotList read KILLS/DEATHS at raw offsets → repointed to the compiled members via BTPilotKills/BTPilotDeaths bridges. **⚠ ROOT-CAUSE of the empty scoreboard: the PilotList SELECT-TARGET highlight did a raw-offset deref (`*(local+0x284)` then `*(tgt+0x190)`) that AV'd on our layout — caught silently by the SEH GuardedExecute, aborting Execute BEFORE the KILLS/DEATHS draw. Replaced with BTPilotIsSelected (accessors).** And the 10s console flush now only zeros currentScore when a console host exists (solo keeps the running score). **Verified: SCORE climbs +5.83/hit → 164; on the kill the Comm roster shows KILLS=1 / DEATHS=0, log killCount=1 deaths=0 score=164 rank=0.** Env: BT_SCORE_LOG. - ✅ **DEATHS + RANK fixed** (commit a9210e0). RANK=-1: `Player::DefaultFlags` creates every player NonScoring (CalcRanking ranks only `IsScoringPlayer()`); call `SetScoringPlayerFlag()` in the DropZoneReply mech branch → RANK=0. DEATHS runaway: `@004c012c` (our VehicleDead MESSAGE_ENTRY) is actually the drop-zone **respawn-retry** helper — the engine's RequestDropZone (PLAYER.cpp:400) posts a 2s-delayed VehicleDead(deathCount=-2) timer, and our handler ++deathCount + **re-posted it unconditionally** → an infinite loop (the drop-zone handshake never completes in bring-up). Fix: if the player already has a vehicle (drop zone acquired), the retry is moot → return without counting/re-posting → DEATHS=0, loop gone. MP real-death DEATHS: RESOLVED 2026-07-12 (the observed-death tally). Each node scores every pilot from LOCALLY OBSERVED events (the same model as the cross-pod KILLS credit): a REPLICANT mech's once-per-death transition tallies onto its owning player's local copy (BTPlayerCountObservedDeath, btplayer.cpp; call site mech4.cpp death transition, replicant-gated against double-counting the master's own VehicleDead path). The display bridge BTPilotDeaths clamps the engine's -2/-1 pre-acquire seed to 0 (a remote player's copy never runs the local acquire that zeroes it -- the raw negative was the user-visible "DEATHS -1"). - **duckState** writer (button-4 crouch lamp; only ctor-zeroed today). ### Phase 4 — DEEP/DEFERRED Byte-exact re-base of the PoweredSubsystem:HeatSink LEAF (Sensor/Emitter/Myomers) → radarPercent heat penalty (delete the sensor.cpp:287-288 guard — ✅ LANDED: the guard is deleted and the leaf is byte-exact, sensor.cpp:407-413) + the raw-offset cluster reads (partially locked since — MechWeapon byte-exact); radar pip/name symbology + staticEntities (task #17, under reconstruction 2026-07-12); CycleDisplayMode→ModeManager mask (✅ RESOLVED 2026-07-19, Gitea #6 [T2]: @4d1ae4 relabeled the NotifyOfDisplayModeChange override (vtbl+0x4C) and made virtual; desktop 'N'/pad RightThumb → CycleDisplayModeNow; secondary schematic swap dama→crit→heat pixel-verified via BT_DEV_GAUGES_DOCK+BT_SHOT; pod buttons = streamed EventMappings 0x15→msg 0x15 / 0x18→msg 0x14); numericSpeed units (open). ### Authentically-static (do NOT "fix") [currentTemperature "~77 static" REMOVED 2026-07-11 — dynamic at the authentic heat economy, tasks #9/#10.] Degradation/Failure temps (fixed threshold markers); AmbientTemperature 300; MaxRunSpeed (per-mech constant); cmArmor/cmCrit all-green (undamaged solo player — BT is PvP-only); CoolantMassLeakRate 0 on a pristine mech (damage-gated); Player RANK 1 in solo. --- ## AUDIT 2026-07-19 (Gitea #10) — systematic per-widget gauge-data audit Every widget on the cockpit displays audited for BINARY-CORRECT data: static pass (CFG configure blocks + `BTL4MethodDescription[]` + widget sources vs the decomp) + live pass (two instrumented sessions, BLH/ARENA1, docked composite + BT_SHOT series; drivers: BT_PRESET_TEST, BT_VIEWCYCLE_TEST, BT_FLUSH_TEST, BT_AUTOFIRE(+AF_MISSILE), BT_GOTO movement; logs audit2/3/4.log). Verdicts: **CORRECT** (binary-correct data, evidence cited) / **WRONG** (defect; fixed or filed) / **AUTH-STATIC** (authentically static — do not fix) / **DEFERRED-FEED** (widget correct, data source a documented deferred system). ### Coverage inventory (static pass) All 26 distinct CFG primitives resolve: 17 BT-specific registrations (btl4grnd.cpp:135-152) + engine base (numeric/numericSpeed/digitalClock/rankAndScore/bgPixelMap/bgBitMap/configure/ reconfigure/externalConfigure); parse-skip list EMPTY ([gskip]=0), all 50 attr bindings resolve (0 NULL) — the gauge-complete wave claims re-verified against btl4grnd.cpp + the CFG keyword census. ### Verdict table | # | Surface / widget | Binding → producer | Evidence | Tier | Verdict | |---|---|---|---|---|---| | 1 | Heat: condenser temp two-part bars ×6 (TMP) | HeatSink/CurrentTemperature + Degradation/Failure endpoints → heat.cpp table | attr wave incr.1 + tasks #9/#10 heat economy; LIVE: bank S 515→680 climbing under autofire (audit4) | T2 | CORRECT | | 2 | Heat: SETS valve sliders ×6 (vertNormalSlider) | Condenser/ValveSetting → coolantFlowScale (RecomputeCondenserValves FUN_0049f788, 1/N) | task #13 live valveState 1→5 → flow 1/6→5/10 | T2 | CORRECT | | 3 | Heat: LEK LeakGauge column | CoolantMassLeakRate → coolantDraw (damage-gated) | writer census; pristine=0 | T2 | AUTH-STATIC (pristine) | | 4 | Heat: COOLANT A numeric | HeatSink/AmbientTemperature → AggregateHeatSink 300 (ctor @4ae8d0) | LIVE: reads 300 (audit3/4 shots) | T2 | AUTH-STATIC (hardcoded 300) | | 5 | Heat: COOLANT S numeric | HeatSink/CurrentTemperature (bank) | LIVE: 515→653→680 climbing | T2 | CORRECT | | 6 | Heat: RES coolant vertBar C | Reservoir/CoolantMass → coolantLevel@0x12C (CFG:4526) | Gitea #7; re-verified LIVE this audit: [flush] 6→0 in ~1.0s held, PFX cloud | T2 | CORRECT | | 7 | GeneratorCluster A-D panels | Generator/OutputVoltage → outputVoltage@0x1DC + state lamps | gauge wave (@004c72ac); task #12 GenSelect live | T2 | CORRECT | | 8 | Comm: pilotList KILLS | victim-posted ScoreMessage → killCount (cross-player credit) | commit 34aaa7d; solo 0 observed (authentic) | T2 | CORRECT | | 9 | Comm: pilotList DEATHS | observed-death tally + −2/−1 display clamp (BTPilotDeaths) | 2026-07-12 resolution; solo 0 observed | T2 | CORRECT | | 10 | Comm: SELECT-TARGET highlight | BTPilotIsSelected accessors (ex raw-offset AV) | commit 34aaa7d root-cause note | T2 | CORRECT | | 11 | Plasma: rankAndScore | engine primitive → external serial annunciator | not a D3D surface (L4PLASMA=com2) | T0 | CORRECT (n/a on dev composite) | | 12 | Quad mini-panels: temp bars / cooling-loop / power-bus lamps / evolt bar / cmode lamp | SubsystemCluster children (@004c8140 map) reading InputVoltage/HeatSink/temps | VEHICLE_SUBSYSTEMS.md phase 2; LIVE: panels render per-subsystem, [vss] roster matches authored aux screens 1-10 | T2 | CORRECT | | 13 | Quad: recharge dial (SegmentArc270) | PercentDone → rechargeLevel@0x320 | ~~writer census: Emitter::ComputeOutputVoltage @004ba738 only — projectile dial full-once~~ **CORRECTED (Gitea #12, 2026-07-19): the census missed vtbl slot 17 @004b9c9c** — `rechargeLevel=(rechargeRate−recoil)/rechargeRate`, called per frame by the recovered @004bbd04 Loading/state-7 cases: the projectile dial authentically ANIMATES through reload (port fix pending — the port sim never calls slot 17) | T1 | CORRECT (emitters) + **WRONG-STATIC (projectile; fix pending)** | | 14 | Quad: ammo count (ammoCountA) | AmmoBin::ammoCount via BTAmmoBinCountPtr bridge | AFC100 fix 2026-07-12; LIVE: 24 fresh (audit4) vs 19 after missile autofire (audit3) — counter moves | T2 | CORRECT | | 15 | Quad: ConfigMap regroup lamps | LBE4ControlsManager buttonGroup map state | CORRECTED 2026-07-21: @004c6ee0 is the LinkToEntity override (not SetColor); armed at viewpoint bind → LIVE | T1 | LIVE (authentic LinkToEntity path; BT_CONFIGMAP deleted) | | 16 | Eng pages: prepEngr SYSTEM NN + subsystem label + class cells | roster walk, sub+0x1dc auxScreen == screen (BTGetSubsystemAuxScreen) | FUN_004c7e48 matched line-by-line; [vss] dump: scr1/2/4=PPC/Strk6/ERMed, 5-8=Sens/Myo/ERMed/ERMed, 9/10=Strk6/PPC — page headers+labels CORRECT | T1/T2 | CORRECT | | 17 | Eng ballistic page: ammoCountB / jam / fire / reload numeric / destroyed lamp | bin count bridge + weaponAlarm@0x364 (MechWeapon layout static_assert-locked) | ctor @004c9558 coords matched; LIVE renders | T2 | CORRECT | | 18 | Eng ballistic page: eject wipe (BitMapInverseWipeScalar @004c61c8) | subsys+0x3f8 eject timer | port tracked NULL (class not reconstructed) | T3 | DEFERRED-FEED | | 19 | Eng emitter/myomer pages: SeekVoltageGraph (POWER curve) | 4 Seek* attrs + subsystem vtbl+0x3C sampler | WAS WRONG (bring-up no-op Execute) — **FIXED (Gitea #11, 2026-07-19)**: full reconstruction, see finding A addendum | T2 | CORRECT (fixed) | | 20 | Eng emitter page: seek-step lamp (bteseek) | Emitter::seekVoltageIndex@0x3F0 (static_assert-locked) | @004c93b0 :2146 | T2 | CORRECT | | 21 | Eng myomer page: seek-step lamp | Myomers@0x320 INFERRED (ctor @004c8df4 not in the decomp export) | marked in code | T3 | CORRECT (best-effort; flagged) | | 22 | Eng page: linked heat-sink number (hs+0x1d4 guarded raw) | PrepEngr numeric #2 | marked BEST-EFFORT (guarded) | T3 | CORRECT (guarded; flagged) | | 23 | sec: radar map (wedge/blips/scale) | Mech Radar* attrs; wedge ← Torso::currentTwist@0x1D8 (Gitea #1) | #1 [T2]; LIVE: blips + wedge render, zoom via SetTargetRange | T2 | CORRECT | | 24 | sec: radar pip/name symbology | GetVideoObject/LookUpPip/GetNameID | task #17 under reconstruction — cross-blip stand-in | T3 | DEFERRED-FEED | | 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 | | 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 | | 32 | sec: view cycling (N / pod 0x15) | CycleDisplayMode → vtbl+0x4C @4d1ae4 | #6 resolution re-verified live ([mode] display notify 0/1/2) | T2 | CORRECT | | 33 | sec: CONTROL MODE lamp (BAS/MID/ADV) | ControlsMapper/DisplayMode oneOfSeveralPixInt | attr wave incr.5; M-cycle verified #6 | T2 | CORRECT | | 34 | sec: duck / searchlight button lamps | duckState (ctor-zeroed only) / Searchlight LightOn | duckState writer missing (P3 leftover) | T3 | DEFERRED-FEED (duck); CORRECT (light attr published) | | 35 | sec: messageBoard ticker | StatusMessagePool (NULL stub) + kill ticker strip 0 | 7fc4acb; kill ticker live 2026-07-12, other strips unsurveyed | T2/T3 | DEFERRED-FEED (partial) | | 36 | MFD preset paging (J/K/L, pod RIO banks) | SetPresetMode table @0051dbf0 (little-endian re-decode) | #9; re-verified live this audit: cycles visit EXACTLY the populated set (MFD1: 1,2,4; MFD2: 1-4; MFD3: 1,2) | T2 | CORRECT | | 37 | HUD: range ladder + caret (500 m/s slide) | BTSetHudTargetRange ← targeting; HudSimulation :5652-5670 | task #37 [T1] + live | T1/T2 | CORRECT | | 38 | HUD: weapon pips (loaded/dark/hidden; group filter) | attr 0x1C WeaponState == stateConst2; damage state 1 hides | 2026-07-18 fix (projectile pips un-pinned); pips visible in audit shots | T2 | CORRECT | | 39 | HUD: bottom 21-tick TORSO-TWIST tape | RotationOfTorsoHorizontal / HorizontalTorsoLimit (attrs 4/5/6) | task #58; BLH fixed-torso → centred (authentic) in audit shots | T2 | CORRECT (AUTH-STATIC on BLH) | | 40 | HUD: compass + threat trail | CompassHeading (attr 0xD, yaw+twist :5676); ThreatVector (attr 0xC) ← TakeDamage push | task #37/#38; compass rose renders in shots | T2 | CORRECT | | 41 | HUD: lock ring (4°/frame spin) | Lock rule :5619-5634 (own zone <0.75, target zone <1.0) | task #38 | T2 | CORRECT | | 42 | HUD: target hotbox / edge arrows | HotBoxVector projection (live per-axis) | task #37 | T2 | CORRECT | | 43 | HUD: simple-X mode | PrimaryHudOn mask 0x20 state switch | ctor @4689-4705 | T1/T2 | CORRECT | | 44 | HUD: PNAME1-8.bgf 3D name marker chain | — | deferred | T4 | DEFERRED-FEED | | 45 | HUD attr table @005110b8 (ids 3-0xD) | hud.hpp/CLASSMAP corrected this audit | finding B below | T1 | CORRECT (fixed) | **Counts: 34 CORRECT · 1 WRONG (filed: SeekVoltageGraph — since FIXED, Gitea #11) · 6 AUTH-STATIC (incl. dual-verdict rows 13/29/39) · 6 DEFERRED-FEED (rows 18/24/34-duck/35/44 + the sec messageBoard partial).** (Dual-verdict rows counted in both columns.) ### Finding A — the "SYSTEM 10 PPC ammo readout" (issue entry point a) — ROOT-CAUSED, FILED **The page header, label and screen→subsystem mapping are CORRECT** ([vss] live dump matches the authored sub+0x1dc assignments; CFG prepEngr(ModeMFD3Eng2, 10, ...) pairing consistent; FUN_004c7e48 reconstruction line-exact). The ammo readout on the SYSTEM 10 (PPC) page is a **GHOST — stale pixels from the previously-shown SYSTEM 9 (Streak 6) page** on the SAME shared Eng3 bit-plane. Reproduced live (audit3): SYSTEM 04 ERMED LASER (emitter) showed the "0019" ammo box left by the SYSTEM 02 Streak page; SYSTEM 08's POWER box showed the SENSOR RANGE label left by SYSTEM 05. Mechanism chain, all decomp-verified: - Ballistic/sensor eng children (ammoCountB @(0xdc,0x104)-(0x140,0x12C), jam/fire lamps, reload numeric — ctor @004c9558) draw INSIDE the top data box (0x97,0x80)-(0x17d,0x13b). - On a page switch the engine moves gauges active↔inactive (DeactivateGaugeBases, GAUGREND.cpp:3909) — BecameInactive() is a no-op everywhere [T0]: NOTHING erases on deactivation. - PrepEngrScreen::BecameActive (@004c7e48) erases the top box ONLY for the ballistic/sensor/gauss cases (topBox=1); the emitter/PPC and myomer cases pass 0 — **because in the binary those pages' box is owned by the SeekVoltageGraph**: its localView IS the box rect (ctor @004c6798 SetPositionWithinPort(0x97,0x80,0x17d,0x13b)), BecameActive @004c6920 poisons the cached sample (this+0xAC = 10000.0f), and the next Execute (@004c6934) calls the clear helper @004c6be4 (SetColor 0 + filled rect 1000×1000 clipped to the view = FULL BOX ERASE) before re-plotting the voltage curve. Every topBox=0 page has a graph child (EnergyWeaponCluster @004c93b0 :2137; MyomerCluster @004c8df4) — the design is coherent and ghost-free on the pod. - **The port's SeekVoltageGraph::Execute is a marked bring-up NO-OP** (btl4gau2.cpp:331), so the erase never runs and the POWER curve never draws → the ghost. **Fix required (LARGE — filed, not half-fixed per the no-stand-ins rule):** full SeekVoltageGraph reconstruction — publish the 4 CurrentSeekVoltageIndex/MinSeekVoltageIndex/MaxSeekVoltageIndex/ SeekVoltage attributes on Emitter(+PPC)+Myomers, identify + reconstruct the subsystem vtbl+0x3C voltage-response sampler, x87-recover the plot math (@004c6934 loop constants _DAT_004c6bdc/_be0, DrawTicks @004c6c6c, DrawCursor @004c6c30, clear @004c6be4 — Ghidra dropped the FPU args; tools/disas2.py). An erase-only patch was deliberately NOT made (it would be a stand-in for the missing curve). **✅ RESOLVED (Gitea #11, 2026-07-19) — the full widget landed (btl4gau2.cpp/.hpp):** - **x87 recovery (capstone, tools/disas2.py):** plot step/limit _DAT_004c6bdc/_be0 = 1200/12000; the 80-bit consts @004c6bd0/@004c6d74 = EXACTLY 1/12000 (the voltage→y normaliser); x = Round(response(v)·xScale 230), y = Round(v·(1/12000)·yScale 187); change-test = the response sampled at 12000 V vs the @0xAC cache (9999 activation sentinel); clear @004c6be4 = SetOperation(Replace)+color 0+filled 1000×1000 clipped to the view; ticks @004c6c6c = per-gear loop [*min..*max], current gear = full L, others 10-px stubs, XOR pair to move; cursor @004c6c30 = 8×8 XOR square at (x-4,y-4) from the LIVE voltage (*this[0x29]); destroyed branch = centred edestryd.pcc via (Round(xScale)-w)/2; revive → own vtbl+0xC == BecameActive (slot 3 of PTR_0051a1fc, vtable-dump verified). Members this[0x30..0x32] = cursorX/cursorY/tickIndexShown. - **The vtbl+0x3C sampler identified from the vtable dumps** (Emitter @00512078 slot 15 = @004bb42c; Myomers @005117dc slot 15 = @004b8f94): Emitter = sqrt(P(v)/2.0e8f [0x4d3ebc20]), P @004bb3f4 = damageFraction@0x444 · v² · 0.5 [_DAT_004bb428] · energyCoefficient@0x454; Myomers = sqrt(AvailableOutput(v)·3.6/350). **FUN_004dd138 == sqrt** (part_015.c:4026) — the myomers.cpp "fabs / fp-magnitude" reading was corrected and the best-effort name GetSpeedReading renamed **SeekVoltageResponse**. Port dispatch: `BTSeekVoltageSample` (emitter.cpp) → `BTMyomersSeekSample` (myomers.cpp) complete-type bridges; `BTSubsystemDestroyed` (powersub.cpp) for the simulationState==1 test. - **Emitter's AUTHENTIC attribute table recovered + published** (binary @0x511dd4, ids 0x1D-0x25 chained after MechWeapon's 0x1C): LaserOn@0x418(firingActive), LaserScale@0x42C, LaserRotation@0x41C, CurrentSeekVoltageIndex@0x3F0, RecommendedSeekVoltageIndex@0x3F4, MinSeekVoltageIndex@0x3F8, MaxSeekVoltageIndex@0x3FC, SeekVoltage@0x400, OutputVoltage@0x414(currentLevel — RAW volts, the cursor feed). Emitter field renames per the table: @0x3F8 minSeekVoltageIndex, @0x3FC maxSeekVoltageIndex (static_assert-locked). The MechWeapon 0x1D "OutputVoltage" PORT ALIAS was RETIRED (binary table ends 0x1C; Find walks lowest-id-first so the alias shadowed the authentic row — the cursor would have read the 0..1 rechargeLevel instead of volts). PPC::DefaultData → Emitter::GetAttributeIndex(). - **Verified live** (BLH DEV.EGG, BT_AUTOFIRE + BT_PRESET_TEST + the new **BT_PRESET_HOLD=** steady-state hold): both #10 repro pairs ghost-free held for minutes (SYS09 Streak→SYS10 PPC; SYS02 Streak→SYS04 ERMed; SYS05 Sensors→SYS06 Myomers); curves draw (emitter = straight line, authentically — sqrt of v² is linear; myomer = steep low curve), cursors track the charge cycle, gear ticks render; [seek] log (BT_SEEK_LOG) shows exactly ONE replot per activation, all 6 graph instances. NOTE: a BT_SHOT landing exactly on a page-switch frame can capture the 1-frame label-vs-box transition (PrepEngr labels draw in BecameActive; the box clears at the graph's next rated Execute) — the binary has the same lag class; it self-heals next frame and the 90/120 cadence phase-lock made it look persistent during cycling verification. ### Finding B — HUD binary attr table @005110c0 conflict (issue entry point b) — RESOLVED, FIXED Re-dumped section_dump.txt @5110b8: the table starts ONE RECORD EARLIER than transcribed — a full 16-byte record {id 3, "FlickerRate", 0x1D8|1, 0} (the old reading took the FlickerRate name ptr as a "category label"), so every subsequent offset in hud.hpp was shifted one slot. Binary truth: Rotation@0x1DC ← Torso::currentTwist(+0x1D8), LimitR@0x1E0, LimitL@0x1E4, Speed@0x1E8, RangeToTarget@0x1EC, (0x1F0 unbound), Visible@0x1F4(init 1), Lock@0x1F8(init 0), HotBoxVector P3D@0x1FC, ThreatVector P3D@0x208, CompassHeading Scalar@0x214 (= yaw euler[0] + twist :5676). Cross-checked against the ctor @004b7f94 and HudSimulation @004b7830 — ALL consistent (the Lock rule :5622/:5633 writes @0x1F8; the 1200-default + 500 m/s slide writes @0x1EC). **Behavioral impact: NONE** — the port reticle feeds through its own globals (gBTHudLockState etc.) which implement the correct semantics, l4gauge.cfg never binds HUD attrs, and the mislabeled SetCompassHeading (@004b7810, actually SetThreatVector — writes @0x208) has no port caller. FIXED: hud.hpp member renames + table comment, hud.cpp ctor inits (Visible=1/Lock=0 per binary), SetThreatVector rename, CLASSMAP.md entry. The ids 4-0xD NAME↔USE pairings from task #37 stand. ### Finding C — [T3]/[T4] tag review (issue entry point c) context/gauges-hud.md contains NO [T3] tags (all previously-T3 claims were promoted in the #1/#6/#7/#9 waves). The single [T4] — the un-exported reticlePosition writer / fixed-torso free-aim channel (mech+0x36c) — remains a genuine unknown and stays flagged. In-code BEST-EFFORT markers audited as table rows 18/21/22/24/34. ### Finding D — BT_SHOT dock-capture regression (found + fixed during this audit) The 2026-07-18 capture reorder left BT_SHOT BEFORE BTDrawGaugeInset, so BT_DEV_GAUGES_DOCK screenshots silently omitted the gauge panel (audit runs 1-2 captured cockpit-only frames). Moved the capture after the inset (L4VIDEO.cpp) — dock shots again show what the screen shows. Also noted: BT_DEV_GAUGES_DOCK alone does NOT enable the composite — DevGaugeComposite() gates on BT_DEV_GAUGES (L4VB16.cpp:105); set BOTH. --- ## Cockpit surround — the default desktop layout (2026-07-20) Replaces dock-bottom as the DEFAULT under `BT_DEV_GAUGES`: the 3D world CENTERED with the six gauge surfaces composited AROUND it in the single main window at ½ native scale, plus clickable RIO button lamps. Iterated as a visual reference with the user (scratchpad/cockpit_pod.py) then landed in the engine. **Files.** - `engine/MUNGA_L4/l4vb16.h` — `BTCockpitLayout`/`BTCockpitBtn` structs, the compute/draw/mouse declarations, and `BTLampBrightnessOf` (shared inline lamp decode, dedup of L4GLASSWIN's). - `engine/MUNGA_L4/L4VB16.cpp` — `gBTGaugeCockpit`/`gBTCockpitCanvasW/H`; `BTCockpitCanvasFor` + `BTCockpitComputeLayout` (layout single source of truth); `BTDrawCockpitPanels` (band fill → button lamp quads → 9-entry surface loop with green tint / palette sec + Eng-sibling slot map → GDI-atlas flight labels; wrapped in a `D3DSBT_ALL` state block = the floating-rocks isolation); `BTCockpitMouseDown/Up` (client→bb hit-test + glass press/release/right-latch contract); `BTApplyWorldViewport` + `DevGaugeDocked` cockpit branches. Hooked at the top of `BTDrawGaugeInset` (before BT_SHOT, so shots include it). - `game/btl4main.cpp` — glass preset (cockpit default, panels opt-in), mode resolution + work-area-clamped window sizing (`-res` = the view size), WndProc `WM_L/RBUTTON` routing. - `engine/MUNGA_L4/L4VIDEO.cpp` — `BTWorldAspectOf` cockpit branch (view rect on-screen aspect). - `engine/MUNGA_L4/L4VIDRND.cpp` — `CameraShipHUDRenderable::Render` made viewport-relative (camera-seat banner + ranking window land over the view, not clipped into the surround bands). **Layout math.** `SCALE=0.5; MFD 320×240, radar 240×320; OVL=44 (corner overlap into the view); LAMP=16 (visible lamp edge, user asked for larger than glass's 10); REDCELL=64 (hidden hit depth); RAILW=26; sideBand=276, topBand=212, bottomBand=336; canvas=(viewW+552, viewH+548).` Corner MFDs overlap the view corner by OVL both axes; Mfd2 drops 25% of its height into the top; sec flush below (view bottom clean). Buttons: the full rect is the click target, the surface draws over it so only the LAMP edge shows (PaintGlass painter trick). Address banks = L4GLASSWIN BuildMfd/BuildRadar/BuildFlight. **Gating precedence** (resolved once → `gBTGaugeCockpit`): `BTGlassPanelsActive` (BT_GLASS_PANELS) > `BT_COCKPIT=1` > `BT_DEV_GAUGES_WINDOW` / `BT_DEV_GAUGES_DOCK` > **cockpit default**. `BT_COCKPIT=0` = dock-bottom. `DevGaugeDocked()` includes cockpit so the separate window stands down. Green tint `BT_COCKPIT_TINT=RRGGBB` (default 0x27E8 ≈ rgb(33,255,66)); radar stays palette amber. **Pod-build seam.** Panels + dim lamps render in ALL builds; only `PadRIO::SetScreenButton/ GetLampState` are `#ifdef BT_GLASS` (via `CkEmit`/`CkLampState`). Pod build: cockpit draws, clicks no-op. Labels: lazy GDI DIB → MANAGED A8R8G8B8 texture cache (survives device reset). **Verified (2026-07-20, awaiting human playtest):** both build/ (BT_GLASS off) and build-glass/ compile (0 error C). Glass BT_SHOT matches the mockup (green MFDs, amber radar, red/yellow/blue lamps, labeled flight blocks, reticle centered on target). 500-click PostMessage storm during a live mission SURVIVED (no AV — the Gitea #18 Receiver-gap fix holds); 20 hits logged with correct addresses/coords. BT_COCKPIT=0 dock-bottom pixel-unregressed. BT_GLASS_PANELS=1 stands cockpit down (Cyd's windows up). Pod build renders the panels.