feat(slices): multi-slice receivers (FlexRadio-style) — soft fan-out + per-slice flag/audio

Independent RX slices sharing one panadapter: each has its own
freq/mode/filter/AGC/NR-NB-SNB-ANF, S-meter, volume and sound card.
VFO A = main receiver; B+ are soft fan-out WDSP channels within the
captured span (ideal for Pluto/QO-100; HW DDC path is Stage 3).

- WDSPEngine: soft fan-out (TSliceChan, ProcessSlices in DSP thread,
  per-slice NCO/mode/filter/AGC/NR/NB/SNB/ANF + S-meter, OnSliceAudio),
  FSliceLock serialises add/remove/set vs DSP; reopened on rate change.
- RadioController: TCtrlSlice with its own TAudioOutput/device,
  Add/Remove/SetSlice*, RefreshSliceShifts keeps slices on target Hz
  across centre changes. Slice letters start at 'B' (A = main receiver).
- UI: per-slice flags (VfoOverlay) with letter badge/close, filter-band
  markers in BOTH GL and CPU renderers, drag-retune, wheel; flags placed
  beside the filter band with flip, like the main flag. Ctrl+click adds.
- Main flag (slice A) visible whenever the left panel is hidden OR any
  slice exists; synced with panel-driven DSP/mode/AGC changes.
- Perf: S-meter decoupled from flag chrome cache (FMeterDirty +
  meter-only RefreshMeter via EnsureRendered); InvalidateVfoOverlay
  avoids global overlay-cache churn on meter ticks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 23:46:30 +03:00
