Direct X 12 – Initialization 初始化

DX12的初始化需要我们熟悉一些基本的DX12类型和一些基本的计算机图形学概念;本章节的第一与第二部分将会阐述这部分内容。之后我们将详细得告诉大家如何初始化DX12。接下来,我们会介绍一个在即时渲染应用(比如游戏)中被广泛使用的计时器。最后我们将研究一套代码的框架,而本书所有的demo都是基于该框架以及其接口进行开发的。

目标:

1.对于DX12在3D硬件中所扮演的角色

2.理解COM是如何与DX12进行交互

3.了解图形学基础知识,比如2D图片是如何存储的,翻页的原理,深度缓存(depth buffering),多重采样(multi-sampling),以及CPU和GPU的交互

4.学习如何使用性能计数方法来获得高分辨率的是计时器读取 

5.学习如何初始化DX12

6.熟悉本书中demo所用的框架代码

4.1 准备工作

DX12初始化的工作需要我们熟悉基本的图形学概念与Direct3D类型。我们将在本章节中介绍这些概念与类型,所以各位老铁请拭目以待!

4.1.1 Direct3D 12 总览

DX12是一种底层的图形学API,它被用来控制GPU并让我们能给GPU进行编程,因此我们可以通过DX12来渲染虚拟的三维世界。比如,为了向GPU提交一个清除渲染目标(比如屏幕)的指令,我们需要调用DX12中的一个方法,ID3D12CommandList::ClearRenderTargetView。此时DX12和硬件驱动将会把该指令“翻译”成GPU所理解的机器指令。因此,我们不必担心GPU的技术细节(1080还是1070),只需要该硬件支持我们所使用的DX12.所以GPU的生产商NVIDIA,Intel和AMD必须与Direct3D的研发团队合作并提供一致的Direct3D驱动。

相较于之前的DX11,DX12加入了一些新的渲染特性。但是其主要的改进在于,DX12显著地降低了CPU的开销并且加强了多线程的支持。为了达到上述提到的性能目标,DX12相较于DX11是一种更底层的API;DX12的抽象层更少,需要开发人员进行额外的手动管理,并且DX12更紧密地体现了现代GPU的架构。当然,这一切的性能提高的前提就是这瘠薄API更难用了,各位老铁也更容易秃了,哈哈哈!

4.1.2 COM

组件对象模型(Component Object Model,简称COM)能让DX12成为一种独立的编程语言并且使其能够向下兼容。我们常常将COM对象参照为一种接口,这使得我们能将COM当作是一个C++类。当使用C++编写DX12程序时,大多数COM细节是隐藏的。我们唯一须要记住的,我们能通过某些函数或者另一个COM接口的函数来获取指向COM接口的指针,但是我们不能通过C++的new关键字来创建一个COM接口。此外,COM对象有引用计数。所以一个COM接口完成了它的使命,我们须要调用它的的Release方法而不是简单的delete(所有的COM接口都继承自IUnkown COM接口,其提供了Release方法),COM对象将会其引用计数变为0时,释放其内存空间。

为了管理COM对象的生命周期,Windows Runtime Library(WRL)提供了Microsoft::WRL::ComPtr的类(#include <wrl.h>),其可以被当作一个COM对象的智能指针。当一个ComPtr的实例处在作用域之外,ComPtr会自动调用Release方法,我们不用手动调用其Release函数。在本书中我们使用的三个主要的ComPtr函数:

1.Get:返回一个指向COM接口的指针,其通常被用作函数的参数,而该参数需要一个原始的COM接口指针。比如:

ComPtr<ID3D12RootSignature> mRootSignature; 
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());

2.GetAddressOf:返回指向COM接口的指针的地址,其通常被用作函数的参数。比如:

ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
ThrowIfFailed(md3dDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT,mDirectCmdListAlloc.GetAddressOf()));

3.Reset:将ComPtr的实例设为空指针,且减少COM接口的引用计数。同样,你也能直接将空指针指向一个ComPtr的实例。

关于COM还有很多内容可讲,但是对于使用DX12来说,都没有太大的意义。各位老铁请记住,COM接口大多都有着大写的首字母I。比如,代表command list的COM接口就被称为ID3D12GraphicsCommandList.

4.1.3 贴图格式

