大多数浏览器和
Developer App 均支持流媒体播放。
-
探索 Metal 中的编译工作流程
Metal 着色语言是基于 C++ 的一种强大语言,让 app 可以获得惊人的渲染效果,同时保持灵活的着色器开发管线。了解如何使用动态库和函数指针更轻松地构建和扩展渲染管线。我们还将展示如何使用二进制函数存档、函数链接和函数拼接在运行时加速着色器编译。
资源
- Creating a Metal Dynamic Library
- Metal
- Metal Feature Set Tables
- Metal Shading Language Specification
- Shader Libraries
相关视频
WWDC23
WWDC22
WWDC21
WWDC20
-
下载
大家好 我叫里妮·帕特尔 来自GPU软件工程团队 本课程中 我将为您介绍Metal中 新的着色器编译工作流程 Metal着色语言是基于C++的语言 其编译模型 与CPU的编译模型非常相似 随着GPU的工作量随复杂度上升 Metal也随之一起发展 以满足支撑现在用例 所需的灵活性和表现 您在进行染色器授权时 会遇到这样一些常见的问题 在管道之间共享实用代码 无重编译地对着色器行为进行即时修改 或者在应用启动之间 重利用编译后GPU二进制文件 现在我们通过一套简单的着色器代码 讨论一下这些情况
这里是一个简单的像素着色器 它根据结果条件 返回的结果是foo()或者bar() 如果这些函数被多个管道调用 我们会希望将它们只编译一次 然后链接给每条管道
在实时运行时 我们可能需要将一个不同的实现 链接到这些函数 或者我们需要一个像素着色器能够扩展 好为baz()处理一个 新的case声明 我们也可能希望去调用 一个用户提供的函数“bat” 而非来自像素函数的“baz” 如您所见 一个着色器授权管道时 有很多不同的要求 Metal能提供多种API 来支持这些不同的实现 每种方法都要在编译时间和着色表现间 进行取舍 今天我想要聊一聊这种新的编译流程 它会帮您找到 表现和灵活性之间的最佳平衡 我们从对于服务渲染管道 以及渲染管道函数指针的动态库 的新支持开始说起 我们还会再额外介绍 关于binaryArchive API的内容 接着再说一下私有链接的函数 最后则介绍Metal 将可见函数进行缝合的 全新的功能 那么我们就从Metal 所支持的动态库开始吧 动态库是软件工程中的常见工具 它们是一种共享对象文件 使您可以将实用代码 划分成独立的实现单元 有助于减少编译后的着色器代码数量 在多个管道之间重复利用 另外 它们还允许您 动态地链接 加载 共享GPU二进制代码
去年我们为计算管道引入了动态语言 为能更好向您介绍Metal的动态库 建议您观看去年的课程: “用Metal构建GPU二进制文件” 今年我们将用动态库来渲染和平铺管道 有了这额外的支持 您现在可以在 计算和渲染工作中共享实用库了 现在我们知道动态库是什么了 那就来讲一些相关用例 Helper函数在 一般计算 顶点着色器 像素着色器 和平铺着色器中很常用 现在有了动态库的额外助力 来帮助渲染管道 您可以执行大量的实用代码 并在工作之间进行共享 您可以将运行时可能用到的库 进行预编译 从而避免编译被拖缓 对于运行中的函数也是如此 只需在创建管道时 简单地更改加载的库即可 通过它们 您的用户不用提供源 就可以给着色器代码授权 直接作为管道的一部分加载 刚才谈过了何时使用动态库 现在我们来看看如何创建并使用它们 在示例像素着色器中 我们可以调用函数foo()和bar() 但在编译时 我们没有为任一函数提供实现 相反地 这些函数的实现 存在于一个Metal库中 我们稍后在创建渲染管道时会进行链接 为您可能用到的函数 提供各自的库也是可以实现的 现在我们来看一下在Metal构建动态库 所能提供的工具和灵活性 首先将您的 Metal着色器源代码编译到AIR 您可以用Xcode的Metal工具链 作为构建过程的一部分 也可以用newLibraryWithSource API 实时从源编译 当编译后的Metal着色器 引入到了AIR中 您现就可用newDynamicLibrary API 简单快捷地创建动态库 该库已经是GPU二进制且随时可调用了 但若您想在随后的运行中再次使用它呢 这就需要您将动态库序列化到硬盘 此工作通过serializeToURL API实现 随后再次使用则以 newDynamicLibraryWithURL API调用 现在我们实地进行一次 从动态库和像素着色器调用函数的过程 本例中 我们声明函数 foo()和bar()使用外部关键词 但未提供它们的定义 使用它们时 直接从像素着色器调用函数即可 而您构建自己的Metal库时 可以为外部函数提供实现 请记住这里在运行时 也可以用其他的东西替换这些实现 要实现这一点 您需要将动态库 添加到合适的预加载库数组中 本例是一个像素着色器 但是每个阶段和管道都有同样的属性 这些符号会像 被添加到这个数组的库 一样的顺序被理解 本流程很适合于 测试新的实现 这就是动态库的内容 macOS Monterey系统下的 Apple GPU家族7及以上 都支持在计算管道时使用Metal动态库 其他GPU以及 Mac家族2设备也可以使用它 这里是支持动态库的一些metal设备 在iOS 15下 Apple6设备及以上 可以使用本功能 所有支持Apple6功能的设备 都支持渲染和平铺管道
下面我们谈一谈 今年函数指针方面的改进
函数指针是代码中一个简单的结构 它允许我们调用之前未见过的函数 从而使代码变得可扩展 去年我们引入了计算管道函数指针 这里推荐您观看去年的课程 “Metal函数指针入门” 今年 我们将函数指针的支持 扩展到Apple Silicon 的渲染和平铺管道上来 与动态库相同 函数指针允许创建自定义的管道 有了函数指针 GPU管道可以调用 管道编译期间未涉及的代码 有了函数指针表 代码执行行为可以发生动态的改变 无论您是将不同函数表绑定 还是在GPU管道索引到函数指针表 同时 您还可以通过函数指针 决定如何平衡编译表现和运行表现 例如想要更快的编译速度 您可以将函数指针预编译成 GPU二进制文件 快速deal-in管道 而若想要更好的运行表现 您可以让管道引用AIR形式的函数 允许编译器做最大程度的优化 现在我们看一下 如何在代码中设置函数指针 这里有3项构建模块 我们先实体化函数 然后用这些函数配置一条管道 最后创建函数表 这些完成之后 使用新的渲染循环 就不用涉及很多代码了 下面我们详细地一步步来操作 要使用函数指针 我们首先声明函数的描述符 将其实体化并编译成GPU二进制版本 这可以减少管道的创建耗时 它很简单 就像声明一个描述符 并设置编译成二进制的选项一样 当Metal函数foo() 从库经描述符创建后 函数会由GPU后端的编译器进行编译 接下来我们要配置渲染管道的描述符 首先 我们将函数经由管道描述符 添加到它们起作用的环节 例如可能是顶点 像素或者平铺环节 添加的函数可以 选择AIR或者二进制形式 当添加AIR函数 编译器会静态链接到可见函数 允许后端编译器对代码进行优化 另一方面若您添加二进制函数 则会告知驱动 哪些外部编译的函数 可以从已给定的管道中调用 还有一件事要提一下 就是当您创建一条 使用二进制函数的管道 若您调用的代码拥有复合调用链 就像画面所示这种 那么明确规定必要的 call sec depth最大值是非常重要的 因为编译器无法 通过静态分析来决定这个深度 编译器会默认以最大深度来运行 所以若未正确规定 就会产生栈溢出 相反地 若正确设定好这个深度 就会有更好的资源概念和优化表现 所以一旦描述符完整设置好 您就可以创建管道 随时可以开始使用函数指针 创建好管道后 下一步是创建可见函数表 然后用API里的函数句柄填表 首先用描述符创建一个可见函数表 设定渲染阶段 然后创建函数句柄来引用那些函数 函数句柄和函数表 都针对一个给定的管道 和选定的阶段 然后您可将句柄通过setFunction API 插入到函数表中 现在我们看看所有设置都就绪后 要如何使用函数表 首先 作为命令和代码的一部分 我们将可见函数表绑定到缓冲索引 在着色器内部 可见函数表 是作为缓冲绑定传递的 我们随后可以通过此表调用我们的函数 这就是一个使用函数指针的简单范例 使用函数指针时一个常见的问题是 你创建了一个流程 随后却发现你需要访问 一个或多个额外的函数 现在 您可以实现这一要求 做法就是用同样的描述符 创建第二条管道来添加额外的函数 但是这会触发管道编译 要想加速这个过程 Metal允许您设定 你是否计划未来会扩展原来的管道 这样就能更快的从已存管道 创建新的管道 而它可以使用 所有原来管道最初创建的函数指针表 通过编码实现这些 需要在创建初始的管道时 将所有您希望扩展的环节的 supportAddingBinaryFunctions 设置成YES 然后当您需要创建扩展的管道时 创建 RenderPipelineFunctionDescriptor 然后将新的二进制函数的bat文件 包含进像素长度函数列表 最后 通过renderPipeline1 的额外的二进制函数 调用新的RenderPipelineState函数 创建renderPipeline2 二者是一样的 但是2包含了额外的函数指针bat 就是这样 看过了如何使用函数指针 接着来看一下哪些场景使用它们 Apple GPU家族6及以上 macOS Big Sur和iOS 14 都支持计算管道中的函数指针 Mac家族2设备也支持 今年我们将函数指针 扩展到Apple GPU家族6及以上 Mac OS Monterey和iOS 15 的渲染和平铺管道
下一个要聊的是关于 二进制函数的编译系统开销的管理 着色器的编译是非常时间密集的 所以您会希望 控制其带给应用的系统开销 为改进这方面表现 我们去年为Metal引入BinaryArchives BinaryArchives可以 将编译后的二进制版管道 收集并存储到硬盘 节省编译时间和后续运行 降低与编译相关的内存成本 今年binaryArchives新添了 存储可见和插入函数功能 允许您大幅压缩系统开销 那么现在来看一下 binaryArchives是如何存取的吧 若要将一个函数添加到BinaryArchive 只需调用addFunctionWithDescriptor 将函数描述符和源库作为实参传递过去 若从BinaryArchive 加载二进制函数指针 则将BinaryArchive置于函数描述符 的binaryArchives数组 然后调用Metal的library方法 newFunctionWithDescriptor 若数组中任何文件有编译后函数指针 它就会立刻返回 而不再进行重编译 这里是一些 newFunctionWithDescriptor 如何处理binaryArchives的规则 我们首先在BinaryArchive列表 搜索函数的二进制版本 若找到了该函数 则马上返回 若未找到 则检查CompileToBinary选项设定 若未要求二进制编译 就返回该函数的AIR版本 另一方面 若要求进行二进制编译 则根据管道选项 FailOnBinaryArchiveMiss的设置 要么实时编译函数的二进制版 要么返回nil 当您将MTLBinaryArchive 整合到您的应用 您可使用同样的文件来存储 所有的GPU编译的代码 你的渲染器 平铺还有计算管道 以及你的二进制函数指针 当您的文件已经被管道声明的对象 和二进制函数预占满 您就可以将其序列化到一个硬盘 这样收集和存储 GPU二进制文件的方式 有助于在随后应用程序上的 渲染器编译的加速 当使用有函数指针的管道 您或许会希望把 管道声明的对象本身进行缓存 但当管道含有不同的 函数指针组合的的时候 为何要进行缓存呢 例如这里有三个管道描述符 它们除了使用的函数指针之外 其他都一模一样 所以 若你使用AIR函数指针 您需要将管道所有的排列全部进行缓存 然而当使用二进制函数指针时 仅缓存一个单独的变量即可 因为当新函数指针添加进来时 管道二进制代码不会变 您可以使用这份archive 来找到管道的其他变量 无论管道描述符里使用 哪个二进制函数指针 其都不受影响 总结一下 在Metal里 您会很喜欢用binaryArchives 因为它是控制管道编译成本的绝佳工具 所有的设备都支持binaryArchives 但是要将函数指针 添加到BinaryArchive 则取决于函数指针的能力 现在我想简要地聊一聊 今年的另一个新特点 即私有链接功能 到目前 我们讨论了 动态库和函数指针是如何 为您的着色器管道 提供了极大灵活性 但有时候为了更好的表现 您或许会希望静态地 将一个外部函数链接到管道 去年我们为 linkedFunctions API添加了 静态链接AIR函数的功能 然而这需要函数指针的支持 因为这些在函数表中是可用的 今年我们引入了privateFunctions 函数和privateFunctions 都在AIR层面静态链接 但由于这些是私有函数 不用为函数指针生成函数句柄 这是编译器可以 全力优化您的着色器代码 那么这些在哪里可用呢 由于本功能是在AIR层面上工作 所以macOS Monterey和iOS 15 的所有设备都支持 接下来谈一谈 今天要聊的最后一项新功能 函数缝合 有些应用程序需要实时生成动态内容 例如根据用户的输入 为图像效果实现自定义化 或者例如基于输入数据的复合计算内核 函数缝合是应对这些的绝佳工具 在出现函数缝合之前 唯一的解决之道 就是生成Metal源字符串 这种操控字符串的技术比较低效 而且也意味着 从Metal到Air的转译是实时的 是一种很昂贵的操作 那么现在一起看一看函数缝合如何工作 函数缝合提供了一个机制 可以从计算图和预编译函数 实时生成函数 计算图是有向非循环图 在其中有两种节点 代表所生成函数的实参的输入节点 以及代表函数调用的函数节点 边也有两种:代表数据如何 从一个节点流动到另一个的数据边 还有代表函数调用执行顺序的控制边 我们来看一下函数缝合如何使用计算图 生成一个函数 我们先了解一下可缝合函数的概念 图中的函数必须拥有可缝合的属性 这种函数是可见函数 可以通过 functionStitching API使用 可缝合函数可作为 您应用程序bundle 携带的Metal库的一部分 以此避免metal到AIR转译的成本
缝合过程直接生成的函数是AIR形式的 且完全跳过Metal前段 生成的函数是一个常规可缝合函数 所以可以链接到管道 或直接作为函数指针使用 或用来生成其他函数 想一想前面的计算图 现在假设我们有两个 之前描述的那种来自库的函数A和C 那么我们看看当将这些函数 绑定到计算图会发生什么 缝合器将对应的函数类型 应用给每个函数节点 N0和N1从FunctionA获得类型 N2从FunctionC获得类型 之后 缝合器查看 使用输入节点的函数的参数类型 然后推断出节点们的类型 例如Input0的类型 被推断为devisedinpointer 因为这是N0和N1的第一个实参
缝合器随后生成一个 Metal所描述的函数的等价体 functionStitching API 使我们可以直接 从AIR生成一个包含了这种函数的库 现在我们弄清了函数缝合如何工作 下面是如何在API中使用它 首先我们需要定义缝合所得函数的输入 本例中 我们简单地为所有变量 生成足够的输入节点即可 接着我们为每个想在图中 调用的函数创建一个函数节点 对每个函数调用 我们定义其名称 实参 以及如果有明确的顺序需求的话 再定义一下控制独立度 最后用函数名称 计算图中使用的函数节点 以及任何我们想应用的函数属性 来创建计算图 我们还要分配 一个outputNode节点 用来返回缝合所得函数的输出值 这里就得到了一份计算图 现在我们可以用它创建一个函数了 第一步是创建 StitchedLibraryDescriptor 我们将stitchableFunctions 和functionGraph添加到描述符 然后用此描述符创建一个库 接着以此库创建缝合后的函数 这个缝合函数就完工了 任何需要可缝合函数的地方都可以使用 也可以作为函数 应用到另一个缝合计算图中 这就是函数缝合的内容 macOS Monterey和iOS 15设备 都可使用本API 做一个快速的总结 今天我们了解了 用于渲染管道的动态库和函数指针 可以静态链接到可见函数的 私有链接函数 以及在动态创建着色器时 函数缝合如何节省编译时间
那么何时选这个而不选那个呢 链接Helper和实用函数时 动态库是非常好的选择 当您使用一套固定实用函数 且这些函数不会频繁变更时 它们是最佳选择 函数指针让着色器可以调用 除了签名外 其余别的内容一无所知的函数 它无需知道有多少个函数 函数名称 也甚至不需要了解 开发者用AIR或二进制 做出的速度-灵活度权衡 今年您还可以对函数指针进行缓存 私有函数为您提供了通过名称 将函数静态链接到管道声明对象的功能 它们对于管道属于内部的 所以不能在可见函数表中进行编码 但它们允许编译器实现最高程度的优化 且所有的GPU家族都可支持它们 最后 函数缝合为您提供了 直接将代码段预编译成AIR的途径 可实时执行函数编译 若您今天在编写Metal着色器字符串 不得不承担实时从源代码编译的成本 那么函数缝合可以极大地加速这个流程 希望能给您能善加利用这些编译器功能 获得Metal使用新体验 感谢您的观看看 祝WWDC 2021过得愉快 [音乐]
-
-
5:38 - Shading language
// Declare external functions extern float4 foo(FragmentInput input); extern float4 bar(FragmentInput input); // Use functions in shader fragment float4 main(FragmentInput input [[stage_in]]) { switch(condition(input)) { case 0: return foo(input); case 1: return bar(input); } }
-
9:01 - Declare and instantiate visible functions
// Declare a descriptor and set CompileToBinary options MTLFunctionDescriptor* functionDescriptor = [MTLFunctionDescriptor new]; functionDescriptor.options = MTLFunctionOptionCompileToBinary; // Backend compile the function functionDescriptor.name = @"foo"; id<MTLFunction> foo = [library newFunctionWithDescriptor:functionDescriptor
-
9:30 - Configure pipeline descriptor
// Provide a list of functions that the pipeline stage may call // AIR functions renderPipeDesc.fragmentLinkedFunctions.functions = @[foo, bar, baz]; // Binary functions renderPipeDesc.fragmentLinkedFunctions.binaryFunctions = @[foo, bar, baz];
-
10:47 - Create and populate visible function table
// Create visible function table [renderPipeline newVisibleFunctionTableWithDescriptor:stage:]; // Create function handles [renderPipeline functionHandleWithFunction:stage:]; // Insert handles into table [visibleFunctionTable setFunction:atIndex:];
-
11:21 - Encoding and calling function pointers
// Bind visible function table objects to each stage [renderCommandEncoder setFragmentVisibleFunctionTable:atBufferIndex:]; // Usage in shader fragment float4 shaderFunc(FragmentData vo[[stage_in]], visible_function_table<float4(float3)>materials[[buffer(0)]]) { //... return materials[materialSelector](coord); }
-
12:20 - Incremental pipeline creation
// Enable incrementally adding binary functions per stage renderPipeDesc.supportAddingFragmentBinaryFunctions = YES; // Create render pipeline functions descriptor MTLRenderPipelineFunctionsDescriptor extraDesc; extraDesc.fragmentAdditionalBinaryFunctions = @[bat]; // Instantiate render pipeline state id<MTLRenderPipelineState> renderPipeline2 = [renderPipeline1 newRenderPipelineStateWithAdditionalBinaryFunctions:extraDesc
-
20:30 - Stitching process
[[stitchable]] int FunctionA(device int*, int) {…} [[stitchable]] int FunctionC(int, int) {…} [[stitchable]] int ResultFunction(device int* Input0, int Input1, int Input2) { int N0 = FunctionA(Input0, Input1); int N1 = FunctionA(Input0, Input2); int N2 = FunctionC(N0, N1); return N2; }
-
21:32 - Creating the graph
// Create input nodes inputs[0] = [[MTLFunctionStitchingInputNode alloc] initWithArgumentIndex:0]; // Create function nodes n0 = [[MTLFunctionStitchingFunctionNode alloc] initWithName:@"FunctionA" arguments:@[inputs[0], inputs[1]] controlDependencies:@[]]; n1 = [[MTLFunctionStitchingFunctionNode alloc] initWithName:@"FunctionA" arguments:@[inputs[0], inputs[2]] controlDependencies:@[]]; n2 = [[MTLFunctionStitchingFunctionNode alloc] initWithName:@"FunctionC" arguments:@[n0, n1] controlDependencies:@[]]; // Create graph graph = [[MTLFunctionStitchingGraph alloc] initWithFunctionName:@"ResultFunction" nodes:@[n0, n1] outputNode:n2 attributes:@[]];
-
22:18 - Configure stitched library descriptor
// Configure stitched library descriptor MTLStitchedLibraryDescriptor* descriptor = [MTLStitchedLibraryDescriptor new]; descriptor.functions = @[stitchableFunctions]; descriptor.functionGraphs = @[graph]; // Create stitched function id<MTLLibrary> lib = [device newLibraryWithDescriptor:descriptor error:&error]; id<MTLFunction> stitchedFunction = [lib newFunctionWithName:@"ResultFunction"];
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。