Кросс-компиляция «Hello, World» под ARM

Я всю жизнь разрабатываю под x86 (за исключением экспериментов с MIPS-I/PSX, но это больше из исторического интереса), и мне всегда хотелось написать что-нибудь на D под вторую главную архитектуру современности — AArch64. Недавно я досконально изучил, как это правильно делать, причем самым, на мой вкус, безболезненным способом, который не требует WSL, Docker и монструозных тулчейнов — понадобится только обычный свежий LDC плюс несколько дополнительных бинарей, которые легко скачать в готовом виде.

Итак, программа (src/main.d):

module main;

extern(C) nothrow @nogc
{
    int printf(const(char)* fmt, ...);
}

extern(C) int main(int argc, char** argv) nothrow @nogc
{
    printf("Hello, World!\n");
    return 0;
}

Для начала нужно убедиться, что ваша версия LDC собрана с поддержкой aarch64: ldc2 --version.

Важный вопрос: в каком режиме компилировать — BetterC или нет? По моему мнению, на большинстве популярных платформ BetterC дает ноль преимуществ, отнимая важные фичи языка вроде классов. Он хорош для микроконтроллеров, для WASM, для всякой экзотики, но точно не для привычного GNU userspace, который наличествует на любой железке мощнее калькулятора. Так что я предлагаю сразу идти нормальным путем и заменить те функции druntime, которые вам нужны, самописными реализациями. Для простейшего примера, впрочем, это не требуется.

Моя команда сборки выглядит следующим образом:

ldc2 -Isrc -i --vgc
    -defaultlib= ^
    -debuglib= ^
    -mtriple=aarch64-linux-gnu ^
    -linker=lld ^
    -Xcc=--sysroot=sysroot/aarch64-linux-gnu/libc ^
    -Xcc=--gcc-toolchain=sysroot ^
    -Xcc=-unwindlib=none ^
    src/main.d ^
    -of=test

Она предполагает, что в папке sysroot находится минимальное системное окружение, необходимое для компиляции linux-приложений. Я его взял отсюда: https://github.com/ClickHouse/sysroot. Заберите папку linux-aarch64 из репозитория со всем содержимым и переименуйте в sysroot. Это же окружение понадобится для тестирования.

При желании можно оформить то же самое для DUB:

{
    "name": "test",

    "targetType": "executable",
    "targetName": "test",

    "sourcePaths": ["src"],
    "importPaths": ["src"],

    "dflags-ldc": [
        "--vgc",
        "-defaultlib=",
        "-debuglib=",
        "-mtriple=aarch64-linux-gnu",
        "-linker=lld",
        "-Xcc=--sysroot=sysroot/aarch64-linux-gnu/libc",
        "-Xcc=--gcc-toolchain=sysroot",
        "-Xcc=-unwindlib=none"
    ],
    "postBuildCommands-windows": [
        "copy /Y test.exe C:\\Users\\gecko\\Documents\\vbshared\\arm\\test",
        "del test.exe"
    ]
}

Последнее нужно под Windows, чтобы на выходе получился бинарник без расширения «.exe».

Если все хорошо, то вы получите файл test, который можно сразу проверить:

file ./test

Команда выведет что-то вроде

./test: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV),
dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for
GNU/Linux 3.7.0, with debug_info, not stripped

Если есть ARM-компьютер, то можно запустить приложение на нем (я тестировал на Khadas VIM2), но для тестов гораздо практичнее настроить виртуальное окружение. Если у вас Linux, то рекомендую https://github.com/multiarch/qemu-user-static — нужно скачать qemu-aarch64-static на странице релизов. Это эмулятор пользовательского режима QEMU, который позволяет запускать бинарники AArch64 непосредственно на хост-системе.

Использую я его так:

./qemu-aarch64-static -L ./sysroot/aarch64-linux-gnu/libc/ ./test

Параметр -L передает эмулятору путь к корневой папке окружения, там должны быть динамический линкер /lib/ld-linux-aarch64.so.1 и папка lib64 с библиотеками C/POSIX — проверьте. Важно убедиться, что вместо симлинков не лежат пустые файлы, под Windows часто так получается.

Для Windows я, к сожалению, не нашел эмулятор юзерспейса linux-aarch64 (может, его и нет вовсе). Ручной запуск ARM-машины QEMU — это жуткий геморрой. Самый беспроблемный способ, который не заставляет постоянно запускать и останавливать тяжелые сессии эмуляции (равно как и вводить много команд вручную) — тупо установить обычный десктопный Linux в VirtualBox и там гонять qemu-aarch64-static, не выключая систему. Это немного костыльно, но жить можно, особенно если держать весь проект сразу в shared-папке хоста. Мне лично это удобно тем, что я и так использую Linux на виртуалке для других тестов.

LDC поддерживает SPIR-V!

