|
' w7 H% G! j X, t
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>
. ], B1 a: B2 U' \<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>- _, F4 K' u3 {7 S0 Y1 i: s
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>/ _% |# |$ J& s
<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>
" h9 x; P8 H+ n- ^( P5 S: a<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>& H2 c1 c0 ]' B- t0 t3 i$ i
<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ }); _! g0 d' l4 u9 [ K
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节( m( u# }) v/ S
2 P$ q/ d: B- `2 ^' @. U6 ?* J. P5 F// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作
+ ?# S* {* \* y</code></pre>
" A- Z! V- l, _8 l1 X9 K<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>/ z2 q6 T3 `5 [
<pre><code class="language-js">somebuffer.unmap()$ y; w z, Q( `$ Y# z C
</code></pre>
5 f# I6 x- H/ \5 y- _<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>! U+ X& u3 q# G }5 p; w4 u
<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>& V/ |1 V2 I9 E# a
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>
5 H- n+ u& u" S H; Q<h2 id="创建时映射">创建时映射</h2>( |' V/ n, V# S) [
<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>
8 L# }7 t! n0 M4 }. v# d<pre><code class="language-js">const buffer = device.createBuffer({$ w; H! V3 D+ }4 _4 C; \
usage: GPUBufferUsage.UNIFORM,
$ r# H& S+ w/ f3 |/ Q- u" j size: 256,) m8 I8 D# z% ~# V( I
mappedAtCreation: true,' n3 \: ^4 D2 r9 L1 t0 M" J
})- p W0 a3 A) [% X( x6 Y. ]! q! }
// 然后马上就可以获取映射后的 ArrayBuffer* @ C9 t$ V- j1 `
const mappedArrayBuffer = buffer.getMappedRange()) v& i8 ~( R) _- c$ M. K }/ L
3 j; W& |0 f& T f
/* 在这里执行一些写入操作 */" g3 [- ]4 f( \$ |( B
U* s8 ?. }- u6 g9 l! v' F
// 解映射,还管理权给 GPU
- v" p: [. E; F% X5 Mbuffer.unmap()
; c3 ]1 V8 \1 \1 z) |* h1 F `</code></pre>
5 t& ~ c0 X9 S<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>
; r# b& K) h! P4 E4 N) z3 M<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>5 w2 r2 J2 M. [% L! c. }$ f0 W
<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>
3 _$ r, F4 r. \) z' Q/ U8 H; `( s4 ^<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>
6 q+ E ]0 _) [. G5 e1 j<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>: ?: Q! }, }7 {1 o" e; Z2 c; s
<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>& S6 J# u7 C1 T7 Z/ n: H
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>
( P" b* @5 ~0 Z" D<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>& N& s0 w7 S: z. h
<pre><code class="language-js">const texture = getTheRenderedTexture()7 m# w% ~6 Q; m
! ^5 S* F# U2 j/ K
const readbackBuffer = device.createBuffer({. h4 `. k6 W( L5 P+ U, @! K; B. O
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
; Q: C ]9 g1 O! L, A p size: 4 * textureWidth * textureHeight,) ] ^* _* N8 t& L
})
# @2 {2 u: r" |, \0 I/ ]/ S. L' y. Q
// 使用指令编码器将纹理拷贝到 GPUBuffer
h- M Y, E/ W- ?( Sconst encoder = device.createCommandEncoder()7 q; t( m9 i5 V" D
encoder.copyTextureToBuffer(
6 f5 L! ^; |" @" q- M3 y2 Z { texture },
% C$ F$ |$ S& q3 o: k4 E/ h/ Q { buffer, rowPitch: textureWidth * 4 },, k2 o$ Y* W% R. e0 H( O2 g
[textureWidth, textureHeight],8 r% V) U) Z: n5 Z- T4 q+ G& x ^
)4 I* Y: Z* R3 _2 }
device.submit([encoder.finish()]); l* P9 p5 {/ i6 q( A0 i3 H
. A M; N) b+ L V3 n1 J. b
// 映射,令 CPU 端的内存可以访问到数据6 m9 |0 T- V& O2 Z4 J$ N
await buffer.mapAsync(GPUMapMode.READ)
9 ] H7 R3 V0 X- W9 h7 q2 B// 保存屏幕截图. P b. m7 w# r
saveScreenshot(buffer.getMappedRange())
( [5 l `+ g/ k1 i* a! p9 E// 解映射6 H, i, G5 W& V5 n3 [
buffer.unmap()
2 O- h9 x1 j: O5 \9 |7 S0 W( ?: G</code></pre>9 S6 j8 Q0 ^+ r6 S) \. U
4 A& U& e. _0 p7 e' _& ? |
|