iOS 开发者的 Metal 养成计划「3」 —— 并行渲染和 GPU 高并发运算

发布于:2018-01-17 22:20,阅读数:1130,点赞数:1


# 引言 看了上一篇文章以后相信读者已经对四种渲染命令中的其中两种已经有概念了。 本文会介绍四种绘制命令中剩下的两种:并行渲染命令和计算命令。 # Metal Command System 在上一篇文章中提到的,Metal 的命令共有四种: - `Blit Command`:块命令,用于在`MTLBuffer`和`MTLTexture`之间拷贝数据。 - `Render Command`:渲染命令,用于绘图。 - `Parallel Render Command`:并行渲染命令。 - `Compute Command`:计算命令,可用于高并发数据处理。 本文会介绍这四种命令中的后两种。 ### Parallel Render Command 正如它的字面意思:并行渲染命令。 > An object that splits up a single render pass so it can be simultaneously encoded from multiple threads. 单线程编码有时会带来一些性能问题,而`MTLParallelRenderCommandEncoder`已经为开发者做好了多线程编码命令的封装,但资源同步问题仍然需要开发者自己解决。 ![](//cdn.blog.yuusann.com/img/corpus/18006_1.jpg) 真正的渲染的任务仍然使用`Render Command`,使用方法跟在上篇文章中提到的完全一致。而`Parallel Render Command`仅仅是管理了一批可以同时对同一个渲染通道(`render pass`)进行编码的`Render Command`的一个封装 使用`Parallel Render Command`时,渲染命令编码器不再由`MTLCommandBuffer`产生,而是直接由`MTLParallelRenderCommandEncoder`产生。对着一系列的渲染命令进行编码以后,统一提交即可。 实现为: ```swift // 构造并行渲染命令编码器 guard let parallelRenderCommand = commandBuffer.makeParallelRenderCommandEncoder(descriptor: renderPassDescriptor) else { // error } // 构造一系列渲染命令编码器 guard let rc1 = parallelRenderCommandEncoder.makeRenderCommandEncoder(), let rc2 = parallelRenderCommandEncoder.makeRenderCommandEncoder(), let rc3 = parallelRenderCommandEncoder.makeRenderCommandEncoder() else { // error } // 创建 DispatchQueue let dispatchQueue = DispatchQueue.init(label: "parallel render", attributes: .concurrent) // 开始并行编码 dispatchQueue.async { // 省略对渲染命令的编码操作 // ... rc1.endEncoding() } dispatchQueue.async { // 省略对渲染命令的编码操作 // ... rc2.endEncoding() } dispatchQueue.async { // 省略对渲染命令的编码操作 // ... rc3.endEncoding() } // 使用 barrier 的操作,等待前面的编码执行完毕以后执行 dispatchQueue.async(execute: DispatchWorkItem.init(flags: .barrier) { // 结束并行渲染命令编码器编码 parallelRenderCommandEncoder.endEncoding() // 提交命令并等待执行结束 commandBuffer.commit() commandBuffer.waitUntilCompleted() }) ``` 更多内容请参考:[「Encoding a Single Rendering Pass Using Multiple Threads」](https://developer.apple.com/library/content/documentation/Miscellaneous/Conceptual/MetalProgrammingGuide/Render-Ctx/Render-Ctx.html#//apple_ref/doc/uid/TP40014221-CH7-SW1) ### Compute Command 似乎是对标`Compute Shader`来的,命令用于超高并发 GPU 运算。 从 Unity 的官方文档看到: > Compute shaders in Unity closely match DirectX 11 DirectCompute technology. Platforms where compute shaders work: > > - Windows and Windows Store, with a DirectX 11 or DirectX 12 graphics API and Shader Model 5.0 GPU > - macOS and iOS using Metal graphics API > - Android, Linux and Windows platforms with Vulkan API > - Modern OpenGL platforms (OpenGL 4.3 on Linux or Windows; OpenGL ES 3.1 on Android). Note that Mac OS X does not support OpenGL 4.3 > - Modern consoles (Sony PS4 and Microsoft Xbox One) 这个在 OpenGL 4.3 / OpenGLES 3.1 才新增的功能,iOS 和 macOS 都一致非常任性地`不支持`。大概这就是大佬吧,有了 Metal 就是这么自信。 > Explains how to perform data-parallel processing. 使用`MTLComputeCommand`就可以进行超高并发的数据计算了。在着色器中,索引的维度是可选的,因此这里可以是处理图像,也可以是处理普通数据。处理数据可以使用一维数据,处理图像则使用二维索引,此外还有三维索引,可以用来处理图片组做三维的数据处理。 计算命令和着色器编程是强相关的。以计算数据为例,简单的数据处理可以是从一个数组经过一定的逻辑处理生成另一个数组。 ![](//cdn.blog.yuusann.com/img/corpus/18006_2.jpg) 与 CPU 计算不同的是,这里处理的数据可以是百万级的数据。纵然是百万级的数据,手机的 GPU 仍然能在 0.01 秒内处理完毕。在笔者的测试中,iPod Touch 6 (处理器为 A8 降频,性能与 iPhone 5s 差不多) 使用计算命令能够在一秒内完成十亿次以上的开方运算。 但在使用计算命令时需要额外注意的是,事实上每一个 Metal 命令的运行时间都是有时间限制的。在超过时间限制后会发出`MTLCommandBufferError.timeout`报错。 > Execution of this command buffer took more time than than the system allows, so execution was interrupted and aborted. 在渲染图像时一般不会超出限制时间,但在使用计算命令处理复杂数据时有可能会超出限制时间,运行会被强制中断并报如下错误: > Execution of the command buffer was aborted due to an error during execution. Caused GPU Hang Error (IOAF code 3) 因此使用时需要额外注意。 构造命令的方式大同小异: ```swift // 根据函数名构造 MTLFunction 对象 guard let function = library.makeFunction(name: name) else { // error } // 由 MTLDevice 构造 MTLComputePipeLineState 对象 guard let pipelineState = try? device.makeComputePipelineState(function: function) else { // error } // 从队列中构造一个 MTLCommandBuffer guard let commandBuffer = commandQueue.makeCommandBuffer() else { // error } // 构造 MTLComputeCommandEncoder guard let commandEncoder = commandBuffer.makeComputeCommandEncoder() else { // error } commandEncoder.setComputePipelineState(pipelineState) ``` 使用时: ```swift // 设置参数 Buffer,如果用于计算数据,一般会设置多个输入多个输出的 Buffer。 encoder.setBuffer(...) // 以下为设置并发线程。 // 由于是计算数组数据,使用一维索引,因此深度和高度都设为1。使用渲染管线中每个渲染组最大的线程数。 let threadsPerThreadgroup = MTLSizeMake(pipelineState.maxTotalThreadsPerThreadgroup, 1, 1) // 相除后得出线程组数量。 let threadsGroupCount = MTLSize(width: size / threadsPerThreadgroup.width, height: 1, depth: 1) // 设置并发线程。 encoder.dispatchThreadgroups(threadsGroupCount, threadsPerThreadgroup: threadsPerThreadgroup) ``` 可以看到在使用计算命令时可以设置每个线程组的线程并发数和线程组的数量。GPU 拥有成百上千个流处理器,在动辄几百个线程并发的任务中能发挥出它最大的能力。 计算命令的逻辑在着色器程序中。关于着色器如何编写,将在后面的文章中讲。 更多信息请参考:[「NextPrevious Data-Parallel Compute Processing: Compute Command Encoder」](https://developer.apple.com/library/content/documentation/Miscellaneous/Conceptual/MetalProgrammingGuide/Compute-Ctx/Compute-Ctx.html#//apple_ref/doc/uid/TP40014221-CH6-SW1) # 小结 至此,Metal 命令系统中的四种命令已经介绍完毕了。 如果分一下组,渲染命令和并行渲染命令是为图像渲染准备的,块命令为拷贝数据而准备,而计算命令是为图像处理以及并发数据处理准备的。但光有这四种命令,没有着色器程序的话是什么也做不了的。 关于着色器以及更多内容请看接下来的文章。


评论:0条


返回列表

返回主页