From 67a4f09e861a14a85cb673568d3a052f1abbdcd2 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Thu, 6 Aug 2026 11:31:49 -0500 Subject: [PATCH] POD MFD bring-up: BT_POD_SURFACES bare-panel mode + placement receipts + probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nick's crash cart is the first shot at driving the real MFD panels, so the pieces the cab needs that the desktop glass path lacked: - BT_POD_SURFACES=1 (L4GLASSWIN): crop every display window to its SURFACE (MFDs exactly 640x480, radar 480x640 portrait), drop the on-screen RIO button banks -- the cab's buttons are PHYSICAL, so drawing fake ones onto a real panel is exactly wrong -- go frameless, and skip the Flight Controls pad entirely (6 windows, not 7). Per-window ",bare" in glass_layout.cfg for mixed rigs (idempotent with the global mode). - Placement receipts: each window logs which PHYSICAL monitor it landed on ("[glasswin] 'Heat MFD' surface=Heat at X,Y 640x480 bare -> monitor \.\DISPLAYn (...)"). Nobody can see 7 surfaces at once on a cab, and over Chrome Remote Desktop you cannot see the panels at all -- the log is the only confirmation the map is right. - tools/podprobe.ps1: run-on-the-pod topology probe (no install/admin) -- GPUs, every monitor's virtual-desktop rect, EDID make/model, serial ports (the RIO board), session type, plus a PROPOSED glass_layout.cfg assigning the six surfaces to the non-primary monitors. Self-tested here (correctly reports a single-monitor laptop and declines to map). Local verify: 5 MFDs at 640x480 bare + portrait radar + Flight Controls dropped, receipts printed. KB: pod-hardware §MFD PANELS ON REAL HARDWARE (incl. WHY the 1995 path is not the route -- NVIDIA Horizontal Span is gone from post-XP drivers and per-adapter exclusive fullscreen is fragile over a remote session) + glass-cockpit env table. Co-Authored-By: Claude Fable 5 --- context/glass-cockpit.md | 1 + context/pod-hardware.md | 38 +++++++++ engine/MUNGA_L4/L4GLASSWIN.cpp | 96 ++++++++++++++++++++- tools/podprobe.ps1 | 150 +++++++++++++++++++++++++++++++++ 4 files changed, 281 insertions(+), 4 deletions(-) create mode 100644 tools/podprobe.ps1 diff --git a/context/glass-cockpit.md b/context/glass-cockpit.md index 943ec10..5dff570 100644 --- a/context/glass-cockpit.md +++ b/context/glass-cockpit.md @@ -73,6 +73,7 @@ that one answer. Precedence, highest first: | Env | Layout | Buttons live in | |---|---|---| | `BT_GLASS_PANELS`≠0 | per-display cockpit windows (the "exploded" view, `L4GLASSWIN`) | each display's own window | +| `BT_POD_SURFACES`=1 | **POD MODE** (2026-08-06): crop each panel window to its SURFACE (MFD 640×480, radar 480×640), drop the on-screen button banks + the Flight Controls pad, frameless. For REAL pod glass, where the buttons are physical — see [[pod-hardware]] §MFD PANELS. Per-window: `,bare` in `glass_layout.cfg`. | 6 bare surfaces | | `BT_DEV_GAUGES_WINDOW` | the legacy separate MFD window | the single combined pad panel | | `BT_DEV_GAUGES_DOCK` | the docked bottom gauge strip | the single combined pad panel | | `BT_COCKPIT=0` | ...also the docked strip (the documented opt-out) | the single combined pad panel | diff --git a/context/pod-hardware.md b/context/pod-hardware.md index 768b859..f22f418 100644 --- a/context/pod-hardware.md +++ b/context/pod-hardware.md @@ -184,6 +184,44 @@ All cockpit surfaces are bit-plane MASKS over ONE shared `SVGA16` pixelBuffer: ` byte; `Heat`=0x4000, `Mfd2`=0x0400, `Comm`=0x8000, `Mfd1`=0x0100, `Mfd3`=0x1000; `Eng1-3` = engineering-mode alt planes; `overlay`=0x00C0 (shares the sec surface). See [[gauges-hud]]. [T2] +## 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 +to drive the real panels. **The 1995 display path is NOT the way in.** That rig spanned the five +MFDs as ONE 1280×480 surface via **NVIDIA Horizontal Span, which every driver after XP dropped**; +`SVGA16::BuildWindows` also wants an exclusive-fullscreen D3D device per adapter, which is +fragile on modern drivers and over a remote session. Both are still in-tree and still the +authentic reference — they are just not the bring-up route. + +**The route is the glass per-display windows** ([[glass-cockpit]]): one window per surface, which +maps 1:1 onto one physical panel per MFD, needs no special driver, and is already field-proven on +desktops. Two pieces were added for the cab: +- **`BT_POD_SURFACES=1` (pod surface mode, L4GLASSWIN)** — crops every window to its SURFACE + (MFDs exactly 640×480, radar 480×640 portrait), drops the on-screen RIO button banks (the cab's + buttons are PHYSICAL — drawing fake ones over a real panel is precisely wrong), goes frameless, + and does not create the Flight Controls pad at all (6 windows, not 7). Per-window equivalent: + append `,bare` to a line in `glass_layout.cfg` (mixed rigs). +- **Placement receipts** — every window logs `[glasswin] '' surface=<port> at X,Y WxH + bare -> monitor \.\DISPLAYn (origin WxH, PRIMARY)`. On a cab nobody can see seven surfaces at + once, and over CRD you cannot see the panels at all: the log IS the confirmation that a picture + landed on the right glass. + +**Runbook** (`tools/podprobe.ps1`, PowerShell, no install/admin — run it ON the pod PC): +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 + 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 = +upper right (hot box), Left/Right Weapons = lower left/right, Secondary/Radar = the secondary +screen (portrait). Main 3D view stays the game's own window on the main-view monitor. + +**Known constraints / open on-pod questions:** whether the panels are attached to this PC at all +(the probe answers it); whether Windows offers a 640×480 mode on them (if not, the surface renders +at native size CENTRED, not scaled — a scale-to-fit option is the obvious follow-up); whether +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. + ## 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: diff --git a/engine/MUNGA_L4/L4GLASSWIN.cpp b/engine/MUNGA_L4/L4GLASSWIN.cpp index 7d7a7ee..806594f 100644 --- a/engine/MUNGA_L4/L4GLASSWIN.cpp +++ b/engine/MUNGA_L4/L4GLASSWIN.cpp @@ -407,6 +407,52 @@ enum { LayoutOff = 0, LayoutLoad = 1, LayoutSave = 2 }; static const char *layoutFileName = "glass_layout.cfg"; +// POD SURFACE MODE (BT_POD_SURFACES=1, 2026-08-06) -- the REAL pod bring-up +// shape. On the actual cab each MFD is its own 640x480 panel with PHYSICAL +// buttons and lamps around it, so the desktop chrome the glass windows draw +// (the RIO button bank + the frame) is exactly wrong there: it would paint +// fake buttons onto a panel that has real ones behind glass. In pod mode a +// window is cropped to its SURFACE, frameless, and the bank is dropped: +// one clean picture per physical panel, positioned by 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) +static int + PodSurfaceMode() +{ + static int m = -1; + if (m < 0) + { + const char *e = getenv("BT_POD_SURFACES"); + m = (e != NULL && *e != '\0' && *e != '0') ? 1 : 0; + if (m) + DEBUG_STREAM << "[glasswin] POD SURFACE MODE: bare surfaces, no " + "button banks (physical pod buttons assumed)\n" << std::flush; + } + return m; +} + +// Crop a built window to its surface: drop the bank, move the picture to the +// client origin, go frameless. Called at the end of each Build* when pod mode +// is on (and from LoadLayout for a per-window ",bare"). +static void + MakeBareSurface(GWin &w) +{ + if (w.portPrimary == NULL) // Flight Controls -- nothing to show + { + w.clientW = w.clientH = 0; + w.buttonCount = 0; + return; + } + const int sw = w.surfaceRect.right - w.surfaceRect.left; + const int sh = w.surfaceRect.bottom - w.surfaceRect.top; + w.buttonCount = 0; // the pod's buttons are real + SetRect(&w.surfaceRect, 0, 0, sw, sh); + w.clientW = sw; + w.clientH = sh; + w.noFrame = 1; +} + // A framed window is a normal tool window; ",noframe" makes it a bare // WS_POPUP -- no caption, no border, nothing but the display and its buttons. static DWORD @@ -580,16 +626,15 @@ static void // Trailing options after the numbers, comma-separated. Unknown ones // are ignored (so an older build reading a newer file just loses the // option, never the line) -- the bindings.txt grammar rule. - int noframe = 0; + int noframe = 0, bare = 0; for (const char *opt = strchr(eq + 1, ','); opt != NULL; opt = strchr(opt + 1, ',')) { const char *t = opt + 1; while (*t == ' ' || *t == '\t') ++t; if (_strnicmp(t, "noframe", 7) == 0) - { noframe = 1; - break; - } + else if (_strnicmp(t, "bare", 4) == 0) + bare = 1; // pod panel: surface only (see PodSurfaceMode) } GWin *gw = FindGWinByTitle(s); @@ -598,6 +643,8 @@ static void gw->wantX = x; gw->wantY = y; gw->noFrame = noframe; + if (bare && gw->buttonCount > 0) + MakeBareSurface(*gw); // per-window pod panel (mixed rigs) gw->restored = 1; ++restored; } @@ -1090,6 +1137,16 @@ void BuildFlight(gWins[6]); // 0x38-0x47 gWinCount = 7; + // POD SURFACE MODE: crop every display to its picture and drop the Flight + // Controls pad (it is pure desktop chrome -- the cab has a real stick). + if (PodSurfaceMode()) + { + for (int i = 0; i < gWinCount; ++i) + MakeBareSurface(gWins[i]); + if (gWins[6].portPrimary == NULL) + gWinCount = 6; // drop Flight Controls + } + titleFont = CreateFontW(-11, 0, 0, 0, FW_BOLD, 0, 0, 0, DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI"); buttonFont = CreateFontW(-10, 0, 0, 0, FW_NORMAL, 0, 0, 0, @@ -1145,6 +1202,37 @@ void SetLayeredWindowAttributes(w.hwnd, 0, 255, LWA_ALPHA); SetTimer(w.hwnd, RepaintTimerId, RepaintMilliseconds, NULL); ShowWindow(w.hwnd, SW_SHOWNOACTIVATE); + + // PLACEMENT RECEIPT (pod bring-up, 2026-08-06): which PHYSICAL monitor + // each display actually landed on. On a real cab -- or any remote + // session -- nobody can see all seven surfaces at once, so the log is + // the only way to confirm the map without walking around the pod. + { + RECT wr = { 0, 0, 0, 0 }; + GetWindowRect(w.hwnd, &wr); + HMONITOR mon = MonitorFromWindow(w.hwnd, MONITOR_DEFAULTTONEAREST); + MONITORINFOEXA mi; + memset(&mi, 0, sizeof(mi)); + mi.cbSize = sizeof(mi); + const char *devName = "?"; + int mx = 0, my = 0, mw = 0, mh = 0, primary = 0; + if (mon != NULL && GetMonitorInfoA(mon, (MONITORINFO *)&mi)) + { + devName = mi.szDevice; + mx = mi.rcMonitor.left; my = mi.rcMonitor.top; + mw = mi.rcMonitor.right - mi.rcMonitor.left; + mh = mi.rcMonitor.bottom - mi.rcMonitor.top; + primary = (mi.dwFlags & MONITORINFOF_PRIMARY) ? 1 : 0; + } + DEBUG_STREAM << "[glasswin] '" << w.title << "' surface=" + << (w.portPrimary ? w.portPrimary : "-") + << " at " << wr.left << "," << wr.top + << " " << (wr.right - wr.left) << "x" << (wr.bottom - wr.top) + << (w.noFrame ? " bare" : " framed") + << " -> monitor " << devName + << " (" << mx << "," << my << " " << mw << "x" << mh + << (primary ? ", PRIMARY)" : ")") << "\n" << std::flush; + } } DEBUG_STREAM << "[glasswin] per-display cockpit up (" << gWinCount diff --git a/tools/podprobe.ps1 b/tools/podprobe.ps1 new file mode 100644 index 0000000..46161eb --- /dev/null +++ b/tools/podprobe.ps1 @@ -0,0 +1,150 @@ +# podprobe.ps1 -- pod / crash-cart display + I/O topology probe. +# +# Run ON THE POD PC (PowerShell, no install, no admin): +# powershell -ExecutionPolicy Bypass -File podprobe.ps1 +# +# Prints and writes podprobe.txt: GPUs, every monitor with its VIRTUAL DESKTOP +# rect (the coordinates glass_layout.cfg needs), EDID make/model (to identify +# the original pod panels), serial ports (the RIO board), and a PROPOSED +# glass_layout.cfg mapping the six pod surfaces onto the non-primary monitors. +# +# Then, in the game's working directory: +# copy the proposed glass_layout.cfg in +# set BT_GLASS=1 & set BT_GLASS_PANELS=1 & set BT_POD_SURFACES=1 +# set BT_GLASS_LAYOUT=load (add =save to persist drags instead) +# btl4.exe -platform pod +# The log prints one "[glasswin] '<title>' ... -> monitor \\.\DISPLAYn" line per +# surface: that is the receipt that a picture landed on the right panel. + +$ErrorActionPreference = 'Continue' +$out = New-Object System.Collections.ArrayList +function W($s) { [void]$out.Add($s); Write-Host $s } + +W "==================== BT411 POD PROBE ====================" +W ("host : {0}" -f $env:COMPUTERNAME) +W ("when : {0}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) +W ("windows : {0}" -f (Get-CimInstance Win32_OperatingSystem).Caption) +W "" + +W "---- GPUs ----" +try { + Get-CimInstance Win32_VideoController | ForEach-Object { + W (" {0}" -f $_.Name) + W (" driver {0} ({1}) mode {2}x{3} ram {4} MB" -f ` + $_.DriverVersion, $_.DriverDate, $_.CurrentHorizontalResolution, + $_.CurrentVerticalResolution, [int]($_.AdapterRAM / 1MB)) + } +} catch { W " (video controller query failed: $_)" } +W "" + +W "---- MONITORS (virtual-desktop rects -- these are the layout coordinates) ----" +Add-Type -AssemblyName System.Windows.Forms | Out-Null +$screens = [System.Windows.Forms.Screen]::AllScreens +$i = 0 +foreach ($s in $screens) { + $b = $s.Bounds + W (" [{0}] {1}{2} x={3} y={4} {5}x{6} bpp={7}" -f ` + $i, $s.DeviceName, $(if ($s.Primary) { " *PRIMARY*" } else { "" }), + $b.X, $b.Y, $b.Width, $b.Height, $s.BitsPerPixel) + $i++ +} +W (" total screens: {0}" -f $screens.Count) +W "" + +W "---- EDID identity (which physical panel is which) ----" +try { + Get-CimInstance -Namespace root\wmi -ClassName WmiMonitorID | ForEach-Object { + $mk = -join ($_.ManufacturerName | Where-Object { $_ -ne 0 } | ForEach-Object { [char]$_ }) + $nm = -join ($_.UserFriendlyName | Where-Object { $_ -ne 0 } | ForEach-Object { [char]$_ }) + $sn = -join ($_.SerialNumberID | Where-Object { $_ -ne 0 } | ForEach-Object { [char]$_ }) + W (" {0} | make={1} model='{2}' serial={3} year={4}" -f ` + $_.InstanceName, $mk, $nm, $sn, $_.YearOfManufacture) + } +} catch { W " (EDID query failed -- normal over some remote sessions: $_)" } +W "" + +W "---- SERIAL PORTS (the RIO board lives on one of these) ----" +try { + $sp = Get-CimInstance Win32_SerialPort + if ($sp) { $sp | ForEach-Object { W (" {0} {1}" -f $_.DeviceID, $_.Name) } } + else { + $reg = 'HKLM:\HARDWARE\DEVICEMAP\SERIALCOMM' + if (Test-Path $reg) { + (Get-ItemProperty $reg).PSObject.Properties | + Where-Object { $_.Name -notlike 'PS*' } | + ForEach-Object { W (" {0} -> {1}" -f $_.Name, $_.Value) } + } else { W " (none found)" } + } +} catch { W " (serial query failed: $_)" } +W "" + +W "---- REMOTE SESSION ----" +W (" SESSIONNAME={0} (a console session drives the real panels; an RDP" -f $env:SESSIONNAME) +W " session gets a VIRTUAL display and will NOT light the pod monitors." +W " Chrome Remote Desktop attaches to the CONSOLE -- that is what we want." +W "" + +# ---------------- proposed layout ---------------- +# Pod surface roles, in the order the cab reads them. Sizes are the native +# surface sizes the game creates in BT_POD_SURFACES mode. +$roles = @( + @{ title = 'Heat MFD'; w = 640; h = 480; note = 'upper LEFT (coolant)' }, + @{ title = 'Engineering'; w = 640; h = 480; note = 'upper CENTER (engineering)' }, + @{ title = 'Comm MFD'; w = 640; h = 480; note = 'upper RIGHT (hot box / comm)' }, + @{ title = 'Left Weapons'; w = 640; h = 480; note = 'lower LEFT (weapons)' }, + @{ title = 'Right Weapons'; w = 640; h = 480; note = 'lower RIGHT (weapons)' }, + @{ title = 'Secondary / Radar'; w = 480; h = 640; note = 'secondary screen (radar, portrait)' } +) + +$targets = @($screens | Where-Object { -not $_.Primary } | Sort-Object { $_.Bounds.Y }, { $_.Bounds.X }) +W "---- PROPOSED glass_layout.cfg ----" +if ($targets.Count -eq 0) { + W " ! No non-primary monitors found. Either the pod panels are not attached to" + W " this PC, or this is a remote/virtual display session. Nothing to map yet." +} else { + W (" {0} non-primary monitor(s) available; assigning in top-to-bottom, left-to-right order." -f $targets.Count) + if ($targets.Count -lt $roles.Count) { + W (" ! Fewer panels than surfaces -- the last {0} surface(s) stay on the primary display." -f ($roles.Count - $targets.Count)) + } +} + +$cfg = New-Object System.Collections.ArrayList +[void]$cfg.Add('# BT411 glass cockpit window layout -- generated by tools/podprobe.ps1') +[void]$cfg.Add('# "<title>=x,y,w,h,noframe,bare" (bare = surface only, no desktop buttons)') +[void]$cfg.Add('# Coordinates are VIRTUAL DESKTOP pixels; run the probe again after any') +[void]$cfg.Add('# display rearrangement, because Windows renumbers the desktop origin.') +for ($r = 0; $r -lt $roles.Count; $r++) { + $role = $roles[$r] + if ($r -lt $targets.Count) { + $b = $targets[$r].Bounds + # centre the surface on the panel (exact fit when the panel is 640x480) + $x = $b.X + [int](($b.Width - $role.w) / 2) + $y = $b.Y + [int](($b.Height - $role.h) / 2) + $line = "{0}={1},{2},{3},{4},noframe,bare" -f $role.title, $x, $y, $role.w, $role.h + [void]$cfg.Add($line) + W (" {0,-18} -> {1} ({2}x{3}) at {4},{5} [{6}]" -f ` + $role.title, $targets[$r].DeviceName, $b.Width, $b.Height, $x, $y, $role.note) + } else { + [void]$cfg.Add(("# {0}=<x>,<y>,{1},{2},noframe,bare # no panel assigned" -f $role.title, $role.w, $role.h)) + } +} + +$cfgPath = Join-Path (Get-Location) 'glass_layout.cfg.proposed' +$cfg | Set-Content -Path $cfgPath -Encoding ASCII +W "" +W (" wrote {0}" -f $cfgPath) +W " -> review it, rename to glass_layout.cfg in the game's working directory." +W "" +W "---- NOTES ----" +W " * The pod's main 3D view is the GAME's own window (not a glass panel); put it" +W " on the main-view monitor with the normal window/fullscreen controls." +W " * 640x480 panels: if Windows does not offer that mode, the surface still" +W " renders 640x480 inside whatever mode the panel runs -- centred, not scaled." +W " * The 1995 rig spanned the five MFDs as ONE 1280x480 surface via NVIDIA" +W " Horizontal Span, which modern drivers removed. This per-panel window path" +W " replaces it and needs no special driver." + +$txt = Join-Path (Get-Location) 'podprobe.txt' +$out | Set-Content -Path $txt -Encoding ASCII +Write-Host "" +Write-Host ("full report written to {0}" -f $txt)