Metal 系列教程(1)- Metal 介紹及基本使用

Metal 介紹及基本使用

最近作的一個技術研究,metal 的國內相關資料不多,因此整理了這一系列文章,但願能幫到有用的人。html

什麼是 Metal

Metal 是一個和 OpenGL ES 相似的面向底層的圖形編程接口,經過使用相關的 api 能夠直接操做 GPU ,最先在 2014 年的 WWDC 的時候發佈,並於今年發佈了 Metal 2。
Metal 是 iOS 平臺獨有的,意味着它不能像 OpenGL ES 那樣支持跨平臺,可是它能最大的挖掘蘋果移動設備的 GPU 能力,進行復雜的運算,像 Unity 等遊戲引擎都經過 Metal 對 3D 能力進行了優化, App Store 還有相應的運用 Metal 技術的遊戲專題。c++

Metal 具備特色web

  • GPU 支持的 3D 渲染
  • 和 CPU 並行處理數據 (深度學習)
  • 提供低功耗接口
  • 能夠和 CPU 共享資源內存

這樣可能有些抽象,層級的關係大概以下,咱們平時更多的接觸的上面兩層。:
UIKit -> Core Graphics -> Metal/OpenGL ES -> GPU Driver -> GPU編程

GPU 相關知識

爲了更好的理解 Metal 的工做流程和機制,這裏補充一些 GPU 工做相關流程。swift

手機包含兩個不一樣的處理單元,CPU 和 GPU。CPU 是個多面手,而且不得不處理全部的事情,而 GPU 則能夠集中來處理好一件事情,就是並行地作浮點運算。事實上,圖像處理和渲染就是在將要渲染到窗口上的像素上作許許多多的浮點運算。
經過有效的利用 GPU,能夠成百倍甚至上千倍地提升手機上的圖像渲染能力。若是不是基於 GPU 的處理,手機上實時高清視頻濾鏡是不現實,甚至不可能的。
精細到屏幕繪製的每一幀上,每次準備畫下一幀前,屏幕會發出一個垂直同步信號(vertical synchronization),簡稱 VSync
屏幕一般以固定頻率進行刷新,這個刷新率就是 VSync 信號產生的頻率。api

通常來講,計算機系統中 CPU、GPU、屏幕是以上面這種方式協同工做的。CPU 計算好顯示內容提交到 GPU,GPU 渲染完成後將渲染結果放入幀緩衝區,隨後視頻控制器會按照 VSync 信號逐行讀取幀緩衝區的數據,通過可能的數模轉換傳遞給屏幕顯示。數組

基礎流程

這邊以經過 Metal 渲染一個三角形做爲例子,來介紹一下基本的使用。安全

Xcode 版本 8.3.3 ,語言 Objective-Cbash

須要注意的是 Metal 必須在真機上運行,而且至少要是 A7 處理器,就是 5s 或者以上。網絡

初始化

新建一個普通的工程 Single View Application,在 VC 中導入 Metal Framework。

#import <Metal/Metal.h>複製代碼
MTLDevice

都說是操做 GPU 了,固然咱們要拿到 GPU 對象,Metal 中提供了 MTLDevice 的接口,表明了 GPU。

//獲取設備
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
if (device == nil) {
    NSLog(@"don't support metal !");
    return;
}複製代碼

當設備不支持 Metal 的時候會返回空。

MTLDevice 表明 GPU 的接口,提供了以下的能力:

  • 查詢設備狀態
  • 建立 buffer 和 texture
  • 指令轉換和隊列化渲染進行指令的計算
MTLCommandQueue

有了 GPU 以後,咱們須要一個渲染隊列 MTLCommandQueue,隊列是單一隊列,確保了指令可以按順序執行,裏面的是將要渲染的指令 MTLCommandBuffer,這是個線程安全的隊列,能夠支持多個 CommandBuffer 同時編碼。
經過 MTLDevice 能夠獲取隊列

id<MTLCommandQueue> queue = self.device.newCommandQueue;複製代碼
MTKView

要用 Metal 來直接繪製的話,須要用特殊的界面 MTKView,同時給它設置對應的 device 爲咱們上面獲取到 MTLDevice,並把它添加到當前的界面中。

