mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
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:
+29
-19
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|
||||||
## Чего зоны НЕ видят
|
## Чего зоны НЕ видят
|
||||||
|
|||||||
@@ -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"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user