UE4.25 中 ShaderPatch 问题

在 4.25 引擎版本中调用 FShaderCodeLibrary::CreatePatchLibrary 来创建 ShaderCode Patch 会触发 check 抛异常:

这是因为 FEditorShaderCodeArchive 的构造函数中调用了 ShaderHashTable 的 Initialize,并给了默认值0x1000

1
2
3
4
5
6
7
8
9
10
FEditorShaderCodeArchive(FName InFormat)
: FormatName(InFormat)
, Format(nullptr)
{
Format = GetTargetPlatformManagerRef().FindShaderFormat(InFormat);
check(Format);

SerializedShaders.ShaderHashTable.Initialize(0x10000);
SerializedShaders.ShaderMapHashTable.Initialize(0x10000);
}

导致在后续的流程中 (FSerializedShaderArchive::Serialize) 调用 Initialize 的时候 check 失败了(因为 HaseSize 已经有值了,并不是 0,对其再调用 Initialize 就触发了 check):

查了下 FEditorShaderCodeArchive 构造函数中调用 Initialize 的代码是在 4.25 之后的引擎版本才有的,所以影响到的之后 4.25+ 的版本。
代码对比:

解决方案:把 FSerializedShaderArchive::SerializeShaderMapHashTableInitializeShaderHashTableInitialize 在 Editor 下注释掉,因为 FEditorShaderCodeArchive 的代码只在 Editor 下有效,并且是只在生成 ShaderPatch 时有用。

这就造成了以下几个问题:

  1. FEditorShaderCodeArchive的构造只有 Eidotor 并且 ShaderPatch 是才有用,也就意味着这里写的 ShaderMapHashTableInitializeShaderHashTableInitialize只有在创建 ShaderPatch 时才会执行
  2. 在打基础包时执行 Cook 会编译 shader,但是不会执行 FEditorShaderCodeArchive 的构造,ShaderMapHashTableInitializeShaderHashTableInitialize 也就不会执行,就需要在使用的地方来调用它们的初始化

这也是 UE 中没有管理好这两个状态的地方:在 FEditorShaderCodeArchiveFSerializedShaderArchive::Serialize中都做了 Initialize 的操作,在打基础包时造成了 ShaderMapHashTableShaderHashTableInitialize 已经被 FEditorShaderCodeArchive 初始化的情况下又被 FSerializedShaderArchive::Serialize 执行了一遍,导致 Crash,但是我们又不能粗暴地把任何一处的初始化操作去掉,只能通过检测 ShaderMapHashTableShaderHashTableInitialize 是否已经被执行,来选择性的跳过。

阅读代码可以知道 ShaderMapHashTableShaderHashTableInitialize 只应该执行一次,并且初始化之后 HashSize 和 IndexSize 应该具有非 0 值:

Runtime/Core/Public/Containers/HashTable.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FORCEINLINE void FHashTable::Initialize(uint32 InHashSize, uint32 InIndexSize)
{
check(HashSize == 0u);
check(IndexSize == 0u);

HashSize = InHashSize;
IndexSize = InIndexSize;

check(HashSize <= 0x10000);
check(FMath::IsPowerOfTwo(HashSize));

if (IndexSize)
{
HashMask = (uint16)(HashSize - 1);

Hash = new uint32[HashSize];
NextIndex = new uint32[IndexSize];

FMemory::Memset(Hash, 0xff, HashSize * 4);
}
}

Initialize 时会检测当前的 HashSizeIndexSize是否为 0,并在之后进行赋值。所以,我们只要获取 FHashTableHashSizeIndexSize 检测它们是否为 0 即可判断当前的 HashTable 对象是否已经被 Initialize 过,但是,UE 里的 FHashTable 里这两个成员都是 protected 的,只能修改引擎来实现了:

添加获取 FHashTableHashSizeIndexSize 属性的成员函数:

1
2
3
4
5
6
7
8
class FHashTable
{
public:
// ...
FORCEINLINE uint32 GetHashSize()const{return HashSize;};
FORCEINLINE uint32 GetIndexSize()const{return IndexSize;};
// ...
};

然后在 FSerializedShaderArchive::Serialize 进行检测,如果已被初始化则跳过 Initialize 逻辑:

Runtime/RenderCore/Private/ShaderCodeArchive.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
void FSerializedShaderArchive::Serialize(FArchive& Ar)
{
Ar << ShaderMapHashes;
Ar << ShaderHashes;
Ar << ShaderMapEntries;
Ar << ShaderEntries;
Ar << PreloadEntries;
Ar << ShaderIndices;

check(ShaderHashes.Num() == ShaderEntries.Num());
check(ShaderMapHashes.Num() == ShaderMapEntries.Num());

if (Ar.IsLoading())
{
// ++[SHADER_PATCH][lipengzha]
auto ShaderHashInitialized = [](const FHashTable& HashTable)->bool
{
return HashTable.GetHashSize() || HashTable.GetIndexSize();
};
// ++[SHADER_PATCH][lipengzha]
{
const uint32 HashSize = FMath::Min<uint32>(0x10000, 1u << FMath::CeilLogTwo(ShaderMapHashes.Num()));

// ++[SHADER_PATCH][lipengzha]
if(!ShaderHashInitialized(ShaderMapHashTable))
{
ShaderMapHashTable.Initialize(HashSize, ShaderMapHashes.Num());
}
// ++[SHADER_PATCH][lipengzha]

for (int32 Index = 0; Index < ShaderMapHashes.Num(); ++Index)
{
const uint32 Key = GetTypeHash(ShaderMapHashes[Index]);
ShaderMapHashTable.Add(Key, Index);
}
}
{
const uint32 HashSize = FMath::Min<uint32>(0x10000, 1u << FMath::CeilLogTwo(ShaderHashes.Num()));

// ++[SHADER_PATCH][lipengzha]
if(!ShaderHashInitialized(ShaderHashTable))
{
ShaderHashTable.Initialize(HashSize, ShaderHashes.Num());
}
// ++[SHADER_PATCH][lipengzha]

for (int32 Index = 0; Index < ShaderHashes.Num(); ++Index)
{
const uint32 Key = GetTypeHash(ShaderHashes[Index]);
ShaderHashTable.Add(Key, Index);
}
}
}
}

这样可以统一 ShaderPatch 和 Runtime 的 HashTable 的 Initialize 流程。

而且,需要注意的是:生成出来的 ShaderPatch 的 ushaderbytecode 文件是与基础包内的文件名一致的,所以不能使用引擎启动时的默认挂载(会导致基础包内的 ushaderbytecode 文件无法被加载,从而 crash)。

应该在挂载之后自己处理 ShaderPatch 的 ushaderbytecode 文件的加载,使用以下函数加载:

1
2
3
4
bool UFlibPatchParserHelper::LoadShaderbytecode(const FString& LibraryName, const FString& LibraryDir)
{
return FShaderCodeLibrary::OpenLibrary(LibraryName, LibraryDir);
}

注意:ShaderPatch 的更新不直接支持 Patch 的迭代,如:1.0 Metadata + 1.1 的 ShaderPatch,并不能生成 1.2 的 ShaderPatch,必须要基于 1.1 的完整 Metadata 才可以,即每次 Patch 必须要基于上一次完整的 Metadate 数据(Project 和 Global 的 ushaderbytecode 文件),在工程管理上每次打包都需要把完整的 Metadata 收集起来。