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 в качестве полноценного шейдерного языка! Я лично в это верю.

Статус dlib2

В последнее время редко пишу о dlib, но хочу заверить: разработка библиотеки на месте не стоит, в данный момент я разрабатываю dlib2, а точнее — ее низкоуровневый компонент dcore. dcore — это процедурный API, совместимый с betterC и минимально заменяющий стандартную библиотеку. Он полностью автономный, не зависит от Phobos, поддерживает компиляцию в машинный код под bare metal (то есть, без операционной системы) и в WebAssembly. Он предназначен для создания компактных приложений в стиле C, а также для написания высокоуровневых частей библиотеки на его основе.

Может возникнуть вопрос — для чего это нужно, если есть libc? dcore, конечно, использует стандартные функции C при компиляции в таргеты, которые поддерживают libc. Но все упирается в WebAssembly: своего Emscripten на D не существует. Поэтому, для упрощения написания WASM-модулей, я решил сделать минимальный набор абстракций, которые скрывают платформозависимые реализации самых важных компонентов приложения под универсальным низкоуровневым API. dcore элементарно позволяет использовать printf, malloc и т.д., не задумываясь, доступны ли они на целевой платформе.

Еще один важный момент — математика. Я уже публиковал пост о бенчмарках различных реализаций синуса и косинуса — dcore поддерживает все стандартные математические функции (sin, cos, tan, asin, acos, sqrt, cbrt и др.), предоставляя на каждой платформе оптимальные реализации: LLVM-интринсики, функции C, функции Phobos и кастомные, без зависимостей, предназначенные для нестандартных таргетов.

Из нового:

  • dcore.time — удобный враппер над C-шными localtime и gmtime. Позволяет получать текущую системную время-дату, в текущем часовом поясе или по Гринвичу;
  • dcore.sys — информация о системе: архитектура CPU, количество ядер, объем памяти, название и версия ОС. Модуль поддерживает x86/x64, ARM, ARM64, IA64, MIPS, SPARC, PPC. Информация об ОС поддерживается для всех версий Windows и двух больших семейств Unix — Linux/Solaris/AIX (через sysconf) и BSD/OSX (через sysctl);
  • dcore.process — позволяет кроссплатформенно получить PID.

Мини-проект: радиоуправляемая платформа на Arduino

Пост не имеет отношения к D, но я решил опубликовать его тут, так как тематика относительно близкая.

Arduino я начал изучать год назад. Для непосвященных: это созданный итальянскими инженерами недорогой 8-битный микроконтроллер с распиновкой, очень простой для понимания начинающими, в комплекте с работающим из коробки C-тулчейном, юзер-френдли IDE и инструментарием для прошивки платы по COM-порту. Вообще электрическими схемами я увлекаюсь с самого раннего детства — всегда любил что-то делать своими руками из железного конструктора, проводов и лампочек =) С появлением компьютера это хобби не угасло, хотя до недавних пор и отошло на второй план. Мечту сделать игрушечный радиоуправляемый аппарат я не оставлял много лет — и вот, наконец, сумел воплотить ее в жизнь!

За основу я взял китайский клон Arduino Uno R3 (благо они очень дешевые, можно не жалеть для экспериментов). Я использовал, в основном, прилагавшиеся к плате детали, но пришлось также докупить электромоторы и драйвер L298N. Что удобно, моторы можно купить сразу с колесом — таким образом, остается только укрепить ходовую часть на шасси.

Шасси, за неимением лучшего материала, я сделал из Лего. На этом этапе, как ни странно, я застрял надолго, так как электронные компоненты оказалось очень непросто надежно прикрепить к лего-деталям. Для удобства я докупил лего-совместимый корпус для Arduino, однако толкового способа закрепить моторы я так, к сожалению, и не придумал — в итоге ходовая часть получилась немного расхлябанная, но тем не менее работоспособная.

Отдельной проблемой стало питание. Сначала я думал запитать плату от USB-пауэрбанка, но ничего не вышло: Ардуино работает пару секунд и выключается. Пришлось добавить батарейный отсек на 6 элементов AA (9В). Драйвер моторов запитал параллельно с платой, от разъема VIN (который, в отличие от обычных 5-вольтных разъемов питания, напрямую отдает 9В от адаптера, что очень удобно). Теоретически, для большей надежности можно было бы запитать драйвер полностью независимо от Ардуино, добавив клемму между DC-штекером и платой, но я не стал так заморачиваться.

