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

455 lines
13 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "PackageRehydrationProcess.h"
#include "HAL/FileManager.h"
#include "Internationalization/Internationalization.h"
#include "Misc/PackageName.h"
#include "Misc/PackagePath.h"
#include "Misc/ScopeExit.h"
#include "Misc/ScopedSlowTask.h"
#include "PackageUtils.h"
#include "Serialization/MemoryArchive.h"
#include "UObject/Linker.h"
#include "UObject/Package.h"
#include "UObject/PackageTrailer.h"
#include "UObject/UObjectGlobals.h"
#include "Virtualization/VirtualizationSystem.h"
#include "VirtualizationSourceControlUtilities.h"
#define LOCTEXT_NAMESPACE "Virtualization"
namespace UE::Virtualization
{
/**
* Archive allowing serialization to a fixed sized memory buffer. Exceeding the
* length of the buffer will cause the archive to log a warning and set its
* error state to true.
*/
class FFixedBufferWriterArchive : public FMemoryArchive
{
public:
FFixedBufferWriterArchive() = delete;
/**
* @param InBuffer Pointer to the buffer to write to
* @param InLength Total length (in bytes) that can be written to the buffer
*/
FFixedBufferWriterArchive(uint8* InBuffer, int64 InLength)
: Buffer(InBuffer)
, Length(InLength)
{
SetIsSaving(true);
}
virtual ~FFixedBufferWriterArchive() = default;
/** Returns how much space remains in the internal buffer. */
int64 GetRemainingSpace() const
{
return Length - Offset;
}
private:
virtual void Serialize(void* Data, int64 Num) override
{
if (Offset + Num <= Length)
{
FMemory::Memcpy(&Buffer[Offset], Data, Num);
Offset += Num;
}
else
{
UE_LOG(LogSerialization, Error, TEXT("Attempting to write %lld bytes to a FFixedBufferWriterArchive with only %lld bytes of space remaining"), Num, GetRemainingSpace());
SetError();
}
}
uint8* Buffer = nullptr;
int64 Length = INDEX_NONE;
};
/** Utility for opening a package for reading, including localized error handling */
TUniquePtr<FArchive> OpenFileForReading(const FString& FilePath, TArray<FText>& OutErrors)
{
TUniquePtr<FArchive> FileHandle(IFileManager::Get().CreateFileReader(*FilePath));
if (!FileHandle)
{
FText Message = FText::Format(LOCTEXT("VAHydration_OpenFailed", "Unable to open the file '{0}' for reading"),
FText::FromString(FilePath));
OutErrors.Add(MoveTemp(Message));
}
return FileHandle;
}
/** Utility for serializing data from a package, including localized error handling */
bool ReadData(TUniquePtr<FArchive>& Ar, void* DstBuffer, int64 BytesToRead, const FString& FilePath, TArray<FText>& OutErrors)
{
check(Ar.IsValid());
Ar->Serialize(DstBuffer, BytesToRead);
if (!Ar->IsError())
{
return true;
}
else
{
FText Message = FText::Format(LOCTEXT("VAHydration_ReadFailed", "Failed to read {0} bytes from file {1}"),
BytesToRead,
FText::FromString(FilePath));
OutErrors.Add(MoveTemp(Message));
return false;
}
}
/** Shared rehydration code */
bool TryRehydrateBuilder(const FString& FilePath, FPackageTrailer& OutTrailer, FPackageTrailerBuilder& OutBuilder, TArray<FText>& OutErrors)
{
if (!FPackageName::IsPackageFilename(FilePath))
{
return false; // Only rehydrate valid packages
}
if (!FPackageTrailer::TryLoadFromFile(FilePath, OutTrailer))
{
return false; // Only rehydrate packages with package trailers
}
TArray<FIoHash> VirtualizedPayloads = OutTrailer.GetPayloads(EPayloadStorageType::Virtualized);
if (VirtualizedPayloads.IsEmpty())
{
return false; // If the package has no virtualized payloads then we can skip the rest
}
{
TUniquePtr<FArchive> FileAr = OpenFileForReading(FilePath, OutErrors);
if (!FileAr)
{
return false;
}
OutBuilder = FPackageTrailerBuilder::CreateFromTrailer(OutTrailer, *FileAr, FilePath);
}
int32 PayloadsHydrated = 0;
IVirtualizationSystem& System = IVirtualizationSystem::Get();
TArray<FPullRequest> Requests;
Requests.Reserve(VirtualizedPayloads.Num());
for (const FIoHash& Id : VirtualizedPayloads)
{
Requests.Emplace(FPullRequest(Id));
}
if (!System.PullData(Requests))
{
FText Message = FText::Format(LOCTEXT("VAHydration_PullFailed", "Unable to pull the data for the package '{0}'"),
FText::FromString(FilePath));
OutErrors.Add(Message);
return false;
}
for (const FPullRequest& Request : Requests)
{
if (OutBuilder.UpdatePayloadAsLocal(Request.GetIdentifier(), Request.GetPayload()))
{
PayloadsHydrated++;
}
else
{
FText Message = FText::Format(LOCTEXT("VAHydration_UpdateStatusFailed", "Unable to update the status for the payload '{0}' in the package '{1}'"),
FText::FromString(LexToString(Request.GetIdentifier())),
FText::FromString(FilePath));
OutErrors.Add(Message);
return false;
}
}
check(OutBuilder.GetNumVirtualizedPayloads() == 0);
return PayloadsHydrated > 0;
}
void RehydratePackages(TConstArrayView<FString> PackagePaths, ERehydrationOptions Options, FRehydrationResult& OutResult)
{
TRACE_CPUPROFILER_EVENT_SCOPE(UE::Virtualization::RehydratePackagesOnDisk);
IVirtualizationSystem& System = IVirtualizationSystem::Get();
if (!System.IsEnabled())
{
return;
}
const double StartTime = FPlatformTime::Seconds();
ON_SCOPE_EXIT
{
OutResult.TimeTaken = FPlatformTime::Seconds() - StartTime;
UE_LOG(LogVirtualization, Display, TEXT("Rehydration process complete"));
UE_LOG(LogVirtualization, Verbose, TEXT("Rehydration process took %.3f(s)"), OutResult.TimeTaken);
};
FScopedSlowTask Progress(1.0f, LOCTEXT("VAHydration_TaskOnDisk", "Re-hydrating Assets On Disk..."));
Progress.MakeDialog();
TArray<TPair<FString, FString>> PackagesToReplace;
// TODO: Should we consider a way to batch this so many payloads can be downloaded at once?
// Running this over a large project would be much faster if we could batch. An easy way
// to do this might be to gather all of the payloads needed and do a prefetch first?
double Time = FPlatformTime::Seconds();
TSet<FString> ConsideredPackages;
ConsideredPackages.Reserve(PackagePaths.Num());
// Attempt to rehydrate the packages
for(int32 Index = 0; Index < PackagePaths.Num(); ++Index)
{
const FString& FilePath = PackagePaths[Index];
bool bIsDuplicate = false;
ConsideredPackages.Add(FilePath, &bIsDuplicate);
if (bIsDuplicate)
{
UE_LOG(LogVirtualization, Verbose, TEXT("Skipping duplicate package entry '%s'"), *FilePath);
continue; // Skip duplicate packages
}
FPackageTrailer Trailer;
FPackageTrailerBuilder Builder;
if (TryRehydrateBuilder(FilePath, Trailer, Builder, OutResult.Errors))
{
FString NewPackagePath = DuplicatePackageWithNewTrailer(FilePath, Trailer, Builder, OutResult.Errors);
if (!NewPackagePath.IsEmpty())
{
PackagesToReplace.Emplace(FilePath, MoveTemp(NewPackagePath));
}
else
{
// Error?
return;
}
}
if (FPlatformTime::Seconds() - Time > 10.0)
{
const float ProgressPercent = ((float)Index / (float)PackagePaths.Num()) * 100.0f;
UE_LOG(LogVirtualization, Display, TEXT("%d/%d - %.1f%%"), Index, PackagePaths.Num(), ProgressPercent);
Time = FPlatformTime::Seconds();
}
}
const int32 NumSkippedPackages = PackagePaths.Num() - ConsideredPackages.Num();
ConsideredPackages.Empty();
UE_CLOG(NumSkippedPackages > 0, LogVirtualization, Warning, TEXT("Discarded %d duplicate package paths"), NumSkippedPackages);
// We need to reset the loader of any loaded package that should have its package file replaced
for (const TPair<FString, FString>& Pair : PackagesToReplace)
{
const FString& OriginalFilePath = Pair.Key;
FString PackageName;
if (FPackageName::TryConvertFilenameToLongPackageName(OriginalFilePath, PackageName))
{
UPackage* Package = FindObjectFast<UPackage>(nullptr, *PackageName);
if (Package != nullptr)
{
UE_LOG(LogVirtualization, Verbose, TEXT("Detaching '%s' from disk so that it can be rehydrated"), *OriginalFilePath);
ResetLoadersForSave(Package, *OriginalFilePath);
}
}
}
// Should we try to check out packages from revision control?
if (EnumHasAnyFlags(Options, ERehydrationOptions::Checkout))
{
TArray<FString> FilesToCheckState;
FilesToCheckState.Reserve(PackagesToReplace.Num());
for (const TPair<FString, FString>& Pair : PackagesToReplace)
{
FilesToCheckState.Add(Pair.Key);
}
if (!TryCheckoutFiles(FilesToCheckState, OutResult.Errors, &OutResult.CheckedOutPackages))
{
return;
}
}
int64 OriginalSizeTotal = 0;
int64 HydratedSizeTotal = 0;
for (const TPair<FString, FString>& Pair : PackagesToReplace)
{
const FString& OriginalFilePath = Pair.Key;
const FString& NewPackagePath = Pair.Value;
if (CanWriteToFile(OriginalFilePath))
{
const int64 OriginalSize = IFileManager::Get().GetStatData(*OriginalFilePath).FileSize;
const int64 HydratedSize = IFileManager::Get().GetStatData(*NewPackagePath).FileSize;
if (IFileManager::Get().Move(*OriginalFilePath, *NewPackagePath))
{
OutResult.RehydratedPackages.Add(OriginalFilePath);
OriginalSizeTotal += OriginalSize;
HydratedSizeTotal += HydratedSize;
// Could consider path name here to match the virtualization process.
UE_LOG(LogVirtualization, Verbose, TEXT("Hydrating %s: %s -> %s"),
*OriginalFilePath,
*FText::AsMemory(OriginalSize).ToString(),
*FText::AsMemory(HydratedSize).ToString());
}
else
{
FText Message = FText::Format(LOCTEXT("VAHydration_MoveFailed", "Unable to replace the package '{0}' with the hydrated version"),
FText::FromString(OriginalFilePath));
OutResult.AddError(MoveTemp(Message));
return;
}
}
else
{
FText Message = FText::Format(
LOCTEXT("VAHydration_PackageLocked", "The package file '{0}' has virtualized payloads but is locked for modification and cannot be hydrated"),
FText::FromString(OriginalFilePath));
OutResult.AddError(MoveTemp(Message));
return;
}
}
if (OriginalSizeTotal != HydratedSizeTotal)
{
UE_LOG(LogVirtualization, Display, TEXT("Total hydration increase %s (%s -> %s)"),
*FText::AsMemory(HydratedSizeTotal - OriginalSizeTotal).ToString(),
*FText::AsMemory(OriginalSizeTotal).ToString(),
*FText::AsMemory(HydratedSizeTotal).ToString());
}
UE_LOG(LogVirtualization, Display, TEXT("Hydrated %d package(s)"), OutResult.RehydratedPackages.Num());
}
void RehydratePackages(TConstArrayView<FString> PackagePaths, uint64 PaddingAlignment, TArray<FText>& OutErrors, TArray<FSharedBuffer>& OutPackages, TArray<FRehydrationInfo>* OutInfo)
{
TRACE_CPUPROFILER_EVENT_SCOPE(UE::Virtualization::RehydratePackagesInMem);
IVirtualizationSystem& System = IVirtualizationSystem::Get();
FScopedSlowTask Progress(1.0f, LOCTEXT("VAHydration_TaskInMem", "Re-hydrating Assets In Memory..."));
Progress.MakeDialog();
OutPackages.Empty(PackagePaths.Num());
if (OutInfo != nullptr)
{
OutInfo->Empty(PackagePaths.Num());
}
for (const FString& FilePath : PackagePaths)
{
FUniqueBuffer FileBuffer;
FPackageTrailer Trailer;
FPackageTrailerBuilder Builder;
if (System.IsEnabled() && TryRehydrateBuilder(FilePath, Trailer, Builder, OutErrors))
{
TUniquePtr<FArchive> FileAr = OpenFileForReading(FilePath, OutErrors);
if (!FileAr)
{
return;
}
const int64 OriginalFileLength = FileAr->TotalSize();
const int64 TruncatedFileLength = OriginalFileLength - Trailer.GetTrailerLength();
const int64 RehydratedTrailerLength = (int64)Builder.CalculateTrailerLength();
const int64 RehydratedFileLength = TruncatedFileLength + RehydratedTrailerLength;
const int64 BufferLength = Align(RehydratedFileLength, PaddingAlignment);
FileBuffer = FUniqueBuffer::Alloc((uint64)BufferLength);
if (!ReadData(FileAr, FileBuffer.GetData(), TruncatedFileLength, FilePath, OutErrors))
{
return;
}
FFixedBufferWriterArchive Ar((uint8*)FileBuffer.GetData() + TruncatedFileLength, RehydratedTrailerLength);
if (!Builder.BuildAndAppendTrailer(nullptr, Ar))
{
FText Message = FText::Format(LOCTEXT("VAHydration_TrailerFailed", "Failed to create a new trailer for file {0}"),
FText::FromString(FilePath));
OutErrors.Add(MoveTemp(Message));
return;
}
check(Ar.GetRemainingSpace() == 0);
if (OutInfo != nullptr)
{
FRehydrationInfo& Info = OutInfo->AddDefaulted_GetRef();
Info.OriginalSize = OriginalFileLength;
Info.RehydratedSize = RehydratedFileLength;
Info.NumPayloadsRehydrated = Builder.GetNumLocalPayloads() - Trailer.GetNumPayloads(EPayloadStorageType::Local);
}
}
else
{
TUniquePtr<FArchive> FileAr = OpenFileForReading(FilePath, OutErrors);
if (!FileAr)
{
return;
}
const int64 FileLength = FileAr->TotalSize();
const int64 BufferLength = Align(FileLength, PaddingAlignment);
FileBuffer = FUniqueBuffer::Alloc((uint64)BufferLength);
if (!ReadData(FileAr, FileBuffer.GetData(), FileLength, FilePath, OutErrors))
{
return;
}
if (OutInfo != nullptr)
{
FRehydrationInfo& Info = OutInfo->AddDefaulted_GetRef();
Info.OriginalSize = FileLength;
Info.RehydratedSize = FileLength;
Info.NumPayloadsRehydrated = 0;
}
}
OutPackages.Add(FileBuffer.MoveToShared());
}
// Make sure the arrays that we return have an entry per requested file
check(PackagePaths.Num() == OutPackages.Num());
check(OutInfo == nullptr || PackagePaths.Num() == OutInfo->Num());
}
} // namespace UE::Virtualization
#undef LOCTEXT_NAMESPACE