关于gpu:WebGPU-规范篇-03-缓存对象

40次阅读

共计 6424 个字符,预计需要花费 17 分钟才能阅读完成。

系列博客总目录:https://segmentfault.com/a/1190000040716735


WebGPU 中的缓存对象:GPUBuffer

GPUBuffer 示意一块显存。显存中的数据是线性排列的,也就是能够通过偏移量来寻找显存中的数据。有些 GPUBuffer 能够被映射,被映射后的 GPUBuffer 能够通过 JavaScript 中的 ArrayBuffer 拜访。

GPUBuffer 能够用 GPUDevice.createBuffer(descriptor) 来创立。

[Exposed=(Window, DedicatedWorker), SecureContext]
interface GPUBuffer {Promise<undefined> mapAsync(GPUMapModeFlags mode, optional GPUSize64 offset = 0, optional GPUSize64 size);
  ArrayBuffer getMappedRange(optional GPUSize64 offset = 0, optional GPUSize64 size);
  undefined unmap();

  undefined destroy();};
GPUBuffer includes GPUObjectBase;

GPUBuffer 对象有如下几个办法:

  • mapAsync,即异步映射办法;
  • getMappedRange,获取映射后的范畴,以 ArrayBuffer 示意;
  • unmap,勾销映射;
  • destroy,销毁并回收此 GPUBuffer 指向的显存

GPUBuffer 对象是能够被序列化的。

1 创立

创立一个 GPUBuffer 须要用到 GPUBufferDescriptor 类型的对象。

dictionary GPUBufferDescriptor : GPUObjectDescriptorBase {
  required GPUSize64 size;
  required GPUBufferUsageFlags usage;
  boolean mappedAtCreation = false;
};

对于设施对象和 descriptor 对象,是有要求的:

  • 如果设施对象失落,那就有效;
  • 如果 descriptor.usage 不在设施对象容许的用处之内,也有效;
  • 如果 descriptor.usage 被同时设为 MAP_READMAP_WRITE,有效(即一个 GPUBuffer 不能同时用于映射独写);
  • 如果 descriptor.usage 被设为 MAP_READ,那么联结的用法只能是 COPY_DST
  • 如果 descriptor.usage 被设为 MAP_WRITE,那么联结的用法只能是 COPY_SRC
  • 如果 descriptor.mappedAtCreation 被设为 true,那么 descriptor.size 必须是 4 的倍数。

留神,descriptor.usage 既不是 MAP_READ 也不是 MAP_WRITE 时,将 descriptor.mappedAtCreation 设为 true 是能够的。

descriptor 的 size 属性指定了须要申请多大的显存,单位是 byte。

GPUBuffer 的用处

应用 GPUBufferUsage 来标识 GPUBuffer 对象的用处。

typedef [EnforceRange] unsigned long GPUBufferUsageFlags;
[Exposed=(Window, DedicatedWorker)]
namespace GPUBufferUsage {
  const GPUFlagsConstant MAP_READ      = 0x0001; // 映射并用来独取
  const GPUFlagsConstant MAP_WRITE     = 0x0002; // 映射并用来写入
  const GPUFlagsConstant COPY_SRC      = 0x0004; // 能够作为拷贝源
  const GPUFlagsConstant COPY_DST      = 0x0008; // 能够作为拷贝指标
  const GPUFlagsConstant INDEX         = 0x0010; // 索引缓存
  const GPUFlagsConstant VERTEX        = 0x0020; // 顶点缓存
  const GPUFlagsConstant UNIFORM       = 0x0040; // Uniform 缓存
  const GPUFlagsConstant STORAGE       = 0x0080; // 仅存储型缓存
  const GPUFlagsConstant INDIRECT      = 0x0100; // 间接应用
  const GPUFlagsConstant QUERY_RESOLVE = 0x0200; // 用于查问
};

用得比拟多的是前 8 种。

2 缓存映射

JavaScript 能够对 GPUBuffer 对象进行映射操作,而后能力拜访对应的显存。一旦 GPUBuffer 对象被映射,就能够调用其 getMappedRange 办法获取可被操作的显存,以 ArrayBuffer 示意。

进入映射状态的 GPUBuffer 对象所申请的那块显存不能被 GPU 应用,必须在提交给队列之前调用其 unmap 办法解除映射。

typedef [EnforceRange] unsigned long GPUMapModeFlags;
[Exposed=(Window, DedicatedWorker)]
namespace GPUMapMode {
  const GPUFlagsConstant READ  = 0x0001;
  const GPUFlagsConstant WRITE = 0x0002;
};

异步映射:mapAsync 办法

异步地映射 GPUBuffer 申请那块显存,它的返回的无定义的 Promise,或者你能够间接应用 await 语法期待其映射实现。

它有三个参数:

  • modeGPUMapMode 类型,示意映射之后用来干什么,这个是必选参数
  • offset,unsigned longlong 类型,示意从哪里开始映射,字节数量,默认是 0,必须是 8 的倍数
  • size,unsigned longlong 类型,示意映射多少大小,字节数量,默认是 GPUBuffer 申请显存的大小减去 offset 的差,必须是 4 的倍数

留神,mode 要与 GPUBuffer 的 usage 统一且只能二选一,且调用此办法之前 GPUBuffer 要未映射。

获取映射的缓存:getMappedRange 办法

它能返回一个 ArrayBuffer,即 GPUBuffer 申请那块显存的 CPU 端表白。

它有两个参数:

  • offset,可选参数,示意从申请的显存的哪个字节开始获取,如果不给就是 0;必须是 8 的倍数且不超申请的大小;
  • size,可选参数,示意要多长,如果不给就是申请的显存的最大值减去 offset 的差;是 4 的倍数。

留神,这个办法要在 GPUBuffer 映射的时候能力用。

解除映射:unmap 办法

调用后 GPUBuffer 对象所申请的显存就能够再被 GPU 应用了。

留神,当一个 MAP_READ 类型的且在创立时没设为映射的 GPUBuffer 被勾销映射时,它所对应的 ArrayBuffer 上的所有批改都会有效。

对于为什么要显存的映射 / 勾销映射,当前会出专门的一篇文章介绍。

常见用例

① 创立 UBO

const uniformBuffer = device.createBuffer({
  size: 16,
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
})

COPY_DST 通常就意味着有数据会复制到此 GPUBuffer 上,这种 GPUBuffer 能够通过 queue.writeBuffer 办法写入数据:

const color = new Float32Array([0, .5, 0, 1])
device.queue.writeBuffer(
  uniformBuffer, // 传给谁
  0, 
  color.buffer, // 传递 ArrayBuffer
  color.byteOffset, // 从哪里开始
  color.byteLength // 取多长
)

② 创立映射读 / 映射写型的 GPUBuffer

const myMapReadBuffer = device.createBuffer({
  usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
  size: 1000,
});
const myMapWriteBuffer = device.createBuffer({
  usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
  size: 1000,
});

MAP_WRITE 类型(映射写)的 GPUBuffer,它能够间接对其 ArrayBuffer 进行写入类型数组实现数据传递:

const gpuBuffer = device.createBuffer({
  mappedAtCreation: true,
  size: 4,
  usage: GPUBufferUsage.MAP_WRITE
});
const arrayBuffer = gpuBuffer.getMappedRange();

// 通过 TypedArray 向 ArrayBuffer 写数据
new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

同理,映射读 MAP_READ 类型的 GPUBuffer,天然也能够用 ArrayBuffer 读取数据。

③ 复制指标 / 复制源类型的 GPUBuffer

COPY_DSTCOPY_SRC 类型,这俩和映射读 / 映射写有什么区别呢?

映射读 / 映射写 即 MAP_READ / MAP_WRITE 类型的意味着在拼装阶段能够通过 JavaScript 来写入和独取映射后的 GPUBuffer。

复制指标和复制源指的是一个 Pipeline(后续会讲到)中,这块 GPUBuffer 所申请的显存的用处,被标记为 COPY_DST 的,能够被指令编码器通过 copyBufferToBuffer 办法进行复制操作:

// 获取一块状态为映射了的显存,以及一个对应的 arrayBuffer 对象来写数据
const gpuWriteBuffer = device.createBuffer({
  mappedAtCreation: true,
  size: 4,
  usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC // 👈 留神这里
});
const arrayBuffer = gpuWriteBuffer.getMappedRange();

// 通过 TypedArray 向 ArrayBuffer 写数据
new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

// 解除显存对象的映射,稍后它就能在 GPU 中进行复制操作
gpuWriteBuffer.unmap();

// 创立一个新的状态为未映射的 GPUBuffer,它之后要拿来读取数据
const gpuReadBuffer = device.createBuffer({
  size: 4,
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ // 👈 留神这里
});

// 创立一个名为 copyEncoder 的指令编码器,用来复制显存
const copyEncoder = device.createCommandEncoder();
copyEncoder.copyBufferToBuffer(
  gpuWriteBuffer /* 源显存(对象)*/,
  0 /* 起始字节(从哪开始读)*/,
  gpuReadBuffer /* 指标显存(对象)*/,
  0 /* 起始字节(从哪开始写)*/,
  4 /* 大小 */
);

// 提交咱们写好的复制性能的指令
const copyCommands = copyEncoder.finish();
device.queue.submit([copyCommands]);

除了 指令编码器的 copyBufferToBuffer 办法外,① 中提到的队列对象的 writeBuffer 也能够写入数据。

注:映射读 / 映射写 / 复制指标 / 复制源 通常会搭配应用,在留神他们的性能区别和作用办法即可。

④ 创立 VBO

const vbo = device.createBuffer({
  size: vbodata.byteLength,
  usage: GPUBufferUsage.VERTEX,
  mappedAtCreation: true // 创立时立即映射,让 CPU 端能读写数据
})

VBO 要通过 passEncoder 的 setVertexBuffer 办法写入数据。

有时候还要配合 GPUBufferUsage.INDEX 即索引缓存来应用。

⑤ 创立存储型缓存对象

个别应用 STORAGE 用处的 GPUBuffer,是在计算管线中。

// 第一个矩阵
const firstMatrix = new Float32Array([
  2 /* 2 行 */, 4 /* 4 列 */,
  1, 2, 3, 4,
  5, 6, 7, 8
]);
const gpuBufferFirstMatrix = device.createBuffer({
  mappedAtCreation: true,
  size: firstMatrix.byteLength,
  usage: GPUBufferUsage.STORAGE,
});
const arrayBufferFirstMatrix = gpuBufferFirstMatrix.getMappedRange();
new Float32Array(arrayBufferFirstMatrix).set(firstMatrix);
gpuBufferFirstMatrix.unmap();

// 第二个矩阵,略,大抵同第一个矩阵,数据不太一样

// 后果矩阵
const resultMatrixBufferSize = Float32Array.BYTES_PER_ELEMENT * (2 + firstMatrix[0] * secondMatrix[1]);
const resultMatrixBuffer = device.createBuffer({
  size: resultMatrixBufferSize,
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
});

queue.writeBuffercommandEncoder.copyBufferToBuffer 异同

相同点都是向 GPUBuffer 写入数据,看它俩的定义:

undefined copyBufferToBuffer(
  GPUBuffer source,
  GPUSize64 sourceOffset,
  GPUBuffer destination,
  GPUSize64 destinationOffset,
  GPUSize64 size);
  
undefined writeBuffer(
  GPUBuffer buffer,
  GPUSize64 bufferOffset,
  [AllowShared] BufferSource data,
  optional GPUSize64 dataOffset = 0,
  optional GPUSize64 size);

不同点:

  • writeBuffer 是应用 ArrayBuffer 或 ArrayBufferLike(TypedArray 等)作为数据源,且要求 GPUBuffer 是映射的,必须是 COPY_DST
  • copyBufferToBuffer 是 GPUBuffer 之间的复制,须要 COPY_DSTCOPY_SRC 类型的 GPUBuffer;
  • writeBuffer 办法是队列上开启一个“工作”,而 copyBufferToBuffer 是指令编码器的一个操作,要比 writeBuffer 低一个级别。

写数据,还有个间接对映射的 ArrayBuffer 进行写入的路径,就留给读者本人思考与下面二者的异同吧。

正文完
 0