Split parsing from rendering to avoid GDI/CPU cache thrashing

ParseDataBuf is now pure CPU work with no GDI calls -- the parsing
loop stays cache-hot in the 486 L1.  WriteDeferredBuf parses first,
then renders all dirty rows in one batch.  Removes ~105 lines of
inline FLiveDC rendering from 8 methods.  Also fixes missing DC
local variable in Paint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Duensing 2026-03-03 16:53:18 -06:00
parent 3fc2b410ba
commit cb2018dff4

View file

@ -7,11 +7,12 @@ unit KPAnsi;
{ Renders incoming data using standard ANSI/VT100 escape sequences for }
{ cursor positioning, color attributes, and screen manipulation. }
{ }
{ Immediate-mode rendering: each character run is rendered via ExtTextOut }
{ directly to the screen DC as it arrives during parsing. WriteDeferredBuf }
{ acquires a DC, parses data (rendering inline), and releases. No }
{ deferred dirty-row pass needed for normal data flow. FlipToScreen only }
{ handles blink toggle and fallback paths (scrollback view, WM_PAINT). }
{ Split-phase rendering: WriteDeferredBuf first parses the entire input }
{ buffer into the cell buffer (pure CPU, no GDI calls), then renders all }
{ dirty rows in one batch via RenderRow. Scroll-ups are coalesced into }
{ a single ScrollDC call. Separating parsing from rendering keeps the }
{ parsing loop cache-hot and avoids interleaving GDI kernel transitions }
{ with CPU work. FlipToScreen handles blink and fallback paths. }
{ }
{ Installs to the "KP" palette tab alongside TKPComm. }
@ -101,7 +102,7 @@ type
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 }
FLiveDC: HDC; { Non-zero during immediate rendering }
FLiveDC: HDC; { Non-zero during render pass in WriteDeferredBuf }
procedure AllocLine(Line: PTermLine);
procedure CMFontChanged(var Msg: TMessage); message cm_FontChanged;
@ -424,12 +425,6 @@ begin
Line^.Cells[I].Blink := False;
end;
end;
if FLiveDC <> 0 then
begin
FlushPendingScrolls;
RenderRow(FLiveDC, FCursorRow);
end
else
FDirtyRow[FCursorRow] := True;
end;
@ -454,17 +449,8 @@ begin
FScreen.Add(Line);
end;
end;
if FLiveDC <> 0 then
begin
FlushPendingScrolls;
for J := FCursorRow to FRows - 1 do
RenderRow(FLiveDC, J);
end
else
begin
for J := FCursorRow to FRows - 1 do
FDirtyRow[J] := True;
end;
end;
@ -524,9 +510,6 @@ end;
procedure TKPAnsi.DoScrollDown;
var
Line: PTermLine;
ScrollR: TRect;
ClipR: TRect;
UpdateR: TRect;
begin
if FScreen.Count < FRows then
Exit;
@ -538,18 +521,6 @@ begin
GetMem(Line, SizeOf(TTermLineRec));
AllocLine(Line);
FScreen.Insert(0, Line);
if FLiveDC <> 0 then
begin
FlushPendingScrolls;
ScrollR.Left := 0;
ScrollR.Top := 0;
ScrollR.Right := FCols * FCellWidth;
ScrollR.Bottom := FRows * FCellHeight;
ClipR := ScrollR;
ScrollDC(FLiveDC, 0, FCellHeight, ScrollR, ClipR, 0, @UpdateR);
RenderRow(FLiveDC, 0);
end
else
FAllDirty := True;
end;
@ -638,24 +609,6 @@ begin
UpdateScrollbar;
end;
end;
{ Immediate render or deferred dirty }
if FLiveDC <> 0 then
begin
FlushPendingScrolls;
case Mode of
0:
for I := FCursorRow to FRows - 1 do
RenderRow(FLiveDC, I);
1:
for I := 0 to FCursorRow do
RenderRow(FLiveDC, I);
2:
for I := 0 to FRows - 1 do
RenderRow(FLiveDC, I);
end;
end
else
begin
case Mode of
0:
for I := FCursorRow to FRows - 1 do
@ -666,14 +619,12 @@ begin
2:
FAllDirty := True;
end;
end;
end;
procedure TKPAnsi.EraseLine(Mode: Integer);
var
J: Integer;
R: TRect;
Line: PTermLine;
begin
Line := FScreen[FCursorRow];
@ -703,36 +654,6 @@ begin
2: { Erase entire line }
AllocLine(Line);
end;
if FLiveDC <> 0 then
begin
FlushPendingScrolls;
SetBkColor(FLiveDC, AnsiColors[0]);
case Mode of
0:
begin
R.Left := FCursorCol * FCellWidth;
R.Top := FCursorRow * FCellHeight;
R.Right := FCols * FCellWidth;
R.Bottom := R.Top + FCellHeight;
end;
1:
begin
R.Left := 0;
R.Top := FCursorRow * FCellHeight;
R.Right := (FCursorCol + 1) * FCellWidth;
R.Bottom := R.Top + FCellHeight;
end;
2:
begin
R.Left := 0;
R.Top := FCursorRow * FCellHeight;
R.Right := FCols * FCellWidth;
R.Bottom := R.Top + FCellHeight;
end;
end;
ExtTextOut(FLiveDC, R.Left, R.Top, ETO_OPAQUE, @R, nil, 0, nil);
end
else
FDirtyRow[FCursorRow] := True;
end;
@ -1071,13 +992,13 @@ var
ClipR: TRect;
UpdateR: TRect;
Row: Integer;
GhostRow: Integer;
begin
if (FPendingScrolls = 0) or (FLiveDC = 0) then
Exit;
if FPendingScrolls >= FRows then
begin
for Row := 0 to FRows - 1 do
RenderRow(FLiveDC, Row);
FAllDirty := True;
FPendingScrolls := 0;
Exit;
end;
@ -1088,8 +1009,13 @@ begin
ClipR := ScrollR;
ScrollDC(FLiveDC, 0, -(FPendingScrolls * FCellHeight),
ScrollR, ClipR, 0, @UpdateR);
{ Mark exposed rows and cursor ghost row for deferred rendering }
for Row := FRows - FPendingScrolls to FRows - 1 do
RenderRow(FLiveDC, Row);
FDirtyRow[Row] := True;
GhostRow := FLastCursorRow - FPendingScrolls;
if (GhostRow >= 0) and (GhostRow < FRows) then
FDirtyRow[GhostRow] := True;
FLastCursorRow := FCursorRow;
FPendingScrolls := 0;
end;
@ -1241,12 +1167,6 @@ begin
Line^.Cells[I].Blink := False;
end;
end;
if FLiveDC <> 0 then
begin
FlushPendingScrolls;
RenderRow(FLiveDC, FCursorRow);
end
else
FDirtyRow[FCursorRow] := True;
end;
@ -1271,17 +1191,8 @@ begin
AllocLine(Line);
FScreen.Insert(FCursorRow, Line);
end;
if FLiveDC <> 0 then
begin
FlushPendingScrolls;
for J := FCursorRow to FRows - 1 do
RenderRow(FLiveDC, J);
end
else
begin
for J := FCursorRow to FRows - 1 do
FDirtyRow[J] := True;
end;
end;
@ -1367,6 +1278,7 @@ end;
procedure TKPAnsi.Paint;
var
DC: HDC;
Row: Integer;
begin
if FPaintFont = 0 then
@ -1409,9 +1321,6 @@ var
BGIdx: Byte;
RunEnd: Integer;
Remaining: Integer;
RunStartI: Integer;
RunStartCol: Integer;
R: TRect;
begin
Line := nil;
I := 0;
@ -1458,10 +1367,6 @@ begin
(RunEnd - I < Remaining) do
Inc(RunEnd);
{ Save run start for immediate rendering }
RunStartI := I;
RunStartCol := FCursorCol;
{ Fill cells in tight loop }
if FAttrReverse then
begin
@ -1490,28 +1395,6 @@ begin
end;
end;
{ Immediate render or deferred dirty }
if FLiveDC <> 0 then
begin
FlushPendingScrolls;
if FAttrReverse then
begin
SetTextColor(FLiveDC, AnsiColors[BGIdx]);
SetBkColor(FLiveDC, AnsiColors[FGIdx]);
end
else
begin
SetTextColor(FLiveDC, AnsiColors[FGIdx]);
SetBkColor(FLiveDC, AnsiColors[BGIdx]);
end;
R.Left := RunStartCol * FCellWidth;
R.Top := FCursorRow * FCellHeight;
R.Right := FCursorCol * FCellWidth;
R.Bottom := R.Top + FCellHeight;
ExtTextOut(FLiveDC, R.Left, R.Top, ETO_OPAQUE, @R,
@Buf[RunStartI], I - RunStartI, nil);
end
else
FDirtyRow[FCursorRow] := True;
end
else if Buf[I] = #27 then
@ -2289,23 +2172,35 @@ end;
procedure TKPAnsi.WriteDeferredBuf(Buf: PChar; Len: Integer);
var
Row: Integer;
begin
if Len > 0 then
begin
if Len <= 0 then
Exit;
{ Parse into cell buffer -- pure CPU, no GDI calls }
ParseDataBuf(Buf, Len);
{ Render pass: flush coalesced scrolls, then redraw dirty rows }
if HandleAllocated and (FPaintFont <> 0) and (FScrollPos = 0) then
begin
FLiveDC := GetDC(Handle);
SelectObject(FLiveDC, FPaintFont);
SetBkMode(FLiveDC, OPAQUE);
end;
ParseDataBuf(Buf, Len);
if FLiveDC <> 0 then
begin
FlushPendingScrolls;
for Row := 0 to FRows - 1 do
begin
if FAllDirty or FDirtyRow[Row] then
begin
RenderRow(FLiveDC, Row);
FDirtyRow[Row] := False;
end;
end;
FAllDirty := False;
FLastCursorRow := FCursorRow;
ReleaseDC(Handle, FLiveDC);
FLiveDC := 0;
end;
end;
end;