Драйвер L298N — это ключевой компонент двухмоторной ходовой части. Пара моторов нужна для того, чтобы не делать рулевое управление: для поворота аппарата влево-вправо нужно просто вращать моторы в противположных направлениях. L298N нужен именно для этого. Управление этой штукой осуществляется путем подачи сигналов на командные входы. В качестве remote-контроллера я использовал инфракрасный приемник в паре с комплектным пультом — соответствующая программа получилась очень простой:

#include "IRremote.h"

#define IR_RECEIVE_PIN 7

// IN1, IN2 - motor A
// IN3, IN4 - motor B
#define IN1 2
#define IN2 3
#define IN3 4
#define IN4 5

#define ENA A0
#define ENB A1

int receivedValue;
int buttonPressed = 0;

void setup()
{
    pinMode(IN1, OUTPUT);  //motor A
    pinMode(IN2, OUTPUT);  //motor A
    pinMode(IN3, OUTPUT);  //motor B
    pinMode(IN4, OUTPUT);  //motor B
  
    IrReceiver.begin(IR_RECEIVE_PIN, ENABLE_LED_FEEDBACK);
  
    buttonPressed = 0;
}

void loop()
{
    receivedValue = 0;
    
    if (IrReceiver.decode())
    {
        receivedValue = IrReceiver.decodedIRData.command;
        IrReceiver.resume();
    }
  
    if (receivedValue == 28) // forward
    {
        digitalWrite(IN1, HIGH);
        digitalWrite(IN2, LOW);
        digitalWrite(IN3, LOW);
        digitalWrite(IN4, HIGH);
    }
    else if (receivedValue == 82) // back
    {
        digitalWrite(IN1, LOW);
        digitalWrite(IN2, HIGH);
        digitalWrite(IN3, HIGH);
        digitalWrite(IN4, LOW);
    }
    else if (receivedValue == 8) // left
    {
        digitalWrite(IN1, HIGH);
        digitalWrite(IN2, LOW);
        digitalWrite(IN3, HIGH);
        digitalWrite(IN4, LOW);
    }
    else if (receivedValue == 90) // right
    {
        digitalWrite(IN1, LOW);
        digitalWrite(IN2, HIGH);
        digitalWrite(IN3, LOW);
        digitalWrite(IN4, HIGH);
    }
    else // stop
    {
        digitalWrite(IN1, LOW);
        digitalWrite(IN2, LOW);
        digitalWrite(IN3, LOW);
        digitalWrite(IN4, LOW);
    }
    
    delay(100);
}

Аппарат ездит со скоростью около метра в секунду. Минусом двухколесной системы является то, что нельзя поворачиваться во время движения, поэтому более интересным вариантом было бы, конечно, добавить руль — например, сервомотор, поворачивающий дополнительное рулевое колесо.

Dagon 0.19.0

Вышла новая версия движка! Релиз очень большой, все изменения детально описывать в этом посте не буду, перечислю только самое основное. Подробности — на странице релиза.

  • Добавлен модуль dagon.core.persistent — простая БД типа «ключ-значение», которую можно использовать для управления пользовательскими данными игры, такими как настройки и сохранения;
  • InputManager теперь поддерживает разделенные пробелами имена кодов клавиш в привязках. Вместо пробела используется символ плюса, например, kb_left+ctrl для левого Ctrl;
  • Dagon-приложение теперь устанавливает кодировку консоли на UTF-8 в Windows, чтобы нормально выводился нелатинский текст;
  • Добавлен SimpleRenderer, о котором я уже писал ранее — облегченный рендерер для казуальной и стилизованной графики;
  • Добавлена поддержка бокс-проекции для световых зондов. Ее можно включить с помощью свойства Entity.probeUseBoxProjection. Зонды теперь используют альфа-спад для плавного смешивания с данными в G-буфере — благодаря этой фиче сглаживается граница между интерьерным и уличным освещением. Для управления этим эффектом введено свойство Entity.probeFalloffMargin;
  • Добавлена поддержка ортогональных проекций: свойство RenderView.projection теперь принимает константы Perspective, Ortho и OrthoScreen. RenderView.orthoScale управляет масштабом проекции в режиме Ortho;
  • Добавлен новый примитив — цилиндр ShapeCylinder, а также Billboard (прямоугольник, который всегда направлен в сторону камеры);
  • Материалы теперь поддерживают произвольные преобразования текстур (аффинные матрицы 3×3) благодаря свойству Material.textureTransformation. Метод Material.setSprite(Vector2f uvSize, Vector2f uvPosition) можно использовать для наложения на квады фрагментов текстуры — полезно для рендеринга спрайтов и покадровой анимации;
  • Добавлен новый пакет dagon.collision — базовая система обнаружения столкновений;
  • В контроллере персонажей NewtonCharacterComponent появилась поддержка приседания (метод crouch) и улучшенная проверка земли.

