Создайте приложение с помощью WebGPU

Франсуа Бофор
François Beaufort

Для веб-разработчиков WebGPU — это API веб-графики, который обеспечивает унифицированный и быстрый доступ к графическим процессорам. WebGPU предоставляет современные аппаратные возможности и позволяет выполнять операции рендеринга и вычислений на графическом процессоре, аналогично Direct3D 12, Metal и Vulkan.

Хотя это правда, эта история неполная. WebGPU — это результат совместных усилий крупных компаний, таких как Apple, Google, Intel, Mozilla и Microsoft. Некоторые из них осознали , что WebGPU может быть не просто Javascript API, а кроссплатформенным графическим API для разработчиков в разных экосистемах, помимо Интернета.

Для реализации основного варианта использования в Chrome 113 был представлен API JavaScript. Однако параллельно с ним был разработан еще один важный проект: C API webgpu.h . В этом заголовочном файле C перечислены все доступные процедуры и структуры данных WebGPU. Он служит уровнем абстракции аппаратного обеспечения, не зависящим от платформы, позволяя создавать приложения для конкретной платформы, предоставляя согласованный интерфейс для разных платформ.

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

Снимок экрана: красный треугольник, созданный с помощью WebGPU, в окне браузера и на рабочем столе в macOS.
Один и тот же треугольник, созданный с помощью WebGPU, в окне браузера и на рабочем столе.

Как это работает?

Чтобы увидеть готовое приложение, посетите репозиторий кроссплатформенных приложений WebGPU .

Приложение представляет собой минималистичный пример C++, который показывает, как использовать WebGPU для создания настольных и веб-приложений из единой базы кода. Под капотом он использует webgpu.h WebGPU в качестве независимого от платформы слоя абстракции оборудования через оболочку C++ под названием webgpu_cpp.h .

В Интернете приложение создано на основе Emscripten , который имеет привязки , реализующие webgpu.h поверх API JavaScript. На определенных платформах, таких как macOS или Windows, этот проект может быть построен на основе Dawn , кроссплатформенной реализации WebGPU Chromium. Стоит упомянуть wgpu-native , реализацию webgpu.h на Rust, которая также существует, но не используется в этом документе.

Начать

Для начала вам понадобится компилятор C++ и CMake для стандартной обработки кроссплатформенных сборок. В отдельной папке создайте исходный файл main.cpp и файл сборки CMakeLists.txt .

На данный момент файл main.cpp должен содержать пустую функцию main() .

int main() {}

Файл CMakeLists.txt содержит основную информацию о проекте. Последняя строка указывает, что имя исполняемого файла — «app», а его исходный код — main.cpp .

cmake_minimum_required(VERSION 3.13) # CMake version check
project
(app)                         # Create project "app"
set(CMAKE_CXX_STANDARD 20)           # Enable C++20 standard

add_executable
(app "main.cpp")

Запустите cmake -B build , чтобы создать файлы сборки в подпапке «build/», и cmake --build build чтобы фактически собрать приложение и сгенерировать исполняемый файл.

# Build the app with CMake.
$ cmake
-B build && cmake --build build

# Run the app.
$
./build/app

Приложение запускается, но результатов пока нет, так как вам нужен способ рисовать объекты на экране.

Получить рассвет

Чтобы нарисовать треугольник, вы можете воспользоваться Dawn , кроссплатформенной реализацией WebGPU Chromium. Сюда входит библиотека GLFW C++ для рисования на экране. Один из способов загрузить Dawn — добавить его в качестве подмодуля git в свой репозиторий. Следующие команды извлекают его из подпапки «dawn/».

$ git init
$ git submodule add https
://dawn.googlesource.com/dawn

Затем добавьте в файл CMakeLists.txt следующее:

  • Параметр CMake DAWN_FETCH_DEPENDENCIES извлекает все зависимости Dawn.
  • Папка dawn/ sub включена в цель.
  • Ваше приложение будет зависеть от целей dawn::webgpu_dawn , glfw и webgpu_glfw , чтобы вы могли использовать их в файле main.cpp позже.

set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory
("dawn" EXCLUDE_FROM_ALL)
target_link_libraries
(app PRIVATE dawn::webgpu_dawn glfw webgpu_glfw)

Открыть окно

Теперь, когда Dawn доступен, используйте GLFW, чтобы рисовать объекты на экране. Эта библиотека, включенная в состав webgpu_glfw для удобства, позволяет писать код, не зависящий от платформы, для управления окнами.

Чтобы открыть окно с именем «Окно WebGPU» с разрешением 512x512, обновите файл main.cpp , как показано ниже. Обратите внимание, что glfwWindowHint() используется здесь для запроса конкретной инициализации графического API.

#include <GLFW/glfw3.h>

const uint32_t kWidth = 512;
const uint32_t kHeight = 512;

void Start() {
 
if (!glfwInit()) {
   
return;
 
}

  glfwWindowHint
(GLFW_CLIENT_API, GLFW_NO_API);
 
GLFWwindow* window =
      glfwCreateWindow
(kWidth, kHeight, "WebGPU window", nullptr, nullptr);

 
while (!glfwWindowShouldClose(window)) {
    glfwPollEvents
();
   
// TODO: Render a triangle using WebGPU.
 
}
}

int main() {
 
Start();
}

Пересборка приложения и запуск его в прежнем режиме теперь приводят к пустому окну. Вы прогрессируете!

Скриншот пустого окна macOS.
Пустое окно.

Получить устройство с графическим процессором

В JavaScript navigator.gpu — это точка входа для доступа к графическому процессору. В C++ вам необходимо вручную создать переменную wgpu::Instance , которая будет использоваться для той же цели. Для удобства объявите instance в верхней части файла main.cpp и вызовите wgpu::CreateInstance() внутри main() .


#include <webgpu/webgpu_cpp.h>

wgpu
::Instance instance;


int main() {
  instance
= wgpu::CreateInstance();
 
Start();
}

Доступ к графическому процессору является асинхронным из-за формы API JavaScript. В C++ создайте две вспомогательные функции GetAdapter() и GetDevice() , которые соответственно возвращают функцию обратного вызова с помощью wgpu::Adapter и wgpu::Device .

#include <iostream>


void GetAdapter(void (*callback)(wgpu::Adapter)) {
  instance
.RequestAdapter(
     
nullptr,
     
[](WGPURequestAdapterStatus status, WGPUAdapter cAdapter,
         
const char* message, void* userdata) {
       
if (status != WGPURequestAdapterStatus_Success) {
          exit
(0);
       
}
        wgpu
::Adapter adapter = wgpu::Adapter::Acquire(cAdapter);
       
reinterpret_cast<void (*)(wgpu::Adapter)>(userdata)(adapter);
 
}, reinterpret_cast<void*>(callback));
}

void GetDevice(void (*callback)(wgpu::Device)) {
  adapter
.RequestDevice(
     
nullptr,
     
[](WGPURequestDeviceStatus status, WGPUDevice cDevice,
         
const char* message, void* userdata) {
        wgpu
::Device device = wgpu::Device::Acquire(cDevice);
        device
.SetUncapturedErrorCallback(
           
[](WGPUErrorType type, const char* message, void* userdata) {
              std
::cout << "Error: " << type << " - message: " << message;
           
},
           
nullptr);
       
reinterpret_cast<void (*)(wgpu::Device)>(userdata)(device);
 
}, reinterpret_cast<void*>(callback));
}

Для облегчения доступа объявите две переменные wgpu::Adapter и wgpu::Device в верхней части файла main.cpp . Обновите функцию main() для вызова GetAdapter() и назначьте обратный вызов ее результата adapter , затем вызовите GetDevice() и назначьте обратный вызов результата device перед вызовом Start() .

wgpu::Adapter adapter;
wgpu
::Device device;


int main() {
  instance
= wgpu::CreateInstance();
 
GetAdapter([](wgpu::Adapter a) {
    adapter
= a;
   
GetDevice([](wgpu::Device d) {
      device
= d;
     
Start();
   
});
 
});
}

Нарисуйте треугольник

Цепочка обмена не отображается в API JavaScript, поскольку об этом заботится браузер. В C++ его необходимо создать вручную. Еще раз, для удобства, объявите переменную wgpu::Surface в верхней части файла main.cpp . Сразу после создания окна GLFW в Start() вызовите удобную функцию wgpu::glfw::CreateSurfaceForWindow() чтобы создать wgpu::Surface (похожий на холст HTML) и настройте его, вызвав новую вспомогательную функцию ConfigureSurface() в InitGraphics() . Вам также необходимо вызвать surface.Present() , чтобы представить следующую текстуру в цикле while. Это не имеет видимого эффекта, поскольку рендеринг еще не происходит.

#include <webgpu/webgpu_glfw.h>


wgpu
::Surface surface;
wgpu
::TextureFormat format;

void ConfigureSurface() {
  wgpu
::SurfaceCapabilities capabilities;
  surface
.GetCapabilities(adapter, &capabilities);
  format
= capabilities.formats[0];

  wgpu
::SurfaceConfiguration config{
     
.device = device,
     
.format = format,
     
.width = kWidth,
     
.height = kHeight};
  surface
.Configure(&config);
}

void InitGraphics() {
 
ConfigureSurface();
}

void Render() {
 
// TODO: Render a triangle using WebGPU.
}

void Start() {
 

  surface
= wgpu::glfw::CreateSurfaceForWindow(instance, window);

 
InitGraphics();

 
while (!glfwWindowShouldClose(window)) {
    glfwPollEvents
();
   
Render();
    surface
.Present();
    instance
.ProcessEvents();
 
}
}

Сейчас самое время создать конвейер рендеринга с помощью приведенного ниже кода. Для облегчения доступа объявите переменную wgpu::RenderPipeline в верхней части файла main.cpp и вызовите вспомогательную функцию CreateRenderPipeline() в InitGraphics() .

wgpu::RenderPipeline pipeline;


const char shaderCode[] = R"(
   
@vertex fn vertexMain(@builtin(vertex_index) i : u32) ->
     
@builtin(position) vec4f {
       
const pos = array(vec2f(0, 1), vec2f(-1, -1), vec2f(1, -1));
       
return vec4f(pos[i], 0, 1);
   
}
   
@fragment fn fragmentMain() -> @location(0) vec4f {
       
return vec4f(1, 0, 0, 1);
   
}
)";

void CreateRenderPipeline() {
  wgpu
::ShaderModuleWGSLDescriptor wgslDesc{};
  wgslDesc
.code = shaderCode;

  wgpu
::ShaderModuleDescriptor shaderModuleDescriptor{
     
.nextInChain = &wgslDesc};
  wgpu
::ShaderModule shaderModule =
      device
.CreateShaderModule(&shaderModuleDescriptor);

  wgpu
::ColorTargetState colorTargetState{.format = format};

  wgpu
::FragmentState fragmentState{.module = shaderModule,
                                   
.targetCount = 1,
                                   
.targets = &colorTargetState};

  wgpu
::RenderPipelineDescriptor descriptor{
     
.vertex = {.module = shaderModule},
     
.fragment = &fragmentState};
  pipeline
= device.CreateRenderPipeline(&descriptor);
}

void InitGraphics() {
 

 
CreateRenderPipeline();
}

Наконец, отправьте команды рендеринга на графический процессор с помощью функции Render() вызывающей каждый кадр.

void Render() {
  wgpu
::SurfaceTexture surfaceTexture;
  surface
.GetCurrentTexture(&surfaceTexture);

  wgpu
::RenderPassColorAttachment attachment{
     
.view = surfaceTexture.texture.CreateView(),
     
.loadOp = wgpu::LoadOp::Clear,
     
.storeOp = wgpu::StoreOp::Store};

  wgpu
::RenderPassDescriptor renderpass{.colorAttachmentCount = 1,
                                       
.colorAttachments = &attachment};

  wgpu
::CommandEncoder encoder = device.CreateCommandEncoder();
  wgpu
::RenderPassEncoder pass = encoder.BeginRenderPass(&renderpass);
  pass
.SetPipeline(pipeline);
  pass
.Draw(3);
  pass
.End();
  wgpu
::CommandBuffer commands = encoder.Finish();
  device
.GetQueue().Submit(1, &commands);
}

Пересборка приложения с помощью CMake и его запуск теперь приводят к появлению долгожданного красного треугольника в окне! Отдохните – вы это заслужили.

Скриншот красного треугольника в окне macOS.
Красный треугольник в окне рабочего стола.

Компилировать в WebAssembly

Давайте теперь посмотрим на минимальные изменения, необходимые для настройки существующей кодовой базы для рисования этого красного треугольника в окне браузера. Опять же, приложение создано на основе Emscripten , инструмента для компиляции программ C/C++ в WebAssembly, который имеет привязки, реализующие webgpu.h поверх API JavaScript.

Обновить настройки CMake

После установки Emscripten обновите файл сборки CMakeLists.txt следующим образом. Выделенный код — единственное, что вам нужно изменить.

  • set_target_properties используется для автоматического добавления расширения файла «html» к целевому файлу. Другими словами, вы создадите файл «app.html».
  • Опция ссылки на приложение USE_WEBGPU необходима для включения поддержки WebGPU в Emscripten. Без него ваш файл main.cpp не сможет получить доступ к файлу webgpu/webgpu_cpp.h .
  • Здесь также требуется опция ссылки на приложение USE_GLFW , чтобы вы могли повторно использовать свой код GLFW.
cmake_minimum_required(VERSION 3.13) # CMake version check
project
(app)                         # Create project "app"
set(CMAKE_CXX_STANDARD 20)           # Enable C++20 standard

add_executable
(app "main.cpp")

if(EMSCRIPTEN)
  set_target_properties
(app PROPERTIES SUFFIX ".html")
  target_link_options
(app PRIVATE "-sUSE_WEBGPU=1" "-sUSE_GLFW=3")
else()

 
set(DAWN_FETCH_DEPENDENCIES ON)
  add_subdirectory
("dawn" EXCLUDE_FROM_ALL)
  target_link_libraries
(app PRIVATE dawn::webgpu_dawn glfw webgpu_glfw)
endif()

Обновите код

В Emscripten для создания wgpu::surface требуется элемент холста HTML. Для этого вызовите instance.CreateSurface() и укажите селектор #canvas , соответствующий соответствующему элементу HTML-холста на HTML-странице, созданной Emscripten.

Вместо использования цикла while вызовите emscripten_set_main_loop(Render) , чтобы убедиться, что функция Render() вызывается с правильной плавной скоростью, которая правильно согласуется с браузером и монитором.

#include <GLFW/glfw3.h>
#include <webgpu/webgpu_cpp.h>
#include <iostream>
#if defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h>
#else

#include <webgpu/webgpu_glfw.h>
#endif
void Start() {
 
if (!glfwInit()) {
   
return;
 
}

  glfwWindowHint
(GLFW_CLIENT_API, GLFW_NO_API);
 
GLFWwindow* window =
      glfwCreateWindow
(kWidth, kHeight, "WebGPU window", nullptr, nullptr);

#if defined(__EMSCRIPTEN__)
  wgpu
::SurfaceDescriptorFromCanvasHTMLSelector canvasDesc{};
  canvasDesc
.selector = "#canvas";

  wgpu
::SurfaceDescriptor surfaceDesc{.nextInChain = &canvasDesc};
  surface
= instance.CreateSurface(&surfaceDesc);
#else

  surface
= wgpu::glfw::CreateSurfaceForWindow(instance, window);
#endif

 
InitGraphics();

#if defined(__EMSCRIPTEN__)
  emscripten_set_main_loop
(Render, 0, false);
#else

 
while (!glfwWindowShouldClose(window)) {
    glfwPollEvents
();
   
Render();
    surface
.Present();
    instance
.ProcessEvents();
 
}
#endif
}

Создайте приложение с помощью Emscripten

Единственное изменение, необходимое для создания приложения с помощью Emscripten, — это добавление к командам cmake волшебного сценария оболочки emcmake . На этот раз создайте приложение в подпапке build-web и запустите HTTP-сервер. Наконец, откройте браузер и посетите build-web/app.html .

# Build the app with Emscripten.
$ emcmake cmake
-B build-web && cmake --build build-web

# Start a HTTP server.
$ npx http
-server
Скриншот красного треугольника в окне браузера.
Красный треугольник в окне браузера.

Что дальше

Вот что вас может ожидать в будущем:

  • Улучшения в стабилизации API webgpu.h и webgpu_cpp.h.
  • Начальная поддержка Dawn для Android и iOS.

А пока, пожалуйста, сообщайте о проблемах WebGPU для Emscripten и Dawn с предложениями и вопросами.

Ресурсы

Не стесняйтесь изучить исходный код этого приложения.

Если вы хотите больше узнать о создании собственных 3D-приложений на C++ с нуля с помощью WebGPU, ознакомьтесь с документацией Learn WebGPU for C++ и примерами Dawn Native WebGPU .

Если вас интересует Rust, вы также можете изучить графическую библиотеку wgpu, основанную на WebGPU. Взгляните на их демо-версию hello-triangle .

Благодарности

Эту статью рецензировали Корентин Валлес , Кай Ниномия и Рэйчел Эндрю .

Фото Марка-Оливье Жодуана на Unsplash .