Files
BT411/context/pod-hardware.md
T
arcattackandClaude Opus 4.8 c79b9953f9 KB: ToggleCooling routing confirmed wired (the #2 audit)
The #2 audit (BT_CTRLMAP_LOG) proves the Eng-page button->msg-3 route was always
authored (121-mapping L4 ControlMappingsList, ~a dozen EVENT msg 0x3 entries;
weapons via Eng-page aux elem 0x21 mask-gated per ModeMFD page).  It was "silently
dead" ONLY for lack of the id-3 handler, which the prior commit wired -- so coolant
priority via the MFD "Display -> Coolant" button now works end-to-end.  (The
mask-0xffffffff msg-3 entries at 0x12/0x14 go to ThermalSight/Searchlight on the
PowerWatcher branch -- separate, untouched, no collision.)

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

220 lines
17 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: 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 — DEAD until
reconstructed). Newly decoded from the binary message tables: **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 the handler-set is default-constructed
EMPTY — unwired)** — all currently dead; 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
0800, 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`.
Details: `docs/GLASS_COCKPIT.md` §2a; see [[glass-cockpit]].
4. **Interpretation:** `L4MechControlsMapper::InterpretControls` @004d196c applies the ONLY software
detent — snap to 1.0 when |t1.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
0x4a9b5a0x4ab188 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.0230.87 = throttle 3650%). 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 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").
NEW LEADS (manual describes, port lacks input/UI): **CROUCH** (button by the secondary screen;
`Mech::duckState` attr 0x37 + SQUAT clips exist, nothing drives them), **EJECT** (sounds exist:
EjectButton01_z*.wav), 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]]).