Remove render throttle and add smart blink dirty tracking
Data now hits the screen immediately when it arrives — no artificial delay. The render throttle (RenderTickMs, FLastRenderTick) is removed entirely. ParseData and EndUpdate call FlipToScreen unconditionally; callers control batching via BeginUpdate/EndUpdate. Blink toggle no longer calls DirtyAll. New DirtyBlinkRows method only marks the cursor row and rows containing blink cells, reducing blink overhead from ~63ms (25 rows) to ~3ms (1-3 rows) on a 486. Cursor ghost handling in FlipToScreen dirties the old cursor row when the cursor moves between rows. Constant mini-frame values (Stride, CellH, PixSeg, GlyphSeg) are pushed once before the column loop instead of per-cell, saving 320 push instructions per row. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c5d31ca930
commit
b6f08a3150
1 changed files with 95 additions and 51 deletions
|
|
@ -9,8 +9,9 @@ unit KPAnsi;
|
||||||
{ }
|
{ }
|
||||||
{ Rendering uses a font atlas with a nibble lookup table and inline ASM }
|
{ Rendering uses a font atlas with a nibble lookup table and inline ASM }
|
||||||
{ to expand glyph bitmaps directly into a reusable 8bpp DIB pixel buffer. }
|
{ to expand glyph bitmaps directly into a reusable 8bpp DIB pixel buffer. }
|
||||||
{ This eliminates per-pixel branching and 32-bit arithmetic from the inner }
|
{ Constant mini-frame values are hoisted outside the column loop to reduce }
|
||||||
{ loop, with one SetDIBitsToDevice call per dirty row. }
|
{ per-cell overhead. Smart blink tracking dirties only cursor and blink }
|
||||||
|
{ rows instead of the entire screen, eliminating wasteful full repaints. }
|
||||||
{ }
|
{ }
|
||||||
{ Installs to the "KP" palette tab alongside TKPComm. }
|
{ Installs to the "KP" palette tab alongside TKPComm. }
|
||||||
|
|
||||||
|
|
@ -68,13 +69,13 @@ type
|
||||||
FRows: Integer;
|
FRows: Integer;
|
||||||
FScrollbackSize: Integer;
|
FScrollbackSize: Integer;
|
||||||
FCursorVisible: Boolean;
|
FCursorVisible: Boolean;
|
||||||
|
FLastCursorRow: Integer;
|
||||||
FOnKeyData: TKeyDataEvent;
|
FOnKeyData: TKeyDataEvent;
|
||||||
FPaintFont: HFont;
|
FPaintFont: HFont;
|
||||||
FStockFont: Boolean;
|
FStockFont: Boolean;
|
||||||
FBlinkCount: Integer;
|
FBlinkCount: Integer;
|
||||||
FUpdateCount: Integer;
|
FUpdateCount: Integer;
|
||||||
FPendingScroll: Integer;
|
FPendingScroll: Integer;
|
||||||
FLastRenderTick: Longint;
|
|
||||||
FDirtyRow: array[0..255] of Boolean;
|
FDirtyRow: array[0..255] of Boolean;
|
||||||
FAllDirty: Boolean;
|
FAllDirty: Boolean;
|
||||||
FTextBlinkOn: Boolean;
|
FTextBlinkOn: Boolean;
|
||||||
|
|
@ -96,6 +97,7 @@ type
|
||||||
procedure DeleteLines(N: Integer);
|
procedure DeleteLines(N: Integer);
|
||||||
procedure DestroyRowBuffers;
|
procedure DestroyRowBuffers;
|
||||||
procedure DirtyAll;
|
procedure DirtyAll;
|
||||||
|
procedure DirtyBlinkRows;
|
||||||
procedure DirtyRow(Row: Integer);
|
procedure DirtyRow(Row: Integer);
|
||||||
procedure DoScrollDown;
|
procedure DoScrollDown;
|
||||||
procedure DoScrollUp;
|
procedure DoScrollUp;
|
||||||
|
|
@ -178,9 +180,9 @@ const
|
||||||
$00FFFFFF { 15 White (bright) }
|
$00FFFFFF { 15 White (bright) }
|
||||||
);
|
);
|
||||||
|
|
||||||
{ Timer fires at ~18 Hz (Win16 tick resolution is ~55 ms). }
|
{ Blink timer interval. Win16 minimum is ~55 ms (18.2 Hz tick). }
|
||||||
{ Cursor and text blink toggle every 9 ticks (~500 ms). }
|
BlinkTimerMs = 55;
|
||||||
RenderTickMs = 55;
|
{ Cursor and text blink toggle every 9 timer ticks (~500 ms). }
|
||||||
BlinkInterval = 9;
|
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 }
|
||||||
|
|
@ -444,6 +446,7 @@ begin
|
||||||
FScrollback := TList.Create;
|
FScrollback := TList.Create;
|
||||||
FCursorRow := 0;
|
FCursorRow := 0;
|
||||||
FCursorCol := 0;
|
FCursorCol := 0;
|
||||||
|
FLastCursorRow := 0;
|
||||||
FSaveCurRow := 0;
|
FSaveCurRow := 0;
|
||||||
FSaveCurCol := 0;
|
FSaveCurCol := 0;
|
||||||
FAttrFG := 7;
|
FAttrFG := 7;
|
||||||
|
|
@ -465,7 +468,6 @@ begin
|
||||||
FBlinkCount := 0;
|
FBlinkCount := 0;
|
||||||
FUpdateCount := 0;
|
FUpdateCount := 0;
|
||||||
FPendingScroll := 0;
|
FPendingScroll := 0;
|
||||||
FLastRenderTick := 0;
|
|
||||||
FAllDirty := True;
|
FAllDirty := True;
|
||||||
FTextBlinkOn := True;
|
FTextBlinkOn := True;
|
||||||
FRowBufSize := 0;
|
FRowBufSize := 0;
|
||||||
|
|
@ -667,6 +669,44 @@ begin
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
procedure TKPAnsi.DirtyBlinkRows;
|
||||||
|
{ Targeted dirty marking for blink toggle. Instead of DirtyAll (which }
|
||||||
|
{ forces a full 25-row re-render and 25 SetDIBitsToDevice calls), only }
|
||||||
|
{ dirty the cursor row (cursor blink) and rows containing blink cells }
|
||||||
|
{ (text blink). Typical BBS content has 0-3 blink rows, so this reduces }
|
||||||
|
{ blink overhead from ~63ms to ~3ms on a 486. }
|
||||||
|
var
|
||||||
|
I: Integer;
|
||||||
|
J: Integer;
|
||||||
|
Line: PTermLine;
|
||||||
|
begin
|
||||||
|
{ In scrollback view, FlipToScreen sets FAllDirty anyway }
|
||||||
|
if FAllDirty or (FScrollPos <> 0) then
|
||||||
|
Exit;
|
||||||
|
|
||||||
|
{ Dirty cursor row for cursor blink }
|
||||||
|
if FCursorVisible and (FCursorRow >= 0) and (FCursorRow < FRows) then
|
||||||
|
FDirtyRow[FCursorRow] := True;
|
||||||
|
|
||||||
|
{ Dirty rows containing blink cells for text blink }
|
||||||
|
for I := 0 to FRows - 1 do
|
||||||
|
begin
|
||||||
|
if not FDirtyRow[I] and (I < FScreen.Count) then
|
||||||
|
begin
|
||||||
|
Line := FScreen[I];
|
||||||
|
for J := 0 to FCols - 1 do
|
||||||
|
begin
|
||||||
|
if Line^.Cells[J].Blink then
|
||||||
|
begin
|
||||||
|
FDirtyRow[I] := True;
|
||||||
|
Break;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
|
||||||
procedure TKPAnsi.DirtyRow(Row: Integer);
|
procedure TKPAnsi.DirtyRow(Row: Integer);
|
||||||
begin
|
begin
|
||||||
if (Row >= 0) and (Row <= 255) then
|
if (Row >= 0) and (Row <= 255) then
|
||||||
|
|
@ -730,24 +770,14 @@ end;
|
||||||
|
|
||||||
|
|
||||||
procedure TKPAnsi.EndUpdate;
|
procedure TKPAnsi.EndUpdate;
|
||||||
var
|
|
||||||
Now: Longint;
|
|
||||||
begin
|
begin
|
||||||
Dec(FUpdateCount);
|
Dec(FUpdateCount);
|
||||||
if FUpdateCount <= 0 then
|
if FUpdateCount <= 0 then
|
||||||
begin
|
begin
|
||||||
FUpdateCount := 0;
|
FUpdateCount := 0;
|
||||||
{ Render immediately if enough time has elapsed. This avoids }
|
|
||||||
{ depending on WM_TIMER, which is starved by continuous }
|
|
||||||
{ WM_COMMNOTIFY messages during high-speed data reception. }
|
|
||||||
Now := GetTickCount;
|
|
||||||
if Now - FLastRenderTick >= RenderTickMs then
|
|
||||||
begin
|
|
||||||
FLastRenderTick := Now;
|
|
||||||
FlipToScreen;
|
FlipToScreen;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
|
||||||
|
|
||||||
|
|
||||||
procedure TKPAnsi.EraseDisplay(Mode: Integer);
|
procedure TKPAnsi.EraseDisplay(Mode: Integer);
|
||||||
|
|
@ -1233,6 +1263,16 @@ begin
|
||||||
end;
|
end;
|
||||||
FPendingScroll := 0;
|
FPendingScroll := 0;
|
||||||
|
|
||||||
|
{ Dirty old cursor row to erase ghost when cursor moved between rows }
|
||||||
|
if FCursorRow <> FLastCursorRow then
|
||||||
|
begin
|
||||||
|
if (FLastCursorRow >= 0) and (FLastCursorRow <= 255) then
|
||||||
|
FDirtyRow[FLastCursorRow] := True;
|
||||||
|
if (FCursorRow >= 0) and (FCursorRow <= 255) then
|
||||||
|
FDirtyRow[FCursorRow] := True;
|
||||||
|
FLastCursorRow := FCursorRow;
|
||||||
|
end;
|
||||||
|
|
||||||
{ Interleaved render + blast: single buffer is reused per row }
|
{ Interleaved render + blast: single buffer is reused per row }
|
||||||
DC := GetDC(Handle);
|
DC := GetDC(Handle);
|
||||||
for Row := 0 to FRows - 1 do
|
for Row := 0 to FRows - 1 do
|
||||||
|
|
@ -1486,15 +1526,12 @@ begin
|
||||||
{ Reset cursor blink to visible on new data }
|
{ Reset cursor blink to visible on new data }
|
||||||
FBlinkOn := True;
|
FBlinkOn := True;
|
||||||
|
|
||||||
|
{ Render immediately -- no throttle. Data hits the screen as soon }
|
||||||
|
{ as it arrives. BeginUpdate/EndUpdate suppresses intermediate }
|
||||||
|
{ renders when the caller is batching multiple Write calls. }
|
||||||
if FUpdateCount = 0 then
|
if FUpdateCount = 0 then
|
||||||
begin
|
|
||||||
if GetTickCount - FLastRenderTick >= RenderTickMs then
|
|
||||||
begin
|
|
||||||
FLastRenderTick := GetTickCount;
|
|
||||||
FlipToScreen;
|
FlipToScreen;
|
||||||
end;
|
end;
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
|
|
||||||
procedure TKPAnsi.ParseSGR;
|
procedure TKPAnsi.ParseSGR;
|
||||||
|
|
@ -1784,7 +1821,7 @@ begin
|
||||||
{ Start render/blink timer }
|
{ Start render/blink timer }
|
||||||
if not FTimerActive then
|
if not FTimerActive then
|
||||||
begin
|
begin
|
||||||
SetTimer(Handle, 1, RenderTickMs, nil);
|
SetTimer(Handle, 1, BlinkTimerMs, nil);
|
||||||
FTimerActive := True;
|
FTimerActive := True;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
@ -1877,6 +1914,20 @@ begin
|
||||||
FNibbleFG := 255;
|
FNibbleFG := 255;
|
||||||
FNibbleBG := 255;
|
FNibbleBG := 255;
|
||||||
|
|
||||||
|
{ Push constant mini-frame values ONCE before the column loop. }
|
||||||
|
{ These 4 values (Stride, CellH, PixSeg, GlyphSeg) don't change }
|
||||||
|
{ across cells. Only per-cell values (GlyphOfs, PixOfs) are pushed }
|
||||||
|
{ inside the loop. This saves 320 push instructions per row (4 pushes }
|
||||||
|
{ x 80 cells). SP is 8 bytes below Delphi's expectation until the }
|
||||||
|
{ matching ADD SP,8 at the end, but local variable access uses BP, }
|
||||||
|
{ not SP, so this is safe. }
|
||||||
|
asm
|
||||||
|
push Stride
|
||||||
|
push CellH
|
||||||
|
push PixSeg
|
||||||
|
push GlyphSeg
|
||||||
|
end;
|
||||||
|
|
||||||
for Col := 0 to FCols - 1 do
|
for Col := 0 to FCols - 1 do
|
||||||
begin
|
begin
|
||||||
{ Determine effective colors }
|
{ Determine effective colors }
|
||||||
|
|
@ -1912,26 +1963,20 @@ begin
|
||||||
PixOfs := Word(CellH - 1) * Stride + Word(Col) * 8;
|
PixOfs := Word(CellH - 1) * Stride + Word(Col) * 8;
|
||||||
|
|
||||||
asm
|
asm
|
||||||
{ Push all values to explicit mini-frame BEFORE any register }
|
{ Push only per-cell values. Constants already on stack above. }
|
||||||
{ clobber. BASM reads register variables from SI/DI correctly }
|
|
||||||
{ here since nothing has been overwritten yet. }
|
|
||||||
push Stride
|
|
||||||
push CellH
|
|
||||||
push PixSeg
|
|
||||||
push GlyphSeg
|
|
||||||
push PixOfs
|
push PixOfs
|
||||||
push GlyphOfs
|
push GlyphOfs
|
||||||
|
|
||||||
push bp
|
push bp
|
||||||
mov bp, sp
|
mov bp, sp
|
||||||
{ Mini-frame layout (all accessed via SS:[BP+n]): }
|
{ Mini-frame layout (same offsets as before): }
|
||||||
{ [bp] = saved original BP }
|
{ [bp] = saved original BP }
|
||||||
{ [bp+2] = GlyphOfs }
|
{ [bp+2] = GlyphOfs (pushed this cell) }
|
||||||
{ [bp+4] = PixOfs }
|
{ [bp+4] = PixOfs (pushed this cell) }
|
||||||
{ [bp+6] = GlyphSeg }
|
{ [bp+6] = GlyphSeg (pushed once before loop) }
|
||||||
{ [bp+8] = PixSeg }
|
{ [bp+8] = PixSeg (pushed once before loop) }
|
||||||
{ [bp+10] = CellH }
|
{ [bp+10] = CellH (pushed once before loop) }
|
||||||
{ [bp+12] = Stride }
|
{ [bp+12] = Stride (pushed once before loop) }
|
||||||
|
|
||||||
push ds
|
push ds
|
||||||
push bx
|
push bx
|
||||||
|
|
@ -1980,12 +2025,13 @@ begin
|
||||||
pop bx
|
pop bx
|
||||||
pop ds
|
pop ds
|
||||||
pop bp
|
pop bp
|
||||||
add sp, 12 { remove 6 mini-frame words }
|
add sp, 4 { remove per-cell GlyphOfs + PixOfs only }
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ Cursor overlay: if cursor is on this row and visible, re-render the }
|
{ Cursor overlay: if cursor is on this row and visible, re-render the }
|
||||||
{ cursor cell with swapped FG/BG using the same ASM inner loop. }
|
{ cursor cell with swapped FG/BG using the same ASM inner loop. }
|
||||||
|
{ Constants are still on the stack from above -- reused here. }
|
||||||
if FCursorVisible and FBlinkOn and (FScrollPos = 0) and
|
if FCursorVisible and FBlinkOn and (FScrollPos = 0) and
|
||||||
(Row = FCursorRow) and (FCursorCol >= 0) and (FCursorCol < FCols) then
|
(Row = FCursorRow) and (FCursorCol >= 0) and (FCursorCol < FCols) then
|
||||||
begin
|
begin
|
||||||
|
|
@ -2014,10 +2060,6 @@ begin
|
||||||
PixOfs := Word(CellH - 1) * Stride + Word(FCursorCol) * 8;
|
PixOfs := Word(CellH - 1) * Stride + Word(FCursorCol) * 8;
|
||||||
|
|
||||||
asm
|
asm
|
||||||
push Stride
|
|
||||||
push CellH
|
|
||||||
push PixSeg
|
|
||||||
push GlyphSeg
|
|
||||||
push PixOfs
|
push PixOfs
|
||||||
push GlyphOfs
|
push GlyphOfs
|
||||||
|
|
||||||
|
|
@ -2069,9 +2111,14 @@ begin
|
||||||
pop bx
|
pop bx
|
||||||
pop ds
|
pop ds
|
||||||
pop bp
|
pop bp
|
||||||
add sp, 12
|
add sp, 4
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
{ Remove constant mini-frame words pushed before the column loop }
|
||||||
|
asm
|
||||||
|
add sp, 8
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2219,14 +2266,11 @@ begin
|
||||||
FBlinkCount := 0;
|
FBlinkCount := 0;
|
||||||
FBlinkOn := not FBlinkOn;
|
FBlinkOn := not FBlinkOn;
|
||||||
FTextBlinkOn := not FTextBlinkOn;
|
FTextBlinkOn := not FTextBlinkOn;
|
||||||
DirtyAll;
|
DirtyBlinkRows;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ Render pending dirty rows and/or update blink state. This also }
|
{ Render blink changes and any stale dirty rows }
|
||||||
{ catches any data that arrived just before a throttle boundary }
|
|
||||||
{ in EndUpdate (worst-case latency: one timer tick, ~55 ms). }
|
|
||||||
FlipToScreen;
|
FlipToScreen;
|
||||||
FLastRenderTick := GetTickCount;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue