飞雪团队

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

WebGPU 中的缓冲映射机制

[复制链接]

7407

主题

7495

帖子

2万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
24551
发表于 2022-2-12 14:35:42 | 显示全部楼层 |阅读模式
9 ^) O, c- ~! @
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>
' H# m% i2 T: s9 n<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>* k. u1 `7 |. m+ z
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>- D7 w' D) j5 @& K; |, s+ L
<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>  j+ A) Q7 k9 l4 G7 S" e
<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>. H. B; i9 P% ?8 b+ a& ]' Z6 y  D
<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ }), ?7 ]9 z3 ]! C' `' T4 B( U
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节
) q( t& d4 i" j8 q" ^, Y) p" M# |! S
// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作  c" o# V+ M( {1 H% z
</code></pre>
' h! S8 c; D$ Y& i# }<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>9 Y* ~  O* p, L1 M2 f+ W& [
<pre><code class="language-js">somebuffer.unmap()
" E! ?( r' B$ N7 Q0 _</code></pre>3 v- v* L5 J- x4 U! W2 N0 s! k) l
<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>
0 v8 R* t7 z3 ~<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>
2 x/ m+ u7 O/ r, X<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>( M3 `" Z+ b) I, v* A" d) t
<h2 id="创建时映射">创建时映射</h2>( J* x! F) R: [" M* k5 {5 t* F; N
<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>
* x; Q7 \+ I' f8 V4 A2 U% `! K5 L<pre><code class="language-js">const buffer = device.createBuffer({2 @" e7 a! J1 i+ V: T- g- a" d3 m
  usage: GPUBufferUsage.UNIFORM,) o, v; X% K! K+ C$ U
  size: 256,! X; E4 h) d9 a6 n
  mappedAtCreation: true,6 k! K/ k3 {0 W. r2 |& }. |
})- ~& ~1 ?  n+ J. |$ a
// 然后马上就可以获取映射后的 ArrayBuffer4 T# v+ o* H4 D0 X. I. V) }
const mappedArrayBuffer = buffer.getMappedRange()
: W6 `& h3 \, U% A  k8 S
3 c# l6 ]9 u. R2 o3 B/* 在这里执行一些写入操作 */* M5 E7 O/ V. K$ t+ q5 K) {/ r
3 y" g/ @2 L1 A# Q
// 解映射,还管理权给 GPU
9 o- c9 m/ v& v# M. h8 ^% Obuffer.unmap(): s; Q7 a" T% N0 H- y, k! S" J9 q
</code></pre>
' z' P/ g6 m+ V: Y<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>0 Y/ F- t5 J! u( I7 a* b
<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>
0 N6 w: J$ u+ k<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>
4 u" |4 b- S" X% o<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>
$ m/ y$ w- M( D  \0 c7 i<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>% ]6 l$ T/ D6 d# u3 \& s6 b
<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>4 c( ^* k/ v5 g  }$ j
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>
* V* A. r  }: c) L+ d* E<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>8 h4 p! a; K6 T2 i$ b
<pre><code class="language-js">const texture = getTheRenderedTexture(); L( g# D, T) ?/ V  [% M5 R
! I$ ?% C/ \0 M
const readbackBuffer = device.createBuffer({' i' H8 ?. ?# K# N0 T
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
7 S' k  D5 v6 o  size: 4 * textureWidth * textureHeight,& ~4 y8 f. ^( B4 T0 m3 w' @
})
9 W, G2 I* {; C) `9 D
' O# M$ w& ]$ ^' C5 G* Y// 使用指令编码器将纹理拷贝到 GPUBuffer
, w. W+ T) l% `4 `4 g  @2 I5 Jconst encoder = device.createCommandEncoder()
) s) m$ f# r( V- r$ _, Qencoder.copyTextureToBuffer(
/ s$ F  J5 w2 f) [; b+ J  { texture },
+ m- l8 U  R- t9 M0 W  { buffer, rowPitch: textureWidth * 4 },: E9 J  z& A6 M# @0 ]
  [textureWidth, textureHeight],
7 S4 V5 Q0 n# W( W- [)1 E1 U- ^4 I) `( [7 J
device.submit([encoder.finish()])6 o+ J0 @3 h/ O  k) ~/ L
5 ^' n8 V: G" B6 p# E+ h1 c% l
// 映射,令 CPU 端的内存可以访问到数据+ r2 u" t$ m. L, X* {7 M
await buffer.mapAsync(GPUMapMode.READ)
: `7 @- d4 b  A. A9 p// 保存屏幕截图0 |- {6 {! M2 f3 X
saveScreenshot(buffer.getMappedRange())
! d% E! k, Y( G9 o5 @0 h! D& l* h// 解映射
% d1 Q  H# d) J. U2 S( Z; u. N/ M3 ibuffer.unmap()9 q8 r* Z. _3 `2 j9 ?3 i  O( t6 t
</code></pre>
$ r/ h( c+ d; h* t) F# A8 j8 s" [1 a0 F( Y& |. x+ t
回复

使用道具 举报

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

本版积分规则

手机版|飞雪团队

GMT+8, 2025-8-31 01:48 , Processed in 0.059521 second(s), 21 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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