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
+29 -19
View File
@@ -231,11 +231,18 @@ begin
end; end;
function TBandPlanOverlay.NeedsRebuild(W: Integer): Boolean; function TBandPlanOverlay.NeedsRebuild(W: Integer): Boolean;
var HalfPxHz: Double;
begin begin
// Порог перестройки — ПОЛПИКСЕЛЯ, а не 1 Гц: суб-пиксельный сдвиг центра
// не меняет картинку, а на QO-100 с decoder-lock центр подтюнивается
// непрерывно — порог 1 Гц заставлял полный canvas-ребилд полоски (и
// canvas<->raw флип её кэша) на каждом кадре (~1.2 мс/кадр впустую).
HalfPxHz := 0.5 * FSpanHz / Max(W, 1);
if HalfPxHz < 1.0 then HalfPxHz := 1.0;
Result := FCacheDirty or (FCacheW <> W) Result := FCacheDirty or (FCacheW <> W)
or (FKeyW <> W) or (FKeyEnabled <> FEnabled) or (FKeyW <> W) or (FKeyEnabled <> FEnabled)
or (Abs(FKeyCenter - FCenterFreq) >= 1.0) or (Abs(FKeyCenter - FCenterFreq) >= HalfPxHz)
or (Abs(FKeySpan - FSpanHz) >= 1.0); or (Abs(FKeySpan - FSpanHz) >= HalfPxHz);
end; end;
procedure TBandPlanOverlay.RebuildCache(W: Integer); procedure TBandPlanOverlay.RebuildCache(W: Integer);
@@ -252,15 +259,18 @@ end;
procedure TBandPlanOverlay.CompositeStrip(Target: TBitmap; DstY: Integer); procedure TBandPlanOverlay.CompositeStrip(Target: TBitmap; DstY: Integer);
// Альфа-композит FCache поверх Target в (0, DstY) с постоянной BANDPLAN_ALPHA. // Альфа-композит FCache поверх Target в (0, DstY) с постоянной BANDPLAN_ALPHA.
// Один проход по W × STRIP_H пикселей за кадр. // Один проход по W × STRIP_H пикселей за кадр. Дворд-блендинг: два 16-битных
// лейна на умножение, деление /256 вместо /255 (расхождение <0.4%, глазом не
// видно) — побайтовый вариант с div 255 стоил ~1мс/кадр на полной ширине.
var var
X, Y, ty, InvA: Integer; X, Y, ty, N: Integer;
RS, RD: PByte; A256, I256, S, D: LongWord;
const RS, RD: PLongWord;
A = BANDPLAN_ALPHA;
begin begin
if (Target = nil) or (FCacheW <= 0) then Exit; if (Target = nil) or (FCacheW <= 0) then Exit;
InvA := 255 - A; A256 := BANDPLAN_ALPHA + (BANDPLAN_ALPHA shr 7); // 0..256
I256 := 256 - A256;
N := Min(FCacheW, Target.Width);
Target.BeginUpdate(False); Target.BeginUpdate(False);
FCache.BeginUpdate(False); FCache.BeginUpdate(False);
try try
@@ -268,22 +278,22 @@ begin
begin begin
ty := DstY + Y; ty := DstY + Y;
if (ty < 0) or (ty >= Target.Height) then Continue; if (ty < 0) or (ty >= Target.Height) then Continue;
RS := PByte(FCache.ScanLine[Y]); RS := PLongWord(FCache.ScanLine[Y]);
RD := PByte(Target.ScanLine[ty]); RD := PLongWord(Target.ScanLine[ty]);
if (RS = nil) or (RD = nil) then Continue; if (RS = nil) or (RD = nil) then Continue;
for X := 0 to FCacheW - 1 do for X := 0 to N - 1 do
begin begin
if X >= Target.Width then Break; S := RS^; D := RD^;
RD^ := ((((S and $00FF00FF) * A256 + (D and $00FF00FF) * I256) shr 8)
and $00FF00FF)
or ((((S shr 8) and $00FF00FF) * A256 +
((D shr 8) and $00FF00FF) * I256) and $FF00FF00)
{$IFDEF DARWIN} {$IFDEF DARWIN}
RD[1] := Byte((A * RS[1] + InvA * RD[1]) div 255); or $000000FF; // ARGB: альфа в байте 0
RD[2] := Byte((A * RS[2] + InvA * RD[2]) div 255);
RD[3] := Byte((A * RS[3] + InvA * RD[3]) div 255);
{$ELSE} {$ELSE}
RD[0] := Byte((A * RS[0] + InvA * RD[0]) div 255); or $FF000000; // BGRA: альфа в байте 3
RD[1] := Byte((A * RS[1] + InvA * RD[1]) div 255);
RD[2] := Byte((A * RS[2] + InvA * RD[2]) div 255);
{$ENDIF} {$ENDIF}
Inc(RS, 4); Inc(RD, 4); Inc(RS); Inc(RD);
end; end;
end; end;
finally finally
+4 -2
View File
@@ -77,7 +77,8 @@ type
pzUiSpecCurve, // расчёт точек кривой + градиент под кривой + Polyline pzUiSpecCurve, // расчёт точек кривой + градиент под кривой + Polyline
pzUiSpecOverlays, // края/VFO/маркеры/бэндплан/флаги (DrawOverlay в битмап) pzUiSpecOverlays, // края/VFO/маркеры/бэндплан/флаги (DrawOverlay в битмап)
pzUiSpecAlpha, // финальный альфа-OR проход по всему битмапу pzUiSpecAlpha, // финальный альфа-OR проход по всему битмапу
pzUiGlSwap // SwapBuffers (GL) — включает ожидание vsync! pzUiGlSwap, // SwapBuffers (GL) — включает ожидание vsync!
pzUiComposites // композиты бэндплан/панель/флаги (внутри raw-лока)
); );
const const
@@ -113,7 +114,8 @@ const
'ui.spec_curve', 'ui.spec_curve',
'ui.spec_overlays', 'ui.spec_overlays',
'ui.spec_alpha', 'ui.spec_alpha',
'ui.gl_swap' 'ui.gl_swap',
'ui.composites'
); );
var var
+28 -19
View File
@@ -383,7 +383,12 @@ end;
// Альфа-композит FCache поверх Target в точке (FLeft, FTop) — один проход/кадр. // Альфа-композит FCache поверх Target в точке (FLeft, FTop) — один проход/кадр.
procedure TSampleRateOverlay.CompositeCache(Target: TBitmap); procedure TSampleRateOverlay.CompositeCache(Target: TBitmap);
var X, Y, A, InvA, h: Integer; RS, RD: PByte; // Дворд-блендинг (два 16-битных лейна на умножение, /256 вместо /255) —
// per-pixel альфа из альфа-байта кэша. Альфа-байт цели добивается до $FF.
var
X, Y, h: Integer;
A256, I256, S, D: LongWord;
RS, RD: PLongWord;
begin begin
if (Target = nil) or (FCacheW <= 0) then Exit; if (Target = nil) or (FCacheW <= 0) then Exit;
h := FCache.Height; h := FCache.Height;
@@ -393,33 +398,37 @@ begin
for Y := 0 to h - 1 do for Y := 0 to h - 1 do
begin begin
if (FTop + Y < 0) or (FTop + Y >= Target.Height) then Continue; if (FTop + Y < 0) or (FTop + Y >= Target.Height) then Continue;
RS := PByte(FCache.ScanLine[Y]); RS := PLongWord(FCache.ScanLine[Y]);
RD := PByte(Target.ScanLine[FTop + Y]); RD := PLongWord(Target.ScanLine[FTop + Y]);
if (RS = nil) or (RD = nil) then Continue; if (RS = nil) or (RD = nil) then Continue;
Inc(RD, FLeft * 4); Inc(RD, FLeft);
for X := 0 to FCacheW - 1 do for X := 0 to FCacheW - 1 do
begin begin
if (FLeft + X >= 0) and (FLeft + X < Target.Width) then if (FLeft + X >= 0) and (FLeft + X < Target.Width) then
begin begin
S := RS^;
{$IFDEF DARWIN} {$IFDEF DARWIN}
A := RS[0]; InvA := 255 - A; A256 := S and $FF; // ARGB: альфа в байте 0
if A > 0 then
begin
RD[1] := Byte((RS[1]*A + RD[1]*InvA) div 255);
RD[2] := Byte((RS[2]*A + RD[2]*InvA) div 255);
RD[3] := Byte((RS[3]*A + RD[3]*InvA) div 255);
end;
{$ELSE} {$ELSE}
A := RS[3]; InvA := 255 - A; A256 := S shr 24; // BGRA: альфа в байте 3
if A > 0 then
begin
RD[0] := Byte((RS[0]*A + RD[0]*InvA) div 255);
RD[1] := Byte((RS[1]*A + RD[1]*InvA) div 255);
RD[2] := Byte((RS[2]*A + RD[2]*InvA) div 255);
end;
{$ENDIF} {$ENDIF}
if A256 > 0 then
begin
A256 := A256 + (A256 shr 7); // 0..256
I256 := 256 - A256;
D := RD^;
RD^ := ((((S and $00FF00FF) * A256 + (D and $00FF00FF) * I256)
shr 8) and $00FF00FF)
or ((((S shr 8) and $00FF00FF) * A256 +
((D shr 8) and $00FF00FF) * I256) and $FF00FF00)
{$IFDEF DARWIN}
or $000000FF;
{$ELSE}
or $FF000000;
{$ENDIF}
end;
end; end;
Inc(RS, 4); Inc(RD, 4); Inc(RS); Inc(RD);
end; end;
end; end;
finally finally
+519 -183
View File
@@ -18,7 +18,7 @@ unit SpectrumView;
interface interface
uses uses
Classes, SysUtils, Graphics, ExtCtrls, Controls, Math, Classes, SysUtils, Graphics, GraphType, ExtCtrls, Controls, Math,
AppTheme, AppTheme,
AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay, AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay,
WaterfallView, SMeterView, RulerView, WaterfallView, SMeterView, RulerView,
@@ -28,6 +28,15 @@ const
SV_CLR_BG = TColor($00101010); SV_CLR_BG = TColor($00101010);
type type
// Кэш текстовой маски для raw-рендера: строка отрисована белым на чёрном
// (canvas — один раз при создании), при блите канал покрытия умножается
// на нужный цвет. Цвет в ключ не входит.
TRawTextMask = record
Key: string; // '<size>|<bold>|<text>'
Bmp: TBitmap;
LastUse: Int64;
end;
TSpectrumView = class TSpectrumView = class
protected protected
// ── Off-screen bitmaps ──────────────────────────────────────────────────── // ── Off-screen bitmaps ────────────────────────────────────────────────────
@@ -41,6 +50,13 @@ type
FSpPts: array of TPoint; FSpPts: array of TPoint;
FSpPtsLen: Integer; FSpPtsLen: Integer;
FFillSpectrum: Boolean; 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; FTheme: TAppTheme;
FLightTheme: Boolean; FLightTheme: Boolean;
@@ -109,14 +125,31 @@ type
procedure SetMarkerX(V: Integer); procedure SetMarkerX(V: Integer);
procedure SetFillSpectrum(V: Boolean); procedure SetFillSpectrum(V: Boolean);
procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer); procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
procedure DrawMarkerLine(C: TCanvas; W, H: Integer); // Full-raw рендер кадра: ВСЕ операции на битмапе спектра идут через
// Слайс-маркеры разделены на raw-часть (полосы, ScanLine) и canvas-часть // прямой доступ к пикселям (RawBegin/RawEnd), Canvas на нём не трогаем —
// (линии/буквы): DrawSpectrum группирует raw- и canvas-операции, чтобы // на Qt6 каждый переход raw(ScanLine)<->canvas(QPainter) синкает весь
// не гонять Qt6-битмап между режимами доступа (дорогая синхронизация). // битмап (~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 DrawSliceFilterBands(W, H: Integer);
procedure DrawSliceFilterLines(C: TCanvas; W, H: Integer); procedure DrawSliceFilterLinesRaw(W, H: Integer);
procedure DrawBandLetter(C: TCanvas; X1, X2: Integer; L: Char; Clr: TColor); procedure DrawBandLetterRaw(X1, X2: Integer; L: Char; Clr: TColor);
procedure DrawBeaconMarkers(C: TCanvas; W, H: Integer); procedure DrawBeaconMarkersRaw(W, H: Integer);
procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte); procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
procedure FillBandRaw(X1, X2, H: Integer; Color: TColor); procedure FillBandRaw(X1, X2, H: Integer; Color: TColor);
procedure CopyGridToSpectrum(W, H: Integer); procedure CopyGridToSpectrum(W, H: Integer);
@@ -273,10 +306,20 @@ type
implementation implementation
var // Упаковка TColor в пиксель кадра (непрозрачный). Раскладка байтов как в
// ВРЕМЕННО (perf/cpu-profiling): EWSDR_ALPHA_FIX=0 → пропустить альфа-OR // BlendBand: non-Darwin = BGRA, Darwin = ARGB.
// проход в DrawSpectrum (проверка, нужен ли он на Qt6). function RawPack(Color: TColor): LongWord; inline;
GSkipAlphaFix: Boolean = False; 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; end;
destructor TSpectrumView.Destroy; destructor TSpectrumView.Destroy;
var i: Integer;
begin begin
for i := 0 to High(FTextMasks) do
FTextMasks[i].Bmp.Free;
FSpectrumBitmap.Free; FSpectrumBitmap.Free;
FGridBitmap.Free; FGridBitmap.Free;
FWaterfall.Free; FWaterfall.Free;
@@ -529,22 +575,24 @@ begin
end; end;
end; end;
procedure TSpectrumView.DrawMarkerLine(C: TCanvas; W, H: Integer); procedure TSpectrumView.DrawMarkerLineRaw(W, H: Integer);
var MX: Integer; MarkerFreq: Double; MarkerLbl: string; const
MARK_CLR = TColor($004444FF);
var
MX, LblW: Integer;
MarkerFreq: Double;
MarkerLbl: string;
begin begin
MX := Round(FMarkerX / 1000.0 * W); MX := Round(FMarkerX / 1000.0 * W);
if (MX < 0) or (MX >= W) then Exit; if (MX < 0) or (MX >= W) then Exit;
MarkerFreq := (FCenterFreq - FSpanHz / 2) + FMarkerX / 1000.0 * FSpanHz; MarkerFreq := (FCenterFreq - FSpanHz / 2) + FMarkerX / 1000.0 * FSpanHz;
MarkerLbl := FormatFreqSV(Round(MarkerFreq)); MarkerLbl := FormatFreqSV(Round(MarkerFreq));
C.Pen.Color := TColor($004444FF); RawVLine(MX, 0, H - 1, MARK_CLR);
C.Pen.Width := 1; C.Pen.Style := psSolid; LblW := RawTextWidth(MarkerLbl, 7, False);
C.MoveTo(MX, 0); C.LineTo(MX, H); if MX + 4 + LblW < W then
C.Font.Color := TColor($004444FF); RawText(MX + 4, 4, MarkerLbl, MARK_CLR, 7, False, clNone)
C.Font.Size := 7; C.Font.Name := 'Courier New';
if MX + 4 + C.TextWidth(MarkerLbl) < W then
C.TextOut(MX + 4, 4, MarkerLbl)
else else
C.TextOut(MX - 4 - C.TextWidth(MarkerLbl), 4, MarkerLbl); RawText(MX - 4 - LblW, 4, MarkerLbl, MARK_CLR, 7, False, clNone);
end; end;
procedure TSpectrumView.SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double); procedure TSpectrumView.SetBeaconMarkers(Active: Boolean; RefHz, TrkHz: Double);
@@ -561,34 +609,293 @@ begin
FBeaconDecHalf := HalfHz; FBeaconDecHalf := HalfHz;
end; 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; var x: Integer;
begin begin
if FSpanHz <= 0 then Exit; if FSpanHz <= 0 then Exit;
x := Round((FreqHz - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W); x := Round((FreqHz - (FCenterFreq - FSpanHz / 2)) / FSpanHz * W);
if (x < 0) or (x >= W) then Exit; if (x < 0) or (x >= W) then Exit;
C.Pen.Color := Col; C.Pen.Width := 1; C.Pen.Style := Style; if Dotted then RawVLine(x, 0, H - 1, Col, 1, 1, 2)
C.MoveTo(x, 0); C.LineTo(x, H); else RawVLine(x, 0, H - 1, Col);
end; end;
begin begin
if FBeaconMarkActive then if FBeaconMarkActive then
begin begin
Vline(FBeaconRefFreq, clLime, psDot); // где маяк ДОЛЖЕН быть Vline(FBeaconRefFreq, clLime, True); // где маяк ДОЛЖЕН быть
Vline(FBeaconTrkFreq, TColor($000AA5FF), psSolid); // отслеживаемый центроид (оранж) Vline(FBeaconTrkFreq, TColor($000AA5FF), False); // отслеживаемый центроид (оранж)
end; end;
// Узкий фильтр-маркер декодера: грани полосы + центральная линия. Три линии без // Узкий фильтр-маркер декодера: грани полосы + центральная линия. Три линии без
// полупрозрачной заливки (как в GL-рендере) — единый вид и дешевле для CPU. // полупрозрачной заливки (как в GL-рендере) — единый вид и дешевле для CPU.
if FBeaconDecActive and (FSpanHz > 0) then if FBeaconDecActive and (FSpanHz > 0) then
begin begin
Vline(FBeaconDecFreq - FBeaconDecHalf, TColor($0020D0FF), psSolid); // грань фильтра Vline(FBeaconDecFreq - FBeaconDecHalf, TColor($0020D0FF), False); // грань фильтра
Vline(FBeaconDecFreq + FBeaconDecHalf, TColor($0020D0FF), psSolid); Vline(FBeaconDecFreq + FBeaconDecHalf, TColor($0020D0FF), False);
Vline(FBeaconDecFreq, TColor($0020D0FF), psSolid); // центр наведения 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; end;
C.Pen.Style := psSolid;
end; end;
procedure TSpectrumView.BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte); procedure TSpectrumView.BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
@@ -689,7 +996,7 @@ begin
end; end;
end; end;
procedure TSpectrumView.DrawSliceFilterLines(C: TCanvas; W, H: Integer); procedure TSpectrumView.DrawSliceFilterLinesRaw(W, H: Integer);
const const
SLICE_CLR = TColor($0010A8F0); SLICE_CLR = TColor($0010A8F0);
var var
@@ -701,33 +1008,24 @@ begin
O := TVfoOverlay(FSliceOverlays[i]); O := TVfoOverlay(FSliceOverlays[i]);
if (O = nil) or (not O.Visible) then Continue; if (O = nil) or (not O.Visible) then Continue;
CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, SX1, SX2, SVfoX); CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, SX1, SX2, SVfoX);
C.Pen.Color := SLICE_CLR; C.Pen.Width := 1; C.Pen.Style := psSolid; RawVLine(SX1, 0, H - 1, SLICE_CLR);
C.MoveTo(SX1, 0); C.LineTo(SX1, H); RawVLine(SX2, 0, H - 1, SLICE_CLR);
C.MoveTo(SX2, 0); C.LineTo(SX2, H); RawVLine(SVfoX, 0, H - 13, SLICE_CLR, 2); // несущая (жирнее)
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)); DrawBandLetterRaw(SX1, SX2, O.SliceLetter, SliceColor(O.SliceLetter));
end; end;
end; end;
// Буква слайса (A/B/…) по центру полосы фильтра, у самого верха — чтобы // Буква слайса (A/B/…) по центру полосы фильтра, у самого верха — чтобы
// понять, какому флагу принадлежит полоса, даже когда флаги стоят рядом. // понять, какому флагу принадлежит полоса, даже когда флаги стоят рядом.
procedure TSpectrumView.DrawBandLetter(C: TCanvas; X1, X2: Integer; procedure TSpectrumView.DrawBandLetterRaw(X1, X2: Integer; L: Char; Clr: TColor);
L: Char; Clr: TColor);
var var
cx: Integer; cx: Integer;
begin begin
if (L < 'A') or (L > 'Z') then Exit; if (L < 'A') or (L > 'Z') then Exit;
cx := (X1 + X2) div 2; cx := (X1 + X2) div 2;
C.Font.Name := 'Courier New'; C.Font.Size := 8; C.Font.Style := [fsBold]; RawText(cx - RawTextWidth(L, 8, True) div 2, 0, L, Clr, 8, True, clNone);
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; end;
procedure TSpectrumView.CopyGridToSpectrum(W, H: Integer); procedure TSpectrumView.CopyGridToSpectrum(W, H: Integer);
@@ -789,10 +1087,15 @@ begin
end; end;
procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer); procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
// Дворд-блендинг: цвет и альфа постоянны в пределах строки → произведения
// source-лейнов считаются один раз на строку, в пиксельном цикле остаются
// два умножения на dst-лейны (/256 вместо /200-базы оригинала, кривая
// прозрачности та же — квадратичная). Побайтовый вариант стоил ~1.6мс/кадр.
var var
Row, Col, Alpha, YMin, Off: Integer; Row, Col, Alpha, YMin, A256, I256: Integer;
RowPtr: PByte; RowPtr: PLongWord;
GradB, GradG, GradR: Byte; GradB, GradG, GradR: Byte;
SrcPx, SrcLo, SrcHi, D: LongWord;
begin begin
if FSpectrumBitmap = nil then Exit; if FSpectrumBitmap = nil then Exit;
if (W <= 0) or (H <= 0) then Exit; if (W <= 0) or (H <= 0) then Exit;
@@ -810,21 +1113,31 @@ begin
GradB := Byte(FTheme.SpecGradB * Alpha div 200); GradB := Byte(FTheme.SpecGradB * Alpha div 200);
GradG := Byte(FTheme.SpecGradG * Alpha div 200); GradG := Byte(FTheme.SpecGradG * Alpha div 200);
GradR := Byte(FTheme.SpecGradR * 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 for Col := 0 to W - 1 do
begin
if SpPts[Col].Y <= Row then if SpPts[Col].Y <= Row then
begin 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} {$IFDEF DARWIN}
RowPtr[Off+1] := (GradR * Alpha + RowPtr[Off+1] * (200 - Alpha)) div 200; or $000000FF;
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} {$ELSE}
RowPtr[Off] := (GradB * Alpha + RowPtr[Off] * (200 - Alpha)) div 200; or $FF000000;
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} {$ENDIF}
end; end;
Inc(RowPtr);
end;
end; end;
FSpectrumBitmap.EndUpdate(False); FSpectrumBitmap.EndUpdate(False);
end; end;
@@ -1046,6 +1359,23 @@ begin
GC.MoveTo(GX, 0); GC.LineTo(GX, H); GC.MoveTo(GX, 0); GC.LineTo(GX, H);
end; end;
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; end;
PerfAdd(pzUiSpecGrid, PerfTz); // PERF: ребилд кэша сетки (обычно 0) PerfAdd(pzUiSpecGrid, PerfTz); // PERF: ребилд кэша сетки (обычно 0)
PerfTz := PerfNow; // PERF PerfTz := PerfNow; // PERF
@@ -1104,131 +1434,139 @@ begin
PerfAdd(pzUiSpecCurve, PerfTz); // PERF: расчёт точек кривой PerfAdd(pzUiSpecCurve, PerfTz); // PERF: расчёт точек кривой
PerfTz := PerfNow; // PERF PerfTz := PerfNow; // PERF
// ═══ Фаза 1: raw (ScanLine) — сетка, полосы, градиент ════════════════════ // ═══ Фаза 1: raw — весь кадр одним локом ═════════════════════════════════
{$IFDEF DARWIN}
// Cocoa-вариант CopyGridToSpectrum идёт через Canvas.Draw — до RawBegin.
CopyGridToSpectrum(W, H); CopyGridToSpectrum(W, H);
PerfAdd(pzUiSpecGrid, PerfTz); // PERF: += полный memcpy сетки в битмап PerfAdd(pzUiSpecGrid, PerfTz); // PERF: += полный перенос сетки в битмап
PerfTz := PerfNow; // PERF PerfTz := PerfNow; // PERF
{$ENDIF}
if FTXOverlay then RawBegin;
begin try
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} {$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 begin
DrawADCOverloadOverlay(C, W, H);
{$IFNDEF DARWIN}
FSpectrumBitmap.BeginUpdate(False); FSpectrumBitmap.BeginUpdate(False);
for i := 0 to H - 1 do for i := 0 to H - 1 do
begin begin
@@ -1241,9 +1579,10 @@ begin
end; end;
end; end;
FSpectrumBitmap.EndUpdate(False); FSpectrumBitmap.EndUpdate(False);
end;
{$ENDIF} {$ENDIF}
PerfAdd(pzUiSpecAlpha, PerfTz); // PERF: композиты флагов + альфа-OR проход end;
PerfAdd(pzUiSpecAlpha, PerfTz); // PERF: RawEnd (EndUpdate) + редкий ADC-путь
PerfAdd(pzUiDrawSpectrum, PerfT0); // PERF: CPU-рендер кадра спектра PerfAdd(pzUiDrawSpectrum, PerfT0); // PERF: CPU-рендер кадра спектра
end; end;
@@ -1297,7 +1636,4 @@ begin
FSMeter.PaintSMeterRight(Sender); FSMeter.PaintSMeterRight(Sender);
end; end;
initialization
GSkipAlphaFix := GetEnvironmentVariable('EWSDR_ALPHA_FIX') = '0';
end. end.
+29 -21
View File
@@ -273,9 +273,13 @@ const
DB_S: array[1..9] of Double = (-121,-115,-109,-103,-97,-91,-85,-79,-73); DB_S: array[1..9] of Double = (-121,-115,-109,-103,-97,-91,-85,-79,-73);
procedure BlendBitmapKey(Target, Source: TBitmap; DstX, DstY: Integer; Alpha: Byte); procedure BlendBitmapKey(Target, Source: TBitmap; DstX, DstY: Integer; Alpha: Byte);
// Дворд-блендинг (два 16-битных лейна на умножение, /256 вместо /255 —
// расхождение <0.4%): побайтовый вариант с div 255 на 2 флагах стоил ~0.6мс.
// Keyed-пиксели (магента) пропускаются сравнением цветовых байтов дворда.
var var
Y, X, SX0, SY0, SX1, SY1, TY, InvA: Integer; Y, X, SX0, SY0, SX1, SY1, TY: Integer;
SrcRow, DstRow: PByte; A256, I256, S, D: LongWord;
SrcRow, DstRow: PLongWord;
begin begin
if (Target = nil) or (Source = nil) or (Alpha = 0) then Exit; if (Target = nil) or (Source = nil) or (Alpha = 0) then Exit;
SX0 := 0; SY0 := 0; SX0 := 0; SY0 := 0;
@@ -286,37 +290,41 @@ begin
if DstY + (SY1 - SY0) > Target.Height then SY1 := SY0 + Target.Height - DstY; if DstY + (SY1 - SY0) > Target.Height then SY1 := SY0 + Target.Height - DstY;
if (SX1 <= SX0) or (SY1 <= SY0) then Exit; if (SX1 <= SX0) or (SY1 <= SY0) then Exit;
InvA := 255 - Alpha; A256 := Alpha + (Alpha shr 7); // 0..256
I256 := 256 - A256;
Target.BeginUpdate(False); Target.BeginUpdate(False);
Source.BeginUpdate(False); Source.BeginUpdate(False);
try try
for Y := SY0 to SY1 - 1 do for Y := SY0 to SY1 - 1 do
begin begin
TY := DstY + (Y - SY0); TY := DstY + (Y - SY0);
SrcRow := PByte(Source.ScanLine[Y]); SrcRow := PLongWord(Source.ScanLine[Y]);
DstRow := PByte(Target.ScanLine[TY]); DstRow := PLongWord(Target.ScanLine[TY]);
if (SrcRow = nil) or (DstRow = nil) then Continue; if (SrcRow = nil) or (DstRow = nil) then Continue;
Inc(SrcRow, SX0 * 4); Inc(SrcRow, SX0);
Inc(DstRow, DstX * 4); Inc(DstRow, DstX);
for X := SX0 to SX1 - 1 do for X := SX0 to SX1 - 1 do
begin begin
S := SrcRow^;
{$IFDEF DARWIN} {$IFDEF DARWIN}
if not ((SrcRow[1] = $FF) and (SrcRow[2] = $00) and (SrcRow[3] = $FF)) then if (S and $FFFFFF00) <> $FF00FF00 then // ARGB: R=FF,G=00,B=FF
begin
DstRow[1] := Byte((Alpha * SrcRow[1] + InvA * DstRow[1]) div 255);
DstRow[2] := Byte((Alpha * SrcRow[2] + InvA * DstRow[2]) div 255);
DstRow[3] := Byte((Alpha * SrcRow[3] + InvA * DstRow[3]) div 255);
end;
{$ELSE} {$ELSE}
if not ((SrcRow[0] = $FF) and (SrcRow[1] = $00) and (SrcRow[2] = $FF)) then if (S and $00FFFFFF) <> $00FF00FF then // BGRA: B=FF,G=00,R=FF
begin
DstRow[0] := Byte((Alpha * SrcRow[0] + InvA * DstRow[0]) div 255);
DstRow[1] := Byte((Alpha * SrcRow[1] + InvA * DstRow[1]) div 255);
DstRow[2] := Byte((Alpha * SrcRow[2] + InvA * DstRow[2]) div 255);
end;
{$ENDIF} {$ENDIF}
Inc(SrcRow, 4); begin
Inc(DstRow, 4); D := DstRow^;
DstRow^ := ((((S and $00FF00FF) * A256 + (D and $00FF00FF) * I256)
shr 8) and $00FF00FF)
or ((((S shr 8) and $00FF00FF) * A256 +
((D shr 8) and $00FF00FF) * I256) and $FF00FF00)
{$IFDEF DARWIN}
or $000000FF;
{$ELSE}
or $FF000000;
{$ENDIF}
end;
Inc(SrcRow);
Inc(DstRow);
end; end;
end; end;
finally finally
+1 -1
View File
@@ -61,7 +61,7 @@ EWSDR_PERF=0 ./bin/x86_64-linux/ewsdr-debug # выключить заме
| ui.vfo_overlay | UI | обновление главного VFO-флага | | ui.vfo_overlay | UI | обновление главного VFO-флага |
| ui.wideband_paint | UI | кадр wideband-панорамы | | ui.wideband_paint | UI | кадр wideband-панорамы |
| ui.web_push | UI | WebAdapter.PushState | | ui.web_push | UI | WebAdapter.PushState |
| ui.spec_grid…spec_alpha | UI | разбивка ui.draw_spectrum ПО ФАЗАМ (после перестройки): grid=ребилд+memcpy сетки, bands=raw-заливки полос, curve=точки+градиент, overlays=canvas-фаза (линии/текст/кривая/маркеры), alpha=композиты флагов+альфа-OR | | ui.spec_grid…spec_alpha | UI | разбивка ui.draw_spectrum (full-raw рендер): grid=ребилд+memcpy сетки, bands=raw-заливки полос, curve=точки+градиент, overlays=raw-линии/текст/кривая/маркеры, composites=бэндплан/панель/флаги, alpha=RawEnd/EndUpdate (+редкий canvas-путь ADC overload) |
| ui.gl_swap | UI | SwapBuffers отдельно — включает ожидание vsync, НЕ чистый CPU | | ui.gl_swap | UI | SwapBuffers отдельно — включает ожидание vsync, НЕ чистый CPU |
## Чего зоны НЕ видят ## Чего зоны НЕ видят
+7 -2
View File
@@ -17,9 +17,9 @@
<UseVersionInfo Value="True"/> <UseVersionInfo Value="True"/>
<AutoIncrementBuild Value="True"/> <AutoIncrementBuild Value="True"/>
<MinorVersionNr Value="9"/> <MinorVersionNr Value="9"/>
<BuildNr Value="239"/> <BuildNr Value="244"/>
</VersionInfo> </VersionInfo>
<MacroValues Count="57"> <MacroValues Count="62">
<Macro1 Name="LCLWidgetType" Value="qt6"/> <Macro1 Name="LCLWidgetType" Value="qt6"/>
<Macro2 Name="LCLWidgetType" Value="qt6"/> <Macro2 Name="LCLWidgetType" Value="qt6"/>
<Macro3 Name="LCLWidgetType" Value="qt6"/> <Macro3 Name="LCLWidgetType" Value="qt6"/>
@@ -77,6 +77,11 @@
<Macro55 Name="LCLWidgetType" Value="qt6"/> <Macro55 Name="LCLWidgetType" Value="qt6"/>
<Macro56 Name="LCLWidgetType" Value="qt6"/> <Macro56 Name="LCLWidgetType" Value="qt6"/>
<Macro57 Name="LCLWidgetType" Value="qt6"/> <Macro57 Name="LCLWidgetType" Value="qt6"/>
<Macro58 Name="LCLWidgetType" Value="qt6"/>
<Macro59 Name="LCLWidgetType" Value="qt6"/>
<Macro60 Name="LCLWidgetType" Value="qt6"/>
<Macro61 Name="LCLWidgetType" Value="qt6"/>
<Macro62 Name="LCLWidgetType" Value="qt6"/>
</MacroValues> </MacroValues>
<BuildModes> <BuildModes>
<Item Name="Debug" Default="True"/> <Item Name="Debug" Default="True"/>