Slang – универсальный шейдерный язык

Что выглядит читабельнее? Это:

struct VertexOutput
{
    @builtin(position) position: vec4<f32>,
    @location(0) fragmentPosition: vec4<f32>
};

@vertex
fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput
{
    let x = f32(i32(vertexIndex) - 1);
    let y = f32(i32(vertexIndex & 1u) * 2 - 1);
    var output: VertexOutput;
    output.position = vec4<f32>(x * 0.5, y * 0.5, 0.0, 1.0);
    output.fragmentPosition = 0.5 * (vec4<f32>(x, y, 0.0, 1.0) + 1.0);
    return output;
}

…или это:

struct VertexOutput
{
    float4 position: SV_Position;
    float4 fragmentPosition;
};

[shader("vertex")]
VertexOutput vertexMain(uint vertexIndex: SV_VertexID)
{
    let x = float(int(vertexIndex) - 1);
    let y = float(int(vertexIndex & 1u) * 2 - 1);
    VertexOutput output;
    output.position = float4(x * 0.5, y * 0.5, 0.0, 1.0);
    output.fragmentPosition = 0.5 * (float4(x, y, 0.0, 1.0) + 1.0);
    return output;
}

Второй листинг, на мой взгляд, выигрывает сравнение. Это код на Slang, HLSL-подобном языке, который позиционируется в качестве платформонезависимой основы для написания одних и тех же шейдеров под любые графические API, включая D3D12, Vulkan, Metal, OpenGL, а с недавних пор и WebGPU. Идея, конечно, не новая – если вы писали шейдеры в нулевые, то, наверное, помните Cg от NVIDIA – но с со времен попыток написать универсальный компилятор в низкоуровневые шейдерные языки утекло немало воды и сменилось несколько поколений графического железа. Сейчас, в эпоху Vulkan и SPIR-V, кросс-компиляция шейдеров актуальна как никогда: старичок GLSL уже сдает позиции, для новых стандартов создаются новые языки – индустрия переживает очередной этап фрагментации.

Я уже писал о своих впечатлениях от WGSL, встроенного шейдерного языка WebGPU, и многие его конструкции мне до сих кажутся спорными и неудобными. Особенно бесят типы вида vec4<f32>. Не так давно поддержка WGSL была добавлена в компилятор Slang, в связи с чем я теперь всерьез рассматриваю этот язык как основной для создания графического движка на WebGPU.

Главная киллер-фича Slang – это, конечно, модули. Причем, что интересно, есть и препроцессор а ля C с директивами #include, #ifdef и др. От HLSL и GLSL язык выгодно отличается большим набором фич, присущих современным высокоуровневым языкам: поддержкой вывода типов, пространств имен, функций-членов структур (с неизменяемым по умолчанию неявным this – по-моему, отличная идея!), а также конструкторов, геттеров/сеттеров и даже перегрузки операторов. Есть дженерики, интерфейсы, кортежи. Интересен тип Optional, который дополняет любой другой тип поддержкой значения none – чтобы можно было указать отсутствие какого-либо значения. Для SPIR-V и CUDA в языке есть ограниченная поддержка указателей. Очень полезный инструмент – декоратор ForceInline, который заменяет вызов функции подстановкой ее кода. Наконец, в языке есть автоматическое дифференцирование, которое используется в задачах машинного обучения.

Старую как мир проблему некоммутативности умножения матриц и векторов в Slang решили следующим образом: оператор * всегда означает произведение двух матриц, как принято в математике. Чтобы трансформировать вектор матрицей, нужно вместо m * v писать mul(v, m). Для такого старого ветерана OpenGL, как я, слегка непривычно, но жить можно 🙂

Репозиторий

Документация по языку

Обновления

Вышел bindbc-wgpu 0.22.0, соответствующий wgpu-native 22.1.0.1. Обновилось много чего по мелочи, из важного: теперь нужно удалять WGPURenderPassEncoder после отрисовки, иначе у вас wgpuQueueSubmit будет падать с ошибкой.

