Restores the PPC's authentic secondary-display effect: an EnergyDamageType hit scrambles every secondary cockpit display for 0.8s while the main out-the-window view stays clean. Was fully specced (phase-14) with the engine half already present under the original VWE names; only the trigger and the visual were missing (the visual STUBBED since 2007). A -- TRIGGER (game/reconstructed/mech.cpp): in TakeDamageMessageHandler, at the binary's @0x4a03f3 position (after the cylinder resolve, before the burst loop, so ONCE per damage message) fire GetGaugeRenderer()->SpecialEffect(scrambleVideo, damageType*0.2f) on EnergyDamageType (==4). That type is authored on exactly the 14 PPC/ERPPC records, so the branch is structurally PPC-exclusive -- no weapon-class check. Duration DERIVED from the ordinal (4*0.2==0.8s), not a literal. BT_DMG_LOG prints [ppc-scramble]. B -- VISUAL (SVGA16::FunkyVideo, was the 2007 stub): FunkyVideo now arms scrambleActive; new SVGA16::ScrambleRowShift is a per-source-row horizontal shear+roll, read by BOTH DrawDevSurface (surround/dock) and ExpandPlaneToBGRA (glass windows, native + rotated radar), so all secondary surfaces shear together in source space and the main 3D view (separate timing chain) is untouched -- the modern stand-in for the VGA CRTC Horizontal-Total detune. Tunable: BT_SCRAMBLE_SHEAR (px/line, def 4), BT_SCRAMBLE_ROLL (px/sec, def 220); BT_SCRAMBLE_TEST=1 forces it on for tuning by eye. C -- NON-STACKING LATCH (L4GaugeRenderer::SpecialEffect): ignore the re-arm while scrambleVideoFlag is set (matches the binary's `modified` latch) -- a second PPC during the window no longer extends it. Was a divergence. Verified: Release links clean; surround boots + runs with the effect forced on, every secondary MFD + the radar shear while the out-the-window view stays clean, no crash (screenshot). Open (for the playtesters who filed the report): live PPC-fire confirmation + by-eye shear tuning -- k/roll are not recoverable from the binary. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
73 KiB
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 |
|
|
|
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)
- The config
content/GAUGE/l4gauge.cfg— a text file the engineGaugeInterpreterparses (BuildConfigurationFile → Initialize → GetProcedureBody → ParsePrimitive). Each mech's gauge tree is built from the labelGetGameModel()+"Init"(e.g.bhk1Init), which invokes shared blocks (MechInit,Secondary1) thatconfigurethe ports and call gauge primitives. [T1] - Widgets — each
keyword(...)in the config resolves to aMethodDescriptioninBTL4MethodDescription[](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] - 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 throughParseAttribute → FindSubsystem (stricmp GetName()) → GetAttributePointer. A class PUBLISHES an attribute via a<Name>AttributeIDenum +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::Executeaborts. /FORCEtrap — a prose-only slot AVs on first call.- Databinding trap — never raw-read owner offsets; use a bridge (
BTGetSubsystemAuxScreen, …). - ReconStream no-op — use
DEBUG_STREAMfor logs, notDebugStream. - 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)inl4vb16.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 =
L4RIOBANK(2026-07-26: the ONE geometry, shared with the exploded windows — see glass-cockpit §ONE button-bank geometry) at half scale, same address banks (Heat 0x2F, Mfd2 0x27, Comm 0x37, Mfd1 0x0F, Mfd3 0x07 red 8-btn; radar columns 0x10-0x15/0x18-0x1D + foot {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 strip shows (the PaintGlass painter trick). ⚠ Before that date the surround's MFD lamps sat ENTIRELY outside the glass on a 24px band — a 76×24 target where the exploded window gave 156×138; both now reach half the glass. Mouse: main WndProcWM_L/RBUTTON→BTCockpitMouseDown/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) overPadRIO::GetLampState. - Env / precedence (resolved ONCE in btl4main →
glassLayout→gBTGaugeCockpit; the full table lives in glass-cockpit §Layout modes):BT_GLASS_PANELS≠0 (the per-display windows) stands cockpit down >BT_DEV_GAUGES_WINDOW(separate window) >BT_DEV_GAUGES_DOCK(legacy inset) >BT_COCKPIT=0(also the dock strip) > cockpit surround default. ⚠ Before 2026-07-26BT_COCKPIT=0actually landed on the per-display windows — the dock strip was unreachable under glass.-res W H= the WORLD VIEW size (canvas clamped to the work area). Green tint tunable viaBT_COCKPIT_TINT=RRGGBB(default0x27E8). 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. - ⚠ Cockpit-lamp LATENCY under MP load (2026-08-04, glass playtest fix) [T2 runtime-verified].
The lit cockpit buttons are drawn every frame (
BTDrawCockpitPanels, readingPadRIO::GetLampState), but the lamp STATE sweep that fills that store —LampManager::Update→AssertNewLampValue→SetLamp— authentically ridesGaugeRenderer::ExecuteForeground, which fires only once per full gauge cycle (foreground→background→copy). The cycle can't advance to the next foreground turn until the THROTTLED background gauge sweep drains the ~140-instrument active list, and that background task starves under load (issue #45 — "instruments freeze while fps stays healthy"). So on a busy MP mission the lamp sweep ran ~1×/s: buttons froze / flashes stalled while the 3D view (a separate per-frame foreground render) stayed smooth — the "lighting slow or nonexistent" reports (all testers on glass surround). Fix:BTGlassSweepLamps()runs the lamp sweep EVERY frame on the dev/glass composite path (L4VB16.cpp, inBTDrawGaugeInset) — cheap (deduped pushes, ~72 lamps), decoupled from the gauge cycle. The pod never enters this path (real gauge hardware), so its bandwidth-paced serial lamp cadence is untouched. Kill switchBT_GLASS_LAMP_SWEEP=0. NB the RIO serial/lamp stack itself is byte-identical to pod-proven Red Planet (L4LAMP.cpp/L4SERIAL.cppdiffed) — the defect was purely the glass update CADENCE, not the lamp wiring.
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 CoolingLoopConnection→BTCoolingLoopFrame) 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):
- 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, soOneOfSeveral::ExecutedidSetColor(0)+DrawBitMapOpaque(0)= black-on-black. Restored0xff,0. - shadow-field trap (gotcha #2) —
AnimatedSubsystemLamp/AnimatedSourceLampeach RE-DECLAREDint selected;while already inheriting it fromOneOfSeveral(@0xAC). The connection's&selectedbound the derived shadow copy;OneOfSeveral::Executeread the base @0xAC (always 0) → every lamp stuck on frame 0 ("OFF"). Removing the redeclarations (soselectedresolves to the inherited member) fixed the loop NUMBER. - Attribute-table shift (gotcha #8 / attribute-pointer) — the generator lamp resolved its
source via
ResolveLink(AttributePointerOf(subsystem,"InputVoltage")), but theBT_DEV_GAUGESaudio attribute rows shifted the chained attribute ids soAttributePointerOfno longer landed onvoltageSource@0x1D0→ the link resolved to 0 (OFF) even though the masterPoweredSubsystemctor DID bindvoltageSourceto its Generator ([busattach]showed bound=1). Fix: newBTPowerSourceFrame(subsystem)bridge (powersub.cpp) reads the NAMED member viaPoweredSubsystem::ResolveVoltageSource()and returnsGenerator::generatorNumber(@0x1E0), bypassing the attribute table — same pattern asBTCoolingLoopFrame. The lamp caller now passessubsystem_in(not theInputVoltageattribute slot). Lesson: gauge value feeds must read named members through a complete-type bridge, NOTAttributePointerOf+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)readsGenerator::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 readAttributePointerOf(subsystem, "OutputVoltage")directly (btl4gau2.cpp:968, ~1920). Diagnostics retained (envBT_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).SetColorsForceUpdate()s the numeric so it repaints over the freshly drawn dot on the next child-execute pass. The engine'sNumericDisplay::Drawblit isSetColor(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-virtualDrawWarningLampexisted, so the count stayed green-on-black and clashed as a dark box in the green dot. Fix: madeDrawWarningLampvirtual + 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). TheDrawTiledBitmapcolorarg is ignored — the tile is blitted with its own pixels (vtbl+0x58) under the SetColor(fillColor) foreground.[warnPix, valPix)—fillColorsolid (the green over-degrade fill; only whenvalue > low; binary keeps the current colour, stillfillColorfrom zone 1 — no SetColor).[valPix, width)—backgroundColorsolid: 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 pushes0xffthen0. 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 (commit4fbc911).warnPix = round(width*low/high),valPix = round(width*value/high)(value=CurrentTemperature, low=DegradationTemperature, high=FailureTemperature). Port bug (fixed): the Execute had been rewritten withDrawFilledRectanglestarting atwarnPixand NEVER usedtileImage— so the bar read as a solid block "starting in the middle", not striped. The siblingVertTwoPartBar(@004c4724, eng-page vertical temp bars) was already correct (usesDrawTiledBitmap). Fix mirrored the tiled three-zone render intoHorizTwoPartBar::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:
- Raw-flag miscount:
BuildPilotArray's count loop used the binary's rawentry+0x29 & 0x40read on our compiled objects (databinding trap) → garbage → roster latched at 1 row. DECODED: +0x29 bit 0x40 = bit 14 ofsimulationFlags@+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 usePlayer::IsScoringPlayer(); also closes apilotIDs[1]heap overrun the undercount exposed. - 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. - Invisible rows:
LookupPlayerNameBitmapwas a NULL stub + numerals are authenticsignedBlankedZerosFormat(zeros draw blank) → an unselected 0/0 row rendered as a black box: invisible even with a correct roster. WiredBTPilotNameBitmap(btplayer.cpp bridge: compiledplayerBitmapIndex→Mission::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. SUPERSEDED -- that "RESOLVED" claim was FALSE (corrected 2026-07-25, Gitea #45). The observed-tally design never ran:BTPlayerCountObservedDeath's only call site is replicant-gated but sits INSIDEMech::UpdateDeathState's once-per-death transition, which a replicant never enters (it takes the mode-9 early return) -- and it could not have worked anyway, because a replicant victim carries no attribution at all (0 of 18818DMGrows in the corpus areinst=R, which is why 439 of 439DEATH inst=Rrows readkiller=0:0against 0 of 225inst=Mrows). What actually happened: BOTH counters lived only on the OWNING pod, so every REMOTE row read 0/0 all mission -- KILLS becauseEntity::Dispatchreroutes a replicant's ScoreMessage to the master (ENTITY.cpp:244-251), so++killCountlands on the killer's own machine; DEATHS because theVehicleDead(-1)handler runs on the victim's own master. Neither counter rode an update record. [T2 — stated as ratios above; an earlier draft's "125 of 125 over 255 node-logs" was true when measured but is not reproducible, because every rig run appends to the same corpus.] FIXED 2026-07-25 [T2, rig-verified]: DEATHS now reads the binary's OWN+0x280column (BTPlayer::deathTally, our offset 0x274 -- previously declaredpad_0x280and "dead"), notPlayer::deathCount(which is the respawn-handshake identity, seeded -2 -- exactly what the old display clamp was hiding). Both counters replicate owner->replicant via aBTPlayer__UpdateRecordextension, so every pod shows the same numbers. Seedocs/KD_SCOREBOARD_PLAN.md(addendum) +docs/RECONCILE.md.
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::Findwalks 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 0–1200 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_LOGtraces (mech4.cpp, with an independent Möller-Trumbore cross-checkBTGroundRayHitExactin 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'sbutteeuupper 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 →shownovershoots (~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, botY−3·tickMajor−0.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 readingrechargeLevel ≥ 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 (WeaponStatePtr→GaugeAlarm54::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[]atthis+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 eggbitmapindexthe 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*atthis+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_mtletc. (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.BSLships 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 atengine/MUNGA_L4/L4VIDEO.cpp:5682-5710(FlushBitSliceTexture→dpl_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), andFUN_00454a70(class key 0x46) = the engine'sCameraShipHUDRenderable, 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 inL4VIDRND.cpp:2562-2876— the literaldpl_*calls, a Rosetta stone for the binary's node API.
- Mesh table =
- 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 byReticle::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=1hides 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.
Lamp strips: the BOTTOM-UP source-rect convention (do NOT "fix" the flip) [T0/T1]
The gauge blit addresses SOURCE rows bottom-up, and the vertical strip artwork is authored
to match. Video16BitBuffered::DrawBitMapOpaque converts the incoming source rect with
sTop = map_max_y - sTop; sBottom = map_max_y - sBottom and walks rows upward
(engine/MUNGA_L4/L4VB16.cpp:3846-3850, UP_BITMAP :2155) [T0]; the 1995 display blit
@0046bdfc performs the IDENTICAL flip [T1 disasm], and the PCC→BitMap loader stores the PCX
top scanline at memory row 0 unflipped (GRAPH2D.cpp:27-174). Net: in OneOfSeveral::Execute
sy0 = row*frameHeight selects frames counting from the BOTTOM of the image as authored.
The vertical strips are painted for exactly this: BTEMODE.PCC (1×3) reads AUTO/MANUAL/OFF
top-down, i.e. OFF is the bottom frame = value 0 — so identity level→row draws the pod-
correct display (0=OFF, 1=MANUAL, 2=AUTO). Horizontal strips (btebus/btpbus/bteloop,
rows=1) are insensitive to the convention. A "row = (rows-1)-selected" correction here is a
trap — it was nearly landed 2026-07-28 on the assumption the blit is top-down; it would
invert every vertical lamp. (An adversarial workflow settled this against two of five
investigators; the port and the binary agree end-to-end.) Residual [T3]: the DrawPixelMap8
branch's source-y convention is untraced — verify before authoring any vertical PixMap8 strip.
The dropped-colour-parameter family (2026-07-28): the binary's OneOfSeveralStates
(@004c5470) and OneOfSeveralInt (@004c5148) ctors forward caller-supplied
background/foreground colours; the recon dropped both parameters and hardcoded 0,0, so a
BitMap-strip lamp drew SetColor(0) + DrawBitMapOpaque(0,…) = invisible. Hit every one of:
btemode (connect mode, 1×3 — reported as "no Auto mode exists"), btecmode (coolant on/off,
1×2), both bteseek gear-step lamps (1×4). Cluster call sites pass 0xff,0
(part_014.c:1479-1481, :2146-2147). Same bug class as issue #42's dropped MoveToAbsolute.
Also corrected: @004c552c is OneOfSeveralStates' Execute override (clamp ≥0, chain base),
not BecameActive — vtable-diffed.
The gauge EXECUTIVE — why panels froze in MP, and the fix (#45 root, 2026-07-30) [T0 engine / T2 verified]
Instruments repaint via the BACKGROUND task pump (APPMGR.cpp RunMissions): after sim+render, the
loop pumps BackgroundTasks::Execute() (ONE task per pump, ~7 tasks round-robin: net, events,
audio, GAUGES, …) only until the frame deadline — the gauge renderer's turn advances one
gauge of the active list (ProcessOneActiveGauge, ~140 active), and the 16-bit GaugeRate
mask advances once per FULL sweep (so "rate D = every 4th frame" is really every 4th SWEEP).
On a busy MP mission the foreground eats the whole frame budget → 1 pump/frame → a full
instrument rotation took MINUTES at perfectly healthy fps: comms-panel K/D stuck at 0, recharge
tickers frozen — while the underlying tallies (and their replication) were exactly right. This
is the real root of the field "panels don't update in MP" (#45's display half).
Fix (both env-tunable): BT_BG_MIN (APPMGR.cpp, default 32, 0=authentic) guarantees a
minimum pump floor per frame; BT_GAUGE_BATCH (GAUGREND.cpp, default 32, 1=authentic) advances
a batch of gauges per gauge turn (a visit is only a rate-mask check unless due). Verified
4-node: sweeps 0.006/s → 18-20/s, PilotList ~10 Exec/s, all four panels tracked every death
within ~1s, bg cost 2-4 ms/frame. Diagnostics: BT_PERF=1 → [perf] … bgTasks/gaugeTurns/ sweeps/active 1 Hz (APPMGR.cpp); [score] panel DRAW edge log (btl4gau3.cpp, BT_SCORE_LOG).
The COOLANT-LEAK ALARM audio chain — fully built; the shared-Stop is AUTHENTIC (#99, 2026-08-01) [T1 disasm-verified]
The audible leak warning is implemented and works end to end; an earlier note claiming it was "genuinely unbuilt" was wrong (asserted without checking).
The chain, traced live (all probes under BT_ATTRBIND_LOG):
HeatSink::coolantActive (published as the ReportLeak attribute) changes 0->1
-> AttributeWatcherOf<Logical>::Execute sees it ([watchpoll] CHANGE)
-> AudioMatchOf<Logical>::SendNotificationOfChange fires ([matchfire] val=1 -> ctl 1)
-> AudioControlSequence::ReceiveControl(StartAudioControlID) -> StartSequence
-> an AudioIdleWatcher ticks it each frame (IdleAudioControlID -> RunSequence)
-> [seqsend] emits the authored pattern: two chirps then an ~8s sustained tone, looped.
The authored wiring (BTL4.RES): 19 subsystems, ONE shared alarm sequence.
Condenser1-6, GeneratorA-D, Myomers, PPC_1/2, ERMLaser_1-3, SRM6_1/2, Avionics
each bind TWO AudioLogicalTriggers to their own ReportLeak — match 1 -> ctl 1
(Start), match 0 -> ctl 2 (Stop) — and every one of them drives the SAME sequence.
Consequence, and it is AUTHENTIC: any single subsystem clearing its leak sends an unconditional Stop, silencing the alarm even while others still leak; the still-leaking ones never re-assert because their flag has not changed. Reported from the field as a bug ("was sounding, resolved a leak, but there was another"). Do NOT 'fix' it — verified in the arcade image, not merely inferred from the engine source lineage:
| what | where |
|---|---|
| same MUNGA sources compiled in | d: esla_bt\munga\AUDSEQ.CPP / AUDWTHR.CPP strings present |
| object factory | @00466184 case 0x4b -> alloc 0x5C -> ctor @0043ccec |
| vtable | 0x4ead48 |
ReceiveControl |
@0043d3a4 — switch on control IDs 1 / 2 / 9 / 12 = Start / Stop / Idle / Tempo (exactly our enum) |
| Stop case | @0043d3ca -> unconditional call StopSequence @0043d2d4 |
StopSequence |
tests only its OWN isRunning (+0x28), then the Chase loop — no refcount, no awareness of other leak sources |
So the arcade had the identical structure. Silence in 4.11.674 was a different cause: the OpenAL source pool was exhausted in every player session (see the audio pooling fix) — an alarm that cannot acquire a source is silent.
⚠ The bench cannot confirm audibility: it runs with no audio device (live=0 pooled=0), so the control chain is verified but final playback is not.
PPC HIT = a deliberate CRTC horizontal-sync DETUNE on every secondary display (2026-08-06) [T1 disasm-verified]
✅ IMPLEMENTED 2026-08-06 (branch ppc-sync-distortion; screenshot-verified —
every secondary MFD + the radar shear together, the out-the-window view stays
clean). Trigger + visual + non-stacking latch — see
phases/phase-14-ppc-sync-distortion.md for the port details and the
BT_SCRAMBLE_* tuning envs. Reported by playtesters as "being hit by a PPC makes
it look like all of the secondary CRTs were being degaussed" — main (VPX) view
unaffected, PPC strikes only. Both observations are exactly what the binary does.
The disasm chain below is the ground truth the port was built from.
The gate is the damage TYPE, and only the PPC has it. A BTL4.RES
subsystem census gives damageType 4 (EnergyDamageType) = 14 records,
every one PPC or ERPPC; everything else is Ballistic (16), Explosive (30),
Laser (78). So a branch keyed on type 4 is structurally PPC-exclusive.
The chain, on the VICTIM's machine (all @ from BTL4OPT.EXE,
md5 a97075bcb5634d13263e9ad5a2b96fd0):
Mech::TakeDamageMessageHandler@0x4a0230, branch @0x4a03f3— sits between the collision divert and the burst loop, so it runs once per damage message, not per burst:⚠ The duration is derived, not constant:004a03f3 mov ecx,[esi+0x2c] ; damage.damageType 004a03f6 cmp ecx,4 ; EnergyDamageType 004a03f9 jne 0x4a0423 ; everything else -> burst loop 004a03fb mov eax,[0x4efc94] ; the global `application` 004a0400 mov eax,[eax+0x4c] ; -> gauge renderer 004a0405 je 0x4a0423 ; null-guarded 004a0407 fild dword [esi+0x2c] ; (float)damageType == 4.0 004a040a fld xword [0x4a0c08] ; long double 0.2 004a0410 fmulp st(1) ; => 0.8 004a041d call dword [edx+0x4c] ; vtable slot 19, args (0.8f, 0)(float)damageType × 0.2.- Gauge-renderer vtable @
0x51cebc, slot 19 (+0x4c) = @0x46ffcc(anL4GaugeRenderermethod — the 0x46xxxx MUNGA_L4 range, so the capability is shared-engine; BT is what wires it to Energy damage). Second arg must be 0 (sub eax,1; jae ret). Body:svga16 = this[+0x1c52c](null-guarded — same member RP fetches for itsFlashPalette), thenthis[+0x1c534] = 1(active) andthis[+0x1c538] = now + 0.8 sin ticks. - @
0x46d840— thin wrapper, dropsthis, forwards the flag. - @
0x47d76d— the payload, straight VGA CRTC I/O:Globals:out(0x3D4,0x11); v=in(0x3D5); out(0x3D5, v & 0x7F) ; unlock CRTC regs 0-7 out(0x3D4,0x00) ; CRTC 0 = HORIZONTAL TOTAL if (arg==0) { out(0x3D5, saved); modified=0; } ; restore else if (!modified) { modified=1; saved=in(0x3D5); out(0x3D5, saved-9); } ; <<< shorten the scanline out(0x3D4,0x11); out(0x3D5, v) ; restore write-protectmodified@0x4fe0fe,saved@0x4fe0ff. Themodifiedlatch makes it idempotent — overlapping PPC hits do NOT stack, and a second hit does not re-save an already-detuned value. - @
0x47003c(per frame):if (active && now >= expiry) { active = 0; SVGADistortSync(svga16, 0); }— restores the saved Horizontal Total.
Why it reads as a degauss, and why only the secondaries. CRTC register 0 is
the character-clock count per scanline — it is the horizontal scan frequency.
−9 drives every attached monitor's horizontal oscillator off frequency: the
image shears/rolls/wobbles until it re-locks, then snaps back 0.8 s later. All
six secondary displays are derived by the VDB from that one VGA's timing, so
they glitch together; the main view comes off the Division VPX card on an
independent timing chain and is untouched. No relay, no VDB register, no
palette work — the VDB just propagates a deliberately corrupted sync. (The
LampTesla1/2/3 "solid-state relays" in L4CTRL.HPP are NOT involved and are
driven by nothing in the surviving tree.)
⚠ Do not confuse this with the SVGA16::FlashPalette pixel-mask cycler
(flashRate/mask[4], ports 0x302/0x30A/0x312). That machinery is linked and
its per-frame cycler runs in BT, but FlashPalette @0x46d5f4 has zero call
sites and zero address-of references in BTL4OPT.EXE — BT never arms it.
RP does: RPL4OPT.EXE @0x4addce calls FlashPalette(palette 1 = SecondaryPalette, rate 2.0, masks {FF,BF,7F,3F}) from its gauge-renderer ctor —
hardware-assisted alarm blinking by masking off the top two pixel bits. Same
pods, so it is an easy source of cross-game misattribution.
Scope note: callers of vtable slot 19 were not exhaustively enumerated (virtual
dispatch); the PPC site was found via the three application+0x4c uses
(0x4a03fb here, 0x4cc3be / 0x4d1559 unrelated). The low-level path IS
exhaustive — @0x47d76d has exactly one caller, and @0x46d840 exactly two
(set @0x47002b, restore @0x470076).
Key Relationships
- Full history:
docs/GAUGE_COMPOSITE.md; reticle recovery:phases/phase-02-dpl2d-reticle.md. - Uses: attribute-pointer + reconstruction-gotchas; reads subsystems state.
- Renders on: pod-hardware MFD surfaces.
#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.
The tripwires are DEFAULT-ON in every build (BT_PLANE_AUDIT=0 opts out): the
plane-leak trap, plus an out-of-bounds draw-start trap in buildDestPointer
(release Verify is a no-op, so a wild start previously wrote silently -- now
logged [plane] OOB and clamped). First detection of either class writes a
GLITCH matchlog record, so if the artifact has a third cause we have not
seen, the evidence lands in the round logs the operator already collects --
no env setup needed on playtest machines. (Coverage note: an in-buffer
EXTENT overrun from a valid start -- a blit walking past the right/bottom edge
into later rows of the same plane -- is NOT trapped; per-pixel walk checks are
too hot. The plane trap still catches it whenever the color leaks planes.)
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.]