Files
BT411/context/glass-cockpit.md
T
arcattackandClaude Opus 5 12d6862c1f keylight: unship it -- opt-in (BT_KEYLIGHT=1), gone from all player docs
Operator decision: the player base is not on Windows 11, so the RGB keyboard
mirror should not be shipped or promised.  The feature is NOT deleted -- Cyd's
implementation stays intact behind the gate for whoever wants it -- it is
simply off by default and invisible:

  * PadRIO gate flips from opt-out (BT_KEYLIGHT=0 disables) to OPT-IN
    (BT_KEYLIGHT=1 enables).  Default boots produce zero keylight lines, and a
    stub build and a real build now behave identically for players: nothing.
  * Removed from every user-facing artifact: the README paragraph, the
    CONTROLS.md section, the CONTROLS.html footer line.
  * environ.ini template: the entry stays (it is the discoverable home for the
    opt-in) but now says OFF-unless-1, Windows 11 22H2+, and why it is
    unshipped.
  * mkdist's cut-time stub warning removed -- it existed to protect a README
    promise that no longer exists.
  * context/glass-cockpit.md records the decision, the opt-in, and the
    build-time SDK stub gate in one place.

VERIFIED: default boot logs zero keylight lines; BT_KEYLIGHT=1 engages the
feature (the stub on this machine: its one honest log line + the key map).
All three console suites pass; checkctx CLEAN.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:06:00 -05:00

