今天正式开始我的DX12学习,我参考的书籍是《DirectX12 3D游戏开发实战》,以下我们还是尊称它为《龙书》,辅助参考书《Directx12编程指南》,IDE是VS2019。在DX初始化中我感觉主要是搞明白各个基础模块的初始化和他们间的逻辑前后顺序。
首先说下DX12使用的COM组件编程。面向COM组件编程是一种可以跨应用跨语言,共享二进制代码的编程方法。为了管理COM对象的生命周期,代码中大量使用了ComPtr(指向COM对象的智能指针),当一个ComPtr实例超出作用域范围时,他会自动调用相应的COM对象的Release方法。
ComPtr用法如下:
ComPtr
我们首先新建一个Win32的桌面空项目。
![8182b6f39faa5a63e3e83aa3e27fd99f.png](https://img-blog.csdnimg.cn/img_convert/8182b6f39faa5a63e3e83aa3e27fd99f.png)
引入我们需要的头文件,链接库文件,以及使用ComPtr的命名空间。
#include
声明主窗口句柄和窗口过程的回调函数。
HWND
书写WinMain主函数。主函数相当于C++中的Main入口函数。在主函数中首先是一个调试程序的固定写法。
int
接下来我们要初始化Windows窗口。主要分为三步,第一步是创建并显示窗口,第二步是在消息循环中检测消息,第三步是将接收到的消息分派给窗口过程。
创建并显示窗口
//窗口初始化描述结构体(WNDCLASS)
消息循环中检测消息
//消息循环
将接收到的消息分派给窗口过程
//窗口过程函数
写到这里,我们已经完成了窗口的初始化。直接运行可以看到一个背景纯白的窗口。
![703e550f3091e02739e43b10d65d9498.png](https://img-blog.csdnimg.cn/img_convert/703e550f3091e02739e43b10d65d9498.png)
接下来我们定义ThrowIfFailed宏。D3D12大多数创建函数会返回HRESULT,ThrowIfFailed宏可以接收HRESULT值,从而判断是否创建成功。这个宏展开后,是个可以抛出异常的函数,并能将对应文件名行号显示出来。注意:这里的ToString函数用到了<comdef.h>头文件,需要包含。
//AnsiToWString函数(转换成宽字符类型的字符串,wstring)
接下来我们要初始化DirectX12了。大致步骤如下:
- 开启D3D12调试层。
- 创建设备。
- 创建围栏,同步CPU和GPU。
- 获取描述符大小。
- 设置MSAA抗锯齿属性。
- 创建命令队列、命令列表、命令分配器。
- 创建交换链。
- 创建描述符堆。
- 创建描述符。
- 资源转换。
- 设置视口和裁剪矩形。
- 设置围栏刷新命令队列。
- 将命令从列表传至队列。
我们将各个模块封装成函数。首先是创建设备。我们先来了解下DXGI API,设计DXGI的基本理念是使用多种图形API中所共有的底层任务能借助一组通用API来进行处理。而我们的设备即是通过DXGI API的IDXGIFactory接口来创建的。所以我们先要CreateDXGIFactory,然后再CreateDevice。
void
接下来是通过设备创建围栏fence,以便之后同步CPU和GPU。
void
然后我们获取描述符大小,这个大小可以让我们知道描述符堆中每个元素的大小(描述符在不同的GPU平台上的大小各异),方便我们之后在地址中做偏移来找到堆中的描述符元素。这里我们获取三个描述符大小,分别是RTV(渲染目标缓冲区描述符)、DSV(深度模板缓冲区描述符)、CBV_SRV_UAV(常量缓冲区描述符、着色器资源缓冲描述符和随机访问缓冲描述符)。
void
再然后是设置MSAA抗锯齿属性。先填充多重采样属性结构体,然后通过CheckFeatureSupport函数设置NumQualityLevels。注意:此处不使用MSAA,采样数量设置为0。
void
再然后创建命令队列、命令列表和命令分配器。他们三者的关系是:首先CPU创建命令列表,然后将关联在命令分配器上的命令传入命令列表,最后将命令列表传入命令队列给GPU处理。这一步只是做了三者的创建工作。注意,我们在初始化D3D12_COMMAND_QUEUE_DESC时,只初始化了两项,其他两项我们在大括号中默认初始化了。
void
接下来我们要创建交换链了,交换链中存着渲染目标资源,即后台缓冲区资源。我们通过上文中提到过的DXGI API下的IDXGIFactory接口来创建交换链。这里我们还是禁用MSAA多重采样。因为其设置比较麻烦,这里直接设置MSAA会出错,所以count还是为1,质量为0。还要注意一点,CreateSwapChain函数的第一个参数其实是命令队列接口指针,不是设备接口指针,参数描述有误导。
void
接下来我们创建描述符堆(descriptorHeap),描述符堆是存放描述符的一段连续内存空间。因为是双后台缓冲,所以我们要创建存放2个RTV的RTV堆,而深度模板缓存只有一个,所以创建1个DSV的DSV堆。具体过程是,先填充描述符堆属性结构体,然后通过设备创建描述符堆。
void
有了描述符堆之后,我们就可以创建堆中的描述符了,我们首先创建RTV。具体过程是,先从RTV堆中拿到首个RTV句柄,然后获得存于交换链中的RT资源,最后创建RTV将RT资源和RTV句柄联系起来,并在最后根据RTV大小做了在堆中的地址偏移。
注意这里用到了CD3DX12_CPU_DESCRIPTOR_HANDL,这个变体在d3dx12.h头文件中定义,DX库并没有集成,需要我们自行下载。这里的CD3DX12_CPU_DESCRIPTOR_HANDLE是个变体类,它的构造函数初始化了D3D12_CPU_DESCRIPTOR_HANDLE结构体中的元素,所以直接调用其构造函数即可。之后我们会经常用到CD3DX12开头的变体类来简化初始化的代码书写,用法和作用基本一致。
void
创建完RTV后,我们就要创建DSV了。具体过程是,先在CPU中创建好DS资源,然后通过CreateCommittedResource函数将DS资源提交至GPU显存中,最后创建DSV将显存中的DS资源和DSV句柄联系起来。
void
创建完描述符后,我们需要标记DS资源的状态。为什么要标记呢?因为资源在不同的时间段有着不同的作用,比如,有时候它是只读,有时候又是可写入的。我们用ResourceBarrier下的Transition函数来转换资源状态。
cmdList
等所有命令都进入cmdList后,还需要用ExecuteCommandLists函数,将命令从命令列表传入命令队列,也就是从CPU传入GPU的过程。注意:在传入命令队列前必须关闭命令列表。
ThrowIfFailed
为了使CPU和GPU同步,我们之前已经创建了围栏接口,现在需要实现围栏代码。实现思想是,围栏初始值为0(可理解为GPU端的围栏值),现我们定义一个当前围栏值也为0(可理解为CPU端的围栏值),当CPU将命令传递至GPU后,当前围栏值++(CPU围栏++),而当GPU处理完CPU传来的命令后,围栏值++(GPU围栏++),然后判定围栏值(CPU围栏值)和当前围栏值(GPU围栏值)的大小,来确定GPU是否命中围栏点,如果没有命中,则等待命中后触发事件。
int
接下来我们设置视口和裁剪矩形。
void
到这里为止我们基本的代码模块都写好了。由于DX12初始化篇幅比较长,所以我分了几篇来写。未完待续。。。。。。