|
|
% E: k$ u0 L9 H* ?* W+ A4 w
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>
" V% A# Y' i( N' G* [- e J<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>1 B. v2 U3 ~4 r% z
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>
7 z7 c- r% I3 Y<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>
5 h3 s7 q+ [3 P<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>$ O$ E# _0 v9 M2 n
<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })& l! d% d0 x- z5 \7 C
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节, q+ d2 i$ A& [" ^
. ?- A+ w) U6 O/ ?// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作+ T# t- q0 B. J/ {! |# n$ j+ K: z7 h
</code></pre>" U4 O4 h! z" P' X x2 C: C
<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>
- j$ W" e: ?5 H<pre><code class="language-js">somebuffer.unmap()( a! J, I/ m$ l1 |6 P
</code></pre>% i9 k1 u1 y* A
<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>/ d* U$ J/ J. z# x4 @- R0 C' }/ N
<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>
+ W: L! s( z0 w9 T" v% V<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>
% x0 Y, k& W: ~8 l<h2 id="创建时映射">创建时映射</h2>
) _1 F* F' ^* h# Q1 Y9 T<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>
- L! t4 @5 r$ {2 \<pre><code class="language-js">const buffer = device.createBuffer({5 I0 B: o. T+ E) S) k
usage: GPUBufferUsage.UNIFORM,
8 q4 M: n9 }' O9 A* k, i) J size: 256,
7 ^2 e- t2 \$ [8 m) }6 H mappedAtCreation: true,6 h7 c% R) l* U1 m
})+ h# m$ M* k* {8 `( u+ K
// 然后马上就可以获取映射后的 ArrayBuffer
, a7 d" C* K1 G/ ?/ tconst mappedArrayBuffer = buffer.getMappedRange()4 D, Y' K8 b; W; Z8 e0 @' N
6 }: z* q' n" r# G* j$ d
/* 在这里执行一些写入操作 */
" r" Q0 {* ]$ U8 Y* }& c0 |$ [' j0 T! X! v, _
// 解映射,还管理权给 GPU
9 @ ^- ~. h G1 i9 ]* pbuffer.unmap()
5 _! Q" i1 B5 w: q! D$ [</code></pre>! e9 k- R3 U; i, ^& e
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>' h* t; ]' z" ~2 `0 U
<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>) k4 [, ?. H ` t d
<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>
" s2 M" m' A; Z; x W o K<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>
( {/ M1 p4 @7 b. ?2 f<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
! l9 B+ u7 }7 s& ~. E& v<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>
' i% s5 Y8 I7 I<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>
r8 b2 V. V& p+ |! A6 ~<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>9 |% R3 k* b- |
<pre><code class="language-js">const texture = getTheRenderedTexture()+ P2 t4 k% o0 }% N; e( H& k
9 X5 D) e6 C( o; c
const readbackBuffer = device.createBuffer({* X; |4 ^7 n* y7 }0 \& E
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
7 l$ Z A8 K# G. T size: 4 * textureWidth * textureHeight,
+ V8 G7 T0 W3 j m! [, A% H})& z V( T# Z: S5 B8 o
: e" F" ]" D( Q' _* G
// 使用指令编码器将纹理拷贝到 GPUBuffer4 ]7 \! b$ I6 u, q' ^* z7 L) G
const encoder = device.createCommandEncoder()
. E& ~% e; S6 O" M( \encoder.copyTextureToBuffer(: d" J" v4 T. J' x" E7 v
{ texture },
( l6 j5 q: L4 u# o { buffer, rowPitch: textureWidth * 4 },
/ F/ f! X* L# e [textureWidth, textureHeight],* ~3 {7 P5 h# B8 L
)' I( \ P4 S4 b- l. ?+ A2 b
device.submit([encoder.finish()])
% h( L8 O8 ^8 J- m7 \% s, A" n3 p d# d. {$ [
// 映射,令 CPU 端的内存可以访问到数据
$ u H+ \2 v2 ?0 {4 Wawait buffer.mapAsync(GPUMapMode.READ)
/ m0 n% M |5 n8 P. D! g. \" N: m// 保存屏幕截图
3 U. F% y# ?! P; J% h3 CsaveScreenshot(buffer.getMappedRange()): _ h6 Y% _( q5 E
// 解映射# i; f" d6 P4 L8 a2 }# Y+ t0 Y
buffer.unmap()1 ?, ^+ [! \. q6 a. p7 o
</code></pre>6 x( }8 b- o8 w8 ~2 x" t5 r- @8 y/ O& j
: l& f9 B7 v% u" @1 E0 x |
|