「这是我参加11月更文应战的第17天,活动详情检查:2021最终一次更文应战」

概述

经过本示例,你将了解如何办理数据依靠,并防止 CPU 和 GPU 之间的处理器等候。

本示例烘托接连的三角形,这些三角形沿着正弦波顺序排列。每一帧都会更新三角形极点的方位,然后烘托新图像。这些动态更新的数据会发生一种运动幻觉,三角形似乎沿着正弦波移动。

Metal 框架之同步 CPU 与 GPU 工作

该示例将三角形极点存储在 CPU 和 GPU 共享的缓冲区中。 CPU 将数据写入缓冲区,GPU 读取它。

数据依靠和处理器等候

资源共享形成处理器之间的数据依靠性; CPU 有必要在 GPU 读取资源之前完结对资源的写入。 假如 GPU 在 CPU 写入资源之前读取资源,则 GPU 会读取未界说的资源数据。 假如在 CPU 写入资源时 GPU 读取资源,GPU 会读取不正确的资源数据。

Metal 框架之同步 CPU 与 GPU 工作
这些数据依靠性会在 CPU 和 GPU 之间形成处理器等候; 每个处理器在开端自己的作业之前有必要等候另一个处理器完结它的作业。

不过因为 CPU 和 GPU 是独立的处理器,因而能够经过运用一个资源的多个实例使它们一起作业。 每帧中,有必要为着色器供给相同的参数,但这并不意味着需求引证相同的资源目标。相反,能够创立一个资源的多个实例池,并在每次烘托帧时运用不同的实例。如下图所示,CPU 能够将方位数据写入第 n+1 帧运用的缓冲区,一起 GPU 从第 n 帧运用的缓冲区中读取方位数据。 经过运用缓冲区的多个实例,不断烘托帧的情况下,CPU 和 GPU 就能够接连作业并防止中止。

Metal 框架之同步 CPU 与 GPU 工作

用 CPU 初始化数据

自界说一个结构体 AAPLVertex 来标明每个极点,包含方位和色彩属性:

typedef struct
{
  vector_float2 position;
  vector_float4 color;
} AAPLVertex;

自界说一个 AAPLTriangle 类,该类供给一个获取三角形接口,该三角形由 3 个极点组成:

+(const AAPLVertex *)vertices
{
     const float TriangleSize = 64; 
     static const AAPLVertex triangleVertices[] = 
     { 
         // Pixel Positions, RGBA colors. 
         { { -0.5*TriangleSize, -0.5*TriangleSize }, { 1, 1, 1, 1 } },
         { { 0.0*TriangleSize, +0.5*TriangleSize }, { 1, 1, 1, 1 } }, 
         { { +0.5*TriangleSize, -0.5*TriangleSize }, { 1, 1, 1, 1 } } 
      };
     return triangleVertices;
}

用方位和色彩初始化多个三角形极点,并将它们存储在三角形数组 (_triangles) 中:

NSMutableArray *triangles = [[NSMutableArray alloc] initWithCapacity:NumTriangles]; 
// Initialize each triangle.
for(NSUInteger t = 0; t < NumTriangles; t++)
{ 
    vector_float2 trianglePosition; 
    // Determine the starting position of the triangle in a horizontal line. 
    trianglePosition.x = ((-((float)NumTriangles) / 2.0) + t) * horizontalSpacing;
    trianglePosition.y = 0.0; 
    // Create the triangle, set its properties, and add it to the array. 
    AAPLTriangle * triangle = [AAPLTriangle new]; 
    triangle.position = trianglePosition; 
    triangle.color = Colors[t % NumColors]; 
    [triangles addObject:triangle];
}
_triangles = triangles;

分配数据存储

核算三角形极点的总存储巨细。 App 烘托了 50 个三角形,每个三角形有3个极点,共150个极点,每个极点是 AAPLVertex 结构的巨细:

const NSUInteger triangleVertexCount = [AAPLTriangle vertexCount];
_totalVertexCount = triangleVertexCount * _triangles.count;
const NSUInteger triangleVertexBufferSize = _totalVertexCount * sizeof(AAPLVertex);

