DXIL 资源处理¶
简介¶
DXIL 中的资源通过 LLVM IR 中的 TargetExtType
表示,并最终由 DirectX 后端转换为 DXIL 中的元数据。
在 DXC 和 DXIL 中,静态资源表示为 SRV(着色器资源视图)、UAV(统一访问视图)、CBV(常量缓冲区视图)和采样器的列表。此元数据包含一个“资源记录 ID”,该 ID 唯一标识资源和类型信息。从着色器模型 6.6 开始,还有动态资源,它们放弃元数据,而是通过指令流中的 annotateHandle
操作进行描述。
在 LLVM 中,我们尝试统一 DXC 中存在的一些替代表示,目的是使编译器中间端中资源的处理更简单、更一致。
资源类型信息和属性¶
DXIL 中的资源与许多属性相关联。
- 资源 ID
每个资源类型(SRV、UAV 等)都必须唯一的任意 ID。
在 LLVM 中,我们不费心表示它,而是选择在 DXIL 降低时间生成它。
- 绑定信息
关于资源来源的信息。这可能是 (a) 注册空间、该空间中的下界和绑定的尺寸,或者 (b) 动态资源堆中的索引。
在 LLVM 中,我们在 句柄创建内联函数 的参数中表示绑定信息。在生成 DXIL 时,我们将这些调用转换为元数据,
dx.op.createHandle
、dx.op.createHandleFromBinding
、dx.op.createHandleFromHeap
和dx.op.createHandleForLib
,根据需要。- 类型信息
可通过资源访问的数据类型。对于缓冲区和纹理,它可以是
float
或float4
等简单类型、结构体或原始字节。对于常量缓冲区,它只是一个大小。对于采样器,它是采样器的类型。在 LLVM 中,我们将此信息嵌入到资源的
target()
类型中。请参阅 资源类型。- 资源种类信息
资源的种类。在 HLSL 中,我们有
ByteAddressBuffer
、RWTexture2D
和RasterizerOrderedStructuredBuffer
等。这些映射到一组 DXIL 种类,如RawBuffer
和Texture2D
,其中包含某些属性的字段,如IsUAV
和IsROV
。在 LLVM 中,我们在
target()
类型中表示这一点。我们省略了可从类型信息推导出的信息,但我们确实有字段可以根据需要编码IsWriteable
、IsROV
和SampleCount
。
注意
待办事项:DXIL 元数据中有两个字段未表示为目标类型的一部分:IsGloballyCoherent
和 HasCounter
。
由于这些是从分析中得出的,因此将它们存储在类型上意味着我们需要在编译器管道期间更改类型。这根本不实用。我不确定我们是否需要在编译器管道期间将此信息序列化到 IR 中 - 我们可能可以使用一个分析传递,在我们需要时计算信息。
如果分析不足,我们将需要类似于 annotateHandle
的东西(但仅限于这两个属性)或在句柄创建中对其进行编码。
资源类型¶
我们定义了一组类似于 HLSL 表示各种资源的 TargetExtTypes
,尽管有一些参数化内容。这与 DXIL 不同,因为将类型简化为“dx.srv”和“dx.uav”之类的类型将意味着对这些类型的操作必须过于通用。
缓冲区¶
target("dx.TypedBuffer", ElementType, IsWriteable, IsROV, IsSigned)
target("dx.RawBuffer", ElementType, IsWriteable, IsROV)
我们需要两种独立的缓冲区类型来解释 16 字节 bufferLoad / bufferStore 操作(适用于 DXIL 的 TypedBuffers)与 rawBufferLoad / rawBufferStore 操作(用于 DXIL 的 RawBuffers 和 StructuredBuffers)之间的差异。我们称后者为“RawBuffer”以匹配操作的命名,但它可以表示 Raw 和 Structured 变体。
HLSL 的 Buffer 和 RWBuffer 表示为具有元素类型的 TypedBuffer,该元素类型是标量整数或浮点数类型,或最多 4 种此类类型的向量。HLSL 的 ByteAddressBuffer 是具有 i8 元素类型的 RawBuffer。HLSL 的 StructuredBuffers 是具有结构体、向量或标量类型的 RawBuffer。
这里一个不幸的必要条件是 TypedBuffer 需要一个额外的参数来区分有符号与无符号整数。这是因为在 LLVM IR 中,int 类型没有符号,因此为了保留此信息,我们需要一个侧信道。
这些类型通常由 BufferLoad 和 BufferStore 操作以及原子操作使用。
有一些字段来描述所有这些类型的变体
字段 |
描述 |
---|---|
ElementType |
单个元素的类型,例如 |
IsWriteable |
字段是否可写。这区分了 SRV(不可写)和 UAV(可写)。 |
IsROV |
UAV 是否是光栅化器排序视图。对于 SRV 始终为 |
IsSigned |
int 元素类型是否带符号(仅限“dx.TypedBuffer”) |
资源操作¶
资源句柄¶
我们提供了几种不同的方法,可以通过 llvm.dx.handle.*
内联函数在 IR 中实例化资源。这些内联函数在返回类型上进行了重载,为资源返回适当的句柄,并在内联函数的参数中表示绑定信息。
我们需要的三种操作是 llvm.dx.handle.fromBinding
、llvm.dx.handle.fromHeap
和 llvm.dx.handle.fromPointer
。这些大致等效于 DXIL 操作 dx.op.createHandleFromBinding
、dx.op.createHandleFromHeap
和 dx.op.createHandleForLib
,但它们折叠了随后的 dx.op.annotateHandle
操作。请注意,我们没有 dx.op.createHandle 的类似物,因为 dx.op.createHandleFromBinding
包含了它。
为了简化降低,我们在使用绑定空间起始处的索引而不是绑定自身的下界处的索引方面与 DXIL 匹配。
参数 |
类型 |
描述 |
|
---|---|---|---|
返回值 |
一个 |
一个可以进行操作的句柄 |
|
|
1 |
|
根签名中此资源的注册空间 ID。 |
|
2 |
|
其注册空间中绑定的下界。 |
|
3 |
|
绑定的范围大小。 |
|
4 |
|
要访问的绑定空间起始处的索引。 |
|
5 |
i1 |
如果资源索引可能是非统一的,则必须为 |
注意
待办事项:我们可以删除统一性位吗?我怀疑我们可以从统一性分析中推导出它……
示例
; RWBuffer<float4> Buf : register(u5, space3)
%buf = call target("dx.TypedBuffer", <4 x float>, 1, 0, 0)
@llvm.dx.handle.fromBinding.tdx.TypedBuffer_f32_1_0(
i32 3, i32 5, i32 1, i32 0, i1 false)
; RWBuffer<int> Buf : register(u7, space2)
%buf = call target("dx.TypedBuffer", i32, 1, 0, 1)
@llvm.dx.handle.fromBinding.tdx.TypedBuffer_i32_1_0t(
i32 2, i32 7, i32 1, i32 0, i1 false)
; Buffer<uint4> Buf[24] : register(t3, space5)
%buf = call target("dx.TypedBuffer", <4 x i32>, 0, 0, 0)
@llvm.dx.handle.fromBinding.tdx.TypedBuffer_i32_0_0t(
i32 2, i32 7, i32 24, i32 0, i1 false)
; struct S { float4 a; uint4 b; };
; StructuredBuffer<S> Buf : register(t2, space4)
%buf = call target("dx.RawBuffer", {<4 x float>, <4 x i32>}, 0, 0)
@llvm.dx.handle.fromBinding.tdx.RawBuffer_sl_v4f32v4i32s_0_0t(
i32 4, i32 2, i32 1, i32 0, i1 false)
; ByteAddressBuffer Buf : register(t8, space1)
%buf = call target("dx.RawBuffer", i8, 0, 0)
@llvm.dx.handle.fromBinding.tdx.RawBuffer_i8_0_0t(
i32 1, i32 8, i32 1, i32 0, i1 false)
参数 |
类型 |
描述 |
|
---|---|---|---|
返回值 |
一个 |
一个可以进行操作的句柄 |
|
|
0 |
|
要访问的资源的索引。 |
|
1 |
i1 |
如果资源索引可能是非统一的,则必须为 |
示例
; RWStructuredBuffer<float4> Buf = ResourceDescriptorHeap[2];
declare
target("dx.RawBuffer", <4 x float>, 1, 0)
@llvm.dx.handle.fromHeap.tdx.RawBuffer_v4f32_1_0(
i32 %index, i1 %non_uniform)
; ...
%buf = call target("dx.RawBuffer", <4 x f32>, 1, 0)
@llvm.dx.handle.fromHeap.tdx.RawBuffer_v4f32_1_0(
i32 2, i1 false)
缓冲区加载和存储¶
相关类型:缓冲区
我们需要分别处理来自“dx.TypedBuffer”和“dx.RawBuffer”的缓冲区加载和存储。对于TypedBuffer,我们有llvm.dx.typedBufferLoad
和llvm.dx.typedBufferStore
,它们通过简单的索引加载和存储16字节的“行”数据。对于RawBuffer,我们有llvm.dx.rawBufferPtr
,它返回一个可以根据需要进行索引、加载和存储的指针。
类型化的加载和存储操作始终作用于正好16字节的数据,因此只有几个有效的重载。对于32位或更小的类型,我们对4元素向量进行操作,例如<4 x i32>
、<4 x float>
或<4 x half>
。请注意,在16位情况下,每个16位值占用32位的存储空间。对于64位类型,我们对2元素向量进行操作 - <2 x double>
或<2 x i64>
。当在HLSL级别使用像Buffer<float>这样的类型时,预期它将在每16字节的行中操作单个浮点数 - 也就是说,加载将使用<4 x float>
变体,然后提取第一个元素。
注意
在DXC中,尝试对Buffer<double4>
进行操作会导致编译器崩溃。我们可能应该在前端拒绝此操作。
TypedBuffer 内在函数被降低到bufferLoad和bufferStore操作,而RawBufferPtr访问的内存上的操作被降低到rawBufferLoad和rawBufferStore。请注意,如果我们想支持1.2之前的DXIL版本,我们也需要将RawBuffer加载和存储降低到非原始操作。
注意
待办事项:我们需要在这里考虑CheckAccessFullyMapped。
在DXIL中,加载操作始终返回一个i32
状态值,但当它未使用时,这并不是很符合人体工程学。我们可以(1) 咬紧牙关,让加载始终返回{%ret_type, %i32},(2) 创建一个变体或仅当使用状态时更新签名,或者(3) 将其隐藏在某个侧边通道中。我倾向于(2),但可能会被说服(1) 的丑陋之处值得其简单性。
参数 |
类型 |
描述 |
|
---|---|---|---|
返回值 |
缓冲区类型的一个4或2元素向量 |
从缓冲区加载的数据 |
|
|
0 |
|
要从中加载的缓冲区 |
|
1 |
|
缓冲区的索引 |
示例
%ret = call <4 x float> @llvm.dx.typedBufferLoad.tdx.TypedBuffer_f32_0_0t(
target("dx.TypedBuffer", f32, 0, 0) %buffer, i32 %index)
%ret = call <4 x i32> @llvm.dx.typedBufferLoad.tdx.TypedBuffer_i32_0_0t(
target("dx.TypedBuffer", i32, 0, 0) %buffer, i32 %index)
%ret = call <4 x half> @llvm.dx.typedBufferLoad.tdx.TypedBuffer_f16_0_0t(
target("dx.TypedBuffer", f16, 0, 0) %buffer, i32 %index)
%ret = call <2 x double> @llvm.dx.typedBufferLoad.tdx.TypedBuffer_f64_0_0t(
target("dx.TypedBuffer", double, 0, 0) %buffer, i32 %index)
参数 |
类型 |
描述 |
|
---|---|---|---|
返回值 |
|
||
|
0 |
|
要存储到的缓冲区 |
|
1 |
|
缓冲区的索引 |
|
2 |
缓冲区类型的一个4或2元素向量 |
要存储的数据 |
示例
call void @llvm.dx.bufferStore.tdx.Buffer_f32_1_0t(
target("dx.TypedBuffer", f32, 1, 0) %buf, i32 %index, <4 x f32> %data)
call void @llvm.dx.bufferStore.tdx.Buffer_f16_1_0t(
target("dx.TypedBuffer", f16, 1, 0) %buf, i32 %index, <4 x f16> %data)
call void @llvm.dx.bufferStore.tdx.Buffer_f64_1_0t(
target("dx.TypedBuffer", f64, 1, 0) %buf, i32 %index, <2 x f64> %data)
参数 |
类型 |
描述 |
|
---|---|---|---|
返回值 |
|
指向缓冲区元素的指针 |
|
|
0 |
|
要从中加载的缓冲区 |
|
1 |
|
缓冲区的索引 |
示例
; Load a float4 from a buffer
%buf = call ptr @llvm.dx.rawBufferPtr.tdx.RawBuffer_v4f32_0_0t(
target("dx.RawBuffer", <4 x f32>, 0, 0) %buffer, i32 %index)
%val = load <4 x float>, ptr %buf, align 16
; Load the double from a struct containing an int, a float, and a double
%buf = call ptr @llvm.dx.rawBufferPtr.tdx.RawBuffer_sl_i32f32f64s_0_0t(
target("dx.RawBuffer", {i32, f32, f64}, 0, 0) %buffer, i32 %index)
%val = getelementptr inbounds {i32, f32, f64}, ptr %buf, i32 0, i32 2
%d = load double, ptr %val, align 8
; Load a float from a byte address buffer
%buf = call ptr @llvm.dx.rawBufferPtr.tdx.RawBuffer_i8_0_0t(
target("dx.RawBuffer", i8, 0, 0) %buffer, i32 %index)
%val = getelementptr inbounds float, ptr %buf, i64 0
%f = load float, ptr %val, align 4
; Store to a buffer containing float4
%addr = call ptr @llvm.dx.rawBufferPtr.tdx.RawBuffer_v4f32_0_0t(
target("dx.RawBuffer", <4 x f32>, 0, 0) %buffer, i32 %index)
store <4 x float> %val, ptr %addr
; Store the double in a struct containing an int, a float, and a double
%buf = call ptr @llvm.dx.rawBufferPtr.tdx.RawBuffer_sl_i32f32f64s_0_0t(
target("dx.RawBuffer", {i32, f32, f64}, 0, 0) %buffer, i32 %index)
%addr = getelementptr inbounds {i32, f32, f64}, ptr %buf, i32 0, i32 2
store double %d, ptr %addr
; Store a float into a byte address buffer
%buf = call ptr @llvm.dx.rawBufferPtr.tdx.RawBuffer_i8_0_0t(
target("dx.RawBuffer", i8, 0, 0) %buffer, i32 %index)
%addr = getelementptr inbounds float, ptr %buf, i64 0
store float %f, ptr %val