在之前的一些文章中,介绍了UE热更新丢失Shader使用默认材质的处理问题,以及Shader Patch的方案。
详情可见文章:

其实运行时丢失材质的根本原因是新添加或修改的资源所依赖的Shader没有被打包,运行时读取失败导致的错误。本篇文章来介绍一下Shader更新的策略和优缺点,以及对引擎内机制的分析,并提供一个结合Shader Patch与Inline Shader Code优点的优化方案,已经在HotPacther中实现。
UE在Cook时有两种Shader的序列化策略:
- Share Material Shader Code:会把项目所需的Shader单独的序列化为Shader Code Library文件(
ushaderbytecode或metailib),能够降低包体大小,避免Shader的冗余,读取shader时会从shaderbytecode中查找。 - Inline Shader Code:将Shader序列化至材质uasset,避免管理Shader Code Library,每个材质在Cook之后它本身就包含了所需的shader,整个包内具有冗余,会增加包体的大小。
 
在新建工程的默认项目配置中,Project Settings-Project-Packaging-Shadre Material Shader Code为关闭状态。

但是为了降低包体的大小,通常在打包时会开启,打出来的包就包含了ushaderbytecode:

但是,如果我们后续更新了材质,没有更新包内变动之后的shader,就会导致前文中介绍的材质丢失的问题。
所以材质丢失是有两个前提决定的:
- 开启了Share Material Shader Code
 - 更新材质后没有更新ushaderbytecode库
 
根据这两个前提,可以制定Shader更新的策略:
- 每次更新之后就更新最新的Shade Code Library
 - 对更新的材质实现inline Shader Code的cook
 
但是,这两个策略又引出了各自的问题:
Shader Code Library:
- 完整的Shader Code Library太过巨大
 - Shader Patch需要管理额外的Metadata文件
 - 每次Patch必须基于完整的shadebytecode
 
Inline Shader Code:
- UE默认Cook Commandlet没有提供Cook单个资源的方法
 - Inline Shader Code会增大包体
 
Shader Patch
在UE里进行Shader Patch的流程,我之前已经写了一篇文章:UE热更新:Create Shader Patch。

这里讲一些上篇文章中没提到的创建Shader code library机制的内容。在材质资源中有一个属性bShareCode标记当前的Shader是存在于Shader Library中还是Inline:
1  | bool FShaderMapBase::Serialize(FArchive& Ar, bool bInlineShaderResources, bool bLoadedByCookedMaterial, bool bInlineShaderCode)  | 
FShaderLibraryCooker::IsShaderLibraryEnabled的实现(在UE4.26之前是FShaderCodeLibrary::IsEnabled):
1  | bool FShaderCodeLibrary::IsEnabled()  | 
而FShaderCodeLibraryImpl::Impl的创建在以下两个函数中:
1  | // for Cooking  | 
其实是分了两个阶段:Cook存储时和读取时,都需要获取到当前资源依赖的shader是存储在shader libraray中还是inline了。
默认情况下,这两个函数在启动编辑器时都不会被执行,因为毕竟理论上他们一个是在打包时一个是在非Editor运行时才被需要,他们分别被用在Cook时:
1  | void UCookOnTheFlyServer::InitShaderCodeLibrary(void)  | 
以及FEngineLoop::PreInitPreStartupScreen中(Non-Editor运行时):
并且,对于开启了Share Shader Library的情况,UE也提供了一个机制(4.26+),可以强制制定某些资源的Shader是Inline的,使用一下方式配置:
1  | [ShaderCodeLibrary]  | 
在执行Cook时有ShouldInlineShaderCode的检测:
实现强制inline。
Inline Shader Code

在上一节的介绍中,已经提到了在Cook时是否将Shader序列化到Shader Library的前提条件:FShaderCodeLibrary::IsEnabled(4.25-)或FShaderLibraryCooker::IsShaderLibraryEnabled(4.26+),想要实现Inline Shader Code的方法:
- UE的CookCommandlet,在项目设置中关闭
bShareMaterialShaderCode - 自己实现Cook流程,不执行
FShaderLibraryCooker::InitForCooking 
对于第一种方式,每次想要Cook Inline的时候,都需要手动去修改项目设置,较为不便,所以推荐第二个方式。但是UE没有提供Cook单个资源的接口,我在HotPacther中实现了Cook单个资源的方式,支持编辑器中选中资源、目录,以及在Patch中对资源进行Cook:

在代码中也做了封装:
1  | static bool CookPackage(  | 
使用这个接口就能实现在编辑器中的Cook,当Cook资源是材质时,会使用Inline Shader Code的方式序列化。
Better Way
前面已经提到了,使用Shader Patch和Inline Shader Code的方式各有各的优缺点:
- Shader Patch需要完整Cook,对两份完整的Shader code做Patch,项目巨大时执行效率不高,并且有额外的管理成本。
 - Inline Shader Code:无需管理任何额外文件,缺点是无法共用shader,具有冗余。
 
所以,我为HotPacther提供一个结合两者优点的方案:
- Cook时依然开启Share Shader Code Library
 - 仅把当前Patch中Cook资源时所有编译的Shader收集起来,生成单独的Shader Library
 
这样不用再手动执行Shader Patch,也不用担心Inline Shader Code的方式导致包体增大的问题,是一种结合了两者优点的方案。
得益于HotPatcher的流程机制,能够在插件的Cook阶段中把当前平台所有Cook资源的shader收集起来(每个打包的平台可能会具有多种shader格式,如Android的GLSL_ES或Vulkan)。

在所有的资源都Cook完毕之后,采集到了本次Cook所有Shader,将其生成Shader Code Library并直接打包至pak中。是一种无需进行新旧版本patch的增量shader方案,所以,也无需完整的Cook,更无需管理Shader code的版本。
所有的功能在HotPatcher中只需简单地开关即可实现:

若不开启bSharedShaderLibrary,则当前Patch中的Cook的shader全部是inline shader code。反之Shader则是Shared的,降低Shader的冗余。
还有一些其他的选项:
bNativeShader:与项目设置中SharedMaterialNativeLibraries相同,开启之后IOS平台会编译成metallibShaderNameRule:为当前Patch生产的Shader Library的命名,提供了三种模式:当前Patch的版本号、项目名、自定义ShaderLibMountPoint则是将shader library打包至Pak中所存储的路径,在运行时加载Shader Library需要指定。
注意:打包时默认具有
Global和项目名的Shader Library,注意不要有重名。
在插件中开启bSharedShaderLibrary后,Windows和Android会生成ushaderbytecode,并打包至Pak中:


IOS如果开启bNativeShader,则会编译出metallib和metalmap:

由于IOS的Native Shader的加载机制,不能把metallib和metalmap打包到pak中,不能通过UFS,需要直接从磁盘加载。
为Patch打包出的Shader Library,需要在Mount之后调用加载才能使用,我在插件中同样有封装函数:
1  | bool UFlibPatchParserHelper::LoadShaderbytecode(const FString& LibraryName, const FString& LibraryDir);  | 
这里需要传递的参数,就是ShaderNameRule中的规则,如果是版本号,就传递版本号的字符串,第二个参数就是Shader Library在Pak中的路径,传递ShaderLibMountPoint值即可。
如果开启了Shared Shader Libraty,但是只进行了Mount,没有LoadShaderbytecode,同样会出现材质丢失的效果。
Cook Shader Format
HotPatcher在Cook阶段,会使用项目设置中配置的各个平台的Shader格式进行Cook,每个平台可以具有多种Shader Format,这里以最常用的Windows/Android/IOS三个平台为例。
Windows
Window打包时是获取GEngineIni下的TargetRHIs,默认是PCD3D_SM5,也可以添加ES3.1:
1  | [/Script/WindowsTargetPlatform.WindowsTargetSettings]  | 
引擎中Windows获取Shader Formats的相关代码:GenericWindowsTargetPlatform.h#L229
Android
Android是也是在引擎中配置的(UE在4.25及之后的版本上舍弃了ES2的支持):

引擎中Android获取Shader Formats的相关代码:AndroidTargetPlatform.cpp#L390
IOS
IOS则是Metal,但是否编译为Native,项目设置和HotPatcher中都有bNativeShader的选项,并且也要根据是否在Mac上编译或者Win上具有Metal Compiler(4.26+)。


注意:引擎中访问Windows Metal Toolchin的路径为
C:\Program Files\Metal Developer Tools,默认安装会在这个位置,若不在这个路径下,就需要在Project Settings-IOS-Build下修改Override location of Metal Toolchain。

在HotPatcher中勾上bNativeShader就会在支持的环境上编译出metallic/metalmap。
引擎中IOS平台获取Shader Formats的相关代码:IOSTargetPlatform.cpp#L581
Native Metal Shader
之前笔记中提到,苹果推出了Windows上的 Metal Shader Compiler,可以在Windows上编译出Metal Native Shader:Windows Metal Shader Compiler for IOS,但是UE在4.26之后才提供了引擎层面的支持,在4.25及之前的引擎版本中,metal只能通过Mac来打包,使用Windows远程构建就只能使用Text Shader,也就是ushaderbytecode,性能低于Native,在运行时有以下log:
1  | LogMetal: Display: Loaded a text shader (will be slower to load)  | 
引擎中的代码:Engine/Source/Runtime/Apple/MetalRHI/Private/Shaders/Types/Templates/MetalBaseShader.h
在支持Compile Native Metal的环境上(Mac/Win),HotPatcher也支持了对Patch中资源的Shader编译Native格式。
虽然项目设置中有打包时启用Native Shader的选项,但是这个开关并不会对打包后运行时的代码有任何影响。不管基础包是否开启了Native,在运行时三种shader的更新方式都可以:
- ushaderbytecode
 - inline shader code
 - Native(metallib/metalmap)
 
对于调用OpenLibrary的加载,在Metal平台会优先加载Native的,若加载失败才会加载ushaderbytecode。
- 尝试加载metalmap,若加载失败,则
FMetalDynamicRHI::RHICreateShaderLibrary返回nullptr (Apple\MetalRHI\Private\MetalShaders.cpp) - 加载metalmap失败,则加载ushaderbytecode
 
1  | class FShaderLibraryInstance  | 
所以,不管基础包内的shader library是何种类型,这Shader的更新方式都可以用。
加载Native Metal Shader有一个需要注意的问题,
metallib/metalmap文件名必须全小写,不然会加载失败。通过HotPatcher打包的Native Shader Library已做处理。
我创建了一个测试环境,不管是Native/ushaderbytecode,都能够正常加载:



End
本文研究了UE中热更Shader几种不同方式的优缺点,并基于HotPacther实现了无需Patch的Shader增量更新,可以完美地替代Shader Patch和Inline Shader Code的方案。
根据该功能,可以实现某个地图或者某种模块依赖的shader全在独立的包中,这样能够让shader也解耦,完全可以按需下载而不用完整的存在基础包中。