Optimize TKPAnsi rendering with batched TextOut and dirty row tracking

Separate parsing from rendering to eliminate per-character GDI calls.
ProcessChar now only updates cell data in memory; rendering is deferred
to FlipToScreen which batches consecutive same-color cells into single
TextOut calls (~5-10 per row instead of 80). Partial BitBlt transfers
only the dirty row band to the screen. Non-blinking rows render to one
buffer and BitBlt to the second, halving GDI work for typical content.

Also removes redundant GetCommError from KPComm receive path and adds
BeginUpdate/EndUpdate batching in the test app's CommEvent handler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Duensing 2026-02-26 21:15:51 -06:00
parent be566a5767
commit ca99d1d21b
4 changed files with 576 additions and 235 deletions

View file

@ -61,37 +61,47 @@ type
FOnKeyData: TKeyDataEvent; FOnKeyData: TKeyDataEvent;
FPaintFont: HFont; FPaintFont: HFont;
FStockFont: Boolean; FStockFont: Boolean;
FMemDC: HDC; FBlinkPhase: Integer;
FMemBmp: HBitmap; FUpdateCount: Integer;
FMemOldBmp: HBitmap; FDirtyRow: array[0..255] of Boolean;
FAllDirty: Boolean;
FBufDC: array[0..1] of HDC;
FBufBmp: array[0..1] of HBitmap;
FBufOldBmp: array[0..1] of HBitmap;
FBufW: Integer; FBufW: Integer;
FBufH: Integer; FBufH: Integer;
FTextBlinkOn: Boolean;
FBlinkCount: Integer;
procedure AllocLine(Line: PTermLine); procedure AllocLine(Line: PTermLine);
procedure ClearBufRect(X, Y, W, H: Integer; BG: TColor);
procedure ClearLine(Line: PTermLine); procedure ClearLine(Line: PTermLine);
procedure CMFontChanged(var Msg: TMessage); message cm_FontChanged; procedure CMFontChanged(var Msg: TMessage); message cm_FontChanged;
procedure CreateBackBuffer; procedure CreateBuffers;
procedure CreatePaintFont; procedure CreatePaintFont;
procedure DeleteChars(N: Integer); procedure DeleteChars(N: Integer);
procedure DeleteLines(N: Integer); procedure DeleteLines(N: Integer);
procedure DestroyBackBuffer; procedure DestroyBuffers;
procedure DirtyAll;
procedure DirtyRow(Row: Integer);
procedure DoScrollDown; procedure DoScrollDown;
procedure DoScrollUp; procedure DoScrollUp;
procedure DrawRow(Row: Integer);
procedure EraseDisplay(Mode: Integer); procedure EraseDisplay(Mode: Integer);
procedure EraseLine(Mode: Integer); procedure EraseLine(Mode: Integer);
procedure ExecuteCSI(FinalCh: Char); procedure ExecuteCSI(FinalCh: Char);
procedure ExecuteMusic; procedure ExecuteMusic;
procedure FlipToScreen;
procedure FreeLineList(List: TList); procedure FreeLineList(List: TList);
function GetCursorCol: Integer; function GetCursorCol: Integer;
function GetCursorRow: Integer; function GetCursorRow: Integer;
procedure InsertChars(N: Integer); procedure InsertChars(N: Integer);
procedure InsertLines(N: Integer); procedure InsertLines(N: Integer);
procedure PaintLine(Line: PTermLine; PixelY: Integer);
procedure ParseData(const S: string); procedure ParseData(const S: string);
procedure ParseSGR; procedure ParseSGR;
procedure ProcessChar(Ch: Char); procedure ProcessChar(Ch: Char);
procedure RecalcCellSize; procedure RecalcCellSize;
procedure RedrawBuffers;
procedure ResizeScreen; procedure ResizeScreen;
procedure ScrollBufs(N: Integer);
procedure SetCols(Value: Integer); procedure SetCols(Value: Integer);
procedure SetCursorVisible(Value: Boolean); procedure SetCursorVisible(Value: Boolean);
procedure SetRows(Value: Integer); procedure SetRows(Value: Integer);
@ -110,7 +120,9 @@ type
public public
constructor Create(AOwner: TComponent); override; constructor Create(AOwner: TComponent); override;
destructor Destroy; override; destructor Destroy; override;
procedure BeginUpdate;
procedure Clear; procedure Clear;
procedure EndUpdate;
procedure Reset; procedure Reset;
procedure Write(const S: string); procedure Write(const S: string);
property CursorCol: Integer read GetCursorCol; property CursorCol: Integer read GetCursorCol;
@ -227,6 +239,12 @@ begin
end; end;
procedure TKPAnsi.BeginUpdate;
begin
Inc(FUpdateCount);
end;
procedure TKPAnsi.Clear; procedure TKPAnsi.Clear;
var var
I: Integer; I: Integer;
@ -253,6 +271,7 @@ begin
FCursorCol := 0; FCursorCol := 0;
FScrollPos := 0; FScrollPos := 0;
UpdateScrollbar; UpdateScrollbar;
FAllDirty := True;
Invalidate; Invalidate;
end; end;
@ -315,13 +334,15 @@ begin
FWrapMode := True; FWrapMode := True;
FPaintFont := 0; FPaintFont := 0;
FStockFont := False; FStockFont := False;
FMemDC := 0; FBlinkPhase := 0;
FMemBmp := 0; FUpdateCount := 0;
FMemOldBmp := 0; FAllDirty := True;
FBufDC[0] := 0;
FBufDC[1] := 0;
FBufBmp[0] := 0;
FBufBmp[1] := 0;
FBufW := 0; FBufW := 0;
FBufH := 0; FBufH := 0;
FTextBlinkOn := True;
FBlinkCount := 0;
{ Set a monospace font -- OEM charset selected in CreatePaintFont } { Set a monospace font -- OEM charset selected in CreatePaintFont }
Font.Name := 'Terminal'; Font.Name := 'Terminal';
@ -338,31 +359,6 @@ begin
end; end;
procedure TKPAnsi.CreateBackBuffer;
var
ScreenDC: HDC;
begin
DestroyBackBuffer;
FBufW := FCols * FCellWidth;
FBufH := FRows * FCellHeight;
if (FBufW < 1) or (FBufH < 1) then
Exit;
{ Create a memory DC with compatible bitmap for double buffering. }
{ We manage this DC directly with GDI calls -- no TCanvas/TBitmap }
{ involvement -- so our OEM_CHARSET font stays selected and }
{ cannot be overridden by VCL font realization. }
ScreenDC := GetDC(0);
FMemDC := CreateCompatibleDC(ScreenDC);
FMemBmp := CreateCompatibleBitmap(ScreenDC, FBufW, FBufH);
ReleaseDC(0, ScreenDC);
FMemOldBmp := SelectObject(FMemDC, FMemBmp);
if FPaintFont <> 0 then
SelectObject(FMemDC, FPaintFont);
SetBkMode(FMemDC, OPAQUE);
end;
procedure TKPAnsi.CreatePaintFont; procedure TKPAnsi.CreatePaintFont;
var var
LF: TLogFont; LF: TLogFont;
@ -376,7 +372,9 @@ begin
FStockFont := False; FStockFont := False;
{ Build LOGFONT requesting OEM_CHARSET with raster precision for CP437 } { Build LOGFONT requesting OEM_CHARSET with raster precision for CP437 }
{ box-drawing, block elements, and other BBS ANSI art glyphs. } { box-drawing, block elements, and other BBS ANSI art glyphs. Raster }
{ precision prevents the font mapper from substituting a TrueType font }
{ that might remap character codes through Unicode tables. }
FillChar(LF, SizeOf(LF), 0); FillChar(LF, SizeOf(LF), 0);
LF.lfHeight := Font.Height; LF.lfHeight := Font.Height;
LF.lfPitchAndFamily := FIXED_PITCH or FF_MODERN; LF.lfPitchAndFamily := FIXED_PITCH or FF_MODERN;
@ -394,21 +392,25 @@ begin
{ Verify Windows actually gave us an OEM charset font } { Verify Windows actually gave us an OEM charset font }
GetObject(NewFont, SizeOf(ActualLF), @ActualLF); GetObject(NewFont, SizeOf(ActualLF), @ActualLF);
if ActualLF.lfCharSet = OEM_CHARSET then if ActualLF.lfCharSet = OEM_CHARSET then
begin FPaintFont := NewFont
FPaintFont := NewFont; else
if FMemDC <> 0 then
SelectObject(FMemDC, FPaintFont);
Exit;
end;
{ Windows substituted a non-OEM font; discard it }
DeleteObject(NewFont); DeleteObject(NewFont);
end; end;
{ Fall back to the stock OEM fixed font -- guaranteed CP437 } if FPaintFont = 0 then
begin
FPaintFont := GetStockObject(OEM_FIXED_FONT); FPaintFont := GetStockObject(OEM_FIXED_FONT);
FStockFont := True; FStockFont := True;
if FMemDC <> 0 then end;
SelectObject(FMemDC, FPaintFont);
{ Select font into both buffer DCs }
if FPaintFont <> 0 then
begin
if FBufDC[0] <> 0 then
SelectObject(FBufDC[0], FPaintFont);
if FBufDC[1] <> 0 then
SelectObject(FBufDC[1], FPaintFont);
end;
end; end;
@ -416,6 +418,364 @@ procedure TKPAnsi.CreateParams(var Params: TCreateParams);
begin begin
inherited CreateParams(Params); inherited CreateParams(Params);
Params.Style := Params.Style or ws_VScroll; Params.Style := Params.Style or ws_VScroll;
{ CS_OWNDC gives us a private DC whose state (selected font, BkMode, }
{ colors) persists across GetDC/ReleaseDC calls. This eliminates the }
{ per-frame cost of SelectObject + SetBkMode that otherwise dominates }
{ rendering time on Win16. }
Params.WindowClass.Style := Params.WindowClass.Style or cs_OwnDC;
end;
procedure TKPAnsi.ClearBufRect(X, Y, W, H: Integer; BG: TColor);
var
R: TRect;
HBr: HBrush;
I: Integer;
begin
R.Left := X;
R.Top := Y;
R.Right := X + W;
R.Bottom := Y + H;
HBr := CreateSolidBrush(ColorToRGB(BG));
for I := 0 to 1 do
begin
if FBufDC[I] <> 0 then
FillRect(FBufDC[I], R, HBr);
end;
DeleteObject(HBr);
end;
procedure TKPAnsi.CreateBuffers;
var
ScreenDC: HDC;
I: Integer;
begin
DestroyBuffers;
FBufW := FCols * FCellWidth;
FBufH := FRows * FCellHeight;
if (FBufW < 1) or (FBufH < 1) then
Exit;
ScreenDC := GetDC(0);
for I := 0 to 1 do
begin
FBufDC[I] := CreateCompatibleDC(ScreenDC);
FBufBmp[I] := CreateCompatibleBitmap(ScreenDC, FBufW, FBufH);
FBufOldBmp[I] := SelectObject(FBufDC[I], FBufBmp[I]);
if FPaintFont <> 0 then
SelectObject(FBufDC[I], FPaintFont);
SetBkMode(FBufDC[I], OPAQUE);
end;
ReleaseDC(0, ScreenDC);
end;
procedure TKPAnsi.DestroyBuffers;
var
I: Integer;
begin
for I := 0 to 1 do
begin
if FBufDC[I] <> 0 then
begin
SelectObject(FBufDC[I], FBufOldBmp[I]);
DeleteObject(FBufBmp[I]);
DeleteDC(FBufDC[I]);
FBufDC[I] := 0;
FBufBmp[I] := 0;
FBufOldBmp[I] := 0;
end;
end;
end;
procedure TKPAnsi.DirtyAll;
begin
FAllDirty := True;
end;
procedure TKPAnsi.DirtyRow(Row: Integer);
begin
if (Row >= 0) and (Row <= 255) then
FDirtyRow[Row] := True;
end;
procedure TKPAnsi.DrawRow(Row: Integer);
begin
if (Row >= 0) and (Row < FScreen.Count) then
PaintLine(FScreen[Row], Row * FCellHeight);
end;
procedure TKPAnsi.PaintLine(Line: PTermLine; PixelY: Integer);
{ Render a line to both buffer DCs using batched TextOut. }
{ Groups consecutive cells with identical colors into single calls }
{ (typically 5-10 per row instead of 80 per-cell calls). }
var
Col: Integer;
BatchStart: Integer;
BatchLen: Integer;
I: Integer;
X: Integer;
CellFG: TColor;
CellBG: TColor;
CurFG: TColor;
CurBG: TColor;
HasBlink: Boolean;
Buf: array[0..255] of Char;
begin
if FBufDC[0] = 0 then
Exit;
{ Check if any cell on this line blinks }
HasBlink := False;
for Col := 0 to FCols - 1 do
begin
if Line^.Cells[Col].Blink then
begin
HasBlink := True;
Break;
end;
end;
if HasBlink then
begin
{ Blink row: must render to each buffer separately }
for I := 0 to 1 do
begin
if FBufDC[I] = 0 then
Continue;
CurFG := TColor(-1);
CurBG := TColor(-1);
BatchStart := 0;
BatchLen := 0;
for Col := 0 to FCols - 1 do
begin
CellBG := Line^.Cells[Col].BG;
if (I = 1) and Line^.Cells[Col].Blink then
CellFG := CellBG
else
CellFG := Line^.Cells[Col].FG;
if (CellFG <> CurFG) or (CellBG <> CurBG) then
begin
if BatchLen > 0 then
begin
X := BatchStart * FCellWidth;
WinProcs.TextOut(FBufDC[I], X, PixelY, @Buf[0], BatchLen);
end;
if CellFG <> CurFG then
begin
SetTextColor(FBufDC[I], ColorToRGB(CellFG));
CurFG := CellFG;
end;
if CellBG <> CurBG then
begin
SetBkColor(FBufDC[I], ColorToRGB(CellBG));
CurBG := CellBG;
end;
BatchStart := Col;
BatchLen := 0;
end;
Buf[BatchLen] := Line^.Cells[Col].Ch;
Inc(BatchLen);
end;
if BatchLen > 0 then
begin
X := BatchStart * FCellWidth;
WinProcs.TextOut(FBufDC[I], X, PixelY, @Buf[0], BatchLen);
end;
end;
end
else
begin
{ No blink: render to buffer 0 only, then copy row to buffer 1 }
CurFG := TColor(-1);
CurBG := TColor(-1);
BatchStart := 0;
BatchLen := 0;
for Col := 0 to FCols - 1 do
begin
CellFG := Line^.Cells[Col].FG;
CellBG := Line^.Cells[Col].BG;
if (CellFG <> CurFG) or (CellBG <> CurBG) then
begin
if BatchLen > 0 then
begin
X := BatchStart * FCellWidth;
WinProcs.TextOut(FBufDC[0], X, PixelY, @Buf[0], BatchLen);
end;
if CellFG <> CurFG then
begin
SetTextColor(FBufDC[0], ColorToRGB(CellFG));
CurFG := CellFG;
end;
if CellBG <> CurBG then
begin
SetBkColor(FBufDC[0], ColorToRGB(CellBG));
CurBG := CellBG;
end;
BatchStart := Col;
BatchLen := 0;
end;
Buf[BatchLen] := Line^.Cells[Col].Ch;
Inc(BatchLen);
end;
if BatchLen > 0 then
begin
X := BatchStart * FCellWidth;
WinProcs.TextOut(FBufDC[0], X, PixelY, @Buf[0], BatchLen);
end;
{ Copy rendered row from buffer 0 to buffer 1 }
if FBufDC[1] <> 0 then
BitBlt(FBufDC[1], 0, PixelY, FBufW, FCellHeight,
FBufDC[0], 0, PixelY, SRCCOPY);
end;
end;
procedure TKPAnsi.FlipToScreen;
var
DC: HDC;
Row: Integer;
MinDirty: Integer;
MaxDirty: Integer;
FullBlt: Boolean;
Line: PTermLine;
X: Integer;
Y: Integer;
SrcY: Integer;
H: Integer;
begin
if not HandleAllocated then
Exit;
if FBufDC[0] = 0 then
RecalcCellSize;
if FBufDC[0] = 0 then
Exit;
FullBlt := FAllDirty;
if FScrollPos <> 0 then
begin
{ Scrollback view: full redraw from scrollback + screen data }
RedrawBuffers;
FullBlt := True;
end
else
begin
{ Render only dirty rows into both buffers (batched TextOut) }
MinDirty := FRows;
MaxDirty := -1;
for Row := 0 to FRows - 1 do
begin
if FAllDirty or FDirtyRow[Row] then
begin
if Row < FScreen.Count then
PaintLine(FScreen[Row], Row * FCellHeight);
FDirtyRow[Row] := False;
if Row < MinDirty then
MinDirty := Row;
if Row > MaxDirty then
MaxDirty := Row;
end;
end;
FAllDirty := False;
end;
DC := GetDC(Handle);
if FullBlt then
begin
{ Full screen BitBlt (scrollback, resize, blink all-dirty) }
BitBlt(DC, 0, 0, FBufW, FBufH,
FBufDC[FBlinkPhase], 0, 0, SRCCOPY);
end
else if MaxDirty >= 0 then
begin
{ Partial BitBlt: only the dirty row band }
SrcY := MinDirty * FCellHeight;
H := (MaxDirty - MinDirty + 1) * FCellHeight;
BitBlt(DC, 0, SrcY, FBufW, H,
FBufDC[FBlinkPhase], 0, SrcY, SRCCOPY);
end;
{ Cursor overlay -- drawn directly to screen, not in buffers }
if FCursorVisible and FBlinkOn and (FScrollPos = 0) and
(FCursorRow >= 0) and (FCursorRow < FRows) and
(FCursorRow < FScreen.Count) and
(FCursorCol >= 0) and (FCursorCol < FCols) then
begin
Line := FScreen[FCursorRow];
X := FCursorCol * FCellWidth;
Y := FCursorRow * FCellHeight;
SetTextColor(DC, ColorToRGB(Line^.Cells[FCursorCol].BG));
SetBkColor(DC, ColorToRGB(Line^.Cells[FCursorCol].FG));
WinProcs.TextOut(DC, X, Y, @Line^.Cells[FCursorCol].Ch, 1);
end;
ReleaseDC(Handle, DC);
end;
procedure TKPAnsi.RedrawBuffers;
var
Row: Integer;
VisRow: Integer;
SbkCount: Integer;
Line: PTermLine;
begin
if FBufDC[0] = 0 then
Exit;
SbkCount := FScrollback.Count;
for Row := 0 to FRows - 1 do
begin
VisRow := Row - FScrollPos;
if VisRow < 0 then
begin
if (SbkCount + VisRow >= 0) and (SbkCount + VisRow < SbkCount) then
Line := FScrollback[SbkCount + VisRow]
else
Line := nil;
end
else if VisRow < FScreen.Count then
Line := FScreen[VisRow]
else
Line := nil;
if Line = nil then
ClearBufRect(0, Row * FCellHeight, FBufW, FCellHeight, AnsiColors[0])
else
PaintLine(Line, Row * FCellHeight);
end;
end;
procedure TKPAnsi.ScrollBufs(N: Integer);
var
R: TRect;
I: Integer;
ScrollY: Integer;
begin
if FBufDC[0] = 0 then
Exit;
ScrollY := N * FCellHeight;
R.Left := 0;
R.Top := 0;
R.Right := FBufW;
R.Bottom := FBufH;
for I := 0 to 1 do
ScrollDC(FBufDC[I], 0, -ScrollY, R, R, 0, nil);
end; end;
@ -427,12 +787,8 @@ begin
if N < 1 then if N < 1 then
N := 1; N := 1;
Line := FScreen[FCursorRow]; Line := FScreen[FCursorRow];
{ Shift cells left }
for I := FCursorCol to FCols - 1 - N do for I := FCursorCol to FCols - 1 - N do
begin
Line^.Cells[I] := Line^.Cells[I + N]; Line^.Cells[I] := Line^.Cells[I + N];
end;
{ Clear vacated cells at end }
for I := FCols - N to FCols - 1 do for I := FCols - N to FCols - 1 do
begin begin
if I >= 0 then if I >= 0 then
@ -444,12 +800,14 @@ begin
Line^.Cells[I].Blink := False; Line^.Cells[I].Blink := False;
end; end;
end; end;
FDirtyRow[FCursorRow] := True;
end; end;
procedure TKPAnsi.DeleteLines(N: Integer); procedure TKPAnsi.DeleteLines(N: Integer);
var var
I: Integer; I: Integer;
J: Integer;
Line: PTermLine; Line: PTermLine;
begin begin
if N < 1 then if N < 1 then
@ -461,37 +819,24 @@ begin
Line := FScreen[FCursorRow]; Line := FScreen[FCursorRow];
FreeMem(Line, SizeOf(TTermLineRec)); FreeMem(Line, SizeOf(TTermLineRec));
FScreen.Delete(FCursorRow); FScreen.Delete(FCursorRow);
{ Add a blank line at the bottom }
GetMem(Line, SizeOf(TTermLineRec)); GetMem(Line, SizeOf(TTermLineRec));
AllocLine(Line); AllocLine(Line);
FScreen.Add(Line); FScreen.Add(Line);
end; end;
end; end;
end; for J := FCursorRow to FRows - 1 do
FDirtyRow[J] := True;
procedure TKPAnsi.DestroyBackBuffer;
begin
if FMemDC <> 0 then
begin
SelectObject(FMemDC, FMemOldBmp);
DeleteObject(FMemBmp);
DeleteDC(FMemDC);
FMemDC := 0;
FMemBmp := 0;
FMemOldBmp := 0;
end;
end; end;
destructor TKPAnsi.Destroy; destructor TKPAnsi.Destroy;
begin begin
if FTimerActive then if FTimerActive and HandleAllocated then
begin begin
KillTimer(Handle, 1); KillTimer(Handle, 1);
FTimerActive := False; FTimerActive := False;
end; end;
DestroyBackBuffer; DestroyBuffers;
if (FPaintFont <> 0) and not FStockFont then if (FPaintFont <> 0) and not FStockFont then
begin begin
DeleteObject(FPaintFont); DeleteObject(FPaintFont);
@ -519,6 +864,8 @@ begin
GetMem(Line, SizeOf(TTermLineRec)); GetMem(Line, SizeOf(TTermLineRec));
AllocLine(Line); AllocLine(Line);
FScreen.Insert(0, Line); FScreen.Insert(0, Line);
ScrollBufs(-1);
FDirtyRow[0] := True;
end; end;
@ -538,6 +885,19 @@ begin
AllocLine(Line); AllocLine(Line);
FScreen.Add(Line); FScreen.Add(Line);
UpdateScrollbar; UpdateScrollbar;
ScrollBufs(1);
FDirtyRow[FRows - 1] := True;
end;
procedure TKPAnsi.EndUpdate;
begin
Dec(FUpdateCount);
if FUpdateCount <= 0 then
begin
FUpdateCount := 0;
FlipToScreen;
end;
end; end;
@ -601,6 +961,17 @@ begin
UpdateScrollbar; UpdateScrollbar;
end; end;
end; end;
{ Mark affected rows dirty for deferred batch rendering }
case Mode of
0:
for I := FCursorRow to FRows - 1 do
FDirtyRow[I] := True;
1:
for I := 0 to FCursorRow do
FDirtyRow[I] := True;
2:
FAllDirty := True;
end;
end; end;
@ -634,10 +1005,9 @@ begin
end; end;
end; end;
2: { Erase entire line } 2: { Erase entire line }
begin
ClearLine(Line); ClearLine(Line);
end; end;
end; FDirtyRow[FCursorRow] := True;
end; end;
@ -1008,12 +1378,8 @@ begin
if N < 1 then if N < 1 then
N := 1; N := 1;
Line := FScreen[FCursorRow]; Line := FScreen[FCursorRow];
{ Shift cells right }
for I := FCols - 1 downto FCursorCol + N do for I := FCols - 1 downto FCursorCol + N do
begin
Line^.Cells[I] := Line^.Cells[I - N]; Line^.Cells[I] := Line^.Cells[I - N];
end;
{ Clear inserted cells }
for I := FCursorCol to FCursorCol + N - 1 do for I := FCursorCol to FCursorCol + N - 1 do
begin begin
if I < FCols then if I < FCols then
@ -1025,30 +1391,32 @@ begin
Line^.Cells[I].Blink := False; Line^.Cells[I].Blink := False;
end; end;
end; end;
FDirtyRow[FCursorRow] := True;
end; end;
procedure TKPAnsi.InsertLines(N: Integer); procedure TKPAnsi.InsertLines(N: Integer);
var var
I: Integer; I: Integer;
J: Integer;
Line: PTermLine; Line: PTermLine;
begin begin
if N < 1 then if N < 1 then
N := 1; N := 1;
for I := 1 to N do for I := 1 to N do
begin begin
{ Remove bottom line }
if FScreen.Count > 0 then if FScreen.Count > 0 then
begin begin
Line := FScreen[FScreen.Count - 1]; Line := FScreen[FScreen.Count - 1];
FreeMem(Line, SizeOf(TTermLineRec)); FreeMem(Line, SizeOf(TTermLineRec));
FScreen.Delete(FScreen.Count - 1); FScreen.Delete(FScreen.Count - 1);
end; end;
{ Insert blank line at cursor row }
GetMem(Line, SizeOf(TTermLineRec)); GetMem(Line, SizeOf(TTermLineRec));
AllocLine(Line); AllocLine(Line);
FScreen.Insert(FCursorRow, Line); FScreen.Insert(FCursorRow, Line);
end; end;
for J := FCursorRow to FRows - 1 do
FDirtyRow[J] := True;
end; end;
@ -1135,123 +1503,41 @@ end;
procedure TKPAnsi.Paint; procedure TKPAnsi.Paint;
var var
Row: Integer; Row: Integer;
Col: Integer; Line: PTermLine;
X: Integer; X: Integer;
Y: Integer; Y: Integer;
Line: PTermLine;
StartCol: Integer;
BatchBuf: array[0..255] of Char;
BatchLen: Integer;
BatchFG: TColor;
BatchBG: TColor;
CellFG: TColor;
VisRow: Integer;
SbkOffset: Integer;
SbkCount: Integer;
R: TRect;
HBr: HBrush;
begin begin
{ Ensure back buffer exists (created by RecalcCellSize) } if FBufDC[0] = 0 then
if FMemDC = 0 then
RecalcCellSize; RecalcCellSize;
if FMemDC = 0 then if FBufDC[0] = 0 then
Exit; Exit;
{ Pre-fill entire back buffer with black so no white pixels can } { Ensure all rows are rendered into buffers }
{ leak through gaps between rows or at the edges. }
R.Left := 0;
R.Top := 0;
R.Right := FBufW;
R.Bottom := FBufH;
HBr := CreateSolidBrush(ColorToRGB(AnsiColors[0]));
FillRect(FMemDC, R, HBr);
DeleteObject(HBr);
SbkCount := FScrollback.Count;
for Row := 0 to FRows - 1 do for Row := 0 to FRows - 1 do
begin begin
Y := Row * FCellHeight; if Row < FScreen.Count then
PaintLine(FScreen[Row], Row * FCellHeight);
{ Determine which line to display based on scroll position } FDirtyRow[Row] := False;
VisRow := Row - FScrollPos;
if VisRow < 0 then
begin
{ Drawing from scrollback }
SbkOffset := SbkCount + VisRow;
if (SbkOffset >= 0) and (SbkOffset < SbkCount) then
Line := FScrollback[SbkOffset]
else
Line := nil;
end
else
begin
{ Drawing from active screen }
if VisRow < FScreen.Count then
Line := FScreen[VisRow]
else
Line := nil;
end; end;
FAllDirty := False;
if Line = nil then { BitBlt to canvas (provided by VCL WM_PAINT handler) }
Continue;
{ Batch consecutive cells with same effective colors into a single }
{ TextOut call for performance. Uses a fixed char array instead }
{ of string concatenation to avoid heap allocation per cell. }
{ When text blink is in the off phase, blinking cells render with }
{ FG = BG so the text becomes invisible. }
Col := 0;
while Col < FCols do
begin
StartCol := Col;
BatchBG := Line^.Cells[Col].BG;
if Line^.Cells[Col].Blink and not FTextBlinkOn then
BatchFG := BatchBG
else
BatchFG := Line^.Cells[Col].FG;
BatchBuf[0] := Line^.Cells[Col].Ch;
BatchLen := 1;
Inc(Col);
{ Extend batch while effective colors match }
while Col < FCols do
begin
if Line^.Cells[Col].BG <> BatchBG then
Break;
if Line^.Cells[Col].Blink and not FTextBlinkOn then
CellFG := BatchBG
else
CellFG := Line^.Cells[Col].FG;
if CellFG <> BatchFG then
Break;
BatchBuf[BatchLen] := Line^.Cells[Col].Ch;
Inc(BatchLen);
Inc(Col);
end;
X := StartCol * FCellWidth;
SetTextColor(FMemDC, ColorToRGB(BatchFG));
SetBkColor(FMemDC, ColorToRGB(BatchBG));
WinProcs.TextOut(FMemDC, X, Y, @BatchBuf[0], BatchLen);
end;
{ Draw cursor if on this row and visible }
if FCursorVisible and FBlinkOn and (FScrollPos = 0) and
(Row = FCursorRow) and (FCursorCol < FCols) then
begin
X := FCursorCol * FCellWidth;
{ Invert the cursor cell }
SetTextColor(FMemDC, ColorToRGB(Line^.Cells[FCursorCol].BG));
SetBkColor(FMemDC, ColorToRGB(Line^.Cells[FCursorCol].FG));
WinProcs.TextOut(FMemDC, X, Y,
@Line^.Cells[FCursorCol].Ch, 1);
end;
end;
{ Blit the completed frame to screen in one operation }
BitBlt(Canvas.Handle, 0, 0, FBufW, FBufH, BitBlt(Canvas.Handle, 0, 0, FBufW, FBufH,
FMemDC, 0, 0, SRCCOPY); FBufDC[FBlinkPhase], 0, 0, SRCCOPY);
{ Cursor overlay }
if FCursorVisible and FBlinkOn and (FScrollPos = 0) and
(FCursorRow >= 0) and (FCursorRow < FRows) and
(FCursorRow < FScreen.Count) and
(FCursorCol >= 0) and (FCursorCol < FCols) then
begin
Line := FScreen[FCursorRow];
X := FCursorCol * FCellWidth;
Y := FCursorRow * FCellHeight;
SetTextColor(Canvas.Handle, ColorToRGB(Line^.Cells[FCursorCol].BG));
SetBkColor(Canvas.Handle, ColorToRGB(Line^.Cells[FCursorCol].FG));
WinProcs.TextOut(Canvas.Handle, X, Y, @Line^.Cells[FCursorCol].Ch, 1);
end;
end; end;
@ -1260,27 +1546,21 @@ var
I: Integer; I: Integer;
begin begin
for I := 1 to Length(S) do for I := 1 to Length(S) do
begin
ProcessChar(S[I]); ProcessChar(S[I]);
end;
{ Snap to bottom on new data } { Snap to bottom on new data }
if FScrollPos <> 0 then if FScrollPos <> 0 then
begin begin
FScrollPos := 0; FScrollPos := 0;
UpdateScrollbar; UpdateScrollbar;
FAllDirty := True;
end; end;
{ Reset cursor blink to visible on new data } { Reset cursor blink to visible on new data }
FBlinkOn := True; FBlinkOn := True;
{ Invalidate + Update forces an immediate synchronous repaint. } if FUpdateCount = 0 then
{ Without Update, WM_PAINT is low-priority and gets starved by } FlipToScreen;
{ incoming WM_COMMNOTIFY messages, causing visible input lag. }
{ The paint loop uses a fixed char array (no heap allocs) and our }
{ own GDI memory DC, so repainting is fast enough for this. }
Invalidate;
Update;
end; end;
@ -1426,6 +1706,9 @@ begin
Line^.Cells[FCursorCol].Bold := FAttrBold; Line^.Cells[FCursorCol].Bold := FAttrBold;
Line^.Cells[FCursorCol].Blink := FAttrBlink; Line^.Cells[FCursorCol].Blink := FAttrBlink;
{ Mark row dirty for deferred batch rendering }
FDirtyRow[FCursorRow] := True;
Inc(FCursorCol); Inc(FCursorCol);
end; end;
end; end;
@ -1536,10 +1819,6 @@ begin
if not HandleAllocated then if not HandleAllocated then
Exit; Exit;
{ Destroy back buffer first so the old font is not selected in any }
{ DC when CreatePaintFont deletes it. }
DestroyBackBuffer;
{ Recreate the OEM charset paint font from current Font properties } { Recreate the OEM charset paint font from current Font properties }
CreatePaintFont; CreatePaintFont;
@ -1565,8 +1844,9 @@ begin
Width := FCols * FCellWidth + GetSystemMetrics(sm_CxVScroll); Width := FCols * FCellWidth + GetSystemMetrics(sm_CxVScroll);
Height := FRows * FCellHeight; Height := FRows * FCellHeight;
{ (Re)create the GDI back buffer at the new size } { (Re)create dual bitmaps; mark all rows dirty for next render }
CreateBackBuffer; CreateBuffers;
FAllDirty := True;
{ Start cursor blink timer } { Start cursor blink timer }
if not FTimerActive then if not FTimerActive then
@ -1639,7 +1919,9 @@ begin
if Value <> FCursorVisible then if Value <> FCursorVisible then
begin begin
FCursorVisible := Value; FCursorVisible := Value;
Invalidate; { Mark cursor row dirty so BitBlt erases/redraws cursor overlay }
FDirtyRow[FCursorRow] := True;
FlipToScreen;
end; end;
end; end;
@ -1702,7 +1984,7 @@ end;
procedure TKPAnsi.WMEraseBkgnd(var Msg: TWMEraseBkgnd); procedure TKPAnsi.WMEraseBkgnd(var Msg: TWMEraseBkgnd);
begin begin
{ Suppress background erase -- double-buffered Paint covers everything } { Suppress background erase -- TextOut with OPAQUE covers everything }
Msg.Result := 1; Msg.Result := 1;
end; end;
@ -1714,20 +1996,44 @@ end;
procedure TKPAnsi.WMTimer(var Msg: TWMTimer); procedure TKPAnsi.WMTimer(var Msg: TWMTimer);
var
DC: HDC;
Line: PTermLine;
X: Integer;
Y: Integer;
begin begin
FBlinkOn := not FBlinkOn; FBlinkOn := not FBlinkOn;
{ Text blink runs at half the cursor rate (every other tick) so } { Toggle text blink phase by swapping which buffer is displayed. }
{ blinking text toggles roughly once per second. } { Buffers are already up-to-date; just BitBlt the other one. }
Inc(FBlinkCount); Inc(FBlinkPhase);
if FBlinkCount >= 2 then if FBlinkPhase > 1 then
FBlinkPhase := 0;
if not HandleAllocated then
Exit;
if FBufDC[0] = 0 then
Exit;
DC := GetDC(Handle);
BitBlt(DC, 0, 0, FBufW, FBufH,
FBufDC[FBlinkPhase], 0, 0, SRCCOPY);
{ Cursor overlay }
if FCursorVisible and FBlinkOn and (FScrollPos = 0) and
(FCursorRow >= 0) and (FCursorRow < FRows) and
(FCursorRow < FScreen.Count) and
(FCursorCol >= 0) and (FCursorCol < FCols) then
begin begin
FBlinkCount := 0; Line := FScreen[FCursorRow];
FTextBlinkOn := not FTextBlinkOn; X := FCursorCol * FCellWidth;
Y := FCursorRow * FCellHeight;
SetTextColor(DC, ColorToRGB(Line^.Cells[FCursorCol].BG));
SetBkColor(DC, ColorToRGB(Line^.Cells[FCursorCol].FG));
WinProcs.TextOut(DC, X, Y, @Line^.Cells[FCursorCol].Ch, 1);
end; end;
{ Always repaint: cursor blink and text blink both need it } ReleaseDC(Handle, DC);
Invalidate;
end; end;
@ -1767,7 +2073,7 @@ begin
begin begin
FScrollPos := NewPos; FScrollPos := NewPos;
SetScrollPos(Handle, sb_Vert, SbkCount - FScrollPos, True); SetScrollPos(Handle, sb_Vert, SbkCount - FScrollPos, True);
Invalidate; FlipToScreen;
end; end;
end; end;

