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…

相关文章
相关标签/搜索