Соответственно, обновил и демонстрационное приложение wgpu-dlang, плюс избавился там от библиотеки STB для загрузки изображений.

Обновления

bindbc-wgpu 0.19.0, соответствующий wgpu-native 0.19.x. Каких-то существенных изменений в этой версии нет, кроме того, что структуры и функции опроса статистики конвейера вынесены в платформоспецифичную часть API (bindbc.wgpu.types2).

Также рад сообщить, что bindbc-wgpu теперь успешно собирается и работает под Linux – как и демо-приложение с моделью Cerberus. Тестировал на Mint 21.2 с wgpu-native 0.19.1.1.

Работа над новым релизом Dagon продолжается: на днях внес множество исправлений, связанных с поддержкой Linux. В частности, теперь расширения dagon:newton и dagon:imgui загружают библиотеки из папки проекта, если не находят их в /usr/lib. При сборке под 64-битный Linux нужные библиотеки теперь копируются в папку с проектом – это должно упростить работу с расширениями, пользователи неоднократно жаловались на неинтуитивность подключения Newton. Также обновлен биндинг ImGui.

dlib 1.2 и другие обновления

Несколько обновлений за последние месяцы:

dlib 1.2.0 – добавлены функции гомотетии (homothetyMatrix, homothetyMatrix2D) в модуль dlib.math.transformation, функции конвертации радианов в обороты и обратно (radtorev, revtorad) в модуль dlib.math.utils. В dlib.image.render.shapes появилась функция отрисовки прямоугольника (drawRect) – автор реализации ReactiveAlkali. Добавлен файл CODE_OF_CONDUCT.md – краткий кодекс поведения для сообщества dlib:

Code of Conduct

Этот проект приветствует вклад в любой форме и поощряет открытое обсуждение и обмен мнениями о его функциях, архитектуре и деталях реализации. Для поддержания надлежащей культуры общения был сформулирован настоящий Кодекс поведения. Это относится к системе отслеживания проблем проекта, чату и, возможно, другим каналам связи.

1. Мы не приемлем нецензурные выражения, грубые и/или пренебрежительные сообщения, использование сексуальной, жестокой и иной оскорбительной лексики и изображений.

2. Любая критика должна быть конструктивной и разумной. Простого личного недовольства без каких-либо объективных причин недостаточно, чтобы делать критические заявления и влиять на развитие dlib.

3. Этот проект держится в стороне от нетехнологических вопросов и тем. Мы приветствуем всех, независимо от возраста, гендерной идентичности, религии, этнической принадлежности, гражданства или культурного происхождения, и наше сообщество не придерживается какой-либо определенной идеологии. Сайты, репозитории, каналы связи и прочие ресурсы, связанные с dlib, не должны использоваться как площадка для пропаганды.

Также добавлена поддержка Doxygen для генерирования документации.

bindbc-wgpu 0.16.0 – синхронизация с wgpu-native 0.16.0.1. Кстати, также обновил демку wgpu-dlang, добавил поддержку мипмаппинга и перевел на dlib 1.2.0.

Dagon 0.15 пока, к сожалению, откладывается на неопределенный срок – не доходят руки сделать поддержку 1D-текстур, которой не хватает для того, чтобы реализовать загрузчик KTX. В марте добавил новые параметры постобработки для ручного управления эффектом Depth of Field (dofManual, dofNearStart, dofNearDistance, dofFarStart, dofFarDistance) – это нужно, если необходимо реализовать кастомную расфокусированность, труднодостижимую стандартными параметрами focalDepth, focalLength и fStop.

WebGPU: впечатления за два года

