Praise the Metal – Part 2: The Geometry Pass

Ok, time to do something fun. Now that we know how to do the basics for a single frame, let’s dive into how objects get drawn.

I’m assuming you are familiar with the crimild::Geometry class in Crimild, but I guess a refreshment won’t hurt:

crimild_geometry

Each crimild::Geometry instance should contain at least one primitive, which in turn store all the vertex data and how it’s supposed to be represented (triangles, lines, etc). On the other hand, each geometry is associated with a crimild::Material instance describing the way they are rendered (through shaders). Finally, the crimild::Geometry class extends from crimild::Node, therefore inheriting the world transformation.

Rendering Geometries

In Crimild, there are several steps required in order to render some primitives on the screen. Let’s summarize them as follows:

  1. Acquire the geometry’s associated material
  2. Enable the corresponding shaders for that material
  3. Enable lights
  4. Enable textures
  5. Enable the vertex data for the gometry
  6. Set shader uniforms, like transformations and other constants
  7. Perform the draw call

Please note that the list above does not consider grouping geometries by materials or the concept of instancing for vertex data. And I’m leaving anything related with the shadow pass and skinning out as well. Sorry, but I didn’t want to complicate things too much for this post. You can check any of the crimild::RenderPass implementations to verify what an actual geometry pass looks like.

Anyway, if we were using an OpenGL-based rendered, all of the steps mentioned above would be split into a series of state changes. Fortunately for us, Metal groups most of those state changes into as few as possible, thanks to precompiled pipelines.

Pipelines

So, what are pipelines? I mentioned them before in my previous posts, but now the time has come to see them in action.

A single pipeline contains the rendering configuration used during the geometry pass. In Metal, we use the MTLRenderPipelineDescriptor to specify vertex layout, shaders, rasterizer options (such as multisampling), blending and framebuffer attachments (as in color, depth or stencil attachments).

Describing pipelines

Crimild creates new pipelines and linked them with instances of the crimild::ShaderProgram class. This is performed by the crimild::metal::ShaderProgramCatalog class.

To be honest, at the time of this writing I’m still not completely comfortable with this design choice, since I’m starting to believe that it makes more sense for pipelines to be linked with instances of crimild::Material instead. I’m still struggling with that idea and I’ll definitely revisit it in the future.

Here’s an example of how Metal’s pipelines are described within Crimild:

// 1
NSString *vertexProgramName = [NSString stringWithUTF8String: program->getVertexShader()->getSource().c_str()];
id <MTLFunction> vertexProgram = [getDefaultLibrary() newFunctionWithName: vertexProgramName];
    
// 2
NSString *fragmentProgramName = [NSString stringWithUTF8String: program->getFragmentShader()->getSource().c_str()];
id <MTLFunction> fragmentProgram = [getDefaultLibrary() newFunctionWithName: fragmentProgramName];
    
MTLRenderPipelineDescriptor *desc = [MTLRenderPipelineDescriptor new];
desc.sampleCount = 1;
desc.vertexFunction = vertexProgram; // 1
desc.fragmentFunction = fragmentProgram; // 2
desc.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;

This code describes a simple pipeline, asuming a forward render pass (I haven’t started working on deferred rendering yet). Notice that the first few lines get the precompiled shader functions and set those to the pipeline. For simplicity, I’m not showing error handling or alternative flows.

There are many more things that can be described per pipeline, and we’ll see some of them below.

Once described, the pipeline need to be compiled and the result is stored into a MTLRenderPipelineState object:

id <MTLRenderPipelineState> renderPipeline = [getRenderer()->getDevice() newRenderPipelineStateWithDescriptor: desc error: &error];

Using pipelines

In run-time, switching pipelines is a one-liner:

[getRenderEncoder() setRenderPipelineState: renderPipeline];

This single line of code will apply all of the rendering settings defined for that particular pipeline. Pretty neat, uh?

I haven’t talk about render encoders yet. I’m leaving that for a future post. For the moment, just think about them as the ones that will translate our pipelines into actual render commands.

Vertex data

Before we move on, please note that Crimild favors interleaved float-arrays for vertices and indexed primitives, so I’m mostly going to work with that kind of data for the rest of the posts in this series. Do keep in mind that Metal, like OpenGL, provides many more ways to layout and use our vertex data.

Vertex buffer layout

As I said before, Crimild expects vertex data to be interleaved. That is, all data is stored in a single float array, one vertex after the other. Now, each vertex may contain several components like positions, normals, texture coordinates, weights, and more. For example, a simple triangle containing 3 floats for positions, 3 floats for normals and 2 floats for texture coordinates will be represented in memory like this:

float data[] = {
   /* position */        /* normals */      /* uv */
   -1.0f, -1.0f, 0.0f,   0.0f, 0.0f, 1.0f,  0.0f, 1.0f,
   1.0f, -1.0f, 0.0f,    0.0f, 0.0f, 1.0f,  1.0f, 1.0f,
   0.0f, 1.0f, 0.0f,     0.0f, 0.0f, 1.0f,  0.5f, 0.0f
};

In order to specify the vertex format that we’re going to use, Metal does so as part of the pipeline description step by employing the MTLVertexDescriptor class. For example, the following code will describe the vertex layout that we need for the triangle above:

MTLRenderPipelineDescriptor *desc = [MTLRenderPipelineDescriptor new];
    
