mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
Модуль Windows стоит в uses ПОСЛЕ SysUtils, поэтому его трёхпараметрический GetEnvironmentVariable(PChar;PChar;DWORD) перекрывает однопараметрический из SysUtils, и три вызова в PlatformUtils падали с «wrong number of parameters». Лечение — явная квалификация SysUtils.GetEnvironmentVariable. ★Сломалось это не сейчас: unit Windows появился в uses ещё вd209697(вместе с QueryPerformanceCounter для монотонных часов), и с тех пор под win64 не собиралось вовсе — просто сборочная машина туда не заходила. GetAppCfgDir с %APPDATA% живёт сe9f4bf1и доd209697работал. Новый стенд test/platform. Кросс-RTL обычно не установлен, поэтому ветки {$IFDEF WINDOWS} и {$IFDEF DARWIN} на Linux не компилируются ВООБЩЕ, и ошибка в них всплывает только на сборочной машине. Стенд переписывает копию PlatformUtils.pas так, чтобы платформенные условия читались как свои (-dSIMWIN / -dSIMMAC), и компилирует без линковки (-Cn): системных функций тут нет, но синтаксис, типы и — главное — разрешение имён проверяются по-настоящему. Для Windows подкладывается заглушка unit Windows, объявляющая ровно те имена, которыми настоящий перекрывает SysUtils; на ней и держится вся проверка. ★Негативный контроль: на коде до правки стенд выдаёт РОВНО те же три ошибки (строки 210, 271, 273), что и сборочная машина. Чего он не делает: живых вызовов системных счётчиков — их правильность доказывает только прогон на самой платформе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
301 lines
13 KiB
ObjectPascal
301 lines
13 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;
|
||
|
||
// Тики счётчика → микросекунды, без переполнения. ★Наивное (Ticks * 1000000)
|
||
// div Freq переполняет Int64, когда Ticks переваливает за 2^63/10^6 = 9.223e12:
|
||
// при типовой для Windows 8+ частоте QPC 10 МГц это 10.7 суток аптайма, дальше
|
||
// разрыв повторяется каждые 21.35 суток (2^64/10^6 тиков). В момент разрыва
|
||
// время прыгает в большой минус, и код на абсолютных дедлайнах встаёт намертво:
|
||
// планировщик TX_CHRONO перестаёт слать маркеры, пейсинг отправителя DUC
|
||
// выпускает пакеты без выдержки. Считаем через частное и остаток: остаток
|
||
// меньше частоты, поэтому второе слагаемое не переполняется, а первое упирается
|
||
// в потолок Int64 через сотни тысяч лет. Функция вынесена в интерфейс, чтобы
|
||
// стенд мог проверить её на граничных значениях там, где Windows нет.
|
||
function TicksToUs(Ticks, Freq: Int64): 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
|
||
// ★ОДНО слово на всё состояние часов, и вот почему. Публикуется оно обычной
|
||
// записью, без барьера, а читают его все потоки. Пара «значение + флаг
|
||
// готовности» здесь недопустима: на слабой модели памяти (Windows ARM)
|
||
// читатель вправе увидеть выставленный флаг раньше самого значения, получить
|
||
// частоту 0 и вернуть время 0 — единичный НОЛЬ в монотонных часах, то есть
|
||
// прыжок на десятки лет назад в бухгалтерии TX. С одним словом такого
|
||
// расхождения не бывает: выровненная запись Int64 атомарна, а читатель
|
||
// снимает её РОВНО ОДИН РАЗ в локальную переменную.
|
||
// 0 — источник ещё не выбран;
|
||
// >0 — частота QPC;
|
||
// -1 — QPC непригоден, считаем по GetTickCount64 (выбор защёлкнут навсегда:
|
||
// смена источника на ходу — это тот же скачок времени).
|
||
QPCFreq: Int64 = 0;
|
||
|
||
procedure InitWinClock;
|
||
// Идемпотентна. Зовётся из initialization — то есть ДО старта потоков.
|
||
var F: Int64;
|
||
begin
|
||
if QPCFreq <> 0 then Exit;
|
||
if QueryPerformanceFrequency(F) and (F > 0) then
|
||
QPCFreq := F
|
||
else
|
||
// Счётчика нет — часы обязаны хотя бы ИДТИ: замершее время для кода на
|
||
// абсолютных дедлайнах означает мёртвую передачу, а не потерю точности.
|
||
QPCFreq := -1;
|
||
end;
|
||
{$ENDIF}
|
||
|
||
function TicksToUs(Ticks, Freq: Int64): Int64;
|
||
begin
|
||
if Freq <= 0 then Exit(0);
|
||
Result := (Ticks div Freq) * 1000000 + ((Ticks mod Freq) * 1000000) div Freq;
|
||
end;
|
||
|
||
{$IFDEF DARWIN}
|
||
// ★macOS: gettimeofday — СТЕННЫЕ часы, шаг NTP или правка руками уводят их
|
||
// вперёд и назад. Для абсолютных дедлайнов это негодный источник: назад —
|
||
// планировщик замирает до недостижимого срока, вперёд — выдаёт пачку и
|
||
// начисляет фиктивный долг. mach_absolute_time монотонен и есть на любой
|
||
// версии системы (в отличие от clock_gettime, появившегося в 10.12).
|
||
type
|
||
TMachTimebase = record
|
||
Numer: LongWord;
|
||
Denom: LongWord;
|
||
end;
|
||
|
||
function mach_absolute_time: QWord; cdecl; external 'c' name 'mach_absolute_time';
|
||
function mach_timebase_info(var Info: TMachTimebase): Integer; cdecl;
|
||
external 'c' name 'mach_timebase_info';
|
||
|
||
var
|
||
// ★Тоже ОДНО слово: numer в старшей половине, denom в младшей. Раздельные
|
||
// numer/denom дали бы ту же гонку публикации, что и на Windows (Apple Silicon
|
||
// — слабая модель памяти), а делить на прочитанный ноль нельзя вовсе.
|
||
MachBase: Int64 = 0;
|
||
|
||
procedure InitMachClock;
|
||
// Идемпотентна. Зовётся из initialization — то есть ДО старта потоков.
|
||
var
|
||
Info: TMachTimebase;
|
||
begin
|
||
if MachBase <> 0 then Exit;
|
||
Info.Numer := 0;
|
||
Info.Denom := 0;
|
||
if (mach_timebase_info(Info) <> 0) or (Info.Denom = 0) or (Info.Numer = 0) then
|
||
begin
|
||
// Тайминг-базы нет (быть такого не должно) — считаем тики наносекундами,
|
||
// как на Intel, где numer = denom = 1.
|
||
Info.Numer := 1;
|
||
Info.Denom := 1;
|
||
end;
|
||
MachBase := (Int64(Info.Numer) shl 32) or Int64(Info.Denom);
|
||
end;
|
||
{$ENDIF}
|
||
|
||
function MonotonicUs: Int64;
|
||
{$IFDEF WINDOWS}
|
||
var
|
||
V, F: Int64;
|
||
begin
|
||
F := QPCFreq; // снимок ОДНОГО слова: см. объявление
|
||
if F = 0 then
|
||
begin
|
||
InitWinClock; // страховка: сюда доходит только вызов раньше
|
||
F := QPCFreq; // initialization, чего в норме не бывает
|
||
end;
|
||
if F < 0 then Exit(Int64(GetTickCount64) * 1000);
|
||
QueryPerformanceCounter(V);
|
||
Result := TicksToUs(V, F);
|
||
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}
|
||
{$IFDEF DARWIN}
|
||
var
|
||
T, Ns, MachNumer, MachDenom: QWord;
|
||
B: Int64;
|
||
begin
|
||
B := MachBase; // снимок ОДНОГО слова: см. объявление
|
||
if B = 0 then
|
||
begin
|
||
InitMachClock; // страховка: сюда доходит только вызов раньше
|
||
B := MachBase; // initialization, чего в норме не бывает
|
||
end;
|
||
MachNumer := QWord(B) shr 32;
|
||
MachDenom := QWord(B) and $FFFFFFFF;
|
||
if (MachNumer = 0) or (MachDenom = 0) then Exit(0);
|
||
T := mach_absolute_time;
|
||
// Через частное и остаток по той же причине, что и на Windows: на Apple
|
||
// Silicon numer/denom = 125/3, и произведение тиков на 125 растёт быстро.
|
||
Ns := (T div MachDenom) * MachNumer + ((T mod MachDenom) * MachNumer) div MachDenom;
|
||
Result := Int64(Ns div 1000);
|
||
end;
|
||
{$ELSE}
|
||
var
|
||
TV: TTimeVal;
|
||
begin
|
||
// Прочие Unix: монотонного источника под рукой нет, остаются стенные часы.
|
||
// ★Знать об этом обязан тот, кто ведёт абсолютные дедлайны (см. TCIServer).
|
||
fpgettimeofday(@TV, nil);
|
||
Result := Int64(TV.tv_sec) * 1000000 + TV.tv_usec;
|
||
end;
|
||
{$ENDIF}
|
||
{$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).
|
||
// ★Явная квалификация: на Windows в uses стоит unit Windows, и его
|
||
// GetEnvironmentVariable(PChar;PChar;DWORD) перекрывает однопараметрический
|
||
// из SysUtils — сборка под win64 падала на «wrong number of parameters».
|
||
S := LowerCase(Trim(SysUtils.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}
|
||
// Квалификация обязательна — см. HasOpenGLSpectrumSwitch.
|
||
Result := SysUtils.GetEnvironmentVariable('APPDATA');
|
||
if Result = '' then
|
||
Result := SysUtils.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;
|
||
|
||
// ★Часы инициализируем ЗДЕСЬ, до старта любых потоков: ленивая инициализация
|
||
// из рабочего потока публиковала бы состояние обычной записью, а на слабой
|
||
// модели памяти сосед вправе увидеть её частично. Одного нуля в монотонных
|
||
// часах хватает, чтобы бухгалтерия TX получила скачок времени.
|
||
initialization
|
||
{$IFDEF WINDOWS}
|
||
InitWinClock;
|
||
{$ENDIF}
|
||
{$IFDEF DARWIN}
|
||
InitMachClock;
|
||
{$ENDIF}
|
||
|
||
end.
|