【GDC 2016】Practical DirectX 12- Programming Model and Hardware Capabilities

有一个叫做“选择困境”的心理学理论,人们害怕选择的理由主要有两个:

  1. 因为要为选择承担责任而焦虑
  2. 因为放弃了另外一种选择而担忧

—— 弗洛姆《逃避自由》

今天要学习的是关于DX12相关技术能力的GDC分享,在这里可以学到DX12的最佳实践以及DX12的硬件能力两大部分,文章的陈述由一个个的要点组成,各个要点并行存在。

1. Work Submission

Work Submission对应的是渲染数据的提交,这部分包含了四个小节:

  • Multi Threading
  • Command Lists
  • Bundles
  • Command Queues

下面分别进行陈述。

1.1 Multi Threading

这里给出了DX11跟DX12的区别:

  • DX11区分了渲染线程跟驱动线程(CPU),其中渲染线程从游戏线程中获取数据并提交DrawCall(生产者),驱动线程负责将DrawCall数据上传到GPU并触发对应的操作(消费者)
  • DX12不再需要对worker线程(分析下文推测,worker线程用于实现Command List的构建或者Command Buffer的提交,在此前,这个环节会是一个瓶颈)进行加速,而是通过CommandList接口直接完成Command Buffer的提交

在DX12 API下,要想得到较好的表现,就需要保证引擎的逻辑能够跟随CPU Core数自动伸缩:

  • 采用Task Graph的代码架构是最佳的
  • 只需要一个渲染线程用于提交Command Lists
  • 需要多个worker线程并行构建Command List

1.2 Command Lists

在其他Command List被渲染线程提交的时候,新的Command List不会受到影响(多个Command List相互并列,解耦),这个对我们的启发是:

  • 在Command List提交或者Present的时候,不需要进入idle状态,还可以继续工作,避免空转
  • Command List是支持重用的,但是应用层需要自行维护Command List的生命周期,避免使用上的竞争与冲突

这里也说到了,不建议将渲染拆分成过多的Command List,具体原因后面有解释,大致意思是Command List的提交也是有消耗的,也就是说提交一次比提交两次消耗低。

这里给出一个推荐的使用数据:

  • 每帧大约维持15 ~ 30个Command List
  • 每帧调用5 ~ 10次ExecuteCommandList接口

每次执行ExecuteCommandList接口都有一个固定的消耗,即这个调用会自动触发一次flush,因此推荐尽量将多个CommandList合并提交。

每次ExecuteCommandList接口调用最少需要消耗200微秒(用于判定当前CommandList是否过于短暂),能保持在500微秒左右就更好。

CommandList中的数据足够多可以较好的隐藏操作系统的调度延迟:如果ExecuteCommandList接口调用时间过于短暂,甚至比操作系统scheduler提交一个新的CommandList接口的时间还要短,那么就会导致空转(因为还没来得及提交新的,老的就执行完了,接不上了)

这里给了个示意图来辅助说明。

1.3 Bundles

先来看下什么是Bundles[3]:

Beyond command lists, the API exploits functionality present in GPU hardware by adding a second level of command lists, which are referred to as bundles. The purpose of bundles is to allow apps to group a small number of API commands together for later execution. At bundle creation time, the driver will perform as much pre-processing as is possible to make these cheap to execute later. Bundles are designed to be used and re-used any number of times. Command lists, on the other hand, are typically executed only a single time. However, a command list can be executed multiple times

翻译一下,跟CommandList一样,Bundle也可以看成是对若干DX API Call的组合,只是Bundle可以作为一个整体添加到CommandList中,相当于API调用打组(不确定是否支持嵌套,即Bundle中装Bundle)。Bundle的好处是可以将一系列需要重复执行的API Call打成组(函数封装)后面进行多次调用,打组可以使得整个组的API执行的效率要更高(目测是比单个API执行之和要高,评论区,雨山_4dab
给出了说明,在创建bundle的时候,驱动会执行大量的预处理来加速后续的执行流程,而这些预处理则需要依赖于对后续多个渲染命令的分析,这就是打组的意义)。

