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

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

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(...)
Your render loop
Geec viqxuj tuuq

Indirect rendering
Emsohert qapyepevc

1. Initializing the Uniform Buffer

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

var uniformsBuffer: MTLBuffer!
mutating func initializeUniforms() {
  let bufferLength = MemoryLayout<Uniforms>.stride
    uniformsBuffer =
  Renderer.device.makeBuffer(length: bufferLength, options: [])
  uniformsBuffer.label = "Uniforms"
}
lazy var modelParamsBuffer: MTLBuffer = {
  let buffer = Renderer.device.makeBuffer(
    length: MemoryLayout<ModelParams>.stride)!
  buffer.label = "Model Parameters Buffer"
  return buffer
}()
func updateUniforms(scene: GameScene, uniforms: Uniforms) {
  var uniforms = uniforms
  uniformsBuffer.contents().copyMemory(
    from: &uniforms,
    byteCount: MemoryLayout<Uniforms>.stride)
}
updateUniforms(scene: scene, uniforms: uniforms)

2. Setting up an Indirect Command Buffer (ICB)

You’re now ready to create some indirect commands.

var icb: MTLIndirectCommandBuffer!
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
}
guard let icb = Renderer.device.makeIndirectCommandBuffer(
  descriptor: icbDescriptor,
  maxCommandCount: models.count,
  options: []) else { fatalError("Failed to create ICB") }
self.icb = icb

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.

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)
}
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)
Vertex buffer layouts
Tejper duvwir zasoofd

icbCommand.drawIndexedPrimitives(
  .triangle,
  indexCount: submesh.indexCount,
  indexType: submesh.indexType,
  indexBuffer: submesh.indexBuffer,
  indexBufferOffset: submesh.indexBufferOffset,
  instanceCount: 1,
  baseVertex: 0,
  baseInstance: 0)
mutating func initialize(models: [Model]) {
  initializeUniforms()
  initializeICBCommands(models)
}
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.

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()
}
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….

renderEncoder.executeCommandsInBuffer(
  icb, range: 0..<scene.models.count)
The indirect command buffer inherits pipelines ( inheritPipelineState = YES) but the render pipeline set on this encoder does not support indirect command buffers ( supportIndirectCommandBuffers = NO )
pipelineDescriptor.supportIndirectCommandBuffers = indirect
Indirect encoding
Ehgahefx avkukirk

Execute indirect commands
Ukumofe arkitann zanguxfc

The indirect command list
Jqa uwmiyuwc quhxujb hamg

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.
© 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