|
|
) F$ |" v( o" x8 B0 p; n
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>4 z7 Z& r& t6 C( ?! ?" X& i
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>. B# g& \2 o1 f- w% X
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>5 r2 \( I7 P9 {( ]! b! P& p
<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>+ D8 U7 d: o f5 M# p
<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>
. E' }; V( f$ ]' t q<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })* }# A" _% D2 u% Q9 H
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节
; ?* a8 x) h8 a" j- X& N8 g
$ M5 q1 g9 k: {; i+ P, j// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作. C& D7 ^/ L' s1 p2 B
</code></pre>
) M* _ w! q" O<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>
3 h8 M* a8 U N<pre><code class="language-js">somebuffer.unmap()
" \0 d3 a' \' J4 s7 ~: H, _</code></pre>! f9 q: S F0 q" r: T1 S
<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>: Q+ |2 s6 @+ ~1 T
<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>
% ^' `$ d+ X: ]# p8 S<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>: Z# m9 o0 \$ e4 q. I- Z/ u
<h2 id="创建时映射">创建时映射</h2>
1 G! R" f/ S( M( M! [ M<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>% W' d) E! @) n
<pre><code class="language-js">const buffer = device.createBuffer({& e- h" ?! t6 J e
usage: GPUBufferUsage.UNIFORM,
/ T# ?* H: ~% e7 ^. l size: 256,
# B9 _; c. I; K mappedAtCreation: true,
% H4 B0 Y3 [* l, [7 S9 j" g% N})+ I0 s1 k1 P. @; c# `* Q7 h, Y
// 然后马上就可以获取映射后的 ArrayBuffer
; ]" E' n/ O# f; ]7 vconst mappedArrayBuffer = buffer.getMappedRange()
, G3 c8 v9 H" P: t
" v$ D! F! H- ?$ g4 c. \" X/* 在这里执行一些写入操作 */
8 S, p4 r& Z9 ]) L8 c& `5 [ Z. x+ y, u H& y# K; I
// 解映射,还管理权给 GPU
: p2 x# _2 d9 }8 G$ kbuffer.unmap()1 D5 Z* k6 R! ^7 O" e
</code></pre>5 O0 {( h0 R5 y/ \
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1># `- V K' n0 ^1 T- ^# W
<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>
8 ^) T6 {: K U' ?- Q<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>
3 }+ y) B: }0 }7 D* d9 w/ R<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>
# o8 H) @$ I! |$ y, g& S<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>. H$ [2 R3 f- n' |! f. w! d
<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>3 i4 I2 R) H6 B( b4 u
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>( C+ N% W) t/ V" o: ^' {& p% `
<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>) v0 ^: [' l8 t4 V- p/ O/ C" N# A
<pre><code class="language-js">const texture = getTheRenderedTexture()
# R4 v, ], H4 s6 M8 S
% O" |* h1 [2 o1 a7 yconst readbackBuffer = device.createBuffer({; `; X& Z( y' R* M) h
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,8 M& L3 ?; `" K/ R
size: 4 * textureWidth * textureHeight,
: f/ j8 d4 w. F, k) v) U B% l})
) ~' |5 f: @4 x! N% j: E6 k7 }( R+ F) i M# y) |1 B
// 使用指令编码器将纹理拷贝到 GPUBuffer* t4 h+ Q, f0 F4 ]
const encoder = device.createCommandEncoder()" `, g& j% z# } C e/ `6 h
encoder.copyTextureToBuffer($ M; e: I! T& O. D2 y
{ texture },
# b2 e( J" N. N; f/ x$ v { buffer, rowPitch: textureWidth * 4 },
5 n* g. M( y: {! E- ]9 z# [2 g [textureWidth, textureHeight],
& W# ?. c% q% |- N4 o)
! \! {6 P. ^: |device.submit([encoder.finish()]); x- j! k# R0 z- z# Z+ d
5 n; L$ c; t) G1 b: [
// 映射,令 CPU 端的内存可以访问到数据: r% X' f, a! `* p- R+ h
await buffer.mapAsync(GPUMapMode.READ)
6 @* Q2 s& e. [6 T& [/ R// 保存屏幕截图; ]6 q2 E* |& k: n
saveScreenshot(buffer.getMappedRange())3 z0 r8 q+ @3 o, C% G7 N
// 解映射
7 X' s% N$ o$ kbuffer.unmap()
0 g* G( `4 b$ ^; L. n</code></pre>
: u* S$ a" Q7 J$ M! }9 ` d
6 X7 v7 L7 v% E# | C6 q1 m |
|