Author SHA1 Message Date
Joe DiPrimaandClaude Opus 5 39144813a4 pod: kit carries PODTEST.EGG (it was never in the repo)
First real new-build drop on the cart exposed it: the kit pushed environ.ini
and glass_layout.cfg into the fresh 4.11.817 extract, but runpod.bat launches
`-egg PODTEST.EGG` and that mission only ever existed on the pod -- mkdist
ships git-TRACKED content only, so the new install had no mission to run.
Master copy now sits at C:\bt411\ with the other kit files and podkit.ps1
installs it.

Does not affect testers: play_solo/join/play_steam pick their own missions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rw7No5wLTpkaUgA3ANbtZZ
2026-08-06 19:20:23 -05:00
Joe DiPrimaandClaude Opus 5 d213c980a9 fix: MFD panels never appeared with the HARDWARE RIO (PadRIO coupling)
Nick ran the cab with the real board and the mission came up on the main view
with every MFD dark -- and not one [glasswin] line in the log.

BTGlassPanels_Create() was called only from the END OF THE PadRIO CONSTRUCTOR.
The panels began as the frames around the on-screen RIO button banks, so "the
buttons ride the device" was reasonable then; it is backwards on a real cab.
L4CONTROLS=RIO:COM1 means PadRIO is never constructed, so the MFD windows were
never created -- silently, since nothing is wrong from the display layer's
point of view.

The panels are a DISPLAY concern.  Creation moves to LBE4ControlsManager,
after the L4CONTROLS parse where every device branch converges, and a
symmetric BTGlassPanels_Destroy() goes in its destructor: ~PadRIO tore them
down, ~RIO knows nothing about them, and windows outliving the surfaces they
blit would crash on the next mission cycle.  Both calls are idempotent
(Create returns on gWinCount != 0), so the proven PadRIO path is unchanged
and simply arrives first.  Both guarded by BT_GLASS -- L4GLASSWIN is only in
the build when the gate is on.

Verified on the cart: hardware RIO up AND all three surfaces on their intended
displays in the same run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rw7No5wLTpkaUgA3ANbtZZ
2026-08-06 19:05:04 -05:00
Joe DiPrimaandClaude Opus 5 6fcff95010 pod: the HARDWARE RIO on COM1 (L4CONTROLS=RIO:COM1,KEYBOARD)
The real cockpit board now drives the cab instead of PadRIO, frozen in the
profile so all five tester launchers get it.  Nothing above the seam changed
-- the 109-mapping L4 control table installs exactly as on a desktop, and the
cab keeps the GLASS display stack.  BT_PLATFORM=pod is NOT the way to this;
that would drag in the 1995 gauge path.  RIO:COM1 -> \.\COM1 at 9600 8N1.

Evidence the link is real, not just "the port opened":
  RIO successfully initialized!
  FAILURE.LOG: 4 missing boards (Slot 3:0, 3:2, 4:0, 5:0), 16 dead lamps
  [ctrlmap] push stick x=0.0595238 y=0   <- physical stick outside deadband
A specific 4-of-many board inventory is the proof: a dead serial line reports
the WHOLE address space missing.  Reproduced after deleting FAILURE.LOG.  The
dead lamps are the boards this partial crash cart does not have.

Banner honesty: "GLASS (PadRIO ...)" was hardcoded, so a wired cab reported
PadRIO on the very line you read to check which device won.  It now names the
resolved one -- GLASS (hardware RIO; plasma off [L4PLASMA]).

Recorded in pod-hardware.md, including that RIO and PAD are mutually exclusive
(both assign rioPointer, last token wins) and that a CENTRED stick reads x=0,
which is indistinguishable from no data -- so the by-hand check of stick,
throttle, pedals, buttons and the Ranger calibration is still outstanding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rw7No5wLTpkaUgA3ANbtZZ
2026-08-06 18:11:28 -05:00
Joe DiPrimaandClaude Opus 5 05d8164801 pod: make the frozen rig survive a NEW BUILD, not just a reboot
Both frozen files live inside the versioned install and neither ships in the
zip -- environ.ini is generated on first run, glass_layout.cfg is ours.  So
Nick extracting the next build would get a cab that comes up wrong with no
error anywhere, which is exactly the failure the freeze was supposed to end.

Masters now live at the stable C:\bt411\ (podprofile.ini, glass_layout.cfg,
podkit.ps1).  setup_pod.bat pushes them into the newest BT411_* folder -- run
once per extract.  runpod.bat resolves the newest install and applies the kit
itself, so the remote launch path needs no per-build edit.  Re-tuning means
editing the MASTER: the apply overwrites the install's copy on purpose, so a
moved panel has one place to look.

Also records, for playtesting on the cab: all five tester launchers set no pod
key at all, so each inherits the rig from environ.ini without knowing the pod
exists -- verified by running play_solo.bat on the cart (7 settings applied,
not 9, because the bat sets BT_PLATFORM and BT_START_INSIDE itself and the
real environment wins).  That is the case against a separate pod-only ini: a
second file would need every launcher to opt in.

Verified: kit re-applies idempotently, and the rewritten runpod.bat brings the
cab up correct -- 9 settings, GLASS, -fit borderless, all three surfaces on
their intended displays.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rw7No5wLTpkaUgA3ANbtZZ
2026-08-06 17:59:57 -05:00
Joe DiPrimaandClaude Opus 5 d96fa4f4e2 pod: BT_GLASS=1 is not a gate -- name the real one (BT_PLATFORM=glass)
BT_GLASS is a compile-time #ifdef; getenv("BT_GLASS") appears nowhere in the
tree.  The line rode along from the bring-up launcher into the frozen profile
and into the pod-hardware runbook, reading like the switch that turns the
glass path on.  It never did anything -- the rig worked because glass is the
DEFAULT profile when nothing is set.

Replaced with BT_PLATFORM=glass (the real spelling, so the cart does not lean
on that default) and noted why NOT BT_PLATFORM=pod: the pod profile selects
the 1995 multi-surface gauge path, which needs the NVIDIA horizontal span no
modern driver has.  Behaviourally identical -- both land gBTPlatformGlass=1.

Re-verified on the cart: 9 settings applied, GLASS profile, -fit borderless,
all three surfaces on their intended displays.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rw7No5wLTpkaUgA3ANbtZZ
2026-08-06 17:44:07 -05:00
Joe DiPrimaandClaude Opus 5 654277bb7b pod: freeze the ALPHA-MR rig in environ.ini (BT_FIT, L4PLASMA=NONE)
The cart's display config was carried by a launcher .bat, so it only came up
right if the game was started one particular way.  Move it into the file the
engine already reads before anything touches the environment.

  content/environ.ini  <- scratchpad/pod/podprofile.ini, merged idempotently
                          between markers by scratchpad/pod/mergeprofile.ps1

Two gates were missing for that to be enough:

  BT_FIT=1      the env spelling of -fit, so the borderless main view does not
                depend on one launcher's command line (shortcut, scheduled
                task and autostart all have to produce the same rig)
  L4PLASMA=     NONE / OFF / 0 -> no marquee at all.  Leaving it unset does
                NOT work: the GLASS profile force-defaults it to SCREEN, which
                drops a desktop plasma window on the cab's glass.  The boot
                banner now reports the live state instead of always claiming
                "plasma window".

Also lands the bring-up engine work this depended on: monitor:<name|index>
layout binding (device-bound, not pixel-bound -- desktop rects move when a
display re-enumerates), ",bare" implying frameless, rotation-aware radar
surface sizing, BT_GAUGE_SEC_ROT accepting 0-3 (it silently forced 3 for
anything but 1), 180-degree ExpandPlaneToBGRA, and the BT_POD_CHANMAP /
BT_POD_IDENT / BT_POD_CHANTEST identification gates.

Verified on the cart, build 4.11.813, launcher carrying none of it:
  [boot] environ.ini: 9 setting(s) applied
  [boot] platform profile: GLASS (PadRIO; plasma off [L4PLASMA])
  [cockpit] -fit: borderless 800x600
  [glasswin] radar rotation 0 (none)
  ... all three surfaces on their intended \.\DISPLAYn
No [plasmawin] line in an otherwise-logging run = the ctor never ran.

