Author SHA1 Message Date
CydandClaude Opus 4.8 bc83f6ca24 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>
2026-08-14 13:13:07 -05:00
CydandClaude Opus 4.8 42c789b993 PPC hit scrambles the secondary displays (phase-14, all three work items)
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>
2026-08-14 13:12:40 -05:00
CydandClaude Opus 4.8 2133abd31d docs: PPC-hit CRTC sync-distortion finding + phase-14 port spec
Disassembly of BTL4OPT.EXE (2026-08-06) recovered the PPC's authentic
secondary-display effect: an EnergyDamageType (==4) hit calls the gauge
renderer's SpecialEffect(scrambleVideo, damageType*0.2f), which detunes the
VGA CRTC Horizontal Total by -9 for 0.8s -- every secondary cockpit display
loses horizontal sync ("looks like the CRTs are being degaussed"), the main
VPX view is untouched.  damageType 4 is authored on exactly the 14 PPC/ERPPC
records, so the branch is structurally PPC-exclusive.

  - phases/phase-14-ppc-sync-distortion.md: the port spec (trigger + visual +
    fidelity constraints + verification).
  - context/gauges-hud.md: full disasm chain + the FlashPalette non-confusion.
  - context/combat-damage.md: the damageType==4 branch in the damage handler.

NOT YET IMPLEMENTED -- this commit is the spec; the effect is the next work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 13:11:57 -05:00
13 changed files with 653 additions and 150 deletions
+10
View File
@@ -717,6 +717,16 @@ Benches: `scratchpad/night12/scorekill.sh` (cross-node kill: killer `kills=1 awa
victim respawns, death #1 single-cycle) + `scoreself.sh` (#134 negation). Collision-death tail victim respawns, death #1 single-cycle) + `scoreself.sh` (#134 negation). Collision-death tail
fallthrough is inspection-tier [T3] — shares the benched tail code; field wall-deaths exercise it. fallthrough is inspection-tier [T3] — shares the benched tail code; field wall-deaths exercise it.
**The handler's OTHER damageType branch — `damageType==4` (Energy) = the PPC cockpit-sync glitch
[T1, 2026-08-06].** Between the collision divert and the burst loop sits a second type test
@`0x4a03f3`: `cmp [esi+0x2c],4 / jne 0x4a0423`. On a match it calls the gauge renderer's vtable
slot 19 with `((float)damageType × 0.2, 0)` = `(0.8f, 0)`, which detunes the **VGA CRTC Horizontal
Total by 9** for 0.8 s — every secondary cockpit display loses horizontal sync, the main VPX view
is untouched. `EnergyDamageType` is authored on **exactly the 14 PPC/ERPPC records and nothing
else**, so this is structurally PPC-exclusive. It fires **once per damage message** (outside the
burst loop). Full chain + addresses: [[gauges-hud]] §"PPC HIT = a deliberate CRTC horizontal-sync
DETUNE"; port spec: `phases/phase-14-ppc-sync-distortion.md`. **Implemented 2026-08-06** (branch `ppc-sync-distortion`).
## (HISTORICAL — the gap as found 2026-07-29, superseded above) [T1] ## (HISTORICAL — the gap as found 2026-07-29, superseded above) [T1]
The authored crit machinery exists and is reconstructed — `Mech__DamageZone::CriticalHit @0049ccc4` The authored crit machinery exists and is reconstructed — `Mech__DamageZone::CriticalHit @0049ccc4`
(half the damage to armour, half to ONE critical subsystem chosen by `criticalWeight`, capped by (half the damage to armour, half to ONE critical subsystem chosen by `criticalWeight`, capped by
+84
View File
@@ -13,6 +13,7 @@ open_questions:
- "Upper-MFD PRESET pages RESOLVED 2026-07-19 (Gitea #9): SetPresetMode table @0051dbf0 re-decoded (little-endian -> ModeMFD bits 0-14), per-MFD pod button banks identified from the .CTL dump, desktop J/K/L cycle wired" - "Upper-MFD PRESET pages RESOLVED 2026-07-19 (Gitea #9): SetPresetMode table @0051dbf0 re-decoded (little-endian -> ModeMFD bits 0-14), per-MFD pod button banks identified from the .CTL dump, desktop J/K/L cycle wired"
- "Always-active msg-4 records IDENTIFIED 2026-07-20 (glass input audit): 0x2C = Reservoir InjectCoolant (the flush button), 0x2F/0x2E/0x2D/0x2B/0x2A/0x29 = Condenser1-6 MoveValve, 0x1A-0x1D = GeneratorA-D ToggleGeneratorOnOff (@0050fb90; wired 2026-07-25, powersub.cpp); plus 0x13 = Mech DuckRequest (CROUCH -- COMPLETE 2026-08-06, [[locomotion]]), 0x28 = Mech BalanceCoolant, 0x12/0x14 = ThermalSight/Searchlight toggles (searchlight visuals done 2026-08-05) -- see pod-hardware.md + docs/GLASS_COCKPIT.md; statuses re-swept 2026-08-06" - "Always-active msg-4 records IDENTIFIED 2026-07-20 (glass input audit): 0x2C = Reservoir InjectCoolant (the flush button), 0x2F/0x2E/0x2D/0x2B/0x2A/0x29 = Condenser1-6 MoveValve, 0x1A-0x1D = GeneratorA-D ToggleGeneratorOnOff (@0050fb90; wired 2026-07-25, powersub.cpp); plus 0x13 = Mech DuckRequest (CROUCH -- COMPLETE 2026-08-06, [[locomotion]]), 0x28 = Mech BalanceCoolant, 0x12/0x14 = ThermalSight/Searchlight toggles (searchlight visuals done 2026-08-05) -- see pod-hardware.md + docs/GLASS_COCKPIT.md; statuses re-swept 2026-08-06"
- "MP DEATHS resolved 2026-07-12 (observed-death tally + display clamp); remaining: verify multi-death tallies stay in sync across a long session (GAUGE_COMPOSITE.md)" - "MP DEATHS resolved 2026-07-12 (observed-death tally + display clamp); remaining: verify multi-death tallies stay in sync across a long session (GAUGE_COMPOSITE.md)"
- "PPC `scrambleVideo` IMPLEMENTED 2026-08-06 (branch ppc-sync-distortion): a PPC hit scrambles every secondary display for 0.8 s (modern per-scanline shear stand-in for the CRTC Horizontal-Total detune). Trigger in Mech::TakeDamageMessageHandler (damageType==4), visual in SVGA16::FunkyVideo/ScrambleRowShift (DrawDevSurface + ExpandPlaneToBGRA), non-stacking latch in L4GaugeRenderer::SpecialEffect. Screenshot-verified; live PPC-fire confirmation + by-eye shear tuning (BT_SCRAMBLE_SHEAR/ROLL) left to playtesters. Spec: phases/phase-14-ppc-sync-distortion.md"
--- ---
# Cockpit Gauges / MFD HUD # Cockpit Gauges / MFD HUD
@@ -755,6 +756,89 @@ pooling fix) — an alarm that cannot acquire a source is silent.
⚠ The bench cannot confirm audibility: it runs with no audio device (`live=0 ⚠ 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. 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`):
1. `Mech::TakeDamageMessageHandler` @`0x4a0230`, branch @**`0x4a03f3`** — sits
between the collision divert and the burst loop, so it runs **once per
damage message**, not per burst:
```
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)
```
⚠ The duration is **derived, not constant**: `(float)damageType × 0.2`.
2. Gauge-renderer vtable @`0x51cebc`, slot 19 (`+0x4c`) = @**`0x46ffcc`**
(an `L4GaugeRenderer` method — 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 its `FlashPalette`), then
`this[+0x1c534] = 1` (active) and `this[+0x1c538] = now + 0.8 s` in ticks.
3. @`0x46d840` — thin wrapper, drops `this`, forwards the flag.
4. @**`0x47d76d`** — the payload, straight VGA CRTC I/O:
```
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-protect
```
Globals: `modified` @`0x4fe0fe`, `saved` @`0x4fe0ff`. The `modified` latch
makes it **idempotent** — overlapping PPC hits do NOT stack, and a second
hit does not re-save an already-detuned value.
5. @**`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 ## Key Relationships
- Full history: `docs/GAUGE_COMPOSITE.md`; reticle recovery: `phases/phase-02-dpl2d-reticle.md`. - Full history: `docs/GAUGE_COMPOSITE.md`; reticle recovery: `phases/phase-02-dpl2d-reticle.md`.
- Uses: [[attribute-pointer]] + [[reconstruction-gotchas]]; reads [[subsystems]] state. - Uses: [[attribute-pointer]] + [[reconstruction-gotchas]]; reads [[subsystems]] state.
-22
View File
@@ -115,28 +115,6 @@ each, so they cover the foot band too, and the hit test takes the first match
what keeps 0x16/0x17/0x1F/0x1E reachable. That class of bug is why the verification below tests what keeps 0x16/0x17/0x1F/0x1E reachable. That class of bug is why the verification below tests
first-hit reachability, not mere presence. first-hit reachability, not mere presence.
**A FLASHING radar button goes RED — `BT_RADAR_FLASH_RED` (2026-08-14, player request)
[T2 pixel-measured]. GLASS-ONLY DEVIATION (#154-class).** The radar's buttons are the yellow
class, so an alerting lamp pulsed *yellow among yellows* — a brightness change only, and the
night-16 miss that also produced the #172 ring and the protrusion bonus. The three desktop
renderers now swap the **bright** colour of a **flashing** radar button to the house red
(230,70,70 — the MFD family), so an alert reads as a *colour* change. Only the bright phase
moves; dim/off keep the yellow family, so a non-alerting radar button is unchanged. One
decode, `BTRadarLampBright(lampState)` in `L4VB16.h` beside `BTLampBrightnessOf`, consumed by
all three renderers (`CkLampColors` in the surround, `PaintGlass` in the exploded windows,
`L4PADPANEL`) so they cannot drift — the lesson of the L4RIOBANK geometry split. `=0` restores
the yellow bright. The pod's own buttons are single-colour backlit hardware and cannot do this;
the serial RIO lamp path is untouched, so this is desktop-only by construction.
**Bench hook `BT_LAMPTEST=<addr>[,<state>]` (2026-08-14).** Forces ONE lamp's state at
`PadRIO::GetLampState` — the single read point all three desktop renderers share — so lamp
rendering can be checked without inducing the fault that would raise it (a generator trip, an
ammo alarm). Default state `0x33` = the Panic lamp's alert shape (flashFast + state1Off +
state2Bright, a hard dark/bright pulse); addresses and states take `0x` hex. This is how the
red-flash change above was verified: `BT_LAMPTEST=0x1b` (GeneratorB's radar lamp, the night-16
one) then a pixel tally of the radar window — **620 house-red px with the feature on, 0 with
`BT_RADAR_FLASH_RED=0`**, and the yellow count falling by exactly that many (2820 → 2196).
## Sticky window placement — BT_GLASS_LAYOUT (2026-07-28) [T2 round-trip-verified] ## Sticky window placement — BT_GLASS_LAYOUT (2026-07-28) [T2 round-trip-verified]
The `BT_GLASS_PANELS` windows self-place in a pod-faithful ring around the main window The `BT_GLASS_PANELS` windows self-place in a pod-faithful ring around the main window
+8 -11
View File
@@ -72,16 +72,13 @@ A. FIXED THIS BUILD -- PLEASE VERIFY (headliners first)
The Nova spawned showing its REAR laser's range pips in the forward The Nova spawned showing its REAR laser's range pips in the forward
view. The HUD now commits the forward view at spawn. (Only the pips view. The HUD now commits the forward view at spawn. (Only the pips
were wrong -- rear weapons never actually fired forward.) were wrong -- rear weapons never actually fired forward.)
#151 (Oracle) Panic eject scored -499 -- CORRECTION: -499 IS the machine #151 (Oracle) Panic eject scored -499
Our line here was wrong (we mis-read our own ticket history; the Solved, and the machine agrees with YOUR math: eject = your mech
correct decode was already on the ticket). Bench-verified: eject self-destructs (scored as self-damage, ~-500) + the death cost
is scored as SUICIDE (about -999, the kill value of your own (-500) = about -1000, i.e. 1000 points -> roughly ZERO. The extra
mech) PLUS the death cost (-500). 1000 - 999 - 500 = -499 -500 you saw was the #162 double-death bug, fixed on testing night.
exactly -- the original's own arithmetic, reproduced three times TEST: repeat the maneuver -- you should land within a few points
on the bench, never doubled. If you eject tonight and land at of zero, not hundreds below.
-499 again, the machine is RIGHT, not broken.
DESIGN CALL (restored): keep the authentic ~-1500 swing, or
house-rule a flat eject cost? Opinions to the thread.
FEEL Three more fixes on the original's clock (same bug class as #173): FEEL Three more fixes on the original's clock (same bug class as #173):
the cockpit HIT-SHAKE (was bouncing ~2x too fast -- hits now rock the cockpit HIT-SHAKE (was bouncing ~2x too fast -- hits now rock
you at the pod's pace with less rebound), the FOOTSTEP loudness you at the pod's pace with less rebound), the FOOTSTEP loudness
@@ -203,7 +200,7 @@ G. INTERNAL ENGINEERING BACKLOG (nothing to test)
============================================================================= =============================================================================
End of list. 57 open issues total: End of list. 57 open issues total:
16 verify (3 headliners), 12 reproduce, 6 era questions, 3 design calls, 17 verify (3 headliners), 12 reproduce, 6 era questions, 2 design calls,
10 known gaps, 11 internal -- plus 4 "not bugs" that are the real machine. 10 known gaps, 11 internal -- plus 4 "not bugs" that are the real machine.
Since Wednesday: 15 fixes fielded, 4 tickets closed by evidence alone, and Since Wednesday: 15 fixes fielded, 4 tickets closed by evidence alone, and
Oracle's questionnaire + soundboard find settled five more. Your reports Oracle's questionnaire + soundboard find settled five more. Your reports
+3 -10
View File
@@ -1472,21 +1472,14 @@ static void
const GButton &b = w->buttons[i]; const GButton &b = w->buttons[i];
const RECT &r = b.rect; const RECT &r = b.rect;
int held = (b.address == pressedAddress) || latched[b.address & 0x7F]; int held = (b.address == pressedAddress) || latched[b.address & 0x7F];
int lampState = PadRIO::GetLampState(b.address); int shade = LampBrightnessOf(PadRIO::GetLampState(b.address), tick);
int shade = LampBrightnessOf(lampState, tick);
if (held) if (held)
shade = 3; shade = 3;
// A FLASHING radar button goes RED at its bright phase (L4VB16.h).
unsigned long radarBright = BTRadarLampBright(lampState);
COLORREF radarBrightRGB = RGB((radarBright >> 16) & 0xFF,
(radarBright >> 8) & 0xFF,
radarBright & 0xFF);
COLORREF fill, text_color; COLORREF fill, text_color;
if (b.color == ClrYellow) if (b.color == ClrYellow)
{ {
fill = (shade == 3) ? radarBrightRGB fill = (shade == 3) ? RGB(245, 210, 60)
: (shade != 0) ? RGB(140, 118, 38) : (shade != 0) ? RGB(140, 118, 38)
: RGB(70, 60, 24); : RGB(70, 60, 24);
text_color = RGB(0, 0, 0); text_color = RGB(0, 0, 0);
@@ -1508,7 +1501,7 @@ static void
// Solid lamp fill + a bright border. Under the imagery, only the protruding // Solid lamp fill + a bright border. Under the imagery, only the protruding
// edge shows once the surface is blitted on top. // edge shows once the surface is blitted on top.
{ {
COLORREF bright = (b.color == ClrYellow) ? radarBrightRGB COLORREF bright = (b.color == ClrYellow) ? RGB(245, 210, 60)
: (b.color == ClrBlue) ? RGB(205, 228, 255) : (b.color == ClrBlue) ? RGB(205, 228, 255)
: RGB(230, 70, 70); : RGB(230, 70, 70);
HBRUSH face = CreateSolidBrush(fill); HBRUSH face = CreateSolidBrush(fill);
+7 -2
View File
@@ -829,13 +829,18 @@ void
switch (type) switch (type)
{ {
case scrambleVideo: case scrambleVideo:
if (graphicsDisplay != NULL) // NON-STACKING LATCH (phase-14 fidelity): the binary latches on `modified`
// @0x4fe0fe -- a second PPC hit while the scramble is ALREADY live does NOT
// re-save the (already detuned) value and does NOT extend the window. This
// original unconditionally overwrote scrambleVideoTimeout, so rapid PPC fire
// STACKED the effect -- a divergence from the binary. Ignore re-arm while live.
if (graphicsDisplay != NULL && !scrambleVideoFlag)
{ {
scrambleVideoFlag = True; scrambleVideoFlag = True;
scrambleVideoTimeout = ((Scalar)Now()) + duration; scrambleVideoTimeout = ((Scalar)Now()) + duration;
Check(graphicsDisplay); Check(graphicsDisplay);
((SVGA16*) graphicsDisplay)->FunkyVideo(True); ((SVGA16*) graphicsDisplay)->FunkyVideo(True, duration); // duration -> the recovery envelope
} }
break; break;
+2 -9
View File
@@ -290,21 +290,14 @@ static void
// commanded lamp state (with flash phase) decides. // commanded lamp state (with flash phase) decides.
// //
int shade = 0; int shade = 0;
int lampState = 0;
if (lamp_capable) if (lamp_capable)
{ {
lampState = PadRIO::GetLampState(cell.address); shade = LampBrightnessOf(PadRIO::GetLampState(cell.address), tick);
shade = LampBrightnessOf(lampState, tick);
} }
if (held) if (held)
{ {
shade = 3; shade = 3;
} }
// A FLASHING radar/Sec button goes RED at its bright phase (L4VB16.h).
unsigned long radarBright = BTRadarLampBright(lampState);
COLORREF radarBrightRGB = RGB((radarBright >> 16) & 0xFF,
(radarBright >> 8) & 0xFF,
radarBright & 0xFF);
COLORREF fill; COLORREF fill;
COLORREF text_color; COLORREF text_color;
@@ -315,7 +308,7 @@ static void
} }
else if (yellow) else if (yellow)
{ {
fill = (shade == 3) ? radarBrightRGB fill = (shade == 3) ? RGB(245, 210, 60)
: (shade != 0) ? RGB(140, 118, 38) : (shade != 0) ? RGB(140, 118, 38)
: RGB(70, 60, 24); : RGB(70, 60, 24);
text_color = RGB(0, 0, 0); text_color = RGB(0, 0, 0);
-24
View File
@@ -1282,30 +1282,6 @@ int BTPadRIOActive(void)
int int
PadRIO::GetLampState(int unit) PadRIO::GetLampState(int unit)
{ {
//
// BENCH HOOK (2026-08-14): BT_LAMPTEST=<addr>[,<state>] forces ONE lamp's
// state, so the desktop lamp rendering can be checked without inducing the
// fault that would raise it (a generator trip, an ammo alarm, ...). The
// default state is the alert shape the Panic lamp uses -- flashFast +
// state1Off + state2Bright (0x33), a hard dark/bright pulse. Addresses and
// states take 0x hex. Read once; every desktop renderer reads its lamps
// through here, so one hook covers the surround, the exploded windows and
// the pad panel alike.
//
static int s_testAddr = -2;
static int s_testState = 0x33;
if (s_testAddr == -2)
{
s_testAddr = -1;
const char *e = getenv("BT_LAMPTEST");
if (e != NULL)
sscanf(e, "%i,%i", &s_testAddr, &s_testState);
}
if (s_testAddr >= 0 && unit == s_testAddr)
{
return s_testState;
}
if (activeInstance == NULL || unit < 0 || unit >= LampCount) if (activeInstance == NULL || unit < 0 || unit >= LampCount)
{ {
return 0; return 0;
+240 -23
View File
@@ -615,17 +615,41 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa
Word *source = pixelBuffer.Data.MapPointer; Word *source = pixelBuffer.Data.MapPointer;
Word *dest = (Word*)rect.pBits; Word *dest = (Word*)rect.pBits;
int postRowIncrement = (rect.Pitch / 2) - w; int postRowIncrement = (rect.Pitch / 2) - w;
// 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) if (monoTint < 0)
{ {
// PALETTE surface (sec/radar) -- palette-LUT expand (== SVGA16::Update case 0). // PALETTE surface (sec/radar) -- palette-LUT expand (== SVGA16::Update case 0).
SVGA16Palette *pal = &palette[paletteID]; SVGA16Palette *pal = &palette[paletteID];
for (int y = 0; y < h; y++) for (int y = 0; y < h; y++)
{ {
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++) for (int x = 0; x < w; x++)
{ {
PaletteTriplet *pe = &(pal->paletteData.Color[*source & 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 = ((pe->Red >> 3) << 11) | ((pe->Green >> 2) << 5) | (pe->Blue >> 3);
dest++; source++; dest++;
} }
dest += postRowIncrement; dest += postRowIncrement;
} }
@@ -637,10 +661,27 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa
Word tint = (Word) monoTint; Word tint = (Word) monoTint;
for (int y = 0; y < h; y++) for (int y = 0; y < h; y++)
{ {
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++) for (int x = 0; x < w; x++)
{ {
*dest = (*source & mask) ? tint : 0; Word px;
dest++; source++; 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; dest += postRowIncrement;
} }
@@ -734,16 +775,38 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate
Word *base = pixelBuffer.Data.MapPointer; Word *base = pixelBuffer.Data.MapPointer;
SVGA16Palette *pal = &palette[paletteID]; 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) if (rotateQuadrant == 0)
{ {
// Native orientation, top-down straight copy. // Native orientation, top-down straight copy.
for (int y = 0; y < h; y++) 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; unsigned long *d = dst + y * w;
double rowScroll = roff + shr * (double)y;
for (int x = 0; x < w; x++) for (int x = 0; x < w; x++)
{ {
Word s = src[x]; 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) if (monoTint < 0)
{ {
PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]); PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]);
@@ -791,6 +854,8 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate
// 90-degree rotation: output is transposed (ow = h, oh = w). rotate 3 = CW, // 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). // 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; int ow = h, oh = w;
for (int oy = 0; oy < oh; oy++) for (int oy = 0; oy < oh; oy++)
{ {
@@ -808,7 +873,21 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate
sx = w - 1 - oy; sx = w - 1 - oy;
sy = ox; sy = ox;
} }
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) if (monoTint < 0)
{ {
PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]); PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]);
@@ -1028,17 +1107,12 @@ static void CkFill(LPDIRECT3DDEVICE9 dev, int x, int y, int w, int h, D3DCOLOR c
} }
// Lamp fill + border for a button, from the glass PaintGlass color ramps. // Lamp fill + border for a button, from the glass PaintGlass color ramps.
static void CkLampColors(int colorClass, int shade, int inert, D3DCOLOR *fill, D3DCOLOR *border, static void CkLampColors(int colorClass, int shade, int inert, D3DCOLOR *fill, D3DCOLOR *border)
int lampState = 0)
{ {
if (colorClass == 1) // yellow (radar) -- RED while flashing (see L4VB16.h) if (colorClass == 1) // yellow (radar)
{ {
unsigned long bright = BTRadarLampBright(lampState); *border = D3DCOLOR_XRGB(245, 210, 60);
D3DCOLOR brightC = D3DCOLOR_XRGB((int)((bright >> 16) & 0xFF), *fill = (shade == 3) ? D3DCOLOR_XRGB(245, 210, 60)
(int)((bright >> 8) & 0xFF),
(int)( bright & 0xFF));
*border = brightC;
*fill = (shade == 3) ? brightC
: (shade != 0) ? D3DCOLOR_XRGB(140, 118, 38) : (shade != 0) ? D3DCOLOR_XRGB(140, 118, 38)
: D3DCOLOR_XRGB(70, 60, 24); : D3DCOLOR_XRGB(70, 60, 24);
} }
@@ -1223,7 +1297,7 @@ void BTDrawCockpitPanels(LPDIRECT3DDEVICE9 device)
int shade = BTLampBrightnessOf(state, tick); int shade = BTLampBrightnessOf(state, tick);
if (shade < 2) continue; // dark half-cycle: no ring if (shade < 2) continue; // dark half-cycle: no ring
D3DCOLOR fill, border; D3DCOLOR fill, border;
CkLampColors(b.colorClass, 3 /*bright*/, b.inert, &fill, &border, state); CkLampColors(b.colorClass, 3 /*bright*/, b.inert, &fill, &border);
int rx = b.x - kRingGap - kRingWidth; int rx = b.x - kRingGap - kRingWidth;
int ry = b.y - kRingGap - kRingWidth; int ry = b.y - kRingGap - kRingWidth;
int rw = b.w + 2 * (kRingGap + kRingWidth); int rw = b.w + 2 * (kRingGap + kRingWidth);
@@ -1244,7 +1318,7 @@ void BTDrawCockpitPanels(LPDIRECT3DDEVICE9 device)
int shade = BTLampBrightnessOf(state, tick); int shade = BTLampBrightnessOf(state, tick);
if (gCkPressed == b.address || gCkLatched[b.address & 0x7F]) shade = 3; if (gCkPressed == b.address || gCkLatched[b.address & 0x7F]) shade = 3;
D3DCOLOR fill, border; D3DCOLOR fill, border;
CkLampColors(b.colorClass, shade, b.inert, &fill, &border, state); CkLampColors(b.colorClass, shade, b.inert, &fill, &border);
CkFill(device, b.x, b.y, b.w, b.h, border); CkFill(device, b.x, b.y, b.w, b.h, border);
CkFill(device, b.x + 1, b.y + 1, b.w - 2, b.h - 2, fill); CkFill(device, b.x + 1, b.y + 1, b.w - 2, b.h - 2, fill);
@@ -5549,6 +5623,9 @@ SVGA16::SVGA16(
for (int _i = 0; _i < 10; _i++) // DEV-COMPOSITE: lazily created on first surface draw for (int _i = 0; _i < 10; _i++) // DEV-COMPOSITE: lazily created on first surface draw
mDevSurfaceTex[_i] = NULL; mDevSurfaceTex[_i] = NULL;
paletteGeneration = 0; // GLASS dirty-skip palette tracking paletteGeneration = 0; // GLASS dirty-skip palette tracking
scrambleActive = False; // PPC sync-scramble (phase-14) -- armed by FunkyVideo
scrambleStartMs = 0;
scrambleDurationS = 0.0;
//STUBBED: VIDEO RB 1/15/07 //STUBBED: VIDEO RB 1/15/07
# if defined(DEBUG) # if defined(DEBUG)
Tell("SVGA16::SVGA16()\n"); Tell("SVGA16::SVGA16()\n");
@@ -6508,13 +6585,153 @@ void
Check_Fpu(); Check_Fpu();
} }
//
// 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 void
SVGA16::FunkyVideo(Logical on_off) SVGA16::FunkyVideo(Logical on_off, Scalar duration)
{ {
//STUBBED: VIDEO RB 1/15/07 if (on_off)
//Check(this); {
//SVGAFunkyVideo(on_off); scrambleActive = True;
//Check_Fpu(); 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.
}
//
// 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.
//
Logical
SVGA16::ScrambleParams(int width, double *scale, double *shear,
double *rollOff, int *centerX, int *shakeRow) const
{
*shakeRow = 0;
if (width <= 0)
return False;
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)
{
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 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;
} }
//######################################################################## //########################################################################
+21 -41
View File
@@ -110,46 +110,6 @@ inline int BTLampBrightnessOf(int state, unsigned long tick)
return ((tick / half) & 1) ? level2 : level1; return ((tick / half) & 1) ? level2 : level1;
} }
//
// Is this lamp FLASHING rather than solid? Bits 0-1 of the wire word are the
// flash mode (0 = solid, 1/2/3 = slow/med/fast), so any non-zero value means
// the lamp is pulsing -- which is the pod's own way of saying ALERT.
//
// RADAR FLASH = RED (2026-08-14, player request; GLASS-ONLY DEVIATION). The
// radar's buttons are the yellow class, and a yellow lamp pulsing among other
// yellow lamps is easy to miss -- the night-16 miss that also produced the
// #172 ring and the protrusion bonus. The desktop renderers therefore swap
// the BRIGHT colour of a FLASHING radar button to the red family, so an alert
// reads as a colour change and not just a brightness change. The pod's own
// buttons are physical bicolour-less lamps and cannot do this, so it is a
// desktop affordance only -- and it is confined to the three desktop
// renderers (surround, exploded windows, pad panel); the serial RIO lamp
// path is untouched. BT_RADAR_FLASH_RED=0 restores the yellow bright.
//
inline int BTLampIsFlashing(int state)
{
return (state & 0x3) != 0;
}
//
// The bright colour a radar (yellow-class) button should use right now: the
// red family while the lamp is flashing, its own yellow otherwise. ONE
// definition, consumed by all three desktop renderers so they cannot drift
// (the lesson of the L4RIOBANK geometry split). Returns 0x00RRGGBB.
//
inline unsigned long BTRadarLampBright(int lampState)
{
static int enabled = -1;
if (enabled < 0)
{
const char *e = getenv("BT_RADAR_FLASH_RED");
enabled = (e != 0 && e[0] == '0') ? 0 : 1; // default ON
}
return (enabled && BTLampIsFlashing(lampState))
? 0x00E64646ul // 230,70,70 -- the house red (MFD family)
: 0x00F5D23Cul; // 245,210,60 -- the radar's own yellow
}
//######################################################################## //########################################################################
//######################### Video16BitBuffered ########################### //######################### Video16BitBuffered ###########################
//######################################################################## //########################################################################
@@ -372,7 +332,27 @@ public:
UnflashPalette(int palette_number); UnflashPalette(int palette_number);
void 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, 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
ScrambleParams(int width, double *scale, double *shear,
double *rollOff, int *centerX, int *shakeRow) const;
Logical scrambleActive;
unsigned long scrambleStartMs;
double scrambleDurationS;
protected: protected:
-8
View File
@@ -394,14 +394,6 @@ static const char *kEnvironIniDefault =
"# Phosphor colour of the mono MFDs, RRGGBB. Default is a green tube.\n" "# Phosphor colour of the mono MFDs, RRGGBB. Default is a green tube.\n"
"#BT_COCKPIT_TINT=21FF42\n" "#BT_COCKPIT_TINT=21FF42\n"
"\n" "\n"
"# A FLASHING button on the radar turns RED instead of yellow. The radar's\n"
"# buttons are all yellow, so a lamp calling for attention used to change only\n"
"# in BRIGHTNESS -- easy to miss with eleven other yellow buttons around it.\n"
"# Now the bright half of its flash is red, so an alert changes COLOUR. Only\n"
"# a flashing button is affected: a radar button doing nothing looks exactly as\n"
"# it always did. 1 = on (the default), 0 = keep the yellow.\n"
"#BT_RADAR_FLASH_RED=0\n"
"\n"
"# ---- Display ----------------------------------------------------------------\n" "# ---- Display ----------------------------------------------------------------\n"
"\n" "\n"
"# The cockpit is fitted into the window at ONE uniform scale, centred, with\n" "# The cockpit is fitted into the window at ONE uniform scale, centred, with\n"
+26
View File
@@ -82,6 +82,7 @@
// The torso twist is reached via a BRIDGE (BTGetTorsoTwist, defined in torso.cpp) // The torso twist is reached via a BRIDGE (BTGetTorsoTwist, defined in torso.cpp)
// for the same reason. // for the same reason.
#include "dmgtable.hpp" #include "dmgtable.hpp"
#include <GAUGREND.hpp> // GaugeRenderer::SpecialEffect (virtual) -- PPC scramble trigger
extern Scalar BTGetTorsoTwist(Subsystem *torso); // torso.cpp (Torso complete there) extern Scalar BTGetTorsoTwist(Subsystem *torso); // torso.cpp (Torso complete there)
// heat-bank ambient bridge (heatfamily_reslice.cpp, AggregateHeatSink complete // heat-bank ambient bridge (heatfamily_reslice.cpp, AggregateHeatSink complete
// there) -- mech.cpp cannot include the subsystem headers (local-stub collision) // there) -- mech.cpp cannot include the subsystem headers (local-stub collision)
@@ -1198,6 +1199,31 @@ void
} }
} }
reportZone = message->damageZone; // post-resolve (@0x4a0396 write) reportZone = message->damageZone; // post-resolve (@0x4a0396 write)
//
// @0x4a03f3 [T1] -- PPC/ERPPC SECONDARY-DISPLAY SCRAMBLE. EnergyDamageType
// (==4) is authored on exactly the 14 PPC/ERPPC subsystem records and nothing
// else, so this branch is structurally PPC-exclusive -- NO explicit weapon
// class check (the data authorship IS the gate). It sits OUTSIDE the burst
// loop below, so it fires ONCE per damage message, not per burst. The
// duration is DERIVED from the type ordinal, not a literal: (Scalar)4 * 0.2 ==
// 0.8s (binary loads long double 0.2 @0x4a0c08, fmulp). SpecialEffect ->
// L4GaugeRenderer scrambles every SECONDARY cockpit display (SVGA16 sync
// detune, FunkyVideo) for that window; the main 3D view is on an independent
// timing chain and is left clean. Spec: phases/phase-14-ppc-sync-distortion.md.
//
if (message->damageData.damageType == Damage::EnergyDamageType)
{
GaugeRenderer *gauges =
(application != 0) ? application->GetGaugeRenderer() : 0; // APP.h:355 -- named, not application+0x4c
if (gauges != 0) // the binary null-guards the renderer too (@0x4a0405)
gauges->SpecialEffect(GaugeRenderer::scrambleVideo,
(Scalar)message->damageData.damageType * 0.2f);
if (BTEnvOn("BT_DMG_LOG", 0))
DEBUG_STREAM << "[ppc-scramble] EnergyDamageType hit -> scrambleVideo for "
<< ((Scalar)message->damageData.damageType * 0.2f) << "s\n" << std::flush;
}
// //
// #80 -- the faithful application loop (binary @0x4a0423-0x4a04d8), which // #80 -- the faithful application loop (binary @0x4a0423-0x4a04d8), which
// REPLACES the engine-base single application. Three things the base // REPLACES the engine-base single application. Three things the base
+252
View File
@@ -0,0 +1,252 @@
# 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 (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. 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
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"