|
|
5 Z# h! x, G( E1 L3 j
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>4 n6 E( Y5 ?' S9 j# `8 i
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>
/ a7 e. s6 v6 e t<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>
6 y4 }( {, v# o<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>; ]! |/ j# w1 n
<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>
6 N/ y5 N- P1 U<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })# t1 t7 f7 B. z
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节+ i* m, l+ {1 ?0 E
7 J/ M$ P6 y% Z& w& d' n// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作9 i v2 k$ J: I4 c# r
</code></pre>
/ Q' g' Y( z; c* ?( t+ r<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>
8 q; `& x6 k7 U3 e& V<pre><code class="language-js">somebuffer.unmap()& o$ Q5 J% j7 @0 w; [
</code></pre>2 X0 i8 A) d5 ~4 e/ t, N+ k
<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>2 k* V5 \$ X8 E/ o
<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>
4 K* w8 H' u7 K; r4 Z<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>7 u. w0 `- [" [
<h2 id="创建时映射">创建时映射</h2>
6 @( C/ {) B4 M5 l' V" i" P3 J<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>. E4 U' J* X+ a
<pre><code class="language-js">const buffer = device.createBuffer({- [$ I' F; u1 H t# G3 x/ e3 ^
usage: GPUBufferUsage.UNIFORM,* |, q! b+ u/ @3 u% E
size: 256,7 q( h+ C8 s0 }& A/ Z
mappedAtCreation: true,; M4 j$ ?7 m+ b
})4 J" m; H' x1 K! P, o* Y
// 然后马上就可以获取映射后的 ArrayBuffer
) R: F9 |1 s0 B* m* C [const mappedArrayBuffer = buffer.getMappedRange()) n+ M: Q, p5 V5 ?4 g
$ y$ E: F. \/ _. `- _8 H
/* 在这里执行一些写入操作 */% r; b$ P: i. C7 O
. |7 U# o: \1 g// 解映射,还管理权给 GPU
& @1 Y: ]0 T$ B( d$ W- rbuffer.unmap() G; P' `5 c) }4 Q( y
</code></pre>
/ w' X3 N% w9 ? q0 J<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>
& x9 }- J0 f1 S2 V4 l, \2 D% O<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>
6 c1 u6 s0 k1 \# }<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>$ |8 |" @/ W: Q
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>
5 ?7 s) p) R5 i; q8 b<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>8 V# i3 \# B( e% u) o
<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>
6 \! y8 Y3 U0 q<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>
$ C! s6 G% Y4 B# e! M/ A<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>
* h7 z) A0 h% d2 \% x- V<pre><code class="language-js">const texture = getTheRenderedTexture()( k. R& p; K6 e( F" f
: i8 x0 s: r6 a6 ?' Econst readbackBuffer = device.createBuffer({# L3 U" X# d/ D
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,7 z; G4 f! R0 E: b" [
size: 4 * textureWidth * textureHeight,
2 x1 t5 W% i- Q})
4 H9 z* m! |2 M- {
9 w# w( f$ F8 y- N( _; B// 使用指令编码器将纹理拷贝到 GPUBuffer U6 s n2 G$ i5 E% ^2 U: x
const encoder = device.createCommandEncoder()9 w2 r1 j) v+ l- l! u# {! G
encoder.copyTextureToBuffer(
3 K6 s! A+ y% A& h% |! k { texture },* P% |0 G% c9 B5 n Q# v# E
{ buffer, rowPitch: textureWidth * 4 },
: Z! w: `5 N; t7 B5 } [textureWidth, textureHeight],
: M9 i/ S' s) i* C1 T)
7 w6 H- Q; k- G# u/ p& G% y8 _device.submit([encoder.finish()])
7 V* M9 ]7 L+ G, O q+ a
( v o8 W- J: z- q// 映射,令 CPU 端的内存可以访问到数据
' G3 L2 f3 o" B! ^: x% f9 U5 Eawait buffer.mapAsync(GPUMapMode.READ)
4 A9 K2 }5 O, y8 \// 保存屏幕截图4 [! M" B+ L8 [3 w- ]# r
saveScreenshot(buffer.getMappedRange()): m. N( n/ y5 ]/ m
// 解映射
' v% A2 V& p& ?& r; d5 B7 Vbuffer.unmap()
% y4 J9 u3 F$ Y, `7 Q/ w</code></pre>
2 t- k0 |: I: l5 Y' V
( D7 b5 N3 V& Z O C |
|