从 WebGL 到 WebGPU

弗朗索瓦·博福
François Beaufort

作为 WebGL 开发者,您可能会为开始使用 WebGPU 而感到畏惧,并且很激动人心。WebGPU 是 WebGL 的继任者 WebGL,它将现代图形 API 的发展带入了 Web。

看到 WebGL 和 WebGPU 有共同的许多核心概念,我感到很欣慰。这两个 API 都允许您在 GPU 上运行称为着色器的小程序。WebGL 支持顶点和 Fragment 着色器,而 WebGPU 也支持计算着色器。WebGL 使用 OpenGL 着色语言 (GLSL),而 WebGPU 使用 WebGPU 着色语言 (WGSL)。虽然这两种语言不同,但基本概念大体上是相同的。

有鉴于此,本文重点介绍 WebGL 与 WebGPU 之间的一些差异,以帮助您顺利上手。

全局状态

WebGL 存在很多全球性状态。有些设置适用于所有渲染操作,例如绑定哪些纹理和缓冲区。您可以通过调用各种 API 函数来设置此全局状态,并且该状态将保持有效状态,直到您对其进行更改。WebGL 中的全局状态是主要错误来源,因为开发者很容易忘记更改全局设置。此外,全局状态会使代码共享变得困难,因为开发者需要小心,以免意外更改全局状态,影响代码的其他部分。

WebGPU 是一种无状态 API,并不保持全局状态。相反,它使用管道的概念来封装在 WebGL 中是全局性的所有渲染状态。流水线包含要使用的混合、拓扑和属性等信息。流水线是不可变的。如果要更改某些设置,您需要再创建一个流水线。WebGPU 还使用命令编码器来批量处理命令,并按记录的顺序执行这些命令。这在阴影映射中非常实用。例如,在对象单个遍历中,应用可以记录多个命令流,每个命令流对应一个光的阴影贴图。

总而言之,随着 WebGL 的全局状态模型难以创建强大的可组合库和应用,WebGPU 大大减少了开发者在向 GPU 发送命令时需要跟踪的状态数。

不再同步

在 GPU 上,发送命令并同步等待这些命令通常效率低下,因为这可能会清空流水线并导致出现气泡。在 WebGPU 和 WebGL 中尤其如此,它们使用多进程架构,其中 GPU 驱动程序在独立于 JavaScript 的进程中运行。

例如,在 WebGL 中,调用 gl.getError() 需要在 JavaScript 进程和 GPU 进程之间进行同步的 IPC。这可能会导致在两个进程通信时,CPU 端出现气泡。

为了避免此类气泡,WebGPU 设计为完全异步错误模型和所有其他操作都是异步发生的。例如,在创建纹理时,即使纹理实际上是一个错误,操作也会立即成功。您只能异步发现错误。这种设计可确保跨进程通信没有气泡,并使应用具有可靠的性能。

计算着色器

计算着色器是在 GPU 上运行的程序,用于执行通用计算。它们仅适用于 WebGPU,而不适用于 WebGL。

与顶点和片段着色器不同,它们不限于图形处理,并且可用于各种任务,例如机器学习、物理模拟和科学计算。计算着色器由数百甚至数千个线程并行执行,这使得它们对于处理大型数据集非常高效。如需了解 GPU 计算和更多详细信息,请参阅这篇有关 WebGPU 的详尽文章

视频帧处理

使用 JavaScript 和 WebAssembly 处理视频帧存在一些缺点:将数据从 GPU 内存复制到 CPU 内存的成本很高,并且可通过工作器和 CPU 线程实现的并行性有限。WebGPU 没有这些限制,由于它与 WebCodecs API 紧密集成,因此非常适合处理视频帧。

以下代码段展示了如何将 VideoFrame 作为外部纹理导入到 WebGPU 中并对其进行处理。您可以试用此演示

// Init WebGPU device and pipeline...
// Configure canvas context...
// Feed camera stream to video...

(function render() {
  const videoFrame = new VideoFrame(video);
  applyFilter(videoFrame);
  requestAnimationFrame(render);
})();

function applyFilter(videoFrame) {
  const texture = device.importExternalTexture({ source: videoFrame });
  const bindgroup = device.createBindGroup({
    layout: pipeline.getBindGroupLayout(0),
    entries: [{ binding: 0, resource: texture }],
  });
  // Finally, submit commands to GPU
}

默认应用可移植性

WebGPU 会强制您请求 limits。默认情况下,requestDevice() 返回的 GPUDevice 可能与实体设备的硬件功能不匹配,而是返回在所有 GPU 中合理且最低的共同分母。通过要求开发者申请设备数量上限,WebGPU 可确保应用在尽可能多的设备上运行。

画布处理

在您创建 WebGL 上下文并提供上下文属性(例如 alpha、抗锯齿、colorSpace、深度、keepDrawingBuffer 或模板)后,WebGL 会自动管理画布。

另一方面,WebGPU 需要您自行管理画布。例如,为了在 WebGPU 中实现抗锯齿,您需要创建一个多重采样纹理并对其进行渲染。然后,您需要将多重采样纹理解析为常规纹理,并将该纹理绘制到画布上。通过这种手动管理,您可以从单个 GPUDevice 对象输出到任意数量的画布。相反,WebGL 只能为每个画布创建一个上下文。

查看 WebGPU 多画布演示

顺便提一下,浏览器目前对每页的 WebGL 画布数量有限制。在撰写本文时,Chrome 和 Safari 最多只能同时使用 16 个 WebGL 画布;Firefox 最多可以创建 200 个 WebGL 画布。另一方面,每页的 WebGPU 画布数量没有限制。

显示 Safari、Chrome 和 Firefox 浏览器中 WebGL 画布数量上限的屏幕截图
Safari、Chrome 和 Firefox 中的 WebGL 画布数量上限(从左到右)- 演示

实用的错误消息

WebGPU 为从 API 返回的每条消息提供一个调用堆栈。这意味着您可以快速查看代码中的错误位置,这对调试和修正错误很有帮助

除了提供调用堆栈之外,WebGPU 错误消息也易于理解和可操作。错误消息通常包含对错误的说明以及有关如何修正错误的建议。

借助 WebGPU,您还可以为每个 WebGPU 对象提供自定义 label。然后,浏览器会在 GPUError 消息、控制台警告和浏览器开发者工具中使用此标签。

从名称到索引

在 WebGL 中,很多事物都是通过名称连接的。例如,您可以在 GLSL 中声明一个名为 myUniform 的统一变量,并使用 gl.getUniformLocation(program, 'myUniform') 获取其位置。如果 uniform 变量的名称输错了,这样会很方便。

另一方面,在 WebGPU 中,所有内容都完全通过字节偏移量或索引(通常称为“位置”)连接。您有责任确保 WGSL 和 JavaScript 中的代码位置保持同步。

mipmap 生成

在 WebGL 中,您可以创建纹理的级别 0 mip,然后调用 gl.generateMipmap()。然后,WebGL 会为您生成所有其他 mip 级别。

在 WebGPU 中,您必须自行生成 mipmap。没有用于执行此操作的内置函数。如需详细了解相关决定,请参阅规范讨论。您可以使用 webgpu-utils 等便捷库生成 mipmap,也可以自行了解如何操作。

存储缓冲区和存储纹理

WebGL 和 WebGPU 均支持统一缓冲区,并且您可以将大小有限的常量参数传递给着色器。存储缓冲区(看起来很像统一缓冲区),仅受 WebGPU 支持,并且比统一缓冲区更强大、更灵活。

  • 传递给着色器的存储缓冲区数据可能比统一缓冲区大得多。虽然规范指出统一缓冲区绑定的大小不得超过 64KB(请参阅 maxUniformBufferBindingSize),但在 WebGPU 中,存储缓冲区绑定的最大大小至少为 128MB(请参阅 maxStorageBufferBindingSize)。

  • 存储缓冲区是可写的,并且支持某些原子操作,而统一缓冲区只是只读。这样可以实现新的算法类。

  • 存储缓冲区绑定支持运行时大小的数组,以实现更灵活的算法,而必须在着色器中提供统一的缓冲区数组大小。

存储纹理仅在 WebGPU 中受支持,并且对于纹理来说,存储缓冲区就等同于统一缓冲区。它们比常规纹理更灵活,支持随机访问写入(未来也支持读取)。

缓冲区和纹理更改

例如,在 WebGL 中,您可以创建缓冲区或纹理,然后随时分别使用 gl.bufferData()gl.texImage2D() 更改其大小。

在 WebGPU 中,缓冲区和纹理是不可变的。也就是说,一旦创建完毕,您便无法更改其大小、用途或格式。您只能更改其内容。

空间惯例差异

在 WebGL 中,Z 裁剪空间的范围为 -1 到 1。在 WebGPU 中,Z 裁剪空间的范围为 0 到 1。这意味着 z 值为 0 的对象离镜头最近,而 z 值为 1 的对象离镜头最远。

WebGL 和 WebGPU 中的 Z 裁剪空间范围的图示。
WebGL 和 WebGPU 中的 Z 裁剪空间范围。

WebGL 采用 OpenGL 惯例,其中 Y 轴向上,Z 轴朝向查看器。WebGPU 使用 Metal 惯例,其中 Y 轴向下,Z 轴位于屏幕外。请注意,Y 轴方向在帧缓冲区坐标、视口坐标和 fragment/像素坐标中向下。在剪辑空间中,Y 轴方向仍像在 WebGL 中一样向上。

致谢

感谢 Corentin Wallez、Gregg Tavares、Stephen White、Ken Russell 和 Rachel Andrew 对本文进行审核。

此外,我还建议访问 WebGPUFundamentals.org,以深入了解 WebGPU 和 WebGL 之间的差异。