From 716e10e3fa86878015d8ea7822a4e448e5dbb9b1 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Mon, 10 Aug 2026 13:30:41 -0500 Subject: [PATCH] serial write hardening: bound the overlapped-write wait (500ms + CancelIo + 3s sick window) -- a dropped USB-serial link parked the game forever in GetOverlappedResult (ALPHA-MR minidump: SendPacket <- RIO ctor); txTO= counter joins the [rio] health line Co-Authored-By: Claude Fable 5 --- engine/MUNGA_L4/L4PCSPAK.cpp | 53 +++++++++++++++++++++++++++++++++--- engine/MUNGA_L4/L4RIO.cpp | 3 ++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/engine/MUNGA_L4/L4PCSPAK.cpp b/engine/MUNGA_L4/L4PCSPAK.cpp index 144e85d..cab66c4 100644 --- a/engine/MUNGA_L4/L4PCSPAK.cpp +++ b/engine/MUNGA_L4/L4PCSPAK.cpp @@ -410,6 +410,20 @@ int PCSerialPacket::ReceivePacket(BYTE *destPtr) // returns len (zero if none) // Returns: // void //------------------------------------------------------------------------- +// +// Serial-write health (see the bounded wait below): after a write timeout +// the link is SICK for 3 s and writes fast-drop -- one stall, not one per +// poll. BTPcspakTxTimeouts feeds the [rio] telemetry. +// +static unsigned gPcspakTxTimeout = 0; +static DWORD gPcspakTxSickUntil = 0; + +extern "C" unsigned + BTPcspakTxTimeouts() +{ + return gPcspakTxTimeout; +} + void PCSerialPacket::SendPacket(BYTE *srcPtr) //sends packet { //-------------------------------------------------------------- @@ -418,6 +432,11 @@ void PCSerialPacket::SendPacket(BYTE *srcPtr) //sends packet if(!enabled) //if disabled, don't even try return; + if (gPcspakTxSickUntil != 0 && GetTickCount() < gPcspakTxSickUntil) + { + return; // adapter sick: drop, don't stall + } + BYTE cmd = *srcPtr; //get the command byte cmd &= 0x7F; //remove MSB int length = txLengthPtr[cmd]; //get the length in al @@ -459,11 +478,37 @@ void PCSerialPacket::SendPacket(BYTE *srcPtr) //sends packet return; } } - DWORD bytesWritten; - GetOverlappedResult(hComm, &overlapSend, &bytesWritten, TRUE); - if(bytesWritten != length + 1) + // + // BOUNDED completion wait (2026-08-10, ALPHA-MR field minidump). The + // RIO's "COM1" is a USB-serial adapter; when its USB link drops with a + // write in flight the overlapped write NEVER completes, and the original + // GetOverlappedResult(..., TRUE) parked the whole game at 0 CPU forever + // (cdb-proven: NtWaitForSingleObject <- GetOverlappedResult <- + // SendPacket <- RIO ctor). Now: 500 ms bound (9600 baud needs ~21 ms), + // CancelIo on expiry, and a 3 s SICK window that fast-drops writes so a + // dead adapter costs one stall, not one per 50 ms poll -- the game stays + // at frame rate with a degraded RIO and the keyboard fallback alive. + // gPcspakTxTimeout feeds the [rio] health line (txTO=). + // + DWORD bytesWritten = 0; + DWORD wait_result = WaitForSingleObject(overlapSend.hEvent, 500); + if (wait_result == WAIT_OBJECT_0) { - DEBUG_STREAM << "Not all data was written to serial port!\n" << std::flush; + GetOverlappedResult(hComm, &overlapSend, &bytesWritten, FALSE); + gPcspakTxSickUntil = 0; + if (bytesWritten != (DWORD)(length + 1)) + { + DEBUG_STREAM << "Not all data was written to serial port!\n" + << std::flush; + } + } + else + { + // No print here -- serial-path discipline; the counter surfaces in + // the [rio] health line (txTO=) from the glass-tick printer. + CancelIo(hComm); + ++gPcspakTxTimeout; + gPcspakTxSickUntil = GetTickCount() + 3000; } delete buffer; } diff --git a/engine/MUNGA_L4/L4RIO.cpp b/engine/MUNGA_L4/L4RIO.cpp index 71adb88..2b34826 100644 --- a/engine/MUNGA_L4/L4RIO.cpp +++ b/engine/MUNGA_L4/L4RIO.cpp @@ -1267,6 +1267,8 @@ static unsigned gRioRRetryMirror = 0; static unsigned gRioRAbandonMirror = 0; static unsigned gRioRFullMirror = 0; +extern "C" unsigned BTPcspakTxTimeouts(); // L4PCSPAK.cpp: bounded-write stat + // // The printer half -- called from BTGlassPanels_Tick beside the [glassperf] // report (a stream site that prints every second without incident), NEVER @@ -1326,6 +1328,7 @@ extern "C" void << " age=" << (gRioLastReplyTick != 0 ? (now - gRioLastReplyTick) : 0) << "ms stalls=" << gRioStallCount + << " txTO=" << BTPcspakTxTimeouts() << " lineErr=" << gRioLineErrMirror << " overrun=" << gRioOverrunMirror << " abandon=" << gRioAbandonMirror