Metal Binary Archives on macOS: Pipeline Compilation and Cache Strategy
Reduce Metal pipeline startup work with binary archives, compatible descriptors, measured fallbacks, and a safe cache lifecycle.
Creating a Metal pipeline state can involve more than allocating a small object. Metal combines shader functions with a pipeline descriptor and prepares a configuration the GPU can execute. When an application first creates a frequently used render or compute pipeline during an interactive frame, compilation or specialization work can show up as startup latency or a visible hitch.
MTLBinaryArchive is a container for pipeline descriptors and their associated compiled shader code. A pipeline descriptor can reference one or more archives so Metal can find previously compiled functions when creating a pipeline state. The feature is a way to capture and reuse pipeline work under documented compatibility rules, not a promise that every GPU can consume every archive or that pipeline creation becomes free.
Define the cache key before collecting archives
The archive is useful only when it corresponds to pipeline configurations the application actually creates. Record the complete descriptor state that affects the pipeline: shader functions, function constants, vertex layout, attachment formats, sample count, blending, rasterization, and relevant device or feature assumptions. A different descriptor is a different pipeline request even when the visible effect looks similar.
Do not let an arbitrary runtime key determine which archive is loaded. Treat the archive as a versioned build artifact associated with a known shader set and application release. If the shader source or pipeline schema changes, invalidate or replace the archive rather than assuming a stale file is safe. Keep an uncached runtime path because the archive may be missing, unreadable, incompatible, or simply lack a requested function.
Build and load pipeline states with a fallback
At runtime, create an archive using the same MTLDevice that creates the pipeline. Add descriptors for the functions the app wants to record. To consume it, assign the archive collection to the pipeline descriptor’s binaryArchives property and create the state through the device. If the descriptor has not been captured or the archive does not have a usable entry, normal pipeline creation remains necessary.
import Metal
func makeComputePipeline(
device: MTLDevice,
function: MTLFunction,
archive: MTLBinaryArchive?
) throws -> MTLComputePipelineState {
let descriptor = MTLComputePipelineDescriptor()
descriptor.computeFunction = function
if let archive {
descriptor.binaryArchives = [archive]
}
return try device.makeComputePipelineState(
descriptor: descriptor,
options: [],
reflection: nil
)
}
This function has a deliberately narrow signature: the caller owns the device, library, function, and archive lifetime. The exact synchronous or asynchronous pipeline-state creation overload should match the app’s SDK and latency budget. Keep shader compilation or disk I/O off a rendering deadline, and surface an actionable error if the fallback path itself fails.
For an archive build step, configure the same descriptor used for the real pipeline, pass it to addComputePipelineFunctions or the matching render-pipeline method, then serialize the archive to a controlled file location. Serialization is not a reason to block a frame. A shipped application should usually build and validate its intended archive set in a controlled pipeline or use Apple’s documented device-built archive workflow, instead of depending on one end user’s device to be the only source of compiled artifacts.
Device-built archives and distribution
Apple’s device-built archive workflow captures pipeline configurations from a running app and can use the metal-tt Metal translator as part of producing GPU-targeted binaries for distribution. The captured pipeline configuration script describes the states that the compiler needs to build. This helps a developer collect real-world pipeline usage and package compatible binaries for supported GPU families.
Collection is only as complete as the exercised workload. A rarely used renderer path, uncommon texture format, feature-constant combination, or optional visual effect can be absent if tests never instantiate it. Include representative UI states, export paths, and supported render modes in the capture run. Compare the resulting set with a registry of pipeline descriptors that the app expects, and report missing entries before shipping.
Do not assume that an archive made on one GPU is a universal binary. Follow Apple’s documented translator and supported-GPU workflow, and test the packaged result on the device families and OS versions the app supports. Feature support is capability-based and changes across Metal GPU families; query the actual device where runtime behavior depends on a capability. Avoid hard-coding a Mac model name or assuming that a successful build on a developer workstation proves the package is suitable for every target.
Cache integrity and lifecycle
Store archives under an application-controlled cache or bundle location with a deterministic schema version. If an archive is generated at runtime, write it atomically to a temporary file and only replace the current cache after serialization succeeds. Keep a pristine shipped archive or a regenerate path when deletion or corruption would otherwise make the renderer unusable. Record an opaque cache generation and a digest of shader inputs for support diagnostics; do not log private data.
An archive is a performance artifact, not the authority for shader source or user data. It can be removed and recreated. On read failure, record the error, fall back once to normal pipeline creation, and decide whether to rebuild in the background. Avoid tight rebuild loops when disk access or Metal compilation keeps failing. A version migration should never discard a working app state just because an optional acceleration cache is stale.
There are several distinct caches in this system. The app’s archive file persists pipeline data across launches; Metal may have internal caches; the operating system may cache shader or library work; and the renderer may retain pipeline-state objects in memory. Measuring only one layer can lead to a false conclusion. Compare cold first launch, subsequent launch, and a cache-deleted run with identical inputs. Measure pipeline-state creation latency and first-frame latency separately.
Concurrency and frame scheduling
Prepare frequently used pipeline states before a latency-sensitive draw if the application can predict them. Cache state objects by a complete immutable pipeline key and share them according to Metal’s API ownership model. Do not create a fresh pipeline per frame. If pipeline generation is asynchronous, publish the result only if the requested renderer generation is still current; a window can close or the selected rendering mode can change while compilation runs.
Do not serialize archives while a frame is waiting for GPU submission. Build or update them on a background task with explicit cancellation and atomic file replacement. If archive generation increases memory or GPU work, limit its scope and schedule it when the application can tolerate the cost. The archive doesn’t remove the need to synchronize resources or observe command-buffer completion; pipeline creation and GPU command submission are separate lifecycle concerns.
Failure modes and diagnostics
A cache miss is not inherently an error. It means Metal did not use a precompiled function for that request, so measure whether the runtime compile cost matters. An archive load error, unsupported GPU family, descriptor mismatch, or missing shader library can require different actions. Report these as structured states rather than collapsing everything into “Metal unavailable.”
Attach labels to pipeline descriptors and archives where supported. During development, log a stable pipeline key, archive generation, device registry identifier where permitted, state-creation duration, and whether the archive path was enabled. Never infer that an archive hit occurred solely because the pipeline state creation call succeeded; use available Metal diagnostics and controlled cold/warm experiments.
Test every expected pipeline on clean machines, with no generated cache, with an old cache, and with a deliberately missing archive. Verify that the unarchived fallback renders correct output. Test archive update interruption and ensure a partial file is not mistaken for a valid one. Confirm that the graphics output is the same for cached and uncached pipeline creation within the product’s pixel or tolerance contract.
Acceptance criteria
Define a startup budget for state creation and a frame budget for the first rendered interaction. In a representative capture, every expected descriptor should be accounted for. On the supported test matrix, compare median and tail latency across cold and warm launches, record fallback frequency, and assert that an archive failure does not block or corrupt rendering. Re-run after shader, descriptor, SDK, or target-device changes.
Binary archives can move repeated pipeline compilation work earlier in the development or launch lifecycle. They cannot guarantee a cache hit for an unrecorded descriptor, eliminate all driver work, or replace runtime capability checks. Treat the archive as an optional, measurable optimization attached to a correctly versioned pipeline catalog.
Related:
- Metal on macOS: Command Buffers, Resource Hazards, and GPU Completion
- Core Animation on macOS: Transactions, Model State, and Presentation
Sources: