🤘🏻 TUTORIAL: Metal HelloTriangle using Swift 5 and no Xcode
Tags: swift, metal, tutorial, graphics
Published: 2021-04-06 17:20:22

[Take a look at the code](https://github.com/javiersalcedopuyo/MetalHelloTriangle)

A "Hello Triangle" is the first exercise a graphics dev does when learning a new API, analogous to the "Hello World" exercise with a new language.
They vary a lot in terms of complexity and length, from the brutal 1k lines of Vulkan to the mere 100 of Metal.

I had heard really good things about both Metal and Swift, so I decided to give them a try. And so far I'm impressed. Well done Apple!

Other APIs, like OpenGL, Vulkan or DirectX have really nice documentation and tutorials all over the web, however, when I decided to start with Metal I couldn't find much. Almost every tutorial was meant for iOS apps, and Apple's docs are still on Objective C.
So I decided to document my journey, so it can help others facing similar issues.

---

# 🤷🏼‍♂️ Why not using Xcode?

I don't really have an answer other than **laziness**. I just didn't want to rely on/learn all the Swift UI framework for this, since I don't want to write apps, just a very simple toy engine.
*I wanted to focus on learning Metal.*

That meant no *xcodeproj*, no storyboards, manually managing windows, compile shaders, etc.

On top of that, I come from Linux, so I'm pretty much used to work from the terminal (and Swift's CLI package manager is quite good).

**SourceKit LSP** gives a very good development experience too, so I can use my tightly customised VSCode/Vim setups and be productive from the get-go without needing to learn how to use a new IDE.

However, dealing with the window management is so messy that I might reconsider this part in the future.

After this "disclaimer", buckle up and let’s get started!

---

# 🛠 Creating the project
Thanks to Swift's package manager's CLI, this is super simple and straightforward, simply open your terminal of choice and run this:
```zsh
$ mkdir MetalHelloTriangle
$ cd MetalHelloTriangle
$ swift package init --type executable
```
This will create a Hello World executable project with everything you need.
Test it with:
```zsh
$ swift run
```

Next, since Metal is only available for Apple devices, we'll need to add a restriction to the platforms this will run on.
Add this to *Package.swift* after the *name* field (auto-generated, it should already exist on the root directory of the project)
```swift
// Package.swift
let package = Package(
    name: "MetalHelloTriangle",
    platforms: [ .macOS(.v10_15) ], // NEW LINE
    dependencies: [
...
```
I'm not planning to run this on any device besides my laptop, so I only set macOS as platform.

---