Обновления

Улучшения в Dagon

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

dmech, мой старый физический движок, включает продвинутые алгоритмы проверки столкновений, которые до недавнего времени пропадали зря (в частности, Minkowski Portal Refinement и солвер Джонсона) — я решил исправить этот недочет и добавить их в Dagon в качестве пакета dagon.collision. В данный момент он поддерживает обнаружение пересечений между любыми выпуклыми телами и проверку пересечения выпуклого тела с лучом.

Продолжаю работу над упрощенным рендером, о котором уже писал ранее. Появились билборды и универсальная ортографическая проекция: теперь можно, к примеру, легко сделать изометрическое 3D со спрайтовой графикой, как на скриншоте. Среди прочих улучшений — рефакторинг компонента FreeviewComponent и улучшенный контроллер персонажа Newton (появилась поддержка приседания). Оптимизирован deferred-рендер, сокращено количество переключений кадровых буферов. Добавлен новый встроенный примитив — цилиндр.

BindBC-GLSLang

Написал биндинг к glslang, референсному компилятору GLSL от Khronos — можно генерировать модули SPIR-V для WebGPU-приложений прямо в D, без использования внешнего ПО.

https://github.com/gecko0307/bindbc-glslang

Раздел со статьями

Добавил раздел «Статьи» для быстрого доступа к ним. Там, в основном, написанное за последние 10 лет и доступное онлайн, так как более ранние мои материалы публиковались еще в PDF-версии журнала «FPS» и бумажных изданиях — ссылки на них поставить затруднительно.

std140: статическая валидация структур

std140 — самый платформонезависимый лейаут uniform-блоков. Если вы передаете параметры в шейдер структурами, а не по одному, то лучше использовать именно его. Конечно, у переносимости есть своя цена: стандарт предусматривает несколько ограничений, самое известное из которых — выравнивание по 16 байтам. То есть, все поля структуры должны быть по смещению кратны 16 байтам. Использование невыровненных данных приводит к тому, что в шейдере считываются неправильные значения, и это вызывает недоумение у начинающих.

Благодаря CTFE и статической интроспекции в D можно проверять структуры на соответствие этому правилу на этапе компиляции — все необходимое есть в std.traits:

bool isFieldsOffsetAligned(T, alias numBytes)()
{
    static if (is(T == struct))
    {
        static foreach(f; T.tupleof)
        {
            static if (f.offsetof % numBytes != 0)
                return false;
        }
        
        return true;
    }
    else return false;
}

alias isStd140Compliant(T) = isFieldsOffsetAligned!(T, 16);

Теперь можно делать так:

import dlib.math.vector;
import dlib.math.matrix;

struct Uniforms
{
    float someParameter;
    Matrix4x4f modelViewMatrix;
    Matrix4x4f normalMatrix;
    Vector4f worldPosition;
}

static assert(isStd140Compliant!Uniforms,
    Uniforms.stringof ~ " does not conform to std140 layout");

Возникает вопрос, почему бы просто не делать align(16)? Можно!

struct AlignedUniforms
{
  align(16):
    float a;
    Vector3f b;
    Matrix4x4f c;
}

Преимуществом такого подхода является право использовать любые типы для полей, но это неэффктивно: например, вместо того, чтобы впустую расходовать три байта после a, можно было бы упаковать a вместе с b в один 16-байтный вектор. Поэтому я лично не фанат автоматического компиляторного выравнивания — лучше это делать вручную, защитившись от ошибок необходимыми статическими проверками.

Обновления

bindbc-newton 0.3.2

Небольшой исправляющий релиз, который фиксит список путей к Newton под Linux.

https://github.com/gecko0307/bindbc-newton/releases/tag/v0.3.2

dlib2 компилируется под WASM

Поддержка Web Assembly в LDC существует достаточно давно, и я когда-то уже делал экспериментальный мини-проект — приложение с треугольником, рисующимся через OpenGL ES, которое можно скомпилировать как под десктоп, так и в WASM-модуль. Для этой работы пришлось написать минимальную замену стандартной библиотеке, поскольку поддержка WASM в Phobos чуть более, чем никакая. Надеюсь изменить эту ситуацию в dlib2 — dcore уже сейчас можно собрать под WASM!

