Для веб-разработчиков 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 .
Приложение представляет собой минималистичный пример 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();
}
Пересборка приложения и запуск его в прежнем режиме теперь приводят к пустому окну. Вы прогрессируете!
Получить устройство с графическим процессором
В 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 и его запуск теперь приводят к появлению долгожданного красного треугольника в окне! Отдохните – вы это заслужили.
Компилировать в 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 .