From 55d5bf01cf718620f419cdd80280262d40f93573 Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Fri, 29 May 2026 14:21:40 +0300 Subject: [PATCH] Optimize CPU waterfall/spectrum rendering WaterfallView: drop per-frame TLazIntfImage + LoadFromIntfImage (device bitmap recreation + full pixel-format conversion every frame). Write pixels directly into the bitmap via BeginUpdate/ScanLine like SpectrumView already does, with platform-native byte order (ARGB on Cocoa, BGRA elsewhere). Replace per-pixel float colorization with a 512-entry dB->color LUT rebuilt only when WfLow/WfHigh/theme change beyond a small threshold. Turn the bitmap itself into a ring buffer: each frame builds a single new row and writes one ScanLine; scrolling is done at paint time via two CopyRect calls. Removes both full-frame passes per frame (image memmove + buffer->bitmap copy), cutting per-frame cost from O(W*H) to O(W). TX mode preserved by leaving out-of-span columns untouched in the row buf. SpectrumView: rewrite the per-frame alpha-restore pass from a strided per-byte loop to a sequential per-dword OR with the alpha mask (bit-identical output, sequential memory access). Co-Authored-By: Claude Opus 4.8 --- SpectrumView.pas | 21 ++++-- WaterfallView.pas | 188 ++++++++++++++++++++++++++++------------------ 2 files changed, 126 insertions(+), 83 deletions(-) diff --git a/SpectrumView.pas b/SpectrumView.pas index 0b79d14..4e0271a 100644 --- a/SpectrumView.pas +++ b/SpectrumView.pas @@ -722,7 +722,8 @@ procedure TSpectrumView.DrawSpectrum; var C, GC: TCanvas; i, Yp, W, H, GX: Integer; - AlphaPtr: PByte; + RowLW: PLongWord; + AMask: LongWord; DBmin, DBmax, dB: Double; VfoX, X1, X2: Integer; TXVfoX, TXX1, TXX2: Integer; @@ -930,18 +931,22 @@ begin if Assigned(FVfoOverlay) then FVfoOverlay.DrawOverlay(FSpectrumBitmap, W, H); + // Восстанавливаем непрозрачность: Canvas-операции (текст/линии со сглаживанием) + // могли занулить альфу. OR маской выставляет альфу в $FF, RGB не меняя. +{$IFDEF DARWIN} + AMask := $000000FF; // ARGB: альфа в байте 0 +{$ELSE} + AMask := $FF000000; // BGRA: альфа в байте 3 +{$ENDIF} FSpectrumBitmap.BeginUpdate(False); for i := 0 to H - 1 do begin -{$IFDEF DARWIN} - AlphaPtr := PByte(FSpectrumBitmap.ScanLine[i]); // ARGB: alpha at byte 0 -{$ELSE} - AlphaPtr := PByte(FSpectrumBitmap.ScanLine[i]) + 3; // BGRA: alpha at byte 3 -{$ENDIF} + RowLW := PLongWord(FSpectrumBitmap.ScanLine[i]); + if RowLW = nil then Continue; for Yp := 0 to W - 1 do begin - AlphaPtr^ := $FF; - Inc(AlphaPtr, 4); + RowLW^ := RowLW^ or AMask; + Inc(RowLW); end; end; FSpectrumBitmap.EndUpdate(False); diff --git a/WaterfallView.pas b/WaterfallView.pas index 37324c9..e204605 100644 --- a/WaterfallView.pas +++ b/WaterfallView.pas @@ -13,7 +13,7 @@ interface uses Classes, SysUtils, Graphics, ExtCtrls, Controls, Math, - IntfGraphics, FPImage, LCLIntf, LCLType, GraphType, AppTheme; + LCLIntf, LCLType, AppTheme; type TWaterfallView = class @@ -22,8 +22,12 @@ type FWfBitmap: TBitmap; FWfBitmapW: Integer; FWfBitmapH: Integer; - FWfIntfImg: TLazIntfImage; - FWfPixels: array of LongWord; + FWfRowBuf: array of LongWord; // одна строка (новейшая), нативный порядок + FWfHead: Integer; // физ. строка bitmap с новейшей линией + FWfLut: array[0..511] of LongWord; + FWfLutLow: Double; + FWfLutHigh: Double; + FWfLutLight: Boolean; FWaterfallBuf: array[0..1023] of Single; FWaterfallBufCount: Integer; FWaterfallDirty: Boolean; @@ -48,6 +52,7 @@ type FPbWaterfall: TControl; procedure DrawMarkerLine(C: TCanvas; W, H: Integer); + procedure BuildWfLut(WfLow, WfHigh: Double); public constructor Create; destructor Destroy; override; @@ -147,23 +152,22 @@ begin Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B; end; -procedure InitRawDesc32WF(var Desc: TRawImageDescription; AWidth, AHeight: Integer); +// Преобразует логический $AARRGGBB в нативный для ScanLine порядок байт. +// DARWIN (Cocoa): в памяти [A,R,G,B]; прочие (Qt6/Win32): [B,G,R,A]. +// Платформы little-endian, поэтому байт 0 — младший байт LongWord. +function NativePixel(ARGB: LongWord): LongWord; inline; +{$IFDEF DARWIN} begin - FillChar(Desc, SizeOf(Desc), 0); - Desc.Format := ricfRGBA; - Desc.Width := AWidth; - Desc.Height := AHeight; - Desc.Depth := 32; - Desc.BitOrder := riboBitsInOrder; - Desc.ByteOrder := riboLSBFirst; - Desc.LineOrder := riloTopToBottom; - Desc.BitsPerPixel := 32; - Desc.LineEnd := rileDWordBoundary; - Desc.BlueShift := 0; Desc.BluePrec := 8; - Desc.GreenShift := 8; Desc.GreenPrec := 8; - Desc.RedShift := 16; Desc.RedPrec := 8; - Desc.AlphaShift := 24; Desc.AlphaPrec := 8; + Result := (ARGB shr 24) // A → байт 0 + or (((ARGB shr 16) and $FF) shl 8) // R → байт 1 + or (((ARGB shr 8) and $FF) shl 16) // G → байт 2 + or ((ARGB and $FF) shl 24); // B → байт 3 end; +{$ELSE} +begin + Result := ARGB; // $AARRGGBB на LE уже даёт в памяти [B,G,R,A] +end; +{$ENDIF} // ════════════════════════════════════════════════════════════════════════════ // TWaterfallView @@ -174,7 +178,7 @@ begin inherited Create; FWaterfallBitmap := TBitmap.Create; FWfBitmap := TBitmap.Create; - FWfIntfImg := nil; + FWfBitmap.PixelFormat := pf32bit; FWfBitmapW := 0; FWfBitmapH := 0; FWfManualHigh := -80.0; @@ -191,6 +195,9 @@ begin FTheme := DarkTheme; FLightTheme := False; FSpanHz := 192000; + FWfLutLow := 1.0; // невалидно → LUT перестроится при первом кадре + FWfLutHigh := 0.0; + FWfLutLight := False; ResetWfBuf; end; @@ -198,7 +205,6 @@ destructor TWaterfallView.Destroy; begin FWaterfallBitmap.Free; FWfBitmap.Free; - FreeAndNil(FWfIntfImg); inherited; end; @@ -210,8 +216,10 @@ begin // Сброс размера — чтобы следующий кадр перезаполнил фон новой темой FWfBitmapW := 0; FWfBitmapH := 0; - SetLength(FWfPixels, 0); - FreeAndNil(FWfIntfImg); + FWfLutLow := 1.0; // невалидно → LUT перестроится + FWfLutHigh := 0.0; + SetLength(FWfRowBuf, 0); + FWfHead := 0; end; procedure TWaterfallView.ResetWfAvgBuf; @@ -249,8 +257,8 @@ begin FWaterfallBitmap.SetSize(W, H); FWfBitmapW := 0; FWfBitmapH := 0; - SetLength(FWfPixels, 0); - FreeAndNil(FWfIntfImg); + SetLength(FWfRowBuf, 0); + FWfHead := 0; end; procedure TWaterfallView.DrawMarkerLine(C: TCanvas; W, H: Integer); @@ -271,6 +279,30 @@ begin C.TextOut(MX - 4 - C.TextWidth(MarkerLbl), 4, MarkerLbl); end; +// Перестраивает палитру dB→цвет (512 точек) один раз на кадр вместо +// вычисления цвета с плавающей точкой на каждый пиксель. +procedure TWaterfallView.BuildWfLut(WfLow, WfHigh: Double); +var + i: Integer; + dB, Range: Double; + C: LongWord; +begin + Range := WfHigh - WfLow; + if Range < 1E-6 then Range := 1E-6; + for i := 0 to High(FWfLut) do + begin + dB := WfLow + (i / High(FWfLut)) * Range; + if FLightTheme then + C := WaterfallLightTheme(dB, WfLow, WfHigh) + else + C := WaterfallEnhancedColorThetis(dB, WfLow, WfHigh); + FWfLut[i] := NativePixel(C); + end; + FWfLutLow := WfLow; + FWfLutHigh := WfHigh; + FWfLutLight := FLightTheme; +end; + // ──────────────────────────────────────────────────────────────────────────── // DrawWaterfall // ──────────────────────────────────────────────────────────────────────────── @@ -284,10 +316,8 @@ var HistMinDB, HistMaxDB, HistStepDB: Double; NoiseFloorDB, SignalTopDB: Double; CumCount, LowTargetCount, HighTargetCount, HistIdx: Integer; - WfHigh, WfLow, InvRange, Step: Double; - Px: PLongWord; - Desc: TRawImageDescription; - Row, Col: Integer; RowPtr: PByte; Src: PLongWord; BPPi: Integer; + WfHigh, WfLow, LutScale, Step: Double; + Row, Idx: Integer; RowPtr: PByte; Pal: LongWord; Hist: array[0..191] of Integer; SrcCount: Integer; const ALPHA_HIGH = 0.10; ALPHA_LOW = 0.08; @@ -340,25 +370,44 @@ begin if WfHigh < WfLow + 40.0 then WfHigh := WfLow + 40.0; if WfHigh > 0.0 then WfHigh := 0.0; if WfLow < -160 then WfLow := -160; - InvRange := 255.0 / (WfHigh - WfLow); + LutScale := High(FWfLut) / (WfHigh - WfLow); + + // Палитра зависит только от WfLow/WfHigh/темы — перестраиваем лишь при + // ощутимом изменении (AGC сдвигает границы медленно). + if (FWfLutLight <> FLightTheme) or + (Abs(WfLow - FWfLutLow) > 0.05) or + (Abs(WfHigh - FWfLutHigh) > 0.05) then + BuildWfLut(WfLow, WfHigh); if (FWfBitmapW <> W) or (FWfBitmapH <> H) then begin FWfBitmapW := W; FWfBitmapH := H; - SetLength(FWfPixels, W * H); - Pal := $FF000000 + SetLength(FWfRowBuf, W); + Pal := NativePixel($FF000000 or ((LongWord(FTheme.Panel) and $FF) shl 16) or (LongWord(FTheme.Panel) and $FF00) - or ((LongWord(FTheme.Panel) shr 16) and $FF); - FillDWord(FWfPixels[0], W * H, Pal); + or ((LongWord(FTheme.Panel) shr 16) and $FF)); + FillDWord(FWfRowBuf[0], W, Pal); FWfBitmap.SetSize(W, H); - FreeAndNil(FWfIntfImg); + FWfHead := 0; + // Заливаем всё кольцо фоном + FWfBitmap.BeginUpdate(False); + try + for Row := 0 to H - 1 do + begin + RowPtr := PByte(FWfBitmap.ScanLine[Row]); + if RowPtr <> nil then FillDWord(RowPtr^, W, Pal); + end; + finally + FWfBitmap.EndUpdate(False); + end; end; - if H > 1 then Move(FWfPixels[0], FWfPixels[W], (H - 1) * SizeOf(LongWord) * W); - + // Строим только новейшую строку в FWfRowBuf. В TX-режиме столбцы вне TX-полосы + // НЕ трогаем — FWfRowBuf хранит предыдущую верхнюю строку, поэтому + // «замороженные» участки остаются вертикально непрерывными, как и раньше. Step := (SrcCount - 1.0) / Max(1, W - 1); - WatSrcF := 0.0; Px := @FWfPixels[0]; + WatSrcF := 0.0; for X := 0 to W - 1 do begin if FTXMode and (FTXSpanHz > 0) and (FSpanHz > 0) then @@ -366,11 +415,8 @@ begin dB := FCenterFreq - FSpanHz * 0.5 + X * FSpanHz / Max(1, W - 1) - FTXFreq; if (dB < -FTXSpanHz * 0.5) or (dB > FTXSpanHz * 0.5) then begin - if H > 1 then Px^ := FWfPixels[X + W] - else Px^ := 0; - Inc(Px); WatSrcF := WatSrcF + Step; - Continue; + Continue; // оставляем FWfRowBuf[X] без изменений end; frac := (dB + FTXSpanHz * 0.5) / FTXSpanHz * (SrcCount - 1); WatS0 := Trunc(frac); @@ -386,41 +432,23 @@ begin frac := WatSrcF - WatS0; dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac; end; - if FLightTheme then - Pal := WaterfallLightTheme(dB, WfLow, WfHigh) - else - Pal := WaterfallEnhancedColorThetis(dB, WfLow, WfHigh); - Px^ := Pal; Inc(Px); + Idx := Round((dB - WfLow) * LutScale); + if Idx < 0 then Idx := 0 else if Idx > High(FWfLut) then Idx := High(FWfLut); + FWfRowBuf[X] := FWfLut[Idx]; WatSrcF := WatSrcF + Step; end; - if FWfIntfImg = nil then - begin - FWfIntfImg := TLazIntfImage.Create(W, H); - InitRawDesc32WF(Desc, W, H); - FWfIntfImg.DataDescription := Desc; - FWfIntfImg.CreateData; + // Кольцевой сдвиг: новая верхняя строка занимает слот выше предыдущей. + // Полнокадровый Move изображения и полная перезаливка bitmap больше не нужны — + // пишем единственную строку, а прокрутка делается в PaintWaterfall (CopyRect). + FWfHead := (FWfHead - 1 + H) mod H; + FWfBitmap.BeginUpdate(False); + try + RowPtr := PByte(FWfBitmap.ScanLine[FWfHead]); + if RowPtr <> nil then Move(FWfRowBuf[0], RowPtr^, W * 4); + finally + FWfBitmap.EndUpdate(False); end; - BPPi := FWfIntfImg.DataDescription.BitsPerPixel div 8; - if (BPPi = 4) and (FWfIntfImg.PixelData <> nil) then - Move(FWfPixels[0], FWfIntfImg.PixelData^, W * H * 4) - else - begin - Src := @FWfPixels[0]; - for Row := 0 to H - 1 do - begin - RowPtr := FWfIntfImg.GetDataLineStart(Row); - if RowPtr = nil then begin Inc(Src, W); Continue; end; - for Col := 0 to W - 1 do - begin - RowPtr[0] := Byte(Src^); RowPtr[1] := Byte(Src^ shr 8); - RowPtr[2] := Byte(Src^ shr 16); - if BPPi >= 4 then RowPtr[3] := $FF; - Inc(Src); Inc(RowPtr, BPPi); - end; - end; - end; - FWfBitmap.LoadFromIntfImage(FWfIntfImg); end; // ──────────────────────────────────────────────────────────────────────────── @@ -428,13 +456,23 @@ end; // ──────────────────────────────────────────────────────────────────────────── procedure TWaterfallView.PaintWaterfall(Sender: TObject); -var PB: TPaintBox; W, H: Integer; +var PB: TPaintBox; W, H, Top: Integer; begin PB := TPaintBox(Sender); W := PB.Width; H := PB.Height; if (W <= 0) or (H <= 0) then Exit; - if (FWfBitmap <> nil) and (FWfBitmap.Width = W) and (FWfBitmap.Height = H) then - PB.Canvas.Draw(0, 0, FWfBitmap) + if (FWfBitmap <> nil) and (FWfBitmap.Width = W) and (FWfBitmap.Height = H) + and (Length(FWfRowBuf) = W) then + begin + // Кольцевой буфер: дисплейная строка 0 = физ. строка FWfHead. + // Верхний сегмент — строки FWfHead..H-1, нижний — 0..FWfHead-1. + Top := H - FWfHead; + PB.Canvas.CopyRect(Rect(0, 0, W, Top), + FWfBitmap.Canvas, Rect(0, FWfHead, W, H)); + if FWfHead > 0 then + PB.Canvas.CopyRect(Rect(0, Top, W, H), + FWfBitmap.Canvas, Rect(0, 0, W, FWfHead)); + end else begin PB.Canvas.Brush.Color := FTheme.Panel; PB.Canvas.FillRect(Rect(0, 0, W, H));