[引擎开发] 现代图形API - metal篇
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
Metal是苹果开发的图形计算接口它是在移动端出现的比较早的现代图形API。本文将更侧重于移动端IOS对metal的API做一个大致的引入介绍。
Apple GPU概述
在我们对Metal进行介绍前先来了解一下Apple GPU。
Apple GPU特性
总体而言Apple GPU具有如下特性我们将在后续模块进行详细的介绍
① 省电高续航
② 统一的内存架构CPU和GPU共享系统内存
③ 使用分块延迟渲染TBDR
④ DRAM带宽具有瓶颈
Apple GPU组成
Apple的硬件包含了多个GPU核心GPU Core每个核心包含
① 着色核心ALU) L1 Cache
② 纹理单元TPUL1 Cache
③ 像素后端
④ 分块内存Tile Memory
除此之外为了提升数据读写的效率Apple针对所有核心提供了共享缓存末级缓存GPU Last Level Cache。
其中L1 Cache包括着色核心和纹理单元、Tile Memory和GPU Last Level Cache都属于GPU高速缓存是为了加速和系统内存交换数据而设计的。
threadgroup数据和imageblock数据保存在分块内存Tile Memory而metal资源包括纹理和缓冲区都保存在系统内存System Memory中。
Apple GPU渲染流程
和PC端常见的IMRImmediate Rendering流程不同Apple GPU为了减少带宽消耗使用了分块延迟渲染。这意味着当我们绘制几何体的时候并不会一次性完成整个绘制的流程而是分为两个部分第一个部分进行分块第二个部分进行渲染。
① 分块阶段
把视图分为多个分块Tile计算每个分块关联的图元Primitive分块的数量和核心数量相关
对Tile中的图元进行几何变换对所有顶点执行顶点着色器
将转换后的图元传给每个分块计算的结果记录到系统内存中
② 渲染阶段
初始化Tile中的数据执行加载load/clear/dontcare
对转换后的图元执行光栅化
对所有的所有可见像素执行像素着色器
将Tile中数据写入系统内存执行保存store/dontcare
③ Hidden Surface Removal
特别的Apple的GPU支持了HSRHidden Surface Removal即隐藏表面消除。也就是在执行渲染操作前先对所有像素执行可见性测试不可见的像素不会进行着色计算。这一步发生在Tiling后渲染着色前。
这有别于常规的渲染流程中可见性测试在着色完成后才进行不可见像素的计算相当于被浪费了产生了overdraw。
但是对于需要Alpha Test的像素由于在像素阶段才能确定当前像素是否会被剔除因此无法在执行可见性测试前执行预剔除操作也就是说如果我们做了一些操作使得在像素阶段才能决定一个像素的可见性我们将打断HSR的执行。
Metal基础
根据现代图形API的概念我们在讨论图形接口时一般会关心渲染指令是怎么执行的资源是如何绑定的内存是如何分配和管理的以及资源和渲染pass之间是如何进行同步的。针对metal的基础概念介绍也将从这几个方面入手。
渲染指令
Metal的接口基于Device来定义Device描述了一个GPU。通过Device我们可以创建渲染资源对象Buffer Texture以及创建CommandQueue来执行渲染指令等。
如上图所示Device创建的Command Queue用于创建和管理Command Buffers它可被提交到设备Command Buffer可用于创建不同类型的CommandEncoder包括串行执行的CommandEncoder和并行执行的ParallelCommandEncoder。
CommandEncoder包含了用于渲染CommandRenderEncoder、计算CommandComputeEncoder或位图传输CommandBlitEncoder的Encoder。
Command Queue
Command Queue用于创建和管理Command Buffer。通常在一个图形应用程序中会创建一个常驻的Command Queue它不应被反复创建销毁。
Command Queue支持添加多个Command Buffer后再同时对多个Command Buffer分别进行编码。它无需关心渲染指令的具体内容只用于维护Command Buffer的执行顺序。
Command Buffer
Command Buffer包含了渲染、计算、位图三种编码形式。作为指令的缓冲区它是轻量级的、暂存的上下文数据也就是说Command Buffer一旦使用结束后将被销毁不可复用。
当我们对Command Buffer执行Enqueue后Command Buffer被添加到Command Queue而只有当我们对Command Buffer执行Commit操作后Command Buffer才有可能被执行并且会按照入队的顺序先后执行。
Command Encoder
Command Encoder对应于一个RenderPass它描述了RenderPass相关的渲染状态和渲染指令可用于编码不同类型的渲染指令包括绘制、计算和位图分别对应了几何体的绘制计算着色器的使用和纹理/缓冲区的拷贝。
一个Command Buffer可以编码多个Command Encoder但只能同时存在一个激活的Command Encoder。和Command Buffer一样Command Encoder属于暂存的对象仅作用于创建和渲染指令执行期间。创建时我们可以设定它引用的资源并可以执行相关的渲染操作如
① 设置所需的资源
② 设置所需的渲染状态
③ 设置渲染管道状态
④ 执行绘制图元
Parallel Command Encoder
如果我们想要并行地创建提交Command Encoder我们可以使用多线程同时创建多个Command Buffer。但是在GPU端任一时刻只能对一个Command Encoder执行编码操作为了支持GPU端的并行编码我们可以使用Parallel Command Encoder。
对于Parallel Command Encoder我们可以把一个渲染pass拆分到多个Command Encoder中GPU端可以并行处理同一Encoder特别适合于drawcall数量特别多的情况。
Renderer Pipeline State
图形渲染管道的大部分配置由Renderer Pipeline State来描述它绑定到Render Command Encoder上。
Pipeline State Object是可以在整个生命周期重复使用的对象它的创建比较昂贵并且可能涉及到着色器的编译。我们通常是预先创建PSO再将其每帧设置到Command Encoder上。metal提供了同步和异步创建PSO的两种方式。
最佳的实践实在离线的时候编译着色器并构建链接库在游戏加载时候预创建所有PSO。
GPU Channel
由于在TBDR的架构中顶点和片元是分别处理的这意味着Vertex和Pixel阶段是由两个不同的GPU Channel负责的因此在GPU端Vertex(Tiling PixelRenderingCompute这三个通道是可以重叠的。
在合理的调度下我们可以看到pass A的pixel 能够和pass B的vertex以及async compute一起并行执行的现象。pass A本身的vertex和pixel由于存在依赖关系是无法并行的。
多线程渲染
如果我们在CPU端同时提交多个GPU工作的话那么就可以实现多线程渲染。这可以通过每个线程使用独立的Command Buffer录制来实现并通过Command Queue来确保不同工作在GPU上的执行顺序。
let commandBuffer1 = commandQueue.makeCommandBuffer();
let commandBuffer2 = commandQueue.makeCommandBuffer();
// 添加到CommandQueue的顺序和GPU执行的顺序一致
commandBuffer1.enqueue();
commandBuffer2.enqueue();
// 多线程使用不同的commandbuffer编码任务并提交
queue.async(group : group) {
encodeGBufferPass(commandBuffer2, ...);
commandBuffer2.commit();
}
queue.async(group : group) {
encodeDeferredShading(commandBuffer1, ...);
commandBuffer1.commit();
}
CPU和GPU同步
CPU和GPU的工作模式是CPU准备数据而GPU处理数据这意味着CPU处于工作状态的时候GPU可能正在休息而GPU忙碌的时候CPU则无事可做。
为了避免这种状态我们通常会在GPU绘制当前帧数据的时候让CPU开始准备下一帧的数据这意味着CPU通常会比GPU提前一些。
这可以通过不同帧的数据使用不同的命令缓冲区来实现通过设置信号量来实现同步信号量的初始值代表最多等待的帧数。CPU完成处理后信号量减一GPU完成处理后信号量加一。当CPU不能再提前时它将阻塞等待GPU。
Apple Developer Documentationhttps://developer.apple.com/documentation/metal/resource_synchronization/synchronizing_cpu_and_gpu_work 此外为了保证帧率的稳定也就是每帧画面呈现到屏幕上的时间是比较接近的我们可以设置CommandBuffer至少需要执行特定时间(present : afterMinimumDuration来实现这一点。
协调CPU和GPU的工作一个最佳的实践是三重缓冲这样可以尽可能地提高CPU和GPU的利用率。
Indirect Command Buffer
特别的metal支持了一个完全意义上的GPU驱动管线应该是让GPU具备发起调用和绑定的能力即在GPU中使用Indirect Command Buffer进行渲染指令的编码。
CPU端只需发起Indirect Command Buffer的调用即可。
渲染通道
和dx12这样主要面向桌面端图形API不同metal和vulkan这种面向移动端的图形API都有明确的渲染通道概念Command Encoder并且需要明确指明开始和结束的时间。
不同Render Command Encoder的主要差异在于它们绑定的Attachment不同这是由RenderPassDescriptor设置的。
之所以要特别的指明渲染通道的概念是因为在移动端中Attachment的写入对带宽消耗非常大并且通常是手机发热的罪魁祸首因此我们应该明确指出Command Encoder的切换并且控制Command Encoder的数量。
Subpass
为了减少Attachment的切换对于一些临时的最终不会输出到渲染目标的数据我们可以将其写入到On-chip Memory中然后通过FrameBuffer Fetch的支持读取写入到On-chip Memory的数据。相比起写入到System MemoryOn-chip Memory的写入对带宽消耗很低。
由于这个数据仅存在于线程组中所以我们只能拿到线程组内的数据也就是特定Tile内的数据能够相互访问。通常情况下我们只会访问当前像素位置上的数据。通过线程组内存的使用我们降低了带宽的消耗与之而来的代价是使用了过多的线程组内存这可能会导致可用的线程数下降。
一个非常常用的例子就是移动端的延迟光照计算
Load Action和Store Action
对于Command Encoder来说在设置Attachment的时候我们需要指定对应的Attachment的属性即Load Action和Store Action它们分别描述了在渲染pass的开始和结束对Attachment执行的操作仅针对于系统内存的操作不描述memoryless对象
Load Action包括了Clear、Load和DontCare。使用Load操作后将会保留纹理中的内容但是需要消耗一定的时间从系统内存加载数据这和往系统内存写入数据带来的消耗是类似的使用Clear操作后则不会花费时间加载数据但仍然会消耗时间去填充数据使用DontCare操作则既不会加载也不会填充在性能上也是其中最好的。
Depth Attachment
在像素着色器中metal不支持读取深度附件Depth Attachment的数据。
这意味着如果我们想要获取深度数据我们需要额外创建一张Color Attachment将其设置为深度R16或R32绑定这个Attachment并在着色器中手动写入深度信息。也就是说它与其它Attachment在使用上并无差异只不过记录的是深度信息。
Color Attachment
在有些图形API中我们不支持颜色附件Color Attachment的同时读写也就是说在某个像素绘制的时候我们不能既读又写。
但是对于metal而言颜色附件是可以同时读写的这意味着我们可以实现程序上自定义的混合操作也就是自己读取当前像素颜色应用一些混合公式再写入最终的缓冲区而可以不依赖于硬件提供的Color Blending。
关闭光栅化
对于不需要ps计算的pass如只需写入深度的pass像prez pass我们可以通过设置fragmentFunction为nil来屏蔽光栅化的过程。
Command Buffer使用
Command Buffer用于渲染指令的编码它内部可以编码多个Command Encoder甚至我们可以用一个Command Buffer编码整个场景的绘制。
为了提高的GPU的利用率我们应该把我们的工作尽早地提交给GPU因此更好的做法是合理地拆分Command Buffer使得一帧内有多个Command Buffer。
结合GPU Channel一章中提到的内容通过对Command Buffer拆分将pass A和pass B分开提交可以让一些GPU任务很好地并行起来。
资源管理
IOS是统一的内存架构可共享系统内存意味着CPU和GPU的数据存储在统一的系统内存。
当我们对纹理或缓冲区进行修改时我们对虚拟内存的修改会同时体现到CPU和GPU上。
资源类型
metal中的资源对象包括纹理对象和缓冲区对象。
其中缓冲区是可以存放任意类型的格式它是无格式的我们可以指定其分配的长度。
而纹理是一种格式化的图像数据包括了Texture1DTexture2DTexture3D, TextureCube以及Texture1DArray和Textue2DArray。纹理有对应的height、width、depth属性同时也可以指定Mipmap。
资源存储模式
为了在共享内存架构下对资源访问的优化当我们创建GPU资源的时候我们可以指定资源在CPU或GPU中可访问的存储模式(MTLStorageMode)。
Shared Mode
对于可供CPU和GPU同时访问的资源我们可将其设置为shared模式比如一些需要频繁更新的资源,这也是系统的默认属性。CPU和GPU不能同时访问该资源这意味着如果我们希望在CPU端修改该资源我们需要在提交引用该资源的命令前完成修改如果我们在GPU端修改该内容在命令完成前CPU端不得读取该资源。
Private Mode
对于仅在GPU端访问的资源可将其设置为private模式如大部分的美术资源。metal对于私有资源的访问进行了优化它不需要显式的同步所以在必要的情况下比如使用的资源为渲染目标Render Target时我们应该将属性设置为private模式的。
由于CPU端无法访问资源当我们要初始化私有资源的时候我们需要先将数据写入共享内存然后再把数据复制到私有资源。
Memoryless Mode
特别的针对渲染目标我们可以将其设置为memoryless模式。这意味着当我们绘制像素到渲染我们无需将结果写入到系统内存System memory而是可以将其写入到更高效的片上缓存中on-chip memory。我们将之称之为Memoryless因为它不占用系统内存并且由于高效的缓存带宽消耗相比之下也非常低。
特别的在macOS中包含托管模式MTLStorage.managed由于IOS是共享系统内存所以不存在这一模式我们不做讨论。
资源状态
每个资源都有特定的状态量可以设置这些设置描述了资源位置、同步等相关属性
① Storage Mode 存储模式
Shared Mode, Private Mode, Memoryless Mode
② CPU Cache Mode CPU缓存模式
defaultCache默认CPU缓存模式
writeCombined针对CPU写入但不读取的资源优化
③ Aliasable是否可别名
当我们把资源手动设置为可别名后makeAliasable我们可以实现内存的重叠这意味着不同的资源可以映射到同一段显存。
我们可以在不再需要这段显存后调用该接口如对临时的RenderTarget以便之后复用
temporaryRenderTarget.makeAliasable()
④ Purgeable Mode可清除状态
描述资源在内存中是否可以常驻的状态合理地将一些资源设置为Non-Volatile可以降低内存的使用。
Volatile资源可以被丢弃以释放内存
Non-Volatile资源不能被清除
Empty资源正在或即将被清除
⑤ HazardTracking Mode
当我们设置资源堆模式为Track后metal会跟踪资源访问的依赖性也就是会自动做资源的同步管理。在上一个对资源修改的命令还未完成的时候会延迟下一个相关指令的执行。
反之设为Untrack需要我们手动管理资源的同步。
通常来说如果我们需要系统管理内存我们将其设置为Track如果资源不需要同步或者我们自己来维护同步可将其设置为Untrack。
⑥ Usage Mode使用状态
Read : 可读 Write : 可写
显存分配
在显存分配上我们可以直接从设备Device上创建也可以从设备上创建堆再从堆heap上分配资源。
通过创建堆我们可以实现自定义的显存管理。创建堆后我们可以从堆上进行资源的子分配Sub-Allocating包括创建Buffer和Texture相比起创建堆子分配的消耗较低。
根据分配资源的方式堆包括了三种不同类型
关于稀疏纹理有一个很好的例子是用它来管理场景中的mipmap资源
创建堆时需要我们指定堆的属性包括Storage Mode存储模式CPUCache Mode缓存模式Tracking Mode跟踪资源模式等堆中分配的所有资源共享这些设置。
换句话来说对于不同属性的资源我们需要在不同的堆中创建。
资源同步
metal中如果要对资源进行同步要么使用资源的track模式让驱动协助管理同步要么使用barrier, fence或者event手动进行同步。
如果我们交由track来管理那么系统会认为不同编码器对相同资源的读写是串行的。
Memory Barrier
我们可以为单个资源设置内存屏障通过指定它前后所处的RenderStage它由RenderCommandEncoder调用。
对资源在两个调用之间加入内存屏障可以确保先前的绘制结果对于之后的绘制阶段来说是有效的。这是一种内存有效性的保证。
Fence
fence的设计比较轻量它由Device创建并CommandEncoder调用它是一种执行顺序的保证只有在上一阶段的任务完成才会开始下一个任务。
fence仅提供了两个接口一个是updateFence另一个是waitForFence。编码在调用waitForFence后会等待直到updateFence被调用
renderEncoder.updateFence(gBufferFence after:.fragment)
computeEncoder.waitForFence(gbufferFence)
比如deferredshading中的计算编码需要依赖于gbuffer pass完成输出的gbuffer那么则在gbuffer pass fragment阶段完成后UpdateFence, 在shading pass中WaitForFence。
这里通过使用[waitForFence:beforeStage:]和[updateFence:afterStage:]的接口我们可以获得Command Encoder内细粒度更高的同步管理它描述了在哪个阶段后设置同步在哪个阶段前执行等待。
可选的阶段MTLRenderStages包括vertex, fragment, tile, mesh ,object。
fence的使用非常简单但由于它是基于CommandEncoder调用它只能用作Command Encoder之间的同步这里其实还隐式包含了一个含义那就是fence只能作用在同一个Command Buffer内。
Event
另外一种event同步是在Command Buffer中指定的它可以处理更高级别的同步包括跨Command Buffer的同步跨设备的同步以及CPU和GPU的同步。
这包括了MTLEvent和MTLSharedEvent其中MTLEvent用于GPU内部的同步MTLSharedEvent用于CPU和GPU的同步。可共享事件的开销要高于不可共享事件。
事件可以包含一个信号量(Semaphore)这是一个无符号的64位整数调用encodeSignalEvent我们会增加信号量的值表明工作负载已经完成。调用encodeWaitForEvent, 我们会减少信号量的值表明等待工作负载的完成等待仅在信号量小于特定值的时候发生这意味着工作尚未完成。
我们也可以直接调用Event的Signal和Wait来管理同步。
资源传输
在开发过程中我们会频繁地将资源在CPU和GPU之间传输这可以通过Blit Command Encoder来完成它提供了如下功能
● 填充缓冲区fill)
● 在缓冲区之间纹理之间缓冲区和纹理之间复制数据 (copy)
● 优化内存布局optimizeContentsForGPUAcces, optimizeContentsForCPUAccess)
● 生成纹理mipmapgenerateMipmaps)
如果我们想要从CPU传入美术资源纹理的初始化数据考虑到资源纹理通常不会更新我们将其设为Private Mode仅GPU访问然后通过Blit从CPU可访问的资源拷贝数据到资源纹理中。
对于StorageMode为Share的资源优化内存布局可以分别优化CPU和GPU单项的访问性能但是会损害另一侧的访问性能。
资源绑定
Argument Buffer
Argument Buffer是metal针对资源绑定优化给出的解决方案。
我们在提交drawcall前会绑定相关的资源大量的资源绑定在CPU上非常耗时的。metal给出的解决思路是将所有绑定的资源记录在Argument Buffer中而Encoder只需要绑定这个Buffer。
在Argument Buffer 中我们可以绑定如下的内容
● 纹理缓冲区采样器内联常量标量向量矩阵等
相比起传统绑定Argument Buffer绑定可能存在如下好处
● 减少绑定次数
● 对于多帧不变的绑定Arugument Buffer可以缓存下来无需频繁更新
使用了参数缓冲区进行绑定后我们在shader中对资源的访问会有所变化从直接读取资源转换为通过下标和结构体来访问。
metal支持在CPU和GPU中对Argument Buffer进行编码GPU编码可以与ICB结合进行GPU驱动渲染。
GPU优化
metal在XCode中提供了相当完善的GPU性能分析工具包括Capture, Trace和Counter。我们可以通过Counter关注一些GPU可能存在的性能热点。以下内容整理自WWDC的介绍。
ALU性能
ALU主要负责处理着色器中的数学计算包括位运算和关系运算等。
为了优化ALU模块我们可以考虑对浮点计算进行优化比如使用16位浮点数来代替32位浮点数使用近似公式如菲涅尔近似公式查找表如预积分的LUT考虑优化分支耦合执行相同的指令尽可能不要安排小于GPU最小工作单元的任务32个线程等等。
执行效率
16-bit floating point(2) > 32-bit interger add/sub/conditionals(2) > 32-bit floating point (1)> 32-bit integer(0.5) > complex(log2, exp2)
优化分支
耦合执行所有线程符合相同的条件a所有线程执行相同的指令
差异执行线程对于条件a不一致一些线程执行不同的指令
概括来说执行不同指令的分支后总体的执行时间约为不含分支的两倍
纹理读取
纹理保存在设备内存中每个着色核心包含了专用的纹理单元L1缓存由纹理单元负责纹理的读取。
为了优化纹理访问的效率我们可以考虑mipmap降低分辨率修改过滤选项更小的像素格式或者进行纹理压缩ASTC。
纹理处理单元TPU
● 为Attachment执行MTLLoadActionLoad
● 着色器读取/采样
对gather操作和常规像素格式访问做了优化
采样速率
Conventional 32-bit formats1 > RG11B10Float/RGB9E5Float1 > YUV0.5 > 128-bitformat(0.25
纹理过滤速率
1D/2D,Cube Nearest/Linear(1.0x)
1D,2D,Cube Mipmap Linear(0.5x)
3D Nearest/Linear (0.5x)
3D Nearest Linear Mipmap(0.25x)
2D 8xAnisotropy(0.125x)
Shadow(pcf) (0.5x)
纹理压缩
● 块状压缩ASTC, PVRTC) - 美术资产
● 无损压缩 - 运行时纹理
纹理写入
着色核心中像素后端负责纹理的写入。
为了优化纹理写入的效率我们可以考虑降低纹理的分辨率并优化分歧写入。
像素后端
● 为Attachment执行MTLLoadActionStore
● 着色器写入纹理
对一致写入做了优化
写入效率
81632-bit formats(1.0x) > 64-bit formats(0.5x) > 128-bit formats(0.25x)
分块内存
分块内存TileMemory存储了threadgroup计算管道和Imageblock图形管道数据是着色核心内部的高速缓存。
使用了过多的分块内存可能会导致可并行核心数下降我们可以考虑减少并行线程组的数量或者使用一些SIMD操作此外还可以调整内存访问的顺序如让邻近线程访问邻近元素并避免多个线程访问同一位置。
使用情况
● 从Theadgroup或Imageblock读取数据
● 读写color attachment
● 执行混合操作
在将结果最终写入到系统内存前数据暂存在分块内存中。
缓冲区
缓冲区由设备内存记录并由着色核心访问它支持不同的地址空间。
为了优化缓冲区读写我们可以更紧密地打包数据如使用更小的类型并使用向量读写float4此外应该避免寄存器溢出register spills如移除一些动态下标。
地址空间
● 设备读写非缓存
● 常量只读缓存预捕获
GPU末级缓存
GPU末级缓存GPU Last Level Cache是由所有GPU核心共享的缓存它用于
● 缓存纹理和缓冲区数据
● 缓存设备量(device atomics)
为了优化GPU Last Level Cache的性能我们可以先优化纹理和缓冲区的性能并使得着色器优先使用线程组变量而非设备变量并在一个尽可能小的空间访问内存。
设备内存
记录在系统内存用于存储
● 纹理数据
● 缓冲区数据
● Tile顶点缓冲区Tile阶段的输出
可由GPU Last Level Cache缓存。
内存带宽
衡量了GPU和系统之间的内存传输GPU在以下情况访问系统内存
● 访问缓冲区
● 访问纹理
为了降低带宽我们可以先优化纹理和缓冲区的性能并只加载当前pass需要的数据和存储未来pass需要的数据并使用纹理压缩。
GPU利用率
GPU通过在不同线程之间切换隐藏延迟在以下情况下GPU会创建新线程
● 有足够的内部资源
● 有可以调度的命令
GPU利用率描述了GPU的每个核心处于忙碌状态的比例我们可以通过计算占用率顶点占用率和片元占用率来衡量GPU占用率。
如果整体的占用率较低可能是因为
● 着色器耗尽了内部资源
● 线程完成速度快于线程创建速度
● 渲染小区域或调度小的计算任务
片元输入插值器
Fragment Input Interpolar是着色核心完成的它的内部有专门的片元输入插值器是固定的功能并且是全精度的。
我们可以减少移除给片元着色器的顶点属性来对其进行优化。
HSR
HSR这一阶段使得非透明物件的提交顺序不影响最终进入片元计算的像素即规避了overdraw。
我们可以关注执行片元的像素(FS invocations)和写入的像素(pixels store)之间的比例来关注overdraw的情况。
为了使得HSR更好地应用我们应该按照如下顺序绘制模型
● 不透明物件
● alpha测试/discard/depth feedback的物件
● 透明物件
总结
metal在工作提交、显存管理上和dx12,vulkan这样的API设计比较接近而在资源管理和绑定上的设计差异较大这使得我们在考虑跨平台的应用时需要充分理解API层面的设计。此外Apple的开发者文档较为详细且完善并且每年也有开发者大会对整个API的设计以及一些新推出的特性进行介绍这也是值得我们持续关注和学习的。