|
$ I% S' Z% u$ R! I$ z5 g
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>6 g* f: v5 ]# \( E9 q
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>
, m9 p, k* B b/ U<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>0 J- ^/ o- f3 K2 K
<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>
2 x" p; E+ Q0 E$ r- H5 E<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>" m# s1 Q+ a6 [1 O2 ?7 [
<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })
- |/ n3 i& R8 tawait someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节
! v7 H5 {* D2 F/ S
; u$ y! E& S6 ~9 w// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作
8 `$ m2 n; n9 o</code></pre>4 {3 Q9 \0 f- x9 n0 d: z( `, f
<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>% y* e% N/ Y( d& Q
<pre><code class="language-js">somebuffer.unmap()" _6 R8 x/ W& V
</code></pre>4 w% C2 R V d& L" \* u
<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>
) ?. ?/ F) X6 q# f1 C+ [<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>* j5 U% m- [ M; N+ G" P0 n
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>
, X3 a3 G$ s+ e6 q) s) s1 e<h2 id="创建时映射">创建时映射</h2>
. Z8 \0 U5 c! v' \/ w<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>
4 p! c' S6 i; @" t( X, [<pre><code class="language-js">const buffer = device.createBuffer({& e& A2 [2 b' a- D' O3 R2 g
usage: GPUBufferUsage.UNIFORM,/ H- `$ @( h( y! ?, X
size: 256,
+ L( j4 z8 L( p' G/ @ mappedAtCreation: true,
4 ]7 R) B$ G1 @" l/ g})
: U( G6 u9 m3 Q/ f0 ?4 I. T8 z1 L: V// 然后马上就可以获取映射后的 ArrayBuffer1 X0 |+ P* j0 G
const mappedArrayBuffer = buffer.getMappedRange()
% Z, W$ t7 F# y, U2 H3 O( ~; H1 _7 J! U9 N+ _; g: @+ h7 @4 |# u S( Q
/* 在这里执行一些写入操作 */' F4 p. c$ v6 W7 T3 _( i
; u) a2 f1 c3 i+ M4 ~
// 解映射,还管理权给 GPU+ V) l/ s$ k% \0 V
buffer.unmap()0 I( |( X. F9 E
</code></pre>7 m1 C; G) n' j. }# R
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>! T1 {& [" N& Z
<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>
: k" M( `; A; A: i. L j6 {<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>
- q" T& p# H7 m3 W' G7 c<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>
5 e" Z$ ]1 ]+ ?) u) z<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>, N; j" k0 g) x) P. x% ?" ?6 v
<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>5 Y1 L# \9 x; r/ S7 Y% J
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>% p) x: F1 z& q1 X
<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>
; T. c; X3 }7 [* _<pre><code class="language-js">const texture = getTheRenderedTexture()
1 V9 y3 u, F6 W2 g1 s3 i7 U8 K! F7 O' l, a, p
const readbackBuffer = device.createBuffer({. C% g0 P1 b; q
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,: f+ D7 ^" w: x9 K, X& e3 Y
size: 4 * textureWidth * textureHeight,
/ t+ d) P8 ?0 U$ \" d, j) T})
: z8 {6 |2 M5 P% i
) ~9 R3 T9 |6 o [// 使用指令编码器将纹理拷贝到 GPUBuffer+ D* O: Q' |5 f5 X9 G
const encoder = device.createCommandEncoder()6 e& H5 I- ?# G5 s
encoder.copyTextureToBuffer(
; a I* C2 ?; \" M7 U9 e { texture },. K' Z' m9 u1 z( p. I( ]
{ buffer, rowPitch: textureWidth * 4 },
7 \/ g' D8 k5 a8 j* ` [textureWidth, textureHeight],
+ V1 u! S1 b! h6 O7 V( |! H! U- n)
( M2 v3 h% c' u! B) Q% y9 ^device.submit([encoder.finish()])
1 y1 j6 B T% A; M& b+ J! r( B- b% n: w s
// 映射,令 CPU 端的内存可以访问到数据8 P3 n" D8 {. U5 I( _
await buffer.mapAsync(GPUMapMode.READ)
' U. s2 D$ W5 p f5 U% |$ w0 h2 d// 保存屏幕截图
* D9 k) \" B' isaveScreenshot(buffer.getMappedRange())+ i, A# l; h8 v7 f1 m; W% t
// 解映射
: U7 z; x; p- X& o9 r4 f- Xbuffer.unmap(): C }% d$ P/ M' v
</code></pre>
$ ^4 z6 M$ i( }" b# I% j1 t
+ t$ d' A Y% X |
|