Glass cockpit refit: one mode resolver, one button geometry, and a cockpit that scales

Replicates the RP412 cockpit line into BT411's own architecture -- porting the
geometry and keeping our renderers, so BT_SHOT single-frame verification stays
intact.

MODE RESOLVER.  Where the secondary displays go was decided in TWO places with
duplicated precedence, and the boot banner read NEITHER -- it announced
"per-display cockpit windows" for every glass boot, surround included.  The
split had also broken BT_COCKPIT=0: documented as the dock-bottom opt-out, the
profile block converted it to BT_GLASS_PANELS=1, so the docked strip was
UNREACHABLE under the glass profile.  One resolver now, consumed by the banner,
the pad-panel decision and the window sizing; BT_GLASS_PANELS is explicit-only;
dock/window modes auto-raise BT_PAD_PANEL so the button field always has a home.

L4RIOBANK -- one button geometry.  Both renderers carried their own copy and had
drifted: an MFD button was 156x138 reaching under the glass in the exploded
window and a 76x24 sliver entirely OUTSIDE the glass in the surround.  One
module owns it now, both are consumers, placement stays per-renderer.  The pod's
under-glass rule (RP412 L4MFDVIEW): reach half the glass in behind the display,
leave a lamp strip clearing the edge, paint buttons first and imagery over --
so the lamp reads as a bar and practically the whole display is the press
target.  The strip scales off the display's SHORT axis (the map is portrait)
with a readable floor.  Retired L4GLASSWIN's three local placers and seven
layout constants.

LAMP FLASH DECODE was wrong [T1].  BTLampBrightnessOf returned max(state1,
state2) and blanked on the alternate phase; RIO::LampState (L4RIO.h [T0]) says
solid shows state 1 and flashing ALTERNATES the two.  Agrees only when one state
is Off -- true for the Panic lamp, which is why it survived -- but L4LAMP.cpp:252
commands flashFast + state1Dim + state2Bright, a dim->bright pulse that rendered
as a hard bright->off blink.  Three copies existed (l4vb16.h, L4GLASSWIN,
L4PADPANEL), all three wrong; the two locals now forward to the one fixed inline.

THE COCKPIT SCALES.  The fixed canvas was stretched into whatever the client
area was, so a window dragged to a different shape squashed the instruments (the
projection was aspect-corrected in task #20; the panels never were).  Now one
uniform scale, centred, leftover black.  D3D9 applies it as a Present
destination rect, which DISCARD forbids -- so the WINDOWED swap effect becomes
COPY when the surround is up and multisampling is off.  The click mapping had to
follow (mapping against the full client drifts the hit test off every button by
the bar width), as did the world aspect (under a uniform scale it is the view
rect's own).  -fit / -windowed-fullscreen: borderless over the monitor.

 - ordering trap: the first WM_SIZE beats the device, so a -fit boot logged
   aspect=3.14 and applied it on frame 1.  The letterbox INTENT is decided in
   btl4main; L4VIDEO only confirms or withdraws it.

PLAYER-TUNABLE DISPLAYS.  BT_MFD_SCALE (+ _UL/_UC/_UR/_LL/_LR), BT_RADAR_SCALE,
BT_RADAR_POS (CENTER/LEFT/RIGHT/MIDLEFT/MIDRIGHT).  The surround BANDS derive
from the resolved sizes -- that is why the sizes could not stay constants: the
band a display hangs in has to grow with it or the canvas clips it.  100%
reproduces the historical L276 R276 T223 B336 exactly.  A corner map goes flush
to the CANVAS edge and the lower MFD slides beside it (measuring off the view
edge overlapped them by 232px).

MAP LEGEND GRID -- measured, not inherited.  scratchpad/measurelegend.py over a
native capture: top 3, cell 102, pitch 107 of 640.  RP412's map is 13 + 6x102 @
105 -- same cell height, different top and pitch, so its numbers do NOT
transfer.  Our old even division had the pitch right by luck and sat 3px high of
the labels.

environ.ini.  It was read ~300 lines into WinMain, AFTER the platform-profile
block had run its getenv()s -- so every setting the profile reads was silently
ignored FROM THE FILE and only worked as a real env var.  It also putenv()'d
comments verbatim.  Now loaded immediately after the first-breath line, comments
skipped, the real environment WINS over the file, and a fully documented default
is written on first run (the bindings.txt convention: untracked, so
extract-over-top never clobbers a player's settings).

VERIFICATION HARNESS (new, reusable): BT_RIOBANK_LOG=1 dumps every bank;
checkbank.py proves no address is SHADOWED (an address whose rect is covered by
earlier buttons is dead however big it looks -- the overlapping under-glass banks
make that a live hazard); clickbank.py posts a real click at every button centre.

Verified: 72/72 placed, 0 shadowed, 72/72 dispatched in BOTH modes, after a
resize, at 150%/135%, and at 75%+BOTTOMRIGHT; wide/tall drags and -fit
undistorted on a 3440x1440; exploded/dock/pod/dev un-regressed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-26 02:23:40 -05:00
co-authored by Claude Opus 5
parent 1cda880c6d
commit 61563c9efe
15 changed files with 1670 additions and 282 deletions
+78 -112
View File
@@ -13,6 +13,7 @@
#include "l4glasswin.h"
#include "l4padrio.h"
#include "l4vb16.h"
#include "l4riobank.h" // the shared button-bank geometry (with the surround)
#include "../munga/gaugrend.h"
#include <windows.h>
@@ -41,23 +42,17 @@ int
}
//###########################################################################
// Layout constants (tunable -- Cyd flagged refinements to the exact button
// sub-positions + the yellow left/right split).
// Window constants. The BUTTON GEOMETRY (how far a bank reaches under the
// glass, the lamp strip, the column pitch, the per-column nudge against the
// imagery legends) moved to L4RIOBANK on 2026-07-26 so the cockpit surround
// draws the identical field; what is left here is this window's own chrome.
//###########################################################################
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)
CellW = 58, // flight-controls cell (no glass behind it)
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,
@@ -67,11 +62,6 @@ enum
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;
@@ -137,71 +127,53 @@ static const char *
}
//###########################################################################
// Lamp decode (identical to L4PADPANEL::LampBrightnessOf -- RIOBase::LampState)
// Lamp decode: the ONE copy lives in l4vb16.h (BTLampBrightnessOf). This TU
// used to carry its own, as did L4PADPANEL -- three copies of the same
// formula, all three carrying the same dim-to-bright flash bug (see the
// header's 2026-07-26 note).
//###########################################################################
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;
return BTLampBrightnessOf(state, tick);
}
//###########################################################################
// 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;
}
// (The local PlaceCellAt / PlaceLine / PlaceRect placers retired 2026-07-26 --
// the geometry they carried now comes from L4RIOBANK, shared with the cockpit
// surround. AdoptBank below is the only thing that writes GButton rects.)
//---------------------------------------------------------------------------
// Copy a laid-out bank into this window at the given offset. A glass window
// passes -bounds so the bank's bounding box lands at the client origin (the
// bank reaches OUTSIDE the display, so its rects arrive with coordinates
// left of / above it); the flight window, which has no glass, passes 0.
//---------------------------------------------------------------------------
static void
PlaceLine(GWin &w, int startAddr, int count, int step,
int x, int y, int horizontal, ColorClass color)
AdoptBank(GWin &w, const BTRioBank &bank, ColorClass color, int dx, int dy)
{
for (int i = 0; i < count; ++i)
for (int i = 0; i < bank.buttonCount &&
w.buttonCount < (int)(sizeof(w.buttons) / sizeof(w.buttons[0])); ++i)
{
int cx = horizontal ? (x + i * CellW) : x;
int cy = horizontal ? y : (y + i * CellH);
PlaceCellAt(w, startAddr + i * step, cx, cy, color);
const BTRioButton &src = bank.buttons[i];
GButton &b = w.buttons[w.buttonCount++];
b.address = src.address;
b.color = (src.colorClass == 1) ? ClrYellow
: (src.colorClass == 2) ? ClrBlue : color;
b.rect.left = src.x + dx;
b.rect.top = src.y + dy;
b.rect.right = src.x + dx + src.w;
b.rect.bottom = src.y + dy + src.h;
}
}
// 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).
// Geometry is L4RIOBANK's -- the under-glass rule, shared with the surround.
static void
BuildMfd(GWin &w, const char *title, const char *portP, const char *portA, int bankHi)
{
@@ -212,23 +184,19 @@ static void
w.monoTint = MfdMonoTint;
w.rotate = 0;
int surfX = Margin;
int surfY = Margin + TitleH + EdgeProtrude; // room for the top buttons to poke out
BTRioBankMetrics metrics;
BTRioBankMetricsFor(MfdSurfW, MfdSurfH, &metrics);
// 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);
BTRioBank bank;
BTRioBankLayout(BTRioBankMfd, 0, 0, MfdSurfW, MfdSurfH,
bankHi, 0, &metrics, 0, &bank);
w.clientW = Margin + MfdSurfW + Margin;
w.clientH = surfY + MfdSurfH + EdgeProtrude + Margin;
BTRioBankDump(title, &bank);
int dx = -bank.boundsX, dy = -bank.boundsY;
AdoptBank(w, bank, ClrRed, dx, dy);
SetRect(&w.surfaceRect, dx, dy, dx + MfdSurfW, dy + MfdSurfH);
w.clientW = bank.boundsW;
w.clientH = bank.boundsH;
}
// The secondary/radar display: 12 yellow Secondary buttons split left / right
@@ -243,37 +211,23 @@ static void
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.
// Left column 0x10-0x15, right 0x18-0x1D, foot row 0x16/0x17/0x1F/0x1E.
// (0x15 sits on the left column -- Cyd listed 0x10-0x14; move it if wanted.)
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;
BTRioBankMetrics metrics;
BTRioBankMetricsFor(RadarSurfW, RadarSurfH, &metrics);
BTRioBank bank;
BTRioBankLayout(BTRioBankRadar, 0, 0, RadarSurfW, RadarSurfH,
0x10, 0x18, &metrics, bottomAddrs, &bank);
BTRioBankDump("Secondary / Radar", &bank);
int dx = -bank.boundsX, dy = -bank.boundsY;
AdoptBank(w, bank, ClrYellow, dx, dy);
SetRect(&w.surfaceRect, dx, dy, dx + RadarSurfW, dy + RadarSurfH);
w.clientW = bank.boundsW;
w.clientH = bank.boundsH;
}
// The Flight Controls window: the no-display banks -- throttle/panic/door/icom
@@ -286,15 +240,27 @@ static void
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;
// No glass to hide behind, so these cells carry their own size (see
// l4riobank.h) -- two 1-wide columns, eight rows each. Cell minus the
// 4px gap keeps the historical CellW/CellH PITCH and the 2px inset the
// old PlaceCellAt drew, so the window is pixel-identical.
BTRioBankMetrics metrics;
metrics.strip = 0;
metrics.gap = Gap;
metrics.cellW = CellW - Gap;
metrics.cellH = CellH - 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
BTRioBank bank;
memset(&bank, 0, sizeof(bank));
BTRioBankFlightGrid(2, 2, 0x38, 8, 1, 0, 0, &metrics, &bank); // throttle/panic/door/icom
BTRioBankFlightGrid(CellW + Gap + 2, 2, 0x40, 8, 1, 0, 0, &metrics, &bank); // joystick + fire
w.clientW = col2 + CellW + Margin;
w.clientH = topY + 8 * CellH + Margin;
BTRioBankDump("Flight Controls", &bank);
AdoptBank(w, bank, ClrBlue, 0, 0);
// no translation here (the cells are already at their 2px inset); the
// client is the historical two-column extent
w.clientW = CellW + Gap + CellW;
w.clientH = 8 * CellH;
}
//###########################################################################