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:
+78
-112
@@ -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;
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include "l4padpanel.h"
|
||||
#include "l4padrio.h"
|
||||
#include "l4vb16.h" // BTLampBrightnessOf -- the one lamp decode
|
||||
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
@@ -191,33 +192,18 @@ static int
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// Lamp decode (RIOBase::LampState): flash = low 2 bits (solid/slow/med/
|
||||
// fast), filament brightness = max of the state1 (>>2) and state2 (>>4)
|
||||
// 2-bit fields (0 off / 1 dim / 3 bright). Flash blinks between the
|
||||
// commanded brightness and off, half-periods 500/250/125 ms (vRIO).
|
||||
// Lamp decode (RIOBase::LampState): the ONE copy lives in l4vb16.h
|
||||
// (BTLampBrightnessOf) -- flash mode in the low 2 bits picks a 500/250/125 ms
|
||||
// half-period and the two 2-bit brightness fields (state1 >>2, state2 >>4;
|
||||
// 0 off / 1 dim / 3 bright) ALTERNATE across it. This TU used to carry its
|
||||
// own max-then-blank copy, as did L4GLASSWIN; see the header's 2026-07-26
|
||||
// note for the dim-to-bright pulse all three got wrong.
|
||||
//###########################################################################
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
#include "mungal4.h"
|
||||
#pragma hdrstop
|
||||
|
||||
//###########################################################################
|
||||
// L4RIOBANK -- the ONE button-bank geometry for a pod display. Design +
|
||||
// the under-glass rule: l4riobank.h. Consumed by the cockpit surround
|
||||
// (L4VB16.cpp, canvas space) and the exploded per-display windows
|
||||
// (L4GLASSWIN.cpp, client space); neither owns a copy any more.
|
||||
//###########################################################################
|
||||
|
||||
#include "l4riobank.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
//-------------------------------------------------------------------
|
||||
// The lamp strip at NATIVE glass size, scaled down with the display
|
||||
// but floored so a half-scale surround lamp is still a bar and not a
|
||||
// smudge. RP412 L4MFDVIEW works at native only and hardcodes 10.
|
||||
//-------------------------------------------------------------------
|
||||
const int nativeShortAxis = 480; // MFD 640x480 / map 480x640
|
||||
const int nativeStrip = 10;
|
||||
const int minimumStrip = 6;
|
||||
const int defaultGap = 4;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Per-column horizontal nudge for the 4 MFD buttons, left->right, so
|
||||
// they line up with the (unevenly spaced) DISPLAY / PROGRAM / ...
|
||||
// legends painted in the imagery. Measured against the native 640
|
||||
// glass in L4GLASSWIN (RedOffsetX + kRedColDX) and scaled here, so
|
||||
// both renderers inherit the same alignment. Tunable.
|
||||
//-------------------------------------------------------------------
|
||||
const int nativeMfdW = 640;
|
||||
const int mfdColumnDX[4] = { 0, 1, 5, 5 };
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// The map paints its own legend beside each side button (the six
|
||||
// cells: MAP+, MAP-, IR, crouch, searchlight, display-mode), and the
|
||||
// buttons have to line up with THAT grid, not with height/6.
|
||||
//
|
||||
// MEASURED off a native 480x640 capture (scratchpad/measurelegend.py
|
||||
// over an exploded Secondary/Radar window): the first cell border sits
|
||||
// 3 rows down and the six cells are 102 tall on a 107 pitch. RP412's
|
||||
// map measures 13 + 6x102 on a 105 pitch -- same cell height, different
|
||||
// top and pitch, so its numbers do NOT transfer; these are ours.
|
||||
//
|
||||
// Our previous even division (displayH/6 + 1 = 107) had the pitch right
|
||||
// by luck and sat 3px high of the labels.
|
||||
//-------------------------------------------------------------------
|
||||
const int mapLegendSpan = 640;
|
||||
const int mapLegendTop = 3;
|
||||
const int mapLegendCell = 102;
|
||||
const int mapLegendPitch = 107;
|
||||
|
||||
int Scaled(int nativeValue, int actual, int nativeExtent)
|
||||
{
|
||||
if (nativeExtent <= 0) return nativeValue;
|
||||
return (nativeValue * actual) / nativeExtent;
|
||||
}
|
||||
|
||||
void Push(
|
||||
BTRioBank *bank,
|
||||
int address, int x, int y, int w, int h,
|
||||
int colorClass, int inert, const char *label
|
||||
)
|
||||
{
|
||||
if (bank->buttonCount >= BTRioBankMaxButtons) return;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
BTRioButton *button = &bank->buttons[bank->buttonCount++];
|
||||
button->address = address;
|
||||
button->x = x;
|
||||
button->y = y;
|
||||
button->w = w;
|
||||
button->h = h;
|
||||
button->colorClass = colorClass;
|
||||
button->inert = inert;
|
||||
button->label = label;
|
||||
}
|
||||
|
||||
void GrowBounds(BTRioBank *bank, int x, int y, int w, int h)
|
||||
{
|
||||
if (bank->boundsW == 0 && bank->boundsH == 0)
|
||||
{
|
||||
bank->boundsX = x; bank->boundsY = y;
|
||||
bank->boundsW = w; bank->boundsH = h;
|
||||
return;
|
||||
}
|
||||
int left = (x < bank->boundsX) ? x : bank->boundsX;
|
||||
int top = (y < bank->boundsY) ? y : bank->boundsY;
|
||||
int right = bank->boundsX + bank->boundsW;
|
||||
int bottom = bank->boundsY + bank->boundsH;
|
||||
if (x + w > right) right = x + w;
|
||||
if (y + h > bottom) bottom = y + h;
|
||||
bank->boundsX = left;
|
||||
bank->boundsY = top;
|
||||
bank->boundsW = right - left;
|
||||
bank->boundsH = bottom - top;
|
||||
}
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
|
||||
void
|
||||
BTRioBankMetricsFor(int displayW, int displayH, BTRioBankMetrics *out)
|
||||
{
|
||||
if (out == 0) return;
|
||||
|
||||
int shortAxis = (displayW < displayH) ? displayW : displayH;
|
||||
|
||||
int strip = Scaled(nativeStrip, shortAxis, nativeShortAxis);
|
||||
if (strip < minimumStrip) strip = minimumStrip;
|
||||
|
||||
out->strip = strip;
|
||||
out->gap = defaultGap;
|
||||
|
||||
// The cells with no glass behind them (the flight bank, the map's foot
|
||||
// row) are drawn in full, so they get a plain readable size scaled off
|
||||
// the same axis rather than a derived depth.
|
||||
out->cellW = Scaled(58, shortAxis, nativeShortAxis);
|
||||
out->cellH = Scaled(28, shortAxis, nativeShortAxis);
|
||||
if (out->cellW < 12) out->cellW = 12;
|
||||
if (out->cellH < 12) out->cellH = 12;
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
|
||||
void
|
||||
BTRioBankLayout(
|
||||
BTRioBankStyle style,
|
||||
int originX,
|
||||
int originY,
|
||||
int displayW,
|
||||
int displayH,
|
||||
int anchorA,
|
||||
int anchorB,
|
||||
const BTRioBankMetrics *metrics,
|
||||
const int *bottomAddrs,
|
||||
BTRioBank *out
|
||||
)
|
||||
{
|
||||
if (out == 0 || metrics == 0) return;
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
const int strip = metrics->strip;
|
||||
const int gap = metrics->gap;
|
||||
|
||||
// the glass itself is always inside the bounds
|
||||
GrowBounds(out, originX, originY, displayW, displayH);
|
||||
|
||||
if (style == BTRioBankMfd)
|
||||
{
|
||||
//---------------------------------------------------------------
|
||||
// 4 above the glass, 4 below. Addresses descend from the anchor
|
||||
// row-major (vRIO CockpitLayout::Mfd): top = anchorA-i, bottom =
|
||||
// anchorA-4-i. Each button reaches HALF THE GLASS in and clears
|
||||
// the edge by `strip`, so the two banks meet in the middle and
|
||||
// every pixel of the display is some button's press target.
|
||||
//---------------------------------------------------------------
|
||||
int depth = displayH / 2;
|
||||
if (depth < strip) depth = strip;
|
||||
|
||||
int slotW = displayW / 4;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
int dx = Scaled(mfdColumnDX[i], displayW, nativeMfdW);
|
||||
int x = originX + i * slotW + dx;
|
||||
int w = slotW - gap;
|
||||
|
||||
int topY = originY - strip;
|
||||
int botY = originY + displayH - depth;
|
||||
Push(out, anchorA - i, x, topY, w, depth + strip, 0, 0, 0);
|
||||
Push(out, anchorA - 4 - i, x, botY, w, depth + strip, 0, 0, 0);
|
||||
GrowBounds(out, x, topY, w, depth + strip);
|
||||
GrowBounds(out, x, botY, w, depth + strip);
|
||||
}
|
||||
}
|
||||
else if (style == BTRioBankRadar)
|
||||
{
|
||||
//---------------------------------------------------------------
|
||||
// The portrait map: 6 buttons down each side (anchorA left,
|
||||
// anchorB right) plus an optional 4 along the foot. Same rule
|
||||
// turned on its side -- each column reaches half the map in and
|
||||
// leaves `strip` clearing the edge.
|
||||
//
|
||||
// The FOOT ROW IS PUSHED FIRST, deliberately: the columns now
|
||||
// reach half the width each, so they cover the foot band too,
|
||||
// and the hit test takes the first match. Foot-first keeps
|
||||
// those four addresses reachable.
|
||||
//---------------------------------------------------------------
|
||||
int depth = displayW / 2;
|
||||
if (depth < strip) depth = strip;
|
||||
|
||||
if (bottomAddrs != 0)
|
||||
{
|
||||
int bandH = metrics->cellH;
|
||||
int bandY = originY + displayH - bandH;
|
||||
int bandW = displayW / 4;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
int x = originX + i * bandW;
|
||||
Push(out, bottomAddrs[i], x, bandY, bandW - gap,
|
||||
bandH + strip, 1, 0, 0);
|
||||
GrowBounds(out, x, bandY, bandW - gap, bandH + strip);
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Six cells down the height, on the map's MEASURED legend grid
|
||||
// (see mapLegend* above). Top and bottom are scaled SEPARATELY
|
||||
// and subtracted rather than scaling a height directly: rounding
|
||||
// a height would let the buttons drift out of step with the
|
||||
// labels a display or two down the column.
|
||||
//---------------------------------------------------------------
|
||||
int leftX = originX - strip;
|
||||
int rightX = originX + displayW - depth;
|
||||
for (int i = 0; i < 6; ++i)
|
||||
{
|
||||
int cellTop = mapLegendTop + i * mapLegendPitch;
|
||||
int top = (cellTop * displayH) / mapLegendSpan;
|
||||
int bottom = ((cellTop + mapLegendCell) * displayH) / mapLegendSpan;
|
||||
int h = bottom - top;
|
||||
if (h < 1) h = 1;
|
||||
int y = originY + top;
|
||||
Push(out, anchorA + i, leftX, y, depth + strip, h, 1, 0, 0);
|
||||
Push(out, anchorB + i, rightX, y, depth + strip, h, 1, 0, 0);
|
||||
GrowBounds(out, leftX, y, depth + strip, h);
|
||||
GrowBounds(out, rightX, y, depth + strip, h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
|
||||
void
|
||||
BTRioBankFlightGrid(
|
||||
int originX,
|
||||
int originY,
|
||||
int baseAddress,
|
||||
int count,
|
||||
int columns,
|
||||
const char *const *labels,
|
||||
const int *inert,
|
||||
const BTRioBankMetrics *metrics,
|
||||
BTRioBank *out
|
||||
)
|
||||
{
|
||||
if (out == 0 || metrics == 0 || columns <= 0) return;
|
||||
|
||||
const int cw = metrics->cellW;
|
||||
const int ch = metrics->cellH;
|
||||
const int gap = metrics->gap;
|
||||
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
int column = i % columns;
|
||||
int row = i / columns;
|
||||
int x = originX + column * (cw + gap);
|
||||
int y = originY + row * (ch + gap);
|
||||
Push(out, baseAddress + i, x, y, cw, ch, 2,
|
||||
(inert != 0) ? inert[i] : 0,
|
||||
(labels != 0) ? labels[i] : 0);
|
||||
GrowBounds(out, x, y, cw, ch);
|
||||
}
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
|
||||
void
|
||||
BTRioBankDump(const char *tag, const BTRioBank *bank)
|
||||
{
|
||||
static int enabled = -1;
|
||||
static int printed = 0;
|
||||
|
||||
if (enabled < 0)
|
||||
{
|
||||
const char *e = getenv("BT_RIOBANK_LOG");
|
||||
enabled = (e != 0 && e[0] != '0') ? 1 : 0;
|
||||
}
|
||||
if (!enabled || bank == 0 || printed >= 16) return;
|
||||
++printed;
|
||||
|
||||
DEBUG_STREAM << "[riobank] " << (tag ? tag : "?")
|
||||
<< " bounds=(" << bank->boundsX << "," << bank->boundsY << ","
|
||||
<< bank->boundsW << "," << bank->boundsH << ") "
|
||||
<< bank->buttonCount << " buttons\n";
|
||||
for (int i = 0; i < bank->buttonCount; ++i)
|
||||
{
|
||||
const BTRioButton &b = bank->buttons[i];
|
||||
DEBUG_STREAM << "[riobank] addr=0x" << std::hex << b.address << std::dec
|
||||
<< " rect=(" << b.x << "," << b.y << "," << b.w << "," << b.h << ")"
|
||||
<< " class=" << b.colorClass << "\n";
|
||||
}
|
||||
DEBUG_STREAM << std::flush;
|
||||
}
|
||||
+348
-70
@@ -2,6 +2,7 @@
|
||||
#pragma hdrstop
|
||||
|
||||
#include "l4vb16.h"
|
||||
#include "l4riobank.h" // the shared button-bank geometry (with the exploded windows)
|
||||
#include "../munga/gaugrend.h"
|
||||
#include "L4VIDEO.h"
|
||||
#include "DXUtils.h"
|
||||
@@ -195,41 +196,229 @@ int gBTCockpitCanvasW = 0; // backbuffer canvas dims (set by btl4main; the
|
||||
int gBTCockpitCanvasH = 0; // aspect calc needs them after a client resize)
|
||||
|
||||
// Layout constants (backbuffer px). MFD = 640x480 native x 0.5; radar = 480x640
|
||||
// portrait x 0.5. The lamp EDGE (kCkLamp) protrudes past the surface; the FULL
|
||||
// button rect reaches kCkRedCell INTO the display (hit-only, covered by the
|
||||
// surface). User asked for larger/more-clickable buttons than the glass windows'
|
||||
// 10px edge -> kCkLamp 16.
|
||||
// portrait x 0.5.
|
||||
//
|
||||
// ⚠ 2026-07-26: the BUTTON rects no longer come from here -- L4RIOBANK owns
|
||||
// that geometry now and the exploded per-display windows draw the identical
|
||||
// field. What changed for the surround: its MFD lamps used to sit ENTIRELY
|
||||
// outside the glass on a kCkRedH=24 strip (a 76x24 click target), while the
|
||||
// same button in the exploded window reached 128px under the display. Both
|
||||
// now follow the pod's under-glass rule -- half the glass deep, a scaled lamp
|
||||
// strip clearing the edge. kCkTopBand still has to reserve room for the top
|
||||
// row's protruding strip, so the band constants stay.
|
||||
enum {
|
||||
kCkMFDW = 320, kCkMFDH = 240,
|
||||
kCkMFDW = 320, kCkMFDH = 240, // the POD sizes (100%) -- see BTCkSizes
|
||||
kCkRADW = 240, kCkRADH = 320,
|
||||
kCkOVL = 44, // (int)(0.14f * 320): corner overlap into the view
|
||||
kCkLamp = 16, // radar rail / bottom-cell protrusion
|
||||
kCkRedH = 24, // MFD red lamp height -- sits fully OUTSIDE the surface
|
||||
kCkRedGap = 3, // gap between the red lamp and the MFD screen edge
|
||||
kCkRailW = 26, // radar side-rail width
|
||||
kCkGap = 2,
|
||||
kCkSideBand = kCkMFDW - kCkOVL, // 276
|
||||
kCkTopBand = (kCkMFDH - kCkOVL) + kCkRedGap + kCkRedH, // 223 (fits the top red lamps)
|
||||
kCkBotBand = kCkRADH + kCkLamp // 336 (radar flush below view)
|
||||
kCkLamp = 16, // legacy band reserve (radar rail / bottom cell)
|
||||
kCkRedH = 24, // legacy band reserve above the top MFD row
|
||||
kCkRedGap = 3, // gap between that reserve and the MFD screen edge
|
||||
kCkGap = 2
|
||||
};
|
||||
|
||||
//===========================================================================//
|
||||
// PLAYER-SCALED DISPLAYS (2026-07-26).
|
||||
//
|
||||
// The pod bolted its instruments down at one size; a desktop panel has room to
|
||||
// trade viewscreen for instrument, so the player scales them -- BT_MFD_SCALE
|
||||
// sets all five MFDs, BT_MFD_SCALE_UL/_UC/_UR/_LL/_LR override individually,
|
||||
// BT_RADAR_SCALE the map. Percentages of the pod size, 25-200; anything
|
||||
// unreadable or out of range falls back to the group setting, then to 100.
|
||||
//
|
||||
// Resolved ONCE (the surround recomputes its layout every frame and this reads
|
||||
// the environment), and the SURROUND BANDS derive from the result -- which is
|
||||
// the whole reason the sizes could not just be constants: the band a display
|
||||
// hangs in has to grow with it or the canvas clips it.
|
||||
//===========================================================================//
|
||||
struct BTCkSizes
|
||||
{
|
||||
int mfdW[5], mfdH[5]; // UL, UC, UR, LL, LR -- kCkMfdBank order
|
||||
int radW, radH;
|
||||
int sideBandL, sideBandR, topBand, botBand;
|
||||
};
|
||||
|
||||
static int CkScalePercent(const char *name, int fallback)
|
||||
{
|
||||
const char *text = getenv(name);
|
||||
if (text == NULL) return fallback;
|
||||
int percent = atoi(text);
|
||||
if (percent <= 0) return fallback;
|
||||
if (percent < 25) percent = 25;
|
||||
if (percent > 200) percent = 200;
|
||||
return percent;
|
||||
}
|
||||
|
||||
// Where the map sits. The pod had it dead centre under the viewscreen, which
|
||||
// on a wide panel is exactly where the road is -- BT_RADAR_POS moves it out of
|
||||
// the way. In a bottom corner it takes that corner and the lower MFD there
|
||||
// slides inboard beside it; halfway up a side it leaves the bottom row
|
||||
// entirely (and the bottom band shrinks to what the lower MFDs need).
|
||||
enum BTCkRadarPos { CkRadarBottomCenter = 0, CkRadarBottomLeft, CkRadarBottomRight,
|
||||
CkRadarMidLeft, CkRadarMidRight };
|
||||
|
||||
static int CkRadarPosition(void)
|
||||
{
|
||||
static int cached = -1;
|
||||
if (cached >= 0) return cached;
|
||||
|
||||
static const struct { const char *name; int pos; } names[] = {
|
||||
{ "CENTER", CkRadarBottomCenter }, { "CENTRE", CkRadarBottomCenter },
|
||||
{ "BOTTOM", CkRadarBottomCenter },
|
||||
{ "LEFT", CkRadarBottomLeft }, { "BOTTOMLEFT", CkRadarBottomLeft },
|
||||
{ "RIGHT", CkRadarBottomRight }, { "BOTTOMRIGHT", CkRadarBottomRight },
|
||||
{ "MIDLEFT", CkRadarMidLeft }, { "LEFTCENTER", CkRadarMidLeft },
|
||||
{ "LEFTCENTRE", CkRadarMidLeft },
|
||||
{ "MIDRIGHT", CkRadarMidRight }, { "RIGHTCENTER", CkRadarMidRight },
|
||||
{ "RIGHTCENTRE", CkRadarMidRight }
|
||||
};
|
||||
|
||||
cached = CkRadarBottomCenter;
|
||||
const char *text = getenv("BT_RADAR_POS");
|
||||
if (text != NULL)
|
||||
for (int i = 0; i < (int)(sizeof(names) / sizeof(names[0])); i++)
|
||||
if (_stricmp(text, names[i].name) == 0) { cached = names[i].pos; break; }
|
||||
|
||||
static const char *described[] = { "bottom centre", "bottom left", "bottom right",
|
||||
"left side, centred", "right side, centred" };
|
||||
DEBUG_STREAM << "[cockpit] radar on the " << described[cached] << "\n" << std::flush;
|
||||
return cached;
|
||||
}
|
||||
|
||||
static int CkRadarOnASide(int pos)
|
||||
{ return (pos == CkRadarMidLeft || pos == CkRadarMidRight) ? 1 : 0; }
|
||||
|
||||
static const BTCkSizes *BTCkResolvedSizes(void)
|
||||
{
|
||||
static BTCkSizes s;
|
||||
static int resolved = 0;
|
||||
if (resolved) return &s;
|
||||
resolved = 1;
|
||||
|
||||
int group = CkScalePercent("BT_MFD_SCALE", 100);
|
||||
static const char *perDisplay[5] = {
|
||||
"BT_MFD_SCALE_UL", "BT_MFD_SCALE_UC", "BT_MFD_SCALE_UR",
|
||||
"BT_MFD_SCALE_LL", "BT_MFD_SCALE_LR"
|
||||
};
|
||||
int percent[5];
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
percent[i] = CkScalePercent(perDisplay[i], group);
|
||||
s.mfdW[i] = kCkMFDW * percent[i] / 100;
|
||||
s.mfdH[i] = kCkMFDH * percent[i] / 100;
|
||||
}
|
||||
int radarPercent = CkScalePercent("BT_RADAR_SCALE", 100);
|
||||
s.radW = kCkRADW * radarPercent / 100;
|
||||
s.radH = kCkRADH * radarPercent / 100;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// The bands: each is whatever the LARGEST display hanging in it needs.
|
||||
// A corner MFD overlaps the view by kCkOVL, so it only claims
|
||||
// (its size - the overlap) of the band.
|
||||
//-------------------------------------------------------------------
|
||||
int radarPos = CkRadarPosition();
|
||||
|
||||
int leftMax = (s.mfdW[0] > s.mfdW[3]) ? s.mfdW[0] : s.mfdW[3]; // UL, LL
|
||||
int rightMax = (s.mfdW[2] > s.mfdW[4]) ? s.mfdW[2] : s.mfdW[4]; // UR, LR
|
||||
s.sideBandL = leftMax - kCkOVL;
|
||||
s.sideBandR = rightMax - kCkOVL;
|
||||
|
||||
// top: the corner MFDs claim (h - overlap); UC drops 75% of its height in
|
||||
int topCorner = ((s.mfdH[0] > s.mfdH[2]) ? s.mfdH[0] : s.mfdH[2]) - kCkOVL;
|
||||
int topCentre = (int)(0.75f * s.mfdH[1]);
|
||||
s.topBand = (topCorner > topCentre) ? topCorner : topCentre;
|
||||
s.topBand += kCkRedGap + kCkRedH; // room for the top lamp strip
|
||||
|
||||
// bottom: the lower MFDs hang (h - overlap) below the view; the radar,
|
||||
// when it is down there, hangs its full height plus its lamp strip
|
||||
int botCorner = ((s.mfdH[3] > s.mfdH[4]) ? s.mfdH[3] : s.mfdH[4]) - kCkOVL;
|
||||
s.botBand = botCorner;
|
||||
if (!CkRadarOnASide(radarPos))
|
||||
{
|
||||
int radarBand = s.radH + kCkLamp;
|
||||
if (radarBand > s.botBand) s.botBand = radarBand;
|
||||
}
|
||||
|
||||
// a side-mounted radar has to fit BESIDE the view, in that side's band
|
||||
if (CkRadarOnASide(radarPos))
|
||||
{
|
||||
int *band = (radarPos == CkRadarMidLeft) ? &s.sideBandL : &s.sideBandR;
|
||||
if (s.radW + kCkLamp > *band) *band = s.radW + kCkLamp;
|
||||
}
|
||||
|
||||
if (s.sideBandL < 1) s.sideBandL = 1;
|
||||
if (s.sideBandR < 1) s.sideBandR = 1;
|
||||
|
||||
DEBUG_STREAM << "[cockpit] displays UL " << percent[0] << "% UC " << percent[1]
|
||||
<< "% UR " << percent[2] << "% LL " << percent[3] << "% LR " << percent[4]
|
||||
<< "% radar " << radarPercent << "% bands L" << s.sideBandL
|
||||
<< " R" << s.sideBandR << " T" << s.topBand << " B" << s.botBand
|
||||
<< "\n" << std::flush;
|
||||
return &s;
|
||||
}
|
||||
|
||||
// Canvas = view region + the surround bands.
|
||||
void BTCockpitCanvasFor(int viewW, int viewH, int *canvasW, int *canvasH)
|
||||
{
|
||||
if (canvasW) *canvasW = viewW + 2 * kCkSideBand;
|
||||
if (canvasH) *canvasH = viewH + kCkTopBand + kCkBotBand;
|
||||
const BTCkSizes *s = BTCkResolvedSizes();
|
||||
if (canvasW) *canvasW = viewW + s->sideBandL + s->sideBandR;
|
||||
if (canvasH) *canvasH = viewH + s->topBand + s->botBand;
|
||||
}
|
||||
|
||||
//===========================================================================//
|
||||
// LETTERBOX FIT -- one uniform scale, centred, leftover black. Contract +
|
||||
// why the swap effect changes: l4vb16.h.
|
||||
//===========================================================================//
|
||||
int gBTCockpitLetterbox = 0;
|
||||
|
||||
int BTCockpitFitRect(int clientW, int clientH, RECT *out)
|
||||
{
|
||||
int cw = gBTCockpitCanvasW, ch = gBTCockpitCanvasH;
|
||||
if (out == NULL || cw <= 0 || ch <= 0 || clientW <= 0 || clientH <= 0)
|
||||
return 0;
|
||||
|
||||
// The larger axis is the constraint; integer math throughout so the rect
|
||||
// is stable frame to frame (a float scale wobbles by a pixel and the bars
|
||||
// shimmer during a drag).
|
||||
int w = clientW;
|
||||
int h = (int)((__int64)clientW * ch / cw);
|
||||
if (h > clientH)
|
||||
{
|
||||
h = clientH;
|
||||
w = (int)((__int64)clientH * cw / ch);
|
||||
}
|
||||
if (w < 1) w = 1;
|
||||
if (h < 1) h = 1;
|
||||
|
||||
out->left = (clientW - w) / 2;
|
||||
out->top = (clientH - h) / 2;
|
||||
out->right = out->left + w;
|
||||
out->bottom = out->top + h;
|
||||
|
||||
return (w != clientW || h != clientH) ? 1 : 0;
|
||||
}
|
||||
|
||||
const RECT *BTCockpitPresentDest(void *hwnd, RECT *storage)
|
||||
{
|
||||
if (!gBTCockpitLetterbox || !gBTGaugeCockpit || hwnd == NULL || storage == NULL)
|
||||
return NULL;
|
||||
RECT client;
|
||||
if (!GetClientRect((HWND)hwnd, &client))
|
||||
return NULL;
|
||||
if (!BTCockpitFitRect(client.right, client.bottom, storage))
|
||||
return NULL; // already an exact fit -- present full client
|
||||
return storage;
|
||||
}
|
||||
|
||||
// The five MFD banks: {cockpit surface slot, high address}. Top row = bankHi-i
|
||||
// (descending), bottom row = bankHi-4-i (L4GLASSWIN BuildMfd order + the
|
||||
// BTGlassPanels_Create address assignment).
|
||||
static const struct { int slot, bankHi; } kCkMfdBank[5] =
|
||||
static const struct { int slot, bankHi; const char *name; } kCkMfdBank[5] =
|
||||
{
|
||||
{ 0, 0x2F }, // Heat (UL)
|
||||
{ 1, 0x27 }, // Mfd2 (UC)
|
||||
{ 5, 0x37 }, // Comm (UR)
|
||||
{ 2, 0x0F }, // Mfd1 (LL)
|
||||
{ 3, 0x07 }, // Mfd3 (LR)
|
||||
{ 0, 0x2F, "surround/Heat MFD" }, // UL
|
||||
{ 1, 0x27, "surround/Engineering" }, // UC
|
||||
{ 5, 0x37, "surround/Comm MFD" }, // UR
|
||||
{ 2, 0x0F, "surround/Left Weapons" }, // LL
|
||||
{ 3, 0x07, "surround/Right Weapons" }, // LR
|
||||
};
|
||||
|
||||
static void CkPushBtn(BTCockpitLayout *L, int addr, int x, int y, int w, int h,
|
||||
@@ -247,66 +436,114 @@ void BTCockpitComputeLayout(int canvasW, int canvasH, BTCockpitLayout *out)
|
||||
memset(L, 0, sizeof(*L));
|
||||
L->canvasW = canvasW;
|
||||
L->canvasH = canvasH;
|
||||
int viewW = canvasW - 2 * kCkSideBand;
|
||||
int viewH = canvasH - kCkTopBand - kCkBotBand;
|
||||
const BTCkSizes *S = BTCkResolvedSizes();
|
||||
int viewW = canvasW - S->sideBandL - S->sideBandR;
|
||||
int viewH = canvasH - S->topBand - S->botBand;
|
||||
if (viewW < 64) viewW = 64;
|
||||
if (viewH < 64) viewH = 64;
|
||||
int vx = kCkSideBand, vy = kCkTopBand;
|
||||
int vx = S->sideBandL, vy = S->topBand;
|
||||
L->viewX = vx; L->viewY = vy; L->viewW = viewW; L->viewH = viewH;
|
||||
|
||||
// Surface dest rects (slots: 0 Heat, 1 Mfd2, 2 Mfd1, 3 Mfd3, 4 sec, 5 Comm).
|
||||
// Corner MFDs overlap the view corner by kCkOVL both axes; Mfd2 drops 25% of
|
||||
// its height into the top; sec is flush below (view bottom edge clean).
|
||||
// Sizes are the player-scaled ones (BTCkResolvedSizes), indexed in
|
||||
// kCkMfdBank order: 0 UL, 1 UC, 2 UR, 3 LL, 4 LR.
|
||||
int cx = vx + viewW / 2;
|
||||
// Heat TL
|
||||
L->surfX[0] = vx - kCkMFDW + kCkOVL; L->surfY[0] = vy - kCkMFDH + kCkOVL;
|
||||
L->surfW[0] = kCkMFDW; L->surfH[0] = kCkMFDH;
|
||||
L->surfX[0] = vx - S->mfdW[0] + kCkOVL; L->surfY[0] = vy - S->mfdH[0] + kCkOVL;
|
||||
L->surfW[0] = S->mfdW[0]; L->surfH[0] = S->mfdH[0];
|
||||
// Mfd2 top-center (25% into view)
|
||||
L->surfX[1] = cx - kCkMFDW / 2; L->surfY[1] = vy - (int)(0.75f * kCkMFDH);
|
||||
L->surfW[1] = kCkMFDW; L->surfH[1] = kCkMFDH;
|
||||
L->surfX[1] = cx - S->mfdW[1] / 2; L->surfY[1] = vy - (int)(0.75f * S->mfdH[1]);
|
||||
L->surfW[1] = S->mfdW[1]; L->surfH[1] = S->mfdH[1];
|
||||
// Mfd1 BL
|
||||
L->surfX[2] = vx - kCkMFDW + kCkOVL; L->surfY[2] = vy + viewH - kCkOVL;
|
||||
L->surfW[2] = kCkMFDW; L->surfH[2] = kCkMFDH;
|
||||
L->surfX[2] = vx - S->mfdW[3] + kCkOVL; L->surfY[2] = vy + viewH - kCkOVL;
|
||||
L->surfW[2] = S->mfdW[3]; L->surfH[2] = S->mfdH[3];
|
||||
// Mfd3 BR
|
||||
L->surfX[3] = vx + viewW - kCkOVL; L->surfY[3] = vy + viewH - kCkOVL;
|
||||
L->surfW[3] = kCkMFDW; L->surfH[3] = kCkMFDH;
|
||||
// sec center, flush below
|
||||
L->surfX[4] = cx - kCkRADW / 2; L->surfY[4] = vy + viewH;
|
||||
L->surfW[4] = kCkRADW; L->surfH[4] = kCkRADH;
|
||||
L->surfW[3] = S->mfdW[4]; L->surfH[3] = S->mfdH[4];
|
||||
// Comm TR
|
||||
L->surfX[5] = vx + viewW - kCkOVL; L->surfY[5] = vy - kCkMFDH + kCkOVL;
|
||||
L->surfW[5] = kCkMFDW; L->surfH[5] = kCkMFDH;
|
||||
L->surfX[5] = vx + viewW - kCkOVL; L->surfY[5] = vy - S->mfdH[2] + kCkOVL;
|
||||
L->surfW[5] = S->mfdW[2]; L->surfH[5] = S->mfdH[2];
|
||||
|
||||
// --- MFD red buttons: 4 top (bankHi-i) + 4 bottom (bankHi-4-i) ---
|
||||
int slotW = kCkMFDW / 4; // 80
|
||||
for (int m = 0; m < 5; m++)
|
||||
//-------------------------------------------------------------------
|
||||
// sec (the map). Bottom centre as the pod had it, either bottom
|
||||
// corner, or halfway up either side (BT_RADAR_POS). Only when it
|
||||
// takes a bottom CORNER does a lower MFD have to move -- it slides
|
||||
// inboard beside the map; on a side the map is out of that row's way
|
||||
// already.
|
||||
//-------------------------------------------------------------------
|
||||
{
|
||||
int s = kCkMfdBank[m].slot, hi = kCkMfdBank[m].bankHi;
|
||||
int sx = L->surfX[s], sy = L->surfY[s];
|
||||
for (int i = 0; i < 4; i++)
|
||||
int radarPos = CkRadarPosition();
|
||||
L->surfW[4] = S->radW;
|
||||
L->surfH[4] = S->radH;
|
||||
// A corner map goes FLUSH to the canvas edge and the lower MFD whose
|
||||
// corner it took sits BESIDE it -- measured off the canvas edge, not
|
||||
// the view edge, or the two overlap by the band width (they did:
|
||||
// 232px of map drawn over the Right Weapons MFD).
|
||||
int canvasLeft = vx - S->sideBandL;
|
||||
int canvasRight = vx + viewW + S->sideBandR;
|
||||
switch (radarPos)
|
||||
{
|
||||
int bx = sx + i * slotW;
|
||||
// Lamps sit fully OUTSIDE the MFD (a small gap off the edge) so nothing
|
||||
// occludes them or the DISPLAY/PROGRAM legends -- 4 above, 4 below.
|
||||
CkPushBtn(L, hi - i, bx, sy - kCkRedGap - kCkRedH, slotW - kCkGap, kCkRedH, 0, 0, 0);
|
||||
CkPushBtn(L, hi - 4 - i, bx, sy + kCkMFDH + kCkRedGap, slotW - kCkGap, kCkRedH, 0, 0, 0);
|
||||
case CkRadarBottomLeft:
|
||||
L->surfX[4] = canvasLeft;
|
||||
L->surfY[4] = vy + viewH;
|
||||
L->surfX[2] = L->surfX[4] + S->radW; // LL slides inboard
|
||||
break;
|
||||
case CkRadarBottomRight:
|
||||
L->surfX[4] = canvasRight - S->radW;
|
||||
L->surfY[4] = vy + viewH;
|
||||
L->surfX[3] = L->surfX[4] - S->mfdW[4]; // LR slides inboard
|
||||
break;
|
||||
case CkRadarMidLeft:
|
||||
L->surfX[4] = vx - S->radW + kCkOVL;
|
||||
L->surfY[4] = vy + (viewH - S->radH) / 2;
|
||||
break;
|
||||
case CkRadarMidRight:
|
||||
L->surfX[4] = vx + viewW - kCkOVL;
|
||||
L->surfY[4] = vy + (viewH - S->radH) / 2;
|
||||
break;
|
||||
default:
|
||||
L->surfX[4] = cx - S->radW / 2;
|
||||
L->surfY[4] = vy + viewH;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// --- radar yellow rails (6/side) + 4 bottom cells ---
|
||||
// --- MFD buttons + radar columns: L4RIOBANK, the shared under-glass field ---
|
||||
// Each bank is laid out at ITS display's scaled size, so the lamp strips and
|
||||
// the under-glass reach track a resized display instead of drifting off it.
|
||||
{
|
||||
int rx = L->surfX[4], ry = L->surfY[4];
|
||||
int slotH = kCkRADH / 6;
|
||||
for (int i = 0; i < 6; i++)
|
||||
for (int m = 0; m < 5; m++)
|
||||
{
|
||||
int y = ry + i * slotH;
|
||||
CkPushBtn(L, 0x10 + i, rx - kCkLamp, y, kCkLamp + kCkRailW, slotH - kCkGap, 1, 0, 0);
|
||||
CkPushBtn(L, 0x18 + i, rx + kCkRADW - kCkRailW, y, kCkRailW + kCkLamp, slotH - kCkGap, 1, 0, 0);
|
||||
int s = kCkMfdBank[m].slot;
|
||||
BTRioBankMetrics mfdMetrics;
|
||||
BTRioBankMetricsFor(L->surfW[s], L->surfH[s], &mfdMetrics);
|
||||
|
||||
BTRioBank bank;
|
||||
BTRioBankLayout(BTRioBankMfd, L->surfX[s], L->surfY[s],
|
||||
L->surfW[s], L->surfH[s], kCkMfdBank[m].bankHi, 0, &mfdMetrics, 0, &bank);
|
||||
BTRioBankDump(kCkMfdBank[m].name, &bank);
|
||||
for (int i = 0; i < bank.buttonCount; i++)
|
||||
{
|
||||
const BTRioButton &b = bank.buttons[i];
|
||||
CkPushBtn(L, b.address, b.x, b.y, b.w, b.h, b.colorClass, b.inert, b.label);
|
||||
}
|
||||
}
|
||||
|
||||
static const int bottomAddr[4] = { 0x16, 0x17, 0x1F, 0x1E };
|
||||
int botLeft = rx + kCkRailW, botSpan = kCkRADW - 2 * kCkRailW, botW = botSpan / 4;
|
||||
for (int i = 0; i < 4; i++)
|
||||
CkPushBtn(L, bottomAddr[i], botLeft + i * botW, ry + kCkRADH - 14, botW - kCkGap, 14 + kCkLamp, 1, 0, 0);
|
||||
BTRioBankMetrics radarMetrics;
|
||||
BTRioBankMetricsFor(L->surfW[4], L->surfH[4], &radarMetrics);
|
||||
|
||||
BTRioBank radar;
|
||||
BTRioBankLayout(BTRioBankRadar, L->surfX[4], L->surfY[4],
|
||||
L->surfW[4], L->surfH[4], 0x10, 0x18, &radarMetrics, bottomAddr, &radar);
|
||||
BTRioBankDump("surround/radar", &radar);
|
||||
for (int i = 0; i < radar.buttonCount; i++)
|
||||
{
|
||||
const BTRioButton &b = radar.buttons[i];
|
||||
CkPushBtn(L, b.address, b.x, b.y, b.w, b.h, b.colorClass, b.inert, b.label);
|
||||
}
|
||||
}
|
||||
|
||||
// --- flight blue blocks: THROTTLE/AUX (0x38-0x3F) under Mfd1, JOYSTICK
|
||||
@@ -316,18 +553,34 @@ void BTCockpitComputeLayout(int canvasW, int canvasH, BTCockpitLayout *out)
|
||||
static const int thrInert[8] = { 1, 1, 1, 1, 1, 0, 1, 0 };
|
||||
static const char *joyLabel[8] = { "Main", "Hat Bk", "Hat Up", "Hat R",
|
||||
"Hat L", "Pinky", "Middle", "Upper" }; // 0x40..0x47
|
||||
const int cw = 68, ch = 30, g = 4;
|
||||
int blockW = 4 * cw + 3 * g; // 284
|
||||
int belowLamp = kCkMFDH + kCkRedGap + kCkRedH + 8; // clear the MFD's bottom red lamps
|
||||
int thrX = L->surfX[2], thrY = L->surfY[2] + belowLamp;
|
||||
int joyX = L->surfX[3] + kCkMFDW - blockW, joyY = L->surfY[3] + belowLamp;
|
||||
for (int i = 0; i < 8; i++)
|
||||
// These cells carry TEXT, so the surround runs them bigger than the
|
||||
// exploded window's 58x28 -- the caller owns the cell size
|
||||
// (l4riobank.h), only the grid walk is shared.
|
||||
BTRioBankMetrics metrics;
|
||||
metrics.strip = 0;
|
||||
metrics.gap = 4;
|
||||
metrics.cellW = 68;
|
||||
metrics.cellH = 30;
|
||||
|
||||
// Hang each block below ITS lower MFD, clearing that display's own
|
||||
// (scaled) bottom lamp strip -- a shrunken MFD used to leave the block
|
||||
// floating in the gap, an enlarged one to overlap it.
|
||||
int blockW = 4 * metrics.cellW + 3 * metrics.gap; // 284
|
||||
int thrX = L->surfX[2];
|
||||
int thrY = L->surfY[2] + L->surfH[2] + kCkRedGap + kCkRedH + 8;
|
||||
int joyX = L->surfX[3] + L->surfW[3] - blockW;
|
||||
int joyY = L->surfY[3] + L->surfH[3] + kCkRedGap + kCkRedH + 8;
|
||||
|
||||
BTRioBank flight;
|
||||
memset(&flight, 0, sizeof(flight));
|
||||
BTRioBankFlightGrid(thrX, thrY, 0x38, 8, 4, thrLabel, thrInert, &metrics, &flight);
|
||||
BTRioBankFlightGrid(joyX, joyY, 0x40, 8, 4, joyLabel, 0, &metrics, &flight);
|
||||
BTRioBankDump("surround/flight", &flight);
|
||||
for (int i = 0; i < flight.buttonCount; i++)
|
||||
{
|
||||
int c = i % 4, r = i / 4;
|
||||
CkPushBtn(L, 0x38 + i, thrX + c * (cw + g), thrY + r * (ch + g), cw, ch, 2,
|
||||
thrInert[i], thrLabel[i] ? thrLabel[i] : "");
|
||||
CkPushBtn(L, 0x40 + i, joyX + c * (cw + g), joyY + r * (ch + g), cw, ch, 2,
|
||||
0, joyLabel[i]);
|
||||
const BTRioButton &b = flight.buttons[i];
|
||||
CkPushBtn(L, b.address, b.x, b.y, b.w, b.h, b.colorClass, b.inert,
|
||||
b.label ? b.label : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -959,8 +1212,33 @@ int BTCockpitMouseDown(int cx, int cy, int clientW, int clientH, int rightButton
|
||||
if (!gBTGaugeCockpit) return 0;
|
||||
int bbW = gBTCockpitCanvasW, bbH = gBTCockpitCanvasH;
|
||||
if (bbW <= 0 || bbH <= 0 || clientW <= 0 || clientH <= 0) return 0;
|
||||
int bx = cx * bbW / clientW;
|
||||
int by = cy * bbH / clientH;
|
||||
|
||||
// Client -> canvas. Under the letterbox the canvas occupies a centred
|
||||
// sub-rect, so the click has to come back through the SAME transform
|
||||
// Present used -- mapping against the full client would drift the hit test
|
||||
// off every button by the bar width. A click on a bar hits nothing.
|
||||
int bx, by;
|
||||
if (gBTCockpitLetterbox)
|
||||
{
|
||||
RECT fit;
|
||||
if (BTCockpitFitRect(clientW, clientH, &fit))
|
||||
{
|
||||
if (cx < fit.left || cx >= fit.right || cy < fit.top || cy >= fit.bottom)
|
||||
return 0;
|
||||
bx = (cx - fit.left) * bbW / (fit.right - fit.left);
|
||||
by = (cy - fit.top) * bbH / (fit.bottom - fit.top);
|
||||
}
|
||||
else
|
||||
{
|
||||
bx = cx * bbW / clientW;
|
||||
by = cy * bbH / clientH;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bx = cx * bbW / clientW;
|
||||
by = cy * bbH / clientH;
|
||||
}
|
||||
|
||||
BTCockpitLayout L;
|
||||
BTCockpitComputeLayout(bbW, bbH, &L);
|
||||
|
||||
+44
-11
@@ -53,6 +53,30 @@ struct BTCockpitLayout
|
||||
extern int gBTGaugeCockpit; // mode select; set once by btl4main
|
||||
extern int gBTCockpitCanvasW, gBTCockpitCanvasH; // backbuffer canvas dims (for the aspect calc)
|
||||
|
||||
//
|
||||
// LETTERBOX FIT (2026-07-26). The cockpit canvas is a FIXED size -- D3D9
|
||||
// stretches it into whatever the client area is, so a window dragged to a
|
||||
// different shape used to squash the instruments (the projection was
|
||||
// aspect-corrected, but the panels, lamps and MFD glass were not).
|
||||
//
|
||||
// Now the canvas is fitted at ONE uniform scale, centred, with the leftover
|
||||
// left black: the cockpit scales both ways and never distorts, whatever the
|
||||
// window (drag, maximise, -fit borderless). D3D9 does the scaling at Present
|
||||
// time via a destination rect, which D3DSWAPEFFECT_DISCARD forbids -- so the
|
||||
// WINDOWED swap effect becomes D3DSWAPEFFECT_COPY when the surround is up and
|
||||
// multisampling is off (COPY cannot multisample). gBTCockpitLetterbox says
|
||||
// whether that happened; when it is 0 the old full-client stretch stands, so
|
||||
// an MSAA run still works, just without the letterbox.
|
||||
//
|
||||
extern int gBTCockpitLetterbox; // 1 = the swap chain can take a dest rect
|
||||
|
||||
// The uniform-scale centred rect for a client area. Returns 1 when it differs
|
||||
// from the full client (letterboxing needed), 0 when it fills it exactly.
|
||||
int BTCockpitFitRect(int clientW, int clientH, RECT *out);
|
||||
|
||||
// Present destination for the main device window, or NULL for the full client.
|
||||
const RECT *BTCockpitPresentDest(void *hwnd, RECT *storage);
|
||||
|
||||
void BTCockpitCanvasFor(int viewW, int viewH, int *canvasW, int *canvasH);
|
||||
void BTCockpitComputeLayout(int canvasW, int canvasH, BTCockpitLayout *out);
|
||||
void BTDrawCockpitPanels(LPDIRECT3DDEVICE9 device);
|
||||
@@ -60,21 +84,30 @@ int BTCockpitMouseDown(int cx, int cy, int clientW, int clientH, int rightButto
|
||||
void BTCockpitMouseUp(void);
|
||||
|
||||
// Lamp brightness decode (RIOBase::LampState) -- shared by the cockpit draw and
|
||||
// the glass per-display windows. level = max of the two 2-bit fields; the low 2
|
||||
// bits pick a flash half-period (500/250/125 ms). Returns 0..3.
|
||||
// the glass per-display windows. Returns 0 (off) / 1 (dim) / 3 (bright).
|
||||
//
|
||||
// The wire word is TWO brightness fields plus a flash mode (L4RIO.h [T0]):
|
||||
// bits 0-1 solid=0 / flashSlow=1 / flashMed=2 / flashFast=3
|
||||
// bits 2-3 state 1 brightness (state1Off=0, state1Dim=1, state1Bright=3)
|
||||
// bits 4-5 state 2 brightness (state2Off=0, state2Dim=1, state2Bright=3)
|
||||
// Solid shows state 1; flashing ALTERNATES state 1 and state 2 at a
|
||||
// 500/250/125 ms half-period.
|
||||
//
|
||||
// ⚠ FIXED 2026-07-26: this used to return max(state1,state2) and blank to 0 on
|
||||
// the alternate phase. That agrees with the enum only when one state is Off --
|
||||
// true for the Panic lamp (L4CTRL.cpp flashFast+state1Off+state2Bright) and for
|
||||
// L4LAMP's off/dim pulse, which is why it went unnoticed -- but L4LAMP.cpp:252
|
||||
// commands flashFast + state1Dim + state2Bright, a DIM-to-BRIGHT pulse that
|
||||
// rendered as a hard BRIGHT-to-OFF blink. RP412's L4MFDVIEW LampLevel has the
|
||||
// faithful formula; this is it.
|
||||
inline int BTLampBrightnessOf(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 = (flash == 1) ? 500 : (flash == 2) ? 250 : 125;
|
||||
if ((tick / half) & 1) return 0;
|
||||
}
|
||||
return level;
|
||||
int flash = state & 0x3;
|
||||
if (flash == 0) return level1;
|
||||
unsigned long half = (flash == 1) ? 500 : (flash == 2) ? 250 : 125;
|
||||
return ((tick / half) & 1) ? level2 : level1;
|
||||
}
|
||||
|
||||
//########################################################################
|
||||
|
||||
@@ -3419,6 +3419,34 @@ DPLRenderer::DPLRenderer(
|
||||
mPresentParams.MultiSampleQuality--;
|
||||
}
|
||||
mPresentParams.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||
|
||||
//
|
||||
// COCKPIT LETTERBOX (2026-07-26): the fixed canvas is fitted into the client
|
||||
// at Present time via a destination rect, which DISCARD forbids -- COPY is
|
||||
// the swap effect that takes one. Windowed + surround only, and only with
|
||||
// multisampling off (COPY cannot multisample). Full contract: l4vb16.h.
|
||||
//
|
||||
// btl4main sets the INTENT (it has to: the first WM_SIZE beats the device
|
||||
// here); this is where it becomes real, or gets withdrawn.
|
||||
{
|
||||
extern int gBTGaugeCockpit;
|
||||
extern int gBTCockpitLetterbox;
|
||||
if (!fullscreen && gBTGaugeCockpit && gBTCockpitLetterbox &&
|
||||
mPresentParams.MultiSampleType == D3DMULTISAMPLE_NONE)
|
||||
mPresentParams.SwapEffect = D3DSWAPEFFECT_COPY;
|
||||
else
|
||||
gBTCockpitLetterbox = 0; // withdrawn -- the canvas stretches
|
||||
// only meaningful when there IS a cockpit canvas to fit
|
||||
if (gBTGaugeCockpit)
|
||||
DEBUG_STREAM << "[cockpit] letterbox " << (gBTCockpitLetterbox ? "ON" : "off")
|
||||
<< " (swap effect "
|
||||
<< (mPresentParams.SwapEffect == D3DSWAPEFFECT_COPY ? "COPY" : "DISCARD")
|
||||
<< (mPresentParams.MultiSampleType != D3DMULTISAMPLE_NONE
|
||||
? "; multisampling on -- COPY unavailable, canvas stretches to the client"
|
||||
: "")
|
||||
<< ")\n" << std::flush;
|
||||
}
|
||||
|
||||
mPresentParams.hDeviceWindow = hWnd;
|
||||
mPresentParams.Flags = 0;
|
||||
mPresentParams.FullScreen_RefreshRateInHz = (fullscreen)?60:D3DPRESENT_RATE_DEFAULT;
|
||||
@@ -8799,7 +8827,11 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
|
||||
|
||||
// DIAG (turn-hitch hunt): draw CPU is _rt0..here; Present blocks on the GPU.
|
||||
LARGE_INTEGER _rt1; QueryPerformanceCounter(&_rt1);
|
||||
hr = mDevice->Present(NULL, NULL, NULL, NULL);
|
||||
// COCKPIT LETTERBOX: uniform-scale the canvas into the client (NULL = the
|
||||
// old full-client stretch, which is what every non-cockpit run gets).
|
||||
RECT _ckDestStorage;
|
||||
const RECT *_ckDest = BTCockpitPresentDest(mPresentParams.hDeviceWindow, &_ckDestStorage);
|
||||
hr = mDevice->Present(NULL, _ckDest, NULL, NULL);
|
||||
// The wait-screen overlay stands down PERMANENTLY the moment the first
|
||||
// REAL scene frame presents (user report 2026-07-22: the launch handoff
|
||||
// briefly interleaved idle-overlay presents with game frames = flicker).
|
||||
@@ -9125,7 +9157,12 @@ void DPLRenderer::ExecuteIdle()
|
||||
}
|
||||
backbuffer->Release();
|
||||
}
|
||||
HRESULT present_hr = mDevice->Present(NULL, NULL, NULL, NULL);
|
||||
// same letterbox as the scene present -- the wait overlay paints into
|
||||
// this backbuffer, so it has to land in the same rect
|
||||
RECT wait_dest_storage;
|
||||
const RECT *wait_dest =
|
||||
BTCockpitPresentDest(mPresentParams.hDeviceWindow, &wait_dest_storage);
|
||||
HRESULT present_hr = mDevice->Present(NULL, wait_dest, NULL, NULL);
|
||||
static int s_wait_path_logged = 0;
|
||||
if (!s_wait_path_logged)
|
||||
{
|
||||
@@ -9772,12 +9809,18 @@ static float BTWorldAspectOf(int client_w, int client_h)
|
||||
// ON-SCREEN aspect = (viewW/canvasW * client_w) / (viewH/canvasH * client_h).
|
||||
extern int gBTGaugeCockpit;
|
||||
extern int gBTCockpitCanvasW, gBTCockpitCanvasH;
|
||||
extern int gBTCockpitLetterbox;
|
||||
if (gBTGaugeCockpit && gBTCockpitCanvasW > 0 && gBTCockpitCanvasH > 0)
|
||||
{
|
||||
BTCockpitLayout L;
|
||||
BTCockpitComputeLayout(gBTCockpitCanvasW, gBTCockpitCanvasH, &L);
|
||||
if (L.viewW > 0 && L.viewH > 0)
|
||||
{
|
||||
// LETTERBOXED: the canvas is scaled UNIFORMLY, so the view's
|
||||
// on-screen aspect is just its own -- the client no longer enters
|
||||
// into it (2026-07-26).
|
||||
if (gBTCockpitLetterbox)
|
||||
return (float)L.viewW / (float)L.viewH;
|
||||
float w = (float)L.viewW * (float)client_w / (float)gBTCockpitCanvasW;
|
||||
float h = (float)L.viewH * (float)client_h / (float)gBTCockpitCanvasH;
|
||||
if (h > 0.0f) return w / h;
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
//===========================================================================//
|
||||
// File: l4riobank.h //
|
||||
// Project: MUNGA_L4 Brick: cockpit RIO button-bank geometry //
|
||||
// Contents: the ONE layout rule for the buttons around a pod display //
|
||||
//---------------------------------------------------------------------------//
|
||||
// Copyright (C) 1994-1995, Virtual World Entertainment, Inc. //
|
||||
// PROPRIETARY AND CONFIDENTIAL //
|
||||
//===========================================================================//
|
||||
|
||||
#ifndef _L4RIOBANK_H_
|
||||
#define _L4RIOBANK_H_
|
||||
|
||||
//###########################################################################
|
||||
// Every pod secondary display carries a bank of illuminated RIO pushbuttons
|
||||
// mounted around its glass. We render that field TWICE -- once composited
|
||||
// into the main window (the COCKPIT SURROUND, L4VB16.cpp) and once per
|
||||
// display in its own desktop window (the EXPLODED view, L4GLASSWIN.cpp) --
|
||||
// and until 2026-07-26 each renderer carried its own copy of the geometry.
|
||||
// They drifted: the exploded windows put the buttons UNDER the glass (a big
|
||||
// click target, only a thin lamp strip clearing the edge) while the surround
|
||||
// left the MFD lamps entirely outside the surface, so the same button was a
|
||||
// 156x138 target in one mode and a 76x24 sliver in the other.
|
||||
//
|
||||
// This is that geometry, once. A caller supplies a display rect and the
|
||||
// bank's anchor address(es); it gets back the button rects IN THAT SAME
|
||||
// SPACE plus the bounding box the bank needs. Placement -- WHERE each
|
||||
// display sits -- stays with the renderer, because that genuinely differs
|
||||
// (one composited canvas vs seven desktop windows).
|
||||
//
|
||||
// THE UNDER-GLASS RULE (ported from RP412 L4MFDVIEW, which took it from the
|
||||
// pod): a button reaches half the display in BEHIND the glass and only
|
||||
// `strip` clears the edge. The renderer draws the buttons FIRST and the
|
||||
// display imagery OVER them, so the lamp reads as a slim bar while
|
||||
// practically the whole display is the press target. Nothing is occluded --
|
||||
// the picture wins every pixel it covers.
|
||||
//
|
||||
// Addresses per [[pod-hardware]] "The button space + lamps"; the anchors are
|
||||
// the caller's -- this file never hardcodes a bank address.
|
||||
//###########################################################################
|
||||
|
||||
enum BTRioBankStyle
|
||||
{
|
||||
BTRioBankMfd = 0, // 4 buttons above the glass + 4 below, addresses
|
||||
// DESCENDING from anchorA row-major (0x2F..0x28)
|
||||
BTRioBankRadar // 6 down each side (anchorA left, anchorB right)
|
||||
// + an optional 4 along the bottom
|
||||
};
|
||||
|
||||
enum
|
||||
{
|
||||
BTRioBankMaxButtons = 24
|
||||
};
|
||||
|
||||
struct BTRioButton
|
||||
{
|
||||
int address; // RIO buttonGroup address (0x00-0x47)
|
||||
int x, y, w, h; // caller-space rect (the FULL hit target)
|
||||
int colorClass; // 0 red (MFD) / 1 yellow (radar) / 2 blue (flight)
|
||||
int inert; // 1 = no authored .CTL mapping -> drawn greyed
|
||||
const char *label; // non-NULL -> the face is drawn + labelled
|
||||
};
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// How much of a button shows, and how the field is spaced. Both renderers
|
||||
// work at different scales (the surround halves the glass; the exploded
|
||||
// windows are native), so this is DERIVED from the display size -- the pod
|
||||
// rule applied at whatever size you hand it.
|
||||
//---------------------------------------------------------------------------
|
||||
struct BTRioBankMetrics
|
||||
{
|
||||
int strip; // how much of a button clears the display edge (the lamp)
|
||||
int gap; // gap between neighbouring buttons in a row/column
|
||||
int cellW, cellH; // the labelled no-glass cell (flight bank, map foot row)
|
||||
};
|
||||
|
||||
struct BTRioBank
|
||||
{
|
||||
BTRioButton buttons[BTRioBankMaxButtons];
|
||||
int buttonCount;
|
||||
|
||||
// The bank + display bounding box, in the caller's space. A window
|
||||
// caller sizes its client to boundsW/H and passes an origin that puts
|
||||
// boundsX/Y at zero; the composited caller ignores these and just uses
|
||||
// the button rects, which are already in canvas space.
|
||||
int boundsX, boundsY, boundsW, boundsH;
|
||||
};
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// The pod rule at a given display size. The lamp strip scales off the SHORT
|
||||
// axis against the native 480 (an MFD is 640x480 landscape, the map 480x640
|
||||
// portrait -- both have a 480 short axis) and never drops below a readable
|
||||
// floor, because a 5px lamp on a half-scale surround MFD is a smudge.
|
||||
//---------------------------------------------------------------------------
|
||||
void BTRioBankMetricsFor(int displayW, int displayH, BTRioBankMetrics *out);
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// Lay out one display's bank. originX/Y is the DISPLAY's top-left; button
|
||||
// rects come back around it, reaching `strip` outside the glass (so they can
|
||||
// land at negative coordinates relative to the origin -- see boundsX/Y).
|
||||
// anchorA/B bank base addresses (style-dependent; see BTRioBankStyle)
|
||||
// bottomAddrs BTRioBankRadar only: the 4 foot-row addresses, or NULL
|
||||
//---------------------------------------------------------------------------
|
||||
void BTRioBankLayout(
|
||||
BTRioBankStyle style,
|
||||
int originX,
|
||||
int originY,
|
||||
int displayW,
|
||||
int displayH,
|
||||
int anchorA,
|
||||
int anchorB,
|
||||
const BTRioBankMetrics *metrics,
|
||||
const int *bottomAddrs,
|
||||
BTRioBank *out
|
||||
);
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// The no-display bank (throttle/panic 0x38-0x3F, joystick/fire 0x40-0x47):
|
||||
// a grid of labelled cells, drawn in full since there is no glass to hide
|
||||
// behind. APPENDS to `out` (so both columns share one bank) and grows its
|
||||
// bounds; zero `out` before the first call. `labels`/`inert` are `count`
|
||||
// long and may be NULL.
|
||||
//
|
||||
// These cells carry TEXT, so their size is the caller's call, not a scale of
|
||||
// some glass -- fill metrics->cellW/cellH yourself (the surround runs bigger
|
||||
// cells than the exploded window because its canvas is half-scale and the
|
||||
// labels still have to read).
|
||||
//---------------------------------------------------------------------------
|
||||
void BTRioBankFlightGrid(
|
||||
int originX,
|
||||
int originY,
|
||||
int baseAddress,
|
||||
int count,
|
||||
int columns,
|
||||
const char *const *labels,
|
||||
const int *inert,
|
||||
const BTRioBankMetrics *metrics,
|
||||
BTRioBank *out
|
||||
);
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// One-shot geometry dump: `BT_RIOBANK_LOG=1` prints every button's address and
|
||||
// rect as the banks are built, so the field can be checked for coverage and
|
||||
// address shadowing without driving a mouse. Capped at the first few banks so
|
||||
// a per-frame caller (the surround recomputes its layout every draw) prints
|
||||
// one pass and then goes quiet.
|
||||
//---------------------------------------------------------------------------
|
||||
void BTRioBankDump(const char *tag, const BTRioBank *bank);
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user