From cb2018dff47a9e1b3f05b83a59cb1c10bef86d19 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Tue, 3 Mar 2026 16:53:18 -0600 Subject: [PATCH] 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 --- delphi/KPANSI.PAS | 231 +++++++++++++--------------------------------- 1 file changed, 63 insertions(+), 168 deletions(-) diff --git a/delphi/KPANSI.PAS b/delphi/KPANSI.PAS index c68336f..3a7d41f 100644 --- a/delphi/KPANSI.PAS +++ b/delphi/KPANSI.PAS @@ -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,13 +425,7 @@ begin Line^.Cells[I].Blink := False; end; end; - if FLiveDC <> 0 then - begin - FlushPendingScrolls; - RenderRow(FLiveDC, FCursorRow); - end - else - FDirtyRow[FCursorRow] := True; + 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; + for J := FCursorRow to FRows - 1 do + FDirtyRow[J] := True; end; @@ -523,10 +509,7 @@ end; procedure TKPAnsi.DoScrollDown; var - Line: PTermLine; - ScrollR: TRect; - ClipR: TRect; - UpdateR: TRect; + Line: PTermLine; begin if FScreen.Count < FRows then Exit; @@ -538,19 +521,7 @@ 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; + FAllDirty := True; end; @@ -638,34 +609,15 @@ 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 - FDirtyRow[I] := True; - 1: - for I := 0 to FCursorRow do - FDirtyRow[I] := True; - 2: - FAllDirty := True; - end; + 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; @@ -673,7 +625,6 @@ end; procedure TKPAnsi.EraseLine(Mode: Integer); var J: Integer; - R: TRect; Line: PTermLine; begin Line := FScreen[FCursorRow]; @@ -703,37 +654,7 @@ 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; + FDirtyRow[FCursorRow] := True; end; @@ -1067,17 +988,17 @@ end; procedure TKPAnsi.FlushPendingScrolls; var - ScrollR: TRect; - ClipR: TRect; - UpdateR: TRect; - Row: Integer; + ScrollR: TRect; + 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,13 +1167,7 @@ begin Line^.Cells[I].Blink := False; end; end; - if FLiveDC <> 0 then - begin - FlushPendingScrolls; - RenderRow(FLiveDC, FCursorRow); - end - else - FDirtyRow[FCursorRow] := True; + 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; + for J := FCursorRow to FRows - 1 do + FDirtyRow[J] := True; 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,29 +1395,7 @@ 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; + FDirtyRow[FCursorRow] := True; end else if Buf[I] = #27 then begin @@ -2289,22 +2172,34 @@ end; procedure TKPAnsi.WriteDeferredBuf(Buf: PChar; Len: Integer); +var + Row: Integer; begin - if Len > 0 then + 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 - if HandleAllocated and (FPaintFont <> 0) and (FScrollPos = 0) then + FLiveDC := GetDC(Handle); + SelectObject(FLiveDC, FPaintFont); + SetBkMode(FLiveDC, OPAQUE); + FlushPendingScrolls; + for Row := 0 to FRows - 1 do begin - FLiveDC := GetDC(Handle); - SelectObject(FLiveDC, FPaintFont); - SetBkMode(FLiveDC, OPAQUE); - end; - ParseDataBuf(Buf, Len); - if FLiveDC <> 0 then - begin - FlushPendingScrolls; - ReleaseDC(Handle, FLiveDC); - FLiveDC := 0; + 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;