# 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) — fires `GetGaugeRenderer()->SpecialEffect(scrambleVideo, damageType*0.2f)` once per EnergyDamageType message. `BT_DMG_LOG` prints `[ppc-scramble]`. - **B — visual:** `SVGA16::FunkyVideo` (was the 2007 stub) now arms `scrambleActive`; `SVGA16::ScrambleRowShift` (new) is the per-source-row horizontal shear, read by BOTH `DrawDevSurface` (surround/dock) and `ExpandPlaneToBGRA` (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=1` forces it always-on for tuning by eye without waiting for a PPC hit.** - **C — non-stacking latch:** `L4GaugeRenderer::SpecialEffect` now ignores the re-arm while `scrambleVideoFlag` is set (matches the binary's `modified` latch); 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`/`roll` are 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. ```cpp // @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 `SVGA16` `pixelBuffer` (`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` / `ProcessVideoEffects` already own the flag and the 0.8 s timeout and are already called per frame. `FunkyVideo` should only set/clear state — no timing logic of its own. --- ## 5. Fidelity constraints (do not "improve" these) 1. Duration is `damageType * 0.2f`, **not** a literal `0.8f`. 2. Fires on `EnergyDamageType` — never add an explicit PPC class check; the data authorship *is* the gate. 3. **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. Our `SpecialEffect` currently *does* overwrite `scrambleVideoTimeout`, which extends the effect on a second hit — **that is a divergence.** Match the binary: ignore re-arm while `scrambleVideoFlag` is set. (The original's latch was in `FunkyVideo`; ours must go in `SpecialEffect` or `FunkyVideo`, but it must exist.) 4. Victim-side only. The shooter sees nothing; this runs in the victim's damage handler. 5. 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 `k` and `roll`. - **Against the original (optional, decisive):** in the DOSBox-X fork, log writes to CRTC index 0 via `0x3D4`/`0x3D5` during a real BT mission. Prediction: exactly one write of `saved-9` per 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 ports `0x302/0x30A/0x312`). That machinery is linked and its per-frame cycler runs, but `FlashPalette` @`0x46d5f4` has **zero call sites and zero address-of references** in `BTL4OPT.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" in `L4CTRL.HPP` are **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 — @`0x47d76d` has one caller, @`0x46d840` two (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"