mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
feat(dxcluster): споты на всех панадаптерах + кнопка DX в блоке RX
Подписи спотов рисовались только на главном пане. Теперь их видит каждый пан, в чьё окно попадает частота спота. Оверлей — ЭКЗЕМПЛЯР НА ПАН, а не один общий: кэш полосы подписей ключуется центром/спаном/шириной, а у панов они свои — общий оверлей пересобирался бы на каждый пан каждый кадр, то есть ровно то, ради чего кэш и заводился. Живёт в TPanafallPanel (AttachDXSpots/DetachDXSpots/DXOverlay, Owner=панель), база спотов по-прежнему одна на всех. FDXSpotOverlay в MainForm стал алиасом на оверлей пана 0 — как FSpecView для FPan.View: настройки, тема и тик затухания идут общим циклом по FPans, и главный пан перестал быть особым случаем (иначе тумблер DX гасил бы подписи только на нём). ★ Version стора глобальна, а окно у пана своё, поэтому в EnsureRendered на смену версии сначала берётся снимок окна и сверяется его подпись (FNV-1a по позывному, частоте, моде и метке времени). Совпала — ни пересборки битмапа, ни перезаливки GL-текстуры: спот, севший на чужой диапазон, до этого пана не доходит. Без этого цена пересборки множилась бы на число панов. Снимок берётся один раз и переиспользуется раскладкой — стор второй раз не дёргаем. Клик по подписи на пане N = QSY слайса ЭТОГО пана (активного, если он здесь, иначе первого; нет ни одного — создаём в точке) с модой по комментарию кластера и полосой пресета этой моды. Своего VFO у панов N нет, а ретюнить их DDC под спот нельзя — увезло бы весь пан. Мода спота → режим вынесена в общую DXSpotRadioMode для обоих путей. Низкий пан (грид): полоса подписей и штрихи пропускаются целиком — раньше в GL-пути полоса легла бы поверх спектра, а штрихи пошли бы снизу вверх. Кнопка DX переехала из тулбара в блок RX левой панели, сразу после CTUN (ряд стал четырёхколоночным: CTUN | DX | Channel | BEACON): споты — часть приёмного вида, а не глобальная команда уровня DISCOVER/START/SETUP. Заодно снят расчёт SafeLeft, резервировавший под неё место в шапке. Подписи по умолчанию ВЫКЛЮЧЕНЫ (show_spots: дефолт и фолбэк чтения были True), состояние переживает перезапуск. Клик по кнопке подливает состояние в открытый SETUP: там своя копия конфига, и первая же правка любого поля страницы вернула бы подписи обратно. fix: Stop клиента кластера больше не держит главный поток. Он делал Terminate + WaitFor, а поток в этот момент сидит в блокирующем recv и просыпается лишь по своему кванту (до 1 с), на застрявшем send — до SO_SNDTIMEO. Всё это время окно висело на выходе и на переподключении после правки настроек. Теперь Stop рвёт живой сокет SockShutdown (хэндл — зеркало под тем же локом, поток снимает его ДО close, поэтому shutdown по закрытому fd невозможен), а FormDestroy зовёт неблокирующий RequestStop первой строкой: поток доживает параллельно с разборкой радио, и join в конце уже никого не ждёт. Сборка ewsdr (--ws=qt6) и ewsdrd — ОК. На железе не проверено: доп. паны требуют подключённого радио. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+93
-25
@@ -12,6 +12,11 @@ unit DXSpotOverlay;
|
||||
W × FBandH с постоянной альфой (BlendBitmapKey, тот же дворд-блендинг).
|
||||
Полноэкранный кэш W × H на кадр стоил бы в разы дороже — поэтому штрихи
|
||||
ниже полосы в кэш НЕ входят.
|
||||
• Версия стора глобальна, а окно у каждого пана своё, поэтому на смену
|
||||
версии сначала берётся снимок окна и сверяется его ПОДПИСЬ: спот, севший
|
||||
на чужой диапазон, до полосы подписей этого пана не доходит. Без этого
|
||||
каждый пан пересобирал бы кэш (и перезаливал GL-текстуру) на любой спот
|
||||
откуда угодно — цена, которая множится на число панов.
|
||||
• Штрихи от полосы до низа спектра рисует вызывающая сторона теми же
|
||||
примитивами, что и все прочие маркеры: CPU — RawVLine внутри RawBegin/
|
||||
RawEnd (никакого Canvas на битмапе кадра), GL — DrawLine. Оверлей отдаёт
|
||||
@@ -77,20 +82,27 @@ type
|
||||
FKeyLight: Boolean;
|
||||
FKeyRows: Integer;
|
||||
FKeyAgeTick: Int64;
|
||||
FKeyStamp: Int64; // подпись набора, по которому собран кэш
|
||||
FAgeTick: Int64; // бампается таймером UI — обновить затухание
|
||||
|
||||
FRenderVer: Int64; // ++ на каждую пересборку кэша — ключ GL-текстуры
|
||||
FLayoutTTL: Integer; // горизонт затухания, снят один раз на раскладку
|
||||
FLayoutTTL: Integer; // горизонт затухания, снят один раз на раскладку
|
||||
// Снимок окна: берётся при смене вида/версии стора, живёт до пересборки —
|
||||
// подпись считается по нему же, второй раз стор не дёргаем.
|
||||
FArr: TDXSpotArray;
|
||||
FArrN: Integer;
|
||||
FArrStamp: Int64;
|
||||
FItems: array of TDXSpotItem;
|
||||
FItemCount: Integer;
|
||||
FHidden: Integer; // сколько спотов не влезло в лесенку
|
||||
|
||||
function FreqToX(FreqHz: Double; W: Integer): Integer;
|
||||
function AgeColor(const S: TDXSpot): TColor;
|
||||
procedure TakeSnapshot;
|
||||
procedure Layout(C: TCanvas; W: Integer);
|
||||
procedure DrawSelf(C: TCanvas; W: Integer);
|
||||
procedure RebuildCache(W: Integer);
|
||||
function NeedsRebuild(W: Integer): Boolean;
|
||||
procedure RebuildCache(W: Integer; Ver: Int64);
|
||||
function ViewKeysChanged(W: Integer): Boolean;
|
||||
procedure SetMaxRows(V: Integer);
|
||||
public
|
||||
constructor Create(AOwner: TComponent); override;
|
||||
@@ -157,6 +169,13 @@ const
|
||||
CHIP_PAD_X = 3;
|
||||
CHIP_GAP = 3; // минимальный зазор между чипами в одном ряду
|
||||
|
||||
// Подпись видимого набора (FNV-1a 64). Базис — канонический $CBF29CE4…,
|
||||
// сложенный из половин: цельным литералом он не влезает в Int64, а
|
||||
// сравниваем мы подпись только сама с собой, так что важна лишь стабильность.
|
||||
FNV_BASIS_HI = QWord($CBF29CE4);
|
||||
FNV_BASIS_LO = QWord($84222325);
|
||||
FNV_PRIME: QWord = $00000100000001B3;
|
||||
|
||||
function Mix(A, B: TColor; Num, Den: Integer): TColor;
|
||||
// A→B на Num/Den (0 = A, Den = B).
|
||||
var ra, ga, ba, rb, gb, bb: Integer;
|
||||
@@ -190,6 +209,9 @@ begin
|
||||
FCacheDirty := True;
|
||||
FKeyCenter := -1; FKeySpan := -1; FKeyW := -1; FKeyVersion := -1;
|
||||
FKeyEnabled := False; FKeyLight := False; FKeyRows := -1; FKeyAgeTick := -1;
|
||||
FKeyStamp := 0;
|
||||
FArrN := 0;
|
||||
FArrStamp := 0;
|
||||
FAgeTick := 0;
|
||||
FRenderVer := 0;
|
||||
end;
|
||||
@@ -293,28 +315,55 @@ end;
|
||||
|
||||
{ ── Раскладка лесенки ────────────────────────────────────────────────────── }
|
||||
|
||||
procedure TDXSpotOverlay.TakeSnapshot;
|
||||
// Снимок видимого окна + его подпись. Подпись складывает всё, от чего зависит
|
||||
// картинка полосы: позывной, частоту, моду и метку времени (она правит цвет
|
||||
// затухания и обновляется при пере-споте). Совпала с прошлой — пересобирать
|
||||
// нечего, какой бы ни стала версия стора.
|
||||
var
|
||||
i, k: Integer;
|
||||
H: QWord;
|
||||
LoHz, HiHz: Double;
|
||||
begin
|
||||
FArrN := 0;
|
||||
SetLength(FArr, 0);
|
||||
FArrStamp := 0;
|
||||
if (FStore = nil) or (FSpanHz <= 0) then Exit;
|
||||
|
||||
// Горизонт затухания: явный фильтр по возрасту, иначе TTL стора (чтение
|
||||
// под локом — поэтому один раз на снимок, а не на каждый спот).
|
||||
FLayoutTTL := FMaxAgeMin;
|
||||
if FLayoutTTL <= 0 then FLayoutTTL := FStore.TTLMinutes;
|
||||
|
||||
LoHz := FCenterFreq - FSpanHz / 2;
|
||||
HiHz := FCenterFreq + FSpanHz / 2;
|
||||
FArrN := FStore.SnapshotRange(LoHz, HiHz, FModes, FMaxAgeMin, FArr);
|
||||
|
||||
H := (FNV_BASIS_HI shl 32) or FNV_BASIS_LO; // FNV-1a 64
|
||||
for i := 0 to FArrN - 1 do
|
||||
begin
|
||||
for k := 1 to Length(FArr[i].Call) do
|
||||
H := (H xor QWord(Ord(FArr[i].Call[k]))) * FNV_PRIME;
|
||||
H := (H xor QWord(Round(FArr[i].FreqHz))) * FNV_PRIME;
|
||||
H := (H xor QWord(Ord(FArr[i].Mode))) * FNV_PRIME;
|
||||
H := (H xor QWord(Round(FArr[i].Stamp * 86400))) * FNV_PRIME;
|
||||
end;
|
||||
FArrStamp := Int64(H);
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.Layout(C: TCanvas; W: Integer);
|
||||
// Споты приходят из стора отсортированными по частоте; каждому даём ПЕРВЫЙ
|
||||
// ряд сверху, где подпись не наезжает на предыдущую. Не нашлось ряда —
|
||||
// спот скрыт (считаем в FHidden), в лесенке дырок не оставляем.
|
||||
// спот скрыт (считаем в FHidden), в лесенке дырок не оставляем. Работаем по
|
||||
// снимку FArr — его взял TakeSnapshot перед пересборкой.
|
||||
var
|
||||
Arr: TDXSpotArray;
|
||||
N, i, r, TW, ChipW, ChipX, Row: Integer;
|
||||
RowRight: array[0..DXSPOT_MAX_ROWS-1] of Integer;
|
||||
LoHz, HiHz: Double;
|
||||
begin
|
||||
FItemCount := 0;
|
||||
FHidden := 0;
|
||||
if (FStore = nil) or (FSpanHz <= 0) or (W <= 0) then Exit;
|
||||
|
||||
// Горизонт затухания: явный фильтр по возрасту, иначе TTL стора (чтение
|
||||
// под локом — поэтому один раз на раскладку, а не на каждый спот).
|
||||
FLayoutTTL := FMaxAgeMin;
|
||||
if FLayoutTTL <= 0 then FLayoutTTL := FStore.TTLMinutes;
|
||||
|
||||
LoHz := FCenterFreq - FSpanHz / 2;
|
||||
HiHz := FCenterFreq + FSpanHz / 2;
|
||||
N := FStore.SnapshotRange(LoHz, HiHz, FModes, FMaxAgeMin, Arr);
|
||||
N := FArrN;
|
||||
if N = 0 then Exit;
|
||||
|
||||
for r := 0 to FMaxRows - 1 do RowRight[r] := -CHIP_GAP - 1;
|
||||
@@ -322,9 +371,9 @@ begin
|
||||
|
||||
for i := 0 to N - 1 do
|
||||
begin
|
||||
TW := C.TextWidth(Arr[i].Call);
|
||||
TW := C.TextWidth(FArr[i].Call);
|
||||
ChipW := TW + CHIP_PAD_X * 2;
|
||||
ChipX := FreqToX(Arr[i].FreqHz, W) - ChipW div 2;
|
||||
ChipX := FreqToX(FArr[i].FreqHz, W) - ChipW div 2;
|
||||
if ChipX < 0 then ChipX := 0;
|
||||
if ChipX + ChipW > W then ChipX := W - ChipW;
|
||||
if ChipX < 0 then Continue; // подпись шире окна — не рисуем
|
||||
@@ -343,11 +392,11 @@ begin
|
||||
end;
|
||||
RowRight[Row] := ChipX + ChipW;
|
||||
|
||||
FItems[FItemCount].X := FreqToX(Arr[i].FreqHz, W);
|
||||
FItems[FItemCount].X := FreqToX(FArr[i].FreqHz, W);
|
||||
FItems[FItemCount].Chip := Rect(ChipX, Row * FRowH + 2,
|
||||
ChipX + ChipW, Row * FRowH + FRowH);
|
||||
FItems[FItemCount].Color := AgeColor(Arr[i]);
|
||||
FItems[FItemCount].Spot := Arr[i];
|
||||
FItems[FItemCount].Color := AgeColor(FArr[i]);
|
||||
FItems[FItemCount].Spot := FArr[i];
|
||||
Inc(FItemCount);
|
||||
end;
|
||||
SetLength(FItems, FItemCount);
|
||||
@@ -404,7 +453,9 @@ end;
|
||||
|
||||
{ ── Кэш ──────────────────────────────────────────────────────────────────── }
|
||||
|
||||
function TDXSpotOverlay.NeedsRebuild(W: Integer): Boolean;
|
||||
function TDXSpotOverlay.ViewKeysChanged(W: Integer): Boolean;
|
||||
// Всё, что делает картинку заведомо другой, КРОМЕ версии стора: её разбирает
|
||||
// EnsureRendered отдельно (версия глобальна, окно — наше).
|
||||
var HalfPxHz: Double;
|
||||
begin
|
||||
if FStore = nil then Exit(False);
|
||||
@@ -414,7 +465,6 @@ begin
|
||||
if HalfPxHz < 1.0 then HalfPxHz := 1.0;
|
||||
Result := FCacheDirty or (FCacheW <> W) or (FKeyW <> W)
|
||||
or (FKeyEnabled <> FEnabled)
|
||||
or (FKeyVersion <> FStore.Version)
|
||||
or (FKeyLight <> FLightTheme)
|
||||
or (FKeyRows <> FMaxRows)
|
||||
or (FKeyAgeTick <> FAgeTick)
|
||||
@@ -422,7 +472,9 @@ begin
|
||||
or (Abs(FKeySpan - FSpanHz) >= HalfPxHz);
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.RebuildCache(W: Integer);
|
||||
procedure TDXSpotOverlay.RebuildCache(W: Integer; Ver: Int64);
|
||||
// Снимок (TakeSnapshot) к этому моменту уже взят вызывающим — здесь только
|
||||
// рисование и запись ключей.
|
||||
begin
|
||||
if (W <= 0) or (FStore = nil) then
|
||||
begin
|
||||
@@ -439,7 +491,8 @@ begin
|
||||
FKeySpan := FSpanHz;
|
||||
FKeyW := W;
|
||||
FKeyEnabled := FEnabled;
|
||||
FKeyVersion := FStore.Version;
|
||||
FKeyVersion := Ver;
|
||||
FKeyStamp := FArrStamp;
|
||||
FKeyLight := FLightTheme;
|
||||
FKeyRows := FMaxRows;
|
||||
FKeyAgeTick := FAgeTick;
|
||||
@@ -449,9 +502,24 @@ end;
|
||||
{ ── Рисование ────────────────────────────────────────────────────────────── }
|
||||
|
||||
procedure TDXSpotOverlay.EnsureRendered(W: Integer);
|
||||
var Ver: Int64;
|
||||
begin
|
||||
if (not Active) or (W <= 0) then Exit;
|
||||
if NeedsRebuild(W) then RebuildCache(W);
|
||||
Ver := FStore.Version;
|
||||
if ViewKeysChanged(W) then
|
||||
begin
|
||||
TakeSnapshot;
|
||||
RebuildCache(W, Ver);
|
||||
Exit;
|
||||
end;
|
||||
if FKeyVersion = Ver then Exit; // в сторе с прошлого кадра ничего
|
||||
// Стор правили — но, может, не в нашем окне: снимок с подписью дешевле
|
||||
// пересборки полосы (и, в GL, перезаливки текстуры). Версию запоминаем в
|
||||
// любом случае: этот вопрос уже разобран.
|
||||
FKeyVersion := Ver;
|
||||
TakeSnapshot;
|
||||
if FArrStamp = FKeyStamp then Exit;
|
||||
RebuildCache(W, Ver);
|
||||
end;
|
||||
|
||||
procedure TDXSpotOverlay.DrawOverlay(Target: TBitmap; W, H: Integer);
|
||||
|
||||
Reference in New Issue
Block a user