HLSL 中的资源绑定

本主题介绍将高级着色器语言 (HLSL) 着色器模型 5.1 与 Direct3D 12 配合使用的一些特定功能。 所有 Direct3D 12 硬件都支持着色器模型 5.1,因此对此模型的支持不依赖于硬件功能级别。

资源类型和数组

着色器模型 5 (SM5.0) 资源语法使用 register 关键字 (keyword) 将有关资源的重要信息中继到 HLSL 编译器。 例如,以下语句声明在槽 t3、t4、t5 和 t6 中绑定的四个纹理的数组。 t3 是唯一显示在该语句中的寄存器槽,而它仅仅是四个寄存器的数组中的第一个。

Texture2D<float4> tex1[4] : register(t3)

HLSL 中的着色器模型 5.1 (SM5.1) 资源语法基于现有的寄存器资源语法,旨在更方便地完成移植。 HLSL 中的 Direct3D 12 资源绑定到逻辑寄存器空间中的虚拟寄存器:

  • t – 表示着色器资源视图 (SRV)
  • s – 表示采样器
  • u – 表示无序访问视图 (UAV)
  • b – 表示常量缓冲区视图 (CBV)

引用着色器的根签名必须与声明的寄存器槽兼容。 例如,根签名的以下部分将与使用的纹理槽 t3 至 t6 兼容,因为它使用槽 t0 至 t98 描述了描述符表。

DescriptorTable( CBV(b1), SRV(t0,numDescriptors=99), CBV(b2) )

资源声明可以是标量值、1D 数组或多维数组:

Texture2D<float4> tex1 : register(t3,  space0)
Texture2D<float4> tex2[4] : register(t10)
Texture2D<float4> tex3[7][5][3] : register(t20, space1)

SM5.1 使用与 SM5.0 相同的资源类型和元素类型。 SM5.1 声明限制更灵活,并且仅受运行时/硬件限制的约束。 space 关键字 (keyword) 指定声明变量绑定到的逻辑寄存器空间。 space 如果省略关键字 (keyword) ,则默认空间索引 0 将隐式分配给范围 (因此tex2上述范围驻留在space0) 。 register(t3, space0) 永远不会与 register(t3, space1)冲突,也永远不会与另一个空间中可能包含 t3 的任何数组冲突。

数组资源可能具有无限大小,通过将第一个维度指定为空来声明该大小,或 0:

Texture2D<float4> tex1[] : register(t0)

匹配的描述符表可能是:

DescriptorTable( CBV(b1), UAV(u0, numDescriptors = 4), SRV(t0, numDescriptors=unbounded) )

HLSL 中的无限制数组匹配描述符表中使用 numDescriptors 设置的固定数字,HLSL 中的固定大小匹配描述符表中的无限制声明。

允许多维数组,包括无限制的大小。 这些多维数组将在寄存器空间中平展。

Texture2D<float4> tex2[3000][10] : register(t0, space0); // t0-t29999 in space0
Texture2D<float4> tex3[0][5][3] : register(t5, space1)

不允许资源范围别名。 换句话说,对于每个资源类型 (t、s、u、b) ,声明的寄存器范围不得重叠。 这也包括无限制的范围。 在不同寄存器空间中声明的范围不得重叠。 请注意,上述未绑定 tex2 () 驻留在 中 space0,而未绑定 tex3 的驻留在 中 space1,因此它们不会重叠。

访问已声明为数组的资源就像为其编制索引一样简单。

Texture2D<float4> tex1[400] : register(t3);
sampler samp[7] : register(s0);
tex1[myMaterialID].Sample(samp[samplerID], texCoords);

(索引的使用 myMaterialID 存在重要的默认限制,在 samplerID 上面的代码中) ,不允许它们在 波次中变化。 即使是基于实例更改索引,也被视为有变化。

如果需要更改索引,请在索引中指定 NonUniformResourceIndex 限定符,例如:

tex1[NonUniformResourceIndex(myMaterialID)].Sample(samp[NonUniformResourceIndex(samplerID)], texCoords);

