WebGPU 不仅仅只和 Web 有关
信息
TL;DR. 本文介绍了图形 API 发展与分裂,WebGPU 的优势,以及简单演示如何使用 WebGPU 进行本地开发。 柴认为,WebGPU 的优势在于其领域无关性,可以避免被 CUDA 生态绑死; 从教学的角度来看,WebGPU 也是一个很好的选择,因为它将被很多人使用,有很多文档和强大的用户基础。 神经网络推理很有可能会将 WebGPU 作为将来的选择,Web 浏览器已经变得越来越强大。
从独立自主可控国产化的角度来看,ISA, CPU, GPU, OS, EDA, Browser 是技术新基建,需要投入。
Transcript: https://bit.ly/3sLaYRo 以下为中文整理(with LLM):
我在 2020 年的直播课程中对着一群使用 Mac 的学生讲授计算机图形学。这是一次实践课,尽管它并不那么实用,学生们正在做计算机图形学作业。你看到他们举手,这些都是 Mac 用户。我意识到我实际上必须将整个代码库回退到 OpenGL 的旧版本,因为 macOS 不支持其最新版本——它甚至实际上正在弃用 OpenGL 的新版本。可移植性一直都很重要。当涉及到图形编程时,要满足可移植性也并不容易,即使是本地图形编程。OpenGL 长期以来是进行可移植图形处理的首选 API,但现在情况已不再是这样。我们将首先看到 WebGPU 实际上是一个好候选者来取代它,可以用于本地 GPU 编程,而不仅仅是用于 Web. 然后我将展示一个更详细、更实用的介绍,介绍如何使用 WebGPU 进行本地开发,使用 C++. 我将以一个关于采用 WebGPU 是否为时过早的观点结束,因为它仍在进行中,设计过程仍未完成。可能为时过早,也可能不是。而我认为或许现在正是时候,接下来将更详细地进行介绍。
背景
我是一名计算机图形学研究员,做过很多原型。在很多情况下,我必须从头开始编写使用实时图形、使用 GPU 编程的小型应用程序,所以我经常把一切推倒重来,并且总是想知道我上次采用的解决方案目前是否仍然是最相关的。我需要更快地迭代,所以并不总是选择低级别方案,但我仍然需要对我所做的事情有很多控制。我认为这在很多需求上都很有代表性。
另一个有趣的部分是,我还教授计算机图形学。为此,我总是在想:“开始使用 GPU 编程的最佳方式是什么?” 无论是用于 3D 图形,还是用于 GPU 目前可以做的所有其他事情,它不仅限于 3D 图形。由于这两个原因,我对 WebGPU 产生了兴趣,并开始编写这个指南,介绍如何使用 WebGPU, 它不仅仅是用于 Web 网页,更重要的是,更专注于桌面编程。
一旦 WebGPU API 在 Web 浏览器中正式发布,它将使你能够在 Web 中使用它。
图形 API
我们的第一个重点,图形 API. 图形API,像 OpenGL、WebGPU. 第一个问题实际上是,我们为什么需要这样的东西来制作 3D 或访问 GPU? 然后再是思考,有哪些选项?
我首先假设我们需要使用 GPU,否则,我们也的确不需要图形 API. 我们为什么可能需要使用 GPU? 因为这是一个大规模并行计算设备,很多问题实际上是可以大规模并行化的。3D 图形就是这样,许多模拟、物理模拟也是如此。神经网络训练和评估也是如此,一些密码学问题也是如此。我做出的另一个假设是,我们打算让我们的代码在多个平台上可移植,因为如果我们没有这种需求,这只需要专注于目标平台,做出特定的选择。
为什么需要图形 API?
我们为什么需要一个图形 API? 因为 GPU 就像机器中的另一台机器,它有自己的内存(也即是显存)。这是一个完全不同的设备,实际上通过 PCI Express 连接与 CPU 通信,但它们都有自己的生命,这真的就像一个远程机器。如果你习惯于网络编程,可以把它想象成服务器,CPU 是客户端。它们相隔很远,这些之间的通信需要时间。我们在这里做的所有事情,当我们使用像 C++ 这样的语言进行编程时,通常是描述 CPU 的行为。如果我们想在 GPU 上运行东西,我们实际上必须与驱动程序交谈,以向 GPU 传达我们期望它做什么。
这就是我们需要图形 API 来处理这种通信的原因。
图形 API 的选择
有哪些选项?有很多不同的方式来做这件事。有些选项由 GPU 的制造商直接提供,由设备本身提供,有些由操作系统提供。最后,我们将看到可移植的 API, 它们试图被多个驱动程序、多个操作系统标准实现。
- 特定于供应商的 API. 我不会太关注这类。例如,有 CUDA, 这是 NVIDIA 特定用于通用 GPU 编程的 API. 我也简要提到了 Mantle, 它不再存在,但它是 Vulkan 的基础。当它出现时,它是 AMD 特定的 API.
- 对于特定于操作系统的 API. 众所周知的是 Direct3D. 这已经存在很长时间了,尽管每个不同版本的 Direct3D 确实与前一个版本非常不同。例如,Direct3D 12 与 Direct3D 11 非常不同,后者也与前几个版本不同。这就是 Windows 和 Xbox 的本地图形 API. 如果你只针对这些平台,那么就直接使用 DirectX API. 还有 Metal, 用于 macOS 和 iOS. 如果你去其它类型的设备,特别是游戏机,你也会有这些平台提供的一些特定 API.
- 我主要关注桌面,因为通常,我们想要一些东西是可移植的,这样我们就不用为这些不同的API重写了。我们需要可移植的 API. 长期以来,3D 的可移植 API 是 OpenGL.
OpenGL 也有一些变体。一个是 OpenGL ES, 它针对低端设备,特别是手机。WebGL 基本上是将 OpenGL ES 映射为 JavaScript API. 还有 OpenCL, 它更专注于通用 GPU 编程。OpenGL 专注于 3D 图形,OpenCL 关注非 3D 相关的事情。实际上,随着时间的推移,计算管线和渲染管线实际上变得更接近了,现在在同一个应用程序中同时使用它们非常常见。在我看来,不再有必要有两类不同的 API.
OpenGL 的继任者与它过去的位置非常不同,新的 API 完全打破了这一点,变得更加低级,这便是 Vulkan. Vulkan 本应该像 OpenGL 一样可移植,但事实是,苹果并没有真正采用它,因为他们想专注于 Metal, 他们不想支持 Vulkan. 这就是为什么现在事情变得有一点点复杂,要有一个面向未来的、可移植的方式来与 GPU 通信——这就是 WebGPU, 它也是一个可移植的 API,因为它是为 Web 开发的 API,因此必须是可移植的,事情变得柳暗花明了。
如何确保可移植性和性能
我们如何获得一个真正可移植的 API? 如果你查看一些兼容性表,你会看到实际上没有万能的灵丹妙药——没有 API 可以在所有东西上运行。假设我们还没有 WebGPU, 能不能像 WebGL 一样,只是映射一个现有的 API 并让它在 JavaScript 中暴露出来?WebGL 做出的选择是取不同平台或设备之间共有的最基本的功能和特性,但这限制了最低端的设备。如果我们想获得更多的性能和 GPU 的更高级功能,我们需要一些不同的东西。这引出了一个问题——WebGPU 的草案到底会如何设计,浏览器开发人员实际上会如何在实践中实现它?这实际上是一个类似的问题:如果我们进行桌面 GPU 编程,我们如何开发与所有平台兼容的东西?
WebGPU 开发者们目前根据浏览器的情况所做出的决定,实际上依旧是在重复相同或类似的开发任务。在上层有个 WebGPU 抽象层,这个 WebGPU API 有多个后端。这既适用于 Firefox 实现,也适用于 Chrome 实现。它们在运行的平台后面都有不同的低级 API. 开发者们正在考虑如何共享和重用他们在使用 WebGPU 时的努力,以便这些努力也能在非 JavaScript 的本地编程环境中得到应用。具体来说,通过定义一个共同的头文件 webgpu.h
, 不同的 WebGPU 实现(比如基于 Metal、DirectX、Vulkan 等)可以在本地代码中被使用。这意味着,作为开发者,你可以编写一次代码,然后根据用户的平台,它会自动选择合适的底层技术(Metal、DirectX、Vulkan 等)来执行。这样,WebGPU 不仅仅是一个 Web 图形API,它也可以被视为一个跨平台的桌面图形 API.
RHI 与低级 API 开发分离
这种将不同技术抽象化的做法并不新鲜,实际上已经被很多其他主流的程序和库采用。例如,Unreal Engine 通过渲染硬件接口(RHI)来抽象不同的图形后端。Qt,一个用户界面框架,也有类似的机制来与不同的低级图形 API 进行交互。NVIDIA 开发的库也面临着类似的需求,并提出了重用这种抽象层的建议。这表明,在软件开发中,面对跨平台兼容性的挑战,共享和重用抽象层是一个常见且有效的策略。
问题是,在所有这些不同的渲染硬件接口中,我们应该使用哪一个?会是 WebGPU, 还是这个 NVIDIA 版本,还是 Unreal 版本,还是 Qt 版本?它们中的许多都是以特定应用程序为开发目标的,有时会有一些偏见。比如 Unreal 和 Qt, 非常适合开发游戏引擎或开发 UI 框架。如果你不确定,如果你想学习一个将在许多不同场景中重用的接口,那么它应该更加地与特定领域无关。此时,WebGPU 似乎是一个很好的选择,因为它将被很多人使用,它将是 Web 级别的 API. 将会有很多文档和强大的用户基础。
我只是要以一点变化来结束这部分。如果我们将其与 OpenGL 的情况进行比较,驱动程序有很多责任。这就是为什么实际上,OpenGL 在某个时候变得难以处理。它对驱动程序的要求太高了。特别是要进入向后兼容性,驱动程序仍然必须确保他们支持 OpenGL 1.0, 这真的很旧,不再符合现代 GPU 的当前架构。有了这种开发 RHI 的模型,事实是我们将低级驱动程序部分与可能由不同团队开发的面向用户的接口分离了,因为驱动程序在这里将由 AMD 或 NVIDIA 开发,而渲染硬件接口背后的库是由 Firefox 团队或 Chrome 团队开发的。这是一种强大的开发工作共享方式。许多现代 GPU 编程的选择将朝这个方向发展。
简单回顾一下,如果我们同时需要可移植性和性能,我们需要使用某种 RHI. 否则,我们将不得不多次编写相同的东西。WebGPU 是一个很好的候选者,由于它是领域无关的,它有足够的潜力成为你学习并在许多不同情境中重用的东西。它很可能会面向未来,因为它是为了持久而开发的,并且是依据 Web 浏览器长期维护而开发的。
另外,你还会得到一些额外的好处。首先,我发现它是一个合理的抽象级别。它的组织方式比 OpenGL 更现代。如果你把它和 Vulkan 比较,它处于更高的级别,所以它没有成千上万的代码行。到目前为止,我真的很喜欢使用它。另外,由于 Chrome 和 Firefox 正在竞争挑战 WebGPU 实现和性能,最终很可能会得到真正低开销的成果。
如何开始使用 WebGPU
我们现在可以进入第二部分,如果你想现在就开始使用 WebGPU 进行开发,我们如何开始?第一件事就是构建一个真正小的 Hello World 程序。然后我会展示应用程序的骨架,但我不会详细介绍所有部分,因为我已经开始写指南来进一步了解。相反,我会专注于如何调试,因为这也是非常重要的,特别是因为 CPU 和 GPU 有双重时间线,使得调试可能会变得棘手。
Hello World
#include <webgpu/webgpu.h>
int main(int, char**) {
// 1. Create a descriptor
WGPUInstanceDescriptor desc = {};
desc.nextInChain = nullptr;
// 2. Create an instance using the descriptor
WGPUInstance instance = wgpuCreateInstance(&desc);
// 3. Check for errors
if (!instance) {
std::cerr << "Could not inintialize WebGPU!" << std::endl;
return 1;
}
// 4. Display the object (WGPUInstance is a simple pointer)
std::cout << "WebGPU instance: " << instance << std::endl;
}
我们如何开始?首先就是包括这个 WebGPU 头文件,当然。然后我们可以创建 WebGPU 实例。我们不需要任何上下文,甚至不需要一个窗口来创建它。当我们在 WebGPU 中创建对象时,过程看起来总是一样的。有一个创建某物的函数,它接受一个描述符(这是一个结构体,包含所有可能的参数,因为有些情况下可能有很多),而不是在创建实例函数中有很多参数——它们都被隐藏在描述符中。你可以在尝试设置一些默认值。然后,检查错误并显示,这是真正的最小的 Hello World 程序。
记住,在描述符中总是有这个 nextInChain
字段,它是专门用于扩展机制的,你必须将其设置为 null
指针。创建实例函数 wgpuCreateInstance
将返回一个盲句柄,它基本上是一个指针,你可以随意复制而不用担心复制它的成本。还有两种其他类型在 API 中被操作,一些枚举值和一些结构体,它们只是用来详细描述描述符,以便在描述符中组织事物。
如何构建
我们如何在实践中构建这个程序?当 C++ 程序涉及到构建系统,一切变得棘手起来。 显而易见的问题是,这个 webgpu.h
头文件在哪里?以及如果我们有一个声明符号的头文件,它们必须在某个地方被定义,那么我们如何链接到 WebGPU 的实现呢?
正如我提到的,与其他 API 不同,这些都不是由驱动程序提供的。我们有多种选择。要么我们使用基于 Firefox 后端的 wgpu-native, 要么我们使用基于 Chrome 后端的 Dawn. 最后一个选项有点不同,我们也可以只使用 Emscripten, 它提供了 webgpu.h
头文件,但不提供任何符号。Emscripten 的工作方式是将 C/C++ 代码转换成 WebAssembly(WASM),它将这些 WebGPU 调用转换为 JavaScript 调用,这些 JavaScript 调用则是 WebGPU API 的调用。我将排除 Emscripten,因为它有点不同,但请记住,它总是一个可能性。
当我开始在这方面工作时,我会想有一个类似我在使用 OpenGL 时的体验,比如像 Glad 这样的小工具。我可以下载一些不是太大的第三方依赖,并且链接到我的程序。它不需要花时间来构建,不需要复杂的设置,不会侵入我的构建系统。我在寻找这个,并开始比较 wgpu-native 和 Dawn. 首先, wgpu 是基于 Rust 的,这很棒,但如果我正在做一个 C++ 项目,从头构建不那么容易,在这一点上我有点担心,然后我看了看 Dawn. 事实上 Dawn 没有标准构建系统,它真的需要一些额外的工具。它还需要 Python, 因为它自动生成代码的某些部分。构建 Dawn 也不容易——我并不是说这不可能,只是不容易做到。从与学生分享的角度来看,例如,或者对于那些不是构建系统专家并且不想有复杂东西的人来说,这也是一个阻碍。
信息
演讲人在这里介绍了两个衍生项目,主要是第一个分发项目,降低了构建门槛:
- WebGPU distribution, CMake 集成
- Mach, 即Mach 核心, 基于 Zig, 目前暂停维护
它们都是特定时期的历史产物,因此我把有关部分介绍省略,请参考链接了解更多。
#define WEBGPU_BACKEND_WGPU
#define WEBGPU_BACKEND_DAWN
add_subdirectory(webgpu)
add_executable(App main.cpp)
target_link_libraries(App PRIVATE webgpu)
target_copy_webgpu_binaries(App)
cmake -B build && cmake --build build
build/App
如果我们回到这个最初的问题,如何构建这个 Hello World 程序,我们将只是下载其中一个分发版本到 webgpu
路径,可以放在我们的 main.cpp
旁边,添加这个非常简单的 CMakeLists 文件,我们就可以进行构建。运行后可以看到我们的程序只是显示实例指针的地址。由于它不是 null
, 这意味着它奏效了,我们可以使用 WebGPU.
应用程序骨架
那么应用程序会是什么样子呢?首先将是设备的初始化,我稍后会重点关注这个。然后我们通常会加载资源。这是分配内存并将事物从 CPU 复制到 GPU 内存,纹理、缓冲区,还有着色器,这些是在 GPU 上执行的程序。然后我们可以初始化一组绑定,告诉 GPU 如何访问不同的资源,因为 GPU 内部包含多种不同类型的内存,每种内存都有其特定的优化访问路径。这种设计允许根据不同的使用场景选择最合适的内存类型,从而在 GPU 编程中大幅提升性能。这与 CPU 的内存管理方式相比,有着本质的不同。
void Application::run() {
initDevice(); // Different for native or web target
loadResources(); // Textures, Buffers, Samplers, Shaders
initBindings(); // Expose resources to shaders (Texture view)
initPipeline(); // Prepare configurations of the GPU's pipeline
submitCommands(); // Main Core
fetchResult(); // Get data back from the GPU
}
然后你需要设置图形或计算管道。GPU 的工作方式独特,它结合了固定的处理阶段和可编程的阶段。固定阶段是直接在硬件中实现的,你不能对其编程,只能调整一些设置。而可编程阶段,如顶点着色器、片段着色器,以及在使用计算管道时的计算着色器,允许你编写代码来控制它们的行为。管道对象就是用来保存你为这些固定和可编程阶段所设置的所有选项的。
着色器编写时,需要使用一种新的语言 WGSL, 这不同于 GLSL、HLSL 或 SPIR-V. 虽然目前你还可以在 Web 环境中使用 SPIR-V 着色器,但未来会逐渐弃用,所以建议学习 WGSL. WGSL 在概念上与其他着色器语言相似,但语法不同。
接下来是应用程序的核心操作。你会向 GPU 提交命令,让它执行着色器并运行管道,最终可能需要从 GPU 获取一些数据。这个过程是异步的,因为 VRAM(显存)和 RAM(系统内存)之间的通信需要时间。当你从 CPU 向 GPU 发送命令时,即使 CPU 函数返回了,也不意味着 GPU 已经完成了任务。这只表示驱动程序已经接收到了你的指令,并会最终将其传递给 GPU. 这种异步性在你需要从 GPU 获取结果之前不会造成问题,但在那之后,你需要处理异步操作。 最后,清理资源也是重要的一步。不同的实现(如 wgpu 和 Dawn)在资源清理方面可能有所不同。如果你在进行图形操作,还可以设置交换链,这是告诉 GPU 如何将渲染的帧缓冲区显示到屏幕上的机制。
有了所有这些,你可以自己写出这样的应用程序:加载一个 3D 模型,转动它,有一些非常基本的 UI, 你可以随意地玩耍。考虑到图形编程的实际需求,我也开发了一些小型库。一个叫 glfw3webgpu, 用于与 GLFW 通信,GLFW 通常是用于打开窗口的库,它只是你添加到项目的单个文件。我还使用 ImGui 库进行用户界面,这非常方便,但它在 WebGPU 后端有一些问题,这是我的分支(现在已经合并到 imgui)。接下来我将重点关注设备创建。
设备创建时的 Tips
void Application::initDevice() {
WGPUInstanceDescriptor desc;
desc.nextInChain = nullptr;
WGPUInstance instance = wgpuCreateInstance(&desc);
WGPURequestAdapterOptions adapterOpts;
adapterOpts.nextInChain = nullptr;
// [...]
WGPUAdapter adapter = wgpuInstanceRequestAdapterSync(instance, &adapterOpts);
RequiredLimit requiredLimits = /* important */;
WGPUDeviceDescriptor deviceDesc;
deviceDesc.nextInChain = nullptr;
deviceDesc.requiredLimits = &requiredLimits;
// [...]
this->device = wgpuAdapterRequestDeviceSync(adapter, &deviceDesc);
}
设备创建分为两部分。一是适配器 WGPUAdapter
的创建,然后是设备 device
的创建。获取适配器实际上并不涉及创建新的实体,而是获取关于硬件上下文的信息,包括设备的硬件限制和功能。基于这些信息,我们可以为所使用的设备对象设置虚拟限制,以此来抽象化适配器。这一机制至关重要,因为若不设定任何限制,设备将继承适配器的硬件限制,这可能导致应用程序在不可预知的地方失败。例如,尝试创建的纹理超出了最大纹理数量限制,导致代码执行失败。这种情况在不同硬件上尤其常见,因为不同的 GPU 可能有不同的限制。
因此,强烈建议在创建设备时明确指定限制。这样做可能导致设备无法创建,但至少能立即知道设备不支持应用程序的要求。设置限制实际上是在声明应用程序的最低硬件要求。为了避免失败,可以定义多个具有不同限制预设的质量层次。获取适配器后,检查支持的限制,并选择第一个完全支持的限制集合。然后根据这个集合来配置设备,并可能通过一个全局变量或应用程序属性来标记当前的质量层次,使应用程序能够无缝适应。
对于将 Web 作为构建目标的设备初始化,过程在 JavaScript 中处理。因此,不需要执行上述步骤,只需使用 Emscripten 提供的 emscripten_webgpu_get_device
函数,并在 JavaScript 中完成初始化。这对于确保代码的可移植性至关重要,尤其是在目标平台为 Web 时。
如何调试 WebGPU
我们如何调试使用 WebGPU 的应用程序?首先要注意的是错误回调。
错误回调
你可以在创建设备后立即设置两个错误回调。
// Right after device creation
wgpuDeviceSetUncapturedErrorCallback(device, /* ... */);
wgpuDeviceSetDeviceLostCallback(device, /* ... */);
对于本地编程来说,可能最重要的是设置 UncapturedErrorCallback
。这个回调基本上是通过它发送所有错误。下面的例子是一个参考,我建议你在这里设置一个断点,这样你的程序就会在遇到错误时即时地停下,方便你调试。
auto handle = device.setUncapturedErrorCallback(
[](ErrorType type, char const* message) {
std::cout << "Device error: (type: " << type << ")";
if (message) std::cout << "\n" << message;
std::cout << std::endl; // Breakpoint here
});
错误消息的表现形式因不同的实现而异。以 wgpu-native 为例,其错误消息的表述相对简洁明了。相比之下,Dawn 的实现提供了更为详细的错误描述,包括在遇到问题之前执行的所有步骤,以及使用标签来标识问题相关的对象。这种标签系统允许开发者在定义资源时为其分配一个易于识别的标签,从而在错误消息中快速定位问题资源。
此外,不同的 WebGPU 实现在着色器验证方面也有所不同。Dawn 使用 Tint 作为其着色器编译器,而 wgpu 则使用 Mega. 这两个编译器在报告错误时采用了不同的方式,这可能成为开发者选择 Dawn 而非 wgpu 的一个考虑因素。需要注意的是,尽管 Dawn 提供了更详细的错误报告,但其分发版本仍需要时间编译,因为它不提供预构建版本。
图形调试器
另一个非常有用的东西是图形调试器。这不是 WebGPU 特有的,但当你想要与 GPU 交谈时,你需要使用它。典型的例子包括 RenderDoc 和 NVIDIA Nsight. 这些调试器的主要特点是它们展示了来自底层 API 的信息,而非直接来自 WebGPU. 由于这些工具工作在 CPU 与 GPU 交互的层面,它们捕获的是底层 API 的信息。尽管如此,通过观察操作的顺序,开发者通常可以推断出哪些命令与代码中的特定部分相对应,尤其是在应用程序相对简单时。然而,随着应用程序复杂度的增加,这种方法可能会遇到困难。
性能调试
// Not implemented in WebGPU yet
QuerySetDescriptor querySetDesc;
querySetDesc.count = 1;
querySetDesc.type = WGPUQueryType::Timestamp;
WGPUQuerySet querySet = device.CreateQuerySet(querySetDesc);
ComputePassTimestampWrite timestampWrites;
timestampWrites.location = ComputePassTimestampLocation::Beginning;
timestampWrites.queryIndex = 0;
timestampWrites.querySet = querySet;
// [...]
在调试过程中,性能分析是另一个关键方面,但它并不直观,特别是在 GPU 编程中。由于 CPU 和 GPU 在不同的时间线上运行,CPU 发出的命令何时开始执行以及何时完成对 CPU 来说是未知的,因此无法直接用 CPU 端的工具来测量 GPU 性能。理论上,WebGPU 通过时间戳查询提供了一种测量 GPU 性能的方法,即通过 GPU 来测量特定操作的执行时间并返回结果。然而,这一功能尚未实现,并且存在关于其是否会被实现的担忧,主要是因为时间戳查询可能泄露敏感信息,特别是在需要保护用户隐私的网络环境中。尽管如此,这一功能在本地编程中仍然有其潜在用途,尽管它可能不会在 JavaScript API 中启用。
computePass.pushDebugGroup("MC: Count vertices"); // <-- Start
computePass.setPipeline(pipeline);
computePass.setBindGroup(0, bindGroup, 0, nullptr);
computePass.dispatchWorkgroups(resolution - 1, resolution - 1, resolution - 1);
computePass.popDebugGroup(); // <-- End
此外,调试组是一个有用的工具,它本身不影响程序的执行,但可以在如 Nsight 等工具中提供性能分析的视图,允许开发者在 C++ 代码中进行标注,从而有助于性能测量。
作为一个具体的例子,我比较了两种用于生成轮廓线的有符号距离场算法,并将 wgpu-native 与 Dawn 的实现进行了对比。需要注意的是,这些比较并不基于广泛的基准测试,而仅仅是作为一个示例。结果显示,尽管 Dawn 在性能上略慢,但通过禁用某些特性可能获得性能提升。这些初步结果表明,无论是 wgpu-native 还是 Dawn, 它们都能以低层次访问 GPU 的方式实现相似的性能,没有引入过多的开销。这强调了通过实际比较非 WebGPU 实现的重要性,对于未来的性能优化和实现,我持乐观态度。
WebGPU 是否准备好了?
也许剩下的关键问题是,它是否准备好现在用于本地应用程序?
目前的痛点
第一件事是后端支持。相当不错,无论是 wgpu 还是 Dawn, 对于现代桌面,它得到了很好的支持,但我应该提到一些痛点。其中一个是我之前提到的异步操作,用于从 GPU 获取数据,我最终仍然需要等待一些无限循环,通过提交空队列,只是为了让后端检查异步操作是否完成。也许在标准的后续版本中会有统一的方法来做到这一点。
另一件事是,我有点怀念着色器模型内省(Shader Model Introspection),指在图形编程中,能够查询和检查着色器程序的属性和结构的能力。这包括但不限于查询着色器中定义的变量、它们的类型、布局(如绑定槽位)以及其他元数据。内省允许开发者在运行时获取有关着色器的详细信息,而不是仅仅依赖于静态代码分析或文档。
@group(0) @binding(0)
var<uniform> uniforms: Uniforms;
@group(0) @binding(1)
var distance_grid_write: texture_storage_3d<rgba16float, write>;;
通过仅使用数字(如绑定 0,绑定 1)而不是变量名来指定如何从 CPU 向 GPU 的着色器传递数据,可能会使得 C++ 代码变得不那么清晰。这种方法缺乏直观性,因为开发者必须记住每个数字代表的具体资源,而不是通过更具描述性的名称来引用它们。
const pipline = device.createRenderPipeline({
layout: "auto",
// [...]
})
JavaScript 版本的 WebGPU 尝试通过自动布局特性来解决这个问题,即通过分析着色器代码自动推断绑定布局。然而,这种自动化的方法引入了一些问题,导致它被从本地(非 Web)版本中移除。这可能是因为自动布局算法可能无法准确地处理所有情况,或者因为它引入了性能开销或复杂性,这在低级图形编程中通常是不可接受的。
我在这里举了很多 wgpu 和 Dawn 实现之间的差异,但我重点关注的是,二者关于资源管理和内存释放机制的技术细节还没有达成一致:
wgpuDeviceDrop(device);
wgpuDeviceRelease(device);
wgpuDeviceReference(device);
在 wgpu 中,资源的释放是通过 Rust 语言的 Drop 特性来管理的。当一个对象不再被任何变量引用时,Rust 的所有权系统保证该对象会被自动销毁。这意味着,一旦 Drop 被调用,该对象就被认为是不再被任何东西使用的,从而触发资源的释放和内存的回收。与 wgpu 不同,Dawn 使用显式的 Release 方法来管理资源的释放。调用 Release 并不立即销毁对象,而是表示调用者已经完成了对该资源的使用。如果还有其他地方正在使用该资源(即引用计数不为零),资源不会被释放。这种方式下,Dawn 实际上暴露了一个引用计数功能,允许多个部分共享并管理同一个资源,直到所有的引用都被释放,资源才真正被销毁。
WebGPU 的限制
最后,一些限制可能即使在更长的时间内仍然存在。
- 在桌面环境中,时间戳查询应该是可行的,但在 JavaScript 环境中可能存在问题。如果程序逻辑依赖于时间戳查询来优化执行路径,这可能会成为一个挑战。
- 在复杂的应用程序中,着色器的编译可能需要相当长的时间(从几秒到几分钟不等)。在桌面应用程序中,可以缓存编译后的着色器版本,或者由驱动程序管理。问题在于,是否有足够的控制来管理这个缓存,以及如何避免用户体验问题,比如用户访问他们常用的网络应用程序时,需要等待着色器重新编译。
- 平铺渲染是一种针对低端设备(特别是那些内存带宽较低的设备)的渲染技术。问题在于,是否能在 WebGPU 中检测到平铺渲染的需求,以便为这类低功耗设备调整代码。到目前为止,只是提供了 一些其他非常有趣的辩论 。
- 如果你不是针对 Web 目标,扩展机制 可以使用。
我应该使用 WebGPU 吗?
在寻找既可移植又面向未来的 API 的过程中,我选择了 OpenGL, 但随着时间的推移,这个选择开始显现出一些问题。因此,我建议转向 WebGPU. 现在是开始探索和使用 WebGPU 的好时机。需要做好准备,接下来的几个月可能会有一些变动,这些变动可能会修复一些问题,也可能会引入一些微小的更改,这些更改可能会破坏现有的代码。不过,此时不应该预期会有重大变化。现在学习 WebGPU, 可以让你对它最终的形态有一个大致的了解。我认为 WebGPU 是一个领域不可知的渲染硬件接口,它既不是太低级,也更符合底层的方式,因此可以实现高效性能,同时又不至于过于底层。它是面向未来的,将成为网络的 API, 因此将拥有庞大的用户基础。我相信,由于这个原因,WebGPU 将成为最常用的图形 API, 包括在桌面环境中。尽管 WebGPU 还未完全完成,但它正在积极开发中,预计不久将准备就绪。
在我的案例中,我之前从未考虑过针对 Web, 因为我不想调整我的应用程序以使其与 Web 兼容。而现在,我不必担心这个问题,我可以继续在桌面上进行我的实验,然后轻松地在网络上分享它们,而无需担心兼容性问题。尽管目前使用 WebGPU 支持就绪浏览器的人不多,但这种情况正在改变。
我展示的一个例子不涉及 3D, 而是展示了人们如何使用 WebGPU 进行神经网络推理,例如用于稳定扩散的图像生成。这是一个非常相关的例子,说明了 WebGPU 如何在网络中启用使用 GPU 的应用,即使这些应用与 3D 无关。很可能在许多其他领域也会有广泛的应用。
最后,我提供了一份 学习指南,其中包含了更多关于我在这里提到的所有内容的详细信息。我还创建了一个 Discord 服务器来支持这个指南,但我也鼓励你加入 wgpu和Dawn 的现有社区,因为那里聚集了大多数开发者。