|
|
/ S# j1 z/ W" A/ C
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>7 ?2 X- n X7 T
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>
0 Z+ L% |6 x4 q3 L3 u9 n<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>4 A/ J, v- x) }$ R# z* `
<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>
0 H/ [4 e8 k$ x5 e<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>" j2 z$ V2 R* u3 n1 ?
<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })4 u. x1 m1 h' e7 Y+ C6 F
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节
5 Z: x, Q2 S1 L4 V
7 d8 S" U' l% c3 ]// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作 w+ [2 `. ?& q2 i5 v% X, |4 H
</code></pre>
. j: G+ A" L$ F- {; I<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>* d& |" k8 z6 C
<pre><code class="language-js">somebuffer.unmap()
4 \2 h- V% W3 x# g</code></pre>" M5 N7 }7 V+ K k% I3 a5 r
<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>
6 B. Y9 o5 f* X. c2 W H9 v% n<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>; Y/ D! Z+ ?% |! B9 n
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>
, V( r q, t6 @/ @2 A: d) y<h2 id="创建时映射">创建时映射</h2>
7 {: X9 p$ i+ d+ v# ~0 _<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>
1 r1 e: r0 |6 b7 X5 E% o- x<pre><code class="language-js">const buffer = device.createBuffer({
( m9 O$ q7 q7 z# h" S* j4 V" P2 ? usage: GPUBufferUsage.UNIFORM,+ C) z* W/ {" B& O. x$ F7 c
size: 256,
; ~; i- _! h) c) t1 Q mappedAtCreation: true,
: G8 ^' H: e! I+ [})4 q5 E2 I/ }/ E' X+ l
// 然后马上就可以获取映射后的 ArrayBuffer
, g+ Z0 ?! e) e5 t& t# S8 _const mappedArrayBuffer = buffer.getMappedRange()
2 T- O% Y, y( _8 A
. h. w) l. U7 _4 b3 H/* 在这里执行一些写入操作 */
A% J- |9 e' q
: c# @5 K( H" i% ~// 解映射,还管理权给 GPU
, x: w* O( U: [buffer.unmap()% l8 g: e; q* j f% B8 A
</code></pre>3 ]( l. g: e5 O
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>; X) c0 Y* L& n! O5 ], D
<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>0 G+ B& u% E, [6 c6 x+ Y: H7 d
<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>
" Z, a! T# g% q+ ~' A<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>8 h# b6 t5 P9 i$ {6 V* P
<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
9 u" q* C" f' K, ]; G" {! ~2 V) N<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>
- g- i/ Y5 }; A5 \( ]; K4 R<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>
7 `" j. h1 }: k4 @8 G<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>* C5 q. ]* z/ f$ H
<pre><code class="language-js">const texture = getTheRenderedTexture()' M8 J: m" O7 l( | i
6 ?, z+ m5 j2 Bconst readbackBuffer = device.createBuffer({% x; f2 c8 L; @7 M; t7 o
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
n: L; f! R. y8 \4 ?* l size: 4 * textureWidth * textureHeight,
. { X( ~/ s4 m})" C) h' c3 W+ M+ t2 z
. Y+ T: E7 |' r// 使用指令编码器将纹理拷贝到 GPUBuffer' E1 R4 h- ~! X/ Y
const encoder = device.createCommandEncoder()& k( u; d" r$ e" Q/ t
encoder.copyTextureToBuffer(
$ J" U" L/ M5 E# G7 I( a5 j+ H z) ~ { texture },' b9 B. E! X6 ]: |
{ buffer, rowPitch: textureWidth * 4 }, W% B; V6 P) ]3 @4 r
[textureWidth, textureHeight],
7 y( A P2 Y5 O& l)
* Z5 F# i- Y( g$ z& P* s" ?2 C) ldevice.submit([encoder.finish()])1 E$ C1 a8 l! p
# w( ]/ L) n/ P) y
// 映射,令 CPU 端的内存可以访问到数据
; x: y$ _8 v1 h, ?1 `) A+ j7 J! ~( M; i4 wawait buffer.mapAsync(GPUMapMode.READ)- r+ k w( z" V& a: h
// 保存屏幕截图
( z! C' w& n; o" V7 P( gsaveScreenshot(buffer.getMappedRange())
* k0 d, {; J) \" I0 _1 I5 w. @// 解映射
1 t: t7 [' k# O* c% Rbuffer.unmap()& X. w: A6 U; @. |; [
</code></pre>
# D+ k, U$ x3 i' ], `9 Z) D0 f9 H1 @4 w) K9 {9 \: M& e
|
|