// Copyright Epic Games, Inc. All Rights Reserved. /*============================================================================= DerivedDataCacheCommandlet.cpp: Commandlet for DDC maintenence =============================================================================*/ #include "Commandlets/DerivedDataCacheCommandlet.h" #include "UObject/UObjectHash.h" #include "UObject/UObjectIterator.h" #include "UObject/Package.h" #include "Misc/ConfigCacheIni.h" #include "Misc/PackageName.h" #include "PackageHelperFunctions.h" #include "DerivedDataCacheInterface.h" #include "GlobalShader.h" #include "Interfaces/ITargetPlatform.h" #include "Interfaces/ITargetPlatformManagerModule.h" #include "ShaderCompiler.h" #include "DistanceFieldAtlas.h" #include "Misc/RedirectCollector.h" #include "Engine/Texture.h" #include "CookOnTheSide/CookOnTheFlyServer.h" #include "Algo/RemoveIf.h" #include "Algo/Transform.h" #include "Settings/ProjectPackagingSettings.h" DEFINE_LOG_CATEGORY_STATIC(LogDerivedDataCacheCommandlet, Log, All); class UDerivedDataCacheCommandlet::FObjectReferencer: public FGCObject { public: FObjectReferencer(TMap& InReferencedObjects) : ReferencedObjects(InReferencedObjects) { } private: void AddReferencedObjects(FReferenceCollector& Collector) override { Collector.AllowEliminatingReferences(false); Collector.AddReferencedObjects(ReferencedObjects); Collector.AllowEliminatingReferences(true); } FString GetReferencerName() const override { return TEXT("UDerivedDataCacheCommandlet"); } FString ReferencerName; TMap& ReferencedObjects; }; class UDerivedDataCacheCommandlet::FPackageListener: public FUObjectArray::FUObjectCreateListener , public FUObjectArray::FUObjectDeleteListener { public: FPackageListener() { GUObjectArray.AddUObjectDeleteListener(this); GUObjectArray.AddUObjectCreateListener(this); // We might be late to the party, check if some UPackage already have been created for (TObjectIterator PackageIter; PackageIter; ++PackageIter) { NewPackages.Add(*PackageIter); } } ~FPackageListener() { GUObjectArray.RemoveUObjectDeleteListener(this); GUObjectArray.RemoveUObjectCreateListener(this); } TSet& GetNewPackages() { return NewPackages; } private: void NotifyUObjectCreated(const class UObjectBase* Object, int32 Index) override { if (Object->GetClass() == UPackage::StaticClass()) { NewPackages.Add(const_cast(static_cast(Object))); } } void NotifyUObjectDeleted(const class UObjectBase* Object, int32 Index) override { if (Object->GetClass() == UPackage::StaticClass()) { NewPackages.Remove(const_cast(static_cast(Object))); } } void OnUObjectArrayShutdown() override { GUObjectArray.RemoveUObjectDeleteListener(this); GUObjectArray.RemoveUObjectCreateListener(this); } TSet NewPackages; }; UDerivedDataCacheCommandlet::UDerivedDataCacheCommandlet(FVTableHelper& Helper) : Super(Helper) { } UDerivedDataCacheCommandlet::UDerivedDataCacheCommandlet(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { LogToConsole = false; } void UDerivedDataCacheCommandlet::MaybeMarkPackageAsAlreadyLoaded(UPackage* Package) { if (ProcessedPackages.Contains(Package->GetFName())) { UE_LOG(LogDerivedDataCacheCommandlet, Verbose, TEXT("Marking %s already loaded."), *Package->GetName()); Package->SetPackageFlags(PKG_ReloadingForCooker); } } static void WaitForCurrentShaderCompilationToFinish(bool& bInOutHadActivity) { if (GShaderCompilingManager->IsCompiling()) { bInOutHadActivity = true; int32 CachedShaderCount = GShaderCompilingManager->GetNumRemainingJobs(); if (CachedShaderCount > 0) { UE_LOG(LogDerivedDataCacheCommandlet, Display, TEXT("Waiting for %d shaders to finish."), CachedShaderCount); } int32 NumCompletedShadersSinceLastLog = 0; while (GShaderCompilingManager->IsCompiling()) { const int32 CurrentShaderCount = GShaderCompilingManager->GetNumRemainingJobs(); NumCompletedShadersSinceLastLog += (CachedShaderCount - CurrentShaderCount); CachedShaderCount = CurrentShaderCount; if (NumCompletedShadersSinceLastLog >= 1000) { UE_LOG(LogDerivedDataCacheCommandlet, Display, TEXT("Waiting for %d shaders to finish."), CachedShaderCount); NumCompletedShadersSinceLastLog = 0; } // Process any asynchronous shader compile results that are ready, limit execution time GShaderCompilingManager->ProcessAsyncResults(true, false); GDistanceFieldAsyncQueue->ProcessAsyncTasks(); } GShaderCompilingManager->FinishAllCompilation(); // Final blocking check as IsCompiling() may be non-deterministic UE_LOG(LogDerivedDataCacheCommandlet, Display, TEXT("Done waiting for shaders to finish.")); } // these shouldn't be predicated on whether the shaders were being compiled GDistanceFieldAsyncQueue->BlockUntilAllBuildsComplete(); } static void WaitForCurrentTextureBuildingToFinish(bool& bInOutHadActivity) { for (TObjectIterator Texture; Texture; ++Texture) { Texture->FinishCachePlatformData(); } }; static void PumpAsync(bool* bInOutHadActivity = nullptr) { bool bHadActivity = false; WaitForCurrentShaderCompilationToFinish(bHadActivity); WaitForCurrentTextureBuildingToFinish(bHadActivity); if (bInOutHadActivity) { *bInOutHadActivity = *bInOutHadActivity || bHadActivity; } } void UDerivedDataCacheCommandlet::CacheLoadedPackages(UPackage* CurrentPackage, uint8 PackageFilter, const TArray& Platforms) { TRACE_CPUPROFILER_EVENT_SCOPE(UDerivedDataCacheCommandlet::CacheLoadedPackages); const double BeginCacheTimeStart = FPlatformTime::Seconds(); // We will only remove what we process from the list to avoid unprocessed package being forever forgotten. TSet& NewPackages = PackageListener->GetNewPackages(); TArray ObjectsWithOuter; for (auto NewPackageIt = NewPackages.CreateIterator(); NewPackageIt; ++NewPackageIt) { UPackage* NewPackage = *NewPackageIt; const FName NewPackageName = NewPackage->GetFName(); if (!ProcessedPackages.Contains(NewPackageName)) { if ((PackageFilter & NORMALIZE_ExcludeEnginePackages) != 0 && NewPackage->GetName().StartsWith(TEXT("/Engine"))) { UE_LOG(LogDerivedDataCacheCommandlet, Display, TEXT("Skipping %s as Engine package"), *NewPackageName.ToString()); // Add it so we don't convert the FName to a string everytime we encounter this package ProcessedPackages.Add(NewPackageName); NewPackageIt.RemoveCurrent(); } else if (NewPackage == CurrentPackage || !PackagesToProcess.Contains(NewPackageName)) { UE_LOG(LogDerivedDataCacheCommandlet, Display, TEXT("Processing %s"), *NewPackageName.ToString()); ProcessedPackages.Add(NewPackageName); NewPackageIt.RemoveCurrent(); ObjectsWithOuter.Reset(); GetObjectsWithOuter(NewPackage, ObjectsWithOuter, true /* bIncludeNestedObjects */, RF_ClassDefaultObject /* ExclusionFlags */); for (UObject* Object: ObjectsWithOuter) { for (auto Platform: Platforms) { Object->BeginCacheForCookedPlatformData(Platform); } CachingObjects.Add(Object); } } } else { NewPackageIt.RemoveCurrent(); } } BeginCacheTime += FPlatformTime::Seconds() - BeginCacheTimeStart; ProcessCachingObjects(Platforms); } bool UDerivedDataCacheCommandlet::ProcessCachingObjects(const TArray& Platforms) { TRACE_CPUPROFILER_EVENT_SCOPE(UDerivedDataCacheCommandlet::ProcessCachingObjects); bool bHadActivity = false; if (CachingObjects.Num() > 0) { PumpAsync(); double CurrentTime = FPlatformTime::Seconds(); for (auto It = CachingObjects.CreateIterator(); It; ++It) { // Call IsCachedCookedPlatformDataLoaded once a second per object since it can be quite expensive if (CurrentTime - It->Value > 1.0) { UObject* Object = It->Key; bool bIsFinished = true; for (auto Platform: Platforms) { // IsCachedCookedPlatformDataLoaded can be quite slow for some objects // Do not call it if bIsFinished is already false bIsFinished = bIsFinished && Object->IsCachedCookedPlatformDataLoaded(Platform); } if (bIsFinished) { bHadActivity = true; Object->WillNeverCacheCookedPlatformDataAgain(); Object->ClearAllCachedCookedPlatformData(); It.RemoveCurrent(); } else { It->Value = CurrentTime; } } } } return bHadActivity; } void UDerivedDataCacheCommandlet::FinishCachingObjects(const TArray& Platforms) { // Timing variables double DDCCommandletMaxWaitSeconds = 60. * 10.; GConfig->GetDouble(TEXT("CookSettings"), TEXT("DDCCommandletMaxWaitSeconds"), DDCCommandletMaxWaitSeconds, GEditorIni); const double FinishCacheTimeStart = FPlatformTime::Seconds(); double LastActivityTime = FinishCacheTimeStart; while (CachingObjects.Num() > 0) { bool bHadActivity = ProcessCachingObjects(Platforms); double CurrentTime = FPlatformTime::Seconds(); if (!bHadActivity) { PumpAsync(&bHadActivity); } if (!bHadActivity) { if (CurrentTime - LastActivityTime >= DDCCommandletMaxWaitSeconds) { UObject* Object = CachingObjects.CreateIterator()->Key; UE_LOG(LogDerivedDataCacheCommandlet, Display, TEXT("Timed out for %.2lfs waiting for %d objects to finish caching. First object: %s."), DDCCommandletMaxWaitSeconds, CachingObjects.Num(), *Object->GetFullName()); break; } else { const double WaitingForCacheSleepTime = 0.050; FPlatformProcess::Sleep(WaitingForCacheSleepTime); } } else { LastActivityTime = CurrentTime; } } FinishCacheTime += FPlatformTime::Seconds() - FinishCacheTimeStart; } int32 UDerivedDataCacheCommandlet::Main(const FString& Params) { // Avoid putting those directly in the constructor because we don't // want the CDO to have a second copy of these being active. PackageListener = MakeUnique(); ObjectReferencer = MakeUnique(CachingObjects); TArray Tokens, Switches; ParseCommandLine(*Params, Tokens, Switches); bool bFillCache = Switches.Contains("FILL"); // do the equivalent of a "loadpackage -all" to fill the DDC bool bStartupOnly = Switches.Contains("STARTUPONLY"); // regardless of any other flags, do not iterate packages // Subsets for parallel processing uint32 SubsetMod = 0; uint32 SubsetTarget = MAX_uint32; FParse::Value(*Params, TEXT("SubsetMod="), SubsetMod); FParse::Value(*Params, TEXT("SubsetTarget="), SubsetTarget); bool bDoSubset = SubsetMod > 0 && SubsetTarget < SubsetMod; double FindProcessedPackagesTime = 0.0; double GCTime = 0.0; FinishCacheTime = 0.; BeginCacheTime = 0.; if (!bStartupOnly && bFillCache) { FCoreUObjectDelegates::PackageCreatedForLoad.AddUObject(this, &UDerivedDataCacheCommandlet::MaybeMarkPackageAsAlreadyLoaded); Tokens.Empty(2); Tokens.Add(FString("*") + FPackageName::GetAssetPackageExtension()); FString MapList; if (FParse::Value(*Params, TEXT("Map="), MapList)) { for (int StartIdx = 0; StartIdx < MapList.Len();) { int EndIdx = StartIdx; while (EndIdx < MapList.Len() && MapList[EndIdx] != '+') { EndIdx++; } Tokens.Add(MapList.Mid(StartIdx, EndIdx - StartIdx) + FPackageName::GetMapPackageExtension()); StartIdx = EndIdx + 1; } } else { Tokens.Add(FString("*") + FPackageName::GetMapPackageExtension()); } uint8 PackageFilter = NORMALIZE_DefaultFlags; if (Switches.Contains(TEXT("MAPSONLY"))) { PackageFilter |= NORMALIZE_ExcludeContentPackages; } if (Switches.Contains(TEXT("PROJECTONLY"))) { PackageFilter |= NORMALIZE_ExcludeEnginePackages; } if (!Switches.Contains(TEXT("DEV"))) { PackageFilter |= NORMALIZE_ExcludeDeveloperPackages; } if (!Switches.Contains(TEXT("NOREDIST"))) { PackageFilter |= NORMALIZE_ExcludeNoRedistPackages; } // assume the first token is the map wildcard/pathname TSet FilesInPath; TArray Unused; TArray TokenFiles; for (int32 TokenIndex = 0; TokenIndex < Tokens.Num(); TokenIndex++) { TokenFiles.Reset(); if (!NormalizePackageNames(Unused, TokenFiles, Tokens[TokenIndex], PackageFilter)) { UE_LOG(LogDerivedDataCacheCommandlet, Display, TEXT("No packages found for parameter %i: '%s'"), TokenIndex, *Tokens[TokenIndex]); continue; } FilesInPath.Append(TokenFiles); } TArray> PackagePaths; PackagePaths.Reserve(FilesInPath.Num()); for (FString& Filename: FilesInPath) { FString PackageName; FString FailureReason; if (!FPackageName::TryConvertFilenameToLongPackageName(Filename, PackageName, &FailureReason)) { UE_LOG(LogDerivedDataCacheCommandlet, Warning, TEXT("Unable to resolve filename %s to package name because: %s"), *Filename, *FailureReason); continue; } PackagePaths.Emplace(MoveTemp(Filename), FName(*PackageName)); } // Respect settings that instruct us not to enumerate some paths TArray LocalDirsToNotSearch; const UProjectPackagingSettings* const PackagingSettings = GetDefault(); for (const FDirectoryPath& DirToNotSearch: PackagingSettings->TestDirectoriesToNotSearch) { FString LocalPath; if (FPackageName::TryConvertGameRelativePackagePathToLocalPath(DirToNotSearch.Path, LocalPath)) { LocalDirsToNotSearch.Add(LocalPath); } else { UE_LOG(LogCook, Warning, TEXT("'ProjectSettings -> Project -> Packaging -> Test directories to not search' has invalid element '%s'"), *DirToNotSearch.Path); } } TArray LocalFilenamesToSkip; if (FPackageName::FindPackagesInDirectories(LocalFilenamesToSkip, LocalDirsToNotSearch)) { TSet PackageNamesToSkip; Algo::Transform(LocalFilenamesToSkip, PackageNamesToSkip, [](const FString& Filename) { FString PackageName; if (FPackageName::TryConvertFilenameToLongPackageName(Filename, PackageName)) { return FName(*PackageName); } return FName(NAME_None); }); int32 NewNum = Algo::StableRemoveIf(PackagePaths, [&PackageNamesToSkip](const TPair& PackagePath) { return PackageNamesToSkip.Contains(PackagePath.Get<1>()); }); PackagePaths.SetNum(NewNum); } ITargetPlatformManagerModule* TPM = GetTargetPlatformManager(); const TArray& Platforms = TPM->GetActiveTargetPlatforms(); for (int32 Index = 0; Index < Platforms.Num(); Index++) { TArray DesiredShaderFormats; Platforms[Index]->GetAllTargetedShaderFormats(DesiredShaderFormats); for (int32 FormatIndex = 0; FormatIndex < DesiredShaderFormats.Num(); FormatIndex++) { const EShaderPlatform ShaderPlatform = ShaderFormatToLegacyShaderPlatform(DesiredShaderFormats[FormatIndex]); // Kick off global shader compiles for each target platform. Note that shader platform alone is not sufficient to distinguish between WindowsEditor and WindowsClient, which after UE 4.25 have different DDC CompileGlobalShaderMap(ShaderPlatform, Platforms[Index], false); } } const int32 GCInterval = 100; int32 NumProcessedSinceLastGC = 0; bool bLastPackageWasMap = false; if (PackagePaths.Num() == 0) { UE_LOG(LogDerivedDataCacheCommandlet, Display, TEXT("No packages found to load.")); } else { UE_LOG(LogDerivedDataCacheCommandlet, Display, TEXT("%d packages to load..."), PackagePaths.Num()); } // Gather the list of packages to process PackagesToProcess.Empty(PackagePaths.Num()); for (int32 PackageIndex = PackagePaths.Num() - 1; PackageIndex >= 0; PackageIndex--) { PackagesToProcess.Add(PackagePaths[PackageIndex].Get<1>()); } // Process each package for (int32 PackageIndex = PackagePaths.Num() - 1; PackageIndex >= 0; PackageIndex--) { TPair& PackagePath = PackagePaths[PackageIndex]; const FString& Filename = PackagePath.Get<0>(); FName PackageFName = PackagePath.Get<1>(); // check(!ProcessedPackages.Contains(PackageFName)); if (ProcessedPackages.Contains(PackageFName)) { UE_LOG(LogDerivedDataCacheCommandlet, Display, TEXT("Duplicate Package Processed (%d) %s"), FilesInPath.Num() - PackageIndex, *Filename); continue; } // If work is distributed, skip packages that are meant to be process by other machines if (bDoSubset) { FString PackageName = PackageFName.ToString(); if (FCrc::StrCrc_DEPRECATED(*PackageName.ToUpper()) % SubsetMod != SubsetTarget) { continue; } } UE_LOG(LogDerivedDataCacheCommandlet, Display, TEXT("Loading (%d) %s"), FilesInPath.Num() - PackageIndex, *Filename); UPackage* Package = LoadPackage(NULL, *Filename, LOAD_None); if (Package == NULL) { UE_LOG(LogDerivedDataCacheCommandlet, Error, TEXT("Error loading %s!"), *Filename); bLastPackageWasMap = false; } else { bLastPackageWasMap = Package->ContainsMap(); NumProcessedSinceLastGC++; } // even if the load failed this could be the first time through the loop so it might have all the startup packages to resolve GRedirectCollector.ResolveAllSoftObjectPaths(); // Find any new packages and cache all the objects in each package CacheLoadedPackages(Package, PackageFilter, Platforms); // Perform a GC if conditions are met if (NumProcessedSinceLastGC >= GCInterval || PackageIndex < 0 || bLastPackageWasMap) { const double StartGCTime = FPlatformTime::Seconds(); if (NumProcessedSinceLastGC >= GCInterval || PackageIndex < 0) { UE_LOG(LogDerivedDataCacheCommandlet, Display, TEXT("GC (Full)...")); CollectGarbage(RF_NoFlags); NumProcessedSinceLastGC = 0; } else { UE_LOG(LogDerivedDataCacheCommandlet, Display, TEXT("GC...")); CollectGarbage(RF_Standalone); } GCTime += FPlatformTime::Seconds() - StartGCTime; bLastPackageWasMap = false; } } } FinishCachingObjects(GetTargetPlatformManager()->GetActiveTargetPlatforms()); GetDerivedDataCacheRef().WaitForQuiescence(true); UE_LOG(LogDerivedDataCacheCommandlet, Display, TEXT("BeginCacheTime=%.2lfs, FinishCacheTime=%.2lfs, GCTime=%.2lfs."), BeginCacheTime, FinishCacheTime, GCTime); return 0; }