集成第三方压缩算法

使用 ModularFeature 集成第三方压缩算法,以 ZSTD 为例。

在 UE 中如果想要自己添加一个压缩算法的实现则需要自己实现一个继承自 ICompressionFormat 的类,然后注册给IModulelarFeatures,那么以 ZSTD 为例,来示范一下怎么真实地添加一个压缩算法。

首先还是要来介绍一下 ICompressionFormat 中提供的四个接口函数的语义要求:

1
2
3
4
5
6
7
8
9
10
11
struct ICompressionFormat : public IModularFeature, public IModuleInterface
{
// 获取当前实现的压缩算法的名字
virtual FName GetCompressionFormatName() = 0;
// 执行压缩
virtual bool Compress(void* CompressedBuffer, int32& CompressedSize, const void* UncompressedBuffer, int32 UncompressedSize, int32 CompressionData) = 0;
// 执行解压
virtual bool Uncompress(void* UncompressedBuffer, int32& UncompressedSize, const void* CompressedBuffer, int32 CompressedSize, int32 CompressionData) = 0;
// 获取压缩算法可以执行压缩的数据大小
virtual int32 GetCompressedBufferSize(int32 UncompressedSize, int32 CompressionData) = 0;
};

然后开干,集成 ZSTD 首先需要去 facebook/zstd 上把代码拉取下来,然后把代码提取出来(Lib 目录下除了 dll 目录外都可以拷贝过来),放到插件的 Souce/ThirdParty 下,并在插件的 *.build.cs 中将其添加至 PublicIncludePaths 中。

首先先要对 ZSTD 的代码进行修改,因为 UE 的编译环境和警告等级的关系是没办法把代码拷过来就可以直接编译过的,常见的操作为忽略某些警告,但是 ZSTD 有一点特别的地方在于它里面具有 XXHash 的代码在编译时会与 LiveCoding 中的有冲突会有重定义错误,所以需要对 ZSTD 代码中的 XXHash 进行改名。

当把 ZSTD 的代码在 UE 中能够顺利的编译过的时候可以进入下一个流程,创建并实现 ZSTDICompressionFormat实现。

1
2
3
4
5
6
7
8
9
struct FZstdCompressionFormat : public ICompressionFormat
{
virtual FName GetCompressionFormatName()override;
virtual bool Compress(void* CompressedBuffer, int32& CompressedSize, const void* UncompressedBuffer, int32 UncompressedSize, int32 CompressionData)override;
virtual bool Uncompress(void* UncompressedBuffer, int32& UncompressedSize, const void* CompressedBuffer, int32 CompressedSize, int32 CompressionData)override;
virtual int32 GetCompressedBufferSize(int32 UncompressedSize, int32 CompressionData)override;

static int32 Level;
};

只是继承了 ICompressionFormat 然后添加了一个 Level 的 static 数据成员,用于记录在 ZSTD 中使用哪个压缩级别。

剩下的事情就是从 ZSTD 的代码里找到能够实现 ICompressionFormat 接口语义的函数:

1
2
3
ZSTDLIB_API size_t ZSTD_compress(void* dst, size_t dstCapacity,const void* src, size_t srcSize,int compressionLevel);
ZSTDLIB_API size_t ZSTD_decompress(void* dst, size_t dstCapacity,const void* src, size_t compressedSize);
ZSTDLIB_API size_t ZSTD_compressBound(size_t srcSize); /*!< maximum compressed size in worst case single-pass scenario */

通过查看 ZSTD 的代码可以发现这三个函数组合起来就可以实现 ICompressionFormat 中的所有功能,比较简单,都是转发调用:
PS: 通过实现 GetCompressionFormatName 来指定该压缩 Feature 的名字,我这里给了zstd,在项目设置里指定的时候就要使用这个名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
FName FZstdCompressionFormat::GetCompressionFormatName()
{
return TEXT("zstd");
}

bool FZstdCompressionFormat::Compress(void* CompressedBuffer, int32& CompressedSize, const void* UncompressedBuffer, int32 UncompressedSize, int32 CompressionData)
{
UE_LOG(LogTemp, Log, TEXT("FZstdCompressionFormat::Compress level is %d"), FZstdCompressionFormat::Level);
int32 Result = ZSTD_compress(CompressedBuffer, CompressedSize, UncompressedBuffer, UncompressedSize, FZstdCompressionFormat::Level);
if (Result > 0)
{
if (Result > GetCompressedBufferSize(UncompressedSize, CompressionData))
{
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("%d < %d"), Result, GetCompressedBufferSize(UncompressedSize, CompressionData));
// we cannot safely go over the BufferSize needed!
return false;
}
CompressedSize = Result;
return true;
}
return false;
}
bool FZstdCompressionFormat::Uncompress(void* UncompressedBuffer, int32& UncompressedSize, const void* CompressedBuffer, int32 CompressedSize, int32 CompressionData)
{
int32 Result = ZSTD_decompress(UncompressedBuffer, UncompressedSize, CompressedBuffer, CompressedSize);
if (Result > 0)
{
UncompressedSize = Result;
return true;
}
return false;
}
int32 FZstdCompressionFormat::GetCompressedBufferSize(int32 UncompressedSize, int32 CompressionData)
{
return ZSTD_compressBound(UncompressedSize);
}

到这里 ZSTD 的的集成工作就完毕了,只剩下最后一步,那就是把这个 Feature 添加到 IModularFeatures 中,可以供引擎使用。

因为我是创建了一个插件,所以可以把注册的逻辑写到模块的 StartupModule 中,反之卸载模块时取消注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define ZSTD_LEVEL_OPTION_STRING TEXT("-ZstdLevel=")
void FlibzstdModule::StartupModule()
{
FString CommandLine = FCommandLine::Get();
if (CommandLine.Contains(ZSTD_LEVEL_OPTION_STRING, ESearchCase::IgnoreCase))
{
int32 level;
FParse::Value(FCommandLine::Get(), *FString(ZSTD_LEVEL_OPTION_STRING).ToLower(), level);
FZstdCompressionFormat::Level = FMath::Clamp(level, ZSTD_minCLevel(),ZSTD_maxCLevel());
}

ZstdCompressionFormat = new FZstdCompressionFormat();
IModularFeatures::Get().RegisterModularFeature(COMPRESSION_FORMAT_FEATURE_NAME, ZstdCompressionFormat);

}

void FlibzstdModule::ShutdownModule()
{
IModularFeatures::Get().UnregisterModularFeature(COMPRESSION_FORMAT_FEATURE_NAME, ZstdCompressionFormat);
delete ZstdCompressionFormat;

}

与前面讲的注册方法一致,我这里还添加了一个引擎启动时的命令行参数 -ZstdLevel= 可以用来传递使用 ZSTD 进行压缩的的压缩等级。

打包 Pak 使用 ZSTD 算法,使用我的 HotPatcher 可以在 UnrealPakOptions 中添加 -compressionformats=zstd,zlib 参数:

使用 UnrealPak 检测压缩格式:

注意,因为没有编译引擎,所以是不能直接通过 UnrealPak.exe 来解压使用 ZSTD 压缩的 Pak 的。