对于 Web 开发者,WebGPU 是一种 Web 图形 API,可让您统一、快速访问 GPU。与 Direct3D 12、Metal 和 Vulkan 类似,WebGPU 会显示现代硬件功能,并允许在 GPU 上执行渲染和计算操作。
虽然真实,但这个故事并不完整。WebGPU 是由 Apple、Google、Intel、Mozilla 和 Microsoft 等主流公司共同努力的结果。其中一些人意识到 WebGPU 可能不只是一个 JavaScript API,而是一个跨平台图形 API,适用于除 Web 以外的生态系统中的开发者。
为了实现主要用例,Chrome 113 中引入了 JavaScript API。不过,我们还在此过程中开发了另一个重要项目:webgpu.h C API。此 C 头文件列出了 WebGPU 的所有可用过程和数据结构。它充当与平台无关的硬件抽象层,使您能够通过跨不同平台提供一致的接口来构建特定于平台的应用。
在本文档中,您将学习如何使用 WebGPU 编写在 Web 和特定平台上运行的小型 C++ 应用。提前剧透:浏览器窗口和桌面窗口中将显示相同的红色三角形,只是对代码库所做的调整极少。
![一个屏幕截图,其中显示了一个由 WebGPU 提供支持的红色三角形,该三角形在浏览器窗口中,一个桌面窗口,在 macOS 上。](https://developer.chrome.google.cn/static/docs/web-platform/webgpu/build-app/image/screenshot-a-red-triangl-82836ca46abb9.png?authuser=5&hl=sr)
工作原理
要查看已完成的应用,请访问 WebGPU 跨平台应用代码库。
此应用是一个极简的 C++ 示例,展示了如何使用 WebGPU 通过单一代码库构建桌面应用和 Web 应用。在后台,它通过名为 webgpu_cpp.h 的 C++ 封装容器使用 WebGPU 的 webgpu.h 作为与平台无关的硬件抽象层。
在 Web 上,该应用是基于 Emscripten 构建的,其中提供了基于 JavaScript API 实现 webgpu.h 的绑定。在特定平台(如 macOS 或 Windows)上,此项目可以根据 Dawn(Chromium 的跨平台 WebGPU 实现)进行构建。值得注意的是,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.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/”子文件夹中创建 build 文件,运行 cmake --build build
以实际构建应用并生成可执行文件。
# Build the app with CMake.
$ cmake -B build && cmake --build build
# Run the app.
$ ./build/app
应用正在运行,但尚未输出任何输出,因为您需要一种在屏幕上绘制内容的方法。
获取 Dawn
如需绘制三角形,您可以利用 Chromium 的跨平台 WebGPU 实现 Dawn。其中包括用于绘制到屏幕的 GLFW C++ 库。下载 Dawn 的一种方法是将它作为 git 子模块添加到您的代码库中。以下命令会从“dawn/”子文件夹中提取该文件。
$ git init
$ git submodule add https://dawn.googlesource.com/dawn
然后,按如下方式附加到 CMakeLists.txt
文件:
- CMake
DAWN_FETCH_DEPENDENCIES
选项会提取所有 Dawn 依赖项。 - 目标中包含
dawn/
子文件夹。 - 您的应用将依赖于
webgpu_cpp
、webgpu_dawn
、glfw
和webgpu_glfw
目标,以便您稍后在main.cpp
文件中使用它们。
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE webgpu_cpp webgpu_dawn glfw webgpu_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 窗口的屏幕截图。](https://developer.chrome.google.cn/static/docs/web-platform/webgpu/build-app/image/screenshot-a-empty-macos-55308df210fa3.png?authuser=5&hl=sr)
获取 GPU 设备
在 JavaScript 中,navigator.gpu
是访问 GPU 的入口点。在 C++ 中,您需要手动创建用于同一用途的 wgpu::Instance
变量。为方便起见,请在 main.cpp
文件顶部声明 instance
,并在 main()
内调用 wgpu::CreateInstance()
。
…
#include <webgpu/webgpu_cpp.h>
wgpu::Instance instance;
…
int main() {
instance = wgpu::CreateInstance();
Start();
}
由于 JavaScript API 的形状,对 GPU 的访问是异步进行的。在 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));
}
为了便于访问,请在 main.cpp
文件顶部声明两个变量 wgpu::Adapter
和 wgpu::Device
。更新 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();
});
});
}
绘制三角形
交换链不会在 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};
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::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()
函数中向 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 窗口中红色三角形的屏幕截图。](https://developer.chrome.google.cn/static/docs/web-platform/webgpu/build-app/image/screenshot-a-red-triangl-403ff28f43f9.png?authuser=5&hl=sr)
编译为 WebAssembly
现在,我们来看看调整现有代码库以在浏览器窗口中绘制此红色三角形所需的最少更改。同样,该应用是基于 Emscripten 构建的,该工具用于将 C/C++ 程序编译到 WebAssembly,该工具通过绑定在 JavaScript API 上实现 webgpu.h。
更新 CMake 设置
安装 Emscripten 后,请按如下方式更新 CMakeLists.txt
构建文件。您只需更改突出显示的代码。
set_target_properties
用于自动向目标文件添加“html”文件扩展名。也就是说,您将生成“app.html”文件。- 如需在 Emscripten 中启用 WebGPU 支持,必须使用
USE_WEBGPU
应用链接选项。如果没有该文件,您的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 webgpu_cpp webgpu_dawn glfw webgpu_glfw)
endif()
更新代码
在 Emscripten 中,创建 wgpu::surface
需要 HTML 画布元素。为此,请调用 instance.CreateSurface()
并指定 #canvas
选择器,以匹配 Emscripten 生成的 HTML 页面中相应的 HTML 画布元素。
请调用 emscripten_set_main_loop(Render)
,以确保以适当的速率调用 Render()
函数,使之与浏览器和监控器正确对齐,而不是使用 when 循环。
#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
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
![浏览器窗口中红色三角形的屏幕截图。](https://developer.chrome.google.cn/static/docs/web-platform/webgpu/build-app/image/screenshot-a-red-triangl-4d54843f1cde3.png?authuser=5&hl=sr)
后续步骤
未来将发生以下情况:
- 改进了 webgpu.h API 和 webgpu_cpp.h API 的稳定性。
- Dawn 对 Android 和 iOS 进行了初步支持。
在此期间,请提交 Escripten 的 WebGPU 问题和 Dawn 问题,并提出建议和问题。
资源
欢迎随时浏览此应用的源代码。
如果您想深入了解如何使用 WebGPU 从头开始以 C++ 编写原生 3D 应用,请参阅“了解 C++ 版 WebGPU”文档和 Dawn 原生 WebGPU 示例。
如果您对 Rust 感兴趣,还可以探索基于 WebGPU 的 wgpu 图形库。可以看看他们的 hello-triangle 演示。
致谢
本文由 Corentin Wallez、Kai Ninomiya 和 Rachel Andrew 审核。
照片由 Marc-Olivier Jodoin 拍摄,来源是 Unsplash 用户。