飞雪团队

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

WebGPU 中的缓冲映射机制

[复制链接]

6738

主题

6826

帖子

2万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
22542
发表于 2022-2-12 14:35:42 | 显示全部楼层 |阅读模式
$ N, W* C/ |' m) e4 G3 q) t% p
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>
, S$ |7 L1 p' k1 }8 {<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>8 K2 s3 l$ Q! I, k: E7 H
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p># j. I7 N( Q$ h$ @
<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>
9 f9 }* o# Y' I1 I<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>; b; q8 S' e- A
<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })
* V" A. m" U$ ~# j* Fawait someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节; q3 V6 L0 l, n/ y. s2 L9 d2 s7 F
6 E3 N) U- X$ B  B
// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作
/ @7 U  }+ `  I6 T- Q: J; c  m9 Y</code></pre>
6 S. s' ?4 T  z% S. o<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>
! `( c2 \$ u8 r* q, g<pre><code class="language-js">somebuffer.unmap()
! t! i4 [2 s! P7 a: X</code></pre>
" O4 F" B" F+ b1 ]4 L8 }<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>
1 c  i* ]% |( @* v, @. i! |# l<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>
& P/ _( e# l# a% {$ ~9 Z  M* _<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>7 c7 s0 k: l) H& {5 ?- Z2 V+ c
<h2 id="创建时映射">创建时映射</h2>. @0 u* q& i. c
<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>
$ M# t9 I. N/ e2 P<pre><code class="language-js">const buffer = device.createBuffer({) }3 `. @  o* K- Z- W
  usage: GPUBufferUsage.UNIFORM,1 }% z9 V5 x4 H+ n4 B
  size: 256,
6 ~, z" _6 q8 `0 d  mappedAtCreation: true,
" x8 I2 R( _, ?})
9 O# q6 b  [6 n1 A" ?; R// 然后马上就可以获取映射后的 ArrayBuffer9 v$ H/ T- u% j
const mappedArrayBuffer = buffer.getMappedRange()
6 c' s5 `( g7 h  D9 K3 L8 `# |
: G" {8 @+ ?: n, H3 W/* 在这里执行一些写入操作 */
6 _4 C/ w, R8 ~2 B
* D9 k3 X9 @7 n/ G9 r/ X// 解映射,还管理权给 GPU% |$ Y+ m% d* o% o
buffer.unmap()
6 b# ?+ {# e9 e$ ?; ]</code></pre>( g. q; {& j. a. @" t0 N, ~, b
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>
5 t3 o2 v0 D: U$ ~5 f<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>- V" h: `  [3 ]* t! Q1 v( i
<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>  P! ]/ C6 K- f2 G" S- {
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>
; M  o- L+ H$ P<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
7 V8 F0 L( h1 Q$ Z, t# G$ o: o<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>3 o5 \  l. v( ?
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>
( a  v! A/ J( v5 }: g<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>/ l, ?# m2 Z% V* j+ `; i( j
<pre><code class="language-js">const texture = getTheRenderedTexture()
" y1 R8 A2 S5 U& e" r' ]& z' ?9 d4 _. c2 {: d$ a8 \1 K6 P; m
const readbackBuffer = device.createBuffer({1 N3 v4 `& R- a5 }
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,8 J; K4 i' }- q
  size: 4 * textureWidth * textureHeight,
! m& c! \% ~4 w; Q0 \& x})) `- y! n3 V  T8 p$ Y  S4 t

! ?' k8 P5 V7 Y4 E1 R7 k/ e// 使用指令编码器将纹理拷贝到 GPUBuffer
6 ^" l. r" N4 C- h+ G) u( Pconst encoder = device.createCommandEncoder()" W* e8 v7 o) M( h. R' W/ L
encoder.copyTextureToBuffer(- E$ a) O0 j- t0 d1 r' S/ Q
  { texture },
) J3 `& R5 L2 H+ u# s  y  { buffer, rowPitch: textureWidth * 4 },
( [- l& v- l# j' t1 W+ _  [textureWidth, textureHeight],
1 L# T  h8 N1 z( y6 H4 x: f)
: W3 K  T+ P* x% Adevice.submit([encoder.finish()])
' Q8 N" Q# O% W
  {, e4 f/ f1 F8 P# G* @9 `// 映射,令 CPU 端的内存可以访问到数据5 `5 l6 E  \+ g
await buffer.mapAsync(GPUMapMode.READ)
$ \; }; d$ k) r/ m. T- V4 R+ _; w// 保存屏幕截图
0 g4 K/ v2 J" b1 J% fsaveScreenshot(buffer.getMappedRange())
/ p$ Y% V$ Y9 d- P0 @8 e// 解映射# w* ~5 A* ]) [" Q" Q
buffer.unmap()
& e5 P! X6 r3 c2 @- c6 ?</code></pre>
; Z* J/ M7 \1 q% V" `
: Q4 y5 @2 y' J2 n  n5 x
回复

使用道具 举报

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

本版积分规则

手机版|飞雪团队

GMT+8, 2025-6-15 16:15 , Processed in 0.082286 second(s), 22 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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