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 <noreply@anthropic.com>
This commit is contained in:
parent
5dd464eb18
commit
acf1a6b691
6 changed files with 393 additions and 238 deletions
|
|
@ -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;
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
123
drv/commdrv.h
123
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;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
|
|
|||
38
drv/isr.c
38
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue