# Мультислайсы и панадаптеры (как во FlexRadio)

Статус: **Этапы 1 + 2 РЕАЛИЗОВАНЫ и ЗАКОММИЧЕНЫ** (ветка `feature/multislice`,
коммит `1527b0f`; проверено на эфире) + UI-полировка флагов (`30a6616`,
`3049f91`, `a3ac459`).

**2026-07-10: Этап 3 ПЕРЕПРОЕКТИРОВАН.** Вместо старой идеи «off-span слайс с
edge-маркером» принята Flex-модель: **аппаратный DDC = отдельный панадаптер**
(свой спектр+водопад), а слайсы ВСЕГДА живут внутри какого-то панадаптера.
Старый черновик (edge-маркеры + slices-рейл) перенесён в «Отвергнутые
альтернативы» в конце файла. Реализация Этапа 3 не начата.

---

## Что реализовано (Этапы 1 + 2)

- **WDSPEngine** — софтовый fan-out: `TSliceChan` + `FSlices[0..MAX_SLICES-1]`
  (`MAX_SLICES=6`), свои WDSP-каналы (`SLICE_CHAN_BASE=2`), `ProcessSlices` в
  DSP-потоке (свежая копия IQ на канал, свой NCO/mode/filter/AGC/NR/NB/SNB/ANF +
  S-метр → `OnSliceAudio`), `FSliceLock` сериализует add/remove/set vs DSP,
  переоткрытие после смены sample rate.
- **RadioController** — `TCtrlSlice` (абс. `TargetHz`, буква, свой `TAudioOutput`/
  звуковуха), `Add/Remove/SetSlice*`, `RefreshSliceShifts` держит слайсы на
  `TargetHz` при смене центра, гейт `SliceFitsCapture` (вне полосы = Этап 3).
  Буквы доп. слайсов начинаются с `'B'` (A = главный приёмник).
- **UI** — по флагу `TVfoOverlay` на слайс (буква-бейдж, «×», fly-out MODE/DSP/AUD,
  vol/mute/split/TX, S-метр), маркеры полосы фильтра слайса в **обоих** рендерерах
  (GL `DrawSliceFilterMarkers` + CPU), drag-ретюн несущей, колесо над флагом.
  Ctrl+ЛКМ по спектру = добавить слайс. Флаг главного (A) виден когда скрыта
  левая панель ЛИБО есть слайсы B+ (синхронизируется с правками с левой панели).
- **UI-полировка флагов** (`30a6616`, `3049f91`, `a3ac459`):
  - `MainForm.LayoutFlags` — единый packing всех флагов (A+B+) в один ряд;
    идентификация уехавшего флага буквой по центру полосы (`DrawBandLetter`
    CPU + `DrawBandLetterGL` GL с кэшем текстур букв); палитра
    `VfoOverlay.SliceColor(L)`; кнопки через `FlatButton.PaintFlatButton`;
    полная тематизация из `TAppTheme`.
- **Перф** — S-метр развязан с кэшем «хрома» флага (`FMeterDirty` + meter-only
  `RefreshMeter` через `EnsureRendered`); `InvalidateVfoOverlay` не дёргает
  глобальный перезалив оверлеев на тик метра.

Остаток Этапов 1-2 (не сделано): персист слайсов в `Settings` (список +
устройство); per-slice AGC-Top (сейчас общий с главным); полноценный
мультислайс-TX (пока TX только главный).

---

## Модель FlexRadio (итог разбора 2026-07-10)

Во Flex **две независимые сущности**, и это ключ ко всему дизайну:

- **Панадаптер (panafall)** — спектр+водопад. Самостоятельный объект: свой
  широкополосный захват в железе, свой центр/span/зум, свои настройки водопада.
- **Слайс** — узкополосный приёмник, ВСЕГДА прикреплён к какому-то панадаптеру
  и рисуется на нём флагом. Слайс никогда не имеет собственного спектра.
  Тянешь слайс за край — панорама скроллится за ним.

Хочешь слушать другой участок — открываешь **второй панадаптер** (радио выделяет
ещё один захват) и создаёшь слайсы в нём. Лимиты: 6400 — 2 пана/2 слайса,
6600 — 4/4, 6700 — 8/8.

**Второй приёмник (второй АЦП)** у Flex называется SCU (Spectral Capture Unit):
полный тракт «антенный вход → АЦП → захват». 6600/6700 — два SCU, 6400 — один.
У панадаптера НЕТ переключателя «RX1/RX2» — есть селектор **антенны**
(ANT1/ANT2/RXA/RXB/XVTR), и радио само сажает панадаптер на SCU под выбранную
антенну. Слайсы наследуют антенну своего пана. Diversity (когерентное сложение
двух SCU) — отдельный режим поверх.

**Окна:** все панафоллы тайлятся ВНУТРИ одного главного окна (сплиты с
перетаскиваемыми разделителями). Вынос панафолла в отдельное OS-окно штатный
SmartSDR НЕ умеет — наш pop-out будет улучшением поверх Flex-модели.

---

## Итоговая модель EWSDR

```
Панадаптер (спектр+водопад; свой захват IQ; свой ADC-источник; свой span/зум/
│           палитра/wf-AGC; свой WDSP-анализатор)
└─ Слайсы (флаги внутри панорамы; софт-fan-out IQ ЭТОГО панадаптера;
           свой WDSP-канал/режим/фильтр/AGC/DSP/звуковуха — уже реализовано)
```

Правила:
1. Слайс всегда принадлежит панадаптеру (`PanId`). Понятия «off-span слайс»
   больше нет: хочешь слайс вне захвата — создай панадаптер там.
2. Пан 0 (главный) существует всегда; его «слайс A» — текущий главный тракт
   (WDSP-канал 0). Панадаптеры N>0 живут целиком на слайс-механике Этапа 1
   (каналы `SLICE_CHAN_BASE+`), при создании пана автоматически создаётся
   один слайс в его центре (поведение Flex).
3. ADC-источник — свойство **панадаптера**, не слайса (бейдж `A1`/`A2` в шапке).
   В отличие от Flex показываем именно АЦП, а не антенну: на HPSDR-платах
   маппинг «вход→АЦП» жёсткий. Антенны ADC0 (Alex ANT1/2/3) — отдельная
   ГЛОБАЛЬНАЯ ручка, общая для всех панов этого АЦП (детали в 3.4).
4. Буквы слайсов — глобальный пул A..G через все паны (как во Flex A-H):
   буква = личность слайса, пан = место жительства.
5. Возможности бэкенда описывает запись capabilities (см. таблицу) — UI по ней
   прячет/блокирует кнопки, а не хардкодит «if Pluto».

Матрица бэкендов:

| Бэкенд                  | Панадаптеров | Незав. частоты панов | АЦП | Примечание |
|-------------------------|--------------|----------------------|-----|------------|
| HPSDR 1 ADC (Hermes)    | до 4 (DDC)   | да                   | 1   | ADC-селектор скрыт |
| HPSDR 2 ADC (Angelia/Orion/Saturn) | до 4 (DDC, DDCBase=2) | да | 2 | селектор A1/A2 на пан |
| Pluto (обычный)         | 1            | —                    | 1   | как сейчас, ничего не меняется |
| Pluto+ (2r2t)           | 2            | **нет — общий LO**   | 2   | пан RX2 частотно связан с RX1 |

