在 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 收集起来。