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 <noreply@anthropic.com>
This commit is contained in:
Scott Duensing 2026-03-02 17:15:35 -06:00
parent 64b3962c59
commit 8e3bad86e3
3 changed files with 91 additions and 33 deletions

View file

@ -143,6 +143,7 @@ type
procedure InsertChars(N: Integer); procedure InsertChars(N: Integer);
procedure InsertLines(N: Integer); procedure InsertLines(N: Integer);
procedure ParseData(const S: string); procedure ParseData(const S: string);
procedure ParseDataBuf(Buf: PChar; Len: Integer);
procedure ParseSGR; procedure ParseSGR;
procedure ProcessChar(Ch: Char); procedure ProcessChar(Ch: Char);
procedure RecalcCellSize; procedure RecalcCellSize;
@ -171,6 +172,7 @@ type
procedure TickBlink; procedure TickBlink;
procedure Write(const S: string); procedure Write(const S: string);
procedure WriteDeferred(const S: string); procedure WriteDeferred(const S: string);
procedure WriteDeferredBuf(Buf: PChar; Len: Integer);
property CursorCol: Integer read GetCursorCol; property CursorCol: Integer read GetCursorCol;
property CursorRow: Integer read GetCursorRow; property CursorRow: Integer read GetCursorRow;
published published
@ -1554,29 +1556,38 @@ end;
procedure TKPAnsi.ParseData(const S: string); procedure TKPAnsi.ParseData(const S: string);
{ Process incoming data with an inlined fast path for printable characters. } { String wrapper -- delegates to ParseDataBuf for actual processing. }
{ ~80% of BBS data is printable text in normal state. Inlining avoids the } begin
{ per-character method call to ProcessChar, and caching the Line pointer } if Length(S) > 0 then
{ eliminates repeated TList lookups for consecutive chars on the same row. } 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 } { Does NOT call FlipToScreen -- the caller handles rendering. }
{ after ParseData returns, ensuring immediate rendering. }
var var
I: Integer; I: Integer;
Ch: Char;
Line: PTermLine; Line: PTermLine;
FGIdx: Byte; FGIdx: Byte;
BGIdx: Byte; BGIdx: Byte;
RunEnd: Integer;
Remaining: Integer;
begin begin
Line := nil; Line := nil;
I := 0;
for I := 1 to Length(S) do while I < Len do
begin begin
Ch := S[I];
{ Fast path: printable character in normal state } { Fast path: printable character in normal state }
if (FParseState = psNormal) and (Ch >= ' ') then if (FParseState = psNormal) and (Buf[I] >= ' ') then
begin begin
{ Handle wrap at right margin }
if FCursorCol >= FCols then if FCursorCol >= FCols then
begin begin
if FWrapMode then if FWrapMode then
@ -1597,33 +1608,55 @@ begin
if Line = nil then if Line = nil then
Line := FScreen[FCursorRow]; Line := FScreen[FCursorRow];
{ Compute colors once for the entire run }
if FAttrBold then if FAttrBold then
FGIdx := FAttrFG + 8 FGIdx := FAttrFG + 8
else else
FGIdx := FAttrFG; FGIdx := FAttrFG;
BGIdx := FAttrBG; 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 if FAttrReverse then
begin begin
while I < RunEnd do
begin
Line^.Cells[FCursorCol].Ch := Buf[I];
Line^.Cells[FCursorCol].FG := BGIdx; Line^.Cells[FCursorCol].FG := BGIdx;
Line^.Cells[FCursorCol].BG := FGIdx; Line^.Cells[FCursorCol].BG := FGIdx;
Line^.Cells[FCursorCol].Bold := FAttrBold;
Line^.Cells[FCursorCol].Blink := FAttrBlink;
Inc(FCursorCol);
Inc(I);
end;
end end
else else
begin begin
while I < RunEnd do
begin
Line^.Cells[FCursorCol].Ch := Buf[I];
Line^.Cells[FCursorCol].FG := FGIdx; Line^.Cells[FCursorCol].FG := FGIdx;
Line^.Cells[FCursorCol].BG := BGIdx; Line^.Cells[FCursorCol].BG := BGIdx;
end;
Line^.Cells[FCursorCol].Ch := Ch;
Line^.Cells[FCursorCol].Bold := FAttrBold; Line^.Cells[FCursorCol].Bold := FAttrBold;
Line^.Cells[FCursorCol].Blink := FAttrBlink; Line^.Cells[FCursorCol].Blink := FAttrBlink;
FDirtyRow[FCursorRow] := True;
Inc(FCursorCol); Inc(FCursorCol);
Inc(I);
end;
end;
FDirtyRow[FCursorRow] := True;
end end
else else
begin begin
{ Slow path: control chars, escape sequences } { Slow path: control chars, escape sequences }
Line := nil; Line := nil;
ProcessChar(Ch); ProcessChar(Buf[I]);
Inc(I);
end; end;
end; end;
@ -1637,7 +1670,6 @@ begin
FScrollbarDirty := True; FScrollbarDirty := True;
FAllDirty := True; FAllDirty := True;
end; end;
end; end;
@ -2453,6 +2485,13 @@ begin
end; end;
procedure TKPAnsi.WriteDeferredBuf(Buf: PChar; Len: Integer);
begin
if Len > 0 then
ParseDataBuf(Buf, Len);
end;
{ ----------------------------------------------------------------------- } { ----------------------------------------------------------------------- }
{ Component registration } { Component registration }
{ ----------------------------------------------------------------------- } { ----------------------------------------------------------------------- }