```
openHPSDR (Protocol 2, MAX_DDCS=80):
  ADC0 ─┬─ DDC2 (широкий) → Пан 0 → анализатор + слайсы A,B…
        └─ DDC3 (узкий)   → Пан 1 → анализатор + слайсы C,D…
  ADC1 ──── DDC4          → Пан 2 (другая антенна) → …
  • Каждый DDC тюнится куда угодно, свой rate. DDC0/1 заняты PureSignal
    на Angelia/Orion/Saturn (DDCBase=2, HPSDRNetwork.pas:1485).

Pluto+ (AD9363 2r2t, libiio):
  RX LO ─┬─ RX1 → псевдо-DDC0 → Пан 0
  (один!)└─ RX2 → псевдо-DDC1 → Пан 1 (та же частота/rate, другая антенна)
  • Оба канала на ОДНОМ LO/rate, фазокогерентны (база под diversity/пеленгацию).
  • Обычный Pluto: один RX разведён на плате — Пан 1 недоступен.
```

---

## Этап 3 (переработан): «DDC = панадаптер»

Подэтапы упорядочены так, чтобы каждый собирался и работал сам по себе.
3.0 — чистый рефакторинг без новых фич, дальше функциональность наращивается.

### 3.0 — TPanafallPanel: выделить панадаптер из MainForm

**Статус: фазы A и B1 СДЕЛАНЫ (коммиты c125e59, cc9919c) и ПРОВЕРЕНЫ
пользователем на экране (A — 2026-07-10, B1 — 2026-07-11: создание/закрытие
флагов, видимость флага A, мышь/колесо/drag, тик S-метров — работает).**
Создан `PanafallPanel.pas`:
- Фаза A: панель владеет view (CPU/GL), контролами (спектр/линейка/сплиттер/
  водопад/пан-зум/кнопки зума), полной геометрией стека (`Layout`), механикой
  сплиттера и мышью пан/зум-полосы. MainForm держит алиасы (`FSpecView`,
  `PbSpectrum`, …) на члены `FPan`; размеры областей — из
  `FPan.SpectrumWidth/SpectrumHeight/WaterfallHeight`.
- Фаза B1: панель владеет коллекцией флагов слайсов (LayoutFlags, диспетчер
  мыши флагов, Create/DestroySliceFlag, PushSliceFlagState, TickFlags,
  UpdateMainFlagVisibility). Флаг A создаёт MainForm и отдаёт через
  `AttachMainFlag`; семантические события флагов проводятся `OnWireFlag`,
  роутинг по `FPan.CurSliceId`.

**Решение по фазе B2 (мышь спектра/водопада, dirty-флаги/тик, SampleRate/
BandPlan-оверлеи): НЕ переносить сейчас.** Семантика мыши (тюн VFO A/B,
XVTR-клампы, маяк, CTUN-drag) станет per-pan только в 3.3, когда появится
API «ретюн пана» — перенос сейчас потребовал бы хуков (ApplyVfoAHook,
BeaconSeedHook…), которые в 3.3 переделались бы заново. Мышь/тик переезжают
в панель В СОСТАВЕ 3.3 вместе с per-pan семантикой. Для 3.1/3.2 текущего
состояния панели достаточно.
**GL-аудит per-instance ПРОЙДЕН**: в `SpectrumViewOpengl`/`WaterfallViewOpengl`
все текстуры (`FGridLabelTex`…`FLetterTex`, `FSliceTex`, `FTexture`,
`FMarkerTex`) и LUT водопада — поля экземпляра, глобальных/class var GL-ресурсов
нет; палитры — read-only const (шарить безопасно).

Самый большой и самый важный кусок. Сейчас панадаптер размазан по MainForm:
`FSpecView: TSpectrumView` создаётся и прошивается ~20 свойствами напрямую
(`MainForm.pas:1018-1035`), канва `PbSpectrum: TControl` (TOpenGLControl,
`MainForm.pas:2220`), `FPanZoomBar` (`MainForm.pas:312`), мышиные хендлеры
`PbSpectrumMouse*` (`MainForm.pas:439-445`), `LayoutFlags`, тик обновления.

Сделать composite `TPanafallPanel` (новый модуль `PanafallPanel.pas`,
наследник TCustomControl/TPanel), который владеет:

- канвой (`TOpenGLControl` или `TPaintBox` по `USE_OPENGL` — как сейчас);
- своим `TSpectrumView`/`TSpectrumViewOpenGL`;
- своим `TPanZoomBar` и линейкой;
- коллекцией флагов слайсов этого пана (`FVfoOverlay` пана + `FSliceFlags`)
  и `LayoutFlags` (packing переезжает сюда);
- мышиной логикой канвы (тюн/drag/колесо/Ctrl+ЛКМ/drag маркера слайса);
- шапкой пана (см. 3.3) — в пане 0 шапка скрыта до появления пана 1.

Наружу: события `OnTune(PanId, Hz)`, `OnAddSlice(PanId, Hz)`, `OnSlice*`
(существующие слайс-события + PanId), `OnPanClose/OnPanPopOut/OnPanADC`;
входные сеттеры состояния (центр/span/mode/wf-настройки) — то, что сейчас
раздаётся FSpecView напрямую.

MainForm после рефакторинга держит `FPans: array of TPanafallPanel`
(пан 0 всегда), общий тик раздаёт кадры по панам, весь остальной код
(левая панель, FreqDisplay, контроллер) работает с «активным слайсом» —
механика `FActiveFlag/CurSliceId` уже есть, добавляется лишь `ActivePan`.

Критерий готовности 3.0: поведение приложения ИДЕНТИЧНО текущему
(один пан, все фичи слайсов работают), diff в MainForm — только вынос.

Отдельная задача внутри 3.0 — **аудит GL-ресурсов** `SpectrumViewOpengl` на
per-instance: каждое окно/канва = свой GL-контекст, текстуры между контекстами
НЕ шарятся. Проверить: текстуры водопада, LUT палитры, кэш текстур букв
(`DrawBandLetterGL`), текстуры флагов (`FSliceTex`), шейдеры/списки — ничего
static/глобального. (CPU-LUT палитры общий — это ок.)

### 3.1 — Бэкенд HPSDR: per-DDC частота/rate/ADC + роутинг

Плита готова ~на 70 %, дельты точечные:

1. **Per-DDC частота в High Priority.** `SendFullHP`
   (`HPSDRNetwork.pas:1552`) пишет частотное слово только активного DDC
   (`Buf[9 + DDCBase*4]`). Сделать цикл по активным DDC:
   `Buf[9+ddc*4] := FreqToPhaseWord(FDDCFreq[ddc])`. Контроллер держит
   `FDDCFreq: array[0..MAX_DDCS-1] of Double`, обновляет при ретюне пана.
2. **Per-DDC rate/ADC/enable в `ConfigureDDCs`** (`HPSDRNetwork.pas:1460`):
   сейчас один rate/ADCSource на все N. Принимать массив записей
   `(Enabled, RateKHz, ADCSource)` — протокол это позволяет
   (`DDCConfig[ddc*6+0]=ADCSource`, rate там же per-DDC).
3. **Роутинг IQ по панам.** `RadioController.OnDDCIQ`
   (`RadioController.pas:828`) сейчас гейтит `DDCIndex = FActiveDDC`
   (`:875`), остальное выбрасывает. Заменить на маппинг
   `DDCIndex → PanId`: пакет пана 0 — в существующий путь, пакеты
   панов N>0 — в `WDSPEngine.PushPanIQ(PanId, Data)` (см. 3.2).
4. **Аллокатор DDC**: пул hw-индексов от `DDCBase+1` (DDCBase=2 на
   PureSignal-платах, `HPSDRNetwork.pas:1485`), захват при создании пана,
   освобождение при закрытии, слот-стабильность как у слайсов.
