飞雪团队

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

WebGPU 中的缓冲映射机制

[复制链接]

8920

主题

9008

帖子

2万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
29090
发表于 2022-2-12 14:35:42 | 显示全部楼层 |阅读模式

& h  q  J; X4 q8 J) V8 _( }2 T6 j<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>
# C5 D2 @" }* Z<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>3 l  `1 H( [) j) q* k
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>
/ Y0 f6 y" @5 Q1 a: i6 u/ ^" X<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>
2 Z( g$ n, T9 X6 ~8 `% [<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>
3 S1 j  Z+ W/ b3 {! L<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ }): i. g* g; P% M" m$ o, |6 _
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节
" m& m+ P& y& g5 D
$ k1 v4 U: u- G$ A" ^$ Q// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作
+ w; r: p/ p* d# P2 J</code></pre>0 ?) L6 v. x' l
<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>7 s+ g: \/ ?6 v( K# _
<pre><code class="language-js">somebuffer.unmap()
# Y  N. ^- j; q# f- t, V</code></pre>
5 d  K$ `, I4 e<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>
6 w* r0 K' I6 a0 L<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>% V  {- M4 @9 X/ y! l. S
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>/ U8 C$ U2 |8 n0 h; |: R
<h2 id="创建时映射">创建时映射</h2>3 L% k* j. z" \- P
<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>+ C* z7 s' j& [
<pre><code class="language-js">const buffer = device.createBuffer({, o' [8 _" G5 p1 t+ Q$ \
  usage: GPUBufferUsage.UNIFORM,
0 z1 {" w7 |# \* N& v' A8 p, V  size: 256,
0 N" A. S" f6 v2 u7 T3 a  mappedAtCreation: true,+ s2 |2 M& l: F
})7 q9 w0 @5 v' ~- Z& b
// 然后马上就可以获取映射后的 ArrayBuffer
: O3 q: P7 X, Z- P3 j) lconst mappedArrayBuffer = buffer.getMappedRange()( S6 H3 D" e- E1 u

# j/ L4 f# j$ N/* 在这里执行一些写入操作 */) o' |) x* H7 }' ~

" O1 d4 x6 ?4 p. J// 解映射,还管理权给 GPU) E, P( F/ K# Q
buffer.unmap(): X5 }9 K: {0 I' l& f7 K, p! K1 p; x8 g
</code></pre>
4 U0 _# I9 {5 i( t<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>
* q& c" l3 V0 e+ J- x* v( C<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>
/ a$ P) T- z5 Q' i8 j- p<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>, |! t0 Q( T5 L" @% `/ E, L3 J+ L
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>
8 j( f+ X0 k1 b* q<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
# ~$ o2 x2 Z0 S9 a<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>
' f  ?5 F1 T8 y7 Z<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>2 C% x+ |1 E6 O) z: S; n
<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>: ?7 b+ Y1 Y3 {: }' M& w
<pre><code class="language-js">const texture = getTheRenderedTexture()0 @& W: ]  i% q$ ?, ?! Q

% {2 V' h5 I# X: {! \1 ~1 ^const readbackBuffer = device.createBuffer({4 K7 E5 Q( O5 l6 j- v& t7 I! k
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
! U& b/ {* q# k/ b. s9 }) d  size: 4 * textureWidth * textureHeight,: d2 q: \4 m( n# j/ |( c
})
9 }3 A' a8 r2 ~2 y7 }# D. r
$ {) S/ `5 Q1 E4 {  K// 使用指令编码器将纹理拷贝到 GPUBuffer0 ?6 s3 V; x; z/ D
const encoder = device.createCommandEncoder()# d# ?" `& ]( E5 [
encoder.copyTextureToBuffer(
- ]# m+ \; l/ l( B: Y  { texture },, K9 o2 j* q& a) K2 `1 g& M
  { buffer, rowPitch: textureWidth * 4 },% b  P/ ^1 g1 Q
  [textureWidth, textureHeight],8 Q0 |: h0 J# g) l7 M
)
' X( g! C0 G2 ]2 Sdevice.submit([encoder.finish()])
( {! g* [( c6 o$ l" ^5 o$ }/ A& L; [( K7 M7 j+ t0 ?3 w
// 映射,令 CPU 端的内存可以访问到数据
5 H& U) J: w3 D1 q& bawait buffer.mapAsync(GPUMapMode.READ)8 @+ w# S5 V5 d- r/ X
// 保存屏幕截图) Q3 s+ J+ |$ |1 i7 z8 S& C1 Z9 J
saveScreenshot(buffer.getMappedRange())
8 ?' G' {8 `) B7 n8 E# a. y// 解映射
, k' Z+ c7 `) v( b  g* O  G7 K5 dbuffer.unmap()
. d! f% v% F' d: r</code></pre>1 I9 s# L' b1 F7 f* j) x
& R. g" B$ [  [+ D/ @/ \6 d
回复

使用道具 举报

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

本版积分规则

手机版|飞雪团队

GMT+8, 2026-7-5 00:23 , Processed in 0.087849 second(s), 22 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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