Files
ewsdr/SpectrumView.pas
T
ew8bakandClaude Fable 5 0b4d126dac perf: zone CPU profiler (PerfLog) + phase-restructured CPU spectrum render
Инструментация (ВРЕМЕННАЯ, не мержить в main):
- PerfLog.pas: зонные таймеры, отчёт раз в 5с в stderr (core% = доля ядра),
  EWSDR_PERF=0 выкл, EWSDR_PERF_FILE=путь — дублировать в файл
- Замеры // PERF: сеть (DDC IQ/wideband), Pluto (rx_conv/tx_resamp/tx_fill),
  DSP (push_iq/fexchange/slices/tx), аудио, UI (тики/draw/paint/gl_swap,
  разбивка draw_spectrum по фазам)
- doc/PERF_PROFILING.md: карта зон, методика, выводы

Оптимизация CPU-рендера спектра (по результатам замеров на эфире):
диагноз — дорога не пиксельная работа, а чередование raw(ScanLine)/
canvas(QPainter) доступа к битмапу на Qt6 (~6 переходов за кадр, каждый
синкает весь битмап; полоса фильтра в тысячи пикселей стоила 4x дороже
полного memcpy кадра). DrawSpectrum перестроен в фазы calc->raw->canvas->raw
(2 перехода): FillBandRaw вместо Canvas.FillRect, DrawSliceFilterMarkers
разделён на raw-полосы + canvas-линии, композиты флагов и альфа-OR в
финальной raw-фазе. Кадр 8.3мс -> 6.1мс; spec_bands 1.87мс -> 0.03мс.
Альфа-OR проход на Qt6 подтверждён нужным (без него canvas-пиксели теряют
альфу и пропадают при блите), сам цикл ~0.08мс — оставлен; тумблер
EWSDR_ALPHA_FIX=0 только для экспериментов. Кисть перед AGC-подписями
теперь ставится явно (была остаточной после перестройки фаз).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:07:36 +03:00

1304 lines
57 KiB
ObjectPascal

