DirectX11--深入理解與使用緩沖區資源


前言

在Direct3D 11中,緩沖區屬於其中一種資源類型,它在內存上的布局是一維線性的。根據HLSL支持的類型以及C++的使用情況,緩沖區可以分為下面這些類型:

  1. 頂點緩沖區(Vertex Buffer)
  2. 索引緩沖區(Index Buffer)
  3. 常量緩沖區(Constant Buffer)
  4. 有類型的緩沖區(Typed Buffer)
  5. 結構化緩沖區(Structured Buffer)
  6. 追加/消耗緩沖區(Append/Consume Buffer)
  7. 字節地址緩沖區(Byte Address Buffer)
  8. 間接參數緩沖區(Indirect Argument Buffer)(可能不施工)

因此這一章主要講述上面這些資源的創建和使用方法

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。

頂點緩沖區(Vertex Buffer)

顧名思義,頂點緩沖區存放的是一連串的頂點數據,盡管緩沖區的數據實際上還是一堆二進制流,但在傳遞給輸入裝配階段的時候,就會根據頂點輸入布局將其裝配成HLSL的頂點結構體數據。頂點緩沖區的數據可以用自定義的頂點結構體數組來初始化。頂點可以包含的成員有:頂點坐標(必須有),頂點顏色,頂點法向量,紋理坐標,頂點切線向量等等。每個頂點的成員必須匹配合適的DXGI數據格式。

當然,純粹的頂點數組只是針對單個物體而言的。如果需要繪制大量相同的物體,需要同時用到多個頂點緩沖區。這允許你將頂點數據分開成多個頂點緩沖區來存放。

這里還提供了頂點緩沖區的另一種形式:實例緩沖區。我們可以提供一到多個的頂點緩沖區,然后再提供一個實例緩沖區。其中實例緩沖區存放的可以是物體的世界矩陣、世界矩陣的逆轉置、材質等。這樣做可以減少大量重復數據的產生,以及減少大量的CPU繪制調用。

頂點輸入布局

由於內容重復,可以點此跳轉進行回顧

CreateVertexBuffer函數--創建頂點緩沖區

頂點緩沖區的創建需要區分下面兩種情況:

  1. 頂點數據是否需要動態更新
  2. 是否需要綁定到流輸出

如果頂點緩沖區在創建的時候提供了D3D11_SUBRESOURCE_DATA來完成初始化,並且之后都不需要更新,則可以使用D3D11_USAGE_IMMUTABLE

如果頂點緩沖區需要頻繁更新,則可以使用D3D11_USAGE_DYNAMIC,並允許CPU寫入(D3D11_CPU_ACCESS_WRITE)。

如果頂點緩沖區需要綁定到流輸出,則說明頂點緩沖區需要允許GPU寫入,可以使用D3D11_USAGE_DEFAULT,並且需要提供綁定標簽D3D11_BIND_STREAM_OUTPUT

下圖說明了頂點緩沖區可以綁定的位置:

頂點緩沖區不需要創建資源視圖,它可以直接綁定到輸入裝配階段或流輸出階段。

創建頂點緩沖區和一般的創建緩沖區函數如下:

// ------------------------------
// CreateBuffer函數
// ------------------------------
// 創建緩沖區
// [In]d3dDevice			D3D設備
// [In]data					初始化結構化數據
// [In]byteWidth			緩沖區字節數
// [Out]structuredBuffer	輸出的結構化緩沖區
// [In]usage				資源用途
// [In]bindFlags			資源綁定標簽
// [In]cpuAccessFlags		資源CPU訪問權限標簽
// [In]structuredByteStride 每個結構體的字節數
// [In]miscFlags			資源雜項標簽
HRESULT CreateBuffer(
	ID3D11Device * d3dDevice,
	void * data,
	UINT byteWidth,
	ID3D11Buffer ** buffer,
	D3D11_USAGE usage,
	UINT bindFlags,
	UINT cpuAccessFlags,
	UINT structureByteStride,
	UINT miscFlags)
{
	D3D11_BUFFER_DESC bufferDesc;
	bufferDesc.Usage = usage;
	bufferDesc.ByteWidth = byteWidth;
	bufferDesc.BindFlags = bindFlags;
	bufferDesc.CPUAccessFlags = cpuAccessFlags;
	bufferDesc.StructureByteStride = structureByteStride;
	bufferDesc.MiscFlags = miscFlags;

	D3D11_SUBRESOURCE_DATA initData;
	ZeroMemory(&initData, sizeof(initData));
	initData.pSysMem = data;

	return d3dDevice->CreateBuffer(&bufferDesc, &initData, buffer);
}


