什么时候可以安全地重写和重用MTLBuffer或其他Metal顶点缓冲区?

问题描述 投票:0回答:1

我刚开始接触Metal,但在掌握一些基本知识时遇到了麻烦。我已经阅读了很多有关Metal的网页,并浏览了Apple的示例等等,但是我的理解仍然存在空白。我认为我的主要困惑是:处理顶点缓冲区的正确方法是什么,如何知道何时可以安全地重用它们?正如我将在下面描述的那样,这种混乱以多种方式表现出来,也许我的混乱的那些不同表现需要以不同的方式解决。

[更具体地说,我在macOS上的Objective-C中使用MTKView子类来显示非常简单的2D形状:该视图的整体框架内部为背景色,该框架内部的0+矩形子帧具有不同的它们内部的背景色,然后在每个子帧内添加0+种各种颜色的阴影正方形。我的顶点函数只是一个简单的坐标转换,而我的片段函数只是基于苹果的三角演示应用程序通过它接收的颜色。对于具有单个正方形的单个子帧,我可以正常工作。到目前为止一切顺利。

有几件事使我感到困惑。

[一个:我可以设计代码,用一个顶点缓冲区和一个对drawPrimitives:的调用来渲染整个对象,一口气绘制所有(子)帧和正方形。但是,这并不是最佳选择,因为它破坏了我的代码的封装,其中每个子帧表示一个对象(包含0+正方形的对象)的状态;我想允许每个对象负责绘制自己的内容。因此,最好让每个对象都设置一个顶点缓冲区并进行自己的drawPrimitives:调用。但是由于对象将按顺序绘制(这是一个单线程应用程序),所以我想在所有这些绘制操作中重用相同的顶点缓冲区,而不是让每个对象都必须分配并拥有一个单独的顶点缓冲区。但是我可以吗?调用drawPrimitives:之后,我想必须将顶点缓冲区的内容复制到GPU上,并且我认为(?)不能同步完成,因此立即开始修改顶点缓冲区并不安全用于下一个对象的绘图。所以:我怎么知道何时用缓冲区完成Metal,然后可以再次对其进行修改?

[二:即使#1有一个明确定义的答案,这样我就可以阻塞直到Metal用完缓冲区,然后为下一个drawPrimitives:调用开始对其进行修改,这是否合理?我想这意味着我的CPU线程将反复阻塞以等待内存传输,这不是很好。那么,这是否将我推向每个对象都有自己的顶点缓冲区的设计?

三:好吧,假设每个对象都有自己的顶点缓冲区,或者我用一个大顶点缓冲区对整个对象进行一次“大爆炸”渲染(我认为这个问题适用于两种设计)。在我在命令缓冲区上调用presentDrawable:然后又调用commit之后,我的应用程序将关闭并做一些工作,然后尝试更新显示,因此现在我的绘图代码再次执行。我想重用之前分配的顶点缓冲区,覆盖其中的数据以进行新的更新显示。但是再次:我怎么知道什么时候安全?据我了解,commit返回我的代码这一事实并不意味着Metal已经完成了将我的顶点缓冲区复制到GPU的工作,并且在一般情况下,我不得不假设这可能会花费任意长时间,因此重新输入绘图代码时可能尚未完成。正确的说法是什么?再说一次:我应该只是等待直到它们可用时才等待(但是我应该这样做),还是应该有第二组顶点缓冲区,以防万一Metal仍在忙于第一组顶点缓冲区? (这似乎将问题推到了最后,因为当输入我的绘画代码进行third更新时,以前使用的两个缓冲区都可能不可用,对吧?因此,我可以添加第三一组顶点缓冲区,然后fourth更新...)

四:为了绘制框架和子帧,我只想编写一个每个人都可以调用的可重用的“ drawFrame”类型的函数,但是我对正确的设计感到有些困惑。使用OpenGL,这很容易:

- (void)drawViewFrameInBounds:(NSRect)bounds
{
    int ox = (int)bounds.origin.x, oy = (int)bounds.origin.y;

    glColor3f(0.77f, 0.77f, 0.77f);
    glRecti(ox, oy, ox + 1, oy + (int)bounds.size.height);
    glRecti(ox + 1, oy, ox + (int)bounds.size.width - 1, oy + 1);
    glRecti(ox + (int)bounds.size.width - 1, oy, ox + (int)bounds.size.width, oy + (int)bounds.size.height);
    glRecti(ox + 1, oy + (int)bounds.size.height - 1, ox + (int)bounds.size.width - 1, oy + (int)bounds.size.height);
}

但是对于Metal,我不确定什么是好的设计。我猜该函数不能只是将自己的小顶点缓冲区声明为局部静态数组,将其抛出顶点然后调用drawPrimitives:,因为如果连续两次调用它,则Metal可能尚未复制该顶点当第二个调用要修改缓冲区时,来自第一个调用的数据。我显然不想每次调用该函数时都分配一个新的顶点缓冲区。我可以让调用者传入顶点缓冲区以供该函数使用,但这只是将问题推到了一个水平。那么,呼叫者应如何处理这种情况?也许我可以让函数将新顶点追加到调用方提供的缓冲区中不断增长的顶点列表的末尾;但这似乎迫使整个渲染都经过完全预先计划(以便我可以预先分配一个适当大小的大缓冲区,以适合所有人将要绘制的所有顶点–这需要顶层绘制代码以某种方式知道每个对象最终都会生成许多顶点(这违反了封装),或者要进行设计,其中我有一个扩展的顶点缓冲区,当其容量证明不足时,可以根据需要重新分配顶点缓冲区。我知道该怎么做;但他们都觉得不对。我在为正确的设计而苦苦挣扎,因为我认为我对Metal的内存模型还不够了解。有什么建议吗?很长的多部分问题表示歉意,但我认为所有这些都归因于相同的基本缺乏理解。

objective-c macos metal
1个回答
0
投票

对您的基本问题的简短回答是:在该命令缓冲区完成之前,您不应覆盖添加到命令缓冲区的命令所使用的资源。确定的最佳方法是添加完成处理程序。您也可以轮询命令缓冲区的status属性,但是效果不佳。

首先,在提交命令缓冲区之前,不会将任何内容复制到GPU。此外,正如您指出的那样,即使在提交命令缓冲区之后,也不能假定数据已完全复制到GPU。

第二,在简单情况下,应将一帧的所有图形放入单个命令缓冲区。创建和提交大量命令缓冲区(例如,每个绘制的对象都需要一个缓冲区)会增加开销。

将这两点结合起来通常意味着您通常无法在同一帧内重复使用资源。基本上,您将必须使用双缓冲区或三缓冲区来同时获得正确性和良好的性能。

一种典型的技术是创建一个由信号灯保护的小型缓冲区池。信号量计数最初是池中的缓冲区数。想要缓冲区的代码在信号量上等待,成功后,将缓冲区从池中取出。它还应在命令缓冲区中添加一个完成处理程序,该处理程序将缓冲区放回池中并发出信号量。

可以使用动态缓冲池。如果代码需要缓冲区,并且池为空,则它将创建缓冲区而不是阻塞。然后,完成后,它将缓冲区添加到池中,从而有效地增加了池的大小。但是,这样做通常没有任何意义。如果CPU在GPU之前运行,则只需要三个以上的缓冲区,就没有真正的好处。

关于您希望每个对象都绘制自己的愿望,当然可以做到。我将使用一个大的顶点缓冲区以及一些有关到目前为止已使用了多少元数据的元数据。每个需要绘制的对象都会将其顶点数据附加到缓冲区中,并对引用该顶点数据的绘制命令进行编码。您可以使用vertexStart参数使绘图命令在顶点缓冲区中引用正确的位置。

您还应考虑使用图元重新启动值进行索引绘制,因此只有一个绘制命令可以绘制所有图元。每个对象都会将其图元添加到共享的顶点数据和索引缓冲区中,然后由一些高级控制器来绘制。

© www.soinside.com 2019 - 2024. All rights reserved.