在某些硬件上,使用此限定符会生成额外的代码来强制正确性(包括跨线程),但对性能的影响非常微小。 如果在绘制/调度调用中更改索引且未指定此限定符,则结果是不确定的。

描述符数组和纹理数组

自 DirectX 10 以来一直可以使用纹理数组。 纹理数组需要一个描述符,但是,所有数组切片必须采用相同的格式、宽度、高度和 mip 计数。 此外,该数组必须占用虚拟地址空间中的一个连续范围。 以下代码演示了从着色器访问纹理数组的示例。

Texture2DArray<float4> myTex2DArray : register(t0); // t0
float3 myCoord(1.0f,1.4f,2.2f); // 2.2f is array index (rounded to int)
color = myTex2DArray.Sample(mySampler, myCoord);

在纹理数组中,索引可以任意变化,而无需指定 NonUniformResourceIndex 之类的限定符。

等效的描述符数组是:

Texture2D<float4> myArrayOfTex2D[] : register(t0); // t0+
float2 myCoord(1.0f, 1.4f);
color = myArrayOfTex2D[2].Sample(mySampler,myCoord); // 2 is index

请注意,对数组索引的隐晦式浮点用法已由 myArrayOfTex2D[2] 取代。 此外,描述符数组在维度方面提供更大的灵活性。 此示例中的类型 Texture2D 不能有变化,但格式、宽度、高度和 mip 计数可随每个描述符一起变化。

使用纹理数组的描述符数组是合法的:

Texture2DArray<float4> myArrayOfTex2DArrays[2] : register(t0);

声明包含描述符的结构的数组是不合法的,例如,不支持以下代码。

struct myStruct {
    Texture2D                    a; 
    Texture2D                    b;
    ConstantBuffer<myConstants>  c;
};
myStruct foo[10000] : register(....);

这样就会允许内存布局 abcabcabc...,但会造成语言限制,因此不受支持。 与此相关的一种受支持方法如下所示,不过,在此情况下,内存布局是 aaa...bbb...ccc...

Texture2D                     a[10000] : register(t0);
Texture2D                     b[10000] : register(t10000);
ConstantBuffer<myConstants>   c[10000] : register(b0);

若要实现 abcabcabc... 内存布局,请在不使用 myStruct 结构的情况下使用描述符表。

资源别名

在 HLSL 着色器中指定的资源范围是逻辑范围。 在运行时,会通过根签名机制将其绑定到具体的堆范围。 通常,逻辑范围将映射到不会与其他堆范围重叠的某个堆范围。 但是,使用根签名机制可能会产生兼容类型的堆范围的别名(重叠)。 例如,上述示例中的 tex2tex3 范围可以映射到相同(或重叠)的堆范围,这会对 HLSL 程序中的别名纹理造成影响。 如果需要这种别名,则必须使用 D3D10_SHADER_RESOURCES_MAY_ALIAS 选项编译着色器,该选项是使用效果编译器工具的 /res_may_alias 选项 (FXC) 设置的。 假设资源可以采用别名,该选项会防止特定的加载/存储优化,因此可让编译器生成正确的代码。

差异和派生对象

SM5.1 不对资源索引施加限制;即 tex2[idx].Sample(…) – 索引 idx 可以是文本常量、cbuffer 常量或内插的值。 尽管编程模型提供这种高灵活性,但仍需注意几个问题:

  • 如果索引在 quad 中有差异,则硬件计算的派生对象和派生的数量(例如 LOD)可能是不确定的。 在此情况下,HLSL 编译器会尽最大努力发出警告,但不会阻止着色器进行编译。 此行为类似于差异控制流中的计算派生对象。
  • 如果资源索引有差异,相比于索引统一的情况,性能将会下降,因为硬件需要针对多个资源执行操作。 必须在 HLSL 代码中使用 NonUniformResourceIndex 函数来标记可能会出现差异的资源索引。 否则结果是不确定的。

像素着色器中的 UAV

与 SM5.0 一样,SM5.1 不会对像素着色器中的 UAV 范围施加约束。

常量缓冲区

与 SM5.0 相比,SM5.1 常量缓冲区 (cbuffer) 语法已发生更改,可让开发人员为常量缓冲区编制索引。 为了启用可编制索引的常量缓冲区,SM5.1 引入了 ConstantBuffer“模板”构造:

struct Foo
{
    float4 a;
    int2 b;
};
ConstantBuffer<Foo> myCB1[2][3] : register(b2, space1);
ConstantBuffer<Foo> myCB2 : register(b0, space1);

以上代码声明了类型为 Foo 且大小为 6 的常量缓冲区变量 myCB1,以及一个标量常量缓冲区变量 myCB2。 现在可按如下所示在着色器中为常量缓冲区变量编制索引:

myCB1[i][j].a.xyzw
myCB2.b.yy

字段“a”和“b”不会成为全局变量,必须将其视为字段。 为实现向后兼容,SM5.1 支持标量 cbuffers 的旧式 cbuffer 概念。 以下语句将“a”和“b”设置为类似于 SM5.0 中的全局只读变量。 但是,此类旧式 cbuffer 不可编制索引。

cbuffer : register(b1)
{
    float4 a;
    int2 b;
};

目前,着色器编译器仅支持对用户定义的结构使用 ConstantBuffer 模板。

出于兼容性原因,HLSL 编译器可能会自动为 space0 中声明的范围分配资源寄存器。 如果在 register 子句中省略“space”,将使用默认值 space0。 编译器使用“第一个孔适合”启发式算法来分配寄存器。 可以通过反射 API 检索分配。该 API 已经过扩展,添加了用于空间的 Space 字段,而 BindPoint 字段指示该资源寄存器范围的下限。

SM5.1 中的字节码更改

SM5.1 更改了在指令中声明和引用资源寄存器的方式。 该语法涉及到声明寄存器“变量”,类似于对组共享内存寄存器的声明方式:

Texture2D<float4> tex0          : register(t5,  space0);
Texture2D<float4> tex1[][5][3]  : register(t10, space0);
Texture2D<float4> tex2[8]       : register(t0,  space1);
SamplerState samp0              : register(s5, space0);

float4 main(float4 coord : COORD) : SV_TARGET
{
    float4 r = coord;
    r += tex0.Sample(samp0, r.xy);
    r += tex2[r.x].Sample(samp0, r.xy);
    r += tex1[r.x][r.y][r.z].Sample(samp0, r.xy);
    return r;
}

此声明可分解为:

// Resource Bindings:
//
// Name                                 Type  Format         Dim    ID   HLSL Bind     Count
// ------------------------------ ---------- ------- ----------- -----   --------- ---------
// samp0                             sampler      NA          NA     S0    a5            1
// tex0                              texture  float4          2d     T0    t5            1
// tex1[0][5][3]                     texture  float4          2d     T1   t10        unbounded
// tex2[8]                           texture  float4          2d     T2    t0.space1     8
//
//
//
// Input signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// COORD                    0   xyzw        0     NONE   float   xyzw
//
//
// Output signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// SV_TARGET                0   xyzw        0   TARGET   float   xyzw
//
ps_5_1
dcl_globalFlags refactoringAllowed
dcl_sampler s0[5:5], mode_default, space=0
dcl_resource_texture2d (float,float,float,float) t0[5:5], space=0
dcl_resource_texture2d (float,float,float,float) t1[10:*], space=0
dcl_resource_texture2d (float,float,float,float) t2[0:7], space=1
dcl_input_ps linear v0.xyzw
dcl_output o0.xyzw
dcl_temps 2
sample r0.xyzw, v0.xyxx, t0[0].xyzw, s0[5]
add r0.xyzw, r0.xyzw, v0.xyzw
ftou r1.x, r0.x
sample r1.xyzw, r0.xyxx, t2[r1.x + 0].xyzw, s0[5]
add r0.xyzw, r0.xyzw, r1.xyzw
ftou r1.xyz, r0.zyxz
imul null, r1.yz, r1.zzyz, l(0, 15, 3, 0)
iadd r1.y, r1.z, r1.y
iadd r1.x, r1.x, r1.y
sample r1.xyzw, r0.xyxx, t1[r1.x + 10].xyzw, s0[5]
add o0.xyzw, r0.xyzw, r1.xyzw
ret
// Approximately 12 instruction slots are used.

