|
- i% e; B: x/ t& ^8 G$ M: W
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>
" n- q) q+ ~2 Q<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>
! T0 p% `! N, r; K9 x- u6 R<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>! u' A' {+ l) ^
<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>
- `9 I: p- E5 g/ X<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>
+ j, U/ b1 {/ M+ n8 z0 x5 q+ [<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })8 @$ r3 N' j( R/ g& R9 O
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节
. [* [" ^7 x/ O" b7 {$ f9 F. W) p2 {: ?
// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作
( T0 a' j. N; C/ y* `: b</code></pre>
8 n- f) k5 m0 B- `<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>
8 s" H0 q) o; n5 j( t<pre><code class="language-js">somebuffer.unmap()) ^; z0 `. c4 C* j( F* N
</code></pre>2 Z6 [5 d/ l6 P/ u
<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>3 l. d6 H" O! H0 D$ P( U) V
<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>
4 z: t% \& h0 ]% l- }! ^1 z<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>
, ]3 M$ c; A6 P p<h2 id="创建时映射">创建时映射</h2>; Q9 @& q6 t+ ?3 n5 B
<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>( M; @* o8 P2 D! N3 @* S
<pre><code class="language-js">const buffer = device.createBuffer({
3 U& v( i; @" |- i% G7 d9 f usage: GPUBufferUsage.UNIFORM,; |0 W: y4 h6 S# a" f3 V
size: 256,' t0 ~) r. y, I9 P- D w: d# N
mappedAtCreation: true,
6 H* g( ~) v# E/ A& B1 S, I})2 y9 \. \- h$ E% b
// 然后马上就可以获取映射后的 ArrayBuffer
* o& S& @- ~/ w: j% ^" b, Xconst mappedArrayBuffer = buffer.getMappedRange()8 Y7 [1 d; U1 ^- e6 Y
- v0 Q( t) J. d- _6 \( F/* 在这里执行一些写入操作 */ Z6 r" f- r. u
, Q' U6 _8 p4 a7 y// 解映射,还管理权给 GPU0 \5 {) n9 i e; F
buffer.unmap()4 B% g6 s% ?1 O% |
</code></pre>
$ c3 X9 L: @! V8 O5 D7 z<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>/ C( |2 i2 ]* _9 t" t9 h
<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>
/ Y7 {0 j( G. E" O# C1 D; {<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>
" ~ n2 u0 Y% ^: ^' S5 A<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>
" l3 v, ]( \- F+ ~1 ]<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>' I9 F7 F" O4 @
<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>
Z7 B# A I4 w<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>
' s% H' `8 m8 O1 j( Y<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>
5 L' G) {9 W2 I0 \& |! y& `5 B<pre><code class="language-js">const texture = getTheRenderedTexture()
0 X y% B* Y* E/ v
& E9 q" K, r4 t- E1 w4 ]& pconst readbackBuffer = device.createBuffer({* G4 W! U( g0 L3 [- l+ n
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
1 l8 }; d* c& X6 i size: 4 * textureWidth * textureHeight,
( b4 |' `# L4 |+ o, T4 I Y})5 G+ t3 v% k$ e, w0 l# E
8 }. E' D4 z& s) s6 ?// 使用指令编码器将纹理拷贝到 GPUBuffer: k b" `7 m" z, V" B4 S* k
const encoder = device.createCommandEncoder()
$ P) P4 }/ F/ Z4 I, {encoder.copyTextureToBuffer(" i, Z B0 V* I1 z& I" ~
{ texture },' P! v8 h- u' |/ n: o+ J6 j
{ buffer, rowPitch: textureWidth * 4 },( j# {! K. ^# i/ t3 K) r0 H
[textureWidth, textureHeight],
! c5 @/ {2 x2 N/ n)0 V6 U, P N( F( |$ [
device.submit([encoder.finish()])
/ {9 D9 W; C" h- L- c$ U& @0 D; W0 |& \6 w$ V. x
// 映射,令 CPU 端的内存可以访问到数据
; x3 }, B2 D5 e8 c) {$ x" \4 Oawait buffer.mapAsync(GPUMapMode.READ)" A) Z) |8 \% ^* t% Q/ ^5 B
// 保存屏幕截图
! [! u9 N. b3 y4 i% C- S: XsaveScreenshot(buffer.getMappedRange()); R4 N" R, E1 e6 [% G2 w
// 解映射
& n* |9 b3 P# R: Q' a$ Kbuffer.unmap()
* W( Z4 f' |7 c5 H! P1 j7 R" Y</code></pre>: R2 D+ n7 T( e7 I* j i
- Z+ `6 W2 j9 A |
|