2025 Graphics & Games
WWDC25 · 32 min · Graphics & Games
Explore Metal 4 games
Learn to optimize your game engine with the latest advancements in Metal 4. We’ll cover how to unify your command encoding to minimize CPU overhead, scale up your graphics resource management to support massive scenes and maximize your memory budget, and load large libraries of pipeline states quickly. To get the most out of this session, first watch “Discover Metal 4.”
Watch at developer.apple.com ↗Chapters
Code shown on screen · 17 snippets
Synchronize access to a buffer within an encoder
// Synchronize access to a buffer within an encoder
id<MTL4ComputeCommandEncoder> encoder = [commandBuffer computeCommandEncoder];
[encoder copyFromBuffer:src sourceOffset:0 toBuffer:buffer1 destinationOffset:0 size:64];
[encoder barrierAfterEncoderStages:MTLStageBlit
beforeEncoderStages:MTLStageDispatch
visibilityOptions:MTL4VisibilityOptionDevice];
[encoder setComputePipelineState:pso];
[argTable setAddress:buffer1.gpuAddress atIndex:0];
[encoder setArgumentTable:argTable];
[encoder dispatchThreads:threadsPerGrid threadsPerThreadgroup:threadsPerThreadgroup];
[encoder endEncoding];code snippet. Configure superset of color attachments
// Configure superset of color attachments
MTL4RenderPassDescriptor *desc = [MTLRenderPassDescriptor renderPassDescriptor];
desc.supportColorAttachmentMapping = YES;
desc.colorAttachments[0].texture = colortex0;
desc.colorAttachments[1].texture = colortex1;
desc.colorAttachments[2].texture = colortex2;
desc.colorAttachments[3].texture = colortex3;
desc.colorAttachments[4].texture = colortex4; Set color attachment map entries
// Set color attachment map entries
MTLLogicalToPhysicalColorAttachmentMap* myAttachmentRemap = [MTLLogicalToPhysicalColorAttachmentMap new];
[myAttachmentRemap setPhysicalIndex:0 forLogicalIndex:0];
[myAttachmentRemap setPhysicalIndex:3 forLogicalIndex:1];
[myAttachmentRemap setPhysicalIndex:4 forLogicalIndex:2]; Set a color attachment map per pipeline
// Set a color attachment map per pipeline
[renderEncoder setRenderPipelineState:myPipeline];
[renderEncoder setColorAttachmentMap:myAttachmentRemap];
// Draw with myPipeline
[renderEncoder setRenderPipelineState:myPipeline2];
[renderEncoder setColorAttachmentMap:myAttachmentRemap2];
// Draw with myPipeline2 Encode a single render pass with 3 render encoders
// Encode a single render pass with 3 render encoders with suspend/resume options
id<MTL4RenderCommandEncoder> enc0 = [cmdbuf0 renderCommandEncoderWithDescriptor:desc options:MTL4RenderEncoderOptionSuspending];
id<MTL4RenderCommandEncoder> enc1 = [cmdbuf1 renderCommandEncoderWithDescriptor:desc options:MTL4RenderEncoderOptionResuming | MTL4RenderEncoderOptionSuspending];
id<MTL4RenderCommandEncoder> enc2 = [cmdbuf2 renderCommandEncoderWithDescriptor:desc options:MTL4RenderEncoderOptionResuming];
id<MTL4CommandBuffer> cmdbufs[] = { cmdbuf0, cmdbuf1, cmdbuf2 };
[commandQueue commit:cmdbufs count:3] Synchronize drawable contents
// Synchronize drawable contents
id<MTLDrawable> drawable = [metalLayer nextDrawable];
[queue waitForDrawable:drawable];
// ... encode render commands to commandBuffer ...
[queue commit:&commandBuffer count:1];
[queue signalDrawable:drawable];
[drawable present]; Encode a queue barrier to synchronize data
// Encode a queue barrier to synchronize data
id<MTL4ComputeCommandEncoder> compute = [commandBuffer computeCommandEncoder];
[compute dispatchThreadgroups:threadGrid threadsPerThreadgroup:threadsPerThreadgroup];
[compute endEncoding];
id<MTL4RenderCommandEncoder> render = [commandBuffer renderCommandEncoderWithDescriptor:des];
[render barrierAfterQueueStages:MTLStageDispatch
beforeStages:MTLStageFragment
visibilityOptions:MTL4VisibilityOptionDevice];
[renderCommandEncoder drawPrimitives:MTLPrimitiveTypeTriangle
vertexStart:vertexStart
vertexCount:vertexCount];
[render endEncoding]; Create a texture view pool
// Create a texture view pool
MTLResourceViewPoolDescriptor *desc = [[MTLResourceViewPoolDescriptor alloc] init];
desc.resourceCount = 500;
id <MTLTextureViewPool> myTextureViewPool =
[myDevice newTextureViewPoolWithDescriptor:myTextureViewPoolDescriptor
error:nullptr]; Set a texture view
// Set a texture view
MTLResourceID myTextureView = [myTextureViewPool setTextureView:myTexture
descriptor:myTextureViewDescriptor
atIndex:5];
[myArgumentTable setTexture:myTextureView
atIndex:0]; Choose appropriate sparse page size
MTLHeapDescriptor *desc = [MTLHeapDescriptor new];
desc.type = MTLHeapTypePlacement;
desc.storageMode = MTLStorageModePrivate;
desc.maxCompatiblePlacementSparsePageSize = MTLSparsePageSize64;
desc.size = alignedHeapSize;
id<MTLHeap> heap = [device newHeapWithDescriptor:desc]; Update buffer mappings
// Update buffer mappings
MTL4UpdateSparseBufferMappingOperation bufferOperation;
bufferOperation.mode = MTLSparseTextureMappingModeMap;
bufferOperation.bufferRange.location = bufferOffsetInTiles;
bufferOperation.bufferRange.length = length;
bufferOperation.heapOffset = heapOffsetInTiles;
[cmdQueue updateBufferMappings:myBuf heap:myHeap operations:&bufferOperation count:1]; Set unspecialized configuration
// In MTL4RenderPipelineColorAttachmentDescriptor
// Set unspecialized configuration
pipelineDescriptor.colorAttachments[i].pixelFormat = MTLPixelFormatUnspecialized;
pipelineDescriptor.colorAttachments[i].writeMask = MTLColorWriteMaskUnspecialized;
pipelineDescriptor.colorAttachments[i].blendingState = MTL4BlendStateUnspecialized; Create a specialized transparent pipeline
// Create a specialized transparent pipeline
// Set the previously unspecialized properties
pipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
pipelineDescriptor.colorAttachments[0].writeMask =
MTLColorWriteMaskRed | MTLColorWriteMaskGreen | MTLColorWriteMaskBlue;
pipelineDescriptor.colorAttachments[0].blendingState = MTL4BlendStateEnabled;
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne;
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor =
MTLBlendFactorOneMinusSourceAlpha;
pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd;
id<MTLRenderPipelineState> transparentPipeline =
[compiler newRenderPipelineStateBySpecializationWithDescriptor:pipelineDescriptor
pipeline:unspecializedPipeline
error:&error];
// Similarly, create the specialized opaque and hologram pipelines Determine thread count
// Determine thread count
NSInteger numThreads = 2;
if (@available(macOS 13.3, iOS 19, visionOS 3, tvOS 19, *))
{
numThreads = [device maximumConcurrentCompilationTaskCount];
} Set a proper QoS class for your compilation threads
// Create thread pool
for (NSInteger i = 0; i < numThreads; ++i)
{
// Creating a thread with a QoS class DEFAULT
pthread_attr_set_qos_class_np(&attr, QOS_CLASS_DEFAULT, 0) ;
pthread_create(&threadIds[i], &attr, entryPoint, NULL);
pthread_attr_destroy(&attr);
} Harvest pipeline configuration scripts
// Harvest pipeline configuration scripts with the pipeline data set serializer
// Create a pipeline data set serializer that only captures descriptors
MTL4PipelineDataSetSerializerDescriptor *desc = [MTL4PipelineDataSetSerializerDescriptor new];
desc.configuration = MTL4PipelineDataSetSerializerConfigurationCaptureDescriptors;
id<MTL4PipelineDataSetSerializer> serializer =
[device newPipelineDataSetSerializerWithDescriptor:desc];
// Set the pipeline data set serializer when creating the compiler
MTL4CompilerDescriptor *compilerDesc = [MTL4CompilerDescriptor new];
[compilerDesc setPipelineDataSetSerializer:serializer];
id<MTL4Compiler> compiler = [device newCompilerWithDescriptor:compilerDesc error:nil];
// Create pipelines using the compiler as usual
// Serialize the descriptors as a pipeline script
NSData *data = [serializer serializeAsPipelinesScriptWithError:&err];
// Write the pipeline script data to disk
NSString *path = [NSString pathWithComponents:@[folder, @"pipelines.mtl4-json"]];
BOOL success = [data writeToFile:path options:NSDataWritingAtomic error:&err]; Query pipeline state from MTLArchive
// Query pipeline state from MTLArchive
id<MTL4Archive> archive = [device newArchiveWithURL:archiveURL error:&error];
id<MTLRenderPipelineState> pipeline =
[archive newRenderPipelineStateWithDescriptor:descriptor error:&error];
if (pipeline == nil)
{
// handle lookup miss
pipeline = [compiler newRenderPipelineStateWithDescriptor:descriptor
compilerTaskOptions:nil
} Resources
- Metal binary archives
- Reading and writing to sparse textures
- Synchronizing passes with producer barriers
- Synchronizing passes with consumer barriers
- Synchronizing passes with a fence
- Synchronizing stages within a pass
- Resource synchronization
- Drawing a triangle with Metal 4
- Using the Metal 4 compilation API
- Understanding the Metal 4 core API
- Human Interface Guidelines: Designing for games
Related sessions
-
24 min -
27 min -
25 min -
23 min -
30 min -
30 min