MTLVertexDescriptor* vertexDesc = [[MTLVertexDescriptor alloc] init];
vertexDesc.attributes[0].format = MTLVertexFormatFloat3;
vertexDesc.attributes[0].bufferIndex = 0;
vertexDesc.attributes[0].offset = VertexFormat::VF_P3_N3_UV2.getPositionsOffset() * sizeof( float );;
vertexDesc.attributes[1].format = MTLVertexFormatFloat3;
vertexDesc.attributes[1].bufferIndex = 0;
vertexDesc.attributes[1].offset = VertexFormat::VF_P3_N3_UV2.getNormalsOffset() * sizeof( float );
vertexDesc.attributes[2].format = MTLVertexFormatFloat2;
vertexDesc.attributes[2].bufferIndex = 0;
vertexDesc.attributes[2].offset = VertexFormat::VF_P3_N3_UV2.getTextureCoordsOffset() * sizeof( float );
vertexDesc.layouts[0].stride = VertexFormat::VF_P3_N3_UV2.getVertexSize() * sizeof( float );
vertexDesc.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;

desc.vertexDescriptor = vertexDesc;

Since we’re talking about raw memory here, we need to make sure that our offsets and sizes take the size of our data (in this case, float) into consideration.

Bare in mind that since pipelines are pre-compiled, we shouldn’t change the vertex format once set. Otherwise, we will be forced to compile the pipelines again, loosing all benefits along the way.

In contrast, in OpenGL it doesn’t really matters if we change the vertex layout since we will be resetting it on every draw call (I think VAOs are supposed to fix that, though). Honestly, I haven’t seen a use case when you need to do this on run-time, but both APIs allow it.

Loading vertex data

Time to talk about one of the greatest features of the Metal API: buffers. In Metal, we don’t have separated buffers for each data type as in OpenGL. Instead, there’s only one implementation. The MTLBuffer protocol is used to store unformatted data that can later be used either for vertices, shader constants, textures or any other use of raw memory that you can imagine. And, most importantly, buffers can be shared between the CPU and the GPU, which means we don’t need to upload (or download) data from one another anymore.

And, of course, they’re really simple to use too:

crimild::VertexBufferObject *vbo = /* our VBO */
id < MTLBuffer > vertexArray = 
   [getDevice() newBufferWithBytes: vbo->getData()
                            length: vbo->getSizeInBytes()
                           options: MTLResourceOptionCPUCacheModeDefault];

In the newest versions of Metal, the last argument let us specify the strategy for the buffer storage. We can request shared, private or managed memory at this point. Crimild uses only shared memory at the moment and I still need to see that other two options in action to understand how to support them.

Using vertex data

Vertex data is generated at load time, usually at the beginning of our program. In order to use a vertex buffer in run-time, we need to do so in a similar way as for our pipelines:

[getRenderEncoder() setVertexBuffer: vertexArray]
                             offset: 0
                            atIndex: 0];

The last arguments, offset and index, are used to identify them in the shaders themselves. MLSL will be discussed in the next post, we can assume that those fields could be related with how attribute and uniform locations work in OpenGL.

Indexed primitives

Metal supports indexed primitives which let us reuse vertices and save some memory. Generating a buffer containing the indices for our primitive is pretty much the same as for vertex buffers:

crimild::IndexBufferObject *ibo = /* our IBO */
id < MTLBuffer > indexArray = 
   [getDevice() newBufferWithBytes: ibo->getData()
                            length: ibo->getSizeInBytes()
                           options: MTLResourceOptionCPUCacheModeDefault];

Once we have the index buffer created, we will use it during our render pass at the time we trigger the actual draw call for a given geometry:

[getRenderEncoder() drawIndexedPrimitives: MTLPrimitiveTypeTriangle
                               indexCount: indexCount
                                indexType: MTLIndexTypeUInt16
                              indexBuffer: indexBuffer
                        indexBufferOffset: 0];

There’s a lot to be said about the draw call, and that will be the main subject of my next couple of posts.

Uniforms

Time for some serious mind blower. The way Metal handles uniforms is simply fantastic. Instead of having to send each uniform (like transformations, lights or materials) separately to the GPU as in OpenGL, in Metal we can group them together into a single buffer and dispatch said buffer with just a single call.

id < MTLBuffer > uniforms = /* create the uniforms buffer *
[getRenderEncoder() setVertexBuffer: uniforms offset: 0 atIndex: 0];

Someone’s uncle once said: “with great power, come great design choices”. And I have to admit that working with uniforms in this way wasn’t easy for me. Crimild is designed based on OpenGL and similar APIs, and switching to the new paradigm required a couple of dirty tricks.

At first, I split the uniform buffers into a series of groups based on their functionalities, (as in, one group for transformations, another for lighting, materials, and so on). I didn’t liked that approach because it felt too much old-school and it wasn’t really taking advantage of the new mechanisms.

Instead, Crimild defines a single structure for all uniforms and let the renderer to set them up. I’m expecting that this design will change in the future, since it lacks the means to extend it with more functionalities (for example, bone data for animation), but for the moment I can live with it.

One thing to notice about uniforms is that we can link them with either the vertex shader function, the fragment shader function or both. Alas, we need to do it manually in our program. Therefore, if we need them in both shaders functions we will end up with code like this:

[getRenderEncoder() setVertexBuffer: uniforms offset: 0 atIndex: 0];
[getRenderEncoder() setFragmentBuffer: uniforms offset: 0 atIndex: 0];

Bonfire ahead, therefore pause

dark_souls_2_standart_icon

Time to take a little break. The next stop in our voyage (pun intended) will take us to the wonderful world of the Metal Shading Language, we’ll revisit uniforms and we’re going to start talking about the draw call itself. See you soon.

To be continued…

Advertisements

4 thoughts on “Praise the Metal – Part 2: The Geometry Pass

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s