Metal Tutorial: Getting Started

In this Metal tutorial, you will learn how to get started with Apple’s 3D graphics API by rendering a simple triangle to the screen. By Andrew Kharchyshyn.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 3 of this article. Click here to view the first page.

7) Creating a Command Queue

The final one-time-setup step that you need to do is to create an MTLCommandQueue.

Think of this as an ordered list of commands that you tell the GPU to execute, one at a time.

To create a command queue, simply add a new property:

var commandQueue: MTLCommandQueue!

Then, add the following line at the end of viewDidLoad():

commandQueue = device.makeCommandQueue()

Congrats — your one-time setup code is done!

Rendering the Triangle

Now, it’s time to move on to the code that executes each frame — to render the triangle!

This is done in five steps:

  1. Create a Display Link
  2. Create a Render Pass Descriptor
  3. Create a Command Buffer
  4. Create a Render Command Encoder
  5. Commit your Command Buffer
Note: In theory, this app doesn’t actually need to render things once per frame, because the triangle doesn’t move after it’s drawn. However, most apps do have moving pieces, so you’ll do things this way to learn the process. This also gives a nice starting point for future tutorials.

1) Creating a Display Link

You need a way to redraw the screen every time the device screen refreshes.

CADisplayLink is a timer synchronized to the displays refresh rate. The perfect tool for the job! To use it, add a new property to the class:

var timer: CADisplayLink!

Initialize it at the end of viewDidLoad() as follows:

timer = CADisplayLink(target: self, selector: #selector(gameloop))
timer.add(to: RunLoop.main, forMode: .default)

This sets up your code to call a method named gameloop() every time the screen refreshes.

Finally, add these stub methods to the class:

func render() {
  // TODO
}

@objc func gameloop() {
  autoreleasepool {
    self.render()
  }
}

Here, gameloop() simply calls render() each frame, which, right now, just has an empty implementation. Time to flesh this out.

2) Creating a Render Pass Descriptor

The next step is to create an MTLRenderPassDescriptor, which is an object that configures which texture is being rendered to, what the clear color is and a bit of other configuration.

Add these lines inside render(), in place of // TODO:

guard let drawable = metalLayer?.nextDrawable() else { return }
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(
  red: 0.0, 
  green: 104.0/255.0, 
  blue: 55.0/255.0, 
  alpha: 1.0)

First, you call nextDrawable() on the Metal layer you created earlier, which returns the texture in which you need to draw in order for something to appear on the screen.

Next, you configure the render pass descriptor to use that texture. You set the load action to Clear, which means “set the texture to the clear color before doing any drawing,” and you set the clear color to the green color used on the site.

3) Creating a Command Buffer

The next step is to create a command buffer. Think of this as the list of render commands that you wish to execute for this frame. The cool thing is that nothing actually happens until you commit the command buffer, giving you fine-grained control over when things occur.

Creating a command buffer is easy. Simply add this line to the end of render():

let commandBuffer = commandQueue.makeCommandBuffer()!

A command buffer contains one or more render commands. You’ll create one of these next.

4) Creating a Render Command Encoder

To create a render command, you use a helper object called a render command encoder. To try this out, add these lines to the end of render():

let renderEncoder = commandBuffer
  .makeRenderCommandEncoder(descriptor: renderPassDescriptor)!
renderEncoder.setRenderPipelineState(pipelineState)
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder
  .drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
renderEncoder.endEncoding()

Here, you create a command encoder and specify the pipeline and vertex buffer that you created earlier.

The most important part is the call to drawPrimitives(type:vertexStart:vertexCount:instanceCount:). Here, you’re telling the GPU to draw a set of triangles, based on the vertex buffer. To keep things simple, you are only drawing one. The method arguments tell Metal that each triangle consists of three vertices, starting at index 0 inside the vertex buffer, and there is one triangle total.

When you’re done, you simply call endEncoding().

5) Committing Your Command Buffer

The final step is to commit the command buffer. Add these lines to the end of render():

commandBuffer.present(drawable)
commandBuffer.commit()

The first line is needed to make sure that the GPU presents the new texture as soon as the drawing completes. Then you commit the transaction to send the task to the GPU.

Phew! That was a ton of code, but, at long last, you are done! Build and run the app and bask in your triangular glory:

The most beautiful triangle I’ve ever seen!

The most beautiful metal triangle I've ever seen!

Where to Go From Here?

The final project for this tutorial is in the tutorial materials bundle using the Download Materials button at the top or bottom of this tutorial.

You have learned a ton about the Metal API! You now have an understanding of some of the most important concepts in Metal, such as shaders, devices, command buffers, pipelines and more.

Also, be sure to check out some great resources from Apple:

You also might enjoy the Beginning Metal course on our site, where we explain these same concepts in video form, but with even more detail.

Or you can dive into books: Check out our Metal by Tutorials book.

I hope you enjoyed this tutorial, and if you have any comments or questions, please join the forum discussion below!