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 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-20 14:43:40 -05:00
co-authored by Claude Opus 4.8
parent 157da70ec9
commit f338595685
10 changed files with 1175 additions and 10 deletions
+1
View File
@@ -253,6 +253,7 @@ if(BT_GLASS)
"engine/MUNGA_L4/L4PADRIO.cpp" "engine/MUNGA_L4/L4PADRIO.cpp"
"engine/MUNGA_L4/L4PADBINDINGS.cpp" "engine/MUNGA_L4/L4PADBINDINGS.cpp"
"engine/MUNGA_L4/L4PADPANEL.cpp" "engine/MUNGA_L4/L4PADPANEL.cpp"
"engine/MUNGA_L4/L4GLASSWIN.cpp"
"engine/MUNGA_L4/L4PLASMAWIN.cpp" "engine/MUNGA_L4/L4PLASMAWIN.cpp"
) )
endif() endif()
+29 -1
View File
@@ -18,7 +18,7 @@ every trade-off.
| Gate | Default | Adds | | 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 | | `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 - 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). 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; 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. 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 ## Key Relationships
- Extends: [[pod-hardware]] (RIO input path) · Uses: [[gauges-hud]] dev-gauge modes · - Extends: [[pod-hardware]] (RIO input path) · Uses: [[gauges-hud]] dev-gauge modes ·
+14
View File
@@ -121,6 +121,20 @@ mislanch.cpp's extern but not projweap.cpp's → first AUTOCANNON shot AV'd). **
bridge-signature change, `grep -rn "extern .*<name>"` and update every declaration; then grep bridge-signature change, `grep -rn "extern .*<name>"` and update every declaration; then grep
the fresh link output for the symbol name** — the pre-existing LNK2019 wall camouflages new the fresh link output for the symbol name** — the pre-existing LNK2019 wall camouflages new
entries if you only eyeball it. 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 ## 7. Dtor-epilogue rule — do not reconstruct compiler glue
+90
View File
@@ -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` - dropzone (5, the `dropzone=` field): one, two, three, four, five ← was `one`
- advancedDamage (0/1): the advanced damage model toggle ← was hardcoded `1` - advancedDamage (0/1): the advanced damage model toggle ← was hardcoded `1`
- vehicleValue: 1000 (const), loadzones=1 (const), bitmapindex (per-pilot ordinal) - 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).
+831
View File
@@ -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 <windows.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 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<n> 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<L4GraphicsPort*>(gr->GetGraphicsPort(w->portPrimary));
if (w->portAlt != NULL)
{
L4GraphicsPort *alt = static_cast<L4GraphicsPort*>(gr->GetGraphicsPort(w->portAlt));
// Show whichever of the Mfd<n>/Eng<n> 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<SVGA16*>(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; }
}
+11 -3
View File
@@ -9,6 +9,7 @@
#include "l4padrio.h" #include "l4padrio.h"
#include "l4padpanel.h" #include "l4padpanel.h"
#include "l4glasswin.h"
#include "l4ctrl.h" #include "l4ctrl.h"
#include <windows.h> #include <windows.h>
@@ -168,10 +169,16 @@ PadRIO::PadRIO():
<< "live on focus; L4PADFLIP=" << flipStickAxes << ")\n" << std::flush; << "live on focus; L4PADFLIP=" << flipStickAxes << ")\n" << std::flush;
// //
// The on-screen cockpit button panel (step 2d) rides the device: the // The on-screen cockpit buttons ride the device. BT_GLASS_PANELS (the
// glass preset sets BT_PAD_PANEL=1. // 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(); BTPadPanel_Create();
} }
@@ -179,6 +186,7 @@ PadRIO::PadRIO():
PadRIO::~PadRIO() PadRIO::~PadRIO()
{ {
BTGlassPanels_Destroy(); // safe no-op if the glass windows were never created
BTPadPanel_Destroy(); BTPadPanel_Destroy();
if (activeInstance == this) if (activeInstance == this)
{ {
+105
View File
@@ -6,6 +6,14 @@
#include "L4VIDEO.h" #include "L4VIDEO.h"
#include "DXUtils.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) #if defined(TRACE_SCREEN_COPY)
static BitTrace Screen_Copy("Screen Copy"); static BitTrace Screen_Copy("Screen Copy");
#define SET_SCREEN_COPY() Screen_Copy.Set() #define SET_SCREEN_COPY() Screen_Copy.Set()
@@ -153,6 +161,18 @@ static bool DevGaugeDocked()
return v != 0 || gBTGaugeDockBottom != 0; 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 // 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 // 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)); 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 // 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): // 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) void BTDrawGaugeInset(LPDIRECT3DDEVICE9 device)
{ {
if (!DevGaugeComposite() || !DevGaugeDocked() || device == NULL) return; if (!DevGaugeComposite() || !DevGaugeDocked() || device == NULL) return;
if (GlassPanelsOwnSurfaces()) return; // the per-display glass windows draw the surfaces
if (gBTGaugeDockBottom) if (gBTGaugeDockBottom)
{ {
// DOCK-BOTTOM: the strip owns the bottom band of the target (the world // 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) void BTGaugeWindowRenderAndPresent(LPDIRECT3DDEVICE9 device)
{ {
if (!DevGaugeComposite() || DevGaugeDocked() || device == NULL) return; if (!DevGaugeComposite() || DevGaugeDocked() || device == NULL) return;
if (GlassPanelsOwnSurfaces()) return; // the per-display glass windows draw the surfaces
if (!BTGaugeWindowEnsure(device)) return; if (!BTGaugeWindowEnsure(device)) return;
IDirect3DSurface9 *gaugeBB = NULL; IDirect3DSurface9 *gaugeBB = NULL;
+20
View File
@@ -7,6 +7,15 @@
class L4graphicsPort; 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 ########################### //######################### Video16BitBuffered ###########################
@@ -306,6 +315,17 @@ public:
void DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int paletteID, void DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int paletteID,
int monoTint, float dstX, float dstY, float dstW, float dstH, int monoTint, float dstX, float dstY, float dstW, float dstH,
int rotateCCW = 0); 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);
}; };
//######################################################################## //########################################################################
+41
View File
@@ -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();
+33 -6
View File
@@ -45,6 +45,22 @@ const char* const ProgName = "btl4";
Application *btl4App = NULL; Application *btl4App = NULL;
HWND hWnd = 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). // Bring-up player DRIVE input (Tier 2 locomotion).
// Populated by the keyboard handler in WndProc below (WASD / arrow keys) // 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 // window / BT_DEV_GAUGES_DOCK=1 overlay inset), and the desktop
// plasma window. Each only when not already set, so a developer // plasma window. Each only when not already set, so a developer
// env overrides the preset. // env overrides the preset.
if (getenv("L4CONTROLS") == NULL) putenv("L4CONTROLS=PAD,KEYBOARD"); if (getenv("L4CONTROLS") == NULL) putenv("L4CONTROLS=PAD,KEYBOARD");
if (getenv("BT_DEV_GAUGES") == NULL) putenv("BT_DEV_GAUGES=1"); if (getenv("BT_DEV_GAUGES") == NULL) putenv("BT_DEV_GAUGES=1");
if (getenv("L4PLASMA") == NULL) putenv("L4PLASMA=SCREEN"); if (getenv("L4PLASMA") == NULL) putenv("L4PLASMA=SCREEN");
if (getenv("BT_PAD_PANEL") == NULL) putenv("BT_PAD_PANEL=1"); 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 #endif
else else
@@ -352,7 +372,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
std::cout << "[boot] platform profile: " std::cout << "[boot] platform profile: "
<< (fe_menu_mode ? "MENU (front end -- no profile applied)" << (fe_menu_mode ? "MENU (front end -- no profile applied)"
: gBTPlatformPod ? "POD (RIO cockpit input; multi-surface gauges/MFDs via pod hardware or explicit L4GAUGE)" : 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)") : "DEV (single window + keyboard)")
<< std::endl << std::flush; << std::endl << std::flush;
@@ -543,7 +563,14 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
{ {
extern int gBTGaugeDockBottom; extern int gBTGaugeDockBottom;
extern int BTGaugeStripHeightFor(int width); 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; gBTGaugeDockBottom = 1;
// Readability default (user-reported: the strip at 800 wide is a // Readability default (user-reported: the strip at 800 wide is a