作为 iOS 框架一部分的金属文件

Metal file as part of an iOS framework

我正在尝试创建一个适用于 METAL Api (iOS) 的框架。我是这个平台的新手,我想知道如何构建框架来处理 .metal 文件(我正在构建静态库,而不是动态库)。它们应该是 .a 文件的一部分,还是作为框架包中的资源文件?或者还有其他方法吗?谢谢

更新: 对于那些解决这个问题的人——我最终遵循了 warrenm 1 的建议选项——将 .metal 文件转换为字符串并调用 newLibraryWithSource:options:error:。 虽然它在性能上不是最好的,但它允许我只发送一个框架文件,而无需导入额外的资源。这对于创建使用 Metal、ARKit 等着色器文件的框架的人来说可能很有用。

有很多方法可以为 Metal 着色器提供静态库,所有方法都有不同的权衡。我将尝试在此处列举它们。

1) 将您的 .metal 文件转换为静态字符串,这些静态字符串被烘焙到您的静态库中。

这可能是最糟糕的选择。这个想法是将 Metal 着色器代码预处理为字符串,这些字符串作为字符串文字包含在静态库中。然后,您将使用 newLibraryWithSource:options:error: API(或其异步兄弟)将源转换为 MTLLibrary 并检索函数。这需要您设计一个过程来执行 .metal 到字符串的转换,并且您会失去着色器预编译的好处,从而使生成的应用程序变慢。

2) 将 .metal 文件与您的静态库一起发送,并要求库用户将它们添加到他们的应用程序目标中

综合考虑,这是一个不错的选择,尽管它会给您的用户带来更多负担并公开您的 Metal 着色器源(如果这是一个问题)。静态库中的代码可以使用 "default library" (newDefaultLibrary),因为代码会被 Xcode 自动编译到应用程序的 default.metallib 中,后者嵌入在应用程序包中作为资源。

3) 随静态库发送一个 .metallib 文件

这是易用性、性能和安全性之间的一个很好的中间点(因为它不公开您的着色器源,只公开其 IR)。基本上,您可以在项目中创建一个 "Metal Library" 目标,将着色器代码放入其中。这将生成一个 .metallib 文件,您可以将其与静态库一起发送,并让您的用户将其作为资源嵌入到他们的应用程序目标中。您的静态库可以在运行时使用 newLibraryWithData:error:newLibraryWithURL:error: API 加载 .metallib。由于您的着色器将被预编译,因此创建库会更快,并且您将保持编译时诊断的优势。

提问者建议的方法可能行不通(因此缺少示例代码)。金属着色器 (.metal) 只是函数的集合,它不是 MTLLibrary (.metallib) 制作的。这是从字符 (const char *) 数组(与 NSString 不同)编译金属着色器的工作代码;随后是在运行前将 .metal 文件转换为 .metallib 文件的说明。

在运行时编译金属着色器

以下示例也可用于为用户提供着色器编辑器,并允许您仅更新应用的着色器部分,而无需用户更新整个应用:

NSError* error = NULL;
const char* vshSource =
"using namespace metal;\n"
"typedef struct {\n"
"    packed_float2 position;\n"
"    packed_float2 texcoord;\n"
"} Vertex;\n"

"typedef struct {\n"
"    float3x3 matrix;\n"
"    float3 offset;\n"
"} ColorConversion;\n"

"typedef struct {\n"
"    float4 position [[position]];\n"
"    float2 texcoord;\n"
"} Varyings;\n"

"vertex Varyings vertexPassthrough(\n"
"device Vertex* verticies [[ buffer(0) ]],\n"
"unsigned int vid [[ vertex_id ]]\n"
") {\n"
"   Varyings out;\n"
"   device Vertex& v = verticies[vid];\n"
"    out.position = float4(float2(v.position), 0.0, 1.0);\n"
"    out.texcoord = v.texcoord;\n"
"    return out;\n"
"}\n";

const char* fshSource =
"using namespace metal;\n"
"typedef struct {\n"
    "packed_float2 position;\n"
    "packed_float2 texcoord;\n"
"} Vertex;\n"

"typedef struct {\n"
    "float3x3 matrix;\n"
    "float3 offset;\n"
"} ColorConversion;\n"

"typedef struct {\n"
    "float4 position [[position]];\n"
    "float2 texcoord;\n"
"} Varyings;\n"

"fragment half4 fragmentColorConversion(\n"
                                       "Varyings in [[ stage_in ]],\n"
                                       "texture2d<float, access::sample> textureBGRA [[ texture(0) ]],\n"
                                       "constant ColorConversion &colorConversion [[ buffer(0) ]]\n"
                                       ") {\n"
    "constexpr sampler s(address::clamp_to_edge, filter::linear);\n"
    "return half4(half3(textureBGRA.sample(s, in.texcoord).rgb), 1.0);\n"
"}\n";

id <MTLFunction> vertexProgram;
id <MTLLibrary> vertexLibrary = [_device newLibraryWithSource:[NSString stringWithUTF8String:vshSource] options:NULL error:&error];
if (NULL != vertexLibrary)
{
    vertexProgram = [vertexLibrary newFunctionWithName:@"vertexPassthrough"];
} else {
    NSLog(@"Error compiling vertex program: %@", error.description);
}

id <MTLFunction> fragmentProgram;
id <MTLLibrary> fragmentLibrary = [_device newLibraryWithSource:[NSString stringWithUTF8String:fshSource] options:NULL error:&error];
if (NULL != fragmentLibrary)
{
    fragmentProgram = [fragmentLibrary newFunctionWithName:@"fragmentColorConversion"];
}  else {
    NSLog(@"Error compiling fragment program: %@", error.description);
}

