Files
BT411/docs/GLASS_COCKPIT.md
T
CydandClaude Opus 4.8 f338595685 Glass cockpit: per-display windows (BT_GLASS_PANELS)
Break the desktop glass cockpit's secondary displays out of the single
combined pad panel + D3D gauge strip into ONE window per pod display, each
carrying that display's surface with its RIO button bank, arranged
pod-faithfully around the main view.  New TU engine/MUNGA_L4/L4GLASSWIN.*
(BT_GLASS-only), selected by the -platform glass preset via the new runtime
gate BT_GLASS_PANELS (=0 falls back to the legacy single panel + dock).

- Surfaces are CPU-expanded from the shared gauge buffer
  (SVGA16::ExpandPlaneToBGRA, no D3D) and StretchDIBits'd in -- the D3D
  dev-composite path (dock / window / overlay) stands down while active.
- Buttons sit UNDER the imagery: big hidden click targets that reach into
  the display, with only a small protruding edge showing as a lamp light.
  Red MFD banks tile the top/bottom; radar rails run the sides + a bottom
  row; a Flight Controls window hosts the no-display banks.
- Windows: Heat / Engineering / Comm / Left Weapons / Right Weapons / radar
  + Flight Controls; edge-to-edge (no in-client margin/title; OS caption
  labels each).
- Fix blank surfaces: application/ghWnd are duplicate-defined and bind
  non-deterministically under /FORCE, so the fresh L4GLASSWIN TU read NULL.
  Reach the renderer/window via BTResolveGaugeRenderer/BTResolveMainWindow
  defined in btl4main.cpp off the real btl4App/hWnd pointers.  New gotcha:
  reconstruction-gotchas.md S6 duplicate-GLOBAL corollary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 14:43:40 -05:00

27 KiB
Raw Blame History

GLASS_COCKPIT — running ledger (desktop developer layer)

Detailed running log for the glass-cockpit branch work: PadRIO, on-screen panel/plasma, miniconsole, Steam transport/lobby. Curated digest: context/glass-cockpit.md (read that first); this file carries the step-by-step detail, evidence, and verification transcripts.


2026-07-17 — Step 1: branch + compile gates + scaffolding

  • Branch glass-cockpit created from master @ e2c21c4.
  • CMakeLists.txt: option(BT_GLASS OFF) + option(BT_STEAM OFF); BT_STEAM without BT_GLASS is a configure-time FATAL_ERROR; gates append to BT_DEFS (flows to all three targets); STATUS line prints gate state at configure. Gated target_sources blocks will be added per-TU as they land (CMake would reject nonexistent source files now).
  • .gitignore: build-pod/, build-steam/, content/bindings.txt, content/frontend.egg, content/steam_appid.txt.
  • Verification (this step): defaults configure prints BT_GLASS=OFF BT_STEAM=OFF and builds (purity build); -DBT_GLASS=ON and ON/ON configures print correct gate state; the BT_STEAM-without-BT_GLASS negative test FATAL_ERRORs. Full 3-config BUILD matrix becomes meaningful from step 2 (first gated code) and is re-run there.

2026-07-17 — Step 2a: the .CTL positional-attribute off-by-one — CONFIRMED and FIXED (ungated)

The bug [T2, live-verified]: the streamed .CTL "L4" mapping resource carries the 1995 binary's positional attribute ids (stick=3, throttle=4, ...), resolved at install time by Simulation::GetAttributePointer(id)AttributeIndexSet::Find(id) = attributeIndex[id-1] (positional). MechControlsMapper chained its enum on Subsystem::NextAttributeID == 2, but the binary's chain reaches 3 (one base attribute we don't publish) — so EVERY streamed mapper record landed one member late.

Evidence (new BT_CTRLMAP_LOG=1 diagnostic in LBE4ControlsManager::CreateStreamedMappings, engine/MUNGA_L4/L4CTRL.cpp — permanent, env-gated, default off). Before the fix (build-pod Debug, -egg DEV.EGG, dev platform):

rec 0  subsys 0 group 1(Joy)    elem 0  attrID 3 -> +0xe4   (= throttlePosition: WRONG, stick intended)
rec 1  subsys 0 group 0(Scalar) elem 0  attrID 4 -> +0xe8   (= pedalsPosition:   WRONG, throttle intended)
rec 2  subsys 0 group 3(Button) elem 63 attrID 6 -> +0xf0   (= speedDemand:      WRONG, reverse intended)
rec 4-6 attrID 10/11/12 -> lookRight/lookBehind/lookDown    (one late; LookLeft/Right/Behind intended)

Spacing attr3→attr4 was 4 bytes (Scalar) where stick→throttle must be 8 (ControlsJoystick). The weapons chain (attrID 19 → +0x31c TriggerState) was already correct — mechweap was pad-aligned in task #16 (gotcha #11). Torso (subsys 17) also aligned (MechSubsystem publishes binary id 2). ONLY the mapper chain (subsys 0) was off. On the dev box the keyboard INPUT BRIDGE (writes the mapper members directly, after the engine push) masked it; on a REAL POD the serial RIO throttle scalar would have driven pedalsPosition and the stick x/y would have driven throttle+pedals — the Phase-8 landmine prior art predicted.

The fix (ungated, pod-correctness — user-approved 2026-07-17): mechmppr.hpp pins the enum to the binary numbering (MechControlsMapperPadFirstAttributeID = 2, stick=3 ... PilotArray= 0x16); mechmppr.cpp pads id 2 with a valid never-bound entry (the mech.cpp attrPad idiom) and locks the base + pinned ids with static_asserts. L4MechControlsMapper::TargetRangeExponent (chained) moves 0x16→0x17, matching the binary's own arithmetic; it is bound by name (audio zoom-blip), so no consumer shifts.

After the fix: attrID 3 -> +0xdc (stick), attrID 4 -> +0xe4 (throttle, 8-byte spacing ✓), attrID 6 -> +0xec (reverse), 10/11/12 -> +0xfc/+0x100/+0x104 (the look trio); torso + weapons unchanged. Regression: headless forced-walk run (BT_FORCE_THROTTLE=1) drives normally — [mppr] thr=1 -> speedDemand=61.501, gait cycles, position advances, 31/31 subsystems simulated, no faults.

2026-07-17 — Steps 2b-2e: PadRIO, stand-downs, displays, preset — DONE + verified

2b (RIOBase + PadRIO, commit ac57a47): L4RIO.h splits abstract RIOBase (enums, the five analog Scalars, pure GetNextEvent/SetLamp, no-op serial ops, NEW IsOperational()) out of serial RIO (: PCSerialPacket, RIOBase — ctor assigns as before, zero behavioral delta); L4CTRL.h rioPointer retyped RIOBase*; gated L4CONTROLS=PAD factory arm. NEW gated TUs L4PADRIO.* (XInput+keyboard → the surface; 3s hot-plug; focus-guarded keys; per-poll AnalogEvent heartbeat time-gated so the manager's drain loop terminates; lampState[]; static SetScreenButton — button addresses AND the keypad space 0x50-0x6F → KeyEvents) and L4PADBINDINGS.* (content\bindings.txt, default written on first run; deflect/slew/set axis model; addresses validated vs ButtonCount).

2c (stand-downs, commit 9f35a80): engine query BTRIODevicePresent() (ungated — pod-correct); both dev bridges (mech4.cpp attr writes, mechmppr.cpp BT_KEY_BRIDGE) gate their WRITES: unset=auto / "0"=off / else on; forced harness always rides. Definitive live test: OS-injected LSHIFT hold slewed PadRIO throttle 0→0.33→1.0 with pre==thr every frame — the value arrives via the ENGINE push through the streamed .CTL binding (bridge stood down), speedDemand=61.501 from authentic InterpretControls.

2d/2e (displays + preset, commits db9ac94 + 418acf0): the on-screen panel follows vRIO (C:\VWE\vrioCockpitLayout.cs/PanelCanvas.cs, per Cyd): five 4x2 MFD clusters (descending addresses), Throttle/Secondary/Screen/Joystick 1x8 columns, two 4x4 hex keypads (0x50 internal→pilot unit, 0x60 external→operator unit), physical names, red/yellow/blue coloration, 500/250/125ms flash, right-click latch — 104 controls. PlasmaWindow : Video8BitBuffered renders the 128x32 plasma via the SAME gauge-renderer path as serial (buffer is TOP-DOWN — the flip rendered it upside down, user-reported + fixed). MFD surfaces = the EXISTING dev-gauge modes (no new code, no L4VB16 exposure). -platform glass preset + run.cmd token. Button-trap guard (e2c21c4 pattern): the authentic base ConfigureMappableMessageHandler FAIL trap is abort() under DEBUGOFF — any aux/zoom button without its reconstructed L4 override KILLED the game on press (panel makes all reachable). Default now log+ignore; BT_BUTTON_TRAP=1 restores the hard trap. Verified: synthetic panel click → 2 [FAIL] (press+release through panel→PadRIO→ProcessRIOEvent→streamed mapping), game survives. NOTE: each [FAIL] marks a missing L4 override — reconstruction backlog, see open-questions.

Step-2 verification matrix (all passed 2026-07-17):

  1. Purity: defaults build-pod/ compiles (known mech3 /FORCE externals only); seam audit — BT_GLASS/BT_STEAM mentions ONLY in CMakeLists, btl4main.cpp preset region, L4CTRL.cpp (include+PAD arm), L4GREND.cpp (include+SCREEN branch), gated TUs, run.cmd docs.
  2. Pod solo forced-walk clean (§2a).
  3. 2-node loopback MP on the pod build: A -egg MP.EGG -net 1501 + B -net 1601 + btconsole.py relay — both nodes launch into the running mission (77/76 ticks, mechs at their drop zones, gait/target loops live).
  4. Glass build: PadRIO drives through the authentic mapper path; panel click path live; plasma window; all boot lines present. Deferred to the user (visual-only): panel/plasma appearance vs vRIO on screen, XInput stick feel, L4PADFLIP polarity.

2026-07-17 — Step 3: the miniconsole — DONE + verified (commit c4ca87b)

Architecture: the console stays a wire peer. game/glass/btl4fe.* (GDI catalog menu + the console-shape egg writer) and btl4console.* (the LocalConsole marshal: a worker-thread console CLIENT speaking the tools/btconsole.py protocol verbatim — 1040-byte chunked ReceiveEggFileMessage, 20s settle, RunMission×2, stay connected, own the clock, StopMission(id 6)@expiry; logs content\marshal.log). The engine runs the 100% stock console ladder — ZERO engine hooks; the planned L4APP.H setters proved unnecessary (launches pass real argv on relaunch). WinMain: one gated block — zero-mission-arg glass launch → menu → relaunch self with mission argv (BT_FE_EGG/PODS/SECS arm the marshal; BT_FE_LOOP returns exits to the menu; menu relaunches CLEAR the handoff — the leak re-armed a marshal in cycle test 1).

Verified (synthetic menu drives, all autonomous):

  • Golden egg: frontend.egg section sequence + line offsets IDENTICAL to MP.EGG; ordinal pages verbatim; GDI name-glyph pages in MP.EGG framing (32/8 rows, 16 bytes/line).
  • Solo cycle: menu → egg → relaunch → marshal → stock ladder → mission RUNS (58+ ticks) → 60s clock → stop → menu returns with clean env.
  • HOST LAN 2-node: peer started manually (-net 1601); menu-hosted launch → marshal feeds BOTH pods (2-pilot egg, 10 chunks each) → both in-mission (65/18 ticks) → stop both → host returns to the menu, peer idles at console-wait.

2026-07-17 — Step 4 pre: Steamworks SDK 1.64 vendored (commit 23c0019)

Official Valve zip (provided by Cyd at the repo root), extracted to extern/steamworks_sdk_164/ and trimmed to public/ + redistributable_bin/ + Readme (12.5 MB); zip removed. Consumed only under -DBT_STEAM=ON.

2026-07-17/18 — Step 4: the Steam wire — CODE COMPLETE (commits e79e8fa, 48ede2f, f703bb1)

4a (e79e8fa) — the wire seam: file-local BTNetSend/Recv/Accept/Close wrappers around the 9 raw Winsock sites in L4NET.CPP; OFF builds are pure code motion. Regression: 2-node loopback MP through the seam — mesh forms, full 31/31 mission on both nodes.

4b (48ede2f) — the transport: gated TU L4STEAMNET — pseudo-SOCKETs (0x5EA0xxxx) behind the seam; the engine's TCP byte stream rides reliable-NoNagle Steam messages re-assembled into per-connection rings (recv semantics preserved: empty→WSAEWOULDBLOCK, closed→0); seam completions: CheckSocket (peer = token address), OpenConnection TCP_OPEN delegate, GetMyAddress self-token. Install on BT_STEAM_NET=1; graceful degrade to Winsock at every step. BOTH pumps needed (SteamAPI_RunCallbacks + sockets RunCallbacks).

4c (f703bb1) — the lobby + THE FAKEIP LESSON [T2]: prior art assumed FakeIP survives the per-mission process relaunch — DISPROVEN live (menu process got 169.254.124.110, the mission process got 169.254.54.234; the pod timed out connecting to its own stale address). Redesign: roster addresses are opaque ipv4-shaped TOKENS (169.254.77.N, ports 1501 console / 1502 game) minted by the host at GO and mapped to Steam IDENTITIES; connections ride ConnectP2P(identity, channel) (console=0, game=1); the map hands off via env (BT_FE_MYFAKE/BT_FE_STEAMMAP). btl4lobby: ISteamMatchmaking room (btl4=1 filter), pilots publish name/mech/color, host GO mints tokens + writes btl4map + btl4go.

Verified single-machine (ON/ON, live Steam client): menu → HOST STEAM LOBBY → room → GO → map 169.254.77.1=<steamID64> → egg roster carries the token → mission process [steamnet] up ... 1 roster token(s) incl. self → pod self-matches → stock console ladder → mission RUNS (46 ticks). 3-config build matrix green throughout. Remaining (user hardware): the live 2-account cross-machine session — procedure in docs/STEAM_TEST.md.

2026-07-20 — Glass input-coverage audit + keyboard reconciliation

Trigger: making BT_GLASS the permanent player experience — familiar desktop keys dead, "most panel buttons do nothing". Full audit of every input surface (bindings.txt keys, the 72-button panel, the streamed .CTL EventMapping records, the typed 1995 channel), then the fix set.

Finding 1 — THE panel-dead-buttons root cause: MechRIOMapper message ids shifted [T1→T2 fixed]

MechRIOMapper's enum chained Aux1QuadMessageID = L4MechControlsMapper::NextMessageID (=0x19), registering all 18 RIO handlers at ids 0x19-0x2a. The binary RIO table @0051dd30 RE-REGISTERS the base aux/zoom ids 3..0x13 with the override handlers (Hotbox = 0x1a @0051de98) — that re-registration over the base FAIL-trap ids is the whole point of the derived table. Streamed .CTL events carry the binary ids, so every MFD-bank/zoom press found only the base ConfigureMappableMessageHandler[FAIL] (26 of 72 panel buttons dead; on a real pod with the hard trap, an abort()). Hotbox worked by accident (sender + entry both used the same wrong 0x2a). Fixed: ids pinned to the binary numbering + static_assert locks (btl4mppr.hpp). Verified live: panel 0x0F/0x08/0x27/0x20 → [mode] preset lines, 0x10/0x11 zoom no longer FAIL. (Residual fidelity gap: the binary RIO table also carries its OWN Keypress id 0x19 → @004d2514, unreconstructed; we still register the shared L4 Keypress 0x17 — works, logged in the backlog.)

Finding 2 — the always-active msg-4 records IDENTIFIED [T1] (closes the #9 open question)

Binary message tables decoded from section_dump (16-byte {id,name,fn,0} entries): per-receiver- class id spaces. Roster (ARENA1 MadCat): subs 3=Reservoir, 4-9=Condenser1-6, 10-13=GeneratorA-D. So: 0x2C = the COOLANT FLUSH button (Reservoir InjectCoolant, WORKS), 0x2F/0x2E/0x2D/0x2B/ 0x2A/0x29 = per-condenser VALVE buttons (MoveValve, WORK), 0x1A-0x1D = Generator A-D ON/OFF (ToggleGeneratorOnOff id 4, table @0050fb90 fn @004b1ed0 — DEAD, unreconstructed). Also new: 0x13 → Mech DuckRequest (0x1a @0049fa00 — the manual's CROUCH button!), 0x28 → Mech BalanceCoolant (0x16 @0049f728), 0x12 → ThermalSight toggle (id 3), 0x14 → Searchlight + Searchlight2 ToggleLamp (id 3 @004b860c — body reconstructed but Searchlight:: MessageHandlers is default-constructed EMPTY, so it never fires: the quick win). Weapon Eng-page extras: msg 0x3 = ToggleCooling (HeatableSubsystem @0050e41c fn @004ad6f8, unwired), msg 0xb = ToggleSeekVoltage (energy @004ba478) / EjectAmmo (ammo weapons @004bb9b8), Myomers msg 9 = ToggleSeekVoltage (@004b8a48; body exists, unwired). Unhandled messages are SILENT (Receiver::Receive ignores an unfound id) — the dead-button census cannot come from [FAIL] logs alone; it comes from the BT_CTRLMAP_LOG record dump × the handler tables.

Finding 3 — glass keyboard: stand-down + double dispatch [T2]

ALL btinput actions (drive keys, M/N/J/K/L/H/C/V/B/G/F5-F9) stand down when PadRIO owns the path — nothing on the game side polls keys directly (BTInputPoll is the only GetAsyncKeyState consumer). Meanwhile the typed 1995 channel stays LIVE (glass preset = PAD,KEYBOARD) with NO suppression — so Cyd's old bindings double-dispatched: W (keypad row) also typed 'w' = pilot select 0; C (fire middle) also typed 'c' = MFD3 Eng3; NUMPAD key-UPS alias to letter hotkeys (VK_NUMPAD2=0x62='b' = MFD3 Quad, VK_F5=0x74='t' = pilot select 3). This is the historic pre-btinput double-dispatch, back on the glass side.

The fix set (landed)

  1. btl4mppr.hpp — RIO id pinning (ungated, pod-correctness; the step-2a idiom).
  2. L4PADRIO.* — typed-channel suppression tables built from the loaded bindings (+hardcoded keys), PadRIO::SuppressKey consumed by a #ifdef BT_GLASS arm next to the btinput hook in L4CTRL.cpp:1506 block; V joins backtick on the view-toggle edge; J/K/L fire gBTPresetCycle[3] (the same seam the dev poll feeds — no binary analog exists for a "cycle" button; the body is the authentic SetPresetMode).
  3. L4PADBINDINGS.cpp + content\bindings.txt — the MERGED default map (user decision: keyboard = the ~20 core gameplay actions on the CONTROLS.MAP keys; the PANEL covers all 72 pod buttons by click/latch). W/S throttle, A/D pedals, Q/E twist, R/F elevation, X all-stop, 1-4/Space/Ctrl fire + LALT reverse + B look-behind, M=0x18, N=0x15, H=0x2C (flush hold), C=0x2F (Condenser1 valve), G=0x0E (weapon-1 configure, Mfd1-Quad-gated), F5-F9=Mfd2 bank 0x27-0x24+0x22 (page-gated gen A-D/mode), numpad flight cluster kept. DISPLACED from Cyd's map (each reachable on the panel or by editing bindings.txt): number row→secondary panel, QWERTY row→pilot keypad (dropped), arrows→hat looks (arrows now drive), V/C/B→fire 0x47/0x46/0x45, LCTRL→throttle-down. XInput pad section untouched. NOTE (control modes, authentic): in BAS mode the pod turns with the STICK (Q/E here) and the pedals are inactive; A/D turn in MID/ADV — exactly the manual's behavior, unlike the desktop bridge which re-routes A/D per mode.
  4. Un-touched: CONTROLS.MAP, btinput defaults, pod serial path. bindings.txt is read only by PadBindingProfile inside the gated PadRIO TU — the pod build never opens it.

Verification (2026-07-20, build-glass2, ARENA1 solo, all logs in-session)

  • Panel: 0x0F→preset(0,1), 0x08→(0,0), 0x27→(1,1), 0x20→(1,0), zoom no-FAIL; 0x15 display 1→2→0; 0x2C hold → [flush] Reservoir level 6→0; dead-class buttons (0x13/0x1A/0x12/0x14) silent as predicted. ZERO [FAIL] lines in the whole session (was 2 per bank press).
  • Keyboard: W hold → thr 0→1, speedDemand 61.501, gait advances; Q → turnDemand 0.94 (BAS); M → control mode -> 1; A hold → turnDemand 1.0 (MID); Space → [emitter] FIRED; N → display notify 1; J/K/L → presets (0,1)/(1,1)/(2,1); H → reservoir drain; C → condenser#1 valveState 1→5. BT_SHOT frame confirms visually: MID lit on the control-mode column, CRITICAL DAMAGE schematic up, MFDs on Eng pages, heading moved. V not re-injected (user took foreground mid-run) — code-identical to the verified backtick edge [T3].
  • Suppression: typed 'w' swallowed (no [keych]), typed 't' flows ([keych] 0x74), WM_KEYUP VK_F5 swallowed (the 't'-alias hazard).
  • Un-regression: build-pod (gates OFF) compiles 0 errors; forced-walk run drives (speedDemand=61.501, 32 subsystems, streamed ids 3→+0xdc / 4→+0xe4 unchanged).

2026-07-18 — Merge origin/master (commit f889e24): btinput meets PadRIO

Master's 33-commit run landed (D1 relay/operator-console line, CONTROLS.MAP+XInput binding engine btinput.cpp, camera seats, torso pitch, sign fixes, the 1995 manual, version stamping). The reconciliation: btinput (ungated, dev/pod desktops) joined the stand-down convention — BTInputPoll yields and BTInputSuppressKey claims nothing when an operational cockpit device (serial RIO / PadRIO) owns the input path (BTRIODevicePresent; BT_KEY_BRIDGE override; forced harness exempt). One input system per mode: CONTROLS.MAP/btinput on dev+pod desktops, bindings.txt/PadRIO on glass. The D1 relay keeps its own raw sockets by design (relay and Steam are alternative wires; the BTNet seam covers the arcade mesh both need). Verified: 3-config build, glass stand-down live, pod forced-walk with btinput active, 2-node loopback MP 31/31.

2026-07-19 — Mission-control options: the AUTHORITATIVE catalog (TeslaSuite console.ini)

Source: C:\VWE\TeslaSuite\410console\4_10-console-extracted\Console.ini, the Mac 4.10 operator console's BT:: adventure tree (the tag= values are the egg field values; display names are the console's). Cross-checked vs 410console/battletech-port/BATTLETECH-PORT-SPEC.md + reference/ cavern.egg. This is every option the original console exposed for a BattleTech mission.

Mission-level[mission]: adventure=BattleTech (fixed), temperature=27 (fixed), scenario=freeforall (the tag is always freeforall; the RESPAWN choice is the ROLE):

  • map (8): cavern, grass, rav, polar3, polar4, arena1, arena2, dbase
  • time (4): morning, day, evening, night ← was day/night/morning/dusk (wrong: evening)
  • weather (3): clear, fog (light), soup (heavy)
  • role (2): Default (model=dfltrole, respawn) / NoReturn (model=noreturn, one life)
  • length: seconds

Per-pilot — participant block:

  • experience (4 [T1 game decode]): novice, standard, veteran, expert ← was hardcoded expert
  • vehicle (18, friendly→tag): Black Hawk=blkhawk, Loki=loki, Nova=bhk1, Mad Cat=madcat, Thor=thor, Owens=owens, Kotori=own1, Summoner=thr1, Hellbringer=lok1, Timber Wolf=mad1, Avatar=avatar, Satyr=ava1, Sunder=sunder, Sunder V1=snd1, Vulture=vulture, Mad Dog=vul1, Mischief=lok2, Zanin Neko=mad2 (the 18 ModelList names; matches the 2026-07-18 vehicle sweep)
  • color/camo (7, display→tag): White=White, Black=Black, Brown=Brown, Red=Crimson, Green=Green, Gray=Grey, Tan=Tan ← was White/Black/Crimson only
  • badge/emblem (7, the badge= field): VGL, Davion, Kurita, Liao, Marik, None, Steiner ← was VGL
  • patch/badge-color (8, the patch= field): Yellow, Blue, Grey, Green, Violet, Red, White, Black ← was Yellow
  • dropzone (5, the dropzone= field): one, two, three, four, five ← was one
  • advancedDamage (0/1): the advanced damage model toggle ← was hardcoded 1
  • vehicleValue: 1000 (const), loadzones=1 (const), bitmapindex (per-pilot ordinal)

2026-07-20 — Per-display cockpit windows (BT_GLASS_PANELS)

Goal (Cyd): stop collapsing the desktop cockpit into combined surfaces. Break each SECONDARY display out into its OWN window carrying that display's surface WITH its RIO button bank placed around it, arranged pod-faithfully around the main 3D view — a dev cockpit that reads like the pod, so button/lamp/display behavior can be validated in position.

New TU: engine/MUNGA_L4/L4GLASSWIN.{h,cpp} (BT_GLASS-only; added to the CMake BT_GLASS target_sources). Builds SEVEN GDI windows and positions them around the main window:

Window Surface (port) RIO bank Buttons
Heat MFD (UL) Heat (0x4000) 0x28-0x2F 8 red, 4 above / 4 below
MFD 2 (UC) Mfd2/Eng2 (0x0400) 0x20-0x27 8 red, 4 above / 4 below
Comm MFD (UR) Comm (0x8000) 0x30-0x37 8 red, 4 above / 4 below
MFD 1 (LL) Mfd1/Eng1 (0x0100) 0x08-0x0F 8 red, 4 above / 4 below
MFD 3 (LR) Mfd3/Eng3 (0x1000) 0x00-0x07 8 red, 4 above / 4 below
Secondary/Radar sec (palette, portrait) 0x10-0x1B Secondary 12 yellow, 6 left / 6 right
Flight Controls (none) 0x38-0x47 (+ panic/icom) neutral, 2 columns

Architecture — GDI + CPU blit, NO D3D for the secondaries. Each window is a layered, topmost GDI window (the L4PADPANEL pattern: double-buffered paint, ~62 ms repaint timer, left-click = momentary / right-click = latch → PadRIO::SetScreenButton; lamps from PadRIO::GetLampState). The surface is CPU-expanded from the shared gauge pixelBuffer by the new SVGA16::ExpandPlaneToBGRA(mask, paletteID, monoTint, rotateQuadrant, dst, &ow, &oh) (L4VB16.cpp) — the no-D3D analog of DrawDevSurface: palette-LUT expand for sec (monoTint < 0), mono bit-plane → tint for the MFDs; the portrait secondary CRT is unrotated by transposing into dst (rotate 1/3, BT_GAUGE_SEC_ROT) — then StretchDIBits (HALFTONE) into the surface rect. An Mfd/Eng pair shows whichever plane is the live (non-BlankColor) one, as on the pod monitor. This reuses the plasma-window CPU→window pattern and sidesteps the whole D3D dev-composite bug family (additional-swap-chain lifetime, device-state save/restore, depth-stencil mismatch, layered strobing).

Gate: BT_GLASS_PANELS (runtime env; BTGlassPanelsActive()). Default ON under -platform glass (preset putenv in btl4main.cpp); =0 restores the legacy single pad panel

  • docked gauge strip. Wired at three seams:
  • L4PADRIO ctor: BTGlassPanelsActive()BTGlassPanels_Create() instead of BTPadPanel_Create(); dtor destroys both (safe no-ops).
  • L4VB16: BTDrawGaugeInset / BTGaugeWindowRenderAndPresent early-return when GlassPanelsOwnSurfaces() (the per-display windows own the surfaces).
  • btl4main: skip the gBTGaugeDockBottom + 1100×600 world-window forcing (world stays normal size — no bottom strip).

Placement. ComputeLayout() rings the windows around ghWnd (the main game window): top row Heat/Mfd2/Comm above it, Mfd1/Mfd3 at the lower corners, radar below-center, Flight Controls left of the seat; clamped to the work area, with an 800×600-centered fallback before the main window exists + a one-shot reposition once ghWnd comes up (in the repaint timer).

Verified [T2 startup]. -DBT_GLASS=ON Debug build links (the usual /FORCE mech3 subsystem set is pre-existing, unrelated). Bounded -platform glass -egg DEV.EGG launch: [glasswin] per-display cockpit up (7 windows: 5 MFD + radar + flight), [plasmawin] … up, [padrio] XInput pad found, no failure/abort markers, full mission loop un-regressed (killed after 18 s). Visual layout / surface content / click-through NOT yet human-verified.

Tunable (Cyd flagged refinements): surface display sizes, the exact button sub-positions around each screen, the yellow left/right split (first cut 0x10-0x15 / 0x16-0x1B), and the MFD mono phosphor tint — all named constants at the top of L4GLASSWIN.cpp. Deferred: folding the plasma window into the same arrangement; per-window draggable/saved positions; moving/resizing the main window into a true center cell for a tighter ring.

Blank-surface bug (found + fixed same day) — /FORCE duplicate-global race

First glass-panels run showed the buttons but BLANK display surfaces. BT_GLASS_LOG diagnostics in BlitSurface traced it stage by stage: application == 0x00000000 in L4GLASSWIN.cpp for the ENTIRE run (call 1 → 481), so application->GetGaugeRenderer() was never even reached. Yet the legacy dock path (BTDrawGaugeSurfaces, L4VB16.cpp, SAME engine lib) logged application=047BA9C8 gr=0C57FF78 — non-NULL.

Root cause: the 1995 headers declare free globals (application, ghWnd, …) without inline/extern, so they are DEFINED in both munga_engine and bt410_l4. Under /FORCE:MULTIPLE the linker binds each .obj's reference to one copy or the other non-deterministically per link — the newly-added L4GLASSWIN.obj bound the copy the game never assigns (stays NULL), and a relink even flipped L4VB16.obj. Routing through an engine helper is therefore unreliable (its binding is equally random).

Fix: BTResolveGaugeRenderer() / BTResolveMainWindow() (declared in l4vb16.h) are DEFINED in game/btl4main.cpp off the launcher's OWN file-scope pointers — btl4App (set to new_app at btl4main.cpp:631) and the global hWnd (the real main window). Those live in btl4main.obj and are the actual assigned pointers, so the resolvers are always correct regardless of the duplicate-symbol binding. BlitSurface and the placement code call the resolvers, never the application/ghWnd globals. New durable gotcha: context/reconstruction-gotchas.md §6 duplicate-GLOBAL corollary.

Verified after the fix (BT_GLASS_LOG): [glass] 'Heat MFD' port=Heat mask=0x4000 pal=3 enable=2 ow=640 oh=480 nonzero=22227/307200, [glass] 'Secondary / Radar' port=sec mask=0x3f pal=1 enable=3 ow=480 oh=640 nonzero=26206/307200 — surfaces resolve and carry live pixels. BT_GLASS_LOG left in as a permanent per-surface resolve/pixel diagnostic (throttled).