_mtkView = [[MTKView alloc] initWithFrame:self.view.frame device:_device];
[self.view addSubview:_mtkView];複製代碼

渲染

咱們配置好 MTLDevice,MTLCommandQueue 和 MTKView 以後,咱們開始準備須要渲染到界面上的內容了,就是要塞進隊列中的緩衝數據 MTLCommandBuffer 。
簡單的流程就是先構造 MTLCommandBuffer ,再配置 CommandEncoder ,包括配置資源文件,渲染管線等,再經過 CommandEncoder 進行編碼,最後才能提交到隊列中去。

MTLCommandBuffer

有了隊列以後,咱們開始構建隊列中的 MTLCommandBuffer,一開始獲取的 Buffer 是空的,要經過 MTLCommandEncoder 編碼器來 Encode ,一個 Buffer 能夠被多個 Encoder 進行編碼。

MTLCommandBuffer 是包含了多種類型的命令編碼 - 根據不一樣的 編碼器 決定 包含了哪些數據。 一般狀況下,app 的一幀就是渲染爲一個單獨的 Command Buffer。MTLCommandBuffer 是不支持重用的輕量級的對象,每次須要的時候都是獲取一個新的 Buffer。

Buffer 有方法能夠 Label ,用來增長標籤,方便調試時使用。

臨時對象,在執行以後,惟一有效的操做就是等到被執行或者完成的時候的回調,同步或者經過 block 回調,檢查 buffer 的運行結果。

建立

  • MTLCommandQueue - commandBuffer 方法 ,只能加到建立它的隊列中。
  • 獲取 retain 的對象 commandBufferWithUnretainedReferences 可以重用 通常不推薦

這裏咱們經過以下方法建立

//command buffer
    id<MTLCommandBuffer> commandBuffer = [_queue commandBuffer];複製代碼

執行

  • enqueue 順序執行
  • commit 插隊儘快執行 (若是前面有 commit 就仍是排隊等着)

監聽結果

commandBuffer.addCompletedHandler { (buffer) in
}
commandBuffer.waitUntilCompleted()

commandBuffer.addScheduledHandler { (buffer) in
}
commandBuffer.waitUntilScheduled()複製代碼
建立 Metal 資源

接下來我須要把咱們須要繪製的內容 encode 到咱們上面生成 MTLCommandBuffer 中。

如今咱們要配置須要繪製的內容,即資源。
在 Metal 中資源分爲兩種:

  • MTLBuffer 表明着未格式化的內存,能夠是任何類型的數據。 Buffer 用來作頂點着色和計算狀態。
  • MTLTexture 表明着有着特殊紋理類型和像素格式的格式化的圖像數據。用來作頂點,面和計算的源

咱們這裏是要畫一個三角形,因此要有三個頂點,而後須要繪製三角形的圖片。
分別用 MTLBuffer 來讀入三個頂點。

在 Metal 中是歸一化的座標系,以屏幕中心爲原點(0, 0, 0),且是始終不變的。面對屏幕,你的右邊是x正軸,上面是y正軸,屏幕指向你的爲z正軸。長度單位這樣來定:窗口範圍按此單位剛好是(-1,-1)到(1,1),即屏幕左下角座標爲(-1,-1),右上角座標爲(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 界面的時候默認爲0.0 和 1.0,w 是爲了方便 3D 計算的。

咱們要把頂點數據轉爲字節,經過 MTLDevice 的 - (id )newBufferWithBytes:(const void *)pointer length:(NSUInteger)length options:(MTLResourceOptions)options;
方法構造爲 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 以後,咱們來構建面 Fragment。這裏咱們用一張圖片做爲咱們的三角形的貼圖。
首先獲取圖片的 image 對象:

UIImage *image = [UIImage imageNamed:name];複製代碼

接下來經過 MTKTextureLoader 來構建 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 怎麼去使用這些數據,這裏就須要 Shader 了,這部分代碼是在 GPU 中執行的,因此要用特殊的語言去編寫,即 Metal Shading Language,它是 C++ 14的超集,封裝了一些 Metal 的數據格式和經常使用方法。
你能夠添加多個 Metal 文件,最後都會編譯到二進制文件default.metallib 中。
經過 Xcode 的 File - New - File 菜單,新建一個 Metal 文件。

meta
meta

添加下面兩個函數,分別表明頂點的處理函數,和 片斷處理函數。

#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;

}複製代碼

