SDL3 GPU?

Мир эксплицитных графических API переживает бурный период фрагментации. Это похоже на 90-е, когда одновременно были Glide, Direct3D 5-6, OpenGL 1.x и программные рендеры, только сейчас все куда сложнее, потому что и железо намного сложнее.

Есть Vulkan, который де-юре является современной заменой OpenGL — в том смысле, что работает на тех же платформах. Де-факто, я считаю, не является, так как далеко не каждое существующее приложение OpenGL реально перенести на Vulkan без тотальной переделки. На macOS его нет, как, собственно, и актуальных версий OpenGL. Я считаю, что на чистом Vulkan писать опасно для ментального здоровья — поверх него обязательно нужна абстракция.

Есть Direct3D 12, который работает только под Windows и на XBox. Я его традиционно в расчет не беру, поскольку писать windows-онли приложения в наши дни уже моветон. Как минимум, поддержка Linux нужна обязательно.

Есть Metal, который аналогично существует только в экосистеме Apple. Писать на нем напрямую тоже не надо, если только вы не создаете мак-эксклюзивы.

Есть WebGPU, который, казалось, должен был исправить ситуацию, объединив предыдущую троицу под единым API. С точки зрения дизайна эта задача с грехом пополам продвигается, хотя и медленно, но вот реализации пока оставляют желать лучшего. wgpu от Mozilla написан на Rust и работает как-то не очень стабильно — до сих пор утечки памяти в простейших приложениях. Ну и WGSL поделил сообщество графических разработчиков на два лагеря — одним ок, другие не признают ничего, кроме SPIR-V. Язык и правда слишком специфический, с сильным уклоном в Rust, да и в целом от WebGPU ощущение такое, что это чисто растоманский проект, не учитывающий другие языки и парадигмы программирования. И мне лично жутко не нравится его модель биндинга ресурсов. Все эти bind group layout’ы неудобные… API пилят уже пять лет, а на нем все еще страшно писать реальный код — то и дело что-то ломается непредсказуемым образом.

Наконец, появился GPU API в SDL3. Надо сказать, библиотека вообще замечательная, она уже много лет делает возможной поддержку единой кодовой базы в играх (да и не только в играх) под Windows и Linux. То, что в третьей версии появился аж целый графический API — это нехилый такой аргумент перейти на нее как можно быстрее. Мне, конечно, было очень интересно сравнить этот API с WebGPU, и данный пост я пишу как раз с этой целью.

Но прежде — небольшое резюме. Что вся эта ситуация означает для инди-разработчика? К сожалению, ничего хорошего. Сегодня уже как минимум пять мейнстримных графических API, а завтра будет сколько? Изобретут еще пять, или в результате конкуренции останутся два, как в нулевые? Кто кому уступит? Непонятно, что выбрать, потому что не хочется вкладываться в технологию, которая может сгинуть. Корпорации, разрабатывающие AAA-продукты, обладают ресурсами для поддержки всех технологий, с них не убудет, но для пет-проектов и альтернативных движков, таких, как мой, это недостижимо — приходится выбирать что-то одно.

SDL3 GPU vs WebGPU

Главное, чем подкупает SDL3 GPU — минималистичность. Инициалирующие структуры не выглядят, как монстры а ля объекты JavaScript. Привязка рендера к окну — это всего лишь один вызов функции SDL_ClaimWindowForGPUDevice, а не целая эпопея с платформоспецифичными функциями, как в WebGPU. Сказывается, что SDL полностью берет управление окном и контекстом на себя. Также тут нет управления очередью и почти не нужно беспокоиться о синхронизации: вы просто создаете командные буферы под ваши задачи и отправляете их на исполнение функцией SDL_SubmitGPUCommandBuffer. Командный буфер, как и в других низкоуровневых API, — это список команд видеопроцессору выполнить какую-либо операцию (создать ресурс, скопировать данные, нарисовать примитив).