5. **Rate слайс-панов**: выбор из 192/384/768 кГц (дефолт 384). Бюджет
   Ethernet: поток DDC ≈ rate × 6 байт (24-бит I+Q) → 384 кГц ≈ 18 Мбит/с;
   3 доп. пана + главный на 1.536 МГц укладываются даже в 100 Мбит с запасом,
   но кап «до 4 панов» держим жёстко (и по CPU тоже).

Ретюн пана N>0 = запись `FDDCFreq[ddc]` + `SendFullHP` (без реконфига DDC);
смена rate/ADC = `ConfigureDDCs` (как сейчас при смене sample rate).

**СТАТУС 3.1 (2026-07-11): РЕАЛИЗОВАНО, собирается (ewsdr qt6 + ewsdrd),
на эфире не проверено (проверяемо только с 3.2/3.3 — панов ещё никто не
создаёт).** Отличие от текста плана: per-DDC состояние (freq/rate/ADC/enable)
живёт в `THPSDRNetwork` (`FPanDDC*`, пишет `SetPanDDC`/`SetPanDDCFreq` из
`TRadioBackend`), а контроллер держит только per-pan маппинг — эквивалентно
`FDDCFreq[]` из плана, но HP-кадр собирается из одного места. Сделано:

- `TBackendCaps.MaxPans`/`IndependentPanFreq` (HPSDR 4/True, Pluto 1/False);
  `SetPanDDC`/`SetPanDDCFreq` — виртуальные no-op в базе, override в HPSDR.
- `HPSDRNetwork`: `ConfigureDDCs` → кэш главного DDC + `RebuildDDCSpecific`
  (главный + активные пан-DDC, каждый со своим rate/ADC); `SendFullHP` пишет
  частотные слова всех активных пан-DDC (`Buf[9+ddc*4]`); хелпер
  `DDCBaseIndex` (дедуп трёх списков плат); сброс пан-состояния в `Connect`.
- `RadioController`: `MAX_PANS=4`, `PAN_DDC_RATES=(192,384,768)`;
  `AddPanDDC/RemovePanDDC/SetPanDDCFreq/PanDDC{Active,Freq,RateKHz}`
  (аллокатор hw-индексов от `FActiveDDC+1`, обратный маппинг `FDDCPanMap`);
  `OnDDCIQ` роутит: активный DDC → главный тракт, пан-DDC →
  `WDSPEngine.PushPanIQ` (пока заглушка-дроп, тело — 3.2); паны гасятся в
  `StopRunning`, пул сбрасывается в `StartRunning`.
- ⚠ Попутный фикс: `StartRunning` не включал Saturn (BoardType 10) в список
  плат с DDCBase=2 → на Saturn гейт `FActiveDDC` ждал DDC0. Исправлено
  (`[3,4,5,10]`, как в `DDCBaseIndex`).
- Частоты панов — сырые Гц железа: XVTR-трансляция на паны НЕ распространяется
  (XVTR — свойство главного тракта; решить при 3.3/3.4, если понадобится).

### 3.2 — WDSPEngine: анализатор и IQ-тракт на панадаптер

Сейчас: анализаторы `RX_DISP_ID=0` / `TX_DISP_ID=1` (`WDSPEngine.pas:133`),
создание `XCreateAnalyzer` (`:1140`), настройки+span-clip
`ApplyRXAnalyzerSettings` (`:1163-1211`), снапшот спектра
`Spectrum0(1, RX_DISP_ID, …)` из DSP-потока (`:1597`), выдача пикселей
через `FOnSpectrum` (`:2850`). Слайсы едят копию ГЛАВНОГО аккумулятора
в `ProcessSlices`.

Дельты:

1. **Пан-анализаторы**: `PAN_DISP_BASE = 8` (не 4/5 — они заняты в ветке
   `feature/qo100-zoom-panel`, при будущем merge не столкнёмся);
   `XCreateAnalyzer(PAN_DISP_BASE+PanId)` при создании пана,
   `ApplyPanAnalyzerSettings(PanId)` — параметризованная копия
   `ApplyRXAnalyzerSettings` (свой rate, свой span-clip/zoom, свои
   FFT/avg/detector — можно стартовать с общих настроек, разделить позже).
2. **Per-pan аккумулятор**: `TPanChan = record` (`FPanAccI/Q`, `FPanAccPos`,
   rate, disp id, активные слайсы). `PushPanIQ(PanId, Data)` из сетевого
   потока кладёт в аккумулятор пана; по накоплению блока в DSP-потоке:
   копия → `Spectrum0(1, PAN_DISP_BASE+PanId, …)` + `ProcessSlices` по
   слайсам ЭТОГО пана.
3. **Слайс знает пана**: в `TSliceChan` добавляется `PanId`; NCO shift
   слайса = `TargetHz − PanCenterHz`; rate канала слайса = rate его пана
   (для панов N>0 это 192-768 кГц — WDSP-каналу дешевле, чем полный rate!).
   `RefreshSliceShifts` обобщается на «центр пана», гейт `SliceFitsCapture` —
   на «span пана».
4. **Выдача пикселей**: `FOnPanSpectrum(PanId, Pixels, N)` (или существующий
   колбэк + PanId), MainForm-тик раздаёт по `FPans[PanId]`.
5. **Пул WDSP-каналов** общий: 0=RXA (слайс A пана 0), 1=TX, слайсы 2..7
   (`SLICE_CHAN_BASE`, `MAX_SLICES=6`) — на все паны суммарно. Этого хватает:
   6 доп. слайсов + главный = 7 приёмников.

Блокировки: `FSliceLock` уже сериализует слайсы vs DSP; добавить такую же
дисциплину для add/remove пана (сетевой поток пишет в аккумулятор пана —
создание/удаление пана только под локом, как переоткрытие слайсов при
ChangeSampleRate).

**СТАТУС 3.2 (2026-07-11): РЕАЛИЗОВАНО + СМОУК-ТЕСТ БЕЗ ЖЕЛЕЗА ПРОШЁЛ
ПОЛНОСТЬЮ.** Отличия/уточнения против текста плана:

- Пан-пакеты идут в ОБЩУЮ очередь IQ с меткой `TIQQueueItem.PanId` (0 =
  главный) — порядок прихода сохранён, один семафор; `IQ_QUEUE_SIZE` 64→256.
  Диспетчер в `TDSPThread` → `PushPanItemToDSP` (декод + feed анализатора +
  аккумулятор + `ProcessSlicesFor(PanId)`), всё под `FSliceLock`.
- `MAX_PANS=4` переехал в WDSPEngine (единый для движка/контроллера/UI);
  `PAN_DISP_BASE=8` (id панов 9..11).
- Блок пана `BufSize = FAudioBufSize·Rate/FAudioRate` ⇒ выходной аудио-блок
  слайсов ЛЮБОГО пана всегда `FAudioBufSize` (буферы не перевыделяются);
  `AllocSliceBuffers` берёт максимум блока по всем панам. Rate пана обязан
  быть кратен 48к (гейт в `AddPan`).
- `TSliceChan.PanId` + `AddSlice(..., APanId)`; `OpenSliceChannel` открывает
  канал от rate/блока СВОЕГО пана; `SliceDSPRateFor(Mode, InRate)` — WFM-слайс
  поднимается от rate пана. Слайсы панов переживают `ChangeSampleRate`
  главного (пересоздаются в Open, как раньше).
- Анализатор пана: `CreatePanAnalyzer`/`ApplyPanAnalyzerSettings` (без
  span-clip/zoom — пан-зум в 3.3+), живёт в OpenAnalyzer/CloseAnalyzer
  (пересоздание при смене rate главного). Смена детектора/усреднения/FFT/
  пикселей главного перекатывается на паны (`ReapplyPanDisplaySettings`).
