飞雪团队

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

WebGPU 中的缓冲映射机制

[复制链接]

9171

主题

9259

帖子

2万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
29843
发表于 2022-2-12 14:35:42 | 显示全部楼层 |阅读模式
; p% j& A! u* F' x
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>
6 j1 C5 d: y4 t# M2 T2 D<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>% W: i* Z. z1 W4 ^( a" m# K
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>
- f8 s+ s! |/ t2 v  O3 Q<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>
1 k5 `* U0 `6 i( j* m" l<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>
) f6 g- \% ^3 Y, u<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })
# H/ c4 g2 v& fawait someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节
+ ?7 ]! I" I4 [7 T) |1 `9 a, b4 W# [6 _. U2 t: g
// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作5 {2 \$ z0 I4 G
</code></pre>5 z! r1 b, E4 `: q2 Q
<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>
' ]  u7 i1 T! I# M( N<pre><code class="language-js">somebuffer.unmap(). C" z1 F! M9 W
</code></pre>6 y, u; D- d- B. a5 C  R
<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>: U8 T: X( U2 |0 W
<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>, P, B; q& g. B
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>3 j! Y8 `7 Y9 g( N# {$ X# t
<h2 id="创建时映射">创建时映射</h2># r% D- L$ c+ h' n
<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>
9 e2 K) U. ]% H" T8 h<pre><code class="language-js">const buffer = device.createBuffer({
/ J: N- ^. G: ~- |9 l  usage: GPUBufferUsage.UNIFORM,& P+ D% k2 g8 @! i$ c: c& V* ]
  size: 256,% `7 u& m! j1 c; \5 _  T9 x
  mappedAtCreation: true,
: S; _$ Z. @$ [# y8 w})
4 Q7 e  |7 \$ F// 然后马上就可以获取映射后的 ArrayBuffer1 L9 J4 d7 v  X4 e" ^6 A/ H, D
const mappedArrayBuffer = buffer.getMappedRange()+ {5 I2 `9 C% W( _1 h$ o
6 P. j, t8 T" r
/* 在这里执行一些写入操作 */
/ Z: ]2 h$ |1 T) |) N
+ `) q6 U# e8 M0 w// 解映射,还管理权给 GPU8 [6 S6 R7 X6 I, o
buffer.unmap()+ _. D! e+ R1 l5 z  X1 B
</code></pre>4 W  J5 _2 O! h: c$ O0 M8 ]
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>
1 v# r; m8 M7 P# O, \4 J5 q  q<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>
  _9 f7 F% O6 K* W  R  Q" A5 s<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>/ q4 W" J* a& T) @0 d4 P% ]+ `2 a
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>9 J# `  W; u' u8 M& @  U. v  s
<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
* K& Y3 P7 @! e1 z5 ]- F. a<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>$ b7 v4 s1 W7 O* f  V
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>8 `& r! B4 q: ~- x
<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>
# p0 ?) }$ ^  P" \8 Q4 ]. H0 X<pre><code class="language-js">const texture = getTheRenderedTexture()8 R, s- z  t. L) a
; i# W; v7 w7 |
const readbackBuffer = device.createBuffer({; M: ]! X% G3 B- }" c- x+ H9 L
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
# M% o0 `8 ]% p0 t$ \  size: 4 * textureWidth * textureHeight,
% b6 O8 r3 l+ C6 T})6 U/ ^+ P/ t# B5 N
% u9 G1 v; G4 @6 u# ?
// 使用指令编码器将纹理拷贝到 GPUBuffer
% \9 b6 _% O! m: J% D0 s& P5 vconst encoder = device.createCommandEncoder()
( ]! M$ _8 H# zencoder.copyTextureToBuffer(/ k! u) p6 J3 H7 C4 p
  { texture },# r0 l0 f+ v( L
  { buffer, rowPitch: textureWidth * 4 },9 S% c: N5 D+ E  A) j
  [textureWidth, textureHeight],! @9 M5 r6 ]0 i4 `
)
- |5 x5 W( n2 R/ F7 \device.submit([encoder.finish()])- K. G- X! |2 {6 k! m- v- }; |

9 g# F6 J7 e! _& M" h4 F$ {// 映射,令 CPU 端的内存可以访问到数据; Q; P4 @1 o7 ~# r9 `
await buffer.mapAsync(GPUMapMode.READ)) }0 {" W7 T4 `% v6 s
// 保存屏幕截图! b& U9 A) V- Y0 X) Q
saveScreenshot(buffer.getMappedRange())
, J7 v: ?/ S2 p3 C// 解映射. R( Y8 F) R/ q* w8 \6 H9 {2 Z
buffer.unmap()' i; l$ \, N" ^8 H2 _+ M8 m
</code></pre>
, z& G. L7 @2 |
0 J( ?. Y9 A9 @7 k" ~  g
回复

使用道具 举报

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

本版积分规则

手机版|飞雪团队

GMT+8, 2026-9-2 23:53 , Processed in 0.328412 second(s), 21 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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