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:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
@@ -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
|
||||
@@ -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). */
|
||||
|
||||
Reference in New Issue
Block a user