Un-pack the 7-display cockpit in-engine (L4MFDSPLIT=1)

The pod drove five monochrome MFDs from the color channels of two video
outputs - SVGA16 packs bit-slices of the shared gauge canvas into R/G/B
of gauge window 3 (upper MFDs) and R/G of window 4 (lower MFDs), with the
map palettized on the secondary and physically mounted portrait. The
desktop reconstruction previously required an external BitBlt-mirror
wrapper.

With L4MFDSPLIT=1, SVGA16 renders each display into its own window
(MFDSplitView, plain GDI) straight from the canvas + port bit-masks:
five green-screen MFD windows and the 90CW-rotated Map, tiled in the pod
grid to the right of the main view (L4MFDSCALE percent, default 50). The
packed D3D windows stay hidden but keep presenting off-screen, leaving
the original path untouched. Handles spanning mode (2-window setups).

Also: the plasma glass now opens directly below the main view (clamped
to the work area; L4PLASMAPOS=x,y overrides) per playtest feedback.

Verified: window grid comes up as main + 5 MFDs + Map + plasma with the
packed windows hidden; screenshots confirm a green MFD score readout and
the portrait tactical map rendering correctly. dist packer and BUILD.md
updated; the launcher wrapper is obsolete for split-mode use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-12 14:10:52 -05:00
co-authored by Claude Fable 5
parent 7362fd0811
commit 214a8e079c
10 changed files with 497 additions and 8 deletions
+171 -1
View File
@@ -2,6 +2,7 @@
#pragma hdrstop
#include "l4vb16.h"
#include "l4mfdview.h"
#include "../munga/gaugrend.h"
#include "L4VIDEO.h"
#include "DXUtils.h"
@@ -176,7 +177,12 @@ void SVGA16::BuildWindows(unsigned int width, unsigned int height, bool windowed
gaugeWindows[j] = CreateWindowEx(0, L"MainWndClass", L"RPL4", WS_BORDER, surfaceWindowRect.left, surfaceWindowRect.top, surfaceWindowRect.right-surfaceWindowRect.left, surfaceWindowRect.bottom-surfaceWindowRect.top, (HWND)NULL, (HMENU)NULL, L4Application::GetAppInstance(), (LPVOID)NULL);
if (gaugeWindows[j])
{
ShowWindow(gaugeWindows[j], SW_SHOW);
// In split-view mode the packed windows are only kept as D3D
// device targets; the split windows are what the player sees.
if (!splitViews)
{
ShowWindow(gaugeWindows[j], SW_SHOW);
}
memset(&mPresentParams[j], 0, sizeof(D3DPRESENT_PARAMETERS));
@@ -3809,7 +3815,76 @@ SVGA16::SVGA16(
int *aux2Index
):Video16BitBuffered(init_width, init_height)
{
//------------------------------------------------------------------
// Split-view mode: decide before BuildWindows so the packed gauge
// windows can stay hidden.
//------------------------------------------------------------------
splitViews = False;
{
const char *split_string = getenv("L4MFDSPLIT");
if (split_string != NULL && atoi(split_string) != 0)
{
splitViews = True;
}
}
for (int view = 0; view < SplitViewCount; ++view)
{
splitView[view] = NULL;
}
BuildWindows(init_width,init_height,windowed, secondaryIndex, aux1Index, aux2Index);
if (splitViews)
{
//---------------------------------------------------------------
// One window per cockpit display. L4MFDSCALE is the view size in
// percent of the gauge canvas (default 50). Default layout is the
// pod grid to the right of the main view:
// [ main ] [ UL ] [ C ] [ UR ]
// [ LL ] [ map ] [ LR ]
//---------------------------------------------------------------
int scale_percent = 50;
const char *scale_string = getenv("L4MFDSCALE");
if (scale_string != NULL)
{
int requested = atoi(scale_string);
if (requested >= 10 && requested <= 200)
{
scale_percent = requested;
}
}
int cell_w = (init_width * scale_percent) / 100;
int cell_h = (init_height * scale_percent) / 100;
int frame_h = GetSystemMetrics(SM_CYCAPTION) + 2 * GetSystemMetrics(SM_CYFIXEDFRAME);
int frame_w = 2 * GetSystemMetrics(SM_CXFIXEDFRAME);
int grid_x = init_width + frame_w;
int row2_y = cell_h + frame_h;
splitView[SplitMFDUpperLeft] = new MFDSplitView(
"MFD upper left", init_width, init_height,
cell_w, cell_h, grid_x, 0);
splitView[SplitMFDUpperCenter] = new MFDSplitView(
"MFD upper center", init_width, init_height,
cell_w, cell_h, grid_x + (cell_w + frame_w), 0);
splitView[SplitMFDUpperRight] = new MFDSplitView(
"MFD upper right", init_width, init_height,
cell_w, cell_h, grid_x + 2 * (cell_w + frame_w), 0);
splitView[SplitMFDLowerLeft] = new MFDSplitView(
"MFD lower left", init_width, init_height,
cell_w, cell_h, grid_x, row2_y);
splitView[SplitMFDLowerRight] = new MFDSplitView(
"MFD lower right", init_width, init_height,
cell_w, cell_h, grid_x + 2 * (cell_w + frame_w), row2_y);
// map is portrait: source rotated 90 degrees clockwise
splitView[SplitMap] = new MFDSplitView(
"Map", init_height, init_width,
cell_h, cell_w,
grid_x + (cell_w + frame_w) + (cell_w - cell_h) / 2, row2_y);
DEBUG_STREAM << "SVGA16: split views active - 5 MFD windows + rotated map (scale "
<< scale_percent << "%)\n" << std::flush;
}
//STUBBED: VIDEO RB 1/15/07
# if defined(DEBUG)
Tell("SVGA16::SVGA16()\n");
@@ -3993,6 +4068,13 @@ SVGA16::~SVGA16()
// Set VWE video splitter clock divider
//---------------------------------------------------------
//SVGASetSplitterClock(False);
for (int view = 0; view < SplitViewCount; ++view)
{
delete splitView[view];
splitView[view] = NULL;
}
Check_Fpu();
}
@@ -4029,6 +4111,43 @@ int GetShiftAmount(DWORD mask)
return amount;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Render one MFD's bit-slice of the shared canvas into its split-view
// window as a green screen. The mask arrives in the duplicated
// (mask | mask<<16) form the packed path uses.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
void SVGA16::FillSplitMFD(
SplitViewID view,
DWORD mask,
int shift
)
{
if (splitView[view] == NULL)
{
return;
}
Word mask16 = (Word)(mask & 0xFFFF);
Word max_value = (Word)(mask16 >> shift);
if (max_value == 0)
{
return;
}
Word *source = pixelBuffer.Data.MapPointer;
unsigned long *dest = splitView[view]->Pixels();
int count = pixelBuffer.Data.Size.x * pixelBuffer.Data.Size.y;
for (int i = 0; i < count; ++i)
{
unsigned long value = (unsigned long)((*source++ & mask16) >> shift);
unsigned long green = (value * 255ul) / max_value;
*dest++ = green << 8;
}
splitView[view]->Repaint();
}
Logical SVGA16::Update(Logical forceAll)
{
HRESULT hr;
@@ -4101,6 +4220,57 @@ Logical SVGA16::Update(Logical forceAll)
}
}
//------------------------------------------------------------------
// Split views: render this window's displays into their own desktop
// windows, straight from the shared canvas (CPU-side, no D3D).
//------------------------------------------------------------------
if (splitViews)
{
if (mDisplayToUpdate == 0)
{
if (splitView[SplitMap] != NULL)
{
// Map is portrait-mounted: rotate 90 degrees clockwise.
// dest(x,y) = source(row = srcH-1-x, col = y)
Word *source_base = pixelBuffer.Data.MapPointer;
unsigned long *dest = splitView[SplitMap]->Pixels();
int src_w = pixelBuffer.Data.Size.x;
int src_h = pixelBuffer.Data.Size.y;
for (int dy = 0; dy < src_w; ++dy)
{
for (int dx = 0; dx < src_h; ++dx)
{
Word pixel = source_base[(src_h - 1 - dx) * src_w + dy];
PaletteTriplet *entry =
&secPalette->paletteData.Color[pixel & secMask];
*dest++ = ((unsigned long) entry->Red << 16) |
((unsigned long) entry->Green << 8) |
((unsigned long) entry->Blue);
}
}
splitView[SplitMap]->Repaint();
}
}
else if (mDisplayToUpdate == 1)
{
FillSplitMFD(SplitMFDUpperLeft, ulMask, ulMask_sh);
FillSplitMFD(SplitMFDUpperCenter, ucMask, ucMask_sh);
FillSplitMFD(SplitMFDUpperRight, urMask, urMask_sh);
if (NUMGAUGEWINDOWS < 3)
{
// spanning mode: the lower MFDs ride this window too
FillSplitMFD(SplitMFDLowerLeft, llMask, llMask_sh);
FillSplitMFD(SplitMFDLowerRight, lrMask, lrMask_sh);
}
}
else
{
FillSplitMFD(SplitMFDLowerLeft, llMask, llMask_sh);
FillSplitMFD(SplitMFDLowerRight, lrMask, lrMask_sh);
}
}
Word *data = pixelBuffer.Data.MapPointer;
D3DLOCKED_RECT rect;