// ------------------------------
// CreateVertexBuffer函數
// ------------------------------
// [In]d3dDevice			D3D設備
// [In]data					初始化數據
// [In]byteWidth			緩沖區字節數
// [Out]vertexBuffer		輸出的頂點緩沖區
// [InOpt]dynamic			是否需要CPU經常更新
// [InOpt]streamOutput		是否還用於流輸出階段(不能與dynamic同時設為true)
HRESULT CreateVertexBuffer(
	ID3D11Device * d3dDevice,
	void * data,
	UINT byteWidth,
	ID3D11Buffer ** vertexBuffer,
	bool dynamic,
	bool streamOutput)
{
	UINT bindFlags = D3D11_BIND_VERTEX_BUFFER;
	D3D11_USAGE usage;
	UINT cpuAccessFlags = 0;
	if (dynamic && streamOutput)
	{
		return E_INVALIDARG;
	}
	else if (!dynamic && !streamOutput)
	{
		usage = D3D11_USAGE_IMMUTABLE;
	}
	else if (dynamic)
	{
		usage = D3D11_USAGE_DYNAMIC;
		cpuAccessFlags |= D3D11_CPU_ACCESS_WRITE;
	}
	else
	{
		bindFlags |= D3D11_BIND_STREAM_OUTPUT;
		usage = D3D11_USAGE_DEFAULT;
	}

	return CreateBuffer(d3dDevice, data, byteWidth, vertexBuffer,
		usage, bindFlags, cpuAccessFlags, 0, 0);
}

實例緩沖區(Instanced Buffer)

由於涉及到硬件實例化,推薦直接跳到硬件實例化一章閱讀。

索引緩沖區(Index Buffer)

索引緩沖區通常需要與頂點緩沖區結合使用,它的作用就是以索引的形式來引用頂點緩沖區中的某一頂點,並按索引緩沖區的順序和圖元類型來組裝圖元。它可以有效地減少頂點緩沖區中重復的頂點數據,從而減小網格模型占用的數據大小。使用相同的索引值就可以多次引用同一個頂點。

索引緩沖區的使用不需要創建資源視圖,它僅用於輸入裝配階段,並且在裝配的時候你需要指定每個索引所占的字節數:

DXGI_FORMAT 字節數 索引范圍
DXGI_FORMAT_R8_UINT 1 0-255
DXGI_FORMAT_R16_UINT 2 0-65535
DXGI_FORMAT_R32_UINT 4 0-2147483647

將索引緩沖區綁定到輸入裝配階段后,你就可以用帶Indexed的Draw方法,指定起始索引偏移值和索引數目來進行繪制。

CreateIndexBuffer函數--創建索引緩沖區

索引緩沖區的創建只考慮數據是否需要動態更新。

如果索引緩沖區在創建的時候提供了D3D11_SUBRESOURCE_DATA來完成初始化,並且之后都不需要更新,則可以使用D3D11_USAGE_IMMUTABLE

如果索引緩沖區需要頻繁更新,則可以使用D3D11_USAGE_DYNAMIC,並允許CPU寫入(D3D11_CPU_ACCESS_WRITE)。