2D的贴图是一个数据的矩阵。2D贴图的一个用处是存储2D图像,贴图中的每一个元素都存储了相对应像素的颜色。然而,这并不是唯一的用处;比如,在法线贴图中,贴图中的每一个元素都存储了一个三维的向量而非一种颜色。通常我们认为贴图是用来存储图像的数据,但是贴图有着更广泛的用处。1D贴图就像是一维的数组,2D贴图则像是二维的数组,3D贴图像是三维的数组。在之后的章节中,我们将讨论贴图更多的用处,除了作为一组数据的数组;它们能拥有纹理映射等级(mipmap level),并且GPU能够对贴图进行特殊处理,比如应用筛选与多重采样(multi-sampling)。此外,贴图不能够存储随机的数据类型;它只能存储确定的数据类型,而这些数据类型有DXGI_FORMAT的枚举类型来代表。比如:

1.DXGI_FORMAT_R32G32B32_FLOAT:由三个32位的浮点数组成

2.DXGI_FORMAT_R16G16B16A16_UNORM:由四个16位且范围从0到1的浮点数组成

3.DXGI_FORMAT_R32G32_UINT:由两个32位无符号整型数组成

4.DXGI_FORMAT_R8G8B8A8_UNORM:由四个8位且范围从0到1的浮点数组成

5.DXGI_FORMAT_R8G8B8A8_SNORM:由四个8位且范围从-1到1的浮点数组成

6.DXGI_FORMAT_R8G8B8A8_SINT:由四个8位且范围从-128到127的整型数组成

7.DXGI_FORMAT_R8G8B8A8_UINT:由四个8位且范围从0到255的整型数组成

请注意,R,G,B,A这几个字母代表红色(red),绿色(green),蓝色(blue),和透明度(alpha)。色彩是由三原色,也就是红,绿,蓝所组成(比如,将相同比例的红色与绿色混合能得到黄色)。Alpha一般被用来控制透明度。然而,就像我们之前所说的,贴图不一定需要存储色彩的信息。比如,DXGI_FORMAT_R32G32B32_FLOAT拥有三个浮点数的元素,因此这个类型能够用其浮点数存储任何三维向量。在DX12中也有无类型的数据类型存在,我们一般用这种类型来预留内存空间,并在贴图绑定至渲染管线之后重新定义该数据(类似于c++的reinterpret cast);比如,DXGI_FORMAT_R16G16B16A16_TYPELESS用了四个16位的数据来预留空间,它并没有确定其数据由四个整型,浮点数或者,无符号整型所组成。

在第六章中我们将看到DXGI_FORMAT的枚举类型被用来表述顶点(vertex)数据类型和索引(index)数据类型。

4.1.4 交换链(Swap Chain)以及翻页(Page Flipping)

为了避免动画的闪烁,最好的方法是将整个一帧的动画都“画”在一张看不见的图片上,这张图片或者贴图也被称为Back Buffer。当整个场景已经被存入Back Buffer,它对于屏幕来说是完整的一帧的画面;用这种方法,看的人就不会看到这一帧被画的过程,只是看到了完整的画面。为了达到这种效果,两个贴图缓存将被硬件所保留,一个被称为Front Buffer,另一个被称为Back Buffer。Front Buffer存储了显示器正在显示的画面,同时下一帧的动画或者画面正在被存入Back Buffer。当一帧的画面被存入Back Buffer之后,Back Buffer与Front Buffer的角色将会调换:对于下一帧的画面,Back Buffer将变为Front Buffer。交换Front Buffer与Back Buffer的行为被称为Presenting。这是一个很有效率的操作,因为我们只是交换分别指向Front Buffer与Back Buffer的指针。下图描述了该操作。

对于第n帧,缓存A正在被显示器所显示,同时我们将下一帧的画面渲染至缓存B,而此时缓存B正作为当前的Back Buffer。一旦这一帧的画面显示完毕,指针将被交换,同时缓存B将变为Front Buffer,然后缓存A将变为新的Back Buffer。接着,我们将下一帧也就是n+1帧的画面渲染至缓存A。一旦这一帧也显示完毕,这两个指针将再一次被交换。

swap chain中的Front Buffer和Back Buffer。在DX12中,IDXGISwapChain接口代表swap chain。这个接口存储了front buffer和back buffer,同时这个接口也提供了调整缓存的方法(IDXGISwapChain::ResizeBuffers)和显示缓存的方法(IDXGISwapChain::Present)。

使用两个缓存区(Front和Back)被称为双缓存(double buffering)。我们也可以使用三个缓存区,但通常来说两个缓存区已经足够了。

4.1.5 深度缓存(Depth Buffering)

depth buffer虽然也是一张贴图,但它存储着每一个像素的深度值而并非每一个像素的颜色。深度值的范围从0.0到1.0,0.0表示这个一个物体在观察者的视锥体中最靠近该观察者,而1.0则正好相反,表示最远。depth buffer中的每一个元素都与back buffer中的每个像素一一对应(比如,back buffer中的第ij个元素对应depth buffer中的第ij个元素)。所以如果back buffer的像素大小为1280 x 1024,那么depth buffer的像素大小也应该是1280 x 1024。

下图展示了一些物体被另一些物体所遮盖。为了让DX12能够决定哪些像素须要被显示,它采用了depth buffering的技术,其又被称为Z缓存(z buffering)。简而言之,正因为depth buffering的存在,我们绘制多个物体的先后顺序将不会影响这个物体是否被其他物体所遮盖。

为了解决深度的问题,有人会建议我们以由远到近的顺序来绘制场景中的物体。这样的话,距离观察者较近的物体将会显示在较远的物体之上。这种方法也是画家进行绘画时所采用的方法。但问题是,如果场景非常之大,按照物体至观察者距离进行排序将会十分费时。不过硬件所提供给我们的depth buffering解决了这个问题。

为了展现depth buffering是如何工作的,请看下面这个例子。下图展示了一个观察者的视野范围,并附上了一张视野范围的2D侧面图。我们可以看到,有三个不同的像素将有机会被渲染至视窗中的P点(我们当然知道距离最近的像素应该被渲染到P点,因为这个像素遮住了其他的像素,但是计算机并不知道这些。)首先,在进行任何渲染任务之前,back buffer将被清空为一个默认的颜色,同时depth buffer将被清空为默认值,通常默认值为1.0(最远值)。现在,假设我们以圆柱体,球体,椎体的顺序来绘制这些物体。下面这张表格总结了像素P与其相对应的深度值d是如何更新的;这个操作也被用来处理像素P以外的其他像素点。

视窗相当于我们通过3D的场景所生成的2D图像(back buffer)。我们看到了三个不同的像素点能够被投影到像素点P。而这个图像告诉了我们像素点P1应该被绘制到P,因为它距离观察者更近且覆盖了另两个像素点。depth buffer算法为计算机提供了一套程序化的操作。但需要注意的是,不同像素点的深度值(depth value)将会随着观察者观察场景的位置以及方向不同而改变,但depth buffer中的深度值都是范围0.0到1.0的标准化值。

正如你所看到的,只有当我们发现一个像素点有着更小的深度值,我们才会更新这个像素点和其相对应的深度值。这样的话,在一切操作完成之后,距离观察者最近的像素点将被绘制。

总的来说,depth buffering通过计算每个像素点的深度值来进行工作。深度测试将可能被绘制的像素点的深度值与深度缓存中的像素点的深度值进行比较。深度值较小,也就是距离观察者更近的像素点才能通过深度测试。

depth buffer是一张图片,所以它必须以某种特定的数据格式来创建。depth buffer能够使用的数据格式有下面几种:

1. DXGI_FORMAT_D32_FLOAT_S8X24_UINT:指定了一个32位的浮点数depth buffer,而且为stencil buffer预留了8位范围为0到255的无符号整型数,同时24位用来作为填充。

2. DXGI_FORMAT_D32_FLOAT:指定了一个32位浮点数depth buffer。

3. DXGI_FORMAT_D32_UNORM_S8_UINT:指定了一个24位无符号范围从0到1的浮点数depth buffer,而且为stencil buffer预留了8位范围为0到255的无符号整型数。

4. DXGI_FORMAT_D16_UNORM:指定了一个16位无符号范围从0到1的浮点数depth buffer。

老铁们记住了,stencil buffer并不是必须的,但如果你使用了stencil buffer,那么stencil buffer将会附着在depth buffer。比如DXGI_FORMAT_D24_UNORM_S8_UINT,这个格式用了一个24位的数作为depth buffer,同时用了一个8位的数作为stencil buffer。正因如此,depth buffer也被称为depth / stencil buffer。关于如何使用stencil buffer,我们将在之后的章节告诉大家。

4.1.6 资源与描述符(descriptor)

