BT_POD_RGB: the authentic RGB-split output -- one window per VGA PORT

Implements what the pod actually does (pod-hardware.md THE RGB SPLIT): a VGA
port's R/G/B lines each drive a separate mono MFD monitor, so a window is not
one MFD -- it is one PORT carrying up to three.  BT_POD_RGB=1 collapses the
five MFD windows into the two ports the cab drives (Port A: Comm=red,
Mfd2=green, Heat=blue; Port B: Mfd1=red, Mfd3=green) and composites each
group's planes into the colour channels, leaving the radar on its own
full-colour port.  Channel comes from the live port (GetEnableID), never a
hardcoded table, so an Eng-page swap follows automatically; BlankColor planes
contribute nothing, exactly as on the pod.  Implies BT_POD_SURFACES (bare
640x480 pictures -- the cab's buttons are physical).

Verified locally (bare windows, "RGB COMPOSITE of 2 plane(s): Comm Mfd2") and
LIVE ON NICK'S CRASH CART over the tailnet: Port A -> DISPLAY4, Port B ->
DISPLAY2, radar -> DISPLAY1, all exact-fit.  Pod scratch kit included (ssh
helper, layout cfgs for both modes, launcher; the mission-egg launcher fix --
a bare MP.EGG exits the mission loop with no relay, and BT_FE_SOLO parks at
the menu, so neither lights the panels).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-06 12:55:05 -05:00
co-authored by Claude Fable 5
parent e0e9dcc292
commit e179c7033f
4 changed files with 155 additions and 11 deletions
+130 -1
View File
@@ -115,6 +115,14 @@ struct GWin
int wantX, wantY;
int restored; // wantX/wantY came from glass_layout.cfg -> don't re-snap
int noFrame; // ",noframe" in the cfg -> WS_POPUP, drag by the surface
// RGB CHANNEL GROUP (BT_POD_RGB): on splitter-wired pod glass this window
// is one VGA PORT feeding up to three mono monitors through R/G/B. These
// are the OTHER surfaces sharing the port; each is composited into its own
// channel by BlitSurface. groupCount 0 = ordinary single-surface window.
const char *groupPort[3];
const char *groupAlt[3]; // the Eng<n> twin, or NULL
int groupCount;
};
static GWin gWins[8];
@@ -124,7 +132,8 @@ 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 unsigned long *gStage = NULL; // 640*480 BGRA staging (shared, main-thread)
static unsigned long *gStage2 = NULL; // RGB composite scratch (BT_POD_RGB)
static HFONT titleFont = NULL;
static HFONT buttonFont = NULL;
@@ -417,6 +426,21 @@ static const char *layoutFileName = "glass_layout.cfg";
// The Flight Controls window (no surface at all) is not created.
// BT_POD_SURFACES=1 -> every display window goes bare
// glass_layout.cfg "<title>=x,y,bare" -> just that one (mixed rigs)
// BT_POD_RGB=1 -- the pod's REAL video wiring (see pod-hardware.md §THE RGB
// SPLIT): one VGA port per window, its R/G/B analog lines split to three
// monochrome MFD monitors. Implies pod surface mode.
static int
PodRgbSplitMode()
{
static int m = -1;
if (m < 0)
{
const char *e = getenv("BT_POD_RGB");
m = (e != NULL && e[0] != 0 && e[0] != '0') ? 1 : 0;
}
return m;
}
static int
PodSurfaceMode()
{
@@ -425,6 +449,8 @@ static int
{
const char *e = getenv("BT_POD_SURFACES");
m = (e != NULL && *e != '\0' && *e != '0') ? 1 : 0;
if (!m && PodRgbSplitMode())
m = 1; // the RGB split is pod glass -- always bare
if (m)
DEBUG_STREAM << "[glasswin] POD SURFACE MODE: bare surfaces, no "
"button banks (physical pod buttons assumed)\n" << std::flush;
@@ -817,6 +843,71 @@ static void
}
int ow = 0, oh = 0;
svga->ExpandPlaneToBGRA(mask, palId, tint, w->rotate, gStage, &ow, &oh);
// ---------------------------------------------------------------------
// RGB CHANNEL COMPOSITE (BT_POD_RGB, 2026-08-06) -- the AUTHENTIC pod
// output. The cab does not give each mono MFD its own video port: one
// VGA port's R/G/B analog lines are SPLIT to three monochrome monitors,
// and the game puts a different surface in each colour channel of one
// palettized framebuffer (L4GAUGE.CFG: Comm=red, Mfd2=green, Heat=blue
// on clut2; Mfd1=red, Mfd3=green on clut1 -- and
// L4GraphicsPort::BuildSecondaryColor writes exactly ONE component per
// port, which IS the split). So on splitter-wired glass this window is
// not one MFD: it is one VGA PORT, carrying up to three. Composite the
// group's planes into this frame, each in its own channel, and let the
// hardware separate them again.
//
// Channel comes from the port itself (GetEnableID), so the authored
// config -- including a page swap that hands the channel to an Eng
// plane -- decides, never a hardcoded table here.
// ---------------------------------------------------------------------
if (w->groupCount > 0 && ow > 0 && oh > 0)
{
const int n = ow * oh;
for (int gi = 0; gi < w->groupCount; ++gi)
{
L4GraphicsPort *gp =
static_cast<L4GraphicsPort*>(gr->GetGraphicsPort(w->groupPort[gi]));
if (gp == NULL)
continue;
// honour the live Mfd<n>/Eng<n> page swap: take whichever twin
// currently owns a channel
if (gp->GetEnableID() == L4GraphicsPort::BlankColor
&& w->groupAlt[gi] != NULL)
{
L4GraphicsPort *ga =
static_cast<L4GraphicsPort*>(gr->GetGraphicsPort(w->groupAlt[gi]));
if (ga != NULL && ga->GetEnableID() != L4GraphicsPort::BlankColor)
gp = ga;
}
const int ch = gp->GetEnableID();
unsigned long chMask;
switch (ch & 0x7F)
{
case L4GraphicsPort::RedChannel: chMask = 0x00FF0000ul; break;
case L4GraphicsPort::GreenChannel: chMask = 0x0000FF00ul; break;
case L4GraphicsPort::BlueChannel: chMask = 0x000000FFul; break;
default: continue; // blank / direct-colour -> contributes nothing
}
int gw = 0, gh = 0;
// -1 tint would palette-expand; pass white so the plane comes back
// as full-intensity mono, then keep only this port's channel.
svga->ExpandPlaneToBGRA(gp->GetBitMask(), gp->paletteID,
(int)0x00FFFFFF, w->rotate, gStage2, &gw, &gh);
if (gw != ow || gh != oh)
continue;
for (int i = 0; i < n; ++i)
gStage[i] |= (gStage2[i] & chMask);
}
if (log)
{
DEBUG_STREAM << "[glass] '" << w->title << "' RGB COMPOSITE of "
<< w->groupCount << " plane(s):";
for (int gi = 0; gi < w->groupCount; ++gi)
DEBUG_STREAM << " " << w->groupPort[gi];
DEBUG_STREAM << "\n" << std::flush;
}
}
if (log)
{
int nz = 0, n = ow * oh;
@@ -1126,6 +1217,8 @@ void
sRepositionedToMain = false;
if (gStage == NULL)
gStage = new unsigned long[640 * 480];
if (gStage2 == NULL)
gStage2 = 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
@@ -1147,6 +1240,42 @@ void
gWinCount = 6; // drop Flight Controls
}
// RGB SPLIT MODE (BT_POD_RGB): the cab's real wiring -- ONE VGA port per
// window, its R/G/B lines split to three mono monitors. Collapse the five
// separate MFD windows into the TWO ports the pod actually drives
// (L4GAUGE.CFG clut2 = Comm/Mfd2/Heat, clut1 = Mfd1/Mfd3), each window
// compositing its group into the colour channels; the radar keeps its own
// full-colour port. Channels are read live from each port, so a page swap
// to an Eng plane follows automatically.
if (PodRgbSplitMode())
{
GWin port2 = gWins[0]; // reuse the Heat window's shape
port2.title = "VGA Port A"; // Comm=red, Mfd2=green, Heat=blue
port2.portPrimary = "Heat"; // base plane (blue); group adds the rest
port2.portAlt = NULL;
port2.groupCount = 2;
port2.groupPort[0] = "Comm"; port2.groupAlt[0] = NULL;
port2.groupPort[1] = "Mfd2"; port2.groupAlt[1] = "Eng2";
port2.monoTint = (int)0x000000FF; // Heat rides BLUE
GWin port1 = gWins[3]; // the Mfd1 window's shape
port1.title = "VGA Port B"; // Mfd1=red, Mfd3=green (blue spare)
port1.portPrimary = "Mfd1";
port1.portAlt = "Eng1";
port1.groupCount = 1;
port1.groupPort[0] = "Mfd3"; port1.groupAlt[0] = "Eng3";
port1.monoTint = (int)0x00FF0000; // Mfd1 rides RED
GWin radar = gWins[5]; // unchanged: its own colour port
gWins[0] = port2;
gWins[1] = port1;
gWins[2] = radar;
gWinCount = 3;
DEBUG_STREAM << "[glasswin] RGB SPLIT MODE: 2 VGA ports + radar "
"(each port's R/G/B feeds three mono panels)\n" << std::flush;
}
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,