For the past several years, I've been spoiled -- as have many developers -- by the lack of an 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.
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.
https://gist.github.com/48f06777233f02a6e4aba0a440e804e3
Some of the code that we use implements Swift's try/catch mechanism, so we'll just wrap everything up inside a block: https://gist.github.com/47f3a1d30ebea60337a3adf6a0cd26dd
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. https://gist.github.com/48c33504eeb0d4951d1aca582911a242
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.
https://gist.github.com/2a5b8b9bda2d6bfa8e5bcd9fe09a1690
Now that we have a device & 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 ceating a desciptor which will configue the ultimate pipeline. Think of it as a builder stucture. https://gist.github.com/b63ca97503d842e499539de3259ed81a
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. Converseley, you don't want to statically compile shader code in a playground. https://gist.github.com/ee99a6073a80ce493bebf337bbd46ef1
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 inforrmation 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.
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. https://gist.github.com/3ba3d6690d10aea33ca0bc052a50ef87
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. https://gist.github.com/262d1d457c88c1042508422d31bf3e2d
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. https://gist.github.com/2fe2642a39b8462e112dfbc5400f6f28
Finally, after all of that setup, we can prepare to draw a triangle based around 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. https://gist.github.com/f1121bad8bfe0840b2f932ffd7f0d30f
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!
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.