Files
2025-05-18 13:04:45 +08:00

692 lines
27 KiB
C++

// 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<FTopLevelAssetPath>& 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<FString> Tokens, Switches;
TMap<FString, FString> 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<ICollectionContainer> 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<FTopLevelAssetPath> ClassesPathForActor;
if (!ActorClassPathFilterString.IsEmpty())
{
UE_LOG(LogNaniteDisplacedMesh, Display, TEXT("Use actor class path filter: %s"), *ActorClassPathFilterString);
FStringView Argument = ActorClassPathFilterString;
TArray<FStringView> 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<FAssetData> 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<FAssetData> LevelAssetsAddedByDependencies;
{
TSet<FName> StopSearchAt;
StopSearchAt.Reserve(LevelAssets.Num());
for (const FAssetData& LevelAsset : LevelAssets)
{
StopSearchAt.Add(LevelAsset.PackageName);
}
TSet<FName> DependenciesToProcess;
TArray<FName> 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<FAssetData> AllLevels;
AssetRegistry.GetAssets(WorldFilter, AllLevels);
TSet<FName> AlreadyScannedReferences;
TSet<FName> 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<FName> 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<FName> 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<FAssetData> 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<FString> ExistingPackageNames = GetPackagesInFolders(LinkedPackageFolders, LinkedDisplacedMeshAssetNamePrefix);
const TSet<FString> 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<FString> 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<FString> DeletedFilenames;
if (bDeleteUnused)
{
const TArray<FString> 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<FString> 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<FName> 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<UWorld>(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<FString> UGenerateNaniteDisplacedMeshCommandlet::GetPackagesInFolders(const TSet<FString>& Folders, const FString& NamePrefix)
{
TSet<FString> PackageNames;
const IAssetRegistry& AssetRegistry = FAssetRegistryModule::GetRegistry();
for (const FString& Folder : Folders)
{
TArray<FAssetData> 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<FString>& Args)
{
TArray<FString> 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<UGenerateNaniteDisplacedMeshCommandlet>();
FGCObjectScopeGuard ScopeGuard(Commandlet);
Commandlet->Main(CmdLineParams);
}
static FAutoConsoleCommand ConsoleCommand = FAutoConsoleCommand(
TEXT("GenerateNaniteDisplacedMesh"),
TEXT("Generate nanite displacement mesh assets"),
FConsoleCommandWithArgsDelegate::CreateStatic(&RunCommandlet)
);
}