- Выдача пикселей: `OnPanSpectrum`/`OnPanWaterfall(PanId, Pixels, N)` из
  display-потока; `GetPixels` пана под `FSliceLock` (гонка с RemovePan),
  колбэки — вне лока.
- Контроллер: `TCtrlSlice.PanId`, `AddSlice(..., APanId)`, гейт/`CapturedSpan`
  per-pan, `SetSliceTarget`/`RefreshSliceShifts` относительно центра СВОЕГО
  пана, ретюн пана двигает NCO его слайсов; `AddPanDDC` создаёт пан в движке
  (до включения DDC), `RemovePanDDC` убирает слайсы (со звуковухами) + движок
  + DDC.
- Смоук-тест (scratchpad `pantest.pas`, консольный, libwdsp): AddPan-валидации,
  спектр/водопад пана 384к с тоном +52к — пик ровно на месте (пиксель 650/651),
  слайс пана (USB, NCO +51к) выдал аудио-тон 1043 Гц RMS 0.68, главный тракт
  параллельно жив (пик −30к на месте), RemovePan мгновенно глушит слайс, без
  крэшей. Попутно зафиксирована конвенция синтетического IQ: «+Гц на дисплее»
  = I=cos, Q=−sin (полезно для будущих тестов).
- НЕ проверено на эфире (нужен UI 3.3, панов никто не создаёт из GUI).

### 3.3 — UI: второй панадаптер в главном окне

- **Стек панов**: область спектра = вертикальный стек `TPanafallPanel` с
  `TSplitter` между ними (Flex-стиль). Пан 0 сверху. Высоты в персист.
- **Шапка пана** (тонкая полоска над спектром, темизирована как флаги):
  частота центра · диапазон · rate · бейдж ADC `A1/A2` (3.4) ·
  кнопки `⧉` (pop-out, 3.5) и `×` (закрыть; у пана 0 нет). Шапка пана 0
  появляется только когда панов ≥ 2.
- **Добавить пан**: кнопка `+` в шапке/тулбаре (только если
  `caps.MaxPans > 1`): диалог/дефолт — частота (ввод или «текущая ±»),
  rate, ADC. Создание пана автоматически создаёт его первый слайс в центре.
- **Активный пан** = пан активного слайса. Клик по канве пана делает его
  слайс активным (существующая механика `FActiveFlag`), левая панель /
  FreqDisplay / CAT правят активный слайс — как сейчас.
- **Ctrl+ЛКМ** на любом пане = добавить слайс в этот пан (буква из
  глобального пула).
- **Тик/FPS**: общий таймер, кадры раздаются каждому пану; FPS-гейт из
  перф-работ применяется per-pan. Свёрнутый/скрытый пан кадры не рендерит.
- **TX**: TX-бейдж на флаге работает с любого слайса любого пана — DUC
  независим от DDC, TX-частота = частота слайса. (Полный мультислайс-TX —
  отдельная старая задача, тут ничего нового не ломаем.)

**СТАТУС 3.3 (2026-07-11): БАЗОВЫЙ UI РЕАЛИЗОВАН, собирается (GUI+демон),
бэкенд-тракт проверен END-TO-END против эмулятора hpsdrsim -hermes -P2
(scratchpad pantest2: DDC1 включается, IQ течёт, спектр/водопад пана рисуются,
слайс живой S-метр −95дБм, ретюн/снятие чистые, seq-ошибок нет). НА ЭКРАНЕ
НЕ ПРОВЕРЕНО.** Что сделано:

- `TPanafallPanel`: `PanId`, регион в стеке (`StackTop/StackHeight`, Layout
  считает внутри региона), шапка (TPanel+TLabel+«×»→OnPanClose; видна при
  панах ≥2), кнопка «⊞» в ряду пан/зума (`AddPanEnabled`, только пан 0),
  `ShowZoomRow=False` у панов N (их зум позже), pan-aware `ViewWindow`
  (SliceAtPixel/LayoutFlags от окна СВОЕГО пана), `FOnFlagsInvalidate(Self)`.
- MainForm: `FPans[0..MAX_PANS-1]` (пан 0 = FPan), `LayoutPanStack` (доли
  `FPanShare` + межпанные сплиттеры, пересчёт по MouseUp), «⊞» →
  `OnAddPanClick` (AddPanDDC на частоте активного VFO, rate 384к, автослайс
  в центре, копия display-параметров view, ApplyDarkTheme), «×» шапки →
  отложенное `ClosePanadapter` в тике (нельзя free из своей кнопки).
- Пиксели: `engine.OnPanSpectrum/OnPanWaterfall` → MainForm напрямую
  (display-поток, `FPanCbLock` закрывает гонку с созданием/закрытием пана);
  рендер в SpectrumTimerTick по dirty-флагам, `SyncPanViews` держит
  центр/span вьюх и текст шапок.
- Мышь панов N (НОВЫЕ простые обработчики, VFO/CTUN/маяк/оверлеи главного
  сюда не переносились — решение по B2 выполнено): Ctrl+ЛКМ = слайс в этом
  пане, ЛКМ по маркеру = drag слайса (кламп полосой пана), ЛКМ по фону =
  drag ЦЕНТРА пана (live-ретюн DDC), колесо над флагом любого пана = тюн
  того слайса; флаги/CurSliceId/инвалидции обобщены на все панели
  (PanOfOverlay/PushFlagStateAllPans/LayoutFlagsAllPans).
- Паны живут только в Run-сессии: на STOP UI убирает их вместе с контроллером.
- Отложено: пан-зум панов N, per-pan детектор/усреднение, персист (после 3.3),
  wideband при маленьком регионе пана 0 может вылезти за регион (редкий кейс),
  колесо над фоном пана N крутит главный VFO (как везде вне слайсов).

**ФИКСЫ ПО ПЕРВОМУ ЭКРАННОМУ ТЕСТУ (2026-07-11, фидбек юзера):**

1. **Сплиттер спектр/водопад был сломан при ≥2 панах**: drag-математика
   (`SplitterMouseMove/Up`) считала доступную высоту от ВСЕГО родителя
   (`FParent.ClientHeight`, `TopOff=0`), игнорируя регион пана в стеке,
   шапку и wideband. Симптомы: у пана 0 ratio занижался (спектр сжимался
   при тяге вниз), у пана 1 сплиттер прыгал в регион пана 0. Фикс: Layout
   запоминает `FSplitTopOff/FSplitAvailH` (геометрия «спектр+водопад»
   СВОЕГО региона), drag использует только их.
2. **Межпанный сплиттер** не давал обратной связи до отпускания — теперь
   живой «призрак» (сам сплиттер едет за мышью, клампится 15%-минимумом
   обоих панов, подсветка при захвате), доли пересчитываются по MouseUp
   от финального положения призрака.
3. **Клик по фону пана N не перестраивал частоту** (только drag центра).
   Теперь пресс без движения (<4px) = клик-тюн `PanClickTune`: слайс пана
   (активный, если он этого пана, иначе первый; нет — создаётся) на частоту
   клика со снапом 100 Гц и клампом полосой пана. Drag центра включается
   после гистерезиса 4px, курсор `crSizeWE` — только с этого момента
   (раньше менялся на голый пресс).
4. **Диспетчер флагов/маркеров на панах N** получал координаты ВОДОПАДА
   (прямоугольники флагов — в системе спектра): клики в верхней части
   водопада могли молча съедаться невидимым флагом. Теперь флаги/hover —
   только для событий спектра, курсор на водопаде не трогаем.