在渲染进程中,GPU将会写入资源(比如,back buffer或者depth/stencil buffer),也会读取资源(比如,模型贴图,或者存储着3D坐标的缓存)。在我们发出绘制指令(draw command)之前,我们需要将资源绑定/关联至draw call中所引用的渲染管线。有一些资源在每一次draw call中都会变化,所以有必要的话,每一次draw call我们都要更新这些绑定。但是,这些资源并不能直接被绑定至GPU,它们需要被描述符(descriptor)对象所引用。而描述符(descriptor)对象可以被认为是一种轻量级的结构体,这种结构体将会将资源描述给GPU。所以有了一个资源描述符(descriptor),GPU才能拿到该资源的数据并知道这个资源的必要信息。通过声明在draw call中被引用的描述符,我们才能将资源绑定至渲染管线。

各位老铁可能会问,为什么需要描述符(descriptor)这个看似多余的操作?GPU资源本质上是普通的内存数据块,所以这些资源能在渲染管线的各个阶段被GPU所使用;比方说,贴图是一个绘制对象,但之后可能会是一个着色器资源(shader resource,贴图将被采样,然后再着色器中被使用)。资源自己并不会给自己定性为绘制对象,depth/stencil buffer或者shader resource。所以我们可能只想要将资源数据的一部分绑定至渲染管线。此外,一个资源在被创建时可能用的是无类型格式(typeless format),这样的话GPU也不会知道该资源的格式与用途。

上一段就解释了为什么我们需要描述符(descriptor)。除了声明资源的数据,描述符(descriptor)还会向GPU描述资源;它们会告诉DX12这些资源如何被使用(比如,资源在哪一个阶段被绑定至管线),它们也会告诉DX12这些资源的哪一些分区将被绑定至描述符(descriptor),如果资源在创建时被声明为无类型格式(typeless format),那么我们必须在创建描述符(descriptor)时确定资源的格式类型。

由于在之前的DX版本中我们都未曾使用descriptor这个词,而是使用view,所以本书中view与descriptor有着相同的意义。在DX12中view仍然在某些部分中被使用。本书中某些地方我们也会使用view;比方说,constant buffer view和constant buffer descriptor指的是同一样东西。

描述符(descriptor)也有一个类型,而这个类型也表明了该资源将被如何使用。在本书中,我们主要使用以下几个类型:

1. CBV / SRV / UAV描述符分别指的是constant buffer,shader resource buffer以及unordered access view资源。

2. Sampler descriptor指的是sampler资源(一般在贴图中使用)。

3. RTV descriptor指的是render target资源。

4. DSV descriptor指的是depth/stencil资源。

descriptor heap就是一个descriptor的数组;它由一组特定类型的descriptor所组成。所以,每一种类型的descriptor需要一个独立的descriptor heap。当然你也能创建几个descriptor heap都包含了相同类型的descriptor。

我们可以让多个descriptor引用相同的资源。比如,我们可以让几个不同的descriptor引用同一个资源的不同分区。之前我们也提到了,一个资源能在渲染管线的不同阶段被绑定。但这样的话,每一个阶段,我们需要一个独立的descriptor。比如,我们将贴图作为render target和shader resource,那么我就需要撞见两个descriptor:一个RTV类型的还有一个SRV类型。同样,如果你用无类型格式来创建一个资源,那么贴图中的元素(RGB)可以是浮点数也可以是整型,这样的话,我们就会需要两个descriptor,一个声明了浮点数格式,而另一个声明了整型类型。

Descriptor须要在初始化时就被创建。这是因为DX12中会有一些类型检测和验证,我们最好是在初始化时创建descriptor而不是运行时再创建它们。

在August 2009 SDK文档中写道:“创建一个完整类型的资源限制了该资源的格式。但是这将在运行时能提供优化【。。。】”所以,只有当你真的需要无类型格式的便利性时,才创建一个无类型的资源。

4.1.7 Multisampling理论

因为在显示器上的像素点并不是无限小的,一根随机的直线并不能在显示器上完美的呈现。下图表现了一种锯齿效果。

我们可以观察到,下方的线是带有抗锯齿效果的,它通过周围像素的颜色来最终确定每个像素的颜色;结果上来看,它更顺滑而且减轻了锯齿的效果。

缩小显示器上每个像素的尺寸能有效缓解锯齿的效果。但是,当无法增加增加显示器的像素时,我们就能使用抗锯齿(anti-aliasing)技术。有一种技术被称为超采样(supersampling),它将back buffer与depth buffer放大四倍。这样,3D场景将以更高的分辨率被绘制到back buffer。然后,当需要显示back buffer到显示器时,back buffer的分辨率将被压缩,四个像素的颜色平均值将会是最终的像素颜色。所以超采样的工作原理是增加了分辨率。

超采样的开销很大是因为它增加了需要处理的像素的数量。然而DX12支持一种教委妥协的抗锯齿技术,被称为多重采样。这种技术,在子像素中分享了一些数据,以此使其开销低于超采样。假设我有四倍多重采样(一个像素拥有四个子像素),它同样拥有四倍于原始分辨率的back buffer和depth buffer;然后多重采样技术不会计算每一个子像素的颜色,它只会计算原始像素中心点的颜色,之后四个子像素将会基于可视性(depth/stencil test仍然是基于所有子像素比较depth值)和覆盖性(子像素的中心点是否在多边形内还是多边形外)分享这个计算结果。参考下图中的例子。

我们可以认为该像素越过了多边形的边界。(a)原始像素点的中心颜色是灰色,所以三个像素点将拥有灰色,然而另一个像素点没有被多边形覆盖,所以它将不会是灰色。(b)为了计算压缩后的像素颜色,我们将平均这四个像素的颜色(三个灰色和一个白色),这样我们将得到一个更浅的灰色。这种效果能个有效地减轻多边形边缘的锯齿效果。

我们可以看一下超采样与多重采样最大的区别。若是使用超采样的技术,图像的颜色是基于放大后的每个子像素的计算所得,所以每一个子像素都可能是不同的颜色。若是使用多重采样技术, 像素点的颜色仍然是基于原始像素点的颜色,但只有在多边形边界内的子像素才能获得这个颜色,多边形之外的颜色将保持原样(back buffer中该像素点的颜色)。由于计算图像中每个像素点的颜色是渲染管线中开销最大的一个步骤,多重采样相比于超采样还是节省很大的性能。从另一个角度来看,超采样后的颜色更为准觉。

在上图中,我们展示了一个像素点被划分为四个像素点,且四个子像素的尺寸相同。但在实际应用中,子像素的尺寸和中心点的位置会根据硬件厂商的标准而变化。所以DX12并不会确定子像素的排列与尺寸。

4.1.8 DX12中的超采样

在下一节中,我们将填写DXGI_SAMPLE_DESC的结构体。这个结构体拥有两个成员:

typedef 
struct DXGI_SAMPLE_DESC
    {
        UINT Count;
        UINT Quality;
    }   DXGI_SAMPLE_DESC

Count确定了每一个像素点需要进行多少次采样(多少子像素),而Quality被用来确定你想要的品质等级(这个所谓的品质等级会根据不同的硬件厂商而变化)。更高的采样数量(Count)或者更高的品质等级(Quality)意味着渲染的开销将会更高,所以抗锯齿效果与应用的运行速度需要由你来进行平衡。Quality取决于贴图的格式以及每个像素的采样次数。

通过调用ID3D12Device::CheckFeatureSupport,我们可以根据贴图的格式列出Quality的数量以及采样的数量:

typedef 
struct D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS
    {
        DXGI_FORMAT    Format;
        UINT           SampleCount;
        D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG Flags;
        UINT           NumQualityLevels;
    }   D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS;

D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
msQualityLevels.Format = mBackBufferFormat;
msQualityLevels.SampleCount = 4;
msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;
msQualityLevels.NumQualityLevels = 0;
ThrowIfFailed(md3dDevice->CheckFeatureSUpport(
 D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
 &msQualityLevels,
 sizeof(msQualityLevels)));

请注意,ID3D12Device::CheckFeatureSupport方法的第二个参数,既是输入也是输出参数。对于输入来说,我们通过它来确定贴图的格式,采样数量和flag。同时这个方法会以我们能够支持的quality level作为输出。对于一张贴图,有效的quality level的范围是0到NumQualityLevels-1。

每个像素的可采样的最大范围,定义如下:

#define D3D12_MAX_MULTISAMPLE_SAMPLE_COUNT (32)

但是,考虑到性能已经多重采样的内存占用空间,我们一般采用四倍多重采样或者八倍多重采样。如果你不希望采用多重采样,那么可以把sample count设成1并且吧quality level设成0。所有支持DX12的硬件设备也能够支持四倍多重采样。

我们需要记住,swap chain buffer和depth buffer都需要DXGI_SAMPLE_DESC,而且他们的多重采样设置必须是相同的。

4.1.9 Feature Levels

DX12引入了feature levels(在代码中由D3D_FEATURE_LEVEL的枚举类型所表示),它基本代表了DX9到DX12的各个版本:

enum D3D_FEATURE_LEVEL
    {
        D3D_FEATURE_LEVEL_9_1	= 0x9100,
        D3D_FEATURE_LEVEL_9_2	= 0x9200,
        D3D_FEATURE_LEVEL_9_3	= 0x9300,
        D3D_FEATURE_LEVEL_10_0	= 0xa000,
        D3D_FEATURE_LEVEL_10_1	= 0xa100,
        D3D_FEATURE_LEVEL_11_0	= 0xb000,
        D3D_FEATURE_LEVEL_11_1	= 0xb100,
        D3D_FEATURE_LEVEL_12_0	= 0xc000,
        D3D_FEATURE_LEVEL_12_1	= 0xc100
    } 	D3D_FEATURE_LEVEL;

Feature levels严格定义了一组功能(详细请看SDK文档,其中有介绍每一个feature level所支持的特性)。比如,如果一个GPU支持feature level 12,那么除了一些例外(比如多重采样的采样数仍需要被确定)它必须支持所有DX12的特性。一旦开发者知道了硬件所支持的feature,也就知道了哪些DX的特性能够被应用,所以这个Feature能使开发变得更简单。

如果用户的硬件并不支持某个feature level,应用可以退回一个更旧的feature level。例如,为了使应用能拥有更大的受众,应用可以支持DX12,DX11,DX10和DX9.3所对应的硬件。应用可以检测feature level所支持的版本。

4.1.10 DirectX Graphic Infrastructure

DirectX Graphic Infrastructure(DXGI)是一种与Direct3D一同使用的API。DXGI的基本理念是,一些图形学相关的任务是多种图形学API所共有的。例如,2D的渲染API和3D的渲染API都需要swap chain和page flipping来实现流畅的动画;所以swap chain的接口IDXGISwapChain是DXGI API的一部分。DXGI除了了其他的一些常见的图形学功能,比如全屏模式的转换,例举图形系统的信息,如显示适配器(显卡),显示器和支持的显示模式(分辨率,刷新率等等);DXGI也定义了各种其支持的表面格式(DXGI_FORMAT)。

我们来简单描述下在DX12初始化时会被使用到的DXGI概念和接口。DXGI的一个关键接口就是IDXGIFactory,IDXGIFactory主要被用来创建IDXGISwapChain和枚举显示适配器(显卡)。显示适配器被用来执行图形学的各种功能。通常来说,显示适配器是一块硬件设备(比如说显卡);然而,系统也能拥有软件显示适配器,其模拟了硬件的图形学功能。一个系统也能拥有几个适配器(比如说多块显卡)。IDXGIAdapter接口代表了适配器。我们可以通过以下代码枚举系统上的所有适配器:

void D3DApp::LogAdapters()
{
    UINT i = 0;
    IDXGIAdapter* adapter = nullptr;
    std::vector<IDXGIAdapter*> adapterList;
    while(mdxgiFactory->EnumAdapters(i, &adapter)!=DXGI_ERROR_NOT_FOUND)
    {
        DXGI_ADAPTER_DESC desc;
        adapter->GetDesc(&desc);
        std::wstring text = L"***Adapter:";
        text += desc.Description;
        text += L"\n";
        OutputDebugString(text.c_str());
        adapterList.push_back(adapter);
        i++;
    }
    for(size_t i = 0; i < adapterList.size(); ++i)
    {
        LogAdaptersOutputs(adapterList[i]);
        ReleaseCom(adapterList[i]);
    }
}

一个系统也能有多个显示器。一个显示器是就是一个显示输出。IDXGIOutput接口就代表输出。每一个适配器将与一个输出列表相关联。比如,一个系统拥有两张显卡和三个显示器,其中两个显示器与一张显卡相关联,而另一个显示器与剩下的显卡关联。这种情况下,一个适配器就有两个输出端,另一个适配器有一个输出端。通过下列代码,我们可以将一个适配器所关联的输出都列举出来:

void D3DApp::LogAdapterOutputs(IDXGIAdapter* adapter)
{
    UINT i = 0;
    IDXGIOutput* output = nullptr;
    while(adapter->EnumOutputs(i, &output) != DXGI_ERROR_NOT_FOUND)
    {
        DXGI_OUTPUT_DESC desc;
        output->GetDesc(&esc);
        std::wstring text = L"***Output:";
        text += desc.DeviceName;
        text += L"\n";
        OutputDebugString(text.c_str());
        LogOutputDisplayModes(output, DXGI_FORMAT_B8G8R8A8_UNORM);
        ReleaseCom(output);
        ++i;
    }
}

每一个显示器都有一系列的显示模式。显示模式指的是结构体DXGI_MODE_DESC中的数据:

typedef 
struct DXGI_MODE_DESC
    {
        UINT Width;                                // 分辨率宽度
        UINT Height;                               // 分辨率高度
        DXGI_RATIONAL RefreshRate;                 // 刷新率
        DXGI_FORMAT Format;                        // 显示格式
        DXGI_MODE_SCANLINE_ORDER ScanlineOrdering; // 隔行扫描(interlaced)或者逐行扫描(progressive)
        DXGI_MODE_SCALING Scaling;                 // 图片在显示器中是如何拉伸的
    }   DXGI_MODE_DESC;

typedef 
struct DXGI_RATIONAL
    {
        UINT Numerator;      // 分子
        UINT Denominator;    // 分母
    }   DXGI_RATIONAL;

typedef 
enum DXGI_MODE_SCANLINE_ORDER
    {
        DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED = 0,
        DXGI_MODE_SCANLINE_ORDER_PROGRESSIVE = 1,
        DXGI_MODE_SCANLINE_ORDER_UPPER_FIELD_FIRST = 2,
        DXGI_MODE_SCANLINE_ORDER_LOWER_FIELD_FIRST = 3,
    }   DXGI_MODE_SCANLINE_ORDER;

typedef 
enum DXGI_MODE_SCALING
    {
        DXGI_MODE_SCALING_UNSPECIFIED = 0,
        DXGI_MODE_SCALING_CENTERED = 1,
        DXGI_MODE_SCALING_STRETCHED = 2
    }   DXGI_MODE_SCALING;

确定了显示模式的格式之后,我们可以通过以下代码获得输出端(显示器)所支持的所有显示模式:

void D3DApp::LogOUtputDisplayModes(IDXGIOutput* output, DXGI_FORMAT format)
{
    UINT count = 0;
    UINT flags = 0;

    // 通过nullptr来获取显示模式的数量
    output->GetDisplayModeList(format, flags, &count, nullptr);
    std::vector<DXGI_MODE_DESC> modeList(count);
    output->GetDisplayModeList(format, flags, &count, &modeList[0]);
    for(auto& x : modeList)
    {
        UINT n = x.RefreshRate.Numerator;
        UINT d = x.RefreshRate.Denominator;
        std::wstring text = 
            L"Width = " + std::to_wstring(x.Width) + L"" +
            L"Height = " + std::to_wstring(x.Height) + L"" +
            L"Refresh = " + std::to_wstring(n) + L"/" + std::to_wstring(d) +
            L"\n";
        ::OutputDebugString(text.c_str());
    }
}

在全屏模式下,列举显示模式是十分重要的。为了获得全屏模式的性能优化,特定的显示模式(包括刷新率)必须匹配显示器所支持的显示模式。

更多关于DXGI的参考资料,笔者推荐大家阅读下面的文章:

DXGI Overview

DirectX Graphics Infrastructure: Best Practices

DXGI 1.4 Improvements

4.1.11 检测Feature Support

我们已经使用了ID3D12Device::CheckFeatureSupport来检测多重采样的支持。然而,这只是该方法所支持的一个特性。

HRESULT ID3D12Device::CheckFeatureSupport(
    D3D12_FEATURE Feature,
    void *pFeatureSupportData,
    UINT FeatureSupportDataSize);

1. Feature:D3D12_FEATURE的枚举成员表明了我们想要检测的feature support。

typedef 
enum D3D12_FEATURE
    {
        D3D12_FEATURE_D3D12_OPTIONS	= 0,  // 检测DX12各种feature的支持
        D3D12_FEATURE_ARCHITECTURE	= 1,  // 检测硬件结构feature的支持
        D3D12_FEATURE_FEATURE_LEVELS	= 2,  // 检测feature level的支持
        D3D12_FEATURE_FORMAT_SUPPORT	= 3,  // 检测某些贴图格式的feature support(比如,这种格式是否能被用作render target,是否能被用作blending)
        D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS  = 4, // 检测多重采样feature的支持
    } 	D3D12_FEATURE;

