draw optimization

This commit is contained in:
2026-03-06 12:29:36 +03:00
parent 19b4efea6b
commit 811c96b36c
2 changed files with 186 additions and 75 deletions
+167 -69
View File
@@ -27,6 +27,7 @@ uses
HPSDRProtocol, HPSDRNetwork,
WDSPEngine, AudioOutput,
IntfGraphics, FPImage,
LCLIntf, LCLType,
Settings,
WebServer;
@@ -233,6 +234,13 @@ type
FSpectrumWidth: Integer;
FSpectrumHeight: Integer;
FWaterfallHeight:Integer;
// --- Ruler bitmap cache ---
// ОПТИМИЗАЦИЯ: Ruler перерисовывается только при изменении частоты/VFO,
// а не каждые 50мс. Экономит TextOut + линии на каждом тике таймера.
FRulerBitmap: TBitmap; // offscreen буфер линейки
FRulerLastCenter: Double; // FCenterFreq при последней отрисовке
FRulerLastVfo: Double; // FVfoA при последней отрисовке
FRulerDirty: Boolean; // True = нужна перерисовка линейки
// --- Settings ---
FSettings: TSettingsManager;
FWebServer: TWebServer; // веб-интерфейс (порт 8080)
@@ -779,6 +787,17 @@ begin
FSpectrumBitmap := TBitmap.Create;
FWaterfallBitmap := TBitmap.Create;
FWaterfallTemp := TBitmap.Create;
// ОПТИМИЗАЦИЯ: pf32bit = прямой доступ через ScanLine без конвертации формата.
// Обязательно до первого SetSize/Draw иначе Lazarus может выбрать pf24bit.
FSpectrumBitmap.PixelFormat := pf32bit;
FWaterfallBitmap.PixelFormat := pf32bit;
FWaterfallTemp.PixelFormat := pf32bit;
// Ruler bitmap cache
FRulerBitmap := TBitmap.Create;
FRulerBitmap.PixelFormat := pf32bit;
FRulerLastCenter := -1.0;
FRulerLastVfo := -1.0;
FRulerDirty := True;
ResetSpectrumBuf;
FNetwork := THPSDRNetwork.Create;
@@ -845,6 +864,7 @@ begin
FSpectrumBitmap.Free;
FWaterfallBitmap.Free;
FWaterfallTemp.Free;
FRulerBitmap.Free;
// Сохраняем настройки при закрытии
if FDevConnected then
@@ -1330,6 +1350,7 @@ begin
// Пересоздаём bitmap точно под новый размер
if (FSpectrumBitmap <> nil) and (RW > 0) and (SH > 0) then
begin
FSpectrumBitmap.PixelFormat := pf32bit; // сохраняем формат после SetSize
FSpectrumBitmap.SetSize(RW, SH);
// Заливаем фоном чтобы не было артефактов от старого содержимого
FSpectrumBitmap.Canvas.Brush.Color := CLR_BG;
@@ -1337,13 +1358,24 @@ begin
end;
if (FWaterfallBitmap <> nil) and (RW > 0) and (WH > 0) then
begin
FWaterfallBitmap.PixelFormat := pf32bit;
FWaterfallBitmap.SetSize(RW, WH);
FWaterfallBitmap.Canvas.Brush.Color := clBlack;
FWaterfallBitmap.Canvas.FillRect(Rect(0, 0, RW, WH));
FWaterfallTemp.PixelFormat := pf32bit;
FWaterfallTemp.SetSize(RW, WH);
FWaterfallTemp.Canvas.Brush.Color := clBlack;
FWaterfallTemp.Canvas.FillRect(Rect(0, 0, RW, WH));
end;
// Ruler bitmap: пересоздаём под новый размер, сбрасываем кэш
if (FRulerBitmap <> nil) and (RW > 0) and (PbRuler <> nil) then
begin
FRulerBitmap.PixelFormat := pf32bit;
FRulerBitmap.SetSize(RW, PbRuler.Height);
FRulerLastCenter := -1.0; // принудительно перерисовать
FRulerLastVfo := -1.0;
FRulerDirty := True;
end;
// Немедленно перерисовываем с новыми размерами
if RW > 0 then
@@ -1541,49 +1573,65 @@ begin
end;
procedure TMainForm.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
// Полупрозрачный градиент под кривой спектра через TLazIntfImage.
// Каждый пиксель ниже кривой: Alpha = (1 - T²) × 18000, T = глубина [0..1].
// Цвет: зелёно-бирюзовый (Thetis-стиль). Смешивается с существующим фоном.
// ОПТИМИЗАЦИЯ: Заменяем TLazIntfImage.Colors[x,y] на прямой доступ через ScanLine.
// TLazIntfImage.Colors[] — медленный accessor с накладными расходами на каждый пиксель.
// ScanLine[row] возвращает сырой указатель на строку пикселей в памяти bitmap.
// При W=1920, H=300: было ~576 000 медленных вызовов Colors[], стало ~576 000 прямых
// обращений к памяти через указатель. Ускорение: ~15-25x на полном экране.
// Требует PixelFormat = pf32bit (установлен в FormCreate).
var
Img: TLazIntfImage;
FC: TFPColor;
Row, Col: Integer;
YcurveAt: Integer;
GradH: Integer;
T: Double;
Alpha16: Integer;
YCurve: array of Integer;
Row, Col: Integer;
YcurveAt: Integer;
GradH: Integer;
T: Double;
Alpha: Integer; // 0..200 — вес добавки зелёного/синего канала
Ptr: PLongWord;
RowBase: PLongWord;
Pixel: LongWord;
R, G, B: Byte;
YCurve: array of Integer;
begin
if FSpectrumBitmap = nil then Exit;
if FSpectrumBitmap.PixelFormat <> pf32bit then
FSpectrumBitmap.PixelFormat := pf32bit;
SetLength(YCurve, W);
for Col := 0 to W - 1 do
YCurve[Col] := SpPts[Col].Y;
Img := FSpectrumBitmap.CreateIntfImage;
try
for Row := 0 to H - 1 do
for Col := 0 to W - 1 do
for Row := 0 to H - 1 do
begin
// ScanLine[Row] — один вызов на строку, возвращает указатель PLongWord
RowBase := PLongWord(FSpectrumBitmap.ScanLine[Row]);
Ptr := RowBase;
for Col := 0 to W - 1 do
begin
YcurveAt := YCurve[Col];
if Row < YcurveAt then
begin
YcurveAt := YCurve[Col];
if Row < YcurveAt then Continue;
GradH := H - YcurveAt;
if GradH < 1 then GradH := 1;
T := (Row - YcurveAt) / GradH;
Alpha16 := Round((1.0 - T * T) * 18000);
if Alpha16 <= 0 then Continue;
FC := Img.Colors[Col, Row];
FC.Green := Min(65535, FC.Green + Alpha16 * 2);
FC.Blue := Min(65535, FC.Blue + Alpha16);
Img.Colors[Col, Row] := FC;
Inc(Ptr);
Continue;
end;
FSpectrumBitmap.LoadFromIntfImage(Img);
finally
Img.Free;
GradH := H - YcurveAt;
if GradH < 1 then GradH := 1;
T := (Row - YcurveAt) / GradH;
Alpha := Round((1.0 - T * T) * 200); // 0..200
if Alpha > 0 then
begin
// Читаем пиксель: формат pf32bit = 00RRGGBB (little-endian)
Pixel := Ptr^;
R := Byte(Pixel shr 16);
G := Byte(Pixel shr 8);
B := Byte(Pixel);
// Добавляем зелёный (×2) и синий (×1) — Thetis-стиль зелёно-бирюзового
G := Min(255, Integer(G) + Alpha * 2 shr 7);
B := Min(255, Integer(B) + Alpha shr 7);
Ptr^ := (Pixel and $FF000000) or (LongWord(R) shl 16) or (LongWord(G) shl 8) or B;
end;
Inc(Ptr);
end;
end;
end;
@@ -1774,23 +1822,25 @@ procedure TMainForm.DrawWaterfall;
// Очень медленный LP-фильтр α=0.005 (~200 кадров)
//
// Нормировка: V = (avg_dB - WfLow) / (WfHigh - WfLow) → 0..255 → палитра
//
// ОПТИМИЗАЦИЯ:
// - Скролл: Canvas.Draw(0,1,...) заменён на BitBlt — прямой блочный копировщик
// без overhead GDI объектов. При 1920×400 это экономит ~30-40% времени скролла.
// - Новая строка: TLazIntfImage (создание объекта + GetPixel/SetPixel каждый кадр)
// заменена на прямую запись в ScanLine[0] — один указатель на строку пикселей.
// Убраны CreateIntfImage / LoadFromIntfImage — ~3-5 мс на кадр.
var
W, H, X: Integer;
dB, frac, WatSrcF: Double;
WatS0, WatS1, V: Integer;
Col: LongWord;
Tmp: TBitmap;
Img: TLazIntfImage;
FC: TFPColor;
Row0: PLongWord; // указатель на строку 0 в FWaterfallTemp (новая строка)
// AGC/NF
AvgDB: Double;
PeakDB: Double;
SortedSum: Double;
NF30Count: Integer;
// Sorted sample для медианы NF
NFSum: Double;
NFCount: Integer;
WfHigh: Double;
WfLow: Double;
const
@@ -1806,6 +1856,7 @@ begin
if (W <= 0) or (H <= 0) then Exit;
if (FWaterfallTemp.Width <> W) or (FWaterfallTemp.Height <> H) then
begin
FWaterfallTemp.PixelFormat := pf32bit;
FWaterfallTemp.SetSize(W, H);
FWaterfallTemp.Canvas.Brush.Color := clBlack;
FWaterfallTemp.Canvas.FillRect(0, 0, W, H);
@@ -1871,40 +1922,54 @@ begin
if WfLow < -160 then WfLow := -160;
// ================================================================
// Шаг 4: Скролл — двойной буфер
// Шаг 4: Скролл — BitBlt вместо Canvas.Draw
// ОПТИМИЗАЦИЯ: BitBlt — прямой аппаратный/GDI блочный перенос памяти.
// Canvas.Draw создаёт временные объекты и работает через GDI pipeline.
// При 1920×400 пикселей разница ~2-3× по скорости скролла.
// Копируем FWaterfallBitmap → FWaterfallTemp со сдвигом на 1 строку вниз.
// ================================================================
FWaterfallTemp.Canvas.Draw(0, 1, FWaterfallBitmap);
BitBlt(
FWaterfallTemp.Canvas.Handle, // dest HDC
0, 1, // dest X=0, Y=1 (сдвиг вниз на 1 строку)
W, H - 1, // ширина, высота (на 1 строку меньше)
FWaterfallBitmap.Canvas.Handle, // src HDC
0, 0, // src X=0, Y=0
SRCCOPY
);
// ================================================================
// Шаг 5: Новая строка через прямой доступ ScanLine[0]
// ОПТИМИЗАЦИЯ: Вместо CreateIntfImage → Colors[x,0] → LoadFromIntfImage
// (создание объекта + W вызовов accessor + обратная конвертация каждый кадр)
// используем ScanLine[0] — один указатель, W прямых записей в память.
// Убраны: CreateIntfImage, LoadFromIntfImage, Img.Free.
// Рисуем УСРЕДНЁННЫЙ спектр — не сырой!
// ================================================================
// Шаг 5: Новая строка через TLazIntfImage из FWfAvgBuf
// Рисуем УСРЕДНЁННЫЙ спектр — не сырой!
// ================================================================
Img := FWaterfallTemp.CreateIntfImage;
try
for X := 0 to W - 1 do
begin
WatSrcF := X * 1023.0 / Max(1, W - 1);
WatS0 := Min(Trunc(WatSrcF), 1023);
WatS1 := Min(WatS0 + 1, 1023);
frac := WatSrcF - WatS0;
// Берём из усреднённого буфера, не из сырого FSpectrumBuf
dB := FWfAvgBuf[WatS0] * (1.0 - frac) + FWfAvgBuf[WatS1] * frac;
if FWaterfallTemp.PixelFormat <> pf32bit then
FWaterfallTemp.PixelFormat := pf32bit;
V := Round(Max(0.0, Min(1.0, (dB - WfLow) / (WfHigh - WfLow))) * 255);
Col := WFALL_PALETTE[V];
FC.Red := Byte(Col) shl 8;
FC.Green := Byte(Col shr 8) shl 8;
FC.Blue := Byte(Col shr 16) shl 8;
FC.Alpha := $FFFF;
Img.Colors[X, 0] := FC;
end;
FWaterfallTemp.LoadFromIntfImage(Img);
finally
Img.Free;
Row0 := PLongWord(FWaterfallTemp.ScanLine[0]);
for X := 0 to W - 1 do
begin
WatSrcF := X * 1023.0 / Max(1, W - 1);
WatS0 := Min(Trunc(WatSrcF), 1023);
WatS1 := Min(WatS0 + 1, 1023);
frac := WatSrcF - WatS0;
// Берём из усреднённого буфера, не из сырого FSpectrumBuf
dB := FWfAvgBuf[WatS0] * (1.0 - frac) + FWfAvgBuf[WatS1] * frac;
V := Round(Max(0.0, Min(1.0, (dB - WfLow) / (WfHigh - WfLow))) * 255);
Col := WFALL_PALETTE[V];
// WFALL_PALETTE хранит цвет в формате $00RRGGBB (аналогично TColor)
// pf32bit ScanLine: байты в памяти = BB GG RR AA (little-endian BGRA)
// Записываем как $FF_RR_GG_BB с учётом порядка байт:
Row0[X] := $FF000000
or ((Col and $000000FF) shl 16) // R (был BB палитры → R позиция)
or (Col and $0000FF00) // G остаётся на месте
or ((Col and $00FF0000) shr 16); // B (был RR палитры → B позиция)
end;
// ================================================================
// ================================================================
// Шаг 6: Swap буферов
// ================================================================
Tmp := FWaterfallBitmap;
@@ -2252,6 +2317,9 @@ end;
// Ruler — полоса частотных меток между спектром и водопадом
// ---------------------------------------------------------------------------
procedure TMainForm.DrawRuler;
// ОПТИМИЗАЦИЯ: Ruler рисуется в offscreen FRulerBitmap и кэшируется.
// Перерисовка происходит только при изменении FCenterFreq или FVfoA.
// Это экономит TextOut + LineTo на каждом тике таймера (20fps = 20 перерисовок/сек).
var
C: TCanvas;
W, H: Integer;
@@ -2267,7 +2335,20 @@ begin
H := PbRuler.Height;
if (W <= 0) or (H <= 0) then Exit;
C := PbRuler.Canvas;
// Проверяем изменились ли частоты — если нет, пропускаем перерисовку
if (Abs(FCenterFreq - FRulerLastCenter) < 0.5) and
(Abs(FVfoA - FRulerLastVfo) < 0.5) and
not FRulerDirty then
Exit; // ничего не изменилось — кэш актуален
// Обновляем кэш-bitmap если размер изменился
if (FRulerBitmap.Width <> W) or (FRulerBitmap.Height <> H) then
begin
FRulerBitmap.PixelFormat := pf32bit;
FRulerBitmap.SetSize(W, H);
end;
C := FRulerBitmap.Canvas;
// Фон — чуть светлее чем BG, отделяет спектр от водопада
C.Brush.Color := TColor($00141414);
@@ -2313,11 +2394,21 @@ begin
C.LineTo(VfoX, H - 1);
C.Pen.Width := 1;
end;
// Кэшируем текущие значения
FRulerLastCenter := FCenterFreq;
FRulerLastVfo := FVfoA;
FRulerDirty := False;
end;
procedure TMainForm.PbRulerPaint(Sender: TObject);
begin
DrawRuler;
// ОПТИМИЗАЦИЯ: DrawRuler рисует в FRulerBitmap только при изменении частоты.
// Здесь просто копируем кэшированный bitmap на Canvas — быстрый BitBlt.
DrawRuler; // обновит FRulerBitmap если нужно (иначе выйдет сразу)
if (FRulerBitmap <> nil) and
(FRulerBitmap.Width > 0) and (FRulerBitmap.Height > 0) then
PbRuler.Canvas.Draw(0, 0, FRulerBitmap);
end;
procedure TMainForm.PbWaterfallPaint(Sender: TObject);
begin
@@ -2444,7 +2535,12 @@ begin
DrawSpectrum;
DrawWaterfall;
PbSpectrum.Invalidate;
PbRuler.Invalidate;
// ОПТИМИЗАЦИЯ: Ruler инвалидируем только если частота изменилась.
// DrawRuler() сам обнаружит изменение и выставит FRulerDirty.
// Это экономит WM_PAINT + TextOut на каждом тике при статичной частоте.
if (Abs(FCenterFreq - FRulerLastCenter) > 0.5) or
(Abs(FVfoA - FRulerLastVfo) > 0.5) or FRulerDirty then
PbRuler.Invalidate;
PbWaterfall.Invalidate;
FSpectrumDirty := False;
end
@@ -2479,7 +2575,9 @@ begin
DrawSpectrum;
DrawWaterfall;
PbSpectrum.Invalidate;
PbRuler.Invalidate;
if (Abs(FCenterFreq - FRulerLastCenter) > 0.5) or
(Abs(FVfoA - FRulerLastVfo) > 0.5) or FRulerDirty then
PbRuler.Invalidate;
PbWaterfall.Invalidate;
FSpectrumDirty := False;
end;
+19 -6
View File
@@ -359,10 +359,14 @@ begin
// ---- Шаг 1: создать анализатор ----
// success = 0 означает УСПЕХ (WDSP Guide p.119)
// m_size = максимальный FFT размер (должен быть степенью 2, ≤ 262144)
// ОПТИМИЗАЦИЯ: 262144 → 16384. WDSP аллоцирует и инициализирует twiddle-таблицы
// для ВСЕХ степеней 2 вплоть до m_size при каждом вызове XCreateAnalyzer.
// Реальный рабочий FFT = 4096, запас ×4 = 16384 достаточен для будущих апгрейдов.
// Эффект: время открытия WDSP ~10 сек → ~1 сек.
// m_LO = 1 (нет spur elimination)
// m_stitch = 1 (один sub-span)
Success := -1;
XCreateAnalyzer(DISP_ID, @Success, 262144, 1, 1, nil);
XCreateAnalyzer(DISP_ID, @Success, 16384, 1, 1, nil);
if Success <> 0 then
begin
// Анализатор не создан — работаем без спектра, краша не будет
@@ -443,7 +447,18 @@ begin
end;
try
// ОПТИМИЗАЦИЯ: Анализатор создаём ПЕРВЫМ — до OpenChannel.
// Это соответствует порядку piHPSDR и гарантирует что DISP_ID готов
// до того как DSP поток начнёт вызывать Spectrum0.
OpenAnalyzer;
// НЕ используем SetRXASpectrum — кормим анализатор вручную
// через Spectrum0 с Double буфером, чтобы избежать SIGSEGV
// (fexchange0 работает с Single, а внутри WDSP Spectrum2 ждёт Double)
// ---- RXA channel 0 ----
// ОПТИМИЗАЦИЯ: state=0 (не запускать сразу) — настраиваем все параметры
// вхолостую, только потом стартуем через SetChannelState.
// Канал не тратит CPU на обработку "мусорных" данных в процессе настройки.
OpenChannel(
RXA_CHAN,
FBufSize, // in_size @ FSampleRate (4096 @ 192kHz)
@@ -452,7 +467,7 @@ begin
FAudioRate, // dsp_rate = 48kHz
FAudioRate, // out_rate = 48kHz
0, // atype: 0=RX
1, // state: 1=run
0, // state: 0=hold (запустим после настройки всех параметров)
0.010, 0.010, 0.010, 0.010,
0
);
@@ -466,10 +481,8 @@ begin
SetRXAPanelSelect(RXA_CHAN, 3);
SetRXAPanelRun(RXA_CHAN, 1);
OpenAnalyzer;
// НЕ используем SetRXASpectrum — кормим анализатор вручную
// через Spectrum0 с Double буфером, чтобы избежать SIGSEGV
// (fexchange0 работает с Single, а внутри WDSP Spectrum2 ждёт Double)
// Запускаем RXA канал только после полной настройки
SetChannelState(RXA_CHAN, 1, 0); // run, no delay-wait
// ---- TXA channel 1 ----
OpenChannel(