5. **(2-й раунд фидбека)** У панов N сплиттер спектр/водопад «залипал
   призраком» (спектр не пересчитывался, при следующем drag прыжок к
   старому ratio): в `OnAddPanClick` не был подвешен `OnSplitterMoved`
   (у пана 0 = `RightPanelResize` → полный re-layout). Подвешен тот же
   обработчик. Пан 0 и межпанные сплиттеры юзер подтвердил рабочими.
6. **Курсор-«рука» на всей площади панов N (разгадан, 3-й раунд)**: юзер
   заметил, что рука = курсор кнопки «⊞». Механизм: GL-канвы — QOpenGLWidget
   с `WA_NativeWindow` = отдельная Wayland-поверхность; `crDefault` в LCL
   означает «курсор не задан», и поверхность НАВСЕГДА наследует курсор,
   бывший под мышью в момент создания виджета. Пан N создаётся кликом по
   «⊞» (`crHandPoint` у TFlatButton) → рука прилипает; пан 0 создаётся на
   старте (мышь нигде) → норм. Фикс: `Build` ставит спектру/водопаду ЯВНЫЙ
   `crArrow` (реальный push), обработчики панов N восстанавливают `crArrow`,
   не `crDefault`. ⚠Урок на 3.5 (pop-out) и вообще: любой динамически
   создаваемый нативный GL-виджет должен получать явный курсор при создании.

Статус: все фиксы подтверждены юзером на экране, закоммичено 95b4f74
(+ dae0c19 банд-столл отдельно).

**RATE ПАНОВ (реализовано 2026-07-11, после коммита 3.3):**

Решение (обсуждено с юзером): (1) при создании пан наследует rate главного
с клампом ≤768к (48/96/192к как есть; вне сетки → 384к); (2) rate пана
независим и меняется кликом по «NNN kHz» в шапке пана → меню полной сетки
железа; (3) за сменой rate главного НЕ следует (издержки Ethernet/CPU, churn).

- `WDSPEngine.SetPanRate(PanId, RateHz)`: под FSliceLock — DestroyPanAnalyzer →
  пересбор аккумулятора (BufSize/AccI/AccQ) → CreatePanAnalyzer → переоткрытие
  слайс-каналов пана (Close+OpenSliceChannel, state слайсов сохраняется).
- `RadioController`: PAN_DDC_RATES (192/384/768) УДАЛЁН — валидация
  `PanRateAllowed` = Caps.RatePresets ∩ кратные 48 кГц (полная сетка HPSDR
  48-1536к). `SetPanDDCRate`: движок → FPanRateKHz → `SetPanDDC` (реконфиг
  DDC тем же RebuildDDCSpecific+SendFullHP) → кламп слайсов к новой полосе.
  `FPanADC[]` — ADC пана теперь хранится (нужен реконфигу; база под 3.4).
- UI: rate в шапке — отдельный TLabel (`SetHeaderRate`, у панов N кликабелен,
  акцентный цвет, crHandPoint), `OnPanRateClick` → TPopupMenu из RatePresets
  (текущий отмечен) → `PanRateMenuItemClick` → SetPanDDCRate + SyncPanViews +
  PushAllSliceFlagStates + LayoutFlags. Пан 0 показывает rate некликабельно
  (его rate — существующий SampleRateOverlay).
- ★СМОУК-ТЕСТ БЕЗ ЖЕЛЕЗА ALL PASS (scratchpad panratetest.pas, паттерн 3.2):
  синтетический тон +10.8кГц → пан 384к: пик спектра на месте, слайс 800Гц
  RMS 0.33 → SetPanRate 192к → пик/аудио живы → 768к → живы → RemovePan чисто.
НА ЭКРАНЕ НЕ ПРОВЕРЕНО, НЕ закоммичено. Смена rate пана на эфире = короткий
разрыв IQ этого пана (реконфиг DDC) — ожидаемо.

### 3.4 — ADC-селектор панадаптера (+ антенны Alex, уточнено 2026-07-11)

Источник сигнала пана — ДВЕ независимые ручки, не одна:

1. **АЦП (per-pan)**: `TPanafall.ADCSource: 0/1`; в шапке пана селектор/бейдж
   `A1`/`A2`. Показывается только при `NumADCs=2` (детект платы уже есть —
   `NumADCs` в ConfigureDDCs-пути). Переключение = `ConfigureDDCs` с новым
   ADCSource для DDC этого пана. Пан 0 тоже можно перекинуть на ADC1 — это
   закрывает сценарий старой ветки `feature/rx2-support` как частный случай.
2. **Антенна первого АЦП (глобальная, НЕ per-pan)**: у плат с Alex на ADC0
   уже есть аппаратный селектор RX-антенны **ANT1/ANT2/ANT3** (+ EXT-входы) —
   реализован per-band в Antenna/Alex-табе (`Alex.RxAnt[band]`, XVTR-override).
   Селектор один на весь ADC0 ⇒ антенну нельзя сделать свойством пана: смена
   антенны меняет её для ВСЕХ панов, сидящих на ADC0. В UI пана максимум —
   бейдж текущей антенны (например `A1·ANT2`) и/или шорткат к глобальному
   переключателю с явным пониманием, что это общая ручка. ADC1 (RX2-вход) —
   отдельный разъём без Alex-коммутации.

- Diversity (sync-DDC, оба АЦП когерентно в один канал) — НЕ здесь; протокол
  умеет, дизайну панов не мешает, отдельная будущая фича.

**СТАТУС 3.4 (2026-07-11): ГОТОВО, коммит 8bf8bcf, юзер подтвердил беглым
тестом. ⚠Ожидаются вопросы юзера по пункту «смена rate не сбрасывает АЦП».**
- `Caps.NumADCs` (HPSDR: 2 для BoardType 4/5/10 — ORION/ORION2/SATURN, те же
  платы, что Pkt.NumADCs в RebuildDDCSpecific; Pluto: 1; достоверно после
  Connect).
- Контроллер: `PanADC(PanId)` / `SetPanADC(PanId, ADC)`. Пан 0 = главный DDC
  (`FMainADCSrc` + ConfigureDDCs с тем же rate/dither/random; FMainADCSrc
  теперь прокинут и в оба штатных вызова ConfigureDDCs — смена rate не
  сбрасывает АЦП). Паны N = `FPanADC[]` + SetPanDDC (RebuildDDCSpecific).
- UI: бейдж «A1»/«A2» в шапке КАЖДОГО пана (вкл. пан 0 — закрывает сценарий
  rx2-support), виден только при NumADCs>1, клик = тумблер, акцентный цвет.
- ⚠Ограничение: шапка пана 0 видна только при ≥2 панах ⇒ с единственным
  паном перекинуть пан 0 на ADC2 из UI нельзя (добавь пан или жди персиста).
  Антенны Alex (ANT1/2/3) — по-прежнему глобально в Antenna-табе.

### 3.5 — Pop-out: панадаптер в отдельном OS-окне

Улучшение поверх Flex (SmartSDR так не умеет). Кнопка `⧉` в шапке.

- `TPanFloatForm` — простая TForm-обёртка, в которую переезжает
  `TPanafallPanel`; `×` окна возвращает пан в стек главного окна.
- **Риск №1 — GL + reparent**: перенос живого `TOpenGLControl` между
  окнами на Qt6/Wayland может убить контекст. Безопасный путь: НЕ
  репарентить канву, а пересоздать вьюху в новом окне (панель отдаёт
  state: центр/span/wf-историю можно не сохранять — водопад начнётся
  заново; слайсы/флаги живут в контроллере и переподключаются).
  Сначала попробовать честный reparent, fallback — пересоздание.
- **Риск №2 — общие GL-ресурсы**: закрывается аудитом из 3.0.
- Персист: флаг «пан N в отдельном окне» + геометрия окна.

