在 4.25 引擎版本中调用 FShaderCodeLibrary::CreatePatchLibrary 来创建 ShaderCode Patch 会触发 check 抛异常:
这是因为 FEditorShaderCodeArchive 的构造函数中调用了 ShaderHashTable 的 Initialize,并给了默认值0x1000:
1 | FEditorShaderCodeArchive(FName InFormat) |

导致在后续的流程中 (FSerializedShaderArchive::Serialize) 调用 Initialize 的时候 check 失败了(因为 HaseSize 已经有值了,并不是 0,对其再调用 Initialize 就触发了 check):
查了下 FEditorShaderCodeArchive 构造函数中调用 Initialize 的代码是在 4.25 之后的引擎版本才有的,所以影响到的之后 4.25+ 的版本。
代码对比:
- 4.24.3-release/Engine/Source/Runtime/RenderCore/Private/ShaderCodeLibrary.cpp#L922
- 4.25.0-release/Engine/Source/Runtime/RenderCore/Private/ShaderCodeLibrary.cpp#L801
解决方案:把 FSerializedShaderArchive::Serialize 中ShaderMapHashTable的 Initialize 和ShaderHashTable的 Initialize 在 Editor 下注释掉,因为 FEditorShaderCodeArchive 的代码只在 Editor 下有效,并且是只在生成 ShaderPatch 时有用。
这就造成了以下几个问题:
FEditorShaderCodeArchive的构造只有 Eidotor 并且 ShaderPatch 是才有用,也就意味着这里写的ShaderMapHashTable的Initialize和ShaderHashTable的Initialize只有在创建 ShaderPatch 时才会执行- 在打基础包时执行 Cook 会编译 shader,但是不会执行 FEditorShaderCodeArchive 的构造,
ShaderMapHashTable的Initialize和ShaderHashTable的Initialize也就不会执行,就需要在使用的地方来调用它们的初始化
这也是 UE 中没有管理好这两个状态的地方:在 FEditorShaderCodeArchive 和FSerializedShaderArchive::Serialize中都做了 Initialize 的操作,在打基础包时造成了 ShaderMapHashTable 和ShaderHashTable的 Initialize 已经被 FEditorShaderCodeArchive 初始化的情况下又被 FSerializedShaderArchive::Serialize 执行了一遍,导致 Crash,但是我们又不能粗暴地把任何一处的初始化操作去掉,只能通过检测 ShaderMapHashTable 和ShaderHashTable的 Initialize 是否已经被执行,来选择性的跳过。
阅读代码可以知道 ShaderMapHashTable 和ShaderHashTable的 Initialize 只应该执行一次,并且初始化之后 HashSize 和 IndexSize 应该具有非 0 值:
1 | FORCEINLINE void FHashTable::Initialize(uint32 InHashSize, uint32 InIndexSize) |
在 Initialize 时会检测当前的 HashSize 和IndexSize是否为 0,并在之后进行赋值。所以,我们只要获取 FHashTable 的HashSize和 IndexSize 检测它们是否为 0 即可判断当前的 HashTable 对象是否已经被 Initialize 过,但是,UE 里的 FHashTable 里这两个成员都是 protected 的,只能修改引擎来实现了:
添加获取 FHashTable 的HashSize和 IndexSize 属性的成员函数:
1 | class FHashTable |
然后在 FSerializedShaderArchive::Serialize 进行检测,如果已被初始化则跳过 Initialize 逻辑:
1 | void FSerializedShaderArchive::Serialize(FArchive& Ar) |
这样可以统一 ShaderPatch 和 Runtime 的 HashTable 的 Initialize 流程。
而且,需要注意的是:生成出来的 ShaderPatch 的 ushaderbytecode 文件是与基础包内的文件名一致的,所以不能使用引擎启动时的默认挂载(会导致基础包内的 ushaderbytecode 文件无法被加载,从而 crash)。

应该在挂载之后自己处理 ShaderPatch 的 ushaderbytecode 文件的加载,使用以下函数加载:
1 | bool UFlibPatchParserHelper::LoadShaderbytecode(const FString& LibraryName, const FString& LibraryDir) |
注意:ShaderPatch 的更新不直接支持 Patch 的迭代,如:1.0 Metadata + 1.1 的 ShaderPatch,并不能生成 1.2 的 ShaderPatch,必须要基于 1.1 的完整 Metadata 才可以,即每次 Patch 必须要基于上一次完整的 Metadate 数据(Project 和 Global 的 ushaderbytecode 文件),在工程管理上每次打包都需要把完整的 Metadata 收集起来。