本文档记录Metal 2配合Xcode 9在macOS High Serria、iOS 8+开发过程遇到的摄像头、Capture GPU Frame与Shader编译调试问题及解决办法。另外,修正了GPUImage源码中对Mac摄像头不支持yuv输出的“不恰当”地说法(至少在macOS High Serria是不恰当的)。
1. 调用iMac摄像头
1.1 摄像头的position属性为AVCaptureDevicePositionUnspecified
在iOS开发中,一般通过AVCaptureDevicePosition(Front或Back)确认访问前后置摄像头。虽然iMac有前置摄像头,然而,它的position属性为nil。因此,当macOS和iOS共享一份代码,遍历[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]
返回的AVCaptureDevice列表时,AVCaptureDevice的position
属性并不等于AVCaptureDevicePositionFront
,而是AVCaptureDevicePositionUnspecified
。
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in devices)
{
if ([device position] == cameraPosition)
{
_inputCamera = device;
}
}
if (!_inputCamera)
{
return nil;
}
或者,如果做平台条件编译,直接用默认摄像头即可,示例代码如下所示。
#if TARGET_OS_MAC
[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
#endif
正常情况下,iMac 5k摄像头的输出信息如下所示。
1.2 输出yuv像素格式并兼容Metal 2
因为AVFoundation和Metal 2支持macOS和iOS,所以我将音视频输入部分写成同一份源码并加上适当的条件编译。出于性能考虑,iOS一般让摄像头输出yuv像素数据,并且在yuv空间上做些图像处理操作,这就得测试yuv转rgb的shader是否正常工作的。因此,令macOS输出yuv格式数据在此场合是合理的。阅读GPUImage源码,可发现如下注释:
// Despite returning a longer list of supported pixel formats, only RGB, RGBA, BGRA, and the YUV 4:2:2 variants seem to return cleanly
打印AVCaptureVideoDataOutput.availableVideoCVPixelFormatTypes
属性,发现其支持yuv420sp、yuv422sp和RGBA及BGRA。
因此,GPUImage在macOS上直接输出32BGRA数据,绕过这个坑。实际上,上述说法对于macOS High Sierra是不成立的,其他版本的Mac没测试。
下面,以iMac 5k为例调用摄像头,设置AVCaptureVideoDataOutput.videoSettings = nil; // receives samples in device format
后,返回kCVPixelFormatType_422YpCbCr8 ,即yuv422sp('2vuy')。可知,和iOS一样,iMac摄像头原始格式为yuv422p。也证明了GPUImage的说法不那么正确,也有了接下来的折腾。
好了,问题来了,macOS输出yuv格式数据有点小坑。下面描述输出为yuv420p时,通过CVMetalTextureCacheCreateTextureFromImage
创建Metal纹理时返回kCVReturnFirst(-6660)的解决过程。
指定videoSettings的输出格式为yuv420p后,在CVMetalTextureCacheCreateTextureFromImage
创建Metal纹理时返回kCVReturnFirst(-6660)。显然,没创建出可用的纹理。
为处理-6660,加上[videoOutput setVideoSettings:@{(id) kCVPixelBufferMetalCompatibilityKey: @(TRUE)}];
反而导致CVPixelBufferGetPlaneCount(cameraFrame)
返回值为0。具体原因是,每次调用setVideoSettings会覆盖上一次设置的结果。解决办法是,先构造完整videoSettings字典,再设置。比如,
#if TARGET_OS_MAC
NSDictionary *videoSettings = @{
(id) kCVPixelBufferMetalCompatibilityKey: @(TRUE),
(id) kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)
};
videoOutput.videoSettings = videoSettings;
#else
[videoOutput setVideoSettings:@{(id) kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)}];
#endif
同理,单独指定为kCVPixelFormatType_32BGRA在CVMetalTextureCacheCreateTextureFromImage
创建纹理时也是-6660,解决办法同上。
2. Capture GPU Frame
按照GPUImage的做法,在macOS High Serria且Scheme为默认值情况下,Capture GPU Frame功能不可能用。具体表现为,Xcode 9的Capture GPU Frame功能变灰、快捷图标消失。启动app也不打印Metal API Extended Validation信息。正常情况下,打印信息示例如下。
[DYMTLInitPlatform] platform initialization successful
Metal GPU Frame Capture Enabled
Metal API Validation Enabled
通常我们第一反应是,可以强制修改Scheme的GPU Frame Capture为Metal(默认为Automatically Enable)。实际上,这个改动之后,Capture GPU Frame快捷图标是出现了,然而,问题并没解决。
其实,这不是Xcode的bug,是代码逻辑不对。解决起来很简单,而且不需要强制修改Scheme的GPU Frame Capture为Metal,默认的Automatically Enable就够用了。只要代码逻辑合理,Capture GPU Frame会自动设置为可用状态。
3. Shader编译与调试
3.1 Metal shader文件不可放入Bundle
.metal文件放入Bundle中,Xcode编译时并不检查shader代码是否正确。相应地,运行后使用defaultLibrary得不到预编译的着色器函数。
3.2 在线调试看不到Shader源码
在线调试Metal shader时,发现找不到源码,Xcode提示如下所示:
Cannot show the function source
Xcode could not find the library source. Make sure debugging information is enabled for library compilation under target build settings.
具体原因是,在Metal出现前的老Xcode创建的项目一般会出现此问题。项目太古老,用Xcode 9打开后,它不会自动设置此项。
解决办法:Produce debugging information将debug设置Yes。使用Xcode 9等新版本创建Metal项目,默认将此项设置为Yes。现在,在线调试时Shader源码可正常修改、编译。
题外话,之前尝试修改Bitcode设置YES或NO,并不能解决此问题。
3.3 Metal文件不支持平台相关的条件编译
举例:
#if TARGET_OS_MAC
XXX
#else
YYY
#endif
在macOS上运行Metal应用,实际编译结果得到YYY。
3.4 Metal文件包含Metal文件或自定义头文件
Metal文件可包含另一个Metal文件,在Xcode 9,这是可行的。也可包含自定义头文件。缺点是Xcode无法自动跳转到相应的文件,相当不方便,希望官方后续能解决此缺陷。
3.5 cannot have global constructors (llvm.global_ctors) in FragmentFunctionXXX
从3.4节可知,Metal文件允许包含另一个Metal文件。那么,你会想,能否定义一些常用颜色转换矩阵在公共metal文件,然后在其它metal中直接使用,从而避免每次绘制都上传这些数据呢?
你问我支不支持,我当然支持你的想法。可是,也得看看编译器怎么看。实际上,如果定义的是行或列向量,这是可行的。然而,如果定义全局矩阵(比如,half3x3),而且全局矩阵参与计算,最终结果为Fragment Function的返回值,Xcode编译期间会报错:
cannot have global constructors (llvm.global_ctors) in FragmentFunctionXXX
怎么解决呢?目前来看,只能打消定义全局矩阵的念头。定义几个常用的采样器就够了,要啥自行车。
3.6 空CommandBuffer导致Capture GPU Frame无法结束
每次渲染提交不带MTLRenderCommandEncoder的MTLCommandBuffer,进行Capture GPU Frame,Xcode状态栏会疯狂读取MTLCommandBuffer数据,无法自拔。比如,定时执行如下代码。
id commandBuffer = [_commandQueue commandBuffer];
[commandBuffer commit];
为什么呢?因为代码存在逻辑错误,才会出现这种现象,上述代码只是示意。那,哪里错了呢?咱们还是用图说话吧。
小结
使用Metal 2遇到的问题不止这些。之后会整理成文档,逐步发布。不得不说,现在的Metal比2015年那会儿在工具的支持上强太多了。比如,Xcode支持断点预览纹理,从此不再频繁Capture GPU Frame。
另外,Capture GPU Frame在macOS App上的速度非常快,比iOS爽太多。放两个截图示意下。