你知道应该在什么场合承认自己的渺小吗?
在智慧面前,在美面前,在大自然面前,
唯独不是在人群面前,
在人群中应该意识到自己的尊严。 - 契诃夫 -
NVIDIA发布了DX12 推荐 And Don'ts,即DX12使用上的一些建议,因为其中很多建议对日常工作非常有帮助,为了降低后续查找资料的难度,特此进行翻译,并将个人理解加入其中,大家如果看到有理解存在问题的地方还请不吝赐教。
Introduction
相对于之前的DX API,DX 12给了开发者更多的自由度,同时也带来了更重大的责任。从resource state barriers到使用fence对command queues进行同步,无不表明了这个理念。
非法的API调用异常将不会被捕捉或者纠正,因此要想得到正确的效果,开发者在使用的过程中需要用好debug runtime工具,并且留意任何的报错信息,此外还需要对DX 12的Spcification文档有非常熟练的理解。
这里整理了NVIDIA在实践过程中总结的DX12一些使用tips,根据所从属的类型不同,做了分别阐述。
1. Engine Architecture/Structure
推荐
- 对于并行(多线程)的DrawCall提交,推荐使用tasks graph architecture
- 这样就可以在充分考虑resource跟command queue的依赖关系的同时很好的兼顾到Draw Submission的并行性:意思是基于task graph来实现DrawCall的并行提交,可以保证依赖关系不会出问题
- 推荐使用一个‘Master Render Thread’完成work submission,同时使用若干‘Worker Threads’进行command list的记录、资源创建以及PSO ‘Pipeline Stata Object’ (PSO) 的编译。worker线程相当于生产者,Render线程相当于消费者。
- 推荐为不同的硬件厂商(IHV,Independent Hardware Vendors)提供不同的渲染路径(?工作量有点大?)
- 从而方便(还是避免?)app通过推理来寻找到当前硬件下的最佳执行路径
不要
- 不要依赖驱动在驱动线程中实现DX12相关工作的并行处理,而是尽量将之放在work submission的worker线程中
- 在DX11中,驱动确实会将一些异步任务分散到驱动的worker线程中,而这种做法在DX12中将不会发生(是不能,还是不推荐?)
- 虽然DX12中work submission的总体消耗是下降了,但是由于将驱动线程从应用线程中剥离出来(为什么要剥离?),因此应用线程的总体工作量并没有下降,反而有所增长。基于硬件(CPU Cores)并行的work submission并行越高效,draw call submission的性能提升就越明显。
2. Work Submission – Command Lists & Bundles
推荐
-
要明白在DX12中,需要开发者自己来保证与控制GPU/CPU之间的并行计算效果。
- 提交command lists并不会在GPU上触发任何计算操作
- 只有最终调用ExecuteCommadList()接口的时候,GPU才会开始工作
-
使用并行的方式完成渲染指令的提交,并且最好使得各个提交线程的负载是均衡的。
- 渲染指令的录制(或者说填充,Recording)是一项高CPU消耗的工作,且这项工作无法转交给驱动线程完成
- Command lists无法被多个线程共享,因此多线程提交的意思就是,各个线程分别提交到不同的Command List上去
-
需要注意,Command List的Reset以及Setup都是有消耗的
- 虽然如此,为了实现高效的并行提交,一个合理数目的Command Lists还是有必要的
- Fences会因为各种原因(多个Command Queues,获取查询的结果等)触发Command Lists的强制拆分
Command Lists的数量最好控制在15~30之间,或者更低;另外,每帧可以考虑将这些Command Lists(缩写CLs)转换成5~10次ExecuteCommandLists()调用
-
如果可以的话,尽量对bundles中记录的fragments进行重用
- 重用可以避免重复的CPU消耗
-
建议通过稀疏的方式(什么叫稀疏的方式?)实现bundle resource跟inheritance之间的绑定(bundle resource是什么,inheritance是什么,两者为什么要绑定?)
- 由于利用了更多的完全烘焙好的bundles(thoroughly cooked bundles,烘焙好是什么意思?),因此通过这种方式可以以较低的overhead来实现bundle的重用。
- 在使用一个单独的compute command queue的时候,需要仔细考虑这种做法是否真的有帮助(意思是这种做法虽然在某些情况下可以有所收益,也会因为操作不当而出现性能下降?)
虽然从理论上来说,Compute Task跟Graphic Task是可以并行执行的,但是GPU内部并行执行与规划的细节也可能导致最终的结果跟预期存在一些差异。
需要清楚知道哪些异步Compute Task跟哪些Graphics Task是可以融洽的并行执行的,并通过Fence来将这些Task组合起来执行
如果我们希望异步Compute Task与Graphics Task能够很好的并行执行,就需要确保所有frames都使用同一个CBV/SRV/UAV/descriptor heap表示的ring-buffer(为什么?)
不要
-
每个bundles不建议记录过多的 draw calls (e.g.~12 draw calls比较合适)
- draw call过多会导致bundle的重用困难
-
不要将Graphics Queue中的Compute Task跟专属的Compute Queue中的Compute Task重叠在一起(意思是并行执行?)
- 这种做法会导致异步compute queue中存在孔洞(意思是在执行的时候会需要等待,导致不能真正做到并行?)
- 在这种情况下,尽量将需要放在Graphics Queue中Compute Task转换成Graphic Task
-
不要提交尺寸过小的command lists.
- 小尺寸的command lists可能会导致其执行时间过短,甚至比操作系统操控CPU提交一个新的Command List所需要的时间更短,从而导致CPU的时钟浪费。
- 在上一个ExecuteCommandLists调用之后,操作系统需要花费50-80 ms来触发command lists,如果某个command list的执行时间比这个快的话,就会导致硬件Queue中存在孔洞(闲置)
- 可以通过GPUView来查看是否有CPU浪费
-
不要将整个场景的渲染塞入到少数几个Command List中
- Command List数目过少可能无法有效利用所有的CPU Core(即可能存在某些Core闲置)
- 此外,一个过大的Command List也就意味着GPU可能会有很大一部分时间处于闲置状态(等待Command List的Recording)
-
不要等到所有的渲染指令都记录完成之后,才进行Command List的提交
- 这种做法就会导致GPU处于闲置(不会执行上一帧的内容吗?),从而无法很好的实现CPU跟GPU之间的并行
-
通常情况下只有少量Command List可以重用
- 从物件可见性角度来看,帧与帧之间其实是有很多的变化的
- Post-processing的Command List的重用性比较好
不要创建过多的线程,或者过多的Command List
线程数过多会导致CPU的资源被挤占反而降低使用效率,Command List过多则会导致过高的overhead。
3. Pipeline State Objects (PSOs)
推荐
-
建议通过worker线程以异步方式完成PSO的创建
- 创建PSO时会触发shader的编译,因而会引发相关的阻塞
-
实际开发中,可以考虑先使用一些更为通用的PSO(通用Shader编译速度会快很多)之后基于通用PSO来生成专用的PSO(怎么做呢?)
- 从通用PSO到专用PSO的生成是需要手动完成的,驱动不会自动帮你完成这个工作
-
尽量避免在运行时编译PSO,否则会导致阻塞
- 即使驱动自带的shader disk cache机制会进行一些优化来提升性能
-
尽可能的减少PSO之间的状态切换
- 每个PSO并不一定会对应于GPU上的一个原子状态切换(atomic state change on the GPU)
-
对于PSO中无关紧要的字段,可以考虑替换成与上一个PSO相同的数值,以降低切换导致的消耗
- 这种做法可以更好的实现PSO的重用
-
尽可能的使用/all_resources_bound / D3DCOMPILE_ALL_RESOURCES_BOUND编译
- 这个flag可以更方便编译器对贴图访问进行优化,经过验证,打开这个开关可以实现至少1%的帧率提升。
不要
- 如无十分必要,尽量避免在同一个Command Queue中混杂Compute Task跟Graphics Task
- Compute跟Graphics Task之间的PSO切换成本比较高
- 如无十分必要,尽量避免开开关关tessellation
- 这种切换成本也很高
- 要牢记,PSO的创建会引发shader的编译以及阻塞
- 因此尽量使用异步编译,且提前编译的方式完成PSO的创建
- 此外,PSO的编译线程的优先级可以适当调低
- 如果不紧急的话,就使用idle优先级,以避免对游戏线程的干扰
- 如果着急的话,可以临时提高一下优先级
4. Root Signatures
在DirectX 12(DX12)中,Root Signature是一种描述Shader(例如顶点、像素或计算着色器)如何访问常量缓冲区(CBV)、着色器资源视图(SRV)和无序访问视图(UAV)等资源的数据结构。Root Signature定义了Shader所需的输入资源的布局,并在GPU和CPU之间建立了一种高效的通信机制。这有助于提高渲染性能,因为它允许开发者更好地控制资源的绑定和访问,Root Signature的使用流程总结如下:
创建Root Signature:首先需要定义一个Root Signature,其中包含Shader所需的所有资源绑定。这包括CBV、SRV、UAV以及采样器等。这些资源可以直接绑定到Root Signature中,或者通过描述符表(Descriptor Table)进行间接绑定。
编译Shader:在创建了Root Signature之后,开发者需要编译Shader程序,使其与Root Signature兼容。这意味着Shader的输入签名必须与Root Signature的布局相匹配。
绑定Root Signature:在创建了Root Signature并编译了与之兼容的Shader之后,开发者需要将Root Signature绑定到命令列表。这允许GPU在执行命令列表时知道如何访问Shader所需的资源。
设置描述符:接下来,开发者需要为Root Signature中的每个资源绑定设置描述符。描述符是一个数据结构,包含了指向资源(如纹理、缓冲区等)的指针。这些描述符可以存储在描述符堆(Descriptor Heap)中,以便在运行时进行高效访问。
绑定描述符:将描述符绑定到Root Signature中的资源槽。这可以通过命令列表中的SetGraphicsRootDescriptorTable或SetComputeRootDescriptorTable方法完成。
执行命令列表:最后,将命令列表提交给GPU以进行执行。在执行过程中,GPU将根据Root Signature中的描述符访问所需的资源,并根据Shader进行渲染或计算操作。
推荐
- 在NVIDIA的硬件上,尽可能的将常量以及CBVs直接放进到root signature中。
- 可以考虑先从PS开始
- 在NVIDIA的硬件上,常量(包括CBVs)放在root signature中会极大的提升性能,尤其是那些用于控制shader执行逻辑的开关常量
- 这种做法还可以降低shader stages的执行频率?(怎么做到的)
- 将CBVs放在root signature中,就不在需要一个descriptor heap来存储descriptor数据,版本入口(version entries)或者额外的indirection数据(即不需要再调用CreateConstantBufferView()接口 )
- 不过使用的时候需要注意,root view不会做越界检查,也没有其他使用限制,因此自己在使用的时候要注意进行合法性检查。
- 可以考虑先从PS开始
- 在使用的时候,注意将常量, CBVs, SRVs and UAVs的当前值缓存在系统存储空间中,只有当真正检测到数据变动时,才修正root signature的内容(不是十分理解),这种做法可以有效提升性能。
- 只对那些必要的stage开放CBVs, SRVs 以及UAVs的可见性
- 要打开这些View的可见性,会对驱动以及GPU产生一些overhead
- 可以通过DENY_*_ACCESS flags来显示资源的可见性
- 降低Root Signature的变化频率
- Root Signature的变化不会有太高消耗,但是因此而导致的Root Signature的Entry的初始化消耗却不能忽略
- 在Tier 1的硬件上处理CBV, UAV, SRV以及Sampler descriptors,而在Tier 2的硬件上处理CBV以及UAV descriptors
- 对这些Tiers的硬件而言,当command list执行的时候,即使shader不需要引用这些descriptors,应用层也要将所有定义在root signature中的descriptors以及descriptor table填好
- 对Tier 3而言,也要将不需要使用的descriptors绑定好,不过不需要浪费时间进行解绑,因为解绑会很容易导致state thrashing bottlenecks。
不要
- 不要将具有不同更新频率的CBVs合入到同一个CBV descriptor tables
- 理论上来说,一个table中的所有CBVs具有相同的更新频率是最好的
- 为了对root signature以及descriptor table进行重用,需要将二者保持在一个不要过大的尺寸
- 对每套材质,对其进行收缩以得到一套最小的entries。
- 不要在root table entries上对同一个shader stage设置visible与deny flags
- 当前驱动下,只有D3D12_SHADER_VISIBILITY_ALL打开之后deny flag才会生效。
- 除了有大量draw call或者dispatch call需要用到常量SRV/UAV的情况,不要将常量SRV/UAV直接放到root signature中。
- 在对Root Signature进行修改之后,记得检查resource bindings的状态,确保不要出现undefined
- 改动root signatures会移除或者清理掉在上一个root signature中用到的resource的binding state
5. Allocators and Lists
推荐
- 在具有相同或者相近draw call数的时候,可以考虑对allocators进行重用(不相近的话就不能重用?)
- 预热可以对Allocation进行加速
- 最少需要2*T + N 个Allocators
- 2指的是,一套lists/allocators是上一帧的,在当前帧依然会被GPU消耗,第二套则是当前帧在构造的
- T指的是会创建command list的线程数(注意,allocators不是free threaded,即只有满足条件的线程才能使用)
- N指的是为bundles额外增加的allocators pool大小
- 在下一帧重用之前,先调用Call Allocator::Reset
- 否则allocator的尺寸会不断增长,直到内存消耗殆尽
不要
- 需要谨记Allocator跟Lists都会消耗显存
- 如果Allocators尺寸过大,会导致GPU的工作方式与工作效果存在异常
- 不要频繁创建与销毁Allocators而是要考虑重用以避免创造与销毁带来的overhead
- 对于尺寸差异过大的draw call序列,就不要想着重用Allocators了
- 因为这回导致allocator的尺寸不匹配实际使用情况而造成的浪费
- 在对一套command list进行reset之后,不要忘了对与之对应的allocators也做相应的reset
- 如果不对allocators进行reset也就相当于造成了内存泄漏
- 不要对依然被活跃的command list使用的allocators进行重用(或者释放)
- 这种操作是非法的,并且会导致正在被使用的内存(显存)被释放或者覆盖,从而导致表现异常
6. Resources
推荐
- 要避免显存的overcommitment
- 使用IDXGIAdapter3:: QueryVideoMemoryInfo()查询当前可用显存的精确信息
- Foreground应用不需要将所有的显存,或者高比例的显存空间都分配掉
- 注意跟随操作系统分配的budget而变化
- 可以考虑使用IDXGIAdapter3::RegisterVideoMemoryBudgetChangeNotificationEvent
- 可以考虑基于可用的存储空间来对graphics settings进行调整(意思是graphics settings中可用的显存空间会跟随系统分配的budget动态变化?)
- 在系统内存中创建溢出heaps,并在显存不够时将一些使用频率不高的资源从显存移除放到系统内存中。
- 相对于DX11,DX12为app提供了内存管理上的便利
- 可以将command list拆分,从而使得每个command list都有与之匹配的可用显存
- 可以追踪到每个Command List中用到的内容
- 当显存快要超过预算时,可以考虑在执行command list前后使用MakeResident/Evict做一下清理
- 尽可能的使用committed类型的资源,这类资源可以提供给driver更多的信息:
- 从而允许驱动更好的管理显存
- Placed类型资源的一个好的使用方式是,比如终其一生只将不同的只读贴图存入到资源heap中。
- 将MakeResident calls 合到一起(由于page table updates,CPU跟GPU都会有一些消耗)
- 可以降低driver跟GPU的overhead
- 通过MakeResident/MakeUnresident来匹配给定尺寸的显存预算
- 在必要的时候丢弃tiled resources的mip levels
- 需要处理MakeResident调用失败的情况
- 需要注意,在同一个heap中,某种资源类型可能会有不同的alignment规则
- 实际工作中需要考虑在同一个device feature level下处理可变(即存在不同)的resource binding Tiers
- 所有stages的UAV数目可能会需要限制在8或者64
- CBV数则可能被限制在每个stage 14个
- Sampler数则可能被限制在每个stage 16 个
- 需要关注heap的aliasing规则
- 需要的时候可以看下tiled resource的specification
- 需要注意对于资源、SRVs、DSVs等而言,存在多种不同的heap类型。
- 在某些heap tiers上,其限制规则会比其他tiers更严格
- 要对资源的heap tier的能力进行check
- 在使用CopyTextureRegion接口拷贝DS贴图的时候,要注意填写合适的D3D12_TEXTURE_COPY_LOCATION
- 只拷贝depth部分会使得拷贝时间消耗较久(反直觉)
不要
- DS与RT的Placed Resources的重用会比较麻烦
- 在上层逻辑中对这些资源进行clear以实现重用的时候,会有这样一种可能,即这些资源可能会被其他硬件操作所依赖,从而导致对这些资源的清理与重用变得非常昂贵。
- 不要过于相信tiled resource的可用性,最好对cap bits进行check
- 需要考虑不同的DX12的硬件种类(classes?)
- 不要认为或者尝试一次性完成所有显存的分配
- 不要以为MakeUnresident调用会立马得到消耗数据
- 消耗数据可能会被延迟到另一个MakeResident调用触发了对应存储空间的使用时
- 使用GPUView analysis找出延迟的paging requests
- 消耗数据可能会被延迟到另一个MakeResident调用触发了对应存储空间的使用时
- 尽可能的避免资源的销毁与重新创建
- 使用MakeUnresident与MakeResident接口会更好一些
- 可以节省资源创建与销毁的时间
7. Barriers, Fences & Hazards
推荐
-
减少barriers跟fences的使用
- 在DX11/DX12应用中经常看到不必要的barrier使用导致的性能损耗
- DX11驱动会自动完成barriers的优化,在DX12中,这项工作需要手动完成
- 任何Barrier跟fence的使用都会破坏并行性
- 在DX11/DX12应用中经常看到不必要的barrier使用导致的性能损耗
-
尽可能的使用尽可能少的resource usage flags
- 尽量避免使用D3D12_RESOURCE_USAGE_GENERIC_READ flag,除非真的需要将这个联合flag中的每一位都打开。
- 多余的flags会触发多余的flush与stall,从而导致不必要的性能下降
- 在调用ID3D12CommandList::ResourceBarrier时,记得指定尽可能少的targets
- 添加不必要的依赖就会导致冗余
- 将Barriers打包到一次ID3D12CommandList::ResourceBarrier调用
- 从而避免连续不断的barrier导致的阻塞
- 尽可能的使用split barriers
- 使用_BEGIN_ONLY/_END_ONLY flags
- 可以帮助驱动更高效的工作
- 可以使用fences在多个ExecuteCommandLists调用间来signal events/advance
不要
- 不要插入冗余的barriers
- 破坏了并行性
- 举个例子,将资源从D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE 转到D3D12_RESOURCE_STATE_RENDER_TARGET再转回来,而在这两个转换之间没有任何的draw call发生,是完完全全的冗余与浪费
- 避免read-to-read barriers
- 为所有后续的读操作准备好正确的state(不要进行多次到read的转换)
- 没有合理的理由,不要使用D3D12_RESOURCE_USAGE_GENERIC_READ
- 对于需要write-to-read的资源转换,需要确保在转换的时候,转换结果能够覆盖接下来到下一次转成write之前的所有read需求,从API层面,可以通过将多个read flags组合到一起来实现,这里建议将所有后续read所需要的flags都放进去。
- 不要仅仅因为一个barrier而连续多次调用ID3D12CommandList::ResourceBarrier
- Fence触发signals/advance的频率不会高于每个ExecuteCommandLists调用一次
8. Multi GPU
推荐
- 使用DX12的标准check来确认当前机器上有多少个GPU
- 不再需要使用厂商提供的API
- 记得检查CROSS_NODE_SHARING tier
- 自行控制哪些surface需要sync哪些不要
- 可以使用对资源的显示控制能力
- 创建需要在每个node(对应于一个GPU,下同)上都需要synchronized to的资源
- 使用合适的CreationNodeMask
- 将这些资源的可见性开放给需要access的node
- 在需要的时候才将这些资源拷贝到当前node上Copy them to the current node when needed
- 降低必要的同步次数
- 如果设备支持tier 2跨node共享,在使用的时候多跟tier 1类型的实现进行比对。
- 使用专门设计的copy queues完成跨node的拷贝操作
- main queue将并行用作渲染
不要
- 不要尝试从implicit MGPU scaling(不太明白这个词的含义)中得到好处
- 不要指望surface syncs会自动完成
- 如果需要sync,确保开发者对整个过程有完全的控制
9. Swap Chains
推荐
- 使用flip mode(front buffer跟back buffer的来回切换)的swap-chains
- 使用SetFullScreenState(TRUE)加上一个(borderless)全屏窗口以及一个non-windowed的flip model swap-chain切换到一个true immediate independent flip mode(没太看懂)
- 根据微软的描述,这是目前在tearing下(没懂这里的含义),在调用Present(0,0)时唯一能够实现性能飞升的模式。
- 其他模式在tearing下不支持无限的帧率(unlimited frame rates)
- 注意使用DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH flag
- 如果窗口尺寸跟屏幕分辨率保持一致的话,就不需要这个flag来实现无限帧率
- 如果设置了这个flag,在调用SetFullScreenState(TRUE)之前使用ResizeTarget()来调整分辨率可以得到不错的表现,然后我们就可以得到不受限的FPS了。
- 如果不是全屏模式(true immediate independent flip mode),就需要仔细控制swap-chain中的延迟与buffer数来得到满意的FPS与延迟。
- 使用IDXGISwapChain2::SetMaximumFrameLatency(MaxLatency)接口来设置想要的延迟
- 需要打开DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT flag才能正常工作
- DXGI会在我们presented MaxLatency-1次之后开始阻塞
- 这个数值默认是3,也就意味着FPS不能高于2 * RefershRate,如果显示器评率是60HZ,也就意味着我们的帧率不能超过120FPS
- 考虑将swap-chain的buffer数目设置得比目标缓存帧数(需要相应增加command allocators跟动态数据还有相关的frame fences)大1~2,并且将"max frame latency"设置成这个数值(buffer数目)
- 使用IDXGISwapChain2::SetMaximumFrameLatency(MaxLatency)接口来设置想要的延迟
- 如果不是处于全屏模式 (true immediate independent flip mode),可以考虑使用一个waitable object swap-chai搭配WaitForSingleObjectEx()来获得高帧率
- 虽然这种做法可能会导致部分frame完全不可见,不过却是一个获得benchmarking数据的好方法
- 使用waitable object swapchain搭配GetFrameLatencyWaitableObject(),可以知道某个buffer是否可以被present,或者是否可以渲染到其上面,有如下的一些可用选项:
- 使用一个额外的off-screen surface
- 先将画面渲染到off-screen surface,之后将timeout设置为0来测试waitable object 来判断某个buffer是否有效,如果有效的话,就可以将数据拷贝到swap-chain的back buffer上,并present,如果雾效,就重新启动一次画面渲染。
- 使用一个具有3或4个buffer的swapchain
- 直接渲染到back buffer.,在Present之前,先测试一下waitable object是否有效,如果有效,就调用Present(),否则从头开始
不要
-
在DXGI开始阻塞Present()之前,每个swap-chain有最多三个queued frame的限制
- 在创建swapchain的时候打开DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT flag并使用IDXGISwapChain2::SetMaximumFrameLatency来修改默认值
在切换到true immediate indepent flip mode之后,记得通过SetFullScreenState(TRUE)调用ResizeBuffers()
10. SetStablePowerState
- 不要从游戏引擎代码中调用SetStablePowerState(TRUE)
- 在使用之前,建议谨慎考虑是否希望以低性能的代价来实现高稳定,具体可以参考blog中的详细讨论.
- 当且仅当我们需要稳定的结果的时候,才考虑从一个separate standalone的app中调用SetStablePowerState
- 为了避免后续疑问,需要在显著处标注此功能是否已开启(其中一种方式是同时记录时钟数与性能结果,NVIDIA的blog中有一段代码片段给出了如何在N卡上查询GPU的时钟数)
- 在测试其他API的时候,记得使用DX12的API以及NVIDIA的standalone程序来稳定时钟数
- 这里有一篇单独的blog对这个问题有更进一步的讨论,有兴趣的同学可以翻看一下:SetStablePowerState.exe: Disabling GPU Boost on Windows 10 for more deterministic timestamp queries on NVIDIA GPUs
11. DirectX12 Hardware Features and other Maxwell Features
推荐
-
使用硬件conservative(保守)光栅化而非full-speed保守光栅化
- 没必要使用GS来执行一个慢版本的软件光栅化
-
如果可以的话,尽量通过NvAPI (when available)访问其他的Maxwell features
- Advanced Rasterization features
- 为基于quad的几何体准备的Bounding box rasterization mode
- 新的MSAA features如post depth coverage mask以及对coverage mask进行覆盖来修正sub-samples的位置等
- 可编程的MSAA sample位置
- Fast Geometry Shader features
- 在不需要几何体扩增(geometry amplifications)的前提下,只通过一个geometry pass就完成cubemap的显然
- 在不需要几何体扩增(geometry amplifications)的前提下,完成多个viewport的渲染
- 如果需要在PS中拿到每个三角面片数据,可以考虑使用快速执行的GS (fast pass-through geometry shader)
- Advanced Rasterization features
新的interlocked操作
增强型的blending操作
新的texture filtering操作
不要
- 不要大量使用Raster Order View (ROV) 技术
- 保序并不是免费的
- 使用的时候注意与传统的方法(如advanced blending ops and atomics)进行比对
Sampler Feedback Streaming(SFS)
SFS是DX12的一项特性,基于此特性,在渲染的时候,可以拿到实际物体渲染的贴图的各个部分的Mips数据,而基于这个数据,我们可以将贴图划分成不同的tile,之后将mip等级的计算从全图下降到tile级别,从而实现内存消耗的降低与IO的减少。
从上图可以看到,基于此方案实现的Streaming逻辑可以做到内存利用率2.5倍的提升与60% IO的降低。