DirectX12:4.1 初始化预备知识

目的:

  1. 基本了解Direct3D在3D硬件编程中的作用。
  2. 了解COM在Direct3D中扮演的角色。
  3. 学习基本的图形概念,如2D图像如何存储,页面翻转,深度缓冲,多采样,以及CPU和GPU如何交互。
  4. 学习如何使用性能计数器函数获得高分辨率计时器读数。
  5. 了解如何初始化Direct3D。
  6. 熟悉本书所有演示使用的应用程序框架的一般结构。

4.1.1 Direct3D 12 概述

D3D是一个低级图形API,用于控制和编程GPU,用硬件加速渲染。与11对比,除了一些新的渲染特性,还进行了重新设计,性能优化上极大减少了CPU开销,改进了多线程支持,API抽象程度降低。使用更加困难,但是性能更好。

4.1.2 组件对象模型COM

Component Object Model 简称COM,是一种允许DirectX独立于编程语言并具有向后兼容性的技术。通常将COM对象称为接口,可以将其视为c++类来使用。但不能使用new方法来获取一个指向COM对象的指针,需要用特定的函数处理。自身采用引用计数,计数为0会自行释放所占用的内存。

Windows提供Microsoft::WRL::ComPtr类管理COM对象的生命周期,其中三个常用方法为:

  1. Get:返回一个指向此底层COM接口的指针
  2. GetAddressOf:返回一个指向此底层COM接口的指针的地址
  3. Reset:将此ComPtr实例设置为nullptr,直接复制为nullptr效果一样

4.1.3 纹理格式

一般的2D纹理存储图像的对应像素的颜色值。但是纹理可以有更广泛的用途,一维纹理就像数据元素的一维数组,二维纹理就像数据元素的二维数组,三维纹理就相对于三维数组。GPU可以对其进行特殊操作,比如滤波和多重采样。

纹理是以重数据元素构成的矩阵,但只能存储特定格式的数据元素。

注意,纹理不能存储任意类型的数据。只能存储DXGI_FORMAT枚举类型。

下面有几个例子:

  1. DXGI_FORMAT_R32G32B32_FLOAT: 每个元素有三个32位浮点组件。
  2. DXGI_FORMAT_R16G16B16A16_UNORM: 每个元素有四个映射到[0,1]范围的16位组件。
  3. DXGI_FORMAT_R32G32_UINT: 每个元素都有两个32位无符号整数组件。
  4. DXGI_FORMAT_R8G8B8A8_UNORM: 每个元素有四个映射到[0,1]范围的8位无符号组件。
  5. DXGI_FORMAT_R8G8B8A8_SNORM: 每个元素有四个映射到[- 1,1]范围的8位有符号组件。
  6. DXGI_FORMAT_R8G8B8A8_SINT: 每个元素有四个映射到[- 128,127]范围的8位有符号整数组件。
  7. DXGI_FORMAT_R8G8B8A8_UINT: 每个元素有四个映射到[0,255]范围的8位无符号整数组件。

RGBA分别代表红绿蓝,透明度。虽然这些类型名字是和颜色有关的,但是并不意味着必须存储颜色信息。比如:

DXGI_FORMAT_R32G32B32_FLOAT

是可以存储任何3D向量的。

另外有无类型格式的纹理类型,用来预留内存,等纹理被绑定到管线后再解释是什么数据类型,比如

DXGI_FORMAT_R16G16B16A16_TYPELESS

除了上文提到的,DXGI_FORMAT枚举类型还可以用来描述顶点以及索引的数据格式。

4.1.4 The Swap China and Page Flipping

交换链和页面翻转

为了不让玩家看到没渲染完毕的画面,需要在后台渲染好之后在交换指针,后台变前台,前台变后台继续渲染下一帧。前后缓冲区形成了交换链。

D3D中交换链是 IDXGISwapChain 接口。该接口存储前后缓冲区的纹理数据,并提供调整缓冲区大小的方法。

交换操作称为Presenting,呈现、提交、显示。

IDXGISwapChain::ResizeBuffers

IDXGISwapChain::Present

使用两个缓冲区称为双缓冲。可以使用两个以上的缓冲区,使用三个就是三缓冲,一般两个缓冲区就够了。

缓冲区可以提升效率,但也会带来新的问题,画面闪烁、撕裂,掉帧等,解决这些问题就需要垂直同步

4.1.5 深度缓冲

深度缓冲是纹理的一个例子。不存储颜色信息,存储的是特定像素的深度信息。与后缓冲区的像素一一对应。深度信息的0.0表示离查看器最近的,1.0表示最远。

如图一个简单的场景,存在遮挡关系,需要让DirectX确定像素的前后关系,就是用了深度缓冲或Z-buffer的技术。使用深度缓冲时,绘制对象的顺序就不重要了。

