Praise the Metal – Part 3: Metal Shading Language

Hello, Voyagers. Welcome to another entry in this series of articles about Metal and how Crimild managed to support it.

In this post I’m going to briefly introduce MLSL. Truth is, there are many things that can be said about this new shading language which will require much more than a blog post. So I promise I’m going to try and keep things as simple as possible.

Overview

Metal provides its own language for writing shaders, the Metal Shading Language (or MLSL for short) and it can be used for both graphics and compute processing. Designed for LLVM and clang, and based on a static subset of C++11, MLSL comes with a whole new set of improvements over, for example, OpenGL’s own GLSL.

I said that MLSL is based around C++11, and it does so by providing its own extensions and restrictions to the language. Mechanisms like function overloading and templates are really useful in the field, even when I didn’t have the chance to exploit them as much as I like. Another amazing feature is that you can declare structs and functions in your header files and reuse them inside other shader files or even from our C++ code. Indeed, now we can design actual libraries with reusable code for our shaders without having to rely on macros and other dirty tricks.

On the downside, recursive function calls and lambdas are out of the equation, as well as dynamic memory usage with the new or delete operators (obviously).  But look at the bright side, goto statements are banned as well. Because, you know, this isn’t the 80’s anymore

It’s also worth mentioning that, since we cannot use C++’s standard library in our shaders, Metal does provide its own, named the Metal Standard Library. This library includes all kind of helper functions for simple math, graphics, texture handling, and more.

Writing and Compiling Shaders

Metal shaders are saved on files with the .metal extension which will be automatically compiled by Xcode whenever the application is built. Compiled shaders are stored within libraries to be used later by pipelines as we saw in my previous post. There’s no need to ship shader code within the application anymore and any coding error can be reported also at build time.

While offline shader compilation at built time is the recommended approach, Metal also provides a run-time shader compiler just in case you need to build them dynamically. Again, it’s not the recommended choice since you will be suffering a performance penalty at draw time.

MLSL code is organized in a different way than GLSL. For starters, we can have both vertex and fragment shader functions in the same .metal file. Also, there’s no need to split our code into separated programs, greatly reducing the amount of duplicated code.

Let’s see MLSL in action…

The Vertex Shader Function

For the rest of the post I’m going to describe a simple shader implementation for diffuse shading. For such an example, the vertex shader function only needs to compute the projected vertex position and would look something like this:

vertex VertexOut crimild_vertex_shader_unlit_diffuse( VertexIn vert [[ stage_in ]],
                                                      constant crimild::metal::MetalStandardUniforms &uniforms [[ buffer( 1 ) ]] )
{
    float4x4 mvMatrix = uniforms.vMatrix * uniforms.mMatrix;
    float4 mvPosition = mvMatrix * float4( vert.position, 1.0 );

    VertexOut out;
    out.position = uniforms.pMatrix * mvPosition;

    return out;
}

This function uses the vertex qualifier to indicate that it will be executed for each vertex, generating per-vertex output. Those of you familiar with GLSL should be able to recognize what this function does quite easily.

The VertexIn and VertexOut data types are declared as follows:

struct VertexIn {
    float3 position [[ attribute( 0 ) ]];
    float3 normal [[ attribute( 1 ) ]];
    float2 uv [[ attribute( 2 ) ]];
};

struct VertexOut {
    float4 position [[ position ]];
    float3 normal;
    float3 eye;
};

The Fragment Shader Function

Fragment shader functions must use the fragment qualifier. They are executed for each fragment in the stream and generate per-fragment output.

Here’s the code for our diffuse shading fragment shader

fragment float4 crimild_fragment_shader_unlit_diffuse( VertexOut projectedVertex [[ stage_in ]],
                                                       constant crimild::metal::MetalStandardUniforms &uniforms [[ buffer( 1 ) ]] )
{
    return uniforms.material.diffuse;
}

Notice that both shader function may be declared with any name (except main) as long as they use the corresponding qualifiers.

Argument Tables and Uniforms

OK, I bet you already noticed those attributes surrounded by brackets in some of the arguments in the code above. Remember my last post where I mentioned how to use buffers when rendering geometries?

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

Each command encoder contain several argument tables, one per argument type (buffers, textures and samplers). The atIndex parameter in the above function call is used to index the corresponding table (in this case, buffers).

Then, MLSL will use the index in the shader code when referencing the argument. For example, referencing uniforms in either the vertex or fragment function is done in the following fashion:

constant crimild::metal::MetalStandardUniforms &uniforms [[ buffer( 1 ) ]]

Speaking of uniforms, In my last post I described the mechanisms to create uniform buffers and pass them to each of the shader functions. I said Crimild uses a single structure for all standard uniforms that are used by the MetalRenderer and such struct looks like this:

        typedef struct {
            simd::float4 ambient;
            simd::float4 diffuse;
            simd::float4 specular;
            float shininess;
        } MaterialUniform;

        typedef struct {
            simd::float3 position;
            simd::float3 attenuation;
            simd::float3 direction;
            simd::float4 color;
        } LightUniform;

        typedef struct {
            MaterialUniform material;

            unsigned int lightCount;
            LightUniform lights[ CRIMILD_METAL_MAX_LIGHTS ];

            simd::float4x4 pMatrix;
            simd::float4x4 vMatrix;
            simd::float4x4 mMatrix;
        } MetalStandardUniforms;

The MetalStandandarUniforms struct is declared in its own header file and can be used from either the Metal API or MLSL. For complex data types, like vectors and matrices, we can make use the facilities provided by the simd library. We could use static arrays too, although it’s not recommended.

Stock Shaders

Crimild comes with a set of stock shaders for Metal as it does for OpenGL. At the time of this writing there are only a bunch of them like diffuse shading, phong lighting, textures and a basic forward rendering shader that mixes all of them together. Many more will be implemented in the future as new features are added.

In Crimild, the crimild::Shader class is responsible to store the actual shader code. Since shaders in Metal are pre-compiled, that class now holds the name of either the vertex or the fragment shader functions instead.

Wrapping up

That will be all about MLSL. Well, not all of it, but at least what I considered most relevant for the moment. We’ll see more of MLSL when we talk about post-processing and image effects.

We’re ready to finish the draw process now. In the next entry, we’re finally going to talk about render encoders and how to execute the draw call itself. Praise the Metal.

To be continued…

 

Advertisements

4 thoughts on “Praise the Metal – Part 3: Metal Shading Language

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