飞雪团队

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

WebGPU 中的缓冲映射机制

[复制链接]

4137

主题

4225

帖子

1万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
14711
发表于 2022-2-12 14:35:42 | 显示全部楼层 |阅读模式
8 J' z# L, j$ G9 N4 J& [; \: L
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>
* S: k  J# c; I/ M' i7 |4 F. ~<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>- O- J2 m0 c0 s0 P9 n
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>
9 m5 p+ ?* O: c<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>- y5 z$ f6 I# Z% m
<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>1 {* N% w% k  E7 {7 E4 d
<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ }); K2 E: }! K( i1 }* v1 `
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节
8 a/ v3 u7 c% F
7 c) l& {& a8 B, D- q( U6 @// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作
: m, G# Q% g. }4 X6 j</code></pre>
2 e, V9 t0 B+ M; t<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>
5 {1 K3 ^; U" W# m0 o; B  c, U; l3 A<pre><code class="language-js">somebuffer.unmap()5 i! e# m+ u+ g. [, a# p9 V# e
</code></pre>
7 z& \7 B. P& e5 Z% }  U6 [8 g<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p># S* ~& g  t; l* J1 H( m+ B
<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>/ M9 _4 P5 {$ N; \- \
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>* Q' h, _1 T3 [: \+ L6 s% i
<h2 id="创建时映射">创建时映射</h2>3 g* c" N& P( Y- z
<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>. L  p! {, F* D, P4 I9 d. I& y
<pre><code class="language-js">const buffer = device.createBuffer({) l+ X7 W! ]/ g; A' P
  usage: GPUBufferUsage.UNIFORM,
# j. Y' P" j7 G& ~; X+ W; P  size: 256,3 \; E2 \: T* R5 z  q
  mappedAtCreation: true,
, W! Z. y4 o: k})
0 `# K# ~6 S* [! }; l* C. n2 m// 然后马上就可以获取映射后的 ArrayBuffer+ Q7 z5 I5 f! G1 D3 T( a$ t  `! {
const mappedArrayBuffer = buffer.getMappedRange()
7 L% J# k; d1 _. u+ u' t* F( c+ w& ^# u% x9 z4 H
/* 在这里执行一些写入操作 */
$ t1 K# I' l8 i' K- A) J; T! P4 Z* a
// 解映射,还管理权给 GPU
" X. @( s% c2 ^1 S: M) {2 @- ^! ybuffer.unmap()" ^" ~4 U; l& E6 W* B. B
</code></pre>% ~/ v' Z: `! z+ u* c; m$ H
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>
/ C4 ]5 Y4 u2 T7 g4 m4 Q9 N<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>( g4 E1 L9 \6 x0 V7 ^
<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>
! g. n4 _1 }1 G& q& K" E! Y7 y<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>4 Y# ~  l3 Y+ Q3 ~; J1 F7 W
<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
- p( n1 j! g3 w' x! e' A$ e$ N, L<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>
9 R3 H- X" p3 `5 @+ w4 i<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>
1 o. ]: I, V1 M1 q<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>/ \8 G2 X/ Z* \! ]  ?' `
<pre><code class="language-js">const texture = getTheRenderedTexture()0 f: B5 L+ b- M& Z5 i

; _7 E8 F* x# @, U) Aconst readbackBuffer = device.createBuffer({+ }- g8 x$ T( M& l5 B4 G
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ," ]8 e: v5 i6 f" H- ^8 A, I8 `
  size: 4 * textureWidth * textureHeight,. Y4 S, c( e) T+ {
})/ R% \, R4 v* }0 J

5 l4 r  t3 Z! {: i/ q// 使用指令编码器将纹理拷贝到 GPUBuffer
  U( w% ^! Z& h6 oconst encoder = device.createCommandEncoder(): M5 f# [3 O" [  l+ x
encoder.copyTextureToBuffer($ E- L" ^1 R/ }9 B  n) C" e$ h
  { texture },
" ?+ h" X- A- v  { buffer, rowPitch: textureWidth * 4 },
- f+ \" E% d" Y7 A% U# b% n/ C  [textureWidth, textureHeight],
* h: K! f$ d' V. l& Y* x)
  y7 U4 y1 Y' O' y! adevice.submit([encoder.finish()])# S+ l0 l, _- @4 y% e
. ?3 `& m- b' ?  v2 u# ?& }
// 映射,令 CPU 端的内存可以访问到数据& l0 I$ o* w9 O1 F5 w8 N+ {
await buffer.mapAsync(GPUMapMode.READ)
5 i+ ^, r/ t% N  `5 e// 保存屏幕截图
$ z6 O6 C# T9 [0 NsaveScreenshot(buffer.getMappedRange())3 o- A) D! J9 e% I
// 解映射
8 a6 z, a+ i& R0 i. u. @buffer.unmap()0 N+ `7 z/ }* _! l5 ]9 _" W
</code></pre>- C5 Y* G* C3 m# Q1 |( K5 M2 K! a& y
$ }# w3 i/ w. H& _5 A
回复

使用道具 举报

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

本版积分规则

手机版|飞雪团队

GMT+8, 2024-4-25 19:04 , Processed in 0.060087 second(s), 21 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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