飞雪团队

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

WebGPU 中的缓冲映射机制

[复制链接]

9180

主题

9268

帖子

2万

积分

管理员

Rank: 9Rank: 9Rank: 9

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

) ]; M7 s/ \! L! p  Q! l' z9 l<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>
; y, r& v% Q5 }; A<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p># ]- f$ f# Q+ k% ]* P
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>0 G9 u8 D' E+ A) _7 O, j, k1 Z) l# r
<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>
" V5 ?7 U  u; t9 q<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>
+ H9 a* w9 {: Z2 m4 H, b+ r9 r# m7 r<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })
; j( T9 `3 V9 z9 o3 N% k, dawait someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节" @7 {- z, F+ i! H5 b

4 i' ]( a4 U% s" [3 w% F0 b// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作/ ]+ N$ M* c( w8 d! r: f
</code></pre>3 I, a4 P: o6 ?+ b
<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>
6 Y( T: U0 E- V/ ?% |/ f<pre><code class="language-js">somebuffer.unmap()) g4 ?$ j3 C7 G5 E3 H
</code></pre>' U* }+ ~. B5 B
<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>$ O$ V  A4 ~6 j6 j- r) |5 a% Z5 Y; `
<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>
( ^8 m0 ]! ~) \. Y: _' [<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>
1 H2 J( S/ P$ U1 N<h2 id="创建时映射">创建时映射</h2>8 C' Y3 p8 C/ q# @- J1 R
<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>
6 C2 A+ o  r0 B, S- o<pre><code class="language-js">const buffer = device.createBuffer({/ u0 c3 w" g3 {7 _3 M3 K: o
  usage: GPUBufferUsage.UNIFORM,. V  `$ u& J2 w
  size: 256,
5 i2 |* m2 g3 Y- z  mappedAtCreation: true," S% F2 j2 K- ?1 Z
})6 p( i9 N# U  A  r: ~  N" p
// 然后马上就可以获取映射后的 ArrayBuffer
% i( F  [3 O( fconst mappedArrayBuffer = buffer.getMappedRange()
2 r3 F" B2 G" u. g9 ]8 B9 x! \
- y7 N1 g4 ~0 ~: d0 q; Q8 Z/* 在这里执行一些写入操作 */1 s+ |2 }. }: c0 g
, o. P* s+ p$ B8 Z/ N7 \
// 解映射,还管理权给 GPU
1 \5 f! j8 P  J. d/ ebuffer.unmap()' I5 R3 D6 U3 v' ]' X9 B
</code></pre>  R) h$ N* A& s: S' n* x$ X$ l
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>
+ n; K! ?0 a6 n4 K) J+ z- R+ C, i$ D<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>! v0 _# R2 r3 [4 w6 X1 n
<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>9 q0 Q& Y7 u8 ~2 u; D0 N4 Q  n
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>1 o  L' Y( b6 L  m# [
<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
; q: r6 c- g+ b- q5 T<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>3 {; S0 A( U) o8 z  S3 M
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>
. ~- W6 h8 x8 q3 V<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>  s. ~: I( c( ^; O% ~* u
<pre><code class="language-js">const texture = getTheRenderedTexture()
! T3 l2 J/ \, R+ }$ s: ^! J( [, x  v8 X; S' u; n5 Z
const readbackBuffer = device.createBuffer({
+ X6 B; w: E+ x( F" f$ R8 [' ~' }& e  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
3 A  U8 ^, R8 D2 j  size: 4 * textureWidth * textureHeight,9 E- [  v" b" `6 B8 N( c  Z
})
3 c( _5 f/ @$ \$ X
9 w/ m+ G4 V1 p& W8 E// 使用指令编码器将纹理拷贝到 GPUBuffer
' I4 Y" o; K6 L# I4 Lconst encoder = device.createCommandEncoder()
- c- B% G9 X" b# I5 D* ]" h$ xencoder.copyTextureToBuffer(3 d) o# X6 g% g% j9 O- O
  { texture },: |% @( Q; O5 I- w* m
  { buffer, rowPitch: textureWidth * 4 },  v  M  R2 {  f' l
  [textureWidth, textureHeight],
8 c( I7 s7 {3 i- s" P, `! r)  n: X& ?* x6 E! Q
device.submit([encoder.finish()])
1 T* @7 J  r. y5 L' S! b8 K1 R, a4 L$ G0 y
// 映射,令 CPU 端的内存可以访问到数据/ P; x0 ^+ b# I5 b" {! q; M
await buffer.mapAsync(GPUMapMode.READ)
; n7 n: ?8 ?1 \( I// 保存屏幕截图
$ t& I. T9 Y3 _# J- A( YsaveScreenshot(buffer.getMappedRange())
2 _" L! \8 {* u( r7 c0 @// 解映射, S) k. n' E  A% L" k2 ]
buffer.unmap()" v/ K5 k" n" T2 j; t! T- i- U7 H
</code></pre>
8 d  x- f% Z- h; ~# D
/ P# [- d, ]6 x- i# |
回复

使用道具 举报

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

本版积分规则

手机版|飞雪团队

GMT+8, 2026-9-9 11:47 , Processed in 0.061890 second(s), 22 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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