d3d12龙书阅读----Direct3D的初始化
使用d3d我们可以对gpu进行控制与编程,以硬件加速的方式来完成3d场景的渲染,d3d层与硬件驱动会将相应的代码转换成gpu可以执行的机器指令,与之前的版本相比,d3d12大大减少了cpu的开销,同时也改进了对多线程的支持,但是使用的api也更加复杂。
接下来,我们将先介绍在d3d初始化中一些重要的概念,之后通过具体的代码进行介绍。
组件对象模型(com)
COM 在 D3D 编程中提供了一种结构化和标准化的方式来处理对象和接口,有助于简化图形编程的复杂性,并提高代码的兼容性和可维护性
在使用com对象时,com对象会统计其引用次数,因此,在使用完com接口之后,我们需要使用它的release方法,当com对象的引用次数为0时,它将自己释放它所占的内存。
为了辅助管理com对象的生存周期,我们可以使用Microsoft::WRL::ComPtr类,我们可以把它当做是com对象的智能指针,当一个ComPtr超出了作用域的范围,它便会自动调用相应Com对象的release方法,免去了我们手动调用的麻烦。
定义举例:
//DXGI接口
Microsoft::WRL::ComPtr<IDXGIFactory4> mdxgiFactory;
Microsoft::WRL::ComPtr<IDXGISwapChain> mSwapChain;
//D3D接口
Microsoft::WRL::ComPtr<ID3D12Device> md3dDevice;
Microsoft::WRL::ComPtr<ID3D12Fence> mFence;
UINT64 mCurrentFence = 0;
Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
Microsoft::WRL::ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> mCommandList;
static const int SwapChainBufferCount = 2;
int mCurrBackBuffer = 0;
Microsoft::WRL::ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];
Microsoft::WRL::ComPtr<ID3D12Resource> mDepthStencilBuffer;
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mRtvHeap;
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mDsvHeap;
comptr的常用方法:
1.Get方法:返回一个指向此底层com接口的指针 一般将原始的接口指针作为参数传递给函数
Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
ThrowIfFailed(mdxgiFactory->CreateSwapChain(
mCommandQueue.Get(),
&sd,
mSwapChain.GetAddressOf()));
2.GetAddress方法:返回指向此底层com接口指针的地址 凭借此方法可利用函数参数返回接口指针
Microsoft::WRL::ComPtr<IDXGISwapChain> mSwapChain;
ThrowIfFailed(mdxgiFactory->CreateSwapChain(
mCommandQueue.Get(),
&sd,
mSwapChain.GetAddressOf()));
3.Reset方法:将comptr设置为nullptr并且释放与之相关的所有引用
Microsoft::WRL::ComPtr<ID3D12Resource> mDepthStencilBuffer;
mDepthStencilBuffer.Reset();
纹理
在本书中,纹理涉及的范围较广,可以把它看成是由数据元素构成的矩阵(1D/2D/3D),可以存储贴图信息与缓冲区信息等等。
缓冲区包括前台缓冲区,后台缓冲区,深度缓冲区,模版缓冲区等等。
其中前台与后台缓冲区,前台缓冲区存储的是当前显示在屏幕上的图像数据,而下一帧的图像数据绘制在后台缓冲区中,当后台缓冲区绘制完成之后,两种缓冲区的角色互换,只需交换两个缓冲区的指针即可,如下图所示:
这种方法又被称为双缓冲,而还有一种方法被叫做三缓冲,是为了解决gpu渲染速度与显示器的刷新率之间的矛盾:
在三重缓冲中,有一个正在显示的缓冲区,一个等待显示的缓冲区,和一个正在由 GPU 渲染的缓冲区。当 GPU 完成渲染时,它会将渲染好的帧移到等待显示的缓冲区。当显示器准备好刷新时,它会显示等待中的帧,并将之前显示的帧移动到渲染队列。这样,GPU 可以继续渲染下一帧,而不必等待显示器的刷新。
对于纹理而言,其中存储的数据格式并不是固定的,而是受到一定的限制,常用的设置数据格式有:
描述符
描述符是d3d中的又一重要概念,在发出绘制命令之前,我们需要将本次draw call的相应资源绑定到渲染流水线上,但是这一过程并不是直接将资源绑定,而是通过描述符来完成。
通过中间层描述符,有几大好处:
- 不同的描述符可以指定不同的资源
- 通过描述符可以为GPU解释资源 将资源使用到渲染流水线的不同阶段 告知资源如何使用(我们可以为一个资源创建两个不同的描述符)
- 可以使用描述符来绑定资源的局部数据
- 资源在创建时采用了无类型格式,描述符可以为其指明具体的类型
常用的描述符可分为以下几类:
描述符堆中存有一系列描述符,本质上是存放某种特定类型描述符的一块内存:
//描述符堆的描述符的定义
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mRtvHeap;
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mDsvHeap;
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
rtvHeapDesc.NumDescriptors = SwapChainBufferCount;
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
rtvHeapDesc.NodeMask = 0;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
&rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf())));
//描述符的定义
Microsoft::WRL::ComPtr<ID3D12Resource> mDepthStencilBuffer;
D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc;
dsvDesc.Flags = D3D12_DSV_FLAG_NONE;
dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
dsvDesc.Format = mDepthStencilFormat;
dsvDesc.Texture2D.MipSlice = 0;
md3dDevice->CreateDepthStencilView(mDepthStencilBuffer.Get(), &dsvDesc, DepthStencilView());
CPU与GPU的交互
命令列表,命令队列与命令分配器
每个gpu都会至少维护一个命令队列(本质上是一个ring buffer,环形缓冲区),cpu可利用命令列表将其中的命令提交给gpu执行,同时命令列表里面的命令存储于命令分配器上,命令队列是从命令分配器中来提取命令。
总结一下:
在头文件中加入相应com接口的定义
//命令队列
Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
//命令分配器
Microsoft::WRL::ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
//命令列表
Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> mCommandList;
之后进行初始化
void D3DApp::CreateCommandObjects()
{
//填写命令队列结构体
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
//创建命令队列
//IID_PPV_ARGS 将COM ID类型转换为void**类型 作为函数参数
ThrowIfFailed(md3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue)));
//创建命令分配器
ThrowIfFailed(md3dDevice->CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(mDirectCmdListAlloc.GetAddressOf())));
//创建命令列表
ThrowIfFailed(md3dDevice->CreateCommandList(
0,
D3D12_COMMAND_LIST_TYPE_DIRECT,
mDirectCmdListAlloc.Get(), // 关联命令分配器
nullptr,
IID_PPV_ARGS(mCommandList.GetAddressOf())));
//起始时让命令列表处于关闭状态 因为我们在使用命令列表前需要对其进行reset操作(安全地复用旧列表所占用的底层内存)而在reset之前需要关闭命令列表
mCommandList->Close();
}
向命令列表中加入命令
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->RSSetScissorRects(1, &mScissorRect);
将命令列表提交给命令队列 然后执行
ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
CPU与GPU之间的同步
为了实现cpu与gpu之间的同步,我们需要强制cpu等待 直到gpu完成所有命令的处理,d3d通过围栏实现这一点:
//定义围栏com接口 以及相应的围栏点
Microsoft::WRL::ComPtr<ID3D12Fence> mFence;
UINT64 mCurrentFence = 0;
//创建围栏对象
ThrowIfFailed(md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(&mFence)));
//使cpu与gpu同步
void D3DApp::FlushCommandQueue()
{
//增加围栏点的值
mCurrentFence++;
//设置新的围栏点
ThrowIfFailed(mCommandQueue->Signal(mFence.Get(), mCurrentFence));
// 直到gpu处理完围栏点之前的命令 围栏点的值才会增加 循环才会结束
if(mFence->GetCompletedValue() < mCurrentFence)
{
HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);
ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));
WaitForSingleObject(eventHandle, INFINITE);
CloseHandle(eventHandle);
}
}
资源转换
有时候我们可能需要对一个资源先进行写操作,然后再进行读操作进行显示,为了防止在进行写操作的时候读,d3d设置了一组资源状态属性,防止类似上述这种资源冒险的情况发生,例如:
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
上述代码将资源从渲染目标状态转换为呈现状态
D3D的初始化
接下来的部分只是大致介绍一下流程 以及重要函数
至于每个函数 每个描述子结构体的参数的详细介绍 可自行查阅
创建设备
进行d3d初始化首先要创建d3d设备
启动调试层
#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, // default adapter
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&md3dDevice));
// 创建失败回退到warp设备
if(FAILED(hardwareResult))
{
ComPtr<IDXGIAdapter> pWarpAdapter;
ThrowIfFailed(mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));
ThrowIfFailed(D3D12CreateDevice(
pWarpAdapter.Get(),
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&md3dDevice)));
}
创建围栏
这一点前面已经说明
检测对4x msaa的支持
首先填写质量等级的结构体 设置为4x 然后使用checkfeaturesupport来检测硬件是否支持
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.");
创建命令队列与列表
描述并创建交换链
void D3DApp::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 = m4xMsaaState ? 4 : 1;
sd.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 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()));
}
创建描述符堆
我们需要创建描述符堆来存储相应的描述符
一个堆用于存储rtv 即交换链的两个缓冲区
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
rtvHeapDesc.NumDescriptors = SwapChainBufferCount;
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
rtvHeapDesc.NodeMask = 0;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
&rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf())));
一个腿用于存储dsv 即深度缓冲区
D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;
dsvHeapDesc.NumDescriptors = 1;
dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
dsvHeapDesc.NodeMask = 0;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
&dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));
创建渲染目标视图 rtv
在之前创建了交换链的缓冲区之后 我们还需要创建相应的描述子/视图 才能将其绑定到渲染流水线进行输出
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(mRtvHeap->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < SwapChainBufferCount; i++)
{
ThrowIfFailed(mSwapChain->GetBuffer(i, IID_PPV_ARGS(&mSwapChainBuffer[i])));
md3dDevice->CreateRenderTargetView(mSwapChainBuffer[i].Get(), nullptr, rtvHeapHandle);
rtvHeapHandle.Offset(1, mRtvDescriptorSize);
}
创建深度缓冲区及其视图 dsv
填写深度缓冲区描述符然后使用CreateCommittedResource创建
D3D12_RESOURCE_DESC depthStencilDesc;
depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
depthStencilDesc.Alignment = 0;
depthStencilDesc.Width = mClientWidth;
depthStencilDesc.Height = mClientHeight;
depthStencilDesc.DepthOrArraySize = 1;
depthStencilDesc.MipLevels = 1;
depthStencilDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
depthStencilDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
D3D12_CLEAR_VALUE optClear;
optClear.Format = mDepthStencilFormat;
optClear.DepthStencil.Depth = 1.0f;
optClear.DepthStencil.Stencil = 0;
ThrowIfFailed(md3dDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&depthStencilDesc,
D3D12_RESOURCE_STATE_COMMON,
&optClear,
IID_PPV_ARGS(mDepthStencilBuffer.GetAddressOf())));
//创建深度视图 用于绑定资源
D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc;
dsvDesc.Flags = D3D12_DSV_FLAG_NONE;
dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
dsvDesc.Format = mDepthStencilFormat;
dsvDesc.Texture2D.MipSlice = 0;
md3dDevice->CreateDepthStencilView(mDepthStencilBuffer.Get(), &dsvDesc, DepthStencilView());
// 将深度缓冲区资源设置为depth buffer 涉及到之前提到的资源的转换
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(mDepthStencilBuffer.Get(),
D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_DEPTH_WRITE));
设置视口与裁剪矩形
可以先设置视口与裁剪矩形的范围:
mScreenViewport.TopLeftX = 0;
mScreenViewport.TopLeftY = 0;
mScreenViewport.Width = static_cast<float>(mClientWidth);
mScreenViewport.Height = static_cast<float>(mClientHeight);
mScreenViewport.MinDepth = 0.0f;
mScreenViewport.MaxDepth = 1.0f;
mScissorRect = { 0, 0, mClientWidth, mClientHeight };
之后我们可以在实际渲染过程中设置视口与裁剪矩形:
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->RSSetScissorRects(1, &mScissorRect);
本结示例代码
void InitDirect3DApp::Draw(const GameTimer& gt)
{
//reset命令分配器 注意这里要保证里面的命令已经全部被gpu执行完毕
ThrowIfFailed(mDirectCmdListAlloc->Reset());
//reset命令列表
ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(), nullptr));
//将资源从呈现状态转换到渲染目标状态 读到写
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));
//reset视口与裁剪矩形 每次reset命令列表都要reset
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->RSSetScissorRects(1, &mScissorRect);
//清空后台缓冲区与深度缓冲区
mCommandList->ClearRenderTargetView(CurrentBackBufferView(), Colors::LightSteelBlue, 0, nullptr);
mCommandList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
// 指明我们要写入的缓冲区
mCommandList->OMSetRenderTargets(1, &CurrentBackBufferView(), true, &DepthStencilView());
// 将后台缓冲区从渲染目标状态转换到呈现状态
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
// 关闭命令列表
ThrowIfFailed(mCommandList->Close());
// 执行命令
ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
// 交换前后缓冲区
ThrowIfFailed(mSwapChain->Present(0, 0));
mCurrBackBuffer = (mCurrBackBuffer + 1) % SwapChainBufferCount;
// 等待gpu执行完所有命令 保证同步
FlushCommandQueue();
}
标签:ThrowIfFailed,mCommandList,WRL,ComPtr,龙书,----,D3D12,缓冲区,d3d12
From: https://www.cnblogs.com/dyccyber/p/18069119