剖析Apple M1 GPU,第二部分

2021-01-23 02:15:32

不到一个月前,我开始研究Apple M1 GPU,希望开发出免费的开源驱动程序。本周,我达到了第二个里程碑:用我自己的开源代码绘制一个三角形。顶点着色器和片段着色器是用机器代码手写的,我通过IOKit内核驱动程序以与系统的Metal用户空间驱动程序相同的方式与硬件连接。

新代码的大部分负责构建共享内存中的各种命令缓冲区和描述符,这些命令缓冲区和描述符用于控制GPU的行为。 Metal可以访问的任何状态都对应于这些缓冲区中的位,因此理解它们将是下一个主要任务。到目前为止,我较少关注内容,而是更关注它们之间的联系。特别是,这些结构包含指向彼此的指针,有时相互嵌套多层。该项目三角形的启动过程可以让您鸟瞰存储器中所有这些不同的部分如何组合在一起。

例如,应用程序提供的顶点数据位于它们自己的缓冲区中。另一个缓冲区中的内部表指向这些顶点缓冲区中的每一个。该内部表将作为输入直接传递给在另一个缓冲区中指定的顶点着色器。顶点着色器的描述(包括可执行内存中代码的地址)由另一个缓冲区指向,该缓冲区本身是从主命令缓冲区引用的,而该缓冲区由IOKit调用中的句柄引用以提交命令缓冲区。 ew!

换句话说,该演示代码并非旨在演示对命令缓冲区细粒度细节的理解,而是演示“不存在任何遗漏”。由于GPU的虚拟地址在运行之间会发生变化,因此该演示验证了所需的所有指针是否均已识别,并可以使用我们自己的(简单的)分配器在内存中自由地重新分配。由于在macOS上的内存和命令缓冲区分配方面存在一些“魔术”,因此使此代码在早期阶段工作可让您放心。

我采用了零星的抚养程序。由于我的IOKit包装器与Metal应用程序存在于相同的地址空间中,因此包装器可能会在提交给GPU之前修改命令缓冲区。作为早期的“ hello world”,我确定了内存中渲染目标的透明颜色的编码,并演示了可以根据需要修改颜色。同样,在学习了用于启动反汇编程序的指令集时,我用手写的等效项替换了着色器,并确认只要可以写出机器代码,就可以在GPU上执行代码。但是不必在系统的这些“叶子节点”处停止;修改着色器代码后,我尝试将着色器代码上传到可执行缓冲区的其他部分,同时修改命令缓冲区的代码指针以进行补偿。之后,我可以尝试自己上传着色器的命令。以这种方式进行迭代,我可以构建所需的每个结构,同时分别测试每个结构。

尽管有曲线球,但此过程比直接跳到构造缓冲区(可能通过“重播”)的替代方案要好得多。几年前,我曾使用过这种替代技术来启动Mali,但是它带来了非常困难的调试工作的实质性缺点。如果五百行幻数中有一个错字,除了GPU的错误之外,将没有反馈。但是,一次只工作一点,就可以立即查明并修复错误,从而提供更快的周转时间和更愉快的提拔体验。

但是那里有弯球!当我尝试为颜色分配缓冲区时,我在修改透明颜色上的一时兴高采烈消失了。尽管编码与以前相同,但GPU仍无法正确清除。我想知道修改指针的方式是否有问题,我尝试将颜色放置在Metal驱动程序已经创建的未使用的内存部分中,并且有效。内容是一样的,我修改指针的方式是一样的,但是GPU不喜欢我的内存分配。我想知道我分配内存的方式是否有问题,但是包装所证实的,我用来调用内存分配IOKit调用的参数与Metal使用的参数位相同。我最后的努力是检查是否必须通过某些辅助通道(例如mmap系统调用)显式映射GPU内存。 IOKit确实具有与设备无关的内存映射调用,但是没有发现任何增强的跟踪来发现任何旁通道系统调用映射的迹象。

麻烦正在酝酿中。在花费大量时间追寻“不可能的”错误之后,我感到很疯狂,我想知道系统调用中是否没有某些“魔术”……而是GPU内存本身。这是一个愚蠢的理论,因为如果成立,它将产生严重的“鸡与蛋”问题:如果必须由另一个GPU分配来祝福一个GPU分配,谁来祝福第一个分配?

但是我感到很愚蠢,甚至感到绝望,因此我继续尝试通过在应用程序流的中间插入一个内存分配调用来测试该理论,以便每个后续分配都将位于不同的地址。在此更改之前和之后倾销GPU内存并检查差异,这揭示了我的第一个恐怖:GPU内存中的辅助缓冲区跟踪了所有必需的分配。特别是,我注意到此缓冲区中的值以可预测的偏移量(每个0x40字节)增加了1,这表明该缓冲区包含一个分配句柄的数组。实际上,这些值恰好对应于GPU内存分配调用中从内核返回的句柄。

撇开该理论的明显问题,我还是进行了测试,修改了此表以在新分配的句柄末尾包含一个额外的条目,并修改了标题数据结构以使条目数增加一个。仍然没有骰子。尽管令人沮丧,但这并没有完全淹没该理论。实际上,我注意到了条目的一些特殊之处:与我的想法相反,并非所有条目都对应有效的句柄。不,除最后一个条目外,所有其他条目均有效。内核的句柄是1索引的,但是在每个内存转储中,最终句柄始终为0,不存在。也许这就像一个哨兵值,类似于C中以NULL终止的字符串。这种解释引出了为什么的问题?如果标题已经包含条目数,则标记值是多余的。

我按了。我没有在句柄上添加额外的条目,而是将最后一个条目n复制到了额外的条目n + 1,然后用新的句柄覆盖了(现在倒数第二个)条目n。

谜团解决了吗?我使代码正常工作,因此在某种意义上,答案一定是肯定的。但这很难令人满意。在每一步,不太可能的解决方案只会引发更多问题。鸡和蛋的问题最容易解决:此映射表与根命令缓冲区一起通过特殊的IOKit选择器进行分配,该选择器独立于常规缓冲区分配,并且映射表的句柄与提交命令缓冲区选择器。此外,通过命令缓冲区提交传递必需的句柄的想法并非闻所未闻。在主线Linux驱动程序上使用了类似的机制。但是,与简单的CPU端数组相比,在共享内存中使用64字节表条目的基本原理仍然难以捉摸。

让内存分配陷入困境,前进的道路并非没有颠簸(和坑坑洼洼),而是耐心地进行了迭代,直到我自己与Metal并行构造了整个GPU内存,仅依靠专有用户空间来初始化设备。最后,剩下的就是我自己开始IOKit握手的信念飞跃,而我有了我的第一个三角形。

自从上一篇博客文章(可在GitHub上获得)以来,这些更改总计约1700行代码。我整理了一个简单的演示,在屏幕上显示了GPU,为一个三角形制作了动画。此时的窗口系统集成实际上是不存在的:需要XQuartz,并且在具有纯净标量代码的软件中会发生(64x64 Morton级交错)帧缓冲区的虚化。不过,M1的CPU足够快以应付。

现在,用户空间驱动程序的每个部分都已引导,接下来,我们可以独立地遍历指令集和命令缓冲区。 我们可以逗弄一些小细节,然后将代码从数百个莫名其妙的魔术常数逐位转换为真正的驱动程序。 向前!