Drawing a triangle with Metal 4
Render a colorful, rotating 2D triangle by running draw commands with a render pipeline on a GPU.
Overview
This sample demonstrates how to render imagery by sending commands to the GPU with the Metal 4 API, and relates to WWDC25 session 205: Discover Metal 4.
Multiple times a second, the sample’s app displays a colorful triangle by:
Updating the vertex data for the triangle
Encoding draw commands as a frame of visual content
Running the draw commands on a Metal device that represents an Apple silicon GPU
Updating the display after the GPU finishes rendering that frame
Apps can give a person the impression of motion by rendering and displaying frames at a sufficient frequency, typically at 60 frames or more per second.
The renderer encodes one frame at a time, and has three frames of content in flight at the same time. Starting when the first frame is visible on the display, the renderer is continually managing three frames at once:
The first frame is in its final lifetime phase as the frame that’s visible to a person on the device’s display.
The second frame is in its second lifetime phase where the GPU renders it in a render pass, which is the collection of render commands that draw the triangle.
The third frame is in its first lifetime phase where the renderer encodes the draw commands for the next render pass by using the Metal API on the CPU.
The renderer manages the frames as each progresses through its three lifetime phases. The diagram below illustrates how the first frames move through time, where each column represents a snapshot of the app’s current frames and their states:
[Image]
Create a renderer
The sample implements two separate renderer classes and the app creates a new instance of the one that’s appropriate for the system it’s running on. The two classes are:
Metal4Renderer, a renderer class that works with the Metal 4 APIMetalRenderer, a renderer class that works with previous Metal API versions
The app checks whether the system supports Metal 4 by calling supportsFamily(_:) in the MetalKitViewDelegate class.
The app creates a Metal 4 renderer if the operating system supports MTLGPUFamily.metal4; otherwise it creates an instance of the other renderer, which supports previous versions of Metal.
The two renderers are identical in their behavior, but they use different Metal API generations to submit the same render commands to the GPU.
Create long-term resources
The Metal 4 renderer’s initializer starts by creating an instance of MTL4CommandQueue, MTL4CommandBuffer, and MTLLibrary with the view’s MTLDevice.
Generally, you send work to the GPU by encoding commands into a command buffer, and then submitting one or more command buffers to a queue. Your app can have multiple command buffers and queues, but the sample’s Metal4Renderer class needs only one of each.
The initializer creates other resources the renderer needs by calling helper methods.
The renderer defines kMaxFramesInFlight near the top of its primary source file.
The sample applies this constant when it creates separate instances of the resources the renderer needs for each in-flight frame, which includes the buffers that store a triangle’s geometry and color information.
Most of the helper methods that create the renderer’s long-term resources at launch are relatively short. For example, the makeTriangleDataBuffers: method creates kMaxFramesInFlight instances of MTLBuffer because each in-flight frame needs a separate buffer to store its triangle vertex data.
Creating a separate buffer instance for each in-flight frame eliminates the possibility of modifying a buffer for a later frame before or as the GPU reads from the same buffer to render an earlier frame.
The makeArgumentTable method creates just a single argument table that the renderer can reuse each time it encodes render commands into a render pass the GPU eventually runs. You set the resource bindings for any pass you encode with an MTL4CommandBuffer, including compute and render passes, by configuring an MTL4ArgumentTable instance.
Each argument table can store bindings to instances of various resource types, including:
For this sample, the argument table only needs to store two buffer bindings, one for the buffer that stores vertex triangle data, and another buffer that stores the viewport’s width and height.
The makeResidencySet and makeCommandAllocators: methods create a single MTLResidencySet instance, and an MTL4CommandAllocator instance for each in-flight frame, respectively.
The end of the initializer configures the renderer’s initial state so that it’s ready to render the first frame when the system requests it.
The initializer adds two residency sets to the renderer’s command queue:
The long-term residency set, which the renderer configures to track all of its MTLBuffer instances
The view’s residency set, which MetalKit configures
See Simplifying GPU resource management with residency sets for more information about working with residency sets.
Create a render pipeline
The renderer’s compileRenderPipeline: method creates a render pipeline by configuring an MTL4RenderPipelineDescriptor instance and passing it to an MTL4Compiler instance’s newRenderPipelineStateWithDescriptor:compilerTaskOptions:error: method.
The renderer’s configureRenderPipeline: method sets the various properties the compiler needs to create a render pipeline state.
The makeVertexShaderConfiguration helper method creates an MTL4LibraryFunctionDescriptor instance that refers to the renderer’s vertex shader.
Similarly, the makeFragmentShaderConfiguration helper method creates another function descriptor instance that refers to the renderer’s fragment shader.
Draw a frame by encoding a render pass
The app is ready to render frames after its renderer creates and sets up all its resources at launch, including data buffers and a render pipeline state. Each time the system calls the app’s draw(in:) method, its MTKViewDelegate implementation calls the renderer’s renderFrameToView: method, which encodes and runs the commands that render the frame with the following steps:
Check that the MTKView parameter has valid currentDrawable and currentMTL4RenderPassDescriptor properties.
Increment the frame number, which tracks the resources it can reuse from previous frames that don’t need them any longer.
Prepare a command buffer.
Create and configure a render pass encoder.
Set the viewport to the size of the app’s view.
Configure the arguments for the render pass, which in this case are two data buffers.
Encode a draw command for the triangle.
Mark the end of the render pass and the command buffer that contains it.
Run the render pass by submitting the command buffer to the Metal device’s command queue, and display the result when it finishes.
Notify the renderer when it’s safe to reuse this frame’s resources for a new frame by signaling its shared event.
The remaining sections explain the important details of these steps.
Prepare a command buffer
The renderer uses the same MTL4CommandBuffer instance to render every frame. You can reuse a Metal 4 command buffer instance immediately after submitting it to an MTL4CommandQueue. This is because a command allocator stores a record of the command buffer’s contents when you submit it to a queue.
The renderer prepares the command buffer for a new set of commands by calling its beginCommandBuffer(allocator:) method.
The renderer reuses an MTL4CommandAllocator instance the GPU no longer needs by rotating through the kMaxFramesInFlight allocators it creates at launch.
The renderer ensures the next allocator in the rotation is available by calling the waitOnSharedEvent:forEarlierFrame: method. That method calls the wait(untilSignaledValue:timeoutMS:) method of the renderer’s MTLSharedEvent instance, which can potentially block the caller for 10 milliseconds before it returns.
The command queue updates the shared event after the Metal device finishes rendering the previous frame that uses the same allocator, which indicates to this method that it’s now available to reuse. Ideally, the shared event’s method returns immediately because the earlier frame using the allocator is done rendering and no longer needs it.
Create an encoder for a render pass
The renderFrameToView: method creates a render command encoder by retrieving an MTL4RenderPassDescriptor instance from the view’s currentMTL4RenderPassDescriptor property and passing it to the command buffer’s makeRenderCommandEncoder(descriptor:options:) method. The view’s property represents a valid configuration for a render pass to render a frame in a format that’s compatible with that view.
The command buffer’s factory method returns an MTL4RenderCommandEncoder instance, which provides methods that configure a render pass and encode the commands for that pass.
The method also gives the render encoder a unique name that can help you identify its render pass from other passes in Metal debugger. For more information about Metal debugger and inspecting passes, see:
Configure the viewport for the render pass
The renderer’s setViewport method configures an MTLViewport and passes it to the encoder’s setViewport(_:)method.
The method configures the viewport’s 2D size by setting the x and y members to the dimensions of the app’s view, in pixels.
Configure any arguments for the render pass
The renderer’s setRenderPassArguments: method configures two arguments for the render pass, both of which are MTLBuffer instances.
The method retrieves the next triangle vertex buffer in the rotation. Each render pass needs its own copy of triangle vertex data because the data for each frame is unique, and the GPU needs access to each frame’s input data until it finishes rendering that frame. The renderer tracks and rotates through kMaxFramesInFlight buffers of triangle vertex data in an array, similar to the command allocators because each frame has slightly different coordinates for the triangle as it rotates.
The method calls the renderer’s configureVertexDataForBuffer: method, which calculates the positions of the triangle’s vertices by applying a rotation angle and then copies the vertex data into the MTLBuffer.
Encode draw commands
The renderer draws exactly one triangle with a single call to drawPrimitives(primitiveType:vertexStart:vertexCount:):
This renderer only needs one draw command, but yours can encode multiple drawing commands in a single render pass.
End the render pass
The renderFrameToView: method marks the conclusion of the render pass by calling the encoder’s endEncoding() method.
It then marks the end of the command buffer by calling its endCommandBuffer() method because it only needs to encode a single render pass. However, your app can encode multiple passes of different types in a single command buffer with a series of encoder types, including the following:
Run the render pass by submitting the command buffer
The renderer sends the command buffer to run on the GPU in its submitCommandBufferForView: method. The method starts by retrieving the CAMetalDrawable instance the view stores in its currentDrawable property.
The method adds the following actions to the renderer’s MTL4CommandQueue instance, which run on the GPU timeline:
Wait for the view’s drawable with the waitForDrawable(_:) method.
Submit the command buffer to run on the GPU with the commit:count: method.
Notify the drawable that the GPU is finished running the render pass with the signalDrawable(_:) method.
The Metal device needs to wait until the view’s drawable is available because it stores the output from the render pass and provides the mechanism that updates the content on the display. When the drawable is ready, the GPU runs the single render pass in the command buffer, which saves the results to the drawable’s texture.
The method concludes by calling the drawable’s present() method, which instructs the drawable to show its content on the device’s display shortly after it gets the notification from the command queue. The MTLDrawable protocol defines this method, which the CAMetalDrawable protocol inherits.
Notify the renderer when a frame’s resources are ready for reuse
The last command the renderFrameToView: method adds to the command queue notifies the renderer when it can reuse this frame’s triangle vertex buffer and command allocator, by signaling its MTLSharedEvent instance with the current frame number.
For example, if the frameNumber equals 4 and kMaxFramesInFlight equals 3, this signal informs the renderer when its okay to reuse the fourth frame’s resources and apply them for frame seven.