DXIL之一般问题
DXIL之一般问题(General Issues)
一个重要的目标是使HLSL更接近于严格的C/C++子集。这对DXIL的设计以及下面提到的将来硬件功能请求有着影响。
术语
资源(Resource)指的是以下之一:- SRV - 着色器资源视图(只读)
- UAV - 无序的访问视图(读写)
- CBV - 常量缓冲(buffer)视图(只读)
- Sampler (采样器)
Intrinsics通常指的是在核心LLVM IR中缺失的操作。DXIL将HLSL内置函数(也称为intrinsic)表示为外部函数调用,而不是LLVM intrinsics。
就是说,在DXIL中, Intrinsic是HLSL的的一些操作,没法表示成LLVM IR , 而这些操作在LLVM 看来是外部函数调用。
DXIL 抽象级别
DXIL具有类似于“标量化”的DXBC的抽象级别。DXIL是一个较低级别的中间表示(IR),可方便在驱动程序编译器中进行快速且稳健的即时编译(JIT)。特别地,以下步骤用于将HLSL抽象降低到DXIL:
- 优化函数参数的复制
- 内联函数
- 分配(allocate)和转换(tranform)着色器签名
- 降低矩阵复杂度,优化中间存储
- 线性化多维数组和用户定义类型的访问
- 标量化向量
标量IR
DXIL操作使用标量数量。可以将多个标量数量组合在一起形成结构体,以表示多个返回值,这在内存操作(例如加载/存储、采样等)中非常有用,因为它可以提高访问的连续性。元数据(Metadata)、资源声明(Resource)和调试(Debugging)信息可能包含向量,以更接近地传达源代码的结构给工具和调试器。
将来版本的IR可能包含向量或分组提示,用于处理小于32位的数据类型,例如half和i16。
内存访问
从概念上讲,DXIL与DXBC在访问不同类型的内存时保持了一致性。越界行为和各种限制也得以保留。可索引的线程本地变量和组共享变量被表示为变量,并通过类似LLVM C的指针进行访问。
从DXIL的角度来看,经过重新排序的资源(如纹理)具有不透明的内存布局。对这些资源的访问是通过内部函数进行的。
常量缓冲区内存有两种布局方式:(1)legacy(遗留布局),与DXBC的布局匹配; (2)线性布局。SM6 DXIL使用内部函数(intrinsics)来读取任一布局下的常量缓冲区。
着色器签名需要进行打包,并位于一种特殊类型的内存中,无法按线性方式查看。通过DXIL中的特殊内部函数(intrinsics)来访问签名值。如果需要将签名参数传递给函数,则首先在线程本地内存中创建一个副本,然后将副本传递给函数。
类型化缓冲区(typed buffer)表示具有数据转换的内存。通过DXIL中的特殊函数以元素粒度索引来进行类型化缓冲区的加载/存储/原子操作。
支持以下指针类型:
- 非可索引的线程本地变量。
- 可索引的线程局部变量(DXBC x-寄存器).
- Groupshared变量(DXBC g-寄存器).
- 设备内存指针.
- 类似于常量缓冲区的内存指针。
DXIL 指针的类型是通过 LLVM addrspace 构造进行区分的。HLSL 编译器会尽最大努力推断出准确的指针 addrspace,以便驱动程序编译器可以发出最有效的指令。
一个指针可以通过多种方式产生:
-
全局变量
-
AllocaInst(Alloca Instruction)是LLVM的一种指令,用于在函数栈帧上为局部变量分配内存空间.
-
作为某些指针算术运算的结果进行合成。
DXIL在其表示中使用32位指针。
Out-of-bounds behavior 越界行为
可索引的线程本地访问是通过LLVM指针进行的,并具有类似C的越界语义。Groupshared 访问也是通过LLVM指针进行的。Groupshared指针的来源必须是单个TGSM(Thread Group Shared Memory)分配。如果Groupshared指针使用内部GEP(GetElementPtr)指令,它应该不会发生越界。对于内部指针的越界访问行为是未定义的。对于来自常规GEP的Groupshared指针,越界访问的行为与DXBC相同。对于越界访问,加载操作会返回0;越界存储操作会被静默丢弃。
资源访问在越界行为方面与 DXBC 保持一致。对于越界的加载操作,返回值为 0;越界的存储操作会被静默(slightly)丢弃。
在 SM6.0 及之后的版本中,越界指针访问具有未定义的(类似于 C 语言的)行为。可以使用 LLVM 内存优化传递来优化此类访问。如果需要指定越界行为,可以使用内部函数来访问内存。
内存访问粒度 (Memory access granularity)
Intrinsic 和资源访问可能会涵盖比指令要求的更宽的访问范围。DXIL 定义了对于线程本地内存的 i1、i16、i32、i64、f16、f32、f64 类型的内存访问,以及对于内存 I/O(即组共享内存(groupshared memory)和通过常量缓冲区(CBs)、无序访问视图(UAVs)和纹理采样器视图(SRVs)等资源访问的内存)的 i32、f32 和 f64 类型的访问。虚拟值的数量
在 DXIL 中,虚拟值的数量没有限制。IR 保证处于静态单赋值(SSA)形式。对于经过优化的着色器,优化器会运行 -mem2reg LLVM pass果有好处的话,还会执行其他的内存到寄存器提升操作。控制流限制
DXIL 的控制流图必须是可还原的,通过 T1-T2 测试进行检查。DXIL 不保留 DXBC 的结构化控制流。保留结构化控制流属性将对通过 LLVM 对 DXIL 进行优化的第三方工具造成重大负担,并降低 DXIL 的吸引力。 DXIL 允许 switch 标签块的 fall-through。这与 DXBC 不同,DXBC 中是禁止 fall-through 的。 DXIL 不支持 DXBC 的 label 和 call 指令;可以使用 LLVM 函数代替(参见下文)。这些指令的主要用途是:(1)不支持 HLSL 接口,和(2)注释为 [call] 的 switch 语句中的 case-body 的提取,这不是一个关注的情况。
函数
DXIL 支持函数和调用指令,而不是 DXBC 的标签(labels)和调用(calls)。不允许递归调用;DXIL 验证器会强制执行此规则。 这些函数是常规的 LLVM 函数。参数可以通过值传递或引用传递。这些函数旨在为大型复杂着色器的分离编译提供便利。然而,驱动程序编译器可以根据需要选择性地进行函数内联。
标识符
DXIL 的标识符必须符合 LLVM IR 的标识符规则。标识符重整(mangling )规则是使用 Clang 3.7 与 HLSL 目标时采用的规则。
保留了以下标识符前缀:
- dx., dxil.
- llvm.dx., llvm.dxil.
地址宽度
DXIL 只会使用 32 位地址来表示指针。字节偏移量也是 32 位。着色器限制
DXIL 不支持以下内容:- 递归
- 异常
- 间接函数调用和动态分派(类似于多态,在运行时确定类型)
入口点 (Entry Point)
dx.entryPoints 元数据指定了一个入口点记录的列表,每个入口点对应一个记录。在模块中,库可以为每个模块指定多个入口点,但目前此功能不在 DXIL 规范中;其他着色器模型必须且只能指定一个入口点。 例如:define void @"\01?myfunc1@@YAXXZ"() #0 { ... } define float @"\01?myfunc2@@YAMXZ"() #0 { ... } !dx.entryPoints = !{ !1, !2 } !1 = !{ void ()* @"\01?myfunc1@@YAXXZ", !"myfunc1", !3, null, null } !2 = !{ float ()* @"\01?myfunc2@@YAMXZ", !"myfunc2", !5, !6, !7 }
每个入口点元数据记录指定以下内容:
- 对入口点函数全局符号的引用
- 非修饰名称(没有经过重载处理的原始名称)
- 签名列表
- 资源列表
- 着色器功能和其他属性的标签-值对(tag-value pair)列表
'null' 值表示特定节点的缺失。
着色器功能(Shader capabilities )是除了着色器模型指定的属性之外的附加属性。这个列表以i32标签和紧随其后的值的形式组织。
细分着色器表示
Hull shader 被表示为两个函数,通过元数据相关联:(1) 控制点阶段函数,它是 hull shader 的入口点,和 (2) 片元常量阶段函数。例如:
!dx.entryPoints = !{ !1 } !1 = !{ void ()* @"ControlPointFunc", ..., !2 } ; shader entry record !2 = !{ !"HS", !3 } !3 = !{ void ()* @"PatchConstFunc", ... } ; additional hull shader state
片元常量函数表示原始的HLSL计算,并且没有像DXBC中那样分为fork和join阶段。 驱动程序编译器可以在目标GPU上获得性能提高时执行此类分离操作。
在从 DXBC 转换为 DXIL 的过程中,原始的片元常量函数无法在 DXBC 到 DXIL 的转换过程中恢复。相反,每个 fork 和 join 阶段的指令都会被一个循环“包裹”,该循环迭代相应数量的 phase-instance-count 次数。因此,fork/join 实例 ID 变成了循环的归纳变量。LoadPatchConstant 内置函数(见下文)表示从 DXBC vpc 寄存器加载数据。
下表总结了加载 hull shader 和 domain shader 的输入以及存储输出的内置函数名称。CP 代表控制点,PC 代表片元常量。
Operation | Control Point (Hull) | Patch Constant | Domain |
---|---|---|---|
Store Input CP | |||
Load Input CP | LoadInput | LoadInput | |
Store Output CP | StoreOutput | ||
Load Output CP | LoadOutputControlPoint | LoadInput | |
Store PC | StorePatchConstant | ||
Load PC | LoadPatchConstant | LoadPatchConstant | |
Store Output Vertex | StoreOutput |
在片元常量阶段中,LoadPatchConstant 函数仅由 DXBC-to-DXIL 转换器生成,用于访问 DXBC 的 vpc 寄存器。HLSL 编译器产生的中间表示 (IR) 直接引用了 LLVM IR 值。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· .NET Core 中如何实现缓存的预热?
· 三行代码完成国际化适配,妙~啊~
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?