From 8e3bad86e3f54e7ac54729b91ebf5b61a6f53b9b Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 2 Mar 2026 17:15:35 -0600 Subject: [PATCH] Bypass 255-byte string limit and batch plain text runs in parser Add ReadInputBuf to TKPComm for direct PChar reads up to 2048 bytes, eliminating short string allocation and 8x fewer ReadComm API calls. Add ParseDataBuf to TKPAnsi with run batching: scans ahead for printable text runs, computes colors once per run, fills cells in tight loop without per-character state/wrap checks. Co-Authored-By: Claude Opus 4.6 --- delphi/KPANSI.PAS | 89 ++++++++++++++++++++++++++++++++------------- delphi/KPCOMM.PAS | 17 +++++++++ delphi/TESTMAIN.PAS | 18 +++++---- 3 files changed, 91 insertions(+), 33 deletions(-) diff --git a/delphi/KPANSI.PAS b/delphi/KPANSI.PAS index a527bbf..7f4cde1 100644 --- a/delphi/KPANSI.PAS +++ b/delphi/KPANSI.PAS @@ -143,6 +143,7 @@ type procedure InsertChars(N: Integer); procedure InsertLines(N: Integer); procedure ParseData(const S: string); + procedure ParseDataBuf(Buf: PChar; Len: Integer); procedure ParseSGR; procedure ProcessChar(Ch: Char); procedure RecalcCellSize; @@ -171,6 +172,7 @@ type procedure TickBlink; procedure Write(const S: string); procedure WriteDeferred(const S: string); + procedure WriteDeferredBuf(Buf: PChar; Len: Integer); property CursorCol: Integer read GetCursorCol; property CursorRow: Integer read GetCursorRow; published @@ -1554,29 +1556,38 @@ end; procedure TKPAnsi.ParseData(const S: string); -{ Process incoming data with an inlined fast path for printable characters. } -{ ~80% of BBS data is printable text in normal state. Inlining avoids the } -{ per-character method call to ProcessChar, and caching the Line pointer } -{ eliminates repeated TList lookups for consecutive chars on the same row. } +{ String wrapper -- delegates to ParseDataBuf for actual processing. } +begin + if Length(S) > 0 then + ParseDataBuf(@S[1], Length(S)); +end; + + +procedure TKPAnsi.ParseDataBuf(Buf: PChar; Len: Integer); +{ Process incoming data from a PChar buffer (no string allocation needed). } +{ Fast path batches runs of printable characters: colors are computed once } +{ per run, and cells are filled in a tight loop without per-character state } +{ checks. Run length is bounded by end of input, end of current row, or } +{ next non-printable character -- whichever comes first. } { } -{ Does NOT call FlipToScreen -- the caller (Write) calls FlipToScreen } -{ after ParseData returns, ensuring immediate rendering. } +{ Does NOT call FlipToScreen -- the caller handles rendering. } var - I: Integer; - Ch: Char; - Line: PTermLine; - FGIdx: Byte; - BGIdx: Byte; + I: Integer; + Line: PTermLine; + FGIdx: Byte; + BGIdx: Byte; + RunEnd: Integer; + Remaining: Integer; begin Line := nil; + I := 0; - for I := 1 to Length(S) do + while I < Len do begin - Ch := S[I]; - { Fast path: printable character in normal state } - if (FParseState = psNormal) and (Ch >= ' ') then + if (FParseState = psNormal) and (Buf[I] >= ' ') then begin + { Handle wrap at right margin } if FCursorCol >= FCols then begin if FWrapMode then @@ -1597,33 +1608,55 @@ begin if Line = nil then Line := FScreen[FCursorRow]; + { Compute colors once for the entire run } if FAttrBold then FGIdx := FAttrFG + 8 else FGIdx := FAttrFG; BGIdx := FAttrBG; + { Find run end: stop at control char, end of input, or end of row } + Remaining := FCols - FCursorCol; + RunEnd := I; + while (RunEnd < Len) and (Buf[RunEnd] >= ' ') and + (RunEnd - I < Remaining) do + Inc(RunEnd); + + { Fill cells in tight loop -- no per-character state/wrap checks } if FAttrReverse then begin - Line^.Cells[FCursorCol].FG := BGIdx; - Line^.Cells[FCursorCol].BG := FGIdx; + while I < RunEnd do + begin + Line^.Cells[FCursorCol].Ch := Buf[I]; + Line^.Cells[FCursorCol].FG := BGIdx; + Line^.Cells[FCursorCol].BG := FGIdx; + Line^.Cells[FCursorCol].Bold := FAttrBold; + Line^.Cells[FCursorCol].Blink := FAttrBlink; + Inc(FCursorCol); + Inc(I); + end; end else begin - Line^.Cells[FCursorCol].FG := FGIdx; - Line^.Cells[FCursorCol].BG := BGIdx; + while I < RunEnd do + begin + Line^.Cells[FCursorCol].Ch := Buf[I]; + Line^.Cells[FCursorCol].FG := FGIdx; + Line^.Cells[FCursorCol].BG := BGIdx; + Line^.Cells[FCursorCol].Bold := FAttrBold; + Line^.Cells[FCursorCol].Blink := FAttrBlink; + Inc(FCursorCol); + Inc(I); + end; end; - Line^.Cells[FCursorCol].Ch := Ch; - Line^.Cells[FCursorCol].Bold := FAttrBold; - Line^.Cells[FCursorCol].Blink := FAttrBlink; FDirtyRow[FCursorRow] := True; - Inc(FCursorCol); end else begin { Slow path: control chars, escape sequences } Line := nil; - ProcessChar(Ch); + ProcessChar(Buf[I]); + Inc(I); end; end; @@ -1637,7 +1670,6 @@ begin FScrollbarDirty := True; FAllDirty := True; end; - end; @@ -2453,6 +2485,13 @@ begin end; +procedure TKPAnsi.WriteDeferredBuf(Buf: PChar; Len: Integer); +begin + if Len > 0 then + ParseDataBuf(Buf, Len); +end; + + { ----------------------------------------------------------------------- } { Component registration } { ----------------------------------------------------------------------- } diff --git a/delphi/KPCOMM.PAS b/delphi/KPCOMM.PAS index b76f178..f02821c 100644 --- a/delphi/KPCOMM.PAS +++ b/delphi/KPCOMM.PAS @@ -68,6 +68,7 @@ type public constructor Create(AOwner: TComponent); override; destructor Destroy; override; + function ReadInputBuf(Buf: PChar; BufSize: Integer): Integer; property Input: string read GetInput; property Output: string write SetOutput; property InBufferCount: Integer read GetInBufferCount; @@ -273,6 +274,22 @@ begin end; +function TKPComm.ReadInputBuf(Buf: PChar; BufSize: Integer): Integer; +{ Read up to BufSize bytes into a caller-supplied buffer. Bypasses the } +{ 255-byte short string limit of the Input property, allowing the drain } +{ loop to read 2048+ bytes per call and reducing ReadComm/API overhead. } +var + BytesRead: Integer; +begin + Result := 0; + if not FPortOpen or (FCommId < 0) then + Exit; + BytesRead := ReadComm(FCommId, Buf, BufSize); + if BytesRead > 0 then + Result := BytesRead; +end; + + function TKPComm.GetOutBufferCount: Integer; var Stat: TComStat; diff --git a/delphi/TESTMAIN.PAS b/delphi/TESTMAIN.PAS index 65e6ecb..fa472cc 100644 --- a/delphi/TESTMAIN.PAS +++ b/delphi/TESTMAIN.PAS @@ -167,9 +167,11 @@ end; procedure TMainForm.Run; const RenderMs = 50; { Minimum ms between renders during bulk flow (20 fps) } + BufSize = 2048; { Read buffer -- 8x larger than 255-byte string limit } var Msg: TMsg; - S: string; + Buf: array[0..BufSize - 1] of Char; + Len: Integer; HasData: Boolean; Now: Longint; LastRenderTick: Longint; @@ -194,16 +196,16 @@ begin if FDone then Break; - { Drain all available serial data before rendering. Batching } - { many chunks into one render pass avoids per-chunk GDI overhead. } - { Messages are checked between chunks so keyboard stays responsive.} + { Drain all available serial data before rendering. Reads up to } + { 2048 bytes per call, bypassing the 255-byte short string limit. } + { Messages are checked between chunks so keyboard stays responsive. } HasData := False; if FComm.PortOpen then begin - S := FComm.Input; - while (Length(S) > 0) and not FDone do + Len := FComm.ReadInputBuf(@Buf, BufSize); + while (Len > 0) and not FDone do begin - FAnsi.WriteDeferred(S); + FAnsi.WriteDeferredBuf(@Buf, Len); HasData := True; { Check for messages between chunks } while PeekMessage(Msg, 0, 0, 0, pm_Remove or pm_NoYield) do @@ -216,7 +218,7 @@ begin TranslateMessage(Msg); DispatchMessage(Msg); end; - S := FComm.Input; + Len := FComm.ReadInputBuf(@Buf, BufSize); end; end;