From f338595685dca67e4d762d91cd46eede7bb7d09a Mon Sep 17 00:00:00 2001 From: Cyd Date: Mon, 20 Jul 2026 14:43:40 -0500 Subject: [PATCH] Glass cockpit: per-display windows (BT_GLASS_PANELS) Break the desktop glass cockpit's secondary displays out of the single combined pad panel + D3D gauge strip into ONE window per pod display, each carrying that display's surface with its RIO button bank, arranged pod-faithfully around the main view. New TU engine/MUNGA_L4/L4GLASSWIN.* (BT_GLASS-only), selected by the -platform glass preset via the new runtime gate BT_GLASS_PANELS (=0 falls back to the legacy single panel + dock). - Surfaces are CPU-expanded from the shared gauge buffer (SVGA16::ExpandPlaneToBGRA, no D3D) and StretchDIBits'd in -- the D3D dev-composite path (dock / window / overlay) stands down while active. - Buttons sit UNDER the imagery: big hidden click targets that reach into the display, with only a small protruding edge showing as a lamp light. Red MFD banks tile the top/bottom; radar rails run the sides + a bottom row; a Flight Controls window hosts the no-display banks. - Windows: Heat / Engineering / Comm / Left Weapons / Right Weapons / radar + Flight Controls; edge-to-edge (no in-client margin/title; OS caption labels each). - Fix blank surfaces: application/ghWnd are duplicate-defined and bind non-deterministically under /FORCE, so the fresh L4GLASSWIN TU read NULL. Reach the renderer/window via BTResolveGaugeRenderer/BTResolveMainWindow defined in btl4main.cpp off the real btl4App/hWnd pointers. New gotcha: reconstruction-gotchas.md S6 duplicate-GLOBAL corollary. Co-Authored-By: Claude Opus 4.8 --- CMakeLists.txt | 1 + context/glass-cockpit.md | 30 +- context/reconstruction-gotchas.md | 14 + docs/GLASS_COCKPIT.md | 90 ++++ engine/MUNGA_L4/L4GLASSWIN.cpp | 831 ++++++++++++++++++++++++++++++ engine/MUNGA_L4/L4PADRIO.cpp | 14 +- engine/MUNGA_L4/L4VB16.cpp | 105 ++++ engine/MUNGA_L4/L4VB16.h | 20 + engine/MUNGA_L4/l4glasswin.h | 41 ++ game/btl4main.cpp | 39 +- 10 files changed, 1175 insertions(+), 10 deletions(-) create mode 100644 engine/MUNGA_L4/L4GLASSWIN.cpp create mode 100644 engine/MUNGA_L4/l4glasswin.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 77a1f3b..935724d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -253,6 +253,7 @@ if(BT_GLASS) "engine/MUNGA_L4/L4PADRIO.cpp" "engine/MUNGA_L4/L4PADBINDINGS.cpp" "engine/MUNGA_L4/L4PADPANEL.cpp" + "engine/MUNGA_L4/L4GLASSWIN.cpp" "engine/MUNGA_L4/L4PLASMAWIN.cpp" ) endif() diff --git a/context/glass-cockpit.md b/context/glass-cockpit.md index 2b16ac4..02a0e70 100644 --- a/context/glass-cockpit.md +++ b/context/glass-cockpit.md @@ -18,7 +18,7 @@ every trade-off. | Gate | Default | Adds | |---|---|---| -| `BT_GLASS` | OFF | PadRIO input (XInput+keyboard), on-screen cockpit button panel, desktop plasma window, `glass` platform preset, miniconsole mission launcher | +| `BT_GLASS` | OFF | PadRIO input (XInput+keyboard), per-display cockpit windows (or the legacy single button panel), desktop plasma window, `glass` platform preset, miniconsole mission launcher | | `BT_STEAM` | OFF (requires `BT_GLASS`) | Steam transport (`ISteamNetworkingSockets`) + `ISteamMatchmaking` lobby | - A **no-flags configure is the pure pod game** — the build of record. `cmake` options are @@ -85,6 +85,34 @@ copied from them. unbound keys keep their authentic meanings (5/z presets, t-o pilot select, +/- zoom). In BAS control mode the pod turns with the STICK (Q/E) and pedals are inactive — authentic; A/D turn in MID/ADV. Full audit + census: `docs/GLASS_COCKPIT.md` §2026-07-20. +- **Per-display cockpit windows (`BT_GLASS_PANELS`): DONE 2026-07-20 [T2 startup-verified].** + The `-platform glass` preset now breaks the secondary displays out of the single combined + panel/strip into ONE desktop window PER pod display, each carrying that display's surface + WITH its RIO button bank placed around it, arranged pod-faithfully around the main view + (`engine/MUNGA_L4/L4GLASSWIN.*`, new TU). Windows: **Heat (UL) / Mfd2 (UC) / Comm (UR) / + Mfd1 (LL) / Mfd3 (LR)** each with 8 RED MFD buttons split 4 above / 4 below the surface; + **Secondary/Radar** (portrait `sec`) with the 12 YELLOW Secondary buttons split left/right; + a **Flight Controls** window for the no-display banks (throttle 0x38-3F, joystick/fire + 0x40-47, panic). Each window is a GDI window (like [[gauges-hud]]'s pad panel + the plasma + window) — the surface is CPU-expanded from the shared gauge buffer + (`SVGA16::ExpandPlaneToBGRA`, no D3D) and `StretchDIBits`'d in, so the **whole D3D + dev-composite path (dock / separate window / overlay) stands down** while these are up + (`BTDrawGaugeInset`/`BTGaugeWindowRenderAndPresent` early-return; no `gBTGaugeDockBottom` + strip). Runtime gate **`BT_GLASS_PANELS`** (default ON under `-platform glass`, `=0` falls + back to the single pad panel + docked gauges) — read by `L4PADRIO` (which panel to create), + `L4VB16` (suppress compositing), and `btl4main` (skip the world-window dock strip). The + display↔bank map (spine of the feature): Heat 0x28-2F, Mfd2 0x20-27, Comm 0x30-37, + Mfd1 0x08-0F, Mfd3 0x00-07, sec 0x10-1B ([[pod-hardware]] §Bank→MFD). Verified: build + + bounded glass launch — `[glasswin] per-display cockpit up (7 windows…)`, no crash, full + mission loop un-regressed; surfaces carry LIVE gauge content (`BT_GLASS_LOG` = per-surface + resolve/pixel diagnostic). Layout constants (surface sizes, exact button sub-positions, + yellow left/right split) are marked tunable in `L4GLASSWIN.cpp`. Detail: `docs/GLASS_COCKPIT.md`. + ⚠ **Bug found + fixed during bring-up:** surfaces were blank because `application`/`ghWnd` are + duplicate-defined globals that bind non-deterministically under `/FORCE` — the fresh + `L4GLASSWIN` TU read `application == NULL` → gauge renderer NULL. Fix: reach the + renderer/window via `BTResolveGaugeRenderer()`/`BTResolveMainWindow()`, DEFINED in + `game/btl4main.cpp` off its real `btl4App`/`hWnd` pointers (never the globals directly). See + [[reconstruction-gotchas]] §6 duplicate-GLOBAL corollary. ## Key Relationships - Extends: [[pod-hardware]] (RIO input path) · Uses: [[gauges-hud]] dev-gauge modes · diff --git a/context/reconstruction-gotchas.md b/context/reconstruction-gotchas.md index dc11e27..560901c 100644 --- a/context/reconstruction-gotchas.md +++ b/context/reconstruction-gotchas.md @@ -121,6 +121,20 @@ mislanch.cpp's extern but not projweap.cpp's → first AUTOCANNON shot AV'd). ** bridge-signature change, `grep -rn "extern .*"` and update every declaration; then grep the fresh link output for the symbol name** — the pre-existing LNK2019 wall camouflages new entries if you only eyeball it. +**Duplicate-GLOBAL corollary (glass per-display windows, 2026-07-20):** a global DEFINED in two +libs (the 1995 headers declare free globals without `inline`/`extern`, so `application`, +`ghWnd`, … exist in BOTH `munga_engine` and `bt410_l4`) links under `/FORCE:MULTIPLE` with +per-object binding that is **non-deterministic across links** — a given `.obj` can resolve +`application` to the copy the game ASSIGNS (correct) or to the other copy (stays NULL). A +NEWLY-ADDED engine TU is the classic victim: it read `application == NULL` forever while +`L4VB16.cpp` in the same lib read the assigned pointer, and a relink flipped which one was +right. Symptom: a feature that reads an engine global silently no-ops (here: the gauge +renderer came back NULL → blank glass surfaces), no crash, no link error. **Rule: from a fresh +TU, never touch these duplicate globals directly — resolve through a tiny accessor DEFINED in +the TU that OWNS the real pointer** (`game/btl4main.cpp` holds `btl4App`/`hWnd` as its own +file-scope pointers; `BTResolveGaugeRenderer()`/`BTResolveMainWindow()` there are always +correct). Do NOT "fix" it by adding the accessor to another engine TU — that binding is just as +random. [T2] ## 7. Dtor-epilogue rule — do not reconstruct compiler glue diff --git a/docs/GLASS_COCKPIT.md b/docs/GLASS_COCKPIT.md index bf3817d..e4ede09 100644 --- a/docs/GLASS_COCKPIT.md +++ b/docs/GLASS_COCKPIT.md @@ -285,3 +285,93 @@ cavern.egg`. This is every option the original console exposed for a BattleTech - dropzone (5, the `dropzone=` field): one, two, three, four, five ← was `one` - advancedDamage (0/1): the advanced damage model toggle ← was hardcoded `1` - vehicleValue: 1000 (const), loadzones=1 (const), bitmapindex (per-pilot ordinal) + +--- + +## 2026-07-20 — Per-display cockpit windows (BT_GLASS_PANELS) + +**Goal (Cyd):** stop collapsing the desktop cockpit into combined surfaces. Break each +SECONDARY display out into its OWN window carrying that display's surface WITH its RIO button +bank placed around it, arranged pod-faithfully around the main 3D view — a dev cockpit that +reads like the pod, so button/lamp/display behavior can be validated in position. + +**New TU: `engine/MUNGA_L4/L4GLASSWIN.{h,cpp}`** (BT_GLASS-only; added to the CMake BT_GLASS +`target_sources`). Builds SEVEN GDI windows and positions them around the main window: + +| Window | Surface (port) | RIO bank | Buttons | +|---|---|---|---| +| Heat MFD (UL) | `Heat` (0x4000) | 0x28-0x2F | 8 red, 4 above / 4 below | +| MFD 2 (UC) | `Mfd2`/`Eng2` (0x0400) | 0x20-0x27 | 8 red, 4 above / 4 below | +| Comm MFD (UR) | `Comm` (0x8000) | 0x30-0x37 | 8 red, 4 above / 4 below | +| MFD 1 (LL) | `Mfd1`/`Eng1` (0x0100) | 0x08-0x0F | 8 red, 4 above / 4 below | +| MFD 3 (LR) | `Mfd3`/`Eng3` (0x1000) | 0x00-0x07 | 8 red, 4 above / 4 below | +| Secondary/Radar | `sec` (palette, portrait) | 0x10-0x1B Secondary | 12 yellow, 6 left / 6 right | +| Flight Controls | (none) | 0x38-0x47 (+ panic/icom) | neutral, 2 columns | + +**Architecture — GDI + CPU blit, NO D3D for the secondaries.** Each window is a layered, +topmost GDI window (the L4PADPANEL pattern: double-buffered paint, ~62 ms repaint timer, +left-click = momentary / right-click = latch → `PadRIO::SetScreenButton`; lamps from +`PadRIO::GetLampState`). The surface is CPU-expanded from the shared gauge pixelBuffer by the +new **`SVGA16::ExpandPlaneToBGRA(mask, paletteID, monoTint, rotateQuadrant, dst, &ow, &oh)`** +(L4VB16.cpp) — the no-D3D analog of `DrawDevSurface`: palette-LUT expand for `sec` (monoTint +< 0), mono bit-plane → tint for the MFDs; the portrait secondary CRT is unrotated by +transposing into `dst` (rotate 1/3, `BT_GAUGE_SEC_ROT`) — then `StretchDIBits` (HALFTONE) into +the surface rect. An Mfd/Eng pair shows whichever plane is the live (non-`BlankColor`) one, as +on the pod monitor. This reuses the plasma-window CPU→window pattern and sidesteps the whole +D3D dev-composite bug family (additional-swap-chain lifetime, device-state save/restore, +depth-stencil mismatch, layered strobing). + +**Gate: `BT_GLASS_PANELS`** (runtime env; `BTGlassPanelsActive()`). Default ON under +`-platform glass` (preset putenv in `btl4main.cpp`); `=0` restores the legacy single pad panel ++ docked gauge strip. Wired at three seams: +- `L4PADRIO` ctor: `BTGlassPanelsActive()` → `BTGlassPanels_Create()` instead of + `BTPadPanel_Create()`; dtor destroys both (safe no-ops). +- `L4VB16`: `BTDrawGaugeInset` / `BTGaugeWindowRenderAndPresent` early-return when + `GlassPanelsOwnSurfaces()` (the per-display windows own the surfaces). +- `btl4main`: skip the `gBTGaugeDockBottom` + 1100×600 world-window forcing (world stays + normal size — no bottom strip). + +**Placement.** `ComputeLayout()` rings the windows around `ghWnd` (the main game window): +top row Heat/Mfd2/Comm above it, Mfd1/Mfd3 at the lower corners, radar below-center, Flight +Controls left of the seat; clamped to the work area, with an 800×600-centered fallback before +the main window exists + a one-shot reposition once `ghWnd` comes up (in the repaint timer). + +**Verified [T2 startup].** `-DBT_GLASS=ON` Debug build links (the usual `/FORCE` mech3 +subsystem set is pre-existing, unrelated). Bounded `-platform glass -egg DEV.EGG` launch: +`[glasswin] per-display cockpit up (7 windows: 5 MFD + radar + flight)`, `[plasmawin] … up`, +`[padrio] XInput pad found`, no failure/abort markers, full mission loop un-regressed (killed +after 18 s). Visual layout / surface content / click-through NOT yet human-verified. + +**Tunable (Cyd flagged refinements):** surface display sizes, the exact button sub-positions +around each screen, the yellow left/right split (first cut 0x10-0x15 / 0x16-0x1B), and the MFD +mono phosphor tint — all named constants at the top of `L4GLASSWIN.cpp`. Deferred: folding the +plasma window into the same arrangement; per-window draggable/saved positions; moving/resizing +the main window into a true center cell for a tighter ring. + +### Blank-surface bug (found + fixed same day) — /FORCE duplicate-global race + +First glass-panels run showed the buttons but BLANK display surfaces. `BT_GLASS_LOG` +diagnostics in `BlitSurface` traced it stage by stage: `application == 0x00000000` in +`L4GLASSWIN.cpp` for the ENTIRE run (call 1 → 481), so `application->GetGaugeRenderer()` was +never even reached. Yet the legacy dock path (`BTDrawGaugeSurfaces`, `L4VB16.cpp`, SAME engine +lib) logged `application=047BA9C8 gr=0C57FF78` — non-NULL. + +Root cause: the 1995 headers declare free globals (`application`, `ghWnd`, …) without +`inline`/`extern`, so they are DEFINED in both `munga_engine` and `bt410_l4`. Under +`/FORCE:MULTIPLE` the linker binds each `.obj`'s reference to one copy or the other +**non-deterministically per link** — the newly-added `L4GLASSWIN.obj` bound the copy the game +never assigns (stays NULL), and a relink even flipped `L4VB16.obj`. Routing through an engine +helper is therefore unreliable (its binding is equally random). + +Fix: `BTResolveGaugeRenderer()` / `BTResolveMainWindow()` (declared in `l4vb16.h`) are DEFINED +in `game/btl4main.cpp` off the launcher's OWN file-scope pointers — `btl4App` (set to `new_app` +at btl4main.cpp:631) and the global `hWnd` (the real main window). Those live in `btl4main.obj` +and are the actual assigned pointers, so the resolvers are always correct regardless of the +duplicate-symbol binding. `BlitSurface` and the placement code call the resolvers, never the +`application`/`ghWnd` globals. New durable gotcha: `context/reconstruction-gotchas.md` §6 +duplicate-GLOBAL corollary. + +Verified after the fix (`BT_GLASS_LOG`): `[glass] 'Heat MFD' port=Heat mask=0x4000 pal=3 +enable=2 ow=640 oh=480 nonzero=22227/307200`, `[glass] 'Secondary / Radar' port=sec mask=0x3f +pal=1 enable=3 ow=480 oh=640 nonzero=26206/307200` — surfaces resolve and carry live pixels. +`BT_GLASS_LOG` left in as a permanent per-surface resolve/pixel diagnostic (throttled). diff --git a/engine/MUNGA_L4/L4GLASSWIN.cpp b/engine/MUNGA_L4/L4GLASSWIN.cpp new file mode 100644 index 0000000..fdf0746 --- /dev/null +++ b/engine/MUNGA_L4/L4GLASSWIN.cpp @@ -0,0 +1,831 @@ +#include "mungal4.h" +#pragma hdrstop + +//########################################################################### +// L4GLASSWIN -- per-display glass cockpit windows. Design + layout rules: +// l4glasswin.h. Each pod secondary display is its OWN GDI window carrying +// the display surface (CPU-blit from the shared gauge buffer) with its RIO +// button bank placed around it; a Flight Controls window hosts the +// no-display banks. Modeled on L4PADPANEL (GDI cells + lamp decode) and +// L4PLASMAWIN (CPU pixelBuffer -> StretchDIBits), so no D3D is involved. +//########################################################################### + +#include "l4glasswin.h" +#include "l4padrio.h" +#include "l4vb16.h" +#include "../munga/gaugrend.h" + +#include +#include +#include +#include + +// The main game window (for pod-faithful placement) is reached via +// BTResolveMainWindow() (l4vb16.h) -- NOT the `ghWnd` global directly, which this +// newly-added engine TU can bind as the NULL /FORCE-duplicate copy. + +//########################################################################### +// Mode gate +//########################################################################### + +int + BTGlassPanelsActive() +{ + static int v = -1; + if (v < 0) + { + const char *e = getenv("BT_GLASS_PANELS"); + v = (e != NULL && e[0] != '0') ? 1 : 0; + } + return v; +} + +//########################################################################### +// Layout constants (tunable -- Cyd flagged refinements to the exact button +// sub-positions + the yellow left/right split). +//########################################################################### + +enum +{ + Margin = 0, // no padding around the edges (surface fills the window) + TitleH = 0, // no in-client title band (the OS caption labels the window) + CellW = 58, // default button cell (flight-controls window) + CellH = 28, + RedCellH = 128, // red MFD buttons -- tall, extend ~100px into the display + EdgeProtrude = 10, // ...and this far OUT past the display edge + Gap = 4, + RailW = 40, // radar side-rail button thickness (rotated 90deg, fill height) + RailSlotAdd = 1, // rail pitch = 640/6 + 1 = 107 -> 104px buttons + 3px gaps = 639 (fits 640) + RailGap = 3, // vertical gap between rail buttons (own value; red/horizontals use Gap) + RedOffsetX = 5, // nudge the red MFD buttons right to line up with the imagery + MfdSurfW = 640, MfdSurfH = 480, // mono MFD surface at native 640x480 + RadarSurfW = 480, RadarSurfH = 640, // portrait secondary/radar CRT at native (rotated 640x480) + RepaintTimerId = 1, + RepaintMilliseconds = 62, // == L4PADPANEL (half the fastest lamp half-period) + MfdMonoTint = 0x00FFFFFF // mono MFD phosphor colour (BGRA); tunable +}; + +enum ColorClass { ClrRed, ClrYellow, ClrBlue }; + +// Per-column horizontal nudge (px) for the 4 red MFD buttons, left->right, to line +// them up with the (unevenly-spaced) imagery legends. Tunable -- Cyd is dialing +// these in against DISPLAY / PROGRAM / etc. Applied on top of RedOffsetX. +static const int kRedColDX[4] = { -5, -4, 0, 0 }; + +struct GButton +{ + int address; + ColorClass color; + RECT rect; // client-space +}; + +struct GWin +{ + const char *title; + HWND hwnd; + + // Surface (portPrimary == NULL -> no surface, e.g. Flight Controls). + const char *portPrimary; + const char *portAlt; // the Eng preset plane sibling, or NULL + int monoTint; // -1 = palette-expand (radar); else BGRA mono tint + int rotate; // 0 / 1 / 3 (portrait secondary) + RECT surfaceRect; + + GButton buttons[24]; + int buttonCount; + int showLabels; // draw the hex/name on each button? (Flight Controls only) + + int clientW, clientH; + int frameW, frameH; + int wantX, wantY; +}; + +static GWin gWins[8]; +static int gWinCount = 0; + +static int pressedAddress = -1; +static unsigned char latched[128]; +static int gRadarRot = 3; // BT_GAUGE_SEC_ROT (default CW, upright) +static bool sRepositionedToMain = false; +static unsigned long *gStage = NULL; // 640*480 BGRA staging (shared, main-thread) + +static HFONT titleFont = NULL; +static HFONT buttonFont = NULL; +static HFONT subFont = NULL; + +//########################################################################### +// Named handle/joystick buttons (the Flight Controls window) +//########################################################################### + +struct NamedButton { int address; const char *name; }; +static const NamedButton namedButtons[] = +{ + { 0x3D, "Panic" }, { 0x3F, "Throttle" }, + { 0x40, "Main" }, { 0x41, "Hat Bk" }, { 0x42, "Hat Up" }, + { 0x43, "Hat R" }, { 0x44, "Hat L" }, { 0x45, "Pinky" }, + { 0x46, "Middle"}, { 0x47, "Upper" }, +}; +enum { NamedButtonCount = sizeof(namedButtons) / sizeof(namedButtons[0]) }; + +static const char * + PhysicalName(int address) +{ + for (int i = 0; i < NamedButtonCount; ++i) + if (namedButtons[i].address == address) + return namedButtons[i].name; + return NULL; +} + +//########################################################################### +// Lamp decode (identical to L4PADPANEL::LampBrightnessOf -- RIOBase::LampState) +//########################################################################### + +static int + LampBrightnessOf(int state, unsigned long tick) +{ + int level1 = (state >> 2) & 0x3; + int level2 = (state >> 4) & 0x3; + int level = (level1 > level2) ? level1 : level2; + if (level == 0) + return 0; + int flash = state & 0x3; + if (flash != 0) + { + unsigned long half_period = (flash == 1) ? 500 : (flash == 2) ? 250 : 125; + if ((tick / half_period) & 1) + return 0; + } + return level; +} + +//########################################################################### +// Layout builders +//########################################################################### + +static void + PlaceCellAt(GWin &w, int address, int x, int y, ColorClass color) +{ + GButton &b = w.buttons[w.buttonCount++]; + b.address = address; + b.color = color; + b.rect.left = x + 2; + b.rect.top = y + 2; + b.rect.right = x + CellW - 2; + b.rect.bottom = y + CellH - 2; +} + +static void + PlaceLine(GWin &w, int startAddr, int count, int step, + int x, int y, int horizontal, ColorClass color) +{ + for (int i = 0; i < count; ++i) + { + int cx = horizontal ? (x + i * CellW) : x; + int cy = horizontal ? y : (y + i * CellH); + PlaceCellAt(w, startAddr + i * step, cx, cy, color); + } +} + +// Place a button with an explicit size (custom-shaped cells: wide red buttons, +// tall rotated radar rails). +static void + PlaceRect(GWin &w, int address, int x, int y, int cw, int ch, ColorClass color) +{ + GButton &b = w.buttons[w.buttonCount++]; + b.address = address; + b.color = color; + b.rect.left = x; + b.rect.top = y; + b.rect.right = x + cw; + b.rect.bottom = y + ch; +} + +// An MFD display: 8 red buttons split 4 above / 4 below the surface (top row = +// the high 4 addresses descending, matching the L4PADPANEL cluster order). +static void + BuildMfd(GWin &w, const char *title, const char *portP, const char *portA, int bankHi) +{ + memset(&w, 0, sizeof(w)); + w.title = title; + w.portPrimary = portP; + w.portAlt = portA; + w.monoTint = MfdMonoTint; + w.rotate = 0; + + int surfX = Margin; + int surfY = Margin + TitleH + EdgeProtrude; // room for the top buttons to poke out + + // Buttons OVERLAY the display: 4 tile the full width, reaching RedCellH INTO the + // display and EdgeProtrude OUT past the edge (top row + bottom row; hi addresses + // descending -- L4PADPANEL order). + int slotW = MfdSurfW / 4; + for (int i = 0; i < 4; ++i) + { + int x = surfX + i * slotW + RedOffsetX + kRedColDX[i]; + PlaceRect(w, bankHi - i, x, surfY - EdgeProtrude, slotW - Gap, RedCellH + EdgeProtrude, ClrRed); // top + PlaceRect(w, bankHi - 4 - i, x, surfY + MfdSurfH - RedCellH, slotW - Gap, RedCellH + EdgeProtrude, ClrRed); // bottom + } + SetRect(&w.surfaceRect, surfX, surfY, surfX + MfdSurfW, surfY + MfdSurfH); + + w.clientW = Margin + MfdSurfW + Margin; + w.clientH = surfY + MfdSurfH + EdgeProtrude + Margin; +} + +// The secondary/radar display: 12 yellow Secondary buttons split left / right +// of the portrait surface (0x10-0x15 left, 0x16-0x1B right). +static void + BuildRadar(GWin &w) +{ + memset(&w, 0, sizeof(w)); + w.title = "Secondary / Radar"; + w.portPrimary = "sec"; + w.portAlt = NULL; + w.monoTint = -1; // palette + w.rotate = gRadarRot; + + int surfX = Margin + EdgeProtrude; // room for the left rail to poke out + int surfY = Margin + TitleH; + + // Rails: big clickable strips that reach RailW INTO the display (hidden under the + // imagery) and poke EdgeProtrude OUT past the side edge (the lamp light). + // Left 0x10..0x15, right 0x18 (top)..0x1D (bottom). (0x15 kept on the left rail + // -- Cyd listed 0x10-0x14; move it if wanted.) + int railCount = 6; + int slotH = RadarSurfH / railCount + RailSlotAdd; + int leftX = surfX - EdgeProtrude; + int rightX = surfX + RadarSurfW - RailW; + for (int i = 0; i < railCount; ++i) + { + int y = surfY + i * slotH; + PlaceRect(w, 0x10 + i, leftX, y, EdgeProtrude + RailW, slotH - RailGap, ClrYellow); + PlaceRect(w, 0x18 + i, rightX, y, RailW + EdgeProtrude, slotH - RailGap, ClrYellow); + } + SetRect(&w.surfaceRect, surfX, surfY, surfX + RadarSurfW, surfY + RadarSurfH); + + // Bottom row: 16, 17, 1F, 1E (left to right), BETWEEN the rails -- reaches CellH + // up into the display, pokes EdgeProtrude out the bottom. + static const int bottomAddrs[4] = { 0x16, 0x17, 0x1F, 0x1E }; + int botLeft = surfX + RailW; + int botSpan = RadarSurfW - 2 * RailW; + int botW = botSpan / 4; + int botY = surfY + RadarSurfH - CellH; + for (int i = 0; i < 4; ++i) + PlaceRect(w, bottomAddrs[i], botLeft + i * botW, botY, botW - Gap, CellH + EdgeProtrude, ClrYellow); + + w.clientW = surfX + RadarSurfW + EdgeProtrude + Margin; + w.clientH = surfY + RadarSurfH + EdgeProtrude + Margin; +} + +// The Flight Controls window: the no-display banks -- throttle/panic/door/icom +// (0x38-0x3F) and joystick/fire (0x40-0x47), two neutral columns. +static void + BuildFlight(GWin &w) +{ + memset(&w, 0, sizeof(w)); + w.title = "Flight Controls"; + w.portPrimary = NULL; + w.showLabels = 1; // the only window that shows hex/name on its buttons + + int topY = Margin + TitleH; + int col1 = Margin; + int col2 = Margin + CellW + Gap; + + PlaceLine(w, 0x38, 8, +1, col1, topY, 0, ClrBlue); // throttle / panic / door / icom + PlaceLine(w, 0x40, 8, +1, col2, topY, 0, ClrBlue); // joystick + fire buttons + + w.clientW = col2 + CellW + Margin; + w.clientH = topY + 8 * CellH + Margin; +} + +//########################################################################### +// Placement -- pod-faithful ring around the main game window +//########################################################################### + +static void + WorkArea(RECT *wa) +{ + if (!SystemParametersInfoW(SPI_GETWORKAREA, 0, wa, 0)) + { + wa->left = 0; wa->top = 0; + wa->right = GetSystemMetrics(SM_CXSCREEN); + wa->bottom = GetSystemMetrics(SM_CYSCREEN); + } +} + +static bool + GetMainRect(RECT *out) +{ + HWND mh = (HWND)BTResolveMainWindow(); + if (mh != NULL && IsWindow(mh) && GetWindowRect(mh, out)) + return true; + // Fallback before the main window exists: assume 800x600 centered. + RECT wa; WorkArea(&wa); + int cw = 800, ch = 600; + out->left = (wa.left + wa.right - cw) / 2; + out->top = (wa.top + wa.bottom - ch) / 2; + out->right = out->left + cw; + out->bottom = out->top + ch; + return false; +} + +static void + ComputeLayout() +{ + RECT m; GetMainRect(&m); + RECT wa; WorkArea(&wa); + int gap = 10; + int cx = (m.left + m.right) / 2; + int cy = (m.top + m.bottom) / 2; + + for (int i = 0; i < gWinCount; ++i) + { + GWin &w = gWins[i]; + int fw = w.frameW, fh = w.frameH; + int x = m.left, y = m.top; + switch (i) + { + case 0: x = m.left; y = m.top - fh - gap; break; // Heat (UL) + case 1: x = cx - fw / 2; y = m.top - fh - gap; break; // Mfd2 (UC) + case 2: x = m.right - fw; y = m.top - fh - gap; break; // Comm (UR) + case 3: x = m.left - fw - gap; y = m.bottom - fh; break; // Mfd1 (LL) + case 4: x = m.right + gap; y = m.bottom - fh; break; // Mfd3 (LR) + case 5: x = cx - fw / 2; y = m.bottom + gap; break; // Radar (center) + case 6: x = m.left - fw - gap; y = cy - fh / 2; break; // Flight (left of seat) + } + // Clamp fully on-screen (small desktops -> panels pile at the edges). + if (x + fw > wa.right) x = wa.right - fw; + if (y + fh > wa.bottom) y = wa.bottom - fh; + if (x < wa.left) x = wa.left; + if (y < wa.top) y = wa.top; + w.wantX = x; + w.wantY = y; + } +} + +static void + ApplyLayout() +{ + for (int i = 0; i < gWinCount; ++i) + if (gWins[i].hwnd != NULL) + SetWindowPos(gWins[i].hwnd, HWND_TOPMOST, gWins[i].wantX, gWins[i].wantY, + 0, 0, SWP_NOSIZE | SWP_NOACTIVATE); +} + +//########################################################################### +// Painting +//########################################################################### + +static void + DrawLabelText(HDC dc, const char *text, HFONT font, RECT *rect, + COLORREF color, UINT format) +{ + WCHAR wide[32]; + int n = 0; + for (const char *s = text; *s && n < 31; ++s) + wide[n++] = (WCHAR)*s; + wide[n] = 0; + HFONT old_font = (HFONT)SelectObject(dc, font); + SetTextColor(dc, color); + DrawTextW(dc, wide, -1, rect, format); + SelectObject(dc, old_font); +} + +static int sGlassLog = -1; +static unsigned long sBlitCalls = 0; + +static void + BlitSurface(HDC dc, GWin *w) +{ + if (sGlassLog < 0) + sGlassLog = getenv("BT_GLASS_LOG") ? 1 : 0; + int log = sGlassLog && ((sBlitCalls++ % 240) == 0); + + // Reach the renderer via BTResolveGaugeRenderer (l4vb16.h; DEFINED in + // game/btl4main.cpp off the real btl4App pointer), NOT the `application` global + // directly -- a fresh engine TU can bind the NULL /FORCE-duplicate copy. + GaugeRenderer *gr = BTResolveGaugeRenderer(); + if (gr == NULL) + { + if (log) DEBUG_STREAM << "[glass] '" << w->title << "' gauge renderer not ready\n" << std::flush; + return; + } + + L4GraphicsPort *port = static_cast(gr->GetGraphicsPort(w->portPrimary)); + if (w->portAlt != NULL) + { + L4GraphicsPort *alt = static_cast(gr->GetGraphicsPort(w->portAlt)); + // Show whichever of the Mfd/Eng pair is the live (non-blank) plane, + // exactly like the pod monitor + BTDrawGaugeSurfaces (Gitea #9). + bool primaryBlank = (port == NULL) || + (port->GetEnableID() == L4GraphicsPort::BlankColor); + if (primaryBlank && alt != NULL && + alt->GetEnableID() != L4GraphicsPort::BlankColor) + port = alt; + else if (port == NULL) + port = alt; + } + if (port == NULL) + { + if (log) DEBUG_STREAM << "[glass] '" << w->title << "' port '" << w->portPrimary + << "' NULL (not installed)\n" << std::flush; + return; + } + + SVGA16 *svga = static_cast(port->graphicsDisplay); + if (svga == NULL || gStage == NULL) + { + if (log) DEBUG_STREAM << "[glass] '" << w->title << "' svga=" << (void*)svga + << " stage=" << (void*)gStage << "\n" << std::flush; + return; + } + + // Surface expand. BT_GLASS_MFD_PAL (experiment): render the mono MFD windows + // through the palette LUT (low byte, secondary palette) instead of the 1-bit + // mono tint, to compare. NB the MFD's own graphics live in a HIGH bit-plane + // the 256-entry palette can't index, so this shows the shared low-byte content. + int mask = port->GetBitMask(); + int palId = port->paletteID; + int tint = w->monoTint; + static int sMfdPal = -1; + if (sMfdPal < 0) sMfdPal = getenv("BT_GLASS_MFD_PAL") ? 1 : 0; + if (sMfdPal && tint >= 0) + { + tint = -1; // palette-expand + mask = 0xFF; // index the low (palette) byte + palId = SVGA16::SecondaryPalette; + } + int ow = 0, oh = 0; + svga->ExpandPlaneToBGRA(mask, palId, tint, w->rotate, gStage, &ow, &oh); + if (log) + { + int nz = 0, n = ow * oh; + for (int i = 0; i < n; ++i) if (gStage[i] != 0) nz++; + DEBUG_STREAM << "[glass] '" << w->title << "' port=" << w->portPrimary + << " mask=0x" << std::hex << port->GetBitMask() << std::dec + << " pal=" << port->paletteID << " enable=" << port->GetEnableID() + << " ow=" << ow << " oh=" << oh << " nonzero=" << nz << "/" << n + << "\n" << std::flush; + } + if (ow <= 0 || oh <= 0) + return; + + BITMAPINFO info; + memset(&info, 0, sizeof(info)); + info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + info.bmiHeader.biWidth = ow; + info.bmiHeader.biHeight = -oh; // top-down + info.bmiHeader.biPlanes = 1; + info.bmiHeader.biBitCount = 32; + info.bmiHeader.biCompression = BI_RGB; + + const RECT &r = w->surfaceRect; + SetStretchBltMode(dc, HALFTONE); + SetBrushOrgEx(dc, 0, 0, NULL); + StretchDIBits(dc, + r.left, r.top, r.right - r.left, r.bottom - r.top, + 0, 0, ow, oh, + gStage, &info, DIB_RGB_COLORS, SRCCOPY); +} + +static void + PaintGlass(HWND window, GWin *w) +{ + PAINTSTRUCT paint; + HDC winDC = BeginPaint(window, &paint); + unsigned long tick = GetTickCount(); + + RECT client; + GetClientRect(window, &client); + + // Double-buffer the whole frame (== L4PADPANEL issue #13). + HDC dc = CreateCompatibleDC(winDC); + HBITMAP backing = CreateCompatibleBitmap(winDC, + client.right - client.left, client.bottom - client.top); + HBITMAP oldBacking = (HBITMAP)SelectObject(dc, backing); + HBRUSH background = CreateSolidBrush(RGB(24, 24, 24)); + FillRect(dc, &client, background); + DeleteObject(background); + SetBkMode(dc, TRANSPARENT); + + // (No in-client title -- the OS window caption labels each window.) + + // Buttons are drawn FIRST (UNDER the display). Each is a big clickable rect; + // the surface blit further down masks the part inside the display, leaving only + // the bit that protrudes past the display edge visible -- a solid lamp light. + // (The Flight Controls window has no surface, so its buttons show in full.) + for (int i = 0; i < w->buttonCount; ++i) + { + const GButton &b = w->buttons[i]; + const RECT &r = b.rect; + int held = (b.address == pressedAddress) || latched[b.address & 0x7F]; + int shade = LampBrightnessOf(PadRIO::GetLampState(b.address), tick); + if (held) + shade = 3; + + COLORREF fill, text_color; + if (b.color == ClrYellow) + { + fill = (shade == 3) ? RGB(245, 210, 60) + : (shade != 0) ? RGB(140, 118, 38) + : RGB(70, 60, 24); + text_color = RGB(0, 0, 0); + } + else if (b.color == ClrBlue) + { + fill = (shade == 3) ? RGB(205, 228, 255) + : (shade != 0) ? RGB(110, 135, 180) + : RGB(70, 86, 120); + text_color = RGB(0, 0, 0); + } + else // ClrRed (MFD) + { + fill = (shade == 3) ? RGB(230, 70, 70) + : (shade != 0) ? RGB(120, 50, 50) + : RGB(64, 40, 40); + text_color = RGB(255, 255, 255); + } + // Solid lamp fill + a bright border. Under the imagery, only the protruding + // edge shows once the surface is blitted on top. + { + COLORREF bright = (b.color == ClrYellow) ? RGB(245, 210, 60) + : (b.color == ClrBlue) ? RGB(205, 228, 255) + : RGB(230, 70, 70); + HBRUSH face = CreateSolidBrush(fill); + FillRect(dc, &r, face); + DeleteObject(face); + HPEN pen = CreatePen(PS_SOLID, 1, bright); + HGDIOBJ oldPen = SelectObject(dc, pen); + HGDIOBJ oldBr = SelectObject(dc, GetStockObject(NULL_BRUSH)); + Rectangle(dc, r.left, r.top, r.right, r.bottom); + SelectObject(dc, oldBr); + SelectObject(dc, oldPen); + DeleteObject(pen); + } + + // Labels only on the Flight Controls window; MFD/radar buttons are bare. + if (w->showLabels) + { + char hex[8]; + sprintf(hex, "%02X", b.address); + const char *name = PhysicalName(b.address); + if (name != NULL) + { + RECT top_rect = { r.left, r.top + 1, r.right, (r.top + r.bottom) / 2 + 3 }; + RECT bottom_rect = { r.left, (r.top + r.bottom) / 2, r.right, r.bottom - 1 }; + DrawLabelText(dc, name, buttonFont, &top_rect, text_color, + DT_CENTER | DT_TOP | DT_SINGLELINE | DT_END_ELLIPSIS); + DrawLabelText(dc, hex, subFont, &bottom_rect, text_color, + DT_CENTER | DT_BOTTOM | DT_SINGLELINE); + } + else + { + RECT cell_rect = r; + DrawLabelText(dc, hex, buttonFont, &cell_rect, text_color, + DT_CENTER | DT_VCENTER | DT_SINGLELINE); + } + } + + // Latch = gold outline; momentary held = white outline (== L4PADPANEL). + if (latched[b.address & 0x7F]) + { + HBRUSH gold = CreateSolidBrush(RGB(255, 215, 0)); + RECT o = r; + FrameRect(dc, &o, gold); + DeleteObject(gold); + } + else if (held) + { + RECT o = r; + FrameRect(dc, &o, (HBRUSH)GetStockObject(WHITE_BRUSH)); + } + } + + // Surface (imagery) LAST -- ON TOP of the buttons, masking their inside so only + // the protruding lamp edges show. (No-op for the Flight Controls window.) + if (w->portPrimary != NULL) + { + BlitSurface(dc, w); + HPEN pen = CreatePen(PS_SOLID, 1, RGB(70, 70, 70)); + HPEN oldPen = (HPEN)SelectObject(dc, pen); + HBRUSH oldBrush = (HBRUSH)SelectObject(dc, GetStockObject(NULL_BRUSH)); + Rectangle(dc, w->surfaceRect.left - 1, w->surfaceRect.top - 1, + w->surfaceRect.right + 1, w->surfaceRect.bottom + 1); + SelectObject(dc, oldBrush); + SelectObject(dc, oldPen); + DeleteObject(pen); + } + + BitBlt(winDC, 0, 0, client.right - client.left, client.bottom - client.top, + dc, 0, 0, SRCCOPY); + SelectObject(dc, oldBacking); + DeleteObject(backing); + DeleteDC(dc); + EndPaint(window, &paint); +} + +//########################################################################### +// WndProc / input +//########################################################################### + +static int + HitTest(GWin *w, int x, int y) +{ + for (int i = 0; i < w->buttonCount; ++i) + { + const RECT &r = w->buttons[i].rect; + if (x >= r.left && x < r.right && y >= r.top && y < r.bottom) + return w->buttons[i].address; + } + return -1; +} + +static LRESULT CALLBACK + GlassWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) +{ + GWin *w = (GWin*)GetWindowLongPtrW(window, GWLP_USERDATA); + + switch (message) + { + case WM_PAINT: + if (w != NULL) { PaintGlass(window, w); return 0; } + break; + + case WM_ERASEBKGND: + return 1; // PaintGlass fills the frame (double-buffered) + + case WM_TIMER: + if (wparam == RepaintTimerId) + { + // One-shot: once the real main window is up, snap the ring onto it. + HWND mh = (HWND)BTResolveMainWindow(); + if (!sRepositionedToMain && mh != NULL && IsWindow(mh)) + { + ComputeLayout(); + ApplyLayout(); + sRepositionedToMain = true; + } + InvalidateRect(window, NULL, FALSE); + } + return 0; + + case WM_LBUTTONDOWN: + if (w != NULL) + { + int a = HitTest(w, (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam)); + if (a >= 0) + { + if (latched[a & 0x7F]) + { + latched[a & 0x7F] = 0; + PadRIO::SetScreenButton(a, 0); + } + else + { + pressedAddress = a; + SetCapture(window); + PadRIO::SetScreenButton(a, 1); + } + InvalidateRect(window, NULL, FALSE); + } + } + return 0; + + case WM_LBUTTONUP: + if (pressedAddress >= 0) + { + PadRIO::SetScreenButton(pressedAddress, 0); + pressedAddress = -1; + ReleaseCapture(); + InvalidateRect(window, NULL, FALSE); + } + return 0; + + case WM_RBUTTONDOWN: + if (w != NULL) + { + int a = HitTest(w, (int)(short)LOWORD(lparam), (int)(short)HIWORD(lparam)); + if (a >= 0) + { + latched[a & 0x7F] = latched[a & 0x7F] ? 0 : 1; + PadRIO::SetScreenButton(a, latched[a & 0x7F]); + InvalidateRect(window, NULL, FALSE); + } + } + return 0; + + case WM_CLOSE: + ShowWindow(window, SW_HIDE); // hide; the device owns the lifetime + return 0; + } + return DefWindowProcW(window, message, wparam, lparam); +} + +//########################################################################### +// Create / destroy +//########################################################################### + +void + BTGlassPanels_Create() +{ + 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). + { + const char *rv = getenv("BT_GAUGE_SEC_ROT"); + gRadarRot = (rv != NULL && rv[0] == '1') ? 1 : 3; + } + + memset(latched, 0, sizeof(latched)); + pressedAddress = -1; + sRepositionedToMain = false; + if (gStage == NULL) + gStage = new unsigned long[640 * 480]; + + // Build the seven windows. Order fixes the ComputeLayout roles (0..6). + BuildMfd (gWins[0], "Heat MFD", "Heat", NULL, 0x2F); // UL 0x28-0x2F + BuildMfd (gWins[1], "Engineering", "Mfd2", "Eng2", 0x27); // UC 0x20-0x27 + BuildMfd (gWins[2], "Comm MFD", "Comm", NULL, 0x37); // UR 0x30-0x37 + BuildMfd (gWins[3], "Left Weapons", "Mfd1", "Eng1", 0x0F); // LL 0x08-0x0F + BuildMfd (gWins[4], "Right Weapons", "Mfd3", "Eng3", 0x07); // LR 0x00-0x07 + BuildRadar(gWins[5]); // center 0x10-0x1B + BuildFlight(gWins[6]); // 0x38-0x47 + gWinCount = 7; + + 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, + DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI"); + subFont = CreateFontW(-9, 0, 0, 0, FW_NORMAL, 0, 0, 0, + DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI"); + + WNDCLASSW wc; + memset(&wc, 0, sizeof(wc)); + wc.lpfnWndProc = GlassWndProc; + wc.hInstance = GetModuleHandleW(NULL); + wc.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW); + wc.lpszClassName = L"BTGlassWin"; + RegisterClassW(&wc); + + DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX; + + // Frame sizes first (ComputeLayout positions by frame dims). + for (int i = 0; i < gWinCount; ++i) + { + RECT fr = { 0, 0, gWins[i].clientW, gWins[i].clientH }; + AdjustWindowRect(&fr, style, FALSE); + gWins[i].frameW = fr.right - fr.left; + gWins[i].frameH = fr.bottom - fr.top; + } + ComputeLayout(); + + for (int i = 0; i < gWinCount; ++i) + { + GWin &w = gWins[i]; + WCHAR wtitle[64]; + int n = 0; + for (const char *s = w.title; *s && n < 63; ++s) wtitle[n++] = (WCHAR)*s; + wtitle[n] = 0; + + // Layered + topmost (== L4PADPANEL): DWM composites the panel independently + // of the game's D3D swap chain, ending GDI-vs-D3D presentation strobing. + w.hwnd = CreateWindowExW( + WS_EX_TOPMOST | WS_EX_LAYERED, + L"BTGlassWin", wtitle, style, + w.wantX, w.wantY, w.frameW, w.frameH, + NULL, NULL, GetModuleHandleW(NULL), NULL); + if (w.hwnd == NULL) + { + DEBUG_STREAM << "[glasswin] CreateWindow failed for '" << w.title + << "'\n" << std::flush; + continue; + } + SetWindowLongPtrW(w.hwnd, GWLP_USERDATA, (LONG_PTR)&w); + SetLayeredWindowAttributes(w.hwnd, 0, 255, LWA_ALPHA); + SetTimer(w.hwnd, RepaintTimerId, RepaintMilliseconds, NULL); + ShowWindow(w.hwnd, SW_SHOWNOACTIVATE); + } + + DEBUG_STREAM << "[glasswin] per-display cockpit up (" << gWinCount + << " windows: 5 MFD + radar + flight)\n" << std::flush; +} + +void + BTGlassPanels_Destroy() +{ + for (int i = 0; i < gWinCount; ++i) + { + if (gWins[i].hwnd != NULL) + { + KillTimer(gWins[i].hwnd, RepaintTimerId); + DestroyWindow(gWins[i].hwnd); + gWins[i].hwnd = NULL; + } + } + gWinCount = 0; + if (titleFont) { DeleteObject(titleFont); titleFont = NULL; } + if (buttonFont) { DeleteObject(buttonFont); buttonFont = NULL; } + if (subFont) { DeleteObject(subFont); subFont = NULL; } + if (gStage) { delete[] gStage; gStage = NULL; } +} diff --git a/engine/MUNGA_L4/L4PADRIO.cpp b/engine/MUNGA_L4/L4PADRIO.cpp index 0f82e4a..ac17383 100644 --- a/engine/MUNGA_L4/L4PADRIO.cpp +++ b/engine/MUNGA_L4/L4PADRIO.cpp @@ -9,6 +9,7 @@ #include "l4padrio.h" #include "l4padpanel.h" +#include "l4glasswin.h" #include "l4ctrl.h" #include @@ -168,10 +169,16 @@ PadRIO::PadRIO(): << "live on focus; L4PADFLIP=" << flipStickAxes << ")\n" << std::flush; // - // The on-screen cockpit button panel (step 2d) rides the device: the - // glass preset sets BT_PAD_PANEL=1. + // The on-screen cockpit buttons ride the device. BT_GLASS_PANELS (the + // glass preset default) breaks each secondary display into its own window + // with its RIO bank around it; otherwise the single combined pad panel + // (BT_PAD_PANEL=1) is used. // - if (getenv("BT_PAD_PANEL") != NULL && *getenv("BT_PAD_PANEL") != '0') + if (BTGlassPanelsActive()) + { + BTGlassPanels_Create(); + } + else if (getenv("BT_PAD_PANEL") != NULL && *getenv("BT_PAD_PANEL") != '0') { BTPadPanel_Create(); } @@ -179,6 +186,7 @@ PadRIO::PadRIO(): PadRIO::~PadRIO() { + BTGlassPanels_Destroy(); // safe no-op if the glass windows were never created BTPadPanel_Destroy(); if (activeInstance == this) { diff --git a/engine/MUNGA_L4/L4VB16.cpp b/engine/MUNGA_L4/L4VB16.cpp index aebb191..603cd7c 100644 --- a/engine/MUNGA_L4/L4VB16.cpp +++ b/engine/MUNGA_L4/L4VB16.cpp @@ -6,6 +6,14 @@ #include "L4VIDEO.h" #include "DXUtils.h" +#ifdef BT_GLASS +#include "l4glasswin.h" // BTGlassPanelsActive(): the per-display windows own the surfaces +#endif +// BTResolveGaugeRenderer()/BTResolveMainWindow() (l4vb16.h) are DEFINED in +// game/btl4main.cpp off the real app/window pointers -- NOT here: an engine TU +// can bind the NULL /FORCE-duplicate copy of `application`/`ghWnd`, and which +// copy an obj binds is non-deterministic per link. + #if defined(TRACE_SCREEN_COPY) static BitTrace Screen_Copy("Screen Copy"); #define SET_SCREEN_COPY() Screen_Copy.Set() @@ -153,6 +161,18 @@ static bool DevGaugeDocked() return v != 0 || gBTGaugeDockBottom != 0; } +// GLASS per-display windows (L4GLASSWIN) own the instrument surfaces in their OWN +// GDI windows -- when active, the whole D3D dev-composite path (dock / separate +// window / overlay) must stand down so nothing double-draws into the main window. +static bool GlassPanelsOwnSurfaces() +{ +#ifdef BT_GLASS + return BTGlassPanelsActive() != 0; +#else + return false; +#endif +} + //===========================================================================// // DEV-COMPOSITE (option B) -- draw the woken gauge renderer's SECONDARY/radar // surface as an inset in the MAIN window. The gauge widgets have already @@ -260,6 +280,89 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa device->DrawPrimitiveUP(D3DPT_TRIANGLEFAN, 2, quad, sizeof(InsetVert)); } +//===========================================================================// +// GLASS per-display windows -- the CPU (no-D3D) analog of DrawDevSurface: expand +// one bit-plane of the shared gauge pixelBuffer into a 32-bit BGRA image that the +// GDI window blits with StretchDIBits (L4GLASSWIN.cpp). The rotation the D3D path +// did via texture-UV remap is baked in here by transposing into `dst`. +//===========================================================================// +void SVGA16::ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotateQuadrant, + unsigned long *dst, int *outW, int *outH) +{ + int w = pixelBuffer.Data.Size.x; // 640 + int h = pixelBuffer.Data.Size.y; // 480 + if (dst == NULL || w <= 0 || h <= 0) + { + if (outW) *outW = 0; + if (outH) *outH = 0; + return; + } + Word *base = pixelBuffer.Data.MapPointer; + SVGA16Palette *pal = &palette[paletteID]; + + if (rotateQuadrant == 0) + { + // Native orientation, top-down straight copy. + for (int y = 0; y < h; y++) + { + Word *src = base + y * w; + unsigned long *d = dst + y * w; + for (int x = 0; x < w; x++) + { + Word s = src[x]; + if (monoTint < 0) + { + PaletteTriplet *pe = &(pal->paletteData.Color[s & mask]); + d[x] = ((unsigned long)pe->Red << 16) | + ((unsigned long)pe->Green << 8) | (unsigned long)pe->Blue; + } + else + { + d[x] = (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). + int ow = h, oh = w; + for (int oy = 0; oy < oh; oy++) + { + unsigned long *d = dst + oy * ow; + for (int ox = 0; ox < ow; ox++) + { + int sx, sy; + if (rotateQuadrant == 3) // 90 CW: out(ox,oy) = src(oy, h-1-ox) + { + sx = oy; + sy = h - 1 - ox; + } + else // 90 CCW: out(ox,oy) = src(w-1-oy, ox) + { + sx = w - 1 - oy; + sy = ox; + } + Word s = base[sy * w + sx]; + 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 = ow; + if (outH) *outH = oh; +} + // // The pod's SIX instrument surfaces, all bit-plane views of the ONE shared 640x480 // gauge buffer (masks/positions from content/GAUGE/L4GAUGE.CFG MechInit @4395): @@ -401,6 +504,7 @@ void BTDrawGaugeSurfaces(LPDIRECT3DDEVICE9 device, float px, float py, float pw, void BTDrawGaugeInset(LPDIRECT3DDEVICE9 device) { if (!DevGaugeComposite() || !DevGaugeDocked() || device == NULL) return; + if (GlassPanelsOwnSurfaces()) return; // the per-display glass windows draw the surfaces if (gBTGaugeDockBottom) { // DOCK-BOTTOM: the strip owns the bottom band of the target (the world @@ -544,6 +648,7 @@ static bool BTGaugeWindowEnsure(LPDIRECT3DDEVICE9 device) void BTGaugeWindowRenderAndPresent(LPDIRECT3DDEVICE9 device) { if (!DevGaugeComposite() || DevGaugeDocked() || device == NULL) return; + if (GlassPanelsOwnSurfaces()) return; // the per-display glass windows draw the surfaces if (!BTGaugeWindowEnsure(device)) return; IDirect3DSurface9 *gaugeBB = NULL; diff --git a/engine/MUNGA_L4/L4VB16.h b/engine/MUNGA_L4/L4VB16.h index 55f56c2..d8670ce 100644 --- a/engine/MUNGA_L4/L4VB16.h +++ b/engine/MUNGA_L4/L4VB16.h @@ -7,6 +7,15 @@ class L4graphicsPort; +class GaugeRenderer; + +// GLASS: resolve the gauge renderer / main window. DEFINED in game/btl4main.cpp +// off the REAL app + window pointers it holds as locals -- NOT off the +// `application`/`ghWnd` globals, which are duplicate-defined and bind +// non-deterministically per link under /FORCE (the "duplicate-symbol race" -- +// CMakeLists.txt:186), so a newly-added engine TU (L4GLASSWIN) can read NULL. +GaugeRenderer *BTResolveGaugeRenderer(); +void *BTResolveMainWindow(); // HWND (kept void* -- no windows.h dependency) //######################################################################## //######################### Video16BitBuffered ########################### @@ -306,6 +315,17 @@ public: void DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int paletteID, int monoTint, float dstX, float dstY, float dstW, float dstH, int rotateCCW = 0); + + // GLASS (per-display windows, L4GLASSWIN): CPU-expand ONE gauge surface (a + // bit-plane of the shared pixelBuffer) into a 32-bit BGRA image for GDI + // StretchDIBits -- no D3D. Same pixel rule as DrawDevSurface: monoTint < 0 -> + // palette-expand via palette[paletteID]; else fill (word & mask) ? monoTint : 0 + // (monoTint is a 0x00RRGGBB value here, NOT R5G6B5). rotateQuadrant: 0 = native + // (outW=bufW, outH=bufH); 1 = 90 CCW / 3 = 90 CW -> transposed (outW=bufH, + // outH=bufW), for the portrait secondary CRT. `dst` must hold >= bufW*bufH + // dwords; the image is written TOP-DOWN. *outW/*outH receive the produced size. + void ExpandPlaneToBGRA(int mask, int paletteID, int monoTint, int rotateQuadrant, + unsigned long *dst, int *outW, int *outH); }; //######################################################################## diff --git a/engine/MUNGA_L4/l4glasswin.h b/engine/MUNGA_L4/l4glasswin.h new file mode 100644 index 0000000..caec3df --- /dev/null +++ b/engine/MUNGA_L4/l4glasswin.h @@ -0,0 +1,41 @@ +#pragma once + +//########################################################################### +// +// L4GLASSWIN -- per-display glass cockpit windows (BT_GLASS only; this TU is +// only in the build when the gate is on -- see CMakeLists.txt). +// +// Under `-platform glass` with BT_GLASS_PANELS=1 (the glass preset default), +// each pod SECONDARY display gets its OWN desktop window carrying that +// display's surface AND its RIO button bank placed AROUND it, mirroring the +// physical pod cockpit: +// +// Heat (UL) / Mfd2 (UC) / Comm (UR) / Mfd1 (LL) / Mfd3 (LR) -- red MFD +// banks split 4 buttons ABOVE / 4 BELOW the surface; +// sec radar (center, portrait) -- yellow +// Secondary buttons split LEFT / RIGHT of the surface; +// Flight Controls -- the +// no-display banks (throttle 0x38-3F, joystick/fire 0x40-47, panic). +// +// Each window is a GDI window (like L4PADPANEL + L4PLASMAWIN): the surface is +// CPU-expanded from the shared gauge pixelBuffer (SVGA16::ExpandPlaneToBGRA) +// and StretchDIBits'd in -- NO D3D, so the whole D3D dev-composite path +// (dock / separate window / overlay) stands down while these are up. Clicks +// inject through PadRIO::SetScreenButton; lamps render from GetLampState. +// Created / destroyed by the PadRIO ctor / dtor. +// +//########################################################################### + +void + BTGlassPanels_Create(); +void + BTGlassPanels_Destroy(); + +// +// True when BT_GLASS_PANELS mode is selected (default ON under `-platform +// glass`, OFF otherwise). Read by the render loop / L4VB16 dev-composite to +// suppress the single L4PADPANEL and the D3D gauge compositing. Cheap + +// cached; safe to call before the windows exist. +// +int + BTGlassPanelsActive(); diff --git a/game/btl4main.cpp b/game/btl4main.cpp index fba5caf..935a4a5 100644 --- a/game/btl4main.cpp +++ b/game/btl4main.cpp @@ -45,6 +45,22 @@ const char* const ProgName = "btl4"; Application *btl4App = NULL; HWND hWnd = NULL; +// GLASS: resolvers for the per-display windows (declared in l4vb16.h), defined +// HERE off the launcher's own app + window pointers. They deliberately do NOT +// touch the `application`/`ghWnd` globals: those are duplicate-defined and bind +// non-deterministically per link under /FORCE (CMakeLists.txt:186), so a fresh +// engine TU (L4GLASSWIN) can read the NULL copy. btl4App/hWnd live in THIS obj +// and are the real, assigned pointers -- always correct. +class GaugeRenderer; +GaugeRenderer *BTResolveGaugeRenderer() +{ + return btl4App ? btl4App->GetGaugeRenderer() : 0; +} +void *BTResolveMainWindow() +{ + return (void *)hWnd; +} + //===========================================================================// // Bring-up player DRIVE input (Tier 2 locomotion). // Populated by the keyboard handler in WndProc below (WASD / arrow keys) @@ -319,10 +335,14 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine // window / BT_DEV_GAUGES_DOCK=1 overlay inset), and the desktop // plasma window. Each only when not already set, so a developer // env overrides the preset. - if (getenv("L4CONTROLS") == NULL) putenv("L4CONTROLS=PAD,KEYBOARD"); - if (getenv("BT_DEV_GAUGES") == NULL) putenv("BT_DEV_GAUGES=1"); - if (getenv("L4PLASMA") == NULL) putenv("L4PLASMA=SCREEN"); - if (getenv("BT_PAD_PANEL") == NULL) putenv("BT_PAD_PANEL=1"); + if (getenv("L4CONTROLS") == NULL) putenv("L4CONTROLS=PAD,KEYBOARD"); + if (getenv("BT_DEV_GAUGES") == NULL) putenv("BT_DEV_GAUGES=1"); + if (getenv("L4PLASMA") == NULL) putenv("L4PLASMA=SCREEN"); + if (getenv("BT_PAD_PANEL") == NULL) putenv("BT_PAD_PANEL=1"); + // Break the secondary displays into per-display windows (surface + its RIO + // bank around it). Set BT_GLASS_PANELS=0 to fall back to the single + // combined pad panel + docked gauge strip. + if (getenv("BT_GLASS_PANELS") == NULL) putenv("BT_GLASS_PANELS=1"); } #endif else @@ -352,7 +372,7 @@ 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 + on-screen panel + dev MFDs + plasma window)" + : gBTPlatformGlass ? "GLASS (PadRIO + per-display cockpit windows [BT_GLASS_PANELS] + plasma window)" : "DEV (single window + keyboard)") << std::endl << std::flush; @@ -543,7 +563,14 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine { extern int gBTGaugeDockBottom; extern int BTGaugeStripHeightFor(int width); - if (getenv("BT_DEV_GAUGES") != 0 && getenv("BT_DEV_GAUGES_WINDOW") == 0) + // BT_GLASS_PANELS breaks the gauges into their own per-display windows, so + // the world window keeps its normal size (no bottom strip). + int glassOwnsGauges = 0; +#ifdef BT_GLASS + { extern int BTGlassPanelsActive(); glassOwnsGauges = BTGlassPanelsActive(); } +#endif + if (!glassOwnsGauges && + getenv("BT_DEV_GAUGES") != 0 && getenv("BT_DEV_GAUGES_WINDOW") == 0) { gBTGaugeDockBottom = 1; // Readability default (user-reported: the strip at 800 wide is a