本文檔記錄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爽太多。放兩個截圖示意下。