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

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

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

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 00:11:31 +03:00
co-authored by Claude Fable 5
parent 0b4d126dac
commit 0adaff2d21
7 changed files with 617 additions and 247 deletions
+29 -19
View File
@@ -231,11 +231,18 @@ begin
end;
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