emulator: in-fork plasma display (serial2=plasma) window in the explode layout

Bring the pod's 128x32 plasma readout in-fork, completing the self-contained
glass cockpit -- no vPLASMA process, no pipe.

- serialplasma.{cpp,h}: CSerialPlasma, a serial<n>=plasma backend that parses
  the game's ESC P graphics stream (L4PLASMA streams whole changed rows;
  receive-only, no replies) into a 128x32 1bpp framebuffer. Transcribed from
  vPLASMA's PlasmaProtocol + VPlasmaDevice (graphics path); the factory
  text-mode/fonts aren't used by the game and aren't rendered, but their escape
  operands are still consumed so the parser can't desync. Seam PLASMA_GetFrame()
  for the renderer.
- vpxlog.cpp: draw the framebuffer as an amber-on-black window in the explode
  layout, lazily created once a plasma port exists, parked centered under the
  lower-left MFD (VPX_PLASMA="x,y,w,h" overrides); WS_EX_NOACTIVATE so it can't
  steal DOSBox focus.
- Register SERIAL_TYPE_PLASMA (serialport.h/.cpp, dosbox.cpp -- documented in
  the vpx-device README step 3d, since those stock files live in the git-ignored
  src tree).
- Confs: serial2 in the _rio confs + deploy templates switches from
  namedpipe pipe:vplasma to the in-fork plasma.

