d3d12龍書閱讀----Direct3D的初始化

dyccyber發表於2024-03-12

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),可以儲存貼圖資訊與緩衝區資訊等等。

緩衝區包括前臺緩衝區,後臺緩衝區,深度緩衝區,模版緩衝區等等。
其中前臺與後臺緩衝區,前臺緩衝區儲存的是當前顯示在螢幕上的影像資料,而下一幀的影像資料繪製在後臺緩衝區中,當後臺緩衝區繪製完成之後,兩種緩衝區的角色互換,只需交換兩個緩衝區的指標即可,如下圖所示:
img

這種方法又被稱為雙緩衝,而還有一種方法被叫做三緩衝,是為了解決gpu渲染速度與顯示器的重新整理率之間的矛盾:
在三重緩衝中,有一個正在顯示的緩衝區,一個等待顯示的緩衝區,和一個正在由 GPU 渲染的緩衝區。當 GPU 完成渲染時,它會將渲染好的幀移到等待顯示的緩衝區。當顯示器準備好重新整理時,它會顯示等待中的幀,並將之前顯示的幀移動到渲染佇列。這樣,GPU 可以繼續渲染下一幀,而不必等待顯示器的重新整理。

對於紋理而言,其中儲存的資料格式並不是固定的,而是受到一定的限制,常用的設定資料格式有:
img

描述符

描述符是d3d中的又一重要概念,在發出繪製命令之前,我們需要將本次draw call的相應資源繫結到渲染流水線上,但是這一過程並不是直接將資源繫結,而是透過描述符來完成。
透過中間層描述符,有幾大好處:

  1. 不同的描述符可以指定不同的資源
  2. 透過描述符可以為GPU解釋資源 將資源使用到渲染流水線的不同階段 告知資源如何使用(我們可以為一個資源建立兩個不同的描述符)
  3. 可以使用描述符來繫結資源的區域性資料
  4. 資源在建立時採用了無型別格式,描述符可以為其指明具體的型別
    常用的描述符可分為以下幾類:
    img
    描述符堆中存有一系列描述符,本質上是存放某種特定型別描述符的一塊記憶體:
//描述符堆的描述符的定義
 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();
}

相關文章