Hello WebGPU 🟢

Incomplete Translation

This is a community translation of the original English page, which is not fully translated yet. You are welcome to contribute!

Итоговый код: step001

WebGPU – это интерфейс для работы с графическим оборудованием (Render Hardware Interface, RHI), что означает, что это библиотека программирования, предназначенная для предоставления унифицированного интерфейса для различных графических аппаратных средств и операционных систем.

Для вашего C++ кода WebGPU — это всего лишь один заголовочный файл, который содержит все доступные процедуры и структуры данных: webgpu.h.

Однако при сборке программы ваш компилятор должен знать (на этапе финальной линковки), где найти фактическую реализацию этих функций. В отличие от нативных API, реализация WebGPU не предоставляется драйвером, поэтому мы должны явно указать её.

../../_images/rhi-vs-opengl.png

Интерфейс для работы с графическим оборудованием (RHI), такой как WebGPU, не предоставляется напрямую драйверами: нам нужно подключить библиотеку, которая реализует API поверх низкоуровневого API, поддерживаемого системой.

Установка WebGPU

Существует две основные реализации нативного заголовочного файла WebGPU:

  • wgpu-native, предоставляющая нативный интерфейс для библиотеки wgpu разработанной для Firefox.

  • Google’s Dawn, от Google, разработанная для Chrome.

../../_images/different-backend.png

Существует (как минимум) две реализации WebGPU, разработанные для двух основных веб-движков.

Эти две реализации всё ещё имеют некоторые различия, но они исчезнут по мере стабилизации спецификации WebGPU. Я стараюсь писать это руководство так, чтобы оно работало для обеих реализаций.

Чтобы упростить интеграцию любой из них в проект на CMake, я предоставляю репозиторий WebGPU-distribution, который позволяет выбрать один из следующих вариантов:

Слишком много вариантов? (Кликните)

Вы больше предпочитаете быструю сборку, чем подробные сообщениями об ошибках?

Да, быстрая сборка и не требуется подключение к Интернету при первой сборке

Выберите Вариант A (wgpu-native)!

Нет, я предпочитаю подробные сообщения об ошибках.

Выберите Вариант B (Dawn)!

Я не хочу выбирать.

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

Option A: Легкий wgpu-native

Поскольку wgpu-native написан на Rust, мы не можем легко собрать его с нуля, поэтому дистрибутив включает предварительно скомпилированные библиотеки:

Important

WIP: Используйте ссылку “для любой платформы” вместо платформо-специфичных, так как я ещё не автоматизировал их генерацию, и они обычно отстают от основной.

Note

Предварительно скомпилированные бинарники предоставляются самим проектом wgpu-native, так что вы можете им доверять. Единственное, что добавляет мой дистрибутив, — это файл CMakeLists.txt, который упрощает интеграцию.

Плюсы

  • Это самый легковесный вариант для сборки.

Минусы

  • Вы не собираете из исходников.

  • wgpu-native не предоставляет таких подробных отладочных сообщений, как Dawn.

Option B: Комфортный Dawn

Dawn предоставляет более подробные сообщения об ошибках, и поскольку она написана на C++, мы можем собрать её из исходников и глубже анализировать стек вызовов в случае сбоя:

Note

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

Плюсы

  • Dawn гораздо удобнее для разработки, так как предоставляет более подробные сообщения об ошибках.

  • В целом она опережает wgpu-native в плане реализации (но wgpu-native со временем догонит).

Минусы

  • Хотя я сократил количество дополнительных зависимостей, вам всё ещё нужно установить Python и git.

  • Дистрибутив загружает исходный код Dawn и его зависимости, поэтому при первой сборке вам потребуется подключение к Интернету.

  • Первоначальная сборка занимает значительно больше времени и занимает больше места на диске.

Note

На Linux ознакомьтесь с документацией по сборке Dawn для списка необходимых пакетов. По состоянию на 7 апреля 2024 года список следующий (для Ubuntu):

sudo apt-get install libxrandr-dev libxinerama-dev libxcursor-dev mesa-common-dev libx11-xcb-dev pkg-config nodejs npm

Option C: Гибкость обоих вариантов

