WinComm/drv/ring0.md
2026-10-12 16:22:16 -05:00

202 lines
9.9 KiB
Markdown

# 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:
1. Hardware IRQ to the VMM's interrupt table at ring 0, then to VPICD (the
virtual interrupt controller).
2. 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.
3. 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.
4. The DPMI host enters KPCOMM's protected-mode handler (`isrIrqNN`) at
ring 3 on its locked stack.
5. Every `in`/`out` on 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.
6. 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
1. **Measure headroom (debug build).**
- Record the most bytes drained in a single `handleRx` call. That is
the FIFO fill level when the handler finally ran, which measures
latency directly.
- Also count interrupts per second and `CE_OVERRUN` occurrences.
- 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.
2. **Check for I/O trapping.** Time a few thousand `_inp` calls 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.
3. **Tune the trigger.** The default `COMnRxTRIGGER=4` keeps 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.** `checkNotify` calls `PostMessage`; 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) and `maskIrq` go 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`/
`handleTx` without 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_IRQ` for the port's IRQ. The VxD's
hardware-interrupt callback services the UART and fills ring buffers in
locked memory, then calls `VPICD_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_Event` or a VMM event into the
system VM, then `PostMessage` from 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.md` in this directory, for the current architecture