在Direct3D 11中,缓冲区属于其中一种资源类型,它在内存上的布局是一维线性的。根据HLSL支持的类型以及C++的使用状况,缓冲区能够分为下面这些类型:html
所以这一章主要讲述上面这些资源的建立和使用方法git
DirectX11 With Windows SDK完整目录github
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。数组
顾名思义,顶点缓冲区存放的是一连串的顶点数据,尽管缓冲区的数据实际上仍是一堆二进制流,但在传递给输入装配阶段的时候,就会根据顶点输入布局将其装配成HLSL的顶点结构体数据。顶点缓冲区的数据能够用自定义的顶点结构体数组来初始化。顶点能够包含的成员有:顶点坐标(必须有),顶点颜色,顶点法向量,纹理坐标,顶点切线向量等等。每一个顶点的成员必须匹配合适的DXGI数据格式。app
固然,纯粹的顶点数组只是针对单个物体而言的。若是须要绘制大量相同的物体,须要同时用到多个顶点缓冲区。这容许你将顶点数据分开成多个顶点缓冲区来存放。ide
这里还提供了顶点缓冲区的另外一种形式:实例缓冲区。咱们能够提供一到多个的顶点缓冲区,而后再提供一个实例缓冲区。其中实例缓冲区存放的能够是物体的世界矩阵、世界矩阵的逆转置、材质等。这样作能够减小大量重复数据的产生,以及减小大量的CPU绘制调用。函数
因为内容重复,能够点此跳转进行回顾布局
顶点缓冲区的建立须要区分下面两种状况:ui
若是顶点缓冲区在建立的时候提供了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); }
因为涉及到硬件实例化,推荐直接跳到硬件实例化一章阅读。
索引缓冲区一般须要与顶点缓冲区结合使用,它的做用就是以索引的形式来引用顶点缓冲区中的某一顶点,并按索引缓冲区的顺序和图元类型来组装图元。它能够有效地减小顶点缓冲区中重复的顶点数据,从而减少网格模型占用的数据大小。使用相同的索引值就能够屡次引用同一个顶点。
索引缓冲区的使用不须要建立资源视图,它仅用于输入装配阶段,而且在装配的时候你须要指定每一个索引所占的字节数:
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方法,指定起始索引偏移值和索引数目来进行绘制。
索引缓冲区的建立只考虑数据是否须要动态更新。
若是索引缓冲区在建立的时候提供了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); }
常量缓冲区是咱们接触到的第一个能够给全部可编程着色器程序使用的缓冲区。因为着色器函数的形参无法从C++端传入,咱们只能经过相似全局变量的方式来让着色器函数访问,这些参数被打包在一个常量缓冲区中。而C++能够经过建立对应的常量缓冲区来绑定到HLSL对应的cbuffer
,以实现从C++到HLSL的数据的传递。C++的常量缓冲区是以字节流来对待;而HLSL的cbuffer
内部能够像结构体那样包含各类类型的参数,并且还须要注意它的打包规则。
关于常量缓冲区,有太多值得须要注意的细节了:
cbuffer
须要指定register(b#)
, #
的范围为0到14packoffset
修饰符能够指定起始向量和份量位置cbuffer
的全部成员都被更新。为了减小没必要要的更新,能够根据这些参数的更新频率划分出多个常量缓冲区以节省带宽资源*.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) */; }
常量缓冲区的建立须要区分下面两种状况:
若是常量缓冲区在建立的时候提供了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); }
这是一种建立和使用起来最简单的缓冲区,但实际使用频率远不如上面所讲的三种缓冲区。它的数据能够在HLSL被解释成基本HLSL类型的数组形式。
在HLSL中,若是是只读的缓冲区类型,则声明方式以下:
Buffer<float4> gBuffer : register(t0);
须要留意的是,当前缓冲区和纹理须要共用纹理寄存器,即t#,所以要注意和纹理避开使用同一个寄存器槽。
若是是可读写的缓冲区类型,则声明方式以下:
RWBuffer<float4> gRWBuffer : register(u0);
有类型的缓冲区具备下面的方法:
方法 | 做用 |
---|---|
void GetDimensions(out uint) | 获取资源各个维度下的大小 |
T Load(in int) | 按一维索引读取缓冲区数据 |
T Operator | Buffer 仅容许读取,RWBuffer 容许读写 |
有类型的缓冲区须要建立着色器资源视图以绑定到对应的着色器阶段。因为HLSL的语法知识定义了有限的类型和元素数目,但在DXGI_FORMAT
中,有许多种成员都可以用于匹配一种HLSL类型。好比,HLSL的float4
你可使用DXGI_FORMAT_R32G32B32A32_FLOAT
, DXGI_FORMAT_R16G16B16A16_FLOAT
或DXGI_FORMAT_R8G8B8A8_UNORM
。而HLSL的int2
你可使用DXGI_FORMAT_R32G32_SINT
,DXGI_FORMAT_R16G16_SINT
或DXGI_FORMAT_R8G8_SINT
。
有类型的缓冲区一般须要绑定到着色器上做为资源使用,所以须要将bindFlags
设为D3D11_BIND_SHADER_RESOURCE
。
此外,有类型的缓冲区的建立须要区分下面两种状况:
若是缓冲区在建立的时候提供了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);
结构化缓冲区能够说是缓冲区的复合形式,它容许模板类型T
是用户自定义的类型,即缓冲区存放的内容能够被解释为结构体数组。
如今HLSL有以下结构体:
struct Data { float3 v1; float2 v2; };
若是是只读的结构化缓冲区,则声明方式以下:
StructuredBuffer<Data> gStructuredBuffer : register(t0);
若是是可读写的结构化缓冲区类型,则声明方式以下:
RWStructuredBuffer<Data> gRWStructuredBuffer : register(u0);
结构化缓冲区也具备下面的方法:
方法 | 做用 |
---|---|
void GetDimensions(out uint) | 获取资源各个维度下的大小 |
T Load(in int) | 按一维索引读取结构化缓冲区数据 |
T Operator | StructuredBuffer 仅容许读取,RWStructuredBuffer 容许读写 |
结构化缓冲区的建立和有类型的缓冲区建立比较类似,区别在于:
MiscFlags
指定D3D11_RESOURCE_MISC_BUFFER_STRUCTURED
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());
追加缓冲区和消耗缓冲区类型其实是结构化缓冲区的特殊变体资源。由于涉及到修改操做,它们都只能以无序访问视图的方式来使用。若是你只是但愿这些结构体数据通过着色器变换而且不须要考虑最终的输出顺序要一致,那么使用这两个缓冲区是一种不错的选择。
ConsumeStructuredBuffer<float3> gVertexIn : register(u0); AppendStructuredBuffer<float3> gVertexOut : register(u1);
在HLSL中,AppendStructuredBuffer
仅提供了Append
方法用于尾端追加成员;而ConsumeStructuredBuffer
则仅提供了Consume
方法用于消耗尾端成员。这两种操做实际上能够看作是对栈的操做。此外,你也可使用GetDimensions
方法来获取当前缓冲区还剩下多少元素。
一旦某个线程消耗了一个数据元素,就不能再被另外一个线程给消耗掉,而且一个线程将只消耗一个数据。须要注意的是,由于线程之间的执行顺序是不肯定的,所以没法根据线程ID来肯定当前消耗的是哪一个索引的资源。
此外,追加/消耗缓冲区实际上并不能动态增加,你必须在建立缓冲区的时候就要分配好足够大的空间。
追加/消耗缓冲区能够经由CreateStructuredBuffer
函数来建立,须要指定cpuUpdates
为false
, gpuUpdates
为true
.
比较关键的是UAV的建立,须要像结构化缓冲区同样指定Format
为DXGI_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);
(未完待续)
DirectX11 With Windows SDK完整目录
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。