UE uses Zlib
by default as the compression algorithm for packaging resources, but it is not the best choice in terms of compression ratio and decompression speed. The efficiency comparison of various compression algorithms can be seen from the Squash Compression Benchmark. I chose the Facebook open-source ZStandard to replace Zlib, as ZSTD provides a good compression ratio while also having decent decompression speed. This article will not only explain how to integrate a compression algorithm into UE but will also briefly introduce a modular organization method for some features in UE—ModularFeature
. Using this method makes it easy to replace certain functionalities, and the replacement compression algorithm in this article serves as a practical example.
I integrated ZSTD into UE by writing a plugin. The source code is available on GitHub: hxhb/ue-zstd, which supports Android, Windows, iOS, and macOS. Feel free to give it a star.
Introduction
In fact, the compression ratio of ZStandard is similar to that of zlib when considering compression speed (ZSTD’s compression level is around 10
), but ZSTD decompresses faster. ZSTD’s compression levels range from 1 to 22
, with the default set to 3
. In the plugin I developed, the default level is set to 10
, which can be specified by using the -zstdlevel=
parameter when starting the engine (if you compiled UnrealPak, you can also specify the -zstdlevel=
parameter in the project settings under Pacakge
- PakFileCompressionCommandlineOptions
).
ModularFeature
All ModularFeature implementations in UE must inherit from IModularFeature
. It has no members and serves simply as a generic type. The organization of ModularFeature
is managed by the IModularFeatures
class (with the default implementation being FModularFeatures
). This class provides methods to register, unregister, and retrieve instances of IModularFeature
by name. Its core functionality is to associate a Name with a set of implementations. For example, all compression algorithms in UE fall under the COMPRESSION_FORMAT_FEATURE_NAME
, which corresponds to an array where each element is an implementation of a compression algorithm; this allows us to specify which compression algorithm to use based on our requirements.
1 | // Runtime/Core/Private/Features/ModularFeatures.cpp |
As seen above, registering a ModularFeature
simply involves adding an element to a Map. Note that ModularFeaturesMap
is not a regular TMap
; it is a TMiltiMap
, allowing a key to correspond to multiple values. It is a static data member of FCompression
.
The specific process is:
- Register the
ModularFeature
we created withIModularFeatures
during the loading of UE’s Module; - When using the feature, we can search for all implementations of a certain
ModularFeature
based on the name of ourModularFeature
category; - We can then call the implemented functionalities of
ModularFeature
through our own defined interfaces.
Note that IModularFeatures::GetModularFeature
is implemented using templates, allowing for direct retrieval of the specific type of a given Feature category.
Specifying Compression Algorithms
First, let’s take a look at how to replace the compression algorithm used during packaging in UE:
Open Project Settings
and find Packing
- Pak File Compression Format(s)
:
It is an FString type where you can enter a string separated by commas. If multiple formats are specified, they will be prioritized in order, falling back to other formats in case of errors or unavailability (such as plugins not being enabled).
This string is passed to the UnrealPak
as the -compressionformats=
parameter.
1 | // Programs/AutomationTool/Scripts/CopyBuildToStagingDirectory.Automation.cs |
In the PakFileUtilities
module, the default compression algorithm is ZLib
.
1 | // Developer/PakFileUtilities/Private/PakFileUtilities.cpp |
If a different algorithm is specified, the actual compression algorithm will be retrieved via ICompressionFormat* FCompression::GetCompressionFormat(FName FormatName, bool bErrorOnFailure)
. Here’s its implementation:
1 | ICompressionFormat* FCompression::GetCompressionFormat(FName FormatName, bool bErrorOnFailure) |
It is seen that it retrieves all FeatureImplementation
from IModularFeatures::Get().GetModularFeatureImplementations<ICompressionFormat>
. If you want to add your own compression algorithm, you can write a plugin and include it, thus allowing it to be used by name.
As mentioned at the beginning, to add it, you need to register a Feature
using IModularFeature::RegisterModularFeature
:
1 | IModularFeatures::Get().RegisterModularFeature(COMPRESSION_FORMAT_FEATURE_NAME, ICompressionFormatPointer); |
Here, COMPRESSION_FORMAT_FEATURE_NAME
is a macro that corresponds to a string CompressionFormat
, marking a group of Feature
types. ICompressionFormatPointer
is the pointer to the encapsulation of the compression algorithm you wish to add, and that object needs to inherit from ICompressionFormat
.
1 | // Runtime/Core/Public/Misc/ICompressionFormat.h |
Additionally, in project settings, you can use PakFileCompressionCommandlineOptions
to control the parameters for the compression algorithm. This parameter is passed to UnrealPak
in CopyBuildToStagingDirectory.Automation.cs
. For now, I won’t elaborate on this; it will be discussed later.
Integrating ZSTD
As discussed earlier, integrating a compression algorithm requires implementing a class that inherits from ICompressionFormat
and registering it with IModularFeatures
. Taking ZSTD as an example, here’s how to add a compression algorithm.
First, let’s review the four interface functions provided in ICompressionFormat
and their semantic requirements:
1 | struct ICompressionFormat : public IModularFeature, public IModuleInterface |
Next, let’s proceed to integrate ZSTD. First, pull the code from facebook/zstd and extract it (you can copy everything from the Lib directory except for the dll
directory) into the Source/ThirdParty
folder of your plugin. Then add its path to PublicIncludePaths
in your plugin’s *.build.cs
.
You would need to modify ZSTD’s code because the compilation environment and warning levels in UE may not allow the code to compile correctly. Common operations involve ignoring certain warnings, but ZSTD has a specific scenario where its code containing XXHash
may conflict with existing definitions in LiveCoding
, leading to redefinition errors, so the XXHash
code in ZSTD has to be renamed.
Once the ZSTD code compiles successfully in UE, you can move on to creating and implementing the ZSTD ICompressionFormat
:
1 | struct FZstdCompressionFormat : public ICompressionFormat |
This merely derives from ICompressionFormat
and adds a static member Level
to track which compression level to use in ZSTD.
The remaining task is to identify the functions in ZSTD’s code that can implement the semantics of the ICompressionFormat
interface:
1 | ZSTDLIB_API size_t ZSTD_compress(void* dst, size_t dstCapacity, const void* src, size_t srcSize, int compressionLevel); |
By examining ZSTD’s code, you can see that these three functions together can implement all functionalities of ICompressionFormat
. The implementation is straightforward—all that is required is to forward function calls:
Note: The GetCompressionFormatName
function specifies the name of the compression feature, which I set to zstd
. This name will be used in project settings.
1 | FName FZstdCompressionFormat::GetCompressionFormatName() |
At this point, the integration of ZSTD is complete. The final step is to add this feature to IModularFeatures
, making it available for the engine to use.
As I created a plugin, I will write the registration logic in the module’s StartupModule
, and unregister it in ShutdownModule
.
1 |
|
Using the same registration method as before, I also added a command-line parameter when the engine starts, -ZstdLevel=
, which can be used to specify the compression level for ZSTD.
To use the ZSTD
algorithm for packaging Pak, you can use my HotPatcher to add the -compressionformats=zstd,zlib
parameter in UnrealPakOptions
:
To check the compression format used by UnrealPak
:
Note: Since the engine is not compiled, you cannot directly use
UnrealPak.exe
to decompress Pak files compressed withZSTD
.
How to Use
If you’re calling the ExecuteUnrealPak
function from the PakFileUtilities
module directly in the UE editor, you only need to enable this plugin in your project, similar to how I used HotPatcher.
However, when packaging directly in the editor (including using ProjectLauncher
), UE launches a separate process and does not load this plugin, which can cause issues. Therefore, I will also discuss the solution.
Runtime Usage
To use zstd
at runtime, you can retrieve the zstd
object registered in the engine and use its interfaces to call the ICompressionFormat
methods.
You can obtain it through FCompression
:
1 | ICompressionFormat* ZstdCompressionFormat = FCompression::GetCompressionFormat(TEXT("zstd"), true); |
Once you have it, you can call Compress
, Uncompress
, and other interfaces to perform compression and decompression.
Modifying UnrealPak
When we use File
- Package Project
- PLATFORM
in the editor, the process goes through several stages:
- Compiling the project and any dependent plugins.
- Cooking the resources in the project.
- Using
UnrealPak.exe
to package the cooked resources. - Copying the packaged results.
Since I integrated zstd
, it needs to be used during the resource cooking phase, where the Package
- PakFileCompressionFormat(s)
in project settings takes effect.
However, since the plugin I created is placed in the project, UnrealPak
as a separate program won’t load this plugin, which makes it a bit awkward and unusable directly. To resolve this, modifications should be made to UnrealPak
, ensuring it loads the libzstd
plugin and registers it with IModularFeature
on startup. Since UnrealPak
belongs to Programs
, you will need the source version of the engine for compilation.
The specific approach is as follows:
- Place the plugin in the engine’s
Engine\Plugins\Runtime
directory. - Run
GenerateProjectFiles.bat
from the engine directory. - Edit the
UnrealPak
code to add a dependency on thelibzstd
module and dynamically load the module in the main function:
1 | // Engine\Source\Programs\UnrealPak\Private\UnrealPak.cpp |
You can also see my modified UnrealPak
code here: UnrealPak_422Source.7z.
Then compile UnrealPak
in Development
mode. The resulting UnrealPak.exe
will now support zstd
compression/decompression functionality.
You can also place this UnrealPak.exe
in the installed version of the engine to use it.
Here’s my compiled version of UnrealPak
that supports ZSTD
(UE4.22.3): UnrealPak422_ZSTD.
Now you can specify zstd
in the project settings under Package
- PakFileCompressionFormat(s)
:
Passing Parameters to zstd
Remember the PakFileCompressionCommandlineOptions
in the project settings that I put aside earlier? This parameter can be passed as command-line arguments to UnrealPak.exe
. Any content filed here will be forwarded to UnrealPak.exe
. Our modified UnrealPak
will load the libzstd
module on startup, and we can analyze the command line input in the StartupModule
of the libzstd
module to control various compression parameters (like compression level):
1 |
|
This way, you can fill -zstdlevel=22
in the PakFileCompressionCommandlineOptions
to control the compression level that you want to use.
Note that
Package
-PakFileCompressionCommandlineOptions
is inactive by default. If you don’t add any other compression algorithms and use the method of obtaining parameters that I described above, any entries here will have no effect.
Final Thoughts
Using this method, you can integrate other compression algorithms on your own, such as lz4, etc. I will write about the compression ratio and performance analysis of ZSTD during packaging when I have more time.
UPDATE
On April 1, 2021, UE announced the integration of RAD’s Oodle compression algorithm suite into UE in both UE4.27 and UE5. While 4.27 hasn’t been released yet, code and link libraries have already been uploaded. I extracted Oodle and fixed some errors in earlier engine versions, allowing it to be used directly in older engine versions.
For detailed content, please refer to:
The usage method can be referenced in this article by specifying -compressionformats=Oodle
as the command-line argument, as well as -compresslevel=Leviathan
to designate the compression level. Testing shows that a Pak file compressed with zlib and measuring 295M was reduced to 282M using Oodle, while Oodle’s decompression speed surpasses that of zlib.