diff --git a/delphi/KPANSI.PAS b/delphi/KPANSI.PAS index d495e07..a527bbf 100644 --- a/delphi/KPANSI.PAS +++ b/delphi/KPANSI.PAS @@ -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;