飞雪团队

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

WebGPU 中的缓冲映射机制

[复制链接]

4822

主题

4910

帖子

1万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
16784
发表于 2022-2-12 14:35:42 | 显示全部楼层 |阅读模式
4 Q! o) w* M  l8 y2 E
<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>
/ c9 l- x. x! O; m8 x( W<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>2 d: h6 J' c- R* c! R# e. C  l
<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>7 }. z; `9 j" u
<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>
* F# z3 F; D( g/ b; e" y7 d/ P<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>
- {6 D4 h  i/ t- I<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })
+ ~4 R9 b# J7 L" c* C- \$ [- T: }await someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节4 T1 ]5 E+ L  t% ^7 L

8 j4 K) T2 b, h/ S// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作
% y) E, F5 D5 c4 @</code></pre>( d, Q5 U4 H& y2 Q8 O+ s; ?$ U
<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>
+ \+ ?7 H3 H& P9 m; r<pre><code class="language-js">somebuffer.unmap()" D" E! e; D% x- x2 e
</code></pre>5 L+ V  T, [& N6 W3 k2 \6 d
<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>
) W- t3 r- ~4 C4 X) t: q" x4 Z<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>8 k& a/ J! l7 U, x; R8 T* T) ?
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>9 q. S4 F: ~5 J& R. j4 F: y
<h2 id="创建时映射">创建时映射</h2>
2 k. z/ R8 l" S' e) m( J<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>
1 ^; k. J$ i8 p# ~8 [' W( u<pre><code class="language-js">const buffer = device.createBuffer({$ l2 C' C' ~4 \
  usage: GPUBufferUsage.UNIFORM,
% ?+ i  n$ @: r+ Q  size: 256,  Z$ T% V6 v/ E! G: e& }
  mappedAtCreation: true,
& t+ R- H3 A7 X( A( O})% d) C% X; g2 P- c5 @
// 然后马上就可以获取映射后的 ArrayBuffer1 T- @3 K9 N! U  r7 Z) E
const mappedArrayBuffer = buffer.getMappedRange()
" o( U* p; ~. s# W) H, H: @1 M6 y0 S7 g( ~3 W2 ^
/* 在这里执行一些写入操作 */8 g# x$ ?" b5 J; ]$ t- x
# W! ~: h0 [% G! e8 h! E* K7 C
// 解映射,还管理权给 GPU
, S% n! E8 n, a; j9 R6 |5 Tbuffer.unmap()3 ?4 `  t: [1 ?9 ?; m
</code></pre>5 v# [# s0 g/ `0 n, W/ D4 D/ t9 V
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>
7 S# w. R! |8 J3 c* U<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>  |+ F3 [: n5 K5 Q
<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>- _0 V- m( I9 g& F" r0 C
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>) y" v7 X6 ]! }
<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>
' ~  W" _3 r. k- v7 @<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>* S0 h6 R) p- R# [; C4 H
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>, N# ]* a% {7 P8 {/ j; r! W. ?
<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>
- [9 S5 C7 A. n& `<pre><code class="language-js">const texture = getTheRenderedTexture()
, ~- _6 r$ x, Q$ _5 V' }8 F. S* q( [) Z/ L( G
const readbackBuffer = device.createBuffer({
/ a+ D0 I, e8 p* o7 j: R2 m  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,% v2 e: c0 M6 [' Q0 N
  size: 4 * textureWidth * textureHeight,# u" r5 Z/ e; u
})
" }! }3 K* P# u% G! U
  D6 L% K' ^' @+ @( j// 使用指令编码器将纹理拷贝到 GPUBuffer9 X7 h+ l8 L1 b, r: o' r( K
const encoder = device.createCommandEncoder()
1 g! l& ]# A' H6 L- aencoder.copyTextureToBuffer(5 C7 e$ e3 K2 u  l- ~0 v& Y
  { texture },6 u$ z+ a7 m; e3 {
  { buffer, rowPitch: textureWidth * 4 },0 L1 y4 k. K1 [1 E3 k; i- D/ R
  [textureWidth, textureHeight],
6 m2 K. X1 t. ~5 k; D- B)7 k2 O% C4 ?. `# `% [- X- V
device.submit([encoder.finish()])
; w8 `) x2 l" D  _
; Z6 n2 d  r; F5 g// 映射,令 CPU 端的内存可以访问到数据
' o  U5 P* v9 `/ v1 g' iawait buffer.mapAsync(GPUMapMode.READ)0 b" A3 }/ k* v. J3 [
// 保存屏幕截图
- I. z: C4 I6 i( AsaveScreenshot(buffer.getMappedRange()). B- m. j% H) q$ D# T
// 解映射
0 _3 W! L) C/ u+ \- l0 r/ {buffer.unmap()
# _% [7 p# y% v! j</code></pre>  \% I0 y$ e2 ]

2 v+ {( h7 N% V5 `
回复

使用道具 举报

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

本版积分规则

手机版|飞雪团队

GMT+8, 2024-9-20 01:05 , Processed in 0.063629 second(s), 22 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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