使用 WebGPU 构建应用

François Beaufort
François Beaufort

发布时间:2023 年 7 月 20 日;上次更新时间:2025 年 9 月 24 日

对于 Web 开发者而言,WebGPU 是一种 Web 图形 API,可提供对 GPU 的统一快速访问。WebGPU 可公开现代硬件功能,并允许在 GPU 上执行渲染和计算操作,类似于 Direct3D 12、Metal 和 Vulkan。

虽然这个故事是真实的,但并不完整。WebGPU 是 Apple、Google、Intel、Mozilla 和 Microsoft 等主要公司共同努力的成果。其中,一些人意识到 WebGPU 不仅仅是一个 JavaScript API,而是一个面向 Web 以外的各个生态系统中的开发者的跨平台图形 API。

为了实现主要使用情形,Chrome 113 中引入了 JavaScript API。不过,与此同时,还开发了另一个重要项目:webgpu.h C API。此 C 头文件列出了 WebGPU 的所有可用程序和数据结构。它充当与平台无关的硬件抽象层,让您能够通过在不同平台之间提供一致的接口来构建平台专用应用。

本文档将介绍如何使用 WebGPU 编写一个可在 Web 和特定平台上运行的小型 C++ 应用。剧透一下,您只需对代码库进行极少的调整,即可获得与浏览器窗口和桌面窗口中相同的红色三角形。

屏幕截图:一个浏览器窗口和一个桌面窗口(在 macOS 上),其中显示了一个由 WebGPU 提供支持的红色三角形。
浏览器窗口和桌面窗口中由 WebGPU 提供支持的同一三角形。

工作原理

如需查看已完成的应用,请查看 WebGPU 跨平台应用代码库。

该应用是一个极简的 C++ 示例,展示了如何使用 WebGPU 从单一代码库构建桌面应用和 Web 应用。在后台,它使用 WebGPU 的 webgpu.h 作为平台无关的硬件抽象层,通过名为 webgpu_cpp.h 的 C++ 封装容器来实现。

在 Web 上,该应用是针对 emdawnwebgpu (Emscripten Dawn WebGPU) 构建的,该应用具有在 JavaScript API 之上实现 webgpu.h 的绑定。在 macOS 或 Windows 等特定平台上,此项目可以针对 Chromium 的跨平台 WebGPU 实现 Dawn 进行构建。值得一提的是,还存在 wgpu-native(webgpu.h 的 Rust 实现),但本文档中未使用它。

开始使用

首先,您需要一个 C++ 编译器和 CMake,以便以标准方式处理跨平台构建。在专用文件夹中,创建一个 main.cpp 源文件和一个 CMakeLists.txt build 文件。

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/”子文件夹中创建 build 文件,并运行 cmake --build build 以实际构建应用并生成可执行文件。

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

应用正在运行,但目前还没有输出,因为您需要一种在屏幕上绘制内容的方法。

获取 Dawn

如需绘制三角形,您可以利用 Dawn,这是 Chromium 的跨平台 WebGPU 实现。这包括用于绘制到屏幕的 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_dawnwebgpu_glfwglfw 目标,以便您稍后在 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 中,方便您编写与平台无关的窗口管理代码。

如需打开分辨率为 512x512 的名为“WebGPU window”的窗口,请按如下所示更新 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 是您访问 GPU 的入口点。在 C++ 中,您需要手动创建一个 wgpu::Instance 变量,用于实现相同的目的。为方便起见,请在 main.cpp 文件的顶部声明 instance,并在 Init() 内调用 wgpu::CreateInstance()

#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(); } 

main.cpp 文件的顶部声明两个变量 wgpu::Adapterwgpu::Device。更新 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++ 中,您需要手动创建。为方便起见,请再次在 main.cpp 文件的顶部声明一个 wgpu::Surface 变量。在 Start() 中创建 GLFW 窗口后,立即调用便捷的 wgpu::glfw::CreateSurfaceForWindow() 函数来创建 wgpu::Surface(类似于 HTML 画布),并通过调用 InitGraphics() 中的新辅助函数 ConfigureSurface() 来配置它。您还需要调用 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();  } } 

现在,您可以使用以下代码创建渲染流水线。为了便于访问,请在 main.cpp 文件的顶部声明一个 wgpu::RenderPipeline 变量,并在 InitGraphics() 中调用辅助函数 CreateRenderPipeline()

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() 函数中向 GPU 发送渲染命令。

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) 构建的,该应用具有在 JavaScript API 之上实现 webgpu.h 的绑定。它使用 Emscripten(一种用于将 C/C++ 程序编译为 WebAssembly 的工具)。

更新 CMake 设置

安装 Emscripten 后,按如下方式更新 CMakeLists.txt build 文件。 突出显示的代码是您唯一需要更改的内容。

  • set_target_properties 用于自动向目标文件添加“html”文件扩展名。换句话说,您将生成一个“app.html”文件。
  • emdawnwebgpu_cpp 目标链接库可在 Emscripten 中启用 WebGPU 支持。如果没有它,您的 main.cpp 文件将无法访问 webgpu/webgpu_cpp.h 文件。
  • 借助 ASYNCIFY=1 应用链接选项,同步 C++ 代码可以与异步 JavaScript 进行交互。
  • USE_GLFW=3 应用链接选项会告知 Emscripten 使用其内置的 GLFW 3 API JavaScript 实现。
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() 

更新代码

请调用 emscripten_set_main_loop(Render),而不是使用 while 循环,以确保以适当的平滑速率调用 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 shell 脚本作为前缀。这次,在 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 
浏览器窗口中红色三角形的屏幕截图。
浏览器窗口中的红色三角形。

后续步骤

展望未来,我们计划在 Android 和 iOS 上提供对 Dawn 的初始支持。

与此同时,请提交 Emscripten 的 WebGPU 问题Dawn 问题,并附上建议和问题。

资源

您可以随时查看此应用的源代码

如果您想深入了解如何使用 WebGPU 从头开始在 C++ 中创建原生 3D 应用,请参阅 Learn WebGPU for C++ 文档Dawn Native WebGPU Examples

如果您对 Rust 感兴趣,还可以探索基于 WebGPU 的 wgpu 图形库。不妨看看他们的 hello-triangle 演示。

致谢

本文由 Corentin WallezKai NinomiyaRachel Andrew 审核。