The Joy of Swift Metal

Russell Mirabelli
Atlas
Published in
5 min readMar 14, 2018

Super-fast shader and pipeline exploration for iOS and macOS

For the past several years, I’ve been spoiled — as have many developers — by the speed of the edit/compile/run cycle when working in Swift Playgrounds.

Swift Playgrounds give us a chance to easily write quick code to test out concepts without building up a full application, deploying to device or simulator, and observing the results. I’ve used Swift Playgrounds to better understand many technologies, including:

  • Swift language concepts
  • CoreAnimation
  • SpriteKit
  • SceneKit

One area that had been eluding me has been working in Metal in a Swift Playground. Metal is Apple’s low-level graphics system, built to take advantage of their own hardware and provide great performance. I rather doubted that there’d be practical output to working directly in Metal — SceneKit does a great job for me — but I still felt the need to scratch that itch. And thus was another playground adventure begun.

Configuring the playground

When you’re creating a playground for Metal development, you’ll need to set it up as a macOS playground. iOS playgrounds are not eligible, as the simulator doesn’t provide the direct GPU access we need for Metal development.

We need to import a few frameworks. We need access to Cocoa, in order to handle our macOS view. We need Metal in order to, well, work with Metal. Lastly, we need PlaygroundSupport in order to show the Metal view.

Some of the code that we use implements Swift’s try/catch mechanism, so we’ll just wrap everything up inside a block:

The basics of a metal program

Creating a Metal device

The simplest step, which underpins all of the other steps, is to create a metal device. This returns a handle to the metal device for the system. There aren’t any options, choices, or really anything to discuss. You need a device, so get it.

Setting up a memory buffer for vertex data

We need some data to draw with. Metal works best with standard Float data. We’re going to set up to use a default viewport, which extends in the cartesian plane from -1 to 1 in both X and Y axes and keep all of our points at zero on the Z axis.

After we configure a triangle, the next step is to make that array of vertices available to our device. We do this by telling the device to create a buffer from our array of bytes, and we need to provide a size for this buffer. Importantly, to compute the size, do not use the size of a data element; this is not guaranteed to meet up with actual physical memory. Computations need to be made by using the stride.

Creating a render pipeline

Now that we have a device and some data, it’s time to create our render pipeline, which is to say the code through which all of our future render commands will be funneled for drawing onscreen. The first step is the simplest, we start by creating a descriptor which will configure the ultimate pipeline. Think of it as a builder structure.

Runtime-compiled shaders

This is where things get interesting. In Metal, we typically create a .metal shader file, which is separately compiled in the project then loaded at runtime. This doesn’t lend itself to effective playground use, though. In a playground, we want to be able to modify code at will, and that includes shader code. Thankfully, we have a way to do this. You wouldn’t want to dynamically compile shader code at runtime in a real app, of course. Conversely, you don’t want to statically compile shader code in a playground.

This consists of two shaders, a vertex shader and a fragment shader. A vertex shader provides instructions on how to transform all vertices before submission to the renderer. In this case, we’re simply passing our vertex information unchanged. A fragment shader, often referred to as a pixel shader, is responsible for computing the final appearance of any given pixel. In this case, I’m simply returning a constant color for any given pixel.

Finalizing the pipeline

Once you have your shaders, you can provide them to the pipeline descriptor. We set up the vertexFunction and fragmentFunction, as well as the output pixel format from the pipeline. After completing the descriptor setup, we finalize that by calling makeRenderPipelineState.

Setting up a view

For an iOS dev, there are some steps we’re not familiar with to create a view that can be used in a playground. On iOS, each UIView has a layer, and we would add our metal layer as a sublayer to the existing UIView layer. NSViews in macOS do not have a layer unless we delibarately set one. This is not particularly complex at all, but it’s just subtly different enough that it’s worth mentioning.

Prepare a command buffer

We’re almost ready to draw into our layer. Next, we have to set up a buffer for our drawing commands and prepare a texture that we’ll render into.

Create a buffer of render commands and show

Finally, after all of that setup, we can prepare to draw a triangle based on our vertex buffer. Then, we can show the buffer we’ve created. In a real app, this portion would typically be found within a display link, repeated each frame (with the drawable being obtained each frame), but for this project, a single pass at drawing will be sufficient.

A glorious triangle!

Our reward for all of this code (and it’s a fair amount)? A glorious, single triangle on a red background. Not much to look at, but it’s a place we can grow from!

What a wondrous triangle!

Afterward

This article really isn’t meant to provide an in-depth understanding of Metal. What I’m more interested in is providing you with a starting point, from which you can begin your own exploration. That’s the joy of playgrounds, and so I hope I’ve gotten you set up in a way that you can experiment and learn more for yourself. I’d love to hear about what you’re learning.

--

--