飞雪团队

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

WebGPU 中的缓冲映射机制

[复制链接]

8310

主题

8398

帖子

2万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
27260
发表于 2022-2-12 14:35:42 | 显示全部楼层 |阅读模式
- w" \9 S  ^# [
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>. w1 p0 ^5 p' D
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>
, i  o' Q( j4 ?/ _+ \) R<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>1 i. G! r8 S0 D7 L- S2 Y
<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>$ u; `2 d# @% X9 v
<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>
- O% E4 D7 J9 `6 s+ h8 c& [" t<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })/ N7 P  C  d. I' i
await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节; \5 _, P0 w) _6 \

  a- s2 @( ]" D- B- ?1 }8 [// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作& @2 Y5 |+ ?! u2 e" i
</code></pre>
; ~) M, x" g; s<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>0 H  h% v- ~/ t& N# {: _
<pre><code class="language-js">somebuffer.unmap()% z2 [  k4 }8 ~& b
</code></pre>3 x4 l: U+ O" M; C# h" w$ v! W6 N9 d
<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>( E9 Y$ j/ U# G( ^2 k7 Q
<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>3 G- J* y. G& I+ x' j+ P7 ?( W
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>1 r' `; C( d$ x, N8 f. F
<h2 id="创建时映射">创建时映射</h2>
! o/ b7 @7 U% u2 {5 F/ d<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>& r3 p% b+ b' Y$ q
<pre><code class="language-js">const buffer = device.createBuffer({
* n5 ^" i' J, ]  ^/ n  usage: GPUBufferUsage.UNIFORM,
* a( B1 H( W8 t1 A- Z6 s  size: 256,
% [2 n8 Y! h1 l: p& i, u/ S1 I  mappedAtCreation: true,
1 A9 l7 s, T+ f0 {/ O) M})* K% l  z+ h# O/ t9 A6 u" E
// 然后马上就可以获取映射后的 ArrayBuffer
) @5 y: Z! l4 I7 e: Tconst mappedArrayBuffer = buffer.getMappedRange()
6 _, I' ]" P8 I' @
4 t5 X9 ?% y2 ~3 l5 U2 ]/* 在这里执行一些写入操作 */
* X$ D( O( M: O* W* K5 ^: x
% C- A' n2 Q& W" n% k* ^// 解映射,还管理权给 GPU
' ^" r# p/ g2 p. bbuffer.unmap(); Z0 ?. h# C" y+ ^. [! K
</code></pre>
# H9 A3 w+ f0 V% `( v7 V; t<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>7 K5 j. A9 q  O  h+ g& r
<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>' x4 F- |- K0 @, i" ?7 k2 u; ~- j+ e
<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>
) C5 d( O6 i0 N) \" G* @<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>
) |8 i0 v. D- p5 V/ E3 `5 l<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
, R: x9 X4 h5 I) m8 r8 u# ^<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>" N$ D  n" {1 I4 @7 v  U* `5 `
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>
, t8 X( r+ j. l  J3 v, ]) v2 C<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>
! t  R1 c: _% S& W1 p9 Q<pre><code class="language-js">const texture = getTheRenderedTexture(), X- r* R- }+ P0 `, \
. [, o$ B  E8 A/ n8 W' V' Y
const readbackBuffer = device.createBuffer({
' d6 `" Z9 F( ?7 ?+ s; X  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,% e0 Y* X. D' S9 |) ^
  size: 4 * textureWidth * textureHeight,4 D. Z% d7 G$ T  `: E8 w" E5 n
})1 m7 Y0 K1 g, _! u. @$ N& Y
, M! `- f" `. c9 g  H$ Z2 f
// 使用指令编码器将纹理拷贝到 GPUBuffer
/ I5 F  L" B" |+ E' f  v) uconst encoder = device.createCommandEncoder()
; j8 G+ z$ }; ?encoder.copyTextureToBuffer(
! Y9 \* s. O. d) I  |5 _* Q  { texture },# v  V9 |# @, U$ n7 r% C% B
  { buffer, rowPitch: textureWidth * 4 },2 @! K2 A5 V8 b+ W
  [textureWidth, textureHeight],
. O9 L& L. W9 P8 S; \+ T)
6 l+ ^, i6 e% b5 u& ydevice.submit([encoder.finish()])
' V7 F4 M$ u% U4 Y8 M$ s7 g4 I
5 @6 |* r, S+ [% n// 映射,令 CPU 端的内存可以访问到数据
, J( h& L! ]1 p+ G/ Z8 ~await buffer.mapAsync(GPUMapMode.READ)
% j5 ]7 |; w6 C; C// 保存屏幕截图
2 A$ ^7 f2 _8 r% c6 }" fsaveScreenshot(buffer.getMappedRange())
3 [! A" i7 o& P: A// 解映射1 W% `) y: y! u( Y0 T+ Z4 @7 G5 |
buffer.unmap(), |% \* l* r: B  Y
</code></pre>
6 R, d/ ^( T: S. p: a2 U/ @4 m  `8 h
回复

使用道具 举报

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

本版积分规则

手机版|飞雪团队

GMT+8, 2026-3-1 15:43 , Processed in 0.083009 second(s), 21 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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