WebGPU 基础知识-运行计算
WebGPU是WebGPU标准的一部分,由Khronos Group领导的WebGPU工作组制定,是一种为Web公开GPU硬件功能的API。它提供对网络上硬件的图形和计算能力的访问,旨在提供基于现代GPU的高性能、低延迟的图形渲染服务。
推荐使用NSDT 3DConvert进行3D模型格式转换,支持glb、obj、stp、fbx、ifc等多种3D模型格式之间进行互相转换,在转换过程中,能够很好的保留模型原有的颜色、材质等信息。
在 GPU 上运行计算 Run computations on the GPU
让我们编写一个在 GPU 上进行一些计算的基本示例。
我们从相同的代码开始获取 WebGPU 设备。
async function main() {
const adapter = await gpu?.requestAdapter();
const device = await adapter?.requestDevice();
if (!device) {
fail('need a browser that supports WebGPU');
return;
}
当我们创建着色器模块时:
const module = device.createShaderModule({
label: 'doubling compute module',
code: `
@group(0) @binding(0) var<storage, read_write> data: array<f32>;
@compute @workgroup_size(1) fn computeSomething(
@builtin(global_invocation_id) id: vec3<u32>
) {
let i = id.x;
data[i] = data[i] * 2.0;
}
`,
});
首先,我们声明一个名为 data 的变量,类型为 storage ,我们希望它能够读取和写入。
@group(0) @binding(0) var<storage, read_write> data: array<f32>;
我们将其类型声明为 array ,这意味着一个 32 位浮点值数组。我们告诉它我们将在绑定组 0( @group(0) )中的绑定位置 0( binding(0) )上指定这个数组。
然后我们用 @compute 属性声明一个名为 computeSomething 的函数,使其成为计算着色器。
@compute @workgroup_size(1) fn computeSomething(
@builtin(global_invocation_id) id: vec3u
) {
...
计算着色器(Compute shaders)需要声明我们稍后将介绍的工作组大小。现在我们只是将用 属性 @workgroup_size(1)把它设置为 1 。我们声明它有一个使用 vec3u 的参数 id 。 vec3u 是三个无符号 32 位整数值。就像我们上面的顶点着色器一样,这是迭代次数。不同之处在于计算着色器迭代次数是 3 维的(有 3 个值)。我们声明 id 以从内置的 global_invocation_id 中获取它的值。
您可以认为计算着色器是这样运行的。这是一个简化说明,但现在就可以了。
// pseudo code
for (z = 0; z < depth; ++z) {
for (y = 0; y < height; ++y) {
for (x = 0; x < width; ++x) {
const global_invocation_id = {x, y, z};
computeShaderFn(global_invocation_id);
}
}
}
最后我们使用 id 的 x 属性对 data 进行索引,并将每个值乘以2。
let i = id.x;
data[i] = data[i] * 2.0;
上面, i 只是 3 个迭代数字中的第一个。
现在我们已经创建了着色器,我们需要创建一个管道。
const pipeline = device.createComputePipeline({
label: 'doubling compute pipeline',
layout: 'auto',
compute: {
module,
entryPoint: 'computeSomething',
},
});
接下来我们需要一些数据。
const input = new Float32Array([1, 3, 5]);
该数据仅存在于 JavaScript 中。为了让 WebGPU 使用它,需要创建一个存在于 GPU 上的缓冲区并将数据复制到缓冲区中。
// create a buffer on the GPU to hold our computation
// input and output
const workBuffer = device.createBuffer({
label: 'work buffer',
size: input.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
// Copy our input data to that buffer
device.queue.writeBuffer(workBuffer, 0, input);
上面我们调用 device.createBuffer 来创建缓冲区。 size 是以字节为单位的大小,在这种情况下它将是 12,因为 Float32Array 的 3 个值的字节大小是 12。
我们创建的每个 WebGPU 缓冲区都必须指定一个 usage 。我们可以传递一堆标志以供使用,但并非所有标志都可以一起使用。这里我们说我们希望通过传递 GPUBufferUsage.STORAGE 将此缓冲区用作 storage 。这使得它与着色器中的 var<storage,...> 兼容。此外,我们希望能够将数据复制到此缓冲区,因此我们包含 GPUBufferUsage.COPY_DST 标志。最后,我们希望能够从缓冲区复制数据,因此我们包含了 GPUBufferUsage.COPY_SRC 。
请注意,不能直接从 JavaScript 读取 WebGPU 缓冲区的内容。相反,您必须“映射”它,这是从 WebGPU 请求访问缓冲区的另一种方式,因为缓冲区可能正在使用并且因为它可能只存在于 GPU 上。
可以在 JavaScript 中映射的 WebGPU 缓冲区不能用于其他用途。换句话说,我们无法映射我们刚刚在上面创建的缓冲区,如果我们尝试添加标志以使其可映射,我们将收到一个与 STORAGE 用法不兼容的错误。
因此,为了查看我们的计算结果,我们需要另一个缓冲区。运行计算后,我们将把上面的缓冲区复制到这个结果缓冲区并设置它的标志以便我们可以映射它。
// create a buffer on the GPU to get a copy of the results
const resultBuffer = device.createBuffer({
label: 'result buffer',
size: input.byteLength,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
});
MAP_READ 表示我们希望能够映射此缓冲区以读取数据。
为了告诉我们的着色器我们希望它工作的缓冲区,我们需要创建一个 bindGroup。
// Setup a bindGroup to tell the shader which
// buffer to use for the computation
const bindGroup = device.createBindGroup({
label: 'bindGroup for work buffer',
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: workBuffer } },
],
});
我们从管道中获取 bindGroup 的布局。然后我们设置 bindGroup 条目。
现在我们可以开始编码命令了。
// Encode commands to do the computation
const encoder = device.createCommandEncoder({
label: 'doubling encoder',
});
const pass = encoder.beginComputePass({
label: 'doubling compute pass',
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(input.length);
pass.end();
我们创建一个命令编码器。并开始一个计算过程。先设置管道,然后设置 bindGroup。这里, pass.setBindGroup(0, bindGroup) 中的 0 对应shader中的 @group(0) 。然后我们调用 dispatchWorkgroups ,在这种情况下,我们将它传递给 input.length ,即 3 告诉 WebGPU 运行计算着色器 3 次。然后我们结束pass。
下面是执行 dispatchWorkgroups 时的情况。
计算完成后,我们要求 WebGPU 从 buffer 复制到 resultBuffer。
// Encode a command to copy the results to a mappable buffer.
encoder.copyBufferToBuffer(workBuffer, 0, resultBuffer, 0, resultBuffer.size);
现在我们可以 finish 编码器获取命令缓冲区,然后提交该命令缓冲区。
// Finish encoding and submit the commands
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
然后我们映射结果缓冲区并获得数据的副本。
// Read the results
await resultBuffer.mapAsync(GPUMapMode.READ);
const result = new Float32Array(resultBuffer.getMappedRange());
console.log('input', input);
console.log('result', result);
resultBuffer.unmap();
要映射结果缓冲区,我们调用 mapAsync 并且必须调用 await 才能完成。映射后,我们可以调用不带参数的 resultBuffer.getMappedRange() ,它将返回整个缓冲区的 ArrayBuffer 。我们把它放在一个 Float32Array 类型的数组视图中,然后我们可以查看值。一个重要的细节是, getMappedRange 返回的 ArrayBuffer 仅在我们调用 unmap 之前有效。在 unmap 之后,它的长度被设置为 0,并且它的数据不再可访问。
运行我们可以看到我们得到了结果,所有的数字都翻了一番。
我们将在其他文章中介绍如何真正使用计算着色器。现在,您希望已经对 WebGPU 的功能有了一些了解。其他一切由您决定!将 WebGPU 视为类似于其他编程语言。它提供了一些基本功能,剩下的就留给您发挥创意了。
让 WebGPU 编程与众不同的是这些函数、顶点着色器、片段着色器和计算着色器,它们在您的 GPU 上运行。一个 GPU 可以有超过 10000 个处理器,这意味着它们可以并行执行超过 10000 个计算,这可能比你的 CPU 可以并行执行的计算高出 3 个或更多数量级。