2. pFeatureSupportData:一个指向feature support的数据结构体的指针。而结构体的类型取决于上一个参数Feature:

如果你传入的Feature是D3D12_FEATURE_D3D12_OPTIONS,那么你需要传入D3D12_FEATURE_DATA_D3D12_OPTIONS的实例。对于其他Feature枚举值也是同理。

3. FeatureSupportDataSize:指的是上一个参数pFeatureSupportData的结构体的大小(size)。

ID3D12Device::CheckFeatureSupport检测了许多feature的支持。其中有许多我们在本书中并未运用,而且是更进一步的检测;笔者推荐大家可以参考SDK的文档,上面有更多关于各种feature结构的信息。然而作为例子,我们将检测supported feature level:

typedef 
struct D3D12_FEATURE_DATA_FEATURE_LEVELS
    {
        UINT NumFeatureLevels;
        const D3D_FEATURE_LEVEL *pFeatureLevelsRequested;
        D3D_FEATURE_LEVEL MaxSupportedFeatureLevel;
    }   D3D12_FEATURE_DATA_FEATURE_LEVELS;

D3D12_FEATURE_LEVEL featureLevels[3] = 
{
    D3D_FEATURE_LEVEL_11_0, // 首先检测对于DX11的支持
    D3D_FEATURE_LEVEL_10_0, // 之后检测对于DX10的支持
    D3D_FEATURE_LEVEL_9_3   // 最后检测对于DX9的支持
};
D3D12_FEATURE_DATA_FEATURE_LEVELS featureLevelsInfo;
featureLevelsInfo.NumFeatureLevels = 3;
featureLevelsInfo.pFeatureLevelsRequested = featureLevels;
md3dDevice->CheckFeatureSupport(
    D3D12_FEATURE_FEATURE_LEVELS,
    &featureLevelsInfo,
    sizeof(featureLevelsInfo));

请注意,第二个参数featureLevelsInfo即是一个输入参数又是一个输出参数。对于输入,我们确定了feature level数组(featureLevels)的元素个数,同时也确定了一个指向feature level数组的指针(pFeatureLevelsRequested),该指针包含了我们需要检测的硬件所支持的feature。最后该函数将输出featureLevelsInfo的MaxSupportedFeatureLevel。

4.1.12 Residency

一个电子游戏将会使用许多资源,比如贴图,3D模型,但是大部分的资源并不会同时被GPU需要或处理。比如,在游戏场景中,在一片森林中有一个巨大的洞穴,只有当玩家进入该洞穴时,我们才需要加载与洞穴相关的资源。同时,我们不再需要森林的相关资源。

在DX12中,我们需要管理各种资源的residency(主要是该资源是否需要在GPU的显存中),比如从GPU显存中清除一些资源,并在需要时在此将这些资源加载至GPU显存中。不过基本的理念是需要将GPU显存中常住的资源数量进行最小化,因为显存中常常没有足够的空间来让整个游戏的资源进行常驻,或者有时用户正好在运行另一个应用,而该应用也将需要GPU的显存。为了性能考虑,我们也应该避免在短时间内反复从GPU显存中加载并清除同一个资源。理想状态是,如果你需要清除一个资源,那么该资源在短时间内将不被需要或者加载。游戏中的区域与关卡的切换就是资源Residency的例子。

通常来说,当一个资源被创建时,它将寄存于显存中。当它被销毁时,它将被从显存中清除。然而,我们也能通过以下函数来控制资源的residency:

HRESULT ID3D12Device::MakeResident(
    UINT NumObjects,
    ID3D12Pageable *const *ppObjects);
    
HRESULT ID3D12Device::Evict(
    UINT NumObjects,
    ID3D12Pageable *const *ppObjects);

本书中的demo相较于游戏项目来说,还是很简单的,所以我们并不会管理资源的residency。关于residency更多的信息,大家可以参考以下链接:

Memory Management In Direct3D 12 – Residency

《Direct X 12 – Initialization 初始化》有4条留言

  1. 原书307页ID3D12CommandList::CopySubresourceRegion这里是个错误,ID3D12CommandList没有CopySubresourceRegion,只有CopyBufferRegion和CopyTextureRegion

    回复

留下评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据