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:
parent
64b3962c59
commit
8e3bad86e3
3 changed files with 91 additions and 33 deletions
|
|
@ -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 }
|
||||||
{ ----------------------------------------------------------------------- }
|
{ ----------------------------------------------------------------------- }
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue