A Live Cam keeps the map and drops the panes

A Live Cam host raced with the full glass cockpit over its view: five
instrument MFD panes with no pod behind them, compositing as black
rectangles across the viewscreen, and the map stood on end. Both are
cabinet artefacts. The panes belong to a pod; the map is portrait because
that is how the glass was bolted into the cabinet. A camera has no
cabinet, so it keeps the map - landscape - and nothing else. The arcade
got the same result by running its camera cabinet with -lc.

The role is picked in the lobby, so there is no -lc on the command line to
carry it to the renderers. Application::IsCameraStation joins
suppressGauges, set by the front end where the egg settles
owner_is_camera - before the renderers exist, since the single-binary race
loop builds a fresh application per race after the menu. Set on every
launch either way: a stale True would strip the cockpit off the next pod.

In the compositor: the five MFD panes are not built, which LayoutCockpit
and FillSplitMFD already tolerate, so no layout surgery; the map's glass
swaps to 432x324 while still honouring L4RADARSCALE and L4RADARPOS; and
the per-frame copy runs straight through rather than rotating 90 degrees
clockwise. The pane's source dimensions swap to match, so the buffer the
copy fills is the size the pane allocated - checked both ways.

Untested on screen, unlike the rest of this work: it only engages with a
lobby member present and that needs the second machine. What is checked is
that it builds, that the buffer arithmetic agrees, and that every pane
access on the glass-cockpit path is NULL-guarded.