# 🪟 Opening a (barebones) Window
Here started the first problem with not using Xcode. It was a mess. Easily, 90% of the development time was wasted on this.
I wish I could find something similar to [GLFW](https://www.glfw.org/) but I couldn't.
So I ended up relying on the **AppDelegate/ViewController** framework, following [this blogpost](https://kicsipixel.github.io/2020/nostoryboard/), and the first part of [this other](https://medium.com/@shoheiyokoyama/rendering-graphics-content-using-the-metalkit-framework-ea3503f34535).

First we will create a new file `AppDelegate.swift`, it'll contain, you guessed it, the class *AppDelegate*, which will create and own the window.

We'll start by importing Cocoa and creating the class

```swift
// AppDelegate.swift
import Cocoa

let WIDTH  = 800
let HEIGHT = 600

class AppDelegate: NSObject, NSApplicationDelegate
{
    private var mWindow: NSWindow?

    func applicationDidFinishLaunching(_ aNotification: Notification)
    {
        // This will be called once when we run the engine
    }
}
```

Meanwhile, in *main.swift*, we set the app delegate:

```swift
// main.swift
import Cocoa

let delegate = AppDelegate()
NSApplication.shared.delegate = delegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
```

Time to create the window itself
```swift
// AppDelegate.swift
func applicationDidFinishLaunching(_ aNotification: Notification)
{
    let screenSize = NSScreen.main?.frame.size ?? .zero

    let rect = NSMakeRect((screenSize.width  - CGFloat(WIDTH))  * 0.5,
                          (screenSize.height - CGFloat(HEIGHT)) * 0.5,
                          CGFloat(WIDTH),
                          CGFloat(HEIGHT))

    mWindow = NSWindow(contentRect: rect,
                       styleMask:   [.miniaturizable,
                                     .closable,
                                     .resizable,
                                     .titled],
                        backing:    .buffered,
                        defer:      false)

    mWindow?.title = "Hello Triangle"
    mWindow?.makeKeyAndOrderFront(nil)
}
```
Ok, now we have a blank window, but we need to be able to draw stuff into it.

Next, we add the window's *content view controller*.
Create a new *ViewController* class and add an instance of it to *mWindow* before setting it as key:
```swift
// AppDelegate.swift
class ViewController : NSViewController
{
    override func loadView()
    {
        let rect = NSRect(x: 0, y: 0, width: WIDTH, height: HEIGHT)
        view = NSView(frame: rect)
        view.wantsLayer = true
        view.layer?.backgroundColor = NSColor.red.cgColor
    }
}
```
```swift
// AppDelegate.applicationDidFinishLaunching
window?.title = "Hello Triangle!"
window?.contentViewController = ViewController() // NEW LINE
window?.makeKeyAndOrderFront(nil)
```
Running this will give us a window with an nice red background.

We'll let Metal handle the refresh loop, and for that we'll need to replace the view controller's view for a *MTKView*.
To do that, we need to get the appropriate GPU.
Add a new member *mDevice* to AppDelegate
```swift
// AppDelegate.swift
import MetalKit // NEW LINE

class AppDelegate: NSObject, NSApplicationDelegate
{
    private var window:   NSWindow?
    private var device:   MTLDevice? // NEW LINE

    func applicationDidFinishLaunching(_ aNotification: Notification)
    {
        ...
        window?.makeKeyAndOrderFront(nil)

        mDevice = MTLCreateSystemDefaultDevice()   // NEW LINE
        if mDevice == nil { fatalError("NO GPU") } // NEW LINE
    }
```

To handle the *MTKView* we'll create a new class that extends *MTKViewDelegate*. This will be our `Renderer.swift`

```swift
// Renderer.swift
import MetalKit

class Renderer : NSObject
{
    public var mView: MTKView

    public init(view: MTKView)
    {
        mView = view
        super.init()
        mView.delegate = self
    }

    private func update()
    {
        // Uncomment this to check it's working
        // print("Hello frame!")
    }
}

extension Renderer: MTKViewDelegate
{
    public func draw(in view: MTKView)
    {
        // Called every frame
        self.update()
    }

    public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize)
    {
        // This will be called on resize
    }
}
```

Now we create the MTKView and replace the ViewController's with it.

```swift
// AppDelegate.swift
...
    private var mRenderer: Renderer? // NEW LINE

    func applicationDidFinishLaunching(_ aNotification: Notification)
    {
        ...
        mDevice = MTLCreateSystemDefaultDevice()
        if mDevice == nil { fatalError("NO GPU") }

        // NEW BLOCK START
        let view  = MTKView(frame: rect, device: mDevice)
        mRenderer = Renderer(view: view)

        mWindow?.contentViewController?.view = view
        // NEW BLOCK END
    }
```

And we're done!
*As a side note, this last step will remove the red background because we're no longer using the ViewController's original view. This could probably be done better, but I don't want to dwell on it.*

This implementation is really brittle (for some reason it doesn't get on top of other windows, and closing it doesn’t stop the application), but it gets the job done. I'm here for the graphics API after all, nothing else.

I'd appreciate any help/feedback in the comments though.

---

# 🔺 Rendering a plain triangle

I don't want this article to be too long, so I'll assume you already know how the general graphics pipeline works.
In case you want to get a detailed explanation, you can go to the [Apple docs](https://developer.apple.com/documentation/metal/basic_tasks_and_concepts).
[This other article](https://medium.com/@shoheiyokoyama/rendering-graphics-content-using-the-metalkit-framework-ea3503f34535) was really useful for me too.

Let's start by adding the triangle's vertex data and putting that into a *vertex buffer*:

```swift
// Renderer.swift
// NEW BLOCK START
let VERTEX_DATA: [SIMD3<Float>] =
[
    [ 0.0,  1.0, 0.0],
    [-1.0, -1.0, 0.0],
    [ 1.0, -1.0, 0.0]
]
// NEW BLOCK END

class Renderer : NSObject
{
    ...
    private func update()
    {
        // NEW BLOCK START
        let dataSize     = VERTEX_DATA.count * MemoryLayout.size(ofValue: VERTEX_DATA[0])
        let vertexBuffer = mView.device?.makeBuffer(bytes:   VERTEX_DATA,
                                                    length:  dataSize,
                                                    options: [])
        // NEW BLOCK END
    }
}
```

## Render Pipeline
Now we have to create a *Render Pipeline* to process said data.

```swift
// Renderer.swift
class Renderer : NSObject
{
    private var mPipeline: MTLRenderPipelineState
    ...
    public init(view: MTKView)
    {
        mView = view

        // NEW BLOCK START
        let pipelineDescriptor = MTLRenderPipelineDescriptor()
        pipelineDescriptor.colorAttachments[0].pixelFormat = mView.colorPixelFormat
        // TODO: pipelineDescriptor.vertexFunction                  = 
        // TODO: pipelineDescriptor.fragmentFunction                = fragmentFunction
        // TODO: pipelineDescriptor.vertexDescriptor                = vertDesc

        guard let ps = try! mView.device?.makeRenderPipelineState(descriptor: pipelineDescriptor) else
        {
            fatalError("Couldn't create pipeline state")
        }
        mPipeline = ps
        // NEW BLOCK END

        super.init()
        mView.delegate = self
    }
}
```

As you can see, we still need a couple more pieces to create the pipeline, namely the shader functions and the vertex descriptor (this last one is optional and will be discussed later).
Let's start with the shaders.

### Shaders

Metal uses it's own shading language called **MLSL**, with the file extension `.metal`.
For this example, we'll keep both vertex and fragment functions in the same file and we will just paint everything red.  
Create a new directory `Sources/Shaders` and a `HelloTriangle.metal` file in it.

```GLSL
// Sources/Shaders/HelloTriangle.metal
#include <metal_stdlib>
using namespace metal;

struct VertexIn
{
    float3 position [[ attribute(0) ]];
};

vertex
VertexOut vertex_main(VertexIn vert [[ stage_in ]])
{
    return float4(vert.position, 1.0f);
}

fragment
float4 fragment_main()
{
    return float4(1,0,0,1);
}
```

Metal uses precompiled shaders in what it's called *libraries*.
To compile them without Xcode, we'll need to run a couple of commands.  
First, compile the `.metal` into `.air` files.
```zsh
xcrun metal -c HelloTriangle.metal -o HelloTriangle.air
```

Second, pack the `.air` files into a single `.metallib`
```zsh
xcrun metal HelloTriangle.air -o HelloTriangle.metallib
```

More info about the manual compilation of Metal shaders and Libraries [here](https://developer.apple.com/documentation/metal/libraries/building_a_library_with_metal_s_command-line_tools).

Now that we have the library, we just need to load it.

```swift
// Renderer.swift
import MetalKit

let SHADERS_DIR_LOCAL_PATH        = "/Sources/Shaders" // NEW LINE
let DEFAULT_SHADER_LIB_LOCAL_PATH = SHADERS_DIR_LOCAL_PATH + "/HelloTriangle.metallib" // NEW LINE

...

public init(view: MTKView)
{
    mView = view

    // NEW BLOCK START
    let shaderLibPath = FileManager.default
                                    .currentDirectoryPath +
                        DEFAULT_SHADER_LIB_LOCAL_PATH

    guard let library = try! mView.device?.makeLibrary(filepath: shaderLibPath) else
    {
        fatalError("No shader library!")
    }
    let vertexFunction   = library.makeFunction(name: "vertex_main")
    let fragmentFunction = library.makeFunction(name: "fragment_main")
    // NEW BLOCK END

    let pipelineDescriptor = MTLRenderPipelineDescriptor()
    pipelineDescriptor.colorAttachments[0].pixelFormat = mView.colorPixelFormat
    pipelineDescriptor.vertexFunction                  = vertexFunction // NEW LINE
    pipelineDescriptor.fragmentFunction                = fragmentFunction // NEW LINE
}
```

Make sure the name in `library.makeFunction` matches the one you want to call for that stage in the shader.

### Vertex descriptors
You can technically pass the vertex data raw through buffer pointers and vertex indices, but it's much better to use vertex descriptors.
They, as their name implies, describe how the vertex data is arranged in memory, giving us a lot of flexibility and opportunities for optimisation.
Again, I want to keep this tutorial short, so I'll leave [this article](https://metalbyexample.com/vertex-descriptors/) in case you want to learn the specifics.

Creating a vertex descriptor is really simple, we just need to tell it how many attributes each vertex has, their formats, the index of the buffer they belong to and the offset and stride.  
In this case, we're only passing the position, so that's 1 argument of format `float3`.

```swift
// Renderer.swift
...
let vertexFunction   = library.makeFunction(name: "vertex_main")
let fragmentFunction = library.makeFunction(name: "fragment_main")

// NEW BLOCK START
let vertDesc = MTLVertexDescriptor()
vertDesc.attributes[0].format      = .float3
vertDesc.attributes[0].bufferIndex = 0
vertDesc.attributes[0].offset      = 0
vertDesc.layouts[0].stride         = MemoryLayout<SIMD3<Float>>.stride
// NEW BLOCK END
...
```

## Command Queue

Finally, last step, we need to tell the GPU the commands we want it to perform. We do that through *CommandEncoders*, grouped into *CommandBuffers*, lined up into a Command Queue.
![Command Structure as shown in Apple's docs](https://docs-assets.developer.apple.com/published/67e10c53e3/27dc137c-d852-461e-aff2-e93ab71f44e9.png)

We have to setup a command buffer each frame, but we can reuse a single Command Queue, so we'll have it as a class member.

```swift
// Renderer.swift
...
public class Renderer : NSObject
{
    public  var mView:         MTKView

    private let mPipeline:     MTLRenderPipelineStat
    private let mCommandQueue: MTLCommandQueue // NEW LINE

    public init(view: MTKView)
    {
        mView = view

        // NEW BLOCK START
        guard let cq = mView.device?.makeCommandQueue() else
        {
            fatalError("Could not create command queue")
        }
        mCommandQueue = cq
        // NEW BLOCK END
        ...
    }
```

Next, in the *update* method, we setup the commands.

```swift
// Renderer.swift
...
private func update()
{
    let dataSize     = VERTEX_DATA.count * MemoryLayout.size(ofValue: VERTEX_DATA[0])
    let vertexBuffer = mView.device?.makeBuffer(bytes:   VERTEX_DATA,
                                                length:  dataSize,
                                                options: [])
    // NEW BLOCK START
    let commandBuffer  = mCommandQueue.makeCommandBuffer()!

    let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: mView.currentRenderPassDescriptor!)
    commandEncoder?.setRenderPipelineState(mPipeline)
    commandEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
    commandEncoder?.drawPrimitives(type: .triangle,
                                   vertexStart: 0,
                                   vertexCount: 3,
                                   instanceCount: 1)
    commandEncoder?.endEncoding()

    commandBuffer.present(mView.currentDrawable!)
    commandBuffer.commit()
    // NEW BLOCK END
}
```

---

# 🎨 Adding some color

Ok, now we have a nice red triangle. Let's give it some color.
First, add the color to the vertex data:

```swift
// Renderer.swift
let VERTEX_DATA: [SIMD3<Float>] =
[
    // v0
    [ 0.0,  1.0, 0.0 ], // position
    [ 1.0,  0.0, 0.0 ], // color
    // v1
    [-1.0, -1.0, 0.0 ],
    [ 0.0,  1.0, 0.0 ],
    // v2
    [ 1.0, -1.0, 0.0 ],
    [ 0.0,  0.0, 1.0 ]
]
```

Now is when the *Vertex Descriptors* come in handy. Let them know about the changes in the data structure.

```swift
// Renderer.swift
let vertDesc = MTLVertexDescriptor()
vertDesc.attributes[0].format      = .float3
vertDesc.attributes[0].bufferIndex = 0
vertDesc.attributes[0].offset      = 0
vertDesc.attributes[1].format      = .float3 // NEW LINE
vertDesc.attributes[1].bufferIndex = 0 // NEW LINE
vertDesc.attributes[1].offset      = MemoryLayout<SIMD3<Float>>.stride // NEW LINE
vertDesc.layouts[0].stride         = MemoryLayout<SIMD3<Float>>.stride * 2 // LINE MODIFIED!
```

The layout stride is now twice the `SIMD3<Float>` because each vertex has 2 float3 of data.

And finally, the shader needs some updates.
Add a new struct that will serve as the output of the vertex stage and as the input of the fragment stage.
Also add a new attribute *color* to the pre-existent *VertexIn*.

```GLSL
// HelloTriangle.metal
#include <metal_stdlib>
using namespace metal;

struct VertexIn
{
    float3 position [[ attribute(0) ]];
    float3 color    [[ attribute(1) ]];
};

struct VertexOut
{
    float4 position [[ position ]];
    float3 color;
};

vertex
VertexOut vertex_main(VertexIn vert [[ stage_in ]])
{
    VertexOut out;
    out.position = float4(vert.position, 1.0f);
    out.color    = vert.color;
    return out;
}

fragment
float4 fragment_main(VertexOut frag [[ stage_in ]])
{
    return sqrt(float4(frag.color, 1.0));
}
```

We do a square root of the color, to perform a simple Gamma correction, but that's totally optional.

Et voilà!
![Colourful triangle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/owikxl1c697b0aef34qh.png)

From here, the sky is the limit. Add movement, projections, textures, lights, render passes, etc, etc.

Let me know any issues/improvements/feedback in the comments, and happy coding! :D

---

## 📚 References
- [NSWindow without Storyboard](kicsipixel.github.io/2020/nostoryboard/)
- [Rendering graphics content using the MetalKit framework](https://medium.com/@shoheiyokoyama/rendering-graphics-content-using-the-metalkit-framework-ea3503f34535)
- [Building a Library with Metal's Command-Line Tools](https://developer.apple.com/documentation/metal/libraries/building_a_library_with_metal_s_command-line_tools)
- [Vertex Data and Vertex Descriptors](https://metalbyexample.com/vertex-descriptors/)