2.2.1缓冲区资源-缓冲区/结构化缓冲区资源
这是我们遇到的第一种缓冲区类型,可从可编程着色器阶段的HLSL代码中直接访问。在HLSL中,常量缓冲区是用cbuffer关键字声明的资源对象。清单2.9中提供了一个示例声明。
cbuffer Transforms
{
matrix WorldMatrixj
matrix ViewProjMatrix;
matrix SkinMatrices[26];
} ;
cbuffer LightParameters
{
float3 LightPositionWS;
float4 LightColor;
} ;
cbuffer ParticlelnsertParameters
{
float4 EmitterLocation;
float4 RandomVector;
};
在这个cbuffer结构中声明的每个变量都可以直接在HLSL着色器程序中使用,就好像它是在全局范围内声明的一样。主机应用程序使用上面列表中指定的常量缓冲区的名称来按名称识别缓冲区,并将适当的内容加载到缓冲区中。但是,缓冲区名称不在HLSL中使用。与常量缓冲区名称一样,常量缓冲区的各个元素的名称和类型也可以通过着色器反射API进行访问。这些方法可用于确定每个子参数的名称和类别,使应用程序能够随时知道要插入缓冲区的信息。
缓冲区/结构化缓冲区资源
我们的下一种类型的缓冲区由两个不同的名称引用,这取决于它所包含的数据类型。标准缓冲区资源是指其元素是内置数据类型之一的缓冲区。通过这种方式,标准缓冲区资源类似于值的数组。每个值都存储在一个唯一的数组位置,并且可以由其位置的索引引用。这允许GPU上的许多不同实例同时轻松访问资源。由于每个元素都是唯一标识的,开发人员可以很容易地构建程序以避免任何内存冲突。图2.14用图形表示缓冲资源。
与缓冲区资源非常相似的是结构化缓冲区资源。两者之间唯一的区别是,结构化缓冲区允许用户将结构定义为基本元素,而不是内置的数据类型之一。这使得将特定处理问题的数据映射到资源相对简单。如果开发人员可以在C++中定义一个合适的结构,那么可以在HLSL中定义相应的结构,并且缓冲区将包含这些结构的数组,可编程阴影可以将其用作资源对象。
结构化缓冲区旨在提供灵活的内存资源,以简化自定义算法的开发。由于结构中的数据格式可以使用HLSL中的任何可用类型,因此可以根据特定情况进行自定义。可用结构的形式与常量缓冲区的形式非常相似,只是结构化缓冲区提供了所需结构的数组,而不是像常量缓冲区那样的单个实例。这一概念如图2.15所示。在该图中,显示了一个假设结构,它可以表示粒子系统中粒子的数据。
在这里,我们看到粒子结构使用多个变量,并且完整的可分离系统包含在结构化缓冲区中。需要注意的一个非常有趣的点是,这些缓冲区类型是我们讨论的第一批资源,可用于可编程流水线阶段的写入和读取。我们将在本节的剩余部分中探讨如何使用此功能的细节。
使用缓冲区/结构化缓冲区
由于这种提供大量结构化数据的能力,缓冲区和结构化缓冲区是可编程着色器阶段访问的较大数据结构的绝佳选择。这些缓冲区是第一个附加到具有资源视图的管道而不是直接绑定的资源。这些着色器可用于对所有可编程管道阶段进行读取访问,在像素和计算着色器阶段也可以进行读取/写入访问,具体取决于使用的资源视图。这意味着这些资源可以潜在地提供不同管道阶段之间以及同一管道阶段内的各个处理元素之间的通信手段。
管道阶段的资源访问能力由用于将资源绑定到管道的资源视图的类型决定。与常量缓冲区不同,缓冲区/结构化缓冲区必须通过资源视图绑定到管道,这意味着它必须使用适当的绑定标志创建,以便与shadersource视图、无序访问视图或两者绑定。着色器资源视图允许绑定到所有可编程管道阶段,而无序访问视图仅允许绑定到像素和计算着色器阶段。由于对资源执行写访问需要无序的访问视图,因此无人机的可用绑定位置有效地确定了可以在哪里执行这些写访问。着色器资源视图和无序访问视图的可用连接点如图2.16所示。与常量缓冲区一样,当用于只读用途时,缓冲区/结构化缓冲区可以绑定到多个位置。这是通过将它绑定到具有一个更大资源视图的管道来完成的。但是,当使用无序访问视图时,它可能只绑定到一个位置。这是由运行时强制执行的,如果资源绑定到多个位置进行写入,运行时将打印错误消息。无序访问视图的使用也排除了着色器资源视图的同时使用,因为它可能会引入写后读取问题。
创建缓冲区/结构化缓冲区
正如我们在其他缓冲区资源创建讨论中所看到的,必须确定缓冲区/结构化缓冲区将经历的使用场景。这些资源通常有三种不同类型的使用模式。第一种情况是缓冲区中的数据在应用程序的整个生命周期内都是静态的。这方面的一个例子是使用缓冲器/结构化缓冲器源来保存预先计算的数据,例如预先计算的辐射传输数据或某种其他形式的查找表。第二种情况是CPU需要定期将数据加载到缓冲区时。在许多通用计算系统中,这是一种可能的情况,其中GPU被用作CPU的协处理器,并不断向GPU提供新的数据以进行处理。在这两种情况下,GPU只能读取缓冲区数据,而不能写入资源。
最后一种情况出现在GPU自身以某种方式更新缓冲区内容时。这方面的一个常见例子可以在GPU执行某种类型的物理模拟的情况下找到,该物理模拟稍后用于呈现模拟结果的表示。在这种情况下,GPU将需要对缓冲器进行读写访问,以便读取内容,执行一些更新,然后将结果写回缓冲器。这三种情况中的每一种都需要对创建缓冲区时使用的用法、CPUAccess和绑定标志进行不同的设置。
除了这些标志之外,在使用结构化缓冲区时,我们还必须提供一些额外的大小信息。所有类型的缓冲区都需要指定ByteWidth参数,该参数以字节为单位指示所需的缓冲区大小。结构化缓冲区还要求我们指定所使用的结构元素的大小。当资源被可编程流水线级之一访问时,该信息被用作步长。使用结构化缓冲区的另一个要求是指示缓冲区资源确实将用作结构化缓冲区。这是通过设置3DS11_RESOURCE_MISC_BUFFER_STRUCTURED杂项标志来完成的。一个典型的示例创建如清单2.10所示。
ID3DllBuffer* CreateStructuredBuffer( UINT count,
UINT structsize,
bool CPUWritable,
bool GPUWritable,
D3D11_SUBRES0URCE_DATA* pData )
{
D3D11_BUFFER_DESC desc;
desc.ByteWidth = count * structsize;
desc.MiscFlagS = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED;
desc.StructureByteStride = structsize;
// Select the appropriate usage and CPU access flags based on the passed in flags
if ( !CPUWritable && IGPUWritable )
{
desc.BindFlagS = D3D11_BIND_SHADER_RESOURCE;
desc.Usage = D3D11_USAGE_IMMUTABLE;
desc.CPUAccessFlags = 0;
}
else if ( CPUWritable && IGPUWritable )
{
desc.BindFlagS = D3D11_BIND_SHADER_RESOURCE;
desc.Usage = D3D11_USAGE_DYNAMIC;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
}
else if ( ICPUWritable && GPUWritable )
{
desc.BindFlagS = D3D11_BIND_SHADER_RESOURCE |D3D11_BIND_UNORDERED_ACCESS;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.CPUAccessFlags = 0;
}
else if ( CPUWritable && GPUWritable )
{
// Handle the error here...
// Resources can't be writable by both CPU and GPU simultaneously!
}
// Create the buffer with the specified configuration
ID3DllBuffer* pBuffer = 0;
HRESULT hr = g_pDevice->CreateBuffer( &desc, pData, &pBuffer );
if ( FAILED( hr ) )
{
// Handle the error here...
return( 0 );
}
return( pBuffer );
}
我们将大小信息划分为要包括在缓冲区中的结构数量的计数,以及指示每个结构的大小(以字节为单位)的structsize参数。然后,这些值用于计算缓冲区的总字节宽度。此外,上面讨论的三种使用场景中的每一种都在代码列表中表示;它们通过CPU访问、使用和绑定标志来区分自己。
资源视图要求
们已经看到,唯一可以与缓冲区或结构化缓冲区一起使用的资源视图是着色器资源视图和无序处理视图。事实上,这些是将这些缓冲区绑定到管道的唯一方法。对于标准缓冲区类型,必须在SRV描述结构中直接提供格式。对于结构化缓冲区资源,格式应设置为DXGI_F0RMAT_UNKNOWN,因为不会有DXGI格式可用于匹配所有可能的结构类型。相反,格式是从着色器程序中的HLSL结构声明派生的,并且元素的大小由创建缓冲区资源时的应用程序提供。
缓冲区资源的着色器资源视图描述结构需要指定开始元素以及要包含在视图中的元素数量。这可以选择缓冲区中的整个元素范围,也可以用于选择总资源的子集。当从HLSL观看时,这有效地将所选数据映射到[0,width-1]范围,所有其他访问都被视为越界。这些值在D3D11_BUFFER_SRV结构中指定,如清单2.11所示
ID3DllShaderResourceView* CreateBufferSRV( ID3DllResource* pResource )
{
D3D11_SHADER_RES0URCE_VIEW_DESC desc;
// For structured buffers, DXGI_FORMAT_UNKNOWN must be used!
// For standard buffers, utilize the appropriate format,
desc.Format = DXGI_FORMAT_R32G32B32_FLOAT;
desc.ViewDimension = D3D11_SRV_DIMENSI0N_BUFFER;
desc.Buffer.ElementOffset = 0;
desc.Buffer.ElementWidth = 100;
ID3DllShaderResourceView* pView = 0;
HRESULT hr = g_pDevice->CreateShaderResourceView( pResource, &desc,&pView );
return( pView );
}
通过调整ElementOffset和ElementWidth参数,可以使用单个大型缓冲区源来包含一组较小的数据集,每个数据集都有自己的SRV,以提供对它们的访问。通过使用着色器资源视图来限制对资源的特定子集的访问,并创建适当数量的着色器资源视图,应用程序可以通过使用特定着色器资源视图有效地控制哪些数据对每次调用着色器程序可见。也可以使用多个视图同时选择缓冲区的不同范围。
无序访问视图使用相同的元素范围选择和格式规范,还提供了一组可以为特殊使用场景指定的附加标志。这些标志使无序访问视图能够用于附加/消费缓冲区和字节地址缓冲区,这两种缓冲区都是通过无序访问视图使用缓冲区资源的专用方式。以下两节将更详细地讨论这些功能的具体细节。清单2.12演示了如何创建无序访问视图.
ID3D11UnorderedAccessView* CreateBufferUAV( ID3DllResource* pResource )
{
D3D11_UN0RDERED_ACCESS_VIEW_DESC desc;
// For structured buffers, DXGI_FORMAT_UNKNOWN must be used!
// For standard buffers, utilize the appropriate format,
desc.Format = DXGI_FORMAT_R32G32B32_FLOAT;
desc.ViewDimension = D3D11_UAV_DIMENSI0N_BUFFER;
desc.Buffer.FirstElement = 0;
desc.Buffer.NumElements = 100;
desc.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_C0UNTER;
//desc.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_APPEND;
//desc.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_RAW;
ID3DllUnorderedAccessView* pView = 0;
HRESULT hr = g_pDevice->CreateUnorderedAccessView( pResource, &desc,&pView );
return( pView );
}
该列表显示了D3D11_BUFFER_UAV_FLAG_C0UNTER标志的设置,并注释掉了append和raw标志。此标志用于为可通过HLSL访问的缓冲区源提供计数器。此计数器通过使用HLSL缓冲区资源对象的方法进行操作,如下节所述。
HLSL结构化缓冲区资源对象
由于这种类型的缓冲区资源在可编程着色器阶段是可用的,因此有必要了解如何通过HLSL访问它。幸运的是,资源对象的声明和使用非常简单。结构化缓冲区要求在shader程序中定义相应的结构。一旦一个合适的结构可用,结构化缓冲区就用类模板语法声明。清单2.13中显示了一个示例声明,它取自第12章中描述的流体模拟演示.
// Declare the structure that represents one fluid column's state
struct GridPoint
{
float Height;
float4 Flow;
};
// Declare the input and output resources
RWStructuredBuffer<GridPoint> NewWaterState : register( u0 );
StructuredBuffer<GridPoint> CurrentWaterState : register( t0 );
在这个代码列表中,我们看到实际上有两种不同的方式来声明结构化缓冲区——StructuredBuffer<>和RWStructuredBuffero。这两种形式分别表示绑定到具有无序资源视图(用于只读访问)和无序访问视图(用于读写访问)的管道的结构化缓冲区之间的差异。寄存器语句也表明了这一点,读/写资源使用u#寄存器,只读资源使用t#寄存器。声明类型的选择将决定应用程序必须使用哪种类型的资源视图来将资源绑定到此着色器程序中使用的可编程着色器阶段。
一旦对象被声明,我们可以看到有一种简单的方法可以从HLSL中交互结构化缓冲区。使用类似数组的语法访问各个元素,使用括号来指示要使用哪个索引。这与C++的操作非常相似,并且可以使用点运算符访问结构的各个成员。当资源与着色器资源视图绑定时,只能读取元素。但是,对于无序访问视图,也可以使用相同的语法对其进行写入。当使用资源视图选择缓冲区资源的一个子集时,其元素看起来是重新映射的,使得子范围中的第一个元素用索引0访问,并且每个后续元素用递增的索引访问。
这些HLSL资源对象还提供了一个GetDimensions()方法,该方法返回缓冲区的大小,或者缓冲区的尺寸和structuredbuffer的结构元素大小。着色器程序可以使用这些来实现范围检查。如果使用D3D11_BUFFER_UAV_FLAG_C0UNTER标志创建UAVis(如本章“创建缓冲区/结构化缓冲区”部分所述),也可以使用RWStructuredBuffer中内置的计数器变量。如果UAV是在设置了该标志的情况下创建的,则HLSL程序可以使用IncrementCounter()和DecrementCount()方法。这些可用于通过提供已存储在结构化缓冲器中的元素的总数来实现定制的数据结构。使用此功能需要将UAV格式设置为DXGI_format_R32_UNNOWN