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

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

我们将解释如何充分利用 WebGPU 性能。这次我们以绘制大量物体为例,根据"使用纹理"中的代码进行一些更改并绘制 900 个立方体。

要均匀分布立方体,可以按如下方式更新 worldMatrix:

javascript 复制代码
    for (let i=0; i<30*30; i++) {
        draw({context, pipeline, verticesBuffer, indicesBuffer, uniformBindGroup, uniformBuffer, depthTexture, i});
    }
javascript 复制代码
  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()]);

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

javascript 复制代码
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 以数组格式定义。

javascript 复制代码
@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从数组中提取的。

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

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

javascript 复制代码
  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

javascript 复制代码
  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。

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

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

javascript 复制代码
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 上的缓冲区中。

相关推荐
西岸行者11 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意11 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码12 天前
嵌入式学习路线
学习
毛小茛12 天前
计算机系统概论——校验码
学习
babe小鑫12 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms12 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下12 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。12 天前
2026.2.25监控学习
学习
im_AMBER12 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J12 天前
从“Hello World“ 开始 C++
c语言·c++·学习