From fbf4ed7c404a124219b5535b97c5ee573d8e32d4 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Sun, 1 Mar 2026 19:28:56 -0600 Subject: [PATCH] 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 --- delphi/KPANSI.PAS | 34 ++++++++++++++++++++++++++++------ delphi/TESTMAIN.PAS | 16 ++++++++-------- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/delphi/KPANSI.PAS b/delphi/KPANSI.PAS index c3b4a02..d495e07 100644 --- a/delphi/KPANSI.PAS +++ b/delphi/KPANSI.PAS @@ -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 @@ -1220,8 +1221,9 @@ procedure TKPAnsi.FlipToScreen; { screen via SetDIBitsToDevice immediately after rendering. One GDI call } { per dirty row, zero for the pixel expansion itself. } var - DC: HDC; - Row: Integer; + 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 } { ----------------------------------------------------------------------- } diff --git a/delphi/TESTMAIN.PAS b/delphi/TESTMAIN.PAS index f3f4f70..77330b3 100644 --- a/delphi/TESTMAIN.PAS +++ b/delphi/TESTMAIN.PAS @@ -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; + S := FComm.Input; + if Length(S) > 0 then + 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;