飞雪团队

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

WebGPU 中的缓冲映射机制

[复制链接]

8087

主题

8175

帖子

2万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
26591
发表于 2022-2-12 14:35:42 | 显示全部楼层 |阅读模式
8 b& m3 D4 t7 r, c
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>1 y) [4 r2 d# c3 `- b7 {7 G
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>1 a# j5 d+ B$ J# U/ ]) Q. J
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>5 j9 {7 f4 Y: P8 w2 a" t5 L2 @/ m7 h
<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>1 Y1 n8 `2 ]6 M+ b
<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>7 e3 h% D( b. o$ q: E/ h
<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })
9 F  q$ N6 n5 s- E) Cawait someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节
) h& b! }* c) H  q7 @* |; r' [. i: [8 b, `& O3 A& V
// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作* f" D8 V8 o6 H- l/ g
</code></pre>, i+ M' D' F+ i5 p
<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>9 R5 D9 _& S* P3 P* d& N
<pre><code class="language-js">somebuffer.unmap(); w- P4 u  S: G
</code></pre>
0 k! M9 J, p4 ^: `# ]% v<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>
* S8 r2 A$ z% z( Q3 b" m$ l4 w<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>
: r: ~3 _# ^* e( s1 x* Q( W<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>$ B6 Z4 s# m$ a# @' M- n) L
<h2 id="创建时映射">创建时映射</h2>
# v+ U% g. R" j' V0 |$ k. ]<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>
. w+ a. t: ]5 S2 {8 \<pre><code class="language-js">const buffer = device.createBuffer({
8 l, ?# @/ g1 H4 C  usage: GPUBufferUsage.UNIFORM,; Q, N0 J4 G0 q- n4 D( Y, F
  size: 256,
/ k- V* u) `6 ?- ^+ _  mappedAtCreation: true," {3 ~' b3 k0 @2 S$ V" y, ~% I
})5 h* W2 S8 X8 U7 N  K
// 然后马上就可以获取映射后的 ArrayBuffer
& J7 G# r+ m$ g- @  h$ W6 dconst mappedArrayBuffer = buffer.getMappedRange()
8 v3 n; O" @/ v, O- S2 Q
& l7 ?2 g0 }1 z5 b. |/* 在这里执行一些写入操作 */9 J7 \7 p3 R  y+ H

$ H* Z3 y% Y% n; }* \6 |// 解映射,还管理权给 GPU, y) `( m9 s; O' s/ G5 y
buffer.unmap()  o+ ^: w2 O8 R) A4 M; I7 U, L
</code></pre>
3 T% u7 D0 h1 h6 q/ z" R8 J<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>  G6 Q4 p& |% g. B
<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>
0 E4 C' |6 d1 j% r8 ^5 i+ Y<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>
# \: m1 X, [1 ?' Z+ o1 X7 r# @  p- o<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>
' A$ O# }7 y7 \<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
" O+ R* I$ O. G+ ?! k0 v; J<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>
0 A/ h5 ]: F  |7 @; \. S9 ]<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>" U" O: @* [  h' f  j
<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>
* L* ]- O; P4 u* O<pre><code class="language-js">const texture = getTheRenderedTexture()
, l6 k" g* A: m+ T7 B  O2 G+ x& D$ `8 r! ]0 `
const readbackBuffer = device.createBuffer({# O2 K/ Q- D! E6 E* k* w& y
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,: M, A6 N: d8 `" |( J! e. S
  size: 4 * textureWidth * textureHeight," m& V+ Z* I# Y4 B, }0 X
})
! w1 M1 q. b! f; _4 X/ q
; @  O! u- w, `/ [: H% j- Q' x// 使用指令编码器将纹理拷贝到 GPUBuffer
% n9 v6 o( M/ H0 b, econst encoder = device.createCommandEncoder()
% D: K- v6 k  o. w1 Pencoder.copyTextureToBuffer(
( c9 w4 ]$ O$ `$ x& _  { texture },. x% V4 l3 R3 Y
  { buffer, rowPitch: textureWidth * 4 },
2 s4 T- \8 }$ a7 u. s0 O  [textureWidth, textureHeight],, d$ t! F+ \- `2 R
)
8 y7 O% T7 C7 U3 T; H  pdevice.submit([encoder.finish()])
0 B1 R0 Q. e! F6 P$ k( N' m% u
# a1 G) X, u7 ]( Y// 映射,令 CPU 端的内存可以访问到数据+ A5 C' k& M. R! q
await buffer.mapAsync(GPUMapMode.READ)
) p$ ?3 o2 b% @( T  [: q) u// 保存屏幕截图
, Y% K3 ~; A8 @) c7 VsaveScreenshot(buffer.getMappedRange())
5 O. I/ j0 g# Z# V2 H) r7 x7 p// 解映射
0 X0 n/ M# O4 P. r. O" rbuffer.unmap()7 n' s4 K( }( ]- b$ {; h6 j! }9 y
</code></pre>
) ?  S/ O6 H8 _, ]. ~6 s1 J  A0 a0 O. ~1 ^; g$ q
回复

使用道具 举报

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

本版积分规则

手机版|飞雪团队

GMT+8, 2025-12-17 18:38 , Processed in 0.092973 second(s), 21 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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