В этом варианте мы включаем только несколько CMake-файлов в наш проект, которые затем динамически загружают либо wgpu-native, либо Dawn в зависимости от параметра конфигурации:

cmake -B build -DWEBGPU_BACKEND=WGPU
# или
cmake -B build -DWEBGPU_BACKEND=DAWN

Note

Сопровождающий код использует этот Вариант C.

Это предоставляется main веткой моего репозитория:

Tip

README этого репозитория содержит инструкции по добавлению его в ваш проект с помощью FetchContent_Declare. Если вы сделаете это, вы, скорее всего, будете использовать более новую версию Dawn или wgpu-native, чем та, против которой написано это руководство. В результате примеры из этой книги могут не скомпилироваться. Смотрите ниже, как загрузить версию, против которой написана эта книга.

Плюсы

  • Вы можете иметь две сборки одновременно: одну с Dawn и одну с wgpu-native.

Минусы

  • Это “мета-дистрибуция”, которая загружает нужный вам вариант на этапе конфигурации (т.е. при первом вызове cmake), поэтому вам потребуется подключение к Интернету и git в этот момент.

И, конечно, в зависимости от вашего выбора применяются плюсы и минусы Варианта A и Варианта B.

Интеграция

Независимо от выбранного дистрибутива, интеграция одинакова:

  1. Скачайте zip-архив выбранного вами варианта.

  2. Распакуйте его в корне проекта, должна появиться директория webgpu/, содержащая файл CMakeLists.txt и некоторые другие файлы (.dll или .so).

  3. Добавьте add_subdirectory(webgpu) в ваш CMakeLists.txt.

# Включаем директорию, чтобы определить цель 'webgpu'
add_subdirectory(webgpu)

Important

Имя ‘webgpu’ здесь обозначает директорию, где находится webgpu, поэтому должен быть файл webgpu/CMakeLists.txt. В противном случае это означает, что webgpu.zip не был распакован в правильную директорию; вы можете либо переместить его, либо адаптировать директиву add_subdirectory.

  1. Добавьте target webgpu как зависимость нашего приложения, используя команду target_link_libraries (после add_executable(App main.cpp)).

# Добавляем таргет 'webgpu' как зависимость нашего App
target_link_libraries(App PRIVATE webgpu)

Tip

На этот раз имя ‘webgpu’ – это один из таргетов определённых в webgpu/CMakeLists.txt с помощью вызова add_library(webgpu ...), оно не связано с именем директории.

Дополнительный шаг при использовании предварительно скомпилированных бинарников: вызовите функцию target_copy_webgpu_binaries(App) в конце CMakeLists.txt, это гарантирует, что .dll/.so файл, от которого зависит ваш бинарник во время выполнения, будет скопирован рядом с ним. При распространении вашего приложения убедитесь, что вы также распространяете этот динамический библиотечный файл.

# Бинарник приложения должен находить wgpu.dll или libwgpu.so во время выполнения,
# поэтому мы автоматически копируем его (он называется WGPU_RUNTIME_LIB в общем случае)
# рядом с бинарником.
target_copy_webgpu_binaries(App)

Note

В случае Dawn нет предварительно скомпилированных бинарников для копирования, но я всё равно определяю функцию target_copy_webgpu_binaries (она ничего не делает), чтобы вы могли использовать один и тот же CMakeLists с обоими дистрибутивами.

Тестирование установки

Чтобы протестировать установку, мы просто создаём экземпляр WebGPU, т.е. эквивалент navigator.gpu который мы могли бы получить в JavaScript. Затем мы проверяем его и уничтожаем.

Important

Убедитесь, что вы заинклудили <webgpu/webgpu.h> перед использованием любой функции или типа WebGPU!

// Includes
#include <webgpu/webgpu.h>
#include <iostream>

Дескрипторы и создание

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

// Мы создаём дескриптор
WGPUInstanceDescriptor desc = {};
desc.nextInChain = nullptr;

// Мы создаём экземпляр с использованием этого дескриптора
WGPUInstance instance = wgpuCreateInstance(&desc);

Note

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

