Welcome to another entry about Metal support in Crimild. I’m really amazed by the fact that I managed to write several posts in a row in just a couple of weeks. Hopefully, I can keep up with the rest. Because I’m not done yet.
Let’s recap what we discussed so far:
In the first post, the basics concepts for Metal were introduced as well as the reasons for Crimild to support it.
In Part 1 we talked about what needs to be performed during the initialization and the rendering phase, introducing synchronization along the way.
In Part 2 we went deep into the geometry pass and how to describe render pipelines for our visible objects.
In Part 3 we showed the power of the Metal Shading Language and how shaders are written.
Now it’s time to address the step that’s still missing in our rendering process: how to actually send render commands to the GPU using encoders. In addition, I’m going to briefly introduce framebuffers in Metal and how they are handled during the render pass, although I’ll leave the post-processing pass and image effects details for a future post.
This post is supposed to tie up all loose ends in our previous entries, so let’s start…
We mentioned encoders several times before in previous posts, but we’ve never defined what they are. Command encoders are used to write commands and states into a single command buffer in a way that can be executed by the GPU.
Metal provides three different kind of command encoders: Render, Compute and Blit. It’s important to note that while we can interleave encoders so they write into the same command buffer, only one of them can be active at any point in time.
Creating Render Encoders
At the moment, only render encoders are supported by Crimild, defined by the MTLRenderCommandEncoder protocol, and they are created whenever framebuffers are bound during the render process.
Since encoders write into specific buffers, you create new ones by requesting a new instance from the MTLCommandBuffer itself:
auto renderEncoder = [getRenderer()->getCommandBuffer() renderCommandEncoderWithDescriptor: renderPassDescriptor];
For render encoders, we need to describe them in terms of a rendering pass which are objects describing rendering states and commands. The MTLRenderPassDescriptor class defines the attachments that serve as the rendering destination for commands in a command buffer. We may have up to four color attachments, but only up to one for depth and another for stencil operations.
A render pass that will draw to the default drawable (i.e., the screen) is typically described as follows:
auto renderPassDescriptor = [MTLRenderPassDescriptor new];
renderPassDescriptor.colorAttachments[ 0 ].loadAction = MTLLoadActionClear;
const RGBAColorf &clearColor = fbo->getClearColor();
renderPassDescriptor.colorAttachments[ 0 ].clearColor = MTLClearColorMake( clearColor[ 0 ], clearColor[ 1 ], clearColor[ 2 ], clearColor[ 3 ] );
renderPassDescriptor.colorAttachments[ 0 ].storeAction = MTLStoreActionStore;
renderPassDescriptor.colorAttachments[ 0 ].texture = getRenderer()->getDrawable().texture;
The code above describes a render pass that will clear the color attachment and store the results of the rendering process into the default drawable’s texture provided by the renderer.
Alternatively, you can set a different texture as the attachment’s target if you need to perform offscreen rendering, as we will see when I show you how to do post-processing effects in later posts.
In Crimild, render passes and encoders are linked with instances of crimild::FrameBufferObject, which seemed like the natural choice for me, and the related crimild::Catalog implementation takes care of creating and using them.
Specifying resources for a render command encoder
When drawing geometry, we need to specify which resources are bound with the vertex and/or the fragment shader functions. A render command provides methods to assign resources (as in buffers, textures and samplers) to the corresponding argument table as we saw in the last post.
[getRenderEncoder() setVertexBuffer: uniforms offset: 0 atIndex: 1];
[getRenderEncoder() setFragmentBuffer: uniforms offset: 0 atIndex: 1];
[getRenderEncoder() setVertexBuffer: vertexArray offset: 0 atIndex: 0];
In Crimild, resources are set to the render encoder at different points in the render process by different entities. Data buffers, textures and samplers are usually handled by catalogs while uniform and constant buffers are handled by the MetalRenderer itself.
Specifying the render pipeline
We also need to associate a compiled render pipeline state to our encoder for use in rendering:
[getRenderEncoder() setRenderPipelineState: renderPipeline];
The Draw Call
Everything’s set. It’s time to execute the actual draw call.
Metal provides several draw methods depending on the primitives you want to render. Crimild uses indexed primitives by default, so the corresponding method is invoked in this step:
[getRenderEncoder() drawIndexedPrimitives: MTLPrimitiveTypeTriangle
The first argument determines which type of primitive we are going to draw. In this case, we will draw indexed triangles and we specify the index buffer to interpret the vertices, which were passed to the render encoder before the call to this function.
Ending the Rendering Pass
And then we reach the final point in the render process. To terminate a rendering pass, we invoke the endEncoding method on the active render encoder. Once the encoding has finished, you can start a new one on the same buffer if needed.
Crimild automatically invokes the endCoding method when unbinding framebuffers, ensuring that all render commands have been set at that point.
Once all command encoders have been described, our command buffer is committed and the drawable will be presented to the screen, as we saw in Part 1.
Side effects? What side effects?
If you’re familiar with Crimild you might have notice a little side effect (actually, a constraint) when working with the Metal-based renderer. Basically, it’s enforcing the use of framebuffers, meaning that it will only work with the forward render pass approach (or anything more complex than that). It wasn’t to be like that when I started. The original goal was to support every kind of render pass, regardless of whether or not it required offscreen rendering. In the end, having at least one offscreen framebuffer is the most natural way of with Metal. At least for Crimild. So, no Metal for you unless you’re willing to pay the price.
On the plus side, working with a deferred render approach seems a lot easier now. I don’t have anything productive yet regarding such a technique (at least not in Metal), but it’s something that I want to do in the near future since it will bring a lot of benefits.
Wait, there’s more…
As I said at the beginning of this article, I’m not done with this series yet. At this point we will be able to render some objects to the screen but, if we only follow the steps discussed so far, the result will be a bit disappointing:
Where are the textures and labels? Where’s the post processing effect? There are no menus either. You’re right. There are lot of things that are yet to be discovered.
In the next post, we’re going to see how to handle textures and lighting in Metal, as well as describing alpha testing and other state changes.
To be continued…