Кросс-компиляция «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 на виртуалке для других тестов.

Рендеринг волос в Dagon 2

Добавил анизотропную BRDF Kajiya-Kay для рендеринга волос:

Текстура волос может иметь альфа-канал, но вместо альфа-смешивания используется упрощенная прозрачность на основе дизеринга Байера.

dlib 1.7

В рамках Dagon 2.0 скопилось множество улучшений для dlib, и данный релиз подытоживает эту работу.

Добавлен модуль dlib.math.base — замена std.math, которая заменяет некоторые часто используемые функции быстрыми интринсиками LLVM при компиляции LDC (подробнее об этом тут). Ускорены следующие элементарные функции: fabs, sqrt, sin, cos, tan, ceil, floor, round, trunc, rint, nearbyint, pow, exp, exp2, log, log2, log10, fmax, fmin, fma, copysign. В среднем они вдвое быстрее, чем в Phobos. Единственный нюанс, который следует помнить — лучше не импортировать оба модуля одновременно, иначе будет конфликт, и вам придется использовать квалифицированные имена при вызовах.

Добавлена встроенная SIMD-оптимизация для перемножения матриц 4×4 (Matrix4x4f). Это ускоряет оператор в 3-4 раза. В связи с этим удален модуль dlib.math.sse как устаревший, неподдерживаемый и, как выяснилось, нерабочий под x86_64.

Хэш-таблица FlatHashMap из Dagon 2.0 добавлена в библиотеку как dlib.container.hashmap. Реализация xxHash64, соответственно, — как dlib.hash.xxhash64 в новом пакете dlib.hash. Напомню, что это очень быстрая реализация ассоциативных массивов без использования сборщика мусора, которая в десятки раз эффективнее, чем dlib.container.dict и в 1,5-2 раза — чем встроенные ассоциативные массивы D.

Полностью переработан dlib.random — пакет теперь работает на основе перемешанного конгруэнтного генератора (PCG), реализованного для 32- и 64-битных чисел. Это современный эффективный генератор псевдослучайных чисел, который превосходит как Вихрь Мерсенна (std.random), так и линейный конгруэнтный генератор (rand в стандартной библиотеке C). Добавлены новые функции choice (случайный выбор из заданного множества вариантов) and rollDice (случайное целое число от 0 до заданного порога). Функция seed, генерирующая зерно для инициализации генератора из системной энтропии, теперь использует функцию смешивания fmix64 из MurmurHash3 вместо mix Боба Дженкинса.

Не используйте std.random!

Стандартная библиотека использует классический Вихрь Мерсенна (MT19937). Недавно я, вдохновившись эффективностью PCG для шейдерных вычислений, добавил соответствующий модуль в dlib, заменив им rand в dlib.random. Оказалось, что моя функция random для выборки равномерного распределения в диапазоне 0..1 аж в 4 раза быстрее, чем std.random.uniform!

Бенчмарк для 10000000 вызовов:

std.random.uniform: 77 ms, 38 μs, and 3 hnsecs total
dlib.random.random: 19 ms, 599 μs, and 3 hnsecs total

Стандартное отклонение у обеих функций примерно одинаковое:

std.random.uniform: 0.544σ
dlib.random.random: 0.413σ

Статья по теории рендеринга в Dagon

Наконец-то дошли руки написать теоретическую статью о модели освещения, которую я использую в движке, со всеми формулами и ссылками на научную литературу. Это GGX/Trowbridge-Reitz, на которую опирается освещение в Unreal Engine 4 и многих других движках — физически обоснованная модель зеркально отраженного света для изотропных шероховатых поверхностей. Ее многие сейчас используют, одни и те же уравнения можно встретить в самых разных движках, но мало кто задумывается, откуда они взялись.

Поскольку в тексте много математики, я оформил статью в виде PDF — можно скачать файл или читать при помощи встроенного в браузер просмотрщика.

Не используйте ассоциативные массивы!

Шучу, конечно. Но специализированная хэш-таблица действительно может быть заметно быстрее встроенных ассоциативных массивов D. На днях начал писать менеджер ассетов для Dagon 2.0 и решил заодно заменить dlib.container.dict для хранения ссылок на ассеты чем-то поэффективнее. Результатом стал класс FlatHashMap — таблица, которая хранит все элементы в одном непрерывном блоке памяти, что значительно улучшает производительность за счет кэш-дружественности. Кроме того, такая таблица работает в среднем за постоянное время, что выгоднейшим образом отличает ее от префиксного дерева (на котором основан dlib.container.dict) — скорость префиксного дерева пропорциональна длине ключей, а flat hash map работает с хэшами фиксированного размера.

В качестве хэш-функции я взял быстрый xxHash64, который можно использовать в реальном времени. Моя версия в Dagon — это частичный D-порт xxhash-clean, минимальной референсной реализации xxHash. Результаты бенчмарка следующие: вставки у FlatHashMap получились аж в 36 раз быстрее, чем у dlib.container.dict, и в 1,7 раз быстрее, чем у ассоциативных массивов. Поиск — в 15 раз быстрее, чем у dlib.container.dict, и в 1,6 раз быстрее, чем у ассоциативных массивов. Эффективен и opApply — это обычный линейный обход буфера безо всякой рекурсии и сложной логики.

SMAA против всех