Передача данных в видеопамять осуществляется при помощи так называемых трансфер-буферов (SDL_GPUTransferBuffer). Они отображаются в системную память, так что вы можете копировать в них данные вызовом memcpy или любым другим удобным вам способом. Когда трансфер-буфер готов, он используется как источник данных для прохода копирования (SDL_GPUCopyPass), который заносится в командный буфер. В общем, основную идею эксплицитных API (сократить частоту синхронизаций CPU и GPU до необходимого минимума) тут постарались сохранить, и это чувствуется.

Отдельная головная боль WebGPU — управление свопчейном, и тут SDL3 GPU снова устраняет все сложности. Вы просто получаете текущий задний буфер функцией SDL_WaitAndAcquireGPUSwapchainTexture, обновляете ваш color target и используете его для вызова SDL_BeginGPURenderPass — и все!

Единого шейдерного языка в SDL3 GPU нет — он зависит от выбранного бэкенда. Если вы выбрали Vulkan, то это SPIR-V, если Direct3D 12 — DXIL, если Metal — MSL. Так что для мультитаргетного движка вам обязательно понадобится тулчейн трансляции шейдеров, например SPIRV-Cross. Это, пожалуй, единственная сложность, в особенности если вы не используете C/C++. Но для D я уже решил эту проблему, написав биндинги bindbc-glslang и bindbc-spirvcross. Процесс ручной компиляции шейдеров может показаться немного мудреным, но на самом деле это вещь из разряда «написал и забыл». Использование SPIR-V в качестве внутреннего промежуточного представления дает невиданную свободу — можно писать на привычном GLSL, а можно попробовать что-то новое.

Отдельная песня — привязка ресурсов к шейдерам. В SDL эту задачу решили элегантно, все 4 дескриптор-сета жестко распределены:

  • Сет 0 — текстуры/сэмплеры и SSBO вершинной стадии
  • Сет 1 — UBO вершинной стадии
  • Сет 2 — текстуры/сэмплеры и SSBO фрагментной стадии
  • Сет 3 — UBO фрагментной стадии

Биндинг-поинты вы задаете сами (в шейдере и программном коде). Единственное, что важно знать перед созданием шейдера — сколько именно сэмплеров, SSBO и UBO в нем используется. Это может поставить в тупик, так как усложняет абстракцию в движке, но даю лайфхак: эту информацию предоставляет все тот же SPIRV-Cross (см. spvc_resources_get_resource_list_for_type). Я считаю, по сравнению с WebGPU это просто рай.

Для рендеринга используются объекты SDL_GPUGraphicsPipeline (которые задают параметры растеризатора) и SDL_GPURenderPass (к которому биндятся ресурсы). В deferred-движке их удобно объединить в одну абстракцию RenderPass. Пайплайны традиционно неизменяемые, так что после OpenGL придется привыкать к новым реалиям и оптимизировать рендер.

Вот, вроде бы, и все. Я пока не затронул compute, это тема для отдельной статьи. Разработчики SDL приятно удивили, выкатив отличный API за сравнительно короткие сроки — уже сейчас на нем вполне можно делать серьезные проекты. Хочется верить, что он не заглохнет и будет развиваться.

Обновления

Улучшения в 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» и бумажных изданиях — ссылки на них поставить затруднительно.

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, как я, слегка непривычно, но жить можно 🙂

Репозиторий

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

Dagon 0.9.0 и dlib 0.15.0

