fix(dsp): канал TXA жил на два потока без разделения

TX-DSP поток зовёт fexchange0(TXA_CHAN) через буферы FTXIn/FTXOut, и ровно туда
же на каждом T/R лезет SetTXRun из потока контроллера. Гейт FTXActive их не
разделял: поток проверяет его СНАРУЖИ, а внутрь блока входит позже. Три дефекта
одной природы:

* деструктор звал SetTXRun(False), который поток НЕ останавливает — он лишь
  опускает гейт и тут же сам сливает канал своими fexchange0, то есть лезет в
  TXA одновременно с ещё живым потоком, прямо перед сносом объекта. Теперь
  Destroy сразу зовёт Close: тот опускает гейт, будит поток, дожидается WaitFor
  и лишь потом закрывает каналы. Комментарий «останавливаем TX поток» врал;
* слив на отпускании PTT шёл без всякой синхронизации — окно гонки примерно
  1-1.5% на каждый T/R (блок ~150 мкс против периода 10.7 мс). Появился FTXLock
  по идиоме RX-стороны (FMainGated + FMainLock): поток берёт лок вокруг
  ProcessTXBlock и ПЕРЕЧИТЫВАЕТ гейт под ним, оба слива в SetTXRun идут под тем
  же локом. Ждать контроллеру не дольше одного блока TXA;
* ветка ЗАПУСКА оставалась открытой: SetMOX не отсекает MOX=True поверх уже
  идущей передачи (прислать повтор вправе CAT, web и TCI), а SetTXRun(True)
  обнулял индексы mic-кольца и дёргал SetChannelState мимо лока. ★Худший исход
  не теоретический: TX-поток дописывает свой локальный tail поверх обнулённого,
  кольцо выглядит почти полным СТАРЫХ сэмплов, и они уходят в эфир пачкой.
  Теперь `if Run and FTXActive then Exit` плюс весь старт одной транзакцией под
  FTXLock, FTXActive публикуется последним.

★Перенастройку TXA в ChangeSampleRate под лок НЕ берём: там SetChannelState с
dmode=1 ждёт фейда, а фейд прокручивают fexchange0 самого потока — под локом это
клин на ~106 мс (тот самый старый фриз TX→RX). Синхронизация там своя:
остановка канала. Второй известный кросс-поточный доступ к TXA — pscc из
сетевого потока (PureSignal), так же устроено в Thetis.

Попутно закрыт use-after-free: TX-поток создаёт SetTXRun, которому открытый
движок не нужен, а гасил его только код ЗА гейтом `if not FInitialized then
Exit` в Close — на неоткрытом движке поток переживал Free объекта. Ловилось не
там, где сделано: следующий TThread.Create в процессе падал с AV (в стенде — на
секции часов после пейсинга, раньше — на третьем TRadioController в FreeEngines).
Остановка потока перенесена в начало Close, до гейта.

Стенд: порядок секций теперь намеренный — часы идут ПОСЛЕ тяжёлых частей и
служат сторожем починки потока (упадёт снова — увидим там). Новая проверка
повторного MOX с ★негативным контролем: без гейта «было 3000, стало 0».
Для неё в движке появилось диагностическое свойство TXMicFill. 276/276,
test/cat 50/50, GUI (--ws=qt6) и демон собираются.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
This commit is contained in:
2026-08-24 21:20:38 +03:00
co-authored by Claude Opus 5
parent edc19fa882
commit fe3d862d00
2 changed files with 133 additions and 58 deletions
+15 -1
View File
@@ -2002,7 +2002,7 @@ var
Cfg: TTCISettings;
C: TRawClient;
Markers, Answered, Quantum, LongGaps, MaxGapMs, MinReserve: Integer;
N, MinStart, SumStart, StaleRuns: Integer;
N, MinStart, SumStart, StaleRuns, i, FillBefore: Integer;
DbgOwed, DbgFlight: Double;
DbgWin, DbgQ, DbgLead, SeedLead: Integer;
begin
@@ -2048,6 +2048,20 @@ begin
C.WaitText('trx:', 1500);
Check('пейсинг: модуляция из TCI взята', Ctrl.TCIMicActive);
// ── Повторный MOX поверх идущей передачи ──
// ★Прислать MOX=True второй раз вправе и CAT, и web, и TCI, а SetMOX этого
// не отсекает. Перезапускать по нему TX-тракт нельзя: сброс индексов
// mic-кольца гоняется с чтением из TX-потока, и в худшем случае поток
// дописывает свой tail поверх обнулённого — кольцо выглядит почти полным
// СТАРЫХ данных, которые уходят в эфир пачкой.
for i := 1 to 3000 do Ctrl.FDSPEngine.PushTXMicSampleD(0.25);
FillBefore := Ctrl.FDSPEngine.TXMicFill;
Ctrl.SetMOX(True);
Check('повтор MOX: передача не перезапускается', Ctrl.FDSPEngine.TXActive);
Check('повтор MOX: mic-кольцо не обнуляется',
Ctrl.FDSPEngine.TXMicFill >= FillBefore - 512,
Format('было %d, стало %d', [FillBefore, Ctrl.FDSPEngine.TXMicFill]));
// ── Здоровый клиент ──
// Прогрев: пока шёл разбор команды и проверки выше, клиент не отвечал, и
// окно в полёте успело набиться маркерами. Плюс за это время адаптер выдаёт