Hello World
第零步永远是环境配置
VS新建一个项目
在预编译头中链接跟D3D有关的lib,这事儿就成了。
1 |
顺便可以引用一些常用的头文件这一步可以等一会.
1 |
|
Hello World之前,来获取你的显卡信息吧
因为DX的概念实在过于纷繁复杂,在开始正式的旅程之前.我们可以做个小热身.
让我们来写一个最简单的D3D程序,来获取设备具有的显示适配器,它仅仅用到了一点点DXGI的功能.
DXGI DirectX Graphics Infrastructure 是一套用来枚举显示器,显示模式,选择缓冲区格式,在进程间共享资源,为窗口或者监视器呈现渲染的帧的底层接口。
DXGI的主要目的是管理可以独立于DirectX运行时的底层任务.
程序员可以直接访问DXGI,也可以通过Direct3d API来处理与DXGI之间的交互。
让我们首先考虑新建一个DXGI工厂,他能生产一系列的DXGI对象,简单的说就是工具人(并不).
先让我们新建一个类,并在类中添加DXGI工厂的指针作为成员.1
IDXGIFactory* m_dxgi_factory_ = nullptr;
在构造函数中初始化它.1
HRESULT hr = CreateDXGIFactory(IID_PPV_ARGS(&m_dxgi_factory_));
之后新建一个输出显示器的函数,这里作为示例,我们直接让它弹窗.1
2
3
4
5
6
7
8
9
10
11UINT i = 0;
IDXGIAdapter* adapter = nullptr;
//通过工厂我们可以枚举所有的显示适配器
while (m_dxgi_factory_->EnumAdapters(i,&adapter) != DXGI_ERROR_NOT_FOUND) {
DXGI_ADAPTER_DESC desc;
adapter->GetDesc(&desc);
std::wstring wstr = L"Adapter info:\n"; //Unicode字符集下
wstr += desc.Description;
MessageBox(nullptr, wstr.c_str(), L"Error", 0);
++i;
}
输出了显卡的型号
可以看到在这里我们获取了适配器的描述符
1 | adapter->GetDesc(&desc); |
描述符用来描述资源的各类属性.这里留到后面再讲.
好的,在确定了一切没有问题后,就可以正式开始探索D3D的奥秘了。
一个并不是那么简单的Hello world程序(1)
Debug layer
首先让我们打开调试层DebugLayer.
1 | D3D12GetDebugInterface(IID_PPV_ARGS(m_debug_controller_.GetAddressOf())); |
我们来看一下代码.第一行获取Debug接口,并将其赋值给mdebug_controller.在D3D中,存在大量填入指针的引用来给指针赋值的函数. 而IID_PPV_ARGS是一个常用的宏.能极大的简化一些函数的调用
1 |
EnableDebugLayer() 启用D3D的调试层.能够使VS的输出界面捕获到D3D的调试信息.
注意EnableDebugLayer() 这个函数在千万不能再D3DDevice创建后在调用,否则会直接造成Device Remove.导致Device无效.
因此DebugLayer建议放置在最先调用.
.Windows窗体和D3D环境的创建
我们的第一步,是构造D3D环境.在此之前,先介绍一下组件对象模型的概念.也就是COM Component Object Model.DirectX提供了一组的COM供用户使用,这些COM在C++里可以被视作c++类.
为了辅助管理COM对象的生命周期,可以使用ComPtr类来管理COM组件
比如1
ComPtr<ID3D12RootSignature> m_root_signature
我们假定我们构造一个D3DAPP类来实现最简单的功能.我们先引入两个辅助的头文件.这两个辅助头文件都可以在微软官方的示例中找到.
也就是d3dx12.h和DXSampleHelper.h.引入这两个头文件并不会妨碍理解D3D的运作原理.这些函数和类只是简单地封装.
d3dx12.h提供了一些方便的接口,能使得我们的开发更高效.
而DXSampleHelper.h中的函数能够使得学习过程省去不少麻烦.尽管未来大概率会重构一遍.
但是学习的时候就要一步步来嘛.
首先我们构造一个大体的框架.让我们首先构造一个能够独立运行的windows app.构建这样的app很简单.只需要创建一个windows项目即可.
点击调试会创建这样的窗口.这个窗口很烦.因为有冗杂的菜单栏.
让我们删掉这个多余的菜单,第一步我们根本用不着它.
在register class,也就是注册窗口类的函数中把这个菜单置空.对应的资源就没必要删除了.1
wcex.lpszMenuName = nullptr;
如果对这方面不太熟悉.那么说明还需要理解一下基本的Windows编程.尽管完全不理解问题也不大.
干掉之后,整个界面就清爽了
现在,基本的窗口也有了.但是这个窗口什么也不能显示.接下来就是我们的工作了.
我们先在预编译头引入关键的头文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Windows 头文件
// C 运行时头文件
我们假定把功能都写在一个App类里.首先,新建一个这样的App类
1 |
|
我们首先引入一个DXGI工厂.这个工厂能为我们创建一些必要的资源.也是比较主要的入口.
1 | //in App.h |
后面那个4是类似版本号的玩意.数字越高证明这个工厂的接口被设计的越牛皮.然而本质上是没有太大区别的.既然被设计为向下兼容,区别只在于他们的调用方式.
因为Microsoft官网的Sample用的是4,这里也用4作为factory吧.
在构造函数里,我们得先开启DebugLayer.1
2
3
4
5
6
7
8
9
10
11
12
13
14 UINT dxgiFactoryFlags = 0;
{
ComPtr<ID3D12Debug> debugController;
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)))){
debugController->EnableDebugLayer();
// Enable additional debug layers.
// dxgiFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG;
}
}
}
没啥可说的.之前已经介绍过了.而上述代码直接从微软官方那拷贝下来的.
1 | dxgiFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG; |
上面这段注释掉.因为我们接下来用的例子不会运用这个flag
接下来创建DXGI工厂,方便起见,我们直接获取默认的显卡适配器.
1 | ThrowIfFailed(CreateDXGIFactory( IID_PPV_ARGS(&factory))); |
有了适配器之后,我们可以创建一个D3DDevice了
1 | ThrowIfFailed(D3D12CreateDevice( |
同时引入该device作为成员属性.然后我们创建一个commandQueue,命令队列接收CPU提交的命令,GPU从中读取并执行这些命令.后面我们会详细讲道它.现在先让我们把它创建起来.并引入为成员变量.
1 | D3D12_COMMAND_QUEUE_DESC queueDesc = {}; |
在此之后,我们创建一个交换链(swap_chain)的描述.(不是描述符)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
swapChainDesc.BufferCount = frame_count;
swapChainDesc.Width = m_width;
swapChainDesc.Height = m_height;
swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
swapChainDesc.SampleDesc.Count = 1;
ComPtr<IDXGISwapChain1> swapChain;
ThrowIfFailed(factory->CreateSwapChainForHwnd(
m_commandQueue.Get(),
m_hwnd,
&swapChainDesc,
nullptr,
nullptr,
&swapChain
));
注意这里将m_hwnd,m_width等引入为成员变量,等一会我们会给它赋上值.frame_count应该是一个常量,这里将它设置为2.也就双重缓冲.
这里有很多陌生的概念,我们来介绍一下.
swap chain 交换链
交换链是一种优化机制.为了减少当前帧画面的绘制时间.可以先将下一帧的内容绘制在后台缓冲区(可能多个).将当前内容绘制在前台缓冲区.
当绘制下一帧的时候,前后缓冲区互相交换.也就是交换.
通常而言.双重缓冲已经足够了.
描述符
这是一种类似句柄的东西,它用来描述一种D3D的资源.D3D的资源通常可以复用.这样可以用不同的描述符来描述同一个资源.
一个描述符是以一种对GPU不透明(opaque)的形式,轻量的描述一个GPU对象的一组数据.
但我们通过DXGI工厂创建交换链,以及通过Device创建命令队列的时候,这里用的只是对普通对象的描述,简单理解为类似一种配置就行了.
1 | ThrowIfFailed(swapChain.As(&m_swapChain)); |
看到上述代码的对象描述中有很多的常量.我们可以做一下简短的介绍
英文能力强的话完全可以看文档来理解,虽然我觉得也没人会看我的这篇博客.
常量名 | 属性名称 | 简单解释 |
---|---|---|
D3D12_COMMAND_QUEUE_FLAG_NONE | Flags | 表示一个默认的命令队列 |
D3D12_COMMAND_LIST_TYPE_DIRECT | Type | 表示该命令缓冲区是直接被GPU执行的 |
DXGI_FORMAT_R8G8B8A8_UNORM | Format | 表示这个交换链的显示格式是R8G8B8A8,这里的常量是DXGI_FORMAT的一系列枚举值当中的一个 |
DXGI_USAGE_RENDER_TARGET_OUTPUT | BufferUsage | 拿来干啥的:这个交换链是输出给RENDER TARGET的. |
DXGI_SWAP_EFFECT_FLIP_DISCARD | SwapEffect | Present 呈现后丢掉back buffer的东西.flip是一种呈现模式,这在win8之后引入.这里的DISCARD是win10的特性,(毕竟这篇博客也是根据dx12来写的. |
不要急,Direct的概念就是扎堆的.很难拆成一步步的小例子来.所以这对完全没有图形编程经验的新人程序员非常不友好.但是无论如何,我们不得不尝试理解这些概念.毕竟我们到现在连第一步都没迈出去.
我们接下来要创建一个描述符堆
描述符堆
粗要的解释,就是一个描述符的容器.
要创建一个描述符堆,自然也需要一个描述符堆的描述,
我们可以看到很多D3D的资源都是采用这种方式来创建的.
在这里我们要创建swap chain对应的render target view的描述符堆.有了这个描述符堆,我们就可以为我们的swap_chain对应的两个缓冲区所对应的frame创建对应的RTV了.
1 | { |
RTV render_target_view
这里的view 的意思是”对应格式所需要的数据”,比如说,Constant buffer view (CBV) 的意思是正确格式的常量缓冲数据.所以不要被view这个词汇困扰了.
RTV其实就是一组数据.
接下来我们为每个frame(back buffer)创建对应的RTV.
注意这里的CPUDescriptorHandleForHeapStart,获取堆的首个资源的句柄.这个虽然叫做句柄,但确出乎意料的有着移位的功能,当然这是因为这个CD3DX12_CPU_DESCRIPTOR_HANDLE是经过封装的,他的是对D3D12_CPU_DESCRIPTOR_HANDLE这个基类的唯一成员,做对应的加法.从而达到一种类似枚举的效果.
1 | { |
这里的最后我们创建了一个命令分配器.嗯,姑且这么称呼吧.
以上,D3D的Hello World程序的第一部分终于搭建完毕了.和以往的Hello World比,这确实是很令人头大的玩意.
命令与同步
我们现在得到了一个App类,修改一下App的构造函数.引入必要的参数.
1 | class App { |
这样,我们继续我们的下一步.
要实现CPU和GPU的通信,除了命令队列外,还需要一个命令表.CPU负责将命令列表(CommandList)中的命令提交到命令队列上去.注意这里引入的是ID3D12GraphicCommandList的接口.这个接口是ID3D12CommandList的一个继承.
1 |
|
这些命令列表的命令实际上是存储在另一个在之前已经创建的叫做命令分配器的结构中。
这个命令列表初始化创建出来之后,是处于打开状态的。对于同一个命令分配器,同时只能为单一的命令列表分配命令。由于我们在初始化阶段,所以先将这个命令列表关闭。因此将渲染管线的状态设置为nullptr,
之后,我们创建一个同步围栏.围栏是使得GPU与CPU同步的一种机制.
DX龙书里举了个例子,比方说有一种资源R,存储着几何体的位置信息.在 CPU对R进行位置更新p1后,向命令队列里添加了对于位置p1的信息.随后如果CPU将资源位置更新为p2,那么便有可能在GPU之前绘制命令前发送这种情况.这不是我们想要的。
那么我们可以强制CPU等待到GPU执行命令队列中的命令达到某一指定的围栏点。这种方法被称为冲洗(龙书上叫刷新)命令队列(Flushing the command queue)。
我们创建一个围栏
1 | { |
注意围栏对象维护着一个UINT64类型的值,此为标识围栏点的整数。每当需要一个新的围栏点时,我们给这个标识+1.并发送信号给GPU.
好了,一切准备就绪.现在我们开始编写我们的Hello World程序.
此时我们的构造函数代码是这样的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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105 UINT dxgiFactoryFlags = 0;
// Enable the debug layer (requires the Graphics Tools "optional feature").
// NOTE: Enabling the debug layer after device creation will invalidate the active device.
{
ComPtr<ID3D12Debug> debugController;
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
{
debugController->EnableDebugLayer();
// Enable additional debug layers.
dxgiFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG;
}
}
ThrowIfFailed(CreateDXGIFactory(IID_PPV_ARGS(&factory)));
ComPtr<IDXGIAdapter> warpAdapter;
ThrowIfFailed(factory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter)));
ThrowIfFailed(D3D12CreateDevice(
warpAdapter.Get(),
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&m_device)
));
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
ThrowIfFailed(m_device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_commandQueue)));
DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
swapChainDesc.BufferCount = frame_count;
swapChainDesc.Width = m_width;
swapChainDesc.Height = m_height;
swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
swapChainDesc.SampleDesc.Count = 1;
ComPtr<IDXGISwapChain1> swapChain;
ThrowIfFailed(factory->CreateSwapChainForHwnd(
m_commandQueue.Get(), // Swap chain needs the queue so that it can force a flush on it.
m_hwnd,
&swapChainDesc,
nullptr,
nullptr,
&swapChain
));
// This sample does not support fullscreen transitions.
ThrowIfFailed(factory->MakeWindowAssociation(m_hwnd, DXGI_MWA_NO_ALT_ENTER));
ThrowIfFailed(swapChain.As(&m_swapChain));
m_frameIndex = m_swapChain->GetCurrentBackBufferIndex();
// Create descriptor heaps.
{
// Describe and create a render target view (RTV) descriptor heap.
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
rtvHeapDesc.NumDescriptors = frame_count;
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
ThrowIfFailed(m_device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&m_rtvHeap)));
m_rtvDescriptorSize = m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
}
// Create frame resources.
{
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart());
// Create a RTV for each frame.
for (UINT n = 0; n < frame_count; n++)
{
ThrowIfFailed(m_swapChain->GetBuffer(n, IID_PPV_ARGS(&m_renderTargets[n])));
m_device->CreateRenderTargetView(m_renderTargets[n].Get(), nullptr, rtvHandle);
rtvHandle.Offset(1, m_rtvDescriptorSize);
}
}
ThrowIfFailed(m_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&m_commandAllocator)));
// Create the command list.
ThrowIfFailed(m_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_commandAllocator.Get(), nullptr, IID_PPV_ARGS(&m_commandList)));
// Command lists are created in the recording state, but there is nothing
// to record yet. The main loop expects it to be closed, so close it now.
ThrowIfFailed(m_commandList->Close());
// Create synchronization objects.
{
ThrowIfFailed(m_device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_fence)));
m_fenceValue = 1;
// Create an event handle to use for frame synchronization.
m_fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (m_fenceEvent == nullptr)
{
ThrowIfFailed(HRESULT_FROM_WIN32(GetLastError()));
}
}
(上述代码基本是copy忍者。
可以将这些初始化环境的CODE抽离构造函数,并放入Init函数中.
你好,世界
我们先给App添加一个函数,使得它在WM_PAINT的时候被调用而开启渲染.直接了当的写上这些代码,接下来在WM_PAINT中调用它.
1 | ThrowIfFailed(m_commandAllocator->Reset()); |
第一段我们重置了commandlist.和对应的分配器.以重写填入命令.
接下来的步骤都是为了我们填入清空Render Target而服务的.我们通过ResourceBarrier将资源转化成对应的形式.并最终让swap_chainc呈现.
然而很糟糕的是microsoft的官方代码在我这一直报Error,哪怕他完全能够运行.我自己写的也没有逃过这个命运:
D3D12 ERROR: ID3D12CommandQueue::ExecuteCommandLists: Using ResourceBarrier on Command List (0x00000125883F0BA0:’Unnamed ID3D12GraphicsCommandList Object’): Before state (0x4: D3D12RESOURCE_STATE_RENDER_TARGET) of resource (0x00000125875A9240:’Unnamed ID3D12Resource Object’) (subresource: 0) specified by transition barrier does not match with the state (0x0: D3D12_RESOURCE_STATE[COMMON|PRESENT]) specified in the previous call to ResourceBarrier [ RESOURCE_MANIPULATION ERROR #527: RESOURCE_BARRIER_BEFORE_AFTER_MISMATCH]
官网对这个Error的解释是:
If the before states passed to the ResourceBarrier do not match the after states of previous calls to ResourceBarrier, including the aliasing case.
就理论而言,这不应该会发生才对.实际上把这段程序丢给别人的机子上运行的时候,也没有出现这样的问题.
目前还没有解决这个问题,而是换了个调试的东西.