天天看點

cpu緩沖區大小怎麼設定_DX12繪制篇:多常量緩沖區

龍書第7章開篇,即是介紹如何通過幀資源和渲染項來優化程式,但是這幾部分因為涉及多物體及多幀繪制,開始就封裝那麼多東西,會不太好了解。是以我嘗試先将這兩塊封裝放一邊,而剝離并實作另一塊核心代碼“設定多緩沖區”,并且還是單物體繪制。搞明白這些内容,再在這基礎上去加上幀資源和渲染項的代碼,并在最後封裝。我覺得,這樣才是學習了解的最佳途徑。

之前的案例中,我們隻使用了一個常量緩沖區,是以CBV數量也是一個。但是在實際繪制中,我們會基于資源的更新頻率對常量資料進行分組。比如說一個靜态物體,它的世界矩陣隻需設定一次即可,但是觀察矩陣和投影矩陣會在改變錄影機或者改變視窗時發生變化,是以需要多次更新,這樣我們就可以将世界矩陣和觀察投影矩陣分開存儲在兩個常量緩沖區中,來進行優化。是以我們這次案例效果還是之前的一個立方體,但是會将常量緩沖區設定成2個。

1.拆分常量資料結構體

首先将原來的worldViewProj矩陣拆開,分别置入兩個常量資料結構體中。

struct ObjectConstants
{
	XMFLOAT4X4 world = MathHelper::Identity4x4();	
};
struct PassConstants
{
	XMFLOAT4X4 viewProj = MathHelper::Identity4x4();
};
           

相應的也要聲明兩個常量資源上傳堆。

std::unique_ptr<UploadBufferResource<ObjectConstants>> objCB = nullptr;
std::unique_ptr<UploadBufferResource<PassConstants>> passCB = nullptr;
           

2.建立CBV

雖然我們還是繪制一個幾何體,但是因為現在有2個常量資料結構體,是以我們就要設定2個常量緩沖區,同時也要建立2個CBV。當然了,建立CBV前還是要先建立CBV堆。注意:我們要将堆中的描述符數量設定成2。

//建立CBV堆
D3D12_DESCRIPTOR_HEAP_DESC cbHeapDesc;
cbHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
cbHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
cbHeapDesc.NumDescriptors = 2;	//此處一個堆中包含2個CBV
cbHeapDesc.NodeMask = 0;
ThrowIfFailed(d3dDevice->CreateDescriptorHeap(&cbHeapDesc, IID_PPV_ARGS(&cbvHeap)));
           

接下來建立第一個CBV,也就是ObjectConstants的CBV。這裡的位址計算貌似很複雜,其實是分了兩部分。第一部分是計算了“子物體在常量緩沖區中的位址”,即objCB_Address,它是通過子物體的數量和常量資料大小來做位址偏移計算的,目前我們沒有子物體,是以這裡的位址就是GetGPUVirtualAddress()函數傳回的首位址。第二部分是計算了“CBV元素在CBV堆中的位址”,即handle,句柄我們可以暫時了解成指針,它是通過元素索引和cbvSrvUav類型資料大小來計算得到位址偏移的(句柄的位址偏移是通過Offset函數來實作的)。通過上面兩個位址,我們就建立出了第一個CBV。從代碼中可以很清晰的看到,CBV和常量資料是一一對應的。

objCB = std::make_unique<UploadBufferResource<ObjectConstants>>(d3dDevice.Get(), 1, true);
//獲得常量緩沖區首位址
D3D12_GPU_VIRTUAL_ADDRESS objCB_Address;
objCB_Address = objCB->Resource()->GetGPUVirtualAddress();
int objCbElementIndex = 0;	//常量緩沖區子物體個數(子緩沖區個數)下标
objCB_Address += objCbElementIndex * objConstSize;//子物體在常量緩沖區中的位址
int heapIndex = 0;	//CBV堆中的CBV元素索引
auto handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(cbvHeap->GetCPUDescriptorHandleForHeapStart());//獲得CBV堆首位址
handle.Offset(heapIndex, cbv_srv_uavDescriptorSize);	//CBV句柄(CBV堆中的CBV元素位址)
//建立CBV描述符
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc0;
cbvDesc0.BufferLocation = objCB_Address;
cbvDesc0.SizeInBytes = objConstSize;
d3dDevice->CreateConstantBufferView(&cbvDesc0, handle);
           

同理,我們建立第二個CBV,也就是PassConstants的CBV。可以看到我們的heapIndex此時是1,因為是第二個CBV了。

passCB = std::make_unique<UploadBufferResource<PassConstants>>(d3dDevice.Get(), 1, true);
//獲得常量緩沖區首位址
D3D12_GPU_VIRTUAL_ADDRESS passCB_Address;
passCB_Address = passCB->Resource()->GetGPUVirtualAddress();
int passCbElementIndex = 0;
passCB_Address += passCbElementIndex * passConstSize;
heapIndex = 1;
handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(cbvHeap->GetCPUDescriptorHandleForHeapStart());
handle.Offset(heapIndex, cbv_srv_uavDescriptorSize);
//建立CBV描述符
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc1;
cbvDesc1.BufferLocation = passCB_Address;
cbvDesc1.SizeInBytes = passConstSize;
d3dDevice->CreateConstantBufferView(&cbvDesc1, handle);
           

3.複制常量資料至GPU

接下來我們要用CopyData函數将常量緩沖區中的資料傳至GPU,因為現在有2個常量緩沖區,是以要傳2次。而在建構世界矩陣的時候,我們乘了一個在世界坐标X方向平移了2個機關的矩陣,即w *= XMMatrixTranslation(2.0f, 0.0f, 0.0f),然後将它傳入objConstants緩沖區,觀察投影矩陣則傳入passConstants緩沖區。

void D3D12InitApp::Update(GameTime& gt)
{
	ObjectConstants objConstants;
	PassConstants passConstants;
	//建構觀察矩陣
	float x = radius * sinf(phi) * cosf(theta);
	float y = radius * cosf(phi);
	float z = radius * sinf(phi) * sinf(theta);
	/*float y = 0;
	float x = 0;
	float z = 10;*/
	XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
	XMVECTOR target = XMVectorZero();
	XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
	XMMATRIX v = XMMatrixLookAtLH(pos, target, up);

	//建構世界矩陣
	XMMATRIX w = XMLoadFloat4x4(&world);
	w *= XMMatrixTranslation(2.0f, 0.0f, 0.0f);
	//拿到投影矩陣
	XMMATRIX p = XMLoadFloat4x4(&proj);
	//矩陣計算
	XMMATRIX VP_Matrix = v * p;
	XMStoreFloat4x4(&passConstants.viewProj, XMMatrixTranspose(VP_Matrix));
	//passConstants.totalTime = gt.TotalTime();
	//passConstants.pulseColor = XMFLOAT4(Colors::Gold);
	passCB->CopyData(0, passConstants);

	//XMMATRIX指派給XMFLOAT4X4
	XMStoreFloat4x4(&objConstants.world, XMMatrixTranspose(w));
	//将資料拷貝至GPU緩存
	objCB->CopyData(0, objConstants);
}
           

4.建構根簽名

根簽名作用是将常量資料綁定至寄存器槽,供着色器程式通路。因為現在我們有2個常量資料結構體,是以要建立2個元素的根參數,即2個CBV表,并綁定2個寄存器槽。

