Render bridge (live_bridge.py, vrview_gl.py): - Hat glances render (left/right frame the canopy, hat-down = clean rear). Root bug was a stale _ckpt['fix'] key -> KeyError every glance frame -> render aborted (screen froze on hold, snapped back on release). The glance itself is the authentic eye-DCS action-0x1f reflush fp_cam already applies. - Torso twist turret-true (root-axis yaw, zero parallax); lasers follow torso. - Rear glance drops the canopy shell for a clean view (original-hardware behavior); mission-fade shroud 9fd hidden. - Wireframe debug mode (VRVIEW_WIREFRAME / 'w' key), scene-pass scoped. - Renderer output = 832x512, the dPL3 board's native framebuffer res. DOSBox-X fork: namedpipe serial backend (serialnamedpipe.cpp/.h) for vRIO/vPLASMA, replacing com0com; overlapped non-blocking I/O; typed frames (0x00 data / 0x01 DTR+RTS). Tracked copies + apply steps in vpx-device. Docs: COCKPIT-CAGE-NOTES (full glance/twist/rear forensics), XP-PORT-PLAN (back-burnered), RIO-NOTES (namedpipe + keypad), pipe/egg conf variants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
380 lines
11 KiB
C++
380 lines
11 KiB
C++
/*
|
|
* VWE fork: serial-over-named-pipe backend. See serialnamedpipe.h for the
|
|
* wire contract. RX pacing is a faithful copy of the (patched) directserial
|
|
* state machine so the RIO's validated rxpollus/rxburst tuning carries over.
|
|
*/
|
|
|
|
#include "dosbox.h"
|
|
|
|
#if defined(WIN32)
|
|
|
|
#include "logging.h"
|
|
#include "pic.h"
|
|
#include "setup.h"
|
|
#include "serialport.h"
|
|
#include "serialnamedpipe.h"
|
|
|
|
bool getBituSubstring(const char* name, Bitu* data, CommandLine* cmd);
|
|
|
|
#define PIPE_RETRY_MS 500.0
|
|
|
|
enum { P_RX_IDLE = 0, P_RX_WAIT, P_RX_BLOCKED, P_RX_FASTWAIT };
|
|
|
|
// frame types (identical both directions)
|
|
enum { FRAME_DATA = 0x00, FRAME_LINES = 0x01 };
|
|
|
|
CSerialNamedPipe::CSerialNamedPipe(Bitu id, CommandLine* cmd) : CSerial(id, cmd) {
|
|
InstallationSuccessful = false;
|
|
|
|
std::string tmp;
|
|
if (!cmd->FindStringBegin("pipe:", tmp, false)) {
|
|
LOG_MSG("Serial%d: namedpipe requires pipe:<name>", (int)COMNUMBER);
|
|
return;
|
|
}
|
|
// pipe:vrio -> \\.\pipe\vrio; a full \\... path passes through
|
|
if (tmp.size() >= 2 && tmp[0] == '\\' && tmp[1] == '\\')
|
|
pipename = tmp;
|
|
else
|
|
pipename = std::string("\\\\.\\pipe\\") + tmp;
|
|
|
|
Bitu rx_poll_us = 0;
|
|
if (getBituSubstring("rxpollus:", &rx_poll_us, cmd)) {
|
|
if (rx_poll_us < 50) rx_poll_us = 50;
|
|
if (rx_poll_us > 1000) rx_poll_us = 1000;
|
|
rx_poll_ms = (float)rx_poll_us / 1000.0f;
|
|
}
|
|
Bitu rx_burst = 0;
|
|
if (getBituSubstring("rxburst:", &rx_burst, cmd)) {
|
|
if (rx_burst < 1) rx_burst = 1;
|
|
if (rx_burst > 64) rx_burst = 64;
|
|
rx_burst_div = (float)rx_burst;
|
|
}
|
|
if (getBituSubstring("rxdelay:", &rx_retry_max, cmd)) {
|
|
if (!(rx_retry_max <= 10000)) rx_retry_max = 0;
|
|
}
|
|
|
|
LOG_MSG("Serial%d: namedpipe client of %s (poll %.2fms burst x%g)",
|
|
(int)COMNUMBER, pipename.c_str(), rx_poll_ms, rx_burst_div);
|
|
|
|
CSerial::Init_Registers();
|
|
// unplugged cable until the peer's 0x01 arrives
|
|
setRI(false);
|
|
setCD(false);
|
|
setDSR(false);
|
|
setCTS(false);
|
|
|
|
InstallationSuccessful = true;
|
|
rx_state = P_RX_IDLE;
|
|
tryConnect(); // eager first attempt
|
|
setEvent(SERIAL_POLLING_EVENT, rx_poll_ms); // receive/reconnect tick
|
|
}
|
|
|
|
CSerialNamedPipe::~CSerialNamedPipe() {
|
|
if (pipe != INVALID_HANDLE_VALUE) {
|
|
CancelIo(pipe);
|
|
CloseHandle(pipe);
|
|
}
|
|
// events are cleared by the framework on port destruction
|
|
}
|
|
|
|
void CSerialNamedPipe::tryConnect() {
|
|
next_retry_ms = PIC_FullIndex() + PIPE_RETRY_MS;
|
|
HANDLE h = CreateFileA(pipename.c_str(), GENERIC_READ | GENERIC_WRITE,
|
|
0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
|
|
if (h == INVALID_HANDLE_VALUE) return; // no server yet: cable stays unplugged
|
|
DWORD mode = PIPE_READMODE_BYTE;
|
|
SetNamedPipeHandleState(h, &mode, NULL, NULL);
|
|
pipe = h;
|
|
inbuf.clear();
|
|
txq.clear();
|
|
tx_inflight = false;
|
|
LOG_MSG("Serial%d: namedpipe connected to %s", (int)COMNUMBER, pipename.c_str());
|
|
sendLineState(); // contract: one 0x01 on connect
|
|
}
|
|
|
|
void CSerialNamedPipe::dropConnection(const char* why) {
|
|
if (pipe != INVALID_HANDLE_VALUE) {
|
|
CancelIo(pipe);
|
|
CloseHandle(pipe);
|
|
pipe = INVALID_HANDLE_VALUE;
|
|
}
|
|
tx_inflight = false;
|
|
txq.clear();
|
|
inbuf.clear(); // partial frame is meaningless now
|
|
// disconnect = all lines low (rxq keeps already-delivered wire bytes)
|
|
setCTS(false);
|
|
setDSR(false);
|
|
setCD(false);
|
|
next_retry_ms = PIC_FullIndex() + PIPE_RETRY_MS;
|
|
LOG_MSG("Serial%d: namedpipe disconnected (%s)", (int)COMNUMBER, why);
|
|
}
|
|
|
|
// TX is queue + single in-flight overlapped write, reaped from the poll
|
|
// tick. The emu thread NEVER waits on the pipe: a peer that isn't reading
|
|
// (e.g. both sides sending their connect hello before either reads --
|
|
// first live vRIO run, 2026-07-13) wedges the queue, not the emulator;
|
|
// after TX_WEDGE_MS of no progress the connection drops (unplugged cable).
|
|
#define TXQ_MAX 8192
|
|
#define TX_WEDGE_MS 2000.0
|
|
|
|
void CSerialNamedPipe::queueSend(const uint8_t* data, DWORD len) {
|
|
if (pipe == INVALID_HANDLE_VALUE) return; // unplugged: discard
|
|
if (txq.size() + len > TXQ_MAX) {
|
|
dropConnection("tx queue overflow (peer not reading)");
|
|
return;
|
|
}
|
|
txq.insert(txq.end(), data, data + len);
|
|
kickTx();
|
|
}
|
|
|
|
void CSerialNamedPipe::kickTx() {
|
|
if (pipe == INVALID_HANDLE_VALUE || tx_inflight || txq.empty()) return;
|
|
tx_pend.swap(txq);
|
|
txq.clear();
|
|
ZeroMemory(&tx_ov, sizeof(tx_ov));
|
|
DWORD done = 0;
|
|
if (WriteFile(pipe, tx_pend.data(), (DWORD)tx_pend.size(), &done, &tx_ov)) {
|
|
return; // completed synchronously
|
|
}
|
|
if (GetLastError() == ERROR_IO_PENDING) {
|
|
tx_inflight = true;
|
|
tx_wedge_ms = PIC_FullIndex() + TX_WEDGE_MS;
|
|
return;
|
|
}
|
|
dropConnection("write failed");
|
|
}
|
|
|
|
void CSerialNamedPipe::pollTx() {
|
|
if (pipe == INVALID_HANDLE_VALUE || !tx_inflight) return;
|
|
DWORD done = 0;
|
|
if (GetOverlappedResult(pipe, &tx_ov, &done, FALSE)) {
|
|
tx_inflight = false;
|
|
kickTx();
|
|
return;
|
|
}
|
|
DWORD err = GetLastError();
|
|
if (err == ERROR_IO_INCOMPLETE) {
|
|
if (PIC_FullIndex() >= tx_wedge_ms)
|
|
dropConnection("write wedged (peer not reading)");
|
|
return;
|
|
}
|
|
tx_inflight = false;
|
|
dropConnection("write failed");
|
|
}
|
|
|
|
void CSerialNamedPipe::sendLineState() {
|
|
uint8_t f[2] = { FRAME_LINES,
|
|
(uint8_t)((cur_dtr ? 1 : 0) | (cur_rts ? 2 : 0)) };
|
|
queueSend(f, 2);
|
|
}
|
|
|
|
void CSerialNamedPipe::pumpPipe() {
|
|
if (pipe == INVALID_HANDLE_VALUE) return;
|
|
for (;;) {
|
|
DWORD avail = 0;
|
|
if (!PeekNamedPipe(pipe, NULL, 0, NULL, &avail, NULL)) {
|
|
dropConnection("peer closed");
|
|
return;
|
|
}
|
|
if (avail == 0) break;
|
|
uint8_t buf[512];
|
|
DWORD want = avail < sizeof(buf) ? avail : (DWORD)sizeof(buf);
|
|
DWORD got = 0;
|
|
ZeroMemory(&rx_ov, sizeof(rx_ov));
|
|
// overlapped handle needs an OVERLAPPED; Peek guaranteed the bytes
|
|
// are buffered, so the wait below completes immediately.
|
|
if (!ReadFile(pipe, buf, want, &got, &rx_ov)) {
|
|
if (GetLastError() != ERROR_IO_PENDING ||
|
|
!GetOverlappedResult(pipe, &rx_ov, &got, TRUE)) {
|
|
dropConnection("read failed");
|
|
return;
|
|
}
|
|
}
|
|
if (got == 0) {
|
|
dropConnection("read failed");
|
|
return;
|
|
}
|
|
inbuf.insert(inbuf.end(), buf, buf + got);
|
|
}
|
|
// slice complete frames
|
|
size_t pos = 0;
|
|
while (pos < inbuf.size()) {
|
|
uint8_t type = inbuf[pos];
|
|
if (type == FRAME_DATA) {
|
|
if (pos + 2 > inbuf.size()) break; // need len
|
|
uint8_t len = inbuf[pos + 1];
|
|
if (len == 0) { // contract: len >= 1
|
|
dropConnection("zero-length data frame");
|
|
return;
|
|
}
|
|
if (pos + 2 + len > inbuf.size()) break; // incomplete
|
|
for (uint8_t i = 0; i < len; i++)
|
|
rxq.push_back(inbuf[pos + 2 + i]);
|
|
pos += 2 + (size_t)len;
|
|
} else if (type == FRAME_LINES) {
|
|
if (pos + 2 > inbuf.size()) break;
|
|
uint8_t lines = inbuf[pos + 1];
|
|
// null-modem cross: their DTR -> our DSR+CD, their RTS -> our CTS
|
|
setDSR((lines & 1) != 0);
|
|
setCD((lines & 1) != 0);
|
|
setCTS((lines & 2) != 0);
|
|
pos += 2;
|
|
} else {
|
|
LOG_MSG("Serial%d: namedpipe unknown frame type 0x%02x -- protocol bug",
|
|
(int)COMNUMBER, type);
|
|
dropConnection("unknown frame type");
|
|
return;
|
|
}
|
|
}
|
|
inbuf.erase(inbuf.begin(), inbuf.begin() + pos);
|
|
}
|
|
|
|
bool CSerialNamedPipe::doReceive() {
|
|
if (rxq.empty()) pumpPipe();
|
|
if (rxq.empty()) return false;
|
|
uint8_t b = rxq.front();
|
|
rxq.pop_front();
|
|
receiveByteEx(b, 0);
|
|
return true;
|
|
}
|
|
|
|
void CSerialNamedPipe::handleUpperEvent(uint16_t type) {
|
|
switch (type) {
|
|
case SERIAL_POLLING_EVENT: {
|
|
setEvent(SERIAL_POLLING_EVENT, rx_poll_ms);
|
|
if (pipe == INVALID_HANDLE_VALUE) {
|
|
if (PIC_FullIndex() >= next_retry_ms) tryConnect();
|
|
} else {
|
|
pollTx(); // reap / restart the overlapped write
|
|
pumpPipe(); // keep line-state frames fresh even when idle
|
|
}
|
|
// directserial's RX state machine (rxpollus/rxburst semantics)
|
|
switch (rx_state) {
|
|
case P_RX_IDLE:
|
|
if (CanReceiveByte()) {
|
|
if (doReceive()) {
|
|
rx_state = P_RX_WAIT;
|
|
setEvent(SERIAL_RX_EVENT, bytetime * 0.9f / rx_burst_div);
|
|
}
|
|
} else {
|
|
rx_state = P_RX_BLOCKED;
|
|
setEvent(SERIAL_RX_EVENT, bytetime * 0.9f / rx_burst_div);
|
|
}
|
|
break;
|
|
case P_RX_BLOCKED:
|
|
if (!CanReceiveByte()) {
|
|
rx_retry++;
|
|
if (rx_retry >= rx_retry_max) {
|
|
rx_retry = 0;
|
|
removeEvent(SERIAL_RX_EVENT);
|
|
if (doReceive()) {
|
|
while (doReceive()); // overrun, like directserial
|
|
rx_state = P_RX_WAIT;
|
|
setEvent(SERIAL_RX_EVENT, bytetime * 0.9f / rx_burst_div);
|
|
} else {
|
|
rx_state = P_RX_IDLE;
|
|
}
|
|
}
|
|
} else {
|
|
removeEvent(SERIAL_RX_EVENT);
|
|
rx_retry = 0;
|
|
if (doReceive()) {
|
|
rx_state = P_RX_FASTWAIT;
|
|
setEvent(SERIAL_RX_EVENT, bytetime * 0.65f / rx_burst_div);
|
|
} else {
|
|
rx_state = P_RX_IDLE;
|
|
}
|
|
}
|
|
break;
|
|
case P_RX_WAIT:
|
|
case P_RX_FASTWAIT:
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
case SERIAL_RX_EVENT: {
|
|
switch (rx_state) {
|
|
case P_RX_IDLE:
|
|
LOG_MSG("internal error in serialnamedpipe");
|
|
break;
|
|
case P_RX_BLOCKED:
|
|
case P_RX_WAIT:
|
|
case P_RX_FASTWAIT:
|
|
if (CanReceiveByte()) {
|
|
rx_retry = 0;
|
|
if (doReceive()) {
|
|
if (rx_state == P_RX_WAIT)
|
|
setEvent(SERIAL_RX_EVENT, bytetime * 0.9f / rx_burst_div);
|
|
else {
|
|
rx_state = P_RX_FASTWAIT;
|
|
setEvent(SERIAL_RX_EVENT, bytetime * 0.65f / rx_burst_div);
|
|
}
|
|
} else {
|
|
rx_state = P_RX_IDLE;
|
|
}
|
|
} else {
|
|
setEvent(SERIAL_RX_EVENT, bytetime * 0.65f / rx_burst_div);
|
|
rx_state = P_RX_BLOCKED;
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
case SERIAL_TX_EVENT: {
|
|
if (rx_state == P_RX_IDLE && CanReceiveByte()) {
|
|
if (doReceive()) {
|
|
rx_state = P_RX_WAIT;
|
|
setEvent(SERIAL_RX_EVENT, bytetime * 0.9f / rx_burst_div);
|
|
}
|
|
}
|
|
ByteTransmitted();
|
|
break;
|
|
}
|
|
case SERIAL_THR_EVENT: {
|
|
ByteTransmitting();
|
|
setEvent(SERIAL_TX_EVENT, bytetime * 1.1f);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CSerialNamedPipe::updatePortConfig(uint16_t divider, uint8_t lcr) {
|
|
(void)divider; (void)lcr; // a pipe has no baud rate
|
|
}
|
|
|
|
void CSerialNamedPipe::updateMSR() {
|
|
// modem-in lines are event-driven (0x01 frames); nothing to poll
|
|
}
|
|
|
|
void CSerialNamedPipe::transmitByte(uint8_t val, bool first) {
|
|
if (first) setEvent(SERIAL_THR_EVENT, bytetime / 10);
|
|
else setEvent(SERIAL_TX_EVENT, bytetime);
|
|
uint8_t f[3] = { FRAME_DATA, 1, val };
|
|
queueSend(f, 3); // unconnected pipe swallows it (unplugged cable)
|
|
}
|
|
|
|
void CSerialNamedPipe::setBreak(bool value) {
|
|
(void)value; // not in the contract; RIO/plasma don't use break
|
|
}
|
|
|
|
void CSerialNamedPipe::setRTSDTR(bool rts, bool dtr) {
|
|
if (rts == cur_rts && dtr == cur_dtr) return;
|
|
cur_rts = rts;
|
|
cur_dtr = dtr;
|
|
sendLineState();
|
|
}
|
|
|
|
void CSerialNamedPipe::setRTS(bool val) {
|
|
if (val == cur_rts) return;
|
|
cur_rts = val;
|
|
sendLineState();
|
|
}
|
|
|
|
void CSerialNamedPipe::setDTR(bool val) {
|
|
if (val == cur_dtr) return;
|
|
cur_dtr = val;
|
|
sendLineState();
|
|
}
|
|
|
|
#endif // WIN32
|