Metal中如何等待进程内GPU操作完成?共享缓冲区同步方案
我正在为MacOS平台的一款应用编写图像处理插件(共享库),宿主应用会传递给我一个需处理的MTLBuffer。我通过blitEncoder将该缓冲区复制到纹理中,随后运行访问该纹理的内核。但有时会从缓冲区获取到错误的纹理数据,若在复制前休眠30毫秒则能100%解决问题,但会严重影响性能。
由于无法访问宿主应用的闭源代码,无法获取其处理该缓冲区所用的命令缓冲区,也就无法添加栅栏、同步机制或完成处理程序,仅能操作该缓冲区本身。该缓冲区处于MTLStorageModeShared模式,信息如下:
MTLBuffer(len=134217728, contents=0x43e078000, '<AGXG13XFamilyBuffer: 0x2a0a9bf10> label = GF Metal Memory Pool Buffer length = 134217728 cpuCacheMode = MTLCPUCacheModeDefaultCache storageMode = MTLStorageModeShared hazardTrackingMode = MTLHazardTrackingModeTracked resourceOptions = MTLResourceCPUCacheModeDefaultCache MTLResourceStorageModeShared MTLResourceHazardTrackingModeTracked purgeableState = MTLPurgeableStateNonVolatile')
我尝试过栅栏、事件等方案,也调用过blitEncoder->synchronizeResource(buffer);,但因缓冲区非托管模式,该操作无效。请问是否有方法等待缓冲区就绪(即其上所有待处理操作完成),或添加进程级全局GPU栅栏以等待所有GPU操作完成?
相关代码:
// Create encoder for blitting to the GPU texture MTL::BlitCommandEncoder* blitEncoder = commandBuffer->blitCommandEncoder(); // This fixes it, at a high cost in performance: // std::this_thread::sleep_for(std::chrono::milliseconds(30)); const MTL::Origin origin = {0, 0, 0}; const MTL::Size size = {width, height, 1}; // Match the texture's dimensions blitEncoder->copyFromBuffer(buffer, 0, // buffer, source offset rowbytes, // source bytes/row 0, // source bytes/image (0 for 2d) size, // sourceSize, texture, // dest texture 0, // slice 0, // level origin // dest origin ); blitEncoder->endEncoding();
1. 利用Shared模式的隐式同步(推荐)
对于MTLStorageModeShared的缓冲区,CPU可以通过didModifyRange方法强制等待GPU完成所有针对该缓冲区的待处理写入操作。这个方法是官方针对Shared资源设计的同步手段,比固定休眠高效得多,因为它是按需等待,而非固定时长。
修改代码如下:
MTL::BlitCommandEncoder* blitEncoder = commandBuffer->blitCommandEncoder(); // 等待GPU完成对整个缓冲区的所有写入操作 buffer->didModifyRange(NS::Range::Make(0, buffer->length())); const MTL::Origin origin = {0, 0, 0}; const MTL::Size size = {width, height, 1}; blitEncoder->copyFromBuffer(buffer, 0, rowbytes, 0, size, texture, 0, 0, origin); blitEncoder->endEncoding();
2. 借助BlitEncoder触发同步
如果didModifyRange效果不理想,可以尝试在复制操作前,通过BlitEncoder执行一个空操作或者显式同步指令,强制驱动处理完缓冲区的pending任务:
MTL::BlitCommandEncoder* blitEncoder = commandBuffer->blitCommandEncoder(); // 执行空复制操作触发同步(复制0字节,仅用于同步信号) blitEncoder->copyFromBuffer(buffer, 0, buffer, 0, 0); // 或者尝试显式同步(部分驱动可能支持Shared模式下的该指令) // blitEncoder->synchronizeResource(buffer); const MTL::Origin origin = {0, 0, 0}; const MTL::Size size = {width, height, 1}; blitEncoder->copyFromBuffer(buffer, 0, rowbytes, 0, size, texture, 0, 0, origin); blitEncoder->endEncoding();
3. 全局GPU队列等待(极端场景)
如果以上方法都无效,可以等待当前设备默认队列的所有任务完成,相当于全局GPU栅栏。这个方法会影响性能,但比固定休眠精准:
// 获取当前设备的默认命令队列 MTL::CommandQueue* queue = device->defaultCommandQueue(); // 创建空命令缓冲区并等待其完成,确保之前的GPU任务都执行完毕 MTL::CommandBuffer* syncBuffer = queue->commandBuffer(); syncBuffer->commit(); syncBuffer->waitUntilCompleted(); // 之后执行你的复制操作 MTL::BlitCommandEncoder* blitEncoder = commandBuffer->blitCommandEncoder(); const MTL::Origin origin = {0, 0, 0}; const MTL::Size size = {width, height, 1}; blitEncoder->copyFromBuffer(buffer, 0, rowbytes, 0, size, texture, 0, 0, origin); blitEncoder->endEncoding();
注意事项
- 优先使用
didModifyRange,这是Metal针对Shared资源同步的原生方案,性能最优。 - 绝对避免固定时长休眠,所有同步操作都应基于事件触发,而非时间预估,才能在保证正确性的同时最大化性能。
内容的提问来源于stack exchange,提问作者GaryO