Мы встречаем ещё одну идиому в структуре WGPUInstanceDescriptor: первое поле дескриптора всегда является указателем с именем nextInChain. Это универсальный способ для API позволить добавлять пользовательские расширения в будущем или возвращать несколько записей данных. Во многих случаях мы устанавливаем его в nullptr.

Проверка

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

Чтобы проверить, что объект действителен, мы можем просто сравнить его с nullptr, или использовать булевый оператор:

// Мы можем проверить, действительно ли создан экземпляр
if (!instance) {
    std::cerr << "Не удалось инициализировать WebGPU!" << std::endl;
    return 1;
}

// Отображаем объект (WGPUInstance  это простой указатель, он может быть
// скопирован без опасений о его размере).
std::cout << "WGPU instance: " << instance << std::endl;

Это должно вывести что-то вроде WGPU instance: 000001C0D2637720 при запуске.

Уничтожение и управление временем жизни

Все объекты, которые могут быть созданы с помощью WebGPU, должны быть в конечном итоге освобождены (released). Процедура, которая создаёт объект, всегда выглядит как wgpuCreateSomething, а её эквивалент для освобождения wgpuSomethingRelease.

Обратите внимание, что каждый объект внутренне содержит счётчик ссылок, и его освобождение освобождает связанную память только в том случае, если никакая другая часть вашего кода больше на него не ссылается (т.е. счётчик падает до 0):

WGPUSomething sth = wgpuCreateSomething(/* дескриптор */);

// Это означает "увеличить счётчик ссылок объекта sth на 1"
wgpuSomethingReference(sth);
// Теперь счётчик ссылок равен 2 (он устанавливается в 1 при создании)

// Это означает "уменьшить счётчик ссылок объекта sth на 1
// и если он достигнет 0, то уничтожить объект"
wgpuSomethingRelease(sth);
// Теперь счётчик ссылок вернулся к 1, объект всё ещё можно использовать

// Освобождаем снова
wgpuSomethingRelease(sth);
// Теперь счётчик ссылок равен 0, объект уничтожен и
// больше не должен использоваться!

В частности, нам нужно освободить глобальный экземпляр WebGPU:

// Мы освобождаем экземпляр WebGPU
wgpuInstanceRelease(instance);

Поведение, зависящее от реализации

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

// Если используется Dawn
#define WEBGPU_BACKEND_DAWN

// Если используется wgpu-native
#define WEBGPU_BACKEND_WGPU

// Если используется emscripten
#define WEBGPU_BACKEND_EMSCRIPTEN

Сборка для веба

Дистрибутивы WebGPU, перечисленные выше, полностью совместимы с Emscripten, если у вас возникнут проблемы с сборкой вашего приложения для веба, вы можете обратиться к специальному приложению.

Поскольку мы будем добавлять несколько опций, специфичных для веб-сборки, мы можем добавить раздел в конце нашего CMakeLists.txt:

# Опции, специфичные для Emscripten
if (EMSCRIPTEN)
    {{Опции специфичные для Emscripten}}
endif()

Пока что мы только изменяем расширение выходного файла, чтобы это была HTML-страница (а не модуль WebAssembly или JavaScript-библиотека):

# Генерируем полноценную веб-страницу, а не просто модуль WebAssembly
set_target_properties(App PROPERTIES SUFFIX ".html")

По какой-то причине дескриптор экземпляра должен быть null (что означает “использовать по умолчанию”) при использовании Emscripten, поэтому мы можем уже использовать наш макрос WEBGPU_BACKEND_EMSCRIPTEN:

// Мы создаём дескриптор
WGPUInstanceDescriptor desc = {};
desc.nextInChain = nullptr;

// Мы создаём экземпляр с использованием этого дескриптора
#ifdef WEBGPU_BACKEND_EMSCRIPTEN
WGPUInstance instance = wgpuCreateInstance(nullptr);
#else //  WEBGPU_BACKEND_EMSCRIPTEN
WGPUInstance instance = wgpuCreateInstance(&desc);
#endif //  WEBGPU_BACKEND_EMSCRIPTEN

Заключение

В этой главе мы настроили WebGPU и узнали, что доступны несколько бэкендов. Мы также увидели основные идиомы создания и уничтожения объектов, которые будут использоваться постоянно в API WebGPU!

Итоговый код: step001