GLSL中一致和常数之间的浮点行为不同



我正试图在GLSL中实现模拟的双精度,我观察到一个奇怪的行为差异导致GLSL中出现细微的浮点错误。

考虑以下片段着色器,写入4浮点纹理以打印输出。

layout (location = 0) out vec4 Output
uniform float s;
void main()
{
float a = 0.1f;
float b = s;
const float split = 8193.0; // = 2^13 + 1
float ca = split * a;
float cb = split * b;
float v1a = ca - (ca - a);
float v1b = cb - (cb - b);
Output = vec4(a,b,v1a,v1b);
}

这是我观察到的输出

GLSL输出,统一:

a = 0.1            0x3dcccccd
b = 2.86129e-06    0x36400497
v1a = 0.0999756    0x3dccc000
v1b = 2.86129e-06  0x36400497

现在,以给定的b1b2的值作为输入,v2b的值不具有预期的结果。或者至少它没有与CPU上相同的结果(如图所示):

C++输出:

a = 0.100000     0x3dcccccd
b = 0.000003     0x36400497
v1a = 0.099976   0x3dccc000
v1b = 0.000003   0x36400000

注意v1b值的差异(0x364004970x36400000)。

因此,为了弄清楚发生了什么(以及谁是对的),我尝试在GLSL中重新进行计算,用一个稍微修改过的着色器将uniform值替换为一个常量,并用it值替换uniform。

layout (location = 0) out vec4 Output
void main()
{
float a = 0.1f;
float b = uintBitsToFloat(0x36400497u);
const float split = 8193.0; // = 2^13 + 1
float ca = split * a;
float cb = split * b;
float v1a = ca - (ca - a);
float v1b = cb - (cb - b);
Output = vec4(a,b,v1a,v1b);
}

这一次,我得到了与C++版本相同的计算结果。

带常数的GLSL输出:

a = 0.1            0x3dcccccd
b = 2.86129e-06    0x36400497
v1a = 0.0999756    0x3dccc000
v1b = 2.86102e-06  0x36400000

我的问题是,是什么使浮点计算在统一变量和常数之间表现不同?这是某种幕后编译器优化吗?

以下是我笔记本电脑英特尔GPU的OpenGL供应商字符串,但我在nVidia卡上也观察到了同样的行为。

Renderer : Intel(R) HD Graphics 520
Vendor   : Intel
OpenGL   : 4.5.0 - Build 23.20.16.4973
GLSL     : 4.50 - Build 23.20.16.4973

因此,正如@njuffa在评论中所提到的,通过对依赖于严格IEEE754操作的值使用precise修饰符来解决问题:

layout (location = 0) out vec4 Output
uniform float s;
void main()
{
float a = 0.1f;
float b = s;
const float split = 8193.0; // = 2^13 + 1
precise float ca = split * a;
precise float cb = split * b;
precise float v1a = ca - (ca - a);
precise float v1b = cb - (cb - b);
Output = vec4(a,b,v1a,v1b);
}

输出:

a = 0.1            0x3dcccccd
b = 2.86129e-06    0x36400497
v1a = 0.0999756    0x3dccc000
v1b = 2.86102e-06  0x36400000

编辑:很可能只需要最后一个precise来约束导致其计算的操作,以避免不必要的优化。

GPU不一定具有/使用IEEE 754某些实现的位数较少,因此结果会有所不同。这与您在FPU上比较floatdouble的结果相同。但是,如果GLSL实现允许它看到:,则可以尝试强制精度

  • 在OpenGL ES 2.0/GLSL中,哪里需要精度说明符?

    不确定它是否也适用于标准GL/GLSL,因为我从未使用过它。

在更糟糕的情况下,如果GPU允许,请使用doubledvec,但请注意,目前还没有64位插值器(至少据我所知)。

要排除由于按纹理传递结果而导致的舍入,请参见:

  • GLSL调试打印

您还可以通过打印来检查GPU上的尾数位数

1.0+1.0/2.0
1.0+1.0/4.0
1.0+1.0/8.0
1.0+1.0/16.0
...
1.0+1.0/2.0^i

最后一个未打印为1.0的数字的i是尾数比特的数目。所以你可以检查一下是不是23。。。

最新更新