【GDC 2016】Practical DirectX 12-

2022-08-13  本文已影响0人  离原春草

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

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

—— 弗洛姆《逃避自由》

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

1. Work Submission

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

下面分别进行陈述。

1.1 Multi Threading

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

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

1.2 Command Lists

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

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

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

每次执行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的性能:

1.4 Command Queues

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

Compute Queue

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

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

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

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包含了一次渲染所需要设置的一系列数据,可以参考右侧的三角图:包含了如下的一些属性数据:

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

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

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

2.2 RST

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

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为:

AMD的使用建议为:

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

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的空间只会增长,不会下降:

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

3.2 Resources

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

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

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

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

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

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

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

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

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

3.3 Residency

MakeResident/Evit操作中:

MakeResident操作是同步的:

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

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

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

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

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

资源使用建议:

4. Synchronization

包含两个部分:
 Barriers
 Fences

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

其中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合并在一起执行可以会有更好的表现 —— 是因为将多次阻塞减少成一次吗?)。

4.2 Fences

5. Miscellaneous

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

5.1 Multi-GPU

5.2 Swap Chains

5.3 Set Stable Power State

尽量避免使用这个接口:

5.4 Pixel vs Compute

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

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

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

在使用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

6.3 Raster Ordered Views

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

6.4 Typed UAV Loads

6.5 Stencil Output

支持输出Stencil

参考

[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?

上一篇 下一篇

猜你喜欢

热点阅读