From acf1a6b691a1e960be9ac4714aca3f24b84d112d Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Sun, 1 Mar 2026 18:34:19 -0600 Subject: [PATCH] Remove BeginUpdate/EndUpdate, fix rendering starvation, add variable docs Remove BeginUpdate/EndUpdate batching from TKPAnsi entirely -- Write now renders immediately via FlipToScreen after every ParseData call. Remove FPendingScroll (caused rendering deadlock: EndUpdate refused to call FlipToScreen while FPendingScroll > 0, but only FlipToScreen cleared it). DoScrollUp simplified to set FAllDirty directly. CommEvent drain loop retained (required by edge-triggered CN_RECEIVE) but each chunk renders immediately -- no deferred batching. Edge-triggered notifications verified starvation-free at all levels: ISR, driver, KPCOMM dispatch, terminal rendering, and keyboard output path. Add comprehensive variable comments to all project files: TKPAnsi (44 fields), TKPComm (23 fields), TMainForm (9 fields), PortStateT, and driver globals. Co-Authored-By: Claude Opus 4.6 --- delphi/KPANSI.PAS | 302 +++++++++++++++++++++++++++----------------- delphi/KPCOMM.PAS | 80 ++++++++---- delphi/TESTMAIN.PAS | 44 +++---- drv/commdrv.c | 44 +++++-- drv/commdrv.h | 123 ++++++++++-------- drv/isr.c | 38 ++++-- 6 files changed, 393 insertions(+), 238 deletions(-) diff --git a/delphi/KPANSI.PAS b/delphi/KPANSI.PAS index de16ac0..9400611 100644 --- a/delphi/KPANSI.PAS +++ b/delphi/KPANSI.PAS @@ -45,48 +45,80 @@ type TKPAnsi = class(TCustomControl) private - FScreen: TList; - FScrollback: TList; - FCursorRow: Integer; - FCursorCol: Integer; - FSaveCurRow: Integer; - FSaveCurCol: Integer; - FAttrFG: Integer; - FAttrBG: Integer; - FAttrBold: Boolean; - FAttrBlink: Boolean; - FAttrReverse: Boolean; - FParseState: TParseState; - FParamStr: string; - FMusicStr: string; - FCellWidth: Integer; - FCellHeight: Integer; - FBlinkOn: Boolean; - FTimerActive: Boolean; - FScrollPos: Integer; - FWrapMode: Boolean; - FCols: Integer; - FRows: Integer; - FScrollbackSize: Integer; - FCursorVisible: Boolean; - FLastCursorRow: Integer; - FOnKeyData: TKeyDataEvent; - FPaintFont: HFont; - FStockFont: Boolean; - FBlinkCount: Integer; - FUpdateCount: Integer; - FPendingScroll: Integer; - FDirtyRow: array[0..255] of Boolean; - FAllDirty: Boolean; - FTextBlinkOn: Boolean; - FGlyphBufH: THandle; - FGlyphBuf: Pointer; - FRowBufH: THandle; - FRowBuf: Pointer; - FDibInfo: TDibInfo; - FRowBufSize: Integer; - FNibbleFG: Byte; - FNibbleBG: Byte; + { Terminal buffer state } + FScreen: TList; { Active screen lines (FRows PTermLine ptrs) } + FScrollback: TList; { Scrollback history (up to FScrollbackSize) } + + { Cursor position (0-based row/col within the active screen) } + FCursorRow: Integer; { Current row (0 = top) } + FCursorCol: Integer; { Current column (0 = left) } + FSaveCurRow: Integer; { Saved row for SCP/RCP (ESC[s / ESC[u) } + FSaveCurCol: Integer; { Saved column for SCP/RCP } + + { Current text attributes (set by SGR escape sequences) } + FAttrFG: Integer; { Foreground color index 0-7 } + FAttrBG: Integer; { Background color index 0-7 } + FAttrBold: Boolean; { Bold: maps FG to bright (index + 8) } + FAttrBlink: Boolean; { Blink: cell toggles visibility on timer } + FAttrReverse: Boolean; { Reverse video: swap FG/BG at render time } + + { ANSI escape sequence parser state } + FParseState: TParseState; { Current parser state machine position } + FParamStr: string; { Accumulated CSI parameter digits/semicolons } + FMusicStr: string; { Accumulated ANSI music string (ESC[M..^N) } + + { Font metrics (measured from OEM charset paint font) } + FCellWidth: Integer; { Character cell width in pixels (typically 8) } + FCellHeight: Integer; { Character cell height in pixels (typ 12-16) } + + { Blink/timer state } + FBlinkOn: Boolean; { Cursor blink phase: True=visible } + FTimerActive: Boolean; { True if WM_TIMER is running (SetTimer) } + + { Scrollback view } + FScrollPos: Integer; { Lines scrolled back (0=live, >0=viewing history) } + + { Terminal modes } + FWrapMode: Boolean; { Auto-wrap at right margin (DEC ?7h/l) } + + { Terminal dimensions } + FCols: Integer; { Number of columns (default 80) } + FRows: Integer; { Number of rows (default 25) } + FScrollbackSize: Integer; { Max scrollback lines to retain (default 500) } + + { Cursor visibility (DEC ?25h/l) } + FCursorVisible: Boolean; { True if cursor is shown } + FLastCursorRow: Integer; { Previous cursor row for ghost cleanup } + + { Events } + FOnKeyData: TKeyDataEvent; { Keyboard data callback (keys -> serial) } + + { Paint font } + FPaintFont: HFont; { GDI font handle for OEM_CHARSET rendering } + FStockFont: Boolean; { True if FPaintFont is a stock object (no delete) } + + { Rendering control } + FBlinkCount: Integer; { Timer ticks since last blink toggle } + + { Dirty tracking: per-row flags for incremental rendering } + FDirtyRow: array[0..255] of Boolean; { True = row needs re-render } + FAllDirty: Boolean; { True = all rows need re-render } + FScrollbarDirty: Boolean; { True = scrollbar range/position needs update } + FTextBlinkOn: Boolean; { Text blink phase: True=visible, False=hidden } + + { Font atlas: glyph bitmaps + nibble lookup table (GlobalAlloc) } + FGlyphBufH: THandle; { GlobalAlloc handle for glyph block (8256 bytes) } + FGlyphBuf: Pointer; { Far ptr: offset 0..63 = nibble table, 64+ = glyphs } + + { Row pixel buffer: reusable 8bpp DIB for one terminal row } + FRowBufH: THandle; { GlobalAlloc handle for pixel buffer } + FRowBuf: Pointer; { Far ptr to pixel data (cols*cellW*cellH bytes) } + FDibInfo: TDibInfo; { BITMAPINFO with 16-color ANSI palette } + FRowBufSize: Integer; { Pixel buffer size in bytes } + + { Nibble table color cache: avoids rebuild when colors unchanged } + FNibbleFG: Byte; { FG index currently in nibble table (255=invalid) } + FNibbleBG: Byte; { BG index currently in nibble table (255=invalid) } procedure AllocLine(Line: PTermLine); procedure BuildAtlas; procedure ClearLine(Line: PTermLine); @@ -136,9 +168,7 @@ type public constructor Create(AOwner: TComponent); override; destructor Destroy; override; - procedure BeginUpdate; procedure Clear; - procedure EndUpdate; procedure Reset; procedure Write(const S: string); property CursorCol: Integer read GetCursorCol; @@ -263,12 +293,6 @@ begin end; -procedure TKPAnsi.BeginUpdate; -begin - Inc(FUpdateCount); -end; - - procedure TKPAnsi.BuildAtlas; { Render all 256 CP437 characters into a monochrome bitmap, then extract } { per-glyph pixel masks into the glyph block at offset 64. Each glyph } @@ -466,9 +490,8 @@ begin FPaintFont := 0; FStockFont := False; FBlinkCount := 0; - FUpdateCount := 0; - FPendingScroll := 0; FAllDirty := True; + FScrollbarDirty := False; FTextBlinkOn := True; FRowBufSize := 0; FGlyphBufH := 0; @@ -729,15 +752,13 @@ begin AllocLine(Line); FScreen.Insert(0, Line); { Scroll down is rare; just repaint everything } - FAllDirty := True; - FPendingScroll := 0; + FAllDirty := True; end; procedure TKPAnsi.DoScrollUp; var Line: PTermLine; - I: Integer; begin if FScreen.Count < FRows then Exit; @@ -745,39 +766,19 @@ begin Line := FScreen[0]; FScrollback.Add(Line); FScreen.Delete(0); - TrimScrollback; + { TrimScrollback deferred to ParseData for batching } { Add blank line at bottom } GetMem(Line, SizeOf(TTermLineRec)); AllocLine(Line); FScreen.Add(Line); - UpdateScrollbar; + FScrollbarDirty := True; - Inc(FPendingScroll); - if FPendingScroll >= FRows then - begin - { Scrolled more than one screen; just repaint everything } - FAllDirty := True; - FPendingScroll := 0; - end - else - begin - { Shift dirty flags up to match the scrolled line positions } - for I := 1 to FRows - 1 do - FDirtyRow[I - 1] := FDirtyRow[I]; - FDirtyRow[FRows - 1] := True; - end; + { Without ScrollDC, all rows must be re-rendered after a scroll } + { because the on-screen pixels haven't moved to match FScreen. } + FAllDirty := True; end; -procedure TKPAnsi.EndUpdate; -begin - Dec(FUpdateCount); - if FUpdateCount <= 0 then - begin - FUpdateCount := 0; - FlipToScreen; - end; -end; procedure TKPAnsi.EraseDisplay(Mode: Integer); @@ -1230,10 +1231,8 @@ procedure TKPAnsi.FlipToScreen; { screen via SetDIBitsToDevice immediately after rendering. One GDI call } { per dirty row, zero for the pixel expansion itself. } var - DC: HDC; - Row: Integer; - R: TRect; - ScrollY: Integer; + DC: HDC; + Row: Integer; begin if not HandleAllocated then Exit; @@ -1242,26 +1241,16 @@ begin if FRowBuf = nil then Exit; - { Scrollback view: force full redraw, ignore pending scroll } + { Scrollback view: force full redraw (line mapping changes) } if FScrollPos <> 0 then - begin - FAllDirty := True; - FPendingScroll := 0; - end; + FAllDirty := True; - { Deferred scroll: shift existing screen pixels up } - if (FPendingScroll > 0) and not FAllDirty then + { Deferred scrollbar update (batched from DoScrollUp) } + if FScrollbarDirty then begin - R.Left := 0; - R.Top := 0; - R.Right := FCols * FCellWidth; - R.Bottom := FRows * FCellHeight; - ScrollY := FPendingScroll * FCellHeight; - DC := GetDC(Handle); - ScrollDC(DC, 0, -ScrollY, R, R, 0, nil); - ReleaseDC(Handle, DC); + UpdateScrollbar; + FScrollbarDirty := False; end; - FPendingScroll := 0; { Dirty old cursor row to erase ghost when cursor moved between rows } if FCursorRow <> FLastCursorRow then @@ -1488,8 +1477,7 @@ begin Exit; { Full repaint: render each row into the shared buffer and blast it } - FPendingScroll := 0; - FAllDirty := True; + FAllDirty := True; for Row := 0 to FRows - 1 do begin @@ -1509,28 +1497,92 @@ end; procedure TKPAnsi.ParseData(const S: string); +{ Process incoming data with an inlined fast path for printable characters. } +{ ~80% of BBS data is printable text in normal state. Inlining avoids the } +{ per-character method call to ProcessChar, and caching the Line pointer } +{ eliminates repeated TList lookups for consecutive chars on the same row. } +{ } +{ Does NOT call FlipToScreen -- the caller (Write) calls FlipToScreen } +{ after ParseData returns, ensuring immediate rendering. } var - I: Integer; + I: Integer; + Ch: Char; + Line: PTermLine; + FGIdx: Byte; + BGIdx: Byte; begin + Line := nil; + for I := 1 to Length(S) do - ProcessChar(S[I]); + begin + Ch := S[I]; + + { Fast path: printable character in normal state } + if (FParseState = psNormal) and (Ch >= ' ') then + begin + if FCursorCol >= FCols then + begin + if FWrapMode then + begin + FCursorCol := 0; + Inc(FCursorRow); + if FCursorRow >= FRows then + begin + FCursorRow := FRows - 1; + DoScrollUp; + end; + Line := nil; + end + else + FCursorCol := FCols - 1; + end; + + if Line = nil then + Line := FScreen[FCursorRow]; + + if FAttrBold then + FGIdx := FAttrFG + 8 + else + FGIdx := FAttrFG; + BGIdx := FAttrBG; + + if FAttrReverse then + begin + Line^.Cells[FCursorCol].FG := BGIdx; + Line^.Cells[FCursorCol].BG := FGIdx; + end + else + begin + Line^.Cells[FCursorCol].FG := FGIdx; + Line^.Cells[FCursorCol].BG := BGIdx; + end; + Line^.Cells[FCursorCol].Ch := Ch; + Line^.Cells[FCursorCol].Bold := FAttrBold; + Line^.Cells[FCursorCol].Blink := FAttrBlink; + FDirtyRow[FCursorRow] := True; + Inc(FCursorCol); + end + else + begin + { Slow path: control chars, escape sequences } + Line := nil; + ProcessChar(Ch); + end; + end; + + { Deferred scrollback trim -- batched from DoScrollUp } + TrimScrollback; { Snap to bottom on new data } if FScrollPos <> 0 then begin - FScrollPos := 0; - UpdateScrollbar; - FAllDirty := True; + FScrollPos := 0; + FScrollbarDirty := True; + FAllDirty := True; end; { Reset cursor blink to visible on new data } FBlinkOn := True; - - { Render immediately -- no throttle. Data hits the screen as soon } - { as it arrives. BeginUpdate/EndUpdate suppresses intermediate } - { renders when the caller is batching multiple Write calls. } - if FUpdateCount = 0 then - FlipToScreen; end; @@ -2212,15 +2264,28 @@ end; procedure TKPAnsi.TrimScrollback; +{ Batch-optimized: free excess items, shift remainder down in one pass, } +{ then shrink from the end. O(n) total vs O(k*n) for k front-deletions. } var - Line: PTermLine; + Excess: Integer; + I: Integer; + Line: PTermLine; begin - while FScrollback.Count > FScrollbackSize do + Excess := FScrollback.Count - FScrollbackSize; + if Excess <= 0 then + Exit; + { Free the oldest lines } + for I := 0 to Excess - 1 do begin - Line := FScrollback[0]; + Line := FScrollback[I]; FreeMem(Line, SizeOf(TTermLineRec)); - FScrollback.Delete(0); end; + { Shift remaining items down in one pass } + for I := 0 to FScrollback.Count - Excess - 1 do + FScrollback[I] := FScrollback[I + Excess]; + { Remove excess slots from the end (O(1) per deletion) } + for I := 1 to Excess do + FScrollback.Delete(FScrollback.Count - 1); end; @@ -2318,7 +2383,10 @@ end; procedure TKPAnsi.Write(const S: string); begin if Length(S) > 0 then + begin ParseData(S); + FlipToScreen; + end; end; diff --git a/delphi/KPCOMM.PAS b/delphi/KPCOMM.PAS index 61c74de..224e71e 100644 --- a/delphi/KPCOMM.PAS +++ b/delphi/KPCOMM.PAS @@ -45,29 +45,42 @@ type TKPComm = class(TComponent) private - FCommId: Integer; - FHWndNotify: HWnd; - FCommPort: Integer; - FSettings: string; - FPortOpen: Boolean; - FInBufferSize: Integer; - FOutBufferSize: Integer; - FRThreshold: Integer; - FSThreshold: Integer; - FHandshaking: THandshaking; - FInputLen: Integer; - FInputMode: TInputMode; - FDTREnable: Boolean; - FRTSEnable: Boolean; - FNullDiscard: Boolean; - FEOFEnable: Boolean; - FParityReplace: string; - FBreakState: Boolean; - FCommEvent: Integer; - FCTSState: Boolean; - FDSRState: Boolean; - FCDState: Boolean; - FOnComm: TNotifyEvent; + { Port state } + FCommId: Integer; { Comm port handle from OpenComm (-1 = closed) } + FHWndNotify: HWnd; { Hidden window receiving WM_COMMNOTIFY } + + { Configuration (published properties) } + FCommPort: Integer; { Port number (1-based: 1=COM1, 2=COM2, ...) } + FSettings: string; { Baud/parity/data/stop string (e.g. '9600,N,8,1') } + FPortOpen: Boolean; { True while port is open and operational } + FInBufferSize: Integer; { Receive ring buffer size in bytes } + FOutBufferSize: Integer; { Transmit ring buffer size in bytes } + FRThreshold: Integer; { RX byte count triggering CN_RECEIVE (0=disabled) } + FSThreshold: Integer; { TX free space triggering CN_TRANSMIT (0=disabled) } + FHandshaking: THandshaking; { Flow control mode (none/XonXoff/RtsCts/both) } + FInputLen: Integer; { Max bytes per Input read (0=up to 255) } + FInputMode: TInputMode; { Text or binary read mode } + + { Modem control lines } + FDTREnable: Boolean; { DTR line state (True=asserted) } + FRTSEnable: Boolean; { RTS line state (True=asserted) } + + { DCB options } + FNullDiscard: Boolean; { Strip null bytes from received data } + FEOFEnable: Boolean; { Detect EOF character in stream } + FParityReplace: string; { Replacement char for parity errors ('' = none) } + + { Runtime state } + FBreakState: Boolean; { True while break signal is being sent } + FCommEvent: Integer; { Last event code passed to OnComm (comEv* const) } + + { Modem line shadow state (toggled on transition events from driver) } + FCTSState: Boolean; { CTS line level (toggled on ev_CTS) } + FDSRState: Boolean; { DSR line level (toggled on ev_DSR) } + FCDState: Boolean; { DCD/RLSD line level (toggled on ev_RLSD) } + + { Event handler } + FOnComm: TNotifyEvent; { Fired on comm events; check CommEvent for code } procedure ApplyHandshaking; procedure ApplyOptions; procedure ClosePort; @@ -153,7 +166,7 @@ const NotifyClassName = 'KPCommNotify'; var - NotifyClassRegistered: Boolean; + NotifyClassRegistered: Boolean; { True after RegisterClass for notification window } { ----------------------------------------------------------------------- } @@ -264,12 +277,18 @@ end; procedure TKPComm.ClosePort; +var + Msg: TMsg; begin { Set FPortOpen first to prevent stale WM_COMMNOTIFY processing } FPortOpen := False; if FCommId >= 0 then begin + { Disable notifications BEFORE dropping modem lines so the ISR } + { stops posting WM_COMMNOTIFY while we tear down. } + EnableCommNotification(FCommId, 0, -1, -1); + if FBreakState then begin ClearCommBreak(FCommId); @@ -283,6 +302,12 @@ begin if FHWndNotify <> 0 then begin + { Flush any stale WM_COMMNOTIFY that the ISR posted before we } + { disabled notifications. Without this, DispatchMessage would } + { dereference the freed window structure and lock up. } + while PeekMessage(Msg, FHWndNotify, wm_CommNotify, + wm_CommNotify, pm_Remove) do + { discard }; DestroyWindow(FHWndNotify); FHWndNotify := 0; end; @@ -475,10 +500,13 @@ begin end; SetWindowLong(FHWndNotify, 0, Longint(Self)); - { Enable event mask for modem status, errors, and breaks } + { Enable event mask for modem status, errors, and breaks. } + { ev_RxChar is deliberately excluded: it fires per received byte, } + { flooding WM_COMMNOTIFY with CN_EVENT on every ISR. Data arrival } + { is already handled by CN_RECEIVE via EnableCommNotification. } SetCommEventMask(FCommId, ev_CTS or ev_DSR or ev_RLSD or ev_Ring or - ev_Err or ev_Break or ev_RxChar); + ev_Err or ev_Break); { Enable comm notifications -- -1 disables the notification } if FRThreshold > 0 then diff --git a/delphi/TESTMAIN.PAS b/delphi/TESTMAIN.PAS index c28f335..2f3a5db 100644 --- a/delphi/TESTMAIN.PAS +++ b/delphi/TESTMAIN.PAS @@ -17,15 +17,18 @@ uses type TMainForm = class(TForm) private - FComm: TKPComm; - FAnsi: TKPAnsi; - FLabelPort: TLabel; - FEditPort: TEdit; - FLabelSettings: TLabel; - FEditSettings: TEdit; - FBtnOpen: TButton; - FBtnClose: TButton; - FLabelStatus: TLabel; + { Components (owned by Self, freed automatically) } + FComm: TKPComm; { Serial communications component } + FAnsi: TKPAnsi; { ANSI terminal display } + + { Toolbar controls } + FLabelPort: TLabel; { "Port:" label } + FEditPort: TEdit; { COM port number entry } + FLabelSettings: TLabel; { "Settings:" label } + FEditSettings: TEdit; { Baud/parity/data/stop entry } + FBtnOpen: TButton; { Opens the serial port } + FBtnClose: TButton; { Closes the serial port } + FLabelStatus: TLabel; { Displays "Open" or "Closed" } procedure AnsiKeyData(Sender: TObject; const Data: string); procedure BtnCloseClick(Sender: TObject); procedure BtnOpenClick(Sender: TObject); @@ -85,19 +88,16 @@ begin case FComm.CommEvent of comEvReceive: begin - { Drain all available data in a single update batch. This } - { suppresses per-Write rendering so we get one paint at the } - { end instead of one per 255-byte chunk. } - FAnsi.BeginUpdate; - try - repeat - S := FComm.Input; - if Length(S) > 0 then - FAnsi.Write(S); - until Length(S) = 0; - finally - FAnsi.EndUpdate; - end; + { Drain all available data. The driver uses edge-triggered } + { CN_RECEIVE: it posts once when rxCount crosses the threshold } + { and won't re-post until rxCount drops below and re-crosses. } + { If we don't drain here, remaining data stalls permanently. } + { Write renders each chunk immediately (no batching). } + repeat + S := FComm.Input; + if Length(S) > 0 then + FAnsi.Write(S); + until Length(S) = 0; end; end; end; diff --git a/drv/commdrv.c b/drv/commdrv.c index bdf66d8..32c6af8 100644 --- a/drv/commdrv.c +++ b/drv/commdrv.c @@ -193,7 +193,7 @@ static void readPortConfig(int16_t commId, uint16_t FAR *baseAddr, uint8_t F static uint16_t readSystemIni(const char FAR *section, const char FAR *key, uint16_t defVal); // ----------------------------------------------------------------------- -// Port base addresses and IRQ table +// Port base addresses and IRQ defaults (overridden by SYSTEM.INI) // ----------------------------------------------------------------------- static const uint16_t portBase[MAX_PORTS] = { COM1_BASE, COM2_BASE, COM3_BASE, COM4_BASE @@ -204,12 +204,14 @@ static const uint8_t portIrq[MAX_PORTS] = { }; // ----------------------------------------------------------------------- -// Global port state array +// Global port state array -- one PortStateT per COM port. +// Accessed from both application context and ISR context. +// Segment address loaded into DS by ISR entry points (isr3/isr4). // ----------------------------------------------------------------------- PortStateT ports[MAX_PORTS]; // ----------------------------------------------------------------------- -// Global instance handle +// DLL instance handle (saved in LibMain for resource loading) // ----------------------------------------------------------------------- static HANDLE ghInstance = NULL; @@ -575,6 +577,7 @@ int16_t FAR PASCAL _export cflush(int16_t commId, int16_t queue) port->rxHead = 0; port->rxTail = 0; port->rxCount = 0; + port->rxNotifySent = 0; port->comDeb.qInHead = 0; port->comDeb.qInTail = 0; port->comDeb.qInCount = 0; @@ -586,6 +589,7 @@ int16_t FAR PASCAL _export cflush(int16_t commId, int16_t queue) port->txHead = 0; port->txTail = 0; port->txCount = 0; + port->txNotifySent = 0; port->comDeb.qOutHead = 0; port->comDeb.qOutTail = 0; port->comDeb.qOutCount = 0; @@ -754,6 +758,8 @@ int16_t FAR PASCAL _export enableNotification(int16_t commId, HWND hwnd, int16_t port->hwndNotify = hwnd; port->rxNotifyThresh = rxThresh; port->txNotifyThresh = txThresh; + port->rxNotifySent = 0; + port->txNotifySent = 0; dbgStr("KPCOMM: enableNotif OK\r\n"); return TRUE; @@ -1089,6 +1095,8 @@ static void initPortState(PortStateT *port, int16_t commId) port->txImmediate = -1; port->rxNotifyThresh = -1; port->txNotifyThresh = -1; + port->rxNotifySent = 0; + port->txNotifySent = 0; } @@ -1286,6 +1294,12 @@ int16_t FAR PASCAL _export reccom(int16_t commId, void FAR *buf, int16_t len) port->rxCount--; bytesRead++; } + // Reset edge flag under CLI so the next ISR will re-post + // CN_RECEIVE when new data crosses the threshold again. + if (port->rxNotifyThresh >= 0 && + port->rxCount < (uint16_t)port->rxNotifyThresh) { + port->rxNotifySent = 0; + } _enable(); // If flow control was asserted and buffer has drained, deassert @@ -1444,13 +1458,15 @@ int16_t FAR PASCAL _export setque(int16_t commId, int16_t rxSz, int16_t txSz) port->rxSize = (uint16_t)rxSz; port->rxHead = 0; port->rxTail = 0; - port->rxCount = 0; - port->txBufH = newTxH; - port->txBuf = newTxBuf; - port->txSize = (uint16_t)txSz; - port->txHead = 0; - port->txTail = 0; - port->txCount = 0; + port->rxCount = 0; + port->rxNotifySent = 0; + port->txBufH = newTxH; + port->txBuf = newTxBuf; + port->txSize = (uint16_t)txSz; + port->txHead = 0; + port->txTail = 0; + port->txCount = 0; + port->txNotifySent = 0; port->comDeb.qInSize = (uint16_t)rxSz; port->comDeb.qInCount = 0; port->comDeb.qInHead = 0; @@ -1518,6 +1534,14 @@ int16_t FAR PASCAL _export sndcom(int16_t commId, void FAR *buf, int16_t len) port->txCount++; bytesWritten++; } + // Reset TX edge flag under CLI so the ISR will re-post + // CN_TRANSMIT when free space crosses the threshold again. + if (port->txNotifyThresh >= 0) { + uint16_t txFree = port->txSize - port->txCount; + if (txFree < (uint16_t)port->txNotifyThresh) { + port->txNotifySent = 0; + } + } _enable(); // Sync ComDEB queue counts diff --git a/drv/commdrv.h b/drv/commdrv.h index 7cd3975..026fabe 100644 --- a/drv/commdrv.h +++ b/drv/commdrv.h @@ -322,86 +322,99 @@ typedef struct { // ----------------------------------------------------------------------- // Port state structure +// +// One per COM port. Accessed from both application context (reccom, +// sndcom, etc.) and ISR context (handleRx, handleTx, checkNotify). +// Fields shared between contexts are protected by _disable()/_enable(). // ----------------------------------------------------------------------- typedef struct { - // Stock-compatible ComDEB -- must be kept synchronized. - // cevt returns a FAR pointer to this. + // Stock-compatible ComDEB -- must be at offset 0. + // cevt (SetCommEventMask) returns a FAR pointer to this. + // Third-party code reads msrShadow at offset 35 (KB Q101417). ComDebT comDeb; - uint16_t baseAddr; // UART base I/O address - uint8_t irq; // IRQ number (3 or 4) - int16_t commId; // Port ID (0=COM1, 1=COM2, ...) - uint8_t isOpen; // Port open flag - uint8_t is16550; // 16550 FIFO detected - uint8_t fifoEnabled; // FIFO enabled (COMnFIFO setting) - uint8_t fifoTrigger; // RX FIFO trigger FCR bits (FCR_TRIG_*) + // Hardware identification (from SYSTEM.INI or defaults) + uint16_t baseAddr; // UART base I/O address (e.g. 0x03F8 for COM1) + uint8_t irq; // IRQ number (3 for COM2/4, 4 for COM1/3) + int16_t commId; // Port index (0=COM1, 1=COM2, 2=COM3, 3=COM4) + uint8_t isOpen; // Nonzero while port is open (guards ISR dispatch) + uint8_t is16550; // Nonzero if 16550 FIFO detected at init + uint8_t fifoEnabled; // Nonzero if FIFO use enabled (COMnFIFO in SYSTEM.INI) + uint8_t fifoTrigger; // RX FIFO trigger level FCR bits (FCR_TRIG_*) - // Ring buffers (GlobalAlloc'd GMEM_FIXED) - HGLOBAL rxBufH; // Receive buffer handle - uint8_t FAR *rxBuf; // Receive ring buffer - uint16_t rxSize; // Buffer size - uint16_t rxHead; // Write position (ISR writes) - uint16_t rxTail; // Read position (app reads) - uint16_t rxCount; // Bytes in buffer + // Receive ring buffer (GlobalAlloc'd GMEM_FIXED for ISR stability) + HGLOBAL rxBufH; // GlobalAlloc handle (for GlobalFree on close) + uint8_t FAR *rxBuf; // Far pointer to buffer data + uint16_t rxSize; // Buffer capacity in bytes + uint16_t rxHead; // Write position -- ISR increments + uint16_t rxTail; // Read position -- reccom increments + uint16_t rxCount; // Bytes in buffer (ISR increments, reccom decrements) - HGLOBAL txBufH; // Transmit buffer handle - uint8_t FAR *txBuf; // Transmit ring buffer - uint16_t txSize; // Buffer size - uint16_t txHead; // Write position (app writes) - uint16_t txTail; // Read position (ISR reads) - uint16_t txCount; // Bytes in buffer + // Transmit ring buffer (GlobalAlloc'd GMEM_FIXED for ISR stability) + HGLOBAL txBufH; // GlobalAlloc handle (for GlobalFree on close) + uint8_t FAR *txBuf; // Far pointer to buffer data + uint16_t txSize; // Buffer capacity in bytes + uint16_t txHead; // Write position -- sndcom increments + uint16_t txTail; // Read position -- ISR (handleTx) increments + uint16_t txCount; // Bytes in buffer (sndcom increments, ISR decrements) - // DCB shadow - uint16_t baudRate; // Current baud rate - uint8_t byteSize; // Data bits (5-8) - uint8_t parity; // Parity mode - uint8_t stopBits; // Stop bits + // Serial parameters (cached from DCB at inicom/setcom time) + uint16_t baudRate; // Baud rate (raw or CBR_* index for >32767) + uint8_t byteSize; // Data bits per character (5-8) + uint8_t parity; // Parity mode (NOPARITY, ODDPARITY, EVENPARITY, ...) + uint8_t stopBits; // Stop bits (ONESTOPBIT, TWOSTOPBITS) - // Flow control state - uint8_t hsMode; // Handshaking mode - uint8_t txStopped; // TX halted by flow control - uint8_t rxStopped; // We sent XOFF / dropped RTS - uint8_t xonChar; // XON character (default 0x11) - uint8_t xoffChar; // XOFF character (default 0x13) - uint16_t xoffLim; // Send XOFF when rxCount > rxSize - xoffLim - uint16_t xonLim; // Send XON when rxCount < xonLim + // Flow control state (managed by ISR and application code) + uint8_t hsMode; // Handshaking mode (HS_NONE/XONXOFF/RTSCTS/BOTH) + uint8_t txStopped; // Nonzero = TX halted (received XOFF or CTS dropped) + uint8_t rxStopped; // Nonzero = we sent XOFF or dropped RTS + uint8_t xonChar; // XON character to send/recognize (default 0x11 DC1) + uint8_t xoffChar; // XOFF character to send/recognize (default 0x13 DC3) + uint16_t xoffLim; // Assert flow control when rxCount > rxSize - xoffLim + uint16_t xonLim; // Release flow control when rxCount < xonLim - // Modem control shadow - uint8_t dtrState; // DTR line state - uint8_t rtsState; // RTS line state (when not flow-controlled) + // Modem control line shadow (tracks EscapeCommFunction calls) + uint8_t dtrState; // Nonzero = DTR is asserted + uint8_t rtsState; // Nonzero = RTS is asserted (when not flow-controlled) - // Error accumulator - uint16_t errorFlags; // CE_* error flags (sticky until read) + // Error accumulator (sticky CE_* bits, cleared by stacom/GetCommError) + uint16_t errorFlags; // Accumulated CE_RXOVER, CE_OVERRUN, CE_FRAME, etc. - // Event notification - HWND hwndNotify; // Window for WM_COMMNOTIFY - int16_t rxNotifyThresh; // CN_RECEIVE threshold (-1=disabled) - int16_t txNotifyThresh; // CN_TRANSMIT threshold (-1=disabled) + // WM_COMMNOTIFY event notification + HWND hwndNotify; // Target window for PostMessage (0=disabled) + int16_t rxNotifyThresh; // CN_RECEIVE fires when rxCount >= this (-1=disabled) + int16_t txNotifyThresh; // CN_TRANSMIT fires when txFree >= this (-1=disabled) + uint8_t rxNotifySent; // Edge flag: 1 = CN_RECEIVE posted, suppresses repeats + uint8_t txNotifySent; // Edge flag: 1 = CN_TRANSMIT posted, suppresses repeats - // ISR state - void (FAR *prevIsr)(void); // Previous ISR in chain - uint8_t irqMask; // PIC mask bit for this IRQ - uint8_t breakState; // Break signal active + // ISR chaining and PIC management + void (FAR *prevIsr)(void); // Previous ISR vector (restored on unhook) + uint8_t irqMask; // PIC mask bit for this IRQ (1 << irq) + uint8_t breakState; // Nonzero while break signal is active on line - // Priority transmit - int16_t txImmediate; // -1=none, else char to send immediately + // Priority transmit (XON/XOFF flow control characters) + int16_t txImmediate; // -1=none, 0..255=char to send before buffered data - // DCB copy for GetCommState - DCB dcb; // Full DCB for GETDCB/SETCOM + // Full DCB copy (returned by getdcb, updated by setcom) + DCB dcb; } PortStateT; // ----------------------------------------------------------------------- -// Global port state array +// Global port state array (one entry per COM port, indexed by commId) // ----------------------------------------------------------------------- extern PortStateT ports[MAX_PORTS]; // ----------------------------------------------------------------------- -// Dynamically resolved PostMessage (USER.EXE not loaded at boot) +// Dynamically resolved PostMessage +// +// COMM.DRV loads at boot from [boot] in SYSTEM.INI, before USER.EXE +// is available. PostMessage is resolved lazily on first use via +// GetModuleHandle("USER") + GetProcAddress. NULL until resolved. // ----------------------------------------------------------------------- typedef BOOL (FAR PASCAL *PostMessageProcT)(HWND, UINT, WPARAM, LPARAM); extern PostMessageProcT pfnPostMessage; -// ISR hit counter for diagnostics +// ISR hit counter for diagnostics (incremented in isr4, wraps at 65535) extern volatile uint16_t isrHitCount; // ----------------------------------------------------------------------- diff --git a/drv/isr.c b/drv/isr.c index 4dbda3c..db8743b 100644 --- a/drv/isr.c +++ b/drv/isr.c @@ -21,11 +21,17 @@ void _far _interrupt isr4(void); // ----------------------------------------------------------------------- // Saved previous ISR vectors for IRQ3 and IRQ4 +// +// When we hook an IRQ via DPMI, the original vector is saved here so +// unhookIsr can restore it. Only saved/restored on first-use/last-use +// of each IRQ (ref-counted for shared IRQs like COM1+COM3). // ----------------------------------------------------------------------- -static void (_far _interrupt *prevIsr3)(void) = NULL; -static void (_far _interrupt *prevIsr4)(void) = NULL; +static void (_far _interrupt *prevIsr3)(void) = NULL; // Original IRQ3 vector +static void (_far _interrupt *prevIsr4)(void) = NULL; // Original IRQ4 vector -// Track how many ports are using each IRQ so we know when to unhook +// Reference counts: how many open ports share each IRQ. +// hookIsr increments; unhookIsr decrements. Vector is only +// installed/restored when the count transitions through 0. static int16_t irq3RefCount = 0; static int16_t irq4RefCount = 0; @@ -92,16 +98,32 @@ static void checkNotify(PortStateT *port) notifyBits = 0; - // CN_RECEIVE: rxCount crossed threshold from below - if (port->rxNotifyThresh >= 0 && port->rxCount >= (uint16_t)port->rxNotifyThresh) { - notifyBits |= CN_RECEIVE; + // CN_RECEIVE: edge-triggered -- post once when rxCount reaches + // threshold, suppress until count drops below and re-crosses. + // Without edge detection, every ISR with rxCount >= 1 floods the + // message queue with stale WM_COMMNOTIFY, delaying keyboard input. + if (port->rxNotifyThresh >= 0) { + if (port->rxCount >= (uint16_t)port->rxNotifyThresh) { + if (!port->rxNotifySent) { + notifyBits |= CN_RECEIVE; + port->rxNotifySent = 1; + } + } else { + port->rxNotifySent = 0; + } } - // CN_TRANSMIT: space available crossed threshold + // CN_TRANSMIT: edge-triggered -- post once when free space reaches + // threshold, suppress until space drops below and re-crosses. if (port->txNotifyThresh >= 0) { uint16_t txFree = port->txSize - port->txCount; if (txFree >= (uint16_t)port->txNotifyThresh) { - notifyBits |= CN_TRANSMIT; + if (!port->txNotifySent) { + notifyBits |= CN_TRANSMIT; + port->txNotifySent = 1; + } + } else { + port->txNotifySent = 0; } }