perf(spectrum): full-raw CPU renderer + dword blenders (frame 8.3ms -> 1.7ms)

Canvas на битмапе кадра больше не используется вообще (кроме редкого
ADC-overload пути с восстановлением альфы) — убраны все raw<->canvas
переходы, каждый из которых синкал весь битмап на Qt6 (~2мс):

- RawBegin/RawEnd: прямой доступ к пикселям через RawImage.Data +
  BytesPerLine (bottom-up DIB win32 учтён отрицательным шагом)
- RawVLine/RawHLine (с пунктиром), RawFillRect, RawTriangleDown,
  RawCurve (кривая = связные вертикальные сегменты по колонкам)
- кэш текстовых масок (TRawTextMask, LRU 48): строка рендерится canvas'ом
  один раз белым-на-чёрном в свой мини-битмап, блитится raw с цветом;
  все тексты кадра теперь Courier New
- конвертированы: маркер (метка без случайной цветной плашки), бикон-
  маркеры, буквы слайсов, слайс-линии, AGC (пунктир 4/2 и 1/2, подпись
  с фоном SpecFilter как раньше), края/VFO/TX-курсоры с треугольниками
- пофреймовый альфа-OR проход удалён: альфа чинится один раз в кэше
  СЕТКИ после ребилда (canvas-текст там), кадр наследует её через memcpy

Дворд-блендеры (два 16-бит лейна на умножение, /256 вместо /255,
расхождение <0.4%) вместо побайтовых с div 255:
- BandPlanOverlay.CompositeStrip (~1мс -> ~0.2мс)
- SampleRateOverlay.CompositeCache (per-px альфа)
- VfoOverlay.BlendBitmapKey (keyed-магента дворд-сравнением)
- SpectrumView.DrawSpectrumGradient (source-лейны 1 раз на строку,
  ~1.6мс -> ~0.8мс)

Порог перестройки бэндплана: полпикселя вместо 1 Гц (на QO-100 с
decoder-lock центр подтюнивается непрерывно — был canvas-ребилд полоски
каждый кадр). Композиты внутри raw-лока (их BeginUpdate вложенные).
PerfLog: зона ui.composites отделена от ui.spec_alpha (=RawEnd).

