Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Manually Compile Metal Shaders

I'm interested in moving away from Xcode and manually compiling Metal shaders in a project for a mixed-language application.

I have no idea how to do this, though. Xcode hides the details of shader compilation and subsequent loading into the application at runtime (you just call device.newDefaultLibrary()). Is this even possible, or will I have to use runtime shader compilation for my purposes?

like image 935
lcmylin Avatar asked Aug 30 '15 16:08

lcmylin


2 Answers

Generally, you have three ways to load a shader library in Metal:

  • Use runtime shader compilation from shader source code via the MTLDevice newLibraryWithSource:options:error: or newLibraryWithSource:options:completionHandler: methods. Although purists may shy away from runtime compilation, this option has minimal practical overhead, and so is completely viable. Your primary practical reason for avoiding this option might be to avoid making your shader source code available as part of your application, to protect your IP.

  • Load compiled binary libraries using the MTLLibrary newLibraryWithFile:error: or newLibraryWithData:error: methods. Follow the instructions in Using Command Line Utilities to Build a Library to create these individual binary libraries at build time.

  • Let Xcode compile your various *.metal files at build time into the default library available through MTLDevice newDefaultLibrary.

like image 123
Bill Hollings Avatar answered Sep 21 '22 09:09

Bill Hollings


Here's actual code that creates vertex and fragment programs from a string; using it allows you to compile shaders at runtime (shown in the code following the shader string method).

To eliminate the need for the use of escape sequences (e.g., n...), I use the STRINGIFY macro. To workaround its limitation on the use of double quotes, I wrote a block that takes an array of header file names and creates import statements from them. It then inserts them into the shader at the appropriate place; I did the same for include statements. It simplifies and expedites the insertion of what are sometimes rather lengthy lists.

Incorporating this code will not only allow you to select a particular shader to use based on localization, but, if necessary, could also be used to update your app's shaders without having to update the app. You would simply create and ship a text file containing your shader code, which your app could be preprogrammed to reference as the shader source.

#if !defined(_STRINGIFY)
#define __STRINGIFY( _x )   # _x
#define _STRINGIFY( _x )   __STRINGIFY( _x )
#endif

typedef NSString *(^StringifyArrayOfIncludes)(NSArray <NSString *> *includes);
static NSString *(^stringifyHeaderFileNamesArray)(NSArray <NSString *> *) = ^(NSArray <NSString *> *includes) {
    NSMutableString *importStatements = [NSMutableString new];
    [includes enumerateObjectsUsingBlock:^(NSString * _Nonnull include, NSUInteger idx, BOOL * _Nonnull stop) {
        [importStatements appendString:@"#include <"];
        [importStatements appendString:include];
        [importStatements appendString:@">\n"];
    }];

    return [NSString new];
};

typedef NSString *(^StringifyArrayOfHeaderFileNames)(NSArray <NSString *> *headerFileNames);
static NSString *(^stringifyIncludesArray)(NSArray *) = ^(NSArray *headerFileNames) {
    NSMutableString *importStatements = [NSMutableString new];
    [headerFileNames enumerateObjectsUsingBlock:^(NSString * _Nonnull headerFileName, NSUInteger idx, BOOL * _Nonnull stop) {
        [importStatements appendString:@"#import "];
        [importStatements appendString:@_STRINGIFY("")];
        [importStatements appendString:headerFileName];
        [importStatements appendString:@_STRINGIFY("")];
        [importStatements appendString:@"\n"];
    }];

    return [NSString new];
};

