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

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

Опубликовано: 20 июля 2023 г., Последнее обновление: 27 августа 2025 г.

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

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

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

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

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

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

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

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

В веб-версии приложение собрано с использованием emdawnwebgpu (Emscripten Dawn WebGPU), привязки которого реализуют webgpu.h поверх JavaScript API. На некоторых платформах, таких как 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.22) # 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.
  • Параметр CMake DAWN_BUILD_MONOLITHIC_LIBRARY объединяет все компоненты Dawn в одну библиотеку.
  • Подпапка dawn/ включена в целевой объект.
  • Ваше приложение будет зависеть от целей webgpu_dawn , webgpu_glfw и glfw , поэтому вы сможете позже использовать их в файле main.cpp .

set(DAWN_FETCH_DEPENDENCIES ON)
set(DAWN_BUILD_MONOLITHIC_LIBRARY STATIC)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)

target_link_libraries(app PRIVATE webgpu_dawn webgpu_glfw 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();
}

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

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

Получить устройство GPU

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

#include <webgpu/webgpu_cpp.h>


wgpu::Instance instance;


void Init() {
  static const auto kTimedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny;
  wgpu::InstanceDescriptor instanceDesc{.requiredFeatureCount = 1,
                                        .requiredFeatures = &kTimedWaitAny};
  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-холсту) и настройте его, вызвав новую вспомогательную функцию 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 и его запуск теперь приводят к появлению долгожданного красного треугольника в окне! Сделайте перерыв — вы его заслужили.

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

Компиляция в 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.22) # 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)
set(DAWN_BUILD_MONOLITHIC_LIBRARY STATIC)
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 webgpu_dawn webgpu_glfw 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 .