飞雪团队

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

WebGPU 中的缓冲映射机制

[复制链接]

8566

主题

8654

帖子

2万

积分

管理员

Rank: 9Rank: 9Rank: 9

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

; z4 `" t6 [6 L  K9 {( L' Y<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>* Y4 Z9 h, _8 \: e9 @
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>
; Y" X$ K7 @+ J, w" i+ m<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>) Y* K) G, P% w* m8 m4 k
<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>$ Z8 r. l9 d9 \' v1 W' E
<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>* ?# n0 p) D& l" ~4 P2 s8 P" \
<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })! x1 H& Z* ?2 v! [: \
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节
1 h4 e: [& o) Q' e. M7 G* R
2 G* H  H+ @" f* @0 ~. Y+ R6 y// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作
5 n" {9 e1 I1 ?6 [$ ^</code></pre>
  M% W7 \. d' a! h% l  ~3 Q<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>- M( K6 {6 j, Q0 E/ w* O9 o. `
<pre><code class="language-js">somebuffer.unmap()0 K6 a/ z! C/ g3 L4 w1 U+ c
</code></pre>
9 _3 _; h  g; x- V6 |( w  M6 M<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>
; z: ]0 w/ R5 }6 Z$ w<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>
4 O# ~8 a% \" \  w+ Y( s<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>
& n+ E+ G# l8 Y/ e( T<h2 id="创建时映射">创建时映射</h2>" F) _$ p6 [7 }+ K* u* ?
<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>/ x3 X& |4 A& R/ C& l$ @0 N) [
<pre><code class="language-js">const buffer = device.createBuffer({
3 ^4 ?3 ]& O* @8 m: r& @3 t) v: M  usage: GPUBufferUsage.UNIFORM,
9 [) K- d  {6 p. @  size: 256,
$ a4 n: ^7 [0 V. `+ o  mappedAtCreation: true,
; O* r% ~4 g, O+ Y})
4 K8 c; x1 L3 @7 _" [: H$ ?$ [// 然后马上就可以获取映射后的 ArrayBuffer4 a4 q2 J! }4 k3 u
const mappedArrayBuffer = buffer.getMappedRange()' w2 A2 e2 q; @+ w, T8 ~
( y; z4 b1 `6 ~' `5 ?, R
/* 在这里执行一些写入操作 */
# d) G* Q, L( v' v% x3 b/ ?. S2 e7 V4 M( `. L- X
// 解映射,还管理权给 GPU3 ~$ h. C, i, i' ?3 ~, t1 K8 g7 D) A
buffer.unmap()4 [3 U7 x8 I7 @/ d3 v% y6 b
</code></pre>; h( r* ]' _/ B4 i- j6 v; v- A
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>1 B$ U: }. N# O2 o
<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>* ?2 y% |' U+ Z2 t' a9 e
<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>! N" L& K9 W" n: Z  I( a
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>/ f: _$ R' {3 V9 e
<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>3 p- C3 G1 {( r1 e( f8 h- T/ H+ h3 i4 r
<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>
5 ?& n4 J5 L+ k# \5 k; f<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>* K! ?/ \) U& X2 d$ q9 W0 I, Q
<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>; X. I* L6 X; n7 D# m4 u
<pre><code class="language-js">const texture = getTheRenderedTexture(), k8 @* y& Q: V0 l1 S
+ A5 R8 a0 K6 b* `' ^% }) h
const readbackBuffer = device.createBuffer({
, h7 \" z! m# Y5 r7 i  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,; s5 c. a6 `. K" P0 d
  size: 4 * textureWidth * textureHeight,4 M5 j, Q& y' Q
})$ }: `3 _' o8 Y! T7 _# {2 B' H

* D2 r$ ]7 P& t6 E7 a// 使用指令编码器将纹理拷贝到 GPUBuffer
& a" l% ~+ V6 S7 Z+ T3 X9 ~/ \; Pconst encoder = device.createCommandEncoder()
9 a: ^8 H6 D, H* cencoder.copyTextureToBuffer(6 P1 L" Q6 L% i/ J! V0 @3 |
  { texture },& g5 B. l6 ~/ {+ J5 p! ?
  { buffer, rowPitch: textureWidth * 4 },
, ]5 h3 I6 M1 o+ y& L( G# n6 ?  [textureWidth, textureHeight],
4 _1 t% c+ \* H+ p, K$ G5 n)& p$ I  e1 c$ R9 ?" I1 I
device.submit([encoder.finish()])! E; N% O4 e4 T. K
& @4 q9 I. y  [# R* I
// 映射,令 CPU 端的内存可以访问到数据. L0 P! O; J0 r; A& f
await buffer.mapAsync(GPUMapMode.READ)
' E# G4 v+ i* {// 保存屏幕截图
3 p/ T: z: F. l; N! T* F& ]saveScreenshot(buffer.getMappedRange())1 j, W3 s5 p: s0 y- Z
// 解映射
: i5 O( ^$ {4 [- wbuffer.unmap(), s/ y* a! o  D/ M
</code></pre>: L( R% T1 ~2 s2 K+ E

7 g$ H6 b5 U! {' p1 d" m. r* h3 J
回复

使用道具 举报

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

本版积分规则

手机版|飞雪团队

GMT+8, 2026-4-16 23:26 , Processed in 0.060672 second(s), 21 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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