View file

@ -68,6 +68,7 @@ type
public public
constructor Create(AOwner: TComponent); override; constructor Create(AOwner: TComponent); override;
destructor Destroy; override; destructor Destroy; override;
function ReadInputBuf(Buf: PChar; BufSize: Integer): Integer;
property Input: string read GetInput; property Input: string read GetInput;
property Output: string write SetOutput; property Output: string write SetOutput;
property InBufferCount: Integer read GetInBufferCount; property InBufferCount: Integer read GetInBufferCount;
@ -273,6 +274,22 @@ begin
end; 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; function TKPComm.GetOutBufferCount: Integer;
var var
Stat: TComStat; Stat: TComStat;

View file

@ -167,9 +167,11 @@ end;
procedure TMainForm.Run; procedure TMainForm.Run;
const const
RenderMs = 50; { Minimum ms between renders during bulk flow (20 fps) } RenderMs = 50; { Minimum ms between renders during bulk flow (20 fps) }
BufSize = 2048; { Read buffer -- 8x larger than 255-byte string limit }
var var
Msg: TMsg; Msg: TMsg;
S: string; Buf: array[0..BufSize - 1] of Char;
Len: Integer;
HasData: Boolean; HasData: Boolean;
Now: Longint; Now: Longint;
LastRenderTick: Longint; LastRenderTick: Longint;
@ -194,16 +196,16 @@ begin
if FDone then if FDone then
Break; Break;
{ Drain all available serial data before rendering. Batching } { Drain all available serial data before rendering. Reads up to }
{ many chunks into one render pass avoids per-chunk GDI overhead. } { 2048 bytes per call, bypassing the 255-byte short string limit. }
{ Messages are checked between chunks so keyboard stays responsive. } { Messages are checked between chunks so keyboard stays responsive. }
HasData := False; HasData := False;
if FComm.PortOpen then if FComm.PortOpen then
begin begin
S := FComm.Input; Len := FComm.ReadInputBuf(@Buf, BufSize);
while (Length(S) > 0) and not FDone do while (Len > 0) and not FDone do
begin begin
FAnsi.WriteDeferred(S); FAnsi.WriteDeferredBuf(@Buf, Len);
HasData := True; HasData := True;
{ Check for messages between chunks } { Check for messages between chunks }
while PeekMessage(Msg, 0, 0, 0, pm_Remove or pm_NoYield) do while PeekMessage(Msg, 0, 0, 0, pm_Remove or pm_NoYield) do
@ -216,7 +218,7 @@ begin
TranslateMessage(Msg); TranslateMessage(Msg);
DispatchMessage(Msg); DispatchMessage(Msg);
end; end;
S := FComm.Input; Len := FComm.ReadInputBuf(@Buf, BufSize);
end; end;
end; end;