Skip to content
All essays
iOSMarch 27, 202513 min

Swift Metal: High-Performance Graphics Programming

Master Metal for GPU-accelerated graphics. Metal shading language and 3D rendering.

Ü
Ümit Uz
Mobile & Full Stack Developer

Metal provides low-level access to GPU for high-performance graphics and compute. Learn to create stunning visual effects.

Metal Setup

Initialize Metal Device

swift
import Metal
import MetalKit

class MetalRenderer {
    var device: MTLDevice
    var commandQueue: MTLCommandQueue

    init() {
        guard let device = MTLCreateSystemDefaultDevice() else {
            fatalError("Metal is not supported on this device")
        }

        self.device = device
        self.commandQueue = device.makeCommandQueue()!
    }
}

Basic Rendering

Metal View Setup

swift
struct MetalView: UIViewRepresentable {
    func makeUIView(context: Context) -> MTKView {
        let mtkView = MTKView()

        guard let device = MTLCreateSystemDefaultDevice() else {
            return mtkView
        }

        mtkView.device = device
        mtkView.delegate = context.coordinator
        mtkView.enableSetNeedsDisplay = false
        mtkView.isPaused = false

        context.coordinator.setupMetal(view: mtkView)

        return mtkView
    }

    func updateUIView(_ uiView: MTKView, context: Context) {}

    func makeCoordinator() -> Renderer {
        Renderer()
    }

    class Renderer: NSObject, MTKViewDelegate {
        var device: MTLDevice?
        var commandQueue: MTLCommandQueue?
        var pipelineState: MTLRenderPipelineState?

        func setupMetal(view: MTKView) {
            device = view.device
            commandQueue = device?.makeCommandQueue()

            view.clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)

            setupPipelineState(view: view)
        }

        func setupPipelineState(view: MTKView) {
            guard let device = device,
                  let library = device.makeLibrary(source: """
                #include <metal_stdlib>
                using namespace metal;

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

                vertex VertexOut vertex_main(uint vertexID [[vertex_id]]) {
                    VertexOut out;
                    out.position = float4(0.0, 0.0, 0.0, 1.0);
                    out.color = float4(1.0, 0.0, 0.0, 1.0);
                    return out;
                }

                fragment float4 fragment_main(VertexOut in [[stage_in]]) {
                    return in.color;
                }
                """, options: []) else {
                return
            }

            let vertexFunction = library.makeFunction(name: "vertex_main")
            let fragmentFunction = library.makeFunction(name: "fragment_main")

            let pipelineDescriptor = MTLRenderPipelineDescriptor()
            pipelineDescriptor.vertexFunction = vertexFunction
            pipelineDescriptor.fragmentFunction = fragmentFunction
            pipelineDescriptor.colorAttachments[0].pixelFormat = view.colorPixelFormat

            pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
        }

        func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}

        func draw(in view: MTKView) {
            guard let drawable = view.currentDrawable,
                  let descriptor = view.currentRenderPassDescriptor,
                  let commandBuffer = commandQueue?.makeCommandBuffer(),
                  let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor),
                  let pipelineState = pipelineState else {
                return
            }

            encoder.setRenderPipelineState(pipelineState)
            encoder.endEncoding()

            commandBuffer.present(drawable)
            commandBuffer.commit()
        }
    }
}

Shaders

Vertex and Fragment Shaders

swift
// Metal Shaders (.metal file)
#include <metal_stdlib>
using namespace metal;

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

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

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

fragment float4 fragment_main(VertexOut in [[stage_in]]) {
    return in.color;
}

Lighting Shader

swift
// Advanced Metal shader with lighting
struct Light {
    float3 position;
    float3 color;
    float intensity;
};

struct Material {
    float3 ambient;
    float3 diffuse;
    float3 specular;
    float shininess;
};