再进行任何渲染前,深度缓冲区会被清除为默认值,通常是1.0,即可行的深度最大值。后缓冲区也会重置为默认颜色。

每一个像素点,遇到深度值小于当前深度值的时候会更新,否则不更新。

深度缓冲是一个纹理,需要有特定的存储格式:

  1. DXGI_FORMAT_D32_FLOAT_S8X24_UINT: 指定32位浮点深度缓冲区,8位(无符号整数)预留给模板缓冲区映射到[0,255]范围,24位不用于填充。
  2. DXGI_FORMAT_D32_FLOAT: 指定一个32位浮点深度缓冲区。
  3. DXGI_FORMAT_D24_UNORM_S8_UINT: 指定一个映射到[0,1]范围的无符号24位深度缓冲区,并为模板缓冲区保留8位(无符号整数)映射到[0,255]范围。该缓冲区可以称为模板缓冲区,会在第11章详述。
  4. DXGI_FORMAT_D16_UNORM:指定映射到[0,1]范围的无符号16位深度缓冲区。

4.1.6 Resources and Descriptors

资源和描述符

渲染过程中,GPU会不断的写入和读取资源。比如从缓冲区读取纹理或位置信息,写操作向后台缓冲区或者模板缓冲区写入数据等。

在绘制命令发出之前,需要绑定或者链接资源到即将要渲染的drawcall的渲染管道。有些资源每次渲染会变化,就需要更新绑定信息。但是GPU资源不是直接绑定到渲染管线的。资源是通过描述符对象被间接引用的,可以将其视为向GPU描述资源的轻量级结构,本质上,它是一种间接的层次;给定一个资源描述符,GPU可以获取实际的资源数据,并知道它的必要信息。

使用描述符的原因:

GPU资源,本身是普通的内存块,可以被管线不同阶段使用。并且有时候某个阶段只需要资源中的部分数据被绑定到管线上,需要有方法将部分资源从整块中取出。还有一种情况,就是无类型格式,需要描述符为GPU解释资源类型。

主要作用有,获取局部资源,向管线解释资源类型,告知资源如何使用,用在什么阶段。

4.1.7 多重采样

抗锯齿/多重采样MSAA

显示器像素不是无穷小的,会有走样。

抗锯齿通过采样和相邻像素来生成一个像素的最终颜色,使图像更平滑。

显示器分辨率增加,减小像素点的大小也可以显著减少走样现象。但是好的显示器比较贵。而且硬件总会受限。

所以可以应用超采样技术(SSAA)。原理是让后缓冲区和深度缓冲区为屏幕分辨率的4倍大小,交换前后缓冲时,进行向下采样,每4个像素点的颜色平均成一个。超采样通过软件分辨率来实现。但是超采样对内存和像素处理又多了较高的要求,四倍的内存和像素处理数量,满足条件的硬件也不是很便宜的。

D3D支持一种折中的技术,称为多重采样。这种技术不用对每一个像素进行计算,它计算一次中心像素的颜色,然后基于可视性和覆盖性,将得到的信息分享给其他子像素。比如4X多重采样,1个像素中有4个子像素,后缓冲区和深度缓冲也是分辨率的四倍,但是不用计算四个子像素的颜色,而是计算像素中心的颜色,基于每个子像素被深度/模板测试的结果和子像素中心是否在多边形内部这两个因素,将这个颜色分给四个子像素。

除此之外还有更多的反锯齿技术。

4.1.8 DirectX中的MSAA

DXGI_SAMPLE_DESC

该结构体有两个成员,定义如下:

1
2
3
4
typedef struct DXGI_SAMPLE_DESC {
UINT Count;
UINT Quality;
} DXGI_SAMPLE_DESC;

Count成员指定每像素采样的数量,Quality用于指定所需的质量级别,“质量级别”的含义可能因硬件制造商而异。

两个参数越大,效果越好,但是开销也会更高。

质量级别的范围取决于纹理格式和每像素采样的数量。

可以使用ID3D12Device::CheckFeatureSupport方法查询给定纹理格式的质量级别数量和样本数量。

1
2
3
4
5
6
typedef struct D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS{
_In_ DXGI_FORMAT Format;
_In_ UINT SampleCount;
_In_ D3D12_MULTISAMPLE_QUALITY_LEVEL_FLAGS Flags;
_Out_ UINT NumQualityLevels;
}D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS;
1
2
3
4
5
6
7
8
9
10
11
12
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)
)
);

其中第二个参数兼具输入和输出的属性。

如果不希望使用多重采样,可以将采样数量设置为1,质量级别设置为0。

创建交换链缓冲区和深度缓冲区时都需要填写该结构体,而且设置一定要相同。

4.1.9Feature Levels

功能级别

Direct3D 11引入了特征级别的概念(在代码中由D3D_FEATURE_LEVEL枚举类型表示),它大致对应于从版本9到版本11的各种Direct3D版本:

1
2
3
4
5
6
7
8
9
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;

一个支持feature level 11的GPU必须支持整个Direct3D 11功能集。如果用户的硬件不支持某个级别,则应用程序可以退回到旧的级别。

4.1.10 DirectX 图形基础结构

DirectX Graphics Infrastructure (DXGI) 是一个与Direct3D一起使用的API。

主要目的是让多种图形API中所共有的底层人物可以借助一些通用API来进行处理。

例如,2D渲染API和3D渲染API一样需要交换链和页面翻转来获得平滑的动画,因此,交换链接口IDXGISwapChain就是DXGI API的一部分。

DXGI处理其他常见的图形功能,如全屏模式转换、枚举图形系统信息(如显示适配器、监视器和支持的显示模式,分辨率、刷新率等。它还定义了各种受支持的表面格式DXGI_FORMAT。

DXGI的一个关键接口:IDXGIFactory。它主要用于创建IDXGISwapChain接口和枚举显示适配器。显示适配器实现图形化功能。显示适配器是一个物理硬件(例如,显卡)或者是系统提供的一个软件显示适配器,用来模拟硬件图形功能。一个系统可以有多个适配器(例如有多个显卡)。适配器由IDXGIAdapter接口表示。可以用以下代码枚举系统上的所有适配器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void D3DApp::LogAdapters()
{
UINT i = 0;
IDXGIAdapter* adapter = nullptr;
std::vector 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)
{
LogAdapterOutputs(adapterList[i]);
ReleaseCom(adapterList[i]);
}
}

下面是这个方法的输出示例:

1
2
***Adapter: NVIDIA GeForce GTX 760
***Adapter: Microsoft Basic Render Driver

Microsoft Basic Render Driver在Win8及以上版本中都有。

一个系统可以有多个显示器。一个显示器就是一个显示输出(display output)。显示输出由IDXGIOutput接口表示。每个适配器都与一个或多个显示输出相关联。例如,考虑一个具有两个显卡和三个显示器的系统,其中两个显示器连接到一个显卡,而第三个显示器连接到另一个显卡。在本例中,一个适配器有两个与之关联的显示输出,而另一个适配器有一个与之关联的显示输出。可以用以下代码枚举与适配器关联的所有输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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(&desc);
std::wstring text = L”***Output: “;
text += desc.DeviceName;
text += L”\n”;
OutputDebugString(text.c_str());
LogOutputDisplayModes(output, DXGI_FORMAT_B8G8R8A8_UNORM);
ReleaseCom(output);
++i;
}
}

注意,根据文档,显卡正常的情况下,“Microsoft Basic Render Driver”没有显示输出。

每个监视器都支持一组显示模式。显示模式是指DXGI_MODE_DESC中的以下数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typedef struct DXGI_MODE_DESC {
UINT Width; // 分辨率宽度
UINT Height;// 分辨率高度
DXGI_RATIONAL RefreshRate; //刷新率
DXGI_FORMAT Format;// 显示格式
DXGI_MODE_SCANLINE_ORDER ScanlineOrdering;// 逐行扫描 隔行扫描
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;

确定显示模式的具体格式之后,可以通过以下代码获得某个显示输出对改格式所支持的全部显示模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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());
}
}

下面是这段代码的一些输出示例:

1
2
3
4
***Output: [\\.\DISPLAY2](file://./DISPLAY2)

Width = 1920 Height = 1080 Refresh = 59950/1000
Width = 1920 Height = 1200 Refresh = 59950/1000

在进入全屏模式时,枚举显示模式尤为重要。为了获得最佳的全屏性能,指定的显示模式(包括刷新率)必须与显示器支持的显示模式完全匹配。指定枚举显示模式保证了这点。

更多资料:

DXGI Overview:
https://docs.microsoft.com/zh-cn/windows/win32/direct3ddxgi/d3d10-graphics-programming-guide-dxgi?redirectedfrom=MSDN

DirectX Graphics Infrastructure:

Best Improvements:
http://msdn.microsoft.com/en-us/library/windows/desktop/ee417025(v=vs.85).aspx

DXGI 1.4 Improvements:
https://msdn.microsoft.com/en-us/library/windows/desktop/mt427784%28v=vs.85%29.aspx](https://msdn.microsoft.com/en-us/library/windows/desktop/mt427784(v=vs.85).aspx)

4.1.11检查功能支持

之前使用ID3D12Device::CheckFeatureSupport方法来检查当前图形驱动程序的多采样支持。但这只是可以用这个函数检查的其中一个特性支持,该方法的原型如下:

1
2
3
4
HRESULT ID3D12Device::CheckFeatureSupport(
D3D12_FEATURE Feature,
void *pFeatureSupportData,
UINT FeatureSupportDataSize);

译文:

Feature: D3D12_FEATURE枚举类型的一个成员,标识我们想要检查的特性的类型:

  • D3D12_FEATURE_D3D12_OPTIONS:检查当前图形驱动对各种Direct3D 12特性的支持情况。
  • D3D12_FEATURE_ARCHITECTURE:检查图形适配器中GPU的硬件体系架构特性。
  • D3D12_FEATURE_FEATURE_LEVELS:检查功能级别的支持情况。
  • 4D3D12_FEATURE_FORMAT_SUPPORT:检查对于给定纹理格式的支持情况(例如,该格式是否可以用作渲染目标,该格式是否可以与混合一起使用)。
  • D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS:检查对多重采样的支持情况。

pFeatureSupportData:指向某种数据结构的指针,存储了检测到的特定功能支持的信息。具体类型取决于Feature参数,参数与对应的具体数据结构如下:

  • D3D12_FEATURE_D3D12_OPTIONS—>D3D12_FEATURE_DATA_D3D12_OPTIONS
  • D3D12_FEATURE_ARCHITECTURE—>D3D12_FEATURE_DATA_ARCHITECTURE
  • D3D12_FEATURE_FEATURE_LEVELS—>D3D12_FEATURE_DATA_FEATURE_LEVELS
  • D3D12_FEATURE_FORMAT_SUPPORT—>D3D12_FEATURE_DATA_FORMAT_SUPPORT
  • D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS—>D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS

FeatureSupportDataSize:传回pFeatureSupportData参数中的数据结构的大小。

ID3D12Device::CheckFeatureSupport函数检查对许多特性的支持,包括一些高级特性。

有关每种功能结构的数据成员的详细信息,请参阅SDK文档。下面有一个例子展示了如何检查支持的特性级别(§4.1.9):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct D3D12_FEATURE_DATA_FEATURE_LEVELS
{
UINT NumFeatureLevels;
const D3D_FEATURE_LEVEL* pFeatureLevelsRequested;
D3D_FEATURE_LEVEL MaxSupportedFeatureLevel;
} D3D12_FEATURE_DATA_FEATURE_LEVELS;

D3D_FEATURE_LEVEL featureLevels[3] =
{
D3D_FEATURE_LEVEL_11_0, // 首先检查是否支持11
D3D_FEATURE_LEVEL_10_0, // 再检查是否支持10
D3D_FEATURE_LEVEL_9_3 //最后检查9.3
};
D3D12_FEATURE_DATA_FEATURE_LEVELS featureLevelsInfo;
featureLevelsInfo.NumFeatureLevels = 3;
featureLevelsInfo.pFeatureLevelsRequested = featureLevels;
md3dDevice->CheckFeatureSupport(D3D12_FEATURE_FEATURE_LEVELS, &featureLevelsInfo,sizeof(featureLevelsInfo));

需要注意:CheckFeatureSupport方法的第二个参数兼具输入与输出的属性。

作为输入的时候,需要先指定功能级别数组NumFeatureLevels中元素的个数,再将pFeatureSupportData指针指向功能级别数组,最后该函数把MaxSupportedFeatureLevel返回当前硬件可支持的最高功能级别。

4.1.12 Residency

资源驻留

一个复杂的游戏会使用大量的资源,比如纹理和3D网格,但是GPU并不是一直需要这些资源。

在Direct3D 12中,应用程序通过控制资源在显存中的去留,主动管理资源的驻留情况。基本的想法是最小化应用程序使用的GPU内存,因为可能没有足够的内存来存储整个游戏的所有资源,或者用户运行的其他应用程序需要GPU内存。

需要注意,考虑到性能,应用程序应该避免在很短的时间内交换相同的GPU内存中的资源,因为这样做有开销。典型的场景是游戏关卡或者区域的切换。

默认情况下,当一个资源被创建时会驻留在显存中,当资源被销毁时就会GPU中清除。

当然,有手动控制资源常驻的方法:

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

对于这两种方法,第二个参数是ID3D12Pageable资源的数组,第一个参数是数组中的资源数量。

神秘代码:mt186622

Author: 木尘痕
Link: https://muchenhen.com/2020/10/06/DirectX12-4-1-%E5%88%9D%E5%A7%8B%E5%8C%96%E9%A2%84%E5%A4%87%E7%9F%A5%E8%AF%86/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.