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 平臺獨(dú)有的,意味著它不能像 OpenGL ES 那樣支持跨平臺,但是它能最大的挖掘蘋果移動設(shè)備的 GPU 能力,進(jìn)行復(fù)雜的運(yùn)算,像 Unity 等游戲引擎都通過 Metal 對 3D 能力進(jìn)行了優(yōu)化, App Store 還有相應(yīng)的運(yùn)用 Metal 技術(shù)的游戲?qū)n}。
Metal 具有特點(diǎn)
- 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 的工作流程和機(jī)制,這里補(bǔ)充一些 GPU 工作相關(guān)流程。
手機(jī)包含兩個不同的處理單元,CPU 和 GPU。CPU 是個多面手,并且不得不處理所有的事情,而 GPU 則可以集中來處理好一件事情,就是并行地做浮點(diǎn)運(yùn)算。事實(shí)上,圖像處理和渲染就是在將要渲染到窗口上的像素上做許許多多的浮點(diǎn)運(yùn)算。
通過有效的利用 GPU,可以成百倍甚至上千倍地提高手機(jī)上的圖像渲染能力。如果不是基于 GPU 的處理,手機(jī)上實(shí)時高清視頻濾鏡是不現(xiàn)實(shí),甚至不可能的。
精細(xì)到屏幕繪制的每一幀上,每次準(zhǔn)備畫下一幀前,屏幕會發(fā)出一個垂直同步信號(vertical synchronization),簡稱 VSync
屏幕通常以固定頻率進(jìn)行刷新,這個刷新率就是 VSync 信號產(chǎn)生的頻率。
一般來說,計(jì)算機(jī)系統(tǒng)中 CPU、GPU、屏幕是以上面這種方式協(xié)同工作的。CPU 計(jì)算好顯示內(nèi)容提交到 GPU,GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū),隨后視頻控制器會按照 VSync 信號逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給屏幕顯示。
基礎(chǔ)流程
這邊以通過 Metal 渲染一個三角形作為例子,來介紹一下基本的使用。
Xcode 版本 8.3.3 ,語言 Objective-C
需要注意的是 Metal 必須在真機(jī)上運(yùn)行,并且至少要是 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)換和隊(duì)列化渲染進(jìn)行指令的計(jì)算
MTLCommandQueue
有了 GPU 之后,我們需要一個渲染隊(duì)列 MTLCommandQueue,隊(duì)列是單一隊(duì)列,確保了指令能夠按順序執(zhí)行,里面的是將要渲染的指令 MTLCommandBuffer,這是個線程安全的隊(duì)列,可以支持多個 CommandBuffer 同時編碼。
通過 MTLDevice 可以獲取隊(duì)列
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)容了,就是要塞進(jìn)隊(duì)列中的緩沖數(shù)據(jù) MTLCommandBuffer 。
簡單的流程就是先構(gòu)造 MTLCommandBuffer ,再配置 CommandEncoder ,包括配置資源文件,渲染管線等,再通過 CommandEncoder 進(jìn)行編碼,最后才能提交到隊(duì)列中去。
MTLCommandBuffer
有了隊(duì)列之后,我們開始構(gòu)建隊(duì)列中的 MTLCommandBuffer,一開始獲取的 Buffer 是空的,要通過 MTLCommandEncoder 編碼器來 Encode ,一個 Buffer 可以被多個 Encoder 進(jìn)行編碼。
MTLCommandBuffer 是包含了多種類型的命令編碼 - 根據(jù)不同的 編碼器 決定 包含了哪些數(shù)據(jù)。 通常情況下,app 的一幀就是渲染為一個單獨(dú)的 Command Buffer。MTLCommandBuffer 是不支持重用的輕量級的對象,每次需要的時候都是獲取一個新的 Buffer。
Buffer 有方法可以 Label ,用來增加標(biāo)簽,方便調(diào)試時使用。
臨時對象,在執(zhí)行之后,唯一有效的操作就是等到被執(zhí)行或者完成的時候的回調(diào),同步或者通過 block 回調(diào),檢查 buffer 的運(yùn)行結(jié)果。
創(chuàng)建
- MTLCommandQueue - commandBuffer 方法 ,只能加到創(chuàng)建它的隊(duì)列中。
- 獲取 retain 的對象 commandBufferWithUnretainedReferences 能夠重用 一般不推薦
這里我們通過如下方法創(chuàng)建
//command buffer
id<MTLCommandBuffer> commandBuffer = [_queue commandBuffer];
執(zhí)行
- enqueue 順序執(zhí)行
- commit 插隊(duì)盡快執(zhí)行 (如果前面有 commit 就還是排隊(duì)等著)
監(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 用來做頂點(diǎn)著色和計(jì)算狀態(tài)。
- MTLTexture 代表著有著特殊紋理類型和像素格式的格式化的圖像數(shù)據(jù)。用來做頂點(diǎn),面和計(jì)算的源
我們這里是要畫一個三角形,所以要有三個頂點(diǎn),然后需要繪制三角形的圖片。
分別用 MTLBuffer 來讀入三個頂點(diǎn)。
在 Metal 中是歸一化的坐標(biāo)系,以屏幕中心為原點(diǎn)(0, 0, 0),且是始終不變的。面對屏幕,你的右邊是x正軸,上面是y正軸,屏幕指向你的為z正軸。長度單位這樣來定:窗口范圍按此單位恰好是(-1,-1)到(1,1),即屏幕左下角坐標(biāo)為(-1,-1),右上角坐標(biāo)為(1,1)。
所以我們要畫在中間一個正三角形的話,三個頂點(diǎn)分別為
(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 里面代表頂點(diǎn)需要 4 個 float ,代表 x,y,z,w。最后二位我們繪制 2D 界面的時候默認(rèn)為0.0 和 1.0,w 是為了方便 3D 計(jì)算的。
我們要把頂點(diǎn)數(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];
有了頂點(diǎn) 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 文件,最后都會編譯到二進(jìn)制文件default.metallib 中。
通過 Xcode 的 File - New - File 菜單,新建一個 Metal 文件。
添加下面兩個函數(shù),分別代表頂點(diǎn)的處理函數(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 個浮點(diǎn)數(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 代表是一個頂點(diǎn)函數(shù) VertexOut 代表返回值,該方法有兩個入?yún)ⅰ?/p>
vertexArray 后面的 buff(0) 代表去后面配置的 index 為 0 的 MTLBuffer 資源
-
vid 代表著進(jìn)入的頂點(diǎn)的 id 即順序。
其實(shí)還有很多入?yún)⑼ㄟ^查閱文檔可以看到- [[vertex_id]]
- [[instance_id]]
- [[base_vertex]]
- [[base_instance]]
這里可以對頂點(diǎn)進(jìn)行處理,如轉(zhuǎn)向,3D 場景下的光影的計(jì)算等等,然后返回處理之后的頂點(diǎn)信息,這里直接返回,并沒有做額外的處理。
myFragmentShader 同上,fragment 代表是一個處理片段的方法,方法有三個入?yún)?/p>
VertexOut vertexIn [[stage_in]] 代表著從頂點(diǎn)返回的頂點(diǎn)信息
texture2d<float,access::sample> inputImage [[ texture(0) ]] 讀入的圖片資源
sampler textureSampler 采樣器
頂點(diǎn)著色器返回了 VertexOut 結(jié)構(gòu)體,通過 [[stage_in]] 入?yún)?,它的值會是根?jù)你的渲染的位置來插值。所以這個方法的主要內(nèi)容就是根據(jù),之前返回的頂點(diǎn)信息,去圖像中采樣得到相應(yīng)位置的樣色,并返回顏色。
渲染管線
著色器這邊的工作已經(jīng)完成,下面我們需要把它和我們的 CommandBuffer 關(guān)聯(lián)起來,就需要我們的 PipelineState 渲染管線了。
渲染管線就好比是 CPU 和 GPU 直接的管道,通過它來配置運(yùn)行在 GPU 中的頂點(diǎn)和段著色器,就是我們寫在 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 編碼器。
指令編碼器包括 渲染 計(jì)算 位圖復(fù)制三種編碼器。
- MTLRenderCommandEncoder 渲染 3D 編碼器
- MTLComputeCommandEncoder 計(jì)算編碼器
- MTLBlitCommandEncoder 位圖復(fù)制編碼器 拷貝 buffer texture 同時也能生成 mipmap
mipmap 指的是一種紋理映射技術(shù),將低一級圖像的每邊的分辨率取為高一級圖像的每邊的分辨率的二分之一,而同一級分辨率的紋理組則由紅、綠、藍(lán)三個分量的紋理數(shù)組組成。由于這一個查找表包含了同一紋理區(qū)域在不同分辨率下的紋理顏色值,因此被稱為 Mipmap。比如一張 64x64 的圖片,會生成 32x32,16x16 等,需要 20x20 的話就會用 32x32 和 16x16 的進(jìn)行計(jì)算,大大的提高渲染的效率。
這里我們是為了渲染一個三角形,所以這里用的是 MTLRenderCommandEncoder 。
相關(guān)代碼如下
- 創(chuàng)建 MTLRenderPassDescriptor 描述符 配置一些基本參數(shù)
- 通過描述符構(gòu)建 Encoder
- 配置 VertexBuffer 后面的 index 就是 Shader 里面對應(yīng) [[buffer[0]]] 的 0 【index 最多是 31 個】
- 配置 FragmentTexture
- 設(shè)置渲染的頂點(diǎn)配置(這里設(shè)置為三角 從第一個頂點(diǎn)開始取 取 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)在所有的工作就都完成了,運(yùn)行項(xiàng)目就可以看到如下的三角形了,里面填充的是我之前導(dǎo)入的圖片。
調(diào)試
如何進(jìn)行調(diào)試和評估性能呢?
這里 iOS 提供了兩個工具
- Xcode 中的 Capute GPU Frame
- Instruments 中的 Metal System Trace
Capute GPU Frame
第一個是用來 Debug 的工具,運(yùn)行的時候點(diǎn)擊 Debug ,選擇 Capute GPU Frame,就會看到如下的界面,相關(guān)的說明我已經(jīng)附在圖上了,用法和 Capute View Hierachy 很像。
比較強(qiáng)大的一個功能是點(diǎn)擊動態(tài)更新的按鈕可以在修改完之后直接應(yīng)用,避免了 app 編譯帶來的時間消耗。
Metal System Trace
- 打開 Instruments 之后選擇需要調(diào)試的應(yīng)用
- 點(diǎn)擊 record 之后開始錄制
- 完成之后點(diǎn)擊停止,分析之后會有如下界面
從上到下分別是 Application 在 CPU 中執(zhí)行,對應(yīng)的是 Buffer 和 Encoder 的初始化工作
隨著箭頭往下是 Graphic Driver Activity ,在 GPU 驅(qū)動處理,這部分操作也是在 CPU 中。
再往下就是進(jìn)入到 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 還能提供強(qiáng)大的計(jì)算能力。
在 WWDC 2015,蘋果發(fā)布了 Metal Performance Shaders (MPS) 框架,iOS 9 上的一組高性能的圖像濾鏡,其實(shí)就是邊寫好的 Shaders,提供了優(yōu)秀的圖像處理能力。同時還提供了高性能的矩陣運(yùn)算的 Shaders ,能用來做機(jī)器學(xué)習(xí)的運(yùn)算,在 GPU 上運(yùn)行卷積神經(jīng)網(wǎng)絡(luò)。
而且非常棒的是,今年的 WWDC 2017 上 Metal 也將開始支持 macOS 。
更多的實(shí)踐可以參考蘋果的官方文檔:
Metal 的最佳實(shí)踐
我們可以用來做什么?
- 圖片處理 濾鏡/調(diào)整
- 視頻處理
- 機(jī)器學(xué)習(xí)
- 大計(jì)算工作 分擔(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/