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();
}