Не секрет, что компилятор LDC, благодаря бэкенду на LLVM, поддерживает множество разных целевых платформ — и в их числе, оказывается, есть SPIR-V. Мне удалось собрать вот такое минимальное вычислительное ядро (которое, правда, ничего не делает):

module main;

import ldc.attributes;

@callingConvention("spir_kernel")
@llvmAttr("hlsl.shader", "compute")
extern(C) void compute_main() nothrow @nogc
{
    //
}
ldc2 -betterC -vgc -c --output-o -of=main.spv --mtriple=spirv-unknown-vulkan -mattr=+spirv1.0 source/main.d

Если вывести ассемблерный листинг, получается следующее:

OpCapability Shader
OpMemoryModel Logical GLSL450
OpEntryPoint GLCompute %3 "compute_main"
OpSource Unknown 0
OpName %3 "compute_main"
%1 = OpTypeVoid
%2 = OpTypeFunction %1
%3 = OpFunction %1 None %2
%4 = OpLabel
OpReturn
OpFunctionEnd

Как видно, можно использовать этот SPIR-V модуль как вычислительный шейдер в Vulkan и OpenGL 4.6. Поддержки других видов шейдеров пока нет, но кто знает — может быть в один прекрасный день получится использовать D в качестве полноценного шейдерного языка! Я лично в это верю.

Тригонометрия в бенчмарках

«Неважно, что ты любишь больше:
косинус ли, синус ли…»

Тригонометрия — основа многих приложений, от компьютерной графики до научных симуляций. Все мы привыкли вызывать sin и cos, не задумываясь, как они реализованы. А реализации могут быть разные! Работая над математической библиотекой для dlib2, я провел интересное исследование — какая тригонометрия лучше? Конечно, есть функции из std.math, и в большинстве случаев подойдут именно они. Но не все так просто — все зависит от того, что именно вы разрабатываете.

Если вы собираете обычное приложение, то кажется, что беспокоиться не о чем. Но если вам, по тем или иным причинам, нельзя обращаться к Phobos? Тогда есть два основных пути — sin и cos из стандартной библиотеки C, либо кастомная реализация, если код собирается под голое железо (например, при создании ядра ОС или программировании встраиваемой электроники). Но если вы используете LDC, то ничто не мешает использовать интринсики LLVM — они, оказывается, работают быстрее, чем std.math!

Я провел ряд тестов для всех вариантов тригонометрии:

  • Тест на точность — вычисление синуса и косинуса для 200 аргументов от -π до +π. Замерялась максимальная погрешность — расхождение результата с std.math.sin и std.math.cos;
  • Тест на производительность — время вычисления синуса и косинуса 1000000 раз.

Во всех кейсах я использовал LDC 1.39.0 под Windows 10. Получилось следующее:

  • std.math.sin, std.math.cos:
    • Время выполнения: 4 мс
  • LLVM интринсики llvm_sin, llvm_cos:
    • Время выполнения: 2 мс
    • Точность: абсолютная (макс. погрешность для sin: 0, для cos: 0)
  • Функции sin, cos из стандартной библиотеки C:
    • Время выполнения: 21 мс
    • Точность: абсолютная (макс. погрешность для sin: 0, для cos: 0)
  • Моя кастомная реализация на таблицах:
    • Время выполнения: 33 мс
    • Точность: порядка 10-7 (макс. погрешность для sin: 2.97038e-07, для cos: 1.78188e-07)

Также я пробовал версию с ассемблерными вставками, но она получилась почему-то медленнее кастомной — видимо, при использовании инлайнового ассемблера компилятор не задействует какие-то оптимизации (а еще есть мнение, что x87 fsin, fcos на современных процессорах медленные сами по себе). Смысла в таком варианте реализации особо нет, так что я его не стал рассматривать для включения в библиотеку.

В итоге в dlib2 войдут четыре реализации с таким приоритетом:

  • Если используется LDC, то синус и косинус — это интринсики (то есть, кодогенератор сам выбирает оптимальную реализацию под нужную архитектуру);
  • Если используются другие компиляторы (DMD, GDC):
    • Если код компилируется с поддержкой Phobos, то используются функции из std.math;
    • Если код собирается в режиме version(NoPhobos), но не version(FreeStanding) (то есть, под Windows или Unix-подобную ОС), то используются функции рантайма C;
    • Если же идет компиляция в bare metal, то используется кастомная реализация на таблицах.

Итоги 2018 года

