Fix input lag: add PM_NOYIELD, skip idle GetDC, eliminate redundant renders

PeekMessage without PM_NOYIELD surrenders the timeslice on every empty
queue check (~55ms per yield in Win16).  Adding pm_NoYield keeps the
polling loop hot so keystrokes and serial echoes are processed without
scheduler delays.

FlipToScreen was calling GetDC/ReleaseDC on every loop iteration even
with zero dirty rows.  Added early-exit scan before acquiring a DC.

TickBlink was calling FlipToScreen redundantly (main loop also calls it).
Removed the FlipToScreen from TickBlink and reordered the main loop to
TickBlink (dirty only) then FlipToScreen (single render pass).

Also: removed FBlinkOn := True reset from ParseData (was dirtying the
cursor row on every incoming chunk), added WriteDeferred for parse-only
without render, moved FlipToScreen from private to public, added Show
call before entering the polling loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Duensing 2026-03-01 19:28:56 -06:00
parent ec0ec8f074
commit fbf4ed7c40
2 changed files with 36 additions and 14 deletions

View file

@ -134,7 +134,6 @@ type
procedure EraseLine(Mode: Integer);
procedure ExecuteCSI(FinalCh: Char);
procedure ExecuteMusic;
procedure FlipToScreen;
procedure FreeLineList(List: TList);
function GetCursorCol: Integer;
function GetCursorRow: Integer;
@ -166,8 +165,10 @@ type
destructor Destroy; override;
procedure Clear;
procedure Reset;
procedure FlipToScreen;
procedure TickBlink;
procedure Write(const S: string);
procedure WriteDeferred(const S: string);
property CursorCol: Integer read GetCursorCol;
property CursorRow: Integer read GetCursorRow;
published
@ -1222,6 +1223,7 @@ procedure TKPAnsi.FlipToScreen;
var
DC: HDC;
Row: Integer;
HasDirty: Boolean;
begin
if not HandleAllocated then
Exit;
@ -1251,6 +1253,22 @@ begin
FLastCursorRow := FCursorRow;
end;
{ Early exit: skip GetDC/ReleaseDC when nothing needs rendering }
if not FAllDirty then
begin
HasDirty := False;
for Row := 0 to FRows - 1 do
begin
if FDirtyRow[Row] then
begin
HasDirty := True;
Break;
end;
end;
if not HasDirty then
Exit;
end;
{ Interleaved render + blast: single buffer is reused per row }
DC := GetDC(Handle);
for Row := 0 to FRows - 1 do
@ -1570,8 +1588,6 @@ begin
FAllDirty := True;
end;
{ Reset cursor blink to visible on new data }
FBlinkOn := True;
end;
@ -2315,7 +2331,6 @@ begin
FBlinkOn := not FBlinkOn;
FTextBlinkOn := not FTextBlinkOn;
DirtyBlinkRows;
FlipToScreen;
end;
end;
@ -2371,6 +2386,13 @@ begin
end;
procedure TKPAnsi.WriteDeferred(const S: string);
begin
if Length(S) > 0 then
ParseData(S);
end;
{ ----------------------------------------------------------------------- }
{ Component registration }
{ ----------------------------------------------------------------------- }

View file

@ -169,11 +169,12 @@ var
Msg: TMsg;
S: string;
begin
Show;
FDone := False;
while not FDone do
begin
{ Process all pending Windows messages (keyboard, paint, scrollbar) }
while PeekMessage(Msg, 0, 0, 0, pm_Remove) do
while PeekMessage(Msg, 0, 0, 0, pm_Remove or pm_NoYield) do
begin
if Msg.message = wm_Quit then
begin
@ -187,18 +188,17 @@ begin
if FDone then
Break;
{ Poll serial data directly -- no WM_COMMNOTIFY, no events }
{ Poll serial data -- read one chunk, then yield to messages }
if FComm.PortOpen then
begin
repeat
S := FComm.Input;
if Length(S) > 0 then
FAnsi.Write(S);
until Length(S) = 0;
FAnsi.WriteDeferred(S);
end;
{ Tick blink state (uses GetTickCount, no WM_TIMER) }
{ Tick blink (dirties rows if interval elapsed), then render }
FAnsi.TickBlink;
FAnsi.FlipToScreen;
end;
end;