每个着色器资源范围现在包含一个对着色器字节码唯一的 ID(名称)。 例如,tex1 (t10) 纹理数组将成为着色器字节码中的“T1”。 将唯一 ID 分配到每个资源范围可以实现两个目的:

  • 明确标识 (看到哪个资源范围,dcl_resource_texture2d) 在指令中编制索引 (查看示例指令) 。
  • 将一组属性(例如元素类型、步幅大小、光栅器工作模式等)附加到声明。

请注意,范围 ID 与 HLSL 下限声明无关。

反射资源绑定 (列在顶部) 和着色器声明指令 (dcl_*) 的顺序相同,以帮助识别 HLSL 变量与字节码 ID 之间的对应关系。

SM5.1 中的每个声明指令使用 3D 操作数来定义范围 ID、下限和上限。 将发出一个附加的标记来指定寄存器空间。 还可能会发出其他标记来传递范围的其他属性,例如,cbuffer 或结构化缓冲区声明指令会发出 cbuffer 或结构的大小。 可以在 d3d12TokenizedProgramFormat.h 和 D3D10ShaderBinary::CShaderCodeParser 中找到确切的编码详细信息。

SM5.1 指令不会发出附加的资源操作数信息作为指令的一部分(与在 SM5.0 中一样)。 此信息现在会在声明指令中提供。 在 SM5.0 中,指令索引资源要求在扩展的操作码标记中描述资源属性,因为索引编制操作会模糊化与声明的关联。 在 SM5.1 中,每个 ID(例如“t1”)明确地与描述所需资源信息的单个声明相关联。 因此,不再发出在指令中用来描述资源信息的扩展操作码标记。

在非声明指令中,采样器、SRV 和 UAV 的资源操作数是一个 2D 操作数。 第一个索引是指定范围 ID 的文本常量。 第二个索引代表索引的线性化值。 该值是相对于对应寄存器空间的开头(而不是相对于逻辑范围的开头)计算的,以更好地与根签名相关联,并降低调整索引所造成的驱动程序编译器负担。

CBV 的资源操作数是一个 3D 操作数,其中包含:范围的文本 ID、常量缓冲区的索引,以及在常量缓冲区的特定实例中的偏移量。

示例 HLSL 声明

HLSL 程序不需要知道有关根签名的任何信息。 他们可以将绑定分配给虚拟“寄存器”绑定空间,t# 分配给 SLV,u# 分配给 U#,将 b# 分配给 CBV,s# 用于采样器,或依赖编译器选取赋值 (,并在) 之后使用着色器反射查询生成的映射。 根签名将描述符表、根描述符和根常量映射到此虚拟寄存器空间。

下面是 HLSL 着色器可以使用的一些示例声明。 请注意,不存在对根签名或描述符表的引用。

Texture2D foo[5] : register(t2);
Buffer bar : register(t7);
RWBuffer dataLog : register(u1);

Sampler samp : register(s0);

struct Data
{
    UINT index;
    float4 color;
};
ConstantBuffer<Data> myData : register(b0);

Texture2D terrain[] : register(t8); // Unbounded array
Texture2D misc[] : register(t0,space1); // Another unbounded array 
                                        // space1 avoids overlap with above t#

struct MoreData
{
    float4x4 xform;
};
ConstantBuffer<MoreData> myMoreData : register(b1);

struct Stuff
{
    float2 factor;
    UINT drawID;
};
ConstantBuffer<Stuff> myStuff[][3][8]  : register(b2, space3)