这里给了一个案例,上图中,API Call被组合成Bundle,Bundle又被塞入到CommandList中,多个CommandList组合成单个Frame的所有API调用。可以清楚的看到,图中展示了Bundle 0跟Bundle 1都被各自调用了两次,即CommandList 1跟CommandList 2中各调用了一次(不清楚这里具体对应的是什么应用情景,同一组物件出现在多个RenderPass中?或者PostProcessing需要在多个View中做相同调用?)

再回到原文来,这里说到Bundle是一种可以提前进行图形API提交的手段(没看出来,具体怎么个提前方法?),且Bundle是目前GPU执行效率最高的一种手段。Bundle会自动继承CommandList上的States,当然,这种继承是有消耗的,在使用的时候需要注意一下。

在使用得当的情况下,可以提升CPU的性能:

  • NVIDIA的建议是:如果需要对5个以上的API Call进行重复调用
  • AMD的建议是:只有在CPU侧有性能瓶颈的时候才考虑Bundle的使用(意思是除此之外,别无好处?)

1.4 Command Queues

Command Queue有三种,如下图所示,其中3D Queue对应的是传统的Graphics Queue,比如VS/PS等渲染流程的相关调用就存储在这个Queue中;Compute Queue用于存储一些计算相关的操作;Copy Queue顾名思义用于存储一些资源大拷贝操作。

Compute Queue

  • 在使用的时候,要注意方法,使用得当的话,可以达到10%的性能增益。
  • 在使用的时候考虑Compute Queue会是一个更好的选择,在如下的情况下,会得到不错的性能提升:
    • 如果使用的是非异步的compute shader路径
  • 如果compute task的规划做得并不非常仔细
  • 跟hyperthreading(超线程,单物理核当成多逻辑核使用以减少CPU闲置时间)一样的使用规则,两个重数据技术可能会在资源占用上存在竞争关系,比如导致cache命中率下降等问题,可以参考下面的图
  • 如果因为“GPU利用率不高”而尝试Compute Queue方案,首先要做的不是引入Compute Queue,而是先对Compute Shader进行优化,找到GPU利用率不高的根本原因,而非不管三七二十一就采用Compute Queue

为了保证最佳的并行效率,可以考虑在Graphics跟Compute之间做平衡,避免两者在资源使用上的冲突从而导致等待。

在使用的过程中,如果不做仔细的规划,做好Graphics的工作管线与Compute管线之间的配合就可能会导致较差的性能表现。上图给出的就是完全不受限的实施策略,好处是使用简单,不好的地方是控制力差,性能表现上不去,每一帧的性能表现波动较大。

在外部添加Fence来对Graphics管线与Compute管线的各个环节进行对齐,可以得到较为平稳的性能表现,但是需要添加一些额外的控制逻辑。

Copy Queue

  • 可以用于在后台完成一些数据的拷贝工作,不影响Graphics Queue的执行(不需要额外占用Graphics Queue的算力或资源)
  • 可以用于通过PCIE完成资源的传输,这个能力对于多GPU并存时,在不同GPU之间的异步传输非常有帮助
  • 不再需要频繁的去查询Copy Queue的完成状态

NVIDIA的使用建议:在对Depth+Stencil的资源进行拷贝的时候需要注意一下,如果单独对Depth进行拷贝,其性能消耗反而更高

2. Hardware State

包含Pipeline State Objects (PSOs)与Root Signature Tables (RSTs)两部分。

2.1 PSO

Pipeline state overview解释说到,在DX11之前的API中,Graphic Pipeline State是通过单一的接口进行逐一设置的,但是由于各个State之间并不是完全独立的,在设置的时候可能会触发其他的一批设置,从而在设置的时候会存在效率问题。

DX11为了减少需要手工管理的State种类,对State进行粗糙的分类。到了DX12,里为了能够提前计算出依赖,并按照依赖关系进行设置,可以提高整体的效率,将Shader跟所有的State打包在一起,称为Pipeline State Object,简称PSO,PSO包含了一次渲染所需要设置的一系列数据,可以参考右侧的三角图:包含了如下的一些属性数据:

  • Root signature
  • Steam output
  • Blend state
  • Rasterizer state
  • Depth stencil state
  • Input layout
  • Primitive type
  • Output render targets
  • Multisampling properties
  • shader bytecode

