D8efc72a3e9b0ff3b1006ac8ff344dcb
Metal【13】—— Argument Buffers

上一篇我们提到了 Triple Buffer,接着这个,我们继续聊聊另外一个比较像的东西,Argument Buffers。虽然严格意义上,它们的定义不一样,但是有个共同的作用:一定程度上,优化性能。

先划个重点:

  • Triple Buffer:尽可能的减少空闲时间,使得 CPU 和 GPU 最大程度的被利用起来,从而更快的完成任务、实现更流畅的效果。
  • Argument Buffers:把需要用到的资源,比如 texture、buffer 等,整合到一起,从而减少 CPU 开销,提升性能。

PS:

订阅后的朋友,可以加我微信:wxidlongze,拉你进群。交流,扯淡,学习资源分享~

最后,源码在文末~

那么,开始吧~


1. 定义/优势

An argument buffer is an opaque data representation of a group of resources that can be collectively assigned as graphics or compute function arguments. An argument buffer can contain multiple resources of various sizes and types, such as buffers, textures, samplers, and inlined constant data.

从官方定义中,我们可以看到,Argument Buffer 是把我们之前传递给着色器的那些参数,比如 buffers,textures,samplers 和一些常量数据,聚合在一起,以组的方式进行管理。

简单来说,就是把要用上的资源 (textures, samplers, and inlined constant data) encode 到一个专用的buffer 上。这个 buffer 就是 Argument Buffer。

所以 Argument Buffer 本身也是一个 MTLBuffer 对象。

那么,为什么要这做?分开传递不是更直接吗?

下面我们对比下分开管理(之前我们所有 Demo 中用到的方式)和 使用 Argument Buffer 的差异。


1.1 Individual Resources

简单回顾一下之前的内容。

在每次 draw call 之前,我们需要使用 Command Encoder 配置各种资源,写入 Command Buffer 中。具体点,是这样一个过程:

这里都是我们之前经常用到的一些数据。

比如 roughness,intensity 是可调节参数,我们通常会把它封装成一个结构体,然后通过 MTLBuffer 统一维护。

texture,sampler 就是纹理相关。

代码上,类似这样:

[renderEncoder setFragmentBuffer:_indirectBuffer offset:0 atIndex:0];
[renderEncoder setFragmentSamplerState:_sampler atIndex:0];
[renderEncoder setFragmentTexture:_texture atIndex:0];

当渲染比较复杂场景的时候,对象繁多,这时候会涉及很多次 draw call,如下:

那么,这会带来什么问题呢?

Commands that set individual resources can become numerous and expensive, especially for large apps or games.

  • setFragmentBuffer、setFragmentSamplerState、setFragmentTexture 这些函数的调用,会有对应的 binding 操作(binding buffer data to a graphics or compute function so it can be processed by the GPU),即我们常说的绑定,让相应的数据关联起来(capture state and track residency),它会带来额外的开销。
  • 当一些数据没有频繁改动的时候,在 render loop(即每次渲染的时候)中配置,是很浪费的。

1.2 Argument Buffers

所以,在性能方面,为了优化这两个问题,2017 Metal 2 中,引入了 Argument buffers 这个概念。

使用 Argument buffers 后,相同的场景下,resources 的管理方式会变成这样:

将刚才提到的多个资源,统一放到一个特殊的 MTLBuffer 中去管理,这样每次 draw 的时候,有效的将 API calls 从原先的 5 个变成现在的 2个( set the argument buffer + draw),有效的优化了上述提到的性能问题,降低 CPU 开销。

结合下面这张图,进一步深化这个优化过程。

再来一张官方的性能对比图,资源越多的时候,优化越明显

下面我们具体看下使用上的差异。

Argument buffers 在着色器中,以自定义的结构体形式存在。结构体每个元素,就代表之前的独立资源。

举个例子,在 .metal 文件中,声明形如 FragmentShaderArguments 的 Argument buffers:

typedef struct FragmentShaderArguments {
    texture2d<half> exampleTexture  [[ id(AAPLArgumentBufferIDExampleTexture)  ]];
    sampler         exampleSampler  [[ id(AAPLArgumentBufferIDExampleSampler)  ]];
    device float   *exampleBuffer   [[ id(AAPLArgumentBufferIDExampleBuffer)   ]];
    uint32_t        exampleConstant [[ id(AAPLArgumentBufferIDExampleConstant) ]];
} FragmentShaderArguments;

