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

Posted

技术标签:

【中文标题】作为 iOS 框架一部分的金属文件【英文标题】:Metal file as part of an iOS framework 【发布时间】:2018-03-26 07:51:03 【问题描述】:

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

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

【问题讨论】:

很高兴您找到了适合您的方法。对于后人,我在下面发表了评论并分享了一个概念验证项目,该项目允许您保持着色器预编译的好处,同时仍然只发送一个文件。如果我在生产中这样做,那几乎肯定是我会采用的方法。 【参考方案1】:

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

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

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

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

考虑到所有因素,这是一个不错的选择,尽管它会给您的用户带来更多负担并暴露您的金属着色器源(如果这是一个问题)。静态库中的代码可以使用“默认库”(newDefaultLibrary),因为 Xcode 会自动将代码编译到应用的 default.metallib 中,该 default.metallib 作为资源嵌入到应用包中。

3) 在静态库旁边发布一个 .metallib 文件

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

【讨论】:

感谢您的详细解答。只是为了让事情清楚 - 如果我不想向我的用户发送另一个文件 - 第一个解决方案只是一个合适的? 我想您可以通过从 metallib 文件中获取字节并将它们作为字节的文字数组写入静态库源中来嵌入预编译库。这将满足一个文件的要求,同时也提供预编译的好处。 我刚刚测试了最后一种方法,它确实有效。这是一个概念验证项目,它可以完成所有工作:构建一个.metallib,将其烘焙到一个头文件中,并在运行时创建一个MTLLibrary,可以从中创建管道:dropbox.com/s/8w30r1gyutj9twc/EmbeddedKernelSample.zip?dl=0。这绝不是生产就绪的,但它应该足以说明这种方法。 太棒了!我会试试这个。 我通过将 metallib 拖放到 Build Phases 中的 Copy Files 阶段并选择 Resources 作为 Destination 添加了 metallib。那行得通,但是它将源代码硬编码为 ../../../Library/Developer/Xcode/DerivedData/VidWorkspace-gnnwdwbnewfpadcksukplsporkda/Build/Products/Debug-iphoneos/MyMetalLib.metallib 之类的东西,它无法生存如果我想在其他地方重用该项目......你如何告诉 Xcode 每次都指向正确的位置?【参考方案2】:

作为一个希望在 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) 方法访问框架的默认库。上面的示例代码显示了更多细节。

对于带有 SCNProgram 的 Scenekit/ARKit

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 )
        

【讨论】:

【参考方案3】:

提问者建议的方法可能行不通(因此,缺少示例代码)。金属着色器 (.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 库的唯一明确声明,然后是实际代码:

函数和库

本章描述了如何创建一个 MTLFunction 对象作为 参考金属着色器或计算函数以及如何组织 并使用 MTLLibrary 对象访问函数。

MTLFunction 表示着色器或计算函数

一个 MTLFunction 对象代表一个单独的函数 Metal 着色语言并在 GPU 上作为 图形或计算管道。有关金属底纹的详细信息 语言,请参阅金属着色语言指南。

在 Metal 运行时和图形之间传递数据或状态,或者 用 Metal 着色语言编写的计算函数,您分配一个 纹理、缓冲区和采样器的参数索引。参数索引 标识哪个纹理、缓冲区或采样器被引用 Metal 运行时和 Metal 着色代码。

对于渲染过程,您指定一个 MTLFunction 对象用作 MTLRenderPipelineDescriptor 对象中的顶点或片段着色器,如 在创建渲染管道状态中有详细说明。对于计算通行证,您 创建 MTLComputePipelineState 时指定一个 MTLFunction 对象 目标设备的对象,如指定计算状态中所述 和计算命令编码器的资源。

库是函数的存储库

一个 MTLLibrary 对象代表一个或多个 MTLFunction 的存储库 对象。单个 MTLFunction 对象代表一个 Metal 函数 这是用着色语言编写的。在金属底纹中 语言源代码,任何使用 Metal 函数的函数 限定符(顶点、片段或内核)可以表示为 库中的 MTLFunction 对象。没有这些之一的金属功能 函数限定符不能直接由 MTLFunction 表示 对象,尽管它可以被着色器中的另一个函数调用。

库中的 MTLFunction 对象可以通过以下任一方式创建 这些来源:

已编译成二进制库的金属着色语言代码 应用构建过程中的格式。 包含金属着色语言源代码的文本字符串,由应用在运行时编译。

在应用构建过程中编译着色器语言源文件并构建库(.metallib 文件)比在运行时编译着色器源代码获得更好的应用性能。您可以在 Xcode 中或使用命令行实用程序构建库。

使用 Xcode 构建库

任何着色器源文件位于 您的项目会自动用于生成默认库, 您可以使用 MTLDevice 的 newDefaultLibrary 方法从 Metal 框架代码访问它。

使用命令行实用程序构建库

图 8-1 显示了构成编译器的命令行实用程序 金属着色器源代码的工具链。当您包含 .metal 文件时 在你的项目中,Xcode 调用这些工具来构建一个库文件 您可以在运行时在您的应用中访问。

不使用 Xcode 将着色器源编译到库中:

    使用 metal 工具将每个 .metal 文件编译为单个 .air 文件,该文件存储着色器语言代码的中间表示 (IR)。 (可选)使用 metal-ar 工具将多个 .air 文件一起归档到一个 .metalar 文件中。 (metal-ar 类似于 Unix ar。) 使用 metallib 工具从 IR .air 文件或存档 .metalar 文件构建 Metal .metallib 库文件。

示例:使用命令行实用程序构建库文件

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

要在框架代码中访问生成的库,请调用 newLibraryWithFile:error: 方法:

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);

【讨论】:

嗨詹姆斯,我是问这个问题的人。您的回答非常翔实,谢谢分享。我不得不说的一件事——将着色器文件转换为字符串确实允许我构建一个静态库。代码:id&lt;MTLLibrary&gt; defaultLibrary = [_device newLibraryWithSource:&lt;METAL FILE AS STRING&gt; options:[MTLCompileOptions new] error:&amp;errors]; id&lt;MTLFunction&gt; capturedImageVertexFunction = [defaultLibrary newFunctionWithName:&lt;FUNCTION NAME AS STRING&gt;]; 我更新了我的答案,以准确演示如何在运行时编译金属着色器。

以上是关于作为 iOS 框架一部分的金属文件的主要内容,如果未能解决你的问题,请参考以下文章

iOS 8是否支持动态链接?

Struts是啥?

如何在 Flutter 插件的 Swift 编写的 iOS 部分中使用 Objective-C 框架

iOS开发高级分享 - iOS 13 中的新框架 — MetriKit

在 ios App 中添加金属支持作为可选渲染器

开源 | 基于Metal的机器学习框架Bender:可在iOS上运行TensorFlow模型