DirectX11--深入理解与使用缓冲区资源

阅读目录 前言 顶点缓冲区(Vertex Buffer) CreateVertexBuffer函数--创建顶点缓冲区 索引缓冲区(Index Buffer) CreateIndexBuffer函数--创建索引缓冲区 常量缓冲区(Constant Buffer) CreateConstantBuffer函数--创建常量缓冲区 有类型的缓冲区(Typed Buffer) CreateTypedBuffer函数--创建有类型的缓冲区 将缓冲区保存的结果拷贝到内存 结构化缓冲区(Structured Buffer) CreateStructuredBuffer函数--创建结构化缓冲区 追加/消耗缓冲区(Append/Consume Buffer) 追加/消耗缓冲区的创建 回到顶部 前言 在Direct3D 11中,缓冲区属于其中一种资源类型,它在内存上的布局是一维线性的。根据HLSL支持的类型以及C++的使用情况,缓冲区可以分为下面这些类型: 顶点缓冲区(Vertex Buffer) 索引缓冲区(Index Buffer) 常量缓冲区(Constant Buffer) 有类型的缓冲区(Typed Buffer) 结构化缓冲区(Structured Buffer) 追加/消耗缓冲区(Append/Consume Buffer) 字节地址缓冲区(Byte Address Buffer)(未完工) 间接参数缓冲区(Indirect Argument Buffer)(未完工) 因此这一章主要讲述上面这些资源的创建和使用方法 DirectX11 With Windows SDK完整目录 Github项目源码 欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。 回到顶部 顶点缓冲区(Vertex Buffer) 顾名思义,顶点缓冲区存放的是一连串的顶点数据,尽管缓冲区的数据实际上还是一堆二进制流,但在传递给输入装配阶段的时候,就会根据顶点输入布局将其装配成HLSL的顶点结构体数据。顶点缓冲区的数据可以用自定义的顶点结构体数组来初始化。顶点可以包含的成员有:顶点坐标(必须有),顶点颜色,顶点法向量,纹理坐标,顶点切线向量等等。每个顶点的成员必须匹配合适的DXGI数据格式。 当然,纯粹的顶点数组只是针对单个物体而言的。如果需要绘制大量相同的物体,需要同时用到多个顶点缓冲区。这允许你将顶点数据分开成多个顶点缓冲区来存放。 这里还提供了顶点缓冲区的另一种形式:实例缓冲区。我们可以提供一到多个的顶点缓冲区,然后再提供一个实例缓冲区。其中实例缓冲区存放的可以是物体的世界矩阵、世界矩阵的逆转置、材质等。这样做可以减少大量重复数据的产生,以及减少大量的CPU绘制调用。 CreateVertexBuffer函数--创建顶点缓冲区 顶点缓冲区的创建需要区分下面两种情况: 顶点数据是否需要动态更新 是否需要绑定到流输出 如果顶点缓冲区在创建的时候提供了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); } 回到顶部 索引缓冲区(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内部可以像结构体那样包含各种类型的参数,而且还需要注意它的打包规则。 关于常量缓冲区,有太多值得需要注意的细节了: 每个着色器阶段最多允许15个常量缓冲区,并且每个缓冲区最多可以容纳4096个标量。HLSL的cbuffer需要指定register(b#), #的范围为0到14 在C++创建常量缓冲区时大小必须为16字节的倍数,因为HLSL的常量缓冲区本身以及对它的读写操作需要严格按16字节对齐 对常量缓冲区的成员使用packoffset修饰符可以指定起始向量和分量位置 在更新常量缓冲区时由于数据是提交完整的字节流数据到GPU,会导致HLSL中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) */; } CreateConstantBuffer函数--创建常量缓冲区 常量缓冲区的创建需要区分下面两种情况: 是否需要CPU经常更新 是否需要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 gBuffer : register(t0); 需要留意的是,当前缓冲区和纹理需要共用纹理寄存器,即t#,因此要注意和纹理避开使用同一个寄存器槽。 如果是可读写的缓冲区类型,则声明方式如下: RWBuffer 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。 CreateTypedBuffer函数--创建有类型的缓冲区 有类型的缓冲区通常需要绑定到着色器上作为资源使用,因此需要将bindFlags设为D3D11_BIND_SHADER_RESOURCE。 此外,有类型的缓冲区的创建需要区分下面两种情况: 是否允许CPU写入/读取 是否允许GPU写入 如果缓冲区在创建的时候提供了D3D11_SUBRESOURCE_DATA来完成初始化,并且之后都不需要更新,则可以使用D3D11_USAGE_IMMUTABLE。 如果缓冲区需要频繁更新,则可以使用D3D11_USAGE_DYNAMIC,并允许CPU写入(D3D11_CPU_ACCESS_WRITE)。 如果缓冲区需要允许GPU写入,说明后面可能需要创建UAV绑定到RWBuffer,为此还需要给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使用,则需要创建着色器资源视图: 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(md3dDevice->CreateShaderResourceView(mBuffer.Get(), &srvDesc, mBufferSRV.GetAddressOf())); 而如果我们希望它作为RWBuffer使用,则需要创建无序访问视图: 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; // 元素数目 md3dDevice->CreateUnorderedAccessView(mBuffer.Get(), &uavDesc, mBufferUAV.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 gStructuredBuffer : register(t0); 如果是可读写的结构化缓冲区类型,则声明方式如下: RWStructuredBuffer gRWStructuredBuffer : register(u0); 结构化缓冲区也具有下面的方法: 方法 作用 void GetDimensions(out uint) 获取资源各个维度下的大小 T Load(in int) 按一维索引读取结构化缓冲区数据 T Operator StructuredBuffer仅允许读取,RWStructuredBuffer允许读写 CreateStructuredBuffer函数--创建结构化缓冲区 结构化缓冲区的创建和有类型的缓冲区创建比较相似,区别在于: 需要在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_U
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信