引言
- 查看龙书时发现,第四章介绍预备知识的代码不太利于学习。因为它不像是LearnOpenGL那样从头开始一步一步教你敲代码,导致你没有一种整体感。
- 如果你把它当作某一块的代码进行学习,你跟着敲会发现,总有几个变量是没有定义的。这是因为书上的代码都是把框架里的某一部分粘过来,缺少上文中对变量的定义,也根本不利于学习。
- 学习图形学API就是为了使用GPU进行图形运算,说白了我们学习的DirectX就是一个工具,因此熟练掌握工具、能使用工具生产作品才是最重要的。因此不妨从4.3开始学习,学到那块不会再查了解前面的预备知识就会好很多。
- 由于现在学习的代码都是框架中的一部分,因此我的学习方法是:
1. 看书学习理论,并查看书中代码。
2. 在VS中Ctrl+F搜索书中代码,对照搜索到的代码框架跟着敲。如果遇到哪个变量是未定义的,直接Ctrl+鼠标左键(点击未定义的变量),跳转到这个变量的定义后,复制定义到我的代码里。
3. 如果遇到书中未详细介绍的内容,直接在MSDN查询相关信息。 - 下图展示了我从4.3章节开始,根据龙书源代码框架,学习书本示例代码的过程:
- 本文包含的核心英文单词和释义如下:
英文单词 | 中文含义 |
---|
Create | 创建 |
Fence | 围栏 |
Factory | 工厂 |
Controller | 控制器 |
Failed | 失败 |
FEATURE | 特性 |
Level | 级别 |
Adapter | 适配器 |
Enable | 启用 |
Enum | 枚举 |
Descriptor | 描述符 |
Handle | 句柄 |
Increment | 增量 |
HEAP | 堆 |
TYPE LESS | 无类型 |
Support | 支持 |
Quality | 质量 |
Sample Count | 采样数 |
NORM | 规范 |
MULTI | 多 |
Flags | 标志 |
command | 命令 |
Queue | 队列 |
DESC | 描述 |
Allocator | 分配器 |
DIRECT | 直接的 |
Address | 住址 |
Refresh Rate | 刷新率 |
numerator | 分子 |
Denominator | 分母 |
Scanline Ordering | 扫描线排序 |
MODE | 模式 |
SCANLINE | 扫描线 |
UNSPECIFIED | 未指明 |
Scaling | 缩放 |
RENDER TARGET OUTPUT | 渲染目标输出 |
一、初始化Direct3D
步骤序号 | 步骤内容 |
---|
1 | 用 D3D12CreateDevice 函数创建 ID3D12Device 接口实例 |
2 | 创建一个 ID3D12Fence 对象,并查询描述符的大小 |
3 | 检测用户设备对 4X MSAA 质量级别的支持 |
4 | 依次创建命令队列、命令列表分配器和主命令列表 |
5 | 描述并创建交换链 |
6 | 创建应用程序所需的描述符堆 |
7 | 调整后台缓冲区的大小,并为它创建渲染目标视图 |
8 | 创建深度/模板缓冲区及与之关联的深度/模板视图 |
9 | 设置视口和裁剪矩形 |
- 书中的代码没有上下文和完整框架,不妨跟着我一起学!
- 先看书中4.3章对每一个步骤的描述,再查看我提供的完整代码,二者对照岂不美哉!
- 本文第一章分为九个小节,对应初始化Direct3d的九个步骤,每个小节第一部分提供了完整可运行代码,第二部分提供代码涉及的知识点。
(1)创建设备
1.1 完整示例
- 要初始Direct3D,必须先创建Direct3D设备(ID3D12Device)。
- Direce3D设备代表着一个显示适配器,显示适配器一般指3D图形硬件即显卡。但是显示适配器也可以用软件来代替,通过模拟硬件显卡的计算处理过程,软件也可以作为适配器(如WARP适配器)。
- Direct3D设备既可以检测系统环境对功能的支持情况,又能创建所有其他的Direct3D接口对象(如资源、视图和命令列表)。
- 创建Direct3D设备使用函数D3D12CreateDevice。
- 话不多说,直接上代码:
#include<windows.h>
#include<wrl.h>
#include<d3d12.h>
#include<dxgi1_4.h>
#include<string>
using namespace Microsoft::WRL;
inline std::wstring AnsiToWString(const std::string& str)
{
WCHAR buffer[512];
MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, buffer, 512);
return std::wstring(buffer);
}
class DxException
{
public:
DxException() = default;
DxException(HRESULT hr, const std::wstring& functionName, const std::wstring& filename, int lineNumber);
std::wstring ToString()const;
HRESULT ErrorCode = S_OK;
std::wstring FunctionName;
std::wstring Filename;
int LineNumber = -1;
};
#ifndef ThrowIfFailed
#define ThrowIfFailed(x) \
{ \
HRESULT hr__ = (x); \
std::wstring wfn = AnsiToWString(__FILE__); \
if(FAILED(hr__)) { throw DxException(hr__, L#x, wfn, __LINE__); } \
}
#endif
ComPtr<IDXGIFactory4> mdxgiFactory;
ComPtr<ID3D12Device> md3dDevice;
bool InitDirect3D()
{
#if defined(DEBUG) || defined(_DEBUG)
{
ComPtr<ID3D12Debug> debugController;
ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
debugController->EnableDebugLayer();
}
#endif
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));
HRESULT hardwareResult = D3D12CreateDevice(
nullptr,
D3D_FEATURE_LEVEL_12_0,
IID_PPV_ARGS(&md3dDevice));
if (FAILED(hardwareResult))
{
ComPtr<IDXGIAdapter> pWarpAdapter;
ThrowIfFailed(
mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));
ThrowIfFailed(D3D12CreateDevice(
pWarpAdapter.Get(),
D3D_FEATURE_LEVEL_12_0,
IID_PPV_ARGS(&md3dDevice)
));
}
}
int WINAPI WinMain(
_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nShowCmd)
{
InitDirect3D();
}
- 代码如上,大致过一遍有个记忆就行。如果对其中哪些地方有疑问或者好奇,可以在MSDN上搜索相关类型或函数查看相关信息。
1.2 相关知识
- 组件对象模型(Component Object Model,COM)是一种令DirectX不受编程语言束缚,并且使之向后兼容的技术。我们通常将COM对象视为一种接口,但由于我们的编程目的,我们可以把它当作一个C++类来使用。当我们使用C++编写DirectX程序时,COM帮我们隐藏了大量的底层细节。我们只需要知道,要获取指向某COM接口的指针,需借助特定函数或另一COM接口的方法。
- Windows运行时库(Windows Runtime Library,WRL)为COM对象提供了Microsoft::WRL::ComPtr类,它位于<wrl.h>头文件中,它是COM对象的智能指针。当一个COM对象超出作用域范围时,智能指针会调用相应COM对象的Release方法,省去了我们手动调用的麻烦。
- 书中的COM接口、对象和指针,让我晕头转向,因为我并没有接触过COM技术。分析一下,上文示例代码中有:
ComPtr<IDXGIFactory4> mdxgiFactory;
ComPtr<ID3D12Device> md3dDevice;
- 无论是IDXGIFactory4还是ID3D12Device,这两种Direct3D中的对象都是通过COM指针来表示的。因此我们现在可以知道的是:
使用C++开发Direct3D应用程序时,每个Direct3D对象都是一种COM对象或者说COM接口,我们要记录对象就要使用COM指针,我们要获取这种对象就要使用特定函数或另一COM对象的方法。 - 不必迷茫,只需要知道Direct3D对象的记录和获取方法如下即可:
ComPtr<Direct3D对象类型> 对象名。
对象名 = 特定函数();
对象名 = 其他Direct3D对象.函数();
函数名 | 描述 |
---|
Get | 返回指向此底层COM对象的指针 |
GetAddressOf | 返回指向此底层COM对象的指针的地址 |
Rest | 将ComPtr实例设置为nullptr释放与之相关的所有引用 |
- 注意:
Rest函数的作用和将ComPtr对象赋值为nullptr的效果相同。
COM对象都以大写字母 “I” 开头,例如表示设备的COM对象为ID3D12Device。
COM对象、COM接口、COM实例,都是一个意思。 - 读到这里就清楚了吧,为了使DirectX不受语言束缚,DirectX中的对象都被定义为一种以 “I” 开头的COM对象。而使用COM对象最方便的方法就是使用COM指针即ComPtr类,因此我们使用ComPtr类记录每个DirectX对象即可。
- 如果读者对Windows平台为什么要使用wstring等问题感兴趣,可以查找MSDN或其他搜索。如果对IID_PPV_ARGS宏感兴趣,可以查看书籍4.2.1即95页中间对其描述:这个宏会展开为两项,第一项根据__uuidof获取了COM对象的ID(全局唯一标识符,GUID)。IID_PPV_ARGS辅助函数本质是把指针强制转换为void类型。
#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)
(2)创建围栏并获取描述符的大小
2.1 完整示例
- CPU和GPU的同步需要使用到围栏,第一步中我们创建好设备,第二步就可以创建围栏了。
- 另外,如果使用描述符进行工作,需要获取描述符的大小。描述符在不同GPU平台上的大小不同,因此我们需要将获取的描述符大小信息缓存起来,方便需要时直接引用。
- 话不多说,直接上代码:
ComPtr<ID3D12Fence> mFence;
ThrowIfFailed(md3dDevice->CreateFence(
0,
D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(&mFence)
));
UINT mRtvDescriptorSize = 0;
UINT mDsvDescriptorSize = 0;
UINT mCbvSrvUavDescriptorSize = 0;
mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
- 上述代码直接放在第一步 InitDirect3D 函数末尾即可,需要注意的是mFence、mRtvDescriptorSize和mDsvDescriptorSize等变量应该定义为全局变量,上文为易于理解才写在函数内部。
- 可以看到新出现的四个变量都是通过 md3dDevice 对象获取到的,印证了:
1. “Direct3D12 设备对象能创建所有其他的Direct3D接口对象”。
2. “COM对象需要使用特定方法或通过其他COM对象的方法获取”。
2.2 相关知识
- 资源与描述符,详见4.1.6节。在渲染过程中,GPU需要对资源进行读和写。但是GPU和资源并不是直接绑定的,而是通过一个媒婆 “描述符” ,这个中间人进行绑定的。
- 描述符是一种对送往GPU的资源进行描述的轻量级结构,它是一个中间层。当GPU需要对资源进行读或写时,GPU就会问媒婆:“资源在哪里?我应该按照哪种数据格式进行读写?”。
- 可见描述符的作用有两点:
1. 指定资源数据。
2. 为GPU解释资源信息。 - 创建资源时可用无类型格式,如DXGI_FORMAT_R8G8B8A8_TYPELESS类型,我们知道它是4个分量组成,每个分量占8个位,但是我们不知道每个分量的8个位应该解析为整数、浮点数还是无符号整数?
- 如果某个资源在创建时采用了无类型格式,那么在为它创建描述符时必须指明其具体类型。
- 注意:视图和描述符的含义是等价的。
- 每个描述符都有一种具体类型,此类型指明了资源的具体作用。常用的描述符如下:
描述符 | 含义 |
---|
CBV | 常量缓冲区视图 |
SRV | 着色器资源视图 |
UAV | 无序访问视图 |
sampler | 采样器资源描述符 |
RTV | 渲染目标视图资源 |
DSV | 深度/模板视图资源 |
- 描述符堆中存有一系列描述符,本质上是存放用户程序中某特定类型描述符的一块内存。我们需要为每一种类型的描述符创建出单独的描述符,当然同一种描述符也可以创建多个描述符堆。
- 我们可以用不同的描述符来描述同一个资源,达到以不同的数据格式或内容部分去读写资源的目的。
- 由于创建描述符的过程中需要执行一些类型的检测和验证工作,索引最好不要在运行时才创建描述符,创建描述符的最佳时机为初始化期间。
- 当然有时确实需要使用无类型资源所带来的灵活性,此时在一定限度内,可以考虑在运行时创建描述符。
(3)检测对4X MSAA 质量级别的支持
3.1 完整示例
- 本书中我们要使用4X MSAA,之所以选择4X,是因为借助此采样数量就可以获取开销不高但性能非凡的效果。而使用4X MSAA之前,我们要先检查设备是否支持4X MSAA 质量级别的图像。
- 话不多说,直接上代码:
#include<windows.h>
#include<wrl.h>
#include<d3d12.h>
#include<dxgi1_4.h>
#include<string>
#include<assert.h>
using namespace Microsoft::WRL;
inline std::wstring AnsiToWString(const std::string& str)
{
WCHAR buffer[512];
MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, buffer, 512);
return std::wstring(buffer);
}
class DxException
{
public:
DxException() = default;
DxException(HRESULT hr, const std::wstring& functionName, const std::wstring& filename, int lineNumber);
std::wstring ToString()const;
HRESULT ErrorCode = S_OK;
std::wstring FunctionName;
std::wstring Filename;
int LineNumber = -1;
};
#ifndef ThrowIfFailed
#define ThrowIfFailed(x) \
{ \
HRESULT hr__ = (x); \
std::wstring wfn = AnsiToWString(__FILE__); \
if(FAILED(hr__)) { throw DxException(hr__, L#x, wfn, __LINE__); } \
}
#endif
ComPtr<IDXGIFactory4> mdxgiFactory;
ComPtr<ID3D12Device> md3dDevice;
bool InitDirect3D()
{
#if defined(DEBUG) || defined(_DEBUG)
{
ComPtr<ID3D12Debug> debugController;
ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
debugController->EnableDebugLayer();
}
#endif
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));
HRESULT hardwareResult = D3D12CreateDevice(
nullptr,
D3D_FEATURE_LEVEL_12_0,
IID_PPV_ARGS(&md3dDevice));
if (FAILED(hardwareResult))
{
ComPtr<IDXGIAdapter> pWarpAdapter;
ThrowIfFailed(
mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));
ThrowIfFailed(D3D12CreateDevice(
pWarpAdapter.Get(),
D3D_FEATURE_LEVEL_12_0,
IID_PPV_ARGS(&md3dDevice)
));
}
ComPtr<ID3D12Fence> mFence;
ThrowIfFailed(md3dDevice->CreateFence(
0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence)));
UINT mRtvDescriptorSize = 0;
UINT mDsvDescriptorSize = 0;
UINT mCbvSrvUavDescriptorSize = 0;
mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
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)
));
UINT m4xMsaaQuality = 0;
m4xMsaaQuality = msQualityLevels.NumQualityLevels;
assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");
}
int WINAPI WinMain(
_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nShowCmd)
{
InitDirect3D();
}
- 在此给出完整代码,若读者对第二步的完整代码不清楚,也可以在此查看。
- 可以看到我们先定义了一种资源类型的格式,那就是要查询的纹理格式。你可以把代码复制到你的项目里运行试试,当然你要记得使用Windows API开发项目,如果只会创建控制台项目,你可以看看我之前文章中的第六步。
- 详细的讲解都在代码里了,我就不多说了。我的显卡不是很好,但运行后依旧不会报错,你可以试试运行,如果报错就说明你的显卡不支持这种4X MSAA 的图像质量级别了。
- 你可以将纹理资源的格式改为:DXGI_FORMAT_R16G16B16A16_UNORM,这样的纹理显然更精细点,然后再运行试试。
- 多次运行,你发现都不会报错,你觉得要么是程序错了要么是你的显卡太棒了!那不妨试试修改采样数量,即msQualityLevels.SampleCount,将它改为8、16和32呢?
3.2 相关知识
- 多重采样技术的原理位于4.1.7小节,在书籍85页,我就不做过多阐述了。需要注意的是ID3DDevice->CheckFeatureSupport方法的第二个参数兼具输入和输出的属性。
- 如果不希望使用多重采样,可以将采用数量设置为1,并将质量级别设为0。
- 在创建交换链缓冲区和深度缓冲区时都需要填写DXGI_SAMPLE_DESC结构体。当创建后台缓冲区和深度缓冲区时,多重采样的有关设置必须相同。
- 功能支持的检测位于4.1.11小节。函数ID3D12Device::CheckFeatureSupport可以对许多功能进行检测,其第一个参数的类型是一个枚举类:D3D12_FEATURE,第二个参数是枚举类对应的数据结构指针,第三个参数是传入的数据结构变量所占字节数。
- 使用ID3D12Device::CheckFeatureSupport检测系统支持Direct3D最高版本的代码为:
#include<windows.h>
#include<wrl.h>
#include<d3d12.h>
#include<dxgi1_4.h>
#include<string>
#include<assert.h>
using namespace Microsoft::WRL;
inline std::wstring AnsiToWString(const std::string& str)
{
WCHAR buffer[512];
MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, buffer, 512);
return std::wstring(buffer);
}
class DxException
{
public:
DxException() = default;
DxException(HRESULT hr, const std::wstring& functionName, const std::wstring& filename, int lineNumber);
std::wstring ToString()const;
HRESULT ErrorCode = S_OK;
std::wstring FunctionName;
std::wstring Filename;
int LineNumber = -1;
};
#ifndef ThrowIfFailed
#define ThrowIfFailed(x) \
{ \
HRESULT hr__ = (x); \
std::wstring wfn = AnsiToWString(__FILE__); \
if(FAILED(hr__)) { throw DxException(hr__, L#x, wfn, __LINE__); } \
}
#endif
ComPtr<IDXGIFactory4> mdxgiFactory;
ComPtr<ID3D12Device> md3dDevice;
bool InitDirect3D()
{
#if defined(DEBUG) || defined(_DEBUG)
{
ComPtr<ID3D12Debug> debugController;
ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
debugController->EnableDebugLayer();
}
#endif
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));
HRESULT hardwareResult = D3D12CreateDevice(
nullptr,
D3D_FEATURE_LEVEL_12_0,
IID_PPV_ARGS(&md3dDevice));
if (FAILED(hardwareResult))
{
ComPtr<IDXGIAdapter> pWarpAdapter;
ThrowIfFailed(
mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));
ThrowIfFailed(D3D12CreateDevice(
pWarpAdapter.Get(),
D3D_FEATURE_LEVEL_12_0,
IID_PPV_ARGS(&md3dDevice)
));
}
ComPtr<ID3D12Fence> mFence;
ThrowIfFailed(md3dDevice->CreateFence(
0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence)));
UINT mRtvDescriptorSize = 0;
UINT mDsvDescriptorSize = 0;
UINT mCbvSrvUavDescriptorSize = 0;
mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
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)
));
UINT m4xMsaaQuality = 0;
m4xMsaaQuality = msQualityLevels.NumQualityLevels;
assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");
D3D_FEATURE_LEVEL feature[3] =
{
D3D_FEATURE_LEVEL_12_0,
D3D_FEATURE_LEVEL_12_1,
D3D_FEATURE_LEVEL_12_2,
};
D3D12_FEATURE_DATA_FEATURE_LEVELS featureLevelsInfo;
featureLevelsInfo.NumFeatureLevels = 3;
featureLevelsInfo.pFeatureLevelsRequested = feature;
md3dDevice->CheckFeatureSupport(
D3D12_FEATURE_FEATURE_LEVELS,
&featureLevelsInfo,
sizeof(featureLevelsInfo)
);
featureLevelsInfo.MaxSupportedFeatureLevel;
}
int WINAPI WinMain(
_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nShowCmd)
{
InitDirect3D();
}
- 我最高支持的版本是:D3D_FEATURE_LEVEL_12_1。真是太捞了,等工作有钱了去买一台intel i9 + 4090,那时候估计就不贵了,释放性能!
(4)创建命令队列和列表
4.1 完整示例
- 我们需要使用命令队列来存储GPU需要执行的命令,用命令列表来存储CPU想要提交的命令。过程非常简单,让我们直接来做吧!
- 代码如下:
#include<windows.h>
#include<wrl.h>
#include<d3d12.h>
#include<dxgi1_4.h>
#include<string>
#include<assert.h>
using namespace Microsoft::WRL;
inline std::wstring AnsiToWString(const std::string& str)
{
WCHAR buffer[512];
MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, buffer, 512);
return std::wstring(buffer);
}
class DxException
{
public:
DxException() = default;
DxException(HRESULT hr, const std::wstring& functionName, const std::wstring& filename, int lineNumber);
std::wstring ToString()const;
HRESULT ErrorCode = S_OK;
std::wstring FunctionName;
std::wstring Filename;
int LineNumber = -1;
};
#ifndef ThrowIfFailed
#define ThrowIfFailed(x) \
{ \
HRESULT hr__ = (x); \
std::wstring wfn = AnsiToWString(__FILE__); \
if(FAILED(hr__)) { throw DxException(hr__, L#x, wfn, __LINE__); } \
}
#endif
ComPtr<IDXGIFactory4> mdxgiFactory;
ComPtr<ID3D12Device> md3dDevice;
ComPtr<ID3D12Fence> mFence;
UINT mRtvDescriptorSize = 0;
UINT mDsvDescriptorSize = 0;
UINT mCbvSrvUavDescriptorSize = 0;
DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
UINT m4xMsaaQuality = 0;
void CreateCommandObjects()
{
ComPtr<ID3D12CommandQueue> mCommandQueue;
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
ThrowIfFailed(md3dDevice->CreateCommandQueue(
&queueDesc, IID_PPV_ARGS(&mCommandQueue)
));
ComPtr<ID3D12CommandAllocator> mCommandAllocator;
ThrowIfFailed(md3dDevice->CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(mCommandAllocator.GetAddressOf())
));
ComPtr<ID3D12GraphicsCommandList> mCommandList;
ThrowIfFailed(md3dDevice->CreateCommandList(
0,
D3D12_COMMAND_LIST_TYPE_DIRECT,
mCommandAllocator.Get(),
nullptr,
IID_PPV_ARGS(mCommandList.GetAddressOf())
));
mCommandList->Close();
}
bool InitDirect3D()
{
#if defined(DEBUG) || defined(_DEBUG)
{
ComPtr<ID3D12Debug> debugController;
ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
debugController->EnableDebugLayer();
}
#endif
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));
HRESULT hardwareResult = D3D12CreateDevice(
nullptr,
D3D_FEATURE_LEVEL_12_0,
IID_PPV_ARGS(&md3dDevice));
if (FAILED(hardwareResult))
{
ComPtr<IDXGIAdapter> pWarpAdapter;
ThrowIfFailed(
mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));
ThrowIfFailed(D3D12CreateDevice(
pWarpAdapter.Get(),
D3D_FEATURE_LEVEL_12_0,
IID_PPV_ARGS(&md3dDevice)
));
}
ThrowIfFailed(md3dDevice->CreateFence(
0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence)));
mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
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)
));
m4xMsaaQuality = msQualityLevels.NumQualityLevels;
assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");
CreateCommandObjects();
}
int WINAPI WinMain(
_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nShowCmd)
{
InitDirect3D();
}
- 可以看到在函数CreateCommandObjects()中我们完成了命令队列和命令列表的创建,当然为了易于理解我将命令队列、命令列表和命令分配器声明为了局部变量,你应该把它们放到函数外面作为全局变量,如下所示:
ComPtr<ID3D12CommandQueue> mCommandQueue;
ComPtr<ID3D12CommandAllocator> mCommandAllocator;
ComPtr<ID3D12GraphicsCommandList> mCommandList;
void CreateCommandObjects()
{
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
ThrowIfFailed(md3dDevice->CreateCommandQueue(
&queueDesc, IID_PPV_ARGS(&mCommandQueue)
));
ThrowIfFailed(md3dDevice->CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(mCommandAllocator.GetAddressOf())
));
ThrowIfFailed(md3dDevice->CreateCommandList(
0,
D3D12_COMMAND_LIST_TYPE_DIRECT,
mCommandAllocator.Get(),
nullptr,
IID_PPV_ARGS(mCommandList.GetAddressOf())
));
mCommandList->Close();
}
- 现在你就不会感觉好像什么都没有得到了吧!从这里我们也可以看出初始化Direct3D工作的一大重点就是:声明一系列核心的Direct3D变量,并且在初始化时获取它们!
4.2 相关知识
- 在进行图形学编程的时候,有两种处理器在参与工作:CPU和GPU。两者并行运行,但有时也需要同步。为了获得最佳性能,理想的情况下是不进行同步,但许多时候同步是必须要进行的。
- 每个GPU都至少维护着一个命令队列(本质上是环形缓冲区,即ring buffer)。使用Direct3D API,CPU可利用命令列表将命令提交到这个队列中去。
- 当一系列命令被提交至命令队列之时,它们并不会被GPU立即执行。由于GPU可能正在处理先前插入命令队列,因此新到达的命令常常会先等待再执行。
- 如果命令队列全空,会浪费GPU大量的运算能力。如果命令队列全满,会导致CPU不可避免的等待,因此会浪费CPU大量的运算能力。对于游戏这样的高性能应用程序来说,应该充分利用硬件资源,保持CPU和GPU的同时忙碌。
- ExecuteCommandLists是一种常用的ID3D12CommandQueue接口(对象)方法,利用它可将命令列表里的命令添加到命令队列中。它包含两个参数,第一个参数是UINT型变量,代表命令列表中命令的数量,第二个参数是命令列表数组的首指针。GPU会从数组中的第一个命令开始顺序执行。
- ID3D12GraphicsCommandList接口封装了一系列图形渲染命令,它实际上继承于ID3D12CommandList接口。调用其方法即可向命令列表添加命令,但需要注意的是命令并不会立即执行,其方法仅仅是将命令加入了命令列表而已。调用ExecuteCommandLists方法才会将命名真正地送入命令队列,供GPU在合适的时机处理。
- 随着内容不断深入,我们将逐步掌握D3D12GraphicsCommandList所支持的各种命令。当命令被加入命令列表后,我们必须调用D3D12GraphicsCommandList::Close()方法来结束命令的记录。在调用ExecuteCommandLists方法将命令提交给命令列表之前,一定要先将其关闭,即调用Close方法。
- 我们可以通过ID3D12Device->GetNodeCount()方法查询系统中GPU适配器节点(物理GPU)的数量。
- 很明显每个命令列表对应一个命令分配器,那么一个命令分配器是否可以对应多个命令列表呢?答案是可以的,我们可以让一个命令分配器关联多个命令列表,但必须关闭同一命令分配器的其他命令列表,即任何时候只能有一个命令列表处于打开状态。这保证了命令列表中的所有命令都会按顺序连续地添加到命令分配器内,即只有当正在使用的命令列表使用完已关闭时,其他命令列表才可以使用。
- 注意,当创建或重置一个命令列表时,它会处于 “打开” 的状态,因此你如果为同一个命令分配器连续创建或重置两个命令列表时,就会得到报错信息。
- 在调用ExecuteCommandLists方法将命令列表中的命令提交给命令队列后,我们就可以通过D3D12GraphicsCommandList::Reset方法将命令列表恢复为刚创建时的初态。此方法功能类似于std::vector::clear方法,仅使得存储元素的数量归零,但是仍保持其当前的容量。借助此方法,我们就可以继续复用其底层内存,避免释放列表再新建的繁琐操作。
- 命令队列可能会引用命令分配器中的数据,但是重置命令列表却不会影响命令队列,这是因为相关的命令分配器仍在维护着其内存中被命令队列引用的一系列命令。因此当调用ExecuteCommandLists方法后就可以重置命名列表。但是注意,在没有确定GPU执行完命令分配器中的所有命令之前,千万不要重置命令分配器!
- 因此差不多会在每一帧重置命令列表,但命令分配器却不一定。
(5)描述并创建交换链
5.1 完整示例
#include<windows.h>
#include<wrl.h>
#include<d3d12.h>
#include<dxgi1_4.h>
#include<string>
#include<assert.h>
using namespace Microsoft::WRL;
inline std::wstring AnsiToWString(const std::string& str)
{
WCHAR buffer[512];
MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, buffer, 512);
return std::wstring(buffer);
}
class DxException
{
public:
DxException() = default;
DxException(HRESULT hr, const std::wstring& functionName, const std::wstring& filename, int lineNumber);
std::wstring ToString()const;
HRESULT ErrorCode = S_OK;
std::wstring FunctionName;
std::wstring Filename;
int LineNumber = -1;
};
#ifndef ThrowIfFailed
#define ThrowIfFailed(x) \
{ \
HRESULT hr__ = (x); \
std::wstring wfn = AnsiToWString(__FILE__); \
if(FAILED(hr__)) { throw DxException(hr__, L#x, wfn, __LINE__); } \
}
#endif
ComPtr<IDXGIFactory4> mdxgiFactory;
ComPtr<ID3D12Device> md3dDevice;
ComPtr<ID3D12Fence> mFence;
UINT mRtvDescriptorSize = 0;
UINT mDsvDescriptorSize = 0;
UINT mCbvSrvUavDescriptorSize = 0;
DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
UINT m4xMsaaQuality = 0;
ComPtr<ID3D12CommandQueue> mCommandQueue;
ComPtr<ID3D12CommandAllocator> mCommandAllocator;
ComPtr<ID3D12GraphicsCommandList> mCommandList;
int mClientWidth = 800;
int mClientHeight = 600;
const int SwapChainBufferCount = 2;
HWND mhMainWnd = nullptr;
ComPtr<IDXGISwapChain> mSwapChain;
void CreateSwapChain()
{
mSwapChain.Reset();
DXGI_SWAP_CHAIN_DESC sd;
sd.BufferDesc.Width = mClientWidth;
sd.BufferDesc.Height = mClientHeight;
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.BufferDesc.Format = mBackBufferFormat;
sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
sd.SampleDesc.Count = 1;
sd.SampleDesc.Quality = 0;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.BufferCount = SwapChainBufferCount;
sd.OutputWindow = mhMainWnd;
sd.Windowed = true;
sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
ThrowIfFailed(mdxgiFactory->CreateSwapChain(
mCommandQueue.Get(),
&sd,
mSwapChain.GetAddressOf()
));
}
void CreateCommandObjects()
{
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
ThrowIfFailed(md3dDevice->CreateCommandQueue(
&queueDesc, IID_PPV_ARGS(&mCommandQueue)
));
ThrowIfFailed(md3dDevice->CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(mCommandAllocator.GetAddressOf())
));
ThrowIfFailed(md3dDevice->CreateCommandList(
0,
D3D12_COMMAND_LIST_TYPE_DIRECT,
mCommandAllocator.Get(),
nullptr,
IID_PPV_ARGS(mCommandList.GetAddressOf())
));
mCommandList->Close();
md3dDevice->GetNodeCount();
}
bool InitDirect3D()
{
#if defined(DEBUG) || defined(_DEBUG)
{
ComPtr<ID3D12Debug> debugController;
ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
debugController->EnableDebugLayer();
}
#endif
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));
HRESULT hardwareResult = D3D12CreateDevice(
nullptr,
D3D_FEATURE_LEVEL_12_0,
IID_PPV_ARGS(&md3dDevice));
if (FAILED(hardwareResult))
{
ComPtr<IDXGIAdapter> pWarpAdapter;
ThrowIfFailed(
mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));
ThrowIfFailed(D3D12CreateDevice(
pWarpAdapter.Get(),
D3D_FEATURE_LEVEL_12_0,
IID_PPV_ARGS(&md3dDevice)
));
}
ThrowIfFailed(md3dDevice->CreateFence(
0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence)));
mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
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)
));
m4xMsaaQuality = msQualityLevels.NumQualityLevels;
assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");
CreateCommandObjects();
CreateSwapChain();
}
#include<Windows.h>
#define WINDOW_WIDTH 800
#define WINDOW_HEIGHT 600
#define WINDOW_TITLE L"GameEngine"
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(
_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nShowCmd)
{
SetProcessDPIAware();
int cx = GetSystemMetrics(SM_CXSCREEN);
int cy = GetSystemMetrics(SM_CYMAXTRACK);
WNDCLASSEX wndClass = { 0 };
wndClass.cbSize = sizeof(WNDCLASSEX);
wndClass.style = CS_DBLCLKS | CS_NOCLOSE | CS_VREDRAW | CS_HREDRAW;
wndClass.lpfnWndProc = WndProc;
wndClass.cbClsExtra = 0;
wndClass.cbWndExtra = 0;
wndClass.hInstance = hInstance;
wndClass.hIcon = (HICON)::LoadImage(NULL, L"Image.ico", IMAGE_ICON, 0, 0,
LR_DEFAULTSIZE | LR_LOADFROMFILE);
wndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndClass.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH);
wndClass.lpszMenuName = NULL;
wndClass.lpszClassName = L"ForTheDreamOfGameDevelop";
if (!RegisterClassEx(&wndClass))
return -1;
mhMainWnd = CreateWindow(L"ForTheDreamOfGameDevelop",
WINDOW_TITLE,
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, WINDOW_WIDTH,
WINDOW_HEIGHT, NULL, NULL, hInstance, NULL);
InitDirect3D();
MoveWindow(mhMainWnd, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, true);
ShowWindow(mhMainWnd, nShowCmd);
UpdateWindow(mhMainWnd);
MSG msg = { 0 };
while (msg.message != WM_QUIT)
{
if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
UnregisterClass(L"ForTheDreamOfGameDevelop", wndClass.hInstance);
return 0;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_PAINT:
ValidateRect(hwnd, NULL);
break;
case WM_KEYDOWN:
if (wParam == VK_ESCAPE)
DestroyWindow(hwnd);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hwnd, message, wParam, lParam);
}
return 0;
}