预期差异 vs DXC 和 FXC

简介

HLSL 目前有两个参考编译器,DirectX 着色器编译器 (DXC)效果编译器 (FXC)。这两个参考编译器并不完全一致。参考中的一些已知分歧在 DXC 的 GitHub 上 有跟踪,但还有许多其他已知问题。

Clang 实现的 HLSL 也不完全匹配任何参考实现,而是被编写为匹配 草案语言规范

本文档是对 Clang 实现的 HLSL 与现有参考编译器之间的已知差异的非详尽收集。

一般原则

Clang 与早期参考编译器之间的预期差异主要集中在提高一致性和正确性上。两个参考编译器并不总是能在所有上下文中以相同的方式应用语言规则。

Clang 还通过提供不同的诊断信息偏离了参考编译器,无论是文本消息还是产生诊断信息的上下文。在努力与符合规范的 HLSL 代码保持高度的源代码兼容性的同时,Clang 可能会为不正确的代码生成更早且更健壮的诊断信息,或者拒绝参考编译器错误接受的代码。

语言版本

Clang 的目标是与 DXC 实现的 HLSL 2021 语言兼容。在 HLSL 的早期版本中删除的语言特性可能会在逐案基础上添加,但未计划在初始实现中添加。

重载解析

Clang 的 HLSL 实现采用 C++ 重载解析规则,如基于提案 00070008 提议的 HLSL 202x 中一样。

Clang 与 DXC 重载解析之间最大的区别在于用于识别最佳匹配重载的算法。在下面 多参数重载 部分中,详细介绍了算法差异。这里重点介绍三个高级差异

  • 不应该有 DXC 和 Clang 都成功解析重载,而解析后的重载在两者之间不同的情况。

  • 在某些情况下,Clang 会成功解析 DXC 无法解析的重载,因为我们在 Clang 中修剪了重载集以消除歧义。

  • 在某些情况下,DXC 会成功解析 Clang 无法解析的重载,原因有两个: (1) DXC 仅为内置函数生成部分重载集, (2) DXC 解析了可能应该是歧义的情况。

Clang 的实现将标准重载解析规则扩展到 HLSL 库功能。这会导致 Clang 与 DXC 之间的重载解析行为发生细微变化。以下是一些示例

void halfOrInt16(half H);
void halfOrInt16(uint16_t U);
void halfOrInt16(int16_t I);

void takesDoubles(double, double, double);

cbuffer CB {
  bool B;
  uint U;
  int I;
  float X, Y, Z;
  double3 R, G;
}

void takesSingleDouble(double);
void takesSingleDouble(vector<double, 1>);

void scalarOrVector(double);
void scalarOrVector(vector<double, 2>);

export void call() {
  half H;
  halfOrInt16(I); // All: Resolves to halfOrInt16(int16_t).

#ifndef IGNORE_ERRORS
  halfOrInt16(U); // All: Fails with call ambiguous between int16_t and uint16_t
                  // overloads

  // asfloat16 is a builtin with overloads for half, int16_t, and uint16_t.
  H = asfloat16(I); // DXC: Fails to resolve overload for int.
                    // Clang: Resolves to asfloat16(int16_t).
  H = asfloat16(U); // DXC: Fails to resolve overload for int.
                    // Clang: Resolves to asfloat16(uint16_t).
#endif
  H = asfloat16(0x01); // DXC: Resolves to asfloat16(half).
                       // Clang: Resolves to asfloat16(uint16_t).

  takesDoubles(X, Y, Z); // Works on all compilers
#ifndef IGNORE_ERRORS
  fma(X, Y, Z); // DXC: Fails to resolve no known conversion from float to
                //   double.
                // Clang: Resolves to fma(double,double,double).

  double D = dot(R, G); // DXC: Resolves to dot(double3, double3), fails DXIL Validation.
                        // FXC: Expands to compute double dot product with fmul/fadd
                        // Clang: Fails to resolve as ambiguous against
                        //   dot(half, half) or dot(float, float)
#endif

#ifndef IGNORE_ERRORS
  tan(B); // DXC: resolves to tan(float).
          // Clang: Fails to resolve, ambiguous between integer types.

#endif

  double D;
  takesSingleDouble(D); // All: Fails to resolve ambiguous conversions.
  takesSingleDouble(R); // All: Fails to resolve ambiguous conversions.

  scalarOrVector(D); // All: Resolves to scalarOrVector(double).
  scalarOrVector(R); // All: Fails to resolve ambiguous conversions.
}

注意

在 Clang 中,有意决定排除 dot(vector<double,N>, vector<double,N>) 重载,并允许重载解析解析 vector<float,N> 重载。这种方法提供了 -Wconversion 诊断信息,通知用户转换而不是静默地改变相对于其他重载的精度(正如 FXC 所做的那样),或者生成将导致验证失败的代码(正如 DXC 所做的那样)。

多参数重载

除了单元素转换中的差异之外,Clang 和 DXC 在多参数重载解析方面也存在很大差异。为了实现 非成员运算符重载,需要 C++ 多参数重载解析行为(或非常类似的行为)。

Clang 采用 HLSL 草案规范 中来自 C++ 的灵感语言,如果对于所有参数,转换序列都不比相应的转换序列差,并且对于至少一个参数,它更好,那么重载 f1 就会比 f2 更合适。

cbuffer CB {
  int I;
  float X;
  float4 V;
}

void twoParams(int, int);
void twoParams(float, float);
void threeParams(float, float, float);
void threeParams(float4, float4, float4);

export void call() {
  twoParams(I, X); // DXC: resolves twoParams(int, int).
                   // Clang: Fails to resolve ambiguous conversions.

  threeParams(X, V, V); // DXC: resolves threeParams(float4, float4, float4).
                        // Clang: Fails to resolve ambiguous conversions.
}

对于上面的示例,由于使用混合参数调用的 twoParams 会产生隐式转换序列,它们是 { ExactMatch, FloatingIntegral } 和 { FloatingIntegral, ExactMatch }。在这两种情况下,一个参数在另一个序列中的转换都更差,因此重载是模棱两可的。

threeParams 示例中,序列是 { ExactMatch, VectorTruncation, VectorTruncation } 或 { VectorSplat, ExactMatch, ExactMatch },同样在这两种情况下,至少一个参数在另一个序列中的转换都更差,因此重载是模棱两可的。

注意

下面记录的 DXC 行为是未记录的,因此这是从观察和阅读源代码中推断出来的。

DXC 用于确定最佳重载的方法会为每个参数表达式的每个隐式转换序列生成一个整数得分值。强制转换的得分基于一个难以反向工程的位掩码构造。似乎

  • 精确匹配是 0

  • 维度增加是 1

  • 提升是 2

  • 整数 -> 浮点数转换是 4

  • 浮点数 -> 整数转换是 8

  • 强制转换是 16

这些掩码相互或运算以生成强制转换的得分。

然后将每个转换序列的得分相加以生成重载候选者的得分。得分最低的重载候选者是最佳候选者。如果多个重载都匹配最低得分,则调用是模棱两可的。