In some previous articles, the issue of UE hot updates missing shaders and using default materials has been introduced, along with a Shader Patch solution. For details, please refer to the articles:
- UE Hot Update: Create Shader Patch
- [UE Hot Update: Questions & Answers#Hot Update Resources Not Working / Material Missing](https://imzlp.com/posts/16895/#Hot Update Resources Not Working-Material Missing).
The fundamental reason for the runtime loss of materials is that the shaders dependent on newly added or modified resources were not packaged, leading to reading failures at runtime. This article will introduce the strategy and pros and cons of shader updates, analyze the mechanisms within the engine, and provide an optimized solution that combines the advantages of Shader Patch and Inline Shader Code, which has been implemented in HotPatcher.
UE has two serialization strategies for shaders during cooking:
- Share Material Shader Code: This will serialize the required shaders for the project into a separate Shader Code Library file (
ushaderbytecode
ormetailib
), which can reduce package size and avoid shader redundancy, allowing shaders to be searched from shaderbytecode during read. - Inline Shader Code: This serialization method includes shaders in the material uasset, avoiding the management of Shader Code Library. Each material, after cooking, itself contains the required shaders, leading to redundancy within the entire package and increasing package size.
In the default project configuration of a new project, Project Settings
-Project
-Packaging
-Shader Material Shader Code
is set to off.
However, in order to reduce the package size, it is generally enabled during packaging, and the resulting package contains ushaderbytecode:
If we subsequently update the material without updating the shaders that have changed within the package, it will lead to the material loss issue mentioned earlier. Thus, material loss is determined by two prerequisites:
- Share Material Shader Code is enabled.
- The shader library
ushaderbytecode
is not updated after the material update.
Based on these two prerequisites, a shader update strategy can be formulated:
- Update the latest Shader Code Library every time an update is made.
- Implement inline Shader Code cooking for the updated materials.
However, these two strategies also lead to their respective issues:
Shader Code Library:
- The complete Shader Code Library is excessively large.
- Shader Patch requires the management of additional metadata files.
- Each patch must be based on the complete shaderbytecode.
Inline Shader Code:
- UE’s default Cook Commandlet does not provide a method to cook a single resource.
- Inline Shader Code increases package size.
Shader Patch
The process of performing Shader Patch in UE has been previously described in my article: UE Hot Update: Create Shader Patch .
Here, I will discuss the mechanisms for creating the Shader Code Library not mentioned in the previous article. In the material resource, there is an attribute bShareCode
marking whether the current shader exists in the Shader Library or is inline:
1 | bool FShaderMapBase::Serialize(FArchive& Ar, bool bInlineShaderResources, bool bLoadedByCookedMaterial, bool bInlineShaderCode) |
The implementation of FShaderLibraryCooker::IsShaderLibraryEnabled
(prior to UE4.26, it was FShaderCodeLibrary::IsEnabled
):
1 | bool FShaderCodeLibrary::IsEnabled() |
The creation of FShaderCodeLibraryImpl::Impl
takes place in the following two functions:
1 | // for Cooking |
This essentially divides into two phases: when cooking for storage, and when reading, it is essential to determine whether the currently dependent shader is stored in the shader library or inlined.
By default, these two functions will not be executed during the editor’s startup because, theoretically, one is required during packaging and the other during non-Editor runtime. They are respectively used during cooking as follows:
1 | void UCookOnTheFlyServer::InitShaderCodeLibrary(void) |
And in FEngineLoop::PreInitPreStartupScreen
(Non-Editor runtime):
Moreover, for situations where Share Shader Library is enabled, UE has also provided a mechanism (4.26+) to forcibly specify that certain resources’ shaders should be inline, configured using the following method:
1 | [ShaderCodeLibrary] |
During the execution of cooking, there is a check for ShouldInlineShaderCode
:
To realize forced inlining.
Inline Shader Code
In the introduction of the previous section, it has been mentioned whether shaders are serialized to the Shader Library during cooking based on the prerequisite: FShaderCodeLibrary::IsEnabled
(4.25-) or FShaderLibraryCooker::IsShaderLibraryEnabled
(4.26+). To achieve Inline Shader Code:
- Turn off
bShareMaterialShaderCode
in project settings for UE’s CookCommandlet. - Implement a custom cooking process that does not execute
FShaderLibraryCooker::InitForCooking
.
For the first method, every time you want to cook inline, you need to manually modify the project settings, which is inconvenient, so the second method is recommended. However, UE does not provide an interface for cooking a single resource. I have implemented a method to cook a single resource in HotPatcher, supporting selection of resources and directories within the editor, as well as cooking resources within patches:
It is also encapsulated in code:
1 | static bool CookPackage( |
By using this interface, you can perform cooking in the editor, and when the resource being cooked is a material, it will be serialized using Inline Shader Code.
Better Way
As mentioned earlier, using Shader Patch and Inline Shader Code each has its own strengths and weaknesses:
- Shader Patch requires complete cooking, doing patches on two complete pieces of shader code can be inefficient for large projects and incurs additional management costs.
- Inline Shader Code: where no additional files need to be managed, the downside is that shaders cannot be shared, resulting in redundancy.
Thus, I provide a plan that combines the strengths of both in HotPatcher:
- Keep the Share Shader Code Library enabled during cooking.
- Only gather all compiled shaders from the resources cooked in the current patch and generate a separate Shader Library.
This eliminates the need to manually execute Shader Patch and removes concerns about package size increases from using Inline Shader Code. It is a solution that leverages the advantages of both. Thanks to the process mechanisms of HotPatcher, it can collect all the shaders for the current platform’s cooked resources during the cooking phase (as each packaging platform may have multiple shader formats, like Android’s GLSL_ES
or Vulkan
).
Once all resources are done cooking, all shaders from this cook will be collected, generating a Shader Code Library to be directly packaged into pak. This implementation does not require new and old version patches for incremental shaders, thus, there is no need for complete cooking, and no need to manage shader code versions.
All functions in HotPatcher can be simply toggled on or off:
If bSharedShaderLibrary
is not enabled, then all cooked shaders in the current patch are inline shader codes. Conversely, the shaders will be shared, reducing shader redundancy.
There are also several other options:
bNativeShader
: Same as the project settingSharedMaterialNativeLibraries
. Once enabled, the iOS platform will compile to metallib.ShaderNameRule
: Naming for the Shader Library generated for the current patch, providing three modes: the current patch version, project name, or custom.ShaderLibMountPoint
specifies the path where the shader library is stored in the pak for loading during runtime.
Note: By default, the pak includes the
Global
and project name’s Shader Library, and care should be taken to avoid naming collisions.
After enabling bSharedShaderLibrary
in the plugin, both Windows and Android will generate ushaderbytecode and package it into pak:
If bNativeShader
is enabled for iOS, it will compile metallib and metalmap:
Due to iOS’s loading mechanism for Native Shader, metallib and metalmap cannot be packaged into pak. They must be loaded directly from disk, not through UFS.
For the Shader Library packaged for the patch, it must be called after mounting for use. I have similarly encapsulated a loading function in the plugin:
1 | bool UFlibPatchParserHelper::LoadShaderbytecode(const FString& LibraryName, const FString& LibraryDir); |
The parameters to be passed here are derived from the ShaderNameRule
. If it is the version number, pass the version number as a string. The second parameter is the Shader Library path in the pak, passing the value of ShaderLibMountPoint
.
If Shared Shader Library is enabled but only mounted without calling LoadShaderbytecode, it will still result in material loss effects.
Cook Shader Format
During the cooking phase, HotPatcher will use the Shader formats configured in project settings for each platform. Each platform can have multiple Shader Formats; we take the most commonly used Windows/Android/iOS platforms as examples.
Windows
When packing for Windows, it retrieves GEngineIni
‘s TargetRHIs
, which defaults to PCD3D_SM5
, and can also include ES3.1
:
1 | [/Script/WindowsTargetPlatform.WindowsTargetSettings] |
The engine’s code relevant to acquiring Windows Shader Formats can be found in: GenericWindowsTargetPlatform.h#L229
Android
Android is also configured in the engine (UE dropped support for ES2 in version 4.25 and later):
The engine’s code relevant to acquiring Android Shader Formats can be found at: AndroidTargetPlatform.cpp#L390
iOS
iOS uses Metal, and whether to compile as Native can be toggled within project settings and HotPatcher with the bNativeShader
option. Additionally, it should also depend on whether compiled on Mac or if the Windows environment has a Metal Compiler (4.26+).
Note: Accessing the path of Metal Toolchain in the engine for Windows is
C:\Program Files\Metal Developer Tools
. By default, it will be installed there; if it is located elsewhere, it needs to be modified underProject Settings-IOS-Build
inOverride location of Metal Toolchain
.
By enabling bNativeShader
in HotPatcher, it will compile metallic/metalmap in supported environments.
The relevant engine code for acquiring Shader Formats on the iOS platform can be found at: IOSTargetPlatform.cpp#L581
Native Metal Shader
As mentioned in previous notes, Apple has introduced a Metal Shader Compiler for Windows, which can compile Metal Native Shader on Windows: Windows Metal Shader Compiler for iOS. However, UE only provided engine-level support after version 4.26; before version 4.25, metal could only be packaged using a Mac, and using remote build on Windows would only allow for Text Shader, which is ushaderbytecode, performing lower than Native. During runtime, it generates the following log:
1 | LogMetal: Display: Loaded a text shader (will be slower to load) |
The engine’s code can be found at: Engine/Source/Runtime/Apple/MetalRHI/Private/Shaders/Types/Templates/MetalBaseShader.h
In an environment that supports compiling Native Metal (Mac/Win), HotPatcher also supports compiling Patch resource shaders in Native format.
Although there is an option to enable Native Shader during packaging in project settings, this switch does not affect the code at runtime after packaging. Regardless of whether the base package has Native enabled, all three shader update methods can be utilized:
- ushaderbytecode
- inline shader code
- Native (metallib/metalmap)
For the loading of OpenLibrary
, on the Metal platform, it will prioritize loading Native first, and only if that fails will it load ushaderbytecode.
- Attempt to load metalmap; if it fails,
FMetalDynamicRHI::RHICreateShaderLibrary
returns nullptr (Apple\MetalRHI\Private\MetalShaders.cpp
) - If loading the metalmap fails, then load ushaderbytecode.
1 | class FShaderLibraryInstance |
Thus, regardless of the type of shader library within the base package, the shader update method can be employed.
There is a necessary point to note when loading Native Metal Shader: the names of
metallib
/metalmap
files must be all lowercase, or else loading will fail. Native Shader Libraries packaged by HotPatcher have this processed.
I created a test environment where both Native and ushaderbytecode can be loaded properly:
End
This article has explored the advantages and disadvantages of various methods for hot-updating shaders in UE, and based on HotPatcher has implemented incremental shader updates without the need for patching, providing a perfect alternative to Shader Patch and Inline Shader Code solutions.
With this functionality, shaders that depend on a specific map or module can be contained within an independent package, allowing the shaders to be decoupled and downloaded as needed without existing fully within the base package.