这里的 exampleTexture、exampleSampler、exampleBuffer、exampleConstant 等定义,和原先我们独立管理的时候基本一致。但是这里统一使用 [[id(n)]] 限定符,同时定义了各个资源的 index 索引。

在传入 fragment 的时候,和之前的 MTLBuffer 一样,使用 [[ buffer(n) ]] 限定符就可以。

fragment float4
fragmentShader(       RasterizerData            in                 [[ stage_in ]],
               device FragmentShaderArguments & fragmentShaderArgs [[ buffer(AAPLFragmentBufferIndexArguments) ]]) {
      // Get the sampler encoded in the argument buffer
    sampler exampleSampler = fragmentShaderArgs.exampleSampler;
    // ...
}

为了方便理解,我们顺带看一下旧有方式定义的 fragmentShader:

fragment float4
fragmentShader(RasterizerData    in             [[ stage_in ]],
               texture2d<half>      exampleTexture [[texture(0)]],
               sampler              exampleSampler [[sampler(0)]],
               device float       *exampleBuffer   [[buffer(0)]],
              constant uint32_t& exampleConstant [[buffer(1)]]) {
  // ...
}

而在 CPU 阶段,我们需要做的是数据的编码,以及资源的配置。其实也和普通的 MTLBuffer 管理方式一样,区别就是我们之前提到的,将配置工作,提前到了初始化阶段,只进行一次,如下:

// 1: MTLArgumentEncoder
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
id<MTLArgumentEncoder> argumentEncoder =
    [fragmentFunction newArgumentEncoderWithBufferIndex:AAPLFragmentBufferIndexArguments];

// 2: Argument buffer
NSUInteger argumentBufferLength = argumentEncoder.encodedLength;
_fragmentShaderArgumentBuffer = [_device newBufferWithLength:argumentBufferLength options:0];
[argumentEncoder setArgumentBuffer:_fragmentShaderArgumentBuffer offset:0];


// 3: Resources
[argumentEncoder setTexture:_texture atIndex:AAPLArgumentBufferIDExampleTexture];
[argumentEncoder setSamplerState:_sampler atIndex:AAPLArgumentBufferIDExampleSampler];
[argumentEncoder setBuffer:_indirectBuffer offset:0 atIndex:AAPLArgumentBufferIDExampleBuffer];

// 4: Constant
uint32_t *numElementsAddress = [argumentEncoder constantDataAtIndex:AAPLArgumentBufferIDExampleConstant];
*numElementsAddress = bufferElements;

按照划分的顺序,依次来看:

首先,使用专用的 MTLArgumentEncoder 来进行 buffer 的编码。

/*!
 * @protocol MTLArgumentEncoder
 * @discussion MTLArgumentEncoder encodes buffer, texture, sampler, and constant data into a buffer.
 */
API_AVAILABLE(macos(10.13), ios(11.0))
@protocol MTLArgumentEncoder <NSObject>

之前我们接触的 MTLRenderCommandEncoder 都是在 draw 的时候实时创建,这里可以在 init 的时候就创建 MTLArgumentEncoder。通过 newArgumentEncoderWithBufferIndex 方法,和相应下标的 argument buffer 关联起来。

Argument buffer 的创建方式和之前的 MTLBuffer 一致,它的 size(bytes),可以直接通过 encodedLength 来获取,这个参数值表示 Argument buffer 中所有资源所包含的 size。

/*!
 * @property encodedLength
 * @abstract The number of bytes required to store the encoded resource bindings.
 */
@property (readonly) NSUInteger encodedLength;

然后再通过调用 setArgumentBuffer 方法,来标识 _fragmentShaderArgumentBuffer 是 Argument buffer。同时之后的数据绑定,是和这个 buffer 相关联。

/*!
 * @method setArgumentBuffer:offset:
 * @brief Sets the destination buffer and offset at which the arguments will be encoded.
 */
- (void)setArgumentBuffer:(nullable id <MTLBuffer>)argumentBuffer offset:(NSUInteger)offset;

然后就是 setTexture、setSamplerState、setBuffer,和之前一样,不再累述。

最后注意一下常量数据的传递,它有点特殊。它是直接内嵌在 Argument buffer 中,而不是我们创建好这个东西,然后告诉 Argument buffer 它的地址,去指向它。而是反过来,通过 constantDataAtIndex 来获取它在 Argument buffer 中的实际地址,然后我们往里面写入具体的常量值。

```objective-c
/*!

  • @method constantDataAtIndex:
  • @brief Returns a pointer to the constant data at the given bind point index.
top Created with Sketch.