**СТАТУС 3.5 (2026-07-11): ГОТОВО, коммит 8bf8bcf, юзер подтвердил беглым
тестом — честный GL-reparent на Qt6/Wayland РАБОТАЕТ (fallback-пересоздание
не понадобилось).**
- Кнопка «⧉» в шапке панов N>0 → `PopOutPan`: `TForm.CreateNew` (Caption
  «EWSDR · PAN N», тема, OnMouseWheel = FormMouseWheel — колесо над
  флагами/фоном работает как в стеке), `TPanafallPanel.ReparentTo(F)`
  (Parent всех контролов), StackTop/Height=0/0 (= всё окно), сплиттер пана
  скрыт, LayoutPanStack пере-стекует оставшихся (плавающие скипаются).
- GL: reparent пересоздаёт хэндлы канв → контексты умирают. По прецеденту
  MSAA: `ResetGLCache` (обнуление id без glDelete) — расширен, теперь
  сбрасывает и водопад (`TWaterfallViewOpenGL.ResetGLCache`, новый);
  история водопада живёт в CPU-буфере — тексура перезальётся.
- [×] окна = dock back (`CloseAction := caNone` + `DockBackPan`: reparent в
  PanelRight, сброс GL, Release окна). «×» шапки в окне = закрыть пан
  целиком (ClosePanadapter освобождает и окно). STOP закрывает всё.
- Резерв на случай мёртвого GL после reparent: пересоздание вьюхи/контролов
  (в коде НЕ делалось — сначала экранный тест).
- Персист геометрии — вместе с общим персистом панов.

**СТАТУС «хирургический ChangeSampleRate» (2026-07-12): ГОТОВО — смоук-тест
ALL PASS, юзер подтвердил на экране («всё работает»). Отладочные пробы
RATEPROBE удалены.** Наблюдение юзера: смена rate ГЛАВНОГО пана
коротко замораживала водопады панов N (обратное — нет). Причина была в движке:
`ChangeSampleRate` делал полный Close/Open (стоп DSP-потока, разрушение ВСЕХ
анализаторов вкл. пановых, закрытие всех слайс-каналов, сброс общей IQ-очереди).
Протокол чист: оба пути шлют одинаковый полный DDC Specific (`RebuildDDCSpecific`).
Сделано (WDSPEngine.pas + RadioController.pas):
- Гейт `FMainGated` + барьер `FMainLock`: DSP-поток при взведённом гейте дропает
  main-пакеты (PanId=0) из общей очереди НЕ блокируясь, паны обрабатываются без
  пауз; двойная проверка гейта (снаружи и под локом) закрывает гонку установки.
- `ChangeSampleRate` теперь хирургический: закрывает/переоткрывает ТОЛЬКО
  RXA_CHAN + слайс-каналы пана 0 (их InRate = главный rate); DSP/display-потоки,
  TXA, TX-поток, анализаторы и паны не трогаются. RX-анализатор переармируется
  `ApplyRXAnalyzerSettings` (как при зуме) — история главного сбрасывается
  (неизбежно), пановые НЕ. Ошибка реконфига → `FInitialized:=False` (контроллер
  увидит по `Initialized`).
- Общий код RXA вынесен: `OpenMainRXChannel` (открытие+реставрация состояния из
  полей движка, вкл. FShiftHz) / `CloseMainRXChannel` — используются Open/Close/
  ChangeSampleRate.
- Контроллер `SetSampleRate` больше НЕ сбрасывает `FWDSPReady` на время смены —
  иначе OnDDCIQ переставал кормить паны (это был второй источник паузы).
- ★Находка по пробам: `SetChannelState(chan,0,dmode=1)` на ПРОСТАИВАЮЩЕМ канале
  зря ждёт флаш ~130мс. Каналы при закрытии всегда idle (слайсы — дисциплина
  FSliceLock, RXA — гейт-барьер) → dmode=1→0 в `CloseSliceChannel` и
  `CloseMainRXChannel`. Итог: смена rate 331→120мс, макс. пауза аудио слайса
  пана 137→40мс (~2 блока, на глаз незаметно). Бонус: закрытие слайсов/панов и
  SetPanRate тоже ускорились на ~130мс/канал.
- Смоук-тест scratchpad `ratetest.pas` (синтетический тон, фидер-поток, замер
  межблочных пауз аудио пан-слайса вокруг смены 384к→192к): ALL PASS — пан без
  паузы >41мс, main-аудио/слайс пана 0/слайс пана 1 живы и слышат тон после
  смены. Оба бинарника собираются.

**СТАТУС «пан-зум панов N» (2026-07-12): ГОТОВО — смоук panzoomtest ALL PASS,
юзер подтвердил на экране, коммит cb6bdfa.**
- Движок: `TPanChan.ZoomFactor/PanSlider`, span-clip в
  `ApplyPanAnalyzerSettings` (та же математика, что у главного; ZOOM_LIMIT 100),
  `SetPanZoomPan` (no-op guard — SetAnalyzer сбрасывает FFT-историю, урок
  dae0c19). Зум переживает смену rate пана (SetPanRate пересоздаёт анализатор
  из записи пана).
- Контроллер: `FPanZoomF/FPanPanSl` (сессия пана, сброс в Add/RemovePanDDC —
  band-персиста у панов нет), формула окна вынесена в юнит-хелпер
  `ViewWindowFor` (GetViewWindow главного теперь тоже через него),
  `GetPanViewWindow(0)` делегирует главному; `PanZoomIn/Out` по той же
  лестнице ZOOM_MAG_LADDER, якорь = ЦЕНТР текущего окна (`PanZoomStepTo`,
  у панов нет VFO-якоря главного), `PanZoomDefault`, геттеры.
- UI: `TPanZoomEvent` получил Sender (PanZoomBar), `OnPanZoomWindow`
  диспетчеризует бар→пан; кнопки −/⌂/+ общие (`PanFromSender`); у панов N
  включён ShowZoomRow, бар проводится в CreatePanadapter; единые
  `SyncPanZoomUI/RefreshPanZoom` (вьюха+бар ← контроллер) используются в
  SyncPanViews и drag-ретюне; `TPanafallPanel.ViewWindow` и `PanViewWindow` →
  `GetPanViewWindow` ⇒ вся мышь/флаги панов автоматически зум-корректны.
  Темизация баров панов N в ApplyDarkTheme.
- Смоук panzoomtest.pas: пик тона +50к на пане 384к совпадает с формулой окна
  при зуме 0 / 0.5+пан 0.7 / сбросе (±0.6%).
- ⚠Фикс по первому экранному тесту: кнопки −/⌂/+ панов N рулили ГЛАВНЫМ зумом —
  MakeFlatBtn создаёт кнопки с Owner=контейнер (не панель), PanFromSender их
  не находит. Диспетчер кнопок = PanOfZoomButton (матч по идентичности
  BtnZoomIn/Def/Out всех панов). Урок: PanFromSender работает только для
  контролов с Owner=панель (TLabel шапки — да, MakeFlatBtn-кнопки — нет).

**СТАТУС «грид-раскладка панов» (2026-07-12): РЕАЛИЗОВАНА, собирается, НА
ЭКРАНЕ НЕ ПРОВЕРЕНА, НЕ закоммичена.**
- Панель: регион пана обобщён до прямоугольника — `StackLeft/StackWidth`
  (0/0 = вся ширина), Layout смещает ВСЁ по X0 (шапка/спектр/линейка/
  сплиттер/водопад/ряд зума); `PositionPanZoomBar` получил X0 и 5-ю кнопку;
  новая кнопка `FBtnGrid` («▦»/«▤», глифом рулит хозяин) + `GridClick`
  (задать до Build) + `GridBtnEnabled`; ReparentTo переносит кнопку.
