Files
BT411/context/gauges-hud.md
T
arcattackandClaude Fable 5 6bb03aed0b Gitea #48 ROOT-CAUSED + FIXED: MFD "stray blocks / misplaced lamps" = uninitialized translation-table entries leaking pixels into other displays' bit-planes
The conviction was empirical, not theoretical: a new write-site trap
(BT_PLANE_AUDIT) in the seven L4VB16 drawing primitives logs any draw whose
color carries bits outside its port's plane mask -- the exact cross-display
corruption condition (Replace ORs an unmasked color; Or/Xor ignore the mask
entirely; And clears foreign planes).  A quiet solo session produced 15-30
leaks per minute:

    [plane] PORT 'sec' pixmap draw at(0,0) idx 217 entry=0xffffff00 mask=0x3f
    [plane] LEAK DrawPixelMap8[table] at(639,0) color=0xffffff00 mask=0x3f leak=0xff00

THE DEFECT: L4GraphicsPort::translationTable[256] is never initialized in the
ctor, and BuildSecondaryTranslation fills only the entries its BitWrangler
reaches -- 2^numberOfBits: 64 for the sec plane (mask 0x3F), 4 for the overlay
(0xC0).  Entries above that stay heap garbage.  Every draw resolves color
through this table, and the PIXMAP path indexes it with raw pixel values
0..255: the 480x640 radar background carries pixel index 217, whose garbage
entry's high bits (0xFF00 = ALL EIGHT MFD planes) were stamped into the shared
640x480 buffer -- invisible on the culprit page (the in-plane bits happened
dark) and visible as bright fragments at the same coordinates on every OTHER
display.  That is the operator's screenshot exactly: the same-position blips
on Mfd1+Mfd2 and the Heat-display block.

FIX (engine, both layers):
  * zero translationTable in the L4GraphicsPort ctor (plane-neutral default);
  * BuildSecondaryTranslation now cycles the in-plane pattern across entries
    [2^bits..255] -- high-index art degrades to its (index mod 2^bits) colour
    IN-PLANE and can never leak.  The 1995 binary ships the SAME 64-entry fill
    and survived on 6-bit art discipline; garbage is not a preservable
    behaviour, so the cycle-fill is a guarded PORT deviation (documented).

A/B PROOF: same probe, 60s -- 0 leaks after the fix (15-30/min before).
sim3 3-pod regression: zero crashes.

SECOND real defect found + fixed en route: sessions configure the CAMERA seat
first ("cameraInit" -- which includes the MISSION-REVIEW context:
configure(0, sec, 0, 0x00FF, native, rgb, mrpal.pcc), a DirectColor context
whose translation tables legitimately hold full RGB565 values spanning all 16
plane bits), and the viewpoint swap to the mech re-configured WITHOUT tearing
that tree down (btl4app's s_gaugeTreeBuilt latch treats the mech build as the
first).  The orphaned review gauges kept executing through stale DirectColor
ports.  BTL4GaugeRenderer::ConfigureForModel now overrides (ConfigureForModel
made virtual in L4GREND.h) and tears the prior entity-bound tree down before
every rebuild -- safe on first build, and the review screen rebuilds the same
way when the seat returns to the camera.

Also logged (open-questions): the review-screen PlayerStatus panels read the
compiled player at RAW BINARY OFFSETS (+0x1FC vehicle / +0x1C8 score /
+0x1C4 alive-dead box) -- the databinding trap, dormant until the review
screen runs; bridge before enabling the review.

Ruled out along the way (with evidence): the pilot-list label/erase geometry
(erase box 128x32 covers the 64x16 name rasters), the radar name labels
(view-level ClipImage clips them), PlayerStatus in-game execution ([ps] probe:
never runs in-game), and the PNAME bitmap dimensions (egg generator emits
64x16 exactly).

