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
② 纹理单元(TPU)L1 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端常见的IMR(Immediate Rendering)流程不同,Apple GPU为了减少带宽消耗,使用了分块延迟渲染。这意味着当我们绘制几何体的时候,并不会一次性完成整个绘制的流程,而是分为两个部分,第一个部分进行分块,第二个部分进行渲染。
① 分块阶段
把视图分为多个分块(Tile),计算每个分块关联的图元(Primitive),分块的数量和核心数量相关;
对Tile中的图元进行几何变换,对所有顶点执行顶点着色器;
将转换后的图元传给每个分块,计算的结果记录到系统内存中;
② 渲染阶段
初始化Tile中的数据,执行加载(load/clear/dontcare)
对转换后的图元执行光栅化
对所有的所有可见像素执行像素着色器
将Tile中数据写入系统内存,执行保存(store/dontcare)
③ Hidden Surface Removal
特别的,Apple的GPU支持了HSR(Hidden 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), Pixel(Rendering),Compute这三个通道是可以重叠的。
在合理的调度下,我们可以看到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的利用率。
Metal Best Practices Guide: Triple BufferingDescribes the recommended best practices for developing a Metal game or app.https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/TripleBuffering.html
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 Memory,On-chip Memory的写入对带宽消耗很低。
由于这个数据仅存在于线程组中,所以我们只能拿到线程组内的数据,也就是特定Tile内的数据能够相互访问。通常情况下,我们只会访问当前像素位置上的数据。通过线程组内存的使用,我们降低了带宽的消耗,与之而来的代价是使用了过多的线程组内存,这可能会导致可用的线程数下降。
一个非常常用的例子就是移动端的延迟光照计算:
Apple Developer Documentationhttps://developer.apple.com/documentation/metal/metal_sample_code_library/rendering_a_scene_with_deferred_lighting_in_objective-c?language=objc
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中的资源对象包括纹理对象和缓冲区对象。
其中,缓冲区是可以存放任意类型的格式,它是无格式的,我们可以指定其分配的长度。
而纹理是一种格式化的图像数据,包括了Texture1D,Texture2D,Texture3D, 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资源:
Apple Developer Documentationhttps://developer.apple.com/documentation/metal/metal_sample_code_library/streaming_large_images_with_metal_sparse_textures
创建堆时,需要我们指定堆的属性,包括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内。
Apple Developer Documentationhttps://developer.apple.com/documentation/metal/memory_heaps/implementing_a_multistage_image_filter_using_heaps_and_fences
Event
另外一种event同步,是在Command Buffer中指定的,它可以处理更高级别的同步,包括跨Command Buffer的同步,跨设备的同步以及CPU和GPU的同步。
这包括了MTLEvent和MTLSharedEvent,其中,MTLEvent用于GPU内部的同步,MTLSharedEvent用于CPU和GPU的同步。可共享事件的开销要高于不可共享事件。
事件可以包含一个信号量(Semaphore),这是一个无符号的64位整数,调用encodeSignalEvent,我们会增加信号量的值,表明工作负载已经完成。调用encodeWaitForEvent, 我们会减少信号量的值,表明等待工作负载的完成,等待仅在信号量小于特定值的时候发生,这意味着工作尚未完成。
我们也可以直接调用Event的Signal和Wait来管理同步。
Apple Developer Documentationhttps://developer.apple.com/documentation/metal/memory_heaps/implementing_a_multistage_image_filter_using_heaps_and_events
资源传输
在开发过程中,我们会频繁地将资源在CPU和GPU之间传输,这可以通过Blit Command Encoder来完成,它提供了如下功能:
● 填充缓冲区(fill)
● 在缓冲区之间,纹理之间,缓冲区和纹理之间复制数据 (copy)
● 优化内存布局(optimizeContentsForGPUAcces, optimizeContentsForCPUAccess)
● 生成纹理mipmap(generateMipmaps)
如果我们想要从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 formats(1) > RG11B10Float/RGB9E5Float(1) > YUV(0.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
● 着色器写入纹理
对一致写入做了优化
写入效率
8,16,32-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的设计,以及一些新推出的特性进行介绍,这也是值得我们持续关注和学习的。