WebGPU:未来的跨平台图形 API

对于网络开发人员来说,WebGPU 是一个网络图形 API,它为 Web 公开 GPU 硬件的功能,允许在 GPU 上进行渲染和计算操作,从而提供对 GPU 统一且快速的访问,与 Direct3D 12、Metal 和 Vulkan 类似。

WebGPU 是 Apple、谷歌、英特尔、Mozilla 和微软等大公司协作的成果。他们研究发现,WebGPU 不仅是一个 JavaScript API,更可以是一个跨平台图形 API,可供开发人员在网络以外的生态系统中使用。为实现这个目标,Chrome 113 引入了 JavaScript API。与此同时,还开发了另一个重要项目:webgpu.h C API。该 C 头文件列出了 WebGPU 的所有可用程序和数据结构。它是一个与平台无关的硬件抽象层,在不同平台上提供一致的接口,让你可以构建特定平台的应用程序。

本文将教大家学习如何使用 WebGPU 编写一个可在网络和特定平台上运行的简单 C++ 应用程序。先剧透一下,只需对你的代码库进行最小限度的调整,就能获得在浏览器窗口和桌面窗口中同时显示的红色三角形。

WebGPU 在 macOS 的浏览器窗口和桌面窗口绘制同一个红色三角形


实现步骤

如需查看已完成的应用程序,请访问 WebGPU 跨平台应用程序存储库

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

网络中的该应用是基于 Emscripten 构建的,后者在 JavaScript API 上绑定了 webgpu.h。在 macOS 或 Windows 等平台上,该项目可通过 Chromium 的跨平台 WebGPU 实现 Dawn 构建。需要注意的是,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

要绘制三角形,可以利用 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_dawnwebgpu_cppwebgpu_glfw 目标,以便随后在 main.cpp 文件中使用它们。
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE webgpu_dawn webgpu_cpp webgpu_glfw)

打开一个窗口

有了 Dawn 之后,你可以使用 GLFW 在屏幕上绘图。webgpu_glfw 中包含了这个库,便于你编写与平台无关的窗口管理代码。

要打开一个分辨率为 512x512 的名为“WebGPU 窗口”的窗口,请更新 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 变量,充当 WebGPU 入口点。为方便起见,请在 main.cpp 文件的顶部声明 instance,并在 main() 中调用 wgpu::CreateInstance()

…
#include <webgpu/webgpu_cpp.h>

wgpu::Instance instance;
…

int main() {
  instance = wgpu::CreateInstance();
  Start();
}

由于 JavaScript API 的形状,访问 GPU 是异步的。在 C++ 中,创建一个辅助函数 GetDevice(),该函数接收一个回调函数参数,用生成的 wgpu::Device 调用该函数。

如果 WebAssembly JavaScript Promise Integration API 可用,下列代码将简化,但撰写本文时,情况并非如此。
void GetDevice(void (*callback)(wgpu::Device)) {
  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);
        adapter.RequestDevice(
            nullptr,
            [](WGPURequestDeviceStatus status, WGPUDevice cDevice,
               const char* message, void* userdata) {
              wgpu::Device device = wgpu::Device::Acquire(cDevice);
              reinterpret_cast<void (*)(wgpu::Device)>(userdata)(device);
            },
            userdata);
      },
      reinterpret_cast<void*>(callback));
}

为方便访问,请在 main.cpp 文件顶部声明一个 wgpu::Device 变量,并更新 main() 函数,调用 GetDevice() 并将结果回调赋值给 device,然后再调用 Start()

wgpu::Device device;
…

int main() {
 instance = wgpu::CreateInstance();
 GetDevice([](wgpu::Device dev) {
   device = dev;
   Start();
 });
}

画一个三角形

JavaScript API 中没有公开交换链,因为浏览器会处理,但在 C++ 中你需要手动创建交换链。为了方便起见,请再次在 main.cpp 文件顶部声明一个 wgpu::SwapChain 变量。在 Start() 中创建 GLFW 窗口后,调用适用的 wgpu::glfw::CreateSurfaceForWindow() 函数,创建一个 wgpu::Surface (类似于 HTML 画布),然后,调用 InitGraphics() 中的新辅助函数 SetupSwapChain() 使用 wgpu::Surface 来设置交换链。另外,你还需要调用 swapChain.Present(),在循环中显示下一个纹理。但这些操作不会产生明显效果,因为还没有进行渲染。