View file

@ -351,7 +351,6 @@ end;
function TKPComm.GetInput: string; function TKPComm.GetInput: string;
var var
Stat: TComStat;
BytesToRead: Integer; BytesToRead: Integer;
BytesRead: Integer; BytesRead: Integer;
Buf: array[0..255] of Char; Buf: array[0..255] of Char;
@ -360,15 +359,12 @@ begin
if not FPortOpen or (FCommId < 0) then if not FPortOpen or (FCommId < 0) then
Exit; Exit;
GetCommError(FCommId, Stat); { Read directly without querying GetCommError first. ReadComm }
BytesToRead := Stat.cbInQue; { returns the number of bytes actually available (up to BytesToRead) }
{ so the extra GetCommError round-trip is unnecessary overhead. }
BytesToRead := 255;
if (FInputLen > 0) and (BytesToRead > FInputLen) then if (FInputLen > 0) and (BytesToRead > FInputLen) then
BytesToRead := FInputLen; BytesToRead := FInputLen;
if BytesToRead > 255 then
BytesToRead := 255;
if BytesToRead <= 0 then
Exit;
BytesRead := ReadComm(FCommId, @Buf, BytesToRead); BytesRead := ReadComm(FCommId, @Buf, BytesToRead);
if BytesRead <= 0 then if BytesRead <= 0 then
@ -558,13 +554,11 @@ end;
procedure TKPComm.ProcessReceiveNotify; procedure TKPComm.ProcessReceiveNotify;
var
Stat: TComStat;
begin begin
if FRThreshold <= 0 then if FRThreshold <= 0 then
Exit; Exit;
GetCommError(FCommId, Stat); { WM_COMMNOTIFY with CN_RECEIVE means data is available -- the driver }
if Integer(Stat.cbInQue) >= FRThreshold then { already checked the threshold. No need to call GetCommError here. }
DoCommEvent(comEvReceive); DoCommEvent(comEvReceive);
end; end;

