Decouple rendering from data processing with timer-based display

Data parsing (ProcessChar) is now purely memory operations. EndUpdate
and ParseData set FRenderPending instead of calling FlipToScreen.
A 55ms timer (~18 Hz, matching Win16 tick resolution) drives rendering:
dirty rows are painted and BitBlt'd to screen only on timer ticks.
Blink toggles every 9 ticks (~500ms). This prevents high-throughput
data (door games, file transfers) from saturating the CPU with GDI
calls — between timer ticks, data just accumulates in cell buffers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Duensing 2026-02-26 21:28:14 -06:00
parent a96dbb3faa
commit c565be8489

View file

@ -62,6 +62,8 @@ type
FPaintFont: HFont; FPaintFont: HFont;
FStockFont: Boolean; FStockFont: Boolean;
FBlinkPhase: Integer; FBlinkPhase: Integer;
FBlinkCount: Integer;
FRenderPending: Boolean;
FUpdateCount: Integer; FUpdateCount: Integer;
FDirtyRow: array[0..255] of Boolean; FDirtyRow: array[0..255] of Boolean;
FAllDirty: Boolean; FAllDirty: Boolean;
@ -163,7 +165,10 @@ const
$00FFFFFF { 15 White (bright) } $00FFFFFF { 15 White (bright) }
); );
CursorBlinkMs = 500; { Timer fires at ~18 Hz (Win16 tick resolution is ~55 ms). }
{ Cursor and text blink toggle every 9 ticks (~500 ms). }
RenderTickMs = 55;
BlinkInterval = 9;
{ OUT_RASTER_PRECIS may not be defined in Delphi 1.0 WinTypes } { OUT_RASTER_PRECIS may not be defined in Delphi 1.0 WinTypes }
OutRasterPrecis = 6; OutRasterPrecis = 6;
@ -334,6 +339,8 @@ begin
FPaintFont := 0; FPaintFont := 0;
FStockFont := False; FStockFont := False;
FBlinkPhase := 0; FBlinkPhase := 0;
FBlinkCount := 0;
FRenderPending := False;
FUpdateCount := 0; FUpdateCount := 0;
FAllDirty := True; FAllDirty := True;
FBufDC[0] := 0; FBufDC[0] := 0;
@ -875,7 +882,7 @@ begin
if FUpdateCount <= 0 then if FUpdateCount <= 0 then
begin begin
FUpdateCount := 0; FUpdateCount := 0;
FlipToScreen; FRenderPending := True;
end; end;
end; end;
@ -1539,7 +1546,7 @@ begin
FBlinkOn := True; FBlinkOn := True;
if FUpdateCount = 0 then if FUpdateCount = 0 then
FlipToScreen; FRenderPending := True;
end; end;
@ -1827,10 +1834,10 @@ begin
CreateBuffers; CreateBuffers;
FAllDirty := True; FAllDirty := True;
{ Start cursor blink timer } { Start render/blink timer }
if not FTimerActive then if not FTimerActive then
begin begin
SetTimer(Handle, 1, CursorBlinkMs, nil); SetTimer(Handle, 1, RenderTickMs, nil);
FTimerActive := True; FTimerActive := True;
end; end;
@ -1976,43 +1983,25 @@ end;
procedure TKPAnsi.WMTimer(var Msg: TWMTimer); procedure TKPAnsi.WMTimer(var Msg: TWMTimer);
var var
DC: HDC; NeedFlip: Boolean;
Line: PTermLine;
X: Integer;
Y: Integer;
begin begin
FBlinkOn := not FBlinkOn; NeedFlip := FRenderPending;
FRenderPending := False;
{ Toggle text blink phase by swapping which buffer is displayed. } { Blink counter: toggle cursor and text blink every BlinkInterval ticks }
{ Buffers are already up-to-date; just BitBlt the other one. } Inc(FBlinkCount);
if FBlinkCount >= BlinkInterval then
begin
FBlinkCount := 0;
FBlinkOn := not FBlinkOn;
Inc(FBlinkPhase); Inc(FBlinkPhase);
if FBlinkPhase > 1 then if FBlinkPhase > 1 then
FBlinkPhase := 0; FBlinkPhase := 0;
NeedFlip := True;
if not HandleAllocated then
Exit;
if FBufDC[0] = 0 then
Exit;
DC := GetDC(Handle);
BitBlt(DC, 0, 0, FBufW, FBufH,
FBufDC[FBlinkPhase], 0, 0, SRCCOPY);
{ Cursor overlay }
if FCursorVisible and FBlinkOn and (FScrollPos = 0) and
(FCursorRow >= 0) and (FCursorRow < FRows) and
(FCursorRow < FScreen.Count) and
(FCursorCol >= 0) and (FCursorCol < FCols) then
begin
Line := FScreen[FCursorRow];
X := FCursorCol * FCellWidth;
Y := FCursorRow * FCellHeight;
SetTextColor(DC, ColorToRGB(Line^.Cells[FCursorCol].BG));
SetBkColor(DC, ColorToRGB(Line^.Cells[FCursorCol].FG));
WinProcs.TextOut(DC, X, Y, @Line^.Cells[FCursorCol].Ch, 1);
end; end;
ReleaseDC(Handle, DC); if NeedFlip then
FlipToScreen;
end; end;