// 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 OpenFileForReading(const FString& FilePath, TArray& OutErrors) { TUniquePtr 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& Ar, void* DstBuffer, int64 BytesToRead, const FString& FilePath, TArray& 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& 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 VirtualizedPayloads = OutTrailer.GetPayloads(EPayloadStorageType::Virtualized); if (VirtualizedPayloads.IsEmpty()) { return false; // If the package has no virtualized payloads then we can skip the rest } { TUniquePtr FileAr = OpenFileForReading(FilePath, OutErrors); if (!FileAr) { return false; } OutBuilder = FPackageTrailerBuilder::CreateFromTrailer(OutTrailer, *FileAr, FilePath); } int32 PayloadsHydrated = 0; IVirtualizationSystem& System = IVirtualizationSystem::Get(); TArray 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 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> 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 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& Pair : PackagesToReplace) { const FString& OriginalFilePath = Pair.Key; FString PackageName; if (FPackageName::TryConvertFilenameToLongPackageName(OriginalFilePath, PackageName)) { UPackage* Package = FindObjectFast(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 FilesToCheckState; FilesToCheckState.Reserve(PackagesToReplace.Num()); for (const TPair& Pair : PackagesToReplace) { FilesToCheckState.Add(Pair.Key); } if (!TryCheckoutFiles(FilesToCheckState, OutResult.Errors, &OutResult.CheckedOutPackages)) { return; } } int64 OriginalSizeTotal = 0; int64 HydratedSizeTotal = 0; for (const TPair& 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 PackagePaths, uint64 PaddingAlignment, TArray& OutErrors, TArray& OutPackages, TArray* 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 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 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