如何在进程间同步MTLTexture和IOSurface?

6

当我在一个XPC进程中写入IOSurface,并且同时被用作主应用程序中MTLTexture的后备存储时,我需要使用哪些API,并需要注意什么预防措施?

在我的XPC服务中,我有以下内容:

IOSurface *surface = ...;
CIRenderDestination *renderDestination = [... initWithIOSurface:surface];

// Send the IOSurface to the client using an NSXPCConnection.
// In the service, periodically write to the IOSurface.

在我的应用程序中,我有以下内容:

IOSurface *surface = // ... fetch IOSurface from NSXPConnection.
id<MTLTexture> texture = [device newTextureWithDescriptor:... iosurface:surface];

// The texture is used in a fragment shader (Read-only)

我有一个正在运行正常更新循环的MTKView。我希望我的XPC服务能够定期使用Core Image写入IOSurface,然后由应用程序端的Metal渲染新内容。
需要什么同步来确保这样做?双重或三重缓冲策略是一种,但对我来说并不适用,因为我可能没有足够的内存来分配2倍或3倍数量的表面。(上面的示例仅使用一个表面以清晰明了为例,但实际上我可能有数十个表面正在绘制。每个表面表示图像的一个瓷砖。图像可以像JPG / TIFF /等允许的那样大。) WWDC 2010-442谈到了IOSurface,并简要提到所有这些都“只是起作用”,但这是在OpenGL的上下文中,并没有提到Core Image或Metal。
最初,我认为Core Image和/或Metal将调用IOSurfaceLock()IOSurfaceUnlock()来保护读写访问,但实际上似乎并非如此。 (并且IOSurfaceRef.h头文件中的注释表明锁定仅用于CPU访问。) 如果我真的可以让Core Image的CIRenderDestination任意写入IOSurface,而我在应用程序的更新循环中从相应的MTLTexture读取,那么这是否可能?如果是这样,那么如果绑定到IOSurface的所有纹理共享同一视频内存,那么如何做到这一点,正如WWDC视频所述,如果在同一次传递期间发生读取和写入,那么表面的内容就会出现撕裂。
1个回答

4
你需要做的是确保在应用程序中使用 IOSurface 进行绘图之前,CoreImage 的绘制已经完成。如果两边都使用 OpenGL 或者 Metal,你需要调用 glFlush()[-MTLRenderCommandEncoder waitUntilScheduled]。我猜测 CoreImage 中的某个东西正在进行这方面的调用。
如果没有正确同步,会出现撕裂或者半新半旧的图像,这种情况很明显。我曾经在 XPC 之间使用 IOSurface 时遇到过类似问题。
你可以在 -waitUntilScheduled-waitUntilCompleted 上设置一些符号断点,看看 CI 是否在你的 XPC 上调用了它们(假设 文档 没有明确告诉你)。Metal 中还有其他同步原语,但我不太熟悉。它们可能也很有用。(据我所知,CI 现在全部都是用 Metal 实现的。)
此外,IOSurface对象具有-incrementUseCount-decrementUseCount-localUseCount方法。值得检查这些方法是否被CI适当地设置。(有关详细信息,请参见<IOSurface/IOSurfaceObjC.h>。)

CIRenderTask waitUntilCompletedAndReturnError 会确保 Core Image 完成,但使用它需要在启动 Core Image 渲染时以某种方式阻止应用程序中的 Metal 渲染循环,然后在完成后取消阻止渲染循环。苹果的示例代码使用两个 IOSurface 来实现这一点,并要求服务不断地告诉应用程序可以从哪个表面读取。我想避免双重或三重缓冲以减少内存开销。MTLSharedEventHandle 看起来很有前途,但它只适用于 10.14+,而且文档非常薄弱... - kennyc
从我所了解的情况来看,“IOSurface useCount”似乎与您可以对表面执行的操作无关。Core Image和Metal似乎能够在不考虑“useCount”值的情况下读取和写入表面。我发现唯一依赖于“useCount”的是“CVPixelBufferPoolRef”,它使用“useCount”的值来确定何时可以安全地回收基于“IOSurface”的像素缓冲区。 - kennyc
听起来你真正需要的是更好的内存管理。为什么需要将如此大的图像保留在内存中?(这是macOS还是iOS?)您是否需要将整个图像保留在一个连续的块中,或者可以将这些大型图像分成瓦片,以便可以对瓦片进行合理的缓存? - user1118321
macOS上,XPC服务有一个单独的CIImage,可以表示非常大的图像。IOSurfaceMTLTexture都限制在每边约16,384个像素,因此我通过创建多个表面来平铺图像,然后使用Core Image的能力将特定区域渲染到瓷砖表面上。有足够的表面加载到内存中以表示完整分辨率的图像。当我需要在应用程序的MTKView中绘制其中一个或多个表面时,我将它们包装在Metal纹理中,并复制到可绘制对象中。这已被证明比使用Core Image实时绘制瓷砖要快得多。 - kennyc
你考虑过使用mip-mapping吗?如果你没有绘制整个图像,你可以只在VRAM中存储你正在绘制的部分(也许还要加上一些额外的来预测变换)。如果你正在绘制整个图像,你不需要完整的分辨率,因为你用户的显示器无法容纳它。你可以使用更高的mipmap级别,甚至不包括基础图像,以节省大量内存。 - user1118321
我在片段着色器中使用mipmap偏移,具体取决于哪个细节级别是最新的。CIImage(带有滤镜)首先以较低的分辨率呈现,然后在第二次呈现时以全分辨率呈现。XPC服务为其可以呈现的每个“mipmap”级别设置了一个瓷砖集。在应用程序中,我只需将该表面的内容复制到适当的mipmap切片纹理中。为了实现画布的最佳平移和缩放,我希望尽可能多地准备好图像,这取决于系统内存,因此希望分配如此多的表面。 - kennyc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接