- MainForm: `FPanGridMode` (сессия), `FPanGridRowShare` (доля верхнего ряда,
  0.15..0.85, дефолт 0.5), `FPanColShare[1..]` (доли колонок, init 1.0,
  нормируются суммой), `FPanVSplitters[1..]` (вертикальный сплиттер СЛЕВА от
  колонки пана i, crHSplit, создаётся в CreatePanadapter, free в
  ClosePanadapter). `PanGridActive` = режим включён И >=2 доп. панов в стеке
  (иначе автоткат в стек, режим помнится). LayoutPanStack: ветка грида — пан 0
  верхний ряд во всю ширину (wideband внутри как в стеке), паны N колонками;
  горизонтальный сплиттер ряда = сплиттер ПЕРВОЙ колонки во всю ширину
  (PanSplitMouseDown/Up ветвятся по PanGridActive: в гриде правят
  FPanGridRowShare напрямую), сплиттеры остальных колонок скрыты; вертикальные
  PanVSplitMouse* — зеркало горизонтальных (призрак, min 15% ширины,
  DShare=DeltaX/TotalW*ShareSum). Кнопка «▦/▤» видна при FPanGridMode или
  >=3 застекованных панах (выключить можно всегда). PopOutPan обнуляет
  StackLeft/Width и прячет V-сплиттер. Темизация: V-сплиттеры T.Border,
  BtnGrid через StyleButton.
- Флаги/мышь/зум панов грид не трогает: всё в координатах канвы спектра,
  канва целиком переезжает в регион-колонку.
- Проверить на экране: тумблер при 3 панах, оба типа сплиттеров, wideband в
  гриде, pop-out/возврат из грида, закрытие пана в гриде (автоткат),
  узкие колонки (зум/клик-тюн/drag), отсутствие регрессий при 1-2 панах.

**ФИКСЫ ПО ПЕРВОМУ ЭКРАННОМУ ТЕСТУ ГРИДА (2026-07-12, собирается, не
проверено):** три независимых бага из фидбека юзера.
1. **Наезд панов при выходе из грида (и вообще при 4 панах в стеке).**
   LayoutPanStack делил высоту по долям (после add/close все доли равные,
   при 4 панах = 25%), а Layout панели клампит спектр/водопад к минимумам
   (MIN_SH=60/MIN_WH=40) — виджеты вылезали за регион, следующий пан
   рисовался поверх (пан 0 терял водопад и ряд кнопок). Фикс:
   `TPanafallPanel.MinLayoutHeight` (те же константы/DPI, что Layout) +
   `PanMinHeight(PanId)` в MainForm (пан 0 += wideband `FPanWideH`);
   раздача в стеке клампит каждый пан минимумом и режет раздутые так,
   чтобы оставшимся хватило на их минимумы; в гриде Row0H клампится
   минимумом пана 0 и минимумом нижнего ряда (макс. по колонкам);
   драг межпанного сплиттера ограждён `Max(15%, минимум содержимого)`
   в обеих ветках (стек/грид).
2. **«Блуждающие» кнопки после STOP (− − ⌂ +, кнопки на линейке, ⊞ накрыт).**
   MakeFlatBtn создаёт кнопки с Owner=КОНТЕЙНЕР (не панель) → при гибели
   пана (STOP → DestroyAllExtraPans → P.Free) кнопки −/⌂/+/⊞/▦ НЕ
   освобождались и оставались жить на PanelRight в старых координатах,
   а PanOfZoomButton, не найдя пан, роутил их клики на главный зум.
   Фикс: TPanafallPanel.Destroy явно освобождает FBtnZoomOut/Def/In,
   FBtnAddPan, FBtnGrid (FBtnPopPan/ClosePan — Owner=FHeaderPanel,
   умирают с ним). Перезапуск программы «лечил», потому что утечка —
   в пределах процесса.
3. **Жор CPU (+~40% за пан).** SyncPanViews (тик, частота кадров) звал
   SyncPanZoomUI с БЕЗУСЛОВНЫМ `PbPanZoom.Invalidate` на каждый пан каждый
   кадр (регрессия cb6bdfa) = полный софт-рендер зум-бара + Qt-рекомпозит
   @~50fps × N панов. Фикс: инвалидация только внутри
   ZoomBar.SetFreq/SetState (они no-op при тех же значениях), событийный
   Invalidate перенесён в RefreshPanZoom. Дополнительно жор усиливался
   багом 1 (наезжающие GL-виджеты сверх окна). Замер юзера — после фикса.

Ответ на вопрос «почему только 3 доп. пана»: MAX_PANS=4 (WDSPEngine) —
пан 0 + 3 доп.; 6 было у СЛАЙСОВ (MAX_SLICES=6 доп.). Поднять можно (P2
позволяет много DDC), но сначала подтвердить CPU после фиксов.

Фиксы 1-3 ПОДТВЕРЖДЕНЫ юзером на экране 2026-07-12 («работает», CPU со
всеми панами 17-20%). Добит хвост:
4. **Пропорция спектр/водопад пана 0 не восстанавливалась после закрытия
   панов (50/50 → 70/30).** Layout клампил ratio минимумами С ЗАПИСЬЮ в
   FSplitterRatio: в маленьком регионе (стек панов) MIN_SH/AvailH ≈ 0.7
   запоминался навсегда. Фикс: кламп в локальную EffRatio, FSplitterRatio
   меняет только drag сплиттера юзером.

### 3.6 — Pluto+ (2r2t): второй пан на втором антенном входе

Опционально, после 3.0-3.3. Обычного Pluto НЕ касается (там один RX разведён).

- **Детект**: число RX-каналов в iio-контексте (`voltage0/1` + `voltage2/3`
  у `cf-ad9361-lpc`); прошивка должна быть с `adi,2rx-2tx-mode-enable`
  (на Pluto+ обычно так). Если каналов 2 → `caps.MaxPans=1`, как сейчас.
- **PlutoBackend** (`PlutoBackend.pas:222-236`): сейчас RX-поток пушит
  `FOnDDCIQ(0, pkt)`. В 2r2t буфер отдаёт 4 канала интерливом —
  деинтерлив на два пакета → `FOnDDCIQ(0, …)` и `FOnDDCIQ(1, …)`.
  Дальше это обычные псевдо-DDC, роутинг из 3.1/3.2 работает без изменений.
- **Частотная связка (главное отличие)**: оба RX на одном LO и одном rate.
  `caps.IndependentPanFreq = False` → пан 1 создаётся ТОЛЬКО с центром/rate
  пана 0; ретюн любого пана ретюнит общий LO (двигает оба); в шапке пана 1 —
  признак связки (`🔗 LO`), контролы центра либо скрыты, либо честно тюнят
  общий LO. Зум/span-clip у каждого пана свой (анализаторы независимы).
- **Пропускная способность**: ×2 потока. Через гигабитный Ethernet Pluto+
  (наш libiio network context) проходит свободно; на USB2 пришлось бы резать
  rate — не наш кейс.
- **QO-100 beacon-lock**: лок правит общий LO — оба пана едут вместе,
  инвариант «всё стоит на месте» сохраняется, конфликта нет.
- **Бонус на потом**: каналы фазокогерентны — готовая база под честный
  diversity (сложение с фазовращателем) и пеленгацию. Не в этом этапе.
- TX2 (второй выход, общий TX LO) — вне этого плана.

---

## Персист (собрать в один заход после 3.3)

