It is a draggable top-level window whenever L4PLASMA=SCREEN, which makes it the last one still being placed fresh every launch. It joins mfd_layout.cfg under "Plasma Display", the caption it already carries - the same key-is-the-title rule the display panes follow. Position only, like the panes: the glass is 128x32 at L4PLASMASCALE, so its size is a setting rather than something to drag. It registers and loads at the point it creates its window rather than leaving that to SVGA16. The glass comes from the gauge renderer and the panes from the video mode, and nothing guarantees which is built first; loading in both places means whichever runs second simply re-applies a placement the first already has. Its window procedure picks up the same WM_EXITSIZEMOVE save the panes have, so a drag writes the file straight away, and the destructor forgets the window before destroying it. Verified by round trip in the exploded view: dragged to 640,880, the file took "Plasma Display=640,880,528,167" alongside the panes and the game window, and a fresh launch in load mode put it physically back at 640,880. The three load lines in the log - one window, then two, then eight - are the ordering doing its job. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
348 lines
8.6 KiB
C++
348 lines
8.6 KiB
C++
#include "mungal4.h"
|
|
#pragma hdrstop
|
|
|
|
#include "l4plasmascreen.h"
|
|
#include "l4app.h"
|
|
#include "l4mfdview.h" // RPWindowLayout_*
|
|
|
|
namespace
|
|
{
|
|
const char plasmaWindowClass[] = "RPPlasmaScreen";
|
|
|
|
LRESULT CALLBACK
|
|
PlasmaWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|
{
|
|
PlasmaScreen *screen =
|
|
(PlasmaScreen *) GetWindowLongPtrA(hwnd, GWLP_USERDATA);
|
|
|
|
switch (message)
|
|
{
|
|
case WM_PAINT:
|
|
if (screen != NULL)
|
|
{
|
|
// handled below through the friend-free public surface:
|
|
// the window procedure only asks the screen to repaint.
|
|
PAINTSTRUCT ps;
|
|
HDC hdc = BeginPaint(hwnd, &ps);
|
|
// PlasmaScreen stores what StretchDIBits needs in
|
|
// window properties to keep the class decoupled.
|
|
const BITMAPINFO *bmi =
|
|
(const BITMAPINFO *) GetPropA(hwnd, "PlasmaBitmapInfo");
|
|
const void *pixels = GetPropA(hwnd, "PlasmaPixels");
|
|
RECT client;
|
|
GetClientRect(hwnd, &client);
|
|
if (bmi != NULL && pixels != NULL)
|
|
{
|
|
SetStretchBltMode(hdc, COLORONCOLOR);
|
|
StretchDIBits(
|
|
hdc,
|
|
0, 0, client.right, client.bottom,
|
|
0, 0, PlasmaScreen::plasmaWidth, PlasmaScreen::plasmaHeight,
|
|
pixels, bmi, DIB_RGB_COLORS, SRCCOPY
|
|
);
|
|
}
|
|
EndPaint(hwnd, &ps);
|
|
return 0;
|
|
}
|
|
break;
|
|
|
|
case WM_EXITSIZEMOVE:
|
|
//
|
|
// Dragged to a new spot. Write the whole arrangement now rather
|
|
// than trusting teardown, so a hard kill still leaves what was
|
|
// on screen. No-op unless RP412MFDLAYOUT is save.
|
|
//
|
|
RPWindowLayout_Save();
|
|
return 0;
|
|
|
|
case WM_CLOSE:
|
|
// The glass is part of the cockpit; just hide it.
|
|
ShowWindow(hwnd, SW_HIDE);
|
|
return 0;
|
|
|
|
case WM_ERASEBKGND:
|
|
return 1;
|
|
}
|
|
return DefWindowProcA(hwnd, message, wParam, lParam);
|
|
}
|
|
}
|
|
|
|
//########################################################################
|
|
//############################ PlasmaScreen ##############################
|
|
//########################################################################
|
|
|
|
PlasmaScreen *PlasmaScreen::activeInstance = NULL;
|
|
|
|
void
|
|
PlasmaScreen::Position(void *parent, int x, int y, int client_width, int client_height)
|
|
{
|
|
if (activeInstance == NULL || activeInstance->window == NULL)
|
|
{
|
|
return;
|
|
}
|
|
|
|
HWND glass = (HWND) activeInstance->window;
|
|
int window_w = client_width;
|
|
int window_h = client_height;
|
|
|
|
if (parent != NULL)
|
|
{
|
|
// fold the glass into the cockpit window as a chrome-less pane
|
|
SetWindowLongPtrA(glass, GWL_STYLE, WS_CHILD | WS_VISIBLE);
|
|
SetParent(glass, (HWND) parent);
|
|
}
|
|
else
|
|
{
|
|
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
|
|
RECT bounds;
|
|
bounds.left = 0;
|
|
bounds.top = 0;
|
|
bounds.right = client_width;
|
|
bounds.bottom = client_height;
|
|
AdjustWindowRect(&bounds, style, FALSE);
|
|
window_w = bounds.right - bounds.left;
|
|
window_h = bounds.bottom - bounds.top;
|
|
}
|
|
|
|
SetWindowPos(
|
|
glass, NULL,
|
|
x, y,
|
|
window_w,
|
|
window_h,
|
|
SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW
|
|
);
|
|
}
|
|
|
|
void
|
|
PlasmaScreen::Hide()
|
|
{
|
|
if (activeInstance != NULL && activeInstance->window != NULL)
|
|
{
|
|
ShowWindow((HWND) activeInstance->window, SW_HIDE);
|
|
}
|
|
}
|
|
|
|
PlasmaScreen::PlasmaScreen():
|
|
Video8BitBuffered(plasmaWidth, plasmaHeight)
|
|
{
|
|
Check_Pointer(this);
|
|
|
|
window = NULL;
|
|
bitmapInfo = NULL;
|
|
|
|
scale = 4;
|
|
const char *scale_string = getenv("L4PLASMASCALE");
|
|
if (scale_string != NULL)
|
|
{
|
|
int requested = atoi(scale_string);
|
|
if (requested >= 1 && requested <= 16)
|
|
{
|
|
scale = requested;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Build the 8bpp DIB header with the plasma-orange palette:
|
|
// index 0 is the dark glass, everything else lights up.
|
|
//---------------------------------------------------------------
|
|
BITMAPINFOHEADER *header = (BITMAPINFOHEADER *) new char[
|
|
sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD)];
|
|
memset(header, 0, sizeof(BITMAPINFOHEADER));
|
|
header->biSize = sizeof(BITMAPINFOHEADER);
|
|
header->biWidth = plasmaWidth;
|
|
header->biHeight = -plasmaHeight; // top-down, matching PixelMap8
|
|
header->biPlanes = 1;
|
|
header->biBitCount = 8;
|
|
header->biCompression = BI_RGB;
|
|
header->biClrUsed = 256;
|
|
|
|
RGBQUAD *palette = (RGBQUAD *)((char *) header + sizeof(BITMAPINFOHEADER));
|
|
for (int i = 0; i < 256; ++i)
|
|
{
|
|
if (i == 0)
|
|
{
|
|
palette[i].rgbRed = 24; palette[i].rgbGreen = 10; palette[i].rgbBlue = 4;
|
|
}
|
|
else
|
|
{
|
|
palette[i].rgbRed = 255; palette[i].rgbGreen = 144; palette[i].rgbBlue = 32;
|
|
}
|
|
palette[i].rgbReserved = 0;
|
|
}
|
|
bitmapInfo = header;
|
|
|
|
CreateGlassWindow();
|
|
|
|
Tell("PlasmaScreen: on-screen plasma display, scale x" << scale << "\n");
|
|
}
|
|
|
|
PlasmaScreen::~PlasmaScreen()
|
|
{
|
|
Check_Pointer(this);
|
|
|
|
if (activeInstance == this)
|
|
{
|
|
activeInstance = NULL;
|
|
}
|
|
if (window != NULL)
|
|
{
|
|
RPWindowLayout_Forget(window);
|
|
RemovePropA((HWND) window, "PlasmaBitmapInfo");
|
|
RemovePropA((HWND) window, "PlasmaPixels");
|
|
DestroyWindow((HWND) window);
|
|
window = NULL;
|
|
}
|
|
delete [] (char *) bitmapInfo;
|
|
bitmapInfo = NULL;
|
|
}
|
|
|
|
Logical
|
|
PlasmaScreen::TestInstance() const
|
|
{
|
|
return valid;
|
|
}
|
|
|
|
void
|
|
PlasmaScreen::CreateGlassWindow()
|
|
{
|
|
HINSTANCE instance = GetModuleHandleA(NULL);
|
|
|
|
static Logical class_registered = False;
|
|
if (!class_registered)
|
|
{
|
|
class_registered = True;
|
|
|
|
WNDCLASSA window_class;
|
|
memset(&window_class, 0, sizeof(window_class));
|
|
window_class.style = CS_HREDRAW | CS_VREDRAW;
|
|
window_class.lpfnWndProc = PlasmaWndProc;
|
|
window_class.hInstance = instance;
|
|
window_class.hCursor = LoadCursor(NULL, IDC_ARROW);
|
|
window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
|
|
window_class.lpszClassName = plasmaWindowClass;
|
|
RegisterClassA(&window_class);
|
|
}
|
|
|
|
RECT bounds;
|
|
bounds.left = 0;
|
|
bounds.top = 0;
|
|
bounds.right = plasmaWidth * scale;
|
|
bounds.bottom = plasmaHeight * scale;
|
|
AdjustWindowRect(&bounds, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, FALSE);
|
|
|
|
int window_w = bounds.right - bounds.left;
|
|
int window_h = bounds.bottom - bounds.top;
|
|
|
|
//---------------------------------------------------------------
|
|
// Default position: directly below the main view (the pod mounts
|
|
// the glass adjacent to the displays). The main window sits at
|
|
// (0,0) with the -res size; clamp to the work area in case the
|
|
// main view fills the screen. L4PLASMAPOS=x,y overrides.
|
|
//---------------------------------------------------------------
|
|
int x = 0;
|
|
int y = (int) L4Application::GetScreenHeight();
|
|
|
|
const char *pos_string = getenv("L4PLASMAPOS");
|
|
if (pos_string != NULL)
|
|
{
|
|
int px = 0, py = 0;
|
|
if (sscanf(pos_string, "%d,%d", &px, &py) == 2)
|
|
{
|
|
x = px;
|
|
y = py;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
RECT work;
|
|
if (SystemParametersInfoA(SPI_GETWORKAREA, 0, &work, 0))
|
|
{
|
|
if (y + window_h > work.bottom)
|
|
{
|
|
y = work.bottom - window_h;
|
|
}
|
|
if (y < 0)
|
|
{
|
|
y = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
window = CreateWindowExA(
|
|
0,
|
|
plasmaWindowClass,
|
|
"Plasma Display",
|
|
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
|
|
x, y,
|
|
window_w,
|
|
window_h,
|
|
NULL, NULL, instance, NULL
|
|
);
|
|
|
|
if (window != NULL)
|
|
{
|
|
SetWindowLongPtrA((HWND) window, GWLP_USERDATA, (LONG_PTR) this);
|
|
SetPropA((HWND) window, "PlasmaBitmapInfo", bitmapInfo);
|
|
SetPropA((HWND) window, "PlasmaPixels", pixelBuffer->Data.MapPointer);
|
|
ShowWindow((HWND) window, SW_SHOWNOACTIVATE);
|
|
activeInstance = this;
|
|
|
|
//
|
|
//-----------------------------------------------------------
|
|
// Join the saved layout. Position only, like the display
|
|
// panes: the glass is 128x32 at L4PLASMASCALE, so its size
|
|
// is a setting rather than something to drag.
|
|
//
|
|
// Loading here rather than leaving it to SVGA16 keeps this
|
|
// independent of which is built first - the glass comes from
|
|
// the gauge renderer, the panes from the video mode, and
|
|
// whichever runs second re-applies a placement the first
|
|
// already has.
|
|
//-----------------------------------------------------------
|
|
//
|
|
RPWindowLayout_Register(window, "Plasma Display", False);
|
|
RPWindowLayout_Load();
|
|
}
|
|
else
|
|
{
|
|
DEBUG_STREAM << "PlasmaScreen: window creation failed\n" << std::flush;
|
|
}
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Called from the gauge renderer's background task, same slot the serial
|
|
// plasma used for streaming. Repaint when any line changed.
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
Logical
|
|
PlasmaScreen::Update(Logical forceall)
|
|
{
|
|
Check(this);
|
|
|
|
Logical dirty = forceall;
|
|
|
|
for (int y = 0; y < plasmaHeight; ++y)
|
|
{
|
|
if (changedLine[y])
|
|
{
|
|
changedLine[y] = 0;
|
|
dirty = True;
|
|
}
|
|
}
|
|
|
|
if (dirty && window != NULL)
|
|
{
|
|
// paint now - queued WM_PAINTs starve behind the game loop's
|
|
// one-message-per-frame pump
|
|
RedrawWindow((HWND) window, NULL, NULL,
|
|
RDW_INVALIDATE | RDW_UPDATENOW);
|
|
}
|
|
|
|
return False; // 'done' - nothing left to stream
|
|
}
|
|
|
|
void
|
|
PlasmaScreen::WaitForUpdate()
|
|
{
|
|
Check(this);
|
|
}
|