|
|
: Z$ t* n6 z; t ^7 u: U+ I) a
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>
' x a1 a0 Z x! ^; u& L4 r3 A( c<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>
8 F- |9 c* G) e9 L/ i; U# D<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>
6 F# [' B5 \5 E( M5 ?% O<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>3 ^" G; P& I: V
<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>- ]% S* w- N- O* H6 @+ e
<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })# a5 ^# U8 S$ D7 E! ]
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节# G/ C+ W, W% v
. V# V3 t2 Q2 z# g// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作
, H3 E x M& m" f7 l: g3 T</code></pre>
9 k7 F7 `8 l0 W<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>$ f, g3 o9 s* n* t
<pre><code class="language-js">somebuffer.unmap()
5 d. z, W) s6 w+ ^: R</code></pre>
* _. @8 w5 A6 T" p( v6 H3 Y6 _<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>& @2 [4 |% `3 h7 d$ Z
<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>5 ?" n8 e6 n7 a. g4 R
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>" F* h/ }0 z- D6 T; {7 F3 R" m$ u
<h2 id="创建时映射">创建时映射</h2>) }3 n: r4 Y& J6 ^5 y# B
<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>
3 _4 {+ k4 m( q' ?<pre><code class="language-js">const buffer = device.createBuffer({" n/ t. H( y" Y6 ], K `4 |
usage: GPUBufferUsage.UNIFORM,
) M$ S' q& r7 f: W size: 256,6 t/ U1 C! e/ A' ^. `
mappedAtCreation: true,
% E/ O4 n- @8 A( H. q8 H) `( i})0 ]" @6 I/ z+ A# U
// 然后马上就可以获取映射后的 ArrayBuffer
( ]4 U8 {7 B) J7 X! [, V( Wconst mappedArrayBuffer = buffer.getMappedRange(), K; c4 v; u1 @6 w8 ?; c8 T1 _* s6 u
3 J# f1 r' \ k+ b- w' W" Q* O
/* 在这里执行一些写入操作 */ _1 }7 D2 B2 `2 A
3 l- B* [. y3 j" S2 Z
// 解映射,还管理权给 GPU
/ {; N8 Q+ ]. Lbuffer.unmap()) G, l6 {- a* U$ H! i' c
</code></pre>( f* n% Z' I" X* M% d
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>
- q" `8 H$ i$ ~ f! l$ z; D<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>
/ [+ x6 ]0 i" Q9 ~7 R' m<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p> u9 U: Y; ]3 S5 X6 o9 e
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>
* K3 f0 T. ?+ W& o8 K. |<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
8 I. h4 U# Z: z- J6 p7 d" k/ \<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>5 m% Y; d2 p0 Q5 Z5 Q
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>
/ H1 |# T# i7 {, i5 R* Q<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>
" v& A6 ^, e+ Q+ @6 {<pre><code class="language-js">const texture = getTheRenderedTexture()
" o4 v. n1 J5 y$ Q- ~: K. B G$ l! n( h$ p% L- Y, N# Z/ u
const readbackBuffer = device.createBuffer({. O7 t/ G" _( W# a
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,* Q+ h, ]! R' L- B
size: 4 * textureWidth * textureHeight,
( H; U5 ~% e8 ^+ q- n- n1 j1 ?})
$ R! f4 x5 f, \3 t
1 W. J7 n+ V5 S// 使用指令编码器将纹理拷贝到 GPUBuffer
& N* l6 R% j4 K3 zconst encoder = device.createCommandEncoder()
! m3 B! C+ y. c) @encoder.copyTextureToBuffer(4 F( N4 N4 Z( f! E3 D, Y+ ^5 }2 b
{ texture },' A _- X; K) G0 H
{ buffer, rowPitch: textureWidth * 4 },
3 ?9 p! V% a$ A1 [# Y% g- U [textureWidth, textureHeight],* s, u- \5 s3 s4 u
)! t& o7 `3 J B7 Q- x
device.submit([encoder.finish()])
, }+ U ]8 F7 s2 C5 M4 ]5 L. k- W* R( }+ W9 O* o( S; S
// 映射,令 CPU 端的内存可以访问到数据
( S8 p. ~$ g5 B0 mawait buffer.mapAsync(GPUMapMode.READ)
- w# U3 I' x3 L( V' p0 v// 保存屏幕截图5 n( u; w+ {, u* n2 X
saveScreenshot(buffer.getMappedRange())' D( T7 e; Z* A6 v3 x) q
// 解映射- f, ~( z) i4 t" ^. U+ C8 K
buffer.unmap()
$ ~; J3 Y& u n2 y7 f</code></pre>
4 h5 w& ]9 @( T* C
) J* Y I0 f9 j1 l( z. C |
|