|
5 u' @0 b- n3 N. J% q3 ?8 V<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1> L: ?1 J' z J+ L Z8 ~# v( n
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>3 {1 w" z% J6 Y/ i2 _0 `8 K* R
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>) S, @' u6 H* \
<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>: A& s& Y& W4 z" {! m
<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>, J+ s" `9 q. A' J
<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })6 V4 `( c" W5 Y4 I
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节
7 r9 j* [5 A( Y$ Z; d( c
: N/ o3 j0 G, ~+ y# M$ _// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作( _; i9 Y% w6 B, J
</code></pre>( O- e6 q' j/ L# T2 t& e! M, T
<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>
9 P3 i2 d6 m# U<pre><code class="language-js">somebuffer.unmap()0 Y |: w4 ?. E6 t0 M+ F7 ?9 P
</code></pre>
2 G, t1 C6 ?% Z$ B* @3 v/ T<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>$ A' V; ?2 I0 {/ G ]/ n* J
<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>0 }; L5 p! e1 w7 b3 v
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>
1 r6 W' S# V8 A<h2 id="创建时映射">创建时映射</h2>
- V0 a" T! F- J5 p/ p<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>: U% P( v: D! W7 c2 e& O- v, L
<pre><code class="language-js">const buffer = device.createBuffer({5 b" ^! w( n$ @5 y% w$ j
usage: GPUBufferUsage.UNIFORM,
' j F2 D5 k7 ^1 {+ K# f$ t size: 256,
3 ]3 m$ O/ t* T% F) x1 J7 s p0 B3 ^ mappedAtCreation: true,
8 d) ]( _' o/ u9 U})% s# B& F2 x: ^4 w7 |
// 然后马上就可以获取映射后的 ArrayBuffer: g3 h2 w4 H7 P: [. P
const mappedArrayBuffer = buffer.getMappedRange()
" \. J$ _) T) @1 V! _+ V6 q. T# P
3 D* h! x& K) p: F7 X* M/* 在这里执行一些写入操作 */
9 |/ z( w9 L, R% O3 Y+ z7 ?! |6 Q' | z! d v$ \# ^8 N
// 解映射,还管理权给 GPU ?8 L6 ?, R/ ~! N( n+ H9 ^2 e
buffer.unmap()& ]3 C0 M( g+ n
</code></pre>
6 @1 I6 q+ `, d7 N<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>
4 ~- ^' b/ ?9 |- U( F<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>( m; T( q4 X" v0 p. I
<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>6 t+ }$ r4 y" Q/ ] A
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>
c/ t* o" ^/ ^3 }<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
$ I( F* g5 ^9 M2 B; W<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>
1 S! U% v& l! m1 W( _<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>, j, ?6 s9 {. \7 V
<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>! N3 T1 G4 d! v* @! ^$ q$ s
<pre><code class="language-js">const texture = getTheRenderedTexture()- }: ]6 ~, \7 a4 O* _# b t% a3 _
* a# ~7 q+ r; Y' Y5 a& sconst readbackBuffer = device.createBuffer({; \& ?! G9 c3 _0 o
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,; n, j u: f% u% t( y
size: 4 * textureWidth * textureHeight,
/ c" `8 S8 J) ?/ O: ^})& \# A; G/ g, J3 ~1 W/ `+ b9 G
( ^, W0 n( H: y9 t
// 使用指令编码器将纹理拷贝到 GPUBuffer" g/ d3 ?/ x' c( {
const encoder = device.createCommandEncoder()
( f" k8 _8 u1 o Rencoder.copyTextureToBuffer(
: ~! A) W2 ~" g. z; \+ |( @ { texture },
; |) P! F d! I# g3 s5 m3 t { buffer, rowPitch: textureWidth * 4 },
A; o' s( \1 I6 Q- T5 x3 T [textureWidth, textureHeight],8 C; M; d' z/ }; ?, S. ^4 L
); j4 w9 k. r* q, }
device.submit([encoder.finish()])
# Q# }3 D2 h8 Q4 I% h/ s1 \7 @5 L2 F; A+ _; w# N
// 映射,令 CPU 端的内存可以访问到数据
6 E5 J ~1 Y% g5 E) Sawait buffer.mapAsync(GPUMapMode.READ)2 ^; |* \+ g3 z/ y2 g
// 保存屏幕截图1 ]5 ?& L& C$ Z! h B
saveScreenshot(buffer.getMappedRange())" l. }3 l1 s* P+ ^& f5 W
// 解映射
' d5 d( v2 r& R* q" tbuffer.unmap()7 ]* F' r* [( d5 `5 I7 }! u4 R8 C* o
</code></pre>4 f; B9 ^% W0 t) O% x) f( x
) |) o8 M3 g6 l |
|