Например, вот такой «Hello, World» на десктопе печатает в стандартный вывод, а в браузере — в консоль, причем с поддержкой UTF-8:

module main;

import dcore.stdio;

extern(C):

void main()
{
    // Minimal cross-platform betterC application.
    // Will print to stdout on desktop and to the console in browser.
    
    printf("Hello from D! Привет из D!n");
}

В ближайшее время напишу пост по статусу dlib2 и всем деталям планируемых фич.

Багфиксы и улучшения в MiniGL

Актуализировал и чуть доработал MiniGL, мой программный растеризатор. Исправил альфа-смешивание, а также теперь можно отключить запись и чтение z-буфера для отрисовки экранных спрайтов. Обновил также демонстрационное приложение — теперь это почти готовый движок для коридорных ретро-шутеров: есть проверка столкновений со стенами, стрельба магией и спрайт оружия.

Упрощенный рендеринг

По мере усложнения стандартного рендер-движка Dagon, повышаются и системные требования — в данный момент он требует довольно мощную видеокарту геймерского класса (желательно NVIDIA, желательно не ниже Turing). Но, поскольку далеко не все игры обязаны иметь топовую графику, неплохо предусмотреть в движке некий облегченный режим, оптимальный для казуальных жанров и стилизации под ретро, где не нужен сложный пайплайн с реалистичным освещением и PBR. В Dagon рендер уже давно структурно вынесен в отдельную систему, которую можно модифицировать и даже полностью заменять, не трогая остальной код движка — модель данных сцены и менеджер ресурсов в Dagon полностью независимы от рендера. Это позволило без особых сложностей добавить упрощенный рендер-движок SimpleRenderer, который вы можете создать в вашем классе игры (на базе Game или Application) и заменить им стандартный DeferredRenderer.

SimpleRenderer полностью переопределяет рендеринг объектов. Здесь по умолчанию нет физически обоснованных источников света, карт окружения и т.д. — за освещение отвечает простейшая модель Блинна-Фонга, которая в данный момент работает с одним глобальным направленным источником света (environment.sun текущей сцены). Нет normal mapping’а и прочих эффектов материала — учитывается только baseColorFactor/baseColorTexture. Но зато вы можете назначить вашим материалам любой шейдер с любыми эффектами — в DeferredRenderer такой возможности нет (все объекты с пользовательскими шейдерами трактуются как forward и рендерятся отдельно, после всех deferred-проходов). Также в этой системе поддерживаются слои, что позволяет явным образом задавать порядок рендеринга группам объектов — например, можно занести все прозрачные объекты в отдельный слой, который рисуется поверх дефолтного.

SimpleRenderer отлично подойдет для создания игр для low-end железа, он будет работать даже на самых слабых системах.

Бокс-проекция

Добавил в Dagon поддержку бокс-проекции для световых зондов окружения (EnvironmentProbe). Техника старая, но никем не отмененная — а главное, хорошо сочетается с deferred-рендером!

Стандартный environment mapping предполагает, что стенки виртуальной среды, с которой считывается освещение, бесконечно удалены от объектов сцены. Это допущение работает для открытого пространства, но не годится для интерьера. Бокс-проекция корректирует сэмплинг из карты окружения так, что результат выровнен по сторонам бокса заданного размера, благодаря чему минимумом ресурсов достигается сносного качества непрямое освещение в интерьере (если, конечно, карта окружения 1 в 1 совпадает с моделью комнаты). Это эффективный способ аппроксимировать локальный GI в ограниченном пространстве: окружение интерьера, с которого рендерилась карта, статичное, но любые другие объекты могут быть динамическими. При трансформации камеры или объекта внутри комнаты, отражения соответствующим образом меняются, причем как зеркальные, так и диффузные.

Получается даже имитировать объемные источники света! На скриншотах ниже нет ничего, кроме параллельного источника света для солнца и двух статичных карт окружения — для улицы и для интерьера. Отражения окон и светящегося блока на полу получаются автоматически:

Главным минусом техники является то, что создать под нее правильную карту не очень просто — нужно учитывать различия в координатных системах.

Сжатие текстур, часть IV. Мобильные форматы: ETC и PVRTC

Продолжение серии постов о сжатых текстурных форматах. Предыдущие части: часть I, часть II, часть III.

