Chapters

Hide chapters

Metal by Tutorials

Third Edition · macOS 12 · iOS 15 · Swift 5.5 · Xcode 13

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 reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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 shortly, 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 can now get started.

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

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

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

  • Shadows
  • The PBR forward renderer
  • Animation
  • Alpha testing
  • Textured models
  • Models with materials but no textures

There are a couple of added nifty features.

Firstly, in the Textures group, 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 exists by name already, TextureController adds the texture to the textures array, stores the array index and name into textureIndices 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 saves on duplication of textures, and stores all the app textures in one central array, making it easier to process into a heap later.

Secondly, when setting up character joint animation, you used function constants when you defined the pipeline state for the vertex shader. This project also uses function constants for defining the shadow pipeline state.

In the Render Passes group, 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 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 these incurs a renderEncoder.setFragmentTexture(texture:at:) command. Using argument buffers, you can group these six textures into one buffer, and set this buffer on the render command encoder with just one command. This argument buffer doesn’t only have to point to textures, it can point to any other data necessary to render the frame.

Creating the Shader Structure

➤ In the Shaders group, open PBR.metal.

struct ShaderMaterial {
  texture2d<float> baseColorTexture [[id(BaseColor)]];
  texture2d<float> normalTexture [[id(NormalTexture)]];
  texture2d<float> roughnessTexture [[id(RoughnessTexture)]];
  texture2d<float> metallicTexture [[id(MetallicTexture)]];
  texture2d<float> aoTexture [[id(AOTexture)]];
  texture2d<float> opacityTexture [[id(OpacityTexture)]];
  Material material [[id(OpacityTexture + 1)]];
};
#import "Material.h"
fragment float4 fragment_PBR(
  FragmentIn in [[stage_in]],
  constant Params &params [[buffer(ParamsBuffer)]],
  constant Light *lights [[buffer(LightBuffer)]],
  constant ShaderMaterial &shaderMaterial [[buffer(MaterialBuffer)]],
  depth2d<float> shadowTexture [[texture(ShadowTexture)]])
Material material = _material;
Material material = shaderMaterial.material;
texture2d<float> baseColorTexture = shaderMaterial.baseColorTexture;
texture2d<float> normalTexture = shaderMaterial.normalTexture;
texture2d<float> metallicTexture = shaderMaterial.metallicTexture;
texture2d<float> roughnessTexture = shaderMaterial.roughnessTexture;
texture2d<float> aoTexture = shaderMaterial.aoTexture;
texture2d<float> opacityTexture = shaderMaterial.opacityTexture;

Creating the Argument Buffer

To pass these textures, you create an argument buffer that matches the shader structure.

var materialsBuffer: MTLBuffer!
mutating func initializeMaterials() {
  guard let fragment =
    Renderer.library.makeFunction(name: "fragment_PBR") else {
      fatalError("Fragment function does not exist")
    }
  let materialEncoder = fragment.makeArgumentEncoder(
    bufferIndex: MaterialBuffer.index)
  materialsBuffer = Renderer.device.makeBuffer(
    length: materialEncoder.encodedLength,
    options: [])
}
// 1
materialEncoder.setArgumentBuffer(materialsBuffer, offset: 0)
// 2
let range = Range(BaseColor.index...OpacityTexture.index)
materialEncoder.setTextures(allTextures, range: range)
// 3
let index = OpacityTexture.index + 1
let address = materialEncoder.constantData(at: index)
address.copyMemory(
  from: &material,
  byteCount: MemoryLayout<Material>.stride)
initializeMaterials()

Updating the Draw Call

➤ In the Geometry group, open Model.swift.

updateFragmentMaterials(
  encoder: encoder,
  submesh: submesh)
encoder.setFragmentBuffer(
  submesh.materialsBuffer,
  offset: 0,
  index: MaterialBuffer.index)
submesh.allTextures.forEach { texture in
  if let texture = texture {
    encoder.useResource(texture, usage: .read)
  }
}

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()
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
Ytumakodt al bumaju

if let heap = TextureController.heap {
  renderEncoder.useHeap(heap)
}
submesh.allTextures.forEach { texture in
  if let texture = texture {
    encoder.useResource(texture, usage: .read)
  }
}

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.
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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now