有了PSO之后,就不再需要那么多的SetRenderState的接口了。

在使用PSO的时候需要注意的是:

  • 驱动是不支持通过多线程来完成PSO的编译的,因此在使用的时候需要使用应用自己的worker线程完成这项工作,每个PSO的编译大概会需要几百毫秒(看看是否可以考虑放在离线预生成)

此外,如果某几个PSO具有较高的相似性(比如VS/PS相同,但是RenderStates稍有不同等),建议将之放在同一个线程中进行编译生成,这样做的好处是:

  • 如果shader并未发生变化的话,可以对shader编译结果进行重用
  • 而多个不同的线程对同一个shader进行编译的话,会触发等待,只有当第一个线程使用完成之后才会释放shader资源,交由其他线程使用

2.2 RST

Root Signature前面也出现过,这个术语描述的是什么内容呢?(即使看过下面的描述,还是不太理解为什么叫这个名字)

  • Root Signature本身是一个对象,这个对象会用于对DX12应用中(某一次Drawcall?)的所有resource bindings进行管理
  • Root Signature包含了一系列的可用于设置属性的slots(这些slots用于指定对应的资源,从而方便shader使用的时候按照某种方式正确访问)
  • Pipeline State会基于sloat的layout信息来判断两个shader是否兼容(兼容并不是说相等,而是具有相同的layout的,即具有相同的slots定义),并且在需要的时候也会将这些slots映射到shader中的属性上
  • 将某个Root Signature跟另一个与之兼容Root Signature交换,会通过一次调用完成Root Signature上每个Slot数据的交换,正是因为这个原因,Pipeline State可以非常高效的完成大量状态的设置(只需要将这些状态在离线的时候烘焙成一个PSO,使用的时候直接切换PSO即可)。
  • Graphics command list 既有graphics root signature,也有compute root signature,两者相互独立。一个compute command list只有一个compute root signature,这些root signatures之间是相互独立的。
  • 每个Root Signature的最大长度是64个DWORD(4bytes)
  • Root signature通常由static sampler descriptors与root parameters(主要成分)组成,而root parameters又包含如下三种类型的参数:
    • root constants:root arguments中的inline常量,在shader中以constant buffer的形式存在,每个root constant为一个DWORD的长度,如果某个参数(如矩阵)一个DWORD放不下,就需要同时占用几个Root Constant
      • Root Arguments是包含了某个root signature中所有root parameters数值的Memory Pool,可以理解为root parameters取值的集合
    • root descriptors:root arguments中的inline描述符,每个root descriptor占用两个DWORD=64bit,这是GPU中虚拟地址的长度,访问的时候需要经历一次间接寻址,访问速度低于Root Constant。提供给descriptor的slot type需要是如下几种类型:
      • D3D12_ROOT_PARAMETER_TYPE_CBV
      • D3D12_ROOT_PARAMETER_TYPE_SRV
      • D3D12_ROOT_PARAMETER_TYPE_UAV
    • descriptor tables:指向descriptor heap中一串(某个范围)descriptors起始地址的指针,基于这个指针,我们可以访问到CBV/SRV/UAV/Samplers。这里需要注意的是,table并不是说我们分配了一块空间存储了对应的数据,而是存储了到某个descriptor heap的offset & length,而descriptor heap中的元素存储的则是指向Resource Heap中资源的指针。
      • 每个table占用一个DWORD,用来表示某个位置开始的一系列descriptor,相当于descriptor指针,因此如果我们需要使用多个连续的descriptor,那么我们不需要逐个定义它们,只需要用一个table即可
      • 在访问的时候需要经历两次重定向[7]:一次到descriptor heap中的descriptor,一次则是从descriptor到对应的资源?访问速度慢于root descriptor
      • 类型有CBV, UAV, SRV 或者 texture sampler(texture sampler需要放在一个独立的table中,指向专门的sampler descriptor heap)

Root Signature的创建逻辑可以参考下面代码片段,每个root parameter都有一个叫做shader visibility的属性,通过这个属性可以指定这个资源是在哪个shader stage可见(VS还是PS等),对于同一个slot可以分别绑到两个资源上,只要这两个资源shader visibility不同即可:

ID3DBlob* rsBlob;
ID3DBlob* errorBlob;
// 1. 先创建对应的D3D12_ROOT_SIGNATURE_DESC 
D3D12_ROOT_SIGNATURE_DESC myRootSignature {
        .NumParameters = 3,
        .pParameters = (D3D12_ROOT_PARAMETER[3]){
            { .ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX, .ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS,  .Constants = { .Num32BitValues = 50 } },
            { .ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL,  .ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE, .DescriptorTable = {
                .NumDescriptorRanges = 1, .pDescriptorRanges = (D3D12_DESCRIPTOR_RANGE[1]) { { .RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV, .NumDescriptors = 128 } } } },
            { .ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL,  .ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS,  .Constants = { .Num32BitValues = 1  } },
        },
        .NumStaticSamplers = 1,
        .pStaticSamplers = (D3D12_STATIC_SAMPLER_DESC[1]) {
            { .ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL, .Filter = D3D12_FILTER_ANISOTROPIC, .AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP,
                .AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP, .AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP, .MaxLOD = 1000.0f, .MaxAnisotropy = 16 }
        },
        .Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT,
    };

// 2. 再调用D3D12SerializeRootSignature完成Root Signature的序列化,输出一个二进制的blob
result = D3D12SerializeRootSignature (
    &myRootSignature,        
    D3D_ROOT_SIGNATURE_VERSION_1,
    &rsBlob,
    &errorBlob
);
const char* err = "";
if ( !SUCCEEDED(result) )
{
    err = errorBlob->lpVtbl->GetBufferPointer ( errorBlob );
    RETURN_ERROR(-1, "D3D12SerializeRootSignature failed (0x%08X) (%s)", result, errorBlob->lpVtbl->GetBufferPointer ( errorBlob ) );
}

// 3. 基于上面的二进制blob创建Root Signature
result = renderer->device->lpVtbl->CreateRootSignature (
    renderer->device,
    0,
    rsBlob->lpVtbl->GetBufferPointer ( rsBlob ),
    rsBlob->lpVtbl->GetBufferSize ( rsBlob ),
    &IID_ID3D12RootSignature,
    &renderer->rs
);
if ( !SUCCEEDED(result) )
    RETURN_ERROR(-1, "CreateRootSignature failed (0x%08X)", result );
rsBlob->lpVtbl->Release ( rsBlob );

Root Signature通常会跟bundle或者CommandList一起使用,这里只以CommandList举例。

Pipeline State Object的创建需要指定Root Signature:

struct PipelineStateStreamType 
{  
…  
} pipelineStateStream;

pipelineStateStream.pRootSignature = MyRootSignatureComPtr.Get();

在使用CommandList的时候,Root Signature默认是没有定义的,需要手动指定(需要注意,在定义PSO的时候即使指定了Root Signature,这里还是要显式调用一次[7]):

cmdList->SetGraphicsRootSignature(MyRootSignatureComPtr.Get());

一旦通过上述接口设定了Root Signature,那么之前的root signature bindings就会过期,而在调用下一次Draw Call或者Dispatch Call之前,就需要重新设定这些bingdings。

RST的使用Tips为:

  • RST要尽可能的小,如果过大,可以考虑拆成多个,多个RST之间并无从属或者层级关系,是并列的
  • 将使用(变化)频率最高的项放在最前面,推测RST在切换的时候,会逐项进行比对是否需要进行变化,越早检测到变化,就越早开始触发State的切换,从而提高切换效率
  • 尽量保证每个drawcall只有一个slot的变化(但是这个无法保证,资源变化情况是不可控的,只是变化的slot越多,切换时间消耗就越长)
  • 资源的可见范围(资源可见的stage数目)越小越好,比如,假设没有必要,就不要使用D3D12_SHADER_VISIBILITY_ALL,又比如尽可能多的使用DENY_*_SHADER_ROOT_ACCESS flag等
  • RST上没有做任何的越界判定的,因此在使用的时候需要自行检查,避免越界访问导致的异常或者崩溃
  • 在Root Signature发生变化的时候,尽量避免资源绑定处于一个undefined的状态

