Files
ewsdr/WaterfallView.pas
T
ew8bakandClaude Fable 5 e260995218 feat(display): скорость водопада с max-hold накоплением + MSAA спектра
- настройка Speed (wf_speed_div 1/2/4/8, Waterfall→Rendering): замедление
  накапливает кадры max-hold'ом в SetWaterfallData — короткие посылки (CW)
  не выпадают из строки; двойная децимация MainForm убрана
  (EffectiveWfInterval, CPU-путь не быстрее полукадровой)
- настройка Anti-aliasing (spec_msaa 1/2/4/8, дефолт 4x, Spectrum→Rendering):
  MultiSampling на GL-контроле спектра; смена в рантайме пересоздаёт
  контекст → ResetGLCache обнуляет текстурные кэши без glDelete

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 16:48:55 +03:00

592 lines
23 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
unit WaterfallView;
{
WaterfallView.pas — рендеринг водопада.
TWaterfallView — изолированный класс, не зависит от SpectrumView.
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, Graphics, ExtCtrls, Controls, Math,
LCLIntf, LCLType, AppTheme;
const
// Максимум точек строки водопада; держать в синхроне с
// WDSPEngine.SPECTRUM_PIXELS.
WF_MAX_PIXELS = 4096;
// Палитры водопада (индексы совпадают с настройкой wf_palette)
WF_PAL_CLASSIC = 0; // Thetis enhanced (тёмная) / пастельная (светлая тема)
WF_PAL_INFERNO = 1;
WF_PAL_TURBO = 2;
type
TWaterfallView = class
protected
FWaterfallBitmap: TBitmap;
FWfBitmap: TBitmap;
FWfBitmapW: Integer;
FWfBitmapH: Integer;
FWfRowBuf: array of LongWord; // одна строка (новейшая), нативный порядок
FWfHead: Integer; // физ. строка bitmap с новейшей линией
FWfLut: array[0..511] of LongWord; // нативный порядок байт (CPU bitmap)
FWfLutARGB: array[0..511] of LongWord; // логический $AARRGGBB (GL-путь)
FWfLutLow: Double;
FWfLutHigh: Double;
FWfLutLight: Boolean;
FWfLutPalette: Integer;
FWfLutGamma: Double;
FWfPalette: Integer;
FWfGamma: Double;
FWaterfallBuf: array[0..WF_MAX_PIXELS-1] of Single;
FWaterfallBufCount: Integer;
FWaterfallDirty: Boolean;
FWfFrameCounter: Integer;
FWfFrameInterval: Integer;
FWfAGCEnabled: Boolean;
FWfNFEnabled: Boolean;
FWfManualHigh: Double;
FWfManualLow: Double;
FWfAGCOffset: Double;
FWfHigh: Double;
FWfLow: Double;
FCenterFreq: Double;
FSpanHz: Double;
FTXMode: Boolean;
FTXFreq: Double;
FTXSpanHz: Double;
FMarkerActive: Boolean;
FMarkerX: Integer;
FTheme: TAppTheme;
FLightTheme: Boolean;
FPbWaterfall: TControl;
procedure DrawMarkerLine(C: TCanvas; W, H: Integer);
procedure BuildWfLut(WfLow, WfHigh: Double);
function LutNeedsRebuild(WfLow, WfHigh: Double): Boolean;
function PaletteColor(T: Double): LongWord; // t∈[0..1] → $AARRGGBB
procedure SetWfPalette(V: Integer);
procedure SetWfGamma(V: Double);
procedure UpdateAutoLevels; // гистограммный AGC → FWfLow/FWfHigh
procedure GetEffectiveLevels(out Lo, Hi: Double);
public
constructor Create;
destructor Destroy; override;
property CenterFreq: Double read FCenterFreq write FCenterFreq;
property SpanHz: Double read FSpanHz write FSpanHz;
property TXMode: Boolean read FTXMode write FTXMode;
property TXFreq: Double read FTXFreq write FTXFreq;
property TXSpanHz: Double read FTXSpanHz write FTXSpanHz;
property WfAGCEnabled: Boolean read FWfAGCEnabled write FWfAGCEnabled;
property WfNFEnabled: Boolean read FWfNFEnabled write FWfNFEnabled;
property WfManualHigh: Double read FWfManualHigh write FWfManualHigh;
property WfManualLow: Double read FWfManualLow write FWfManualLow;
property WfAGCOffset: Double read FWfAGCOffset write FWfAGCOffset;
property WaterfallDirty: Boolean read FWaterfallDirty write FWaterfallDirty;
property WfFrameInterval: Integer read FWfFrameInterval write FWfFrameInterval;
property MarkerActive: Boolean read FMarkerActive write FMarkerActive;
property MarkerX: Integer read FMarkerX write FMarkerX;
property PbWaterfall: TControl write FPbWaterfall;
property WfPalette: Integer read FWfPalette write SetWfPalette;
property WfGamma: Double read FWfGamma write SetWfGamma;
procedure SetWaterfallData(const Pixels: array of Single; Count: Integer); virtual;
procedure DrawWaterfall; virtual;
procedure PaintWaterfall(Sender: TObject); virtual;
procedure SetWaterfallBitmapSize(W, H: Integer); virtual;
procedure ResetWfAvgBuf; virtual;
procedure ResetWfBuf; virtual;
procedure SetTheme(const T: TAppTheme); virtual;
end;
implementation
// ════════════════════════════════════════════════════════════════════════════
// Вспомогательные функции
// ════════════════════════════════════════════════════════════════════════════
function FormatFreqWF(Hz: Double): string;
var Mhz: Int64; KHz, Rest: Integer;
begin
Mhz := Trunc(Hz / 1000000);
KHz := Trunc((Hz - Mhz * 1000000) / 1000);
Rest := Trunc(Hz) mod 1000;
Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
end;
// Классическая тёмная палитра (Thetis enhanced), T∈[0..1]
function ClassicDarkColor(T: Double): LongWord;
var
Local: Double;
R, G, B: Integer;
begin
if T <= 0.0 then begin Result := $FF000000; Exit; end;
if T >= 1.0 then begin Result := ($FF shl 24) or (255 shl 16) or (124 shl 8) or 192; Exit; end;
if T < (2.0 / 9.0) then
begin Local := T / (2.0/9.0); R := 0; G := 0; B := Round(Local * 255.0); end
else if T < (3.0 / 9.0) then
begin Local := (T - 2.0/9.0) / (1.0/9.0); R := 0; G := Round(Local*255.0); B := 255; end
else if T < (4.0 / 9.0) then
begin Local := (T - 3.0/9.0) / (1.0/9.0); R := 0; G := 255; B := Round((1.0-Local)*255.0); end
else if T < (5.0 / 9.0) then
begin Local := (T - 4.0/9.0) / (1.0/9.0); R := Round(Local*255.0); G := 255; B := 0; end
else if T < (7.0 / 9.0) then
begin Local := (T - 5.0/9.0) / (2.0/9.0); R := 255; G := Round((1.0-Local)*255.0); B := 0; end
else if T < (8.0 / 9.0) then
begin Local := (T - 7.0/9.0) / (1.0/9.0); R := 255; G := 0; B := Round(Local*255.0); end
else
begin
Local := (T - 8.0/9.0) / (1.0/9.0);
R := Round((0.75 + 0.25 * (1.0 - Local)) * 255.0);
G := Round(Local * 255.0 * 0.5);
B := 255;
end;
R := EnsureRange(R, 0, 255);
G := EnsureRange(G, 0, 255);
B := EnsureRange(B, 0, 255);
Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B;
end;
// Линейная интерполяция по таблице стопов, T∈[0..1]
function GradientColor(const SR, SG, SB: array of Integer; T: Double): LongWord;
var
F: Double;
Seg, N, R, G, B: Integer;
begin
N := Length(SR);
if T <= 0.0 then
begin
Result := ($FF shl 24) or (SR[0] shl 16) or (SG[0] shl 8) or SB[0];
Exit;
end;
if T >= 1.0 then
begin
Result := ($FF shl 24) or (SR[N-1] shl 16) or (SG[N-1] shl 8) or SB[N-1];
Exit;
end;
F := T * (N - 1);
Seg := Min(N - 2, Trunc(F));
F := F - Seg;
R := EnsureRange(Round(SR[Seg] * (1.0 - F) + SR[Seg+1] * F), 0, 255);
G := EnsureRange(Round(SG[Seg] * (1.0 - F) + SG[Seg+1] * F), 0, 255);
B := EnsureRange(Round(SB[Seg] * (1.0 - F) + SB[Seg+1] * F), 0, 255);
Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B;
end;
const
// Классическая палитра светлой темы (пастель, фон E0E6E8)
LIGHT_R: array[0..7] of Integer = (224, 160, 20, 0, 0, 220, 255, 255);
LIGHT_G: array[0..7] of Integer = (230, 185, 80, 185, 200, 210, 90, 20);
LIGHT_B: array[0..7] of Integer = (232, 210, 200, 210, 80, 0, 0, 20);
// Inferno (matplotlib), перцептуально-равномерная, 11 опорных точек
INF_R: array[0..10] of Integer = ( 0, 22, 66, 106, 147, 188, 221, 243, 252, 246, 252);
INF_G: array[0..10] of Integer = ( 0, 11, 10, 23, 38, 55, 81, 120, 165, 215, 255);
INF_B: array[0..10] of Integer = ( 4, 57, 104, 110, 103, 84, 58, 25, 10, 70, 164);
// Turbo (Google), улучшенный jet без артефактов яркости, 9 опорных точек
TRB_R: array[0..8] of Integer = ( 48, 70, 24, 97, 164, 249, 247, 202, 122);
TRB_G: array[0..8] of Integer = ( 18, 118, 221, 252, 252, 163, 94, 33, 4);
TRB_B: array[0..8] of Integer = ( 59, 232, 194, 108, 60, 63, 33, 6, 3);
// Преобразует логический $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
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
// ════════════════════════════════════════════════════════════════════════════
constructor TWaterfallView.Create;
begin
inherited Create;
FWaterfallBitmap := TBitmap.Create;
FWfBitmap := TBitmap.Create;
FWfBitmap.PixelFormat := pf32bit;
FWfBitmapW := 0;
FWfBitmapH := 0;
FWfManualHigh := -80.0;
FWfManualLow := -130.0;
FWfAGCOffset := 0.0;
FWfHigh := -80.0;
FWfLow := -130.0;
FWfAGCEnabled := True;
FWfNFEnabled := False;
FWaterfallBufCount := 1024;
FWfFrameInterval := 2;
FWfFrameCounter := 0;
FWaterfallDirty := True;
FTheme := DarkTheme;
FLightTheme := False;
FSpanHz := 192000;
FWfPalette := WF_PAL_CLASSIC;
FWfGamma := 1.0;
FWfLutLow := 1.0; // невалидно → LUT перестроится при первом кадре
FWfLutHigh := 0.0;
FWfLutLight := False;
FWfLutPalette := -1;
FWfLutGamma := 0.0;
ResetWfBuf;
end;
destructor TWaterfallView.Destroy;
begin
FWaterfallBitmap.Free;
FWfBitmap.Free;
inherited;
end;
procedure TWaterfallView.SetTheme(const T: TAppTheme);
begin
FTheme := T;
FLightTheme := T.BG > TColor($00808080);
FWaterfallDirty := True;
// Сброс размера — чтобы следующий кадр перезаполнил фон новой темой
FWfBitmapW := 0;
FWfBitmapH := 0;
FWfLutLow := 1.0; // невалидно → LUT перестроится
FWfLutHigh := 0.0;
SetLength(FWfRowBuf, 0);
FWfHead := 0;
end;
procedure TWaterfallView.ResetWfAvgBuf;
begin
FWfHigh := FWfManualHigh;
FWfLow := FWfManualLow;
FWaterfallDirty := True;
end;
procedure TWaterfallView.ResetWfBuf;
var i: Integer;
begin
for i := 0 to High(FWaterfallBuf) do FWaterfallBuf[i] := -130.0;
FWfHigh := FWfManualHigh;
FWfLow := FWfManualLow;
FWaterfallDirty := True;
end;
procedure TWaterfallView.SetWfPalette(V: Integer);
begin
V := EnsureRange(V, WF_PAL_CLASSIC, WF_PAL_TURBO);
if V = FWfPalette then Exit;
FWfPalette := V;
FWfLutPalette := -1; // LUT перестроится на следующем кадре
FWaterfallDirty := True;
end;
procedure TWaterfallView.SetWfGamma(V: Double);
begin
V := EnsureRange(V, 0.3, 3.0);
if Abs(V - FWfGamma) < 1E-6 then Exit;
FWfGamma := V;
FWfLutGamma := 0.0; // LUT перестроится на следующем кадре
FWaterfallDirty := True;
end;
procedure TWaterfallView.SetWaterfallData(const Pixels: array of Single; Count: Integer);
var i, N: Integer;
begin
N := Min(Count, WF_MAX_PIXELS);
// Замедление (WfFrameInterval>1) накапливает кадры max-hold'ом, а не
// выбрасывает их — короткие посылки (CW) не выпадают из строки.
if (FWfFrameCounter = 0) or (N <> FWaterfallBufCount) then
for i := 0 to N - 1 do FWaterfallBuf[i] := Pixels[i]
else
for i := 0 to N - 1 do
if Pixels[i] > FWaterfallBuf[i] then FWaterfallBuf[i] := Pixels[i];
FWaterfallBufCount := N;
Inc(FWfFrameCounter);
if FWfFrameCounter >= Max(1, FWfFrameInterval) then
begin
FWfFrameCounter := 0;
FWaterfallDirty := True;
end;
end;
procedure TWaterfallView.SetWaterfallBitmapSize(W, H: Integer);
begin
FWaterfallBitmap.SetSize(W, H);
FWfBitmapW := 0;
FWfBitmapH := 0;
SetLength(FWfRowBuf, 0);
FWfHead := 0;
end;
procedure TWaterfallView.DrawMarkerLine(C: TCanvas; W, H: Integer);
var MX: Integer; MarkerFreq: Double; MarkerLbl: string;
begin
MX := Round(FMarkerX / 1000.0 * W);
if (MX < 0) or (MX >= W) then Exit;
MarkerFreq := (FCenterFreq - FSpanHz / 2) + FMarkerX / 1000.0 * FSpanHz;
MarkerLbl := FormatFreqWF(Round(MarkerFreq));
C.Pen.Color := TColor($004444FF);
C.Pen.Width := 1; C.Pen.Style := psSolid;
C.MoveTo(MX, 0); C.LineTo(MX, H);
C.Font.Color := TColor($004444FF);
C.Font.Size := 7; C.Font.Name := 'Courier New';
if MX + 4 + C.TextWidth(MarkerLbl) < W then
C.TextOut(MX + 4, 4, MarkerLbl)
else
C.TextOut(MX - 4 - C.TextWidth(MarkerLbl), 4, MarkerLbl);
end;
// Цвет палитры для нормированного уровня T∈[0..1] (без гаммы — она в LUT)
function TWaterfallView.PaletteColor(T: Double): LongWord;
begin
case FWfPalette of
WF_PAL_INFERNO: Result := GradientColor(INF_R, INF_G, INF_B, T);
WF_PAL_TURBO: Result := GradientColor(TRB_R, TRB_G, TRB_B, T);
else
if FLightTheme then
Result := GradientColor(LIGHT_R, LIGHT_G, LIGHT_B, T)
else
Result := ClassicDarkColor(T);
end;
end;
function TWaterfallView.LutNeedsRebuild(WfLow, WfHigh: Double): Boolean;
begin
Result := (FWfLutLight <> FLightTheme) or
(FWfLutPalette <> FWfPalette) or
(Abs(FWfLutGamma - FWfGamma) > 1E-6) or
(Abs(WfLow - FWfLutLow) > 0.05) or
(Abs(WfHigh - FWfLutHigh) > 0.05);
end;
// Перестраивает палитру dB→цвет (512 точек) один раз на кадр вместо
// вычисления цвета с плавающей точкой на каждый пиксель.
procedure TWaterfallView.BuildWfLut(WfLow, WfHigh: Double);
var
i: Integer;
T: Double;
C: LongWord;
begin
for i := 0 to High(FWfLut) do
begin
T := i / High(FWfLut);
// Гамма <1 вытягивает слабые сигналы из шума, >1 притемняет
if Abs(FWfGamma - 1.0) > 1E-6 then T := Power(T, FWfGamma);
C := PaletteColor(T);
FWfLutARGB[i] := C;
FWfLut[i] := NativePixel(C);
end;
FWfLutLow := WfLow;
FWfLutHigh := WfHigh;
FWfLutLight := FLightTheme;
FWfLutPalette := FWfPalette;
FWfLutGamma := FWfGamma;
end;
// Гистограммный AGC: оценивает шумовой пол (30-й перцентиль) и верх сигналов
// (98-й), плавно подтягивает FWfLow/FWfHigh. Общий для CPU- и GL-пути.
procedure TWaterfallView.UpdateAutoLevels;
var
X, SrcCount, LowTargetCount, HighTargetCount, CumCount, HistIdx: Integer;
Hist: array[0..191] of Integer;
HistMinDB, HistStepDB, NoiseFloorDB, SignalTopDB: Double;
TargetLow, TargetHigh: Double;
const
ALPHA_HIGH = 0.10; ALPHA_LOW = 0.08;
WF_AUTO_OFFSET = -4.0; WF_MIN_RANGE = 48.0; WF_MAX_RANGE = 62.0;
HIST_MAX_DB = 22.0;
begin
if not FWfAGCEnabled then Exit;
SrcCount := EnsureRange(FWaterfallBufCount, 2, WF_MAX_PIXELS);
FillChar(Hist, SizeOf(Hist), 0);
HistMinDB := -170.0;
HistStepDB := (HIST_MAX_DB - HistMinDB) / Length(Hist);
for X := 0 to SrcCount - 1 do
begin
HistIdx := EnsureRange(Trunc((FWaterfallBuf[X] - HistMinDB) / HistStepDB), 0, High(Hist));
Inc(Hist[HistIdx]);
end;
LowTargetCount := Round(SrcCount * 0.30);
HighTargetCount := Round(SrcCount * 0.98);
CumCount := 0; NoiseFloorDB := FWfLow; SignalTopDB := FWfHigh;
for HistIdx := 0 to High(Hist) do
begin
Inc(CumCount, Hist[HistIdx]);
if CumCount >= LowTargetCount then
begin NoiseFloorDB := HistMinDB + (HistIdx + 0.5) * HistStepDB; Break; end;
end;
CumCount := 0;
for HistIdx := 0 to High(Hist) do
begin
Inc(CumCount, Hist[HistIdx]);
if CumCount >= HighTargetCount then
begin SignalTopDB := HistMinDB + (HistIdx + 0.5) * HistStepDB; Break; end;
end;
TargetLow := NoiseFloorDB + WF_AUTO_OFFSET + FWfAGCOffset;
if FWfNFEnabled then
TargetHigh := Max(TargetLow + WF_MIN_RANGE, SignalTopDB + 6.0)
else
TargetHigh := TargetLow + 52.0;
if TargetHigh > TargetLow + WF_MAX_RANGE then TargetHigh := TargetLow + WF_MAX_RANGE;
FWfLow := FWfLow + ALPHA_LOW * (TargetLow - FWfLow);
FWfHigh := FWfHigh + ALPHA_HIGH * (TargetHigh - FWfHigh);
end;
procedure TWaterfallView.GetEffectiveLevels(out Lo, Hi: Double);
begin
if FWfAGCEnabled then begin Lo := FWfLow; Hi := FWfHigh; end
else begin Lo := FWfManualLow; Hi := FWfManualHigh; end;
if Hi < Lo + 40.0 then Hi := Lo + 40.0;
if Hi > 0.0 then Hi := 0.0;
if Lo < -160 then Lo := -160;
end;
// ────────────────────────────────────────────────────────────────────────────
// DrawWaterfall
// ────────────────────────────────────────────────────────────────────────────
procedure TWaterfallView.DrawWaterfall;
var
W, H, X: Integer;
dB, frac, WatSrcF: Double;
WatS0, WatS1: Integer;
WfHigh, WfLow, LutScale, Step: Double;
Row, Idx: Integer; RowPtr: PByte;
Pal: LongWord; SrcCount: Integer;
begin
if FWaterfallBitmap = nil then Exit;
W := FWaterfallBitmap.Width; H := FWaterfallBitmap.Height;
if (W <= 0) or (H <= 0) then Exit;
SrcCount := EnsureRange(FWaterfallBufCount, 2, WF_MAX_PIXELS);
UpdateAutoLevels;
GetEffectiveLevels(WfLow, WfHigh);
LutScale := High(FWfLut) / (WfHigh - WfLow);
// Палитра зависит только от WfLow/WfHigh/темы/палитры/гаммы — перестраиваем
// лишь при ощутимом изменении (AGC сдвигает границы медленно).
if LutNeedsRebuild(WfLow, WfHigh) then
BuildWfLut(WfLow, WfHigh);
if (FWfBitmapW <> W) or (FWfBitmapH <> H) then
begin
FWfBitmapW := W; FWfBitmapH := H;
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(FWfRowBuf[0], W, Pal);
FWfBitmap.SetSize(W, H);
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;
// Строим только новейшую строку в FWfRowBuf. В TX-режиме столбцы вне TX-полосы
// НЕ трогаем — FWfRowBuf хранит предыдущую верхнюю строку, поэтому
// «замороженные» участки остаются вертикально непрерывными, как и раньше.
Step := (SrcCount - 1.0) / Max(1, W - 1);
WatSrcF := 0.0;
for X := 0 to W - 1 do
begin
if FTXMode and (FTXSpanHz > 0) and (FSpanHz > 0) then
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
WatSrcF := WatSrcF + Step;
Continue; // оставляем FWfRowBuf[X] без изменений
end;
frac := (dB + FTXSpanHz * 0.5) / FTXSpanHz * (SrcCount - 1);
WatS0 := Trunc(frac);
if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2;
WatS1 := WatS0 + 1;
frac := frac - WatS0;
dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac;
end else
begin
WatS0 := Trunc(WatSrcF);
if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2;
WatS1 := WatS0 + 1;
frac := WatSrcF - WatS0;
dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac;
end;
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;
// Кольцевой сдвиг: новая верхняя строка занимает слот выше предыдущей.
// Полнокадровый 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;
// Строка потреблена — иначе фронтенд (OnWaterfallReady) считал бы кадр
// «несобранным» и дёргал перерисовку на каждый кадр анализатора.
FWaterfallDirty := False;
end;
// ────────────────────────────────────────────────────────────────────────────
// PaintWaterfall
// ────────────────────────────────────────────────────────────────────────────
procedure TWaterfallView.PaintWaterfall(Sender: TObject);
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)
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));
end;
if FMarkerActive then DrawMarkerLine(PB.Canvas, W, H);
end;
end.