兩個結構體
VertexIn 和 VertexOut
裏面的 float4 和 float2 表明着 4 個和 2 個浮點數的向量。
能夠經過以下方式構造和取值,具體的不展開能夠查看相關文檔。

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 表明是一個頂點函數 VertexOut 表明返回值,該方法有兩個入參。

  • vertexArray 後面的 buff(0) 表明去後面配置的 index 爲 0 的 MTLBuffer 資源
  • vid 表明着進入的頂點的 id 即順序。
    其實還有不少入參經過查閱文檔能夠看到

    • [[vertex_id]]
    • [[instance_id]]
    • [[base_vertex]]
    • [[base_instance]]

這裏能夠對頂點進行處理,如轉向,3D 場景下的光影的計算等等,而後返回處理以後的頂點信息,這裏直接返回,並無作額外的處理。

myFragmentShader 同上,fragment 表明是一個處理片斷的方法,方法有三個入參

  • VertexOut vertexIn [[stage_in]] 表明着從頂點返回的頂點信息

  • texture2d inputImage [[ texture(0) ]] 讀入的圖片資源

  • sampler textureSampler 採樣器

頂點着色器返回了 VertexOut 結構體,經過 [[stage_in]] 入參,它的值會是根據你的渲染的位置來插值。因此這個方法的主要內容就是根據,以前返回的頂點信息,去圖像中採樣獲得相應位置的樣色,並返回顏色。

渲染管線

着色器這邊的工做已經完成,下面咱們須要把它和咱們的 CommandBuffer 關聯起來,就須要咱們的 PipelineState 渲染管線了。

渲染管線就比如是 CPU 和 GPU 直接的管道,經過它來配置運行在 GPU 中的頂點和段着色器,就是咱們寫在 metal 中的編譯好的代碼,多個 c++ 函數的組合。

PipelineState 對象是線程安全的,因此這個對象是能夠複用的,不一樣的 CommandBuffer 均可以使用它,建立它是有性能消耗的,建議和 Device 和 Queue 一塊兒初始化並做爲全局對象。

生成 PipelineState 對象須要獲取咱們剛剛寫在 Metal 中的幾個函數。
經過下面的方法,咱們能夠獲得表明整個 Metal 的函數庫 MTLLibrary 對象。

id<MTLLibrary> library = [_device newDefaultLibrary];複製代碼

經過 MTLLibrary 的 newFunctionWithName 方法,能夠獲得對應的方法。

[library newFunctionWithName:@"myVertexShader"];複製代碼

下面咱們開始構造咱們的 MTLRenderPipelineState

//構造Pipeline
    MTLRenderPipelineDescriptor *des = [MTLRenderPipelineDescriptor new];


    //獲取 shader 的函數
    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 編碼器

有了資源文件,渲染管線以後,咱們能夠開始作最後的步驟了,構造 MTLCommandEncoder 編碼器。
指令編碼器包括 渲染 計算 位圖複製三種編碼器。

  • MTLRenderCommandEncoder 渲染 3D 編碼器
  • MTLComputeCommandEncoder 計算編碼器
  • MTLBlitCommandEncoder 位圖複製編碼器 拷貝 buffer texture 同時也能生成 mipmap

mipmap 指的是一種紋理映射技術,將低一級圖像的每邊的分辨率取爲高一級圖像的每邊的分辨率的二分之一,而同一級分辨率的紋理組則由紅、綠、藍三個份量的紋理數組組成。因爲這一個查找表包含了同一紋理區域在不一樣分辨率下的紋理顏色值,所以被稱爲 Mipmap。好比一張 64x64 的圖片,會生成 32x32,16x16 等,須要 20x20 的話就會用 32x32 和 16x16 的進行計算,大大的提升渲染的效率。

