WebGPU api 使 web 开发人员能够使用底层系统的 GPU(图形处理器)进行高性能计算并绘制可在浏览器中渲染的复杂图形。WebGPU 是 WebGL 的继任者,为现代 GPU 提供更好的兼容、支持更通用的 GPU 计算、更快的操作以及能够访问到更高级的 GPU 特性。
WebGPU提供了与现代 GPU API 兼容的更新的通用架构,它会让你感到更加丝滑。它支持图形渲染,同时对 GPGPU 计算也有一流的支持。在 CPU 端渲染单个对象的成本要低得多,并且它支持现代化的 GPU 渲染特性,例如,基于计算的粒子和用于后期处理的滤镜,如颜色效果、锐化和景深模拟。此外,它也可以直接在 GPU 上处理诸如剔除和骨骼动画模型等耗费大量计算资源的任务。
WebGPU API的介绍与使用设备 GPU 和运行 WebGPU API 的 web 浏览器之间有多个抽象层。理解这些,对你开始学习 WebGPU 很有用:
逻辑设备(通过 GPUDevice (en-US) 对象实例表示)是 web 应用程序访问所有 WebGPU 功能的基础。访问设备的过程如下:
将其与一些特性检测检查结合起来,可以按如下方式实现上述过程:
async function init() {
if (!navigator.gpu) {
throw Error("WebGPU not supported.");
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw Error("Couldn't request WebGPU adapter.");
}
const device = await adapter.requestDevice();
//...
}
管线(pipeline)是一个逻辑结构,其包含在你完成程序工作的可编程阶段。WebGPU 目前能够处理两种类型的管线:
上面提到的着色器是通过 GPU 处理的指令集。WebGPU 着色器语言是用称为 WebGPU 着色器语言(WGSL)的低级的类 Rust 语言编写的。
你可以通过几种不同的方式去构建 WebGPU 应用程序,但该过程应包含以下步骤:
在下面的部分,我们将研究一个基本的渲染管线演示,让你知道探索它需要什么。稍后,我们也将研究一个基础的计算管线示例,看看它与渲染管线有什么不同。
在我们的基础的渲染管线示例中,我们给 <canvas> 元素一个纯蓝色背景并且在其上绘制三角形。
我们使用以下着色器代码。顶点着色阶段(@vertex 代码块)接受包含位置和颜色的数据分块,根据给定的位置定位顶点,插入颜色,然后将数据传入到片元着色器阶段。片元着色阶段(@fragment 代码块)接受来自顶点着色器阶段的数据,并更具给定的颜色为顶点着色。
const shaders = `
struct VertexOut {
@builtin(position) position : vec4f,
@location(0) color : vec4f
}
@vertex
fn vertex_main(@location(0) position: vec4f,
@location(1) color: vec4f) -> VertexOut
{
var output : VertexOut;
output.position = position;
output.color = color;
return output;
}
@fragment
fn fragment_main(fragData: VertexOut) -> @location(0) vec4f
{
return fragData.color;
}
`;
备注: 在我们的演示中,我们将我们的代码存储在模板字面量中,但是你可以将其存储在任何地方,以便可以很容易地将其作为文本取回,并输入到你的 WebGPU 程序中。例如,另一种常见的作法是将着色器存储在 <script> 元素中并且使用 Node.textContent 取回内容。用于 WGSL 的正确 MIME 类型是 text/wgsl。
为了确保你的着色器代码可提供给 WebGPU,你必须通过 GPUDevice.createShaderModule() (en-US) 调用,将其放入 GPUShaderModule (en-US) 中,将你的着色器代码作为描述符对象中的属性传递。例如:
const shaderModule = device.createShaderModule({
code: shaders,
});
在渲染管线中,我们需要指定在哪个位置渲染图形。在这种情况下,我们获得对屏幕上 <canvas> 元素的引用,然后使用 webgpu 参数调用 htmlCanvasElement.getContext(),以返回它的 GPU 上下文(一个 GPUCanvasContext 实例)。
从这里继续,我们将通过调用 GPUCanvasContext.configure() (en-US) 去配置上下文,向它传递包含渲染信息的可选对象,包括 GPUDevice (en-US)、纹理的格式以及在半透明纹理时使用的 alpha 模式。
const canvas = document.querySelector("#gpuCanvas");
const context = canvas.getContext("webgpu");
context.configure({
device: device,
format: navigator.gpu.getPreferredCanvasFormat(),
alphaMode: "premultiplied",
});
备注: 确定纹理格式的最佳做法是使用 GPU.getPreferredCanvasFormat() 方法;这将为用户的设备选择最有效的格式(bgra8unorm 或 rgba8unorm)。
接下来,我们将以 WebGPU 可以使用的数据形式向它的程序提供我们的数据。我们的数据最初在 Float32Array 中提供,每个三角形包含 8 个数据点——X、Y、Z、W 代表位置,R、G、B、A 代表颜色。
const vertices = new Float32Array([
0.0, 0.6, 0, 1, 1, 0, 0, 1, -0.5, -0.6, 0, 1, 0, 1, 0, 1, 0.5, -0.6, 0, 1, 0,
0, 1, 1,
]);
但是,我们这里有一个问题。我们需要将我们的数据放入 GPUBuffer (en-US)。在幕后,这种类型的缓冲区与 GPU 的核心非常紧密的集成在一起,以实现所需的高性能处理。由于副作用,该内存不能通过主机系统上运行的进程(例如浏览器)访问。
GPUBuffer (en-US) 通过调用 GPUDevice.createBuffer() (en-US) 创建。我们给它与 vertices 数组长度等同的大小,这样它可以包含所有数据,以及 VERTEX 和 COPY_DST 使用标志去指示缓冲区将用于顶点缓冲区和复制操作的目的地。
const vertexBuffer = device.createBuffer({
size: vertices.byteLength, // make it big enough to store vertices in
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
我们将使用映射操作将我们的数据放入 GPUBuffer,就像我们在计算管线实例中,将数据从 GPU 读回到 JavaScript。然而,在这种情况下,我们将使用便利的 GPUQueue.writeBuffer() (en-US) 方法,它将要写入缓冲区的、要写入数据源的、每个偏移值和要写入数据的的大小作为参数(我们已经指定了数据的整个长度)。然后浏览器会找出写入数据的最高效的方式。
device.queue.writeBuffer(vertexBuffer, 0, vertices, 0, vertices.length);
现在我们已经将数据放入缓冲区,设置的下一部分是实际创建我们的管线,为渲染准备好。
首先,我们创建一个对象,该对象描述我们顶点数据所需的布局。这完美地描述了我们在 vertices 数组和顶点着色阶段看到的内容——每个顶点都有位置和颜色数据。两者都采用 float32x4 格式(映射到 WGSL 的 vec4<f32> 类型),颜色数据从每个顶点的 16 字节偏移量开始。arrayStride 指定了步幅,表示构成每个顶点的字节数,stepMode 指定了应该按顶点获取数据。
const vertexBuffers = [
{
attributes: [
{
shaderLocation: 0, // 位置
offset: 0,
format: "float32x4",
},
{
shaderLocation: 1, // 颜色
offset: 16,
format: "float32x4",
},
],
arrayStride: 32,
stepMode: "vertex",
},
];
下一步,我们创建一个描述符对象,该对象指定了我们渲染管线阶段的配置。对于两个着色阶段,我们指定了可以在 shaderModule 中找到相关代码的 GPUShaderModule (en-US),以及找到每个阶段入口点的函数名称。
此外,在顶点着色阶段,我们提供我们的 vertexBuffers 对象,来提供顶点数据的预期状态。在我们的片元着色阶段,我们提供了一组颜色目标说明的数组,其指示渲染的格式(这与我们之前在 canvas 上下文配置中指定的格式相匹配)。
我们也指定了一个 primitive 说明,在这种情况下,它只是说明了我们将要绘制的原始类型,以及 layout 为 auto。layout 属性定义了在管线执行期间,所有 GPU 资源(缓冲区、纹理等)的布局(结构、用途和类型。在更复杂的应用程序中,这将采用 GPUPipelineLayout (en-US) 对象的形式,使用 GPUDevice.createPipelineLayout() (en-US) 创建(你可以在我们的基础的计算管线看见这个示例),它允许 GPU 提前弄清楚如何更有效地运行管线。然而,在这里我们指定了 auto 值,这将导致管线基于着色器代码中定义的任何绑定生成隐式绑定组布局。
const pipelineDescriptor = {
vertex: {
module: shaderModule,
entryPoint: "vertex_main",
buffers: vertexBuffers,
},
fragment: {
module: shaderModule,
entryPoint: "fragment_main",
targets: [
{
format: navigator.gpu.getPreferredCanvasFormat(),
},
],
},
primitive: {
topology: "triangle-list",
},
layout: "auto",
};
最终,我们通过传递 pipelineDescriptor 参数给 GPUDevice.createRenderPipeline() (en-US) 方法调用,我们创建了一个 GPURenderPipeline (en-US)。
const renderPipeline = device.createRenderPipeline(pipelineDescriptor);
现在所有设置都已完成,实际上,我们可以运行一个渲染通道并在我们的 <canvas> 上进行绘制。为了对稍后发送给 GPU 的任何指令进行编码,你需要创建一个 GPUCommandEncoder (en-US) 实例,这是调用 GPUDevice.createCommandEncoder() (en-US) 完成的。
const commandEncoder = device.createCommandEncoder();
下一步,我们通过调用 GPUCommandEncoder.beginRenderPass() (en-US) 创建 GPURenderPassEncoder (en-US) 实例来开始运行渲染通道。该方法采用一个描述符对象作为参数,唯一的必须属性是 colorAttachments 数组。在该实例中,我们指定了:
const clearColor = { r: 0.0, g: 0.5, b: 1.0, a: 1.0 };
const renderPassDescriptor = {
colorAttachments: [
{
clearValue: clearColor,
loadOp: "clear",
storeOp: "store",
view: context.getCurrentTexture().createView(),
},
],
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
现在,我们可以调用渲染通道编码器的方法去绘制我们的三角形:
passEncoder.setPipeline(renderPipeline);
passEncoder.setVertexBuffer(0, vertexBuffer);
passEncoder.draw(3);
要完成对指令序列的编码并将它们发送给 GPU,还需要三个步骤。
这三个步骤可以通过以下两行来实现。
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
在我们的基础计算演示中,我们让 GPU 计算一些值,将它们存储到输出缓存中,将数据复制到暂存缓冲区,然后映射该暂存缓冲区,以便数据可以读出到 JavaScript 并且记录到控制台。
该应用程序与基础的渲染演示有着相似的结构。我们以与之前相同的方式创建一个 GPUDevice (en-US) 引用,并通过调用 GPUDevice.createShaderModule() (en-US) 将我们的着色器代码封装到 GPUShaderModule (en-US)。这里的区别在于我们的着色器代码仅有一个着色阶段,@compute 阶段:
// 定义全局的缓冲区大小
const BUFFER_SIZE = 1000;
const shader = `
@group(0) @binding(0)
var<storage, read_write> output: array<f32>;
@compute @workgroup_size(64)
fn main(
@builtin(global_invocation_id)
global_id : vec3u,
@builtin(local_invocation_id)
local_id : vec3u,
) {
// Avoid accessing the buffer out of bounds
if (global_id.x >= ${BUFFER_SIZE}) {
return;
}
output[global_id.x] =
f32(global_id.x) * 1000. + f32(local_id.x);
}
`;
在该示例中,我们创建了两个 GPUBuffer (en-US) 实例去处理我们的数据,output 缓冲区高速地写入 GPU 计算结果,stagingBuffer 缓冲区用于将 output 的内容复制到自身,它可以被映射以允许 JavaScript 访问这些值。
const output = device.createBuffer({
size: BUFFER_SIZE,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
const stagingBuffer = device.createBuffer({
size: BUFFER_SIZE,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
当创建管线时,我们需要为管线指定一个绑定组。这将首先创建 GPUBindGroupLayout (en-US)(通过调用 GPUDevice.createBindGroupLayout() (en-US)),该布局定义了 GPU 资源(例如将在此管线中使用的缓冲区)的结构和用途。此布局将用作绑定组的模板。在这种情况下,我们将管线与一个单一的内存缓冲区绑定,绑定到绑定槽 0(这与我们的着色器代码中的相关绑定号匹配——@binding(0)),可在管线的计算阶段使用,并将缓冲区的用途定义为 storage。
const bindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: "storage",
},
},
],
});
下一步,我们通过调用 GPUDevice.createBindGroup() (en-US) 创建 GPUBindGroup (en-US)。我们通过此方法调用一个描述符对象,该对象指定了这个绑定组应该基于的绑定组布局,以及绑定到布局中定义的插槽变量的详细信息。在这种情况下,我们声明了绑定插槽 0,并指定了之前定义的 output 缓冲区应该绑定到它。
const bindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: output,
},
},
],
});
备注: 你可以通过调用 GPUComputePipeline.getBindGroupLayout() (en-US) 方法检索在创建绑定组时使用的隐式布局。还有一个可以用于渲染管线的版本:参见 GPURenderPipeline.getBindGroupLayout() (en-US)。
上述一切就绪后,我们现在可以通过调用 GPUDevice.createComputePipeline() (en-US) 并向它传递一个管线描述符对象创建计算管线。这与创建渲染管线的方式类似。我们描述计算着色器,指定在哪个模块中查找代码以及入口点是什么。我们也为管线指定了 layout,在本例中,我们通过调用 GPUDevice.createPipelineLayout() (en-US) 创建一个基于之前定义的 bindGroupLayout 的布局。
const computePipeline = device.createComputePipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout],
}),
compute: {
module: shaderModule,
entryPoint: "main",
},
});
唯一的区别是,这里我们没有指定原始类型,因为我们不需要绘制任何东西。
在结构上,运行计算通道与运行渲染通道类似。首先,我们使用 GPUCommandEncoder.beginComputePass() (en-US) 创建通道编码器。
在发出指令时,我们使用相同的方式指定管线,使用 GPUComputePassEncoder.setPipeline() (en-US)。然后,我们使用 GPUComputePassEncoder.setBindGroup() (en-US) 指定想要使用的 bindGroup 来指定在计算中使用的数据,并使用 GPUComputePassEncoder.dispatchWorkgroups() (en-US) 指定要运行并行计算的 GPU 工作组的数量。
最后,我们使用 GPURenderPassEncoder.end() (en-US) 发出渲染通道指令结束的信号。
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.dispatchWorkgroups(Math.ceil(BUFFER_SIZE / 64));
passEncoder.end();
在使用 GPUQueue.submit() (en-US) 将编码指令提交给 GPU 执行之前,我们使用 GPUCommandEncoder.copyBufferToBuffer() (en-US) 将 output 缓冲区的内容复制到 stagingBuffer 缓冲区中。
// 复制 output 缓冲去到 staging 缓冲区
commandEncoder.copyBufferToBuffer(
output,
0, // 来源缓冲区偏移量
stagingBuffer,
0, // 目的缓冲区偏移量
BUFFER_SIZE
);
// 通过将命令缓冲区数组传递给命令队列以执行来结束
device.queue.submit([commandEncoder.finish()]);
一旦输出数据可用于 stagingBuffer,我们使用 GPUBuffer.mapAsync() (en-US) 方法将数据映射到中间内存,并使用 GPUBuffer.getMappedRange() (en-US) 获取映射范围的引用,将数据复制到 JavaScrip,并将其记录到控制台。完成后,我们还会取消映射到 stagingBuffer。
// 映射 staging 缓冲区,以便读回到 JS
await stagingBuffer.mapAsync(
GPUMapMode.READ,
0, // 偏移量
BUFFER_SIZE // 长度
);
const copyArrayBuffer = stagingBuffer.getMappedRange(0, BUFFER_SIZE);
const data = copyArrayBuffer.slice();
stagingBuffer.unmap();
console.log(new Float32Array(data));
WebGPU 调用在 GPU 进程中以异步的方式进行验证。如果发现错误,有问题的调用会在 GPU 端标记为无效。如果依赖于一个无效调用返回值的另一个调用被执行,那么该对象将也被标记为无效,以此类推。因此,WebGPU 的错误被称为“传染性错误”。
每个 GPUDevice (en-US) 实例都维护这自己的错误作用域栈。这个栈最初是空的,但是你可以通过调用 GPUDevice.pushErrorScope() (en-US) 来开始推入错误作用域到栈,以捕获特定类型的错误。
一旦完成错误捕获,你可以通过调用 GPUDevice.popErrorScope() (en-US) 来结束捕获。这会从栈中弹出作用域并返回一个 Promise,该 Promise 兑现为一个对象(GPUInternalError (en-US)、GPUOutOfMemoryError (en-US) 或 GPUValidationError (en-US)),描述在作用域内捕获的第一个错误,如果没有错误捕获,则是 null。
在适当的“验证”部分,我们试图去提供帮助你理解为什么你的 WebGPU 代码发生错误的有用信息,其中列出了避免错误的条件。例如,参见 GPUDevice.createBindGroup() 检验部分 (en-US)。其中一些信息很复杂,我们决定仅列出以下错误标准,而不是重复规范:
你可以在关于 WebGPU 错误处理的解释中找到更多有关信息——参见对象的有效性和销毁状态以及错误。WebGPU 错误处理的最佳实践提供了很多有关真实世界的示例和建议的有用信息。
备注: 在 WebGL 中处理错误的历史方式是提供一个 getError() (en-US) 方法,该方法返回错误的信息。这种方式存在问题,因为它会同步返回错误,这对性能是不利的——每次调用都需要往返到 GPU 并且需要所有先前发出的操作都已经完成。它的状态模型也是扁平的,这意味着错误可以在不相关的代码之间泄露。WebGPU 的创建者决定改变这一点。
来源:https://developer.mozilla.org/zh-CN/docs/Web/API/WebGPU_API
CSS3 硬件加速又叫做 GPU 加速,是利用 GPU 进行渲染,减少 CPU 操作的一种优化方案。页面css做图片滑动,滚动,特别是手机端,可能出现卡顿,闪白等情况,解决这些动画卡顿的情况,我们通常可以采用GPU加速的方式
你是否曾经尝试过运行复杂的计算,却发现它需要花费很长时间,并且拖慢了你的进程?有很多方法可以解决这个问题,例如使用 web worker 或后台线程。GPU 减轻了 CPU 的处理负荷
用 GPU.js 使你的应用程序快 10 倍。作为开发者,我们总是寻找机会来提高应用程序的性能。当涉及到网络应用时,我们主要在代码中进行这些改进。但是,你有没有想过将 GPU 的力量结合到你的网络应用中来提高性能?
Chrome 团队宣布,经过多年的开发,他们终于发布了 WebGPU 实现,目前已在 Chrome 113 Beta 中默认启用。WebGPU 可用于在 Web 上进行高性能 3D 图形和数据并行计算。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!