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

Перлы из сабреддита /r/vulkan

К Vulkan я испытываю если не неприязнь, то во всяком случае изрядный скепсис. Это API для машин, а не для людей. Писать на нем напрямую — это ад, и веских причин пытать себя вулкановскими ужасами (во всяком случае, в инди-разработке) я за эти десять лет так и не увидел. Оптимизация CPU-bound частей рендера? Вы не Epic Games, чтобы этим заниматься — ресурсов не хватит. DLSS, трассировка лучей? В реальности все эти навороты в играх отключают первым делом, чтобы играть, а не смотреть слайдшоу, ибо видеокарты у большинства далеко не топовые.

Но вот что забавно: вулканисты сами признают, что использовать Vulkan необязательно! Если не верите, откройте Reddit. Вот что я там нашел:

pls use vk-bootstrap, dynamic rendering, VMA in big 2026. Don’t do raw vulkan. You will lose motivation sooner.

То есть, без разнообразных обвязок и прибамбасов писать на Vulkan принципиально не рекомендуется! Это прям классно — напомнило времена GLU, GLUT и тому подобного. Я и сам пробовал много всяких компромиссных вариантов — вкатывался в WebGPU, щупал LLGL и GPU API в SDL3, но везде одна и та же жопа в разных ракурсах: нужны дополнительные инструменты, как минимум для компиляции шейдеров и SPIR-V рефлексии. Оверхед по расходу памяти от всех этих обвязок немаленький, в WebGPU вообще память течет — привет растоманам. И я уж молчу про то, что обвязки систематически ломают обратную совместимость, так что писать на них — то еще удовольствие.

А вот это вообще анекдот:

Your options are basically:
— Learn Vulkan, but use AI to fill in the boilerplate.
— Use CUDA instead of Vulkan.

Без ИИ сегодня, конечно, вообще никуда 😂 А графическая разработка — такая уж область, ChatGPT ногу сломит. Ну и наконец:

OpenGL is perfectly viable in 2026 for hobby programmers and you’ll probably even get better performance out of it than not knowing how to use Vulkan and using it anyway. Hardware support is also completely fine. If you want to learn Vulkan, go do it. If you don’t, there’s better alternatives.

Занавес. Directed by Robert B. Weide.

Обновления

bindbc-wgpu 0.17.0 – синхронизация с wgpu-native 0.17.0. Биндинг теперь не предоставляет готовые сборки библиотеки, их нужно устанавливать самостоятельно (можно скачать со страницы релиза wgpu-native). Также теперь все сигнатуры функций WebGPU хранятся в модулях bindbc.wgpu.types и bindbc.wgpu.types2, в bindbc.wgpu.funcs — только символы. Это позволило сократить кодовую базу и упростить интроспекцию в bindbc.wgpu.loader.

Сайт https://timurgafarov.ru, который раньше был альтернативным адресом для сайта PixelPerfect, теперь перенаправляет на этот блог https://gamedev.timurgafarov.ru. Актуальное портфолио со ссылками на мои проекты вы можете найти на https://gecko0307.github.io.

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 интересный, непривычный — рано или поздно привыкать все равно придется.

Первое знакомство с WGSL

У тех, кто работает с низкоуровневой графикой, сегодня на слуху WebGPU — новый кроссплатформенный API для доступа к возможностям современных видеокарт. WebGPU призван объединить Vulkan, Metal и D3D12 под унифицированным набором функций и станет не просто веб-стандартом, но и, в перспективе, неплохой заменой OpenGL: реализации этого API уже существуют в виде рабочих прототипов wgpu-native от Mozilla и Dawn от Google — любой может использовать их в своих собственных приложениях.

WebGPU имеет сравнительно простую архитектуру, доступную для понимания «простыми смертными» практически с первого прочтения заголовочного файла. Единственной проблемой до недавнего времени было отсутствие консенсуса по шейдерному языку — существующие реализации WebGPU использовали двоичное промежуточное представление SPIR-V от Khronos, а Apple настаивала на текстовом языке на основе WSL. Компромиссом стал WGSL (WebGPU Shading Language), высокоуровневый язык со строго определенной семантикой и буквальной трансляцией в/из SPIR-V. Многие разработчики оказались недовольны, так как SPIR-V уже успел стать привычным решением и оброс инструментами — сегодня можно компилировать в SPIR-V код на всех языках предыдущих поколений. Однако я вижу больше преимуществ, чем недостатков — перечислю некоторые из них.

  • Использование SPIR-V усложняет жизнь при создании игрового движка, требует внедрения дополнительной стадии компиляции шейдеров на стороне разработчика. Референсным компилятором шейдеров считатеся GLSLang от Khronos, но его довольно трудно встроить в приложение как библиотеку, особенно если вы не пишете на C++ — приходится использовать GLSLang как приложение, и это усложняет тулчейн разработки, если нужна кроссплатформенность. Встроенный в API высокоуровневый язык решает эту проблему.
  • WGSL разрабатывается как текстовый аналог SPIR-V — они имеют общий набор возможностей. Это значит, что не будет повторения ситуации с GLSL, когда язык по-разному обрабатывается в компиляторах от различных поставщиков. Сохраняется главное преимущество SPIR-V при высоком удобстве использования.
  • Vulkan-диалект GLSL 4.60, являющийся де-факто стандартным языком под SPIR-V, имеет множество костылей и архаизмов — у WGSL более продуманный синтаксис, лишенный неявности и многозначности.

