同步CPU和GPU工作
通过使用资源的多个实例,避免CPU和GPU工作之间的停顿。
概述
在此示例代码项目中,您将学习如何管理数据依赖性如何避免CPU和GPU之间的处理器停顿。
该项目将沿着正铉波连续渲染三角形。在每个帧中,样本都会更新每个三角形顶点的位置,然后渲染一个新图像。这些动态数据更新会产生运动的错觉,其中的三角形似乎沿正炫波移动。
该示例将三角形顶点存储在CPU和GPU之间共享的缓冲区中。CPU将数据写入缓冲区,而GPU读取数据。
xcode项目包含用于在macOS,iOS 和 tvOS 上运行示例的方案。默认方案时macOS,它将在mac上运行示例。
了解数据依赖性和处理器停顿的解决方案
资源共享会在处理器之间创建数据依赖关系,CPU必须在GPU读取资源之前完成对资源的写入。如果GPU在CPU写入资源执之前先读取资源,则GPU读取未定义的资源数据。如果在CPU对其进行写入时GPU读取了资源,则GPU会读取不正确的资源数据。

这些数据相关性会在CPU和GPU之间造成处理器停顿。每个处理器必须等另一个处理器完成其工作,然后才能开始自己的工作。
但是,由于CPU和GPU是独立的处理器,因此可以通过使用资源的多个实例使它们同时工作。每个框架都必须为着色器提供相同的参数,但这并不意味着您需要引用相同的资源对象。取而代之的是,您创建一个油多个资源实例组成的池,并在每次渲染框架时使用一个不同的实例。例如,如下所示,CPU可以n+1在GPU从用于frame的缓冲区中读取位置数据的同时,将位置数据写入用于fragme的缓冲区中n。通过使用缓冲区的多个实例,只要保持渲染帧,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[] = {
{{ -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}},
}
}
使用位置和颜色初始化多个三角形顶点,并将它们存储在三角形triangles数组中,
NSMutableArray *triangles = [NSMutableArray alloc] initwithCapacity:NumTriangles];
for (NSUInteger t = 0 ; t < NumTriangles; t++) {
vector_float2 trianglePosition;
trianglePosition.x = ((-((float)NumTriangles)/2.0) + t ) * horizontalSpacing;
trianglePosition.y = 0.0;
AAPLTriangle *triangle = [AAPLTriangle new];
triangle.position = trianglePosition;
triangle.color = Colors[t % NumColors];
[triangles add object:triangle];
}
_triangles = triangles;
分配数据存储
计算三角形顶点的总存储大小,您的应用程序渲染了50个三角形;每个三角形由3个顶点,总共150个顶点,每个顶点的大小为AAPLVertex:
const NSUInteger triangleVertexCount = [AAPLTriangle vertexCount];
_totalVertexCount = triangleVertextCount * _triangles.count;
const NSUInteger triangleVertexBufferSize = _totalVertexCount * sizeof (AAPLVertex);
初始化多个缓冲区以存储顶点数据的多个副本。为每个缓冲区分配恰好足够的内存以存储150个顶点
for (NSUInter bufferIndex = 0; bufferIndex < MaxFramesInFlight; bufferIndex++) {
_vertexBuffers【bufferIndex】= [_device newBufferWithLength:triangleVertexBufferSize options:MTLResourceStorageModeShared];
_vertextBuffers[bufferindex].label = [NSString stringwithformat:@"vertex buffer #%lu",(unsigned long)bufferIndex];
}
初始化后,_vertexBuffers数组中缓冲区实例的内容为空
用CPU更新数据
在draw(in:)渲染循环开始时的每一帧中,使用cpu更新方法updateState中一个缓冲区实例的内容
AAPLVertex *currentTriangleVertices = _vertexBuffers[_currentBuffer].contents;
for(NSUInteger triangle = 0; triangle < NumTriangles; triangle++) {
vector_float2 trianglePosition. =_triangles[triangle].position;
trianglePosition.y = (sin(trianglePosition.x/waveMagnitude + _wavePosition) * waveMagnitude);
_triangles[triangle].positon = trianglePosition;
for (NSUInteger vertex = 0 ; vertex < triangleVertexCount; vertex++) {
NSUInter currentVertex = vertex + (triangle * triangleVertexCount);
currentTriangleVertices[currentVertex].position = triangleVertices[vertex].position;
currnetTriangleVertices[currentvertex].color = _triangles[triangle].color;
}
}
更新缓冲区实例后,在其余的帧中,将不使用CPU访问其数据。
在提交引用它的命令缓冲区之前,必须完成对一个缓冲区实例的所有cpu写操作。否则,GPU可能会在CPU仍在对其进行写入时开始读取该缓冲区实例。
编码GPU命令
接下来,对在渲染过程中引用缓冲区实例的命令进行编码
[renderEncoder setVertexBuffer:_vertexBuffers[_currentBuffer] offset:0 atIndex:AAPLVertexInputIndexVertices];
[renderEncoder setVertexByters:&_viewportSize length:sizeof(_viewportSize) atIndex:AAPLVertexInputIndexViewportSize];
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:_totalVertexCount];
提交并执行GPU命令
在渲染循环结束时,调用命令缓冲区的commit方法将您的工作提交给GPU
[commandBuffer commit];
GPU开始工作,并从顶点着色器中的vertices缓冲区读取数据,该着色器将缓冲区RasterizerData 实例作为输入参数
vertex RasterizerData
vertexShader (const uint vertexID [[vertex_id]],
const device AAPLVertex *vertices [[ buffer(AAPLVertexInputIndexVertices)]],
constant vector_uint2 * viewportSizePointer [[buffer(AAPLVertexInputIndexViewpostSize)]])
在您的应用程序中重用多个缓冲区实例
对余每个帧,如上所述,执行以下步骤,当两个处理器都完成工作时,便完成了整个帧的工作。
-
将数据写入缓冲区实例
-
对应用缓冲区实例的命令进行编码
-
提交包含编码命令的命令缓冲区
-
从缓冲区实例读取数据
当框架的工作完成时,CPU和GPU不再需要该框架中使用的缓冲区实例。但是,丢弃使用的缓冲实例并为每个帧创建一个新的缓冲实例既昂贵又浪费.相反,如下所示,将您的应用设置为循环通过可重复使用的缓冲区实例的先进先出(FIFO)队列。队列中_vertexBuffer缓冲区实例的最大数量由MaxFramesInFlight值定义,设置为3.
static const NSUInteger MaxFramesInFlight = 3;
在渲染循环开始的每一帧中,您将更新队列中的下一个缓冲区实例.您可以按顺序循环浏览队列,并且每帧仅更新一个缓冲区实例。在每三帧的末尾,您将返回队列的开始。
_curentBuffer = (_currentBuffer + 1 ) % MaxFramesInFlight;
[self updateState];
core animation 提供了优化的可显示资源,通常称为可绘制资源,供您渲染内容并将其显示在屏幕上,可绘制对象是高效但昂贵的系统资源,因此core animation 限制了可在应用程序中同时使用的可绘制对象的数量。默认显示为3,但是您可以使用属性将其设置为2(仅2和3是受支持的值)。因为可绘制对象的最大数量为3,所有此示例将创建3个缓冲区实例。您不需要创建比可用的最大可绘制更多的缓冲区实例。
管理CPU和GPU工作率
当您有多个缓冲区实例时,可以使cpu n+1 在一个实例的框架下开始工作,而GPU n在另一个实例的框架下完成工作。此实现通过使CPU和GPU同时工作来提供应用程序的效率,但是,您需要管理应用程序的工作速率,以免超过可用的缓冲区实例数。
要管理应用程序的工作速率,请使用信号量等待全帧完成。以防CPU的运行速度比GPU快得多。信号量时一个非金属对象,可用于控制对在多个处理器(或线程)之间共享的资源的访问。信号量具有一个相关的计数值,您可以对它进行递减或递增,以指示处理器是开始还是完成了对资源的访问。在您的应用中,信号量控制cpu和GPU对缓冲区实例的访问。
您可以使用计数值为MaxFramesInFlight来初始化信号量,以匹配缓冲区实例的数量。此值表示您的应用在任何给定时间最多可以同时处理3个帧。
_inFlightSemaphore = dispath_semaphore_create(MaxFramesFlight);
在渲染循环开始时,您将信号量计数值减1,这表明您已准备好在新帧上工作,但是计数值降至0以下,则信号使CPU等待,直到您增加该值
dispatch_semaphore_wait(_inFlightSemaphore,Dispath_TIME_FORVER);
在渲染循环的末尾,您将注册命令缓冲区完成处理程序,GPU完成命令缓冲区的执行后,它将调用此完成处理程序,并将信号量的计数值增加1。这表明您已完成给定帧的所有工作,并且可以重复使用该帧中使用的缓冲区实例
__block dispath_sempahore_t block_semaphore = _inFlightSempaphore;
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buffer) {
dispatch_semaphore_signal(block_semaphore);
}];
设置缓冲区的可变性
您的应用程序在单个线程上执行所有每帧渲染设置,首先,它使用CPU将数据写入缓冲区实例。之后,它将对应用缓冲区实例的渲染命令进行编码,最后,它提交用于GPU执行的命令缓冲供区,由于这些任务总是按此顺序在单个线程上发生,因此应用程序保证在完成对引用缓冲区实例的命令的编码之前,已完成向缓冲区实例中写入数据。
此顺序使您可以将缓冲区实例标记为不可变的。配置渲染管道描述符时,将mutability 缓冲区实例索引处的顶点缓冲区的属性设置为MTLMutablility.immutable.
pipelineStateDescriptor.vertexBuffers[AAPLVertexInputIndexVertices].mutability = MTLMutabilityIm mutable;
metal 可以优化不可变缓冲区的性能,但不能优化可变缓冲区的性能,为了获得最佳效果,请尽可能使用不可变的缓冲区。
网友评论