飞雪团队

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

WebGPU 中的缓冲映射机制

[复制链接]

6478

主题

6566

帖子

2万

积分

管理员

Rank: 9Rank: 9Rank: 9

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

/ o1 I# p6 `  |4 {; b5 g, G/ g<h1 id="1-什么是缓冲映射">1. 什么是缓冲映射</h1>7 T& b, a3 [% m0 S: T% X  M9 i
<p>就不给定义了,直接简单的说,映射(Mapping)后的某块显存,就能被 CPU 访问。</p>
- J4 H. _. e3 s" g* w<p>三大图形 API(D3D12、Vulkan、Metal)的 Buffer(指显存)映射后,CPU 就能访问它了,此时注意,GPU 仍然可以访问这块显存。这就会导致一个问题:IO冲突,这就需要程序考量这个问题了。</p>
9 K& G! Y6 G2 F4 Z9 h* v<p>WebGPU 禁止了这个行为,改用传递“所有权”来表示映射后的状态,颇具 Rust 的哲学。每一个时刻,CPU 和 GPU 是单边访问显存的,也就避免了竞争和冲突。</p>/ h: W# s4 i5 W& ^# N, x
<p>当 JavaScript 请求映射显存时,所有权并不是马上就能移交给 CPU 的,GPU 这个时候可能手头上还有别的处理显存的操作。所以,<code>GPUBuffer</code> 的映射方法是一个异步方法:</p>' O; P8 f. m$ d7 T7 l9 o
<pre><code class="language-js">const someBuffer = device.createBuffer({ /* ... */ })
3 {4 b$ `! z) g6 d" m6 t$ U( y# r7 H0 Lawait someBuffer.mapAsync(GPUMapMode.READ, 0, 4) // 从 0 开始,只映射 4 个字节! G/ c( s0 V$ t1 U

' \0 w% b& j4 R" s/ d// 之后就可以使用 getMappedRange 方法获取其对应的 ArrayBuffer 进行缓冲操作
8 Q. Y: d' A- e3 z. W+ {</code></pre>
* Z5 G0 B5 R8 e: W6 ?; X9 _<p>不过,解映射操作倒是一个同步操作,CPU 用完后就可以解映射:</p>, l1 i3 l( u4 r5 g' A7 e1 x
<pre><code class="language-js">somebuffer.unmap(): [  H6 x+ B1 T5 \# M  U& p
</code></pre>. O! J; m% V2 f( n
<p>注意,<code>mapAsync</code> 方法将会直接在 WebGPU 内部往设备的默认队列中压入一个操作,此方法作用于 WebGPU 中三大时间轴中的 <strong>队列时间轴</strong>。而且在 mapAsync 成功后,内存才会增加(实测)。</p>, ~/ L' s  ^+ j
<p>当向队列提交指令缓冲后(此指令缓冲的某个渲染通道要用到这块 GPUBuffer),内存上的数据才会提交给 GPU(猜测)。</p>. d8 c# u0 ^! L, o4 ~; ~
<p>由于测试地不多,我在调用 <code>destroy</code> 方法后并未显著看到内存的变少,希望有朋友能测试。</p>! t! t6 F$ R$ Q- |; B1 h0 X& i
<h2 id="创建时映射">创建时映射</h2>
: z! L5 r! b6 G; g<p>可以在创建缓冲时传递 <code>mappedAtCreation: true</code>,这样甚至都不需要声明其 usage 带有 <code>GPUBufferUsage.MAP_WRITE</code></p>$ Y5 {! x3 z! {+ R
<pre><code class="language-js">const buffer = device.createBuffer({8 j8 U) q9 ~8 i1 u/ h
  usage: GPUBufferUsage.UNIFORM,
* G. t  a" Z9 j$ Q  size: 256,* a* w8 Q  R! ]' y, S1 n
  mappedAtCreation: true,
/ Q  e: Z; H5 m5 Z})
( R2 O5 P7 w2 [2 J  e6 a// 然后马上就可以获取映射后的 ArrayBuffer" Z; i3 o2 y. Y& y; \9 O4 \
const mappedArrayBuffer = buffer.getMappedRange()
8 |* [) c7 q  m5 @1 Z
1 f+ ~) P' A+ I- u. L4 t+ t5 b/* 在这里执行一些写入操作 */
; @' E5 W* `6 ^8 K( m& B% a# M1 x5 B" X3 V" [. A
// 解映射,还管理权给 GPU
7 Y5 B6 g8 T1 b/ |( K. Ibuffer.unmap()
. @7 c8 {$ U0 ]- a$ d5 u# Z</code></pre>" M% g( f  j  `3 N# h, c8 m
<h1 id="2-缓冲数据的流向">2 缓冲数据的流向</h1>& Y8 k& j% b% b( D+ c- p% `- {* }
<h2 id="21-cpu-至-gpu">2.1 CPU 至 GPU</h2>3 O2 I" h/ `2 ^) {/ o2 U! W/ `6 p: l
<p>JavaScript 这端会在 rAF 中频繁地将大量数据传递给 GPUBuffer 映射出来的 ArrayBuffer,然后随着解映射、提交指令缓冲到队列,最后传递给 GPU.</p>& O" [/ m$ H0 A
<p>上述最常见的例子莫过于传递每一帧所需的 VertexBuffer、UniformBuffer 以及计算通道所需的 StorageBuffer 等。</p>
9 X/ Y( z. ^0 P- |6 E* `<p>使用队列对象的 <code>writeBuffer</code> 方法写入缓冲对象是非常高效率的,但是与用来写入的映射后的一个 GPUBuffer 相比,<code>writeBuffer</code> 有一个额外的拷贝操作。推测会影响性能,虽然官方推荐的例子中有很多 writeBuffer 的操作,大多数是用于 UniformBuffer 的更新。</p>3 y; f" z# D5 i- K9 g$ t8 t
<h2 id="22-gpu-至-cpu">2.2 GPU 至 CPU</h2>. [$ K. e) F' L; C2 f" H
<p>这样反向的传递比较少,但也不是没有。譬如屏幕截图(保存颜色附件到 ArrayBuffer)、计算通道的结果统计等,就需要从 GPU 的计算结果中获取数据。</p>4 G3 e9 J& _  @' k
<p>譬如,官方给的从渲染的纹理中获取像素数据例子:</p>8 j+ Q7 Z% |4 `4 q, X
<pre><code class="language-js">const texture = getTheRenderedTexture()
7 G8 A8 F( E& t; D1 ~
5 ?% A9 r8 k# B, `3 F$ G- Hconst readbackBuffer = device.createBuffer({* q: N8 d' N( V- J8 i# [
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
) r' `2 A4 t3 U2 ?! i$ B  size: 4 * textureWidth * textureHeight,
( i, }6 U" j  Y1 B& ?1 Z})
% X9 o* H) _. ]) D5 N! j/ X( }% p8 W, b
// 使用指令编码器将纹理拷贝到 GPUBuffer! n; G4 K: \4 T
const encoder = device.createCommandEncoder()
, A) D( o# V# i) c5 i' [5 yencoder.copyTextureToBuffer(
( N/ x3 s  {( q+ e: i  { texture },' V7 i) z$ f- B$ C( F. m+ X  r! H
  { buffer, rowPitch: textureWidth * 4 },% P/ F/ R, i- {) A  i2 p
  [textureWidth, textureHeight],/ \0 o( `9 d# i/ S; o
)- E* ^: Q0 p* r; f
device.submit([encoder.finish()])
) e+ K' n3 D/ E9 B+ v( q6 e  n4 v* ~6 o% ~0 X
// 映射,令 CPU 端的内存可以访问到数据
) j9 f& O8 C$ [9 s2 Fawait buffer.mapAsync(GPUMapMode.READ)
7 ~4 c; q( R% |0 J+ ]// 保存屏幕截图
0 j" P9 N1 N" ~$ }, I( N; JsaveScreenshot(buffer.getMappedRange()): f/ P5 Z/ o, H1 W# j' n+ d- {
// 解映射# O  K( Y$ o. Y1 z$ G% ]
buffer.unmap()
8 I$ W! |1 N$ m  A</code></pre>4 {) h* U4 p. h8 _/ ~" @8 b' f
& g: p- ?2 b, f( ]# e, u
回复

使用道具 举报

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

本版积分规则

手机版|飞雪团队

GMT+8, 2025-5-1 06:42 , Processed in 0.058738 second(s), 22 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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