飞雪团队

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

WebGPU 中的缓冲映射机制

[复制链接]

7788

主题

7876

帖子

2万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
25694
发表于 2022-2-12 14:35:42 | 显示全部楼层 |阅读模式
9 J8 o" y2 i9 a# N: ^
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>
0 `. A- M& R. q5 E<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>' W# B( U& [3 y% J
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>
9 h& J& E+ F* @5 V9 R; z<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>
: C$ M. P6 Q+ N: @: k. [! I" [. a  ?<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>
9 Q1 t5 h2 f" ?0 p# T. a<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })
' I6 n8 c" [/ Z& Hawait someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节0 |" F7 }8 Q5 W0 W; K" `5 l# R
! \7 @2 x) r: \
// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作* O1 x  k& z, l+ E% [/ n( q
</code></pre>" x/ `$ k4 Q: A' j" W
<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>
( Y$ e- e3 m3 Q# u. ]<pre><code class="language-js">somebuffer.unmap()3 u9 G  A6 D6 t' u% E  e
</code></pre>
! ?4 X6 c; C; ]8 a" C<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>! s* Z7 R. i4 @2 y8 {. F
<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>
( s* n; Z; v, ^<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>
2 e" |& q7 K6 P) Q$ P5 e<h2 id="创建时映射">创建时映射</h2>$ t! O/ K( b( ?: [3 _9 F
<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>8 r0 F" i1 \( z# C' C
<pre><code class="language-js">const buffer = device.createBuffer({
. m$ P5 @- b. [% B$ F: u  usage: GPUBufferUsage.UNIFORM,2 V& V0 B8 V' m8 }* l0 T/ \, a
  size: 256,
, z- D0 O4 j3 M& S3 [+ U  mappedAtCreation: true,5 {; E/ }  C2 U4 R6 g' C
})
2 D0 e2 C( M) v0 J// 然后马上就可以获取映射后的 ArrayBuffer1 \6 Y: p/ A9 J  B
const mappedArrayBuffer = buffer.getMappedRange()3 l' d; E/ B$ h: I, E! q' F0 }

  B1 C/ M8 h' l/* 在这里执行一些写入操作 */
! q: m) `8 W/ d
4 b7 `# O- `3 _" F5 w7 I// 解映射,还管理权给 GPU
! U' |- i( r0 v. s  `) _buffer.unmap()( B) u/ J+ S( S
</code></pre>
* l; L$ F8 T1 y! g7 p. v1 m<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1># o5 x! p2 r; Z; ]% j) ?6 L
<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>% I' l0 c8 [! S6 w5 c
<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>  X( q9 Z5 K7 ?
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>) p" Z# w. S' P/ Q1 R
<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>5 j/ I8 I3 S  \4 v8 M( V  g- B
<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>' X$ ?8 ~# y4 X3 q% z
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>
) J3 v+ O& Y4 @( g<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>
* O7 z& D+ ?, p  V<pre><code class="language-js">const texture = getTheRenderedTexture()4 \; f: k8 E) D. D5 y- H- i1 L$ _
. Z  ^, n" U9 I( A2 q+ c& s* O
const readbackBuffer = device.createBuffer({
) J& Q4 X  I5 _: l  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,# ?" g) g8 V) H  r/ Q& r. N& e6 b
  size: 4 * textureWidth * textureHeight,  q* M/ W2 s9 p, u' U
})
9 D9 p: P" v# y$ v% I) I- V# v2 k) C( ]3 k0 c
// 使用指令编码器将纹理拷贝到 GPUBuffer
1 B0 @$ d6 G$ r: P7 uconst encoder = device.createCommandEncoder()
$ B9 r2 r: D" Hencoder.copyTextureToBuffer(
1 q1 p# I* J4 [/ w+ D  { texture },
, a* I. L8 V$ o1 E  { buffer, rowPitch: textureWidth * 4 },7 D1 ~- M- _! q; H3 j
  [textureWidth, textureHeight],6 A" U  P/ V2 F& s$ h5 i2 Z
)/ d7 w: D2 z. @% [7 k( M
device.submit([encoder.finish()])
' ]) E" V4 G% I  E0 A+ l- w/ z6 J) m" o0 I
// 映射,令 CPU 端的内存可以访问到数据
4 `9 t2 q1 `, i) G. u' G1 E- |7 z( Nawait buffer.mapAsync(GPUMapMode.READ)
9 M$ m% u& d3 Q8 _* E1 ?' P/ W// 保存屏幕截图
8 C9 r5 j3 G/ _3 ]+ X6 N9 HsaveScreenshot(buffer.getMappedRange())1 d9 l! j7 g& n: C& ^+ p
// 解映射6 P9 ?( S9 Z5 X. G
buffer.unmap()) `: N; K- s' D9 `
</code></pre>+ }" V) W) u; H5 L7 E& v

, o( z" E% C$ g0 I" f
回复

使用道具 举报

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

本版积分规则

手机版|飞雪团队

GMT+8, 2025-11-6 18:49 , Processed in 0.067912 second(s), 21 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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