DXIL 资源处理¶
简介¶
DXIL 中的资源通过 LLVM IR 中的 TargetExtType
表示,并最终由 DirectX 后端降级为 DXIL 中的元数据。
在 DXC 和 DXIL 中,静态资源表示为 SRV(着色器资源视图)、UAV(无序访问视图)、CBV(常量缓冲区视图)和采样器的列表。此元数据包含一个“资源记录 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
的东西(但仅限于这两个属性)或在句柄创建中编码这些属性。
资源类型¶
我们定义了一组 TargetExtTypes
,它类似于各种资源的 HLSL 表示形式,尽管有一些参数化。这与 DXIL 不同,因为将类型简化为类似 “dx.srv” 和 “dx.uav” 类型意味着对这些类型的操作将不得不过于通用。
缓冲区¶
target("dx.TypedBuffer", ElementType, IsWriteable, IsROV, IsSigned)
target("dx.RawBuffer", ElementType, IsWriteable, IsROV)
我们需要两种不同的缓冲区类型来解释 DXIL 的 TypedBuffers 上使用的 16 字节 bufferLoad / bufferStore 操作与用于 DXIL 的 RawBuffers 和 StructuredBuffers 的 rawBufferLoad / rawBufferStore 操作之间的差异。我们将后者称为 “RawBuffer”,以匹配操作的命名,但它可以表示 Raw 和 Structured 变体。
HLSL 的 Buffer 和 RWBuffer 表示为 TypedBuffer,其元素类型是标量整数或浮点类型,或者是最多 4 种此类类型的向量。HLSL 的 ByteAddressBuffer 是 RawBuffer,元素类型为 i8。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.resource.handlefrombinding
、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.resource.handlefrombinding.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.resource.handlefrombinding.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.resource.handlefrombinding.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.resource.handlefrombinding.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.resource.handlefrombinding.tdx.RawBuffer_i8_0_0t(
i32 1, i32 8, i32 1, i32 0, i1 false)
; RWBuffer<float4> Global[3] : register(u6, space5)
; RWBuffer<float4> Buf = Global[2];
%buf = call target("dx.TypedBuffer", <4 x float>, 1, 0, 0)
@llvm.dx.resource.handlefrombinding.tdx.TypedBuffer_f32_1_0(
i32 5, i32 6, i32 3, i32 2, 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)
将资源作为内存访问¶
相关类型:缓冲区和纹理
从资源加载和存储通常在 LLVM 中使用仅可通过句柄对象访问的内存上的操作来表示。给定一个句柄,llvm.dx.resource.getpointer 提供一个指针,该指针可用于读取资源和(取决于类型)写入资源。
使用 llvm.dx.resource.getpointer 的访问在 DXILResourceAccess 传递中被替换为直接加载和存储操作。这些直接加载和存储操作将在本文档后面描述。
注意
目前,dx.resource.getpointer 返回的指针位于默认地址空间中,但这在未来可能会更改。
参数 |
类型 |
描述 |
|
---|---|---|---|
返回值 |
指针 |
指向缓冲区中对象的指针 |
|
|
0 |
|
要访问的缓冲区 |
|
1 |
|
缓冲区中的索引 |
示例
%ptr = call ptr @llvm.dx.resource.getpointer.p0.tdx.TypedBuffer_v4f32_0_0_0t(
target("dx.TypedBuffer", <4 x float>, 0, 0, 0) %buffer, i32 %index)
加载、采样和收集¶
相关类型:缓冲区和纹理
DXIL 中的所有加载、采样和收集操作都返回 ResRet 类型。这些类型是结构,包含某种基本类型的 4 个元素,以及 CheckAccessFullyMapped 操作使用的第 5 个元素。其中一些操作,如 RawBufferLoad,包含一个掩码和/或对齐方式,告诉我们如何解释这四个值的一些信息。
在这些操作的 LLVM IR 表示中,我们改为返回标量或向量,但我们保留了只返回最多 4 个基本类型元素的要求。这避免了中间格式中一些不必要的强制转换和结构操作,同时使降级到 DXIL 变得简单直接。
映射到返回 ResRet 的操作的 LLVM 内联函数返回一个匿名结构,其中 element-0 是标量或向量类型,element-1 是 CheckAccessFullyMapped
调用的 i1
结果。我们根本没有单独调用 CheckAccessFullyMapped
,因为这是唯一可以对此值执行的操作。实际上,这可能意味着当 HLSL 源代码中缺少此操作时,我们会插入一个 DXIL 操作来检查,但这实际上与 DXC 在实践中的行为相符。
对于 TypedBuffer 和 Texture,我们直接从资源的包含类型映射到内联函数的返回值。由于这些资源被约束为仅包含最多 4 个元素的标量和向量,因此降级到 DXIL 操作通常很简单。我们在这里的一个例外是元素中的 double 类型是特殊的 - 这些类型在 LLVM 内联函数中是允许的,但降级为 DXIL 的 i32 对,后跟 MakeDouble
操作。
参数 |
类型 |
描述 |
|
---|---|---|---|
返回值 |
包含类型和检查位的结构 |
从缓冲区加载的数据和检查位 |
|
|
0 |
|
要从中加载的缓冲区 |
|
1 |
|
缓冲区中的索引 |
示例
%ret = call {<4 x float>, i1}
@llvm.dx.resource.load.typedbuffer.v4f32.tdx.TypedBuffer_v4f32_0_0_0t(
target("dx.TypedBuffer", <4 x float>, 0, 0, 0) %buffer, i32 %index)
%ret = call {float, i1}
@llvm.dx.resource.load.typedbuffer.f32.tdx.TypedBuffer_f32_0_0_0t(
target("dx.TypedBuffer", float, 0, 0, 0) %buffer, i32 %index)
%ret = call {<4 x i32>, i1}
@llvm.dx.resource.load.typedbuffer.v4i32.tdx.TypedBuffer_v4i32_0_0_0t(
target("dx.TypedBuffer", <4 x i32>, 0, 0, 0) %buffer, i32 %index)
%ret = call {<4 x half>, i1}
@llvm.dx.resource.load.typedbuffer.v4f16.tdx.TypedBuffer_v4f16_0_0_0t(
target("dx.TypedBuffer", <4 x half>, 0, 0, 0) %buffer, i32 %index)
%ret = call {<2 x double>, i1}
@llvm.dx.resource.load.typedbuffer.v2f64.tdx.TypedBuffer_v2f64_0_0t(
target("dx.TypedBuffer", <2 x double>, 0, 0, 0) %buffer, i32 %index)
对于 RawBuffer,HLSL 加载操作可能会返回任意大小的结果,但我们仍然将 LLVM 内联函数限制为仅返回最多 4 个基本类型元素。这意味着较大的加载表示为一系列加载,这与 DXIL 匹配。与 RawBufferLoad 操作不同,我们不需要掩码/类型大小和对齐方式的参数,因为我们可以从降级期间加载的返回类型中计算出这些参数。
参数 |
类型 |
描述 |
|
---|---|---|---|
返回值 |
标量或向量和检查位的结构 |
从缓冲区加载的数据和检查位 |
|
|
0 |
|
要从中加载的缓冲区 |
|
1 |
|
缓冲区中的索引 |
|
2 |
|
给定索引处结构的偏移量 |
示例
; float
%ret = call {float, i1}
@llvm.dx.resource.load.rawbuffer.f32.tdx.RawBuffer_f32_0_0_0t(
target("dx.RawBuffer", float, 0, 0, 0) %buffer,
i32 %index,
i32 0)
%ret = call {float, i1}
@llvm.dx.resource.load.rawbuffer.f32.tdx.RawBuffer_i8_0_0_0t(
target("dx.RawBuffer", i8, 0, 0, 0) %buffer,
i32 %byte_offset,
i32 0)
; float4
%ret = call {<4 x float>, i1}
@llvm.dx.resource.load.rawbuffer.v4f32.tdx.RawBuffer_v4f32_0_0_0t(
target("dx.RawBuffer", float, 0, 0, 0) %buffer,
i32 %index,
i32 0)
%ret = call {float, i1}
@llvm.dx.resource.load.rawbuffer.v4f32.tdx.RawBuffer_i8_0_0_0t(
target("dx.RawBuffer", i8, 0, 0, 0) %buffer,
i32 %byte_offset,
i32 0)
; struct S0 { float4 f; int4 i; };
%ret = call {<4 x float>, i1}
@llvm.dx.resource.load.rawbuffer.v4f32.tdx.RawBuffer_sl_v4f32v4i32s_0_0t(
target("dx.RawBuffer", {<4 x float>, <4 x i32>}, 0, 0, 0) %buffer,
i32 %index,
i32 0)
%ret = call {<4 x i32>, i1}
@llvm.dx.resource.load.rawbuffer.v4i32.tdx.RawBuffer_sl_v4f32v4i32s_0_0t(
target("dx.RawBuffer", {<4 x float>, <4 x i32>}, 0, 0, 0) %buffer,
i32 %index,
i32 1)
; struct Q { float4 f; int3 i; }
; struct R { int z; S x; }
%ret = call {i32, i1}
@llvm.dx.resource.load.rawbuffer.i32(
target("dx.RawBuffer", {i32, {<4 x float>, <3 x i32>}}, 0, 0, 0)
%buffer, i32 %index, i32 0)
%ret = call {<4 x float>, i1}
@llvm.dx.resource.load.rawbuffer.i32(
target("dx.RawBuffer", {i32, {<4 x float>, <3 x i32>}}, 0, 0, 0)
%buffer, i32 %index, i32 4)
%ret = call {<3 x i32>, i1}
@llvm.dx.resource.load.rawbuffer.i32(
target("dx.RawBuffer", {i32, {<4 x float>, <3 x i32>}}, 0, 0, 0)
%buffer, i32 %index, i32 20)
; byteaddressbuf.Load<int64_t4>
%ret = call {<4 x i64>, i1}
@llvm.dx.resource.load.rawbuffer.v4i64.tdx.RawBuffer_i8_0_0t(
target("dx.RawBuffer", i8, 0, 0, 0) %buffer,
i32 %byte_offset,
i32 0)
存储¶
相关类型:纹理和缓冲区
TextureStore、BufferStore 和 RawBufferStore DXIL 操作将四个组件写入纹理或缓冲区。这些操作包括一个掩码参数,当写入的组件少于 4 个时使用,但值得注意的是,这仅采用连续的 x、xy、xyz 和 xyzw 值。
我们定义 LLVM 存储内联函数以在存储多个组件时接受向量,而不是使用 undef 和掩码,但在其他方面与 DXIL 操作相当匹配。
对于 TypedBuffer,我们只需要一个坐标,并且我们必须始终写入一个向量,因为不可能进行部分写入。与上面描述的加载操作类似,我们专门处理 64 位类型,并且仅处理 2 元素向量而不是 4 元素向量。
示例
参数 |
类型 |
描述 |
|
---|---|---|---|
返回值 |
|
||
|
0 |
|
要存储到的缓冲区 |
|
1 |
|
缓冲区中的索引 |
|
2 |
缓冲区类型的 4 元素或 2 元素向量 |
要存储的数据 |
示例
call void @llvm.dx.resource.store.typedbuffer.tdx.Buffer_v4f32_1_0_0t(
target("dx.TypedBuffer", f32, 1, 0) %buf, i32 %index, <4 x f32> %data)
call void @llvm.dx.resource.store.typedbuffer.tdx.Buffer_v4f16_1_0_0t(
target("dx.TypedBuffer", f16, 1, 0) %buf, i32 %index, <4 x f16> %data)
call void @llvm.dx.resource.store.typedbuffer.tdx.Buffer_v2f64_1_0_0t(
target("dx.TypedBuffer", f64, 1, 0) %buf, i32 %index, <2 x f64> %data)
对于 RawBuffer,我们需要两个索引,并且我们接受标量和 4 个或更少元素的向量。请注意,我们在此处允许 4 个 64 位元素的向量。
示例
参数 |
类型 |
描述 |
|
---|---|---|---|
返回值 |
|
||
|
0 |
|
要存储到的缓冲区 |
|
1 |
|
缓冲区中的索引 |
|
2 |
|
结构化缓冲区元素中的字节偏移量 |
|
3 |
标量或向量 |
要存储的数据 |
示例
; float
call void @llvm.dx.resource.store.rawbuffer.tdx.RawBuffer_f32_1_0_0t.f32(
target("dx.RawBuffer", float, 1, 0, 0) %buffer,
i32 %index, i32 0, float %data)
call void @llvm.dx.resource.store.rawbuffer.tdx.RawBuffer_i8_1_0_0t.f32(
target("dx.RawBuffer", i8, 1, 0, 0) %buffer,
i32 %index, i32 0, float %data)
; float4
call void @llvm.dx.resource.store.rawbuffer.tdx.RawBuffer_v4f32_1_0_0t.v4f32(
target("dx.RawBuffer", <4 x float>, 1, 0, 0) %buffer,
i32 %index, i32 0, <4 x float> %data)
call void @llvm.dx.resource.store.rawbuffer.tdx.RawBuffer_i8_1_0_0t.v4f32(
target("dx.RawBuffer", i8, 1, 0, 0) %buffer,
i32 %index, i32 0, <4 x float> %data)
; struct S0 { float4 f; int4 i; }
call void @llvm.dx.resource.store.rawbuffer.v4f32(
target("dx.RawBuffer", { <4 x float>, <4 x i32> }, 1, 0, 0) %buffer,
i32 %index, i32 0, <4 x float> %data0)
call void @llvm.dx.resource.store.rawbuffer.v4i32(
target("dx.RawBuffer", { <4 x float>, <4 x i32> }, 1, 0, 0) %buffer,
i32 %index, i32 16, <4 x i32> %data1)
; struct Q { float4 f; int3 i; }
; struct R { int z; S x; }
call void @llvm.dx.resource.store.rawbuffer.i32(
target("dx.RawBuffer", {i32, {<4 x float>, <3 x half>}}, 1, 0, 0)
%buffer,
i32 %index, i32 0, i32 %data0)
call void @llvm.dx.resource.store.rawbuffer.v4f32(
target("dx.RawBuffer", {i32, {<4 x float>, <3 x half>}}, 1, 0, 0)
%buffer,
i32 %index, i32 4, <4 x float> %data1)
call void @llvm.dx.resource.store.rawbuffer.v3f16(
target("dx.RawBuffer", {i32, {<4 x float>, <3 x half>}}, 1, 0, 0)
%buffer,
i32 %index, i32 20, <3 x half> %data2)
; byteaddressbuf.Store<int64_t4>
call void @llvm.dx.resource.store.rawbuffer.tdx.RawBuffer_i8_1_0_0t.v4f64(
target("dx.RawBuffer", i8, 1, 0, 0) %buffer,
i32 %index, i32 0, <4 x double> %data)
常量缓冲区加载¶
相关类型:CBuffers
CBufferLoadLegacy 操作(尽管名称如此,但它是任何 DXIL 版本中从 cbuffer 加载的唯一受支持的方式)加载 cbuffer 的单个“行”,正好是 16 字节。操作的返回值由 CBufRet 类型表示,该类型具有 2 个 64 位值、4 个 32 位值和 8 个 16 位值的变体。
我们在 LLVM IR 中用 3 个独立的操作表示这些,它们分别返回 2 元素、4 元素或 8 元素结构。
参数 |
类型 |
描述 |
|
---|---|---|---|
返回值 |
4 个 32 位值的结构 |
cbuffer 的单行,解释为 4 个 32 位值 |
|
|
0 |
|
要从中加载的缓冲区 |
|
1 |
|
缓冲区中的索引 |
示例
%ret = call {float, float, float, float}
@llvm.dx.resource.load.cbufferrow.4(
target("dx.CBuffer", target("dx.Layout", {float}, 4, 0)) %buffer,
i32 %index)
%ret = call {i32, i32, i32, i32}
@llvm.dx.resource.load.cbufferrow.4(
target("dx.CBuffer", target("dx.Layout", {i32}, 4, 0)) %buffer,
i32 %index)
参数 |
类型 |
描述 |
|
---|---|---|---|
返回值 |
2 个 64 位值的结构 |
cbuffer 的单行,解释为 2 个 64 位值 |
|
|
0 |
|
要从中加载的缓冲区 |
|
1 |
|
缓冲区中的索引 |
示例
%ret = call {double, double}
@llvm.dx.resource.load.cbufferrow.2(
target("dx.CBuffer", target("dx.Layout", {double}, 8, 0)) %buffer,
i32 %index)
%ret = call {i64, i64}
@llvm.dx.resource.load.cbufferrow.2(
target("dx.CBuffer", target("dx.Layout", {i64}, 4, 0)) %buffer,
i32 %index)
参数 |
类型 |
描述 |
|
---|---|---|---|
返回值 |
8 个 16 位值的结构 |
cbuffer 的单行,解释为 8 个 16 位值 |
|
|
0 |
|
要从中加载的缓冲区 |
|
1 |
|
缓冲区中的索引 |
示例
%ret = call {half, half, half, half, half, half, half, half}
@llvm.dx.resource.load.cbufferrow.8(
target("dx.CBuffer", target("dx.Layout", {half}, 2, 0)) %buffer,
i32 %index)
%ret = call {i16, i16, i16, i16, i16, i16, i16, i16}
@llvm.dx.resource.load.cbufferrow.8(
target("dx.CBuffer", target("dx.Layout", {i16}, 2, 0)) %buffer,
i32 %index)