Last time we looked at the
Metal Shading Language basics. Before we look into more advanced topics, I just thought now is a good time to revisit what we have learned so far, especially about the graphics pipeline which I admit, I might have gone too fast on that topic (thank you, anonymous reader, for your suggestion and valuable feedback!)
Lets look at the graphics pipeline in more detail, and I’ll start with a piece of history here. It all started about a decade ago, when shaders were introduced as a way for programmers to be able to influence the fixed pipeline that existed until then. At the same time, floating point support was also introduced for
GPUs facilitating the birth of GPGPU (general-purpose computing on graphics processing units). As a consequence, the new programmable pipeline was significantly changed:
As you can see, the new
pipeline now has two
shader stages and that is where we can write our own customized code that the
GPU will then run. The first part of a graphics program always runs on the
CPU and is often called host code. Here is where most of the resource allocation happens, as well as staging the data transfer to and from the
GPU. The most important part of the program, however, runs on the
GPU. The two
shaders go into a separate file with a .metal extension (in other
GPGPU frameworks such as
OpenCL it is named kernel code).
The pipeline starts with the
CPU stage where the input is sent to the GPU in the form of vertices. They undergo transformation and per-vertex lighting. At this point the
vertex shader can be used to manipulate the vertices prior to
rasterization. After that, vertices undergo
rasterization resulting in fragments. The
fragment shader can then be run on each fragment before the pixel values are output to the
framebuffer for display.
Now let’s look at
Metal's own pipeline. We will refer back to the part 2 source code and we’ll mention line numbers to exemplify the concepts we touch. Building a
Metal application is done in two stages. The first one is the Initialization stage:
The very first step is getting the device (line 19 in
MetalView.swift). A device is the direct connection to the
GPU driver and hardware; it’s the source we need to create all the other objects in
Metal. The second initialization step is creating a command queue (line 40) which is our channel to submit work to the
GPU. The third initialization step is creating the buffers, textures and other resources (lines 20-27). The
newBufferWithBytes function will allocate a new block of shared memory, copy the provided pointer into it, and return a handle to that buffer. The fourth initialization step is creating the render pipeline (lines 28-37) which is a chain of steps that start with taking vertex data from one end and producing a rasterized image on the other end. The pipeline consists of two elements: the descriptor which holds the
shader information and the pixel format, and the state which is built from the
descriptor and contains the compiled
shaders. The fifth (last) initialization step is creating a view. For us it was easier to inherit from MTKView (line 11) rather than creating a new
CAMetalLayer and adding it as a subview.
Next, let’s look at the second stage of building a
Metal application, the Drawing stage:
The very first step is getting the command buffer (line 40). All of the work that goes to the
GPU will be enqueued into this buffer. We need the
command queue from the previous stage to have a
command buffer created. The second step is setting up a render pass (lines 38-39). A render pass descriptor tells
Metal what actions to take while an image is being rendered. Configuring it requires that we specify what color textures are we rendering to (the
currentDrawable texture). We need to also choose which color the screen will be cleared to before we draw any geometry. The third step is the actually drawing (lines 43-44). We specify the buffer where the vertices are stored and then the primitives we need drawn. The fourth and final step is to commit the command buffer (lines 46-47) to the
GPU. When calling
command buffer gets encoded, sent to the end of the command queue, and executed on the
GPU when its time comes.
I hope this part of the tutorial brought more clarity to understanding general topics such as the
graphics pipeline and the
Metal pipeline. I look forward to returning back to more coding in the next part of this tutorial.
Until next time!