perf(tr): устранение GUI-фриза ~100мс на TX→RX (живой TX-DSP поток)

TX-DSP поток (TTXDSPThread) больше не пересоздаётся на каждом T/R — он
создаётся один раз и живёт весь сеанс, на приём/передачу переключается
флагом FTXActive (поток при False просто Continue, не трогает канал/ring).
Тердаун перенесён в TWDSPEngine.Close. Раньше Terminate+WaitFor+Free на
каждом отпускании PTT давал фиксированные ~100мс пустого join-оверхеда
pthread_join, блокируя GUI-поток. Итог: 206→106мс, крупные фризы ушли.
Остаток ~106мс = плавный WDSP fade-out TXA (SetChannelState dmode=1) —
оставлен как есть (dmode=0 дал бы trailing key-click в эфир).

Попутно:
- MainForm: убраны прямые PbSpectrum.Invalidate из drag/wheel VFO/центра —
  кадр рисует таймер по FSpectrumDirty (FPS-гейт; прямой Invalidate давал
  GL-кадр на каждое событие мыши мимо гейта → треск в HDMI-звуке AMD APU).
- AudioOutput: const-секция после {$ENDIF} блока PA-констант.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 16:32:35 +03:00
co-authored by Claude Opus 4.8
parent e260995218
commit 98493f08b3
4 changed files with 35 additions and 33 deletions
+1
View File
@@ -189,6 +189,7 @@ const
); );
{$ENDIF} {$ENDIF}
{$ENDIF} {$ENDIF}
const
PA_NO_ERROR = 0; PA_NO_ERROR = 0;
PA_FLOAT32 = LongWord(1); PA_FLOAT32 = LongWord(1);
PA_NO_FLAG = LongWord(0); PA_NO_FLAG = LongWord(0);
+9 -13
View File
@@ -1155,18 +1155,16 @@ begin
rfVfoA: rfVfoA:
begin begin
FreqDispA.Frequency := Round(FController.FVfoA); FreqDispA.Frequency := Round(FController.FVfoA);
// Не рисуем синхронно из wheel/drag: частые события забивают UI-поток. // Не рисуем синхронно из wheel/drag: частые события забивают UI-поток,
// Invalidate лишь планирует; DrawSpectrum откладываем (FSpectrumDirty). // а мгновенный Invalidate давал GL-кадр на КАЖДОЕ событие мыши (при
// Линейку дёргаем сразу — её рендер дешёвый, иначе отстаёт от спектра. // драге рендер упирался в vsync и обходил FPS-гейт — на AMD APU это
// трещало в HDMI-звуке того же GPU). Кадр отрисует ближайший тик
// FSpectrumTimer по FSpectrumDirty. Линейка дешёвая — её сразу.
SyncSpecViewFreq; SyncSpecViewFreq;
FSpectrumDirty := True; FSpectrumDirty := True;
PbSpectrum.Invalidate;
if PbRuler <> nil then PbRuler.Invalidate; if PbRuler <> nil then PbRuler.Invalidate;
if FController.FLastVfoScrolled then if FController.FLastVfoScrolled then
begin
FWaterfallDirty := True; FWaterfallDirty := True;
if PbWaterfall <> nil then PbWaterfall.Invalidate;
end;
end; end;
rfActiveVfo: rfActiveVfo:
begin begin
@@ -1182,11 +1180,11 @@ begin
rfCenterFreq: rfCenterFreq:
begin begin
// CTUN-скролл центра (drag/web): ruler-метки сдвинулись → инвалидируем // CTUN-скролл центра (drag/web): ruler-метки сдвинулись → инвалидируем
// кэш; спектр откладываем (FSpectrumDirty) — drag шлёт много событий. // кэш; спектр откладываем (FSpectrumDirty) — drag шлёт много событий,
// кадр отрисует таймер (FPS-гейт, см. rfVfoA).
FSpecView.InvalidateRulerCache; FSpecView.InvalidateRulerCache;
SyncSpecViewFreq; SyncSpecViewFreq;
FSpectrumDirty := True; FSpectrumDirty := True;
PbSpectrum.Invalidate;
if PbRuler <> nil then PbRuler.Invalidate; if PbRuler <> nil then PbRuler.Invalidate;
end; end;
rfVfoB: rfVfoB:
@@ -1196,8 +1194,7 @@ begin
begin begin
FSpecView.InvalidateRulerCache; FSpecView.InvalidateRulerCache;
SyncSpecViewFreq; SyncSpecViewFreq;
FSpectrumDirty := True; FSpectrumDirty := True; // кадр — по таймеру (FPS-гейт, см. rfVfoA)
PbSpectrum.Invalidate;
if PbRuler <> nil then PbRuler.Invalidate; if PbRuler <> nil then PbRuler.Invalidate;
end; end;
end; end;
@@ -4568,8 +4565,7 @@ begin
FController.SetSliceTarget(FSliceDragId, NewHz); FController.SetSliceTarget(FSliceDragId, NewHz);
PushSliceFlagState(FSliceDragId); // обновить частоту во флаге PushSliceFlagState(FSliceDragId); // обновить частоту во флаге
PositionSliceFlags; PositionSliceFlags;
FSpectrumDirty := True; FSpectrumDirty := True; // кадр — по таймеру (FPS-гейт, как при drag VFO)
PbSpectrum.Invalidate;
end; end;
Exit; Exit;
end; end;
+22 -18
View File
@@ -1259,6 +1259,17 @@ begin
FreeAndNil(FDSPThread); FreeAndNil(FDSPThread);
end; end;
// TX-DSP поток живёт весь сеанс (не пересоздаётся на T/R) — гасим его здесь,
// до закрытия WDSP-каналов (он зовёт fexchange0 на TXA_CHAN).
FTXActive := False;
if Assigned(FTXThread) then
begin
FTXThread.Terminate;
RTLEventSetEvent(FTXMicSem);
FTXThread.WaitFor;
FreeAndNil(FTXThread);
end;
// Закрываем WDSP-каналы слайсов (записи FSlices сохраняем — переоткроются в Open). // Закрываем WDSP-каналы слайсов (записи FSlices сохраняем — переоткроются в Open).
FSliceLock.Enter; FSliceLock.Enter;
try try
@@ -2130,37 +2141,30 @@ end;
procedure TWDSPEngine.SetTXRun(Run: Boolean); procedure TWDSPEngine.SetTXRun(Run: Boolean);
begin begin
FTXActive := Run;
FDispPos := 0; FDispPos := 0;
FTXDispPos := 0; FTXDispPos := 0;
if Run then
FActiveDisplayID := TX_DISP_ID
else
FActiveDisplayID := RX_DISP_ID;
if Run then if Run then
begin begin
if FInitialized then FActiveDisplayID := TX_DISP_ID;
SetChannelState(TXA_CHAN, 1, 0); // run, no delay // Сброс mic ring ДО активации, чтобы простаивавший поток не прочитал старое.
// Сброс mic ring buffer и запуск TX потока
FTXMicHead := 0; FTXMicHead := 0;
FTXMicTail := 0; FTXMicTail := 0;
if FInitialized then
SetChannelState(TXA_CHAN, 1, 0); // run, no delay
// TX-DSP поток создаётся ОДИН раз и живёт весь сеанс. НЕ пересоздаём на каждом
// T/R: join (WaitFor) давал фиксированные ~100 мс фриза GUI на отпускании PTT.
// При FTXActive=False поток простаивает (см. TTXDSPThread.Execute: Continue).
if not Assigned(FTXThread) then if not Assigned(FTXThread) then
begin begin
FTXThread := TTXDSPThread.Create(Self); FTXThread := TTXDSPThread.Create(Self);
FTXThread.Priority := tpHighest; FTXThread.Priority := tpHighest;
end; end;
FTXActive := True; // активируем последним
end else begin end else begin
FTXActive := False; // поток уходит в простой (не трогает канал/ring)
FActiveDisplayID := RX_DISP_ID;
if FInitialized then if FInitialized then
SetChannelState(TXA_CHAN, 0, 1); // stop, with slew SetChannelState(TXA_CHAN, 0, 1); // stop, with slew (поток уже idle)
// Останавливаем TX поток
if Assigned(FTXThread) then
begin
FTXThread.Terminate;
RTLEventSetEvent(FTXMicSem);
FTXThread.WaitFor;
FreeAndNil(FTXThread);
end;
FTXMicHead := 0; FTXMicHead := 0;
FTXMicTail := 0; FTXMicTail := 0;
end; end;
+3 -2
View File
@@ -17,9 +17,9 @@
<UseVersionInfo Value="True"/> <UseVersionInfo Value="True"/>
<AutoIncrementBuild Value="True"/> <AutoIncrementBuild Value="True"/>
<MinorVersionNr Value="9"/> <MinorVersionNr Value="9"/>
<BuildNr Value="245"/> <BuildNr Value="246"/>
</VersionInfo> </VersionInfo>
<MacroValues Count="63"> <MacroValues Count="64">
<Macro1 Name="LCLWidgetType" Value="qt6"/> <Macro1 Name="LCLWidgetType" Value="qt6"/>
<Macro2 Name="LCLWidgetType" Value="qt6"/> <Macro2 Name="LCLWidgetType" Value="qt6"/>
<Macro3 Name="LCLWidgetType" Value="qt6"/> <Macro3 Name="LCLWidgetType" Value="qt6"/>
@@ -83,6 +83,7 @@
<Macro61 Name="LCLWidgetType" Value="qt6"/> <Macro61 Name="LCLWidgetType" Value="qt6"/>
<Macro62 Name="LCLWidgetType" Value="qt6"/> <Macro62 Name="LCLWidgetType" Value="qt6"/>
<Macro63 Name="LCLWidgetType" Value="qt6"/> <Macro63 Name="LCLWidgetType" Value="qt6"/>
<Macro64 Name="LCLWidgetType" Value="qt6"/>
</MacroValues> </MacroValues>
<BuildModes> <BuildModes>
<Item Name="Debug" Default="True"/> <Item Name="Debug" Default="True"/>