macOS、iOS的Metal 2开发爬坑记录:摄像头、Capture GPU Frame、Shader调试与GPUImage存在的问题

本文档记录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];

为什么呢?因为代码存在逻辑错误,才会出现这种现象,上述代码只是示意。那,哪里错了呢?咱们还是用图说话吧。

macOS、iOS的Metal 2开发爬坑记录:摄像头、Capture GPU Frame、Shader调试与GPUImage存在的问题_第1张图片

小结

使用Metal 2遇到的问题不止这些。之后会整理成文档,逐步发布。不得不说,现在的Metal比2015年那会儿在工具的支持上强太多了。比如,Xcode支持断点预览纹理,从此不再频繁Capture GPU Frame。
另外,Capture GPU Frame在macOS App上的速度非常快,比iOS爽太多。放两个截图示意下。

macOS、iOS的Metal 2开发爬坑记录:摄像头、Capture GPU Frame、Shader调试与GPUImage存在的问题_第2张图片
GPU Stack
断点查看纹理内容

你可能感兴趣的:(macOS、iOS的Metal 2开发爬坑记录:摄像头、Capture GPU Frame、Shader调试与GPUImage存在的问题)