mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
Посреди передачи из MSHV на водопаде появлялись всплески своего сигнала. Цепочка: очередь DUC пустеет дольше подушки отправителя (DUC_FIFO_THROTTLE = 2000 отсчётов = 10.4 мс) → FIFO радио сохнет → модуляция обрывается → в эфире остаётся голая несущая на частоте гетеродина DUC, в стороне от тона ровно на звуковой сдвиг. Доказано pcap-съёмом: шесть всплесков в дампе — ровно столько, сколько видел оператор, и каждый стоит за паузой 10.3-23.5 мс, а паузы 8 мс и короче не дали ни одного. Виноват не клиент и не блокировка UI, а зернистость НАШЕГО запроса. Слой первый: PushTxChrono жил на общем тике сервера 20 мс, а просил блок клиента целиком (2048 отсчётов = 42.7 мс) — маркер выходил через два или три тика, то есть через 40 или 60 мс. Слой второй: MSHV отвечает пачками по 4-5 блоков раз в ~44 мс (STREAM_C = 4096 при 96 кГц), и мелкий квант этого не лечит — нужен запас не меньше пачки. Сделано: * квант запроса = один блок TXA (512 отсчётов движка), а не блок клиента; * свой поток-планировщик TxTickLoop с АБСОЛЮТНЫМИ дедлайнами (опоздание одного пробуждения не сдвигает сетку); общий тик маркеров больше не шлёт; * бухгалтерия Owed/InFlight в кадрах на канал, гасится по k до интерполятора; потолок долга обязан быть выше окна в полёте (TCI_TX_OWED_HEADROOM_Q), иначе связывающим становится он и подача падает до 58% реального времени при полностью исправном клиенте; * SendBinNow: маркеры пишутся в сокет напрямую под FWriteLock, минуя очередь (та выпускается лишь на пробуждении потока клиента, TCI_POLL_MS = 20 мс — вдвое больше кванта, и подача снова рвалась); * FReapLock: планировщик TX — новый поток, а правило «клиента освобождает только тик-поток» держалось на том, что им же он и пользуется; * аванс под зернистость клиента: измеряется по ПЕРИОДУ между пачками (размер пачки зависит от того, сколько мы запросили ⇒ положительная обратная связь), переживает конец передачи, умеет уменьшаться по выдержке TCI_TX_LEAD_DOWN_MS, потолок TCI_TX_LEAD_MAX_MS; * старт передачи: KickTxTick будит планировщика на фронте PTT, TxPreWarm шлёт один маркер ДО SetMOX (41 мс раздумий клиента накладываются на нашу же подготовку тракта) под гейтом «передатчик свободен и чужого источника нет», PrimeDUCIQ для источника TCI растянут до Max(6096, аванс×4) — путь микрофона радио, CW и web не затронут; * посев аванса TCI_TX_LEAD_DEF_MS = 50 мс, пока про клиента ничего не известно: обучение к первому осушению физически не успевает. Монотонные часы одного источника для всех потоков — PlatformUtils.MonotonicUs (абсолютные дедлайны не терпят часов, способных прыгнуть от NTP). На железе: опасных осушений посреди передачи НОЛЬ (было 12 за 11 с), четыре передачи из пяти вообще без единого, включая старт; прогон 15:21 чист везде, в том числе на первой передаче после подключения. Всплесков оператор больше не видит. Приборы: TxTrace.pas (EWSDR_TXTRACE=1) и стенд test/hpsdr — кольцевой tcpdump capture.sh, разбор дампа pcap_tx_scan.py, разбор трассы txtrace_scan.py. Стенд test/tci: часть F «Пейсинг TX», 260 проверок, провалов нет; живой клиент с рампой, RTT и потерями — test/tci/tx_chrono_bench.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gmVQnna1i4EbSZ2VGm6KD
172 lines
5.3 KiB
ObjectPascal
172 lines
5.3 KiB
ObjectPascal
unit PlatformUtils;
|
||
|
||
{
|
||
PlatformUtils.pas - small helpers for desktop/runtime platform checks.
|
||
}
|
||
|
||
{$IFDEF FPC}
|
||
{$MODE Delphi}
|
||
{$ENDIF}
|
||
|
||
interface
|
||
|
||
{$IFNDEF HEADLESS}
|
||
uses
|
||
Controls;
|
||
{$ENDIF}
|
||
|
||
function HasOpenGLSpectrumSwitch: Boolean;
|
||
function CurrentScreenDPI: Integer;
|
||
// Целочисленный масштаб канвы экрана: 2 на Retina, 1 везде остальном.
|
||
// Windows/Linux всегда получают 1 ⇒ умножение на него ничего не меняет.
|
||
function GetScreenScale: Integer;
|
||
// То же, но для канвы конкретного контрола: на macOS масштаб берётся у окна,
|
||
// которому контрол принадлежит, а не у главного экрана. Это то самое число, в
|
||
// котором Cocoa рисует эту канву, и оно меняется, когда окно перетаскивают на
|
||
// монитор с другим масштабом. Так и надо считать размер offscreen-битмапа.
|
||
{$IFNDEF HEADLESS}
|
||
function GetControlScale(AControl: TControl): Integer;
|
||
{$ENDIF}
|
||
// Монотонные микросекунды одного источника для всех потоков. Нужны там, где
|
||
// интервалы считает не человек, а код: пейсинг запросов TX-аудио по TCI ведёт
|
||
// абсолютные дедлайны, и часы, способные прыгнуть от NTP, там не годятся.
|
||
function MonotonicUs: Int64;
|
||
|
||
// Возвращает каталог для хранения конфигурации (с завершающим разделителем).
|
||
// macOS: ~/Library/Application Support/ewsdr/
|
||
// Linux: ~/.config/ewsdr/
|
||
// Windows: %APPDATA%\ewsdr\
|
||
function GetAppCfgDir: string;
|
||
|
||
implementation
|
||
|
||
uses
|
||
SysUtils {$IFNDEF HEADLESS}, Forms{$ENDIF}
|
||
{$IFDEF WINDOWS}, Windows{$ENDIF}
|
||
{$IFDEF LINUX}, Linux, UnixType{$ENDIF}
|
||
{$IF DEFINED(UNIX) AND NOT DEFINED(LINUX)}, BaseUnix{$ENDIF}
|
||
{$IF DEFINED(DARWIN) AND NOT DEFINED(HEADLESS)}, MacScale{$ENDIF};
|
||
|
||
{$IFDEF WINDOWS}
|
||
var
|
||
QPCFreq: Int64 = 0;
|
||
{$ENDIF}
|
||
|
||
function MonotonicUs: Int64;
|
||
{$IFDEF WINDOWS}
|
||
var
|
||
V: Int64;
|
||
begin
|
||
if QPCFreq <= 0 then
|
||
if not QueryPerformanceFrequency(QPCFreq) then Exit(0);
|
||
QueryPerformanceCounter(V);
|
||
Result := (V * 1000000) div QPCFreq;
|
||
end;
|
||
{$ELSE}
|
||
{$IFDEF LINUX}
|
||
var
|
||
TS: TTimeSpec;
|
||
begin
|
||
if clock_gettime(CLOCK_MONOTONIC, @TS) <> 0 then Exit(0);
|
||
Result := Int64(TS.tv_sec) * 1000000 + TS.tv_nsec div 1000;
|
||
end;
|
||
{$ELSE}
|
||
var
|
||
TV: TTimeVal;
|
||
begin
|
||
fpgettimeofday(@TV, nil);
|
||
Result := Int64(TV.tv_sec) * 1000000 + TV.tv_usec;
|
||
end;
|
||
{$ENDIF}
|
||
{$ENDIF}
|
||
|
||
function HasOpenGLSpectrumSwitch: Boolean;
|
||
var
|
||
I: Integer;
|
||
S: string;
|
||
begin
|
||
Result := False;
|
||
for I := 1 to ParamCount do
|
||
begin
|
||
S := LowerCase(ParamStr(I));
|
||
if (S = '--opengl') or (S = '-opengl') or (S = '/opengl') then
|
||
Exit(True);
|
||
end;
|
||
// Бандл под LaunchServices не может передать argv, поэтому тот же
|
||
// переключатель читается из окружения (Info.plist -> LSEnvironment).
|
||
S := LowerCase(Trim(GetEnvironmentVariable('EWSDR_OPENGL')));
|
||
if (S = '1') or (S = 'true') or (S = 'yes') or (S = 'on') then
|
||
Exit(True);
|
||
end;
|
||
|
||
function GetScreenScale: Integer;
|
||
begin
|
||
{$IF DEFINED(DARWIN) AND NOT DEFINED(HEADLESS)}
|
||
// Cocoa рисует канву в физических пикселях, а TBitmap всегда 1x. Кто рисует
|
||
// через offscreen-битмап, должен растить его в GetScreenScale раз.
|
||
Result := MacBackingScale;
|
||
{$ELSE}
|
||
Result := 1;
|
||
{$ENDIF}
|
||
end;
|
||
|
||
{$IFNDEF HEADLESS}
|
||
function GetControlScale(AControl: TControl): Integer;
|
||
{$IF DEFINED(DARWIN)}
|
||
var
|
||
Host: TWinControl;
|
||
begin
|
||
// У TPaintBox и прочих TGraphicControl своего NSView нет — рисуют они на
|
||
// канве ближайшего родителя с хендлом, его окно и спрашиваем.
|
||
if AControl is TWinControl then
|
||
Host := TWinControl(AControl)
|
||
else if AControl <> nil then
|
||
Host := AControl.Parent
|
||
else
|
||
Host := nil;
|
||
while (Host <> nil) and not Host.HandleAllocated do
|
||
Host := Host.Parent;
|
||
|
||
if Host <> nil then
|
||
Result := MacBackingScaleForView(Pointer(Host.Handle))
|
||
else
|
||
Result := 0;
|
||
|
||
// Контрола ещё нет на экране: до первой отрисовки сгодится главный экран.
|
||
if Result < 1 then
|
||
Result := GetScreenScale;
|
||
{$ELSE}
|
||
begin
|
||
Result := 1;
|
||
{$ENDIF}
|
||
end;
|
||
{$ENDIF}
|
||
|
||
function CurrentScreenDPI: Integer;
|
||
begin
|
||
{$IFNDEF HEADLESS}
|
||
Result := Screen.PixelsPerInch;
|
||
if Result <= 0 then Result := 96;
|
||
{$ELSE}
|
||
Result := 96; // headless-демон без LCL: экрана нет, дефолт DPI
|
||
{$ENDIF}
|
||
end;
|
||
|
||
function GetAppCfgDir: string;
|
||
begin
|
||
{$IFDEF WINDOWS}
|
||
Result := GetEnvironmentVariable('APPDATA');
|
||
if Result = '' then
|
||
Result := GetEnvironmentVariable('LOCALAPPDATA');
|
||
if Result = '' then
|
||
Result := ExtractFilePath(ParamStr(0));
|
||
Result := IncludeTrailingPathDelimiter(Result) + 'ewsdr' + PathDelim;
|
||
{$ELSE}
|
||
Result := IncludeTrailingPathDelimiter(GetAppConfigDir(False));
|
||
{$ENDIF}
|
||
if not DirectoryExists(Result) then
|
||
ForceDirectories(Result);
|
||
end;
|
||
|
||
end.
|