Замер на эфире (Pluto, 576k, 52fps): кадр 8.3мс -> 1.67мс (5x),
со слайсом 2.1-2.3мс; UI-поток ~45% -> ~26% ядра, из них 15% — блиты
paint_spectrum/paint_waterfall (потолок Qt6-вывода). Визуально проверено.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 00:11:31 +03:00
co-authored by Claude Fable 5
parent 0b4d126dac
commit 0adaff2d21
7 changed files with 617 additions and 247 deletions
+519 -183
View File
@@ -18,7 +18,7 @@ unit SpectrumView;
interface
uses
Classes, SysUtils, Graphics, ExtCtrls, Controls, Math,
Classes, SysUtils, Graphics, GraphType, ExtCtrls, Controls, Math,
AppTheme,
AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay,
WaterfallView, SMeterView, RulerView,
@@ -28,6 +28,15 @@ const
SV_CLR_BG = TColor($00101010);
type
// Кэш текстовой маски для raw-рендера: строка отрисована белым на чёрном
// (canvas — один раз при создании), при блите канал покрытия умножается
// на нужный цвет. Цвет в ключ не входит.
TRawTextMask = record
Key: string; // '<size>|<bold>|<text>'
Bmp: TBitmap;
LastUse: Int64;
end;
TSpectrumView = class
protected
// ── Off-screen bitmaps ────────────────────────────────────────────────────
@@ -41,6 +50,13 @@ type
FSpPts: array of TPoint;
FSpPtsLen: Integer;
FFillSpectrum: Boolean;
// ── Full-raw рендер кадра (без Canvas на битмапе спектра) ────────────────
FRawBase: PByte; // база пикселей кадра; валидна между RawBegin/RawEnd
FRawBPL: Integer; // bytes per line (отрицателен при bottom-up DIB)
FRawW: Integer;
FRawH: Integer;
FTextMasks: array of TRawTextMask;
FTextTick: Int64;
// ── Тема ─────────────────────────────────────────────────────────────────
FTheme: TAppTheme;
FLightTheme: Boolean;
@@ -109,14 +125,31 @@ type
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-битмап между режимами доступа (дорогая синхронизация).
// Full-raw рендер кадра: ВСЕ операции на битмапе спектра идут через
// прямой доступ к пикселям (RawBegin/RawEnd), Canvas на нём не трогаем —
// на Qt6 каждый переход raw(ScanLine)<->canvas(QPainter) синкает весь
// битмап (~2мс), поэтому линии/пунктир/кривая рисуются вручную, а тексты
// блитятся из кэша масок (canvas-рендер строки один раз при изменении).
procedure RawBegin;
procedure RawEnd;
procedure RawVLine(X, Y1, Y2: Integer; Color: TColor; LineW: Integer = 1;
OnPx: Integer = 0; OffPx: Integer = 0);
procedure RawHLine(Y, X1, X2: Integer; Color: TColor;
OnPx: Integer = 0; OffPx: Integer = 0);
procedure RawFillRect(X1, Y1, X2, Y2: Integer; Color: TColor);
procedure RawTriangleDown(CX, HalfW, Hgt: Integer; Color: TColor);
procedure RawCurve(W: Integer; Color: TColor);
function GetTextMask(const S: string; FontSize: Integer;
Bold: Boolean): TBitmap;
function RawTextWidth(const S: string; FontSize: Integer;
Bold: Boolean): Integer;
procedure RawText(X, Y: Integer; const S: string; Color: TColor;
FontSize: Integer; Bold: Boolean; BgColor: TColor);
procedure DrawMarkerLineRaw(W, H: Integer);
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 DrawSliceFilterLinesRaw(W, H: Integer);
procedure DrawBandLetterRaw(X1, X2: Integer; L: Char; Clr: TColor);
procedure DrawBeaconMarkersRaw(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);
@@ -273,10 +306,20 @@ type
implementation
var
// ВРЕМЕННО (perf/cpu-profiling): EWSDR_ALPHA_FIX=0 → пропустить альфа-OR
// проход в DrawSpectrum (проверка, нужен ли он на Qt6).
GSkipAlphaFix: Boolean = False;
// Упаковка TColor в пиксель кадра (непрозрачный). Раскладка байтов как в
// BlendBand: non-Darwin = BGRA, Darwin = ARGB.
function RawPack(Color: TColor): LongWord; inline;
var R, G, B: Byte;
begin
R := Color and $FF; G := (Color shr 8) and $FF; B := (Color shr 16) and $FF;
{$IFDEF DARWIN}
Result := LongWord($FF) or (LongWord(R) shl 8) or (LongWord(G) shl 16) or
(LongWord(B) shl 24);
{$ELSE}
Result := LongWord(B) or (LongWord(G) shl 8) or (LongWord(R) shl 16) or
$FF000000;
{$ENDIF}
end;
// ════════════════════════════════════════════════════════════════════════════
// Вспомогательные функции
@@ -358,7 +401,10 @@ begin
end;
destructor TSpectrumView.Destroy;
var i: Integer;
begin
for i := 0 to High(FTextMasks) do
FTextMasks[i].Bmp.Free;
FSpectrumBitmap.Free;
FGridBitmap.Free;
FWaterfall.Free;
@@ -529,22 +575,24 @@ begin
end;
end;
procedure TSpectrumView.DrawMarkerLine(C: TCanvas; W, H: Integer);
var MX: Integer; MarkerFreq: Double; MarkerLbl: string;
procedure TSpectrumView.DrawMarkerLineRaw(W, H: Integer);
const
MARK_CLR = TColor($004444FF);
var
MX, LblW: 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)
RawVLine(MX, 0, H - 1, MARK_CLR);
LblW := RawTextWidth(MarkerLbl, 7, False);
if MX + 4 + LblW < W then
RawText(MX + 4, 4, MarkerLbl, MARK_CLR, 7, False, clNone)
else
C.TextOut(MX - 4 - C.TextWidth(MarkerLbl), 4, MarkerLbl);
RawText(MX - 4 - LblW, 4, MarkerLbl, MARK_CLR, 7, False, clNone);
end;
procedure TSpectrumView.SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double);
@@ -561,34 +609,293 @@ begin
FBeaconDecHalf := HalfHz;
end;
procedure TSpectrumView.DrawBeaconMarkers(C: TCanvas; W, H: Integer);
procedure TSpectrumView.DrawBeaconMarkersRaw(W, H: Integer);
// Две вертикали: опорная частота маяка (зелёная пунктир) и отслеживаемый
// центроид (оранжевая сплошная). Расхождение видно глазом → понятно, сел ли
// контур на пик маяка или на шум.
procedure Vline(FreqHz: Double; Col: TColor; Style: TPenStyle);
procedure Vline(FreqHz: Double; Col: TColor; Dotted: Boolean);
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);
if Dotted then RawVLine(x, 0, H - 1, Col, 1, 1, 2)
else RawVLine(x, 0, H - 1, Col);
end;
begin
if FBeaconMarkActive then
begin
Vline(FBeaconRefFreq, clLime, psDot); // где маяк ДОЛЖЕН быть
Vline(FBeaconTrkFreq, TColor($000AA5FF), psSolid); // отслеживаемый центроид (оранж)
Vline(FBeaconRefFreq, clLime, True); // где маяк ДОЛЖЕН быть
Vline(FBeaconTrkFreq, TColor($000AA5FF), False); // отслеживаемый центроид (оранж)
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); // центр наведения
Vline(FBeaconDecFreq - FBeaconDecHalf, TColor($0020D0FF), False); // грань фильтра
Vline(FBeaconDecFreq + FBeaconDecHalf, TColor($0020D0FF), False);
Vline(FBeaconDecFreq, TColor($0020D0FF), False); // центр наведения
end;
end;
// ════════════════════════════════════════════════════════════════════════════
// Full-raw примитивы кадра (валидны только между RawBegin/RawEnd)
// ════════════════════════════════════════════════════════════════════════════
procedure TSpectrumView.RawBegin;
var RI: TRawImage;
begin
FSpectrumBitmap.BeginUpdate(False);
RI := FSpectrumBitmap.RawImage;
FRawW := FSpectrumBitmap.Width;
FRawH := FSpectrumBitmap.Height;
FRawBPL := RI.Description.BytesPerLine;
FRawBase := RI.Data;
if (FRawBase <> nil) and
(RI.Description.LineOrder = riloBottomToTop) then
begin
// win32 DIB: строки снизу вверх — идём с последней с отрицательным шагом
FRawBase := FRawBase + (FRawH - 1) * FRawBPL;
FRawBPL := -FRawBPL;
end;
end;
procedure TSpectrumView.RawEnd;
begin
FRawBase := nil;
FSpectrumBitmap.EndUpdate(False);
end;
procedure TSpectrumView.RawVLine(X, Y1, Y2: Integer; Color: TColor;
LineW: Integer; OnPx: Integer; OffPx: Integer);
var
P: PLongWord;
RowP: PByte;
Y, K, Period: Integer;
Px: LongWord;
begin
if FRawBase = nil then Exit;
if X < 0 then begin Inc(LineW, X); X := 0; end;
if X + LineW > FRawW then LineW := FRawW - X;
if LineW <= 0 then Exit;
if Y1 < 0 then Y1 := 0;
if Y2 > FRawH - 1 then Y2 := FRawH - 1;
if Y2 < Y1 then Exit;
Px := RawPack(Color);
Period := OnPx + OffPx;
RowP := FRawBase + Y1 * FRawBPL + X * 4;
for Y := Y1 to Y2 do
begin
if (Period = 0) or ((Y mod Period) < OnPx) then
begin
P := PLongWord(RowP);
for K := 1 to LineW do begin P^ := Px; Inc(P); end;
end;
Inc(RowP, FRawBPL);
end;
end;
procedure TSpectrumView.RawHLine(Y, X1, X2: Integer; Color: TColor;
OnPx: Integer; OffPx: Integer);
var
P: PLongWord;
X, Period: Integer;
Px: LongWord;
begin
if FRawBase = nil then Exit;
if (Y < 0) or (Y >= FRawH) then Exit;
if X1 < 0 then X1 := 0;
if X2 > FRawW then X2 := FRawW;
if X2 <= X1 then Exit;
Px := RawPack(Color);
Period := OnPx + OffPx;
P := PLongWord(FRawBase + Y * FRawBPL + X1 * 4);
for X := X1 to X2 - 1 do
begin
if (Period = 0) or ((X mod Period) < OnPx) then P^ := Px;
Inc(P);
end;
end;
procedure TSpectrumView.RawFillRect(X1, Y1, X2, Y2: Integer; Color: TColor);
var
Y: Integer;
Px: LongWord;
begin
if FRawBase = nil then Exit;
if X1 < 0 then X1 := 0;
if Y1 < 0 then Y1 := 0;
if X2 > FRawW then X2 := FRawW;
if Y2 > FRawH then Y2 := FRawH;
if (X2 <= X1) or (Y2 <= Y1) then Exit;
Px := RawPack(Color);
for Y := Y1 to Y2 - 1 do
FillDWord((FRawBase + Y * FRawBPL + X1 * 4)^, X2 - X1, Px);
end;
// Треугольник остриём вниз (курсор VFO): вершина основания на Y=0.
procedure TSpectrumView.RawTriangleDown(CX, HalfW, Hgt: Integer; Color: TColor);
var
Y, Half: Integer;
begin
for Y := 0 to Hgt do
begin
Half := Round(HalfW * (Hgt - Y) / Hgt);
RawHLine(Y, CX - Half, CX + Half + 1, Color);
end;
end;
// Кривая спектра: вертикальный сегмент в каждой колонке от Y предыдущей
// точки до текущей — классический connected-line без Canvas.Polyline.
procedure TSpectrumView.RawCurve(W: Integer; Color: TColor);
var
X, Cur, Prev, Y0, Y1, Y: Integer;
Px: LongWord;
P: PByte;
begin
if (FRawBase = nil) or (W < 1) or (Length(FSpPts) < W) then Exit;
if W > FRawW then W := FRawW;
Px := RawPack(Color);
Prev := FSpPts[0].Y;
for X := 0 to W - 1 do
begin
Cur := FSpPts[X].Y;
if Cur < Prev then begin Y0 := Cur; Y1 := Prev; end
else begin Y0 := Prev; Y1 := Cur; end;
if Y0 < 0 then Y0 := 0;
if Y1 > FRawH - 1 then Y1 := FRawH - 1;
P := FRawBase + Y0 * FRawBPL + X * 4;
for Y := Y0 to Y1 do
begin
PLongWord(P)^ := Px;
Inc(P, FRawBPL);
end;
Prev := Cur;
end;
end;
function TSpectrumView.GetTextMask(const S: string; FontSize: Integer;
Bold: Boolean): TBitmap;
const
MAX_MASKS = 48;
var
i, Oldest: Integer;
Key: string;
B: TBitmap;
TW, TH: Integer;
procedure ApplyFont(Cv: TCanvas);
begin
Cv.Font.Name := 'Courier New';
Cv.Font.Size := FontSize;
if Bold then Cv.Font.Style := [fsBold] else Cv.Font.Style := [];
end;
begin
Inc(FTextTick);
Key := Format('%d|%d|%s', [FontSize, Ord(Bold), S]);
for i := 0 to High(FTextMasks) do
if FTextMasks[i].Key = Key then
begin
FTextMasks[i].LastUse := FTextTick;
Exit(FTextMasks[i].Bmp);
end;
// Рендер маски: белый текст на чёрном (единственное место, где текст
// проходит через Canvas — на собственном маленьком битмапе, один раз).
B := TBitmap.Create;
B.PixelFormat := pf32bit;
B.SetSize(4, 4);
ApplyFont(B.Canvas);
TW := Max(1, B.Canvas.TextWidth(S));
TH := Max(1, B.Canvas.TextHeight(S));
B.SetSize(TW, TH);
ApplyFont(B.Canvas);
B.Canvas.Brush.Color := clBlack; B.Canvas.Brush.Style := bsSolid;
B.Canvas.FillRect(Rect(0, 0, TW, TH));
B.Canvas.Font.Color := clWhite;
B.Canvas.Brush.Style := bsClear;
B.Canvas.TextOut(0, 0, S);
if Length(FTextMasks) >= MAX_MASKS then
begin
// LRU-вытеснение (маркерная метка при drag плодит новые строки)
Oldest := 0;
for i := 1 to High(FTextMasks) do
if FTextMasks[i].LastUse < FTextMasks[Oldest].LastUse then Oldest := i;
FTextMasks[Oldest].Bmp.Free;
FTextMasks[Oldest].Key := Key;
FTextMasks[Oldest].Bmp := B;
FTextMasks[Oldest].LastUse := FTextTick;
end else
begin
SetLength(FTextMasks, Length(FTextMasks) + 1);
FTextMasks[High(FTextMasks)].Key := Key;
FTextMasks[High(FTextMasks)].Bmp := B;
FTextMasks[High(FTextMasks)].LastUse := FTextTick;
end;
Result := B;
end;
function TSpectrumView.RawTextWidth(const S: string; FontSize: Integer;
Bold: Boolean): Integer;
begin
Result := GetTextMask(S, FontSize, Bold).Width;
end;
procedure TSpectrumView.RawText(X, Y: Integer; const S: string; Color: TColor;
FontSize: Integer; Bold: Boolean; BgColor: TColor);
var
M: TBitmap;
MX, MY, TY, A, InvA: Integer;
SrcRow, P: PByte;
CR, CG, CB: Byte;
begin
if FRawBase = nil then Exit;
M := GetTextMask(S, FontSize, Bold);
if BgColor <> clNone then
RawFillRect(X, Y, X + M.Width, Y + M.Height, BgColor);
CR := Color and $FF; CG := (Color shr 8) and $FF; CB := (Color shr 16) and $FF;
M.BeginUpdate(False);
try
for MY := 0 to M.Height - 1 do
begin
TY := Y + MY;
if (TY < 0) or (TY >= FRawH) then Continue;
SrcRow := PByte(M.ScanLine[MY]);
if SrcRow = nil then Continue;
for MX := 0 to M.Width - 1 do
begin
if (X + MX >= 0) and (X + MX < FRawW) then
begin
{$IFDEF DARWIN}
A := SrcRow[2]; // G-канал маски (текст белый ⇒ покрытие)
{$ELSE}
A := SrcRow[1];
{$ENDIF}
if A > 0 then
begin
InvA := 255 - A;
P := FRawBase + TY * FRawBPL + (X + MX) * 4;
{$IFDEF DARWIN}
P[1] := Byte((CR * A + P[1] * InvA) div 255);
P[2] := Byte((CG * A + P[2] * InvA) div 255);
P[3] := Byte((CB * A + P[3] * InvA) div 255);
P[0] := $FF;
{$ELSE}
P[0] := Byte((CB * A + P[0] * InvA) div 255);
P[1] := Byte((CG * A + P[1] * InvA) div 255);
P[2] := Byte((CR * A + P[2] * InvA) div 255);
P[3] := $FF;
{$ENDIF}
end;
end;
Inc(SrcRow, 4);
end;
end;
finally
M.EndUpdate(False);
end;
C.Pen.Style := psSolid;
end;
procedure TSpectrumView.BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
@@ -689,7 +996,7 @@ begin
end;
end;
procedure TSpectrumView.DrawSliceFilterLines(C: TCanvas; W, H: Integer);
procedure TSpectrumView.DrawSliceFilterLinesRaw(W, H: Integer);
const
SLICE_CLR = TColor($0010A8F0);
var
@@ -701,33 +1008,24 @@ 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;
RawVLine(SX1, 0, H - 1, SLICE_CLR);
RawVLine(SX2, 0, H - 1, SLICE_CLR);
RawVLine(SVfoX, 0, H - 13, SLICE_CLR, 2); // несущая (жирнее)
// Буква слайса внутри полосы фильтра сверху — привязка полосы к флагу,
// цвет = цвет слайса (совпадает с бейджем во флаге).
DrawBandLetter(C, SX1, SX2, O.SliceLetter, SliceColor(O.SliceLetter));
DrawBandLetterRaw(SX1, SX2, O.SliceLetter, SliceColor(O.SliceLetter));
end;
end;
// Буква слайса (A/B/…) по центру полосы фильтра, у самого верха — чтобы
// понять, какому флагу принадлежит полоса, даже когда флаги стоят рядом.
procedure TSpectrumView.DrawBandLetter(C: TCanvas; X1, X2: Integer;
L: Char; Clr: TColor);
procedure TSpectrumView.DrawBandLetterRaw(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 := [];
RawText(cx - RawTextWidth(L, 8, True) div 2, 0, L, Clr, 8, True, clNone);
end;
procedure TSpectrumView.CopyGridToSpectrum(W, H: Integer);
@@ -789,10 +1087,15 @@ begin
end;
procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
// Дворд-блендинг: цвет и альфа постоянны в пределах строки → произведения
// source-лейнов считаются один раз на строку, в пиксельном цикле остаются
// два умножения на dst-лейны (/256 вместо /200-базы оригинала, кривая
// прозрачности та же — квадратичная). Побайтовый вариант стоил ~1.6мс/кадр.
var
Row, Col, Alpha, YMin, Off: Integer;
RowPtr: PByte;
Row, Col, Alpha, YMin, A256, I256: Integer;
RowPtr: PLongWord;
GradB, GradG, GradR: Byte;
SrcPx, SrcLo, SrcHi, D: LongWord;
begin
if FSpectrumBitmap = nil then Exit;
if (W <= 0) or (H <= 0) then Exit;
@@ -810,21 +1113,31 @@ begin
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]);
// Альфа 0..200 → 0..256; source-лейны умножаются здесь, один раз на строку
A256 := (Alpha * 256 + 100) div 200;
I256 := 256 - A256;
SrcPx := RawPack(TColor(GradR or (LongWord(GradG) shl 8) or
(LongWord(GradB) shl 16)));
SrcLo := (SrcPx and $00FF00FF) * LongWord(A256);
SrcHi := ((SrcPx shr 8) and $00FF00FF) * LongWord(A256);
RowPtr := PLongWord(FSpectrumBitmap.ScanLine[Row]);
for Col := 0 to W - 1 do
begin
if SpPts[Col].Y <= Row then
begin
Off := Col * 4;
D := RowPtr^;
RowPtr^ := (((SrcLo + (D and $00FF00FF) * LongWord(I256)) shr 8)
and $00FF00FF)
or ((SrcHi + ((D shr 8) and $00FF00FF) * LongWord(I256))
and $FF00FF00)
{$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;
or $000000FF;
{$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;
or $FF000000;
{$ENDIF}
end;
Inc(RowPtr);
end;
end;
FSpectrumBitmap.EndUpdate(False);
end;
@@ -1046,6 +1359,23 @@ begin
GC.MoveTo(GX, 0); GC.LineTo(GX, H);
end;
end;
{$IFNDEF DARWIN}
// Альфа кэша сетки → $FF один раз после ребилда: canvas-текст/AA на Qt6
// оставляют битую альфу, а кадр копирует её memcpy как есть. Пофреймовый
// OR-проход по кадру удалён — на битмапе кадра Canvas больше не бывает.
FGridBitmap.BeginUpdate(False);
for i := 0 to H - 1 do
begin
RowLW := PLongWord(FGridBitmap.ScanLine[i]);
if RowLW = nil then Continue;
for Yp := 0 to W - 1 do
begin
RowLW^ := RowLW^ or $FF000000;
Inc(RowLW);
end;
end;
FGridBitmap.EndUpdate(False);
{$ENDIF}
end;
PerfAdd(pzUiSpecGrid, PerfTz); // PERF: ребилд кэша сетки (обычно 0)
PerfTz := PerfNow; // PERF
@@ -1104,131 +1434,139 @@ begin
PerfAdd(pzUiSpecCurve, PerfTz); // PERF: расчёт точек кривой
PerfTz := PerfNow; // PERF
// ═══ Фаза 1: raw (ScanLine) — сетка, полосы, градиент ════════════════════
// ═══ Фаза 1: raw — весь кадр одним локом ═════════════════════════════════
{$IFDEF DARWIN}
// Cocoa-вариант CopyGridToSpectrum идёт через Canvas.Draw — до RawBegin.
CopyGridToSpectrum(W, H);
PerfAdd(pzUiSpecGrid, PerfTz); // PERF: += полный memcpy сетки в битмап
PerfAdd(pzUiSpecGrid, PerfTz); // PERF: += полный перенос сетки в битмап
PerfTz := PerfNow; // PERF
{$ENDIF}
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 поверх непрозрачного фона альфу не занижает). Если артефактов
// не будет — проход можно удалить совсем.
RawBegin;
try
{$IFNDEF DARWIN}
if not GSkipAlphaFix then
CopyGridToSpectrum(W, H);
PerfAdd(pzUiSpecGrid, PerfTz); // PERF: += полный memcpy сетки в битмап
PerfTz := PerfNow; // PERF
{$ENDIF}
// Полосы фильтра
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
// Буква главного флага (A) на его полосе — только когда есть слайсы,
// иначе одиночный приём не засоряем.
if (FSliceOverlays <> nil) and (FSliceOverlays.Count > 0) and
(FActiveVfo = 0) and Assigned(FVfoOverlay) then
DrawBandLetterRaw(X1, X2, FVfoOverlay.SliceLetter,
SliceColor(FVfoOverlay.SliceLetter));
// Края/несущие/буквы слайсов (полосы уже нарисованы выше).
DrawSliceFilterLinesRaw(W, H);
// AGC линии. Подпись с фоном цвета полосы фильтра — как рисовал старый
// canvas-TextOut с кистью bsSolid/SpecFilter.
if FWDSPReady then
begin
AGCy := Round((DBmax - FAGCThresh) * InvRange * H);
if (AGCy >= 0) and (AGCy < H) then
begin
RawHLine(AGCy, 0, W, FTheme.SpecAgcColor, 4, 2); // psDash
RawText(4, AGCy - 9, 'AGC T', FTheme.SpecAgcColor, 6, False,
FTheme.SpecFilter);
end;
AGCHangY := Round((DBmax - FAGCHangLevel) * InvRange * H);
if (AGCHangY >= 0) and (AGCHangY < H) and (Abs(AGCHangY - AGCy) > 4) then
begin
RawHLine(AGCHangY, 0, W, FTheme.SpecAgcHangColor, 1, 2); // psDot
RawText(4, AGCHangY + 2, 'AGC H', FTheme.SpecAgcHangColor, 6, False,
FTheme.SpecFilter);
end;
end else begin
AGCy := Round((DBmax - (-FAGCTop)) * InvRange * H);
if (AGCy >= 0) and (AGCy < H) then
begin
RawHLine(AGCy, 0, W, FTheme.SpecAgcColor, 4, 2); // psDash
RawText(4, AGCy - 9, Format('AGC -%ddB', [FAGCTop]),
FTheme.SpecAgcColor, 6, False, FTheme.SpecFilter);
end;
end;
// Линия спектра
RawCurve(W, FTheme.SpecLine);
// Края фильтра + VFO
RawVLine(X1, 0, H - 1, FTheme.SpecFilterEdge);
RawVLine(X2, 0, H - 1, FTheme.SpecFilterEdge);
RawVLine(VfoX, 0, H - 13, FTheme.SpecVfoCursor, 2);
RawTriangleDown(VfoX, 5, 8, FTheme.SpecVfoCursor);
if FTXOverlay and (FTXVfoIndex <> FActiveVfo) then
begin
RawVLine(TXX1, 0, H - 1, TColor($002030E0));
RawVLine(TXX2, 0, H - 1, TColor($002030E0));
RawVLine(TXVfoX, 0, H - 13, TColor($002030E0), 2);
RawTriangleDown(TXVfoX, 5, 8, TColor($002030E0));
end;
if FMarkerActive then DrawMarkerLineRaw(W, H);
if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkersRaw(W, H);
PerfAdd(pzUiSpecOverlays, PerfTz); // PERF: raw-линии/текст/кривая/маркеры
PerfTz := PerfNow; // PERF
// Композиты оверлеев — внутри внешнего лока: их собственные BeginUpdate/
// EndUpdate становятся вложенными (только счётчик), иначе каждая пара на
// верхнем уровне заново дёргает handle-менеджмент битмапа (~0.3-0.4мс на
// пару — замерено 1.8мс на кадр при пиксельной работе на ~0.15мс).
// Spectrum-канва внутри не трогается: SampleRateOverlay меряет текст на
// собственном скретч-битмапе (параметр C — рудимент), кэши флагов
// рендерятся на своих битмапах.
// Бэндплан QO-100 — полоска внизу спектра, под панелями оверлеев.
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);
PerfAdd(pzUiComposites, PerfTz); // PERF: композиты бэндплан/панель/флаги
PerfTz := PerfNow; // PERF
finally
RawEnd;
end;
// ADC overload — редкий canvas-путь (DrawAlertOverlay). Ломает альфу и
// режим доступа только в кадрах, где реально виден; после его canvas-
// операций восстанавливаем непрозрачность (пофреймовый OR-проход для
// обычных кадров удалён — Canvas на битмапе кадра больше не бывает).
if FADCOverloadVisible then
begin
DrawADCOverloadOverlay(C, W, H);
{$IFNDEF DARWIN}
FSpectrumBitmap.BeginUpdate(False);
for i := 0 to H - 1 do
begin
@@ -1241,9 +1579,10 @@ begin
end;
end;
FSpectrumBitmap.EndUpdate(False);
end;
{$ENDIF}
PerfAdd(pzUiSpecAlpha, PerfTz); // PERF: композиты флагов + альфа-OR проход
end;
PerfAdd(pzUiSpecAlpha, PerfTz); // PERF: RawEnd (EndUpdate) + редкий ADC-путь
PerfAdd(pzUiDrawSpectrum, PerfT0); // PERF: CPU-рендер кадра спектра
end;
@@ -1297,7 +1636,4 @@ begin
FSMeter.PaintSMeterRight(Sender);
end;
initialization
GSkipAlphaFix := GetEnvironmentVariable('EWSDR_ALPHA_FIX') = '0';
end.