該節(jié)是ffmpeg開發(fā)播放器學(xué)習(xí)筆記
的第五節(jié)《Metal 渲染YUV》
Metal是Apple開發(fā)的適用于iOS/macOS/iPadOS平臺的圖形渲染與硬件加速庫召调。Metal 提供對圖形處理器 (GPU) 的接近直接訪問膨桥,使您能最大程度地發(fā)揮 iOS、macOS 和 Apple tvOS app 中的圖形和計算潛能唠叛。Metal 構(gòu)建于易用的低開銷架構(gòu)之上只嚣,而且提供預(yù)編譯的 GPU 著色器和精細(xì)的資源控制,并支持多線程處理艺沼。相對于OpenGL,它是采用了面向?qū)ο蟮脑O(shè)計更易于使用,在Apple的系統(tǒng)平臺上可發(fā)揮更大的性能優(yōu)勢册舞。
? 第一節(jié) - Hello FFmpeg
? 第二節(jié) - 軟解視頻流,渲染 RGB24
? 第三節(jié) - 認(rèn)識YUV
? 第四節(jié) - 硬解碼,OpenGL渲染YUV
?? 第五節(jié) - Metal 渲染YUV
?? 第六節(jié) - 解碼音頻,使用AudioQueue 播放
?? 第七節(jié) - 音視頻同步
?? 第八節(jié) - 完善播放控制
?? 第九節(jié) - 倍速播放
?? 第十節(jié) - 增加視頻過濾效果
?? 第十一節(jié) - 音頻變聲
該節(jié) Demo 地址: https://github.com/czqasngit/ffmpeg-player/releases/tag/Metal-YUV
實例代碼提供了Objective-C
與Swift
兩種實現(xiàn),為了方便說明,文章引用的是Objective-C
代碼,因為Swift
代碼指針看著不簡潔。
該節(jié)最終效果如下圖:
目標(biāo)
- 了解Metal基本使用流程
- 初始化Metal
- 了解
metal
小程序 - 了解Metal計算線程分布
- 利用Metal渲染YUV
了解Metal基本使用流程
下面這張流程圖大致展示了Metal的工程原理:
1.獲取GPU設(shè)備實例
要使用Metal進行計算或渲染,首先需要獲取到當(dāng)前系統(tǒng)支持的GPU實例,后續(xù)所有的操作都必須建立在這個GPU實例的計算上進行障般。值得注意的是macOS平臺可能會有多個GPU實例调鲸。
2.初始化計算管線
Metal使用.metal
文件來編寫小程序,它的風(fēng)格類似C++。編寫好的.metal
文件會在編譯時統(tǒng)一生成default.metallib
資源文件挽荡。通過字符串查找到需要使用的小程序并最終生成計算管理實例藐石。
3.創(chuàng)建指令隊列
Metal的計算是通過計算隊列實例來管理的,它的目標(biāo)就是合理的調(diào)度GPU計算資源按提交的計算指令一個一個的計算。
4.創(chuàng)建指令緩沖對象
Metal的每一次計算都需要創(chuàng)建一個新的指令緩沖對象,Metal的計算目的是GPU,GPU不能直接訪問內(nèi)存數(shù)據(jù),緩沖對象分配好當(dāng)前計算所需要的顯存,在計算時Metal直接讀取顯存數(shù)據(jù)進行計算定拟。
5.將內(nèi)存數(shù)據(jù)發(fā)送到顯存
Metal計算所需要的數(shù)據(jù)需要在提交計算前從內(nèi)存發(fā)送到顯存,并在緩沖區(qū)中保存贯钩。
6.將指令緩沖對象提交到緩沖隊列計算
一切數(shù)據(jù)準(zhǔn)備好之后就可以將指令緩沖對象提交到指令隊列,等待指令隊列的調(diào)度并完成計算。
初始化Metal
1.獲取MTLDevice
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
通過上面的代碼可以獲取到一個最優(yōu)的GPU實例,如果你需要獲取所有的GPU實例你可以使用如下代碼:
NSArray<id<MTLDevice>> devices = MTLCopyAllDevices();
2.創(chuàng)建MTLComputePipelineState
創(chuàng)建MTLLibrary
id<MTLLibrary> library = [device newDefaultLibrary];
MTLLibrary實例是所有的metal
小程序的集合,它可以理解成metal
小程序庫办素。
從MTLLibrary中獲取MTLFunction
id<MTLFunction> function = [library newFunctionWithName:@"yuv420ToRGB"];
MTLFunction是一個具體的小程序
生成MTLComputePipelineState實例
id<MTLComputePipelineState> computePipline = [device newComputePipelineStateWithFunction:function error:&error];
指定計算管線計算時需要調(diào)用的GPU小程序函數(shù)
3.創(chuàng)建MTLCommandQueue
id<MTLCommandQueue> commandQueue = [device newCommandQueue];
到此,Metal使用的初始化工作就完成了
了解metal
小程序
metal
小程序采用了類似C++風(fēng)格的編碼方式,函數(shù)申明使用kernel
關(guān)鍵字角雷。
#include <metal_stdlib>
using namespace metal;
kernel void foo(texture2d<float, access::read> texture [[ texture(0) ]],
constant uint2 &byteSize [[ buffer(1) ]],
texture2d<float, access::write> outTexture [[ texture(2) ]],
uint2 gid [[ thread_position_in_grid ]]) {
}
textture
變量是一個texture2d二維紋理對象,它的數(shù)據(jù)格式是float。方括號里表示它在Metal框架中的對應(yīng)的內(nèi)存中的數(shù)據(jù)類型是texture,變量位于位置0,只有讀取權(quán)限性穿。
byteSize
變量是一個uint2類型的數(shù)據(jù),uint2即包含了兩個uint的結(jié)構(gòu)體類型勺三。它對應(yīng)內(nèi)存中的Buffer數(shù)據(jù)類型(即一維數(shù)據(jù)),變量位于位置1,只有讀取權(quán)限。
outTexture
變量是一個texture2d二維紋理對象,它的數(shù)據(jù)格式是float需曾。它在Metal框架中的對應(yīng)的內(nèi)存中的數(shù)據(jù)類型是texture,變量位于位置2,只有寫入權(quán)限吗坚。
gid
則是Metal框架計算時帶入的變量,它表示當(dāng)前計算的線程號,通過線程號可以獲取到具體要計算的數(shù)據(jù)祈远。這會在稍后進行更詳細(xì)的說明。
以上是一個基礎(chǔ)的函數(shù)申明,申明的只讀取變量即是從內(nèi)存發(fā)送到顯存的數(shù)據(jù),只寫變量則是計算完成后輸出到內(nèi)存的數(shù)據(jù)商源。
更多詳細(xì)的Metal Language
可參考: https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf
了解Metal計算線程
Metal
在執(zhí)行每一個Command Buffer的時候车份,都將其模擬成一個網(wǎng)格,每一個網(wǎng)格都有一個單獨的線程執(zhí)行牡彻。
GPU執(zhí)行程序的邏輯與CPU最大的不同就是并行執(zhí)行大量相互不干擾的邏輯,迸發(fā)線程數(shù)量比CPU多很多,這也是硬件加速的本質(zhì)扫沼。
網(wǎng)格看上去類似這樣的:
這里繪制的示意圖為了更貼近本節(jié)的內(nèi)容,所以繪制成了二維網(wǎng)格。需要注意的是,Metal還可以執(zhí)行一維網(wǎng)格與三維網(wǎng)格,原理都是一樣的庄吼。
圖中藍(lán)色的部分是實際需要計算的數(shù)據(jù),橙色的部分則是無數(shù)據(jù)的網(wǎng)格缎除。在設(shè)定Metal最終計算的風(fēng)格大小的時候往往會多設(shè)置一部分?jǐn)?shù)據(jù),防止某些邊緣界線的數(shù)據(jù)被遺漏了。這些無數(shù)據(jù)的網(wǎng)格需要在小程序里進行過濾总寻。
Metal
在計算的時候不是以單一的網(wǎng)格作為計算基礎(chǔ),而是將一組網(wǎng)格作為一個計算基礎(chǔ)器罐,一次性一組網(wǎng)絡(luò)。它看上去是這樣的:
這里模擬一組網(wǎng)格是4x4,表明Metal在調(diào)度執(zhí)行的時候一次性計算16個網(wǎng)格渐行。這一組執(zhí)行完了再執(zhí)行下一組,因為GPU的線程并發(fā)數(shù)量也是有限的,配置一個最優(yōu)的執(zhí)行大小對計算速度也是有影響的轰坊。
利用Metal渲染YUV
1.初始化CVMetalTextureCacheRef
初始化CVMetalTextureCacheRef只需要執(zhí)行一次
CVMetalTextureCacheRef metalTextureCache = NULL;
CVReturn ret = CVMetalTextureCacheCreate(kCFAllocatorDefault, NULL, device, NULL, &metalTextureCache);
CVMetalTextureCacheRef用于緩存后期創(chuàng)建CVMetalTextureRef與CVPixelBufferRef之間的映射,如果有同樣的CVPixelBufferRef實例被再次創(chuàng)建時,可直接使用緩存的CVMetalTextureRef實例。
2.AVFrame轉(zhuǎn)換成CVPixelBufferRef
需要得到用于表達Metal中紋理資源對象實例,需要將AVFrame轉(zhuǎn)換成CVPixelBufferRef
- (BOOL)setupCVPixelBufferIfNeed:(AVFrame *)frame {
if(!pixelBufferPoolRef) {
NSMutableDictionary *pixelBufferAttributes = [[NSMutableDictionary alloc] init];
if(frame->color_range == AVCOL_RANGE_MPEG) {
pixelBufferAttributes[_CFToString(kCVPixelBufferPixelFormatTypeKey)] = @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange);
} else {
pixelBufferAttributes[_CFToString(kCVPixelBufferPixelFormatTypeKey)] = @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange);
}
pixelBufferAttributes[_CFToString(kCVPixelBufferMetalCompatibilityKey)] = @(TRUE);
pixelBufferAttributes[_CFToString(kCVPixelBufferWidthKey)] = @(frame->width);
pixelBufferAttributes[_CFToString(kCVPixelBufferHeightKey)] = @(frame->height);
/// bytes per row(alignment)
pixelBufferAttributes[_CFToString(kCVPixelBufferBytesPerRowAlignmentKey)] = @(frame->linesize[0]);
// pixelBufferAttributes[_CFToString(kCVPixelBufferIOSurfacePropertiesKey)] = @{};
CVReturn cvRet = CVPixelBufferPoolCreate(kCFAllocatorDefault,
NULL,
(__bridge CFDictionaryRef)pixelBufferAttributes,
&(self->pixelBufferPoolRef));
if(cvRet != kCVReturnSuccess) {
NSLog(@"create cv buffer pool failed: %d", cvRet);
return NO;
}
}
return YES;
}
if(![self setupCVPixelBufferIfNeed:frame]) return NULL;
CVPixelBufferRef _pixelBufferRef;
CVReturn cvRet = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferPoolRef, &_pixelBufferRef);
if(cvRet != kCVReturnSuccess) {
NSLog(@"create cv buffer failed: %d", cvRet);
return NULL;
}
CVPixelBufferLockBaseAddress(_pixelBufferRef, 0);
/// copy y
size_t yBytesPerRowSize = CVPixelBufferGetBytesPerRowOfPlane(_pixelBufferRef, 0);
void *yBase = CVPixelBufferGetBaseAddressOfPlane(_pixelBufferRef, 0);
memcpy(yBase, frame->data[0], yBytesPerRowSize * frame->height);
/// copy uv
void *uvBase = CVPixelBufferGetBaseAddressOfPlane(_pixelBufferRef, 1);
size_t uvBytesPerRowSize = CVPixelBufferGetBytesPerRowOfPlane(_pixelBufferRef, 1);
memcpy(uvBase, frame->data[1], uvBytesPerRowSize * frame->height / 2);
CVPixelBufferUnlockBaseAddress(_pixelBufferRef, 0);
return _pixelBufferRef;
3.編寫小程序
#include <metal_stdlib>
using namespace metal;
///y_inTexture: Y
///uv_inTexture: UV
///byteSize: Y的寬高
///outTexture: RGBA
///gid: 執(zhí)行線程所在的Grid位置
kernel void yuv420ToRGB(texture2d<float, access::read> y_inTexture [[ texture(0) ]],
texture2d<float, access::read> uv_inTexture [[ texture(1) ]],
constant uint2 &byteSize [[ buffer(2) ]],
texture2d<float, access::write> outTexture [[ texture(3) ]],
uint2 gid [[ thread_position_in_grid ]]) {
/// 超出實際紋理寬高的網(wǎng)格不計算,直接返回
if(gid.x > byteSize.x || gid.y > byteSize.y) return;
// if(gid.x % 2 == 0 || gid.y % 2 == 0 || gid.x % 3 == 0 || gid.y % 3 == 0) {
// outTexture.write(float4(0, 0, 0, 1.0), gid);
// return;
// }
/// 獲取y分量數(shù)據(jù),由于在創(chuàng)建MetalTexture的時候在方法CVMetalTextureCacheCreateTextureFromImage
/// 中指定了歸一化的格式,所以這里得到的y值范圍是[0, 1]
float4 yFloat4 = y_inTexture.read(gid);
/// Y與UV在YUV420P格式下的比例是4:1
/// YUV420P垂直與水平分別是2:1的比例
/// gid是包含X祟印,Y坐標(biāo),所以這里gid/2實際上是縮小了4倍衰倦,符合YUV420P中Y與UV的比例
/// 每4個Y共享一組UV
float4 uvFloat4 = uv_inTexture.read(gid/2);
float y = yFloat4.x;
float cb = uvFloat4.x;
float cr = uvFloat4.y;
/// 按YCbCr轉(zhuǎn)RGB的公式進行數(shù)據(jù)轉(zhuǎn)換
float r = y + 1.403 * (cr - 0.5);
float g = y - 0.343 * (cb - 0.5) - 0.714 * (cr - 0.5);
float b = y + 1.770 * (cb - 0.5);
outTexture.write(float4(r, g, b, 1.0), gid);
}
4.獲取Metal紋理資源對象MTLTexture
在Metal中所有的資源對象都是MTLResource,常用的兩種類型則是MTLTexture與MTLBuffer,它們都是MTLResource子類型。MTLTexture是紋理(2維或者3維)資源對象,MTLBuffer是連續(xù)存儲數(shù)據(jù)的資源對象旁理。以下實例以是yTexture分量的MTLTexture對象:
size_t yWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0);
size_t yHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0);
CVMetalTextureRef yMetalTexture;
CVReturn ret = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
self->metalTextureCache,
pixelBuffer,
NULL,
MTLPixelFormatR8Unorm,
yWidth,
yHeight,
0,
&yMetalTexture);
pixelBuffer
是CVPixelBufferRef實例,它保存了具體的數(shù)據(jù)樊零。
MTLPixelFormatR8Unorm
指定了數(shù)據(jù)格式y(tǒng)Texture存儲的數(shù)據(jù)是一個8位無符號整形數(shù)據(jù),它的存儲范圍是[0, 255],歸一化后對應(yīng)的小程序取值范圍是[0, 1]。在metal
小程序執(zhí)行時,讀取MTLTexture數(shù)據(jù)一次讀取一個8位作為并歸一化處理孽文。
得到CVMetalTextureRef即可獲取MTLTexture實例
id<MTLTexture> yTexture = CVMetalTextureGetTexture(yMetalTexture);
由于當(dāng)前CVPixelBufferRef實例中存儲的是NV12格式的數(shù)據(jù),所以除了Y分量數(shù)據(jù),還需要創(chuàng)建UV分量的MTLTexture驻襟。代碼如下:
size_t uvWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1);
size_t uvHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1);
CVMetalTextureRef uvMetalTexture;
ret = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
self->metalTextureCache,
pixelBuffer,
NULL,
MTLPixelFormatRG8Unorm,
uvWidth,
uvHeight,
1,
&uvMetalTexture);
if(ret != kCVReturnSuccess) return;
id<MTLTexture> uvTexture = CVMetalTextureGetTexture(uvMetalTexture);
if(!uvTexture) return;
MTLPixelFormatRG8Unorm
格式指定了由兩個8位無符號整形的數(shù)據(jù)格式。在metal
小程序執(zhí)行時,讀取MTLTexture數(shù)據(jù)一次讀取兩個8位作為并歸一化處理芋哭。
5.創(chuàng)建MTLCommandBuffer
每一個指令執(zhí)行都有單獨的運行時數(shù)據(jù),這些數(shù)據(jù)需要開辟GPU顯存來存在
id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
6.創(chuàng)建MTLComputeCommandEncoder
內(nèi)存中的數(shù)據(jù)需要發(fā)送到顯存,需要通過MTLComputeCommandEncoder來完成
id<MTLComputeCommandEncoder> commandEncoder = [commandBuffer computeCommandEncoder];
設(shè)置指令緩沖對象執(zhí)行時的計算管線
[commandEncoder setComputePipelineState:self.computePipline];
設(shè)置小程序的紋理參數(shù)
[commandEncoder setTexture:yTexture atIndex:0];
[commandEncoder setTexture:uvTexture atIndex:1];
設(shè)置Metal執(zhí)行時的實際網(wǎng)格大小
simd_uint2 byteSize = simd_make_uint2((uint32_t)yWidth, (uint32_t)yHeight);
[commandEncoder setBytes:&byteSize length:sizeof(simd_uint2) atIndex:2];
由于Metal只能訪問MTLResource資源對象,這里設(shè)置的&byteSize
相當(dāng)于最終也會轉(zhuǎn)化成MTLBuffer沉衣。也可以先創(chuàng)建MTLBuffer,配置好數(shù)據(jù)再設(shè)置到小程序?qū)?yīng)的變量位置。
設(shè)置輸出紋理對象
CAMetalLayer *layer = (CAMetalLayer *)self.layer;
id<CAMetalDrawable> drawable = [layer nextDrawable];
[commandEncoder setTexture:drawable.texture atIndex:3];
當(dāng)前繪制的視圖繼承至MetalKit提供的MTKView,它提供了可繪制RGBA的紋理資源對象减牺。
設(shè)置Metal執(zhí)行的線程組(一次性執(zhí)行多少個網(wǎng)格-線程)與線程組數(shù)量
NSUInteger threadExecutionWidth = self.computePipline.threadExecutionWidth;
NSUInteger maxTotalThreadsPerThreadgroup = self.computePipline.maxTotalThreadsPerThreadgroup;
MTLSize threadgroupSize = MTLSizeMake(threadExecutionWidth,
maxTotalThreadsPerThreadgroup / threadExecutionWidth,
1);
MTLSize threadgroupCount = MTLSizeMake((yWidth + threadgroupSize.width - 1) / threadgroupSize.width,
(yHeight + threadgroupSize.height - 1) / threadgroupSize.height,
1);
[commandEncoder dispatchThreadgroups:threadgroupCount threadsPerThreadgroup:threadgroupSize];
MTLComputePipelineState提供了當(dāng)前GPU實例的最大的一次性執(zhí)行線程的最大寬與一次性可以執(zhí)行的最大線程數(shù)豌习。通過這兩個數(shù)據(jù)可以計算出一次性執(zhí)行線程組的大小,也可以自行設(shè)置大小,但不能超過。
這里將threadgroupCount設(shè)置成超出了實際紋理寬高的大小,防止邊界遺漏拔疚。
紋理寬度使用Y分量的寬高,UV分量的寬高為Y分量的1/4肥隆。
提交并顯示RGBA紋理
[commandEncoder endEncoding];
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> _Nonnull buffer) {
CVBufferRelease(yMetalTexture);
CVBufferRelease(uvMetalTexture);
}];
[commandBuffer presentDrawable:drawable];
[commandBuffer commit];
到此,利用Metal渲染YUV就完成。
總結(jié):
- 了解Metal基本使用流程,Metal基于面向?qū)ο?相對于OpenGL更方便使用稚失。Metal還提供了計算管線,可輕松利用GPU能力實現(xiàn)大并發(fā)無依賴的數(shù)據(jù)計算栋艳。
- 初始化Metal使用環(huán)境,了解了MTLDevice,MTLComputePipelineState等對象的作用
- 了解
metal
小程序,編寫了將YUV轉(zhuǎn)換成RGB的metal
小程序 - 了解Metal計算線程的執(zhí)行流程與邏輯
- 利用Metal與MetalKit完成了YUV的渲染
更多內(nèi)容請關(guān)注微信公眾號<<程序猿搬磚>>