从 WebGL 过渡到 WebGPU 时代

家好,很高兴又见面了,我是"高级前端‬进阶‬",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。

注意,本篇文章是译文,原文链接已经在文末指出,同时对原文内容做了部分修改。

前言

作为一名 WebGL 开发人员,可能会对使用 WebGPU 感到既害怕又兴奋,WebGPU 是 WebGL 的后继者,它将现代图形 API 的进步带到了 Web 上。

令人欣慰的是,WebGL 和 WebGPU 共享许多核心概念。 这两个 API 都允许开发者在 GPU 上运行称为着色器的小程序。 WebGL 支持顶点和片段着色器,而 WebGPU 也支持计算着色器。 WebGL 使用 OpenGL 着色语言 (GLSL),而 WebGPU 使用 WebGPU 着色语言 (WGSL)。 尽管两种语言不同,但基本概念大多相同。

本文重点介绍了 WebGL 和 WebGPU 之间的一些差异,以帮助开发者快速入门。

全局状态(Global state)

WebGL 有很多全局状态,某些设置适用于所有渲染操作,例如:绑定哪些纹理和缓冲区。 开发者可以通过调用各种 API 函数来设置此全局状态,并且在更改之前一直有效。

但是,WebGL 中的全局状态是错误的主要来源,因为开发者很容易忘记更改全局设置。 此外,全局状态使代码共享变得困难,因为开发人员需要格外小心以防意外更改全局状态,从而影响代码的其他部分。

WebGPU 是无状态的 API,并且不维护全局状态。 相反,它使用管道的概念来封装 WebGL 中全局的所有渲染状态。 管道包含要使用的混合、拓扑和属性等信息,而且管道是不可变的。 如果要更改某些设置,则需要创建另一个管道。

如果熟悉 OpenGL,可能还记得使用着色器程序,可以将管道视为更强大的版本,管道描述了 GPU 在处理一组数据时将执行的所有操作。比如下面的例子:

{
    // 1.
    let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
        label: Some("Render Pass"),
        color_attachments: &[
            // This is what @location(0) in the fragment shader targets
            Some(wgpu::RenderPassColorAttachment {
                view: &view,
                resolve_target: None,
                ops: wgpu::Operations {
                    load: wgpu::LoadOp::Clear(
                        wgpu::Color {
                            r: 0.1,
                            g: 0.2,
                            b: 0.3,
                            a: 1.0,
                        }
                    ),
                    store: true,
                }
            })
        ],
        depth_stencil_attachment: None,
    });
    // NEW!
    render_pass.set_pipeline(&self.render_pipeline); // 2.
    render_pass.draw(0..3, 0..1); // 3.
}
// ...

WebGPU 还使用命令编码器将命令一起批处理并按照记录的顺序执行。 例如,这在阴影贴图中很有用,在一次传递对象时,应用程序可以记录多个命令流,每个命令流对应每个灯光的阴影贴图。

const commandEncoder = gpuDevice.createCommandEncoder();
commandEncoder.clearBuffer(buffer);
const commandBuffer = commandEncoder.finish();
gpuDevice.queue.submit([commandBuffer]);

总而言之,由于 WebGL 的全局状态模型使得创建健壮、可组合的库和应用程序变得困难且脆弱,因此 WebGPU 显著减少了开发人员在向 GPU 发送命令时需要跟踪的状态量。

不再同步(Sync no more)

在 GPU 上,发送命令并同步等待结果通常效率很低,因为这可能会刷新管道并导致气泡(bubbles)。 在 WebGPU 和 WebGL 中尤其如此,它们使用多进程架构,GPU 驱动程序在与 JavaScript 不同的进程中运行。

在某些架构中,管道的执行阶段必须始终在每个周期执行一个操作。 在这种情况下,气泡是通过向执行阶段提供 NOP(“无操作”)指令来实现的。

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

gl.getError(); // gl.NO_ERROR (0)
gl.enable(gl.FOOBAR);
gl.getError(); // gl.INVALID_ENUM;

为了避免这些气泡,WebGPU 被设计为完全异步的。 错误模型和所有其他操作都是异步发生的。 例如,当创建纹理时,操作立即成功,即使纹理实际上是错误的。 开发者只能异步发现错误,这种设计可以保持跨进程通信无气泡,并为应用程序提供可靠的性能。

计算着色器

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

