Preparation
Before writing any Metal code, this page walks you through cloning the project, understanding its structure, and the key concepts behind Metal rendering.
Prerequisites
Before diving into Metal, itβs helpful to have a solid foundation in key concepts.
- Swift
- Swift is the language used for iOS development, including Metal.
- Resources: Swift Programming Language Book
- Graphics Fundamentals
- Understanding how graphics work under the hood helps you optimize performance and troubleshoot issues.
- Resources:
- Learn OpenGL β Great for understanding the basics of rendering pipelines.
- Scratchapixel β In-depth tutorials on graphics algorithms.
- Metal API
- Metal is Appleβs low-level graphics framework, optimized for performance on Apple Silicon and GPUs. It abstracts hardware details while giving you fine-grained control over rendering pipelines.
- Resources:
- Metal Programming Guide β Official guide to Metal concepts and APIs.
- Metal Shading Language Specification β The complete MSL language reference (PDF). Bookmark Β§2 (address spaces) and Β§5 (vertex/fragment attributes) β youβll return to both constantly.
- Metal Feature Set Tables β Per-device capability matrix: max texture size, buffer binding slots, supported pixel formats, and more.
- Resources:
- Metal is Appleβs low-level graphics framework, optimized for performance on Apple Silicon and GPUs. It abstracts hardware details while giving you fine-grained control over rendering pipelines.
- Useful other resources:
Setting up the project
Clone the repository
git clone https://github.com/Fe0437/MetalTutorials.git
Launch Xcode and open the MetalTutorials.xcodeproj file to begin exploring the project structure and tutorials.
π Switching between tutorials
To switch between tutorials, open MTMetalTutorialsApp.swift and modify the content view by changing the function call in the WindowGroup:
@main
struct MetalTutorialsApp: App {
var body: some Scene {
WindowGroup {
// substitute here to choose the tutorial
MT1ContentView() // β change to MT2ContentView(), MT3ContentView(), β¦
}
}
}
Each MTxContentView wires up the corresponding MTKView and renderer for that tutorial.
Each tutorial folder is self-contained and builds on the previous one. The Metal shader namespaces (MT1::, MT2::, MT3::) keep everything clearly separated.
How Metal works
The Metal rendering loop
To understand each tutorial, follow this basic Metal rendering loop pattern:
flowchart TD
subgraph setup["One-time setup"]
direction TB
A([App launch]) --> B[/"Create MTLDevice\nβ the GPU handle"/]
B --> C[/"Create MTKView\nβ the Metal-backed window"/]
C --> D[/"Assign MTKViewDelegate\nβ your renderer object"/]
D --> E[/"Compile pipelines,\nallocate buffers & textures"/]
end
subgraph loop["Per-frame render loop βΊ"]
direction TB
F([draw called by MTKView]) --> G[/"new MTLCommandBuffer\nβ records this frame"/]
G --> H[/"new MTLRenderCommandEncoder\nβ encodes draw calls"/]
H --> I[["setVertexBuffer Β· setRenderPipelineState\nΒ· drawPrimitives / drawIndexedPrimitives"]]
I --> J[/"endEncoding"/]
J --> K[/"present drawable\nthen commit"/]
end
setup --> loop
K -.->|next frame| F
Understanding this loop before diving into the tutorials will make the code much easier to follow.
UIViewRepresentable pattern
sequenceDiagram
participant SwiftUI
participant MyMetalView as MyMetalView<br/>(UIViewRepresentable)
participant MTRenderer as MTRenderer<br/>(MTKViewDelegate)
participant MTKView
SwiftUI->>MyMetalView: makeCoordinator()
MyMetalView-->>SwiftUI: MTRenderer instance
SwiftUI->>MyMetalView: makeUIView(context:)
MyMetalView->>MTKView: init(frame:device:)
MyMetalView->>MTKView: delegate = context.coordinator
MyMetalView-->>SwiftUI: MTKView
loop every frame
MTKView->>MTRenderer: draw(in:)
end
The tutorials use UIViewRepresentable to embed the MTKView (a UIKit class) into SwiftUI:
struct MyMetalView: UIViewRepresentable {
typealias UIViewType = MTKView
func makeUIView(context: Context) -> MTKView { β¦ }
func updateUIView(_ uiView: MTKView, context: Context) { }
// the coordinator IS the renderer / MTKViewDelegate
func makeCoordinator() -> MyRenderer { β¦ }
}
The Coordinator pattern lets the renderer own the MTKView delegate without the view being recreated on every SwiftUI update.
Rendering fundamentals
The vocabulary youβll see everywhere
Vertex β a single point in 3D space, bundled with extra attributes like a color, normal, or texture coordinate. A mesh is just a list of vertices (and usually an index list connecting them into triangles).
Triangle β the fundamental drawing primitive. Everything rendered is decomposed into triangles because they are always flat and cheap to rasterize.
Fragment β a candidate pixel produced during rasterization. Once a triangle is projected onto the screen, the GPU fills it with fragments β one per screen pixel covered β each carrying values interpolated from the three vertices.
Rasterization β the hardware step that converts a projected triangle into a grid of fragments. Metal handles this automatically between the vertex shader and the fragment shader; you donβt write rasterization code yourself.
Shader β a small program written in Metal Shading Language (MSL) that runs on the GPU. A vertex shader transforms each vertexβs position. A fragment shader decides the final color of each fragment.
Draw call β a CPU command that says βprocess this batch of vertices using this pipeline.β Every drawPrimitives or drawIndexedPrimitives call in the tutorials is one draw call.
Framebuffer / drawable β the image the GPU is currently writing into. When a frame is complete, Metal presents the drawable to the screen.
Coordinate spaces
Vertices travel through a chain of coordinate spaces before reaching the screen:
Model Space β World Space β View Space β Clip Space β NDC β Screen Space
β β β
model matrix view matrix projection matrix
| Space | What it means |
|---|---|
| Model Space | Coordinates local to the mesh (origin at the objectβs center) |
| World Space | Mesh placed in the scene βmodel matrix applied |
| View Space | Scene seen from the camera βview matrix applied |
| Clip Space | After projection;w encodes depth; GPU clips triangles here |
| NDC | After Γ· w: X and Y in [β1, +1], Z in [0, 1] (Metal convention) |
| Screen Space | Viewport transform maps NDC coordinates to pixel positions |
The MVP matrix (Model Γ View Γ Projection) is combined on the CPU into one matrix. The vertex shader multiplies each vertex position by it once β collapsing three transforms into a single GPU multiply.
Metal object hierarchy
Every Metal object is created by something above it in this tree. The device represents the GPU and is the root of everything β nothing works without it.
MTLDevice β represents the GPU
βββ MTLLibrary β compiled Metal shader library (.metallib)
β βββ MTLFunction β one vertex, fragment, or kernel function
βββ MTLCommandQueue β persistent queue; submits frames to the GPU
β βββ MTLCommandBuffer β one frame's worth of recorded commands
β βββ MTLRenderCommandEncoder β encodes draw calls for a render pass
β βββ MTLComputeCommandEncoder β encodes GPU compute kernels
β βββ MTLBlitCommandEncoder β encodes copy/blit operations
βββ MTLRenderPipelineState β compiled vertex + fragment pair (expensive)
βββ MTLDepthStencilState β depth and stencil test configuration
βββ MTLBuffer β raw GPU memory (vertices, uniforms, indicesβ¦)
βββ MTLTexture β image data on the GPU
βββ MTLHeap β arena allocator for buffers and textures
Object lifetime and creation cost
| Object | Lifetime | Cost to create | Typical pattern |
|---|---|---|---|
MTLDevice |
App lifetime | Free | Create once at startup |
MTLLibrary |
App lifetime | Disk I/O | Create once, cache |
MTLFunction |
App lifetime | Very low | Create once per function |
MTLCommandQueue |
App lifetime | Low | Create once |
MTLRenderPipelineState |
App lifetime | High (GPU compile) | Create once, never recreate per-frame |
MTLDepthStencilState |
App lifetime | Low | Create once |
MTLBuffer |
As needed | Medium | Pool or reuse where possible |
MTLTexture |
As needed | Medium | Avoid allocating per-frame |
MTLHeap |
As needed | Medium | Allocate once; suballocate inside |
MTLCommandBuffer |
Per frame | Very low | New each frame; commit to queue |
MTLRenderCommandEncoder |
Per render pass | Very low | Create inside draw function |
Apple documentation reference
Every key Metal object used across these tutorials, with links to the official Apple docs.
| Object | Docs | Role | Tutorials |
|---|---|---|---|
MTLDevice |
β | Represents the GPU; factory for all other objects | All |
MTLCommandQueue |
β | Persistent queue; submits command buffers to the GPU | All |
MTLCommandBuffer |
β | One frameβs recorded commands; committed to the queue | All |
MTLRenderCommandEncoder |
β | Records draw calls inside a render pass | All |
MTLRenderPipelineState |
β | Compiled vertex + fragment pair; expensive β create once | All |
MTLRenderPipelineDescriptor |
β | Configuration struct used to build a pipeline state | All |
MTKView |
β | Metal-backed view; manages the drawable and drives the render loop | All |
MTKViewDelegate |
β | Two callbacks:drawableSizeWillChange + draw(in:) |
All |
MTLLibrary |
β | Container for compiled Metal shader functions | All |
MTLFunction |
β | A single vertex, fragment, or kernel shader function | All |
MTLViewport |
β | Maps NDC coordinates to a pixel rectangle on screen | 1 |
MTLRenderPassDescriptor |
β | Describes load/store actions for each color and depth attachment | All |
MTLDepthStencilState |
β | Depth test configuration: comparator, write-enabled | 2β6 |
MTLDepthStencilDescriptor |
β | Configuration struct used to build a depth/stencil state | 2β6 |
MDLAsset |
β | Loads 3D geometry files (OBJ, USDZβ¦) via Model I/O | 2 |
MTKMesh |
β | Metal-ready vertex and index buffers from a Model I/O asset | 2 |
MTLTexture |
β | Image on the GPU; used for render targets, shadow maps, GBuffer layers | 3β6 |
MTLTextureDescriptor |
β | Specifies format, dimensions, and usage flags for a texture | 3β6 |
MTLHeap |
β | Arena allocator; one useHeap call covers all suballocated resources |
6 |
MTLIndirectCommandBuffer |
β | GPU-writable draw call buffer enabling GPU-driven rendering | 6 |
MTLArgumentEncoder |
β | Encodes argument buffers for bindless/Tier-2 resource binding | 6 |
MTLComputeCommandEncoder |
β | Records compute dispatches (e.g., fills the ICB per submesh) | 6 |
MTLBlitCommandEncoder |
β | Copies data between resources (staging buffer β private texture) | 6 |