介绍

在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. yolov5实战之模型剪枝

    续yolov5实战之二维码检测 目录 前沿 为什么要做轻量化 什么是剪枝 稀疏化训练 剪枝 微调 结语 模型下载 前沿   在上一篇yolov5的博客中,我们用yolov5训练了一个二维码检测器,可以 ...

  2. Solon 也是 SSE(Server Send Events)后端开发的优选

    Solon 2.3.6 在开发异步接口时,顺带也为 Solon Web 提供了 SSE (Server-Sent Events) 协议的支持插件: <dependency> <gro ...

  3. XTTS系列之三:中转空间的选择和优化

    通常选择XTTS做迁移的数据库都不会太小的,至少都是几T.几十T这样的规模,这种级别的数据量原有空间不够用,所以在迁移过程临时用作存放迁移数据库备份文件的空间也是需要提前考虑规划的问题. 最近就有客户 ...

  4. Java杂记————object.getClass()和object.class以及Java中的toString()方法的的区别

    不说废话,直接上干货: (注意大小写:object为对象,Object为类) 1,object.getClass()它是Object类的实例方法,返回一个对象运行时的类的Class对象,换句话说,它返 ...

  5. java后台导出表格文件

    Java类所需jar包 import java.io.File; import java.io.IOException; import java.io.InputStream; import java ...

  6. 【WebSocket】多节点下WebSocket消息收发解决案例

    单体Webscoket springboot版本: 2.1.1.RELEASE jdk: 1.8 示例代码 WebsocketServer @ServerEndpoint("/client/ ...

  7. grafana 容器无法启动,打印权限问题

    报错日志 open /var/lib/grafana/alerting/1/notifications: permission denied 问题原因 sudo chown -R docker: /v ...

  8. openpyxl 设置某列单元格样式

    1 # 边框线 2 border_set = Border(left=Side(border_style='thin', color='000000'), 3 right=Side(border_st ...

  9. VBA与VB的区别

    从语言结构上讲,VBA是VB的一个子集,它们的语法结构是一样的.两者的开发环境也几乎相同.但是,VB是独立的开发工具,它不需要依附于任何其他应用程序,它有自己完全独立的工作环境和编译.链接系统.VBA ...

  10. Mysql高级5-SQL优化

    一.插入数据优化 1.1 批量插入 如果有多条数据需要同时插入,不要每次插入一条,然后分多次插入,因为每执行一次插入的操作,都要进行数据库的连接,多个操作就会连接多次,而一次批量操作只需要连接1次 1 ...