#include <webgpu/webgpu_glfw.h>
…

wgpu::SwapChain swapChain;

void SetupSwapChain(wgpu::Surface surface) {
  wgpu::SwapChainDescriptor scDesc{
      .usage = wgpu::TextureUsage::RenderAttachment,
      .format = wgpu::TextureFormat::BGRA8Unorm,
      .width = kWidth,
      .height = kHeight,
      .presentMode = wgpu::PresentMode::Fifo};
  swapChain = device.CreateSwapChain(surface, &scDesc);
}

void InitGraphics(wgpu::Surface surface) {
  SetupSwapChain(surface);
}

void Render() {
  // TODO: Render a triangle using WebGPU.
}

void Start() {
  …
  wgpu::Surface surface =
      wgpu::glfw::CreateSurfaceForWindow(instance, window);

  InitGraphics(surface);

  while (!glfwWindowShouldClose(window)) {
    glfwPollEvents();
    Render();
    swapChain.Present();
  }
}

接下来,用下面的代码创建渲染管道。为便于访问,请在 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 = wgpu::TextureFormat::BGRA8Unorm};

  wgpu::FragmentState fragmentState{.module = shaderModule,
                                    .entryPoint = "fragmentMain",
                                    .targetCount = 1,
                                    .targets = &colorTargetState};

  wgpu::RenderPipelineDescriptor descriptor{
      .vertex = {.module = shaderModule, .entryPoint = "vertexMain"},
      .fragment = &fragmentState};
  pipeline = device.CreateRenderPipeline(&descriptor);
}

void InitGraphics(wgpu::Surface surface) {
  …
  CreateRenderPipeline();
}

最后,在每帧中调用的 Render() 函数中向 GPU 发送渲染命令。

void Render() {
  wgpu::RenderPassColorAttachment attachment{
      .view = swapChain.GetCurrentTextureView(),
      .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

现在,我们来看看在浏览器窗口中绘制红色三角形需要对代码库做的改动。如上所述,该应用程序将使用 Emscripten(一种将 C/C++ 程序编译为 WebAssembly 的工具)构建,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_dawn webgpu_cpp webgpu_glfw)
endif()

更新代码

在 Emscripten 中,创建 wgpu::surface 需要一个 HTML 画布元素。因此,要调用 instance.CreateSurface() 并指定 #canvas 选择器,以匹配 Emscripten 生成的 HTML 页面中的相应 HTML 画布元素。

不要使用 while 循环,调用 emscripten_set_main_loop(Render),以确保 Render() 函数以适当的平稳速率调用,并与浏览器和显示器保持一致。

#include <GLFW/glfw3.h>
#include <webgpu/webgpu_cpp.h>
#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};
  wgpu::Surface surface = instance.CreateSurface(&surfaceDesc);
#else
  wgpu::Surface surface =
      wgpu::glfw::CreateSurfaceForWindow(instance, window);
#endif

  InitGraphics(surface);
  
  #if defined(__EMSCRIPTEN__)
  emscripten_set_main_loop(Render, 0, false);
#else
  while (!glfwWindowShouldClose(window)) {
    glfwPollEvents();
    Render();
    swapChain.Present();
  }
#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

浏览器窗口的红色三角形


下一步

我们今后努力的方向:

  • 改进 webgpu.h 和 webgpu_cpp.h API 的稳定性
  • Dawn 初步支持安卓和 iOS 系统

欢迎大家在 Emscripten 的 WebGPU 问题Dawn 问题中提出思考和建议。


其他资源

欢迎访问此应用程序的源代码。

如果你想深入了解如何使用 WebGPU 从零创建 C++ 原生 3D 应用程序,请查看 Learn WebGPU for C++ 文档Dawn Native WebGPU 示例

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


致谢

本文由 Corentin Wallez、Kai Ninomiya 和 Rachel Andrew 审阅。

图片来自 Marc-Olivier Jodoin (Unsplash)。



原文作者:François Beaufort
原文链接:https://developer.chrome.com/en/blog/webgpu-cross-platform/
推荐阅读
相关专栏
开发者实践
186 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。