WebGPU学习(10)---如何利用 WebGPU 实现高性能

虽然是WebGPU,但是速度很慢!?

我们将解释如何充分利用 WebGPU 性能。这次我们以绘制大量物体为例,根据“使用纹理”中的代码进行一些更改并绘制 900 个立方体。
WebGPU学习(10)---如何利用 WebGPU 实现高性能_第1张图片
要均匀分布立方体,可以按如下方式更新 worldMatrix:

    for (let i=0; i<30*30; i++) {
        draw({context, pipeline, verticesBuffer, indicesBuffer, uniformBindGroup, uniformBuffer, depthTexture, i});
    }
  const worldMatrix = glMatrix.mat4.create();
	const now = Date.now() / 1000;
  glMatrix.mat4.translate(
    worldMatrix,
    worldMatrix,
    glMatrix.vec3.fromValues((i % 30) * 5 - 100, Math.floor(i / 30) * 5 + -50, 0)
  );
  glMatrix.mat4.rotate(
    worldMatrix,
    worldMatrix,
    1,
    glMatrix.vec3.fromValues(Math.sin(now), Math.cos(now), 0)
  );

	g_device.queue.writeBuffer(
    uniformBuffer,
    4 * 16 * 2,
    worldMatrix.buffer,
    worldMatrix.byteOffset,
    worldMatrix.byteLength
  );

可以看一个不考虑性能调整的多路立方体绘制示例。我们发现绘制非常断断续续且缓慢。

缓慢的原因

无法重用CommandEncoder

基本上,g_device.queue.submit([commandEncoder.finish()])速度非常慢。在此代码中,它被调用了 900 次。但是理想情况下,最好只在绘制结束时执行一次。

无法重用RenderPassEncoder

在当前代码中,RenderPassEncoder也无法重用。我们要尽可能的去重复使用RenderPassEncoder,可以从 CommandEncoder 多次生成 RenderPassEncoder。

其他问题

下面的示例将 passEncoder.end();g_device.queue.submit([commandEncoder.finish()]); 放在draw函数之外,以便仅在绘图帧的开头生成 commandEncoder 和 renderPassEncoder。这是示例。
WebGPU学习(10)---如何利用 WebGPU 实现高性能_第2张图片
但是我们发现,除了一个立方体之外,所有立方体都消失了。这是因为 GPU 仅在执行 g_device.queue.submit([commandEncoder.finish()]); 时执行绘图命令。

即使每次在绘制函数中更新WorldMatrix,Uniform区域也只是针对一个立方体。当draw函数处理完成并且Uniform区域中的WorldMatrix更新为最后的位置信息后,在绘制帧结束时,所有的立方体最终都通过g_device.queue.submit([commandEncoder.finish()]);来绘制。因此,所有立方体都引用表示最后位置的WorldMatrix,并且所有立方体都绘制在最后位置。

因此,为了将所有立方体绘制在正确的位置,我们需要重写Uniform区域缓冲区,然后每次执行g_device.queue.submit([commandEncoder.finish()]);。然而,这并不能加快 WebGPU 处理速度。

这就是WebGPU编程的难点。我们应该怎么办?

解决方法

一种解决方案是将所有立方体的所有 WorldMatrix 解压到缓冲区中。 然后,仅在绘图帧结束时执行一次 g_device.queue.submit([commandEncoder.finish()]);

这是一个改进版本的示例代码。

const cubeNumber = 30*30;
const vertWGSL = `
struct Uniforms {
  projectionMatrix : mat4x4<f32>,
  viewMatrix : mat4x4<f32>,
}
@binding(0) @group(0) var<uniform> uniforms : Uniforms;

struct WorldStorage {
  worldMatrices : array<mat4x4<f32>>,
}
@binding(3) @group(0) var<storage> worldStorage : WorldStorage;
...

worldMatrix 定义已移至单独的新Storage Buffer。在处理大量数据时,Storage Buffer比Uniform Buffer更好。

900 个 WorldMatrix 以数组格式定义。

@vertex
fn main(
  @builtin(instance_index) instance_index: u32,
  @location(0) position: vec4<f32>,
  @location(1) color: vec4<f32>,
  @location(2) uv: vec2<f32>  
) -> VertexOutput {

	var output : VertexOutput;
	output.Position = uniforms.projectionMatrix * uniforms.viewMatrix * worldUniforms.worldMatrices[instance_index] * position;
  output.fragUV = uv;
  
  return output;
}

WorldMatrix是使用内置变量实例号instance_index从数组中提取的。

  const storageBufferSize = 4 * 16 * cubeNumber; // 4x4 matrix * 3
  const storageBufferCubes = g_device.createBuffer({
    size: storageBufferSize,
    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
  });

这次,我们为多维数据集的数量创建一个新的存储缓冲区“storageBufferCubes”。

  const uniformBindGroup = g_device.createBindGroup({
    layout: pipeline.getBindGroupLayout(0),
    entries: [
      {
        binding: 0,
        resource: {
          buffer: uniformBuffer,
        },
      },
      {
        binding: 1,
        resource: texture.createView(),
      },
      {
        binding: 2,
        resource: sampler,
      },
      {
        binding: 3,
        resource: {
        	buffer: storageBufferCubes, // <--- 追加
        }
      },
    ],
  });

BindGroup 还将 storageBufferCubes 指定为binding:3

  for (let i=0; i<cubeNumber; i++) {
  	const worldMatrix = glMatrix.mat4.create();
    const now = Date.now() / 1000;
    glMatrix.mat4.translate(
      worldMatrix,
      worldMatrix,
      glMatrix.vec3.fromValues((i % 30) * 5 - 100, Math.floor(i / 30) * 5 + -50, 0)
    );
    glMatrix.mat4.rotate(
      worldMatrix,
      worldMatrix,
      1,
      glMatrix.vec3.fromValues(Math.sin(now), Math.cos(now), 0)
    );

    g_device.queue.writeBuffer(
      storageBufferCubes,
      4 * 16 * i,
      worldMatrix.buffer,
      worldMatrix.byteOffset,
      worldMatrix.byteLength
    );
  }

在getTransformationMatrix中,900个WorldMatrix被写入storageBufferCubes。

  passEncoder.setPipeline(pipeline);
  passEncoder.setBindGroup(0, uniformBindGroup);
  passEncoder.setVertexBuffer(0, verticesBuffer);
  passEncoder.draw(cubeVertexCount, cubeNumber); // <---绘制900个实例

另外,绘制时,在draw函数的第二个参数中指定要绘制实例的立方体数量。 现在,将一次绘制900个立方体,着色器将根据每个实例编号引用WorldMatrix并在适当的位置绘制。

function frame(
{context, pipeline, verticesBuffer, indicesBuffer, uniformBindGroup, uniformBuffer, depthTexture}:
{context: GPUCanvasContext, pipeline: GPURenderPipeline, verticesBuffer: GPUBuffer, uniformBindGroup: GPUBindGroup, uniformBuffer: GPUBuffer, depthTexture: GPUTexture, texture: GPUTexture}
): void {
  for (let i=0; i<30*30; i++) {
    draw({context, pipeline, verticesBuffer, indicesBuffer, uniformBindGroup, uniformBuffer, depthTexture, i});
  }
  
  passEncoder.end();
  passEncoder = undefined;
  g_device.queue.submit([commandEncoder.finish()]);
  commandEncoder = undefined;
  
  requestAnimationFrame(frame.bind(frame, {context, pipeline, verticesBuffer, uniformBindGroup, uniformBuffer, depthTexture, texture}));
}

请注意, g_device.queue.submit([commandEncoder.finish()]); 仅在绘制帧结束时执行一次。

其他调整

重用RenderPipeline和BindGroup

在这种情况下,我们只需要一个RenderPipeline,但在复杂的场景中,根据对象的不同,使用的着色器和顶点信息会有所不同,因此我们需要相应地使用多个RenderPipeline。 为了加快速度,不要在每次执行绘制过程时生成 RenderPipeline,而是多次重复使用创建的 RenderPipeline。 BindGroup 也是如此。

使用RenderBundle

如果我们是多次绘制常规内容,请考虑将它们转换为 RenderBundle 以重用绘图。 RenderBundle 在“使用 RenderBundle”部分中进行了解释。

总结

使用WebGPU,我们需要自己优化绘图命令,类似于驱动层对WebGL所做的事情。因此,如果编码没有适当优化,结果可能会比WebGL慢。

确定每个 WebGPU 函数调用的性能特征并优化渲染代码非常重要。为了做到这一点,在某些情况下可能需要检查我们正在创建的库或应用程序的设计。在许多情况下,需要将着色器可以访问的大部分数据预先部署到 GPU 上的缓冲区中。

你可能感兴趣的:(webgpu,webgpu,图形渲染,学习)