Skip to content
macOSDeep Dive Published Updated 5 min readViews unavailable

Metal Heaps on macOS: Size Resources and Reuse Aliased Memory Safely

Use MTLHeap sizing and aliasing on macOS to reuse transient GPU memory safely, while keeping placement, synchronization, and capacity explicit.

MTLHeap is a pool from which a Metal app can allocate buffers, textures, and other supported resources. It gives the app more control over resource allocation and can reduce memory use when temporary resources are used in different stages rather than concurrently. The important boundary is that reusing backing memory is a correctness decision: Metal cannot infer that an earlier resource is no longer in use by work already submitted to the GPU.

Start with resource footprints and a workload budget

Ask the device for the size and alignment of the resource you intend to allocate. These values are a planning input, not a universal heap-size constant: texture format, dimensions, mip levels, usage, and device characteristics affect the footprint. For a single private scratch texture, a sizing probe looks like this:

guard let device = MTLCreateSystemDefaultDevice() else {
    fatalError("No Metal device is available")
}

let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(
    pixelFormat: .rgba8Unorm,
    width: 2048,
    height: 2048,
    mipmapped: false
)
textureDescriptor.storageMode = .private
textureDescriptor.usage = [.shaderRead, .shaderWrite]

let footprint = device.heapTextureSizeAndAlign(descriptor: textureDescriptor)
print("texture footprint: \(footprint.size) bytes; alignment: \(footprint.align)")

For multiple resources that must be live simultaneously, budget each allocation and its required alignment, then leave headroom for the actual workload. If resources are used in non-overlapping stages, aliasing may let a later allocation reuse a prior resource’s backing memory; do not count that saving until the GPU dependency is explicit. Heap and resource creation APIs return optional objects, so keep an allocation-failure path instead of treating a capacity estimate as a guarantee.

All resources suballocated from a heap share its storage mode and CPU cache mode. Separate heaps when workloads require different storage or cache behavior. On a Mac, do not hard-code a storage mode on the assumption that every GPU has the same memory architecture; consult Apple’s current resource-storage guidance and retain hardware-appropriate defaults unless the workload requires a specific mode.

Choose automatic placement before taking on manual placement

MTLHeapType.automatic is the default. Metal chooses where new resources go, and this is generally the simpler choice for short-lived resources. A placement heap gives the app control over resource placement and lifetime, which can be useful for long-lived allocations or a carefully designed allocator, but it introduces more bookkeeping. Do not mix the two models casually: makeAliasable() is valid only for heap resources allocated from an automatic heap.

A basic allocation path checks both heap creation and resource creation:

func makeScratchTexture(
    device: MTLDevice,
    descriptor: MTLTextureDescriptor,
    plannedHeapBytes: Int
) -> (heap: MTLHeap, texture: MTLTexture)? {
    let heapDescriptor = MTLHeapDescriptor()
    heapDescriptor.type = .automatic
    heapDescriptor.storageMode = descriptor.storageMode
    heapDescriptor.size = plannedHeapBytes // computed from the complete live-resource plan

    guard let heap = device.makeHeap(descriptor: heapDescriptor),
          let scratch = heap.makeTexture(descriptor: descriptor) else {
        return nil // choose a fallback or reduce the workload
    }
    return (heap, scratch)
}

plannedHeapBytes must describe the resources that may be live together, not just the first texture in the example. maxAvailableSize(alignment:) can help diagnose fragmentation when a later allocation fails, while usedSize and currentAllocatedSize answer different questions about heap use and backing allocation. Instrument these values at workload boundaries rather than logging every frame.

An aliasable resource is no longer yours to access

Resources begin non-aliasable. After a resource becomes aliasable, Metal may reuse its memory for a later allocation from the same automatic heap. The old object may still exist as an object, but reading or writing through that aliased resource is undefined and can corrupt data. Treat makeAliasable() as the end of the resource’s data lifetime; remove it from active render-graph state and never encode later work that accesses it.

The safe order is: submit the stage that uses a transient resource, establish that the GPU has finished that stage, mark the resource aliasable, and only then create or use a new resource that may occupy the same storage. A deliberately serialized diagnostic path can make that boundary visible:

commandBuffer.commit()
commandBuffer.waitUntilCompleted() // diagnostic serialization; avoid per-frame stalls
scratch.makeAliasable()

Waiting on the CPU makes the ordering easy to inspect but blocks progress and is usually unsuitable for a frame loop. For a production multi-stage graph, use the appropriate Metal fence or event dependency between GPU operations. Apple’s heap-and-fence sample demonstrates a dynamic texture pipeline where a fence protects data dependencies and aliasing reuses scratch memory. Merely ending an encoder or calling commit() does not mean the GPU has finished using the resource.

Keep hazards and ownership visible in the render graph

Automatic hazard tracking does not remove the need to order aliases. Metal’s memory-heaps guidance specifically calls out synchronization for resources within heaps. When two command queues or stages can overlap, describe the dependency with a fence or event that matches the queues involved. A resource can be reused only after every earlier reader and writer is complete - not just after the most obvious render pass.

Keep a live-range table for each transient resource: creation stage, last GPU read/write, dependency that proves completion, and the next allocation allowed to alias it. When that table is hard to maintain, allocate separate resources first and optimize only after measuring memory pressure. The easier-to-audit version is often the right baseline.

Validate allocation and reuse on target hardware

Exercise the worst-case set of simultaneously live textures and buffers, not only a small startup scene. Record resource descriptors, footprint estimates, heap size, usedSize, currentAllocatedSize, and allocation failures. Test a cold start, repeated scene transitions, cancellation while a command buffer is in flight, and a fallback path when a heap or resource allocation returns nil. Use Metal validation and GPU captures to look for commands that still reference an aliasable resource.

Heaps can lower allocation overhead and memory use, but neither benefit is automatic. A larger heap can increase pressure; excessive heap fragmentation can make a resource fail even when aggregate free bytes appear sufficient. Begin with explicit resource ownership, add aliasing only at verified synchronization points, and compare memory and frame-time measurements on the Mac GPU families your app supports.

Related:

Sources:

Comments