Files
UnrealEngine/Engine/Source/Developer/DerivedDataCache/Private/PakFileCacheStore.cpp
2025-05-18 13:04:45 +08:00

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