飞雪团队

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

WebGPU 中的缓冲映射机制

[复制链接]

6382

主题

6470

帖子

2万

积分

管理员

Rank: 9Rank: 9Rank: 9

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

' i* f$ N7 N% j% |9 X! e<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>1 L% g3 \/ p( O0 A+ c
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>
2 n# g3 x, G" u$ i' V( p9 f<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>
# g6 h, ?1 i. ^  D<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>. d8 C1 \. ]' c0 ^5 J! \
<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>
- w9 V2 B, ~2 A% \<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })8 v% o' l; \6 w, B; }4 c
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节" S' X* W; [: L# }: t8 c
; Y: p% ^! H- O. `" ]+ b9 m8 D
// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作
7 \/ g- J; T( f, l6 `( S</code></pre>
# c9 J2 s9 a9 P; f1 Z0 j<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>
; N! ]9 M) q( C3 G<pre><code class="language-js">somebuffer.unmap()
$ H0 U& X/ `' B</code></pre>0 M; D5 j8 q6 d6 c& I
<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>
# Q1 T6 f: t/ b9 @7 o<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>' E/ Z- X' b/ e1 M$ W5 A
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>$ r" ~- |7 T1 g/ H2 c
<h2 id="创建时映射">创建时映射</h2>0 D. `. c, c9 }# a8 t
<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>2 K( }' P, v0 ?* l' c! ]7 j
<pre><code class="language-js">const buffer = device.createBuffer({" M  _! t6 g4 G: ?( k0 U" j
  usage: GPUBufferUsage.UNIFORM,
1 I# ?0 n( q2 M% J& `  size: 256,
: O9 \+ Z% i9 T1 k4 x  mappedAtCreation: true,
2 a5 h" d* \9 W, i+ D# u9 T) p# L}); ^/ z" t" G; X; C& U
// 然后马上就可以获取映射后的 ArrayBuffer
1 b9 Y6 o9 g! _5 U6 `const mappedArrayBuffer = buffer.getMappedRange()' h3 ^; X, M- E, E9 M1 }# ?5 ~1 k

3 U" X( }( K) i' T/ k. E/* 在这里执行一些写入操作 */
8 B( r# O4 U0 J
3 n/ Z. L5 w- _& I( T2 ^% h// 解映射,还管理权给 GPU
) d+ f2 j7 v$ Z( j3 x- H5 Ebuffer.unmap()
& P  ^& D' m; H3 W+ M</code></pre>
7 U# l7 ^. A8 h; s<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>1 u7 t% ~) g9 ?' X+ r% v2 V
<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>
( L9 Y* ~% t7 b) p( D, x" s7 r<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>2 w1 \- E9 X$ i+ J$ D# s& ]9 S* d
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>6 j1 M# V" D9 T1 x6 z
<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>+ T1 V5 y6 M; W" b6 A2 l6 C) C9 h
<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>5 ]6 j. `" T4 ~  m' W9 X2 n" i4 C0 j
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>
) c; Z9 x6 s6 C0 S2 v6 `) ^<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>
% G! N+ R2 P) `  i* {8 Z<pre><code class="language-js">const texture = getTheRenderedTexture()5 D1 g! Q8 G: S# t2 @+ D

' i$ x, Z; P, h  d6 c% M$ G# U+ m( D  nconst readbackBuffer = device.createBuffer({6 Q, I% j1 h; U/ X: C+ x8 V# v
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
! A' w. N# y  n7 _9 n% V; v  size: 4 * textureWidth * textureHeight,2 N4 y2 E- U2 I: G6 b
})
8 @0 Z4 F; g2 n. M4 D. _. l5 G
1 D0 p2 O0 x( Z  s// 使用指令编码器将纹理拷贝到 GPUBuffer0 S: b. g5 D2 |( w; F
const encoder = device.createCommandEncoder()- o' b$ t6 Y% p* F( ^! j* u
encoder.copyTextureToBuffer(' X( K, k2 I8 }1 E( m* u
  { texture },
1 m5 s, V/ A4 K/ Z+ D  { buffer, rowPitch: textureWidth * 4 },
' p" [+ T/ _( o5 [3 C9 K6 {  [textureWidth, textureHeight],
; _  [7 D( `4 E7 |/ n6 e)) p1 I# Y8 U( W% E( s6 e' F: F% H  Y. X
device.submit([encoder.finish()])
; M/ l* b9 x, y; C  X8 H- ], [3 Y' b; i! a
// 映射,令 CPU 端的内存可以访问到数据
5 O3 }  d- P) A5 p3 nawait buffer.mapAsync(GPUMapMode.READ)7 r# @. T" N# l
// 保存屏幕截图
. X; o6 z! F  e# xsaveScreenshot(buffer.getMappedRange())( D; u! M) U  V9 ?" x, N1 I% {6 l
// 解映射4 Q* X  K3 I; a7 S
buffer.unmap()
4 s9 g# n/ E+ ]  s5 t8 Z5 @4 o</code></pre>
; [  S# ^* M) m0 W# s" I2 p5 X2 K* @" o! L3 W
回复

使用道具 举报

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

本版积分规则

手机版|飞雪团队

GMT+8, 2025-4-19 10:39 , Processed in 0.059573 second(s), 21 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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