// Copyright Epic Games, Inc. All Rights Reserved. #include "GenerateNaniteDisplacedMeshCommandlet.h" #include "AssetRegistry/ARFilter.h" #include "AssetRegistry/AssetData.h" #include "AssetRegistry/AssetRegistryModule.h" #include "CollectionManagerModule.h" #include "Engine/Level.h" #include "Engine/World.h" #include "HAL/FileManager.h" #include "ICollectionContainer.h" #include "ICollectionManager.h" #include "Misc/ConfigCacheIni.h" #include "Misc/PackageName.h" #include "NaniteDisplacedMeshEditorModule.h" #include "NaniteDisplacedMeshLog.h" #include "PackageSourceControlHelper.h" #include "String/ParseTokens.h" #include "UObject/GCObjectScopeGuard.h" #include "UObject/LinkerLoad.h" #include "UObject/ObjectRedirector.h" #include "UObject/ObjectResource.h" #include "UObject/Package.h" #include "UObject/UObjectThreadContext.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(GenerateNaniteDisplacedMeshCommandlet) namespace UE::NaniteDisplacedMesh::Private::GenerateNaniteDisplacedMesh { bool ShouldLoadLevel(const FAssetData& LevelAssetData, const TSet& ImportToLookFor) { if (ImportToLookFor.IsEmpty()) { return true; } if (ULevel::GetIsLevelUsingExternalActorsFromAsset(LevelAssetData)) { return true; } /** * Load_Verify tell the linker to not load the package but it will create the linker if the file exist and is valid. * LOAD_NoVerify tell the linker to not check the import of the package (in the editor this will avoid loading the hard dependencies of the package) * * This will create the package and allow us to check the import map without fully loading the level */ if (UPackage* Package = LoadPackage(nullptr, *LevelAssetData.PackageName.ToString(), LOAD_Verify | LOAD_NoVerify)) { FLinkerLoad* LinkerLoad = Package->GetLinker(); // Clear the load flags so that an load further down line won't be affected by the existing flags LinkerLoad->LoadFlags = LOAD_None; for (const FObjectImport& Dependency : LinkerLoad->ImportMap) { if (Dependency.OuterIndex.IsImport() && !Dependency.ObjectName.IsNone()) { const FObjectImport& PackageDependency = LinkerLoad->ImportMap[Dependency.OuterIndex.ToImport()]; if (PackageDependency.OuterIndex.IsNull()) { FTopLevelAssetPath Import(PackageDependency.ObjectName, Dependency.ObjectName); // Low level optimization here. Only fully load the level if it import an class that might generate an displaced mesh. if (ImportToLookFor.Contains(Import)) { return true; } } } } } return false; } } bool UGenerateNaniteDisplacedMeshCommandlet::IsRunning() { static UClass* ThisCommandlet = StaticClass(); static UClass* RunningCommandlet = GetRunningCommandletClass(); return RunningCommandlet != nullptr && RunningCommandlet->IsChildOf(ThisCommandlet); } int32 UGenerateNaniteDisplacedMeshCommandlet::Main(const FString& CmdLineParams) { const uint64 StartTime = FPlatformTime::Cycles64(); bool bSuccess = true; // Process the arguments TArray Tokens, Switches; TMap Params; ParseCommandLine(*CmdLineParams, Tokens, Switches, Params); const FString CollectionFilter = Params.FindRef(TEXT("GNDMCollectionFilter")); const FString SubmitWithDescription = Params.FindRef(TEXT("GNDMSubmitWithDescription")); const FString ActorClassPathFilterString = Params.FindRef(TEXT("GNDMActorClassPathFilter")); const bool bDeleteUnused = Switches.Contains(TEXT("GNDMDeleteUnused")); FPackageSourceControlHelper SourceControlHelper; UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Revision control enabled: %s"), SourceControlHelper.UseSourceControl() ? TEXT("true") : TEXT("false")); FARFilter Filter; Filter.ClassPaths.Add(UWorld::StaticClass()->GetClassPathName()); Filter.bIncludeOnlyOnDiskAssets = true; if (!CollectionFilter.IsEmpty()) { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Use collection filter: %s"), *CollectionFilter); ICollectionManager& CollectionManager = FCollectionManagerModule::GetModule().Get(); TSharedPtr CollectionContainer; FName CollectionName; ECollectionShareType::Type ShareType = ECollectionShareType::CST_All; if (CollectionManager.TryParseCollectionPath(CollectionFilter, &CollectionContainer, &CollectionName, &ShareType)) { CollectionContainer->GetObjectsInCollection(CollectionName, ShareType, Filter.SoftObjectPaths, ECollectionRecursionFlags::SelfAndChildren); } } TArray ClassesPathForActor; if (!ActorClassPathFilterString.IsEmpty()) { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Use actor class path filter: %s"), *ActorClassPathFilterString); FStringView Argument = ActorClassPathFilterString; TArray ActorClassesString; UE::String::ParseTokens(Argument, TEXT(","), ActorClassesString, UE::String::EParseTokensOptions::SkipEmpty | UE::String::EParseTokensOptions::Trim); ClassesPathForActor.Reserve(ActorClassesString.Num()); for (const FStringView& ClassString : ActorClassesString) { ClassesPathForActor.Emplace(ClassString); } } // Loading the memory settings from the cook int32 ValueInMB; if (GConfig->GetInt(TEXT("CookSettings"), TEXT("MemoryMinFreeVirtual"), ValueInMB, GEditorIni)) { ValueInMB = FMath::Max(ValueInMB, 0); MemoryMinFreeVirtual = ValueInMB * 1024ULL * 1024ULL; UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Loaded MemoryMinFreeVirtual from CookSettings (%d MiB)"), ValueInMB); } if (GConfig->GetInt(TEXT("CookSettings"), TEXT("MemoryMaxUsedVirtual"), ValueInMB, GEditorIni)) { ValueInMB = FMath::Max(ValueInMB, 0); MemoryMaxUsedVirtual = ValueInMB * 1024ULL * 1024ULL; UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Loaded MemoryMaxUsedVirtual from CookSettings (%d MiB)"), ValueInMB); } if (GConfig->GetInt(TEXT("CookSettings"), TEXT("MemoryMinFreePhysical"), ValueInMB, GEditorIni)) { ValueInMB = FMath::Max(ValueInMB, 0); MemoryMinFreePhysical = ValueInMB * 1024ULL * 1024ULL; UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Loaded MemoryMinFreePhysical from CookSettings (%d MiB)"), ValueInMB); } if (GConfig->GetInt(TEXT("CookSettings"), TEXT("MemoryMaxUsedPhysical"), ValueInMB, GEditorIni)) { ValueInMB = FMath::Max(ValueInMB, 0); MemoryMaxUsedPhysical = ValueInMB * 1024ULL * 1024ULL; UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Loaded MemoryMaxUsedPhysical from CookSettings (%d MiB)"), ValueInMB); } IAssetRegistry& AssetRegistry = FAssetRegistryModule::GetRegistry(); // Force the full scan has some recent change have broken the dependency graph if the class of the asset being scanned isn't know yet know by the asset registry //if (UE::AssetRegistry::ShouldSearchAllAssetsAtStart()) //{ // UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Searching the levels that need to be processed and their dependencies (this may take a while)...")); //} //else //{ UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Searching all assets (this may take a while)...")); // This is automatically called in the regular editor but not always when running a commandlet // Must also search synchronously because AssetRegistry.IsLoadingAssets() won't account for this search AssetRegistry.SearchAllAssets(true); //} // Make sure the level are loaded in the asset registry for (const FSoftObjectPath& SoftObjectPaths : Filter.SoftObjectPaths) { AssetRegistry.WaitForPackage(SoftObjectPaths.GetLongPackageName()); } TArray LevelAssets; AssetRegistry.GetAssets(Filter, LevelAssets); // Add dependencies of the levels to list of item that also need to be processed even if they are not in the original request list. TArray LevelAssetsAddedByDependencies; { TSet StopSearchAt; StopSearchAt.Reserve(LevelAssets.Num()); for (const FAssetData& LevelAsset : LevelAssets) { StopSearchAt.Add(LevelAsset.PackageName); } TSet DependenciesToProcess; TArray CurrentDependencies; UE::AssetRegistry::FDependencyQuery QueryFlags; QueryFlags.Required = UE::AssetRegistry::EDependencyProperty::Game | UE::AssetRegistry::EDependencyProperty::Build; for (const FAssetData& LevelAsset : LevelAssets) { // Get the dependencies of the level recursively AssetRegistry.GetDependencies(LevelAsset.PackageName, CurrentDependencies, UE::AssetRegistry::EDependencyCategory::Package, QueryFlags); while (!CurrentDependencies.IsEmpty()) { FName DependendPackageName = CurrentDependencies.Pop(EAllowShrinking::No); if (!StopSearchAt.Contains(DependendPackageName)) { StopSearchAt.Add(DependendPackageName); DependenciesToProcess.Add(DependendPackageName); AssetRegistry.WaitForPackage(DependendPackageName.ToString()); AssetRegistry.GetDependencies(DependendPackageName, CurrentDependencies, UE::AssetRegistry::EDependencyCategory::Package, QueryFlags); } } } /** * Temporary solution to handle the fact that the dependencies created by the content bundles are not present in the asset registry. * This assume that the level instance is created in via an actor that live in a content bundle won't also have a content bundle. */ { AssetRegistry.WaitForCompletion(); FARFilter WorldFilter; WorldFilter.ClassPaths.Add(UWorld::StaticClass()->GetClassPathName()); WorldFilter.bIncludeOnlyOnDiskAssets = true; TArray AllLevels; AssetRegistry.GetAssets(WorldFilter, AllLevels); TSet AlreadyScannedReferences; TSet LevelsName; LevelsName.Reserve(AllLevels.Num()); for (const FAssetData& LevelAsset : AllLevels) { LevelsName.Add(LevelAsset.PackageName); } FString ExternalActorFolder(TEXT("/__ExternalActors__/")); FString ExternalObjectFolder(TEXT("/__ExternalObjects__/")); UE::AssetRegistry::FDependencyQuery WorkAroundQueryFlags; /** * For all levels search their references for 1 level and then search their dependencies for 1 level. * Example: Level referenced by an possible the level instance actor that live in a content bundle. */ for (const FAssetData& LevelAsset : AllLevels) { if (StopSearchAt.Contains(LevelAsset.PackageName)) { continue; } TArray LevelReferencers; AssetRegistry.GetReferencers(LevelAsset.PackageName, LevelReferencers, UE::AssetRegistry::EDependencyCategory::Package, WorkAroundQueryFlags); for (const FName& LevelReference : LevelReferencers) { if (StopSearchAt.Contains(LevelReference)) { DependenciesToProcess.Add(LevelAsset.PackageName); StopSearchAt.Add(LevelAsset.PackageName); break; } if (AlreadyScannedReferences.Contains(LevelReference)) { continue; } bool bHasAddedLevelToDependenciesToProcess = false; // Limit the references search to the external actors and objects FString LevelReferenceAsString = LevelReference.ToString(); if (LevelReferenceAsString.Contains(ExternalActorFolder) || LevelReferenceAsString.Contains(ExternalObjectFolder)) { TArray Dependencies; AssetRegistry.GetDependencies(LevelReference, Dependencies, UE::AssetRegistry::EDependencyCategory::Package, WorkAroundQueryFlags); for (const FName& Dependency : Dependencies) { // Check if the asset is refered in the original dependencies chain and that it is a level. if (StopSearchAt.Contains(Dependency) && LevelsName.Contains(Dependency)) { bHasAddedLevelToDependenciesToProcess = true; DependenciesToProcess.Add(LevelAsset.PackageName); break; } } if (bHasAddedLevelToDependenciesToProcess) { StopSearchAt.Add(LevelReference); break; } else { AlreadyScannedReferences.Add(LevelReference); } } } } } FARFilter DependenciesFilter; DependenciesFilter.ClassPaths.Add(UWorld::StaticClass()->GetClassPathName()); DependenciesFilter.bIncludeOnlyOnDiskAssets = true; DependenciesFilter.PackageNames = DependenciesToProcess.Array(); AssetRegistry.GetAssets(DependenciesFilter, LevelAssetsAddedByDependencies); } // Prepare the actor filters for the level that use world partition or one file per actor { FARFilter ActorsARFilter; ActorsARFilter.ClassPaths = MoveTemp(ClassesPathForActor); ActorsARFilter.bRecursiveClasses = true; ActorsARFilter.bIncludeOnlyOnDiskAssets = true; AssetRegistry.CompileFilter(ActorsARFilter, PreCompiledActorsFilter); // The class filtering doesn't handle the redirector for the blueprints properly so we add them there // Not need to wait for the load of everything in the asset registry as the loading of the dependencies chain should include everything we need TArray Redirectors; AssetRegistry.GetAssetsByClass(UObjectRedirector::StaticClass()->GetClassPathName(), Redirectors); for (const FAssetData& Redirector : Redirectors) { FSoftObjectPath RedirectedPath = AssetRegistry.GetRedirectedObjectPath(Redirector.GetSoftObjectPath()); FTopLevelAssetPath RedirectedPathAsTopLevelAsset(RedirectedPath.GetAssetPath()); if (PreCompiledActorsFilter.ClassPaths.Contains(RedirectedPathAsTopLevelAsset)) { PreCompiledActorsFilter.ClassPaths.Add(FTopLevelAssetPath(Redirector.PackageName, Redirector.AssetName)); } } } FNaniteDisplacedMeshEditorModule& Module = FNaniteDisplacedMeshEditorModule::GetModule(); Module.OverrideNaniteDisplacedMeshLink.BindUObject(this, &UGenerateNaniteDisplacedMeshCommandlet::OnLinkDisplacedMesh); const int32 LevelCount = LevelAssets.Num() + LevelAssetsAddedByDependencies.Num(); UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Processing %d level(s)..."), LevelCount); UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Processing %d level(s) from the original query..."), LevelAssets.Num()); uint32 LevelIndex = 0; for (const FAssetData& LevelAsset : LevelAssets) { ++LevelIndex; UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("-------------------------------------------------------------------")); UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Process Level: %s (%d/%d)"), *LevelAsset.GetSoftObjectPath().ToString(), LevelIndex, LevelCount); LoadLevel(LevelAsset); } UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Processing the %d dependencies of the level(s) ..."), LevelAssetsAddedByDependencies.Num()); for (const FAssetData& LevelAsset : LevelAssetsAddedByDependencies) { ++LevelIndex; UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("-------------------------------------------------------------------")); UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Process Level: %s (%d/%d)"), *LevelAsset.GetSoftObjectPath().ToString(), LevelIndex, LevelCount); UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("This level was added because it's a dependency of one of the processed level(s)")); LoadLevel(LevelAsset); } Module.OverrideNaniteDisplacedMeshLink.Unbind(); UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("======================= All levels processed ======================")); UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Linked %d unique mesh(es)"), LinkedPackageNames.Num()); UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("-------------------------------------------------------------------")); if (LinkedPackageFolders.Num() > 0) { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Find existing meshes (with prefix %s) in %d folder(s):\n\t%s"), LinkedDisplacedMeshAssetNamePrefix, LinkedPackageFolders.Num(), *FString::Join(LinkedPackageFolders, TEXT("\n\t"))); const TSet ExistingPackageNames = GetPackagesInFolders(LinkedPackageFolders, LinkedDisplacedMeshAssetNamePrefix); const TSet UnusedPackageNames = ExistingPackageNames.Difference(LinkedPackageNames); UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("-------------------------------------------------------------------")); if (ExistingPackageNames.Num() > 0) { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Found %d existing mesh(es):\n\t%s"), ExistingPackageNames.Num(), *FString::Join(ExistingPackageNames, TEXT("\n\t"))); } else { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("No existing meshes found")); } UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("-------------------------------------------------------------------")); TArray AddedPackageNamesAsArray = AddedPackageNames.Array(); if (AddedPackageNames.Num() > 0) { // Might be redundant, but not sure if we can rely on the auto check out on save SourceControlHelper.AddToSourceControl(AddedPackageNamesAsArray); UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Found %d added mesh(es):\n\t%s"), AddedPackageNames.Num(), *FString::Join(AddedPackageNames, TEXT("\n\t"))); } else { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("No added meshes found")); } UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("-------------------------------------------------------------------")); if (UnusedPackageNames.Num() > 0) { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Found %d unused mesh(es):\n\t%s"), UnusedPackageNames.Num(), *FString::Join(UnusedPackageNames, TEXT("\n\t"))); } else { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("No unused meshes found")); } UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("-------------------------------------------------------------------")); TArray DeletedFilenames; if (bDeleteUnused) { const TArray PackagesToDelete = UnusedPackageNames.Array(); DeletedFilenames.Append(SourceControlHelpers::PackageFilenames(PackagesToDelete)); if (PackagesToDelete.Num() > 0) { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Deleting %d unused mesh(es)..."), PackagesToDelete.Num()); if (!SourceControlHelper.Delete(PackagesToDelete)) { UE_LOG(LogNaniteDisplacedMesh, Error, TEXT("Failed to delete unused meshes!")); bSuccess = false; } } else { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("No unused meshes to delete")); } UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("-------------------------------------------------------------------")); } if (bSuccess && !SubmitWithDescription.IsEmpty()) { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Use submit description: %s"), *SubmitWithDescription); TArray SubmitFilenames = SourceControlHelpers::PackageFilenames(AddedPackageNamesAsArray); SubmitFilenames.Append(DeletedFilenames); if (SubmitFilenames.Num() > 0) { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Submitting %d changed mesh(es)..."), SubmitFilenames.Num()); if (!SourceControlHelpers::CheckInFiles(SubmitFilenames, SubmitWithDescription)) { UE_LOG(LogNaniteDisplacedMesh, Error, TEXT("Failed to submit changed meshes!")); bSuccess = false; } } else { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("No changed meshes to submit")); } UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("-------------------------------------------------------------------")); } } else { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("No meshes linked in processed levels")); } CollectGarbage(RF_NoFlags); const FPlatformMemoryStats MemoryStats = FPlatformMemory::GetStats(); UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Peak virtual memory usage: %" UINT64_FMT " MiB"), MemoryStats.PeakUsedVirtual / (1024 * 1024)); UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Peak physical memory usage: %" UINT64_FMT " MiB"), MemoryStats.PeakUsedPhysical / (1024 * 1024)); const uint64 EndTime = FPlatformTime::Cycles64(); const int64 DurationInSeconds = FMath::CeilToInt(FPlatformTime::ToSeconds64(EndTime - StartTime)); UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Commandlet duration: %" INT64_FMT " second(s)"), DurationInSeconds); return bSuccess ? 0 : 1; } UNaniteDisplacedMesh* UGenerateNaniteDisplacedMeshCommandlet::OnLinkDisplacedMesh(UNaniteDisplacedMesh* ExistingDisplacedMesh, FValidatedNaniteDisplacedMeshParams&& InParameters, const FNaniteDisplacedMeshLinkParameters& InLinkParameters) { if (!FUObjectThreadContext::Get().IsRoutingPostLoad) { FNaniteDisplacedMeshEditorModule& Module = FNaniteDisplacedMeshEditorModule::GetModule(); Module.OverrideNaniteDisplacedMeshLink.Unbind(); bool bCreatedNewAsset = false; FNaniteDisplacedMeshLinkParameters ModifiedLinkParameter = InLinkParameters; ModifiedLinkParameter.bOutCreatedNewMesh = &bCreatedNewAsset; ModifiedLinkParameter.LinkDisplacedMeshAssetSetting = ELinkDisplacedMeshAssetSetting::LinkAgainstPersistentAsset; UNaniteDisplacedMesh* NaniteDisplacedMesh = LinkDisplacedMeshAsset(ExistingDisplacedMesh, MoveTemp(InParameters), ModifiedLinkParameter); if (InLinkParameters.bOutCreatedNewMesh) { *InLinkParameters.bOutCreatedNewMesh = bCreatedNewAsset; } if (NaniteDisplacedMesh != nullptr) { const FString PackageName = NaniteDisplacedMesh->GetPackage()->GetPathName(); UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Linked mesh: %s"), *PackageName); LinkedPackageNames.Add(PackageName); LinkedPackageFolders.Emplace(InLinkParameters.DisplacedMeshFolder); if (bCreatedNewAsset) { AddedPackageNames.Add(PackageName); } } Module.OverrideNaniteDisplacedMeshLink.BindUObject(this, &UGenerateNaniteDisplacedMeshCommandlet::OnLinkDisplacedMesh); return NaniteDisplacedMesh; } else { FNaniteDisplacedMeshEditorModule& Module = FNaniteDisplacedMeshEditorModule::GetModule(); Module.OverrideNaniteDisplacedMeshLink.Unbind(); FValidatedNaniteDisplacedMeshParams ValidatedParameters = InParameters; UNaniteDisplacedMesh* NaniteDisplacedMesh = LinkDisplacedMeshAsset(ExistingDisplacedMesh, MoveTemp(InParameters), InLinkParameters); // Capture the request but process it later to avoid some issues during the save of the assets QueuedLinkingRequest.Emplace(ExistingDisplacedMesh, MoveTemp(ValidatedParameters), InLinkParameters); Module.OverrideNaniteDisplacedMeshLink.BindUObject(this, &UGenerateNaniteDisplacedMeshCommandlet::OnLinkDisplacedMesh); return NaniteDisplacedMesh; } } void UGenerateNaniteDisplacedMeshCommandlet::LoadLevel(const FAssetData& AssetData) { if (AssetData.GetClass() != UWorld::StaticClass()) { return; } if (!UE::NaniteDisplacedMesh::Private::GenerateNaniteDisplacedMesh::ShouldLoadLevel(AssetData, PreCompiledActorsFilter.ClassPaths)) { return; } TSet LoadTags; // Tell the level to don't the external objects (has we will load those that interest us manually) LoadTags.Add(ULevel::DontLoadExternalObjectsTag); // Finish loading the potentially partially loaded package FLinkerInstancingContext InstancingContext(MoveTemp(LoadTags)); LoadPackage(nullptr, *AssetData.PackageName.ToString(), LOAD_None, nullptr, &InstancingContext); // Get the world UWorld* World = Cast(AssetData.GetAsset(LoadTags)); if (World == nullptr) { return; } World->AddToRoot(); auto ShouldKickGC = [this]() { const FPlatformMemoryStats MemoryStats = FPlatformMemory::GetStats(); if (MemoryMinFreeVirtual > 0 && MemoryStats.AvailableVirtual < MemoryMinFreeVirtual) { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Low virtual memory available (%d MiB). kicking GC."), MemoryStats.AvailableVirtual / 1024 / 1024); return true; } if (MemoryMinFreePhysical > 0 && MemoryStats.AvailablePhysical < MemoryMinFreePhysical) { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Low physical memory available (%d MiB). kicking GC."), MemoryStats.AvailablePhysical / 1024 / 1024); return true; } if (MemoryMaxUsedVirtual > 0 && MemoryStats.UsedVirtual > MemoryMaxUsedVirtual) { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("High virtual memory usage (%d MiB). kicking GC."), MemoryStats.UsedVirtual / 1024 / 1024); return true; } if (MemoryMaxUsedPhysical > 0 && MemoryStats.UsedPhysical > MemoryMaxUsedPhysical) { UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("High physical memory usage (% dMiB). kicking GC."), MemoryStats.UsedPhysical / 1024 / 1024); return true; } return false; }; // Load the external actors (we should look with the open world team to see if there is a better way to do this) if (const ULevel* PersistantLevel = World->PersistentLevel) { if (PersistantLevel->bUseExternalActors) { const FString ExternalActorsPath = ULevel::GetExternalActorsPath(AssetData.PackageName.ToString()); IAssetRegistry& AssetRegistry = FAssetRegistryModule::GetRegistry(); // No need to scan the folder since those are already scanned when we scanned the level dependencies FARFilter ActorsPathFilter; ActorsPathFilter.PackagePaths.Add(FName(*ExternalActorsPath)); ActorsPathFilter.bRecursivePaths = true; ActorsPathFilter.bIncludeOnlyOnDiskAssets = true; FARCompiledFilter CompiledActorsPathFilter; AssetRegistry.CompileFilter(ActorsPathFilter, CompiledActorsPathFilter); PreCompiledActorsFilter.PackagePaths = MoveTemp(CompiledActorsPathFilter.PackagePaths); AssetRegistry.EnumerateAssets(PreCompiledActorsFilter, [this, &ShouldKickGC](const FAssetData& AssetData) { // This will load the actor and the commandlet will capture the emitted linking events. AssetData.GetAsset(); while (!QueuedLinkingRequest.IsEmpty()) { FOnLinkDisplacedMeshArgs LinkDisplacedMeshArgs = QueuedLinkingRequest.Pop(EAllowShrinking::No); OnLinkDisplacedMesh(LinkDisplacedMeshArgs.ExistingNaniteDisplacedMesh.Get(), MoveTemp(LinkDisplacedMeshArgs.Parameters), LinkDisplacedMeshArgs.LinkParameters); } if (ShouldKickGC()) { CollectGarbage(RF_NoFlags); } return true; }); } else { // If the world don't use the external actors we still need to process the linking requests captured and kick the GC if needed while (!QueuedLinkingRequest.IsEmpty()) { FOnLinkDisplacedMeshArgs LinkDisplacedMeshArgs = QueuedLinkingRequest.Pop(EAllowShrinking::No); OnLinkDisplacedMesh(LinkDisplacedMeshArgs.ExistingNaniteDisplacedMesh.Get(), MoveTemp(LinkDisplacedMeshArgs.Parameters), LinkDisplacedMeshArgs.LinkParameters); } if (ShouldKickGC()) { CollectGarbage(RF_NoFlags); } } } World->RemoveFromRoot(); } TSet UGenerateNaniteDisplacedMeshCommandlet::GetPackagesInFolders(const TSet& Folders, const FString& NamePrefix) { TSet PackageNames; const IAssetRegistry& AssetRegistry = FAssetRegistryModule::GetRegistry(); for (const FString& Folder : Folders) { TArray Assets; AssetRegistry.GetAssetsByPath(*Folder, Assets, true, true); const FString PackageNamePrefix = Folder / NamePrefix; for (const FAssetData& Asset : Assets) { const FString PackageName = Asset.PackageName.ToString(); if (PackageName.StartsWith(PackageNamePrefix)) PackageNames.Add(PackageName); } } return MoveTemp(PackageNames); } namespace UE::GenerateNaniteDisplacedMesh::Private { void RunCommandlet(const TArray& Args) { TArray CmdLineArgs; if (Args.IsValidIndex(0)) CmdLineArgs.Add(FString::Printf(TEXT("-GNDMCollectionFilter=\"%s\""), *Args[0])); if (Args.IsValidIndex(1) && Args[1].Equals(TEXT("true"), ESearchCase::IgnoreCase)) CmdLineArgs.Add(TEXT("-GNDMDeleteUnused")); if (Args.IsValidIndex(2)) CmdLineArgs.Add(FString::Printf(TEXT("-GNDMSubmitWithDescription=\"%s\""), *Args[2])); const FString CmdLineParams = FString::Join(CmdLineArgs, TEXT(" ")); UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Run commandlet GenerateNaniteDisplacedMesh: %s"), *CmdLineParams); UGenerateNaniteDisplacedMeshCommandlet* Commandlet = NewObject(); FGCObjectScopeGuard ScopeGuard(Commandlet); Commandlet->Main(CmdLineParams); } static FAutoConsoleCommand ConsoleCommand = FAutoConsoleCommand( TEXT("GenerateNaniteDisplacedMesh"), TEXT("Generate nanite displacement mesh assets"), FConsoleCommandWithArgsDelegate::CreateStatic(&RunCommandlet) ); }