AMD的使用建议为:

  • 每个DrawCall中只有常量与CBV(Constant buffer view,存储了shader计算所需要的常量数据,如World/Projection/View Matrix等)的变化应该被放在RST中
  • 如果每个drawcall变化的CBV超过一个,那么将这些CBV放在RST中通常会有更好的性能表现

NVIDIA的使用建议:将所有的常量与CBVs放在RST中:

  • 常量与CBVs放在RST会提高shader执行效率
  • 如果使用Root Constants就可以少创建一个CBV,从而减少了CPU的工作量

3. Memory Management

包含Command Allocators、Resources以及Residency三部分。

3.1 Command Allocators

Command Allocator的数目 = Recording Threads的数目 x buffered frames的数目 + 给bundle预留的额外Allocator Pool的数目。

上面的公式也说明了Command Allocator的用法,即每个(Command List的)Recording Thread都对应于一个Allocator,而当我们需要缓存多帧的数据时,这些Allocator也保持在有效可用状态,没有被回收。

Allocator的空间只会增长,不会下降:

  • 不能从Allocator中将已经分配的空间重新取出来
  • 通常情况下Allocator分配的空间都是分配给Command List的

尽可能的将Allocators通过Allocator Pool的方式管理起来,方便实现重用

3.2 Resources

Resources在D3D中可以卡成是对GPU物理存储空间的一种抽象,Resource需要通过GPU的虚拟地址空间来获取到实际的物理存储数据,它的创建是不受线程约束的(free-threaded,即可以在任何线程中完成)

基于虚拟地址,可以创建如下三种Resources类型:

  • Commited resources:这是D3D中最常见的资源类型,当需要创建这类资源时,会分配一块虚拟地址区域,并隐式分配一个足够大的Heap来装载整个资源,同时将虚拟地址范围提交到被这个Heap所包围的物理存储空间。

  • Placed resources:,这是D3D12新增的一种资源类型。在D3D12中,Heap的创建可以跟资源分开,比如可以创建一块大尺寸的Heap,之后可以根据需要,对Heap进行二次分配,将一块块的空间分配给对应的资源。这个过程不需要创建Tiled或者Reserved Resources,而是将资源的创建交由app完成,多个资源对应的物理空间可能会存在重叠,在使用的时候需要通过ID3D12GraphicsCommandList::ResourceBarrier 来对物理存储空间进行重用,具体可以参考 ID3D12Device::CreatePlacedResource.

  • Reserved resources:Reserved resources跟D3D11的tiled resources概念很像。这种资源在创建的时候,只会分配一个虚拟地址,而不会将这个虚拟地址与实际的heap相关联或映射。后续在需要的时候,app会自行将这两者进行关联。这类资源的能力表现与D3D中对应的资源类型一样,都可以以64kb为基本粒度映射到一个heap上去,这个过程是通过UpdateTileMappings完成的。具体可以参考 ID3D12Device::CreateReservedResource.

上图给出了不同资源类型是否需要分配物理空间以及是否具有虚拟地址的概括性说明。

对于Committed Resources而言,会为之分配一个能够承载这个资源的最小尺寸的heap。

在应用层需要通过MakeResident/Evit接口(这两个接口总是成对出现)调用来完成对应资源的相应操作(在Command List使用对应的资源之前,需要先保证MakeResident已经正确返回)

上述接口调用之后,剩下的工作就依赖于操作系统的Paging Logic完成:

  • MakeResident调用之后,会由操作系统决定资源的具体存放地址
  • 在这个接口返回之前,这个资源将处于不可用状态

如果是通过Heap来分配空间,建议分配一个大尺寸的Heap:

  • Heap的尺寸可以按照10~100MB的数量级来分配
  • 之后在Heap中通过二次分配来为资源划分对应的空间

每个Heap的空间分配需要调用一次MakeResident跟Evit,资源在Heap上的空间二次分配就不需要再走这套逻辑了。

因为存在二次分配,因此应用层需要自行对Heap的分配情况进行管控,包括空间的分配与回收等。

3.3 Residency

