Metal初识
Metal
Metal 是一个和 OpenGL ES 类似的面向底层的图形编程接口,通过使用相关的 api 可以直接操作 GPU ,最早在 2014 年的 WWDC 的时候发布,并于2019年发布了 Metal 2。
Metal框架支持GPU硬件加速、高级3D图形渲染以及大数据并行运算。且提供了先进而精简的API来确保框架的细粒度(fine-grain),并且在组织架构、程序处理、图形呈现、运算指令以及指令相关数据资源的管理上都支持底层控制。其核心目的是尽可能的减少CPU开销,而将运行时产生的大部分负载交由GPU承担
主要功能
3D图形渲染 和并行运算
Metal的特点
消除“隐藏”的性能瓶颈,如隐式状态验证。你可以在多线程异步控制GPU,有效用于平行创建和提交命令缓冲区
描述了缓冲和纹理对象代表了GPU的内存分配。纹理对象有特定的像素格式,并可用于纹理图像或附件对象
使用相同的数据结构和资源(如缓冲区、纹理和命令队列),用于图形和计算操作。此外,金属着色语言支持图形和计算功能。Metal使得资源能够和runtime接口、图形着色器、并计算函数之间共享
Metal 着色器可以和你的app代码一样在运行时加载,编译,这样的好处时能够更好的生成代码,以及编译调试
Metal 不能再后台执行命令代码,否则系统崩溃
Metal命令对象之间的关系
命令缓存区(command buffer) 是从命令队列(command queue) 创建的
命令编码器(command encoders) 将命令编码到命令缓存区中
提交命令缓存区并将其发送到GPU
GPU执⾏命令并将结果呈现为可绘制
image.png
apple建议Metal
The Metal framework provides protocols to manage persistent objects throughout the lifetime of your app. These objects are expensive to create but are usually initialized once and reused often. You do not need to create these objects at the beginning of every render or compute loop.
Metal框架提供协议,可在应用程序的整个生命周期内管理持久对象。这些对象创建起来很昂贵,但通常会初始化一次并经常重复使用。您无需在每个渲染或计算循环的开始时创建这些对象。
MTKView中的代理方法建议用独立的类管理,建议不要在ViewController里面实现.
demo实现
创建MTKView
1.我们可以在Main.storyboard中指定view的类型然后指定view的类型.
_view = (MTKView *)self.view;
2.我们也可以初始化MTKView的对象,然后addSubView到某父试图上
_view = [[MTKView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:_view];
设置Device
_view.device = MTLCreateSystemDefaultDevice();
设置渲染对象
apple建议渲染代码独立出来方便管理,我们穿件一个MyRender类
_renderer = [[MyRender alloc] initWithMetalKitView:_view];
if(!_renderer)
{
NSLog(@"Renderer failed initialization");
return;
}
[_renderer mtkView:_view drawableSizeWillChange:_view.drawableSize];
_view.delegate = _renderer;
Metal shader language
typedef struct
{
float4 clipSpacePosition [[position]];
float4 color;
} RasterizerData;
vertex RasterizerData
vertexShader(uint vertexID [[vertex_id]],
constant CCVertex *vertices [[buffer(CCVertexInputIndexVertices)]],
constant vector_uint2 *viewportSizePointer [[buffer(CCVertexInputIndexViewportSize)]])
{
out.clipSpacePosition = vertices[vertexID].position;
out.color = vertices[vertexID].color;
return out;
}
fragment float4 fragmentShader(RasterizerData in [[stage_in]])
{
//返回输入的片元颜色
return in.color;
}
MyRender的实现
初始化方法中
{
//我们用来渲染的设备(又名GPU)
id<MTLDevice> _device;
// 我们的渲染管道有顶点着色器和片元着色器 它们存储在.metal shader 文件中
id<MTLRenderPipelineState> _pipelineState;
//命令队列,从命令缓存区获取
id<MTLCommandQueue> _commandQueue;
//当前视图大小,这样我们才可以在渲染通道使用这个视图
vector_uint2 _viewportSize;
}
//初始化MTKView
- (nonnull instancetype)initWithMetalKitView:(nonnull MTKView *)mtkView
{
self = [super init];
if(self)
{
NSError *error = NULL;
//1.获取GPU 设备
_device = mtkView.device;
//2.在项目中加载所有的(.metal)着色器文件
// 从bundle中获取.metal文件
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
//从库中加载顶点函数
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
//从库中加载片元函数
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
//3.配置用于创建管道状态的管道
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
//管道名称
pipelineStateDescriptor.label = @"Simple Pipeline";
//可编程函数,用于处理渲染过程中的各个顶点
pipelineStateDescriptor.vertexFunction = vertexFunction;
//可编程函数,用于处理渲染过程中各个片段/片元
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
//一组存储颜色数据的组件
pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;
//4.同步创建并返回渲染管线状态对象
_pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
//判断是否返回了管线状态对象
if (!_pipelineState)
{
//如果我们没有正确设置管道描述符,则管道状态创建可能失败
NSLog(@"Failed to created pipeline state, error %@", error);
return nil;
}
//5.创建命令队列
_commandQueue = [_device newCommandQueue];
}
return self;
}
Do not build new pipelines for new command encoders
不要为新的命令编码器构建新的管道
代理方法
//每当视图改变方向或调整大小时调用
- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size
{
// 保存可绘制的大小,因为当我们绘制时,我们将把这些值传递给顶点着色器
_viewportSize.x = size.width;
_viewportSize.y = size.height;
}
//每当视图需要渲染帧时调用
- (void)drawInMTKView:(nonnull MTKView *)view
{
//1. 顶点数据/颜色数据
static const CCVertex triangleVertices[] =
{
//顶点, RGBA 颜色值
{ { 0.5, -0.25, 0.0, 1.0 }, { 1, 0, 0, 1 } },
{ { -0.5, -0.25, 0.0, 1.0 }, { 0, 1, 0, 1 } },
{ { -0.0f, 0.25, 0.0, 1.0 }, { 0, 0, 1, 1 } },
};
//2.为当前渲染的每个渲染传递创建一个新的命令缓冲区
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
//指定缓存区名称
commandBuffer.label = @"MyCommand";
//3.
// MTLRenderPassDescriptor:一组渲染目标,用作渲染通道生成的像素的输出目标。
MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
//判断渲染目标是否为空
if(renderPassDescriptor != nil)
{
//4.创建渲染命令编码器,这样我们才可以渲染到something
id<MTLRenderCommandEncoder> renderEncoder =[commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
//渲染器名称
renderEncoder.label = @"MyRenderEncoder";
//5.设置我们绘制的可绘制区域
/*
typedef struct {
double originX, originY, width, height, znear, zfar;
} MTLViewport;
*/
//视口指定Metal渲染内容的drawable区域。 视口是具有x和y偏移,宽度和高度以及近和远平面的3D区域
//为管道分配自定义视口需要通过调用setViewport:方法将MTLViewport结构编码为渲染命令编码器。 如果未指定视口,Metal会设置一个默认视口,其大小与用于创建渲染命令编码器的drawable相同。
MTLViewport viewPort = {
0.0,0.0,_viewportSize.x,_viewportSize.y,-1.0,1.0
};
[renderEncoder setViewport:viewPort];
//[renderEncoder setViewport:(MTLViewport){0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0 }];
//6.设置当前渲染管道状态对象
[renderEncoder setRenderPipelineState:_pipelineState];
//7.从应用程序OC 代码 中发送数据给Metal 顶点着色器 函数
//顶点数据+颜色数据
// 1) 指向要传递给着色器的内存的指针
// 2) 我们想要传递的数据的内存大小
// 3)一个整数索引,它对应于我们的“vertexShader”函数中的缓冲区属性限定符的索引。
[renderEncoder setVertexBytes:triangleVertices
length:sizeof(triangleVertices)
atIndex:CCVertexInputIndexVertices];
//viewPortSize 数据
//1) 发送到顶点着色函数中,视图大小
//2) 视图大小内存空间大小
//3) 对应的索引
[renderEncoder setVertexBytes:&_viewportSize
length:sizeof(_viewportSize)
atIndex:CCVertexInputIndexViewportSize];
//8.画出三角形的3个顶点
// @method drawPrimitives:vertexStart:vertexCount:
//@brief 在不使用索引列表的情况下,绘制图元
//@param 绘制图形组装的基元类型
//@param 从哪个位置数据开始绘制,一般为0
//@param 每个图元的顶点个数,绘制的图型顶点数量
/*
MTLPrimitiveTypePoint = 0, 点
MTLPrimitiveTypeLine = 1, 线段
MTLPrimitiveTypeLineStrip = 2, 线环
MTLPrimitiveTypeTriangle = 3, 三角形
MTLPrimitiveTypeTriangleStrip = 4, 三角型扇
*/
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:0
vertexCount:3];
//9.表示已该编码器生成的命令都已完成,并且从NTLCommandBuffer中分离
[renderEncoder endEncoding];
//10.一旦框架缓冲区完成,使用当前可绘制的进度表
[commandBuffer presentDrawable:view.currentDrawable];
}
//11.最后,在这里完成渲染并将命令缓冲区推送到GPU
[commandBuffer commit];
}
类的理解
MTLLibrary
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
//从库中加载顶点函数
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
//从库中加载片元函数
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"];
MTLLibrary的对象主要是从metal文件中获取到顶点函数和片元函数.
对比OpenGLES 从shader文件中读取内容.
MTLRenderPipelineDescriptor
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
//管道名称
pipelineStateDescriptor.label = @"Simple Pipeline";
//可编程函数,用于处理渲染过程中的各个顶点
pipelineStateDescriptor.vertexFunction = vertexFunction;
//可编程函数,用于处理渲染过程中各个片段/片元
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
//一组存储颜色数据的组件
pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;
MTLRenderPipelineDescriptor关联shader函数,设置颜色数据的组件
类比OpenGLES 中Shader对象,但是openGLES中的shader分片元和片元着色器,这个用一个对象持有两个函数.
MTLRenderPipelineState
id<MTLRenderPipelineState> _pipelineState;
//4.同步创建并返回渲染管线状态对象
_pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&error];
//判断是否返回了管线状态对象
if (!_pipelineState)
{
//如果我们没有正确设置管道描述符,则管道状态创建可能失败
NSLog(@"Failed to created pipeline state, error %@", error);
return nil;
}
MTLRenderPipelineState管理片元和顶点函数,类比OpenGLES中的program。
MTLCommandQueue
id<MTLCommandQueue> _commandQueue;
//5.创建命令队列
_commandQueue = [_device newCommandQueue];
命令队列,可以为当前渲染的每个渲染传递创建一个新的命令缓冲区
MTLCommandBuffer
//2.为当前渲染的每个渲染传递创建一个新的命令缓冲区
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
//指定缓存区名称
commandBuffer.label = @"MyCommand";
设备要执行的命令的序列表,感觉这个有点想OpenGLES 的Context;
MTLRenderCommandEncoder
// MTLRenderPassDescriptor:一组渲染目标,用作渲染通道生成的像素的输出目标。
MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
id<MTLRenderCommandEncoder> renderEncoder =[commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
//渲染器名称
renderEncoder.label = @"MyRenderEncoder";
用于图形渲染状态的容器,以及用于将该状态转换为设备可以执行的命令格式的代码。