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 }
|
{ ANSI escape sequence parser state }
|
||||||
FParseState: TParseState; { Current parser state machine position }
|
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) }
|
FMusicStr: string; { Accumulated ANSI music string (ESC[M..^N) }
|
||||||
|
|
||||||
{ Font metrics (measured from OEM charset paint font) }
|
{ Font metrics (measured from OEM charset paint font) }
|
||||||
|
|
@ -77,6 +78,7 @@ type
|
||||||
|
|
||||||
{ Scrollback view }
|
{ Scrollback view }
|
||||||
FScrollPos: Integer; { Lines scrolled back (0=live, >0=viewing history) }
|
FScrollPos: Integer; { Lines scrolled back (0=live, >0=viewing history) }
|
||||||
|
FPendingScrolls: Integer; { Scroll-up count pending for ScrollDC coalescing }
|
||||||
|
|
||||||
{ Terminal modes }
|
{ Terminal modes }
|
||||||
FWrapMode: Boolean; { Auto-wrap at right margin (DEC ?7h/l) }
|
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;
|
procedure ParseParamBuf(Buf: PChar; Len: Integer;
|
||||||
var Count: Integer);
|
var Params: array of Integer; var Count: Integer);
|
||||||
var
|
var
|
||||||
I: Integer;
|
I: Integer;
|
||||||
Start: Integer;
|
Value: Integer;
|
||||||
Token: string;
|
InNum: Boolean;
|
||||||
begin
|
begin
|
||||||
Count := 0;
|
Count := 0;
|
||||||
if Length(S) = 0 then
|
Value := 0;
|
||||||
Exit;
|
InNum := False;
|
||||||
Start := 1;
|
for I := 0 to Len - 1 do
|
||||||
for I := 1 to Length(S) do
|
|
||||||
begin
|
begin
|
||||||
if S[I] = ';' then
|
if Buf[I] = ';' then
|
||||||
begin
|
begin
|
||||||
if Count <= High(Params) then
|
if Count <= High(Params) then
|
||||||
begin
|
begin
|
||||||
Token := Copy(S, Start, I - Start);
|
if InNum then
|
||||||
if Length(Token) > 0 then
|
Params[Count] := Value
|
||||||
Params[Count] := StrToIntDef(Token, 0)
|
|
||||||
else
|
else
|
||||||
Params[Count] := 0;
|
Params[Count] := 0;
|
||||||
Inc(Count);
|
Inc(Count);
|
||||||
end;
|
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;
|
||||||
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
|
if Count <= High(Params) then
|
||||||
begin
|
begin
|
||||||
Token := Copy(S, Start, Length(S) - Start + 1);
|
if InNum then
|
||||||
if Length(Token) > 0 then
|
Params[Count] := Value
|
||||||
Params[Count] := StrToIntDef(Token, 0)
|
|
||||||
else
|
else
|
||||||
Params[Count] := 0;
|
Params[Count] := 0;
|
||||||
Inc(Count);
|
Inc(Count);
|
||||||
|
|
@ -475,13 +481,14 @@ begin
|
||||||
FAttrBlink := False;
|
FAttrBlink := False;
|
||||||
FAttrReverse := False;
|
FAttrReverse := False;
|
||||||
FParseState := psNormal;
|
FParseState := psNormal;
|
||||||
FParamStr := '';
|
FParamLen := 0;
|
||||||
FMusicStr := '';
|
FMusicStr := '';
|
||||||
FCellWidth := 8;
|
FCellWidth := 8;
|
||||||
FCellHeight := 16;
|
FCellHeight := 16;
|
||||||
FBlinkOn := True;
|
FBlinkOn := True;
|
||||||
FLastBlinkTick := GetTickCount;
|
FLastBlinkTick := GetTickCount;
|
||||||
FScrollPos := 0;
|
FScrollPos := 0;
|
||||||
|
FPendingScrolls := 0;
|
||||||
FWrapMode := True;
|
FWrapMode := True;
|
||||||
FPaintFont := 0;
|
FPaintFont := 0;
|
||||||
FStockFont := False;
|
FStockFont := False;
|
||||||
|
|
@ -763,9 +770,10 @@ begin
|
||||||
FScreen.Add(Line);
|
FScreen.Add(Line);
|
||||||
FScrollbarDirty := True;
|
FScrollbarDirty := True;
|
||||||
|
|
||||||
{ Without ScrollDC, all rows must be re-rendered after a scroll }
|
{ Track pending scrolls for coalesced ScrollDC in FlipToScreen. }
|
||||||
{ because the on-screen pixels haven't moved to match FScreen. }
|
{ Multiple scrolls during one ParseData call collapse into a single }
|
||||||
FAllDirty := True;
|
{ ScrollDC call, then only the newly exposed bottom rows are rendered.}
|
||||||
|
Inc(FPendingScrolls);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -888,7 +896,7 @@ var
|
||||||
P1: Integer;
|
P1: Integer;
|
||||||
P2: Integer;
|
P2: Integer;
|
||||||
begin
|
begin
|
||||||
ParseParams(FParamStr, Params, Count);
|
ParseParamBuf(@FParamBuf[0], FParamLen, Params, Count);
|
||||||
|
|
||||||
if Count > 0 then
|
if Count > 0 then
|
||||||
P1 := Params[0]
|
P1 := Params[0]
|
||||||
|
|
@ -1219,11 +1227,17 @@ end;
|
||||||
procedure TKPAnsi.FlipToScreen;
|
procedure TKPAnsi.FlipToScreen;
|
||||||
{ Render dirty rows into the shared 8bpp DIB buffer, blasting each to the }
|
{ Render dirty rows into the shared 8bpp DIB buffer, blasting each to the }
|
||||||
{ screen via SetDIBitsToDevice immediately after rendering. One GDI call }
|
{ 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
|
var
|
||||||
DC: HDC;
|
DC: HDC;
|
||||||
Row: Integer;
|
Row: Integer;
|
||||||
HasDirty: Boolean;
|
GhostRow: Integer;
|
||||||
|
HasDirty: Boolean;
|
||||||
|
ScrollR: TRect;
|
||||||
|
ClipR: TRect;
|
||||||
|
UpdateR: TRect;
|
||||||
begin
|
begin
|
||||||
if not HandleAllocated then
|
if not HandleAllocated then
|
||||||
Exit;
|
Exit;
|
||||||
|
|
@ -1234,7 +1248,10 @@ begin
|
||||||
|
|
||||||
{ Scrollback view: force full redraw (line mapping changes) }
|
{ Scrollback view: force full redraw (line mapping changes) }
|
||||||
if FScrollPos <> 0 then
|
if FScrollPos <> 0 then
|
||||||
|
begin
|
||||||
FAllDirty := True;
|
FAllDirty := True;
|
||||||
|
FPendingScrolls := 0;
|
||||||
|
end;
|
||||||
|
|
||||||
{ Deferred scrollbar update (batched from DoScrollUp) }
|
{ Deferred scrollbar update (batched from DoScrollUp) }
|
||||||
if FScrollbarDirty then
|
if FScrollbarDirty then
|
||||||
|
|
@ -1243,6 +1260,39 @@ begin
|
||||||
FScrollbarDirty := False;
|
FScrollbarDirty := False;
|
||||||
end;
|
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 }
|
{ Dirty old cursor row to erase ghost when cursor moved between rows }
|
||||||
if FCursorRow <> FLastCursorRow then
|
if FCursorRow <> FLastCursorRow then
|
||||||
begin
|
begin
|
||||||
|
|
@ -1598,7 +1648,7 @@ var
|
||||||
I: Integer;
|
I: Integer;
|
||||||
Code: Integer;
|
Code: Integer;
|
||||||
begin
|
begin
|
||||||
ParseParams(FParamStr, Params, Count);
|
ParseParamBuf(@FParamBuf[0], FParamLen, Params, Count);
|
||||||
|
|
||||||
{ SGR with no parameters means reset }
|
{ SGR with no parameters means reset }
|
||||||
if Count = 0 then
|
if Count = 0 then
|
||||||
|
|
@ -1746,7 +1796,7 @@ begin
|
||||||
case Ch of
|
case Ch of
|
||||||
'[':
|
'[':
|
||||||
begin
|
begin
|
||||||
FParamStr := '';
|
FParamLen := 0;
|
||||||
FParseState := psCSI;
|
FParseState := psCSI;
|
||||||
end;
|
end;
|
||||||
else
|
else
|
||||||
|
|
@ -1762,7 +1812,11 @@ begin
|
||||||
case Ch of
|
case Ch of
|
||||||
'0'..'9', ';':
|
'0'..'9', ';':
|
||||||
begin
|
begin
|
||||||
FParamStr := FParamStr + Ch;
|
if FParamLen < 32 then
|
||||||
|
begin
|
||||||
|
FParamBuf[FParamLen] := Ch;
|
||||||
|
Inc(FParamLen);
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
'?':
|
'?':
|
||||||
begin
|
begin
|
||||||
|
|
@ -1771,7 +1825,7 @@ begin
|
||||||
'M':
|
'M':
|
||||||
begin
|
begin
|
||||||
{ Check if this is ANSI music: ESC[M starts music mode }
|
{ Check if this is ANSI music: ESC[M starts music mode }
|
||||||
if Length(FParamStr) = 0 then
|
if FParamLen = 0 then
|
||||||
begin
|
begin
|
||||||
FMusicStr := '';
|
FMusicStr := '';
|
||||||
FParseState := psMusic;
|
FParseState := psMusic;
|
||||||
|
|
@ -1796,20 +1850,26 @@ begin
|
||||||
begin
|
begin
|
||||||
case Ch of
|
case Ch of
|
||||||
'0'..'9', ';':
|
'0'..'9', ';':
|
||||||
FParamStr := FParamStr + Ch;
|
begin
|
||||||
|
if FParamLen < 32 then
|
||||||
|
begin
|
||||||
|
FParamBuf[FParamLen] := Ch;
|
||||||
|
Inc(FParamLen);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
'h': { Set Mode }
|
'h': { Set Mode }
|
||||||
begin
|
begin
|
||||||
if FParamStr = '7' then
|
if (FParamLen = 1) and (FParamBuf[0] = '7') then
|
||||||
FWrapMode := True
|
FWrapMode := True
|
||||||
else if FParamStr = '25' then
|
else if (FParamLen = 2) and (FParamBuf[0] = '2') and (FParamBuf[1] = '5') then
|
||||||
FCursorVisible := True;
|
FCursorVisible := True;
|
||||||
FParseState := psNormal;
|
FParseState := psNormal;
|
||||||
end;
|
end;
|
||||||
'l': { Reset Mode }
|
'l': { Reset Mode }
|
||||||
begin
|
begin
|
||||||
if FParamStr = '7' then
|
if (FParamLen = 1) and (FParamBuf[0] = '7') then
|
||||||
FWrapMode := False
|
FWrapMode := False
|
||||||
else if FParamStr = '25' then
|
else if (FParamLen = 2) and (FParamBuf[0] = '2') and (FParamBuf[1] = '5') then
|
||||||
FCursorVisible := False;
|
FCursorVisible := False;
|
||||||
FParseState := psNormal;
|
FParseState := psNormal;
|
||||||
end;
|
end;
|
||||||
|
|
@ -2180,7 +2240,7 @@ begin
|
||||||
FAttrBlink := False;
|
FAttrBlink := False;
|
||||||
FAttrReverse := False;
|
FAttrReverse := False;
|
||||||
FParseState := psNormal;
|
FParseState := psNormal;
|
||||||
FParamStr := '';
|
FParamLen := 0;
|
||||||
FMusicStr := '';
|
FMusicStr := '';
|
||||||
FWrapMode := True;
|
FWrapMode := True;
|
||||||
FSaveCurRow := 0;
|
FSaveCurRow := 0;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue