我刚开始学习Metal,在掌握一些基本概念上遇到了问题。我已经阅读了很多关于Metal的网页,并按照苹果的例子进行实践,但我的理解仍然存在漏洞。 我认为我最困惑的点是:处理顶点缓冲区的正确方法是什么,以及何时可以安全地重复使用它们?这种困惑有几个方面,我将在下面描述,也许这些不同方面的困惑需要用不同的方式解决。
具体来说,我正在macOS上使用Objective-C中的MTKView子类显示非常简单的2D形状:视图的整体框架带有内部背景颜色,其内部包含0个以上的矩形子框架,每个子框架内部有不同的背景颜色,然后每个子框架内还有各种颜色的扁平着色正方形。 我的顶点函数只是一个简单的坐标转换,我的片段函数只是根据Apple的三角形演示应用程序传递接收到的颜色。 对于仅具有单个正方形的单个子框架,我已将其正常工作。 到目前为止都很好。
有几件事情让我感到困惑。
一:我可以设计我的代码使用单个顶点缓冲区和单个
二:即使#1有明确定义的答案,可以阻塞直到Metal完成缓冲区,然后开始修改以进行下一个
三:好的,假设每个对象都有它自己的顶点缓冲区,或者我使用一个大的顶点缓冲区进行一次“大爆发”渲染整个物体(我认为这个问题适用于这两种设计)。在我调用
四:为了绘制帧和子帧,我想编写一个可重复使用的“drawFrame”类型函数,让每个人都可以调用,但是我有些困惑应该如何设计。OpenGL很容易:
具体来说,我正在macOS上使用Objective-C中的MTKView子类显示非常简单的2D形状:视图的整体框架带有内部背景颜色,其内部包含0个以上的矩形子框架,每个子框架内部有不同的背景颜色,然后每个子框架内还有各种颜色的扁平着色正方形。 我的顶点函数只是一个简单的坐标转换,我的片段函数只是根据Apple的三角形演示应用程序传递接收到的颜色。 对于仅具有单个正方形的单个子框架,我已将其正常工作。 到目前为止都很好。
有几件事情让我感到困惑。
一:我可以设计我的代码使用单个顶点缓冲区和单个
drawPrimitives:
调用来呈现整个内容,以一次大规模的操作绘制所有(子)框架和正方形。然而,这并不是最优的,因为它会破坏我的封装代码,其中每个子框架代表一个对象的状态(包含0个或多个正方形的内容)。我希望允许每个对象负责绘制其自己的内容。因此,最好让每个对象设置一个顶点缓冲区,并进行自己的drawPrimitives:
调用。但是,由于对象将按顺序绘制(这是单线程应用程序),因此我希望跨所有这些绘图操作重复使用相同的顶点缓冲区,而不是让每个对象分配和拥有单独的顶点缓冲区。但是我能做到吗?在调用drawPrimitives:
之后,我想缓冲区的内容必须被复制到GPU上,并且我认为(?)这不是同步完成的,因此不能立即开始修改顶点缓冲区以进行下一个对象的绘制。那么:如何知道Metal何时完成缓冲区并且可以再次开始修改?二:即使#1有明确定义的答案,可以阻塞直到Metal完成缓冲区,然后开始修改以进行下一个
drawPrimitives:
调用,这是否是合理的设计?我想这意味着我的CPU线程会不断阻塞等待内存传输,这并不好。 那么,这是否基本上将我推向每个对象都有自己的顶点缓冲区的设计?三:好的,假设每个对象都有它自己的顶点缓冲区,或者我使用一个大的顶点缓冲区进行一次“大爆发”渲染整个物体(我认为这个问题适用于这两种设计)。在我调用
presentDrawable:
并在我的命令缓冲区上调用commit
之后,我的应用程序将会做一些工作,然后会尝试更新显示,因此我的绘图代码现在再次执行。我想重复使用之前分配的顶点缓冲区,在其中覆盖数据以进行新的、更新的显示。但是再次强调:我怎么知道这是安全的呢?据我所知,commit
返回到我的代码中并不意味着Metal已经完成将我的顶点缓冲区复制到GPU的工作,而且在一般情况下,我必须假定它可能需要任意长的时间,因此当我重新输入我的绘图代码时,它可能还没有完成。正确的方法是什么?还有:我只是阻塞等待它们可用(无论我应该如何做),还是应该有第二组顶点缓冲区,以防Metal仍然忙于第一组?(这似乎只是把问题推到更远的未来,因为当第三次更新时,我的绘图代码被输入时,前面使用的两组缓冲区可能还没有可用,对吧?那么我可以添加第三组顶点缓冲区,但是第四次更新...)四:为了绘制帧和子帧,我想编写一个可重复使用的“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的内存模型理解不够充分。有什么建议吗?非常抱歉问题很长,但我认为所有这些都涉及到同一个基本缺乏理解。
DISPATCH_DECL(dispatch_semaphore);
,然后转到#define DISPATCH_DECL(name) OS_OBJECT_DECL_SUBCLASS(name, dispatch_object)
,以此类推。看起来它最终确实会转换为NSObject; 但这是否有文档记录和保证呢? - bhallerdispatch_release()
的文档中的注释。此外,块捕获引用变量并执行必要的内存管理。因此,该块会保留信号量(或self
,具体取决于访问方式),而命令缓冲区在将其添加为完成处理程序后会保留该块。 - Ken Thomases