// ------------------------------
// CreateIndexBuffer函數
// ------------------------------
// [In]d3dDevice			D3D設備
// [In]data					初始化數據
// [In]byteWidth			緩沖區字節數
// [Out]indexBuffer			輸出的索引緩沖區
// [InOpt]dynamic			是否需要CPU經常更新
HRESULT CreateIndexBuffer(
	ID3D11Device * d3dDevice,
	void * data,
	UINT byteWidth,
	ID3D11Buffer ** indexBuffer,
	bool dynamic)
{
	D3D11_USAGE usage;
	UINT cpuAccessFlags = 0;
	if (dynamic)
	{
		usage = D3D11_USAGE_DYNAMIC;
		cpuAccessFlags |= D3D11_CPU_ACCESS_WRITE;
	}
	else
	{
		usage = D3D11_USAGE_IMMUTABLE;
	}

	return CreateBuffer(d3dDevice, data, byteWidth, indexBuffer,
		usage, D3D11_BIND_INDEX_BUFFER, cpuAccessFlags, 0, 0);
}

常量緩沖區(Constant Buffer)

常量緩沖區是我們接觸到的第一個可以給所有可編程着色器程序使用的緩沖區。由於着色器函數的形參沒法從C++端傳入,我們只能通過類似全局變量的方式來讓着色器函數訪問,這些參數被打包在一個常量緩沖區中。而C++可以通過創建對應的常量緩沖區來綁定到HLSL對應的cbuffer,以實現從C++到HLSL的數據的傳遞。C++的常量緩沖區是以字節流來對待;而HLSL的cbuffer內部可以像結構體那樣包含各種類型的參數,而且還需要注意它的打包規則。

