Files
BT412/docs/GAUGE_COMPOSITE.md
T
arcattackandClaude Opus 4.8 a256813395 gauges: secondary/radar MFD composites live on a dev box (Milestone B)
The pod's secondary/radar cockpit surface now renders as an inset in the
800x600 dev window under BT_DEV_GAUGES: radar/tactical grid, SPEED/HEADING/
MAGNETIC/PROP dials, and the color-coded ARMOR DAMAGE mech schematic -- real
gauge content (nzSec=27247), composited from the shared CPU pixelBuffer.

Composite pass (engine): SVGA16::DrawDevInset palette-expands the secondary
plane into a MANAGED texture on the MAIN device and draws an XYZRHW inset quad;
BTDrawGaugeInset() reaches the gauge renderer's 'sec' port and is called from
DPLRenderer::ExecuteImplementation as the last draw before EndScene.

Three reconstruction bugs fixed to get real content:
1. BTL4Application::MakeGaugeRenderer signature mismatch (REAL fix): the
   reconstructed no-arg override only HID the 2007-engine's widened 3-arg
   virtual MakeGaugeRenderer(int*,int*,int*), so the engine built a base
   L4GaugeRenderer whose ctor never parsed gauge/l4gauge.cfg -> empty symbol
   table -> "undefined label 'bhk1Init'" -> no ports/gauges. Matched the 3-arg
   signature so it truly overrides (args ignored; BT is fullscreen). Same bug
   class as the BTL4GaugeRenderer(false,NULL,NULL,NULL) ctor fix.
2. Parse hung on undefined primitives (gated dev accommodation): the BT gauge
   primitive table (BTL4MethodDescription) is still a stub, so an unknown
   primitive -> ReportParsingError -> Fail() -> a MODAL dialog freezing the
   parse. Under BT_DEV_GAUGES, skip the unknown primitive's params so labels
   register and the base "configure" primitive builds the ports.
3. Gauge widgets AV on NULL data bindings (gated): NumericDisplayScalar's NULL
   value_pointer -> GaugeConnectionDirectOf ctor deref (bind NULL->static zero);
   RankAndScore::Execute derefs unreconstructed game state (Gauge::GuardedExecute
   SEH wrapper -> Disable(True) on first fault).

All guards gated on BT_DEV_GAUGES: default DEV un-regressed (TARGET DESTROYED,
0 crashes) and the pod path is byte-unchanged (strict Fail, no guards).

Remaining (docs/GAUGE_COMPOSITE.md): the real gauge WIDGET reconstruction
(BTL4MethodDescription method table + gauge->game-state data bindings), then
Step 2 (RP<->BT MFD port-name reconcile) + Step 3 (2-window layout).

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

182 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `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
`<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 → `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<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).
## Follow-ups (after Milestone B)
- **Widget recon (the real fix):** reconstruct `BTL4MethodDescription` (BT gauge classes) + the gauge→mech
data bindings so the skipped/guarded gauges show real data and the POD path (strict `Fail`, no guards) works.
- **Step 2** — reconcile the MFD port names (`auxUL2/auxC/...` in `SVGA16::Update` vs `Heat/Comm/Mfd1-3` in
`L4GAUGE.CFG`) → the 5 MFD surfaces render (only `sec`/radar works today).
- **Step 3** — the 2-window layout (3D-view window + an instrument-panel window tiling radar + the 5 MFDs).