void D3D12InitApp::BuildRootSignature()
{
	//根參數可以是描述符表、根描述符、根常量
	CD3DX12_ROOT_PARAMETER slotRootParameter[2];
	//建立由單個CBV所組成的描述符表
	CD3DX12_DESCRIPTOR_RANGE cbvTable0;
	cbvTable0.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, //描述符類型
		1, //描述符數量
		0);//描述符所綁定的寄存器槽号
	CD3DX12_DESCRIPTOR_RANGE cbvTable1;
	cbvTable1.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, //描述符類型
		1, //描述符數量
		1);//描述符所綁定的寄存器槽号
	slotRootParameter[0].InitAsDescriptorTable(1, &cbvTable0);
	slotRootParameter[1].InitAsDescriptorTable(1, &cbvTable1);

	//根簽名由一組根參數構成
	CD3DX12_ROOT_SIGNATURE_DESC rootSig(2, //根參數的數量
		slotRootParameter, //根參數指針
		0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
	//用單個寄存器槽來建立一個根簽名,該槽位指向一個僅含有單個常量緩沖區的描述符區域
	ComPtr<ID3DBlob> serializedRootSig = nullptr;
	ComPtr<ID3DBlob> errorBlob = nullptr;
	HRESULT hr = D3D12SerializeRootSignature(&rootSig, D3D_ROOT_SIGNATURE_VERSION_1, &serializedRootSig, &errorBlob);

	if (errorBlob != nullptr)
	{
		OutputDebugStringA((char*)errorBlob->GetBufferPointer());
	}
	ThrowIfFailed(hr);

	ThrowIfFailed(d3dDevice->CreateRootSignature(0,
		serializedRootSig->GetBufferPointer(),
		serializedRootSig->GetBufferSize(),
		IID_PPV_ARGS(&rootSignature)));
}
           

5.設定根描述符表

在Draw函數中,我們會使用cmdList->SetGraphicsRootDescriptorTable()函數綁定指令至流水線,而現在我們因為有2個描述符表,是以要綁定兩次。注意:根參數的起始索引要和根描述符表的位址(即CBV堆中的元素位址)對應。

//設定根描述符表
int objCbvIndex = 0;
auto handle = CD3DX12_GPU_DESCRIPTOR_HANDLE(cbvHeap->GetGPUDescriptorHandleForHeapStart());
handle.Offset(objCbvIndex, cbv_srv_uavDescriptorSize);
cmdList->SetGraphicsRootDescriptorTable(0, //根參數的起始索引
	handle);

int passCbvIndex = 1;
handle = CD3DX12_GPU_DESCRIPTOR_HANDLE(cbvHeap->GetGPUDescriptorHandleForHeapStart());
handle.Offset(passCbvIndex, cbv_srv_uavDescriptorSize);
cmdList->SetGraphicsRootDescriptorTable(1, //根參數的起始索引
	handle);
           

6.着色器程式

在shader程式中,我們會傳入兩個常量結構體,分别來自寄存器b0和b1,然後通過兩次矩陣計算得到裁剪空間坐标(因為在cpu階段我們将矩陣拆開了,是以在着色器中每個頂點要多計算一次矩陣乘法,但是對于GPU的強大并行能力來說,這并不算什麼)。

cbuffer cbPerObject : register(b0)
{
	float4x4 gWorld; 
};

cbuffer cbPass : register(b1)
{
        float4x4 gViewProj;
};

struct VertexIn
{
	float3 PosL  : POSITION;
        float4 Color : COLOR;
};

struct VertexOut
{
	float4 PosH  : SV_POSITION;
	float4 Color : COLOR;
};

VertexOut VS(VertexIn vin)
{
	VertexOut vout;
        float3 PosW = mul(float4(vin.PosL, 1.0f), gWorld).xyz;
        vout.PosH = mul(float4(PosW, 1.0f), gViewProj);
	vout.Color = vin.Color;
    
        return vout;
}

float4 PS(VertexOut pin) : SV_Target
{
    return pin.Color;
}
           

編譯運作,顯示正常,并且立方體在世界坐标的X方向移動了2個機關。說明兩個常量緩沖區的值都起作用了。

cpu緩沖區大小怎麼設定_DX12繪制篇:多常量緩沖區

轉動一下錄影機,因為錄影機的目标點是(0,0),是以可知立方體的世界坐标确實改變了。

cpu緩沖區大小怎麼設定_DX12繪制篇:多常量緩沖區

有了這篇的鋪墊,我們接下來将會繪制多個幾何體(沒有執行個體化),并學習渲染項的代碼管理,以及幀資源的代碼優化,最終繪制出多個幾何體執行個體。