這裏咱們是爲了渲染一個三角形,因此這裏用的是 MTLRenderCommandEncoder 。
相關代碼以下

  1. 建立 MTLRenderPassDescriptor 描述符 配置一些基本參數
  2. 經過描述符構建 Encoder
  3. 配置 VertexBuffer 後面的 index 就是 Shader 裏面對應 [[buffer[0]]] 的 0 【index 最可能是 31 個】
  4. 配置 FragmentTexture
  5. 設置渲染的頂點配置(這裏設置爲三角 從第一個頂點開始取 取 3 個)
  6. 編碼結束
//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];複製代碼

繪製

編碼結束以後,就能夠開始準備提交到 GPU 了。
配置須要繪製的 Layer,獲取 MTKView 的 Layer 就能夠。

CAMetalLayer *metalLayer = (CAMetalLayer*)[_mtkView layer];
id<CAMetalDrawable> drawable = [metalLayer nextDrawable];

//commit
[commandBuffer presentDrawable:drawable];
[commandBuffer commit];複製代碼

如今全部的工做就都完成了,運行項目就能夠看到以下的三角形了,裏面填充的是我以前導入的圖片。

調試

如何進行調試和評估性能呢?
這裏 iOS 提供了兩個工具

  • Xcode 中的 Capute GPU Frame
  • Instruments 中的 Metal System Trace

Capute GPU Frame
第一個是用來 Debug 的工具,運行的時候點擊 Debug ,選擇 Capute GPU Frame,就會看到以下的界面,相關的說明我已經附在圖上了,用法和 Capute View Hierachy 很像。


比較強大的一個功能是點擊動態更新的按鈕能夠在修改完以後直接應用,避免了 app 編譯帶來的時間消耗。

Metal System Trace

  1. 打開 Instruments 以後選擇須要調試的應用
  2. 點擊 record 以後開始錄製
  3. 完成以後點擊中止,分析以後會有以下界面

從上到下分別是 Application 在 CPU 中執行,對應的是 Buffer 和 Encoder 的初始化工做
隨着箭頭往下是 Graphic Driver Activity ,在 GPU 驅動處理,這部分操做也是在 CPU 中。
再往下就是進入到 GPU 了,就部分纔是真正的工做。
最後是到 Display 就是展現界面了,在 Display 下面是 Vsync 信號,表明着同步信號,用來刷新界面。

放大以後能夠看到詳細的 Buffer / Render ,並且上面顯示的名字,正是 以前設置的 Label 的名字。

總結

流程總結

最後咱們再來經過下面這個圖來梳理下的流程。

  1. 配置 Device 和 Queue
  2. 獲取 CommandBuffer
  3. 配置 CommandBufferEncoder
  4. 配置 PipelineState
  5. 建立資源
  6. Encoder Buffer 【若有須要的話能夠用 Threadgroups 來分組 Encoder 數據】
  7. 提交到 Queue 中

Metal 能力

根據不一樣的 CommandBufferEncoder 能夠提供不一樣的能力,除了優秀的 3D 渲染能力,Metal 還能提供強大的計算能力。

在 WWDC 2015,蘋果發佈了 Metal Performance Shaders (MPS) 框架,iOS 9 上的一組高性能的圖像濾鏡,其實就是邊寫好的 Shaders,提供了優秀的圖像處理能力。同時還提供了高性能的矩陣運算的 Shaders ,能用來作機器學習的運算,在 GPU 上運行卷積神經網絡。

並且很是棒的是,今年的 WWDC 2017 上 Metal 也將開始支持 macOS 。
更多的實踐能夠參考蘋果的官方文檔:
Metal 的最佳實踐

咱們能夠用來作什麼?

  • 圖片處理 濾鏡/調整
  • 視頻處理
  • 機器學習
  • 大計算工做 分擔 CPU 壓力

參考

MetalProgrammingGuide : developer.apple.com/library/con…
metal-image-processing : www.invasivecode.com/weblog/meta…
Metal Shading Language : developer.apple.com/metal/Metal…
the-metal-shading-language-in-practice : www.objc.io/issues/18-g…
metal-performance-shaders-in-swift : metalbyexample.com/metal-perfo…

相關文章
相關標籤/搜索