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

26. Indirect Command Encoding
Written by Marius Horga & Caroline Begbie

Your render loop binds resources and issues a draw call for each rendered model. This process has worked well so far. Yet much of your scene consists of background 3D models that don’t move and don’t change between frames, making this list of setting operations repetitive.

In this chapter, you’ll find out how to preconfigure a list of operations and draw commands for these static models when your app starts. You’ll then be able to remove that long list from your render loop.

You may not see the immediate gains of indirect rendering on the CPU. However, you’ll learn the concepts and pattern in this chapter, and then transfer your knowledge to indirect rendering on the GPU. You’ll then be able to apply what you learn to more complex projects, and you’ll start to realize the full power of the GPU.

The Starter Project

The GPU requires a lot of information to be able to render a model. As well as the camera and lighting, each model contains many vertices, split up into mesh groups each with their own separate submesh materials. The following image shows a house with at least five different submeshes:

A house model with submeshes expanded
A house model with submeshes expanded

The scene you’ll render, in contrast, will only render two static models, each with one mesh and one submesh. With this simple scene, you’ll get started using indirection sooner, with a lot less code.

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

The starter app
The starter app

The project contains only the bare minimum to render these two textured models. There are no shadows, transparency or lighting.

These are the important things to notice:

  • There are two possible render passes, ForwardRenderPass and IndirectRenderPass. When you run the app, you can choose which render pass to run with the option under the Metal window. Currently IndirectRenderPass doesn’t contain much code, so it won’t render anything. IndirectRenderPass.swift is where you’ll add most of the code in this chapter.

  • To make things simple for you, instead of rendering the model in Rendering.swift, the rendering code is all in ForwardRenderPass. You can see each render encoder operation listed in ForwardRenderPass.draw(commandBuffer:scene:uniforms). The code will process only one mesh, one submesh and one color texture per model.

  • Up until now, you’ve changed Uniforms.modelMatrix and Params.tiling for every model. This isn’t strictly correct. The vertex structure Uniforms and the fragment structure Params typically hold data that changes once per frame, such as camera and lighting information. modelMatrix and tiling are per-object properties. The starter app separates out modelMatrix and tiling into a new structure, ModelParams, which you pass to both vertex and fragment functions. As you’re not doing any lighting, Params doesn’t exist in this app.

Indirect Command Buffers (ICB)

This is a list of some of your operations in your current render loop:

encoder.setRenderPipelineState(...)
encoder.setVertexBuffer(...)
encoder.setFragmentBytes(...)
encoder.setFragmentBuffer(...)
encoder.drawIndexedPrimitives(...)

Since, for static models, these operations don’t need updating every frame, you can set up a list of rendering operations, during your app loading, in an Indirect Command Buffer, or ICB for short.

In your render loop, you can then issue one single execute command to your render command encoder, and all the bindings and commands in the ICB will transfer to the GPU.

Your rendering process currently looks like this:

Your render loop
Your render loop

You load all the model data, materials and pipeline states at the start of the app. For each render pass, you create a render command encoder and bind all the resources, one after another, to that encoder, ending with a draw call. You repeat the drawing process for each model.

As well as initializing all your resources at the start of the app, you’ll also initialize your render commands there too. You’ll set up each draw command with pointers to the relevant uniform, material and vertex buffers and specify how to do the draw. During the render loop, you can just issue one execute command to the render command encoder, and the encoder will send the list of commands, all at once, off to the GPU.

Your rendering process will then look like this:

Indirect rendering
Indirect rendering

Remember that your aim is to do as much as you can when your app first loads, and as little as you have to per frame. To achieve this, you’ll:

  1. Place your uniform data in a buffer. Unfortunately you can’t send ad hoc bytes to the GPU using an ICB, but you can still update the uniforms buffer each frame.
  2. Set up an indirect command buffer. This buffer will hold all the draw commands.
  3. Loop through the models, setting up the bindings in the indirect command buffer.
  4. Ensure the resources are resident on the GPU.
  5. Execute the command list.

That’s quite a todo list, so let’s get started!

1. Initializing the Uniform Buffer

➤ In the Render Passes folder, open IndirectRenderPass.swift.

IndirectRenderPass contains the minimum code to conform to RenderPass. The pipeline is the same as that in the forward render pass, so it will call the same shader functions.

➤ Add a new property to IndirectRenderPass:

var uniformsBuffer: MTLBuffer!

You create a buffer that will hold the camera uniform data.

➤ Add the initializer method to IndirectRenderPass:

mutating func initializeUniforms() {
  let bufferLength = MemoryLayout<Uniforms>.stride
    uniformsBuffer =
  Renderer.device.makeBuffer(length: bufferLength, options: [])
  uniformsBuffer.label = "Uniforms"
}

➤ In the Geometry folder, open Model.swift and add a new buffer property to Model:

lazy var modelParamsBuffer: MTLBuffer = {
  let buffer = Renderer.device.makeBuffer(
    length: MemoryLayout<ModelParams>.stride)!
  buffer.label = "Model Parameters Buffer"
  return buffer
}()

Currently, in the render loop, you transfer ModelParams to the GPU as ad hoc bytes. Instead, you’ll hold the model matrix and tiling data in this buffer.

Even though the models are static, you’ll still have to update the camera uniforms every frame, in case the user has changed the camera position.

➤ Open IndirectRenderPass.swift and add this new method to IndirectRenderPass:

func updateUniforms(scene: GameScene, uniforms: Uniforms) {
  var uniforms = uniforms
  uniformsBuffer.contents().copyMemory(
    from: &uniforms,
    byteCount: MemoryLayout<Uniforms>.stride)
}

You load up the uniforms buffer with the current data for each frame.

➤ Call this method at the top of draw(commandBuffer:scene:uniforms:):

updateUniforms(scene: scene, uniforms: uniforms)

2. Setting up an Indirect Command Buffer (ICB)

You’re now ready to create some indirect commands.

ICBs allow you to set three objects using command arguments:

  • Pipeline State
  • Vertex Buffer
  • Fragment Buffer

➤ Open ForwardRenderPass.swift, and look at draw(commandBuffer:scene:uniforms:). Refresh your memory on where you use those three render operations in the render loop. You’re going to move these operations to an indirect command list and use buffers instead of setting vertex or fragment bytes.

➤ Open IndirectRenderPass.swift, and add a new property to IndirectRenderPass.

var icb: MTLIndirectCommandBuffer!

This buffer will hold the render command list.

➤ Create a new method in IndirectRenderPass:

mutating func initializeICBCommands(_ models: [Model]) {
  // 1
  let icbDescriptor = MTLIndirectCommandBufferDescriptor()
  // 2
  icbDescriptor.commandTypes = [.drawIndexed]
  // 3
  icbDescriptor.inheritBuffers = false
  // 4
  icbDescriptor.maxVertexBufferBindCount = 25
  icbDescriptor.maxFragmentBufferBindCount = 25
  // 5
  icbDescriptor.inheritPipelineState = true
}

Going through these settings:

  1. You create an Indirect Command Buffer descriptor.
  2. You specify that (eventually) the GPU should expect an indexed draw call. That’s a draw call that uses an index buffer for indexing into the vertices.
  3. If you set inheritBuffers as true, you can only set the buffers on the render command encoder and not on the ICB.
  4. You set the maximum number of buffers that the ICB can bind to in the vertex and fragment shader parameters to 25. This is far too many, but you can renumber the buffer indices when your app is complete.
  5. You set inheritPipelineState to true. Because this app contains such simple models, you can set the render pipeline state at the start of the render pass, and all encoder commands will inherit the current pipeline state. If you require a different pipeline for different submeshes, you’d set inheritPipelineState to false and add setting the render pipeline state to the list of indirect encoder command bindings.

➤ At the end of initializeICBCommands(_:), create the indirect command buffer:

guard let icb = Renderer.device.makeIndirectCommandBuffer(
  descriptor: icbDescriptor,
  maxCommandCount: models.count,
  options: []) else { fatalError("Failed to create ICB") }
self.icb = icb

The ICB will need one command per draw call. In this app, you’re only performing one draw call per model, but in a more complex app where you’re doing a draw call for every submesh, you’d have to iterate through the models prior to setting up the ICB to find out how many draw calls you’ll do.

3. Setting up the Model Bindings

Now that you’ve set up an indirect command buffer, you’ll add the list of bindings to it.

➤ Add the following code to the end of initializeICBCommands(_:):

for (modelIndex, model) in models.enumerated() {
  var modelParams = ModelParams(
    modelMatrix: model.transform.modelMatrix,
    tiling: model.tiling)
  model.modelParamsBuffer.contents().copyMemory(
    from: &modelParams,
    byteCount: MemoryLayout<ModelParams>.stride)
}

You iterate through the models and update each model’s buffer with its model matrix and tiling data.

➤ Continue adding code at the end of the for loop:

let mesh = model.meshes[0]
let submesh = mesh.submeshes[0]
let icbCommand = icb.indirectRenderCommandAt(modelIndex)
icbCommand.setVertexBuffer(
  uniformsBuffer, offset: 0, at: UniformsBuffer.index)
icbCommand.setVertexBuffer(
  model.modelParamsBuffer, offset: 0, at: ModelParamsBuffer.index)
icbCommand.setFragmentBuffer(
  model.modelParamsBuffer, offset: 0, at: ModelParamsBuffer.index)
icbCommand.setVertexBuffer(
  mesh.vertexBuffers[VertexBuffer.index],
  offset: 0,
  at: VertexBuffer.index)
icbCommand.setVertexBuffer(
  mesh.vertexBuffers[UVBuffer.index],
  offset: 0,
  at: UVBuffer.index)
icbCommand.setFragmentBuffer(
  submesh.materialBuffer, offset: 0, at: MaterialBuffer.index)

This code will look familiar to you from the render loop in ForwardRenderPass.draw(commandBuffer:scene:uniforms:). You set all the necessary data for each model’s draw call, keeping track of each draw command with modelIndex.

For setting the vertex buffers, it’s important to match how you formatted your vertex buffers when first loading the models.

➤ In the Geometry folder, open VertexDescriptor.swift, and examine how the vertex buffers are configured.

Vertex buffer layouts
Vertex buffer layouts

There are only two vertex buffers, as the vertex descriptor is simplified from previous chapters.

➤ Open IndirectRenderPass.swift, and add the draw call to the end of the previous for loop in initializeICBCommands(_:):

icbCommand.drawIndexedPrimitives(
  .triangle,
  indexCount: submesh.indexCount,
  indexType: submesh.indexType,
  indexBuffer: submesh.indexBuffer,
  indexBufferOffset: submesh.indexBufferOffset,
  instanceCount: 1,
  baseVertex: 0,
  baseInstance: 0)

This draw command is very similar to the one that you’ve already been using. There are a couple of extra arguments:

  • baseVertex: The vertex in the vertex buffer to start rendering from.
  • baseInstance: The instance to start rendering from. You only have one instance of each model here.

The command list is now complete.

➤ In IndirectRenderPass, create a new method:

mutating func initialize(models: [Model]) {
  initializeUniforms()
  initializeICBCommands(models)
}

Here, you call the two methods that will initialize your buffers.

➤ In the Renderer folder, open Renderer.swift, and add this to the end of initialize(_:):

indirectRenderPass.initialize(models: scene.models)

4. Making the Resources Resident on the GPU

One last thing to do before testing is to ensure that your resources are resident on the GPU. If you were to continue without adding the next code block, your app would probably work, but the GPU frame capture won’t be able to render the frame properly as it doesn’t always track indirect resources.

➤ Open IndirectRenderPass.swift, and create a new method in IndirectRenderPass:

func useResources(
  encoder: MTLRenderCommandEncoder, models: [Model]
) {
  encoder.pushDebugGroup("Using resources")
  encoder.useResource(
    uniformsBuffer,
    usage: .read,
    stages: .vertex)
  for model in models {
    let mesh = model.meshes[0]
    let submesh = mesh.submeshes[0]
    [
      model.modelParamsBuffer,
      mesh.vertexBuffers[VertexBuffer.index],
      mesh.vertexBuffers[UVBuffer.index],
      submesh.indexBuffer
    ].forEach { buffer in
      encoder.useResource(buffer, usage: .read, stages: .vertex)
    }
    [
      model.modelParamsBuffer,
      submesh.materialBuffer
    ].forEach { buffer in
      encoder.useResource(buffer, usage: .read, stages: .fragment)
    }
  }
  encoder.popDebugGroup()
}

Here, you ensure that all resources are definitely resident on the GPU. The code is longwinded, but the CPU overhead of running it is negligible. Ensuring that all resources are on the GPU means that you are less likely to run the risk of your computer locking up, or some other buffer corruption.

➤ Call this method in draw(commandBuffer:scene:uniforms:) after the guard where you create the render command encoder.

useResources(encoder: renderEncoder, models: scene.models)

5. Executing the Command List

All the code you have written in this chapter has been building up to one command. Drum roll….

➤ Still in IndirectRenderPass.swift, add the following code to draw(commandBuffer:scene:uniforms:), before renderEncoder.endEncoding():

renderEncoder.executeCommandsInBuffer(
  icb, range: 0..<scene.models.count)

This code will execute all the commands in the indirect command buffer’s list within the range specified here. If you specify a range of 0..<1, then only the first draw call would be performed.

➤ Build and run the app, and switch to Indirect encoding.

And… your app crashes:

The indirect command buffer inherits pipelines ( inheritPipelineState = YES) but the render pipeline set on this encoder does not support indirect command buffers ( supportIndirectCommandBuffers = NO )

When you use a pipeline state in an indirect command list, you have to tell it that it should support indirect command buffers.

➤ Open Pipelines.swift, and add this to createForwardPSO(indirect:) before return:

pipelineDescriptor.supportIndirectCommandBuffers = indirect

In IndirectRenderPass.init(), you set up the pipeline state with indirect set to true.

➤ Build and run the app, and switch to Indirect encoding.

Indirect encoding
Indirect encoding

This may not be the most exciting result. Both forward rendering and indirect encoding renders look exactly the same. But behind the scenes, it’s a different story. Very little is happening in your render loop, and all the heavy lifting is done at the very start of the app. Success!

➤ Capture the GPU workload, and expand both the Indirect Command Encoding render pass and Indirect Command Encoding.

Execute indirect commands
Execute indirect commands

➤ In the bound resources, double-click Indirect Command Buffer.

The indirect command list
The indirect command list

You see both your draw call commands listed with their encoded resources.

As previous stated, in this app you won’t notice any improvement in performance. CPU indirect rendering is only worthwhile if you’re rendering thousands of static models. However, you now have the skills to approach GPU-driven rendering in the next chapter!

Key Points

  • Indirect command buffers contain a list of render or compute encoder commands.
  • You can create the list of commands on the CPU at the start of your app. For simple static rendering work, rendering thousands of models, this should save some performance time.
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.
© 2026 Kodeco Inc.