mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +00:00
perf: zone CPU profiler (PerfLog) + phase-restructured CPU spectrum render
Инструментация (ВРЕМЕННАЯ, не мержить в main): - PerfLog.pas: зонные таймеры, отчёт раз в 5с в stderr (core% = доля ядра), EWSDR_PERF=0 выкл, EWSDR_PERF_FILE=путь — дублировать в файл - Замеры // PERF: сеть (DDC IQ/wideband), Pluto (rx_conv/tx_resamp/tx_fill), DSP (push_iq/fexchange/slices/tx), аудио, UI (тики/draw/paint/gl_swap, разбивка draw_spectrum по фазам) - doc/PERF_PROFILING.md: карта зон, методика, выводы Оптимизация CPU-рендера спектра (по результатам замеров на эфире): диагноз — дорога не пиксельная работа, а чередование raw(ScanLine)/ canvas(QPainter) доступа к битмапу на Qt6 (~6 переходов за кадр, каждый синкает весь битмап; полоса фильтра в тысячи пикселей стоила 4x дороже полного memcpy кадра). DrawSpectrum перестроен в фазы calc->raw->canvas->raw (2 перехода): FillBandRaw вместо Canvas.FillRect, DrawSliceFilterMarkers разделён на raw-полосы + canvas-линии, композиты флагов и альфа-OR в финальной raw-фазе. Кадр 8.3мс -> 6.1мс; spec_bands 1.87мс -> 0.03мс. Альфа-OR проход на Qt6 подтверждён нужным (без него canvas-пиксели теряют альфу и пропадают при блите), сам цикл ~0.08мс — оставлен; тумблер EWSDR_ALPHA_FIX=0 только для экспериментов. Кисть перед AGC-подписями теперь ставится явно (была остаточной после перестройки фаз). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+5
-1
@@ -21,7 +21,8 @@ unit AudioOutput;
|
||||
interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, Math, SyncObjs, DynLibs;
|
||||
Classes, SysUtils, Math, SyncObjs, DynLibs,
|
||||
PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры
|
||||
|
||||
const
|
||||
MY_AUDIO_BUFFER_SIZE = 128; // default PA frames per callback — как piHPSDR
|
||||
@@ -628,10 +629,13 @@ procedure TAudioOutput.Write(const Left, Right: array of Single; Count: Integer)
|
||||
// Порядок: сначала пишем данные в FBuf, потом обновляем FInPt (memory barrier)
|
||||
var
|
||||
i: Integer;
|
||||
T0: Int64; // PERF
|
||||
begin
|
||||
if not FOpen then Exit;
|
||||
T0 := PerfNow; // PERF
|
||||
for i := 0 to Count - 1 do
|
||||
WriteDouble(Left[i], Right[i]);
|
||||
PerfAdd(pzAudioWrite, T0); // PERF: запись блока в PA ring (все экземпляры)
|
||||
end;
|
||||
|
||||
end.
|
||||
|
||||
+8
-1
@@ -19,7 +19,8 @@ uses
|
||||
{$ELSE}
|
||||
Sockets, BaseUnix,
|
||||
{$ENDIF}
|
||||
HPSDRProtocol, SyncObjs, Settings, RadioBackend;
|
||||
HPSDRProtocol, SyncObjs, Settings, RadioBackend,
|
||||
PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры
|
||||
|
||||
const
|
||||
DUC_TX_QUEUE_SIZE = 256; // power of two, bounded latency on TX underrun/overrun
|
||||
@@ -1093,11 +1094,14 @@ procedure THPSDRNetwork.HandleDDCIQ(const Buf: array of Byte; Len: Integer;
|
||||
DDCIdx: Integer);
|
||||
var
|
||||
Pkt: TDDCIQPacket;
|
||||
T0: Int64; // PERF
|
||||
begin
|
||||
if not Assigned(FOnDDCIQ) then Exit;
|
||||
T0 := PerfNow; // PERF
|
||||
FillChar(Pkt, SizeOf(Pkt), 0);
|
||||
Move(Buf[0], Pkt, Min(Len, SizeOf(Pkt)));
|
||||
FOnDDCIQ(DDCIdx, Pkt); // прямой вызов из потока — UI не трогает
|
||||
PerfAdd(pzNetDDCIQ, T0); // PERF: копия пакета + постановка в DSP-очередь
|
||||
end;
|
||||
|
||||
procedure THPSDRNetwork.HandleMicData(const Buf: array of Byte; Len: Integer);
|
||||
@@ -1114,9 +1118,11 @@ procedure THPSDRNetwork.HandleWidebandData(const Buf: array of Byte; Len: Intege
|
||||
var
|
||||
Seq: LongWord;
|
||||
I, SamplesInPacket, TargetCount, S: Integer;
|
||||
T0: Int64; // PERF
|
||||
begin
|
||||
if (not FWidebandEnabled) or (ADCIdx <> FWidebandADC) or
|
||||
(FWBSampleBits <> 16) or (Len < 6) then Exit;
|
||||
T0 := PerfNow; // PERF
|
||||
|
||||
SamplesInPacket := (Len - 4) div 2;
|
||||
if SamplesInPacket <= 0 then Exit;
|
||||
@@ -1157,6 +1163,7 @@ begin
|
||||
FWBFrameCount := 0;
|
||||
FWBSeqValid := False;
|
||||
end;
|
||||
PerfAdd(pzNetWideband, T0); // PERF: сборка wideband-кадра + колбэк
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -37,6 +37,7 @@ uses
|
||||
WidebandView,
|
||||
StatusBar,
|
||||
PlatformUtils,
|
||||
PerfLog, // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры
|
||||
WinFirewall,
|
||||
BoardUtils, WisdomBuilder, UISync,
|
||||
FlatEdit, FMRepeater,
|
||||
@@ -3168,7 +3169,10 @@ var
|
||||
PeakTarget, MinTarget: Double;
|
||||
PeakDiff, MinDiff: Double;
|
||||
PeakAlpha, MinAlpha: Double;
|
||||
PerfT0: Int64; // PERF
|
||||
begin
|
||||
PerfT0 := PerfNow; // PERF
|
||||
try
|
||||
if not FController.FRunning then
|
||||
begin
|
||||
if PbSMeterRight <> nil then PbSMeterRight.Invalidate;
|
||||
@@ -3222,6 +3226,9 @@ begin
|
||||
else
|
||||
SetStatusText(5, 'TX idle');
|
||||
end;
|
||||
finally
|
||||
PerfAdd(pzUiMeterTick, PerfT0); // PERF: тик метра целиком (10 Гц)
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.SpectrumTimerTick(Sender: TObject);
|
||||
@@ -3229,7 +3236,10 @@ var
|
||||
NowTick: QWord;
|
||||
ElapsedTick: QWord;
|
||||
I: Integer;
|
||||
PerfT0, PerfTz: Int64; // PERF
|
||||
begin
|
||||
PerfT0 := PerfNow; // PERF
|
||||
try
|
||||
// Синхронизируем размеры если размер панели изменился (после ресайза).
|
||||
// Важно: проверяем только видимые панели — когда спектр скрыт,
|
||||
// PbSpectrum.Height может быть ненулевым при FSpectrumHeight=0,
|
||||
@@ -3302,13 +3312,16 @@ begin
|
||||
// Обновляем оверлей если видим
|
||||
if Assigned(FVfoOverlay) and FVfoOverlay.Visible then
|
||||
begin
|
||||
PerfTz := PerfNow; // PERF
|
||||
FVfoOverlay.UpdateSMeter(FController.FLastSMeter);
|
||||
FVfoOverlay.UpdateVfo(FController.FVfoA);
|
||||
PushVfoOverlayExtState;
|
||||
PositionVfoOverlay;
|
||||
PerfAdd(pzUiVfoOverlay, PerfTz); // PERF: обновление главного VFO-флага
|
||||
end;
|
||||
if Length(FSliceFlags) > 0 then
|
||||
begin
|
||||
PerfTz := PerfNow; // PERF
|
||||
PositionSliceFlags; // держим слайс-флаги на частоте
|
||||
// S-метр слайсов троттлим до ~10 Гц: каждое изменение метра = софтовый
|
||||
// ре-рендер флага (дорого), 60 Гц там не нужно.
|
||||
@@ -3319,6 +3332,7 @@ begin
|
||||
if FSliceFlags[I] <> nil then
|
||||
FSliceFlags[I].UpdateSMeter(FController.SliceSMeter(FSliceFlags[I].SliceId));
|
||||
end;
|
||||
PerfAdd(pzUiSliceFlags, PerfTz); // PERF: позиционирование + S-метры слайс-флагов
|
||||
end;
|
||||
// Панорама и водопад теперь рисуются независимо по факту новых кадров.
|
||||
// Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще.
|
||||
@@ -3385,7 +3399,14 @@ begin
|
||||
// Исходящее зеркало состояния + спектр/водопад в web — в адаптере (всё из
|
||||
// FController, включая последний кадр пикселей). Сервер сам гейтит по FRunning.
|
||||
if Assigned(FWebAdapter) then
|
||||
begin
|
||||
PerfTz := PerfNow; // PERF
|
||||
FWebAdapter.PushState;
|
||||
PerfAdd(pzUiWebPush, PerfTz); // PERF: web-зеркало состояния + кадры клиентам
|
||||
end;
|
||||
finally
|
||||
PerfAdd(pzUiSpectrumTick, PerfT0); // PERF: тик спектра целиком (кадровый таймер)
|
||||
end;
|
||||
end;
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
+313
@@ -0,0 +1,313 @@
|
||||
unit PerfLog;
|
||||
|
||||
{ ===========================================================================
|
||||
PerfLog — лёгкий зонный CPU-профилировщик для поиска горячих точек.
|
||||
|
||||
ВРЕМЕННЫЙ ОТЛАДОЧНЫЙ МОДУЛЬ (ветка perf/cpu-profiling) — не мержить в main.
|
||||
|
||||
Использование в горячем коде:
|
||||
var t0: Int64;
|
||||
...
|
||||
t0 := PerfNow;
|
||||
<работа>
|
||||
PerfAdd(pzDspFexMain, t0);
|
||||
|
||||
Накладные расходы: два чтения монотонных часов + несколько сложений.
|
||||
Никаких строк/аллокаций/локов в горячем пути. Каждая зона пишется из
|
||||
одного потока (single-writer), поэтому обычные Int64-инкременты безопасны;
|
||||
репортер читает значения racy — для отладки это приемлемо.
|
||||
|
||||
Отчёт раз в PERF_REPORT_SEC секунд в StdErr (и в файл, если задан
|
||||
EWSDR_PERF_FILE). Отключение: переменная окружения EWSDR_PERF=0.
|
||||
|
||||
Колонки отчёта:
|
||||
calls — вызовов за интервал
|
||||
c/s — вызовов в секунду
|
||||
avg — среднее время на вызов, мкс
|
||||
max — максимум за интервал, мкс
|
||||
total — суммарно за интервал, мс
|
||||
core% — доля одного ядра CPU (total / интервал * 100)
|
||||
=========================================================================== }
|
||||
|
||||
{$mode objfpc}{$H+}
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TPerfZone = (
|
||||
// Сетевой поток (приём UDP от радио)
|
||||
pzNetDDCIQ, // HandleDDCIQ: разбор IQ-пакета + постановка в очередь
|
||||
pzNetWideband, // HandleWidebandData: wideband-пакеты ADC
|
||||
|
||||
// Pluto (libiio, свои RX/TX потоки — мимо HPSDRNetwork)
|
||||
pzPlutoRxConv, // RX: конверсия iio-буфера int16→24bit BE + FOnDDCIQ
|
||||
pzPlutoTxResamp, // TX: полифазный интерполятор 192k→device-rate + FIFO
|
||||
pzPlutoTxFill, // TX: наполнение iio-буфера из FIFO (без блокирующего push)
|
||||
|
||||
// DSP-поток (WDSPEngine)
|
||||
pzDspPushIQ, // PushIQItemToDSP целиком (декод 24-бит + акк + всё ниже)
|
||||
pzDspSpectrum0, // Spectrum0() — скармливание display-блока анализатору
|
||||
pzDspFexMain, // fexchange0 главного RX-канала
|
||||
pzDspAudioCb, // копирование аудио + FOnAudio (маршрут аудио)
|
||||
pzDspSlices, // ProcessSlices целиком (все слайсы)
|
||||
pzDspSliceFex, // fexchange0 одного слайс-канала
|
||||
pzDspTxBlock, // ProcessTXBlock (TX-тракт)
|
||||
|
||||
// Display-поток (WDSPEngine.UpdateSpectrum)
|
||||
pzDispGetPixels, // GetPixels спектр+водопад из анализатора WDSP
|
||||
|
||||
// Аудио-выход (PortAudio ring, из DSP-потока)
|
||||
pzAudioWrite, // TAudioOutput.Write (все экземпляры: главный + слайсы)
|
||||
|
||||
// UI-поток (главная форма, таймеры, отрисовка)
|
||||
pzUiSpectrumTick, // SpectrumTimerTick целиком
|
||||
pzUiMeterTick, // MeterTimerTick целиком
|
||||
pzUiDrawSpectrum, // SpectrumView.DrawSpectrum (CPU или GL)
|
||||
pzUiDrawWaterfall, // SpectrumView.DrawWaterfall
|
||||
pzUiPaintSpectrum, // PaintSpectrum: блит битмапа на канву (Qt6!)
|
||||
pzUiPaintWaterfall,// PaintWaterfall: блит битмапа на канву (Qt6!)
|
||||
pzUiSliceFlags, // обновление S-метров слайс-флагов (ре-рендер флага)
|
||||
pzUiVfoOverlay, // обновление VFO-оверлея (S-метр/частота/позиция)
|
||||
pzUiWidebandPaint, // отрисовка wideband-панорамы
|
||||
pzUiWebPush, // WebAdapter.PushState (зеркало состояния + кадры в web)
|
||||
|
||||
// Разбивка CPU-рендера спектра (внутри ui.draw_spectrum)
|
||||
pzUiSpecGrid, // ребилд кэша сетки + полный memcpy сетки в битмап
|
||||
pzUiSpecBands, // полоса фильтра + слайс-маркеры + AGC-линии (BlendBand)
|
||||
pzUiSpecCurve, // расчёт точек кривой + градиент под кривой + Polyline
|
||||
pzUiSpecOverlays, // края/VFO/маркеры/бэндплан/флаги (DrawOverlay в битмап)
|
||||
pzUiSpecAlpha, // финальный альфа-OR проход по всему битмапу
|
||||
pzUiGlSwap // SwapBuffers (GL) — включает ожидание vsync!
|
||||
);
|
||||
|
||||
const
|
||||
PERF_REPORT_SEC = 5;
|
||||
|
||||
PerfZoneName: array[TPerfZone] of string = (
|
||||
'net.ddc_iq',
|
||||
'net.wideband',
|
||||
'pluto.rx_conv',
|
||||
'pluto.tx_resamp',
|
||||
'pluto.tx_fill',
|
||||
'dsp.push_iq',
|
||||
'dsp.spectrum0',
|
||||
'dsp.fex_main',
|
||||
'dsp.audio_cb',
|
||||
'dsp.slices',
|
||||
'dsp.slice_fex',
|
||||
'dsp.tx_block',
|
||||
'disp.get_pixels',
|
||||
'audio.write',
|
||||
'ui.spectrum_tick',
|
||||
'ui.meter_tick',
|
||||
'ui.draw_spectrum',
|
||||
'ui.draw_waterfall',
|
||||
'ui.paint_spectrum',
|
||||
'ui.paint_waterfall',
|
||||
'ui.slice_flags',
|
||||
'ui.vfo_overlay',
|
||||
'ui.wideband_paint',
|
||||
'ui.web_push',
|
||||
'ui.spec_grid',
|
||||
'ui.spec_bands',
|
||||
'ui.spec_curve',
|
||||
'ui.spec_overlays',
|
||||
'ui.spec_alpha',
|
||||
'ui.gl_swap'
|
||||
);
|
||||
|
||||
var
|
||||
PerfEnabled: Boolean = True;
|
||||
|
||||
// Монотонные микросекунды.
|
||||
function PerfNow: Int64;
|
||||
|
||||
// Прибавить (PerfNow - StartUs) к зоне. Вызывать в конце измеряемого участка.
|
||||
procedure PerfAdd(Zone: TPerfZone; StartUs: Int64);
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
SysUtils, Classes
|
||||
{$IFDEF WINDOWS}, Windows{$ENDIF}
|
||||
{$IFDEF LINUX}, BaseUnix, Linux{$ENDIF}
|
||||
{$IF DEFINED(UNIX) AND NOT DEFINED(LINUX)}, BaseUnix, Unix{$ENDIF};
|
||||
|
||||
type
|
||||
TZoneAcc = record
|
||||
Count: Int64;
|
||||
TotalUs: Int64;
|
||||
MaxUs: Int64;
|
||||
end;
|
||||
|
||||
var
|
||||
Zones: array[TPerfZone] of TZoneAcc;
|
||||
|
||||
{$IFDEF WINDOWS}
|
||||
var
|
||||
QPCFreq: Int64 = 0;
|
||||
|
||||
function PerfNow: Int64;
|
||||
var C: Int64;
|
||||
begin
|
||||
if QPCFreq = 0 then QueryPerformanceFrequency(QPCFreq);
|
||||
QueryPerformanceCounter(C);
|
||||
Result := (C * 1000000) div QPCFreq;
|
||||
end;
|
||||
{$ELSE}
|
||||
{$IFDEF LINUX}
|
||||
function PerfNow: Int64;
|
||||
var ts: TTimeSpec;
|
||||
begin
|
||||
clock_gettime(CLOCK_MONOTONIC, @ts);
|
||||
Result := Int64(ts.tv_sec) * 1000000 + ts.tv_nsec div 1000;
|
||||
end;
|
||||
{$ELSE}
|
||||
function PerfNow: Int64;
|
||||
var tv: TTimeVal;
|
||||
begin
|
||||
fpGetTimeOfDay(@tv, nil);
|
||||
Result := Int64(tv.tv_sec) * 1000000 + tv.tv_usec;
|
||||
end;
|
||||
{$ENDIF}
|
||||
{$ENDIF}
|
||||
|
||||
procedure PerfAdd(Zone: TPerfZone; StartUs: Int64);
|
||||
var El: Int64;
|
||||
begin
|
||||
if not PerfEnabled then Exit;
|
||||
El := PerfNow - StartUs;
|
||||
if El < 0 then El := 0;
|
||||
Inc(Zones[Zone].Count);
|
||||
Inc(Zones[Zone].TotalUs, El);
|
||||
if El > Zones[Zone].MaxUs then Zones[Zone].MaxUs := El;
|
||||
end;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Репортер
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type
|
||||
TPerfReporter = class(TThread)
|
||||
private
|
||||
FFile: TextFile;
|
||||
FFileOpen: Boolean;
|
||||
procedure Emit(const S: string);
|
||||
procedure Report(ElapsedUs: Int64);
|
||||
protected
|
||||
procedure Execute; override;
|
||||
public
|
||||
constructor Create;
|
||||
end;
|
||||
|
||||
var
|
||||
Reporter: TPerfReporter = nil;
|
||||
|
||||
constructor TPerfReporter.Create;
|
||||
var FN: string;
|
||||
begin
|
||||
FFileOpen := False;
|
||||
FN := GetEnvironmentVariable('EWSDR_PERF_FILE');
|
||||
if FN <> '' then
|
||||
begin
|
||||
AssignFile(FFile, FN);
|
||||
try
|
||||
Rewrite(FFile);
|
||||
FFileOpen := True;
|
||||
except
|
||||
FFileOpen := False;
|
||||
end;
|
||||
end;
|
||||
FreeOnTerminate := False;
|
||||
inherited Create(False);
|
||||
end;
|
||||
|
||||
procedure TPerfReporter.Emit(const S: string);
|
||||
begin
|
||||
try
|
||||
WriteLn(StdErr, S);
|
||||
except
|
||||
end;
|
||||
if FFileOpen then
|
||||
begin
|
||||
WriteLn(FFile, S);
|
||||
Flush(FFile);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TPerfReporter.Report(ElapsedUs: Int64);
|
||||
var
|
||||
Z: TPerfZone;
|
||||
Snap: array[TPerfZone] of TZoneAcc;
|
||||
Sec: Double;
|
||||
Any: Boolean;
|
||||
begin
|
||||
// Снимок + сброс (racy, но для отладки достаточно).
|
||||
for Z := Low(TPerfZone) to High(TPerfZone) do
|
||||
begin
|
||||
Snap[Z] := Zones[Z];
|
||||
Zones[Z].Count := 0;
|
||||
Zones[Z].TotalUs := 0;
|
||||
Zones[Z].MaxUs := 0;
|
||||
end;
|
||||
|
||||
Any := False;
|
||||
for Z := Low(TPerfZone) to High(TPerfZone) do
|
||||
if Snap[Z].Count > 0 then begin Any := True; Break; end;
|
||||
if not Any then Exit;
|
||||
|
||||
Sec := ElapsedUs / 1000000.0;
|
||||
Emit(Format('[PERF] ===== interval %.2f s =====', [Sec]));
|
||||
Emit(Format('[PERF] %-18s %8s %8s %8s %8s %9s %6s',
|
||||
['zone', 'calls', 'c/s', 'avg_us', 'max_us', 'total_ms', 'core%']));
|
||||
for Z := Low(TPerfZone) to High(TPerfZone) do
|
||||
if Snap[Z].Count > 0 then
|
||||
Emit(Format('[PERF] %-18s %8d %8.0f %8.1f %8d %9.1f %6.1f',
|
||||
[PerfZoneName[Z],
|
||||
Snap[Z].Count,
|
||||
Snap[Z].Count / Sec,
|
||||
Snap[Z].TotalUs / Snap[Z].Count,
|
||||
Snap[Z].MaxUs,
|
||||
Snap[Z].TotalUs / 1000.0,
|
||||
Snap[Z].TotalUs / ElapsedUs * 100.0]));
|
||||
end;
|
||||
|
||||
procedure TPerfReporter.Execute;
|
||||
var
|
||||
LastUs, NowUs: Int64;
|
||||
Waited: Integer;
|
||||
begin
|
||||
LastUs := PerfNow;
|
||||
while not Terminated do
|
||||
begin
|
||||
// Спим мелкими шагами, чтобы быстро завершаться.
|
||||
Waited := 0;
|
||||
while (Waited < PERF_REPORT_SEC * 1000) and not Terminated do
|
||||
begin
|
||||
Sleep(100);
|
||||
Inc(Waited, 100);
|
||||
end;
|
||||
if Terminated then Break;
|
||||
NowUs := PerfNow;
|
||||
Report(NowUs - LastUs);
|
||||
LastUs := NowUs;
|
||||
end;
|
||||
end;
|
||||
|
||||
initialization
|
||||
if GetEnvironmentVariable('EWSDR_PERF') = '0' then
|
||||
PerfEnabled := False;
|
||||
if PerfEnabled then
|
||||
Reporter := TPerfReporter.Create;
|
||||
|
||||
finalization
|
||||
if Reporter <> nil then
|
||||
begin
|
||||
Reporter.Terminate;
|
||||
Reporter.WaitFor;
|
||||
if Reporter.FFileOpen then
|
||||
CloseFile(Reporter.FFile);
|
||||
Reporter.Free;
|
||||
end;
|
||||
|
||||
end.
|
||||
+11
-1
@@ -26,7 +26,8 @@ interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, ctypes,
|
||||
RadioBackend, IIOBindings, HPSDRProtocol, Settings;
|
||||
RadioBackend, IIOBindings, HPSDRProtocol, Settings,
|
||||
PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры
|
||||
|
||||
type
|
||||
{ TPlutoBackend }
|
||||
@@ -195,6 +196,7 @@ var
|
||||
outc: Integer;
|
||||
vi, vq: LongInt;
|
||||
u: LongWord;
|
||||
T0: Int64; // PERF
|
||||
|
||||
procedure FlushPkt;
|
||||
begin
|
||||
@@ -220,6 +222,7 @@ begin
|
||||
if Terminated then Break;
|
||||
if n <= 0 then begin Sleep(2); Continue; end;
|
||||
|
||||
T0 := PerfNow; // PERF: только конверсия, без блокирующего refill
|
||||
step := iio_buffer_step(FBk.FRxBuf);
|
||||
pFirst := iio_buffer_first(FBk.FRxBuf, FBk.FRxChI);
|
||||
pEnd := iio_buffer_end(FBk.FRxBuf);
|
||||
@@ -246,6 +249,7 @@ begin
|
||||
Inc(p, step);
|
||||
end;
|
||||
if outc > 0 then FlushPkt; // хвост блока
|
||||
PerfAdd(pzPlutoRxConv, T0); // PERF: int16→24bit BE + FOnDDCIQ на весь iio-буфер
|
||||
end;
|
||||
end;
|
||||
|
||||
@@ -262,10 +266,12 @@ var
|
||||
pFirst, pEnd, p: PByte;
|
||||
h: Integer;
|
||||
n: ptrint;
|
||||
T0: Int64; // PERF
|
||||
begin
|
||||
while not Terminated do
|
||||
begin
|
||||
if FBk.FTxBuf = nil then begin Sleep(5); Continue; end;
|
||||
T0 := PerfNow; // PERF
|
||||
step := iio_buffer_step(FBk.FTxBuf);
|
||||
pFirst := iio_buffer_first(FBk.FTxBuf, FBk.FTxChI);
|
||||
pEnd := iio_buffer_end(FBk.FTxBuf);
|
||||
@@ -291,6 +297,7 @@ begin
|
||||
Inc(p, step);
|
||||
end;
|
||||
LeaveCriticalSection(FBk.FTxLock);
|
||||
PerfAdd(pzPlutoTxFill, T0); // PERF: наполнение буфера (без блокирующего push)
|
||||
|
||||
if Terminated then Break;
|
||||
n := iio_buffer_push(FBk.FTxBuf); // блокирует → пейсинг на device-rate
|
||||
@@ -853,8 +860,10 @@ procedure TPlutoBackend.SendDUCIQ(const IData, QData: array of Integer);
|
||||
var
|
||||
i, p, k, t, L, taps, base, n: Integer;
|
||||
ai, aq: Double;
|
||||
T0: Int64; // PERF
|
||||
begin
|
||||
if (not FTxStreaming) or (FTxRatio < 1) or (Length(FTxCoef) = 0) then Exit;
|
||||
T0 := PerfNow; // PERF
|
||||
n := Length(IData);
|
||||
if Length(QData) < n then n := Length(QData);
|
||||
L := FTxRatio;
|
||||
@@ -893,6 +902,7 @@ begin
|
||||
finally
|
||||
LeaveCriticalSection(FTxLock);
|
||||
end;
|
||||
PerfAdd(pzPlutoTxResamp, T0); // PERF: полифазный интерполятор + 24→16 + FIFO
|
||||
end;
|
||||
|
||||
procedure TPlutoBackend.ClearDUCIQQueue;
|
||||
|
||||
+198
-75
@@ -21,7 +21,8 @@ uses
|
||||
Classes, SysUtils, Graphics, ExtCtrls, Controls, Math,
|
||||
AppTheme,
|
||||
AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay,
|
||||
WaterfallView, SMeterView, RulerView;
|
||||
WaterfallView, SMeterView, RulerView,
|
||||
PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры
|
||||
|
||||
const
|
||||
SV_CLR_BG = TColor($00101010);
|
||||
@@ -109,10 +110,15 @@ type
|
||||
procedure SetFillSpectrum(V: Boolean);
|
||||
procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer);
|
||||
procedure DrawMarkerLine(C: TCanvas; W, H: Integer);
|
||||
procedure DrawSliceFilterMarkers(C: TCanvas; W, H: Integer);
|
||||
// Слайс-маркеры разделены на raw-часть (полосы, ScanLine) и canvas-часть
|
||||
// (линии/буквы): DrawSpectrum группирует raw- и canvas-операции, чтобы
|
||||
// не гонять Qt6-битмап между режимами доступа (дорогая синхронизация).
|
||||
procedure DrawSliceFilterBands(W, H: Integer);
|
||||
procedure DrawSliceFilterLines(C: TCanvas; W, H: Integer);
|
||||
procedure DrawBandLetter(C: TCanvas; X1, X2: Integer; L: Char; Clr: TColor);
|
||||
procedure DrawBeaconMarkers(C: TCanvas; W, H: Integer);
|
||||
procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte);
|
||||
procedure FillBandRaw(X1, X2, H: Integer; Color: TColor);
|
||||
procedure CopyGridToSpectrum(W, H: Integer);
|
||||
procedure DrawADCOverloadOverlay(C: TCanvas; W, H: Integer);
|
||||
procedure CalcFilterBandX(VfoFreq: Double; W: Integer;
|
||||
@@ -267,6 +273,11 @@ type
|
||||
|
||||
implementation
|
||||
|
||||
var
|
||||
// ВРЕМЕННО (perf/cpu-profiling): EWSDR_ALPHA_FIX=0 → пропустить альфа-OR
|
||||
// проход в DrawSpectrum (проверка, нужен ли он на Qt6).
|
||||
GSkipAlphaFix: Boolean = False;
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Вспомогательные функции
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -618,13 +629,53 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
// Непрозрачная вертикальная полоса через raw-доступ (ScanLine) — замена
|
||||
// Canvas.FillRect для полосы фильтра в DrawSpectrum: полоса рисуется в
|
||||
// raw-фазе (между memcpy сетки и градиентом), а FillRect дёргал бы Canvas
|
||||
// и форсил лишнюю синхронизацию битмапа на Qt6.
|
||||
procedure TSpectrumView.FillBandRaw(X1, X2, H: Integer; Color: TColor);
|
||||
var
|
||||
Y, W: Integer;
|
||||
Row: PLongWord;
|
||||
Px: LongWord;
|
||||
R, G, B: Byte;
|
||||
begin
|
||||
if FSpectrumBitmap = nil then Exit;
|
||||
W := FSpectrumBitmap.Width;
|
||||
if X1 < 0 then X1 := 0;
|
||||
if X2 > W then X2 := W;
|
||||
if X2 <= X1 then Exit;
|
||||
R := Color and $FF; G := (Color shr 8) and $FF; B := (Color shr 16) and $FF;
|
||||
{$IFDEF DARWIN}
|
||||
// ARGB в памяти (см. BlendBand): байт 0 = альфа, дальше R,G,B.
|
||||
Px := LongWord($FF) or (LongWord(R) shl 8) or (LongWord(G) shl 16) or
|
||||
(LongWord(B) shl 24);
|
||||
{$ELSE}
|
||||
// BGRA в памяти: байты B,G,R,A.
|
||||
Px := LongWord(B) or (LongWord(G) shl 8) or (LongWord(R) shl 16) or $FF000000;
|
||||
{$ENDIF}
|
||||
FSpectrumBitmap.BeginUpdate(False);
|
||||
try
|
||||
for Y := 0 to H - 1 do
|
||||
begin
|
||||
Row := PLongWord(FSpectrumBitmap.ScanLine[Y]);
|
||||
if Row = nil then Continue;
|
||||
Inc(Row, X1);
|
||||
FillDWord(Row^, X2 - X1, Px);
|
||||
end;
|
||||
finally
|
||||
FSpectrumBitmap.EndUpdate(False);
|
||||
end;
|
||||
end;
|
||||
|
||||
// Маркер фильтра каждого слайса (B+) на спектре — полупрозрачная полоса
|
||||
// пропускания + края + центральная несущая. Янтарный цвет, как в GL-вьюхе
|
||||
// (SpectrumViewOpengl.DrawSliceFilterMarkers). Рисуется под кривой спектра.
|
||||
procedure TSpectrumView.DrawSliceFilterMarkers(C: TCanvas; W, H: Integer);
|
||||
// (SpectrumViewOpengl.DrawSliceFilterMarkers). Полосы (raw ScanLine-блендинг)
|
||||
// и линии/буквы (canvas) разнесены по разным процедурам — их зовут из разных
|
||||
// фаз DrawSpectrum, чтобы не чередовать режимы доступа к битмапу.
|
||||
procedure TSpectrumView.DrawSliceFilterBands(W, H: Integer);
|
||||
const
|
||||
SLICE_R = $F0; SLICE_G = $A8; SLICE_B = $10; // янтарный (совпадает с SLICE_CLR в GL)
|
||||
SLICE_CLR = TColor($0010A8F0);
|
||||
var
|
||||
i, SX1, SX2, SVfoX: Integer;
|
||||
O: TVfoOverlay;
|
||||
@@ -635,6 +686,21 @@ begin
|
||||
if (O = nil) or (not O.Visible) then Continue;
|
||||
CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, SX1, SX2, SVfoX);
|
||||
if SX2 > SX1 then BlendBand(SX1, SX2, H, SLICE_R, SLICE_G, SLICE_B, 60);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.DrawSliceFilterLines(C: TCanvas; W, H: Integer);
|
||||
const
|
||||
SLICE_CLR = TColor($0010A8F0);
|
||||
var
|
||||
i, SX1, SX2, SVfoX: Integer;
|
||||
O: TVfoOverlay;
|
||||
begin
|
||||
for i := 0 to FSliceOverlays.Count - 1 do
|
||||
begin
|
||||
O := TVfoOverlay(FSliceOverlays[i]);
|
||||
if (O = nil) or (not O.Visible) then Continue;
|
||||
CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, SX1, SX2, SVfoX);
|
||||
C.Pen.Color := SLICE_CLR; C.Pen.Width := 1; C.Pen.Style := psSolid;
|
||||
C.MoveTo(SX1, 0); C.LineTo(SX1, H);
|
||||
C.MoveTo(SX2, 0); C.LineTo(SX2, H);
|
||||
@@ -901,8 +967,11 @@ var
|
||||
LabelBandW, SrcCount: Integer;
|
||||
GridFreqS, GridLine, pixPerStep: Double;
|
||||
N, gridMult: Integer;
|
||||
PerfT0, PerfTz: Int64; // PERF
|
||||
begin
|
||||
if FSpectrumBitmap = nil then Exit;
|
||||
PerfT0 := PerfNow; // PERF
|
||||
PerfTz := PerfT0; // PERF
|
||||
W := FSpectrumBitmap.Width; H := FSpectrumBitmap.Height;
|
||||
if (W <= 0) or (H <= 0) then Exit;
|
||||
C := FSpectrumBitmap.Canvas;
|
||||
@@ -978,68 +1047,27 @@ begin
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
CopyGridToSpectrum(W, H);
|
||||
PerfAdd(pzUiSpecGrid, PerfTz); // PERF: ребилд кэша сетки (обычно 0)
|
||||
PerfTz := PerfNow; // PERF
|
||||
|
||||
// ── 2. Полоса фильтра ─────────────────────────────────────────────────────
|
||||
// ═══ Фаза 0: чистые вычисления — битмап не трогаем ═══════════════════════
|
||||
// Дальше битмап обрабатывается тремя непрерывными фазами: raw (ScanLine) →
|
||||
// canvas (QPainter) → raw. Чередование режимов доступа на Qt6 форсит
|
||||
// дорогую синхронизацию всего битмапа на каждом переходе (замерено: полоса
|
||||
// фильтра в пару тысяч пикселей стоила 4× дороже полного memcpy кадра),
|
||||
// поэтому порядок операций здесь важнее «логичной» группировки по смыслу.
|
||||
|
||||
// Полоса фильтра (X-координаты главного и TX-VFO)
|
||||
if FActiveVfo = 0 then CalcFilterBandX(FVfoA, W, X1, X2, VfoX)
|
||||
else CalcFilterBandX(FVfoB, W, X1, X2, VfoX);
|
||||
if FTXOverlay then
|
||||
TXX1 := 0; TXX2 := 0; TXVfoX := 0;
|
||||
if FTXOverlay and (FTXVfoIndex <> FActiveVfo) then
|
||||
begin
|
||||
if FTXVfoIndex <> FActiveVfo then
|
||||
begin
|
||||
if X2 > X1 then BlendBand(X1, X2, H, $30, $C0, $30, 100);
|
||||
if FTXVfoIndex = 0 then CalcFilterBandX(FVfoA, W, TXX1, TXX2, TXVfoX)
|
||||
else CalcFilterBandX(FVfoB, W, TXX1, TXX2, TXVfoX);
|
||||
if TXX2 > TXX1 then BlendBand(TXX1, TXX2, H, $E0, $30, $30, 100);
|
||||
end else
|
||||
if X2 > X1 then BlendBand(X1, X2, H, $E0, $30, $30, 100);
|
||||
end else
|
||||
begin
|
||||
C.Brush.Color := FTheme.SpecFilter; C.Brush.Style := bsSolid; C.Pen.Style := psClear;
|
||||
if X2 > X1 then C.FillRect(Rect(X1, 0, X2, H));
|
||||
C.Pen.Style := psSolid;
|
||||
if FTXVfoIndex = 0 then CalcFilterBandX(FVfoA, W, TXX1, TXX2, TXVfoX)
|
||||
else CalcFilterBandX(FVfoB, W, TXX1, TXX2, TXVfoX);
|
||||
end;
|
||||
|
||||
// Буква главного флага (A) на его полосе — только когда есть слайсы,
|
||||
// иначе одиночный приём не засоряем.
|
||||
if (FSliceOverlays <> nil) and (FSliceOverlays.Count > 0) and
|
||||
(FActiveVfo = 0) and Assigned(FVfoOverlay) then
|
||||
DrawBandLetter(C, X1, X2, FVfoOverlay.SliceLetter, SliceColor(FVfoOverlay.SliceLetter));
|
||||
|
||||
// Полосы фильтра слайсов (B+) — под кривой спектра, как в GL-вьюхе.
|
||||
DrawSliceFilterMarkers(C, W, H);
|
||||
|
||||
// ── 3. AGC линии ──────────────────────────────────────────────────────────
|
||||
if FWDSPReady then
|
||||
begin
|
||||
AGCy := Round((DBmax - FAGCThresh) * InvRange * H);
|
||||
if (AGCy >= 0) and (AGCy < H) then
|
||||
begin
|
||||
C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash;
|
||||
C.MoveTo(0, AGCy); C.LineTo(W, AGCy);
|
||||
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6;
|
||||
C.TextOut(4, AGCy - 9, 'AGC T');
|
||||
end;
|
||||
AGCHangY := Round((DBmax - FAGCHangLevel) * InvRange * H);
|
||||
if (AGCHangY >= 0) and (AGCHangY < H) and (Abs(AGCHangY - AGCy) > 4) then
|
||||
begin
|
||||
C.Pen.Color := FTheme.SpecAgcHangColor; C.Pen.Width := 1; C.Pen.Style := psDot;
|
||||
C.MoveTo(0, AGCHangY); C.LineTo(W, AGCHangY);
|
||||
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcHangColor; C.Font.Size := 6;
|
||||
C.TextOut(4, AGCHangY + 2, 'AGC H');
|
||||
end;
|
||||
end else begin
|
||||
AGCy := Round((DBmax - (-FAGCTop)) * InvRange * H);
|
||||
if (AGCy >= 0) and (AGCy < H) then
|
||||
begin
|
||||
C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash;
|
||||
C.MoveTo(0, AGCy); C.LineTo(W, AGCy);
|
||||
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6;
|
||||
C.TextOut(4, AGCy - 9, Format('AGC -%ddB', [FAGCTop]));
|
||||
end;
|
||||
end;
|
||||
|
||||
// ── 4. Кривая спектра ─────────────────────────────────────────────────────
|
||||
// Точки кривой спектра
|
||||
if FSpPtsLen <> W + 2 then
|
||||
begin SetLength(FSpPts, W + 2); FSpPtsLen := W + 2; end;
|
||||
SrcCount := EnsureRange(FSpectrumBufCount, 2, 1024);
|
||||
@@ -1073,18 +1101,86 @@ begin
|
||||
end;
|
||||
FSpPts[W] := Point(W-1, H); FSpPts[W+1] := Point(0, H);
|
||||
|
||||
// ── 5. Градиент (заливка под кривой) ──────────────────────────────────────
|
||||
if FFillSpectrum then
|
||||
PerfAdd(pzUiSpecCurve, PerfTz); // PERF: расчёт точек кривой
|
||||
PerfTz := PerfNow; // PERF
|
||||
|
||||
// ═══ Фаза 1: raw (ScanLine) — сетка, полосы, градиент ════════════════════
|
||||
CopyGridToSpectrum(W, H);
|
||||
PerfAdd(pzUiSpecGrid, PerfTz); // PERF: += полный memcpy сетки в битмап
|
||||
PerfTz := PerfNow; // PERF
|
||||
|
||||
if FTXOverlay then
|
||||
begin
|
||||
C.Brush.Style := bsSolid; C.Pen.Style := psClear;
|
||||
if FTXVfoIndex <> FActiveVfo then
|
||||
begin
|
||||
if X2 > X1 then BlendBand(X1, X2, H, $30, $C0, $30, 100);
|
||||
if TXX2 > TXX1 then BlendBand(TXX1, TXX2, H, $E0, $30, $30, 100);
|
||||
end else
|
||||
if X2 > X1 then BlendBand(X1, X2, H, $E0, $30, $30, 100);
|
||||
end else
|
||||
if X2 > X1 then FillBandRaw(X1, X2, H, FTheme.SpecFilter);
|
||||
|
||||
// Полосы фильтра слайсов (B+) — под градиентом/кривой, как в GL-вьюхе.
|
||||
DrawSliceFilterBands(W, H);
|
||||
|
||||
PerfAdd(pzUiSpecBands, PerfTz); // PERF: полосы фильтра (raw-заливки)
|
||||
PerfTz := PerfNow; // PERF
|
||||
|
||||
// Градиент (заливка под кривой)
|
||||
if FFillSpectrum then
|
||||
DrawSpectrumGradient(FSpPts, W, H);
|
||||
|
||||
PerfAdd(pzUiSpecCurve, PerfTz); // PERF: += градиент под кривой
|
||||
PerfTz := PerfNow; // PERF
|
||||
|
||||
// ═══ Фаза 2: canvas (QPainter) — линии, текст, кривая, маркеры ═══════════
|
||||
// Буква главного флага (A) на его полосе — только когда есть слайсы,
|
||||
// иначе одиночный приём не засоряем.
|
||||
if (FSliceOverlays <> nil) and (FSliceOverlays.Count > 0) and
|
||||
(FActiveVfo = 0) and Assigned(FVfoOverlay) then
|
||||
DrawBandLetter(C, X1, X2, FVfoOverlay.SliceLetter, SliceColor(FVfoOverlay.SliceLetter));
|
||||
|
||||
// Края/несущие/буквы слайсов (raw-полосы уже нарисованы в фазе 1).
|
||||
DrawSliceFilterLines(C, W, H);
|
||||
|
||||
// AGC линии. Кисть выставляем явно: TextOut подписей рисует фон кистью, и
|
||||
// до перестройки фаз здесь всегда была bsSolid/SpecFilter (от FillRect
|
||||
// полосы) — сохраняем тот же вид, не полагаясь на остаточное состояние.
|
||||
C.Brush.Color := FTheme.SpecFilter; C.Brush.Style := bsSolid;
|
||||
if FWDSPReady then
|
||||
begin
|
||||
AGCy := Round((DBmax - FAGCThresh) * InvRange * H);
|
||||
if (AGCy >= 0) and (AGCy < H) then
|
||||
begin
|
||||
C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash;
|
||||
C.MoveTo(0, AGCy); C.LineTo(W, AGCy);
|
||||
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6;
|
||||
C.TextOut(4, AGCy - 9, 'AGC T');
|
||||
end;
|
||||
AGCHangY := Round((DBmax - FAGCHangLevel) * InvRange * H);
|
||||
if (AGCHangY >= 0) and (AGCHangY < H) and (Abs(AGCHangY - AGCy) > 4) then
|
||||
begin
|
||||
C.Pen.Color := FTheme.SpecAgcHangColor; C.Pen.Width := 1; C.Pen.Style := psDot;
|
||||
C.MoveTo(0, AGCHangY); C.LineTo(W, AGCHangY);
|
||||
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcHangColor; C.Font.Size := 6;
|
||||
C.TextOut(4, AGCHangY + 2, 'AGC H');
|
||||
end;
|
||||
end else begin
|
||||
AGCy := Round((DBmax - (-FAGCTop)) * InvRange * H);
|
||||
if (AGCy >= 0) and (AGCy < H) then
|
||||
begin
|
||||
C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash;
|
||||
C.MoveTo(0, AGCy); C.LineTo(W, AGCy);
|
||||
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6;
|
||||
C.TextOut(4, AGCy - 9, Format('AGC -%ddB', [FAGCTop]));
|
||||
end;
|
||||
end;
|
||||
|
||||
// ── 6. Линия спектра ──────────────────────────────────────────────────────
|
||||
// Линия спектра
|
||||
C.Pen.Style := psSolid; C.Pen.Color := FTheme.SpecLine; C.Pen.Width := 1;
|
||||
C.Polyline(Slice(FSpPts, W));
|
||||
|
||||
// ── 7. Края фильтра + VFO ─────────────────────────────────────────────────
|
||||
// Края фильтра + VFO
|
||||
C.Pen.Color := FTheme.SpecFilterEdge; C.Pen.Width := 1; C.Pen.Style := psSolid;
|
||||
C.MoveTo(X1, 0); C.LineTo(X1, H);
|
||||
C.MoveTo(X2, 0); C.LineTo(X2, H);
|
||||
@@ -1106,6 +1202,11 @@ begin
|
||||
if FMarkerActive then DrawMarkerLine(C, W, H);
|
||||
if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkers(C, W, H);
|
||||
DrawADCOverloadOverlay(C, W, H);
|
||||
|
||||
PerfAdd(pzUiSpecOverlays, PerfTz); // PERF: canvas-фаза (линии/текст/кривая)
|
||||
PerfTz := PerfNow; // PERF
|
||||
|
||||
// ═══ Фаза 3: raw — композиты оверлеев + альфа-проход ═════════════════════
|
||||
// Бэндплан QO-100 — полоска внизу спектра (над ruler), под панелями оверлеев.
|
||||
if Assigned(FBandPlanOverlay) then
|
||||
FBandPlanOverlay.DrawOverlay(FSpectrumBitmap, W, H);
|
||||
@@ -1121,20 +1222,29 @@ begin
|
||||
// (Canvas-операции сохраняют альфу $FF), поэтому там его не делаем.
|
||||
// На Qt6/win32 бэкенд другой и сглаженные края могут занулять альфу —
|
||||
// оставляем быстрый dword-OR до проверки на этих платформах.
|
||||
// ВРЕМЕННО (perf/cpu-profiling): EWSDR_ALPHA_FIX=0 пропускает проход —
|
||||
// для визуальной проверки, нужен ли он на Qt6 вообще (подозрение: нет,
|
||||
// SourceOver поверх непрозрачного фона альфу не занижает). Если артефактов
|
||||
// не будет — проход можно удалить совсем.
|
||||
{$IFNDEF DARWIN}
|
||||
FSpectrumBitmap.BeginUpdate(False);
|
||||
for i := 0 to H - 1 do
|
||||
if not GSkipAlphaFix then
|
||||
begin
|
||||
RowLW := PLongWord(FSpectrumBitmap.ScanLine[i]);
|
||||
if RowLW = nil then Continue;
|
||||
for Yp := 0 to W - 1 do
|
||||
FSpectrumBitmap.BeginUpdate(False);
|
||||
for i := 0 to H - 1 do
|
||||
begin
|
||||
RowLW^ := RowLW^ or $FF000000; // BGRA: альфа в байте 3
|
||||
Inc(RowLW);
|
||||
RowLW := PLongWord(FSpectrumBitmap.ScanLine[i]);
|
||||
if RowLW = nil then Continue;
|
||||
for Yp := 0 to W - 1 do
|
||||
begin
|
||||
RowLW^ := RowLW^ or $FF000000; // BGRA: альфа в байте 3
|
||||
Inc(RowLW);
|
||||
end;
|
||||
end;
|
||||
FSpectrumBitmap.EndUpdate(False);
|
||||
end;
|
||||
FSpectrumBitmap.EndUpdate(False);
|
||||
{$ENDIF}
|
||||
PerfAdd(pzUiSpecAlpha, PerfTz); // PERF: композиты флагов + альфа-OR проход
|
||||
PerfAdd(pzUiDrawSpectrum, PerfT0); // PERF: CPU-рендер кадра спектра
|
||||
end;
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -1142,8 +1252,11 @@ end;
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
procedure TSpectrumView.DrawWaterfall;
|
||||
var T0: Int64; // PERF
|
||||
begin
|
||||
T0 := PerfNow; // PERF
|
||||
FWaterfall.DrawWaterfall;
|
||||
PerfAdd(pzUiDrawWaterfall, T0); // PERF: CPU-рендер строк водопада
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.DrawRuler;
|
||||
@@ -1156,15 +1269,22 @@ end;
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
procedure TSpectrumView.PaintSpectrum(Sender: TObject);
|
||||
var PB: TPaintBox;
|
||||
var
|
||||
PB: TPaintBox;
|
||||
T0: Int64; // PERF
|
||||
begin
|
||||
T0 := PerfNow; // PERF
|
||||
PB := TPaintBox(Sender);
|
||||
PB.Canvas.Draw(0, 0, FSpectrumBitmap);
|
||||
PerfAdd(pzUiPaintSpectrum, T0); // PERF: блит битмапа спектра (Qt6 canvas)
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.PaintWaterfall(Sender: TObject);
|
||||
var T0: Int64; // PERF
|
||||
begin
|
||||
T0 := PerfNow; // PERF
|
||||
FWaterfall.PaintWaterfall(Sender);
|
||||
PerfAdd(pzUiPaintWaterfall, T0); // PERF: блит битмапа водопада (Qt6 canvas)
|
||||
end;
|
||||
|
||||
procedure TSpectrumView.PaintRuler(Sender: TObject);
|
||||
@@ -1177,4 +1297,7 @@ begin
|
||||
FSMeter.PaintSMeterRight(Sender);
|
||||
end;
|
||||
|
||||
initialization
|
||||
GSkipAlphaFix := GetEnvironmentVariable('EWSDR_ALPHA_FIX') = '0';
|
||||
|
||||
end.
|
||||
|
||||
@@ -17,7 +17,8 @@ interface
|
||||
uses
|
||||
Classes, SysUtils, Graphics, Controls, Math, Types,
|
||||
OpenGLContext, GL,
|
||||
AppTheme, SpectrumView, VfoOverlay, BandPlanOverlay, WaterfallViewOpengl;
|
||||
AppTheme, SpectrumView, VfoOverlay, BandPlanOverlay, WaterfallViewOpengl,
|
||||
PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры
|
||||
|
||||
type
|
||||
TGLTextureCache = record
|
||||
@@ -936,10 +937,12 @@ procedure TSpectrumViewOpenGL.PaintSpectrum(Sender: TObject);
|
||||
var
|
||||
W, H: Integer;
|
||||
DBmax, DBmin, InvRange: Double;
|
||||
T0, Tsw: Int64; // PERF
|
||||
begin
|
||||
if Sender is TOpenGLControl then
|
||||
FGLControl := TOpenGLControl(Sender);
|
||||
if (FGLControl = nil) or (not FGLControl.MakeCurrent) then Exit;
|
||||
T0 := PerfNow; // PERF
|
||||
W := FGLControl.Width;
|
||||
H := FGLControl.Height;
|
||||
if (W <= 0) or (H <= 0) then Exit;
|
||||
@@ -971,7 +974,10 @@ begin
|
||||
DrawCachedOverlays(W, H);
|
||||
|
||||
FLastADCVisible := FADCOverloadVisible;
|
||||
Tsw := PerfNow; // PERF
|
||||
FGLControl.SwapBuffers;
|
||||
PerfAdd(pzUiGlSwap, Tsw); // PERF: SwapBuffers (в т.ч. ожидание vsync)
|
||||
PerfAdd(pzUiPaintSpectrum, T0); // PERF: GL-кадр спектра (включая SwapBuffers)
|
||||
end;
|
||||
|
||||
end.
|
||||
|
||||
@@ -26,6 +26,7 @@ interface
|
||||
uses
|
||||
Classes, SysUtils, Math, SyncObjs,
|
||||
BeaconDecoder,
|
||||
PerfLog, // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры
|
||||
WDSP;
|
||||
|
||||
const
|
||||
@@ -1364,10 +1365,12 @@ procedure TWDSPEngine.PushIQItemToDSP(const Item: TIQQueueItem);
|
||||
var
|
||||
i, Pos: Integer;
|
||||
IR, QR: LongInt;
|
||||
T0: Int64; // PERF
|
||||
const
|
||||
SCALE = 1.0 / 8388608.0;
|
||||
begin
|
||||
if FTXActive and not FKeepRXDuringTX then Exit;
|
||||
T0 := PerfNow; // PERF
|
||||
// Post-TX hold: дропаем входящие RX-IQ пакеты пока активно окно мьюта.
|
||||
// Это закрывает ОБА канала сразу — и водопад/спектр (анализатор не
|
||||
// кормится), и аудио (RXA не запускается, FOnAudio не вызывается).
|
||||
@@ -1401,9 +1404,11 @@ begin
|
||||
if FRXAccPos >= FBufSize then
|
||||
ProcessRXBlock;
|
||||
end;
|
||||
PerfAdd(pzDspPushIQ, T0); // PERF: весь путь пакета через DSP-поток
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.FeedDisplaySample(const I, Q: Double);
|
||||
var T0: Int64; // PERF
|
||||
begin
|
||||
if not FAnalyzerOpen then Exit;
|
||||
|
||||
@@ -1413,7 +1418,9 @@ begin
|
||||
|
||||
if FDispPos >= DISPLAY_BLOCK_SIZE then
|
||||
begin
|
||||
T0 := PerfNow; // PERF
|
||||
Spectrum0(1, RX_DISP_ID, 0, 0, @FDispBuf[0]);
|
||||
PerfAdd(pzDspSpectrum0, T0); // PERF: скармливание блока анализатору
|
||||
FDispPos := 0;
|
||||
end;
|
||||
end;
|
||||
@@ -1437,12 +1444,14 @@ procedure TWDSPEngine.ProcessRXBlock;
|
||||
var
|
||||
i: Integer;
|
||||
Err: Integer;
|
||||
T0: Int64; // PERF
|
||||
// OutL/OutR теперь поля класса — не аллоцируем каждый раз
|
||||
begin
|
||||
FRXAccPos := 0;
|
||||
if not FInitialized then Exit;
|
||||
|
||||
try
|
||||
T0 := PerfNow; // PERF
|
||||
// Упаковываем в interleaved буфер для fexchange0
|
||||
for i := 0 to FBufSize - 1 do
|
||||
begin
|
||||
@@ -1453,6 +1462,7 @@ begin
|
||||
// DSP обработка
|
||||
Err := 0;
|
||||
fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err);
|
||||
PerfAdd(pzDspFexMain, T0); // PERF: главный RX-канал WDSP
|
||||
|
||||
// S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter.
|
||||
// GetRXAMeter сбрасывает аккумулятор после вызова, поэтому второй
|
||||
@@ -1474,12 +1484,14 @@ begin
|
||||
and (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX))
|
||||
and (FPostTXMuteUntil = 0) then
|
||||
begin
|
||||
T0 := PerfNow; // PERF
|
||||
for i := 0 to FAudioBufSize - 1 do
|
||||
begin
|
||||
FOutL[i] := FRXOut[i * 2] * FVolume;
|
||||
FOutR[i] := FRXOut[i * 2 + 1] * FVolume;
|
||||
end;
|
||||
FOnAudio(FOutL, FOutR, FAudioBufSize);
|
||||
PerfAdd(pzDspAudioCb, T0); // PERF: маршрут RX-аудио (web/радио/звуковуха)
|
||||
end;
|
||||
|
||||
// Софтовые слайсы: fan-out того же IQ-блока на доп. WDSP-каналы.
|
||||
@@ -1586,8 +1598,10 @@ procedure TWDSPEngine.ProcessSlices;
|
||||
var
|
||||
s, i, k, Err: Integer;
|
||||
V: Double;
|
||||
T0, TS: Int64; // PERF
|
||||
begin
|
||||
if not Assigned(FOnSliceAudio) then Exit;
|
||||
T0 := PerfNow; // PERF
|
||||
FSliceLock.Enter;
|
||||
try
|
||||
for s := 0 to MAX_SLICES - 1 do
|
||||
@@ -1601,7 +1615,9 @@ begin
|
||||
FSliceIn[i * 2 + 1] := FRXAccQ[i];
|
||||
end;
|
||||
Err := 0;
|
||||
TS := PerfNow; // PERF
|
||||
fexchange0(FSlices[s].Chan, @FSliceIn[0], @FSliceOut[0], @Err);
|
||||
PerfAdd(pzDspSliceFex, TS); // PERF: fexchange0 одного слайса
|
||||
// S-метр канала читаем здесь (DSP-поток) — GetRXAMeter сбрасывает аккумулятор.
|
||||
FSlices[s].SMeter := GetRXAMeter(FSlices[s].Chan, RXA_S_AV);
|
||||
V := FSlices[s].Volume;
|
||||
@@ -1615,6 +1631,7 @@ begin
|
||||
finally
|
||||
FSliceLock.Leave;
|
||||
end;
|
||||
PerfAdd(pzDspSlices, T0); // PERF: fan-out всех слайсов целиком
|
||||
end;
|
||||
|
||||
function TWDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi: Integer;
|
||||
@@ -2198,8 +2215,10 @@ var
|
||||
i, tail: Integer;
|
||||
Err: Integer;
|
||||
Avail: Integer;
|
||||
T0: Int64; // PERF
|
||||
begin
|
||||
if not FInitialized then Exit;
|
||||
T0 := PerfNow; // PERF
|
||||
|
||||
Avail := (FTXMicHead - FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1);
|
||||
|
||||
@@ -2235,6 +2254,7 @@ begin
|
||||
// FTXOut содержит FTXOutBufSize IQ пар @ FTXSampleRate (interleaved double)
|
||||
if Assigned(FOnTXIQ) then
|
||||
FOnTXIQ(FTXOut, FTXOutBufSize);
|
||||
PerfAdd(pzDspTxBlock, T0); // PERF: TX-блок целиком (fexchange0 + analyzer + IQ out)
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.PushTXMicSamples16(const Src: array of SmallInt; N: Integer);
|
||||
@@ -2532,8 +2552,10 @@ var
|
||||
Flag: Integer;
|
||||
i, N: Integer;
|
||||
DispID: Integer;
|
||||
T0: Int64; // PERF
|
||||
begin
|
||||
if not FInitialized or not FAnalyzerOpen then Exit;
|
||||
T0 := PerfNow; // PERF
|
||||
N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS);
|
||||
DispID := FActiveDisplayID; // RX_DISP_ID или TX_DISP_ID, зависит от FTXActive
|
||||
|
||||
@@ -2556,6 +2578,7 @@ begin
|
||||
if Assigned(FOnWaterfall) then
|
||||
FOnWaterfall(FWaterfallPixels, N);
|
||||
end;
|
||||
PerfAdd(pzDispGetPixels, T0); // PERF: GetPixels + колбэки спектр/водопад
|
||||
end;
|
||||
|
||||
procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single;
|
||||
|
||||
@@ -16,7 +16,8 @@ interface
|
||||
uses
|
||||
Classes, SysUtils, Graphics, Controls, Math,
|
||||
OpenGLContext, GL,
|
||||
AppTheme, WaterfallView;
|
||||
AppTheme, WaterfallView,
|
||||
PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры
|
||||
|
||||
type
|
||||
TWaterfallViewOpenGL = class(TWaterfallView)
|
||||
@@ -461,9 +462,11 @@ procedure TWaterfallViewOpenGL.PaintWaterfall(Sender: TObject);
|
||||
var
|
||||
W, H: Integer;
|
||||
R, G, B: GLFloat;
|
||||
T0, Tsw: Int64; // PERF
|
||||
begin
|
||||
if Sender is TOpenGLControl then FGLControl := TOpenGLControl(Sender);
|
||||
if (FGLControl = nil) or (not FGLControl.MakeCurrent) then Exit;
|
||||
T0 := PerfNow; // PERF
|
||||
W := FGLControl.Width;
|
||||
H := FGLControl.Height;
|
||||
if (W <= 0) or (H <= 0) then Exit;
|
||||
@@ -491,7 +494,10 @@ begin
|
||||
end;
|
||||
DrawTextureWrapped(W, H);
|
||||
DrawMarker(W, H);
|
||||
Tsw := PerfNow; // PERF
|
||||
FGLControl.SwapBuffers;
|
||||
PerfAdd(pzUiGlSwap, Tsw); // PERF: SwapBuffers (в т.ч. ожидание vsync)
|
||||
PerfAdd(pzUiPaintWaterfall, T0); // PERF: GL-кадр водопада (включая SwapBuffers)
|
||||
end;
|
||||
|
||||
procedure TWaterfallViewOpenGL.SetTheme(const T: TAppTheme);
|
||||
|
||||
+5
-1
@@ -18,7 +18,8 @@ interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, Math, Graphics, Controls, ExtCtrls,
|
||||
OpenGLContext, GL, AppTheme, WDSP;
|
||||
OpenGLContext, GL, AppTheme, WDSP,
|
||||
PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры
|
||||
|
||||
type
|
||||
TWidebandView = class
|
||||
@@ -1120,11 +1121,14 @@ begin
|
||||
end;
|
||||
|
||||
procedure TWidebandView.Paint(Sender: TObject);
|
||||
var T0: Int64; // PERF
|
||||
begin
|
||||
T0 := PerfNow; // PERF
|
||||
if Sender is TOpenGLControl then
|
||||
PaintGL(TOpenGLControl(Sender))
|
||||
else if Sender is TPaintBox then
|
||||
PaintCPU(TPaintBox(Sender).Canvas);
|
||||
PerfAdd(pzUiWidebandPaint, T0); // PERF: кадр wideband-панорамы (CPU или GL)
|
||||
end;
|
||||
|
||||
procedure TWidebandView.PaintRuler(Sender: TObject);
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# CPU-профилирование (ветка perf/cpu-profiling)
|
||||
|
||||
Временная инструментация для поиска самых дорогих по CPU мест.
|
||||
Модуль `PerfLog.pas` + зонные замеры, помеченные комментарием `// PERF`.
|
||||
**Не мержить в main** — после выводов ветка выбрасывается или чистится.
|
||||
|
||||
## Как запустить
|
||||
|
||||
```sh
|
||||
lazbuild -B --ws=qt6 ewsdr.lpr
|
||||
./bin/x86_64-linux/ewsdr-debug # отчёт в stderr каждые 5 с
|
||||
EWSDR_PERF_FILE=/tmp/ewsdr-perf.log ./bin/x86_64-linux/ewsdr-debug # + в файл
|
||||
EWSDR_PERF=0 ./bin/x86_64-linux/ewsdr-debug # выключить замеры
|
||||
```
|
||||
|
||||
## Как читать отчёт
|
||||
|
||||
```
|
||||
[PERF] ===== interval 5.00 s =====
|
||||
[PERF] zone calls c/s avg_us max_us total_ms core%
|
||||
[PERF] dsp.push_iq 2929 586 55.1 412 161.4 3.2
|
||||
```
|
||||
|
||||
`core%` — доля одного ядра CPU, потраченная в зоне за интервал. Это главная
|
||||
колонка: сортируем по ней и смотрим, куда уходит время. `max_us` показывает
|
||||
выбросы (джиттер), `avg_us` — стоимость одного вызова.
|
||||
|
||||
Зоны вложены: `dsp.push_iq` включает `dsp.spectrum0` + `dsp.fex_main` +
|
||||
`dsp.audio_cb` + `dsp.slices`; `dsp.slices` включает `dsp.slice_fex` и
|
||||
`audio.write` слайсов; `ui.spectrum_tick` включает `ui.draw_*`, `ui.slice_flags`,
|
||||
`ui.vfo_overlay`, `ui.web_push`; `dsp.tx_block` включает `pluto.tx_resamp`
|
||||
(SendDUCIQ зовётся из FOnTXIQ). Разница «родитель минус дети» = ненаблюдаемый
|
||||
остаток (циклы копирования, мелочь).
|
||||
|
||||
Бэкенды: на openHPSDR работает `net.*`, на Pluto вместо них `pluto.*`
|
||||
(RX-поток Pluto конвертит iio-буфер и кладёт в ту же DSP-очередь).
|
||||
|
||||
## Карта зон
|
||||
|
||||
| Зона | Поток | Что меряет |
|
||||
|---|---|---|
|
||||
| net.ddc_iq | receive | копия IQ-пакета + постановка в DSP-очередь (openHPSDR) |
|
||||
| net.wideband | receive | сборка wideband-кадра (openHPSDR) |
|
||||
| pluto.rx_conv | Pluto RX | конверсия iio-буфера int16→24bit BE + отдача в DSP-очередь |
|
||||
| pluto.tx_resamp | TX DSP | полифазный интерполятор 192k→device-rate + 24→16 + FIFO |
|
||||
| pluto.tx_fill | Pluto TX | наполнение iio-буфера из FIFO (без блокирующего push) |
|
||||
| dsp.push_iq | DSP | весь путь пакета: декод 24-бит, аккумуляция, всё ниже |
|
||||
| dsp.spectrum0 | DSP | скармливание display-блока анализатору WDSP |
|
||||
| dsp.fex_main | DSP | fexchange0 главного RX-канала |
|
||||
| dsp.audio_cb | DSP | маршрут RX-аудио (web/радио/звуковуха) |
|
||||
| dsp.slices | DSP | fan-out всех слайсов целиком |
|
||||
| dsp.slice_fex | DSP | fexchange0 одного слайс-канала |
|
||||
| dsp.tx_block | TX DSP | TX-блок целиком |
|
||||
| disp.get_pixels | display | GetPixels спектр+водопад |
|
||||
| audio.write | DSP | запись в PortAudio ring (главный + все слайсы) |
|
||||
| ui.spectrum_tick | UI | кадровый таймер целиком |
|
||||
| ui.meter_tick | UI | 10 Гц метр-таймер |
|
||||
| ui.draw_spectrum / ui.draw_waterfall | UI | CPU-рендер кадра в битмап |
|
||||
| ui.paint_spectrum / ui.paint_waterfall | UI | блит на канву (Qt6) или GL-кадр со SwapBuffers |
|
||||
| ui.slice_flags | UI | позиционирование + S-метры слайс-флагов |
|
||||
| ui.vfo_overlay | UI | обновление главного VFO-флага |
|
||||
| ui.wideband_paint | UI | кадр wideband-панорамы |
|
||||
| ui.web_push | UI | WebAdapter.PushState |
|
||||
| ui.spec_grid…spec_alpha | UI | разбивка ui.draw_spectrum ПО ФАЗАМ (после перестройки): grid=ребилд+memcpy сетки, bands=raw-заливки полос, curve=точки+градиент, overlays=canvas-фаза (линии/текст/кривая/маркеры), alpha=композиты флагов+альфа-OR |
|
||||
| ui.gl_swap | UI | SwapBuffers отдельно — включает ожидание vsync, НЕ чистый CPU |
|
||||
|
||||
## Чего зоны НЕ видят
|
||||
|
||||
- Общесистемное: Qt event loop, композитор, PortAudio callback-поток
|
||||
(сам колбэк не инструментирован — он читает ring без работы).
|
||||
- Для сравнения с общим CPU процесса: `pidstat -p $(pidof ewsdr-debug) 5 -t`
|
||||
(покажет и раскладку по потокам — сопоставляется с зонами по потокам).
|
||||
|
||||
## Методика измерений
|
||||
|
||||
Снять отчёты в фиксированных сценариях по ~30 с каждый и сравнить core%:
|
||||
|
||||
1. RX, 1 слайс (только VFO A), спектр CPU-рендер.
|
||||
2. То же + OpenGL (`ewsdr-debug --opengl` или как включается GL-свитч).
|
||||
3. RX + 3–4 слайса (Ctrl+ЛКМ) — рост dsp.slices / dsp.slice_fex / audio.write.
|
||||
4. Разные FPS дисплея и размеры FFT.
|
||||
5. Открытый web-клиент (ui.web_push, dsp.audio_cb с Opus).
|
||||
6. Wideband включён/выключен (openHPSDR).
|
||||
7. Pluto: разные sample rate (576k/1536k/3072k) — pluto.rx_conv и dsp.push_iq
|
||||
растут линейно с рейтом; TX (MOX/TUN) — pluto.tx_resamp vs dsp.tx_block.
|
||||
@@ -17,9 +17,9 @@
|
||||
<UseVersionInfo Value="True"/>
|
||||
<AutoIncrementBuild Value="True"/>
|
||||
<MinorVersionNr Value="9"/>
|
||||
<BuildNr Value="232"/>
|
||||
<BuildNr Value="239"/>
|
||||
</VersionInfo>
|
||||
<MacroValues Count="50">
|
||||
<MacroValues Count="57">
|
||||
<Macro1 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro2 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro3 Name="LCLWidgetType" Value="qt6"/>
|
||||
@@ -70,6 +70,13 @@
|
||||
<Macro48 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro49 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro50 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro51 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro52 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro53 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro54 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro55 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro56 Name="LCLWidgetType" Value="qt6"/>
|
||||
<Macro57 Name="LCLWidgetType" Value="qt6"/>
|
||||
</MacroValues>
|
||||
<BuildModes>
|
||||
<Item Name="Debug" Default="True"/>
|
||||
|
||||
Reference in New Issue
Block a user