CPU and GPU Synchronization - CPU和GPU同步
演示如何更新缓冲区数据并同步CPU和GPU之间的访问。
在此示例中,您将学习如何正确更新和渲染CPU与图形处理单元(GPU)之间共享的动画资源。 特别是,您将学习如何修改每帧数据,避免数据访问危险,并且并行执行CPU和GPU工作。
CPU/GPU Parallelism and Shared Resource Access - CPU / GPU并行和共享资源访问
CPU和GPU是独立的异步处理器。在Metal应用程序或游戏中,CPU对命令进行编码,GPU执行命令。在每个帧中重复该序列,并且当CPU和GPU都完成其工作时完成全帧的工作。 CPU和GPU可以并行工作,无需等待彼此完成工作。例如,GPU可以执行帧1的命令,而CPU对帧2的命令进行编码。
这种CPU / GPU并行性对于您的Metal应用程序或游戏具有很大的优势,有效地使您可以同时在两个处理器上运行。但是,这些处理器仍然可以协同工作,并且通常可以访问相同的共享资源,例如顶点缓冲区或片段纹理。必须小心处理共享资源访问;否则,CPU和GPU可能同时访问共享资源,从而导致竞争条件和数据损坏。
与大多数Metal应用或游戏一样,此示例通过更新每个帧中的顶点数据来渲染动画内容。请考虑以下顺序:
- 1) 渲染循环启动一个新帧。
- 2) CPU将新顶点数据写入顶点缓冲区。
- 3) CPU对渲染命令进行编码并提交命令缓冲区。
- 4) GPU开始执行命令缓冲区。
- 5) GPU从顶点缓冲区读取顶点数据。
- 6) GPU将像素渲染到drawable。
- 7) 渲染循环完成帧。
在此序列中,CPU和GPU共享一个顶点缓冲区。 如果处理器在开始自己的工作之前等待彼此完成工作,则共享顶点缓冲区没有访问冲突。 此模型避免了访问冲突,但浪费了宝贵的处理时间:当一个处理器正在工作时,另一个处理器处于空闲状态。
Metal旨在最大化CPU和GPU并行性,因此这些处理器应该保持忙碌并且应该同时工作。 理想情况下,GPU应该读取帧1的顶点数据,而CPU正在为帧2写入顶点数据。但是,共享单个顶点缓冲区意味着CPU可以在GPU读取之前覆盖前一帧的顶点数据,从而导致难看的渲染效果。
为了减少处理器空闲时间并避免访问冲突,可以使用多个缓冲区而不是单个缓冲区来共享顶点数据。 例如,CPU和GPU可以共享帧1的顶点缓冲区1,帧2的顶点缓冲区2,帧3的顶点缓冲区3,等等。 在此模型中,共享顶点数据在每个帧中保持一致,并且处理器同时访问不同的顶点缓冲区。
Implement a Triple Buffer Model - 实现三重缓冲模型
此示例呈现数百个小四边形,也称为sprites
。要为精灵设置动画,样本会在每个帧的开头更新它们的位置,并将它们写入顶点缓冲区。帧完成后,CPU和GPU不再需要该帧的顶点缓冲区。丢弃使用过的顶点缓冲区并为每个帧创建一个新的顶点缓冲区是浪费的。相反,可以使用可重用顶点缓冲区的FIFO队列实现更可持续的模型。
队列中的最大缓冲区数由MaxBuffersInFlight
的值定义,设置为3
。此常量值定义设备的任何部分可以同时处理的最大帧数;这包括应用程序,驱动程序或显示。该应用程序仅适用于CPU或GPU中的框架,但OS本身可以在驱动程序或显示级别的框架上工作。使用三个缓冲区可为设备提供足够的余地,以便高效且有效地工作。缓冲区太少会导致处理器停顿和资源争用,而缓冲区太多会导致内存开销和帧延迟增加。
for(NSUInteger bufferIndex = 0; bufferIndex < MaxBuffersInFlight; bufferIndex++)
{
_vertexBuffers[bufferIndex] = [_device newBufferWithLength:spriteVertexBufferSize
options:MTLResourceStorageModeShared];
}
在drawInMTKView:
渲染循环开始时,示例遍历_vertexBuffer
数组中的每个缓冲区,每帧只更新一个缓冲区。 在每三个帧结束时,在使用所有三个缓冲区之后,样本循环回到数组的开始并更新_vertexBuffer [0]
缓冲区的内容。
Manage the Rate of Work - 管理工作率
为避免过早覆盖数据,样本必须确保GPU在重新使用之前已处理缓冲区的内容。 否则,CPU可能会覆盖先前三帧写入但尚未被GPU读取的顶点数据。 当CPU为GPU生成工作的速度超过GPU完成工作时,就会出现这种情况。
如果CPU在GPU之前运行得太远,则此示例使用信号量等待全帧完成。
_inFlightSemaphore = dispatch_semaphore_create(MaxBuffersInFlight);
在渲染循环开始时,信号量检查进行或等待信号。 如果可以使用或重用缓冲区,则CPU工作继续进行;否则,它等待缓冲区可用。
dispatch_semaphore_wait(_inFlightSemaphore, DISPATCH_TIME_FOREVER);
GPU无法直接发信号通知信号,但它可以向CPU发出完成回调。
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buffer)
{
dispatch_semaphore_signal(block_sema);
}];
addCompletedHandler:
方法注册在GPU完成命令缓冲区后立即调用的代码块。 此命令缓冲区与为帧提交顶点缓冲区的命令缓冲区相同,因此接收完成回调指示可以安全地重用顶点缓冲区。