9.9 KiB
Ring-0 Interrupt Handling -- Notes for Later
KPCOMM.DRV handles UART interrupts in a ring-3, protected-mode handler hooked through DPMI. The stock Windows 3.1 COMM.DRV runs its interrupt code at ring 0 in enhanced mode. This file records what is known about that, when switching would be worth it, and how to do it, so the work can be picked up later without redoing the research.
Decision (2026-10-12): not now. Measure first; the cheap steps below may make it unnecessary. If it is ever needed, a small VxD is a better design than copying the stock driver's hook.
Each statement below is marked verified (checked against a source) or recollection (believed true, not yet checked).
The current path and its cost
In 386 enhanced mode a UART interrupt currently goes:
- Hardware IRQ to the VMM's interrupt table at ring 0, then to VPICD (the virtual interrupt controller).
- VPICD gives the IRQ to the VM that owns it (the system VM, via VCD's virtualization of the COM IRQ) and requests a virtual interrupt there.
- If the system VM is not running, the scheduler has to switch to it. VPICD boosts the VM's priority, but delivery still waits for the switch and for the VM's virtual interrupt flag to be set.
- The DPMI host enters KPCOMM's protected-mode handler (
isrIrqNN) at ring 3 on its locked stack. - Every
in/outon the UART may trap to VCD, if VCD is still trapping the ports for the owning VM -- unknown, see step 2 of the plan. EOI on port 20h is virtualized by VPICD. - IRET back down to ring 0, then resume whatever was running.
Two costs follow from this:
- Latency. The time from the IRQ to our handler includes VM scheduling. It is normally small, but under load (a busy DOS box, DOS file I/O running in V86 mode, long CLI periods elsewhere) it can approach or exceed the FIFO budget. At 115200 baud a byte arrives every 87 us; with RX trigger 1 the 16-byte FIFO leaves about 1.3 ms of slack, with trigger 8 about 0.7 ms.
- CPU. Each interrupt pays several ring transitions. A full-speed 115200 stream can mean about 2,900 interrupts per second at the default RX trigger of 4, and about 11,500 at trigger 1. A guess of 10-30 us per reflected interrupt on a 486 would put trigger 1 at 10-35% of the CPU and trigger 4 at roughly a quarter of that. These are recollection-level estimates, not measurements.
Ring-0 handling would remove the VM scheduling and the ring transitions (and any I/O trapping): the handler runs at hardware-interrupt time whichever VM is active. It does not help when something else keeps interrupts disabled for too long, such as floppy access in enhanced mode; only FIFO depth covers that.
Measure first
-
Measure headroom (debug build).
- Record the most bytes drained in a single
handleRxcall. That is the FIFO fill level when the handler finally ran, which measures latency directly. - Also count interrupts per second and
CE_OVERRUNoccurrences. - Log the three at close with
dbgHex16. - Run a 115200 download while copying files in File Manager, dragging windows, reading a floppy, and running a DOS box in the background (and full-screen).
- Reading it: a peak that stays well under ~12 bytes with no overruns means ring 0 buys nothing you would notice. A peak at 14-16 means overruns are near.
- Record the most bytes drained in a single
-
Check for I/O trapping. Time a few thousand
_inpcalls on the UART's LSR against BIOS ticks. About 1 us each is untrapped ISA I/O; several microseconds or more means VCD traps the port. If it traps, acquiring the port through VCD's API (option C) may be enough on its own. -
Tune the trigger. The default
COMnRxTRIGGER=4keeps about 1 ms of slack at a quarter of trigger 1's interrupt rate.- Overruns: try 1 (about 1.3 ms of slack).
- CPU: try 8 (about 0.7 ms of slack, half the interrupts of 4).
The 16550A's character timeout delivers stragglers within 4 character times at any level. If bytes below the trigger level stall until more data arrives, the UART or emulator is not raising the timeout interrupt; use 1 there and note which hardware it was. (An old commit message claimed this happened at trigger 8. It was never verified.)
Measure on real hardware: emulator UART and interrupt timing is not representative.
Option A -- the stock driver's VCD hook
Verified (KB Q80901, Windows 3.1 application compatibility):
"COMM.DRV provides its own interrupt handling in enhanced mode." "a new function allows COMM.DRV to register its own ring-0 handler"
The same article says COMMDRV30=TRUE in [386Enh] restores the Windows
3.0 behavior. Both COMM.DRV and VCD read COMxBASE and COMxIRQ.
Recollection (to be confirmed from the DDK or disassembly):
- What changed in 3.1. In 3.0, VCD's own ring-0 code serviced the UART for COMM.DRV. In 3.1, COMM.DRV hands VCD the address of its own ISR, and VCD calls it at hardware-interrupt time through ring-0 aliases of COMM.DRV's 16-bit code and data segments. That way all comm logic stays in COMM.DRV.
- The protocol. COMM.DRV reaches VCD through its protected-mode API entry point: INT 2Fh AX=1684h with BX=000Eh (VCD's device ID) returns ES:DI. Acquire, release, and ISR-registration functions are called through that entry point.
- Sources. The Windows 3.1 DDK shipped sample sources for COMM.DRV
(
ibmcom.asm,ibmint.asm, ...) and probably for VCD; the exact protocol is there.
How to recover the protocol:
- From the 3.1 DDK sources, if a copy can be found.
- From the CyberCom disassembly already in hand: look for INT 2Fh with AX=1684h and BX=000Eh, then trace what it calls through the returned entry point. If CyberCom does it, it is the template.
- From the stock COMM.DRV: same search.
Constraints on KPCOMM's interrupt code if it runs at ring 0:
- No Windows API calls.
checkNotifycallsPostMessage; that must become a deferred event (VCD/VMM schedules a callback in the system VM, which then posts), or a flag polled from application-context calls. - No DPMI.
chainIsr(DPMI chaining) andmaskIrqgo away; VPICD owns masking, EOI (VPICD_Phys_EOI), and sharing.serviceIrq's EOI/chain/spurious logic is replaced by whatever VCD expects as a return. - Segments and stack. The code runs with VCD-provided ring-0 aliases.
DS must be the alias passed in, not
seg ports. Far pointers to GlobalAlloc buffers carry ring-3 selectors; ring 0 may load DPL-3 data selectors, but check how VCD expects buffers to be addressed. The stack is VCD's; 16-bit code on a 32-bit stack needs care with SP versus ESP. - Two paths. Standard mode has no VxDs, so the current DPMI path must
remain as a fallback, and both must share
servicePort/handleRx/handleTxwithout duplicating them. - Undocumented. The interface is undocumented for third-party drivers and specific to Windows 3.1 (and possibly differs in WfW 3.11).
The parts of isr.c that are already ring-safe are servicePort,
handleRx, handleTx, handleLsr, handleMsr, updateRxFlow,
releaseRxFlow, updateTxHold, startTx, writeIer, and writeMcr.
They use only port I/O and the port state. raiseEvents is safe;
checkNotify is not.
Option B -- a KPCOMM VxD
The cleaner design: a small 32-bit virtual device that owns the UART at ring 0, as Windows for Workgroups 3.11's SERIAL.386 does for its COMM.DRV (recollection: WfW 3.11 shipped SERIAL.386, which takes over from VCD's comm handling).
- Interrupts.
VPICD_Virtualize_IRQfor the port's IRQ. The VxD's hardware-interrupt callback services the UART and fills ring buffers in locked memory, then callsVPICD_Phys_EOI. - Talking to COMM.DRV. COMM.DRV gets a PM API entry point (INT 2Fh 1684h with the VxD's ID) to open and close ports, and to map the shared buffers and ComDEB-like state into its address space. The ring-3 COMM.DRV keeps the Windows API and DCB logic, and the buffers stay single-producer/single-consumer as now.
- Notifications.
Call_Priority_VM_Eventor a VMM event into the system VM, thenPostMessagefrom there. - Port ownership. Either cooperate with VCD (acquire the port through
VCD so DOS boxes get the contention dialog) or replace VCD entirely with
device=in[386Enh], as TurboCom-style products did (recollection). Replacing VCD means taking on DOS-box virtualization too. - Toolchain. Windows 3.1 DDK (VxD includes,
vmm.inc/vpicd.inc), MASM 5.10b or 6.x, LINK386, and ADDHDR or equivalent. Debug with WDEB386 on a serial terminal. MSVC 1.52 cannot build VxDs. - Cost. The largest of the options. A bug hangs the machine, and standard mode still needs the DPMI path.
Option C -- VCD port acquisition only
If step 2 of the plan shows VCD trapping UART I/O for the system VM, acquiring the port through VCD's PM API may stop the trapping while keeping the ring-3 handler. It is cheaper than A or B and addresses only the per-I/O cost, not scheduling latency. It needs the same VCD API information as option A (recollection-level today).
Summary
| Option | Fixes latency | Fixes CPU cost | Effort | Risk |
|---|---|---|---|---|
| Tune trigger level | No | Yes, up to 4-8x fewer interrupts | Trivial | Less slack per interrupt |
| C: VCD acquisition | No | Partly, only if I/O is trapped | Small-medium | Undocumented API |
| A: VCD ring-0 hook | Yes | Yes | Medium-large | Undocumented; restructured ISR; two paths |
| B: KPCOMM VxD | Yes | Yes | Large | New toolchain; ring-0 debugging; two paths |
References
- KB Q80901, Windows 3.1 application compatibility (COMM.DRV ring-0
handler,
COMMDRV30): https://jeffpar.github.io/kbarchive/kb/080/Q80901/ - KB Q83449, Windows 3.1 serial communications Q&A
- Windows 3.1 DDAG, communication drivers: https://dos-help.soulsphere.org/ddag31qh.hlp/Communication-Driver_Functions.html
README.mdin this directory, for the current architecture