unit SpectrumView;
{
SpectrumView.pas — рендеринг спектрограммы.
TSpectrumView инкапсулирует рисование спектра и агрегирует дочерние
компоненты: TWaterfallView, TSMeterView, TRulerView. MainForm создаёт один
экземпляр TSpectrumView и работает с ним единообразно: свойства/методы
водопада, S-метра и линейки делегируются в соответствующие sub-view.
Зависимости: нет обратной зависимости на MainForm.
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
uses
Classes, SysUtils, Graphics, ExtCtrls, Controls, Math,
AppTheme,
AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay,
WaterfallView, SMeterView, RulerView,
PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры
const
SV_CLR_BG = TColor($00101010);
type
TSpectrumView = class
protected
// ── Off-screen bitmaps ────────────────────────────────────────────────────
FSpectrumBitmap: TBitmap;
FGridBitmap: TBitmap;
FGridBitmapW: Integer;
FGridBitmapH: Integer;
FFMGridLastCenter: Double;
FFMGridLastStepHz: Double;
FFMGridLastSpan: Double;
FSpPts: array of TPoint;
FSpPtsLen: Integer;
FFillSpectrum: Boolean;
// ── Тема ─────────────────────────────────────────────────────────────────
FTheme: TAppTheme;
FLightTheme: Boolean;
// ── Radio state ───────────────────────────────────────────────────────────
FVfoA: Double;
FVfoB: Double;
FActiveVfo: Integer;
FCenterFreq: Double;
FSpanHz: Double;
FMode: Integer;
FFilterBW: Integer;
FAGCTop: Integer;
FAGCThresh: Double;
FAGCHangLevel: Double;
FWDSPReady: Boolean;
// ── Display settings ──────────────────────────────────────────────────────
FSpecRefLevel: Double;
FSpecRange: Double;
FSpecGridStep: Double;
FFMGridStepHz: Double;
// ── TX overlay ────────────────────────────────────────────────────────────
FTXMode: Boolean;
FTXFreq: Double;
FTXSpanHz: Double;
FTXVfoIndex: Integer;
FTXOverlay: Boolean;
// ── ADC overload overlay ─────────────────────────────────────────────────
FADCOverloadVisible: Boolean;
FSampleRateOverlay: TSampleRateOverlay;
FVfoOverlay: TVfoOverlay;
FSliceOverlays: TFPList; // доп. слайс-флаги (B+); не владеет (владелец MainForm)
FBandPlanOverlay: TBandPlanOverlay;
// ── Marker ────────────────────────────────────────────────────────────────
FMarkerActive: Boolean;
FMarkerX: Integer;
// ── QO-100 beacon markers (диагностика лока) ──────────────────────────────
FBeaconMarkActive: Boolean;
FBeaconRefFreq: Double; // опорная частота маяка (где ДОЛЖЕН быть)
FBeaconTrkFreq: Double; // отслеживаемый центроид маяка сейчас
// узкий фильтр-маркер декодера (наведение пользователем)
FBeaconDecActive: Boolean;
FBeaconDecFreq: Double;
FBeaconDecHalf: Double; // полуширина фильтра, Гц
// ── Spectrum buffer ───────────────────────────────────────────────────────
FSpectrumBuf: array[0..1023] of Single;
FSpectrumBufCount: Integer;
FSpectrumDirty: Boolean;
// ── Sub-views ─────────────────────────────────────────────────────────────
FWaterfall: TWaterfallView;
FSMeter: TSMeterView;
FRuler: TRulerView;
// ── Приватные методы рендеринга ───────────────────────────────────────────
function ActiveVfoFreq: Double;
function ScaleX(X, Total, Width: Integer): Integer;
procedure SetFMGridStepHz(V: Double);
procedure SetCenterFreq(V: Double);
procedure SetSpanHz(V: Double);
procedure SetVfoA(V: Double);
procedure SetVfoB(V: Double);
procedure SetActiveVfo(V: Integer);
procedure SetTXMode(V: Boolean);
procedure SetTXFreq(V: Double);
procedure SetTXSpanHz(V: Double);
procedure SetMarkerActive(V: Boolean);
procedure SetMarkerX(V: Integer);
procedure SetFillSpectrum(V: Boolean);
procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
procedure DrawMarkerLine(C: TCanvas; W, H: Integer);
// Слайс-маркеры разделены на raw-часть (полосы, ScanLine) и canvas-часть
// (линии/буквы): DrawSpectrum группирует raw- и canvas-операции, чтобы
// не гонять Qt6-битмап между режимами доступа (дорогая синхронизация).
procedure DrawSliceFilterBands(W, H: Integer);
procedure DrawSliceFilterLines(C: TCanvas; W, H: Integer);
procedure DrawBandLetter(C: TCanvas; X1, X2: Integer; L: Char; Clr: TColor);
procedure DrawBeaconMarkers(C: TCanvas; W, H: Integer);
procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
procedure FillBandRaw(X1, X2, H: Integer; Color: TColor);
procedure CopyGridToSpectrum(W, H: Integer);
procedure DrawADCOverloadOverlay(C: TCanvas; W, H: Integer);
procedure CalcFilterBandX(VfoFreq: Double; W: Integer;
out X1, X2, VfoX: Integer);
procedure CalcFilterBandXFor(VfoFreq: Double; Mode, BW, W: Integer;
out X1, X2, VfoX: Integer);
function GetWaterfallDirty: Boolean;
procedure SetWaterfallDirty(V: Boolean);
// Waterfall sub-view delegating accessors
function GetWfAGCEnabled: Boolean;
procedure SetWfAGCEnabled(V: Boolean);
function GetWfNFEnabled: Boolean;
procedure SetWfNFEnabled(V: Boolean);
function GetWfManualHigh: Double;
procedure SetWfManualHigh(V: Double);
function GetWfManualLow: Double;
procedure SetWfManualLow(V: Double);
function GetWfAGCOffset: Double;
procedure SetWfAGCOffset(V: Double);
function GetWfFrameInterval: Integer;
procedure SetWfFrameInterval(V: Integer);
procedure SetPbWaterfall(V: TControl);
// SMeter sub-view delegating accessors
function GetLastSMeter: Double;
procedure SetLastSMeter(V: Double);
function GetSMeterPeak: Double;
procedure SetSMeterPeak(V: Double);
function GetSMeterMin: Double;
procedure SetSMeterMin(V: Double);
function GetLastFwdW: Double;
procedure SetLastFwdW(V: Double);
function GetLastSWR: Double;
procedure SetLastSWR(V: Double);
function GetPAMaxPower: Double;
procedure SetPAMaxPower(V: Double);
function GetTransmitting: Boolean;
procedure SetTransmitting(V: Boolean);
procedure SetPbSMeterRight(V: TPaintBox);
// Ruler sub-view delegating accessor
procedure SetPbRuler(V: TPaintBox);
public
constructor Create;
destructor Destroy; override;
// ── Состояние радио ───────────────────────────────────────────────────────
property VfoA: Double read FVfoA write SetVfoA;
property VfoB: Double read FVfoB write SetVfoB;
property ActiveVfo: Integer read FActiveVfo write SetActiveVfo;
property CenterFreq: Double read FCenterFreq write SetCenterFreq;
property SpanHz: Double read FSpanHz write SetSpanHz;
property Mode: Integer read FMode write FMode;
property FilterBW: Integer read FFilterBW write FFilterBW;
property AGCTop: Integer read FAGCTop write FAGCTop;
property AGCThresh: Double write FAGCThresh;
property AGCHangLevel: Double write FAGCHangLevel;
property WDSPReady: Boolean read FWDSPReady write FWDSPReady;
// ── Водопад ───────────────────────────────────────────────────────────────
property WfAGCEnabled: Boolean read GetWfAGCEnabled write SetWfAGCEnabled;
property WfNFEnabled: Boolean read GetWfNFEnabled write SetWfNFEnabled;
property WfManualHigh: Double read GetWfManualHigh write SetWfManualHigh;
property WfManualLow: Double read GetWfManualLow write SetWfManualLow;
property WfAGCOffset: Double read GetWfAGCOffset write SetWfAGCOffset;
// ── Отображение ───────────────────────────────────────────────────────────
property SpecRefLevel: Double read FSpecRefLevel write FSpecRefLevel;
property SpecRange: Double read FSpecRange write FSpecRange;
property SpecGridStep: Double read FSpecGridStep write FSpecGridStep;
property FMGridStepHz: Double read FFMGridStepHz write SetFMGridStepHz;
// ── TX overlay ────────────────────────────────────────────────────────────
property TXMode: Boolean read FTXMode write SetTXMode;
property TXFreq: Double read FTXFreq write SetTXFreq;
property TXSpanHz: Double read FTXSpanHz write SetTXSpanHz;
property TXVfoIndex: Integer read FTXVfoIndex write FTXVfoIndex;
property TXOverlay: Boolean read FTXOverlay write FTXOverlay;
property ADCOverloadVisible: Boolean read FADCOverloadVisible write FADCOverloadVisible;
// ── Маркер ────────────────────────────────────────────────────────────────
property MarkerActive: Boolean read FMarkerActive write SetMarkerActive;
property MarkerX: Integer read FMarkerX write SetMarkerX;
// QO-100 beacon-маркеры: опорная частота (зелёная) + позиция трекера (оранж).
// Спектр перерисовывается каждый кадр, поэтому отдельный invalidate не нужен.
procedure SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double);
procedure SetBeaconDecMarker(Active: Boolean; FreqHz, HalfHz: Double);
// ── S-метр ────────────────────────────────────────────────────────────────
property LastSMeter: Double read GetLastSMeter write SetLastSMeter;
property SMeterPeak: Double read GetSMeterPeak write SetSMeterPeak;
property SMeterMin: Double read GetSMeterMin write SetSMeterMin;
property LastFwdW: Double read GetLastFwdW write SetLastFwdW;
property LastSWR: Double read GetLastSWR write SetLastSWR;
property PAMaxPower: Double read GetPAMaxPower write SetPAMaxPower;
property Transmitting: Boolean read GetTransmitting write SetTransmitting;
// ── Флаги обновления ──────────────────────────────────────────────────────
property SpectrumDirty: Boolean read FSpectrumDirty write FSpectrumDirty;
property FillSpectrum: Boolean read FFillSpectrum write SetFillSpectrum;
property WaterfallDirty: Boolean read GetWaterfallDirty write SetWaterfallDirty;
property WfFrameInterval: Integer read GetWfFrameInterval write SetWfFrameInterval;
// ── Ссылки на PaintBox ────────────────────────────────────────────────────
property PbWaterfall: TControl write SetPbWaterfall;
property PbRuler: TPaintBox write SetPbRuler;
property PbSMeterRight: TPaintBox write SetPbSMeterRight;
property SampleRateOverlay: TSampleRateOverlay read FSampleRateOverlay write FSampleRateOverlay;
property VfoOverlay: TVfoOverlay read FVfoOverlay write FVfoOverlay;
// Доп. слайс-флаги (B+): рисуются поверх спектра после главного оверлея.
procedure AddSliceOverlay(O: TVfoOverlay);
procedure RemoveSliceOverlay(O: TVfoOverlay);
function SliceOverlayCount: Integer;
function SliceOverlayAt(Index: Integer): TVfoOverlay;
property BandPlanOverlay: TBandPlanOverlay read FBandPlanOverlay write FBandPlanOverlay;
// ── Данные от DSP ─────────────────────────────────────────────────────────
procedure SetSpectrumData(const Pixels: array of Single; Count: Integer); virtual;
procedure SetWaterfallData(const Pixels: array of Single; Count: Integer); virtual;
// ── Рендеринг ─────────────────────────────────────────────────────────────
procedure DrawSpectrum; virtual;
procedure DrawWaterfall; virtual;
procedure DrawRuler; virtual;
// ── Управление размером bitmap ────────────────────────────────────────────
procedure SetSpectrumBitmapSize(W, H: Integer); virtual;
procedure SetWaterfallBitmapSize(W, H: Integer); virtual;
procedure SetRulerSize(W, H: Integer); virtual;
function SpectrumBitmapWidth: Integer; virtual;
// ── Paint-обработчики ────────────────────────────────────────────────────
procedure PaintSpectrum(Sender: TObject); virtual;
procedure PaintWaterfall(Sender: TObject); virtual;
procedure PaintRuler(Sender: TObject); virtual;
procedure PaintSMeterRight(Sender: TObject); virtual;
// ── Утилиты ───────────────────────────────────────────────────────────────
procedure InvalidateGridCache; virtual;
procedure InvalidateOverlayCache; virtual;
// Инвалидация ТОЛЬКО текстуры главного VFO-флага (без band/samplerate/слайсов).
// Нужна для дешёвого обновления S-метра главного флага в GL-вьюхе.
procedure InvalidateVfoOverlay; virtual;
procedure SetTheme(const T: TAppTheme); virtual;
procedure InvalidateRulerCache; virtual;
procedure ResetSpectrumBuf; virtual;
procedure ResetWfAvgBuf; virtual;
procedure FillDemoSpectrum; virtual;
function NeedsRulerRedraw: Boolean; virtual;
end;
implementation
var
// ВРЕМЕННО (perf/cpu-profiling): EWSDR_ALPHA_FIX=0 → пропустить альфа-OR
// проход в DrawSpectrum (проверка, нужен ли он на Qt6).
GSkipAlphaFix: Boolean = False;
// ════════════════════════════════════════════════════════════════════════════
// Вспомогательные функции
// ════════════════════════════════════════════════════════════════════════════
function FormatFreqSV(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;
function MixColorBGR(C1, C2: TColor; T: Double): TColor;
var
B1, G1, R1, B2, G2, R2, B, G, R: Integer;
begin
if T < 0.0 then T := 0.0 else if T > 1.0 then T := 1.0;
B1 := (C1 shr 16) and $FF; G1 := (C1 shr 8) and $FF; R1 := C1 and $FF;
B2 := (C2 shr 16) and $FF; G2 := (C2 shr 8) and $FF; R2 := C2 and $FF;
B := Round(B1 + (B2-B1)*T); G := Round(G1 + (G2-G1)*T); R := Round(R1 + (R2-R1)*T);
Result := TColor((B shl 16) or (G shl 8) or R);
end;
procedure PaintVerticalGradient(C: TCanvas; W, H: Integer; TopColor, BottomColor: TColor);
var Y: Integer; T: Double;
begin
if (W <= 0) or (H <= 0) then Exit;
C.Pen.Style := psClear; C.Brush.Style := bsSolid;
for Y := 0 to H - 1 do
begin
T := Y / Max(1, H - 1);
C.Brush.Color := MixColorBGR(TopColor, BottomColor, T);
C.FillRect(Rect(0, Y, W, Y + 1));
end;
C.Pen.Style := psSolid;
end;
// ════════════════════════════════════════════════════════════════════════════
// TSpectrumView
// ════════════════════════════════════════════════════════════════════════════
constructor TSpectrumView.Create;
begin
inherited Create;
FSpectrumBitmap := TBitmap.Create;
FSpectrumBitmap.PixelFormat := pf32bit;
FGridBitmap := TBitmap.Create;
FGridBitmap.PixelFormat := pf32bit;
FGridBitmapW := 0; FGridBitmapH := 0;
FFMGridLastCenter := -1.0; FFMGridLastStepHz := -1.0; FFMGridLastSpan := -1.0;
FFMGridStepHz := 0.0;
FSpPtsLen := 0;
// Spectrum display defaults
FSpecRefLevel := -20.0;
FSpecRange := 110.0;
FSpecGridStep := 10.0;
FSpanHz := 192000;
// TX overlay defaults
FTXMode := False;
FTXFreq := 0.0;
FTXSpanHz := 192000.0;
FTXVfoIndex := 0;
FTXOverlay := False;
FADCOverloadVisible := False;
FSpectrumBufCount := 1024;
FSpectrumDirty := True;
FLightTheme := False;
FFillSpectrum := True;
FTheme := DarkTheme;
// Sub-views
FWaterfall := TWaterfallView.Create;
FSMeter := TSMeterView.Create;
FRuler := TRulerView.Create;
FSliceOverlays := TFPList.Create;
ResetSpectrumBuf;
end;
destructor TSpectrumView.Destroy;
begin
FSpectrumBitmap.Free;
FGridBitmap.Free;
FWaterfall.Free;
FSMeter.Free;
FRuler.Free;
FSliceOverlays.Free; // не владеет флагами — только список
inherited;
end;
procedure TSpectrumView.AddSliceOverlay(O: TVfoOverlay);
begin
if (O <> nil) and (FSliceOverlays.IndexOf(O) < 0) then FSliceOverlays.Add(O);
end;
procedure TSpectrumView.RemoveSliceOverlay(O: TVfoOverlay);
begin
FSliceOverlays.Remove(O);
end;
function TSpectrumView.SliceOverlayCount: Integer;
begin
Result := FSliceOverlays.Count;
end;
function TSpectrumView.SliceOverlayAt(Index: Integer): TVfoOverlay;
begin
Result := TVfoOverlay(FSliceOverlays[Index]);
end;
// ────────────────────────────────────────────────────────────────────────────
// Setters with cascade to sub-views
// ────────────────────────────────────────────────────────────────────────────
procedure TSpectrumView.SetCenterFreq(V: Double);
begin
FCenterFreq := V;
FWaterfall.CenterFreq := V;
FRuler.CenterFreq := V;
end;
procedure TSpectrumView.SetSpanHz(V: Double);
begin
FSpanHz := V;
FWaterfall.SpanHz := V;
FRuler.SpanHz := V;
end;
procedure TSpectrumView.SetVfoA(V: Double);
begin
FVfoA := V;
FRuler.VfoA := V;
end;
procedure TSpectrumView.SetVfoB(V: Double);
begin
FVfoB := V;
FRuler.VfoB := V;
end;
procedure TSpectrumView.SetActiveVfo(V: Integer);
begin
FActiveVfo := V;
FRuler.ActiveVfo := V;
end;
procedure TSpectrumView.SetTXMode(V: Boolean);
begin
FTXMode := V;
FWaterfall.TXMode := V;
end;
procedure TSpectrumView.SetTXFreq(V: Double);
begin
FTXFreq := V;
FWaterfall.TXFreq := V;
end;
procedure TSpectrumView.SetTXSpanHz(V: Double);
begin
FTXSpanHz := V;
FWaterfall.TXSpanHz := V;
end;
procedure TSpectrumView.SetMarkerActive(V: Boolean);
begin
FMarkerActive := V;
FWaterfall.MarkerActive := V;
end;
procedure TSpectrumView.SetMarkerX(V: Integer);
begin
FMarkerX := V;
FWaterfall.MarkerX := V;
end;
procedure TSpectrumView.SetFillSpectrum(V: Boolean);
begin
if FFillSpectrum = V then Exit;
FFillSpectrum := V;
FSpectrumDirty := True;
end;
function TSpectrumView.GetWaterfallDirty: Boolean;
begin
Result := FWaterfall.WaterfallDirty;
end;
procedure TSpectrumView.SetWaterfallDirty(V: Boolean);
begin
FWaterfall.WaterfallDirty := V;
end;
function TSpectrumView.GetWfAGCEnabled: Boolean; begin Result := FWaterfall.WfAGCEnabled; end;
procedure TSpectrumView.SetWfAGCEnabled(V: Boolean); begin FWaterfall.WfAGCEnabled := V; end;
function TSpectrumView.GetWfNFEnabled: Boolean; begin Result := FWaterfall.WfNFEnabled; end;
procedure TSpectrumView.SetWfNFEnabled(V: Boolean); begin FWaterfall.WfNFEnabled := V; end;
function TSpectrumView.GetWfManualHigh: Double; begin Result := FWaterfall.WfManualHigh; end;
procedure TSpectrumView.SetWfManualHigh(V: Double); begin FWaterfall.WfManualHigh := V; end;
function TSpectrumView.GetWfManualLow: Double; begin Result := FWaterfall.WfManualLow; end;
procedure TSpectrumView.SetWfManualLow(V: Double); begin FWaterfall.WfManualLow := V; end;
function TSpectrumView.GetWfAGCOffset: Double; begin Result := FWaterfall.WfAGCOffset; end;
procedure TSpectrumView.SetWfAGCOffset(V: Double); begin FWaterfall.WfAGCOffset := V; end;
function TSpectrumView.GetWfFrameInterval: Integer; begin Result := FWaterfall.WfFrameInterval; end;
procedure TSpectrumView.SetWfFrameInterval(V: Integer); begin FWaterfall.WfFrameInterval := V; end;
procedure TSpectrumView.SetPbWaterfall(V: TControl); begin FWaterfall.PbWaterfall := V; end;
function TSpectrumView.GetLastSMeter: Double; begin Result := FSMeter.LastSMeter; end;
procedure TSpectrumView.SetLastSMeter(V: Double); begin FSMeter.LastSMeter := V; end;
function TSpectrumView.GetSMeterPeak: Double; begin Result := FSMeter.SMeterPeak; end;
procedure TSpectrumView.SetSMeterPeak(V: Double); begin FSMeter.SMeterPeak := V; end;
function TSpectrumView.GetSMeterMin: Double; begin Result := FSMeter.SMeterMin; end;
procedure TSpectrumView.SetSMeterMin(V: Double); begin FSMeter.SMeterMin := V; end;
function TSpectrumView.GetLastFwdW: Double; begin Result := FSMeter.LastFwdW; end;
procedure TSpectrumView.SetLastFwdW(V: Double); begin FSMeter.LastFwdW := V; end;
function TSpectrumView.GetLastSWR: Double; begin Result := FSMeter.LastSWR; end;
procedure TSpectrumView.SetLastSWR(V: Double); begin FSMeter.LastSWR := V; end;
function TSpectrumView.GetPAMaxPower: Double; begin Result := FSMeter.PAMaxPower; end;
procedure TSpectrumView.SetPAMaxPower(V: Double); begin FSMeter.PAMaxPower := V; end;
function TSpectrumView.GetTransmitting: Boolean; begin Result := FSMeter.Transmitting; end;
procedure TSpectrumView.SetTransmitting(V: Boolean); begin FSMeter.Transmitting := V; end;
procedure TSpectrumView.SetPbSMeterRight(V: TPaintBox); begin FSMeter.PbSMeterRight := V; end;
procedure TSpectrumView.SetPbRuler(V: TPaintBox); begin FRuler.PbRuler := V; end;
// ────────────────────────────────────────────────────────────────────────────
// Приватные вспомогательные методы
// ────────────────────────────────────────────────────────────────────────────
function TSpectrumView.ActiveVfoFreq: Double;
begin
if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB;
end;
function TSpectrumView.ScaleX(X, Total, Width: Integer): Integer;
begin
if Total = 0 then Result := 0
else Result := Round(X / Total * Width);
end;
procedure TSpectrumView.SetFMGridStepHz(V: Double);
begin
if Abs(FFMGridStepHz - V) > 0.5 then
begin
FFMGridStepHz := V;
FRuler.FMGridStepHz := V;
InvalidateGridCache;
FRuler.InvalidateRulerCache;
end;
end;
procedure TSpectrumView.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 := FormatFreqSV(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;
procedure TSpectrumView.SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double);
begin
FBeaconMarkActive := Active;
FBeaconRefFreq := RefHz;
FBeaconTrkFreq := TrkHz;
end;
procedure TSpectrumView.SetBeaconDecMarker(Active: Boolean; FreqHz, HalfHz: Double);
begin
FBeaconDecActive := Active;
FBeaconDecFreq := FreqHz;
FBeaconDecHalf := HalfHz;
end;
procedure TSpectrumView.DrawBeaconMarkers(C: TCanvas; W, H: Integer);
// Две вертикали: опорная частота маяка (зелёная пунктир) и отслеживаемый
// центроид (оранжевая сплошная). Расхождение видно глазом → понятно, сел ли
// контур на пик маяка или на шум.
procedure Vline(FreqHz: Double; Col: TColor; Style: TPenStyle);
var x: Integer;
begin
if FSpanHz <= 0 then Exit;
x := Round((FreqHz - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
if (x < 0) or (x >= W) then Exit;
C.Pen.Color := Col; C.Pen.Width := 1; C.Pen.Style := Style;
C.MoveTo(x, 0); C.LineTo(x, H);
end;
begin
if FBeaconMarkActive then
begin
Vline(FBeaconRefFreq, clLime, psDot); // где маяк ДОЛЖЕН быть
Vline(FBeaconTrkFreq, TColor($000AA5FF), psSolid); // отслеживаемый центроид (оранж)
end;
// Узкий фильтр-маркер декодера: грани полосы + центральная линия. Три линии без
// полупрозрачной заливки (как в GL-рендере) — единый вид и дешевле для CPU.
if FBeaconDecActive and (FSpanHz > 0) then
begin
Vline(FBeaconDecFreq - FBeaconDecHalf, TColor($0020D0FF), psSolid); // грань фильтра
Vline(FBeaconDecFreq + FBeaconDecHalf, TColor($0020D0FF), psSolid);
Vline(FBeaconDecFreq, TColor($0020D0FF), psSolid); // центр наведения
end;
C.Pen.Style := psSolid;
end;
procedure TSpectrumView.BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
var
Y, X, InvA: Integer;
Row: PByte;
W: Integer;
begin
if (FSpectrumBitmap = nil) or (Alpha = 0) then Exit;
W := FSpectrumBitmap.Width;
if X1 < 0 then X1 := 0;
if X2 > W then X2 := W;
if X2 <= X1 then Exit;
InvA := 255 - Alpha;
FSpectrumBitmap.BeginUpdate(False);
try
for Y := 0 to H - 1 do
begin
Row := PByte(FSpectrumBitmap.ScanLine[Y]);
if Row = nil then Continue;
Inc(Row, X1 * 4);
for X := X1 to X2 - 1 do
begin
{$IFDEF DARWIN}
Row[1] := Byte((Alpha * R + InvA * Row[1]) div 255);
Row[2] := Byte((Alpha * G + InvA * Row[2]) div 255);
Row[3] := Byte((Alpha * B + InvA * Row[3]) div 255);
{$ELSE}
Row[0] := Byte((Alpha * B + InvA * Row[0]) div 255);
Row[1] := Byte((Alpha * G + InvA * Row[1]) div 255);
Row[2] := Byte((Alpha * R + InvA * Row[2]) div 255);
{$ENDIF}
Inc(Row, 4);
end;
end;
finally
FSpectrumBitmap.EndUpdate(False);
end;
end;
// Непрозрачная вертикальная полоса через raw-доступ (ScanLine) — замена
// Canvas.FillRect для полосы фильтра в DrawSpectrum: полоса рисуется в
// raw-фазе (между memcpy сетки и градиентом), а FillRect дёргал бы Canvas
// и форсил лишнюю синхронизацию битмапа на Qt6.
procedure TSpectrumView.FillBandRaw(X1, X2, H: Integer; Color: TColor);
var
Y, W: Integer;
Row: PLongWord;
Px: LongWord;
R, G, B: Byte;
begin
if FSpectrumBitmap = nil then Exit;
W := FSpectrumBitmap.Width;
if X1 < 0 then X1 := 0;
if X2 > W then X2 := W;
if X2 <= X1 then Exit;
R := Color and $FF; G := (Color shr 8) and $FF; B := (Color shr 16) and $FF;
{$IFDEF DARWIN}
// ARGB в памяти (см. BlendBand): байт 0 = альфа, дальше R,G,B.
Px := LongWord($FF) or (LongWord(R) shl 8) or (LongWord(G) shl 16) or
(LongWord(B) shl 24);
{$ELSE}
// BGRA в памяти: байты B,G,R,A.
Px := LongWord(B) or (LongWord(G) shl 8) or (LongWord(R) shl 16) or $FF000000;
{$ENDIF}
FSpectrumBitmap.BeginUpdate(False);
try
for Y := 0 to H - 1 do
begin
Row := PLongWord(FSpectrumBitmap.ScanLine[Y]);
if Row = nil then Continue;
Inc(Row, X1);
FillDWord(Row^, X2 - X1, Px);
end;
finally
FSpectrumBitmap.EndUpdate(False);
end;
end;
// Маркер фильтра каждого слайса (B+) на спектре — полупрозрачная полоса
// пропускания + края + центральная несущая. Янтарный цвет, как в GL-вьюхе
// (SpectrumViewOpengl.DrawSliceFilterMarkers). Полосы (raw ScanLine-блендинг)
// и линии/буквы (canvas) разнесены по разным процедурам — их зовут из разных
// фаз DrawSpectrum, чтобы не чередовать режимы доступа к битмапу.
procedure TSpectrumView.DrawSliceFilterBands(W, H: Integer);
const
SLICE_R = $F0; SLICE_G = $A8; SLICE_B = $10; // янтарный (совпадает с SLICE_CLR в GL)
var
i, SX1, SX2, SVfoX: Integer;
O: TVfoOverlay;
begin
for i := 0 to FSliceOverlays.Count - 1 do
begin
O := TVfoOverlay(FSliceOverlays[i]);
if (O = nil) or (not O.Visible) then Continue;
CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, SX1, SX2, SVfoX);
if SX2 > SX1 then BlendBand(SX1, SX2, H, SLICE_R, SLICE_G, SLICE_B, 60);
end;
end;
procedure TSpectrumView.DrawSliceFilterLines(C: TCanvas; W, H: Integer);
const
SLICE_CLR = TColor($0010A8F0);
var
i, SX1, SX2, SVfoX: Integer;
O: TVfoOverlay;
begin
for i := 0 to FSliceOverlays.Count - 1 do
begin
O := TVfoOverlay(FSliceOverlays[i]);
if (O = nil) or (not O.Visible) then Continue;
CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, SX1, SX2, SVfoX);
C.Pen.Color := SLICE_CLR; C.Pen.Width := 1; C.Pen.Style := psSolid;
C.MoveTo(SX1, 0); C.LineTo(SX1, H);
C.MoveTo(SX2, 0); C.LineTo(SX2, H);
C.Pen.Width := 2;
C.MoveTo(SVfoX, 0); C.LineTo(SVfoX, H - 12);
C.Pen.Width := 1;
// Буква слайса внутри полосы фильтра сверху — привязка полосы к флагу,
// цвет = цвет слайса (совпадает с бейджем во флаге).
DrawBandLetter(C, SX1, SX2, O.SliceLetter, SliceColor(O.SliceLetter));
end;
end;
// Буква слайса (A/B/…) по центру полосы фильтра, у самого верха — чтобы
// понять, какому флагу принадлежит полоса, даже когда флаги стоят рядом.
procedure TSpectrumView.DrawBandLetter(C: TCanvas; X1, X2: Integer;
L: Char; Clr: TColor);
var
cx: Integer;
begin
if (L < 'A') or (L > 'Z') then Exit;
cx := (X1 + X2) div 2;
C.Font.Name := 'Courier New'; C.Font.Size := 8; C.Font.Style := [fsBold];
C.Font.Color := Clr;
C.Brush.Style := bsClear;
C.TextOut(cx - C.TextWidth(L) div 2, 0, L);
C.Brush.Style := bsSolid;
C.Font.Style := [];
end;
procedure TSpectrumView.CopyGridToSpectrum(W, H: Integer);
var
Y: Integer;
Src, Dst: PByte;
begin
if (FSpectrumBitmap = nil) or (FGridBitmap = nil) then Exit;
if (W <= 0) or (H <= 0) then Exit;
{$IFDEF DARWIN}
// On macOS/Cocoa, Canvas writes into a CGBitmapContext; ScanLine reads the
// raw backing buffer which is not synced until BeginUpdate is called on the
// source bitmap. Canvas.Draw goes through CoreGraphics and sees the live
// CGContext, so use it instead of the direct ScanLine copy.
FSpectrumBitmap.Canvas.Draw(0, 0, FGridBitmap);
{$ELSE}
FSpectrumBitmap.BeginUpdate(False);
try
for Y := 0 to H - 1 do
begin
Src := PByte(FGridBitmap.ScanLine[Y]);
Dst := PByte(FSpectrumBitmap.ScanLine[Y]);
if (Src <> nil) and (Dst <> nil) then
Move(Src^, Dst^, W * 4);
end;
finally
FSpectrumBitmap.EndUpdate(False);
end;
{$ENDIF}
end;
procedure TSpectrumView.DrawADCOverloadOverlay(C: TCanvas; W, H: Integer);
begin
if not FADCOverloadVisible then Exit;
DrawAlertOverlay(FSpectrumBitmap, C, W, H, 'ADC OVERLOAD', 'Input clipping detected');
end;
procedure TSpectrumView.CalcFilterBandX(VfoFreq: Double; W: Integer;
out X1, X2, VfoX: Integer);
begin
CalcFilterBandXFor(VfoFreq, FMode, FFilterBW, W, X1, X2, VfoX);
end;
// То же, но для произвольного режима/полосы (маркеры слайсов).
procedure TSpectrumView.CalcFilterBandXFor(VfoFreq: Double; Mode, BW, W: Integer;
out X1, X2, VfoX: Integer);
var
Lo_Hz, Hi_Hz, Half: Double;
begin
Half := BW / 2;
VfoX := Round((VfoFreq - FCenterFreq + FSpanHz / 2) / FSpanHz * W);
case Mode of
0: begin Lo_Hz := -BW; Hi_Hz := -100; end;
1: begin Lo_Hz := 100; Hi_Hz := BW; end;
else begin Lo_Hz := -Half; Hi_Hz := Half; end;
end;
X1 := Max(0, Min(W - 1, VfoX + Round(Lo_Hz / FSpanHz * W)));
X2 := Max(0, Min(W - 1, VfoX + Round(Hi_Hz / FSpanHz * W)));
end;
procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
var
Row, Col, Alpha, YMin, Off: Integer;
RowPtr: PByte;
GradB, GradG, GradR: Byte;
begin
if FSpectrumBitmap = nil then Exit;
if (W <= 0) or (H <= 0) then Exit;
YMin := H;
for Col := 0 to W - 1 do
if SpPts[Col].Y < YMin then YMin := SpPts[Col].Y;
if YMin >= H then Exit;
FSpectrumBitmap.BeginUpdate(False);
for Row := YMin to H - 1 do
begin
Alpha := Round((1.0 - Sqr((Row - YMin) / Max(1.0, H - YMin - 1.0))) * 200);
if Alpha <= 0 then Continue;
if Alpha > 200 then Alpha := 200;
GradB := Byte(FTheme.SpecGradB * Alpha div 200);
GradG := Byte(FTheme.SpecGradG * Alpha div 200);
GradR := Byte(FTheme.SpecGradR * Alpha div 200);
RowPtr := PByte(FSpectrumBitmap.ScanLine[Row]);
for Col := 0 to W - 1 do
if SpPts[Col].Y <= Row then
begin
Off := Col * 4;
{$IFDEF DARWIN}
RowPtr[Off+1] := (GradR * Alpha + RowPtr[Off+1] * (200 - Alpha)) div 200;
RowPtr[Off+2] := (GradG * Alpha + RowPtr[Off+2] * (200 - Alpha)) div 200;
RowPtr[Off+3] := (GradB * Alpha + RowPtr[Off+3] * (200 - Alpha)) div 200;
{$ELSE}
RowPtr[Off] := (GradB * Alpha + RowPtr[Off] * (200 - Alpha)) div 200;
RowPtr[Off+1] := (GradG * Alpha + RowPtr[Off+1] * (200 - Alpha)) div 200;
RowPtr[Off+2] := (GradR * Alpha + RowPtr[Off+2] * (200 - Alpha)) div 200;
{$ENDIF}
end;
end;
FSpectrumBitmap.EndUpdate(False);
end;
// ────────────────────────────────────────────────────────────────────────────
// Публичные методы данных
// ────────────────────────────────────────────────────────────────────────────
procedure TSpectrumView.SetSpectrumData(const Pixels: array of Single; Count: Integer);
var i, N: Integer;
begin
N := Min(Count, 1024);
for i := 0 to N - 1 do FSpectrumBuf[i] := Pixels[i];
FSpectrumBufCount := N;
FSpectrumDirty := True;
end;
procedure TSpectrumView.SetWaterfallData(const Pixels: array of Single; Count: Integer);
begin
FWaterfall.SetWaterfallData(Pixels, Count);
end;
// ────────────────────────────────────────────────────────────────────────────
// Размеры bitmap
// ────────────────────────────────────────────────────────────────────────────
procedure TSpectrumView.SetSpectrumBitmapSize(W, H: Integer);
begin
if (W <= 0) or (H <= 0) then Exit;
FSpectrumBitmap.SetSize(W, H);
FSpectrumBitmap.Canvas.Brush.Color := FTheme.BG;
FSpectrumBitmap.Canvas.FillRect(Rect(0, 0, W, H));
InvalidateGridCache;
FSpPtsLen := 0;
end;
procedure TSpectrumView.SetWaterfallBitmapSize(W, H: Integer);
begin
FWaterfall.SetWaterfallBitmapSize(W, H);
end;
procedure TSpectrumView.SetRulerSize(W, H: Integer);
begin
FRuler.SetRulerSize(W, H);
end;
function TSpectrumView.SpectrumBitmapWidth: Integer;
begin
Result := FSpectrumBitmap.Width;
end;
// ────────────────────────────────────────────────────────────────────────────
// Утилиты
// ────────────────────────────────────────────────────────────────────────────
procedure TSpectrumView.InvalidateGridCache;
begin
FGridBitmapW := 0; FGridBitmapH := 0;
FSpectrumDirty := True;
end;
procedure TSpectrumView.InvalidateOverlayCache;
begin
FSpectrumDirty := True;
end;
procedure TSpectrumView.InvalidateVfoOverlay;
begin
// База (CPU-путь) перерисовывает флаг по FCacheDirty/FMeterDirty оверлея — здесь
// достаточно пометить спектр грязным. GL-подкласс переопределяет.
FSpectrumDirty := True;
end;
procedure TSpectrumView.SetTheme(const T: TAppTheme);
begin
FTheme := T;
FLightTheme := T.BG > TColor($00808080);
InvalidateGridCache;
FWaterfall.SetTheme(T);
FSMeter.SetTheme(T);
FRuler.SetTheme(T);
end;
procedure TSpectrumView.InvalidateRulerCache;
begin
FRuler.InvalidateRulerCache;
end;
procedure TSpectrumView.ResetSpectrumBuf;
var i: Integer;
begin
for i := 0 to 1023 do FSpectrumBuf[i] := -130.0;
FWaterfall.ResetWfBuf;
end;
procedure TSpectrumView.ResetWfAvgBuf;
begin
FWaterfall.ResetWfAvgBuf;
end;
procedure TSpectrumView.FillDemoSpectrum;
var i: Integer; FreqOff, Noise, Sig: Double; W: Integer;
begin
W := FSpectrumBitmap.Width;
if W <= 0 then W := 1024;
for i := 0 to W - 1 do
begin
if W > 1 then FreqOff := (i / (W - 1) - 0.5) * FSpanHz
else FreqOff := 0;
Noise := -110 + (Random - 0.5) * 6;
if Abs(FreqOff) < 2000 then Sig := -50 - Abs(FreqOff) / 200
else Sig := -999;
FSpectrumBuf[i mod 1024] := Max(Noise, Sig);
end;
end;
function TSpectrumView.NeedsRulerRedraw: Boolean;
begin
Result := FRuler.NeedsRulerRedraw;
end;
// ────────────────────────────────────────────────────────────────────────────
// DrawSpectrum
// ────────────────────────────────────────────────────────────────────────────
procedure TSpectrumView.DrawSpectrum;
var
C, GC: TCanvas;
i, Yp, W, H, GX: Integer;
{$IFNDEF DARWIN}
RowLW: PLongWord;
{$ENDIF}
DBmin, DBmax, dB: Double;
VfoX, X1, X2: Integer;
TXVfoX, TXX1, TXX2: Integer;
AGCy, AGCHangY: Integer;
SrcF, Frac, dBv: Double;
S0, S1: Integer;
InvRange: Double;
LabelBandW, SrcCount: Integer;
GridFreqS, GridLine, pixPerStep: Double;
N, gridMult: Integer;
PerfT0, PerfTz: Int64; // PERF
begin
if FSpectrumBitmap = nil then Exit;
PerfT0 := PerfNow; // PERF
PerfTz := PerfT0; // PERF
W := FSpectrumBitmap.Width; H := FSpectrumBitmap.Height;
if (W <= 0) or (H <= 0) then Exit;
C := FSpectrumBitmap.Canvas;
DBmax := FSpecRefLevel;
DBmin := FSpecRefLevel - FSpecRange;
InvRange := 1.0 / (DBmax - DBmin);
// ── 1. Фон+сетка из кэша ──────────────────────────────────────────────────
if (FGridBitmapW <> W) or (FGridBitmapH <> H) or
((FFMGridStepHz > 0) and
((Abs(FCenterFreq - FFMGridLastCenter) > 0.5) or
(FFMGridStepHz <> FFMGridLastStepHz) or
(Abs(FSpanHz - FFMGridLastSpan) > 1.0))) then
begin
FGridBitmapW := W; FGridBitmapH := H;
FFMGridLastCenter := FCenterFreq;
FFMGridLastStepHz := FFMGridStepHz;
FFMGridLastSpan := FSpanHz;
FGridBitmap.SetSize(W, H);
GC := FGridBitmap.Canvas;
PaintVerticalGradient(GC, W, H, FTheme.SpecGradTop, FTheme.SpecGradBot);
LabelBandW := 28;
GC.Pen.Style := psClear; GC.Brush.Style := bsSolid;
GC.Brush.Color := FTheme.SpecLabelBand;
GC.FillRect(Rect(0, 0, LabelBandW, H));
GC.Pen.Style := psSolid;
GC.Font.Size := 7; GC.Font.Name := 'Courier New';
GC.Font.Color := FTheme.SpecLabelText;
GC.Brush.Style := bsClear;
dB := DBmax - FSpecGridStep;
while dB >= DBmin do
begin
Yp := Round((DBmax - dB) * InvRange * H);
GC.Pen.Color := FTheme.SpecGrid; GC.Pen.Width := 1;
GC.MoveTo(0, Yp); GC.LineTo(W-1, Yp);
GC.TextOut(1, Yp - 9, Format('%4.0f', [dB]));
dB := dB - FSpecGridStep;
end;
if FFMGridStepHz > 0 then
begin
GridFreqS := FCenterFreq - FSpanHz / 2;
pixPerStep := W * FFMGridStepHz / FSpanHz;
if pixPerStep >= 1.0 then
gridMult := Max(1, Ceil(4.0 / pixPerStep))
else
gridMult := 0;
if gridMult > 0 then
begin
N := Ceil(GridFreqS / FFMGridStepHz);
GridLine := N * FFMGridStepHz;
while GridLine <= FCenterFreq + FSpanHz / 2 + 0.5 do
begin
if (N mod gridMult) = 0 then
begin
GX := Round((GridLine - GridFreqS) / FSpanHz * W);
if (GX >= 0) and (GX < W) then
begin
GC.Pen.Color := FTheme.SpecGrid;
GC.MoveTo(GX, 0); GC.LineTo(GX, H);
end;
end;
Inc(N);
GridLine := GridLine + FFMGridStepHz;
end;
end;
end else
begin
for i := 0 to 8 do
begin
GX := ScaleX(i, 8, W);
GC.Pen.Color := FTheme.SpecGrid;
GC.MoveTo(GX, 0); GC.LineTo(GX, H);
end;
end;
end;
PerfAdd(pzUiSpecGrid, PerfTz); // PERF: ребилд кэша сетки (обычно 0)
PerfTz := PerfNow; // PERF
// ═══ Фаза 0: чистые вычисления — битмап не трогаем ═══════════════════════
// Дальше битмап обрабатывается тремя непрерывными фазами: raw (ScanLine) →
// canvas (QPainter) → raw. Чередование режимов доступа на Qt6 форсит
// дорогую синхронизацию всего битмапа на каждом переходе (замерено: полоса
// фильтра в пару тысяч пикселей стоила 4× дороже полного memcpy кадра),
// поэтому порядок операций здесь важнее «логичной» группировки по смыслу.
// Полоса фильтра (X-координаты главного и TX-VFO)
if FActiveVfo = 0 then CalcFilterBandX(FVfoA, W, X1, X2, VfoX)
else CalcFilterBandX(FVfoB, W, X1, X2, VfoX);
TXX1 := 0; TXX2 := 0; TXVfoX := 0;
if FTXOverlay and (FTXVfoIndex <> FActiveVfo) then
begin
if FTXVfoIndex = 0 then CalcFilterBandX(FVfoA, W, TXX1, TXX2, TXVfoX)
else CalcFilterBandX(FVfoB, W, TXX1, TXX2, TXVfoX);
end;
// Точки кривой спектра
if FSpPtsLen <> W + 2 then
begin SetLength(FSpPts, W + 2); FSpPtsLen := W + 2; end;
SrcCount := EnsureRange(FSpectrumBufCount, 2, 1024);
if FTXMode and (FTXSpanHz > 0) and (FSpanHz > 0) then
begin
for i := 0 to W - 1 do
begin
SrcF := FCenterFreq - FSpanHz * 0.5 + i * FSpanHz / Max(1, W - 1);
Frac := SrcF - FTXFreq;
if (Frac < -FTXSpanHz * 0.5) or (Frac > FTXSpanHz * 0.5) then
dBv := -200.0
else
begin
SrcF := (Frac + FTXSpanHz * 0.5) / FTXSpanHz * (SrcCount - 1);
S0 := Min(Trunc(SrcF), SrcCount - 1);
S1 := Min(S0 + 1, SrcCount - 1);
Frac := SrcF - S0;
dBv := FSpectrumBuf[S0] * (1.0 - Frac) + FSpectrumBuf[S1] * Frac;
end;
FSpPts[i] := Point(i, Max(0, Min(H-1, Round((DBmax - dBv) * InvRange * H))));
end;
end else
for i := 0 to W - 1 do
begin
SrcF := i * (SrcCount - 1.0) / Max(1, W - 1);
S0 := Min(Trunc(SrcF), SrcCount - 1);
S1 := Min(S0 + 1, SrcCount - 1);
Frac := SrcF - S0;
dBv := FSpectrumBuf[S0] * (1.0 - Frac) + FSpectrumBuf[S1] * Frac;
FSpPts[i] := Point(i, Max(0, Min(H-1, Round((DBmax - dBv) * InvRange * H))));
end;
FSpPts[W] := Point(W-1, H); FSpPts[W+1] := Point(0, H);
PerfAdd(pzUiSpecCurve, PerfTz); // PERF: расчёт точек кривой
PerfTz := PerfNow; // PERF
// ═══ Фаза 1: raw (ScanLine) — сетка, полосы, градиент ════════════════════
CopyGridToSpectrum(W, H);
PerfAdd(pzUiSpecGrid, PerfTz); // PERF: += полный memcpy сетки в битмап
PerfTz := PerfNow; // PERF
if FTXOverlay then
begin
if FTXVfoIndex <> FActiveVfo then
begin
if X2 > X1 then BlendBand(X1, X2, H, $30, $C0, $30, 100);
if TXX2 > TXX1 then BlendBand(TXX1, TXX2, H, $E0, $30, $30, 100);
end else
if X2 > X1 then BlendBand(X1, X2, H, $E0, $30, $30, 100);
end else
if X2 > X1 then FillBandRaw(X1, X2, H, FTheme.SpecFilter);
// Полосы фильтра слайсов (B+) — под градиентом/кривой, как в GL-вьюхе.
DrawSliceFilterBands(W, H);
PerfAdd(pzUiSpecBands, PerfTz); // PERF: полосы фильтра (raw-заливки)
PerfTz := PerfNow; // PERF
// Градиент (заливка под кривой)
if FFillSpectrum then
DrawSpectrumGradient(FSpPts, W, H);
PerfAdd(pzUiSpecCurve, PerfTz); // PERF: += градиент под кривой
PerfTz := PerfNow; // PERF
// ═══ Фаза 2: canvas (QPainter) — линии, текст, кривая, маркеры ═══════════
// Буква главного флага (A) на его полосе — только когда есть слайсы,
// иначе одиночный приём не засоряем.
if (FSliceOverlays <> nil) and (FSliceOverlays.Count > 0) and
(FActiveVfo = 0) and Assigned(FVfoOverlay) then
DrawBandLetter(C, X1, X2, FVfoOverlay.SliceLetter, SliceColor(FVfoOverlay.SliceLetter));
// Края/несущие/буквы слайсов (raw-полосы уже нарисованы в фазе 1).
DrawSliceFilterLines(C, W, H);
// AGC линии. Кисть выставляем явно: TextOut подписей рисует фон кистью, и
// до перестройки фаз здесь всегда была bsSolid/SpecFilter (от FillRect
// полосы) — сохраняем тот же вид, не полагаясь на остаточное состояние.
C.Brush.Color := FTheme.SpecFilter; C.Brush.Style := bsSolid;
if FWDSPReady then
begin
AGCy := Round((DBmax - FAGCThresh) * InvRange * H);
if (AGCy >= 0) and (AGCy < H) then
begin
C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash;
C.MoveTo(0, AGCy); C.LineTo(W, AGCy);
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6;
C.TextOut(4, AGCy - 9, 'AGC T');
end;
AGCHangY := Round((DBmax - FAGCHangLevel) * InvRange * H);
if (AGCHangY >= 0) and (AGCHangY < H) and (Abs(AGCHangY - AGCy) > 4) then
begin
C.Pen.Color := FTheme.SpecAgcHangColor; C.Pen.Width := 1; C.Pen.Style := psDot;
C.MoveTo(0, AGCHangY); C.LineTo(W, AGCHangY);
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcHangColor; C.Font.Size := 6;
C.TextOut(4, AGCHangY + 2, 'AGC H');
end;
end else begin
AGCy := Round((DBmax - (-FAGCTop)) * InvRange * H);
if (AGCy >= 0) and (AGCy < H) then
begin
C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash;
C.MoveTo(0, AGCy); C.LineTo(W, AGCy);
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6;
C.TextOut(4, AGCy - 9, Format('AGC -%ddB', [FAGCTop]));
end;
end;
// Линия спектра
C.Pen.Style := psSolid; C.Pen.Color := FTheme.SpecLine; C.Pen.Width := 1;
C.Polyline(Slice(FSpPts, W));
// Края фильтра + VFO
C.Pen.Color := FTheme.SpecFilterEdge; C.Pen.Width := 1; C.Pen.Style := psSolid;
C.MoveTo(X1, 0); C.LineTo(X1, H);
C.MoveTo(X2, 0); C.LineTo(X2, H);
C.Pen.Color := FTheme.SpecVfoCursor; C.Pen.Width := 2;
C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 12);
C.Brush.Color := FTheme.SpecVfoCursor; C.Brush.Style := bsSolid; C.Pen.Width := 1;
C.Polygon([Point(VfoX-5,0), Point(VfoX+5,0), Point(VfoX,8)]);
if FTXOverlay and (FTXVfoIndex <> FActiveVfo) then
begin
C.Pen.Color := TColor($002030E0); C.Pen.Width := 1; C.Pen.Style := psSolid;
C.MoveTo(TXX1, 0); C.LineTo(TXX1, H);
C.MoveTo(TXX2, 0); C.LineTo(TXX2, H);
C.Pen.Width := 2;
C.MoveTo(TXVfoX, 0); C.LineTo(TXVfoX, H - 12);
C.Brush.Color := TColor($002030E0); C.Brush.Style := bsSolid; C.Pen.Width := 1;
C.Polygon([Point(TXVfoX-5,0), Point(TXVfoX+5,0), Point(TXVfoX,8)]);
end;
if FMarkerActive then DrawMarkerLine(C, W, H);
if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkers(C, W, H);
DrawADCOverloadOverlay(C, W, H);
PerfAdd(pzUiSpecOverlays, PerfTz); // PERF: canvas-фаза (линии/текст/кривая)
PerfTz := PerfNow; // PERF
// ═══ Фаза 3: raw — композиты оверлеев + альфа-проход ═════════════════════
// Бэндплан QO-100 — полоска внизу спектра (над ruler), под панелями оверлеев.
if Assigned(FBandPlanOverlay) then
FBandPlanOverlay.DrawOverlay(FSpectrumBitmap, W, H);
if Assigned(FSampleRateOverlay) then
FSampleRateOverlay.DrawOverlay(FSpectrumBitmap, C, W, H);
if Assigned(FVfoOverlay) then
FVfoOverlay.DrawOverlay(FSpectrumBitmap, W, H);
// Доп. слайс-флаги (B+) поверх главного.
for i := 0 to FSliceOverlays.Count - 1 do
TVfoOverlay(FSliceOverlays[i]).DrawOverlay(FSpectrumBitmap, W, H);
// Восстановление непрозрачности. На Cocoa проверено, что проход не нужен
// (Canvas-операции сохраняют альфу $FF), поэтому там его не делаем.
// На Qt6/win32 бэкенд другой и сглаженные края могут занулять альфу —
// оставляем быстрый dword-OR до проверки на этих платформах.
// ВРЕМЕННО (perf/cpu-profiling): EWSDR_ALPHA_FIX=0 пропускает проход —
// для визуальной проверки, нужен ли он на Qt6 вообще (подозрение: нет,
// SourceOver поверх непрозрачного фона альфу не занижает). Если артефактов
// не будет — проход можно удалить совсем.
{$IFNDEF DARWIN}
if not GSkipAlphaFix then
begin
FSpectrumBitmap.BeginUpdate(False);
for i := 0 to H - 1 do
begin
RowLW := PLongWord(FSpectrumBitmap.ScanLine[i]);
if RowLW = nil then Continue;
for Yp := 0 to W - 1 do
begin
RowLW^ := RowLW^ or $FF000000; // BGRA: альфа в байте 3
Inc(RowLW);
end;
end;
FSpectrumBitmap.EndUpdate(False);
end;
{$ENDIF}
PerfAdd(pzUiSpecAlpha, PerfTz); // PERF: композиты флагов + альфа-OR проход
PerfAdd(pzUiDrawSpectrum, PerfT0); // PERF: CPU-рендер кадра спектра
end;
// ────────────────────────────────────────────────────────────────────────────
// Делегирование к sub-views
// ────────────────────────────────────────────────────────────────────────────
procedure TSpectrumView.DrawWaterfall;
var T0: Int64; // PERF
begin
T0 := PerfNow; // PERF
FWaterfall.DrawWaterfall;
PerfAdd(pzUiDrawWaterfall, T0); // PERF: CPU-рендер строк водопада
end;
procedure TSpectrumView.DrawRuler;
begin
FRuler.DrawRuler;
end;
// ────────────────────────────────────────────────────────────────────────────
// Paint-обработчики
// ────────────────────────────────────────────────────────────────────────────
procedure TSpectrumView.PaintSpectrum(Sender: TObject);
var
PB: TPaintBox;
T0: Int64; // PERF
begin
T0 := PerfNow; // PERF
PB := TPaintBox(Sender);
PB.Canvas.Draw(0, 0, FSpectrumBitmap);
PerfAdd(pzUiPaintSpectrum, T0); // PERF: блит битмапа спектра (Qt6 canvas)
end;
procedure TSpectrumView.PaintWaterfall(Sender: TObject);
var T0: Int64; // PERF
begin
T0 := PerfNow; // PERF
FWaterfall.PaintWaterfall(Sender);
PerfAdd(pzUiPaintWaterfall, T0); // PERF: блит битмапа водопада (Qt6 canvas)
end;
procedure TSpectrumView.PaintRuler(Sender: TObject);
begin
FRuler.PaintRuler(Sender);
end;
procedure TSpectrumView.PaintSMeterRight(Sender: TObject);
begin
FSMeter.PaintSMeterRight(Sender);
end;
initialization
GSkipAlphaFix := GetEnvironmentVariable('EWSDR_ALPHA_FIX') = '0';
end.