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