从 WebGL 到 WebGPU

François Beaufort
François Beaufort

作为 WebGL 开发者,您可能会对开始使用 WebGPU 感到不安和兴奋,因为 WebGPU 是 WebGL 的继任产品,可为网络带来现代图形 API 的进步。

令人欣慰的是,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,该 GPUDevice 可能与实体设备的硬件功能不匹配,而是返回所有 GPU 的合理且最低的共同标准。通过要求开发者请求设备限制,WebGPU 可确保应用在尽可能多的设备上运行。

画布处理

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

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

查看 WebGPU 多个画布演示

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

<ph type="x-smartling-placeholder">
</ph> 屏幕截图:Safari、Chrome 和 Firefox 浏览器中的 WebGL 画布数量已达上限 <ph type="x-smartling-placeholder">
</ph> 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 的对象距离最远。

<ph type="x-smartling-placeholder">
</ph> WebGL 和 WebGPU 中的 Z 裁剪空间范围的图示。 <ph type="x-smartling-placeholder">
</ph> WebGL 和 WebGPU 中的 Z 裁剪空间范围。

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

致谢

感谢 Corentin Wallez、Gregg Tavares、Stephen White、Ken Russell 和 Rachel Andrew 审阅本文。

此外,我还建议访问 WebGPUFundamentals.org 来深入了解 WebGPU 和 WebGL 之间的区别。