UE半精度下SpirV-Cross生成了错误的Metal Shader | Blurred code

UE半精度下SpirV-Cross生成了错误的Metal Shader

2024/04/13

LastMod:2024/05/05

Categories: UE

UE5 Metal后端打开半精度Shader编译支持

虚幻默认在IOS上是禁用半精度的,但是实际上手机上需要这个功能。 禁用的地方在于 BaseEngine.ini

[/Script/IOSRuntimeSettings.IOSRuntimeSettings]
ForceFloats=True 

在游戏里的DefaultEngine.ini找个地方重新覆盖这个变量为false即可打开半精度支持。

SpirV-Cross生成了错误的Metal Shader

打开半精度后发现有部分材质有 error: call to 'clamp' is ambiguous的编译错误,仔细调查后发现问题在于SpirV-Cross生成的MSL有问题。

虚幻编译MSL的基本流程是通过ShaderConductor调用 hlsl --(dxc)-> spirv -- (spirv-cross) --> msl.

通过简单的二分查找,发现这个问题出现在我们TA连的一些材质里会使用了 arctangent2这个节点,后面经过一系列的计算然后最后经过saturate就必然出现这个问题。

找了个最简单的材质连了一下,并且把spir-v打印出来,发现生成的spir-v是没有问题的,正确的处理了half精度。 那么只能开始怀疑是spirv-cross的问题。

%15 = OpExtInst %half %1 Atan2 %half_0x1p_0 %half_0x1p_1
%16 = OpExtInst %half %1 FClamp %15 %half_0x0p_0 %half_0x1p_0

最简单可复现例子

hlsl写了一个最简单的例子: https://godbolt.org/z/95axs5xWY

通过dxc -T ps_6_6 -E PSMain -spirv -enable-16bit-types 进行编译。

float4 PSMain(PSInput input) : SV_Target0
{
  return saturate( atan2(1.0h, 2.0h));
}

经过dxc编译 + spir-v编译以后最后确定生成了有问题的msl (https://shader-playground.timjones.io/85e3977ba4553f118bd37e67b325852c)

out.out_var_SV_Target0 = float4(float(clamp(precise::atan2(half(1.0), half(2.0)), half(0.0), half(1.0))));

问题定位

问题现在确定在spirv-cross里了,打开他的代码库翻了一下,以precise::atan2为关键词搜索了一下,果然发现了可能是问题原因的代码:

https://github.com/KhronosGroup/SPIRV-Cross/blob/06407561ece7d7e78544112189f3eee13adf9959/spirv_msl.cpp#L10415-L10416

// Override for MSL-specific extension syntax instructions.
// In some cases, deliberately select either the fast or precise versions of the MSL functions to match Vulkan math precision results.
void CompilerMSL::emit_glsl_op(uint32_t result_type, uint32_t id, uint32_t eop, const uint32_t *args, uint32_t count)
{
	case GLSLstd450Atan2:
		emit_binary_func_op(result_type, id, args[0], args[1], "precise::atan2");
		break;

通过这个函数的注释可以看到,在生成msl的时候,对部分函数需要特殊处理fast或者precise版本,而atan2是其中之一,这里默认选择了precise版本,也就是无论spir-v里是half还是float, 都会生成precise::atan2

所以这会导致我们生成一个 clamp(float,half,half)这样的调用,MSL不支持这种混合精度的重载从而导致编译报错。

解决方案

MSL: invalid code generated when atan2 and saturate are used in mixed precision environment · Issue #2309 · KhronosGroup/SPIRV-Cross

首先给他提了一个issue,看Khronos那边怎么处理这个情况..但是就算khronos那边修复了,也要等虚幻下次合并spir-v cross才能用上了..

2024/5/5补:

Info

Khronos已经确认并修复了这个问题,见 https://github.com/KhronosGroup/SPIRV-Cross/pull/2317 碰见这个问题可以pick这个改动

临时方案可以用虚幻的节点Arctangent2Fast绕过去。 具体的实现在fastMath.ush里,这个函数的拟合来自 https://seblagarde.wordpress.com/2014/12/01/inverse-trigonometric-functions-gpu-optimization-for-amd-gcn-architecture/, 用mathmatica拟合的结果。

误差分析和渲染测试可以看原文,我在虚幻里测试了基本没有肉眼可见的差异。

float atan2Fast( float y, float x )
{
	float t0 = max( abs(x), abs(y) );
	float t1 = min( abs(x), abs(y) );
	float t3 = t1 / t0;
	float t4 = t3 * t3;

	// Same polynomial as atanFastPos
	t0 =         + 0.0872929;
	t0 = t0 * t4 - 0.301895;
	t0 = t0 * t4 + 1.0;
	t3 = t0 * t3;

	t3 = abs(y) > abs(x) ? (0.5 * PI) - t3 : t3;
	t3 = x < 0 ? PI - t3 : t3;
	t3 = y < 0 ? -t3 : t3;

	return t3;
}