fragment float4 lighting_fragment_main(
    VertexOut in [[stage_in]],
    constant Light& light [[buffer(0)]],
    constant Material& material [[buffer(1)]]
) {
    // Ambient
    float3 ambient = material.ambient * light.color * light.intensity;

    // Diffuse
    float3 lightDir = normalize(light.position - in.position.xyz);
    float diff = max(dot(normalize(in.normal), lightDir), 0.0);
    float3 diffuse = material.diffuse * light.color * diff * light.intensity;

    // Specular
    float3 viewDir = normalize(-in.position.xyz);
    float3 reflectDir = reflect(-lightDir, normalize(in.normal));
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    float3 specular = material.specular * light.color * spec * light.intensity;

    return float4(ambient + diffuse + specular, 1.0);
}

Compute Shaders

GPU Computing

swift
class ComputeProcessor {
    var device: MTLDevice
    var commandQueue: MTLCommandQueue

    init() {
        guard let device = MTLCreateSystemDefaultDevice() else {
            fatalError("Metal not supported")
        }

        self.device = device
        self.commandQueue = device.makeCommandQueue()!
    }

    func processTexture(texture: MTLTexture) {
        guard let commandBuffer = commandQueue.makeCommandBuffer(),
              let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
            return
        }

        let computePipelineState = createComputePipeline()

        computeEncoder.setComputePipelineState(computePipelineState)
        computeEncoder.setTexture(texture, index: 0)

        let threadsPerThreadgroup = MTLSize(width: 16, height: 16, depth: 1)
        let threadgroupsPerGrid = MTLSize(
            width: (texture.width + 15) / 16,
            height: (texture.height + 15) / 16,
            depth: 1
        )

        computeEncoder.dispatchThreadgroups(
            threadgroupsPerGrid,
            threadsPerThreadgroup: threadsPerThreadgroup
        )

        computeEncoder.endEncoding()
        commandBuffer.commit()
    }

    private func createComputePipeline() -> MTLComputePipelineState {
        let source = """
            #include <metal_stdlib>
            using namespace metal;

            kernel void texture_process(
                texture2d<float, access::read> input [[texture(0)]],
                texture2d<float, access::write> output [[texture(1)]],
                uint2 gid [[thread_position_in_grid]]
            ) {
                float4 color = input.read(gid);
                float gray = dot(color.rgb, float3(0.299, 0.587, 0.114));
                output.write(float4(gray, gray, gray, color.a), gid);
            }
        """

        let library = try! device.makeLibrary(source: source, options: nil)
        let function = library.makeFunction(name: "texture_process")

        let computeDescriptor = MTLComputePipelineDescriptor()
        computeDescriptor.computeFunction = function

        return try! device.makeComputePipelineState(descriptor: computeDescriptor)
    }
}

MetalKit Integration

MTKViewDelegate

swift
class GameRenderer: NSObject, MTKViewDelegate {
    var device: MTLDevice
    var commandQueue: MTLCommandQueue
    var pipelineState: MTLRenderPipelineState?

    init(mtkView: MTKView) {
        guard let device = MTLCreateSystemDefaultDevice() else {
            fatalError("Metal not supported")
        }

        self.device = device
        self.commandQueue = device.makeCommandQueue()!

        super.init()

        mtkView.device = device
        mtkView.delegate = self
        mtkView.clearColor = MTLClearColor(
            red: 0.1,
            green: 0.1,
            blue: 0.1,
            alpha: 1.0
        )

        setupPipeline(view: mtkView)
    }

    func setupPipeline(view: MTKView) {
        // Setup render pipeline
    }

    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
        // Handle resize
    }

    func draw(in view: MTKView) {
        guard let drawable = view.currentDrawable,
              let descriptor = view.currentRenderPassDescriptor,
              let commandBuffer = commandQueue.makeCommandBuffer(),
              let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else {
            return
        }

        // Render code

        encoder.endEncoding()
        commandBuffer.present(drawable)
        commandBuffer.commit()
    }
}

Performance Optimization

Best Practices

  1. 1Batching: Batch draw calls
  2. 2Culling: Use frustum culling
  3. 3LOD: Level of detail for distant objects
  4. 4Memory: Manage GPU memory carefully
  5. 5Profiling: Use Metal performance tools
  6. 6Shaders: Optimize shader code
  7. 7State Changes: Minimize state changes
  8. 8Multithreading: Use command buffers efficiently

Metal enables stunning graphics and high-performance computing!

Related essays

Next essay
Swift SpriteKit: 2D Game Development