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


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

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

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

        for (let i=0; i<30*30; i++) {
            draw({context, pipeline, verticesBuffer, indicesBuffer, uniformBindGroup, uniformBuffer, depthTexture, i});
        }
    
    • 1
    • 2
    • 3
      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
      );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

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

    缓慢的原因

    无法重用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;
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

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

    这次,我们为多维数据集的数量创建一个新的存储缓冲区“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, // <--- 追加
            }
          },
        ],
      });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    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
        );
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

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

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

    另外,绘制时,在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}));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

    其他调整

    重用RenderPipeline和BindGroup

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

    使用RenderBundle

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

    总结

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

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

  • 相关阅读:
    P2002 消息扩散
    QGraphicsView、QGraphicsScene、QGraphicsItem的应用
    leetcode-二叉树的最近公共祖先-递归
    DSP Trick:向量长度估算
    【Kubernetes】基于K8S & SpringCloud OpenFeign的一种微服务构建模式
    【NR 定位】3GPP NR Positioning 5G定位标准解读(十)-增强的小区ID定位
    第3章业务功能开发(线索关联市场活动,动态搜索)
    python socket 传输opencv读取的图像
    看完抱你学会Exchanger
    Ubuntu系统升级
  • 原文地址:https://blog.csdn.net/u013929284/article/details/132939378