diff --git a/delphi/KPANSI.PAS b/delphi/KPANSI.PAS index 5fbee4f..de16ac0 100644 --- a/delphi/KPANSI.PAS +++ b/delphi/KPANSI.PAS @@ -9,8 +9,9 @@ unit KPAnsi; { } { 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. } -{ This eliminates per-pixel branching and 32-bit arithmetic from the inner } -{ loop, with one SetDIBitsToDevice call per dirty row. } +{ Constant mini-frame values are hoisted outside the column loop to reduce } +{ 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. } @@ -68,13 +69,13 @@ type FRows: Integer; FScrollbackSize: Integer; FCursorVisible: Boolean; + FLastCursorRow: Integer; FOnKeyData: TKeyDataEvent; FPaintFont: HFont; FStockFont: Boolean; FBlinkCount: Integer; FUpdateCount: Integer; FPendingScroll: Integer; - FLastRenderTick: Longint; FDirtyRow: array[0..255] of Boolean; FAllDirty: Boolean; FTextBlinkOn: Boolean; @@ -96,6 +97,7 @@ type procedure DeleteLines(N: Integer); procedure DestroyRowBuffers; procedure DirtyAll; + procedure DirtyBlinkRows; procedure DirtyRow(Row: Integer); procedure DoScrollDown; procedure DoScrollUp; @@ -178,9 +180,9 @@ const $00FFFFFF { 15 White (bright) } ); - { Timer fires at ~18 Hz (Win16 tick resolution is ~55 ms). } - { Cursor and text blink toggle every 9 ticks (~500 ms). } - RenderTickMs = 55; + { Blink timer interval. Win16 minimum is ~55 ms (18.2 Hz tick). } + BlinkTimerMs = 55; + { Cursor and text blink toggle every 9 timer ticks (~500 ms). } BlinkInterval = 9; { OUT_RASTER_PRECIS may not be defined in Delphi 1.0 WinTypes } @@ -444,6 +446,7 @@ begin FScrollback := TList.Create; FCursorRow := 0; FCursorCol := 0; + FLastCursorRow := 0; FSaveCurRow := 0; FSaveCurCol := 0; FAttrFG := 7; @@ -465,7 +468,6 @@ begin FBlinkCount := 0; FUpdateCount := 0; FPendingScroll := 0; - FLastRenderTick := 0; FAllDirty := True; FTextBlinkOn := True; FRowBufSize := 0; @@ -667,6 +669,44 @@ begin 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); begin if (Row >= 0) and (Row <= 255) then @@ -730,22 +770,12 @@ end; procedure TKPAnsi.EndUpdate; -var - Now: Longint; begin Dec(FUpdateCount); if FUpdateCount <= 0 then begin 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; - end; + FlipToScreen; end; end; @@ -1233,6 +1263,16 @@ begin end; 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 } DC := GetDC(Handle); for Row := 0 to FRows - 1 do @@ -1486,14 +1526,11 @@ begin { Reset cursor blink to visible on new data } 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 - begin - if GetTickCount - FLastRenderTick >= RenderTickMs then - begin - FLastRenderTick := GetTickCount; - FlipToScreen; - end; - end; + FlipToScreen; end; @@ -1784,7 +1821,7 @@ begin { Start render/blink timer } if not FTimerActive then begin - SetTimer(Handle, 1, RenderTickMs, nil); + SetTimer(Handle, 1, BlinkTimerMs, nil); FTimerActive := True; end; @@ -1877,6 +1914,20 @@ begin FNibbleFG := 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 begin { Determine effective colors } @@ -1912,26 +1963,20 @@ begin PixOfs := Word(CellH - 1) * Stride + Word(Col) * 8; asm - { Push all values to explicit mini-frame BEFORE any register } - { 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 only per-cell values. Constants already on stack above. } push PixOfs push GlyphOfs push bp mov bp, sp - { Mini-frame layout (all accessed via SS:[BP+n]): } + { Mini-frame layout (same offsets as before): } { [bp] = saved original BP } - { [bp+2] = GlyphOfs } - { [bp+4] = PixOfs } - { [bp+6] = GlyphSeg } - { [bp+8] = PixSeg } - { [bp+10] = CellH } - { [bp+12] = Stride } + { [bp+2] = GlyphOfs (pushed this cell) } + { [bp+4] = PixOfs (pushed this cell) } + { [bp+6] = GlyphSeg (pushed once before loop) } + { [bp+8] = PixSeg (pushed once before loop) } + { [bp+10] = CellH (pushed once before loop) } + { [bp+12] = Stride (pushed once before loop) } push ds push bx @@ -1980,12 +2025,13 @@ begin pop bx pop ds pop bp - add sp, 12 { remove 6 mini-frame words } + add sp, 4 { remove per-cell GlyphOfs + PixOfs only } end; end; { 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. } + { Constants are still on the stack from above -- reused here. } if FCursorVisible and FBlinkOn and (FScrollPos = 0) and (Row = FCursorRow) and (FCursorCol >= 0) and (FCursorCol < FCols) then begin @@ -2014,10 +2060,6 @@ begin PixOfs := Word(CellH - 1) * Stride + Word(FCursorCol) * 8; asm - push Stride - push CellH - push PixSeg - push GlyphSeg push PixOfs push GlyphOfs @@ -2069,9 +2111,14 @@ begin pop bx pop ds pop bp - add sp, 12 + add sp, 4 end; end; + + { Remove constant mini-frame words pushed before the column loop } + asm + add sp, 8 + end; end; @@ -2219,14 +2266,11 @@ begin FBlinkCount := 0; FBlinkOn := not FBlinkOn; FTextBlinkOn := not FTextBlinkOn; - DirtyAll; + DirtyBlinkRows; end; - { Render pending dirty rows and/or update blink state. This also } - { catches any data that arrived just before a throttle boundary } - { in EndUpdate (worst-case latency: one timer tick, ~55 ms). } + { Render blink changes and any stale dirty rows } FlipToScreen; - FLastRenderTick := GetTickCount; end;