VPX_PF_WATCH (fork, cpu/paging.cpp): on a guest page fault at the watched linear address, dump guest registers, the last 64 serial RX deliveries with guest cs:eip at each, the code bytes at the faulting EIP and the stack top. serialnamedpipe's doReceive feeds the ring. Armed in podrun.sh and launch_pod.ps1. The first catch decoded the residual fault completely: CS:EIP 00FF:000066D4 in the DPMI host, EBX = F000CA60 -- an IVT entry read as a dword, segment F000 offset CA60, a BIOS default interrupt handler -- and the faulting access is [EBX+0x3004], whose 0x80000000 segment-base wrap gives exactly cr2 7000FA64. The host probes a word 0x3004 bytes past a real-mode vector value treated as a flat pointer: harmless for its own low-memory handlers, a fault for BIOS F000:xxxx defaults. The serial ring shows a steady 1-byte/1-3ms vRIO stream with nothing special at the fault -- the stream determines which vectors get walked, not the crash itself. The 0x3004 appears nowhere in DPMI32VM.OVL or 32RTM.EXE as an immediate, so the probe now also dumps code bytes at EIP; faulthunt.sh loops runs until the next catch. Shipped baseline streak: 4/4 clean -- consistent with exposure, not yet discriminating. The inside view now loads the COCKPIT skeleton: the fleet-wide X-variant naming convention (MAD->MAX etc., all 64 skeletons present) selects the same 25-joint chain with a single object -- max_cop.bgf, the MAX_COP canopy shell with the PUNCH-texel windows from the capture forensics. The donor names the same mechanism from the decomp side (inside = SkeletonType_A with '_cop' selection). Fallback to the body skeleton when no X file exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
386 lines
11 KiB
C++
386 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);
|
|
}
|
|
|
|
/* VPX page-fault watch (cpu/paging.cpp): records each delivered RX byte with
|
|
* the guest cs:eip at delivery, so a watched fault can say how close it was
|
|
* to a serial delivery and where the guest was interrupted. */
|
|
extern void VPX_SerialTraceByte(double now, uint8_t b);
|
|
|
|
bool CSerialNamedPipe::doReceive() {
|
|
if (rxq.empty()) pumpPipe();
|
|
if (rxq.empty()) return false;
|
|
uint8_t b = rxq.front();
|
|
rxq.pop_front();
|
|
VPX_SerialTraceByte(PIC_FullIndex(), b);
|
|
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
|