飞雪团队

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 13583|回复: 0

WebGPU 中的缓冲映射机制

[复制链接]

7726

主题

7814

帖子

2万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
25508
发表于 2022-2-12 14:35:42 | 显示全部楼层 |阅读模式
! {5 O2 P+ M; _7 l' T) [
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>- d( c$ `5 D! J! Z
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>" z* |+ B1 `+ z5 ]# h& e: l
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>* L* J- W8 w) d; {
<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>
; G+ E) p" s' t, s1 m& t<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>
1 P9 Q+ M1 F* T; s) Z1 l1 h3 I<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })
; y7 ~9 K, u! ?8 `; o! mawait someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节
$ d, G* E3 b5 b' m0 a/ Q' a1 m8 L. h. H3 M! i' o- I- {/ D
// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作8 v. P# }$ V7 N) b! o2 F
</code></pre>2 i7 ~2 w( f! i: [: {5 ~* M0 P6 G
<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>
7 ~7 ~4 ]( {2 c$ E$ H9 G<pre><code class="language-js">somebuffer.unmap()
1 ~3 n* T2 Q- E, z% t* g& b</code></pre>% R  n+ W- O) _; k$ W" ~
<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>
6 m$ h5 @5 j' b) T% _7 @6 y<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>6 b) G' J6 P/ R2 Y' A! L
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>
$ n2 R, R  X$ a7 Q<h2 id="创建时映射">创建时映射</h2>
6 A8 R' Y- [$ H6 f<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>
  `5 l( ^" P5 z$ J; I<pre><code class="language-js">const buffer = device.createBuffer({" ?+ m: v9 k% W0 m4 L) T
  usage: GPUBufferUsage.UNIFORM,- {) m( {$ `; ^$ A4 E' u
  size: 256,
$ x& B3 L( U: N( w. |  mappedAtCreation: true,3 F8 u* x+ t/ L  L
}), V/ O2 O0 B% \
// 然后马上就可以获取映射后的 ArrayBuffer
$ I; K: F( L' t' E* Yconst mappedArrayBuffer = buffer.getMappedRange()& v7 q; N+ \9 t; l7 f: F8 l  h/ [

' O* K) E" E  H1 \/* 在这里执行一些写入操作 */& Y& Z* q* V7 B, o4 r  \2 j8 t
1 W& [6 P' \# a. ^# O
// 解映射,还管理权给 GPU: g8 R5 c1 H& e* o
buffer.unmap()/ \9 c6 y+ \2 F& s8 W% z
</code></pre>
+ ^0 `# U8 E/ S0 I  ?<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>
) a! r) P2 z. z  g* B; Q<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>
9 j! D9 l( i) l7 r4 _$ e5 K& L<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>7 f- a" h1 c: }0 u
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>; Y5 _( j& {  w+ U: Z: h( O- X
<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
' ^: N3 N6 E1 k! e0 C4 k, [/ _<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>
4 R/ W, @, j# y9 B6 a4 q0 P2 P& B<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>3 g0 C- L; z$ t& p$ u& j( p
<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>* [7 p& y- e4 n# f0 N( ~4 D8 c$ g- I
<pre><code class="language-js">const texture = getTheRenderedTexture()
) `8 o5 Q7 P' o: I; i
" \4 Y* R# F3 x- [$ Aconst readbackBuffer = device.createBuffer({
2 ^( l) t8 Q' O! K1 D  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
4 `, H3 s. d# ]  R8 `$ J7 Q/ Z  size: 4 * textureWidth * textureHeight,3 W+ C: E) N2 U$ Y4 g3 }
})
$ [- w3 s* B6 R( q8 V/ I7 L" j8 T/ k. m  o
// 使用指令编码器将纹理拷贝到 GPUBuffer
8 i% p7 T6 j" h8 H) a% dconst encoder = device.createCommandEncoder()  e+ o7 [' @/ Q6 u
encoder.copyTextureToBuffer(* b/ j. u2 Y7 _; p# C2 y
  { texture },
- K* l- j( B/ n& ?: V/ t  { buffer, rowPitch: textureWidth * 4 },
+ Q7 v7 i  f/ ]1 m. H  [textureWidth, textureHeight],
  E  `/ Q+ s- z)/ b( J% @, N5 X9 g  v4 M. J
device.submit([encoder.finish()])
2 D- O+ Z6 e" ^2 P2 H: `* |" W9 n3 Q3 @$ M8 B8 C' i8 P1 v
// 映射,令 CPU 端的内存可以访问到数据( P. g# V1 V! D8 R
await buffer.mapAsync(GPUMapMode.READ)9 C, s/ _* d: e% @
// 保存屏幕截图0 s9 ]+ r* y1 ?& e  M0 r1 x; K
saveScreenshot(buffer.getMappedRange())
  ]& v5 o" J0 L8 \1 e, z// 解映射
) K: j8 E# @( E8 Ebuffer.unmap()
6 U9 b3 V7 l* b, T5 a0 ]6 d</code></pre>
/ |! y5 ^" {5 d3 U, n3 |. m% y3 S; t
! w4 P; [" A  a2 k% N+ q
回复

使用道具 举报

懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|飞雪团队

GMT+8, 2025-11-1 06:14 , Processed in 0.069637 second(s), 22 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表