Eliminate heap allocs in ANSI parser, add ScrollDC for scroll rendering
Replace FParamStr string with fixed char buffer (FParamBuf/FParamLen) to eliminate per-character heap allocations during CSI escape sequence parsing. Replace ParseParams (Copy + StrToIntDef per token) with ParseParamBuf that parses integers directly from the char buffer with zero allocations. Replace FAllDirty in DoScrollUp with FPendingScrolls counter. In FlipToScreen, coalesce pending scrolls into a single ScrollDC call that shifts on-screen pixels, then only render the newly exposed bottom rows. Reduces per-scroll GDI cost from 25 SetDIBitsToDevice calls to 1-3. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
02e01848d6
commit
75d598c4c5
1 changed files with 98 additions and 38 deletions
|
|
@ -64,7 +64,8 @@ type
|
|||
|
||||
{ ANSI escape sequence parser state }
|
||||
FParseState: TParseState; { Current parser state machine position }
|
||||
FParamStr: string; { Accumulated CSI parameter digits/semicolons }
|
||||
FParamBuf: array[0..31] of Char; { CSI parameter digits/semicolons }
|
||||
FParamLen: Integer; { Current length of FParamBuf }
|
||||
FMusicStr: string; { Accumulated ANSI music string (ESC[M..^N) }
|
||||
|
||||
{ Font metrics (measured from OEM charset paint font) }
|
||||
|
|
@ -77,6 +78,7 @@ type
|
|||
|
||||
{ Scrollback view }
|
||||
FScrollPos: Integer; { Lines scrolled back (0=live, >0=viewing history) }
|
||||
FPendingScrolls: Integer; { Scroll-up count pending for ScrollDC coalescing }
|
||||
|
||||
{ Terminal modes }
|
||||
FWrapMode: Boolean; { Auto-wrap at right margin (DEC ?7h/l) }
|
||||
|
|
@ -227,42 +229,46 @@ type
|
|||
|
||||
|
||||
{ ----------------------------------------------------------------------- }
|
||||
{ Helper: parse semicolon-delimited parameter string into integer array }
|
||||
{ Helper: parse semicolon-delimited parameters from char buffer }
|
||||
{ Zero-allocation: parses integers directly without Copy or StrToIntDef. }
|
||||
{ ----------------------------------------------------------------------- }
|
||||
|
||||
procedure ParseParams(const S: string; var Params: array of Integer;
|
||||
var Count: Integer);
|
||||
procedure ParseParamBuf(Buf: PChar; Len: Integer;
|
||||
var Params: array of Integer; var Count: Integer);
|
||||
var
|
||||
I: Integer;
|
||||
Start: Integer;
|
||||
Token: string;
|
||||
Value: Integer;
|
||||
InNum: Boolean;
|
||||
begin
|
||||
Count := 0;
|
||||
if Length(S) = 0 then
|
||||
Exit;
|
||||
Start := 1;
|
||||
for I := 1 to Length(S) do
|
||||
Value := 0;
|
||||
InNum := False;
|
||||
for I := 0 to Len - 1 do
|
||||
begin
|
||||
if S[I] = ';' then
|
||||
if Buf[I] = ';' then
|
||||
begin
|
||||
if Count <= High(Params) then
|
||||
begin
|
||||
Token := Copy(S, Start, I - Start);
|
||||
if Length(Token) > 0 then
|
||||
Params[Count] := StrToIntDef(Token, 0)
|
||||
if InNum then
|
||||
Params[Count] := Value
|
||||
else
|
||||
Params[Count] := 0;
|
||||
Inc(Count);
|
||||
end;
|
||||
Start := I + 1;
|
||||
Value := 0;
|
||||
InNum := False;
|
||||
end
|
||||
else if (Buf[I] >= '0') and (Buf[I] <= '9') then
|
||||
begin
|
||||
Value := Value * 10 + (Ord(Buf[I]) - Ord('0'));
|
||||
InNum := True;
|
||||
end;
|
||||
end;
|
||||
{ Last token after final semicolon (or entire string if no semicolons) }
|
||||
{ Last value after final semicolon (or entire buffer if no semicolons) }
|
||||
if Count <= High(Params) then
|
||||
begin
|
||||
Token := Copy(S, Start, Length(S) - Start + 1);
|
||||
if Length(Token) > 0 then
|
||||
Params[Count] := StrToIntDef(Token, 0)
|
||||
if InNum then
|
||||
Params[Count] := Value
|
||||
else
|
||||
Params[Count] := 0;
|
||||
Inc(Count);
|
||||
|
|
@ -475,13 +481,14 @@ begin
|
|||
FAttrBlink := False;
|
||||
FAttrReverse := False;
|
||||
FParseState := psNormal;
|
||||
FParamStr := '';
|
||||
FParamLen := 0;
|
||||
FMusicStr := '';
|
||||
FCellWidth := 8;
|
||||
FCellHeight := 16;
|
||||
FBlinkOn := True;
|
||||
FLastBlinkTick := GetTickCount;
|
||||
FScrollPos := 0;
|
||||
FPendingScrolls := 0;
|
||||
FWrapMode := True;
|
||||
FPaintFont := 0;
|
||||
FStockFont := False;
|
||||
|
|
@ -763,9 +770,10 @@ begin
|
|||
FScreen.Add(Line);
|
||||
FScrollbarDirty := True;
|
||||
|
||||
{ Without ScrollDC, all rows must be re-rendered after a scroll }
|
||||
{ because the on-screen pixels haven't moved to match FScreen. }
|
||||
FAllDirty := True;
|
||||
{ Track pending scrolls for coalesced ScrollDC in FlipToScreen. }
|
||||
{ Multiple scrolls during one ParseData call collapse into a single }
|
||||
{ ScrollDC call, then only the newly exposed bottom rows are rendered.}
|
||||
Inc(FPendingScrolls);
|
||||
end;
|
||||
|
||||
|
||||
|
|
@ -888,7 +896,7 @@ var
|
|||
P1: Integer;
|
||||
P2: Integer;
|
||||
begin
|
||||
ParseParams(FParamStr, Params, Count);
|
||||
ParseParamBuf(@FParamBuf[0], FParamLen, Params, Count);
|
||||
|
||||
if Count > 0 then
|
||||
P1 := Params[0]
|
||||
|
|
@ -1219,11 +1227,17 @@ end;
|
|||
procedure TKPAnsi.FlipToScreen;
|
||||
{ Render dirty rows into the shared 8bpp DIB buffer, blasting each to the }
|
||||
{ screen via SetDIBitsToDevice immediately after rendering. One GDI call }
|
||||
{ per dirty row, zero for the pixel expansion itself. }
|
||||
{ per dirty row, zero for the pixel expansion itself. Coalesced ScrollDC }
|
||||
{ shifts on-screen pixels to match FScreen after scrolling, reducing the }
|
||||
{ per-scroll GDI cost from 25 rows to just the newly exposed rows. }
|
||||
var
|
||||
DC: HDC;
|
||||
Row: Integer;
|
||||
HasDirty: Boolean;
|
||||
DC: HDC;
|
||||
Row: Integer;
|
||||
GhostRow: Integer;
|
||||
HasDirty: Boolean;
|
||||
ScrollR: TRect;
|
||||
ClipR: TRect;
|
||||
UpdateR: TRect;
|
||||
begin
|
||||
if not HandleAllocated then
|
||||
Exit;
|
||||
|
|
@ -1234,7 +1248,10 @@ begin
|
|||
|
||||
{ Scrollback view: force full redraw (line mapping changes) }
|
||||
if FScrollPos <> 0 then
|
||||
begin
|
||||
FAllDirty := True;
|
||||
FPendingScrolls := 0;
|
||||
end;
|
||||
|
||||
{ Deferred scrollbar update (batched from DoScrollUp) }
|
||||
if FScrollbarDirty then
|
||||
|
|
@ -1243,6 +1260,39 @@ begin
|
|||
FScrollbarDirty := False;
|
||||
end;
|
||||
|
||||
{ Coalesced ScrollDC: shift on-screen pixels to match FScreen after }
|
||||
{ one or more DoScrollUp calls, then only render the newly exposed rows. }
|
||||
if (FPendingScrolls > 0) and not FAllDirty then
|
||||
begin
|
||||
if FPendingScrolls < FRows then
|
||||
begin
|
||||
ScrollR.Left := 0;
|
||||
ScrollR.Top := 0;
|
||||
ScrollR.Right := FCols * FCellWidth;
|
||||
ScrollR.Bottom := FRows * FCellHeight;
|
||||
ClipR := ScrollR;
|
||||
DC := GetDC(Handle);
|
||||
ScrollDC(DC, 0, -(FPendingScrolls * FCellHeight),
|
||||
ScrollR, ClipR, 0, @UpdateR);
|
||||
ReleaseDC(Handle, DC);
|
||||
{ Dirty the newly exposed bottom rows }
|
||||
for Row := FRows - FPendingScrolls to FRows - 1 do
|
||||
begin
|
||||
if Row >= 0 then
|
||||
FDirtyRow[Row] := True;
|
||||
end;
|
||||
{ Dirty row that now shows cursor ghost from pre-scroll pixels }
|
||||
GhostRow := FCursorRow - FPendingScrolls;
|
||||
if (GhostRow >= 0) and (GhostRow < FRows) then
|
||||
FDirtyRow[GhostRow] := True;
|
||||
{ Sync FLastCursorRow since pixel positions shifted }
|
||||
FLastCursorRow := FCursorRow;
|
||||
end
|
||||
else
|
||||
FAllDirty := True;
|
||||
FPendingScrolls := 0;
|
||||
end;
|
||||
|
||||
{ Dirty old cursor row to erase ghost when cursor moved between rows }
|
||||
if FCursorRow <> FLastCursorRow then
|
||||
begin
|
||||
|
|
@ -1598,7 +1648,7 @@ var
|
|||
I: Integer;
|
||||
Code: Integer;
|
||||
begin
|
||||
ParseParams(FParamStr, Params, Count);
|
||||
ParseParamBuf(@FParamBuf[0], FParamLen, Params, Count);
|
||||
|
||||
{ SGR with no parameters means reset }
|
||||
if Count = 0 then
|
||||
|
|
@ -1746,7 +1796,7 @@ begin
|
|||
case Ch of
|
||||
'[':
|
||||
begin
|
||||
FParamStr := '';
|
||||
FParamLen := 0;
|
||||
FParseState := psCSI;
|
||||
end;
|
||||
else
|
||||
|
|
@ -1762,7 +1812,11 @@ begin
|
|||
case Ch of
|
||||
'0'..'9', ';':
|
||||
begin
|
||||
FParamStr := FParamStr + Ch;
|
||||
if FParamLen < 32 then
|
||||
begin
|
||||
FParamBuf[FParamLen] := Ch;
|
||||
Inc(FParamLen);
|
||||
end;
|
||||
end;
|
||||
'?':
|
||||
begin
|
||||
|
|
@ -1771,7 +1825,7 @@ begin
|
|||
'M':
|
||||
begin
|
||||
{ Check if this is ANSI music: ESC[M starts music mode }
|
||||
if Length(FParamStr) = 0 then
|
||||
if FParamLen = 0 then
|
||||
begin
|
||||
FMusicStr := '';
|
||||
FParseState := psMusic;
|
||||
|
|
@ -1796,20 +1850,26 @@ begin
|
|||
begin
|
||||
case Ch of
|
||||
'0'..'9', ';':
|
||||
FParamStr := FParamStr + Ch;
|
||||
begin
|
||||
if FParamLen < 32 then
|
||||
begin
|
||||
FParamBuf[FParamLen] := Ch;
|
||||
Inc(FParamLen);
|
||||
end;
|
||||
end;
|
||||
'h': { Set Mode }
|
||||
begin
|
||||
if FParamStr = '7' then
|
||||
if (FParamLen = 1) and (FParamBuf[0] = '7') then
|
||||
FWrapMode := True
|
||||
else if FParamStr = '25' then
|
||||
else if (FParamLen = 2) and (FParamBuf[0] = '2') and (FParamBuf[1] = '5') then
|
||||
FCursorVisible := True;
|
||||
FParseState := psNormal;
|
||||
end;
|
||||
'l': { Reset Mode }
|
||||
begin
|
||||
if FParamStr = '7' then
|
||||
if (FParamLen = 1) and (FParamBuf[0] = '7') then
|
||||
FWrapMode := False
|
||||
else if FParamStr = '25' then
|
||||
else if (FParamLen = 2) and (FParamBuf[0] = '2') and (FParamBuf[1] = '5') then
|
||||
FCursorVisible := False;
|
||||
FParseState := psNormal;
|
||||
end;
|
||||
|
|
@ -2180,7 +2240,7 @@ begin
|
|||
FAttrBlink := False;
|
||||
FAttrReverse := False;
|
||||
FParseState := psNormal;
|
||||
FParamStr := '';
|
||||
FParamLen := 0;
|
||||
FMusicStr := '';
|
||||
FWrapMode := True;
|
||||
FSaveCurRow := 0;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue