Опубликовано: 20 июля 2023 г., Последнее обновление: 17 июня 2025 г.
Для веб-разработчиков WebGPU — это API веб-графики, который обеспечивает унифицированный и быстрый доступ к графическим процессорам. WebGPU раскрывает современные аппаратные возможности и позволяет выполнять операции рендеринга и вычислений на графическом процессоре, аналогично Direct3D 12, Metal и Vulkan.
Хотя это правда, эта история неполна. WebGPU — это результат совместных усилий, в том числе крупных компаний, таких как Apple, Google, Intel, Mozilla и Microsoft. Некоторые из них поняли , что WebGPU может быть чем-то большим, чем просто API Javascript, а кроссплатформенным графическим API для разработчиков в разных экосистемах, помимо веба.
Для выполнения основного варианта использования в Chrome 113 был представлен JavaScript API. Однако параллельно с ним был разработан еще один важный проект: webgpu.h C API. Этот заголовочный файл C перечисляет все доступные процедуры и структуры данных WebGPU. Он служит в качестве независимого от платформы уровня абстракции оборудования, позволяя вам создавать платформенно-зависимые приложения, предоставляя согласованный интерфейс на разных платформах.
В этом документе вы узнаете, как написать небольшое приложение C++ с использованием WebGPU, которое работает как в Интернете, так и на определенных платформах. Внимание, спойлер: вы получите тот же красный треугольник, который появляется в окне браузера и в окне рабочего стола с минимальными изменениями в вашей кодовой базе.
Как это работает?
Чтобы увидеть готовое приложение, посетите репозиторий кроссплатформенных приложений WebGPU .
Приложение представляет собой минималистичный пример C++, который показывает, как использовать WebGPU для создания настольных и веб-приложений из единой кодовой базы. Под капотом он использует webgpu.h WebGPU как платформенно-независимый аппаратный абстракционный слой через обертку C++, называемую webgpu_cpp.h .
В сети приложение построено на основе emdawnwebgpu (Emscripten Dawn WebGPU), который имеет привязки, реализующие webgpu.h поверх JavaScript API. На определенных платформах, таких как macOS или Windows, этот проект может быть построен на основе Dawn , кроссплатформенной реализации WebGPU Chromium. Стоит упомянуть, что wgpu-native , реализация Rust webgpu.h, также существует, но не используется в этом документе.
Начать
Для начала вам понадобится компилятор 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/
включена в цель. - Ваше приложение будет зависеть от целей
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 window" с разрешением 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();
}
Пересоздание приложения и запуск его как прежде теперь приводит к пустому окну. Вы делаете успехи!
Получить устройство GPU
В JavaScript navigator.gpu
— это точка входа для доступа к GPU. В C++ вам нужно вручную создать переменную wgpu::Instance
, которая используется для той же цели. Для удобства объявите instance
в верхней части файла main.cpp
и вызовите wgpu::CreateInstance()
внутри Init()
.
#include <webgpu/webgpu_cpp.h>
…
wgpu::Instance instance;
…
void Init() {
wgpu::InstanceDescriptor instanceDesc{
.capabilities = {.timedWaitAnyEnable = true}};
instance = wgpu::CreateInstance(&instanceDesc);
}
int main() {
Init();
Start();
}
Объявите две переменные wgpu::Adapter
и wgpu::Device
в верхней части файла main.cpp
. Обновите функцию Init()
, чтобы она вызывала instance.RequestAdapter()
и назначала ее результат обратного вызова adapter
, затем вызовите adapter.RequestDevice()
и назначайте ее результат обратного вызова device
.
#include <iostream>
#include <dawn/webgpu_cpp_print.h>
…
wgpu::Adapter adapter;
wgpu::Device device;
void Init() {
…
wgpu::Future f1 = instance.RequestAdapter(
nullptr, wgpu::CallbackMode::WaitAnyOnly,
[](wgpu::RequestAdapterStatus status, wgpu::Adapter a,
wgpu::StringView message) {
if (status != wgpu::RequestAdapterStatus::Success) {
std::cout << "RequestAdapter: " << message << "\n";
exit(0);
}
adapter = std::move(a);
});
instance.WaitAny(f1, UINT64_MAX);
wgpu::DeviceDescriptor desc{};
desc.SetUncapturedErrorCallback([](const wgpu::Device&,
wgpu::ErrorType errorType,
wgpu::StringView message) {
std::cout << "Error: " << errorType << " - message: " << message << "\n";
});
wgpu::Future f2 = adapter.RequestDevice(
&desc, wgpu::CallbackMode::WaitAnyOnly,
[](wgpu::RequestDeviceStatus status, wgpu::Device d,
wgpu::StringView message) {
if (status != wgpu::RequestDeviceStatus::Success) {
std::cout << "RequestDevice: " << message << "\n";
exit(0);
}
device = std::move(d);
});
instance.WaitAny(f2, UINT64_MAX);
}
Нарисуй треугольник.
Цепочка обмена не отображается в JavaScript API, так как об этом заботится браузер. В C++ вам нужно создать ее вручную. Еще раз, для удобства, объявите переменную wgpu::Surface
в верхней части файла main.cpp
. Сразу после создания окна GLFW в Start()
вызовите удобную функцию wgpu::glfw::CreateSurfaceForWindow()
, чтобы создать wgpu::Surface
(похожую на HTML canvas) и настроить ее, вызвав новую вспомогательную функцию 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,
.presentMode = wgpu::PresentMode::Fifo};
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::ShaderSourceWGSL wgsl{{.code = shaderCode}}; wgpu::ShaderModuleDescriptor shaderModuleDescriptor{.nextInChain = &wgsl}; 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
Давайте теперь посмотрим на минимальные изменения, необходимые для настройки вашей существующей кодовой базы для рисования этого красного треугольника в окне браузера. Опять же, приложение создано на основе emdawnwebgpu (Emscripten Dawn WebGPU), который имеет привязки, реализующие webgpu.h поверх JavaScript API. Он использует Emscripten , инструмент для компиляции программ C/C++ в WebAssembly.
Обновите настройки CMake
После установки Emscripten обновите файл сборки CMakeLists.txt
следующим образом. Выделенный код — единственное, что вам нужно изменить.
-
set_target_properties
используется для автоматического добавления расширения файла "html" к целевому файлу. Другими словами, вы сгенерируете файл "app.html". - Библиотека целевых ссылок
emdawnwebgpu_cpp
обеспечивает поддержку WebGPU в Emscripten. Без нее ваш файлmain.cpp
не сможет получить доступ к файлуwebgpu/webgpu_cpp.h
. - Параметр ссылки на приложение
ASYNCIFY=1
позволяет синхронному коду C++ взаимодействовать с асинхронным JavaScript. - Параметр ссылки на приложение
USE_GLFW=3
указывает Emscripten использовать встроенную реализацию JavaScript API GLFW 3.
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")
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
if(EMSCRIPTEN)
set_target_properties(app PROPERTIES SUFFIX ".html")
target_link_libraries(app PRIVATE emdawnwebgpu_cpp webgpu_glfw)
target_link_options(app PRIVATE "-sASYNCIFY=1" "-sUSE_GLFW=3")
else()
target_link_libraries(app PRIVATE dawn::webgpu_dawn glfw webgpu_glfw)
endif()
Обновить код
Вместо использования цикла while вызовите emscripten_set_main_loop(Render)
, чтобы убедиться, что функция Render()
вызывается с надлежащей плавной скоростью, которая правильно согласуется с браузером и монитором.
#include <iostream>
#include <GLFW/glfw3.h>
#if defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h>
#endif
#include <dawn/webgpu_cpp_print.h>
#include <webgpu/webgpu_cpp.h>
#include <webgpu/webgpu_glfw.h>
void Start() {
…
#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 .