大多数浏览器和
Developer App 均支持流媒体播放。
-
利用 metal-cpp 以 C++ 语言进行 Metal 编程
您的 C++ 游戏和 App 现在可以充分利用 Metal 的功能了。我们将介绍如何通过 metal-cpp 帮助您将 C++ 代码桥接到 Metal,探索它们分别如何管理对象的生命周期,并且演示有助于这些语言在您的 App 中无缝协作的实用工具。我们还将分享如何设计 App 架构,以便巧妙集成 Objective-C 和 C++ 的最佳实践。
资源
相关视频
WWDC22
-
下载
♪ 柔和乐器演奏的嘻哈音乐 ♪ ♪ 大家好 我叫 Keyi Yu 是 Metal Ecosystem 团队的工程师 很高兴今天能在此 为您介绍 metal-cpp metal-cpp 专门服务于想要 在 Apple 平台 创作 Metal App 的 C++ 用户 metal-cpp 是一个低开销库 可以将 C++ App 连接到 Metal 首先大致介绍一下 metal-cpp 及其工作原理 然后我还会详细介绍下 Objective-C 对象的生命周期 C++ 和 Objective-C 处理 生命周期的方式有些差异 我会介绍该如何处理这些差异 Xcode 和 metal-cpp 有些很棒的实用程序 可以帮助您管理 App 中的 对象生命周期 最后 我将展示如何集成 C++ 代码 和 Objective-C 类 首先来说说 metal-cpp 及其工作原理 Metal 为 Apple 平台上 加速图形处理和运算 奠定了基础 使您的 App 和游戏得以 充分利用 GPU 的强大功能 最初设计 metal-cpp 时使用的是 Objective-C 的强大功能及惯例 但如果您的代码库用的是 C++ 则可能需要另外桥接(bridge) 您的代码和 Metal 的 Objective-C 代码 metal-cpp 应运而生! metal-cpp 能够充当 C++ App 和 Objective-C Metal 之间的枢纽 当 App 中应用了 metal-cpp 您就可以在 C++ 中 使用 Metal 类和函数 此外 在运行时 metal-cpp 也可以帮助 调用 Objective-C 函数 metal-cpp 是轻量级的 Metal C++ 包装器 之所以说轻量级 是因为 metal-cpp 已经成为了 带有内联函数调用的仅头文件库 能够通过达到 C++ 调用 Objective-C API 的一对一映射 实现 100% Metal API 覆盖率 为此 metal-cpp 包装了 Foundation 和 CoreAnimation 框架的部分内容 由于 metal-cpp 已 在 Apache 2 许可下开源 因此您可以修改库并轻松将其 添加到您的 App metal-cpp 能够使用 C 直接调用 Objective-C 运行时 该机制与 Objective-C 编译器用于 执行 Objective-C 方法 的机制完全相同 所以这个包装器增加的开销不多 由于 metal-cpp 实现了 C++ 对 Objective-C 调用的 一对一映射 它也遵循相同的 Cocoa 内存管理规则 这个问题稍后会详谈 这种一对一映射还能够让 连贯衔接所有开发者工具 包括 GPU 帧捕获和 Xcode 调试器 屏幕上是用 metal-cpp 绘制三角形 所需的一系列调用 如果您已熟悉掌握 C++ 现在正是学习 Metal 的好时机 语言语法不会成为您的学习障碍 如果您已经 在 Objective-C 中使用过 Metal 在函数调用方面 Metal 的 Objective-C 接口 和 metal-cpp 两者差别很小 让我来演示下 metal-cpp 的操作有多简便 首先 创建一个命令缓冲区 填写供 GPU 执行的命令 可以简单将 C++ 原始指针 作为 Objective-C ID 的映射 然后创建一个渲染命令编码器 用命令缓冲区编写渲染命令 C++ 函数渲染命令编码器 和 Objective-C 函数中的 带描述符的渲染命令编码器 是一样的 唯一的区别是编程语言的 名称惯例 接着设置一个渲染管道状态对象 其中包含顶点 片段着色器 以及其他各种渲染状态 然后 对绘制调用进行编码 以绘制三角形 然后给出指示 表明渲染命令的编码已经完成 我展示了可绘制对象 因此三角形会显示在屏幕上 最后 提交了命令缓冲区 来告诉 GPU 可以开始执行命令了 显然 metal-cpp 和 Objective-C Metal 两者几乎一模一样 有了 metal-cpp 您无需担心 语言语法会带来问题 您可以直接查看 Metal 文档 来学习 Metal 的概念和用法 您可能已经测试过 之前用过的这个延迟光照示例了 现在 该延迟光照示例有了新版本 支持使用 metal-cpp 希望这可以帮助您在实践中学习 如何使用 metal-cpp 进行编码 我也很高兴为您带来 一系列应用了 Metal API 的 渐进的 C++ 示例 让我来展示如何用它完成不同的任务
现在您对 metal-cpp 有了一点了解 您该如何在实践中应用呢? 我们去年发布了 metal-cpp 您可以在这个网页找到下载资料 和说明 让我来为您示范一下步骤 下载 metal-cpp 后 您需要告知 Xcode 其路径 请看 我把 metal-cpp 放到了当前项目下 然后 需要将 C++17 或更高版本 设置为 C++ 语言方言 接下来 在项目中添加三个框架 Foundation QuartzCore 和 Metal 还有最后一件事 完成后才能使用 这些框架的 C++ 接口 metal-cpp 中有三个头文件 由于 metal-cpp 是只有头文件的库 您需要在导入头文件之前 生成头文件的实现 为此需要定义三个宏: NS_PRIVATE_IMPLEMENTATION CA_PRIVATE_IMPLEMENTATION 和 MTL_PRIVATE_IMPLEMENTATION 如果您好奇 metal-cpp 在后台 对宏的作用 请查看 metal-cpp 文件夹 中的桥接头文件 头文件可以单独使用 也可被放入一个单独的头文件 您可以随时在需要时导入头文件 但请记住 不要重复定义 NS、CA 或 MTL_PRIVATE_IMPLEMENTATION 宏 否则可能导致重复定义错误 为了高效使用 metal-cpp 您需要了解 Cocoa 的内存管理规则 学习如何利用功能强大的实用程序来 管理对象生命周期 以及学习如何设计 与其他框架接口时 的 App 架构 先说对象生命周期管理 在 App 运行期间 您通常需要分配和释放内存 还需要管理命令缓冲区 管道对象和资源 为了辅助管理这段内存 Objective-C 和 Cocoa 对象 包含了一个引用计数 metal-cpp 中也是同样 引用计数可以帮助您管理内存 应用引用计数之后 所有对象都将包含 一个 retainCount 属性 App 中的组件能够增加计数 使其正在交互的对象保持活跃 等完成后再减少对象 当 retainCount 达到 0 时 运行时将释放对象 Objective-C 中有两类引用计数 一种称为手动引用计数(Manual Retain-Release) 即 MRR 另一个是自动引用计数 (Automatic Reference Counting) 即 ARC 使用 ARC 功能编译代码时 编译器将采用您创建的引用 并自动将调用插入到 底层的内存管理机制 metal-cpp 对象需要手动保留和释放 因此 您需要了解 Cocoa 惯例 才能知道保留和释放对象的时机 与在 C++ 中创建对象不同 metal-cpp 对象 既不会被用 new 创建 也不会被用 delete 销毁 在 Cocoa 惯例中 您创建的任何对象 凡是以 alloc、new、copy mutableCopy 或 create 开头的方法创建 都归您所有 您可以行使所有权来保留获得对象 当您不再需要该对象时 则必须放弃对这一对象 的所有权 您可以立即或稍后释放该对象 对于不归您所有的对象 您不能放弃其所有权 否则可能导致 double free 漏洞 接下来 我将进行展示一个 Cocoa 惯例的示例 在 A 类中 某个方法 使用了 alloc 来创建对象 并用 init 初始化此对象 请注意 永远不要 在一个对象上两次调用 init A 类取得所有权 获得了释放该对象的权力 现在 该对象的 retainCount 为 1 接下来 B 类 使用 retain 来获取对象 并获得了该对象的所有权 到目前为止 这里有两个对象共享着 这个橙色方块对象的所有权 retainCount 增加 1
A 类不再需要这个对象 所以 A 类应该手动调用释放 结果 retainCount 减 1 现在 拥有该对象的只有 B 类了 好的 最后 B 类也想释放这个对象 现在 retainCount 为 0 于是运行时释放对象 换一种情况 假设 B 类的方法 返回了一个对象 但在其余程序中 您仍然需要此对象 换句话说 虽然您想放弃 B 类方法对象的所有权 但您不希望该对象被立即释放 在这种情况下 则需要 在 B 类中调用 autorelease 调用 autorelease 后 retainCount 仍为 1 也就是说 之后仍然可以使用该对象 来思考一下: 既然 B 类不再拥有该对象 那么该由谁来释放呢? Foundation 框架 提供了一个重要对象 也就是 AutoreleasePool Autorelease API 将对象 放入 AutoreleasePool 于是现在 AutoreleasePool 获得了对象的所有权 当 AutoreleasePool 被销毁时 会减少接收者的 retainCount 您不一定需要手动创建自动释放对象 Metal 本身便自带了几个 自动释放对象 所有创建临时对象的方法 都可以将它们添加到 AutoreleasePool 只需在后台调用 autorelease 即可 AutoreleasePool 会负责释放对象 换句话说 有了 AutoreleasePool 的帮助 您可以更从容地进行编码 您可以为主 App 设置 AutoreleasePool 我们也鼓励您在较小范围内 创建和管理额外的 AutoreleasePool 来减少您程序的工作集 您还需要为创建的每个线程 使用AutoreleasePool 这是一个使用 AutoreleasePool 和自动释放对象的示例 在该示例中 一个 AutoreleasePool 由 alloc 创建 此时您拥有其所有权 且需要您手动释放 现在我们有了一个 AutoreleasePool 正如之前说过的 您需要创建一个命令缓冲区 由于不是用 alloc 或 create 创建 您对其没有所有权 相反 它是由 Metal 创建的+ 自动释放对象 该命令缓冲区将被放入 AutoreleasePool 由 AutoreleasePool 来负责释放 直到释放 AutoreleasePool 之前 您都可以随意使用它 接着 您需要创建一个 RenderPassDescriptor 该 RenderPassDescriptor 也将同样被放入 AutoreleasePool 与 RenderCommandEncoder 相同 它也是一个由 Metal 创建的 自动释放对象 不要忘记这个 currentDrawable 对象 它也会被放入 AutoreleasePool 中 在这段代码的最后 我使用了 pPool->release 来释放 AutoreleasePool 在被释放之前 AutoreleasePool 会释放拥有的所有对象 在这种情况下 它会释放 CommandBuffer RenderPassDescriptor RenderCommandEncoder 和当前可绘制对象 最后再释放 AutoreleasePool 目前为止 您已经了解了 Cocoa 惯例 自动释放对象和 AutoreleasePool 只有正确管理对象生命周期 才能避免内存泄漏和僵尸对象 而我们恰好为您提供了合适的工具 可以避免及排除这些问题 我将专注于两个实用程序: NS::SharedPtr 和 NSZombie NS::SharedPtr 是新增的实用程序 可以帮助您管理 对象生命周期 可以在 Foundation 框架下的 metal-cpp 文件夹中找到它 请注意 它和 std:shared_ptr 并不完全相同 它不依赖于 C++ 标准库 且没有额外的存储引用计数的成本 这就是 NS::SharedPtr 其转移和保留函数能够清晰表达 消耗一个对象的意图 转移函数能够在 不增加指针引 referenceCount 的情况下 创建一个 SharedPtr 来将所有权高效转移到 SharedPtr 保留函数能够向传入的对象 发送保留命令 您可以使用此功能来让 AutoreleasePool 中的对象保持活动 并用其来表示 指针所有者 在被指对象的生命周期内 拥有既得利益 您可以如您所想 通过 get 和 operator-> 来访问底层对象 SharedPtr 也能够按您所需 复制、移动构造及赋值 复制操作会增加 retainCount 而移动操作速度迅速 且不影响 retainCount 一般情况下 SharedPtrs 只会向被指对象 发送一个释放命令 只要您想 也可以通过调用分离函数 避免这种情况 回到正题 了解传输或保留这两种指针创建方法 之间的区别非常重要 对于 TransferPtr 来说 假设这里有一个 MRR 对象 引用计数为 1 在将它传递给 TransferPtr 函数后 SharedPtr 将取得对象的所有权 但它的 retainCount 不会改变 当指针超出范围时 SharedPtr 的析构函数便会运行 并在 MRR 对象上调用释放函数 将 retainCount 减为 0 另一个函数是 NS::RetainPtr 当您想将对象留下备用 而不是现在释放时 就应该使用 NS::RetainPtr 以这个 MRR 对象为例 retainCount 数是 1 在将它传递给 RetainPtr 函数之后 retainCount 加一 超出范围后 该 RetainPtr 调用 释放此 MRR 对象的函数 所以 retainCount 是 1 一般来说 NS::TransferPtr 会为您 取得一个对象的所有权 而 NS::RetainPtr 会帮您 在不想释放对象时保留该对象 当您将对象传递给这两个函数 NS::TransferPtr 不会改变引用计数 但是 NS::RetainPtr 将 使引用计数增加 1 因为 NS::RetainPtr 会在后台 为您调用保留函数 这两个函数的析构函数 都为传入的对象调用释放 因此 引用计数减 1 如果引用计数为 0 该对象将在运行时被释放 这是 NS::TransferPtr 的示例 说起渲染通道 我之前用它画了个三角形 我需要这个渲染管道状态 以下是创建渲染管道状态对象的调用 这些是渲染管道描述符需要的属性 根据 Cocoa 惯例 凡是以 new 和 alloc 开头的调用 其对象都归我所有 所以我需要为这些对象手动调用释放 但使用 NS::SharedPtr 后 就不需要我来为这些 MRR 对象 调用释放了 因为 NS::SharedPtrs 拥有这些对象的所有权 看这里 我将原始指针传递 给了 TransferPtr 函数 完成后 我在上一页中 示范的操作就没有必要了 现在无需我来调用释放 如果您熟悉 ARC 可能会发现 一起使用 MRR 与 NS::SharedPtr 体验和使用 ARC 十分相似 您可能会在手动处理内存时 遇到释放后使用错误 如果您尝试使用已经释放的对象 就有可能出现这类报错 NSZombie 可以有效检查出这类错误 当出现释放后使用错误时 NSZombie 将触发断点 为您提供堆栈跟踪 您可以很容易地运用带有环境变量 来启用 Zombies 只需将 NSZombieEnabled 设为 YES 即可 或者 如果您使用的是 Xcode 则可以在 scheme 中 启用 Zombies 让我来演示一下工作原理 我现在想创建一个 具有相同渲染管道设置的 新渲染管道状态对象 所以在这个 newRenderPipelineState 函数中 我重用了 pDesc 对象
点击运行后 Xcode 触发断点 并显示堆栈跟踪 这意味着有哪里出了问题 嗯 出什么问题了? 也许 NSZombie 可以帮上忙 于是我在 scheme 中启用了 NSZombie
当我再次运行程序时 NSZombie 会触发断点 在控制台输出中有了些新东西: “message sent to deallocated instance” (消息已发送到已释放实例) 哦 原来是因为我重用了 一个已经释放的对象 这个对象就是渲染管道描述符 所以我只能在释放之前 使用这个渲染管道描述符 顺着这样的步骤 问题就解决了 收看今年的演讲 “Profile and optimize your game’s memory” 您可以了解更多工具和细节 比如说 该如何在 Instruments 内存分配中 跟踪 retainCount 欢迎您随时查看 Apple 平台上的其他工具 您会找到不是可以 帮助您调试游戏 提高性能的工具 您已经了解了该如何在 metal-cpp 中管理对象生命周期 但您可能仍有与其他框架交互的需求 比如说 游戏控制器和音频 Objective-C 也同样可以实现 该如何与这些 API 交互 并设计出简明的 App 架构呢? 假设您用 Objective-C 写了一个 ViewController 而编写渲染器时却用了 C++ 和 metal-cpp 那么您需要就需要 从 ViewController 调用渲染器方法 比如绘制 挑战之处在于 需要将两种语言分开的同时 让两种语言协同工作 想要解决 就需要创建一个适配器类 使其从 Objective-C 文件中调用 C++ 这样做之后 您便可以 在想要实现功能的文件中 专门使用 Objective-C 或 C++ 举个例子 在 Objective-C 中 创建一个 RendererAdapter 在执行过程中 我添加了一个 Objective-C 方法 方便我从 ViewController 里 直接调用它 在界面内 我声明了一个指向 渲染器对象的 C++ 指针 在方法主体内部 我直接调用了渲染器的 C++ 方法 该方法需要将 MTK:View 作为 C++ 对象放入绘制方法中 也就是说 通过使用 __bridge 关键字 视图被强制转换成了 C++ 类型 关于这个强制转换 我会之后再详细说明 与之相对 您需要做的 则是调用 MTKView 其编写工具是渲染器的 Objective-C 而渲染器则由 C++ 编写 这也同样具有挑战性 想要解决 同样需要 创建一个适配器类 有了这个类 在 C++ 文件中 您可以使用 C++ 接口 调用 Objective-C 方法 比如说 创建一个 ViewAdapter 类 这个接口我是用 C++ 编写的 所以在渲染器类中 我可以轻松调用 C++ 视图方法 在执行过程中 我从 MTKView 调用了 Objective-C 方法 包括 currentDrawable 和 depthStencilTexture 您可能看到了 这里有些 __bridge 关键字 是用来在 metal-cpp 对象 和 Objective-C 对象 之间进行转换的 正如在一开始说过的 metal-cpp 对象需要 手动保留和释放 而 Objective-C 创建的对象 使用的是自动引用计数 您需要在 MRR 和 ARC 之间 来回移动对象 以下是三种类型的桥式转换 可以帮助您在 Objective-C 和 C++ 之间进行转换 还可以帮助您转移对象所有权 _bridge 转换可以在 Objective-C 和 metal cpp 对象之间 进行强制转换 之间不进行所有权转移 __bridge_retained 可以将 Objective-C 指针 强制转换为 metal-cpp 指针 并从 ARC 获得所有权 __bridge_transfer 能够 将 metal cpp 指针 移动到 Objective-C 并 将所有权转移给 ARC 回到我们的问题 您需要在 metal-cpp 对象 和 Objective-C 对象之间进行转换 如果所有权没有发生转移 您可以使用 __bridge 转换 如果您想将对象从 metal-cpp 转换为 Objective-C 并将所有权转移给 Objective-C 则应该使用 __bridge_transfer 转换 如果您想将对象从 Objective-C 转换为 metal-cpp 并从 ARC 中移出所有权 则应该使用 __bridge_retained 转换 假设一个情景中 我必须使用 MetalKit 来利用资产加载代码 也就是说 在我的 C++ App 中 我需要一个纹理 来充当 metal-cpp 对象 但它是由 Objective-C 方法创建的 我需要想办法将所有权转出 ARC 方便我手动释放它 在这种情况下 我需要选用 __bridge_retained 转移 这有个从目录加载纹理的 C++ 函数 我现在想返回一个 metal-cpp 纹理 但在里面 我在 MetalKit 中调用了 一些 Objective-C 函数 我需要定义纹理加载器所需的选项 接着 我通过从 MetalKit 调用 Objective-C 方法 创建了一个纹理加载器 有了加载器之后 我可以创建一个纹理对象 并从目录中加载纹理 这个方法也属于 MetalKit 的 Objective-C 方法 现在 我有了一个 Objective-C 类型的纹理 我需要将其转换为 metal-cpp 对象 并将其从 ARC 中取出 将步骤都铭记于心后 就可以动手编写代码了 让我来展示一下桥式转换 的工作原理吧 第一步是定义纹理加载器需要的 纹理加载器选项 可以放心将 metal-cpp 存储模式和使用方法 转换为 Objective-C 类型 因为 metal-cpp 类型 对它们的定义为相同值 请看 我现在创建了一个纹理加载器 我有了一个 metal-cpp 对象的设备 现在需要将它传递 给 initWithDevice 方法 由于 metal-cpp 对象 属于 Objective-C 对象 因此可以像 toll-free 对象 一样进行转换 所有权没有进行转让 现在我使用了纹理加载器选项 和一个纹理加载器来创建纹理 我想将加载的纹理 作为 metal-cpp 对象返回 因此 我需要将其转出 ARC 并转换为相应的指针类型 可以通过 __bridge_retained 转换完成 之后 我就可以将这个纹理 作为任意 metal-cpp 对象 我要负责释放该对象 在本节视频 我介绍了可以在您的程序中 帮助您处理两种不同语言的 适配器模式 我还展示了如何通过 三种类型的桥接转换 实现 Objective-C 和 C++ 交互 总的说来 metal-cpp 是一个非常高效的 轻量级 Metal C++ 包装器 我还介绍了 在使用 metal-cpp 时 该如何管理对象生命周期 又该如何用简便的方式 与 Objective-C 交互 以及我们的开发者工具 能为您的调试提供哪些帮助 请下载 metal-cpp 体验众多效果惊人的范例吧! 看看在 Metal 的辅助下 您能做出怎样的创造 我们期待能看到您的 C++ App 在所有 Apple 平台上运行 感谢收看! ♪
-
-
3:10 - Draw a single triangle in C++
MTL::CommandBuffer* pCmd = _pCommandQueue->commandBuffer(); MTL::RenderCommandEncoder* pEnc = pCmd->renderCommandEncoder( pRpd ); pEnc->setRenderPipelineState( _pPSO ); pEnc->drawPrimitives( MTL::PrimitiveTypeTriangle, NS::UInteger(0), NS::UInteger(3)); pEnc->endEncoding(); pCmd->presentDrawable( pView->currentDrawable() ); pCmd->commit();
-
3:27 - Draw a single triangle in Objective-C
id<MTLCommandBuffer> cmd = [_commandQueue commandBuffer]; id<MTLRenderCommandEncoder> enc = [cmd renderCommandEncoderWithDescriptor:pRpd]; [enc setRenderPipelineState:_pPSO]; [enc drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; [enc endEncoding]; [cmd presentDrawable:view.currentDrawable]; [cmd commit];
-
6:10 - Generate the implementation
#define NS_PRIVATE_IMPLEMENTATION #define CA_PRIVATE_IMPLEMENTATION #define MTL_PRIVATE_IMPLEMENTATION #include <Foundation/Foundation.hpp> #include <Metal/Metal.hpp> #include <QuartzCore/QuartzCore.hpp>
-
11:46 - How to use autoreleased objects and AutoreleasePool
NS::AutoreleasePool* pPool = NS::AutoreleasePool::alloc()->init(); MTL::CommandBuffer* pCmd = _pCommandQueue->commandBuffer(); MTL::RenderPassDescriptor* pRpd = pView->currentRenderPassDescriptor(); MTL::RenderCommandEncoder* pEnc = pCmd->renderCommandEncoder( pRpd ); pEnc->endEncoding(); pCmd->presentDrawable( pView->currentDrawable() ); pCmd->commit(); pPool->release();
-
11:47 - How NS::TransferPtr works
{ auto ptr = NS::TransferPtr( pMRR ); // Do something with ptr . . . }
-
17:19 - How NS::RetainPtr works
{ auto ptr = NS::RetainPtr( pMRR ); // Do something with ptr . . . }
-
20:43 - Create an adapter class calling C++ from Objective-C files
@interface AAPLRendererAdapter () { AAPLRenderer* _pRenderer; } @end @implementation AAPLRendererAdapter - (void)drawInMTKView:(MTKView *)pMtkView { _pRenderer->draw((__bridge MTK::View*)pMtkView); } @end
-
21:49 - Create an adapter class calling Objective-C from C++ files
CA::MetalDrawable* AAPLViewAdapter::currentDrawable() const { return (__bridge CA::MetalDrawable*)[(__bridge MTKView *)m_pMTKView currentDrawable]; } MTL::Texture* AAPLViewAdapter::depthStencilTexture() const { return (__bridge MTL::Texture*)[(__bridge MTKView *)m_pMTKView depthStencilTexture]; }
-
24:59 - Cast between Objective-C and C++ objects and transfer ownership
MTL::Texture* newTextureFromCatalog( MTL::Device* pDevice, const char* name, MTL::StorageMode storageMode, MTL::TextureUsage usage ) { NSDictionary<MTKTextureLoaderOption, id>* options = @{ MTKTextureLoaderOptionTextureStorageMode : @( (MTLStorageMode)storageMode ), MTKTextureLoaderOptionTextureUsage : @( (MTLTextureUsage)usage ) }; MTKTextureLoader* textureLoader = [[MTKTextureLoader alloc] initWithDevice:(__bridge id<MTLDevice>)pDevice]; NSError* __autoreleasing err = nil; id< MTLTexture > texture = [textureLoader newTextureWithName:[NSString stringWithUTF8String:name] scaleFactor:1 bundle:nil options:options error:&err]; return (__bridge_retained MTL::Texture*)texture; }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。