- (NSString *)shader
{
    NSString *includes = stringifyIncludesArray(@[@"metal_stdlib", @"simd/simd.h"]);
    NSString *imports  = stringifyHeaderFileNamesArray(@[@"ShaderTypes.h"]);
    NSString *code     = [NSString stringWithFormat:@"%s",
                          _STRINGIFY(
                                     using namespace metal;

                                     typedef struct {
                                         float scale_factor;
                                         float display_configuration;
                                     } Uniforms;

                                     typedef struct {
                                         float4 renderedCoordinate [[position]];
                                         float2 textureCoordinate;
                                     } TextureMappingVertex;

                                     vertex TextureMappingVertex mapTexture(unsigned int vertex_id [[ vertex_id ]],
                                                                            constant Uniforms &uniform [[ buffer(1) ]])
                                     {
                                         float4x4 renderedCoordinates;
                                         float4x2 textureCoordinates;

                                         if (uniform.display_configuration == 0 ||
                                             uniform.display_configuration == 2 ||
                                             uniform.display_configuration == 4 ||
                                             uniform.display_configuration == 6)
                                         {
                                             renderedCoordinates = float4x4(float4( -1.0, -1.0, 0.0, 1.0 ),
                                                                                     float4(  1.0, -1.0, 0.0, 1.0 ),
                                                                                     float4( -1.0,  1.0, 0.0, 1.0 ),
                                                                                     float4(  1.0,  1.0, 0.0, 1.0 ));

                                             textureCoordinates = float4x2(float2( 0.0, 1.0 ),
                                                                                    float2( 2.0, 1.0 ),
                                                                                    float2( 0.0, 0.0 ),
                                                                                    float2( 2.0, 0.0 ));
                                         } else if (uniform.display_configuration == 1 ||
                                                    uniform.display_configuration == 3 ||
                                                    uniform.display_configuration == 5 ||
                                                    uniform.display_configuration == 7)
                                         {
                                             renderedCoordinates = float4x4(float4( -1.0, -1.0, 0.0, 1.0 ),
                                                                                     float4( -1.0,  1.0, 0.0, 1.0 ),
                                                                                     float4(  1.0, -1.0, 0.0, 1.0 ),
                                                                                     float4(  1.0,  1.0, 0.0, 1.0 ));
                                             if (uniform.display_configuration == 1 ||
                                                 uniform.display_configuration == 5)
                                             {
                                                 textureCoordinates = float4x2(float2( 0.0,  1.0 ),
                                                                                        float2( 1.0,  1.0 ),
                                                                                        float2( 0.0, -1.0 ),
                                                                                        float2( 1.0, -1.0 ));
                                             } else if (uniform.display_configuration == 3 ||
                                                        uniform.display_configuration == 7)
                                             {
                                                  textureCoordinates = float4x2(float2( 0.0,  2.0 ),
                                                                                        float2( 1.0,  2.0 ),
                                                                                        float2( 0.0,  0.0 ),
                                                                                        float2( 1.0,  0.0 ));
                                             }
                                         }

                                         TextureMappingVertex outVertex;
                                         outVertex.renderedCoordinate = float4(uniform.scale_factor, uniform.scale_factor , 1.0f, 1.0f ) * renderedCoordinates[vertex_id];
                                         outVertex.textureCoordinate = textureCoordinates[vertex_id];

                                         return outVertex;
                                     }

                                     fragment half4 displayTexture(TextureMappingVertex mappingVertex [[ stage_in ]],
                                                                   texture2d<float, access::sample> texture [[ texture(0) ]],
                                                                   sampler samplr [[sampler(0)]],
                                                                   constant Uniforms &uniform [[ buffer(1) ]]) {

                                         if (uniform.display_configuration == 1 ||
                                             uniform.display_configuration == 2 ||
                                             uniform.display_configuration == 4 ||
                                             uniform.display_configuration == 6 ||
                                             uniform.display_configuration == 7)
                                         {
                                             mappingVertex.textureCoordinate.x = 1 - mappingVertex.textureCoordinate.x;
                                         }
                                         if (uniform.display_configuration == 2 ||
                                             uniform.display_configuration == 6)
                                         {
                                             mappingVertex.textureCoordinate.y = 1 - mappingVertex.textureCoordinate.y;
                                         }

                                         if (uniform.scale_factor < 1.0)
                                         {
                                             mappingVertex.textureCoordinate.y += (texture.get_height(0) - (texture.get_height(0) * uniform.scale_factor));
                                         }

                                         half4 new_texture = half4(texture.sample(samplr, mappingVertex.textureCoordinate));

                                         return new_texture;
                                     }

                                     )];

    return [NSString stringWithFormat:@"%@\n%@", includes, imports, code];
}

        /*
        * Metal setup: Library
        */
        __autoreleasing NSError *error = nil;

        NSString* librarySrc = [self shader];
        if(!librarySrc) {
            [NSException raise:@"Failed to read shaders" format:@"%@", [error localizedDescription]];
        }

        _library = [_device newLibraryWithSource:librarySrc options:nil error:&error];
        if(!_library) {
            [NSException raise:@"Failed to compile shaders" format:@"%@", [error localizedDescription]];
        }

        id <MTLFunction> vertexProgram = [_library newFunctionWithName:@"mapTexture"];
        id <MTLFunction> fragmentProgram = [_library newFunctionWithName:@"displayTexture"];
.
.
.
like image 34
James Bush Avatar answered Sep 21 '22 09:09

James Bush