Год подошел концу, а это значит, что наступило время для традиционного подведения итогов.

  • 2018-й ознаменовался множеством значительных улучшений в движке Dagon. Самые важные из них включают переход на OpenGL 4.0, HDR, отложенный рендеринг, новую систему шейдеров, поддержку карт окружения, улучшенные частицы, новые эффекты пост-обработки (SSAO, HDR glow, motion blur, хроматическая аберрация, улучшенный color grading), экспорт сцен из Blender. Также была улучшена поддержка джойстиков и реализована поддержка рулей.
  • Я написал цикл уроков по базовым возможностям Dagon и открыл англоязычный блог на Medium.
  • Вышло три релиза коллекции библиотек dlib — 0.13.0, 0.14.0, 0.15.0.
  • Я запустил краудфандинг своих проектов на Patreon и начал продавать 3D-модели на CGTrader.
  • На сайте LightHouse Software вышли две мои статьи по D — «Преобразование карт окружения при помощи dlib», «Музыкальный D: синтезатор в 100 строк».

Ну и, конечно, не могу не назвать самые значимые для меня события в мире CG, СПО и геймдева:

  • Прекращение поддержки OpenGL на macOS.
  • Покупка GitHub корпорацией Microsoft.
  • Открытие исходников PhysX под свободной лицензией.
  • Появление поддержки WebAssembly в компиляторе LDC, включение фронтенда D в GCC, портирование бэкенда DMD на D, отвязывание всех компиляторов D от линкера MSVS при сборке 64-битных приложений. Инструменты развиваются, язык не стоит на месте, и все это, конечно, не может не радовать. 
  • Выход BindBC, нового биндинг-фреймворка от автора Derelict (я стал одним из первых его пользователей).
  • Выход бета-версии Blender 2.80. Очень впечатлили возможности Eevee, но слегка разочаровал отказ от BGE — я-то надеялся, что игровой движок, напротив, будет улучшен и интегрирован с Eevee.
  • Бесплатный релиз движка Armory. Давно и с большим интересом слежу за этим проектом, хотя и не собираюсь отказываться от Dagon в пользу чего-то стороннего.
  • Krita 4.0 и GIMP 2.10.
  • RTX от NVIDIA — трассировка лучей на GPU с игровой производительностью — и, соответственно, выход первых потребительских видеокарт с полной поддержкой этой технологии (линейка GeForce RTX 2080 на базе архитектуры Turing)
  • C&D-письмо автору Spyro: Myths Awaken. Нападки корпораций на фанатское творчество не прекращаются, и это печально.

Dagon + BindBC

На днях произошло два крупных события. Во-первых, вышла бета-версия LDC 1.13.0, которая теперь тоже самодостаточна — для сборки 64-битных приложений не нужны библиотеки из Visual Studio. По умолчанию используется линкер LLD.

Во-вторых, я решил отказаться от Derelict в пользу новой разработки Aldacron’а — BindBC. Это фреймворк для создания динамических биндингов, не использующий классы и сборщик мусора (@nogc), и потому отлично вписывающийся в мои принципы разработки. Из других преимуществ — поддержка OpenGL 4.6 и SDL 2.0.9, простота использования (вместо неинтуитивных DerelictGL3.load() и DerelictGL3.reload() теперь просто loadOpenGL()) и более простая обработка ошибок без исключений.

Из других значительных нововведений в Dagon отмечу рендеринг воды, новый шейдер неба по модели Рэлея и автоматический деплой — Dub теперь копирует библиотеки и внутренние данные движка в папку с проектом после каждой сборки.

x86_64! Ура, товарищи!

Наконец-то мне удалось собрать Atrium под 64-битную Windows! Пришлось ставить VS 2015, но оно стоило того…

Заодно проверил на практике LDC 1.0.0 — производительность, как и несколько лет назад, в разы выше, чем с использованием DMD, что не может не радовать. Теперь все тестовые сборки и промежуточные релизы будут собираться LDC и снабжаться 64-битными версиями.

Сборка Atrium из исходников стала проще

Исходный код в репозитории Atrium на GitHub теперь сопровождается инструментарием для сборки: программой Cook и файлами конфигурации для использования DMD (по умолчанию) и LDC. Для сборки проекта необходимо лишь изменить под себя пути к исполняемым файлам компилятора в файлах конфигурации (default.conf или ldc-linux.conf). Подробнее об этом читайте в файле INSTALL.

Atrium собирается последней версией LDC

Совсем недавно вышла бета-версия готовящегося к релизу LDC 0.10.0 — компилятора D, использующего LLVM для генерации машинного кода.

Решив проверить LDC на практике, я только что собрал им Atrium. Результат очень обрадовал, при компиляции в релизном режиме и с уровнем оптимизаций -O3, LDC выдал весьма шустрый код: ~100 FPS против ~80 у DMD 2.060! При этом я всего один раз столкнулся с необходимостью поправить исходники — при использовании ассоциативного массива указателей.

Как вывод: LDC вполне созрел для использовании в качестве основного компилятора D под Linux — во всяком случае, для работы с OpenGL и графикой реального времени. К сожалению, я не в курсе, как у него обстоят дела c поддержкой Windows — надеюсь, со временем этот вопрос устаканится.