MakeResident/Evit操作中:

  • CPU+GPU的消耗比较高,因此如果可以的话,尽量对MakeResident与UpdateTileMappings接口的调用进行Batch处理(这里的意思是,将所有的调用集中在一起完成,还是将多次调用合并成一次?根据上下文来理解,推测是后一种)
  • 将高消耗的工作分配到多帧完成,以平滑表现
  • 另外需要注意的是Evit调用通常不是立马生效的

MakeResident操作是同步的:

  • 在资源变成Resident之前,MakeResident接口是不会返回的
  • 操作系统为了查找到一块合适的资源存放空间可能会花费不少的时间,在这之前,MakeResident接口之后的工作都是阻塞的
  • 为了提升效率,建议将MakeResident调用放在worker线程中

通常情况下,每个应用有多少的显存可用?

  • 可以通过IDXGIAdapter3::QueryVidoMemoryInfo接口查询
  • 通常情况下,前台app会确保一定的显存,其他app所占有的显存就不确定了,app需要监听操作系统的budget变化(即随时可能发生分配给app的显存尺寸变化的情况)

APP需要处理MakeResident调用失败的情况

  • 通常调用失败的意思是没有足够的空间可供分配
  • 当然也有时候因为碎片化的原因,即使整体来看空间利用率不高,但是依然会返回失败

对于非resident的资源的访问是非法的,相当于野指针,严重的情况会导致crash,那么如果空间不够了,我们要怎么办?

在系统内存中创建overflow heaps,并将部分资源从显存heap迁移到系统heap。由于app清楚的知道哪些资源是更紧急的,而这些信息是驱动层以及操作系统所拿不到的,因此可以通过这种方式来缓解显存不足的困境。

在实践的时候,可以通过开多个instance来测试一下表现。

资源使用建议:

  • Aliasing targets(具体含义可以参考后面的Memory Aliasing)可以有效降低显存占用
  • 驱动更倾向于使用Committed类型的RTV/DSV资源

4. Synchronization

包含两个部分:
 Barriers
 Fences

Resource barriers,添加一些操作指令以完成资源类型(Resource State,可以看成是GPU对一个资源的描述,同一个资源在不同用途下,其数据存储规则是不一样的,比如buffer是按照线性存储的以方便进行读取,而RT则是按照block存储的方便进行压缩与写入等)的转换,比如从RT转换为Texture,在GPU完成这个转换工作之前,会阻塞其他工作的执行,这里有三种类型的Barrier:

  • D3D12_RESOURCE_BARRIER_TYPE_TRANSITION:用于实现具有不同usage(资源类型)的subresourcce的转换,调用者需要指定subresource转换前后的usage。
  • D3D12_RESOURCE_BARRIER_TYPE_ALIASING:用于实现映射到同一个tile pool上的两个不同资源的转换,调用的时候需要传入转换前后的资源指针,其中某一个或者两个都可以为空,表示的是可能导致aliasing的tiled resource,这类转换常用于reserved以及placed resources.。
  • D3D12_RESOURCE_BARRIER_TYPE_UAV:UAV barrier指的是所有UAV的访问(读或写)都需要在下一次UAV访问之前完成

其中GPU Memory Aliasing指的是同一块物理存储空间分别被多个资源使用,比如我们在CPU中常用的内存池概念,分配一块大的物理空间,并将这块空间拆分成若干小尺寸的内存块,每块内存分别交由不同的数据结构使用。内存池的存在是为了避免空间分配与释放的时间消耗,这个过程在GPU上比较缓慢(不知道CPU对系统内存的操作是否也比较缓慢?)

tiled resource指的是通过若干小尺寸的物理存储空间来存放的一个大的逻辑资源,也就是说,这个完整的逻辑资源,其数据在物理存储上并不是连续的:

Topic Description
为什么会存在tiled resources? 如果某个资源的部分region是未使用状态(可以理解为稀疏存储)时使用以减少显存的浪费,硬件会在采样时做好边界的处理(适合存储virtual texture?)
创建tiled resources Tiled resources可以通过指定 D3D11_RESOURCE_MISC_TILED flag完成创建
Tiled Resource APIs 给出了tiled resources 跟 tile pool的相关API
管线中如何访问tiled resources Tiled resources 可用作SRV, RTV, DSV (depth stencil views) ,UAV,同时也可用于某些不需要使用view的绑定场合,比如vertex buffer bindings.
Tiled resources features tiers Direct3D 11.2将硬件对Tiled Resource的支持分为了两级(tier),可以通过the D3D11_TILED_RESOURCES_TIER 查询.