Live-validated 2026-07-24. Real pods keep directserial realport:COM2; the
standalone vPLASMA app keeps namedpipe pipe:vplasma.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-24 12:48:07 -05:00
co-authored by Claude Opus 4.8
parent e6eba36713
commit 844c11f792
9 changed files with 313 additions and 5 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ joysticktype=none
[serial]
# in-fork virtual RIO board on COM1 (pad:<n> and inverty are optional args)
serial1=rio
serial2=namedpipe pipe:vplasma
serial2=plasma
[autoexec]
mount c "@@ROOT@@\ALPHA_1"
mount d "@@ROOT@@\net-boot"
+1 -1
View File
@@ -44,7 +44,7 @@ joysticktype=none
[serial]
# in-fork virtual RIO board on COM1 (pad:<n> and inverty are optional args)
serial1=rio
serial2=namedpipe pipe:vplasma
serial2=plasma
[autoexec]
mount c "@@ROOT@@\ALPHA_1"
mount d "@@ROOT@@\net-boot"
+1 -1
View File
@@ -40,7 +40,7 @@ joysticktype=none
[serial]
# in-fork virtual RIO board on COM1 (pad:<n> and inverty are optional args)
serial1=rio
serial2=namedpipe pipe:vplasma
serial2=plasma
[autoexec]
mount c "C:\VWE\TeslaRel410\ALPHA_1"
mount d "C:\VWE\TeslaRel410\emulator\net-boot"
+1 -1
View File
@@ -38,7 +38,7 @@ joysticktype=none
[serial]
# in-fork virtual RIO board on COM1 (pad:<n> and inverty are optional args)
serial1=rio
serial2=namedpipe pipe:vplasma
serial2=plasma
[autoexec]
mount c "C:\VWE\TeslaRel410\ALPHA_1"
mount d "C:\VWE\TeslaRel410\emulator\net-boot"
@@ -42,7 +42,7 @@ joysticktype=none
# in-fork virtual RIO board on COM1 (pad:<n> / inverty / bindings:<path>
# optional); plasma display stays on the vPLASMA pipe (optional).
serial1=rio
serial2=namedpipe pipe:vplasma
serial2=plasma
[autoexec]
mount c "C:\VWE\TeslaRel410\ALPHA_1"
mount d "C:\VWE\TeslaRel410\emulator\net-boot"
+29
View File
@@ -18,6 +18,19 @@ version control because the DOSBox-X source tree itself
via serialrio's queued `RIO_HostButton`); head windows are `WS_EX_NOACTIVATE`
so a click never pulls foreground off DOSBox (which would demote the emu
thread and stop the SDL pad poll). No-op when no `serial=rio` port is present.
- **`serialplasma.cpp` / `serialplasma.h`** — `serial<n>=plasma` backend
(2026-07-24): the pod's 128×32 dot-matrix plasma readout on COM2, emulated
in-fork like `serial=rio` (no vPLASMA process, no pipe). The game
(`L4PLASMA.CPP`) renders into its own 1bpp buffer and streams changed rows
with a single `ESC P` graphics command — receive-only, no replies — so the
device is just that parser filling a 128×32 framebuffer (transcribed from
vPLASMA `PlasmaProtocol` + `VPlasmaDevice`, graphics path; the factory
text-mode/fonts aren't used by the game and aren't rendered, but their escape
operands are still consumed so the parser can't desync). `vpxlog` draws it as
an amber-on-black window in the **explode** layout (seam `PLASMA_GetFrame`),
lazily created once the port exists, parked centered under the lower-left MFD
(`VPX_PLASMA="x,y,w,h"` overrides). Real pods keep `directserial
realport:COM2`; the vPLASMA app keeps `namedpipe pipe:vplasma`.
- **`vweawe.cpp`** — the dual-AWE32 sound device: two vendored EMU8000
cores at 0x620/0x640 (+0x400/+0x800 triplets), rear-card DSP/mixer stub
at 0x240, autonomous render thread with direct winmm output. Needs the
@@ -142,6 +155,22 @@ Tested against DOSBox-X `v2026.06.02`, MSYS2 mingw64.
`RIO_HostKeyEvent((int)event.key.keysym.scancode, event.type ==
SDL_KEYDOWN, event.key.repeat != 0, event.key.keysym.mod)` and `break;`
when it returns true (consumed keys must never reach the mapper).
3d. The in-fork plasma serial backend (same seam pattern as RIO):
```
cp emulator/vpx-device/serialplasma.cpp emulator/src/src/hardware/serialport/
cp emulator/vpx-device/serialplasma.h emulator/src/src/hardware/serialport/
```
plus the four stock edits (copy the `rio` pattern exactly):
- `src/src/hardware/serialport/Makefile.am`: append
`serialplasma.cpp serialplasma.h` to `libserial_a_SOURCES`.
- `src/include/serialport.h`: add `SERIAL_TYPE_PLASMA` right after
`SERIAL_TYPE_RIO` (unguarded).
- `src/src/hardware/serialport/serialport.cpp`: `#include "serialplasma.h"`;
add the `type=="plasma"` case to BOTH dispatch switches and `"plasma"` to
`serialTypes[]`.
- `src/src/dosbox.cpp`: add `"plasma"` to the `serials[]` allowed-values list.
(The plasma WINDOW is drawn by `vpxlog.cpp` via the `PLASMA_GetFrame` extern
— already in this dir's `vpxlog.cpp`, so it lands with step 1.)
4. Build:
```
cd emulator/src
+159
View File
@@ -0,0 +1,159 @@
/*
* VWE fork: in-fork cockpit plasma display (serial<n>=plasma). See
* serialplasma.h. Graphics-only (ESC P) transcription of vPLASMA's device;
* receive-only, single emulator thread except the framebuffer snapshot which
* the VPX render thread reads under a lock.
*/
#include "dosbox.h"
#include "logging.h"
#include "pic.h"
#include "serialport.h"
#include "serialplasma.h"
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#define PLASMA_ESC 0x1B
static CSerialPlasma* plasma_instance = nullptr;
bool PLASMA_GetFrame(uint8_t out[512]) {
if (!plasma_instance) return false;
plasma_instance->getFrame(out);
return true;
}
CSerialPlasma::CSerialPlasma(Bitu id, CommandLine* cmd) : CSerial(id, cmd) {
(void)cmd;
InstallationSuccessful = false;
logv = (getenv("VWE_PLASMA_LOG") != nullptr);
LOG_MSG("Serial%d: in-fork plasma display (128x32, ESC P graphics)", (int)COMNUMBER);
CSerial::Init_Registers();
// the display is always present -- assert the modem-in lines the game reads
setRI(false);
setCD(true);
setDSR(true);
setCTS(true);
if (plasma_instance == nullptr) plasma_instance = this;
else LOG_MSG("Serial%d: another plasma port already owns the display window", (int)COMNUMBER);
InstallationSuccessful = true;
}
CSerialPlasma::~CSerialPlasma() {
if (plasma_instance == this) plasma_instance = nullptr;
}
void CSerialPlasma::logf(const char* fmt, ...) {
if (!logv) return;
va_list ap; va_start(ap, fmt);
fprintf(stderr, "[plasma] ");
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
va_end(ap);
}
void CSerialPlasma::getFrame(uint8_t out[512]) {
std::lock_guard<std::mutex> lk(fbMx);
memcpy(out, fb, 512);
}
// ---- ESC command parser (VPlasmaDevice.Step, graphics path) ------------------
void CSerialPlasma::feed(uint8_t b) {
switch (st) {
case P_TEXT:
// The game is graphics-only; a printable/control byte in text mode is
// not something it sends, so swallow it (text mode isn't rendered).
if (b == PLASMA_ESC) st = P_ESC;
return;
case P_ESC:
st = P_TEXT;
switch (b) {
case '@': // clear screen
{ std::lock_guard<std::mutex> lk(fbMx); memset(fb, 0, 512); }
logf("clear (ESC @)");
break;
case 'L': // home cursor -- no-op for graphics
break;
case 'G': case 'K': case 'H': // cursor mode / font / attributes
case 'Q': case 'R': // set row / column
case 'I': case 'i': // draw / display page
st = P_OPERAND; // consume 1 operand, unused (text mode)
break;
case 'P': // graphics write
hdrFill = 0;
st = P_GHDR;
break;
default:
logf("unknown ESC 0x%02X ('%c')", b, (b >= 0x20 && b < 0x7F) ? (char)b : '?');
break;
}
return;
case P_OPERAND:
st = P_TEXT; // operand read + discarded
return;
case P_GHDR:
hdr[hdrFill++] = b;
if (hdrFill == 5) { // screen, y, xbyte, w, h
int w = hdr[3], h = hdr[4];
dataLen = w * h;
dataIdx = 0;
logf("ESC P y=%u xbyte=%u %ux%u", hdr[1], hdr[2], (unsigned)w, (unsigned)h);
st = dataLen > 0 ? P_GDATA : P_TEXT;
}
return;
case P_GDATA:
{
int w = hdr[3];
int rowOfBlock = w > 0 ? dataIdx / w : 0;
int byteOfRow = w > 0 ? dataIdx % w : 0;
int y = hdr[1] + rowOfBlock; // 0..31
int xByte = hdr[2] + byteOfRow; // 0..15
// MSB = leftmost pixel; the game packs whole rows (xbyte=0, w=16).
if (y >= 0 && y < 32 && xByte >= 0 && xByte < 16) {
std::lock_guard<std::mutex> lk(fbMx);
fb[y * 16 + xByte] = b;
}
if (byteOfRow == w - 1 && y >= 0 && y < 32) graphicsRows++;
if (++dataIdx >= dataLen) {
st = P_TEXT;
if ((graphicsRows % 200) == 0) logf("rows drawn: %ld", graphicsRows);
}
}
return;
}
}
// ---- UART plumbing (receive-only; drain the game's TX like the pipe) ---------
void CSerialPlasma::transmitByte(uint8_t val, bool first) {
if (first) setEvent(SERIAL_THR_EVENT, bytetime / 10);
else setEvent(SERIAL_TX_EVENT, bytetime);
feed(val);
}
void CSerialPlasma::handleUpperEvent(uint16_t type) {
if (type == SERIAL_THR_EVENT) {
ByteTransmitting();
setEvent(SERIAL_TX_EVENT, bytetime * 1.1f);
} else if (type == SERIAL_TX_EVENT) {
ByteTransmitted();
}
}
void CSerialPlasma::updatePortConfig(uint16_t divider, uint8_t lcr) { (void)divider; (void)lcr; }
void CSerialPlasma::updateMSR() {}
void CSerialPlasma::setBreak(bool value) { (void)value; }
void CSerialPlasma::setRTSDTR(bool rts, bool dtr) { (void)rts; (void)dtr; }
void CSerialPlasma::setRTS(bool val) { (void)val; }
void CSerialPlasma::setDTR(bool val) { (void)val; }
+69
View File
@@ -0,0 +1,69 @@
/*
* VWE fork: in-fork cockpit plasma display (serial<n>=plasma).
*
* The pod's 128x32 dot-matrix plasma readout on COM2. Like serial=rio, it's
* emulated natively inside DOSBox-X and rendered as a window (by vpxlog, in the
* explode layout) -- no external vPLASMA process, no pipe.
*
* The GAME (L4PLASMA.CPP) renders everything itself into a local 1bpp buffer
* and streams changed rows with a single ESC P graphics command; it is a
* receive-only display (no replies). This device implements exactly that path:
* parse the ESC command stream into a 128x32 1bpp framebuffer. The factory
* test tool's text mode (fonts/attributes/cursor) is NOT used by the game and
* is not rendered here -- its escape operands are still consumed so the parser
* never desyncs. Transcribed from the vPLASMA app (C:\VWE\vrio VPlasma.Core:
* PlasmaProtocol + VPlasmaDevice), graphics path only.
*
* Conf: serial2=plasma (real pods keep directserial realport:COM2;
* the vPLASMA app keeps namedpipe pipe:vplasma)
*/
#ifndef DOSBOX_SERIALPLASMA_H
#define DOSBOX_SERIALPLASMA_H
#include "dosbox.h"
#include "serialport.h"
#include <mutex>
// vpxlog reads this each frame to draw the plasma window: 128x32 1bpp packed,
// 16 bytes/row, MSB = leftmost pixel (512 bytes). Returns false when no
// serial<n>=plasma port is installed, so no plasma window is shown.
bool PLASMA_GetFrame(uint8_t out[512]);
class CSerialPlasma : public CSerial {
public:
CSerialPlasma(Bitu id, CommandLine* cmd);
virtual ~CSerialPlasma();
void updatePortConfig(uint16_t divider, uint8_t lcr);
void updateMSR();
void transmitByte(uint8_t val, bool first);
void setBreak(bool value);
void setRTSDTR(bool rts, bool dtr);
void setRTS(bool val);
void setDTR(bool val);
void handleUpperEvent(uint16_t type);
void getFrame(uint8_t out[512]); // render-thread snapshot
private:
void feed(uint8_t b); // parse one wire byte (emu thread)
// parser state machine (VPlasmaDevice.Step)
enum { P_TEXT = 0, P_ESC, P_OPERAND, P_GHDR, P_GDATA };
int st = P_TEXT;
uint8_t hdr[5] = {}; // ESC P header: screen, y, xbyte, w, h
int hdrFill = 0;
int dataIdx = 0, dataLen = 0;
// 128x32 1bpp framebuffer, 16 bytes/row, MSB = leftmost pixel
std::mutex fbMx;
uint8_t fb[512] = {};
long graphicsRows = 0;
bool logv = false;
void logf(const char* fmt, ...);
};
#endif // DOSBOX_SERIALPLASMA_H
+51
View File
@@ -1728,6 +1728,30 @@ static void pal_draw(HDC dc, int g, int cw, int ch, bool dump) {
SwapBuffers(dc);
}
/* ---- in-fork plasma display (serial=plasma) --------------------------------
* Render serialplasma's 128x32 1bpp framebuffer (16 bytes/row, MSB=leftmost)
* as an amber-on-black window in the explode layout. Seam = PLASMA_GetFrame
* (returns false when no serial=plasma port -> no plasma window). */
extern bool PLASMA_GetFrame(uint8_t out[512]);
static void draw_plasma(HDC dc, int cw, int ch, const uint8_t *fb) {
static unsigned char img[128 * 32 * 3];
for (int y = 0; y < 32; y++)
for (int x = 0; x < 128; x++) {
int lit = (fb[y * 16 + (x >> 3)] >> (7 - (x & 7))) & 1;
unsigned char *d = &img[(y * 128 + x) * 3];
if (lit) { d[0] = 255; d[1] = 150; d[2] = 30; } /* amber plasma glow */
else { d[0] = 8; d[1] = 4; d[2] = 0; } /* near-black glass */
}
glViewport(0, 0, cw, ch);
glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_DEPTH_TEST); glDisable(GL_TEXTURE_2D); glDisable(GL_LIGHTING);
glPixelZoom((float)cw / 128.0f, -(float)ch / 32.0f); /* scale + flip y */
glRasterPos2f(-1.0f, 1.0f);
glDrawPixels(128, 32, GL_RGB, GL_UNSIGNED_BYTE, img);
SwapBuffers(dc);
}
/* ---- click-to-press: map a click in a head window to a RIO button ----------
* Mirrors the bezel geometry (draw_mfd_bezel / draw_radar_bezel): returns the
* RIO address under (mx,my) in client coords, or -1 for a non-button click.
@@ -1913,6 +1937,7 @@ static DWORD WINAPI rt_main(LPVOID) {
static const int ex_y[10] = { 560, 0,0,0,0, 560, 560, 20, 20, 20 };
HWND pwnd[10]; HDC pdc[10]; HGLRC pgl[10];
bool phave[10];
HWND pl_wnd = NULL; HDC pl_dc = NULL; HGLRC pl_gl = NULL; bool pl_have = false; /* plasma window */
int slot = 0; /* debug-grid position counter */
for (int g = 0; g < NWIN; g++) {
phave[g] = false;
@@ -1979,6 +2004,32 @@ static DWORD WINAPI rt_main(LPVOID) {
RECT cr; GetClientRect(pwnd[g], &cr);
pal_draw(pdc[g], g, cr.right, cr.bottom, dump);
}
/* in-fork plasma window (explode only): appears once a serial=plasma
* port exists; lazily created below the layout, then rendered live. */
if (explode) {
uint8_t pfb[512];
if (PLASMA_GetFrame(pfb)) {
if (!pl_have) {
int pw = 512, ph = 128, px, py;
if (phave[5]) { /* park centered just under the lower-left MFD (g5) */
RECT r; GetWindowRect(pwnd[5], &r);
px = r.left + ((r.right - r.left) - pw) / 2;
py = r.bottom + 6;
} else { px = 84; py = 1070; } /* explode-layout fallback */
env_rect("VPX_PLASMA", &px, &py, &pw, &ph);
pl_have = make_gl_window("Plasma display (128x32)", pw, ph, px, py,
false, &pl_wnd, &pl_dc, &pl_gl);
if (pl_have) /* don't steal foreground from DOSBox */
SetWindowLongPtrA(pl_wnd, GWL_EXSTYLE,
GetWindowLongPtrA(pl_wnd, GWL_EXSTYLE) | WS_EX_NOACTIVATE);
}
if (pl_have) {
wglMakeCurrent(pl_dc, pl_gl);
RECT cr; GetClientRect(pl_wnd, &cr);
draw_plasma(pl_dc, cr.right, cr.bottom, pfb);
}
}
}
/* explode mode: once the DOSBox main screen exists, park it
* centered under the Division window (one-shot; user can move it
* afterwards). */