在 WebGPU 中使用时间戳查询
原文 https://github.com/OmarShehata/webgpu-compute-rasterizer/blob/main/how-to-use-timestamp-queries.md
本文如何使用 WebGPU 的时间戳查询(timestamp-query)功能来计算你的 GPU 指令执行耗时。
在 WebGPU 中,时间戳查询是一项可选功能,不一定全部实现版本都有。撰写本文时,出于安全考虑,在浏览器上是禁用的(具体原因参考 gpuweb/gpuweb #2218)
概述
下面先简单介绍时间戳查询的流程:
-
请求设备对象时加上
timestamp-query
功能请求 -
创建一个容量为 N 的查询集(当前帧要存多少个时间戳)
-
创建一个存储型缓冲对象(storage-buffer),大小为 8×N 字节,因为时间戳查询的结果需要用 64 位存储;
-
调用
commandEncoder.writeTimestamp
方法记录时间戳;它会在所有的指令结束后记录时间戳; -
调用
commandEncoder.resolveQuerySet
方法将时间戳查询结果写入存储型缓冲 -
将存储型缓冲复制到 CPU 可读的内存中,解码为 BigInt64Array(参考 BigInt - JavaScript | MDN)
按步教学
可在这个 PR 中找到完整的示例:Example usage of timestamp queries by OmarShehata · Pull Request #5 · OmarShehata/webgpu-compute-rasterizer · GitHub
0. 让浏览器具备时间戳查询功能
默认浏览器是关闭不安全的 API 的,使用下列参数启动 Chrome 浏览器即可:
--disable-dawn-features=disallow_unsafe_apis
译者注:从
--disable-dawn-features
可以看出,这个启动参数是 Chrome 系特有的,火狐和 Safari 不通用。具体操作可以右击 Chrome 或 Edge 的快捷方式,在“属性” - “目标”后跟随这一串字符串。
1. 创建 Queryset 和缓冲对象
请求设备对象时,需要把 timestamp-query
添加进 requiredFeatures
数组中:
const device = await adapter.requestDevice({
requiredFeatures: ["timestamp-query"],
})
如果浏览器未启用时间戳查询或者不支持,就会报错:
Uncaught (in promise) TypeError: Failed to execute 'requestDevice' on 'GPUAdapter': Unsupported feature: timestamp-query
然后,创建一个查询集和查询缓冲对象:
const capacity = 3 // 要存多少个查询结果
const querySet = device.createQuerySet({
type: "timestamp",
count: capacity,
})
const queryBuffer = device.createBuffer({
size: 8 * capacity,
usage: GPUBufferUsage.QUERY_RESOLVE
| GPUBufferUsage.STORAGE
| GPUBufferUsage.COPY_SRC
| GPUBufferUsage.COPY_DST,
})
2. 写入时间戳
在调配渲染管线的代码之间,调用 commandEncoder.writeTimestamp(querySet, index)
方法记录时间戳:
// 在各种指令编码过程中记录时间戳
commandEncoder.writeTimestamp(querySet, 0) // 初始时间戳
// draw(...)
commandEncoder.writeTimestamp(querySet, 1)
index
<= 定义的容量值 - 1.
3. 解析时间戳到缓冲对象中
在每一帧代码的末尾,调用 commandEncoder.resolveQuerySet
方法,以正确写入存储型缓冲对象:
commandEncoder.resolveQuerySet(
querySet,
0, // 从哪个查询开始
capacity, // 要写入多少个查询
queryBuffer,
0 // 写入缓冲对象的偏移值
)
4. 读取查询结果
即从存储型缓冲对象读取数据。获取 ArrayBuffer 后,用 BigInt 类型数组读取即可。时间戳值的单位是纳秒。
// === `commandEncoder.finish()` 调用之后 ===
// 使用下面的 readBuffer 函数读取 queryBuffer 对象中的数据
const arrayBuffer = await readBuffer(device, queryBuffer);
// 使用 BigInt 类型数组读取数据
const timingsNanoseconds = new BigInt64Array(arrayBuffer);
const readBuffer = async (device, buffer) => {
const size = buffer.size
const gpuReadBuffer = device.createBuffer({
size,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
})
const copyEncoder = device.createCommandEncoder()
copyEncoder.copyBufferToBuffer(buffer, 0, gpuReadBuffer, 0, size)
const copyCommands = copyEncoder.finish()
device.queue.submit([copyCommands])
await gpuReadBuffer.mapAsync(GPUMapMode.READ)
return gpuReadBuffer.getMappedRange()
}
5. (可选)添加标签
为了让输出信息更加友好,可将每个时间戳加上标签,输出的时候有利于辨别差异。将纳秒转为毫秒也有用。
致谢
非常感谢 Markus Schütz 在 Potree 关于 WebGPU 有关实现中 提供的时间戳查询示例。
感谢顾扬在 问题 3354 中解释了如何在 Chrome 中启用时间戳查询。