// Get a GPU buffer in a mapped state and an arrayBuffer for writing.
const gpuBuffer = device.createBuffer({
  mappedAtCreation: true,
  size: 4,
  usage: GPUBufferUsage.MAP_WRITE
});
const arrayBuffer = gpuBuffer.getMappedRange();

// Write bytes to buffer.
new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

与顶点和片段着色器不同,计算着色器不仅限于图形处理,而且可以用于各种任务,例如:机器学习、物理模拟和科学计算。 计算着色器由数百甚至数千个线程并行执行,这使得它们对于处理大型数据集非常高效。

视频帧处理

使用 JavaScript 和 WebAssembly 处理视频帧有一些缺点,比如:

WebGPU 没有这些限制,由于它与 WebCodecs API 紧密集成,因此非常适合处理视频帧。以下代码片段展示了如何将 VideoFrame 作为外部纹理导入到 WebGPU 中并对其进行处理。

// 初始化 WebGPU 设备和管道...
// 配置画布上下文...
// 将相机流馈送到视频...
(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 }],
  });
  // 提交命令到 GPU
}

默认应用程序可移植性

WebGPU 强制请求限制(request limits), 默认情况下,requestDevice() 返回的 GPUDevice 可能与物理设备的硬件功能不匹配,而是所有 GPU 的合理且最低公分母。 通过要求开发人员请求设备限制,WebGPU 确保应用程序将在尽可能多的设备上运行。

画布处理

在开发者创建 WebGL 上下文并提供上下文属性(例如 alpha、antialias、colorSpace、depth、preserveDrawingBuffer 或 stencil)后,WebGL 会自动管理画布。

canvas.getContext("webgl", { antialias: false, depth: false });

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

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

有用的错误消息

WebGPU 为从 API 返回的每条消息提供一个调用堆栈。 这意味着开发者可以快速查看代码中发生错误的位置,有助于调试和修复错误。

除了提供调用堆栈之外,WebGPU 错误消息也易于理解且可操作。 错误消息通常包括错误的描述以及如何修复错误的建议。

interface mixin GPUObjectBase {
    attribute USVString label;
};

WebGPU 还允许开发者为每个 WebGPU 对象提供自定义标签。 然后浏览器会在 GPUError 消息、控制台警告和浏览器开发人员工具中使用该标签。

从名称到索引

在 WebGL 中,许多事物都是通过名称联系起来的。 例如,可以在 GLSL 中声明一个名为 myUniform 的统一变量,并使用 gl.getUniformLocation(program, 'myUniform') 获取其位置。 如果错误输入统一变量的名称,这会很方便,因为会收到错误消息。

struct A {
  @location(0) x: f32,
  // Despite locations being 16-bytes, x and y cannot share a location
  @location(1) y: f32
}

// in1 occupies locations 0 and 1.
// in2 occupies location 2.
// The return value occupies location 0.
@fragment
fn fragShader(in1: A, @location(2) in2: f32) -> @location(0) vec4 {
 // ...
}

另一方面,在 WebGPU 中,一切都完全通过字节偏移或索引(通常称为位置)连接。 开发者有责任保持 WGSL 和 JavaScript 中代码的位置同步。

空间约定差异

在 WebGL 中,Z 剪辑空间范围是从-1 到 1。在 WebGPU 中,Z 剪辑空间范围是从 0 到 1。这意味着 z 值为 0 的对象距离相机最近,而 z 值为 0 的对象距离相机最近, 1 最远。

WebGL 使用 OpenGL 约定,其中 Y 轴向上,Z 轴朝向观察者。 WebGPU 使用 Metal 约定,其中 Y 轴向下,Z 轴在屏幕之外。 请注意,在帧缓冲区坐标、视口坐标和片段/像素坐标中,Y 轴方向向下。 在剪辑空间中,Y 轴方向仍然像 WebGL 中一样向上。


参考资料

https://developer.chrome.com/blog/from-webgl-to-webgpu/

https://webglfundamentals.org/webgl/lessons/resources/webgl-state-diagram.html

https://sotrh.github.io/learn-wgpu/beginner/tutorial3-pipeline/#using-a-pipeline

https://gpuweb.github.io/gpuweb/#command-encoding

https://en.wikipedia.org/wiki/Pipeline_stall

https://gpuweb.github.io/gpuweb/wgsl/#input-output-locations

展开阅读全文

页面更新:2024-03-13

标签:画布   上下文   纹理   气泡   开发者   全局   管道   命令   状态   错误   时代

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top