这里给出了一个使用示例,假如我们需要使用两张RT来进行ping-pong渲染:

  1. Bind Texture A as a render-target, draw models to it
  2. Bind Texture B as a render-target, draw a quad to it, which reads from Texture A and does a horizontal blur
  3. Bind Texture A as a render-target, draw a quad to it, which reads from Texture B and does a vertical blur

通过resource barriers, 那么整体流程不变,只需要在其中插入合适的barriers即可 :

  1. Issue a resource barrier, transitioning A and B from "uninitialized" state to "color target" state.
  2. Bind Texture A as a render-target, draw models to it
    2.1 Issue a resource barrier, transitioning A from "color target" state to "readable texture" state.
  3. Bind Texture B as a render-target, draw a quad to it which reads from Texture A and does a horizontal blur
    3.1 Issue a resource barrier, transitioning B from "color target" state to "readable texture" state.
    3.2 Issue a resource barrier, transitioning A from "readable texture" state to "color target" state.
  4. Bind Texture A as a render-target, draw a quad to it which reads from Texture B and does a vertical blur

关于Barrier的使用可以参考Using Resource Barriers to Synchronize Resource States in Direct3D 12。

Fences - 指的是命令stream中的一个标记(marker),通过这个标记,我们可以知道GPU,或者CPU是否已经完成某项工作,从而好开始进行数据同步。

举个例子,GPU command queue/list可以包含CPU设定的某个Fence,这个Fence将在CPU发出信号通知GPU之前阻止GPU进行后续的工作。因此假如我们有一条指令是让GPU从内存中某块buffer中读取数据,且在读取之前,需要CPU对此buffer进行填充,那么就需要在读取之前,加入一个Fence用于等待CPU填充完成。反而言之,如果CPU需要读取GPU填充的某块buffer,同样可以使用Fence来保证CPU读取到的数据的正确性,在每一帧的帧末,通常会添加一个Fence来通知CPU,GPU的所有工作已经完成。

4.1 Barriers

Barrier如果使用不当,会导致性能的下降,这里有一些使用上的建议。

将barriers放在一起(batch,由于Barrier会导致Cache Flush跟Resource State的Transition,因此将多个Barrier合并在一起执行可以会有更好的表现 —— 是因为将多次阻塞减少成一次吗?)。

  • 使用尽可能少的usage flags(对应于资源的类型,比如RT,比如SRV),以避免重复的flushing(usage flag跟flushing是什么关系?)
  • 尽量避免将一个readable的资源通过barrier又转成readable(推测意思为避免readable变成writable,但是没有使用,接着又转成readable,[4]中给了另一种解释,是说对某个资源调用了两次转换成readable格式的barrier,这里在驱动层面是不会做优化的,消耗等于两次转换,因此在使用的时候,需要避免这种情况,如此看来RenderGraph还是非常方便的,可以自动管理相应转换逻辑,避免冗余操作),考虑在完成一次转换后,将所有可以用到这个转换后资源的sub stage都放在一起,避免转来转去的浪费。
  • 尽可能的使用“split-barriers”,通过Split Barriers,可以将某个转换过程从同步变成异步,以实现并行计算的效率提升,当然,前提是转换完成之前,我们暂时不需要对转换前后的资源进行访问
  • COPY_SOURCE(D3D12_RESOURCE_STATE_COPY_SOURCE)比SHADER_RESOURCE(D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE或者D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE)消耗更高
  • Barrier的数目跟需要写入的Surface数目大致成2:1的比例(读一次,写一次?)

4.2 Fences

  • 相当于GPU的信标(Semaphore)
  • Fence的执行消耗(不论是CPU还是GPU)都跟调用ExecuteCommandLists差不多
  • 每次ExecuteCommandLists调用最多只能触发一次Signal/Advance同步

5. Miscellaneous

