飞雪团队

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

WebGPU 中的缓冲映射机制

[复制链接]

7583

主题

7671

帖子

2万

积分

管理员

Rank: 9Rank: 9Rank: 9

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

% G) y  Z3 u( C% K<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>6 ~, _, ~6 t  W+ P8 Z8 {
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>, }1 D) N) Y5 n; w9 q
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p># g/ M  F& Q3 T
<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>
+ p  Y; B) v% S8 q0 v! h( M. ], Z<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>3 ?4 \6 L; A, t& A, V
<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })8 O0 Q" H" p" k& H
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节
9 f4 M8 @! ~! a: C- I# C3 Y7 {& [8 x+ w6 X: D$ X% e/ u
// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作
- K3 g' L2 K3 H3 j3 k! W</code></pre>& F) p0 Z7 ^( ?) a! {3 Q! s
<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>
& A- P  }" S0 l4 C* J6 x<pre><code class="language-js">somebuffer.unmap()4 v5 u4 P( C( J( ~2 D' b
</code></pre>+ c5 U! |( _0 X  ~
<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>9 q8 d7 f; [* K9 [
<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>
* o6 _$ \- L( U. r: Z7 u3 F<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>$ `! d( e' S- O  z9 k0 T
<h2 id="创建时映射">创建时映射</h2>
7 e. n: W3 c6 f( z& |- E<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>
' x, R( K  c& y7 d1 Z& s0 D<pre><code class="language-js">const buffer = device.createBuffer({: r6 b+ i0 b5 D6 A7 I( A8 Z  q
  usage: GPUBufferUsage.UNIFORM,( A9 _- G, Y4 \/ s0 n5 }% h2 P
  size: 256,4 j8 A: ?& {/ s* Y/ W
  mappedAtCreation: true,3 T$ J% d/ I5 k* w: S
})/ ~$ o/ b2 n7 l/ k* V6 e( Q
// 然后马上就可以获取映射后的 ArrayBuffer
3 }1 d8 r0 f' T0 D! M8 iconst mappedArrayBuffer = buffer.getMappedRange()
- b: E7 ~0 Y" I# B/ M- S5 K, @1 d3 j/ ^/ Y7 C
/* 在这里执行一些写入操作 */4 y- E% U  Y# m
) K0 w3 _4 o* w0 R( K0 C: S
// 解映射,还管理权给 GPU
; E% w# \& g5 K1 p+ o4 L- S- L. Bbuffer.unmap()  G! ]! F' [( |6 N
</code></pre>1 [4 {' u- f7 Z) `+ a- S7 V6 |
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>
! g( k3 m% }7 [4 W9 @<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>
  H1 O1 w! k1 b* s- X2 {$ s1 h<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>) Y4 R+ t0 R4 X( F
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>* M+ [# `2 K+ T  s6 X& x& e
<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>- c  W& T3 ^  ^( p; L' m
<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>6 Q4 N5 Q( f; K- @3 o+ g% _3 I& j
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>5 Z9 f: h( N+ i- e0 `& Y5 f, k
<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>& d" v$ ]( _# B6 g5 Q: X* u
<pre><code class="language-js">const texture = getTheRenderedTexture()  ^5 k( e/ E5 D1 r) v% ?$ o( ?7 Q9 Z- z! L

  C9 x, R0 j* y4 R$ Aconst readbackBuffer = device.createBuffer({+ v/ c3 I1 W4 M/ {# _! r& t# R
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
4 z4 R3 R* v: b) R5 ?9 Y' w  size: 4 * textureWidth * textureHeight,, q- |/ Z# Q3 x% a  p
})
) K1 p- A7 L* y, E" w- }, F) Y4 E% |  n, V/ Z
// 使用指令编码器将纹理拷贝到 GPUBuffer
  n: H3 C( P4 D7 B  }) s0 u8 b8 `const encoder = device.createCommandEncoder()
8 k) S7 M, S, {# b$ Gencoder.copyTextureToBuffer(
9 e8 {3 l+ g5 f8 W7 q  { texture },
7 N% \  K3 D7 G8 c9 e5 k  { buffer, rowPitch: textureWidth * 4 },
& l$ p( E2 q6 R6 O+ |" B1 s  [textureWidth, textureHeight],5 D% {6 ^* O% v1 q  T$ B/ y9 m
)
! y$ W! w. {3 l3 R$ }2 x$ W+ Odevice.submit([encoder.finish()])! S7 X7 ~/ Q- `% B
" L0 c! ?) }& m( @' T
// 映射,令 CPU 端的内存可以访问到数据
0 x  U; F, q* L; kawait buffer.mapAsync(GPUMapMode.READ)
. j( H; S* C  Q$ B. a// 保存屏幕截图
% f; {/ R/ Y7 {+ t0 CsaveScreenshot(buffer.getMappedRange()): v8 x6 w. Q8 a% S& v
// 解映射
3 @0 U5 Y; G5 _: \, t5 ibuffer.unmap()
& P5 F% Y6 R" @6 t</code></pre>
' M& C# P  n# q0 w2 ~3 o" p. }7 a# D8 d6 X$ m6 `
回复

使用道具 举报

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

本版积分规则

手机版|飞雪团队

GMT+8, 2025-9-18 09:37 , Processed in 0.059131 second(s), 21 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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