KB: pod-hardware.md gains the ALPHA-MR section (mapping, the two wiring
deviations, the frozen profile, the session-0 remote-work traps) and its RGB
SPLIT "OPEN: which way is the cart wired" is now SETTLED -- it is splitter
wired, the composite is what lit it.  glass-cockpit.md documents monitor:
binding, BT_FIT and L4PLASMA=NONE.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rw7No5wLTpkaUgA3ANbtZZ
2026-08-06 17:40:39 -05:00
24 changed files with 1050 additions and 664 deletions
-10
View File
@@ -637,16 +637,6 @@ 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
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]
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
-84
View File
@@ -13,7 +13,6 @@ 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"
- "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)"
- "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
@@ -756,89 +755,6 @@ pooling fix) — an alarm that cannot acquire a source is silent.
⚠ 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.
## 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
- Full history: `docs/GAUGE_COMPOSITE.md`; reticle recovery: `phases/phase-02-dpl2d-reticle.md`.
- Uses: [[attribute-pointer]] + [[reconstruction-gotchas]]; reads [[subsystems]] state.
+33 -1
View File
@@ -156,6 +156,26 @@ want bare. From then on those windows are frameless **and pinned** — with no c
nothing to drag them by, which is the point: the arrangement is finished. Delete the flag to get
the frame back.
### `monitor:<name|index>` — bind a window to a DISPLAY, not to pixels (2026-08-06) [T2 live]
Instead of an `x,y,w,h` rect, a line may name a display device:
```
Secondary / Radar=monitor:DISPLAY4,bare
VGA Port A=monitor:DISPLAY2,bare
```
The resolver (`ResolveMonitorSpec` + `MonScanProc`, `L4GLASSWIN`) matches the full device name OR
its tail, so `monitor:DISPLAY4` works without typing `\\.\`; a bare integer is a 0-based index in
enumeration order. The surface is CENTRED on that monitor. This is what makes a fixed rig
survivable: **desktop rects move** when a display is re-detected, a USB graphics adapter
re-enumerates or someone changes a resolution, and a pixel rect then silently puts a panel on the
wrong glass — a device binding does not. Required for [[pod-hardware]] §ALPHA-MR, where a
mis-bound window means a picture on the wrong CRT with no error anywhere.
`,bare` **implies frameless** (it used to need `,noframe` too, and a "bare" window came back
656×519 WITH a caption — a real bug found on the cab).
Options are comma-separated after the numbers and **unknown ones are ignored**, so an older build
reading a newer file loses the option but never the line — the `bindings.txt` grammar rule
applied here.
@@ -183,6 +203,15 @@ appears in the cfg like any panel and honours `,noframe`. Verified: a drag wrote
at 321,222. NB the plasma window blits directly every frame (`GetDC`+`StretchDIBits`), so unlike
the panels it has no `WM_TIMER` focus-throttle to worry about.
**Turning it OFF: `L4PLASMA=NONE` (also `OFF`/`0`, 2026-08-06) [T2].** `L4GREND` creates a
marquee whenever `L4PLASMA` is set at all (`SCREEN` → the desktop window, anything else → a real
`PlasmaDisplay` on that serial port), and the GLASS profile force-defaults it to `SCREEN` — so on
a rig with no marquee wired, leaving the variable unset is NOT enough; the profile fills it back
in and a plasma window lands on the cab's glass. The `NONE`/`OFF`/`0` spelling nulls the string
before the branch, so no external display is constructed at all. The boot banner reads
`GLASS (PadRIO; plasma off [L4PLASMA])` when it takes, and the absence of the
`[plasmawin] desktop plasma display up` line is the runtime proof.
**Verified 2026-07-26 [T2]:** `BT_RIOBANK_LOG=1` dumps every bank's rects;
`scratchpad/checkbank.py` reports the per-bank census and proves no address is SHADOWED (has a
point no earlier button covers); `scratchpad/clickbank.py` then posts a real click at every
@@ -214,7 +243,10 @@ window.
- **World aspect** under the letterbox is the view rect's OWN aspect — the client no longer
enters into it.
- **`-fit`** (alias `-windowed-fullscreen`): borderless `WS_POPUP` over the monitor, canvas
letterboxed inside. Verified on a 3440×1440 ultrawide.
letterboxed inside. Verified on a 3440×1440 ultrawide. **`BT_FIT=1` is the env spelling**
(2026-08-06) — same flag, settable from `environ.ini`, so a fixed rig keeps its borderless main
view however it is launched instead of depending on one launcher's command line. Targets
`MONITOR_DEFAULTTOPRIMARY`, i.e. the PRIMARY display. [[pod-hardware]] §ALPHA-MR
**Ordering trap found live:** the letterbox flag was first set at device creation, but the
first `WM_SIZE` arrives BEFORE the device exists — a `-fit` boot logged `aspect=3.14` (the
+135 -7
View File
@@ -217,12 +217,21 @@ colour channel of one shared palettized framebuffer. Mechanism, end to end:
mono-tinted window on its own Windows display (see §MFD PANELS ON REAL HARDWARE), which is right
when every panel has its own output. **On splitter-wired glass it is wrong** — three monitors
would share one Windows display and each would show only its channel's share of a single tinted
image. Driving original splitter hardware needs a CHANNEL-COMPOSITE mode: extract three planes
into ONE 640x480 RGB image with pure (255,0,0)/(0,255,0)/(0,0,255) tints, one window per VGA
output. `SVGA16::ExpandPlaneToBGRA` already does the per-plane extraction with a tint, so the
composite is a small addition. OPEN: which way Nick's crash cart is wired (Windows shows three
separate 640x480 displays there, which suggests per-panel outputs via the Trigger 6 USB adapter,
not a splitter) — settle it by eye before building.
image.
**`BT_POD_RGB=1` — the CHANNEL-COMPOSITE mode (built + field-verified 2026-08-06) [T2].** It
collapses the six panel windows into **three**: two VGA-port windows plus the radar. Each port
window composites its group's planes into ONE 640×480 BGRA image, each member ORed into its own
channel mask (`0x00FF0000` / `0x0000FF00` / `0x000000FF`), so the analog splitter downstream hands
each mono CRT exactly its picture. Grouping follows `L4GAUGE.CFG`: *VGA Port A* = Heat + Comm +
Mfd2, *VGA Port B* = Mfd1 + Mfd3; the radar keeps real colour on its own port.
`SVGA16::ExpandPlaneToBGRA` does the per-plane extraction, and `BT_POD_RGB` implies
`BT_POD_SURFACES`. Two companion gates came out of the bring-up: **`BT_POD_CHANMAP=<Port>=<colour>,…`**
remaps a surface's colour line when a cab's harness disagrees with the 1995 config (it did — see
§ALPHA-MR), and **`BT_POD_IDENT`** / **`BT_POD_CHANTEST`** paint identifying/cycling test patterns
so you can tell which physical CRT is which without trusting the desktop layout.
**SETTLED:** Nick's cart IS splitter-wired — its three 640×480 "displays" are VGA outputs, each
feeding a three-monitor splitter, and the composite is what lit the glass correctly.
## MFD PANELS ON REAL HARDWARE — the bring-up path (2026-08-06) [T2 local / T4 on-pod]
Nick's crash cart (pod hardware + Chrome Remote Desktop on a burner account) is the first chance
@@ -249,7 +258,7 @@ desktops. Two pieces were added for the cab:
1. Probe: GPUs, every monitor's virtual-desktop rect, EDID make/model (identifies the original
panels), serial ports (the RIO board), session type, and a PROPOSED `glass_layout.cfg` that
assigns the six surfaces to the non-primary monitors top-to-bottom/left-to-right, centred.
2. Drop the cfg in the game's working directory; run with `BT_GLASS=1 BT_GLASS_PANELS=1
2. Drop the cfg in the game's working directory; run with `BT_GLASS_PANELS=1
BT_POD_SURFACES=1 BT_GLASS_LAYOUT=load` (`=save` to persist drags instead).
3. Read the receipts; re-assign titles to monitors in the cfg until each picture is on its panel.
Surface→panel roles: Heat MFD = upper left (coolant), Engineering = upper centre, Comm MFD =
@@ -262,6 +271,125 @@ at native size CENTRED, not scaled — a scale-to-fit option is the obvious foll
Chrome Remote Desktop holds the CONSOLE session (it should — an RDP session would get a virtual
display and light nothing); and the RIO serial input, which is a separate task from the displays.
## ALPHA-MR — the verified cart config, FROZEN (2026-08-06) [T2, field-verified by eye]
The bring-up above landed: real pictures on real pod glass, then user-confirmed correct
("ok this is correct"). What that took, and where it now lives permanently.
**The rig.** Win10 IoT Enterprise LTSC on the cart; Intel HD630 + a "Trigger 6 External Graphics"
USB adapter. Four console displays: **DISPLAY3 800×600 PRIMARY at 0,0** (main view), plus
**DISPLAY4 @800,122**, **DISPLAY2 @1440,123**, **DISPLAY1 @2080,124** (each 640×480). Physical
cab: five mono MFD CRTs (two left stacked, two right stacked, one centre loose on the desk) plus
the colour radar LCD in the yellow-button frame.
**The mapping** (`glass_layout.cfg`, `monitor:<name>` binding — see [[glass-cockpit]]):
```
Secondary / Radar = monitor:DISPLAY4,bare # colour LCD, yellow-button frame
VGA Port A = monitor:DISPLAY2,bare # RGB triple: Heat + Comm + Mfd2
VGA Port B = monitor:DISPLAY1,bare # RGB triple: Mfd1 + Mfd3
```
Three windows, not six — under `BT_POD_RGB` each VGA-port window carries three MFDs on its three
colour planes (§THE RGB SPLIT). Desktop-coordinate order says NOTHING about physical cab position:
the first assignment, made by sorting monitors left-to-right, was wrong. Only eyes on the glass
settle it.
**Two deviations from the 1995 config, both real, both this cab's wiring:**
- **`BT_POD_CHANMAP=Comm=blue,Heat=red`** — kills/deaths and coolant loops came up on each
other's panel. `L4GAUGE.CFG` has Comm on red and Heat on blue; THIS cab's splitter harness
wires them the other way. The env override remaps a port's colour channel without touching the
authentic config file. [T2]
- **`BT_GAUGE_SEC_ROT=0`** — the radar here is a LANDSCAPE LCD, so the 640×480 source needs no
rotation. The 1995 pod's 270° rotation existed because its secondary CRT was mounted PORTRAIT;
it is a mounting fact, not a format fact. (En route: 180° looked plausible and was upside down.)
**Frozen in `content/environ.ini`**, not in a launcher — `scratchpad/pod/podprofile.ini` is the
block, `scratchpad/pod/mergeprofile.ps1` merges it idempotently between markers. environ.ini is
read before anything touches the environment ([[build-and-run]]), and the real environment still
WINS, so a .bat can override any of it for a one-off. The cart's `runpod.bat` now sets only
`BT_LOG`/`BT_GLASS_LOG` and the `-egg`; `runpod_env.bat` keeps the all-inline version as the
fallback. Two gates were added for this:
- **`BT_FIT=1`** — the env spelling of `-fit`, so the borderless main view survives however the
game is started (shortcut, scheduled task, autostart) instead of riding on one launcher's
command line. It fits `MONITOR_DEFAULTTOPRIMARY` = DISPLAY3, which is the main-view panel.
- **`L4PLASMA=NONE`** (also `OFF`/`0`) — no marquee at all. Needed because the GLASS profile
force-defaults `L4PLASMA=SCREEN`, so simply leaving it unset still puts a desktop plasma window
on the pod's glass. The boot banner reports `plasma off [L4PLASMA]` when it takes.
Verified on build 4.11.813 with the launcher carrying none of it: `[boot] environ.ini: 9
setting(s) applied`, `[cockpit] -fit: borderless 800x600`, `[glasswin] radar rotation 0 (none)`,
and all three surfaces on their intended `\\.\DISPLAYn`.
**Surviving a NEW BUILD, not just a reboot.** Both frozen files live INSIDE the versioned install
(`content\environ.ini`, `content\glass_layout.cfg`) and NEITHER ships in the zip — environ.ini is
generated on first run. So extracting a new build gives a cab that comes up wrong with no error
anywhere. The masters therefore live at the stable `C:\bt411\` (`podprofile.ini`,
`glass_layout.cfg`, `podkit.ps1`), and **`setup_pod.bat` pushes them into the newest `BT411_*`
folder** — run it once after each extract. `runpod.bat` resolves the newest install and applies
the kit itself before launching, so the remote path needs no hand-editing per build. Re-tuning the
cab means editing the MASTER at `C:\bt411\`, not the install's copy: the apply overwrites it, on
purpose, so there is one place to look when a panel moves. Repo copies: `scratchpad/pod/`.
**The tester launchers work on the cab unchanged (verified 2026-08-06) [T2].** `play_solo.bat`,
`join.bat`, `join_lan.bat`, `play_steam.bat` and `joyconfig.bat` set `BT_PLATFORM=glass`,
`BT_START_INSIDE=1`, `BT_DEV_GAUGES=1` plus their own mode flag — and **not one pod key** between
them. So each inherits the whole rig from environ.ini without knowing the pod exists. This is the
argument against a separate pod-only settings file: a second file would need every launcher to opt
in, which is the shape of the "competing keymaps" bug that made glass the default profile.
Observed: `play_solo.bat` on the cart logs `environ.ini: 7 setting(s) applied` — 7 not 9, because
the bat sets `BT_PLATFORM` and `BT_START_INSIDE` itself and the real environment wins.
Steam MP needs the **client installed and logged in** (AppID 480/Spacewar, so any account, no
purchase); installed on the cart 2026-08-06. ⚠ **#68: a failed JOIN/HOST exits the process
SILENTLY** — "Steam not running" and a real crash look identical to a player.
### The HARDWARE RIO is live on the cart (2026-08-06) [T2]
`L4CONTROLS=RIO:COM1,KEYBOARD` in the frozen profile — the real cockpit board instead of PadRIO.
The whole stock path above the seam (mapper, lamps, streamed `.CTL` mappings) runs unchanged; the
109-mapping `L4` control table installs exactly as it does on a desktop. **This is independent of
the platform profile** — the cab keeps the GLASS display stack (panels + RGB split) and only the
input device changes, which is what the glass/PadRIO seam was built for. Do NOT reach for
`BT_PLATFORM=pod` to get it.
- **Wiring:** `RIO:COMn``\\.\COMn` at **9600 8N1** (`L4SERIAL`); bare `RIO` defaults to COM1.
Reads are non-blocking (`ReadIntervalTimeout=MAXDWORD`, totals 0), so an absent or dead board
cannot hang the game — it just goes quiet.
- **RIO and PAD are MUTUALLY EXCLUSIVE** — both assign `rioPointer` in the `L4CONTROLS` parser, so
the last token wins. Selecting the hardware RIO turns the XInput pad OFF on that machine.
`KEYBOARD` is safe alongside: it only sets `flags.keyboardExists` and never touches
`primaryControlType`.
- **The board's own self-test is the diagnostic.** `RIO::RIO` runs it (`perform_tests = True` by
default) and writes `content\FAILURE.LOG` pages `RIOBoardErrors` / `RIODeadLamps` / `RIOErrors`,
deleting them first so the file is always current. On the cart, reproducibly: **4 missing boards**
(`Slot=3:Address=0`, `3:2`, `4:0`, `5:0`) and **16 dead lamps** (`AuxUpperRight1-8`,
`AuxUpperCenter1`, `TeslaRelay1/3`, `PanicButton`, `IcomAmpEnableRelay`, `IcomIncRelay`,
`FloorEntry`, `undefined_0x17`) — the crash cart is a PARTIAL cab, and those are the boards it
does not have. **A specific 4-of-many inventory is itself the proof the link is good**: a dead
serial line reports the whole address space missing, not four slots.
- **Input reaching the mech** was seen live as `[ctrlmap] push stick x=0.0595238 y=0` with nobody
at the cab — the physical stick sitting just outside the 5% deadband. NB when the stick is
centred this reads `x=0`, which is indistinguishable from no data: a centred-stick zero proves
nothing either way. ⚠ **Not yet checked by hand:** that stick/throttle/pedal travel and the
physical buttons drive the mech correctly, and whether the cab's pots match the `Ranger`
calibration baked into `RIO::RIO` (`JoystickX -96..96`, `JoystickY -97..108`, `Throttle 0..800`,
pedals `0..470`). A mismatch shows up as drift or short travel, not as an error.
- The boot banner names the resolved device — `GLASS (hardware RIO; plasma off [L4PLASMA])`. It
used to hardcode "PadRIO", which is a lie on a wired cab and exactly the line you read to check.
-**THE COUPLING BUG this exposed (fixed 2026-08-06) [T2].** `BTGlassPanels_Create()` was called
ONLY from the end of the **PadRIO constructor** — the panels began life as the frames around the
on-screen RIO button banks, so "the buttons ride the device". Selecting the hardware RIO means
PadRIO is never constructed, so **every MFD window silently failed to appear**: the mission came
up on the main view with all the pod glass dark, and not one `[glasswin]` line in the log. The
MFD panels are a DISPLAY concern, so creation moved to `LBE4ControlsManager` after the
`L4CONTROLS` parse, where every device branch converges — plus a symmetric `BTGlassPanels_Destroy()`
in its destructor, because `~PadRIO` tore them down and `~RIO` knows nothing about them (windows
outliving the surfaces they blit would crash on the next mission cycle). Both calls are
idempotent (`Create` returns on `gWinCount != 0`), so the PadRIO path is unchanged and simply
arrives first.
**Working on the cart, remotely.** SSH over Tailscale lands in **session 0**, which has a dummy
"WinDisc" display and CANNOT see or enumerate session 1's windows — GUI work must go through
`schtasks /run /tn BT411Run` (task registered `/IT`), and window enumeration over SSH silently
returns nothing rather than failing. Kill `btl4.exe` BEFORE scp'ing a new exe or the file is
locked. The receipts in `podrun.log` are the remote eyes; a missing `DEBUG_STREAM` line in an
otherwise-logging run is real evidence that code path did not execute.
## The 1995 player manual — alignment audit (2026-07-18) [T1, primary source]
`reference/manual/Tesla40_BT_manual.pdf` (34pp, from Nick). CONFIRMS the reconstruction on
every checked control behavior:
+38
View File
@@ -9,6 +9,10 @@
#include "dxutils.h"
#ifdef BT_GLASS
#include "l4padrio.h"
#ifdef BT_GLASS
#include "l4glasswin.h" // the per-display MFD windows -- created below for
// EITHER RIO (they used to ride the PadRIO ctor)
#endif
#endif
//
@@ -865,6 +869,29 @@ LBE4ControlsManager::LBE4ControlsManager():
while (temp[0] != '\0');
}
//--------------------------------------------------
// The per-display MFD windows (2026-08-06)
//--------------------------------------------------
// These used to be created ONLY at the end of the PadRIO constructor,
// because they began life as the frames around the on-screen RIO button
// banks -- "the buttons ride the device". On a real cab that coupling is
// backwards: selecting the HARDWARE RIO means PadRIO is never built, so
// the MFD panels never appeared at all. Found on the crash cart, where
// the mission came up on the main view with every MFD dark.
//
// The windows are a DISPLAY concern, so create them here -- after the
// L4CONTROLS parse, where every device branch converges -- and let them
// exist whichever RIO won. In pod surface mode there are no on-screen
// buttons on them at all. Create() is idempotent (`gWinCount != 0`
// returns), so the PadRIO call site stays valid and simply arrives first.
//
#ifdef BT_GLASS
if (rioPointer != NULL && BTGlassPanelsActive())
{
BTGlassPanels_Create();
}
#endif
//--------------------------------------------------
// Clear all lamps (and reset analog I/O)
//--------------------------------------------------
@@ -1089,6 +1116,17 @@ LBE4ControlsManager::~LBE4ControlsManager()
rioPointer = NULL;
}
#ifdef BT_GLASS
//
// Symmetric with the Create above (2026-08-06). ~PadRIO tears the panels
// down itself, so this is a no-op there -- but the HARDWARE RIO's dtor
// knows nothing about them, and windows that outlive the surfaces they
// blit are a crash waiting for the next mission cycle. Destroy is safe
// if they were never created.
//
BTGlassPanels_Destroy();
#endif
Check_Fpu();
}
+382 -12
View File
@@ -123,6 +123,10 @@ struct GWin
const char *groupPort[3];
const char *groupAlt[3]; // the Eng<n> twin, or NULL
int groupCount;
char monitorName[40]; // the PHYSICAL monitor this window landed on
// (\.\DISPLAYn), stamped at creation; shown
// by BT_POD_IDENT so the cab can be mapped.
};
static GWin gWins[8];
@@ -253,17 +257,26 @@ static void
// (0x15 sits on the left column -- Cyd listed 0x10-0x14; move it if wanted.)
static const int bottomAddrs[4] = { 0x16, 0x17, 0x1F, 0x1E };
// The SURFACE SIZE follows the rotation: a quarter turn (1 or 3) transposes
// the 640x480 source to 480x640 portrait -- the pod's rotated CRT -- while
// 0 (none) and 2 (180) keep it LANDSCAPE. Sizing this independently of the
// rotation squashed the picture into the wrong aspect on a landscape
// replacement panel (crash cart, 2026-08-06).
const int quarterTurn = (gRadarRot == 1 || gRadarRot == 3);
const int surfW = quarterTurn ? RadarSurfW : RadarSurfH; // 480 : 640
const int surfH = quarterTurn ? RadarSurfH : RadarSurfW; // 640 : 480
BTRioBankMetrics metrics;
BTRioBankMetricsFor(RadarSurfW, RadarSurfH, &metrics);
BTRioBankMetricsFor(surfW, surfH, &metrics);
BTRioBank bank;
BTRioBankLayout(BTRioBankRadar, 0, 0, RadarSurfW, RadarSurfH,
BTRioBankLayout(BTRioBankRadar, 0, 0, surfW, surfH,
0x10, 0x18, &metrics, bottomAddrs, &bank);
BTRioBankDump("Secondary / Radar", &bank);
int dx = -bank.boundsX, dy = -bank.boundsY;
AdoptBank(w, bank, ClrYellow, dx, dy);
SetRect(&w.surfaceRect, dx, dy, dx + RadarSurfW, dy + RadarSurfH);
SetRect(&w.surfaceRect, dx, dy, dx + surfW, dy + surfH);
w.clientW = bank.boundsW;
w.clientH = bank.boundsH;
}
@@ -426,6 +439,116 @@ static const char *layoutFileName = "glass_layout.cfg";
// The Flight Controls window (no surface at all) is not created.
// BT_POD_SURFACES=1 -> every display window goes bare
// glass_layout.cfg "<title>=x,y,bare" -> just that one (mixed rigs)
// BT_POD_CHANMAP -- per-cart channel override, e.g.
// BT_POD_CHANMAP=Comm=blue,Heat=red
// The 1995 config authors which colour line each surface rides (L4GAUGE.CFG),
// but a rebuilt cab can have its RGB splitter wired to different panels than
// the original -- on Nick's cart the coolant (Heat) and hot-box (Comm) screens
// came out swapped, which is a WIRING difference, not a software bug. Rather
// than edit authentic config, override the channel here, per rig.
// Returns 0/1/2 (red/green/blue) for a named port, or -1 if unmapped.
static int
PodChannelOverride(const char *portName)
{
static int loaded = 0;
static char names[8][20];
static int chans[8];
static int count = 0;
if (!loaded)
{
loaded = 1;
const char *e = getenv("BT_POD_CHANMAP");
while (e != NULL && *e && count < 8)
{
while (*e == ' ' || *e == ',') ++e;
const char *eq = strchr(e, '=');
if (eq == NULL) break;
int n = 0;
for (const char *p = e; p < eq && n < 19; ++p) names[count][n++] = *p;
names[count][n] = 0;
const char *v = eq + 1;
int ch = -1;
if (_strnicmp(v, "red", 3) == 0) ch = L4GraphicsPort::RedChannel;
else if (_strnicmp(v, "green", 5) == 0) ch = L4GraphicsPort::GreenChannel;
else if (_strnicmp(v, "blue", 4) == 0) ch = L4GraphicsPort::BlueChannel;
if (ch >= 0)
{
chans[count] = ch;
DEBUG_STREAM << "[glasswin] channel override: " << names[count]
<< " -> " << (ch == L4GraphicsPort::RedChannel ? "red"
: ch == L4GraphicsPort::GreenChannel ? "green" : "blue")
<< std::endl << std::flush;
++count;
}
const char *nx = strchr(v, ',');
if (nx == NULL) break;
e = nx + 1;
}
}
for (int i = 0; i < count; ++i)
if (portName != NULL && _stricmp(names[i], portName) == 0)
return chans[i];
return -1;
}
// The channel a surface actually rides on THIS rig: the override if given,
// else what the pod config authored.
static int
ResolvedChannelOf(const char *portName, L4GraphicsPort *port)
{
int ov = PodChannelOverride(portName);
if (ov >= 0)
return ov;
return (port != NULL) ? (port->GetEnableID() & 0x7F) : -1;
}
// BT_POD_IDENT=1 -- draw each surface's identity ON the glass (title, monitor,
// and in RGB mode the channel->surface map). The only reliable way to learn
// which physical cab panel a Windows display drives.
static int
PodIdentMode()
{
static int m = -1;
if (m < 0)
{
const char *e = getenv("BT_POD_IDENT");
m = (e != NULL && e[0] != 0 && e[0] != '0') ? 1 : 0;
}
return m;
}
// BT_POD_CHANTEST=<seconds> -- CHANNEL WALK, the instrument for mapping
// splitter-wired glass. On a cab, three mono CRTs share one VGA port through
// its R/G/B lines, so "which panel is which" cannot be read off Windows: the
// desktop sees ONE display for three monitors. This drives ONE channel at a
// time -- all-red, then all-green, then all-blue, N seconds each, with the
// channel name drawn huge -- so the answer is simply: watch which panel lights
// up, and when. Readable even over a rough livestream.
// Returns the dwell in seconds (0 = off).
static int
PodChanTestSeconds()
{
static int s = -1;
if (s < 0)
{
const char *e = getenv("BT_POD_CHANTEST");
s = (e != NULL && e[0] != 0) ? atoi(e) : 0;
if (s < 0) s = 0;
if (e != NULL && e[0] != 0 && s == 0) s = 3; // "1"/"on" -> 3s default
}
return s;
}
// Which channel is lit right now (0=red 1=green 2=blue), from the wall clock.
static int
PodChanTestPhase()
{
int dwell = PodChanTestSeconds();
if (dwell <= 0)
return -1;
return (int)((GetTickCount() / (DWORD)(dwell * 1000)) % 3);
}
// BT_POD_RGB=1 -- the pod's REAL video wiring (see pod-hardware.md §THE RGB
// SPLIT): one VGA port per window, its R/G/B analog lines split to three
// monochrome MFD monitors. Implies pod surface mode.
@@ -609,6 +732,73 @@ void
}
// Restore saved positions over the just-computed pod-faithful defaults. Called
//---------------------------------------------------------------------------
// MONITOR BINDING (2026-08-06) -- "monitor:<name-or-index>" instead of x,y.
//
// Absolute desktop coordinates do not survive a reboot on a pod: Windows
// renumbers the virtual desktop whenever adapters enumerate differently (a USB
// display adapter is especially prone), so a layout pinned to 800,122 can land
// on the wrong glass -- or off-screen -- after a power cycle. Binding to the
// MONITOR instead and resolving its rect at startup is stable across boots.
// Heat MFD=monitor:\.\DISPLAY4,bare (device name -- what the receipts print)
// Heat MFD=monitor:2,bare (index into the enumeration order)
// The surface is centred on that monitor; an exact-size panel fills it.
//---------------------------------------------------------------------------
struct MonScan { int index; const char *want; int wantIndex; RECT rect; int found; };
static BOOL CALLBACK
MonScanProc(HMONITOR mon, HDC, LPRECT, LPARAM param)
{
MonScan *sc = (MonScan *)param;
MONITORINFOEXA mi;
memset(&mi, 0, sizeof(mi));
mi.cbSize = sizeof(mi);
if (GetMonitorInfoA(mon, (MONITORINFO *)&mi))
{
int hit = 0;
if (sc->want != NULL)
{
// Match the full device name OR just its tail, so a hand-written
// cfg can say `monitor:DISPLAY4` and skip the \\.\ prefix entirely
// (backslashes in a config file are a foot-gun -- every shell,
// editor and copy-paste path mangles them differently).
size_t dl = strlen(mi.szDevice), wl = strlen(sc->want);
if (_stricmp(mi.szDevice, sc->want) == 0)
hit = 1;
else if (wl > 0 && wl <= dl
&& _stricmp(mi.szDevice + (dl - wl), sc->want) == 0)
hit = 1;
}
if (!hit && sc->wantIndex >= 0 && sc->index == sc->wantIndex)
hit = 1;
if (hit && !sc->found)
{
sc->rect = mi.rcMonitor;
sc->found = 1;
}
}
++sc->index;
return TRUE;
}
// Resolve "monitor:<spec>" to a rect. Returns 1 on success.
static int
ResolveMonitorSpec(const char *spec, RECT *out)
{
while (*spec == ' ' || *spec == ' ') ++spec;
MonScan sc;
memset(&sc, 0, sizeof(sc));
sc.wantIndex = -1;
if (spec[0] >= '0' && spec[0] <= '9')
sc.wantIndex = atoi(spec);
else
sc.want = spec;
EnumDisplayMonitors(NULL, NULL, MonScanProc, (LPARAM)&sc);
if (sc.found)
*out = sc.rect;
return sc.found;
}
// from Create AFTER ComputeLayout, so any window not named in the file keeps its
// computed spot. Restored windows are flagged so the WM_TIMER re-snap (which
// re-runs ComputeLayout once the main window appears) leaves them alone.
@@ -646,7 +836,34 @@ static void
*end = '\0';
int x = 0, y = 0, w = 0, h = 0;
if (sscanf(eq + 1, "%d,%d,%d,%d", &x, &y, &w, &h) < 2)
int haveXY = (sscanf(eq + 1, "%d,%d,%d,%d", &x, &y, &w, &h) >= 2);
int viaMonitor = 0;
if (!haveXY)
{
// "monitor:<name|index>" -- boot-stable binding (see the resolver).
const char *v = eq + 1;
while (*v == ' ' || *v == '\t') ++v;
if (_strnicmp(v, "monitor:", 8) == 0 || _strnicmp(v, "mon:", 4) == 0)
{
char spec[64];
const char *p2 = strchr(v, ':') + 1;
int n2 = 0;
while (*p2 && *p2 != ',' && n2 < (int)sizeof(spec) - 1) spec[n2++] = *p2++;
spec[n2] = 0;
RECT mr;
if (ResolveMonitorSpec(spec, &mr))
{
x = mr.left; y = mr.top;
w = mr.right - mr.left; h = mr.bottom - mr.top;
haveXY = 1;
viaMonitor = 1;
}
else
DEBUG_STREAM << "[glasswin] monitor '" << spec
<< "' not found -- leaving computed placement\n" << std::flush;
}
}
if (!haveXY)
continue;
// Trailing options after the numbers, comma-separated. Unknown ones
@@ -666,11 +883,30 @@ static void
GWin *gw = FindGWinByTitle(s);
if (gw == NULL)
continue;
gw->wantX = x;
gw->wantY = y;
gw->noFrame = noframe;
// "bare" is a pod panel: it always implies frameless. (Without this a
// cfg line saying only ",bare" silently re-framed a window that pod
// surface mode had already cropped -- caught on the bench 2026-08-06.)
gw->noFrame = (noframe || bare) ? 1 : 0;
if (bare && gw->buttonCount > 0)
MakeBareSurface(*gw); // per-window pod panel (mixed rigs)
if (viaMonitor)
{
// x,y,w,h is the MONITOR rect -- centre this surface on it (an
// exact-size pod panel fills it edge to edge).
int sw = gw->clientW > 0 ? gw->clientW : (gw->surfaceRect.right - gw->surfaceRect.left);
int sh = gw->clientH > 0 ? gw->clientH : (gw->surfaceRect.bottom - gw->surfaceRect.top);
gw->wantX = x + ((w - sw) / 2);
gw->wantY = y + ((h - sh) / 2);
DEBUG_STREAM << "[glasswin] '" << gw->title
<< "' bound to monitor " << x << "," << y << " " << w << "x" << h
<< " -> window at " << gw->wantX << "," << gw->wantY
<< std::endl << std::flush;
}
else
{
gw->wantX = x;
gw->wantY = y;
}
gw->restored = 1;
++restored;
}
@@ -841,6 +1077,19 @@ static void
mask = 0xFF; // index the low (palette) byte
palId = SVGA16::SecondaryPalette;
}
// RGB mode: the base surface rides a CHANNEL, so its tint is that pure
// primary -- resolved live (override first), never the build-time guess.
if (w->groupCount > 0 && tint >= 0)
{
switch (ResolvedChannelOf(w->portPrimary, port))
{
case L4GraphicsPort::RedChannel: tint = (int)0x00FF0000; break;
case L4GraphicsPort::GreenChannel: tint = (int)0x0000FF00; break;
case L4GraphicsPort::BlueChannel: tint = (int)0x000000FF; break;
default: break;
}
}
int ow = 0, oh = 0;
svga->ExpandPlaneToBGRA(mask, palId, tint, w->rotate, gStage, &ow, &oh);
@@ -880,9 +1129,9 @@ static void
if (ga != NULL && ga->GetEnableID() != L4GraphicsPort::BlankColor)
gp = ga;
}
const int ch = gp->GetEnableID();
const int ch = ResolvedChannelOf(w->groupPort[gi], gp);
unsigned long chMask;
switch (ch & 0x7F)
switch (ch)
{
case L4GraphicsPort::RedChannel: chMask = 0x00FF0000ul; break;
case L4GraphicsPort::GreenChannel: chMask = 0x0000FF00ul; break;
@@ -1069,6 +1318,113 @@ static void
// (NO full-face flash overlay here either -- same wrongness as the
// L4VB16 one, removed 2026-08-03: the buttons are big rects tucked
// under the surface by design, and the protruding edge is the lamp.)
// ---- CHANNEL WALK (BT_POD_CHANTEST) ---------------------------------
// Overwrite the whole surface with ONE channel at full intensity, so
// exactly one monitor of a splitter-fed trio lights. Cycles R->G->B.
{
const int phase = PodChanTestPhase();
if (phase >= 0)
{
static const COLORREF chColor[3] =
{ RGB(255, 0, 0), RGB(0, 255, 0), RGB(0, 0, 255) };
static const char *chName[3] = { "RED", "GREEN", "BLUE" };
HBRUSH fill = CreateSolidBrush(chColor[phase]);
FillRect(dc, &w->surfaceRect, fill);
DeleteObject(fill);
// Name the channel AND what the pod authors onto it here, so a
// lit panel identifies itself completely: "GREEN = Mfd2".
const char *who = "-";
GaugeRenderer *cgr = BTResolveGaugeRenderer();
if (cgr != NULL)
{
const char *cand[4]; int nc = 0;
cand[nc++] = w->portPrimary;
for (int gi = 0; gi < w->groupCount && nc < 4; ++gi)
cand[nc++] = w->groupPort[gi];
for (int i = 0; i < nc; ++i)
{
L4GraphicsPort *pp =
static_cast<L4GraphicsPort*>(cgr->GetGraphicsPort(cand[i]));
if (pp != NULL && (pp->GetEnableID() & 0x7F) == phase)
{ who = cand[i]; break; }
}
}
char big[96];
sprintf(big, "%s %s = %s", w->monitorName, chName[phase], who);
RECT br = w->surfaceRect;
br.top += (br.bottom - br.top) / 3;
SetBkMode(dc, TRANSPARENT);
DrawLabelText(dc, big, titleFont, &br, RGB(0, 0, 0),
DT_CENTER | DT_SINGLELINE | DT_VCENTER);
}
}
// ---- PANEL IDENTIFY (BT_POD_IDENT=1) --------------------------------
// Pod bring-up: nobody can tell which PHYSICAL panel a Windows display
// is, and desktop coordinate order says nothing about where a panel
// sits in the cab -- guessing that mapping is exactly how it came out
// wrong. So label each surface ON ITS OWN GLASS with what it is, which
// monitor it came from, and (in RGB mode) the channel->surface map.
// Walk the pod, read the panels, write the layout from what you SAW.
if (PodIdentMode())
{
char line1[96], line2[160];
sprintf(line1, "%s", w->title);
if (w->groupCount > 0)
{
// name the channels so a splitter-fed trio is self-describing
char parts[128];
parts[0] = 0;
GaugeRenderer *igr = BTResolveGaugeRenderer();
const char *names[4]; int chans[4]; int nn = 0;
names[nn] = w->portPrimary; chans[nn] = -1; ++nn;
for (int gi = 0; gi < w->groupCount && nn < 4; ++gi)
{ names[nn] = w->groupPort[gi]; chans[nn] = -1; ++nn; }
for (int i = 0; i < nn; ++i)
{
const char *chName = "?";
if (igr != NULL)
{
L4GraphicsPort *pp =
static_cast<L4GraphicsPort*>(igr->GetGraphicsPort(names[i]));
if (pp != NULL)
switch (pp->GetEnableID() & 0x7F)
{
case L4GraphicsPort::RedChannel: chName = "RED"; break;
case L4GraphicsPort::GreenChannel: chName = "GREEN"; break;
case L4GraphicsPort::BlueChannel: chName = "BLUE"; break;
case L4GraphicsPort::AllChannels: chName = "RGB"; break;
default: chName = "blank"; break;
}
}
char one[48];
sprintf(one, "%s%s=%s", (i ? " " : ""), chName, names[i]);
if (strlen(parts) + strlen(one) < sizeof(parts) - 1)
strcat(parts, one);
}
sprintf(line2, "%s %s", w->monitorName, parts);
}
else
{
sprintf(line2, "%s surface=%s", w->monitorName,
w->portPrimary ? w->portPrimary : "-");
}
RECT lr = w->surfaceRect;
lr.bottom = lr.top + 64;
HBRUSH back = CreateSolidBrush(RGB(0, 0, 0));
FillRect(dc, &lr, back);
DeleteObject(back);
SetBkMode(dc, TRANSPARENT);
RECT t1 = lr; t1.top += 4; t1.bottom = t1.top + 30;
DrawLabelText(dc, line1, titleFont, &t1, RGB(255, 255, 0),
DT_LEFT | DT_SINGLELINE | DT_VCENTER);
RECT t2 = lr; t2.top += 32; t2.bottom = t2.top + 28;
DrawLabelText(dc, line2, buttonFont, &t2, RGB(0, 255, 255),
DT_LEFT | DT_SINGLELINE | DT_VCENTER);
}
}
BitBlt(winDC, 0, 0, client.right - client.left, client.bottom - client.top,
@@ -1205,11 +1561,23 @@ void
if (gWinCount != 0)
return; // already up
// BT_GAUGE_SEC_ROT: the portrait-CRT unrotation direction (shared with the
// D3D dev path; default 3 = CW = user-verified upright).
// BT_GAUGE_SEC_ROT: how far to turn the secondary/radar surface.
// 0 = none, 1 = 90 CCW, 2 = 180, 3 = 90 CW (default -- the pod's
// portrait CRT, user-verified upright).
// 0 and 2 keep the picture LANDSCAPE, which a modern replacement panel
// needs. (Until 2026-08-06 this read only '1' and forced everything else
// to 3, so BT_GAUGE_SEC_ROT=2 was silently ignored -- caught on the cart
// when a 180 request kept coming back portrait.)
{
const char *rv = getenv("BT_GAUGE_SEC_ROT");
gRadarRot = (rv != NULL && rv[0] == '1') ? 1 : 3;
if (rv != NULL && rv[0] >= '0' && rv[0] <= '3')
gRadarRot = rv[0] - '0';
else
gRadarRot = 3;
DEBUG_STREAM << "[glasswin] radar rotation " << gRadarRot
<< (gRadarRot == 3 ? " (90 CW)" : gRadarRot == 1 ? " (90 CCW)"
: gRadarRot == 2 ? " (180)" : " (none)")
<< std::endl << std::flush;
}
memset(latched, 0, sizeof(latched));
@@ -1348,6 +1716,8 @@ void
if (mon != NULL && GetMonitorInfoA(mon, (MONITORINFO *)&mi))
{
devName = mi.szDevice;
strncpy(w.monitorName, mi.szDevice, sizeof(w.monitorName) - 1);
w.monitorName[sizeof(w.monitorName) - 1] = 0;
mx = mi.rcMonitor.left; my = mi.rcMonitor.top;
mw = mi.rcMonitor.right - mi.rcMonitor.left;
mh = mi.rcMonitor.bottom - mi.rcMonitor.top;
+19 -7
View File
@@ -90,6 +90,23 @@ L4GaugeRenderer::L4GaugeRenderer(bool windowed, int *secondaryIndex, int *aux1In
char
*plasma_string = getenv("L4PLASMA");
//
// L4PLASMA=NONE / OFF / 0 -- no plasma marquee at all (2026-08-06).
// A real cab drives the 128x32 marquee over serial; a rebuilt cart may
// not have one wired, and the GLASS profile force-defaults L4PLASMA to
// SCREEN, which puts a desktop plasma window on the pod's glass where
// nothing should be. Leaving the variable UNSET is not an option
// there (the profile fills it in), so give "off" an explicit spelling.
//
if (plasma_string != NULL
&& (_stricmp(plasma_string, "NONE") == 0
|| _stricmp(plasma_string, "OFF") == 0
|| strcmp(plasma_string, "0") == 0))
{
Tell("Plasma display disabled (L4PLASMA=none)\n");
plasma_string = NULL;
}
if (plasma_string != NULL)
{
//Win32 Serial support: ADB 06/30/07
@@ -812,18 +829,13 @@ void
switch (type)
{
case scrambleVideo:
// 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)
if (graphicsDisplay != NULL)
{
scrambleVideoFlag = True;
scrambleVideoTimeout = ((Scalar)Now()) + duration;
Check(graphicsDisplay);
((SVGA16*) graphicsDisplay)->FunkyVideo(True, duration); // duration -> the recovery envelope
((SVGA16*) graphicsDisplay)->FunkyVideo(True);
}
break;
+40 -234
View File
@@ -615,41 +615,17 @@ 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): 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++)
{
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++)
{
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]);
PaletteTriplet *pe = &(pal->paletteData.Color[*source & mask]);
*dest = ((pe->Red >> 3) << 11) | ((pe->Green >> 2) << 5) | (pe->Blue >> 3);
dest++;
dest++; source++;
}
dest += postRowIncrement;
}
@@ -661,27 +637,10 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa
Word tint = (Word) monoTint;
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++)
{
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 = (*source & mask) ? tint : 0;
dest++; source++;
}
dest += postRowIncrement;
}
@@ -753,38 +712,16 @@ 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++)
{
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
Word *src = base + y * w;
unsigned long *d = dst + y * w;
double rowScroll = roff + shr * (double)y;
for (int x = 0; x < w; 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];
Word s = src[x];
if (monoTint < 0)
{
PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]);
@@ -802,10 +739,36 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate
return;
}
// 180 degrees (rotate 2): dimensions UNCHANGED -- the case a landscape
// replacement panel needs when the 1995 glass was a portrait CRT mounted
// the other way up. Added 2026-08-06 from the crash cart, whose radar is a
// landscape LCD: 90 CW left the picture portrait and overhanging its
// monitor, and a further 90 CW is this.
if (rotateQuadrant == 2)
{
for (int oy = 0; oy < h; oy++)
{
unsigned long *d = dst + oy * w;
for (int ox = 0; ox < w; ox++)
{
Word s = base[(h - 1 - oy) * w + (w - 1 - ox)];
if (monoTint < 0)
{
PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]);
d[ox] = ((unsigned long)pe->Red << 16) |
((unsigned long)pe->Green << 8) | (unsigned long)pe->Blue;
}
else
d[ox] = (s & mask) ? (unsigned long)monoTint : 0UL;
}
}
if (outW) *outW = w;
if (outH) *outH = h;
return;
}
// 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;
for (int oy = 0; oy < oh; oy++)
{
@@ -823,21 +786,7 @@ void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotate
sx = w - 1 - oy;
sy = ox;
}
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];
Word s = base[sy * w + sx];
if (monoTint < 0)
{
PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]);
@@ -5474,9 +5423,6 @@ 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
scrambleStartMs = 0;
scrambleDurationS = 0.0;
//STUBBED: VIDEO RB 1/15/07
# if defined(DEBUG)
Tell("SVGA16::SVGA16()\n");
@@ -6436,153 +6382,13 @@ void
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
SVGA16::FunkyVideo(Logical on_off, Scalar duration)
SVGA16::FunkyVideo(Logical 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.
}
//
// 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;
//STUBBED: VIDEO RB 1/15/07
//Check(this);
//SVGAFunkyVideo(on_off);
//Check_Fpu();
}
//########################################################################
+1 -21
View File
@@ -332,27 +332,7 @@ public:
UnflashPalette(int palette_number);
void
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;
FunkyVideo(Logical on_off);
protected:
+42 -1
View File
@@ -407,6 +407,32 @@ static const char *gBTCwdFixNote = NULL;
//
int gBTFitDisplay = 0;
//
// L4PLASMA=NONE/OFF/0 means "no marquee at all" (L4GREND.cpp reads the same
// spellings). Only used to keep the boot banner honest.
//
static bool BTPlasmaDisabled(void)
{
const char *p = getenv("L4PLASMA");
return p != NULL && (_stricmp(p, "NONE") == 0 ||
_stricmp(p, "OFF") == 0 ||
strcmp(p, "0") == 0);
}
//
// Which control device the GLASS profile actually resolved to. The banner
// used to hardcode "PadRIO", which is a lie on a cab wired to the real board
// -- and precisely the log line someone reads to check that (2026-08-06).
//
static const char *BTControlsSummary(void)
{
const char *c = getenv("L4CONTROLS");
if (c == NULL) return "PadRIO";
if (_strnicmp(c, "RIO", 3) == 0) return "hardware RIO";
if (_strnicmp(c, "PAD", 3) == 0) return "PadRIO";
return c;
}
static void BTEnsureContentDirectory(void)
{
if (GetFileAttributesA("BTL4.RES") != INVALID_FILE_ATTRIBUTES)
@@ -848,6 +874,18 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
{
gBTFitDisplay = 1;
}
//
// BT_FIT=1 -- the env spelling of -fit, so a POD can freeze it in
// content\environ.ini alongside the rest of its rig config instead of
// depending on every launcher passing the flag (2026-08-06, crash cart:
// the main view has to fill its 800x600 panel on every boot, however the
// game gets started -- shortcut, autostart or scheduled task).
//
{
const char *fitEnv = getenv("BT_FIT");
if (fitEnv != NULL && fitEnv[0] != '\0' && fitEnv[0] != '0')
gBTFitDisplay = 1;
}
// BT_JOYCONFIG=1: the generic-joystick capture wizard (flight sticks /
// HOTAS / pedals -- L4JOY.h). Console prompts detect which device/axis
@@ -1145,8 +1183,11 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
std::cout << "[boot] platform profile: "
<< (fe_menu_mode ? "MENU (front end -- no profile applied)"
: gBTPlatformPod ? "POD (RIO cockpit input; multi-surface gauges/MFDs via pod hardware or explicit L4GAUGE)"
: gBTPlatformGlass ? "GLASS (PadRIO + plasma window)"
: gBTPlatformGlass ? "GLASS"
: "DEV (single window + keyboard)");
if (gBTPlatformGlass && !fe_menu_mode)
std::cout << " (" << BTControlsSummary()
<< (BTPlasmaDisabled() ? "; plasma off [L4PLASMA])" : " + plasma window)");
if (!fe_menu_mode)
std::cout << " [secondary displays: " << kGlassLayoutName[glassLayout] << "]";
std::cout << std::endl << std::flush;
-26
View File
@@ -82,7 +82,6 @@
// The torso twist is reached via a BRIDGE (BTGetTorsoTwist, defined in torso.cpp)
// for the same reason.
#include "dmgtable.hpp"
#include <GAUGREND.hpp> // GaugeRenderer::SpecialEffect (virtual) -- PPC scramble trigger
extern Scalar BTGetTorsoTwist(Subsystem *torso); // torso.cpp (Torso complete there)
// heat-bank ambient bridge (heatfamily_reslice.cpp, AggregateHeatSink complete
// there) -- mech.cpp cannot include the subsystem headers (local-stub collision)
@@ -1123,31 +1122,6 @@ void
}
}
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
// REPLACES the engine-base single application. Three things the base
-252
View File
@@ -1,252 +0,0 @@
# 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"
+11
View File
@@ -0,0 +1,11 @@
# ALPHA-MR cab layout -- VERIFIED BY EYE 2026-08-06 (do not re-guess from
# desktop coordinates; they say nothing about physical cab position).
# DISPLAY4 -> the COLOUR RADAR LCD in the yellow-button frame, so the colour
# surface goes there. (An RGB composite landed here first and
# showed all three channels overlaid.)
# DISPLAY2 -> the VGA port feeding the Heat + Comm + Mfd2 CRT group.
# DISPLAY1 -> the VGA port feeding the Mfd1 + Mfd3 CRT group.
# monitor: binding, not pixel rects -- the USB graphics adapter re-enumerates.
Secondary / Radar=monitor:DISPLAY4,bare
VGA Port A=monitor:DISPLAY2,bare
VGA Port B=monitor:DISPLAY1,bare
+8
View File
@@ -0,0 +1,8 @@
# IDENTIFY PASS -- assignment here is arbitrary on purpose. Each panel prints
# its own name + monitor, so we map the cab from what the GLASS says, not from
# desktop coordinate order (which is what got it wrong the first time).
#
# Bound by MONITOR IDENTITY, not x,y: survives reboots and adapter renumbering.
VGA Port A=monitor:DISPLAY4,bare
VGA Port B=monitor:DISPLAY2,bare
Secondary / Radar=monitor:DISPLAY1,bare
+8
View File
@@ -0,0 +1,8 @@
# Corrected from the cab photo (2026-08-06): DISPLAY4 feeds the COLOUR RADAR
# LCD -- the RGB composite was landing there and showing all three channels
# overlaid. Colour surface to the colour panel; the RGB composites to the
# displays that feed the mono CRT groups. Which CRT group is DISPLAY2 vs
# DISPLAY1 is still unknown -- the channel test settles it.
Secondary / Radar=monitor:DISPLAY4,bare
VGA Port A=monitor:DISPLAY2,bare
VGA Port B=monitor:DISPLAY1,bare
+25
View File
@@ -0,0 +1,25 @@
Add-Type @"
using System;using System.Text;using System.Runtime.InteropServices;
public class W{
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc cb,IntPtr p);
public delegate bool EnumWindowsProc(IntPtr h,IntPtr p);
[DllImport("user32.dll")] public static extern int GetWindowText(IntPtr h,StringBuilder s,int c);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h,out uint p);
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h,out R r);
public struct R{public int L,T,Rr,B;}
}
"@
$target = (Get-Process btl4 -ErrorAction SilentlyContinue).Id
if (-not $target) { Write-Output "btl4 NOT RUNNING"; exit }
[W]::EnumWindows({ param($h,$l)
$o = 0
[W]::GetWindowThreadProcessId($h, [ref]$o) | Out-Null
if ($o -eq $target -and [W]::IsWindowVisible($h)) {
$sb = New-Object System.Text.StringBuilder 256
[W]::GetWindowText($h, $sb, 256) | Out-Null
$r = New-Object 'W+R'
[W]::GetWindowRect($h, [ref]$r) | Out-Null
Write-Output ("WIN '" + $sb.ToString() + "' at " + $r.L + "," + $r.T + " " + ($r.Rr-$r.L) + "x" + ($r.B-$r.T))
}
return $true }, [IntPtr]::Zero) | Out-Null
+21
View File
@@ -0,0 +1,21 @@
# Merge the POD PROFILE block into the cart's content\environ.ini, idempotently.
# Any previous block between the two markers is replaced, so re-running is safe.
$ini = 'C:\bt411\BT411_4.11.801\content\environ.ini'
$prof = 'C:\bt411\BT411_4.11.801\content\podprofile.ini'
$block = Get-Content $prof
$keep = @()
if (Test-Path $ini) {
$inBlock = $false
foreach ($line in (Get-Content $ini)) {
if ($line -match '^# ==== BT411 POD PROFILE') { $inBlock = $true; continue }
if ($line -match '^# ==== END BT411 POD PROFILE') { $inBlock = $false; continue }
if (-not $inBlock) { $keep += $line }
}
}
# Trim trailing blanks so repeated merges do not grow the file.
while ($keep.Count -gt 0 -and $keep[-1].Trim() -eq '') { $keep = $keep[0..($keep.Count-2)] }
($keep + @('') + $block) | Set-Content $ini -Encoding ascii
Write-Output "environ.ini: $((Get-Content $ini).Count) lines, profile keys:"
Get-Content $ini | Select-String '^(BT_|L4)' | ForEach-Object { $_.Line }
+48
View File
@@ -0,0 +1,48 @@
# Apply the ALPHA-MR pod kit to a BT411 install's content directory.
#
# WHY THIS EXISTS: the frozen rig config lives in content\environ.ini and
# glass_layout.cfg -- both INSIDE the versioned install folder, and neither is
# shipped in the zip (environ.ini is generated on first run). So extracting a
# new build gives you a cab that comes up wrong with no error anywhere. The
# MASTERS live beside this script at a stable path; this pushes them into
# whichever install you point it at. Idempotent -- safe to re-run.
param([Parameter(Mandatory=$true)][string]$Content)
$here = Split-Path -Parent $MyInvocation.MyCommand.Definition
$prof = Join-Path $here 'podprofile.ini'
$lay = Join-Path $here 'glass_layout.cfg'
$ini = Join-Path $Content 'environ.ini'
if (-not (Test-Path $Content)) { Write-Output "NO SUCH CONTENT DIR: $Content"; exit 1 }
if (-not (Test-Path $prof)) { Write-Output "MISSING MASTER: $prof"; exit 1 }
# --- the env block: replace anything between the markers, keep the rest ------
$block = Get-Content $prof
$keep = @()
if (Test-Path $ini) {
$inBlock = $false
foreach ($line in (Get-Content $ini)) {
if ($line -match '^# ==== BT411 POD PROFILE') { $inBlock = $true; continue }
if ($line -match '^# ==== END BT411 POD PROFILE') { $inBlock = $false; continue }
if (-not $inBlock) { $keep += $line }
}
while ($keep.Count -gt 0 -and $keep[-1].Trim() -eq '') { $keep = $keep[0..($keep.Count-2)] }
}
($keep + @('') + $block) | Set-Content $ini -Encoding ascii
# --- the layout: the master always wins -------------------------------------
# Re-tuning the cab means editing the MASTER beside this script, not the copy
# in the install -- a BT_GLASS_LAYOUT=save drag inside an install is overwritten
# on the next apply, on purpose. One place to look when a panel moves.
if (Test-Path $lay) { Copy-Item $lay (Join-Path $Content 'glass_layout.cfg') -Force }
# --- the pod test mission ----------------------------------------------------
# PODTEST.EGG is NOT in the repo -- it was made on the cart, so a fresh extract
# has no mission for runpod.bat to launch (found the first time a new build was
# dropped: the launcher ran and nothing came up). Carry it in the kit.
$egg = Join-Path $here 'PODTEST.EGG'
if (Test-Path $egg) { Copy-Item $egg (Join-Path $Content 'PODTEST.EGG') -Force }
Write-Output "pod kit applied to $Content"
Get-Content $ini | Select-String '^(BT_|L4)' | ForEach-Object { " " + $_.Line }
if (Test-Path $lay) { Get-Content (Join-Path $Content 'glass_layout.cfg') | Select-String '^[A-Z]' | ForEach-Object { " " + $_.Line } }
+55
View File
@@ -0,0 +1,55 @@
# ==== BT411 POD PROFILE (ALPHA-MR crash cart) ================================
# Frozen 2026-08-06 from the verified-good bring-up. Everything a launcher
# used to have to pass now lives here, so the rig comes up correct however
# the game is started -- shortcut, scheduled task or autostart.
# Physical mapping + the RGB-split decode: context/pod-hardware.md.
#
# The layout itself (which surface sits on which monitor) is in
# glass_layout.cfg beside this file, loaded by BT_GLASS_LAYOUT=load:
# Secondary / Radar = monitor:DISPLAY4 (colour LCD in the yellow frame)
# VGA Port A = monitor:DISPLAY2 (RGB triple: Heat + Comm + Mfd2)
# VGA Port B = monitor:DISPLAY1 (RGB triple: Mfd1 + Mfd3)
# Platform PROFILE. NB "glass", not "pod": BT_PLATFORM=pod selects the 1995
# multi-surface gauge path (exclusive-fullscreen D3D per adapter + the NVIDIA
# horizontal span), which no modern driver can do. What lights this cab is the
# GLASS profile plus the three switches below. (Glass is already the default
# when nothing is set -- named here so the rig never depends on that default.)
BT_PLATFORM=glass
# One window per surface -- this is the switch that makes panels at all.
BT_GLASS_PANELS=1
# ONE VGA port's R/G/B lines drive THREE mono MFDs -- composite the planes.
BT_POD_RGB=1
# THIS cab's splitter wires Comm and Heat to different colour lines than the
# 1995 L4GAUGE.CFG assumed (kills/deaths and coolant loops came up swapped).
BT_POD_CHANMAP=Comm=blue,Heat=red
# The radar panel here is a LANDSCAPE LCD; the 1995 pod rotated only because
# its CRT was mounted portrait. 0 = leave the 640x480 source alone.
BT_GAUGE_SEC_ROT=0
BT_GLASS_LAYOUT=load
BT_START_INSIDE=1
# Main view fills its 800x600 panel (DISPLAY3, the primary) with no border.
BT_FIT=1
# THE REAL COCKPIT BOARD. RIO:COM1 -> \\.\COM1 at 9600 8N1 (L4SERIAL); the
# stock control path above the seam (mapper, lamps, streamed .CTL mappings)
# runs unchanged -- PadRIO was only ever standing in for this.
# * RIO and PAD are MUTUALLY EXCLUSIVE: both assign rioPointer in the
# L4CONTROLS parser, so the last token wins. This turns the XInput pad
# off on this machine. To get it back for a session, override in the
# launcher: set L4CONTROLS=PAD,KEYBOARD (the real environment wins).
# * KEYBOARD stays in the list as the fallback. It does NOT demote the RIO
# (it only sets a flag), and a silent total loss of input would be
# indistinguishable from a hang.
L4CONTROLS=RIO:COM1,KEYBOARD
# No 128x32 marquee wired on this cart -- and the glass profile would
# otherwise default L4PLASMA to SCREEN and drop a plasma window on the glass.
L4PLASMA=NONE
# ==== END BT411 POD PROFILE ==================================================
+19 -9
View File
@@ -1,12 +1,22 @@
@echo off
REM BT411 pod launch -- console session (started via schtasks /IT).
cd /d C:\bt411\BT411_4.11.801\content
set BT_GLASS=1
set BT_GLASS_PANELS=1
set BT_POD_RGB=1
set BT_GLASS_LAYOUT=load
REM BT411 pod -- ALPHA-MR. Resolves the NEWEST BT411_* install, applies the
REM frozen rig config (so a fresh zip drop needs no hand-editing), and launches
REM straight into a mission. The rig itself is frozen in content\environ.ini;
REM this carries only the log switches and the mission.
REM Fallback if the kit is ever lost: runpod_env.bat sets everything inline.
setlocal
set ROOT=C:\bt411
set INSTALL=
for /f "delims=" %%d in ('dir /b /ad /o-d "%ROOT%\BT411_*" 2^>nul') do (
if not defined INSTALL set INSTALL=%ROOT%\%%d
)
if not defined INSTALL goto noinstall
powershell -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\podkit.ps1" -Content "%INSTALL%\content" >nul
cd /d %INSTALL%\content
set BT_GLASS_LOG=1
set BT_START_INSIDE=1
set BT_FE_SOLO=1
set BT_LOG=podrun.log
start "" ..\build\Release\btl4.exe
start "" ..\build\Release\btl4.exe -egg PODTEST.EGG
exit /b
:noinstall
echo No BT411_* folder found under %ROOT%.
exit /b 1
+16
View File
@@ -0,0 +1,16 @@
@echo off
REM BT411 pod -- ALPHA-MR working config.
REM layout : radar on DISPLAY4 (the colour LCD), RGB composites on 2 and 1
REM fix 1 : Comm/Heat ride swapped colour lines on THIS cab's splitter
REM fix 2 : radar panel is landscape -> UNROTATED landscape, not the pod portrait CRT
cd /d C:\bt411\BT411_4.11.801\content
set BT_GLASS=1
set BT_GLASS_PANELS=1
set BT_POD_RGB=1
set BT_POD_CHANMAP=Comm=blue,Heat=red
set BT_GAUGE_SEC_ROT=0
set BT_GLASS_LAYOUT=load
set BT_GLASS_LOG=1
set BT_START_INSIDE=1
set BT_LOG=podrun.log
start "" ..\build\Release\btl4.exe -egg PODTEST.EGG
+24
View File
@@ -0,0 +1,24 @@
@echo off
REM BT411 pod -- HARDWARE RIO test. Same frozen glass/RGB rig, but the input
REM device is the real cockpit board on COM1 instead of PadRIO.
REM L4CONTROLS=RIO:COM1 -> RIO opens \.\COM1 at 9600 8N1 (L4SERIAL).
REM KEYBOARD stays in the list so the cab is still playable if the board is
REM dead -- a silent loss of all input would be indistinguishable from a hang.
setlocal
set ROOT=C:\bt411
set INSTALL=
for /f "delims=" %%d in ('dir /b /ad /o-d "%ROOT%\BT411_*" 2^>nul') do (
if not defined INSTALL set INSTALL=%ROOT%\%%d
)
if not defined INSTALL goto noinstall
powershell -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\podkit.ps1" -Content "%INSTALL%\content" >nul
cd /d %INSTALL%\content
set L4CONTROLS=RIO:COM1,KEYBOARD
set BT_CTRLMAP_LOG=1
set BT_GLASS_LOG=1
set BT_LOG=podrio.log
start "" ..\build\Release\btl4.exe -egg PODTEST.EGG
exit /b
:noinstall
echo No BT411_* folder found under %ROOT%.
exit /b 1
+24
View File
@@ -0,0 +1,24 @@
@echo off
REM ============================================================================
REM RUN THIS AFTER EXTRACTING A NEW BT411 BUILD.
REM It finds the newest BT411_* folder under C:\bt411 and pushes the cab's
REM frozen rig config into it (environ.ini block + glass_layout.cfg).
REM Without it a fresh extract comes up with the MFDs wrong and no error.
REM ============================================================================
setlocal
set ROOT=C:\bt411
set INSTALL=
for /f "delims=" %%d in ('dir /b /ad /o-d "%ROOT%\BT411_*" 2^>nul') do (
if not defined INSTALL set INSTALL=%ROOT%\%%d
)
if not defined INSTALL goto noinstall
echo Newest install: %INSTALL%
powershell -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\podkit.ps1" -Content "%INSTALL%\content"
echo.
echo Done. play_solo.bat / join.bat / play_steam.bat in that folder will now
echo all come up on the pod glass -- they inherit it from environ.ini.
pause
exit /b
:noinstall
echo No BT411_* folder found under %ROOT%. Extract the zip there first.
pause
+101
View File
@@ -0,0 +1,101 @@
@echo off
REM ===========================================================================
REM podbringup.bat -- ONE double-click pod bring-up. BT411.
REM
REM 1. self-elevates (accept the UAC prompt -- that is the only interaction)
REM 2. installs the built-in Windows OpenSSH SERVER
REM 3. authorizes ONE key (the laptop's bt411 key) and locks its ACL
REM 4. opens SSH on private/domain profiles ONLY (never the public internet)
REM 5. probes the display/GPU/monitor/serial topology
REM 6. sends the results BACK over Tailscale -- no copying by hand
REM
REM Undo (admin cmd):
REM sc stop sshd & sc config sshd start= disabled
REM del C:\ProgramData\ssh\administrators_authorized_keys
REM netsh advfirewall firewall delete rule name="BT411 SSH"
REM ===========================================================================
REM ---- 1. elevate -----------------------------------------------------------
net session >nul 2>&1
if %errorlevel% neq 0 (
echo Requesting administrator rights -- click YES on the prompt...
powershell -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
exit /b
)
setlocal enabledelayedexpansion
set LAPTOP=desktop-ae1su9u
set TS=C:\PROGRA~1\Tailscale\tailscale.exe
set REPORT=%~dp0podbringup_report.txt
set DXOUT=%~dp0podbringup_dxdiag.txt
echo ==================== BT411 POD BRING-UP ====================> "%REPORT%"
echo host=%COMPUTERNAME% user=%USERNAME% session=%SESSIONNAME%>> "%REPORT%"
ver >> "%REPORT%"
echo.>> "%REPORT%"
REM ---- 2. OpenSSH server ----------------------------------------------------
echo [1/5] installing OpenSSH server (this can take a minute)...
echo ---- OpenSSH install ---->> "%REPORT%"
dism /online /quiet /norestart /add-capability /capabilityname:OpenSSH.Server~~~~0.0.1.0 >> "%REPORT%" 2>&1
sc config sshd start= auto >> "%REPORT%" 2>&1
net start sshd >> "%REPORT%" 2>&1
sc query sshd | findstr /i "STATE" >> "%REPORT%" 2>&1
REM ---- 3. authorize the laptop key -----------------------------------------
echo [2/5] authorizing the BT411 key...
if not exist C:\ProgramData\ssh mkdir C:\ProgramData\ssh
set AKF=C:\ProgramData\ssh\administrators_authorized_keys
findstr /c:"bt411-claude-code@laptop" "%AKF%" >nul 2>&1
if errorlevel 1 (
echo ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICmGkinDbZMwQ/UhwqU/oECUIeAN+pBjLgsekj41di7W bt411-claude-code@laptop>> "%AKF%"
)
icacls "%AKF%" /inheritance:r /grant "SYSTEM:F" /grant "BUILTIN\Administrators:F" >> "%REPORT%" 2>&1
echo ---- authorized_keys ---->> "%REPORT%"
type "%AKF%" >> "%REPORT%" 2>&1
REM ---- 4. firewall (private/domain only) ------------------------------------
echo [3/5] firewall rule (private/tailnet only)...
netsh advfirewall firewall show rule name="BT411 SSH" >nul 2>&1
if errorlevel 1 (
netsh advfirewall firewall add rule name="BT411 SSH" dir=in action=allow protocol=TCP localport=22 profile=private,domain >> "%REPORT%" 2>&1
)
REM ---- 5. topology probe ----------------------------------------------------
echo [4/5] probing displays / GPUs / monitors / serial ports...
echo.>> "%REPORT%"
echo ---- OS ---->> "%REPORT%"
wmic os get Caption,Version,BuildNumber,OSArchitecture /format:list >> "%REPORT%" 2>&1
echo ---- GPUs ---->> "%REPORT%"
wmic path Win32_VideoController get Name,DriverVersion,DriverDate,CurrentHorizontalResolution,CurrentVerticalResolution /format:list >> "%REPORT%" 2>&1
echo ---- Monitors ---->> "%REPORT%"
wmic path Win32_DesktopMonitor get DeviceID,Name,ScreenWidth,ScreenHeight,Availability /format:list >> "%REPORT%" 2>&1
echo ---- Serial ports (RIO) ---->> "%REPORT%"
wmic path Win32_SerialPort get DeviceID,Name /format:list >> "%REPORT%" 2>&1
echo ---- Tailscale ---->> "%REPORT%"
"%TS%" ip -4 >> "%REPORT%" 2>&1
echo ---- CONNECT WITH ---->> "%REPORT%"
echo ssh -i ~/.ssh/bt411_pod %USERNAME%@bt411-pod>> "%REPORT%"
echo [5/5] dxdiag (monitor rects + D3D caps, ~30s)...
dxdiag /whql:off /t "%DXOUT%"
REM ---- 6. send it all back --------------------------------------------------
echo Sending results back to %LAPTOP% over Tailscale...
"%TS%" file cp "%REPORT%" "%DXOUT%" %LAPTOP%:
if errorlevel 1 (
echo.
echo ! Taildrop send failed -- right-click these two files and use
echo "Send with Tailscale" instead:
echo %REPORT%
echo %DXOUT%
) else (
echo ...sent.
)
echo.
echo ================= DONE =================
echo ssh user: %USERNAME% host: bt411-pod
type "%REPORT%" | findstr /i "STATE ssh-ed25519 Caption"
echo.
pause