關於常量緩沖區,有太多值得需要注意的細節了:

  1. 每個着色器階段最多允許15個常量緩沖區,並且每個緩沖區最多可以容納4096個標量。HLSL的cbuffer需要指定register(b#), #的范圍為0到14
  2. 在C++創建常量緩沖區時大小必須為16字節的倍數,因為HLSL的常量緩沖區本身以及對它的讀寫操作需要嚴格按16字節對齊
  3. 對常量緩沖區的成員使用packoffset修飾符可以指定起始向量和分量位置
  4. 在更新常量緩沖區時由於數據是提交完整的字節流數據到GPU,會導致HLSL中cbuffer的所有成員都被更新。為了減少不必要的更新,可以根據這些參數的更新頻率划分出多個常量緩沖區以節省帶寬資源
  5. 一個着色器在使用了多個常量緩沖區的情況下,這些常量緩沖區相互間都不能出現同名成員
  6. 單個常量緩沖區可以同時綁定到不同的可編程着色器階段,因為這些緩沖區都是只讀的,不會導致內存訪問沖突。一個包含常量緩沖區的*.hlsli文件同時被多個着色器文件引用,只是說明這些着色器使用相同的常量緩沖區布局,如果該緩沖區需要在多個着色器階段使用,你還需要在C++同時將相同的常量緩沖區綁定到各個着色器階段上

下面是一個HLSL常量緩沖區的例子(注釋部分可省略,效果等價):

cbuffer CBChangesRarely : register(b2)
{
    matrix gView /* : packoffset(c0) */;
    float3 gSphereCenter /* : packoffset(c4.x) */;
    float gSphereRadius /* : packoffset(c4.w) */;
    float3 gEyePosW /* : packoffset(c5.x) */;
    float gPad /* : packoffset(c5.w) */;
}

CreateConstantBuffer函數--創建常量緩沖區

常量緩沖區的創建需要區分下面兩種情況:

  1. 是否需要CPU經常更新
  2. 是否需要GPU更新

如果常量緩沖區在創建的時候提供了D3D11_SUBRESOURCE_DATA來完成初始化,並且之后都不需要更新,則可以使用D3D11_USAGE_IMMUTABLE

如果常量緩沖區需要頻繁更新,則可以使用D3D11_USAGE_DYNAMIC,並允許CPU寫入(D3D11_CPU_ACCESS_WRITE)。

如果常量緩沖區在較長的一段時間才需要更新一次,則可以考慮使用D3D11_USAGE_DEFAULT

下圖說明了常量緩沖區可以綁定的位置:

常量緩沖區的使用同樣不需要創建資源視圖。

// ------------------------------
// CreateConstantBuffer函數
// ------------------------------
// [In]d3dDevice			D3D設備
// [In]data					初始化數據
// [In]byteWidth			緩沖區字節數,必須是16的倍數
// [Out]indexBuffer			輸出的索引緩沖區
// [InOpt]cpuUpdates		是否允許CPU更新
// [InOpt]gpuUpdates		是否允許GPU更新
HRESULT CreateConstantBuffer(
	ID3D11Device * d3dDevice,
	void * data,
	UINT byteWidth,
	ID3D11Buffer ** constantBuffer,
	bool cpuUpdates,
	bool gpuUpdates)
{
	D3D11_USAGE usage;
	UINT cpuAccessFlags = 0;
	if (cpuUpdates && gpuUpdates)
	{
		return E_INVALIDARG;
	}
	else if (!cpuUpdates && !gpuUpdates)
	{
		usage = D3D11_USAGE_IMMUTABLE;
	}
	else if (cpuUpdates)
	{
		usage = D3D11_USAGE_DYNAMIC;
		cpuAccessFlags |= D3D11_CPU_ACCESS_WRITE;
	}
	else
	{
		usage = D3D11_USAGE_DEFAULT;
	}

	return CreateBuffer(d3dDevice, data, byteWidth, constantBuffer,
		usage, D3D11_BIND_CONSTANT_BUFFER, cpuAccessFlags, 0, 0);
}

有類型的緩沖區(Typed Buffer)

這是一種創建和使用起來最簡單的緩沖區,但實際使用頻率遠不如上面所講的三種緩沖區。它的數據可以在HLSL被解釋成基本HLSL類型的數組形式。

在HLSL中,如果是只讀的緩沖區類型,則聲明方式如下:

Buffer<float4> g_Buffer : register(t0);

需要留意的是,當前緩沖區和紋理需要共用紋理寄存器,即t#,因此要注意和紋理避開使用同一個寄存器槽。

如果是可讀寫的緩沖區類型,則聲明方式如下:

RWBuffer<float4> g_RWBuffer : register(u0);

有類型的緩沖區具有下面的方法:

方法 作用
void GetDimensions(out uint) 獲取資源各個維度下的大小
T Load(in int) 按一維索引讀取緩沖區數據
T Operator[](in uint) Buffer僅允許讀取,RWBuffer允許讀寫

有類型的緩沖區需要創建着色器資源視圖以綁定到對應的着色器階段。由於HLSL的語法知識定義了有限的類型和元素數目,但在DXGI_FORMAT中,有許多種成員都能夠用於匹配一種HLSL類型。比如,HLSL的float4你可以使用DXGI_FORMAT_R32G32B32A32_FLOAT, DXGI_FORMAT_R16G16B16A16_FLOATDXGI_FORMAT_R8G8B8A8_UNORM。而HLSL的int2你可以使用DXGI_FORMAT_R32G32_SINTDXGI_FORMAT_R16G16_SINTDXGI_FORMAT_R8G8_SINT

CreateTypedBuffer函數--創建有類型的緩沖區

有類型的緩沖區通常需要綁定到着色器上作為資源使用,因此需要將bindFlags設為D3D11_BIND_SHADER_RESOURCE

此外,有類型的緩沖區的創建需要區分下面兩種情況:

  1. 是否允許CPU寫入/讀取
  2. 是否允許GPU寫入

如果緩沖區在創建的時候提供了D3D11_SUBRESOURCE_DATA來完成初始化,並且之后都不需要更新,則可以使用D3D11_USAGE_IMMUTABLE

如果緩沖區需要頻繁更新,則可以使用D3D11_USAGE_DYNAMIC,並允許CPU寫入(D3D11_CPU_ACCESS_WRITE)。

如果緩沖區需要允許GPU寫入,說明后面可能需要創建UAV綁定到RWBuffer<T>,為此還需要給bindFlags添加D3D11_BIND_UNORDERED_ACCESS

如果緩沖區的數據需要讀出到內存,則可以使用D3D11_USAGE_STAGING,並允許CPU讀取(D3D11_CPU_ACCESS_READ)。

下圖說明了有類型的(與結構化)緩沖區可以綁定的位置:

// ------------------------------
// CreateTypedBuffer函數
// ------------------------------
// [In]d3dDevice			D3D設備
// [In]data					初始化數據
// [In]byteWidth			緩沖區字節數
// [Out]typedBuffer			輸出的有類型的緩沖區
// [InOpt]cpuUpdates		是否允許CPU更新
// [InOpt]gpuUpdates		是否允許使用RWBuffer
HRESULT CreateTypedBuffer(
	ID3D11Device * d3dDevice,
	void * data,
	UINT byteWidth,
	ID3D11Buffer ** typedBuffer,
	bool cpuUpdates,
	bool gpuUpdates)
{
	UINT bindFlags = D3D11_BIND_SHADER_RESOURCE;
	D3D11_USAGE usage;
	UINT cpuAccessFlags = 0;
	if (cpuUpdates && gpuUpdates)
	{
		bindFlags = 0;
		usage = D3D11_USAGE_STAGING;
		cpuAccessFlags |= D3D11_CPU_ACCESS_READ;
	}
	else if (!cpuUpdates && !gpuUpdates)
	{
		usage = D3D11_USAGE_IMMUTABLE;
	}
	else if (cpuUpdates)
	{
		usage = D3D11_USAGE_DYNAMIC;
		cpuAccessFlags |= D3D11_CPU_ACCESS_WRITE;
	}
	else
	{
		usage = D3D11_USAGE_DEFAULT;
		bindFlags |= D3D11_BIND_UNORDERED_ACCESS;
	}

	return CreateBuffer(d3dDevice, data, byteWidth, typedBuffer,
		usage, bindFlags, cpuAccessFlags, 0, 0);
}

關於追加/消耗緩沖區,我們后面再討論。

如果我們希望它作為Buffer<float4>使用,則需要創建着色器資源視圖:

D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
srvDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFER;
srvDesc.Buffer.FirstElement = 0;			// 起始元素的索引
srvDesc.Buffer.NumElements = numElements;	// 元素數目

HR(m_pd3dDevice->CreateShaderResourceView(m_pBuffer.Get(), &srvDesc, m_pBufferSRV.GetAddressOf()));

而如果我們希望它作為RWBuffer<float4>使用,則需要創建無序訪問視圖:

D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
uavDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
uavDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
uavDesc.Buffer.FirstElement = 0;			// 起始元素的索引
uavDesc.Buffer.Flags = 0;
uavDesc.Buffer.NumElements = numElements;	// 元素數目

m_pd3dDevice->CreateUnorderedAccessView(m_pBuffer.Get(), &uavDesc, m_pBufferUAV.GetAddressOf());

將緩沖區保存的結果拷貝到內存

由於這些緩沖區僅支持GPU讀取,我們需要另外新建一個緩沖區以允許它CPU讀取和GPU寫入(STAGING),然后將保存結果的緩沖區拷貝到該緩沖區,再映射出內存即可:

HR(CreateTypedBuffer(md3dDevice.Get(), nullptr, sizeof data,
	mBufferOutputCopy.GetAddressOf(), true, true));

md3dImmediateContext->CopyResource(mVertexOutputCopy.Get(), mVertexOutput.Get());
D3D11_MAPPED_SUBRESOURCE mappedData;
HR(md3dImmediateContext->Map(mVertexOutputCopy.Get(), 0, D3D11_MAP_READ, 0, &mappedData));
memcpy_s(data, sizeof data, mappedData.pData, sizeof data);
md3dImmediateContext->Unmap(mVertexOutputCopy.Get(), 0);

結構化緩沖區(Structured Buffer)

結構化緩沖區可以說是緩沖區的復合形式,它允許模板類型T是用戶自定義的類型,即緩沖區存放的內容可以被解釋為結構體數組。

現在HLSL有如下結構體:

struct Data
{
	float3 v1;
	float2 v2;
};

如果是只讀的結構化緩沖區,則聲明方式如下:

StructuredBuffer<Data> g_StructuredBuffer : register(t0);

支持的方法如下:

方法 描述
GetDimensions 獲取資源各個維度下的大小
Load 讀取緩沖區數據
Operator[] 返回只讀資源變量

如果是可讀寫的結構化緩沖區類型,則聲明方式如下:

RWStructuredBuffer<Data> g_RWStructuredBuffer : register(u0);

每一個RWStructuredBuffer都內置了一個計數器,我們在C++端可以設置它的初始值:

方法 描述
DecrementCounter 遞減對象的隱藏計數器
GetDimensions 獲取資源各個維度下的大小
IncrementCounter 遞增對象的隱藏計數器
Load 讀取緩沖區數據
Operator[] 返回資源變量

CreateStructuredBuffer函數--創建結構化緩沖區

結構化緩沖區的創建和有類型的緩沖區創建比較相似,區別在於:

  1. 需要在MiscFlags指定D3D11_RESOURCE_MISC_BUFFER_STRUCTURED
  2. 需要額外提供structureByteStride說明結構體的大小
// ------------------------------
// CreateStructuredBuffer函數
// ------------------------------
// 如果需要創建Append/Consume Buffer,需指定cpuUpdates為false, gpuUpdates為true
// [In]d3dDevice			D3D設備
// [In]data					初始化數據
// [In]byteWidth			緩沖區字節數
// [In]structuredByteStride 每個結構體的字節數
// [Out]structuredBuffer	輸出的結構化緩沖區
// [InOpt]cpuUpdates		是否允許CPU更新
// [InOpt]gpuUpdates		是否允許使用RWStructuredBuffer
HRESULT CreateStructuredBuffer(
	ID3D11Device * d3dDevice,
	void * data,
	UINT byteWidth,
	UINT structuredByteStride,
	ID3D11Buffer ** structuredBuffer,
	bool cpuUpdates,
	bool gpuUpdates)
{
	UINT bindFlags = D3D11_BIND_SHADER_RESOURCE;
	D3D11_USAGE usage;
	UINT cpuAccessFlags = 0;
	if (cpuUpdates && gpuUpdates)
	{
		bindFlags = 0;
		usage = D3D11_USAGE_STAGING;
		cpuAccessFlags |= D3D11_CPU_ACCESS_READ;
	}
	else if (!cpuUpdates && !gpuUpdates)
	{
		usage = D3D11_USAGE_IMMUTABLE;
	}
	else if (cpuUpdates)
	{
		usage = D3D11_USAGE_DYNAMIC;
		cpuAccessFlags |= D3D11_CPU_ACCESS_WRITE;
	}
	else
	{
		usage = D3D11_USAGE_DEFAULT;
		bindFlags |= D3D11_BIND_UNORDERED_ACCESS;
	}

	return CreateBuffer(d3dDevice, data, byteWidth, structuredBuffer,
		usage, bindFlags, cpuAccessFlags, structuredByteStride, 
		D3D11_RESOURCE_MISC_BUFFER_STRUCTURED);
}

無論是SRV還是UAV,在指定Format時只能指定DXGI_FORMAT_UNKNOWN

如果我們希望它作為StructuredBuffer<Data>使用,則需要創建着色器資源視圖:

D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
srvDesc.Format = DXGI_FORMAT_UNKNOWN;
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFER;
srvDesc.Buffer.FirstElement = 0;			// 起始元素的索引
srvDesc.Buffer.NumElements = numElements;	// 元素數目

HR(m_pd3dDevice->CreateShaderResourceView(m_pBuffer.Get(), &srvDesc, m_pBufferSRV.GetAddressOf()));

而如果我們希望它作為RWStructuredBuffer<float4>使用,則需要創建無序訪問視圖:

D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
uavDesc.Format = DXGI_FORMAT_UNKNOWN;
uavDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
uavDesc.Buffer.FirstElement = 0;			// 起始元素的索引
uavDesc.Buffer.Flags = 0;
uavDesc.Buffer.NumElements = numElements;	// 元素數目

m_pd3dDevice->CreateUnorderedAccessView(m_pBuffer.Get(), &uavDesc, m_pBufferUAV.GetAddressOf());

注意:如果想要開啟結構化緩沖區的隱藏計數器,還需要指定FlagsD3D11_BUFFER_UAV_FLAG_COUNTER

追加/消耗緩沖區(Append/Consume Buffer)

追加緩沖區和消耗緩沖區類型實際上是結構化緩沖區的特殊變體資源。因為涉及到修改操作,它們都只能以無序訪問視圖的方式來使用。如果你只是希望這些結構體數據經過着色器變換並且不需要考慮最終的輸出順序要一致,那么使用這兩個緩沖區是一種不錯的選擇。

ConsumeStructuredBuffer<float3> g_VertexIn : register(u0);
AppendStructuredBuffer<float3> g_VertexOut : register(u1);

在HLSL中,AppendStructuredBuffer僅提供了Append方法用於尾端追加成員;而ConsumeStructuredBuffer則僅提供了Consume方法用於消耗尾端成員。這兩種操作實際上可以看做是對棧的操作。此外,你也可以使用GetDimensions方法來獲取當前緩沖區還剩下多少元素。

一旦某個線程消耗了一個數據元素,就不能再被另一個線程給消耗掉,並且一個線程將只消耗一個數據。需要注意的是,因為線程之間的執行順序是不確定的,因此無法根據線程ID來確定當前消耗的是哪個索引的資源。

此外,追加/消耗緩沖區實際上並不能動態增長,你必須在創建緩沖區的時候就要分配好足夠大的空間。

追加/消耗緩沖區的創建

追加/消耗緩沖區可以經由CreateStructuredBuffer函數來創建,需要指定cpuUpdatesfalse, gpuUpdatestrue.

比較關鍵的是UAV的創建,需要像結構化緩沖區一樣指定FormatDXGI_FORMAT_UNKNOWN。並且無論是追加緩沖區,還是消耗緩沖區,都需要在Buffer.Flags中指定D3D11_BUFFER_UAV_FLAG_APPEND

D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
uavDesc.Format = DXGI_FORMAT_UNKNOWN;
uavDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
uavDesc.Buffer.FirstElement = 0;			// 起始元素的索引
uavDesc.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_APPEND;
uavDesc.Buffer.NumElements = numElements;	// 元素數目
HR(m_pd3dDevice->CreateUnorderedAccessView(m_pVertexInput.Get(), &uavDesc, m_pVertexInputUAV.GetAddressOf()));

然后在將UAV綁定到着色器時,如果是追加緩沖區,通常需要指定初始元素數目為0,然后提供給ID3D11DeviceContext::*SSetUnorderedAccessViews方法的最后一個參數:

UINT initCounts[1] = { 0 };
m_pd3dImmediateContext->CSSetUnorderedAccessViews(0, 1, m_pVertexInputUAV.GetAddressOf(), initCounts);

而如果是消耗緩沖區,則需要指定初始元素數目:

UINT initCounts[1] = { numElements };
m_pd3dImmediateContext->CSSetUnorderedAccessViews(1, 1, m_pVertexInputUAV.GetAddressOf(), initCounts);

字節地址緩沖區(Byte Address Buffer)

字節地址緩沖區為HLSL程序提供了一種更為原始的內存塊。它不使用固定的結構大小來確定在資源中的索引位置,而只是從資源的開頭獲取一個字節偏移量,並將從該偏移量開始的四個字節作為32位無符號整數返回。由於返回的數據總數以4字節增量進行檢索,因此請求的偏移地址也必須是4的倍數。但是,除了這個最小大小要求外,HLSL程序還可以根據需要操作資源內存。返回的無符號整數值也可以被一些類型轉換內置函數重新解釋為其他數據類型。

這種類型的緩沖區用途可能不是很明顯。因為HLSL程序可以按需解釋和操作內存內容,因此不要求每個數據記錄都具有相同的長度,正如我們在結構化緩沖區中看到的那樣。使用可變的記錄長度,HLSL程序可以實現幾乎任何符合資源邊界的數據結果,創建一個可變元素大小的鏈表和創建一個基於數組的二叉樹也同樣變得簡單。只要程序實現了數據結果的訪問予以,就可以完全自由地使用所需的內存。這的確是一個非常強大的特性,它給許多算法類別打開了新世界的大門,這些算法要么以前不可能在GPU上實現,要么難以實現。

因此,字節地址緩沖區旨在允許開發人員在緩沖區資源中實現自定義數據結構,然后,根據定義,內存塊的使用可以由它與之結合使用的算法來進行解釋。例如,如果一個鏈表的數據結構將會用於存儲32位顏色值,每個鏈接節點將包含一個顏色值,后跟指向下一個元素的鏈接,這個鏈接可以是在字節地址緩沖區的起始偏移量,然后用-1或0xFFFFFFFF表示到達鏈表尾。由此構建的鏈表即為靜態鏈表。

在HLSL中,如果是只讀的字節地址緩沖區,則聲明方式如下:

ByteAddressBuffer g_ByteAddressBuffer : register(t0);

支持的方法如下:

方法 描述
GetDimensions 獲取資源各個維度下的大小
Load 讀取一個uint
Load2 讀取兩個uint
Load3 讀取三個uint
Load4 讀取四個uint

如果是可讀寫的結構化緩沖區類型,則聲明方式如下:

RWByteAddressBuffer g_RWByteAddressBuffer : register(u0);

它不僅支持寫入,還支持原子操作:

方法 描述
GetDimensions 獲取資源各個維度下的大小
InterlockedAdd 原子操作的加法
InterlockedAnd 原子操作的按位與
InterlockedCompareExchange 原子操作的值比較和交換
InterlockedCompareStore 原子操作的值比較和存儲
InterlockedExchange 原子操作的值交換
InterlockedMax 原子操作的找最大值
InterlockedMin 原子操作的找最小值
InterlockedOr 原子操作的按位或
InterlockedXor 原子操作的按位異或
Load 讀取一個uint
Load2 讀取兩個uint
Load3 讀取三個uint
Load4 讀取四個uint
Store 寫入一個uint
Store2 寫入兩個uint
Store3 寫入三個uint
Store4 寫入四個uint

字節地址緩沖區的創建

字節地址緩沖區可以經由CreateRawBuffer函數來創建,區別僅在於需要將miscFlags指定為D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS

HRESULT CreateRawBuffer(
	ID3D11Device * d3dDevice,
	void * data,
	UINT byteWidth,
	ID3D11Buffer ** rawBuffer,
	bool cpuUpdates,
	bool gpuUpdates)
{
	UINT bindFlags = D3D11_BIND_SHADER_RESOURCE;
	D3D11_USAGE usage;
	UINT cpuAccessFlags = 0;
	if (cpuUpdates && gpuUpdates)
	{
		bindFlags = 0;
		usage = D3D11_USAGE_STAGING;
		cpuAccessFlags |= D3D11_CPU_ACCESS_READ;
	}
	else if (!cpuUpdates && !gpuUpdates)
	{
		usage = D3D11_USAGE_IMMUTABLE;
	}
	else if (cpuUpdates)
	{
		usage = D3D11_USAGE_DYNAMIC;
		cpuAccessFlags = D3D11_CPU_ACCESS_WRITE;
	}
	else
	{
		usage = D3D11_USAGE_DEFAULT;
		bindFlags |= D3D11_BIND_UNORDERED_ACCESS;
	}

	return CreateBuffer(d3dDevice, data, byteWidth, rawBuffer,
		usage, bindFlags, cpuAccessFlags, 0,
		D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS);
}

要注意的是,字節地址緩沖區作為着色器資源視圖提供時,可以用於所有可編程着色器階段。無序訪問視圖則僅在計算着色階段和像素着色階段。在這兩種情況下,資源視圖的格式都必須是DXGI_FORMAT_R32_TYPELESS

創建着色器資源視圖時,需要將ViewDimension指定為D3D11_SRV_DIMENSION_BUFFEREX,然后再提供標簽D3D11_BUFFEREX_SRV_FLAG_RAW

D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
srvDesc.Format = DXGI_FORMAT_R32_TYPELESS;
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFEREX;
srvDesc.BufferEx.FirstElement = 0;
srvDesc.BufferEx.NumElements = width * height;
srvDesc.BufferEx.Flags = D3D11_BUFFEREX_SRV_FLAG_RAW;

而創建無序訪問視圖時,需要提供標簽D3D11_BUFFER_UAV_FLAG_RAW

D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
uavDesc.Format = DXGI_FORMAT_R32_TYPELESS;
uavDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
uavDesc.Buffer.NumElements = width * height;
uavDesc.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_RAW;

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM