From 0adaff2d21f040bfeab150eaf5962c2721bf1d2b Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 3 Jul 2026 00:11:31 +0300 Subject: [PATCH] perf(spectrum): full-raw CPU renderer + dword blenders (frame 8.3ms -> 1.7ms) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- BandPlanOverlay.pas | 48 +-- PerfLog.pas | 6 +- SampleRateOverlay.pas | 47 +-- SpectrumView.pas | 702 +++++++++++++++++++++++++++++++----------- VfoOverlay.pas | 50 +-- doc/PERF_PROFILING.md | 2 +- ewsdr.lpi | 9 +- 7 files changed, 617 insertions(+), 247 deletions(-) diff --git a/BandPlanOverlay.pas b/BandPlanOverlay.pas index e8b93a2..b1e3f0f 100644 --- a/BandPlanOverlay.pas +++ b/BandPlanOverlay.pas @@ -231,11 +231,18 @@ begin end; function TBandPlanOverlay.NeedsRebuild(W: Integer): Boolean; +var HalfPxHz: Double; 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) or (FKeyW <> W) or (FKeyEnabled <> FEnabled) - or (Abs(FKeyCenter - FCenterFreq) >= 1.0) - or (Abs(FKeySpan - FSpanHz) >= 1.0); + or (Abs(FKeyCenter - FCenterFreq) >= HalfPxHz) + or (Abs(FKeySpan - FSpanHz) >= HalfPxHz); end; procedure TBandPlanOverlay.RebuildCache(W: Integer); @@ -252,15 +259,18 @@ end; procedure TBandPlanOverlay.CompositeStrip(Target: TBitmap; DstY: Integer); // Альфа-композит FCache поверх Target в (0, DstY) с постоянной BANDPLAN_ALPHA. -// Один проход по W × STRIP_H пикселей за кадр. +// Один проход по W × STRIP_H пикселей за кадр. Дворд-блендинг: два 16-битных +// лейна на умножение, деление /256 вместо /255 (расхождение <0.4%, глазом не +// видно) — побайтовый вариант с div 255 стоил ~1мс/кадр на полной ширине. var - X, Y, ty, InvA: Integer; - RS, RD: PByte; -const - A = BANDPLAN_ALPHA; + X, Y, ty, N: Integer; + A256, I256, S, D: LongWord; + RS, RD: PLongWord; begin 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); FCache.BeginUpdate(False); try @@ -268,22 +278,22 @@ begin begin ty := DstY + Y; if (ty < 0) or (ty >= Target.Height) then Continue; - RS := PByte(FCache.ScanLine[Y]); - RD := PByte(Target.ScanLine[ty]); + RS := PLongWord(FCache.ScanLine[Y]); + RD := PLongWord(Target.ScanLine[ty]); if (RS = nil) or (RD = nil) then Continue; - for X := 0 to FCacheW - 1 do + for X := 0 to N - 1 do 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} - RD[1] := Byte((A * RS[1] + InvA * RD[1]) div 255); - RD[2] := Byte((A * RS[2] + InvA * RD[2]) div 255); - RD[3] := Byte((A * RS[3] + InvA * RD[3]) div 255); + or $000000FF; // ARGB: альфа в байте 0 {$ELSE} - RD[0] := Byte((A * RS[0] + InvA * RD[0]) div 255); - RD[1] := Byte((A * RS[1] + InvA * RD[1]) div 255); - RD[2] := Byte((A * RS[2] + InvA * RD[2]) div 255); + or $FF000000; // BGRA: альфа в байте 3 {$ENDIF} - Inc(RS, 4); Inc(RD, 4); + Inc(RS); Inc(RD); end; end; finally diff --git a/PerfLog.pas b/PerfLog.pas index 28281ca..eb0278a 100644 --- a/PerfLog.pas +++ b/PerfLog.pas @@ -77,7 +77,8 @@ type pzUiSpecCurve, // расчёт точек кривой + градиент под кривой + Polyline pzUiSpecOverlays, // края/VFO/маркеры/бэндплан/флаги (DrawOverlay в битмап) pzUiSpecAlpha, // финальный альфа-OR проход по всему битмапу - pzUiGlSwap // SwapBuffers (GL) — включает ожидание vsync! + pzUiGlSwap, // SwapBuffers (GL) — включает ожидание vsync! + pzUiComposites // композиты бэндплан/панель/флаги (внутри raw-лока) ); const @@ -113,7 +114,8 @@ const 'ui.spec_curve', 'ui.spec_overlays', 'ui.spec_alpha', - 'ui.gl_swap' + 'ui.gl_swap', + 'ui.composites' ); var diff --git a/SampleRateOverlay.pas b/SampleRateOverlay.pas index 4833f70..89a10bf 100644 --- a/SampleRateOverlay.pas +++ b/SampleRateOverlay.pas @@ -383,7 +383,12 @@ end; // Альфа-композит FCache поверх Target в точке (FLeft, FTop) — один проход/кадр. 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 if (Target = nil) or (FCacheW <= 0) then Exit; h := FCache.Height; @@ -393,33 +398,37 @@ begin for Y := 0 to h - 1 do begin if (FTop + Y < 0) or (FTop + Y >= Target.Height) then Continue; - RS := PByte(FCache.ScanLine[Y]); - RD := PByte(Target.ScanLine[FTop + Y]); + RS := PLongWord(FCache.ScanLine[Y]); + RD := PLongWord(Target.ScanLine[FTop + Y]); if (RS = nil) or (RD = nil) then Continue; - Inc(RD, FLeft * 4); + Inc(RD, FLeft); for X := 0 to FCacheW - 1 do begin if (FLeft + X >= 0) and (FLeft + X < Target.Width) then begin + S := RS^; {$IFDEF DARWIN} - A := RS[0]; InvA := 255 - A; - 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; + A256 := S and $FF; // ARGB: альфа в байте 0 {$ELSE} - A := RS[3]; InvA := 255 - A; - 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; + A256 := S shr 24; // BGRA: альфа в байте 3 {$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; - Inc(RS, 4); Inc(RD, 4); + Inc(RS); Inc(RD); end; end; finally diff --git a/SpectrumView.pas b/SpectrumView.pas index 7477034..41c35ea 100644 --- a/SpectrumView.pas +++ b/SpectrumView.pas @@ -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; // '||' + 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. diff --git a/VfoOverlay.pas b/VfoOverlay.pas index 5eea9fc..b166f8f 100644 --- a/VfoOverlay.pas +++ b/VfoOverlay.pas @@ -273,9 +273,13 @@ const 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); +// Дворд-блендинг (два 16-битных лейна на умножение, /256 вместо /255 — +// расхождение <0.4%): побайтовый вариант с div 255 на 2 флагах стоил ~0.6мс. +// Keyed-пиксели (магента) пропускаются сравнением цветовых байтов дворда. var - Y, X, SX0, SY0, SX1, SY1, TY, InvA: Integer; - SrcRow, DstRow: PByte; + Y, X, SX0, SY0, SX1, SY1, TY: Integer; + A256, I256, S, D: LongWord; + SrcRow, DstRow: PLongWord; begin if (Target = nil) or (Source = nil) or (Alpha = 0) then Exit; SX0 := 0; SY0 := 0; @@ -286,37 +290,41 @@ begin if DstY + (SY1 - SY0) > Target.Height then SY1 := SY0 + Target.Height - DstY; if (SX1 <= SX0) or (SY1 <= SY0) then Exit; - InvA := 255 - Alpha; + A256 := Alpha + (Alpha shr 7); // 0..256 + I256 := 256 - A256; Target.BeginUpdate(False); Source.BeginUpdate(False); try for Y := SY0 to SY1 - 1 do begin TY := DstY + (Y - SY0); - SrcRow := PByte(Source.ScanLine[Y]); - DstRow := PByte(Target.ScanLine[TY]); + SrcRow := PLongWord(Source.ScanLine[Y]); + DstRow := PLongWord(Target.ScanLine[TY]); if (SrcRow = nil) or (DstRow = nil) then Continue; - Inc(SrcRow, SX0 * 4); - Inc(DstRow, DstX * 4); + Inc(SrcRow, SX0); + Inc(DstRow, DstX); for X := SX0 to SX1 - 1 do begin + S := SrcRow^; {$IFDEF DARWIN} - if not ((SrcRow[1] = $FF) and (SrcRow[2] = $00) and (SrcRow[3] = $FF)) then - 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; + if (S and $FFFFFF00) <> $FF00FF00 then // ARGB: R=FF,G=00,B=FF {$ELSE} - if not ((SrcRow[0] = $FF) and (SrcRow[1] = $00) and (SrcRow[2] = $FF)) then - 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; + if (S and $00FFFFFF) <> $00FF00FF then // BGRA: B=FF,G=00,R=FF {$ENDIF} - Inc(SrcRow, 4); - Inc(DstRow, 4); + begin + 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; finally diff --git a/doc/PERF_PROFILING.md b/doc/PERF_PROFILING.md index 43449f9..b58065a 100644 --- a/doc/PERF_PROFILING.md +++ b/doc/PERF_PROFILING.md @@ -61,7 +61,7 @@ EWSDR_PERF=0 ./bin/x86_64-linux/ewsdr-debug # выключить заме | ui.vfo_overlay | UI | обновление главного VFO-флага | | ui.wideband_paint | UI | кадр wideband-панорамы | | 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 | ## Чего зоны НЕ видят diff --git a/ewsdr.lpi b/ewsdr.lpi index 4dd4370..0b17dae 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -17,9 +17,9 @@ - + - + @@ -77,6 +77,11 @@ + + + + +