L4MFDSPLIT=2 and =0 are left alone. The exploded view still opens five
empty MFD windows for a camera; it is a diagnostic view where that is
arguably the point, and its layout derefs the panes unguarded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-10 15:56:58 -05:00
co-authored by Claude Opus 5
parent aa2ca0376a
commit af82cac5d2
4 changed files with 123 additions and 35 deletions
+1
View File
@@ -33,6 +33,7 @@
Application *application = NULL;
int Exit_Code = 0;
Logical Application::suppressGauges = False;
Logical Application::cameraStation = False;
//
// RP412CAMLOG - see app.h. Cached: the waiting trace asks once a second
+16
View File
@@ -454,6 +454,21 @@ public:
static Logical DoSuppressGauges() { return suppressGauges; }
//
// A Live Cam station. It has no pod, so none of the five instrument
// MFDs have anything to put on them - the panes would just composite
// as black holes over the view. It DOES keep the map, but landscape:
// the pod's is portrait because that is how the glass was physically
// mounted in the cabinet, and a camera has no cabinet.
//
// Settable because the role is picked in the lobby rather than passed
// as -lc on the command line, so the front end has to hand it over.
// That happens before the renderers are built - the single-binary race
// loop makes a fresh application per race, after the menu.
//
static Logical IsCameraStation() { return cameraStation; }
static void SetCameraStation(Logical state) { cameraStation = state; }
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Modules
//
@@ -489,6 +504,7 @@ protected:
*modeManager;
static Logical suppressGauges;
static Logical cameraStation;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Module Creation
+93 -35
View File
@@ -4054,11 +4054,24 @@ void
<< percent[SplitMap] << "%\n" << std::flush;
}
//
// A Live Cam's map lies down: the pod's is 324x432 because the glass
// was mounted portrait in the cabinet, and a camera station has no
// cabinet, so the same pixels are shown the way they are drawn.
//
const Logical camera_station = Application::IsCameraStation();
GlassSize glass[SplitViewCount];
for (int view = 0; view < SplitViewCount; ++view)
{
int base_w = (view == SplitMap) ? cockpitRadarBaseW : cockpitMfdBaseW;
int base_h = (view == SplitMap) ? cockpitRadarBaseH : cockpitMfdBaseH;
if (view == SplitMap && camera_station)
{
int swap = base_w;
base_w = base_h;
base_h = swap;
}
glass[view].w = (base_w * percent[view]) / 100;
glass[view].h = (base_h * percent[view]) / 100;
}
@@ -4656,31 +4669,53 @@ SVGA16::SVGA16(
GlassSize glass[SplitViewCount];
CockpitGlassSizes(scale_num, glass);
splitView[SplitMFDUpperLeft] = new MFDSplitView(
"MFD upper left", init_width, init_height,
glass[SplitMFDUpperLeft].w, glass[SplitMFDUpperLeft].h, 0, 0,
MFDSplitView::MFDStrips, 0x2F, 0, cockpit);
splitView[SplitMFDUpperCenter] = new MFDSplitView(
"MFD upper center", init_width, init_height,
glass[SplitMFDUpperCenter].w, glass[SplitMFDUpperCenter].h, 0, 0,
MFDSplitView::MFDStrips, 0x27, 0, cockpit);
splitView[SplitMFDUpperRight] = new MFDSplitView(
"MFD upper right", init_width, init_height,
glass[SplitMFDUpperRight].w, glass[SplitMFDUpperRight].h, 0, 0,
MFDSplitView::MFDStrips, 0x37, 0, cockpit);
splitView[SplitMFDLowerLeft] = new MFDSplitView(
"MFD lower left", init_width, init_height,
glass[SplitMFDLowerLeft].w, glass[SplitMFDLowerLeft].h, 0, 0,
MFDSplitView::MFDStrips, 0x0F, 0, cockpit);
splitView[SplitMFDLowerRight] = new MFDSplitView(
"MFD lower right", init_width, init_height,
glass[SplitMFDLowerRight].w, glass[SplitMFDLowerRight].h, 0, 0,
MFDSplitView::MFDStrips, 0x07, 0, cockpit);
// map is portrait: source rotated 90 degrees clockwise
splitView[SplitMap] = new MFDSplitView(
"Map", init_height, init_width,
glass[SplitMap].w, glass[SplitMap].h, 0, 0,
MFDSplitView::SideColumns, 0x10, 0x18, cockpit);
//---------------------------------------------------------------
// A Live Cam gets the map and nothing else. The five instrument
// MFDs belong to a pod: with no pod behind them their panes carry
// no image at all and composite as black rectangles over the
// viewscreen, which is what the arcade's camera cabinet avoided by
// running with -lc. LayoutCockpit and FillSplitMFD both already
// skip a NULL pane, so leaving them unbuilt is all it takes.
//---------------------------------------------------------------
if (!Application::IsCameraStation())
{
splitView[SplitMFDUpperLeft] = new MFDSplitView(
"MFD upper left", init_width, init_height,
glass[SplitMFDUpperLeft].w, glass[SplitMFDUpperLeft].h, 0, 0,
MFDSplitView::MFDStrips, 0x2F, 0, cockpit);
splitView[SplitMFDUpperCenter] = new MFDSplitView(
"MFD upper center", init_width, init_height,
glass[SplitMFDUpperCenter].w, glass[SplitMFDUpperCenter].h, 0, 0,
MFDSplitView::MFDStrips, 0x27, 0, cockpit);
splitView[SplitMFDUpperRight] = new MFDSplitView(
"MFD upper right", init_width, init_height,
glass[SplitMFDUpperRight].w, glass[SplitMFDUpperRight].h, 0, 0,
MFDSplitView::MFDStrips, 0x37, 0, cockpit);
splitView[SplitMFDLowerLeft] = new MFDSplitView(
"MFD lower left", init_width, init_height,
glass[SplitMFDLowerLeft].w, glass[SplitMFDLowerLeft].h, 0, 0,
MFDSplitView::MFDStrips, 0x0F, 0, cockpit);
splitView[SplitMFDLowerRight] = new MFDSplitView(
"MFD lower right", init_width, init_height,
glass[SplitMFDLowerRight].w, glass[SplitMFDLowerRight].h, 0, 0,
MFDSplitView::MFDStrips, 0x07, 0, cockpit);
}
//
// The map's source canvas is landscape and the pod rotates it 90
// degrees clockwise to match its portrait mounting, which is why
// the pane is built with the source dimensions swapped. A camera
// shows it as drawn, so it is not swapped and not rotated (see the
// copy in ExecuteBackground).
//
splitView[SplitMap] = Application::IsCameraStation()
? new MFDSplitView(
"Map", init_width, init_height,
glass[SplitMap].w, glass[SplitMap].h, 0, 0,
MFDSplitView::SideColumns, 0x10, 0x18, cockpit)
: new MFDSplitView(
"Map", init_height, init_width,
glass[SplitMap].w, glass[SplitMap].h, 0, 0,
MFDSplitView::SideColumns, 0x10, 0x18, cockpit);
//---------------------------------------------------------------
// Everything above built the panes at a starting size; the
@@ -5136,23 +5171,46 @@ Logical SVGA16::Update(Logical forceAll)
{
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)
if (Application::IsCameraStation())
{
for (int dx = 0; dx < src_h; ++dx)
//
// Live Cam: straight through, row by row. The pod's
// rotation below is about how the glass was bolted
// into the cabinet, and there is no cabinet here.
//
for (int dy = 0; dy < src_h; ++dy)
{
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);
for (int dx = 0; dx < src_w; ++dx)
{
Word pixel = source_base[dy * src_w + dx];
PaletteTriplet *entry =
&secPalette->paletteData.Color[pixel & secMask];
*dest++ = ((unsigned long) entry->Red << 16) |
((unsigned long) entry->Green << 8) |
((unsigned long) entry->Blue);
}
}
}
else
{
// Map is portrait-mounted: rotate 90 degrees clockwise.
// dest(x,y) = source(row = srcH-1-x, col = 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();
+13
View File
@@ -1621,6 +1621,19 @@ namespace
<< std::flush;
}
//
// Tell the renderers, which are built after this menu and need to
// know not to hang five empty instrument panes over the view. Set
// either way: the same process races again after this one, and a
// stale True would strip the cockpit off a pod.
//
Application::SetCameraStation(owner_is_camera);
if (owner_is_camera)
{
DEBUG_STREAM << "FE: Live Cam host - no instrument panes, map landscape\n"
<< std::flush;
}
for (int p = 0; p < pilot_count; ++p)
{
Logical camera_entry = (p == 0 && owner_is_camera);