介绍

在WebGPU中,GPUBuffer是您将要操作的主要对象之一。它与GPUTextures一同代表了您的应用程序向GPU传递用于渲染的大部分数据。在WebGPU中,缓冲区用于顶点和索引数据、uniforms、计算和片段着色器的通用存储,以及作为纹理数据的临时存储区域。

本文档专注于找到将数据有效地输入这些缓冲区的最佳方法,而不考虑其最终用途。

缓冲区数据流

在深入探讨设置缓冲区数据的机制之前,让我们先谈谈它在底层是什么样子。

总体而言,您可以将WebGPU视为使用两种类型的内存:GPU可访问的内存和CPU可访问且能够高效复制到GPU可访问内存的内存。每当您想要从着色器(顶点、片段或计算)中访问数据时,它必须在GPU可访问内存中;每当您想要从JavaScript中访问数据时,它必须在CPU可访问内存中。缓冲区可以是GPU或CPU可访问的,但不能同时是两者,而纹理始终只能是GPU可访问的。

在某些设备上,比如手机,实际上它们可能是同一内存池。在另一些设备上,比如带有独立显卡的个人电脑,它们可能位于不同的物理板上,并且只能通过PCIe总线或类似方式进行通信。由于我们正在为Web开发,我们希望能够编写一个单一的代码路径,可以在最广泛的设备上运行。因此,WebGPU在处理这些内存配置时不像Vulkan那样区分它们。一切都被视为具有独立的CPU和GPU内存池,而由WebGPU实现负责在可能的情况下进行特定架构的优化。

这意味着进入GPU可访问内存的所有数据将大致采用相同的路径:

  1. 创建一个使用CPU可访问内存的“临时”缓冲区,该缓冲区可用于写入和复制。 (usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC)
  2. 对“临时”缓冲区进行映射以进行写入(通过mapAsync()),这使得其内存可以作为JavaScript ArrayBuffer进行写入。
  3. 将数据放入数组缓冲区。
  4. 解除对“临时”缓冲区的映射。
  5. 使用复制命令(例如copyBufferToBuffer()或copyBufferToTexture())将数据从“临时”缓冲区复制到GPU可访问的目标中。

类似的路径用于从GPU可访问内存中读取数据:

  1. 创建一个使用CPU可访问内存的“临时”缓冲区,该缓冲区可用于复制和读取。 (usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST)
  2. 使用复制命令(例如copyBufferToBuffer()或copyTextureToBuffer())将数据从GPU可访问的目标复制到“临时”缓冲区。
  3. 对“临时”缓冲区进行映射以进行读取(通过mapAsync()),这使得其内存可以作为JavaScript ArrayBuffer进行读取。
  4. 从数组缓冲区中读取数据。
  5. 解除对“临时”缓冲区的映射。

正如您将看到的,下面的一些方法通过使这些步骤成为隐式的方式来隐藏它们,但在大多数情况下,您可以假定这正是发生的事情。

当有疑虑时,使用writeBuffer()!

首先要明确的是,如果您对将数据有效输入特定缓冲区的最佳方法有任何疑问,writeBuffer()方法始终是一个安全的后备选择,几乎没有太多缺点。

writeBuffer()是GPUQueue上的一个便捷方法,它将ArrayBuffer中的值复制到GPUBuffer中,以用户代理认为最佳的方式进行。通常,这将是一条相当高效的路径,在某些情况下甚至可能是最高效的路径!(在大多数情况下,当您调用writeBuffer()时,用户代理将为您管理一个隐式的“临时”缓冲区,但在某些体系结构上,它有可能跳过该步骤。)

具体来说,如果您正在从WASM代码中使用WebGPU,那么writeBuffer()是首选路径。这是因为当您使用映射缓冲区时,WASM应用程序需要执行从WASM堆复制的额外步骤。

总的来说,使用writeBuffer()的优势有:

  1. 对于WASM应用程序来说是首选路径。
  2. 总体代码复杂度最低。
  3. 立即设置缓冲区数据。
  4. 如果数据已经在ArrayBuffer中,避免分配/复制映射ArrayBuffer。
  5. 在返回之前无需将映射缓冲区的数组内容设置为零。
  6. 允许用户代理选择上传数据到GPU的(可能是最佳的)模式。

实际上,并没有明显的不利之处。根据确切的使用模式,您可能能够编写一个更定制的缓冲区管理系统,在某种情况下获得更好的性能,但writeBuffer()是一个非常可靠的通用解决方案,用于设置缓冲区数据。

这里是使用writeBuffer()的一个示例。您可以看到代码非常简洁:

// At some point during the app startup...
const projectionMatrixBuffer = gpuDevice.createBuffer({
size: 16 * Float32Array.BYTES_PER_ELEMENT, // Large enough for a 4x4 matrix
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, // COPY_DST is required
}); // Whenever the projection matrix changes (ie: window is resized)...
function updateProjectionMatrixBuffer(projectionMatrix) {
const projectionMatrixArray = projectionMatrix.getAsFloat32Array();
gpuDevice.queue.writeBuffer(projectionMatrixBuffer, 0, projectionMatrixArray, 0, 16);
}

不会改变的缓冲区

有许多情况下,您将创建一个缓冲区,其内容在创建时需要被设置一次,然后永远不再改变。一个简单的例子是静态网格的顶点和索引缓冲区:缓冲区本身需要在创建后立即填充网格数据,之后在渲染循环中对网格进行任何更改都将使用变换矩阵或可能是在顶点着色器中进行的网格蒙皮。缓冲区内容在初始化设置后唯一更改的时间是在最终销毁时。

在这种情况下,在调用createBuffer()时使用mappedAtCreation标志是设置缓冲区数据的最佳方法之一。这将在映射状态下创建缓冲区,以便在创建后立即调用getMappedRange()。这提供了一个ArrayBuffer用于填充,之后调用unmap()并设置缓冲区数据!实际上,浏览器几乎肯定需要在调用unmap()后在后台对数组缓冲区内容进行一次复制,但通常可以确保以高效的方式完成。 (就像在writeBuffer()情况下一样,大多数情况下,用户代理会为您管理一个隐式的临时缓冲区。)

这种方法的主要优势是,如果您的缓冲区数据是动态生成的,您可以通过直接生成数据到映射的缓冲区中,至少可以节省一个CPU端的复制。

这种方法的优势有:

  1. 立即设置缓冲区数据。
  2. 不需要特定的使用标志。
  3. 数据可以直接写入映射的缓冲区,避免CPU端复制。

缺点有:

  1. 仅适用于新创建的缓冲区。
  2. 用户代理在映射之前必须将缓冲区清零。
  3. 如果数据已经在ArrayBuffer中,则需要进行另一次CPU端的复制。

以下是使用mappedAtCreation设置静态顶点数据的示例:

// Creates a grid of vertices on the X, Y plane
function createXYPlaneVertexBuffer(width, height) {
const vertexSize = 3 * Float32Array.BYTES_PER_ELEMENT; // Each vertex is 3 floats (X,Y,Z position) const vertexBuffer = gpuDevice.createBuffer({
size: width * height * vertexSize, // Allocate enough space for all the vertices
usage: GPUBufferUsage.VERTEX, // COPY_DST is not required!
mappedAtCreation: true,
}); const vertexPositions = new Float32Array(vertexBuffer.getMappedRange()), // Build the vertex grid
for (let y = 0; y < height; ++y) {
for (let x = 0; x < width; ++x) {
const vertexIndex = y * width + x;
const offset = vertexIndex * 3; vertexPositions[offset + 0] = x;
vertexPositions[offset + 1] = y;
vertexPositions[offset + 2] = 0;
}
} // Commit the buffer contents to the GPU
vertexBuffer.unmap(); return vertexBuffer;
}

经常写入的缓冲区

如果您有经常更改的缓冲区(例如每帧一次),那么有效地更新它们略微更加复杂。不过在我们进一步讨论之前,应该注意在许多情况下,从性能的角度来看,使用writeBuffer()将是一条完全可以接受的路径!

然而,希望更明确控制其内存使用的应用程序可以使用所谓的“临时缓冲区环”。这种技术使用一个旋转的临时缓冲区集,不断地向GPU可访问缓冲区“提供”新数据。每次更新数据时,首先检查之前使用的临时缓冲区是否已映射并准备好使用,如果是,则将数据写入其中。如果不是,则创建一个新的临时缓冲区,将mappedAtCreation设置为true,以便立即填充。在数据在GPU端复制后,临时缓冲区立即再次映射,一旦映射完成,它就被放入准备使用的缓冲区队列中。如果缓冲区数据经常更新,这通常会导致一个包含2-3个临时缓冲区的循环列表。

这种方法在缓冲区管理方面是最复杂的,并且在持续内存使用方面比其他方法更多。不过,它对GPU的工作流水线有好处,并为您提供了很多控制的能力,可以针对特定情况进行调整。

优势:

  1. 限制缓冲区创建。
  2. 不等待先前使用的缓冲区映射。
  3. 临时缓冲区重用意味着初始化成本仅在每个设置中支付一次。
  4. 数据可以直接写入映射的缓冲区,避免CPU端复制。

缺点:

  1. 比其他方法更复杂。
  2. 更高的持续内存使用。
  3. 用户代理必须在第一次映射时将临时缓冲区清零。
  4. 如果数据已经在ArrayBuffer中,则需要进行另一次CPU端的复制。

以下是临时缓冲区环如何工作的示例,设置顶点数据:

const waveGridSize = 1024;
const waveGridBufferSize = waveGridSize * waveGridSize * 3 * Float32Array.BYTES_PER_ELEMENT;
const waveGridVertexBuffer = gpuDevice.createBuffer({
size: waveGridBufferSize,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
const waveGridStagingBuffers = []; // Updates a grid of vertices on the X, Y plane with wave-like motion
function updateWaveGrid(time) {
// Get a new or re-used staging buffer that's already mapped.
let stagingBuffer;
if (waveGridStagingBuffers.length) {
stagingBuffer = waveGridStagingBuffers.pop();
} else {
stagingBuffer = gpuDevice.createBuffer({
size: waveGridBufferSize,
usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
mappedAtCreation: true,
});
} // Fill in the vertex grid values.
const vertexPositions = new Float32Array(stagingBuffer.getMappedRange()),
for (let y = 0; y < height; ++y) {
for (let x = 0; x < width; ++x) {
const vertexIndex = y * width + x;
const offset = vertexIndex * 3; vertexPositions[offset + 0] = x;
vertexPositions[offset + 1] = y;
vertexPositions[offset + 2] = Math.sin(time + (x + y) * 0.1);
}
}
stagingBuffer.unmap(); // Copy the staging buffer contents to the vertex buffer.
const commandEncoder = gpuDevice.createCommandEncoder({});
commandEncoder.copyBufferToBuffer(stagingBuffer, 0, waveGridVertexBuffer, 0, waveGridBufferSize);
gpuDevice.queue.submit([commandEncoder.finish()]); // Immediately after copying, re-map the buffer. Push onto the list of staging buffers when the
// mapping completes.
stagingBuffer.mapAsync(GPUMapMode.WRITE).then(() => {
waveGridStagingBuffers.push(stagingBuffer);
});
}

数学无处不在,生成在GPU上的数据!

虽然超出了这份文档的范围,但如果我不提及一种快速将数据放入缓冲区的终极技术,我会感到遗憾:在GPU上生成它!具体而言,WebGPU的计算着色器是高效填充缓冲区的绝佳工具。这样做的巨大优势是不需要任何临时缓冲区,因此避免了复制的需要。当然,GPU端的缓冲区生成只有在您的数据可以完全通过算法计算且不适用于从文件加载的模型等情况下才真正奏效。

现实世界的例子 如果您想在现实世界中看到这些技术(以及其他一些技术)在实际工作中的效果,您应该查看我的WebGPU Metaballs演示。使用“metaballMethod”下拉菜单选择要使用的缓冲区填充方法,尽管不要期望在它们之间看到太大的性能差异(除了计算着色器方法)。您还可以查看每种技术的代码,其中有注释解释每种技术。它还详细说明了这里没有涵盖的另外两种模式,主要是因为它们在它们是最有效的路径的情况下相当罕见。

进一步阅读 如果您想更多地了解缓冲区使用的机制,我建议查阅WebGPU Explainer和WebGPU规范的相关部分。特别是规范并不是我所说的“轻松阅读”,但它详细描述了WebGPU缓冲区的预期行为。

玩得开心,创造出酷炫的东西!WebGPU中用于将数据传递到GPU的各种模式可能会使这一领域感到混乱,可能有点令人生畏,但它不必如此!要记住的第一件事是,这种灵活性存在是为了为高端专业应用程序提供一种紧密控制其性能的方式。对于普通的WebGPU开发人员,您可以并且应该从使用最简单的方法开始:调用writeBuffer()来更新缓冲区,也许对于只需要设置一次的缓冲区使用mappedAtCreation。这些不是“简化的”辅助函数!它们是推荐的,高性能的路径,碰巧也是最简单的路径。只有当您发现向缓冲区写入是应用程序的瓶颈,并且您能够确定适合您的用例的替代技术时,才尝试变得更炫酷。

祝您在即将进行的任何项目中好运,我迫不及待想看到Web社区构建的令人瞩目的创意!

WebGPU缓冲区更新最佳实践的更多相关文章

  1. atitit.hbnt orm db 新新增更新最佳实践o99

    atitit.hbnt orm db 新新增更新最佳实践o99 1. merge跟个save了. 1 2. POJO对象处于游离态.持久态.托管态.使用merge()的情况. 1 3. @Dynami ...

  2. atitit.hbnt orm db 新新增更新最佳实践o7

    atitit.hbnt orm db 新新增更新最佳实践o7 1. merge跟个save了. 1 2. POJO对象处于游离态.持久态.托管态.使用merge()的情况. 1 3. @Dynamic ...

  3. 基于ABP落地领域驱动设计-05.实体创建和更新最佳实践

    目录 系列文章 数据传输对象 输入DTO最佳实践 不要在输入DTO中定义不使用的属性 不要重用输入DTO 输入DTO中验证逻辑 输出DTO最佳实践 对象映射 学习帮助 系列文章 基于ABP落地领域驱动 ...

  4. 基于ABP落地领域驱动设计-04.领域服务和应用服务的最佳实践和原则

    目录 系列文章 领域服务 应用服务 学习帮助 系列文章 基于ABP落地领域驱动设计-00.目录和前言 基于ABP落地领域驱动设计-01.全景图 基于ABP落地领域驱动设计-02.聚合和聚合根的最佳实践 ...

  5. Laravel 代码开发最佳实践(持续更新)

    我们这里要讨论的并不是 Laravel 版的 SOLID 原则(想要了解更多 SOLID 原则细节查看这篇文章)亦或是设计模式,而是 Laravel 实际开发中容易被忽略的最佳实践. 内容概览 单一职 ...

  6. 基于AWS的云服务架构最佳实践

    ZZ from: http://blog.csdn.net/wireless_com/article/details/43305701 近年来,对于打造高度可扩展的应用程序,软件架构师们挖掘了若干相关 ...

  7. Web前端优化最佳实践及工具集锦

    Web前端优化最佳实践及工具集锦 发表于2013-09-23 19:47| 21315次阅读| 来源Googe & Yahoo| 118 条评论| 作者王果 编译 Web优化Google雅虎P ...

  8. 面试题_76_to_81_Java 最佳实践的面试问题

    包含 Java 中各个部分的最佳实践,如集合,字符串,IO,多线程,错误和异常处理,设计模式等等. 76)Java 中,编写多线程程序的时候你会遵循哪些最佳实践?(答案)这是我在写Java 并发程序的 ...

  9. Apache Hadoop最佳实践和反模式

    摘要:本文介绍了在Apache Hadoop上运行应用程序的最佳实践,实际上,我们引入了网格模式(Grid Pattern)的概念,它和设计模式类似,它代表运行在网格(Grid)上的应用程序的可复用解 ...

  10. JSP 最佳实践: 用 jsp:include 控制动态内容

    在新的 JSP 最佳实践系列的前一篇文章中,您了解了如何使用 JSP include 伪指令将诸如页眉.页脚和导航组件之类的静态内容包含到 Web 页面中.和服务器端包含一样,JSP include  ...

随机推荐

  1. Stable Diffusion生成图片的参数查看与抹除方法

    前几天分享了几张Stable Diffusion生成的艺术二维码,有同学反映不知道怎么查看图片的参数信息,还有的同学问怎么保护自己的图片生成参数不会泄露,这篇文章就来专门分享如何查看和抹除图片的参数. ...

  2. Cilium系列-1-Cilium特色 功能及适用场景

    系列文章 Cilium 系列文章 Cilium 简介 Cilium 是一个开源的云原生解决方案,用于提供.保护(安全功能)和观察(监控功能)工作负载之间的网络连接,由革命性的内核技术 eBPF 提供动 ...

  3. 小白也能看懂的 ROC 曲线详解

    作者:PrimiHub-Kevin ROC 曲线是一种坐标图式的分析工具,是由二战中的电子和雷达工程师发明的,发明之初是用来侦测敌军飞机.船舰,后来被应用于医学.生物学.犯罪心理学. 如今,ROC 曲 ...

  4. mysql中使用sql语句统计日志计算每天的访问量

    日志建表语句: CREATE TABLE `syslog` ( `syslogid` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) ...

  5. SpringBoot里的Servlet和实现

    Servlet 接口,一个规范, SpringBoot Spring Boot 是 Spring 的子项目,正如其名字,提供 Spring 的引导( Boot )的功能. 通过 Spring Boot ...

  6. p2:认识requests库的常用方法与实战

    二.全面认识requests库的常用方法 requests是python第三方库安装命令方法如下: pip install requests python的默认的测试用来规则 1.模块名必须以test ...

  7. PoW是什么?

    PoW是什么? 工作量证明(proof of work,PoW)是一种用于确认和验证区块链交易和新区块有效性的共识算法.区块链中常见的工作量证明算法包括比特币的SHA-256.以太坊的Ethash.莱 ...

  8. CentOS7升级python3到最新版

    前言 最近在学习sanic,需要python3.7以上的版本,而centos7默认的python版本是3.6.8,所以升级了一下版本,在此笔录. 步骤 首先,从python官网下载最新版的python ...

  9. debian11编译安装freeswitch

    前言 环境: 系统版本:debian 11 x86_64 FreeSWITCH版本:1.10.6 安装步骤 安装依赖(安装之前最好换apt软件源为国内的) apt install -y gnupg2 ...

  10. servlet系列:简介和基本使用以及工作流程

    目录 一.简介 二.Servlet实现 三.基本使用 1.引入pom依赖 2.实现Servlet规范,重写service方法 3.配置web.xml 4.配置Tomcat 6.运行 四.Servlet ...