以下是 Apple 开发者文档出版物的摘录;尽管信息相对简陋,但在就其主题进行交流时,将其用作您和您的听众共享的通用框架的基础。

Creating Libraries During the App Build Process

出于同样的原因,接受的答案是完全错误的;而且,它关于性能权衡的说法值得怀疑。以下是关于编译 Metal 着色器和创建 Metal 库的唯一明确声明,后面是实际代码:

Functions and Libraries

This chapter describes how to create a MTLFunction object as a reference to a Metal shader or compute function and how to organize and access functions with a MTLLibrary object.

MTLFunction Represents a Shader or Compute Function

A MTLFunction object represents a single function that is written in the Metal shading language and executed on the GPU as part of a graphics or compute pipeline. For details on the Metal shading language, see the Metal Shading Language Guide.

To pass data or state between the Metal runtime and a graphics or compute function written in the Metal shading language, you assign an argument index for textures, buffers, and samplers. The argument index identifies which texture, buffer, or sampler is being referenced by both the Metal runtime and Metal shading code.

For a rendering pass, you specify a MTLFunction object for use as a vertex or fragment shader in a MTLRenderPipelineDescriptor object, as detailed in Creating a Render Pipeline State. For a compute pass, you specify a MTLFunction object when creating a MTLComputePipelineState object for a target device, as described in Specify a Compute State and Resources for a Compute Command Encoder.

A Library Is a Repository of Functions

A MTLLibrary object represents a repository of one or more MTLFunction objects. A single MTLFunction object represents one Metal function that has been written with the shading language. In the Metal shading language source code, any function that uses a Metal function qualifier (vertex, fragment, or kernel) can be represented by a MTLFunction object in a library. A Metal function without one of these function qualifiers cannot be directly represented by a MTLFunction object, although it can called by another function within the shader.

The MTLFunction objects in a library can be created from either of these sources:

  • Metal shading language code that was compiled into a binary library format during the app build process.
  • A text string containing Metal shading language source code that is compiled by the app at runtime.

Compiling shader language source files and building a library (.metallib file) during the app build process achieves better app performance than compiling shader source code at runtime. You can build a library within Xcode or by using command line utilities.

Using Xcode to Build a Library

Any shader source files that are in your project are automatically used to generate the default library, which you can access from Metal framework code with the newDefaultLibrary method of MTLDevice.

Using Command Line Utilities to Build a Library

Figure 8-1 shows the command line utilities that form the compiler toolchain for Metal shader source code. When you include .metal files in your project, Xcode invokes these tools to build a library file that you can access in your app at run time.

To compile shader source into a library without using Xcode:

  1. Use the metal tool to compile each .metal file into a single .air file, which stores an intermediate representation (IR) of shader language code.
  2. Optionally, use the metal-ar tool to archive several .air files together into a single .metalar file. (metal-ar is similar to the Unix ar.)
  3. Use the metallib tool to build a Metal .metallib library file from IR .air files or from archive .metalar files.

Example: Building a Library File with Command Line Utilities

xcrun -sdk macosx metal MyLibrary.metal -o MyLibrary.air
    xcrun -sdk macosx metallib MyLibrary.air -o MyLibrary.metallib

To access the resulting library in framework code, call the newLibraryWithFile:error: method:

NSError *libraryError = NULL;
NSString *libraryFile = [[NSBundle mainBundle] pathForResource:@"MyLibrary" ofType:@"metallib"];
id <MTLLibrary> myLibrary = [_device newLibraryWithFile:libraryFile error:&libraryError];
if (!myLibrary) {
    NSLog(@"Library error: %@", libraryError);
}

作为希望在 SceneKit / ARKit 相关框架中包含金属着色器功能的人,可用的答案将我引向了错误的方向。有一个更简单的解决方案,它使用 makeDefaultLibrary(bundle: Bundle) (iOS 10+) 来访问框架的 .metal 依赖项中包含的函数。在这里为处于类似位置的人添加。

TL;DR,像这样访问框架的 MTLLibrary:

        //Get the framework bundle by using `Bundle(for: type(of: self))` from inside any framework class.
        //Then use the bundle to define an MTLLibrary.
        let frameworkBundle = Bundle(for: type(of: self))
        let device = MTLCreateSystemDefaultDevice()
        do {
            let bundleLib = try device?.makeDefaultLibrary(bundle: frameworkBundle)
            print(bundleLib.functionNames) //we can access our framework's metal functions! No build tricks/workarounds.
        } catch {
            print("Couldn't locate default library for bundle: \(frameworkBundle)")
            print( error )
        }

Xcode 通过编译 .metal 依赖项在构建时创建默认的着色器函数库。框架目标和应用程序目标都是如此,所以真正的问题是,如何访问框架的默认库?

可以使用 MTLDevice 上的 makeDefaultLibrary(bundle: Bundle) 方法访问框架的默认库。上面的示例代码显示了更多细节。

对于 Scenekit/ARKit 和 SCNProgram

bundle库可以设置为SCNProgram的库属性,然后可以定义片段和着色器函数,就像.metal文件包含在主项目中一样:

        //The SCNProgram that will use our framework's metal functions
        var program = SCNProgram()

        //Use the framework's bundle to define an MTLLibrary.
        let frameworkBundle = Bundle(for: type(of: self))
        let device = MTLCreateSystemDefaultDevice()
        do {
            let bundleLib = try device?.makeDefaultLibrary(bundle: frameworkBundle)

            //set the SCNProgram's library, and define functions as usual
            program.library = bundleLib
            program.fragmentFunctionName = "yourCustomFrameworkFragmentFunction"
            program.vertexFunctionName = "yourCustomFrameworkVertexFunction"
        } catch {
            print("Couldn't locate default library for bundle: \(frameworkBundle)")
            print( error )
        }