|
|
- d$ h+ u! c; I2 C* d: r
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>/ A" S" i/ b$ c* X. J$ d) F2 a
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>
, _- V1 N' W) |* |<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>
( m8 m5 T8 L& D<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>
: B6 i: x. ^" |: h7 H5 h<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>
* W$ S# _) W9 z% m<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })
% C' j+ p4 J: h4 H1 K5 Rawait someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节8 z4 b/ s! B& p% H9 t2 M! {7 K: @
/ E. K! Q7 o6 {- @) n2 j9 s// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作' p5 W n+ Z/ {$ b) n
</code></pre>7 y3 B. |: C4 Q# U; _/ V- e4 _" u0 L
<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>
/ L4 [4 E# P% F% Q) V3 v; M) c<pre><code class="language-js">somebuffer.unmap()
% f2 \1 C" [# s$ C. n- d1 P</code></pre>
/ j' q" g& |' L% R0 Z4 j<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>
2 Q9 K* w+ P( e8 W# x2 L C<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>& ]( a p. \2 J
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>6 I7 X' a, p( B J' F# D
<h2 id="创建时映射">创建时映射</h2>
. D3 p! N- ]2 O9 k% }5 u& a' ^( L<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>1 t) D" ]; {1 i+ q- r1 p
<pre><code class="language-js">const buffer = device.createBuffer({
& l: `. ]" _# N! g usage: GPUBufferUsage.UNIFORM,9 }; B2 l, T0 V5 o' U! |
size: 256,* `) ?% A. v* o& i- F" C, ?
mappedAtCreation: true,
9 h' ?3 V3 @( Q4 `) ^. A})8 n- j, C2 ? K6 C3 r
// 然后马上就可以获取映射后的 ArrayBuffer
: i0 ^5 q6 l: `; Fconst mappedArrayBuffer = buffer.getMappedRange()
, _" s( c$ O; R. q
6 j# K0 `$ U6 y6 _' I* L8 I% \2 I2 V/* 在这里执行一些写入操作 */
6 \2 A& D* Y9 l/ `" Q% u6 U
+ N! F* Y1 w5 Y/ f7 i// 解映射,还管理权给 GPU
- [% z! X+ q& xbuffer.unmap()" }. ?1 z0 I* d2 B5 ]
</code></pre>. Q4 [2 |% S- f( g$ y0 |, n, e/ n
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>
r. v2 V, |; s3 F1 s* w2 i<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>6 K4 F7 Z% V T; l" k: [3 q
<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>
$ r& J/ m' I+ Y& d4 U( p<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p># ?# T, b" h1 f' }0 S8 c7 N3 {
<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
" g. _( Z6 R( ]6 G<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>) V; |3 X$ m1 K- u" ]( Y' R
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>
7 S9 Q, x3 \- Z |* Z<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>
3 ]5 n( S, d: \- J6 j4 A<pre><code class="language-js">const texture = getTheRenderedTexture()
; _9 S# ]$ r( A- q- ~% v" Z2 {" X5 ?( F
$ W; ]' w) @# Kconst readbackBuffer = device.createBuffer({6 v( I4 ^( C: I, |
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,% {/ _5 u) m6 o) ]
size: 4 * textureWidth * textureHeight,
, h K7 X& n* @" F6 Y) A7 V! w})" J0 b( O& H) ~) v+ p
; l8 e4 P) _, _3 T// 使用指令编码器将纹理拷贝到 GPUBuffer
% z8 v! |& s( W1 |, i2 J: y' J4 sconst encoder = device.createCommandEncoder(); j' e& S: k( ~$ z& j
encoder.copyTextureToBuffer(
a& ?6 U6 r7 r B% [& x* P { texture },
2 q) z z' Z4 r, a1 Y% p" c- W/ s# ~ { buffer, rowPitch: textureWidth * 4 },5 \0 n# C: q0 F( ^9 P8 v- \
[textureWidth, textureHeight],, f2 s' }/ C5 o+ J0 H4 E3 n
)' C5 h2 i. r- R: S) H1 y7 e- C' s0 z0 Z
device.submit([encoder.finish()]), r# w0 S& A7 b5 L
) u/ `5 _+ {7 g4 U1 m* s( N' u: B// 映射,令 CPU 端的内存可以访问到数据, ?8 v( @4 h( O) M9 R3 T
await buffer.mapAsync(GPUMapMode.READ)
& T: X' K9 S* u0 g L% ?; B// 保存屏幕截图
: w' F% {5 g; t2 P+ y* K1 FsaveScreenshot(buffer.getMappedRange())
9 b7 q Z z7 S O: Y, q! }& U// 解映射, r2 q" @0 C% E* ?8 w7 a
buffer.unmap()
" D7 x* k0 }7 J4 N+ Z% Z' M$ G</code></pre>( ^2 k: @9 v& W* D
1 u* b6 G7 O8 b% N3 a6 F# ]+ V* } |
|