The real cockpit board now drives the cab instead of PadRIO, frozen in the profile so all five tester launchers get it. Nothing above the seam changed -- the 109-mapping L4 control table installs exactly as on a desktop, and the cab keeps the GLASS display stack. BT_PLATFORM=pod is NOT the way to this; that would drag in the 1995 gauge path. RIO:COM1 -> \.\COM1 at 9600 8N1. Evidence the link is real, not just "the port opened": RIO successfully initialized! FAILURE.LOG: 4 missing boards (Slot 3:0, 3:2, 4:0, 5:0), 16 dead lamps [ctrlmap] push stick x=0.0595238 y=0 <- physical stick outside deadband A specific 4-of-many board inventory is the proof: a dead serial line reports the WHOLE address space missing. Reproduced after deleting FAILURE.LOG. The dead lamps are the boards this partial crash cart does not have. Banner honesty: "GLASS (PadRIO ...)" was hardcoded, so a wired cab reported PadRIO on the very line you read to check which device won. It now names the resolved one -- GLASS (hardware RIO; plasma off [L4PLASMA]). Recorded in pod-hardware.md, including that RIO and PAD are mutually exclusive (both assign rioPointer, last token wins) and that a CENTRED stick reads x=0, which is indistinguishable from no data -- so the by-hand check of stick, throttle, pedals, buttons and the Ranger calibration is still outstanding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rw7No5wLTpkaUgA3ANbtZZ
464 lines
36 KiB
Markdown
464 lines
36 KiB
Markdown
---
|
||
id: pod-hardware
|
||
title: "Pod Hardware — the fixed target (7 monitors, D3D9, RIO)"
|
||
status: established
|
||
source_sections: "PROGRESS_LOG.md §3; the PLATFORM PROFILE + GAUGE DEV-COMPOSITE notes"
|
||
related_topics: [gauges-hud, rendering, project-overview]
|
||
key_terms: [pod, RIO, MFD, IG-board, SVGA16]
|
||
---
|
||
|
||
# Pod Hardware (the fixed target)
|
||
|
||
The port must run on (a) a dev box and (b) the fixed arcade pod. The pod is a hard constraint that
|
||
bounds the graphics API. Full detail: `docs/PROGRESS_LOG.md §3`.
|
||
|
||
## Displays
|
||
- **2 video cards → 7 monitors:** main 3D view **800×600**; radar **640×480**; five monochrome MFDs
|
||
driven as one **1280×480** horizontally-spanned surface. [T1]
|
||
- Requires **old NVIDIA drivers** for the MFD horizontal spanning — a hard constraint that bounds
|
||
the graphics API → **Target Direct3D 9** (lowest common denominator that runs on old-driver pods
|
||
AND modern cards; one codepath). NOT Vulkan/D3D11+. CUDA is irrelevant (compute, not display). [T1]
|
||
|
||
## Cockpit I/O (RIO)
|
||
Joystick X/Y, throttle, pedals, buttons over **serial COM** (`L4RIO`, `L4SERIAL`). Must be remapped
|
||
to keyboard/gamepad on a dev box; real wiring is a pod bring-up task (Phase 8). The engine is a PUSH
|
||
model — `LBE4ControlsManager` groups are fed by all devices (RIO on the pod, DirectInput on dev);
|
||
the `MechControlsMapper` interprets them ([[locomotion]]). [T2]
|
||
|
||
### The button space + lamps [T0, L4CTRL.h enum]
|
||
buttonGroup addresses 0x00-0x47: `AuxLowerRight/Left 1-8` (0x00-0x0F), `Secondary1-12` (0x10-0x1B),
|
||
`AuxUpperCenter/Left/Right 1-8` (0x20-0x37; **0x30-0x37 = the target HOTBOX**, pilot select),
|
||
icom/door 0x39-0x3C, `Panic` 0x3D (the config-mode lamp),
|
||
`Throttle1` 0x3F (throttle-head = REVERSE THRUST), joystick cluster 0x40-0x47 (Main trigger 0x40,
|
||
hat 0x41-0x44, Pinky/ThumbLow/ThumbHigh 0x45-0x47 — the four MAPPABLE fire buttons). "Lamp buttons"
|
||
are literal: physical illuminated pushbuttons in panels around the screens; the RIO `LampRequest`
|
||
protocol lights them (`MakeLinkedLamp`), reports burned-out bulbs (`RIODeadLamps` failure page,
|
||
per-lamp names). A lit button = active. Keypads are NOT buttons: `keyboardGroup[KeyboardPilot/
|
||
KeyboardExternal]` carry key VALUES ('0'-'9','A'-'F', L4CTRL.cpp:2526). RIO event convention:
|
||
press = `buttonGroup[a].Update(a+1, modeMask)` (mask saved), release = `Update(-a-1, savedMask)`
|
||
(L4CTRL.cpp:2470-2520).
|
||
**Bank→MFD assignment (Gitea #9, from the streamed "L4" .CTL EventMapping dump [T1]):** each
|
||
preset-able MFD owns the 8-button bank AROUND it, mode-mask-gated per page — **Mfd1 (lower
|
||
left) = 0x08-0x0F (AuxLowerLeft), Mfd2 (upper center) = 0x20-0x27 (AuxUpperCenter), Mfd3
|
||
(lower right) = 0x00-0x07 (AuxLowerRight)**: Quad page → the outer buttons direct-select the
|
||
populated Eng pages; Eng page → one button back to Quad, the rest drive the shown subsystem
|
||
(generator select A-D msg 4-7, gen mode msg 8, weapon configure msg 9; also **msg 0x3 =
|
||
ToggleCooling — WIRED 2026-07-20**: the button→msg-3 route was always authored (`BT_CTRLMAP_LOG`
|
||
shows ~a dozen `EVENT msg 0x3` entries, e.g. Eng-page aux `elem 0x21` mask-gated per ModeMFD page
|
||
to each weapon), but was "silently dead" only for lack of the id-3 handler. Reconstructed
|
||
`HeatSink::ToggleCoolingMessageHandler` (@004ad6f8) + restored the `PoweredSubsystem→HeatSink`
|
||
handler chain, so the Eng-page "Coolant" button now toggles the shown weapon's coolant flow. msg
|
||
0xb = ToggleSeekVoltage (energy) / EjectAmmo (ammo weapons) still unreconstructed). **Always-active records IDENTIFIED 2026-07-20 [T1]** (roster:
|
||
sub 3 = Reservoir, 4-9 = Condenser1-6, 10-13 = GeneratorA-D): **0x2C = the COOLANT FLUSH
|
||
button** (Reservoir InjectCoolant, hold-to-flush — works), **0x2F/0x2E/0x2D/0x2B/0x2A/0x29 =
|
||
the per-condenser VALVE buttons** (MoveValve, Cond1-6 — work), **0x1A-0x1D = Generator A-D
|
||
ON/OFF** (`ToggleGeneratorOnOff` id 4, binary table @0050fb90 fn @004b1ed0 — ✅ **WIRED**,
|
||
`powersub.cpp`). Newly decoded from the binary message tables: **0x13 → Mech `DuckRequest`
|
||
(0x1a @0049fa00 — the manual's CROUCH button)** — ✅ **COMPLETE 2026-08-06**: handler (07-26) +
|
||
the master-perf posture CONSUMER + squat/rise clips + MP replication ([[locomotion]] §CROUCH;
|
||
glass key F4); the map legend's `bduck.pcc` widget still lights. (The old "no code consumer"
|
||
verdict was export-gap blindness — [[reconstruction-gotchas]] §20) —, **0x28 → Mech `BalanceCoolant` (0x16
|
||
@0049f728)** ✅ **WIRED 2026-07-21 (#20)**, **0x12 → ThermalSight `ToggleLamp` (id 3, table @0x51120C fn @004b860c)** and **0x14 →
|
||
Searchlight + Searchlight2 `ToggleLamp` (id 3, table @0x51117C fn @004b838c)** — ✅ **BOTH WIRED
|
||
2026-07-25 (#61)**, previously default-constructed blackholes; verified live (0x14 →
|
||
`lightState 0→1`, 0x12 → `thermalActive 0→1`). ⚠ The fn addresses were **swapped** in this file
|
||
before #61 (@004b860c is ThermalSight's) — see [[decomp-reference]] for the table evidence — and
|
||
that swap had propagated a false "1995 latent bug" (retracted, [[subsystems]] WAVE 4).
|
||
An unhandled message is SILENTLY ignored
|
||
(Receiver::Receive finds no handler and does nothing — dead buttons produce NO log).
|
||
0x10/0x11 = map ZoomIn/Out, 0x15 = CycleDisplayMode, 0x18 = CycleControlMode (#6). Buttons
|
||
with NO streamed mapping authored (authentically inert): 0x16/0x17/0x19/0x1E/0x1F/0x38-0x3E.
|
||
Full decode: [[gauges-hud]] §preset pages; the complete 72-button census + verification:
|
||
`docs/GLASS_COCKPIT.md` §2026-07-20.
|
||
|
||
### Desktop input remap — CONTROLS.MAP + XInput (2026-07-18) [T2 live]
|
||
`game/reconstructed/btinput.cpp` + `content/CONTROLS.MAP` (WASD-classic default, compiled-in twin;
|
||
`content/CONTROLS_NUMPAD.MAP` = the corrected community numpad profile). Grammar: `key <name>
|
||
button <addr>|axis <axis> deflect|rate <n>|keypad pilot|external <0-15>|pckey <char>|action
|
||
<name>`; `pad <button>` same targets; `padaxis <src> axis ... [invert] [deadzone d] [rate n]`.
|
||
Axes: Throttle (lever, rate-walks + sticks), LeftPedal/RightPedal/Pedals (turn, springs),
|
||
JoystickX (torso twist), **JoystickY (torso ELEVATION — pitch aim; every 1995 control mode
|
||
routes it to Torso::SetAnalogElevationAxis; keys R/F + pad LeftStickY in the default map;
|
||
verified to the authentic VerticalLimitTop clamp, 20° on the Blackhawk; aims the GUNS — reads
|
||
on the HUD elevation tape, the eyepoint stays level)**.
|
||
**AXIS SIGN CONVENTION (2026-07-18, user-verified) [T1]:** the sim uses MATH convention —
|
||
positive turnDemand / twist / free-aim slew is **CCW (LEFT)**. On the pod the RIO Ranger owned
|
||
the hardware sign; the desktop key bridge negates ONCE per channel (`key_turn = -gBTDrive.turn`;
|
||
`stickPosition.x = -gBTTwistAxis`; the SetFreeAimSlew call-sites pass the bridge value un-negated)
|
||
so input-right = turn/twist RIGHT (manual p8: "pulling your joystick to the right torso twists
|
||
your 'Mech to the right"). Forced/BT_GOTO harness demands are already sim-frame and stay
|
||
un-negated. Lesson: large-rotation screenshot comparisons are AMBIGUOUS — trust tracked
|
||
landmarks, numeric yaw telemetry with an established convention, or the user's live observation.
|
||
Emissions mirror the RIO conventions above exactly; the four fire
|
||
buttons (0x40/45/46/47) publish as levels to the bring-up fire channels instead of buttonGroup
|
||
(avoids double-fire once the streamed ChooseButton mappings are exercised). XInput loads
|
||
dynamically (xinput1_4 → 9_1_0; disconnected-pad probing rate-limited to 1 Hz). **Keys claimed by
|
||
a binding are SUPPRESSED from the legacy WM_CHAR/KEYUP feed** (`BTInputSuppressKey`, hook at
|
||
L4CTRL.cpp keyboard read) — this ended the historic DOUBLE-DISPATCH: 'w' drove AND selected pilot
|
||
0; F5's key-up value 0x74 aliased to the 't' hotkey; letter key-ups (uppercase) fed the developer
|
||
fake-event dispatcher. Unbound keys keep their authentic 1995 dispatcher meaning (reachable from
|
||
any key via `pckey`). The old dual-use 'V' (view toggle + look-behind) is split: V = ViewToggle,
|
||
B = LookBehind. Verified live: bindings load, W/NumPad8 drive (spd rises), X all-stop, aux-button
|
||
emission ([input] 0x2f PRESS), suppression both directions ('r' delivered / 'w' swallowed),
|
||
full-keyspace WM_CHAR+WM_KEYUP fuzz survived. Diag: `BT_INPUT_LOG`.
|
||
|
||
### The pod throttle is ANALOG-CONTINUOUS, not notched (verified end-to-end) [T1]
|
||
The authentic pod throttle path — traced 2026-07 (task #50 throttle-fidelity question):
|
||
1. **Hardware → RIO:** serial `AnalogReply` packet → `Ranger("Throttle", 0, 800, .05)` — raw counts
|
||
0–800, 5% deadband, auto-ranging offset, output a CONTINUOUS Scalar; the sign is inverted
|
||
("Throttle counts BACKWARDS", `engine/MUNGA_L4/L4RIO.cpp:1374-1377`, Ranger @L4RIO.cpp:461-701).
|
||
No quantization/notching anywhere in `Ranger::Update`.
|
||
2. **Manager:** `LBE4ControlsManager::Execute` → `scalarGroup[ScalarThrottle].Update(&rioPointer->
|
||
Throttle, mode_mask)` on every AnalogReply (`L4CTRL.cpp:1379-1382`). ScalarThrottle = index 0 →
|
||
manager+0x24 (scalarGroup base 0x24, 0x20/entry; buttonGroup base 0x1c0, keyboardGroup 0x160).
|
||
3. **Streamed `.CTL` mapping (the "handled elsewhere"):** `MechRIOMapper`'s ctor @004d266c binds NO
|
||
throttle — the bind comes from the type-19 `ControlMappingStream` resource named **"L4"** (child
|
||
of the per-mech type-6 `ControlMappingsList`; installed by `BTL4APP MakeViewpointEntity` via
|
||
`CreateStreamedMappings` @0047703c). BTL4.RES "L4" record [1]:
|
||
`Scalar Throttle → subsystemID 0 (ControlsMapper slot), DirectMapping, attr 4, mask 0xffffffff`.
|
||
Attr 4 = "ThrottlePosition" @ mapper+0x11c (binary IndexEntry table @0050efd8: id 4 → offset
|
||
0x11d-1 = 0x11c, name "ThrottlePosition" @0050f28f). Reverse = record [2]: `Button Throttle1
|
||
(0x3F, on the throttle handle) → attr 6 ReverseThrust@0x124`. Turn = pedals value-bound in the
|
||
ctor (manager+0x44/+0x64 → mapper+0x1b4/+0x1b8).
|
||
**Step-2a fix (2026-07-17) [T2]:** these ids are POSITIONAL (`Find(id)` = `index[id-1]`) and
|
||
our `MechControlsMapper` chain started one low (`Subsystem::NextAttributeID`==2 vs the
|
||
binary's 3) — every streamed record landed one member late (a real-pod landmine: the RIO
|
||
throttle scalar drove `pedalsPosition`). Ids are now PINNED to the binary numbering with an
|
||
id-2 pad + `static_assert` locks (`mechmppr.hpp/.cpp`); audit tool `BT_CTRLMAP_LOG=1`.
|
||
**⚠ The streamed BUTTON mappings (like record [2]) are NOT CONSUMED yet** — the SCALAR records
|
||
drive their attributes, but `MechControlsMapper::AddOrErase` (both overloads, @004b02b0/
|
||
@004b02d4) is still the unreconstructed `Fail("Unhandled mapping!")` stub, so a streamed
|
||
button never reaches its attribute. Consequence found the hard way (user report 2026-07-24,
|
||
"i cant seem to reverse"): **REVERSE THRUST was dead on the desktop.** LALT→0x3F was bound and
|
||
PadRIO pushed the RIO ButtonPressed event correctly, but nothing applied it to attr 6, and the
|
||
only writer of `reverseThrust` was the keyboard bridge's `key_throttle < 0` test — which is
|
||
(a) bypassed whenever a RIO is operational (the pad RIO always is, so the bridge is OFF on the
|
||
desktop) and (b) unreachable anyway, since `L4PADRIO` clamps the Throttle channel to **[0,1]**
|
||
(`low = 0.0f`). Fixed by publishing the 0x3F hold state from `PadRIO::EmitButton` (the single
|
||
chokepoint keyboard/pad/joystick/glass clicks all share) and applying it in
|
||
`InterpretControls`, **gated on `BTPadRIOActive()`** so pod hardware is untouched — marked
|
||
[T3], delete when streamed button mappings land. Verified: hold LALT ⇒ `rev=1`,
|
||
`speedDemand −61.5`; release ⇒ forward. Reverse is a **HOLD**, per the manual. [T2]
|
||
Details: `docs/GLASS_COCKPIT.md` §2a; see [[glass-cockpit]].
|
||
4. **Interpretation:** `L4MechControlsMapper::InterpretControls` @004d196c applies the ONLY software
|
||
detent — snap to 1.0 when |t−1.0| ≤ 0.05 — then `MechControlsMapper::InterpretControls` @004afd10
|
||
computes `speedDemand@0x128 = maxSpeed(mech+0x34c) × throttlePosition(0x11c) × scale(mech+0x5c0)`
|
||
(forward) or `−maxSpeed × throttlePosition` (reverse flag 0x124).
|
||
|
||
**Consequence:** the original pod produced a continuously varying speedDemand while the lever moved
|
||
(updated per serial AnalogReply, NOT per render frame). There are NO "5 throttle notches" anywhere
|
||
in the software path. (Keyboard keys '1'-'5' in @004d1bf0 set controls-manager MODE masks; the
|
||
'+'/'-' pair steps a [0,5] value that drives `pow(2,x)` into mech+0x404 = HUD zoom 1×–32× — neither
|
||
is a speed setting.) The mech's `throttleState@0x4a4` writer remains un-exported (likely in the
|
||
0x4a9b5a–0x4ab188 gap) — [[open-questions]].
|
||
|
||
**MECHANICAL-notch hypothesis (2026-07-16) [T4, hardware]:** the "5 speeds" pod lore may still be
|
||
true — as detents in the throttle QUADRANT hardware, invisible to the software (the pot reads
|
||
continuous counts regardless of where the lever mechanically rests). Independent corroboration
|
||
from the gait math ([[locomotion]]): the gait SM has NO stable state for a demand between the walk
|
||
cap (`walkStrideLength`@0x534) and the run engage speed (`reverseSpeedMax`@0x538, the walk→run
|
||
transition clip's exit speed) — a demand PARKED in that band hunts walk→shift-up→shift-down
|
||
forever, firing the authored EngineShiftFwd/Rev sounds each swing (binary-verified, all-authentic
|
||
data: Blackhawk band = demand 22.02–30.87 = throttle 36–50%). A design like that only ships if the
|
||
hardware discourages parking in the band. Port accommodation: the keyboard lever snaps out of the
|
||
dead band AT REST (`mech4.cpp` "GAIT DETENT"; sweeping through while held stays continuous =
|
||
authentic moving lever). Ask Nick: did the pod throttle quadrant have mechanical detents
|
||
(how many / positions)? — [[open-questions]].
|
||
|
||
## Multi-surface gauge path (intact, pod-only by default)
|
||
The pod multi-surface path EXISTS and is intact: `DPLRenderer::FindBestAdapterIndices` (multi-
|
||
adapter selector, honors PRIMGAUGE/SECGAUGE/MFDGAUGE/SPANDISABLE), `SVGA16::BuildWindows` (a
|
||
fullscreen D3D device + window per gauge/MFD surface, MFD span = width×2), `L4GaugeRenderer` (gated
|
||
on `L4GAUGE`). `content/SETENV.BAT` is the authentic pod env preset (L4CONTROLS=RIO,KEYBOARD;
|
||
L4GAUGE=640x480x16; L4PLASMA=com2). [T2]
|
||
|
||
## Platform profile switch
|
||
`-platform pod|dev` (or `BT_PLATFORM` env; default DEV) selects a runtime env preset WITHOUT forking
|
||
the codepath. **DEV** = keyboard + single 800×600 window. **POD** = RIO input (keyboard fallback off
|
||
serial) + multi-surface gauges (from SETENV.BAT/L4GAUGE on real hardware). `-platform pod` does NOT
|
||
auto-enable L4GAUGE (each surface needs its own fullscreen device on the pod's 2 cards). Off-pod, the
|
||
6 MFD surfaces render in a dev window via `BT_DEV_GAUGES` ([[gauges-hud]]). [T2]
|
||
|
||
## MFD surface model
|
||
All cockpit surfaces are bit-plane MASKS over ONE shared `SVGA16` pixelBuffer: `sec`=palette low
|
||
byte; `Heat`=0x4000, `Mfd2`=0x0400, `Comm`=0x8000, `Mfd1`=0x0100, `Mfd3`=0x1000; `Eng1-3` =
|
||
engineering-mode alt planes; `overlay`=0x00C0 (shares the sec surface). See [[gauges-hud]]. [T2]
|
||
|
||
## ⭐ THE RGB SPLIT — how ONE VGA port drives THREE mono MFDs (decoded 2026-08-06) [T0 engine source + T1 authentic pod config]
|
||
The five monochrome MFDs are NOT five video outputs. Each VGA port's **R, G and B analog lines
|
||
are split to three separate monochrome monitors**, and the software puts a different MFD in each
|
||
colour channel of one shared palettized framebuffer. Mechanism, end to end:
|
||
|
||
1. **Every surface is a bit-plane + a CHANNEL.** `content/GAUGE/L4GAUGE.CFG` (the authentic 1996
|
||
pod config) configures each port as `configure(idx, port, rotation, bitMask, clut, COLOUR, palette)`:
|
||
| port | panel | mask | clut | channel |
|
||
|---|---|---|---|---|
|
||
| `Comm` | upper right | 0x8000 | clut2 | **red** |
|
||
| `Mfd2` (Engineering) | upper centre | 0x0400 | clut2 | **green** |
|
||
| `Heat` | upper left | 0x4000 | clut2 | **blue** |
|
||
| `Mfd1` | lower left | 0x0100 | clut1 | **red** |
|
||
| `Mfd3` | lower right | 0x1000 | clut1 | **green** |
|
||
| `sec` (+`overlay` 0x00C0) | secondary/radar | 0x003F | clut0 | **rgb** (full colour, rotation 270 — the physically ROTATED portrait CRT) |
|
||
`Eng1/2/3` are the engineering-page twins of Mfd1/2/3: same monitor, second bit-plane, switched
|
||
by `reconfigure(...)` giving one plane the channel and the other `blank`.
|
||
2. **The channel assignment is literally a palette write.** `L4GraphicsPort::BuildSecondaryColor`
|
||
(L4VB16.cpp) walks the palette entries owned by the port's bit group (`BitWrangler(byteMask,8)`)
|
||
and writes ONE component: `RedChannel -> triplet->Red`, `GreenChannel -> ->Green`,
|
||
`BlueChannel -> ->Blue`, `AllChannels -> the whole triplet`. `BlankColor` blanks the group
|
||
(`BlankPalette()`), which is how a page swap silences the plane it replaces. The
|
||
`*TransparentZero` variants skip colour 0 so zero reads as transparent for that group.
|
||
3. **So the DAC output carries three independent pictures**, one per analog line, and the splitter
|
||
hands each line to its own mono monitor. Three MFDs per VGA port; the pod's two MFD ports are
|
||
the **1280x480 "horizontally spanned" surface** (2 x 640x480 halves) the Displays section
|
||
describes — clut2 = the upper row (Comm/Mfd2/Heat), clut1 = the lower row (Mfd1/Mfd3, blue
|
||
spare). The radar rides its own port in real colour.
|
||
|
||
**Why this matters for the port [T2]:** our modern path renders each surface as its own
|
||
mono-tinted window on its own Windows display (see §MFD PANELS ON REAL HARDWARE), which is right
|
||
when every panel has its own output. **On splitter-wired glass it is wrong** — three monitors
|
||
would share one Windows display and each would show only its channel's share of a single tinted
|
||
image.
|
||
|
||
**`BT_POD_RGB=1` — the CHANNEL-COMPOSITE mode (built + field-verified 2026-08-06) [T2].** It
|
||
collapses the six panel windows into **three**: two VGA-port windows plus the radar. Each port
|
||
window composites its group's planes into ONE 640×480 BGRA image, each member ORed into its own
|
||
channel mask (`0x00FF0000` / `0x0000FF00` / `0x000000FF`), so the analog splitter downstream hands
|
||
each mono CRT exactly its picture. Grouping follows `L4GAUGE.CFG`: *VGA Port A* = Heat + Comm +
|
||
Mfd2, *VGA Port B* = Mfd1 + Mfd3; the radar keeps real colour on its own port.
|
||
`SVGA16::ExpandPlaneToBGRA` does the per-plane extraction, and `BT_POD_RGB` implies
|
||
`BT_POD_SURFACES`. Two companion gates came out of the bring-up: **`BT_POD_CHANMAP=<Port>=<colour>,…`**
|
||
remaps a surface's colour line when a cab's harness disagrees with the 1995 config (it did — see
|
||
§ALPHA-MR), and **`BT_POD_IDENT`** / **`BT_POD_CHANTEST`** paint identifying/cycling test patterns
|
||
so you can tell which physical CRT is which without trusting the desktop layout.
|
||
**SETTLED:** Nick's cart IS splitter-wired — its three 640×480 "displays" are VGA outputs, each
|
||
feeding a three-monitor splitter, and the composite is what lit the glass correctly.
|
||
|
||
## MFD PANELS ON REAL HARDWARE — the bring-up path (2026-08-06) [T2 local / T4 on-pod]
|
||
Nick's crash cart (pod hardware + Chrome Remote Desktop on a burner account) is the first chance
|
||
to drive the real panels. **The 1995 display path is NOT the way in.** That rig spanned the five
|
||
MFDs as ONE 1280×480 surface via **NVIDIA Horizontal Span, which every driver after XP dropped**;
|
||
`SVGA16::BuildWindows` also wants an exclusive-fullscreen D3D device per adapter, which is
|
||
fragile on modern drivers and over a remote session. Both are still in-tree and still the
|
||
authentic reference — they are just not the bring-up route.
|
||
|
||
**The route is the glass per-display windows** ([[glass-cockpit]]): one window per surface, which
|
||
maps 1:1 onto one physical panel per MFD, needs no special driver, and is already field-proven on
|
||
desktops. Two pieces were added for the cab:
|
||
- **`BT_POD_SURFACES=1` (pod surface mode, L4GLASSWIN)** — crops every window to its SURFACE
|
||
(MFDs exactly 640×480, radar 480×640 portrait), drops the on-screen RIO button banks (the cab's
|
||
buttons are PHYSICAL — drawing fake ones over a real panel is precisely wrong), goes frameless,
|
||
and does not create the Flight Controls pad at all (6 windows, not 7). Per-window equivalent:
|
||
append `,bare` to a line in `glass_layout.cfg` (mixed rigs).
|
||
- **Placement receipts** — every window logs `[glasswin] '<title>' surface=<port> at X,Y WxH
|
||
bare -> monitor \.\DISPLAYn (origin WxH, PRIMARY)`. On a cab nobody can see seven surfaces at
|
||
once, and over CRD you cannot see the panels at all: the log IS the confirmation that a picture
|
||
landed on the right glass.
|
||
|
||
**Runbook** (`tools/podprobe.ps1`, PowerShell, no install/admin — run it ON the pod PC):
|
||
1. Probe: GPUs, every monitor's virtual-desktop rect, EDID make/model (identifies the original
|
||
panels), serial ports (the RIO board), session type, and a PROPOSED `glass_layout.cfg` that
|
||
assigns the six surfaces to the non-primary monitors top-to-bottom/left-to-right, centred.
|
||
2. Drop the cfg in the game's working directory; run with `BT_GLASS_PANELS=1
|
||
BT_POD_SURFACES=1 BT_GLASS_LAYOUT=load` (`=save` to persist drags instead).
|
||
3. Read the receipts; re-assign titles to monitors in the cfg until each picture is on its panel.
|
||
Surface→panel roles: Heat MFD = upper left (coolant), Engineering = upper centre, Comm MFD =
|
||
upper right (hot box), Left/Right Weapons = lower left/right, Secondary/Radar = the secondary
|
||
screen (portrait). Main 3D view stays the game's own window on the main-view monitor.
|
||
|
||
**Known constraints / open on-pod questions:** whether the panels are attached to this PC at all
|
||
(the probe answers it); whether Windows offers a 640×480 mode on them (if not, the surface renders
|
||
at native size CENTRED, not scaled — a scale-to-fit option is the obvious follow-up); whether
|
||
Chrome Remote Desktop holds the CONSOLE session (it should — an RDP session would get a virtual
|
||
display and light nothing); and the RIO serial input, which is a separate task from the displays.
|
||
|
||
## ALPHA-MR — the verified cart config, FROZEN (2026-08-06) [T2, field-verified by eye]
|
||
The bring-up above landed: real pictures on real pod glass, then user-confirmed correct
|
||
("ok this is correct"). What that took, and where it now lives permanently.
|
||
|
||
**The rig.** Win10 IoT Enterprise LTSC on the cart; Intel HD630 + a "Trigger 6 External Graphics"
|
||
USB adapter. Four console displays: **DISPLAY3 800×600 PRIMARY at 0,0** (main view), plus
|
||
**DISPLAY4 @800,122**, **DISPLAY2 @1440,123**, **DISPLAY1 @2080,124** (each 640×480). Physical
|
||
cab: five mono MFD CRTs (two left stacked, two right stacked, one centre loose on the desk) plus
|
||
the colour radar LCD in the yellow-button frame.
|
||
|
||
**The mapping** (`glass_layout.cfg`, `monitor:<name>` binding — see [[glass-cockpit]]):
|
||
```
|
||
Secondary / Radar = monitor:DISPLAY4,bare # colour LCD, yellow-button frame
|
||
VGA Port A = monitor:DISPLAY2,bare # RGB triple: Heat + Comm + Mfd2
|
||
VGA Port B = monitor:DISPLAY1,bare # RGB triple: Mfd1 + Mfd3
|
||
```
|
||
Three windows, not six — under `BT_POD_RGB` each VGA-port window carries three MFDs on its three
|
||
colour planes (§THE RGB SPLIT). Desktop-coordinate order says NOTHING about physical cab position:
|
||
the first assignment, made by sorting monitors left-to-right, was wrong. Only eyes on the glass
|
||
settle it.
|
||
|
||
**Two deviations from the 1995 config, both real, both this cab's wiring:**
|
||
- **`BT_POD_CHANMAP=Comm=blue,Heat=red`** — kills/deaths and coolant loops came up on each
|
||
other's panel. `L4GAUGE.CFG` has Comm on red and Heat on blue; THIS cab's splitter harness
|
||
wires them the other way. The env override remaps a port's colour channel without touching the
|
||
authentic config file. [T2]
|
||
- **`BT_GAUGE_SEC_ROT=0`** — the radar here is a LANDSCAPE LCD, so the 640×480 source needs no
|
||
rotation. The 1995 pod's 270° rotation existed because its secondary CRT was mounted PORTRAIT;
|
||
it is a mounting fact, not a format fact. (En route: 180° looked plausible and was upside down.)
|
||
|
||
**Frozen in `content/environ.ini`**, not in a launcher — `scratchpad/pod/podprofile.ini` is the
|
||
block, `scratchpad/pod/mergeprofile.ps1` merges it idempotently between markers. environ.ini is
|
||
read before anything touches the environment ([[build-and-run]]), and the real environment still
|
||
WINS, so a .bat can override any of it for a one-off. The cart's `runpod.bat` now sets only
|
||
`BT_LOG`/`BT_GLASS_LOG` and the `-egg`; `runpod_env.bat` keeps the all-inline version as the
|
||
fallback. Two gates were added for this:
|
||
- **`BT_FIT=1`** — the env spelling of `-fit`, so the borderless main view survives however the
|
||
game is started (shortcut, scheduled task, autostart) instead of riding on one launcher's
|
||
command line. It fits `MONITOR_DEFAULTTOPRIMARY` = DISPLAY3, which is the main-view panel.
|
||
- **`L4PLASMA=NONE`** (also `OFF`/`0`) — no marquee at all. Needed because the GLASS profile
|
||
force-defaults `L4PLASMA=SCREEN`, so simply leaving it unset still puts a desktop plasma window
|
||
on the pod's glass. The boot banner reports `plasma off [L4PLASMA]` when it takes.
|
||
Verified on build 4.11.813 with the launcher carrying none of it: `[boot] environ.ini: 9
|
||
setting(s) applied`, `[cockpit] -fit: borderless 800x600`, `[glasswin] radar rotation 0 (none)`,
|
||
and all three surfaces on their intended `\\.\DISPLAYn`.
|
||
|
||
**Surviving a NEW BUILD, not just a reboot.** Both frozen files live INSIDE the versioned install
|
||
(`content\environ.ini`, `content\glass_layout.cfg`) and NEITHER ships in the zip — environ.ini is
|
||
generated on first run. So extracting a new build gives a cab that comes up wrong with no error
|
||
anywhere. The masters therefore live at the stable `C:\bt411\` (`podprofile.ini`,
|
||
`glass_layout.cfg`, `podkit.ps1`), and **`setup_pod.bat` pushes them into the newest `BT411_*`
|
||
folder** — run it once after each extract. `runpod.bat` resolves the newest install and applies
|
||
the kit itself before launching, so the remote path needs no hand-editing per build. Re-tuning the
|
||
cab means editing the MASTER at `C:\bt411\`, not the install's copy: the apply overwrites it, on
|
||
purpose, so there is one place to look when a panel moves. Repo copies: `scratchpad/pod/`.
|
||
|
||
**The tester launchers work on the cab unchanged (verified 2026-08-06) [T2].** `play_solo.bat`,
|
||
`join.bat`, `join_lan.bat`, `play_steam.bat` and `joyconfig.bat` set `BT_PLATFORM=glass`,
|
||
`BT_START_INSIDE=1`, `BT_DEV_GAUGES=1` plus their own mode flag — and **not one pod key** between
|
||
them. So each inherits the whole rig from environ.ini without knowing the pod exists. This is the
|
||
argument against a separate pod-only settings file: a second file would need every launcher to opt
|
||
in, which is the shape of the "competing keymaps" bug that made glass the default profile.
|
||
Observed: `play_solo.bat` on the cart logs `environ.ini: 7 setting(s) applied` — 7 not 9, because
|
||
the bat sets `BT_PLATFORM` and `BT_START_INSIDE` itself and the real environment wins.
|
||
Steam MP needs the **client installed and logged in** (AppID 480/Spacewar, so any account, no
|
||
purchase); installed on the cart 2026-08-06. ⚠ **#68: a failed JOIN/HOST exits the process
|
||
SILENTLY** — "Steam not running" and a real crash look identical to a player.
|
||
|
||
### The HARDWARE RIO is live on the cart (2026-08-06) [T2]
|
||
`L4CONTROLS=RIO:COM1,KEYBOARD` in the frozen profile — the real cockpit board instead of PadRIO.
|
||
The whole stock path above the seam (mapper, lamps, streamed `.CTL` mappings) runs unchanged; the
|
||
109-mapping `L4` control table installs exactly as it does on a desktop. **This is independent of
|
||
the platform profile** — the cab keeps the GLASS display stack (panels + RGB split) and only the
|
||
input device changes, which is what the glass/PadRIO seam was built for. Do NOT reach for
|
||
`BT_PLATFORM=pod` to get it.
|
||
|
||
- **Wiring:** `RIO:COMn` → `\\.\COMn` at **9600 8N1** (`L4SERIAL`); bare `RIO` defaults to COM1.
|
||
Reads are non-blocking (`ReadIntervalTimeout=MAXDWORD`, totals 0), so an absent or dead board
|
||
cannot hang the game — it just goes quiet.
|
||
- **RIO and PAD are MUTUALLY EXCLUSIVE** — both assign `rioPointer` in the `L4CONTROLS` parser, so
|
||
the last token wins. Selecting the hardware RIO turns the XInput pad OFF on that machine.
|
||
`KEYBOARD` is safe alongside: it only sets `flags.keyboardExists` and never touches
|
||
`primaryControlType`.
|
||
- **The board's own self-test is the diagnostic.** `RIO::RIO` runs it (`perform_tests = True` by
|
||
default) and writes `content\FAILURE.LOG` pages `RIOBoardErrors` / `RIODeadLamps` / `RIOErrors`,
|
||
deleting them first so the file is always current. On the cart, reproducibly: **4 missing boards**
|
||
(`Slot=3:Address=0`, `3:2`, `4:0`, `5:0`) and **16 dead lamps** (`AuxUpperRight1-8`,
|
||
`AuxUpperCenter1`, `TeslaRelay1/3`, `PanicButton`, `IcomAmpEnableRelay`, `IcomIncRelay`,
|
||
`FloorEntry`, `undefined_0x17`) — the crash cart is a PARTIAL cab, and those are the boards it
|
||
does not have. **A specific 4-of-many inventory is itself the proof the link is good**: a dead
|
||
serial line reports the whole address space missing, not four slots.
|
||
- **Input reaching the mech** was seen live as `[ctrlmap] push stick x=0.0595238 y=0` with nobody
|
||
at the cab — the physical stick sitting just outside the 5% deadband. NB when the stick is
|
||
centred this reads `x=0`, which is indistinguishable from no data: a centred-stick zero proves
|
||
nothing either way. ⚠ **Not yet checked by hand:** that stick/throttle/pedal travel and the
|
||
physical buttons drive the mech correctly, and whether the cab's pots match the `Ranger`
|
||
calibration baked into `RIO::RIO` (`JoystickX -96..96`, `JoystickY -97..108`, `Throttle 0..800`,
|
||
pedals `0..470`). A mismatch shows up as drift or short travel, not as an error.
|
||
- The boot banner names the resolved device — `GLASS (hardware RIO; plasma off [L4PLASMA])`. It
|
||
used to hardcode "PadRIO", which is a lie on a wired cab and exactly the line you read to check.
|
||
|
||
**Working on the cart, remotely.** SSH over Tailscale lands in **session 0**, which has a dummy
|
||
"WinDisc" display and CANNOT see or enumerate session 1's windows — GUI work must go through
|
||
`schtasks /run /tn BT411Run` (task registered `/IT`), and window enumeration over SSH silently
|
||
returns nothing rather than failing. Kill `btl4.exe` BEFORE scp'ing a new exe or the file is
|
||
locked. The receipts in `podrun.log` are the remote eyes; a missing `DEBUG_STREAM` line in an
|
||
otherwise-logging run is real evidence that code path did not execute.
|
||
|
||
## The 1995 player manual — alignment audit (2026-07-18) [T1, primary source]
|
||
`reference/manual/Tesla40_BT_manual.pdf` (34pp, from Nick). CONFIRMS the reconstruction on
|
||
every checked control behavior:
|
||
- **Control modes are named BAS / MID / ADV** (basic/middle/advanced; our Basic/Standard/Veteran
|
||
names came from the RP analog — mechanics identical): BAS = joystick turns the mech, pedals
|
||
inactive, NO torso twist; MID = pedals turn + auto-slow for tightest radius, joystick = torso;
|
||
ADV = as MID but NO auto-slow (wider radius at speed) — exactly our mapper's turn-clamp
|
||
difference. Mode button = top right of the secondary screen.
|
||
- **Stick right = torso right** in MID/ADV (p8) — the corrected sign. **Blackhawk and Owens are
|
||
the fixed-torso exceptions** (named in print!); twist arc "about 120°" (per-mech: Loki/Thor
|
||
tables say Torso Limit 110°, Torso speed 60°/s).
|
||
- **Throttle is a continuous lever** (push=accelerate, pull=stop; inertia); **reverse = HOLD the
|
||
red throttle button** (release = forward) — our 0x3F hold-state model.
|
||
- **Hot Box Selector MFD** = callsign buttons + a **CLOSEST** button (lower right = our hotbox
|
||
button 8 → ChooseNearestPilot); radar = travel-oriented, 60° vision wedge sweeps with twist,
|
||
red hot box + callsign on contacts; **map zoom ± buttons** flank the secondary screen.
|
||
- **Experience gates the hunting aids**: viewscreen hot box + hunting-guide arrow appear in
|
||
STANDARD sim mode only (not veteran/expert); expert mode adds movement heat. (2026-07-18 [T1]:
|
||
4.0→4.10 drift — in OUR 4.10 binary the HUD reads NO experience flag (hotbox/arrows are
|
||
lock-gated in every mode) and movement heat is veteran+expert; full wiring in
|
||
[[experience-levels]].)
|
||
- Cockpit: 5 MFDs (Coolant UL, Engineering top, Hot Box UR, Weapon ×2) + secondary screen with
|
||
side button columns + **EJECT button** beside the joystick + per-mech COOLANT LOOP tables
|
||
(weapons+generators per loop 1-6) and stat sheets (tonnage/armor/reservoir liters/heat
|
||
sinks/top speeds incl. "Super Charged").
|
||
- **MYOMER SEEK / "SUPER CHARGE" — ⚠ the "4.0→4.10 drift" verdict below was WRONG and is
|
||
RETRACTED (same day).** 4.10 HAS the seek→speed coupling — in the un-exported master-perf
|
||
gap (@0x4a9cf2: `speedDemand *= max myomer speedEffect`, turn-freeze at dead drive; see
|
||
[[subsystems]] WAVE 6 correction). The manual is ACCURATE for 4.10; Oracle's pod memory
|
||
(seek-4 humanoid = 182 kph = the printed figure) confirmed it and prompted the re-audit.
|
||
The paragraph below is retained as the record of the error (its BINARY facts — the two
|
||
exported AvailableOutput callers, the single attr ref — were correct but incomplete:
|
||
export-gap functions are invisible to decomp text sweeps).
|
||
~~ORIGINAL (WRONG) VERDICT:~~
|
||
Manual p20/p22: seek "translates to a lower or higher top speed", gear 4 is named
|
||
**Supercharge** (screenshot caption "Seek Level Is Set to Supercharge"), expert-only ("in
|
||
standard simulation mode, you are not able to supercharge"), and "overheated myomers can
|
||
reduce your 'Mech's speed to zero". Stat sheets print THREE top speeds (Loki/Thor: Normal
|
||
143 / **Super Charged 182** / **Gimped 40** kph). The 4.10 BINARY dropped the runtime
|
||
coupling entirely: the mapper demand @004afd10 has no myomer/seek term, `AvailableOutput`
|
||
has exactly two callers (assembly cap + graph), and the SpeedEffect attribute string has one
|
||
reference (its own table row). Even the printed ratio disagrees with 4.10's authored gears
|
||
(182/143 = 1.27 vs 0.9999/0.7 = 1.43) — the gear table and the assembly cap are vestiges.
|
||
In 4.10 the seek dial's live effects are heat (ratio² ≥ 1), the generator brown-out
|
||
threshold (whose real stake is TORSO TWIST — see [[subsystems]] WAVE 6), and the ENG graph.
|
||
"Top Speed Gimped 40" also confirms the limp-gait speed cap as an authored spec. The port
|
||
follows the BINARY (house rule); resurrecting manual-4.0 supercharge would be an opt-in
|
||
deviation for the operator to decide.
|
||
NEW LEADS (manual describes, port lacks input/UI): ~~CROUCH~~ ✅ **CROUCH COMPLETE 2026-08-06**
|
||
([[locomotion]] §CROUCH), ~~EJECT~~ **EJECT WIRED
|
||
2026-08-02** (core: `Mech::EjectPilotMessageHandler` id 0x19 @0049f854 + the crippled-mech
|
||
permission evaluator @0049fa1c; input = binding-engine "Eject" action, Backspace / pad
|
||
LeftThumb; the punch-out kills via graphicAlarm 10 ≥ 9 — KillBonus authors 0 in ALL shipped
|
||
content, so the zero charge is authentic; tails: console relay notice, RIO 0x38 panic control,
|
||
alarm-10 audio/canopy presentation, `SpecialCaseDeathPenalty` role+0x20 consumer), hot-box
|
||
viewscreen framing (the deferred PNAME marker chain). Per-mech
|
||
stat tables = a systematic cross-check source for our streamed subsystem resources.
|
||
|
||
### Coolant-loop cross-check RESULT (2026-07-18) [T1] -- STRUCTURE strongly faithful
|
||
Dumped each mech's subsystem->loop (subsystem's linked-condenser number, `BTCoolingLoopFrame`)
|
||
and diffed vs the manual's COOLANT LOOPS tables. The 4.0->4.10 drift shows here too, but the
|
||
STRUCTURE is remarkably intact:
|
||
- **Backbone identical on ALL 6 mechs:** Generator A/B/C/D on loops 1/2/3/5, **Sensors** (our
|
||
"Avionics") on loop 2, **Myomers** on loop 5, LRMs on loops 1&3, autocannon on loop 4, the
|
||
large energy weapon on loop 6 -- match the manual exactly.
|
||
- **Weapon LOADOUT identical on 5 of 6** (Thor, Vulture, MadCat, Owens, Blackhawk carry the exact
|
||
same weapon set 4.0->4.10). **Loki is the one full rework** (4.0 PPC x2 / AFC100 / SRM6 / Medium
|
||
Laser -> 4.10 AFC50 x2 / ER Medium x2 / SRM4).
|
||
- **The consistent 4.0->4.10 change:** the small energy weapons were REDISTRIBUTED across loops --
|
||
the 2 ER Small Lasers moved off the sensor/heavy loops (4.0) onto the energy-weapon loops 4&6
|
||
(4.10) on Thor/MadCat/Vulture; SRM/laser placement reshuffled on Blackhawk. Owens is a
|
||
near-PERFECT match (loops 1&3 exact; only a 4.0 "second myomer set" on loop 2 is absent).
|
||
- **`Loop 0` = correctly UNCOOLED infrastructure** (condensers, reservoir, gyro, torso, HUD,
|
||
searchlight, thermal sight, ammo bins, message manager, mechtech, controls mapper) -- not a loop.
|
||
CONCLUSION: the coolant-loop reconstruction is faithful; every difference is 4.0->4.10 tuning
|
||
(loadout rework on Loki, small-laser redistribution elsewhere), NOT a bug. `BT_SPEC_LOG` (mech4)
|
||
re-dumps on demand.
|
||
|
||
## Key Relationships
|
||
- Renders: [[gauges-hud]] (the MFD surfaces), [[rendering]] (the main 3D view).
|
||
- Input: [[locomotion]] (the mapper).
|
||
- Bring-up: Phase 8 (get pod specifics from Nick — [[open-questions]]).
|