Demo 地址已更新
https://github.com/Danny1451/MetalLutFilter
Metal 介紹及基本使用
最近做的一個技術(shù)研究挟鸠,metal 的國內(nèi)相關(guān)資料很少掸鹅,所以整理了這一系列文章腐巢,希望能幫到有用的人永品。
什么是 Metal
Metal 是一個和 OpenGL ES 類似的面向底層的圖形編程接口症见,通過使用相關(guān)的 api 可以直接操作 GPU ,最早在 2014 年的 WWDC 的時候發(fā)布,并于今年發(fā)布了 Metal 2寂殉。
Metal 是 iOS 平臺獨有的村怪,意味著它不能像 OpenGL ES 那樣支持跨平臺,但是它能最大的挖掘蘋果移動設(shè)備的 GPU 能力,進行復(fù)雜的運算,像 Unity 等游戲引擎都通過 Metal 對 3D 能力進行了優(yōu)化铲敛, App Store 還有相應(yīng)的運用 Metal 技術(shù)的游戲?qū)n}。
Metal 具有特點
- GPU 支持的 3D 渲染
- 和 CPU 并行處理數(shù)據(jù) (深度學(xué)習(xí))
- 提供低功耗接口
- 可以和 CPU 共享資源內(nèi)存
這樣可能有些抽象,層級的關(guān)系大概如下宏多,我們平時更多的接觸的上面兩層筐喳。:
UIKit -> Core Graphics -> Metal/OpenGL ES -> GPU Driver -> GPU
GPU 相關(guān)知識
為了更好的理解 Metal 的工作流程和機制荣月,這里補充一些 GPU 工作相關(guān)流程管呵。
手機包含兩個不同的處理單元,CPU 和 GPU哺窄。CPU 是個多面手捐下,并且不得不處理所有的事情账锹,而 GPU 則可以集中來處理好一件事情,就是并行地做浮點運算坷襟。事實上奸柬,圖像處理和渲染就是在將要渲染到窗口上的像素上做許許多多的浮點運算。
通過有效的利用 GPU婴程,可以成百倍甚至上千倍地提高手機上的圖像渲染能力廓奕。如果不是基于 GPU 的處理,手機上實時高清視頻濾鏡是不現(xiàn)實档叔,甚至不可能的桌粉。
精細(xì)到屏幕繪制的每一幀上,每次準(zhǔn)備畫下一幀前衙四,屏幕會發(fā)出一個垂直同步信號(vertical synchronization)铃肯,簡稱 VSync
屏幕通常以固定頻率進行刷新,這個刷新率就是 VSync 信號產(chǎn)生的頻率传蹈。
一般來說押逼,計算機系統(tǒng)中 CPU、GPU惦界、屏幕是以上面這種方式協(xié)同工作的挑格。CPU 計算好顯示內(nèi)容提交到 GPU,GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū)表锻,隨后視頻控制器會按照 VSync 信號逐行讀取幀緩沖區(qū)的數(shù)據(jù)恕齐,經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給屏幕顯示。
基礎(chǔ)流程
這邊以通過 Metal 渲染一個三角形作為例子瞬逊,來介紹一下基本的使用显歧。
Xcode 版本 8.3.3 ,語言 Objective-C
需要注意的是 Metal 必須在真機上運行确镊,并且至少要是 A7 處理器士骤,就是 5s 或者以上。
初始化
新建一個普通的工程 Single View Application蕾域,在 VC 中導(dǎo)入 Metal Framework拷肌。
#import <Metal/Metal.h>
MTLDevice
都說是操作 GPU 了,當(dāng)然我們要拿到 GPU 對象旨巷,Metal 中提供了 MTLDevice 的接口巨缘,代表了 GPU。
//獲取設(shè)備
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
if (device == nil) {
NSLog(@"don't support metal !");
return;
}
當(dāng)設(shè)備不支持 Metal 的時候會返回空采呐。
MTLDevice 代表 GPU 的接口若锁,提供了如下的能力:
- 查詢設(shè)備狀態(tài)
- 創(chuàng)建 buffer 和 texture
- 指令轉(zhuǎn)換和隊列化渲染進行指令的計算
MTLCommandQueue
有了 GPU 之后,我們需要一個渲染隊列 MTLCommandQueue斧吐,隊列是單一隊列又固,確保了指令能夠按順序執(zhí)行仲器,里面的是將要渲染的指令 MTLCommandBuffer,這是個線程安全的隊列仰冠,可以支持多個 CommandBuffer 同時編碼乏冀。
通過 MTLDevice 可以獲取隊列
id<MTLCommandQueue> queue = self.device.newCommandQueue;
MTKView
要用 Metal 來直接繪制的話,需要用特殊的界面 MTKView洋只,同時給它設(shè)置對應(yīng)的 device 為我們上面獲取到 MTLDevice辆沦,并把它添加到當(dāng)前的界面中。
_mtkView = [[MTKView alloc] initWithFrame:self.view.frame device:_device];
[self.view addSubview:_mtkView];
渲染
我們配置好 MTLDevice木张,MTLCommandQueue 和 MTKView 之后众辨,我們開始準(zhǔn)備需要渲染到界面上的內(nèi)容了,就是要塞進隊列中的緩沖數(shù)據(jù) MTLCommandBuffer 舷礼。
簡單的流程就是先構(gòu)造 MTLCommandBuffer 鹃彻,再配置 CommandEncoder ,包括配置資源文件妻献,渲染管線等蛛株,再通過 CommandEncoder 進行編碼,最后才能提交到隊列中去育拨。
MTLCommandBuffer
有了隊列之后谨履,我們開始構(gòu)建隊列中的 MTLCommandBuffer,一開始獲取的 Buffer 是空的熬丧,要通過 MTLCommandEncoder 編碼器來 Encode 笋粟,一個 Buffer 可以被多個 Encoder 進行編碼。
MTLCommandBuffer 是包含了多種類型的命令編碼 - 根據(jù)不同的 編碼器 決定 包含了哪些數(shù)據(jù)析蝴。 通常情況下害捕,app 的一幀就是渲染為一個單獨的 Command Buffer。MTLCommandBuffer 是不支持重用的輕量級的對象闷畸,每次需要的時候都是獲取一個新的 Buffer尝盼。
Buffer 有方法可以 Label ,用來增加標(biāo)簽佑菩,方便調(diào)試時使用盾沫。
臨時對象,在執(zhí)行之后殿漠,唯一有效的操作就是等到被執(zhí)行或者完成的時候的回調(diào)赴精,同步或者通過 block 回調(diào),檢查 buffer 的運行結(jié)果绞幌。
創(chuàng)建
- MTLCommandQueue - commandBuffer 方法 蕾哟,只能加到創(chuàng)建它的隊列中。
- 獲取 retain 的對象 commandBufferWithUnretainedReferences 能夠重用 一般不推薦
這里我們通過如下方法創(chuàng)建
//command buffer
id<MTLCommandBuffer> commandBuffer = [_queue commandBuffer];
執(zhí)行
- enqueue 順序執(zhí)行
- commit 插隊盡快執(zhí)行 (如果前面有 commit 就還是排隊等著)
監(jiān)聽結(jié)果
commandBuffer.addCompletedHandler { (buffer) in
}
commandBuffer.waitUntilCompleted()
commandBuffer.addScheduledHandler { (buffer) in
}
commandBuffer.waitUntilScheduled()
創(chuàng)建 Metal 資源
接下來我需要把我們需要繪制的內(nèi)容 encode 到我們上面生成 MTLCommandBuffer 中。
現(xiàn)在我們要配置需要繪制的內(nèi)容渐苏,即資源。
在 Metal 中資源分為兩種:
- MTLBuffer 代表著未格式化的內(nèi)存菇夸,可以是任何類型的數(shù)據(jù)琼富。 Buffer 用來做頂點著色和計算狀態(tài)。
- MTLTexture 代表著有著特殊紋理類型和像素格式的格式化的圖像數(shù)據(jù)庄新。用來做頂點鞠眉,面和計算的源
我們這里是要畫一個三角形,所以要有三個頂點择诈,然后需要繪制三角形的圖片械蹋。
分別用 MTLBuffer 來讀入三個頂點。
在 Metal 中是歸一化的坐標(biāo)系羞芍,以屏幕中心為原點(0, 0, 0)哗戈,且是始終不變的。面對屏幕荷科,你的右邊是x正軸唯咬,上面是y正軸,屏幕指向你的為z正軸畏浆。長度單位這樣來定:窗口范圍按此單位恰好是(-1,-1)到(1,1)胆胰,即屏幕左下角坐標(biāo)為(-1,-1)刻获,右上角坐標(biāo)為(1,1)蜀涨。
所以我們要畫在中間一個正三角形的話,三個頂點分別為
(0.577, -0.25, 0.0, 1.0)
(-0.577, -0.25, 0.0, 1.0)
(0.0, 0.5, 0.0, 1.0)
在 Metal 里面代表頂點需要 4 個 float 蝎毡,代表 x厚柳,y,z顶掉,w草娜。最后二位我們繪制 2D 界面的時候默認(rèn)為0.0 和 1.0,w 是為了方便 3D 計算的痒筒。
我們要把頂點數(shù)據(jù)轉(zhuǎn)為字節(jié)宰闰,通過 MTLDevice 的 *- (id <MTLBuffer>)newBufferWithBytes:(const void )pointer length:(NSUInteger)length options:(MTLResourceOptions)options;
方法構(gòu)造為 MTLBuffer 。
static const float vertexArrayData[] = {
// 前 4 位 位置 x , y , z ,w
0.577, -0.25, 0.0, 1.0,
-0.577, -0.25, 0.0, 1.0,
0.0, 0.5, 0.0, 1.0,
};
id<MTLBuffer> vertexBuffer = [_device newBufferWithBytes:vertexArrayData
length:sizeof(vertexArrayData)
options:0];
有了頂點 Vertex 之后簿透,我們來構(gòu)建面 Fragment移袍。這里我們用一張圖片作為我們的三角形的貼圖。
首先獲取圖片的 image 對象:
UIImage *image = [UIImage imageNamed:name];
接下來通過 MTKTextureLoader 來構(gòu)建 MTLTexture
MTKTextureLoader *loader = [[MTKTextureLoader alloc]initWithDevice:self.device];
NSError* err;
id<MTLTexture> sourceTexture = [loader newTextureWithCGImage:image.CGImage options:nil error:&err];
return sourceTexture;
Shader (著色器) 和 Pipeline (渲染管線)
資源有了老充,我們要告訴 GPU 怎么去使用這些數(shù)據(jù)葡盗,這里就需要 Shader 了,這部分代碼是在 GPU 中執(zhí)行的啡浊,所以要用特殊的語言去編寫觅够,即 Metal Shading Language胶背,它是 C++ 14的超集,封裝了一些 Metal 的數(shù)據(jù)格式和常用方法喘先。
你可以添加多個 Metal 文件钳吟,最后都會編譯到二進制文件default.metallib 中。
通過 Xcode 的 File - New - File 菜單窘拯,新建一個 Metal 文件红且。
添加下面兩個函數(shù),分別代表頂點的處理函數(shù)涤姊,和 片段處理函數(shù)暇番。
#include <metal_stdlib>
using namespace metal;
typedef struct
{
float4 position;
float2 texCoords;
} VertexIn;
typedef struct
{
float4 position [[position]];
float2 texCoords;
}VertexOut;
vertex VertexOut myVertexShader(const device VertexIn* vertexArray [[buffer(0)]],
unsigned int vid [[vertex_id]]){
VertexOut verOut;
verOut.position = vertexArray[vid].position;
verOut.texCoords = vertexArray[vid].texCoords;
return verOut;
}
fragment float4 myFragmentShader(
VertexOut vertexIn [[stage_in]],
texture2d<float,access::sample> inputImage [[ texture(0) ]],
sampler textureSampler [[sampler(0)]]
)
{
float4 color = inputImage.sample(textureSampler, vertexIn.texCoords);
return color;
}
兩個結(jié)構(gòu)體
VertexIn 和 VertexOut
里面的 float4 和 float2 代表著 4 個和 2 個浮點數(shù)的向量。
可以通過如下方式構(gòu)造和取值思喊,具體的不展開可以查看相關(guān)文檔壁酬。
float4(1.0) = float4(1.0,1.0,1.0,1.0)
float4 test = float4(1,2,3,4)
test.x = test.r = 1
test.y = test.g = 2
test.z = test.b = 3
test.w = test.a = 4
...
myVertexShader 為方法名,vertex 代表是一個頂點函數(shù) VertexOut 代表返回值搔涝,該方法有兩個入?yún)ⅰ?/p>
vertexArray 后面的 buff(0) 代表去后面配置的 index 為 0 的 MTLBuffer 資源
-
vid 代表著進入的頂點的 id 即順序厨喂。
其實還有很多入?yún)⑼ㄟ^查閱文檔可以看到- [[vertex_id]]
- [[instance_id]]
- [[base_vertex]]
- [[base_instance]]
這里可以對頂點進行處理,如轉(zhuǎn)向庄呈,3D 場景下的光影的計算等等蜕煌,然后返回處理之后的頂點信息,這里直接返回诬留,并沒有做額外的處理斜纪。
myFragmentShader 同上,fragment 代表是一個處理片段的方法文兑,方法有三個入?yún)?/p>
VertexOut vertexIn [[stage_in]] 代表著從頂點返回的頂點信息
texture2d<float,access::sample> inputImage [[ texture(0) ]] 讀入的圖片資源
sampler textureSampler 采樣器
頂點著色器返回了 VertexOut 結(jié)構(gòu)體盒刚,通過 [[stage_in]] 入?yún)ⅲ闹禃歉鶕?jù)你的渲染的位置來插值绿贞。所以這個方法的主要內(nèi)容就是根據(jù)因块,之前返回的頂點信息,去圖像中采樣得到相應(yīng)位置的樣色涡上,并返回顏色。
渲染管線
著色器這邊的工作已經(jīng)完成拒名,下面我們需要把它和我們的 CommandBuffer 關(guān)聯(lián)起來吩愧,就需要我們的 PipelineState 渲染管線了。
渲染管線就好比是 CPU 和 GPU 直接的管道增显,通過它來配置運行在 GPU 中的頂點和段著色器雁佳,就是我們寫在 metal 中的編譯好的代碼,多個 c++ 函數(shù)的組合。
PipelineState 對象是線程安全的糖权,所以這個對象是可以復(fù)用的堵腹,不同的 CommandBuffer 都可以使用它,創(chuàng)建它是有性能消耗的星澳,建議和 Device 和 Queue 一起初始化并作為全局對象秸滴。
生成 PipelineState 對象需要獲取我們剛剛寫在 Metal 中的幾個函數(shù)。
通過下面的方法募判,我們可以得到代表整個 Metal 的函數(shù)庫 MTLLibrary 對象。
id<MTLLibrary> library = [_device newDefaultLibrary];
通過 MTLLibrary 的 newFunctionWithName 方法咒唆,可以得到對應(yīng)的方法届垫。
[library newFunctionWithName:@"myVertexShader"];
下面我們開始構(gòu)造我們的 MTLRenderPipelineState
//構(gòu)造Pipeline
MTLRenderPipelineDescriptor *des = [MTLRenderPipelineDescriptor new];
//獲取 shader 的函數(shù)
id<MTLLibrary> library = [_device newDefaultLibrary];
des.vertexFunction = [library newFunctionWithName:@"myVertexShader"];
des.fragmentFunction = [library newFunctionWithName:@"myFragmentShader"];
des.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
//生成 MTLRenderPipelineState
NSError *error;
_pipelineState = [_device newRenderPipelineStateWithDescriptor:des
error:&error];
MTLCommandEncoder 編碼器
有了資源文件,渲染管線之后全释,我們可以開始做最后的步驟了装处,構(gòu)造 MTLCommandEncoder 編碼器。
指令編碼器包括 渲染 計算 位圖復(fù)制三種編碼器浸船。
- MTLRenderCommandEncoder 渲染 3D 編碼器
- MTLComputeCommandEncoder 計算編碼器
- MTLBlitCommandEncoder 位圖復(fù)制編碼器 拷貝 buffer texture 同時也能生成 mipmap
mipmap 指的是一種紋理映射技術(shù)妄迁,將低一級圖像的每邊的分辨率取為高一級圖像的每邊的分辨率的二分之一,而同一級分辨率的紋理組則由紅李命、綠登淘、藍(lán)三個分量的紋理數(shù)組組成。由于這一個查找表包含了同一紋理區(qū)域在不同分辨率下的紋理顏色值封字,因此被稱為 Mipmap黔州。比如一張 64x64 的圖片,會生成 32x32,16x16 等阔籽,需要 20x20 的話就會用 32x32 和 16x16 的進行計算流妻,大大的提高渲染的效率。
這里我們是為了渲染一個三角形笆制,所以這里用的是 MTLRenderCommandEncoder 绅这。
相關(guān)代碼如下
- 創(chuàng)建 MTLRenderPassDescriptor 描述符 配置一些基本參數(shù)
- 通過描述符構(gòu)建 Encoder
- 配置 VertexBuffer 后面的 index 就是 Shader 里面對應(yīng) [[buffer[0]]] 的 0 【index 最多是 31 個】
- 配置 FragmentTexture
- 設(shè)置渲染的頂點配置(這里設(shè)置為三角 從第一個頂點開始取 取 3 個)
- 編碼結(jié)束
//render des
MTLRenderPassDescriptor *renderDes = [MTLRenderPassDescriptor new];
renderDes.colorAttachments[0].texture = drawable.texture;
renderDes.colorAttachments[0].loadAction = MTLLoadActionClear;
renderDes.colorAttachments[0].storeAction = MTLStoreActionStore;
renderDes.colorAttachments[0].clearColor = MTLClearColorMake(0.5, 0.65, 0.8, 1); //background color
//command encoder
id<MTLRenderCommandEncoder> encoder = [commandBuffer renderCommandEncoderWithDescriptor:renderDes];
[encoder setCullMode:MTLCullModeNone];
[encoder setFrontFacingWinding:MTLWindingCounterClockwise];
[encoder setRenderPipelineState:self.pipelineState];
[encoder setVertexBuffer:self.vertexBuffer offset:0 atIndex:0];
[encoder setFragmentTexture:textture atIndex:0];
//set render vertex
[encoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:3];
[encoder endEncoding];
繪制
編碼結(jié)束之后,就可以開始準(zhǔn)備提交到 GPU 了在辆。
配置需要繪制的 Layer证薇,獲取 MTKView 的 Layer 就可以。
CAMetalLayer *metalLayer = (CAMetalLayer*)[_mtkView layer];
id<CAMetalDrawable> drawable = [metalLayer nextDrawable];
//commit
[commandBuffer presentDrawable:drawable];
[commandBuffer commit];
現(xiàn)在所有的工作就都完成了开缎,運行項目就可以看到如下的三角形了棕叫,里面填充的是我之前導(dǎo)入的圖片。
調(diào)試
如何進行調(diào)試和評估性能呢奕删?
這里 iOS 提供了兩個工具
- Xcode 中的 Capute GPU Frame
- Instruments 中的 Metal System Trace
Capute GPU Frame
第一個是用來 Debug 的工具俺泣,運行的時候點擊 Debug ,選擇 Capute GPU Frame,就會看到如下的界面伏钠,相關(guān)的說明我已經(jīng)附在圖上了横漏,用法和 Capute View Hierachy 很像。
比較強大的一個功能是點擊動態(tài)更新的按鈕可以在修改完之后直接應(yīng)用熟掂,避免了 app 編譯帶來的時間消耗缎浇。
Metal System Trace
- 打開 Instruments 之后選擇需要調(diào)試的應(yīng)用
- 點擊 record 之后開始錄制
- 完成之后點擊停止,分析之后會有如下界面
從上到下分別是 Application 在 CPU 中執(zhí)行赴肚,對應(yīng)的是 Buffer 和 Encoder 的初始化工作
隨著箭頭往下是 Graphic Driver Activity 素跺,在 GPU 驅(qū)動處理,這部分操作也是在 CPU 中誉券。
再往下就是進入到 GPU 了指厌,就部分才是真正的工作。
最后是到 Display 就是展示界面了踊跟,在 Display 下面是 Vsync 信號踩验,代表著同步信號,用來刷新界面商玫。
放大之后可以看到詳細(xì)的 Buffer / Render 箕憾,而且上面顯示的名字,正是 之前設(shè)置的 Label 的名字拳昌。
總結(jié)
流程總結(jié)
最后我們再來通過下面這個圖來梳理下的流程袭异。
- 配置 Device 和 Queue
- 獲取 CommandBuffer
- 配置 CommandBufferEncoder
- 配置 PipelineState
- 創(chuàng)建資源
- Encoder Buffer 【如有需要的話可以用 Threadgroups 來分組 Encoder 數(shù)據(jù)】
-
提交到 Queue 中
Metal 能力
根據(jù)不同的 CommandBufferEncoder 可以提供不同的能力,除了優(yōu)秀的 3D 渲染能力炬藤,Metal 還能提供強大的計算能力扁远。
在 WWDC 2015,蘋果發(fā)布了 Metal Performance Shaders (MPS) 框架刻像,iOS 9 上的一組高性能的圖像濾鏡畅买,其實就是邊寫好的 Shaders,提供了優(yōu)秀的圖像處理能力细睡。同時還提供了高性能的矩陣運算的 Shaders 谷羞,能用來做機器學(xué)習(xí)的運算,在 GPU 上運行卷積神經(jīng)網(wǎng)絡(luò)溜徙。
而且非常棒的是湃缎,今年的 WWDC 2017 上 Metal 也將開始支持 macOS 。
更多的實踐可以參考蘋果的官方文檔:
Metal 的最佳實踐
我們可以用來做什么蠢壹?
- 圖片處理 濾鏡/調(diào)整
- 視頻處理
- 機器學(xué)習(xí)
- 大計算工作 分擔(dān) CPU 壓力
參考
MetalProgrammingGuide : https://developer.apple.com/library/content/documentation/Miscellaneous/Conceptual/MetalProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40014221-CH1-SW1
metal-image-processing : https://www.invasivecode.com/weblog/metal-image-processing
Metal Shading Language : https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf#//apple_ref/doc/uid/TP40014364
the-metal-shading-language-in-practice : https://www.objc.io/issues/18-games/metal/#the-metal-shading-language-in-practice
metal-performance-shaders-in-swift : http://metalbyexample.com/metal-performance-shaders-in-swift/