|
|
- m' H7 j( r$ Y7 {, G1 s6 T
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>6 U* x4 K; n1 \ `8 j
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>
$ \3 j2 v) P$ E8 N$ Z- x8 {<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>
4 P1 ]! _6 W9 S% ]% @; ?<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>) i) L# @% {; b5 _2 t* h
<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>. T4 u+ O0 }, c6 u: V% I' P
<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })" g3 a/ h$ P' |, X0 H v% W
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节
: v+ F N5 p; f: R T2 q
# I4 r( L0 E5 ?" o) k// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作
( m8 T0 _, B g: n- L! u</code></pre>
$ y" X, k5 g# x<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>; Y0 {1 [9 k$ c
<pre><code class="language-js">somebuffer.unmap()
5 I& d+ X/ c: E: h</code></pre>+ E0 \( ], D) j' W
<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>
8 S6 h8 |8 S. _<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>! |2 ?/ n7 j6 v' \
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>- c6 V: L& O: M
<h2 id="创建时映射">创建时映射</h2>
7 x9 [$ Z; X N. y. b$ K" @6 X<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p># o* t# R& b* p& ^# u X
<pre><code class="language-js">const buffer = device.createBuffer({) c) f8 ]" O& a* i1 M0 M+ h r/ C
usage: GPUBufferUsage.UNIFORM,
' Y% b* \% c3 a/ u& z size: 256,
) T- r2 n2 }) @$ Y9 ~0 Z+ r/ @ mappedAtCreation: true,5 F% t3 C8 v$ f( F5 J' F0 n
}): I$ T0 U5 t6 }; C0 V& N \* G
// 然后马上就可以获取映射后的 ArrayBuffer
# h9 Y. _5 K+ xconst mappedArrayBuffer = buffer.getMappedRange()& g; e7 q) v7 y9 S( @
2 f* P8 c7 [9 I8 E7 ^ y/* 在这里执行一些写入操作 */ f) _% n4 ~: z. j0 V
) `/ z0 y6 i- `; V// 解映射,还管理权给 GPU
$ z8 Y- x3 g! i& w) fbuffer.unmap(); g' a( }# l, D4 k" a
</code></pre>% H. ]5 H5 U- w" D/ v
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>0 S" F% [, H* j: T
<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>
* ^* y) _& F' M<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>7 s/ z. `- J" W
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>$ }; M* g e- {
<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
0 ~' C$ M, R1 u2 ]<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>
% m$ O. Q+ F) {/ i1 Z+ c1 y3 V; H* y, w<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>
; Z" X1 I9 `/ y2 A, V! t<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>
' j$ `5 \: M/ F% r# n, E1 ]<pre><code class="language-js">const texture = getTheRenderedTexture()
8 D9 }% G; W, S' R, | s
+ {6 X- [: I2 t7 _9 [& u' Fconst readbackBuffer = device.createBuffer({& Q! M& S1 Q2 I* G
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,0 T& j: G, p; K" `' F
size: 4 * textureWidth * textureHeight,4 d! m+ p0 l% d
})
6 N& ?6 `, _8 w% \
9 d6 m! m" ?3 C5 v8 Z// 使用指令编码器将纹理拷贝到 GPUBuffer
! m4 V: a! t7 j7 @3 y4 x& N5 U3 @const encoder = device.createCommandEncoder()/ y6 y4 B& e* c9 P+ G* R
encoder.copyTextureToBuffer(
; e0 Z+ y7 K) H% W! x$ ?7 W9 r# M { texture },7 g# H. o5 n: B0 h8 g
{ buffer, rowPitch: textureWidth * 4 },2 L2 E2 W0 I% F
[textureWidth, textureHeight],6 p( t* L6 t( I4 M' `- O
)
4 g1 B0 |$ y7 ~0 B! ]; m$ E: ~device.submit([encoder.finish()])
! S, G) b! r) {. {3 M8 ]
/ k. M( j8 U0 b3 q+ K// 映射,令 CPU 端的内存可以访问到数据# E9 W g( \$ q5 H! I+ |+ D) W' K( w
await buffer.mapAsync(GPUMapMode.READ)
4 [/ z: X, B# ~( f: {// 保存屏幕截图
& o4 B X7 h# UsaveScreenshot(buffer.getMappedRange())
9 k6 J- f" @5 A2 x+ C9 g8 s8 W// 解映射' }$ I9 t( ?! i7 i4 W' C
buffer.unmap()% P3 [2 V3 M# M2 K" B9 s
</code></pre>: x }# ?: I2 B
7 t, @- m7 X2 `2 i* V. @
|
|