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

2023-09-17 09:54:25

虽然是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 上的缓冲区中。

更多推荐

使用自定义XML配置文件在.NET桌面程序中保存设置

本文将详细介绍如何在.NET桌面程序中使用自定义的XML配置文件来保存和读取设置。除了XML之外,我们还将探讨其他常见的配置文件格式,如JSON、INI和YAML,以及它们的优缺点和相关的NuGet类库。最后,我们将重点介绍我们为何选择XML作为配置文件格式,并展示一个实用的示例。1.背景在.NET桌面程序中,通常使用

Quartz.NET,强大的开源作业调度框架

Quartz.NET是一个强大的开源作业调度框架,专为C#和.NET开发而设计。它允许开发人员在应用程序中调度、执行和管理各种类型的作业,例如定时任务、后台作业、周期性作业等。Quartz.NET具有高度可配置性和灵活性,可以满足各种复杂的调度需求。**作用:**Quartz.NET的作用在于简化作业调度的实现并提供可

高防CDN如何保护电商平台的在线支付系统安全

高防CDN如何保护电商平台的在线支付系统安全?随着移动互联网的快速发展,越来越多的用户选择在手机上进行购物和支付。这种形式的便利性和灵活性推动了电商平台的发展,但同时也给电商平台的安全带来了新的挑战。尤其是在线支付系统,由于其涉及用户的金融信息,成为黑客攻击的重点目标。为了保护电商平台的在线支付系统安全,选择合适的高防

案例实践丨基于SkyWalking全链路监控的微服务系统性能调优实践篇

1背景随着开源社区和云计算的快速推进,云原生微服务作为新型应用系统的核心架构,得到了越来越广泛的应用。根据Gartner对微服务的定义:“微服务是范围狭窄、封装紧密、松散耦合、可独立部署且可独立伸缩的应用程序组件。”微服务之父,马丁.福勒,对微服务概述如下:就目前而言,对于微服务业界并没有一个统一的、标准的定义。但通常

Unity 开发人员转CGE(castle Game engine)城堡游戏引擎指导手册

Unity开发人员的城堡游戏引擎概述一、简介2.Unity相当于什么GameObject?3.如何设计一个由多种资产、生物等组成的关卡?4.在哪里放置特定角色的代码(例如生物、物品)?Unity中“向GameObject添加MonoBehaviour”相当于什么?5.Unity子目录相当于什么Assets?6.支持哪些

git基本使用

一、Git简介Git是一个免费的、开源的分布式版本控制系统,可以快速高效地处理从小型到大型的各种项目。Git易于学习,占地面积小,性能极快。它具有廉价的本地库,方便的暂存区域和多个工作流分支等特性。其性能优于Subversion、CVS、Perforce和ClearCase等版本控制工具。1.版本控制的三个阶段版本控制

深度解析Java虚拟机(JVM)的垃圾回收机制

AI绘画关于SD,MJ,GPT,SDXL百科全书面试题分享点我直达2023Python面试题2023最新面试合集链接2023大厂面试题PDF面试题PDF版本java、python面试题项目实战:AI文本OCR识别最佳实践AIGamma一键生成PPT工具直达链接玩转cloudStudio在线编码神器玩转GPUAI绘画、A

浅谈大数据背景下用户侧用电数据在电力系统的应用与发展分析

安科瑞华楠摘要:随着能源互联网、互联网+、新型传感技术的持续推进,电力用户侧用电数据呈现指数级剧增、异构性增强的情况,逐渐构成了用户侧用电行为大数据。然而目前对电力领域的数据价值挖掘于电网内部和电源端,用户侧庞大且蕴含丰富价值的电力数据而未能得到很好利用。文章主要是在用户侧用电行为大数据的基础上,对用户侧电力数据价值在

机器学习(16)---聚类(KMeans)

聚类一、聚类概述1.1无监督学习与聚类算法1.2sklearn中的聚类算法二、KMeans2.1基本原理2.2簇内误差平方和三、sklearn中的KMeans3.1所用模块3.2聚类算法的模型评估指标3.3轮廓系数3.4CHI(卡林斯基-哈拉巴斯指数)四、KMeans做矢量量化4.1概述4.2案例一、聚类概述1.1无监

负载均衡算法介绍及应用连接池负载不均问题分析

在分布式系统架构下,为了满足高并发和高扩展性的要求,负载均衡设备得以广泛的使用。结合应用连接池的配置,在实际使用过程中可能会出现负载不均的问题。本文简单介绍了负载均衡算法、Druid连接池配置以及连接池负载不均的问题分析及优化方法。1、负载均衡基本概念1.1负载均衡介绍在分布式架构下随着逻辑业务的快速发展,系统架构也随

元宇宙安全与著作权相关市场与技术动态:韩国视角

元宇宙市场动态元宇宙安全与著作权维护技术现状元宇宙有可能为商业创造巨大价值,尤其是在零售和时尚领域。时尚产品的象征性价值不仅在物理空间中得以保持,在虚拟空间中也是如此。通过元宇宙平台,企业可以开发虚拟产品,降低供应链和库存管理的风险。随着虚拟化和触觉技术的发展,消费者可以通过体验产品的物理特性做出更好的购买决策。通过将

热文推荐