preface

Today, we carry supercomputers in our pockets, and the iPhone is approaching the computational power of many of our laptops. Even with all of these extra features, we’re still limited by the OpenGL API because it’s a cross-platform solution and versatility is its greatest strength, but it doesn’t take full advantage of Apple’s deep integration across all of its products. OpenGL also has some structural problems that prevent it from drawing efficiently. And with so many expensive operations taking place on each drawing call, Metal changes the order of operations and moves the expensive work out of the drawing call, freeing up more processor bandwidth. In addition, Metal gives programmers complete control over how gpus work, which can improve efficiency. To conclude, OpenGL is a universal graphics programming API that has set the industry standard, and Metal is a highly optimized graphics programming API for Apple devices.

Metal Workflow Flowchart

The image above, taken from apple’s official documentation, gives an overview of metal’s workflow. In simple terms, actual operations such as rendering work and computation work are encapsulated into command encoders, and then multiple encoders are packaged to command buffer objects, and finally command buffer objects are sent to the command queue, waiting to be executed by GPU.

Let’s start by looking at the common classes in Metal

  • MTLDevice: software reference to the GPU hardware device.

  • MTLCommandQueue: Command queue, responsible for creating the command buffer objects for each frame and managing them.

  • MTLLibrary: contains the source code for your vertex shaders and fragment shaders.

  • MTLRenderPipelineState: Sets the information to draw, such as the shader functions to use, the depth and color Settings to use, and how to read vertex data.

  • MTLBuffer: Stores data, such as vertex information, in a form that can be sent to the GPU.

Let’s illustrate the code flow with an example of drawing a triangle. Here we start by building the process using the Metal framework. You may be wondering why you don’t use wheels directly here, because starting with the basic Metal framework will give you a better understanding of these parts. Also, with templates the code often contains things you don’t need in your project, so it’s best to start with the basics to help you understand the framework better.

1. Build the MTL device

Metal is based on the GPU. To interact with the GPU, you need to create a class that references it. In this case, the MTLDevice object is the SOFTWARE abstraction of the GPU.

 var device = MTLCreateSystemDefaultDevice()
Copy the code

2. Create the Metal display layer

func setupMetal() {
        metalLayer = CAMetalLayer.init()
        metalLayer.frame = view.bounds
        metalLayer.device = device
        metalLayer.pixelFormat = .bgra8Unorm
        metalLayer.framebufferOnly = true
        view.layer.addSublayer(metalLayer)
    }
Copy the code

The final metal render is rendered on the CAMetalLayer class layer. So we need to create this layer and add it to the view controller layer.

3. Load data

Func loadData() {// commandQueue commandQueue = device? .makecommandqueue () // create vertexData buffer object let vertexData: [Float] = [0.0, 0.5, 0.0, -0.5, -0.5, 0.0, 0.5, -0.5, 0.0] let dataSize = vertexdata.count * memoryLayout.size (ofValue: vertexData[0]) vertexBuffer = device? .makebuffer (bytes: vertexData, length: dataSize, options:.storagemodeshared) // Load shader let defaultLibrary = device? .makeDefaultLibrary() let fragmentProgram = defaultLibrary? .makeFunction(name: "basic_fragment") let vertexProgram = defaultLibrary? .makeFunction(name: "Basic_vertex") / / create the pipe state description, let pipelineStateDescriptor = MTLRenderPipelineDescriptor () pipelineStateDescriptor.vertexFunction = vertexProgram pipelineStateDescriptor.fragmentFunction = fragmentProgram PipelineStateDescriptor. ColorAttachments [0]. PixelFormat =. Bgra8Unorm / / create the pipe state object do {try pipelineState = device?.makeRenderPipelineState(descriptor: pipelineStateDescriptor) } catch let error { print("Failed to create pipeline state, error \(error)") } }Copy the code

Here we create some render object commands to do some preparatory work. Note: here we only need to create the command queue object once, and then hold it, because this object is expensive to create and does not need to be created every time we render.

4. Apply colours to a drawing

We are not using templates here, so we need to create our own loop to render. Here we use the CADisplaylLink class. The code is as follows:

Create a circle

  timer = CADisplayLink(target: self, selector: #selector(ViewController.gameLoop))
        timer.add(to: .main, forMode: .default)
Copy the code

Rendering process

Func render() {@objc func gameLoop() {autoreleasepool {self.render()}} // Create render command to describe its let renderPassDescriptor = MTLRenderPassDescriptor() guard let drawable = metalLayer.nextDrawable() else { return } renderPassDescriptor.colorAttachments[0].texture = drawable.texture renderPassDescriptor.colorAttachments[0].loadAction . =. The clear renderPassDescriptor colorAttachments [0]. ClearColor = MTLClearColorMake (221.0/255.0, 160.0/255.0, 221.0/255.0, 1.0) the let commandBuffer = commandQueue. MakeCommandBuffer () / / create the render command encoder let renderEncoder = commandBuffer! .makeRenderCommandEncoder(descriptor: renderPassDescriptor) renderEncoder? .setRenderPipelineState(pipelineState) renderEncoder? .setVertexBuffer(vertexBuffer, offset: 0, index: 0 ) renderEncoder? .drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3) renderEncoder? .endencoding () // submits the command encoder to the commandBuffer object, which submits to the command queue, waiting for the GPU to execute the commandBuffer? .present(drawable) commandBuffer? .commit() }Copy the code

As for shaders, we’ll just take a quick look at how to write them. We won’t go into that.

#include <metal_stdlib> using namespace metal; vertex float4 basic_vertex ( const device packed_float3 * vertex_array[[buffer(0)]], Unsigned int vid [[vertex_id]]) {return float4 (vertex_array[vid],1.0); } fragment half4 basic_fragment() {return half4(1.0); }Copy the code

The sample code

Please be aware that the projects in this series are based on xcode13, Metal2.4.