飞雪团队

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

WebGPU 中的缓冲映射机制

[复制链接]

6478

主题

6566

帖子

2万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
21758
发表于 2022-2-12 14:35:42 | 显示全部楼层 |阅读模式
$ {) ]2 Y3 n: ?$ Z$ L- R# `
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>! ~* N; D$ d1 q7 a0 F6 x
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>1 @% D8 q; \1 g  ^4 r
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>
& C% l9 s- [: e/ L! S8 Q<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>: N: D" p, f/ E) v( H5 F
<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>
9 u. E0 X: r, z5 o% `0 R<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })
/ \/ T1 F& @9 y% p/ K0 jawait someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节5 c# O8 z( u# j5 Y

, N- ^) U6 |& i0 G6 D' a// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作' }: o" H# z' ]  Z7 V7 d6 B% v& s
</code></pre>) q2 _$ v) p7 X& k7 X* r, u3 q
<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>
- {* H5 r5 D  I- y4 e, A<pre><code class="language-js">somebuffer.unmap()0 ^- Z7 ?4 C/ Y& k* \9 |
</code></pre>
, f" @4 w9 g, J/ P9 A. {<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>
+ `0 h7 r$ r5 E; \<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>, x! [0 b7 X8 t& u0 {
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>5 @6 h& x: S1 g0 C
<h2 id="创建时映射">创建时映射</h2>
0 i2 v0 a6 z1 j6 S0 Z<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>7 T0 S* z! P6 z; W& a! g
<pre><code class="language-js">const buffer = device.createBuffer({
( ], x' E/ g0 k  usage: GPUBufferUsage.UNIFORM,9 X, h$ }4 ^2 V9 V
  size: 256,1 b# Q6 L7 h$ q/ ^( E1 v5 Q. ?; c* N8 J
  mappedAtCreation: true,
; c$ C, T: E* T+ Q})
, j# [# q- z3 E! {7 [. [0 A, B// 然后马上就可以获取映射后的 ArrayBuffer
% m6 K" O* ^9 J' w9 Q+ y3 gconst mappedArrayBuffer = buffer.getMappedRange()
2 G. }4 W, L6 F, M" @  n' C& ^. b& @  g, l, d% ?0 \( N9 Y
/* 在这里执行一些写入操作 */! d! j% r# R/ h/ q# P1 u; Q) z

; y4 q" ?0 G& x9 B: f, x9 O// 解映射,还管理权给 GPU
; p' N- |9 P2 g) Z9 \2 Rbuffer.unmap()  C; N/ P/ Y+ r0 X& U* R3 s4 P( K' C
</code></pre>' g1 x! j: @8 y- J: g
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>
6 x! F% G! [7 H9 `<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>$ L: k4 U) G% I+ ?- ]. o( h# E
<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>
3 b) r/ b0 ?2 V<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>1 q8 N0 F" Z! p' u2 {
<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
- s  Z) V  x5 W( X( A* F; C5 G<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>
+ b& d4 S) ^, Z! Q; S<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>
- @, a" N7 Q6 C- n. t<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>
7 f: }, {9 p& Z6 i. g% f2 D. E1 M3 U<pre><code class="language-js">const texture = getTheRenderedTexture()# k$ {) t' M, ^+ f

  }# _6 t' J; a$ Uconst readbackBuffer = device.createBuffer({
2 k2 K8 }  h( g: ?7 M# S  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,' s8 H! [9 Z  G4 m: K) M
  size: 4 * textureWidth * textureHeight,
/ I7 F) ]) v. N9 C: W, D' d})
/ Q* X: }5 v& j/ F4 e
3 _2 j2 \; ?6 K( T// 使用指令编码器将纹理拷贝到 GPUBuffer1 O- g' W: X. J) M. z
const encoder = device.createCommandEncoder()
; t# X1 v1 N- k6 [( oencoder.copyTextureToBuffer(  e+ o; @, I+ t& x0 F( j0 _9 j4 Y
  { texture },3 f( G  W* q% @' n9 ^( O
  { buffer, rowPitch: textureWidth * 4 },+ B7 ?, `) [& J( a/ t" o7 \
  [textureWidth, textureHeight],
  _" W. X- e& c2 P& v& j)
- A4 x- e" j, ~  C  t# udevice.submit([encoder.finish()])9 \7 D! N  Z9 l( q
* y' Y; |7 }7 G6 K# q( C! q6 b  a
// 映射,令 CPU 端的内存可以访问到数据( ~2 X9 X& J4 ?) s0 p; y/ n
await buffer.mapAsync(GPUMapMode.READ)
% }( D2 l4 x' X- u// 保存屏幕截图- T" K0 e: W, H( U1 t( `8 b& r
saveScreenshot(buffer.getMappedRange()): Q" N( [, m/ K3 ~
// 解映射
; a2 D9 v8 \, U) @( h4 Z2 Abuffer.unmap()- `2 [+ r( \' i/ O& [0 l
</code></pre>- e' J$ Y# |" D* Z! M) U+ K! |

1 f4 y. a& J( M6 @. ?1 B1 O
回复

使用道具 举报

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

本版积分规则

手机版|飞雪团队

GMT+8, 2025-5-1 07:11 , Processed in 0.544562 second(s), 21 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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