飞雪团队

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

WebGPU 中的缓冲映射机制

[复制链接]

8292

主题

8380

帖子

2万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
27206
发表于 2022-2-12 14:35:42 | 显示全部楼层 |阅读模式
( g  l# a: [9 e, n# Q% }& [
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>+ V/ v0 _6 P4 e. v+ P  d
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>8 H- T+ Y7 d! Y5 E+ y
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>" ~+ X- l1 d) h& A
<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>
- i5 s+ t: q+ y, d, {; k<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>2 _( ~3 o) ~5 \4 i! N
<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })
- d6 a1 g% y8 \2 tawait someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节+ v$ S! n9 P. u5 X8 O( C& ]
4 Z# n% U/ I8 z/ g. F
// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作, |0 U- K) j; H/ T; Z/ H+ R8 |
</code></pre>
7 p) j: }! j8 G( _, l<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>6 W/ \7 ?# D& m1 M! P
<pre><code class="language-js">somebuffer.unmap()
0 ^+ w& H1 m2 V+ s$ l3 @</code></pre>
' Q& V+ H0 s. ]3 @9 B; |<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>
1 }8 @* Q6 W# I/ g4 ~8 m* p5 K<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>
9 `! R1 A& J( Z5 U' E+ X<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>
4 c9 r9 x$ X0 w) t! P* P<h2 id="创建时映射">创建时映射</h2>: a, k0 \! e" X6 X, C0 ]+ Q. v& ^
<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>
" o- X: u7 S# {! u- P! Z<pre><code class="language-js">const buffer = device.createBuffer({
5 ]3 q4 f. {& j- K3 x  T  usage: GPUBufferUsage.UNIFORM,
9 ]+ R+ }4 E1 e8 e& x  size: 256,
3 o5 v8 J0 i* `( j  P# B8 }; i" {$ ^& z  mappedAtCreation: true,
9 |( N0 }4 k) c1 E})6 S! P2 ]% p( R/ B- Y
// 然后马上就可以获取映射后的 ArrayBuffer
3 W( n4 V3 `+ |6 ^9 `+ Aconst mappedArrayBuffer = buffer.getMappedRange()
3 m0 b9 j* [  o6 {
4 Y4 h( p  v8 d- }0 \" f) @/* 在这里执行一些写入操作 */
6 B' F) I( I5 r6 b4 [( a5 t
$ b( T  t7 q' S1 _1 W// 解映射,还管理权给 GPU
% J7 P+ u) m# u1 V; V( \1 kbuffer.unmap()
( L# E& L2 i. O: M1 q</code></pre>. `. v" M3 J4 V( j& b
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>
$ U0 {. @0 r( V( o2 s$ G<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>  A5 Y, J8 K' K. l$ A, u& l
<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>) x1 n6 F: v3 q) i8 B" E7 a
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>) P5 `6 Z: V5 E0 b# q
<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>7 A( d+ Q) }* S: {) }1 r$ Z$ l- Z
<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>4 P; C' R  C9 j
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>, B9 v2 _  H) E1 J; Q0 \9 |
<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>9 v7 |6 u5 `( \7 ^" t5 R
<pre><code class="language-js">const texture = getTheRenderedTexture()
9 ~4 `( }  L* e0 z3 d- k% y9 |/ l
1 n  i  s; i: j: ~8 Sconst readbackBuffer = device.createBuffer({+ k$ Z! J( k! b) Q
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,4 v$ B; j1 K3 O; L, F( m$ _* q' e* n
  size: 4 * textureWidth * textureHeight,
! L8 m( Q. ~+ [# v  ~})+ c* n7 ?$ q+ i& b# a0 M

$ J" ~/ o3 Q1 O  n// 使用指令编码器将纹理拷贝到 GPUBuffer1 O! }  A6 f6 A! Y1 E, k
const encoder = device.createCommandEncoder()
( h+ c* o* B0 Kencoder.copyTextureToBuffer(
) R# W7 u5 r5 O) s* T. A  { texture },
" E; X! K( n' I  { buffer, rowPitch: textureWidth * 4 },4 W; F, n  \) s9 J0 ]& r6 j
  [textureWidth, textureHeight],% H7 d5 f  E6 b5 Z9 ~) ?9 a1 r5 T
)/ B; N* z& g) x% \* K% N
device.submit([encoder.finish()])4 [2 p& ^5 R, b8 ~/ @! Y

0 B4 s( T) x4 @+ i4 V5 E; H// 映射,令 CPU 端的内存可以访问到数据( H* Y# }( I# C9 l, ~" g8 x8 n
await buffer.mapAsync(GPUMapMode.READ)
( V: j3 _- j  l& L; s// 保存屏幕截图0 f. D7 W$ c$ W
saveScreenshot(buffer.getMappedRange())
2 l, z( _" l3 i/ Q$ K// 解映射0 K' l% ?  m/ v
buffer.unmap(); Q5 S9 y% S: y: g; u: Y3 C
</code></pre>. w+ a: s2 U- w, H8 Y) Y, s

7 i# T( e# @. m$ A; X* ^& R
回复

使用道具 举报

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

本版积分规则

手机版|飞雪团队

GMT+8, 2026-2-28 14:41 , Processed in 0.344793 second(s), 21 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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