PPC scramble: animate it as a collapse -> hold-roll+shake -> recover -> lock
Reworks the flat per-row shear into the authentic sync-loss-and-relock
transition the playtesters described, on both render paths (surround
DrawDevSurface + glass ExpandPlaneToBGRA, native + rotated radar):
1. COLLAPSE -- the image squeezes to a thin centred vertical line.
2. HOLD (~0.8s, the window the card held the detuned CRTC) -- the line's
content scrolls WILDLY fast and decelerates, plus a violent per-frame
SHAKE (LCG jitter: vertical row bounce + horizontal scroll jitter).
3. RECOVERY (~0.5s) -- the line broadens back to full while the scroll,
tear and shake settle to zero.
4. LOCK -- clean full display.
Mechanism: FunkyVideo(on, dur) records start+hold; ScrambleParams is a
two-phase envelope (hold: deep collapse + decelerating scroll = integral of
v(t)=rollMax*(1-t/hold); recovery: smoothstep broaden + settle). The read
loops map the source through it -- `scale` sets a black-bordered collapse
band, `rollOff` SCROLLS (wraps) within it, `shear` is a per-row diagonal,
`shakeRow` bounces the source row. The SVGA16 owns the full hold+recovery
clock, so FunkyVideo(False) is a no-op (the card restoring sync is where the
recovery begins) -- the recovery isn't cut off when the 0.8s latch clears.
Tunable by eye (pod-monitor PLL look isn't recoverable from the binary):
BT_SCRAMBLE_COLLAPSE (0.03), _ROLL (9000 px/s), _SHAKE (10 px), _SHEAR (3),
_DUR (hold), _RECOVER (0.5s). BT_SCRAMBLE_TEST loops the transition for
tuning; BT_SCRAMBLE_CYCLE loops it stepping the roll speed.
Verified: Release links clean; surround loops the effect at slowed + true
full speed, all secondaries + radar collapse/roll/shake/recover together,
main view clean, no crash. SHIPPING with these defaults for playtester
feedback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+222
-62
@@ -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;
|
||||
}
|
||||
|
||||
//########################################################################
|
||||
|
||||
Reference in New Issue
Block a user