包含如下的一些内容:
 Multi-GPU
 Swap Chains
 Set Stable Power State
 Pixel vs Compute

5.1 Multi-GPU

  • D3D12 API中目前已经集成了此能力
  • 需要在cross-adapter以及linked-node中间找平衡
  • 需要在不同设备(GPU)之间进行资源的的同步(要使用合适的CreationNodeMask)
  • 需要关注PCIe的带宽
    • PCI 3.0(8x),标注8GB/s(实际上6GB/s)
    • PCI 2.0(8x),标注4GB/s(实际3GB/s)

5.2 Swap Chains

  • app需要手动完成buffer的旋转(D3D10跟D3D11是可以自动完成的),IDXGISwapChian3::GetCurrentBackBufferIndex
    • DXGI_MODE_ROTATION,用于指定back buffers要如何旋转才能跟显示器的画面匹配起来
  • 通过DX API来实现VSYNC_OFF,可以通过如下步骤完成
    • SetFullScreenState(TRUE)
    • 使用一个borderless全屏窗口
    • 将model swap-chain mode翻转

5.3 Set Stable Power State

尽量避免使用这个接口:

  • 这个接口会提升消耗,降低性能表现
    • 改变GPU组件的性能消耗比例

5.4 Pixel vs Compute

在不同厂家的硬件上,决策考虑不一样。

在NVIDIA上,使用PS的理由为:

  • 没有shared memory
  • 希望线程执行时间相同
  • 高频的cbuffer访问
  • 用于2D buffer store
    CS的理由为:
  • 使用group shared memory
  • 线程完成时序随机
  • 使用high # regs
  • 1D/3D的buffer store

在AMD上,使用PS的理由为:

  • 需要使用到DS的rejection能力(即discard、clip操作)
  • 需要图形管线
  • 需要实现颜色压缩
    否则其他计算都建议使用CS

在使用CS的时候,可以考虑sync操作来提升性能。

6. Hardware Features

包含如下的一些内容:
 Conservative Rasterization
 Volume Tiled Resources
 Raster Ordered Views
 Typed UAV Loads
 Stencil Output

6.1 Conservative Rasterization

Conservative Rasterization能力并不是在所有硬件上都具备,在使用前先查看硬件的specification。

从上图来看,这个能力的作用是将三角面片触碰到的像素都纳入渲染范围,而非如此前一般只覆盖那些超过一定比例(50%?)的像素。虽然在此前,可以通过GS来实现,但是性能会比较低;目前可以通过这个能力来实现一些比较先进的技术方案,具体可以参考GDC2015的Hybrid Raytraced Shadows。

6.2 Volume Tiled Resources

  • 添加了对Volume Tiled Resource的支持,可以理解成Tier3的Tiled Resource。
  • 每个Tile依然是64kb大小,并且需要通过CPU来完成tile的mapping
  • 可以非常好的提升性能,在GDC2015的Latency Resistant Sparse Fluid中可以体现。

6.3 Raster Ordered Views

目前支持在写入的时候指定顺序,最常用的使用情景为OIT(Order Independent Transparency),可以理解为可编程的混合逻辑(此前都是固定算法的blending),不过需要注意的是,这项能力并不是免费的。

6.4 Typed UAV Loads

  • 不再有32位的限制
  • 可以移除主机专属的路径(意思是PC也可以使用了?)
  • 从UAV读取数据比从SRV读取数据要慢,因此在只读的时候,建议还是使用SRV

6.5 Stencil Output

支持输出Stencil

  • 此前的实现方式是通过多个pass(每个pass指定stencil mask)来完成输出,现在可以单个pass实现
  • 性能表现与Depth Out相近

参考

[1]. Advanced Graphics Techniques Tutorial Day: Practical DirectX 12 - Programming Model and Hardware Capabilities
[2]. DX12 Do's And Don'ts
[3]. Creating and recording command lists and bundles
[4]. Render Graph与现代图形API
[5]. Root signature and pipeline state
[6]. Root Signatures Overview
[7]. Why talking about the Root Signature?

你可能感兴趣的:(【GDC 2016】Practical DirectX 12- Programming Model and Hardware Capabilities)