// Copyright Epic Games, Inc. All Rights Reserved. #include "IoStoreUtilities.h" #include "IoStoreWriter.h" #include "IoStoreLooseFiles.h" #include "Algo/BinarySearch.h" #include "Algo/TopologicalSort.h" #include "Async/AsyncWork.h" #include "CookedPackageStore.h" #include "CookMetadata.h" #include "CookMetadataFiles.h" #include "DerivedDataCache.h" #include "DerivedDataCacheInterface.h" #include "GenericPlatform/GenericPlatformMisc.h" #include "HAL/FileManager.h" #include "HAL/PlatformFileManager.h" #include "HAL/PlatformMemory.h" #include "Hash/CityHash.h" #include "Hash/xxhash.h" #include "Interfaces/ITargetPlatformManagerModule.h" #include "Interfaces/ITargetPlatform.h" #include "IO/IoDispatcher.h" #include "Misc/App.h" #include "Misc/CommandLine.h" #include "Misc/ConfigCacheIni.h" #include "Misc/Optional.h" #include "Misc/PackageName.h" #include "Misc/Paths.h" #include "Misc/Base64.h" #include "Misc/AES.h" #include "Misc/CoreDelegates.h" #include "Misc/KeyChainUtilities.h" #include "Misc/WildcardString.h" #include "Modules/ModuleManager.h" #include "Serialization/Archive.h" #include "Serialization/BulkData.h" #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" #include "Serialization/JsonWriter.h" #include "Serialization/BufferWriter.h" #include "Serialization/LargeMemoryWriter.h" #include "Serialization/MemoryReader.h" #include "Serialization/AsyncLoading2.h" #include "Serialization/ArrayReader.h" #include "Serialization/ArrayWriter.h" #include "Settings/ProjectPackagingSettings.h" // for EAssetRegistryWritebackMethod #include "Serialization/PackageStore.h" #include "UObject/Class.h" #include "UObject/NameBatchSerialization.h" #include "UObject/PackageFileSummary.h" #include "UObject/ObjectResource.h" #include "UObject/Package.h" #include "UObject/UObjectHash.h" #include "Algo/AnyOf.h" #include "Algo/Find.h" #include "Misc/FileHelper.h" #include "Misc/ScopeLock.h" #include "Async/ParallelFor.h" #include "Async/AsyncFileHandle.h" #include "Async/Async.h" #include "RSA.h" #include "Misc/AssetRegistryInterface.h" #include "AssetRegistry/AssetRegistryState.h" #include "Misc/OutputDeviceFile.h" #include "Misc/FeedbackContext.h" #include "Serialization/LargeMemoryReader.h" #include "Misc/StringBuilder.h" #include "Async/Future.h" #include "Algo/MaxElement.h" #include "Algo/Sort.h" #include "Algo/StableSort.h" #include "Algo/IsSorted.h" #include "PackageStoreOptimizer.h" #include "ShaderCodeArchive.h" #include "ZenStoreHttpClient.h" #include "IPlatformFilePak.h" #include "ZenCookArtifactReader.h" #include "ZenStoreWriter.h" #include "IO/IoContainerHeader.h" #include "ProfilingDebugging/CountersTrace.h" #include "IO/IoStore.h" #include "ZenFileSystemManifest.h" #include "IPlatformFileSandboxWrapper.h" #include "Misc/PathViews.h" #include "HAL/FileManagerGeneric.h" #include "Serialization/CompactBinarySerialization.h" #include "Serialization/ZenPackageHeader.h" #include "String/ParseTokens.h" #include "Tasks/Task.h" IMPLEMENT_MODULE(FDefaultModuleImpl, IoStoreUtilities); #define IOSTORE_CPU_SCOPE(NAME) TRACE_CPUPROFILER_EVENT_SCOPE(IoStore##NAME); #define IOSTORE_CPU_SCOPE_DATA(NAME, DATA) TRACE_CPUPROFILER_EVENT_SCOPE(IoStore##NAME); TRACE_DECLARE_MEMORY_COUNTER(IoStoreSourceReadsUsedBufferMemory, TEXT("IoStoreWriter/SourceReadsUsedBufferMemory")); TRACE_DECLARE_ATOMIC_INT_COUNTER(IoStoreSourceReadsInflight, TEXT("IoStoreWriter/SourceReadsInflight")); TRACE_DECLARE_ATOMIC_INT_COUNTER(IoStoreSourceReadsDone, TEXT("IoStoreWriter/SourceReadsDone")); // Helper to format numbers with comma separators to help readability: 1,234 vs 1234. static FString NumberString(uint64 N) { return FText::AsNumber(N).ToString(); } // Used for tracking how the chunk will get deployed and thus what classification its size is. struct FIoStoreChunkSource { FIoStoreTocChunkInfo ChunkInfo; UE::Cook::EPluginSizeTypes SizeType; }; static FPackageId PackageIdFromChunkId(const FIoChunkId& ChunkId) { return FPackageId::FromValue(*(int64*)(ChunkId.GetData())); } static const FName DefaultCompressionMethod = NAME_Zlib; static const uint64 DefaultCompressionBlockSize = 64 << 10; static const uint64 DefaultCompressionBlockAlignment = 64 << 10; static const uint64 DefaultMemoryMappingAlignment = 16 << 10; static TUniquePtr CreateIoStoreReader(const TCHAR* Path, const FKeyChain& KeyChain); /* * Provides access to previously compressed chunks to the iostore writer, allowing * a) avoiding recompressing things and b) tweaks to compressors dont cause massive patches. */ class FIoStoreChunkDatabase : public IIoStoreWriterReferenceChunkDatabase { public: static const uint8 IoChunkTypeCount = (uint8)EIoChunkType::MAX; struct FRequestedChunkInfo { FIoChunkId Id; FIoHash ChunkHash; }; TArray> Readers; struct FReaderChunks { FString ContainerPathAndFileName; FString ContainerName; FIoContainerId ContainerId; int32 ReaderIndex = -1; TMap ChunkIds; // maps to index for chunk specific structures TArray ChangedChunkHashes; TArray ChunkInfos; TArray ChunkChanged; ~FReaderChunks() = default; FReaderChunks() = default; FReaderChunks(FReaderChunks&&) = delete; FReaderChunks(FReaderChunks&) = delete; FReaderChunks& operator=(FReaderChunks&) = delete; FCriticalSection NewChunksLock; TArray NewChunks; uint64 NewChunksByType[IoChunkTypeCount] = { 0 }; std::atomic_uint64_t FulfillBytes = 0; std::atomic_uint64_t TotalRequestCount = 0; std::atomic_uint64_t ChangedCountByType[IoChunkTypeCount] = {0}; std::atomic_uint64_t UsedChunksByType[IoChunkTypeCount] = {0}; }; struct FMissingContainerInfo { FMissingContainerInfo(FString InContainerName) {ContainerName = InContainerName;} TArray Chunks; FCriticalSection ChunksLock; FString ContainerName; std::atomic_uint64_t TotalRequestCount = 0; std::atomic_uint64_t RequestedChunkCount[IoChunkTypeCount] = {0}; }; // Since we are notified of all containers, every id should be in our database, // or in the missing list. TMap> ChunkDatabase; TMap> MissingContainers; std::atomic_int64_t MatchCount = 0; std::atomic_int64_t RequestCount = 0; int32 ContainerNotFound = 0; // Bytes we actually delivered to the iostore writer int64 FulfillBytes = 0; int64 FulfillBytesPerChunk[(int8)EIoChunkType::MAX] = {}; uint32 CompressionBlockSize = 0; bool bValid = false; bool Init(FString InGlobalContainerFileName, FString InAdditionalContainersPath, const FKeyChain& InDecryptionKeychain) { double StartTime = FPlatformTime::Seconds(); // Try and catch common pathing mistakes since honestly we only care about the path even though we ask // for the global.utoc. FString ContainersDirectory = FPaths::GetPath(MoveTemp(InGlobalContainerFileName)); FPaths::NormalizeDirectoryName(ContainersDirectory); FString GlobalFileName = ContainersDirectory / TEXT("global.utoc"); TUniquePtr GlobalReader = CreateIoStoreReader(*GlobalFileName, InDecryptionKeychain); if (GlobalReader.IsValid() == false) { UE_LOG(LogIoStore, Error, TEXT("Failed to open reference chunk container %s"), *GlobalFileName); return false; } FPaths::NormalizeDirectoryName(InAdditionalContainersPath); TArray ContainerFilePaths; TArray FoundContainerFiles; { IFileManager::Get().FindFiles(FoundContainerFiles, *(ContainersDirectory / TEXT("*.utoc")), true, false); for (const FString& Filename : FoundContainerFiles) { ContainerFilePaths.Emplace(ContainersDirectory / Filename); } } if (InAdditionalContainersPath.Len()) { FoundContainerFiles.Empty(); IFileManager::Get().FindFiles(FoundContainerFiles, *(InAdditionalContainersPath / TEXT("*.utoc")), true, false); for (const FString& Filename : FoundContainerFiles) { ContainerFilePaths.Emplace(InAdditionalContainersPath / Filename); } } CompressionBlockSize = 0; int64 IoChunkCount = 0; for (FString& ContainerFilePath : ContainerFilePaths) { TUniquePtr Reader = CreateIoStoreReader(*ContainerFilePath, InDecryptionKeychain); if (Reader.IsValid() == false) { UE_LOG(LogIoStore, Error, TEXT("Failed to open reference chunk container %s"), *ContainerFilePath); return false; } // Work around the fact that we recently changed container ids to be unique and detect .o containers // and regenerate the Id. FIoContainerId ContainerId = Reader->GetContainerId(); if (ContainerFilePath.Contains(FPackagePath::GetOptionalSegmentExtensionModifier())) { FIoContainerId CheckContainerId = FIoContainerId::FromName(*FPaths::GetBaseFilename(ContainerFilePath)); if (CheckContainerId != ContainerId) { UE_LOG(LogIoStore, Display, TEXT("Reference Chunk: adjusting optional segment container id (0x%llx -> 0x%llx) for %s"), ContainerId.Value(), CheckContainerId.Value(), *ContainerFilePath); ContainerId = CheckContainerId; } } TUniquePtr* ExistingContainer = ChunkDatabase.Find(ContainerId); if (ExistingContainer) { UE_LOG(LogIoStore, Error, TEXT("Reference chunk database loaded containers with the same id:")); UE_LOG(LogIoStore, Error, TEXT(" %s"), *ExistingContainer[0]->ContainerPathAndFileName); UE_LOG(LogIoStore, Error, TEXT(" %s"), *ContainerFilePath); return false; } TUniquePtr ReaderChunks(new FReaderChunks()); ReaderChunks->ContainerName = Reader->GetContainerName(); ReaderChunks->ContainerPathAndFileName = MoveTemp(ContainerFilePath); ReaderChunks->ContainerId = ContainerId; int32 ChunkCount = Reader->GetChunkCount(); ReaderChunks->ChunkInfos.Reserve(ChunkCount); ReaderChunks->ChangedChunkHashes.SetNumZeroed(ChunkCount); uint32 ChunkIndex = 0; Reader->EnumerateChunks([ReaderChunks = ReaderChunks.Get(), &ChunkIndex](FIoStoreTocChunkInfo&& ChunkInfo) { ReaderChunks->ChunkIds.Add(ChunkInfo.Id, ChunkIndex); ReaderChunks->ChunkInfos.Add(MoveTemp(ChunkInfo)); ChunkIndex++; return true; }); ReaderChunks->ChunkChanged.SetNumZeroed(ReaderChunks->ChunkIds.Num()); if (Readers.Num() == 0) { CompressionBlockSize = Reader->GetCompressionBlockSize(); } else if (Reader->GetCompressionBlockSize() != CompressionBlockSize) { UE_LOG(LogIoStore, Error, TEXT("Reference chunk containers had different compression block sizes, failing to init reference db (%u and %u)"), CompressionBlockSize, Reader->GetCompressionBlockSize()); return false; } IoChunkCount += ChunkCount; ReaderChunks->ReaderIndex = Readers.Num(); ChunkDatabase.Add(ReaderChunks->ContainerId, MoveTemp(ReaderChunks)); Readers.Add(MoveTemp(Reader)); } UE_LOG(LogIoStore, Display, TEXT("Reference Chunk loaded %d containers and %s chunks, in %.1f seconds"), Readers.Num(), *FText::AsNumber(IoChunkCount).ToString(), FPlatformTime::Seconds() - StartTime); bValid = true; return true; } virtual void NotifyAddedToWriter(const FIoContainerId& InContainerId, const FString& InContainerName) override { // If we don't have this container in our chunk database, add it to a tracking // structure so we can see how many chunks we're missing. if (ChunkDatabase.Contains(InContainerId) == false) { TUniquePtr MissingContainer = MakeUnique(InContainerName); MissingContainers.Add(InContainerId, MoveTemp(MissingContainer)); } } virtual uint32 GetCompressionBlockSize() const override { return CompressionBlockSize; } // Returns whether we expect to be able to load the chunk from the reference chunk database. // This can be called from any thread, though in the presence of existing hashes it's single threaded. virtual bool ChunkExists(const FIoContainerId& InContainerId, const FIoHash& InChunkHash, const FIoChunkId& InChunkId, int32& OutNumChunkBlocks) { if (!bValid) { return false; } RequestCount.fetch_add(1, std::memory_order_relaxed); uint8 ChunkType = (uint8)InChunkId.GetChunkType(); FRequestedChunkInfo RCI; RCI.ChunkHash = InChunkHash; RCI.Id = InChunkId; TUniquePtr* ReaderChunksPtr = ChunkDatabase.Find(InContainerId); if (ReaderChunksPtr == nullptr) { // Container doesn't exist - could have been added/created or maybe wrong source project. TUniquePtr* MissingContainerPtr = MissingContainers.Find(InContainerId); if (MissingContainerPtr == nullptr) { UE_LOG(LogIoStore, Warning, TEXT("We got a container that was never added! Id = 0x%llx"), InContainerId.Value()); } else { // We expect most chunks to have containers so we don't stress about this lock too much FMissingContainerInfo* MissingContainer = MissingContainerPtr->Get(); { FScopeLock Locker(&MissingContainer->ChunksLock); MissingContainer->Chunks.Add(MoveTemp(RCI)); } MissingContainer->RequestedChunkCount[ChunkType].fetch_add(1, std::memory_order_relaxed); if (MissingContainer->TotalRequestCount.fetch_add(1, std::memory_order_relaxed) == 0) { ContainerNotFound++; } } return false; } // We have the container at least. FReaderChunks* ReaderChunks = ReaderChunksPtr->Get(); ReaderChunks->TotalRequestCount.fetch_add(1, std::memory_order_relaxed); uint32_t* ChunkIndex = ReaderChunks->ChunkIds.Find(InChunkId); if (ChunkIndex == nullptr) { // Chunk doesn't exist - it's entirely new. // We expect most chunks to already exist so we don't stress about this lock too much FScopeLock Locker(&ReaderChunks->NewChunksLock); ReaderChunks->NewChunksByType[ChunkType]++; ReaderChunks->NewChunks.Add(MoveTemp(RCI)); return false; } // We have the chunk - does the hash match? const FIoStoreTocChunkInfo& ChunkInfo = ReaderChunks->ChunkInfos[*ChunkIndex]; if (ChunkInfo.ChunkHash != InChunkHash) { // Chunk exists, but it's changed. ReaderChunks->ChangedCountByType[ChunkType].fetch_add(1, std::memory_order_relaxed); ReaderChunks->ChunkChanged[*ChunkIndex].store(1, std::memory_order_relaxed); // This should be safe because chunk ids are unique within a container, // so we are the only ones poking at this index. ReaderChunks->ChangedChunkHashes[*ChunkIndex] = MoveTemp(RCI.ChunkHash); return false; } // We match. MatchCount.fetch_add(1, std::memory_order_relaxed); ReaderChunks->UsedChunksByType[ChunkType].fetch_add(1, std::memory_order_relaxed); OutNumChunkBlocks = IntCastChecked(ChunkInfo.NumCompressedBlocks); return true; } // This function was not written to be thread safe as it's only ever called from // the iostore begindispatch thread (i.e. is not async) virtual UE::Tasks::FTask RetrieveChunk(const FIoContainerId& InContainerId, const FIoHash& InChunkHash, const FIoChunkId& InChunkId, TUniqueFunction)> InCompleteCallback) { if (!bValid) { InCompleteCallback(FIoStatus(EIoErrorCode::InvalidCode, TEXT("IoStoreChunkDatabase not initialized"))); return UE::Tasks::MakeCompletedTask(); } TUniquePtr* ReaderChunksPtr = ChunkDatabase.Find(InContainerId); if (ReaderChunksPtr == nullptr) { // This should never happen now as we wrap this in a ChunkExists call. InCompleteCallback(FIoStatus(EIoErrorCode::InvalidCode, TEXT("RetrieveChunk can't find the container - invariant violated!"))); return UE::Tasks::MakeCompletedTask(); } FReaderChunks* ReaderChunks = ReaderChunksPtr->Get(); uint32_t* ChunkIndex = ReaderChunks->ChunkIds.Find(InChunkId); if (ChunkIndex == nullptr) { // This should never happen now as we wrap this in a ChunkExists call. InCompleteCallback(FIoStatus(EIoErrorCode::InvalidCode, TEXT("RetrieveChunk can't find the chunk - invariant violated!"))); return UE::Tasks::MakeCompletedTask(); } FIoStoreTocChunkInfo& ChunkInfo = ReaderChunks->ChunkInfos[*ChunkIndex]; uint64 TotalCompressedSize = 0; uint64 TotalUncompressedSize = 0; uint32 CompressedBlockCount = 0; Readers[ReaderChunks->ReaderIndex]->EnumerateCompressedBlocksForChunk(InChunkId, [&TotalUncompressedSize, &CompressedBlockCount, &TotalCompressedSize](const FIoStoreTocCompressedBlockInfo& BlockInfo) { TotalCompressedSize += BlockInfo.CompressedSize; TotalUncompressedSize += BlockInfo.UncompressedSize; CompressedBlockCount ++; return true; }); FulfillBytesPerChunk[(int8)ChunkInfo.ChunkType] += TotalCompressedSize; FulfillBytes += TotalCompressedSize; ReaderChunks->FulfillBytes.fetch_add(TotalCompressedSize, std::memory_order::relaxed); // // At this point we know we can use the block so we can go async. // return UE::Tasks::Launch(TEXT("ReadCompressed"), [this, Id = InChunkId, ReaderIndex = ReaderChunks->ReaderIndex, CompleteCallback = MoveTemp(InCompleteCallback)]() { TIoStatusOr Result = Readers[ReaderIndex]->ReadCompressed(Id, FIoReadOptions()); CompleteCallback(Result); }, UE::Tasks::ETaskPriority::Normal); } void WriteCSV(const FString& InOutputFileName, const TArray& InPackages); void LogSummary(const TArray& IoStoreWriterResults) { if (!bValid) { return; } // In order to try and get a number that's closer to the actual patch size, we need // to strip out the data that isn't actually distributed with the same and is instead // streamed. This is OptionalBulkData and currently is only recognizable here by scanning // the container name. (this is NOT OptionalSegment!) uint64 TotalEntryBytes = 0; uint64 TotalMissBytes = 0; uint64 TotalOptionalMissBytes = 0; for (const FIoStoreWriterResult& Result : IoStoreWriterResults) { bool bIsOptional = Result.ContainerName.Contains(TEXT("optional")); TotalEntryBytes += Result.TotalEntryCompressedSize; TotalMissBytes += Result.ReferenceCacheMissBytes; if (bIsOptional) { TotalOptionalMissBytes += Result.ReferenceCacheMissBytes; } } uint64 FulfillBytesOptional = 0; for (const TPair>& ReaderPair : ChunkDatabase) { bool bIsOptional = ReaderPair.Value->ContainerName.Contains(TEXT("optional")); if (bIsOptional) { FulfillBytesOptional += ReaderPair.Value->FulfillBytes.load(); } } uint64 TotalCandidateBytes = FulfillBytes + TotalMissBytes; uint64 TotalOptionalCandidateBytes = FulfillBytesOptional + TotalOptionalMissBytes; uint64 TotalNonOptCandidateBytes = TotalCandidateBytes - TotalOptionalCandidateBytes; uint64 TotalNonOptFulfillBytes = FulfillBytes - FulfillBytesOptional; UE_LOG(LogIoStore, Display, TEXT("Reference Chunk Database:")); UE_LOG(LogIoStore, Display, TEXT(" %s reused bytes out of %s candidate bytes - %.1f%% hit rate."), *NumberString(FulfillBytes), *NumberString(TotalCandidateBytes), 100.0 * FulfillBytes / (TotalCandidateBytes)); if (TotalOptionalCandidateBytes) { UE_LOG(LogIoStore, Display, TEXT(" %s reused non-optional bytes out of %s candidate non-optional bytes - %.1f%% hit rate."), *NumberString(TotalNonOptFulfillBytes), *NumberString(TotalNonOptCandidateBytes), 100.0 * TotalNonOptFulfillBytes / (TotalNonOptCandidateBytes)); } UE_LOG(LogIoStore, Display, TEXT(" %s candidate bytes out of %s io chunk bytes - %.1f%% coverage."), *NumberString(TotalCandidateBytes), *NumberString(TotalEntryBytes), 100.0 * TotalCandidateBytes / (TotalEntryBytes)); UE_LOG(LogIoStore, Display, TEXT(" %s chunks matched out of %s requests"), *NumberString(MatchCount.load(std::memory_order_relaxed)), *NumberString(RequestCount.load(std::memory_order_relaxed))); FString ChunkNames[FIoStoreChunkDatabase::IoChunkTypeCount]; for (uint8 TypeIndex = 0; TypeIndex < FIoStoreChunkDatabase::IoChunkTypeCount; TypeIndex++) { ChunkNames[TypeIndex] = LexToString((EIoChunkType)TypeIndex); } ChunkDatabase.ValueSort([](const TUniquePtr& A, const TUniquePtr& B) { return A->TotalRequestCount.load(std::memory_order_relaxed) > B->TotalRequestCount.load(std::memory_order_relaxed); }); for (const TPair>& ReaderPair: ChunkDatabase) { FReaderChunks* Reader = ReaderPair.Value.Get(); for (uint8 i = 0; i < FIoStoreChunkDatabase::IoChunkTypeCount; i++) { if (Reader->ChangedCountByType[i] || Reader->NewChunksByType[i] || Reader->UsedChunksByType[i]) { UE_LOG(LogIoStore, Display, TEXT(" %s[%s]: %s changed, %s new, %s reused"), *Reader->ContainerName, *ChunkNames[i], *NumberString(Reader->ChangedCountByType[i]), *NumberString(Reader->NewChunksByType[i]), *NumberString(Reader->UsedChunksByType[i])); } } } if (ContainerNotFound) { UE_LOG(LogIoStore, Display, TEXT(" %s containers were requested that weren't available. This means the "), *NumberString(ContainerNotFound)); UE_LOG(LogIoStore, Display, TEXT(" previous release didn't have these containers. If that doesn't sound right verify")); UE_LOG(LogIoStore, Display, TEXT(" that you used reference containers from the same project. Missing containers:")); for (const TPair>& MissingContainer : MissingContainers) { for (uint8 i = 0; i < FIoStoreChunkDatabase::IoChunkTypeCount; i++) { if (MissingContainer.Value->RequestedChunkCount[i]) { UE_LOG(LogIoStore, Display, TEXT(" %s[%s]: %s requests"), *MissingContainer.Value->ContainerName, *ChunkNames[i], *NumberString(MissingContainer.Value->RequestedChunkCount[i].load(std::memory_order_relaxed))); } } } } } }; struct FReleasedPackages { TSet PackageNames; TMap PackageIdToName; }; [[nodiscard]] static bool LoadKeyChain(const TCHAR* CmdLine, FKeyChain& OutCryptoSettings) { OutCryptoSettings.SetSigningKey(InvalidRSAKeyHandle); OutCryptoSettings.GetEncryptionKeys().Empty(); // First, try and parse the keys from a supplied crypto key cache file FString CryptoKeysCacheFilename; if (FParse::Value(CmdLine, TEXT("cryptokeys="), CryptoKeysCacheFilename)) { if(IFileManager::Get().FileExists(*CryptoKeysCacheFilename)) { UE_LOG(LogIoStore, Display, TEXT("Parsing crypto keys from a crypto key cache file '%s'"), *CryptoKeysCacheFilename); KeyChainUtilities::LoadKeyChainFromFile(CryptoKeysCacheFilename, OutCryptoSettings); } else { UE_LOG(LogIoStore, Error, TEXT("Unable to parse crypto keys as the cache file '%s' does not exist!"), *CryptoKeysCacheFilename); return false; } } else if (FParse::Param(CmdLine, TEXT("encryptionini"))) { FString ProjectDir, EngineDir, Platform; if (FParse::Value(CmdLine, TEXT("projectdir="), ProjectDir, false) && FParse::Value(CmdLine, TEXT("enginedir="), EngineDir, false) && FParse::Value(CmdLine, TEXT("platform="), Platform, false)) { UE_LOG(LogIoStore, Warning, TEXT("A legacy command line syntax is being used for crypto config. Please update to using the -cryptokey parameter as soon as possible as this mode is deprecated")); FConfigFile EngineConfig; FConfigCacheIni::LoadExternalIniFile(EngineConfig, TEXT("Engine"), *FPaths::Combine(EngineDir, TEXT("Config\\")), *FPaths::Combine(ProjectDir, TEXT("Config/")), true, *Platform); bool bDataCryptoRequired = false; EngineConfig.GetBool(TEXT("PlatformCrypto"), TEXT("PlatformRequiresDataCrypto"), bDataCryptoRequired); if (!bDataCryptoRequired) { return true; } FConfigFile ConfigFile; FConfigCacheIni::LoadExternalIniFile(ConfigFile, TEXT("Crypto"), *FPaths::Combine(EngineDir, TEXT("Config\\")), *FPaths::Combine(ProjectDir, TEXT("Config/")), true, *Platform); bool bSignPak = false; bool bEncryptPakIniFiles = false; bool bEncryptPakIndex = false; bool bEncryptAssets = false; bool bEncryptPak = false; if (ConfigFile.Num()) { UE_LOG(LogIoStore, Display, TEXT("Using new format crypto.ini files for crypto configuration")); static const TCHAR* SectionName = TEXT("/Script/CryptoKeys.CryptoKeysSettings"); ConfigFile.GetBool(SectionName, TEXT("bEnablePakSigning"), bSignPak); ConfigFile.GetBool(SectionName, TEXT("bEncryptPakIniFiles"), bEncryptPakIniFiles); ConfigFile.GetBool(SectionName, TEXT("bEncryptPakIndex"), bEncryptPakIndex); ConfigFile.GetBool(SectionName, TEXT("bEncryptAssets"), bEncryptAssets); bEncryptPak = bEncryptPakIniFiles || bEncryptPakIndex || bEncryptAssets; if (bSignPak) { FString PublicExpBase64, PrivateExpBase64, ModulusBase64; ConfigFile.GetString(SectionName, TEXT("SigningPublicExponent"), PublicExpBase64); ConfigFile.GetString(SectionName, TEXT("SigningPrivateExponent"), PrivateExpBase64); ConfigFile.GetString(SectionName, TEXT("SigningModulus"), ModulusBase64); TArray PublicExp, PrivateExp, Modulus; FBase64::Decode(PublicExpBase64, PublicExp); FBase64::Decode(PrivateExpBase64, PrivateExp); FBase64::Decode(ModulusBase64, Modulus); OutCryptoSettings.SetSigningKey(FRSA::CreateKey(PublicExp, PrivateExp, Modulus)); UE_LOG(LogIoStore, Display, TEXT("Parsed signature keys from config files.")); } if (bEncryptPak) { FString EncryptionKeyString; ConfigFile.GetString(SectionName, TEXT("EncryptionKey"), EncryptionKeyString); if (EncryptionKeyString.Len() > 0) { TArray Key; FBase64::Decode(EncryptionKeyString, Key); check(Key.Num() == sizeof(FAES::FAESKey::Key)); FNamedAESKey NewKey; NewKey.Name = TEXT("Default"); NewKey.Guid = FGuid(); FMemory::Memcpy(NewKey.Key.Key, &Key[0], sizeof(FAES::FAESKey::Key)); OutCryptoSettings.GetEncryptionKeys().Add(NewKey.Guid, NewKey); UE_LOG(LogIoStore, Display, TEXT("Parsed AES encryption key from config files.")); } } } else { static const TCHAR* SectionName = TEXT("Core.Encryption"); UE_LOG(LogIoStore, Display, TEXT("Using old format encryption.ini files for crypto configuration")); FConfigCacheIni::LoadExternalIniFile(ConfigFile, TEXT("Encryption"), *FPaths::Combine(EngineDir, TEXT("Config\\")), *FPaths::Combine(ProjectDir, TEXT("Config/")), true, *Platform); ConfigFile.GetBool(SectionName, TEXT("SignPak"), bSignPak); ConfigFile.GetBool(SectionName, TEXT("EncryptPak"), bEncryptPak); if (bSignPak) { FString RSAPublicExp, RSAPrivateExp, RSAModulus; ConfigFile.GetString(SectionName, TEXT("rsa.publicexp"), RSAPublicExp); ConfigFile.GetString(SectionName, TEXT("rsa.privateexp"), RSAPrivateExp); ConfigFile.GetString(SectionName, TEXT("rsa.modulus"), RSAModulus); //TODO: Fix me! //OutSigningKey.PrivateKey.Exponent.Parse(RSAPrivateExp); //OutSigningKey.PrivateKey.Modulus.Parse(RSAModulus); //OutSigningKey.PublicKey.Exponent.Parse(RSAPublicExp); //OutSigningKey.PublicKey.Modulus = OutSigningKey.PrivateKey.Modulus; UE_LOG(LogIoStore, Display, TEXT("Parsed signature keys from config files.")); } if (bEncryptPak) { FString EncryptionKeyString; ConfigFile.GetString(SectionName, TEXT("aes.key"), EncryptionKeyString); FNamedAESKey NewKey; NewKey.Name = TEXT("Default"); NewKey.Guid = FGuid(); if (EncryptionKeyString.Len() == 32 && TCString::IsPureAnsi(*EncryptionKeyString)) { for (int32 Index = 0; Index < 32; ++Index) { NewKey.Key.Key[Index] = (uint8)EncryptionKeyString[Index]; } OutCryptoSettings.GetEncryptionKeys().Add(NewKey.Guid, NewKey); UE_LOG(LogIoStore, Display, TEXT("Parsed AES encryption key from config files.")); } } } } } else { UE_LOG(LogIoStore, Display, TEXT("Using command line for crypto configuration")); FString EncryptionKeyString; FParse::Value(CmdLine, TEXT("aes="), EncryptionKeyString, false); if (EncryptionKeyString.Len() > 0) { UE_LOG(LogIoStore, Warning, TEXT("A legacy command line syntax is being used for crypto config. Please update to using the -cryptokey parameter as soon as possible as this mode is deprecated")); FNamedAESKey NewKey; NewKey.Name = TEXT("Default"); NewKey.Guid = FGuid(); const uint32 RequiredKeyLength = sizeof(NewKey.Key); // Error checking if (EncryptionKeyString.Len() < RequiredKeyLength) { UE_LOG(LogIoStore, Error, TEXT("AES encryption key must be %d characters long"), RequiredKeyLength); return false; } if (EncryptionKeyString.Len() > RequiredKeyLength) { UE_LOG(LogIoStore, Warning, TEXT("AES encryption key is more than %d characters long, so will be truncated!"), RequiredKeyLength); EncryptionKeyString.LeftInline(RequiredKeyLength); } if (!FCString::IsPureAnsi(*EncryptionKeyString)) { UE_LOG(LogIoStore, Error, TEXT("AES encryption key must be a pure ANSI string!")); return false; } const auto AsAnsi = StringCast(*EncryptionKeyString); check(AsAnsi.Length() == RequiredKeyLength); FMemory::Memcpy(NewKey.Key.Key, AsAnsi.Get(), RequiredKeyLength); OutCryptoSettings.GetEncryptionKeys().Add(NewKey.Guid, NewKey); UE_LOG(LogIoStore, Display, TEXT("Parsed AES encryption key from command line.")); } } FString EncryptionKeyOverrideGuidString; FGuid EncryptionKeyOverrideGuid; if (FParse::Value(CmdLine, TEXT("EncryptionKeyOverrideGuid="), EncryptionKeyOverrideGuidString)) { UE_LOG(LogIoStore, Display, TEXT("Using encryption key override '%s'"), *EncryptionKeyOverrideGuidString); FGuid::Parse(EncryptionKeyOverrideGuidString, EncryptionKeyOverrideGuid); } OutCryptoSettings.SetPrincipalEncryptionKey(OutCryptoSettings.GetEncryptionKeys().Find(EncryptionKeyOverrideGuid)); return true; } struct FContainerSourceFile { FString NormalizedPath; FString DestinationPath; bool bNeedsCompression = false; bool bNeedsEncryption = false; }; struct FContainerSourceSpec { FName Name; FString OutputPath; FString OptionalOutputPath; FString StageLooseFileRootPath; TArray SourceFiles; FString PatchTargetFile; TArray PatchSourceContainerFiles; FString EncryptionKeyOverrideGuid; bool bGenerateDiffPatch = false; bool bOnDemand = false; }; struct FCookedFileStatData { enum EFileType { PackageHeader, PackageData, BulkData, OptionalBulkData, MemoryMappedBulkData, ShaderLibrary, Regions, OptionalSegmentPackageHeader, OptionalSegmentPackageData, OptionalSegmentBulkData, Invalid }; int64 FileSize = 0; EFileType FileType = Invalid; }; /** * Finds where the extension starts in a path. * * We can't just find the left most '.' in the file name because the path might * contain a bulkdata cooked index (@see FBulkDataCookedIndex) and we don't want * to include the index part. We are attempting to match up the extensions with * those found in the constructor of FCookedFileStatMap. * * For example take the following filenames and the extensions we want to find: * .m.ubulk -> .m.ubulk * .o.ubulk -> .o.ubulk * .001.ubulk ->.ubulk * * So we want to find the left most '.' unless that is followed by only numeric * values in which case we have found a cooked index and we want to find the '.' * after those values. */ static int32 GetFullExtensionStartIndex(FStringView Path) { int32 ExtensionStartIndex = -1; for (int32 Index = Path.Len() - 1; Index >= 0; --Index) { // Check if we reached the end of the filename if (FPathViews::IsSeparator(Path[Index])) { break; } else if (Path[Index] == '.') { if (ExtensionStartIndex != -1) { // As we have already found a '.' we need to check for the cooked index FStringView Extension = Path.SubStr(Index + 1, (ExtensionStartIndex - Index) - 1); if (UE::String::IsNumericOnlyDigits(Extension)) { return ExtensionStartIndex; } } ExtensionStartIndex = Index; } } return ExtensionStartIndex; } static FStringView GetFullExtension(FStringView Path) { int32 ExtensionStartIndex = GetFullExtensionStartIndex(Path); if (ExtensionStartIndex < 0) { return FStringView(); } else { return Path.RightChop(ExtensionStartIndex); } } class FCookedFileStatMap { public: FCookedFileStatMap() { Extensions.Emplace(TEXT(".umap"), FCookedFileStatData::PackageHeader); Extensions.Emplace(TEXT(".uasset"), FCookedFileStatData::PackageHeader); Extensions.Emplace(TEXT(".uexp"), FCookedFileStatData::PackageData); Extensions.Emplace(TEXT(".ubulk"), FCookedFileStatData::BulkData); Extensions.Emplace(TEXT(".uptnl"), FCookedFileStatData::OptionalBulkData); Extensions.Emplace(TEXT(".m.ubulk"), FCookedFileStatData::MemoryMappedBulkData); Extensions.Emplace(*FString::Printf(TEXT(".uexp%s"), FFileRegion::RegionsFileExtension), FCookedFileStatData::Regions); Extensions.Emplace(*FString::Printf(TEXT(".uptnl%s"), FFileRegion::RegionsFileExtension), FCookedFileStatData::Regions); Extensions.Emplace(*FString::Printf(TEXT(".ubulk%s"), FFileRegion::RegionsFileExtension), FCookedFileStatData::Regions); Extensions.Emplace(*FString::Printf(TEXT(".m.ubulk%s"), FFileRegion::RegionsFileExtension), FCookedFileStatData::Regions); Extensions.Emplace(TEXT(".ushaderbytecode"), FCookedFileStatData::ShaderLibrary); Extensions.Emplace(TEXT(".o.umap"), FCookedFileStatData::OptionalSegmentPackageHeader); Extensions.Emplace(TEXT(".o.uasset"), FCookedFileStatData::OptionalSegmentPackageHeader); Extensions.Emplace(TEXT(".o.uexp"), FCookedFileStatData::OptionalSegmentPackageData); Extensions.Emplace(TEXT(".o.ubulk"), FCookedFileStatData::OptionalSegmentBulkData); } int32 Num() const { return Map.Num(); } void Add(const TCHAR* Path, int64 FileSize) { FString NormalizedPath(Path); FPaths::NormalizeFilename(NormalizedPath); FStringView NormalizePathView(NormalizedPath); int32 ExtensionStartIndex = GetFullExtensionStartIndex(NormalizePathView); if (ExtensionStartIndex < 0) { return; } FStringView Extension = NormalizePathView.RightChop(ExtensionStartIndex); const FCookedFileStatData::EFileType* FindFileType = FindFileTypeFromExtension(Extension); if (!FindFileType) { return; } FCookedFileStatData& CookedFileStatData = Map.Add(MoveTemp(NormalizedPath)); CookedFileStatData.FileType = *FindFileType; CookedFileStatData.FileSize = FileSize; } const FCookedFileStatData* Find(FStringView NormalizedPath) const { return Map.FindByHash(GetTypeHash(NormalizedPath), NormalizedPath); } private: const FCookedFileStatData::EFileType* FindFileTypeFromExtension(const FStringView& Extension) { for (const auto& Pair : Extensions) { if (Pair.Key == Extension) { return &Pair.Value; } } return nullptr; } TArray> Extensions; TMap Map; }; struct FContainerTargetSpec; struct FShaderInfo { enum EShaderType { Normal, Global, Inline }; FIoChunkId ChunkId; FIoBuffer CodeIoBuffer; // the smaller the order, the more likely this will be loaded uint32 LoadOrderFactor = MAX_uint32; TSet ReferencedByPackages; TMap TypeInContainer; // This is used to ensure build determinism and such must be stable // across builds. static bool Sort(const FShaderInfo* A, const FShaderInfo* B) { if (A->LoadOrderFactor == B->LoadOrderFactor) { // Shader chunk IDs are the hash of the shader so this is consistent across builds. return FMemory::Memcmp(A->ChunkId.GetData(), B->ChunkId.GetData(), A->ChunkId.GetSize()) < 0; } return A->LoadOrderFactor < B->LoadOrderFactor; } }; struct FCookedPackage { FPackageId GlobalPackageId; FName PackageName; FName SourcePackageName; // Source package name for redirected package TArray Shaders; TArray ShaderMapHashes; uint64 UAssetSize = 0; uint64 OptionalSegmentUAssetSize = 0; uint64 UExpSize = 0; uint64 TotalBulkDataSize = 0; uint64 LoadOrder = MAX_uint64; // Ordered by dependencies uint64 DiskLayoutOrder = MAX_uint64; // Final order after considering input order files FPackageStoreEntryResource PackageStoreEntry; int32 PreOrderNumber = -1; // Used for sorting in load order bool bPermanentMark = false; // Used for sorting in load order bool bIsLocalized = false; }; struct FLegacyCookedPackage : public FCookedPackage { FString FileName; FString OptionalSegmentFileName; FPackageStorePackage* OptimizedPackage = nullptr; FPackageStorePackage* OptimizedOptionalSegmentPackage = nullptr; }; enum class EContainerChunkType { ShaderCodeLibrary, ShaderCode, PackageData, BulkData, OptionalBulkData, MemoryMappedBulkData, OptionalSegmentPackageData, OptionalSegmentBulkData, Invalid }; struct FContainerTargetFile { FContainerTargetSpec* ContainerTarget = nullptr; FCookedPackage* Package = nullptr; FString NormalizedSourcePath; TOptional SourceBuffer; FString DestinationPath; uint64 SourceSize = 0; uint64 IdealOrder = 0; FIoChunkId ChunkId; FIoHash ChunkHash; FBulkDataCookedIndex BulkDataCookedIndex; TArray PackageHeaderData; EContainerChunkType ChunkType; bool bForceUncompressed = false; TArray FileRegions; }; struct FFileOrderMap { TMap PackageNameToOrder; FString Name; int32 Priority; int32 Index; FFileOrderMap(int32 InPriority, int32 InIndex) : Priority(InPriority) , Index(InIndex) { } }; enum class EFallbackOrderMode { LoadOrder, // Default fallback ordering. Uses LoadOrder dependency ordering AlphabeticalClustered, // As above but clusters are sorted alphabetically at the end for improved patching stability Alphabetical // Pure alphabetical fallback ordering. Best for patching stability but not optimized to reduce seeks }; struct FIoStoreOrderingOptions { bool bClusterByOrderFilePriority = false; bool bAlphaSortClusterPackageLists = false; EFallbackOrderMode FallbackOrderMode = EFallbackOrderMode::LoadOrder; }; struct FIoStoreArguments { FString GlobalContainerPath; FString CookedDir; TArray Containers; FCookedFileStatMap CookedFileStatMap; TArray OrderMaps; FKeyChain KeyChain; FKeyChain PatchKeyChain; FString DLCPluginPath; FString DLCName; FString ReferenceChunkGlobalContainerFileName; FString ReferenceChunkChangesCSVFileName; FString ReferenceChunkAdditionalContainersPath; FString CsvPath; FKeyChain ReferenceChunkKeys; FReleasedPackages ReleasedPackages; TUniquePtr PackageStore; TUniquePtr ScriptObjects; FIoStoreOrderingOptions OrderingOptions; bool bSign = false; bool bRemapPluginContentToGame = false; bool bCreateDirectoryIndex = true; bool bFileRegions = false; EAssetRegistryWritebackMethod WriteBackMetadataToAssetRegistry = EAssetRegistryWritebackMethod::Disabled; bool bWritePluginSizeSummaryJsons = false; // Only valid if WriteBackMetadataToAssetRegistry != Disabled. FOodleDataCompression::ECompressor ShaderOodleCompressor = FOodleDataCompression::ECompressor::Mermaid; FOodleDataCompression::ECompressionLevel ShaderOodleLevel = FOodleDataCompression::ECompressionLevel::Normal; bool IsDLC() const { return DLCPluginPath.Len() > 0; } }; struct FContainerTargetSpec { FIoContainerId ContainerId; FIoContainerId OptionalSegmentContainerId; FIoContainerHeader Header; FIoContainerHeader OptionalSegmentHeader; FName Name; FGuid EncryptionKeyGuid; FString OutputPath; FString OptionalSegmentOutputPath; FString StageLooseFileRootPath; TSharedPtr IoStoreWriter; TSharedPtr OptionalSegmentIoStoreWriter; TArray TargetFiles; TArray> PatchSourceReaders; EIoContainerFlags ContainerFlags = EIoContainerFlags::None; TArray Packages; TSet GlobalShaders; TSet SharedShaders; TSet UniqueShaders; TSet InlineShaders; bool bGenerateDiffPatch = false; }; using FPackageNameMap = TMap; using FPackageIdMap = TMap; class FChunkEntryCsv { public: ~FChunkEntryCsv() { if (OutputArchive.IsValid()) { OutputArchive->Flush(); } } void CreateOutputFile(const TCHAR* OutputFilename) { OutputArchive.Reset(IFileManager::Get().CreateFileWriter(OutputFilename)); if (OutputArchive.IsValid()) { OutputArchive->Logf(TEXT("OrderInContainer, ChunkId, PackageId, PackageName, Filename, ContainerName, Offset, OffsetOnDisk, Size, CompressedSize, Hash, ChunkType, ClassType, PakChunk, Platform, InstallType, DeliveryType")); } } void AddChunk(const FString& ContainerName, int32 Index, const FIoStoreTocChunkInfo& Info, FPackageId PackageId, const FString& PackageName, const FString& ClassType) { if (OutputArchive.IsValid()) { FString PakChunk = TEXT("global"); FString Platform = TEXT("Unknown"); FString InstallType = TEXT("Base"); FString DeliveryType = TEXT("Installed"); // Extract the PakChunk name and the Platform from the container name ContainerName.Split(TEXT("-"), &PakChunk, &Platform); // Determine the Delivery Type Installed/IAS/IAD if (ContainerName.Contains(TEXT("iad"))) { DeliveryType = TEXT("IAD"); } else if (ContainerName.Contains(TEXT("ondemand"))) { DeliveryType = TEXT("IAS"); } // Determine the Install Type Base/Optional if (ContainerName.Contains(TEXT("optional"))) { InstallType = TEXT("Optional"); } OutputArchive->Logf(TEXT("%d, %s, 0x%s, %s, %s, %s, %lld, %lld, %lld, %lld, 0x%s, %s, %s, %s, %s, %s, %s"), Index, *LexToString(Info.Id), *LexToString(PackageId), *PackageName, *Info.FileName, *ContainerName, Info.Offset, Info.OffsetOnDisk, Info.Size, Info.CompressedSize, *LexToString(Info.ChunkHash), *LexToString(Info.ChunkType), *ClassType, *PakChunk, *Platform, *InstallType, *DeliveryType ); } } private: TUniquePtr OutputArchive; }; void SortPackagesInLoadOrderRecursive(TArray& Result, FCookedPackage* Package, TArray& S, TArray& P, int32& C, const TMap>& ReverseEdges) { Package->PreOrderNumber = C; ++C; S.Push(Package); P.Push(Package); const TArray* FindParents = ReverseEdges.Find(Package); if (FindParents) { for (FCookedPackage* Parent : *FindParents) { if (!Parent->bPermanentMark) { if (Parent->PreOrderNumber < 0) { SortPackagesInLoadOrderRecursive(Result, Parent, S, P, C, ReverseEdges); } else { while (P.Top()->PreOrderNumber > Parent->PreOrderNumber) { P.Pop(EAllowShrinking::No); } } } } } if (P.Top() == Package) { FCookedPackage* InStronglyConnectedComponent; do { InStronglyConnectedComponent = S.Top(); S.Pop(EAllowShrinking::No); InStronglyConnectedComponent->bPermanentMark = true; Result.Add(InStronglyConnectedComponent); } while (InStronglyConnectedComponent != Package); P.Pop(EAllowShrinking::No); } } void SortPackagesInLoadOrder(TArray& Packages, const TMap& PackagesMap) { TRACE_CPUPROFILER_EVENT_SCOPE(SortPackagesInLoadOrder); Algo::Sort(Packages, [](const FCookedPackage* A, const FCookedPackage* B) { return A->GlobalPackageId < B->GlobalPackageId; }); TMap> ReverseEdges; ReverseEdges.Reserve(PackagesMap.Num()); for (FCookedPackage* Package : Packages) { for (FPackageId ImportedPackageId : Package->PackageStoreEntry.ImportedPackageIds) { FCookedPackage* FindImportedPackage = PackagesMap.FindRef(ImportedPackageId); if (FindImportedPackage) { TArray& SourceArray = ReverseEdges.FindOrAdd(FindImportedPackage); SourceArray.Add(Package); } } } for (auto& KV : ReverseEdges) { TArray& SourceArray = KV.Value; Algo::Sort(SourceArray, [](const FCookedPackage* A, const FCookedPackage* B) { return A->GlobalPackageId < B->GlobalPackageId; }); } // Path based strongly connected components + topological sort of the components TArray Result; Result.Reserve(Packages.Num()); TArray S; TArray P; for (FCookedPackage* Package : Packages) { if (!Package->bPermanentMark) { S.Reset(); P.Reset(); int32 C = 0; SortPackagesInLoadOrderRecursive(Result, Package, S, P, C, ReverseEdges); } } check(Result.Num() == Packages.Num()); Algo::Reverse(Result); Swap(Packages, Result); uint64 LoadOrder = 0; for (FCookedPackage* Package : Packages) { Package->LoadOrder = LoadOrder++; } } class FClusterStatsCsv { public: ~FClusterStatsCsv() { if (OutputArchive) { OutputArchive->Flush(); } } void CreateOutputFile(const FString& Path) { OutputArchive.Reset(IFileManager::Get().CreateFileWriter(*Path)); if (OutputArchive) { OutputArchive->Logf(TEXT("PackageName,ClusterUExpBytes,BytesToRead,ClustersToRead,ClusterOwner,OrderFile,OrderIndex")); } } void AddPackage(FName PackageName, int64 ClusterUExpBytes, int64 DepUExpBytes, uint32 NumTouchedClusters, FName ClusterOwner, const FFileOrderMap* BlameOrderMap, uint64 LocalOrder) { if (!OutputArchive.IsValid()) { return; } OutputArchive->Logf(TEXT("%s,%lld,%lld,%u,%s,%s,%llu"), *PackageName.ToString(), ClusterUExpBytes, DepUExpBytes, NumTouchedClusters, *ClusterOwner.ToString(), BlameOrderMap ? *BlameOrderMap->Name : TEXT("None"), LocalOrder ); } void Close() { OutputArchive.Reset(); } private: TUniquePtr OutputArchive; }; FClusterStatsCsv ClusterStatsCsv; // If bClusterByOrderFilePriority is false // Order packages by the order of OrderMaps with their associated priority // e.g. Order map 1 with priority 0 (A, B, C) // Order map 2 with priority 10 (B, D) // Final order: (A, C, B, D) // Then cluster packages in this order. // If bClusterByOrderFilePriority is true // Cluster packages first by OrderMaps in priority order, then concatenate clusters in the array order of OrderMaps. // e.g. Order map 1 with priority 0 (A, B, C) // Order map 2 with priority 10 (B, D) // Cluster packages B, D, then A, C // Then reassemble clusters A, C, B, D static void AssignPackagesDiskOrder( const TArray& Packages, const TArray& OrderMaps, const FPackageIdMap& PackageIdMap, const FIoStoreOrderingOptions& OrderingOptions ) { IOSTORE_CPU_SCOPE(AssignPackagesDiskOrder); struct FCluster { TArray Packages; int32 OrderFileIndex; // Index in OrderMaps of the FFileOrderMap which contained Packages.Last() int32 ClusterSequence; FCluster(int32 InOrderFileIndex, int32 InClusterSequence) : OrderFileIndex(InOrderFileIndex) , ClusterSequence(InClusterSequence) { } }; struct FPackageAndOrder { FCookedPackage* Package = nullptr; int64 LocalOrder; const FFileOrderMap* OrderMap; FPackageAndOrder(FCookedPackage* InPackage, int64 InLocalOrder, const FFileOrderMap* InOrderMap) : Package(InPackage) , LocalOrder(InLocalOrder) , OrderMap(InOrderMap) { check(OrderMap); } }; // Order maps sorted by priority TArray PriorityOrderMaps; for (const FFileOrderMap& Map : OrderMaps) { PriorityOrderMaps.Add(&Map); } // Create a fallback order map to avoid null checks later // Lowest priority, last index FFileOrderMap FallbackOrderMap(MIN_int32, MAX_int32); FallbackOrderMap.Name = TEXT("Fallback"); // Generate a simple alphabetically sorted fallback order if requested if (OrderingOptions.FallbackOrderMode == EFallbackOrderMode::Alphabetical) { TArray SortedPackageNames; SortedPackageNames.Reserve(Packages.Num()); for (FCookedPackage* Package : Packages) { SortedPackageNames.Add(Package->PackageName); } Algo::Sort(SortedPackageNames, FNameLexicalLess()); int64 SortIndex = 0; for (FName PackageName : SortedPackageNames) { FallbackOrderMap.PackageNameToOrder.Add(PackageName, SortIndex++); } PriorityOrderMaps.Add(&FallbackOrderMap); } Algo::StableSortBy(PriorityOrderMaps, [](const FFileOrderMap* Map) { return Map->Priority; }, TGreater()); TArray SortedPackages; SortedPackages.Reserve(Packages.Num()); for (FCookedPackage* Package : Packages) { // Default to the fallback order map // Reverse the bundle load order for the fallback map (so that packages are considered before their imports) const FFileOrderMap* UsedOrderMap = &FallbackOrderMap; int64 LocalOrder = -int64(Package->LoadOrder); for (const FFileOrderMap* OrderMap : PriorityOrderMaps) { if (const int64* Order = OrderMap->PackageNameToOrder.Find(Package->PackageName)) { LocalOrder = *Order; UsedOrderMap = OrderMap; break; } } SortedPackages.Emplace(Package, LocalOrder, UsedOrderMap); } const FFileOrderMap* LastBlameOrderMap = nullptr; int32 LastAssignedCount = 0; int64 LastAssignedUExpSize = 0, AssignedUExpSize = 0; int64 LastAssignedBulkSize = 0, AssignedBulkSize = 0; if (OrderingOptions.bClusterByOrderFilePriority) { // Sort by priority of the order map Algo::Sort(SortedPackages, [](const FPackageAndOrder& A, const FPackageAndOrder& B) { // Packages in the same order map should be sorted by their local ordering if (A.OrderMap == B.OrderMap) { return A.LocalOrder < B.LocalOrder; } // First priority, then index if (A.OrderMap->Priority != B.OrderMap->Priority) { return A.OrderMap->Priority > B.OrderMap->Priority; } check(A.OrderMap->Index != B.OrderMap->Index); return A.OrderMap->Index < B.OrderMap->Index; }); } else { // Sort by the order of the order map (...) Algo::Sort(SortedPackages, [](const FPackageAndOrder& A, const FPackageAndOrder& B) { // Packages in the same order map should be sorted by their local ordering if (A.OrderMap == B.OrderMap) { return A.LocalOrder < B.LocalOrder; } // Blame order priority is not considered for the order in which we cluster packages, only for the order in which we assign packages to an order map return A.OrderMap->Index < B.OrderMap->Index; }); } // Keep these containers outside of inner loops to reuse allocated memory across iterations // No need to allocate & free them on every single iteration, just reset element count before using them TSet ClustersToRead; TSet VisitedDeps; TArray DepQueue; TArray OrderedClustersToRead; int32 ClusterSequence = 0; TMap PackageToCluster; TArray Clusters; TSet AssignedPackages; TArray ProcessStack; PackageToCluster.Reserve(SortedPackages.Num()); AssignedPackages.Reserve(SortedPackages.Num()); for (FPackageAndOrder& Entry : SortedPackages) { checkSlow(Entry.OrderMap); // Without this, Entry.OrderMap != LastBlameOrderMap convinces static analysis that Entry.OrderMap may be null if (Entry.OrderMap != LastBlameOrderMap) { if( LastBlameOrderMap != nullptr ) { UE_LOG(LogIoStore, Display, TEXT("Ordered %d/%d packages using order file %s. %.2fMB UExp data. %.2fmb bulk data."), AssignedPackages.Num() - LastAssignedCount, Packages.Num(), *LastBlameOrderMap->Name, (AssignedUExpSize - LastAssignedUExpSize) / 1024.0 / 1024.0, (AssignedBulkSize - LastAssignedBulkSize) / 1024.0 / 1024.0 ); } LastAssignedCount = AssignedPackages.Num(); LastAssignedUExpSize= AssignedUExpSize; LastAssignedBulkSize = AssignedBulkSize; LastBlameOrderMap = Entry.OrderMap; } if (!AssignedPackages.Contains(Entry.Package)) { FCluster* Cluster = new FCluster(Entry.OrderMap->Index, ClusterSequence++); Clusters.Add(Cluster); ProcessStack.Push(Entry.Package); bool bDoClustering = true; if (OrderingOptions.FallbackOrderMode == EFallbackOrderMode::Alphabetical && Entry.OrderMap == &FallbackOrderMap) { // Disable clustering if we're doing alphabetical fallback ordering bDoClustering = false; } int64 ClusterBytes = 0; while (ProcessStack.Num()) { FCookedPackage* PackageToProcess = ProcessStack.Pop(EAllowShrinking::No); if (!AssignedPackages.Contains(PackageToProcess)) { AssignedPackages.Add(PackageToProcess); Cluster->Packages.Add(PackageToProcess); PackageToCluster.Add(PackageToProcess, Cluster); ClusterBytes += PackageToProcess->UExpSize; AssignedUExpSize += PackageToProcess->UExpSize; AssignedBulkSize += PackageToProcess->TotalBulkDataSize; if (bDoClustering) { // Add referenced dependencies to the package for (const FPackageId& ReferencedPackageId : PackageToProcess->PackageStoreEntry.ImportedPackageIds) { FCookedPackage* FindReferencedPackage = PackageIdMap.FindRef(ReferencedPackageId); if (FindReferencedPackage) { ProcessStack.Push(FindReferencedPackage); } } } } } // This is all just for stats, to compute the "NumClustersToRead" column of the CSV, an approximation of seeks per cluster: for (FCookedPackage* Package : Cluster->Packages) { int64 BytesToRead = 0; ClustersToRead.Reset(); VisitedDeps.Reset(); DepQueue.Reset(); DepQueue.Push(Package); while (DepQueue.Num() > 0) { FCookedPackage* Cursor = DepQueue.Pop(EAllowShrinking::No); if( VisitedDeps.Contains(Cursor) == false) { VisitedDeps.Add(Cursor); BytesToRead += Cursor->UExpSize; if (FCluster* ReadCluster = PackageToCluster.FindRef(Cursor)) { ClustersToRead.Add(ReadCluster); } for (const FPackageId& ImportedPackageId : Cursor->PackageStoreEntry.ImportedPackageIds) { FCookedPackage* FindReferencedPackage = PackageIdMap.FindRef(ImportedPackageId); if (FindReferencedPackage) { DepQueue.Push(FindReferencedPackage); } } } } OrderedClustersToRead.Reset(ClustersToRead.Num()); for (FCluster* ClusterToRead : ClustersToRead) { OrderedClustersToRead.Add(ClusterToRead); } Algo::SortBy(OrderedClustersToRead, [](FCluster* C) { return C->ClusterSequence; }, TLess()); int32 NumClustersToRead = 1; // Could replace with "min seeks" for (int32 i = 1; i < OrderedClustersToRead.Num(); ++i) { if (OrderedClustersToRead[i]->ClusterSequence != OrderedClustersToRead[i - 1]->ClusterSequence + 1) { ++NumClustersToRead; } } FName ClusterOwner = Entry.Package->PackageName; ClusterStatsCsv.AddPackage(Package->PackageName, Package == Entry.Package ? ClusterBytes : 0, BytesToRead, NumClustersToRead, ClusterOwner, Entry.OrderMap, Entry.LocalOrder); } } } UE_LOG(LogIoStore, Display, TEXT("Ordered %d packages using fallback bundle order"), AssignedPackages.Num() - LastAssignedCount); check(AssignedPackages.Num() == Packages.Num()); // Sort the clusters if (OrderingOptions.bClusterByOrderFilePriority) { Algo::StableSortBy(Clusters, [](FCluster* Cluster) { return Cluster->OrderFileIndex; }); } if (OrderingOptions.FallbackOrderMode == EFallbackOrderMode::AlphabeticalClustered) { // Sort the fallback order clusters alphabetically Algo::StableSort(Clusters, [FallbackOrderMap](const FCluster* A, const FCluster* B) { if (A->OrderFileIndex == FallbackOrderMap.Index && B->OrderFileIndex == FallbackOrderMap.Index) { return A->Packages[0]->PackageName.LexicalLess(B->Packages[0]->PackageName); } return false; }); } for (FCluster* Cluster : Clusters) { if (OrderingOptions.bAlphaSortClusterPackageLists) { Algo::SortBy(Cluster->Packages, [](const FCookedPackage* Package) { return Package->PackageName; }, FNameLexicalLess()); } else { Algo::Sort(Cluster->Packages, [](const FCookedPackage* A, const FCookedPackage* B) { return A->LoadOrder < B->LoadOrder; }); } } uint64 LayoutIndex = 0; for (FCluster* Cluster : Clusters) { for (FCookedPackage* Package : Cluster->Packages) { Package->DiskLayoutOrder = LayoutIndex++; } delete Cluster; } ClusterStatsCsv.Close(); } static void CreateDiskLayout( const TArray& ContainerTargets, const TArray& Packages, const TArray& OrderMaps, const FPackageIdMap& PackageIdMap, const FIoStoreOrderingOptions& OrderingOptions) { IOSTORE_CPU_SCOPE(CreateDiskLayout); AssignPackagesDiskOrder(Packages, OrderMaps, PackageIdMap, OrderingOptions); for (FContainerTargetSpec* ContainerTarget : ContainerTargets) { TArray SortedTargetFiles; SortedTargetFiles.Reserve(ContainerTarget->TargetFiles.Num()); TMap ShaderTargetFilesMap; ShaderTargetFilesMap.Reserve(ContainerTarget->GlobalShaders.Num() + ContainerTarget->SharedShaders.Num() + ContainerTarget->UniqueShaders.Num() + ContainerTarget->InlineShaders.Num()); for (FContainerTargetFile& TargetFile : ContainerTarget->TargetFiles) { if (TargetFile.ChunkType == EContainerChunkType::ShaderCode) { ShaderTargetFilesMap.Add(TargetFile.ChunkId, &TargetFile); } else { SortedTargetFiles.Add(&TargetFile); } } check(ShaderTargetFilesMap.Num() == ContainerTarget->GlobalShaders.Num() + ContainerTarget->SharedShaders.Num() + ContainerTarget->UniqueShaders.Num() + ContainerTarget->InlineShaders.Num()); Algo::Sort(SortedTargetFiles, [](const FContainerTargetFile* A, const FContainerTargetFile* B) { if (A->ChunkType != B->ChunkType) { return A->ChunkType < B->ChunkType; } if (A->ChunkType == EContainerChunkType::ShaderCodeLibrary) { return A->DestinationPath < B->DestinationPath; } if (A->Package != B->Package) { return A->Package->DiskLayoutOrder < B->Package->DiskLayoutOrder; } if (A->BulkDataCookedIndex != B->BulkDataCookedIndex) { return A->BulkDataCookedIndex < B->BulkDataCookedIndex; } check(A == B) return false; }); int32 Index = 0; int32 ShaderCodeInsertionIndex = -1; while (Index < SortedTargetFiles.Num()) { FContainerTargetFile* TargetFile = SortedTargetFiles[Index]; if (ShaderCodeInsertionIndex < 0 && TargetFile->ChunkType != EContainerChunkType::ShaderCodeLibrary) { ShaderCodeInsertionIndex = Index; } if (TargetFile->ChunkType == EContainerChunkType::PackageData) { TArray> PackageInlineShaders; // Since we are inserting in to a sorted array (on disk order), we have to be stably sorted // beforehand Algo::Sort(TargetFile->Package->Shaders, FShaderInfo::Sort); for (FShaderInfo* Shader : TargetFile->Package->Shaders) { check(Shader->ReferencedByPackages.Num() > 0); FShaderInfo::EShaderType* ShaderType = Shader->TypeInContainer.Find(ContainerTarget); if (ShaderType && *ShaderType == FShaderInfo::Inline) { check(ContainerTarget->InlineShaders.Contains(Shader)); FContainerTargetFile* ShaderTargetFile = ShaderTargetFilesMap.FindRef(Shader->ChunkId); check(ShaderTargetFile); PackageInlineShaders.Add(ShaderTargetFile); } } if (!PackageInlineShaders.IsEmpty()) { SortedTargetFiles.Insert(PackageInlineShaders, Index + 1); Index += PackageInlineShaders.Num(); } } ++Index; } if (ShaderCodeInsertionIndex < 0) { ShaderCodeInsertionIndex = 0; } auto AddShaderTargetFiles = [&ShaderTargetFilesMap, &SortedTargetFiles, &ShaderCodeInsertionIndex] (TSet& Shaders) { if (!Shaders.IsEmpty()) { TArray SortedShaders = Shaders.Array(); Algo::Sort(SortedShaders, FShaderInfo::Sort); TArray ShaderTargetFiles; ShaderTargetFiles.Reserve(SortedShaders.Num()); for (const FShaderInfo* ShaderInfo : SortedShaders) { FContainerTargetFile* ShaderTargetFile = ShaderTargetFilesMap.FindRef(ShaderInfo->ChunkId); check(ShaderTargetFile); ShaderTargetFiles.Add(ShaderTargetFile); } SortedTargetFiles.Insert(ShaderTargetFiles, ShaderCodeInsertionIndex); ShaderCodeInsertionIndex += ShaderTargetFiles.Num(); } }; AddShaderTargetFiles(ContainerTarget->GlobalShaders); AddShaderTargetFiles(ContainerTarget->SharedShaders); AddShaderTargetFiles(ContainerTarget->UniqueShaders); check(SortedTargetFiles.Num() == ContainerTarget->TargetFiles.Num()); uint64 IdealOrder = 0; for (FContainerTargetFile* TargetFile : SortedTargetFiles) { TargetFile->IdealOrder = IdealOrder++; } } } FContainerTargetSpec* AddContainer( FName Name, TArray& Containers) { FIoContainerId ContainerId = FIoContainerId::FromName(Name); for (FContainerTargetSpec* ExistingContainer : Containers) { if (ExistingContainer->Name == Name) { UE_LOG(LogIoStore, Fatal, TEXT("Duplicate container name: '%s'"), *Name.ToString()); return nullptr; } if (ExistingContainer->ContainerId == ContainerId) { UE_LOG(LogIoStore, Fatal, TEXT("Hash collision for container names: '%s' and '%s'"), *Name.ToString(), *ExistingContainer->Name.ToString()); return nullptr; } } FContainerTargetSpec* ContainerTargetSpec = new FContainerTargetSpec(); ContainerTargetSpec->Name = Name; ContainerTargetSpec->ContainerId = ContainerId; Containers.Add(ContainerTargetSpec); return ContainerTargetSpec; } FCookedPackage& FindOrAddPackage( const FIoStoreArguments& Arguments, const FName& PackageName, TArray& Packages, FPackageNameMap& PackageNameMap, FPackageIdMap& PackageIdMap) { FCookedPackage* Package = PackageNameMap.FindRef(PackageName); if (!Package) { FPackageId PackageId = FPackageId::FromName(PackageName); if (FCookedPackage* FindById = PackageIdMap.FindRef(PackageId)) { UE_LOG(LogIoStore, Fatal, TEXT("Package name hash collision \"%s\" and \"%s"), *FindById->PackageName.ToString(), *PackageName.ToString()); } if (const FName* ReleasedPackageName = Arguments.ReleasedPackages.PackageIdToName.Find(PackageId)) { UE_LOG(LogIoStore, Fatal, TEXT("Package name hash collision with base game package \"%s\" and \"%s"), *ReleasedPackageName->ToString(), *PackageName.ToString()); } Package = new FCookedPackage(); Package->PackageName = PackageName; Package->GlobalPackageId = PackageId; Packages.Add(Package); PackageNameMap.Add(PackageName, Package); PackageIdMap.Add(PackageId, Package); } return *Package; } FLegacyCookedPackage* FindOrAddLegacyPackage( const FIoStoreArguments& Arguments, const TCHAR* FileName, TArray& Packages, FPackageNameMap& PackageNameMap, FPackageIdMap& PackageIdMap) { FName PackageName = Arguments.PackageStore->GetPackageNameFromFileName(FileName); if (PackageName.IsNone()) { return nullptr; } FCookedPackage* Package = PackageNameMap.FindRef(PackageName); if (!Package) { FPackageId PackageId = FPackageId::FromName(PackageName); if (FCookedPackage* FindById = PackageIdMap.FindRef(PackageId)) { UE_LOG(LogIoStore, Fatal, TEXT("Package name hash collision \"%s\" and \"%s"), *FindById->PackageName.ToString(), *PackageName.ToString()); } if (const FName* ReleasedPackageName = Arguments.ReleasedPackages.PackageIdToName.Find(PackageId)) { UE_LOG(LogIoStore, Fatal, TEXT("Package name hash collision with base game package \"%s\" and \"%s"), *ReleasedPackageName->ToString(), *PackageName.ToString()); } Package = new FLegacyCookedPackage(); Package->PackageName = PackageName; Package->GlobalPackageId = PackageId; Packages.Add(Package); PackageNameMap.Add(PackageName, Package); PackageIdMap.Add(PackageId, Package); } return static_cast(Package); } static void ParsePackageAssetsFromFiles(TArray& Packages, const FPackageStoreOptimizer& PackageStoreOptimizer) { IOSTORE_CPU_SCOPE(ParsePackageAssetsFromFiles); UE_LOG(LogIoStore, Display, TEXT("Parsing packages...")); TAtomic ReadCount {0}; TAtomic ParseCount {0}; const int32 TotalPackageCount = Packages.Num(); TArray PackageFileSummaries; PackageFileSummaries.SetNum(TotalPackageCount); TArray OptionalSegmentPackageFileSummaries; OptionalSegmentPackageFileSummaries.SetNum(TotalPackageCount); uint8* UAssetMemory = nullptr; uint8* OptionalSegmentUAssetMemory = nullptr; TArray PackageAssetBuffers; PackageAssetBuffers.SetNum(TotalPackageCount); TArray OptionalSegmentPackageAssetBuffers; OptionalSegmentPackageAssetBuffers.SetNum(TotalPackageCount); UE_LOG(LogIoStore, Display, TEXT("Reading package assets...")); { IOSTORE_CPU_SCOPE(ReadUAssetFiles); uint64 TotalUAssetSize = 0; uint64 TotalOptionalSegmentUAssetSize = 0; for (const FCookedPackage* Package : Packages) { const FLegacyCookedPackage* LegacyPackage = static_cast(Package); TotalUAssetSize += LegacyPackage->UAssetSize; TotalOptionalSegmentUAssetSize += LegacyPackage->OptionalSegmentUAssetSize; } UAssetMemory = reinterpret_cast(FMemory::Malloc(TotalUAssetSize)); uint8* UAssetMemoryPtr = UAssetMemory; OptionalSegmentUAssetMemory = reinterpret_cast(FMemory::Malloc(TotalOptionalSegmentUAssetSize)); uint8* OptionalSegmentUAssetMemoryPtr = OptionalSegmentUAssetMemory; for (int32 Index = 0; Index < TotalPackageCount; ++Index) { FLegacyCookedPackage* Package = static_cast(Packages[Index]); PackageAssetBuffers[Index] = UAssetMemoryPtr; UAssetMemoryPtr += Package->UAssetSize; OptionalSegmentPackageAssetBuffers[Index] = OptionalSegmentUAssetMemoryPtr; OptionalSegmentUAssetMemoryPtr += Package->OptionalSegmentUAssetSize; } double StartTime = FPlatformTime::Seconds(); TAtomic TotalReadCount{ 0 }; TAtomic CurrentFileIndex{ 0 }; ParallelFor(TEXT("ReadingPackageAssets.PF"), TotalPackageCount, 1, [&ReadCount, &PackageAssetBuffers, &OptionalSegmentPackageAssetBuffers, &Packages, &CurrentFileIndex, &TotalReadCount](int32 Index) { TRACE_CPUPROFILER_EVENT_SCOPE(ReadUAssetFile); FLegacyCookedPackage* Package = static_cast(Packages[Index]); if (Package->UAssetSize) { TotalReadCount.IncrementExchange(); uint8* Buffer = PackageAssetBuffers[Index]; TUniquePtr FileHandle(FPlatformFileManager::Get().GetPlatformFile().OpenRead(*Package->FileName)); if (FileHandle) { bool bSuccess = FileHandle->Read(Buffer, Package->UAssetSize); UE_CLOG(!bSuccess, LogIoStore, Warning, TEXT("Failed reading file '%s'"), *Package->FileName); } else { UE_LOG(LogIoStore, Warning, TEXT("Couldn't open file '%s'"), *Package->FileName); } } if (Package->OptionalSegmentUAssetSize) { TotalReadCount.IncrementExchange(); uint8* Buffer = OptionalSegmentPackageAssetBuffers[Index]; TUniquePtr FileHandle(FPlatformFileManager::Get().GetPlatformFile().OpenRead(*Package->OptionalSegmentFileName)); if (FileHandle) { bool bSuccess = FileHandle->Read(Buffer, Package->OptionalSegmentUAssetSize); UE_CLOG(!bSuccess, LogIoStore, Warning, TEXT("Failed reading file '%s'"), *Package->OptionalSegmentFileName); } else { UE_LOG(LogIoStore, Warning, TEXT("Couldn't open file '%s'"), *Package->OptionalSegmentFileName); } } uint64 LocalFileIndex = CurrentFileIndex.IncrementExchange() + 1; UE_CLOG(LocalFileIndex % 1000 == 0, LogIoStore, Display, TEXT("Reading %d/%d: '%s'"), LocalFileIndex, Packages.Num(), *Package->FileName); }, EParallelForFlags::Unbalanced); double EndTime = FPlatformTime::Seconds(); UE_LOG(LogIoStore, Display, TEXT("Packages read %s files in %.2f seconds, %s total bytes, %s bytes per second"), *NumberString(TotalReadCount.Load()), EndTime - StartTime, *NumberString(TotalOptionalSegmentUAssetSize + TotalUAssetSize), *NumberString((int64)((TotalOptionalSegmentUAssetSize + TotalUAssetSize) / FMath::Max(.001f, EndTime - StartTime)))); } { IOSTORE_CPU_SCOPE(SerializeSummaries); ParallelFor(TotalPackageCount, [ &ReadCount, &PackageAssetBuffers, &OptionalSegmentPackageAssetBuffers, &PackageFileSummaries, &Packages, &PackageStoreOptimizer](int32 Index) { FLegacyCookedPackage* Package = static_cast(Packages[Index]); if (Package->UAssetSize) { uint8* PackageBuffer = PackageAssetBuffers[Index]; FIoBuffer CookedHeaderBuffer = FIoBuffer(FIoBuffer::Wrap, PackageBuffer, Package->UAssetSize); Package->OptimizedPackage = PackageStoreOptimizer.CreatePackageFromCookedHeader(Package->PackageName, CookedHeaderBuffer); } else { UE_LOG(LogIoStore, Display, TEXT("Including package %s without a .uasset file. Excluded by PakFileRules?"), *Package->PackageName.ToString()); Package->OptimizedPackage = PackageStoreOptimizer.CreateMissingPackage(Package->PackageName); } check(Package->OptimizedPackage->GetId() == Package->GlobalPackageId); if (Package->OptionalSegmentUAssetSize) { uint8* OptionalSegmentPackageBuffer = OptionalSegmentPackageAssetBuffers[Index]; FIoBuffer OptionalSegmentCookedHeaderBuffer = FIoBuffer(FIoBuffer::Wrap, OptionalSegmentPackageBuffer, Package->OptionalSegmentUAssetSize); Package->OptimizedOptionalSegmentPackage = PackageStoreOptimizer.CreatePackageFromCookedHeader(Package->PackageName, OptionalSegmentCookedHeaderBuffer); check(Package->OptimizedOptionalSegmentPackage->GetId() == Package->GlobalPackageId); } // The entry created here will have the correct set of imported packages but the export info will be updated later Package->PackageStoreEntry = PackageStoreOptimizer.CreatePackageStoreEntry(Package->OptimizedPackage, Package->OptimizedOptionalSegmentPackage); }, EParallelForFlags::Unbalanced); } FMemory::Free(UAssetMemory); FMemory::Free(OptionalSegmentUAssetMemory); } static TUniquePtr CreateIoStoreReader(const TCHAR* Path, const FKeyChain& KeyChain) { TUniquePtr IoStoreReader(new FIoStoreReader()); TMap DecryptionKeys; for (const auto& KV : KeyChain.GetEncryptionKeys()) { DecryptionKeys.Add(KV.Key, KV.Value.Key); } FIoStatus Status = IoStoreReader->Initialize(*FPaths::ChangeExtension(Path, TEXT("")), DecryptionKeys); if (Status.IsOk()) { return IoStoreReader; } else { UE_LOG(LogIoStore, Warning, TEXT("Failed creating IoStore reader '%s' [%s]"), Path, *Status.ToString()) return nullptr; } } TArray> CreatePatchSourceReaders(const TArray& Files, const FIoStoreArguments& Arguments) { TArray> Readers; for (const FString& PatchSourceContainerFile : Files) { TUniquePtr Reader = CreateIoStoreReader(*PatchSourceContainerFile, Arguments.PatchKeyChain); if (Reader.IsValid()) { UE_LOG(LogIoStore, Display, TEXT("Loaded patch source container '%s'"), *PatchSourceContainerFile); Readers.Add(MoveTemp(Reader)); } } return Readers; } static bool LoadShaderAssetInfo(const FString& Filename, FCookedPackageStore* PackageStore, TMap>& OutShaderCodeToAssets) { FString JsonText; if (PackageStore && PackageStore->HasZenStoreClient()) { FIoChunkId ChunkId = PackageStore->GetChunkIdFromFileName(Filename); if (!ChunkId.IsValid()) { return false; } TIoStatusOr Buffer = PackageStore->ReadChunk(ChunkId); if (!Buffer.IsOk()) { return false; } FFileHelper::BufferToString(JsonText, Buffer.ValueOrDie().GetData(), (int32)Buffer.ValueOrDie().GetSize()); } else { if (!FFileHelper::LoadFileToString(JsonText, *Filename)) { return false; } } TSharedPtr JsonObject; TSharedRef> Reader = TJsonReaderFactory::Create(JsonText); if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid()) { return false; } TSharedPtr AssetInfoArrayValue = JsonObject->Values.FindRef(TEXT("ShaderCodeToAssets")); if (!AssetInfoArrayValue.IsValid()) { return false; } TArray> AssetInfoArray = AssetInfoArrayValue->AsArray(); for (int32 IdxPair = 0, NumPairs = AssetInfoArray.Num(); IdxPair < NumPairs; ++IdxPair) { TSharedPtr Pair = AssetInfoArray[IdxPair]->AsObject(); if (Pair.IsValid()) { TSharedPtr ShaderMapHashJson = Pair->Values.FindRef(TEXT("ShaderMapHash")); if (!ShaderMapHashJson.IsValid()) { continue; } FSHAHash ShaderMapHash; ShaderMapHash.FromString(ShaderMapHashJson->AsString()); TSharedPtr AssetPathsArrayValue = Pair->Values.FindRef(TEXT("Assets")); if (!AssetPathsArrayValue.IsValid()) { continue; } TSet& Assets = OutShaderCodeToAssets.Add(ShaderMapHash); TArray> AssetPathsArray = AssetPathsArrayValue->AsArray(); for (int32 IdxAsset = 0, NumAssets = AssetPathsArray.Num(); IdxAsset < NumAssets; ++IdxAsset) { Assets.Add(FName(*AssetPathsArray[IdxAsset]->AsString())); } } } return true; } static bool ConvertToIoStoreShaderLibrary( const TCHAR* FileName, FIoChunkId InChunkId, FCookedPackageStore* PackageStore, FOodleDataCompression::ECompressor InShaderOodleCompressor, FOodleDataCompression::ECompressionLevel InShaderOodleLevel, TTuple& OutLibraryIoChunk, TArray>& OutCodeIoChunks, TArray>>& OutShaderMaps, TArray>>& OutShaderMapAssetAssociations) { IOSTORE_CPU_SCOPE(ConvertShaderLibrary); // ShaderArchive-MyProject-PCD3D.ushaderbytecode FStringView BaseFileNameView = FPathViews::GetBaseFilename(FileName); int32 FormatStartIndex = -1; if (!BaseFileNameView.FindLastChar('-', FormatStartIndex)) { UE_LOG(LogIoStore, Error, TEXT("Invalid shader code library file name '%s'."), FileName); return false; } int32 LibraryNameStartIndex = -1; if (!BaseFileNameView.FindChar('-', LibraryNameStartIndex) || FormatStartIndex - LibraryNameStartIndex <= 1) { UE_LOG(LogIoStore, Error, TEXT("Invalid shader code library file name '%s'."), FileName); return false; } FString LibraryName(BaseFileNameView.Mid(LibraryNameStartIndex + 1, FormatStartIndex - LibraryNameStartIndex - 1)); FName FormatName(BaseFileNameView.RightChop(FormatStartIndex + 1)); FIoBuffer LibraryBuffer; if (PackageStore && PackageStore->HasZenStoreClient() && InChunkId.IsValid()) { LibraryBuffer = PackageStore->ReadChunk(InChunkId).ConsumeValueOrDie(); } else { TUniquePtr LibraryAr(IFileManager::Get().CreateFileReader(FileName)); if (!LibraryAr) { UE_LOG(LogIoStore, Error, TEXT("Missing shader code library file '%s'."), FileName); return false; } LibraryBuffer = FIoBuffer(LibraryAr->TotalSize()); LibraryAr->Serialize(LibraryBuffer.GetData(), LibraryBuffer.GetSize()); } FLargeMemoryReader LibraryAr(LibraryBuffer.GetData(), LibraryBuffer.GetSize()); uint32 Version; LibraryAr << Version; FSerializedShaderArchive SerializedShaders; LibraryAr << SerializedShaders; int64 OffsetToShaderCode = LibraryAr.Tell(); FString AssetInfoFileName = FPaths::GetPath(FileName) / FString::Printf(TEXT("ShaderAssetInfo-%s-%s.assetinfo.json"), *LibraryName, *FormatName.ToString()); TMap ShaderCodeToAssets; if (!LoadShaderAssetInfo(AssetInfoFileName, PackageStore && InChunkId.IsValid() ? PackageStore : nullptr, ShaderCodeToAssets)) { UE_LOG(LogIoStore, Error, TEXT("Failed loading asset asset info file '%s'"), *AssetInfoFileName); return false; } FIoStoreShaderCodeArchiveHeader IoStoreLibraryHeader; FIoStoreShaderCodeArchive::CreateIoStoreShaderCodeArchiveHeader(FName(FormatName), SerializedShaders, IoStoreLibraryHeader); checkf(SerializedShaders.ShaderEntries.Num() == IoStoreLibraryHeader.ShaderEntries.Num(), TEXT("IoStore header has different number of shaders (%d) than the original library (%d)"), IoStoreLibraryHeader.ShaderEntries.Num(), SerializedShaders.ShaderEntries.Num()); checkf(SerializedShaders.ShaderMapEntries.Num() == IoStoreLibraryHeader.ShaderMapEntries.Num(), TEXT("IoStore header has different number of shadermaps (%d) than the original library (%d)"), IoStoreLibraryHeader.ShaderMapEntries.Num(), SerializedShaders.ShaderMapEntries.Num()); // Load all the shaders, decompress them and recompress into groups. Each code chunk is shader group chunk now. // This also updates group's compressed size, the only value left to calculate. int32 GroupCount = IoStoreLibraryHeader.ShaderGroupEntries.Num(); OutCodeIoChunks.SetNum(GroupCount); FCriticalSection DiskArchiveAccess; ParallelFor(OutCodeIoChunks.Num(), [&OutCodeIoChunks, &IoStoreLibraryHeader, &DiskArchiveAccess, &LibraryBuffer, &SerializedShaders, &OffsetToShaderCode, InShaderOodleCompressor, InShaderOodleLevel](int32 GroupIndex) { FIoStoreShaderGroupEntry& Group = IoStoreLibraryHeader.ShaderGroupEntries[GroupIndex]; uint8* UncompressedGroupMemory = reinterpret_cast(FMemory::Malloc(Group.UncompressedSize)); check(UncompressedGroupMemory); int32 SmallestShaderInGroupByOrdinal = MAX_int32; uint8* ShaderStart = UncompressedGroupMemory; // Not worth to special-case for group size of 1 (by converting to copy) - such groups are very fast to decompress and recompress. for (uint32 IdxShaderInGroup = 0; IdxShaderInGroup < Group.NumShaders; ++IdxShaderInGroup) { int32 ShaderIndex = IoStoreLibraryHeader.ShaderIndices[Group.ShaderIndicesOffset + IdxShaderInGroup]; SmallestShaderInGroupByOrdinal = FMath::Min(SmallestShaderInGroupByOrdinal, ShaderIndex); const FIoStoreShaderCodeEntry& Shader = IoStoreLibraryHeader.ShaderEntries[ShaderIndex]; checkf((reinterpret_cast(ShaderStart) - reinterpret_cast(UncompressedGroupMemory)) == Shader.UncompressedOffsetInGroup, TEXT("Shader uncompressed offset in group does not agree with its actual placement: Shader.UncompressedOffsetInGroup=%llu, actual=%llu"), Shader.UncompressedOffsetInGroup, (reinterpret_cast(ShaderStart) - reinterpret_cast(UncompressedGroupMemory)) ); // load and decompress the shader at the desired offset const FShaderCodeEntry& IndividuallyCompressedShader = SerializedShaders.ShaderEntries[ShaderIndex]; { // small shaders might be stored without compression, handle them here if (IndividuallyCompressedShader.Size == IndividuallyCompressedShader.UncompressedSize) { FMemory::Memcpy(ShaderStart, LibraryBuffer.GetData() + OffsetToShaderCode + IndividuallyCompressedShader.Offset, IndividuallyCompressedShader.Size); } else { // This function will crash if decompression fails. ShaderCodeArchive::DecompressShaderWithOodle(ShaderStart, IndividuallyCompressedShader.UncompressedSize, LibraryBuffer.GetData() + OffsetToShaderCode + IndividuallyCompressedShader.Offset, IndividuallyCompressedShader.Size); } } ShaderStart += IndividuallyCompressedShader.UncompressedSize; } checkf((reinterpret_cast(ShaderStart) - reinterpret_cast(UncompressedGroupMemory)) == Group.UncompressedSize, TEXT("Uncompressed shader group size does not agree with the actual results (Group.UncompressedSize=%llu, actual=%llu)"), Group.UncompressedSize, (reinterpret_cast(ShaderStart) - reinterpret_cast(UncompressedGroupMemory))); // now compress the whole group int64 CompressedGroupSize = 0; uint8* CompressedShaderGroupMemory = nullptr; ShaderCodeArchive::CompressShaderWithOodle(nullptr, CompressedGroupSize, UncompressedGroupMemory, Group.UncompressedSize, InShaderOodleCompressor, InShaderOodleLevel); checkf(CompressedGroupSize > 0, TEXT("CompressedGroupSize estimate seems wrong (%lld)"), CompressedGroupSize); CompressedShaderGroupMemory = reinterpret_cast(FMemory::Malloc(CompressedGroupSize)); bool bCompressed = ShaderCodeArchive::CompressShaderWithOodle(CompressedShaderGroupMemory, CompressedGroupSize, UncompressedGroupMemory, Group.UncompressedSize, InShaderOodleCompressor, InShaderOodleLevel); checkf(bCompressed, TEXT("We could not compress the shader group after providing an estimated memory.")); TTuple& OutCodeIoChunk = OutCodeIoChunks[GroupIndex]; OutCodeIoChunk.Get<0>() = IoStoreLibraryHeader.ShaderGroupIoHashes[GroupIndex]; // This value is the load order factor for the group (the smaller, the more likely), that IoStore will use to sort the chunks. // We calculate it as the smallest shader index in the group, which shouldn't be bad. Arguably a better way would be to get the lowest-numbered shadermap that references _any_ shader in the group, // but it's much, much slower to calculate and the resulting order would be likely the same/similar most of the time checkf(SmallestShaderInGroupByOrdinal >= 0 && SmallestShaderInGroupByOrdinal < IoStoreLibraryHeader.ShaderEntries.Num(), TEXT("SmallestShaderInGroupByOrdinal has an invalid value of %d (not within [0, %d) range as expected)"), SmallestShaderInGroupByOrdinal, IoStoreLibraryHeader.ShaderEntries.Num()); OutCodeIoChunk.Get<2>() = static_cast(SmallestShaderInGroupByOrdinal); if (CompressedGroupSize < Group.UncompressedSize) { OutCodeIoChunk.Get<1>() = FIoBuffer(CompressedGroupSize); FMemory::Memcpy(OutCodeIoChunk.Get<1>().GetData(), CompressedShaderGroupMemory, CompressedGroupSize); Group.CompressedSize = CompressedGroupSize; } else { // store uncompressed (unlikely, but happens for a 200-byte sized shader that happens to get its own group) OutCodeIoChunk.Get<1>() = FIoBuffer(Group.UncompressedSize); FMemory::Memcpy(OutCodeIoChunk.Get<1>().GetData(), UncompressedGroupMemory, Group.UncompressedSize); Group.CompressedSize = Group.UncompressedSize; } FMemory::Free(UncompressedGroupMemory); FMemory::Free(CompressedShaderGroupMemory); }, EParallelForFlags::Unbalanced ); // calculate and log stats int64 TotalUncompressedSizeViaGroups = 0; // calculate total uncompressed size twice for a sanity check int64 TotalUncompressedSizeViaShaders = 0; int64 TotalIndividuallyCompressedSize = 0; int64 TotalGroupCompressedSize = 0; for (int32 IdxGroup = 0, NumGroups = IoStoreLibraryHeader.ShaderGroupEntries.Num(); IdxGroup < NumGroups; ++IdxGroup) { FIoStoreShaderGroupEntry& Group = IoStoreLibraryHeader.ShaderGroupEntries[IdxGroup]; TotalGroupCompressedSize += Group.CompressedSize; TotalUncompressedSizeViaGroups += Group.UncompressedSize; // now go via shaders for (uint32 IdxShaderInGroup = 0; IdxShaderInGroup < Group.NumShaders; ++IdxShaderInGroup) { int32 ShaderIndex = IoStoreLibraryHeader.ShaderIndices[Group.ShaderIndicesOffset + IdxShaderInGroup]; const FShaderCodeEntry& IndividuallyCompressedShader = SerializedShaders.ShaderEntries[ShaderIndex]; TotalIndividuallyCompressedSize += IndividuallyCompressedShader.Size; TotalUncompressedSizeViaShaders += IndividuallyCompressedShader.UncompressedSize; } } checkf(TotalUncompressedSizeViaGroups == TotalUncompressedSizeViaShaders, TEXT("Sanity check failure: total uncompressed shader size differs if calculated via shader groups (%lld) or individual shaders (%lld)"), TotalUncompressedSizeViaGroups, TotalUncompressedSizeViaShaders ); UE_LOG(LogIoStore, Display, TEXT("%s(%s): Recompressed %d shaders as %d groups. Library size changed from %lld KB (%.2f : 1 ratio) to %lld KB (%.2f : 1 ratio), %.2f%% of previous."), *LibraryName, *FormatName.ToString(), SerializedShaders.ShaderEntries.Num(), IoStoreLibraryHeader.ShaderGroupEntries.Num(), TotalIndividuallyCompressedSize / 1024, static_cast(TotalUncompressedSizeViaShaders) / static_cast(TotalIndividuallyCompressedSize), TotalGroupCompressedSize / 1024, static_cast(TotalUncompressedSizeViaShaders) / static_cast(TotalGroupCompressedSize), 100.0 * static_cast(TotalGroupCompressedSize) / static_cast(TotalIndividuallyCompressedSize) ); FLargeMemoryWriter IoStoreLibraryAr(0, true); FIoStoreShaderCodeArchive::SaveIoStoreShaderCodeArchive(IoStoreLibraryHeader, IoStoreLibraryAr); OutLibraryIoChunk.Key = FIoStoreShaderCodeArchive::GetShaderCodeArchiveChunkId(LibraryName, FormatName); int64 TotalSize = IoStoreLibraryAr.TotalSize(); OutLibraryIoChunk.Value = FIoBuffer(FIoBuffer::AssumeOwnership, IoStoreLibraryAr.ReleaseOwnership(), TotalSize); int32 ShaderMapCount = IoStoreLibraryHeader.ShaderMapHashes.Num(); OutShaderMaps.SetNum(ShaderMapCount); ParallelFor(OutShaderMaps.Num(), [&OutShaderMaps, &IoStoreLibraryHeader](int32 ShaderMapIndex) { TTuple>& OutShaderMap = OutShaderMaps[ShaderMapIndex]; OutShaderMap.Key = IoStoreLibraryHeader.ShaderMapHashes[ShaderMapIndex]; const FIoStoreShaderMapEntry& ShaderMapEntry = IoStoreLibraryHeader.ShaderMapEntries[ShaderMapIndex]; int32 LookupIndexEnd = ShaderMapEntry.ShaderIndicesOffset + ShaderMapEntry.NumShaders; OutShaderMap.Value.Reserve(ShaderMapEntry.NumShaders); // worst-case, 1 group == 1 shader for (int32 ShaderLookupIndex = ShaderMapEntry.ShaderIndicesOffset; ShaderLookupIndex < LookupIndexEnd; ++ShaderLookupIndex) { int32 ShaderIndex = IoStoreLibraryHeader.ShaderIndices[ShaderLookupIndex]; int32 GroupIndex = IoStoreLibraryHeader.ShaderEntries[ShaderIndex].ShaderGroupIndex; OutShaderMap.Value.AddUnique(IoStoreLibraryHeader.ShaderGroupIoHashes[GroupIndex]); } }, EParallelForFlags::Unbalanced ); OutShaderMapAssetAssociations.Reserve(ShaderCodeToAssets.Num()); for (auto& KV : ShaderCodeToAssets) { OutShaderMapAssetAssociations.Emplace(KV.Key, MoveTemp(KV.Value)); } return true; } // Carries association between shaders and packages from shader processing to size assignment. struct FShaderAssociationInfo { struct FShaderChunkInfo { enum EType { // This shader is referenced by one or more packages and can assign its size to // those packages Package, // This shader is needed by the baseline engine in some manner and is a flat // size cost. Global, // This shader isn't global or referenced by packages - theoretically shouldn't // exist. Orphan }; EType Type; TArray ReferencedByPackages; uint32 CompressedSize = 0; UE::Cook::EPluginSizeTypes SizeType; // If we are shared across plugins, this is our pseudo plugin name. This is generated during size assignment. FString SharedPluginName; }; struct FShaderChunkInfoKey { FName PakChunkName; FIoChunkId IoChunkId; friend uint32 GetTypeHash(const FShaderChunkInfoKey& Key) { return GetTypeHash(Key.IoChunkId); } friend bool operator == (const FShaderChunkInfoKey& LHS, const FShaderChunkInfoKey& RHS) { return LHS.PakChunkName == RHS.PakChunkName && LHS.IoChunkId == RHS.IoChunkId; } }; TMap ShaderChunkInfos; // The list of shaders referenced by each package. This can be used to look up in to ContainerShaderInfos. TMap> PackageShaderMap; }; static void ProcessShaderLibraries(const FIoStoreArguments& Arguments, TArray& ContainerTargets, TArray OutShaders, FShaderAssociationInfo& OutAssocInfo) { IOSTORE_CPU_SCOPE(ProcessShaderLibraries); double LibraryStart = FPlatformTime::Seconds(); TMap ChunkIdToShaderInfoMap; TMap> ShaderChunkIdsByShaderMapHash; TMap> PackageNameToShaderMaps; TMap> AllContainerShaderLibraryShadersMap; { IOSTORE_CPU_SCOPE(ConvertShaderLibraries); for (FContainerTargetSpec* ContainerTarget : ContainerTargets) { for (FContainerTargetFile& TargetFile : ContainerTarget->TargetFiles) { if (TargetFile.ChunkType == EContainerChunkType::ShaderCodeLibrary) { TSet& ContainerShaderLibraryShaders = AllContainerShaderLibraryShadersMap.FindOrAdd(ContainerTarget); TArray>> ShaderMaps; TTuple LibraryChunk; TArray> CodeChunks; TArray>> ShaderMapAssetAssociations; if (!ConvertToIoStoreShaderLibrary(*TargetFile.NormalizedSourcePath, TargetFile.ChunkId, Arguments.PackageStore.Get(), Arguments.ShaderOodleCompressor, Arguments.ShaderOodleLevel, LibraryChunk, CodeChunks, ShaderMaps, ShaderMapAssetAssociations)) { UE_LOG(LogIoStore, Warning, TEXT("Failed converting shader library '%s'"), *TargetFile.NormalizedSourcePath); continue; } TargetFile.ChunkId = LibraryChunk.Key; TargetFile.SourceBuffer.Emplace(LibraryChunk.Value); TargetFile.SourceSize = LibraryChunk.Value.GetSize(); const bool bIsGlobalShaderLibrary = FPaths::GetCleanFilename(TargetFile.NormalizedSourcePath).StartsWith(TEXT("ShaderArchive-Global-")); const FShaderInfo::EShaderType ShaderType = bIsGlobalShaderLibrary ? FShaderInfo::Global : FShaderInfo::Normal; for (const TTuple& CodeChunk : CodeChunks) { const FIoChunkId& ShaderChunkId = CodeChunk.Get<0>(); FShaderInfo* ShaderInfo = ChunkIdToShaderInfoMap.FindRef(ShaderChunkId); if (!ShaderInfo) { ShaderInfo = new FShaderInfo(); ShaderInfo->ChunkId = ShaderChunkId; ShaderInfo->CodeIoBuffer = CodeChunk.Get<1>(); ShaderInfo->LoadOrderFactor = CodeChunk.Get<2>(); OutShaders.Add(ShaderInfo); ChunkIdToShaderInfoMap.Add(ShaderChunkId, ShaderInfo); } else { // first, make sure that the code is exactly the same if (ShaderInfo->CodeIoBuffer != CodeChunk.Get<1>()) { UE_LOG(LogIoStore, Error, TEXT("Collision of two shader code chunks, same Id (%s), different code. Packaged game will likely crash, not being able to decompress the shaders."), *LexToString(ShaderChunkId) ); } // If we already exist, then we have two separate LoadOrderFactors, // which one we got first affects build determinism. Take the lower. if (CodeChunk.Get<2>() < ShaderInfo->LoadOrderFactor) { ShaderInfo->LoadOrderFactor = CodeChunk.Get<2>(); } } const FShaderInfo::EShaderType* CurrentShaderTypeInContainer = ShaderInfo->TypeInContainer.Find(ContainerTarget); if (!CurrentShaderTypeInContainer || *CurrentShaderTypeInContainer != FShaderInfo::Global) { // If a shader is both global and shared consider it to be global ShaderInfo->TypeInContainer.Add(ContainerTarget, ShaderType); } ContainerShaderLibraryShaders.Add(ShaderInfo); } for (const TTuple>& ShaderMapAssetAssociation : ShaderMapAssetAssociations) { for (const FName& PackageName : ShaderMapAssetAssociation.Value) { TSet& PackageShaderMaps = PackageNameToShaderMaps.FindOrAdd(PackageName); PackageShaderMaps.Add(ShaderMapAssetAssociation.Key); } } for (TTuple>& ShaderMap : ShaderMaps) { TArray& ShaderMapChunkIds = ShaderChunkIdsByShaderMapHash.FindOrAdd(ShaderMap.Key); ShaderMapChunkIds.Append(MoveTemp(ShaderMap.Value)); } } // end if containerchunktype shadercodelibrary } // end foreach targetfile } // end foreach container } // 1. Update ShaderInfos with which packages we reference. // 2. Add to packages which shaders we use. // 3. Add to PackageStore what shaders we use. { IOSTORE_CPU_SCOPE(UpdatePackageStoreShaders); for (FContainerTargetSpec* ContainerTarget : ContainerTargets) { for (FCookedPackage* Package : ContainerTarget->Packages) { TSet* FindShaderMapHashes = PackageNameToShaderMaps.Find(Package->PackageName); if (FindShaderMapHashes) { for (const FSHAHash& ShaderMapHash : *FindShaderMapHashes) { const TArray* FindChunkIds = ShaderChunkIdsByShaderMapHash.Find(ShaderMapHash); if (!FindChunkIds) { UE_LOG(LogIoStore, Warning, TEXT("Package '%s' in '%s' referencing missing shader map '%s'"), *Package->PackageName.ToString(), *ContainerTarget->Name.ToString(), *ShaderMapHash.ToString()); continue; } Package->ShaderMapHashes.Add(ShaderMapHash); for (const FIoChunkId& ShaderChunkId : *FindChunkIds) { FShaderInfo* ShaderInfo = ChunkIdToShaderInfoMap.FindRef(ShaderChunkId); if (!ShaderInfo) { UE_LOG(LogIoStore, Warning, TEXT("Package '%s' in '%s' referencing missing shader with chunk id '%s'"), *Package->PackageName.ToString(), *ContainerTarget->Name.ToString(), *BytesToHex(ShaderChunkId.GetData(), ShaderChunkId.GetSize())); continue; } check(ShaderInfo); ShaderInfo->ReferencedByPackages.Add(Package); Package->Shaders.AddUnique(ShaderInfo); } } } Algo::Sort(Package->ShaderMapHashes); } } } { IOSTORE_CPU_SCOPE(AddShaderChunks); for (FContainerTargetSpec* ContainerTarget : ContainerTargets) { auto AddShaderTargetFile = [&ContainerTarget](FShaderInfo* ShaderInfo) { FContainerTargetFile& ShaderTargetFile = ContainerTarget->TargetFiles.AddDefaulted_GetRef(); ShaderTargetFile.ContainerTarget = ContainerTarget; ShaderTargetFile.ChunkId = ShaderInfo->ChunkId; ShaderTargetFile.ChunkType = EContainerChunkType::ShaderCode; ShaderTargetFile.bForceUncompressed = true; ShaderTargetFile.SourceBuffer.Emplace(ShaderInfo->CodeIoBuffer); ShaderTargetFile.SourceSize = ShaderInfo->CodeIoBuffer.DataSize(); }; const TSet* FindContainerShaderLibraryShaders = AllContainerShaderLibraryShadersMap.Find(ContainerTarget); if (FindContainerShaderLibraryShaders) { for (FCookedPackage* Package : ContainerTarget->Packages) { for (FShaderInfo* ShaderInfo : Package->Shaders) { if (ShaderInfo->ReferencedByPackages.Num() == 1) { FShaderInfo::EShaderType* ShaderType = ShaderInfo->TypeInContainer.Find(ContainerTarget); if (ShaderType && *ShaderType != FShaderInfo::Global) { *ShaderType = FShaderInfo::Inline; } } } } for (FShaderInfo* ShaderInfo : *FindContainerShaderLibraryShaders) { FShaderAssociationInfo::FShaderChunkInfoKey ShaderChunkInfoKey = { ContainerTarget->Name, ShaderInfo->ChunkId }; FShaderAssociationInfo::FShaderChunkInfo& ShaderChunkInfo = OutAssocInfo.ShaderChunkInfos.Add(ShaderChunkInfoKey); FShaderInfo::EShaderType* ShaderType = ShaderInfo->TypeInContainer.Find(ContainerTarget); check(ShaderType); if (*ShaderType == FShaderInfo::Global) { ContainerTarget->GlobalShaders.Add(ShaderInfo); ShaderChunkInfo.Type = FShaderAssociationInfo::FShaderChunkInfo::Global; } else if (*ShaderType == FShaderInfo::Inline) { ContainerTarget->InlineShaders.Add(ShaderInfo); ShaderChunkInfo.Type = FShaderAssociationInfo::FShaderChunkInfo::Package; checkf(ShaderInfo->ReferencedByPackages.Num() == 1, TEXT("Inline shader chunks must be referenced by 1 package only, but shader chunk %s is referenced by %d"), *LexToString(ShaderInfo->ChunkId), ShaderInfo->ReferencedByPackages.Num()); ShaderChunkInfo.ReferencedByPackages.Add(ShaderInfo->ReferencedByPackages[FSetElementId::FromInteger(0)]->PackageName); } else if (ShaderInfo->ReferencedByPackages.Num() > 1) { ContainerTarget->SharedShaders.Add(ShaderInfo); ShaderChunkInfo.Type = FShaderAssociationInfo::FShaderChunkInfo::Package; for (FCookedPackage* Package : ShaderInfo->ReferencedByPackages) { ShaderChunkInfo.ReferencedByPackages.Add(Package->PackageName); TArray& PackageShaders = OutAssocInfo.PackageShaderMap.FindOrAdd(Package->PackageName); PackageShaders.Add(ShaderChunkInfoKey); } } else { // // Note that we can get here with shaders that get split off in to another container (e.g. sm6 shaders). // Since they are in a different pakChunk they can't get inlined or shared. However, they still "belong" to // the referencing packages for association purposes. // if (ShaderInfo->ReferencedByPackages.Num()) { ShaderChunkInfo.Type = FShaderAssociationInfo::FShaderChunkInfo::Package; for (FCookedPackage* Package : ShaderInfo->ReferencedByPackages) { ShaderChunkInfo.ReferencedByPackages.Add(Package->PackageName); TArray& PackageShaders = OutAssocInfo.PackageShaderMap.FindOrAdd(Package->PackageName); PackageShaders.Add(ShaderChunkInfoKey); } } else { ShaderChunkInfo.Type = FShaderAssociationInfo::FShaderChunkInfo::Orphan; } ContainerTarget->UniqueShaders.Add(ShaderInfo); } AddShaderTargetFile(ShaderInfo); } } } } double LibraryEnd = FPlatformTime::Seconds(); UE_LOG(LogIoStore, Display, TEXT("Shaders processed in %.2f seconds"), LibraryEnd - LibraryStart); } void InitializeContainerTargetsAndPackages( const FIoStoreArguments& Arguments, TArray& Packages, FPackageNameMap& PackageNameMap, FPackageIdMap& PackageIdMap, TArray& ContainerTargets) { auto CreateTargetFileFromCookedFile = [ &Arguments, &Packages, &PackageNameMap, &PackageIdMap](const FContainerSourceFile& SourceFile, FContainerTargetFile& OutTargetFile) -> bool { const FCookedFileStatData* OriginalCookedFileStatData = Arguments.CookedFileStatMap.Find(SourceFile.NormalizedPath); if (!OriginalCookedFileStatData) { UE_LOG(LogIoStore, Warning, TEXT("File not found: '%s'"), *SourceFile.NormalizedPath); return false; } const FCookedFileStatData* CookedFileStatData = OriginalCookedFileStatData; if (CookedFileStatData->FileType == FCookedFileStatData::PackageHeader) { FStringView NormalizedSourcePathView(SourceFile.NormalizedPath); int32 ExtensionStartIndex = GetFullExtensionStartIndex(NormalizedSourcePathView); TStringBuilder<512> UexpPath; UexpPath.Append(NormalizedSourcePathView.Left(ExtensionStartIndex)); UexpPath.Append(TEXT(".uexp")); CookedFileStatData = Arguments.CookedFileStatMap.Find(*UexpPath); if (!CookedFileStatData) { UE_LOG(LogIoStore, Warning, TEXT("Couldn't find .uexp file for: '%s'"), *SourceFile.NormalizedPath); return false; } OutTargetFile.NormalizedSourcePath = UexpPath; } else if (CookedFileStatData->FileType == FCookedFileStatData::OptionalSegmentPackageHeader) { FStringView NormalizedSourcePathView(SourceFile.NormalizedPath); int32 ExtensionStartIndex = GetFullExtensionStartIndex(NormalizedSourcePathView); TStringBuilder<512> UexpPath; UexpPath.Append(NormalizedSourcePathView.Left(ExtensionStartIndex)); UexpPath.Append(TEXT(".o.uexp")); CookedFileStatData = Arguments.CookedFileStatMap.Find(*UexpPath); if (!CookedFileStatData) { UE_LOG(LogIoStore, Warning, TEXT("Couldn't find .o.uexp file for: '%s'"), *SourceFile.NormalizedPath); return false; } OutTargetFile.NormalizedSourcePath = UexpPath; } else { OutTargetFile.NormalizedSourcePath = SourceFile.NormalizedPath; } OutTargetFile.SourceSize = uint64(CookedFileStatData->FileSize); if (CookedFileStatData->FileType == FCookedFileStatData::ShaderLibrary) { OutTargetFile.ChunkType = EContainerChunkType::ShaderCodeLibrary; } else { FLegacyCookedPackage* Package = FindOrAddLegacyPackage(Arguments, *SourceFile.NormalizedPath, Packages, PackageNameMap, PackageIdMap); OutTargetFile.Package = Package; if (!OutTargetFile.Package) { UE_LOG(LogIoStore, Warning, TEXT("Failed to obtain package name from file name '%s'"), *SourceFile.NormalizedPath); return false; } // TODO - Would be nicer to parse this from the package info rather than inferring it from the path OutTargetFile.BulkDataCookedIndex = FBulkDataCookedIndex::ParseFromPath(SourceFile.NormalizedPath); switch (CookedFileStatData->FileType) { case FCookedFileStatData::PackageData: OutTargetFile.ChunkType = EContainerChunkType::PackageData; OutTargetFile.ChunkId = CreateIoChunkId(OutTargetFile.Package->GlobalPackageId.Value(), 0, EIoChunkType::ExportBundleData); Package->FileName = SourceFile.NormalizedPath; // .uasset path Package->UAssetSize = OriginalCookedFileStatData->FileSize; Package->UExpSize = CookedFileStatData->FileSize; break; case FCookedFileStatData::BulkData: OutTargetFile.ChunkType = EContainerChunkType::BulkData; OutTargetFile.ChunkId = CreateBulkDataIoChunkId(OutTargetFile.Package->GlobalPackageId.Value(), 0, OutTargetFile.BulkDataCookedIndex.GetValue(), EIoChunkType::BulkData); OutTargetFile.Package->TotalBulkDataSize += CookedFileStatData->FileSize; break; case FCookedFileStatData::OptionalBulkData: OutTargetFile.ChunkType = EContainerChunkType::OptionalBulkData; OutTargetFile.ChunkId = CreateBulkDataIoChunkId(OutTargetFile.Package->GlobalPackageId.Value(), 0, OutTargetFile.BulkDataCookedIndex.GetValue(), EIoChunkType::OptionalBulkData); Package->TotalBulkDataSize += CookedFileStatData->FileSize; break; case FCookedFileStatData::MemoryMappedBulkData: OutTargetFile.ChunkType = EContainerChunkType::MemoryMappedBulkData; OutTargetFile.ChunkId = CreateBulkDataIoChunkId(OutTargetFile.Package->GlobalPackageId.Value(), 0, OutTargetFile.BulkDataCookedIndex.GetValue(), EIoChunkType::MemoryMappedBulkData); Package->TotalBulkDataSize += CookedFileStatData->FileSize; break; case FCookedFileStatData::OptionalSegmentPackageData: OutTargetFile.ChunkType = EContainerChunkType::OptionalSegmentPackageData; OutTargetFile.ChunkId = CreateIoChunkId(OutTargetFile.Package->GlobalPackageId.Value(), 1, EIoChunkType::ExportBundleData); Package->OptionalSegmentFileName = SourceFile.NormalizedPath; // .o.uasset path Package->OptionalSegmentUAssetSize = OriginalCookedFileStatData->FileSize; break; case FCookedFileStatData::OptionalSegmentBulkData: OutTargetFile.ChunkType = EContainerChunkType::OptionalSegmentBulkData; OutTargetFile.ChunkId = CreateBulkDataIoChunkId(OutTargetFile.Package->GlobalPackageId.Value(), 1, OutTargetFile.BulkDataCookedIndex.GetValue(), EIoChunkType::BulkData); break; default: UE_LOG(LogIoStore, Fatal, TEXT("Unexpected file type %d for file '%s'"), CookedFileStatData->FileType, *OutTargetFile.NormalizedSourcePath); return false; } if (CookedFileStatData->FileType == FCookedFileStatData::BulkData || CookedFileStatData->FileType == FCookedFileStatData::OptionalBulkData) { const FCookedPackageStore::FChunkInfo* ChunkInfo = Arguments.PackageStore->GetChunkInfoFromChunkId(OutTargetFile.ChunkId); if (!ChunkInfo) { UE_LOG(LogIoStore, Warning, TEXT("File not found in manifest: '%s'"), *SourceFile.NormalizedPath); return false; } // It is ok for the actual hash to be missing here, if so it will be calculated later OutTargetFile.ChunkHash = ChunkInfo->ChunkHash; } } // Only keep the regions for the file if neither compression nor encryption are enabled, otherwise the regions will be meaningless. if (Arguments.bFileRegions && !SourceFile.bNeedsCompression && !SourceFile.bNeedsEncryption) { // Read the matching regions file, if it exists. TStringBuilder<512> RegionsFilePath; RegionsFilePath.Append(OutTargetFile.NormalizedSourcePath); RegionsFilePath.Append(FFileRegion::RegionsFileExtension); const FCookedFileStatData* RegionsFileStatData = Arguments.CookedFileStatMap.Find(RegionsFilePath); if (RegionsFileStatData) { TUniquePtr RegionsFile(IFileManager::Get().CreateFileReader(*RegionsFilePath)); if (!RegionsFile.IsValid()) { UE_LOG(LogIoStore, Warning, TEXT("Failed reading file '%s'"), *RegionsFilePath); } else { FFileRegion::SerializeFileRegions(*RegionsFile.Get(), OutTargetFile.FileRegions); } } } return true; }; auto CreateTargetFileFromZen = [ &Arguments, &Packages, &PackageNameMap, &PackageIdMap](const FContainerSourceFile& SourceFile, FContainerTargetFile& OutTargetFile) -> bool { FCookedPackageStore& PackageStore = *Arguments.PackageStore; OutTargetFile.NormalizedSourcePath = SourceFile.NormalizedPath; FStringView Extension = GetFullExtension(SourceFile.NormalizedPath); if (Extension == TEXT(".ushaderbytecode")) { if (const FCookedFileStatData* CookedFileStatData = Arguments.CookedFileStatMap.Find(SourceFile.NormalizedPath)) { OutTargetFile.ChunkType = EContainerChunkType::ShaderCodeLibrary; OutTargetFile.SourceSize = uint64(CookedFileStatData->FileSize); OutTargetFile.ChunkId = FIoChunkId::InvalidChunkId; return true; } else if (const FCookedPackageStore::FChunkInfo* ChunkInfo = PackageStore.GetChunkInfoFromFileName(SourceFile.NormalizedPath)) { OutTargetFile.ChunkType = EContainerChunkType::ShaderCodeLibrary; OutTargetFile.SourceSize = ChunkInfo->ChunkSize; OutTargetFile.ChunkId = ChunkInfo->ChunkId; return true; } UE_LOG(LogIoStore, Warning, TEXT("File not found: '%s'"), *SourceFile.NormalizedPath); return false; } const FCookedPackageStore::FChunkInfo* ChunkInfo = PackageStore.GetChunkInfoFromFileName(SourceFile.NormalizedPath); if (!ChunkInfo) { UE_LOG(LogIoStore, Warning, TEXT("File not found in manifest: '%s'"), *SourceFile.NormalizedPath); return false; } OutTargetFile.ChunkId = ChunkInfo->ChunkId; if (ChunkInfo->ChunkSize == 0) { UE_LOG(LogIoStore, Warning, TEXT("Chunk size not found for: '%s'"), *SourceFile.NormalizedPath); return false; } OutTargetFile.SourceSize = ChunkInfo->ChunkSize; OutTargetFile.ChunkHash = ChunkInfo->ChunkHash; if (ChunkInfo->PackageName.IsNone()) { UE_LOG(LogIoStore, Warning, TEXT("Package name not found for: '%s'"), *SourceFile.NormalizedPath); return false; } OutTargetFile.Package = &FindOrAddPackage(Arguments, ChunkInfo->PackageName, Packages, PackageNameMap, PackageIdMap); const FPackageStoreEntryResource* PackageStoreEntry = PackageStore.GetPackageStoreEntry(OutTargetFile.Package->GlobalPackageId); if (!PackageStoreEntry) { UE_LOG(LogIoStore, Warning, TEXT("Failed to find package store entry for package: '%s'"), *ChunkInfo->PackageName.ToString()); return false; } OutTargetFile.Package->PackageStoreEntry = *PackageStoreEntry; // TODO - Would be nicer to parse this from the package info rather than inferring it from the path OutTargetFile.BulkDataCookedIndex = FBulkDataCookedIndex::ParseFromPath(SourceFile.NormalizedPath); if (Extension == TEXT(".m.ubulk")) { OutTargetFile.ChunkType = EContainerChunkType::MemoryMappedBulkData; OutTargetFile.Package->TotalBulkDataSize += OutTargetFile.SourceSize; } else if (Extension == TEXT(".ubulk")) { OutTargetFile.ChunkType = EContainerChunkType::BulkData; OutTargetFile.Package->TotalBulkDataSize += OutTargetFile.SourceSize; } else if (Extension == TEXT(".uptnl")) { OutTargetFile.ChunkType = EContainerChunkType::OptionalBulkData; OutTargetFile.Package->TotalBulkDataSize += OutTargetFile.SourceSize; } else if (Extension == TEXT(".uasset") || Extension == TEXT(".umap")) { OutTargetFile.ChunkType = EContainerChunkType::PackageData; OutTargetFile.Package->UAssetSize = OutTargetFile.SourceSize; } else if (Extension == TEXT(".o.uasset") || Extension == TEXT(".o.umap")) { OutTargetFile.ChunkType = EContainerChunkType::OptionalSegmentPackageData; OutTargetFile.Package->OptionalSegmentUAssetSize = OutTargetFile.SourceSize; } else if (Extension == TEXT(".o.ubulk")) { OutTargetFile.ChunkType = EContainerChunkType::OptionalSegmentBulkData; } else { UE_LOG(LogIoStore, Warning, TEXT("Unexpected file: '%s'"), *SourceFile.NormalizedPath); return false; } // Only keep the regions for the file if neither compression nor encryption are enabled, otherwise the regions will be meaningless. if (Arguments.bFileRegions && !SourceFile.bNeedsCompression && !SourceFile.bNeedsEncryption) { OutTargetFile.FileRegions = ChunkInfo->FileRegions; } return true; }; // Reserve memory for lookup maps { int32 NumSourceFiles = 0; for (const FContainerSourceSpec& ContainerSource : Arguments.Containers) { NumSourceFiles += ContainerSource.SourceFiles.Num(); } PackageNameMap.Reserve(NumSourceFiles); PackageIdMap.Reserve(NumSourceFiles); } for (const FContainerSourceSpec& ContainerSource : Arguments.Containers) { FContainerTargetSpec* ContainerTarget = AddContainer(ContainerSource.Name, ContainerTargets); ContainerTarget->OutputPath = ContainerSource.OutputPath; ContainerTarget->StageLooseFileRootPath = ContainerSource.StageLooseFileRootPath; ContainerTarget->bGenerateDiffPatch = ContainerSource.bGenerateDiffPatch; if (ContainerSource.bOnDemand) { ContainerTarget->ContainerFlags |= EIoContainerFlags::OnDemand; } if (Arguments.bSign) { ContainerTarget->ContainerFlags |= EIoContainerFlags::Signed; } if (!ContainerTarget->EncryptionKeyGuid.IsValid() && !ContainerSource.EncryptionKeyOverrideGuid.IsEmpty()) { FGuid::Parse(ContainerSource.EncryptionKeyOverrideGuid, ContainerTarget->EncryptionKeyGuid); } ContainerTarget->PatchSourceReaders = CreatePatchSourceReaders(ContainerSource.PatchSourceContainerFiles, Arguments); { IOSTORE_CPU_SCOPE(ProcessSourceFiles); bool bHasOptionalSegmentPackages = false; ContainerTarget->TargetFiles.Reserve(ContainerSource.SourceFiles.Num()); for (const FContainerSourceFile& SourceFile : ContainerSource.SourceFiles) { FContainerTargetFile TargetFile; bool bIsValidTargetFile = Arguments.PackageStore->HasZenStoreClient() ? CreateTargetFileFromZen(SourceFile, TargetFile) : CreateTargetFileFromCookedFile(SourceFile, TargetFile); if (!bIsValidTargetFile) { continue; } TargetFile.ContainerTarget = ContainerTarget; TargetFile.DestinationPath = SourceFile.DestinationPath; TargetFile.bForceUncompressed = !SourceFile.bNeedsCompression; if (SourceFile.bNeedsCompression) { ContainerTarget->ContainerFlags |= EIoContainerFlags::Compressed; } if (SourceFile.bNeedsEncryption) { ContainerTarget->ContainerFlags |= EIoContainerFlags::Encrypted; } if (TargetFile.ChunkType == EContainerChunkType::PackageData) { check(TargetFile.Package); ContainerTarget->Packages.Add(TargetFile.Package); } else if (TargetFile.ChunkType == EContainerChunkType::OptionalSegmentPackageData) { bHasOptionalSegmentPackages = true; } ContainerTarget->TargetFiles.Emplace(MoveTemp(TargetFile)); } if (bHasOptionalSegmentPackages) { if (ContainerSource.OptionalOutputPath.IsEmpty()) { ContainerTarget->OptionalSegmentOutputPath = ContainerTarget->OutputPath + FPackagePath::GetOptionalSegmentExtensionModifier(); } else { // if we have an optional output location, use that directory, combined with the name of the output path ContainerTarget->OptionalSegmentOutputPath = FPaths::Combine(ContainerSource.OptionalOutputPath, FPaths::GetCleanFilename(ContainerTarget->OutputPath) + FPackagePath::GetOptionalSegmentExtensionModifier()); } // The IoContainerId is the hash of the name of the container, which gets returned in the results // as the output path we provide with the extension removed, which for optional containers means // that it contains the .o in the name - so make sure we have a separate id for this. ContainerTarget->OptionalSegmentContainerId = FIoContainerId::FromName(*FPaths::GetCleanFilename(ContainerTarget->OptionalSegmentOutputPath)); UE_LOG(LogIoStore, Display, TEXT("Saving optional container to: '%s', id: 0x%llx (base container id: 0x%llx)"), *ContainerTarget->OptionalSegmentOutputPath, ContainerTarget->OptionalSegmentContainerId.Value(), ContainerTarget->ContainerId.Value()); } } } Algo::Sort(Packages, [](const FCookedPackage* A, const FCookedPackage* B) { return A->GlobalPackageId < B->GlobalPackageId; }); }; void LogWriterResults(const TArray& Results) { struct FContainerStats { uint64 TocCount = 0; uint64 TocSize = 0; uint64 UncompressedContainerSize = 0; uint64 CompressedContainerSize = 0; uint64 PaddingSize = 0; }; UE_LOG(LogIoStore, Display, TEXT("")); UE_LOG(LogIoStore, Display, TEXT("Container Summary")); UE_LOG(LogIoStore, Display, TEXT("==================")); UE_LOG(LogIoStore, Display, TEXT("")); UE_LOG(LogIoStore, Display, TEXT("%-50s %10s %15s %15s %20s %20s"), TEXT("Container"), TEXT("Flags"), TEXT("Chunk(s) #"), TEXT("TOC (KiB)"), TEXT("Raw Size (MiB)"), TEXT("Size (MiB)")); UE_LOG(LogIoStore, Display, TEXT("----------------------------------------------------------------------------------------------------------------------------------------------")); FContainerStats TotalStats; FContainerStats OnDemandStats; for (const FIoStoreWriterResult& Result : Results) { FString CompressionInfo = TEXT("-"); if (Result.CompressionMethod != NAME_None) { const double Procentage = (double(Result.UncompressedContainerSize - Result.CompressedContainerSize) / double(Result.UncompressedContainerSize)) * 100.0; CompressionInfo = FString::Printf(TEXT("(%.2lf%% %s)"), Procentage, *Result.CompressionMethod.ToString()); } FString ContainerSettings = FString::Printf(TEXT("%s/%s/%s/%s/%s"), EnumHasAnyFlags(Result.ContainerFlags, EIoContainerFlags::Compressed) ? TEXT("C") : TEXT("-"), EnumHasAnyFlags(Result.ContainerFlags, EIoContainerFlags::Encrypted) ? TEXT("E") : TEXT("-"), EnumHasAnyFlags(Result.ContainerFlags, EIoContainerFlags::Signed) ? TEXT("S") : TEXT("-"), EnumHasAnyFlags(Result.ContainerFlags, EIoContainerFlags::Indexed) ? TEXT("I") : TEXT("-"), EnumHasAnyFlags(Result.ContainerFlags, EIoContainerFlags::OnDemand) ? TEXT("O") : TEXT("-")); UE_LOG(LogIoStore, Display, TEXT("%-50s %10s %15llu %15.2lf %20.2lf %20.2lf %s"), *Result.ContainerName, *ContainerSettings, Result.TocEntryCount, (double)Result.TocSize / 1024.0, (double)Result.UncompressedContainerSize / 1024.0 / 1024.0, (double)Result.CompressedContainerSize / 1024.0 / 1024.0, *CompressionInfo); if (EnumHasAnyFlags(Result.ContainerFlags, EIoContainerFlags::OnDemand)) { OnDemandStats.TocCount += Result.TocEntryCount; OnDemandStats.TocSize += Result.TocSize; OnDemandStats.UncompressedContainerSize += Result.UncompressedContainerSize; OnDemandStats.CompressedContainerSize += Result.CompressedContainerSize; } TotalStats.TocCount += Result.TocEntryCount; TotalStats.TocSize += Result.TocSize; TotalStats.UncompressedContainerSize += Result.UncompressedContainerSize; TotalStats.CompressedContainerSize += Result.CompressedContainerSize; TotalStats.PaddingSize += Result.PaddingSize; } UE_LOG(LogIoStore, Display, TEXT("----------------------------------------------------------------------------------------------------------------------------------------------")); if (OnDemandStats.TocCount > 0) { UE_LOG(LogIoStore, Display, TEXT("%-50s %10s %15llu %15.2lf %20.2lf %20.2lf"), TEXT("Total On Demand"), TEXT(""), OnDemandStats.TocCount, (double)OnDemandStats.TocSize / 1024.0, (double)OnDemandStats.UncompressedContainerSize / 1024.0 / 1024.0, (double)OnDemandStats.CompressedContainerSize / 1024.0 / 1024.0); } UE_LOG(LogIoStore, Display, TEXT("%-50s %10s %15llu %15.2lf %20.2lf %20.2lf"), TEXT("Total"), TEXT(""), TotalStats.TocCount, (double)TotalStats.TocSize / 1024.0, (double)TotalStats.UncompressedContainerSize / 1024.0 / 1024.0, (double)TotalStats.CompressedContainerSize / 1024.0 / 1024.0); UE_LOG(LogIoStore, Display, TEXT("")); UE_LOG(LogIoStore, Display, TEXT("** Flags: (C)ompressed / (E)ncrypted / (S)igned) / (I)ndexed) / (O)nDemand **")); UE_LOG(LogIoStore, Display, TEXT("")); UE_LOG(LogIoStore, Display, TEXT("Compression block padding: %8.2lf MiB"), (double)TotalStats.PaddingSize / 1024.0 / 1024.0); UE_LOG(LogIoStore, Display, TEXT("")); UE_LOG(LogIoStore, Display, TEXT("Container Directory Index")); UE_LOG(LogIoStore, Display, TEXT("==========================")); UE_LOG(LogIoStore, Display, TEXT("")); UE_LOG(LogIoStore, Display, TEXT("%-45s %15s"), TEXT("Container"), TEXT("Size (KiB)")); UE_LOG(LogIoStore, Display, TEXT("----------------------------------------------------------------------------------------------------------------------------------------------")); for (const FIoStoreWriterResult& Result : Results) { UE_LOG(LogIoStore, Display, TEXT("%-45s %15.2lf"), *Result.ContainerName, double(Result.DirectoryIndexSize) / 1024.0); } UE_LOG(LogIoStore, Display, TEXT("")); UE_LOG(LogIoStore, Display, TEXT("Container Patch Report")); UE_LOG(LogIoStore, Display, TEXT("========================")); UE_LOG(LogIoStore, Display, TEXT("")); UE_LOG(LogIoStore, Display, TEXT("%-44s %16s %16s %16s %16s %16s"), TEXT("Container"), TEXT("Total #"), TEXT("Modified #"), TEXT("Added #"), TEXT("Modified (MiB)"), TEXT("Added (MiB)")); UE_LOG(LogIoStore, Display, TEXT("----------------------------------------------------------------------------------------------------------------------------------------------")); for (const FIoStoreWriterResult& Result : Results) { UE_LOG(LogIoStore, Display, TEXT("%-44s %16d %16d %16d %16.2lf %16.2lf"), *Result.ContainerName, Result.TocEntryCount, Result.ModifiedChunksCount, Result.AddedChunksCount, Result.ModifiedChunksSize / 1024.0 / 1024.0, Result.AddedChunksSize / 1024.0 / 1024.0); } } void LogContainerPackageInfo(const TArray& ContainerTargets) { uint64 TotalStoreSize = 0; uint64 TotalPackageCount = 0; uint64 TotalLocalizedPackageCount = 0; UE_LOG(LogIoStore, Display, TEXT("")); UE_LOG(LogIoStore, Display, TEXT("PackageStore")); UE_LOG(LogIoStore, Display, TEXT("=============")); UE_LOG(LogIoStore, Display, TEXT("")); UE_LOG(LogIoStore, Display, TEXT("%-45s %15s %15s %15s"), TEXT("Container"), TEXT("Size (KiB)"), TEXT("Packages #"), TEXT("Localized #")); UE_LOG(LogIoStore, Display, TEXT("----------------------------------------------------------------------------------------------------------------------------------------------")); for (const FContainerTargetSpec* ContainerTarget : ContainerTargets) { uint64 StoreSize = ContainerTarget->Header.StoreEntries.Num(); uint64 PackageCount = ContainerTarget->Packages.Num(); uint64 LocalizedPackageCount = ContainerTarget->Header.LocalizedPackages.Num(); UE_LOG(LogIoStore, Display, TEXT("%-45s %15.0lf %15llu %15llu"), *ContainerTarget->Name.ToString(), (double)StoreSize / 1024.0, PackageCount, LocalizedPackageCount); TotalStoreSize += StoreSize; TotalPackageCount += PackageCount; TotalLocalizedPackageCount += LocalizedPackageCount; } UE_LOG(LogIoStore, Display, TEXT("----------------------------------------------------------------------------------------------------------------------------------------------")); UE_LOG(LogIoStore, Display, TEXT("%-45s %15.0lf %15llu %15llu"), TEXT("Total"), (double)TotalStoreSize / 1024.0, TotalPackageCount, TotalLocalizedPackageCount); UE_LOG(LogIoStore, Display, TEXT("")); UE_LOG(LogIoStore, Display, TEXT("")); } class FIoStoreWriteRequestManager { public: FIoStoreWriteRequestManager(FPackageStoreOptimizer& InPackageStoreOptimizer, FCookedPackageStore* InPackageStore) : PackageStoreOptimizer(InPackageStoreOptimizer) , PackageStore(InPackageStore) { InitiatorThread = Async(EAsyncExecution::Thread, [this]() { InitiatorThreadFunc(); }); RetirerThread = Async(EAsyncExecution::Thread, [this]() { RetirerThreadFunc(); }); MaxSourceBufferMemory = 3ull << 30; FParse::Value(FCommandLine::Get(), TEXT("MaxSourceBufferMemory="), MaxSourceBufferMemory); MaxConcurrentSourceReads = uint32(FMath::Clamp(FPlatformMisc::NumberOfCoresIncludingHyperthreads()/2, 4, 32)); FParse::Value(FCommandLine::Get(), TEXT("MaxConcurrentSourceReads="), MaxConcurrentSourceReads); UE_LOG(LogIoStore, Display, TEXT("Initialized WriteRequestManager with MaxConcurrentSourceReads=%d (%d cores), MaxSourceBufferMemory=%lluMiB"), MaxConcurrentSourceReads, FPlatformMisc::NumberOfCoresIncludingHyperthreads(), MaxSourceBufferMemory >> 20); } ~FIoStoreWriteRequestManager() { InitiatorQueue.CompleteAdding(); RetirerQueue.CompleteAdding(); InitiatorThread.Wait(); RetirerThread.Wait(); } IIoStoreWriteRequest* Read(const FContainerTargetFile& InTargetFile) { if (InTargetFile.SourceBuffer.IsSet()) { return new FInMemoryWriteRequest(*this, InTargetFile); } else if (PackageStore->HasZenStoreClient()) { return new FZenWriteRequest(*this, InTargetFile); } else { return new FLooseFileWriteRequest(*this, InTargetFile); } } private: struct FQueueEntry; class FWriteContainerTargetFileRequest : public IIoStoreWriteRequest { friend class FIoStoreWriteRequestManager; public: virtual ~FWriteContainerTargetFileRequest() { } virtual uint64 GetOrderHint() override { return TargetFile.IdealOrder; } virtual TArrayView GetRegions() override { return FileRegions; } virtual const FIoHash* GetChunkHash() override { return TargetFile.ChunkHash.IsZero() ? nullptr : &TargetFile.ChunkHash; } virtual void PrepareSourceBufferAsync(UE::Tasks::FTaskEvent& InCompletionEvent) override { CompletionEvent.Emplace(InCompletionEvent); Manager.ScheduleLoad(this); } virtual const FIoBuffer* GetSourceBuffer() override { return &SourceBuffer; } virtual void FreeSourceBuffer() override { if (SourceBuffer.DataSize() > 0) { SourceBuffer = FIoBuffer(); Manager.OnBufferMemoryFreed(SourceBufferSize); } } virtual uint64 GetSourceBufferSizeEstimate() override { return SourceBufferSize; } virtual void LoadSourceBufferAsync() = 0; protected: FWriteContainerTargetFileRequest(FIoStoreWriteRequestManager& InManager,const FContainerTargetFile& InTargetFile) : Manager(InManager) , TargetFile(InTargetFile) , FileRegions(TargetFile.FileRegions) , SourceBufferSize(TargetFile.SourceSize) { } void OnSourceBufferLoaded(bool bTriggerCompletionEvent) { TRACE_COUNTER_DECREMENT(IoStoreSourceReadsInflight); TRACE_COUNTER_INCREMENT(IoStoreSourceReadsDone); QueueEntry->ReleaseRef(Manager); if (bTriggerCompletionEvent) { CompletionEvent.GetValue().Trigger(); } } FIoStoreWriteRequestManager& Manager; const FContainerTargetFile& TargetFile; TArray FileRegions; // Note -- this is filled with the TargetFile.SourceSize value which is the size of the buffer // used for IO, however it's not necessarily the size of the resulting input to iostore as // the buffer can be post-processed after i/o (e.g. CreateOptimizedPackage). uint64 SourceBufferSize; TOptional CompletionEvent; FIoBuffer SourceBuffer; FQueueEntry* QueueEntry = nullptr; }; class FInMemoryWriteRequest : public FWriteContainerTargetFileRequest { public: FInMemoryWriteRequest(FIoStoreWriteRequestManager& InManager, const FContainerTargetFile& InTargetFile) : FWriteContainerTargetFileRequest(InManager, InTargetFile) { } virtual void LoadSourceBufferAsync() override { Manager.MemorySourceReads[(int8)TargetFile.ChunkId.GetChunkType()].IncrementExchange(); SourceBuffer = TargetFile.SourceBuffer.GetValue(); Manager.MemorySourceBytes[(int8)TargetFile.ChunkId.GetChunkType()] += SourceBuffer.DataSize(); OnSourceBufferLoaded(/*bTriggerCompletionEvent*/ true); } }; // Used when staging from cooked files class FLooseFileWriteRequest : public FWriteContainerTargetFileRequest { public: FLooseFileWriteRequest(FIoStoreWriteRequestManager& InManager, const FContainerTargetFile& InTargetFile) : FWriteContainerTargetFileRequest(InManager, InTargetFile) , Package(static_cast(InTargetFile.Package)) { } virtual void LoadSourceBufferAsync() override { Manager.LooseFileSourceReads[(int8)TargetFile.ChunkId.GetChunkType()].IncrementExchange(); SourceBuffer = FIoBuffer(GetSourceBufferSizeEstimate()); QueueEntry->FileHandle.Reset( FPlatformFileManager::Get().GetPlatformFile().OpenAsyncRead(*TargetFile.NormalizedSourcePath)); QueueEntry->AddRef(); // Must keep it around until we've assigned the ReadRequest pointer FAsyncFileCallBack Callback = [this](bool, IAsyncReadRequest* ReadRequest) { Manager.LooseFileSourceBytes[(int8)TargetFile.ChunkId.GetChunkType()] += SourceBuffer.DataSize(); if (TargetFile.ChunkType == EContainerChunkType::PackageData) { SourceBuffer = Manager.PackageStoreOptimizer.CreatePackageBuffer(Package->OptimizedPackage, SourceBuffer); } else if (TargetFile.ChunkType == EContainerChunkType::OptionalSegmentPackageData) { check(Package->OptimizedOptionalSegmentPackage); SourceBuffer = Manager.PackageStoreOptimizer.CreatePackageBuffer(Package->OptimizedOptionalSegmentPackage, SourceBuffer); } OnSourceBufferLoaded(/*bTriggerCompletionEvent*/ true); }; QueueEntry->ReadRequest.Reset( QueueEntry->FileHandle->ReadRequest(0, SourceBuffer.DataSize(), AIOP_Normal, &Callback, SourceBuffer.Data())); QueueEntry->ReleaseRef(Manager); } private: FLegacyCookedPackage* Package; }; class FZenWriteRequest : public FWriteContainerTargetFileRequest { public: FZenWriteRequest(FIoStoreWriteRequestManager& InManager,const FContainerTargetFile& InTargetFile) : FWriteContainerTargetFileRequest(InManager, InTargetFile) {} virtual void LoadSourceBufferAsync() override { Manager.ZenSourceReads[(int8)TargetFile.ChunkId.GetChunkType()].IncrementExchange(); UE::Tasks::FTask ReadTask = Manager.PackageStore->ReadChunkAsync( TargetFile.ChunkId, [this](TIoStatusOr Status) { SourceBuffer = Status.ConsumeValueOrDie(); Manager.ZenSourceBytes[(int8)TargetFile.ChunkId.GetChunkType()] += SourceBuffer.DataSize(); OnSourceBufferLoaded(/*bTriggerCompletionEvent*/ false); }); CompletionEvent.GetValue().AddPrerequisites(ReadTask); CompletionEvent.GetValue().Trigger(); } }; struct FQueueEntry { FQueueEntry* Next = nullptr; TUniquePtr FileHandle; TUniquePtr ReadRequest; FWriteContainerTargetFileRequest* WriteRequest = nullptr; void AddRef() { ++RefCount; } void ReleaseRef(FIoStoreWriteRequestManager& Manager) { if (--RefCount == 0) { Manager.ScheduleRetire(this); } } private: TAtomic RefCount{ 1 }; }; class FQueue { public: FQueue() : Event(FPlatformProcess::GetSynchEventFromPool(false)) { } ~FQueue() { check(Head == nullptr && Tail == nullptr); FPlatformProcess::ReturnSynchEventToPool(Event); } void Enqueue(FQueueEntry* Entry) { check(!bIsDoneAdding); { FScopeLock _(&CriticalSection); if (!Tail) { Head = Tail = Entry; } else { Tail->Next = Entry; Tail = Entry; } Entry->Next = nullptr; } Event->Trigger(); } FQueueEntry* DequeueOrWait() { for (;;) { { FScopeLock _(&CriticalSection); if (Head) { FQueueEntry* Entry = Head; Head = Tail = nullptr; return Entry; } } if (bIsDoneAdding) { break; } Event->Wait(); } return nullptr; } void CompleteAdding() { bIsDoneAdding = true; Event->Trigger(); } private: FCriticalSection CriticalSection; FEvent* Event = nullptr; FQueueEntry* Head = nullptr; FQueueEntry* Tail = nullptr; TAtomic bIsDoneAdding{ false }; }; void ScheduleLoad(FWriteContainerTargetFileRequest* WriteRequest) { FQueueEntry* QueueEntry = new FQueueEntry(); QueueEntry->WriteRequest = WriteRequest; WriteRequest->QueueEntry = QueueEntry; InitiatorQueue.Enqueue(QueueEntry); } void ScheduleRetire(FQueueEntry* QueueEntry) { --NumConcurrentSourceReads; SourceReadCompletedEvent->Trigger(); RetirerQueue.Enqueue(QueueEntry); } void Start(FQueueEntry* QueueEntry) { const uint64 SourceBufferSize = QueueEntry->WriteRequest->GetSourceBufferSizeEstimate(); while (NumConcurrentSourceReads >= MaxConcurrentSourceReads) { TRACE_CPUPROFILER_EVENT_SCOPE(WaitForSourceReads); SourceReadCompletedEvent->Wait(); } uint64 LocalUsedBufferMemory = UsedBufferMemory.Load(); while (LocalUsedBufferMemory > 0 && LocalUsedBufferMemory + SourceBufferSize > MaxSourceBufferMemory) { TRACE_CPUPROFILER_EVENT_SCOPE(WaitForBufferMemory); MemoryAvailableEvent->Wait(); LocalUsedBufferMemory = UsedBufferMemory.Load(); } UsedBufferMemory.AddExchange(SourceBufferSize); TRACE_COUNTER_INCREMENT(IoStoreSourceReadsInflight); TRACE_COUNTER_ADD(IoStoreSourceReadsUsedBufferMemory, SourceBufferSize); QueueEntry->WriteRequest->LoadSourceBufferAsync(); ++NumConcurrentSourceReads; } void Retire(FQueueEntry* QueueEntry) { if (QueueEntry->ReadRequest.IsValid()) { QueueEntry->ReadRequest->WaitCompletion(); QueueEntry->ReadRequest.Reset(); QueueEntry->FileHandle.Reset(); } delete QueueEntry; } void OnBufferMemoryFreed(uint64 Size) { uint64 OldSize = UsedBufferMemory.SubExchange(Size); check(OldSize >= Size); TRACE_COUNTER_SUBTRACT(IoStoreSourceReadsUsedBufferMemory, Size); MemoryAvailableEvent->Trigger(); } void InitiatorThreadFunc() { TRACE_CPUPROFILER_EVENT_SCOPE(SourceReadInitiatorThread); for (;;) { FQueueEntry* QueueEntry = InitiatorQueue.DequeueOrWait(); if (!QueueEntry) { return; } while (QueueEntry) { FQueueEntry* Next = QueueEntry->Next; Start(QueueEntry); QueueEntry = Next; } } } void RetirerThreadFunc() { TRACE_CPUPROFILER_EVENT_SCOPE(SourceReadRetirerThread); for (;;) { FQueueEntry* QueueEntry = RetirerQueue.DequeueOrWait(); if (!QueueEntry) { return; } while (QueueEntry) { FQueueEntry* Next = QueueEntry->Next; Retire(QueueEntry); QueueEntry = Next; } } } FPackageStoreOptimizer& PackageStoreOptimizer; FCookedPackageStore* PackageStore; TFuture InitiatorThread; TFuture RetirerThread; FQueue InitiatorQueue; FQueue RetirerQueue; uint64 MaxSourceBufferMemory = 0; TAtomic UsedBufferMemory { 0 }; int32 MaxConcurrentSourceReads = 0; TAtomic NumConcurrentSourceReads { 0 }; FEventRef MemoryAvailableEvent; FEventRef SourceReadCompletedEvent; public: TAtomic ZenSourceReads[(int8)EIoChunkType::MAX] { 0 }; TAtomic ZenSourceBytes[(int8)EIoChunkType::MAX]{ 0 }; TAtomic MemorySourceReads[(int8)EIoChunkType::MAX]{ 0 }; TAtomic MemorySourceBytes[(int8)EIoChunkType::MAX]{ 0 }; TAtomic LooseFileSourceReads[(int8)EIoChunkType::MAX]{ 0 }; TAtomic LooseFileSourceBytes[(int8)EIoChunkType::MAX]{ 0 }; }; static bool WriteUtf8StringView(FUtf8StringView InView, const FString& InFilename) { TUniquePtr Ar(IFileManager::Get().CreateFileWriter(*InFilename, 0)); if (!Ar) { return false; } UTF8CHAR UTF8BOM[] = { (UTF8CHAR)0xEF, (UTF8CHAR)0xBB, (UTF8CHAR)0xBF }; Ar->Serialize(&UTF8BOM, sizeof(UTF8BOM)); Ar->Serialize((void*)InView.GetData(), InView.Len() * sizeof(UTF8CHAR)); Ar->Close(); return true; } // When we emit plugin size information, we emit one json for each class of sizes we // are monitoring. enum class EPluginGraphSizeClass : uint8 { All, Texture, StaticMesh, SoundWave, SkeletalMesh, Shader, Level, Animation, Niagara, Material, Blueprint, Geometry, Other, COUNT }; static const UTF8CHAR* PluginGraphEntryClassNames[] = { UTF8TEXT("all"), UTF8TEXT("texture"), UTF8TEXT("staticmesh"), UTF8TEXT("soundwave"), UTF8TEXT("skeletalmesh"), UTF8TEXT("shader"), UTF8TEXT("level"), UTF8TEXT("animation"), UTF8TEXT("niagara"), UTF8TEXT("material"), UTF8TEXT("blueprint"), UTF8TEXT("geometry"), UTF8TEXT("other") }; static_assert( UE_ARRAY_COUNT(PluginGraphEntryClassNames) == (size_t)EPluginGraphSizeClass::COUNT, "Must have a name for each plugin graph size class!"); struct FPluginGraphEntry { uint16 IndexInEnabledPlugins = 0; FString Name; TSet DirectDependencies; TSet TotalDependencies; TSet Roots; // Only valid if bIsRoot TSet UniqueDependencies; uint32 DirectRefcount = 0; bool bIsRoot = false; static constexpr uint8 ClassCount = (uint8)EPluginGraphSizeClass::COUNT; UE::Cook::FPluginSizeInfo ExclusiveSizes[ClassCount]; UE::Cook::FPluginSizeInfo InclusiveSizes[ClassCount]; UE::Cook::FPluginSizeInfo UniqueSizes[ClassCount]; uint64 ExclusiveCounts[ClassCount] = {}; uint64 InclusiveCounts[ClassCount] = {}; uint64 UniqueCounts[ClassCount] = {}; }; struct FPluginGraph { TArray Plugins; TMap NameToPlugin; TArray TopologicallySortedPlugins; TArray RootPlugins; // Plugins that can't trace a route between a plugin with bIsRoot==true and themselves. TSet UnrootedPlugins; }; // Rework the hierarchy in to a graph where we have output edges resolved to pointers so we can pass to // library functions. static void GeneratePluginGraph(const UE::Cook::FCookMetadataPluginHierarchy& InPluginHierarchy, FPluginGraph& OutPluginGraph) { double GeneratePluginGraphStart = FPlatformTime::Seconds(); // Allocate up front so our pointer remains stable. OutPluginGraph.Plugins.Reserve(InPluginHierarchy.PluginsEnabledAtCook.Num()); uint16 PluginIndex = 0; for (const UE::Cook::FCookMetadataPluginEntry& Plugin : InPluginHierarchy.PluginsEnabledAtCook) { FPluginGraphEntry& OurEntry = OutPluginGraph.Plugins.AddDefaulted_GetRef(); OurEntry.IndexInEnabledPlugins = PluginIndex; OurEntry.Name = Plugin.Name; // Can store pointer since we reserved as a batch.. OutPluginGraph.NameToPlugin.Add(OurEntry.Name, &OurEntry); PluginIndex++; } OutPluginGraph.RootPlugins.Reserve(InPluginHierarchy.RootPlugins.Num()); for (uint16 RootIndex : InPluginHierarchy.RootPlugins) { FPluginGraphEntry* Root = OutPluginGraph.NameToPlugin[InPluginHierarchy.PluginsEnabledAtCook[RootIndex].Name]; Root->bIsRoot = true; OutPluginGraph.RootPlugins.Add(Root); } for (FPluginGraphEntry& PluginEntry : OutPluginGraph.Plugins) { const UE::Cook::FCookMetadataPluginEntry& Plugin = InPluginHierarchy.PluginsEnabledAtCook[PluginEntry.IndexInEnabledPlugins]; for (uint32 DependencyIndex = Plugin.DependencyIndexStart; DependencyIndex < Plugin.DependencyIndexEnd; DependencyIndex++) { const UE::Cook::FCookMetadataPluginEntry& DependentPlugin = InPluginHierarchy.PluginsEnabledAtCook[InPluginHierarchy.PluginDependencies[DependencyIndex]]; PluginEntry.DirectDependencies.Add(OutPluginGraph.NameToPlugin[DependentPlugin.Name]); } } // From here on out we can operate entirely on our own data - no cook metadata structures - // and we generate the various structures we need. // Sort the plugins topologically. This means that when we iterate linearly, // we know that when we hit a plugin, we've already processed the dependencies. // This takes some memory to track edges but is a depth first search // and not anything quadratic or worse. double TopologicalSortStart = FPlatformTime::Seconds(); { OutPluginGraph.TopologicallySortedPlugins.Reserve(OutPluginGraph.Plugins.Num()); for (FPluginGraphEntry& Plugin : OutPluginGraph.Plugins) { OutPluginGraph.TopologicallySortedPlugins.Add(&Plugin); } auto GetElementDependencies = [&OutPluginGraph](const FPluginGraphEntry* PluginEntry) -> const TSet& { return OutPluginGraph.NameToPlugin[PluginEntry->Name]->DirectDependencies; }; Algo::TopologicalSort(OutPluginGraph.TopologicallySortedPlugins, GetElementDependencies); } // Gather the set of all dependencies. This ends up being technically // O(N^2) in the worst case. It's highly unlikely our plugin DAG will cause that, but we track the times // just so we can keep an eye on it if it ends up taking measurable amounts of time. double InclusiveComputeStart = FPlatformTime::Seconds(); for (FPluginGraphEntry* Plugin : OutPluginGraph.TopologicallySortedPlugins) { for (FPluginGraphEntry* Dependency : Plugin->DirectDependencies) { Plugin->TotalDependencies.Add(Dependency); Dependency->DirectRefcount++; // In the worse case this is another O(N) iteration, which makes us overall O(N^2) for (FPluginGraphEntry* TotalDependencyEntry : Dependency->TotalDependencies) { Plugin->TotalDependencies.Add(TotalDependencyEntry); } } } double InclusiveComputeEnd = FPlatformTime::Seconds(); // Generate the unique dependencies for the root plugins. This is the set of dependencies that only // belong to the root plugin and not to another. These dependencies could be referred to by another // plugin within the unique set - the only requirement is that there exists no path from _another_ // root to the dependency. for (FPluginGraphEntry* RootPlugin : OutPluginGraph.RootPlugins) { // Add us as a root entry for all our total dependencies so all plugins know which root they are in. for (FPluginGraphEntry* Dependency : RootPlugin->TotalDependencies) { OutPluginGraph.NameToPlugin[Dependency->Name]->Roots.Add(RootPlugin); } // Duplicate the TotalDependencies and then remove any dependency that // exists for another root. RootPlugin->UniqueDependencies = RootPlugin->TotalDependencies; for (FPluginGraphEntry* InnerRootPlugin : OutPluginGraph.RootPlugins) { if (RootPlugin == InnerRootPlugin) { continue; } for (FPluginGraphEntry* InnerRootDependency : InnerRootPlugin->TotalDependencies) { RootPlugin->UniqueDependencies.Remove(InnerRootDependency); } } } double UniqueEnd = FPlatformTime::Seconds(); // Generate the unrooted set. These plugins can not trace a path from _any_ root plugin // to themselves. For projects with no root plugins, this will be all plugins. { for (FPluginGraphEntry* Plugin : OutPluginGraph.TopologicallySortedPlugins) { OutPluginGraph.UnrootedPlugins.Add(Plugin); } for (FPluginGraphEntry* Plugin : OutPluginGraph.RootPlugins) { OutPluginGraph.UnrootedPlugins.Remove(Plugin); for (FPluginGraphEntry* Dependency : Plugin->TotalDependencies) { OutPluginGraph.UnrootedPlugins.Remove(Plugin); } } } double GeneratePluginGraphEnd = FPlatformTime::Seconds(); UE_LOG(LogIoStore, Log, TEXT("Generated plugin graph with %d nodes. Times: %.02f total %.02f setup, %.02f sort %.02f inclusive %.02f unique %.02f unrooted."), OutPluginGraph.TopologicallySortedPlugins.Num(), GeneratePluginGraphEnd - GeneratePluginGraphStart, TopologicalSortStart - GeneratePluginGraphStart, InclusiveComputeStart - TopologicalSortStart, InclusiveComputeEnd - InclusiveComputeStart, UniqueEnd - InclusiveComputeEnd, GeneratePluginGraphEnd - UniqueEnd); } static void InsertShadersInPluginHierarchy(UE::Cook::FCookMetadataState& InCookMetadata, FPluginGraph& InPluginGraph, FShaderAssociationInfo& InShaderAssociationInfo) { const UE::Cook::FCookMetadataPluginHierarchy& PluginHierarchy = InCookMetadata.GetPluginHierarchy(); // // Create any shader plugins we need. These are pseudo plugins that we create to hold the size information // when the packages that reference a plugin cross the plugin boundary. We name them based on their dependencies // so it's consistent across builds. These will exist whenever GFPs aren't placed entirely in their own pak chunk. // // The combinatorics are such that doing this for _all_ plugins isn't tenable. However, for product tracking we // actually only care about root GFPs. So instead of gathering all of the plugins entirely, we gather all of the // root plugins. // // The difficulty is that we don't necessarily _have_ any root plugins if the project hasn't defined any, so we // artificially stuff such plugins under "Unrooted". // // It should be noted that the entire point of root GFPs is to separate the data entirely - so if we have any // of these pseudo plugins then there is a content bug as there exists a shared dependency between two "modes". // TMap> ShaderPseudoPlugins; TMap> PluginDependenciesOnShaders; TSet ShaderRootPossibles; ShaderRootPossibles.Add(TEXT("Unrooted")); for (TPair& ShaderChunkInfo : InShaderAssociationInfo.ShaderChunkInfos) { // Only Normal shaders can be assigned if (ShaderChunkInfo.Value.Type != FShaderAssociationInfo::FShaderChunkInfo::Package) { continue; } TSet ShaderPlugins; for (FName PackageName : ShaderChunkInfo.Value.ReferencedByPackages) { FString Plugin = FPackageName::SplitPackageNameRoot(PackageName, nullptr); ShaderPlugins.Add(MoveTemp(Plugin)); } if (ShaderPlugins.Num() == 1) { // We can assign the size to this plugin when the time comes - no pseudo plugin needed. continue; } bool bAllReferencingPluginsAreRooted = true; TSet ShaderRootPlugins; for (FString& ReferencingPluginName : ShaderPlugins) { bool bReferencingPluginIsRooted = false; FPluginGraphEntry** ReferencingPlugin = InPluginGraph.NameToPlugin.Find(ReferencingPluginName); if (ReferencingPlugin) { for (FPluginGraphEntry* RootForReferencingPlugin : (*ReferencingPlugin)->Roots) { ShaderRootPlugins.Add(RootForReferencingPlugin->Name); bReferencingPluginIsRooted = true; } } if (bReferencingPluginIsRooted == false) bAllReferencingPluginsAreRooted = false; } if (ShaderRootPlugins.Num() == 0 || bAllReferencingPluginsAreRooted == false) { // Place in the unrooted list. ShaderRootPlugins.Add(TEXT("Unrooted")); } TStringBuilder<256> PseudoPluginName; PseudoPluginName.Append(TEXT("ShaderPlugin")); TStringBuilder<256> NameConcatenation; // We can't just concat the names because it'll get too long when we use the name as a filename, which is unfortunate. That being said, we // only expect this to happen in degenerate cases - so if it's not too long we list the names for convenience // otherwise we hash it. for (FString& ReferencingPlugin : ShaderRootPlugins) { NameConcatenation.Append(TEXT("_")); NameConcatenation.Append(ReferencingPlugin); } if (NameConcatenation.Len() > 100) // arbitrary length here just to try and avoid hitting MAX_PATH (260) { FXxHash64 NameHash = FXxHash64::HashBuffer(NameConcatenation.GetData(), NameConcatenation.Len()); uint8 HashBytes[8]; NameHash.ToByteArray(HashBytes); PseudoPluginName.Append(TEXT("_")); UE::String::BytesToHexLower(MakeArrayView(HashBytes), PseudoPluginName); } else { PseudoPluginName.Append(NameConcatenation); } FString PseudoPluginNameStr = PseudoPluginName.ToString(); for (FString& ReferencingPlugin : ShaderRootPlugins) { PluginDependenciesOnShaders.FindOrAdd(ReferencingPlugin).Add(PseudoPluginNameStr); } ShaderPseudoPlugins.FindOrAdd(PseudoPluginNameStr).Add(ShaderChunkInfo.Key); ShaderChunkInfo.Value.SharedPluginName = MoveTemp(PseudoPluginNameStr); } // We have the list of pseudo plugins we need to make... we need to copy and append to the list // of plugins in the cook metadata, after stripping out any previous run's pseudo plugins. TArray PluginEntries = PluginHierarchy.PluginsEnabledAtCook; for (int32 PluginIndex = 0; PluginIndex < PluginEntries.Num(); PluginIndex++) { if (PluginEntries[PluginIndex].Type == UE::Cook::ECookMetadataPluginType::Unassigned) { UE_LOG(LogIoStore, Warning, TEXT("Found unassigned plugin type in cook metadata! %s"), *PluginEntries[PluginIndex].Name); } if (PluginEntries[PluginIndex].Type == UE::Cook::ECookMetadataPluginType::ShaderPseudo) { // We can do a swap because we insert these at the end in a group so there shouldn't // by anything else after us. PluginEntries.RemoveAtSwap(PluginIndex); PluginIndex--; } } for (const TPair< FString, TArray>& ShaderPP : ShaderPseudoPlugins) { UE::Cook::FCookMetadataPluginEntry& ShaderPluginEntry = PluginEntries.AddDefaulted_GetRef(); ShaderPluginEntry.Name = ShaderPP.Key; ShaderPluginEntry.Type = UE::Cook::ECookMetadataPluginType::ShaderPseudo; } // We have to redo the dependency tree as well to add the shaders as dependencies. TMap PluginNameToIndex; int32 CurrentIndex = 0; for (UE::Cook::FCookMetadataPluginEntry& Entry : PluginEntries) { PluginNameToIndex.Add(Entry.Name, CurrentIndex); CurrentIndex++; } if (IntFitsIn(PluginEntries.Num()) == false) { UE_LOG(LogIoStore, Warning, TEXT("Post shared shader plugin count is > 65535 (%d) - not updating cook metadata!"), PluginEntries.Num()); return; } TArray DependencyList; for (UE::Cook::FCookMetadataPluginEntry& Entry : PluginEntries) { // Add the normal dependencies. Since we didn't reorder anything we can // use the old dependency list uint32 StartIndex = (uint32)DependencyList.Num(); for (uint32 DependencyIndex = Entry.DependencyIndexStart; DependencyIndex < Entry.DependencyIndexEnd; DependencyIndex++) { const UE::Cook::FCookMetadataPluginEntry& DependentPlugin = PluginHierarchy.PluginsEnabledAtCook[PluginHierarchy.PluginDependencies[DependencyIndex]]; if (DependentPlugin.Type == UE::Cook::ECookMetadataPluginType::ShaderPseudo) { // If we are rerunning stage then we might already have added shader plugins // as dependencies, which we've removed so they won't exist in the lookup // (and we want to redo them anyway). continue; } int32* PluginIndex = PluginNameToIndex.Find(DependentPlugin.Name); if (PluginIndex) { DependencyList.Add(*PluginIndex); } else { UE_LOG(LogIoStore, Warning, TEXT("Couldn't find plugin %s when re-adding dependencies."), *DependentPlugin.Name); } } // However we also need to check for shaders TArray* DependenciesOnShader = PluginDependenciesOnShaders.Find(Entry.Name); if (DependenciesOnShader) { for (FString& ShaderPluginName : (*DependenciesOnShader)) { int32* PluginIndex = PluginNameToIndex.Find(ShaderPluginName); if (PluginIndex) { DependencyList.Add(*PluginIndex); } else { UE_LOG(LogIoStore, Warning, TEXT("Couldn't find shader pseudo plugin %s when adding dependencies."), *ShaderPluginName); } } } Entry.DependencyIndexStart = StartIndex; Entry.DependencyIndexEnd = (uint32)DependencyList.Num(); } // Now blast the old one away and replace. UE::Cook::FCookMetadataPluginHierarchy& MutablePluginHierarchy = InCookMetadata.GetMutablePluginHierarchy(); MutablePluginHierarchy.PluginDependencies = MoveTemp(DependencyList); MutablePluginHierarchy.PluginsEnabledAtCook = PluginEntries; // Sanity check we assigned plugin types for (UE::Cook::FCookMetadataPluginEntry& Entry : MutablePluginHierarchy.PluginsEnabledAtCook) { if (Entry.Type == UE::Cook::ECookMetadataPluginType::Unassigned) { UE_LOG(LogIoStore, Warning, TEXT("We caused an unassigned plugin type in shader pseudo plugin generation! %s"), *Entry.Name); } } } /** * Use the name of the package to assign sizes so that we can track build size at a per-plugin level, * and write out jsons files for each plugin in to the cooked metadata directory. * * Plugins insert themselves in to the package's path at the top level. Content that is unassigned * to a plugin has either /Engine or /Game as it's top level path and will be assigned to a pseudo * plugin. */ static void UpdatePluginMetadataAndWriteJsons( const FString& InAssetRegistryFileName, TMap>>& PackageToChunks, FAssetRegistryState& AssetRegistry, UE::Cook::FCookMetadataState& CookMetadata, FShaderAssociationInfo* InShaderAssociationInfo) { double WritePluginStart = FPlatformTime::Seconds(); FPluginGraph PluginGraph; GeneratePluginGraph(CookMetadata.GetPluginHierarchy(), PluginGraph); if (InShaderAssociationInfo) { InsertShadersInPluginHierarchy(CookMetadata, PluginGraph, *InShaderAssociationInfo); // Generate the graph aggain after we've inserted the new "plugins". PluginGraph = FPluginGraph(); GeneratePluginGraph(CookMetadata.GetPluginHierarchy(), PluginGraph); } double GeneratePluginGraphEnd = FPlatformTime::Seconds(); TSet LoggedPluginNames; FTopLevelAssetPath Texture2DPath(TEXT("/Script/Engine.Texture2D")); FTopLevelAssetPath Texture2DArrayPath(TEXT("/Script/Engine.Texture2DArray")); FTopLevelAssetPath Texture3DPath(TEXT("/Script/Engine.Texture3D")); FTopLevelAssetPath TextureCubePath(TEXT("/Script/Engine.TextureCube")); FTopLevelAssetPath TextureCubeArrayPath(TEXT("/Script/Engine.TextureCubeArray")); FTopLevelAssetPath VirtualTextureBuilderPath(TEXT("/Script/Engine.VirtualTextureBuilder")); FTopLevelAssetPath StaticMeshPath(TEXT("/Script/Engine.StaticMesh")); FTopLevelAssetPath SoundWavePath(TEXT("/Script/Engine.SoundWave")); FTopLevelAssetPath SkeletalMeshPath(TEXT("/Script/Engine.SkeletalMesh")); FTopLevelAssetPath LevelPath(TEXT("/Script/Engine.World")); FTopLevelAssetPath BlueprintPath(TEXT("/Script/Engine.BlueprintGeneratedClass")); FTopLevelAssetPath AnimationSequencePath(TEXT("/Script/Engine.AnimSequence")); FTopLevelAssetPath GeometryCollectionPath(TEXT("/Script/GeometryCollectionEngine.GeometryCollection")); FTopLevelAssetPath NiagaraSystemPath(TEXT("/Script/Niagara.NiagaraSystem")); FTopLevelAssetPath MaterialInstancePath(TEXT("/Script/Engine.MaterialInstanceConstant")); if (InShaderAssociationInfo) { // // Create a bunch of "Assets" that we can iterate over in the same manner as a normal // asset and find its containing "plugin" for the purposes of assigning its size. // TArray ShaderPseudoAssets; TMap> PackageDependencyMap; // All package shaders should now either be able to be assigned to: // 1. a single package (i.e. plugin) // 2. shared between packages in 1 plugin (i.e. a plugin) // 3. shared between packages across plugins (i.e. assignable to a pseudo plugin we created earlier). // uint64 CrossPluginShaderSize = 0; uint64 SinglePluginShaderSize = 0; uint64 InlineShaderSize = 0; uint64 GlobalShaderSize = 0; uint64 OrphanShaderSize = 0; for (TPair& ShaderChunkInfo : InShaderAssociationInfo->ShaderChunkInfos) { if (ShaderChunkInfo.Value.Type == FShaderAssociationInfo::FShaderChunkInfo::Orphan) { OrphanShaderSize += ShaderChunkInfo.Value.CompressedSize; continue; } if (ShaderChunkInfo.Value.Type == FShaderAssociationInfo::FShaderChunkInfo::Global) { GlobalShaderSize += ShaderChunkInfo.Value.CompressedSize; continue; } check(ShaderChunkInfo.Value.Type == FShaderAssociationInfo::FShaderChunkInfo::Package); TStringBuilder<128> PackageName; if (ShaderChunkInfo.Value.SharedPluginName.Len()) { // We know we belong to this plugin. PackageName.Append(ShaderChunkInfo.Value.SharedPluginName); CrossPluginShaderSize += ShaderChunkInfo.Value.CompressedSize; } else { // We know all the plugin prefixes are the same for all referrers if (ShaderChunkInfo.Value.ReferencedByPackages.Num() == 1) { InlineShaderSize += ShaderChunkInfo.Value.CompressedSize; } else { SinglePluginShaderSize += ShaderChunkInfo.Value.CompressedSize; } FString Plugin = FPackageName::SplitPackageNameRoot(ShaderChunkInfo.Value.ReferencedByPackages[0], nullptr); PackageName.Append(MoveTemp(Plugin)); } FPluginGraphEntry** PluginEntryPtr = PluginGraph.NameToPlugin.Find(PackageName.ToString()); if (PluginEntryPtr) { PluginEntryPtr[0]->ExclusiveSizes[(uint8)EPluginGraphSizeClass::All][ShaderChunkInfo.Value.SizeType] += ShaderChunkInfo.Value.CompressedSize; PluginEntryPtr[0]->ExclusiveSizes[(uint8)EPluginGraphSizeClass::Shader][ShaderChunkInfo.Value.SizeType] += ShaderChunkInfo.Value.CompressedSize; } else { FString AllocatedPluginName(PackageName.ToString()); bool bAlreadyLogged = false; LoggedPluginNames.Add(AllocatedPluginName, &bAlreadyLogged); if (bAlreadyLogged == false) { UE_LOG(LogIoStore, Warning, TEXT("Plugin for shader not found: %s"), *AllocatedPluginName); } } // What to name our package? Needs to be unique. We just concat everything so a human // can trace where it came from. PackageName.Append(TEXT("/ShaderPseudoAsset_")); PackageName.Append(ShaderChunkInfo.Key.PakChunkName.ToString()); PackageName.Append(TEXT("_")); // shader hash (iochunkid) UE::String::BytesToHexLower(MakeArrayView(ShaderChunkInfo.Key.IoChunkId.GetData(), sizeof(FIoChunkId)), PackageName); ShaderPseudoAssets.Add({PackageName.ToString(), ShaderChunkInfo.Value.CompressedSize}); // Track who depends on this shader by index. for (FName ReferencingPackage : ShaderChunkInfo.Value.ReferencedByPackages) { TArray& PackageDependencies = PackageDependencyMap.FindOrAdd(ReferencingPackage); PackageDependencies.Add(ShaderPseudoAssets.Num() - 1); } } TMap> FinalizedDependencyMap; // Now convert the package dependency map in to array ranges. TArray DependencyByIndex; for (TPair>& Dependencies : PackageDependencyMap) { TPair& Entry = FinalizedDependencyMap.Add(Dependencies.Key); Entry.Key = DependencyByIndex.Num(); DependencyByIndex.Append(Dependencies.Value); Entry.Value = DependencyByIndex.Num(); } // Move the new info over to the cook metadata. UE::Cook::FCookMetadataShaderPseudoHierarchy PSH; PSH.ShaderAssets = MoveTemp(ShaderPseudoAssets); PSH.PackageShaderDependencyMap = MoveTemp(FinalizedDependencyMap); PSH.DependencyList = MoveTemp(DependencyByIndex); CookMetadata.SetShaderPseudoHieararchy(MoveTemp(PSH)); double TotalShaderSize = (double)(InlineShaderSize + CrossPluginShaderSize + SinglePluginShaderSize + GlobalShaderSize + OrphanShaderSize); UE_LOG(LogIoStore, Display, TEXT("Shader total sizes: %s single package (%.0f%%), %s single root GFP (%.0f%%), %s cross root GFP (%.0f%%), %s global (%.0f%%), %s orphan (%.0f%%) - %s assigned (%.0f%%)"), *NumberString(InlineShaderSize), 100.0 * InlineShaderSize / TotalShaderSize, *NumberString(SinglePluginShaderSize), 100.0 * SinglePluginShaderSize / TotalShaderSize, *NumberString(CrossPluginShaderSize), 100.0 * CrossPluginShaderSize / TotalShaderSize, *NumberString(GlobalShaderSize), 100.0 * GlobalShaderSize / TotalShaderSize, *NumberString(OrphanShaderSize), 100.0 * OrphanShaderSize / TotalShaderSize, *NumberString(InlineShaderSize + SinglePluginShaderSize + CrossPluginShaderSize), 100.0 * (InlineShaderSize + SinglePluginShaderSize + CrossPluginShaderSize) / TotalShaderSize ); } double AssetPackageMapStart = FPlatformTime::Seconds(); const TMap AssetPackageMap = AssetRegistry.GetAssetPackageDataMap(); for (const TPair& AssetPackage : AssetPackageMap) { if (AssetPackage.Value->DiskSize < 0) { // No data on disk! continue; } // Grab the most important asset out and use it to track largest asset classes for the plugin. // This might be null! const FAssetData* AssetData = UE::AssetRegistry::GetMostImportantAsset( AssetRegistry.CopyAssetsByPackageName(AssetPackage.Key), UE::AssetRegistry::EGetMostImportantAssetFlags::IgnoreSkipClasses); const TArray>* PackageChunks = PackageToChunks.Find(FPackageId::FromName(AssetPackage.Key)); if (PackageChunks == nullptr) { // This happens when the package has been stripped by UAT prior to staging by e.g. PakDenyList. continue; } UE::Cook::FPluginSizeInfo PackageSizes; for (const FIoStoreChunkSource& ChunkInfo : *PackageChunks) { PackageSizes[ChunkInfo.SizeType] += ChunkInfo.ChunkInfo.CompressedSize; } // Assign the size to the package's plugin. { FString PluginName = FPackageName::SplitPackageNameRoot(AssetPackage.Key, nullptr); FPluginGraphEntry** PluginEntryPtr = PluginGraph.NameToPlugin.Find(PluginName); if (PluginEntryPtr) { FPluginGraphEntry* PluginEntry = *PluginEntryPtr; PluginEntry->ExclusiveSizes[(uint8)EPluginGraphSizeClass::All].Add(PackageSizes); // If we have asset class info and it's a top contender, track it also. if (AssetData != nullptr) { EPluginGraphSizeClass AssetSizeClass = EPluginGraphSizeClass::Other; if (AssetData->AssetClassPath == Texture2DPath || AssetData->AssetClassPath == Texture3DPath || AssetData->AssetClassPath == TextureCubePath || AssetData->AssetClassPath == TextureCubeArrayPath || AssetData->AssetClassPath == Texture2DArrayPath || AssetData->AssetClassPath == VirtualTextureBuilderPath) { AssetSizeClass = EPluginGraphSizeClass::Texture; } else if (AssetData->AssetClassPath == StaticMeshPath) { AssetSizeClass = EPluginGraphSizeClass::StaticMesh; } else if (AssetData->AssetClassPath == SoundWavePath) { AssetSizeClass = EPluginGraphSizeClass::SoundWave; } else if (AssetData->AssetClassPath == SkeletalMeshPath) { AssetSizeClass = EPluginGraphSizeClass::SkeletalMesh; } else if (AssetData->AssetClassPath == AnimationSequencePath) { AssetSizeClass = EPluginGraphSizeClass::Animation; } else if (AssetData->AssetClassPath == NiagaraSystemPath) { AssetSizeClass = EPluginGraphSizeClass::Niagara; } else if (AssetData->AssetClassPath == MaterialInstancePath) { AssetSizeClass = EPluginGraphSizeClass::Material; } else if (AssetData->AssetClassPath == LevelPath) { AssetSizeClass = EPluginGraphSizeClass::Level; } else if (AssetData->AssetClassPath == BlueprintPath) { AssetSizeClass = EPluginGraphSizeClass::Blueprint; } else if (AssetData->AssetClassPath == GeometryCollectionPath) { AssetSizeClass = EPluginGraphSizeClass::Geometry; } // Note that we can't get shaders here so we don't need to handle ::Shader. PluginEntry->ExclusiveSizes[(uint8)AssetSizeClass].Add(PackageSizes); PluginEntry->ExclusiveCounts[(uint8)AssetSizeClass]++; } } else { bool bAlreadyLogged = false; LoggedPluginNames.Add(MoveTemp(PluginName), &bAlreadyLogged); if (bAlreadyLogged == false) { UE_LOG(LogIoStore, Display, TEXT("Plugin for package not found: %s"), *AssetPackage.Key.ToString()); } } } } // Inclusive is the sum of us plus all our dependencies. for (FPluginGraphEntry* PluginEntry : PluginGraph.TopologicallySortedPlugins) { for (uint8 ClassIndex = 0; ClassIndex < FPluginGraphEntry::ClassCount; ClassIndex++) { PluginEntry->InclusiveSizes[ClassIndex] = PluginEntry->ExclusiveSizes[ClassIndex]; PluginEntry->InclusiveCounts[ClassIndex] = PluginEntry->ExclusiveCounts[ClassIndex]; } for (FPluginGraphEntry* Dependency : PluginEntry->TotalDependencies) { for (uint8 ClassIndex = 0; ClassIndex < FPluginGraphEntry::ClassCount; ClassIndex++) { PluginEntry->InclusiveSizes[ClassIndex].Add(Dependency->ExclusiveSizes[ClassIndex]); PluginEntry->InclusiveCounts[ClassIndex] += Dependency->ExclusiveCounts[ClassIndex]; } } } // Now we need to find the unique size for each root plugin. This is the size of dependencies that only // belong to the root plugin and not to another. Conceptually this is the "assuming all other roots are // installed, this is the size cost to add this plugin to the install". for (FPluginGraphEntry* RootPlugin : PluginGraph.RootPlugins) { for (FPluginGraphEntry* UniqueDependency : RootPlugin->UniqueDependencies) { for (uint8 ClassIndex = 0; ClassIndex < FPluginGraphEntry::ClassCount; ClassIndex++) { RootPlugin->UniqueSizes[ClassIndex].Add(UniqueDependency->ExclusiveSizes[ClassIndex]); RootPlugin->UniqueCounts[ClassIndex] += UniqueDependency->ExclusiveCounts[ClassIndex]; } } } // Find the total size of all plugins that aren't rooted in the root set. UE::Cook::FPluginSizeInfo UnrootedTotal; for (FPluginGraphEntry* Plugin : PluginGraph.UnrootedPlugins) { UnrootedTotal.Add(Plugin->ExclusiveSizes[(uint8)EPluginGraphSizeClass::All]); } double WriteBegin = FPlatformTime::Seconds(); UE::Cook::FCookMetadataPluginHierarchy& MutablePluginHierarchy = CookMetadata.GetMutablePluginHierarchy(); auto GeneratePluginJson = [&MutablePluginHierarchy](TUtf8StringBuilder<4096>& OutPluginMetadataJson, FStringView InName, const FPluginGraphEntry& InGraphEntry, EPluginGraphSizeClass InSizeClass) { OutPluginMetadataJson.Reset(); uint8 SizeClass = (uint8)InSizeClass; if (InGraphEntry.InclusiveSizes[SizeClass].TotalSize()== 0) { // Asset type or its dependencies do not contribute to the record. return; } OutPluginMetadataJson << "{\n"; OutPluginMetadataJson << "\t\"name\":\"" << InName << "\",\n"; OutPluginMetadataJson << "\t\"schema_version\":4,\n"; OutPluginMetadataJson << "\t\"is_root_plugin\":" << (InGraphEntry.bIsRoot ? TEXTVIEW("true") : TEXTVIEW("false")) << ",\n"; OutPluginMetadataJson << "\t\"asset_sizes_class\":\"" << PluginGraphEntryClassNames[SizeClass] << "\",\n"; OutPluginMetadataJson << "\t\"exclusive_asset_class_count\":" << InGraphEntry.ExclusiveCounts[SizeClass] << ",\n"; OutPluginMetadataJson << "\t\"exclusive_installed\":" << InGraphEntry.ExclusiveSizes[SizeClass][UE::Cook::EPluginSizeTypes::Installed] << ",\n"; OutPluginMetadataJson << "\t\"exclusive_optional\":" << InGraphEntry.ExclusiveSizes[SizeClass][UE::Cook::EPluginSizeTypes::Optional] << ",\n"; OutPluginMetadataJson << "\t\"exclusive_ias\":" << InGraphEntry.ExclusiveSizes[SizeClass][UE::Cook::EPluginSizeTypes::Streaming] << ",\n"; OutPluginMetadataJson << "\t\"exclusive_optionalsegment\":" << InGraphEntry.ExclusiveSizes[SizeClass][UE::Cook::EPluginSizeTypes::OptionalSegment] << ",\n"; OutPluginMetadataJson << "\t\"inclusive_installed\":" << InGraphEntry.InclusiveSizes[SizeClass][UE::Cook::EPluginSizeTypes::Installed] << ",\n"; OutPluginMetadataJson << "\t\"inclusive_optional\":" << InGraphEntry.InclusiveSizes[SizeClass][UE::Cook::EPluginSizeTypes::Optional] << ",\n"; OutPluginMetadataJson << "\t\"inclusive_ias\":" << InGraphEntry.InclusiveSizes[SizeClass][UE::Cook::EPluginSizeTypes::Streaming] << ",\n"; OutPluginMetadataJson << "\t\"inclusive_optionalsegment\":" << InGraphEntry.InclusiveSizes[SizeClass][UE::Cook::EPluginSizeTypes::OptionalSegment] << ",\n"; // this only has values for is_root_plugin == true. OutPluginMetadataJson << "\t\"unique_installed\":" << InGraphEntry.UniqueSizes[SizeClass][UE::Cook::EPluginSizeTypes::Installed] << ",\n"; OutPluginMetadataJson << "\t\"unique_optional\":" << InGraphEntry.UniqueSizes[SizeClass][UE::Cook::EPluginSizeTypes::Optional] << ",\n"; OutPluginMetadataJson << "\t\"unique_ias\":" << InGraphEntry.UniqueSizes[SizeClass][UE::Cook::EPluginSizeTypes::Streaming] << ",\n"; OutPluginMetadataJson << "\t\"unique_optionalsegment\":" << InGraphEntry.UniqueSizes[SizeClass][UE::Cook::EPluginSizeTypes::OptionalSegment] << ",\n"; OutPluginMetadataJson << "\t\"direct_refcount\":" << InGraphEntry.DirectRefcount << ",\n"; // pass through any custom fields that were added to the cook metadata. if (InGraphEntry.IndexInEnabledPlugins != TNumericLimits::Max()) { const UE::Cook::FCookMetadataPluginEntry& CookMetadataEntry = MutablePluginHierarchy.PluginsEnabledAtCook[InGraphEntry.IndexInEnabledPlugins]; for (const TPair& CustomValue : CookMetadataEntry.CustomFields) { const FString& FieldName = MutablePluginHierarchy.CustomFieldEntries[CustomValue.Key].Name; UE::Cook::ECookMetadataCustomFieldType FieldType = MutablePluginHierarchy.CustomFieldEntries[CustomValue.Key].Type; OutPluginMetadataJson << "\t\"" << FieldName; if (CustomValue.Value.IsType()) { check(FieldType == UE::Cook::ECookMetadataCustomFieldType::Bool); OutPluginMetadataJson << (CustomValue.Value.Get() ? "\":true,\n" : "\":false,\n"); } else { check(FieldType == UE::Cook::ECookMetadataCustomFieldType::String); OutPluginMetadataJson << "\":\"" << CustomValue.Value.Get() << "\",\n"; } } } { OutPluginMetadataJson << "\t\"roots\":["; int32 RootIndex = 0; for (FPluginGraphEntry* Root : InGraphEntry.Roots) { OutPluginMetadataJson << "\"" << Root->Name << "\""; if (RootIndex + 1 < InGraphEntry.Roots.Num()) { OutPluginMetadataJson << ","; } RootIndex++; } OutPluginMetadataJson << "]\n"; } OutPluginMetadataJson << "}\n"; }; // // Generate the plugin_summary jsons. // // Also write a csv for easier browsing in spreadsheets. TUtf8StringBuilder<4096> Csv; Csv.Append("name,asset_sizes_class,exclusive_installed,exclusive_optional,exclusive_ias,inclusive_installed,inclusive_optional,inclusive_ias,unique_installed,unique_optional,unique_ias,direct_refcount,total_dependency_count\n"); // This is so re-staging the same cook is consistent. for (UE::Cook::FCookMetadataPluginEntry& Plugin : MutablePluginHierarchy.PluginsEnabledAtCook) { Plugin.InclusiveSizes.Zero(); Plugin.ExclusiveSizes.Zero(); } // Instead of writing out a ton of small event files we concat them all. There are a lot in a mature project. // This will be megabytes. TArray PluginMetadataFullJson; uint32 PluginsAddedToFullJson = 0; PluginMetadataFullJson.Append(UTF8TEXTVIEW("{ \"PluginSizeInfos\": [")); auto AddPluginJsonToFull = [&PluginMetadataFullJson, &PluginsAddedToFullJson](TUtf8StringBuilder<4096>& InJsonToAdd) { if (PluginsAddedToFullJson) { PluginMetadataFullJson.Add(UTF8TEXT(',')); } PluginsAddedToFullJson++; PluginMetadataFullJson.Append(InJsonToAdd.GetData(), InJsonToAdd.Len()); }; TUtf8StringBuilder<4096> PluginMetadataJson; for (UE::Cook::FCookMetadataPluginEntry& Plugin : MutablePluginHierarchy.PluginsEnabledAtCook) { const FPluginGraphEntry& PluginEntry = *PluginGraph.NameToPlugin[Plugin.Name]; if (PluginEntry.InclusiveSizes[(uint8)EPluginGraphSizeClass::All].TotalSize() == 0) { continue; } Plugin.InclusiveSizes = PluginEntry.InclusiveSizes[(uint8)EPluginGraphSizeClass::All]; Plugin.ExclusiveSizes = PluginEntry.ExclusiveSizes[(uint8)EPluginGraphSizeClass::All]; for (uint8 ClassIndex = 0; ClassIndex < FPluginGraphEntry::ClassCount; ClassIndex++) { GeneratePluginJson(PluginMetadataJson, Plugin.Name, PluginEntry, (EPluginGraphSizeClass)ClassIndex); if (PluginMetadataJson.Len()>0) { AddPluginJsonToFull(PluginMetadataJson); Csv.Appendf("%ls,%s,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%llu,%u,%u\n", *Plugin.Name, PluginGraphEntryClassNames[ClassIndex], PluginEntry.ExclusiveSizes[ClassIndex][UE::Cook::EPluginSizeTypes::Installed], PluginEntry.ExclusiveSizes[ClassIndex][UE::Cook::EPluginSizeTypes::Optional], PluginEntry.ExclusiveSizes[ClassIndex][UE::Cook::EPluginSizeTypes::Streaming], PluginEntry.InclusiveSizes[ClassIndex][UE::Cook::EPluginSizeTypes::Installed], PluginEntry.InclusiveSizes[ClassIndex][UE::Cook::EPluginSizeTypes::Optional], PluginEntry.InclusiveSizes[ClassIndex][UE::Cook::EPluginSizeTypes::Streaming], PluginEntry.UniqueSizes[ClassIndex][UE::Cook::EPluginSizeTypes::Installed], PluginEntry.UniqueSizes[ClassIndex][UE::Cook::EPluginSizeTypes::Optional], PluginEntry.UniqueSizes[ClassIndex][UE::Cook::EPluginSizeTypes::Streaming], PluginEntry.DirectRefcount, PluginEntry.TotalDependencies.Num()); } } } // Also write a json that contains the sizes for the plugins that don't belong to any root plugin, // so that size information never gets lost. { FPluginGraphEntry OrphanedEntry; OrphanedEntry.bIsRoot = true; OrphanedEntry.DirectRefcount = 0; OrphanedEntry.InclusiveSizes[(uint8)EPluginGraphSizeClass::All] = UnrootedTotal; GeneratePluginJson(PluginMetadataJson, TEXT("OrphanedPlugins"), OrphanedEntry, EPluginGraphSizeClass::All); if (PluginMetadataJson.Len() > 0) { AddPluginJsonToFull(PluginMetadataJson); } Csv.Appendf("OrphanedPlugins,all,0,0,0,%llu,%llu,%llu,0,%u\n", UnrootedTotal[UE::Cook::EPluginSizeTypes::Installed], UnrootedTotal[UE::Cook::EPluginSizeTypes::Optional], UnrootedTotal[UE::Cook::EPluginSizeTypes::Streaming], PluginGraph.UnrootedPlugins.Num()); } PluginMetadataFullJson.Append(UTF8TEXTVIEW("]}")); { FString JsonFilename = FPaths::GetPath(InAssetRegistryFileName) / TEXT("plugin_size_jsons.json"); if (WriteUtf8StringView(MakeStringView(PluginMetadataFullJson), JsonFilename) == false) { UE_LOG(LogIoStore, Error, TEXT("Unable to write plugin json file: %s"), *JsonFilename); return; } } { FString CsvFilename = FPaths::GetPath(InAssetRegistryFileName) / TEXT("plugin_sizes.csv"); if (WriteUtf8StringView(Csv.ToView(), CsvFilename) == false) { UE_LOG(LogIoStore, Error, TEXT("Unable to write plugin csv file: %s"), *CsvFilename); return; } } double WritePluginEnd = FPlatformTime::Seconds(); UE_LOG(LogIoStore, Display, TEXT("Wrote plugin size jsons/csv in %.2f seconds [graph %.2f shaders %.2f sizes %.2f writes %.2f]"), WritePluginEnd - WritePluginStart, GeneratePluginGraphEnd - WritePluginStart, AssetPackageMapStart - GeneratePluginGraphEnd, WriteBegin - AssetPackageMapStart, WritePluginEnd - WriteBegin); } static void AddChunkInfoToAssetRegistry(TMap>>&& PackageToChunks, FAssetRegistryState& AssetRegistry, const FShaderAssociationInfo* ShaderSizeInfo, uint64 InUnassignableShaderCodeBytes, uint64 InAssignableShaderCodeBytes, uint64 TotalCompressedSize) { // // The asset registry has the chunks associate with each package, so we can just iterate the // packages, look up the chunk info, and then save the tags. // // The complicated thing is (as usual), trying to determine which asset gets the blame for the // data. We use the GetMostImportantAsset function for this. // const TMap AssetPackageMap = AssetRegistry.GetAssetPackageDataMap(); uint64 AssetsCompressedSize = 0; uint64 UpdatedAssetCount = 0; for (const TPair& AssetPackage : AssetPackageMap) { if (AssetPackage.Value->DiskSize < 0) { // No data on disk! continue; } FPackageId PackageId = FPackageId::FromName(AssetPackage.Key); const FAssetData* AssetData = UE::AssetRegistry::GetMostImportantAsset( AssetRegistry.CopyAssetsByPackageName(AssetPackage.Key), UE::AssetRegistry::EGetMostImportantAssetFlags::IgnoreSkipClasses); if (AssetData == nullptr) { // e.g. /Script packages. continue; } const TArray>* PackageChunks = PackageToChunks.Find(PackageId); if (PackageChunks == nullptr) { // This happens when the package has been stripped by UAT prior to staging by e.g. PakDenyList. continue; } UE::Cook::FPluginSizeInfo PackageCompressedSize; UE::Cook::FPluginSizeInfo PackageSize; int32 ChunkCount = 0; for (const FIoStoreChunkSource& ChunkInfo : *PackageChunks) { ChunkCount++; PackageSize[ChunkInfo.SizeType] += ChunkInfo.ChunkInfo.Size; PackageCompressedSize[ChunkInfo.SizeType] += ChunkInfo.ChunkInfo.CompressedSize; } FAssetDataTagMap TagsAndValues; TagsAndValues.Add(UE::AssetRegistry::Stage_ChunkCountFName, LexToString(ChunkCount)); TagsAndValues.Add(UE::AssetRegistry::Stage_ChunkSizeFName, LexToString(PackageSize.TotalSize())); TagsAndValues.Add(UE::AssetRegistry::Stage_ChunkCompressedSizeFName, LexToString(PackageCompressedSize.TotalSize())); TagsAndValues.Add(UE::AssetRegistry::Stage_ChunkInstalledSizeFName, LexToString(PackageCompressedSize[UE::Cook::EPluginSizeTypes::Installed])); TagsAndValues.Add(UE::AssetRegistry::Stage_ChunkStreamingSizeFName, LexToString(PackageCompressedSize[UE::Cook::EPluginSizeTypes::Streaming])); TagsAndValues.Add(UE::AssetRegistry::Stage_ChunkOptionalSizeFName, LexToString(PackageCompressedSize[UE::Cook::EPluginSizeTypes::Optional])); AssetRegistry.AddTagsToAssetData(AssetData->GetSoftObjectPath(), MoveTemp(TagsAndValues)); // We assign a package's chunks to a single asset, remove it from the list so that // at the end we can track how many chunks don't get assigned. PackageToChunks.Remove(PackageId); UpdatedAssetCount++; AssetsCompressedSize += PackageCompressedSize.TotalSize(); } // PackageToChunks now has chunks that we never assigned to an asset, and so aren't accounted for. uint64 RemainingByType[(uint8)EIoChunkType::MAX] = {}; for (auto PackageChunks : PackageToChunks) { for (FIoStoreChunkSource& Info : PackageChunks.Value) { RemainingByType[(uint8)Info.ChunkInfo.ChunkType] += Info.ChunkInfo.CompressedSize; } } // Shaders aren't in the PackageToChunks map, but we want to numbers reported to include them. RemainingByType[(uint8)EIoChunkType::ShaderCode] += InUnassignableShaderCodeBytes; AssetsCompressedSize += InAssignableShaderCodeBytes; double PercentAssets = 1.0f; if (TotalCompressedSize != 0) { PercentAssets = AssetsCompressedSize / (double)TotalCompressedSize; } UE_LOG(LogIoStore, Display, TEXT("Added chunk metadata to %s assets."), *FText::AsNumber(UpdatedAssetCount).ToString()); UE_LOG(LogIoStore, Display, TEXT("Assets represent %s bytes of %s chunk bytes (%.1f%%)"), *FText::AsNumber(AssetsCompressedSize).ToString(), *FText::AsNumber(TotalCompressedSize).ToString(), 100 * PercentAssets); UE_LOG(LogIoStore, Display, TEXT("Remaining data by chunk type:")); for (uint8 TypeIndex = 0; TypeIndex < (uint8)EIoChunkType::MAX; TypeIndex++) { if (RemainingByType[TypeIndex] != 0) { UE_LOG(LogIoStore, Display, TEXT(" %-24s%s"), *LexToString((EIoChunkType)TypeIndex), *FText::AsNumber(RemainingByType[TypeIndex]).ToString()); } } } static bool SaveAssetRegistry(const FString& InAssetRegistryFileName, FAssetRegistryState& InAssetRegistry, uint64* OutDevArHash) { TRACE_CPUPROFILER_EVENT_SCOPE(SavingAssetRegistry); FLargeMemoryWriter SerializedAssetRegistry; if (InAssetRegistry.Save(SerializedAssetRegistry, FAssetRegistrySerializationOptions(UE::AssetRegistry::ESerializationTarget::ForDevelopment)) == false) { UE_LOG(LogIoStore, Error, TEXT("Failed to serialize asset registry to memory.")); return false; } if (OutDevArHash) { *OutDevArHash = UE::Cook::FCookMetadataState::ComputeHashOfDevelopmentAssetRegistry(MakeMemoryView(SerializedAssetRegistry.GetData(), SerializedAssetRegistry.TotalSize())); } FString OutputFileName = InAssetRegistryFileName + TEXT(".temp"); TUniquePtr Writer = TUniquePtr(IFileManager::Get().CreateFileWriter(*OutputFileName)); if (!Writer) { UE_LOG(LogIoStore, Error, TEXT("Failed to open destination asset registry. (%s)"), *OutputFileName); return false; } Writer->Serialize(SerializedAssetRegistry.GetData(), SerializedAssetRegistry.TotalSize()); // Always explicitly close to catch errors from flush/close Writer->Close(); if (Writer->IsError() || Writer->IsCriticalError()) { UE_LOG(LogIoStore, Error, TEXT("Failed to write asset registry to disk. (%s)"), *OutputFileName); return false; } // Move our temp file over the original asset registry. if (IFileManager::Get().Move(*InAssetRegistryFileName, *OutputFileName) == false) { // Error already logged by FileManager return false; } UE_LOG(LogIoStore, Display, TEXT("Saved asset registry to disk. (%s)"), *InAssetRegistryFileName); return true; } struct FIoStoreWriterInfo { UE::Cook::EPluginSizeTypes SizeType; FName PakChunkName; }; static bool DoAssetRegistryWritebackDuringStage( EAssetRegistryWritebackMethod InMethod, bool bInWritePluginMetadata, FCookedPackageStore* InPackageStore, const FString& InCookedDir, bool bInCompressionEnabled, TArray>& InIoStoreWriters, TArray& InIoStoreWriterInfos, FShaderAssociationInfo& InShaderAssociationInfo ) { // This version called during container creation. TRACE_CPUPROFILER_EVENT_SCOPE(UpdateAssetRegistryWithSizeInfo); UE_LOG(LogIoStore, Display, TEXT("Adding staging metadata to asset registry...")); // The overwhelming majority of time for the asset registry writeback is loading and saving. FString AssetRegistryFileName; FAssetRegistryState AssetRegistry; FString CookMetadataFileName; UE::Cook::FCookMetadataState CookMetadata; ECookMetadataFiles FilesNeeded = ECookMetadataFiles::AssetRegistry; // We always need the cook metadata in order to update the corresponding hash. EnumAddFlags(FilesNeeded, ECookMetadataFiles::CookMetadata); if (FindAndLoadMetadataFiles(InPackageStore, InCookedDir, FilesNeeded, AssetRegistry, &AssetRegistryFileName, &CookMetadata, &CookMetadataFileName) == ECookMetadataFiles::None) { // already logged return false; } // // We want to separate out the sizes based on where they go in the end product. // uint64 UnassignableShaderCodeBytes = 0; uint64 AssignableShaderCodeBytes = 0; UE::Cook::FPluginSizeInfo ProductSize; TMap>> PackageToChunks; { int32 IoStoreWriterIndex = 0; for (TSharedPtr IoStoreWriter : InIoStoreWriters) { IoStoreWriter->EnumerateChunks( [&PackageToChunks, IoStoreWriterInfo = InIoStoreWriterInfos[IoStoreWriterIndex], &ProductSize, &InShaderAssociationInfo, &UnassignableShaderCodeBytes, &AssignableShaderCodeBytes ](const FIoStoreTocChunkInfo& ChunkInfo) { ProductSize[IoStoreWriterInfo.SizeType] += ChunkInfo.CompressedSize; // Shader code chunks don't have the package in their chunk id, so we have to use other data to look // it up and find it. FPackageId PackageId = PackageIdFromChunkId(ChunkInfo.Id); if (ChunkInfo.ChunkType == EIoChunkType::ShaderCode) { // Update size info for the shader. FShaderAssociationInfo::FShaderChunkInfo* ShaderChunkInfo = InShaderAssociationInfo.ShaderChunkInfos.Find({IoStoreWriterInfo.PakChunkName, ChunkInfo.Id}); ShaderChunkInfo->CompressedSize = ChunkInfo.CompressedSize; ShaderChunkInfo->SizeType = IoStoreWriterInfo.SizeType; // Shaders don't put their package in their chunk - they are just a hash. We have the list of them // already in the shader association so we don't bother adding here. However some can't get assigned to // anything and so we track that size for reporting. if (ShaderChunkInfo->Type == FShaderAssociationInfo::FShaderChunkInfo::Global || ShaderChunkInfo->Type == FShaderAssociationInfo::FShaderChunkInfo::Orphan) { UnassignableShaderCodeBytes += ChunkInfo.CompressedSize; } else { AssignableShaderCodeBytes += ChunkInfo.CompressedSize; } } else { PackageToChunks.FindOrAdd(PackageId).Add({ChunkInfo, IoStoreWriterInfo.SizeType}); } return true; }); IoStoreWriterIndex++; } } if (bInWritePluginMetadata) { UpdatePluginMetadataAndWriteJsons(AssetRegistryFileName, PackageToChunks, AssetRegistry, CookMetadata, &InShaderAssociationInfo); } AddChunkInfoToAssetRegistry(MoveTemp(PackageToChunks), AssetRegistry, &InShaderAssociationInfo, UnassignableShaderCodeBytes, AssignableShaderCodeBytes, ProductSize.TotalSize()); uint64 UpdatedDevArHash = 0; switch (InMethod) { case EAssetRegistryWritebackMethod::OriginalFile: { // Write to an adjacent file and move after if (SaveAssetRegistry(AssetRegistryFileName, AssetRegistry, &UpdatedDevArHash) == false) { return false; } break; } case EAssetRegistryWritebackMethod::AdjacentFile: { if (SaveAssetRegistry(AssetRegistryFileName.Replace(TEXT(".bin"), TEXT("Staged.bin")), AssetRegistry, &UpdatedDevArHash) == false) { return false; } break; } default: { UE_LOG(LogIoStore, Error, TEXT("Invalid asset registry writeback method (should already be handled!) (%d)"), int(InMethod)); return false; } } // Since we modified the dev ar, we need to save the updated hash in the cook metadata so it can still validate. CookMetadata.SetSizesPresent(bInCompressionEnabled ? UE::Cook::ECookMetadataSizesPresent::Compressed : UE::Cook::ECookMetadataSizesPresent::Uncompressed); CookMetadata.SetAssociatedDevelopmentAssetRegistryHashPostWriteback(UpdatedDevArHash); FArrayWriter SerializedCookMetadata; CookMetadata.Serialize(SerializedCookMetadata); FString TempFileName = CookMetadataFileName + TEXT(".temp"); if (FFileHelper::SaveArrayToFile(SerializedCookMetadata, *TempFileName)) { // Move our temp file over the original asset registry. if (IFileManager::Get().Move(*CookMetadataFileName, *TempFileName) == false) { // Error already logged by FileManager return false; } } else { UE_LOG(LogIoStore, Error, TEXT("Failed to save temp file for write updated cook metadata file (%s"), *TempFileName); return false; } return true; } // modified copy from PakFileUtilities static FName RemapLocalizationPathIfNeeded(const FString& Path) { static constexpr TCHAR L10NString[] = TEXT("/L10N/"); static constexpr int32 L10NPrefixLength = sizeof(L10NString) / sizeof(TCHAR) - 1; int32 BeginL10NOffset = Path.Find(L10NString, ESearchCase::IgnoreCase); if (BeginL10NOffset >= 0) { int32 EndL10NOffset = BeginL10NOffset + L10NPrefixLength; int32 NextSlashIndex = Path.Find(TEXT("/"), ESearchCase::IgnoreCase, ESearchDir::FromStart, EndL10NOffset); int32 RegionLength = NextSlashIndex - EndL10NOffset; if (RegionLength >= 2) { FString NonLocalizedPath = Path.Mid(0, BeginL10NOffset) + Path.Mid(NextSlashIndex); return FName(NonLocalizedPath); } } return NAME_None; } void ProcessRedirects(const FIoStoreArguments& Arguments, const TMap& PackagesMap) { TRACE_CPUPROFILER_EVENT_SCOPE(ProcessRedirects); for (const auto& KV : PackagesMap) { FCookedPackage* Package = KV.Value; FName LocalizedSourcePackageName = RemapLocalizationPathIfNeeded(Package->PackageName.ToString()); if (!LocalizedSourcePackageName.IsNone()) { Package->SourcePackageName = LocalizedSourcePackageName; Package->bIsLocalized = true; } } const bool bIsBuildingDLC = Arguments.IsDLC(); if (bIsBuildingDLC && Arguments.bRemapPluginContentToGame) { for (const auto& KV : PackagesMap) { FCookedPackage* Package = KV.Value; const int32 DLCNameLen = Arguments.DLCName.Len() + 1; FString PackageNameStr = Package->PackageName.ToString(); FString RedirectedPackageNameStr = TEXT("/Game"); RedirectedPackageNameStr.AppendChars(*PackageNameStr + DLCNameLen, PackageNameStr.Len() - DLCNameLen); FName RedirectedPackageName = FName(*RedirectedPackageNameStr); Package->SourcePackageName = RedirectedPackageName; } } } void CreateContainerHeader(FContainerTargetSpec& ContainerTarget, bool bIsOptional) { TRACE_CPUPROFILER_EVENT_SCOPE(CreateContainerHeader); FIoContainerHeader& Header = bIsOptional ? ContainerTarget.OptionalSegmentHeader : ContainerTarget.Header; Header.ContainerId = bIsOptional ? ContainerTarget.OptionalSegmentContainerId : ContainerTarget.ContainerId; int32 NonOptionalSegmentStoreEntriesCount = 0; int32 OptionalSegmentStoreEntriesCount = 0; if (bIsOptional) { for (const FCookedPackage* Package : ContainerTarget.Packages) { if (Package->PackageStoreEntry.HasOptionalSegment()) { if (Package->PackageStoreEntry.IsAutoOptional()) { // Auto optional packages fully replace the non-optional segment ++NonOptionalSegmentStoreEntriesCount; } else { ++OptionalSegmentStoreEntriesCount; } } } } else { NonOptionalSegmentStoreEntriesCount = ContainerTarget.Packages.Num(); } struct FStoreEntriesWriter { const int32 StoreTocSize; FLargeMemoryWriter StoreTocArchive = FLargeMemoryWriter(0, true); FLargeMemoryWriter StoreDataArchive = FLargeMemoryWriter(0, true); void Flush(TArray& OutputBuffer) { check(StoreTocArchive.TotalSize() == StoreTocSize); if (StoreTocSize) { const int32 StoreByteCount = StoreTocArchive.TotalSize() + StoreDataArchive.TotalSize(); OutputBuffer.AddUninitialized(StoreByteCount); FBufferWriter PackageStoreArchive(OutputBuffer.GetData(), StoreByteCount); PackageStoreArchive.Serialize(StoreTocArchive.GetData(), StoreTocArchive.TotalSize()); PackageStoreArchive.Serialize(StoreDataArchive.GetData(), StoreDataArchive.TotalSize()); } } }; FStoreEntriesWriter StoreEntriesWriter { static_cast(NonOptionalSegmentStoreEntriesCount * sizeof(FFilePackageStoreEntry)) }; FStoreEntriesWriter OptionalSegmentStoreEntriesWriter { static_cast(OptionalSegmentStoreEntriesCount * sizeof(FFilePackageStoreEntry)) }; auto SerializePackageEntryCArrayHeader = [](FStoreEntriesWriter& Writer, int32 Count) { const int32 RemainingTocSize = Writer.StoreTocSize - Writer.StoreTocArchive.Tell(); const int32 OffsetFromThis = RemainingTocSize + Writer.StoreDataArchive.Tell(); uint32 ArrayNum = Count > 0 ? Count : 0; uint32 OffsetToDataFromThis = ArrayNum > 0 ? OffsetFromThis : 0; Writer.StoreTocArchive << ArrayNum; Writer.StoreTocArchive << OffsetToDataFromThis; }; struct FSoftPackageReferenceWriter { explicit FSoftPackageReferenceWriter(TSet&& SoftReferences, int32 NumPackageEntries) : SoftPackageReferences(SoftReferences.Array()) , TotalEntrySize(NumPackageEntries * sizeof(FFilePackageStoreEntrySoftReferences)) { SoftPackageReferences.Sort(); } void Append(TConstArrayView SoftRefs) { if (SoftPackageReferences.IsEmpty()) { // Skip serialization when there's no soft references for any package check(SoftRefs.IsEmpty()); return; } const int64 RemainingEntrySize = TotalEntrySize - EntryAr.Tell(); const int64 OffsetFromThis = RemainingEntrySize + DataAr.Tell(); uint32 ArrayNum = static_cast(SoftRefs.Num()); uint32 OffsetToDataFromThis = ArrayNum > 0 ? OffsetFromThis : 0; EntryAr << ArrayNum; EntryAr << OffsetToDataFromThis; for (FPackageId SoftRef : SoftRefs) { int32 Index = Algo::BinarySearch(SoftPackageReferences, SoftRef); check(Index != INDEX_NONE); DataAr << Index; } } void Flush(FIoContainerHeaderSoftPackageReferences& OutContainerSoftReferences) { const int64 TotalSize = EntryAr.TotalSize() + DataAr.TotalSize(); if (TotalSize == 0) { check(SoftPackageReferences.IsEmpty()); OutContainerSoftReferences.Empty(); return; } OutContainerSoftReferences.bContainsSoftPackageReferences = true; OutContainerSoftReferences.PackageIds = MoveTemp(SoftPackageReferences); OutContainerSoftReferences.PackageIndices.AddUninitialized(TotalSize); FBufferWriter Ar(OutContainerSoftReferences.PackageIndices.GetData(), TotalSize); Ar.Serialize(EntryAr.GetData(), EntryAr.TotalSize()); Ar.Serialize(DataAr.GetData(), DataAr.TotalSize()); } private: TArray SoftPackageReferences; const int64 TotalEntrySize; FLargeMemoryWriter EntryAr; FLargeMemoryWriter DataAr; }; TArray SortedPackages(ContainerTarget.Packages); Algo::Sort(SortedPackages, [](const FCookedPackage* A, const FCookedPackage* B) { return A->GlobalPackageId < B->GlobalPackageId; }); Header.PackageIds.Reserve(NonOptionalSegmentStoreEntriesCount); Header.OptionalSegmentPackageIds.Reserve(OptionalSegmentStoreEntriesCount); FPackageStoreNameMapBuilder RedirectsNameMapBuilder; RedirectsNameMapBuilder.SetNameMapType(FMappedName::EType::Container); TSet AllLocalizedPackages; if (bIsOptional) { for (const FCookedPackage* Package : SortedPackages) { const FPackageStoreEntryResource& Entry = Package->PackageStoreEntry; if (Entry.HasOptionalSegment()) { FStoreEntriesWriter* TargetEntriesWriter; if (Entry.IsAutoOptional()) { Header.PackageIds.Add(Package->GlobalPackageId); TargetEntriesWriter = &StoreEntriesWriter; } else { Header.OptionalSegmentPackageIds.Add(Package->GlobalPackageId); TargetEntriesWriter = &OptionalSegmentStoreEntriesWriter; } // OptionalImportedPackages const TArray& OptionalSegmentImportedPackageIds = Entry.OptionalSegmentImportedPackageIds; SerializePackageEntryCArrayHeader(*TargetEntriesWriter, OptionalSegmentImportedPackageIds.Num()); for (FPackageId OptionalSegmentImportedPackageId : OptionalSegmentImportedPackageIds) { check(OptionalSegmentImportedPackageId.IsValid()); TargetEntriesWriter->StoreDataArchive << OptionalSegmentImportedPackageId; } // ShaderMapHashes is N/A for optional segments SerializePackageEntryCArrayHeader(*TargetEntriesWriter, 0); } } } else { TSet AllSoftPackageReferences; for (const FCookedPackage* Package : SortedPackages) { for (FPackageId SoftRef : Package->PackageStoreEntry.SoftPackageReferences) { AllSoftPackageReferences.Add(SoftRef); } } FSoftPackageReferenceWriter SoftRefsWriter(MoveTemp(AllSoftPackageReferences), NonOptionalSegmentStoreEntriesCount); for (const FCookedPackage* Package : SortedPackages) { const FPackageStoreEntryResource& Entry = Package->PackageStoreEntry; Header.PackageIds.Add(Package->GlobalPackageId); if (!Package->SourcePackageName.IsNone()) { RedirectsNameMapBuilder.MarkNameAsReferenced(Package->SourcePackageName); FMappedName MappedSourcePackageName = RedirectsNameMapBuilder.MapName(Package->SourcePackageName); if (Package->bIsLocalized) { if (!AllLocalizedPackages.Contains(Package->SourcePackageName)) { Header.LocalizedPackages.Add({ FPackageId::FromName(Package->SourcePackageName), MappedSourcePackageName }); AllLocalizedPackages.Add(Package->SourcePackageName); } } else { Header.PackageRedirects.Add({ FPackageId::FromName(Package->SourcePackageName), Package->GlobalPackageId, MappedSourcePackageName }); } } // ImportedPackages const TArray& ImportedPackageIds = Entry.ImportedPackageIds; SerializePackageEntryCArrayHeader(StoreEntriesWriter, ImportedPackageIds.Num()); for (FPackageId ImportedPackageId : ImportedPackageIds) { check(ImportedPackageId.IsValid()); StoreEntriesWriter.StoreDataArchive << ImportedPackageId; } // ShaderMapHashes const TArray& ShaderMapHashes = Package->ShaderMapHashes; SerializePackageEntryCArrayHeader(StoreEntriesWriter, ShaderMapHashes.Num()); for (const FSHAHash& ShaderMapHash : ShaderMapHashes) { StoreEntriesWriter.StoreDataArchive << const_cast(ShaderMapHash); } // SoftPackageReferences SoftRefsWriter.Append(Entry.SoftPackageReferences); } SoftRefsWriter.Flush(Header.SoftPackageReferences); } Header.RedirectsNameMap = RedirectsNameMapBuilder.GetNameMap(); StoreEntriesWriter.Flush(Header.StoreEntries); OptionalSegmentStoreEntriesWriter.Flush(Header.OptionalSegmentStoreEntries); } int32 CreateTarget(const FIoStoreArguments& Arguments, const FIoStoreWriterSettings& GeneralIoWriterSettings) { IOSTORE_CPU_SCOPE(CreateTarget); TGuardValue GuardAllowUnversionedContentInEditor(GAllowUnversionedContentInEditor, 1); TSharedPtr ChunkDatabase; if (Arguments.ReferenceChunkGlobalContainerFileName.Len()) { ChunkDatabase = MakeShared(); FIoStoreChunkDatabase* ChunkDbPtr = (FIoStoreChunkDatabase*)ChunkDatabase.Get(); if (ChunkDbPtr->Init(Arguments.ReferenceChunkGlobalContainerFileName, Arguments.ReferenceChunkAdditionalContainersPath, Arguments.ReferenceChunkKeys) == false) { UE_LOG(LogIoStore, Warning, TEXT("Failed to initialize reference chunk store. Pak will continue.")); ChunkDatabase.Reset(); } } TArray Packages; FPackageNameMap PackageNameMap; FPackageIdMap PackageIdMap; FPackageStoreOptimizer PackageStoreOptimizer; PackageStoreOptimizer.Initialize(*Arguments.ScriptObjects); TArray ContainerTargets; UE_LOG(LogIoStore, Display, TEXT("Creating container targets...")); { IOSTORE_CPU_SCOPE(CreateContainerTargets); InitializeContainerTargetsAndPackages(Arguments, Packages, PackageNameMap, PackageIdMap, ContainerTargets); } TUniquePtr IoStoreWriterContext; { IOSTORE_CPU_SCOPE(InitializeIoStoreWriters); IoStoreWriterContext.Reset(new FIoStoreWriterContext()); FIoStatus IoStatus = IoStoreWriterContext->Initialize(GeneralIoWriterSettings); check(IoStatus.IsOk()); } TArray OnDemandContainers; TArray> IoStoreWriters; TArray IoStoreWriterInfos; TSharedPtr GlobalIoStoreWriter; { IOSTORE_CPU_SCOPE(InitializeWriters); if (!Arguments.IsDLC()) { IOSTORE_CPU_SCOPE(InitializeGlobalWriter); FIoContainerSettings GlobalContainerSettings; if (Arguments.bSign) { GlobalContainerSettings.SigningKey = Arguments.KeyChain.GetSigningKey(); GlobalContainerSettings.ContainerFlags |= EIoContainerFlags::Signed; } GlobalIoStoreWriter = IoStoreWriterContext->CreateContainer(*Arguments.GlobalContainerPath, GlobalContainerSettings); IoStoreWriters.Add(GlobalIoStoreWriter); IoStoreWriterInfos.Add({UE::Cook::EPluginSizeTypes::Installed, "global"}); } for (FContainerTargetSpec* ContainerTarget : ContainerTargets) { IOSTORE_CPU_SCOPE(InitializeWriter); check(ContainerTarget->ContainerId.IsValid()); if (ContainerTarget->OutputPath.IsEmpty()) { continue; } if (!ContainerTarget->StageLooseFileRootPath.IsEmpty()) { FLooseFilesWriterSettings WriterSettings; WriterSettings.TargetRootPath = ContainerTarget->StageLooseFileRootPath; ContainerTarget->IoStoreWriter = MakeLooseFilesIoStoreWriter(WriterSettings); IoStoreWriterInfos.Add({UE::Cook::EPluginSizeTypes::Streaming, ContainerTarget->Name}); // LooseFiles currently end up as a streamed source. IoStoreWriters.Add(ContainerTarget->IoStoreWriter); } else { FIoContainerSettings ContainerSettings; ContainerSettings.ContainerId = ContainerTarget->ContainerId; ContainerSettings.ContainerFlags = ContainerTarget->ContainerFlags; if (Arguments.bCreateDirectoryIndex) { ContainerSettings.ContainerFlags |= EIoContainerFlags::Indexed; } if (EnumHasAnyFlags(ContainerTarget->ContainerFlags, EIoContainerFlags::Encrypted)) { if (const FNamedAESKey* Key = Arguments.KeyChain.GetEncryptionKeys().Find(ContainerTarget->EncryptionKeyGuid)) { ContainerSettings.EncryptionKeyGuid = ContainerTarget->EncryptionKeyGuid; ContainerSettings.EncryptionKey = Key->Key; } else { UE_LOG(LogIoStore, Error, TEXT("Failed to find encryption key '%s'"), *ContainerTarget->EncryptionKeyGuid.ToString()); return -1; } } if (EnumHasAnyFlags(ContainerTarget->ContainerFlags, EIoContainerFlags::Signed)) { ContainerSettings.SigningKey = Arguments.KeyChain.GetSigningKey(); ContainerSettings.ContainerFlags |= EIoContainerFlags::Signed; } ContainerSettings.bGenerateDiffPatch = ContainerTarget->bGenerateDiffPatch; ContainerTarget->IoStoreWriter = IoStoreWriterContext->CreateContainer(*ContainerTarget->OutputPath, ContainerSettings); ContainerTarget->IoStoreWriter->EnableDiskLayoutOrdering(ContainerTarget->PatchSourceReaders); ContainerTarget->IoStoreWriter->SetReferenceChunkDatabase(ChunkDatabase); IoStoreWriters.Add(ContainerTarget->IoStoreWriter); if (EnumHasAnyFlags(ContainerTarget->ContainerFlags, EIoContainerFlags::OnDemand)) { IoStoreWriterInfos.Add({UE::Cook::EPluginSizeTypes::Streaming, ContainerTarget->Name}); } else { // There's no way to know whether a container is optional without parsing the filename, see // EIoStoreWriterType. FString BaseFileName = FPaths::GetBaseFilename(ContainerTarget->OutputPath, true); // Strip the platform identifier off the pak if (int32 DashIndex=0; BaseFileName.FindLastChar(TEXT('-'), DashIndex)) { BaseFileName.LeftInline(DashIndex); } if (BaseFileName.EndsWith(TEXT("optional"))) { IoStoreWriterInfos.Add({UE::Cook::EPluginSizeTypes::Optional, ContainerTarget->Name}); } else { IoStoreWriterInfos.Add({UE::Cook::EPluginSizeTypes::Installed, ContainerTarget->Name}); } } if (!ContainerTarget->OptionalSegmentOutputPath.IsEmpty()) { ContainerSettings.ContainerId = ContainerTarget->OptionalSegmentContainerId; ContainerTarget->OptionalSegmentIoStoreWriter = IoStoreWriterContext->CreateContainer(*ContainerTarget->OptionalSegmentOutputPath, ContainerSettings); ContainerSettings.ContainerId = ContainerTarget->ContainerId; ContainerTarget->OptionalSegmentIoStoreWriter->SetReferenceChunkDatabase(ChunkDatabase); IoStoreWriters.Add(ContainerTarget->OptionalSegmentIoStoreWriter); IoStoreWriterInfos.Add({UE::Cook::EPluginSizeTypes::OptionalSegment, ContainerTarget->Name}); } if (EnumHasAnyFlags(ContainerTarget->ContainerFlags, EIoContainerFlags::OnDemand)) { OnDemandContainers.Add(ContainerTarget->OutputPath); } } } } const bool bIsLegacyStage = !Arguments.PackageStore->HasZenStoreClient(); if (bIsLegacyStage) { ParsePackageAssetsFromFiles(Packages, PackageStoreOptimizer); if (Arguments.bFileRegions) { // The file regions for packages are relative to the start of the uexp file so we need to make them relative to the start of the export bundle chunk instead for (FContainerTargetSpec* ContainerTarget : ContainerTargets) { for (FContainerTargetFile& TargetFile : ContainerTarget->TargetFiles) { if (TargetFile.ChunkType == EContainerChunkType::PackageData) { uint64 HeaderSize = static_cast(TargetFile.Package)->OptimizedPackage->GetHeaderSize(); for (FFileRegion& Region : TargetFile.FileRegions) { Region.Offset += HeaderSize; } } else if (TargetFile.ChunkType == EContainerChunkType::OptionalSegmentPackageData) { uint64 HeaderSize = static_cast(TargetFile.Package)->OptimizedOptionalSegmentPackage->GetHeaderSize(); for (FFileRegion& Region : TargetFile.FileRegions) { Region.Offset += HeaderSize; } } } } } } UE_LOG(LogIoStore, Display, TEXT("Processing shader libraries, compressing with Oodle %s, level %d (%s)"), FOodleDataCompression::ECompressorToString(Arguments.ShaderOodleCompressor), (int32)Arguments.ShaderOodleLevel, FOodleDataCompression::ECompressionLevelToString(Arguments.ShaderOodleLevel)); TArray Shaders; FShaderAssociationInfo ShaderAssocInfo; ProcessShaderLibraries(Arguments, ContainerTargets, Shaders, ShaderAssocInfo); FIoStoreWriteRequestManager WriteRequestManager(PackageStoreOptimizer, Arguments.PackageStore.Get()); auto AppendTargetFileChunk = [&WriteRequestManager](FContainerTargetSpec* ContainerTarget, const FContainerTargetFile& TargetFile) { FIoWriteOptions WriteOptions; WriteOptions.DebugName = *TargetFile.DestinationPath; WriteOptions.bForceUncompressed = TargetFile.bForceUncompressed; WriteOptions.bIsMemoryMapped = TargetFile.ChunkType == EContainerChunkType::MemoryMappedBulkData; WriteOptions.FileName = TargetFile.DestinationPath; FIoChunkId ChunkId = TargetFile.ChunkId; bool bIsOptionalSegmentChunk = false; switch (TargetFile.ChunkType) { case EContainerChunkType::OptionalSegmentPackageData: { if (TargetFile.Package->PackageStoreEntry.IsAutoOptional()) { // Auto optional packages replace the non-optional part when the container is mounted ChunkId = CreateIoChunkId(TargetFile.Package->GlobalPackageId.Value(), 0, EIoChunkType::ExportBundleData); } bIsOptionalSegmentChunk = true; break; } case EContainerChunkType::OptionalSegmentBulkData: { if (TargetFile.Package->PackageStoreEntry.IsAutoOptional()) { // Auto optional packages replace the non-optional part when the container is mounted ChunkId = CreateBulkDataIoChunkId(TargetFile.Package->GlobalPackageId.Value(), 0, TargetFile.BulkDataCookedIndex.GetValue(), EIoChunkType::BulkData); } bIsOptionalSegmentChunk = true; break; } } if (bIsOptionalSegmentChunk) { ContainerTarget->OptionalSegmentIoStoreWriter->Append(ChunkId, WriteRequestManager.Read(TargetFile), WriteOptions); } else { ContainerTarget->IoStoreWriter->Append(TargetFile.ChunkId, WriteRequestManager.Read(TargetFile), WriteOptions); } }; { IOSTORE_CPU_SCOPE(AppendChunks); for (FContainerTargetSpec* ContainerTarget : ContainerTargets) { if (ContainerTarget->IoStoreWriter) { for (FContainerTargetFile& TargetFile : ContainerTarget->TargetFiles) { AppendTargetFileChunk(ContainerTarget, TargetFile); } } } } UE_LOG(LogIoStore, Display, TEXT("Processing redirects...")); ProcessRedirects(Arguments, PackageIdMap); UE_LOG(LogIoStore, Display, TEXT("Creating disk layout...")); FString ClusterCSVPath; if (FParse::Value(FCommandLine::Get(), TEXT("ClusterCSV="), ClusterCSVPath)) { IOSTORE_CPU_SCOPE(CreateClusterCSV); ClusterStatsCsv.CreateOutputFile(ClusterCSVPath); } SortPackagesInLoadOrder(Packages, PackageIdMap); CreateDiskLayout(ContainerTargets, Packages, Arguments.OrderMaps, PackageIdMap, Arguments.OrderingOptions); { IOSTORE_CPU_SCOPE(AppendContainerHeaderChunks); for (FContainerTargetSpec* ContainerTarget : ContainerTargets) { if (ContainerTarget->IoStoreWriter) { auto WriteContainerHeaderChunk = [](FIoContainerHeader& Header, IIoStoreWriter* IoStoreWriter, const FIoContainerId& IoStoreWriterId) { FLargeMemoryWriter HeaderAr(0, true); HeaderAr << Header; int64 DataSize = HeaderAr.TotalSize(); FIoBuffer ContainerHeaderBuffer(FIoBuffer::AssumeOwnership, HeaderAr.ReleaseOwnership(), DataSize); // The header must have the same ID so that the loading code can find it. check(IoStoreWriterId == Header.ContainerId); FIoWriteOptions WriteOptions; WriteOptions.DebugName = TEXT("ContainerHeader"); WriteOptions.bForceUncompressed = true; IoStoreWriter->Append( CreateIoChunkId(Header.ContainerId.Value(), 0, EIoChunkType::ContainerHeader), ContainerHeaderBuffer, WriteOptions); }; CreateContainerHeader(*ContainerTarget, false); WriteContainerHeaderChunk(ContainerTarget->Header, ContainerTarget->IoStoreWriter.Get(), ContainerTarget->ContainerId); if (ContainerTarget->OptionalSegmentIoStoreWriter) { CreateContainerHeader(*ContainerTarget, true); WriteContainerHeaderChunk(ContainerTarget->OptionalSegmentHeader, ContainerTarget->OptionalSegmentIoStoreWriter.Get(), ContainerTarget->OptionalSegmentContainerId); } } } } uint64 InitialLoadSize = 0; if (GlobalIoStoreWriter) { IOSTORE_CPU_SCOPE(WriteScriptObjects); FIoBuffer ScriptObjectsBuffer = PackageStoreOptimizer.CreateScriptObjectsBuffer(); InitialLoadSize = ScriptObjectsBuffer.DataSize(); FIoWriteOptions WriteOptions; WriteOptions.DebugName = TEXT("ScriptObjects"); GlobalIoStoreWriter->Append(CreateIoChunkId(0, 0, EIoChunkType::ScriptObjects), ScriptObjectsBuffer, WriteOptions); } UE_LOG(LogIoStore, Display, TEXT("Serializing container(s)...")); { IOSTORE_CPU_SCOPE(Serializing); TFuture FlushTask = Async(EAsyncExecution::Thread, [&IoStoreWriterContext]() { IoStoreWriterContext->Flush(); }); bool bPrintProgress = true; while (bPrintProgress) { FlushTask.WaitFor(FTimespan::FromSeconds(2.0)); bPrintProgress = !FlushTask.IsReady(); TStringBuilder<1024> ProgressStringBuilder; const FIoStoreWriterContext::FProgress Progress = IoStoreWriterContext->GetProgress(); if (Progress.SerializedChunksCount >= Progress.TotalChunksCount) { ProgressStringBuilder.Appendf(TEXT("Writing tocs...")); } else if (Progress.SerializedChunksCount) { ProgressStringBuilder.Appendf(TEXT("Writing chunks %llu/%llu..."), Progress.SerializedChunksCount, Progress.TotalChunksCount); if (Progress.CompressedChunksCount) { ProgressStringBuilder.Appendf(TEXT(" [%llu compressed]"), Progress.CompressedChunksCount); } if (Progress.CompressionDDCHitCount || Progress.CompressionDDCPutCount) { ProgressStringBuilder.Appendf(TEXT(" [DDC: %llu hits, %llu puts]"), Progress.CompressionDDCHitCount, Progress.CompressionDDCPutCount); } if (Progress.ScheduledCompressionTasksCount) { ProgressStringBuilder.Appendf(TEXT(" [%llu compression tasks]"), Progress.ScheduledCompressionTasksCount); } } else { ProgressStringBuilder.Appendf(TEXT("Hashing %llu/%llu..."), Progress.HashedChunksCount, Progress.TotalChunksCount); } const FPlatformMemoryStats MemStats = FPlatformMemory::GetStats(); ProgressStringBuilder.Appendf(TEXT(" [Physical: %llu MiB, Virtual: %llu MiB]"), MemStats.UsedPhysical >> 20, MemStats.UsedVirtual >> 20); UE_LOG(LogIoStore, Display, TEXT("%s"), *ProgressStringBuilder); } } if (GeneralIoWriterSettings.bCompressionEnableDDC) { TRACE_CPUPROFILER_EVENT_SCOPE(WaitForDDC); UE_LOG(LogIoStore, Display, TEXT("")); UE_LOG(LogIoStore, Display, TEXT("Waiting for DDC...")); GetDerivedDataCacheRef().WaitForQuiescence(true); FIoStoreWriterContext::FProgress Progress = IoStoreWriterContext->GetProgress(); const uint64 TotalDDCAttempts = Progress.CompressionDDCHitCount + Progress.CompressionDDCMissCount; const double DDCHitRate = TotalDDCAttempts > 0 ? double(Progress.CompressionDDCHitCount) / TotalDDCAttempts * 100.0 : 0.0; const double DDCPutRate = TotalDDCAttempts > 0 ? double(Progress.CompressionDDCPutCount) / TotalDDCAttempts * 100.0 : 0.0; UE_LOG(LogIoStore, Display, TEXT("Compression DDC hits: %llu/%llu (%.2lf%%)"), Progress.CompressionDDCHitCount, TotalDDCAttempts, DDCHitRate); UE_LOG(LogIoStore, Display, TEXT("Compression DDC puts: %llu/%llu (%.2lf%%) [%llu failed]"), Progress.CompressionDDCPutCount, TotalDDCAttempts, DDCPutRate, Progress.CompressionDDCPutErrorCount); UE_LOG(LogIoStore, Display, TEXT("")); } { FIoStoreWriterContext::FProgress Progress = IoStoreWriterContext->GetProgress(); if (Progress.HashDbChunksCount) { UE_LOG(LogIoStore, Display, TEXT("%s / %s hashes were loaded from the hash database, by type:"), *NumberString(Progress.HashDbChunksCount), *NumberString(Progress.TotalChunksCount)); for (uint8 i = 0; i < (uint8)EIoChunkType::MAX; i++) { if (Progress.HashDbChunksByType[i]) { UE_LOG(LogIoStore, Display, TEXT(" %-26s %s"), *LexToString((EIoChunkType)i), *NumberString(Progress.HashDbChunksByType[i])); } } } if (Progress.RefDbChunksCount) { FIoStoreChunkDatabase& TypedChunkDatabase = ((FIoStoreChunkDatabase&)*ChunkDatabase); UE_LOG(LogIoStore, Display, TEXT("%s / %s chunks for %s bytes were loaded from the reference chunk database, by type:"), *NumberString(Progress.RefDbChunksCount), *NumberString(Progress.TotalChunksCount), *NumberString(TypedChunkDatabase.FulfillBytes)); for (uint8 i = 0; i < (uint8)EIoChunkType::MAX; i++) { if (Progress.RefDbChunksByType[i]) { UE_LOG(LogIoStore, Display, TEXT(" %-26s: %s for %s bytes"), *LexToString((EIoChunkType)i), *NumberString(Progress.RefDbChunksByType[i]), *NumberString(TypedChunkDatabase.FulfillBytesPerChunk[i])); } } } if (Progress.CompressedChunksCount) { UE_LOG(LogIoStore, Display, TEXT("%s / %s chunks attempted to compress, by type:"), *NumberString(Progress.CompressedChunksCount), *NumberString(Progress.TotalChunksCount)); for (uint8 i = 0; i < (uint8)EIoChunkType::MAX; i++) { if (Progress.CompressedChunksByType[i] || Progress.BeginCompressChunksByType[i]) { UE_LOG(LogIoStore, Display, TEXT(" %-26s %s / %s"), *LexToString((EIoChunkType)i), *NumberString(Progress.CompressedChunksByType[i]), *NumberString(Progress.BeginCompressChunksByType[i])); } } } if (Progress.CompressionDDCHitCount) { UE_LOG(LogIoStore, Display, TEXT("%s / %s chunks for %s bytes were loaded from DDC, by type:"), *NumberString(Progress.CompressionDDCHitCount), *NumberString(Progress.TotalChunksCount), *NumberString(Progress.CompressionDDCGetBytes)); for (uint8 i = 0; i < (uint8)EIoChunkType::MAX; i++) { if (Progress.CompressionDDCHitsByType[i]) { UE_LOG(LogIoStore, Display, TEXT(" %-26s %s / %s"), *LexToString((EIoChunkType)i), *NumberString(Progress.CompressionDDCHitsByType[i]), *NumberString(Progress.BeginCompressChunksByType[i])); } } } if (Progress.CompressionDDCPutCount) { UE_LOG(LogIoStore, Display, TEXT("%s / %s chunks for %s bytes were stored in DDC, by type:"), *NumberString(Progress.CompressionDDCPutCount), *NumberString(Progress.TotalChunksCount), *NumberString(Progress.CompressionDDCPutBytes)); for (uint8 i = 0; i < (uint8)EIoChunkType::MAX; i++) { if (Progress.CompressionDDCPutsByType[i]) { UE_LOG(LogIoStore, Display, TEXT(" %-26s %s / %s"), *LexToString((EIoChunkType)i), *NumberString(Progress.CompressionDDCPutsByType[i]), *NumberString(Progress.BeginCompressChunksByType[i])); } } } UE_LOG(LogIoStore, Display, TEXT("Source bytes read:")); uint64 ZenTotalBytes = 0; for (uint64 b : WriteRequestManager.ZenSourceBytes) { ZenTotalBytes += b; } UE_LOG(LogIoStore, Display, TEXT(" Zen: %34s"), *NumberString(ZenTotalBytes)); for (uint8 i = 0; i < (uint8)EIoChunkType::MAX; i++) { if (WriteRequestManager.ZenSourceReads[i]) { UE_LOG(LogIoStore, Display, TEXT(" %-22s %12s bytes over %s reads"), *LexToString((EIoChunkType)i), *NumberString(WriteRequestManager.ZenSourceBytes[i].Load()), *NumberString(WriteRequestManager.ZenSourceReads[i].Load())); } } uint64 LooseTotalBytes = 0; for (uint64 b : WriteRequestManager.LooseFileSourceBytes) { LooseTotalBytes += b; } UE_LOG(LogIoStore, Display, TEXT(" Loose File: %27s"), *NumberString(LooseTotalBytes)); for (uint8 i = 0; i < (uint8)EIoChunkType::MAX; i++) { if (WriteRequestManager.LooseFileSourceReads[i]) { UE_LOG(LogIoStore, Display, TEXT(" %-22s %12s bytes over %s reads"), *LexToString((EIoChunkType)i), *NumberString(WriteRequestManager.LooseFileSourceBytes[i].Load()), *NumberString(WriteRequestManager.LooseFileSourceReads[i].Load())); } } uint64 MemoryTotalBytes = 0; for (uint64 b : WriteRequestManager.MemorySourceBytes) { MemoryTotalBytes += b; } UE_LOG(LogIoStore, Display, TEXT(" Memory: %31s"), *NumberString(MemoryTotalBytes)); for (uint8 i = 0; i < (uint8)EIoChunkType::MAX; i++) { if (WriteRequestManager.MemorySourceReads[i]) { UE_LOG(LogIoStore, Display, TEXT(" %-22s %12s bytes over %s reads"), *LexToString((EIoChunkType)i), *NumberString(WriteRequestManager.MemorySourceBytes[i].Load()), *NumberString(WriteRequestManager.MemorySourceReads[i].Load())); } } } if (Arguments.WriteBackMetadataToAssetRegistry != EAssetRegistryWritebackMethod::Disabled) { DoAssetRegistryWritebackDuringStage( Arguments.WriteBackMetadataToAssetRegistry, Arguments.bWritePluginSizeSummaryJsons, Arguments.PackageStore.Get(), Arguments.CookedDir, GeneralIoWriterSettings.CompressionMethod != NAME_None, IoStoreWriters, IoStoreWriterInfos, ShaderAssocInfo); } TArray IoStoreWriterResults; { TRACE_CPUPROFILER_EVENT_SCOPE(GetWriterResults); IoStoreWriterResults.Reserve(IoStoreWriters.Num()); for (TSharedPtr IoStoreWriter : IoStoreWriters) { IoStoreWriterResults.Emplace(IoStoreWriter->GetResult().ConsumeValueOrDie()); } } FGraphEventRef WriteCsvFileTask; if (Arguments.CsvPath.Len() > 0) { WriteCsvFileTask = FFunctionGraphTask::CreateAndDispatchWhenReady([&Arguments, &IoStoreWriters, &IoStoreWriterResults]() { TRACE_CPUPROFILER_EVENT_SCOPE(WriteCsvFiles); bool bPerContainerCsvFiles = FPaths::DirectoryExists(Arguments.CsvPath); FChunkEntryCsv AllContainersOutCsvFile; FChunkEntryCsv* Out = &AllContainersOutCsvFile; if (!bPerContainerCsvFiles) { // When CsvPath is a filename append .utoc.csv to create a unique single csv for all container files, // different from the unique single .pak.csv for all pak files. FString CsvFilename = Arguments.CsvPath + TEXT(".utoc.csv"); AllContainersOutCsvFile.CreateOutputFile(*CsvFilename); } for (int32 Index = 0; Index < IoStoreWriters.Num(); ++Index) { TRACE_CPUPROFILER_EVENT_SCOPE(ListContainer); TSharedPtr Writer = IoStoreWriters[Index]; FIoStoreWriterResult& Result = IoStoreWriterResults[Index]; TArray Chunks; { IOSTORE_CPU_SCOPE(EnumerateChunks); Chunks.Reserve(Result.TocEntryCount); Writer->EnumerateChunks([&Chunks](FIoStoreTocChunkInfo&& ChunkInfo) { Chunks.Add(MoveTemp(ChunkInfo)); return true; }); } { IOSTORE_CPU_SCOPE(SortChunks); auto SortKey = [](const FIoStoreTocChunkInfo& ChunkInfo) { return ChunkInfo.OffsetOnDisk; }; Algo::SortBy(Chunks, SortKey); } { IOSTORE_CPU_SCOPE(WriteCsvFile); FChunkEntryCsv PerContainerOutCsvFile; if (bPerContainerCsvFiles) { // When CsvPath is a dir, then create one unique .utoc.csv per container file FString PerContainerCsvPath = Arguments.CsvPath / Result.ContainerName + TEXT(".utoc.csv"); PerContainerOutCsvFile.CreateOutputFile(*PerContainerCsvPath); Out = &PerContainerOutCsvFile; } for (int32 EntryIndex=0; EntryIndex < Chunks.Num(); ++EntryIndex) { FIoStoreTocChunkInfo& ChunkInfo = Chunks[EntryIndex]; FString PackageName; FPackageId PackageId; if (!ChunkInfo.bHasValidFileName) { FString FileName = Arguments.PackageStore->GetRelativeFilenameFromChunkId(ChunkInfo.Id); if (FileName.Len() > 0) { ChunkInfo.FileName = MoveTemp(FileName); } FName PackageFName = Arguments.PackageStore->GetPackageNameFromChunkId(ChunkInfo.Id); if (!PackageFName.IsNone()) { PackageName = PackageFName.ToString(); PackageId = FPackageId::FromName(FName(*PackageName)); } } Out->AddChunk(Result.ContainerName, EntryIndex, ChunkInfo, PackageId, PackageName, ""); } } } }, TStatId(), nullptr, ENamedThreads::AnyNormalThreadHiPriTask); } IOSTORE_CPU_SCOPE(OutputStats); UE_LOG(LogIoStore, Display, TEXT("Calculating stats...")); uint64 UExpSize = 0; uint64 UAssetSize = 0; uint64 UBulkSize = 0; uint64 HeaderSize = 0; uint64 ImportedPackagesCount = 0; uint64 NoImportedPackagesCount = 0; uint64 NameMapCount = 0; for (const FCookedPackage* Package : Packages) { UExpSize += Package->UExpSize; UAssetSize += Package->UAssetSize; UBulkSize += Package->TotalBulkDataSize; if (bIsLegacyStage) { const FLegacyCookedPackage* LegacyPackage = static_cast(Package); NameMapCount += LegacyPackage->OptimizedPackage->GetNameCount(); HeaderSize += LegacyPackage->OptimizedPackage->GetHeaderSize(); } int32 PackageImportedPackagesCount = Package->PackageStoreEntry.ImportedPackageIds.Num(); ImportedPackagesCount += PackageImportedPackagesCount; NoImportedPackagesCount += PackageImportedPackagesCount == 0; } uint64 GlobalShaderCount = 0; uint64 SharedShaderCount = 0; uint64 UniqueShaderCount = 0; uint64 InlineShaderCount = 0; uint64 GlobalShaderSize = 0; uint64 SharedShaderSize = 0; uint64 UniqueShaderSize = 0; uint64 InlineShaderSize = 0; for (const FContainerTargetSpec* ContainerTarget : ContainerTargets) { for (const FShaderInfo* ShaderInfo : ContainerTarget->GlobalShaders) { ++GlobalShaderCount; GlobalShaderSize += ShaderInfo->CodeIoBuffer.DataSize(); } for (const FShaderInfo* ShaderInfo : ContainerTarget->SharedShaders) { ++SharedShaderCount; SharedShaderSize += ShaderInfo->CodeIoBuffer.DataSize(); } for (const FShaderInfo* ShaderInfo : ContainerTarget->UniqueShaders) { ++UniqueShaderCount; UniqueShaderSize += ShaderInfo->CodeIoBuffer.DataSize(); } for (const FShaderInfo* ShaderInfo : ContainerTarget->InlineShaders) { ++InlineShaderCount; InlineShaderSize += ShaderInfo->CodeIoBuffer.DataSize(); } } LogWriterResults(IoStoreWriterResults); LogContainerPackageInfo(ContainerTargets); UE_LOG(LogIoStore, Display, TEXT("Input: %8d Packages"), Packages.Num()); UE_LOG(LogIoStore, Display, TEXT("Input: %8.2lf GB UExp"), (double)UExpSize / 1024.0 / 1024.0 / 1024.0); UE_LOG(LogIoStore, Display, TEXT("Input: %8.2lf GB UAsset"), (double)UAssetSize / 1024.0 / 1024.0 / 1024.0); UE_LOG(LogIoStore, Display, TEXT("Input: %8.2lf GB UBulk"), (double)UBulkSize / 1024.0 / 1024.0 / 1024.0); UE_LOG(LogIoStore, Display, TEXT("Input: %8.2f MB for %d Global shaders"), (double)GlobalShaderSize / 1024.0 / 1024.0, GlobalShaderCount); UE_LOG(LogIoStore, Display, TEXT("Input: %8.2f MB for %d Shared shaders"), (double)SharedShaderSize / 1024.0 / 1024.0, SharedShaderCount); UE_LOG(LogIoStore, Display, TEXT("Input: %8.2f MB for %d Unique shaders"), (double)UniqueShaderSize / 1024.0 / 1024.0, UniqueShaderCount); UE_LOG(LogIoStore, Display, TEXT("Input: %8.2f MB for %d Inline shaders"), (double)InlineShaderSize / 1024.0 / 1024.0, InlineShaderCount); UE_LOG(LogIoStore, Display, TEXT("")); if (bIsLegacyStage) { UE_LOG(LogIoStore, Display, TEXT("Output: %8llu Name map entries"), NameMapCount); } UE_LOG(LogIoStore, Display, TEXT("Output: %8llu Imported package entries"), ImportedPackagesCount); UE_LOG(LogIoStore, Display, TEXT("Output: %8llu Packages without imports"), NoImportedPackagesCount); UE_LOG(LogIoStore, Display, TEXT("Output: %8d Public runtime script objects"), PackageStoreOptimizer.GetTotalScriptObjectCount()); if (bIsLegacyStage) { UE_LOG(LogIoStore, Display, TEXT("Output: %8.2lf GB HeaderData"), (double)HeaderSize / 1024.0 / 1024.0 / 1024.0); } UE_LOG(LogIoStore, Display, TEXT("Output: %8.2lf MB InitialLoadData"), (double)InitialLoadSize / 1024.0 / 1024.0); if (ChunkDatabase.IsValid()) { TRACE_CPUPROFILER_EVENT_SCOPE(ReferenceChunkLogging); FIoStoreChunkDatabase* ChunkDbPtr = (FIoStoreChunkDatabase*)ChunkDatabase.Get(); ChunkDbPtr->WriteCSV(Arguments.ReferenceChunkChangesCSVFileName, Packages); ChunkDbPtr->LogSummary(IoStoreWriterResults); } UE_LOG(LogIoStore, Display, TEXT("")); if (Arguments.CsvPath.Len() > 0) { TRACE_CPUPROFILER_EVENT_SCOPE(WaitForCsvFiles); UE_LOG(LogIoStore, Display, TEXT("Writing csv file(s) to: %s (*.utoc.csv)"), *Arguments.CsvPath); FTaskGraphInterface::Get().WaitUntilTaskCompletes(WriteCsvFileTask); } return 0; } void FIoStoreChunkDatabase::WriteCSV(const FString& InOutputFileName, const TArray& InPackages) { if (!bValid || InOutputFileName.Len() == 0) { return; } FString ChunkNames[FIoStoreChunkDatabase::IoChunkTypeCount]; for (uint8 TypeIndex = 0; TypeIndex < FIoStoreChunkDatabase::IoChunkTypeCount; TypeIndex++) { ChunkNames[TypeIndex] = LexToString((EIoChunkType)TypeIndex); } // // If we are using a reference cache, the assumption is that we are expecting a high hit rate // on the chunks - this is supposed to be a current release, so theoretically we should only // miss on changed or new data, so it's useful to know where the misses are in case something // major happened that maybe shouldn't have. // TMap PackageIdToName; for (const FCookedPackage* Package : InPackages) { PackageIdToName.Add(Package->GlobalPackageId, Package->PackageName); } auto FindPackageForChunk = [&PackageIdToName](const FIoChunkId& InChunkId) { // See CreateIoChunkId and CreatePackageDataChunkId uint64 ChunkValue = *(uint64*)InChunkId.GetData(); FName* Result = PackageIdToName.Find(FPackageId::FromValue(ChunkValue)); if (Result) { return *Result; } return FName(); }; TUniquePtr ChangeListArchive; ChangeListArchive.Reset(IFileManager::Get().CreateFileWriter(*InOutputFileName)); if (ChangeListArchive.IsValid() == false) { UE_LOG(LogIoStore, Warning, TEXT("Unable to open reference chunk change list file %s"), *InOutputFileName); return; } ChangeListArchive->Logf(TEXT("Container,ChunkType,Class,ChunkId,ChunkHash,ChunkPackage")); for (const TPair>& ReaderIdPair : ChunkDatabase) { FReaderChunks* Reader = ReaderIdPair.Value.Get(); for (TPair Chunk : Reader->ChunkIds) { if (Reader->ChunkChanged[Chunk.Value].load(std::memory_order_relaxed)) { FIoChunkId& Id = Chunk.Key; EIoChunkType ChangedType = Id.GetChunkType(); if (ChangedType == EIoChunkType::ShaderCode || ChangedType == EIoChunkType::ShaderCodeLibrary) { // These don't have the package name in the chunk id. ChangeListArchive->Logf(TEXT("%s,%s,CHANGED,%s,%s,"), *Reader->ContainerName, *ChunkNames[(uint8)ChangedType], *LexToString(Id), *LexToString(Reader->ChangedChunkHashes[Chunk.Value])); } else { ChangeListArchive->Logf(TEXT("%s,%s,CHANGED,%s,%s,%s"), *Reader->ContainerName, *ChunkNames[(uint8)ChangedType], *LexToString(Id), *LexToString(Reader->ChangedChunkHashes[Chunk.Value]), *FindPackageForChunk(Id).ToString()); } } } for (FRequestedChunkInfo& NewChunk : Reader->NewChunks) { if (NewChunk.Id.GetChunkType() == EIoChunkType::ShaderCode || NewChunk.Id.GetChunkType() == EIoChunkType::ShaderCodeLibrary) { // These don't have the package name in the chunk id. ChangeListArchive->Logf(TEXT("%s,%s,NEW,%s,%s,"), *Reader->ContainerName, *ChunkNames[(uint8)NewChunk.Id.GetChunkType()], *LexToString(NewChunk.Id), *LexToString(NewChunk.ChunkHash)); } else { ChangeListArchive->Logf(TEXT("%s,%s,NEW,%s,%s,%s"), *Reader->ContainerName, *ChunkNames[(uint8)NewChunk.Id.GetChunkType()], *LexToString(NewChunk.Id), *LexToString(NewChunk.ChunkHash), *FindPackageForChunk(NewChunk.Id).ToString()); } } } for (const TPair>& MissingContainerPair : MissingContainers) { FMissingContainerInfo* MissingContainer = MissingContainerPair.Value.Get(); for (const FRequestedChunkInfo& NoContainerChunk : MissingContainer->Chunks) { const FString& ChunkName = ChunkNames[(uint8)NoContainerChunk.Id.GetChunkType()]; if (NoContainerChunk.Id.GetChunkType() == EIoChunkType::ShaderCode || NoContainerChunk.Id.GetChunkType() == EIoChunkType::ShaderCodeLibrary) { // These don't have the package name in the chunk id. ChangeListArchive->Logf(TEXT("%s,%s,NOCONTAINER,%s,%s,"), *MissingContainer->ContainerName, *ChunkName, *LexToString(NoContainerChunk.Id), *LexToString(NoContainerChunk.ChunkHash)); } else { ChangeListArchive->Logf(TEXT("%s,%s,NOCONTAINER,%s,%s,%"), *MissingContainer->ContainerName, *ChunkName, *LexToString(NoContainerChunk.Id), *LexToString(NoContainerChunk.ChunkHash), *FindPackageForChunk(NoContainerChunk.Id).ToString()); } } } } bool DumpIoStoreContainerInfo(const TCHAR* InContainerFilename, const FKeyChain& InKeyChain) { TUniquePtr Reader = CreateIoStoreReader(InContainerFilename, InKeyChain); if (!Reader.IsValid()) { return false; } UE_LOG(LogIoStore, Display, TEXT("IoStore Container File: %s"), InContainerFilename); UE_LOG(LogIoStore, Display, TEXT(" Id: 0x%llX"), Reader->GetContainerId().Value()); UE_LOG(LogIoStore, Display, TEXT(" Version: %d"), Reader->GetVersion()); UE_LOG(LogIoStore, Display, TEXT(" Indexed: %d"), EnumHasAnyFlags(Reader->GetContainerFlags(), EIoContainerFlags::Indexed)); UE_LOG(LogIoStore, Display, TEXT(" Signed: %d"), EnumHasAnyFlags(Reader->GetContainerFlags(), EIoContainerFlags::Signed)); bool bIsEncrypted = EnumHasAnyFlags(Reader->GetContainerFlags(), EIoContainerFlags::Encrypted); UE_LOG(LogIoStore, Display, TEXT(" Encrypted: %d"), bIsEncrypted); if (bIsEncrypted) { UE_LOG(LogIoStore, Display, TEXT(" EncryptionKeyGuid: %s"), *Reader->GetEncryptionKeyGuid().ToString()); } bool bIsCompressed = EnumHasAnyFlags(Reader->GetContainerFlags(), EIoContainerFlags::Compressed); UE_LOG(LogIoStore, Display, TEXT(" Compressed: %d"), bIsCompressed); if (bIsCompressed) { UE_LOG(LogIoStore, Display, TEXT(" CompressionBlockSize: %llu"), Reader->GetCompressionBlockSize()); UE_LOG(LogIoStore, Display, TEXT(" CompressionMethods:")); for (FName Method : Reader->GetCompressionMethods()) { UE_LOG(LogPakFile, Display, TEXT(" %s"), *Method.ToString()); } } return true; } int32 CreateContentPatch(const FIoStoreArguments& Arguments, const FIoStoreWriterSettings& GeneralIoWriterSettings) { UE_LOG(LogIoStore, Display, TEXT("Building patch...")); TUniquePtr IoStoreWriterContext(new FIoStoreWriterContext()); FIoStatus IoStatus = IoStoreWriterContext->Initialize(GeneralIoWriterSettings); check(IoStatus.IsOk()); TArray> IoStoreWriters; for (const FContainerSourceSpec& Container : Arguments.Containers) { TArray> SourceReaders = CreatePatchSourceReaders(Container.PatchSourceContainerFiles, Arguments); TUniquePtr TargetReader = CreateIoStoreReader(*Container.PatchTargetFile, Arguments.KeyChain); if (!TargetReader.IsValid()) { UE_LOG(LogIoStore, Error, TEXT("Failed loading target container")); return -1; } EIoContainerFlags TargetContainerFlags = TargetReader->GetContainerFlags(); FIoContainerSettings ContainerSettings; if (Arguments.bCreateDirectoryIndex) { ContainerSettings.ContainerFlags |= EIoContainerFlags::Indexed; } ContainerSettings.ContainerId = TargetReader->GetContainerId(); if (Arguments.bSign || EnumHasAnyFlags(TargetContainerFlags, EIoContainerFlags::Signed)) { ContainerSettings.SigningKey =Arguments.KeyChain.GetSigningKey(); ContainerSettings.ContainerFlags |= EIoContainerFlags::Signed; } if (EnumHasAnyFlags(TargetContainerFlags, EIoContainerFlags::Encrypted)) { ContainerSettings.ContainerFlags |= EIoContainerFlags::Encrypted; const FNamedAESKey* Key = Arguments.KeyChain.GetEncryptionKeys().Find(TargetReader->GetEncryptionKeyGuid()); if (!Key) { UE_LOG(LogIoStore, Error, TEXT("Missing encryption key for target container")); return -1; } ContainerSettings.EncryptionKeyGuid = Key->Guid; ContainerSettings.EncryptionKey = Key->Key; } TSharedPtr IoStoreWriter = IoStoreWriterContext->CreateContainer(*Container.OutputPath, ContainerSettings); IoStoreWriters.Add(IoStoreWriter); TMap SourceHashByChunkId; for (const TUniquePtr& SourceReader : SourceReaders) { SourceReader->EnumerateChunks([&SourceHashByChunkId](const FIoStoreTocChunkInfo& ChunkInfo) { SourceHashByChunkId.Add(ChunkInfo.Id, ChunkInfo.ChunkHash); return true; }); } TMap ChunkFileNamesMap; TargetReader->GetDirectoryIndexReader().IterateDirectoryIndex(FIoDirectoryIndexHandle::RootDirectory(), TEXT(""), [&ChunkFileNamesMap, &TargetReader](FStringView Filename, uint32 TocEntryIndex) -> bool { TIoStatusOr ChunkInfo = TargetReader->GetChunkInfo(TocEntryIndex); if (ChunkInfo.IsOk()) { ChunkFileNamesMap.Add(ChunkInfo.ValueOrDie().Id, FString(Filename)); } return true; }); TargetReader->EnumerateChunks([&TargetReader, &SourceHashByChunkId, &IoStoreWriter, &ChunkFileNamesMap](const FIoStoreTocChunkInfo& ChunkInfo) { FIoHash* FindSourceHash = SourceHashByChunkId.Find(ChunkInfo.Id); if (!FindSourceHash || *FindSourceHash != ChunkInfo.ChunkHash) { FIoReadOptions ReadOptions; TIoStatusOr ChunkBuffer = TargetReader->Read(ChunkInfo.Id, ReadOptions); FIoWriteOptions WriteOptions; FString* FindFileName = ChunkFileNamesMap.Find(ChunkInfo.Id); if (FindFileName) { WriteOptions.FileName = *FindFileName; if (FindSourceHash) { UE_LOG(LogIoStore, Display, TEXT("Modified: %s"), **FindFileName); } else { UE_LOG(LogIoStore, Display, TEXT("Added: %s"), **FindFileName); } } WriteOptions.bIsMemoryMapped = ChunkInfo.bIsMemoryMapped; WriteOptions.bForceUncompressed = ChunkInfo.bForceUncompressed; IoStoreWriter->Append(ChunkInfo.Id, ChunkBuffer.ConsumeValueOrDie(), WriteOptions); } return true; }); } IoStoreWriterContext->Flush(); TArray Results; for (TSharedPtr IoStoreWriter : IoStoreWriters) { Results.Emplace(IoStoreWriter->GetResult().ConsumeValueOrDie()); } LogWriterResults(Results); return 0; } void Split(FStringView Line, FStringView::ViewType Sep, TArray& OutValues) { OutValues.Reset(); int32 Start = 0; int32 End = 0; for (;;) { End = Line.Find(TEXTVIEW(","), Start); if (End < 0) { break; } FStringView Value = Line.Mid(Start, End - Start); OutValues.Add(Value); Start = End + 1; } FStringView Value = Line.Mid(Start); OutValues.Add(Value); } int32 ListContainer( const FKeyChain& KeyChain, const FString& ContainerPathOrWildcard, const FString& CsvPath) { IOSTORE_CPU_SCOPE(ListContainer); TArray ContainerFilePaths; FString AllChunksInfoFilename; TMap> AllChunksInfoMap; if (FParse::Value(FCommandLine::Get(), TEXT("AllChunksInfo="), AllChunksInfoFilename)) { TArray Values; bool bSkipFirstLine = true; auto Visitor = [&AllChunksInfoMap,&Values,&bSkipFirstLine](FStringView Line) { const int32 PackageNameIndex = 1; const int32 ClassTypeIndex = 2; if (bSkipFirstLine) { bSkipFirstLine = false; return; } Split(Line, TEXTVIEW(","), Values); FName PackageName(Values[PackageNameIndex]); FPackageId PackageId = FPackageId::FromName(PackageName); AllChunksInfoMap.Add(PackageId, TPair(PackageName, FName(Values[ClassTypeIndex]))); }; FFileHelper::LoadFileToStringWithLineVisitor(*AllChunksInfoFilename, Visitor); } if (IFileManager::Get().FileExists(*ContainerPathOrWildcard)) { ContainerFilePaths.Add(ContainerPathOrWildcard); } else if (IFileManager::Get().DirectoryExists(*ContainerPathOrWildcard)) { FString Directory = ContainerPathOrWildcard; FPaths::NormalizeDirectoryName(Directory); TArray FoundContainerFiles; IFileManager::Get().FindFiles(FoundContainerFiles, *(Directory / TEXT("*.utoc")), true, false); for (const FString& Filename : FoundContainerFiles) { ContainerFilePaths.Emplace(Directory / Filename); } } else { FString Directory = FPaths::GetPath(ContainerPathOrWildcard); FPaths::NormalizeDirectoryName(Directory); TArray FoundContainerFiles; IFileManager::Get().FindFiles(FoundContainerFiles, *ContainerPathOrWildcard, true, false); for (const FString& Filename : FoundContainerFiles) { ContainerFilePaths.Emplace(Directory / Filename); } } if (ContainerFilePaths.Num() == 0) { UE_LOG(LogIoStore, Error, TEXT("Container '%s' doesn't exist and no container matches wildcard."), *ContainerPathOrWildcard); return -1; } // if CsvPath is a dir, not a file name, then write one csv per container to the dir // otherwise, write all contents to one big csv bool bPerContainerCsvFiles = IFileManager::Get().DirectoryExists(*CsvPath); FChunkEntryCsv AllContainersOutCsvFile; if (!bPerContainerCsvFiles) { AllContainersOutCsvFile.CreateOutputFile(*CsvPath); } for (const FString& ContainerFilePath : ContainerFilePaths) { TUniquePtr Reader = CreateIoStoreReader(*ContainerFilePath, KeyChain); if (!Reader.IsValid()) { UE_LOG(LogIoStore, Warning, TEXT("Failed to read container '%s'"), *ContainerFilePath); continue; } FChunkEntryCsv PerContainerOutCsvFile; FChunkEntryCsv* Out; if (!bPerContainerCsvFiles) { Out = &AllContainersOutCsvFile; } else { // ContainerFilePath is a .utoc, add .csv and put in CsvPath FString PerContainerCsvPath = CsvPath / FPaths::GetCleanFilename(ContainerFilePath) + TEXT(".csv"); PerContainerOutCsvFile.CreateOutputFile(*PerContainerCsvPath); Out = &PerContainerOutCsvFile; } if (!EnumHasAnyFlags(Reader->GetContainerFlags(), EIoContainerFlags::Indexed)) { UE_LOG(LogIoStore, Display, TEXT("No directory index for container '%s'"), *ContainerFilePath); } UE_LOG(LogIoStore, Display, TEXT("Listing container '%s'"), *ContainerFilePath); FString ContainerName = FPaths::GetBaseFilename(ContainerFilePath); TArray Chunks; { IOSTORE_CPU_SCOPE(EnumerateChunks); Reader->EnumerateChunks([&Chunks](FIoStoreTocChunkInfo&& ChunkInfo) { Chunks.Add(MoveTemp(ChunkInfo)); return true; }); } { IOSTORE_CPU_SCOPE(EnumerateChunks); auto SortKey = [](const FIoStoreTocChunkInfo& ChunkInfo) { return ChunkInfo.OffsetOnDisk; }; Algo::SortBy(Chunks, SortKey); } { IOSTORE_CPU_SCOPE(WriteCsvFile); FString PackageName; FString ClassType; for (int32 Index=0; Index < Chunks.Num(); ++Index) { const FIoStoreTocChunkInfo& ChunkInfo = Chunks[Index]; FPackageId PackageId = PackageIdFromChunkId(ChunkInfo.Id); PackageName.Reset(); ClassType.Reset(); if (ChunkInfo.bHasValidFileName && FPackageName::TryConvertFilenameToLongPackageName(ChunkInfo.FileName, PackageName, nullptr)) { PackageId = FPackageId::FromName(FName(*PackageName)); } TPair* PackageInfo = AllChunksInfoMap.Find(PackageId); if (PackageInfo) { PackageName = PackageInfo->Get<0>().ToString(); ClassType = PackageInfo->Get<1>().ToString(); } Out->AddChunk(ContainerName, Index, ChunkInfo, PackageId, PackageName, ClassType); } } } return 0; } bool ListIoStoreContainer(const TCHAR* CmdLine) { FKeyChain KeyChain; if (!LoadKeyChain(CmdLine, KeyChain)) { return false; } FString ContainerPathOrWildcard; if (!FParse::Value(FCommandLine::Get(), TEXT("ListContainer="), ContainerPathOrWildcard)) { UE_LOG(LogIoStore, Error, TEXT("Missing argument -ListContainer=")); return false; } FString CsvPath; if (!FParse::Value(FCommandLine::Get(), TEXT("csv="), CsvPath)) { UE_LOG(LogIoStore, Error, TEXT("Missing argument -Csv=")); return false; } return ListContainer(KeyChain, ContainerPathOrWildcard, CsvPath) == 0; } bool ListContainerBulkData( const FKeyChain& KeyChain, const FString& ContainerPathOrWildcard, const FString& OutFile) { struct FPackageData { FPackageId Id; FString Filename; TArray BulkDataMap; }; struct FContainerData { FString Name; FString Path; TArray Packages; }; TArray Containers; TArray ContainerFilePaths; if (IFileManager::Get().FileExists(*ContainerPathOrWildcard)) { ContainerFilePaths.Add(ContainerPathOrWildcard); } else if (IFileManager::Get().DirectoryExists(*ContainerPathOrWildcard)) { FString Directory = ContainerPathOrWildcard; FPaths::NormalizeDirectoryName(Directory); TArray FoundContainerFiles; IFileManager::Get().FindFiles(FoundContainerFiles, *(Directory / TEXT("*.utoc")), true, false); for (const FString& Filename : FoundContainerFiles) { ContainerFilePaths.Emplace(Directory / Filename); } } else { FString Directory = FPaths::GetPath(ContainerPathOrWildcard); FPaths::NormalizeDirectoryName(Directory); TArray FoundContainerFiles; IFileManager::Get().FindFiles(FoundContainerFiles, *ContainerPathOrWildcard, true, false); for (const FString& Filename : FoundContainerFiles) { ContainerFilePaths.Emplace(Directory / Filename); } } if (ContainerFilePaths.Num() == 0) { UE_LOG(LogIoStore, Error, TEXT("Container '%s' doesn't exist and no container matches wildcard."), *ContainerPathOrWildcard); return false; } for (const FString& ContainerFilePath : ContainerFilePaths) { FString ContainerName = FPaths::GetBaseFilename(ContainerFilePath); if (ContainerName == TEXT("global")) { continue; } TUniquePtr Reader = CreateIoStoreReader(*ContainerFilePath, KeyChain); if (!Reader.IsValid()) { UE_LOG(LogIoStore, Error, TEXT("Failed to read container '%s'"), *ContainerFilePath); continue; } TMap FilenameByChunkId; Reader->GetDirectoryIndexReader().IterateDirectoryIndex(FIoDirectoryIndexHandle::RootDirectory(), TEXT(""), [&FilenameByChunkId, &Reader](FStringView Filename, uint32 TocEntryIndex) -> bool { TIoStatusOr ChunkInfo = Reader->GetChunkInfo(TocEntryIndex); if (ChunkInfo.IsOk()) { FilenameByChunkId.Add(ChunkInfo.ValueOrDie().Id, FString(Filename)); } return true; }); UE_LOG(LogIoStore, Display, TEXT("Listing bulk data in container '%s'"), *ContainerFilePath); FIoChunkId ChunkId = CreateIoChunkId(Reader->GetContainerId().Value(), 0, EIoChunkType::ContainerHeader); TIoStatusOr Status = Reader->Read(ChunkId, FIoReadOptions()); if (!Status.IsOk()) { UE_LOG(LogIoStore, Display, TEXT("Failed to read container header '%s', reason '%s'"), *ContainerFilePath, *Status.Status().ToString()); continue; } FIoContainerHeader ContainerHeader; { FIoBuffer Chunk = Status.ValueOrDie(); FMemoryReaderView Ar(MakeArrayView(Chunk.Data(), Chunk.GetSize())); Ar << ContainerHeader; } FContainerData& Container = Containers.AddDefaulted_GetRef(); Container.Path = ContainerFilePath; Container.Name = MoveTemp(ContainerName); for (const FPackageId& PackageId : ContainerHeader.PackageIds) { ChunkId = CreatePackageDataChunkId(PackageId); Status = Reader->Read(ChunkId, FIoReadOptions()); if (!Status.IsOk()) { UE_LOG(LogIoStore, Display, TEXT("Failed to package data")); continue; } FIoBuffer Chunk = Status.ValueOrDie(); FZenPackageHeader PkgHeader = FZenPackageHeader::MakeView(Chunk.GetView()); FPackageData& Pkg = Container.Packages.AddDefaulted_GetRef(); Pkg.Id = PackageId; Pkg.BulkDataMap = PkgHeader.BulkDataMap; if (FString* Filename = FilenameByChunkId.Find(ChunkId)) { Pkg.Filename = *Filename; } } } const FString Ext = FPaths::GetExtension(OutFile); if (Ext == TEXT("json")) { using FWriter = TSharedPtr>>; using FWriterFactory = TJsonWriterFactory>; FString Json; FWriter Writer = FWriterFactory::Create(&Json); Writer->WriteArrayStart(); TStringBuilder<512> Sb; for (const FContainerData& Container: Containers) { Writer->WriteObjectStart(); Writer->WriteValue(TEXT("Container"), Container.Name); Writer->WriteArrayStart(TEXT("Packages")); for (const FPackageData& Pkg : Container.Packages) { if (Pkg.BulkDataMap.IsEmpty()) { continue; } Writer->WriteObjectStart(); Writer->WriteValue(TEXT("PackageId"), FString::Printf(TEXT("0x%llX"), Pkg.Id.Value())); Writer->WriteValue(TEXT("Filename"), Pkg.Filename); Writer->WriteArrayStart(TEXT("BulkData")); for (const FBulkDataMapEntry& Entry : Pkg.BulkDataMap) { Sb.Reset(); LexToString(static_cast(Entry.Flags), Sb); Writer->WriteObjectStart(); Writer->WriteValue(TEXT("Offset"), Entry.SerialOffset); Writer->WriteValue(TEXT("Size"), Entry.SerialSize); Writer->WriteValue(TEXT("Flags"), Sb.ToString()); Writer->WriteObjectEnd(); } Writer->WriteArrayEnd(); Writer->WriteObjectEnd(); } Writer->WriteArrayEnd(); Writer->WriteObjectEnd(); } Writer->WriteArrayEnd(); Writer->Close(); UE_LOG(LogIoStore, Display, TEXT("Saving '%s'"), *OutFile); if (!FFileHelper::SaveStringToFile(Json, *OutFile)) { return false; } } else { TUniquePtr CsvAr(IFileManager::Get().CreateFileWriter(*OutFile)); CsvAr->Logf(TEXT("Container,Filename,PackageId,Offset,Size,Flags")); TStringBuilder<512> Sb; for (const FContainerData& Container: Containers) { for (const FPackageData& Pkg : Container.Packages) { for (const FBulkDataMapEntry& Entry : Pkg.BulkDataMap) { Sb.Reset(); LexToString(static_cast(Entry.Flags), Sb); CsvAr->Logf(TEXT("%s,%s,0x%s,%lld,%lld,%s"), *Container.Name, *Pkg.Filename, *LexToString(Pkg.Id), Entry.SerialOffset, Entry.SerialSize, Sb.ToString()); } } } UE_LOG(LogIoStore, Display, TEXT("Saving '%s'"), *OutFile); } return true; } bool ListIoStoreContainerBulkData(const TCHAR* CmdLine) { FKeyChain KeyChain; if (!LoadKeyChain(CmdLine, KeyChain)) { return false; } FString ContainerPathOrWildcard; if (!FParse::Value(FCommandLine::Get(), TEXT("ListContainerBulkData="), ContainerPathOrWildcard)) { UE_LOG(LogIoStore, Error, TEXT("Missing argument -ListContainerBulkData=")); return false; } FString OutFile; if (!FParse::Value(FCommandLine::Get(), TEXT("Out="), OutFile)) { UE_LOG(LogIoStore, Error, TEXT("Missing argument -Out=")); return false; } return ListContainerBulkData(KeyChain, ContainerPathOrWildcard, OutFile) == 0; } bool LegacyListIoStoreContainer( const TCHAR* InContainerFilename, int64 InSizeFilter, const FString& InCSVFilename, const FKeyChain& InKeyChain) { TUniquePtr Reader = CreateIoStoreReader(InContainerFilename, InKeyChain); if (!Reader.IsValid()) { return false; } if (!EnumHasAnyFlags(Reader->GetContainerFlags(), EIoContainerFlags::Indexed)) { UE_LOG(LogIoStore, Fatal, TEXT("Missing directory index for container '%s'"), InContainerFilename); } int32 FileCount = 0; int64 FileSize = 0; TArray CompressionMethodNames; for (const FName& CompressionMethodName : Reader->GetCompressionMethods()) { CompressionMethodNames.Add(CompressionMethodName.ToString()); } TArray CompressionBlocks; TArray CompressedBlocks; Reader->EnumerateCompressedBlocks([&CompressedBlocks](const FIoStoreTocCompressedBlockInfo& Block) { CompressedBlocks.Add(Block); return true; }); const FIoDirectoryIndexReader& IndexReader = Reader->GetDirectoryIndexReader(); UE_LOG(LogIoStore, Display, TEXT("Mount point %s"), *IndexReader.GetMountPoint()); struct FEntry { FIoChunkId ChunkId; FIoHash ChunkHash; FString FileName; int64 Offset; int64 Size; int32 CompressionMethodIndex; }; TArray Entries; const uint64 CompressionBlockSize = Reader->GetCompressionBlockSize(); Reader->EnumerateChunks([&Entries, CompressionBlockSize, &CompressedBlocks](const FIoStoreTocChunkInfo& ChunkInfo) { const int32 FirstBlockIndex = int32(ChunkInfo.Offset / CompressionBlockSize); FEntry& Entry = Entries.AddDefaulted_GetRef(); Entry.ChunkId = ChunkInfo.Id; Entry.ChunkHash = ChunkInfo.ChunkHash; Entry.FileName = ChunkInfo.FileName; Entry.Offset = CompressedBlocks[FirstBlockIndex].Offset; Entry.Size = ChunkInfo.CompressedSize; Entry.CompressionMethodIndex = CompressedBlocks[FirstBlockIndex].CompressionMethodIndex; return true; }); struct FOffsetSort { bool operator()(const FEntry& A, const FEntry& B) const { return A.Offset < B.Offset; } }; Entries.Sort(FOffsetSort()); FileCount = Entries.Num(); if (InCSVFilename.Len() > 0) { TArray Lines; Lines.Empty(Entries.Num() + 2); Lines.Add(TEXT("Filename, Offset, Size, Hash, Deleted, Compressed, CompressionMethod")); for (const FEntry& Entry : Entries) { bool bWasCompressed = Entry.CompressionMethodIndex != 0; Lines.Add(FString::Printf( TEXT("%s, %lld, %lld, %s, %s, %s, %d"), *Entry.FileName, Entry.Offset, Entry.Size, *LexToString(Entry.ChunkHash), TEXT("false"), bWasCompressed ? TEXT("true") : TEXT("false"), Entry.CompressionMethodIndex)); } if (FFileHelper::SaveStringArrayToFile(Lines, *InCSVFilename) == false) { UE_LOG(LogIoStore, Display, TEXT("Failed to save CSV file %s"), *InCSVFilename); } else { UE_LOG(LogIoStore, Display, TEXT("Saved CSV file to %s"), *InCSVFilename); } } for (const FEntry& Entry : Entries) { if (Entry.Size >= InSizeFilter) { UE_LOG(LogIoStore, Display, TEXT("\"%s\" offset: %lld, size: %d bytes, hash: %s, compression: %s."), *Entry.FileName, Entry.Offset, Entry.Size, *LexToString(Entry.ChunkHash), *CompressionMethodNames[Entry.CompressionMethodIndex]); } FileSize += Entry.Size; } UE_LOG(LogIoStore, Display, TEXT("%d files (%lld bytes)."), FileCount, FileSize); return true; } int32 ProfileReadSpeed(const TCHAR* InCommandLine, const FKeyChain& InKeyChain) { FString ContainerPath; if (FParse::Value(InCommandLine, TEXT("Container="), ContainerPath) == false) { UE_LOG(LogIoStore, Display, TEXT("")); UE_LOG(LogIoStore, Display, TEXT("ProfileReadSpeed")); UE_LOG(LogIoStore, Display, TEXT("")); UE_LOG(LogIoStore, Display, TEXT("Reads the given utoc file using given a read method. This uses FIoStoreReader, which is not")); UE_LOG(LogIoStore, Display, TEXT("the system the runtime uses to load and stream iostore containers! It's for utility/debug use only.")); UE_LOG(LogIoStore, Display, TEXT("")); UE_LOG(LogIoStore, Display, TEXT("Arguments:")); UE_LOG(LogIoStore, Display, TEXT("")); UE_LOG(LogIoStore, Display, TEXT(" -Container=path/to/utoc [required] The .utoc file to read.")); UE_LOG(LogIoStore, Display, TEXT(" -ReadType={Read, ReadAsync, ReadCompressed} What read function to use on FIoStoreReader. Default: Read")); UE_LOG(LogIoStore, Display, TEXT(" -cryptokeys=path/to/crypto.json [required if encrypted] The keys to decrypt the container.")); UE_LOG(LogIoStore, Display, TEXT(" -MaxJobCount=# The number of outstanding read tasks to maintain. Default: 512.")); UE_LOG(LogIoStore, Display, TEXT(" -Validate Whether to hash the reads and verify they match. Invalid for ReadCompressed. Default: disabled")); return 1; } TUniquePtr Reader = CreateIoStoreReader(*ContainerPath, InKeyChain); if (Reader.IsValid() == false) { return 1; // Already logged } TArray Chunks; Reader->EnumerateChunks([&Chunks](const FIoStoreTocChunkInfo& ChunkInfo) { Chunks.Add(ChunkInfo.Id); return true; }); enum class EReadType { Read, ReadAsync, ReadCompressed }; auto ReadTypeToString = [](EReadType InReadType) { switch (InReadType) { case EReadType::Read: return TEXT("Read"); case EReadType::ReadAsync: return TEXT("ReadAsync"); case EReadType::ReadCompressed: return TEXT("ReadCompressed"); default: return TEXT("INVALID"); } }; EReadType ReadType = EReadType::Read; int32 MaxOutstandingJobs = 512; bool bValidate = false; bValidate = FParse::Param(InCommandLine, TEXT("Validate")); FParse::Value(InCommandLine, TEXT("MaxJobCount="), MaxOutstandingJobs); FString ReadTypeRaw; if (FParse::Value(InCommandLine, TEXT("ReadType="), ReadTypeRaw)) { if (ReadTypeRaw.Compare(TEXT("Read"), ESearchCase::IgnoreCase) == 0) { ReadType = EReadType::Read; } else if (ReadTypeRaw.Compare(TEXT("ReadAsync"), ESearchCase::IgnoreCase) == 0) { ReadType = EReadType::ReadAsync; } else if (ReadTypeRaw.Compare(TEXT("ReadCompressed"), ESearchCase::IgnoreCase) == 0) { ReadType = EReadType::ReadCompressed; } else { UE_LOG(LogIoStore, Error, TEXT("Invalid -ReadType provided: %s. Valid are {Read, ReadAsync, ReadCompressed}"), *ReadTypeRaw); return 1; } } if (MaxOutstandingJobs <= 0) { UE_LOG(LogIoStore, Error, TEXT("Invalid -MaxJobCount provided: %d. Specify a positive integer"), MaxOutstandingJobs); return 1; } if (ReadType == EReadType::ReadCompressed && bValidate) { UE_LOG(LogIoStore, Error, TEXT("Can't validate ReadCompressed as the data is not decompressed and thus can't be hashed")); return 1; } UE_LOG(LogIoStore, Display, TEXT("MaxJobCount: %s"), *FText::AsNumber(MaxOutstandingJobs).ToString()); UE_LOG(LogIoStore, Display, TEXT("ReadType: %s"), ReadTypeToString(ReadType)); UE_LOG(LogIoStore, Display, TEXT("Validation: %s"), bValidate ? TEXT("Enabled") : TEXT("Disabled")); UE_LOG(LogIoStore, Display, TEXT("Container Encrypted: %s"), EnumHasAnyFlags(Reader->GetContainerFlags(), EIoContainerFlags::Encrypted) ? TEXT("Yes") : TEXT("No")); // We need a resettable event, so we can't use any of the task system events. FEvent* JustGotSpaceEvent = FPlatformProcess::GetSynchEventFromPool(); UE::Tasks::FTaskEvent CompletedEvent(TEXT("ProfileReadDone")); std::atomic_int32_t OutstandingJobs = 0; std::atomic_int32_t TotalJobsRemaining = Chunks.Num(); std::atomic_int64_t BytesRead = 0; double StartTime = FPlatformTime::Seconds(); UE_LOG(LogIoStore, Display, TEXT("Dispatching %s chunk reads (%s max at one time)"), *FText::AsNumber(Chunks.Num()).ToString(), *FText::AsNumber(MaxOutstandingJobs).ToString()); for (FIoChunkId& Id : Chunks) { for (;;) { int32 CurrentOutstanding = OutstandingJobs.load(); if (CurrentOutstanding == MaxOutstandingJobs) { // Wait for one to complete. JustGotSpaceEvent->Wait(); continue; } else if (CurrentOutstanding > MaxOutstandingJobs) { UE_LOG(LogIoStore, Warning, TEXT("Synch error -- too many jobs oustanding %d"), CurrentOutstanding); } break; } OutstandingJobs++; UE::Tasks::Launch(TEXT("IoStoreUtil::ReadJob"), [Id = Id, &OutstandingJobs, &JustGotSpaceEvent, &TotalJobsRemaining, &BytesRead, &Reader, &CompletedEvent, MaxOutstandingJobs, ReadType, bValidate]() { FIoHash ReadHash; bool bHashValid = false; switch (ReadType) { case EReadType::ReadCompressed: { FIoStoreCompressedReadResult Result = Reader->ReadCompressed(Id, FIoReadOptions()).ValueOrDie(); BytesRead += Result.IoBuffer.GetSize(); break; } case EReadType::Read: { FIoBuffer Result = Reader->Read(Id, FIoReadOptions()).ValueOrDie(); BytesRead += Result.GetSize(); if (bValidate) { ReadHash = FIoHash::HashBuffer(Result.GetData(), Result.GetSize()); bHashValid = true; } break; } case EReadType::ReadAsync: { UE::Tasks::TTask> Tasks = Reader->ReadAsync(Id, FIoReadOptions()); Tasks.Wait(); FIoBuffer Result = Tasks.GetResult().ValueOrDie(); BytesRead += Result.GetSize(); if (bValidate) { ReadHash = FIoHash::HashBuffer(Result.GetData(), Result.GetSize()); bHashValid = true; } break; } } if (bHashValid) { FIoHash CheckAgainstHash = Reader->GetChunkInfo(Id).ValueOrDie().ChunkHash; if (ReadHash != CheckAgainstHash) { UE_LOG(LogIoStore, Warning, TEXT("Read hash mismatch: Chunk %s"), *LexToString(Id)); } } if (OutstandingJobs.fetch_add(-1) == MaxOutstandingJobs) { // We are the first to make space in our limit, so release the dispatch thread to add more. JustGotSpaceEvent->Trigger(); } int32 JobsRemaining = TotalJobsRemaining.fetch_add(-1); if ((JobsRemaining % 1000) == 1) { UE_LOG(LogIoStore, Display, TEXT("Jobs Remaining: %d"), JobsRemaining - 1); } // Were we the last job issued? if (JobsRemaining == 1) { CompletedEvent.Trigger(); } }); } { double WaitStartTime = FPlatformTime::Seconds(); CompletedEvent.Wait(); UE_LOG(LogIoStore, Display, TEXT("Waited %.1f seconds"), FPlatformTime::Seconds() - WaitStartTime); } FPlatformProcess::ReturnSynchEventToPool(JustGotSpaceEvent); double TotalTime = FPlatformTime::Seconds() - StartTime; int64 BytesPerSecond = int64(BytesRead.load() / TotalTime); UE_LOG(LogIoStore, Display, TEXT("%s bytes in %.1f seconds; %s bytes per second"), *FText::AsNumber((int64)BytesRead.load()).ToString(), TotalTime, *FText::AsNumber(BytesPerSecond).ToString()); return 0; } namespace DescribeUtils { struct FPackageDesc; struct FPackageRedirect { FPackageDesc* Source = nullptr; FPackageDesc* Target = nullptr; }; struct FContainerDesc { FName Name; FIoContainerId Id; FGuid EncryptionKeyGuid; TArray LocalizedPackages; TArray PackageRedirects; bool bCompressed; bool bSigned; bool bEncrypted; bool bIndexed; }; struct FPackageLocation { FContainerDesc* Container = nullptr; uint64 Offset = -1; }; struct FExportDesc { FPackageDesc* Package = nullptr; FName Name; FName FullName; uint64 PublicExportHash; FPackageObjectIndex OuterIndex; FPackageObjectIndex ClassIndex; FPackageObjectIndex SuperIndex; FPackageObjectIndex TemplateIndex; uint64 SerialOffset = 0; uint64 SerialSize = 0; FSHAHash ExportHash; }; struct FExportBundleEntryDesc { FExportBundleEntry::EExportCommandType CommandType = FExportBundleEntry::ExportCommandType_Count; int32 LocalExportIndex = -1; FExportDesc* Export = nullptr; }; struct FImportDesc { FName Name; FPackageObjectIndex GlobalImportIndex; FExportDesc* Export = nullptr; }; struct FScriptObjectDesc { FName Name; FName FullName; FPackageObjectIndex GlobalImportIndex; FPackageObjectIndex OuterIndex; }; struct FPackageDesc { FPackageId PackageId; FName PackageName; uint32 PackageFlags = 0; int32 NameCount = -1; TArray> Locations; TArray ImportedPackageIds; TArray ImportedPublicExportHashes; TArray Imports; TArray Exports; TArray ExportBundleEntries; }; // Info loaded about a set of containers for the purposes of dumping to text in Describe or exploring some other way for debugging struct FContainerPackageInfo { TArray Containers; TArray Packages; TMap ScriptObjectByGlobalIdMap; TMap ExportByKeyMap; FContainerPackageInfo() = default; FContainerPackageInfo( TArray InContainers, TArray InPackages, TMap InScriptObjectByGlobalIdMap, TMap InExportByKeyMap) : Containers(MoveTemp(InContainers)) , Packages(MoveTemp(InPackages)) , ScriptObjectByGlobalIdMap(MoveTemp(InScriptObjectByGlobalIdMap)) , ExportByKeyMap(MoveTemp(InExportByKeyMap)) {} FContainerPackageInfo(const FContainerPackageInfo&) = delete; FContainerPackageInfo(FContainerPackageInfo&&) = default; FContainerPackageInfo& operator=(const FContainerPackageInfo&) = delete; FContainerPackageInfo& operator=(FContainerPackageInfo&&) = default; ~FContainerPackageInfo() { for (FPackageDesc* PackageDesc : Packages) { delete PackageDesc; } for (FContainerDesc* ContainerDesc : Containers) { delete ContainerDesc; } } FString PackageObjectIndexToString(const FPackageDesc* Package, const FPackageObjectIndex& PackageObjectIndex, bool bIncludeName) { if (PackageObjectIndex.IsNull()) { return TEXT(""); } else if (PackageObjectIndex.IsPackageImport()) { FPublicExportKey Key = FPublicExportKey::FromPackageImport(PackageObjectIndex, Package->ImportedPackageIds, Package->ImportedPublicExportHashes); FExportDesc* ExportDesc = ExportByKeyMap.FindRef(Key); if (ExportDesc && bIncludeName) { return FString::Printf(TEXT("0x%" UINT64_X_FMT " '%s'"), PackageObjectIndex.Value(), *ExportDesc->FullName.ToString()); } else { return FString::Printf(TEXT("0x%" UINT64_X_FMT), PackageObjectIndex.Value()); } } else if (PackageObjectIndex.IsScriptImport()) { const FScriptObjectDesc* ScriptObjectDesc = ScriptObjectByGlobalIdMap.Find(PackageObjectIndex); if (ScriptObjectDesc && bIncludeName) { return FString::Printf(TEXT("0x%" UINT64_X_FMT " '%s'"), PackageObjectIndex.Value(), *ScriptObjectDesc->FullName.ToString()); } else { return FString::Printf(TEXT("0x%" UINT64_X_FMT), PackageObjectIndex.Value()); } } else if (PackageObjectIndex.IsExport()) { return FString::Printf(TEXT("%" UINT64_X_FMT), PackageObjectIndex.Value()); } else { return FString::Printf(TEXT("0x%" UINT64_X_FMT), PackageObjectIndex.Value()); } } }; // Try and read all packages inside the containers and the links between them for debugging/analysis TOptional TryGetContainerPackageInfo( const FString& GlobalContainerPath, const FKeyChain& KeyChain, bool bIncludeExportHashes ) { if (!IFileManager::Get().FileExists(*GlobalContainerPath)) { UE_LOG(LogIoStore, Error, TEXT("Global container '%s' doesn't exist."), *GlobalContainerPath); return {}; } TUniquePtr GlobalReader = CreateIoStoreReader(*GlobalContainerPath, KeyChain); if (!GlobalReader.IsValid()) { UE_LOG(LogIoStore, Warning, TEXT("Failed reading global container '%s'"), *GlobalContainerPath); return {}; } UE_LOG(LogIoStore, Display, TEXT("Loading script imports...")); TIoStatusOr ScriptObjectsBuffer = GlobalReader->Read(CreateIoChunkId(0, 0, EIoChunkType::ScriptObjects), FIoReadOptions()); if (!ScriptObjectsBuffer.IsOk()) { UE_LOG(LogIoStore, Warning, TEXT("Failed reading initial load meta chunk from global container '%s'"), *GlobalContainerPath); return {}; } TMap ScriptObjectByGlobalIdMap; FLargeMemoryReader ScriptObjectsArchive(ScriptObjectsBuffer.ValueOrDie().Data(), ScriptObjectsBuffer.ValueOrDie().DataSize()); TArray GlobalNameMap = LoadNameBatch(ScriptObjectsArchive); int32 NumScriptObjects = 0; ScriptObjectsArchive << NumScriptObjects; const FScriptObjectEntry* ScriptObjectEntries = reinterpret_cast(ScriptObjectsBuffer.ValueOrDie().Data() + ScriptObjectsArchive.Tell()); for (int32 ScriptObjectIndex = 0; ScriptObjectIndex < NumScriptObjects; ++ScriptObjectIndex) { const FScriptObjectEntry& ScriptObjectEntry = ScriptObjectEntries[ScriptObjectIndex]; FMappedName MappedName = ScriptObjectEntry.Mapped; check(MappedName.IsGlobal()); FScriptObjectDesc& ScriptObjectDesc = ScriptObjectByGlobalIdMap.Add(ScriptObjectEntry.GlobalIndex); ScriptObjectDesc.Name = GlobalNameMap[MappedName.GetIndex()].ToName(MappedName.GetNumber()); ScriptObjectDesc.GlobalImportIndex = ScriptObjectEntry.GlobalIndex; ScriptObjectDesc.OuterIndex = ScriptObjectEntry.OuterIndex; } for (auto& KV : ScriptObjectByGlobalIdMap) { FScriptObjectDesc& ScriptObjectDesc = KV.Get<1>(); if (ScriptObjectDesc.FullName.IsNone()) { TArray ScriptObjectStack; FScriptObjectDesc* Current = &ScriptObjectDesc; FString FullName; while (Current) { if (!Current->FullName.IsNone()) { FullName = Current->FullName.ToString(); break; } ScriptObjectStack.Push(Current); Current = ScriptObjectByGlobalIdMap.Find(Current->OuterIndex); } while (ScriptObjectStack.Num() > 0) { Current = ScriptObjectStack.Pop(); FullName /= Current->Name.ToString(); Current->FullName = FName(FullName); } } } FString Directory = FPaths::GetPath(GlobalContainerPath); FPaths::NormalizeDirectoryName(Directory); TArray FoundContainerFiles; IFileManager::Get().FindFiles(FoundContainerFiles, *(Directory / TEXT("*.utoc")), true, false); TArray ContainerFilePaths; for (const FString& Filename : FoundContainerFiles) { ContainerFilePaths.Emplace(Directory / Filename); } UE_LOG(LogIoStore, Display, TEXT("Loading containers...")); TArray> Readers; struct FLoadContainerHeaderJob { FName ContainerName; FContainerDesc* ContainerDesc = nullptr; TArray Packages; FIoStoreReader* Reader = nullptr; TArray RawLocalizedPackages; TArray RawPackageRedirects; }; TArray LoadContainerHeaderJobs; for (const FString& ContainerFilePath : ContainerFilePaths) { TUniquePtr Reader = CreateIoStoreReader(*ContainerFilePath, KeyChain); if (!Reader.IsValid()) { UE_LOG(LogIoStore, Warning, TEXT("Failed to read container '%s'"), *ContainerFilePath); continue; } FLoadContainerHeaderJob& LoadContainerHeaderJob = LoadContainerHeaderJobs.AddDefaulted_GetRef(); LoadContainerHeaderJob.Reader = Reader.Get(); LoadContainerHeaderJob.ContainerName = FName(FPaths::GetBaseFilename(ContainerFilePath)); Readers.Emplace(MoveTemp(Reader)); } TAtomic TotalPackageCount{ 0 }; ParallelFor(LoadContainerHeaderJobs.Num(), [&LoadContainerHeaderJobs, &TotalPackageCount](int32 Index) { TRACE_CPUPROFILER_EVENT_SCOPE(LoadContainerHeader); FLoadContainerHeaderJob& Job = LoadContainerHeaderJobs[Index]; FContainerDesc* ContainerDesc = new FContainerDesc(); ContainerDesc->Name = Job.ContainerName; ContainerDesc->Id = Job.Reader->GetContainerId(); ContainerDesc->EncryptionKeyGuid = Job.Reader->GetEncryptionKeyGuid(); EIoContainerFlags Flags = Job.Reader->GetContainerFlags(); ContainerDesc->bCompressed = bool(Flags & EIoContainerFlags::Compressed); ContainerDesc->bEncrypted = bool(Flags & EIoContainerFlags::Encrypted); ContainerDesc->bSigned = bool(Flags & EIoContainerFlags::Signed); ContainerDesc->bIndexed = bool(Flags & EIoContainerFlags::Indexed); Job.ContainerDesc = ContainerDesc; TIoStatusOr IoBuffer = Job.Reader->Read(CreateIoChunkId(Job.Reader->GetContainerId().Value(), 0, EIoChunkType::ContainerHeader), FIoReadOptions()); if (IoBuffer.IsOk()) { FMemoryReaderView Ar(MakeArrayView(IoBuffer.ValueOrDie().Data(), IoBuffer.ValueOrDie().DataSize())); FIoContainerHeader ContainerHeader; Ar << ContainerHeader; Job.RawLocalizedPackages = ContainerHeader.LocalizedPackages; Job.RawPackageRedirects = ContainerHeader.PackageRedirects; TArrayView StoreEntries(reinterpret_cast(ContainerHeader.StoreEntries.GetData()), ContainerHeader.PackageIds.Num()); int32 PackageIndex = 0; Job.Packages.Reserve(StoreEntries.Num()); for (FFilePackageStoreEntry& ContainerEntry : StoreEntries) { const FPackageId& PackageId = ContainerHeader.PackageIds[PackageIndex++]; FPackageDesc* PackageDesc = new FPackageDesc(); PackageDesc->PackageId = PackageId; PackageDesc->ImportedPackageIds = TArrayView(ContainerEntry.ImportedPackages.Data(), ContainerEntry.ImportedPackages.Num()); Job.Packages.Add(PackageDesc); ++TotalPackageCount; } } }, EParallelForFlags::Unbalanced); struct FLoadPackageSummaryJob { FPackageDesc* PackageDesc = nullptr; FIoChunkId ChunkId; TArray> Containers; }; TArray LoadPackageSummaryJobs; TArray Containers; TArray Packages; TMap PackageByIdMap; TMap PackageJobByIdMap; Containers.Reserve(LoadContainerHeaderJobs.Num()); Packages.Reserve(TotalPackageCount); PackageByIdMap.Reserve(TotalPackageCount); PackageJobByIdMap.Reserve(TotalPackageCount); LoadPackageSummaryJobs.Reserve(TotalPackageCount); for (FLoadContainerHeaderJob& LoadContainerHeaderJob : LoadContainerHeaderJobs) { Containers.Add(LoadContainerHeaderJob.ContainerDesc); for (FPackageDesc* PackageDesc : LoadContainerHeaderJob.Packages) { FLoadPackageSummaryJob*& UniquePackageJob = PackageJobByIdMap.FindOrAdd(PackageDesc->PackageId); if (!UniquePackageJob) { Packages.Add(PackageDesc); PackageByIdMap.Add(PackageDesc->PackageId, PackageDesc); FLoadPackageSummaryJob& LoadPackageSummaryJob = LoadPackageSummaryJobs.AddDefaulted_GetRef(); LoadPackageSummaryJob.PackageDesc = PackageDesc; LoadPackageSummaryJob.ChunkId = CreateIoChunkId(PackageDesc->PackageId.Value(), 0, EIoChunkType::ExportBundleData); UniquePackageJob = &LoadPackageSummaryJob; } UniquePackageJob->Containers.Add(&LoadContainerHeaderJob); } } for (FLoadContainerHeaderJob& LoadContainerHeaderJob : LoadContainerHeaderJobs) { for (const auto& RedirectPair : LoadContainerHeaderJob.RawPackageRedirects) { FPackageRedirect& PackageRedirect = LoadContainerHeaderJob.ContainerDesc->PackageRedirects.AddDefaulted_GetRef(); PackageRedirect.Source = PackageByIdMap.FindRef(RedirectPair.SourcePackageId); PackageRedirect.Target = PackageByIdMap.FindRef(RedirectPair.TargetPackageId); } for (const auto& LocalizedPackage : LoadContainerHeaderJob.RawLocalizedPackages) { LoadContainerHeaderJob.ContainerDesc->LocalizedPackages.Add(PackageByIdMap.FindRef(LocalizedPackage.SourcePackageId)); } } ParallelFor(LoadPackageSummaryJobs.Num(), [&LoadPackageSummaryJobs, bIncludeExportHashes](int32 Index) { TRACE_CPUPROFILER_EVENT_SCOPE(LoadPackageSummary); FLoadPackageSummaryJob& Job = LoadPackageSummaryJobs[Index]; for (FLoadContainerHeaderJob* LoadContainerHeaderJob : Job.Containers) { TIoStatusOr ChunkInfo = LoadContainerHeaderJob->Reader->GetChunkInfo(Job.ChunkId); check(ChunkInfo.IsOk()); FPackageLocation& Location = Job.PackageDesc->Locations.AddDefaulted_GetRef(); Location.Container = LoadContainerHeaderJob->ContainerDesc; Location.Offset = ChunkInfo.ValueOrDie().Offset; } FIoStoreReader* Reader = Job.Containers[0]->Reader; FIoReadOptions ReadOptions; if (!bIncludeExportHashes) { ReadOptions.SetRange(0, 16 << 10); } TIoStatusOr IoBuffer = Reader->Read(Job.ChunkId, ReadOptions); check(IoBuffer.IsOk()); const uint8* PackageSummaryData = IoBuffer.ValueOrDie().Data(); const FZenPackageSummary* PackageSummary = reinterpret_cast(PackageSummaryData); if (PackageSummary->HeaderSize > IoBuffer.ValueOrDie().DataSize()) { ReadOptions.SetRange(0, PackageSummary->HeaderSize); IoBuffer = Reader->Read(Job.ChunkId, ReadOptions); PackageSummaryData = IoBuffer.ValueOrDie().Data(); PackageSummary = reinterpret_cast(PackageSummaryData); } TArrayView HeaderDataView(PackageSummaryData + sizeof(FZenPackageSummary), PackageSummary->HeaderSize - sizeof(FZenPackageSummary)); FMemoryReaderView HeaderDataReader(HeaderDataView); FZenPackageVersioningInfo VersioningInfo; if (PackageSummary->bHasVersioningInfo) { HeaderDataReader << VersioningInfo; } FZenPackageCellOffsets CellOffsets; if (!PackageSummary->bHasVersioningInfo || VersioningInfo.PackageVersion >= EUnrealEngineObjectUE5Version::VERSE_CELLS) { HeaderDataReader << CellOffsets.CellImportMapOffset; HeaderDataReader << CellOffsets.CellExportMapOffset; } else { CellOffsets.CellImportMapOffset = PackageSummary->ExportBundleEntriesOffset; CellOffsets.CellExportMapOffset = PackageSummary->ExportBundleEntriesOffset; } TArray PackageNameMap; { TRACE_CPUPROFILER_EVENT_SCOPE(LoadNameBatch); PackageNameMap = LoadNameBatch(HeaderDataReader); } Job.PackageDesc->PackageName = PackageNameMap[PackageSummary->Name.GetIndex()].ToName(PackageSummary->Name.GetNumber()); Job.PackageDesc->PackageFlags = PackageSummary->PackageFlags; Job.PackageDesc->NameCount = PackageNameMap.Num(); Job.PackageDesc->ImportedPublicExportHashes = MakeArrayView(reinterpret_cast(PackageSummaryData + PackageSummary->ImportedPublicExportHashesOffset), (PackageSummary->ImportMapOffset - PackageSummary->ImportedPublicExportHashesOffset) / sizeof(uint64)); const FPackageObjectIndex* ImportMap = reinterpret_cast(PackageSummaryData + PackageSummary->ImportMapOffset); Job.PackageDesc->Imports.SetNum((PackageSummary->ExportMapOffset - PackageSummary->ImportMapOffset) / sizeof(FPackageObjectIndex)); for (int32 ImportIndex = 0; ImportIndex < Job.PackageDesc->Imports.Num(); ++ImportIndex) { FImportDesc& ImportDesc = Job.PackageDesc->Imports[ImportIndex]; ImportDesc.GlobalImportIndex = ImportMap[ImportIndex]; } const FExportMapEntry* ExportMap = reinterpret_cast(PackageSummaryData + PackageSummary->ExportMapOffset); Job.PackageDesc->Exports.SetNum((CellOffsets.CellImportMapOffset - PackageSummary->ExportMapOffset) / sizeof(FExportMapEntry)); for (int32 ExportIndex = 0; ExportIndex < Job.PackageDesc->Exports.Num(); ++ExportIndex) { const FExportMapEntry& ExportMapEntry = ExportMap[ExportIndex]; FExportDesc& ExportDesc = Job.PackageDesc->Exports[ExportIndex]; ExportDesc.Package = Job.PackageDesc; ExportDesc.Name = PackageNameMap[ExportMapEntry.ObjectName.GetIndex()].ToName(ExportMapEntry.ObjectName.GetNumber()); ExportDesc.OuterIndex = ExportMapEntry.OuterIndex; ExportDesc.ClassIndex = ExportMapEntry.ClassIndex; ExportDesc.SuperIndex = ExportMapEntry.SuperIndex; ExportDesc.TemplateIndex = ExportMapEntry.TemplateIndex; ExportDesc.PublicExportHash = ExportMapEntry.PublicExportHash; ExportDesc.SerialOffset = PackageSummary->HeaderSize + ExportMapEntry.CookedSerialOffset; ExportDesc.SerialSize = ExportMapEntry.CookedSerialSize; } const FExportBundleEntry* ExportBundleEntries = reinterpret_cast(PackageSummaryData + PackageSummary->ExportBundleEntriesOffset); const FExportBundleEntry* BundleEntry = ExportBundleEntries; int32 ExportBundleEntriesCount = Job.PackageDesc->Exports.Num() * 2; const FExportBundleEntry* BundleEntryEnd = BundleEntry + ExportBundleEntriesCount; Job.PackageDesc->ExportBundleEntries.Reserve(ExportBundleEntriesCount); while (BundleEntry < BundleEntryEnd) { FExportBundleEntryDesc& EntryDesc = Job.PackageDesc->ExportBundleEntries.AddDefaulted_GetRef(); EntryDesc.CommandType = FExportBundleEntry::EExportCommandType(BundleEntry->CommandType); EntryDesc.LocalExportIndex = BundleEntry->LocalExportIndex; EntryDesc.Export = &Job.PackageDesc->Exports[BundleEntry->LocalExportIndex]; if (BundleEntry->CommandType == FExportBundleEntry::ExportCommandType_Serialize) { if (bIncludeExportHashes) { check(EntryDesc.Export->SerialOffset + EntryDesc.Export->SerialSize <= IoBuffer.ValueOrDie().DataSize()); FSHA1::HashBuffer(IoBuffer.ValueOrDie().Data() + EntryDesc.Export->SerialOffset, EntryDesc.Export->SerialSize, EntryDesc.Export->ExportHash.Hash); } } ++BundleEntry; } }, EParallelForFlags::Unbalanced); UE_LOG(LogIoStore, Display, TEXT("Connecting imports and exports...")); TMap ExportByKeyMap; { TRACE_CPUPROFILER_EVENT_SCOPE(ConnectImportsAndExports); for (FPackageDesc* PackageDesc : Packages) { for (FExportDesc& ExportDesc : PackageDesc->Exports) { if (ExportDesc.PublicExportHash) { FPublicExportKey Key = FPublicExportKey::MakeKey(PackageDesc->PackageId, ExportDesc.PublicExportHash); ExportByKeyMap.Add(Key, &ExportDesc); } } } ParallelFor(Packages.Num(), [&Packages](int32 Index) { FPackageDesc* PackageDesc = Packages[Index]; for (FExportDesc& ExportDesc : PackageDesc->Exports) { if (ExportDesc.FullName.IsNone()) { TRACE_CPUPROFILER_EVENT_SCOPE(GenerateExportFullName); TArray ExportStack; FExportDesc* Current = &ExportDesc; TStringBuilder<2048> FullNameBuilder; TCHAR NameBuffer[FName::StringBufferSize]; for (;;) { if (!Current->FullName.IsNone()) { Current->FullName.ToString(NameBuffer); FullNameBuilder.Append(NameBuffer); break; } ExportStack.Push(Current); if (Current->OuterIndex.IsNull()) { PackageDesc->PackageName.ToString(NameBuffer); FullNameBuilder.Append(NameBuffer); break; } Current = &PackageDesc->Exports[Current->OuterIndex.Value()]; } while (ExportStack.Num() > 0) { Current = ExportStack.Pop(EAllowShrinking::No); FullNameBuilder.Append(TEXT(".")); Current->Name.ToString(NameBuffer); FullNameBuilder.Append(NameBuffer); Current->FullName = FName(FullNameBuilder); } } } }, EParallelForFlags::Unbalanced); for (FPackageDesc* PackageDesc : Packages) { for (FImportDesc& Import : PackageDesc->Imports) { if (!Import.GlobalImportIndex.IsNull()) { if (Import.GlobalImportIndex.IsPackageImport()) { FPublicExportKey Key = FPublicExportKey::FromPackageImport(Import.GlobalImportIndex, PackageDesc->ImportedPackageIds, PackageDesc->ImportedPublicExportHashes); Import.Export = ExportByKeyMap.FindRef(Key); if (!Import.Export) { UE_LOG(LogIoStore, Warning, TEXT("Missing import: 0x%llX in package 0x%s '%s'"), Import.GlobalImportIndex.Value(), *LexToString(PackageDesc->PackageId), *PackageDesc->PackageName.ToString()); } else { Import.Name = Import.Export->FullName; } } else { FScriptObjectDesc* ScriptObjectDesc = ScriptObjectByGlobalIdMap.Find(Import.GlobalImportIndex); if (ScriptObjectDesc) { Import.Name = ScriptObjectDesc->FullName; } else { UE_LOG(LogIoStore, Warning, TEXT("Missing Script Object for Import: 0x%llX in package 0x%s '%s'"), Import.GlobalImportIndex.Value(), *LexToString(PackageDesc->PackageId), *PackageDesc->PackageName.ToString()); } } } } } } return { FContainerPackageInfo{ MoveTemp(Containers), MoveTemp(Packages), MoveTemp(ScriptObjectByGlobalIdMap), MoveTemp(ExportByKeyMap), } }; } } int32 Describe( const FString& GlobalContainerPath, const FKeyChain& KeyChain, const FString& PackageFilter, const FString& OutPath, bool bIncludeExportHashes) { using namespace DescribeUtils; TOptional MaybeInfo = DescribeUtils::TryGetContainerPackageInfo(GlobalContainerPath, KeyChain, bIncludeExportHashes); if (!MaybeInfo.IsSet()) { return -1; } FContainerPackageInfo& Info = MaybeInfo.GetValue(); const TArray& Containers = Info.Containers; const TArray& Packages = Info.Packages; const TMap& ScriptObjectByGlobalIdMap = Info.ScriptObjectByGlobalIdMap; const TMap& ExportByKeyMap = Info.ExportByKeyMap; UE_LOG(LogIoStore, Display, TEXT("Collecting output packages...")); TArray OutputPackages; TSet RelevantPackages; TSet RelevantContainers; { TRACE_CPUPROFILER_EVENT_SCOPE(CollectOutputPackages); if (PackageFilter.IsEmpty()) { OutputPackages.Append(Packages); } else { TArray SplitPackageFilters; const TCHAR* Delimiters[] = { TEXT(","), TEXT(" ") }; PackageFilter.ParseIntoArray(SplitPackageFilters, Delimiters, UE_ARRAY_COUNT(Delimiters), true); TArray PackageNameFilter; TSet PackageIdFilter; for (const FString& PackageNameOrId : SplitPackageFilters) { if (PackageNameOrId.Len() > 0 && FChar::IsDigit(PackageNameOrId[0])) { uint64 Value; LexFromString(Value, *PackageNameOrId); PackageIdFilter.Add(*(FPackageId*)(&Value)); } else { PackageNameFilter.Add(PackageNameOrId); } } TArray PackageStack; for (const FPackageDesc* PackageDesc : Packages) { bool bInclude = false; if (PackageIdFilter.Contains(PackageDesc->PackageId)) { bInclude = true; } else { FString PackageName = PackageDesc->PackageName.ToString(); for (const FString& Wildcard : PackageNameFilter) { if (PackageName.MatchesWildcard(Wildcard)) { bInclude = true; break; } } } if (bInclude) { PackageStack.Push(PackageDesc); } } TSet Visited; while (PackageStack.Num() > 0) { const FPackageDesc* PackageDesc = PackageStack.Pop(); if (!Visited.Contains(PackageDesc)) { Visited.Add(PackageDesc); OutputPackages.Add(PackageDesc); RelevantPackages.Add(PackageDesc->PackageId); for (const FPackageLocation& Location : PackageDesc->Locations) { RelevantContainers.Add(Location.Container); } for (const FImportDesc& Import : PackageDesc->Imports) { if (Import.Export && Import.Export->Package) { PackageStack.Push(Import.Export->Package); } } } } } } UE_LOG(LogIoStore, Display, TEXT("Generating report...")); FOutputDevice* OutputOverride = GWarn; FString OutputFilename; TUniquePtr OutputBuffer; if (!OutPath.IsEmpty()) { OutputBuffer = MakeUnique(*OutPath, true); OutputBuffer->SetSuppressEventTag(true); OutputOverride = OutputBuffer.Get(); } { TRACE_CPUPROFILER_EVENT_SCOPE(GenerateReport); TGuardValue GuardPrintLogTimes(GPrintLogTimes, ELogTimes::None); TGuardValue GuardPrintLogCategory(GPrintLogCategory, false); TGuardValue GuardPrintLogVerbosity(GPrintLogVerbosity, false); auto PackageObjectIndexToString = [&ScriptObjectByGlobalIdMap, &ExportByKeyMap](const FPackageDesc* Package, const FPackageObjectIndex& PackageObjectIndex, bool bIncludeName) -> FString { if (PackageObjectIndex.IsNull()) { return TEXT(""); } else if (PackageObjectIndex.IsPackageImport()) { FPublicExportKey Key = FPublicExportKey::FromPackageImport(PackageObjectIndex, Package->ImportedPackageIds, Package->ImportedPublicExportHashes); FExportDesc* ExportDesc = ExportByKeyMap.FindRef(Key); if (ExportDesc && bIncludeName) { return FString::Printf(TEXT("0x%llX '%s'"), PackageObjectIndex.Value(), *ExportDesc->FullName.ToString()); } else { return FString::Printf(TEXT("0x%llX"), PackageObjectIndex.Value()); } } else if (PackageObjectIndex.IsScriptImport()) { const FScriptObjectDesc* ScriptObjectDesc = ScriptObjectByGlobalIdMap.Find(PackageObjectIndex); if (ScriptObjectDesc && bIncludeName) { return FString::Printf(TEXT("0x%llX '%s'"), PackageObjectIndex.Value(), *ScriptObjectDesc->FullName.ToString()); } else { return FString::Printf(TEXT("0x%llX"), PackageObjectIndex.Value()); } } else if (PackageObjectIndex.IsExport()) { return FString::Printf(TEXT("%d"), PackageObjectIndex.Value()); } else { return FString::Printf(TEXT("0x%llX"), PackageObjectIndex.Value()); } }; for (const FContainerDesc* ContainerDesc : Containers) { if (RelevantContainers.Num() > 0 && !RelevantContainers.Contains(ContainerDesc)) { continue; } OutputOverride->Logf(ELogVerbosity::Display, TEXT("********************************************")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("Container '%s' Summary"), *ContainerDesc->Name.ToString()); OutputOverride->Logf(ELogVerbosity::Display, TEXT("--------------------------------------------")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t ContainerId: 0x%llX"), ContainerDesc->Id.Value()); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t Compressed: %s"), ContainerDesc->bCompressed ? TEXT("Yes") : TEXT("No")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t Signed: %s"), ContainerDesc->bSigned ? TEXT("Yes") : TEXT("No")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t Indexed: %s"), ContainerDesc->bIndexed ? TEXT("Yes") : TEXT("No")); if (ContainerDesc->bEncrypted) { OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\tEncryptionKeyGuid: %s"), *ContainerDesc->EncryptionKeyGuid.ToString()); } if (ContainerDesc->LocalizedPackages.Num()) { bool bNeedsHeader = true; for (const FPackageDesc* LocalizedPackage : ContainerDesc->LocalizedPackages) { if (RelevantPackages.Num() > 0 && !RelevantPackages.Contains(LocalizedPackage->PackageId)) { continue; } if (bNeedsHeader) { OutputOverride->Logf(ELogVerbosity::Display, TEXT("--------------------------------------------")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("Localized Packages")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("==========")); bNeedsHeader = false; } OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t*************************")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t Source: 0x%s '%s'"), *LexToString(LocalizedPackage->PackageId), *LocalizedPackage->PackageName.ToString()); } } if (ContainerDesc->PackageRedirects.Num()) { bool bNeedsHeader = true; for (const FPackageRedirect& Redirect : ContainerDesc->PackageRedirects) { if (RelevantPackages.Num() > 0 && !RelevantPackages.Contains(Redirect.Source->PackageId) && !RelevantPackages.Contains(Redirect.Target->PackageId)) { continue; } if (bNeedsHeader) { OutputOverride->Logf(ELogVerbosity::Display, TEXT("--------------------------------------------")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("Package Redirects")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("==========")); bNeedsHeader = false; } OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t*************************")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t Source: 0x%s '%s'"), *LexToString(Redirect.Source->PackageId), *Redirect.Source->PackageName.ToString()); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t Target: 0x%s '%s'"), *LexToString(Redirect.Target->PackageId), *Redirect.Target->PackageName.ToString()); } } } for (const FPackageDesc* PackageDesc : OutputPackages) { OutputOverride->Logf(ELogVerbosity::Display, TEXT("********************************************")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("Package '%s' Summary"), *PackageDesc->PackageName.ToString()); OutputOverride->Logf(ELogVerbosity::Display, TEXT("--------------------------------------------")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t PackageId: 0x%s"), *LexToString(PackageDesc->PackageId)); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t PackageFlags: %X"), PackageDesc->PackageFlags); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t NameCount: %d"), PackageDesc->NameCount); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t ImportCount: %d"), PackageDesc->Imports.Num()); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t ExportCount: %d"), PackageDesc->Exports.Num()); OutputOverride->Logf(ELogVerbosity::Display, TEXT("--------------------------------------------")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("Locations")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("==========")); int32 Index = 0; for (const FPackageLocation& Location : PackageDesc->Locations) { OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t*************************")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\tLocation %d: '%s'"), Index++, *Location.Container->Name.ToString()); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t Offset: %lld"), Location.Offset); } OutputOverride->Logf(ELogVerbosity::Display, TEXT("--------------------------------------------")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("Imports")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("==========")); Index = 0; for (const FImportDesc& Import : PackageDesc->Imports) { OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t*************************")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\tImport %d: '%s'"), Index++, *Import.Name.ToString()); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\tGlobalImportIndex: %s"), *PackageObjectIndexToString(PackageDesc, Import.GlobalImportIndex, false)); } OutputOverride->Logf(ELogVerbosity::Display, TEXT("--------------------------------------------")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("Exports")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("==========")); Index = 0; for (const FExportDesc& Export : PackageDesc->Exports) { OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t*************************")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\tExport %d: '%s'"), Index++, *Export.Name.ToString()); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t OuterIndex: %s"), *PackageObjectIndexToString(PackageDesc, Export.OuterIndex, true)); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t ClassIndex: %s"), *PackageObjectIndexToString(PackageDesc, Export.ClassIndex, true)); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t SuperIndex: %s"), *PackageObjectIndexToString(PackageDesc, Export.SuperIndex, true)); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t TemplateIndex: %s"), *PackageObjectIndexToString(PackageDesc, Export.TemplateIndex, true)); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t PublicExportHash: %llu"), Export.PublicExportHash); if (bIncludeExportHashes) { OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t ExportHash: %s"), *Export.ExportHash.ToString()); } OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t Offset: %lld"), Export.SerialOffset); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t Size: %lld"), Export.SerialSize); } OutputOverride->Logf(ELogVerbosity::Display, TEXT("--------------------------------------------")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("Export Bundle")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("==========")); for (const FExportBundleEntryDesc& ExportBundleEntry : PackageDesc->ExportBundleEntries) { if (ExportBundleEntry.CommandType == FExportBundleEntry::ExportCommandType_Create) { OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t Create: %d '%s'"), ExportBundleEntry.LocalExportIndex, *ExportBundleEntry.Export->Name.ToString()); } else { OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t\t Serialize: %d '%s'"), ExportBundleEntry.LocalExportIndex, *ExportBundleEntry.Export->Name.ToString()); } } } } return 0; } int32 ValidateCrossContainerRefs( const FString& GlobalContainerPath, const FKeyChain& KeyChain, const FString& ConfigPath, const FString& OutPath ) { TMultiMap ValidEdges; TArray IgnoreRefsFromAssets, IgnoreRefsToAssets; FConfigFile ConfigFile; if (!FConfigCacheIni::LoadLocalIniFile(ConfigFile, *ConfigPath, false)) { UE_LOG(LogIoStore, Error, TEXT("Failed to load config file %s"), *ConfigPath); return -1; } if (const FConfigSection* EdgesSection = ConfigFile.FindSection(TEXT("Edges"))) { for (auto It = EdgesSection->CreateConstIterator(); It; ++It) { ValidEdges.Add(It.Key().ToString(), It.Value().GetValue()); } } if (const FConfigSection* DefaultEdgesSection = ConfigFile.FindSection(TEXT("DefaultEdges"))) { for (auto It = DefaultEdgesSection->CreateConstIterator(); It; ++It) { ValidEdges.Add(FString(), It.Key().ToString()); } } if (const FConfigSection* IgnoreSection = ConfigFile.FindSection(TEXT("Ignore"))) { IgnoreSection->MultiFind(TEXT("IgnoreRefsFrom"), IgnoreRefsFromAssets); IgnoreSection->MultiFind(TEXT("IgnoreRefsTo"), IgnoreRefsToAssets); } if (ValidEdges.Num() == 0) { UE_LOG(LogIoStore, Error, TEXT("No valid edges configured, nothing to validate")); return -1; } if (ValidEdges.FindPair(TEXT(""), TEXT(""))) { UE_LOG(LogIoStore, Error, TEXT("Configuration contains all-to-all edge (empty string to empty string), nothing to validate")); return -1; } using namespace DescribeUtils; TOptional MaybeInfo = DescribeUtils::TryGetContainerPackageInfo(GlobalContainerPath, KeyChain, false); if (!MaybeInfo.IsSet()) { return -1; } FContainerPackageInfo& Info = MaybeInfo.GetValue(); const TArray& Containers = Info.Containers; TArray& Packages = Info.Packages; const TMap& ScriptObjectByGlobalIdMap = Info.ScriptObjectByGlobalIdMap; const TMap& ExportByKeyMap = Info.ExportByKeyMap; // Expand container prefixes from config into full container names and produce transitive closure TMultiMap FinalValidEdges; { TMultiMap ShortNameToContainer; for (auto It = ValidEdges.CreateIterator(); It; ++It) { for (const FContainerDesc* Desc : Containers) { // Empty strings mean 'all containers' if (It.Key().Len() == 0 || Desc->Name.ToString().StartsWith(It.Key())) { ShortNameToContainer.AddUnique(It.Key(), Desc); } if (It.Value().Len() == 0 || Desc->Name.ToString().StartsWith(It.Value())) { ShortNameToContainer.AddUnique(It.Value(), Desc); } } } TMultiMap ValidDirectEdges; for (const TPair& Pair : ValidEdges) { if (Pair.Key.Len() == 0) { // Do 'all containers' later after handling explicit containers continue; } for (auto FromIt = ShortNameToContainer.CreateKeyIterator(Pair.Key); FromIt; ++FromIt) { for (auto ToIt = ShortNameToContainer.CreateKeyIterator(Pair.Value); ToIt; ++ToIt) { ValidDirectEdges.AddUnique(FromIt.Value(), ToIt.Value()); } } } TSet UnassignedFromContainers; for (const FContainerDesc* Container : Containers) { if (!ValidDirectEdges.Contains(Container)) { UnassignedFromContainers.Add(Container); } } for (const TPair& Pair : ValidEdges) { if (Pair.Key.Len() == 0) { for (auto FromIt = ShortNameToContainer.CreateKeyIterator(Pair.Key); FromIt; ++FromIt) { if (!UnassignedFromContainers.Contains(FromIt.Value())) { continue; } for (auto ToIt = ShortNameToContainer.CreateKeyIterator(Pair.Value); ToIt; ++ToIt) { ValidDirectEdges.Add(FromIt.Value(), ToIt.Value()); } } } } // Create a transitive closure (i.e. if it is valid for packages in container A to import packages in B, and B to import packages in C, then it is valid for packages in A to import packages in C) for (const FContainerDesc* StartContainer : Containers) { TSet SeenContainers; TArray Queue; Queue.Add(StartContainer); SeenContainers.Add(StartContainer); while (Queue.Num() > 0) { const FContainerDesc* ToContainer = Queue.Pop(); FinalValidEdges.Add(StartContainer, ToContainer); for (auto ToIt = ValidDirectEdges.CreateKeyIterator(ToContainer); ToIt; ++ToIt) { if (!SeenContainers.Contains(ToIt.Value())) { SeenContainers.Add(ToIt.Value()); Queue.Add(ToIt.Value()); } } } } } TMap>> Errors; Algo::SortBy(Packages, [](FPackageDesc* Desc) { return Desc->PackageName; }, FNameLexicalLess()); for (const FPackageDesc* Package : Packages) { bool bSkip = Algo::AnyOf(IgnoreRefsFromAssets, [Package](const FString& IgnoreString) { FString PackageNameString = Package->PackageName.ToString(); if (PackageNameString == IgnoreString) { return true; } else if (FWildcardString::ContainsWildcards(*IgnoreString) && FWildcardString::IsMatch(*IgnoreString, *PackageNameString)) { return true; } return false; }); if (bSkip) { continue; } bool bNeedsHeader = true; for (const FImportDesc& Import : Package->Imports) { FPackageDesc* ImportPackage = Import.Export ? Import.Export->Package : nullptr; if (!ImportPackage) { UE_CLOG(Import.Name != FName() && !FPackageName::IsScriptPackage(*Import.Name.ToString()), LogIoStore, Error, TEXT("Unresolved import of package %s by package %s"), *Import.Name.ToString(), *Package->PackageName.ToString()); continue; } bSkip = Algo::AnyOf(IgnoreRefsFromAssets, [ImportPackage](const FString& IgnoreString) { FString PackageNameString = ImportPackage->PackageName.ToString(); if (PackageNameString == IgnoreString) { return true; } else if (FWildcardString::ContainsWildcards(*IgnoreString) && FWildcardString::IsMatch(*IgnoreString, *PackageNameString)) { return true; } return false; }); if (bSkip) { continue; } // For each location of the importing paacka for (const DescribeUtils::FPackageLocation& Location : Package->Locations) { bool bValid = Algo::AnyOf(ImportPackage->Locations, [&](const DescribeUtils::FPackageLocation& ImportLocation) { return FinalValidEdges.FindPair(Location.Container, ImportLocation.Container) != nullptr; }); if (!bValid) { Errors.FindOrAdd(Location.Container).Add({ Package, ImportPackage }); } } } } FOutputDevice* OutputOverride = GWarn; FString OutputFilename; TUniquePtr OutputBuffer; if (!OutPath.IsEmpty()) { OutputBuffer = MakeUnique(*OutPath, true); OutputBuffer->SetSuppressEventTag(true); OutputOverride = OutputBuffer.Get(); } OutputOverride->Logf(ELogVerbosity::Display, TEXT("Invalid cross-container reference report")); OutputOverride->Logf(ELogVerbosity::Display, TEXT("Final valid edges: %d"), FinalValidEdges.Num()); for (const TPair& Pair : FinalValidEdges) { OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t%s -> %s"), *Pair.Key->Name.ToString(), *Pair.Value->Name.ToString()); } if (Errors.Num() == 0) { OutputOverride->Logf(ELogVerbosity::Display, TEXT("No errors.")); return 0; } for (const TPair>>& Pair : Errors) { OutputOverride->Logf(ELogVerbosity::Display, TEXT("%s"), *Pair.Key->Name.ToString()); for (const TTuple& Error : Pair.Value) { FString LocationsString = FString::JoinBy(Error.Value->Locations, TEXT(","), [](const DescribeUtils::FPackageLocation& Location) { return Location.Container->Name.ToString(); }); OutputOverride->Logf(ELogVerbosity::Display, TEXT("\t%s -> %s (%s)"), *Error.Key->PackageName.ToString(), *Error.Value->PackageName.ToString(), *LocationsString); } } return 0; } enum class EChunkTypeFilter { None, PackageData, BulkData }; FString LexToString(EChunkTypeFilter Filter) { switch(Filter) { case EChunkTypeFilter::PackageData: return TEXT("PackageData"); case EChunkTypeFilter::BulkData: return TEXT("BulkData"); default: return TEXT("None"); } } static int32 Diff( const FString& SourcePath, const FKeyChain& SourceKeyChain, const FString& TargetPath, const FKeyChain& TargetKeyChain, const FString& OutPath, EChunkTypeFilter ChunkTypeFilter) { struct FContainerChunkInfo { FString ContainerName; TMap ChunkInfoById; int64 UncompressedContainerSize = 0; int64 CompressedContainerSize = 0; }; struct FContainerDiff { TSet Unmodified; TSet Modified; TSet Added; TSet Removed; int64 UnmodifiedCompressedSize = 0; int64 ModifiedCompressedSize = 0; int64 AddedCompressedSize = 0; int64 RemovedCompressedSize = 0; }; using FContainers = TMap; auto ReadContainers = [ChunkTypeFilter](const FString& Directory, const FKeyChain& KeyChain, FContainers& OutContainers) { TArray ContainerFileNames; IFileManager::Get().FindFiles(ContainerFileNames, *(Directory / TEXT("*.utoc")), true, false); for (const FString& ContainerFileName : ContainerFileNames) { FString ContainerFilePath = Directory / ContainerFileName; UE_LOG(LogIoStore, Display, TEXT("Reading container '%s'"), *ContainerFilePath); TUniquePtr Reader = CreateIoStoreReader(*ContainerFilePath, KeyChain); if (!Reader.IsValid()) { UE_LOG(LogIoStore, Warning, TEXT("Failed to read container '%s'"), *ContainerFilePath); continue; } FString ContainerName = FPaths::GetBaseFilename(ContainerFileName); FContainerChunkInfo& ContainerChunkInfo = OutContainers.FindOrAdd(ContainerName); ContainerChunkInfo.ContainerName = MoveTemp(ContainerName); Reader->EnumerateChunks([&ContainerChunkInfo, ChunkTypeFilter](const FIoStoreTocChunkInfo& ChunkInfo) { const EIoChunkType ChunkType = ChunkInfo.Id.GetChunkType(); const bool bCompareChunk = ChunkTypeFilter == EChunkTypeFilter::None || (ChunkTypeFilter == EChunkTypeFilter::PackageData && ChunkType == EIoChunkType::ExportBundleData) || (ChunkTypeFilter == EChunkTypeFilter::BulkData && (ChunkType == EIoChunkType::BulkData || ChunkType == EIoChunkType::OptionalBulkData || ChunkType == EIoChunkType::MemoryMappedBulkData)); if (bCompareChunk) { ContainerChunkInfo.ChunkInfoById.Add(ChunkInfo.Id, ChunkInfo); ContainerChunkInfo.UncompressedContainerSize += ChunkInfo.Size; ContainerChunkInfo.CompressedContainerSize += ChunkInfo.CompressedSize; } return true; }); } }; auto ComputeDiff = [](const FContainerChunkInfo& SourceContainer, const FContainerChunkInfo& TargetContainer) -> FContainerDiff { check(SourceContainer.ContainerName == TargetContainer.ContainerName); FContainerDiff ContainerDiff; for (const auto& TargetChunkInfo : TargetContainer.ChunkInfoById) { if (const FIoStoreTocChunkInfo* SourceChunkInfo = SourceContainer.ChunkInfoById.Find(TargetChunkInfo.Key)) { if (SourceChunkInfo->ChunkHash != TargetChunkInfo.Value.ChunkHash) { ContainerDiff.Modified.Add(TargetChunkInfo.Key); ContainerDiff.ModifiedCompressedSize += TargetChunkInfo.Value.CompressedSize; } else { ContainerDiff.Unmodified.Add(TargetChunkInfo.Key); ContainerDiff.UnmodifiedCompressedSize += TargetChunkInfo.Value.CompressedSize; } } else { ContainerDiff.Added.Add(TargetChunkInfo.Key); ContainerDiff.AddedCompressedSize += TargetChunkInfo.Value.CompressedSize; } } for (const auto& SourceChunkInfo : SourceContainer.ChunkInfoById) { if (!TargetContainer.ChunkInfoById.Contains(SourceChunkInfo.Key)) { ContainerDiff.Removed.Add(SourceChunkInfo.Key); ContainerDiff.RemovedCompressedSize += SourceChunkInfo.Value.CompressedSize; } } return MoveTemp(ContainerDiff); }; FOutputDevice* OutputDevice = GWarn; TUniquePtr FileOutputDevice; if (!OutPath.IsEmpty()) { UE_LOG(LogIoStore, Error, TEXT("Redirecting output to: '%s'"), *OutPath); FileOutputDevice = MakeUnique(*OutPath, true); FileOutputDevice->SetSuppressEventTag(true); OutputDevice = FileOutputDevice.Get(); } FContainers SourceContainers, TargetContainers; TArray AddedContainers, ModifiedContainers, RemovedContainers; TArray ContainerDiffs; UE_LOG(LogIoStore, Display, TEXT("Reading source container(s) from '%s':"), *SourcePath); ReadContainers(SourcePath, SourceKeyChain, SourceContainers); if (!SourceContainers.Num()) { UE_LOG(LogIoStore, Error, TEXT("Failed to read source container(s) from '%s':"), *SourcePath); return -1; } UE_LOG(LogIoStore, Display, TEXT("Reading target container(s) from '%s':"), *TargetPath); ReadContainers(TargetPath, TargetKeyChain, TargetContainers); if (!TargetContainers.Num()) { UE_LOG(LogIoStore, Error, TEXT("Failed to read target container(s) from '%s':"), *SourcePath); return -1; } for (const auto& TargetContainer : TargetContainers) { if (SourceContainers.Contains(TargetContainer.Key)) { ModifiedContainers.Add(TargetContainer.Key); } else { AddedContainers.Add(TargetContainer.Key); } } for (const auto& SourceContainer : SourceContainers) { if (!TargetContainers.Contains(SourceContainer.Key)) { RemovedContainers.Add(SourceContainer.Key); } } for (const FString& ModifiedContainer : ModifiedContainers) { ContainerDiffs.Emplace(ComputeDiff(*SourceContainers.Find(ModifiedContainer), *TargetContainers.Find(ModifiedContainer))); } OutputDevice->Logf(ELogVerbosity::Display, TEXT("")); OutputDevice->Logf(ELogVerbosity::Display, TEXT("------------------------------ Container Diff Summary ------------------------------")); OutputDevice->Logf(ELogVerbosity::Display, TEXT("Source path '%s'"), *SourcePath); OutputDevice->Logf(ELogVerbosity::Display, TEXT("Target path '%s'"), *TargetPath); OutputDevice->Logf(ELogVerbosity::Display, TEXT("Chunk type filter '%s'"), *LexToString(ChunkTypeFilter)); OutputDevice->Logf(ELogVerbosity::Display, TEXT("")); OutputDevice->Logf(ELogVerbosity::Display, TEXT("Source container file(s):")); OutputDevice->Logf(ELogVerbosity::Display, TEXT("")); OutputDevice->Logf(ELogVerbosity::Display, TEXT("%-40s %15s %15s"), TEXT("Container"), TEXT("Size (MB)"), TEXT("Chunks")); OutputDevice->Logf(ELogVerbosity::Display, TEXT("-------------------------------------------------------------------------")); { uint64 TotalSourceBytes = 0; uint64 TotalSourceChunks = 0; for (const auto& NameContainerPair : SourceContainers) { const FContainerChunkInfo& SourceContainer = NameContainerPair.Value; OutputDevice->Logf(ELogVerbosity::Display, TEXT("%-40s %15.2lf %15d"), *SourceContainer.ContainerName, double(SourceContainer.CompressedContainerSize) / 1024.0 / 1024.0, SourceContainer.ChunkInfoById.Num()); TotalSourceBytes += SourceContainer.CompressedContainerSize; TotalSourceChunks += SourceContainer.ChunkInfoById.Num(); } OutputDevice->Logf(ELogVerbosity::Display, TEXT("-------------------------------------------------------------------------")); OutputDevice->Logf(ELogVerbosity::Display, TEXT("%-40s %15.2lf %15d"), *FString::Printf(TEXT("Total of %d container file(s)"), SourceContainers.Num()), double(TotalSourceBytes) / 1024.0 / 1024.0, TotalSourceChunks); } { uint64 TotalTargetBytes = 0; uint64 TotalTargetChunks = 0; uint64 TotalUnmodifiedChunks = 0; uint64 TotalUnmodifiedCompressedBytes = 0; uint64 TotalModifiedChunks = 0; uint64 TotalModifiedCompressedBytes = 0; uint64 TotalAddedChunks = 0; uint64 TotalAddedCompressedBytes = 0; uint64 TotalRemovedChunks = 0; uint64 TotalRemovedCompressedBytes = 0; if (ModifiedContainers.Num()) { OutputDevice->Logf(ELogVerbosity::Display, TEXT("")); OutputDevice->Logf(ELogVerbosity::Display, TEXT("Target container file(s):")); OutputDevice->Logf(ELogVerbosity::Display, TEXT("")); OutputDevice->Logf(ELogVerbosity::Display, TEXT("%-40s %15s %15s %25s %25s %25s %25s %25s %25s %25s %25s"), TEXT("Container"), TEXT("Size (MB)"), TEXT("Chunks"), TEXT("Unmodified"), TEXT("Unmodified (MB)"), TEXT("Modified"), TEXT("Modified (MB)"), TEXT("Added"), TEXT("Added (MB)"), TEXT("Removed"), TEXT("Removed (MB)")); OutputDevice->Logf(ELogVerbosity::Display, TEXT("----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------")); for (int32 Idx = 0; Idx < ModifiedContainers.Num(); Idx++) { const FContainerChunkInfo& SourceContainer = *SourceContainers.Find(ModifiedContainers[Idx]); const FContainerChunkInfo& TargetContainer = *TargetContainers.Find(ModifiedContainers[Idx]); const FContainerDiff& Diff = ContainerDiffs[Idx]; const int32 NumChunks = TargetContainer.ChunkInfoById.Num(); const int32 NumSourceChunks = SourceContainer.ChunkInfoById.Num(); OutputDevice->Logf(ELogVerbosity::Display, TEXT("%-40s %15s %15d %25s %25s %25s %25s %25s %25s %25s %25s"), *TargetContainer.ContainerName, *FString::Printf(TEXT("%.2lf"), double(TargetContainer.CompressedContainerSize) / 1024.0 / 1024.0), NumChunks, *FString::Printf(TEXT("%d (%.2lf%%)"), Diff.Unmodified.Num(), 100.0 * (double(Diff.Unmodified.Num()) / double(NumChunks))), *FString::Printf(TEXT("%.2lf (%.2lf%%)"), double(Diff.UnmodifiedCompressedSize) / 1024.0 / 1024.0, 100.0 * (Diff.UnmodifiedCompressedSize) / double(TargetContainer.CompressedContainerSize)), *FString::Printf(TEXT("%d (%.2lf%%)"), Diff.Modified.Num(), 100.0 * (double(Diff.Modified.Num()) / double(NumChunks))), *FString::Printf(TEXT("%.2lf (%.2lf%%)"), double(Diff.ModifiedCompressedSize) / 1024.0 / 1024.0, 100.0 * (Diff.ModifiedCompressedSize) / double(TargetContainer.CompressedContainerSize)), *FString::Printf(TEXT("%d (%.2lf%%)"), Diff.Added.Num(), 100.0 * (double(Diff.Added.Num()) / double(NumChunks))), *FString::Printf(TEXT("%.2lf (%.2lf%%)"), double(Diff.AddedCompressedSize) / 1024.0 / 1024.0, 100.0 * (Diff.AddedCompressedSize) / double(TargetContainer.CompressedContainerSize)), *FString::Printf(TEXT("%d/%d (%.2lf%%)"), Diff.Removed.Num(), NumSourceChunks, 100.0 * (double(Diff.Removed.Num()) / double(NumSourceChunks))), *FString::Printf(TEXT("%.2lf (%.2lf%%)"), double(Diff.RemovedCompressedSize) / 1024.0 / 1024.0, 100.0 * (Diff.RemovedCompressedSize) / double(SourceContainer.CompressedContainerSize))); TotalTargetBytes += TargetContainer.CompressedContainerSize; TotalTargetChunks += NumChunks; TotalUnmodifiedChunks += Diff.Unmodified.Num(); TotalUnmodifiedCompressedBytes += Diff.UnmodifiedCompressedSize; TotalModifiedChunks += Diff.Modified.Num(); TotalModifiedCompressedBytes += Diff.ModifiedCompressedSize; TotalAddedChunks += Diff.Added.Num(); TotalAddedCompressedBytes += Diff.AddedCompressedSize; TotalRemovedChunks += Diff.Removed.Num(); TotalRemovedCompressedBytes += Diff.RemovedCompressedSize; } } if (AddedContainers.Num()) { for (const FString& AddedContainer : AddedContainers) { const FContainerChunkInfo& TargetContainer = *TargetContainers.Find(AddedContainer); OutputDevice->Logf(ELogVerbosity::Display, TEXT("+%-39s %15.2lf %15d %25s %25s %25s %25s %25s %25s %25s %25s"), *TargetContainer.ContainerName, double(TargetContainer.CompressedContainerSize) / 1024.0 / 1024.0, TargetContainer.ChunkInfoById.Num(), TEXT("-"), TEXT("-"), TEXT("-"), TEXT("-"), TEXT("-"), TEXT("-"), TEXT("-"), TEXT("-")); TotalTargetBytes += TargetContainer.CompressedContainerSize; TotalTargetChunks += TargetContainer.ChunkInfoById.Num(); } } OutputDevice->Logf(ELogVerbosity::Display, TEXT("----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------")); OutputDevice->Logf(ELogVerbosity::Display, TEXT("%-40s %15.2lf %15d %25d %25.2f %25d %25.2f %25d %25.2f %25d %25.2f"), *FString::Printf(TEXT("Total of %d container file(s)"), TargetContainers.Num()), double(TotalTargetBytes) / 1024.0 / 1024.0, TotalTargetChunks, TotalUnmodifiedChunks, double(TotalUnmodifiedCompressedBytes) / 1024.0 / 1024.0, TotalModifiedChunks, double(TotalModifiedCompressedBytes) / 1024.0 / 1024.0, TotalAddedChunks, double(TotalAddedCompressedBytes) / 1024.0 / 1024.0, TotalRemovedChunks, double(TotalRemovedCompressedBytes) / 1024.0 / 1024.0); } return 0; } bool DiffIoStoreContainer(const TCHAR* CmdLine) { FString SourcePath, TargetPath, OutPath; FKeyChain SourceKeyChain, TargetKeyChain; if (!FParse::Value(CmdLine, TEXT("DiffContainer="), SourcePath)) { UE_LOG(LogIoStore, Error, TEXT("Incorrect arguments. Expected: -DiffContainer= -Target=")); return false; } if (!IFileManager::Get().DirectoryExists(*SourcePath)) { UE_LOG(LogIoStore, Error, TEXT("Source directory '%s' doesn't exist"), *SourcePath); return false; } if (!FParse::Value(CmdLine, TEXT("Target="), TargetPath)) { UE_LOG(LogIoStore, Error, TEXT("Incorrect arguments. Expected: -DiffContainer= -Target=")); } if (!IFileManager::Get().DirectoryExists(*TargetPath)) { UE_LOG(LogIoStore, Error, TEXT("Target directory '%s' doesn't exist"), *TargetPath); return false; } FParse::Value(CmdLine, TEXT("DumpToFile="), OutPath); FString CryptoKeysCacheFilename; if (FParse::Value(CmdLine, TEXT("CryptoKeys="), CryptoKeysCacheFilename)) { UE_LOG(LogIoStore, Display, TEXT("Parsing source crypto keys from '%s'"), *CryptoKeysCacheFilename); KeyChainUtilities::LoadKeyChainFromFile(CryptoKeysCacheFilename, SourceKeyChain); } if (FParse::Value(CmdLine, TEXT("TargetCryptoKeys="), CryptoKeysCacheFilename)) { UE_LOG(LogIoStore, Display, TEXT("Parsing target crypto keys from '%s'"), *CryptoKeysCacheFilename); KeyChainUtilities::LoadKeyChainFromFile(CryptoKeysCacheFilename, TargetKeyChain); } else { TargetKeyChain = SourceKeyChain; } EChunkTypeFilter ChunkTypeFilter = EChunkTypeFilter::None; if (FParse::Param(CmdLine, TEXT("FilterBulkData"))) { ChunkTypeFilter = EChunkTypeFilter::BulkData; } else if (FParse::Param(FCommandLine::Get(), TEXT("FilterPackageData"))) { ChunkTypeFilter = EChunkTypeFilter::PackageData; } return Diff(SourcePath, SourceKeyChain, TargetPath, TargetKeyChain, OutPath, ChunkTypeFilter) == 0; } bool LegacyDiffIoStoreContainers(const TCHAR* InContainerFilename1, const TCHAR* InContainerFilename2, bool bInLogUniques1, bool bInLogUniques2, const FKeyChain& InKeyChain1, const FKeyChain* InKeyChain2) { TGuardValue DisableLogTimes(GPrintLogTimes, ELogTimes::None); UE_LOG(LogIoStore, Display, TEXT("FileEventType, FileName, Size1, Size2")); TUniquePtr Reader1 = CreateIoStoreReader(InContainerFilename1, InKeyChain1); if (!Reader1.IsValid()) { return false; } if (!EnumHasAnyFlags(Reader1->GetContainerFlags(), EIoContainerFlags::Indexed)) { UE_LOG(LogIoStore, Warning, TEXT("Missing directory index for container '%s'"), InContainerFilename1); } TUniquePtr Reader2 = CreateIoStoreReader(InContainerFilename2, InKeyChain2 ? *InKeyChain2 : InKeyChain1); if (!Reader2.IsValid()) { return false; } if (!EnumHasAnyFlags(Reader2->GetContainerFlags(), EIoContainerFlags::Indexed)) { UE_LOG(LogIoStore, Warning, TEXT("Missing directory index for container '%s'"), InContainerFilename2); } struct FEntry { FString FileName; FIoHash ChunkHash; uint64 Size; }; TMap Container1Entries; Reader1->EnumerateChunks([&Container1Entries](const FIoStoreTocChunkInfo& ChunkInfo) { FEntry& Entry = Container1Entries.Add(ChunkInfo.Id); Entry.FileName = ChunkInfo.FileName; Entry.ChunkHash = ChunkInfo.ChunkHash; Entry.Size = ChunkInfo.Size; return true; }); int32 NumDifferentContents = 0; int32 NumEqualContents = 0; int32 NumUniqueContainer1 = 0; int32 NumUniqueContainer2 = 0; Reader2->EnumerateChunks([&Container1Entries, &NumDifferentContents, &NumEqualContents, bInLogUniques2, &NumUniqueContainer2](const FIoStoreTocChunkInfo& ChunkInfo) { const FEntry* FindContainer1Entry = Container1Entries.Find(ChunkInfo.Id); if (FindContainer1Entry) { if (FindContainer1Entry->Size != ChunkInfo.Size) { UE_LOG(LogIoStore, Display, TEXT("FilesizeDifferent, %s, %llu, %llu"), *ChunkInfo.FileName, FindContainer1Entry->Size, ChunkInfo.Size); ++NumDifferentContents; } else if (FindContainer1Entry->ChunkHash != ChunkInfo.ChunkHash) { UE_LOG(LogIoStore, Display, TEXT("ContentsDifferent, %s, %llu, %llu"), *ChunkInfo.FileName, FindContainer1Entry->Size, ChunkInfo.Size); ++NumDifferentContents; } else { ++NumEqualContents; } Container1Entries.Remove(ChunkInfo.Id); } else { ++NumUniqueContainer2; if (bInLogUniques2) { UE_LOG(LogIoStore, Display, TEXT("UniqueToSecondContainer, %s, 0, %llu"), *ChunkInfo.FileName, ChunkInfo.Size); } } return true; }); for (const auto& KV : Container1Entries) { const FEntry& Entry = KV.Value; ++NumUniqueContainer1; if (bInLogUniques1) { UE_LOG(LogIoStore, Display, TEXT("UniqueToFirstContainer, %s, %llu, 0"), *Entry.FileName, Entry.Size); } } UE_LOG(LogIoStore, Display, TEXT("Comparison complete")); UE_LOG(LogIoStore, Display, TEXT("Unique to first container: %d, Unique to second container: %d, Num Different: %d, NumEqual: %d"), NumUniqueContainer1, NumUniqueContainer2, NumDifferentContents, NumEqualContents); return true; } int32 Staged2Zen(const FString& BuildPath, const FKeyChain& KeyChain, const FString& ProjectName, const ITargetPlatform* TargetPlatform) { FString PlatformName = TargetPlatform->PlatformName(); FString CookedOutputPath = FPaths::Combine(FPaths::ProjectDir(), TEXT("Saved"), TEXT("Cooked"), PlatformName); if (IFileManager::Get().DirectoryExists(*CookedOutputPath)) { UE_LOG(LogIoStore, Error, TEXT("'%s' already exists"), *CookedOutputPath); return -1; } TArray ContainerFiles; IFileManager::Get().FindFilesRecursive(ContainerFiles, *BuildPath, TEXT("*.utoc"), true, false); if (ContainerFiles.IsEmpty()) { UE_LOG(LogIoStore, Error, TEXT("No container files found")); return -1; } TArray PakFiles; IFileManager::Get().FindFilesRecursive(PakFiles, *BuildPath, TEXT("*.pak"), true, false); if (PakFiles.IsEmpty()) { UE_LOG(LogIoStore, Error, TEXT("No pak files found")); return -1; } UE_LOG(LogIoStore, Display, TEXT("Extracting files from paks...")); FPakPlatformFile PakPlatformFile; for (const auto& KV : KeyChain.GetEncryptionKeys()) { FCoreDelegates::GetRegisterEncryptionKeyMulticastDelegate().Broadcast(KV.Key, KV.Value.Key); } PakPlatformFile.Initialize(&FPlatformFileManager::Get().GetPlatformFile(), TEXT("")); FString CookedEngineContentPath = FPaths::Combine(CookedOutputPath, TEXT("Engine"), TEXT("Content")); IFileManager::Get().MakeDirectory(*CookedEngineContentPath, true); FString CookedProjectContentPath = FPaths::Combine(CookedOutputPath, ProjectName, TEXT("Content")); IFileManager::Get().MakeDirectory(*CookedProjectContentPath, true); FString EngineContentPakPath = TEXT("../../../Engine/Content/"); FString ProjectContentPakPath = FPaths::Combine(TEXT("../../.."), ProjectName, TEXT("Content")); for (const FString& PakFilePath : PakFiles) { PakPlatformFile.Mount(*PakFilePath, 0); TArray FilesInPak; PakPlatformFile.GetPrunedFilenamesInPakFile(PakFilePath, FilesInPak); for (const FString& FileInPak : FilesInPak) { FString FileName = FPaths::GetCleanFilename(FileInPak); if (FileName == TEXT("AssetRegistry.bin")) { FString TargetPath = FPaths::Combine(CookedOutputPath, ProjectName, FileName); PakPlatformFile.CopyFile(*TargetPath, *FileInPak); } else if (FileName.EndsWith(TEXT(".ushaderbytecode"))) { FString TargetPath = FPaths::Combine(CookedProjectContentPath, FileName); PakPlatformFile.CopyFile(*TargetPath, *FileInPak); } else if (FileName.StartsWith("GlobalShaderCache")) { FString TargetPath = FPaths::Combine(CookedOutputPath, TEXT("Engine"), FileName); PakPlatformFile.CopyFile(*TargetPath, *FileInPak); } else if (FileName.EndsWith(TEXT(".ufont"))) { FString TargetPath; if (FileInPak.StartsWith(EngineContentPakPath)) { TargetPath = FPaths::Combine(CookedEngineContentPath, *FileInPak + EngineContentPakPath.Len()); } else if (FileInPak.StartsWith(ProjectContentPakPath)) { TargetPath = FPaths::Combine(CookedProjectContentPath, *FileInPak + ProjectContentPakPath.Len()); } else { UE_DEBUG_BREAK(); continue; } IFileManager::Get().MakeDirectory(*FPaths::GetPath(TargetPath), true); PakPlatformFile.CopyFile(*TargetPath, *FileInPak); } } } struct FBulkDataInfo { FString FileName; IPackageWriter::FBulkDataInfo::EType BulkDataType; TTuple Chunk; }; struct FPackageInfo { FName PackageName; FString FileName; TTuple Chunk; TArray BulkData; FPackageStoreEntryResource PackageStoreEntry; }; struct FCollectedData { TSet SeenChunks; TMap Packages; TMap PackageIdToName; TArray> ContainerHeaderChunks; } CollectedData; UE_LOG(LogIoStore, Display, TEXT("Collecting chunks...")); TArray> IoStoreReaders; IoStoreReaders.Reserve(ContainerFiles.Num()); for (const FString& ContainerFilePath : ContainerFiles) { TUniquePtr Reader = CreateIoStoreReader(*ContainerFilePath, KeyChain); if (!Reader.IsValid()) { UE_LOG(LogIoStore, Warning, TEXT("Failed to read container '%s'"), *ContainerFilePath); continue; } Reader->EnumerateChunks([&Reader, &CollectedData](const FIoStoreTocChunkInfo& ChunkInfo) { if (CollectedData.SeenChunks.Contains(ChunkInfo.Id)) { return true; } CollectedData.SeenChunks.Add(ChunkInfo.Id); EIoChunkType ChunkType = static_cast(ChunkInfo.Id.GetData()[11]); if (ChunkType == EIoChunkType::ExportBundleData || ChunkType == EIoChunkType::BulkData || ChunkType == EIoChunkType::OptionalBulkData || ChunkType == EIoChunkType::MemoryMappedBulkData) { FString PackageNameStr; UE_CLOG(!ChunkInfo.bHasValidFileName, LogIoStore, Fatal, TEXT("Missing file name for package chunk")); if (FPackageName::TryConvertFilenameToLongPackageName(ChunkInfo.FileName, PackageNameStr, nullptr)) { FName PackageName(PackageNameStr); CollectedData.PackageIdToName.Add(FPackageId::FromName(PackageName), PackageName); FPackageInfo& PackageInfo = CollectedData.Packages.FindOrAdd(PackageName); if (ChunkType == EIoChunkType::ExportBundleData) { PackageInfo.FileName = ChunkInfo.FileName; PackageInfo.PackageName = PackageName; PackageInfo.Chunk = MakeTuple(Reader.Get(), ChunkInfo.Id); } else { FBulkDataInfo& BulkDataInfo = PackageInfo.BulkData.AddDefaulted_GetRef(); BulkDataInfo.FileName = ChunkInfo.FileName; BulkDataInfo.Chunk = MakeTuple(Reader.Get(), ChunkInfo.Id); if (ChunkType == EIoChunkType::OptionalBulkData) { BulkDataInfo.BulkDataType = IPackageWriter::FBulkDataInfo::Optional; } else if (ChunkType == EIoChunkType::MemoryMappedBulkData) { BulkDataInfo.BulkDataType = IPackageWriter::FBulkDataInfo::Mmap; } else { BulkDataInfo.BulkDataType = IPackageWriter::FBulkDataInfo::BulkSegment; } } } else { UE_LOG(LogIoStore, Warning, TEXT("Failed to convert file name '%s' to package name"), *ChunkInfo.FileName); } } else if (ChunkType == EIoChunkType::ContainerHeader) { CollectedData.ContainerHeaderChunks.Emplace(Reader.Get(), ChunkInfo.Id); } return true; }); IoStoreReaders.Emplace(MoveTemp(Reader)); } UE_LOG(LogIoStore, Display, TEXT("Reading container headers...")); for (const auto& ContainerHeaderChunk : CollectedData.ContainerHeaderChunks) { FIoBuffer ContainerHeaderBuffer = ContainerHeaderChunk.Key->Read(ContainerHeaderChunk.Value, FIoReadOptions()).ValueOrDie(); FMemoryReaderView Ar(MakeArrayView(ContainerHeaderBuffer.Data(), ContainerHeaderBuffer.DataSize())); FIoContainerHeader ContainerHeader; Ar << ContainerHeader; const FFilePackageStoreEntry* StoreEntry = reinterpret_cast(ContainerHeader.StoreEntries.GetData()); for (const FPackageId& PackageId : ContainerHeader.PackageIds) { const FName* FindPackageName = CollectedData.PackageIdToName.Find(PackageId); if (FindPackageName) { FPackageInfo* FindPackageInfo = CollectedData.Packages.Find(*FindPackageName); check(FindPackageInfo); FPackageStoreEntryResource& PackageStoreEntryResource = FindPackageInfo->PackageStoreEntry; PackageStoreEntryResource.PackageName = *FindPackageName; PackageStoreEntryResource.ImportedPackageIds.SetNum(StoreEntry->ImportedPackages.Num()); FMemory::Memcpy(PackageStoreEntryResource.ImportedPackageIds.GetData(), StoreEntry->ImportedPackages.Data(), sizeof(FPackageId) * StoreEntry->ImportedPackages.Num()); //-V575 } ++StoreEntry; } } FString MetaDataOutputPath = FPaths::Combine(CookedOutputPath, ProjectName, TEXT("Metadata")); TSharedRef ZenCookArtifactReader = MakeShared(CookedOutputPath, MetaDataOutputPath, TargetPlatform); TUniquePtr ZenStoreWriter = MakeUnique(CookedOutputPath, MetaDataOutputPath, TargetPlatform, ZenCookArtifactReader); ICookedPackageWriter::FCookInfo CookInfo; CookInfo.bFullBuild = true; ZenStoreWriter->Initialize(CookInfo); ZenStoreWriter->BeginCook(CookInfo); int32 LocalPackageIndex = 0; TArray PackagesArray; CollectedData.Packages.GenerateValueArray(PackagesArray); TAtomic UploadCount { 0 }; ParallelFor(PackagesArray.Num(), [&UploadCount, &PackagesArray, &ZenStoreWriter](int32 Index) { const FPackageInfo& PackageInfo = PackagesArray[Index]; IPackageWriter::FBeginPackageInfo BeginPackageInfo; BeginPackageInfo.PackageName = PackageInfo.PackageName; ZenStoreWriter->BeginPackage(BeginPackageInfo); IPackageWriter::FPackageInfo PackageStorePackageInfo; PackageStorePackageInfo.PackageName = PackageInfo.PackageName; PackageStorePackageInfo.LooseFilePath = PackageInfo.FileName; PackageStorePackageInfo.ChunkId = PackageInfo.Chunk.Value; FIoBuffer PackageDataBuffer = PackageInfo.Chunk.Key->Read(PackageInfo.Chunk.Value, FIoReadOptions()).ValueOrDie(); ZenStoreWriter->WriteIoStorePackageData(PackageStorePackageInfo, PackageDataBuffer, PackageInfo.PackageStoreEntry, TArray()); for (const FBulkDataInfo& BulkDataInfo : PackageInfo.BulkData) { IPackageWriter::FBulkDataInfo PackageStoreBulkDataInfo; PackageStoreBulkDataInfo.PackageName = PackageInfo.PackageName; PackageStoreBulkDataInfo.LooseFilePath = BulkDataInfo.FileName; PackageStoreBulkDataInfo.ChunkId = BulkDataInfo.Chunk.Value; PackageStoreBulkDataInfo.BulkDataType = BulkDataInfo.BulkDataType; FIoBuffer BulkDataBuffer = BulkDataInfo.Chunk.Key->Read(BulkDataInfo.Chunk.Value, FIoReadOptions()).ValueOrDie(); ZenStoreWriter->WriteBulkData(PackageStoreBulkDataInfo, BulkDataBuffer, TArray()); } IPackageWriter::FCommitPackageInfo CommitInfo; CommitInfo.PackageName = PackageInfo.PackageName; CommitInfo.WriteOptions = IPackageWriter::EWriteOptions::Write; CommitInfo.Status = IPackageWriter::ECommitStatus::Success; ZenStoreWriter->CommitPackage(MoveTemp(CommitInfo)); int32 LocalUploadCount = UploadCount.IncrementExchange() + 1; UE_CLOG(LocalUploadCount % 1000 == 0, LogIoStore, Display, TEXT("Uploading package %d/%d"), LocalUploadCount, PackagesArray.Num()); }, EParallelForFlags::ForceSingleThread); // Single threaded for now to limit memory usage UE_LOG(LogIoStore, Display, TEXT("Waiting for uploads to finish...")); ZenStoreWriter->EndCook(CookInfo); return 0; } int32 GenerateZenFileSystemManifest(ITargetPlatform* TargetPlatform) { FString OutputDirectory = FPaths::Combine(*FPaths::ProjectDir(), TEXT("Saved"), TEXT("Cooked"), TEXT("[Platform]")); OutputDirectory = FPaths::ConvertRelativePathToFull(OutputDirectory); FPaths::NormalizeDirectoryName(OutputDirectory); TUniquePtr LocalSandboxFile = FSandboxPlatformFile::Create(false); LocalSandboxFile->Initialize(&FPlatformFileManager::Get().GetPlatformFile(), *FString::Printf(TEXT("-sandbox=\"%s\""), *OutputDirectory)); const FString RootPathSandbox = LocalSandboxFile->ConvertToAbsolutePathForExternalAppForWrite(*FPaths::RootDir()); FString MetadataPathSandbox = LocalSandboxFile->ConvertToAbsolutePathForExternalAppForWrite(*(FPaths::ProjectDir() / TEXT("Metadata"))); const FString PlatformString = TargetPlatform->PlatformName(); const FString ResolvedRootPath = RootPathSandbox.Replace(TEXT("[Platform]"), *PlatformString); const FString ResolvedMetadataPath = MetadataPathSandbox.Replace(TEXT("[Platform]"), *PlatformString); FZenFileSystemManifest ZenFileSystemManifest(*TargetPlatform, ResolvedRootPath); ZenFileSystemManifest.Generate(); ZenFileSystemManifest.Save(*FPaths::Combine(ResolvedMetadataPath, TEXT("zenfs.manifest"))); return 0; } bool ExtractFilesWriter(const FString& SrcFileName, const FString& DestFileName, const FIoChunkId& ChunkId, const uint8* Data, uint64 DataSize) { TRACE_CPUPROFILER_EVENT_SCOPE(WriteFile); TUniquePtr FileHandle(IFileManager::Get().CreateFileWriter(*DestFileName)); if (FileHandle) { FileHandle->Serialize(const_cast(Data), DataSize); UE_CLOG(FileHandle->IsError(), LogIoStore, Error, TEXT("Failed writing to file \"%s\"."), *DestFileName); return !FileHandle->IsError(); } else { UE_LOG(LogIoStore, Error, TEXT("Unable to create file \"%s\"."), *DestFileName); return false; } }; bool ExtractFilesFromIoStoreContainer( const TCHAR* InContainerFilename, const TCHAR* InDestPath, const FKeyChain& InKeyChain, const FString* InFilter, TMap* OutOrderMap, TArray* OutUsedEncryptionKeys, bool* bOutIsSigned) { return ProcessFilesFromIoStoreContainer(InContainerFilename, InDestPath, InKeyChain, InFilter, ExtractFilesWriter, OutOrderMap, OutUsedEncryptionKeys, bOutIsSigned, -1); } bool ProcessFilesFromIoStoreContainer( const TCHAR* InContainerFilename, const TCHAR* InDestPath, const FKeyChain& InKeyChain, const FString* InFilter, TFunction FileProcessFunc, TMap* OutOrderMap, TArray* OutUsedEncryptionKeys, bool* bOutIsSigned, int32 MaxConcurrentReaders) { TRACE_CPUPROFILER_EVENT_SCOPE(ExtractFilesFromIoStoreContainer); TUniquePtr Reader = CreateIoStoreReader(InContainerFilename, InKeyChain); if (!Reader.IsValid()) { return false; } if (!EnumHasAnyFlags(Reader->GetContainerFlags(), EIoContainerFlags::Indexed)) { UE_LOG(LogIoStore, Error, TEXT("Missing directory index for container '%s'"), InContainerFilename); return false; } if (OutUsedEncryptionKeys) { OutUsedEncryptionKeys->Add(Reader->GetEncryptionKeyGuid()); } if (bOutIsSigned) { *bOutIsSigned = EnumHasAnyFlags(Reader->GetContainerFlags(), EIoContainerFlags::Signed); } UE_LOG(LogIoStore, Display, TEXT("Extracting files from IoStore container '%s'..."), InContainerFilename); struct FEntry { FIoChunkId ChunkId; FString SourceFileName; FString DestFileName; uint64 Offset; bool bIsCompressed; FIoHash ChunkHash; }; TArray Entries; const FIoDirectoryIndexReader& IndexReader = Reader->GetDirectoryIndexReader(); FString DestPath(InDestPath); Reader->EnumerateChunks([&Entries, InFilter, &DestPath](const FIoStoreTocChunkInfo& ChunkInfo) { if (!ChunkInfo.bHasValidFileName) { return true; } if (InFilter && (!ChunkInfo.FileName.MatchesWildcard(*InFilter))) { return true; } FEntry& Entry = Entries.AddDefaulted_GetRef(); Entry.ChunkId = ChunkInfo.Id; Entry.SourceFileName = ChunkInfo.FileName; Entry.DestFileName = DestPath / ChunkInfo.FileName.Replace(TEXT("../../../"), TEXT("")); Entry.Offset = ChunkInfo.Offset; Entry.bIsCompressed = ChunkInfo.bIsCompressed; Entry.ChunkHash = ChunkInfo.ChunkHash; return true; }); const bool bContainerIsEncrypted = EnumHasAnyFlags(Reader->GetContainerFlags(), EIoContainerFlags::Encrypted); const int32 MaxConcurrentTasks = (MaxConcurrentReaders <= 0) ? Entries.Num() : FMath::Min(MaxConcurrentReaders, Entries.Num()); int32 ErrorCount = 0; for (int32 EntryStartIdx = 0; EntryStartIdx < Entries.Num(); ) { TArray> ExtractTasks; ExtractTasks.Reserve(MaxConcurrentTasks); EAsyncExecution ThreadPool = EAsyncExecution::ThreadPool; const int32 NumTasks = FMath::Min(MaxConcurrentTasks, Entries.Num() - EntryStartIdx); for (int32 TaskIndex = 0; TaskIndex < NumTasks; ++TaskIndex) { const FEntry& Entry = Entries[EntryStartIdx + TaskIndex]; UE::Tasks::TTask> ReadTask = Reader->ReadAsync(Entry.ChunkId, FIoReadOptions()); // Once the read is done, write out the file. ExtractTasks.Emplace(UE::Tasks::Launch(TEXT("IoStore_Extract"), [&Entry, &FileProcessFunc, ReadTask]() mutable { TIoStatusOr ReadChunkResult = ReadTask.GetResult(); if (!ReadChunkResult.IsOk()) { UE_LOG(LogIoStore, Error, TEXT("Failed reading chunk for file \"%s\" (%s)."), *Entry.SourceFileName, *ReadChunkResult.Status().ToString()); return false; } const uint8* Data = ReadChunkResult.ValueOrDie().Data(); uint64 DataSize = ReadChunkResult.ValueOrDie().DataSize(); if (Entry.ChunkId.GetChunkType() == EIoChunkType::ExportBundleData) { const FZenPackageSummary* PackageSummary = reinterpret_cast(Data); uint64 HeaderDataSize = PackageSummary->HeaderSize; check(HeaderDataSize <= DataSize); FString DestFileName = FPaths::ChangeExtension(Entry.DestFileName, TEXT(".uheader")); if (!FileProcessFunc(Entry.SourceFileName, DestFileName, Entry.ChunkId, Data, HeaderDataSize)) { return false; } DestFileName = FPaths::ChangeExtension(Entry.DestFileName, TEXT(".uexp")); if (!FileProcessFunc(Entry.SourceFileName, DestFileName, Entry.ChunkId, Data + HeaderDataSize, DataSize - HeaderDataSize)) { return false; } } else if (!FileProcessFunc(Entry.SourceFileName, Entry.DestFileName, Entry.ChunkId, Data, DataSize)) { return false; } return true; }, Prerequisites(ReadTask))); } for (int32 TaskIndex = 0; TaskIndex < NumTasks; ++TaskIndex) { if (ExtractTasks[TaskIndex].GetResult()) { const FEntry& Entry = Entries[EntryStartIdx + TaskIndex]; if (OutOrderMap != nullptr) { OutOrderMap->Add(IndexReader.GetMountPoint() / Entry.SourceFileName, Entry.Offset); } } else { ++ErrorCount; } } EntryStartIdx += NumTasks; } UE_LOG(LogIoStore, Log, TEXT("Finished extracting %d chunks (including %d errors)."), Entries.Num(), ErrorCount); return true; } bool SignIoStoreContainer(const TCHAR* InContainerFilename, const FRSAKeyHandle InSigningKey) { FString TocFilePath = FPaths::ChangeExtension(InContainerFilename, TEXT(".utoc")); FString TempOutputPath = TocFilePath + ".tmp"; IPlatformFile& Ipf = FPlatformFileManager::Get().GetPlatformFile(); ON_SCOPE_EXIT { if (Ipf.FileExists(*TempOutputPath)) { Ipf.DeleteFile(*TempOutputPath); } }; FIoStoreTocResource TocResource; FIoStatus Status = FIoStoreTocResource::Read(*TocFilePath, EIoStoreTocReadOptions::ReadAll, TocResource); if (!Status.IsOk()) { UE_LOG(LogIoStore, Error, TEXT("Failed reading container file \"%s\"."), InContainerFilename); return false; } if (TocResource.ChunkBlockSignatures.Num() != TocResource.CompressionBlocks.Num()) { UE_LOG(LogIoStore, Display, TEXT("Container is not signed, calculating block hashes...")); TocResource.ChunkBlockSignatures.Empty(); TUniquePtr ContainerFileReader; int32 LastPartitionIndex = -1; TArray BlockBuffer; BlockBuffer.SetNum(static_cast(TocResource.Header.CompressionBlockSize)); const int32 BlockCount = TocResource.CompressionBlocks.Num(); FString ContainerBasePath = FPaths::ChangeExtension(InContainerFilename, TEXT("")); TStringBuilder<256> UcasFilePath; for (int32 BlockIndex = 0; BlockIndex < BlockCount; ++BlockIndex) { const FIoStoreTocCompressedBlockEntry& CompressionBlockEntry = TocResource.CompressionBlocks[BlockIndex]; uint64 BlockRawSize = Align(CompressionBlockEntry.GetCompressedSize(), FAES::AESBlockSize); check(BlockRawSize <= TocResource.Header.CompressionBlockSize); const int32 PartitionIndex = int32(CompressionBlockEntry.GetOffset() / TocResource.Header.PartitionSize); const uint64 PartitionRawOffset = CompressionBlockEntry.GetOffset() % TocResource.Header.PartitionSize; if (PartitionIndex != LastPartitionIndex) { UcasFilePath.Reset(); UcasFilePath.Append(ContainerBasePath); if (PartitionIndex > 0) { UcasFilePath.Append(FString::Printf(TEXT("_s%d"), PartitionIndex)); } UcasFilePath.Append(TEXT(".ucas")); IFileHandle* ContainerFileHandle = Ipf.OpenRead(*UcasFilePath, /* allowwrite */ false); if (!ContainerFileHandle) { UE_LOG(LogIoStore, Error, TEXT("Failed opening container file \"%s\"."), *UcasFilePath); return false; } ContainerFileReader.Reset(new FArchiveFileReaderGeneric(ContainerFileHandle, *UcasFilePath, ContainerFileHandle->Size(), 256 << 10)); LastPartitionIndex = PartitionIndex; } ContainerFileReader->Seek(PartitionRawOffset); ContainerFileReader->Precache(PartitionRawOffset, 0); // Without this buffering won't work due to the first read after a seek always being uncached ContainerFileReader->Serialize(BlockBuffer.GetData(), BlockRawSize); FSHAHash& BlockHash = TocResource.ChunkBlockSignatures.AddDefaulted_GetRef(); FSHA1::HashBuffer(BlockBuffer.GetData(), BlockRawSize, BlockHash.Hash); } } FIoContainerSettings ContainerSettings; ContainerSettings.ContainerId = TocResource.Header.ContainerId; ContainerSettings.ContainerFlags = TocResource.Header.ContainerFlags | EIoContainerFlags::Signed; ContainerSettings.EncryptionKeyGuid = TocResource.Header.EncryptionKeyGuid; ContainerSettings.SigningKey = InSigningKey; TIoStatusOr WriteStatus = FIoStoreTocResource::Write(*TempOutputPath, TocResource, TocResource.Header.CompressionBlockSize, TocResource.Header.PartitionSize, ContainerSettings); if (!WriteStatus.IsOk()) { UE_LOG(LogIoStore, Error, TEXT("Failed writing new utoc file file \"%s\"."), *TocFilePath); return false; } Ipf.DeleteFile(*TocFilePath); Ipf.MoveFile(*TocFilePath, *TempOutputPath); return true; } static bool ParsePakResponseFile(const TCHAR* FilePath, TArray& OutFiles) { TRACE_CPUPROFILER_EVENT_SCOPE(ParsePakResponseFile); TArray ResponseFileContents; if (!FFileHelper::LoadFileToStringArray(ResponseFileContents, FilePath)) { UE_LOG(LogIoStore, Error, TEXT("Failed to read response file '%s'."), FilePath); return false; } for (const FString& ResponseLine : ResponseFileContents) { TArray SourceAndDest; TArray Switches; FString NextToken; const TCHAR* ResponseLinePtr = *ResponseLine; while (FParse::Token(ResponseLinePtr, NextToken, false)) { if ((**NextToken == TCHAR('-'))) { new(Switches) FString(NextToken.Mid(1)); } else { new(SourceAndDest) FString(NextToken); } } if (SourceAndDest.Num() == 0) { continue; } if (SourceAndDest.Num() != 2) { UE_LOG(LogIoStore, Error, TEXT("Invalid line in response file '%s'."), *ResponseLine); return false; } FPaths::NormalizeFilename(SourceAndDest[0]); FContainerSourceFile& FileEntry = OutFiles.AddDefaulted_GetRef(); FileEntry.NormalizedPath = MoveTemp(SourceAndDest[0]); FileEntry.DestinationPath = MoveTemp(SourceAndDest[1]); for (int32 Index = 0; Index < Switches.Num(); ++Index) { if (Switches[Index] == TEXT("compress")) { FileEntry.bNeedsCompression = true; } if (Switches[Index] == TEXT("encrypt")) { FileEntry.bNeedsEncryption = true; } } } return true; } static bool ParsePakOrderFile(const TCHAR* FilePath, FFileOrderMap& Map, const FIoStoreArguments& Arguments) { IOSTORE_CPU_SCOPE(ParsePakOrderFile); TArray OrderFileContents; if (!FFileHelper::LoadFileToStringArray(OrderFileContents, FilePath)) { UE_LOG(LogIoStore, Error, TEXT("Failed to read order file '%s'."), FilePath); return false; } Map.Name = FPaths::GetCleanFilename(FilePath); UE_LOG(LogIoStore, Display, TEXT("Order file %s (short name %s) priority %d"), FilePath, *Map.Name, Map.Priority); int64 NextOrder = 0; for (const FString& OrderLine : OrderFileContents) { const TCHAR* OrderLinePtr = *OrderLine; FString PackageName; // Skip comments if (FCString::Strncmp(OrderLinePtr, TEXT("#"), 1) == 0 || FCString::Strncmp(OrderLinePtr, TEXT("//"), 2) == 0) { continue; } if (!FParse::Token(OrderLinePtr, PackageName, false)) { UE_LOG(LogIoStore, Error, TEXT("Invalid line in order file '%s'."), *OrderLine); return false; } FName PackageFName; if (FPackageName::IsValidTextForLongPackageName(PackageName)) { PackageFName = FName(PackageName); } else if (PackageName.StartsWith(TEXT("../../../"))) { FString FullFileName = FPaths::Combine(Arguments.CookedDir, PackageName.RightChop(9)); FPaths::NormalizeFilename(FullFileName); PackageFName = Arguments.PackageStore->GetPackageNameFromFileName(FullFileName); } if (!PackageFName.IsNone() && !Map.PackageNameToOrder.Contains(PackageFName)) { Map.PackageNameToOrder.Emplace(PackageFName, NextOrder++); } } UE_LOG(LogIoStore, Display, TEXT("Order file %s (short name %s) contained %d valid entries"), FilePath, *Map.Name, Map.PackageNameToOrder.Num()); return true; } class FCookedFileVisitor : public IPlatformFile::FDirectoryStatVisitor { FCookedFileStatMap& CookedFileStatMap; public: FCookedFileVisitor(FCookedFileStatMap& InCookedFileSizes) : CookedFileStatMap(InCookedFileSizes) { } virtual bool Visit(const TCHAR* FilenameOrDirectory, const FFileStatData& StatData) { if (StatData.bIsDirectory) { return true; } CookedFileStatMap.Add(FilenameOrDirectory, StatData.FileSize); return true; } }; static bool ParseSizeArgument(const TCHAR* CmdLine, const TCHAR* Argument, uint64& OutSize, uint64 DefaultSize = 0) { FString SizeString; if (FParse::Value(CmdLine, Argument, SizeString) && FParse::Value(CmdLine, Argument, OutSize)) { if (SizeString.EndsWith(TEXT("MB"))) { OutSize *= 1024*1024; } else if (SizeString.EndsWith(TEXT("KB"))) { OutSize *= 1024; } return true; } else { OutSize = DefaultSize; return false; } } static bool ParseOrderFileArguments(FIoStoreArguments& Arguments) { IOSTORE_CPU_SCOPE(ParseOrderFileArguments); uint64 OrderMapStartIndex = 0; FString OrderFileStr; if (FParse::Value(FCommandLine::Get(), TEXT("Order="), OrderFileStr, false)) { TArray OrderFilePriorities; TArray OrderFilePaths; OrderFileStr.ParseIntoArray(OrderFilePaths, TEXT(","), true); FString LegacyParam; if (FParse::Value(FCommandLine::Get(), TEXT("GameOrder="), LegacyParam, false)) { UE_LOG(LogIoStore, Warning, TEXT("-GameOrder= and -CookerOrder= are deprecated in favor of -Order")); TArray LegacyPaths; LegacyParam.ParseIntoArray(LegacyPaths, TEXT(","), true); OrderFilePaths.Append(LegacyPaths); } if (FParse::Value(FCommandLine::Get(), TEXT("CookerOrder="), LegacyParam, false)) { UE_LOG(LogIoStore, Warning, TEXT("-CookerOrder is ignored by IoStore. -GameOrder= and -CookerOrder= are deprecated in favor of -Order.")); } FString OrderPriorityString; if (FParse::Value(FCommandLine::Get(), TEXT("OrderPriority="), OrderPriorityString, false)) { TArray PriorityStrings; OrderPriorityString.ParseIntoArray(PriorityStrings, TEXT(","), true); if (PriorityStrings.Num() != OrderFilePaths.Num()) { UE_LOG(LogIoStore, Error, TEXT("Number of parameters to -Order= and -OrderPriority= do not match")); return false; } for (const FString& PriorityString : PriorityStrings) { int32 Priority = FCString::Atoi(*PriorityString); OrderFilePriorities.Add(Priority); } } else { OrderFilePriorities.AddZeroed(OrderFilePaths.Num()); } check(OrderFilePaths.Num() == OrderFilePriorities.Num()); bool bMerge = false; for (int32 i = 0; i < OrderFilePaths.Num(); ++i) { FString& OrderFile = OrderFilePaths[i]; int32 Priority = OrderFilePriorities[i]; FFileOrderMap OrderMap(Priority, i); if (!ParsePakOrderFile(*OrderFile, OrderMap, Arguments)) { return false; } Arguments.OrderMaps.Add(OrderMap); } } Arguments.OrderingOptions.bClusterByOrderFilePriority = !FParse::Param(FCommandLine::Get(), TEXT("DoNotClusterByOrderPriority")); Arguments.OrderingOptions.bAlphaSortClusterPackageLists = FParse::Param(FCommandLine::Get(), TEXT("AlphaSortClusterPackageLists")); FString FallbackOrderStr; if (FParse::Value(FCommandLine::Get(), TEXT("-FallbackOrdering="), FallbackOrderStr)) { if (FallbackOrderStr == TEXT("LoadOrder")) { Arguments.OrderingOptions.FallbackOrderMode = EFallbackOrderMode::LoadOrder; } else if (FallbackOrderStr == TEXT("AlphabeticalClustered")) { Arguments.OrderingOptions.FallbackOrderMode = EFallbackOrderMode::AlphabeticalClustered; } else if (FallbackOrderStr == TEXT("Alphabetical")) { Arguments.OrderingOptions.FallbackOrderMode = EFallbackOrderMode::Alphabetical; } else { UE_LOG(LogIoStore, Fatal, TEXT("Unrecognized fallback order '%s'."), *FallbackOrderStr); } } return true; } bool ParseContainerGenerationArguments(FIoStoreArguments& Arguments, FIoStoreWriterSettings& WriterSettings) { IOSTORE_CPU_SCOPE(ParseContainerGenerationArguments); if (FParse::Param(FCommandLine::Get(), TEXT("sign"))) { Arguments.bSign = true; } UE_LOG(LogIoStore, Display, TEXT("Container signing - %s"), Arguments.bSign ? TEXT("ENABLED") : TEXT("DISABLED")); Arguments.bCreateDirectoryIndex = !FParse::Param(FCommandLine::Get(), TEXT("NoDirectoryIndex")); UE_LOG(LogIoStore, Display, TEXT("Directory index - %s"), Arguments.bCreateDirectoryIndex ? TEXT("ENABLED") : TEXT("DISABLED")); WriterSettings.CompressionMethod = DefaultCompressionMethod; WriterSettings.CompressionBlockSize = DefaultCompressionBlockSize; TArray CompressionFormats; FString DesiredCompressionFormats; if (FParse::Value(FCommandLine::Get(), TEXT("-compressionformats="), DesiredCompressionFormats) || FParse::Value(FCommandLine::Get(), TEXT("-compressionformat="), DesiredCompressionFormats)) { TArray Formats; DesiredCompressionFormats.ParseIntoArray(Formats, TEXT(",")); for (FString& Format : Formats) { // look until we have a valid format FName FormatName = *Format; if (FCompression::IsFormatValid(FormatName)) { WriterSettings.CompressionMethod = FormatName; break; } } if (WriterSettings.CompressionMethod == NAME_None) { UE_LOG(LogIoStore, Warning, TEXT("Failed to find desired compression format(s) '%s'. Using falling back to '%s'"), *DesiredCompressionFormats, *DefaultCompressionMethod.ToString()); } else { UE_LOG(LogIoStore, Display, TEXT("Using compression format '%s'"), *WriterSettings.CompressionMethod.ToString()); } } ParseSizeArgument(FCommandLine::Get(), TEXT("-alignformemorymapping="), WriterSettings.MemoryMappingAlignment, DefaultMemoryMappingAlignment); ParseSizeArgument(FCommandLine::Get(), TEXT("-compressionblocksize="), WriterSettings.CompressionBlockSize, DefaultCompressionBlockSize); WriterSettings.CompressionBlockAlignment = DefaultCompressionBlockAlignment; uint64 BlockAlignment = 0; if (ParseSizeArgument(FCommandLine::Get(), TEXT("-blocksize="), BlockAlignment)) { WriterSettings.CompressionBlockAlignment = BlockAlignment; } // // If a filename to a global.utoc container is provided, all containers in that directory will have their compressed blocks be // made available for the new containers to reuse. This provides two benefits: // 1. Saves compression time for the new blocks, as ssd/nvme io times are significantly faster. // 2. Prevents trivial bit changes in the compressor from causing patch changes down the line, // allowing worry-free compressor upgrading. // // This should be a path to your last released containers. If those containers are encrypted, be sure to // provide keys via -ReferenceContainerCryptoKeys. // if (FParse::Value(FCommandLine::Get(), TEXT("-ReferenceContainerGlobalFileName="), Arguments.ReferenceChunkGlobalContainerFileName)) { FString CryptoKeysCacheFilename; if (FParse::Value(FCommandLine::Get(), TEXT("-ReferenceContainerCryptoKeys="), CryptoKeysCacheFilename)) { UE_LOG(LogIoStore, Display, TEXT("Parsing reference container crypto keys from a crypto key cache file '%s'"), *CryptoKeysCacheFilename); KeyChainUtilities::LoadKeyChainFromFile(CryptoKeysCacheFilename, Arguments.ReferenceChunkKeys); } if (FParse::Value(FCommandLine::Get(), TEXT("-ReferenceContainerChangesCSVFileName="), Arguments.ReferenceChunkChangesCSVFileName)) { UE_LOG(LogIoStore, Display, TEXT("ReferenceCache will write the list of changed/new packages to %s"), *Arguments.ReferenceChunkChangesCSVFileName); } if (FParse::Value(FCommandLine::Get(), TEXT("-ReferenceContainerAdditionalPath="), Arguments.ReferenceChunkAdditionalContainersPath)) { UE_LOG(LogIoStore, Display, TEXT("ReferenceCache will read additional containers from %s"), *Arguments.ReferenceChunkAdditionalContainersPath); } } // By default, we use uncompressed chunk hashes from cook to avoid reading and hashing chunks unnecessarily. // This flag causes us to read and hash anyway, and then ensure they match. It is very bad if this fails! WriterSettings.bValidateChunkHashes = FParse::Param(FCommandLine::Get(), TEXT("validateChunkHashes")); uint64 PatchPaddingAlignment = 0; if (ParseSizeArgument(FCommandLine::Get(), TEXT("-patchpaddingalign="), PatchPaddingAlignment)) { if (PatchPaddingAlignment < WriterSettings.CompressionBlockAlignment) { WriterSettings.CompressionBlockAlignment = PatchPaddingAlignment; } } // Temporary, this command-line allows us to explicitly override the value otherwise shared between pak building and iostore uint64 IOStorePatchPaddingAlignment = 0; if (ParseSizeArgument(FCommandLine::Get(), TEXT("-iostorepatchpaddingalign="), IOStorePatchPaddingAlignment)) { WriterSettings.CompressionBlockAlignment = IOStorePatchPaddingAlignment; } uint64 MaxPartitionSize = 0; if (ParseSizeArgument(FCommandLine::Get(), TEXT("-maxPartitionSize="), MaxPartitionSize)) { WriterSettings.MaxPartitionSize = MaxPartitionSize; } int32 CompressionMinBytesSaved = 0; if (FParse::Value(FCommandLine::Get(), TEXT("-compressionMinBytesSaved="), CompressionMinBytesSaved)) { WriterSettings.CompressionMinBytesSaved = CompressionMinBytesSaved; } int32 CompressionMinPercentSaved = 0; if (FParse::Value(FCommandLine::Get(), TEXT("-compressionMinPercentSaved="), CompressionMinPercentSaved)) { WriterSettings.CompressionMinPercentSaved = CompressionMinPercentSaved; } WriterSettings.bCompressionEnableDDC = FParse::Param(FCommandLine::Get(), TEXT("compressionEnableDDC")); if (WriterSettings.bCompressionEnableDDC && !FPaths::IsProjectFilePathSet()) { UE_LOG(LogIoStore, Warning, TEXT("Ignoring -compressionEnableDDC due to missing .uproject file as the first unnamed argument.")); WriterSettings.bCompressionEnableDDC = false; } int32 CompressionMinSizeToConsiderDDC = 0; if (FParse::Value(FCommandLine::Get(), TEXT("-compressionMinSizeToConsiderDDC="), CompressionMinSizeToConsiderDDC)) { WriterSettings.CompressionMinSizeToConsiderDDC = CompressionMinSizeToConsiderDDC; } UE_LOG(LogIoStore, Display, TEXT("Using memory mapping alignment '%ld'"), WriterSettings.MemoryMappingAlignment); UE_LOG(LogIoStore, Display, TEXT("Using compression block size '%ld'"), WriterSettings.CompressionBlockSize); UE_LOG(LogIoStore, Display, TEXT("Using compression block alignment '%ld'"), WriterSettings.CompressionBlockAlignment); UE_LOG(LogIoStore, Display, TEXT("Using compression min bytes saved '%d'"), WriterSettings.CompressionMinBytesSaved); UE_LOG(LogIoStore, Display, TEXT("Using compression min percent saved '%d'"), WriterSettings.CompressionMinPercentSaved); UE_LOG(LogIoStore, Display, TEXT("Using max partition size '%lld'"), WriterSettings.MaxPartitionSize); if (WriterSettings.bCompressionEnableDDC) { UE_LOG(LogIoStore, Display, TEXT("Using DDC for compression with min size '%d'"), WriterSettings.CompressionMinSizeToConsiderDDC); } else { UE_LOG(LogIoStore, Display, TEXT("Not using DDC for compression")); } FString CommandListFile; if (FParse::Value(FCommandLine::Get(), TEXT("Commands="), CommandListFile)) { UE_LOG(LogIoStore, Display, TEXT("Using command list file: '%s'"), *CommandListFile); TArray Commands; if (!FFileHelper::LoadFileToStringArray(Commands, *CommandListFile)) { UE_LOG(LogIoStore, Error, TEXT("Failed to read command list file '%s'."), *CommandListFile); return false; } Arguments.Containers.Reserve(Commands.Num()); for (const FString& Command : Commands) { FContainerSourceSpec& ContainerSpec = Arguments.Containers.AddDefaulted_GetRef(); if (FParse::Value(*Command, TEXT("Output="), ContainerSpec.OutputPath)) { ContainerSpec.OutputPath = FPaths::ChangeExtension(ContainerSpec.OutputPath, TEXT("")); } ContainerSpec.bOnDemand = FParse::Param(*Command, TEXT("OnDemand")); FParse::Value(*Command, TEXT("OptionalOutput="), ContainerSpec.OptionalOutputPath); FParse::Value(*Command, TEXT("StageLooseFileRootPath="), ContainerSpec.StageLooseFileRootPath); FString ContainerName; if (FParse::Value(*Command, TEXT("ContainerName="), ContainerName)) { ContainerSpec.Name = FName(ContainerName); } FString PatchSourceWildcard; if (FParse::Value(*Command, TEXT("PatchSource="), PatchSourceWildcard)) { IFileManager::Get().FindFiles(ContainerSpec.PatchSourceContainerFiles, *PatchSourceWildcard, true, false); FString PatchSourceContainersDirectory = FPaths::GetPath(*PatchSourceWildcard); for (FString& PatchSourceContainerFile : ContainerSpec.PatchSourceContainerFiles) { PatchSourceContainerFile = PatchSourceContainersDirectory / PatchSourceContainerFile; FPaths::NormalizeFilename(PatchSourceContainerFile); } } ContainerSpec.bGenerateDiffPatch = FParse::Param(*Command, TEXT("GenerateDiffPatch")); FParse::Value(*Command, TEXT("PatchTarget="), ContainerSpec.PatchTargetFile); FString ResponseFilePath; if (FParse::Value(*Command, TEXT("ResponseFile="), ResponseFilePath)) { if (!ParsePakResponseFile(*ResponseFilePath, ContainerSpec.SourceFiles)) { UE_LOG(LogIoStore, Error, TEXT("Failed to parse Pak response file '%s'"), *ResponseFilePath); return false; } FParse::Value(*Command, TEXT("EncryptionKeyOverrideGuid="), ContainerSpec.EncryptionKeyOverrideGuid); } } } for (const FContainerSourceSpec& Container : Arguments.Containers) { if (Container.Name.IsNone()) { UE_LOG(LogIoStore, Error, TEXT("ContainerName argument missing for container '%s'"), *Container.OutputPath); return false; } } Arguments.bFileRegions = FParse::Param(FCommandLine::Get(), TEXT("FileRegions")); WriterSettings.bEnableFileRegions = Arguments.bFileRegions; FString PatchReferenceCryptoKeysFilename; if (FParse::Value(FCommandLine::Get(), TEXT("PatchCryptoKeys="), PatchReferenceCryptoKeysFilename)) { KeyChainUtilities::LoadKeyChainFromFile(PatchReferenceCryptoKeysFilename, Arguments.PatchKeyChain); } else { Arguments.PatchKeyChain = Arguments.KeyChain; } return true; } int32 CreateIoStoreContainerFiles(const TCHAR* CmdLine) { IOSTORE_CPU_SCOPE(CreateIoStoreContainerFiles); UE_LOG(LogIoStore, Display, TEXT("==================== IoStore Utils ====================")); FIoStoreArguments Arguments; FIoStoreWriterSettings WriterSettings; if (!LoadKeyChain(FCommandLine::Get(), Arguments.KeyChain)) { return 1; } ITargetPlatform* TargetPlatform = nullptr; FString TargetPlatformName; if (FParse::Value(FCommandLine::Get(), TEXT("TargetPlatform="), TargetPlatformName)) { ITargetPlatformManagerModule& TPM = GetTargetPlatformManagerRef(); TargetPlatform = TPM.FindTargetPlatform(TargetPlatformName); if (!TargetPlatform) { UE_LOG(LogIoStore, Error, TEXT("Invalid TargetPlatform: '%s'"), *TargetPlatformName); return 1; } } FParse::Value(FCommandLine::Get(), TEXT("csv="), Arguments.CsvPath); FString ArgumentValue; if (FParse::Value(FCommandLine::Get(), TEXT("List="), ArgumentValue)) { FString ContainerPathOrWildcard = MoveTemp(ArgumentValue); if (Arguments.CsvPath.Len() == 0) { UE_LOG(LogIoStore, Error, TEXT("Incorrect arguments. Expected: -list= -csv=")); } return ListContainer(Arguments.KeyChain, ContainerPathOrWildcard, Arguments.CsvPath); } else if (FParse::Value(FCommandLine::Get(), TEXT("Describe="), ArgumentValue)) { FString ContainerPathOrWildcard = ArgumentValue; FString PackageFilter; FParse::Value(FCommandLine::Get(), TEXT("PackageFilter="), PackageFilter); FString OutPath; FParse::Value(FCommandLine::Get(), TEXT("DumpToFile="), OutPath); bool bIncludeExportHashes = FParse::Param(FCommandLine::Get(), TEXT("IncludeExportHashes")); return Describe(ContainerPathOrWildcard, Arguments.KeyChain, PackageFilter, OutPath, bIncludeExportHashes); } else if (FParse::Value(FCommandLine::Get(), TEXT("ValidateCrossContainerRefs="), ArgumentValue)) { FString ContainerPathOrWildcard = ArgumentValue; FString ConfigPath; FParse::Value(FCommandLine::Get(), TEXT("Config="), ConfigPath); FString OutPath; FParse::Value(FCommandLine::Get(), TEXT("DumpToFile="), OutPath); return ValidateCrossContainerRefs(ContainerPathOrWildcard, Arguments.KeyChain, ConfigPath, OutPath); } else if (FParse::Param(FCommandLine::Get(), TEXT("ProfileReadSpeed"))) { // Load the .UTOC file provided and read it in its entirety, cmdline parsed in function return ProfileReadSpeed(FCommandLine::Get(), Arguments.KeyChain); } else if (FParse::Param(FCommandLine::Get(), TEXT("Diff"))) { FString SourcePath, TargetPath, OutPath; FKeyChain SourceKeyChain, TargetKeyChain; if (!FParse::Value(FCommandLine::Get(), TEXT("Source="), SourcePath)) { UE_LOG(LogIoStore, Error, TEXT("Incorrect arguments. Expected: -Diff -Source= -Target=")); return -1; } if (!IFileManager::Get().DirectoryExists(*SourcePath)) { UE_LOG(LogIoStore, Error, TEXT("Source directory '%s' doesn't exist"), *SourcePath); return -1; } if (!FParse::Value(FCommandLine::Get(), TEXT("Target="), TargetPath)) { UE_LOG(LogIoStore, Error, TEXT("Incorrect arguments. Expected: -Diff -Source= -Target=")); } if (!IFileManager::Get().DirectoryExists(*TargetPath)) { UE_LOG(LogIoStore, Error, TEXT("Target directory '%s' doesn't exist"), *TargetPath); return -1; } FParse::Value(FCommandLine::Get(), TEXT("DumpToFile="), OutPath); FString CryptoKeysCacheFilename; if (FParse::Value(CmdLine, TEXT("CryptoKeys="), CryptoKeysCacheFilename) || FParse::Value(CmdLine, TEXT("SourceCryptoKeys="), CryptoKeysCacheFilename)) { UE_LOG(LogIoStore, Display, TEXT("Parsing source crypto keys from '%s'"), *CryptoKeysCacheFilename); KeyChainUtilities::LoadKeyChainFromFile(CryptoKeysCacheFilename, SourceKeyChain); } if (FParse::Value(CmdLine, TEXT("TargetCryptoKeys="), CryptoKeysCacheFilename)) { UE_LOG(LogIoStore, Display, TEXT("Parsing target crypto keys from '%s'"), *CryptoKeysCacheFilename); KeyChainUtilities::LoadKeyChainFromFile(CryptoKeysCacheFilename, TargetKeyChain); } else { TargetKeyChain = SourceKeyChain; } EChunkTypeFilter ChunkTypeFilter = EChunkTypeFilter::None; if (FParse::Param(FCommandLine::Get(), TEXT("FilterBulkData"))) { ChunkTypeFilter = EChunkTypeFilter::BulkData; } else if (FParse::Param(FCommandLine::Get(), TEXT("FilterPackageData"))) { ChunkTypeFilter = EChunkTypeFilter::PackageData; } return Diff(SourcePath, SourceKeyChain, TargetPath, TargetKeyChain, OutPath, ChunkTypeFilter); } else if (FParse::Param(FCommandLine::Get(), TEXT("Staged2Zen"))) { FString BuildPath; FString ProjectName; if (!FParse::Value(FCommandLine::Get(), TEXT("BuildPath="), BuildPath) || !FParse::Value(FCommandLine::Get(), TEXT("ProjectName="), ProjectName) || !TargetPlatform) { UE_LOG(LogIoStore, Error, TEXT("Incorrect arguments. Expected: -Staged2Zen -BuildPath= -ProjectName= -TargetPlatform=")); return -1; } return Staged2Zen(BuildPath, Arguments.KeyChain, ProjectName, TargetPlatform); } else if (FParse::Param(FCommandLine::Get(), TEXT("CreateContentPatch"))) { if (!ParseContainerGenerationArguments(Arguments, WriterSettings)) { return -1; } for (const FContainerSourceSpec& Container : Arguments.Containers) { if (Container.PatchTargetFile.IsEmpty()) { UE_LOG(LogIoStore, Error, TEXT("PatchTarget argument missing for container '%s'"), *Container.OutputPath); return -1; } } return CreateContentPatch(Arguments, WriterSettings); } else if (FParse::Param(FCommandLine::Get(), TEXT("GenerateZenFileSystemManifest"))) { if (!TargetPlatform) { UE_LOG(LogIoStore, Error, TEXT("Incorrect arguments. Expected: -GenerateZenFileSystemManifest -TargetPlatform=")); return -11; } return GenerateZenFileSystemManifest(TargetPlatform); } else if (FParse::Param(FCommandLine::Get(), TEXT("StartZenServerForStage"))) { TArray SponsorProcessIds; uint32 SponsorProcessId = 0; if (FParse::Value(FCommandLine::Get(), TEXT("SponsorProcessID="), SponsorProcessId) && (SponsorProcessId > 0)) { SponsorProcessIds.Add(SponsorProcessId); } FString MarkerFilename; if (FParse::Value(FCommandLine::Get(), TEXT("ProjectStore="), MarkerFilename)) { TUniquePtr Ar(IFileManager::Get().CreateFileReader(*MarkerFilename)); if (Ar) { UE::Zen::FZenServiceInstance& ZenServiceInstance = UE::Zen::GetDefaultServiceInstance(); if (!ZenServiceInstance.IsServiceReady()) { UE_LOG(LogIoStore, Error, TEXT("Failed to launch ZenServer.")); return -1; } const UE::Zen::FServiceSettings& ZenServiceSettings = ZenServiceInstance.GetServiceSettings(); if (!SponsorProcessIds.IsEmpty() && ZenServiceSettings.IsAutoLaunch() && ZenServiceInstance.GetServiceSettings().SettingsVariant.Get().bLimitProcessLifetime) { if (!ZenServiceInstance.AddSponsorProcessIDs(SponsorProcessIds)) { UE_LOG(LogIoStore, Error, TEXT("Failed to add sponsor process IDs to launched ZenServer.")); return -1; } } return 0; } else { UE_LOG(LogIoStore, Error, TEXT("Failed reading project store marker")); } } UE_LOG(LogIoStore, Error, TEXT("Expected -PackageStoreManifest= or -ProjectStore=")); return -1; } else if (FParse::Value(FCommandLine::Get(), TEXT("CreateDLCContainer="), Arguments.DLCPluginPath)) { if (!ParseContainerGenerationArguments(Arguments, WriterSettings)) { return -1; } Arguments.DLCName = FPaths::GetBaseFilename(*Arguments.DLCPluginPath); Arguments.bRemapPluginContentToGame = FParse::Param(FCommandLine::Get(), TEXT("RemapPluginContentToGame")); UE_LOG(LogIoStore, Display, TEXT("DLC: '%s'"), *Arguments.DLCPluginPath); UE_LOG(LogIoStore, Display, TEXT("Remapping plugin content to game: '%s'"), Arguments.bRemapPluginContentToGame ? TEXT("True") : TEXT("False")); bool bAssetRegistryLoaded = false; FString BasedOnReleaseVersionPath; if (FParse::Value(FCommandLine::Get(), TEXT("BasedOnReleaseVersionPath="), BasedOnReleaseVersionPath)) { UE_LOG(LogIoStore, Display, TEXT("Based on release version path: '%s'"), *BasedOnReleaseVersionPath); FString DevelopmentAssetRegistryPath = FPaths::Combine(BasedOnReleaseVersionPath, TEXT("Metadata"), GetDevelopmentAssetRegistryFilename()); FArrayReader SerializedAssetData; if (FPaths::FileExists(*DevelopmentAssetRegistryPath) && FFileHelper::LoadFileToArray(SerializedAssetData, *DevelopmentAssetRegistryPath)) { FAssetRegistryState ReleaseAssetRegistry; FAssetRegistrySerializationOptions Options; if (ReleaseAssetRegistry.Serialize(SerializedAssetData, Options)) { UE_LOG(LogIoStore, Display, TEXT("Loaded asset registry '%s'"), *DevelopmentAssetRegistryPath); bAssetRegistryLoaded = true; TArray PackageNames; ReleaseAssetRegistry.GetPackageNames(PackageNames); Arguments.ReleasedPackages.PackageNames.Reserve(PackageNames.Num()); Arguments.ReleasedPackages.PackageIdToName.Reserve(PackageNames.Num()); for (FName PackageName : PackageNames) { // skip over packages that were not actually saved out, but were added to the AR - the DLC may now have those packages included, // and there will be a conflict later on if the package is in this list and the DLC list. PackageFlags of 0 means it was // evaluated and skipped. bool bHasAny = false; ReleaseAssetRegistry.EnumerateAssetsByPackageName(PackageName, [&bHasAny, &Arguments, PackageName](const FAssetData* AssetData) { // just check the first one in the list, they will all have the same flags if (AssetData->PackageFlags != 0) { Arguments.ReleasedPackages.PackageNames.Add(PackageName); Arguments.ReleasedPackages.PackageIdToName.Add(FPackageId::FromName(PackageName), PackageName); } bHasAny = true; return false; // stop iterating }); checkf(bHasAny, TEXT("It is unexpected that no assets were found in DevelopmentAssetRegistry for the package %s. This indicates an invalid AR."), *PackageName.ToString()); } } } } } else if (FParse::Value(FCommandLine::Get(), TEXT("CreateGlobalContainer="), Arguments.GlobalContainerPath)) { Arguments.GlobalContainerPath = FPaths::ChangeExtension(Arguments.GlobalContainerPath, TEXT("")); if (!ParseContainerGenerationArguments(Arguments, WriterSettings)) { return -1; } } else { UE_LOG(LogIoStore, Display, TEXT("Usage:")); UE_LOG(LogIoStore, Display, TEXT(" -List= -CSV= [-CryptoKeys=]")); UE_LOG(LogIoStore, Display, TEXT(" -Describe= [-PackageFilter=] [-DumpToFile=] [-CryptoKeys=]")); return -1; } // Common path for creating containers FParse::Value(FCommandLine::Get(), TEXT("CookedDirectory="), Arguments.CookedDir); if (Arguments.CookedDir.IsEmpty()) { UE_LOG(LogIoStore, Error, TEXT("CookedDirectory must be specified")); return -1; } // // -compresslevel is technically consumed by OodleDataCompressionFormat, however the setting that it represents (PackageCompressionLevel_*) // is the intention of package compression, and so should also determine the compression we use for shaders. For Shaders we want fast decompression, // so we always used Mermaid. Note that this relies on compresslevel being passed even when the containers aren't compressed. // FString ShaderOodleLevel; FParse::Value(FCommandLine::Get(), TEXT("compresslevel="), ShaderOodleLevel); if (ShaderOodleLevel.Len()) { if (FOodleDataCompression::ECompressionLevelFromString(*ShaderOodleLevel, Arguments.ShaderOodleLevel)) { UE_LOG(LogIoStore, Display, TEXT("Selected Oodle level %d (%s) from command line for shaders"), (int)Arguments.ShaderOodleLevel, FOodleDataCompression::ECompressionLevelToString(Arguments.ShaderOodleLevel)); } } // Whether or not to write compressed asset sizes back to the asset registry. FString WriteBackMetadataToAssetRegistry; if (FParse::Value(FCommandLine::Get(), TEXT("WriteBackMetadataToAssetRegistry="), WriteBackMetadataToAssetRegistry)) { // StaticEnum not available in UnrealPak, so manual conversion: if (WriteBackMetadataToAssetRegistry.Equals(TEXT("AdjacentFile"), ESearchCase::IgnoreCase)) { Arguments.WriteBackMetadataToAssetRegistry = EAssetRegistryWritebackMethod::AdjacentFile; } else if (WriteBackMetadataToAssetRegistry.Equals(TEXT("OriginalFile"), ESearchCase::IgnoreCase)) { Arguments.WriteBackMetadataToAssetRegistry = EAssetRegistryWritebackMethod::OriginalFile; } else if (WriteBackMetadataToAssetRegistry.Equals(TEXT("Disabled"), ESearchCase::IgnoreCase)) { Arguments.WriteBackMetadataToAssetRegistry = EAssetRegistryWritebackMethod::Disabled; } else { UE_LOG(LogIoStore, Error, TEXT("Invalid WriteBackMetdataToAssetRegistry value: %s - check setting in ProjectSettings -> Packaging"), *WriteBackMetadataToAssetRegistry); UE_LOG(LogIoStore, Error, TEXT("Valid options are: AdjacentFile, OriginalFile, Disabled."), *WriteBackMetadataToAssetRegistry); return -1; } Arguments.bWritePluginSizeSummaryJsons = FParse::Param(FCommandLine::Get(), TEXT("WritePluginSizeSummaryJsons")); } FString PackageStoreManifestFilename; if (FParse::Value(FCommandLine::Get(), TEXT("PackageStoreManifest="), PackageStoreManifestFilename)) { TUniquePtr PackageStore = MakeUnique(Arguments.CookedDir); FIoStatus Status = PackageStore->LoadManifest(*PackageStoreManifestFilename); if (Status.IsOk()) { Arguments.PackageStore = MoveTemp(PackageStore); } else { UE_LOG(LogIoStore, Fatal, TEXT("Failed loading package store manifest '%s'"), *PackageStoreManifestFilename); } } else { FString ProjectStoreFilename; if (FParse::Value(FCommandLine::Get(), TEXT("ProjectStore="), ProjectStoreFilename)) { TUniquePtr PackageStore = MakeUnique(Arguments.CookedDir); FIoStatus Status = PackageStore->LoadProjectStore(*ProjectStoreFilename); if (Status.IsOk()) { Arguments.PackageStore = MoveTemp(PackageStore); } else { UE_LOG(LogIoStore, Fatal, TEXT("Failed loading project store '%s'"), *ProjectStoreFilename); } } else { UE_LOG(LogIoStore, Error, TEXT("Expected -PackageStoreManifest= or -ProjectStore=")); return -1; } } if (!ParseOrderFileArguments(Arguments)) { return false; } FString ScriptObjectsFile; if (FParse::Value(FCommandLine::Get(), TEXT("ScriptObjects="), ScriptObjectsFile)) { TArray ScriptObjectsData; if (!FFileHelper::LoadFileToArray(ScriptObjectsData, *ScriptObjectsFile)) { UE_LOG(LogIoStore, Fatal, TEXT("Failed reading script objects file '%s'"), *ScriptObjectsFile); } Arguments.ScriptObjects = MakeUnique(FIoBuffer::Clone, ScriptObjectsData.GetData(), ScriptObjectsData.Num()); } else { UE_CLOG(!Arguments.PackageStore->HasZenStoreClient(), LogIoStore, Fatal, TEXT("Expected -ScriptObjects= argument")); TIoStatusOr Status = Arguments.PackageStore->ReadChunk(CreateIoChunkId(0, 0, EIoChunkType::ScriptObjects)); UE_CLOG(!Status.IsOk(), LogIoStore, Fatal, TEXT("Failed reading script objects chunk '%s'"), *Status.Status().ToString()); Arguments.ScriptObjects = MakeUnique(Status.ConsumeValueOrDie()); } { IOSTORE_CPU_SCOPE(FindCookedAssets); UE_LOG(LogIoStore, Display, TEXT("Searching for cooked assets in folder '%s'"), *Arguments.CookedDir); FCookedFileVisitor CookedFileVistor(Arguments.CookedFileStatMap); IFileManager::Get().IterateDirectoryStatRecursively(*Arguments.CookedDir, CookedFileVistor); UE_LOG(LogIoStore, Display, TEXT("Found '%d' files"), Arguments.CookedFileStatMap.Num()); } return CreateTarget(Arguments, WriterSettings); }