Files
CydandClaude Opus 4.8 aadda7f9a5 Cockpit camera complete + namedpipe transport + 832x512 native res
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>
2026-07-14 10:08:15 -05:00

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