View file

@ -85,9 +85,19 @@ begin
case FComm.CommEvent of case FComm.CommEvent of
comEvReceive: comEvReceive:
begin 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; S := FComm.Input;
if Length(S) > 0 then if Length(S) > 0 then
FAnsi.Write(S); FAnsi.Write(S);
until Length(S) = 0;
finally
FAnsi.EndUpdate;
end;
end; end;
end; end;
end; end;
@ -131,7 +141,7 @@ begin
FEditSettings.Left := 148; FEditSettings.Left := 148;
FEditSettings.Top := 8; FEditSettings.Top := 8;
FEditSettings.Width := 140; FEditSettings.Width := 140;
FEditSettings.Text := '9600,N,8,1'; FEditSettings.Text := '115200,N,8,1';
FBtnOpen := TButton.Create(Self); FBtnOpen := TButton.Create(Self);
FBtnOpen.Parent := Self; FBtnOpen.Parent := Self;
@ -164,6 +174,15 @@ begin
FAnsi.Left := 0; FAnsi.Left := 0;
FAnsi.Top := 38; FAnsi.Top := 38;
FAnsi.OnKeyData := AnsiKeyData; FAnsi.OnKeyData := AnsiKeyData;
{ Font diagnostic: write known CP437 box-drawing characters. }
{ If the OEM font is working, you should see: }
{ Line 1: single-line box top ┌───┐ }
{ Line 2: shade + full block ░▒▓█ }
{ Line 3: single-line box bottom └───┘ }
{ If you see accented letters (Ú Ä ¿ ° ± ² Û À Ù), the font is }
{ ANSI_CHARSET instead of OEM_CHARSET. }
FAnsi.Write(#$DA#$C4#$C4#$C4#$BF' '#$B0#$B1#$B2#$DB' '#$C0#$C4#$C4#$C4#$D9#13#10);
end; end;

View file

@ -259,6 +259,13 @@ void applyBaudRate(PortStateT *port, uint16_t baud)
base = port->baseAddr; base = port->baseAddr;
divisor = (uint16_t)(BAUD_DIVISOR_BASE / actualBaud); divisor = (uint16_t)(BAUD_DIVISOR_BASE / actualBaud);
// Guard: divisor 0 means the UART treats it as 65536, giving ~1.76 baud.
// This can happen when BuildCommDCB stores a raw truncated value for
// 115200 (e.g. 0xE101 = 57601) and a future rate exceeds 115200.
if (divisor == 0) {
divisor = 1;
}
// Set DLAB to access divisor latch // Set DLAB to access divisor latch
lcr = (uint8_t)_inp(base + UART_LCR); lcr = (uint8_t)_inp(base + UART_LCR);
_outp(base + UART_LCR, lcr | LCR_DLAB); _outp(base + UART_LCR, lcr | LCR_DLAB);
@ -315,6 +322,11 @@ void applyLineParams(PortStateT *port, uint8_t byteSize, uint8_t parity, uint8_t
break; break;
} }
dbgHex16("KPCOMM: applyLine byteSize", (uint16_t)byteSize);
dbgHex16("KPCOMM: applyLine parity", (uint16_t)parity);
dbgHex16("KPCOMM: applyLine stopBits", (uint16_t)stopBits);
dbgHex16("KPCOMM: applyLine LCR", (uint16_t)lcr);
_outp(base + UART_LCR, lcr); _outp(base + UART_LCR, lcr);
port->byteSize = byteSize; port->byteSize = byteSize;
@ -952,6 +964,8 @@ int16_t FAR PASCAL _export inicom(DCB FAR *dcb)
port->comDeb.baudRate = port->baudRate; port->comDeb.baudRate = port->baudRate;
port->comDeb.qInSize = port->rxSize; port->comDeb.qInSize = port->rxSize;
port->comDeb.qOutSize = port->txSize; port->comDeb.qOutSize = port->txSize;
port->comDeb.lcrShadow = (uint8_t)_inp(port->baseAddr + UART_LCR);
port->comDeb.mcrShadow = (uint8_t)_inp(port->baseAddr + UART_MCR);
// Enable receive and line status interrupts // Enable receive and line status interrupts
_outp(port->baseAddr + UART_IER, IER_RDA | IER_LSI | IER_MSI); _outp(port->baseAddr + UART_IER, IER_RDA | IER_LSI | IER_MSI);
@ -1101,7 +1115,9 @@ void primeTx(PortStateT *port)
// reactivateOpenCommPorts - Reactivate all ports after task switch (ordinal 18) // reactivateOpenCommPorts - Reactivate all ports after task switch (ordinal 18)
// //
// Called by Windows when switching back to this VM. // Called by Windows when switching back to this VM.
// Re-enables interrupts and restores MCR state. // Restores full UART state: baud rate, line params (LCR), MCR, FIFOs,
// and re-enables interrupts. A DOS fullscreen app or VM switch may
// have reprogrammed the UART, so we must restore everything.
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
void FAR PASCAL _export reactivateOpenCommPorts(void) void FAR PASCAL _export reactivateOpenCommPorts(void)
{ {
@ -1117,6 +1133,12 @@ void FAR PASCAL _export reactivateOpenCommPorts(void)
continue; continue;
} }
// Restore baud rate (sets DLAB, writes divisor, clears DLAB)
applyBaudRate(port, port->baudRate);
// Restore line parameters (word length, parity, stop bits)
applyLineParams(port, port->byteSize, port->parity, port->stopBits);
// Restore MCR (DTR, RTS, OUT2) // Restore MCR (DTR, RTS, OUT2)
mcr = MCR_OUT2; mcr = MCR_OUT2;
if (port->dtrState) { if (port->dtrState) {