本文參考資料:
juejin.im/post/5b1e8f…
xiaozhuanlan.com/topic/04598…
developer.apple.com/videos/play…
github.com/quinn0809/G…
cloud.tencent.com/developer/a…
devstreaming-cdn.apple.com/videos/wwdc…
Metal處理邏輯
無論是CoreImage、GPUImage框架,還是Metal、OpenGL框架,處理邏輯類似:
輸入(資源+邏輯 )-> 黑盒 -> 輸出
CoreImage 可以選擇GPU處理->Metal->CoreImage,也可以選擇CPU處理
GPUImage 有OpenGL ES版,也有Metal版本(Metal 版本極為簡陋)
Metal使用大致分為:
- build :shader
- initialize :device and Queues Render Objects
- Render:commandBuffer、ResourceUpdate、renderEncoder、Display
build :shader
主要完成shader的編譯,涉及到vertex 、fragment
Metal中的shader是MSL語言,SIMD的存在支持MSL與原生代碼共享數據結構。
一個簡單的vertexShader :
vertex ThreeInputVertexIO threeInputVertex(device packed_float2 *position [[buffer(0)]],device packed_float2 *texturecoord [[buffer(1)]],device packed_float2 *texturecoord2 [[buffer(2)]],uint vid [[vertex_id]])
{ThreeInputVertexIO outputVertices;outputVertices.position = float4(position[vid], 0, 1.0);outputVertices.textureCoordinate = texturecoord[vid];outputVertices.textureCoordinate2 = texturecoord2[vid];return outputVertices;
}
復制代碼
outputVertices.position = float4(position[vid], 0, 1.0);
position[vid] 是float2 SIMD 是 Apple 提供的一款方便原生程序與著色器程序共享數據結構的庫。
開發者可以基于SIMD框架在Objective-C頭文件中定義一系列數據結構,在原生代碼和著色器程序中通過#include包含這個頭文件,兩者就都有了這個結構的定義。
struct ThreeInputVertexIO
{float4 position [[position]];float2 textureCoordinate [[user(texturecoord)]];float2 textureCoordinate [[user(texturecoord2)]];};
復制代碼
device packed_float2 *position [[buffer(0)]]
device packed_float2 *texturecoord [[buffer(1)]]
packed_float2
是類型 position
、texturecoord
是變量名
device
是內存修飾符,Metal種的內存訪問主要有兩種方式:Device模式和Constant模式,由代碼中顯式指定。
Device模式是比較通用的訪問模式,使用限制比較少,而Constant模式是為了多次讀取而設計的快速訪問只讀模式,通過Constant內存模式訪問的參數的數據的字節數量是固定的,特點總結為: Device支持讀寫,并且沒有size的限制; Constant是只讀,并且限定大小; 如何選擇Device和Constant模式? 先看數據size是否會變化,再看訪問的頻率高低,只有那些固定size且經常訪問的部分適合使用constant模式,其他的均用Device。
[[buffer(0)]]
、[[buffer(1)]]
是句柄,在MSL中不同的類型用不同的buffer表示,與renderCommandEncoder時相對應:
//buffer renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)renderEncoder.setVertexBuffer(textureBuffer1, offset: 0, index: 1)renderEncoder.setVertexBuffer(textureBuffer2, offset: 0, index: 2)······//samper[renderEncoder setFragmentSampler:sampler atIndex:0];[renderEncoder setFragmentSampler:sampler1 atIndex:0];······//texturerenderEncoder.setFragmentTexture(texture, index: 0)renderEncoder.setFragmentTexture(texture1, index: 1)······
復制代碼
index 與 [[buffer(0)]]相對應,如,此時上文MSL的vertexShader中
- [[buffer(0)]] 為vertex數據
- [[buffer(1)]]為第一個紋理坐標數據
- [[buffer(2)]]為第二個紋理坐標數據
index與shader中聲明的[[buffer(x)]]嚴格對應,否則在Metal Validation Layer中極可能會報錯(通常是內存讀取越界),或者繪制出不符合預期的結果。 vertexShader的執行次數與頂點數量有關,即vid為索引數。
一個簡單的fragmentShader :
fragment half4 lookupSplitFragment(TwoInputVertexIO fragmentInput [[stage_in]],texture2d<half> inputTexture [[texture(0)]],texture2d<half> inputTexture2 [[texture(1)]],texture2d<half> inputTexture3 [[texture(2)]],constant SplitUniform& uniform [[ buffer(1) ]])
{}
復制代碼
同上文的renderCommandEncoder時,
- inputTexture 為第一個紋理
- inputTexture2 為第二個紋理
- inputTexture3 為第三個紋理
SplitUniform 為自定義的參數,在此shader中的意義為split 的外界值。 SplitUniform的定義如下: 在metal文件中:
typedef struct
{float intensity;float progress;} SplitUniform;
復制代碼
『intensity』
為filter
的濃度
『progress』
為filter
的 split
進度
初始化工作
- devide
- commandQueue
- buffer
- texture
- pipline
初始化Device
devide
是 metal
控制的GPU 入口,是一個一次創建最好永久使用的對象,用來創建buffer
、command
、texture
;在Metal
最佳實踐之南中,指出開發者應該長期持有一個device
對象(device
對象創建比較昂貴)
OC:
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
復制代碼
Swift:
guard let device = MTLCreateSystemDefaultDevice() else {fatalError("Could not create Metal Device")
}
復制代碼
創建 CommandQueue 命令隊列
Metal 最佳實踐指南中,指出大部分情況下,開發者要重復使用一個命令隊列 通過Device -> commandQueue
/// device 創建命令隊列guard let commandQueue = self.device.makeCommandQueue() else {fatalError("Could not create command queue")}
復制代碼
創建 Buffer 數據
Metal 中,所有無結構的數據都使用 Buffer 來管理。與 OpenGL 類似的,頂點、索引等數據都通過 Buffer 管理。 比如:vertexBuffer、textureCoordBuffer
/// 紋理坐標buffer
let coordinateBuffer = device.makeBuffer(bytes: inputTextureCoordinates,length: inputTextureCoordinates.count * MemoryLayout<Float>.size,options: [])!
///頂點數據buffer
let vertexBuffer = device.makeBuffer(bytes: imageVertices,length: imageVertices.count * MemoryLayout<Float>.size,options: [])!
復制代碼
這些Buffer在renderCommandEncoder中 進行編碼然后提交到GPU
創建 Texture
texture 可以理解為被加工的對象,設計者為它增加了一個描述對象MTLTextureDescriptor
在Metal中,有一個抽象對象,專門由于描述 teture 的詳情(fromat,width,height,storageMode)
storageMode為 控制CPU、GPU的內存管理方式。Apple 推薦在 iOS 中使用 shared mode,而在 macOS 中使用 managed mode。
Shared Storage:CPU 和 GPU 均可讀寫這塊內存。
Private Storage: 僅 GPU 可讀寫這塊內存,可以通過 Blit 命令等進行拷貝。
Managed Storage: 僅在 macOS 中允許。僅 GPU 可讀寫這塊內存,但 Metal 會創建一塊鏡像內存供 CPU 使用
復制代碼
//紋理描述 器
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: pixelFormat,width: width,height: height,mipmapped: mipmapped)
//通過 devide創建簡單紋理(比如單色紋理)
guard let newTexture = device.makeTexture(descriptor: textureDescriptor) else {fatalError("Could not create texture of size: (\(width), \(height))")}// 通過 圖片創建 (MetalKit)
var textureLoader = MTKTextureLoader(device: self.device)
let imageTexture = try textureLoader.newTexture(cgImage: img, options: [MTKTextureLoader.Option.SRGB : false])復制代碼
MTKTextureLoader 也建議重復使用
創建 pipline 渲染管線
pipline:最為復雜的東西,也是最簡單的東西,說他復雜是因為,他的成員變量多;說簡單,是因為pipline只是一個所有資源的描述者
colorAttachments,用于寫入顏色數據
depthAttachment,用于寫入深度信息
stencilAttachment,允許我們基于一些條件丟棄指定片段MTLRenderPassDescriptor 里面的 colorAttachments,支持多達 4 個 用來存儲顏色像素數據的 attachment,在 2D 圖像處理時,我們一般只會關聯一個。
即 colorAttachments[0]。
復制代碼
let descriptor = MTLRenderPipelineDescriptor()descriptor.colorAttachments[0].pixelFormat = MTLPixelFormat.bgra8Unormdescriptor.vertexFunction = vertexFunctiondescriptor.fragmentFunction = fragmentFunction
復制代碼
關于shader 函數 的創建:
guard let vertexFunction = defaultLibrary.makeFunction(name: vertexFunctionName) else {fatalError("Could not compile vertex function \(vertexFunctionName)")
}guard let fragmentFunction = defaultLibrary.makeFunction(name: fragmentFunctionName) else {fatalError("Could not compile fragment function \(fragmentFunctionName)")
}
復制代碼
defaultLibrary
為通過device 創建 的 函數庫,上文我們在編譯的時候已經編譯好了頂點著色器以及片段著色器,這是通過
do {let frameworkBundle = Bundle(for: Context.self)let metalLibraryPath = frameworkBundle.path(forResource: "default", ofType: "metallib")!self.defaultLibrary = try device.makeLibrary(filepath:metalLibraryPath)} catch {fatalError("Could not load library")}復制代碼
可以獲取到 defaultLibrary,這是有Metal 提供的方法
到目前為止,我們已經完成了渲染所需的子控件的構造,初始化,下面將介紹 命令編碼,提交,渲染
Render:commandBuffer、ResourceUpdate、renderEncoder、Display
renderEncoder
上文我們創建了渲染管線狀態,這里我們需要根據RenderPassDescriptor生成一個 RenderCommandEncoder,在encoder中鏈接shader GPU 渲染圖像的步驟大致可以分為:加載、渲染、存儲。開發者可以指定這三個步驟具體做什么事。
MTLRenderPassDescriptor * desc = [MTLRenderPassDescriptor new];
desc.colorAttachment[0].texture = myColorTexture;// 指定三個步驟的行為
desc.colorAttachment[0].loadAction = MTLLoadActionClear;
desc.colorAttachment[0].clearColor = MTLClearColorMake(0.39f, 0.34f, 0.53f, 1.0f);
desc.colorAttachment[0].storeAction = MTLStoreActionStore;
復制代碼
myColorTexture 可以理解為容器,用于安置渲染的結果。
上文有提到編碼:
//buffer renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)renderEncoder.setVertexBuffer(textureBuffer1, offset: 0, index: 1)renderEncoder.setVertexBuffer(textureBuffer2, offset: 0, index: 2)······//samper[renderEncoder setFragmentSampler:sampler atIndex:0];[renderEncoder setFragmentSampler:sampler1 atIndex:0];······//texturerenderEncoder.setFragmentTexture(texture, index: 0)renderEncoder.setFragmentTexture(texture1, index: 1)······
復制代碼
編碼所需代碼大致如下:
let commandBuffer = commonQueue.makeCommandBuffer()!let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescripor)!commandEncoder.setRenderPipelineState(pipelineState)commandEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)commandEncoder.setFragmentTexture(texture, index: 0)commandEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)commandEncoder.endEncoding()
復制代碼
提交渲染
commandBuffer.present(drawable)commandBuffer.commit()
復制代碼
渲染時的三幀緩存: 創建三幀的資源緩沖區來形成一個緩沖池。CPU 將每一幀的數據按順序寫入緩沖區供 GPU 使用。
提交時,分為同步提交(阻塞),異步提交(非阻塞) 阻塞:
id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];// 編碼命令...[commandBuffer commit];[commandBuffer waitUntilCompleted];
復制代碼
非阻塞:
id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];// 編碼命令...commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> commandBuffer) {// 回調 CPU...
}[commandBuffer commit];
復制代碼
重申:本文參考資料:
juejin.im/post/5b1e8f…
xiaozhuanlan.com/topic/04598…
developer.apple.com/videos/play…
github.com/quinn0809/G…
cloud.tencent.com/developer/a…
devstreaming-cdn.apple.com/videos/wwdc…