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>
11 KiB
Phase 14 — PPC hit = scrambleVideo, the cockpit-CRT sync detune
Goal: restore the PPC's authentic secondary effect — a PPC strike scrambles every secondary cockpit display for 0.8 s, leaving the main view untouched.
Status: ✅ IMPLEMENTED 2026-08-06 (branch ppc-sync-distortion) [T2 —
visual screenshot-verified, trigger by construction]. Discovered 2026-08-06 by
disassembly of the shipped BTL4OPT.EXE (md5 a97075bcb5634d13263e9ad5a2b96fd0)
after playtesters reported "being hit by a PPC makes it look like all of the
secondary CRTs were being degaussed." Full findings: context/gauges-hud.md
§"PPC HIT = a deliberate CRTC horizontal-sync DETUNE"; cross-ref in
context/combat-damage.md.
What shipped (all three work items):
- A — trigger:
game/reconstructed/mech.cpp,Mech::TakeDamageMessageHandler@0x4a03f3 position (after the cylinder resolve, before the burst loop) — firesGetGaugeRenderer()->SpecialEffect(scrambleVideo, damageType*0.2f)once per EnergyDamageType message.BT_DMG_LOGprints[ppc-scramble]. - B — visual:
SVGA16::FunkyVideo(was the 2007 stub) now armsscrambleActive;SVGA16::ScrambleRowShift(new) is the per-source-row horizontal shear, read by BOTHDrawDevSurface(surround/dock) andExpandPlaneToBGRA(glass windows), so all secondary surfaces + the rotated radar shear together and the main 3D view (a separate timing chain) is untouched. Tunable:BT_SCRAMBLE_SHEAR(px/line, default 4),BT_SCRAMBLE_ROLL(px/sec, default 220).BT_SCRAMBLE_TEST=1forces it always-on for tuning by eye without waiting for a PPC hit. - C — non-stacking latch:
L4GaugeRenderer::SpecialEffectnow ignores the re-arm whilescrambleVideoFlagis set (matches the binary'smodifiedlatch); a second PPC during the window no longer extends it. - 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. Open: live PPC-fire confirmation (one arm per hit, non-stacking
across two hits) + the by-eye shear tuning belong to the playtesters who filed
the report —
k/rollare not recoverable from the binary.
Good news up front: the engine half already exists in our tree under the
original VWE names (SpecialEffect / scrambleVideo / FunkyVideo). Only
two things are missing: the trigger (never ported) and the visual
(stubbed out in 2007). This is a small, well-bounded job.
1. What the original did [T1 — disasm-verified]
On the victim's machine, Mech::TakeDamageMessageHandler @0x4a0230 tests
the damage type between the collision divert and the burst loop:
004a03f3 mov ecx,[esi+0x2c] ; damage.damageType
004a03f6 cmp ecx,4 ; EnergyDamageType
004a03f9 jne 0x4a0423 ; everything else -> burst loop
004a03fb mov eax,[0x4efc94] ; 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 == SpecialEffect(0, 0.8f)
→ L4GaugeRenderer::SpecialEffect(scrambleVideo, 0.8f) @0x46ffcc
→ SVGA16::FunkyVideo(True) @0x47d76d, which reprograms the VGA CRT
controller:
out(0x3D4,0x11); v=in(0x3D5); out(0x3D5, v & 0x7F) ; unlock CRTC regs 0-7
out(0x3D4,0x00) ; CRTC 0 = HORIZONTAL TOTAL
saved = in(0x3D5); out(0x3D5, saved - 9) ; shorten every scanline
out(0x3D4,0x11); out(0x3D5, v) ; restore write-protect
A per-frame timer @0x47003c writes saved back 0.8 s later.
Why only the PPC: a BTL4.RES census gives damageType 4 (Energy) =
14 subsystem records, every one PPC or ERPPC. Everything else is Ballistic
(16), Explosive (30), Laser (78). The branch is structurally PPC-exclusive —
no extra gating needed.
Why only the secondaries: all six secondary displays are derived by the VDB from that one VGA's timing, so they break together. The main out-the-window view comes off the Division VPX card on an independent timing chain and is unaffected. Playtesters confirm both halves.
2. What our tree already has
| Piece | Where | State |
|---|---|---|
enum VideoEffectType { scrambleVideo } (value 0) |
engine/MUNGA/GAUGREND.h:482 |
✅ present |
virtual void GaugeRenderer::SpecialEffect(VideoEffectType, Scalar) {} |
engine/MUNGA/GAUGREND.h:488 |
✅ base no-op |
L4GaugeRenderer::SpecialEffect — sets scrambleVideoFlag, scrambleVideoTimeout = Now()+duration, calls FunkyVideo(True) |
engine/MUNGA_L4/L4GREND.cpp:806 |
✅ implemented |
L4GaugeRenderer::ProcessVideoEffects() — on timeout, FunkyVideo(False) |
engine/MUNGA_L4/L4GREND.cpp:832 |
✅ implemented |
…called every frame from ExecuteForeground |
engine/MUNGA_L4/L4GREND.cpp:370 |
✅ live |
SVGA16::FunkyVideo(Logical) |
engine/MUNGA_L4/L4VB16.cpp:6358 |
❌ STUBBED (//STUBBED: VIDEO RB 1/15/07, body commented out) |
Any caller of SpecialEffect |
— | ❌ NONE |
The base-class declaration carries its original comment:
// Quick and dirty hack to allow calling L4GaugeRenderer::SpecialEffect// from non L4 level. GDU 2/28/96
That hack exists because the damage handler (non-L4 level) had to call it —
independent corroboration that the trigger belonged in mech.cpp, and the
reason you can call it through the base pointer without dragging L4 headers
into a game-layer TU.
3. Work item A — the trigger (game/reconstructed/mech.cpp)
In Mech::TakeDamageMessageHandler, after the damageType==0 collision
divert and before the burst loop, add the type-4 branch.
// @0x4a03f3 [T1] -- PPC/ERPPC only: EnergyDamageType is authored on exactly
// the 14 PPC/ERPPC subsystem records and nothing else. Duration is DERIVED
// from the type ordinal, not a constant: (float)damageType * 0.2 == 0.8f.
if (damage.damageType == Damage::EnergyDamageType) // == 4
{
GaugeRenderer *gauges = (application != 0)
? application->GetGaugeRenderer() : 0; // APP.h:355
if (gauges != 0) // binary null-guards too
{
gauges->SpecialEffect(
GaugeRenderer::scrambleVideo,
(Scalar)damage.damageType * 0.2f); // long double 0.2 @0x4a0c08
}
}
Placement matters. The binary's branch is outside the burst loop, so it
fires once per damage message, not once per burst. Putting it inside the
loop would re-arm it burstCount times.
Use the named accessor — do not raw-read application+0x4c (databinding rule,
context/reconstruction-gotchas.md).
4. Work item B — the visual (SVGA16::FunkyVideo)
There is no CRTC to detune, so reproduce the look, applied to the gauge composite only.
What the original did physically: Horizontal Total sets character clocks per
scanline. Subtracting 9 shortens every line by roughly 9%, far outside any
monitor's sync lock range, so the picture breaks into a rolling diagonal tear
until the value is restored. VWE's own name for it — scrambleVideo — is the
best description of the intended result.
Suggested model (per-scanline horizontal displacement of the gauge buffer):
shift(y, t) = ( y * k + roll(t) ) mod width
k— per-line shear, the fraction of a line lost. ~9% of width is the physically-derived starting point; tune by eye against the playtester description rather than treating it as exact, because the on-screen result depended on how each pod monitor's H-sync PLL misbehaved — that is not recoverable from the binary.roll(t)— a time-varying offset so the tear drifts rather than sitting static. The original rolled because the monitor never re-locked.
Constraints:
- Gauge composite only. Apply to the
SVGA16pixelBuffer(engine/MUNGA_L4/l4vb16.h:243) or at the point the strip/surfaces are presented — never the main 3D view. The main view being clean is a confirmed observation, not an assumption. - All secondary surfaces together. They are bit-planes of one shared buffer, so a single buffer-level effect is authentic by construction; do not implement it per-MFD.
- Keep the existing timing path.
SpecialEffect/ProcessVideoEffectsalready own the flag and the 0.8 s timeout and are already called per frame.FunkyVideoshould only set/clear state — no timing logic of its own.
5. Fidelity constraints (do not "improve" these)
- Duration is
damageType * 0.2f, not a literal0.8f. - Fires on
EnergyDamageType— never add an explicit PPC class check; the data authorship is the gate. - Idempotent, non-stacking. The binary latches on
modified@0x4fe0fe: a second PPC hit while the effect is live does not re-save the (already detuned) value and does not extend or double the effect. OurSpecialEffectcurrently does overwritescrambleVideoTimeout, which extends the effect on a second hit — that is a divergence. Match the binary: ignore re-arm whilescrambleVideoFlagis set. (The original's latch was inFunkyVideo; ours must go inSpecialEffectorFunkyVideo, but it must exist.) - Victim-side only. The shooter sees nothing; this runs in the victim's damage handler.
- Null-guard the gauge renderer — the binary does, and headless/bench runs have none.
6. Verification
- Headless: add a one-line log in the new branch; fire a PPC at a dummy
with
BT_DMG_LOG. Expect exactly one arm per PPC message, zero for laser / autocannon / missile / Gauss. - Non-stacking: two PPC hits ~0.2 s apart must produce one 0.8 s effect measured from the first hit, not 1.0 s or two effects.
- Live: confirm every secondary MFD scrambles together and the main view
stays clean. Have the playtesters who filed the report compare — they are
the only ground truth for
kandroll. - Against the original (optional, decisive): in the DOSBox-X fork, log
writes to CRTC index 0 via
0x3D4/0x3D5during a real BT mission. Prediction: exactly one write ofsaved-9per PPC strike, one restore 0.8 s later, zero for every other weapon.
7. Gotchas
- Do not confuse this with
SVGA16::FlashPalette(the pixel-mask cycler on ports0x302/0x30A/0x312). That machinery is linked and its per-frame cycler runs, butFlashPalette@0x46d5f4has zero call sites and zero address-of references inBTL4OPT.EXE— BT never arms it. RP does (RPL4OPT.EXE@0x4addce, palette 1, rate 2.0, masks{FF,BF,7F,3F}) for alarm blinking. Wrong mechanism, wrong game. - The
LampTesla1/2/3"solid-state relays" inL4CTRL.HPPare not involved and are driven by nothing in the surviving tree. - Scope note: callers of gauge-renderer vtable slot 19 were not exhaustively
enumerated (virtual dispatch), so another arming site may exist. The
low-level path is exhaustive — @
0x47d76dhas one caller, @0x46d840two (set @0x47002b, restore @0x470076).
References
context/gauges-hud.md§"PPC HIT = a deliberate CRTC horizontal-sync DETUNE"context/combat-damage.md§Mech::TakeDamageMessageHandler(Energy branch)HISTORY.md(TeslaRel410) §"Anatomy of a surviving weapon — the PPC"