虽然是WebGPU,但是速度很慢!?
我们将解释如何充分利用 WebGPU 性能。这次我们以绘制大量物体为例,根据“使用纹理”中的代码进行一些更改并绘制 900 个立方体。
 
 要均匀分布立方体,可以按如下方式更新 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。这是示例。
 
 但是我们发现,除了一个立方体之外,所有立方体都消失了。这是因为 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 上的缓冲区中。



















