macOS、iOS的Metal 2开发爬坑记录:摄像头、Capt

作者: 熊皮皮 | 来源:发表于2017-12-03 17:44 被阅读564次

    本文档记录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摄像头的输出信息如下所示。

    <AVCaptureDALDevice: 0x10071e6f0 [FaceTime HD Camera (Built-in)][0x1440000005ac8511]>
    

    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<NSString *, id> *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 <MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
    [commandBuffer commit];
    

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

    小结

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

    GPU Stack 断点查看纹理内容

    相关文章

      网友评论

      • DoKeer:metal和opengl共存同一个项目,gpu frame预览还好用么
      • 落影loyinglin:只要代码逻辑合理,Capture GPU Frame会自动设置为可用状态。

        什么意思?

      本文标题:macOS、iOS的Metal 2开发爬坑记录:摄像头、Capt

      本文链接:https://www.haomeiwen.com/subject/uqtzbxtx.html