我有兴趣放弃 Xcode 并在混合语言应用程序的项目中手动编译 Metal 着色器。
但我不知道该怎么做。 Xcode 隐藏了着色器编译以及随后在运行时加载到应用程序中的详细信息(您只需调用
device.newDefaultLibrary()
)。这是否可能,或者我必须使用运行时着色器编译来实现我的目的?
通常,您可以通过三种方式在 Metal 中加载着色器库:
通过
MTLDevice newLibraryWithSource:options:error:
或 newLibraryWithSource:options:completionHandler:
方法从着色器源代码使用运行时着色器编译。尽管纯粹主义者可能会回避运行时编译,但此选项的实际开销最小,因此完全可行。避免此选项的主要原因可能是避免将着色器源代码作为应用程序的一部分提供,以保护您的 IP。使用
MTLLibrary newLibraryWithFile:error:
或 newLibraryWithData:error:
方法加载已编译的二进制库。按照使用命令行实用程序构建库中的说明在构建时创建这些单独的二进制库。让 Xcode 在构建时将各种
*.metal
文件编译到可通过 MTLDevice newDefaultLibrary
使用的默认库中。这是从字符串创建顶点和片段程序的实际代码;使用它可以让您在运行时编译着色器(如着色器字符串方法后面的代码所示)。
为了消除使用转义序列(例如,n...)的需要,我使用了 STRINGIFY 宏。为了解决使用双引号的限制,我编写了一个块,它采用头文件名数组并从中创建 import 语句。然后将它们插入到着色器的适当位置;我对 include 语句做了同样的事情。它简化并加快了有时相当冗长的列表的插入。
合并此代码不仅允许您根据本地化选择要使用的特定着色器,而且如有必要,还可以用于更新应用程序的着色器,而无需更新应用程序。您只需创建并发送一个包含着色器代码的文本文件,您的应用程序可以对其进行预编程以作为着色器源进行引用。
#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"];
.
.
.
添加我的答案,因为我发现当前的回复并不令人满意,因为到目前为止,它们还没有解决在没有安装 X-code 的情况下构建金属 GPU 二进制文件(.metal 文件到 .metallib 二进制文件)的问题,也没有在每次运行时编译它们。
命令如
xcrun -sdk macosx metal MyLibrary.metal -o MyLibrary.air
仍需要安装 X 代码。你可以解决这个问题。
首先,配置您的项目以包含 metal-cpp metal-cpp-extensions 标头,可在此处获取:https://developer.apple.com/metal/LearnMetalCPP.zip
其次,在 .metal 文件中编写着色器,在运行时编译一次,然后使用步骤 1 中的标头构建 Metal GPU 二进制文件。
一些示例代码:
#define NS_PRIVATE_IMPLEMENTATION
#define MTL_PRIVATE_IMPLEMENTATION
#define MTK_PRIVATE_IMPLEMENTATION
#define CA_PRIVATE_IMPLEMENTATION
#include <Metal/Metal.hpp>
#include <MetalKit/MetalKit.hpp>
#include <AppKit/AppKit.hpp>
void saveBinary(MTL::RenderPipelineDescriptor* renderPipelineDescriptor) {
NS::Error* error = nullptr;
MTL::BinaryArchiveDescriptor* binaryArchiveDescriptor = MTL::BinaryArchiveDescriptor::alloc()->init();
MTL::BinaryArchive* binaryArchive = device->newBinaryArchive(binaryArchiveDescriptor, &error);
NS::URL* saveLocation = NS::URL::alloc()->initFileURLWithPath(NS::String::string("/binary.metallib", NS::StringEncoding::UTF8StringEncoding));
binaryArchive->addRenderPipelineFunctions(renderPipelineDescriptor, &error);
binaryArchive->serializeToURL(saveLocation, &serializeError);
}
‘serializeToURL’函数序列化到磁盘,而binaryArchive对象本身可以加载管道描述符,包括但不限于‘MTL::RenderPipelineDescriptor’。这些描述符本身是使用未编译的内核、片段和顶点着色器函数初始化的。
初始化这些描述符后,您第一次在运行时编译着色器函数。但是,在完成开发后,您可以编译二进制文件并在运行时跳过编译。没有任何 X 代码参与。
这个过程在 swift 中与在 C++ 中本质上是相同的。我发现这个视频很有帮助,我的答案基于此: https://developer.apple.com/videos/play/wwdc2020/10615/