Files
TeslaRel410/emulator/vpx-device/serialplasma.cpp
T
CydandClaude Opus 4.8 844c11f792 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>
2026-07-24 12:48:07 -05:00

160 lines
4.5 KiB
C++

/*
* 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; }