// Georgy Treshchev 2024. #include "ArchiverRaw/RuntimeArchiverRaw.h" #include "RuntimeArchiverDefines.h" #include "Async/Async.h" #include "Misc/EngineVersionComparison.h" #include "Misc/Compression.h" #if UE_VERSION_NEWER_THAN(5, 0, 0) #include "Compression/OodleDataCompressionUtil.h" #endif namespace { /** * Converting a raw format enum to a name used in the Unreal Engine compressor */ EName ToName(ERuntimeArchiverRawFormat Format) { switch (Format) { case ERuntimeArchiverRawFormat::Oodle: { #if UE_VERSION_NEWER_THAN(5, 0, 0) return NAME_Oodle; #else UE_LOG(LogRuntimeArchiver, Error, TEXT("Oodle format is not supported in %s %d"), TEXT(EPIC_PRODUCT_NAME), ENGINE_MAJOR_VERSION); return NAME_None; #endif } case ERuntimeArchiverRawFormat::GZip: { return NAME_Gzip; } case ERuntimeArchiverRawFormat::LZ4: { return NAME_LZ4; } default: return NAME_None; } } /** * Check if the specified format is valid */ bool IsFormatValid(const FName& FormatName) { // There is a bug in the engine where the LZ4 format cannot be found, even though it exists if (FormatName == NAME_LZ4) { return true; } if (!FCompression::IsFormatValid(FormatName)) { return false; } return true; } } #if UE_VERSION_NEWER_THAN(5, 0, 0) /** * Convert plugin-specific archiver data to Oodle compressor-specific data */ namespace OodleConversation { FOodleDataCompression::ECompressor GetCompressor(ERuntimeArchiverCompressionLevel CompressionLevel) { switch (CompressionLevel) { case ERuntimeArchiverCompressionLevel::Compression0: return FOodleDataCompression::ECompressor::Selkie; case ERuntimeArchiverCompressionLevel::Compression1: return FOodleDataCompression::ECompressor::Selkie; case ERuntimeArchiverCompressionLevel::Compression2: return FOodleDataCompression::ECompressor::Mermaid; case ERuntimeArchiverCompressionLevel::Compression3: return FOodleDataCompression::ECompressor::Mermaid; case ERuntimeArchiverCompressionLevel::Compression4: return FOodleDataCompression::ECompressor::Mermaid; case ERuntimeArchiverCompressionLevel::Compression5: return FOodleDataCompression::ECompressor::Kraken; case ERuntimeArchiverCompressionLevel::Compression6: return FOodleDataCompression::ECompressor::Kraken; case ERuntimeArchiverCompressionLevel::Compression7: return FOodleDataCompression::ECompressor::Kraken; case ERuntimeArchiverCompressionLevel::Compression8: return FOodleDataCompression::ECompressor::Leviathan; case ERuntimeArchiverCompressionLevel::Compression9: return FOodleDataCompression::ECompressor::Leviathan; case ERuntimeArchiverCompressionLevel::Compression10: return FOodleDataCompression::ECompressor::Leviathan; default: return FOodleDataCompression::ECompressor::NotSet; } } FOodleDataCompression::ECompressionLevel GetCompressionLevel(ERuntimeArchiverCompressionLevel CompressionLevel) { switch (CompressionLevel) { case ERuntimeArchiverCompressionLevel::Compression0: return FOodleDataCompression::ECompressionLevel::HyperFast1; case ERuntimeArchiverCompressionLevel::Compression1: return FOodleDataCompression::ECompressionLevel::SuperFast; case ERuntimeArchiverCompressionLevel::Compression2: return FOodleDataCompression::ECompressionLevel::VeryFast; case ERuntimeArchiverCompressionLevel::Compression3: return FOodleDataCompression::ECompressionLevel::Fast; case ERuntimeArchiverCompressionLevel::Compression4: return FOodleDataCompression::ECompressionLevel::Normal; case ERuntimeArchiverCompressionLevel::Compression5: return FOodleDataCompression::ECompressionLevel::Fast; case ERuntimeArchiverCompressionLevel::Compression6: return FOodleDataCompression::ECompressionLevel::Normal; case ERuntimeArchiverCompressionLevel::Compression7: return FOodleDataCompression::ECompressionLevel::Optimal1; case ERuntimeArchiverCompressionLevel::Compression8: return FOodleDataCompression::ECompressionLevel::Optimal2; case ERuntimeArchiverCompressionLevel::Compression9: return FOodleDataCompression::ECompressionLevel::Optimal3; case ERuntimeArchiverCompressionLevel::Compression10: return FOodleDataCompression::ECompressionLevel::Optimal4; default: return FOodleDataCompression::ECompressionLevel::None; } } } #endif void URuntimeArchiverRaw::CompressRawDataAsync(ERuntimeArchiverRawFormat RawFormat, ERuntimeArchiverCompressionLevel CompressionLevel, TArray UncompressedData, const FRuntimeArchiverRawMemoryResult& OnResult) { CompressRawDataAsync(RawFormat, CompressionLevel, TArray64(MoveTemp(UncompressedData)), FRuntimeArchiverRawMemoryResultNative::CreateLambda([OnResult](TArray64 CompressedData64) { if (CompressedData64.Num() > TNumericLimits::SizeType>::Max()) { UE_LOG(LogRuntimeArchiver, Error, TEXT("Array with int32 size (max length: %d) cannot fit int64 size data (retrieved length: %lld)\nA standard byte array can hold a maximum of 2 GB of data"), TNumericLimits::SizeType>::Max(), CompressedData64.Num()); OnResult.ExecuteIfBound(TArray()); return; } OnResult.ExecuteIfBound(TArray(MoveTemp(CompressedData64))); })); } void URuntimeArchiverRaw::CompressRawDataAsync(ERuntimeArchiverRawFormat RawFormat, ERuntimeArchiverCompressionLevel CompressionLevel, TArray64 UncompressedData, const FRuntimeArchiverRawMemoryResultNative& OnResult) { AsyncTask(ENamedThreads::AnyBackgroundHiPriTask, [RawFormat, CompressionLevel, UncompressedData = MoveTemp(UncompressedData), OnResult]() mutable { TArray64 CompressedData; CompressRawData(RawFormat, CompressionLevel, MoveTemp(UncompressedData), CompressedData); AsyncTask(ENamedThreads::GameThread, [OnResult, CompressedData = MoveTemp(CompressedData)]() mutable { OnResult.ExecuteIfBound(MoveTemp(CompressedData)); }); }); } bool URuntimeArchiverRaw::CompressRawData(ERuntimeArchiverRawFormat RawFormat, ERuntimeArchiverCompressionLevel CompressionLevel, const TArray64& UncompressedData, TArray64& CompressedData) { const FName FormatName = ToName(RawFormat); if (!IsFormatValid(FormatName)) { UE_LOG(LogRuntimeArchiver, Error, TEXT("The specified format '%s' is not valid"), *FormatName.ToString()); return false; } #if UE_VERSION_NEWER_THAN(5, 0, 0) if (FormatName.IsEqual(NAME_Oodle)) { if (!FOodleCompressedArray::CompressTArray64(CompressedData, UncompressedData, OodleConversation::GetCompressor(CompressionLevel), OodleConversation::GetCompressionLevel(CompressionLevel))) { UE_LOG(LogRuntimeArchiver, Error, TEXT("Unable to compress data for '%s' format"), *FormatName.ToString()); return false; } return true; } #endif int32 CompressedSize = GuessCompressedSize(RawFormat, UncompressedData); if (CompressedSize <= 0) { UE_LOG(LogRuntimeArchiver, Error, TEXT("Unable to get compressed data size for '%s' format"), *FormatName.ToString()); return false; } TArray64 TempCompressedData; TempCompressedData.SetNumUninitialized(CompressedSize); if (!FCompression::CompressMemory(FormatName, TempCompressedData.GetData(), CompressedSize, UncompressedData.GetData(), UncompressedData.Num())) { UE_LOG(LogRuntimeArchiver, Error, TEXT("Unable to compress data for '%s' format"), *FormatName.ToString()); return false; } TempCompressedData.SetNum(CompressedSize, true); CompressedData = MoveTemp(TempCompressedData); return true; } void URuntimeArchiverRaw::UncompressRawDataAsync(ERuntimeArchiverRawFormat RawFormat, TArray CompressedData, const FRuntimeArchiverRawMemoryResult& OnResult) { UncompressRawDataAsync(RawFormat, TArray64(MoveTemp(CompressedData)), FRuntimeArchiverRawMemoryResultNative::CreateLambda([OnResult](TArray64 UncompressedData64) { if (UncompressedData64.Num() > TNumericLimits::SizeType>::Max()) { UE_LOG(LogRuntimeArchiver, Error, TEXT("Array with int32 size (max length: %d) cannot fit int64 size data (retrieved length: %lld)\nA standard byte array can hold a maximum of 2 GB of data"), TNumericLimits::SizeType>::Max(), UncompressedData64.Num()); OnResult.ExecuteIfBound(TArray()); return; } OnResult.ExecuteIfBound(TArray(MoveTemp(UncompressedData64))); })); } void URuntimeArchiverRaw::UncompressRawDataAsync(ERuntimeArchiverRawFormat RawFormat, TArray64 CompressedData, const FRuntimeArchiverRawMemoryResultNative& OnResult) { AsyncTask(ENamedThreads::AnyBackgroundHiPriTask, [RawFormat, CompressedData = MoveTemp(CompressedData), OnResult]() mutable { TArray64 UncompressedData; UncompressRawData(RawFormat, CompressedData = MoveTemp(CompressedData), UncompressedData); AsyncTask(ENamedThreads::GameThread, [OnResult, UncompressedData = MoveTemp(UncompressedData)]() mutable { OnResult.ExecuteIfBound(MoveTemp(UncompressedData)); }); }); } bool URuntimeArchiverRaw::UncompressRawData(ERuntimeArchiverRawFormat RawFormat, TArray64 CompressedData, TArray64& UncompressedData) { const FName FormatName = ToName(RawFormat); if (!IsFormatValid(FormatName)) { UE_LOG(LogRuntimeArchiver, Error, TEXT("The specified format '%s' is not valid"), *FormatName.ToString()); return false; } #if UE_VERSION_NEWER_THAN(5, 0, 0) if (FormatName.IsEqual(NAME_Oodle)) { if (!FOodleCompressedArray::DecompressToTArray64(UncompressedData, CompressedData)) { UE_LOG(LogRuntimeArchiver, Error, TEXT("Unable to uncompress data for '%s' format"), *FormatName.ToString()); return false; } return true; } #endif const int64 UncompressedSize = GuessUncompressedSize(RawFormat, CompressedData); if (UncompressedSize <= 0) { UE_LOG(LogRuntimeArchiver, Error, TEXT("Unable to get compressed data size for '%s' format"), *FormatName.ToString()); return false; } TArray64 TempUncompressedData; TempUncompressedData.SetNumUninitialized(UncompressedSize); if (!FCompression::UncompressMemory(FormatName, TempUncompressedData.GetData(), UncompressedSize, CompressedData.GetData(), CompressedData.Num())) { UE_LOG(LogRuntimeArchiver, Error, TEXT("Unable to uncompress data for '%s' format"), *FormatName.ToString()); return false; } UncompressedData = MoveTemp(TempUncompressedData); return true; } int64 URuntimeArchiverRaw::GuessCompressedSize(ERuntimeArchiverRawFormat RawFormat, const TArray64& UncompressedData) { const FName FormatName = ToName(RawFormat); if (!IsFormatValid(FormatName)) { UE_LOG(LogRuntimeArchiver, Error, TEXT("The specified format '%s' is not valid"), *FormatName.ToString()); return false; } #if UE_VERSION_NEWER_THAN(5, 0, 0) return FCompression::GetMaximumCompressedSize(FormatName, UncompressedData.Num()); #else return FCompression::CompressMemoryBound(FormatName, UncompressedData.Num()); #endif } int64 URuntimeArchiverRaw::GuessUncompressedSize(ERuntimeArchiverRawFormat RawFormat, const TArray64& CompressedData) { if (CompressedData.Num() <= 0) { return 0; } switch (RawFormat) { case ERuntimeArchiverRawFormat::GZip: { const uint8_t* DataPtr = CompressedData.GetData() + CompressedData.Num() - 4; return (static_cast(DataPtr[0]) << 0) + (static_cast(DataPtr[1]) << 8) + (static_cast(DataPtr[2]) << 16) + (static_cast(DataPtr[3]) << 24); } case ERuntimeArchiverRawFormat::LZ4: { return static_cast(CompressedData.Num() * 255); } default: { UE_LOG(LogRuntimeArchiver, Error, TEXT("Unable to determine the uncompressed size of the format '%s'"), *UEnum::GetValueAsString(RawFormat)); } } return 0; }