D3D12将资源状态管理从图形API层移交到应用层,迫使我们自己来管理资源的状态,我们不仅要正确的使用资源状态转换,还要保证转换的性能,这就需要我们深入的了解D3D12资源状态转换的一些规则。让我们先来看看D3D12都有哪些应用场景。
- Transition barrier 最常用的资源状态转换,比如RT->SRV。RT是写状态,SRV是读状态,因此我们需要建立一个转换屏障,保证写操作完成后再去读取资源。
- Aliasing barrier D3D12允许一块堆内存被多个资源占用,这可以提高内存的利用率。因此我们需要一个别名屏障来保证同一时间只有一个资源在使用该内存。
- Unordered access view (UAV) barrier 有的时候我们希望一个UAV先写完再读,虽然这样很低效,但是能保证结果是正确的。因此我们需要一个UAV屏障来保证写完再读。这个和转换屏障很类似,可以理解为转换屏障的UAV版本。
Barrier在底层导致做了什么?
- change state of memory for resource 首先它会改变内存的状态,比如从RT状态改变到SRV状态。写状态和读状态可能要求不同的数据格式,这取决于具体的资源类型及显卡的实现。
- cache flushes 通常为了数据一致性,需要在转换的时候刷新缓存。
- pipeline statlls 状态转换经常会造成gpu的空闲等待,比如在等待写完成。gpu内部其实也是分多个模块的,比如传输模块,计算模块。一个资源转换可能只占用传输模块,计算模块此时如果也被强制等待,就会造成gpu的资源浪费。再比如D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE和D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE。为什么将读状态拆分成像素着色器使用还是非像素着色器使用呢?一部分原因就是告诉显卡,如果发生资源状体转换,那么请只阻塞相应的模块,而没必要将整个gpu都阻塞。
- 还有一些其他工作比如对内存的解压等等,为什么会有内存的解压,我没做过驱动层,只能感性理解,比如写内存的时候,不需要随机读取,因此我们可以压缩内存,减少带宽的使用,当读的时候就需要把它解压缩加速读取速度。这还取决于硬件是否支持。
综上所述Barrier是一个很昂贵的api调用。如何优化Barrier的使用是一个复杂的问题,有几个基本的准则如下:
尽量减少Barrier的使用
尽量减少Barrier的使用,这貌似是一句废话,但是里面的细节却很重要。对于资源的状态转换,有些时候必须显示转换,有些时候不需显示转换,如果你显示转换了,那么就可能造成不必要的flushes和stalls。换句话说有些资源状态的转换是顺理成章的,应用层,驱动层,都不需要进行任何操作,硬件会很自然的正确使用资源,如果你此时强制使用了Barrier就可能强迫GPU进行不必要的flushes和stalls。那么问题来了,哪些资源转换是可以省略的?D3D12将这种省略的状态转换称为Implicit state transitions:"Resources can only be "promoted" out of D3D12_RESOURCE_STATE_COMMON. Similarly, resources will only "decay" to D3D12_RESOURCE_STATE_COMMON."对于隐式提升和衰减都是围绕D3D12_RESOURCE_STATE_COMMON状态进行的。为了方便理解隐式状态转换,D3D12将资源分成两类,一类是Buffer和Simultaneous-Access Textures,这类资源可以从common状态提升到任何状态,除了Depth-stencil resources,因为它必须是non-simultaneous-access textures。这类资源可以连续进行隐式资源转换,比如先转换成读,再转换成写,这些转换都不需要显式Barrier。另一类是Non-Simultaneous-Access Textures,这类资源只能隐式转换到某些状态,具体参考D3D12文档,而且一旦提升到写状态后,就不能继续隐式转换了,但是提升到读后是可以继续隐式转换的。当一个ExecuteCommandLists执行完毕后,以下情况的资源会自动退回到common状态(注意当ExecuteCommandLists调用多个CLS时是无法进行自动回退的。这里就有一个鱼和熊掌的问题了,D3D12要求我们尽可能的合并ExecuteCommandLists的调用,因为可以降低DDI层的消耗,因为调用这个API,操作系统会从用户态切换到系统态,另外驱动层也可以进行一些优化,比如去掉冗余的命令,让显卡异步调用起来。但是合并的ExecuteCommandLists的调用会阻止common状态的回退,导致我们可能需要显式状态转换)
- 任何资源被拷贝队列使用
- buffer资源或被标记为D3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESS状态的贴图资源在任何队列中使用
- 任何资源隐式提升到只读状态
其实可以简单理解Common状态的资源,这类资源需要满足以下两个条件:
1) Have no pending write operations, cache flushes or layout changes.
2) Have a layout that is intrinsically readable by any GPU operation.
因为D3D12标准要求ExecuteCommandLists执行后不允许有任何未完成的操作包括cache flushes and layout changes。因此ExecuteCommandLists之后的资源只需要满足第2条就可以回退到common状态。
因为Buffers and Simultaneous-Access Textures必须保证所有gpu操作都可以读,而且写操作不允许改变layout,因此只要它们没有被使用就会退回common状态。正是因为这个原因,这类资源可以连续进行隐式转换。
总结能隐式转换的时候不要显式转换
除非必须,不要转换到D3D12_RESOURCE_STATE_GENERIC_READ和D3D12_RESOURCE_STATE_COMMON状态
转换到D3D12_RESOURCE_STATE_GENERIC_READ将会导致者整个gpu流水线的flushes和stalls。因为D3D12_RESOURCE_STATE_GENERIC_READ代表了整个流水线都可以Read。只有上传堆资源可以使用D3D12_RESOURCE_STATE_GENERIC_READ状态,因为上传只涉及到cpu的拷贝,占用的是cpu timeline。
不允许进行read to read 的转换,这会增加转换的开销,可以将其转换到combine read state。
我们希望D3D12_RESOURCE_STATE_COMMON状态可以隐式转换,因此不建议显示转换到common状态,除非必要的时候。一种情况是在copy queue中使用的资源,如果之前是在graphic or compute engine使用的资源,在copy queue使用之前需要将其转换到common state,反之亦然。另一种情况是cpu需要访问的贴图资源,需要将资源放入到 CPU-visible heap中,并且资源的状态需要是common状态。
尽量batch Barrier
可以减少DDI的调用,驱动层也可以尝试进行优化,比如去掉冗余的状态转换
使用Split Barriers
如果一个转换过程不需要立即转换,我们可以把这个转换过程的实际时间拉长,通过start和end告诉驱动,在start和end期间可以安排异步执行的任务。比如D3D12_RESOURCE_STATE_DEPTH_WRITE状态转换到D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE。深度写转换到pixel读的时候,占用大量的带宽资源,但是ALU相对空闲,如果不使用split barriers,gpu资源就会浪费ALU部分。如果使用split barriers,驱动程序就有可能会安排一些ALU的任务同步执行。
我们可以利用D3D12的debug layer帮组我们正确的使用Barriers,比如AssertResourceState API。
理解了以上内容,也就理解了AMD和Nivida对于barrier的使用建议,最后附上它们的使用建议。
AMD Barriers
Barriers are how dependencies between operations are conveyed to the API and driver. Barriers open up a whole new world of operations by allowing the application to decide if the GPU can overlap work. They are also an easy way to slow down the rendering by adding too many barriers or can cause corruptions from not having correct resource transitions. The validation layers can often help with identifying missing barriers.
- Minimize the number of barriers used per frame.
- Barriers can drain the GPU of work.
- Don’t issue read to read barriers. Transition the resource into the correct state the first time.
- Batch groups of barriers into a single call to reduce overhead of barriers.
- This creates less calls into the driver and allows the driver to remove redundant operations.
- Avoid
GENERAL
/COMMON
layouts unless required.- Always use the optimized state for your usage.
Nivida
- Minimize the use of barriers and fences
- We have seen redundant barriers and associated wait for idle operations as a major performance problem for DX11 to DX12 ports
- The DX11 driver is doing a great job of reducing barriers – now under DX12 you need to do it
- Any barrier or fence can limit parallelism
- We have seen redundant barriers and associated wait for idle operations as a major performance problem for DX11 to DX12 ports
- Make sure to always use the minimum set of resource usage flags
- Stay away from using D3D12_RESOURCE_USAGE_GENERIC_READ unless you really need every single flag that is set in this combination of flags
- Redundant flags may trigger redundant flushes and stalls and slow down your game unnecessarily
- To reiterate: We have seen redundant and/or overly conservative barrier flags and their associated wait for idle operations as a major performance problem for DX11 to DX12 ports.
- Specify the minimum set of targets in ID3D12CommandList::ResourceBarrier
- Adding false dependencies adds redundancy
- Group barriers in one call to ID3D12CommandList::ResourceBarrier
- This way the worst case can be picked instead of sequentially going through all barriers
- Use split barriers when possible
- Use the _BEGIN_ONLY/_END_ONLY flags
- This helps the driver doing a more efficient job
- Do use fences to signal events/advance across calls to ExecuteCommandLists
Dont's
- Don’t insert redundant barriers
- This limits parallelism
- A transition from D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE to D3D12_RESOURCE_STATE_RENDER_TARGET and back without any draw calls in-between is redundant
- Avoid read-to-read barriers
- Get the resource in the right state for all subsequent reads
- Don’t use D3D12_RESOURCE_USAGE_GENERIC_READ without good reason.
- For transitions from write-to-read states, ensure the transition target is inclusive of all required read states needed before the next transition to write. This is done from the API by combining read state flags– and is preferred over transitioning from read-to-read in subsequent ResourceBarrier calls.
- Don’t sequentially call ID3D12CommandList::ResourceBarrier with just one barrier
- This doesn’t allow the driver to pick the worst case of a set of barriers
- Don’t expect fences to trigger signals/advance at a finer granularity then once per ExecuteCommandLists call.