Ситуация со сжатием текстур на мобильных платформах довольно запутанная, так как 3D-ускорители там существенно отличаются от десктопных. К сожалению, ни iOS, ни Android не поддерживают S3TC (и, тем более, BPTC). В мобильных системах используются свои специализированные форматы сжатия — ETC на Android и PVRTC на iOS.

ETC1 и ETC2

Ericsson Texture Compression / iPACKMAN

Формат сжатия от Ericsson. Поддерживается как в мобильных устройствах, так и в современных браузерах (кроме Firefox).

ETC1 поддерживается практически на всех android-устройствах и является стандартным форматом сжатия в OpenGL ES 2.0. Не поддерживает прозрачность. Блок 4×4 преобразуется в 64-битное представление. Блок разбивается на два субблока (4×2 или 2×4), им присваиваются базовые цвета — либо каждому RGB 4:4:4, либо одному 5:5:5, а второму смещение 3:3:3 относительно первого. Пиксели в субблоке представляются в виде суммы базового цвета и одного из четырех смещений — так называемых модификаторов: pixelColor = baseColor + RGB(modifier, modifier, modifier). Модификаторы, определяемые спецификацией, представляют собой 4×8 таблицу констант — целочисленных значений со знаком. Индексы ряда в таблице [0, 7] хранятся как два 3-битных значения (по одному на субблок), индексы столбца [0, 3] — как 16 2-битных значений (по одному на каждый пиксель). Оставшиеся два бита определяют ориентацию субблоков (flip-бит) и тип хранения базовых цветов (diff-бит). Результат суммирования нормализуется в 8 бит на канал.

ETC2 является стандартным форматом сжатия в OpenGL ES 3.0. Поддерживает прозрачность. ETC2 — это обратно-совместимое надмножество ETC1. Альфа-канал кодируется по такому же принципу, что и цвет: значение прозрачности для пикселя — это сумма базовой альфы и модификатора из таблицы констант 8×16. Блоку 4×4 присваиваются дополнительные 64 бита: 8-битное базовое значение альфа, 4-битный индекс ряда в таблице модификаторов, 4-битный множитель и 16 3-битных индексов столбцов.

Также в ETC2 есть отдельный режим punch-through (GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2), аналогичный ETC1, но в котором diff-бит заменяется битом прозрачности — таким образом, блок интерпретируется либо как полностью прозрачный, либо как полностью непрозрачный.

PVRTC1 и PVRTC2

PowerVR Texture Compression

Используется в видеоускорителях PowerVR (iPhone и другие устройства Apple). Основной целью разработчики формата поставили устранение разрывов цвета вдоль границ блоков, которая присуща S3TC. PVRTC поддерживает альфа-канал.

Имеет две разновидности — 4bpp и 2bpp. На блок всегда выделяется 64 бита информации, поэтому в режиме 4bpp блок имеет размер 4×4 пикселя, в режиме 2bpp — 8×4. В некоторых аппаратных реализациях блоки расположены в памяти не в порядке сканирования (снизу вверх, слева направо), а в Z-последовательности для увеличения пространственной локальности и, как следствие, более эффективного кэширования.

В каждом блоке хранится шесть переменных: для PVRTC1 — данные модуляции (32 бит), флаг punch-through alpha (1 бит), цвет A (15 бит), флаг прозрачности цвета A (1 бит), цвет B (14 бит) и флаг прозрачности цвета B (1 бит). Для PVRTC2 — данные модуляции (32 бит), флаг модуляции (1 бит), цвет B (15 бит), флаг hard transition (1 бит), цвет A (15 бит) и флаг прозрачности (1 бит). Битовая глубина значений A и B задается по-разному в зависимости от того, есть ли альфа-канал: либо RGB 5:5:4(5), либо RGBA 3:4:4:3(4), в скобках указан вариант для 15-битного цвета. Дополнительный бит прозрачности определяет наличие у цвета альфа-канала. В PVRTC его можно задавать независимо для A и B, когда как в PVRTC2 бит прозрачности только один, и оба цвета должны быть в одинаковом формате — либо RGB, либо RGBA. Цвет пикселя вычисляется билинейной интерполяцией цветов A и B. Каждому пикселю блока сопоставляется, в зависимости от разновидности формата, 2-битное или 1-битное значение модуляции, кодирующее вес интерполяции между A и B.

PVRTC2 расширяет алгоритм поддержкой четырех разных режимов блока, задаваемых флагами hard transition в сочетании с флагом модуляции: стандартная билинейная, punch-through alpha, резкий переход, локальная палитра.