Why it "started with the comms feature" (#43): that wave registered the new
gauge classes with the config interpreter, letting more of the authored page
furniture parse and draw than before -- the leaking high-index pixmaps rode in
with it.  [T3 correlation detail; the leak + fix are T2 live-verified.]

Diagnostics kept (env-gated): BT_PLANE_AUDIT (the leak trap, now a permanent
regression tripwire), BT_PS_LOG, the port-level pixmap identity trap.

KB: gauges-hud.md (#48 section), decomp-reference.md (BT_PLANE_AUDIT),
open-questions.md (PlayerStatus databinding entry).  checkctx CLEAN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 17:20:19 -05:00

57 KiB
Raw Blame History

id, title, status, source_sections, related_topics, key_terms, open_questions
id title status source_sections related_topics key_terms open_questions
gauges-hud Cockpit Gauges / MFD HUD system established docs/GAUGE_COMPOSITE.md (full map); CLAUDE.md §8
reconstruction-gotchas
decomp-reference
subsystems
pod-hardware
gauge
methodDescription
attribute-pointer
GaugeRenderer
MFD
PCC
MessageBoard LIVE 2026-07-12 (kill ticker); remaining: only strip 0 (kill) is produced -- survey the other btsmsgs.pcx strips for authentic producers
HUD attr table RESOLVED 2026-07-19 (Gitea #10 audit): re-dumped @5110b8 -- the table starts one record earlier (id 3 FlickerRate@0x1D8), every hud.hpp offset was shifted one slot; hud.hpp/CLASSMAP corrected, no behavioral impact (the reticle feeds via port globals). See GAUGE_COMPOSITE.md AUDIT finding B
SeekVoltageGraph RECONSTRUCTED 2026-07-19 (Gitea #11, was #10 finding A): full widget landed (see §SeekVoltageGraph below) -- ghosts gone steady-state (BT_PRESET_HOLD verification); remaining polish: a 1-frame transition artifact when a BT_SHOT lands on the exact page-switch frame (label BecameActive vs the graph's next rated Execute -- same lag class as the binary; self-heals next frame)
Secondary-view cycling RESOLVED 2026-07-19 (Gitea #6): the selector is the DISPLAY mode (CycleDisplayMode -> vtbl+0x4C override @4d1ae4), NOT CycleControlMode; desktop 'N' / pad RightThumb wired; pixel-verified dama->crit->heat
Upper-MFD PRESET pages RESOLVED 2026-07-19 (Gitea #9): SetPresetMode table @0051dbf0 re-decoded (little-endian -> ModeMFD bits 0-14), per-MFD pod button banks identified from the .CTL dump, desktop J/K/L cycle wired
Always-active msg-4 records IDENTIFIED 2026-07-20 (glass input audit): 0x2C = Reservoir InjectCoolant (the flush button), 0x2F/0x2E/0x2D/0x2B/0x2A/0x29 = Condenser1-6 MoveValve, 0x1A-0x1D = GeneratorA-D ToggleGeneratorOnOff (@0050fb90, unreconstructed); plus 0x13 = Mech DuckRequest (crouch), 0x28 = Mech BalanceCoolant, 0x12/0x14 = ThermalSight/Searchlight toggles -- see pod-hardware.md + docs/GLASS_COCKPIT.md 2026-07-20
MP DEATHS resolved 2026-07-12 (observed-death tally + display clamp); remaining: verify multi-death tallies stay in sync across a long session (GAUGE_COMPOSITE.md)

Cockpit Gauges / MFD HUD

The pod's cockpit instruments: the secondary MFD (radar/heat/comm), the 5 mono MFDs, and the engineering screens. Full map + per-widget history in docs/GAUGE_COMPOSITE.md. The gauge system is COMPLETE at the registration+binding level: all 50 config attribute bindings resolve (0 NULL) and every config gauge primitive is registered + built (0 parse-skips). [T2] AUDIT 2026-07-19 (Gitea #10): the full per-widget verdict table (45 rows: 34 CORRECT / 1 WRONG-filed / 6 AUTH-STATIC / 6 DEFERRED-FEED) is in docs/GAUGE_COMPOSITE.md §"AUDIT 2026-07-19" — the one WRONG was the missing SeekVoltageGraph reconstruction (the emitter/myomer eng pages' POWER curve + the authentic top-box eraser; caused the stale-ammo "SYSTEM 10 PPC" ghost). RESOLVED (Gitea #11): full reconstruction landed — §SeekVoltageGraph below. [T2]

Architecture (three layers)

  1. The config content/GAUGE/l4gauge.cfg — a text file the engine GaugeInterpreter parses (BuildConfigurationFile → Initialize → GetProcedureBody → ParsePrimitive). Each mech's gauge tree is built from the label GetGameModel()+"Init" (e.g. bhk1Init), which invokes shared blocks (MechInit, Secondary1) that configure the ports and call gauge primitives. [T1]
  2. Widgets — each keyword(...) in the config resolves to a MethodDescription in BTL4MethodDescription[] (btl4grnd.cpp). An UNREGISTERED keyword is parse-skipped (never built). Base primitives (numeric/digitalClock/rankAndScore/vertBar-base/segmentArc) are ENGINE (L4GAUGE.cpp); BT-specific ones (vertBar/map/pilotList/GeneratorCluster/ sectorDisplay/prepEngr/messageBoard/vehicleSubSystems/the ColorMapper family) are reconstructed + registered. [T2]
  3. Data binding — gauges bind to game state by NAME via the engine attribute-pointer system: the config's Subsystem/Attribute (e.g. HeatSink/CurrentTemperature) resolves through ParseAttribute → FindSubsystem (stricmp GetName()) → GetAttributePointer. A class PUBLISHES an attribute via a <Name>AttributeID enum + static AttributePointers[] (ATTRIBUTE_ENTRY) + GetAttributeIndex() chained to its parent. ⚠ DENSE-TABLE HAZARD (see reconstruction-gotchas §11). [T2]

Reconstructing a widget — the recipe

MethodDescription Class::methodDescription = { "keyword", Class::Make, { ParameterDescription rows } }; + a line in BTL4MethodDescription[] before &BTL4ChainToPrevious. The static Make(int port, Vector2DOf<int> pos, Entity*, GaugeRenderer*) reads methodDescription.parameterList[], allocs (operator new(binSize) or plain new), placement-news the ctor. Then ctor/dtor/ TestInstance/BecameActive/Execute. Gotchas that bite EVERY widget:

  • Container-Execute must override (§10 gotchas) — else Gauge::Execute aborts.
  • /FORCE trap — a prose-only slot AVs on first call.
  • Databinding trap — never raw-read owner offsets; use a bridge (BTGetSubsystemAuxScreen, …).
  • ReconStream no-op — use DEBUG_STREAM for logs, not DebugStream.
  • Lazy build — wait for the gauge window before concluding "not built" (BT_GAUGE_SKIP_LOG). [T2]

The completed widgets (this project's gauge wave)

  • Attributes published: HeatSink table (Degradation/Failure/NormalizedPressure/CoolantMassLeakRate/ ValveSetting/…), PoweredSubsystem (InputVoltage), MechWeapon (OutputVoltage), Mech (LinearSpeed, radar Position/Quaternion), Sensor (RadarPercent), AggregateHeatSink (AmbientTemperature=300 — the LAST NULL). [T2]
  • Widgets reconstructed + registered: ColorMapper family (cmHeat/cmCrit/cmArmor/multiArmor), headingPointer, vertBar (VertTwoPartBar), segmentArcRatio, oneOfSeveralPixInt, map (radar), PlayerStatus, vehicleSubSystems (the engineering cluster panels), LeakGauge, vertNormalSlider, pilotList (Comm KILLS/DEATHS), GeneratorCluster, SectorDisplay (radar SECTOR X/Z read-out — live), PrepEngrScreen (12 engineering-screen label overlays), MessageBoard (comm ticker — deferred-empty). [T2]
  • Condenser valve gauge: ValveSetting→coolantFlowScale reads the authentic 1/N (RecomputeCondenserValves, FUN_0049f788, was a no-op stub). [T2]

Dev composite (off-pod)

BT_DEV_GAUGES renders the 6 pod MFD surfaces (bit-plane masks over one shared SVGA16 pixelBuffer; SVGA16::DrawDevSurface). On the POD they come from SETENV.BAT/L4GAUGE on the real multi-adapter hardware (FindBestAdapterIndices/BuildWindows, intact). The overlay port (SectorDisplay lives there) shares the sec physical surface via a different bit-plane (0x00C0). [T2]

Cockpit surround (the DEFAULT desktop layout, 2026-07-20) [T2]

Under BT_DEV_GAUGES, the DEFAULT is now the cockpit surround (L4VB16.cpp BTDrawCockpitPanels): the 3D world view CENTERED with the six gauge surfaces composited AROUND it in the SINGLE main window at ½ native scale (MFD 320×240, radar 240×320 portrait), plus clickable RIO button lamps — the pod-faithful arrangement (Coolant UL, Mfd2 upper-center, Comm/Hot Box UR, Mfd1/Mfd3 lower flanks, secondary/radar flush below). The pod monitors physically clip the eyeport, so the corner MFDs + top-center overlap the view edges. Mono MFDs are tinted phosphor green (radar keeps its amber palette); the world viewport is the centered view rect (BTApplyWorldViewport cockpit branch) so the reticle/HUD follow for free (dpl2d maps through the viewport), and gWindowAspect = the view rect's on-screen aspect (BTWorldAspectOf cockpit branch).

  • Layout = single source of truth: BTCockpitLayout / BTCockpitComputeLayout(canvasW,canvasH) in l4vb16.h/L4VB16.cpp, computed FROM the backbuffer (canvas) size — consumed by the window sizing (btl4main.cpp), the world viewport, the panel/button draw, the aspect, and the mouse hit-test. Constants: SCALE=0.5, OVL=44 (corner overlap), LAMP=16 (protruding lamp edge), REDCELL=64 (hidden hit depth), RAILW=26; canvas = view + (552, 548).
  • Buttons = the L4GLASSWIN geometry ×0.5 with the same address banks (Heat 0x2F, Mfd2 0x27, Comm 0x37, Mfd1 0x0F, Mfd3 0x07 red 8-btn; radar rails 0x10-0x15/0x18-0x1D + bottom {0x16,0x17,0x1F,0x1E} yellow; flight 0x38-0x3F/0x40-0x47 blue, labeled). Full rect = hit target; the surface draws OVER it so only the lamp edge shows (the PaintGlass painter trick). Mouse: main WndProc WM_L/RBUTTONBTCockpitMouseDown/Up (client→bb map, glass press/release/right- latch contract) → PadRIO::SetScreenButton (#ifdef BT_GLASS; dim/no-op in pod builds). Lamp brightness = BTLampBrightnessOf (shared inline in l4vb16.h) over PadRIO::GetLampState.
  • Env / precedence (resolved once in btl4main → gBTGaugeCockpit): BT_GLASS_PANELS=1 (Cyd's per-display windows) stands cockpit down > explicit BT_COCKPIT=1 > BT_DEV_GAUGES_WINDOW (separate window) / BT_DEV_GAUGES_DOCK (legacy inset) opt-out > cockpit default. BT_COCKPIT=0 forces the dock-bottom strip. -res W H = the WORLD VIEW size (canvas clamped to the work area). Green tint tunable via BT_COCKPIT_TINT=RRGGBB (default 0x27E8). Labels are a lazy GDI-baked MANAGED atlas (survives device reset). Renders in ALL builds; only the PadRIO click/lamp seam is BT_GLASS-gated. Full detail: docs/GAUGE_COMPOSITE.md.

MP gauge-window FREEZE = dangling bindings + permanent SEH disable (Gitea #12, 2026-07-19) [T2 log-convicted]

The live-MP "dev-gauges window froze entirely mid-session" incident (issue #12) is NOT a swap-chain/present bug: scratchpad/incident_2157/operator_1.log shows a burst of [gauge-fault] '<Widget>' Execute FAULTED -> gauge DISABLED (PilotList, HeadingPointer, MapDisplay, SectorDisplay, TwoPartBars, NumericDisplayScalar …) exactly while ALL THREE mechs were being RE-CREATED at mission launch (second [zonebuild]/[cyl] set at new addresses — the session had a pre-launch host drop/rejoin; a normal 2-player LAN launch on the same build did not re-stream and did not freeze). The gauge tree had been built (lazily) during the LOBBY phase against the FIRST-stream mechs; the re-stream freed them; every gauge Execute then AV'd on its dangling attribute-pointers and the BT_DEV_GAUGES SEH guard (Gauge::GuardedExecute, GAUGE.cpp:618) DISABLED each one — Disable(True) sets rate=0 PERMANENTLY, nothing re-enables or rebinds → the whole window static for the rest of the session (= symptoms 1+3). The AUTHENTIC engine flow handles mission transitions by Application::Shutdown → gaugeRenderer->Shutdown() → ShutdownImplementation → Remove(0) (all gauges deleted, tree lazily rebuilt next mission, APP.cpp:787/GAUGREND.cpp:3264) — the port's in-session re-stream path bypasses it. FIX LANDED (2026-07-19, awaiting human MP verify): BTL4GaugeRenderer::TearDownForViewpointRestream() (btl4grnd.cpp) performs the ENTITY-BOUND half of the engine ShutdownImplementation sequence, same order (RemoveAllAlarms → Remove(0) → entity-grid Clear), deliberately KEEPING warehouse/graphics-ports/interpreter/controls-L4Lamps (RemoveAllLamps mid-session would delete the CONTROLS-owned L4Lamps behind the buttonGroup's &lamp->automaticValue registrations — a dangling mapping; the mech-state "lamps" like AnimatedSubsystemLamp are Gauges and go with Remove(0)). Called from BTL4Application::MakeViewpointEntity before ConfigureForModel("Init", entity) whenever the viewpoint is a RE-make (that handler empirically runs once per stream — twice in the incident log, [ctrlmap] installing ×2) — the tree then rebuilds bound to the NEW mech. Log sentinel: [gauge] viewpoint re-stream: tearing the gauge tree down for rebuild. Residual exposure: Remove(0) flushes each gauge (Update(gaugeRate_A), GAUGREND.cpp:3465) before deleting it, so gauge CONNECTIONS may read the freed mech once more (plain reads; Execute stays SEH-guarded).

Per-weapon panel loop/generator lamps — 3 stacked databinding bugs (2026-07-21) [T2 human-playtest-verified]

Each SubsystemCluster (@004c8140) per-weapon MFD panel draws two image-strip lamps in the TEMP/STATUS area: cooling-loop number (btploop.pcc, frames OFF/1..6, AnimatedSubsystemLamp @004c70a4, fed by CoolingLoopConnectionBTCoolingLoopFrame) and generator letter (btpbus.pcc, frames OFF/A..D, AnimatedSourceLamp @004c7160, fed by PowerSourceConnection). These are the "4 A / 1 B / 5 D" boxes in the reference. Both rendered COMPLETELY BLANK; three independent bugs stacked (all fixed, commit e634709):

  1. Color drop — both lamp ctors dropped the bg/fg color params the binary passes (bg=0xff, fg=0, same as the sibling temp bar) and hardcoded 0,0, so OneOfSeveral::Execute did SetColor(0)+DrawBitMapOpaque(0) = black-on-black. Restored 0xff,0.
  2. shadow-field trap (gotcha #2)AnimatedSubsystemLamp/AnimatedSourceLamp each RE-DECLARED int selected; while already inheriting it from OneOfSeveral (@0xAC). The connection's &selected bound the derived shadow copy; OneOfSeveral::Execute read the base @0xAC (always 0) → every lamp stuck on frame 0 ("OFF"). Removing the redeclarations (so selected resolves to the inherited member) fixed the loop NUMBER.
  3. Attribute-table shift (gotcha #8 / attribute-pointer) — the generator lamp resolved its source via ResolveLink(AttributePointerOf(subsystem,"InputVoltage")), but the BT_DEV_GAUGES audio attribute rows shifted the chained attribute ids so AttributePointerOf no longer landed on voltageSource@0x1D0 → the link resolved to 0 (OFF) even though the master PoweredSubsystem ctor DID bind voltageSource to its Generator ([busattach] showed bound=1). Fix: new BTPowerSourceFrame(subsystem) bridge (powersub.cpp) reads the NAMED member via PoweredSubsystem::ResolveVoltageSource() and returns Generator::generatorNumber (@0x1E0), bypassing the attribute table — same pattern as BTCoolingLoopFrame. The lamp caller now passes subsystem_in (not the InputVoltage attribute slot). Lesson: gauge value feeds must read named members through a complete-type bridge, NOT AttributePointerOf+ResolveLink — the attribute table's chained ids are unstable under the audio rows. The eng-page generator-voltage bar + MyomerCluster seek-voltage graph (GeneratorVoltageConnection, evolt.pcc, btl4gau2.cpp:806/1750) were on the same dead path and are now converted the same way -> BTGeneratorVoltage(subsystem) reads Generator::MeasuredVoltage() (outputVoltage@0x1DC); [voltfeed]-verified (Myomers -> 10000V from GeneratorD). Still on the OLD path but NOT yet converted (different attribute, direct read, not confirmed broken): the GeneratorCluster + EnergyWeaponCluster bars that read AttributePointerOf(subsystem, "OutputVoltage") directly (btl4gau2.cpp:968, ~1920). Diagnostics retained (env BT_LOOP_LOG): [loopfeed] (BTCoolingLoopFrame) + [busfeed] (BTPowerSourceFrame) + [voltfeed] (BTGeneratorVoltage) print each widget's resolved value + source. HUMAN-VERIFIED live 2026-07-21 (pod build, solo ARENA1 cockpit): the loop number + generator letter boxes render correctly on the weapon panels.

Ballistic ammo count stencils out of the fire-ready dot (2026-07-21) [T1 decomp + render-verified]

The base-page missile/round count (ammoCountA, NumericDisplayInteger @0x114/this[0x45]) sits on the weapon's fire-ready "dot" — the cluster image (clusterImage @0xCC/this[0x33]) blitted by WeaponCluster::DrawWarningLamp (@004c932c) in on-colour 0xff (solid green) / off-colour 0 (black/absent), toggled by warningState when percentDone crosses the warn threshold. The count must stay legible against it, so BallisticWeaponCluster overrides DrawWarningLamp (@004c9b50): it chains the base (draws the dot) then swaps the numeric's colours via NumericDisplayInteger:: SetColors(bg,fg) (@00470ec8 → inner NumericDisplay @0x90):

  • dot ABSENT (on==0) → SetColors(0, 0xff) == green digits on black
  • dot PRESENT (on!=0) → SetColors(0xff, 0) == BLACK digits cut out of the solid green dot (the same black-on-green stencil as the loop/generator squares). SetColors ForceUpdate()s the numeric so it repaints over the freshly drawn dot on the next child-execute pass. The engine's NumericDisplay::Draw blit is SetColor(fg) + DrawBitMapOpaque(bg,…) — glyph=fg, surround=bg (opaque), so only the colour swap (not transparency) makes the cut-out. Port bug (fixed): only the base non-virtual DrawWarningLamp existed, so the count stayed green-on-black and clashed as a dark box in the green dot. Fix: made DrawWarningLamp virtual + added the ballistic override. Energy weapons (EnergyWeaponCluster, no ammo count) use the base only — unaffected.

TEMP/STATUS bar (HorizTwoPartBar) tiles a striped pattern from x=0 (2026-07-21) [T1 decomp + render-verified]

The per-weapon TEMP/STATUS bar is a HorizTwoPartBar (@004c4170, Execute @004c4340). It renders three zones along X from the interned tileImage + two colours (fillColor, backgroundColor):

  • [0, warnPix)SetColor(fillColor) + DrawTiledBitmap(tileImage) (@004c2ff8): the striped/ dotted TILE pattern (green dots). The DrawTiledBitmap color arg is ignored — the tile is blitted with its own pixels (vtbl+0x58) under the SetColor(fillColor) foreground.
  • [warnPix, valPix)fillColor solid (the green over-degrade fill; only when value > low; binary keeps the current colour, still fillColor from zone 1 — no SetColor).
  • [valPix, width)backgroundColor solid: the unfilled remainder (BLACK). COLOUR MAP (critical): the two colour params land at [this+0xA0]=fillColor (0xff green) and [this+0xA4]=backgroundColor (0 black) — caller @004c8269 pushes 0xff then 0. Green is the tile
  • over-degrade fill; black is only the remainder. The port first shipped these SWAPPED (zone 3 = fillColor) so the whole [valPix,width] remainder — most of the bar when cold (valPix small) — rendered solid green instead of black. Fixed 2026-07-21 (commit 4fbc911). warnPix = round(width*low/high), valPix = round(width*value/high) (value=CurrentTemperature, low=DegradationTemperature, high=FailureTemperature). Port bug (fixed): the Execute had been rewritten with DrawFilledRectangle starting at warnPix and NEVER used tileImage — so the bar read as a solid block "starting in the middle", not striped. The sibling VertTwoPartBar (@004c4724, eng-page vertical temp bars) was already correct (uses DrawTiledBitmap). Fix mirrored the tiled three-zone render into HorizTwoPartBar::Execute; matches the DOSBox reference (hatched fill block on the left + dotted tick scale). NOTE the horizontal/vertical bars use DIFFERENT zone colours (Horiz: zone2=bg, zone3=fill, 2 colours; Vert: zone2=fill, zone3=extra, 3 colours) — do not assume they are pure mirrors.

The secondary screen's THREE views (Damage / Critical / Heat) — mode-gated [T0/T1/T2]

The sec port stacks three mode-gated mech-schematic layers at offset (50,0) over the always-on radar/heading/speed/messageBoard (Secondary1): Damage (ModeSecondaryDamage, <mech>dama.pcc + colorMapArmor/colorMapperMultiArmor — 4 silhouettes front/left/right/back, pixel-plane ids 60-63, per-DAMAGE-ZONE dz_* tint through the adpal→adpal2 ramp), Critical (ModeSecondaryCritical, <mech>crit.pcc + the cmCrit per-SUBSYSTEM list), Heat (ModeSecondaryHeat + cmHeat). Mode bits (BTL4MODE.HPP, nextModeBit=0 [T0]): Mapping=0x8000, NonMapping=0x10000, Intercom=0x20000, SecondaryDamage=0x40000, SecondaryCritical=0x80000, SecondaryHeat=0x100000 (bits 18-20). ModeInitial includes SecondaryDamage → the ARMOR view is the default-on layer (our port creates BTL4ModeManager(ModeInitial), btl4app.cpp:303). The schematic shows the pilot's OWN mech only — there is no target-damage readout in the cockpit.

The selector is the DISPLAY mode, not the control mode (Gitea #6, RESOLVED 2026-07-19) [T1→T2]. The L4 vtable @0051e440 pins the slots: +0x4C = @004d1ae4 — dispatched by CycleDisplayModeMessageHandler (FUN_004afcac) with the new displayMode (0/1/2) — is the NotifyOfDisplayModeChange override that clears bits 18-20 and sets the mask from the table @0051dbe4 {0x40000,0x80000,0x100000}. (The old "SetControlMode @004d1ae4 switches the secondary VIEW" claim was the mislabel that kept the port's copy a never-called non-virtual.) +0x48 = @004d1acc — dispatched by CycleControlModeMessageHandler (FUN_004afbe0) — just forwards to the base RET no-op @004b048c: a BAS/MID/ADV control-mode change NEVER touches the secondary view (empirically confirmed: M cycles the CONTROL MODE gauge lamp, mask bits 18-20 unchanged, schematic stays on ARMOR DAMAGE). Authentic pod inputs (streamed type-6 .CTL EventMappings, dumped live via BT_CTRLMAP_LOG): secondary-panel button 0x15 → msg 0x15 CycleDisplayMode (the manual-p13 "'Mech status Info center", bottom left of the secondary screen: Armor/Critical/Heat Damage Schematic cycle), button 0x18 → msg 0x14 CycleControlMode (the manual-p6 mode button, top right), buttons 0x10/0x11 → ZoomIn/ZoomOut 0x12/0x13 (the map zoom ± pair). The DOS keyboard fallbacks (Keypress 0x13d/0x13e = extended F3/F4) never fire under the WinTesla VK map (VK_F3=0x72 collides with 'r', VK_F4=0x73 with 's' — the 'p'/VK_F1 collision class), so the desktop was PINNED on Damage. Port wiring (mirrors the M/ModeCycle pattern): 'N' / pad RightThumb → action DisplayCycle → gBTDisplayCycle → CycleDisplayModeNow() (mechmppr.cpp; the same body the pod button message drives). Pixel-verified live (docked gauges + BT_SHOT): ARMOR DAMAGE silhouette → CRITICAL DAMAGE subsystem list → HEAT DAMAGE colored list, mask 0x450421→0x490421→0x510421. Diags: BT_MODE_LOG, BT_VIEWCYCLE_TEST=<frame>, BT_MODECYCLE_TEST=<frame>.

The upper-MFD PRESET pages — 3 MFDs × 5 pages (Gitea #9, RESOLVED 2026-07-19) [T0/T1/T2]

The three preset-able MFDs are Mfd1 (lower left) / Mfd2 (upper center) / Mfd3 (lower right), each a PAIR of bit-planes on one physical monitor: the base Quad plane (Mfd1/2/3, masks 0x0100/0x0400/0x1000, btquad.pcx) and the engineering-page plane (Eng1/2/3, masks 0x0200/0x0800/0x2000, bteng.pcx). Mode bits (BTL4MODE.HPP [T0], bits 0-14): ModeMFD{1,2,3}{Quad,Eng1-4} = 1<<(group*5+item)fully disjoint from Mapping/NonMapping (15/16), Intercom (17) and the #6 Secondary* trio (18-20); ModeInitial puts all three MFDs on Quad. What the pages show: Quad = up to four vehicleSubSystems cluster mini-panels (the quadrants, geometry table @0x51bf34); Eng item i = the FULL-SCREEN engineering detail of the subsystem streamed onto aux screen group*4+i (sub+0x1dc; prepEngr screens 1-12: "SYSTEM NN"

  • per-class label cells + the cluster's eng child — GENERATOR SELECT A-D, POWER graph, COOLING loop, DAMAGE (MJ), ammo count …). Unpopulated screens are authored-empty per mech (Blackhawk: 9 of 12 — scr 1/2/4=PPC/Streak6/ERMed, 5-8=Sensors/Myomers/ERMed/ERMed, 9/10=Streak6/PPC; screens 3, 11, 12 empty).

SetPresetMode(group,item) @004d1b24 swaps the page: table @0051dbf0 = 15 {clear,set} pairs — set = the page's ModeMFD bit, clear = the group's other four (item 0) or all five (items 1-4). ⚠ The old reconstruction had transcribed the little-endian set column as BIG-endian dwords (0x01→0x01000000 …), so a preset press set a garbage high bit — and for group 1 items 3-4 / group 2 items 0-2 STOMPED the live NonMapping/Intercom/Secondary* bits — while the page bits never moved (the "presets unwired" defect). Fixed against section_dump.txt:72901-72908; the "does group 2 duplicate #6's secondary views?" concern is resolved: no — group 2 = MFD3, bits 10-14.

Authentic dispatch (streamed type-19 "L4" .CTL, 121 records dumped via BT_CTRLMAP_LOG): every MFD has its own 8-button RIO bank whose meanings are MODE-MASK-gated — Mfd1 = buttons 0x08-0x0F (AuxLowerLeft), Mfd2 = 0x20-0x27 (AuxUpperCenter), Mfd3 = 0x00-0x07 (AuxLowerRight). On a Quad page the bank's buttons DIRECT-SELECT the populated Eng pages (mapper EventMappings → MechRIOMapper messages Aux1Eng1-4=0x4-0x7, Aux2Eng1-4=0x9-0xC, Aux3Eng1-4=0xE-0x11 → SetPresetMode; no button is streamed for an empty screen); on an Eng page one button returns to Quad (Aux1/2/3Quad = 0x3/0x8/0xD) and the rest remap to the SHOWN subsystem (per-subsystem msgs: 0x4-0x7 = SelectGeneratorA-D, 0x8 = ToggleGeneratorMode, 0x9 = ConfigureMappables, 0x3/0xb = unjam/eject-class functions). The MechRIOMapper keyboard cases in @004d1bf0 mirror it as three key rows: 1-4=MFD1 Eng1-4/5=Quad, a s d f/g, z x c v/b — mostly claimed by the port's WASD bindings, hence dead on desktop. Port wiring: the streamed records install and fire on desktop (btinput passes the LIVE manager mask on button press, so the NUMPAD profile's 0x20-0x27 keys page MFD2 authentically); the default WASD profile adds J/K/L → actions Mfd1/2/3Cycle → gBTPresetCycle → CyclePresetModeNow(group) (btl4mppr.cpp; a port cycle-key shim — 24 mode-dependent pod buttons don't fit a keyboard — that visits exactly the pod-reachable set: Quad + populated Eng pages; the body is the authentic SetPresetMode).

Dev-composite change: BTDrawGaugeSurfaces (L4VB16.cpp) now draws the Eng1-3 planes into their sibling's cell and SKIPS any mono plane whose port channel is currently BlankColor — honoring the mode-driven reconfigure (RemapGraphicsPort) so each dev cell shows the ACTIVE page, like the pod monitor. This SUPERSEDED the 2026-07-12 "frozen-dial" scaffold (GAUGREND.cpp force-activated all 15 page bits under BT_DEV_GAUGES — removed; it made every eng screen paint over the shared Eng plane, pinning it on the highest screen). Pixel-verified live (BT_PRESET_TEST): all three MFDs page Quad → eng details → back to Quad in lockstep with the [mode] mask log; N/M un-regressed. Diags: BT_MODE_LOG ([mode] preset lines), BT_PRESET_TEST=<frame>, BT_CTRLMAP_LOG.

pilotList (Comm KILLS/DEATHS) row semantics + the 1 [T1/T2]

FIXED + RIG-VERIFIED (Gitea #43, 2026-07-24; awaiting live playtest). Field report ("never lists anyone in MP") reproduced on a 2-node rig, THREE stacked causes, all fixed:

  1. Raw-flag miscount: BuildPilotArray's count loop used the binary's raw entry+0x29 & 0x40 read on our compiled objects (databinding trap) → garbage → roster latched at 1 row. DECODED: +0x29 bit 0x40 = bit 14 of simulationFlags @+0x28 = Player:: NonScoringPlayerFlag (= Entity::NextBit, PLAYER.h:392) — the roster authentically lists SCORING players (flag cleared when the mech links its player, mech.cpp:688, all nodes). Both loops now use Player::IsScoringPlayer(); also closes a pilotIDs[1] heap overrun the undercount exposed.
  2. Latch race: the binary's build-once latch assumes 1995 synchronous pod loads; over async relay TCP the faster node ticks InterpretControls before the peer's replicants arrive (rig-proven: staggered node logged roster 1 → then peer arrival). [T3 accommodation, demand-latch precedent]: rebuild whenever the scoring CENSUS changes — also un-dangles a departed peer's freed Player from the roster. Forensic: always-on [score] pilot roster built: N scoring pilot(s) per census change.
  3. Invisible rows: LookupPlayerNameBitmap was a NULL stub + numerals are authentic signedBlankedZerosFormat (zeros draw blank) → an unselected 0/0 row rendered as a black box: invisible even with a correct roster. Wired BTPilotNameBitmap (btplayer.cpp bridge: compiled playerBitmapIndexMission::GetSmallNameBitmap, the same 64×16 egg rasters the radar labels use; replaces the binary's raw pilot+0x1e0 key read). Rig-verified: Aeolus (local, center box) + Boreas (remote, roster strip) both render with callsigns. NOTE: rows need the egg to carry name bitmaps (operator-console eggs do; bare rig eggs like MP2.EGG don't → authentic cache-miss blank box). One ROW PER PILOT in the mission (2-player MP = 2 rows — not duplicate displays). KILLS = killCount (the victim's ScoreMessageHandler credits the shooter cross-player, works for both rows); DEATHS = Player::deathCount: engine-inits to 2 (PLAYER.cpp:759), the LOCAL vehicle-acquire branch zeroes it (btplayer.cpp:1118), then VehicleDead(-1) ++s per death. A REMOTE player's Player object never runs the local acquire → 2 +1 spawn increment = 1 locked. RESOLVED 2026-07-12 [T2]: deaths now tally per node from LOCALLY OBSERVED events (the same model as the cross-pod KILLS credit) — a replicant's once-per-death transition calls BTPlayerCountObservedDeath on its owning player's local copy (replicant-gated: the master's own VehicleDead path counts its node), and BTPilotDeaths clamps the 2/1 pre-acquire seed to 0 for display. Own row counts correctly as before.

Launcher-panel recharge dial — CORRECTION 2026-07-19: it IS live (slot 17 @004b9c9c) [T1]

The weapon panels' SegmentArc270 tick ring reads MechWeapon::rechargeLevel (+0x320). The old "authentically STATIC on projectile weapons" claim was WRONG — the writer census missed the anonymous vtable slot 17 (vtbl+0x44) body @004b9c9c: rechargeLevel = (rechargeRate@0x3DC recoil@0x3E8) / rechargeRate (capstone disasm, issue #12; Ghidra never emitted it). The recovered ProjectileWeaponSimulation @004bbd04 calls it every frame in the Loading(3) and unavailable(7) alarm cases — so the launcher dial authentically ANIMATES 0→1 through each reload. (The census's four other writers stand: ctor @004b99a8, stream init @004b8fec, ResetToInitialState @004b96ec, and Emitter::ComputeOutputVoltage @004ba738 — slot 17 is the Emitter override of the SAME slot; @004b9c9c is the MechWeapon/projectile base body.) The launcher panel's other live indicators: AMMO DIGITS (AmmoBin::ammoCount via the complete-type bridges), jam/fire lamps, eject wipe.

The ballistic panel's FIRE icon = the BAY-FIRE annunciator — FIXED (2026-07-25, #47) [T1 decomp / T2 live]

BallisticWeaponCluster::Execute @004c9a38 (raw decomp): this[0x3b] = *(bin+0x18C)cookOffArmed, the field the btefire.pcc TwoState watches; while armed, this[0x3e] = (Now().ticks bin->cookOffTime@0x190) / ticksPerSecond — the cook-off countdown seconds (negative → 0 at detonation) shown by the NumericDisplayScalarTwoState beside it. The old reconstruction misread bin+0x18C as "the reload state" and bridged the icon to BTAmmoBinFeeding — so it blinked with every feed cycle and never lit on a bay fire (RajelAran: "it doesn't"). Now driven by BTAmmoBinCookOffArmed/CookOffTime complete-type bridges (ammobin.cpp). The member names firing/reloading/reloadSeconds in btl4gau2 are historic; semantically bayFireArmed / latch / secondsToDetonation.

The ENG-button ATTENTION FLASH (jam / bay fire) — BUILT (2026-07-25, #47) [T1 chain, T2 live]

The pod's authored annunciator: on a subsystem condition (TechStatusType — Destroyed 0 / Damaged 1 / CoolantLeaking 2 / Overheating 3 / AmmoBurning 4 / Jammed 5 / BadPower 6) the affected subsystem's OWN bezel buttons flash. Chain: MechTech::TechnicalAssistance (@004ad33c, per-frame) edge-scans every monitored subsystem's GetStatusFlags() bitmask (now VIRTUAL — binary slot 12) → Start/StopEntityAlarm to the gauge renderer (port: direct implementation calls) → GaugeAlarmManager::Activate(alarmModel=83 'mechalrm') → the real BTL4GaugeAlarmManager:: ReadGaugeAlarmStreamItem (btl4galm.cpp, from @004cc2fc) maps {condition, lampCode} items via the @0051cf1c vocabulary to bezel lamps (gotoEngineering = the subsystem's quad-select button; engEject/engBusMode/engCooling = the eng-page bank; per-condenser/per-generator specials) → Lamp::SetAlertState (a COUNTER — stacked alarms) → L4Lamp flushes flashFast RIO states (0x37/0x13) → the pod's physical lamps AND the glass panels (PadRIO IS the rioPointer in glass builds). The shipped 'mechalrm' table: Jammed/AmmoBurning → gotoEngineering + engEject (purge/ unjam); Destroyed → +engCooling+engBusMode; CoolantLeaking → +engCooling; BadPower → +engBusMode. Verified live: bay fire → lamp 0xD (the LRM's select button) flashes 0x37 + engEject 0xB; clears on detonation/purge. Diagnostics: BT_LAMP_LOG[techstat]/[galarm]/[lamp]. Details + the four load-bearing fixes en route: open-questions + decomp-reference §GaugeAlarm.

ConfigMapGauge (the weapon panel's trigger-config joystick) — LIVE via LinkToEntity (2026-07-21)

The per-weapon btjoy.pcc joystick image + 4 cm_* state lamps (off/other/only/both) showing, for each mappable fire button (Pinky/ThumbLow/Trigger/ThumbHigh), whether THIS panel's weapon is bound to it. CORRECTION — the old "authentically DORMANT" claim [task #6] was WRONG. @004c6ee0 is NOT an uncalled SetColor(int): it is the virtual GaugeBase::LinkToEntity override (vtbl slot 9, +0x24 — verified against the binary vtable @0051a1b8 and the T0 GaugeBase virtual roster, GAUGE.h), and @0x94 is the linkedEntity, not a colour. The engine broadcasts LinkToEntity(viewpointEntity) to every gauge when the viewpoint binds (APP.cpp:1277 → GaugeRenderer::LinkToEntity GAUGREND.cpp:3011), arming the Execute gate — the joystick DOES render in the shipped game (matches the DOSBox reference: solid circle = the mapped trigger, hollow = unmapped). The "no caller" analysis failed twice: it looked for direct calls to a virtual, and its slot math used the wrong vtable copy. Port fix: renamed SetColor→LinkToEntity / color→linkedEntity, deleted the BT_CONFIGMAP dev enable (no longer needed — the authentic path lights it). Sampler = LBE4ControlsManager:: buttonGroup[btn].GetMapState (table DAT_00518eb4 PE-recovered); the regroup MECHANISM (ConfigureMappables/ChooseButton, task #6) was always live.

Authentically-static (do NOT "fix")

Degradation/Failure temps are fixed markers; AmbientTemperature 300; CoolantMassLeakRate 0 on a pristine mech (damage-gated); cmArmor/cmCrit all-green (undamaged solo player — BT is PvP-only); RANK 1 solo; MessageBoard empty (no status messages exist in bring-up). A heat-FAILED weapon goes DARK with no lamp — authentic (2026-07-23, closes the #30 remainder) [T1 + T2 field-captured]: BallisticWeaponCluster::Execute @004c9a38 reads exactly one weapon-state check — jammed = (weaponAlarm@0x364 == 5) for the BTEJAM lamp; its only other reads are the bin's cook-off lamp/countdown (bin+0x18C/+0x190). Weapon state 7 (unavailable: FailureHeat / destroyed / mech-disabled / bay dry) has NO presentation — dial at zero, everything dark. Field capture: a solo pilot's right SRM6 tripped FailureHeat (gate1: failHeat=1 heatLvl=2) and "just stopped working, no indication" — reported as a bug; it's the binary's behavior. Recovery: an EJECT tap revives a FailureHeat 7 while rounds remain (decomp-reference §message-tables EjectAmmo). The jam lamp lights ONLY for a true jam (5). CORRECTION (task #10, 2026-07-11): the old "Heat MFD is authentically NEAR-STATIC / currentTemperature ~77 rounds to zero" claim was computed at the DEGENERATE bring-up heat scale. At the authentic 1e7-unit economy (tasks #9-#10) the Heat MFD is fully dynamic: weapons run 77→2000 (the authored failure threshold), condensers/generators run 100-1400 under sustained fire, and the bank plateaus ~600 — temperatures, heatLoad and the heat alarms all animate. [T2]

Radar view-wedge tracks the torso twist (Gitea issue #1 — FIXED 2026-07-17) [T2]

The SECTOR radar's view cone (MapDisplay::DrawViewWedge @004c2484, btl4rdr.cpp) reads viewHorizontalRotation@0x31C, fed by a per-frame GaugeConnectionDirectOf<Radian> the ctor wires from the mech's Torso. The two helpers were NULL stubs, so the connection was never created and the wedge sat at heading 0 (the reported "cone does not track the twist"). Reconstructed from the binary: FindSubObject (FUN_0041f98c) = a SUBSYSTEM-ROSTER walk (count @+0x124 / array @+0x128) matching the streamed name (sub+0xd4) case-insensitively (FUN_004d4b58 = tolower-strcmp) — the "Torso" sub-object IS the roster Torso subsystem; GetHorizontalRotation = torso+0x1D8 = Torso::currentTwist (layout-locked), reached via the existing task-#56 bridge BTGetTorsoTwistAddr (Radian ≡ {Scalar angle}, exact reinterpret). VERIFIED live (MadCat, Standard mode Q/E): wedge rot tracks twist 0→−2.21 rad in lockstep, and the SEMANTIC test passes — body turned away, torso twisted back onto the enemy → reticle goes green AND the wedge points at the enemy's blip while the scope stays body-fixed. ⚠ Test with a TWISTING mech: the Blackhawk's torso is FIXED (limits ±0.01°) — its wedge authentically never moves. Diag: BT_RADAR_LOG ([radar] ctor probe + 1 Hz [radar-wedge] rot trace). [T2]

Remaining = DATA FEEDS, not widgets (deferred systems)

The condenser valve CONTROL is LIVE (task #13, 2026-07-11) [T2]: MoveValve (id 4, the Condenser handler table @0x50E52C — exactly one entry) registered + guarded by the REAL FUN_004ac9c8 = player+0x274 == 0 (2026-07-18 correction: +0x274 = the egg EXPERIENCE level, so this is the NOVICE lockout, not a "ROOKIE role" — see experience-levels; task #12's BTPlayerRoleLocksAdvanced bridge; bring-up seeding = 2 ≈ veteran = UNLOCKED, verified live: press → valveState 1→5 → flow redistribution 1/6 → 5/10). Desktop: 'C' cycles the selected condenser (BT_VALVE_SLOT). The COOLANT FLUSH is LIVE (Gitea #7, 2026-07-19) [T2]: InjectCoolant (id 4, the Reservoir handler table @0x50e680, handler @4aee70) — HOLD flushes reservoir coolant through the loops (InjectCoolant @4aefa4, set%-biased via each sink's flowScale); the coolant vertBar C (Reservoir/CoolantMass = coolantLevel@0x12C, l4gauge.cfg:4526) drops live as the tank drains (BLH: 6.0 → 0 in ~0.6 s held ≈ the manual's 3-4 punches); the bluish FLUSH.PFX condensation cloud (psfx 19) spawns on the ReservoirState 0→1 edge. Desktop: 'H' HELD (action Flush). Full chain + the two ctor decode corrections it surfaced: subsystems §coolant FLUSH. Diags: BT_FLUSH_LOG, BT_FLUSH_TEST=<frame>. The MessageBoard feed needs StatusMessagePool (a NULL stub) + the per-player status queue. [T2]

SeekVoltageGraph — the eng-page POWER graph AND the top-box eraser (Gitea #11, 2026-07-19) [T1→T2]

The emitter/PPC + myomer engineering pages' "POWER" box widget (btl4gau2.cpp/.hpp; ctor @004c6798, BecameActive @004c6920, Execute @004c6934; a CLUSTER-CHILD — built by EnergyWeaponCluster @004c93b0 / MyomerCluster @004c8df4, not a config keyword). Fully reconstructed from the capstone disasm (Ghidra dropped every x87 arg; tools/disas2.py):

  • View/geometry: localView = the page's top data box (0x97,0x80)-(0x17d,0x13b) — exactly 230×187, the plot scales; view coords are view-relative (GRAPH2D.h origin = areaWithinPort.bottomLeft). Ctor sets SetOperation(Xor) — ticks/cursor erase by redraw.
  • The eraser role: BecameActive poisons the cached sample (previousVoltage@0xAC = 9999); the next Execute's change-test (sample the response at 12000 V) then runs the CLEAR @004c6be4 (SetOperation(Replace), color 0, filled 1000×1000 clipped to the box) before replotting — THIS is what erases the sibling pages' stale pixels on the shared Eng bit-plane (the #10 ghosts). Every topBox=0 PrepEngr page owns a graph; the box erase design is coherent.
  • Plot math (recovered): polyline v = 0..12000 step 1200 (_DAT_004c6bdc/_be0): x = Round(response(v)·230), y = Round(v·(1/12000)·187) — the 80-bit consts @004c6bd0/@004c6d74 are EXACTLY 1/12000. Ticks (@004c6c6c): per gear i in [*min..*max], the current gear draws a full L (axis→point→axis), others 10-px axis stubs; XOR pair moves the highlight. Cursor (@004c6c30, emitter pages only): 8×8 XOR square at the LIVE voltage (the ctor's "OutputVoltage" attr pointer). Destroyed subsystem (simulationState==1 via the BTSubsystemDestroyed bridge, powersub.cpp): centred edestryd.pcc once; revive calls own BecameActive (vtbl+0xC, slot 3 of PTR_0051a1fc — verified by vtable dump).
  • The sampler == subsystem vtbl+0x3C (slot 15), reached via the BTSeekVoltageSample dispatch bridge (emitter.cpp → myomers.cpp; databinding rule): Emitter @004bb42c = sqrt(SeekPower(v)/2.0e8) with SeekPower @004bb3f4 = damageFraction·v²·0.5·energyCoefficient (a LINE in v — the emitter graph is authentically straight); Myomers @004b8f94 = sqrt(AvailableOutput(v)·3.6/350) (steep near-vertical curve at BLH values). FUN_004dd138 = sqrt (part_015.c:4026) — the old "fabs/fp-magnitude" reading was wrong; Myomers' old best-effort name "GetSpeedReading" renamed SeekVoltageResponse.
  • The attributes: Myomers already published all 4 Seek* (@0x320-0x330). Emitter's AUTHENTIC table was recovered (binary @0x511dd4, ids 0x1D-0x25) and published (emitter.cpp): LaserOn@0x418, LaserScale@0x42C, LaserRotation@0x41C, Current/Recommended/Min/Max SeekVoltageIndex@0x3F0-0x3FC, SeekVoltage@0x400, OutputVoltage@0x414 = currentLevel (RAW volts — the cursor feed). ⚠ The MechWeapon 0x1D "OutputVoltage" PORT ALIAS (→ rechargeLevel 0..1) was RETIRED: the binary MechWeapon table ends at 0x1C, and AttributeIndexSet::Find walks lowest-id-first, so the alias SHADOWED the authentic Emitter row. Emitter renames: @0x3F8 minSeekVoltageIndex / @0x3FC maxSeekVoltageIndex (were seekStepCounter/seekVoltageCount guesses; the attr table proves the identities). PPC chains Emitter::GetAttributeIndex().
  • Verified live (BLH, autofire): page-cycling + BT_PRESET_HOLD steady-state shots — both #10 repro pairs ghost-free (SYSTEM 09 Streak → SYSTEM 10 PPC held clean; SYSTEM 02 → SYSTEM 04 clean; SYSTEM 05 → 06 Myomers clean); curves draw with moving cursors (charge cycling); exactly ONE replot per activation ([seek] log). Diags: BT_SEEK_LOG ([seek] BecameActive/replot), BT_PRESET_HOLD=<n> (freeze the #9 preset cycler after n pulses — steady-state pixel verification; the 120-frame cycle phase-locks with BT_SHOT's 90).

Cockpit HUD reticle (main screen, inside view) — LIVE

BTReticleRenderable (0x358 bytes, ctor @004cc40c) draws over the finished 3D frame in cockpit view only, via the recovered dpl2d 2D display-list API (recorders @0x487f34-0x488630; opcode map + coordinate model in phases/phase-02-dpl2d-reticle.md; port: game/reconstructed/dpl2d.cpp). Geometry is the ctor's hardcoded calibration (originX 0.35, originY 0.25, scaleY 0.5, right range ladder 0..1200 m, bottom TORSO-TWIST tape (NOT a heading tape — stale wording swept task #58; see the tape entry below), center cross + dot; tick ladders via FUN_004cd938). The range caret binds to the live target range (BTSetHudTargetRange, fed by mech4's targeting step). Weapon pips: the build loop (part_014.c:5386) registers EVERY subsystem IsDerivedFrom(0x511830 = MechWeapon) — lasers, PPCs AND missile launchers (BLH = 7 pips) — via AddWeapon @004cdac0 (verified store map in btl4vid.hpp).

The per-frame Execute @004cdcf0 is RECOVERED (task #37, capstone disasm via tools/disas2.py — the full annotated read: the task-37 commit + btl4vid.cpp comments) [T1], and every instrument is now live [T2]:

  • Right ladder = range 01200 m: a YELLOW width-2 BAR from ladder-top to the caret + a GREEN width-1 caret triangle (ctor @4550-4551 sets green/1 AFTER the yellow bar call — transcription color bug caught by a period reference screenshot, 2026-07-09; same for the bottom bowtie carets @4569-4570); pegs at 1200 with no target; the DISPLAYED range slides at 500 m/s toward the true pick range (HudSimulation :5652 [T1]). VERDICT (Gitea #4, 2026-07-20): the "range slides in/out crazily while walking" report is AUTHENTIC behavior, not a bug [T2 measured]. Per-frame BT_RANGE_LOG traces (mech4.cpp, with an independent Möller-Trumbore cross-check BTGroundRayHitExact in btvisgnd.cpp) on scripted walks: ARENA1 garage cluster — every frame a real structure-face pick, true range legitimately hopping 40↔600 m at silhouette edges, the caret in motion 74% of frames / ~24 direction reversals per 10 s; CAVERN butte field — 806 instant >50 m jumps, 776 butte↔1200-default boundary crossings, 106/139 s windows swinging >300 m (max 1143 m), caret reversing ~54×/10 s. Every value tracks real geometry; the 500 m/s slide (authentic) then never rests. Elevation was NOT the driver (arena floor is flat y=0) — depth discontinuities are. Cyd's "ray falls through geometry" was checked: 0 fall-through frames on arena, 6/8400 (0.07%, single-frame silhouette grazes) on cavern. Bounded infidelities noted for follow-up, NOT the reported symptom: (a) butte collision (stepped YCyl tiers + cone, buttee_c.sld) is NARROWER than the sculpted buttee.bgf rock — measured a 0.27 m graze past the r=14.6 tier while visibly inside rock (terrain-march tier backstops it, error a few meters at point-blank); (b) the cavern's butteeu upper butte sections (buttee.bgf re-instanced at y=50, VideoModel-only, NO solid stream) are pick-transparent — aiming steeply up at a rock tower's top half reads through it; (c) boot transient: before the aim camera is valid, the max-range fallback designates from uninitialized ray floats → shown overshoots (~3400) and slides down for the first ~7 s (cosmetically hidden by the 1200 caret peg). Traces: scratchpad i4__walk.log; issue #4 comment has the summary. ⚠ A period pod screenshot (C:\git\image.webp, likely a DIFFERENT pod revision — its crosshair is ~2.5× taller with arrowhead arms, NOT our binary's ±0.04..0.16 program) structurally CONFIRMS our layout: yellow bar + green caret + colored pip dots on the right ladder, bottom tape + green bowtie, rotating compass circle bottom-left, and a mid-ladder range reading with no lock (the world-pick terrain range). Our glyph constants remain [T1] from OUR 4.10 binary. The weapon pips sit on this same ladder at each weapon's authored max-range mark — caret below a pip = that weapon reaches the target. BLH authored data (live dump): 3× ER-M laser red @500 m (two stacked at one column; PipExtendedRange=1), 2× missile amber (0.6,0.4,0) @800 m, 2× PPC blue @900 m — so the 7 pips read as 3 weapon-SYSTEM groups. (The engineering-panel "RANGE 500M" labels are panel text; the authoritative reach is the streamed WeaponRange.)
  • Bottom 21-tick tape = the TORSO-TWIST indicator (NOT a heading tape): deflection line + carets at ∓(span/2)·(RotationOfTorsoHorizontal / HorizontalTorsoLimit) (HUD attrs 4/5/6). Fixed-torso BLH: centred (authentic static).
  • Circle-with-stem = the COMPASS (HUD attr 0xD CompassHeading, rad→deg rotation) at (botX, botY3·tickMajor0.03); the THREAT trail (attr 0xC ThreatVector) draws inside its rotated frame: 0.05-unit attack-direction marks, fresh <2 s red, expiring at 6 s, 1 s blink tick. Port feed: the player's TakeDamage handler pushes the impact direction.
  • Pips (composed into subB6, master-called): hidden when the weapon's DAMAGE state == 1 (destroyed, attr 1); LIT (A) when the FIRE-CYCLE state == 2 (loaded, attr 0x1C WeaponState @0x350 == 2) else dark ring (B, charging); filtered by weaponMode & elementMask&0xF (the weapon-GROUP bits Front/Rear/Left/Right). Range plays NO part — Execute never reads the stored TargetWithinRange slots. FIX 2026-07-18 [T2]: the pip's "loaded" was a port approximation reading rechargeLevel ≥ 1 — correct for emitters (charge-driven) but statically 1.0 on projectile weapons in the PORT (the port sim never called the slot-17 updater @004b9c9c — see the §Launcher-panel CORRECTION), so missile/AC pips were permanently lit and never blinked on fire. Now reads the AUTHENTIC attr 0x1C = the weaponAlarm StateIndicator level (WeaponStatePtrGaugeAlarm54::LevelPtr), compared == stateConst2 (the const the binary itself stores in AddWeapon). The state cycles Loaded(2)↔Firing(0)/Loading(3)/Jammed(5) for BOTH families, so a missile pip now momentarily drops when fired, exactly like the emitter's. Verified live (Blackhawk, BT_AUTOFIRE=1 BT_AF_MISSILE=1): both SRM6 pips toggle 2↔0/5 on each salvo. (The recharge DIAL is driven by slot 17 @004b9c9c — §Launcher-panel CORRECTION.)
  • Lock ring = subB9 (ring+cross) at frame centre, SPINNING 4°/frame while the Lock attr (0xA) is up. The Lock PRODUCER is the authentic HudSimulation rule (part_013.c:5619-5634 [T1], wired task #38): lock requires a target AND your own HUD's host zone damage < 0.75 (a shot-up targeting computer loses lock) AND the targeted zone's damage < 1.0 (whole-mech target checks zone 0 — so a wreck's dead zone can't re-lock). The hotbox stays visible without lock (box = HotBoxVector, ring = Lock — separate signals). The SAME lock-change block drives the PNAMEx.bgf TARGET NAME PLATE — the target's callsign floating under the crosshair. RECONSTRUCTED + RIG-VERIFIED 2026-07-24 (was "3D chain deferred"); awaiting live playtest. Mechanism, all [T1] from the Execute disassembly (reference/decomp/reticle_execute_004cdcf0.disasm.txt) — the ONLY two functions that touch the plate fields are the ctor @004cc40c and Execute @004cdcf0:
    • Mesh table = playerNameObject[] at this+0x2e8 (PNAME1 lands at +0x2ec), loaded part_014.c:4430-4444; the array is 12 slots (MAX_PLAYER_NAMES), only 8 populated.
    • Selection (@004cec98): target_mech+0x190 (the owning BTPlayer) → player+0x1e0 (playerBitmapIndex) indexes that table 1-based (the binary biases the base instead of decrementing; no bounds check). Same 1-based egg bitmapindex the Comm pilot list uses.
    • Placement (@004cdede): re-placed EVERY frame the aim moves — identity → scale 0.12 uniform → translate (K·reticlePos.x, K·reticlePos.y 0.08, 1.0), K = the x87 long double @0x4cee64 = 0.35714286 = 1/2.8 (the same 2.8 projection constant the hotbox uses). So the plate tracks the aim point, 0.08 eye-space below it — NOT pinned under screen centre.
    • Visibility = the LOCK attribute (the Scalar* at this+0x184, cached +0x188; @004cebf9-@004cec47 branches to the hide path when it reads 0) AND the target having an owning player. Reticle Off or simple-X (PrimaryHudOn clear) also hides it (@004cdd75/@004cddc1).
    • Art: the plate is a bare 1.0×0.25 quad (4:1) whose material bmap:name12_mtl etc. (content/VIDEO/MAT/BMAP.BMF) UV-addresses one shared 128×64 bitslice texmap — 4 materials × 2 v-halves = the 8 player slots. content/VIDEO/TEX/BMAP.BSL ships baked "PLAYER 1..8" fallback art (decoded 2026-07-24), but the pod OVERWRITES those texels at runtime with the egg's 1bpp CALLSIGN rasters — the original source is commented out at engine/MUNGA_L4/L4VIDEO.cpp:5682-5710 (FlushBitSliceTexturedpl_TexmapTexels2D(texmap, storage, 128, 64, 4), with the tell-tale warning "textures for player names not defined"). So the authentic plate shows the operator-set callsign, with "PLAYER n" only as the no-bitmap fallback. Material colour is a neutral (0.5,0.5,0.5) — the plate is grey-white, NOT phosphor green like the reticle glyphs.
    • PORT: same placement expressed in reticle units (÷K ⇒ 0.224 below the aim point, plate 0.336×0.084), drawn as the target player's egg callsign texture (dpl2d_DrawTexturedRect + BTGetPlayerNameTexture + BTReticleTargetPlate). Two PNAME consumers exist — this reticle plate (PNAME only), and FUN_00454a70 (class key 0x46) = the engine's CameraShipHUDRenderable, the camera/broadcast seat's followed-name banner + ranking window, which uses PNAME and PLACE1-8 as sorted [ordinal][callsign] rows and is already live as screen quads (see multiplayer camera seat). Its 1995 source is likewise commented out in L4VIDRND.cpp:2562-2876 — the literal dpl_* calls, a Rosetta stone for the binary's node API.
  • Simple-X mode (PrimaryHudOn off, mask 0x20): the minimal reticle — a small green cross (±0.02..0.08 arms) riding the aim translate (ctor @4689-4705 [T1]) — swapped for the full HUD by Draw's state switch.
  • Target HOTBOX (attr 0xB HotBoxVector) = a rectangle hugging the projected extents — x±4 around the hotbox point, +1/11.5 vertical (K=2.8145 baked projection; the port uses the live per-axis projection) — switching to the left/right edge ARROW past ±1.6 or behind.
  • Reticle state Off/On + PrimaryHudOn (mask 0x20) picks full HUD vs the "simple X" list; the aim group SetMatrix-translates by Reticle::reticlePosition (screen 1..+1 [T0]) every frame — but NOT by torso twist (task #58 CORRECTION): the VIEW is torso-mounted (the eye hangs off jointtorso → jointeye → siteeyepoint), so the crosshair stays SCREEN-CENTERED through a twist — screen center IS the boresight; the twist reads on the bottom tape/compass/radar wedge instead. The reticlePosition writer is un-exported (one xref binary-wide: the read-side lookup part_014.c:5132); its coherent use is the FIXED-torso free-aim channel (mech+0x36c [T4]). The old "translates to the torso boresight" wording here was the falsified body-mounted-view model — see combat-damage Targeting for the full re-correction. This recovery also CONFIRMS the HUD attr-table ids 4/5/6/8/0xA/0xB/0xC/0xD name↔use pairings (hud.hpp had flagged them uncertain; the OFFSETS were re-based 2026-07-19 — the table @5110b8 starts with id 3 FlickerRate@0x1D8, so Rotation=0x1DC…CompassHeading=0x214 Scalar; hud.hpp/ CLASSMAP corrected, names/ids unchanged). The PNAME1-8.bgf target NAME PLATE is RECONSTRUCTED (2026-07-24, §Lock ring above) — no longer deferred. (The canopy shell is now authentic and shows by default — see cockpit-view; BT_HIDE_COCKPIT=1 hides it.)

Player CALLSIGN labels (kill/damage feed, radar/target tags, score display) are 1bpp name BITMAPS from the egg, not text — format, renderers, and the operator-console generator: multiplayer §OPERATOR-SET CALLSIGNS.

Key Relationships

#48 MFD ARTIFACTS -- ROOT-CAUSED + FIXED (2026-07-25) [T2 live-convicted]

The night-3 "stray blocks + misaligned lamps" on Heat/Mfd/sec: uninitialized translation-table entries leaking pixels into other displays' bit-planes. L4GraphicsPort::translationTable[256] was never ctor-initialized, and BuildSecondaryTranslation fills only the entries its BitWrangler reaches -- 2^numberOfBits: 64 for the sec plane (mask 0x3F), 4 for the overlay (0xC0) -- so entries above that stayed heap garbage. Every draw resolves color through this table, and the pixmap path indexes it with RAW PIXEL VALUES (0..255): the 480x640 radar background carries pixel index 217 -> its garbage entry's high bits (e.g. 0xFF00 = ALL EIGHT MFD planes) were written into the shared 640x480 buffer at that position -- invisible on the culprit page (the in-plane low bits happened dark), visible as bright fragments at the same coordinates on EVERY OTHER display. Convicted empirically with the new BT_PLANE_AUDIT write-site trap (L4VB16 primitives: a draw whose color carries bits outside its port's plane mask logs primitive/port/position/mask; Or/Xor ignore the mask entirely and And clears foreign planes -- all audited): 15-30 leaks/minute in a quiet solo session, PORT 'sec' ... idx 217 entry=0xffffff00 mask=0x3f, at exactly the artifact positions in the operator's screenshot. FIX: zero the table in the ctor + cycle the in-plane pattern across entries [2^bits..255] (high-index art degrades to its index-mod-2^bits colour IN-PLANE, can never leak). The 1995 binary ships the SAME 64-entry fill and relied on 6-bit art discipline -- garbage is not a preservable behaviour, so the cycle-fill is a guarded PORT deviation. Post-fix: 0 leaks over 60s on the same probe; sim3 3-pod regression clean. SECOND real defect fixed en route: sessions configure the CAMERA seat first (cameraInit -- which builds the MISSION-REVIEW context: configure(0, sec, 0, 0x00FF, native, rgb, mrpal.pcc), a DirectColor context whose table holds full RGB565 values), and the swap to the mech re-configured WITHOUT tearing that tree down (the btl4app latch treated the mech build as "first") -- the orphaned review gauges kept executing through stale DirectColor ports. BTL4GaugeRenderer::ConfigureForModel now tears down before every rebuild (ConfigureForModel made virtual in L4GREND.h). Note the review-screen PlayerStatus gauges also read the compiled player at RAW BINARY OFFSETS (+0x1FC vehicle / +0x1C8 score / +0x1C4 alive-dead -- the databinding trap, dormant until review runs); logged in open-questions. Why "it started with the comms feature": the #43 wave registered the new gauge classes (PlayerStatus/pilotList/...) with the config interpreter, which let more of the authored page furniture parse + draw than before -- the leaking high-index pixmaps rode in with that. [T3 for this correlation detail; the leak itself and its fix are T2 live-verified.]