В конце 2020 года я с большим энтузиазмом взялся за изучение WebGPU. Для тех, кто не в курсе, поясню: это будущий веб-стандарт низкоуровнего графического API, который позволит браузерным приложениям эффективно задействовать возможности современных видеокарт. Замечательная особенность реализации WebGPU от Mozilla заключается в том, что ее можно использовать в нативных приложениях через C-интерфейс – я, разумеется, сразу занялся созданием собственной привязки WebGPU для D. На сегодняшний день у меня уже практически готов минимальный фреймворк для разработки WebGPU-приложений, исходники которого вы можете найти на GitHub: в текущей стадии он способен загружать и рендерить модели в формате OBJ с тестовой моделью освещения на основе GGX BRDF. Рендер прямой, безо всяких отложенных эффектов, также пока не поддерживается мипмаппинг. Тем не менее, кейс получился вполне достаточный для тестирования основных возможностей API.

Модель Cerberus, отрендеренная при помощи WebGPU

Этот фреймворк я писал довольно долго – в основном, из-за того, что wgpu-native жутко нестабилен, от версии к версии в инициализирующие структуры вносится очень много изменений. Часто бывает, что после очередного обновления приложение компилируется, но падает с какой-то экзотической ошибкой – без поллитры не разберешься (в итоге выясняется, что изменилась какая-нибудь константа, или стал обязательным nextInChain в одном из дескрипторов). Особым “удовольствием” было отлаживать шейдеры на WGSL в процессе стандартизации языка: то синтаксис атрибутов изменится, то разделитель полей в структурах… К тому же нестабильность API долгое время не давала мне определиться с архитектурой некоторых компонентов, ведь WebGPU имеет гораздо больше сущностей, чем OpenGL, и к ним нужно правильно подбирать модели данных.

Скажу честно: после OpenGL ко всем этим бинд-группам, очередям и command encoder’ам привыкнуть достаточно сложно. Порой не понимаешь, в какой класс лучше впихнуть очередную головоломную абстракцию наподобие WGPUBindGroupLayout или WGPURenderPassEncoder. Сложность в том, что сущности WebGPU сильно взаимосвязаны – одну не создашь без другой – и нужно заранее знать очень много информации, чтобы правильно проинициализировать конвейер.

Я почти сразу понял, что бинд-группы используются для раздельной передачи в шейдер ресурсов, обновляемых с различной частотой. Я делаю следующим образом:

Группа 0 – покадровые данные (видовая и проекционная матрицы)
Группа 1 – данные, обновляемые каждый проход (общие настройки сцены)
Группа 2 – свойства материала, текстуры
Группа 3 – свойства объекта (модельно-видовая матрица и др.)

Но нужно понимать, что этот лейаут не глобальный – он назначается для каждого пайплайна отдельно (поэтому и были придуманы эти пресловутые WGPUBindGroupLayout’ы). Вдобавок пайплайн в WebGPU неизменяемый – иными словами, если меняется какой-нибудь режим смешивания, то меняется вообще все. Такой подход может сильно обескуражить – за много лет пользования OpenGL его глобальное состояние стало для меня как родное! Тут вы не можете просто изменить конвейер так, как вам нужно – приходится создавать заранее несколько готовых пайплайнов на все случаи жизни и переключаться между ними функцией wgpuRenderPassEncoderSetPipeline. Способ управления пайплайнами сильно зависит от архитектуры вашего приложения, но в общем случае приходится городить достаточно сложный менеджер рендеринга, который создает проходы, задает им пайплайны, обновляет шейдерные ресурсы и подключает их в нужные моменты циклов перебора объектов сцены. Я до сих пор не уверен, что моя реализация этого менеджера годится для создания полноценного движка – надеюсь, что понимание придет в дальнейшем.

Буду ли я портировать Dagon на WebGPU? Отчасти – возможно, но перенести все функции движка с сохранением обратной совместимости, я думаю, нереально. Пока в этом и нет какой-то острой насущной необходимости, но начинать экспериментировать можно уже сейчас: API интересный, непривычный – рано или поздно привыкать все равно придется.