macOS、iOS的Metal 2開發(fā)爬坑記錄:攝像頭、Capture GPU Frame、Shader調(diào)試與GPUImage存在的問題

本文檔記錄Metal 2配合Xcode 9在macOS High Serria、iOS 8+開發(fā)過程遇到的攝像頭、Capture GPU Frame與Shader編譯調(diào)試問題及解決辦法。另外,修正了GPUImage源碼中對Mac攝像頭不支持yuv輸出的“不恰當”地說法(至少在macOS High Serria是不恰當?shù)模?/p>

1. 調(diào)用iMac攝像頭

1.1 攝像頭的position屬性為AVCaptureDevicePositionUnspecified

在iOS開發(fā)中,一般通過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,所以我將音視頻輸入部分寫成同一份源碼并加上適當?shù)臈l件編譯。出于性能考慮,iOS一般讓攝像頭輸出yuv像素數(shù)據(jù),并且在yuv空間上做些圖像處理操作,這就得測試yuv轉(zhuǎn)rgb的shader是否正常工作的。因此,令macOS輸出yuv格式數(shù)據(jù)在此場合是合理的。閱讀GPUImage源碼,可發(fā)現(xiàn)如下注釋:

// 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屬性,發(fā)現(xiàn)其支持yuv420sp、yuv422sp和RGBA及BGRA。

因此,GPUImage在macOS上直接輸出32BGRA數(shù)據(jù),繞過這個坑。實際上,上述說法對于macOS High Sierra是不成立的,其他版本的Mac沒測試。

下面,以iMac 5k為例調(diào)用攝像頭,設(shè)置AVCaptureVideoDataOutput.videoSettings = nil; // receives samples in device format后,返回kCVPixelFormatType_422YpCbCr8 ,即yuv422sp('2vuy')??芍蚷OS一樣,iMac攝像頭原始格式為yuv422p。也證明了GPUImage的說法不那么正確,也有了接下來的折騰。

好了,問題來了,macOS輸出yuv格式數(shù)據(jù)有點小坑。下面描述輸出為yuv420p時,通過CVMetalTextureCacheCreateTextureFromImage創(chuàng)建Metal紋理時返回kCVReturnFirst(-6660)的解決過程。

指定videoSettings的輸出格式為yuv420p后,在CVMetalTextureCacheCreateTextureFromImage創(chuàng)建Metal紋理時返回kCVReturnFirst(-6660)。顯然,沒創(chuàng)建出可用的紋理。

為處理-6660,加上[videoOutput setVideoSettings:@{(id) kCVPixelBufferMetalCompatibilityKey: @(TRUE)}];反而導(dǎo)致CVPixelBufferGetPlaneCount(cameraFrame)返回值為0。具體原因是,每次調(diào)用setVideoSettings會覆蓋上一次設(shè)置的結(jié)果。解決辦法是,先構(gòu)造完整videoSettings字典,再設(shè)置。比如,

#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創(chuàng)建紋理時也是-6660,解決辦法同上。

2. Capture GPU Frame

按照GPUImage的做法,在macOS High Serria且Scheme為默認值情況下,Capture GPU Frame功能不可能用。具體表現(xiàn)為,Xcode 9的Capture GPU Frame功能變灰、快捷圖標消失。啟動app也不打印Metal API Extended Validation信息。正常情況下,打印信息示例如下。

[DYMTLInitPlatform] platform initialization successful
Metal GPU Frame Capture Enabled
Metal API Validation Enabled

通常我們第一反應(yīng)是,可以強制修改Scheme的GPU Frame Capture為Metal(默認為Automatically Enable)。實際上,這個改動之后,Capture GPU Frame快捷圖標是出現(xiàn)了,然而,問題并沒解決。

其實,這不是Xcode的bug,是代碼邏輯不對。解決起來很簡單,而且不需要強制修改Scheme的GPU Frame Capture為Metal,默認的Automatically Enable就夠用了。只要代碼邏輯合理,Capture GPU Frame會自動設(shè)置為可用狀態(tài)。

3. Shader編譯與調(diào)試

3.1 Metal shader文件不可放入Bundle

.metal文件放入Bundle中,Xcode編譯時并不檢查shader代碼是否正確。相應(yīng)地,運行后使用defaultLibrary得不到預(yù)編譯的著色器函數(shù)。

3.2 在線調(diào)試看不到Shader源碼

在線調(diào)試Metal shader時,發(fā)現(xiàn)找不到源碼,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出現(xiàn)前的老Xcode創(chuàng)建的項目一般會出現(xiàn)此問題。項目太古老,用Xcode 9打開后,它不會自動設(shè)置此項。

解決辦法:Produce debugging information將debug設(shè)置Yes。使用Xcode 9等新版本創(chuàng)建Metal項目,默認將此項設(shè)置為Yes?,F(xiàn)在,在線調(diào)試時Shader源碼可正常修改、編譯。

題外話,之前嘗試修改Bitcode設(shè)置YES或NO,并不能解決此問題。

3.3 Metal文件不支持平臺相關(guān)的條件編譯

舉例:

#if TARGET_OS_MAC
    XXX
#else
    YYY
#endif

在macOS上運行Metal應(yīng)用,實際編譯結(jié)果得到Y(jié)YY。

3.4 Metal文件包含Metal文件或自定義頭文件

Metal文件可包含另一個Metal文件,在Xcode 9,這是可行的。也可包含自定義頭文件。缺點是Xcode無法自動跳轉(zhuǎn)到相應(yīng)的文件,相當不方便,希望官方后續(xù)能解決此缺陷。

3.5 cannot have global constructors (llvm.global_ctors) in FragmentFunctionXXX

從3.4節(jié)可知,Metal文件允許包含另一個Metal文件。那么,你會想,能否定義一些常用顏色轉(zhuǎn)換矩陣在公共metal文件,然后在其它metal中直接使用,從而避免每次繪制都上傳這些數(shù)據(jù)呢?

你問我支不支持,我當然支持你的想法。可是,也得看看編譯器怎么看。實際上,如果定義的是行或列向量,這是可行的。然而,如果定義全局矩陣(比如,half3x3),而且全局矩陣參與計算,最終結(jié)果為Fragment Function的返回值,Xcode編譯期間會報錯:

cannot have global constructors (llvm.global_ctors) in FragmentFunctionXXX

怎么解決呢?目前來看,只能打消定義全局矩陣的念頭。定義幾個常用的采樣器就夠了,要啥自行車。

3.6 空CommandBuffer導(dǎo)致Capture GPU Frame無法結(jié)束

每次渲染提交不帶MTLRenderCommandEncoder的MTLCommandBuffer,進行Capture GPU Frame,Xcode狀態(tài)欄會瘋狂讀取MTLCommandBuffer數(shù)據(jù),無法自拔。比如,定時執(zhí)行如下代碼。

id <MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
[commandBuffer commit];

為什么呢?因為代碼存在邏輯錯誤,才會出現(xiàn)這種現(xiàn)象,上述代碼只是示意。那,哪里錯了呢?咱們還是用圖說話吧。

小結(jié)

使用Metal 2遇到的問題不止這些。之后會整理成文檔,逐步發(fā)布。不得不說,現(xiàn)在的Metal比2015年那會兒在工具的支持上強太多了。比如,Xcode支持斷點預(yù)覽紋理,從此不再頻繁Capture GPU Frame。
另外,Capture GPU Frame在macOS App上的速度非???,比iOS爽太多。放兩個截圖示意下。

GPU Stack
斷點查看紋理內(nèi)容
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容