Chapters

Hide chapters

Metal by Tutorials

Fifth Edition · macOS 26, iOS 26 · Swift 6, Metal 3 · Xcode 26

Section I: Beginning Metal

Section 1: 10 chapters
Show chapters Hide chapters

Section II: Intermediate Metal

Section 2: 8 chapters
Show chapters Hide chapters

Section III: Advanced Metal

Section 3: 8 chapters
Show chapters Hide chapters

29. Metal Performance Shaders
Written by Marius Horga & Caroline Begbie

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In Chapter 19, “Tessellation & Terrains”, you had a brief taste of using the Metal Performance Shaders (MPS) framework. MPS consists of low-level, fine-tuned, high-performance kernels that run off the shelf with minimal configuration. In this chapter, you’ll dive a bit deeper into the world of MPS.

Metal Performance Shaders are a collection of data-parallel primitives that are tuned specifically to Apple hardware. There are several different operation types:

  • Image filters, such as convolutions and histograms.
  • Neural networks for machine learning.
  • Mathematical operations for solving systems of equations.

Think of MPS kernels as convenient black boxes that work efficiently and seamlessly with your command buffer. Simply give it the desired effect, a source and destination resource (buffer or texture), and then encode GPU commands on the fly!

In this chapter, you’ll start off with a simple filter, then look at convolution and more complex image processing. You’ll then get a taste of matrix and vector math using MPS.

The Sobel Filter

The Sobel filter is a great way to detect edges in an image.

➤ In the starter folder for this chapter, open and examine the Sobel project.

Sobel is very simple having only a basic renderer set up in Renderer.swift. The asset catalog contains an image called “fruit”. Renderer loads this image into an MTLTexture ready for you to process it during the draw call.

The fruit image
The fruit image

If you run the app, you’ll probably get a pink view, as the renderer doesn’t render anything.

➤ In draw(in:), replace // add code here with:

let shader = MPSImageSobel(device: device)
shader.encode(
  commandBuffer: commandBuffer,
  sourceTexture: sourceTexture,
  destinationTexture: drawable.texture)

You define a Metal Performance Shader and encode it, using the current command buffer, the source texture and an output texture. The output texture in this case goes directly to the view’s drawable render target.

If you build and run, you’ll get a crash:

failed assertion `frameBufferOnly texture not supported for compute.'

The drawable texture is a highly optimized render target, so you must inform the GPU whenever you want to use the texture in a shader.

➤ At the end of init(metalView:), add this code:

metalView.framebufferOnly = false

The drawable texture will be less efficient, but you will be able to use it directly in an MPS.

➤ Build and run to see the effect of the Sobel filter:

The Sobel filter
The Sobel filter

As you can see, it’s simple to run the shader, and you don’t have to know about how the filter is created.

Note: MPS kernels are not thread-safe, so it’s not recommended to run the same kernel on multiple threads that are all writing to the same command buffer concurrently.

Image Processing

There are a few dozen MPS image filters, among the most common being:

A Gaussian blur matrix
E Gounsuek dyuw zumnof

Convolution
Gajruloxoof

(6 * 1  +  7 * 2  +  3 * 1  +
 4 * 2  +  9 * 4  +  8 * 2  +
 9 * 1  +  2 * 2  +  3 * 1) / 16 = 6
Convolution applied to border pixels
Xahzoqiheey umtyaid va sawsip wuhozd

(0 * 1  +  0 * 2  +  0 * 1  +
 0 * 2  +  6 * 4  +  7 * 2  +
 0 * 1  +  4 * 2  +  9 * 1) / 9 = 6

Bloom

The bloom effect is quite a spectacular one. It amplifies the brightness of objects in the scene and makes them look luminous as if they’re emitting light themselves.

The bloom effect
Hwu htiaf onqehy

The Starter Project

➤ In Xcode, open the starter project, MPSPostProcessing, and build and run the app.

Setting Up the Textures

➤ In the Post Processing folder, open Bloom.swift, and import the MPS framework:

import MetalPerformanceShaders
var outputTexture: MTLTexture!
var finalTexture: MTLTexture!
outputTexture = TextureController.makeTexture(
  size: size,
  pixelFormat: view.colorPixelFormat,
  label: "Output Texture",
  usage: [.shaderRead, .shaderWrite])
finalTexture = TextureController.makeTexture(
  size: size,
  pixelFormat: view.colorPixelFormat,
  label: "Final Texture",
  usage: [.shaderRead, .shaderWrite])

Image Threshold to Zero

The Metal Performance Shader MPSImageThresholdToZero is a filter that returns either the original value for each pixel having a value greater than a specified brightness threshold or 0. It uses the following test:

destinationColor =
  sourceColor > thresholdValue ? sourceColor : 0
guard
  let drawableTexture =
    view.currentDrawable?.texture else { return }
let brightness = MPSImageThresholdToZero(
  device: Renderer.device,
  thresholdValue: 0.3,
  linearGrayColorTransform: nil)
brightness.label = "MPS brightness"
brightness.encode(
  commandBuffer: commandBuffer,
  sourceTexture: drawableTexture,
  destinationTexture: outputTexture)
metalView.framebufferOnly = false

The Blit Command Encoder

➤ Open Bloom.swift, and add this code to the end of postProcess(view:commandBuffer:):

finalTexture = outputTexture
guard let blitEncoder = commandBuffer.makeBlitCommandEncoder()
  else { return }
let origin = MTLOrigin(x: 0, y: 0, z: 0)
let size = MTLSize(
  width: drawableTexture.width,
  height: drawableTexture.height,
  depth: 1)
blitEncoder.copy(
  from: finalTexture,
  sourceSlice: 0,
  sourceLevel: 0,
  sourceOrigin: origin,
  sourceSize: size,
  to: drawableTexture,
  destinationSlice: 0,
  destinationLevel: 0,
  destinationOrigin: origin)
blitEncoder.endEncoding()
Brightness threshold
Rsecvnyihs brgerjulq

Gaussian Blur

MPSImageGaussianBlur is a filter that convolves an image with a Gaussian blur with a given sigma value (the amount of blur) in both the X and Y directions.

let blur = MPSImageGaussianBlur(
  device: Renderer.device,
  sigma: 9.0)
blur.label = "MPS blur"
blur.encode(
  commandBuffer: commandBuffer,
  inPlaceTexture: &outputTexture,
  fallbackCopyAllocator: nil)
Brightness and blur
Qcigphcucm ilm zcuq

Image Add

The final part of creating the bloom effect is to add the pixels of this blurred image to the pixels of the original render.

let add = MPSImageAdd(device: Renderer.device)
add.encode(
  commandBuffer: commandBuffer,
  primaryTexture: drawableTexture,
  secondaryTexture: outputTexture,
  destinationTexture: finalTexture)
Brightness, blur and add
Dzoxylcizl, wkoy ogv odt

let brightness = MPSImageThresholdToZero(
  device: Renderer.device,
  thresholdValue: 0.8,
  linearGrayColorTransform: nil)
Glowing skeletons
Tsecovg yfuzuhacx

Matrix / Vector Mathematics

You learned in the previous section how you could quickly apply a series of MPS filters that are provided by the framework. But what if you wanted to make your own filters?

import Playgrounds
import MetalPerformanceShaders

#Playground {
  guard let device = MTLCreateSystemDefaultDevice(),
    let commandQueue = device.makeCommandQueue()
  else { fatalError() }

  let size = 4
  let count = size * size

  guard let commandBuffer = commandQueue.makeCommandBuffer()
    else { fatalError() }

  commandBuffer.commit()
  await commandBuffer.completed()
}
Playground Canvas
Yvodwxeiqt Sutqen

func createMPSMatrix(withRepeatingValue: Float) -> MPSMatrix {
  // 1
  let rowBytes = MPSMatrixDescriptor.rowBytes(
    forColumns: size,
    dataType: .float32)
  // 2
  let array = [Float](
    repeating: withRepeatingValue,
    count: count)
  // 3
  guard let buffer = device.makeBuffer(
    bytes: array,
    length: size * rowBytes,
    options: [])
  else { fatalError() }
  // 4
  let matrixDescriptor = MPSMatrixDescriptor(
    rows: size,
    columns: size,
    rowBytes: rowBytes,
    dataType: .float32)

  return MPSMatrix(buffer: buffer, descriptor: matrixDescriptor)
}
let A = createMPSMatrix(withRepeatingValue: 3)
let B = createMPSMatrix(withRepeatingValue: 2)
let C = createMPSMatrix(withRepeatingValue: 1)
let multiplicationKernel = MPSMatrixMultiplication(
  device: device,
  transposeLeft: false,
  transposeRight: false,
  resultRows: size,
  resultColumns: size,
  interiorColumns: size,
  alpha: 1.0,
  beta: 0.0)
multiplicationKernel.encode(
  commandBuffer: commandBuffer,
  leftMatrix: A,
  rightMatrix: B,
  resultMatrix: C)
// 1
let contents = C.data.contents()
let pointer = contents.bindMemory(
  to: Float.self,
  capacity: count)
// 2
(0..<count).forEach {
  let result = pointer.advanced(by: $0).pointee
}
Value history
Xayuu sofzujp

Challenge

You may have noticed that in the app where you did the bloom post processing, the Outline option does nothing. Your challenge is to fill out Outline.swift so that you have an outline render:

Outline
Aosgiyo

Key Points

  • Metal Performance Shaders are compute kernels that are performant and easy to use.
  • The framework has filters for image processing, implementations for neural networks, can solve systems of equations with matrix multiplication, and has optimized intersection testing for ray tracing.
  • Convolution takes a small matrix and applies it to a larger matrix. When applied to an image, you can blur or sharpen or distort the image.
  • Bloom adds a glow effect to an image, replicating real world camera artifacts that show up in bright light.
  • The threshold filter can filter out pixels under a given brightness threshold.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2025 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now