560 lines
42 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.
---
id: glass-cockpit
title: "Glass Cockpit — the desktop developer layer (BT_GLASS / BT_STEAM)"
status: living
source_sections: "plan 2026-07-17; CMakeLists.txt gates; BT412 prior art (reference only)"
related_topics: [pod-hardware, build-and-run, multiplayer, gauges-hud]
key_terms: [PadRIO, miniconsole, glass-cockpit]
open_questions: []
---
# Glass Cockpit — the desktop developer layer
**Purpose:** developer tooling so game developers can test their work WITHOUT booting a Tesla II
pod — and stay far enough out of the game that nothing a real pod requires can break. NOT a
consumer product (GDI-grade UI is the ceiling). Pod fidelity outranks desktop convenience in
every trade-off.
## ONE unified build (2026-07-21; the compile-gate split is RETIRED)
The pod/glass/steam **compile** split (three build dirs, three exes) is gone — it caused
version skew (a stale steam exe shipped 2 days behind), tripled build time, and the
"clicks silently dead in the wrong build" incident. Now **everything compiles into the one
`build/` exe** and behavior is selected at RUNTIME:
| Runtime gate | Arms |
|---|---|
| `BT_PLATFORM=glass` | the desktop-cockpit profile: `L4CONTROLS=PAD` → PadRIO (clickable cockpit buttons, bindings.txt keymap), plasma window, and the miniconsole menu on a zero-arg launch |
| `BT_STEAM_NET=1` | the Steam transport (FakeIP/SDR; degrades to Winsock without the Steam client). `steam_api.dll` is **DELAY-LOADED** (`/DELAYLOAD` + delayimp) — machines without the DLL run everything else |
| (nothing) | **GLASS is the desktop DEFAULT (2026-07-22)** — a bare boot gets PadRIO/bindings.txt (and the menu on zero args). The old accidental-DEV trap ("the old keymap again") is closed: every launcher that forgot the env used to boot DEV (play bats, steam bat, the operator console — each needed the same one-line fix). |
| `BT_PLATFORM=dev` | the legacy DEV profile (keyboard-only, the bring-up drive bridge) — explicit opt-in for debugging |
| `BT_PLATFORM=pod` | the cabinet profile. **CONTRACT: the real pod's SETENV.BAT must set this** — the old "bare boot == cabinet shape" assumption no longer holds. |
- The `BT_GLASS` macro is now ALWAYS defined (a seam marker only — the `#ifdef` sites remain
as documentation of the layer boundary). Every gated code path was audited: all are
instance-guarded (`PadRIO::activeInstance`, `steamActive`) or env-gated at runtime.
- **`BT_STEAM` stays a COMPILE gate (the LICENSE gate, default OFF, restored 2026-07-21):**
the Steamworks SDK terms bar a public release with Steam inside, so a release configure
must be able to exclude it entirely — no SDK headers, no `steam_api.lib`, no DLL. The dev
checkout configures `build/` once with `-DBT_STEAM=ON`. `mkdist.py` reads the answer from
`build/CMakeCache.txt`: steam-OFF zips carry no `play_steam.bat`, no `steam_api.dll`
(including a STALE one from an earlier ON build), a README with the steam section stripped,
and a `-nosteam` name suffix so the two flavors never clobber. Verified both flavors:
OFF exe has no steam bits + `BT_STEAM_NET=1` is inert; ON exe degrades to Winsock.
- **Menu gate (amended 2026-07-22):** the miniconsole menu runs on a zero-arg GLASS boot —
and glass is now the desktop default, so a bare double-click of btl4.exe opens the menu
(the desired desktop UX). Cabinet protection moved from "bare boot never menus" to the
**SETENV.BAT contract**: the real pod sets `BT_PLATFORM=pod` (no menu, wait-for-egg);
`BT_PLATFORM=dev` also keeps the old bare-boot shape.
- Verified matrix (2026-07-21): bare boot ≡ old pod exe (same DEV profile, same egg-load path);
glass + scripted click → ConfigureMappables end-to-end; exe runs WITHOUT steam_api.dll
present; glass zero-arg → menu; 2-node loopback MP (145 ticks each); `BT_STEAM_NET=1`
without Steam → "staying on Winsock" + game runs.
- **Convention:** every `BT_*` name is a RUNTIME env gate (`BTEnvOn`) — with TWO exceptions,
both compile gates: `BT_STEAM` (the license gate above) and **`BT_EXPIRE` (2026-07-22, the
tester-build expiry tripwire)** — default ON, the exe refuses to start `BT_EXPIRE_DAYS`
(default 14) after its build DAY (`btversion.h BT_BUILD_UNIX`, UTC-midnight-rounded so the
only-on-change header churns once per day, not per build). Boot logs `[boot] test build
window: N day(s) left`; expired = log + MessageBox + exit. Disable: `-DBT_EXPIRE=OFF`
(mkdist marks such a zip `-noexpire` + warns; README expiry note is `{EXPIRE}`-marker-gated).
Quality gate, not DRM — a clock rollback defeats it. Verified both paths live
(`-DBT_EXPIRE_DAYS=0` force-expire run). Do not add another compile gate without updating
this file.
- No weak-linkage/optional symbols — `/FORCE` turns unresolved externals into runtime AVs
([[reconstruction-gotchas]]); everything links always, so this class of trap no longer varies
by build flavor.
## Layout modes — ONE resolver (2026-07-26) [T2 four-mode boot-verified]
Where the five MFDs + radar go is a SEPARATE axis from the platform profile. Under a glass boot
(`BT_DEV_GAUGES` set) `btl4main.cpp` resolves it **once**, right after the profile putenvs land,
into `glassLayout`; the boot banner, the pad-panel decision and the window-sizing block all read
that one answer. Precedence, highest first:
| Env | Layout | Buttons live in |
|---|---|---|
| `BT_GLASS_PANELS`≠0 | per-display cockpit windows (the "exploded" view, `L4GLASSWIN`) | each display's own window |
| `BT_DEV_GAUGES_WINDOW` | the legacy separate MFD window | the single combined pad panel |
| `BT_DEV_GAUGES_DOCK` | the docked bottom gauge strip | the single combined pad panel |
| `BT_COCKPIT=0` | ...also the docked strip (the documented opt-out) | the single combined pad panel |
| (nothing) | **COCKPIT SURROUND — the glass default** | the main window (D3D-drawn lamps) |
The banner now names the live one: `[boot] platform profile: GLASS (PadRIO + plasma window)
[secondary displays: cockpit surround]`.
**Two bugs this replaced** (both were live until 2026-07-26): the mode was decided in TWO places
with duplicated precedence (the glass profile block picked `BT_PAD_PANEL`/`BT_GLASS_PANELS`; the
sizing block re-derived cockpit-vs-dock), and the banner read NEITHER — it announced "per-display
cockpit windows [BT_GLASS_PANELS]" for every glass boot, surround included. Worse, the profile
block turned `BT_COCKPIT=0` into `BT_GLASS_PANELS=1`, so the docked strip it is documented to
select was **unreachable under the glass profile**. `BT_GLASS_PANELS` is now explicit-only, and
dock/window modes auto-raise `BT_PAD_PANEL` so the 72-button field always has a home.
## ONE button-bank geometry — L4RIOBANK (2026-07-26) [T2 click-verified both modes]
The RIO button field around a pod display is drawn TWICE — composited into the main window
(surround, `L4VB16.cpp`) and one window per display (exploded, `L4GLASSWIN.cpp`) — and each
renderer used to carry its own copy of the geometry. They drifted badly: the same MFD button was
a 156×138 under-glass target in the exploded window and a **76×24 sliver entirely outside the
glass** in the surround. `engine/MUNGA_L4/L4RIOBANK.cpp` is now that geometry, once; both
renderers are consumers, and placement (WHERE each display goes) stays with each renderer because
that genuinely differs.
**The under-glass rule** (ported from RP412 `L4MFDVIEW`, which took it from the pod): a button
reaches **half the glass** in BEHIND the display and only a **lamp strip** clears the edge. The
renderer paints buttons first, imagery over them — so the lamp reads as a slim bar, practically
the whole display is the press target, and nothing is occluded (the picture wins every pixel it
covers). The strip scales off the display's SHORT axis against the native 480 (an MFD is 640×480
landscape, the map 480×640 portrait — both short-axis 480) with a 6px floor, so a half-scale
surround lamp is still a bar and not a smudge. The MFD per-column nudge against the unevenly
spaced DISPLAY/PROGRAM legends (`mfdColumnDX`) moved here too, so both renderers inherit the
same alignment.
**The map's foot row is pushed FIRST**, deliberately: the side columns now reach half the map
each, so they cover the foot band too, and the hit test takes the first match — foot-first is
what keeps 0x16/0x17/0x1F/0x1E reachable. That class of bug is why the verification below tests
first-hit reachability, not mere presence.
**Verified 2026-07-26 [T2]:** `BT_RIOBANK_LOG=1` dumps every bank's rects;
`scratchpad/checkbank.py` reports the per-bank census and proves no address is SHADOWED (has a
point no earlier button covers); `scratchpad/clickbank.py` then posts a real click at every
button's centre. Both modes: **72/72 addresses placed, zero shadowed, zero duplicated, 72/72
dispatched** (`[cockpit] CLICK` / `[glasswin] CLICK`), process survived 144 posted messages per
mode. Pod + dev profiles boot and simulate un-regressed. Surround BT_SHOT confirms the legends
are clear and the lamps read as edge strips.
## The cockpit SCALES — uniform-fit letterbox + `-fit` (2026-07-26) [T2 live]
The canvas is a fixed size and D3D9 stretched it into whatever the client area was, so a window
dragged to a different shape **squashed the instruments** — the projection was aspect-corrected
(task #20) but the panels, lamps and MFD glass were not. Now the canvas is fitted at ONE uniform
scale, centred, leftover black: the cockpit scales both ways and never distorts, whatever the
window.
- `BTCockpitFitRect(clientW, clientH, RECT*)` — integer math throughout, so the rect is stable
frame to frame (a float scale wobbles a pixel and the bars shimmer during a drag).
- D3D9 does the scaling at Present via a **destination rect**, which `D3DSWAPEFFECT_DISCARD`
forbids — so the WINDOWED swap effect becomes **`D3DSWAPEFFECT_COPY`** when the surround is up
and multisampling is off (COPY cannot multisample). `gBTCockpitLetterbox` records whether that
happened; when 0 the old full-client stretch stands, so an MSAA run still works.
- Both present sites take the rect (scene + the wait-screen overlay, which paints into the same
backbuffer). `WM_SIZE` invalidates so the bars repaint black — a COPY present only writes its
dest rect, so stale bar content would otherwise persist.
- **The click transform had to follow.** `BTCockpitMouseDown` mapped client→canvas against the
full client; under the letterbox that drifts the hit test off every button by the bar width.
It now runs the identical fit (and a click on a bar hits nothing).
- **World aspect** under the letterbox is the view rect's OWN aspect — the client no longer
enters into it.
- **`-fit`** (alias `-windowed-fullscreen`): borderless `WS_POPUP` over the monitor, canvas
letterboxed inside. Verified on a 3440×1440 ultrawide.
**Ordering trap found live:** the letterbox flag was first set at device creation, but the
first `WM_SIZE` arrives BEFORE the device exists — a `-fit` boot logged `aspect=3.14` (the
stretch formula) and applied it on frame 1, because nothing resizes the window again. The INTENT
is now decided in `btl4main` alongside `gBTGaugeCockpit`; `L4VIDEO` only confirms or withdraws it.
**Capture note:** `PrintWindow` returns an all-black frame for the borderless `-fit` window
(it works for the normal chrome'd one). Use a screen-region grab (`scratchpad/shot.ps1`) for
`-fit`, and don't read a black PrintWindow there as a render failure — it isn't.
Verified: wide (1684×661) and tall (744×961) drags both letterbox with zero distortion; **72/72
buttons still dispatch through the transform at 0.62× scale with a 389px bar**; exploded / dock /
pod / dev all keep DISCARD and boot un-regressed.
## Player-tunable displays — scale, map placement, environ.ini (2026-07-26) [T2 live]
The pod bolted its instruments down at one size; a desktop panel has room to trade viewscreen for
instrument, so the player scales them. All resolved ONCE (`BTCkResolvedSizes`, L4VB16.cpp) because
the surround recomputes its layout every frame.
| Env | Effect |
|---|---|
| `BT_MFD_SCALE` | all five MFDs, % of pod size (25-200, default 100) |
| `BT_MFD_SCALE_UL/_UC/_UR/_LL/_LR` | one display, overriding the group |
| `BT_RADAR_SCALE` | the portrait map |
| `BT_RADAR_POS` | `CENTER` (default) / `LEFT` / `RIGHT` / `MIDLEFT` / `MIDRIGHT` (+ `BOTTOM*`, `*CENTER` spellings) |
**The bands derive from the resolved sizes** — that is why the sizes could not stay constants: the
surround band a display hangs in has to grow with it or the canvas clips it. Each band is what its
LARGEST occupant needs (a corner MFD only claims `size - kCkOVL`, since it overlaps the view), and
the bottom band drops to just the lower MFDs when the map leaves that row for a side. Verified:
100% reproduces the historical `L276 R276 T223 B336` exactly; 150% gives `L436 R436 T343 B448`;
80% + MIDLEFT gives `L256 R212 T175 B148`.
**A corner map goes flush to the CANVAS edge**, not the view edge, and the lower MFD whose corner
it took sits beside it. Measuring off the view edge (first cut) overlapped them by the band width —
232px of map drawn over the Right Weapons MFD.
**Map legend grid — MEASURED, not inherited [T2].** The side columns must line up with the six
cells the map paints beside them (⊕MAP / ⊖MAP / IR / crouch / searchlight / display-mode =
0x10-0x15 — the imagery independently confirms our address map). `scratchpad/measurelegend.py` over
a native 480×640 capture gives **top 3, cell 102, pitch 107** on the 640 span. RP412's map is
`13 + 6×102 @ pitch 105` — same cell height, different top and pitch, so **its numbers do not
transfer**; ours are ours. Our old even division (`displayH/6 + 1 = 107`) had the pitch right by
luck and sat 3px high of the labels. Top and bottom are now scaled separately and subtracted, so
rounding cannot drift the buttons out of step with the labels down the column.
**`environ.ini` (the settings file) [T2].** ⚠ It was read ~300 lines into `WinMain`, AFTER the
platform-profile block had run its `getenv`s — so every setting the profile reads (`BT_PLATFORM`,
`BT_COCKPIT`, `BT_GLASS_PANELS`, `BT_DEV_GAUGES`, `L4CONTROLS`…) was **silently ignored from the
file** and only worked as a real env var. It also `putenv`'d comment lines verbatim, which would
have turned a shipped commented-out option into a variable literally named `#BT_MFD_SCALE`. Now:
loaded immediately after the first-breath boot line, comments and blanks skipped, and **the real
environment WINS** so a launcher `.bat` overrides the file rather than fighting it. A documented
default is written on first run when absent — the `bindings.txt` convention: untracked, so an
extract-over-top upgrade never clobbers a player's settings, and every option ships commented out
so a fresh install behaves exactly as before. Verified all three: first run writes it and applies
0 settings; uncommented file settings drive the layout (130% group + 60% UC + MIDRIGHT); a real
`BT_MFD_SCALE=90` overrides the file's 130 while the file's other settings still apply.
Verified: **72/72 placed, 0 shadowed, 72/72 dispatched** at baseline, at 150%/135%, and at
75% + BOTTOMRIGHT; exploded / dock / pod / dev boot un-regressed.
## Dead-button backlog CLOSED — DuckRequest was the last (2026-07-26) [T2 live]
The 2026-07-20 audit's "8 buttons dispatch a streamed message with no reconstructed handler" is
finished. ⚠ Only ONE of the eight was actually still missing when this pass started — generator
on/off, ToggleSeekVoltage, EjectAmmo, ToggleCooling and BalanceCoolant had all landed between
07-20 and 07-25 while `pod-hardware.md` / `open-questions.md` still called them dead
(`docs/INPUT_PATH_AUDIT.md` had already flagged the census as "stale in both directions").
**Check the code, not the census.**
**`Mech::DuckRequest` @0049fa00 (id 0x1a, RIO 0x13 — the manual's CROUCH button).** The binary's
entire body is two lines: press-only (`msg+0xc > 0`), then `duckState`(`mech+0x398`) = 1. It is a
one-shot REQUEST flag, not a posture toggle — the handler never clears it and the only other
writer in the whole binary is the mech reset (part_012.c:9439, the same reset that zeroes
`incomingLock`). Registered in `Mech::MessageHandlerEntries`; `[duck]` log line on press.
**Its consumer is a DATABINDING, not code [T1] — which is why the flag looked pointless.**
`duckState` has ZERO readers anywhere in the decomp, because it is published as attribute 0x37 and
consumed through the gauge system: `content/GAUGE/L4GAUGE.CFG` runs a 3-frame `bduck.pcc`
`oneOfSeveralPixInt` widget bound to `DuckState` on the map's legend column ("crouch mode: button
4"). Verified live: pressing 0x13 turns that legend icon grey→orange. **Do not invent a crouch
pose to "finish" this** — the flag plus the indicator IS the code side.
Bonus confirmation: those legend widgets sit at gauge offsets 537/430/322/215/108 — a **107
pitch**, independently corroborating the map legend grid measured from pixels in §Player-tunable
displays.
## THE KEYBOARD IS THE BUTTON BOARD — new default keymap (2026-07-26) [T2]
Ported from RP412's bindings design (user decision: make it the DEFAULT, not an alternate). The
letter and number rows are the MFD button banks **laid out where they sit on the panel**, and
flight moves to the NUMPAD so the board stays free:
| Keys | Bank |
|---|---|
| `1 2 3 4` / `Q W E R` | Heat / coolant 0x2F-0x2C / 0x2B-0x28 |
| `5 6 7 8` / `T Y U I` | Engineering (Mfd2) 0x27-0x24 / 0x23-0x20 |
| `9 0 - =` / `O P [ ]` | Comm / target hotbox 0x37-0x34 / 0x33-0x30 |
| `A S D F` / `Z X C V` | Left Weapons (Mfd1) 0x0F-0x0C / 0x0B-0x08 |
| `H J K L` / `N M , .` | Right Weapons (Mfd3) 0x07-0x04 / 0x03-0x00 |
| `F1`-`F6` / `F7`-`F12` | the map's two columns 0x10-0x15 / 0x18-0x1D |
| numpad | 8/2/4/6 stick, 7/9 pedals, 5 all-stop, 0/1/3/. the four fire buttons |
| `Shift`/`Ctrl` · `Alt` · arrows · `` ` `` | throttle lever · reverse · look/torso-centre · view |
`G` and `B` stay unbound — the physical gap between the lower clusters. **Coverage: 61 of 72
addresses on the keyboard, and the 11 absent ones are exactly those with no authored function**
(0x16/0x17/0x1E/0x1F column gaps, 0x38-0x3E intercom/door). All 72 remain clickable.
It lands on BT's addresses better than it deserves to: `1-4`+`QWER` are the ENTIRE coolant system
(Condensers 1-6, flush, balance), `F6`/`F7` the display/control-mode cycles, `F9`-`F12` Generators
A-D, `F4` the crouch button.
⚠ **What it costs, deliberately.** A bound key is removed from the authentic 1995 typed-hotkey
channel to stop double-dispatch, and this board binds nearly everything — so `5` (Quad page), `z`
(Eng1), `t/y/u/i/o` (pilot select) and `+`/`-` (target zoom) are given up. Unbind a key to get its
1995 meaning back.
⚠ **New rule: a bindings row WINS over a built-in convenience key** (`PadRIO::KeyHasBinding`).
`V` and `J/K/L` are board buttons now, so the view-toggle and preset-cycle polls stand down for
them and the view toggle lives on `` ` `` alone. Without this they would have fired BOTH.
`content/CONTROLS.MAP` was rewritten to mirror the board (90 bindings, 0 parse complaints) so the
glass/pod/dev boots still feel identical — the 2026-07-21 settlement. An existing `bindings.txt`
is never overwritten, so a player only meets this board by deleting theirs.
## Hat labels corrected — 0x42 is TORSO CENTER [T1, streamed mapping]
`docs/INPUT_PATH_AUDIT.md` flagged the 0x41-0x44 labels as wrong and it was right. Settled from
`BT_CTRLMAP_LOG`: **0x42 → subsys 17 (Torso), attrID 14** — not a look at all; the other three go
to the mapper's look trio (0x44 → attrID 10 LookLeft, 0x43 → 11 LookRight, 0x41 → 12 LookBehind).
The shipped `.RES` has no "TORSO CENTER" string (its names are LookBehind/Down/Forward/Left/Right),
so the audit's phrasing was loose but its substance correct. Swept the three UI tables
(`L4GLASSWIN`, `L4PADPANEL`, `L4VB16`): 0x42 is now "Torso Ctr", 0x41/0x43/0x44 "Look Bk/R/L".
## RGB keyboard lamp mirror — L4KEYLIGHT (2026-07-26) [T2 live, claimed a real board]
Ported from RP412 (itself vRIO's `KeyboardLampMirror`). Keys bound to a lamp address in
`bindings.txt` glow with the panel palette through **Windows Dynamic Lighting** — yellow for the
map's side columns (0x10-0x1F), red for the rest — flashing in step with the on-screen buttons
(its `LampLevel` copy matches the FIXED `BTLampBrightnessOf`). Per-key boards light each bound
key; zone-lit boards mirror the strongest lamp board-wide. All WinRT runs on a private worker
thread. Gate: **OPT-IN since 2026-07-26** -- `BT_KEYLIGHT=1` enables (operator decision: the
player base is not on Windows 11, so the feature was unshipped -- removed from README/
CONTROLS.* -- rather than promised); default is OFF and produces zero log lines. Also note
the CMakeLists BUILD gate: an SDK older than 10.0.22000 compiles a dormant five-function
stub (this build machine does), so shipping the real mirror requires a newer-SDK build.
No Dynamic Lighting at runtime = one log line, then dormant.
⚠ **RP412's packing hazard does NOT apply here.** It compiles that file with forced default struct
packing because *its* engine is `/Zp1`, which would break the WinRT ABI. BT411's `BT_OPTS` is
`/permissive /W0 /wd4996 /EHsc /bigobj /MP` — **no `/Zp`** — so only the dialect flags are needed
(`/std:c++17 /permissive-`, per-file in CMakeLists, since the project otherwise builds C++14
/permissive). The scalars-only interface is kept anyway so the isolation survives if packing is
ever added.
Wired in `PadRIO`: map from `bindings.keyBindings` (ActionButton binds only, first-binding-wins
per key), fed from `PadRIO::SetLamp`, stopped in the dtor (which hands the LEDs back to Windows).
Verified: `[keylight] mirroring 25 bound key(s)` + claimed this machine's 24-zone keyboard;
`BT_KEYLIGHT=0` and the pod profile produce zero keylight lines and run clean.
## Lamp flash decode was wrong — fixed (2026-07-26) [T1, from the L4RIO.h enum]
`BTLampBrightnessOf` (l4vb16.h) returned `max(state1, state2)` and blanked to 0 on the alternate
phase. The engine's own enum [T0] says otherwise: bits 0-1 = flash mode, bits 2-3 = **state 1**
brightness, bits 4-5 = **state 2** brightness (0 off / 1 dim / 3 bright) — solid shows state 1,
flashing ALTERNATES the two at 500/250/125 ms. The old formula agrees only when one state is Off,
which is why it survived: the Panic lamp is `flashFast+state1Off+state2Bright` and L4LAMP's other
pulse is off/dim. But **`L4LAMP.cpp:252` commands `flashFast + state1Dim + state2Bright`** — a
dim→bright pulse that rendered as a hard bright→off blink. RP412's `L4MFDVIEW::LampLevel` has the
faithful formula; that is now ours. THREE copies of the decode existed (l4vb16.h, L4GLASSWIN,
L4PADPANEL), all three with the bug; the two locals now forward to the one inline.
## The pre-mission WAITING SCREEN (2026-07-22) [T2]
Every pre-run state (seat wait, roster full, WaitingForEgg, LoadingMission, WaitingForLaunch)
paints an animated overlay -- Consolas green text + a 12-segment spinner phased on
GetTickCount/80 (`BTWaitScreenPaintDC`, L4VIDEO.cpp) -- so a joining player never sees "Not
Responding" or a frozen frame. Two paint paths: PRE-DEVICE (relay seat wait, L4NET
`RelayWaitTick`) paints the window DC directly (nothing else presents -- safe); ONCE THE
DEVICE EXISTS (`DPLRenderer::ExecuteIdle`) it must NOT paint after Present (the black D3D
frame alternating with GDI = the load-time flicker, user-reported) -- and the backbuffer
refuses `GetDC` (D3DERR_INVALIDCALL without D3DPRESENTFLAG_LOCKABLE_BACKBUFFER, which we
won't force game-wide), so the overlay GDI-paints into a cached OFFSCREEN PLAIN surface
(`mWaitOverlaySurface`, GetDC always legal there) and `StretchRect`s onto the backbuffer
BEFORE Present. D3DPOOL_DEFAULT: released at every device-Reset site. One-shot boot log
`[waitscreen] paint path:` says which path is live. Verified: 14 rapid PrintWindow frames
all carry the text (was alternating black).
## PadRIO gotcha: release-on-disconnect (issue #24, 2026-07-23) [T3 -- awaiting live verify]
XInput button emission is EDGE-based (`held != was_held` vs `previousPadButtons`). The
disconnect path zeroed `previousPadButtons` WITHOUT emitting release edges -- any button held
at the moment a controller (wrapper) died stayed pressed game-side FOREVER, and the zeroing
also erased the memory a reconnect would have released from. Field signature: PS3-pad-in-
xbox-mode wrapper died mid-hold -> look-view latched (stuck side view + pitch-down, no
recovery; keyboard-only run clean). Fix: on the connected->disconnected transition, emit
`EmitButton(address, 0)` for every binding whose mask was held, THEN forget. Not reproducible
headless (needs real hardware unplug) -- logic fix, awaiting the reporter's controller.
## Direct-axis release edge (issue #36, 2026-07-23) [T2 idle-verified; awaiting reporter]
The pad/joy DIRECT axis write was gated on `raw != 0` -- centering inside the deadzone stopped
writing and the channel LATCHED (fatal on spring-less channels: the Turn composite kept the
mech turning after release). Fix: per-binding previous-value arrays (`previousPadAxisRaw` /
`previousJoyAxisRaw`); the outside->inside transition writes 0 ONCE then yields the channel
(composition preserved; slew bindings excluded -- a slewed lever stays put by design). Plus
the #24 rule, axis edition: disconnect-while-deflected releases direct channels (Throttle
keeps the last lever position -- freeze beats all-stop mid-fight).
## CONVENTION: bindings.txt format is a COMPATIBILITY SURFACE (2026-07-24)
Players customize `contentindings.txt` (keys, pad, joystick/HOTAS rows) and the file is
NOT shipped in the zip (untracked -> survives extract-over-top upgrades). That only works if
the grammar stays stable across versions, so:
- **Never rename or repurpose** existing verbs (`key`/`pad`/`padaxis`/`joydev`/`joyaxis`/
`joybutton`/`joyhat`), channel names (`Throttle`/`JoystickX`/`JoystickY`/`LeftPedal`/
`RightPedal`/`Turn`), pad/axis token names, or option words (`invert`/`slew`/`deflect`/
`set`/`deadzone`). A parse change that rejects a previously-valid line silently breaks a
player's setup mid-upgrade.
- **New features = NEW row types or NEW trailing options** (the parser already skips unknown
lines with a logged warning -- old builds degrade gracefully on newer files too).
- Semantics of existing rows must stay put (e.g. deflect = spring, slew = lever, direct pad
axis = outside-deadzone-owns + release edge). If a semantic MUST change, gate it behind a
new option word and keep the bare form as-was.
- The BT_JOYCONFIG wizard writes only between its `# >>> BT_JOYCONFIG` markers -- keep that
contract so hand edits and wizard reruns coexist.
## Generic joysticks / HOTAS / pedals -- DirectInput 8 layer (2026-07-23) [T2 rig-verified; awaiting stick hardware]
Tester ask: configure non-Xbox controllers. **`L4JOY.cpp/.h`** (all builds, lazy-init): DI8
enumeration of every non-XInput game device (up to 4), `c_dfDIJoystick2` polling with
`DIPROP_RANGE` ±32767 + reacquire-on-loss + 3 s hot-plug re-enum, normalized
`BTJoyDeviceState` {8 axes -1..1, 32 buttons, 4 POV hats}. **XInput exclusion** = the
documented wbem-free RawInput check (device path contains `IG_` → VID/PID blacklist vs
`guidProduct.Data1`) — LIVE-verified skipping a real XBOX360 pad. **Bindings**
(L4PADBINDINGS): `joydev <slot> [name-substr]` + `joyaxis <X|Y|Z|RX|RY|RZ|SL0|SL1> axis
<channel> [invert] [slew r] [deadzone f]` + `joybutton <0-31>` + `joyhat <0-3> <dir>` — rows
attach to the last joydev slot; unnamed slot binds attached device #slot. **PadRIO poll**
runs them through the SAME channel/event machinery (per-binding prev arrays → detach releases
automatically, the #24 rule); a direct joyaxis on Throttle maps full travel [-1,1]→[0,1] and
writes every frame (a lever OWNS the channel; slewHeld stays clear so the GAIT DETENT still
snaps a lever parked in the walk/run dead band). **Wizard** `BT_JOYCONFIG=1`
(`BTJoyConfigWizard`, console UI, `joyconfig.bat`): baselines all axes (a HOTAS lever rests
anywhere), captures the dominant mover per prompt (0.45 threshold, 250 ms hold, already-
assigned axes excluded), derives invert from the move direction (throttle from FINAL
position), auto-adds the hat look cluster, and rewrites bindings.txt between
`# >>> BT_JOYCONFIG` markers (rest of the file preserved). Sign conventions: JoystickX + =
twist right, JoystickY + = aim up (wizard binds flight-style forward=down via invert), Turn +
= right. Legacy `L4DINPUT.cpp DIJoystick` (L4CONTROLS=DIJOYSTICK, 1995 `Joystick` iface)
left untouched. Verified: grammar accept/reject by line, no-device + XInput-only polling
clean 30 s in-mission, zero DI cost with no joy rows. NOT yet verified: real stick axes
(no hardware here) — needs a tester with a stick.
## Plan of record (2026-07-17)
Working branch `glass-cockpit` (from master `e2c21c4`). Steps: (1) gates+scaffolding [THIS],
(2) PadRIO glass cockpit, (3) miniconsole self-launch, (4) Steam transport+lobby. Full plan:
the approved plan file; running detail: `docs/GLASS_COCKPIT.md`. The retired BT412 trees
(`C:\VWE\BT412`; local branch `local-abandoned-20260717`) are read-only reference — no code is
copied from them.
## Status
- **Step 1 (gates + scaffolding): DONE 2026-07-17.** CMake options + defines; 3-config
configure matrix + negative test + defaults build verified.
- **Step 2a (.CTL id audit): DONE 2026-07-17 [T2].** The streamed `.CTL` positional ids landed
every `MechControlsMapper` record ONE MEMBER LATE (our chain starts at 2, the binary's at 3)
— a latent REAL-POD bug (RIO throttle would drive pedalsPosition), masked on dev boxes by the
keyboard bridge. Fixed ungated: ids pinned to binary numbering + id-2 pad + static_assert
locks (`mechmppr.hpp/.cpp`); torso/weapon chains were already aligned. Permanent env-gated
diagnostic: `BT_CTRLMAP_LOG=1` dumps every streamed record's resolved member offset
(`L4CTRL.cpp CreateStreamedMappings`). Full evidence: `docs/GLASS_COCKPIT.md` §2a.
- **Step 2 (PadRIO glass cockpit): DONE 2026-07-17 [T2].** `RIOBase` seam (`L4RIO.h`,
unconditional) + gated `L4CONTROLS=PAD` → `PadRIO` (`L4PADRIO.*` XInput+keyboard,
`L4PADBINDINGS.*` `content\bindings.txt`); bridge stand-downs auto-yield to a live device
(`BTRIODevicePresent`, `BT_KEY_BRIDGE` unset=auto/0=off/1=on); on-screen panel `L4PADPANEL.*`
follows **vRIO** (`C:\VWE\vrio` CockpitLayout/PanelCanvas — 104 controls incl. the two hex
keypads → real KeyEvents; right-click latch); desktop plasma `L4PLASMAWIN.*`
(`L4PLASMA=SCREEN`, buffer is top-down); `-platform glass` preset + run.cmd token. Secondary
displays reuse the EXISTING dev-gauge modes (dock / `BT_DEV_GAUGES_WINDOW=1` window /
`_DOCK=1` overlay — [[gauges-hud]]); no new MFD code, no `L4VB16.cpp` exposure.
**Button-trap guard:** base `ConfigureMappableMessageHandler` no longer `abort()`s by default
(`BT_BUTTON_TRAP=1` restores) — each `[FAIL]` log = a missing L4 override to reconstruct.
Verified: authentic-path input (engine push → streamed `.CTL` → mapper), panel-click
survival, 2-node loopback MP un-regressed on the pod build. Detail: `docs/GLASS_COCKPIT.md`.
- **Trigger-config + one-keymap (2026-07-21, HUMAN-VERIFIED live).** The cockpit-surround
clicks only work when a `PadRIO` exists, and PadRIO only exists under the **GLASS platform
profile** (`BT_PLATFORM=glass` → `L4CONTROLS=PAD`); a plain launch falls to the DEV profile
(KEYBOARD) and `PadRIO::SetScreenButton` no-ops — every cockpit click silently discarded
(the "held PROGRAM, nothing happened" incident: `[cockpit] CLICK addr=0x8` logged but
ConfigureMappables id 9 never dispatched). `play_solo.bat` now sets `BT_PLATFORM=glass`
with the glass exe. The full regroup loop (hold PROGRAM → Mapping-mode flip suppresses
fire → fire-key taps toggle the binding → joystick-display dot moves → release commits)
is verified live. Diagnostics: `[cfgmap]` ENTER/EXIT (BT_FIRE_LOG, mechweap.cpp) +
`BT_BTNTEST="addr,on,off"` scripted screen-button harness (L4PADRIO.cpp Poll). **Keymap:**
which binding engine runs follows the same profile split — GLASS = PadRIO/`bindings.txt`,
DEV/pod = btinput/`CONTROLS.MAP` — which made the felt keymap flip with the boot flavor.
Settled 2026-07-21 (commit babf5b1): CONTROLS.MAP + the compiled-in default now mirror the
bindings.txt layout (numpad aim cluster, arrows=throttle/pedals, **Alt=reverse 0x3F**,
NumPad5=AllStop, Shift=throttle) so every boot flavor feels identical.
- **Step 3 (miniconsole): DONE 2026-07-17 [T2].** `game/glass/btl4fe.*` (menu + console-shape
egg writer, golden-tested vs MP.EGG) + `btl4console.*` (the marshal: a worker-thread console
CLIENT speaking the btconsole.py wire verbatim; `content\marshal.log`). ZERO engine hooks —
the engine runs the stock console ladder. Per-mission process relaunch; `BT_FE_*` env arms the
marshal (menu relaunches clear it). Verified: solo cycle (menu→mission→60s clock→stop→menu)
and menu-hosted 2-node LAN.
- **Step 4 (Steam): CODE COMPLETE 2026-07-18 [T2 single-machine].** See [[steam-networking]] —
the wire seam + `L4STEAMNET` identity-token transport + `btl4lobby`. Live 2-account
cross-machine session pending (`docs/STEAM_TEST.md`).
- **Input-coverage audit + keyboard reconciliation: DONE 2026-07-20 [T2].** Root cause of the
dead panel: `MechRIOMapper`'s message ids were chained one enum too high (0x19-0x2a) — the
binary RIO table @0051dd30 re-registers the BASE aux/zoom ids 3..0x13 (Hotbox 0x1a); fixed
ungated + static_assert-locked (`btl4mppr.hpp`) → all 24 MFD-bank + 2 zoom buttons live
(52/72 panel buttons worked at the time; the 8 dead ones are ALL wired as of 2026-07-26 —
`DuckRequest` was the last, see §Dead-button backlog closed; 12 have no streamed mapping
authored, authentically inert). **Keyboard = the ~20 core
gameplay actions on the CONTROLS.MAP keys; the PANEL covers every pod address by click**
(right-click = hold latch) — the merged default profile lives in `L4PADBINDINGS.cpp`
(auto-writes `content\bindings.txt`; W/S/A/D/Q/E/R/F/X drive, 1-4/Space/Ctrl fire, M/N/H/C =
pod buttons 0x18/0x15/0x2C/0x2F, `/V view toggle + J/K/L preset cycles hardcoded in PadRIO).
Bound keys are SUPPRESSED from the typed 1995 channel (`PadRIO::SuppressKey`, the btinput
pattern — ended the glass double-dispatch: 'w'=pilot-select, numpad/F-key key-up aliases);
unbound keys keep their authentic meanings (5/z presets, t-o pilot select, +/- zoom).
In BAS control mode the pod turns with the STICK (Q/E) and pedals are inactive — authentic;
A/D turn in MID/ADV. Arrow keys mirror the drive cluster (UP/DOWN = the W/S throttle lever,
LEFT/RIGHT = the A/D pedals). Full audit + census: `docs/GLASS_COCKPIT.md` §2026-07-20.
- **2026-07-20 live "keymap regression" report: STALE EXE [T2].** The human-reported glass
regressions (arrows/lever/detent dead, V/J/K/L/N inert, ground-fire) were a
`build-glass/Release/btl4.exe` built BEFORE 5dd3536 landed — relaunched without rebuilding.
No input-code change was needed; a clean rebuild + full SendInput re-verification (lever
holds + zero stop, MID turns, V toggle shot-pair, level boresight, pod smoke) closed every
symptom, and V's edge is now [T2]. `BT_MPPR_TRACE` now prints mode/pedals/stick/turn/elev.
Procedural rule: rebuild every live build dir after landing an input/engine commit.
Detail: `docs/GLASS_COCKPIT.md` §2026-07-20 (later).
- **2026-07-20 human re-test: GAIT DETENT ported to the PadRIO lever [T2].** The remaining
"our detent is missing" (mixed walk/run mode + repeated accel sample) was the gait DEAD BAND
([[locomotion]]): the pod-build keyboard lever snaps out of the walk/run hysteresis band at
key-rest (mech4.cpp GAIT DETENT), and the PadRIO throttle slew lacked it. Ported faithfully:
mech4.cpp publishes the band in lever units (`gBTGaitDetentLo/Hi` seam, +0.002 engage
margin), `L4PADRIO.cpp` applies the identical nearer-edge snap when no throttle slew key/pad
axis is active. Verified: parks at 0.414/0.465 snap to 0.358/0.504 (band = demand
22.02-30.87), no at-rest sample in-band, exactly ONE up-shift + ONE down-shift clip across a
full graduated sweep, zero-stop + pod build unaffected. Detail: `docs/GLASS_COCKPIT.md`
§2026-07-20 (later still).
A/D turn in MID/ADV. Full audit + census: `docs/GLASS_COCKPIT.md` §2026-07-20.
- **Per-display cockpit windows (`BT_GLASS_PANELS`): DONE 2026-07-20 [T2 startup-verified].**
The `-platform glass` preset now breaks the secondary displays out of the single combined
panel/strip into ONE desktop window PER pod display, each carrying that display's surface
WITH its RIO button bank placed around it, arranged pod-faithfully around the main view
(`engine/MUNGA_L4/L4GLASSWIN.*`, new TU). Windows: **Heat (UL) / Mfd2 (UC) / Comm (UR) /
Mfd1 (LL) / Mfd3 (LR)** each with 8 RED MFD buttons split 4 above / 4 below the surface;
**Secondary/Radar** (portrait `sec`) with the 12 YELLOW Secondary buttons split left/right;
a **Flight Controls** window for the no-display banks (throttle 0x38-3F, joystick/fire
0x40-47, panic). Each window is a GDI window (like [[gauges-hud]]'s pad panel + the plasma
window) — the surface is CPU-expanded from the shared gauge buffer
(`SVGA16::ExpandPlaneToBGRA`, no D3D) and `StretchDIBits`'d in, so the **whole D3D
dev-composite path (dock / separate window / overlay) stands down** while these are up
(`BTDrawGaugeInset`/`BTGaugeWindowRenderAndPresent` early-return; no `gBTGaugeDockBottom`
strip). Runtime gate **`BT_GLASS_PANELS`** (⚠ was "default ON under `-platform glass`" — NO
LONGER TRUE; the surround is the glass default and panels are explicit-only, see §Layout modes
below) — read by `L4PADRIO` (which panel to create),
`L4VB16` (suppress compositing), and `btl4main` (skip the world-window dock strip). The
display↔bank map (spine of the feature): Heat 0x28-2F, Mfd2 0x20-27, Comm 0x30-37,
Mfd1 0x08-0F, Mfd3 0x00-07, sec 0x10-1B ([[pod-hardware]] §Bank→MFD). Verified: build +
bounded glass launch — `[glasswin] per-display cockpit up (7 windows…)`, no crash, full
mission loop un-regressed; surfaces carry LIVE gauge content (`BT_GLASS_LOG` = per-surface
resolve/pixel diagnostic). Layout constants (surface sizes, exact button sub-positions,
yellow left/right split) are marked tunable in `L4GLASSWIN.cpp`. Detail: `docs/GLASS_COCKPIT.md`.
**Bug found + fixed during bring-up:** surfaces were blank because `application`/`ghWnd` are
duplicate-defined globals that bind non-deterministically under `/FORCE` — the fresh
`L4GLASSWIN` TU read `application == NULL` → gauge renderer NULL. Fix: reach the
renderer/window via `BTResolveGaugeRenderer()`/`BTResolveMainWindow()`, DEFINED in
`game/btl4main.cpp` off its real `btl4App`/`hWnd` pointers (never the globals directly). See
[[reconstruction-gotchas]] §6 duplicate-GLOBAL corollary.
- **Glass per-display panel button click-crash (Gitea #18): FIXED 2026-07-20 [T2].** Clicking a
per-display Engineering panel button (0x21 reported) on an **Eng page** hard-crashed — cdb: a
wild call through an uninitialised pointer (`eip=cdcdcdcd` debug / `0x01048748` release) with
**zero btl4 frames above `Receiver::Receive`**. Root cause was NOT a `/FORCE` unresolved
external (the glass link log carried only the known-benign `mech3.obj` `CreateStreamedSubsystem`
set) but a **dense message-handler-table GAP slot** ([[reconstruction-gotchas]] §11): the
streamed Eng-page `.CTL` dispatches subsystem msg **id 3 / 0xb** to whatever subsystem is shown,
and a **weapon (Emitter)** registers only PoweredSubsystem 4-8 + MechWeapon 9-10 — so id 3 is a
gap below `entryCount`, and `Receiver::MessageHandlerSet::Build`'s `new HandlerEntry[]` left it
as heap garbage → `(this->*garbage)(msg)`. The 1995 binary has the identical non-zeroing
`new[]` and survived only on fresh-heap-zero luck (id 3 on a weapon was always meant to be
IGNORED — it's a Condenser/Reservoir action in a different subsystem branch; the uniform
Eng-page button template makes buttons for unimplemented actions authentically inert). The new
per-display windows just made the id reachable live. **Fix (engine, class-wide, faithful):**
`Build` null-inits every slot (`entryID=0/entryName=""/entryHandler=NullHandler`) before
copying inherited / placing supplied, so a gap is deterministically NullHandler → `Receive`
drops it — the authentic "Receiver ignores unhandled messages", not a glass-path guard. Fixes
the entire dead-button class (#14) at once. Verified: click-soaked every MFD bank
(Engineering/L+R Weapons/Heat/Comm, ~130 clicks through Quad↔Eng page cycles) under cdb — ZERO
AVs, and the reconstructed mapper preset selects still fire (`[mode] preset (1,1)/(2,1)…`); pod
build + solo un-regressed. The `[glasswin] CLICK` forensics log (names the button before
dispatch) landed with this. Detail: `engine/MUNGA/RECEIVER.cpp` Build.
## Bindings migration (board-2, 2026-07-26)
`bindings.txt` files without the `# bindings-board 2` marker are migrated ONCE at load
(`PadBindingProfile::MigrateBindingsFile`, L4PADBINDINGS.cpp): rows matching the union of every
HISTORICAL default set (85 canonical rows, c2ee729..1cda880) are dropped as "the old board, not a
choice"; player-authored rows are carried and win over the new defaults (the losing default is
emitted commented `# (yours wins)`); the joystick wizard's marker-delimited section is preserved
verbatim; the original is saved as `bindings.old.txt`. Idempotent after (marker check). Operator
decision: eliminate the two-population fork the preserved-file convention had created -- there is
no "old layout" world anymore, only per-row customization. Restoring bindings.old.txt just
re-migrates: deliberate. Verified end-to-end with the real exe: carry/stand-down/wizard/backup on
an old-world file, byte-identical idempotency on reboot, marker-headed fresh generation.
## Key Relationships
- Extends: [[pod-hardware]] (RIO input path) · Uses: [[gauges-hud]] dev-gauge modes ·
Feeds: [[multiplayer]] (miniconsole hosting, Steam transport) · Build: [[build-and-run]]