1654 lines
49 KiB
C++
1654 lines
49 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "PakFileCacheStore.h"
|
|
|
|
#include "Algo/Accumulate.h"
|
|
#include "Algo/StableSort.h"
|
|
#include "Algo/Transform.h"
|
|
#include "Compression/OodleDataCompression.h"
|
|
#include "DerivedDataBackendInterface.h"
|
|
#include "DerivedDataCachePrivate.h"
|
|
#include "DerivedDataCacheRecord.h"
|
|
#include "DerivedDataCacheUsageStats.h"
|
|
#include "DerivedDataChunk.h"
|
|
#include "DerivedDataRequestOwner.h"
|
|
#include "DerivedDataValue.h"
|
|
#include "HAL/CriticalSection.h"
|
|
#include "HAL/FileManager.h"
|
|
#include "HAL/PlatformFile.h"
|
|
#include "HAL/PlatformFileManager.h"
|
|
#include "HashingArchiveProxy.h"
|
|
#include "Misc/Compression.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Misc/PathViews.h"
|
|
#include "Misc/ScopeExit.h"
|
|
#include "Misc/ScopeRWLock.h"
|
|
#include "ProfilingDebugging/CookStats.h"
|
|
#include "Serialization/CompactBinary.h"
|
|
#include "Serialization/CompactBinaryPackage.h"
|
|
#include "Serialization/CompactBinaryValidation.h"
|
|
#include "Serialization/CompactBinaryWriter.h"
|
|
#include "Serialization/MemoryReader.h"
|
|
#include "Serialization/MemoryWriter.h"
|
|
#include "Templates/Greater.h"
|
|
#include "Templates/UniquePtr.h"
|
|
|
|
namespace UE::DerivedData
|
|
{
|
|
|
|
/**
|
|
* A simple thread safe, pak file based backend.
|
|
*/
|
|
class FPakFileCacheStore : public IPakFileCacheStore
|
|
{
|
|
public:
|
|
FPakFileCacheStore(const TCHAR* InName, const TCHAR* InCachePath, bool bInWriting, ICacheStoreOwner* InOwner = nullptr);
|
|
~FPakFileCacheStore();
|
|
|
|
void Close() final;
|
|
|
|
bool IsWritable() const final { return bWriting && !bClosed; }
|
|
|
|
bool CachedDataProbablyExists(const TCHAR* CacheKey);
|
|
bool GetCachedData(const TCHAR* CacheKey, TArray<uint8>& OutData);
|
|
void PutCachedData(const TCHAR* CacheKey, TArrayView<const uint8> InData, bool bPutEvenIfExists);
|
|
|
|
/**
|
|
* Save the cache to disk
|
|
* @return true if file was saved successfully
|
|
*/
|
|
bool SaveCache() final;
|
|
|
|
/**
|
|
* Load the cache to disk * @param Filename Filename to load
|
|
* @return true if file was loaded successfully
|
|
*/
|
|
bool LoadCache(const TCHAR* InFilename) final;
|
|
|
|
/**
|
|
* Merges another cache file into this one.
|
|
* @return true on success
|
|
*/
|
|
void MergeCache(IPakFileCacheStore* OtherPak) final;
|
|
|
|
const FString& GetFilename() const final
|
|
{
|
|
return CachePath;
|
|
}
|
|
|
|
// ICacheStore
|
|
|
|
void Put(
|
|
TConstArrayView<FCachePutRequest> Requests,
|
|
IRequestOwner& Owner,
|
|
FOnCachePutComplete&& OnComplete) override;
|
|
|
|
void Get(
|
|
TConstArrayView<FCacheGetRequest> Requests,
|
|
IRequestOwner& Owner,
|
|
FOnCacheGetComplete&& OnComplete) final;
|
|
|
|
void PutValue(
|
|
TConstArrayView<FCachePutValueRequest> Requests,
|
|
IRequestOwner& Owner,
|
|
FOnCachePutValueComplete&& OnComplete) override;
|
|
|
|
void GetValue(
|
|
TConstArrayView<FCacheGetValueRequest> Requests,
|
|
IRequestOwner& Owner,
|
|
FOnCacheGetValueComplete&& OnComplete) final;
|
|
|
|
void GetChunks(
|
|
TConstArrayView<FCacheGetChunkRequest> Requests,
|
|
IRequestOwner& Owner,
|
|
FOnCacheGetChunkComplete&& OnComplete) final;
|
|
|
|
// ILegacyCacheStore
|
|
|
|
void LegacyStats(FDerivedDataCacheStatsNode& OutNode) final;
|
|
bool LegacyDebugOptions(FBackendDebugOptions& Options) final;
|
|
|
|
private:
|
|
[[nodiscard]] bool PutCacheRecord(FStringView Name, const FCacheRecord& Record, const FCacheRecordPolicy& Policy, FRequestStats& Stats);
|
|
|
|
[[nodiscard]] FOptionalCacheRecord GetCacheRecordOnly(
|
|
FStringView Name,
|
|
const FCacheKey& Key,
|
|
const FCacheRecordPolicy& Policy,
|
|
FRequestStats& Stats);
|
|
[[nodiscard]] FOptionalCacheRecord GetCacheRecord(
|
|
FStringView Name,
|
|
const FCacheKey& Key,
|
|
const FCacheRecordPolicy& Policy,
|
|
EStatus& OutStatus,
|
|
FRequestStats& Stats);
|
|
|
|
[[nodiscard]] bool PutCacheValue(FStringView Name, const FCacheKey& Key, const FValue& Value, ECachePolicy Policy, FRequestStats& Stats);
|
|
|
|
[[nodiscard]] bool GetCacheValueOnly(FStringView Name, const FCacheKey& Key, ECachePolicy Policy, FValue& OutValue, FRequestStats& Stats);
|
|
[[nodiscard]] bool GetCacheValue(FStringView Name, const FCacheKey& Key, ECachePolicy Policy, FValue& OutValue, FRequestStats& Stats);
|
|
|
|
[[nodiscard]] bool PutCacheContent(FStringView Name, const FCompressedBuffer& Content, FRequestStats& Stats);
|
|
|
|
[[nodiscard]] bool GetCacheContentExists(const FCacheKey& Key, const FIoHash& RawHash, FRequestStats& Stats);
|
|
[[nodiscard]] bool GetCacheContent(
|
|
FStringView Name,
|
|
const FCacheKey& Key,
|
|
const FValueId& Id,
|
|
const FValue& Value,
|
|
ECachePolicy Policy,
|
|
FValue& OutValue,
|
|
FRequestStats& Stats);
|
|
void GetCacheContent(
|
|
const FStringView Name,
|
|
const FCacheKey& Key,
|
|
const FValueId& Id,
|
|
const FValue& Value,
|
|
const ECachePolicy Policy,
|
|
FCompressedBufferReader& Reader,
|
|
TUniquePtr<FArchive>& OutArchive,
|
|
FRequestStats& Stats);
|
|
|
|
[[nodiscard]] bool SaveFile(FStringView Path, FStringView DebugName, FRequestStats& Stats, TFunctionRef<void (FArchive&)> WriteFunction);
|
|
[[nodiscard]] FSharedBuffer LoadFile(FStringView Path, FStringView DebugName, FRequestStats& Stats);
|
|
[[nodiscard]] TUniquePtr<FArchive> OpenFile(FStringBuilderBase& Path, const FStringView DebugName, FRequestStats& Stats);
|
|
[[nodiscard]] bool FileExists(FStringView Path, FRequestStats& Stats);
|
|
|
|
private:
|
|
FDerivedDataCacheUsageStats UsageStats;
|
|
FBackendDebugOptions DebugOptions;
|
|
|
|
struct FCacheValue
|
|
{
|
|
int64 Offset;
|
|
int64 Size;
|
|
uint32 Crc;
|
|
FCacheValue(int64 InOffset, int64 InSize, uint32 InCrc)
|
|
: Offset(InOffset)
|
|
, Size(InSize)
|
|
, Crc(InCrc)
|
|
{
|
|
}
|
|
};
|
|
|
|
/** When set to true, we are a pak writer (we don't do reads). */
|
|
bool bWriting;
|
|
/** When set to true, we are a pak writer and we saved, so we shouldn't be used anymore. Also, a read cache that failed to open. */
|
|
bool bClosed;
|
|
/** Object used for synchronization via scoped read or write locks. */
|
|
FRWLock SynchronizationObject;
|
|
/** Set of files that are being written to disk asynchronously. */
|
|
TMap<FString, FCacheValue> CacheItems;
|
|
/** File handle of pak. */
|
|
TUniquePtr<IFileHandle> FileHandle;
|
|
/** File name of pak. */
|
|
FString CachePath;
|
|
|
|
/** Maximum total size of compressed data stored within a record package with multiple attachments. */
|
|
uint64 MaxRecordSizeKB = 256;
|
|
/** Maximum total size of compressed data stored within a value package, or a record package with one attachment. */
|
|
uint64 MaxValueSizeKB = 1024;
|
|
|
|
ICacheStoreOwner* StoreOwner = nullptr;
|
|
ICacheStoreStats* StoreStats = nullptr;
|
|
|
|
enum
|
|
{
|
|
/** Magic number to use in header */
|
|
PakCache_Magic = 0x0c7c0ddc,
|
|
};
|
|
|
|
friend class IPakFileCacheStore;
|
|
};
|
|
|
|
FPakFileCacheStore::FPakFileCacheStore(const TCHAR* InName, const TCHAR* const InCachePath, const bool bInWriting, ICacheStoreOwner* const InOwner)
|
|
: bWriting(bInWriting)
|
|
, bClosed(false)
|
|
, CachePath(InCachePath)
|
|
, StoreOwner(InOwner)
|
|
{
|
|
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
|
|
if (bWriting)
|
|
{
|
|
PlatformFile.CreateDirectoryTree(*FPaths::GetPath(CachePath));
|
|
FileHandle.Reset(PlatformFile.OpenWrite(*CachePath, /*bAppend*/ false, /*bAllowRead*/ true));
|
|
if (!FileHandle)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Fatal, TEXT("%s: Failed to open pak cache for writing."), *CachePath);
|
|
bClosed = true;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Display, TEXT("%s: Opened pak cache for writing."), *CachePath);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FileHandle.Reset(PlatformFile.OpenRead(*CachePath));
|
|
if (!FileHandle)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Warning, TEXT("%s: Failed to open pak cache for reading."), *CachePath);
|
|
}
|
|
else if (!LoadCache(*CachePath))
|
|
{
|
|
FileHandle.Reset();
|
|
CacheItems.Empty();
|
|
bClosed = true;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Display, TEXT("%s: Opened pak cache for reading. (%" INT64_FMT " MiB)"),
|
|
*CachePath, FileHandle->Size() / 1024 / 1024);
|
|
}
|
|
}
|
|
|
|
if (StoreOwner)
|
|
{
|
|
const ECacheStoreFlags Flags = ECacheStoreFlags::Local | (bWriting ? ECacheStoreFlags::Store : ECacheStoreFlags::Query | ECacheStoreFlags::StopStore);
|
|
StoreOwner->Add(this, Flags);
|
|
StoreStats = StoreOwner->CreateStats(this, Flags, TEXT("Pak File"), InName, CachePath);
|
|
}
|
|
}
|
|
|
|
FPakFileCacheStore::~FPakFileCacheStore()
|
|
{
|
|
Close();
|
|
}
|
|
|
|
void FPakFileCacheStore::Close()
|
|
{
|
|
FDerivedDataBackend::Get().WaitForQuiescence();
|
|
if (!bClosed)
|
|
{
|
|
if (bWriting)
|
|
{
|
|
SaveCache();
|
|
}
|
|
FWriteScopeLock ScopeLock(SynchronizationObject);
|
|
FileHandle.Reset();
|
|
CacheItems.Empty();
|
|
bClosed = true;
|
|
}
|
|
|
|
if (StoreOwner)
|
|
{
|
|
StoreOwner->DestroyStats(StoreStats);
|
|
StoreOwner->RemoveNotSafe(this);
|
|
StoreOwner = nullptr;
|
|
}
|
|
}
|
|
|
|
bool FPakFileCacheStore::CachedDataProbablyExists(const TCHAR* CacheKey)
|
|
{
|
|
COOK_STAT(auto Timer = UsageStats.TimeProbablyExists());
|
|
if (bClosed)
|
|
{
|
|
return false;
|
|
}
|
|
FReadScopeLock ScopeLock(SynchronizationObject);
|
|
bool Result = CacheItems.Contains(FString(CacheKey));
|
|
if (Result)
|
|
{
|
|
COOK_STAT(Timer.AddHit(0));
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
bool FPakFileCacheStore::GetCachedData(const TCHAR* CacheKey, TArray<uint8>& OutData)
|
|
{
|
|
COOK_STAT(auto Timer = UsageStats.TimeGet());
|
|
if (bClosed)
|
|
{
|
|
return false;
|
|
}
|
|
FWriteScopeLock ScopeLock(SynchronizationObject);
|
|
if (FCacheValue* Item = CacheItems.Find(FString(CacheKey)))
|
|
{
|
|
check(FileHandle);
|
|
ON_SCOPE_EXIT
|
|
{
|
|
if (bWriting)
|
|
{
|
|
FileHandle->SeekFromEnd();
|
|
}
|
|
};
|
|
if (Item->Size >= MAX_int32)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Warning, TEXT("%s: Pak file, %s exceeds 2 GiB limit."), *CachePath, CacheKey);
|
|
}
|
|
else if (!FileHandle->Seek(Item->Offset))
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Warning, TEXT("%s: Pak file, bad seek."), *CachePath);
|
|
}
|
|
else
|
|
{
|
|
check(Item->Size);
|
|
check(!OutData.Num());
|
|
OutData.AddUninitialized(int32(Item->Size));
|
|
if (!FileHandle->Read(OutData.GetData(), int64(Item->Size)))
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Warning, TEXT("%s: Pak file, bad read."), *CachePath);
|
|
}
|
|
else if (uint32 TestCrc = FCrc::MemCrc_DEPRECATED(OutData.GetData(), int32(Item->Size)); TestCrc != Item->Crc)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Warning, TEXT("%s: Pak file, bad crc."), *CachePath);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Cache hit on %s"), *CachePath, CacheKey);
|
|
check(OutData.Num());
|
|
COOK_STAT(Timer.AddHit(OutData.Num()));
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Cache miss on %s"), *CachePath, CacheKey);
|
|
}
|
|
OutData.Empty();
|
|
return false;
|
|
}
|
|
|
|
void FPakFileCacheStore::PutCachedData(const TCHAR* CacheKey, TArrayView<const uint8> InData, bool bPutEvenIfExists)
|
|
{
|
|
COOK_STAT(auto Timer = UsageStats.TimePut());
|
|
if (bWriting && !bClosed)
|
|
{
|
|
FWriteScopeLock ScopeLock(SynchronizationObject);
|
|
FString Key(CacheKey);
|
|
TOptional<uint32> Crc;
|
|
check(InData.Num());
|
|
check(Key.Len());
|
|
check(FileHandle);
|
|
|
|
if (bPutEvenIfExists)
|
|
{
|
|
if (FCacheValue* Item = CacheItems.Find(FString(CacheKey)))
|
|
{
|
|
// If there was an existing entry for this key, if it had the same contents, do nothing as the desired value is already stored.
|
|
// If the contents differ, replace it if the size hasn't changed, but if the size has changed,
|
|
// remove the existing entry from the index but leave they actual data payload in place as it is too
|
|
// costly to go back and attempt to rewrite all offsets and shift all bytes that follow it in the file.
|
|
if (Item->Size == InData.Num())
|
|
{
|
|
COOK_STAT(Timer.AddHit(InData.Num()));
|
|
Crc = FCrc::MemCrc_DEPRECATED(InData.GetData(), InData.Num());
|
|
if (Crc.GetValue() != Item->Crc)
|
|
{
|
|
int64 Offset = FileHandle->Tell();
|
|
FileHandle->Seek(Item->Offset);
|
|
FileHandle->Write(InData.GetData(), InData.Num());
|
|
Item->Crc = Crc.GetValue();
|
|
FileHandle->Seek(Offset);
|
|
}
|
|
return;
|
|
}
|
|
|
|
UE_LOG(LogDerivedDataCache, Warning,
|
|
TEXT("%s: Repeated put of %s with different sized contents. Multiple contents will be in the file, "
|
|
"but only the last will be in the index. This has wasted %" INT64_FMT " bytes in the file."),
|
|
*CachePath, CacheKey, Item->Size);
|
|
CacheItems.Remove(Key);
|
|
}
|
|
}
|
|
|
|
int64 Offset = FileHandle->Tell();
|
|
if (Offset < 0)
|
|
{
|
|
CacheItems.Empty();
|
|
FileHandle.Reset();
|
|
UE_LOG(LogDerivedDataCache, Fatal, TEXT("%s: Could not write pak file... out of disk space?"), *CachePath);
|
|
}
|
|
else
|
|
{
|
|
COOK_STAT(Timer.AddHit(InData.Num()));
|
|
if (!Crc.IsSet())
|
|
{
|
|
Crc = FCrc::MemCrc_DEPRECATED(InData.GetData(), InData.Num());
|
|
}
|
|
FileHandle->Write(InData.GetData(), InData.Num());
|
|
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Put %s"), *CachePath, CacheKey);
|
|
CacheItems.Add(Key, FCacheValue(Offset, InData.Num(), Crc.GetValue()));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FPakFileCacheStore::SaveCache()
|
|
{
|
|
FWriteScopeLock ScopeLock(SynchronizationObject);
|
|
check(FileHandle);
|
|
int64 IndexOffset = FileHandle->Tell();
|
|
check(IndexOffset >= 0);
|
|
uint32 NumItems = uint32(CacheItems.Num());
|
|
check(IndexOffset > 0 || !NumItems);
|
|
TArray<uint8> IndexBuffer;
|
|
{
|
|
FMemoryWriter Saver(IndexBuffer);
|
|
uint32 NumProcessed = 0;
|
|
for (TMap<FString, FCacheValue>::TIterator It(CacheItems); It; ++It )
|
|
{
|
|
FCacheValue& Value = It.Value();
|
|
check(It.Key().Len());
|
|
check(Value.Size);
|
|
check(Value.Offset >= 0 && Value.Offset < IndexOffset);
|
|
Saver << It.Key();
|
|
Saver << Value.Offset;
|
|
Saver << Value.Size;
|
|
Saver << Value.Crc;
|
|
NumProcessed++;
|
|
}
|
|
check(NumProcessed == NumItems);
|
|
}
|
|
uint32 IndexCrc = FCrc::MemCrc_DEPRECATED(IndexBuffer.GetData(), IndexBuffer.Num());
|
|
uint32 SizeIndex = uint32(IndexBuffer.Num());
|
|
|
|
uint32 Magic = PakCache_Magic;
|
|
TArray<uint8> Buffer;
|
|
FMemoryWriter Saver(Buffer);
|
|
Saver << Magic;
|
|
Saver << IndexCrc;
|
|
Saver << NumItems;
|
|
Saver << SizeIndex;
|
|
Saver.Serialize(IndexBuffer.GetData(), IndexBuffer.Num());
|
|
Saver << Magic;
|
|
Saver << IndexOffset;
|
|
FileHandle->Write(Buffer.GetData(), Buffer.Num());
|
|
CacheItems.Empty();
|
|
FileHandle.Reset();
|
|
bClosed = true;
|
|
return true;
|
|
}
|
|
|
|
bool FPakFileCacheStore::LoadCache(const TCHAR* InFilename)
|
|
{
|
|
check(FileHandle);
|
|
int64 FileSize = FileHandle->Size();
|
|
check(FileSize >= 0);
|
|
if (FileSize < sizeof(int64) + sizeof(uint32) * 5)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Error, TEXT("%s: Pak cache was corrupted (short)."), InFilename);
|
|
return false;
|
|
}
|
|
int64 IndexOffset = -1;
|
|
int64 Trailer = -1;
|
|
{
|
|
TArray<uint8> Buffer;
|
|
const int64 SeekPos = FileSize - int64(sizeof(int64) + sizeof(uint32));
|
|
FileHandle->Seek(SeekPos);
|
|
Trailer = FileHandle->Tell();
|
|
if (Trailer != SeekPos)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Error, TEXT("%s: Pak cache was corrupted (bad seek)."), InFilename);
|
|
return false;
|
|
}
|
|
check(Trailer >= 0 && Trailer < FileSize);
|
|
Buffer.AddUninitialized(sizeof(int64) + sizeof(uint32));
|
|
FileHandle->Read(Buffer.GetData(), int64(sizeof(int64)+sizeof(uint32)));
|
|
FMemoryReader Loader(Buffer);
|
|
uint32 Magic = 0;
|
|
Loader << Magic;
|
|
Loader << IndexOffset;
|
|
if (Magic != PakCache_Magic || IndexOffset < 0 || IndexOffset + int64(sizeof(uint32) * 4) > Trailer)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Error, TEXT("%s: Pak cache was corrupted (bad footer)."), InFilename);
|
|
return false;
|
|
}
|
|
}
|
|
uint32 IndexCrc = 0;
|
|
uint32 NumIndex = 0;
|
|
uint32 SizeIndex = 0;
|
|
{
|
|
TArray<uint8> Buffer;
|
|
FileHandle->Seek(IndexOffset);
|
|
if (FileHandle->Tell() != IndexOffset)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Error, TEXT("%s: Pak cache was corrupted (bad seek index)."), InFilename);
|
|
return false;
|
|
}
|
|
Buffer.AddUninitialized(sizeof(uint32) * 4);
|
|
FileHandle->Read(Buffer.GetData(), sizeof(uint32) * 4);
|
|
FMemoryReader Loader(Buffer);
|
|
uint32 Magic = 0;
|
|
Loader << Magic;
|
|
Loader << IndexCrc;
|
|
Loader << NumIndex;
|
|
Loader << SizeIndex;
|
|
if (Magic != PakCache_Magic || (SizeIndex != 0 && NumIndex == 0) || (SizeIndex == 0 && NumIndex != 0))
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Error, TEXT("%s: Pak cache was corrupted (bad index header)."), InFilename);
|
|
return false;
|
|
}
|
|
if (IndexOffset + sizeof(uint32) * 4 + SizeIndex != Trailer)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Error, TEXT("%s: Pak cache was corrupted (bad index size)."), InFilename);
|
|
return false;
|
|
}
|
|
}
|
|
{
|
|
TArray<uint8> Buffer;
|
|
Buffer.AddUninitialized(SizeIndex);
|
|
FileHandle->Read(Buffer.GetData(), SizeIndex);
|
|
FMemoryReader Loader(Buffer);
|
|
while (Loader.Tell() < SizeIndex)
|
|
{
|
|
FString Key;
|
|
int64 Offset = 0;
|
|
int64 Size = 0;
|
|
uint32 Crc = 0;
|
|
Loader << Key;
|
|
Loader << Offset;
|
|
Loader << Size;
|
|
Loader << Crc;
|
|
if (!Key.Len() || Offset < 0 || Offset >= IndexOffset || !Size)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Error, TEXT("%s: Pak cache was corrupted (bad index entry)."), InFilename);
|
|
return false;
|
|
}
|
|
CacheItems.Add(Key, FCacheValue(Offset, Size, Crc));
|
|
}
|
|
if (CacheItems.Num() != NumIndex)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Error, TEXT("%s: Pak cache was corrupted (bad index count)."), InFilename);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void FPakFileCacheStore::MergeCache(IPakFileCacheStore* OtherPakInterface)
|
|
{
|
|
FPakFileCacheStore* OtherPak = static_cast<FPakFileCacheStore*>(OtherPakInterface);
|
|
|
|
// Get all the existing keys
|
|
TArray<FString> KeyNames;
|
|
OtherPak->CacheItems.GenerateKeyArray(KeyNames);
|
|
|
|
// Find all the keys to copy
|
|
TArray<FString> CopyKeyNames;
|
|
for(const FString& KeyName : KeyNames)
|
|
{
|
|
if(!CachedDataProbablyExists(*KeyName))
|
|
{
|
|
CopyKeyNames.Add(KeyName);
|
|
}
|
|
}
|
|
UE_LOG(LogDerivedDataCache, Display, TEXT("Merging %d entries (%d skipped)."), CopyKeyNames.Num(), KeyNames.Num() - CopyKeyNames.Num());
|
|
|
|
// Copy them all to the new cache. Don't use the overloaded get/put methods (which may compress/decompress); copy the raw data directly.
|
|
TArray<uint8> Buffer;
|
|
for(const FString& CopyKeyName : CopyKeyNames)
|
|
{
|
|
Buffer.Reset();
|
|
if(OtherPak->FPakFileCacheStore::GetCachedData(*CopyKeyName, Buffer))
|
|
{
|
|
FPakFileCacheStore::PutCachedData(*CopyKeyName, Buffer, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool IPakFileCacheStore::SortAndCopy(const FString &InputFilename, const FString &OutputFilename)
|
|
{
|
|
// Open the input and output files
|
|
FPakFileCacheStore InputPak(TEXT("Input"), *InputFilename, false);
|
|
if (InputPak.bClosed) return false;
|
|
|
|
FPakFileCacheStore OutputPak(TEXT("Output"), *OutputFilename, true);
|
|
if (OutputPak.bClosed) return false;
|
|
|
|
// Sort the key names
|
|
TArray<FString> KeyNames;
|
|
InputPak.CacheItems.GenerateKeyArray(KeyNames);
|
|
KeyNames.Sort();
|
|
|
|
// Copy all the DDC to the new cache
|
|
TArray<uint8> Buffer;
|
|
TArray<uint32> KeySizes;
|
|
for (int KeyIndex = 0; KeyIndex < KeyNames.Num(); KeyIndex++)
|
|
{
|
|
Buffer.Reset();
|
|
// Data over 2 GiB is not copied.
|
|
if (InputPak.GetCachedData(*KeyNames[KeyIndex], Buffer))
|
|
{
|
|
OutputPak.PutCachedData(*KeyNames[KeyIndex], Buffer, false);
|
|
}
|
|
KeySizes.Add(Buffer.Num());
|
|
}
|
|
|
|
// Write out a TOC listing for debugging
|
|
FStringOutputDevice Output;
|
|
Output.Logf(TEXT("Asset,Size" LINE_TERMINATOR_ANSI));
|
|
for(int KeyIndex = 0; KeyIndex < KeyNames.Num(); KeyIndex++)
|
|
{
|
|
Output.Logf(TEXT("%s,%d" LINE_TERMINATOR_ANSI), *KeyNames[KeyIndex], KeySizes[KeyIndex]);
|
|
}
|
|
FFileHelper::SaveStringToFile(Output, *FPaths::Combine(*FPaths::GetPath(OutputFilename), *(FPaths::GetBaseFilename(OutputFilename) + TEXT(".csv"))));
|
|
return true;
|
|
}
|
|
|
|
void FPakFileCacheStore::LegacyStats(FDerivedDataCacheStatsNode& OutNode)
|
|
{
|
|
checkNoEntry();
|
|
}
|
|
|
|
bool FPakFileCacheStore::LegacyDebugOptions(FBackendDebugOptions& InOptions)
|
|
{
|
|
DebugOptions = InOptions;
|
|
return true;
|
|
}
|
|
|
|
void FPakFileCacheStore::Put(
|
|
const TConstArrayView<FCachePutRequest> Requests,
|
|
IRequestOwner& Owner,
|
|
FOnCachePutComplete&& OnComplete)
|
|
{
|
|
for (const FCachePutRequest& Request : Requests)
|
|
{
|
|
bool bOk;
|
|
FRequestStats RequestStats;
|
|
RequestStats.Name = Request.Name;
|
|
RequestStats.Bucket = Request.Record.GetKey().Bucket;
|
|
RequestStats.Type = ERequestType::Record;
|
|
RequestStats.Op = ERequestOp::Put;
|
|
{
|
|
const FCacheRecord& Record = Request.Record;
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(PakFileDDC_Put);
|
|
FRequestTimer RequestTimer(RequestStats);
|
|
bOk = PutCacheRecord(Request.Name, Record, Request.Policy, RequestStats);
|
|
if (bOk)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Cache put complete for %s from '%s'"),
|
|
*CachePath, *WriteToString<96>(Record.GetKey()), *Request.Name);
|
|
}
|
|
}
|
|
RequestStats.Status = bOk ? EStatus::Ok : EStatus::Error;
|
|
if (StoreStats)
|
|
{
|
|
StoreStats->AddRequest(RequestStats);
|
|
}
|
|
OnComplete(Request.MakeResponse(bOk ? EStatus::Ok : EStatus::Error));
|
|
}
|
|
}
|
|
|
|
void FPakFileCacheStore::Get(
|
|
const TConstArrayView<FCacheGetRequest> Requests,
|
|
IRequestOwner& Owner,
|
|
FOnCacheGetComplete&& OnComplete)
|
|
{
|
|
for (const FCacheGetRequest& Request : Requests)
|
|
{
|
|
EStatus Status = EStatus::Error;
|
|
FOptionalCacheRecord Record;
|
|
FRequestStats RequestStats;
|
|
RequestStats.Name = Request.Name;
|
|
RequestStats.Bucket = Request.Key.Bucket;
|
|
RequestStats.Type = ERequestType::Record;
|
|
RequestStats.Op = ERequestOp::Get;
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(PakFileDDC_Get);
|
|
FRequestTimer RequestTimer(RequestStats);
|
|
if ((Record = GetCacheRecord(Request.Name, Request.Key, Request.Policy, Status, RequestStats)))
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Cache hit for %s from '%s'"),
|
|
*CachePath, *WriteToString<96>(Request.Key), *Request.Name);
|
|
}
|
|
else
|
|
{
|
|
Record = FCacheRecordBuilder(Request.Key).Build();
|
|
}
|
|
}
|
|
RequestStats.AddLogicalRead(Record.Get());
|
|
RequestStats.Status = Status;
|
|
if (StoreStats)
|
|
{
|
|
StoreStats->AddRequest(RequestStats);
|
|
}
|
|
OnComplete({Request.Name, MoveTemp(Record).Get(), Request.UserData, Status});
|
|
}
|
|
}
|
|
|
|
void FPakFileCacheStore::PutValue(
|
|
const TConstArrayView<FCachePutValueRequest> Requests,
|
|
IRequestOwner& Owner,
|
|
FOnCachePutValueComplete&& OnComplete)
|
|
{
|
|
for (const FCachePutValueRequest& Request : Requests)
|
|
{
|
|
bool bOk;
|
|
FRequestStats RequestStats;
|
|
RequestStats.Name = Request.Name;
|
|
RequestStats.Bucket = Request.Key.Bucket;
|
|
RequestStats.Type = ERequestType::Value;
|
|
RequestStats.Op = ERequestOp::Put;
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(PakFileDDC_PutValue);
|
|
FRequestTimer RequestTimer(RequestStats);
|
|
bOk = PutCacheValue(Request.Name, Request.Key, Request.Value, Request.Policy, RequestStats);
|
|
if (bOk)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Cache put complete for %s from '%s'"),
|
|
*CachePath, *WriteToString<96>(Request.Key), *Request.Name);
|
|
}
|
|
}
|
|
RequestStats.Status = bOk ? EStatus::Ok : EStatus::Error;
|
|
if (StoreStats)
|
|
{
|
|
StoreStats->AddRequest(RequestStats);
|
|
}
|
|
OnComplete(Request.MakeResponse(bOk ? EStatus::Ok : EStatus::Error));
|
|
}
|
|
}
|
|
|
|
void FPakFileCacheStore::GetValue(
|
|
const TConstArrayView<FCacheGetValueRequest> Requests,
|
|
IRequestOwner& Owner,
|
|
FOnCacheGetValueComplete&& OnComplete)
|
|
{
|
|
for (const FCacheGetValueRequest& Request : Requests)
|
|
{
|
|
bool bOk;
|
|
FValue Value;
|
|
FRequestStats RequestStats;
|
|
RequestStats.Name = Request.Name;
|
|
RequestStats.Bucket = Request.Key.Bucket;
|
|
RequestStats.Type = ERequestType::Value;
|
|
RequestStats.Op = ERequestOp::Get;
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(PakFileDDC_GetValue);
|
|
FRequestTimer RequestTimer(RequestStats);
|
|
bOk = GetCacheValue(Request.Name, Request.Key, Request.Policy, Value, RequestStats);
|
|
if (bOk)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Cache hit for %s from '%s'"),
|
|
*CachePath, *WriteToString<96>(Request.Key), *Request.Name);
|
|
}
|
|
}
|
|
RequestStats.AddLogicalRead(Value);
|
|
RequestStats.Status = bOk ? EStatus::Ok : EStatus::Error;
|
|
if (StoreStats)
|
|
{
|
|
StoreStats->AddRequest(RequestStats);
|
|
}
|
|
OnComplete({Request.Name, Request.Key, Value, Request.UserData, bOk ? EStatus::Ok : EStatus::Error});
|
|
}
|
|
}
|
|
|
|
void FPakFileCacheStore::GetChunks(
|
|
const TConstArrayView<FCacheGetChunkRequest> Requests,
|
|
IRequestOwner& Owner,
|
|
FOnCacheGetChunkComplete&& OnComplete)
|
|
{
|
|
TArray<FCacheGetChunkRequest, TInlineAllocator<16>> SortedRequests(Requests);
|
|
SortedRequests.StableSort(TChunkLess());
|
|
|
|
bool bHasValue = false;
|
|
FValue Value;
|
|
FValueId ValueId;
|
|
FCacheKey ValueKey;
|
|
TUniquePtr<FArchive> ValueAr;
|
|
FCompressedBufferReader ValueReader;
|
|
FOptionalCacheRecord Record;
|
|
for (const FCacheGetChunkRequest& Request : SortedRequests)
|
|
{
|
|
EStatus Status = EStatus::Error;
|
|
FSharedBuffer Buffer;
|
|
uint64 RawSize = 0;
|
|
FRequestStats RequestStats;
|
|
RequestStats.Name = Request.Name;
|
|
RequestStats.Bucket = Request.Key.Bucket;
|
|
RequestStats.Type = Request.Id.IsNull() ? ERequestType::Value : ERequestType::Record;
|
|
RequestStats.Op = ECacheStoreRequestOp::GetChunk;
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(PakFileDDC_Get);
|
|
FRequestTimer RequestTimer(RequestStats);
|
|
const bool bExistsOnly = EnumHasAnyFlags(Request.Policy, ECachePolicy::SkipData);
|
|
if (!(bHasValue && ValueKey == Request.Key && ValueId == Request.Id) || ValueReader.HasSource() < !bExistsOnly)
|
|
{
|
|
ValueReader.ResetSource();
|
|
ValueAr.Reset();
|
|
ValueKey = {};
|
|
ValueId.Reset();
|
|
Value.Reset();
|
|
bHasValue = false;
|
|
if (Request.Id.IsValid())
|
|
{
|
|
if (!(Record && Record.Get().GetKey() == Request.Key))
|
|
{
|
|
FCacheRecordPolicyBuilder PolicyBuilder(ECachePolicy::None);
|
|
PolicyBuilder.AddValuePolicy(Request.Id, Request.Policy);
|
|
Record.Reset();
|
|
Record = GetCacheRecordOnly(Request.Name, Request.Key, PolicyBuilder.Build(), RequestStats);
|
|
}
|
|
if (Record)
|
|
{
|
|
if (const FValueWithId& ValueWithId = Record.Get().GetValue(Request.Id))
|
|
{
|
|
bHasValue = true;
|
|
Value = ValueWithId;
|
|
ValueId = Request.Id;
|
|
ValueKey = Request.Key;
|
|
GetCacheContent(Request.Name, Request.Key, ValueId, Value, Request.Policy, ValueReader, ValueAr, RequestStats);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ValueKey = Request.Key;
|
|
bHasValue = GetCacheValueOnly(Request.Name, Request.Key, Request.Policy, Value, RequestStats);
|
|
if (bHasValue)
|
|
{
|
|
GetCacheContent(Request.Name, Request.Key, Request.Id, Value, Request.Policy, ValueReader, ValueAr, RequestStats);
|
|
}
|
|
}
|
|
}
|
|
if (bHasValue)
|
|
{
|
|
const uint64 RawOffset = FMath::Min(Value.GetRawSize(), Request.RawOffset);
|
|
RawSize = FMath::Min(Value.GetRawSize() - RawOffset, Request.RawSize);
|
|
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Cache hit for %s from '%s'"),
|
|
*CachePath, *WriteToString<96>(Request.Key, '/', Request.Id), *Request.Name);
|
|
if (!bExistsOnly)
|
|
{
|
|
Buffer = ValueReader.Decompress(RawOffset, RawSize);
|
|
RequestStats.LogicalReadSize += Buffer.GetSize();
|
|
}
|
|
Status = bExistsOnly || Buffer.GetSize() == RawSize ? EStatus::Ok : EStatus::Error;
|
|
}
|
|
}
|
|
RequestStats.Status = Status;
|
|
if (StoreStats)
|
|
{
|
|
StoreStats->AddRequest(RequestStats);
|
|
}
|
|
OnComplete({Request.Name, Request.Key, Request.Id, Request.RawOffset,
|
|
RawSize, Value.GetRawHash(), MoveTemp(Buffer), Request.UserData, Status});
|
|
}
|
|
}
|
|
|
|
bool FPakFileCacheStore::PutCacheRecord(
|
|
const FStringView Name,
|
|
const FCacheRecord& Record,
|
|
const FCacheRecordPolicy& Policy,
|
|
FRequestStats& Stats)
|
|
{
|
|
if (!bWriting || bClosed)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const FCacheKey& Key = Record.GetKey();
|
|
const ECachePolicy RecordPolicy = Policy.GetRecordPolicy();
|
|
|
|
// Skip the request if storing to the cache is disabled.
|
|
if (!EnumHasAnyFlags(Policy.GetRecordPolicy(), ECachePolicy::StoreLocal))
|
|
{
|
|
UE_LOG(LogDerivedDataCache, VeryVerbose, TEXT("%s: Skipped put of %s from '%.*s' due to cache policy"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return false;
|
|
}
|
|
|
|
if (DebugOptions.ShouldSimulatePutMiss(Key))
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Simulated miss for put of %s from '%.*s'"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return false;
|
|
}
|
|
|
|
TStringBuilder<256> Path;
|
|
FPathViews::Append(Path, TEXT("Buckets"), Key);
|
|
|
|
// Check if there is an existing record package.
|
|
bool bReplaceExisting = !EnumHasAnyFlags(RecordPolicy, ECachePolicy::QueryLocal);
|
|
bool bSavePackage = bReplaceExisting;
|
|
if (!bReplaceExisting)
|
|
{
|
|
bSavePackage |= !FileExists(Path, Stats);
|
|
}
|
|
|
|
// Serialize the record to a package and remove attachments that will be stored externally.
|
|
FCbPackage Package = Record.Save();
|
|
TArray<FCompressedBuffer, TInlineAllocator<8>> ExternalContent;
|
|
Algo::Transform(Package.GetAttachments(), ExternalContent, &FCbAttachment::AsCompressedBinary);
|
|
Package = FCbPackage(Package.GetObject());
|
|
|
|
// Save the external content to storage.
|
|
for (FCompressedBuffer& Content : ExternalContent)
|
|
{
|
|
if (!PutCacheContent(Name, Content, Stats))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Save the record package to storage.
|
|
const auto WritePackage = [&Package](FArchive& Ar) { Package.Save(Ar); };
|
|
if (bSavePackage)
|
|
{
|
|
if (!SaveFile(Path, Name, Stats, WritePackage))
|
|
{
|
|
return false;
|
|
}
|
|
if (const FCbObject& Meta = Record.GetMeta())
|
|
{
|
|
Stats.LogicalWriteSize += Meta.GetSize();
|
|
}
|
|
Stats.LogicalWriteSize += Algo::TransformAccumulate(Package.GetAttachments(),
|
|
[](const FCbAttachment& Attachment) { return Attachment.AsCompressedBinary().GetRawSize(); }, uint64(0));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
FOptionalCacheRecord FPakFileCacheStore::GetCacheRecordOnly(
|
|
const FStringView Name,
|
|
const FCacheKey& Key,
|
|
const FCacheRecordPolicy& Policy,
|
|
FRequestStats& Stats)
|
|
{
|
|
if (bClosed)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, VeryVerbose,
|
|
TEXT("%s: Skipped get of %s from '%.*s' because this cache store is not available"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return FOptionalCacheRecord();
|
|
}
|
|
|
|
// Skip the request if querying the cache is disabled.
|
|
if (!EnumHasAnyFlags(Policy.GetRecordPolicy(), ECachePolicy::QueryLocal))
|
|
{
|
|
UE_LOG(LogDerivedDataCache, VeryVerbose, TEXT("%s: Skipped get of %s from '%.*s' due to cache policy"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return FOptionalCacheRecord();
|
|
}
|
|
|
|
if (DebugOptions.ShouldSimulateGetMiss(Key))
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Simulated miss for get of %s from '%.*s'"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return FOptionalCacheRecord();
|
|
}
|
|
|
|
TStringBuilder<256> Path;
|
|
FPathViews::Append(Path, TEXT("Buckets"), Key);
|
|
|
|
// Request the record from storage.
|
|
FSharedBuffer Buffer = LoadFile(Path, Name, Stats);
|
|
if (Buffer.IsNull())
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Cache miss with missing record for %s from '%.*s'"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return FOptionalCacheRecord();
|
|
}
|
|
|
|
// Validate that the record can be read as a compact binary package without crashing.
|
|
if (ValidateCompactBinaryPackage(Buffer, ECbValidateMode::Default | ECbValidateMode::Package) != ECbValidateError::None)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Display, TEXT("%s: Cache miss with invalid package for %s from '%.*s'"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return FOptionalCacheRecord();
|
|
}
|
|
|
|
// Load the record from the package.
|
|
FOptionalCacheRecord Record;
|
|
{
|
|
FCbPackage Package;
|
|
if (FCbFieldIterator It = FCbFieldIterator::MakeRange(Buffer); !Package.TryLoad(It))
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Display, TEXT("%s: Cache miss with package load failure for %s from '%.*s'"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return Record;
|
|
}
|
|
Record = FCacheRecord::Load(Package);
|
|
if (Record.IsNull())
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Display, TEXT("%s: Cache miss with record load failure for %s from '%.*s'"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return Record;
|
|
}
|
|
}
|
|
|
|
return Record;
|
|
}
|
|
|
|
FOptionalCacheRecord FPakFileCacheStore::GetCacheRecord(
|
|
const FStringView Name,
|
|
const FCacheKey& Key,
|
|
const FCacheRecordPolicy& Policy,
|
|
EStatus& OutStatus,
|
|
FRequestStats& Stats)
|
|
{
|
|
FOptionalCacheRecord Record = GetCacheRecordOnly(Name, Key, Policy, Stats);
|
|
if (Record.IsNull())
|
|
{
|
|
OutStatus = EStatus::Error;
|
|
return Record;
|
|
}
|
|
|
|
OutStatus = EStatus::Ok;
|
|
|
|
FCacheRecordBuilder RecordBuilder(Key);
|
|
|
|
const ECachePolicy RecordPolicy = Policy.GetRecordPolicy();
|
|
if (!EnumHasAnyFlags(RecordPolicy, ECachePolicy::SkipMeta))
|
|
{
|
|
RecordBuilder.SetMeta(FCbObject(Record.Get().GetMeta()));
|
|
}
|
|
|
|
for (const FValueWithId& Value : Record.Get().GetValues())
|
|
{
|
|
const FValueId& Id = Value.GetId();
|
|
const ECachePolicy ValuePolicy = Policy.GetValuePolicy(Id);
|
|
FValue Content;
|
|
if (GetCacheContent(Name, Key, Id, Value, ValuePolicy, Content, Stats))
|
|
{
|
|
RecordBuilder.AddValue(Id, MoveTemp(Content));
|
|
}
|
|
else if (EnumHasAnyFlags(RecordPolicy, ECachePolicy::PartialRecord))
|
|
{
|
|
OutStatus = EStatus::Error;
|
|
RecordBuilder.AddValue(Value);
|
|
}
|
|
else
|
|
{
|
|
OutStatus = EStatus::Error;
|
|
return FOptionalCacheRecord();
|
|
}
|
|
}
|
|
|
|
return RecordBuilder.Build();
|
|
}
|
|
|
|
bool FPakFileCacheStore::PutCacheValue(
|
|
const FStringView Name,
|
|
const FCacheKey& Key,
|
|
const FValue& Value,
|
|
const ECachePolicy Policy,
|
|
FRequestStats& Stats)
|
|
{
|
|
if (!bWriting || bClosed)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Skip the request if storing to the cache is disabled.
|
|
if (!EnumHasAnyFlags(Policy, ECachePolicy::StoreLocal))
|
|
{
|
|
UE_LOG(LogDerivedDataCache, VeryVerbose, TEXT("%s: Skipped put of %s from '%.*s' due to cache policy"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return false;
|
|
}
|
|
|
|
if (DebugOptions.ShouldSimulatePutMiss(Key))
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Simulated miss for put of %s from '%.*s'"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return false;
|
|
}
|
|
|
|
// Check if there is an existing value package.
|
|
bool bValueExists = false;
|
|
TStringBuilder<256> Path;
|
|
FPathViews::Append(Path, TEXT("Buckets"), Key);
|
|
const bool bReplaceExisting = !EnumHasAnyFlags(Policy, ECachePolicy::QueryLocal);
|
|
if (!bReplaceExisting)
|
|
{
|
|
bValueExists = FileExists(Path, Stats);
|
|
}
|
|
|
|
// Save the value to a package and save the data to external content.
|
|
if (!bValueExists)
|
|
{
|
|
FCbWriter Writer;
|
|
Writer.BeginObject();
|
|
Writer.AddBinaryAttachment("RawHash", Value.GetRawHash());
|
|
Writer.AddInteger("RawSize", Value.GetRawSize());
|
|
Writer.EndObject();
|
|
|
|
FCbPackage Package(Writer.Save().AsObject());
|
|
if (!Value.HasData())
|
|
{
|
|
// Verify that the content exists in storage.
|
|
if (!GetCacheContentExists(Key, Value.GetRawHash(), Stats))
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Failed due to missing data for put of %s from '%.*s'"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Save the external content to storage.
|
|
if (!PutCacheContent(Name, Value.GetData(), Stats))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Save the value package to storage.
|
|
const auto WritePackage = [&Package](FArchive& Ar) { Package.Save(Ar); };
|
|
if (!SaveFile(Path, Name, Stats, WritePackage))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FPakFileCacheStore::GetCacheValueOnly(
|
|
const FStringView Name,
|
|
const FCacheKey& Key,
|
|
const ECachePolicy Policy,
|
|
FValue& OutValue,
|
|
FRequestStats& Stats)
|
|
{
|
|
if (bClosed)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, VeryVerbose,
|
|
TEXT("%s: Skipped get of %s from '%.*s' because this cache store is not available"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return false;
|
|
}
|
|
|
|
// Skip the request if querying the cache is disabled.
|
|
if (!EnumHasAnyFlags(Policy, ECachePolicy::QueryLocal))
|
|
{
|
|
UE_LOG(LogDerivedDataCache, VeryVerbose, TEXT("%s: Skipped get of %s from '%.*s' due to cache policy"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return false;
|
|
}
|
|
|
|
if (DebugOptions.ShouldSimulateGetMiss(Key))
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Simulated miss for get of %s from '%.*s'"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return false;
|
|
}
|
|
|
|
TStringBuilder<256> Path;
|
|
FPathViews::Append(Path, TEXT("Buckets"), Key);
|
|
|
|
// Request the value package from storage.
|
|
FSharedBuffer Buffer = LoadFile(Path, Name, Stats);
|
|
if (Buffer.IsNull())
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Verbose, TEXT("%s: Cache miss with missing value for %s from '%.*s'"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return false;
|
|
}
|
|
|
|
if (ValidateCompactBinary(Buffer, ECbValidateMode::Default | ECbValidateMode::Package) != ECbValidateError::None)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Display, TEXT("%s: Cache miss with invalid package for %s from '%.*s'"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return false;
|
|
}
|
|
|
|
FCbPackage Package;
|
|
if (FCbFieldIterator It = FCbFieldIterator::MakeRange(Buffer); !Package.TryLoad(It))
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Display, TEXT("%s: Cache miss with package load failure for %s from '%.*s'"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return false;
|
|
}
|
|
|
|
const FCbObjectView Object = Package.GetObject();
|
|
const FIoHash RawHash = Object["RawHash"].AsHash();
|
|
const uint64 RawSize = Object["RawSize"].AsUInt64(MAX_uint64);
|
|
if (RawHash.IsZero() || RawSize == MAX_uint64)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Display, TEXT("%s: Cache miss with invalid value for %s from '%.*s'"),
|
|
*CachePath, *WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return false;
|
|
}
|
|
|
|
OutValue = FValue(RawHash, RawSize);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FPakFileCacheStore::GetCacheValue(
|
|
const FStringView Name,
|
|
const FCacheKey& Key,
|
|
const ECachePolicy Policy,
|
|
FValue& OutValue,
|
|
FRequestStats& Stats)
|
|
{
|
|
return GetCacheValueOnly(Name, Key, Policy, OutValue, Stats) && GetCacheContent(Name, Key, {}, OutValue, Policy, OutValue, Stats);
|
|
}
|
|
|
|
bool FPakFileCacheStore::PutCacheContent(const FStringView Name, const FCompressedBuffer& Content, FRequestStats& Stats)
|
|
{
|
|
const FIoHash& RawHash = Content.GetRawHash();
|
|
TStringBuilder<256> Path;
|
|
FPathViews::Append(Path, TEXT("Content"), RawHash);
|
|
if (!FileExists(Path, Stats))
|
|
{
|
|
if (!SaveFile(Path, Name, Stats, [&Content](FArchive& Ar) { Content.Save(Ar); }))
|
|
{
|
|
return false;
|
|
}
|
|
Stats.LogicalWriteSize += Content.GetRawSize();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool FPakFileCacheStore::GetCacheContentExists(const FCacheKey& Key, const FIoHash& RawHash, FRequestStats& Stats)
|
|
{
|
|
TStringBuilder<256> Path;
|
|
FPathViews::Append(Path, TEXT("Buckets"), Key);
|
|
return FileExists(Path, Stats);
|
|
}
|
|
|
|
bool FPakFileCacheStore::GetCacheContent(
|
|
const FStringView Name,
|
|
const FCacheKey& Key,
|
|
const FValueId& Id,
|
|
const FValue& Value,
|
|
const ECachePolicy Policy,
|
|
FValue& OutValue,
|
|
FRequestStats& Stats)
|
|
{
|
|
if (!EnumHasAnyFlags(Policy, ECachePolicy::Query))
|
|
{
|
|
OutValue = Value.RemoveData();
|
|
return true;
|
|
}
|
|
|
|
if (Value.HasData())
|
|
{
|
|
OutValue = EnumHasAnyFlags(Policy, ECachePolicy::SkipData) ? Value.RemoveData() : Value;
|
|
return true;
|
|
}
|
|
|
|
const FIoHash& RawHash = Value.GetRawHash();
|
|
|
|
TStringBuilder<256> Path;
|
|
FPathViews::Append(Path, TEXT("Content"), RawHash);
|
|
if (EnumHasAnyFlags(Policy, ECachePolicy::SkipData))
|
|
{
|
|
if (FileExists(Path, Stats))
|
|
{
|
|
OutValue = Value;
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (FSharedBuffer CompressedData = LoadFile(Path, Name, Stats))
|
|
{
|
|
if (FCompressedBuffer CompressedBuffer = FCompressedBuffer::FromCompressed(MoveTemp(CompressedData));
|
|
CompressedBuffer && CompressedBuffer.GetRawHash() == RawHash)
|
|
{
|
|
OutValue = FValue(MoveTemp(CompressedBuffer));
|
|
return true;
|
|
}
|
|
UE_LOG(LogDerivedDataCache, Display,
|
|
TEXT("%s: Cache miss with corrupted value %s with hash %s for %s from '%.*s'"),
|
|
*CachePath, *WriteToString<16>(Id), *WriteToString<48>(RawHash),
|
|
*WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
UE_LOG(LogDerivedDataCache, Verbose,
|
|
TEXT("%s: Cache miss with missing value %s with hash %s for %s from '%.*s'"),
|
|
*CachePath, *WriteToString<16>(Id), *WriteToString<48>(RawHash), *WriteToString<96>(Key),
|
|
Name.Len(), Name.GetData());
|
|
return false;
|
|
}
|
|
|
|
void FPakFileCacheStore::GetCacheContent(
|
|
const FStringView Name,
|
|
const FCacheKey& Key,
|
|
const FValueId& Id,
|
|
const FValue& Value,
|
|
const ECachePolicy Policy,
|
|
FCompressedBufferReader& Reader,
|
|
TUniquePtr<FArchive>& OutArchive,
|
|
FRequestStats& Stats)
|
|
{
|
|
class FStatsArchive final : TUniquePtr<FArchive>, public FArchiveProxy
|
|
{
|
|
public:
|
|
FStatsArchive(FArchive& InArchive, FRequestStats& InStats)
|
|
: TUniquePtr<FArchive>(&InArchive)
|
|
, FArchiveProxy(InArchive)
|
|
, Stats(InStats)
|
|
{
|
|
}
|
|
|
|
void Serialize(void* V, int64 Length) final
|
|
{
|
|
Stats.PhysicalReadSize += uint64(Length);
|
|
FArchiveProxy::Serialize(V, Length);
|
|
}
|
|
|
|
private:
|
|
FRequestStats& Stats;
|
|
};
|
|
|
|
if (!EnumHasAnyFlags(Policy, ECachePolicy::Query))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Value.HasData())
|
|
{
|
|
if (!EnumHasAnyFlags(Policy, ECachePolicy::SkipData))
|
|
{
|
|
Reader.SetSource(Value.GetData());
|
|
}
|
|
OutArchive.Reset();
|
|
return;
|
|
}
|
|
|
|
const FIoHash& RawHash = Value.GetRawHash();
|
|
|
|
TStringBuilder<256> Path;
|
|
FPathViews::Append(Path, TEXT("Content"), RawHash);
|
|
if (EnumHasAllFlags(Policy, ECachePolicy::SkipData))
|
|
{
|
|
if (FileExists(Path, Stats))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OutArchive = OpenFile(Path, Name, Stats);
|
|
if (OutArchive)
|
|
{
|
|
OutArchive.Reset(new FStatsArchive(*OutArchive.Release(), Stats));
|
|
Reader.SetSource(*OutArchive);
|
|
if (Reader.GetRawHash() == RawHash)
|
|
{
|
|
return;
|
|
}
|
|
UE_LOG(LogDerivedDataCache, Display,
|
|
TEXT("%s: Cache miss with corrupted value %s with hash %s for %s from '%.*s'"),
|
|
*CachePath, *WriteToString<16>(Id), *WriteToString<48>(RawHash),
|
|
*WriteToString<96>(Key), Name.Len(), Name.GetData());
|
|
Reader.ResetSource();
|
|
OutArchive.Reset();
|
|
return;
|
|
}
|
|
}
|
|
|
|
UE_LOG(LogDerivedDataCache, Verbose,
|
|
TEXT("%s: Cache miss with missing value %s with hash %s for %s from '%.*s'"),
|
|
*CachePath, *WriteToString<16>(Id), *WriteToString<48>(RawHash), *WriteToString<96>(Key),
|
|
Name.Len(), Name.GetData());
|
|
}
|
|
|
|
class FCrcBuilder
|
|
{
|
|
public:
|
|
inline void Update(const void* Data, uint64 Size)
|
|
{
|
|
while (Size > 0)
|
|
{
|
|
const int32 CrcSize = int32(FMath::Min<uint64>(Size, MAX_int32));
|
|
Crc = FCrc::MemCrc_DEPRECATED(Data, CrcSize, Crc);
|
|
Size -= CrcSize;
|
|
}
|
|
}
|
|
|
|
inline uint32 Finalize()
|
|
{
|
|
return Crc;
|
|
}
|
|
|
|
private:
|
|
uint32 Crc = 0;
|
|
};
|
|
|
|
class FPakWriterArchive final : public FArchive
|
|
{
|
|
public:
|
|
inline FPakWriterArchive(IFileHandle& InHandle, FStringView InPath)
|
|
: Handle(InHandle)
|
|
, Path(InPath)
|
|
{
|
|
SetIsSaving(true);
|
|
SetIsPersistent(true);
|
|
}
|
|
|
|
inline FString GetArchiveName() const final { return FString(Path); }
|
|
inline int64 TotalSize() final { return Handle.Size(); }
|
|
inline int64 Tell() final { unimplemented(); return 0; }
|
|
inline void Seek(int64 InPos) final { unimplemented(); }
|
|
inline void Flush() final { unimplemented(); }
|
|
inline bool Close() final { unimplemented(); return false; }
|
|
|
|
inline void Serialize(void* V, int64 Length) final
|
|
{
|
|
if (!Handle.Write(static_cast<uint8*>(V), Length))
|
|
{
|
|
SetError();
|
|
}
|
|
}
|
|
|
|
private:
|
|
IFileHandle& Handle;
|
|
FStringView Path;
|
|
};
|
|
|
|
class FPakReaderArchive final : public FArchive
|
|
{
|
|
public:
|
|
inline FPakReaderArchive(IFileHandle& InHandle, FStringView InPath)
|
|
: Handle(InHandle)
|
|
, Path(InPath)
|
|
{
|
|
SetIsLoading(true);
|
|
SetIsPersistent(true);
|
|
}
|
|
|
|
inline FString GetArchiveName() const final { return FString(Path); }
|
|
inline int64 TotalSize() final { return Handle.Size(); }
|
|
inline int64 Tell() final { unimplemented(); return 0; }
|
|
inline void Seek(int64 InPos) final { unimplemented(); }
|
|
inline void Flush() final { unimplemented(); }
|
|
inline bool Close() final { unimplemented(); return false; }
|
|
|
|
inline void Serialize(void* V, int64 Length) final
|
|
{
|
|
if (!Handle.Read(static_cast<uint8*>(V), Length))
|
|
{
|
|
SetError();
|
|
}
|
|
}
|
|
|
|
private:
|
|
IFileHandle& Handle;
|
|
FStringView Path;
|
|
};
|
|
|
|
bool FPakFileCacheStore::SaveFile(
|
|
const FStringView Path,
|
|
const FStringView DebugName,
|
|
FRequestStats& Stats,
|
|
TFunctionRef<void (FArchive&)> WriteFunction)
|
|
{
|
|
FWriteScopeLock ScopeLock(SynchronizationObject);
|
|
check(FileHandle);
|
|
if (const int64 Offset = FileHandle->Tell(); Offset >= 0)
|
|
{
|
|
FPakWriterArchive Ar(*FileHandle, CachePath);
|
|
THashingArchiveProxy<FCrcBuilder> HashAr(Ar);
|
|
WriteFunction(HashAr);
|
|
if (const int64 EndOffset = FileHandle->Tell(); EndOffset >= Offset && !Ar.IsError())
|
|
{
|
|
FCacheValue& Item = CacheItems.Emplace(Path, FCacheValue(Offset, EndOffset - Offset, HashAr.GetHash()));
|
|
UE_LOG(LogDerivedDataCache, VeryVerbose,
|
|
TEXT("%s: File %.*s from '%.*s' written with offset %" INT64_FMT ", size %" INT64_FMT", CRC 0x%08x."),
|
|
*CachePath, Path.Len(), Path.GetData(), DebugName.Len(), DebugName.GetData(), Item.Offset, Item.Size, Item.Crc);
|
|
Stats.PhysicalWriteSize += uint64(Item.Size);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
FSharedBuffer FPakFileCacheStore::LoadFile(const FStringView Path, const FStringView DebugName, FRequestStats& Stats)
|
|
{
|
|
FWriteScopeLock ScopeLock(SynchronizationObject);
|
|
if (const FCacheValue* Item = CacheItems.FindByHash(GetTypeHash(Path), Path))
|
|
{
|
|
check(FileHandle);
|
|
ON_SCOPE_EXIT
|
|
{
|
|
if (bWriting)
|
|
{
|
|
FileHandle->SeekFromEnd();
|
|
}
|
|
};
|
|
check(Item->Size);
|
|
if (FileHandle->Seek(Item->Offset))
|
|
{
|
|
FPakReaderArchive Ar(*FileHandle, CachePath);
|
|
THashingArchiveProxy<FCrcBuilder> HashAr(Ar);
|
|
FUniqueBuffer MutableBuffer = FUniqueBuffer::Alloc(uint64(Item->Size));
|
|
HashAr.Serialize(MutableBuffer.GetData(), Item->Size);
|
|
if (Ar.IsError())
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Display,
|
|
TEXT("%s: File %.*s from '%.*s' failed to read %" INT64_FMT " bytes."),
|
|
*CachePath, Path.Len(), Path.GetData(), DebugName.Len(), DebugName.GetData(), Item->Size);
|
|
}
|
|
else if (const uint32 TestCrc = HashAr.GetHash(); TestCrc != Item->Crc)
|
|
{
|
|
UE_LOG(LogDerivedDataCache, Display,
|
|
TEXT("%s: File %.*s from '%.*s' is corrupted and has CRC 0x%08x when 0x%08x is expected."),
|
|
*CachePath, Path.Len(), Path.GetData(), DebugName.Len(), DebugName.GetData(), TestCrc, Item->Crc);
|
|
}
|
|
else
|
|
{
|
|
Stats.PhysicalReadSize += uint64(Item->Size);
|
|
return MutableBuffer.MoveToShared();
|
|
}
|
|
}
|
|
}
|
|
return FSharedBuffer();
|
|
}
|
|
|
|
TUniquePtr<FArchive> FPakFileCacheStore::OpenFile(FStringBuilderBase& Path, const FStringView DebugName, FRequestStats& Stats)
|
|
{
|
|
const FMonotonicTimePoint StartTime = FMonotonicTimePoint::Now();
|
|
ON_SCOPE_EXIT { Stats.AddLatency(FMonotonicTimePoint::Now() - StartTime); };
|
|
|
|
FReadScopeLock ScopeLock(SynchronizationObject);
|
|
const FStringView PathView(Path);
|
|
if (const FCacheValue* Item = CacheItems.FindByHash(GetTypeHash(PathView), PathView))
|
|
{
|
|
check(Item->Size);
|
|
if (TUniquePtr<FArchive> Ar{IFileManager::Get().CreateFileReader(*CachePath, FILEREAD_Silent | FILEREAD_AllowWrite)})
|
|
{
|
|
Ar->Seek(Item->Offset);
|
|
return Ar;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool FPakFileCacheStore::FileExists(const FStringView Path, FRequestStats& Stats)
|
|
{
|
|
FReadScopeLock ScopeLock(SynchronizationObject);
|
|
const uint32 PathHash = GetTypeHash(Path);
|
|
return CacheItems.ContainsByHash(PathHash, Path);
|
|
}
|
|
|
|
class FCompressedPakFileCacheStore final : public FPakFileCacheStore
|
|
{
|
|
public:
|
|
using FPakFileCacheStore::FPakFileCacheStore;
|
|
|
|
void Put(
|
|
TConstArrayView<FCachePutRequest> Requests,
|
|
IRequestOwner& Owner,
|
|
FOnCachePutComplete&& OnComplete) final;
|
|
|
|
void PutValue(
|
|
TConstArrayView<FCachePutValueRequest> Requests,
|
|
IRequestOwner& Owner,
|
|
FOnCachePutValueComplete&& OnComplete) final;
|
|
|
|
private:
|
|
static const ECompressedBufferCompressor RequiredCompressor = ECompressedBufferCompressor::Kraken;
|
|
static const ECompressedBufferCompressionLevel MinRequiredCompressionLevel = ECompressedBufferCompressionLevel::Optimal2;
|
|
|
|
static FValue Compress(const FValue& Value);
|
|
};
|
|
|
|
void FCompressedPakFileCacheStore::Put(
|
|
const TConstArrayView<FCachePutRequest> Requests,
|
|
IRequestOwner& Owner,
|
|
FOnCachePutComplete&& OnComplete)
|
|
{
|
|
Owner.LaunchTask(TEXT("PakFileDDC_Put"),
|
|
[this, &Owner, Requests = TArray<FCachePutRequest, TInlineAllocator<1>>(Requests), OnComplete = MoveTemp(OnComplete)]() mutable
|
|
{
|
|
for (FCachePutRequest& Request : Requests)
|
|
{
|
|
FCacheRecordBuilder Builder(Request.Record.GetKey());
|
|
Builder.SetMeta(CopyTemp(Request.Record.GetMeta()));
|
|
for (const FValueWithId& Value : Request.Record.GetValues())
|
|
{
|
|
Builder.AddValue(Value.GetId(), Compress(Value));
|
|
}
|
|
Request.Record = Builder.Build();
|
|
}
|
|
Private::LaunchTaskInCacheThreadPool(Owner,
|
|
[this, &Owner, Requests = MoveTemp(Requests), OnComplete = MoveTemp(OnComplete)]() mutable
|
|
{
|
|
FPakFileCacheStore::Put(Requests, Owner, MoveTemp(OnComplete));
|
|
});
|
|
});
|
|
}
|
|
|
|
void FCompressedPakFileCacheStore::PutValue(
|
|
const TConstArrayView<FCachePutValueRequest> Requests,
|
|
IRequestOwner& Owner,
|
|
FOnCachePutValueComplete&& OnComplete)
|
|
{
|
|
Owner.LaunchTask(TEXT("PakFileDDC_PutValue"),
|
|
[this, &Owner, Requests = TArray<FCachePutValueRequest, TInlineAllocator<1>>(Requests), OnComplete = MoveTemp(OnComplete)]() mutable
|
|
{
|
|
for (FCachePutValueRequest& Request : Requests)
|
|
{
|
|
Request.Value = Compress(Request.Value);
|
|
}
|
|
Private::LaunchTaskInCacheThreadPool(Owner,
|
|
[this, &Owner, Requests = MoveTemp(Requests), OnComplete = MoveTemp(OnComplete)]() mutable
|
|
{
|
|
FPakFileCacheStore::PutValue(Requests, Owner, MoveTemp(OnComplete));
|
|
});
|
|
});
|
|
}
|
|
|
|
FValue FCompressedPakFileCacheStore::Compress(const FValue& Value)
|
|
{
|
|
uint64 BlockSize = 0;
|
|
ECompressedBufferCompressor Compressor;
|
|
ECompressedBufferCompressionLevel CompressionLevel;
|
|
if (!Value.HasData() ||
|
|
(Value.GetData().TryGetCompressParameters(Compressor, CompressionLevel, BlockSize) &&
|
|
Compressor == RequiredCompressor &&
|
|
CompressionLevel >= MinRequiredCompressionLevel))
|
|
{
|
|
return Value;
|
|
}
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(PakFileDDC_Compress);
|
|
const FCompositeBuffer Data = Value.GetData().DecompressToComposite();
|
|
return FValue(FCompressedBuffer::Compress(Data, RequiredCompressor, MinRequiredCompressionLevel, BlockSize));
|
|
}
|
|
|
|
IPakFileCacheStore* CreatePakFileCacheStore(const TCHAR* Name, const TCHAR* CachePath, bool bWriting, bool bCompressed, ICacheStoreOwner* Owner)
|
|
{
|
|
return bCompressed ? new FCompressedPakFileCacheStore(Name, CachePath, bWriting, Owner) : new FPakFileCacheStore(Name, CachePath, bWriting, Owner);
|
|
}
|
|
|
|
} // UE::DerivedData
|