Синтаксис WGSL имеет много общего с Rust, особенно заголовки функций:

fn someFunc(x: i32) -> i32 {
    //...
}

Типы объявляются через двоеточие после идентификатора, переменные — при помощи ключевого слова let, константность подразумевается по умолчанию. Для изменяемых переменных есть ключевое слово var. Система типов также пришла из вселенной Rust. Векторные типы имеют форму vec4<f32> (вместо простого vec4), что позволяет явным образом указать битовость используемых чисел. При этом можно объявить type vec4f = vec4<f32>; и писать коротко, если вам так привычнее.

Кстати, очень порадовало, что есть вывод типов — можно не указывать тип переменной, если она тут же инициализируется:

let a = vec4<f32>(0.0, 0.0, 0.0, 1.0);

Вместо ключевого слова layout — нотация с использованием двойных квадратных скобок, внутри которых записываются атрибуты location и др.:

[[location(0)]] position: vec4<f32>;

Встроенные переменные конвейера обозначаются атрибутом builtin, что весьма удобно при объявлении структур для хранения промежуточных результатов:

struct VertexOutput
{
    [[builtin(position)]] position: vec4<f32>;
    [[location(0)]] color: vec4<f32>;
};

Сравните это с GLSL, где для встроенных переменных используется зарезервированный префикс gl_.

Структуры, являющиеся uniform-блоками, помечаются атрибутом block:

[[block]] struct Uniforms {
    //...
};

Прямым аналогом вулкановских set и binding являются атрибуты group и binding.

Vulkan/GLSL:

layout(set=0, binding=0) uniform Uniforms uniforms;

WGSL:

[[group(0), binding(0)]] var uniforms: Uniforms;

Программы на WGSL можно не разделять на два текста — вершинный и фрагментный шейдеры можно хранить в одном файле и, таким образом, использовать общие объявления. Для этого используется атрибут stage. Названия самих входных точек могут быть произвольными, но чаще всего в примерах используют vs_main и fs_main.

[[stage(vertex)]]
fn vs_main() -> VertexOutput
{
    //...
}

[[stage(fragment)]]
fn fs_main(input: FragmentInput) -> [[location(0)]] vec4<f32>
{
    //...
}

Очень непривычно в WGSL записываются циклы:

var i = i32(0);
loop {
    break if (i == 5);
    //...
    continuing {
        i = i + 1;
    }
}

Впрочем, на момент написания статьи обсуждается возможность поддержки классического for.

Подведу итог: с первого взгляда WGSL кажется хорошим решением давней проблемы с языками шейдеров. Высокоуровневое представление SPIR-V — это отличная идея. Непривычный синтаксис и конструкции со спорным юзабилити могут усложнить портирование на WGSL готовых шейдеров, но в целом впечатление от языка весьма позитивное.

Итоги 2019 года

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

  • Блог DLangGamedev переехал на новый адрес и движок: https://gamedev.timurgafarov.ru.
  • Был значительно улучшен движок Dagon: добавлена поддержка декалей, трубчатых источников света и «фонариков», кубических карт окружения. Благодаря сторонним разработчикам появилась поддержка рендеринга ландшафтов, в том числе процедурных с использованием шума OpenSimplex, а также интеграция GUI-тулкита Nuklear. Были полностью переписаны рендер и система постобработки в Dagon, упорядочена структура модулей движка, внесено множество оптимизаций производительности, реализован эффект объемного рассеяния света в атмосфере (volumetric light scattering) для направленных источников света. Mateusz Muszyński на основе Dagon и Nuklear написал клон Sokoban с редактором уровней.
  • Я начал работу по интеграции физического движка Newton Dynamics в Dagon в рамках проекта dagon-newton и биндинга bindbc-newton. Newton был выбран как наиболее функциональный физический движок с интерфейсом C.
  • Вышли dlib 0.16.0 и 0.17.0. Библиотека постепенно приближается к релизу версии 1.0.
  • Я написал bindbc-wgpu, биндинг к графической библиотеке wgpu.

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

  • Появление WebGPU, нового веб-стандарта для высокопроизводительной графики.
  • Выход Blender 2.80 c новым вьюпорт-движком Eevee.
  • Открытие кода Mimalloc, быстрого и компактного аллокатора памяти от Microsoft.