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?
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
.
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"];
.
.
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With