|
2 M5 i6 y! h% m<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>
- T% b) A- e. B1 @% E3 J+ M<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>+ p1 L6 j" @* \# W8 F* {
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>
$ l! Y- _% P/ g: r0 q<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>; f6 m. M) a0 X3 n$ @
<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>) N, d* G% w0 n$ Z/ L
<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })
( v; V/ n2 B/ [await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节6 M1 G2 r* ` S$ L7 [7 ?! Y
- _; {2 A' k' [) A4 e, l// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作
3 s) G' h- A: m% y! Y9 j2 V</code></pre>
* W; j6 X7 z5 C7 a, G/ O7 q<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>. @. l! _2 Y l, v8 I
<pre><code class="language-js">somebuffer.unmap()0 E0 _' I8 ?* L, Q% j% r7 P4 E
</code></pre>( \& @' D; y; w
<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>% V" c% G" p# q& R, ?8 l
<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>0 q7 \; Z- v& y
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>% Q) |; o3 h* V& I3 ?
<h2 id="创建时映射">创建时映射</h2>
7 y7 O |/ w7 a. R<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>2 `+ d' T, o# p7 r0 I: V% }7 J. m
<pre><code class="language-js">const buffer = device.createBuffer({/ c7 B7 `8 }8 W5 p3 T {( f/ K" O6 f
usage: GPUBufferUsage.UNIFORM,
6 g( s/ ~3 L5 v7 {8 {4 v size: 256,
( L t/ v; @9 i' L( R+ v$ a6 U0 C mappedAtCreation: true,
. V4 K& m) T8 d/ l})
* v+ K5 @3 a' q l8 Z' Y" p. Q// 然后马上就可以获取映射后的 ArrayBuffer
- t2 O7 W; G: U% K; `/ Oconst mappedArrayBuffer = buffer.getMappedRange()& S. ?2 \4 j' r
" S5 e( s: {. k9 M
/* 在这里执行一些写入操作 */+ d7 O, t$ l$ P4 P* L
* `9 f0 s) q9 k g* q$ L; m. v// 解映射,还管理权给 GPU
! {0 K. i0 H6 W; B8 Z) m; lbuffer.unmap()
: f, S% C! E* I# u</code></pre>6 E) k# V$ ^$ C- r9 G
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>
$ p( L! u# R* N# ~3 E$ O1 P<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>
5 e5 ^3 d( Z& k# ^1 Y% [<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>
; q7 M1 E2 J+ s- T. A5 ?6 r& K# d<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>" d, a6 s1 r$ H5 ^( U
<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>, `3 A: L! w, d( X9 |
<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>1 B3 Y' q+ B: _4 P
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p># h, c( j2 y n4 Q
<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>1 ?/ E! K/ M) }3 R
<pre><code class="language-js">const texture = getTheRenderedTexture()
8 O3 E" F4 W( I
0 d# S' P$ e9 `6 @const readbackBuffer = device.createBuffer({! S4 b, F9 P- C7 T% u: B0 s. g
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
' e0 M- L1 I! t+ k' D size: 4 * textureWidth * textureHeight,
+ T4 i' x3 J7 F1 b) i+ E0 B})9 f6 m; f H& P1 l% ]5 v; b' B
4 C) m% ^6 x5 j6 t5 Y8 R) E0 Y
// 使用指令编码器将纹理拷贝到 GPUBuffer1 z* B- b A6 H" z# Z
const encoder = device.createCommandEncoder()& G) e' ~$ M* C. L" W6 |2 \# N0 R
encoder.copyTextureToBuffer(
" n$ t- w0 o1 g' v3 e& d' s- F { texture },
! y! R* T! U( H: o { buffer, rowPitch: textureWidth * 4 },
; j$ n- r) {8 ]. f) `5 }, S [textureWidth, textureHeight],# y% ]0 A$ x T* C( |# U6 j
)/ B8 W- `' \3 ]0 O: U8 S# s
device.submit([encoder.finish()])
3 p( A. ]& P0 v3 f; |! y6 `. u; j& [, Q0 L6 \ ^9 ]
// 映射,令 CPU 端的内存可以访问到数据* u4 g2 @7 x2 i! @- W$ ]
await buffer.mapAsync(GPUMapMode.READ)
; \% f I9 f9 v; D4 l// 保存屏幕截图
; J# E9 C3 ? g- I+ P2 o2 FsaveScreenshot(buffer.getMappedRange())
& b& t$ ~$ U3 J// 解映射
+ l: W! m8 L( \$ Sbuffer.unmap(): }% j( U, i1 m" k. J
</code></pre>
{/ C: v# f# R Y8 g, n1 E% ~( z$ u
|
|