|
|
" J3 F6 } I9 b9 |* A
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>7 N6 J# D% D. |7 S2 |$ e
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>0 |/ A8 N/ o0 J( q
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>
& n1 }0 b* u0 N' e<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>: b( h0 c" M0 i) W
<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>
( o. C, L$ @. I+ O: W; ^1 H<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })
0 F9 r8 K; Y2 w: W2 Eawait someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节
# e4 j8 m9 k/ K. C2 N' H- N2 I5 a, G% F
// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作$ M1 C# ?: `) o7 Z
</code></pre>$ C$ O5 k) V& m
<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>* I7 X5 n# f+ Y7 i+ e! @5 ?
<pre><code class="language-js">somebuffer.unmap()
7 Y) x5 e- U, G# ]</code></pre>
F( T! ]" X; [2 C7 |<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>
6 l" k% g" k9 f+ K<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>( A0 i& n. ^7 ~" y6 Y& H4 U( A
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>
3 S7 V8 m1 f7 F! a. _% ^<h2 id="创建时映射">创建时映射</h2>$ ?: q+ a3 Q- F/ Y! ~
<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>! s" s2 N0 h% ^+ x6 ?; D
<pre><code class="language-js">const buffer = device.createBuffer({2 a* _$ N, Y+ c7 t( Y, ^: z6 G
usage: GPUBufferUsage.UNIFORM,
- ^. B1 ~" @7 [# \+ j size: 256,- t& I, t9 F7 R7 x3 X
mappedAtCreation: true,
, e3 D8 x0 d7 y/ v1 r% n})
' `( s8 W2 z a1 |/ X. o// 然后马上就可以获取映射后的 ArrayBuffer
' X" e7 I; Z! h' _; j4 H) nconst mappedArrayBuffer = buffer.getMappedRange()- Y% f$ d: o, T: q
4 K; C$ ]- b5 ]2 z
/* 在这里执行一些写入操作 */9 w% [# f d2 O, y
; o, {5 Z% N H$ t
// 解映射,还管理权给 GPU
% }/ p# H* \* V9 V$ ?! R8 Q& xbuffer.unmap()
; j: u( t6 _( Q</code></pre>
! N3 D i3 d6 F9 a) n6 b<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>
- E' R6 I4 G0 d% M1 ~0 I<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>
( i5 X/ A7 ~0 f<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>* ], _. ^# e! L$ ]8 T c
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>
( D2 W3 w1 i& t% s<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
+ b5 J9 t( ^% M# Y<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>! b3 b$ ~# I* V2 {- f1 a3 f1 P
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>3 s7 y+ h; s% J8 M7 W/ r) n0 T5 r
<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>
" D* u/ N9 R! D6 w* S5 G<pre><code class="language-js">const texture = getTheRenderedTexture()
& z; p6 q% E% D0 F( m0 f7 b$ q. O% g% `2 P+ q$ x- h% D% g
const readbackBuffer = device.createBuffer({3 c/ q2 }' @" \- T) X
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,& z8 u2 ^# h9 E
size: 4 * textureWidth * textureHeight,
2 ~) r$ C& x9 r/ g, M: W2 q8 Q. g})9 y" f' V1 Y! h. N9 m' I' s/ W
. j; y H5 i# p- V$ c// 使用指令编码器将纹理拷贝到 GPUBuffer: ^* U+ K0 |/ a/ J
const encoder = device.createCommandEncoder()6 v5 W% b" w: }9 l% N
encoder.copyTextureToBuffer(
: `4 A3 e" |( t. z4 M { texture },( ^6 Z6 B3 E- \, Q0 s2 Y& z% f
{ buffer, rowPitch: textureWidth * 4 },
6 P8 H, R4 ^% m L [textureWidth, textureHeight],6 T" y# r! e" f1 Q. O: l
)
: C) B/ o' a& ?8 ~) x/ Edevice.submit([encoder.finish()])
8 m, G" o5 \0 U' {6 a$ _/ U C# _6 a
// 映射,令 CPU 端的内存可以访问到数据# R- t2 Z) |# r* Y8 J$ b, c
await buffer.mapAsync(GPUMapMode.READ)
- q! ]- L" a( [( R d# [& R/ y// 保存屏幕截图9 n' t# U$ y$ K6 e
saveScreenshot(buffer.getMappedRange())
) s/ a" M4 ]( j6 f/ {1 T// 解映射; [ \: d' J: Q$ z5 n& {% h
buffer.unmap()0 c% M# Q2 u+ _ X1 D
</code></pre>2 _' H7 `+ v8 ^: d
+ o% z1 r0 [+ M4 ~4 s
|
|