通过共享资源内存来结合CoreGraphics和Metal的功能
Metal API很棒,它为移动设备和台式设备打开了很多可能性。 但是, GPU有时可能有点笨拙,尤其是在诸如字体渲染或曲线绘制之类的高精度项目中,因此有时您确实会错过旧的CPU优秀项目。 我经常碰巧会想使用CoreGraphics的一些功能,但是保持我的GPU管道快速高效,而没有多余的转换或CPU / GPU同步中断 。
启发我写这篇文章的情况是,使用户能够绘制遮罩覆盖图,以识别希望显示某些效果的区域。 当然可以用Metal来实现绘图,但这需要大量工作:保持路径数据,三角剖分和渲染逻辑。 对于快速原型,仅使用CoreGraphics会容易得多,但是最直接的方法是从UI画布创建CGImage ,然后转换为MTLTexture以将其作为遮罩提供给某些着色器。
多维门户
但是,如果我们希望避免不必要的转换并将CoreGraphics路径直接绘制到MTLTexture中怎么办 ? 这似乎完全有可能!
为了实现这一点,我们必须在某些CGContext和MTLTexture之间共享一个内存缓冲区。 Metal可以从MTLBuffer对象创建纹理,而MTLBuffer则有机会指向已分配的内存。
因此,我们需要执行以下操作:
- 为CGContext分配内存
- 创建指向相同内存的MTLBuffer
- 从MTLBuffer创建MTLTexture
- 瞧!
实施
免责声明#1:在此演示中,我使用Alloy ,这是我在Apple平台上所有GPU项目中使用的Metal库。 它在香草金属上提供了一个很小的Swifty API
免责声明2:我既不在演示中也不做任何分解,也没有错误处理。 一切都在视图控制器中完成,无论是强制展开还是致命错误。 本文用作参考片段。
当为MTLBuffer分配无副本内存时,事情有些棘手。 为了使其快乐,它必须完美对齐 ,通常为4096字节。 在创建MTLBuffer时, MTLTextures也很麻烦 :我们必须对齐每个纹理行 ,并且对齐方式取决于我们要使用的像素格式 。 幸运的是,这两个任务都是可行的!
为了演示起见,我们将为MTKView上的可绘制单通道蒙版分配共享资源。
每当我们的MTKView更改大小时,我们都会重新分配掩码。 当然不是必须的,但这是最简单的方法。
首先,我们需要从系统中了解RAM页面大小,因此,我们将使用getpagesize()函数。 现在我们必须计算每行对齐的字节数和总分配大小。 为了分配对齐的内存,我们将使用posix_memalign(_,_,_)系统函数。
请注意,从这一点来看,我们负责分配此内存,但是稍后我们将对其进行处理
现在我们准备从此内存创建CGContext ,您可能已经完成了很多次,因此这应该非常简单:
我在这里使用CGImageAlphaInfo.none表示这是灰度图像,但是您也可以使用.alphaOnly
使用完全相同的内存,我们现在可以创建一个无副本MTLBuffer ,使其负责释放内存。 但是,您当然可以通过将nil传递给最后一个参数来自行管理。
现在,最后一步:我们正在从此缓冲区创建纹理。 纹理的存储模式必须与缓冲区相同。 您可以在此处阅读针对不同平台/操作系统的其他要求。
由于我们最初以Metal应该满意的格式分配内存,因此一切都应该顺利进行。
演示版
好的,我们都准备好了! 将CGContext和MTLTexture缓存到某个位置后,您可以对它们进行任何操作。
我们将创建一个简单的应用程序,使您可以“刮擦”模糊的纹理,最终结果可以在视频中看到:
让我们开始寻找一个可爱的小狗图像 ,将其加载到MTLTexture中 ,然后使用Metal Performance Shaders进行模糊处理。
Simple Metal片段着色器将处理原始纹理,覆盖纹理和蒙版纹理的混合:
然后,只要MTKView要重绘自身,我们就只需调度它。
您应该考虑CPU和GPU之间发生的内存同步。 您不应创建CoreGraphics和Metal可以同时访问此缓冲区的情况,尤其是在macOS上 。
为防止本文过分膨胀,我故意不包括使用UIKit touch API 创建渲染管道状态片段 , 顶点着色器和CoreGraphics绘图代码,您可以在此处找到完整的源代码。
结论
混合使用CoreGraphics和Metal确实非常有用,并且可以同时使用两种方法:从CoreText到Metal绘制文本,或将3d网格渲染到CGContext并以某种方式在CPU上处理 。 剩下的就是你的想象力!