从专用渲染线程/循环渲染到 CAMetalLayer

Rendering to CAMetalLayer from dedicated render thread / loop

在 Windows 世界中,专用渲染线程会循环类似这样的内容:

void RenderThread()
{
    while (!quit)
    {
        UpdateStates();
        RenderToDirect3D();
        // Can either present with no synchronisation,
        // or synchronise after 1-4 vertical blanks.
        // See docs for IDXGISwapChain::Present
        PresentToSwapChain();
    }
}

Cocoa 与 CAMetalLayer 的等价物是什么?所有示例都涉及在主线程中完成的更新,使用 MTKView(使用其内部计时器)或在 iOS 示例中使用 CADisplayLink

我想控制整个渲染循环,而不是仅仅在某个非指定时间间隔接收回调(如果启用 V-Sync,最好阻止它)。

在某种程度上,您会受到可绘制对象可用性的限制。 CAMetalLayer 有固定的可绘制对象池,调用 nextDrawable 将阻塞当前线程,直到可绘制对象可用。不过,这并不意味着您必须在渲染循环的顶部调用 nextDrawable

如果你想按照自己的时间表绘制而不被阻塞等待可绘制对象,渲染到屏幕外渲染缓冲区(即尺寸与可绘制对象大小匹配的 MTLTexture),然后从最近绘制的纹理到可绘制对象的纹理,并以您喜欢的任何节奏呈现。这对于获取帧计时很有用,但是您绘制然后不显示的每一帧都是浪费工作。它还会增加颤抖的风险。

在获取与垂直同步节奏相匹配的回调方面,您的选择是有限的。你最好的几乎肯定是在默认和跟踪 运行 循环模式中安排的 CVDisplayLink,尽管它有 caveats.

如果你想释放 -运行 又不会太超前,你可以使用类似计数信号量和显示器的东西 link。

如果您的应用程序能够保持实时帧速率,您通常会在玻璃上发生的事情之前渲染一两帧,所以您不想在垂直同步上真正阻塞;您只想通知 window 服务器您希望演示文稿与垂直同步相匹配。在 macOS 上,您可以通过将图层的 displaySyncEnabled 设置为 true(默认值)来执行此操作。关闭此功能可能会导致某些显示画面撕裂。

在您想要渲染到屏幕的位置,您可以通过调用 nextDrawable 从图层中获取可绘制对象。您可以从 texture 属性 中获取可绘制对象的纹理。您使用该纹理来设置 MTLRenderPassDescriptor 的渲染目标(颜色附件)。例如:

id<CAMetalDrawable> drawable = layer.nextDrawable;
id<MTLTexture> texture = drawable.texture;
MTLRenderPassDescriptor *desc = [MTLRenderPassDescriptor renderPassDescriptor];
desc.colorAttachments[0].texture = texture;

从这里开始,它与您在 MTKViewdrawRect: 方法中所做的非常相似。你创建一个命令缓冲区(如果你还没有),使用描述符创建一个渲染命令编码器,编码绘图命令,结束编码,告诉命令缓冲区呈现可绘制对象(使用 -presentDrawable:... 方法) , 并提交命令缓冲区。绘制到可绘制对象的纹理上的任何内容都会在呈现时最终出现在屏幕上。

我同意 Warren 的观点,您可能真的不想将循环与显示刷新同步。你想要并行性。您希望 CPU 在 GPU 渲染最新帧(并且显示器显示最后一帧)时处理下一帧。

事实上,有多少 drawables 可以一次飞行并且 nextDrawable 将阻止等待一个,这将防止您的渲染循环提前太多。 (在那之前你可能会使用一些其他的同步,比如管理一个小的缓冲区池。)如果你只想要双缓冲而不是三缓冲,你可以将层的 maximumDrawableCount 设置为 2 而不是它的默认值为 3。