diff --git a/emulator/deploy/net_loop_rio.conf.tmpl b/emulator/deploy/net_loop_rio.conf.tmpl index 0986d9da..e33442c7 100644 --- a/emulator/deploy/net_loop_rio.conf.tmpl +++ b/emulator/deploy/net_loop_rio.conf.tmpl @@ -43,7 +43,7 @@ joysticktype=none [serial] # in-fork virtual RIO board on COM1 (pad: and inverty are optional args) serial1=rio -serial2=namedpipe pipe:vplasma +serial2=plasma [autoexec] mount c "@@ROOT@@\ALPHA_1" mount d "@@ROOT@@\net-boot" diff --git a/emulator/deploy/net_rp_rio.conf.tmpl b/emulator/deploy/net_rp_rio.conf.tmpl index ada8775d..66d50716 100644 --- a/emulator/deploy/net_rp_rio.conf.tmpl +++ b/emulator/deploy/net_rp_rio.conf.tmpl @@ -44,7 +44,7 @@ joysticktype=none [serial] # in-fork virtual RIO board on COM1 (pad: and inverty are optional args) serial1=rio -serial2=namedpipe pipe:vplasma +serial2=plasma [autoexec] mount c "@@ROOT@@\ALPHA_1" mount d "@@ROOT@@\net-boot" diff --git a/emulator/net_loop_rio.conf b/emulator/net_loop_rio.conf index 71ab4167..83075fde 100644 --- a/emulator/net_loop_rio.conf +++ b/emulator/net_loop_rio.conf @@ -40,7 +40,7 @@ joysticktype=none [serial] # in-fork virtual RIO board on COM1 (pad: 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" diff --git a/emulator/net_rp_rio.conf b/emulator/net_rp_rio.conf index 4f380301..89581ba3 100644 --- a/emulator/net_rp_rio.conf +++ b/emulator/net_rp_rio.conf @@ -38,7 +38,7 @@ joysticktype=none [serial] # in-fork virtual RIO board on COM1 (pad: 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" diff --git a/emulator/render-bridge/gauge_arena_rio_sound.conf b/emulator/render-bridge/gauge_arena_rio_sound.conf index 807847c7..eb58505a 100644 --- a/emulator/render-bridge/gauge_arena_rio_sound.conf +++ b/emulator/render-bridge/gauge_arena_rio_sound.conf @@ -42,7 +42,7 @@ joysticktype=none # in-fork virtual RIO board on COM1 (pad: / inverty / bindings: # 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" diff --git a/emulator/vpx-device/README.md b/emulator/vpx-device/README.md index bd2c50c0..e96b09e1 100644 --- a/emulator/vpx-device/README.md +++ b/emulator/vpx-device/README.md @@ -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=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 diff --git a/emulator/vpx-device/serialplasma.cpp b/emulator/vpx-device/serialplasma.cpp new file mode 100644 index 00000000..995d3e77 --- /dev/null +++ b/emulator/vpx-device/serialplasma.cpp @@ -0,0 +1,159 @@ +/* + * VWE fork: in-fork cockpit plasma display (serial=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 +#include +#include +#include + +#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 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 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 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; } diff --git a/emulator/vpx-device/serialplasma.h b/emulator/vpx-device/serialplasma.h new file mode 100644 index 00000000..393fd997 --- /dev/null +++ b/emulator/vpx-device/serialplasma.h @@ -0,0 +1,69 @@ +/* + * VWE fork: in-fork cockpit plasma display (serial=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 + +// 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=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 diff --git a/emulator/vpx-device/vpxlog.cpp b/emulator/vpx-device/vpxlog.cpp index b325b4bf..504588a7 100644 --- a/emulator/vpx-device/vpxlog.cpp +++ b/emulator/vpx-device/vpxlog.cpp @@ -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). */