co-authored by Claude Opus 4.8
parent a04c592f1d
commit 1527b0f849
8 changed files with 1915 additions and 139 deletions
+112 -7
View File
@@ -43,6 +43,12 @@ type
FSampleOverlayTex: TGLTextureCache;
FVfoOverlayTex: TGLTextureCache;
FBandOverlayTex: TGLTextureCache;
// Текстуры флагов слайсов (B+), привязка по указателю оверлея.
FSliceTex: array of record
Overlay: TVfoOverlay;
Tex: TGLTextureCache;
LastX, LastY, LastW, LastH: Integer;
end;
FLastAGCText: string;
FLastAGCY: Integer;
FLastAGCHangY: Integer;
@@ -83,6 +89,9 @@ type
procedure DrawBeaconMarkersGL(W, H: Integer);
procedure DrawADCOverlay(W, H: Integer);
procedure DrawCachedOverlays(W, H: Integer);
procedure DrawSliceOverlays(W, H: Integer);
procedure DrawSliceFilterMarkers(W, H: Integer);
function SliceTexIndex(O: TVfoOverlay): Integer; // индекс в FSliceTex, -1 если нет
function FormatFreqGL(Hz: Double): string;
function ActiveVfoFrequency: Double;
public
@@ -96,6 +105,7 @@ type
procedure PaintSpectrum(Sender: TObject); override;
procedure InvalidateGridCache; override;
procedure InvalidateOverlayCache; override;
procedure InvalidateVfoOverlay; override;
procedure SetTheme(const T: TAppTheme); override;
end;
@@ -138,6 +148,7 @@ begin
end;
destructor TSpectrumViewOpenGL.Destroy;
var i: Integer;
begin
if Assigned(FGLControl) and FGLControl.MakeCurrent then
begin
@@ -149,7 +160,9 @@ begin
DeleteTexture(FSampleOverlayTex);
DeleteTexture(FVfoOverlayTex);
DeleteTexture(FBandOverlayTex);
for i := 0 to High(FSliceTex) do DeleteTexture(FSliceTex[i].Tex);
end;
SetLength(FSliceTex, 0);
inherited Destroy;
end;
@@ -392,6 +405,13 @@ begin
FVfoOverlayDirty := True;
end;
procedure TSpectrumViewOpenGL.InvalidateVfoOverlay;
begin
// Только текстура главного флага — НЕ трогаем FOverlayDirty, иначе band/
// samplerate/слайсы зря перезаливались бы на каждый тик S-метра главного.
FVfoOverlayDirty := True;
end;
procedure TSpectrumViewOpenGL.SetTheme(const T: TAppTheme);
begin
inherited SetTheme(T);
@@ -536,6 +556,29 @@ begin
DrawLine(TXVfoX, 0, TXVfoX, H - 12, TColor($002030E0), 2, False);
DrawRect(TXVfoX - 5, 0, TXVfoX + 5, 8, TColor($002030E0), 1);
end;
DrawSliceFilterMarkers(W, H);
end;
// Маркер фильтра каждого слайса (B+) на спектре: полупрозрачная полоса
// пропускания + центральная несущая. Цвет янтарный — отличить от главного.
procedure TSpectrumViewOpenGL.DrawSliceFilterMarkers(W, H: Integer);
const
SLICE_CLR = TColor($0010A8F0); // янтарный (BGR), как акцент TX-select флага
var
i, SX1, SX2, SVfoX: Integer;
O: TVfoOverlay;
begin
for i := 0 to SliceOverlayCount - 1 do
begin
O := SliceOverlayAt(i);
if (O = nil) or (not O.Visible) then Continue;
CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, SX1, SX2, SVfoX);
if SX2 > SX1 then DrawRect(SX1, 0, SX2, H, SLICE_CLR, 60 / 255);
DrawLine(SX1, 0, SX1, H, SLICE_CLR, 1, False);
DrawLine(SX2, 0, SX2, H, SLICE_CLR, 1, False);
DrawLine(SVfoX, 0, SVfoX, H - 12, SLICE_CLR, 2, False);
end;
end;
procedure TSpectrumViewOpenGL.DrawAGCLines(W, H: Integer; DBmax,
@@ -783,16 +826,11 @@ begin
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
begin
if FOverlayDirty or FVfoOverlayDirty or FVfoOverlayTex.Dirty or
FVfoOverlay.CacheDirty or FVfoOverlay.MeterDirty or
(FLastVfoX <> FVfoOverlay.Left) or (FLastVfoY <> FVfoOverlay.Top) or
(FLastVfoW <> FVfoOverlay.Width) or (FLastVfoH <> FVfoOverlay.Height) then
begin
B := TBitmap.Create;
try
FVfoOverlay.DrawOverlayBitmap(B);
UploadBitmap(FVfoOverlayTex, B, True, VFO_OVERLAY_ALPHA);
finally
B.Free;
end;
UploadBitmap(FVfoOverlayTex, FVfoOverlay.EnsureRendered, True, VFO_OVERLAY_ALPHA);
FLastVfoX := FVfoOverlay.Left;
FLastVfoY := FVfoOverlay.Top;
FLastVfoW := FVfoOverlay.Width;
@@ -801,9 +839,76 @@ begin
end;
DrawTexture(FVfoOverlayTex, FVfoOverlay.Left, FVfoOverlay.Top);
end;
DrawSliceOverlays(W, H);
FOverlayDirty := False;
end;
function TSpectrumViewOpenGL.SliceTexIndex(O: TVfoOverlay): Integer;
var i: Integer;
begin
Result := -1;
for i := 0 to High(FSliceTex) do
if FSliceTex[i].Overlay = O then Exit(i);
end;
// Флаги слайсов (B+): своя GL-текстура на флаг (привязка по указателю оверлея).
// Текстуры «сирот» (флаг удалён) вычищаются здесь же.
procedure TSpectrumViewOpenGL.DrawSliceOverlays(W, H: Integer);
var
n, i, ti, j: Integer;
O: TVfoOverlay;
Alive: Boolean;
begin
// 1. Убрать текстуры флагов, которых уже нет в списке.
i := 0;
while i <= High(FSliceTex) do
begin
Alive := False;
for j := 0 to SliceOverlayCount - 1 do
if SliceOverlayAt(j) = FSliceTex[i].Overlay then begin Alive := True; Break; end;
if not Alive then
begin
DeleteTexture(FSliceTex[i].Tex);
for j := i to High(FSliceTex) - 1 do FSliceTex[j] := FSliceTex[j + 1];
SetLength(FSliceTex, Length(FSliceTex) - 1);
end
else
Inc(i);
end;
// 2. Нарисовать каждый видимый флаг, при нужде (пере)залив текстуру.
n := SliceOverlayCount;
for i := 0 to n - 1 do
begin
O := SliceOverlayAt(i);
if (O = nil) or (not O.Visible) then Continue;
ti := SliceTexIndex(O);
if ti < 0 then
begin
SetLength(FSliceTex, Length(FSliceTex) + 1);
ti := High(FSliceTex);
FSliceTex[ti].Overlay := O;
FSliceTex[ti].Tex.Dirty := True;
FSliceTex[ti].LastX := MaxInt; // форсируем первую заливку
end;
// Перезаливаем текстуру слайса ТОЛЬКО когда изменился он сам (CacheDirty:
// режим/громкость, MeterDirty: S-метр) или его позиция — не по глобальному
// FOverlayDirty. При MeterDirty EnsureRendered перерисует лишь полоску метра.
if O.CacheDirty or O.MeterDirty or FSliceTex[ti].Tex.Dirty or
(FSliceTex[ti].LastX <> O.Left) or (FSliceTex[ti].LastY <> O.Top) or
(FSliceTex[ti].LastW <> O.Width) or (FSliceTex[ti].LastH <> O.Height) then
begin
UploadBitmap(FSliceTex[ti].Tex, O.EnsureRendered, True, VFO_OVERLAY_ALPHA);
FSliceTex[ti].LastX := O.Left;
FSliceTex[ti].LastY := O.Top;
FSliceTex[ti].LastW := O.Width;
FSliceTex[ti].LastH := O.Height;
end;
DrawTexture(FSliceTex[ti].Tex, O.Left, O.Top);
end;
end;
procedure TSpectrumViewOpenGL.PaintSpectrum(Sender: TObject);
var
W, H: Integer;