初始化多个缓冲区以存储极点数据的多个副本。 关于每个缓冲区,分配恰好满足的内存来存储 150 个极点:

for(NSUInteger bufferIndex = 0; bufferIndex < MaxFramesInFlight; bufferIndex++)
{
  _*vertexBuffers[bufferIndex] = [* _device newBufferWithLength:triangleVertexBufferSize 
            options:MTLResourceStorageModeShared];
  _*vertexBuffers[bufferIndex].label = [NSString stringWithFormat:@"Vertex Buffer* #%lu", 
    (unsigned long)bufferIndex];
}

初始化时,_vertexBuffers 数组中缓冲区实例的内容为空。

运用 CPU 更新数据

每一帧中,在 draw(in:) 烘托循环开端时,运用 CPU 更新 updateState 方法中一个缓冲区实例的内容:


// Vertex data for the current triangles.
AAPLVertex *currentTriangleVertices = _vertexBuffers[_currentBuffer].contents;
// Update each triangle.
for(NSUInteger triangle = 0; triangle < NumTriangles; triangle++)
{
  vector_float2 trianglePosition = _triangles[triangle].position;
  // Displace the y-position of the triangle using a sine wave.
  trianglePosition.y = (sin(trianglePosition.x/waveMagnitude + _wavePosition) * waveMagnitude);
  // Update the position of the triangle.
  _triangles[triangle].position = trianglePosition;
  // Update the vertices of the current vertex buffer with the triangle's new position.
  for(NSUInteger vertex = 0; vertex < triangleVertexCount; vertex++)
  {
    NSUInteger currentVertex = vertex + (triangle * triangleVertexCount);
    currentTriangleVertices[currentVertex].position = triangleVertices[vertex].position +
        _triangles[triangle].position;
    currentTriangleVertices[currentVertex].color = _triangles[triangle].color;
  }
}

更新缓冲区实例后,在同一帧余下的时刻内,不能运用 CPU 拜访其数据。


注释:
在提交指令缓冲区(引证一个缓冲区实例)之前,有必要完结缓冲区实例的 CPU 所有写入。
不然,GPU 或许会在 CPU 仍在写入缓冲区实例时开端读取缓冲区实例。

编码 GPU 指令

接下来,对烘托通道中引证缓冲区实例的指令进行编码:

[renderEncoder setVertexBuffer:_vertexBuffers[_currentBuffer]
            offset:0
           atIndex:AAPLVertexInputIndexVertices];
// Set the viewport size.
[renderEncoder setVertexBytes:&_viewportSize           length:sizeof(_viewportSize)           atIndex:AAPLVertexInputIndexViewportSize];
// Draw the triangle vertices.
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle         vertexStart:0         vertexCount:_totalVertexCount];

提交和履行 GPU 指令

在烘托循环结束时,调用指令缓冲区的 commit() 方法将作业提交给 GPU:

[commandBuffer commit];

GPU 从 RasterizerData 极点着色器中的极点缓冲区读取数据,将缓冲区实例作为输入参数:

vertex RasterizerData
vertexShader(const uint vertexID [[ vertex_id ]],
      const device AAPLVertex *vertices [[ buffer(AAPLVertexInputIndexVertices) ]],
      constant vector_uint2 *viewportSizePointer [[ buffer(AAPLVertexInputIndexViewportSize) ]])

复用多个缓冲区实例

当两个处理器都完结了它们的作业时,一个完整的帧的作业就完结了。关于每一帧,履行以下过程:

  1. 将数据写入缓冲区实例。
  2. 对引证缓冲区实例的指令进行编码。
  3. 提交包含编码指令的指令缓冲区。
  4. 从缓冲区实例读取数据。

当一帧的作业完结时,CPU 和 GPU 不再需求该帧中运用的缓冲区实例。 然而,丢弃一个运用过的缓冲区实例并为每一帧创立一个新的实例是贵重且糟蹋的。 相反,如下所示,

设置 App 中的缓冲区实例 (_vertexBuffers)为能够循环运用的先进先出(FIFO)行列,这样便能够额重复运用该行列。 行列中缓冲区实例的最大数量由 MaxFramesInFlight 的值界说,设置为 3:

static const NSUInteger MaxFramesInFlight = 3;

每一帧中,在烘托循环开端时,更新 _vertexBuffer 行列中的下一个缓冲区实例。 您按顺序循环遍历行列,每帧仅更新一个缓冲区实例; 在每三帧结束时,将返回到行列的开头:

// Iterate through the Metal buffers, and cycle back to the first when you've written to the last.
_*currentBuffer = (* _currentBuffer + 1) % MaxFramesInFlight;
// Update buffer data.
[self updateState];

注释:
Core Animation 供给优化的可显示资源,一般称为可制作资源,供你烘托内容并将其显示在屏幕上。
Drawable 是高效但贵重的系统资源,因而 Core Animation 限制了能够在 App 中一起运用的 drawable 的数量。 
默许限制为 3,但能够运用 maximumDrawableCount 属性将其设置为 2(2 和 3 是支撑的值)。
因为可制作目标的最大数量为 3,因而此示例创立了 3 个缓冲区实例。 不需求创立比可用的最大可制作数量更多的缓冲区实例。

办理 CPU 和 GPU 的作业速率

当你有多个缓冲区实例时,你能够让 CPU 用一个实例开端第 n+1 帧的作业,而 GPU 用另一个实例完结第 n 帧的作业。此完结经过使 CPU 和 GPU 一起作业来提高 App 的功率。可是,需求办理 App 的作业速率,防止超出可用缓冲区实例的数量。

要办理 App 的作业速率,需求运用信号量等候全帧完结,以防 CPU 的作业速度比 GPU 快得多。信号量是一个非 Metal 目标,用于控制对跨多个处理器(或线程)共享的资源的拜访。信号量有一个相关的计数值,能够递减或递增该值,标明处理器是已开端仍是已完结对资源的拜访。在 App 中,信号量控制 CPU 和 GPU 对缓冲区实例的拜访。运用 MaxFramesInFlight 的计数值初始化信号量,以匹配缓冲区实例的数量。此值标明 App 在任何给定时刻最多能够一起处理 3 帧:

_inFlightSemaphore = dispatch_semaphore_create(MaxFramesInFlight);

在烘托循环开端时,将信号量的计数值减 1,标明已准备好处理新帧。 当计数值低于 0,信号量会使 CPU 等候,直到添加该值:

dispatch_semaphore_wait(_inFlightSemaphore, DISPATCH_TIME_FOREVER);

在烘托循环结束时,注册一个指令缓冲区完结处理回调。 当 GPU 完结指令缓冲区的履行时,它会调用此回调,并将信号量的计数值添加 1。这标明已完结给定帧的所有作业,能够重用该帧中运用的缓冲区实例:

__block dispatch_semaphore_t block_semaphore = _inFlightSemaphore;
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buffer)
{
  dispatch_semaphore_signal(block_semaphore);
}];

addCompletedHandler(_ :) 方法注册了一个代码块,在 GPU 完结履行相关指令缓冲区后立即调用该代码块。 因为每帧仅运用一个指令缓冲区,因而收到完结回调标明 GPU 已完结该帧。

设置缓冲区的可变性

App 在单个线程上履行所有每帧烘托设置。 首要,运用 CPU 将数据写入缓冲区实例。 之后,编码缓冲区实例的烘托指令。 最终,提交一个指令缓冲区供 GPU 履行。 因为这些任务在单个线程上始终按此顺序履行,因而 App 保证,在对引证缓冲区实例的指令进行编码之前,完结将数据写入缓冲区实例。

此顺序答应将缓冲区实例标记为不可变的。装备烘托管道描述符时,将缓冲区实例索引处的极点缓冲区的 mutability 属性设置为 MTLMutability.immutable:

pipelineStateDescriptor.vertexBuffers[AAPLVertexInputIndexVertices].mutability = MTLMutabilityImmutable;

Metal 能够优化不可变缓冲区的功能,但不能优化可变缓冲区的功能。为获得最佳功能,请尽或许运用不可变缓冲区。

总结

本文论述了形成 CPU 与 GPU 之间的数据依靠的原因,是因为资源共享形成的。介绍如何经过运用资源的多个实例,来防止 CPU 和 GPU 作业之间的等候。

本文示例代码下载