diff --git a/engine/MUNGA_L4/L4GREND.cpp b/engine/MUNGA_L4/L4GREND.cpp index 2342934..6f780d5 100644 --- a/engine/MUNGA_L4/L4GREND.cpp +++ b/engine/MUNGA_L4/L4GREND.cpp @@ -823,7 +823,7 @@ void scrambleVideoTimeout = ((Scalar)Now()) + duration; Check(graphicsDisplay); - ((SVGA16*) graphicsDisplay)->FunkyVideo(True); + ((SVGA16*) graphicsDisplay)->FunkyVideo(True, duration); // duration -> the recovery envelope } break; diff --git a/engine/MUNGA_L4/L4VB16.cpp b/engine/MUNGA_L4/L4VB16.cpp index 904d40c..51bc686 100644 --- a/engine/MUNGA_L4/L4VB16.cpp +++ b/engine/MUNGA_L4/L4VB16.cpp @@ -615,22 +615,39 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa Word *source = pixelBuffer.Data.MapPointer; Word *dest = (Word*)rect.pBits; int postRowIncrement = (rect.Pitch / 2) - w; - // PPC sync-scramble (phase-14): read each SOURCE row with a horizontal wrap - // (ScrambleRowShift == 0 unless FunkyVideo is armed), so the surface tears - // like a monitor that has lost horizontal lock. Indexed read (srcRow[sx]) - // instead of the linear source++. + // PPC sync-scramble (phase-14): map the SOURCE read through the recovery + // envelope (collapse toward a line -> broaden -> lock). Identity when not + // armed (ScrambleParams returns False). Output columns outside the collapsed + // band read index 0 (black), so the picture squeezes to a line and grows back. + double scl, shr, roff; int cx, shake; + Logical scr = ScrambleParams(w, &scl, &shr, &roff, &cx, &shake); + double invS = scr ? (1.0 / scl) : 1.0; if (monoTint < 0) { // PALETTE surface (sec/radar) -- palette-LUT expand (== SVGA16::Update case 0). SVGA16Palette *pal = &palette[paletteID]; for (int y = 0; y < h; y++) { - Word *srcRow = source + y * w; - int sh = ScrambleRowShift(y, w); + int sry = y + shake; if (sry < 0) sry = 0; else if (sry >= h) sry = h - 1; + Word *srcRow = source + sry * w; // vertical shake bounces the row + double rowScroll = roff + shr * (double)y; // scroll + diagonal, per row for (int x = 0; x < w; x++) { - int sx = x + sh; if (sx >= w) sx -= w; - PaletteTriplet *pe = &(pal->paletteData.Color[srcRow[sx] & mask]); + Word px; + if (scr) + { + double srcBase = ((double)x - cx) * invS + cx; // collapse band + if (srcBase < 0.0 || srcBase >= w) + px = 0; // black outside the line + else + { + long sx = (long)(srcBase + rowScroll + 0.5); + sx %= w; if (sx < 0) sx += w; // roll SCROLLS within source + px = srcRow[sx]; + } + } + else px = srcRow[x]; + PaletteTriplet *pe = &(pal->paletteData.Color[px & mask]); *dest = ((pe->Red >> 3) << 11) | ((pe->Green >> 2) << 5) | (pe->Blue >> 3); dest++; } @@ -644,12 +661,26 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa Word tint = (Word) monoTint; for (int y = 0; y < h; y++) { - Word *srcRow = source + y * w; - int sh = ScrambleRowShift(y, w); + int sry = y + shake; if (sry < 0) sry = 0; else if (sry >= h) sry = h - 1; + Word *srcRow = source + sry * w; // vertical shake bounces the row + double rowScroll = roff + shr * (double)y; for (int x = 0; x < w; x++) { - int sx = x + sh; if (sx >= w) sx -= w; - *dest = (srcRow[sx] & mask) ? tint : 0; + Word px; + if (scr) + { + double srcBase = ((double)x - cx) * invS + cx; + if (srcBase < 0.0 || srcBase >= w) + px = 0; + else + { + long sx = (long)(srcBase + rowScroll + 0.5); + sx %= w; if (sx < 0) sx += w; + px = srcRow[sx]; + } + } + else px = srcRow[x]; + *dest = (px & mask) ? tint : 0; dest++; } dest += postRowIncrement; @@ -722,18 +753,38 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate Word *base = pixelBuffer.Data.MapPointer; SVGA16Palette *pal = &palette[paletteID]; + // PPC sync-scramble (phase-14) -- the recovery envelope (collapse -> broaden -> + // lock), mapped in SOURCE space so it survives the rotation. Identity when not + // armed (ScrambleParams False). Reads outside the collapsed band -> index 0 (black). + double scl, shr, roff; int cx, shake; + Logical scr = ScrambleParams(w, &scl, &shr, &roff, &cx, &shake); + double invS = scr ? (1.0 / scl) : 1.0; + if (rotateQuadrant == 0) { // Native orientation, top-down straight copy. for (int y = 0; y < h; y++) { - Word *src = base + y * w; + int sry = y + shake; if (sry < 0) sry = 0; else if (sry >= h) sry = h - 1; + Word *src = base + sry * w; // vertical shake bounces the row unsigned long *d = dst + y * w; - int sh = ScrambleRowShift(y, w); // PPC scramble (0 unless armed) + double rowScroll = roff + shr * (double)y; for (int x = 0; x < w; x++) { - int sx = x + sh; if (sx >= w) sx -= w; - Word s = src[sx]; + Word s; + if (scr) + { + double srcBase = ((double)x - cx) * invS + cx; // collapse band + if (srcBase < 0.0 || srcBase >= w) + s = 0; // black outside the line + else + { + long sx = (long)(srcBase + rowScroll + 0.5); + sx %= w; if (sx < 0) sx += w; // roll SCROLLS within source + s = src[sx]; + } + } + else s = src[x]; if (monoTint < 0) { PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]); @@ -753,14 +804,9 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate // 90-degree rotation: output is transposed (ow = h, oh = w). rotate 3 = CW, // rotate 1 = CCW (the DrawDevSurface convention; BT_GAUGE_SEC_ROT picks it). + // The scramble maps the SOURCE column (sx keyed on source row sy) through the + // same envelope, so the radar recovers the same way the landscape MFDs do. int ow = h, oh = w; - // PPC scramble: precompute the per-SOURCE-ROW shift once. sy is used per output - // pixel and ScrambleRowShift reads the clock, so it must not be called per pixel. - // The shear stays in SOURCE space (keyed on sy), so the rotation displays an - // authentically-sheared raster. - int rowSh[512]; - int shRows = (scrambleActive && h <= 512) ? h : 0; - for (int i = 0; i < shRows; i++) rowSh[i] = ScrambleRowShift(i, w); for (int oy = 0; oy < oh; oy++) { unsigned long *d = dst + oy * ow; @@ -777,8 +823,21 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate sx = w - 1 - oy; sy = ox; } - if (shRows) { sx += rowSh[sy]; if (sx >= w) sx -= w; } - Word s = base[sy * w + sx]; + int syS = sy + shake; if (syS < 0) syS = 0; else if (syS >= h) syS = h - 1; // shake + Word s; + if (scr) + { + double srcBase = ((double)sx - cx) * invS + cx; // collapse band (source col) + if (srcBase < 0.0 || srcBase >= w) + s = 0; + else + { + long msx = (long)(srcBase + roff + shr * (double)sy + 0.5); + msx %= w; if (msx < 0) msx += w; // roll SCROLLS within source + s = base[syS * w + msx]; + } + } + else s = base[syS * w + sx]; if (monoTint < 0) { PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]); @@ -5415,7 +5474,9 @@ SVGA16::SVGA16( BuildWindows(init_width,init_height,windowed, secondaryIndex, aux1Index, aux2Index); for (int _i = 0; _i < 10; _i++) // DEV-COMPOSITE: lazily created on first surface draw mDevSurfaceTex[_i] = NULL; - scrambleActive = False; // PPC sync-scramble (phase-14) -- armed by FunkyVideo + scrambleActive = False; // PPC sync-scramble (phase-14) -- armed by FunkyVideo + scrambleStartMs = 0; + scrambleDurationS = 0.0; //STUBBED: VIDEO RB 1/15/07 # if defined(DEBUG) Tell("SVGA16::SVGA16()\n"); @@ -6376,53 +6437,152 @@ void } // -// PPC sync-scramble (phase-14). The original (`//STUBBED: VIDEO RB 1/15/07`) -// detuned the VGA CRTC Horizontal Total by -9 so every secondary monitor lost -// horizontal lock for 0.8 s. There is no CRTC here, so FunkyVideo just arms / -// disarms a flag; the per-surface expansions (DrawDevSurface for the surround, -// ExpandPlaneToBGRA for the glass windows) shear the pixelBuffer read while it is -// set. The 0.8 s window + non-stacking latch stay in L4GaugeRenderer. +// PPC sync-scramble (phase-14), animated as a RECOVERY. The original +// (`//STUBBED: VIDEO RB 1/15/07`) detuned the VGA CRTC Horizontal Total so every +// secondary monitor lost horizontal lock for ~0.8 s. There is no CRTC here, so +// FunkyVideo records the start + duration and the per-surface expansions +// (DrawDevSurface / ExpandPlaneToBGRA) map the pixelBuffer read through the +// ScrambleParams envelope: at the hit the image COLLAPSES horizontally toward a +// line, then BROADENS back out as the tear + roll DECAY, and LOCKS to the full +// clean display at the end. The 0.8 s window + non-stacking latch stay in +// L4GaugeRenderer; the shape lives here. // void - SVGA16::FunkyVideo(Logical on_off) + SVGA16::FunkyVideo(Logical on_off, Scalar duration) { - scrambleActive = on_off; + if (on_off) + { + scrambleActive = True; + scrambleStartMs = GetTickCount(); + scrambleDurationS = (duration > 0.0f) ? (double)duration : 0.8; // the HOLD + } + // FunkyVideo(False) is a NO-OP: the card restoring the sync (at HOLD end) is + // where the RECOVERY begins, so ScrambleParams keeps running past the flag, + // self-terminating at hold+recovery. scrambleActive stays armed; the clock gates. } // -// Per-SOURCE-ROW horizontal shift for the scramble. Model (phase-14 §4): -// shift(y,t) = ( y*shear + roll*t ) mod width -// a constant per-line shear (the CRTC detune offset every scanline by a fixed -// amount -> a diagonal tear) plus a time roll (the monitor never re-locked, so -// the tear drifts). The physically-derived shear is ~9% of a line, but the -// on-screen result depended on each pod monitor's H-sync PLL -- not recoverable, -// so the default is a legible moderate tear; tune by eye against the playtesters -// who filed the report (BT_SCRAMBLE_SHEAR px/line, BT_SCRAMBLE_ROLL px/sec). +// Two-phase envelope (per the "hold the bad sync, THEN recover" direction): +// HOLD [0, hold] -- the card holds the detuned CRTC. Deep collapse (a line), +// and a WILDLY fast horizontal roll that decelerates over +// the hold (content scrolls, wrapping, so it can't be read). +// RECOVERY [hold, +] -- the sync is restored and the monitor re-locks: the line +// BROADENS back to full while the residual scroll + tear +// settle to zero, then LOCKS (returns False = clean). +// Fills the read-loop parameters: scale (collapse band width fraction), rollOff +// (SCROLL offset in px, wrapped within the source), shear (px/row diagonal), +// centerX. Tunables (read once): BT_SCRAMBLE_COLLAPSE (min band, deeper=smaller), +// BT_SCRAMBLE_ROLL (initial scroll px/sec -- "wildly high"), BT_SCRAMBLE_SHEAR +// (px/row), BT_SCRAMBLE_RECOVER (recovery seconds), BT_SCRAMBLE_DUR (hold seconds +// override). BT_SCRAMBLE_TEST loops it; BT_SCRAMBLE_CYCLE loops stepping the roll. // -int - SVGA16::ScrambleRowShift(int srcRow, int width) const +Logical + SVGA16::ScrambleParams(int width, double *scale, double *shear, + double *rollOff, int *centerX, int *shakeRow) const { - // BT_SCRAMBLE_TEST forces the effect always-on so the shear can be TUNED by eye - // (adjust BT_SCRAMBLE_SHEAR / BT_SCRAMBLE_ROLL and relaunch) without waiting to - // be hit by a PPC. Off by default. - static int sTest = -1; - if (sTest < 0) sTest = (getenv("BT_SCRAMBLE_TEST") != NULL) ? 1 : 0; - if ((!scrambleActive && !sTest) || width <= 0) - return 0; + *shakeRow = 0; + if (width <= 0) + return False; - static double shearPx = -1.0, rollPx = -1.0; - if (shearPx < 0.0) + static int sInit = 0, sTest = 0, sCycle = 0; + static double shearMax = 3.0, rollMax = 9000.0, collapse = 0.03, + recover = 0.5, holdEnv = 0.0, shakeAmp = 10.0; + if (!sInit) { - const char *e = getenv("BT_SCRAMBLE_SHEAR"); - const char *r = getenv("BT_SCRAMBLE_ROLL"); - shearPx = (e != NULL) ? atof(e) : 4.0; // px/line (aggressive ~9%: width*0.09) - rollPx = (r != NULL) ? atof(r) : 220.0; // px/sec drift + sInit = 1; + sTest = (getenv("BT_SCRAMBLE_TEST") != NULL) ? 1 : 0; + sCycle = (getenv("BT_SCRAMBLE_CYCLE") != NULL) ? 1 : 0; + const char *e; + if ((e = getenv("BT_SCRAMBLE_SHEAR")) != NULL) shearMax = atof(e); + if ((e = getenv("BT_SCRAMBLE_ROLL")) != NULL) rollMax = atof(e); + if ((e = getenv("BT_SCRAMBLE_COLLAPSE")) != NULL) collapse = atof(e); + if ((e = getenv("BT_SCRAMBLE_RECOVER")) != NULL) recover = atof(e); + if ((e = getenv("BT_SCRAMBLE_DUR")) != NULL) holdEnv = atof(e); + if ((e = getenv("BT_SCRAMBLE_SHAKE")) != NULL) shakeAmp = atof(e); + if (collapse < 0.004) collapse = 0.004; + if (collapse > 1.0) collapse = 1.0; + if (recover < 0.05) recover = 0.05; } - double roll = rollPx * ((double)GetTickCount() * 0.001); - long s = (long)((double)srcRow * shearPx + roll); - s %= width; - if (s < 0) s += width; - return (int)s; + + double hold = (holdEnv > 0.0) ? holdEnv + : (scrambleDurationS > 0.0 ? scrambleDurationS : 0.8); + double total = hold + recover; + double rlMax = rollMax; + double elapsed; + + if (sTest || sCycle) + { + // Tuning: loop the whole effect (hold+recovery) + a short locked pause. + // CYCLE steps the initial roll speed each loop. + double loop = total + 0.6; + double t = (double)GetTickCount() * 0.001; + long n = (long)(t / loop); + double ph = t - (double)n * loop; + if (ph >= total) + return False; // locked pause between loops + elapsed = ph; + if (sCycle) + { + static const double kR[] = { 3000.0, 6000.0, 9000.0, 14000.0, 20000.0 }; + int nS = (int)(sizeof(kR) / sizeof(kR[0])); + rlMax = kR[(int)(n % nS)]; + static long lastN = -1; + if (n != lastN) { lastN = n; + DEBUG_STREAM << "[scramble-cycle] loop " << n << " rollMax=" << rlMax + << "px/s (hold " << hold << "s + recover " << recover << "s)\n" + << std::flush; } + } + } + else + { + if (!scrambleActive) + return False; + elapsed = ((double)(GetTickCount() - scrambleStartMs)) * 0.001; + if (elapsed >= total) + return False; // LOCKED -- clean display + } + if (elapsed < 0.0) elapsed = 0.0; + + double sScale, sScroll, sShear; + if (elapsed < hold) + { + // HOLD: bad sync held. Deep collapse; roll starts wild and decelerates to a + // stop by the hold end. scroll = integral of v(t)=rlMax*(1-t/hold): the + // content scrolls fast then coasts to rest (rlMax*hold/2 accumulated). + sScale = collapse; + sScroll = rlMax * elapsed * (1.0 - elapsed / (2.0 * hold)); + sShear = shearMax; + } + else + { + // RECOVERY: sync restored, monitor re-locks. Broaden line->full and settle + // the residual scroll + tear to zero (smoothstep), then it hits total = lock. + double tr = (elapsed - hold) / recover; + double b = tr * tr * (3.0 - 2.0 * tr); + sScale = collapse + (1.0 - collapse) * b; + sScroll = (rlMax * hold * 0.5) * (1.0 - b); + sShear = shearMax * (1.0 - b); + } + + // SHAKE: a per-FRAME jitter (LCG, so all surfaces shake together within a frame + // but re-roll each frame), violent at the hit and fading through the recovery. + // Horizontal folds into the scroll; vertical bounces the source row (shakeRow). + double shakeEnv = (elapsed < hold) + ? (1.0 - 0.5 * (elapsed / hold)) // 1.0 at impact -> 0.5 at hold end + : (0.5 * (1.0 - (elapsed - hold) / recover)); // 0.5 -> 0 through recovery + if (shakeEnv < 0.0) shakeEnv = 0.0; + unsigned int fr = (unsigned int)(GetTickCount() / 16); // ~per-frame index + unsigned int r = fr * 1103515245u + 12345u; + double jX = ((double)((r >> 16) & 0x7FFF) / 16384.0) - 1.0; // [-1,1) + r = r * 1103515245u + 12345u; + double jY = ((double)((r >> 16) & 0x7FFF) / 16384.0) - 1.0; + + *scale = sScale; + *shear = sShear; + *rollOff = sScroll + shakeAmp * shakeEnv * jX; // horizontal shake -> scroll + *centerX = width / 2; + *shakeRow = (int)(shakeAmp * shakeEnv * jY + (jY >= 0.0 ? 0.5 : -0.5)); // vertical bounce + return True; } //######################################################################## diff --git a/engine/MUNGA_L4/L4VB16.h b/engine/MUNGA_L4/L4VB16.h index f9dda0e..0401157 100644 --- a/engine/MUNGA_L4/L4VB16.h +++ b/engine/MUNGA_L4/L4VB16.h @@ -332,21 +332,27 @@ public: UnflashPalette(int palette_number); void - FunkyVideo(Logical on_off); + FunkyVideo(Logical on_off, Scalar duration = 0.0f); // PPC sync-scramble (phase-14): the modern stand-in for the VGA CRTC - // Horizontal-Total detune. FunkyVideo(on) sets scrambleActive; every - // gauge-surface expansion (DrawDevSurface for the surround, ExpandPlaneToBGRA - // for the glass windows) then reads the pixelBuffer with a per-SOURCE-ROW - // horizontal shift so all secondary displays shear together -- authentic by - // construction (one VGA's detuned sync drives them all). Returns the source-x - // offset for a source row, 0 when not scrambling. Tunable by eye - // (BT_SCRAMBLE_SHEAR px/line, BT_SCRAMBLE_ROLL px/sec) -- the exact look - // depended on each pod monitor's H-sync PLL and is not recoverable. - int - ScrambleRowShift(int srcRow, int width) const; + // Horizontal-Total detune, animated as a RECOVERY over the effect window -- at + // the hit the image collapses horizontally toward a line, then broadens back + // out as the tear + roll decay, and LOCKS to the full clean display at the end. + // FunkyVideo(on, dur) records the start + duration; ScrambleParams derives the + // per-frame envelope (returns True while active + fills scale/shear/rollOff/ + // centerX); DrawDevSurface (surround) + ExpandPlaneToBGRA (glass) map the + // pixelBuffer read through it, so all secondary surfaces recover together and + // the main 3D view is untouched. Tunable by eye: BT_SCRAMBLE_SHEAR (max + // px/line), BT_SCRAMBLE_ROLL (max px/sec), BT_SCRAMBLE_COLLAPSE (min width + // fraction 0..1), BT_SCRAMBLE_DUR (anim seconds override). BT_SCRAMBLE_TEST + // loops the transition for tuning; BT_SCRAMBLE_CYCLE loops it stepping the + // max shear. (Exact look depended on each pod monitor's PLL -- not recoverable.) Logical - scrambleActive; + ScrambleParams(int width, double *scale, double *shear, + double *rollOff, int *centerX, int *shakeRow) const; + Logical scrambleActive; + unsigned long scrambleStartMs; + double scrambleDurationS; protected: diff --git a/phases/phase-14-ppc-sync-distortion.md b/phases/phase-14-ppc-sync-distortion.md index e77f193..c8b9679 100644 --- a/phases/phase-14-ppc-sync-distortion.md +++ b/phases/phase-14-ppc-sync-distortion.md @@ -16,21 +16,38 @@ secondary CRTs were being degaussed."* Full findings: `context/gauges-hud.md` @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.** +- **B — visual (animated recovery):** `SVGA16::FunkyVideo(on, dur)` (was the 2007 + stub) records the start + hold; `SVGA16::ScrambleParams` (new) is a two-phase + envelope read by BOTH `DrawDevSurface` (surround/dock) and `ExpandPlaneToBGRA` + (glass windows, native + rotated radar), so all secondary surfaces recover + together and the main 3D view (separate timing chain) is untouched. The sequence + per hit: **collapse** to a thin centred line → **HOLD** for the ~0.8 s the card + held bad sync (content scrolls wildly fast + decelerating, plus a violent + **shake**) → **RECOVERY** (~0.5 s: the line broadens back to full while + scroll/tear/shake settle) → **LOCK** (clean). The read loops map the source + through the envelope: horizontal `scale` sets a black-bordered collapse band, + `rollOff` scrolls (wraps) within it, `shear` is a per-row diagonal, and a + per-frame LCG `shake` bounces the row + jitters the scroll. Tunables (by eye — + the pod-monitor PLL look is not recoverable): `BT_SCRAMBLE_COLLAPSE` (min band + fraction, 0.03), `BT_SCRAMBLE_ROLL` (initial scroll px/s, 9000), + `BT_SCRAMBLE_SHAKE` (px, 10), `BT_SCRAMBLE_SHEAR` (px/row, 3), `BT_SCRAMBLE_DUR` + (hold s), `BT_SCRAMBLE_RECOVER` (recovery s, 0.5). **`BT_SCRAMBLE_TEST=1` loops + the whole transition** for tuning without a hit; `BT_SCRAMBLE_CYCLE=1` loops it + stepping the roll speed. - **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. + a second PPC during the window no longer extends it. NB the SVGA16 animation runs + the full hold+recovery on its own clock (`FunkyVideo(False)` is a no-op — the + card restoring sync is where the recovery *begins*), so the recovery isn't cut + off when the 0.8 s latch clears. +- Verified [T2]: Release links clean; surround boots + runs with the effect looped + (`BT_SCRAMBLE_TEST`) at both slowed and true full speed — every secondary MFD + + the radar collapse/roll/shake/recover together while the out-the-window view + stays clean, no crash (screenshots). **SHIPPING with the current defaults for + playtester feedback.** Open: live PPC-fire confirmation (one arm per hit, + non-stacking across two hits) + by-eye tuning of the envelope constants — none + recoverable from the binary, so the testers who filed the report are the ground + truth. **Good news up front:** the engine half already exists in our tree under the **original VWE names** (`SpecialEffect` / `scrambleVideo` / `FunkyVideo`). Only