飞雪团队

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

WebGPU 中的缓冲映射机制

[复制链接]

9008

主题

9096

帖子

2万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
29354
发表于 2022-2-12 14:35:42 | 显示全部楼层 |阅读模式
+ z( s; X: Z' K5 \$ T# p/ c
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>3 u0 x+ `7 Y1 Q7 H6 [
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>/ ~& ?6 a- R6 n9 E( e" _
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>4 M9 q" f  J* ~7 |& S& `  Q
<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>
$ d' P2 d* {3 S8 j<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>3 I* U9 O/ |" l* L, L5 ~; M% S- p7 a
<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })* H; W  O4 J7 ?& U9 n8 [: k4 t
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节
4 D. n, P+ |1 l) k) \  ~# u, ~  W1 W
2 p0 c! v+ V: ]  u. K7 e6 q" C// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作2 i0 ]( m" v- n- j% M
</code></pre>
1 N1 z  I( d$ y# B3 |<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>! O8 a' m& y9 s' r  V
<pre><code class="language-js">somebuffer.unmap()1 h! @8 n3 b0 M9 b$ V- i
</code></pre>
& X% B* O2 a* L0 G<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>
2 p. M8 d: v- w: O<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>& V: N" j1 h1 O' q  T
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>! h( G/ G4 M- u6 I% o; w0 P
<h2 id="创建时映射">创建时映射</h2>
. }# M2 \6 I$ R$ ~, ?& A<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>. b, ~3 }. Z. |/ V  X0 C/ g
<pre><code class="language-js">const buffer = device.createBuffer({; Q2 D) l9 o+ ]  N% V1 G  @" r
  usage: GPUBufferUsage.UNIFORM,
* O! q; ^4 A8 a( K7 A# t  size: 256,
, @. E) T9 M) u% }5 H! Q+ z- D0 z) }  mappedAtCreation: true,( N8 W4 q% [0 I% B, i& ?6 n6 h) ?- N1 L% S
})
" h/ y' M6 m* k; N// 然后马上就可以获取映射后的 ArrayBuffer
; J# V5 {, k  Q4 ^- Z/ Y* L, Dconst mappedArrayBuffer = buffer.getMappedRange()( y! e5 Z9 O* x# |- o7 J" T0 D

: b# x- }- q+ ^: U/ S& Q* W/* 在这里执行一些写入操作 */' O2 Y! K8 p: @# X1 w
* z7 y1 n* a- s* w3 Q
// 解映射,还管理权给 GPU
" V4 F6 z0 H$ a; Nbuffer.unmap()
: ^9 ]; K: I$ F2 u4 b" p& P</code></pre>
$ Y5 U/ D1 J; _7 A<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>2 E6 W4 Q: P, B# |
<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>3 O, b( }$ Y( T
<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>. Z4 K6 b+ b6 x7 m6 Z
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>
6 v/ e8 ^: N$ A/ M* O: G( Z<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
+ O( T* b, R/ @<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>
' c2 F9 e8 t: o8 W+ e<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>
6 j! d6 H$ K3 B6 |0 [* `- G' Q<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>5 H! x- O! v  n# Q
<pre><code class="language-js">const texture = getTheRenderedTexture()2 B. W% X% ~7 O8 S6 Q2 a4 D: F' Y

* m' f2 u( ?  e7 z, uconst readbackBuffer = device.createBuffer({
& |% b2 k& i" P8 `/ |( g  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
! `1 {; p- T3 ?6 `  size: 4 * textureWidth * textureHeight,7 L5 t- L) x" [* n/ V6 A" D
})
& [* a! i1 }, G* X/ j, o( T- u! |$ m+ j& o8 p$ M
// 使用指令编码器将纹理拷贝到 GPUBuffer
. E+ Z- y2 M+ O3 Vconst encoder = device.createCommandEncoder()- d; ~- K! A5 s/ ~0 P5 J
encoder.copyTextureToBuffer(
1 [6 U+ J. p- _/ k) L  { texture },
* w6 }) P* L' t, |0 ^* Z* _3 O  { buffer, rowPitch: textureWidth * 4 },9 p$ v% e7 N) g9 k7 a" @' s6 q4 o
  [textureWidth, textureHeight],  P+ N2 V: O  T9 N" Y8 W7 L$ ^$ R
)
  k: U7 m2 L- j7 ?7 ]2 Udevice.submit([encoder.finish()])5 F; b  x; b( J
# U5 m/ \. Z+ i6 c7 _
// 映射,令 CPU 端的内存可以访问到数据; {  o" p- O" {5 s& a
await buffer.mapAsync(GPUMapMode.READ)9 I" \1 F/ c; p. I
// 保存屏幕截图+ K9 s' s2 a, l1 Z9 G) ]4 n
saveScreenshot(buffer.getMappedRange())% J9 {$ X& l, H8 s7 a" ~2 [
// 解映射
1 Z8 t) i8 L3 |  J' Zbuffer.unmap()
4 b9 B9 ^$ H0 D+ ]5 L</code></pre>
) F8 K& |8 Y2 t0 n5 A: O2 ^: g1 }5 n2 P0 ~- L
回复

使用道具 举报

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

本版积分规则

手机版|飞雪团队

GMT+8, 2026-7-22 20:29 , Processed in 0.062041 second(s), 21 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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