На днях вышли новые версии Dagon и dlib — 0.9.0 и 0.15.0 соответственно. Релиз Dagon — самый крупный со времени портирования движка на современный OpenGL: он содержит 190 коммитов, практически весь рендерер был переписан заново. Вот краткий список изменений:

  • Состоялся переход с прямого рендеринга (forward) на отложенный (deferred). Это должно было рано или поздно произойти, все современные движки в той или иной мере используют отложенные эффекты. В Dagon этот рефакторинг серьезно улучшил производительность и позволил полноценно реализовать SSAO. Количество динамических источников света теперь ограничено только fillrate’ом видеокарты. Ценой стало повышенное потребление видеопамяти, также при отложенном рендеринге невозможно эффективно реализовать прозрачность, поэтому все прозрачные объекты рендерятся в прямом fallback-режиме, и для них не учитываются точечные источники света.
  • Система шейдеров была переписана с нуля. Старая система с бэкендами материалов была заменена на шейдеры, все избыточные классы материалов были объединены в один, создавать новые материалы и передавать шейдерам параметры стало значительно проще. Все шейдеры используют GLSL 4.0 Core, ветвление в шейдерах было заменено на uniform-подпрограммы, что повысило производительность и сделало код более читаемым. Материалы с пользовательскими шейдерами также рендерятся в прямом режиме.
  • Была серьезно улучшена система частиц: появилась поддержка мягких частиц, освещения, отбрасывания теней. Также теперь можно создавать несколько эмиттеров в каждой системе. Опционально в качестве частиц можно рендерить любые объекты вместо биллбордов.
  • Добавлена поддержка Screen space ambient occlusion (SSAO).
  • Добавлены шейдер воды по методу Себастьяна Лагарда и модель неба по Рэлею (экспериментально).
  • Dagon теперь использует BindBC вместо Derelict.
  • Добавлена поддержка автоматического деплоя: при каждой сборке Dub копирует в проект все необходимые файлы для запуска, включая DLL’ки под Windows.

Полный список изменений смотрите на странице релиза. Также было обновлено и демонстрационное приложение.

Изменения в dlib по большей части носят исправляющий и косметический характер: я решил постепенно избавиться от устаревших компонентов, в связи с чем следующие модули пометил как deprecated:

  • dlib.image.parallel
  • dlib.math.fixed
  • dlib.functional.quantifiers
  • функции map, filter, reduce в dlib.functional.range.

Следующие модули были удалены:

  • dlib.container.aarray
  • dlib.math.affine
  • dlib.core.fiber (временно перенесен в отдельную ветку до завершения windows-порта)

Нововведения включают:

  • dlib.text.unmanagedstring — альтернативная реализация строк, не использующая сборщик мусора
  • Улучшенные декодеры текстовых кодировок, модуль dlib.text.encodings.
  • Также dlib теперь может быть собран компилятором GDC (за исключением модуля dlib.math.sse, который в этом случае не будет доступен).

Полный список изменений — на странице релиза.

Интерполяция на основе сигмоиды

Для одного из шейдеров на GLSL мне потребовалась «умная» интерполяция цветов с возможностью изменять резкость перехода от одного значения к другому — от полностью плавного (линейного) до дискретного. В итоге получилась вот такая функция, которую я вывел на основе рациональной сигмоиды — может быть, кому-то пригодится:

float sigmoid(float x, float k)
{
    return (x + x * k - k * 0.5 - 0.5) / 
        (abs(x * k * 4.0 - k * 2.0) - k + 1.0) + 0.5;
}

При k = 0 функция обращается в линейную, при k = 1 — разрывается в точке 0,5. Вы можете увидеть, как это работает, при помощи интерактивного графика на Desmos: https://www.desmos.com/calculator/s0cwcrtzvs.

Результат этой функции передается в привычный mix – то есть, вместо mix(c1, c2, t) пишем mix(c1, c2, sigmoid(t, k)). Получится, например, такое:

(градиенты гамма-скорректированы)

Тени на шейдерах

Наконец-то переписал тени с использованием FBO и GLSL:

Следующим шагом будет variance shadow mapping (VSM).

Bump mapping в Atrium

В движке Atrium реализован шейдерный эффект рельефа с использованием карт нормалей (Normal mapping). Представленная демонстрация показывает эффект в действии для динамических боксов, которыми игрок может манипулировать. Планируется также поддержка Parallax mapping.

Сборка для Windows (2,68 МБ)
Сборка для Linux (3,1 МБ)

Шейдеры написаны на GLSL и требуют наличия расширения ARB_shading_language_100 (или OpenGL 2.0), но это не является критической необходимостью — игра будет работать и со старыми видеокартами без поддержки шейдеров.