В `Settings.pas`:
- список панов: центр, rate, ADC, span/зум, высота в стеке, pop-out+геометрия;
- список слайсов: PanId, TargetHz, mode/filter/AGC/DSP, звуковуха
  (DeviceIndex + имя для повторного матчинга), громкость/mute;
- восстановление на старте: сначала паны (ConfigureDDCs), потом слайсы.
Это закрывает и старый долг «персист слайсов» из Этапа 2.

---

## Оговорки / риски

- **CPU**: слайс на пане N>0 дешевле слайса на пане 0 (WDSP-канал жуёт
  192-768 кГц вместо полного rate). Дорогое место — рендер: каждый пан =
  свой спектр+водопад; FPS-гейт и перф-дисциплина из [[project_perf_profiling]]
  обязательны per-pan.
- **Не портить входной IQ-буфер** между каналами (fexchange0 in-place) —
  правило Этапа 1, действует и для пан-аккумуляторов (копия до Spectrum0,
  как `FSpecBuf`, см. `WDSPEngine.pas:235`).
- **Старт/стоп WDSP-каналов на лету** — задержки RXA стоп/старт
  (`WDSPEngine.pas:~230`), создание/закрытие пана держать вне аудио-глитчей.
- **Ethernet**: кап 4 пана суммарно; следить за суммарным rate при выборе
  rate панов.
- **DDCBase=2** на PureSignal-платах: доп. панам достаются DDC3+ — при
  4 панах на Angelia/Orion/Saturn это DDC2..5, ок.
- **Qt6/Wayland + GL**: reparent канвы (3.5) и per-instance ресурсы (3.0) —
  два главных технических риска всего этапа; аудит 3.0 делать первым.
- Смена sample rate главного пана переоткрывает его слайсы (уже реализовано) —
  панов N>0 это касаться не должно (их rate независим).

## Открытые вопросы (решить по ходу, не блокеры)

- CAT: что репортить как VFO A/B при активном слайсе на пане N>0
  (сейчас CAT смотрит на главный тракт; вероятно «активный слайс = VFO A»).
- Перетаскивание слайса МЕЖДУ панами (drag флага на другой пан): вкусно,
  но не в первую итерацию — сначала слайс живёт в своём пане (кламп по
  span пана, как сейчас по захвату).
- Авто-promotion (слайс уехал за край пана → предлагать новый пан) — нет,
  осознанно: только явное создание пана (решение «сначала статика» остаётся).
- Настройки водопада (палитра/AGC/avg) per-pan или глобально — стартуем с
  наследования глобальных, разделим по запросу. ★РЕШЕНО/РЕАЛИЗОВАНО 2026-07-13
  (см. блок «PER-PAN ДИСПЛЕЙ» ниже).
- **Компоновка панов сеткой** (запрос юзера 2026-07-11): не только вертикальный
  стек — например, пан 0 сверху во всю ширину, паны B и C ниже бок о бок
  (пол-ширины каждый), и т.п. Идея на после 3.3/3.5: обобщить `LayoutPanStack`
  до грид-раскладки (регион пана = прямоугольник, а не полоса; `StackTop/Height`
  → `Rect`), сплиттеры по обеим осям. Не блокер текущего этапа.

**PER-PAN ДИСПЛЕЙ (2026-07-13, собирается, dispcfgtest ALL PASS, НЕ закоммичен,
НА ЭКРАНЕ НЕ ПРОВЕРЕН)**

UX залочен с юзером: кнопка «◑» в шапке каждого пана открывает компактный
поповер настроек отображения. Модель — **глобальный дефолт + override на пан**:
- Пан 0 «◑» правит ГЛОБАЛЬНЫЕ поля рендера (`FController.FWf*`/`FSpec*`),
  зеркалятся в контроллер, persist глобальных при закрытии (FormDestroy →
  SaveGlobalSettings). Solo пан 0 (нет доп. панов ⇒ шапка скрыта) правится
  штатным диалогом настроек — «◑» виден только при видимой шапке (>=2 панов).
- Паны N при первой правке ставят `DisplayOverride:=True`; их рендер
  персистится в воркспейс раздельно по контексту (КВ/xvtr).
- «Reset» (только паны N) — снять override, вернуть глоб. дефолт.
  «Apply all» — раскатать текущий рендер пана на все паны + сделать глоб.
  дефолтом.

Что per-pan (view-side, дёшево — всё уже в `Pan.View`): палитра, gamma,
ref level, range, водопад авто-уровень + offset + ручные min/max. Что осталось
ГЛОБАЛЬНЫМ (движок/перф, не косметика): DisplayPixels (разрешение анализатора),
MSAA, WfSpeedDiv, детектор/усреднение спектра и водопада.

Реализация:
- Новый юнит **PanDisplayPopup.pas**: `TPanDisplayPopup` = borderless
  `fsStayOnTop` форма, гаснет по `OnDeactivate`; Flat*-контролы (combo палитры,
  float-spin gamma, int-spin уровней, чекбокс авто). Combo-дропдаун парентится
  к самой форме (child) ⇒ не деактивирует поповер. Правки применяются к
  `P.View` живьём → `OnApplied(P)`.
- **TPanafallPanel**: `FBtnDisplay` («◑», Owner=FHeaderPanel ⇒ освобождается
  авто с шапкой, в Destroy не нужен), `OnDisplayClick`, `DisplayOverride`.
  `PositionHeaderChildren` каскадит `[×][⧉][◑]` справа налево по `Visible`.
- **MainForm**: `OnPanDisplayClick` (лениво создаёт `FPanDisplayPopup`, показ
  под кнопкой), `OnPanDisplayApplied/Reset/ApplyAll`, `ApplyGlobalRenderToView`,
  `MarkPanDisplayDirty` (инвалидция сетки спектра + водопада; `SetWfPalette/
  Gamma` сами no-op-гардят и метят LUT к пересбору). Поповер прячется в
  `ClosePanadapter` (FPan мог висеть → перепривязка на след. показе).
- **Settings/TPanCfg**: `HasDisplay` + `DPalette/DGamma/DSpecRef/DSpecRange/
  DWfAGC/DWfOffset/DWfHigh/DWfLow`; под-объект `'display'` пишется ТОЛЬКО при
  override; чтение = присутствие блока. Save/RestorePanWorkspaceCtx прокинуты.

Файлы: `PanDisplayPopup.pas` (new), `PanafallPanel.pas`, `MainForm.pas`,
`Settings.pas`. NEXT: экранная проверка (палитра/gamma/уровни на пан, Reset/
ApplyAll, тёплый рестарт восстанавливает override панов N раздельно КВ/xvtr).

## Отвергнутые альтернативы (для истории)

Старый черновик Этапа 3 предлагал «off-span слайсы»: слайс вне видимой
панорамы на своём hard-DDC, представленный edge-маркером у края спектра
(цветная буква + ◄/► + частота) и/или «slices-рейлом» (полоса-менеджер с
чипами слайсов). Отвергнуто 2026-07-10 в пользу Flex-модели «DDC =
панадаптер»: off-span слайс без своего спектра слепой (не видно обстановку
вокруг), сущности расползаются (слайс с DDC ≠ слайс без DDC), а рейл дублирует
флаги. Идея рейла может вернуться позже как компактный обзор всех слайсов —
но не как замена панадаптеру. Палитра «цвет+буква = личность слайса во всех
местах» из черновика — остаётся в силе.

## Связанные заметки памяти

- project_slices_plan (статус), project_pluto_integration, project_perf_profiling
- project_spectrum_zoom (GetViewWindow / span-clip / маппинг мыши)
- project_qo100_zoom_panel (прецедент доп. анализаторов WDSP, disp id 4/5!)
- project_lazopengl_component (QOpenGLWidget/контексты на Qt6 — база для 3.5)
