If you have clicked on this article I'm assuming you know what a GPU is and since it’s Apple, a bit of Swift too.
Apple made Metal to give developers low-level access to the GPUs on iPhones and Mac for rendering 3D graphics and do high-performance computing. This is an official replacement for OpenGL and can be thought of as an analogue to CUDA. Finally, Metal is highly optimised to work with Apple’s hardware architecture so you can get every last bit of juice from the device.
Why would you want to use Metal?
If you are looking to build a custom 3D rendering pipeline (say ray tracing) or your app has to perform computations on huge data (matrices) locally on the device or requires real-time inference of a neural network. These are some exciting possibilities.
Understanding Metal with a mental model
Imagine a factory. There is a worker who loads raw material into a container. This container is then placed on a manufacturing line. Once the container reaches the machine it is operated upon using a tool selected from an available set of tools.
Basic Metal classes and their factory model counterparts
- MTLDevice | Machine
- MTLLibrary | Available set of tools
- MTLCommandQueue | Manufacturing Line
- MTLCommandBuffer | Container
- MTLCommandEncoder | Worker
- MTLComputePipelineState | Configuration of Machine
- Metal Shader (not a class)| Tool.
MTLDevice
This is the GPU hardware represented as an object to let developers communicate with it.
// instantiate an MTLDevice
// expensive call do it once at the beginninglet metalDevice = MTLCreateSystemDefaultDevice()
MTLLibrary
The MTLLibrary is an object that contains all the Metal shaders compiled into a single library. We’ll use this to fetch our required shader functions to define a Pipeline state.
// creates the libraryguard let library = metalDevice?.makeDefaultLibrary()
else { return }// fetches the metal shader named sepiaFilterlet shader = library.makeFunction(name: "sepiaFilter")
Metal Shaders
Metal Shader is not a Metal defined class. It’s a .metal file that has code written in Metal Shading Language. This is the function that the GPU actually executes on each data point or for each pixel to be rendered. People from graphics may know it as vertex or fragment shaders. People from CUDA may know it as kernel functions. These are compiled at build time and are obtained by querying the MTLLibrary.
MTLCommandQueue
The Command Queue contains an ordered list of workload scheduled for the GPU to work upon. Workload i.e. data in a form that GPU can understand is added to the queue in the form of Command Buffers.
// creates the command queue
// also an expensive callguard let commandQueue = metalDevice?.makeCommandQueue()
else { return }
MTLCommandBuffer
This is the container that contains data for GPU computation. Data is encoded (so that the GPU can read them) and added to Command Buffers by Command Encoders. Buffers can be enqueued (append) or committed (append and execute asap) to the Command Queue. The GPU works on each buffer in the same order it was enqueued, i.e FIFO.
// creates the command bufferguard let commandBuffer = commandQueue.makeCommandBuffer()
else { return }
MTLCommandEncoder
Command Encoder put data into Command Buffers in the form of buffers (general purpose data) and textures (images) so that GPU can read from and write to them. It is also used to set the Pipeline state which states what the GPU is supposed to do with the data provided. Depending on whether its rendering or compute it has a few different capabilities and responsibilities.
// creates a compute command encoder
// rendering and blit purpose encoders also existguard let commandEncoder = commandBuffer.makeComputeCommandEncoder() else { return }// setting up texture data to add to the command buffercommandEncoder.setTexture(inputTexture, index: 0)
commandEncoder.setTexture(outputTexture, index: 1)// setting up pipleline statecommandEncoder.setComputePipelineState(computePipelineState!)// end it so other encoders can be used
// encoders are cheap to set up, so no worriescommandEncoder.endEncoding()
MTLComputePipelineState
The Pipeline state tells what the GPU will be doing with the workload it’s provided with by the command queue. Alternatives to ComputePipeline are RenderPipeline, BlitPipeline used for rendering and data management operations.
// fetches the metal shader named sepiaFilterguard let shader = library.makeFunction(name: "sepiaFilter")
else { return }// creates a compute Pipeline state with the shader function we just createdguard let computePipelineState = try? metalDevice?.makeComputePipelineState(function: shader)
else { return }
A small twist
Unlike regular GPU architecture, there is no data being sent from CPU to GPU on iOS. They reside together and therefore have access to the same data thus eliminating communication overhead. Then what do Command Queues send, if not actual data? There comes the argument table. It is something similar to a page table in an OS or simply a look-up table if you will. It contains buffers and textures ordered by indices. So when GPU requests for texture at index 0 it’s allowed to look at data from main memory directly that the texture is supposedly holding but in reality, is just referring to.
Hope this made vague sense. Try re-reading it with the factory model to get a better understanding of it. In the next article, we’ll see the actual code to do image processing using Metal. Also please let me know if I have made any errors.
Reference