簡介
Metal 提供了和 GPU 的底層交互咙咽,讓開發(fā)者可以使自己的iOS、macOS淤年、tvOS應用表現(xiàn)出最佳的圖形和運算處理性能钧敞。該文章從蘋果官方提供的機組 demo 入手,介紹 MetalKit 和 Metal Shading Language 的使用麸粮,并使用它們進行圖形渲染和科學計算溉苛。
MetalKit使用的基本步驟
第一個最重要的類是 MTKView ,這是一個包裹了 UIView 或者 NSView 的對象,具備 Metal-spcific 的核心動畫功能弄诲,渲染的內(nèi)容在 MTKView 上進行顯示愚战。MTKView 比較重要的屬性是 preferredFramesPerSecond 、device 和 delegate 齐遵。
preferredFramesPerSecond 毋庸置疑是用來設置幀率的凤巨,這個幀率不是絕對的,會受限與設備的最大幀率和最小幀率洛搀,當這個值大于最大幀率敢茁,則選擇最大幀率;小于最小幀率留美,則選擇最小幀率彰檬。其默認值為 60 伸刃。
device 用來獲取 Metal 與 GPU 交互的一系列對象,默認值是 nil 逢倍,需要使用
MTLCreateSystemDefaultDevice() 來主動獲取捧颅。
delegate 具備了 MTKViewDelegate ,其提供 mtkView:drawableSizeWillChange: 和 drawInMTKView: 回調(diào)较雕。每刷新一幀 drawInMTKView: 回調(diào)會被調(diào)用一次碉哑,在 drawInMTKView: 里面可以進行繪制或者計算相關的工作;mtkView:drawableSizeWillChange: 回調(diào)是在 MTKView 的 frame 發(fā)生改變的時候回調(diào)亮蒋,可以手機發(fā)生屏幕旋轉(zhuǎn)或者其他需要調(diào)整視圖的操作時扣典,調(diào)整繪制區(qū)域。
第二類是 Metal 與 GPU 交互相關的對象
如圖是一個基本繪制的流程:
我們需要一個具備 MTLCommandQueue 協(xié)議的對象慎玖,其負責在每一幀里生產(chǎn)一系列具備 MTLCommandBuffer 協(xié)議的對象贮尖,Metal 與 GPU 的交互都會被寫入到這些具備 MTLCommandBuffer 協(xié)議的對象里面,而這個寫入的過程需要通過一個具備 MTLXXXCommandEncoder(MTLRenderCommandEncoder趁怔、MTLComputeCommandEncoder 或者其他的Encoder)協(xié)議的對象湿硝。
以下是一個繪制的基本結構:
- (void)prepare {
_device = MTLCreateSystemDefaultDevice();
_commandQueue = [_device newCommandQueue];
}
- (void)drawInMTKView:(nonnull MTKView *)view
{
// 設置背景顏色
Color color = [self makeFancyColor];
view.clearColor = MTLClearColorMake(color.red, color.green, color.blue, color.alpha);
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
commandBuffer.label = @"MyCommand";
MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
if(renderPassDescriptor != nil)
{
id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
renderEncoder.label = @"MyRenderEncoder";
/*
這里寫入繪制相關
*/
[renderEncoder endEncoding];
[commandBuffer presentDrawable:view.currentDrawable];
}
// Finalize rendering here and submit the command buffer to the GPU
[commandBuffer commit];
}
并行計算的 MTLComputeCommandEncoder 對象不需要借助 MTLRenderPassDescriptor 來創(chuàng)建,其結構為:
- (void)prepare {
_device = MTLCreateSystemDefaultDevice();
_commandQueue = [_device newCommandQueue];
}
- (void)drawInMTKView:(nonnull MTKView *)view
{
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
commandBuffer.label = @"MyCommand";
id<MTLComputeCommandEncoder> computeEncoder = [commandBuffer computeCommandEncoder];
/*
并行計算相關
*/
[computeEncoder endEncoding];
/*
有其他的 Encoder 可以繼續(xù)疊加
*/
// Finalize rendering here and submit the command buffer to the GPU
[commandBuffer commit];
}
繪制一個三角形
繪制管線是 GPU 處理圖像渲染的一步步流程润努,如圖所示:
Metal 的繪制管線包含了 Vertex function 关斜、Rasterization、Fragment function 三個階段铺浇,Vertex function 階段接受頂點數(shù)據(jù)(這里的頂點數(shù)據(jù)包括了頂點的位置和顏色信息)痢畜,負責將頂點數(shù)據(jù)繪制到一個 2D 的可視區(qū)域;Rasterization 接受從 Vertex function 傳過來的頂點數(shù)據(jù)随抠,決定哪些數(shù)據(jù)是要繪制在什么地方的裁着;Fragment function 將顏色值賦值到像素(后期如果有紋理繁涂,也在這里操作)拱她,最后輸出繪制后的圖像。創(chuàng)建一個繪制管線的步驟如下:
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
pipelineStateDescriptor.label = @"Simple Pipeline";
pipelineStateDescriptor.vertexFunction = vertexFunction; // 設置 Vertex function
pipelineStateDescriptor.fragmentFunction = fragmentFunction; // 設置 Fragment function
pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat; // 像素顏色格式
// 使用的時候?qū)?_pipelineState 賦值給 Encoder
_pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor
error:&error];
其中 Vertex function 和 Fragment function 分別在頂點著色器和片段著色器中實現(xiàn)扔罪,需要使用 Metal Shading Language 來編寫秉沼,Rasterization 階段不提供編程化的接口。
Metal Shading Language 的語法和 C++ 14 很像矿酵,區(qū)別在于 C++ 14 是 CPU 上運行的語言唬复,Metal Shading Language 運行在 GPU 上,GPU 提供更大并行處理能力全肮,對有大量數(shù)據(jù)需要處理的 Vertex function 和 Fragment function 會大大的提高效率敞咧。編寫的著色器函數(shù)保存為 .metal 文件,其編譯分為兩個階段:
1. Front-end 階段發(fā)生在 XCode build 時辜腺,.metal 文件會被編譯為 IR 文件休建。
2. Back-end 階段發(fā)生在 runtime 乍恐,IR 文件會被編譯為機器碼。
加載 IR 文件的過程通過 device 的 newDefaultLibrary 方法测砂,得到一個具備 MTLLibrary 協(xié)議的對象茵烈。取出里面的 Vertex function 和 Fragment function 通過 MTLLibrary 對象的 newFunctionWithName 方法。
id<MTLLibrary> defaultLibrary = [_device newDefaultLibrary];
id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"]; // 得到 Vertex function
id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"]; // 得到 Fragment function
繪制使用的數(shù)據(jù)類型涉及 SIMD 和一個具備 MTLBuffer 協(xié)議的對象砌些,SIMD 是一個獨立于 Metal 的庫呜投,能簡化算法和 GPU 處理流程,效率高使用十分方便存璃,常在 Metal 應用中使用仑荐。MTLBuffer 是 Metal 提供的用來保存大量頂點數(shù)據(jù)的 buffer ,其內(nèi)存由 GPU 可訪問的內(nèi)存分配有巧,效率高释漆,可以節(jié)省內(nèi)存的占用(當頂點數(shù)據(jù)量龐大的時候)。使用的時候一般自定義一個頂點數(shù)據(jù)結構篮迎,包含坐標點和顏色信息男图,這個數(shù)據(jù)結構的定義會用到 SIMD 里面的類型:
typedef struct
{
vector_float2 position;// 一個二維的位置矢量
vector_float4 color;// 一個四維的顏色矢量
} AAPLVertex;
將定義好的 APPLVertex 數(shù)組傳入 MTLBuffer 對象可以通過:
// Set up a simple MTLBuffer with our vertices which include texture coordinates
static const AAPLVertex quadVertices[] =
{
// Pixel positions, Texture coordinates
{ { 250, -250 }, { 1.f, 0.f } },
{ { -250, -250 }, { 0.f, 0.f } },
{ { -250, 250 }, { 0.f, 1.f } },
{ { 250, -250 }, { 1.f, 0.f } },
{ { -250, 250 }, { 0.f, 1.f } },
{ { 250, 250 }, { 1.f, 1.f } },
};
// Create our vertex buffer, and initialize it with our quadVertices array
_vertices = [_device newBufferWithBytes:quadVertices
length:sizeof(quadVertices)
options:MTLResourceStorageModeShared];
之后使用 Encoder 的 setVertexBuffer: 方法可將 MTLBuffer 對象傳給頂點著色器(Vertex function),運行繪制管線甜橱。
最終的繪制的三角形如圖: