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

25. Managing Resources
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

So far, you’ve created an engine where you can load complex models with textures and materials, animate or update them per frame and render them. Your scenes will start to get more and more complicated as you develop your game, and you’ll want to find more performant ways of doing things and organizing your game resources.

Instead of processing each submesh and laboriously moving each of the submesh’s textures to the GPU, you’ll take advantage of the centralization of your textures in the Texture Controller. By the end of the chapter, you’ll be able to move all your textures to the GPU at once with just one render encoder command.

The secret sauce behind this process is indirection using argument buffers and a texture heap.

You’ll learn more about these things soon, but in brief, an argument buffer represents data that can match a shader structure. You can send the argument buffer to a shader function with one command, instead of sending each of the structure components individually.

An argument buffer containing resources
An argument buffer containing resources

A heap is exactly what it sounds like. You gather up your resources, such as textures and buffers, into an area of memory called a heap. You can then send this heap to the GPU with one command.

A heap containing textures
A heap containing textures

The Starter Project

With the basic idea under your belt, you’re ready to get started.

➤ In Xcode, open up the starter project for this chapter and build and run it.

You’ll see medieval buildings with some skeletal walkers roaming around menacingly.

The project consolidates many of the features that you’ve learned so far:

  • Shadows
  • IBL Lighting with sky box
  • Animation
  • Alpha testing
  • Textured models
  • Models with materials but no textures

There are a few added nifty features:

  1. Shadows are now soft shadows with PCF filtering.

  2. In the Textures folder, in TextureController.swift, TextureController has an extra level of indirection. The old textures dictionary is now named textureIndex and it holds indices into an array of textures.

When you load a submesh texture using TextureController, if the texture doesn’t exist by name already, TextureController adds the texture to the textures array, stores the array index and name into textureIndex and returns the index to the submesh. If the texture already exists by name, then the submesh simply holds the existing array index to the texture.

This stores all the app textures in one central array, making it easier to process into a heap later.

  1. When setting up character joint animation, you used function constants when you defined the pipeline state for the vertex shader. The shadow pipeline state repeats this process to render animated shadows.

  2. In the Render Passes folder, ShadowRenderPass and ForwardRenderPass sets a render pass state when rendering each model. The model then sets the correct mesh pipeline state depending on this render pass state, whether it is shadow or main.

Argument Buffers

When rendering a submesh, you currently send up to six textures and a material individually to the GPU for the fragment shader: Base color, normal, roughness, metalness, ambient occlusion and opacity textures. During the frame render loop, each of the textures requires a renderEncoder.setFragmentTexture(texture:at:) command.

A barrel draw call
U gabzah tpeg rumn

Creating the Shader Structure

➤ In the Shaders folder, open IBL.metal.

#if __METAL_VERSION__
// MARK: - Metal Shading Language

#include <metal_stdlib>
using namespace metal;

struct ShaderMaterial {
  array<texture2d<float>, MaterialTextureCount> textures;
  Material material;
};

#endif // Metal version
#else
// MARK: - Swift side
#include <Metal/Metal.h>

struct ShaderMaterial {
  MTLResourceID textures[MaterialTextureCount];
  Material material;
};
fragment float4 fragment_IBL(
  VertexOut in [[stage_in]],
  constant Params &params [[buffer(ParamsBuffer)]],
  constant Light *lights [[buffer(LightBuffer)]],
  constant ShaderMaterial &shaderMaterial [[buffer(MaterialBuffer)]],
  depth2d<float> shadowTexture [[texture(ShadowTexture)]],
  texturecube<float> skybox [[texture(SkyboxTexture)]],
  texturecube<float> skyboxDiffuse [[texture(SkyboxDiffuseTexture)]],
  texture2d<float> brdfLut [[texture(BRDFLutTexture)]])
Material material = _material;
Material material = shaderMaterial.material;
auto textures = shaderMaterial.textures;
texture2d<float> baseColorTexture = textures[BaseColor];
texture2d<float> normalTexture = textures[NormalTexture];
texture2d<float> metallicTexture = textures[MetallicTexture];
texture2d<float> roughnessTexture = textures[RoughnessTexture];
texture2d<float> aoTexture = textures[AOTexture];
texture2d<float> opacityTexture = textures[OpacityTexture];

Creating the Argument Buffer

Now you’ll set up the argument buffer containing all the texture resource IDs in Swift.

var materialBuffer: MTLBuffer!
mutating func initializeMaterials() {
  // 1
  let materialBufferSize = MemoryLayout<ShaderMaterial>.stride
  materialBuffer = Renderer.device.makeBuffer(
    length: materialBufferSize)
  materialBuffer.label = "Material Buffer"
  // 2
  let textureIDs = allTextures.map { texture in
    texture?.gpuResourceID ?? MTLResourceID()
  }
  // 3
  let pointer = materialBuffer.contents()
    .assumingMemoryBound(to: ShaderMaterial.self)
  // 4
  pointer.pointee.material = material
  pointer.pointee.textures.0 = textureIDs[0]
  pointer.pointee.textures.1 = textureIDs[1]
  pointer.pointee.textures.2 = textureIDs[2]
  pointer.pointee.textures.3 = textureIDs[3]
  pointer.pointee.textures.4 = textureIDs[4]
  pointer.pointee.textures.5 = textureIDs[5]
}
initializeMaterials()
meshes[0].submeshes[0].initializeMaterials()

Updating the Draw Call

➤ In the Renderer folder, open Rendering.swift. This holds the extension on Model, where you render each model.

setMaterials(encoder: encoder, submesh: submesh)
encoder.setFragmentBuffer(
  submesh.materialBuffer,
  offset: 0,
  index: MaterialBuffer.index)
GPU errors
TFI objovh

GPU errors
GVO ahfity

submesh.allTextures.forEach { texture in
  if let texture {
    encoder.useResource(texture, usage: .read, stages: .fragment)
  }
}
Using argument buffers
Uyuqg uqkufedw xebmumx

Indirect Resources
Ifnizirh Civaixper

Ground plane textures
Tviefz lleka malrikut

Resource Heaps

You’ve grouped textures into an argument buffer for each submesh, but you can also combine all your app’s textures into a resource heap.

static var heap: MTLHeap?
static func buildHeap() -> MTLHeap? {
  let heapDescriptor = MTLHeapDescriptor()

  // add code here

  guard let heap =
    Renderer.device.makeHeap(descriptor: heapDescriptor)
    else { return nil }
  return heap
}
let descriptors = textures.map { texture in
  texture.descriptor
}
let sizeAndAligns = descriptors.map { descriptor in
  Renderer.device.heapTextureSizeAndAlign(descriptor: descriptor)
}
heapDescriptor.size = sizeAndAligns.reduce(0) { total, sizeAndAlign in
  let size = sizeAndAlign.size
  let align = sizeAndAlign.align
  return total + size - (size & (align - 1)) + align
}
if heapDescriptor.size == 0 {
  return nil
}
129 - (129 & (128 - 1)) + 128 = 256
let heapTextures = descriptors.map { descriptor -> MTLTexture in
  descriptor.storageMode = heapDescriptor.storageMode
  descriptor.cpuCacheMode = heapDescriptor.cpuCacheMode
  guard let texture = heap.makeTexture(descriptor: descriptor) else {
    fatalError("Failed to create heap textures")
  }
  return texture
}

The Blit Command Encoder

To blit means to copy from one part of memory to another and is typically an extremely fast operation. You create a blit command encoder using a command buffer, just as you did the render and compute command encoders. You then use this encoder when you want to copy a resource such as a texture or Metal buffer.

guard
  let commandBuffer = Renderer.commandQueue.makeCommandBuffer(),
  let blitEncoder = commandBuffer.makeBlitCommandEncoder()
else { return nil }
  zip(textures, heapTextures)
  .forEach { texture, heapTexture in
    heapTexture.label = texture.label
    // blit here
  }
var region =
  MTLRegionMake2D(0, 0, texture.width, texture.height)
for level in 0..<texture.mipmapLevelCount {
  for slice in 0..<texture.arrayLength {
    blitEncoder.copy(
      from: texture,
      sourceSlice: slice,
      sourceLevel: level,
      sourceOrigin: region.origin,
      sourceSize: region.size,
      to: heapTexture,
      destinationSlice: slice,
      destinationLevel: level,
      destinationOrigin: region.origin)
  }
  region.size.width /= 2
  region.size.height /= 2
}
blitEncoder.endEncoding()
commandBuffer.commit()
Self.textures = heapTextures
initializeMaterials()
meshes[0].submeshes[0].initializeMaterials()
func initialize(_ scene: GameScene) {
  TextureController.heap = TextureController.buildHeap()
  for model in scene.models {
    model.meshes = model.meshes.map { mesh in
      var mesh = mesh
      mesh.submeshes = mesh.submeshes.map { submesh in
        var submesh = submesh
        submesh.initializeMaterials()
        return submesh
      }
      return mesh
    }
  }
}
renderer.initialize(scene)
Skeletons on parade
Hfanomufc ax xamiqu

if let heap = TextureController.heap {
  renderEncoder.useHeap(heap, stages: .fragment)
}
submesh.allTextures.forEach { texture in
  if let texture = texture {
    encoder.useResource(texture, usage: .read, stages: .fragment)
  }
}
Rendering with a texture heap
Nunwudayf cayb a vownodu bueh

Residency Sets

So far, you’ve moved your texture binding process from each submesh to each draw call. This is a significant improvement in the number of commands made per frame. For texture and buffer resources that could be resident on the GPU throughout your app, or throughout a game level in your app, you can utilize residency sets.

let residencySet: MTLResidencySet
let setDescriptor = MTLResidencySetDescriptor()
setDescriptor.label = "Residency Set"
setDescriptor.initialCapacity = 1
residencySet = try! device.makeResidencySet(
  descriptor: setDescriptor)
residencySet.addAllocation(TextureController.heap!)
residencySet.commit()
Renderer.commandQueue.addResidencySet(residencySet)
if let heap = TextureController.heap {
  renderEncoder.useHeap(heap, stages: .fragment)
}

Key Points

  • An argument buffer is a collection of pointers to resources that you can pass to shaders.
  • A resource heap is a collection of textures or Metal buffers. A heap can be static, as in this chapter’s example, but you can also reuse space on the heap where you use different textures at different times.
  • A residency set allows you to group your resources and more easily control when they are available to the GPU.
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