Долгое время в Dagon поддерживался лишь один алгоритм антиалиасинга — FXAA. Его главные достоинства — почти нулевой оверхед и довольно приличное качество (лучше хоть какой-то AA, чем вообще никакого). Но есть ситуации, в которых результат FXAA оставляет желать лучшего — например, длинные прямые края под маленькими углами относительно горизонтали. Также FXAA часто приписывают замыливание текстур, но это, скорее всего, миф, возникший из-за каких-то некорректных реализаций алгоритма в определенных играх — нормальный FXAA 3.11 ничего не мылит.

Сегодня многие движки используют TAA, но в Dagon я его пока добавлять не хочу по ряду причин. Все темпоральные техники основаны на идее накопления случайных сэмплов в условиях кадровой когерентности — это нормально работает, только когда камера неподвижна или движется очень медленно. При резких движениях камеры историю приходится сбрасывать, и визуально это проявляется в том, что эффект то ослабевает, то усиливается. Для сглаживания, имхо, это выглядит слишком неестественно — и так уже приходится терпеть темпоральные артефакты всяких SSLR и т.п. Плюс TAA сглаживает всю картинку, а не только края — а это то, от чего индустрия, по идее, пыталась уйти. В общем, даже качественные реализации TAA, например в Unreal, выглядят на мой вкус так себе.

Окончательным компромиссом между стареньким FXAA и современным, но не очень привлекательным TAA стал алгоритм SMAA — субпиксельное морфологическое сглаживание, разработанное в начале 2010-х по заказу AMD. Работает на современных видеокартах очень быстро — на глаз я не заметил никакого замедления. SMAA сглаживает только края, так как алгоритм основан на распознавании краев. Минусом по сравнению с FXAA является повышенное потребление памяти — добавляются два дополнительных буфера.

Чтобы включить SMAA в Dagon 2.0, нужно указать в render.conf

antialiasing: "SMAA";

Либо просто включить высокий профиль качества (profile: 1). FXAA тоже никуда не девается и включен по умолчанию для низкого профиля качества (profile: 0).

antialiasing: "FXAA";

Особенности декодирования PNG

На днях провел самую нетривиальную сессию отладки за всю историю Dagon: по-тихому сломалась загрузка одной из текстур в сцене с лапшичной — 16-битный grayscale PNG, карта высот под parallax mapping. Я уже сталкивался с багом обработки 16-битных изображений в SDL_Image, но пофиксил эту проблему только для RGB — SDL_Image выдает формат SDL_PIXELFORMAT_RGB48, можно перехватывать такие кейсы и конвертировать в 8 бит на канал вручную вместо вызова SDL_ConvertSurface. Но в случае с grayscale чудит уже сам декодер, выдавая почему-то формат SDL_PIXELFORMAT_INDEX8, тогда как grayscale — это не индексированный режим, а отдельный пиксельный формат, поддерживающий разную битовую глубину. Я так и не понял, что с этим делать, и решил для загрузки PNG адаптировать мой старый декодер из dlib.

Сказано — сделано: я создал dagon.resource.png, заменил SuperImage на TextureBuffer и выпилил поддержку APNG, которая в играх все равно не нужна. Но после этого внезапно сломалась загрузка сцены Sponza — для всех текстур декодер просто выдавал мусор в чанках. Я долго ломал голову, искал ошибку в алгоритме разбора, пока не додадался на всякий случай проверить одну из текстур hex-редактором. Оказалось, что это вообще не PNG, а JPEG! SDL_Image такие картинки переваривал без проблем, поэтому я даже не замечал. Поэтому, чтобы вернуть старое поведение, пришлось городить детектор формата на основе сигнатур. В итоге, впрочем, загрузчик текстур стал работать даже чуть быстрее, чем прежний вариант.

Статус Dagon 2

Разработка новой версии движка идет гораздо быстрее, чем я успеваю писать о ней. В августе я уже отчитывался о порте dagon:imgui и dagon:audio, на днях я также портировал dagon:physfs, dagon:video, dagon:security и dagon:server. Перенесен менеджер ввода InputManager, биндинг Wintab и модуль ввода с графического планшета, реализована поддержка компонентов для Entity, поддержка HiDPI, вычислительных шейдеров и ресэмплинга текстур на GPU.

Кроме того, я занимался оптимизацией некоторых частей рендера и решил ряд застарелых проблем производительности:

  • Все стохастические алгоритмы теперь используют перемешанный конгруэнтный генератор (PCG). Это один из лучших генераторов псевдослучайных чисел, подходящих для вычислений в реальном времени. В отличие от предыдущих решений в движке, PCG не использует тригонометрические функции и опирается на целочисленную арифметику. Этот генератор ускоряет шейдеры, в которых используется случайность (сэмплинг Монте-Карло) — SSAO, SSLR, Motion blur;
  • Оптимизирован SSLR, устранены лишние перемножения векторов и матриц для репроекции при raymarching’е;
  • Оптимизирован SSAO, вся тригонометрия вынесена из цикла сэмплинга;
  • Метроном (Cadencer) теперь использует высокоточный наносекундный таймер. Реализован новый планировщик кадров, который сильно снижает нагрузку на CPU и уменьшает тем самым энергопотребление и нагрев, что важно на ноутбуках;
  • Добавлена поддержка режима презентации Mailbox, что, по всей видимости, ставит точку на многолетней проблеме VSync. Отныне никакого тиринга и инпут-лага!