11248 lines
398 KiB
C++
11248 lines
398 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "AssetRegistry.h"
|
|
|
|
#include "Algo/AnyOf.h"
|
|
#include "Algo/HeapSort.h"
|
|
#include "Algo/Transform.h"
|
|
#include "Algo/Unique.h"
|
|
#include "AssetDataGatherer.h"
|
|
#include "AssetDataGathererPrivate.h"
|
|
#include "AssetRegistry/ARFilter.h"
|
|
#include "AssetRegistry/AssetDependencyGatherer.h"
|
|
#include "AssetRegistry/AssetRegistryTelemetry.h"
|
|
#include "AssetRegistryConsoleCommands.h"
|
|
#include "AssetRegistryImpl.h"
|
|
#include "AssetRegistryPrivate.h"
|
|
#include "Async/Async.h"
|
|
#include "Async/ParallelFor.h"
|
|
#include "AutoRTFM.h"
|
|
#include "Blueprint/BlueprintSupport.h"
|
|
#include "DependsNode.h"
|
|
#include "GenericPlatform/GenericPlatformChunkInstall.h"
|
|
#include "GenericPlatform/GenericPlatformFile.h"
|
|
#include "GenericPlatform/GenericPlatformMisc.h"
|
|
#include "HAL/PlatformFileManager.h"
|
|
#include "HAL/PlatformMisc.h"
|
|
#include "HAL/ThreadHeartBeat.h"
|
|
#include "Interfaces/IPluginManager.h"
|
|
#include "IPlatformFilePak.h"
|
|
#include "Math/UnrealMathUtility.h"
|
|
#include "Misc/CommandLine.h"
|
|
#include "Misc/ConfigCacheIni.h"
|
|
#include "Misc/CoreDelegates.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Misc/Optional.h"
|
|
#include "Misc/PackageAccessTracking.h"
|
|
#include "Misc/PackageAccessTrackingOps.h"
|
|
#include "Misc/PackageSegment.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Misc/PathViews.h"
|
|
#include "Misc/RedirectCollector.h"
|
|
#include "Misc/ScopeExit.h"
|
|
#include "Misc/ScopeRWLock.h"
|
|
#include "Misc/TrackedActivity.h"
|
|
#include "Misc/TransactionallySafeRWLock.h"
|
|
#include "AssetRegistry/PackageReader.h"
|
|
#include "Modules/ModuleManager.h"
|
|
#include "ProfilingDebugging/MiscTrace.h"
|
|
#include "Serialization/ArrayReader.h"
|
|
#include "Serialization/CompactBinarySerialization.h"
|
|
#include "Serialization/CompactBinaryWriter.h"
|
|
#include "String/RemoveFrom.h"
|
|
#include "Tasks/Task.h"
|
|
#include "Templates/UnrealTemplate.h"
|
|
#include "TelemetryRouter.h"
|
|
#include "UObject/AssetRegistryTagsContext.h"
|
|
#include "UObject/ConstructorHelpers.h"
|
|
#include "UObject/CoreRedirects.h"
|
|
#include "UObject/MetaData.h"
|
|
#include "UObject/Object.h"
|
|
#include "UObject/SoftObjectPath.h"
|
|
#include "UObject/UObjectGlobals.h"
|
|
#include "UObject/UObjectHash.h"
|
|
#include "UObject/UObjectIterator.h"
|
|
#include "UObject/UObjectThreadContext.h"
|
|
|
|
#include UE_INLINE_GENERATED_CPP_BY_NAME(IAssetRegistry)
|
|
#include UE_INLINE_GENERATED_CPP_BY_NAME(AssetRegistry)
|
|
|
|
#if WITH_EDITOR
|
|
#include "DirectoryWatcherModule.h"
|
|
#include "HAL/IConsoleManager.h"
|
|
#include "HAL/PlatformProcess.h"
|
|
#include "IDirectoryWatcher.h"
|
|
#include "Interfaces/ITargetPlatform.h"
|
|
#include "Interfaces/ITargetPlatformManagerModule.h"
|
|
#endif // WITH_EDITOR
|
|
|
|
#define UE_ENABLE_DIRECTORYWATCH_ROOTS !UE_IS_COOKED_EDITOR
|
|
|
|
/**
|
|
* ********** AssetRegistry threading model **********
|
|
* *** Functions and InterfaceLock ***
|
|
* All data(except events and RWLock) in the AssetRegistry is stored on the FAssetRegistryImpl GuardedData object.
|
|
* No data can be read on GuardedData unless the caller has entered the InterfaceLock.
|
|
* All data on FAssetRegistryImpl is private; this allows us to mark threading model with function prototypes.
|
|
* All functions on FAssetRegistryImpl are intended to be called only within a critical section.
|
|
* const functions require a ReadLock critical section; non-const require a WriteLock.
|
|
* The requirement that functions must be called only from within a critical section(and non-const only within a
|
|
* WriteLock) is not enforced technically; change authors need to carefully follow the synchronization model.
|
|
* FAssetRegistryImpl methods that expect an FInterfaceScope[Read|Write]Lock& may release and re-acquire the lock. Functions
|
|
* re-entering the lock are expected to validate possible re-entrancy issues that may arise from doing so.
|
|
* FAssetRegistryImpl methods that expect an FInterfaceScope[Read|Write]Lock* work like above however passing in nullptr
|
|
* indicates that the function should maintain the lock state from the caller and the caller is responsible for ensuring
|
|
* the threading model is being adhered to.
|
|
|
|
* *** Events, Callbacks, and Object Virtuals ***
|
|
* The AssetRegistry provides several Events(e.g.AssetAddedEvent) that can be subscribed to from arbitrary engine or
|
|
* licensee code, and some functions(e.g.EnumerateAssets) take a callback, and some functions call arbitrary
|
|
* UObject virtuals(e.g. new FAssetData(UObject*)). Some of this arbitrary code can call AssetRegistry functions of
|
|
* their own, and if they were called from within the lock that reentrancy would cause a deadlock when we tried
|
|
* to acquire the RWLock (RWLocks are not reenterable on the same thread). With some exceptions AssetRegistryImpl code
|
|
* is therefore not allowed to call callbacks, send events, or call UObject virtuals from inside a lock.
|
|
|
|
* FEventContext allows deferring events to a point in the top-level interface function outside the lock. The top-level
|
|
* function passes the EventContext in to the GuardedData functions, which add events on to it, and then it broadcasts
|
|
* the events outside the lock. FEventContext also handles deferring events to the Tick function executed from
|
|
* the GameThread, as we have a contract that events are only called from the game thread.
|
|
|
|
* Callbacks are handled on a case-by-case basis; each interface function handles queuing up the data for the callback
|
|
* functions and calling it outside the lock. The one exception is the ShouldSetManager function, which we call
|
|
* from inside the lock, since it is relatively well-behaved code as it is only used by UAssetManager and licensee
|
|
* subclasses of UAssetManager.
|
|
|
|
* UObject virtuals are handled on a case-by-case basis; the primary example is `new FAssetData(UObject*)`, which
|
|
* ProcessLoadedAssetsToUpdateCache takes care to call outside the lock and only on the game thread.
|
|
*
|
|
* *** Updating Caches - InheritanceContext ***
|
|
* The AssetRegistry has a cache for CodeGeneratorClasses and for an InheritanceMap of classes - native and blueprint.
|
|
* Updating these caches needs to be done within a writelock; for CodeGeneratorClasses we do this normally by marking
|
|
* all functions that need to update it as non-const. For InheritanceMap that would be overly pessimistic as several
|
|
* otherwise-const functions need to occasionally update the caches. For InheritanceMap we therefore have
|
|
* FClassInheritanceContext and FClassInheritanceBuffer. The top-level interface functions check whether the
|
|
* inheritance map will need to be updated during their execution, and if so they enter a write lock with the ability
|
|
* to update the members in the InheritanceContext. Otherwise they enter a readlock and the InheritanceBuffer will not
|
|
* be modified. All functions that use the cached data require the InheritanceContext to give them access, to ensure
|
|
* they are only using correctly updated cache data.
|
|
*
|
|
* *** Returning Internal Data ***
|
|
* All interface functions that return internal data return it by copy, or provide a ReadLockEnumerate function that
|
|
* calls a callback under the readlock, where the author of the callback has to ensure other AssetRegistry functions
|
|
* are not called.
|
|
*/
|
|
|
|
static FAssetRegistryConsoleCommands ConsoleCommands; // Registers its various console commands in the constructor
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
const FName WildcardFName(TEXT("*"));
|
|
const FTopLevelAssetPath WildcardPathName(TEXT("/*"), TEXT("*"));
|
|
|
|
const FName Stage_ChunkCountFName(TEXT("Stage_ChunkCount"));
|
|
const FName Stage_ChunkSizeFName(TEXT("Stage_ChunkSize"));
|
|
const FName Stage_ChunkCompressedSizeFName(TEXT("Stage_ChunkCompressedSize"));
|
|
const FName Stage_ChunkInstalledSizeFName(TEXT("Stage_ChunkInstalledSize"));
|
|
const FName Stage_ChunkStreamingSizeFName(TEXT("Stage_ChunkStreamingSize"));
|
|
const FName Stage_ChunkOptionalSizeFName(TEXT("Stage_ChunkOptionalSize"));
|
|
|
|
FString LexToString(EScanFlags Flags)
|
|
{
|
|
const TCHAR* Names[] = {
|
|
TEXT("ForceRescan"),
|
|
TEXT("IgnoreDenyListScanFilters"),
|
|
TEXT("WaitForInMemoryObjects"),
|
|
TEXT("IgnoreInvalidPathWarning")
|
|
};
|
|
|
|
if (Flags == EScanFlags::None)
|
|
{
|
|
return TEXT("None");
|
|
}
|
|
|
|
uint32 AllKnownFlags = (1 << (UE_ARRAY_COUNT(Names)+1)) - 1;
|
|
ensureMsgf(EnumHasAllFlags((EScanFlags)AllKnownFlags, Flags), TEXT("LexToString(UE::AssetRegistry::EScanFlags) is missing some cases"));
|
|
|
|
TStringBuilder<256> Builder;
|
|
for (uint32 i=0; i < UE_ARRAY_COUNT(Names); ++i)
|
|
{
|
|
if (EnumHasAllFlags(Flags, (EScanFlags)(1 << i)))
|
|
{
|
|
if (Builder.Len() != 0)
|
|
{
|
|
Builder << TEXT("|");
|
|
}
|
|
Builder << Names[i];
|
|
}
|
|
}
|
|
|
|
return Builder.ToString();
|
|
}
|
|
}
|
|
|
|
namespace UE::AssetRegistry::Impl
|
|
{
|
|
/** The max time to spend in UAssetRegistryImpl::Tick */
|
|
constexpr float MaxSecondsPerFrameToUseInBlockingInitialLoad = 5.0f;
|
|
float MaxSecondsPerFrame = 0.04f;
|
|
static FAutoConsoleVariableRef CVarAssetRegistryMaxSecondsPerFrame(
|
|
TEXT("AssetRegistry.MaxSecondsPerFrame"),
|
|
UE::AssetRegistry::Impl::MaxSecondsPerFrame,
|
|
TEXT("Maximum amount of time allowed for Asset Registry processing, in seconds"));
|
|
float MaxSecondsPerTickBackgroundThread = 0.1f;
|
|
static FAutoConsoleVariableRef CVarAssetRegistryMaxSecondsPerTickBackgroundThread(
|
|
TEXT("AssetRegistry.MaxSecondsPerTickBackgroundThread"),
|
|
UE::AssetRegistry::Impl::MaxSecondsPerTickBackgroundThread,
|
|
TEXT("Maximum amount of time allowed for Asset Registry processing, in seconds, per iteration on the background thread. Very large values could result in main thread delays due to the background thread holding locks."));
|
|
|
|
/** If true, defer sorting of dependencies until loading is complete */
|
|
bool bDeferDependencySort = false;
|
|
static FAutoConsoleVariableRef CVarAssetRegistryDeferDependencySort(
|
|
TEXT("AssetRegistry.DeferDependencySort"),
|
|
UE::AssetRegistry::Impl::bDeferDependencySort,
|
|
TEXT("If true, the dependency lists on dependency nodes will not be sorted until after the initial load is complete"));
|
|
|
|
/** If true, defer sorting of referencer data until loading is complete, this is enabled by default because of native packages with many referencers */
|
|
bool bDeferReferencerSort = true;
|
|
static FAutoConsoleVariableRef CVarAssetRegistryDeferReferencerSort(
|
|
TEXT("AssetRegistry.DeferReferencerSort"),
|
|
UE::AssetRegistry::Impl::bDeferReferencerSort,
|
|
TEXT("If true, the referencer list on dependency nodes will not be sorted until after the initial load is complete"));
|
|
|
|
bool bDisableDirectoryWatcher = false;
|
|
static FAutoConsoleVariableRef CVarAssetRegistryDisableDirectoryWatcher(
|
|
TEXT("AssetRegistry.DisableDirectoryWatcher"),
|
|
UE::AssetRegistry::Impl::bDisableDirectoryWatcher,
|
|
TEXT("If true, do not listen to mounted directories for file changes"));
|
|
|
|
bool IsDirectoryWatcherEnabled()
|
|
{
|
|
// In-game and in commandlets AR doesn't listen for directory changes
|
|
return !bDisableDirectoryWatcher && GIsEditor && !IsRunningCommandlet();
|
|
}
|
|
|
|
/** Name of UObjectRedirector property */
|
|
const FName DestinationObjectFName(TEXT("DestinationObject"));
|
|
}
|
|
|
|
/**
|
|
* Implementation of IAssetRegistryInterface; forwards calls from the CoreUObject-accessible IAssetRegistryInterface into the AssetRegistry-accessible IAssetRegistry
|
|
*/
|
|
class FAssetRegistryInterface : public IAssetRegistryInterface
|
|
{
|
|
public:
|
|
virtual void GetDependencies(FName InPackageName, TArray<FName>& OutDependencies,
|
|
UE::AssetRegistry::EDependencyCategory Category = UE::AssetRegistry::EDependencyCategory::Package,
|
|
const UE::AssetRegistry::FDependencyQuery& Flags = UE::AssetRegistry::FDependencyQuery()) override
|
|
{
|
|
IAssetRegistry::GetChecked().GetDependencies(InPackageName, OutDependencies, Category, Flags);
|
|
}
|
|
|
|
virtual UE::AssetRegistry::EExists TryGetAssetByObjectPath(const FSoftObjectPath& ObjectPath, FAssetData& OutAssetData) const override
|
|
{
|
|
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
|
|
if (!AssetRegistry)
|
|
{
|
|
return UE::AssetRegistry::EExists::Unknown;
|
|
}
|
|
return AssetRegistry->TryGetAssetByObjectPath(ObjectPath, OutAssetData);
|
|
}
|
|
|
|
virtual UE::AssetRegistry::EExists TryGetAssetPackageData(FName PackageName, class FAssetPackageData& OutPackageData) const override
|
|
{
|
|
FName OutCorrectCasePackageName;
|
|
return TryGetAssetPackageData(PackageName, OutPackageData, OutCorrectCasePackageName);
|
|
}
|
|
|
|
virtual UE::AssetRegistry::EExists TryGetAssetPackageData(FName PackageName, class FAssetPackageData& OutPackageData, FName& OutCorrectCasePackageName) const override
|
|
{
|
|
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
|
|
if (!AssetRegistry)
|
|
{
|
|
return UE::AssetRegistry::EExists::Unknown;
|
|
}
|
|
return AssetRegistry->TryGetAssetPackageData(PackageName, OutPackageData, OutCorrectCasePackageName);
|
|
}
|
|
|
|
virtual bool EnumerateAssets(const FARFilter& Filter, TFunctionRef<bool(const FAssetData&)> Callback,
|
|
UE::AssetRegistry::EEnumerateAssetsFlags InEnumerateFlags) const override
|
|
{
|
|
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
|
|
if (!AssetRegistry)
|
|
{
|
|
return false;
|
|
}
|
|
return AssetRegistry->EnumerateAssets(Filter, Callback, InEnumerateFlags);
|
|
}
|
|
|
|
virtual bool RegisterOnAssetsAddedDelegate(const TFunction<void(TConstArrayView<FAssetData>)>& Function, FDelegateHandle& OutHandle) override
|
|
{
|
|
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
|
|
if (!AssetRegistry)
|
|
{
|
|
return false;
|
|
}
|
|
OutHandle = AssetRegistry->OnAssetsAdded().AddLambda(Function);
|
|
return true;
|
|
}
|
|
|
|
virtual bool UnregisterOnAssetsAddedDelegate(const FDelegateHandle& Handle) override
|
|
{
|
|
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
|
|
if (!AssetRegistry)
|
|
{
|
|
return false;
|
|
}
|
|
return AssetRegistry->OnAssetsAdded().Remove(Handle);
|
|
}
|
|
|
|
virtual bool RegisterOnAssetsRemovedDelegate(const TFunction<void(TConstArrayView<FAssetData>)>& Function, FDelegateHandle& OutHandle) override
|
|
{
|
|
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
|
|
if (!AssetRegistry)
|
|
{
|
|
return false;
|
|
}
|
|
OutHandle = AssetRegistry->OnAssetsRemoved().AddLambda(Function);
|
|
return true;
|
|
}
|
|
|
|
virtual bool UnregisterOnAssetsRemovedDelegate(const FDelegateHandle& Handle) override
|
|
{
|
|
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
|
|
if (!AssetRegistry)
|
|
{
|
|
return false;
|
|
}
|
|
return AssetRegistry->OnAssetsRemoved().Remove(Handle);
|
|
}
|
|
};
|
|
FAssetRegistryInterface GAssetRegistryInterface;
|
|
|
|
// Caching is permanently enabled in editor because memory is not that constrained, disabled by default otherwise
|
|
#define ASSETREGISTRY_CACHE_ALWAYS_ENABLED (WITH_EDITOR)
|
|
|
|
DEFINE_LOG_CATEGORY(LogAssetRegistry);
|
|
|
|
|
|
namespace UE::AssetRegistry::Premade
|
|
{
|
|
|
|
/** Returns whether the given executable configuration supports AssetRegistry Preloading. Called before Main. */
|
|
static bool IsEnabled()
|
|
{
|
|
bool PlatformRequiresCookedData = FPlatformProperties::RequiresCookedData() && (IsRunningGame() || IsRunningDedicatedServer());
|
|
|
|
#if WITH_EDITOR && !ASSETREGISTRY_FORCE_PREMADE_REGISTRY_IN_EDITOR
|
|
bool bUsePremadeInEditor = false;
|
|
if (FCommandLine::IsInitialized())
|
|
{
|
|
bUsePremadeInEditor = FParse::Param(FCommandLine::Get(), TEXT("EnablePremadeAssetRegistry"));
|
|
}
|
|
#else
|
|
constexpr bool bUsePremadeInEditor = WITH_EDITOR;
|
|
#endif
|
|
|
|
return PlatformRequiresCookedData || bUsePremadeInEditor;
|
|
}
|
|
|
|
static bool CanLoadAsync()
|
|
{
|
|
// TaskGraphSystemReady callback doesn't really mean it's running
|
|
return FPlatformProcess::SupportsMultithreading() && FTaskGraphInterface::IsRunning();
|
|
}
|
|
|
|
/** Returns the paths to possible Premade AssetRegistry files, ordered from highest priority to lowest. */
|
|
TArray<FString, TInlineAllocator<2>> GetPriorityPaths()
|
|
{
|
|
TArray<FString, TInlineAllocator<2>> Paths;
|
|
#if WITH_EDITOR
|
|
Paths.Add(FPaths::Combine(FPaths::ProjectDir(), TEXT("EditorClientAssetRegistry.bin")));
|
|
#endif
|
|
Paths.Add(FPaths::Combine(FPaths::ProjectDir(), TEXT("AssetRegistry.bin")));
|
|
return Paths;
|
|
}
|
|
|
|
enum class ELoadResult : uint8
|
|
{
|
|
Succeeded = 0,
|
|
NotFound = 1,
|
|
FailedToLoad = 2,
|
|
Inactive = 3,
|
|
AlreadyConsumed = 4,
|
|
UninitializedMemberLoadResult = 5,
|
|
};
|
|
|
|
// Loads cooked AssetRegistry.bin using an async preload task if available and sync otherwise
|
|
class FPreloader
|
|
{
|
|
public:
|
|
enum class EConsumeResult
|
|
{
|
|
Succeeded,
|
|
Failed,
|
|
Deferred
|
|
};
|
|
using FConsumeFunction = TFunction<void(ELoadResult LoadResult, FAssetRegistryState&& ARState)>;
|
|
|
|
FPreloader()
|
|
{
|
|
//In the editor premade Asset Registry can be enabled by a command line argument so we need to wait until the task graph is ready
|
|
//before we rely on UE::AssetRegistry::Premade::IsEnabled() to return the correct result
|
|
bool PremadeCanBeEnabled =
|
|
#if WITH_EDITOR
|
|
true;
|
|
#else
|
|
UE::AssetRegistry::Premade::IsEnabled();
|
|
#endif
|
|
|
|
if (PremadeCanBeEnabled)
|
|
{
|
|
// run DelayedInitialize when TaskGraph system is ready
|
|
OnTaskGraphReady.Emplace(STATS ? EDelayedRegisterRunPhase::StatSystemReady :
|
|
EDelayedRegisterRunPhase::TaskGraphSystemReady,
|
|
[this]()
|
|
{
|
|
if (UE::AssetRegistry::Premade::IsEnabled())
|
|
{
|
|
LoadState = EState::NotFound;
|
|
DelayedInitialize();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
~FPreloader()
|
|
{
|
|
// We are destructed after Main exits, which means that our AsyncThread was either never called
|
|
// or it was waited on to complete by TaskGraph. Therefore we do not need to handle waiting for it ourselves.
|
|
Shutdown(true /* bFromGlobalDestructor */);
|
|
}
|
|
|
|
/**
|
|
* Block on any pending async load, load if synchronous, and call ConsumeFunction with the results before returning.
|
|
* If Consume has been called previously, the current ConsumeFunction is ignored and this call returns false.
|
|
*
|
|
* @return Whether the load succeeded (this information is also passed to the ConsumeFunction).
|
|
*/
|
|
bool Consume(FConsumeFunction&& ConsumeFunction)
|
|
{
|
|
EConsumeResult Result = ConsumeInternal(MoveTemp(ConsumeFunction), FConsumeFunction());
|
|
check(Result != EConsumeResult::Deferred);
|
|
return Result == EConsumeResult::Succeeded;
|
|
}
|
|
|
|
/**
|
|
* If a load is pending, store ConsumeAsynchronous for later calling and return EConsumeResult::Deferred.
|
|
* If load is complete, or failed, or needs to run synchronously, load if necessary and call ConsumeSynchronous with results before returning.
|
|
* Note if this function returns EConsumeResult::Deferred, the ConsumeAsynchronous will be called from another thread,
|
|
* possibly before this call returns.
|
|
* If Consume has been called previously, this call is ignored and returns EConsumeResult::Failed.
|
|
*
|
|
* @return Whether the load succeeded (this information is also passed to the ConsumeFunction).
|
|
*/
|
|
EConsumeResult ConsumeOrDefer(FConsumeFunction&& ConsumeSynchronous, FConsumeFunction&& ConsumeAsynchronous)
|
|
{
|
|
return ConsumeInternal(MoveTemp(ConsumeSynchronous), MoveTemp(ConsumeAsynchronous));
|
|
}
|
|
|
|
private:
|
|
enum class EState : uint8
|
|
{
|
|
WillNeverPreload,
|
|
LoadSynchronous,
|
|
NotFound,
|
|
Loading,
|
|
Loaded,
|
|
Consumed,
|
|
};
|
|
|
|
|
|
bool TrySetPath()
|
|
{
|
|
for (FString& LocalPath : GetPriorityPaths())
|
|
{
|
|
if (IFileManager::Get().FileExists(*LocalPath))
|
|
{
|
|
ARPath = MoveTemp(LocalPath);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool TrySetPath(const IPakFile& Pak)
|
|
{
|
|
for (FString& LocalPath : GetPriorityPaths())
|
|
{
|
|
if (Pak.PakContains(LocalPath))
|
|
{
|
|
ARPath = MoveTemp(LocalPath);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
ELoadResult TryLoad()
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FCookedAssetRegistryPreloader::TryLoad);
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
checkf(!ARPath.IsEmpty(), TEXT("TryLoad must not be called until after TrySetPath has succeeded."));
|
|
|
|
FAssetRegistryLoadOptions Options;
|
|
const int32 ThreadReduction = 2; // This thread + main thread already has work to do
|
|
int32 MaxWorkers = CanLoadAsync() ? FPlatformMisc::NumberOfCoresIncludingHyperthreads() - ThreadReduction : 0;
|
|
Options.ParallelWorkers = FMath::Clamp(MaxWorkers, 0, 16);
|
|
bool bLoadSucceeded = FAssetRegistryState::LoadFromDisk(*ARPath, Options, Payload);
|
|
UE_CLOG(!bLoadSucceeded, LogAssetRegistry, Warning, TEXT("Premade AssetRegistry path %s existed but failed to load."), *ARPath);
|
|
UE_CLOG(bLoadSucceeded, LogAssetRegistry, Log, TEXT("Premade AssetRegistry loaded from '%s'"), *ARPath);
|
|
LoadResult = bLoadSucceeded ? ELoadResult::Succeeded : ELoadResult::FailedToLoad;
|
|
return LoadResult;
|
|
}
|
|
|
|
void DelayedInitialize()
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FCookedAssetRegistryPreloader::DelayedInitialize);
|
|
// This function will run before any UObject (ie UAssetRegistryImpl) code can run, so we don't need to do any thread safety
|
|
// CanLoadAsync - we have to check this after the task graph is ready
|
|
if (!CanLoadAsync())
|
|
{
|
|
LoadState = EState::LoadSynchronous;
|
|
return;
|
|
}
|
|
|
|
// PreloadReady is in Triggered state until the Async thread is created. It is Reset in KickPreload.
|
|
PreloadReady = FPlatformProcess::GetSynchEventFromPool(true /* bIsManualReset */);
|
|
PreloadReady->Trigger();
|
|
|
|
if (TrySetPath())
|
|
{
|
|
KickPreload();
|
|
}
|
|
else
|
|
{
|
|
// set to NotFound, although PakMounted may set it to found later
|
|
LoadState = EState::NotFound;
|
|
|
|
// The PAK with the main registry isn't mounted yet
|
|
PakMountedDelegate = FCoreDelegates::GetOnPakFileMounted2().AddLambda([this](const IPakFile& Pak)
|
|
{
|
|
FScopeLock Lock(&StateLock);
|
|
if (LoadState == EState::NotFound && TrySetPath(Pak))
|
|
{
|
|
KickPreload();
|
|
// Remove the callback from OnPakFileMounted2 to avoid wasting time in all future PakFile mounts
|
|
// Do not access any of the lambda captures after the call to Remove, because deallocating the
|
|
// DelegateHandle also deallocates our lambda captures
|
|
FDelegateHandle LocalPakMountedDelegate = PakMountedDelegate;
|
|
PakMountedDelegate.Reset();
|
|
FCoreDelegates::GetOnPakFileMounted2().Remove(LocalPakMountedDelegate);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void KickPreload()
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FCookedAssetRegistryPreloader::KickPreload);
|
|
// Called from Within the Lock
|
|
check(LoadState == EState::NotFound && !ARPath.IsEmpty());
|
|
LoadState = EState::Loading;
|
|
PreloadReady->Reset();
|
|
Async(EAsyncExecution::TaskGraph, [this]() { TryLoadAsync(); });
|
|
}
|
|
|
|
void TryLoadAsync()
|
|
{
|
|
// This function is active only after State has been set to Loading and PreloadReady has been Reset
|
|
// Until this function triggers PreloadReady, it has exclusive ownership of bLoadSucceeded and Payload
|
|
// Load outside the lock so that ConsumeOrDefer does not have to wait for the Load before it can defer and exit
|
|
ELoadResult LocalResult = TryLoad();
|
|
// Trigger outside the lock so that a locked Consume function that is waiting on PreloadReady can wait inside the lock.
|
|
PreloadReady->Trigger();
|
|
|
|
FConsumeFunction LocalConsumeCallback;
|
|
{
|
|
FScopeLock Lock(&StateLock);
|
|
// The consume function may have woken up after the trigger and already consumed and changed LoadState to Consumed
|
|
if (LoadState == EState::Loading)
|
|
{
|
|
LoadState = EState::Loaded;
|
|
if (ConsumeCallback)
|
|
{
|
|
LocalConsumeCallback = MoveTemp(ConsumeCallback);
|
|
ConsumeCallback.Reset();
|
|
LoadState = EState::Consumed;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (LocalConsumeCallback)
|
|
{
|
|
// No further threads will read/write payload at this point (until destructor, which is called after all async threads are complete
|
|
// so we can use it outside the lock
|
|
LocalConsumeCallback(LocalResult, MoveTemp(Payload));
|
|
Shutdown();
|
|
}
|
|
}
|
|
|
|
EConsumeResult ConsumeInternal(FConsumeFunction&& ConsumeSynchronous, FConsumeFunction&& ConsumeAsynchronous)
|
|
{
|
|
SCOPED_BOOT_TIMING("FCookedAssetRegistryPreloader::Consume");
|
|
|
|
FScopeLock Lock(&StateLock);
|
|
// Report failure if constructor decided not to preload or this has already been Consumed
|
|
if (LoadState == EState::WillNeverPreload || LoadState == EState::Consumed || ConsumeCallback)
|
|
{
|
|
Lock.Unlock(); // Unlock before calling external code in Consume callback
|
|
ELoadResult LocalResult = (LoadState == EState::Consumed || ConsumeCallback) ? ELoadResult::AlreadyConsumed : ELoadResult::Inactive;
|
|
ConsumeSynchronous(LocalResult, FAssetRegistryState());
|
|
return EConsumeResult::Failed;
|
|
}
|
|
|
|
if (LoadState == EState::LoadSynchronous)
|
|
{
|
|
ELoadResult LocalResult = TrySetPath() ? TryLoad() : ELoadResult::NotFound;
|
|
LoadState = EState::Consumed;
|
|
Lock.Unlock(); // Unlock before calling external code in Consume callback
|
|
ConsumeSynchronous(LocalResult, MoveTemp(Payload));
|
|
Shutdown(); // Shutdown can be called outside the lock since AsyncThread doesn't exist
|
|
return LocalResult == ELoadResult::Succeeded ? EConsumeResult::Succeeded : EConsumeResult::Failed;
|
|
}
|
|
|
|
// Cancel any further searching in Paks since we will no longer accept preloads starting after this point
|
|
FCoreDelegates::GetOnPakFileMounted2().Remove(PakMountedDelegate);
|
|
PakMountedDelegate.Reset();
|
|
|
|
if (ConsumeAsynchronous && LoadState == EState::Loading)
|
|
{
|
|
// The load might have completed and the TryAsyncLoad thread is waiting to enter the lock, but we will still defer since Consume won the race
|
|
ConsumeCallback = MoveTemp(ConsumeAsynchronous);
|
|
return EConsumeResult::Deferred;
|
|
}
|
|
|
|
{
|
|
SCOPED_BOOT_TIMING("BlockingConsume");
|
|
// If the load is in progress, wait for it to finish (which it does outside the lock)
|
|
PreloadReady->Wait();
|
|
}
|
|
|
|
// TryAsyncLoad might not yet have set state to Loaded
|
|
check(LoadState == EState::Loaded || LoadState == EState::Loading || LoadState == EState::NotFound);
|
|
ELoadResult LocalResult = LoadState == EState::NotFound ? ELoadResult::NotFound : LoadResult;
|
|
LoadState = EState::Consumed;
|
|
|
|
// No further async threads exist that will read/write payload at this point so we can use it outside the lock
|
|
Lock.Unlock(); // Unlock before calling external code in Consume callback
|
|
ConsumeSynchronous(LocalResult, MoveTemp(Payload));
|
|
Shutdown(); // Shutdown can be called outside the lock since we have set state to Consumed and the Async thread will notice and exit
|
|
return LocalResult == ELoadResult::Succeeded ? EConsumeResult::Succeeded : EConsumeResult::Failed;
|
|
}
|
|
|
|
/** Called when the Preloader has no further work to do, to free resources early since destruction occurs at end of process. */
|
|
void Shutdown(bool bFromGlobalDestructor = false)
|
|
{
|
|
OnTaskGraphReady.Reset();
|
|
if (PreloadReady)
|
|
{
|
|
// If we are exiting the process early while PreloadReady is still allocated, the event
|
|
// system has already been torn down and there is nothing for us to free for PreloadReady.
|
|
if (!bFromGlobalDestructor)
|
|
{
|
|
FPlatformProcess::ReturnSynchEventToPool(PreloadReady);
|
|
}
|
|
PreloadReady = nullptr;
|
|
}
|
|
ARPath.Reset();
|
|
Payload.Reset();
|
|
}
|
|
|
|
/** simple way to trigger a callback at a specific time that TaskGraph is usable. */
|
|
TOptional<FDelayedAutoRegisterHelper> OnTaskGraphReady;
|
|
|
|
/** Lock that guards members on this (see notes on each member). */
|
|
FCriticalSection StateLock;
|
|
/** Trigger for blocking Consume to wait upon TryLoadAsync. This Trigger is only allocated when in the states NotFound, Loaded, Loading. */
|
|
FEvent* PreloadReady = nullptr;
|
|
|
|
/** Path discovered for the AssetRegistry; Read/Write only within the Lock. */
|
|
FString ARPath;
|
|
|
|
/**
|
|
* The ARState loaded from disk. Owned exclusively by either the first Consume or by TryAsyncLoad.
|
|
* If LoadState is never set to Loading, this state is read/written only by the first thread to call Consume.
|
|
* If LoadState is set to Loading (which happens before threading starts), the thread running TryAsyncLoad
|
|
* owns this payload until it triggers PayloadReady, after which ownership returns to the first thread to call Consume.
|
|
*/
|
|
FAssetRegistryState Payload;
|
|
|
|
FDelegateHandle PakMountedDelegate;
|
|
|
|
/** Callback from ConsumeOrDefer that is set so TryLoadAsync can trigger the Consume when it completes.Read / Write only within the lock. */
|
|
FConsumeFunction ConsumeCallback;
|
|
|
|
/** State machine state. Read/Write only within the lock (or before threading starts). */
|
|
EState LoadState = EState::WillNeverPreload;
|
|
|
|
/** Result of TryLoad.Thread ownership rules are the same as the rules for Payload. */
|
|
ELoadResult LoadResult = ELoadResult::UninitializedMemberLoadResult;
|
|
}
|
|
GPreloader;
|
|
|
|
FAsyncConsumer::~FAsyncConsumer()
|
|
{
|
|
if (Consumed)
|
|
{
|
|
FPlatformProcess::ReturnSynchEventToPool(Consumed);
|
|
Consumed = nullptr;
|
|
}
|
|
}
|
|
|
|
void FAsyncConsumer::PrepareForConsume()
|
|
{
|
|
// Called within the lock
|
|
check(!Consumed);
|
|
Consumed = FPlatformProcess::GetSynchEventFromPool(true /* bIsManualReset */);
|
|
++ReferenceCount;
|
|
};
|
|
|
|
void FAsyncConsumer::Wait(UE::AssetRegistry::FInterfaceWriteScopeLock& ScopeLock)
|
|
{
|
|
// Called within the lock
|
|
if (ReferenceCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
++ReferenceCount;
|
|
|
|
// Wait outside of the lock so that the AsyncThread can enter the lock to call Consume
|
|
{
|
|
ScopeLock.Lock.WriteUnlock();
|
|
ON_SCOPE_EXIT{ ScopeLock.Lock.WriteLock(); };
|
|
check(Consumed != nullptr);
|
|
Consumed->Wait();
|
|
}
|
|
|
|
--ReferenceCount;
|
|
if (ReferenceCount == 0)
|
|
{
|
|
// We're the last one to drop the refcount, so delete Consumed
|
|
check(Consumed != nullptr);
|
|
FPlatformProcess::ReturnSynchEventToPool(Consumed);
|
|
Consumed = nullptr;
|
|
}
|
|
}
|
|
|
|
void FAsyncConsumer::Consume(UAssetRegistryImpl& UARI, UE::AssetRegistry::Impl::FEventContext& EventContext, ELoadResult LoadResult, FAssetRegistryState&& ARState)
|
|
{
|
|
// Called within the lock
|
|
UARI.GuardedData.LoadPremadeAssetRegistry(EventContext, LoadResult, MoveTemp(ARState));
|
|
check(ReferenceCount >= 1);
|
|
check(Consumed != nullptr);
|
|
Consumed->Trigger();
|
|
--ReferenceCount;
|
|
if (ReferenceCount == 0)
|
|
{
|
|
// We're the last one to drop the refcount, so delete Consumed
|
|
FPlatformProcess::ReturnSynchEventToPool(Consumed);
|
|
Consumed = nullptr;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
namespace DependsNode
|
|
{
|
|
typedef TMap<FDependsNode*, FDependsNode::FPackageFlagSet> FPackageDependencyMap;
|
|
|
|
/** Uncompressed version of DependsNode data, used to avoid allocations and
|
|
compression cost when constructing batches of DependsNodes in DependencyDataGathered. */
|
|
struct FConstructData
|
|
{
|
|
FDependsNode* SourceNode;
|
|
FPackageDependencyMap PackageDependencies;
|
|
FDependsNode::FDependsNodeList SearchableNamesNodes;
|
|
};
|
|
|
|
struct FGatherDependencyDataScratchpad
|
|
{
|
|
TMap<FName, FConstructData> AssetNameToConstructDataMap;
|
|
TMap<FName, FName> CachedDepToRedirect;
|
|
TSet<FDependsNode*> ModifiedDependNodes;
|
|
|
|
void Reset()
|
|
{
|
|
AssetNameToConstructDataMap.Reset();
|
|
CachedDepToRedirect.Reset();
|
|
ModifiedDependNodes.Reset();
|
|
}
|
|
|
|
void Shrink()
|
|
{
|
|
AssetNameToConstructDataMap.Shrink();
|
|
CachedDepToRedirect.Shrink();
|
|
ModifiedDependNodes.Shrink();
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
void FAssetRegistryImpl::ConditionalLoadPremadeAssetRegistry(UE::AssetRegistry::FInterfaceWriteScopeLock& ScopeLock, Impl::FEventContext& EventContext)
|
|
{
|
|
AsyncConsumer.Wait(ScopeLock);
|
|
}
|
|
|
|
void FAssetRegistryImpl::ConsumeOrDeferPreloadedPremade(UAssetRegistryImpl& UARI, Impl::FEventContext& EventContext)
|
|
{
|
|
// Called from inside WriteLock on InterfaceLock
|
|
using namespace UE::AssetRegistry::Premade;
|
|
if (!Premade::IsEnabled())
|
|
{
|
|
// if we aren't doing any preloading, then we can set the initial search is done right away.
|
|
// Otherwise, it is set from LoadPremadeAssetRegistry
|
|
bPreloadingComplete = true;
|
|
return;
|
|
}
|
|
|
|
if (Premade::CanLoadAsync())
|
|
{
|
|
FPreloader::FConsumeFunction ConsumeFromAsyncThread = [this, &UARI](Premade::ELoadResult LoadResult, FAssetRegistryState&& ARState)
|
|
{
|
|
Impl::FEventContext EventContext;
|
|
{
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(UARI.InterfaceLock);
|
|
AsyncConsumer.Consume(UARI, EventContext, LoadResult, MoveTemp(ARState));
|
|
}
|
|
UARI.Broadcast(EventContext);
|
|
};
|
|
auto ConsumeOnCurrentThread = [ConsumeFromAsyncThread](Premade::ELoadResult LoadResult, FAssetRegistryState&& ARState) mutable
|
|
{
|
|
Async(EAsyncExecution::TaskGraph, [LoadResult, ARState=MoveTemp(ARState), ConsumeFromAsyncThread=MoveTemp(ConsumeFromAsyncThread)]() mutable
|
|
{
|
|
ConsumeFromAsyncThread(LoadResult, MoveTemp(ARState));
|
|
});
|
|
};
|
|
|
|
AsyncConsumer.PrepareForConsume();
|
|
GPreloader.ConsumeOrDefer(MoveTemp(ConsumeOnCurrentThread), MoveTemp(ConsumeFromAsyncThread));
|
|
}
|
|
else
|
|
{
|
|
GPreloader.Consume([this, &EventContext](Premade::ELoadResult LoadResult, FAssetRegistryState&& ARState)
|
|
{
|
|
LoadPremadeAssetRegistry(EventContext, LoadResult, MoveTemp(ARState));
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Returns the appropriate ChunkProgressReportingType for the given Asset enum */
|
|
EChunkProgressReportingType::Type GetChunkAvailabilityProgressType(EAssetAvailabilityProgressReportingType::Type ReportType)
|
|
{
|
|
EChunkProgressReportingType::Type ChunkReportType;
|
|
switch (ReportType)
|
|
{
|
|
case EAssetAvailabilityProgressReportingType::ETA:
|
|
ChunkReportType = EChunkProgressReportingType::ETA;
|
|
break;
|
|
case EAssetAvailabilityProgressReportingType::PercentageComplete:
|
|
ChunkReportType = EChunkProgressReportingType::PercentageComplete;
|
|
break;
|
|
default:
|
|
ChunkReportType = EChunkProgressReportingType::PercentageComplete;
|
|
UE_LOG(LogAssetRegistry, Error, TEXT("Unsupported assetregistry report type: %i"), (int)ReportType);
|
|
break;
|
|
}
|
|
return ChunkReportType;
|
|
}
|
|
|
|
const TCHAR* GetDevelopmentAssetRegistryFilename()
|
|
{
|
|
return TEXT("DevelopmentAssetRegistry.bin");
|
|
}
|
|
|
|
FAssetData IAssetRegistry::K2_GetAssetByObjectPath(const FSoftObjectPath& ObjectPath, bool bIncludeOnlyOnDiskAssets, bool bSkipARFilteredAssets) const
|
|
{
|
|
return GetAssetByObjectPath(ObjectPath, bIncludeOnlyOnDiskAssets, bSkipARFilteredAssets);
|
|
}
|
|
|
|
void IAssetRegistry::InitializeSerializationOptions(FAssetRegistrySerializationOptions& Options,
|
|
const FString& PlatformIniName,
|
|
UE::AssetRegistry::ESerializationTarget Target) const
|
|
{
|
|
#if WITH_EDITOR
|
|
if (ITargetPlatformManagerModule* TargetPlatformManager = GetTargetPlatformManager())
|
|
{
|
|
for (ITargetPlatform* TargetPlatform : TargetPlatformManager->GetActiveTargetPlatforms())
|
|
{
|
|
if (TargetPlatform->IniPlatformName() == PlatformIniName)
|
|
{
|
|
return InitializeSerializationOptions(Options, TargetPlatform, Target);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (ensureMsgf(PlatformIniName.IsEmpty(), TEXT("Couldn't find TargetPlatform from ini `%s`"), *PlatformIniName)) {
|
|
return InitializeSerializationOptions(Options, nullptr, Target);
|
|
}
|
|
}
|
|
|
|
IAssetRegistry::FLoadPackageRegistryData::FLoadPackageRegistryData(bool bInGetDependencies)
|
|
: bGetDependencies(bInGetDependencies)
|
|
{
|
|
}
|
|
|
|
IAssetRegistry::FLoadPackageRegistryData::~FLoadPackageRegistryData() = default;
|
|
|
|
|
|
UAssetRegistry::UAssetRegistry(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
}
|
|
|
|
namespace UE::AssetRegistry::Impl
|
|
{
|
|
|
|
struct FInitializeContext
|
|
{
|
|
UAssetRegistryImpl& UARI;
|
|
FEventContext Events;
|
|
FClassInheritanceContext InheritanceContext;
|
|
FClassInheritanceBuffer InheritanceBuffer;
|
|
TArray<FString> RootContentPaths;
|
|
bool bRedirectorsNeedSubscribe = false;
|
|
bool bUpdateDiskCacheAfterLoad = false;
|
|
bool bNeedsSearchAllAssetsAtStartSynchronous = false;
|
|
};
|
|
|
|
}
|
|
|
|
UAssetRegistryImpl::UAssetRegistryImpl(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
SCOPED_BOOT_TIMING("UAssetRegistryImpl::UAssetRegistryImpl");
|
|
|
|
UE::AssetRegistry::Impl::FInitializeContext Context{ *this };
|
|
|
|
if (HasAnyFlags(RF_ClassDefaultObject) && !HasAnyFlags(RF_ImmutableDefaultObject))
|
|
{
|
|
check(UE::AssetRegistry::Private::IAssetRegistrySingleton::Singleton == nullptr && IAssetRegistryInterface::Default == nullptr);
|
|
UE::AssetRegistry::Private::IAssetRegistrySingleton::Singleton = this;
|
|
IAssetRegistryInterface::Default = &GAssetRegistryInterface;
|
|
}
|
|
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GetInheritanceContextWithRequiredLock(InterfaceScopeLock, Context.InheritanceContext,
|
|
Context.InheritanceBuffer);
|
|
|
|
GuardedData.Initialize(InterfaceScopeLock, Context);
|
|
InitializeEvents(Context);
|
|
}
|
|
Broadcast(Context.Events);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::IsPathBeautificationNeeded(const FString& InAssetPath) const
|
|
{
|
|
return InAssetPath.Contains(FPackagePath::GetExternalActorsFolderName()) || InAssetPath.Contains(FPackagePath::GetExternalObjectsFolderName());
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
FAssetRegistryImpl::FAssetRegistryImpl()
|
|
{
|
|
}
|
|
|
|
void FAssetRegistryImpl::LoadPremadeAssetRegistry(Impl::FEventContext& EventContext,
|
|
Premade::ELoadResult LoadResult, FAssetRegistryState&& ARState)
|
|
{
|
|
SCOPED_BOOT_TIMING("LoadPremadeAssetRegistry");
|
|
UE_SCOPED_ENGINE_ACTIVITY("Loading premade asset registry");
|
|
|
|
const bool bEmitAssetEvents = GIsEditor;
|
|
|
|
if (SerializationOptions.bSerializeAssetRegistry)
|
|
{
|
|
SCOPED_BOOT_TIMING("LoadPremadeAssetRegistry_Main");
|
|
if (LoadResult == Premade::ELoadResult::Succeeded)
|
|
{
|
|
if (State.GetNumAssets() == 0)
|
|
{
|
|
State = MoveTemp(ARState);
|
|
CachePathsFromState(EventContext, State);
|
|
if (bEmitAssetEvents)
|
|
{
|
|
EventContext.AssetEvents.Reserve(State.GetNumAssets());
|
|
State.EnumerateAllAssets([&EventContext](const FAssetData& AssetData)
|
|
{
|
|
EventContext.AssetEvents.Emplace(AssetData, UE::AssetRegistry::Impl::FEventContext::EEvent::Added);
|
|
});
|
|
}
|
|
}
|
|
else if (State.GetNumAssets() < ARState.GetNumAssets())
|
|
{
|
|
FAssetRegistryState ExistingState = MoveTemp(State);
|
|
State = MoveTemp(ARState);
|
|
CachePathsFromState(EventContext, State);
|
|
if (bEmitAssetEvents)
|
|
{
|
|
EventContext.AssetEvents.Reserve(State.GetNumAssets());
|
|
State.EnumerateAllAssets([&EventContext](const FAssetData& AssetData)
|
|
{
|
|
EventContext.AssetEvents.Emplace(AssetData, UE::AssetRegistry::Impl::FEventContext::EEvent::Added);
|
|
});
|
|
}
|
|
AppendState(EventContext, ExistingState);
|
|
}
|
|
else
|
|
{
|
|
AppendState(EventContext, ARState, FAssetRegistryState::EInitializationMode::OnlyUpdateNew, bEmitAssetEvents);
|
|
}
|
|
UpdatePersistentMountPoints();
|
|
State.bCookedGlobalAssetRegistryState = true;
|
|
}
|
|
else
|
|
{
|
|
UE_CLOG(FPlatformProperties::RequiresCookedData() && (IsRunningGame() || IsRunningDedicatedServer()),
|
|
LogAssetRegistry, Error, TEXT("Failed to load premade asset registry. LoadResult == %d."), static_cast<int32>(LoadResult));
|
|
}
|
|
}
|
|
|
|
{
|
|
SCOPED_BOOT_TIMING("LoadPremadeAssetRegistry_Plugins");
|
|
FPakPlatformFile* PakPlatformFile = (FPakPlatformFile*)(FPlatformFileManager::Get().FindPlatformFile(FPakPlatformFile::GetTypeName()));
|
|
TArray<TSharedRef<IPlugin>> ContentPlugins = IPluginManager::Get().GetEnabledPluginsWithContent();
|
|
for (const TSharedRef<IPlugin>& ContentPlugin : ContentPlugins)
|
|
{
|
|
if (ContentPlugin->CanContainContent())
|
|
{
|
|
FArrayReader SerializedAssetData;
|
|
FString PluginAssetRegistry = ContentPlugin->GetBaseDir() / TEXT("AssetRegistry.bin");
|
|
|
|
// Optimization: if we're using pak files then only search paks (avoid unnecessary fallback to loose)
|
|
bool bFileExists = PakPlatformFile != nullptr ? PakPlatformFile->FindFileInPakFiles(*PluginAssetRegistry) : IFileManager::Get().FileExists(*PluginAssetRegistry);
|
|
if (bFileExists && FFileHelper::LoadFileToArray(SerializedAssetData, *PluginAssetRegistry))
|
|
{
|
|
SerializedAssetData.Seek(0);
|
|
FAssetRegistryState PluginState;
|
|
PluginState.Load(SerializedAssetData);
|
|
|
|
#if WITH_EDITOR
|
|
/*
|
|
* Only update the new assets when using a premade asset registry in editor.
|
|
* The main state will often already include the DLC/plugin assets and is often in a development mode where the plugin state will not be.
|
|
* If we update the existing assets in those cases it will be causing a lost of tags and values that are needed for the editor systems.
|
|
*/
|
|
AppendState(EventContext, PluginState, FAssetRegistryState::EInitializationMode::OnlyUpdateNew, bEmitAssetEvents);
|
|
#else
|
|
AppendState(EventContext, PluginState, FAssetRegistryState::EInitializationMode::Append, bEmitAssetEvents);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// let Tick know that it can finalize the initial search
|
|
bPreloadingComplete = true;
|
|
}
|
|
|
|
void FAssetRegistryImpl::Initialize(UE::AssetRegistry::FInterfaceWriteScopeLock& ScopeLock, Impl::FInitializeContext& Context)
|
|
{
|
|
if (bInitializationComplete)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const double StartupStartTime = FPlatformTime::Seconds();
|
|
|
|
bInitialSearchCompleted.store(true, std::memory_order_relaxed);
|
|
bAdditionalMountSearchInProgress.store(false, std::memory_order_relaxed);
|
|
#if WITH_EDITOR
|
|
SetGameThreadTakeOverGatherEachTick(false);
|
|
#endif
|
|
|
|
UpdateMaxSecondsPerFrame();
|
|
GatherStatus = Impl::EGatherStatus::TickActiveGatherActive;
|
|
PerformanceMode = Impl::EPerformanceMode::MostlyStatic;
|
|
|
|
bSearchAllAssets = false;
|
|
#if NO_LOGGING
|
|
bVerboseLogging = false;
|
|
#else
|
|
bVerboseLogging = LogAssetRegistry.GetVerbosity() >= ELogVerbosity::Verbose;
|
|
#endif
|
|
StoreGatherResultsTimeSeconds = 0.f;
|
|
|
|
// By default update the disk cache once on asset load, to incorporate changes made in PostLoad. This only happens in editor builds
|
|
#if !WITH_EDITOR
|
|
Context.bUpdateDiskCacheAfterLoad = false;
|
|
#else
|
|
if (IsRunningCookCommandlet())
|
|
{
|
|
Context.bUpdateDiskCacheAfterLoad = false;
|
|
}
|
|
else
|
|
{
|
|
Context.bUpdateDiskCacheAfterLoad = true;
|
|
if (GConfig)
|
|
{
|
|
GConfig->GetBool(TEXT("AssetRegistry"), TEXT("bUpdateDiskCacheAfterLoad"), Context.bUpdateDiskCacheAfterLoad, GEngineIni);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
bIsTempCachingAlwaysEnabled = ASSETREGISTRY_CACHE_ALWAYS_ENABLED;
|
|
bIsTempCachingEnabled = bIsTempCachingAlwaysEnabled;
|
|
TempCachedInheritanceBuffer.bDirty = true;
|
|
|
|
SavedGeneratorClassesVersionNumber = MAX_uint64;
|
|
SavedAllClassesVersionNumber = MAX_uint64;
|
|
|
|
// By default do not double check mount points are still valid when gathering new assets
|
|
bVerifyMountPointAfterGather = false;
|
|
|
|
#if WITH_EDITOR
|
|
if (GIsEditor)
|
|
{
|
|
// Double check mount point is still valid because it could have been unmounted
|
|
bVerifyMountPointAfterGather = true;
|
|
}
|
|
#endif // WITH_EDITOR
|
|
|
|
// Collect all code generator classes (currently BlueprintCore-derived ones)
|
|
CollectCodeGeneratorClasses();
|
|
#if WITH_ENGINE && WITH_EDITOR
|
|
{
|
|
UE::TWriteScopeLock WriteLock(SkipClassesLock);
|
|
Utils::PopulateSkipClasses(SkipUncookedClasses, SkipCookedClasses);
|
|
}
|
|
#endif
|
|
|
|
// Read default serialization options
|
|
const bool bSuppressWarnings = true;
|
|
Utils::InitializeSerializationOptionsFromIni(SerializationOptions, &CachedParsedFilterRules, nullptr, UE::AssetRegistry::ESerializationTarget::ForGame, bSuppressWarnings);
|
|
Utils::InitializeSerializationOptionsFromIni(DevelopmentSerializationOptions, nullptr, nullptr, UE::AssetRegistry::ESerializationTarget::ForDevelopment, bSuppressWarnings);
|
|
|
|
bool bStartedAsyncGather = false;
|
|
if (ShouldSearchAllAssetsAtStart())
|
|
{
|
|
verify(TryConstructGathererIfNeeded());
|
|
|
|
if (GlobalGatherer->IsAsyncEnabled())
|
|
{
|
|
SearchAllAssetsInitialAsync(Context.Events, Context.InheritanceContext);
|
|
bStartedAsyncGather = true;
|
|
}
|
|
else
|
|
{
|
|
// For the Editor and editor game we need to take responsibility for the synchronous search;
|
|
// Commandlets and cooked game will handle it themselves.
|
|
#if WITH_EDITOR
|
|
Context.bNeedsSearchAllAssetsAtStartSynchronous = !IsRunningCommandlet();
|
|
#else
|
|
Context.bNeedsSearchAllAssetsAtStartSynchronous = false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
ConsumeOrDeferPreloadedPremade(Context.UARI, Context.Events);
|
|
|
|
// Report startup time. This does not include DirectoryWatcher startup time.
|
|
double StartupDuration = FPlatformTime::Seconds() - StartupStartTime;
|
|
UE_LOG(LogAssetRegistry, Log, TEXT("FAssetRegistry took %0.4f seconds to start up"), StartupDuration);
|
|
|
|
FTelemetryRouter::Get().ProvideTelemetry<UE::Telemetry::AssetRegistry::FStartupTelemetry>({
|
|
StartupDuration,
|
|
bStartedAsyncGather
|
|
});
|
|
|
|
// Content roots always exist; add them as paths
|
|
FPackageName::QueryRootContentPaths(Context.RootContentPaths, false, false, true);
|
|
for (const FString& AssetPath : Context.RootContentPaths)
|
|
{
|
|
AddPath(Context.Events, AssetPath);
|
|
}
|
|
|
|
InitRedirectors(Context.Events, Context.InheritanceContext, Context.bRedirectorsNeedSubscribe);
|
|
|
|
#if WITH_EDITOR
|
|
// Make sure first call to LoadCalculatedDependencies builds the Gatherer list. At that point Classes should be loaded.
|
|
bRegisteredDependencyGathererClassesDirty = true;
|
|
#endif
|
|
|
|
bInitializationComplete = true;
|
|
}
|
|
|
|
}
|
|
|
|
IAssetRegistry::FAssetCollisionEvent& UE::AssetRegistry::FAssetRegistryImpl::OnAssetCollision_Private()
|
|
{
|
|
return AssetCollisionEvent;
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
#if WITH_EDITOR
|
|
void FAssetRegistryImpl::RebuildAssetDependencyGathererMapIfNeeded()
|
|
{
|
|
if (!bRegisteredDependencyGathererClassesDirty)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FWriteScopeLock ScopeLock(RegisteredDependencyGathererClassesLock);
|
|
|
|
RegisteredDependencyGathererClasses.Reset();
|
|
|
|
TArray<UObject*> Classes;
|
|
GetObjectsOfClass(UClass::StaticClass(), Classes);
|
|
|
|
/** Per Class dependency gatherers */
|
|
UE::AssetDependencyGatherer::Private::FRegisteredAssetDependencyGatherer::ForEach([&](UE::AssetDependencyGatherer::Private::FRegisteredAssetDependencyGatherer* RegisteredAssetDependencyGatherer)
|
|
{
|
|
UClass* AssetClass = RegisteredAssetDependencyGatherer->GetAssetClass();
|
|
for (UObject* ClassObject : Classes)
|
|
{
|
|
if (UClass* Class = Cast<UClass>(ClassObject); Class && Class->IsChildOf(AssetClass) && !Class->HasAnyClassFlags(CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists))
|
|
{
|
|
RegisteredDependencyGathererClasses.Add(FTopLevelAssetPath(Class), RegisteredAssetDependencyGatherer);
|
|
}
|
|
}
|
|
});
|
|
|
|
bRegisteredDependencyGathererClassesDirty = false;
|
|
}
|
|
|
|
void FAssetRegistryImpl::ResolveRedirectionsInCachedBPInheritanceMap()
|
|
{
|
|
for (TPair<FTopLevelAssetPath, FTopLevelAssetPath>& Pair : CachedBPInheritanceMap)
|
|
{
|
|
FSoftObjectPath BaseClassPath(Pair.Value);
|
|
FSoftObjectPath TargetBaseClassPath = GRedirectCollector.GetAssetPathRedirection(BaseClassPath);
|
|
if (TargetBaseClassPath.IsAsset())
|
|
{
|
|
Pair.Value = TargetBaseClassPath.GetAssetPath();
|
|
TempCachedInheritanceBuffer.bDirty = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::InitializeEvents(UE::AssetRegistry::Impl::FInitializeContext& Context)
|
|
{
|
|
if (Context.bRedirectorsNeedSubscribe)
|
|
{
|
|
TDelegate<bool(const FString&, FString&)> PackageResolveDelegate;
|
|
PackageResolveDelegate.BindUObject(this, &UAssetRegistryImpl::OnResolveRedirect);
|
|
FCoreDelegates::PackageNameResolvers.Add(PackageResolveDelegate);
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
if (UE::AssetRegistry::Impl::IsDirectoryWatcherEnabled())
|
|
{
|
|
FDirectoryWatcherModule& DirectoryWatcherModule = FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>(TEXT("DirectoryWatcher"));
|
|
IDirectoryWatcher* DirectoryWatcher = DirectoryWatcherModule.Get();
|
|
|
|
if (DirectoryWatcher)
|
|
{
|
|
// The vast majority of directories we are watching are below the Plugin directories. The memory cost per watch
|
|
// is sufficiently high to want to avoid setting up many granular watches when we can also setup two coarse ones.
|
|
|
|
// Don't add any roots in configurations where the feature is disabled; their existence can cause performance
|
|
// problems when there are too many disk changes in a short amount of time and the directory watcher's buffer
|
|
// overflows and it issues a FCA_RescanRequired; in that case with one large root we rescan many unrelated
|
|
// directories.
|
|
|
|
#if UE_ENABLE_DIRECTORYWATCH_ROOTS
|
|
const FString ProjectPluginDir = UE::AssetRegistry::CreateStandardFilename(FPaths::ProjectPluginsDir());
|
|
if (IPlatformFile::GetPlatformPhysical().DirectoryExists(*ProjectPluginDir))
|
|
{
|
|
DirectoryWatchRoots.Add(ProjectPluginDir);
|
|
}
|
|
const FString EnginePluginDir = UE::AssetRegistry::CreateStandardFilename(FPaths::EnginePluginsDir());
|
|
if (IPlatformFile::GetPlatformPhysical().DirectoryExists(*EnginePluginDir))
|
|
{
|
|
DirectoryWatchRoots.Add(EnginePluginDir);
|
|
}
|
|
|
|
for (FString& WatchRoot : DirectoryWatchRoots)
|
|
{
|
|
FDelegateHandle NewHandle;
|
|
DirectoryWatcher->RegisterDirectoryChangedCallback_Handle(
|
|
WatchRoot,
|
|
IDirectoryWatcher::FDirectoryChanged::CreateUObject(this, &UAssetRegistryImpl::OnDirectoryChanged),
|
|
NewHandle,
|
|
IDirectoryWatcher::WatchOptions::IncludeDirectoryChanges);
|
|
|
|
OnDirectoryChangedDelegateHandles.Add(WatchRoot, NewHandle);
|
|
}
|
|
#endif //UE_ENABLE_DIRECTORYWATCH_ROOTS
|
|
|
|
FString ContentFolder;
|
|
for (TArray<FString>::TConstIterator RootPathIt(Context.RootContentPaths); RootPathIt; ++RootPathIt)
|
|
{
|
|
const FString& RootPath = *RootPathIt;
|
|
ContentFolder = UE::AssetRegistry::CreateStandardFilename(FPackageName::LongPackageNameToFilename(RootPath));
|
|
if (IsDirAlreadyWatchedByRootWatchers(ContentFolder))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// A missing directory here could be due to a plugin that specifies it contains content, yet has no content yet.
|
|
// PluginManager mounts these folders anyway which results in them being returned from QueryRootContentPaths.
|
|
// Make sure the directory exits on disk so that the OS level DirectoryWatcher can be used to monitor it.
|
|
IPlatformFile::GetPlatformPhysical().CreateDirectoryTree(*ContentFolder);
|
|
FDelegateHandle NewHandle;
|
|
DirectoryWatcher->RegisterDirectoryChangedCallback_Handle(
|
|
ContentFolder,
|
|
IDirectoryWatcher::FDirectoryChanged::CreateUObject(this, &UAssetRegistryImpl::OnDirectoryChanged),
|
|
NewHandle,
|
|
IDirectoryWatcher::WatchOptions::IncludeDirectoryChanges);
|
|
|
|
OnDirectoryChangedDelegateHandles.Add(RootPath, NewHandle);
|
|
}
|
|
}
|
|
}
|
|
|
|
bUpdateDiskCacheAfterLoad = Context.bUpdateDiskCacheAfterLoad;
|
|
if (bUpdateDiskCacheAfterLoad)
|
|
{
|
|
FCoreUObjectDelegates::OnAssetLoaded.AddUObject(this, &UAssetRegistryImpl::OnAssetLoaded);
|
|
}
|
|
|
|
if (bAddMetaDataTagsToOnGetExtraObjectTags)
|
|
{
|
|
UObject::FAssetRegistryTag::OnGetExtraObjectTagsWithContext.AddUObject(this, &UAssetRegistryImpl::OnGetExtraObjectTags);
|
|
}
|
|
if (Context.bNeedsSearchAllAssetsAtStartSynchronous)
|
|
{
|
|
FCoreDelegates::OnFEngineLoopInitComplete.AddUObject(this, &UAssetRegistryImpl::OnFEngineLoopInitCompleteSearchAllAssets);
|
|
}
|
|
|
|
UE::AssetDependencyGatherer::Private::FRegisteredAssetDependencyGatherer::OnAssetDependencyGathererRegistered.AddUObject(this, &UAssetRegistryImpl::OnAssetDependencyGathererRegistered);
|
|
#endif // WITH_EDITOR
|
|
|
|
// We use OnPreExit and not OnEnginePreExit because OnPreExit will be called if there's an error in engine init and
|
|
// we never get through OnPostEngineInit.
|
|
FCoreDelegates::OnPreExit.AddUObject(this, &UAssetRegistryImpl::OnPreExit);
|
|
|
|
// Listen for new content paths being added or removed at runtime. These are usually plugin-specific asset paths that
|
|
// will be loaded a bit later on.
|
|
FPackageName::OnContentPathMounted().AddUObject(this, &UAssetRegistryImpl::OnContentPathMounted);
|
|
FPackageName::OnContentPathDismounted().AddUObject(this, &UAssetRegistryImpl::OnContentPathDismounted);
|
|
|
|
// If we were called before engine has fully initialized, refresh classes on initialize. If not this won't do anything as it already happened
|
|
FCoreDelegates::OnPostEngineInit.AddUObject(this, &UAssetRegistryImpl::OnPostEngineInit);
|
|
|
|
IPluginManager& PluginManager = IPluginManager::Get();
|
|
if (!IsEngineStartupModuleLoadingComplete())
|
|
{
|
|
FCoreDelegates::OnAllModuleLoadingPhasesComplete.AddUObject(this, &UAssetRegistryImpl::OnInitialPluginLoadingComplete);
|
|
}
|
|
else
|
|
{
|
|
OnInitialPluginLoadingComplete();
|
|
}
|
|
}
|
|
|
|
UAssetRegistryImpl::UAssetRegistryImpl(FVTableHelper& Helper)
|
|
: Super(Helper)
|
|
{
|
|
}
|
|
|
|
bool UAssetRegistryImpl::OnResolveRedirect(const FString& InPackageName, FString& OutPackageName)
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
return GuardedData.ResolveRedirect(InPackageName, OutPackageName);
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
bool FAssetRegistryImpl::ResolveRedirect(const FString& InPackageName, FString& OutPackageName) const
|
|
{
|
|
int32 DotIndex = InPackageName.Find(TEXT("."), ESearchCase::CaseSensitive);
|
|
|
|
FString ContainerPackageName;
|
|
const FString* PackageNamePtr = &InPackageName; // don't return this
|
|
if (DotIndex != INDEX_NONE)
|
|
{
|
|
ContainerPackageName = InPackageName.Left(DotIndex);
|
|
PackageNamePtr = &ContainerPackageName;
|
|
}
|
|
const FString& PackageName = *PackageNamePtr;
|
|
|
|
for (const FAssetRegistryPackageRedirect& PackageRedirect : PackageRedirects)
|
|
{
|
|
if (PackageName.Compare(PackageRedirect.SourcePackageName) == 0)
|
|
{
|
|
OutPackageName = InPackageName.Replace(*PackageRedirect.SourcePackageName, *PackageRedirect.DestPackageName);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void FAssetRegistryImpl::InitRedirectors(Impl::FEventContext& EventContext,
|
|
Impl::FClassInheritanceContext& InheritanceContext, bool& bOutRedirectorsNeedSubscribe)
|
|
{
|
|
bOutRedirectorsNeedSubscribe = false;
|
|
|
|
// plugins can't initialize redirectors in the editor, it will mess up the saving of content.
|
|
if ( GIsEditor )
|
|
{
|
|
return;
|
|
}
|
|
|
|
TArray<TSharedRef<IPlugin>> EnabledPlugins = IPluginManager::Get().GetEnabledPlugins();
|
|
for (const TSharedRef<IPlugin>& Plugin : EnabledPlugins)
|
|
{
|
|
FString PluginConfigFilename = FConfigCacheIni::NormalizeConfigIniPath(FString::Printf(TEXT("%s%s/%s.ini"), *FPaths::GeneratedConfigDir(), ANSI_TO_TCHAR(FPlatformProperties::PlatformName()), *Plugin->GetName() ));
|
|
|
|
bool bShouldRemap = false;
|
|
|
|
if ( !GConfig->GetBool(TEXT("PluginSettings"), TEXT("RemapPluginContentToGame"), bShouldRemap, PluginConfigFilename) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!bShouldRemap)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// if we are -game or -server in editor build we might need to initialize the asset registry manually for this plugin
|
|
if (!FPlatformProperties::RequiresCookedData() && (IsRunningGame() || IsRunningDedicatedServer()))
|
|
{
|
|
TArray<FString> PathsToSearch;
|
|
|
|
FString RootPackageName = FString::Printf(TEXT("/%s/"), *Plugin->GetName());
|
|
PathsToSearch.Add(RootPackageName);
|
|
|
|
Impl::FScanPathContext Context(EventContext, InheritanceContext, PathsToSearch, TArray<FString>());
|
|
ScanPathsSynchronous(nullptr /*ScopeLock*/, Context);
|
|
}
|
|
|
|
FName PluginPackageName = FName(*FString::Printf(TEXT("/%s/"), *Plugin->GetName()));
|
|
EnumerateAssetsByPathNoTags(PluginPackageName,
|
|
[&Plugin, this](const FAssetData& PartialAssetData)
|
|
{
|
|
FString NewPackageNameString = PartialAssetData.PackageName.ToString();
|
|
FString RootPackageName = FString::Printf(TEXT("/%s/"), *Plugin->GetName());
|
|
FString OriginalPackageNameString = NewPackageNameString.Replace(*RootPackageName, TEXT("/Game/"));
|
|
|
|
PackageRedirects.Add(FAssetRegistryPackageRedirect(OriginalPackageNameString, NewPackageNameString));
|
|
return true;
|
|
}, true, false);
|
|
|
|
bOutRedirectorsNeedSubscribe = true;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::OnInitialPluginLoadingComplete()
|
|
{
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
const bool bSuppressWarnings = !UE::AssetRegistry::Utils::HasEngineModuleLoaded();
|
|
UE::AssetRegistry::Utils::UpdateSerializationOptions(GuardedData.SerializationOptions, GuardedData.CachedParsedFilterRules, bSuppressWarnings);
|
|
GuardedData.OnPluginLoadingComplete(true);
|
|
}
|
|
|
|
FCoreDelegates::OnAllModuleLoadingPhasesComplete.RemoveAll(this);
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::OnPluginLoadingComplete(bool bPhaseSuccessful)
|
|
{
|
|
// If we have constructed the GlobalGatherer then we need to readscriptpackages,
|
|
// otherwise we will read them when constructing the gatherer.
|
|
if (GlobalGatherer.IsValid())
|
|
{
|
|
ReadScriptPackages();
|
|
}
|
|
|
|
// Reparse the skip classes the next time ShouldSkipAsset is called, since available classes
|
|
// for the search over all classes may have changed
|
|
#if WITH_ENGINE && WITH_EDITOR
|
|
{
|
|
UE::TWriteScopeLock WriteLock(SkipClassesLock);
|
|
Utils::PopulateSkipClasses(SkipUncookedClasses, SkipCookedClasses);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void FAssetRegistryImpl::ReadScriptPackages()
|
|
{
|
|
GlobalGatherer->SetInitialPluginsLoaded();
|
|
if (GlobalGatherer->IsGatheringDependencies())
|
|
{
|
|
// Now that all scripts have been loaded, we need to create AssetPackageDatas for every script
|
|
// This is also done whenever scripts are referenced in our gather of existing packages,
|
|
// but we need to complete it for all scripts that were referenced but not yet loaded for packages
|
|
// that we already gathered
|
|
for (TObjectIterator<UPackage> It; It; ++It)
|
|
{
|
|
UPackage* Package = *It;
|
|
if (Package)
|
|
{
|
|
if (Package && FPackageName::IsScriptPackage(Package->GetName()))
|
|
{
|
|
FAssetPackageData* ScriptPackageData = State.CreateOrGetAssetPackageData(Package->GetFName());
|
|
#if WITH_EDITORONLY_DATA
|
|
// Get the hash off the script package, it is updated when script is changed so we need to refresh it every run
|
|
ScriptPackageData->SetPackageSavedHash(Package->GetSavedHash());
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::InitializeSerializationOptions(FAssetRegistrySerializationOptions& Options, const ITargetPlatform* TargetPlatform, UE::AssetRegistry::ESerializationTarget Target) const
|
|
{
|
|
if (!TargetPlatform)
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
// Use options we already loaded, the first pass for this happens at object creation time so this is always valid when queried externally
|
|
GuardedData.CopySerializationOptions(Options, Target);
|
|
}
|
|
else
|
|
{
|
|
// Temp container to parse and resolve FilterTags
|
|
UE::AssetRegistry::Impl::FFilterTagRules TempParsedFilterRules;
|
|
const bool bSuppressWarnings = !UE::AssetRegistry::Utils::HasEngineModuleLoaded();
|
|
UE::AssetRegistry::Utils::InitializeSerializationOptionsFromIni(Options, &TempParsedFilterRules, TargetPlatform, Target, bSuppressWarnings);
|
|
}
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::CopySerializationOptions(FAssetRegistrySerializationOptions& OutOptions, ESerializationTarget Target) const
|
|
{
|
|
if (Target == UE::AssetRegistry::ESerializationTarget::ForGame)
|
|
{
|
|
OutOptions = SerializationOptions;
|
|
}
|
|
else
|
|
{
|
|
OutOptions = DevelopmentSerializationOptions;
|
|
}
|
|
}
|
|
|
|
namespace Utils
|
|
{
|
|
|
|
static TSet<FName> MakeNameSet(const TArray<FString>& Strings)
|
|
{
|
|
TSet<FName> Out;
|
|
Out.Reserve(Strings.Num());
|
|
for (const FString& String : Strings)
|
|
{
|
|
Out.Add(FName(*String));
|
|
}
|
|
|
|
return Out;
|
|
}
|
|
|
|
void InitializeSerializationOptionsFromIni(
|
|
FAssetRegistrySerializationOptions& Options,
|
|
Impl::FFilterTagRules* OutRules,
|
|
const ITargetPlatform* TargetPlatform,
|
|
UE::AssetRegistry::ESerializationTarget Target,
|
|
bool bSuppressWarnings)
|
|
{
|
|
FString PlatformIniName;
|
|
EBuildTargetType TargetType = EBuildTargetType::Game;
|
|
if (TargetPlatform)
|
|
{
|
|
#if WITH_EDITOR
|
|
PlatformIniName = TargetPlatform->IniPlatformName();
|
|
TargetType = TargetPlatform->GetRuntimePlatformType();
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
PlatformIniName = ANSI_TO_TCHAR(FPlatformProperties::IniPlatformName());
|
|
TargetType = FPlatformProperties::IsClientOnly() ? EBuildTargetType::Client
|
|
: FPlatformProperties::IsServerOnly() ? EBuildTargetType::Server
|
|
: EBuildTargetType::Game;
|
|
}
|
|
// Use passed in platform, or current platform if empty
|
|
FConfigFile LocalEngineIni;
|
|
FConfigFile* EngineIni = FConfigCacheIni::FindOrLoadPlatformConfig(LocalEngineIni, TEXT("Engine"), *PlatformIniName);
|
|
|
|
Options = FAssetRegistrySerializationOptions(Target);
|
|
// For DevelopmentAssetRegistry, all non-tag options are overridden in the constructor
|
|
const bool bForDevelopment = Target == UE::AssetRegistry::ESerializationTarget::ForDevelopment;
|
|
if (!bForDevelopment)
|
|
{
|
|
EngineIni->GetBool(TEXT("AssetRegistry"), TEXT("bSerializeAssetRegistry"), Options.bSerializeAssetRegistry);
|
|
EngineIni->GetBool(TEXT("AssetRegistry"), TEXT("bSerializeDependencies"), Options.bSerializeDependencies);
|
|
EngineIni->GetBool(TEXT("AssetRegistry"), TEXT("bSerializeNameDependencies"), Options.bSerializeSearchableNameDependencies);
|
|
EngineIni->GetBool(TEXT("AssetRegistry"), TEXT("bSerializeManageDependencies"), Options.bSerializeManageDependencies);
|
|
EngineIni->GetBool(TEXT("AssetRegistry"), TEXT("bSerializePackageData"), Options.bSerializePackageData);
|
|
EngineIni->GetBool(TEXT("AssetRegistry"), TEXT("bFilterAssetDataWithNoTags"), Options.bFilterAssetDataWithNoTags);
|
|
EngineIni->GetBool(TEXT("AssetRegistry"), TEXT("bFilterDependenciesWithNoTags"), Options.bFilterDependenciesWithNoTags);
|
|
EngineIni->GetBool(TEXT("AssetRegistry"), TEXT("bFilterSearchableNames"), Options.bFilterSearchableNames);
|
|
|
|
{
|
|
TArray<FString> AssetBundlesDenyList;
|
|
EngineIni->GetArray(TEXT("AssetRegistry"), TEXT("AssetBundlesDenyList"), AssetBundlesDenyList);
|
|
Options.AssetBundlesDenyList.Append(AssetBundlesDenyList);
|
|
}
|
|
if (TargetType == EBuildTargetType::Server)
|
|
{
|
|
TArray<FString> AssetBundlesDenyList;
|
|
EngineIni->GetArray(TEXT("AssetRegistry"), TEXT("ServerAssetBundlesDenyList"), AssetBundlesDenyList);
|
|
Options.AssetBundlesDenyList.Append(AssetBundlesDenyList);
|
|
}
|
|
if (TargetType == EBuildTargetType::Client)
|
|
{
|
|
TArray<FString> AssetBundlesDenyList;
|
|
EngineIni->GetArray(TEXT("AssetRegistry"), TEXT("ClientAssetBundlesDenyList"), AssetBundlesDenyList);
|
|
Options.AssetBundlesDenyList.Append(AssetBundlesDenyList);
|
|
}
|
|
}
|
|
|
|
if (OutRules)
|
|
{
|
|
EngineIni->GetBool(TEXT("AssetRegistry"), TEXT("bUseAssetRegistryTagsWhitelistInsteadOfBlacklist"), Options.bUseAssetRegistryTagsAllowListInsteadOfDenyList);
|
|
TArray<FString> FilterListItems;
|
|
if (Options.bUseAssetRegistryTagsAllowListInsteadOfDenyList)
|
|
{
|
|
EngineIni->GetArray(TEXT("AssetRegistry"), TEXT("CookedTagsWhitelist"), FilterListItems);
|
|
}
|
|
else
|
|
{
|
|
EngineIni->GetArray(TEXT("AssetRegistry"), TEXT("CookedTagsBlacklist"), FilterListItems);
|
|
}
|
|
|
|
const bool bKeepDevelopmentTags = bForDevelopment || FParse::Param(FCommandLine::Get(), TEXT("ARKeepDevTags"));
|
|
const bool bLoadClasses = true;
|
|
const bool bIsFilteringDevelopmentAR = bKeepDevelopmentTags;
|
|
OutRules->Rules = ParseFilterTags(FilterListItems, bKeepDevelopmentTags, Options.bUseAssetRegistryTagsAllowListInsteadOfDenyList);
|
|
UpdateSerializationOptions(Options, *OutRules, bSuppressWarnings);
|
|
}
|
|
else
|
|
{
|
|
// By default all tags are included
|
|
Options.bUseAssetRegistryTagsAllowListInsteadOfDenyList = false;
|
|
}
|
|
|
|
{
|
|
// this only needs to be done once, and only on builds using USE_COMPACT_ASSET_REGISTRY
|
|
TArray<FString> AsFName;
|
|
EngineIni->GetArray(TEXT("AssetRegistry"), TEXT("CookedTagsAsFName"), AsFName);
|
|
Options.CookTagsAsName = MakeNameSet(AsFName);
|
|
|
|
TArray<FString> AsPathName;
|
|
EngineIni->GetArray(TEXT("AssetRegistry"), TEXT("CookedTagsAsPathName"), AsPathName);
|
|
Options.CookTagsAsPath = MakeNameSet(AsPathName);
|
|
}
|
|
}
|
|
|
|
void UpdateSerializationOptions(
|
|
FAssetRegistrySerializationOptions& InOutOptions,
|
|
Impl::FFilterTagRules& InOutFilterRules,
|
|
bool bSuppressWarnings)
|
|
{
|
|
if (InOutFilterRules.bContainsAnyUnresolvedRule || (InOutFilterRules.ClassesFingerprint != GetRegisteredNativeClassesVersionNumber()))
|
|
{
|
|
InOutOptions.CookFilterlistTagsByClass = ResolveFilterTags(InOutFilterRules, bSuppressWarnings);
|
|
}
|
|
}
|
|
|
|
static int32 CalculateInheritanceDepth(const UClass* Class)
|
|
{
|
|
int32 InheritanceDepth = 0;
|
|
if (Class)
|
|
{
|
|
for (auto Iter = Class->GetSuperStructIterator(); Iter; ++Iter)
|
|
{
|
|
++InheritanceDepth;
|
|
}
|
|
}
|
|
return InheritanceDepth;
|
|
}
|
|
|
|
TMap<FName, Impl::FParsedFilterTagRules> ParseFilterTags(const TArray<FString>& FilterListItems, bool bIsFilteringDevelopmentAR , bool bUseAllowListInsteadOfDenyList)
|
|
{
|
|
TMap<FName, Impl::FParsedFilterTagRules> TagsOrder;
|
|
|
|
// Takes on the pattern "(Class=SomeClass,Tag=SomeTag)"
|
|
// Optional key KeepInDevOnly for tweaking a DevelopmentAssetRegistry (additive if allow list, subtractive if deny list)
|
|
for (const FString& FilterEntry : FilterListItems)
|
|
{
|
|
FString TrimmedEntry = FilterEntry;
|
|
TrimmedEntry.TrimStartAndEndInline();
|
|
if (TrimmedEntry.Left(1) == TEXT("("))
|
|
{
|
|
TrimmedEntry.RightChopInline(1, EAllowShrinking::No);
|
|
}
|
|
if (TrimmedEntry.Right(1) == TEXT(")"))
|
|
{
|
|
TrimmedEntry.LeftChopInline(1, EAllowShrinking::No);
|
|
}
|
|
|
|
TArray<FString> Tokens;
|
|
TrimmedEntry.ParseIntoArray(Tokens, TEXT(","));
|
|
FString ClassName;
|
|
FString TagName;
|
|
bool bKeepInDevOnly = false;
|
|
|
|
for (const FString& Token : Tokens)
|
|
{
|
|
FString KeyString;
|
|
FString ValueString;
|
|
if (Token.Split(TEXT("="), &KeyString, &ValueString))
|
|
{
|
|
KeyString.TrimStartAndEndInline();
|
|
ValueString.TrimStartAndEndInline();
|
|
if (KeyString.Equals(TEXT("Class"), ESearchCase::IgnoreCase))
|
|
{
|
|
ClassName = ValueString;
|
|
}
|
|
else if (KeyString.Equals(TEXT("Tag"), ESearchCase::IgnoreCase))
|
|
{
|
|
TagName = ValueString;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
KeyString = Token.TrimStartAndEnd();
|
|
if (KeyString.Equals(TEXT("KeepInDevOnly"), ESearchCase::IgnoreCase))
|
|
{
|
|
bKeepInDevOnly = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool bAddToList;
|
|
if (bUseAllowListInsteadOfDenyList)
|
|
{
|
|
bAddToList = !bKeepInDevOnly || bIsFilteringDevelopmentAR;
|
|
}
|
|
else
|
|
{
|
|
// "KeepInDevOnly" means "Keep the tag only when creating the DevelopmentAssetRegistry"
|
|
// That is complicated meaning when we are creating a DenyList, wherein adding an element to the list removes
|
|
// it from the filtered collection of tags.
|
|
// We add it to the list if either it applies to all ARs because KeepInDevOnly was not set, or if we are creating
|
|
// the GameAssetRegistry.
|
|
bAddToList = !bKeepInDevOnly || !bIsFilteringDevelopmentAR;
|
|
}
|
|
|
|
if (!ClassName.IsEmpty() && !TagName.IsEmpty())
|
|
{
|
|
Impl::FParsedFilterTagRules& Rules = TagsOrder.FindOrAdd(FName(TagName.Len(), *TagName));
|
|
TArray<Impl::FParsedFilterTagRules::FFilterTagsRule>& OrderedList = Rules.OrderedList;
|
|
const UClass* FilterlistClass = nullptr;
|
|
// Skip known wildcard
|
|
if (ClassName != TEXTVIEW("*"))
|
|
{
|
|
FilterlistClass = Cast<UClass>(StaticFindObject(UClass::StaticClass(), nullptr, *ClassName));
|
|
}
|
|
|
|
if (FilterlistClass)
|
|
{
|
|
const int32 InheritanceDepth = CalculateInheritanceDepth(FilterlistClass);
|
|
// Essentially I want to do OrderedList.HeapPush(FFilterTagsRule{...}, &GetInheritanceDepth);
|
|
// but there is no TArray::HeapPush which takes in an Item and a projection functor.
|
|
// Also there is no equivalent Emplace semantics to construct object inplace without extra copies/moves.
|
|
// So here is an inline implementation of this:
|
|
OrderedList.Emplace(FilterlistClass->GetClassPathName(), FilterlistClass, InheritanceDepth, bAddToList);
|
|
if (!Rules.bContainsAnyUnresolvedRule)
|
|
{
|
|
AlgoImpl::HeapSiftUp(
|
|
OrderedList.GetData(),
|
|
0,
|
|
OrderedList.Num() - 1,
|
|
[](const Impl::FParsedFilterTagRules::FFilterTagsRule& Info)
|
|
{
|
|
return Info.InheritanceDepth;
|
|
},
|
|
TLess<int32>());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FTopLevelAssetPath ClassPathName;
|
|
if (ClassName == TEXTVIEW("*"))
|
|
{
|
|
ClassPathName = UE::AssetRegistry::WildcardPathName;
|
|
}
|
|
else if (FPackageName::IsShortPackageName(ClassName))
|
|
{
|
|
ClassPathName = UClass::TryConvertShortTypeNameToPathName<UClass>(ClassName, ELogVerbosity::Warning, TEXT("Parsing [AssetRegistry] CookedTagsWhitelist or CookedTagsBlacklist"));
|
|
UE_CLOG(ClassPathName.IsNull(), LogAssetRegistry, Warning, TEXT("Failed to convert short class name \"%s\" when parsing ini [AssetRegistry] CookedTagsWhitelist or CookedTagsBlacklist"), *ClassName);
|
|
}
|
|
else
|
|
{
|
|
ClassPathName = FTopLevelAssetPath(ClassName);
|
|
}
|
|
|
|
if (!ClassPathName.IsValid())
|
|
{
|
|
UE_LOG(LogAssetRegistry, Warning,
|
|
TEXT("There is a rule to %s tag \"%s\" for class \"%s\", which is not a valid class path. Ignoring."),
|
|
bAddToList ? TEXT("included") : TEXT("excluded"),
|
|
*TagName,
|
|
*ClassName
|
|
);
|
|
continue;
|
|
}
|
|
|
|
Rules.bContainsAnyUnresolvedRule = true;
|
|
OrderedList.Emplace(ClassPathName, nullptr, -1, bAddToList);
|
|
}
|
|
}
|
|
}
|
|
|
|
return TagsOrder;
|
|
}
|
|
|
|
bool HasEngineModuleLoaded()
|
|
{
|
|
static bool bIsEngineModuleAvaliable = false;
|
|
if (bIsEngineModuleAvaliable)
|
|
{
|
|
return true;
|
|
}
|
|
bIsEngineModuleAvaliable = FModuleManager::Get().IsModuleLoaded(TEXT("Engine"));
|
|
return bIsEngineModuleAvaliable;
|
|
}
|
|
|
|
static void ResolveRules(
|
|
FStringView TagName,
|
|
TArray<Impl::FParsedFilterTagRules::FFilterTagsRule>& OrderedList,
|
|
bool bSuppressWarnings,
|
|
bool* bAreRulesResolved)
|
|
{
|
|
auto RuleIt = OrderedList.CreateIterator();
|
|
if (RuleIt->ClassPath == UE::AssetRegistry::WildcardPathName)
|
|
{
|
|
// Move cursor to next element and delete the rest
|
|
for (++RuleIt; RuleIt; ++RuleIt)
|
|
{
|
|
const bool bIsDuplicate = (OrderedList[0].bInclude == RuleIt->bInclude);
|
|
UE_CLOG(!bSuppressWarnings && !bIsDuplicate, LogAssetRegistry, Warning,
|
|
TEXT("There is a wildcard rule to %s tag \"%.*s\", ")
|
|
TEXT("but there is another rule to %s that tag for class \"%s\". ")
|
|
TEXT("Only wildcard rule is accepted. Ignoring another rule."),
|
|
OrderedList[0].bInclude ? TEXT("include") : TEXT("exclude"),
|
|
TagName.Len(), TagName.GetData(),
|
|
RuleIt->bInclude ? TEXT("include") : TEXT("exclude"),
|
|
*RuleIt->ClassPath.ToString()
|
|
);
|
|
RuleIt.RemoveCurrentSwap();
|
|
}
|
|
// Wildcard is considered resolved rule
|
|
if (bAreRulesResolved)
|
|
{
|
|
*bAreRulesResolved = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
bool bContainAnyUnresolvedRules = false;
|
|
for (; RuleIt; ++RuleIt)
|
|
{
|
|
// Currently there is no case when we don't want to cause sync load of the class we try to resolve.
|
|
// Expose as a function parameter if that ever will need to change.
|
|
constexpr bool bLoadClasses = true;
|
|
RuleIt->ClassPtr = bLoadClasses
|
|
? StaticLoadClass(UObject::StaticClass(), nullptr, RuleIt->ClassPath.ToString(), {}, LOAD_Quiet)
|
|
: Cast<UClass>(StaticFindObject(UClass::StaticClass(), RuleIt->ClassPath));
|
|
if (RuleIt->ClassPtr)
|
|
{
|
|
RuleIt->InheritanceDepth = CalculateInheritanceDepth(RuleIt->ClassPtr);
|
|
}
|
|
else
|
|
{
|
|
// Silently ignore same unresolved rules
|
|
const bool bIsDuplicate = (RuleIt.GetIndex() != 0) && (OrderedList[RuleIt.GetIndex()-1].bInclude == RuleIt->bInclude);
|
|
UE_CLOG(!bSuppressWarnings && !bIsDuplicate, LogAssetRegistry, Warning,
|
|
TEXT("There is a rule to %s tag \"%.*s\" for class \"%s\". Class is unknown. Ignoring."),
|
|
RuleIt->bInclude ? TEXT("include") : TEXT("exclude"),
|
|
TagName.Len(), TagName.GetData(),
|
|
*RuleIt->ClassPath.ToString()
|
|
);
|
|
// Setting unresolved rule to max_int causes resolved rules to be applied anyway and any unresolved to be skipped.
|
|
RuleIt->InheritanceDepth = MAX_int32;
|
|
bContainAnyUnresolvedRules = true;
|
|
}
|
|
}
|
|
|
|
Algo::HeapSortBy(OrderedList, [](const Impl::FParsedFilterTagRules::FFilterTagsRule& Info) { return Info.InheritanceDepth; }, TLess<int32>());
|
|
|
|
if (bAreRulesResolved)
|
|
{
|
|
*bAreRulesResolved = !bContainAnyUnresolvedRules;
|
|
}
|
|
}
|
|
|
|
TMap<FTopLevelAssetPath, TSet<FName>> ResolveFilterTags(Impl::FFilterTagRules& InOutRules, bool bSuppressWarnings)
|
|
{
|
|
TMap<FTopLevelAssetPath, TSet<FName>> Result;
|
|
// keep temp allocation around while we traverse the ordered list
|
|
TSet<const UClass*> ClassesToAdd;
|
|
InOutRules.bContainsAnyUnresolvedRule = false;
|
|
// Populate CookFilterlistTagsByClass by walking the ordered list
|
|
for (TTuple<FName, Impl::FParsedFilterTagRules>& Pair : InOutRules.Rules)
|
|
{
|
|
const FName Tag = Pair.Key;
|
|
TStringBuilder<512> TagNameBuilder;
|
|
Pair.Key.ToString(TagNameBuilder);
|
|
const FStringView TagName = TagNameBuilder.ToView();
|
|
Impl::FParsedFilterTagRules& TagRules = Pair.Value;
|
|
TArray<Impl::FParsedFilterTagRules::FFilterTagsRule>& OrderedList = TagRules.OrderedList;
|
|
if (TagRules.bContainsAnyUnresolvedRule)
|
|
{
|
|
bool bAreRulesResolved = false;
|
|
ResolveRules(TagName, OrderedList, bSuppressWarnings, &bAreRulesResolved);
|
|
TagRules.bContainsAnyUnresolvedRule = !bAreRulesResolved;
|
|
}
|
|
if (TagRules.bContainsAnyUnresolvedRule)
|
|
{
|
|
InOutRules.bContainsAnyUnresolvedRule = true;
|
|
}
|
|
|
|
ClassesToAdd.Reset();
|
|
// The list is sorted in heap order of Inheritance depth,
|
|
// so we can just walk through it and add/remove as needed
|
|
TArray<UClass*> Temp;
|
|
for (const Impl::FParsedFilterTagRules::FFilterTagsRule& Info : OrderedList)
|
|
{
|
|
if (!Info.ClassPtr)
|
|
{
|
|
if (Info.bInclude)
|
|
{
|
|
Result.FindOrAdd(Info.ClassPath).Add(Tag);
|
|
}
|
|
break;
|
|
}
|
|
|
|
Temp.Reset();
|
|
GetDerivedClasses(Info.ClassPtr, Temp, true);
|
|
if (Info.bInclude)
|
|
{
|
|
ClassesToAdd.Reserve(ClassesToAdd.Num() + Temp.Num() + 1);
|
|
ClassesToAdd.Add(Info.ClassPtr);
|
|
ClassesToAdd.Append(Temp);
|
|
}
|
|
else
|
|
{
|
|
for (UClass* Derived : Temp)
|
|
{
|
|
ClassesToAdd.Remove(Derived);
|
|
}
|
|
ClassesToAdd.Remove(Info.ClassPtr);
|
|
}
|
|
}
|
|
|
|
// After the final list of classes is known - add them to the CookFilterlistTagsByClass
|
|
for (const UClass* Class : ClassesToAdd)
|
|
{
|
|
Result.FindOrAdd(Class->GetClassPathName()).Add(Tag);
|
|
}
|
|
}
|
|
InOutRules.ClassesFingerprint = GetRegisteredNativeClassesVersionNumber();
|
|
|
|
return Result;
|
|
}
|
|
|
|
}
|
|
|
|
uint64 FAssetRegistryImpl::GetCurrentGeneratorClassesVersionNumber()
|
|
{
|
|
// Generator classes can only be native, so we can use the less-frequently-updated
|
|
// RegisteredNativeClassesVersionNumber. In monolithic configurations, this will only be
|
|
// updated at program start and when enabling DLC modules.
|
|
return GetRegisteredNativeClassesVersionNumber();
|
|
}
|
|
|
|
uint64 FAssetRegistryImpl::GetCurrentAllClassesVersionNumber()
|
|
{
|
|
return GetRegisteredClassesVersionNumber();
|
|
}
|
|
|
|
void FAssetRegistryImpl::CollectCodeGeneratorClasses()
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry); // Tagged here instead of a higher level because it can occur even when reading
|
|
// Only refresh the list if our registered classes have changed
|
|
uint64 CurrentGeneratorClassesVersionNumber = GetCurrentGeneratorClassesVersionNumber();
|
|
if (SavedGeneratorClassesVersionNumber == CurrentGeneratorClassesVersionNumber)
|
|
{
|
|
return;
|
|
}
|
|
SavedGeneratorClassesVersionNumber = CurrentGeneratorClassesVersionNumber;
|
|
|
|
TArray<UClass*> BlueprintCoreDerivedClasses;
|
|
FTopLevelAssetPath BlueprintCorePathName(GetClassPathBlueprintCore());
|
|
UClass* BlueprintCoreClass = nullptr;
|
|
|
|
{
|
|
// FindObject and GetDerivedClasses are not legal during GarbageCollection. Note that we might be called from
|
|
// an async thread, in which case we might lock this thread until GC completes. This could cause a deadlock if
|
|
// there aren't enough async threads. But CollectCodeGeneratorClasses is not called on runtime or cooked
|
|
// editor because they are monolithic, and so this lock should only occur on uncooked editor platforms, which
|
|
// should have a high enough number of threads to not block garbage collection.
|
|
FGCScopeGuard NoGCScopeGuard;
|
|
|
|
// Work around the fact we don't reference Engine module directly
|
|
BlueprintCoreClass = FindObject<UClass>(BlueprintCorePathName);
|
|
if (!BlueprintCoreClass)
|
|
{
|
|
return;
|
|
}
|
|
GetDerivedClasses(BlueprintCoreClass, BlueprintCoreDerivedClasses);
|
|
}
|
|
|
|
ClassGeneratorNames.Add(BlueprintCoreClass->GetClassPathName());
|
|
for (UClass* BPCoreClass : BlueprintCoreDerivedClasses)
|
|
{
|
|
bool bAlreadyRecorded;
|
|
FTopLevelAssetPath BPCoreClassName = BPCoreClass->GetClassPathName();
|
|
ClassGeneratorNames.Add(BPCoreClassName, &bAlreadyRecorded);
|
|
if (bAlreadyRecorded)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// For new generator classes, add all instances of them to CachedBPInheritanceMap. This is usually done
|
|
// when AddAssetData is called for those instances, but when we add a new generator class we have to recheck all
|
|
// instances of the class since they would have failed to detect they were Blueprint classes before.
|
|
// This can happen if blueprints in plugin B are scanned before their blueprint class from plugin A is scanned.
|
|
State.EnumerateAssetsByClassPathName(BPCoreClassName, [this](const FAssetData* AssetData)
|
|
{
|
|
const FString GeneratedClass = AssetData->GetTagValueRef<FString>(FBlueprintTags::GeneratedClassPath);
|
|
const FString ParentClass = AssetData->GetTagValueRef<FString>(FBlueprintTags::ParentClassPath);
|
|
if (!GeneratedClass.IsEmpty() && !ParentClass.IsEmpty())
|
|
{
|
|
const FTopLevelAssetPath GeneratedClassPathName(FPackageName::ExportTextPathToObjectPath(GeneratedClass));
|
|
const FTopLevelAssetPath ParentClassPathName(FPackageName::ExportTextPathToObjectPath(ParentClass));
|
|
|
|
if (!CachedBPInheritanceMap.Contains(GeneratedClassPathName))
|
|
{
|
|
AddCachedBPClassParent(GeneratedClassPathName, ParentClassPathName);
|
|
|
|
// Invalidate caching because CachedBPInheritanceMap got modified
|
|
TempCachedInheritanceBuffer.bDirty = true;
|
|
}
|
|
}
|
|
return true; // Keep iterating the assets for the class
|
|
});
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::OnPostEngineInit()
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.RefreshNativeClasses();
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::RefreshNativeClasses()
|
|
{
|
|
// Native classes have changed so reinitialize code generator, class inheritance maps,
|
|
// and serialization options
|
|
CollectCodeGeneratorClasses();
|
|
TempCachedInheritanceBuffer.bDirty = true;
|
|
}
|
|
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
void UAssetRegistryImpl::OnFEngineLoopInitCompleteSearchAllAssets()
|
|
{
|
|
SearchAllAssets(true);
|
|
}
|
|
|
|
void UAssetRegistryImpl::OnAssetDependencyGathererRegistered()
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.OnAssetDependencyGathererRegistered();
|
|
}
|
|
#endif
|
|
|
|
void UAssetRegistryImpl::OnPreExit()
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
|
|
TUniquePtr<FAssetDataGatherer> GlobalGatherer;
|
|
{
|
|
FScopeLock GatheredDataGuard(&GatheredDataProcessingLock);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GlobalGatherer = MoveTemp(GuardedData.AccessGlobalGatherer());
|
|
if (GlobalGatherer.IsValid())
|
|
{
|
|
GlobalGatherer->Stop();
|
|
}
|
|
}
|
|
// Now that we are no longer holding the lock, we can destroy the gatherer
|
|
GlobalGatherer.Reset();
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::FinishDestroy()
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
|
|
{
|
|
FScopeLock DeferredEventsLock(&DeferredEventsCriticalSection);
|
|
ClearRequestTick();
|
|
}
|
|
{
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
|
|
// Stop listening for content mount point events
|
|
FPackageName::OnContentPathMounted().RemoveAll(this);
|
|
FPackageName::OnContentPathDismounted().RemoveAll(this);
|
|
FCoreDelegates::OnPostEngineInit.RemoveAll(this);
|
|
FCoreDelegates::OnPreExit.RemoveAll(this);
|
|
IPluginManager::Get().OnLoadingPhaseComplete().RemoveAll(this);
|
|
|
|
#if WITH_EDITOR
|
|
if (UE::AssetRegistry::Impl::IsDirectoryWatcherEnabled())
|
|
{
|
|
// If the directory module is still loaded, unregister any delegates
|
|
if (FModuleManager::Get().IsModuleLoaded("DirectoryWatcher"))
|
|
{
|
|
FDirectoryWatcherModule& DirectoryWatcherModule = FModuleManager::GetModuleChecked<FDirectoryWatcherModule>("DirectoryWatcher");
|
|
IDirectoryWatcher* DirectoryWatcher = DirectoryWatcherModule.Get();
|
|
|
|
if (DirectoryWatcher)
|
|
{
|
|
TArray<FString> RootContentPaths;
|
|
FPackageName::QueryRootContentPaths(RootContentPaths);
|
|
for (TArray<FString>::TConstIterator RootPathIt(RootContentPaths); RootPathIt; ++RootPathIt)
|
|
{
|
|
const FString& RootPath = *RootPathIt;
|
|
const FString ContentFolder = UE::AssetRegistry::CreateStandardFilename(FPackageName::LongPackageNameToFilename(RootPath));
|
|
if (!IsDirAlreadyWatchedByRootWatchers(ContentFolder))
|
|
{
|
|
DirectoryWatcher->UnregisterDirectoryChangedCallback_Handle(ContentFolder, OnDirectoryChangedDelegateHandles.FindRef(RootPath));
|
|
}
|
|
}
|
|
|
|
for (TArray<FString>::TConstIterator RootPathIt(DirectoryWatchRoots); RootPathIt; ++RootPathIt)
|
|
{
|
|
const FString& RootPath = *RootPathIt;
|
|
DirectoryWatcher->UnregisterDirectoryChangedCallback_Handle(RootPath, OnDirectoryChangedDelegateHandles.FindRef(RootPath));
|
|
}
|
|
DirectoryWatchRoots.Empty();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bUpdateDiskCacheAfterLoad)
|
|
{
|
|
FCoreUObjectDelegates::OnAssetLoaded.RemoveAll(this);
|
|
}
|
|
|
|
if (bAddMetaDataTagsToOnGetExtraObjectTags)
|
|
{
|
|
UObject::FAssetRegistryTag::OnGetExtraObjectTagsWithContext.RemoveAll(this);
|
|
}
|
|
FCoreDelegates::OnFEngineLoopInitComplete.RemoveAll(this);
|
|
|
|
UE::AssetDependencyGatherer::Private::FRegisteredAssetDependencyGatherer::OnAssetDependencyGathererRegistered.RemoveAll(this);
|
|
#endif // WITH_EDITOR
|
|
|
|
if (HasAnyFlags(RF_ClassDefaultObject) && !HasAnyFlags(RF_ImmutableDefaultObject))
|
|
{
|
|
check(UE::AssetRegistry::Private::IAssetRegistrySingleton::Singleton == this && IAssetRegistryInterface::Default == &GAssetRegistryInterface);
|
|
UE::AssetRegistry::Private::IAssetRegistrySingleton::Singleton = nullptr;
|
|
IAssetRegistryInterface::Default = nullptr;
|
|
}
|
|
|
|
// Clear all listeners
|
|
PathAddedEvent.Clear();
|
|
PathRemovedEvent.Clear();
|
|
AssetAddedEvent.Clear();
|
|
AssetRemovedEvent.Clear();
|
|
AssetRenamedEvent.Clear();
|
|
AssetUpdatedEvent.Clear();
|
|
AssetUpdatedOnDiskEvent.Clear();
|
|
for (FAssetsEvent& Event : BatchedAssetEvents)
|
|
{
|
|
Event.Clear();
|
|
}
|
|
InMemoryAssetCreatedEvent.Clear();
|
|
InMemoryAssetDeletedEvent.Clear();
|
|
FileLoadedEvent.Clear();
|
|
FileLoadProgressUpdatedEvent.Clear();
|
|
}
|
|
|
|
Super::FinishDestroy();
|
|
}
|
|
|
|
UAssetRegistryImpl::~UAssetRegistryImpl()
|
|
{
|
|
}
|
|
|
|
UAssetRegistryImpl& UAssetRegistryImpl::Get()
|
|
{
|
|
check(UE::AssetRegistry::Private::IAssetRegistrySingleton::Singleton);
|
|
return static_cast<UAssetRegistryImpl&>(*UE::AssetRegistry::Private::IAssetRegistrySingleton::Singleton);
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
bool FAssetRegistryImpl::TryConstructGathererIfNeeded()
|
|
{
|
|
if (GlobalGatherer.IsValid())
|
|
{
|
|
return true;
|
|
}
|
|
else if (IsEngineExitRequested())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
GlobalGatherer = MakeUnique<FAssetDataGatherer>(UAssetRegistryImpl::Get());
|
|
GatherDependencyDataScratchPad.Reset(new UE::AssetRegistry::DependsNode::FGatherDependencyDataScratchpad());
|
|
|
|
UpdateMaxSecondsPerFrame();
|
|
|
|
// Read script packages if all initial plugins have been loaded, otherwise do nothing; we wait for the callback.
|
|
ELoadingPhase::Type LoadingPhase = IPluginManager::Get().GetLastCompletedLoadingPhase();
|
|
if (LoadingPhase != ELoadingPhase::None && LoadingPhase >= ELoadingPhase::PostEngineInit)
|
|
{
|
|
ReadScriptPackages();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void FAssetRegistryImpl::SearchAllAssetsInitialAsync(Impl::FEventContext& EventContext,
|
|
Impl::FClassInheritanceContext& InheritanceContext)
|
|
{
|
|
SetPerformanceMode(Impl::EPerformanceMode::BulkLoading);
|
|
SearchAllAssets(EventContext, InheritanceContext, false /* bSynchronousSearch */);
|
|
}
|
|
|
|
void FAssetRegistryImpl::SetPerformanceMode(Impl::EPerformanceMode NewMode)
|
|
{
|
|
if (PerformanceMode != NewMode)
|
|
{
|
|
const bool bWereDependenciesSorted = ShouldSortDependencies();
|
|
const bool bWereReferencersSorted = ShouldSortReferencers();
|
|
|
|
PerformanceMode = NewMode;
|
|
|
|
const bool bShouldSortDependencies = ShouldSortDependencies();
|
|
const bool bShouldSortReferencers = ShouldSortReferencers();
|
|
|
|
if ((bWereDependenciesSorted != bShouldSortDependencies) || (bWereReferencersSorted != bShouldSortReferencers))
|
|
{
|
|
State.SetDependencyNodeSorting(bShouldSortDependencies, bShouldSortReferencers);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FAssetRegistryImpl::ShouldSortDependencies() const
|
|
{
|
|
// Always sort in static, sometimes sort during loading
|
|
return (PerformanceMode == Impl::MostlyStatic || (PerformanceMode == Impl::BulkLoading && !Impl::bDeferDependencySort));
|
|
}
|
|
|
|
bool FAssetRegistryImpl::ShouldSortReferencers() const
|
|
{
|
|
// Always sort in static, sometimes sort during loading
|
|
return (PerformanceMode == Impl::MostlyStatic || (PerformanceMode == Impl::BulkLoading && !Impl::bDeferReferencerSort));
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::SearchAllAssets(bool bSynchronousSearch)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE_STR("UAssetRegistryImpl::SearchAllAssets");
|
|
using namespace UE::AssetRegistry::Impl;
|
|
|
|
if (bSynchronousSearch)
|
|
{
|
|
// Ensure any ongoing async scan finishes fully first
|
|
WaitForCompletion();
|
|
}
|
|
|
|
FEventContext EventContext;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
FClassInheritanceContext InheritanceContext;
|
|
FClassInheritanceBuffer InheritanceBuffer;
|
|
GetInheritanceContextWithRequiredLock(InterfaceScopeLock, InheritanceContext, InheritanceBuffer);
|
|
if (bSynchronousSearch)
|
|
{
|
|
// make sure any outstanding async preload is complete
|
|
GuardedData.ConditionalLoadPremadeAssetRegistry(InterfaceScopeLock, EventContext);
|
|
}
|
|
GuardedData.SearchAllAssets(EventContext, InheritanceContext, bSynchronousSearch);
|
|
}
|
|
Broadcast(EventContext);
|
|
|
|
if (bSynchronousSearch)
|
|
{
|
|
// Continue calling TickGatherer until completion is signaled, and call ProcessLoadedAssetsToUpdateCache
|
|
WaitForCompletion();
|
|
}
|
|
}
|
|
|
|
bool UAssetRegistryImpl::IsSearchAllAssets() const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
return GuardedData.IsSearchAllAssets();
|
|
}
|
|
|
|
bool UAssetRegistryImpl::IsSearchAsync() const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
return GuardedData.IsInitialSearchStarted();
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::SearchAllAssets(Impl::FEventContext& EventContext,
|
|
Impl::FClassInheritanceContext& InheritanceContext, bool bSynchronousSearch)
|
|
{
|
|
EventContext.bScanStartedEventBroadcast = true;
|
|
|
|
if (!TryConstructGathererIfNeeded())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!bInitialSearchStarted)
|
|
{
|
|
TRACE_BEGIN_REGION(TEXT("Asset Registry Scan"));
|
|
InitialSearchStartTime = FPlatformTime::Seconds();
|
|
bInitialSearchStarted = true;
|
|
bInitialSearchCompleted.store(false, std::memory_order_relaxed);
|
|
#if WITH_EDITOR
|
|
bGatherNeedsApplyObjectRedirects = true;
|
|
#endif
|
|
UpdateMaxSecondsPerFrame(); // MaxSecondsPerFrame only depends on initial search
|
|
}
|
|
|
|
FAssetDataGatherer& Gatherer = *GlobalGatherer;
|
|
if (!Gatherer.IsAsyncEnabled())
|
|
{
|
|
UE_CLOG(!bSynchronousSearch, LogAssetRegistry, Warning, TEXT("SearchAllAssets: Gatherer is in synchronous mode; forcing bSynchronousSearch=true."));
|
|
bSynchronousSearch = true;
|
|
}
|
|
|
|
// Add all existing mountpoints to the GlobalGatherer
|
|
// This will include Engine content, Game content, but also may include mounted content directories for one or more plugins.
|
|
TArray<FString> PackagePathsToSearch;
|
|
FPackageName::QueryRootContentPaths(PackagePathsToSearch);
|
|
for (const FString& PackagePath : PackagePathsToSearch)
|
|
{
|
|
const FString& MountLocalPath = FPackageName::LongPackageNameToFilename(PackagePath);
|
|
Gatherer.AddMountPoint(MountLocalPath, PackagePath);
|
|
Gatherer.SetIsOnAllowList(MountLocalPath, true);
|
|
}
|
|
bSearchAllAssets = true; // Mark that future mounts and directories should be scanned
|
|
|
|
if (bSynchronousSearch)
|
|
{
|
|
Gatherer.WaitForIdle();
|
|
Impl::FTickContext TickContext(*this, EventContext, InheritanceContext);
|
|
TickContext.bHandleDeferred = true;
|
|
TickContext.bHandleCompletion = false; // Our caller will call WaitForCompletion which will handle this
|
|
Impl::EGatherStatus UnusedStatus = TickGatherer(TickContext);
|
|
}
|
|
else
|
|
{
|
|
Gatherer.StartAsync();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::WaitForCompletion()
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UAssetRegistryImpl::WaitForCompletion);
|
|
|
|
using namespace UE::AssetRegistry::Impl;
|
|
|
|
bool bInitialSearchStarted = false;
|
|
bool bInitialSearchCompleted = false;
|
|
bool bAsyncGathering = false;
|
|
|
|
// Try taking over the gather thread for a short time in case it is mostly done.
|
|
// But if it has more than a small amount of work to do, let the gather thread do that work
|
|
// while we consume the results in parallel.
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
// We don't need to take the GatheredDataProcessingLock here because we actually *do*
|
|
// want to block until we can proceed
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
FClassInheritanceContext InheritanceContext;
|
|
FClassInheritanceBuffer InheritanceBuffer;
|
|
GetInheritanceContextWithRequiredLock(InterfaceScopeLock, InheritanceContext, InheritanceBuffer);
|
|
constexpr float TimeToJoinSeconds = 0.100f;
|
|
GuardedData.WaitForGathererIdle(TimeToJoinSeconds);
|
|
bInitialSearchStarted = GuardedData.IsInitialSearchStarted();
|
|
bInitialSearchCompleted = GuardedData.IsInitialSearchCompleted();
|
|
bAsyncGathering = GuardedData.GlobalGatherer && GuardedData.GlobalGatherer->IsAsyncEnabled();
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
if (IsGathering())
|
|
{
|
|
// If we do need to wait, then tick the DirectoryWatcher so we have the most up to date information.
|
|
// This is also important because we ignore rescan events from the directory watcher if they are sent
|
|
// during startup, so if there is a rescan event pending we want to trigger it now and ignore it
|
|
if (UE::AssetRegistry::Impl::IsDirectoryWatcherEnabled())
|
|
{
|
|
FDirectoryWatcherModule& DirectoryWatcherModule = FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>(TEXT("DirectoryWatcher"));
|
|
DirectoryWatcherModule.Get()->Tick(-1.f);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
bool bLocalHasSentFileLoadedEventBroadcast = bInitialSearchCompleted;
|
|
for (;;)
|
|
{
|
|
FEventContext EventContext;
|
|
EGatherStatus Status;
|
|
{
|
|
// Keep the LLM scope limited so it does not surround the broadcast which calls external code
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
FClassInheritanceContext InheritanceContext;
|
|
FClassInheritanceBuffer InheritanceBuffer;
|
|
GetInheritanceContextWithRequiredLock(InterfaceScopeLock, InheritanceContext, InheritanceBuffer);
|
|
if (IsInGameThread())
|
|
{
|
|
// Process any deferred events. Required since deferred events would block sending the FileLoadedEvent
|
|
FScopeLock DeferredEventsLock(&DeferredEventsCriticalSection);
|
|
EventContext = MoveTemp(DeferredEvents);
|
|
DeferredEvents.Clear();
|
|
}
|
|
|
|
GuardedData.WaitForGathererIdleIfSynchronous();
|
|
|
|
UE::AssetRegistry::Impl::FTickContext TickContext(GuardedData, EventContext, InheritanceContext);
|
|
TickContext.bHandleCompletion = true;
|
|
TickContext.bHandleDeferred = true;
|
|
Status = GuardedData.TickGatherer(TickContext);
|
|
}
|
|
#if WITH_EDITOR
|
|
UE::AssetRegistry::Impl::FInterruptionContext InterruptionContext;
|
|
ProcessLoadedAssetsToUpdateCache(EventContext, Status, InterruptionContext);
|
|
#endif
|
|
Broadcast(EventContext, true /* bAllowFileLoadedEvent */);
|
|
bLocalHasSentFileLoadedEventBroadcast |= EventContext.bHasSentFileLoadedEventBroadcast;
|
|
if (!IsTickActive(Status) && Status != EGatherStatus::WaitingForEvents)
|
|
{
|
|
if (Status == EGatherStatus::UnableToProgress)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Display,
|
|
TEXT("UAssetRegistryImpl::WaitForCompletion exiting without completing because TickGatherer returned UnableToProgress. "
|
|
"IsInGameThread() == %s; IsEngineStartupModuleLoadingComplete() == %s"),
|
|
IsInGameThread() ? TEXT("TRUE") : TEXT("FALSE"),
|
|
IsEngineStartupModuleLoadingComplete() ? TEXT("TRUE") : TEXT("FALSE"));
|
|
}
|
|
else if (Status == EGatherStatus::Complete && bInitialSearchStarted)
|
|
{
|
|
// We only perform this validation if we are in a context where we expect the initial search to occur at all
|
|
// In some commandlets, e.g., we do not expect to run the initial search at all
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
if (!GuardedData.IsInitialSearchCompleted())
|
|
{
|
|
UE_LOG(LogAssetRegistry, Error,
|
|
TEXT("Exiting from UAssetRegistryImpl::WaitForCompletion but IsInitialSearchCompleted is still false."
|
|
"EventContext.bHasSentFileLoadedEventBroadcast == %s; IsInGameThread() == %s"),
|
|
EventContext.bHasSentFileLoadedEventBroadcast ? TEXT("TRUE") : TEXT("FALSE"),
|
|
IsInGameThread() ? TEXT("TRUE") : TEXT("FALSE"));
|
|
}
|
|
else
|
|
{
|
|
// If we are the main thread and we are exiting this function, one of two things should be true:
|
|
// a) The search was completed before we enter this function (i.e., bInitialSearchCompleted == true); or
|
|
// b) The search has completed during this function and, as the game thread, we have broadcast the FileLoadedEvent
|
|
// (i.e., EventContext.bHasSentFileLoadedEventBroadcast == true)
|
|
// Otherwise, something has gone wrong
|
|
ensureMsgf(bLocalHasSentFileLoadedEventBroadcast || bInitialSearchCompleted || !IsInGameThread(),
|
|
TEXT("Exiting from UAssetRegistryImpl::WaitForCompletion in an inconsistent state. "
|
|
"bLocalHasSentFileLoadedEventBroadcast == %s; EventContext.bHasSentFileLoadedEventBroadcast == %s; bInitialSearchCompleted == %s; IsInGameThread() == %s"),
|
|
bLocalHasSentFileLoadedEventBroadcast ? TEXT("TRUE") : TEXT("FALSE"),
|
|
EventContext.bHasSentFileLoadedEventBroadcast ? TEXT("TRUE") : TEXT("FALSE"),
|
|
bInitialSearchCompleted ? TEXT("TRUE") : TEXT("FALSE"),
|
|
IsInGameThread() ? TEXT("TRUE") : TEXT("FALSE"));
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
else if (Status == EGatherStatus::WaitingForEvents)
|
|
{
|
|
// If we are able to Broadcast events, then we will be able to do the broadcast and move past this status
|
|
// to completion. But if we cannot broadcast events, then we cannot complete the gather and have to exit.
|
|
// We only guarantee completion of the gather when our caller is in the right state to be able to broadcast.
|
|
// Some callers might not expect this, so issue a warning explaining the problem.
|
|
if (!CanBroadcastEvents())
|
|
{
|
|
const TCHAR* Reason = nullptr;
|
|
if (!IsInGameThread())
|
|
{
|
|
Reason = TEXT("caller is !IsInGameThread()");
|
|
}
|
|
else if (FUObjectThreadContext::Get().IsRoutingPostLoad)
|
|
{
|
|
Reason = TEXT("caller is IsRoutingPostLoad");
|
|
}
|
|
else
|
|
{
|
|
Reason = TEXT("<unknownreason>");
|
|
}
|
|
|
|
UE_LOG(LogAssetRegistry, Warning,
|
|
TEXT("Exiting from UAssetRegistryImpl::WaitForCompletion with gather almost completed ")
|
|
TEXT("but missing the final broadcast of events, so IsInitialSearchCompleted is still false. ")
|
|
TEXT("We cannot execute the broadcast because %s."), Reason);
|
|
break;
|
|
}
|
|
}
|
|
|
|
FThreadHeartBeat::Get().HeartBeat();
|
|
if (Status == EGatherStatus::TickActiveGatherActive && bAsyncGathering)
|
|
{
|
|
// Sleep long enough to avoid causing contention on the CriticalSection in GetAndTrimSearchResults
|
|
constexpr float SleepTimeSeconds = 0.010f;
|
|
FPlatformProcess::SleepNoStats(SleepTimeSeconds);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UAssetRegistryImpl::WaitForPremadeAssetRegistry()
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UAssetRegistryImpl::WaitForPremadeAssetRegistry);
|
|
using namespace UE::AssetRegistry::Impl;
|
|
|
|
FEventContext EventContext;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
FClassInheritanceContext InheritanceContext;
|
|
FClassInheritanceBuffer InheritanceBuffer;
|
|
GetInheritanceContextWithRequiredLock(InterfaceScopeLock, InheritanceContext, InheritanceBuffer);
|
|
GuardedData.ConditionalLoadPremadeAssetRegistry(InterfaceScopeLock, EventContext);
|
|
}
|
|
Broadcast(EventContext);
|
|
}
|
|
|
|
void UAssetRegistryImpl::ClearGathererCache()
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.ClearGathererCache();
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::ClearGathererCache()
|
|
{
|
|
if (GlobalGatherer)
|
|
{
|
|
GlobalGatherer->ClearCache();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::WaitForPackage(const FString& PackageName)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UAssetRegistryImpl::WaitForPackage);
|
|
|
|
if (!GuardedData.IsGathering())
|
|
{
|
|
// IsGathering uses relaxed memory order, so it is possible that another thread has just finished
|
|
// marking IsGathering=false and has not finished writing the data it gathered. But that's not
|
|
// a problem, because to read that data, the caller will need to enter the critical section which was
|
|
// held by the other thread that wrote the data, and entering a critical section waits on a memory fence.
|
|
return;
|
|
}
|
|
FString LocalPath;
|
|
if (!FPackageName::TryConvertLongPackageNameToFilename(PackageName, LocalPath))
|
|
{
|
|
return;
|
|
}
|
|
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.TickGatherPackage(InterfaceScopeLock, EventContext, PackageName, LocalPath);
|
|
}
|
|
Broadcast(EventContext);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::HasAssets(const FName PackagePath, const bool bRecursive) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
return GuardedData.HasAssets(PackagePath, bRecursive);
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
bool FAssetRegistryImpl::HasAssets(const FName PackagePath, const bool bRecursive) const
|
|
{
|
|
bool bHasAssets = State.HasAssets(PackagePath, true /*bARFiltering*/);
|
|
|
|
if (!bHasAssets && bRecursive)
|
|
{
|
|
CachedPathTree.EnumerateSubPaths(PackagePath, [this, &bHasAssets](FName SubPath)
|
|
{
|
|
bHasAssets = State.HasAssets(SubPath, true /*bARFiltering*/);
|
|
return !bHasAssets;
|
|
});
|
|
}
|
|
|
|
return bHasAssets;
|
|
}
|
|
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetAssetsByPackageName(FName PackageName, TArray<FAssetData>& OutAssetData, bool bIncludeOnlyOnDiskAssets, bool bSkipARFilteredAssets) const
|
|
{
|
|
FARFilter Filter;
|
|
Filter.PackageNames.Add(PackageName);
|
|
Filter.bIncludeOnlyOnDiskAssets = bIncludeOnlyOnDiskAssets;
|
|
return GetAssets(Filter, OutAssetData, bSkipARFilteredAssets);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetAssetsByPath(FName PackagePath, TArray<FAssetData>& OutAssetData, bool bRecursive, bool bIncludeOnlyOnDiskAssets) const
|
|
{
|
|
FARFilter Filter;
|
|
Filter.bRecursivePaths = bRecursive;
|
|
Filter.PackagePaths.Add(PackagePath);
|
|
Filter.bIncludeOnlyOnDiskAssets = bIncludeOnlyOnDiskAssets;
|
|
return GetAssets(Filter, OutAssetData);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetAssetsByPaths(TArray<FName> PackagePaths, TArray<FAssetData>& OutAssetData, bool bRecursive, bool bIncludeOnlyOnDiskAssets) const
|
|
{
|
|
FARFilter Filter;
|
|
Filter.bRecursivePaths = bRecursive;
|
|
Filter.PackagePaths = MoveTemp(PackagePaths);
|
|
Filter.bIncludeOnlyOnDiskAssets = bIncludeOnlyOnDiskAssets;
|
|
return GetAssets(Filter, OutAssetData);
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::EnumerateAssetsByPathNoTags(FName PackagePath,
|
|
TFunctionRef<bool(const FAssetData&)> Callback, bool bRecursive, bool bIncludeOnlyOnDiskAssets) const
|
|
{
|
|
if (PackagePath.IsNone())
|
|
{
|
|
return;
|
|
}
|
|
FARFilter Filter;
|
|
Filter.bRecursivePaths = bRecursive;
|
|
Filter.PackagePaths.Add(PackagePath);
|
|
Filter.bIncludeOnlyOnDiskAssets = bIncludeOnlyOnDiskAssets;
|
|
|
|
// CompileFilter takes an inheritance context, but only to handle filters with recursive classes, which we are not using here
|
|
UE::AssetRegistry::Impl::FClassInheritanceContext EmptyInheritanceContext;
|
|
FARCompiledFilter CompiledFilter;
|
|
CompileFilter(EmptyInheritanceContext, Filter, CompiledFilter);
|
|
|
|
TSet<FName> PackagesToSkip;
|
|
if (!bIncludeOnlyOnDiskAssets)
|
|
{
|
|
bool bStopIteration;
|
|
Utils::EnumerateMemoryAssetsHelper(CompiledFilter, PackagesToSkip, bStopIteration,
|
|
[&Callback](const UObject* Object, FAssetData&& PartialAssetData)
|
|
{
|
|
return Callback(PartialAssetData);
|
|
}, true /* bSkipARFilteredAssets */);
|
|
if (bStopIteration)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
EnumerateDiskAssets(CompiledFilter, PackagesToSkip, Callback, UE::AssetRegistry::EEnumerateAssetsFlags::None);
|
|
}
|
|
|
|
}
|
|
|
|
static FTopLevelAssetPath TryConvertShortTypeNameToPathName(FName ClassName)
|
|
{
|
|
FTopLevelAssetPath ClassPathName;
|
|
if (ClassName != NAME_None)
|
|
{
|
|
FString ShortClassName = ClassName.ToString();
|
|
ClassPathName = UClass::TryConvertShortTypeNameToPathName<UStruct>(*ShortClassName, ELogVerbosity::Warning, TEXT("AssetRegistry using deprecated function"));
|
|
UE_CLOG(ClassPathName.IsNull(), LogClass, Error, TEXT("Failed to convert short class name %s to class path name."), *ShortClassName);
|
|
}
|
|
return ClassPathName;
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetAssetsByClass(FTopLevelAssetPath ClassPathName, TArray<FAssetData>& OutAssetData, bool bSearchSubClasses) const
|
|
{
|
|
FARFilter Filter;
|
|
Filter.ClassPaths.Add(ClassPathName);
|
|
Filter.bRecursiveClasses = bSearchSubClasses;
|
|
return GetAssets(Filter, OutAssetData);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetAssetsByTags(const TArray<FName>& AssetTags, TArray<FAssetData>& OutAssetData) const
|
|
{
|
|
FARFilter Filter;
|
|
for (const FName& AssetTag : AssetTags)
|
|
{
|
|
Filter.TagsAndValues.Add(AssetTag);
|
|
}
|
|
return GetAssets(Filter, OutAssetData);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetAssetsByTagValues(const TMultiMap<FName, FString>& AssetTagsAndValues, TArray<FAssetData>& OutAssetData) const
|
|
{
|
|
FARFilter Filter;
|
|
for (const auto& AssetTagsAndValue : AssetTagsAndValues)
|
|
{
|
|
Filter.TagsAndValues.Add(AssetTagsAndValue.Key, AssetTagsAndValue.Value);
|
|
}
|
|
return GetAssets(Filter, OutAssetData);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetAssets(const FARFilter& InFilter, TArray<FAssetData>& OutAssetData,
|
|
bool bSkipARFilteredAssets) const
|
|
{
|
|
using namespace UE::AssetRegistry::Utils;
|
|
|
|
FARCompiledFilter CompiledFilter;
|
|
CompileFilter(InFilter, CompiledFilter);
|
|
if (CompiledFilter.IsEmpty() || !IsFilterValid(CompiledFilter))
|
|
{
|
|
return false;
|
|
}
|
|
return GetAssets(CompiledFilter, OutAssetData, bSkipARFilteredAssets);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetAssets(const FARCompiledFilter& CompiledFilter, TArray<FAssetData>& OutAssetData,
|
|
bool bSkipARFilteredAssets) const
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UAssetRegistryImpl::GetAssets);
|
|
using namespace UE::AssetRegistry::Utils;
|
|
|
|
const UE::AssetRegistry::EEnumerateAssetsFlags Flags = bSkipARFilteredAssets ? UE::AssetRegistry::EEnumerateAssetsFlags::None : UE::AssetRegistry::EEnumerateAssetsFlags::AllowUnfilteredArAssets;
|
|
EnumerateAssetsEvent.Broadcast(CompiledFilter, Flags);
|
|
|
|
TSet<FName> PackagesToSkip;
|
|
if (!CompiledFilter.bIncludeOnlyOnDiskAssets)
|
|
{
|
|
bool bStopIterationUnused;
|
|
EnumerateMemoryAssets(CompiledFilter, PackagesToSkip, bStopIterationUnused,
|
|
InterfaceLock, GuardedData.GetState(),
|
|
[&OutAssetData](FAssetData&& AssetData)
|
|
{
|
|
OutAssetData.Add(MoveTemp(AssetData));
|
|
return true;
|
|
}, bSkipARFilteredAssets);
|
|
}
|
|
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.EnumerateDiskAssets(CompiledFilter, PackagesToSkip, [&OutAssetData](const FAssetData& AssetData)
|
|
{
|
|
OutAssetData.Emplace(AssetData);
|
|
return true;
|
|
}, Flags);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetInMemoryAssets(const FARFilter& InFilter, TArray<FAssetData>& OutAssetData, bool bSkipARFilteredAssets) const
|
|
{
|
|
using namespace UE::AssetRegistry::Utils;
|
|
|
|
FARCompiledFilter CompiledFilter;
|
|
CompileFilter(InFilter, CompiledFilter);
|
|
if (CompiledFilter.IsEmpty() || !IsFilterValid(CompiledFilter))
|
|
{
|
|
return false;
|
|
}
|
|
return GetInMemoryAssets(CompiledFilter, OutAssetData, bSkipARFilteredAssets);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetInMemoryAssets(const FARCompiledFilter& CompiledFilter, TArray<FAssetData>& OutAssetData, bool bSkipARFilteredAssets) const
|
|
{
|
|
using namespace UE::AssetRegistry::Utils;
|
|
|
|
TSet<FName> PackagesToSkipUnused;
|
|
bool bStopIterationUnused;
|
|
EnumerateMemoryAssets(CompiledFilter, PackagesToSkipUnused, bStopIterationUnused,
|
|
InterfaceLock, GuardedData.GetState(),
|
|
[&OutAssetData](FAssetData&& AssetData)
|
|
{
|
|
OutAssetData.Add(MoveTemp(AssetData));
|
|
return true;
|
|
}, bSkipARFilteredAssets);
|
|
return true;
|
|
}
|
|
|
|
bool UAssetRegistryImpl::EnumerateAssets(const FARFilter& InFilter, TFunctionRef<bool(const FAssetData&)> Callback,
|
|
UE::AssetRegistry::EEnumerateAssetsFlags InEnumerateFlags) const
|
|
{
|
|
FARCompiledFilter CompiledFilter;
|
|
CompileFilter(InFilter, CompiledFilter);
|
|
return EnumerateAssets(CompiledFilter, Callback, InEnumerateFlags);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::EnumerateAssets(const FARCompiledFilter& InFilter, TFunctionRef<bool(const FAssetData&)> Callback,
|
|
UE::AssetRegistry::EEnumerateAssetsFlags InEnumerateFlags) const
|
|
{
|
|
using namespace UE::AssetRegistry::Utils;
|
|
|
|
// Verify filter input. If all assets are needed, use EnumerateAllAssets() instead.
|
|
if (InFilter.IsEmpty() || !IsFilterValid(InFilter))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
EnumerateAssetsEvent.Broadcast(InFilter, InEnumerateFlags);
|
|
|
|
TSet<FName> PackagesToSkip;
|
|
if (!InFilter.bIncludeOnlyOnDiskAssets)
|
|
{
|
|
bool bStopIteration;
|
|
EnumerateMemoryAssets(InFilter, PackagesToSkip, bStopIteration,
|
|
InterfaceLock, GuardedData.GetState(),
|
|
[&Callback](FAssetData&& AssetData)
|
|
{
|
|
return Callback(AssetData);
|
|
}, !EnumHasAnyFlags(InEnumerateFlags, UE::AssetRegistry::EEnumerateAssetsFlags::AllowUnfilteredArAssets));
|
|
if (bStopIteration)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
TArray<FAssetData, TInlineAllocator<128>> FoundAssets;
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.EnumerateDiskAssets(InFilter, PackagesToSkip, [&FoundAssets](const FAssetData& AssetData)
|
|
{
|
|
FoundAssets.Emplace(AssetData);
|
|
return true;
|
|
}, InEnumerateFlags);
|
|
}
|
|
for (const FAssetData& AssetData : FoundAssets)
|
|
{
|
|
if (!Callback(AssetData))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
namespace Utils
|
|
{
|
|
|
|
TOptional<FAssetDataTagMap> AddNonOverlappingTags(FAssetData& ExistingAssetData, const FAssetData& NewAssetData)
|
|
{
|
|
TOptional<FAssetDataTagMap> ModifiedTags;
|
|
NewAssetData.TagsAndValues.ForEach([&ExistingAssetData, &ModifiedTags](const TPair<FName, FAssetTagValueRef>& TagPair)
|
|
{
|
|
if (ModifiedTags)
|
|
{
|
|
if (!ModifiedTags->Contains(TagPair.Key))
|
|
{
|
|
ModifiedTags->Add(TagPair.Key, TagPair.Value.GetStorageString());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!ExistingAssetData.TagsAndValues.Contains(TagPair.Key))
|
|
{
|
|
ModifiedTags.Emplace(ExistingAssetData.TagsAndValues.CopyMap());
|
|
ModifiedTags->Add(TagPair.Key, TagPair.Value.GetStorageString());
|
|
}
|
|
}
|
|
});
|
|
return ModifiedTags;
|
|
}
|
|
|
|
void EnumerateMemoryAssetsHelper(const FARCompiledFilter& InFilter, TSet<FName>& OutPackageNamesWithAssets,
|
|
bool& bOutStopIteration, TFunctionRef<bool(const UObject* Object, FAssetData&& PartialAssetData)> Callback,
|
|
bool bSkipARFilteredAssets)
|
|
{
|
|
checkf(IsInGameThread() || IsInAsyncLoadingThread() || IsInParallelLoadingThread(), TEXT("Enumerating in-memory assets can only be done on the game thread or in the loader, there are too many GetAssetRegistryTags() still not thread-safe."));
|
|
bOutStopIteration = false;
|
|
UE_TRACK_REFERENCING_OPNAME_SCOPED(PackageAccessTrackingOps::NAME_ResetContext);
|
|
|
|
// Skip assets that were loaded for diffing
|
|
const uint32 FilterWithoutPackageFlags = InFilter.WithoutPackageFlags | PKG_ForDiffing;
|
|
const uint32 FilterWithPackageFlags = InFilter.WithPackageFlags;
|
|
|
|
struct FFilterData
|
|
{
|
|
const UObject* Object;
|
|
const UPackage* Package;
|
|
FString PackageNameStr;
|
|
FSoftObjectPath ObjectPath;
|
|
};
|
|
|
|
/**
|
|
* The portions of the filter that are safe to execute even in the UObject global hash lock in FThreadSafeObjectIterator
|
|
* Returns true if the object passes the filter and should be copied into an array for calling the rest of the filter
|
|
* outside the lock.
|
|
*/
|
|
auto PassesLockSafeFilter =
|
|
[&InFilter, bSkipARFilteredAssets, FilterWithoutPackageFlags, FilterWithPackageFlags]
|
|
(const UObject* Obj, FFilterData& FilterData)
|
|
{
|
|
if (!Obj->IsAsset())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Skip assets that are currently loading
|
|
if (Obj->HasAnyFlags(RF_NeedLoad))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
check(!Obj->GetPackage()->HasAnyPackageFlags(PKG_PlayInEditor));
|
|
check(!Obj->GetOutermostObject()->GetPackage()->HasAnyPackageFlags(PKG_PlayInEditor));
|
|
|
|
FilterData.Package = Obj->GetOutermost();
|
|
|
|
// Skip assets with any of the specified 'without' package flags
|
|
if (FilterData.Package->HasAnyPackageFlags(FilterWithoutPackageFlags))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Skip assets without any the specified 'with' packages flags
|
|
if (!FilterData.Package->HasAllPackagesFlags(FilterWithPackageFlags))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Skip classes that report themselves as assets but that the editor AssetRegistry is currently not counting as assets
|
|
if (bSkipARFilteredAssets && UE::AssetRegistry::FFiltering::ShouldSkipAsset(Obj))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Package name
|
|
const FName PackageName = FilterData.Package->GetFName();
|
|
|
|
if (InFilter.PackageNames.Num() && !InFilter.PackageNames.Contains(PackageName))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Asset Path
|
|
FilterData.ObjectPath = FSoftObjectPath::ConstructFromObject(Obj);
|
|
if (InFilter.SoftObjectPaths.Num() > 0)
|
|
{
|
|
if (!InFilter.SoftObjectPaths.Contains(FilterData.ObjectPath))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Package path
|
|
PackageName.ToString(FilterData.PackageNameStr);
|
|
if (InFilter.PackagePaths.Num() > 0)
|
|
{
|
|
const FName PackagePath = FName(*FPackageName::GetLongPackagePath(FilterData.PackageNameStr));
|
|
if (!InFilter.PackagePaths.Contains(PackagePath))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
FilterData.Object = Obj;
|
|
return true;
|
|
};
|
|
|
|
auto RunUnsafeFilterAndCallback =
|
|
[&Callback, &OutPackageNamesWithAssets]
|
|
(FFilterData& FilterData, bool& bOutContinue)
|
|
{
|
|
// We mark the package found for this passing asset, so that any followup search for assets on disk will not
|
|
// add a duplicate of this Asset. We do this here for convenience; it would be more correct to call it only for assets that
|
|
// pass the callers remaining filters inside of Callback
|
|
OutPackageNamesWithAssets.Add(FilterData.Package->GetFName());
|
|
|
|
// Could perhaps save some FName -> String conversions by creating this a bit earlier using the UObject constructor
|
|
// to get package name and path.
|
|
FAssetData PartialAssetData(MoveTemp(FilterData.PackageNameStr), FilterData.ObjectPath.ToString(),
|
|
FilterData.Object->GetClass()->GetClassPathName(), FAssetDataTagMap(),
|
|
FilterData.Package->GetChunkIDs(), FilterData.Package->GetPackageFlags());
|
|
|
|
// All filters passed, except for AssetRegistry filter; caller must check that one
|
|
bOutContinue = Callback(FilterData.Object, MoveTemp(PartialAssetData));
|
|
};
|
|
|
|
// Iterate over all in-memory assets to find the ones that pass the filter components
|
|
if (InFilter.ClassPaths.Num() > 0 || InFilter.PackageNames.Num() > 0)
|
|
{
|
|
TArray<UObject*, TInlineAllocator<10>> InMemoryObjects;
|
|
if (InFilter.ClassPaths.Num())
|
|
{
|
|
for (FTopLevelAssetPath ClassName : InFilter.ClassPaths)
|
|
{
|
|
UClass* Class = FindObject<UClass>(ClassName);
|
|
if (Class != nullptr)
|
|
{
|
|
ForEachObjectOfClass(Class, [&InMemoryObjects](UObject* Object)
|
|
{
|
|
InMemoryObjects.Add(Object);
|
|
}, false /* bIncludeDerivedClasses */, RF_NoFlags);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (FName PackageName : InFilter.PackageNames)
|
|
{
|
|
UPackage* Package = FindObjectFast<UPackage>(nullptr, PackageName);
|
|
if (Package != nullptr)
|
|
{
|
|
// Store objects in an intermediate rather than calling FilterInMemoryObjectLambda on them directly
|
|
// because the callback is arbitrary code and might create UObjects, which is disallowed in
|
|
// ForEachObjectWithPackage
|
|
ForEachObjectWithPackage(Package, [&InMemoryObjects](UObject* Object)
|
|
{
|
|
// Avoid adding an element to InMemoryObjects for every UObject
|
|
// There could be many UObjects (thousands) but only a single Asset
|
|
if (Object->IsAsset())
|
|
{
|
|
InMemoryObjects.Add(Object);
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
FFilterData ScratchFilterData;
|
|
for (const UObject* Object : InMemoryObjects)
|
|
{
|
|
if (PassesLockSafeFilter(Object, ScratchFilterData))
|
|
{
|
|
bool bContinue = true;
|
|
RunUnsafeFilterAndCallback(ScratchFilterData, bContinue);
|
|
if (!bContinue)
|
|
{
|
|
bOutStopIteration = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TArray<FFilterData> FirstPassFilterResults;
|
|
FFilterData ScratchFilterData;
|
|
for (FThreadSafeObjectIterator ObjIt; ObjIt; ++ObjIt)
|
|
{
|
|
if (PassesLockSafeFilter(*ObjIt, ScratchFilterData))
|
|
{
|
|
FirstPassFilterResults.Add(MoveTemp(ScratchFilterData));
|
|
}
|
|
}
|
|
|
|
for (FFilterData& FilterData : FirstPassFilterResults)
|
|
{
|
|
bool bContinue = true;
|
|
RunUnsafeFilterAndCallback(FilterData, bContinue);
|
|
if (!bContinue)
|
|
{
|
|
bOutStopIteration = true;
|
|
return;
|
|
}
|
|
|
|
FPlatformMisc::PumpEssentialAppMessages();
|
|
}
|
|
}
|
|
}
|
|
|
|
void EnumerateMemoryAssets(const FARCompiledFilter& InFilter, TSet<FName>& OutPackageNamesWithAssets,
|
|
bool& bOutStopIteration, UE::AssetRegistry::Private::FInterfaceRWLock& InterfaceLock, const FAssetRegistryState& GuardedDataState,
|
|
TFunctionRef<bool(FAssetData&&)> Callback, bool bSkipARFilteredAssets)
|
|
{
|
|
check(!InFilter.IsEmpty() && Utils::IsFilterValid(InFilter));
|
|
|
|
// Avoid contending with the background thread every time we take the interface lock below.
|
|
IAssetRegistry::FPauseBackgroundProcessingScope PauseProcessingScopeGuard;
|
|
|
|
EnumerateMemoryAssetsHelper(InFilter, OutPackageNamesWithAssets, bOutStopIteration,
|
|
[&InFilter, &Callback, &InterfaceLock, &GuardedDataState](const UObject* Object, FAssetData&& PartialAssetData)
|
|
{
|
|
FAssetRegistryTagsContextData Context(Object, EAssetRegistryTagsCaller::AssetRegistryQuery);
|
|
Object->GetAssetRegistryTags(Context, PartialAssetData);
|
|
{
|
|
// GetAssetRegistryTags with EAssetRegistryTagsCaller::AssetRegistryQuery does not add some tags that
|
|
// are too expensive to regularly compute but that exist in the on-disk Asset from SavePackage.
|
|
// Our contract for on-disk versus in-memory tags is that in-memory tags override on-disk tags, but we
|
|
// keep any on-disk tags that do not exist in the in-memory tags because they may be extended tags.
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
const FAssetData* OnDiskAssetData = GuardedDataState.GetAssetByObjectPath(FSoftObjectPath::ConstructFromObject(Object));
|
|
if (OnDiskAssetData)
|
|
{
|
|
TOptional<FAssetDataTagMap> ModifiedTags = Utils::AddNonOverlappingTags(PartialAssetData, *OnDiskAssetData);
|
|
if (ModifiedTags)
|
|
{
|
|
PartialAssetData.TagsAndValues = FAssetDataTagMapSharedView(MoveTemp(*ModifiedTags));
|
|
}
|
|
#if !WITH_EDITORONLY_DATA
|
|
// In non-editor builds, UObject::GetChunkIds returns an empty set.
|
|
// Like our contract for tags, when the information is missing from the UObject, our contract
|
|
// for that information in AssetRegistry queries is that we return the on-disk version of the data.
|
|
// The on-disk version of the data for GetChunkIds is the data that was stored in the generated
|
|
// AssetRegistry by calling AddChunkId for each chunkID that the cooker found the Asset to be in.
|
|
PartialAssetData.SetChunkIDs(OnDiskAssetData->GetChunkIDs());
|
|
#endif
|
|
}
|
|
}
|
|
// After adding tags, PartialAssetData is now a full AssetData
|
|
|
|
// Tags and values
|
|
if (InFilter.TagsAndValues.Num() > 0)
|
|
{
|
|
bool bMatch = false;
|
|
for (const TPair<FName, TOptional<FString>>& FilterPair : InFilter.TagsAndValues)
|
|
{
|
|
FAssetTagValueRef RegistryValue = PartialAssetData.TagsAndValues.FindTag(FilterPair.Key);
|
|
|
|
if (RegistryValue.IsSet() && (!FilterPair.Value.IsSet() || RegistryValue == FilterPair.Value.GetValue()))
|
|
{
|
|
bMatch = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!bMatch)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// All filters passed
|
|
return Callback(MoveTemp(PartialAssetData));
|
|
}, bSkipARFilteredAssets);
|
|
}
|
|
|
|
}
|
|
|
|
void FAssetRegistryImpl::EnumerateDiskAssets(const FARCompiledFilter& InFilter, TSet<FName>& PackagesToSkip,
|
|
TFunctionRef<bool(const FAssetData&)> Callback, UE::AssetRegistry::EEnumerateAssetsFlags InEnumerateFlags) const
|
|
{
|
|
check(!InFilter.IsEmpty() && Utils::IsFilterValid(InFilter));
|
|
PackagesToSkip.Append(CachedEmptyPackages);
|
|
State.EnumerateAssets(InFilter, PackagesToSkip, Callback, InEnumerateFlags);
|
|
}
|
|
|
|
}
|
|
|
|
FAssetData UAssetRegistryImpl::GetAssetByObjectPath(const FSoftObjectPath& ObjectPath, bool bIncludeOnlyOnDiskAssets, bool bSkipARFilteredAssets) const
|
|
{
|
|
if (!bIncludeOnlyOnDiskAssets)
|
|
{
|
|
TStringBuilder<FName::StringBufferSize> Builder;
|
|
ObjectPath.ToString(Builder);
|
|
UObject* Asset = FindObject<UObject>(nullptr, *Builder);
|
|
|
|
if (Asset)
|
|
{
|
|
if (!bSkipARFilteredAssets || !UE::AssetRegistry::FFiltering::ShouldSkipAsset(Asset))
|
|
{
|
|
return FAssetData(Asset, FAssetData::ECreationFlags::None /** Do not allow blueprint classes */,
|
|
EAssetRegistryTagsCaller::AssetRegistryQuery);
|
|
}
|
|
else
|
|
{
|
|
return FAssetData();
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
const FAssetRegistryState& State = GuardedData.GetState();
|
|
const FAssetData* FoundData = State.GetAssetByObjectPath(ObjectPath);
|
|
return (FoundData && !State.IsPackageUnmountedAndFiltered(FoundData->PackageName)
|
|
&& (!bSkipARFilteredAssets || !GuardedData.ShouldSkipAsset(FoundData->AssetClassPath, FoundData->PackageFlags))) ? *FoundData : FAssetData();
|
|
}
|
|
}
|
|
|
|
FAssetData UAssetRegistryImpl::GetAssetByObjectPath(const FName ObjectPath, bool bIncludeOnlyOnDiskAssets) const
|
|
{
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS;
|
|
return GetAssetByObjectPath(FSoftObjectPath(ObjectPath.ToString()), bIncludeOnlyOnDiskAssets);
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS;
|
|
}
|
|
|
|
UE::AssetRegistry::EExists UAssetRegistryImpl::TryGetAssetByObjectPath(const FSoftObjectPath& ObjectPath, FAssetData& OutAssetData) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
const bool bAssetRegistryHasFullKnowledge = GuardedData.IsSearchAllAssets() && !GuardedData.IsGathering();
|
|
const FAssetRegistryState& State = GuardedData.GetState();
|
|
const FAssetData* FoundData = State.GetAssetByObjectPath(ObjectPath);
|
|
if (!FoundData)
|
|
{
|
|
if (!bAssetRegistryHasFullKnowledge)
|
|
{
|
|
return UE::AssetRegistry::EExists::Unknown;
|
|
}
|
|
return UE::AssetRegistry::EExists::DoesNotExist;
|
|
}
|
|
OutAssetData = *FoundData;
|
|
return UE::AssetRegistry::EExists::Exists;
|
|
}
|
|
|
|
UE::AssetRegistry::EExists UAssetRegistryImpl::TryGetAssetPackageData(const FName PackageName, FAssetPackageData& OutAssetPackageData) const
|
|
{
|
|
FName OutCorrectCasePackageName;
|
|
return TryGetAssetPackageData(PackageName, OutAssetPackageData, OutCorrectCasePackageName);
|
|
}
|
|
|
|
UE::AssetRegistry::EExists UAssetRegistryImpl::TryGetAssetPackageData(const FName PackageName, FAssetPackageData& OutAssetPackageData, FName& OutCorrectCasePackageName) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
const bool bLocalIsGathering = GuardedData.IsGathering();
|
|
const bool bAssetRegistryHasFullKnowledge = GuardedData.IsSearchAllAssets() && !bLocalIsGathering;
|
|
const FAssetRegistryState& State = GuardedData.GetState();
|
|
const FAssetPackageData* FoundData = State.GetAssetPackageData(PackageName, OutCorrectCasePackageName);
|
|
if (!FoundData)
|
|
{
|
|
if (!bAssetRegistryHasFullKnowledge)
|
|
{
|
|
return UE::AssetRegistry::EExists::Unknown;
|
|
}
|
|
return UE::AssetRegistry::EExists::DoesNotExist;
|
|
}
|
|
// Currently when we cook we mark FAssetPackageData as being from the IoDispatcher however that isn't true until the content
|
|
// is staged. The correct location will be determined once the registry scan is completed, but until then we should ignore any
|
|
// IoDispatcher located results since they might be stale and about to be overwritten by filesystem data found during scanning.
|
|
// Note, we also must ensure that we ignore found results before the engine startup has completed since we might not have started
|
|
// scanning yet and thus can't trust the found results
|
|
else if (FoundData->GetPackageLocation() == FPackageName::EPackageLocationFilter::IoDispatcher && (bLocalIsGathering || !IsEngineStartupModuleLoadingComplete()))
|
|
{
|
|
return UE::AssetRegistry::EExists::Unknown;
|
|
}
|
|
OutAssetPackageData = *FoundData;
|
|
return UE::AssetRegistry::EExists::Exists;
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetAllAssets(TArray<FAssetData>& OutAssetData, bool bIncludeOnlyOnDiskAssets) const
|
|
{
|
|
const double GetAllAssetsStartTime = FPlatformTime::Seconds();
|
|
TSet<FName> PackageNamesToSkip;
|
|
|
|
// All in memory assets
|
|
if (!bIncludeOnlyOnDiskAssets)
|
|
{
|
|
bool bStopIterationUnused;
|
|
UE::AssetRegistry::Utils::EnumerateAllMemoryAssets(PackageNamesToSkip, bStopIterationUnused,
|
|
[&OutAssetData](FAssetData&& AssetData)
|
|
{
|
|
OutAssetData.Add(MoveTemp(AssetData));
|
|
return true;
|
|
});
|
|
}
|
|
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.EnumerateAllDiskAssets(PackageNamesToSkip,
|
|
[&OutAssetData](const FAssetData& AssetData)
|
|
{
|
|
OutAssetData.Add(AssetData);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
UE_LOG(LogAssetRegistry, VeryVerbose, TEXT("GetAllAssets completed in %0.4f seconds"), FPlatformTime::Seconds() - GetAllAssetsStartTime);
|
|
return true;
|
|
}
|
|
|
|
bool UAssetRegistryImpl::EnumerateAllAssets(TFunctionRef<bool(const FAssetData&)> Callback, UE::AssetRegistry::EEnumerateAssetsFlags InEnumerateFlags) const
|
|
{
|
|
using namespace UE::AssetRegistry;
|
|
|
|
TSet<FName> PackageNamesToSkip;
|
|
// All in memory assets
|
|
if (!EnumHasAnyFlags(InEnumerateFlags, EEnumerateAssetsFlags::OnlyOnDiskAssets))
|
|
{
|
|
bool bStopIteration;
|
|
UE::AssetRegistry::Utils::EnumerateAllMemoryAssets(PackageNamesToSkip, bStopIteration,
|
|
[&Callback](FAssetData&& AssetData)
|
|
{
|
|
return Callback(AssetData);
|
|
});
|
|
if (bStopIteration)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// We have to call the callback on a copy rather than a reference since the callback may reenter the lock
|
|
TArray<FAssetData, TInlineAllocator<128>> OnDiskAssetDatas;
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
OnDiskAssetDatas.Reserve(GuardedData.State.GetNumAssets());
|
|
GuardedData.EnumerateAllDiskAssets(PackageNamesToSkip,
|
|
[&OnDiskAssetDatas](const FAssetData& AssetData)
|
|
{
|
|
OnDiskAssetDatas.Add(AssetData);
|
|
return true;
|
|
}, InEnumerateFlags);
|
|
}
|
|
|
|
if (EnumHasAnyFlags(InEnumerateFlags, EEnumerateAssetsFlags::Parallel))
|
|
{
|
|
ParallelFor(OnDiskAssetDatas.Num(), [&OnDiskAssetDatas, &Callback](int32 Index)
|
|
{
|
|
const FAssetData& AssetData = OnDiskAssetDatas[Index];
|
|
// The Callback returns bContinueIterating, and it would be nice to have a contract that we never
|
|
// call the Callback again after it returns bContinueIterating. But guaranteeing that contract in
|
|
// multithreaded code is not possible without taking a lock which is prohibitively expensive. So
|
|
// for the parallel version of EnumerateAllAssets, we require that the caller handle the Callback
|
|
// being called again even after it returns false. Since we have that requirement, and we don't
|
|
// expect it to be important for performance to early exit as soon as possible, we omit early exiting
|
|
// entirely to get a performance benefit from not needing to frequently test a contended atomic.
|
|
(void)Callback(AssetData);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
for (const FAssetData& AssetData : OnDiskAssetDatas)
|
|
{
|
|
if (!Callback(AssetData))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
namespace Utils
|
|
{
|
|
|
|
void EnumerateAllMemoryAssets(TSet<FName>& OutPackageNamesWithAssets, bool& bOutStopIteration,
|
|
TFunctionRef<bool(FAssetData&&)> Callback)
|
|
{
|
|
checkf(IsInGameThread() || IsInAsyncLoadingThread() || IsInParallelLoadingThread(), TEXT("Enumerating memory assets can only be done on the game thread or in the loader, there are too many GetAssetRegistryTags() still not thread-safe."));
|
|
bOutStopIteration = false;
|
|
for (FThreadSafeObjectIterator ObjIt; ObjIt; ++ObjIt)
|
|
{
|
|
if (ObjIt->IsAsset() && !UE::AssetRegistry::FFiltering::ShouldSkipAsset(*ObjIt))
|
|
{
|
|
FAssetData AssetData(*ObjIt, true /* bAllowBlueprintClass */);
|
|
OutPackageNamesWithAssets.Add(AssetData.PackageName);
|
|
if (!Callback(MoveTemp(AssetData)))
|
|
{
|
|
bOutStopIteration = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void FAssetRegistryImpl::EnumerateAllDiskAssets(TSet<FName>& PackageNamesToSkip, TFunctionRef<bool(const FAssetData&)> Callback, UE::AssetRegistry::EEnumerateAssetsFlags InEnumerateFlags) const
|
|
{
|
|
PackageNamesToSkip.Append(CachedEmptyPackages);
|
|
State.EnumerateAllAssets(PackageNamesToSkip, Callback, InEnumerateFlags);
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::GetPackagesByName(FStringView PackageName, TArray<FName>& OutPackageNames) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
const FAssetRegistryState& State = GuardedData.GetState();
|
|
UE_CLOG(GuardedData.IsInitialSearchStarted() && !GuardedData.IsInitialSearchCompleted(), LogAssetRegistry, Warning,
|
|
TEXT("GetPackagesByName has been called before AssetRegistry gather is complete and it does not wait. ")
|
|
TEXT("The search may return incomplete results."));
|
|
State.GetPackagesByName(PackageName, OutPackageNames);
|
|
|
|
}
|
|
|
|
FName UAssetRegistryImpl::GetFirstPackageByName(FStringView PackageName) const
|
|
{
|
|
FName LongPackageName;
|
|
bool bSearchAllAssets;
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
const FAssetRegistryState& State = GuardedData.GetState();
|
|
UE_CLOG(GuardedData.IsInitialSearchStarted() && !GuardedData.IsInitialSearchCompleted(), LogAssetRegistry, Warning,
|
|
TEXT("GetFirstPackageByName has been called before AssetRegistry gather is complete and it does not wait. ")
|
|
TEXT("The search may fail to find the package."));
|
|
LongPackageName = State.GetFirstPackageByName(PackageName);
|
|
bSearchAllAssets = GuardedData.IsSearchAllAssets();
|
|
}
|
|
#if WITH_EDITOR
|
|
if (!GIsEditor && !bSearchAllAssets)
|
|
{
|
|
// Temporary support for -game:
|
|
// When running editor.exe with -game, we do not have a cooked AssetRegistry and we do not scan either
|
|
// In that case, fall back to searching on disk if the search in the AssetRegistry (as expected) fails
|
|
// In the future we plan to avoid this situation by having -game run the scan as well
|
|
if (LongPackageName.IsNone())
|
|
{
|
|
UE_LOG(LogAssetRegistry, Warning,
|
|
TEXT("GetFirstPackageByName is being called in `-game` to resolve partial package name. ")
|
|
TEXT("This may cause a slow scan on disk. ")
|
|
TEXT("Consider using the fully qualified package name for better performance. "));
|
|
FString LongPackageNameString;
|
|
if (FPackageName::SearchForPackageOnDisk(FString(PackageName), &LongPackageNameString))
|
|
{
|
|
LongPackageName = FName(*LongPackageNameString);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
return LongPackageName;
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetDependencies(const FAssetIdentifier& AssetIdentifier, TArray<FAssetIdentifier>& OutDependencies, UE::AssetRegistry::EDependencyCategory Category, const UE::AssetRegistry::FDependencyQuery& Flags) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
return GuardedData.GetState().GetDependencies(AssetIdentifier, OutDependencies, Category, Flags);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetDependencies(const FAssetIdentifier& AssetIdentifier, TArray<FAssetDependency>& OutDependencies, UE::AssetRegistry::EDependencyCategory Category, const UE::AssetRegistry::FDependencyQuery& Flags) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
return GuardedData.GetState().GetDependencies(AssetIdentifier, OutDependencies, Category, Flags);
|
|
}
|
|
|
|
static void ConvertAssetIdentifiersToPackageNames(const TArray<FAssetIdentifier>& AssetIdentifiers, TArray<FName>& OutPackageNames)
|
|
{
|
|
// add all PackageNames :
|
|
OutPackageNames.Reserve(OutPackageNames.Num() + AssetIdentifiers.Num());
|
|
for (const FAssetIdentifier& AssetId : AssetIdentifiers)
|
|
{
|
|
if (AssetId.PackageName != NAME_None)
|
|
{
|
|
OutPackageNames.Add(AssetId.PackageName);
|
|
}
|
|
}
|
|
|
|
// make unique ; sort in previous contents of OutPackageNames to unique against them too
|
|
OutPackageNames.Sort( FNameFastLess() );
|
|
|
|
int UniqueNum = Algo::Unique( OutPackageNames );
|
|
OutPackageNames.SetNum(UniqueNum, EAllowShrinking::No);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetDependencies(FName PackageName, TArray<FName>& OutDependencies, UE::AssetRegistry::EDependencyCategory Category, const UE::AssetRegistry::FDependencyQuery& Flags) const
|
|
{
|
|
TArray<FAssetIdentifier> TempDependencies;
|
|
if (!GetDependencies(FAssetIdentifier(PackageName), TempDependencies, Category, Flags))
|
|
{
|
|
return false;
|
|
}
|
|
ConvertAssetIdentifiersToPackageNames(TempDependencies, OutDependencies);
|
|
return true;
|
|
}
|
|
|
|
bool UAssetRegistryImpl::ContainsDependency(FName PackageName, FName QueryDependencyName,
|
|
UE::AssetRegistry::EDependencyCategory Category, const UE::AssetRegistry::FDependencyQuery& Flags) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
return GuardedData.GetState().ContainsDependency(FAssetIdentifier(PackageName),
|
|
FAssetIdentifier(QueryDependencyName), Category, Flags);
|
|
}
|
|
|
|
bool IAssetRegistry::K2_GetDependencies(FName PackageName, const FAssetRegistryDependencyOptions& DependencyOptions, TArray<FName>& OutDependencies) const
|
|
{
|
|
UE::AssetRegistry::FDependencyQuery Flags;
|
|
bool bResult = false;
|
|
if (DependencyOptions.GetPackageQuery(Flags))
|
|
{
|
|
bResult = GetDependencies(PackageName, OutDependencies, UE::AssetRegistry::EDependencyCategory::Package, Flags) || bResult;
|
|
}
|
|
if (DependencyOptions.GetSearchableNameQuery(Flags))
|
|
{
|
|
bResult = GetDependencies(PackageName, OutDependencies, UE::AssetRegistry::EDependencyCategory::SearchableName, Flags) || bResult;
|
|
}
|
|
if (DependencyOptions.GetManageQuery(Flags))
|
|
{
|
|
bResult = GetDependencies(PackageName, OutDependencies, UE::AssetRegistry::EDependencyCategory::Manage, Flags) || bResult;
|
|
}
|
|
return bResult;
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetReferencers(const FAssetIdentifier& AssetIdentifier, TArray<FAssetIdentifier>& OutReferencers, UE::AssetRegistry::EDependencyCategory Category, const UE::AssetRegistry::FDependencyQuery& Flags) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
return GuardedData.GetState().GetReferencers(AssetIdentifier, OutReferencers, Category, Flags);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetReferencers(const FAssetIdentifier& AssetIdentifier, TArray<FAssetDependency>& OutReferencers, UE::AssetRegistry::EDependencyCategory Category, const UE::AssetRegistry::FDependencyQuery& Flags) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
return GuardedData.GetState().GetReferencers(AssetIdentifier, OutReferencers, Category, Flags);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetReferencers(FName PackageName, TArray<FName>& OutReferencers, UE::AssetRegistry::EDependencyCategory Category, const UE::AssetRegistry::FDependencyQuery& Flags) const
|
|
{
|
|
TArray<FAssetIdentifier> TempReferencers;
|
|
|
|
if (!GetReferencers(FAssetIdentifier(PackageName), TempReferencers, Category, Flags))
|
|
{
|
|
return false;
|
|
}
|
|
ConvertAssetIdentifiersToPackageNames(TempReferencers, OutReferencers);
|
|
return true;
|
|
}
|
|
|
|
bool IAssetRegistry::K2_GetReferencers(FName PackageName, const FAssetRegistryDependencyOptions& ReferenceOptions, TArray<FName>& OutReferencers) const
|
|
{
|
|
UE::AssetRegistry::FDependencyQuery Flags;
|
|
bool bResult = false;
|
|
if (ReferenceOptions.GetPackageQuery(Flags))
|
|
{
|
|
bResult = GetReferencers(PackageName, OutReferencers, UE::AssetRegistry::EDependencyCategory::Package, Flags) || bResult;
|
|
}
|
|
if (ReferenceOptions.GetSearchableNameQuery(Flags))
|
|
{
|
|
bResult = GetReferencers(PackageName, OutReferencers, UE::AssetRegistry::EDependencyCategory::SearchableName, Flags) || bResult;
|
|
}
|
|
if (ReferenceOptions.GetManageQuery(Flags))
|
|
{
|
|
bResult = GetReferencers(PackageName, OutReferencers, UE::AssetRegistry::EDependencyCategory::Manage, Flags) || bResult;
|
|
}
|
|
|
|
return bResult;
|
|
}
|
|
|
|
TOptional<FAssetPackageData> UAssetRegistryImpl::GetAssetPackageDataCopy(FName PackageName) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
const FAssetPackageData* AssetPackageData = GuardedData.GetState().GetAssetPackageData(PackageName);
|
|
return AssetPackageData ? *AssetPackageData : TOptional<FAssetPackageData>();
|
|
}
|
|
|
|
TArray<TOptional<FAssetPackageData>> UAssetRegistryImpl::GetAssetPackageDatasCopy(TArrayView<FName> PackageNames) const
|
|
{
|
|
TArray<TOptional<FAssetPackageData>> OutAssetPackagesData;
|
|
OutAssetPackagesData.Reserve(PackageNames.Num());
|
|
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
for (FName PackageName : PackageNames)
|
|
{
|
|
if (const FAssetPackageData* AssetPackageData = GuardedData.GetState().GetAssetPackageData(PackageName))
|
|
{
|
|
OutAssetPackagesData.Emplace(*AssetPackageData);
|
|
}
|
|
else
|
|
{
|
|
OutAssetPackagesData.Add(TOptional<FAssetPackageData>());
|
|
}
|
|
}
|
|
|
|
return OutAssetPackagesData;
|
|
}
|
|
|
|
void UAssetRegistryImpl::EnumerateAllPackages(TFunctionRef<void(FName PackageName, const FAssetPackageData& PackageData)> Callback, UE::AssetRegistry::EEnumeratePackagesFlags InEnumerateFlags) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
if(EnumHasAnyFlags(InEnumerateFlags, UE::AssetRegistry::EEnumeratePackagesFlags::Parallel))
|
|
{
|
|
const TMap<FName, const FAssetPackageData*>& DataMap = GuardedData.GetState().GetAssetPackageDataMap();
|
|
int32 MaxId = DataMap.GetMaxIndex();
|
|
ParallelFor(MaxId, [&DataMap, &Callback](int32 Index)
|
|
{
|
|
FSetElementId Id = FSetElementId::FromInteger(Index);
|
|
if (DataMap.IsValidId(Id))
|
|
{
|
|
const TPair<FName, const FAssetPackageData*>& Pair = DataMap.Get(Id);
|
|
Callback(Pair.Key, *Pair.Value);
|
|
}
|
|
});
|
|
}
|
|
else
|
|
{
|
|
for (const TPair<FName, const FAssetPackageData*>& Pair : GuardedData.GetState().GetAssetPackageDataMap())
|
|
{
|
|
Callback(Pair.Key, *Pair.Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool UAssetRegistryImpl::DoesPackageExistOnDisk(FName PackageName, FString* OutCorrectCasePackageName, FString* OutExtension) const
|
|
{
|
|
auto CalculateExtension = [](const FString& PackageNameStr, TConstArrayView<FAssetData> Assets) -> FString
|
|
{
|
|
FTopLevelAssetPath ClassRedirector = UE::AssetRegistry::GetClassPathObjectRedirector();
|
|
bool bContainsMap = false;
|
|
bool bContainsRedirector = false;
|
|
for (const FAssetData& Asset : Assets)
|
|
{
|
|
bContainsMap |= ((Asset.PackageFlags & PKG_ContainsMap) != 0);
|
|
bContainsRedirector |= (Asset.AssetClassPath == ClassRedirector);
|
|
}
|
|
if (!bContainsMap && bContainsRedirector)
|
|
{
|
|
// presence of map -> .umap
|
|
// But we can only assume lack of map -> .uasset if we know the type of every object in the package.
|
|
// If we don't, because there was a redirector, we have to check the package on disk
|
|
|
|
// Note, the 'internal' version of DoesPackageExist must be used to avoid re-entering the AssetRegistry's lock resulting in deadlock
|
|
FPackagePath PackagePath;
|
|
if (FPackageName::InternalDoesPackageExistEx(PackageNameStr, FPackageName::EPackageLocationFilter::Any,
|
|
false /*bMatchCaseOnDisk*/, &PackagePath) != FPackageName::EPackageLocationFilter::None)
|
|
{
|
|
return FString(PackagePath.GetExtensionString(EPackageSegment::Header));
|
|
}
|
|
}
|
|
return bContainsMap ? FPackageName::GetMapPackageExtension() : FPackageName::GetAssetPackageExtension();
|
|
};
|
|
|
|
#if WITH_EDITOR
|
|
if (GIsEditor)
|
|
{
|
|
// The editor always gathers PackageAssetDatas and uses those because they exactly match files on disk, whereas AssetsByPackageName
|
|
// includes memory-only assets that have added themselves to the AssetRegistry's State.
|
|
FString PackageNameStr = PackageName.ToString();
|
|
if (FPackageName::IsScriptPackage(PackageNameStr))
|
|
{
|
|
// Script packages are an exception; the AssetRegistry creates AssetPackageData for them but they exist only in memory
|
|
return false;
|
|
}
|
|
|
|
FName CorrectCasePackageName;
|
|
const FAssetPackageData* AssetPackageData;
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
AssetPackageData = GuardedData.GetState().GetAssetPackageData(PackageName, CorrectCasePackageName);
|
|
}
|
|
const static bool bVerifyNegativeResults = FParse::Param(FCommandLine::Get(), TEXT("AssetRegistryValidatePackageExists"));
|
|
if (bVerifyNegativeResults && !AssetPackageData)
|
|
{
|
|
// Note, the 'internal' version of DoesPackageExist must be used to avoid re-entering the AssetRegistry's lock resulting in deadlock
|
|
FPackagePath PackagePath;
|
|
if (FPackageName::InternalDoesPackageExistEx(PackageNameStr, FPackageName::EPackageLocationFilter::Any,
|
|
false /*bMatchCaseOnDisk*/, &PackagePath) != FPackageName::EPackageLocationFilter::None)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Warning, TEXT("Package %s exists on disk but does not exist in the AssetRegistry"), *PackageNameStr);
|
|
if (OutCorrectCasePackageName)
|
|
{
|
|
*OutCorrectCasePackageName = PackagePath.GetLocalFullPath();
|
|
}
|
|
if (OutExtension)
|
|
{
|
|
*OutExtension = PackagePath.GetExtensionString(EPackageSegment::Header);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (!AssetPackageData)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (OutCorrectCasePackageName)
|
|
{
|
|
*OutCorrectCasePackageName = CorrectCasePackageName.ToString();
|
|
}
|
|
if (OutExtension)
|
|
{
|
|
if (AssetPackageData->Extension == EPackageExtension::Unspecified || AssetPackageData->Extension == EPackageExtension::Custom)
|
|
{
|
|
// Note, the 'internal' version of DoesPackageExist must be used to avoid re-entering the AssetRegistry's lock resulting in deadlock
|
|
FPackagePath PackagePath;
|
|
if(FPackageName::InternalDoesPackageExistEx(PackageNameStr, FPackageName::EPackageLocationFilter::Any,
|
|
false /* bMatchCaseOnDisk*/, &PackagePath) != FPackageName::EPackageLocationFilter::None)
|
|
{
|
|
*OutExtension = PackagePath.GetExtensionString(EPackageSegment::Header);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogAssetRegistry, Error,
|
|
TEXT("UAssetRegistryImpl::DoesPackageExistOnDisk failed to find the extension for %s. The package exists in the AssetRegistry but does not exist on disk."),
|
|
*PackageNameStr);
|
|
TArray<FAssetData> Assets;
|
|
GetAssetsByPackageName(PackageName, Assets, /*bIncludeOnlyDiskAssets*/ true);
|
|
*OutExtension = CalculateExtension(PackageNameStr, Assets);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
*OutExtension = LexToString(AssetPackageData->Extension);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
// Runtime Game and Programs use GetAssetsByPackageName, which will match the files on disk since these configurations do not
|
|
// add loaded assets to the AssetRegistryState
|
|
TArray<FAssetData> Assets;
|
|
GetAssetsByPackageName(PackageName, Assets, /*bIncludeOnlyDiskAssets*/ true);
|
|
if (Assets.Num() == 0)
|
|
{
|
|
return false;
|
|
}
|
|
FString PackageNameStr = PackageName.ToString();
|
|
if (OutCorrectCasePackageName)
|
|
{
|
|
// In Game does not handle matching case, but it still needs to return a value for the CorrectCase field if asked
|
|
*OutCorrectCasePackageName = PackageNameStr;
|
|
}
|
|
if (OutExtension)
|
|
{
|
|
*OutExtension = CalculateExtension(PackageNameStr, Assets);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
FSoftObjectPath UAssetRegistryImpl::GetRedirectedObjectPath(const FSoftObjectPath& ObjectPath)
|
|
{
|
|
// Fast path, if a full registry scan was triggered & has completed
|
|
// In that case, we can skip further scanning while looking for a redirected path
|
|
if (!GuardedData.IsGathering())
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock ReadScopeLock(InterfaceLock);
|
|
if (GuardedData.IsSearchAllAssets())
|
|
{
|
|
return GuardedData.GetRedirectedObjectPath(ReadScopeLock, ObjectPath);
|
|
}
|
|
}
|
|
|
|
FSoftObjectPath RedirectedObjectPath;
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
UE::AssetRegistry::Impl::FClassInheritanceContext InheritanceContext;
|
|
UE::AssetRegistry::Impl::FClassInheritanceBuffer InheritanceBuffer;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock WriteScopeLock(InterfaceLock);
|
|
GetInheritanceContextWithRequiredLock(WriteScopeLock, InheritanceContext, InheritanceBuffer);
|
|
RedirectedObjectPath = GuardedData.GetAndScanRedirectedObjectPath(WriteScopeLock, ObjectPath, &EventContext, &InheritanceContext);
|
|
}
|
|
Broadcast(EventContext);
|
|
|
|
return RedirectedObjectPath;
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
FSoftObjectPath FAssetRegistryImpl::GetRedirectedObjectPath(FInterfaceReadScopeLock& ScopeLock, const FSoftObjectPath& ObjectPath)
|
|
{
|
|
return GetRedirectedObjectPath(ScopeLock, ObjectPath, nullptr, nullptr);
|
|
}
|
|
|
|
FSoftObjectPath FAssetRegistryImpl::GetAndScanRedirectedObjectPath(FInterfaceWriteScopeLock& ScopeLock, const FSoftObjectPath& ObjectPath,
|
|
UE::AssetRegistry::Impl::FEventContext* EventContext, UE::AssetRegistry::Impl::FClassInheritanceContext* InheritanceContext)
|
|
{
|
|
return GetRedirectedObjectPath(ScopeLock, ObjectPath, EventContext, InheritanceContext);
|
|
}
|
|
|
|
template <typename TScopeLock>
|
|
FSoftObjectPath FAssetRegistryImpl::GetRedirectedObjectPath(TScopeLock& ScopeLock, const FSoftObjectPath& ObjectPath,
|
|
UE::AssetRegistry::Impl::FEventContext* EventContext, UE::AssetRegistry::Impl::FClassInheritanceContext* InheritanceContext)
|
|
{
|
|
static_assert(std::is_same_v<TScopeLock, FInterfaceWriteScopeLock> || std::is_same_v<TScopeLock, FInterfaceReadScopeLock>, "Only expect scope lock types of either FInterfaceWriteScopeLock or FInterfaceReadScopeLock");
|
|
constexpr bool bNeedsScanning = std::is_same_v<TScopeLock, FInterfaceWriteScopeLock>;
|
|
check(!bNeedsScanning || (EventContext && InheritanceContext));
|
|
|
|
FSoftObjectPath RedirectedPath = ObjectPath;
|
|
|
|
// For legacy behavior, for the first object pointed to, we look up the object in memory
|
|
// before checking the on-disk assets
|
|
UObject* Asset = ObjectPath.ResolveObject();
|
|
if (Asset)
|
|
{
|
|
RedirectedPath = FSoftObjectPath::ConstructFromObject(Asset);
|
|
UObjectRedirector* Redirector = Cast<UObjectRedirector>(Asset);
|
|
if (!Redirector || !Redirector->DestinationObject)
|
|
{
|
|
return RedirectedPath;
|
|
}
|
|
// For legacy behavior, for all redirects after the initial request, we only check on-disk assets
|
|
RedirectedPath = FSoftObjectPath(Redirector->DestinationObject);
|
|
}
|
|
|
|
FString SubPathString;
|
|
|
|
auto RetrieveAssetData = [&]()
|
|
{
|
|
const FAssetData* AssetData = State.GetAssetByObjectPath(RedirectedPath);
|
|
if (!AssetData && RedirectedPath.IsSubobject())
|
|
{
|
|
// If we found no Asset because it is a subobject, then look for its toplevelobject's Asset
|
|
SubPathString = RedirectedPath.GetSubPathString();
|
|
RedirectedPath = FSoftObjectPath::ConstructFromAssetPath(RedirectedPath.GetAssetPath());
|
|
AssetData = State.GetAssetByObjectPath(RedirectedPath);
|
|
}
|
|
return AssetData;
|
|
};
|
|
|
|
const FAssetData* AssetData = RetrieveAssetData();
|
|
|
|
if constexpr (bNeedsScanning)
|
|
{
|
|
if (!AssetData)
|
|
{
|
|
UE::AssetRegistry::Impl::FScanPathContext Context(*EventContext, *InheritanceContext, {}, { RedirectedPath.ToString() },
|
|
UE::AssetRegistry::EScanFlags::IgnoreInvalidPathWarning);
|
|
ScanPathsSynchronous(&ScopeLock, Context);
|
|
|
|
AssetData = RetrieveAssetData();
|
|
}
|
|
}
|
|
|
|
// Most of the time this will either not be a redirector or only have one redirect, so optimize for that case
|
|
TArray<FSoftObjectPath, TInlineAllocator<2>> SeenPaths = { RedirectedPath };
|
|
|
|
// Need to follow chain of redirectors
|
|
while (AssetData && AssetData->IsRedirector())
|
|
{
|
|
FString Dest;
|
|
|
|
if (!AssetData->GetTagValue(UE::AssetRegistry::Impl::DestinationObjectFName, Dest))
|
|
{
|
|
break;
|
|
}
|
|
|
|
// The FSoftObjectPath functions handle stripping class name if necessary
|
|
RedirectedPath = Dest;
|
|
|
|
if (SeenPaths.Contains(RedirectedPath))
|
|
{
|
|
// Recursive, bail
|
|
break;
|
|
}
|
|
|
|
AssetData = State.GetAssetByObjectPath(RedirectedPath);
|
|
if constexpr (bNeedsScanning)
|
|
{
|
|
if (!AssetData)
|
|
{
|
|
UE::AssetRegistry::Impl::FScanPathContext Context(*EventContext, *InheritanceContext, {}, { RedirectedPath.ToString() });
|
|
ScanPathsSynchronous(&ScopeLock, Context);
|
|
|
|
AssetData = State.GetAssetByObjectPath(RedirectedPath);
|
|
}
|
|
}
|
|
|
|
SeenPaths.Add(RedirectedPath);
|
|
}
|
|
|
|
if (!SubPathString.IsEmpty())
|
|
{
|
|
if (!RedirectedPath.IsSubobject())
|
|
{
|
|
RedirectedPath.SetSubPathString(SubPathString);
|
|
}
|
|
else
|
|
{
|
|
// A complicated case; the redirector pointed to a subobject. Append old subobject path onto the new one
|
|
// Appending old to new will always use '.' because only the first subobject uses ':'
|
|
RedirectedPath.SetSubPathString(RedirectedPath.GetSubPathString() + TEXT(".") + SubPathString);
|
|
}
|
|
}
|
|
return RedirectedPath;
|
|
}
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetAncestorClassNames(FTopLevelAssetPath ClassName, TArray<FTopLevelAssetPath>& OutAncestorClassNames) const
|
|
{
|
|
UE::AssetRegistry::Impl::FClassInheritanceContext InheritanceContext;
|
|
UE::AssetRegistry::Impl::FClassInheritanceBuffer InheritanceBuffer;
|
|
UE::AssetRegistry::FInterfaceRWScopeLock InterfaceScopeLock(InterfaceLock, SLT_ReadOnly);
|
|
const_cast<UAssetRegistryImpl*>(this)->GetInheritanceContextWithRequiredLock(
|
|
InterfaceScopeLock, InheritanceContext, InheritanceBuffer);
|
|
return GuardedData.GetAncestorClassNames(InheritanceContext, ClassName, OutAncestorClassNames);
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
bool FAssetRegistryImpl::GetAncestorClassNames(Impl::FClassInheritanceContext& InheritanceContext, FTopLevelAssetPath ClassName,
|
|
TArray<FTopLevelAssetPath>& OutAncestorClassNames) const
|
|
{
|
|
// Assume we found the class unless there is an error
|
|
bool bFoundClass = true;
|
|
|
|
InheritanceContext.ConditionalUpdate();
|
|
const TMap<FTopLevelAssetPath, FTopLevelAssetPath>& InheritanceMap = InheritanceContext.Buffer->InheritanceMap;
|
|
|
|
// Make sure the requested class is in the inheritance map
|
|
if (!InheritanceMap.Contains(ClassName))
|
|
{
|
|
bFoundClass = false;
|
|
}
|
|
else
|
|
{
|
|
// Now follow the map pairs until we cant find any more parents
|
|
const FTopLevelAssetPath* CurrentClassName = &ClassName;
|
|
const uint32 MaxInheritanceDepth = 65536;
|
|
uint32 CurrentInheritanceDepth = 0;
|
|
while (CurrentInheritanceDepth < MaxInheritanceDepth && CurrentClassName != nullptr)
|
|
{
|
|
CurrentClassName = InheritanceMap.Find(*CurrentClassName);
|
|
|
|
if (CurrentClassName)
|
|
{
|
|
if (CurrentClassName->IsNull())
|
|
{
|
|
// No parent, we are at the root
|
|
CurrentClassName = nullptr;
|
|
}
|
|
else
|
|
{
|
|
OutAncestorClassNames.Add(*CurrentClassName);
|
|
}
|
|
}
|
|
CurrentInheritanceDepth++;
|
|
}
|
|
|
|
if (CurrentInheritanceDepth == MaxInheritanceDepth)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Error, TEXT("IsChildClass exceeded max inheritance depth. There is probably an infinite loop of parent classes."));
|
|
bFoundClass = false;
|
|
}
|
|
}
|
|
|
|
return bFoundClass;
|
|
}
|
|
}
|
|
|
|
void UAssetRegistryImpl::GetDerivedClassNames(const TArray<FTopLevelAssetPath>& ClassNames, const TSet<FTopLevelAssetPath>& ExcludedClassNames,
|
|
TSet<FTopLevelAssetPath>& OutDerivedClassNames) const
|
|
{
|
|
UE::AssetRegistry::Impl::FClassInheritanceContext InheritanceContext;
|
|
UE::AssetRegistry::Impl::FClassInheritanceBuffer InheritanceBuffer;
|
|
UE::AssetRegistry::FInterfaceRWScopeLock InterfaceScopeLock(InterfaceLock, SLT_ReadOnly);
|
|
const_cast<UAssetRegistryImpl*>(this)->GetInheritanceContextWithRequiredLock(
|
|
InterfaceScopeLock, InheritanceContext, InheritanceBuffer);
|
|
GuardedData.GetSubClasses(InheritanceContext, ClassNames, ExcludedClassNames, OutDerivedClassNames);
|
|
}
|
|
|
|
void UAssetRegistryImpl::GetAllCachedPaths(TArray<FString>& OutPathList) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
const FPathTree& CachedPathTree = GuardedData.GetCachedPathTree();
|
|
OutPathList.Reserve(OutPathList.Num() + CachedPathTree.NumPaths());
|
|
CachedPathTree.EnumerateAllPaths([&OutPathList](FName Path)
|
|
{
|
|
OutPathList.Emplace(Path.ToString());
|
|
return true;
|
|
});
|
|
}
|
|
|
|
void UAssetRegistryImpl::EnumerateAllCachedPaths(TFunctionRef<bool(FString)> Callback) const
|
|
{
|
|
EnumerateAllCachedPaths([&Callback](FName Path)
|
|
{
|
|
return Callback(Path.ToString());
|
|
});
|
|
}
|
|
|
|
void UAssetRegistryImpl::EnumerateAllCachedPaths(TFunctionRef<bool(FName)> Callback) const
|
|
{
|
|
TArray<FName> FoundPaths;
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
const FPathTree& CachedPathTree = GuardedData.GetCachedPathTree();
|
|
FoundPaths.Reserve(CachedPathTree.NumPaths());
|
|
CachedPathTree.EnumerateAllPaths([&FoundPaths](FName Path)
|
|
{
|
|
FoundPaths.Add(Path);
|
|
return true;
|
|
});
|
|
}
|
|
for (FName Path : FoundPaths)
|
|
{
|
|
if (!Callback(Path))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void UAssetRegistryImpl::GetSubPaths(const FString& InBasePath, TArray<FString>& OutPathList, bool bInRecurse) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
const FPathTree& CachedPathTree = GuardedData.GetCachedPathTree();
|
|
CachedPathTree.EnumerateSubPaths(*InBasePath, [&OutPathList](FName Path)
|
|
{
|
|
OutPathList.Emplace(Path.ToString());
|
|
return true;
|
|
}, bInRecurse);
|
|
}
|
|
|
|
void UAssetRegistryImpl::GetSubPaths(const FName& InBasePath, TArray<FName>& OutPathList, bool bInRecurse) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
const FPathTree& CachedPathTree = GuardedData.GetCachedPathTree();
|
|
CachedPathTree.EnumerateSubPaths(InBasePath, [&OutPathList](FName Path)
|
|
{
|
|
OutPathList.Emplace(Path);
|
|
return true;
|
|
}, bInRecurse);
|
|
}
|
|
|
|
void UAssetRegistryImpl::EnumerateSubPaths(const FString& InBasePath, TFunctionRef<bool(FString)> Callback, bool bInRecurse) const
|
|
{
|
|
TArray<FName, TInlineAllocator<64>> SubPaths;
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
const FPathTree& CachedPathTree = GuardedData.GetCachedPathTree();
|
|
CachedPathTree.EnumerateSubPaths(FName(*InBasePath), [&SubPaths](FName PathName)
|
|
{
|
|
SubPaths.Add(PathName);
|
|
return true;
|
|
}, bInRecurse);
|
|
}
|
|
for (FName PathName : SubPaths)
|
|
{
|
|
if (!Callback(PathName.ToString()))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void UAssetRegistryImpl::EnumerateSubPaths(const FName InBasePath, TFunctionRef<bool(FName)> Callback, bool bInRecurse) const
|
|
{
|
|
TArray<FName, TInlineAllocator<64>> SubPaths;
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
const FPathTree& CachedPathTree = GuardedData.GetCachedPathTree();
|
|
CachedPathTree.EnumerateSubPaths(InBasePath, [&SubPaths](FName PathName)
|
|
{
|
|
SubPaths.Add(PathName);
|
|
return true;
|
|
}, bInRecurse);
|
|
}
|
|
for (FName PathName : SubPaths)
|
|
{
|
|
if (!Callback(PathName))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void UAssetRegistryImpl::RunAssetsThroughFilter(TArray<FAssetData>& AssetDataList, const FARFilter& Filter) const
|
|
{
|
|
if (Filter.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
FARCompiledFilter CompiledFilter;
|
|
CompileFilter(Filter, CompiledFilter);
|
|
UE::AssetRegistry::Utils::RunAssetsThroughFilter(AssetDataList, CompiledFilter, UE::AssetRegistry::Utils::EFilterMode::Inclusive);
|
|
}
|
|
|
|
void UAssetRegistryImpl::UseFilterToExcludeAssets(TArray<FAssetData>& AssetDataList, const FARFilter& Filter) const
|
|
{
|
|
if (Filter.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
FARCompiledFilter CompiledFilter;
|
|
CompileFilter(Filter, CompiledFilter);
|
|
UseFilterToExcludeAssets(AssetDataList, CompiledFilter);
|
|
}
|
|
|
|
void UAssetRegistryImpl::UseFilterToExcludeAssets(TArray<FAssetData>& AssetDataList, const FARCompiledFilter& CompiledFilter) const
|
|
{
|
|
UE::AssetRegistry::Utils::RunAssetsThroughFilter(AssetDataList, CompiledFilter, UE::AssetRegistry::Utils::EFilterMode::Exclusive);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::IsAssetIncludedByFilter(const FAssetData& AssetData, const FARCompiledFilter& Filter) const
|
|
{
|
|
return UE::AssetRegistry::Utils::RunAssetThroughFilter(AssetData, Filter, UE::AssetRegistry::Utils::EFilterMode::Inclusive);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::IsAssetExcludedByFilter(const FAssetData& AssetData, const FARCompiledFilter& Filter) const
|
|
{
|
|
return UE::AssetRegistry::Utils::RunAssetThroughFilter(AssetData, Filter, UE::AssetRegistry::Utils::EFilterMode::Exclusive);
|
|
}
|
|
|
|
namespace UE::AssetRegistry::Utils
|
|
{
|
|
|
|
bool RunAssetThroughFilter(const FAssetData& AssetData, const FARCompiledFilter& Filter, const EFilterMode FilterMode)
|
|
{
|
|
const bool bPassFilterValue = FilterMode == EFilterMode::Inclusive;
|
|
if (Filter.IsEmpty())
|
|
{
|
|
return bPassFilterValue;
|
|
}
|
|
|
|
const bool bFilterResult = RunAssetThroughFilter_Unchecked(AssetData, Filter, bPassFilterValue);
|
|
return bFilterResult == bPassFilterValue;
|
|
}
|
|
|
|
bool RunAssetThroughFilter_Unchecked(const FAssetData& AssetData, const FARCompiledFilter& Filter, const bool bPassFilterValue)
|
|
{
|
|
// Package Names
|
|
if (Filter.PackageNames.Num() > 0)
|
|
{
|
|
const bool bPassesPackageNames = Filter.PackageNames.Contains(AssetData.PackageName);
|
|
if (bPassesPackageNames != bPassFilterValue)
|
|
{
|
|
return !bPassFilterValue;
|
|
}
|
|
}
|
|
|
|
// Package Paths
|
|
if (Filter.PackagePaths.Num() > 0)
|
|
{
|
|
const bool bPassesPackagePaths = Filter.PackagePaths.Contains(AssetData.PackagePath);
|
|
if (bPassesPackagePaths != bPassFilterValue)
|
|
{
|
|
return !bPassFilterValue;
|
|
}
|
|
}
|
|
|
|
// ObjectPaths
|
|
if (Filter.SoftObjectPaths.Num() > 0)
|
|
{
|
|
const bool bPassesObjectPaths = Filter.SoftObjectPaths.Contains(AssetData.GetSoftObjectPath());
|
|
if (bPassesObjectPaths != bPassFilterValue)
|
|
{
|
|
return !bPassFilterValue;
|
|
}
|
|
}
|
|
|
|
// Classes
|
|
if (Filter.ClassPaths.Num() > 0)
|
|
{
|
|
const bool bPassesClasses = Filter.ClassPaths.Contains(AssetData.AssetClassPath);
|
|
if (bPassesClasses != bPassFilterValue)
|
|
{
|
|
return !bPassFilterValue;
|
|
}
|
|
}
|
|
|
|
// Tags and values
|
|
if (Filter.TagsAndValues.Num() > 0)
|
|
{
|
|
bool bPassesTags = false;
|
|
for (const auto& TagsAndValuePair : Filter.TagsAndValues)
|
|
{
|
|
bPassesTags |= TagsAndValuePair.Value.IsSet()
|
|
? AssetData.TagsAndValues.ContainsKeyValue(TagsAndValuePair.Key, TagsAndValuePair.Value.GetValue())
|
|
: AssetData.TagsAndValues.Contains(TagsAndValuePair.Key);
|
|
if (bPassesTags)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
if (bPassesTags != bPassFilterValue)
|
|
{
|
|
return !bPassFilterValue;
|
|
}
|
|
}
|
|
|
|
return bPassFilterValue;
|
|
}
|
|
|
|
void RunAssetsThroughFilter(TArray<FAssetData>& AssetDataList, const FARCompiledFilter& CompiledFilter, const EFilterMode FilterMode)
|
|
{
|
|
if (!IsFilterValid(CompiledFilter))
|
|
{
|
|
return;
|
|
}
|
|
|
|
const int32 OriginalArrayCount = AssetDataList.Num();
|
|
const bool bPassFilterValue = FilterMode == EFilterMode::Inclusive;
|
|
|
|
AssetDataList.RemoveAll([&CompiledFilter, bPassFilterValue](const FAssetData& AssetData)
|
|
{
|
|
const bool bFilterResult = RunAssetThroughFilter_Unchecked(AssetData, CompiledFilter, bPassFilterValue);
|
|
return bFilterResult != bPassFilterValue;
|
|
});
|
|
if (OriginalArrayCount > AssetDataList.Num())
|
|
{
|
|
AssetDataList.Shrink();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::CompileFilter(const FARFilter& InFilter, FARCompiledFilter& OutCompiledFilter) const
|
|
{
|
|
UE::AssetRegistry::Impl::FClassInheritanceContext InheritanceContext;
|
|
UE::AssetRegistry::Impl::FClassInheritanceBuffer InheritanceBuffer;
|
|
UE::AssetRegistry::FInterfaceRWScopeLock InterfaceScopeLock(InterfaceLock, SLT_ReadOnly);
|
|
if (InFilter.bRecursiveClasses)
|
|
{
|
|
const_cast<UAssetRegistryImpl*>(this)->GetInheritanceContextWithRequiredLock(
|
|
InterfaceScopeLock, InheritanceContext, InheritanceBuffer);
|
|
}
|
|
else
|
|
{
|
|
// CompileFilter takes an inheritance context, but only to handle filters with recursive classes
|
|
// which we are not using here, so leave the InheritanceContext empty
|
|
}
|
|
GuardedData.CompileFilter(InheritanceContext, InFilter, OutCompiledFilter);
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::CompileFilter(Impl::FClassInheritanceContext& InheritanceContext, const FARFilter& InFilter,
|
|
FARCompiledFilter& OutCompiledFilter) const
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FAssetRegistryImpl::CompileFilter);
|
|
|
|
OutCompiledFilter.Clear();
|
|
OutCompiledFilter.PackageNames.Append(InFilter.PackageNames);
|
|
OutCompiledFilter.PackagePaths.Reserve(InFilter.PackagePaths.Num());
|
|
for (FName PackagePath : InFilter.PackagePaths)
|
|
{
|
|
OutCompiledFilter.PackagePaths.Add(FPathTree::NormalizePackagePath(PackagePath));
|
|
}
|
|
OutCompiledFilter.SoftObjectPaths.Append(InFilter.SoftObjectPaths);
|
|
|
|
#if WITH_EDITORONLY_DATA
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
OutCompiledFilter.SoftObjectPaths.Append(UE::SoftObjectPath::Private::ConvertObjectPathNames(InFilter.ObjectPaths));
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
#endif
|
|
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
if (!ensureAlwaysMsgf(InFilter.ClassNames.Num() == 0, TEXT("Asset Registry Filter using ClassNames instead of ClassPaths. First class name: \"%s\""), *InFilter.ClassNames[0].ToString()))
|
|
{
|
|
OutCompiledFilter.ClassPaths.Reserve(InFilter.ClassNames.Num());
|
|
for (FName ClassName : InFilter.ClassNames)
|
|
{
|
|
if (!ClassName.IsNone())
|
|
{
|
|
FTopLevelAssetPath ClassPathName = UClass::TryConvertShortTypeNameToPathName<UStruct>(ClassName.ToString(), ELogVerbosity::Warning, TEXT("Compiling Asset Registry Filter"));
|
|
if (!ClassPathName.IsNull())
|
|
{
|
|
OutCompiledFilter.ClassPaths.Add(ClassPathName);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogAssetRegistry, Error, TEXT("Failed to resolve class path for short class name \"%s\" when compiling asset registry filter"), *ClassName.ToString());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
OutCompiledFilter.ClassPaths.Append(InFilter.ClassPaths);
|
|
OutCompiledFilter.TagsAndValues = InFilter.TagsAndValues;
|
|
OutCompiledFilter.bIncludeOnlyOnDiskAssets = InFilter.bIncludeOnlyOnDiskAssets;
|
|
OutCompiledFilter.WithoutPackageFlags = InFilter.WithoutPackageFlags;
|
|
OutCompiledFilter.WithPackageFlags = InFilter.WithPackageFlags;
|
|
|
|
if (InFilter.bRecursivePaths)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FAssetRegistryImpl::CompileFilter::AddPaths);
|
|
|
|
// Add the sub-paths of all the input paths to the expanded list
|
|
for (const FName& PackagePath : InFilter.PackagePaths)
|
|
{
|
|
CachedPathTree.GetSubPaths(PackagePath, OutCompiledFilter.PackagePaths);
|
|
}
|
|
}
|
|
|
|
if (InFilter.bRecursiveClasses)
|
|
{
|
|
// Add the sub-classes of all the input classes to the expanded list, excluding any that were requested
|
|
if (InFilter.RecursiveClassPathsExclusionSet.Num() > 0 && InFilter.ClassPaths.Num() == 0)
|
|
{
|
|
TArray<FTopLevelAssetPath> ClassNamesObject;
|
|
ClassNamesObject.Add(UE::AssetRegistry::GetClassPathObject());
|
|
|
|
GetSubClasses(InheritanceContext, ClassNamesObject, InFilter.RecursiveClassPathsExclusionSet, OutCompiledFilter.ClassPaths);
|
|
}
|
|
else
|
|
{
|
|
GetSubClasses(InheritanceContext, InFilter.ClassPaths, InFilter.RecursiveClassPathsExclusionSet, OutCompiledFilter.ClassPaths);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
EAssetAvailability::Type UAssetRegistryImpl::GetAssetAvailability(const FAssetData& AssetData) const
|
|
{
|
|
return UE::AssetRegistry::Utils::GetAssetAvailability(AssetData);
|
|
}
|
|
|
|
namespace UE::AssetRegistry::Utils
|
|
{
|
|
|
|
EAssetAvailability::Type GetAssetAvailability(const FAssetData& AssetData)
|
|
{
|
|
#if ENABLE_PLATFORM_CHUNK_INSTALL
|
|
IPlatformChunkInstall* ChunkInstall = FPlatformMisc::GetPlatformChunkInstall();
|
|
|
|
EChunkLocation::Type BestLocation = EChunkLocation::DoesNotExist;
|
|
|
|
// check all chunks to see which has the best locality
|
|
for (int32 PakchunkId : AssetData.GetChunkIDs())
|
|
{
|
|
EChunkLocation::Type ChunkLocation = ChunkInstall->GetPakchunkLocation(PakchunkId);
|
|
|
|
// if we find one in the best location, early out
|
|
if (ChunkLocation == EChunkLocation::BestLocation)
|
|
{
|
|
BestLocation = ChunkLocation;
|
|
break;
|
|
}
|
|
|
|
if (ChunkLocation > BestLocation)
|
|
{
|
|
BestLocation = ChunkLocation;
|
|
}
|
|
}
|
|
|
|
switch (BestLocation)
|
|
{
|
|
case EChunkLocation::LocalFast:
|
|
return EAssetAvailability::LocalFast;
|
|
case EChunkLocation::LocalSlow:
|
|
return EAssetAvailability::LocalSlow;
|
|
case EChunkLocation::NotAvailable:
|
|
return EAssetAvailability::NotAvailable;
|
|
case EChunkLocation::DoesNotExist:
|
|
return EAssetAvailability::DoesNotExist;
|
|
default:
|
|
check(0);
|
|
return EAssetAvailability::LocalFast;
|
|
}
|
|
#else
|
|
return EAssetAvailability::LocalFast;
|
|
#endif
|
|
}
|
|
|
|
}
|
|
|
|
float UAssetRegistryImpl::GetAssetAvailabilityProgress(const FAssetData& AssetData, EAssetAvailabilityProgressReportingType::Type ReportType) const
|
|
{
|
|
return UE::AssetRegistry::Utils::GetAssetAvailabilityProgress(AssetData, ReportType);
|
|
}
|
|
|
|
namespace UE::AssetRegistry::Utils
|
|
{
|
|
|
|
float GetAssetAvailabilityProgress(const FAssetData& AssetData, EAssetAvailabilityProgressReportingType::Type ReportType)
|
|
{
|
|
check(ReportType == EAssetAvailabilityProgressReportingType::PercentageComplete || ReportType == EAssetAvailabilityProgressReportingType::ETA);
|
|
|
|
#if ENABLE_PLATFORM_CHUNK_INSTALL
|
|
IPlatformChunkInstall* ChunkInstall = FPlatformMisc::GetPlatformChunkInstall();
|
|
EChunkProgressReportingType::Type ChunkReportType = GetChunkAvailabilityProgressType(ReportType);
|
|
|
|
bool IsPercentageComplete = (ChunkReportType == EChunkProgressReportingType::PercentageComplete) ? true : false;
|
|
|
|
float BestProgress = MAX_FLT;
|
|
|
|
// check all chunks to see which has the best time remaining
|
|
for (int32 PakchunkID : AssetData.GetChunkIDs())
|
|
{
|
|
float Progress = ChunkInstall->GetChunkProgress(PakchunkID, ChunkReportType);
|
|
|
|
// need to flip percentage completes for the comparison
|
|
if (IsPercentageComplete)
|
|
{
|
|
Progress = 100.0f - Progress;
|
|
}
|
|
|
|
if (Progress <= 0.0f)
|
|
{
|
|
BestProgress = 0.0f;
|
|
break;
|
|
}
|
|
|
|
if (Progress < BestProgress)
|
|
{
|
|
BestProgress = Progress;
|
|
}
|
|
}
|
|
|
|
// unflip percentage completes
|
|
if (IsPercentageComplete)
|
|
{
|
|
BestProgress = 100.0f - BestProgress;
|
|
}
|
|
return BestProgress;
|
|
|
|
#else
|
|
if (ReportType == EAssetAvailabilityProgressReportingType::PercentageComplete)
|
|
{
|
|
return 100.0f;
|
|
}
|
|
else
|
|
{
|
|
return 0.0f;
|
|
}
|
|
#endif
|
|
|
|
}
|
|
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetAssetAvailabilityProgressTypeSupported(EAssetAvailabilityProgressReportingType::Type ReportType) const
|
|
{
|
|
return UE::AssetRegistry::Utils::GetAssetAvailabilityProgressTypeSupported(ReportType);
|
|
}
|
|
|
|
namespace UE::AssetRegistry::Utils
|
|
{
|
|
|
|
bool GetAssetAvailabilityProgressTypeSupported(EAssetAvailabilityProgressReportingType::Type ReportType)
|
|
{
|
|
#if ENABLE_PLATFORM_CHUNK_INSTALL
|
|
IPlatformChunkInstall* ChunkInstall = FPlatformMisc::GetPlatformChunkInstall();
|
|
return ChunkInstall->GetProgressReportingTypeSupported(GetChunkAvailabilityProgressType(ReportType));
|
|
#else
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::PrioritizeAssetInstall(const FAssetData& AssetData) const
|
|
{
|
|
UE::AssetRegistry::Utils::PrioritizeAssetInstall(AssetData);
|
|
}
|
|
|
|
namespace UE::AssetRegistry::Utils
|
|
{
|
|
|
|
void PrioritizeAssetInstall(const FAssetData& AssetData)
|
|
{
|
|
#if ENABLE_PLATFORM_CHUNK_INSTALL
|
|
IPlatformChunkInstall* ChunkInstall = FPlatformMisc::GetPlatformChunkInstall();
|
|
|
|
const TConstArrayView<int32> ChunkIDs = AssetData.GetChunkIDs();
|
|
if (ChunkIDs.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ChunkInstall->PrioritizePakchunk(ChunkIDs[0], EChunkPriority::Immediate);
|
|
#endif
|
|
}
|
|
|
|
}
|
|
|
|
bool UAssetRegistryImpl::HasVerseFiles(FName PackagePath, bool bRecursive /*= false*/) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
return GuardedData.GetVerseFilesByPath(PackagePath, /*OutFilePaths=*/nullptr, bRecursive);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetVerseFilesByPath(FName PackagePath, TArray<FName>& OutFilePaths, bool bRecursive /*= false*/) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
return GuardedData.GetVerseFilesByPath(PackagePath, &OutFilePaths, bRecursive);
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
bool FAssetRegistryImpl::GetVerseFilesByPath(FName PackagePath, TArray<FName>* OutFilePaths, bool bRecursive) const
|
|
{
|
|
TSet<FName> PathList;
|
|
PathList.Reserve(32);
|
|
PathList.Add(PackagePath);
|
|
if (bRecursive)
|
|
{
|
|
CachedPathTree.GetSubPaths(PackagePath, PathList, true);
|
|
}
|
|
|
|
bool bFoundAnything = false;
|
|
for (const FName& PathName : PathList)
|
|
{
|
|
const TArray<FName>* FilePaths = CachedVerseFilesByPath.Find(PathName);
|
|
if (FilePaths)
|
|
{
|
|
bFoundAnything = true;
|
|
if (OutFilePaths)
|
|
{
|
|
OutFilePaths->Append(*FilePaths);
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return bFoundAnything;
|
|
}
|
|
|
|
}
|
|
|
|
bool UAssetRegistryImpl::AddPath(const FString& PathToAdd)
|
|
{
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
bool bResult;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
bResult = GuardedData.AddPath(EventContext, UE::String::RemoveFromEnd(FStringView(PathToAdd), TEXTVIEW("/")));
|
|
}
|
|
Broadcast(EventContext);
|
|
return bResult;
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
bool FAssetRegistryImpl::AddPath(Impl::FEventContext& EventContext, FStringView PathToAdd)
|
|
{
|
|
bool bIsDenied = false;
|
|
// If no GlobalGatherer, then we are in the game or non-cook commandlet and we do not implement deny listing
|
|
if (GlobalGatherer.IsValid())
|
|
{
|
|
FString LocalPathToAdd;
|
|
if (FPackageName::TryConvertLongPackageNameToFilename(PathToAdd, LocalPathToAdd))
|
|
{
|
|
bIsDenied = GlobalGatherer->IsOnDenyList(LocalPathToAdd);
|
|
}
|
|
}
|
|
if (bIsDenied)
|
|
{
|
|
return false;
|
|
}
|
|
return AddAssetPath(EventContext, FName(PathToAdd));
|
|
}
|
|
|
|
}
|
|
|
|
bool UAssetRegistryImpl::RemovePath(const FString& PathToRemove)
|
|
{
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
bool bResult;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
bResult = GuardedData.RemoveAssetPath(EventContext, FName(UE::String::RemoveFromEnd(FStringView(PathToRemove), TEXTVIEW("/"))));
|
|
}
|
|
Broadcast(EventContext);
|
|
return bResult;
|
|
}
|
|
|
|
bool UAssetRegistryImpl::PathExists(const FString& PathToTest) const
|
|
{
|
|
return PathExists(FName(*PathToTest));
|
|
}
|
|
|
|
bool UAssetRegistryImpl::PathExists(const FName PathToTest) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
return GuardedData.GetCachedPathTree().PathExists(PathToTest);
|
|
}
|
|
|
|
void UAssetRegistryImpl::ScanPathsSynchronous(const TArray<FString>& InPaths, bool bForceRescan,
|
|
bool bIgnoreDenyListScanFilters)
|
|
{
|
|
// The contract of this older version of ScanSynchronous always set the WaitForInMemoryObjects flag.
|
|
UE::AssetRegistry::EScanFlags ScanFlags = UE::AssetRegistry::EScanFlags::WaitForInMemoryObjects;
|
|
|
|
if (bForceRescan)
|
|
{
|
|
ScanFlags |= UE::AssetRegistry::EScanFlags::ForceRescan;
|
|
}
|
|
|
|
if (bIgnoreDenyListScanFilters)
|
|
{
|
|
ScanFlags |= UE::AssetRegistry::EScanFlags::IgnoreDenyListScanFilters;
|
|
}
|
|
|
|
ScanPathsSynchronousInternal(InPaths, TArray<FString>(), ScanFlags);
|
|
}
|
|
|
|
void UAssetRegistryImpl::ScanFilesSynchronous(const TArray<FString>& InFilePaths, bool bForceRescan)
|
|
{
|
|
// The contract of this older version of ScanSynchronous always set the WaitForInMemoryObjects flag.
|
|
UE::AssetRegistry::EScanFlags ScanFlags = UE::AssetRegistry::EScanFlags::WaitForInMemoryObjects;
|
|
|
|
if (bForceRescan)
|
|
{
|
|
ScanFlags |= UE::AssetRegistry::EScanFlags::ForceRescan;
|
|
}
|
|
|
|
ScanPathsSynchronousInternal(TArray<FString>(), InFilePaths, ScanFlags);
|
|
}
|
|
|
|
void UAssetRegistryImpl::ScanSynchronous(const TArray<FString>& InPaths, const TArray<FString>& InFilePaths, UE::AssetRegistry::EScanFlags InScanFlags)
|
|
{
|
|
ScanPathsSynchronousInternal(InPaths, InFilePaths, InScanFlags);
|
|
}
|
|
|
|
void UAssetRegistryImpl::ScanPathsSynchronousInternal(const TArray<FString>& InDirs, const TArray<FString>& InFiles,
|
|
UE::AssetRegistry::EScanFlags InScanFlags)
|
|
{
|
|
UE_SCOPED_IO_ACTIVITY(*WriteToString<256>("Scan Paths"));
|
|
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UAssetRegistryImpl::ScanPathsSynchronousInternal);
|
|
UE_TRACK_REFERENCING_OPNAME_SCOPED(PackageAccessTrackingOps::NAME_ResetContext);
|
|
const double SearchStartTime = FPlatformTime::Seconds();
|
|
|
|
const bool bWaitForInMemoryObjects = !!(InScanFlags & UE::AssetRegistry::EScanFlags::WaitForInMemoryObjects);
|
|
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
UE::AssetRegistry::Impl::FClassInheritanceContext InheritanceContext;
|
|
UE::AssetRegistry::Impl::FClassInheritanceBuffer InheritanceBuffer;
|
|
UE::AssetRegistry::Impl::FScanPathContext Context(EventContext, InheritanceContext, InDirs, InFiles,
|
|
InScanFlags, nullptr /* OutFindAssets */);
|
|
|
|
bool bInitialSearchStarted;
|
|
bool bInitialSearchCompleted;
|
|
bool bAdditionalMountSearchInProgress;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GetInheritanceContextWithRequiredLock(InterfaceScopeLock, InheritanceContext, InheritanceBuffer);
|
|
|
|
bInitialSearchStarted = GuardedData.IsInitialSearchStarted();
|
|
bInitialSearchCompleted = GuardedData.IsInitialSearchCompleted();
|
|
bAdditionalMountSearchInProgress = GuardedData.IsAdditionalMountSearchInProgress();
|
|
// make sure any outstanding async preload is complete
|
|
GuardedData.ConditionalLoadPremadeAssetRegistry(InterfaceScopeLock, EventContext);
|
|
GuardedData.ScanPathsSynchronous(&InterfaceScopeLock, Context);
|
|
}
|
|
if (Context.LocalPaths.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
if (bWaitForInMemoryObjects)
|
|
{
|
|
UE::AssetRegistry::Impl::FInterruptionContext InterruptionContext;
|
|
ProcessLoadedAssetsToUpdateCache(EventContext, Context.Status, InterruptionContext);
|
|
}
|
|
#endif
|
|
Broadcast(EventContext);
|
|
|
|
// Log stats
|
|
FString PathsString;
|
|
if (Context.LocalPaths.Num() > 1)
|
|
{
|
|
PathsString = FString::Printf(TEXT("'%s' and %d other paths"), *Context.LocalPaths[0], Context.LocalPaths.Num() - 1);
|
|
}
|
|
else
|
|
{
|
|
PathsString = FString::Printf(TEXT("'%s'"), *Context.LocalPaths[0]);
|
|
}
|
|
|
|
|
|
double Duration = FPlatformTime::Seconds() - SearchStartTime;
|
|
UE::Telemetry::AssetRegistry::FSynchronousScanTelemetry Telemetry;
|
|
Telemetry.Directories = MakeArrayView(InDirs);
|
|
Telemetry.Files = MakeArrayView(InFiles);
|
|
Telemetry.Flags = InScanFlags;
|
|
Telemetry.NumFoundAssets = Context.NumFoundAssets;
|
|
Telemetry.Duration = Duration;
|
|
Telemetry.bInitialSearchStarted = bInitialSearchStarted;
|
|
Telemetry.bInitialSearchCompleted = bInitialSearchCompleted;
|
|
Telemetry.bAdditionalMountSearchInProgress = bAdditionalMountSearchInProgress;
|
|
FTelemetryRouter::Get().ProvideTelemetry(Telemetry);
|
|
UE_LOG(LogAssetRegistry, Verbose, TEXT("ScanPathsSynchronous completed scanning %s to find %d assets in %0.4f seconds"), *PathsString,
|
|
Context.NumFoundAssets, Duration);
|
|
}
|
|
|
|
void UAssetRegistryImpl::PrioritizeSearchPath(const FString& PathToPrioritize)
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.PrioritizeSearchPath(PathToPrioritize);
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::PrioritizeSearchPath(const FString& PathToPrioritize)
|
|
{
|
|
if (!GlobalGatherer.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
GlobalGatherer->PrioritizeSearchPath(PathToPrioritize);
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::AssetCreated(UObject* NewAsset)
|
|
{
|
|
if (ensure(NewAsset) && NewAsset->IsAsset())
|
|
{
|
|
// Add the newly created object to the package file cache because its filename can already be
|
|
// determined by its long package name.
|
|
// @todo AssetRegistry We are assuming it will be saved in a single asset package.
|
|
UPackage* NewPackage = NewAsset->GetOutermost();
|
|
|
|
// Mark this package as newly created.
|
|
NewPackage->SetPackageFlags(PKG_NewlyCreated);
|
|
|
|
const FString NewPackageName = NewPackage->GetName();
|
|
|
|
bool bShouldSkipAssset;
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
// If this package was marked as an empty package before, it is no longer empty, so remove it from the list
|
|
GuardedData.RemoveEmptyPackage(NewPackage->GetFName());
|
|
|
|
// Add the path to the Path Tree, in case it wasn't already there
|
|
GuardedData.AddAssetPath(EventContext, *FPackageName::GetLongPackagePath(NewPackageName));
|
|
bShouldSkipAssset = GuardedData.ShouldSkipAsset(NewAsset);
|
|
}
|
|
|
|
Broadcast(EventContext);
|
|
if (!bShouldSkipAssset)
|
|
{
|
|
checkf(IsInGameThread(), TEXT("AssetCreated is not yet implemented as callable from other threads"));
|
|
// Let subscribers know that the new asset was added to the registry
|
|
FAssetData AssetData = FAssetData(NewAsset, FAssetData::ECreationFlags::AllowBlueprintClass,
|
|
EAssetRegistryTagsCaller::AssetRegistryQuery);
|
|
AssetAddedEvent.Broadcast(AssetData);
|
|
OnAssetsAdded().Broadcast({ AssetData });
|
|
|
|
// Notify listeners that an asset was just created
|
|
InMemoryAssetCreatedEvent.Broadcast(NewAsset);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UAssetRegistryImpl::AssetDeleted(UObject* DeletedAsset)
|
|
{
|
|
checkf(GIsEditor, TEXT("Updating the AssetRegistry is only available in editor"));
|
|
if (!ensure(DeletedAsset) || !DeletedAsset->IsAsset())
|
|
{
|
|
return;
|
|
}
|
|
|
|
UPackage* DeletedObjectPackage = DeletedAsset->GetOutermost();
|
|
bool bIsEmptyPackage = DeletedObjectPackage != nullptr && UPackage::IsEmptyPackage(DeletedObjectPackage, DeletedAsset);
|
|
|
|
bool bShouldSkipAsset;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
|
|
// Deleting the last asset in a package causes the package to be garbage collected.
|
|
// If the UPackage object is GCed, it will be considered 'Unloaded' which will cause it to
|
|
// be fully loaded from disk when save is invoked.
|
|
// We want to keep the package around so we can save it empty or delete the file.
|
|
if (bIsEmptyPackage)
|
|
{
|
|
GuardedData.AddEmptyPackage(DeletedObjectPackage->GetFName());
|
|
}
|
|
bShouldSkipAsset = GuardedData.ShouldSkipAsset(DeletedAsset);
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
// Update Redirectors
|
|
if (FAssetData::IsRedirector(DeletedAsset))
|
|
{
|
|
// Need to remove from GRedirectCollector
|
|
GRedirectCollector.RemoveAssetPathRedirection(FSoftObjectPath::ConstructFromObject(DeletedAsset));
|
|
}
|
|
#endif
|
|
|
|
if (!bShouldSkipAsset)
|
|
{
|
|
FAssetData AssetDataDeleted = FAssetData(DeletedAsset, FAssetData::ECreationFlags::AllowBlueprintClass,
|
|
EAssetRegistryTagsCaller::AssetRegistryQuery);
|
|
|
|
checkf(IsInGameThread(), TEXT("AssetDeleted is not yet implemented as callable from other threads"));
|
|
// Let subscribers know that the asset was removed from the registry
|
|
AssetRemovedEvent.Broadcast(AssetDataDeleted);
|
|
OnAssetsRemoved().Broadcast({AssetDataDeleted});
|
|
|
|
// Notify listeners that an in-memory asset was just deleted
|
|
InMemoryAssetDeletedEvent.Broadcast(DeletedAsset);
|
|
}
|
|
}
|
|
|
|
void UAssetRegistryImpl::AssetRenamed(const UObject* RenamedAsset, const FString& OldObjectPath)
|
|
{
|
|
checkf(GIsEditor, TEXT("Updating the AssetRegistry is only available in editor"));
|
|
if (ensure(RenamedAsset) && RenamedAsset->IsAsset())
|
|
{
|
|
// Add the renamed object to the package file cache because its filename can already be
|
|
// determined by its long package name.
|
|
// @todo AssetRegistry We are assuming it will be saved in a single asset package.
|
|
UPackage* NewPackage = RenamedAsset->GetOutermost();
|
|
const FString NewPackageName = NewPackage->GetName();
|
|
const FString Filename = FPackageName::LongPackageNameToFilename(NewPackageName, FPackageName::GetAssetPackageExtension());
|
|
|
|
// We want to keep track of empty packages so we can properly merge cached assets with in-memory assets
|
|
UPackage* OldPackage = nullptr;
|
|
FString OldPackageName;
|
|
FString OldAssetName;
|
|
if (OldObjectPath.Split(TEXT("."), &OldPackageName, &OldAssetName))
|
|
{
|
|
OldPackage = FindPackage(nullptr, *OldPackageName);
|
|
}
|
|
|
|
// Call IsEmptyPackage outside of the lock; it can call LoadPackage internally.
|
|
bool bOldPackageIsEmpty = OldPackage && UPackage::IsEmptyPackage(OldPackage);
|
|
|
|
bool bShouldSkipAsset;
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.RemoveEmptyPackage(NewPackage->GetFName());
|
|
|
|
if (bOldPackageIsEmpty)
|
|
{
|
|
GuardedData.AddEmptyPackage(OldPackage->GetFName());
|
|
}
|
|
|
|
// Add the path to the Path Tree, in case it wasn't already there
|
|
GuardedData.AddAssetPath(EventContext, *FPackageName::GetLongPackagePath(NewPackageName));
|
|
bShouldSkipAsset = GuardedData.ShouldSkipAsset(RenamedAsset);
|
|
}
|
|
|
|
Broadcast(EventContext);
|
|
if (!bShouldSkipAsset)
|
|
{
|
|
checkf(IsInGameThread(), TEXT("AssetRenamed is not yet implemented as callable from other threads"));
|
|
AssetRenamedEvent.Broadcast(
|
|
FAssetData(RenamedAsset, FAssetData::ECreationFlags::AllowBlueprintClass,
|
|
EAssetRegistryTagsCaller::AssetRegistryQuery),
|
|
OldObjectPath
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UAssetRegistryImpl::AssetsSaved(TArray<FAssetData>&& Assets)
|
|
{
|
|
#if WITH_EDITOR
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
{
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.AssetsSaved(EventContext, MoveTemp(Assets));
|
|
}
|
|
Broadcast(EventContext);
|
|
#endif
|
|
}
|
|
|
|
void UAssetRegistryImpl::AssetUpdateTags(UObject* Object, EAssetRegistryTagsCaller Caller)
|
|
{
|
|
#if WITH_EDITOR
|
|
FAssetData AssetData(Object, FAssetData::ECreationFlags::None, Caller);
|
|
TArray<FAssetData> Assets;
|
|
Assets.Add(MoveTemp(AssetData));
|
|
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
{
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.AssetsSaved(EventContext, MoveTemp(Assets));
|
|
}
|
|
Broadcast(EventContext);
|
|
#endif
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
#if WITH_EDITOR
|
|
void FAssetRegistryImpl::AssetsSaved(UE::AssetRegistry::Impl::FEventContext& EventContext, TArray<FAssetData>&& Assets)
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
for (FAssetData& NewAssetData : Assets)
|
|
{
|
|
FCachedAssetKey Key(NewAssetData);
|
|
FAssetData* DataFromGather = State.GetMutableAssetByObjectPath(Key);
|
|
|
|
AssetDataObjectPathsUpdatedOnLoad.Add(NewAssetData.GetSoftObjectPath());
|
|
|
|
if (!DataFromGather)
|
|
{
|
|
FAssetData* ClonedAssetData = new FAssetData(MoveTemp(NewAssetData));
|
|
AddAssetData(EventContext, ClonedAssetData);
|
|
}
|
|
else
|
|
{
|
|
UpdateAssetData(EventContext, DataFromGather, MoveTemp(NewAssetData), false /* bKeepDeletedTags */);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::AssetTagsFinalized(const UObject& FinalizedAsset)
|
|
{
|
|
#if WITH_EDITOR
|
|
if (!FinalizedAsset.IsAsset())
|
|
{
|
|
return;
|
|
}
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.AddLoadedAssetToProcess(FinalizedAsset);
|
|
#endif
|
|
}
|
|
|
|
bool UAssetRegistryImpl::VerseCreated(const FString& FilePathOnDisk)
|
|
{
|
|
checkf(GIsEditor, TEXT("Updating the AssetRegistry is only available in editor"));
|
|
if (!FAssetDataGatherer::IsVerseFile(FilePathOnDisk))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FString PackageName;
|
|
if (!FPackageName::TryConvertFilenameToLongPackageName(FilePathOnDisk, PackageName, /*OutFailureReason*/nullptr, FPackageName::EConvertFlags::AllowDots))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FNameBuilder VersePackagePathName;
|
|
VersePackagePathName.Append(PackageName);
|
|
VersePackagePathName.Append(FPathViews::GetExtension(FilePathOnDisk, /*bIncludeDot=*/true));
|
|
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.AddVerseFile(EventContext, *VersePackagePathName);
|
|
}
|
|
Broadcast(EventContext);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool UAssetRegistryImpl::VerseDeleted(const FString& FilePathOnDisk)
|
|
{
|
|
checkf(GIsEditor, TEXT("Updating the AssetRegistry is only available in editor"));
|
|
if (!FAssetDataGatherer::IsVerseFile(FilePathOnDisk))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FString PackageName;
|
|
if (!FPackageName::TryConvertFilenameToLongPackageName(FilePathOnDisk, PackageName, /*OutFailureReason*/nullptr, FPackageName::EConvertFlags::AllowDots))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FNameBuilder VersePackagePathName;
|
|
VersePackagePathName.Append(PackageName);
|
|
VersePackagePathName.Append(FPathViews::GetExtension(FilePathOnDisk, /*bIncludeDot=*/true));
|
|
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.RemoveVerseFile(EventContext, *VersePackagePathName);
|
|
}
|
|
Broadcast(EventContext);
|
|
|
|
return true;
|
|
}
|
|
|
|
void UAssetRegistryImpl::PackageDeleted(UPackage* DeletedPackage)
|
|
{
|
|
checkf(GIsEditor, TEXT("Updating the AssetRegistry is only available in editor"));
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
if (ensure(DeletedPackage))
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.RemovePackageData(EventContext, DeletedPackage->GetFName());
|
|
}
|
|
Broadcast(EventContext);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::IsLoadingAssets() const
|
|
{
|
|
return GuardedData.IsLoadingAssets();
|
|
}
|
|
|
|
bool UAssetRegistryImpl::IsGathering() const
|
|
{
|
|
return GuardedData.IsGathering();
|
|
}
|
|
|
|
bool UAssetRegistryImpl::HasSerializedDiscoveryCache() const
|
|
{
|
|
return GuardedData.HasSerializedDiscoveryCache();
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
bool FAssetRegistryImpl::IsLoadingAssets() const
|
|
{
|
|
return !IsInitialSearchCompleted();
|
|
}
|
|
|
|
bool FAssetRegistryImpl::IsGathering() const
|
|
{
|
|
return !IsInitialSearchCompleted() || IsAdditionalMountSearchInProgress();
|
|
}
|
|
|
|
bool FAssetRegistryImpl::HasSerializedDiscoveryCache() const
|
|
{
|
|
return GlobalGatherer->HasSerializedDiscoveryCache();
|
|
}
|
|
}
|
|
|
|
UE::AssetRegistry::Impl::EGatherStatus UAssetRegistryImpl::TickOnBackgroundThread()
|
|
{
|
|
UE::AssetRegistry::Impl::EGatherStatus Status = UE::AssetRegistry::Impl::EGatherStatus::TickActiveGatherActive;
|
|
|
|
do
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
if (GatheredDataProcessingLock.TryLock())
|
|
{
|
|
ON_SCOPE_EXIT { GatheredDataProcessingLock.Unlock(); };
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
UE::AssetRegistry::Impl::FClassInheritanceContext InheritanceContext;
|
|
UE::AssetRegistry::Impl::FInitializeContext InitializeContext{ *this };
|
|
UE::AssetRegistry::Impl::FClassInheritanceBuffer InheritanceBuffer;
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock, UE::AssetRegistry::Private::PriorityLow);
|
|
GetInheritanceContextWithRequiredLock(InterfaceScopeLock, InitializeContext.InheritanceContext, InitializeContext.InheritanceBuffer);
|
|
|
|
UE::AssetRegistry::Impl::FInterruptionContext::ShouldExitEarlyCallbackType EarlyExitHelper =
|
|
[this]()->bool
|
|
{
|
|
if (InterfaceLock.HasWaiters() || IsBackgroundProcessingPaused())
|
|
{
|
|
#if WITH_EDITOR
|
|
++GuardedData.GetBackgroundTickInterruptionsCount();
|
|
#endif
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
UE::AssetRegistry::Impl::FTickContext TickContext(GuardedData, EventContext, InheritanceContext);
|
|
TickContext.InterruptionContext.SetLimitedTickTime(FPlatformTime::Seconds(),
|
|
UE::AssetRegistry::Impl::MaxSecondsPerTickBackgroundThread);
|
|
TickContext.InterruptionContext.SetEarlyExitCallback(EarlyExitHelper);
|
|
TickContext.bHandleDeferred = true;
|
|
Status = GuardedData.TickGatherer(TickContext);
|
|
|
|
if (!EventContext.IsEmpty())
|
|
{
|
|
FScopeLock DeferredEventsLock(&DeferredEventsCriticalSection);
|
|
DeferredEvents.Append(MoveTemp(EventContext));
|
|
EventContext.Clear();
|
|
RequestTick();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If the game thread is holding the processing lock,
|
|
// let's just exit and let the thread run function decide what to do
|
|
return UE::AssetRegistry::Impl::EGatherStatus::UnableToProgress;
|
|
}
|
|
|
|
if (IsBackgroundProcessingPaused())
|
|
{
|
|
return UE::AssetRegistry::Impl::EGatherStatus::UnableToProgress;
|
|
}
|
|
|
|
// This ensures that if there are multiple waiters we don't get in ahead of them
|
|
while(InterfaceLock.HasWaiters())
|
|
{
|
|
if (IsBackgroundProcessingPaused())
|
|
{
|
|
return UE::AssetRegistry::Impl::EGatherStatus::UnableToProgress;
|
|
}
|
|
FPlatformProcess::Yield();
|
|
}
|
|
} while (Status == UE::AssetRegistry::Impl::EGatherStatus::TickActiveGatherIdle);
|
|
|
|
return Status;
|
|
}
|
|
|
|
UAssetRegistryImpl::FEnumerateAssetsEvent& UAssetRegistryImpl::OnEnumerateAssetsEvent()
|
|
{
|
|
return EnumerateAssetsEvent;
|
|
}
|
|
|
|
void UAssetRegistryImpl::Tick(float DeltaTime)
|
|
{
|
|
checkf(IsInGameThread(), TEXT("The tick function executes deferred loads and events and must be on the game thread to do so."));
|
|
TRACE_CPUPROFILER_EVENT_SCOPE_STR("Asset Registry Tick");
|
|
|
|
UE::AssetRegistry::Impl::EGatherStatus Status = UE::AssetRegistry::Impl::EGatherStatus::TickActiveGatherActive;
|
|
double TickStartTime = -1; // Force a full flush if DeltaTime < 0
|
|
if (DeltaTime >= 0)
|
|
{
|
|
TickStartTime = FPlatformTime::Seconds();
|
|
}
|
|
|
|
bool bInterruptedOrShouldProcessDeferredEvents = false;
|
|
float LocalMaxSecondsPerFrame = UE::AssetRegistry::Impl::MaxSecondsPerFrame;
|
|
|
|
// Ticks in !WITH_EDITOR are done on request. Mark that we have satisfied the request; any further requests that come in from
|
|
// other threads while we are ticking will cause an additional queued request. RequestTick is not expected to be called from
|
|
// this thread, because all callers of it come only when writing to DeferredEvents, which we will not do in this thread because
|
|
// we can process the events immediately on this thread.
|
|
#if !WITH_EDITOR
|
|
{
|
|
FScopeLock DeferredEventsLock(&DeferredEventsCriticalSection);
|
|
ClearRequestTick();
|
|
}
|
|
#endif
|
|
|
|
do
|
|
{
|
|
bInterruptedOrShouldProcessDeferredEvents = false;
|
|
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
|
|
bool bHasEnteredGatheredDataProcessingLock = false;
|
|
#if WITH_EDITOR
|
|
bool bTakeOverGather = GuardedData.IsGameThreadTakeOverGatherEachTick();
|
|
if (!bTakeOverGather)
|
|
#endif
|
|
{
|
|
// When we are not trying to block on the gather, we allow the background thread to keep working on
|
|
// tickgatherer, and we only enter the lock and tickgatherer here on the game thread if the background
|
|
// thread is not already in the lock
|
|
bHasEnteredGatheredDataProcessingLock = GatheredDataProcessingLock.TryLock();
|
|
}
|
|
#if WITH_EDITOR
|
|
else
|
|
{
|
|
// When we want to block on the gather results, we take over TickGatherer from the background thread.
|
|
// For the GlobalGatherer's side of this race, see TickOnBackgroundThread and the code that calls it in
|
|
// FAssetDataGatherer::Run.
|
|
{
|
|
// First we use an FInterfaceWriteScopeLock with the default High Priority to register ourselves as
|
|
// waiting on the InterfaceLock.
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
// The GlobalGatherer will see that we are waiting on entering the lock and will exit the lock as soon
|
|
// as possible to allow us to take it. After we take the lock, it will race with us to reenter the
|
|
// GatheredDataProcessingLock and then enter the InterfaceLock, and will block on the InterfaceLock as
|
|
// long as we are still holding it.
|
|
// By requesting pause We tell the GlobalGatherer to leave the GatheredDataProcessingLock and not try
|
|
// to reenter it until we request resume.
|
|
GuardedData.RequestPauseBackgroundProcessing();
|
|
// We drop the InterfaceLock to allow the globalgatherer to continue on if it is waiting on it.
|
|
}
|
|
// After dropping the InterfaceLock, we block on the GatheredDataProcessingLock, waiting for the
|
|
// GlobalGatherer to notice that backgroundprocessing is paused and get out of both of the locks.
|
|
GatheredDataProcessingLock.Lock();
|
|
bHasEnteredGatheredDataProcessingLock = true;
|
|
// We unpause after we finish ticking
|
|
}
|
|
#endif
|
|
|
|
if (bHasEnteredGatheredDataProcessingLock)
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
UE::AssetRegistry::Impl::FClassInheritanceContext InheritanceContext;
|
|
UE::AssetRegistry::Impl::FClassInheritanceBuffer InheritanceBuffer;
|
|
GetInheritanceContextWithRequiredLock(InterfaceScopeLock, InheritanceContext, InheritanceBuffer);
|
|
|
|
// Process any deferred events
|
|
{
|
|
FScopeLock DeferredEventsLock(&DeferredEventsCriticalSection);
|
|
EventContext = MoveTemp(DeferredEvents);
|
|
DeferredEvents.Clear();
|
|
}
|
|
|
|
if (EventContext.IsEmpty())
|
|
{
|
|
// Tick the Gatherer
|
|
UE::AssetRegistry::Impl::FTickContext TickContext(GuardedData, EventContext, InheritanceContext);
|
|
LocalMaxSecondsPerFrame = GuardedData.MaxSecondsPerFrame;
|
|
TickContext.InterruptionContext.SetLimitedTickTime(TickStartTime, LocalMaxSecondsPerFrame);
|
|
TickContext.bHandleCompletion = true;
|
|
TickContext.bHandleDeferred = true;
|
|
Status = GuardedData.TickGatherer(TickContext);
|
|
bInterruptedOrShouldProcessDeferredEvents = TickContext.InterruptionContext.WasInterrupted();
|
|
}
|
|
else
|
|
{
|
|
// Skip the TickGather to deal with the DeferredEvents first
|
|
bInterruptedOrShouldProcessDeferredEvents = true;
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
if (bTakeOverGather)
|
|
{
|
|
// As soon as we execute this unpause, the globalgatherer can race to reenter the locks
|
|
// but it will block entering GatheredDataProcessingLock until we unlock it on the next line.
|
|
GuardedData.RequestResumeBackgroundProcessing();
|
|
}
|
|
#endif
|
|
GatheredDataProcessingLock.Unlock();
|
|
}
|
|
else
|
|
{
|
|
{
|
|
FScopeLock DeferredEventsLock(&DeferredEventsCriticalSection);
|
|
EventContext.Append(MoveTemp(DeferredEvents));
|
|
DeferredEvents.Clear();
|
|
}
|
|
GuardedData.ProcessGameThreadRequestedClasses();
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
if (!bInterruptedOrShouldProcessDeferredEvents)
|
|
{
|
|
UE::AssetRegistry::Impl::FInterruptionContext InterruptionContext;
|
|
InterruptionContext.SetLimitedTickTime(TickStartTime, LocalMaxSecondsPerFrame);
|
|
ProcessLoadedAssetsToUpdateCache(EventContext, Status, InterruptionContext);
|
|
bInterruptedOrShouldProcessDeferredEvents = bInterruptedOrShouldProcessDeferredEvents
|
|
|| InterruptionContext.WasInterrupted();
|
|
}
|
|
#endif
|
|
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE_STR("Asset Registry Event Broadcast");
|
|
Broadcast(EventContext, true /* bAllowFileLoadedEvent */);
|
|
}
|
|
} while ((bInterruptedOrShouldProcessDeferredEvents || Status == UE::AssetRegistry::Impl::EGatherStatus::WaitingForEvents) &&
|
|
(TickStartTime < 0 || (FPlatformTime::Seconds() - TickStartTime) <= LocalMaxSecondsPerFrame));
|
|
}
|
|
|
|
void UAssetRegistryImpl::RequestTick()
|
|
{
|
|
// Called from within DeferredEventsCriticalSection
|
|
// RequestTick is not used if WITH_EDITOR; the AssetRegistry is ticked every frame from
|
|
// UEditorEngine::Tick or a WITH_EDITOR block in UGameEngine::Tick.
|
|
#if !WITH_EDITOR
|
|
if (TickRequestHandle.IsValid())
|
|
{
|
|
// Already Set
|
|
return;
|
|
}
|
|
TickRequestHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda([this](float)
|
|
{
|
|
// DeltaTime: non-negative to indicate we have a time limit, as small as possible to do just the minimum.
|
|
constexpr float DeltaTime = 0.0f;
|
|
this->Tick(DeltaTime);
|
|
return false; // Do not continue ticking
|
|
}));
|
|
#endif
|
|
}
|
|
|
|
void UAssetRegistryImpl::ClearRequestTick()
|
|
{
|
|
// Called from within DeferredEventsCriticalSection
|
|
// RequestTick/ClearRequestTick are not used if WITH_EDITOR; the AssetRegistry is ticked every frame from
|
|
// UEditorEngine::Tick or a WITH_EDITOR block in UGameEngine::Tick.
|
|
#if !WITH_EDITOR
|
|
if (!TickRequestHandle.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
FTSTicker::RemoveTicker(TickRequestHandle);
|
|
TickRequestHandle.Reset();
|
|
#endif
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::WaitForGathererIdleIfSynchronous()
|
|
{
|
|
if (GlobalGatherer && GlobalGatherer->IsSynchronous())
|
|
{
|
|
GlobalGatherer->WaitForIdle();
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::WaitForGathererIdle(float TimeoutSeconds)
|
|
{
|
|
if (GlobalGatherer)
|
|
{
|
|
GlobalGatherer->WaitForIdle(TimeoutSeconds);
|
|
}
|
|
}
|
|
|
|
bool FAssetRegistryImpl::ClassRequiresGameThreadProcessing(const UClass* Class) const
|
|
{
|
|
// This function is not called. See FAssetDataGatherer::TickInternal for where
|
|
// it would be called if it were fully implemented.
|
|
return true;
|
|
}
|
|
|
|
void FAssetRegistryImpl::UpdateMaxSecondsPerFrame()
|
|
{
|
|
float NewMaxSecondsPerFrame = UE::AssetRegistry::Impl::MaxSecondsPerFrame;
|
|
#if WITH_EDITOR
|
|
bool bGatherOnGameThreadOnly = false;
|
|
GConfig->GetBool(TEXT("AssetRegistry"), TEXT("GatherOnGameThreadOnly"), bGatherOnGameThreadOnly, GEngineIni);
|
|
bool bLocalGameThreadTakeOverGatherEachTick = false;
|
|
|
|
// Skip this block even if we're still gathering as part of AdditionalMountSearch;
|
|
// this block applies only during initial search
|
|
if (bInitialSearchStarted && !IsInitialSearchCompleted())
|
|
{
|
|
bool bBlockingInitialLoad;
|
|
GConfig->GetBool(TEXT("AssetRegistry"), TEXT("BlockingInitialLoad"), bBlockingInitialLoad, GEditorPerProjectIni);
|
|
if (bBlockingInitialLoad)
|
|
{
|
|
bLocalGameThreadTakeOverGatherEachTick = true;
|
|
NewMaxSecondsPerFrame = UE::AssetRegistry::Impl::MaxSecondsPerFrameToUseInBlockingInitialLoad;
|
|
if (MaxSecondsPerFrame < NewMaxSecondsPerFrame)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Display,
|
|
TEXT("EditorPerProjectUserSettings.ini:[AssetRegistry]:BlockingInitialLoad=true, setting AssetRegistry load to blocking. The editor will not be interactive until the initial scan completes."));
|
|
}
|
|
}
|
|
}
|
|
if (GlobalGatherer)
|
|
{
|
|
GlobalGatherer->SetGatherOnGameThreadOnly(bGatherOnGameThreadOnly);
|
|
}
|
|
SetGameThreadTakeOverGatherEachTick(bLocalGameThreadTakeOverGatherEachTick);
|
|
|
|
#endif
|
|
MaxSecondsPerFrame = NewMaxSecondsPerFrame;
|
|
}
|
|
|
|
Impl::EGatherStatus FAssetRegistryImpl::TickGatherer(Impl::FTickContext& TickContext)
|
|
{
|
|
using namespace UE::AssetRegistry::Impl;
|
|
|
|
if (!GlobalGatherer.IsValid())
|
|
{
|
|
return EGatherStatus::Complete;
|
|
}
|
|
|
|
// Gather results from the background search
|
|
GlobalGatherer->GetAndTrimSearchResults(BackgroundResults, TickContext.ResultContext);
|
|
TickContext.SetNumGatherFromDiskPendingDirty(); // Invalidate cache if used in an earlier TickGatherer call
|
|
if (!IsGathering() && !TickContext.ResultContext.bIsSearching && TickContext.GetNumPending() == 0)
|
|
{
|
|
// This is the common case of AssetRegistry ticks after the gather completed. We were already complete before
|
|
// the tick, and found nothing new to gather. Early exit now to avoid doing spending time checking each of the
|
|
// pieces of gathered data.
|
|
return EGatherStatus::Complete;
|
|
}
|
|
|
|
TRACE_CPUPROFILER_EVENT_SCOPE_STR_CONDITIONAL("FAssetRegistryImpl::TickGatherer", IsGathering());
|
|
// Initialize per-tick TickContext data and alias some of its fields to reduce duplication
|
|
FEventContext& EventContext = TickContext.EventContext;
|
|
FInterruptionContext& InOutInterruptionContext = TickContext.InterruptionContext;
|
|
TickContext.MountPointsForVerifyAfterGather.Reset();
|
|
TickContext.bHadAssetsToProcess = BackgroundResults.Assets.Num() > 0 || BackgroundResults.Dependencies.Num() > 0;
|
|
TickContext.bIsInGameThread = IsInGameThread();
|
|
TickContext.TimingStartTime = -1.;
|
|
ON_SCOPE_EXIT
|
|
{
|
|
TickContext.RecordTimer();
|
|
};
|
|
|
|
// We will be modifying the arrays that contribute to NumGatherFromDiskPending below, and we will need the updated
|
|
// value after we finish them or when we early exit, so mark it dirty now.
|
|
TickContext.SetNumGatherFromDiskPendingDirty();
|
|
|
|
// Report the search times
|
|
for (double SearchTime : TickContext.ResultContext.SearchTimes)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Verbose, TEXT("### Background search completed in %0.4f seconds"), SearchTime);
|
|
}
|
|
|
|
// Add discovered paths
|
|
if (BackgroundResults.Paths.Num())
|
|
{
|
|
TickContext.LazyStartTimer();
|
|
PathDataGathered(EventContext, BackgroundResults.Paths, InOutInterruptionContext,
|
|
TickContext.MountPointsForVerifyAfterGather);
|
|
}
|
|
if (InOutInterruptionContext.ShouldExitEarly())
|
|
{
|
|
return TickContext.UpdateIntermediateStatus();
|
|
}
|
|
|
|
ConditionalRetryGatherAssetsForGameThread(TickContext);
|
|
|
|
bool bRetryAssetGathering = true;
|
|
int32 OriginalDeferredAssetsCount = 0;
|
|
int32 NumRetries = 0;
|
|
while (bRetryAssetGathering)
|
|
{
|
|
bRetryAssetGathering = false;
|
|
|
|
// Process the normal results and defer anything that isn't ready
|
|
TickContext.RunAssetSearchDataGathered(BackgroundResults.Assets, DeferredAssets);
|
|
if (InOutInterruptionContext.ShouldExitEarly())
|
|
{
|
|
return TickContext.UpdateIntermediateStatus();
|
|
}
|
|
|
|
if (TickContext.bIsInGameThread)
|
|
{
|
|
TickContext.RunAssetSearchDataGathered(BackgroundResults.AssetsForGameThread, DeferredAssetsForGameThread);
|
|
if (InOutInterruptionContext.ShouldExitEarly())
|
|
{
|
|
return TickContext.UpdateIntermediateStatus();
|
|
}
|
|
|
|
// If we cleared out AssetsForGameThread, mark that we no longer need GameThreadRequestedClasses.
|
|
ConditionalClearGameThreadRequestedClasses(TickContext);
|
|
}
|
|
|
|
TSet<FName>* PackagesNeedingDependencyCalculationPointer = nullptr;
|
|
#if WITH_EDITOR
|
|
PackagesNeedingDependencyCalculationPointer = &PackagesNeedingDependencyCalculation;
|
|
#endif
|
|
TickContext.RunDependencyDataGathered(BackgroundResults.Dependencies, DeferredDependencies,
|
|
PackagesNeedingDependencyCalculationPointer);
|
|
if (InOutInterruptionContext.ShouldExitEarly())
|
|
{
|
|
return TickContext.UpdateIntermediateStatus();
|
|
}
|
|
|
|
if (TickContext.bIsInGameThread)
|
|
{
|
|
#if WITH_EDITOR
|
|
PackagesNeedingDependencyCalculationPointer = &PackagesNeedingDependencyCalculationOnGameThread;
|
|
#endif
|
|
TickContext.RunDependencyDataGathered(BackgroundResults.DependenciesForGameThread,
|
|
DeferredDependenciesForGameThread, PackagesNeedingDependencyCalculationPointer);
|
|
if (InOutInterruptionContext.ShouldExitEarly())
|
|
{
|
|
return TickContext.UpdateIntermediateStatus();
|
|
}
|
|
}
|
|
|
|
// Retry deferred assets if we've finished all the other assets; we need to do this in the current tick
|
|
// so we avoid spuriously reporting status == UnableToProgress
|
|
if (BackgroundResults.Assets.IsEmpty()
|
|
&& (!TickContext.bIsInGameThread || BackgroundResults.AssetsForGameThread.IsEmpty())
|
|
&& TickContext.bHandleDeferred)
|
|
{
|
|
if (!DeferredAssets.IsEmpty() || !DeferredDependencies.IsEmpty() ||
|
|
(TickContext.bIsInGameThread && (!DeferredAssetsForGameThread.IsEmpty() || !DeferredDependenciesForGameThread.IsEmpty())))
|
|
{
|
|
if (bProcessedAnyAssetsAfterRetryDeferred)
|
|
{
|
|
bRetryAssetGathering = true;
|
|
}
|
|
else
|
|
{
|
|
if (!bForceCompletionEvenIfClassNotLoaded && bPreloadingComplete && IsEngineStartupModuleLoadingComplete())
|
|
{
|
|
bForceCompletionEvenIfClassNotLoaded = true;
|
|
bRetryAssetGathering = true;
|
|
}
|
|
}
|
|
if (bRetryAssetGathering)
|
|
{
|
|
bProcessedAnyAssetsAfterRetryDeferred = false;
|
|
if (NumRetries == 0)
|
|
{
|
|
OriginalDeferredAssetsCount = DeferredAssets.Num() + DeferredAssetsForGameThread.Num()
|
|
+ 10; // fudge factor to make sure an edge case of 0 does not cause a problem
|
|
}
|
|
if (NumRetries++ >= OriginalDeferredAssetsCount)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Error, TEXT("Runaway loop detected in handling of deferred assets"));
|
|
// This will cause us to return UnableToProgress status
|
|
break;
|
|
}
|
|
BackgroundResults.Assets.Append(MoveTemp(DeferredAssets));
|
|
BackgroundResults.AssetsForGameThread.Append(MoveTemp(DeferredAssetsForGameThread));
|
|
BackgroundResults.Dependencies.Append(MoveTemp(DeferredDependencies));
|
|
BackgroundResults.DependenciesForGameThread.Append(MoveTemp(DeferredDependenciesForGameThread));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load cooked packages that do not have asset data
|
|
if (BackgroundResults.CookedPackageNamesWithoutAssetData.Num())
|
|
{
|
|
TickContext.LazyStartTimer();
|
|
CookedPackageNamesWithoutAssetDataGathered(EventContext, BackgroundResults.CookedPackageNamesWithoutAssetData, InOutInterruptionContext);
|
|
if (InOutInterruptionContext.ShouldExitEarly())
|
|
{
|
|
return TickContext.UpdateIntermediateStatus();
|
|
}
|
|
}
|
|
|
|
// Add Verse files
|
|
if (BackgroundResults.VerseFiles.Num())
|
|
{
|
|
TickContext.LazyStartTimer();
|
|
|
|
VerseFilesGathered(EventContext, BackgroundResults.VerseFiles, InOutInterruptionContext);
|
|
if (InOutInterruptionContext.ShouldExitEarly())
|
|
{
|
|
return TickContext.UpdateIntermediateStatus();
|
|
}
|
|
}
|
|
|
|
// Store blocked files to be reported
|
|
if (BackgroundResults.BlockedFiles.Num())
|
|
{
|
|
EventContext.BlockedFiles.Append(MoveTemp(BackgroundResults.BlockedFiles));
|
|
BackgroundResults.BlockedFiles.Reset();
|
|
}
|
|
|
|
bool bDiskGatherComplete = !TickContext.ResultContext.bIsSearching
|
|
&& TickContext.GetNumGatherFromDiskPending() == 0;
|
|
if (!bDiskGatherComplete)
|
|
{
|
|
// We're still gathering from disk or deferred data in the arrays above, we can't run the on-disk-gather
|
|
// complete code below so there is nothing further to do for now.
|
|
return TickContext.UpdateIntermediateStatus();
|
|
}
|
|
|
|
// Execute calculations in the gather that have to happen after the gather from disk is completed; the gather is
|
|
// not marked complete until these are done.
|
|
|
|
if (!IsEngineStartupModuleLoadingComplete() || !bPreloadingComplete)
|
|
{
|
|
// We can't complete the gather until we've finished startup, because more modules might mount up to that
|
|
// point, and when mounted add more paths that we need to gather.
|
|
// We also can't do some of the calculations until then. LoadCalculatedDependencies has to wait because
|
|
// modules might add new entries to RegisteredDependencyGathererClasses as they are loaded.
|
|
// We also cannot complete, or run calculations requiring knowledge of all assets, if we have a preloaded
|
|
// AssetRegistry and we have not finished preloading it.
|
|
|
|
// Since we can not progress on some of the calculations, return now.
|
|
return TickContext.UpdateIntermediateStatus();
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
// Load Calculated Dependencies for types that register for them via REGISTER_ASSETDEPENDENCY_GATHERER
|
|
if (PackagesNeedingDependencyCalculation.Num() || PackagesNeedingDependencyCalculationOnGameThread.Num())
|
|
{
|
|
TickContext.LazyStartTimer();
|
|
// Only assets whose classes have a RegisteredDependencyGathererClasses entry need to run through
|
|
// LoadCalculatedDependencies. Furthermore, we must always perform their gather on the game thread.
|
|
// PruneAndCoalesce does the non-game thread calculations we can, including removing assets that don't need
|
|
// any calculations, and moves the remaining issues to the container that needs action from the game thread.
|
|
PruneAndCoalescePackagesRequiringDependencyCalculation(PackagesNeedingDependencyCalculation,
|
|
PackagesNeedingDependencyCalculationOnGameThread, InOutInterruptionContext);
|
|
if (InOutInterruptionContext.ShouldExitEarly())
|
|
{
|
|
return TickContext.UpdateIntermediateStatus();
|
|
}
|
|
// All remaining assets should now be in PackagesNeedingDependencyCalculationOnGameThread.
|
|
ensure(PackagesNeedingDependencyCalculation.Num() == 0);
|
|
|
|
if (PackagesNeedingDependencyCalculationOnGameThread.Num() && !TickContext.bIsInGameThread)
|
|
{
|
|
// Since we can not progress, return now.
|
|
return TickContext.UpdateIntermediateStatus();
|
|
}
|
|
|
|
LoadCalculatedDependencies(nullptr, TickContext.InheritanceContext,
|
|
&PackagesNeedingDependencyCalculationOnGameThread, InOutInterruptionContext);
|
|
if (InOutInterruptionContext.ShouldExitEarly())
|
|
{
|
|
return TickContext.UpdateIntermediateStatus();
|
|
}
|
|
}
|
|
|
|
if (bGatherNeedsApplyObjectRedirects)
|
|
{
|
|
ApplyObjectRedirectsToGatheredAssetDatas();
|
|
if (InOutInterruptionContext.ShouldExitEarly())
|
|
{
|
|
return TickContext.UpdateIntermediateStatus();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Finishing the gather is only possible on the game thread (and only for callsites of TickGather that opt into it)
|
|
// because we need to not mark the gather complete until we have broadcast the events, and broadcasting events by
|
|
// contract is only allowed from game thread.
|
|
if (!TickContext.bIsInGameThread || !TickContext.bHandleCompletion)
|
|
{
|
|
return TickContext.SetIntermediateStatus(EGatherStatus::TickGameThreadActiveGatherIdle);
|
|
}
|
|
|
|
if (!IsInitialSearchCompleted())
|
|
{
|
|
if (!EventContext.AssetEvents.IsEmpty())
|
|
{
|
|
// Don't mark the InitialSearch completed until we have sent all the AssetDataAdded events
|
|
// that arose from the final tick of the gatherer. Some callers might do more expensive
|
|
// work for assets added after the initial search completed, and we don't want them to do
|
|
// that more expensive work on the last batch of assets before completion.
|
|
return TickContext.SetIntermediateStatus(EGatherStatus::WaitingForEvents);
|
|
}
|
|
|
|
TickContext.RecordTimer(); // OnInitialSearchComplete reads data set by RecordTimer
|
|
OnInitialSearchCompleted(EventContext);
|
|
}
|
|
else if (IsAdditionalMountSearchInProgress())
|
|
{
|
|
TickContext.RecordTimer(); // OnAdditionalMountSearchComplete reads data set by RecordTimer
|
|
OnAdditionalMountSearchCompleted(EventContext);
|
|
}
|
|
|
|
// Give ProgressUpdateData a final call before going idle so that listeners can record 0 remaining.
|
|
EventContext.ProgressUpdateData.Emplace(
|
|
HighestPending, // NumTotalAssets
|
|
HighestPending, // NumAssetsProcessedByAssetRegistry
|
|
0, // NumAssetsPendingDataLoad
|
|
false // bIsDiscoveringAssetFiles
|
|
);
|
|
// Clear HighestPending
|
|
HighestPending = 0;
|
|
this->GatherStatus = EGatherStatus::Complete;
|
|
return EGatherStatus::Complete;
|
|
}
|
|
|
|
void FAssetRegistryImpl::ProcessGameThreadRequestedClasses()
|
|
{
|
|
if (!bGameThreadRequestedClassesShouldProcess.load(std::memory_order_relaxed))
|
|
{
|
|
return;
|
|
}
|
|
|
|
TSet<FTopLevelAssetPath> ClassesToProcess;
|
|
{
|
|
FScopeLock GameThreadRequestedClassesScopeLock(&GameThreadRequestedClassesLock);
|
|
ClassesToProcess = MoveTemp(GameThreadRequestedClasses);
|
|
GameThreadRequestedClasses.Empty();
|
|
bGameThreadRequestedClassesShouldProcess.store(false, std::memory_order_relaxed);
|
|
}
|
|
|
|
for (const FTopLevelAssetPath& AssetClassPath : ClassesToProcess)
|
|
{
|
|
UClass* AssetClass = FindObject<UClass>(AssetClassPath, EFindObjectFlags::ExactClass);
|
|
if (!AssetClass)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Process each of the possible properties we are waiting on for the GameThreadRequestedClass
|
|
if (AssetClass->GetSparseClassDataStruct())
|
|
{
|
|
AssetClass->GetSparseClassData(EGetSparseClassDataMethod::ArchetypeIfNull);
|
|
}
|
|
}
|
|
bGameThreadRequestedClassesShouldRetry.store(true, std::memory_order_release);
|
|
}
|
|
|
|
void FAssetRegistryImpl::ConditionalRetryGatherAssetsForGameThread(Impl::FTickContext& TickContext)
|
|
{
|
|
if (!TickContext.bIsInGameThread && bGameThreadRequestedClassesShouldRetry.load(std::memory_order_relaxed))
|
|
{
|
|
// When we find we were instructed to retry, block on an acquire memory fence so that the
|
|
// modifications the game thread made to the classes are transferred from the other thread before
|
|
// we continue on.
|
|
std::atomic_thread_fence(std::memory_order_acquire);
|
|
bGameThreadRequestedClassesShouldRetry.store(false, std::memory_order_release);
|
|
for (TPair<FName, TUniquePtr<FAssetData>>& GameThreadPair : BackgroundResults.AssetsForGameThread)
|
|
{
|
|
BackgroundResults.Assets.Add(GameThreadPair.Key, MoveTemp(GameThreadPair.Value));
|
|
}
|
|
BackgroundResults.AssetsForGameThread.Empty();
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::ConditionalClearGameThreadRequestedClasses(Impl::FTickContext& TickContext)
|
|
{
|
|
if (BackgroundResults.AssetsForGameThread.IsEmpty()
|
|
&& bGameThreadRequestedClassesShouldProcess.load(std::memory_order_relaxed))
|
|
{
|
|
FScopeLock GameThreadRequestedClassesScopeLock(&GameThreadRequestedClassesLock);
|
|
GameThreadRequestedClasses.Empty();
|
|
bGameThreadRequestedClassesShouldProcess.store(false, std::memory_order_relaxed);
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::RequestGameThreadProcessClass(const FTopLevelAssetPath& RequestedGameThreadClass)
|
|
{
|
|
FScopeLock GameThreadRequestedClassesScopeLock(&GameThreadRequestedClassesLock);
|
|
GameThreadRequestedClasses.Add(RequestedGameThreadClass);
|
|
bGameThreadRequestedClassesShouldProcess.store(true, std::memory_order_relaxed);
|
|
}
|
|
|
|
namespace Impl // UE::AssetRegistry::Impl
|
|
{
|
|
|
|
FTickContext::FTickContext(FAssetRegistryImpl& InGuardedData, Impl::FEventContext& InEventContext,
|
|
Impl::FClassInheritanceContext& InInheritanceContext)
|
|
: GuardedData(InGuardedData)
|
|
, EventContext(InEventContext)
|
|
, InheritanceContext(InInheritanceContext)
|
|
{
|
|
}
|
|
|
|
void FTickContext::LazyStartTimer()
|
|
{
|
|
if (TimingStartTime <= 0.)
|
|
{
|
|
TimingStartTime = FPlatformTime::Seconds();
|
|
}
|
|
};
|
|
void FTickContext::RecordTimer()
|
|
{
|
|
if (TimingStartTime > 0.)
|
|
{
|
|
GuardedData.StoreGatherResultsTimeSeconds += static_cast<float>(FPlatformTime::Seconds() - TimingStartTime);
|
|
TimingStartTime = -1.;
|
|
}
|
|
};
|
|
|
|
void FTickContext::SetNumGatherFromDiskPendingDirty()
|
|
{
|
|
bNumGatherFromDiskPendingDirty = true;
|
|
}
|
|
|
|
int32 FTickContext::GetNumGatherFromDiskPending()
|
|
{
|
|
if (bNumGatherFromDiskPendingDirty)
|
|
{
|
|
bNumGatherFromDiskPendingDirty = false;
|
|
NumGatherFromDiskPending = CalculateNumGatherFromDiskPending();
|
|
}
|
|
return NumGatherFromDiskPending;
|
|
}
|
|
|
|
int32 FTickContext::GetNumPending()
|
|
{
|
|
int32 NumPending = GetNumGatherFromDiskPending();
|
|
#if WITH_EDITOR
|
|
int32 NumNeedingCalculation = GuardedData.PackagesNeedingDependencyCalculation.Num()
|
|
+ GuardedData.PackagesNeedingDependencyCalculationOnGameThread.Num();
|
|
NumPending += NumNeedingCalculation > 0 ? 1 : 0;
|
|
#endif
|
|
return NumPending;
|
|
}
|
|
|
|
int32 FTickContext::CalculateNumGatherFromDiskPending()
|
|
{
|
|
// Structure that holds temporary data for the current call to TickGatherer
|
|
UE::AssetDataGather::FResultContext& C = this->ResultContext;
|
|
// Structure that holds persistent data stored across ticks.
|
|
UE::AssetDataGather::FResults& B = GuardedData.BackgroundResults;
|
|
// Reference to GuardedData for persistent data stored as fields on GuardedData rather than fields on the
|
|
// FResults struct
|
|
FAssetRegistryImpl& G = GuardedData;
|
|
|
|
return
|
|
C.NumFilesToSearch + C.NumPathsToSearch
|
|
|
|
+ B.Paths.Num() + B.Assets.Num() + B.AssetsForGameThread.Num() + B.Dependencies.Num()
|
|
+ B.DependenciesForGameThread.Num() + B.CookedPackageNamesWithoutAssetData.Num()
|
|
+ B.VerseFiles.Num() + B.BlockedFiles.Num()
|
|
|
|
+ G.DeferredAssets.Num() + G.DeferredAssetsForGameThread.Num() + G.DeferredDependencies.Num()
|
|
+ G.DeferredDependenciesForGameThread.Num()
|
|
;
|
|
};
|
|
|
|
EGatherStatus FTickContext::SetIntermediateStatus(EGatherStatus Status)
|
|
{
|
|
// Report total pending and highest pending for this run so we can show a good progress bar
|
|
int32 LocalNumPending = GetNumPending();
|
|
int32& HighestPending = GuardedData.HighestPending;
|
|
HighestPending = FMath::Max(HighestPending, LocalNumPending);
|
|
if (ResultContext.bIsSearching || bHadAssetsToProcess)
|
|
{
|
|
EventContext.ProgressUpdateData.Emplace(
|
|
HighestPending, // NumTotalAssets
|
|
HighestPending - LocalNumPending, // NumAssetsProcessedByAssetRegistry
|
|
LocalNumPending / 2, // NumAssetsPendingDataLoad, divided by 2 because assets are double counted due to dependencies
|
|
ResultContext.bIsDiscoveringFiles // bIsDiscoveringAssetFiles
|
|
);
|
|
}
|
|
GuardedData.GatherStatus = Status;
|
|
return Status;
|
|
};
|
|
|
|
EGatherStatus FTickContext::UpdateIntermediateStatus()
|
|
{
|
|
EGatherStatus Status;
|
|
if (InterruptionContext.WasInterrupted())
|
|
{
|
|
// When interrupted we don't know the current status, so just keep the previous status, unless
|
|
// the previous status is a temporary status, in which case just switch it over to TickActive
|
|
switch (GuardedData.GatherStatus)
|
|
{
|
|
case EGatherStatus::WaitingForEvents: [[fallthrough]];
|
|
case EGatherStatus::UnableToProgress:
|
|
Status = ResultContext.bAbleToProgress ? EGatherStatus::TickActiveGatherActive
|
|
: EGatherStatus::TickActiveGatherIdle;
|
|
break;
|
|
default:
|
|
Status = GuardedData.GatherStatus;
|
|
break;
|
|
}
|
|
}
|
|
else if (ResultContext.bIsSearching && !ResultContext.bAbleToProgress)
|
|
{
|
|
// Gather from disk is still in progress but is blocked
|
|
Status = EGatherStatus::UnableToProgress;
|
|
}
|
|
else if (ResultContext.bIsSearching)
|
|
{
|
|
// We still have work we can do in the gather
|
|
Status = EGatherStatus::TickActiveGatherActive;
|
|
}
|
|
else
|
|
{
|
|
// We have received all of the assets from disk, but we are blocked on some of them, or we are blocked
|
|
// on some followup calculations we need to do, because we have to wait for EngineStartup or some other
|
|
// condition outside our control.
|
|
Status = EGatherStatus::UnableToProgress;
|
|
}
|
|
|
|
if (Status == Impl::EGatherStatus::TickActiveGatherIdle)
|
|
{
|
|
// if there's no additional work the gatherer thread can perform, change the status from TickActiveGatherIdle
|
|
// to TickGameThreadActiveGatherIdle.
|
|
UE::AssetDataGather::FResults& B = GuardedData.BackgroundResults;
|
|
if (GuardedData.DeferredAssets.Num() == 0
|
|
#if WITH_EDITOR
|
|
&& GuardedData.PackagesNeedingDependencyCalculation.Num() == 0
|
|
#endif
|
|
&& GuardedData.DeferredDependencies.Num() == 0
|
|
&& B.Assets.Num() == 0
|
|
&& B.Dependencies.Num() == 0
|
|
&& B.CookedPackageNamesWithoutAssetData.Num() == 0
|
|
&& B.Paths.Num() == 0)
|
|
{
|
|
Status = Impl::EGatherStatus::TickGameThreadActiveGatherIdle;
|
|
}
|
|
}
|
|
return SetIntermediateStatus(Status);
|
|
};
|
|
|
|
void FTickContext::RunAssetSearchDataGathered(TMultiMap<FName, TUniquePtr<FAssetData>>& InAssetResults,
|
|
TMultiMap<FName, TUniquePtr<FAssetData>>& OutDeferredAssetResults)
|
|
{
|
|
// Process the asset results
|
|
if (InAssetResults.Num())
|
|
{
|
|
LazyStartTimer();
|
|
|
|
EDeferFlag DeferFlag;
|
|
if (bIsInGameThread)
|
|
{
|
|
DeferFlag = EDeferFlag::CanExecuteGameThread;
|
|
}
|
|
else if (!GuardedData.bForceCompletionEvenIfClassNotLoaded)
|
|
{
|
|
// If we are still able to wait on missing classes, then defer rather than kicking to game thread.
|
|
// The Gamethread might end up creating the required data for us if we continue waiting.
|
|
DeferFlag = EDeferFlag::RetryLater;
|
|
}
|
|
else if (!GuardedData.bForceCompletionEvenIfNotOnGameThread)
|
|
{
|
|
DeferFlag = EDeferFlag::QueueToGameThread;
|
|
}
|
|
else
|
|
{
|
|
DeferFlag = EDeferFlag::BypassNow;
|
|
}
|
|
|
|
GuardedData.AssetSearchDataGathered(EventContext, InAssetResults, OutDeferredAssetResults,
|
|
InterruptionContext, MountPointsForVerifyAfterGather, DeferFlag);
|
|
}
|
|
};
|
|
|
|
void FTickContext::RunDependencyDataGathered(TMultiMap<FName, FPackageDependencyData>& DependenciesToProcess,
|
|
TMultiMap<FName, FPackageDependencyData>& OutDeferredDependencies,
|
|
TSet<FName>* OutPackagesNeedingDependencyCalculation)
|
|
{
|
|
// Add dependencies
|
|
if (DependenciesToProcess.Num())
|
|
{
|
|
LazyStartTimer();
|
|
|
|
GuardedData.DependencyDataGathered(DependenciesToProcess, OutDeferredDependencies,
|
|
OutPackagesNeedingDependencyCalculation, InterruptionContext, MountPointsForVerifyAfterGather);
|
|
}
|
|
};
|
|
|
|
} // namespace Impl within UE::AssetRegistry
|
|
|
|
#if WITH_EDITOR
|
|
void FAssetRegistryImpl::ApplyObjectRedirectsToGatheredAssetDatas()
|
|
{
|
|
bGatherNeedsApplyObjectRedirects = false;
|
|
|
|
State.EnumerateAllMutableAssets([](FAssetData& AssetData)
|
|
{
|
|
if (!AssetData.TaggedAssetBundles)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TArray<FAssetBundleEntry>& Bundles = AssetData.TaggedAssetBundles->Bundles;
|
|
int32 NumBundles = Bundles.Num();
|
|
int32 FirstChangedBundle = -1;
|
|
int32 FirstChangedAssetPath = -1;
|
|
FSoftObjectPath FirstChangedAssetPathValue;
|
|
for (FirstChangedBundle = 0; FirstChangedBundle < NumBundles; ++FirstChangedBundle)
|
|
{
|
|
FAssetBundleEntry& Bundle = Bundles[FirstChangedBundle];
|
|
int32 NumAssetPaths = Bundle.AssetPaths.Num();
|
|
for (FirstChangedAssetPath = 0; FirstChangedAssetPath < NumAssetPaths; ++FirstChangedAssetPath)
|
|
{
|
|
FSoftObjectPath Original(Bundle.AssetPaths[FirstChangedAssetPath]);
|
|
FSoftObjectPath Redirected = GRedirectCollector.GetAssetPathRedirection(Original);
|
|
if (Redirected.IsValid())
|
|
{
|
|
FirstChangedAssetPathValue = Redirected;
|
|
break;
|
|
}
|
|
}
|
|
if (FirstChangedAssetPath < NumAssetPaths)
|
|
{
|
|
break;
|
|
}
|
|
FirstChangedAssetPath = -1;
|
|
}
|
|
|
|
if (FirstChangedBundle == NumBundles)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FAssetBundleData NewData;
|
|
NewData.Bundles.Reserve(NumBundles);
|
|
int32 BundleIndex;
|
|
for (BundleIndex = 0; BundleIndex < FirstChangedBundle; ++BundleIndex)
|
|
{
|
|
NewData.Bundles.Add(Bundles[BundleIndex]);
|
|
}
|
|
for (; BundleIndex < NumBundles; ++BundleIndex)
|
|
{
|
|
FAssetBundleEntry& NewBundle = NewData.Bundles.Emplace_GetRef(Bundles[BundleIndex]);
|
|
TArray<FTopLevelAssetPath>& NewAssetPaths = NewBundle.AssetPaths;
|
|
|
|
int32 NumAssetPaths = NewAssetPaths.Num();
|
|
int32 AssetPathIndex = FirstChangedAssetPath != -1 ? FirstChangedAssetPath : 0;
|
|
for (; AssetPathIndex < NumAssetPaths; ++AssetPathIndex)
|
|
{
|
|
FSoftObjectPath Redirected;
|
|
FSoftObjectPath Original(NewAssetPaths[AssetPathIndex]);
|
|
if (FirstChangedAssetPath == -1)
|
|
{
|
|
Redirected = GRedirectCollector.GetAssetPathRedirection(Original);
|
|
}
|
|
else
|
|
{
|
|
check(AssetPathIndex == FirstChangedAssetPath);
|
|
Redirected = FirstChangedAssetPathValue;
|
|
FirstChangedAssetPathValue = FSoftObjectPath();
|
|
FirstChangedAssetPath = -1;
|
|
}
|
|
|
|
if (Redirected.IsNull())
|
|
{
|
|
continue;
|
|
}
|
|
if (!Redirected.GetSubPathUtf8String().IsEmpty())
|
|
{
|
|
UE_LOG(LogAssetRegistry, Warning,
|
|
TEXT("AssetBundleEntry Name %s.%s[%d] was redirected from %s -> %s, but the redirected target is not a TopLevelAssetPath, and AssetBundleEntry Names must be TopLevelAssetPaths. Ignoring the redirector."),
|
|
*AssetData.ToSoftObjectPath().ToString(), *NewBundle.BundleName.ToString(), AssetPathIndex, *Original.ToString(), *Redirected.ToString());
|
|
continue;
|
|
}
|
|
NewAssetPaths[AssetPathIndex] = Redirected.GetAssetPath();
|
|
}
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
NewBundle.BundleAssets.Reset();
|
|
Algo::Transform(NewBundle.AssetPaths, NewBundle.BundleAssets, [](FTopLevelAssetPath Path) { return FSoftObjectPath(Path, {}); });
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
}
|
|
AssetData.TaggedAssetBundles = MakeShared<FAssetBundleData, ESPMode::ThreadSafe>(MoveTemp(NewData));
|
|
});
|
|
}
|
|
#endif
|
|
|
|
void FAssetRegistryImpl::Shrink()
|
|
{
|
|
BackgroundResults.Shrink();
|
|
DeferredAssets.Shrink();
|
|
DeferredAssetsForGameThread.Shrink();
|
|
DeferredDependencies.Shrink();
|
|
DeferredDependenciesForGameThread.Shrink();
|
|
#if WITH_EDITOR
|
|
PackagesNeedingDependencyCalculation.Shrink();
|
|
PackagesNeedingDependencyCalculationOnGameThread.Shrink();
|
|
#endif
|
|
State.Shrink();
|
|
|
|
GatherDependencyDataScratchPad->Shrink();
|
|
}
|
|
|
|
void FAssetRegistryImpl::OnInitialSearchCompleted(Impl::FEventContext& EventContext)
|
|
{
|
|
// Handle any deferred loading operations
|
|
SetPerformanceMode(Impl::EPerformanceMode::MostlyStatic);
|
|
|
|
LogSearchDiagnostics(InitialSearchStartTime);
|
|
TRACE_END_REGION(TEXT("Asset Registry Scan"));
|
|
|
|
GlobalGatherer->UpdateCacheForSaving();
|
|
GlobalGatherer->OnInitialSearchCompleted();
|
|
Shrink();
|
|
|
|
EventContext.bFileLoadedEventBroadcast = true;
|
|
EventContext.bKnownGathersCompleteEventBroadcast = true;
|
|
|
|
bInitialSearchCompleted.store(true, std::memory_order_relaxed);
|
|
UpdateMaxSecondsPerFrame();
|
|
|
|
#if WITH_EDITOR
|
|
ResolveRedirectionsInCachedBPInheritanceMap();
|
|
#endif
|
|
}
|
|
|
|
void FAssetRegistryImpl::OnAdditionalMountSearchCompleted(Impl::FEventContext& EventContext)
|
|
{
|
|
// Handle any deferred loading operations
|
|
SetPerformanceMode(Impl::EPerformanceMode::MostlyStatic);
|
|
|
|
LogSearchDiagnostics(AdditionalMountSearchStartTime);
|
|
TRACE_END_REGION(TEXT("Asset Registry - Additional Mount Search")); // Matching TRACE_BEGIN_REGION in OnContentPathMounted
|
|
|
|
GlobalGatherer->UpdateCacheForSaving();
|
|
GlobalGatherer->OnAdditionalMountSearchCompleted();
|
|
Shrink();
|
|
|
|
EventContext.bKnownGathersCompleteEventBroadcast = true;
|
|
|
|
bAdditionalMountSearchInProgress.store(false, std::memory_order_relaxed);
|
|
}
|
|
|
|
void FAssetRegistryImpl::LogSearchDiagnostics(double StartTime)
|
|
{
|
|
FAssetGatherDiagnostics Diagnostics = GlobalGatherer->GetDiagnostics();
|
|
float Total = Diagnostics.DiscoveryTimeSeconds + Diagnostics.GatherTimeSeconds + StoreGatherResultsTimeSeconds;
|
|
UE::Telemetry::AssetRegistry::FGatherTelemetry Telemetry;
|
|
Telemetry.TotalSearchDurationSeconds = FPlatformTime::Seconds() - StartTime;
|
|
Telemetry.TotalWorkTimeSeconds = Total;
|
|
Telemetry.DiscoveryTimeSeconds = Diagnostics.DiscoveryTimeSeconds;
|
|
Telemetry.GatherTimeSeconds = Diagnostics.GatherTimeSeconds;
|
|
Telemetry.StoreTimeSeconds = StoreGatherResultsTimeSeconds;
|
|
Telemetry.NumCachedDirectories = Diagnostics.NumCachedDirectories;
|
|
Telemetry.NumUncachedDirectories = Diagnostics.NumUncachedDirectories;
|
|
Telemetry.NumCachedAssetFiles = Diagnostics.NumCachedAssetFiles;
|
|
Telemetry.NumUncachedAssetFiles = Diagnostics.NumUncachedAssetFiles;
|
|
FTelemetryRouter::Get().ProvideTelemetry(Telemetry);
|
|
#if !NO_LOGGING
|
|
TStringBuilder<256> Message;
|
|
Message.Appendf(TEXT("AssetRegistryGather time %.4fs: AssetDataDiscovery %0.4fs, AssetDataGather %0.4fs, StoreResults %0.4fs. Wall time %0.4fs.")
|
|
TEXT("\n\tNumCachedDirectories %d. NumUncachedDirectories %d. NumCachedFiles %d. NumUncachedFiles %d."),
|
|
Total, Diagnostics.DiscoveryTimeSeconds, Diagnostics.GatherTimeSeconds, StoreGatherResultsTimeSeconds,
|
|
Diagnostics.WallTimeSeconds, Diagnostics.NumCachedDirectories, Diagnostics.NumUncachedDirectories,
|
|
Diagnostics.NumCachedAssetFiles, Diagnostics.NumUncachedAssetFiles);
|
|
#if WITH_EDITOR
|
|
Message.Appendf(TEXT("\n\tBackgroundTickInterruptions %d."), BackgroundTickInterruptionsCount);
|
|
#endif // WITH_EDITOR
|
|
|
|
UE_LOG(LogAssetRegistry, Log, TEXT("%s"), *Message);
|
|
|
|
if (bVerboseLogging)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Verbose, TEXT("TagMemoryUse:"));
|
|
TagSizeByClass.ValueSort([](int64 A, int64 B) { return A > B; });
|
|
constexpr int64 MinSizeToLog = 1000 * 1000;
|
|
for (const TPair<FTopLevelAssetPath, int64>& Pair : TagSizeByClass)
|
|
{
|
|
if (Pair.Value < MinSizeToLog)
|
|
{
|
|
break;
|
|
}
|
|
UE_LOG(LogAssetRegistry, Verbose, TEXT("%s: %.1f MB"),
|
|
*Pair.Key.ToString(), (float)Pair.Value / (1000.f * 1000.f));
|
|
}
|
|
}
|
|
#endif // !NO_LOGGING
|
|
}
|
|
|
|
void FAssetRegistryImpl::TickGatherPackage(UE::AssetRegistry::FInterfaceWriteScopeLock& ScopeLock, Impl::FEventContext& EventContext, const FString& PackageName, const FString& LocalPath)
|
|
{
|
|
if (!GlobalGatherer.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool bIsInGameThread = IsInGameThread();
|
|
|
|
// Release the lock while performing expensive calls that don't need the lock
|
|
ScopeLock.Lock.WriteUnlock();
|
|
GlobalGatherer->WaitOnPath(LocalPath);
|
|
ScopeLock.Lock.WriteLock();
|
|
check(GlobalGatherer.IsValid());
|
|
|
|
double TimingStartTime = -1.;
|
|
auto LazyStartTimer = [&TimingStartTime]()
|
|
{
|
|
if (TimingStartTime <= 0.)
|
|
{
|
|
TimingStartTime = FPlatformTime::Seconds();
|
|
}
|
|
};
|
|
ON_SCOPE_EXIT
|
|
{
|
|
if (TimingStartTime > 0.)
|
|
{
|
|
StoreGatherResultsTimeSeconds += static_cast<float>(FPlatformTime::Seconds() - TimingStartTime);
|
|
TimingStartTime = -1.;
|
|
}
|
|
};
|
|
|
|
FName PackageFName(PackageName);
|
|
|
|
// Gather results from the background search
|
|
GlobalGatherer->GetPackageResults(BackgroundResults);
|
|
|
|
// The package could be in either the main or the ForGameThread containers but it will only appear in one or the other
|
|
// Either way, we put it into these two local containers and if we have to defer it, we'll put it into the game thread versions
|
|
TArray<TUniquePtr<FAssetData>*> PackageAssets;
|
|
TArray<FPackageDependencyData> PackageDependencyDatas;
|
|
BackgroundResults.Assets.MultiFindPointer(PackageFName, PackageAssets);
|
|
BackgroundResults.AssetsForGameThread.MultiFindPointer(PackageFName, PackageAssets);
|
|
// We can't remove the assets until we've finished the transfer into the PackageAssetsMap below
|
|
BackgroundResults.Dependencies.MultiFind(PackageFName, PackageDependencyDatas);
|
|
BackgroundResults.Dependencies.Remove(PackageFName);
|
|
BackgroundResults.DependenciesForGameThread.MultiFind(PackageFName, PackageDependencyDatas);
|
|
BackgroundResults.DependenciesForGameThread.Remove(PackageFName);
|
|
|
|
DeferredAssets.MultiFindPointer(PackageFName, PackageAssets);
|
|
DeferredAssetsForGameThread.MultiFindPointer(PackageFName, PackageAssets);
|
|
DeferredDependencies.MultiFind(PackageFName, PackageDependencyDatas);
|
|
DeferredDependencies.Remove(PackageFName);
|
|
DeferredDependenciesForGameThread.MultiFind(PackageFName, PackageDependencyDatas);
|
|
DeferredDependenciesForGameThread.Remove(PackageFName);
|
|
|
|
TOptional<TSet<FString>> MountPointsForVerifyAfterGather;
|
|
if (PackageAssets.Num() > 0)
|
|
{
|
|
LazyStartTimer();
|
|
TMultiMap<FName, TUniquePtr<FAssetData>> PackageAssetsMap;
|
|
PackageAssetsMap.Reserve(PackageAssets.Num());
|
|
for (TUniquePtr<FAssetData>* PackageAsset : PackageAssets)
|
|
{
|
|
PackageAssetsMap.Add(PackageFName, MoveTemp(*PackageAsset));
|
|
}
|
|
// Ownership transfer is now complete so remove these packages from the results arrays
|
|
BackgroundResults.Assets.Remove(PackageFName);
|
|
BackgroundResults.AssetsForGameThread.Remove(PackageFName);
|
|
DeferredAssets.Remove(PackageFName);
|
|
DeferredAssetsForGameThread.Remove(PackageFName);
|
|
|
|
TMultiMap<FName, TUniquePtr<FAssetData>> DeferredPackageAssetsMap;
|
|
UE::AssetRegistry::Impl::FInterruptionContext InterruptionContext;
|
|
AssetSearchDataGathered(EventContext, PackageAssetsMap, DeferredPackageAssetsMap,
|
|
InterruptionContext, MountPointsForVerifyAfterGather,
|
|
bIsInGameThread ? EDeferFlag::CanExecuteGameThread : EDeferFlag::RetryLater);
|
|
if (DeferredPackageAssetsMap.Num())
|
|
{
|
|
UE_LOG(LogAssetRegistry, Warning, TEXT("Attempted to add package '%s' to the registry before its UClass was available. \
|
|
Could not execute PostLoadAssetRegistryTags. We will try again later. Until then, dependency data will also be unavailable."),
|
|
*PackageName);
|
|
FDebug::DumpStackTraceToLog(ELogVerbosity::Warning);
|
|
DeferredAssetsForGameThread.Append(MoveTemp(DeferredPackageAssetsMap));
|
|
// If we are deferring this data we won't process the dependency data below anyway (we'll early out of DependencyDataGathered)
|
|
// so put the dependency data back into the BackgroundResults.DependenciesForGameThread which is where we will expect
|
|
// to find it when we reprocess the DeferredAssetsForGameThread after clearing the rest of the results queue.
|
|
for (FPackageDependencyData& Data : PackageDependencyDatas)
|
|
{
|
|
BackgroundResults.DependenciesForGameThread.Add(PackageFName, Data);
|
|
}
|
|
PackageDependencyDatas.Empty();
|
|
}
|
|
}
|
|
if (PackageDependencyDatas.Num() > 0)
|
|
{
|
|
LazyStartTimer();
|
|
TMultiMap<FName, FPackageDependencyData> PackageDependencyDatasMap;
|
|
PackageDependencyDatasMap.Reserve(PackageDependencyDatas.Num());
|
|
for (FPackageDependencyData& DependencyData : PackageDependencyDatas)
|
|
{
|
|
PackageDependencyDatasMap.Add(PackageFName, MoveTemp(DependencyData));
|
|
}
|
|
TSet<FName>* OutPackagesNeedingDependencyCalculation = nullptr;
|
|
#if WITH_EDITOR
|
|
OutPackagesNeedingDependencyCalculation = &PackagesNeedingDependencyCalculation;
|
|
#endif
|
|
UE::AssetRegistry::Impl::FInterruptionContext InterruptionContext;
|
|
DependencyDataGathered(PackageDependencyDatasMap, DeferredDependenciesForGameThread,
|
|
OutPackagesNeedingDependencyCalculation, InterruptionContext, MountPointsForVerifyAfterGather);
|
|
}
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
void FAssetRegistryImpl::LoadCalculatedDependencies(TArray<FName>* AssetPackageNamesToCalculate,
|
|
Impl::FClassInheritanceContext& InheritanceContext, TSet<FName>* InPackagesNeedingDependencyCalculation,
|
|
Impl::FInterruptionContext& InOutInterruptionContext)
|
|
{
|
|
auto CheckForTimeUp = [&InOutInterruptionContext](bool bHadActivity)
|
|
{
|
|
// Only Check TimeUp when we found something to do, otherwise we waste time calling FPlatformTime::Seconds
|
|
if (!bHadActivity)
|
|
{
|
|
return false;
|
|
}
|
|
return InOutInterruptionContext.ShouldExitEarly();
|
|
};
|
|
|
|
RebuildAssetDependencyGathererMapIfNeeded();
|
|
|
|
if (AssetPackageNamesToCalculate)
|
|
{
|
|
for (FName PackageName : *AssetPackageNamesToCalculate)
|
|
{
|
|
// We do not remove the package from InPackagesNeedingDependencyCalculation, because
|
|
// we are only calculating an interim result when AssetsToCalculate is non-null
|
|
// We will run again on each of these PackageNames when TickGatherer finishes gathering all dependencies
|
|
if (InPackagesNeedingDependencyCalculation->Contains(PackageName))
|
|
{
|
|
bool bHadActivity;
|
|
LoadCalculatedDependencies(PackageName, InheritanceContext, bHadActivity);
|
|
if (CheckForTimeUp(bHadActivity))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (TSet<FName>::TIterator It = InPackagesNeedingDependencyCalculation->CreateIterator(); It; ++It)
|
|
{
|
|
bool bHadActivity;
|
|
LoadCalculatedDependencies(*It, InheritanceContext, bHadActivity);
|
|
It.RemoveCurrent();
|
|
if (CheckForTimeUp(bHadActivity))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
check(InPackagesNeedingDependencyCalculation->IsEmpty());
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::LoadCalculatedDependencies(FName PackageName,
|
|
Impl::FClassInheritanceContext& InheritanceContext, bool& bOutHadActivity)
|
|
{
|
|
bOutHadActivity = false;
|
|
|
|
auto GetCompiledFilter = [this, &InheritanceContext](const FARFilter& InFilter) -> FARCompiledFilter
|
|
{
|
|
FARCompiledFilter CompiledFilter;
|
|
CompileFilter(InheritanceContext, InFilter, CompiledFilter);
|
|
return CompiledFilter;
|
|
};
|
|
|
|
FReadScopeLock GathererClassScopeLock(RegisteredDependencyGathererClassesLock);
|
|
|
|
TArray<UE::AssetDependencyGatherer::Private::FRegisteredAssetDependencyGatherer*, TInlineAllocator<2>> Gatherers;
|
|
TArray<FString> DirectoryReferences;
|
|
State.EnumerateAssetsByPackageName(PackageName,
|
|
[this, &Gatherers, &GetCompiledFilter, &bOutHadActivity, PackageName, &DirectoryReferences](const FAssetData* AssetData)
|
|
{
|
|
Gatherers.Reset();
|
|
|
|
// Check the class name instead of trying to load the actual class as that is slow
|
|
// This code could be moved somewhere where it doesn't need to re-query the asset data, but it needs to happen after both dependencies and data are handled
|
|
RegisteredDependencyGathererClasses.MultiFind(AssetData->AssetClassPath, Gatherers);
|
|
|
|
TArray<IAssetDependencyGatherer::FGathereredDependency> GatheredDependencies;
|
|
TArray<FString> GathererDirectoryReferences;
|
|
|
|
IAssetDependencyGatherer::FGatherDependenciesContext Context { *AssetData, State, GetCachedPathTree(), GetCompiledFilter, GatheredDependencies, GathererDirectoryReferences};
|
|
|
|
for (UE::AssetDependencyGatherer::Private::FRegisteredAssetDependencyGatherer* Gatherer : Gatherers)
|
|
{
|
|
bOutHadActivity = true;
|
|
GatheredDependencies.Reset();
|
|
GathererDirectoryReferences.Reset();
|
|
|
|
Gatherer->GatherDependencies(Context);
|
|
|
|
if (GatheredDependencies.Num())
|
|
{
|
|
FDependsNode* SourceNode = State.CreateOrFindDependsNode(FAssetIdentifier(PackageName));
|
|
bool bWasSourceDependencyListSorted = SourceNode->IsDependencyListSorted(EDependencyCategory::Package);
|
|
SourceNode->SetIsDependencyListSorted(EDependencyCategory::Package, false);
|
|
|
|
for (const IAssetDependencyGatherer::FGathereredDependency& GatheredDepencency : GatheredDependencies) //-V1078
|
|
{
|
|
FDependsNode* TargetNode = State.CreateOrFindDependsNode(FAssetIdentifier(GatheredDepencency.PackageName));
|
|
EDependencyProperty DependencyProperties = GatheredDepencency.Property;
|
|
SourceNode->AddDependency(TargetNode, EDependencyCategory::Package, DependencyProperties);
|
|
TargetNode->AddReferencer(SourceNode);
|
|
}
|
|
|
|
SourceNode->SetIsDependencyListSorted(EDependencyCategory::Package, bWasSourceDependencyListSorted);
|
|
}
|
|
|
|
DirectoryReferences.Append(MoveTemp(GathererDirectoryReferences));
|
|
}
|
|
|
|
return true; // Keep iterating the assets in the package
|
|
});
|
|
SetDirectoriesWatchedByPackage(PackageName, MoveTemp(DirectoryReferences));
|
|
}
|
|
|
|
void FAssetRegistryImpl::SetDirectoriesWatchedByPackage(FName PackageName, TConstArrayView<FString> Directories)
|
|
{
|
|
TArray<FString> DirectoryLocalPaths;
|
|
DirectoryLocalPaths.Reserve(Directories.Num());
|
|
for (const FString& DirectoryLocalPathOrLongPackageName : Directories)
|
|
{
|
|
FString DirectoryLocalPath;
|
|
if (!FPackageName::TryConvertLongPackageNameToFilename(DirectoryLocalPathOrLongPackageName, DirectoryLocalPath))
|
|
{
|
|
UE_LOG(LogAssetRegistry, Warning,
|
|
TEXT("AddDirectoryReferencer called with LongPackageName %s that cannot be mapped to a local path. Ignoring it."),
|
|
*DirectoryLocalPathOrLongPackageName);
|
|
}
|
|
DirectoryLocalPath = UE::AssetRegistry::CreateStandardFilename(DirectoryLocalPath);
|
|
DirectoryLocalPaths.Add(MoveTemp(DirectoryLocalPath));
|
|
}
|
|
|
|
uint32 PackageNameHash = GetTypeHash(PackageName);
|
|
TArray<FString>* WatchedDirectories = DirectoriesWatchedByPackage.FindByHash(PackageNameHash, PackageName);
|
|
|
|
// Remove all old values from the reverse map
|
|
if (WatchedDirectories)
|
|
{
|
|
for (const FString& WatchedDirectory : *WatchedDirectories)
|
|
{
|
|
TSet<FName>* DirectoryWatchers = PackagesWatchingDirectory.Find(WatchedDirectory);
|
|
if (DirectoryWatchers)
|
|
{
|
|
DirectoryWatchers->RemoveByHash(PackageNameHash, PackageName);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Directories.IsEmpty())
|
|
{
|
|
if (WatchedDirectories)
|
|
{
|
|
DirectoriesWatchedByPackage.RemoveByHash(PackageNameHash, PackageName);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!WatchedDirectories)
|
|
{
|
|
WatchedDirectories = &DirectoriesWatchedByPackage.FindOrAddByHash(PackageNameHash, PackageName);
|
|
}
|
|
*WatchedDirectories = MoveTemp(DirectoryLocalPaths);
|
|
|
|
for (const FString& WatchedDirectory : *WatchedDirectories)
|
|
{
|
|
TSet<FName>& DirectoryWatchers = PackagesWatchingDirectory.FindOrAdd(WatchedDirectory);
|
|
DirectoryWatchers.AddByHash(PackageNameHash, PackageName);
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::Serialize(FArchive& Ar)
|
|
{
|
|
if (Ar.IsObjectReferenceCollector())
|
|
{
|
|
// The Asset Registry does not have any object references, and its serialization function is expensive
|
|
return;
|
|
}
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.Serialize(Ar, EventContext);
|
|
}
|
|
Broadcast(EventContext);
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::Serialize(FArchive& Ar, Impl::FEventContext& EventContext)
|
|
{
|
|
check(!Ar.IsObjectReferenceCollector()); // Caller should not call in this case
|
|
if (Ar.IsLoading())
|
|
{
|
|
State.Load(Ar);
|
|
CachePathsFromState(EventContext, State);
|
|
UpdatePersistentMountPoints();
|
|
}
|
|
else if (Ar.IsSaving())
|
|
{
|
|
State.Save(Ar, SerializationOptions);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/** Append the assets from the incoming state into our own */
|
|
void UAssetRegistryImpl::AppendState(const FAssetRegistryState& InState, UE::AssetRegistry::EAppendMode AppendMode)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE_STR("UAssetRegistryImpl::AppendState");
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.AppendState(EventContext, InState, AppendMode, /*bEmitAssetEvents*/true);
|
|
}
|
|
Broadcast(EventContext);
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::AppendState(Impl::FEventContext& EventContext, const FAssetRegistryState& InState,
|
|
FAssetRegistryState::EInitializationMode Mode, bool bEmitAssetEvents)
|
|
{
|
|
FAssetRegistryAppendResult LocalAppendResult;
|
|
FAssetRegistryAppendResult* AppendResultPtr = bEmitAssetEvents ? &LocalAppendResult : nullptr;
|
|
|
|
#if WITH_EDITOR
|
|
const FAssetRegistrySerializationOptions& Options = DevelopmentSerializationOptions;
|
|
//Development options have all filters disabled, so there is no need to resolve runtime class filter options
|
|
#else
|
|
FAssetRegistrySerializationOptions& Options = SerializationOptions;
|
|
const bool bSuppressWarnings = !UE::AssetRegistry::Utils::HasEngineModuleLoaded();
|
|
Utils::UpdateSerializationOptions(Options, CachedParsedFilterRules, bSuppressWarnings);
|
|
#endif
|
|
State.InitializeFromExisting(
|
|
InState,
|
|
Options,
|
|
Mode,
|
|
AppendResultPtr);
|
|
|
|
CachePathsFromState(EventContext, InState);
|
|
CachedPathTree.Shrink();
|
|
|
|
if (AppendResultPtr)
|
|
{
|
|
for (const FAssetData* AssetData : AppendResultPtr->AddedAssets)
|
|
{
|
|
EventContext.AssetEvents.Emplace(*AssetData, UE::AssetRegistry::Impl::FEventContext::EEvent::Added);
|
|
}
|
|
for (const FAssetData* AssetData : AppendResultPtr->UpdatedAssets)
|
|
{
|
|
EventContext.AssetEvents.Emplace(*AssetData, UE::AssetRegistry::Impl::FEventContext::EEvent::Updated);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::CachePathsFromState(Impl::FEventContext& EventContext, const FAssetRegistryState& InState)
|
|
{
|
|
SCOPED_BOOT_TIMING("FAssetRegistryImpl::CachePathsFromState");
|
|
|
|
// Refreshes ClassGeneratorNames if out of date due to module load
|
|
CollectCodeGeneratorClasses();
|
|
|
|
#if WITH_EDITOR
|
|
const FAssetRegistrySerializationOptions& Options = DevelopmentSerializationOptions;
|
|
#else
|
|
const FAssetRegistrySerializationOptions& Options = SerializationOptions;
|
|
#endif
|
|
|
|
// Add paths to cache
|
|
InState.EnumerateAllAssets([this, &Options, &EventContext](const FAssetData& AssetData)
|
|
{
|
|
if (Options.bFilterAssetDataWithNoTags && AssetData.TagsAndValues.Num() == 0 &&
|
|
!FPackageName::IsLocalizedPackage(WriteToString<256>(AssetData.PackageName)))
|
|
{
|
|
// based on current serialization settings this should be skipped.
|
|
return;
|
|
}
|
|
|
|
AddAssetPath(EventContext, AssetData.PackagePath);
|
|
|
|
// Populate the class map if adding blueprint
|
|
if (ClassGeneratorNames.Contains(AssetData.AssetClassPath))
|
|
{
|
|
FAssetRegistryExportPath GeneratedClass = AssetData.GetTagValueRef<FAssetRegistryExportPath>(FBlueprintTags::GeneratedClassPath);
|
|
FAssetRegistryExportPath ParentClass = AssetData.GetTagValueRef<FAssetRegistryExportPath>(FBlueprintTags::ParentClassPath);
|
|
|
|
if (GeneratedClass && ParentClass)
|
|
{
|
|
AddCachedBPClassParent(GeneratedClass.ToTopLevelAssetPath(), ParentClass.ToTopLevelAssetPath());
|
|
|
|
// Invalidate caching because CachedBPInheritanceMap got modified
|
|
TempCachedInheritanceBuffer.bDirty = true;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
SIZE_T UAssetRegistryImpl::GetAllocatedSize(bool bLogDetailed) const
|
|
{
|
|
SIZE_T StateSize = 0;
|
|
SIZE_T StaticSize = 0;
|
|
SIZE_T SearchSize = 0;
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.GetAllocatedSize(bLogDetailed, StateSize, StaticSize, SearchSize);
|
|
StaticSize += sizeof(UAssetRegistryImpl);
|
|
#if WITH_EDITOR
|
|
StaticSize += OnDirectoryChangedDelegateHandles.GetAllocatedSize();
|
|
#endif
|
|
}
|
|
|
|
if (bLogDetailed)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Log, TEXT("AssetRegistry Static Size: %" SIZE_T_FMT "k"), StaticSize / 1024);
|
|
UE_LOG(LogAssetRegistry, Log, TEXT("AssetRegistry Search Size: %" SIZE_T_FMT "k"), SearchSize / 1024);
|
|
}
|
|
|
|
return StateSize + StaticSize + SearchSize;
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::GetAllocatedSize(bool bLogDetailed, SIZE_T& StateSize, SIZE_T& StaticSize, SIZE_T& SearchSize) const
|
|
{
|
|
StateSize = State.GetAllocatedSize(bLogDetailed);
|
|
|
|
if (bLogDetailed)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Log, TEXT("AssetRegistry CachedEmptyPackages Size: %" SIZE_T_FMT "k"), CachedEmptyPackages.GetAllocatedSize() / 1024);
|
|
UE_LOG(LogAssetRegistry, Log, TEXT("AssetRegistry CachedBPInheritanceMap Size: %" SIZE_T_FMT "k"), CachedBPInheritanceMap.GetAllocatedSize() / 1024);
|
|
UE_LOG(LogAssetRegistry, Log, TEXT("AssetRegistry ClassGeneratorNames Size: %" SIZE_T_FMT "k"), ClassGeneratorNames.GetAllocatedSize() / 1024);
|
|
UE_LOG(LogAssetRegistry, Log, TEXT("AssetRegistry BackgroundResults Size: %" SIZE_T_FMT "k"), BackgroundResults.GetAllocatedSize() / 1024);
|
|
UE_LOG(LogAssetRegistry, Log, TEXT("AssetRegistry CachedPathTree Size: %" SIZE_T_FMT "k"), CachedPathTree.GetAllocatedSize() / 1024);
|
|
UE_LOG(LogAssetRegistry, Log, TEXT("AssetRegistry CachedParsedFilterRules Size: %" SIZE_T_FMT "k"), CachedParsedFilterRules.GetAllocatedSize() / 1024);
|
|
}
|
|
StaticSize = CachedEmptyPackages.GetAllocatedSize() + CachedBPInheritanceMap.GetAllocatedSize() + ClassGeneratorNames.GetAllocatedSize();
|
|
SearchSize = BackgroundResults.GetAllocatedSize() + CachedPathTree.GetAllocatedSize() + CachedParsedFilterRules.GetAllocatedSize();
|
|
|
|
if (bIsTempCachingEnabled && !bIsTempCachingAlwaysEnabled)
|
|
{
|
|
SIZE_T TempCacheMem = TempCachedInheritanceBuffer.GetAllocatedSize();
|
|
StaticSize += TempCacheMem;
|
|
UE_LOG(LogAssetRegistry, Warning, TEXT("Asset Registry Temp caching enabled, wasting memory: %lldk"), TempCacheMem / 1024);
|
|
}
|
|
|
|
if (GlobalGatherer.IsValid())
|
|
{
|
|
SearchSize += sizeof(*GlobalGatherer);
|
|
SearchSize += GlobalGatherer->GetAllocatedSize();
|
|
|
|
if (bLogDetailed)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Log, TEXT("AssetRegistry GlobalGatherer Size: %" SIZE_T_FMT "k"), GlobalGatherer->GetAllocatedSize() / 1024);
|
|
}
|
|
}
|
|
|
|
StaticSize += SerializationOptions.CookFilterlistTagsByClass.GetAllocatedSize();
|
|
for (const TPair<FTopLevelAssetPath, TSet<FName>>& Pair : SerializationOptions.CookFilterlistTagsByClass)
|
|
{
|
|
StaticSize += Pair.Value.GetAllocatedSize();
|
|
}
|
|
StaticSize += SerializationOptions.AssetBundlesDenyList.GetAllocatedSize();
|
|
for (const FString& Filter : SerializationOptions.AssetBundlesDenyList)
|
|
{
|
|
StaticSize += Filter.GetAllocatedSize();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::LoadPackageRegistryData(FArchive& Ar, FLoadPackageRegistryData& InOutData) const
|
|
{
|
|
FPackageReader Reader;
|
|
if (Reader.OpenPackageFile(&Ar))
|
|
{
|
|
UE::AssetRegistry::Utils::ReadAssetFile(Reader, InOutData);
|
|
}
|
|
}
|
|
|
|
void UAssetRegistryImpl::LoadPackageRegistryData(const FString& PackageFilename, FLoadPackageRegistryData& InOutData) const
|
|
{
|
|
FPackageReader Reader;
|
|
if (Reader.OpenPackageFile(PackageFilename))
|
|
{
|
|
UE::AssetRegistry::Utils::ReadAssetFile(Reader, InOutData);
|
|
}
|
|
}
|
|
|
|
namespace UE::AssetRegistry::Utils
|
|
{
|
|
|
|
bool ReadAssetFile(FPackageReader& PackageReader, IAssetRegistry::FLoadPackageRegistryData& InOutData)
|
|
{
|
|
TArray<FAssetData*> AssetDataList;
|
|
TArray<FString> CookedPackageNamesWithoutAssetDataGathered;
|
|
|
|
const bool bGetDependencies = (InOutData.bGetDependencies);
|
|
FPackageDependencyData DependencyData;
|
|
|
|
bool bReadOk = FAssetDataGatherer::ReadAssetFile(PackageReader, AssetDataList, DependencyData,
|
|
CookedPackageNamesWithoutAssetDataGathered,
|
|
InOutData.bGetDependencies ? FPackageReader::EReadOptions::Dependencies : FPackageReader::EReadOptions::None);
|
|
|
|
if (bReadOk)
|
|
{
|
|
// Copy & free asset data to the InOutData
|
|
InOutData.Data.Reset(AssetDataList.Num());
|
|
for (FAssetData* AssetData : AssetDataList)
|
|
{
|
|
InOutData.Data.Emplace(*AssetData);
|
|
}
|
|
|
|
AssetDataList.Reset();
|
|
|
|
if (InOutData.bGetDependencies)
|
|
{
|
|
InOutData.DataDependencies.Reset(DependencyData.PackageDependencies.Num());
|
|
for (FPackageDependencyData::FPackageDependency& Dependency : DependencyData.PackageDependencies)
|
|
{
|
|
InOutData.DataDependencies.Emplace(Dependency.PackageName);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cleanup the allocated asset data
|
|
for (FAssetData* AssetData : AssetDataList)
|
|
{
|
|
delete AssetData;
|
|
}
|
|
|
|
return bReadOk;
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::InitializeTemporaryAssetRegistryState(FAssetRegistryState& OutState,
|
|
const FAssetRegistrySerializationOptions& Options, bool bRefreshExisting,
|
|
const TSet<FName>& RequiredPackages, const TSet<FName>& RemovePackages) const
|
|
{
|
|
using FAssetDataMap = UE::AssetRegistry::Private::FAssetDataMap;
|
|
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
const FAssetRegistryState& State = GuardedData.GetState();
|
|
if (!RequiredPackages.IsEmpty() || !RemovePackages.IsEmpty())
|
|
{
|
|
if (bRefreshExisting)
|
|
{
|
|
// InitializeFromExistingAndPrune does not support EInitializationMode so we have to Initialize and then Prune
|
|
OutState.InitializeFromExisting(State.CachedAssets, State.CachedDependsNodes, State.CachedPackageData, Options,
|
|
FAssetRegistryState::EInitializationMode::OnlyUpdateExisting);
|
|
OutState.PruneAssetData(RequiredPackages, RemovePackages, Options);
|
|
}
|
|
else
|
|
{
|
|
TSet<int32> UnusedChunksToKeep;
|
|
OutState.InitializeFromExistingAndPrune(State, RequiredPackages, RemovePackages, UnusedChunksToKeep, Options);
|
|
OutState.Shrink();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OutState.InitializeFromExisting(State.CachedAssets, State.CachedDependsNodes, State.CachedPackageData, Options,
|
|
bRefreshExisting ? FAssetRegistryState::EInitializationMode::OnlyUpdateExisting : FAssetRegistryState::EInitializationMode::Rebuild);
|
|
}
|
|
}
|
|
|
|
#if ASSET_REGISTRY_STATE_DUMPING_ENABLED
|
|
void UAssetRegistryImpl::DumpState(const TArray<FString>& Arguments, TArray<FString>& OutPages, int32 LinesPerPage) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.GetState().Dump(Arguments, OutPages, LinesPerPage);
|
|
}
|
|
#endif
|
|
|
|
TSet<FName> UAssetRegistryImpl::GetCachedEmptyPackagesCopy() const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
return GuardedData.GetCachedEmptyPackages();
|
|
}
|
|
|
|
bool UAssetRegistryImpl::ContainsTag(FName TagName) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
return GuardedData.GetState().ContainsTag(TagName);
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
namespace Impl
|
|
{
|
|
|
|
FScanPathContext::FScanPathContext(FEventContext& InEventContext, FClassInheritanceContext& InInheritanceContext,
|
|
const TArray<FString>& InDirs, const TArray<FString>& InFiles, UE::AssetRegistry::EScanFlags InScanFlags,
|
|
TArray<FSoftObjectPath>* FoundAssets)
|
|
: EventContext(InEventContext)
|
|
, InheritanceContext(InInheritanceContext)
|
|
, OutFoundAssets(FoundAssets)
|
|
, bForceRescan(!!(InScanFlags & UE::AssetRegistry::EScanFlags::ForceRescan))
|
|
, bIgnoreDenyListScanFilters(!!(InScanFlags & UE::AssetRegistry::EScanFlags::IgnoreDenyListScanFilters))
|
|
, bIgnoreInvalidPathWarning(!!(InScanFlags & UE::AssetRegistry::EScanFlags::IgnoreInvalidPathWarning))
|
|
{
|
|
if (OutFoundAssets)
|
|
{
|
|
OutFoundAssets->Empty();
|
|
}
|
|
|
|
bool bLogCallstack = false;
|
|
ON_SCOPE_EXIT
|
|
{
|
|
if (bLogCallstack)
|
|
{
|
|
FDebug::DumpStackTraceToLog(ELogVerbosity::Warning);
|
|
}
|
|
};
|
|
if (bIgnoreDenyListScanFilters && !bForceRescan)
|
|
{
|
|
// This restriction is necessary because we have not yet implemented some of the required behavior to handle bIgnoreDenyListScanFilters without bForceRescan;
|
|
// For skipping of directories that we have already scanned, we would have to check whether the directory has been set to be monitored with the proper flag (ignore deny list or not)
|
|
// rather than just checking whether it has been set to be monitored at all
|
|
UE_LOG(LogAssetRegistry, Warning, TEXT("ScanPathsSynchronous: bIgnoreDenyListScanFilters==true is only valid when bForceRescan==true. Setting bForceRescan=true."));
|
|
bForceRescan = true;
|
|
bLogCallstack = true;
|
|
}
|
|
|
|
FString LocalPath;
|
|
FString PackageName;
|
|
FString Extension;
|
|
FPackageName::EFlexNameType FlexNameType;
|
|
LocalFiles.Reserve(InFiles.Num());
|
|
PackageFiles.Reserve(InFiles.Num());
|
|
for (const FString& InFile : InFiles)
|
|
{
|
|
if (InFile.IsEmpty())
|
|
{
|
|
continue;
|
|
}
|
|
else if (!FPackageName::TryConvertToMountedPath(InFile, &LocalPath, &PackageName, nullptr, nullptr, &Extension, &FlexNameType))
|
|
{
|
|
if (!bIgnoreInvalidPathWarning)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Warning, TEXT("ScanPathsSynchronous: %s is not in a mounted path, will not scan."), *InFile);
|
|
bLogCallstack = true;
|
|
}
|
|
continue;
|
|
}
|
|
if (FPackageName::IsTempPackage(PackageName))
|
|
{
|
|
if (!bIgnoreInvalidPathWarning)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Warning, TEXT("ScanPathsSynchronous: %s is in the /Temp path, will not scan."), *InFile);
|
|
bLogCallstack = true;
|
|
}
|
|
continue;
|
|
}
|
|
if (Extension.IsEmpty())
|
|
{
|
|
// The empty extension is not a valid Package extension; it might exist, but we will pay the price to check it
|
|
if (!IFileManager::Get().FileExists(*LocalPath))
|
|
{
|
|
// Find the extension
|
|
// Note, the 'internal' version of DoesPackageExist must be used to avoid re-entering the AssetRegistry's lock resulting in deadlock
|
|
FPackagePath PackagePath = FPackagePath::FromLocalPath(LocalPath);
|
|
if (FPackageName::InternalDoesPackageExistEx(PackagePath, FPackageName::EPackageLocationFilter::Any,
|
|
false /* bMatchCaseOnDisk */, &PackagePath) == FPackageName::EPackageLocationFilter::None)
|
|
{
|
|
// Requesting to scan a non-existent package is not a condition we need to warn about, because it rarely indicates an error,
|
|
// and is often used to check whether a package exists in the state before the scan has finished. Silently ignore it,
|
|
// even if !bIgnoreInvalidPathWarning.
|
|
continue;
|
|
}
|
|
Extension = PackagePath.GetExtensionString(EPackageSegment::Header);
|
|
}
|
|
}
|
|
LocalFiles.Add(LocalPath + Extension);
|
|
PackageFiles.Add(PackageName);
|
|
}
|
|
LocalDirs.Reserve(InDirs.Num());
|
|
PackageDirs.Reserve(InDirs.Num());
|
|
for (const FString& InDir : InDirs)
|
|
{
|
|
if (InDir.IsEmpty())
|
|
{
|
|
continue;
|
|
}
|
|
else if (!FPackageName::TryConvertToMountedPath(InDir, &LocalPath, &PackageName, nullptr, nullptr, &Extension, &FlexNameType))
|
|
{
|
|
if (!bIgnoreInvalidPathWarning)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Warning, TEXT("ScanPathsSynchronous: %s is not in a mounted path, will not scan."), *InDir);
|
|
bLogCallstack = true;
|
|
}
|
|
continue;
|
|
}
|
|
if (FPackageName::IsTempPackage(PackageName))
|
|
{
|
|
if (!bIgnoreInvalidPathWarning)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Warning, TEXT("ScanPathsSynchronous: %s is in the /Temp path, will not scan."), *InDir);
|
|
bLogCallstack = true;
|
|
}
|
|
continue;
|
|
}
|
|
LocalDirs.Add(LocalPath + Extension);
|
|
PackageDirs.Add(PackageName + Extension);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void FAssetRegistryImpl::ScanPathsSynchronous(FInterfaceWriteScopeLock* ScopeLock, Impl::FScanPathContext& Context)
|
|
{
|
|
// We always expect a valid lock to be provided except before initialization is complete.
|
|
check(ScopeLock || !bInitializationComplete);
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
|
|
if (!TryConstructGathererIfNeeded())
|
|
{
|
|
return;
|
|
}
|
|
FAssetDataGatherer& Gatherer = *GlobalGatherer;
|
|
|
|
Context.LocalPaths.Reserve(Context.LocalFiles.Num() + Context.LocalDirs.Num());
|
|
Context.LocalPaths.Append(MoveTemp(Context.LocalDirs));
|
|
Context.LocalPaths.Append(MoveTemp(Context.LocalFiles));
|
|
if (Context.LocalPaths.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
Gatherer.AddRequiredMountPoints(Context.LocalPaths);
|
|
|
|
auto IsInRequestedDir = [&Context](const FAssetData& AssetData)
|
|
{
|
|
TStringBuilder<128> PackageNameStr;
|
|
AssetData.PackageName.ToString(PackageNameStr);
|
|
FStringView PackageName(PackageNameStr.ToString(), PackageNameStr.Len());
|
|
|
|
// First see if the asset is in a requested directory
|
|
for (const FString& RequestedPackageDir : Context.PackageDirs)
|
|
{
|
|
if (FPathViews::IsParentPathOf(RequestedPackageDir, PackageName))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// If not found above, check if the asset was requested specifically
|
|
for (const FString& RequestedPackageFile : Context.PackageFiles)
|
|
{
|
|
if (PackageName.Equals(RequestedPackageFile, ESearchCase::IgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
TSet<FSoftObjectPath> GatheredAssets;
|
|
TSet<FName> GatheredVerseFiles;
|
|
FDelegateHandle GatheredResultsHandle;
|
|
if (Context.bForceRescan)
|
|
{
|
|
GatheredResultsHandle = Gatherer.GetGatheredResultsEvent().AddLambda(
|
|
[&GatheredAssets, &GatheredVerseFiles, &IsInRequestedDir, OutFoundAssets = Context.OutFoundAssets](const UE::AssetDataGather::FResults& Results)
|
|
{
|
|
auto AddGatheredAssets = [&IsInRequestedDir, OutFoundAssets](TSet<FSoftObjectPath>& InOutGatheredAssets, const TMultiMap<FName, TUniquePtr<FAssetData>>& AssetMap)
|
|
{
|
|
for (TMultiMap<FName, TUniquePtr<FAssetData>>::TConstIterator It = AssetMap.CreateConstIterator(); It; ++It)
|
|
{
|
|
const FAssetData* AssetData = It.Value().Get();
|
|
if (OutFoundAssets)
|
|
{
|
|
// The gatherer may have added other assets that were scanned as part of the ongoing background scan,
|
|
// so remove any assets that were not in the requested paths
|
|
if (IsInRequestedDir(*AssetData))
|
|
{
|
|
UE_LOG(LogAssetRegistry, VeryVerbose, TEXT("FAssetRegistryImpl::ScanPathsSynchronous: Found Asset: %s"),
|
|
*AssetData->GetObjectPathString());
|
|
OutFoundAssets->Add(AssetData->GetSoftObjectPath());
|
|
}
|
|
}
|
|
|
|
InOutGatheredAssets.Add(AssetData->GetSoftObjectPath());
|
|
}
|
|
};
|
|
AddGatheredAssets(GatheredAssets, Results.Assets);
|
|
AddGatheredAssets(GatheredAssets, Results.AssetsForGameThread);
|
|
|
|
for (FName VerseFile : Results.VerseFiles)
|
|
{
|
|
GatheredVerseFiles.Add(VerseFile);
|
|
}
|
|
});
|
|
}
|
|
ON_SCOPE_EXIT
|
|
{
|
|
if (Context.bForceRescan)
|
|
{
|
|
Gatherer.GetGatheredResultsEvent().Remove(GatheredResultsHandle);
|
|
}
|
|
};
|
|
|
|
// Conditionally release the lock while performing expensive calls that don't need the lock.
|
|
// If no lock was passed we assume the caller does not want us to unlock.
|
|
if (ScopeLock)
|
|
{
|
|
ScopeLock->Lock.WriteUnlock();
|
|
}
|
|
Gatherer.ScanPathsSynchronous(Context.LocalPaths, Context.bForceRescan, Context.bIgnoreDenyListScanFilters);
|
|
if (ScopeLock)
|
|
{
|
|
ScopeLock->Lock.WriteLock();
|
|
Context.InheritanceContext.OnLockReentered(*this);
|
|
}
|
|
|
|
// If we are forcing a rescan, then delete any old assets that no longer exist. If we are not forcing a rescan,
|
|
// then there should not be any old assets that no longer exist, so we skip the cost of searching for them.
|
|
TSet<FSoftObjectPath> ExistingAssets;
|
|
TSet<FName> ExistingVerseFiles;
|
|
if (Context.bForceRescan)
|
|
{
|
|
// Initialize ExistingAssets to the list of all assets in the given paths.
|
|
if (!Context.PackageDirs.IsEmpty())
|
|
{
|
|
FARFilter Filter;
|
|
Filter.bIncludeOnlyOnDiskAssets = true;
|
|
Filter.bRecursivePaths = true;
|
|
for (const FString& PackageDir : Context.PackageDirs)
|
|
{
|
|
Filter.PackagePaths.Add(FName(*PackageDir));
|
|
}
|
|
FARCompiledFilter CompiledFilter;
|
|
CompileFilter(Context.InheritanceContext, Filter, CompiledFilter);
|
|
TArray<FAssetData> AssetsInPaths;
|
|
State.EnumerateAssets(CompiledFilter, TSet<FName>() /* PackageNamesToSkip */,
|
|
[&ExistingAssets](const FAssetData& AssetData)
|
|
{
|
|
ExistingAssets.Add(AssetData.ToSoftObjectPath());
|
|
return true;
|
|
}, UE::AssetRegistry::EEnumerateAssetsFlags::AllowUnfilteredArAssets);
|
|
for (FName PackagePath : CompiledFilter.PackagePaths)
|
|
{
|
|
TArray<FName>* VerseFiles = CachedVerseFilesByPath.Find(PackagePath);
|
|
if (VerseFiles)
|
|
{
|
|
ExistingVerseFiles.Append(*VerseFiles);
|
|
}
|
|
}
|
|
}
|
|
for (const FString& PackageName : Context.PackageFiles)
|
|
{
|
|
State.EnumerateAssetsByPackageName(FName(*PackageName),
|
|
[&ExistingAssets](const FAssetData* AssetData)
|
|
{
|
|
ExistingAssets.Add(AssetData->ToSoftObjectPath());
|
|
return true;
|
|
});
|
|
for (const TCHAR* Extension : FAssetDataGatherer::GetVerseFileExtensions())
|
|
{
|
|
FName VerseName(*WriteToString<256>(PackageName, Extension), FNAME_Find);
|
|
if (!VerseName.IsNone() && CachedVerseFiles.Contains(VerseName))
|
|
{
|
|
ExistingVerseFiles.Add(VerseName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Impl::FTickContext TickContext(*this, Context.EventContext, Context.InheritanceContext);
|
|
Context.Status = TickGatherer(TickContext);
|
|
|
|
// Temporary hack/partial solution. The expectation is that this function will return cause all assets
|
|
// under the specified directories to be ingested into the registry. However, one of the early steps
|
|
// in ingestion is an attempt to PostLoadAssetRegistryTags. This step requires that we already have loaded
|
|
// the AssetClass UClass for an asset. That may not have happened yet. In the past, we would just have skipped
|
|
// over that step and continued, but now we defer the asset for processing at a later time. However, that means
|
|
// that after running TickGatherer, even without timeslicing, our end state might be that only some assets have
|
|
// been scanned and others have been deferred and so would be unavailable to subsequent queries. Ideally we would
|
|
// solve this by loading the classes that these assets depend on. Instead, we are deferring that task and for now
|
|
// we manually identify any deferred assets that fall under the paths we are scanning and ask the asset registry
|
|
// to process them ignoring any failures of TryPostLoadAssetRegistryTags. We then run a second full Tick to finish
|
|
// out their processing. See UE-210249 for the desired fix.
|
|
TArray<FName> FoundAssetPackageNames;
|
|
{
|
|
// Find any assets that were deferred but fall into the paths we are interested in. Extract them from
|
|
// the DeferredAssets and DeferredAssetsForGameThread containers
|
|
|
|
TMultiMap<FName, TUniquePtr<FAssetData>> CollectedDeferredAssets;
|
|
for (auto Iter = DeferredAssets.CreateIterator(); Iter; ++Iter)
|
|
{
|
|
if (IsInRequestedDir(*Iter.Value()))
|
|
{
|
|
FoundAssetPackageNames.Add(Iter.Key());
|
|
CollectedDeferredAssets.Add(MoveTemp(*Iter));
|
|
Iter.RemoveCurrent();
|
|
}
|
|
}
|
|
for (auto Iter = DeferredAssetsForGameThread.CreateIterator(); Iter; ++Iter)
|
|
{
|
|
if (IsInRequestedDir(*Iter.Value()))
|
|
{
|
|
FoundAssetPackageNames.Add(Iter.Key());
|
|
CollectedDeferredAssets.Add(MoveTemp(*Iter));
|
|
Iter.RemoveCurrent();
|
|
}
|
|
}
|
|
|
|
int32 OriginalNumDeferredAssetsForGameThread = DeferredAssetsForGameThread.Num();
|
|
TOptional<TSet<FString>> MountPointsForVerifyAfterGather;
|
|
|
|
// We don't call AssetsFoundCallback because even for deferred assets it will already have been called.
|
|
// We pass DeferredAssetsForGameThread as the OutDeferred parameter, but we expect nothing is deferred.
|
|
bool bIsInGameThread = IsInGameThread();
|
|
{
|
|
// Force AssetSearchDataGathered to process these assets, skipping the PostLoadAssetRegistryTags if needed
|
|
TGuardValue<bool> ScopedForceCompletionEventIfClassNotLoaded(bForceCompletionEvenIfClassNotLoaded, true);
|
|
TGuardValue<bool> ScopedForceCompletionEvenIfNotOnGameThread(bForceCompletionEvenIfNotOnGameThread, true);
|
|
|
|
AssetSearchDataGathered(Context.EventContext, CollectedDeferredAssets, DeferredAssetsForGameThread,
|
|
TickContext.InterruptionContext, MountPointsForVerifyAfterGather,
|
|
bIsInGameThread ? EDeferFlag::CanExecuteGameThread : EDeferFlag::BypassNow);
|
|
}
|
|
// All of the assets we collected should have been processed or deferred.
|
|
ensure(CollectedDeferredAssets.Num() == 0);
|
|
|
|
// We should not have deferred any new assets because we set the bForceCompletionEvenIf flags to true.
|
|
ensure(DeferredAssetsForGameThread.Num() <= OriginalNumDeferredAssetsForGameThread);
|
|
|
|
// Tick to perform any subsequent processing required for these assets beyond AssetSearchDataGathered
|
|
Impl::FTickContext AssetTickContext(*this, Context.EventContext, Context.InheritanceContext);
|
|
Context.Status = TickGatherer(AssetTickContext);
|
|
}
|
|
FoundAssetPackageNames.Sort(FNameFastLess());
|
|
FoundAssetPackageNames.SetNum(Algo::Unique(FoundAssetPackageNames));
|
|
|
|
#if WITH_EDITOR
|
|
LoadCalculatedDependencies(&FoundAssetPackageNames, Context.InheritanceContext,
|
|
&PackagesNeedingDependencyCalculation, TickContext.InterruptionContext);
|
|
LoadCalculatedDependencies(&FoundAssetPackageNames, Context.InheritanceContext,
|
|
&PackagesNeedingDependencyCalculationOnGameThread, TickContext.InterruptionContext);
|
|
#endif
|
|
|
|
TSet<FSoftObjectPath> OldAssetsToRemove = ExistingAssets.Difference(GatheredAssets);
|
|
for (FSoftObjectPath& OldAssetToRemove : OldAssetsToRemove)
|
|
{
|
|
FAssetData* AssetDataToRemove = State.GetMutableAssetByObjectPath(OldAssetToRemove);
|
|
if (AssetDataToRemove)
|
|
{
|
|
RemoveAssetData(Context.EventContext, AssetDataToRemove);
|
|
}
|
|
}
|
|
|
|
TSet<FName> OldVerseFilesToRemove = ExistingVerseFiles.Difference(GatheredVerseFiles);
|
|
for (FName OldVerseFileToRemove : OldVerseFilesToRemove)
|
|
{
|
|
RemoveVerseFile(Context.EventContext, OldVerseFileToRemove);
|
|
}
|
|
}
|
|
|
|
namespace Utils
|
|
{
|
|
|
|
void InitializeMountPoints(TOptional<TSet<FString>>& MountPoints)
|
|
{
|
|
if (MountPoints.IsSet())
|
|
{
|
|
return;
|
|
}
|
|
MountPoints.Emplace();
|
|
TArray<FString> MountPointsArray;
|
|
FPackageName::QueryRootContentPaths(MountPointsArray, /*bIncludeReadOnlyRoots=*/ true, /*bWithoutLeadingSlashes*/ false, /*WithoutTrailingSlashes=*/ true);
|
|
MountPoints->Append(MoveTemp(MountPointsArray));
|
|
}
|
|
|
|
bool IsPathMounted(const FString& Path, const TSet<FString>& MountPointsNoTrailingSlashes, FString& StringBuffer)
|
|
{
|
|
const int32 SecondSlash = Path.Len() > 1 ? Path.Find(TEXT("/"), ESearchCase::CaseSensitive, ESearchDir::FromStart, 1) : INDEX_NONE;
|
|
if (SecondSlash != INDEX_NONE)
|
|
{
|
|
StringBuffer.Reset(SecondSlash);
|
|
StringBuffer.Append(*Path, SecondSlash);
|
|
if (MountPointsNoTrailingSlashes.Contains(StringBuffer))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (MountPointsNoTrailingSlashes.Contains(Path))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
|
|
FAssetData* FAssetRegistryImpl::ResolveAssetIdCollision(FAssetData& A, FAssetData& B)
|
|
{
|
|
FAssetData* ResolvedAsset = nullptr;
|
|
AssetCollisionEvent.Broadcast(A, B, ResolvedAsset);
|
|
check(!ResolvedAsset || ResolvedAsset == &A || ResolvedAsset == &B);
|
|
|
|
// We could use file age to try to guess which file is correct:
|
|
// FPackageName::InternalDoesPackageExistEx() to get the filename, and IFileManager::GetFileAgeSeconds
|
|
// But that would vary from machine to machine based on when the files were synced.
|
|
// So instead just pick one using an arbitrary deterministic process: alphabetical order
|
|
FAssetData* Keep = ResolvedAsset ? ResolvedAsset : (A.PackageName.LexicalLess(B.PackageName) ? &A : &B);
|
|
FAssetData* Discard = Keep == &A ? &B : &A;
|
|
|
|
FString PackageNameB = B.PackageName.ToString();
|
|
FString FileNameA;
|
|
FString FileNameB;
|
|
UE_LOG(LogAssetRegistry, Warning, TEXT("Invalid duplicate copies of ExternalActor %s. Resolve by deleting the package that is invalid. Chosing alphabetically for this process.")
|
|
TEXT("\n\tDiscarding: %s")
|
|
TEXT("\n\tKeeping: %s"),
|
|
*Keep->GetObjectPathString(),
|
|
*Discard->PackageName.ToString(),
|
|
*Keep->PackageName.ToString());
|
|
|
|
return Keep;
|
|
}
|
|
|
|
bool FAssetRegistryImpl::TryPostLoadAssetRegistryTags(FAssetData* AssetData, EDeferFlag DeferFlag,
|
|
FTopLevelAssetPath& OutRequestedGameThreadClass)
|
|
{
|
|
check(AssetData);
|
|
OutRequestedGameThreadClass = FTopLevelAssetPath();
|
|
if (!AssetData->TagsAndValues.Num())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// bForceCompletionEvenIfClassNotLoaded can be set in some cases even before engine startup is complete, for code
|
|
// that needs to access the assets immediately (e.g. ScanPathsSynchronous). Silently complete those cases. But if
|
|
// we are forcing completion because engine loading is complete, then log a warning for them.
|
|
bool bLogMissingClassWarnings = bPreloadingComplete && IsEngineStartupModuleLoadingComplete();
|
|
UClass* AssetClass = nullptr;
|
|
FTopLevelAssetPath AssetClassPath;
|
|
AssetClassPath = AssetData->AssetClassPath;
|
|
AssetClass = FindObject<UClass>(AssetClassPath, EFindObjectFlags::ExactClass);
|
|
|
|
int32 CycleCount = 0;
|
|
// We want to avoid the expense of detecting the cycle for the common case.
|
|
// CycleCountBeforeCycleDetection is Tuned for the maximum depth encountered on a large project.
|
|
constexpr int32 CycleCountBeforeCycleDetection = 10;
|
|
TOptional<TSet<FTopLevelAssetPath>> CycleSet;
|
|
TOptional<TArray<FTopLevelAssetPath>> CycleArray;
|
|
while (!AssetClass)
|
|
{
|
|
if (++CycleCount > CycleCountBeforeCycleDetection)
|
|
{
|
|
if (!CycleSet)
|
|
{
|
|
CycleSet.Emplace();
|
|
CycleArray.Emplace();
|
|
}
|
|
bool bExists;
|
|
CycleSet->Add(AssetClassPath, &bExists);
|
|
CycleArray->Add(AssetClassPath);
|
|
if (bExists)
|
|
{
|
|
TStringBuilder<256> CycleText;
|
|
for (const FTopLevelAssetPath& CyclePath : *CycleArray)
|
|
{
|
|
CycleText << CyclePath.ToString() << TEXT(", ");
|
|
}
|
|
UE_LOG(LogAssetRegistry, Warning,
|
|
TEXT("Cycle detected in ParentClasses, ObjectRedirectors, or CoreRedirects, starting from AssetClassPath %s. Skipping AssetRegistry normalization of assets with that class.")
|
|
TEXT("\n\tCycle: { %s }"),
|
|
*AssetData->AssetClassPath.ToString(), *CycleText);
|
|
|
|
// We have given the relevant warning, there is nothing further we need to do with this class,
|
|
// so mark the postload completed.
|
|
return true;
|
|
}
|
|
}
|
|
// this is probably a blueprint that has not yet been loaded, try to find its native base class
|
|
const FTopLevelAssetPath* ParentClassPath = CachedBPInheritanceMap.Find(AssetClassPath);
|
|
if (ParentClassPath && !ParentClassPath->IsNull())
|
|
{
|
|
AssetClassPath = *ParentClassPath;
|
|
AssetClass = FindObject<UClass>(AssetClassPath, EFindObjectFlags::ExactClass);
|
|
}
|
|
else
|
|
{
|
|
FTopLevelAssetPath LastAssetClassPath = AssetClassPath;
|
|
// Maybe it's a redirector
|
|
FSoftObjectPath RedirectedPath = GRedirectCollector.GetAssetPathRedirection(FSoftObjectPath::ConstructFromAssetPath(AssetClassPath));
|
|
if (RedirectedPath.IsValid())
|
|
{
|
|
AssetClassPath = RedirectedPath.GetAssetPath();
|
|
}
|
|
else
|
|
{
|
|
FCoreRedirectObjectName NewName = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Class, FCoreRedirectObjectName(AssetClassPath));
|
|
if (NewName.IsValid())
|
|
{
|
|
AssetClassPath = NewName.ToString();
|
|
}
|
|
}
|
|
|
|
if (AssetClassPath != LastAssetClassPath && !AssetClassPath.IsNull())
|
|
{
|
|
AssetClass = FindObject<UClass>(AssetClassPath, EFindObjectFlags::ExactClass);
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!AssetClass)
|
|
{
|
|
if (!bForceCompletionEvenIfClassNotLoaded)
|
|
{
|
|
// We need to wait for the class to load, return false until we are forced to complete.
|
|
return false;
|
|
}
|
|
|
|
// Log a warning and mark the postload completed.
|
|
if (bLogMissingClassWarnings)
|
|
{
|
|
FString Reason;
|
|
if (AssetClassPath.ToString().StartsWith(TEXT("/Script/")))
|
|
{
|
|
Reason = TEXT("The missing class is native--perhaps a CoreRedirector is missing?");
|
|
}
|
|
else
|
|
{
|
|
if (State.GetAssetPackageData(AssetClassPath.GetPackageName()) == nullptr)
|
|
{
|
|
Reason = TEXT("The class is missing on disk or could not be loaded. Perhaps it has been deleted from perforce and the referencing object is broken?");
|
|
}
|
|
}
|
|
//@TODO this should become a Warning once UE-209846 is finished
|
|
UE_LOG(LogAssetRegistry, Verbose,
|
|
TEXT("Unable to PostLoadAssetRegistryTags for '%s' because ancestor class '%s' cannot be found. %s"),
|
|
*AssetData->GetObjectPathString(), *AssetClassPath.ToString(), *Reason);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Now identify the most derived native class in the class hierarchy
|
|
while (!AssetClass->HasAnyClassFlags(CLASS_Native))
|
|
{
|
|
AssetClass = AssetClass->GetSuperClass();
|
|
}
|
|
|
|
// Use the class's CDO to run the virtual functions in ThreadedPostLoadAssetRegistryTags
|
|
UObject* ClassDefaultObject = AssetClass->GetDefaultObject(false);
|
|
if (ClassDefaultObject)
|
|
{
|
|
// It is only safe to read flags from the CDO if we have a barrier between the time where the default object
|
|
// pointer is published, and the time we read the flag. Otherwise, we could end up reading a stale flag that
|
|
// hasn't yet been set to RF_NeedInitialization. It needs to be paired with a release barrier when the default
|
|
// object is set.
|
|
std::atomic_thread_fence(std::memory_order_acquire);
|
|
TSAN_AFTER(ClassDefaultObject); // TSAN doesn't understand fence yet, help it understand what's going on
|
|
|
|
if (ClassDefaultObject->HasAnyFlags(RF_NeedInitialization))
|
|
{
|
|
ClassDefaultObject = nullptr;
|
|
}
|
|
}
|
|
|
|
if (!ClassDefaultObject)
|
|
{
|
|
if (!bForceCompletionEvenIfClassNotLoaded)
|
|
{
|
|
// Continue waiting until the CDO is available
|
|
return false;
|
|
}
|
|
|
|
// Log a warning and mark the postload completed.
|
|
if (bLogMissingClassWarnings)
|
|
{
|
|
ensureMsgf(false,
|
|
TEXT("Unable to PostLoadAssetRegistryTags for '%s' because the CDO for ancestor class '%s' could not be found or was not ready."),
|
|
*AssetData->GetObjectPathString(), *AssetClassPath.ToString());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// We are using RF_NeedInitialization to guarantee that ClassDefaultObject is fully initialized
|
|
// potentially on another thread. For weakly ordered memory platforms, we need to
|
|
// ensure that our read of the vtable ptr isn't performed prior to the read of the class flags
|
|
// otherwise we might see a stale vtable despite seeing RF_NeedInit clear.
|
|
std::atomic_thread_fence(std::memory_order_acquire);
|
|
TSAN_AFTER(ClassDefaultObject); // TSAN doesn't understand fence yet, help it understand what's going on
|
|
|
|
TArray<UObject::FAssetRegistryTag> TagsToModify;
|
|
bool bCanCreateObjects = DeferFlag == EDeferFlag::CanExecuteGameThread;
|
|
UObject::FPostLoadAssetRegistryTagsContext Context(*AssetData, TagsToModify, bCanCreateObjects);
|
|
ClassDefaultObject->ThreadedPostLoadAssetRegistryTags(Context);
|
|
|
|
if (!Context.PostLoadCouldComplete())
|
|
{
|
|
// If we are still able to wait on a missing class, then defer this asset before checking whether we need
|
|
// to kick it to game thread. The Gamethread might end up creating the required data for us if we
|
|
// continue waiting.
|
|
if (!bForceCompletionEvenIfClassNotLoaded)
|
|
{
|
|
// Continue waiting until the CDO is available
|
|
return false;
|
|
}
|
|
|
|
if (!bCanCreateObjects)
|
|
{
|
|
// Assume that we could not complete because we need bCanCreateObjects=true.
|
|
if (!bForceCompletionEvenIfNotOnGameThread && DeferFlag != EDeferFlag::BypassNow)
|
|
{
|
|
// Request completion on the game thread.
|
|
OutRequestedGameThreadClass = AssetClass->GetClassPathName();
|
|
return false;
|
|
}
|
|
|
|
// Log a warning and complete the PostLoad
|
|
UE_LOG(LogAssetRegistry, Display,
|
|
TEXT("Unable to PostLoadAssetRegistryTags for '%s' because class '%s' ThreadedPostLoadAssetRegistryTags requires GameThread to load its CDO, but we need it immediately when off the game thread."),
|
|
*AssetData->GetObjectPathString(), *AssetClass->GetPathName());
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// We're on the gamethread, so we can't complete because of something else. Treat it the same as CDO not ready.
|
|
// Log a warning and complete the postload.
|
|
if (bLogMissingClassWarnings)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Display,
|
|
TEXT("Unable to PostLoadAssetRegistryTags for '%s' because class '%s' ThreadedPostLoadAssetRegistryTags returned !PostLoadCouldComplete."),
|
|
*AssetData->GetObjectPathString(), *AssetClass->GetPathName());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (TagsToModify.Num())
|
|
{
|
|
FAssetDataTagMap TagsAndValues = AssetData->TagsAndValues.CopyMap();
|
|
for (const UObject::FAssetRegistryTag& Tag : TagsToModify)
|
|
{
|
|
if (!Tag.Value.IsEmpty())
|
|
{
|
|
FString& Value = TagsAndValues.FindOrAdd(Tag.Name);
|
|
Value = Tag.Value;
|
|
}
|
|
else
|
|
{
|
|
TagsAndValues.Remove(Tag.Name);
|
|
}
|
|
}
|
|
AssetData->TagsAndValues = FAssetDataTagMapSharedView(MoveTemp(TagsAndValues));
|
|
}
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
bool FAssetRegistryImpl::ShouldSkipGatheredAsset(FAssetData& AssetData)
|
|
{
|
|
// TODO: This pruning of invalid ExternalActors is temporary, to handle the fallout from a bug in SaveAs
|
|
// that is keeping the old ExternalActors as duplicates of the new ones. Remove it after the data has been
|
|
// cleaned up for all affected licensees. If we need such validation permanently, it should be decoupled
|
|
// from the AssetRegistry by adding a delegate.
|
|
// Extra validation for ExternalActors. If duplicate ExternalActors with the same object path exist
|
|
// then we intermittently will fail to find the correct one and WorldPartition will break.
|
|
// Validate that the PackageName matches what is expected from the ObjectPath.
|
|
|
|
#if WITH_EDITORONLY_DATA
|
|
if (AssetData.GetOptionalOuterPathName().IsNone())
|
|
{
|
|
// If no outer path, this can't be an external asset
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
FStringView ExternalActorsFolderName(FPackagePath::GetExternalActorsFolderName());
|
|
TStringBuilder<256> PackageNameStr;
|
|
AssetData.PackageName.ToString(PackageNameStr);
|
|
if (UE::String::FindFirst(PackageNameStr, ExternalActorsFolderName) != INDEX_NONE)
|
|
{
|
|
TStringBuilder<256> ObjectPathString;
|
|
AssetData.AppendObjectPath(ObjectPathString);
|
|
FStringView ObjectPathPackageName = FPackageName::ObjectPathToPackageName(ObjectPathString);
|
|
FStringView PackageNamePackageRoot;
|
|
FStringView PackageNameRelPath;
|
|
FStringView ObjectPathPackageRoot;
|
|
FStringView ObjectPathRelPath;
|
|
|
|
// /PackageRoot/__ExternalActors__/RelPathFromPackageRootToMap/#/##/#######
|
|
// OR
|
|
// /PackageRoot/__ExternalActors__/ContentBundle/######/RelPathFromPackageRootToMap/#/##/#######
|
|
// OR
|
|
// /PackageRoot/__ExternalActors__/EDL/######/ObjectPathPackageRoot/RelPathFromPackageRootToMap/#/##/#######
|
|
// Package roots do not need to be the same; ContentBundles can be injected into /Game maps from plugins
|
|
PackageNamePackageRoot = FPackageName::SplitPackageNameRoot(PackageNameStr, &PackageNameRelPath);
|
|
ObjectPathPackageRoot = FPackageName::SplitPackageNameRoot(ObjectPathPackageName, &ObjectPathRelPath);
|
|
|
|
if (!PackageNameRelPath.StartsWith(ExternalActorsFolderName) || !PackageNameRelPath.RightChop(ExternalActorsFolderName.Len()).StartsWith(TEXT("/")))
|
|
{
|
|
UE_LOG(LogAssetRegistry, Verbose,
|
|
TEXT("Invalid ExternalActor: Package %s is an ExternalActor package but is not in the expected root path for ExternalActors /%.*s/%.*s. Ignoring this actor."),
|
|
*PackageNameStr, PackageNamePackageRoot.Len(), PackageNamePackageRoot.GetData(),
|
|
ExternalActorsFolderName.Len(), ExternalActorsFolderName.GetData());
|
|
return true;
|
|
}
|
|
|
|
bool bIsEDLActor = false;
|
|
bool bIsPluginActor = false;
|
|
FStringView PackageNameRelPathAfterExternalActorRoot = PackageNameRelPath.RightChop(ExternalActorsFolderName.Len() + 1);
|
|
FStringView ContentBundleDirName(TEXTVIEW("ContentBundle"));
|
|
FStringView ExternalDataLayerDirName(TEXTVIEW("EDL"));
|
|
if (PackageNameRelPathAfterExternalActorRoot.StartsWith(ContentBundleDirName))
|
|
{
|
|
PackageNameRelPathAfterExternalActorRoot.RightChopInline(ContentBundleDirName.Len());
|
|
bIsPluginActor = true;
|
|
}
|
|
else if (PackageNameRelPathAfterExternalActorRoot.StartsWith(ExternalDataLayerDirName))
|
|
{
|
|
PackageNameRelPathAfterExternalActorRoot.RightChopInline(ExternalDataLayerDirName.Len());
|
|
bIsEDLActor = true;
|
|
bIsPluginActor = true;
|
|
}
|
|
|
|
bool bAllowValidation = true;
|
|
if (bIsPluginActor)
|
|
{
|
|
bAllowValidation = false; // Don't allow validation unless we succeed in finding the new relpath
|
|
if (PackageNameRelPathAfterExternalActorRoot.StartsWith(TEXT("/")))
|
|
{
|
|
PackageNameRelPathAfterExternalActorRoot.RightChopInline(1);
|
|
int32 NextSlash;
|
|
PackageNameRelPathAfterExternalActorRoot.FindChar('/', NextSlash);
|
|
if (NextSlash != INDEX_NONE)
|
|
{
|
|
PackageNameRelPathAfterExternalActorRoot.RightChopInline(NextSlash + 1);
|
|
// EDL path keeps ObjectPathPackageRoot
|
|
if (bIsEDLActor)
|
|
{
|
|
if (PackageNameRelPathAfterExternalActorRoot.StartsWith(ObjectPathPackageRoot))
|
|
{
|
|
PackageNameRelPathAfterExternalActorRoot.RightChopInline(ObjectPathPackageRoot.Len());
|
|
if (PackageNameRelPathAfterExternalActorRoot.StartsWith(TEXT("/")))
|
|
{
|
|
PackageNameRelPathAfterExternalActorRoot.RightChopInline(1);
|
|
bAllowValidation = true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bAllowValidation = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bAllowValidation && !PackageNameRelPathAfterExternalActorRoot.StartsWith(ObjectPathRelPath))
|
|
{
|
|
TStringBuilder<256> ExpectedPath;
|
|
ExpectedPath << "/" << ObjectPathPackageRoot << "/" << ExternalActorsFolderName << "/" << ObjectPathRelPath;
|
|
UE_LOG(LogAssetRegistry, Verbose,
|
|
TEXT("Invalid ExternalActor: Package %s is an ExternalActor package but its path does not match the expected path %s created from its objectpath %s. Ignoring this actor."),
|
|
*PackageNameStr, *ExpectedPath, *ObjectPathString);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void FAssetRegistryImpl::AssetSearchDataGathered(Impl::FEventContext& EventContext,
|
|
TMultiMap<FName, TUniquePtr<FAssetData>>& AssetResults,
|
|
TMultiMap<FName, TUniquePtr<FAssetData>>& OutDeferredAssetResults,
|
|
Impl::FInterruptionContext& InOutInterruptionContext,
|
|
TOptional<TSet<FString>>& MountPointsForVerifyAfterGather,
|
|
EDeferFlag DeferFlag)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(AssetSearchDataGathered);
|
|
|
|
// Refreshes ClassGeneratorNames if out of date due to module load
|
|
CollectCodeGeneratorClasses();
|
|
|
|
FString PackagePathString;
|
|
FString PackageRoot;
|
|
if (AssetResults.Num() > 0 && bVerifyMountPointAfterGather)
|
|
{
|
|
Utils::InitializeMountPoints(MountPointsForVerifyAfterGather);
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
// This ensures we can search for classes inside PostLoadAssetRegistryTags. We take the lock once out here to reduce overhead
|
|
FGCScopeGuard Guard;
|
|
#endif
|
|
|
|
TSet<FTopLevelAssetPath> MissingClasses;
|
|
bool bInterrupted = false;
|
|
int64 IterationCounter = 0;
|
|
|
|
// Add the found assets
|
|
for (TMultiMap<FName, TUniquePtr<FAssetData>>::TIterator Iter(AssetResults); Iter && !bInterrupted; ++Iter)
|
|
{
|
|
ON_SCOPE_EXIT
|
|
{
|
|
// ShouldExitEarly calls FPlatformTime::Seconds which isn't super cheap
|
|
// Since we can spin very quickly in this loop, avoid checking every single iteration
|
|
if ((++IterationCounter % 10) == 0)
|
|
{
|
|
// Check to see if we have run out of time in this tick
|
|
bInterrupted = InOutInterruptionContext.ShouldExitEarly();
|
|
}
|
|
};
|
|
|
|
// Delete or take ownership of the BackgroundResult; it was originally new'd by an FPackageReader
|
|
TUniquePtr<FAssetData> BackgroundResult(MoveTemp(Iter.Value()));
|
|
FName BackgroundAssetPackageName = Iter.Key();
|
|
CA_ASSUME(BackgroundResult.Get() != nullptr);
|
|
Iter.RemoveCurrent();
|
|
|
|
// Skip assets that are invalid because e.g. they are externalactors that were mistakenly not deleted
|
|
// when their map moved.
|
|
if (ShouldSkipGatheredAsset(*BackgroundResult))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Skip stale gather results from unmounted roots caused by mount then unmount of a path within short period.
|
|
const FName PackagePath = BackgroundResult->PackagePath;
|
|
if (bVerifyMountPointAfterGather)
|
|
{
|
|
PackagePath.ToString(PackagePathString);
|
|
if (!Utils::IsPathMounted(PackagePathString, *MountPointsForVerifyAfterGather, PackageRoot))
|
|
{
|
|
UE_LOG(LogAssetRegistry, Display,
|
|
TEXT("AssetRegistry: An asset has been loaded with an invalid mount point: '%s', Mount Point: '%s'. Ignoring the asset."),
|
|
*BackgroundResult->GetObjectPathString(), *PackagePathString);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
// Postload assets based on their declared class. Queue them for for later retry if their class has not yet loaded.
|
|
FTopLevelAssetPath RequestedGameThreadClass;
|
|
bool bCouldPostLoad = TryPostLoadAssetRegistryTags(BackgroundResult.Get(), DeferFlag, RequestedGameThreadClass);
|
|
if (!bCouldPostLoad)
|
|
{
|
|
if (RequestedGameThreadClass.IsValid() && DeferFlag == EDeferFlag::QueueToGameThread)
|
|
{
|
|
BackgroundResults.AssetsForGameThread.Add(BackgroundAssetPackageName, MoveTemp(BackgroundResult));
|
|
RequestGameThreadProcessClass(RequestedGameThreadClass);
|
|
}
|
|
else
|
|
{
|
|
OutDeferredAssetResults.Add(BackgroundAssetPackageName, MoveTemp(BackgroundResult));
|
|
}
|
|
continue;
|
|
}
|
|
#endif
|
|
bProcessedAnyAssetsAfterRetryDeferred = true;
|
|
|
|
// Look for an existing asset to check whether we need to add or update
|
|
FCachedAssetKey Key(*BackgroundResult);
|
|
FAssetData* ExistingAssetData = State.GetMutableAssetByObjectPath(Key);
|
|
// The background result should not already be registered; it should be impossible since it is in TUnqiuePtr
|
|
check(ExistingAssetData == nullptr || ExistingAssetData != BackgroundResult.Get());
|
|
|
|
#if WITH_EDITOR
|
|
if (ExistingAssetData && ExistingAssetData->PackageName != BackgroundResult->PackageName)
|
|
{
|
|
// This can happen with ExternalActors, which have a Key based on their outermost map, but
|
|
// are in a separate package. It's invalid to have more than one of them, but can happen when
|
|
// actors are moved between packages if the delete is not recorded.
|
|
FAssetData* PackageToKeep = ResolveAssetIdCollision(*ExistingAssetData, *BackgroundResult);
|
|
if (PackageToKeep == ExistingAssetData)
|
|
{
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
check(PackageToKeep == BackgroundResult.Get());
|
|
RemoveAssetData(EventContext, ExistingAssetData);
|
|
ExistingAssetData = nullptr;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (ExistingAssetData)
|
|
{
|
|
#if WITH_EDITOR
|
|
if (IsLoadedAndTrackedAsset(BackgroundResult->GetSoftObjectPath()))
|
|
{
|
|
// If the current AssetData came from a loaded asset, don't overwrite it with the new one from disk
|
|
// The loaded asset is more authoritative because it has run the postload steps.
|
|
// However, the loaded asset is missing the extended tags. Our contract for extended tags is to keep any
|
|
// that do not exist in the non-extended tags. So add on any tags from the BackgroundResult that
|
|
// are not already on the existing asset.
|
|
AddNonOverlappingTags(EventContext, *ExistingAssetData, *BackgroundResult);
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
// The asset exists in the cache from disk and has not yet been loaded into memory, update it with the new background data
|
|
UpdateAssetData(EventContext, ExistingAssetData, MoveTemp(*BackgroundResult), false /* bKeepDeletedTags */);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// The asset isn't in the cache yet, add it and notify subscribers
|
|
#if !NO_LOGGING
|
|
if (bVerboseLogging)
|
|
{
|
|
int64& ClassTagSizes = TagSizeByClass.FindOrAdd(BackgroundResult->AssetClassPath);
|
|
BackgroundResult->TagsAndValues.ForEach([&ClassTagSizes](const TPair<FName, FAssetTagValueRef>& Pair)
|
|
{
|
|
ClassTagSizes += Pair.Value.GetResourceSize();
|
|
});
|
|
}
|
|
#endif
|
|
|
|
AddAssetData(EventContext, BackgroundResult.Release());
|
|
}
|
|
|
|
// Populate the path tree
|
|
AddAssetPath(EventContext, PackagePath);
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::PathDataGathered(Impl::FEventContext& EventContext, TRingBuffer<FString>& PathResults,
|
|
Impl::FInterruptionContext& InOutInterruptionContext, TOptional<TSet<FString>>& MountPointsForVerifyAfterGather)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(PathDataGathered);
|
|
|
|
FString PackageRoot;
|
|
if (PathResults.Num() > 0 && bVerifyMountPointAfterGather)
|
|
{
|
|
Utils::InitializeMountPoints(MountPointsForVerifyAfterGather);
|
|
}
|
|
|
|
CachedPathTree.EnsureAdditionalCapacity(PathResults.Num());
|
|
|
|
while (PathResults.Num() > 0)
|
|
{
|
|
FString Path = PathResults.PopFrontValue();
|
|
|
|
// Skip stale results caused by mount then unmount of a path within short period.
|
|
if (!bVerifyMountPointAfterGather || Utils::IsPathMounted(Path, *MountPointsForVerifyAfterGather, PackageRoot))
|
|
{
|
|
AddAssetPath(EventContext, FName(*Path));
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogAssetRegistry, Display,
|
|
TEXT("AssetRegistry: A path has been loaded with an invalid mount point: '%s', Mount Point: '%s'. Ignoring the path."),
|
|
*Path, *PackageRoot);
|
|
}
|
|
|
|
// Check to see if we have run out of time in this tick
|
|
if (InOutInterruptionContext.ShouldExitEarly())
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::DependencyDataGathered(TMultiMap<FName, FPackageDependencyData>& DependsResults,
|
|
TMultiMap<FName, FPackageDependencyData>& OutDeferredDependencyResults,
|
|
TSet<FName>* OutPackagesNeedingDependencyCalculation, Impl::FInterruptionContext& InOutInterruptionContext,
|
|
TOptional<TSet<FString>>& MountPointsForVerifyAfterGather)
|
|
{
|
|
using namespace UE::AssetRegistry;
|
|
using namespace UE::AssetRegistry::DependsNode;
|
|
using namespace UE::Tasks;
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(DependencyDataGathered);
|
|
|
|
// Don't bother registering dependencies on these packages, every package in the game will depend on them
|
|
static TArray<FName> ScriptPackagesToSkip = TArray<FName>{
|
|
GetScriptPackageNameCoreUObject(),
|
|
GetScriptPackageNameEngine(),
|
|
GetScriptPackageNameBlueprintGraph(),
|
|
GetScriptPackageNameUnrealEd(),
|
|
};
|
|
|
|
// This ensures we can call FindPackage below from a background thread
|
|
FGCScopeGuard Guard;
|
|
|
|
// If we need to verify mounts, initialize the mounts once before iterating
|
|
FString PackageRoot;
|
|
FString PackageNameString;
|
|
const bool bLocalVerifyMountPointAfterGather = bVerifyMountPointAfterGather;
|
|
if (bLocalVerifyMountPointAfterGather)
|
|
{
|
|
Utils::InitializeMountPoints(MountPointsForVerifyAfterGather);
|
|
}
|
|
|
|
TMap<FName, FConstructData>& AssetNameToConstructDataMap = GatherDependencyDataScratchPad->AssetNameToConstructDataMap;
|
|
TMap<FName, FName>& CachedDepToRedirect = GatherDependencyDataScratchPad->CachedDepToRedirect;
|
|
TSet<FDependsNode*>& ModifiedDependNodes = GatherDependencyDataScratchPad->ModifiedDependNodes;
|
|
AssetNameToConstructDataMap.Reserve(DependsResults.Num());
|
|
CachedDepToRedirect.Reserve(DependsResults.Num());
|
|
ModifiedDependNodes.Reserve(DependsResults.Num());
|
|
ON_SCOPE_EXIT{ GatherDependencyDataScratchPad->Reset(); };
|
|
|
|
State.CachedPackageData.Reserve(DependsResults.Num());
|
|
State.CachedDependsNodes.Reserve(DependsResults.Num());
|
|
|
|
bool bInterrupted = false;
|
|
int32 IterationCounter = 0;
|
|
constexpr int32 MinBatchSize = 1024;
|
|
const bool bUseJobs = DependsResults.Num() > MinBatchSize;
|
|
const EParallelForFlags ParallelForFlags = bUseJobs ? EParallelForFlags::None : EParallelForFlags::ForceSingleThread;
|
|
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(DependencyGatheredData_GatherPackages);
|
|
const bool bShouldSortReferencers = ShouldSortReferencers();
|
|
for (TMultiMap<FName, FPackageDependencyData>::TIterator Iter(DependsResults); Iter && !bInterrupted; ++Iter)
|
|
{
|
|
if (DeferredAssets.Contains(Iter.Key()) || DeferredAssetsForGameThread.Contains(Iter.Key()))
|
|
{
|
|
OutDeferredDependencyResults.Add(MoveTemp(*Iter));
|
|
Iter.RemoveCurrent();
|
|
continue;
|
|
}
|
|
|
|
ON_SCOPE_EXIT
|
|
{
|
|
// ShouldExitEarly calls FPlatformTime::Seconds which isn't super cheap
|
|
// Since we can spin very quickly in this loop, avoid checking every single iteration
|
|
if ((++IterationCounter % MinBatchSize) == 0)
|
|
{
|
|
// Check to see if we have run out of time in this tick
|
|
bInterrupted = InOutInterruptionContext.ShouldExitEarly();
|
|
}
|
|
};
|
|
|
|
FPackageDependencyData Result = MoveTemp(Iter.Value());
|
|
Iter.RemoveCurrent();
|
|
|
|
checkf(!GIsEditor || Result.bHasPackageData, TEXT("We rely on PackageData being read for every gathered Asset in the editor."));
|
|
FName SourceNodePackageName = Result.PackageName;
|
|
if (Result.bHasPackageData)
|
|
{
|
|
// Update package data
|
|
FAssetPackageData* PackageData = State.CreateOrGetAssetPackageData(SourceNodePackageName);
|
|
*PackageData = Result.PackageData;
|
|
}
|
|
|
|
// DependsResults might contain multiple values with the same SourcePackageName.
|
|
// We don't want to create more constructed nodes than necessary, so we create
|
|
// or find existing FConstructData and append results as needed.
|
|
bool bSourceNodeAlreadyExisted = false;
|
|
FConstructData* ConstructData = nullptr;
|
|
if (FConstructData* ExistingConstructData = AssetNameToConstructDataMap.Find(SourceNodePackageName))
|
|
{
|
|
ConstructData = ExistingConstructData;
|
|
}
|
|
else
|
|
{
|
|
if (bLocalVerifyMountPointAfterGather)
|
|
{
|
|
SourceNodePackageName.ToString(PackageNameString);
|
|
if (!Utils::IsPathMounted(PackageNameString, *MountPointsForVerifyAfterGather, PackageRoot))
|
|
{
|
|
UE_LOG(LogAssetRegistry, Display,
|
|
TEXT("AssetRegistry: DependencyData has been loaded with an invalid mount point: '%s', Mount Point: '%s'. Ignoring the DependencyData."),
|
|
*PackageNameString, *PackageRoot);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
FDependsNode* SourceNode = State.CreateOrFindDependsNode(SourceNodePackageName, &bSourceNodeAlreadyExisted);
|
|
if (!bSourceNodeAlreadyExisted)
|
|
{
|
|
if (FPackageDependencyData* DependencyDependencyData = DependsResults.Find(SourceNodePackageName))
|
|
{
|
|
SourceNode->Reserve(DependencyDependencyData->DependsNodeReservations);
|
|
}
|
|
}
|
|
|
|
if (Result.bHasDependencyData)
|
|
{
|
|
SourceNode->Reserve(Result.DependsNodeReservations);
|
|
SourceNode->SetAllowShrinking(false);
|
|
SourceNode->SetIsReferencersSorted(bShouldSortReferencers);
|
|
|
|
ModifiedDependNodes.Add(SourceNode);
|
|
|
|
ConstructData = &AssetNameToConstructDataMap.Add(SourceNodePackageName);
|
|
ConstructData->SourceNode = SourceNode;
|
|
}
|
|
}
|
|
|
|
if (ConstructData)
|
|
{
|
|
OutPackagesNeedingDependencyCalculation->Add(SourceNodePackageName);
|
|
|
|
FPackageDependencyMap& PackageDependencies = ConstructData->PackageDependencies;
|
|
PackageDependencies.Reserve(Result.PackageDependencies.Num());
|
|
for (const FPackageDependencyData::FPackageDependency& DependencyData : Result.PackageDependencies)
|
|
{
|
|
FName DependencyPackageName = DependencyData.PackageName;
|
|
if (EnumHasAnyFlags(DependencyData.Property, EDependencyProperty::Hard)
|
|
&& ScriptPackagesToSkip.Contains(DependencyPackageName))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FName& RedirectedName = CachedDepToRedirect.FindOrAdd(DependencyPackageName, NAME_None);
|
|
if (RedirectedName.IsNone())
|
|
{
|
|
RedirectedName = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Package,
|
|
FCoreRedirectObjectName(NAME_None, NAME_None, DependencyPackageName)).PackageName;
|
|
}
|
|
DependencyPackageName = RedirectedName;
|
|
|
|
// Skip dependencies to self
|
|
if (SourceNodePackageName == DependencyPackageName)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bool bDependsNodeAlreadyExisted = false;
|
|
FDependsNode* DependsNode = State.CreateOrFindDependsNode(DependencyPackageName, &bDependsNodeAlreadyExisted);
|
|
ModifiedDependNodes.Add(DependsNode);
|
|
|
|
if (!bDependsNodeAlreadyExisted)
|
|
{
|
|
// We created a new node so lookup its reservation
|
|
if (FPackageDependencyData* DependencyDependencyData = DependsResults.Find(DependencyPackageName))
|
|
{
|
|
DependsNode->Reserve(DependencyDependencyData->DependsNodeReservations);
|
|
}
|
|
}
|
|
|
|
// See if we need to read the script package Guid.
|
|
// Script nodes are never represented as source nodes so we can use the initialization
|
|
// state to check if we need to set the package Guid or not
|
|
// This node may have been created outside of this loop so we should always check
|
|
if (DependsNode->IsScriptPath() && !DependsNode->IsScriptDependenciesInitialized())
|
|
{
|
|
DependsNode->SetIsScriptDependenciesInitialized(true);
|
|
FNameBuilder DependencyPackageNameStr(DependencyPackageName);
|
|
|
|
// Get the guid off the script package, it is updated when
|
|
// script is changed so we need to refresh it every run
|
|
UPackage* Package = FindPackage(nullptr, *DependencyPackageNameStr);
|
|
|
|
if (Package)
|
|
{
|
|
FAssetPackageData* ScriptPackageData = State.CreateOrGetAssetPackageData(DependencyPackageName);
|
|
#if WITH_EDITORONLY_DATA
|
|
ScriptPackageData->SetPackageSavedHash(Package->GetSavedHash());
|
|
#endif
|
|
}
|
|
}
|
|
|
|
FDependsNode::FPackageFlagSet& FlagSet = PackageDependencies.FindOrAdd(DependsNode);
|
|
FlagSet.Add(FDependsNode::PackagePropertiesToByte(DependencyData.Property));
|
|
}
|
|
|
|
FDependsNode::FDependsNodeList& SearchNameDependencyNodes = ConstructData->SearchableNamesNodes;
|
|
for (const FPackageDependencyData::FSearchableNamesDependency& NamesDependency : Result.SearchableNameDependencies)
|
|
{
|
|
SearchNameDependencyNodes.Reserve(SearchNameDependencyNodes.Num() + NamesDependency.ValueNames.Num());
|
|
for (const FName& ValueName : NamesDependency.ValueNames)
|
|
{
|
|
FAssetIdentifier AssetId(NamesDependency.PackageName, NamesDependency.ObjectName, ValueName);
|
|
FDependsNode* DependsNode = State.CreateOrFindDependsNode(AssetId);
|
|
|
|
FSetElementId AddId = ModifiedDependNodes.Add(DependsNode);
|
|
SearchNameDependencyNodes.Add(DependsNode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (AssetNameToConstructDataMap.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
TArray<FConstructData*, TInlineAllocator<1>> ConstructDatas;
|
|
ConstructDatas.Reserve(AssetNameToConstructDataMap.Num());
|
|
for (TPair<FName, FConstructData>& Pair : AssetNameToConstructDataMap)
|
|
{
|
|
ConstructDatas.Add(&Pair.Value);
|
|
}
|
|
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(DependencyGatheredData_RemoveReferencers);
|
|
// Remove referencers - Single threaded due to the poor memory access patterns
|
|
// between nodes and their dependencies
|
|
for (FConstructData* ConstructData : ConstructDatas)
|
|
{
|
|
FDependsNode* SourceNode = ConstructData->SourceNode;
|
|
const FPackageDependencyMap& PackageDependencies = ConstructData->PackageDependencies;
|
|
|
|
// We will populate the node dependencies below. Empty the set here in case this file was already read
|
|
// Also remove references to all existing dependencies, those will be also repopulated below
|
|
SourceNode->IterateOverDependencies([SourceNode, &PackageDependencies] (FDependsNode* InDependency,
|
|
EDependencyCategory Category, EDependencyProperty Properties, bool bDuplicate)
|
|
{
|
|
if (!bDuplicate)
|
|
{
|
|
// Only remove nodes that we aren't going to re-add further down
|
|
if (!PackageDependencies.Contains(InDependency))
|
|
{
|
|
bool bOldAllowShrinking = InDependency->GetAllowShrinking();
|
|
InDependency->SetAllowShrinking(false);
|
|
InDependency->RemoveReferencer(SourceNode);
|
|
InDependency->SetAllowShrinking(bOldAllowShrinking);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
ParallelFor(TEXT("DependencyGatheredData_AddDependencies"), ConstructDatas.Num(), MinBatchSize,
|
|
[ShouldSortDependencyList = ShouldSortDependencies(), &ConstructDatas] (int Index)
|
|
{
|
|
FConstructData* ConstructData = ConstructDatas[Index];
|
|
FDependsNode* SourceNode = ConstructData->SourceNode;
|
|
const FPackageDependencyMap& PackageDependencies = ConstructData->PackageDependencies;
|
|
const FDependsNode::FDependsNodeList& SearchableNameNodes = ConstructData->SearchableNamesNodes;
|
|
|
|
SourceNode->ClearDependencies();
|
|
SourceNode->SetIsDependencyListSorted(EDependencyCategory::All, ShouldSortDependencyList);
|
|
|
|
// Add package dependencies
|
|
for (const TPair<FDependsNode*, FDependsNode::FPackageFlagSet>& Pair : PackageDependencies)
|
|
{
|
|
SourceNode->AddPackageDependencySet(Pair.Key, Pair.Value);
|
|
|
|
}
|
|
|
|
// Add searchable name node dependencies
|
|
for (FDependsNode* DependsNode : SearchableNameNodes)
|
|
{
|
|
SourceNode->AddDependency(DependsNode, EDependencyCategory::SearchableName, EDependencyProperty::None);
|
|
}
|
|
}, ParallelForFlags);
|
|
|
|
// Add referencers - Single threaded due to the poor memory access patterns between nodes and their dependencies
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(DependencyGatheredData_AddReferencers);
|
|
for (FConstructData* ConstructData : ConstructDatas)
|
|
{
|
|
FDependsNode* SourceNode = ConstructData->SourceNode;
|
|
const FPackageDependencyMap& PackageDependencies = ConstructData->PackageDependencies;
|
|
const FDependsNode::FDependsNodeList& SearchableNameNodes = ConstructData->SearchableNamesNodes;
|
|
|
|
// Doubly-link all of the PackageDependencies
|
|
for (const TPair<FDependsNode*, FDependsNode::FPackageFlagSet>& Pair : PackageDependencies)
|
|
{
|
|
FDependsNode* DependsNode = Pair.Key;
|
|
|
|
// Don't add sorted, we'll sort references afterwards
|
|
DependsNode->SetIsReferencersSorted(false);
|
|
DependsNode->AddReferencer(SourceNode);
|
|
}
|
|
|
|
// Add node for all name references
|
|
for (FDependsNode* DependsNode : SearchableNameNodes)
|
|
{
|
|
// Don't add sorted, we'll sort references afterwards
|
|
DependsNode->SetIsReferencersSorted(false);
|
|
DependsNode->AddReferencer(SourceNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort all referencers now that we are done inserting
|
|
FDependsNode::FDependsNodeList ModifiedDependNodesArray = ModifiedDependNodes.Array();
|
|
ParallelFor(TEXT("DependencyGatheredData_SortNodes"), ModifiedDependNodesArray.Num(), 1,
|
|
[&ModifiedDependNodesArray, bShouldSortReferencers = ShouldSortReferencers()] (int Index)
|
|
{
|
|
FDependsNode* Node = ModifiedDependNodesArray[Index];
|
|
|
|
// Set AllowShrinking first so SetIsReferencersSorted will reduce our memory use if we have over allocated
|
|
Node->SetAllowShrinking(true);
|
|
Node->SetIsReferencersSorted(bShouldSortReferencers);
|
|
}, ParallelForFlags);
|
|
}
|
|
|
|
void FAssetRegistryImpl::CookedPackageNamesWithoutAssetDataGathered(Impl::FEventContext& EventContext,
|
|
TRingBuffer<FString>& CookedPackageNamesWithoutAssetDataResults, Impl::FInterruptionContext& InOutInterruptionContext)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(CookedPackageNamesWithoutAssetDataGathered);
|
|
|
|
struct FConfigValue
|
|
{
|
|
FConfigValue()
|
|
{
|
|
if (GConfig)
|
|
{
|
|
GConfig->GetBool(TEXT("AssetRegistry"), TEXT("LoadCookedPackagesWithoutAssetData"), bShouldProcess, GEngineIni);
|
|
}
|
|
}
|
|
|
|
bool bShouldProcess = true;
|
|
};
|
|
static FConfigValue ShouldProcessCookedPackages;
|
|
|
|
// Add the found assets
|
|
if (ShouldProcessCookedPackages.bShouldProcess)
|
|
{
|
|
while (CookedPackageNamesWithoutAssetDataResults.Num() > 0)
|
|
{
|
|
// If this data is cooked and it we couldn't find any asset in its export table then try to load the entire package
|
|
// Loading the entire package will make all of its assets searchable through the in-memory scanning performed by GetAssets
|
|
EventContext.RequiredLoads.Add(CookedPackageNamesWithoutAssetDataResults.PopFrontValue());
|
|
|
|
// This function has a bug in multithreaded environment (ie UE-209843). But the feature of loading cooked packages seems to be never used
|
|
// so let's try to deprecate it. Also a new way to load cooked package is on the way.
|
|
UE_LOG(LogAssetRegistry, Warning, TEXT("CookedPackageNamesWithoutAssetDataGathered : Deprecated in 5.6 due to poor performance, will be removed in a future version, contact Epic if you need this functionality."));
|
|
}
|
|
// Avoid marking the scan complete before we have loaded all the relevant assets. By interrupting here
|
|
// we intend to ensure that the event context is processed, triggering a LoadPackage, and then a ProcessLoadedAssetsToUpdateCache,
|
|
// and only then resume scanning from disk. However, in the current multithreaded implementation this is not guaranteed
|
|
// as only the main thread broadcasts events but the background thread might come around for another time slice before
|
|
// the main thread does so. UE-209843
|
|
if (InOutInterruptionContext.IsTimeSlicingEnabled())
|
|
{
|
|
InOutInterruptionContext.RequestEarlyExit();
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Do nothing will these packages. For projects which could run entirely from cooked data, this
|
|
// process will involve opening every single package synchronously on the game thread which will
|
|
// kill performance. We need a better way.
|
|
CookedPackageNamesWithoutAssetDataResults.Empty();
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::VerseFilesGathered(Impl::FEventContext& EventContext, TRingBuffer<FName>& VerseResults,
|
|
Impl::FInterruptionContext& InOutInterruptionContext)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(VerseFilesGathered);
|
|
|
|
while (VerseResults.Num() > 0)
|
|
{
|
|
FName VerseFilePath = VerseResults.PopFrontValue();
|
|
|
|
AddVerseFile(EventContext, VerseFilePath);
|
|
|
|
// Check to see if we have run out of time in this tick
|
|
if (InOutInterruptionContext.ShouldExitEarly())
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::AddEmptyPackage(FName PackageName)
|
|
{
|
|
CachedEmptyPackages.Add(PackageName);
|
|
}
|
|
|
|
bool FAssetRegistryImpl::RemoveEmptyPackage(FName PackageName)
|
|
{
|
|
return CachedEmptyPackages.Remove(PackageName) > 0;
|
|
}
|
|
|
|
bool FAssetRegistryImpl::AddAssetPath(Impl::FEventContext& EventContext, FName PathToAdd)
|
|
{
|
|
return CachedPathTree.CachePath(PathToAdd, [this, &EventContext](FName AddedPath)
|
|
{
|
|
EventContext.PathEvents.Emplace(AddedPath.ToString(), Impl::FEventContext::EEvent::Added);
|
|
});
|
|
}
|
|
|
|
bool FAssetRegistryImpl::RemoveAssetPath(Impl::FEventContext& EventContext, FName PathToRemove, bool bEvenIfAssetsStillExist)
|
|
{
|
|
if (!bEvenIfAssetsStillExist)
|
|
{
|
|
// Check if there were assets in the specified folder. You can not remove paths that still contain assets
|
|
bool bHasAsset = false;
|
|
EnumerateAssetsByPathNoTags(PathToRemove, [&bHasAsset](const FAssetData&)
|
|
{
|
|
bHasAsset = true;
|
|
return false;
|
|
}, true /* bRecursive */, false /* bIncludeOnlyOnDiskAssets */);
|
|
|
|
// If the verse file caches contain this path then keep it around
|
|
bHasAsset |= CachedVerseFilesByPath.Contains(PathToRemove);
|
|
|
|
if (bHasAsset)
|
|
{
|
|
// At least one asset still exists in the path. Fail the remove.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
CachedPathTree.RemovePath(PathToRemove, [this, &EventContext](FName RemovedPath)
|
|
{
|
|
EventContext.PathEvents.Emplace(RemovedPath.ToString(), Impl::FEventContext::EEvent::Removed);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
void FAssetRegistryImpl::AddAssetData(Impl::FEventContext& EventContext, FAssetData* AssetData)
|
|
{
|
|
#if WITH_EDITOR
|
|
// Update Redirectors
|
|
if (AssetData->IsRedirector())
|
|
{
|
|
FString RedirectDestinationString;
|
|
AssetData->GetTagValue(UE::AssetRegistry::Impl::DestinationObjectFName, RedirectDestinationString);
|
|
FSoftObjectPath RedirectDestination = RedirectDestinationString;
|
|
if (!RedirectDestination.IsNull())
|
|
{
|
|
GRedirectCollector.AddAssetPathRedirection(AssetData->GetSoftObjectPath(), RedirectDestination);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
State.AddAssetData(AssetData);
|
|
|
|
if (!ShouldSkipAsset(AssetData->AssetClassPath, AssetData->PackageFlags))
|
|
{
|
|
EventContext.AssetEvents.Emplace(*AssetData, Impl::FEventContext::EEvent::Added);
|
|
}
|
|
|
|
// Populate the class map if adding blueprint
|
|
if (ClassGeneratorNames.Contains(AssetData->AssetClassPath))
|
|
{
|
|
const FString GeneratedClass = AssetData->GetTagValueRef<FString>(FBlueprintTags::GeneratedClassPath);
|
|
const FString ParentClass = AssetData->GetTagValueRef<FString>(FBlueprintTags::ParentClassPath);
|
|
if (!GeneratedClass.IsEmpty() && !ParentClass.IsEmpty() && GeneratedClass != TEXTVIEW("None") && ParentClass != TEXTVIEW("None"))
|
|
{
|
|
const FTopLevelAssetPath SavedGeneratedClassPathName(GeneratedClass);
|
|
const FTopLevelAssetPath GeneratedClassPathName(AssetData->PackageName, SavedGeneratedClassPathName.GetAssetName());
|
|
const FTopLevelAssetPath ParentClassPathName(ParentClass);
|
|
if (ensureAlwaysMsgf(!GeneratedClassPathName.IsNull() && !ParentClassPathName.IsNull(),
|
|
TEXT("Short class names used in AddAssetData: GeneratedClass=%s, ParentClass=%s. Short class names in these tags on the Blueprint class should have been converted to path names."),
|
|
*GeneratedClass, *ParentClass))
|
|
{
|
|
AddCachedBPClassParent(GeneratedClassPathName, ParentClassPathName);
|
|
|
|
// Invalidate caching because CachedBPInheritanceMap got modified
|
|
TempCachedInheritanceBuffer.bDirty = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::UpdateAssetData(Impl::FEventContext& EventContext, FAssetData* AssetData,
|
|
FAssetData&& NewAssetData, bool bKeepDeletedTags)
|
|
{
|
|
// Update the class map if updating a blueprint
|
|
if (ClassGeneratorNames.Contains(AssetData->AssetClassPath))
|
|
{
|
|
const FString OldGeneratedClass = AssetData->GetTagValueRef<FString>(FBlueprintTags::GeneratedClassPath);
|
|
const FString OldParentClass = AssetData->GetTagValueRef<FString>(FBlueprintTags::ParentClassPath);
|
|
const FString NewGeneratedClass = NewAssetData.GetTagValueRef<FString>(FBlueprintTags::GeneratedClassPath);
|
|
const FString NewParentClass = NewAssetData.GetTagValueRef<FString>(FBlueprintTags::ParentClassPath);
|
|
if (OldGeneratedClass != NewGeneratedClass || OldParentClass != NewParentClass)
|
|
{
|
|
if (!OldGeneratedClass.IsEmpty() && OldGeneratedClass != TEXTVIEW("None"))
|
|
{
|
|
const FTopLevelAssetPath OldGeneratedClassName(OldGeneratedClass);
|
|
if (ensureAlwaysMsgf(!OldGeneratedClassName.IsNull(),
|
|
TEXT("Short class name used: OldGeneratedClass=%s. Short class names in tags on the Blueprint class should have been converted to path names."),
|
|
*OldGeneratedClass))
|
|
{
|
|
CachedBPInheritanceMap.Remove(OldGeneratedClassName);
|
|
|
|
// Invalidate caching because CachedBPInheritanceMap got modified
|
|
TempCachedInheritanceBuffer.bDirty = true;
|
|
}
|
|
}
|
|
|
|
if (!NewGeneratedClass.IsEmpty() && !NewParentClass.IsEmpty() && NewGeneratedClass != TEXTVIEW("None") && NewParentClass != TEXTVIEW("None"))
|
|
{
|
|
const FTopLevelAssetPath NewGeneratedClassName(NewGeneratedClass);
|
|
const FTopLevelAssetPath NewParentClassName(NewParentClass);
|
|
if (ensureAlwaysMsgf(!NewGeneratedClassName.IsNull() && !NewParentClassName.IsNull(),
|
|
TEXT("Short class names used in AddAssetData: GeneratedClass=%s, ParentClass=%s. Short class names in these tags on the Blueprint class should have been converted to path names."),
|
|
*NewGeneratedClass, *NewParentClass))
|
|
{
|
|
AddCachedBPClassParent(NewGeneratedClassName, NewParentClassName);
|
|
}
|
|
|
|
// Invalidate caching because CachedBPInheritanceMap got modified
|
|
TempCachedInheritanceBuffer.bDirty = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bKeepDeletedTags)
|
|
{
|
|
TOptional<FAssetDataTagMap> UpdatedTags;
|
|
AssetData->TagsAndValues.ForEach([&NewAssetData, &UpdatedTags](const TPair<FName, FAssetTagValueRef>& TagPair)
|
|
{
|
|
if (UpdatedTags)
|
|
{
|
|
if (!UpdatedTags->Contains(TagPair.Key))
|
|
{
|
|
UpdatedTags->Add(TagPair.Key, TagPair.Value.GetStorageString());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!NewAssetData.TagsAndValues.Contains(TagPair.Key))
|
|
{
|
|
UpdatedTags.Emplace(NewAssetData.TagsAndValues.CopyMap());
|
|
UpdatedTags->Add(TagPair.Key, TagPair.Value.GetStorageString());
|
|
}
|
|
}
|
|
});
|
|
if (UpdatedTags)
|
|
{
|
|
NewAssetData.TagsAndValues = FAssetDataTagMapSharedView(MoveTemp(*UpdatedTags));
|
|
}
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
// Update Redirectors
|
|
if (AssetData->IsRedirector() || NewAssetData.IsRedirector())
|
|
{
|
|
FSoftObjectPath OldSource = AssetData->GetSoftObjectPath();
|
|
FSoftObjectPath NewSource = NewAssetData.GetSoftObjectPath();
|
|
FSoftObjectPath OldTarget;
|
|
FSoftObjectPath NewTarget;
|
|
if (AssetData->IsRedirector())
|
|
{
|
|
FString TargetString;
|
|
AssetData->GetTagValue(UE::AssetRegistry::Impl::DestinationObjectFName, TargetString);
|
|
OldTarget = TargetString;
|
|
}
|
|
if (NewAssetData.IsRedirector())
|
|
{
|
|
FString TargetString;
|
|
NewAssetData.GetTagValue(UE::AssetRegistry::Impl::DestinationObjectFName, TargetString);
|
|
NewTarget = TargetString;
|
|
}
|
|
if (OldSource != NewSource && OldTarget.IsValid())
|
|
{
|
|
GRedirectCollector.RemoveAssetPathRedirection(OldSource);
|
|
}
|
|
if (NewTarget.IsValid())
|
|
{
|
|
GRedirectCollector.AddAssetPathRedirection(NewSource, NewTarget);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
bool bModified;
|
|
State.UpdateAssetData(AssetData, MoveTemp(NewAssetData), &bModified);
|
|
|
|
if (bModified && !ShouldSkipAsset(AssetData->AssetClassPath, AssetData->PackageFlags))
|
|
{
|
|
EventContext.AssetEvents.Emplace(*AssetData, Impl::FEventContext::EEvent::Updated);
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::AddNonOverlappingTags(Impl::FEventContext& EventContext, FAssetData& ExistingAssetData,
|
|
const FAssetData& NewAssetData)
|
|
{
|
|
TOptional<FAssetDataTagMap> ModifiedTags = Utils::AddNonOverlappingTags(ExistingAssetData, NewAssetData);
|
|
if (ModifiedTags)
|
|
{
|
|
State.SetTagsOnExistingAsset(&ExistingAssetData, MoveTemp(*ModifiedTags));
|
|
if (!ShouldSkipAsset(ExistingAssetData.AssetClassPath, ExistingAssetData.PackageFlags))
|
|
{
|
|
EventContext.AssetEvents.Emplace(ExistingAssetData, Impl::FEventContext::EEvent::Updated);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FAssetRegistryImpl::RemoveAssetData(Impl::FEventContext& EventContext, FAssetData* AssetData)
|
|
{
|
|
if (!ensure(AssetData))
|
|
{
|
|
return false;
|
|
}
|
|
return RemoveAssetDatas(EventContext, { &AssetData, 1 });
|
|
}
|
|
|
|
bool FAssetRegistryImpl::RemoveAssetDatas(Impl::FEventContext& EventContext, TArrayView<FAssetData*> AssetDatas)
|
|
{
|
|
#if WITH_EDITOR
|
|
TArray<FSoftObjectPath, TInlineAllocator<1>> RedirectSources;
|
|
RedirectSources.Reserve(AssetDatas.Num());
|
|
#endif
|
|
|
|
for (FAssetData* AssetData : AssetDatas)
|
|
{
|
|
if (!ShouldSkipAsset(AssetData->AssetClassPath, AssetData->PackageFlags))
|
|
{
|
|
EventContext.AssetEvents.Emplace(*AssetData, Impl::FEventContext::EEvent::Removed);
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
// Save a copy of the AssetData's SoftObjectPath if we need to remove it; we don't want to remove the
|
|
// redirection unless the AssetRegistryState confirms the AssetData existed and was removed, but after
|
|
// it is removed our AssetData pointer might become a dangling pointer.
|
|
FSoftObjectPath RedirectSource = AssetData->IsRedirector() ? AssetData->GetSoftObjectPath() : FSoftObjectPath();
|
|
RedirectSources.Add(RedirectSource);
|
|
#endif
|
|
|
|
// Remove from the class map if removing a blueprint
|
|
if (ClassGeneratorNames.Contains(AssetData->AssetClassPath))
|
|
{
|
|
const FString OldGeneratedClass = AssetData->GetTagValueRef<FString>(FBlueprintTags::GeneratedClassPath);
|
|
if (!OldGeneratedClass.IsEmpty() && OldGeneratedClass != TEXTVIEW("None"))
|
|
{
|
|
const FTopLevelAssetPath OldGeneratedClassPathName(FPackageName::ExportTextPathToObjectPath(OldGeneratedClass));
|
|
if (ensureAlwaysMsgf(!OldGeneratedClassPathName.IsNull(),
|
|
TEXT("Short class name used: OldGeneratedClass=%s"), *OldGeneratedClass))
|
|
{
|
|
CachedBPInheritanceMap.Remove(OldGeneratedClassPathName);
|
|
|
|
// Invalidate caching because CachedBPInheritanceMap got modified
|
|
TempCachedInheritanceBuffer.bDirty = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TBitArray<FDefaultBitArrayAllocator> bRemoved;
|
|
TBitArray<FDefaultBitArrayAllocator> bRemovedDependencyData;
|
|
bRemoved.SetNumUninitialized(AssetDatas.Num());
|
|
bRemovedDependencyData.SetNumUninitialized(AssetDatas.Num());
|
|
State.RemoveAssetDatas(AssetDatas, true /* bRemoveDependencyData */, bRemoved, bRemovedDependencyData);
|
|
|
|
#if WITH_EDITOR
|
|
for (TBitArray<FDefaultBitArrayAllocator>::FConstIterator It(bRemoved); It; ++It)
|
|
{
|
|
if (It.GetValue())
|
|
{
|
|
// Update Redirectors
|
|
FSoftObjectPath& RedirectSource = RedirectSources[It.GetIndex()];
|
|
if (RedirectSource.IsValid())
|
|
{
|
|
GRedirectCollector.RemoveAssetPathRedirection(RedirectSource);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return bRemoved.Contains(true);
|
|
}
|
|
|
|
void FAssetRegistryImpl::RemovePackageData(Impl::FEventContext& EventContext, const FName PackageName)
|
|
{
|
|
// Even if we could point to the array, we have to copy the array since RemoveAssetData may re-allocate it.
|
|
TArray<FAssetData*, TInlineAllocator<1>> PackageAssets;
|
|
State.EnumerateMutableAssetsByPackageName(PackageName, [&PackageAssets](FAssetData* AssetData)
|
|
{
|
|
PackageAssets.Add(AssetData);
|
|
return true;
|
|
});
|
|
|
|
if (PackageAssets.Num() > 0)
|
|
{
|
|
FAssetIdentifier PackageAssetIdentifier(PackageName);
|
|
// If there were any EDependencyCategory::Package referencers, re-add them to a new empty dependency node, as it would be when the referencers are loaded from disk
|
|
// We do not have to handle SearchableName or Manage referencers, because those categories of dependencies are not created for non-existent AssetIdentifiers
|
|
TArray<TPair<FAssetIdentifier, FDependsNode::FPackageFlagSet>> PackageReferencers;
|
|
{
|
|
FDependsNode** FoundPtr = State.CachedDependsNodes.Find(PackageAssetIdentifier);
|
|
FDependsNode* DependsNode = FoundPtr ? *FoundPtr : nullptr;
|
|
if (DependsNode)
|
|
{
|
|
DependsNode->GetPackageReferencers(PackageReferencers);
|
|
}
|
|
}
|
|
|
|
for (FAssetData* PackageAsset : PackageAssets)
|
|
{
|
|
RemoveAssetData(EventContext, PackageAsset);
|
|
}
|
|
|
|
// Readd any referencers, creating an empty DependsNode to hold them
|
|
if (PackageReferencers.Num())
|
|
{
|
|
FDependsNode* NewNode = State.CreateOrFindDependsNode(PackageAssetIdentifier);
|
|
for (TPair<FAssetIdentifier, FDependsNode::FPackageFlagSet>& Pair : PackageReferencers)
|
|
{
|
|
FDependsNode* ReferencerNode = State.CreateOrFindDependsNode(Pair.Key);
|
|
if (ReferencerNode != nullptr)
|
|
{
|
|
ReferencerNode->AddPackageDependencySet(NewNode, Pair.Value);
|
|
NewNode->AddReferencer(ReferencerNode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::AddVerseFile(Impl::FEventContext& EventContext, FName VerseFilePathToAdd)
|
|
{
|
|
bool bAlreadyExists = false;
|
|
CachedVerseFiles.Add(VerseFilePathToAdd, &bAlreadyExists);
|
|
if (!bAlreadyExists)
|
|
{
|
|
FName VerseDirectoryPath(FPathViews::GetPath(WriteToString<256>(VerseFilePathToAdd)));
|
|
|
|
// Ensure this path is represented in the CachedPathTree
|
|
AddPath(EventContext, WriteToString<256>(VerseDirectoryPath));
|
|
|
|
TArray<FName>& FilePathsArray = CachedVerseFilesByPath.FindOrAdd(VerseDirectoryPath);
|
|
FilePathsArray.Add(VerseFilePathToAdd);
|
|
EventContext.VerseEvents.Emplace(VerseFilePathToAdd, Impl::FEventContext::EEvent::Added);
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::RemoveVerseFile(Impl::FEventContext& EventContext, FName VerseFilePathToRemove)
|
|
{
|
|
if (CachedVerseFiles.Remove(VerseFilePathToRemove))
|
|
{
|
|
FName VerseDirectoryPath(FPathViews::GetPath(WriteToString<256>(VerseFilePathToRemove)));
|
|
TArray<FName>* FilePathsArray = CachedVerseFilesByPath.Find(VerseDirectoryPath);
|
|
if (ensure(FilePathsArray)) // We found it in CachedVerseFiles, so we must also find it here
|
|
{
|
|
FilePathsArray->Remove(VerseFilePathToRemove);
|
|
if (FilePathsArray->IsEmpty())
|
|
{
|
|
CachedVerseFilesByPath.Remove(VerseDirectoryPath);
|
|
|
|
// Try to remove this path from the general CachedPathTree - assuming no other files are keeping it around
|
|
RemoveAssetPath(EventContext, VerseDirectoryPath);
|
|
}
|
|
}
|
|
EventContext.VerseEvents.Emplace(VerseFilePathToRemove, Impl::FEventContext::EEvent::Removed);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
|
|
void UAssetRegistryImpl::OnDirectoryChanged(const TArray<FFileChangeData>& FileChanges)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UAssetRegistryImpl::OnDirectoryChanged);
|
|
|
|
double StartTime = FPlatformTime::Seconds();
|
|
|
|
// Take local copy of FileChanges array as we wish to collapse pairs of 'Removed then Added' FileChangeData
|
|
// entries into a single 'Modified' entry.
|
|
TArray<FFileChangeData> FileChangesProcessed(FileChanges);
|
|
|
|
for (int32 FileEntryIndex = 0; FileEntryIndex < FileChangesProcessed.Num(); FileEntryIndex++)
|
|
{
|
|
if (FileChangesProcessed[FileEntryIndex].Action == FFileChangeData::FCA_Added)
|
|
{
|
|
// Search back through previous entries to see if this Added can be paired with a previous Removed
|
|
const FString& FilenameToCompare = FileChangesProcessed[FileEntryIndex].Filename;
|
|
for (int32 SearchIndex = FileEntryIndex - 1; SearchIndex >= 0; SearchIndex--)
|
|
{
|
|
if (FileChangesProcessed[SearchIndex].Action == FFileChangeData::FCA_Removed &&
|
|
FileChangesProcessed[SearchIndex].Filename == FilenameToCompare)
|
|
{
|
|
// Found a Removed which matches the Added - change the Added file entry to be a Modified...
|
|
FileChangesProcessed[FileEntryIndex].Action = FFileChangeData::FCA_Modified;
|
|
|
|
// ...and remove the Removed entry
|
|
FileChangesProcessed.RemoveAt(SearchIndex);
|
|
FileEntryIndex--;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
// Check that the change is related to a directory that has actually been mounted.
|
|
FStringBuilderBase MountPointPackageName;
|
|
FStringBuilderBase MountPointFilePath;
|
|
FStringBuilderBase RelativePath;
|
|
for (int32 FileEntryIndex = FileChangesProcessed.Num() - 1; FileEntryIndex >= 0; FileEntryIndex--)
|
|
{
|
|
FFileChangeData& Data = FileChangesProcessed[FileEntryIndex];
|
|
if (Data.Action != FFileChangeData::FCA_RescanRequired && !FPackageName::TryGetMountPointForPath(
|
|
Data.Filename, MountPointPackageName, MountPointFilePath, RelativePath))
|
|
{
|
|
FileChangesProcessed.RemoveAt(FileEntryIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
bool bInitialSearchStarted;
|
|
bool bInitialSearchCompleted;
|
|
bool bAdditionalMountSearchInProgress;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
bInitialSearchStarted = GuardedData.IsInitialSearchStarted();
|
|
bInitialSearchCompleted = GuardedData.IsInitialSearchCompleted();
|
|
bAdditionalMountSearchInProgress = GuardedData.IsAdditionalMountSearchInProgress();
|
|
UE::AssetRegistry::Impl::FClassInheritanceContext InheritanceContext;
|
|
UE::AssetRegistry::Impl::FClassInheritanceBuffer InheritanceBuffer;
|
|
GetInheritanceContextWithRequiredLock(InterfaceScopeLock, InheritanceContext, InheritanceBuffer);
|
|
GuardedData.OnDirectoryChanged(InterfaceScopeLock, EventContext, InheritanceContext, FileChangesProcessed);
|
|
}
|
|
Broadcast(EventContext);
|
|
|
|
FTelemetryRouter::Get().ProvideTelemetry<UE::Telemetry::AssetRegistry::FDirectoryWatcherUpdateTelemetry>({
|
|
FileChanges,
|
|
FPlatformTime::Seconds() - StartTime,
|
|
bInitialSearchStarted,
|
|
bInitialSearchCompleted,
|
|
bAdditionalMountSearchInProgress,
|
|
});
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::OnDirectoryChanged(FInterfaceWriteScopeLock& ScopeLock, Impl::FEventContext& EventContext,
|
|
Impl::FClassInheritanceContext& InheritanceContext, TArray<FFileChangeData>& FileChangesProcessed)
|
|
{
|
|
TArray<FString> NewDirs;
|
|
TArray<FString> NewFiles;
|
|
TArray<FString> ModifiedFiles;
|
|
for (int32 FileIdx = 0; FileIdx < FileChangesProcessed.Num(); ++FileIdx)
|
|
{
|
|
if (FileChangesProcessed[FileIdx].Action == FFileChangeData::FCA_RescanRequired)
|
|
{
|
|
if (bInitialSearchStarted && !IsInitialSearchCompleted()) // Initial search only
|
|
{
|
|
// Ignore rescan request during initial scan as it is probably caused by the scan itself
|
|
UE_LOG(LogAssetRegistry, Log, TEXT("FAssetRegistry ignoring rescan request for %s during startup"), *FileChangesProcessed[FileIdx].Filename);
|
|
}
|
|
else
|
|
{
|
|
OnDirectoryRescanRequired(ScopeLock, EventContext, InheritanceContext, FileChangesProcessed[FileIdx].Filename,
|
|
FileChangesProcessed[FileIdx].TimeStamp);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
FString LongPackageName;
|
|
const FString File = FString(FileChangesProcessed[FileIdx].Filename);
|
|
const bool bIsPackageFile = FPackageName::IsPackageExtension(*FPaths::GetExtension(File, true));
|
|
const bool bIsValidPackageName = FPackageName::TryConvertFilenameToLongPackageName(
|
|
File,
|
|
LongPackageName,
|
|
/*OutFailureReason*/ nullptr,
|
|
/* Verse files can be of the wildcard pattern `*.*.verse`. */
|
|
FAssetDataGatherer::IsVerseFile(File) && !bIsPackageFile ? FPackageName::EConvertFlags::AllowDots : FPackageName::EConvertFlags::None);
|
|
const bool bIsValidPackage = bIsPackageFile && bIsValidPackageName;
|
|
|
|
if (bIsValidPackage)
|
|
{
|
|
FName LongPackageFName(*LongPackageName);
|
|
|
|
bool bAddedOrCreated = false;
|
|
switch (FileChangesProcessed[FileIdx].Action)
|
|
{
|
|
case FFileChangeData::FCA_Added:
|
|
// This is a package file that was created on disk. Mark it to be scanned for asset data.
|
|
NewFiles.AddUnique(File);
|
|
bAddedOrCreated = true;
|
|
UE_LOG(LogAssetRegistry, Verbose, TEXT("File was added to content directory: %s"), *File);
|
|
break;
|
|
|
|
case FFileChangeData::FCA_Modified:
|
|
// This is a package file that changed on disk. Mark it to be scanned immediately for new or removed asset data.
|
|
ModifiedFiles.AddUnique(File);
|
|
bAddedOrCreated = true;
|
|
UE_LOG(LogAssetRegistry, Verbose, TEXT("File changed in content directory: %s"), *File);
|
|
break;
|
|
|
|
case FFileChangeData::FCA_Removed:
|
|
// This file was deleted. Remove all assets in the package from the registry.
|
|
RemovePackageData(EventContext, LongPackageFName);
|
|
// If the package was a package we were tracking as empty (due to e.g. a rename in editor), remove it.
|
|
// Disk now matches editor
|
|
RemoveEmptyPackage(LongPackageFName);
|
|
UE_LOG(LogAssetRegistry, Verbose, TEXT("File was removed from content directory: %s"), *File);
|
|
break;
|
|
}
|
|
if (bAddedOrCreated && CachedEmptyPackages.Contains(LongPackageFName))
|
|
{
|
|
UE_LOG(LogAssetRegistry, Warning, TEXT("%s: package was marked as deleted in editor, but has been modified on disk. It will once again be returned from AssetRegistry queries."),
|
|
*File);
|
|
RemoveEmptyPackage(LongPackageFName);
|
|
}
|
|
}
|
|
else if (bIsValidPackageName)
|
|
{
|
|
// Is this a Verse file?
|
|
if (FAssetDataGatherer::IsVerseFile(File))
|
|
{
|
|
switch (FileChangesProcessed[FileIdx].Action)
|
|
{
|
|
case FFileChangeData::FCA_Added:
|
|
// This is a Verse file that was created on disk.
|
|
NewFiles.AddUnique(File);
|
|
UE_LOG(LogAssetRegistry, Verbose, TEXT("Verse file was added to content directory: %s"), *File);
|
|
break;
|
|
|
|
case FFileChangeData::FCA_Modified:
|
|
// Note: Since content of Verse files is not scanned, no need to handle FCA_Modified
|
|
break;
|
|
|
|
case FFileChangeData::FCA_Removed:
|
|
RemoveVerseFile(EventContext, FName(WriteToString<256>(LongPackageName, FPathViews::GetExtension(File, /*bIncludeDot*/ true))));
|
|
UE_LOG(LogAssetRegistry, Verbose, TEXT("Verse file was removed from content directory: %s"), *File);
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// This could be a directory or possibly a file with no extension or a wrong extension.
|
|
// No guaranteed way to know at this point since it may have been deleted.
|
|
switch (FileChangesProcessed[FileIdx].Action)
|
|
{
|
|
case FFileChangeData::FCA_Added:
|
|
{
|
|
if (FPaths::DirectoryExists(File))
|
|
{
|
|
NewDirs.Add(File);
|
|
UE_LOG(LogAssetRegistry, Verbose, TEXT("Directory was added to content directory: %s"), *File);
|
|
}
|
|
break;
|
|
}
|
|
case FFileChangeData::FCA_Removed:
|
|
{
|
|
FName Path(UE::String::RemoveFromEnd(FStringView(LongPackageName), TEXTVIEW("/")));
|
|
RemoveAssetPath(EventContext, Path);
|
|
UE_LOG(LogAssetRegistry, Verbose, TEXT("Directory was removed from content directory: %s"), *File);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
if (bIsValidPackageName)
|
|
{
|
|
// If a package changes in a referenced directory, modify the Assets that monitor that directory
|
|
FString ParentDirectory = UE::AssetRegistry::CreateStandardFilename(FPaths::GetPath(File));
|
|
TArray<FName, TInlineAllocator<1>> WatcherPackageNames;
|
|
while (!ParentDirectory.IsEmpty())
|
|
{
|
|
TSet<FName>* PackagesWatchingThisDirectory = PackagesWatchingDirectory.Find(ParentDirectory);
|
|
if (PackagesWatchingThisDirectory)
|
|
{
|
|
for (FName PackageWatchingThisDirectory : *PackagesWatchingThisDirectory)
|
|
{
|
|
WatcherPackageNames.Add(PackageWatchingThisDirectory);
|
|
}
|
|
}
|
|
FString NewParentDirectory = FPaths::GetPath(ParentDirectory);
|
|
if (ParentDirectory == NewParentDirectory)
|
|
{
|
|
break;
|
|
}
|
|
ParentDirectory = MoveTemp(NewParentDirectory);
|
|
}
|
|
Algo::Sort(WatcherPackageNames, FNameFastLess());
|
|
WatcherPackageNames.SetNum(Algo::Unique(WatcherPackageNames));
|
|
|
|
for (FName WatcherPackageName : WatcherPackageNames)
|
|
{
|
|
// ScanModifiedAssetFiles accepts LongPackageNames as well as LocalPaths
|
|
ModifiedFiles.AddUnique(WatcherPackageName.ToString());
|
|
}
|
|
}
|
|
#endif
|
|
|
|
}
|
|
|
|
if (NewFiles.Num() || NewDirs.Num())
|
|
{
|
|
if (GlobalGatherer.IsValid())
|
|
{
|
|
for (FString& NewDir : NewDirs)
|
|
{
|
|
GlobalGatherer->OnDirectoryCreated(NewDir);
|
|
}
|
|
GlobalGatherer->OnFilesCreated(NewFiles);
|
|
if (GlobalGatherer->IsSynchronous())
|
|
{
|
|
Impl::FScanPathContext Context(EventContext, InheritanceContext, NewDirs, NewFiles,
|
|
UE::AssetRegistry::EScanFlags::None, nullptr /* OutFoundAssets */);
|
|
ScanPathsSynchronous(&ScopeLock, Context);
|
|
}
|
|
}
|
|
}
|
|
ScanModifiedAssetFiles(ScopeLock, EventContext, InheritanceContext, ModifiedFiles, UE::AssetRegistry::EScanFlags::None);
|
|
}
|
|
|
|
void FAssetRegistryImpl::OnDirectoryRescanRequired(FInterfaceWriteScopeLock& ScopeLock, Impl::FEventContext& EventContext,
|
|
Impl::FClassInheritanceContext& InheritanceContext, FString& DirPath, int64 BeforeTimeStamp)
|
|
{
|
|
TArray<TPair<FString,FString>> DirPathsAndPackageNames;
|
|
FString DirPathAsPackageName;
|
|
FString NormalizedDirPath = UE::AssetRegistry::CreateStandardFilename(DirPath);
|
|
if (FPackageName::TryConvertFilenameToLongPackageName(NormalizedDirPath, DirPathAsPackageName))
|
|
{
|
|
DirPathsAndPackageNames.Emplace(DirPath, MoveTemp(DirPathAsPackageName));
|
|
}
|
|
else
|
|
{
|
|
TArray<FString> ContentRoots;
|
|
FPackageName::QueryRootContentPaths(ContentRoots);
|
|
TStringBuilder<64> UnusedPackageName;
|
|
TStringBuilder<256> MountedFilePath;
|
|
TStringBuilder<16> UnusedRelPath;
|
|
for (FString& MountedLongPackageName : ContentRoots)
|
|
{
|
|
if (FPackageName::TryGetMountPointForPath(MountedLongPackageName, UnusedPackageName, MountedFilePath, UnusedRelPath))
|
|
{
|
|
FString NormalizeMountedFilePath = UE::AssetRegistry::CreateStandardFilename(FString(MountedFilePath));
|
|
if (FPaths::IsUnderDirectory(NormalizeMountedFilePath, NormalizedDirPath))
|
|
{
|
|
DirPathsAndPackageNames.Emplace(MoveTemp(NormalizeMountedFilePath), MoveTemp(MountedLongPackageName));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (DirPathsAndPackageNames.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
struct FDirectoryResults
|
|
{
|
|
TArray<FString> NewFiles;
|
|
TArray<FString> ModifiedFiles;
|
|
TSet<FName> RemovedLongPackageNames;
|
|
};
|
|
int32 NumDirs = DirPathsAndPackageNames.Num();
|
|
TArray<FDirectoryResults> Results;
|
|
Results.SetNum(NumDirs);
|
|
FDateTime BeforeDateTime = FDateTime::FromUnixTimestamp(BeforeTimeStamp);
|
|
|
|
for (int32 DirIndex = 0; DirIndex < NumDirs; ++DirIndex)
|
|
{
|
|
FString& PackageNamePath = DirPathsAndPackageNames[DirIndex].Value;
|
|
FDirectoryResults& Result = Results[DirIndex];
|
|
EnumerateAssetsByPathNoTags(*PackageNamePath, [&Result](const FAssetData& AssetData)
|
|
{
|
|
Result.RemovedLongPackageNames.Add(AssetData.PackageName);
|
|
return true;
|
|
}, true /* bRecursive */, true /* bIncludeOnlyOnDiskAssets */);
|
|
}
|
|
|
|
ParallelFor(NumDirs, [this, &DirPathsAndPackageNames, &Results, &BeforeDateTime](int32 DirIndex)
|
|
{
|
|
FDirectoryResults& Result = Results[DirIndex];
|
|
TPair<FString, FString>& Pair = DirPathsAndPackageNames[DirIndex];
|
|
FString& LocalPath = Pair.Key;
|
|
FString& PackageNamePath = Pair.Value;
|
|
|
|
FPackageName::IteratePackagesInDirectory(LocalPath,
|
|
[&LocalPath, &PackageNamePath, &BeforeDateTime, &Result]
|
|
(const TCHAR* Filename, const FFileStatData& StatData)
|
|
{
|
|
// Convert Filename to a PackagePath. We know the base dir so its faster to use that than FPackageName
|
|
// which has to scan all mount dirs
|
|
FStringView RelPath;
|
|
FString NormalizedFilename = UE::AssetRegistry::CreateStandardFilename(Filename);
|
|
if (!FPathViews::TryMakeChildPathRelativeTo(NormalizedFilename, LocalPath, RelPath))
|
|
{
|
|
return true;
|
|
}
|
|
const bool bIsPackageFile = FPackageName::IsPackageExtension(
|
|
*FString(FPathViews::GetExtension(RelPath, true /* bIncludeDot */)));
|
|
RelPath = FPathViews::GetBaseFilenameWithPath(RelPath);
|
|
TStringBuilder<256> FilePackagePath;
|
|
FilePackagePath << PackageNamePath;
|
|
FPathViews::AppendPath(FilePackagePath, RelPath);
|
|
for (int32 Index = 0; Index < FilePackagePath.Len(); ++Index)
|
|
{
|
|
TCHAR& Char = FilePackagePath.GetData()[Index];
|
|
if (Char == '\\')
|
|
{
|
|
Char = '/';
|
|
}
|
|
}
|
|
const bool bIsValidPackageName = FPackageName::IsValidTextForLongPackageName(FilePackagePath);
|
|
if (!bIsPackageFile || !bIsValidPackageName)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (StatData.CreationTime > BeforeDateTime)
|
|
{
|
|
Result.NewFiles.Add(NormalizedFilename);
|
|
}
|
|
else if (StatData.ModificationTime > BeforeDateTime)
|
|
{
|
|
Result.ModifiedFiles.Add(NormalizedFilename);
|
|
}
|
|
Result.RemovedLongPackageNames.Remove(FName(FilePackagePath.ToView()));
|
|
|
|
return true;
|
|
});
|
|
});
|
|
|
|
TArray<FName> FinalRemovedLongPackageNames;
|
|
FDirectoryResults& FinalResult = Results[0];
|
|
FinalRemovedLongPackageNames.Append(FinalResult.RemovedLongPackageNames.Array());
|
|
for (int32 DirIndex = 1; DirIndex < NumDirs; ++DirIndex)
|
|
{
|
|
FDirectoryResults& ResultToMerge = Results[DirIndex];
|
|
FinalResult.NewFiles.Append(MoveTemp(ResultToMerge.NewFiles));
|
|
FinalResult.ModifiedFiles.Append(MoveTemp(ResultToMerge.ModifiedFiles));
|
|
FinalRemovedLongPackageNames.Append(ResultToMerge.RemovedLongPackageNames.Array());
|
|
}
|
|
|
|
for (FName LongPackageName : FinalRemovedLongPackageNames)
|
|
{
|
|
// This file was deleted. Remove all assets in the package from the registry.
|
|
RemovePackageData(EventContext, LongPackageName);
|
|
// If the package was a package we were tracking as empty (due to e.g. a rename in editor), remove it.
|
|
// Disk now matches editor
|
|
RemoveEmptyPackage(LongPackageName);
|
|
}
|
|
if (FinalResult.NewFiles.Num())
|
|
{
|
|
if (GlobalGatherer.IsValid())
|
|
{
|
|
GlobalGatherer->OnFilesCreated(FinalResult.NewFiles);
|
|
if (GlobalGatherer->IsSynchronous())
|
|
{
|
|
TArray<FString> UnusedNewDirs;
|
|
Impl::FScanPathContext Context(EventContext, InheritanceContext, UnusedNewDirs, FinalResult.NewFiles,
|
|
UE::AssetRegistry::EScanFlags::None, nullptr /* OutFoundAssets */);
|
|
ScanPathsSynchronous(&ScopeLock, Context);
|
|
}
|
|
}
|
|
}
|
|
ScanModifiedAssetFiles(ScopeLock, EventContext, InheritanceContext, FinalResult.ModifiedFiles, UE::AssetRegistry::EScanFlags::None);
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::OnAssetLoaded(UObject *AssetLoaded)
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.AddLoadedAssetToProcess(*AssetLoaded);
|
|
}
|
|
|
|
void UAssetRegistryImpl::ProcessLoadedAssetsToUpdateCache(UE::AssetRegistry::Impl::FEventContext& EventContext,
|
|
UE::AssetRegistry::Impl::EGatherStatus Status, UE::AssetRegistry::Impl::FInterruptionContext& InOutInterruptionContext)
|
|
{
|
|
// Note this function can be reentered due to arbitrary code execution in construction of FAssetData
|
|
if (!IsInGameThread())
|
|
{
|
|
// Calls to GetAssetRegistryTags are only allowed on the GameThread
|
|
return;
|
|
}
|
|
|
|
// Early exit to save cputime if we're still processing cache data
|
|
if (IsTickActive(Status) && InOutInterruptionContext.IsTimeSlicingEnabled())
|
|
{
|
|
return;
|
|
}
|
|
|
|
constexpr int32 BatchSize = 16;
|
|
TArray<const UObject*> BatchObjects;
|
|
TArray<FAssetData, TInlineAllocator<BatchSize>> BatchAssetDatas;
|
|
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.GetProcessLoadedAssetsBatch(BatchObjects, BatchSize, bUpdateDiskCacheAfterLoad);
|
|
if (BatchObjects.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Refreshes ClassGeneratorNames if out of date due to module load
|
|
GuardedData.CollectCodeGeneratorClasses();
|
|
}
|
|
|
|
while (BatchObjects.Num() > 0)
|
|
{
|
|
bool bTimedOut = false;
|
|
int32 CurrentBatchSize = BatchObjects.Num();
|
|
BatchAssetDatas.Reset(CurrentBatchSize);
|
|
int32 Index = 0;
|
|
while (Index < CurrentBatchSize)
|
|
{
|
|
const UObject* LoadedObject = BatchObjects[Index++];
|
|
if (!LoadedObject->IsAsset())
|
|
{
|
|
// If the object has changed and is no longer an asset, ignore it. This can happen when an Actor is modified during cooking to no longer have an external package
|
|
continue;
|
|
}
|
|
BatchAssetDatas.Add(FAssetData(LoadedObject, FAssetData::ECreationFlags::AllowBlueprintClass,
|
|
EAssetRegistryTagsCaller::AssetRegistryLoad));
|
|
|
|
// Check to see if we have run out of time in this tick
|
|
if (InOutInterruptionContext.ShouldExitEarly())
|
|
{
|
|
bTimedOut = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.PushProcessLoadedAssetsBatch(EventContext, BatchAssetDatas,
|
|
TArrayView<const UObject*>(BatchObjects).Slice(Index, CurrentBatchSize-Index));
|
|
if (bTimedOut)
|
|
{
|
|
break;
|
|
}
|
|
GuardedData.GetProcessLoadedAssetsBatch(BatchObjects, BatchSize, bUpdateDiskCacheAfterLoad);
|
|
}
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::AddLoadedAssetToProcess(const UObject& AssetLoaded)
|
|
{
|
|
// Make sure the loaded asset is from a monitored path
|
|
if (GlobalGatherer.IsValid())
|
|
{
|
|
FString LocalPath;
|
|
if (!FPackageName::TryConvertLongPackageNameToFilename(AssetLoaded.GetPackage()->GetName(), LocalPath))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!GlobalGatherer->IsMonitored(LocalPath))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
LoadedAssetsToProcess.Add(&AssetLoaded);
|
|
}
|
|
|
|
void FAssetRegistryImpl::GetProcessLoadedAssetsBatch(TArray<const UObject*>& OutLoadedAssets, uint32 BatchSize,
|
|
bool bUpdateDiskCacheAfterLoad)
|
|
{
|
|
if (!GlobalGatherer.IsValid() || !bUpdateDiskCacheAfterLoad)
|
|
{
|
|
OutLoadedAssets.Reset();
|
|
return;
|
|
}
|
|
|
|
OutLoadedAssets.Reset(BatchSize);
|
|
while (!LoadedAssetsToProcess.IsEmpty() && OutLoadedAssets.Num() < static_cast<int32>(BatchSize))
|
|
{
|
|
const UObject* LoadedAsset = LoadedAssetsToProcess.PopFrontValue().Get();
|
|
if (!LoadedAsset)
|
|
{
|
|
// This could be null, in which case it already got freed, ignore
|
|
continue;
|
|
}
|
|
|
|
// Take a new snapshot of the asset's data every time it loads or saves
|
|
|
|
UPackage* InMemoryPackage = LoadedAsset->GetOutermost();
|
|
if (InMemoryPackage->IsDirty())
|
|
{
|
|
// Package is dirty, which means it has changes other than just a PostLoad
|
|
// In editor, ignore the update of the asset; it will be updated when saved
|
|
// In the cook commandlet, in which editoruser-created changes are impossible, do the update anyway.
|
|
// Occurrences of IsDirty in the cook commandlet are spurious and a code bug.
|
|
if (!IsRunningCookCommandlet())
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
OutLoadedAssets.Add(LoadedAsset);
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::PushProcessLoadedAssetsBatch(Impl::FEventContext& EventContext,
|
|
TArrayView<FAssetData> LoadedAssetDatas, TArrayView<const UObject*> UnprocessedFromBatch)
|
|
{
|
|
// Add or update existing for all of the AssetDatas created by the batch
|
|
for (FAssetData& NewAssetData : LoadedAssetDatas)
|
|
{
|
|
if (ShouldSkipGatheredAsset(NewAssetData))
|
|
{
|
|
continue;
|
|
}
|
|
FCachedAssetKey Key(NewAssetData);
|
|
FAssetData* DataFromGather = State.GetMutableAssetByObjectPath(Key);
|
|
|
|
AssetDataObjectPathsUpdatedOnLoad.Add(NewAssetData.GetSoftObjectPath());
|
|
|
|
if (!DataFromGather)
|
|
{
|
|
FAssetData* ClonedAssetData = new FAssetData(MoveTemp(NewAssetData));
|
|
AddAssetData(EventContext, ClonedAssetData);
|
|
}
|
|
else
|
|
{
|
|
// When updating disk-based AssetData with the AssetData from a loaded UObject, we keep
|
|
// existing tags from disk even if they are not returned from the
|
|
// GetAssetRegistryTags(EAssetRegistryTagsCaller::AssetRegistryLoad) function on the loaded UObject.
|
|
// We do this because the tags might be tags that are only calculated during
|
|
// GetAssetRegistryTags(EAssetRegistryTagsCaller::SavePackage).
|
|
// Modified tag values on the other hand do overwrite the old values from disk.
|
|
// This means that the only way to delete no-longer present tags from an AssetData
|
|
// is to resave the package, or to manually call AssetUpdateTags(EAssetRegistryTagsCaller::FullUpdate) from c++.
|
|
UpdateAssetData(EventContext, DataFromGather, MoveTemp(NewAssetData), true /* bKeepDeletedTags */);
|
|
}
|
|
}
|
|
|
|
// Push back any objects from the batch that were not processed due to timing out
|
|
for (const UObject* Obj : ReverseIterate(UnprocessedFromBatch))
|
|
{
|
|
LoadedAssetsToProcess.EmplaceFront(Obj);
|
|
}
|
|
}
|
|
|
|
bool FAssetRegistryImpl::IsLoadedAndTrackedAsset(const FSoftObjectPath& InObjectPath) const
|
|
{
|
|
// Must be in our "updated on load" set
|
|
if (!AssetDataObjectPathsUpdatedOnLoad.Contains(InObjectPath))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Must also be loaded in memory
|
|
if (InObjectPath.ResolveObject() == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
#endif // WITH_EDITOR
|
|
|
|
void UAssetRegistryImpl::ScanModifiedAssetFiles(const TArray<FString>& InFilePaths)
|
|
{
|
|
ScanModifiedAssetFiles(InFilePaths, UE::AssetRegistry::EScanFlags::None);
|
|
}
|
|
|
|
void UAssetRegistryImpl::ScanModifiedAssetFiles(const TArray<FString>& InFilePaths, UE::AssetRegistry::EScanFlags ScanFlags)
|
|
{
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
UE::AssetRegistry::Impl::FClassInheritanceContext InheritanceContext;
|
|
UE::AssetRegistry::Impl::FClassInheritanceBuffer InheritanceBuffer;
|
|
GetInheritanceContextWithRequiredLock(InterfaceScopeLock, InheritanceContext, InheritanceBuffer);
|
|
GuardedData.ScanModifiedAssetFiles(InterfaceScopeLock, EventContext, InheritanceContext, InFilePaths, ScanFlags);
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
// Our caller expects up to date results after calling this function,
|
|
// but in-memory results will override the on-disk results we just scanned,
|
|
// and our in-memory results might be out of date due to being queued but not yet processed.
|
|
// So ProcessLoadedAssetsToUpdateCache before returning to make sure results are up to date.
|
|
UE::AssetRegistry::Impl::FInterruptionContext InterruptionContext;
|
|
ProcessLoadedAssetsToUpdateCache(EventContext, UE::AssetRegistry::Impl::EGatherStatus::Complete, InterruptionContext);
|
|
#endif
|
|
|
|
Broadcast(EventContext);
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::ScanModifiedAssetFiles(FInterfaceWriteScopeLock& ScopeLock, Impl::FEventContext& EventContext,
|
|
Impl::FClassInheritanceContext& InheritanceContext, const TArray<FString>& InFilePaths,
|
|
UE::AssetRegistry::EScanFlags InScanFlags)
|
|
{
|
|
if (InFilePaths.Num() > 0)
|
|
{
|
|
// ScanModifiedAssetFiles always does a force rescan of the given files
|
|
InScanFlags |= UE::AssetRegistry::EScanFlags::ForceRescan;
|
|
|
|
// Re-scan and update the asset registry with the new asset data
|
|
TArray<FSoftObjectPath> FoundAssets;
|
|
Impl::FScanPathContext Context(EventContext, InheritanceContext, TArray<FString>(), InFilePaths,
|
|
InScanFlags, &FoundAssets);
|
|
ScanPathsSynchronous(&ScopeLock, Context);
|
|
|
|
// Convert all the filenames to package names
|
|
TArray<FString> ModifiedPackageNames;
|
|
ModifiedPackageNames.Reserve(InFilePaths.Num());
|
|
for (const FString& File : InFilePaths)
|
|
{
|
|
ModifiedPackageNames.Add(FPackageName::FilenameToLongPackageName(File));
|
|
}
|
|
|
|
// Get the assets that are currently inside the package
|
|
TArray<FSoftObjectPath> ExistingAssetDatas;
|
|
ExistingAssetDatas.Reserve(InFilePaths.Num());
|
|
for (const FString& PackageName : ModifiedPackageNames)
|
|
{
|
|
TArray<const FAssetData*, TInlineAllocator<1>> PackageAssets;
|
|
State.EnumerateAssetsByPackageName(*PackageName, [&PackageAssets](const FAssetData* AssetData)
|
|
{
|
|
PackageAssets.Add(AssetData);
|
|
return true;
|
|
});
|
|
if (PackageAssets.Num() > 0)
|
|
{
|
|
ExistingAssetDatas.Reserve(ExistingAssetDatas.Num() + PackageAssets.Num());
|
|
for (const FAssetData* AssetData : PackageAssets)
|
|
{
|
|
ExistingAssetDatas.Add(AssetData->ToSoftObjectPath());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove any assets that are no longer present in the package
|
|
for (FSoftObjectPath& OldAssetPath : ExistingAssetDatas)
|
|
{
|
|
if (!FoundAssets.Contains(OldAssetPath))
|
|
{
|
|
FAssetData* OldAssetData = State.GetMutableAssetByObjectPath(OldAssetPath);
|
|
if (OldAssetData)
|
|
{
|
|
RemoveAssetData(EventContext, OldAssetData);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send ModifiedOnDisk event for every Asset that was modified
|
|
for (const FSoftObjectPath& FoundAsset : FoundAssets)
|
|
{
|
|
const FAssetData* AssetData = State.GetAssetByObjectPath(FCachedAssetKey(FoundAsset));
|
|
if (AssetData)
|
|
{
|
|
EventContext.AssetEvents.Emplace(*AssetData, Impl::FEventContext::EEvent::UpdatedOnDisk);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::OnContentPathMounted(const FString& InAssetPath, const FString& FileSystemPath)
|
|
{
|
|
// Sanitize
|
|
FString AssetPathWithTrailingSlash;
|
|
if (!InAssetPath.EndsWith(TEXT("/"), ESearchCase::CaseSensitive))
|
|
{
|
|
// We actually want a trailing slash here so the path can be properly converted while searching for assets
|
|
AssetPathWithTrailingSlash = InAssetPath + TEXT("/");
|
|
}
|
|
else
|
|
{
|
|
AssetPathWithTrailingSlash = InAssetPath;
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
FDirectoryWatcherModule& DirectoryWatcherModule = FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>(TEXT("DirectoryWatcher"));
|
|
IDirectoryWatcher* DirectoryWatcher = nullptr;
|
|
if (UE::AssetRegistry::Impl::IsDirectoryWatcherEnabled())
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
DirectoryWatcher = DirectoryWatcherModule.Get();
|
|
if (DirectoryWatcher)
|
|
{
|
|
// Make sure the directory exits on disk so that the OS level DirectoryWatcher can be used to monitor it.
|
|
IPlatformFile::GetPlatformPhysical().CreateDirectoryTree(*FileSystemPath);
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
UE::AssetRegistry::Impl::FClassInheritanceContext InheritanceContext;
|
|
UE::AssetRegistry::Impl::FClassInheritanceBuffer InheritanceBuffer;
|
|
GetInheritanceContextWithRequiredLock(InterfaceScopeLock, InheritanceContext, InheritanceBuffer);
|
|
GuardedData.OnContentPathMounted(InterfaceScopeLock, EventContext, InheritanceContext, InAssetPath, AssetPathWithTrailingSlash,
|
|
FileSystemPath);
|
|
|
|
// Listen for directory changes in this content path
|
|
#if WITH_EDITOR
|
|
const FString StandardFileSystemPath = UE::AssetRegistry::CreateStandardFilename(FileSystemPath);
|
|
// In-game doesn't listen for directory changes
|
|
if (DirectoryWatcher && !IsDirAlreadyWatchedByRootWatchers(StandardFileSystemPath))
|
|
{
|
|
if (!OnDirectoryChangedDelegateHandles.Contains(AssetPathWithTrailingSlash))
|
|
{
|
|
FDelegateHandle NewHandle;
|
|
DirectoryWatcher->RegisterDirectoryChangedCallback_Handle(
|
|
StandardFileSystemPath,
|
|
IDirectoryWatcher::FDirectoryChanged::CreateUObject(this, &UAssetRegistryImpl::OnDirectoryChanged),
|
|
NewHandle,
|
|
IDirectoryWatcher::WatchOptions::IncludeDirectoryChanges);
|
|
|
|
OnDirectoryChangedDelegateHandles.Add(AssetPathWithTrailingSlash, NewHandle);
|
|
}
|
|
}
|
|
#endif // WITH_EDITOR
|
|
}
|
|
|
|
Broadcast(EventContext);
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::OnContentPathMounted(UE::AssetRegistry::FInterfaceWriteScopeLock& ScopeLock, Impl::FEventContext& EventContext,
|
|
Impl::FClassInheritanceContext& InheritanceContext, const FString& InAssetPath,
|
|
const FString& AssetPathWithTrailingSlash, const FString& FileSystemPath)
|
|
{
|
|
// Content roots always exist
|
|
AddPath(EventContext, UE::String::RemoveFromEnd(FStringView(AssetPathWithTrailingSlash), TEXTVIEW("/")));
|
|
|
|
if (GlobalGatherer.IsValid() && bSearchAllAssets)
|
|
{
|
|
if (GlobalGatherer->IsSynchronous())
|
|
{
|
|
Impl::FScanPathContext Context(EventContext, InheritanceContext, { FileSystemPath }, TArray<FString>());
|
|
ScanPathsSynchronous(&ScopeLock, Context);
|
|
}
|
|
else
|
|
{
|
|
if (!IsGathering())
|
|
{
|
|
TRACE_BEGIN_REGION(TEXT("Asset Registry - Additional Mount Search")); // Matching TRACE_END_REGION in OnAdditionalMountSearchCompleted
|
|
AdditionalMountSearchStartTime = FPlatformTime::Seconds();
|
|
bAdditionalMountSearchInProgress.store(true, std::memory_order_relaxed);
|
|
GlobalGatherer->SetIsAdditionalMountSearchInProgress(true);
|
|
#if WITH_EDITOR
|
|
bGatherNeedsApplyObjectRedirects = true;
|
|
#endif
|
|
}
|
|
|
|
GlobalGatherer->AddMountPoint(FileSystemPath, InAssetPath);
|
|
GlobalGatherer->SetIsOnAllowList(FileSystemPath, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::OnContentPathDismounted(const FString& InAssetPath, const FString& FileSystemPath)
|
|
{
|
|
// Sanitize
|
|
FString AssetPathNoTrailingSlash = InAssetPath;
|
|
if (AssetPathNoTrailingSlash.EndsWith(TEXT("/"), ESearchCase::CaseSensitive))
|
|
{
|
|
// We don't want a trailing slash here as it could interfere with RemoveAssetPath
|
|
AssetPathNoTrailingSlash.LeftChopInline(1, EAllowShrinking::No);
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
FDirectoryWatcherModule& DirectoryWatcherModule = FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>(TEXT("DirectoryWatcher"));
|
|
IDirectoryWatcher* DirectoryWatcher = nullptr;
|
|
if (UE::AssetRegistry::Impl::IsDirectoryWatcherEnabled())
|
|
{
|
|
DirectoryWatcher = DirectoryWatcherModule.Get();
|
|
}
|
|
#endif
|
|
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.OnContentPathDismounted(EventContext, InAssetPath, AssetPathNoTrailingSlash, FileSystemPath);
|
|
|
|
// Stop listening for directory changes in this content path
|
|
#if WITH_EDITOR
|
|
const FString StandardFileSystemPath = UE::AssetRegistry::CreateStandardFilename(FileSystemPath);
|
|
if (DirectoryWatcher && !IsDirAlreadyWatchedByRootWatchers(StandardFileSystemPath))
|
|
{
|
|
// Make sure OnDirectoryChangedDelegateHandles key is symmetrical with the one used in OnContentPathMounted
|
|
FString AssetPathWithTrailingSlash;
|
|
if (!InAssetPath.EndsWith(TEXT("/"), ESearchCase::CaseSensitive))
|
|
{
|
|
AssetPathWithTrailingSlash = InAssetPath + TEXT("/");
|
|
}
|
|
else
|
|
{
|
|
AssetPathWithTrailingSlash = InAssetPath;
|
|
}
|
|
|
|
FDelegateHandle DirectoryChangedHandle;
|
|
if (ensure(OnDirectoryChangedDelegateHandles.RemoveAndCopyValue(AssetPathWithTrailingSlash, DirectoryChangedHandle)))
|
|
{
|
|
DirectoryWatcher->UnregisterDirectoryChangedCallback_Handle(StandardFileSystemPath, DirectoryChangedHandle);
|
|
}
|
|
}
|
|
#endif // WITH_EDITOR
|
|
}
|
|
Broadcast(EventContext);
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::OnContentPathDismounted(Impl::FEventContext& EventContext, const FString& InAssetPath,
|
|
const FString& AssetPathNoTrailingSlash, const FString& FileSystemPath)
|
|
{
|
|
if (GlobalGatherer.IsValid())
|
|
{
|
|
GlobalGatherer->RemoveMountPoint(FileSystemPath);
|
|
}
|
|
|
|
FName MountPoint = FName(FStringView(AssetPathNoTrailingSlash));
|
|
if (PersistentMountPoints.Contains(MountPoint))
|
|
{
|
|
// This path is marked to never remove its AssetDatas. Skip the code below to remove it.
|
|
return;
|
|
}
|
|
|
|
// Remove all cached assets and Verse files found at this location
|
|
{
|
|
FName AssetPathNoTrailingSlashFName(*AssetPathNoTrailingSlash);
|
|
TArray<FAssetData*> AllAssetDataToRemove;
|
|
TSet<FName> PathList;
|
|
const bool bRecurse = true;
|
|
CachedPathTree.GetSubPaths(AssetPathNoTrailingSlashFName, PathList, bRecurse);
|
|
PathList.Add(AssetPathNoTrailingSlashFName);
|
|
for (FName PathName : PathList)
|
|
{
|
|
// Gather assets
|
|
State.EnumerateMutableAssetsByPackagePath(PathName, [&AllAssetDataToRemove](FAssetData* AssetData)
|
|
{
|
|
AllAssetDataToRemove.Add(AssetData);
|
|
return true;
|
|
});
|
|
|
|
// Forget Verse files
|
|
const TArray<FName>* VerseFilesInPath = CachedVerseFilesByPath.Find(PathName);
|
|
if (VerseFilesInPath)
|
|
{
|
|
for (FName FilePath : *VerseFilesInPath)
|
|
{
|
|
CachedVerseFiles.Remove(FilePath);
|
|
}
|
|
CachedVerseFilesByPath.Remove(PathName);
|
|
}
|
|
}
|
|
|
|
RemoveAssetDatas(EventContext, AllAssetDataToRemove);
|
|
}
|
|
|
|
// Remove the root path
|
|
{
|
|
const bool bEvenIfAssetsStillExist = true;
|
|
RemoveAssetPath(EventContext, FName(*AssetPathNoTrailingSlash), bEvenIfAssetsStillExist);
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::UpdatePersistentMountPoints()
|
|
{
|
|
State.EnumerateAllPaths([this](FName Path)
|
|
{
|
|
TStringBuilder<256> PathString(InPlace, Path);
|
|
bool bHadClassesPrefix;
|
|
FStringView MountPoint = FPathViews::GetMountPointNameFromPath(PathString, &bHadClassesPrefix, false /* bInWithoutSlashes*/);
|
|
if (!MountPoint.IsEmpty() && !bHadClassesPrefix)
|
|
{
|
|
// Format returned by GetMountPointNameFromPath is e.g. /Engine, which is the format we need:
|
|
// LongPackageName with no trailing slash
|
|
PersistentMountPoints.Add(FName(MountPoint));
|
|
}
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::SetTemporaryCachingMode(bool bEnable)
|
|
{
|
|
checkf(IsInGameThread(), TEXT("Changing Caching mode is only available on the game thread because it affects behavior on all threads"));
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.SetTemporaryCachingMode(bEnable);
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::SetTemporaryCachingMode(bool bEnable)
|
|
{
|
|
if (bIsTempCachingAlwaysEnabled || bEnable == bIsTempCachingEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bIsTempCachingEnabled = bEnable;
|
|
TempCachedInheritanceBuffer.bDirty = true;
|
|
if (!bEnable)
|
|
{
|
|
TempCachedInheritanceBuffer.Clear();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::SetTemporaryCachingModeInvalidated()
|
|
{
|
|
checkf(IsInGameThread(), TEXT("Invalidating temporary cache is only available on the game thread because it affects behavior on all threads"));
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.SetTemporaryCachingModeInvalidated();
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::SetTemporaryCachingModeInvalidated()
|
|
{
|
|
TempCachedInheritanceBuffer.bDirty = true;
|
|
}
|
|
|
|
}
|
|
|
|
bool UAssetRegistryImpl::GetTemporaryCachingMode() const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
return GuardedData.IsTempCachingEnabled();
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
void FAssetRegistryImpl::AddCachedBPClassParent(const FTopLevelAssetPath& ClassPath, const FTopLevelAssetPath& NotYetRedirectedParentPath)
|
|
{
|
|
// We do not check for CoreRedirects for ClassPath, because this function is only called on behalf of ClassPath being loaded,
|
|
// and the code author would have changed the package containing ClassPath to match the redirect they added.
|
|
// But we do need to check for CoreRedirects in the ParentPath, because when a parent class is renamed, we do not resave
|
|
// all packages containing subclasses to update their FBlueprintTags::ParentClassPath AssetData tags.
|
|
FTopLevelAssetPath ParentPath = NotYetRedirectedParentPath;
|
|
#if WITH_EDITOR
|
|
FCoreRedirectObjectName RedirectedParentObjectName = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Class,
|
|
FCoreRedirectObjectName(NotYetRedirectedParentPath.GetAssetName(), NAME_None, NotYetRedirectedParentPath.GetPackageName()));
|
|
if (!RedirectedParentObjectName.OuterName.IsNone())
|
|
{
|
|
UE_LOG(LogAssetRegistry, Error,
|
|
TEXT("Class redirect exists from %s -> %s, which is invalid because ClassNames must be TopLevelAssetPaths. ")
|
|
TEXT("Redirect will be ignored in AssetRegistry queries."),
|
|
*NotYetRedirectedParentPath.ToString(), *RedirectedParentObjectName.ToString());
|
|
}
|
|
else
|
|
{
|
|
ParentPath = FTopLevelAssetPath(RedirectedParentObjectName.PackageName, RedirectedParentObjectName.ObjectName);
|
|
}
|
|
#endif
|
|
CachedBPInheritanceMap.Add(ClassPath, ParentPath);
|
|
}
|
|
|
|
void FAssetRegistryImpl::UpdateInheritanceBuffer(Impl::FClassInheritanceBuffer& OutBuffer) const
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UAssetRegistryImpl::UpdateTemporaryCaches)
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE_TRACK_REFERENCING_OPNAME_SCOPED(PackageAccessTrackingOps::NAME_ResetContext);
|
|
|
|
TMap<UClass*, TSet<UClass*>> NativeSubclasses = GetAllDerivedClasses();
|
|
|
|
uint32 NumNativeClasses = 1; // UObject has no superclass
|
|
for (const TPair<UClass*, TSet<UClass*>>& Pair : NativeSubclasses)
|
|
{
|
|
NumNativeClasses += Pair.Value.Num();
|
|
}
|
|
OutBuffer.InheritanceMap.Reserve(NumNativeClasses + CachedBPInheritanceMap.Num());
|
|
OutBuffer.InheritanceMap = CachedBPInheritanceMap;
|
|
OutBuffer.InheritanceMap.Add(UE::AssetRegistry::GetClassPathObject(), FTopLevelAssetPath());
|
|
|
|
for (TPair<FTopLevelAssetPath, TArray<FTopLevelAssetPath>>& Pair : OutBuffer.ReverseInheritanceMap)
|
|
{
|
|
Pair.Value.Reset();
|
|
}
|
|
OutBuffer.ReverseInheritanceMap.Reserve(NativeSubclasses.Num());
|
|
|
|
for (const TPair<UClass*, TSet<UClass*>>& Pair : NativeSubclasses)
|
|
{
|
|
FTopLevelAssetPath SuperclassName = Pair.Key->GetClassPathName();
|
|
|
|
TArray<FTopLevelAssetPath>* OutputSubclasses = &OutBuffer.ReverseInheritanceMap.FindOrAdd(SuperclassName);
|
|
OutputSubclasses->Reserve(Pair.Value.Num());
|
|
for (UClass* Subclass : Pair.Value)
|
|
{
|
|
if (!Subclass->HasAnyClassFlags(CLASS_Deprecated | CLASS_NewerVersionExists))
|
|
{
|
|
FTopLevelAssetPath SubclassName = Subclass->GetClassPathName();
|
|
OutputSubclasses->Add(SubclassName);
|
|
OutBuffer.InheritanceMap.Add(SubclassName, SuperclassName);
|
|
|
|
if (Subclass->Interfaces.Num())
|
|
{
|
|
// Add any implemented interfaces to the reverse inheritance map, but not to the forward map
|
|
for (const FImplementedInterface& Interface : Subclass->Interfaces)
|
|
{
|
|
if (UClass* InterfaceClass = Interface.Class) // could be nulled out by ForceDelete of a blueprint interface
|
|
{
|
|
TArray<FTopLevelAssetPath>& Implementations = OutBuffer.ReverseInheritanceMap.FindOrAdd(InterfaceClass->GetClassPathName());
|
|
Implementations.Add(SubclassName);
|
|
}
|
|
}
|
|
|
|
// Refetch OutputSubClasses from ReverseInheritanceMap because we just modified the ReverseInheritanceMap and may have resized
|
|
OutputSubclasses = OutBuffer.ReverseInheritanceMap.Find(SuperclassName);
|
|
check(OutputSubclasses); // It was added above
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add non-native classes to reverse map
|
|
for (const TPair<FTopLevelAssetPath, FTopLevelAssetPath>& Kvp : CachedBPInheritanceMap)
|
|
{
|
|
const FTopLevelAssetPath& ParentClassName = Kvp.Value;
|
|
if (!ParentClassName.IsNull())
|
|
{
|
|
TArray<FTopLevelAssetPath>& ChildClasses = OutBuffer.ReverseInheritanceMap.FindOrAdd(ParentClassName);
|
|
ChildClasses.Add(Kvp.Key);
|
|
}
|
|
|
|
}
|
|
|
|
OutBuffer.SavedAllClassesVersionNumber = GetCurrentAllClassesVersionNumber();
|
|
OutBuffer.bDirty = false;
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::GetInheritanceContextWithRequiredLock(UE::AssetRegistry::FInterfaceRWScopeLock& InOutScopeLock,
|
|
UE::AssetRegistry::Impl::FClassInheritanceContext& InheritanceContext,
|
|
UE::AssetRegistry::Impl::FClassInheritanceBuffer& StackBuffer)
|
|
{
|
|
using namespace UE::AssetRegistry;
|
|
|
|
uint64 CurrentGeneratorClassesVersionNumber = FAssetRegistryImpl::GetCurrentGeneratorClassesVersionNumber();
|
|
uint64 CurrentAllClassesVersionNumber = FAssetRegistryImpl::GetCurrentAllClassesVersionNumber();
|
|
bool bNeedsWriteLock = false;
|
|
if (GuardedData.GetSavedGeneratorClassesVersionNumber() != CurrentGeneratorClassesVersionNumber)
|
|
{
|
|
// ConditionalUpdate writes to protected data in CollectCodeGeneratorClasses, so we cannot proceed under a read lock
|
|
bNeedsWriteLock = true;
|
|
}
|
|
if (GuardedData.IsTempCachingEnabled() &&
|
|
!GuardedData.GetTempCachedInheritanceBuffer().IsUpToDate(CurrentAllClassesVersionNumber))
|
|
{
|
|
// Temp caching is enabled, so we will be reading the protected data in TempCachedInheritanceBuffer
|
|
// It's out of date, so we need to write to it first, so we cannot proceed under a read lock
|
|
bNeedsWriteLock = true;
|
|
}
|
|
if (bNeedsWriteLock)
|
|
{
|
|
InOutScopeLock.ReleaseReadOnlyLockAndAcquireWriteLock_USE_WITH_CAUTION();
|
|
}
|
|
|
|
// Note that we have to reread all data since we may have dropped the lock
|
|
InheritanceContext.StackBuffer = &StackBuffer;
|
|
InheritanceContext.SelectBuffer(GuardedData, CurrentGeneratorClassesVersionNumber, CurrentAllClassesVersionNumber);
|
|
}
|
|
|
|
void UAssetRegistryImpl::GetInheritanceContextWithRequiredLock(UE::AssetRegistry::FInterfaceWriteScopeLock& InOutScopeLock,
|
|
UE::AssetRegistry::Impl::FClassInheritanceContext& InheritanceContext,
|
|
UE::AssetRegistry::Impl::FClassInheritanceBuffer& StackBuffer)
|
|
{
|
|
using namespace UE::AssetRegistry;
|
|
|
|
uint64 CurrentGeneratorClassesVersionNumber = FAssetRegistryImpl::GetCurrentGeneratorClassesVersionNumber();
|
|
uint64 CurrentAllClassesVersionNumber = FAssetRegistryImpl::GetCurrentAllClassesVersionNumber();
|
|
InheritanceContext.StackBuffer = &StackBuffer;
|
|
InheritanceContext.SelectBuffer(GuardedData, CurrentGeneratorClassesVersionNumber, CurrentAllClassesVersionNumber);
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
void UAssetRegistryImpl::OnGetExtraObjectTags(FAssetRegistryTagsContext Context)
|
|
{
|
|
if (bAddMetaDataTagsToOnGetExtraObjectTags)
|
|
{
|
|
// Adding metadata tags from disk is only necessary for cooked assets; uncooked assets still have the metadata and add them elsewhere
|
|
// in UObject::GetAssetRegistryTags. Adding the tags from disk into uncooked assets would make the tags impossible to remove when
|
|
// the uncooked assets are resaved.
|
|
if (Context.GetObject()->GetPackage()->HasAnyPackageFlags(PKG_Cooked))
|
|
{
|
|
// It is critical that bIncludeOnlyOnDiskAssets=true otherwise this will cause an infinite loop
|
|
const FAssetData AssetData = GetAssetByObjectPath(FSoftObjectPath::ConstructFromObject(Context.GetObject()), /*bIncludeOnlyOnDiskAssets=*/true);
|
|
TSet<FName>& MetaDataTags = UObject::GetMetaDataTagsForAssetRegistry();
|
|
for (const FName MetaDataTag : MetaDataTags)
|
|
{
|
|
auto OutTagsContainsTagPredicate = [MetaDataTag](const UObject::FAssetRegistryTag& Tag) { return Tag.Name == MetaDataTag; };
|
|
if (!Context.ContainsTag(MetaDataTag))
|
|
{
|
|
FAssetTagValueRef TagValue = AssetData.TagsAndValues.FindTag(MetaDataTag);
|
|
if (TagValue.IsSet())
|
|
{
|
|
Context.AddTag(UObject::FAssetRegistryTag(MetaDataTag, TagValue.AsString(), UObject::FAssetRegistryTag::TT_Alphabetical));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool UAssetRegistryImpl::IsDirAlreadyWatchedByRootWatchers(const FString& Directory) const
|
|
{
|
|
return DirectoryWatchRoots.ContainsByPredicate([&Directory](const FString& WatchRoot) -> bool {
|
|
return FPaths::IsUnderDirectory(Directory, WatchRoot);
|
|
});
|
|
}
|
|
|
|
#endif
|
|
|
|
void UAssetRegistryImpl::RequestPauseBackgroundProcessing()
|
|
{
|
|
#if WITH_EDITOR
|
|
GuardedData.RequestPauseBackgroundProcessing();
|
|
#endif
|
|
}
|
|
|
|
void UAssetRegistryImpl::RequestResumeBackgroundProcessing()
|
|
{
|
|
#if WITH_EDITOR
|
|
GuardedData.RequestResumeBackgroundProcessing();
|
|
#endif
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
namespace Impl
|
|
{
|
|
|
|
void FClassInheritanceBuffer::Clear()
|
|
{
|
|
InheritanceMap.Empty();
|
|
ReverseInheritanceMap.Empty();
|
|
}
|
|
|
|
bool FClassInheritanceBuffer::IsUpToDate(uint64 CurrentAllClassesVersionNumber) const
|
|
{
|
|
return !bDirty && SavedAllClassesVersionNumber == CurrentAllClassesVersionNumber;
|
|
}
|
|
|
|
SIZE_T FClassInheritanceBuffer::GetAllocatedSize() const
|
|
{
|
|
return InheritanceMap.GetAllocatedSize() + ReverseInheritanceMap.GetAllocatedSize();
|
|
}
|
|
|
|
void FClassInheritanceContext::BindToBuffer(FClassInheritanceBuffer& InBuffer, FAssetRegistryImpl& InAssetRegistryImpl,
|
|
bool bInInheritanceMapUpToDate, bool bInCodeGeneratorClassesUpToDate)
|
|
{
|
|
AssetRegistryImpl = &InAssetRegistryImpl;
|
|
Buffer = &InBuffer;
|
|
bInheritanceMapUpToDate = bInInheritanceMapUpToDate;
|
|
bCodeGeneratorClassesUpToDate = bInCodeGeneratorClassesUpToDate;
|
|
}
|
|
|
|
void FClassInheritanceContext::ConditionalUpdate()
|
|
{
|
|
check(Buffer != nullptr); // It is not valid to call ConditionalUpdate with an empty FClassInheritanceContext
|
|
if (bInheritanceMapUpToDate)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!bCodeGeneratorClassesUpToDate)
|
|
{
|
|
AssetRegistryImpl->CollectCodeGeneratorClasses();
|
|
bCodeGeneratorClassesUpToDate = true;
|
|
}
|
|
AssetRegistryImpl->UpdateInheritanceBuffer(*Buffer);
|
|
bInheritanceMapUpToDate = true;
|
|
}
|
|
|
|
void FClassInheritanceContext::SelectBuffer(FAssetRegistryImpl& GuardedData, uint64 CurrentGeneratorClassesVersionNumber, uint64 CurrentAllClassesVersionNumber)
|
|
{
|
|
// If bIsTempCachingAlwaysEnabled, then we are guaranteed that bIsTempCachingEnabled=true.
|
|
// We rely on this to simplify logic and only check bIsTempCachingEnabled
|
|
check(!GuardedData.IsTempCachingAlwaysEnabled() || GuardedData.IsTempCachingEnabled());
|
|
|
|
bool bLocalCodeGeneratorClassesUpToDate = GuardedData.GetSavedGeneratorClassesVersionNumber() == CurrentGeneratorClassesVersionNumber;
|
|
if (GuardedData.IsTempCachingEnabled())
|
|
{
|
|
// Use the persistent buffer
|
|
UE::AssetRegistry::Impl::FClassInheritanceBuffer& TempCachedInheritanceBuffer = GuardedData.GetTempCachedInheritanceBuffer();
|
|
bool bLocalInheritanceMapUpToDate = TempCachedInheritanceBuffer.IsUpToDate(CurrentAllClassesVersionNumber);
|
|
BindToBuffer(TempCachedInheritanceBuffer, GuardedData, bLocalInheritanceMapUpToDate, bLocalCodeGeneratorClassesUpToDate);
|
|
}
|
|
else
|
|
{
|
|
// Use the StackBuffer for the duration of the caller
|
|
check(StackBuffer);
|
|
BindToBuffer(*StackBuffer, GuardedData, false /* bInInheritanceMapUpToDate */, bLocalCodeGeneratorClassesUpToDate);
|
|
}
|
|
}
|
|
|
|
void FClassInheritanceContext::OnLockReentered(FAssetRegistryImpl& GuardedData)
|
|
{
|
|
uint64 CurrentGeneratorClassesVersionNumber = FAssetRegistryImpl::GetCurrentGeneratorClassesVersionNumber();
|
|
uint64 CurrentAllClassesVersionNumber = FAssetRegistryImpl::GetCurrentAllClassesVersionNumber();
|
|
SelectBuffer(GuardedData, CurrentGeneratorClassesVersionNumber, CurrentAllClassesVersionNumber);
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::GetSubClasses(Impl::FClassInheritanceContext& InheritanceContext,
|
|
const TArray<FTopLevelAssetPath>& InClassNames, const TSet<FTopLevelAssetPath>& ExcludedClassNames, TSet<FTopLevelAssetPath>& SubClassNames) const
|
|
{
|
|
InheritanceContext.ConditionalUpdate();
|
|
|
|
TSet<FTopLevelAssetPath> ProcessedClassNames;
|
|
for (const FTopLevelAssetPath& ClassName : InClassNames)
|
|
{
|
|
// Now find all subclass names
|
|
GetSubClasses_Recursive(InheritanceContext, ClassName, SubClassNames, ProcessedClassNames, ExcludedClassNames);
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::GetSubClasses_Recursive(Impl::FClassInheritanceContext& InheritanceContext, FTopLevelAssetPath InClassName,
|
|
TSet<FTopLevelAssetPath>& SubClassNames, TSet<FTopLevelAssetPath>& ProcessedClassNames, const TSet<FTopLevelAssetPath>& ExcludedClassNames) const
|
|
{
|
|
if (ExcludedClassNames.Contains(InClassName))
|
|
{
|
|
// This class is in the exclusion list. Exclude it.
|
|
}
|
|
else if (ProcessedClassNames.Contains(InClassName))
|
|
{
|
|
// This class has already been processed. Ignore it.
|
|
}
|
|
else
|
|
{
|
|
SubClassNames.Add(InClassName);
|
|
ProcessedClassNames.Add(InClassName);
|
|
|
|
auto AddSubClasses = [this, &InheritanceContext, &SubClassNames, &ProcessedClassNames, &ExcludedClassNames]
|
|
(FTopLevelAssetPath ParentClassName)
|
|
{
|
|
const TArray<FTopLevelAssetPath>* FoundSubClassNames = InheritanceContext.Buffer->ReverseInheritanceMap.Find(ParentClassName);
|
|
if (FoundSubClassNames)
|
|
{
|
|
for (FTopLevelAssetPath ClassName : (*FoundSubClassNames))
|
|
{
|
|
GetSubClasses_Recursive(InheritanceContext, ClassName, SubClassNames, ProcessedClassNames,
|
|
ExcludedClassNames);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Add Subclasses of the given classname
|
|
AddSubClasses(InClassName);
|
|
}
|
|
}
|
|
|
|
|
|
#if WITH_EDITOR
|
|
void FAssetRegistryImpl::RequestPauseBackgroundProcessing()
|
|
{
|
|
if (GlobalGatherer.IsValid())
|
|
{
|
|
GlobalGatherer->PauseProcessing();
|
|
}
|
|
}
|
|
|
|
void FAssetRegistryImpl::RequestResumeBackgroundProcessing()
|
|
{
|
|
if (GlobalGatherer.IsValid())
|
|
{
|
|
GlobalGatherer->ResumeProcessing();
|
|
}
|
|
}
|
|
|
|
bool FAssetRegistryImpl::IsBackgroundProcessingPaused() const
|
|
{
|
|
if (GlobalGatherer.IsValid())
|
|
{
|
|
return GlobalGatherer->IsProcessingPauseRequested();
|
|
}
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
static FString GAssetRegistryManagementPathsPackageDebugName;
|
|
static FAutoConsoleVariableRef CVarAssetRegistryManagementPathsPackageDebugName(
|
|
TEXT("AssetRegistry.ManagementPathsPackageDebugName"),
|
|
GAssetRegistryManagementPathsPackageDebugName,
|
|
TEXT("If set, when manage references are set, the chain of references that caused this package to become managed will be printed to the log"));
|
|
#endif
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
/**
|
|
* In SetManagerReferences, we execute a graph search within the graph of PackageDependencies for Assets to decide
|
|
* which Assets should be marked as having Managed properties. We use a graph search painting algorithm to
|
|
* apply multiple properties within a single graph search. A painting algorithm keeps track of the current list of
|
|
* properties to apply to each visited vertex, and filters that list of properties depending on the edge properties
|
|
* when traversing an edge. The list of Managed properties we paint during SetManageReferences is given by the values
|
|
* of EManageSearchColor.
|
|
*/
|
|
enum class EManageSearchColor : uint8
|
|
{
|
|
CookRule = 0,
|
|
Chunking = 1,
|
|
Num,
|
|
};
|
|
constexpr int32 ManageSearchColorsNum = static_cast<int32>(EManageSearchColor::Num);
|
|
static_assert(ManageSearchColorsNum <= 8 * sizeof(uint32), "We store lists of SearchColors as a uint32 bitfield.");
|
|
enum class EManageSearchColorBit : uint32
|
|
{
|
|
None = 0,
|
|
CookRule = (1 << static_cast<int32>(EManageSearchColor::CookRule)),
|
|
Chunking = (1 << static_cast<int32>(EManageSearchColor::Chunking)),
|
|
All = CookRule | Chunking,
|
|
};
|
|
ENUM_CLASS_FLAGS(EManageSearchColorBit);
|
|
|
|
/**
|
|
* Data held for each Asset (aka node in our graph search) across the multiple graph searches conducted within
|
|
* SetManageReferences, and across multiple calls to SetManageReferences
|
|
*
|
|
* Each call to SetManageReferences is called a round. The rounds occur in descending priority order of managers;
|
|
* each round does a graph search for the nodes reported as managed by managers in that round. All the managers in
|
|
* a given round have the same priority.
|
|
*
|
|
*/
|
|
struct FSetManageReferencesNodeData
|
|
{
|
|
// Values preserved across multiple rounds
|
|
/**
|
|
* ManagedInEarlierRound bit (1 << N) is set at the end of a round for assets painted with ((EManageSearchColor)N)
|
|
* in the round, and later rounds can use that information in ShouldSetManager calls to e.g. ignore the management
|
|
* of a node by a lower-priority manager.
|
|
*/
|
|
EManageSearchColorBit ManagedInEarlierRound = EManageSearchColorBit::None;
|
|
|
|
// Values reset for each round.
|
|
/**
|
|
* Whether the asset was modified by the round (and therefore needs to have transient properties restored at the
|
|
* end of the round.
|
|
*/
|
|
bool bModifiedByRound = false;
|
|
/**
|
|
* ManagedInThisRound bit (1 << N) is set during a round's graph search for the asset painted with
|
|
* ((EManageSearchColor)N) in the round.
|
|
*/
|
|
EManageSearchColorBit ManagedInThisRound = EManageSearchColorBit::None;
|
|
/**
|
|
* Whether the asset has a direct manager in the current round, as described by the caller of the round in
|
|
* FSetManageReferencesContext.ManagerMap. Direct Managers apply to all EManageSearchColors.
|
|
*/
|
|
bool bDirectManagedInThisRound = false;
|
|
|
|
/**
|
|
* For a manager node in the current round, the list of directly managed assets. Directly managed assets paint
|
|
* with all EManageSearchColors.
|
|
*/
|
|
TArray<FDependsNode*> DirectManagedAssetsThisRound;
|
|
|
|
/**
|
|
* DebugInstigator[N] is the source node (aka asset) that caused this node to be painted with
|
|
* ((EManageSearchColor)N) in the current round. This information is used to provide instigator chains for what
|
|
* caused an asset to be added to the cook or to a chunk.
|
|
*/
|
|
FDependsNode* DebugInstigator[ManageSearchColorsNum] = {};
|
|
|
|
// Values reset for the graph search from each manager with a round.
|
|
/**
|
|
* Whether the asset was modified by the manager's search (and therefore needs to have transient properties
|
|
* restored at the end of the search).
|
|
*/
|
|
bool bModifiedByCurrentManager = false;
|
|
/**
|
|
* VisitedByCurrentManager bit (1 <<N) records whether the asset was found to be painted with
|
|
* ((EManageSearchColor)N) by the manager.
|
|
*/
|
|
EManageSearchColorBit VisitedByCurrentManager = EManageSearchColorBit::None;
|
|
/**
|
|
* The AssetRegistry dependency properties that should be assigned to the targetnode for the Management edge we
|
|
* create for the node from the current manager; this accumulates the properties we set for each painted color,
|
|
* as well as the special EDependencyProperty::Direct property.
|
|
*/
|
|
EDependencyProperty CurrentManagerProperties = EDependencyProperty::None;
|
|
};
|
|
|
|
using FSetManageReferencesNodeDataMap = TMap<FDependsNode*, TUniquePtr<FSetManageReferencesNodeData>>;
|
|
struct FSetManageReferencesScratch
|
|
{
|
|
FSetManageReferencesNodeDataMap NodeData;
|
|
};
|
|
|
|
#if WITH_EDITOR
|
|
void PrintAssetRegistryManagementPathsPackageDebugInfo(FDependsNode* Node, UE::AssetRegistry::FSetManageReferencesNodeDataMap* NodeData)
|
|
{
|
|
if (Node)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Display, TEXT("SetManageReferences is printing out the reference chain that caused '%s' to be managed"), *GAssetRegistryManagementPathsPackageDebugName);
|
|
TSet<FDependsNode*> AllVisitedNodes;
|
|
for (;;)
|
|
{
|
|
TUniquePtr<FSetManageReferencesNodeData>* CurrentNodeData = NodeData->Find(Node);
|
|
if (!CurrentNodeData)
|
|
{
|
|
break;
|
|
}
|
|
FDependsNode* ReferencingNode = (*CurrentNodeData)->DebugInstigator[(int32)EManageSearchColor::Chunking];
|
|
if (!ReferencingNode)
|
|
{
|
|
break;
|
|
}
|
|
|
|
UE_LOG(LogAssetRegistry, Display, TEXT(" %s"), *ReferencingNode->GetIdentifier().ToString());
|
|
if (AllVisitedNodes.Contains(ReferencingNode))
|
|
{
|
|
UE_LOG(LogAssetRegistry, Display, TEXT(" ... (Circular reference back to %s)"), *ReferencingNode->GetPackageName().ToString());
|
|
break;
|
|
}
|
|
|
|
AllVisitedNodes.Add(ReferencingNode);
|
|
Node = ReferencingNode;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogAssetRegistry, Warning, TEXT("Node with AssetRegistryManagementPathsPackageDebugName '%s' was not found"), *GAssetRegistryManagementPathsPackageDebugName);
|
|
}
|
|
}
|
|
#endif // WITH_EDITOR
|
|
|
|
} // namespace UE::AssetRegistry
|
|
|
|
void UAssetRegistryImpl::SetManageReferences(const TMultiMap<FAssetIdentifier, FAssetIdentifier>& ManagerMap,
|
|
bool bClearExisting, UE::AssetRegistry::EDependencyCategory RecurseType, TSet<FDependsNode*>& ExistingManagedNodes,
|
|
ShouldSetManagerPredicate ShouldSetManager)
|
|
{
|
|
if (!ShouldSetManager)
|
|
{
|
|
ShouldSetManager = [](const FAssetIdentifier& Manager, const FAssetIdentifier& Source, const FAssetIdentifier& Target,
|
|
UE::AssetRegistry::EDependencyCategory Category, UE::AssetRegistry::EDependencyProperty Properties,
|
|
EAssetSetManagerFlags::Type Flags)
|
|
{
|
|
return EAssetSetManagerResult::SetButDoNotRecurse;
|
|
};
|
|
}
|
|
auto ShouldSetManagerByContext = [&ShouldSetManager](UE::AssetRegistry::FShouldSetManagerContext& ShouldSetContext)
|
|
{
|
|
return ShouldSetManager(ShouldSetContext.Manager, ShouldSetContext.Source, ShouldSetContext.Target,
|
|
ShouldSetContext.EdgeARCategory, ShouldSetContext.EdgeARProperties, ShouldSetContext.EdgeFlags);
|
|
};
|
|
|
|
UE::AssetRegistry::FSetManageReferencesContext Context;
|
|
Context.ManagerMap = &ManagerMap;
|
|
Context.bClearExisting = bClearExisting;
|
|
Context.RecurseType = RecurseType;
|
|
Context.ShouldSetManager = ShouldSetManagerByContext;
|
|
SetManageReferences(Context);
|
|
}
|
|
|
|
void UAssetRegistryImpl::SetManageReferences(UE::AssetRegistry::FSetManageReferencesContext& Context)
|
|
{
|
|
// For performance reasons we call the ShouldSetManager callback when inside the lock. Licensee UAssetManagers
|
|
// are responsible for not calling AssetRegistry functions from ShouldSetManager as that would create a deadlock
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.SetManageReferences(Context);
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
static EAssetSetManagerAssetFlags ConstructAssetManagerAssetFlags(const FDependsNode& DependsNode)
|
|
{
|
|
return DependsNode.IsScriptPath() ? EAssetSetManagerAssetFlags::ScriptPackage
|
|
: EAssetSetManagerAssetFlags::None;
|
|
};
|
|
|
|
void FAssetRegistryImpl::SetManageReferences(FSetManageReferencesContext& Context)
|
|
{
|
|
if (!Context.ManagerMap)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Error, TEXT("SetManageReferences: Context.ManagerMap must not be null."));
|
|
FDebug::DumpStackTraceToLog(ELogVerbosity::Warning);
|
|
return;
|
|
}
|
|
if (!Context.ShouldSetManager.IsSet())
|
|
{
|
|
UE_LOG(LogAssetRegistry, Error, TEXT("SetManageReferences: Context.ShouldSetManager must be a bound pointer."));
|
|
FDebug::DumpStackTraceToLog(ELogVerbosity::Warning);
|
|
return;
|
|
}
|
|
|
|
FSetManageReferencesScratch* Scratch = nullptr;
|
|
FSetManageReferencesNodeDataMap* NodeData = nullptr;
|
|
// TODO: Invalidate Scratch and give an error if dependsnodes are written in between calls; Scratch has pointers
|
|
// and cached data into the AssetRegistry's list of DependsNodes.
|
|
|
|
TArray<FDependsNode*> CurrentRoundModifiedNodes;
|
|
|
|
if (!Context.Scratch)
|
|
{
|
|
Context.Scratch = MakePimpl<FSetManageReferencesScratch>();
|
|
Context.Scratch->NodeData.Reserve(State.CachedDependsNodes.Num());
|
|
}
|
|
Scratch = Context.Scratch.Get();
|
|
NodeData = &Scratch->NodeData;
|
|
|
|
// We use a TMap of TUniquePtr rather than direct c++ member data so that we can have persistent pointers to
|
|
// NodeDatas throughout the graph search, rather than possibly have our data pointers be invalidated by a
|
|
// reallocation when a new node is added. This has a cputime cost cost - an extra memory access per read.
|
|
// It also has a complexity cost, because instead of calling FindOrAdd to create if non-existing, we also have
|
|
// to check whether it was added and allocate the TUniquePtr if so. This FindOrAddNodeData handles that
|
|
// complexity of creation.
|
|
auto FindOrAddNodeData = [NodeData](FDependsNode* Node) -> FSetManageReferencesNodeData&
|
|
{
|
|
TUniquePtr<FSetManageReferencesNodeData>& Existing = NodeData->FindOrAdd(Node);
|
|
if (!Existing)
|
|
{
|
|
Existing.Reset(new FSetManageReferencesNodeData());
|
|
}
|
|
return *Existing;
|
|
};
|
|
|
|
if (Context.bClearExisting)
|
|
{
|
|
// Clear information from Scratch about Earlier rounds
|
|
for (TPair<FDependsNode*, TUniquePtr<FSetManageReferencesNodeData>>& NodePair : *NodeData)
|
|
{
|
|
NodePair.Value->ManagedInEarlierRound = EManageSearchColorBit::None;
|
|
}
|
|
|
|
// Clear all outgoing manage dependencies, and update the referencers of any nodes with incoming
|
|
// manage dependencies
|
|
TSet<FDependsNode*> NodesToRefreshReferencers;
|
|
for (const TPair<FAssetIdentifier, FDependsNode*>& Pair : State.CachedDependsNodes)
|
|
{
|
|
FDependsNode* SourceNode = Pair.Value;
|
|
SourceNode->IterateOverDependencies([&NodesToRefreshReferencers]
|
|
(FDependsNode* TargetNode, EDependencyCategory Category, EDependencyProperty Property, bool bUnique)
|
|
{
|
|
NodesToRefreshReferencers.Add(TargetNode);
|
|
}, EDependencyCategory::Manage);
|
|
SourceNode->ClearDependencies(EDependencyCategory::Manage);
|
|
}
|
|
|
|
for (FDependsNode* NodeToClear : NodesToRefreshReferencers)
|
|
{
|
|
NodeToClear->SetIsReferencersSorted(false);
|
|
NodeToClear->RefreshReferencers();
|
|
|
|
CurrentRoundModifiedNodes.Add(NodeToClear);
|
|
FSetManageReferencesNodeData& ModifiedData = FindOrAddNodeData(NodeToClear);
|
|
ModifiedData.bModifiedByRound = true;
|
|
}
|
|
}
|
|
|
|
TArray<FDependsNode*> CurrentRoundManagers;
|
|
TArray<FDependsNode*> CurrentManagerModifiedNodes;
|
|
TArray<TPair<FDependsNode*, EManageSearchColorBit>> CurrentManagerVisitQueue;
|
|
|
|
// Convert ManageMap from edges in the FAssetIdentifier graph into edges in the FDependsNode graph.
|
|
// Also mark each explicitly managed node; AssetManager's ShouldSetManager function might decide to skip
|
|
// an indirect manage reference if there is a direct manage reference in the same round.
|
|
CurrentRoundManagers.Reserve(Context.ManagerMap->Num());
|
|
for (const TPair<FAssetIdentifier, FAssetIdentifier>& Pair : *Context.ManagerMap)
|
|
{
|
|
FDependsNode* ManagedNode = State.FindDependsNode(Pair.Value);
|
|
if (!ManagedNode)
|
|
{
|
|
UE_LOG(LogAssetRegistry, Warning, TEXT("Cannot set %s to manage asset %s because %s does not exist!"),
|
|
*Pair.Key.ToString(), *Pair.Value.ToString(), *Pair.Value.ToString());
|
|
continue;
|
|
}
|
|
FDependsNode* ManagerNode = State.CreateOrFindDependsNode(Pair.Key);
|
|
|
|
CurrentRoundManagers.Add(ManagerNode);
|
|
FindOrAddNodeData(ManagedNode).bDirectManagedInThisRound = true;
|
|
FindOrAddNodeData(ManagerNode).DirectManagedAssetsThisRound.Add(ManagedNode);
|
|
}
|
|
|
|
Algo::Sort(CurrentRoundManagers);
|
|
CurrentRoundManagers.SetNum(Algo::Unique(CurrentRoundManagers));
|
|
|
|
// For each manager, do a graph search starting from its directly managed nodes.
|
|
for (FDependsNode* ManagerNode : CurrentRoundManagers)
|
|
{
|
|
FSetManageReferencesNodeData& ManagerNodeData = FindOrAddNodeData(ManagerNode);
|
|
check(CurrentManagerModifiedNodes.IsEmpty());
|
|
check(CurrentManagerVisitQueue.IsEmpty());
|
|
|
|
auto VisitNode = [ManagerNode, &Context, &CurrentManagerModifiedNodes,
|
|
&CurrentManagerVisitQueue, &FindOrAddNodeData]
|
|
(FDependsNode* SourceNode, FDependsNode* TargetNode, EDependencyCategory DependencyType,
|
|
EDependencyProperty PackageDependencyProperties, EManageSearchColorBit PaintColors)
|
|
{
|
|
// Only recurse if we haven't already visited, and this node passes recursion test
|
|
FSetManageReferencesNodeData& TargetNodeData = FindOrAddNodeData(TargetNode);
|
|
PaintColors &= ~TargetNodeData.VisitedByCurrentManager;
|
|
if (PaintColors == EManageSearchColorBit::None)
|
|
{
|
|
return;
|
|
}
|
|
|
|
EManageSearchColorBit ColorsManagedInEarlierRound = TargetNodeData.ManagedInEarlierRound & PaintColors;
|
|
EManageSearchColorBit ColorsNotManagedInEarlierRound = (~TargetNodeData.ManagedInEarlierRound) & PaintColors;
|
|
|
|
bool bDirectSet = SourceNode == ManagerNode;
|
|
EAssetSetManagerFlags::Type EdgeFlagsOtherThanManagedInEarlierRound = (EAssetSetManagerFlags::Type)(
|
|
(bDirectSet ? EAssetSetManagerFlags::IsDirectSet : 0)
|
|
| (TargetNodeData.bDirectManagedInThisRound && !bDirectSet
|
|
? EAssetSetManagerFlags::TargetHasDirectManager : 0));
|
|
FShouldSetManagerContext ShouldSetContext
|
|
{
|
|
.Manager = ManagerNode->GetIdentifier(),
|
|
.Source = SourceNode->GetIdentifier(),
|
|
.Target = TargetNode->GetIdentifier(),
|
|
.ManagerAssetFlags = ConstructAssetManagerAssetFlags(*ManagerNode),
|
|
.SourceAssetFlags = ConstructAssetManagerAssetFlags(*SourceNode),
|
|
.TargetAssetFlags = ConstructAssetManagerAssetFlags(*TargetNode),
|
|
.EdgeFlags = EdgeFlagsOtherThanManagedInEarlierRound,
|
|
.EdgeARCategory = DependencyType,
|
|
.EdgeARProperties = PackageDependencyProperties
|
|
};
|
|
|
|
EManageSearchColorBit ColorsToSet = EManageSearchColorBit::None;
|
|
EManageSearchColorBit ColorsToRecurse = EManageSearchColorBit::None;
|
|
|
|
if (ColorsNotManagedInEarlierRound != EManageSearchColorBit::None)
|
|
{
|
|
EAssetSetManagerResult::Type Result = (*Context.ShouldSetManager)(ShouldSetContext);
|
|
switch (Result)
|
|
{
|
|
case EAssetSetManagerResult::DoNotSet:
|
|
break;
|
|
case EAssetSetManagerResult::SetButDoNotRecurse:
|
|
ColorsToSet |= ColorsNotManagedInEarlierRound;
|
|
break;
|
|
case EAssetSetManagerResult::SetAndRecurse:
|
|
ColorsToSet |= ColorsNotManagedInEarlierRound;
|
|
ColorsToRecurse |= ColorsNotManagedInEarlierRound;
|
|
break;
|
|
default:
|
|
checkNoEntry();
|
|
break;
|
|
}
|
|
}
|
|
if (ColorsManagedInEarlierRound != EManageSearchColorBit::None)
|
|
{
|
|
ShouldSetContext.EdgeFlags = (EAssetSetManagerFlags::Type)
|
|
(ShouldSetContext.EdgeFlags | EAssetSetManagerFlags::TargetHasExistingManager);
|
|
EAssetSetManagerResult::Type Result = (*Context.ShouldSetManager)(ShouldSetContext);
|
|
switch (Result)
|
|
{
|
|
case EAssetSetManagerResult::DoNotSet:
|
|
break;
|
|
case EAssetSetManagerResult::SetButDoNotRecurse:
|
|
ColorsToSet |= ColorsManagedInEarlierRound;
|
|
break;
|
|
case EAssetSetManagerResult::SetAndRecurse:
|
|
ColorsToSet |= ColorsManagedInEarlierRound;
|
|
ColorsToRecurse |= ColorsManagedInEarlierRound;
|
|
break;
|
|
default:
|
|
checkNoEntry();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ColorsToSet != EManageSearchColorBit::None)
|
|
{
|
|
TargetNodeData.VisitedByCurrentManager |= ColorsToSet;
|
|
TargetNodeData.bModifiedByCurrentManager = true;
|
|
CurrentManagerModifiedNodes.Add(TargetNode);
|
|
|
|
TargetNodeData.CurrentManagerProperties |=
|
|
(bDirectSet ? EDependencyProperty::Direct : EDependencyProperty::None)
|
|
| (EnumHasAnyFlags(ColorsToSet, EManageSearchColorBit::CookRule)
|
|
? EDependencyProperty::CookRule : EDependencyProperty::None)
|
|
// Painting with any color, or marking it as Direct, also marks it as painted with Chunking.
|
|
// There is not a separate flag to store this in EDependencyProperty; the existence of the
|
|
// dependency implies it is at least a Chunking dependency.
|
|
;
|
|
for (int32 PaintColor = 0; PaintColor < ManageSearchColorsNum; ++PaintColor)
|
|
{
|
|
if (EnumHasAnyFlags(ColorsToSet, (EManageSearchColorBit)(1 << ManageSearchColorsNum))
|
|
&& !TargetNodeData.DebugInstigator[PaintColor])
|
|
{
|
|
TargetNodeData.DebugInstigator[PaintColor] = SourceNode;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ColorsToRecurse != EManageSearchColorBit::None)
|
|
{
|
|
CurrentManagerVisitQueue.Push({ TargetNode, ColorsToRecurse });
|
|
}
|
|
};
|
|
|
|
// Visit the directly managed nodes and populate the starting vertices for the graph search of indirectly
|
|
// managed nodes
|
|
for (FDependsNode* ManagedNode : ManagerNodeData.DirectManagedAssetsThisRound)
|
|
{
|
|
VisitNode(ManagerNode, ManagedNode, EDependencyCategory::Manage,
|
|
EDependencyProperty::Game | EDependencyProperty::Build, EManageSearchColorBit::All);
|
|
}
|
|
|
|
// Find and Visit all indirectly managed nodes, but only if we have a recurse type
|
|
if (Context.RecurseType != EDependencyCategory::None)
|
|
{
|
|
while (CurrentManagerVisitQueue.Num())
|
|
{
|
|
// Pull off end of array to avoid the cost of shifts; order of visitation doesn't matter
|
|
TPair<FDependsNode*, EManageSearchColorBit> VisitPair = CurrentManagerVisitQueue.Pop();
|
|
FDependsNode* SourceNode = VisitPair.Key;
|
|
EManageSearchColorBit VertexPaintColors = VisitPair.Value;
|
|
|
|
SourceNode->IterateOverDependencies([&VisitNode, SourceNode, VertexPaintColors]
|
|
(FDependsNode* TargetNode, EDependencyCategory DependencyCategory,
|
|
EDependencyProperty DependencyProperties, bool bDuplicate)
|
|
{
|
|
// Skip dependencies that are EditorOnly and non-build.
|
|
// Propagate only through UsedInGame or Build dependencies.
|
|
if (EnumHasAnyFlags(DependencyProperties, EDependencyProperty::Game | EDependencyProperty::Build))
|
|
{
|
|
// ChunkManagement propagates through either UsedGame or Build dependencies, but CookRules
|
|
// propagate only through Game dependencies.
|
|
EManageSearchColorBit EdgePaintColors;
|
|
EdgePaintColors = EnumHasAnyFlags(DependencyProperties, EDependencyProperty::Game) ?
|
|
EManageSearchColorBit::All : EManageSearchColorBit::Chunking;
|
|
EManageSearchColorBit TargetPaintColors = VertexPaintColors & EdgePaintColors;
|
|
VisitNode(SourceNode, TargetNode, DependencyCategory, DependencyProperties, TargetPaintColors);
|
|
}
|
|
}, Context.RecurseType);
|
|
}
|
|
}
|
|
|
|
ManagerNode->SetIsDependencyListSorted(UE::AssetRegistry::EDependencyCategory::Manage, false);
|
|
ManagerNodeData.bModifiedByRound = true;
|
|
CurrentRoundModifiedNodes.Add(ManagerNode);
|
|
|
|
for (FDependsNode* ModifiedNode : CurrentManagerModifiedNodes)
|
|
{
|
|
FSetManageReferencesNodeData& ModifiedData = FindOrAddNodeData(ModifiedNode);
|
|
if (!ModifiedData.bModifiedByCurrentManager)
|
|
{
|
|
// A duplicate of a NodeData we already handled earlier in the list
|
|
continue;
|
|
}
|
|
|
|
ModifiedNode->SetIsReferencersSorted(false);
|
|
ModifiedNode->AddReferencer(ManagerNode);
|
|
ManagerNode->AddDependency(ModifiedNode, EDependencyCategory::Manage,
|
|
ModifiedData.CurrentManagerProperties);
|
|
|
|
ModifiedData.ManagedInThisRound |= ModifiedData.VisitedByCurrentManager;
|
|
ModifiedData.bModifiedByRound = true;
|
|
CurrentRoundModifiedNodes.Add(ModifiedNode);
|
|
|
|
ModifiedData.bModifiedByCurrentManager = false;
|
|
ModifiedData.VisitedByCurrentManager = EManageSearchColorBit::None;
|
|
ModifiedData.CurrentManagerProperties = EDependencyProperty::None;
|
|
}
|
|
CurrentManagerModifiedNodes.Reset();
|
|
CurrentManagerVisitQueue.Reset();
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
if (!GAssetRegistryManagementPathsPackageDebugName.IsEmpty())
|
|
{
|
|
FDependsNode* PackageDebugInfoNode = State.FindDependsNode(FName(*GAssetRegistryManagementPathsPackageDebugName));
|
|
PrintAssetRegistryManagementPathsPackageDebugInfo(PackageDebugInfoNode, NodeData);
|
|
}
|
|
#endif
|
|
|
|
bool bShouldSortDependencies = ShouldSortDependencies();
|
|
bool bShouldSortReferencers = ShouldSortReferencers();
|
|
for (FDependsNode* ModifiedNode : CurrentRoundModifiedNodes)
|
|
{
|
|
FSetManageReferencesNodeData& ModifiedData = FindOrAddNodeData(ModifiedNode);
|
|
if (!ModifiedData.bModifiedByRound)
|
|
{
|
|
// A duplicate of a NodeData we already handled earlier in the list
|
|
continue;
|
|
}
|
|
|
|
// Restore all modified nodes to manage dependencies sorted and references sorted, so we can efficiently read
|
|
// them in future operations
|
|
ModifiedData.ManagedInEarlierRound |= ModifiedData.ManagedInThisRound;
|
|
ModifiedNode->SetIsDependencyListSorted(EDependencyCategory::Manage, bShouldSortDependencies);
|
|
ModifiedNode->SetIsReferencersSorted(bShouldSortReferencers);
|
|
|
|
ModifiedData.bModifiedByRound = false;
|
|
ModifiedData.ManagedInThisRound = EManageSearchColorBit::None;
|
|
ModifiedData.bDirectManagedInThisRound = false;
|
|
ModifiedData.DirectManagedAssetsThisRound.Empty();
|
|
for (FDependsNode*& DebugInstigator : ModifiedData.DebugInstigator)
|
|
{
|
|
DebugInstigator = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
bool UAssetRegistryImpl::SetPrimaryAssetIdForObjectPath(const FSoftObjectPath& ObjectPath, FPrimaryAssetId PrimaryAssetId)
|
|
{
|
|
UE::AssetRegistry::Impl::FEventContext EventContext;
|
|
bool bResult;
|
|
{
|
|
LLM_SCOPE(ELLMTag::AssetRegistry);
|
|
UE::AssetRegistry::FInterfaceWriteScopeLock InterfaceScopeLock(InterfaceLock);
|
|
bResult = GuardedData.SetPrimaryAssetIdForObjectPath(EventContext, ObjectPath, PrimaryAssetId);
|
|
}
|
|
Broadcast(EventContext);
|
|
return bResult;
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
bool FAssetRegistryImpl::SetPrimaryAssetIdForObjectPath(Impl::FEventContext& EventContext, const FSoftObjectPath& ObjectPath, FPrimaryAssetId PrimaryAssetId)
|
|
{
|
|
FAssetData* AssetData = State.GetMutableAssetByObjectPath(ObjectPath);
|
|
|
|
if (!AssetData)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FAssetDataTagMap TagsAndValues = AssetData->TagsAndValues.CopyMap();
|
|
TagsAndValues.Add(FPrimaryAssetId::PrimaryAssetTypeTag, PrimaryAssetId.PrimaryAssetType.ToString());
|
|
TagsAndValues.Add(FPrimaryAssetId::PrimaryAssetNameTag, PrimaryAssetId.PrimaryAssetName.ToString());
|
|
|
|
FAssetData NewAssetData(*AssetData);
|
|
NewAssetData.TagsAndValues = FAssetDataTagMapSharedView(MoveTemp(TagsAndValues));
|
|
UpdateAssetData(EventContext, AssetData, MoveTemp(NewAssetData), false /* bKeepDeletedTags */);
|
|
|
|
return true;
|
|
}
|
|
|
|
}
|
|
|
|
bool FAssetRegistryDependencyOptions::GetPackageQuery(UE::AssetRegistry::FDependencyQuery& Flags) const
|
|
{
|
|
Flags = UE::AssetRegistry::FDependencyQuery();
|
|
|
|
bool bHasFilter = false;
|
|
if (bIncludeSoftPackageReferences || bIncludeHardPackageReferences)
|
|
{
|
|
if (!bIncludeSoftPackageReferences) Flags.Required |= UE::AssetRegistry::EDependencyProperty::Hard;
|
|
if (!bIncludeHardPackageReferences) Flags.Excluded |= UE::AssetRegistry::EDependencyProperty::Hard;
|
|
bHasFilter = true;
|
|
}
|
|
if (bIncludeGamePackageReferences || bIncludeEditorOnlyPackageReferences)
|
|
{
|
|
if (!bIncludeGamePackageReferences) Flags.Excluded |= UE::AssetRegistry::EDependencyProperty::Game;
|
|
if (!bIncludeEditorOnlyPackageReferences) Flags.Required |= UE::AssetRegistry::EDependencyProperty::Game;
|
|
bHasFilter = true;
|
|
}
|
|
return bHasFilter;
|
|
}
|
|
|
|
bool FAssetRegistryDependencyOptions::GetSearchableNameQuery(UE::AssetRegistry::FDependencyQuery& Flags) const
|
|
{
|
|
Flags = UE::AssetRegistry::FDependencyQuery();
|
|
return bIncludeSearchableNames;
|
|
}
|
|
|
|
bool FAssetRegistryDependencyOptions::GetManageQuery(UE::AssetRegistry::FDependencyQuery& Flags) const
|
|
{
|
|
Flags = UE::AssetRegistry::FDependencyQuery();
|
|
if (bIncludeSoftManagementReferences || bIncludeHardManagementReferences)
|
|
{
|
|
if (!bIncludeSoftManagementReferences) Flags.Required |= UE::AssetRegistry::EDependencyProperty::Direct;
|
|
if (!bIncludeHardManagementReferences) Flags.Excluded |= UE::AssetRegistry::EDependencyProperty::Direct;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void FAssetDependency::WriteCompactBinary(FCbWriter& Writer) const
|
|
{
|
|
Writer.BeginArray();
|
|
Writer << AssetId;
|
|
static_assert(sizeof(uint8) >= sizeof(Category));
|
|
Writer.AddInteger((uint8)Category);
|
|
static_assert(sizeof(uint8) >= sizeof(Properties));
|
|
Writer.AddInteger((uint8)Properties);
|
|
Writer.EndArray();
|
|
}
|
|
|
|
bool LoadFromCompactBinary(FCbFieldView Field, FAssetDependency& Dependency)
|
|
{
|
|
FCbArrayView ArrayField = Field.AsArrayView();
|
|
if (ArrayField.Num() < 3)
|
|
{
|
|
Dependency = FAssetDependency();
|
|
return false;
|
|
}
|
|
FCbFieldViewIterator Iter = ArrayField.CreateViewIterator();
|
|
if (!LoadFromCompactBinary(Iter++, Dependency.AssetId))
|
|
{
|
|
Dependency = FAssetDependency();
|
|
return false;
|
|
}
|
|
uint8 Value;
|
|
if (LoadFromCompactBinary(Iter++, Value))
|
|
{
|
|
Dependency.Category = (UE::AssetRegistry::EDependencyCategory)Value;
|
|
}
|
|
else
|
|
{
|
|
Dependency = FAssetDependency();
|
|
return false;
|
|
}
|
|
if (LoadFromCompactBinary(Iter++, Value))
|
|
{
|
|
Dependency.Properties = (UE::AssetRegistry::EDependencyProperty)Value;
|
|
}
|
|
else
|
|
{
|
|
Dependency = FAssetDependency();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
|
|
const FAssetRegistryState& FAssetRegistryImpl::GetState() const
|
|
{
|
|
return State;
|
|
}
|
|
|
|
const FPathTree& FAssetRegistryImpl::GetCachedPathTree() const
|
|
{
|
|
return CachedPathTree;
|
|
}
|
|
|
|
const TSet<FName>& FAssetRegistryImpl::GetCachedEmptyPackages() const
|
|
{
|
|
return CachedEmptyPackages;
|
|
}
|
|
|
|
bool FAssetRegistryImpl::ShouldSkipAsset(FTopLevelAssetPath AssetClass, uint32 PackageFlags) const
|
|
{
|
|
#if WITH_ENGINE && WITH_EDITOR
|
|
UE::TReadScopeLock ReadLock(SkipClassesLock);
|
|
return Utils::ShouldSkipAsset(AssetClass, PackageFlags, SkipUncookedClasses, SkipCookedClasses);
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool FAssetRegistryImpl::ShouldSkipAsset(const UObject* InAsset) const
|
|
{
|
|
#if WITH_ENGINE && WITH_EDITOR
|
|
UE::TReadScopeLock ReadLock(SkipClassesLock);
|
|
return Utils::ShouldSkipAsset(InAsset, SkipUncookedClasses, SkipCookedClasses);
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
void FAssetRegistryImpl::PruneAndCoalescePackagesRequiringDependencyCalculation(TSet<FName>& BackgroundPackages,
|
|
TSet<FName>& GameThreadPackages, Impl::FInterruptionContext& InOutInterruptionContext)
|
|
{
|
|
RebuildAssetDependencyGathererMapIfNeeded();
|
|
|
|
FReadScopeLock GathererClassScopeLock(RegisteredDependencyGathererClassesLock);
|
|
|
|
// In many cases, this loop will be tight. If so, we don't want to spend a bunch of time checking whether we've
|
|
// run out of processing time. So only check every N iterations.
|
|
uint64 IterationCounter = 0;
|
|
auto ProcessSet = [&InOutInterruptionContext, &IterationCounter, this](TSet<FName>& SourceSet,
|
|
TSet<FName>* OptDestinationSet)->void
|
|
{
|
|
for (auto Iter = SourceSet.CreateIterator(); Iter; ++Iter)
|
|
{
|
|
bool HasAnyRegisteredDependencyGatherers = false;
|
|
State.EnumerateAssetsByPackageName(*Iter,
|
|
[this, &HasAnyRegisteredDependencyGatherers](const FAssetData* AssetData)
|
|
{
|
|
if (RegisteredDependencyGathererClasses.Contains(AssetData->AssetClassPath))
|
|
{
|
|
HasAnyRegisteredDependencyGatherers = true;
|
|
return false; // stop iterating
|
|
}
|
|
return true; // Keep iterating
|
|
});
|
|
|
|
// If we need to process this asset and we have a destination set, move it there
|
|
if ((OptDestinationSet != nullptr) && HasAnyRegisteredDependencyGatherers)
|
|
{
|
|
OptDestinationSet->Add(*Iter);
|
|
Iter.RemoveCurrent();
|
|
}
|
|
else if (!HasAnyRegisteredDependencyGatherers)
|
|
{
|
|
// If we don't have to process this asset, remove it from whichever list it is in
|
|
Iter.RemoveCurrent();
|
|
}
|
|
|
|
if ((++IterationCounter % 50) == 0)
|
|
{
|
|
if (InOutInterruptionContext.ShouldExitEarly())
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
ProcessSet(GameThreadPackages, nullptr);
|
|
if (InOutInterruptionContext.ShouldExitEarly())
|
|
{
|
|
return;
|
|
}
|
|
ProcessSet(BackgroundPackages, &GameThreadPackages);
|
|
}
|
|
#endif
|
|
|
|
namespace Impl
|
|
{
|
|
|
|
void FEventContext::Clear()
|
|
{
|
|
bScanStartedEventBroadcast = false;
|
|
bFileLoadedEventBroadcast = false;
|
|
bKnownGathersCompleteEventBroadcast = false;
|
|
bHasSentFileLoadedEventBroadcast = false;
|
|
ProgressUpdateData.Reset();
|
|
PathEvents.Empty();
|
|
AssetEvents.Empty();
|
|
RequiredLoads.Empty();
|
|
BlockedFiles.Empty();
|
|
}
|
|
|
|
bool FEventContext::IsEmpty() const
|
|
{
|
|
return !bScanStartedEventBroadcast &&
|
|
!bFileLoadedEventBroadcast &&
|
|
!bKnownGathersCompleteEventBroadcast &&
|
|
!ProgressUpdateData.IsSet() &&
|
|
PathEvents.Num() == 0 &&
|
|
AssetEvents.Num() == 0 &&
|
|
RequiredLoads.Num() == 0 &&
|
|
BlockedFiles.Num() == 0;
|
|
}
|
|
|
|
void FEventContext::Append(FEventContext&& Other)
|
|
{
|
|
if (&Other == this)
|
|
{
|
|
return;
|
|
}
|
|
bScanStartedEventBroadcast |= Other.bScanStartedEventBroadcast;
|
|
Other.bScanStartedEventBroadcast = false;
|
|
bFileLoadedEventBroadcast |= Other.bFileLoadedEventBroadcast;
|
|
Other.bFileLoadedEventBroadcast = false;
|
|
bKnownGathersCompleteEventBroadcast |= Other.bKnownGathersCompleteEventBroadcast;
|
|
Other.bKnownGathersCompleteEventBroadcast = false;
|
|
if (Other.ProgressUpdateData.IsSet())
|
|
{
|
|
ProgressUpdateData = MoveTemp(Other.ProgressUpdateData);
|
|
}
|
|
PathEvents.Append(MoveTemp(Other.PathEvents));
|
|
AssetEvents.Append(MoveTemp(Other.AssetEvents));
|
|
RequiredLoads.Append(MoveTemp(Other.RequiredLoads));
|
|
BlockedFiles.Append(MoveTemp(Other.BlockedFiles));
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
void UAssetRegistryImpl::ReadLockEnumerateAllTagToAssetDatas(TFunctionRef<bool(FName TagName, IAssetRegistry::FEnumerateAssetDatasFunc EnumerateAssets)> Callback) const
|
|
{
|
|
UE::AssetRegistry::FInterfaceReadScopeLock InterfaceScopeLock(InterfaceLock);
|
|
GuardedData.GetState().EnumerateTagToAssetDatas(Callback);
|
|
}
|
|
|
|
bool UAssetRegistryImpl::CanBroadcastEvents() const
|
|
{
|
|
return IsInGameThread() && !FUObjectThreadContext::Get().IsRoutingPostLoad;
|
|
}
|
|
|
|
void UAssetRegistryImpl::Broadcast(UE::AssetRegistry::Impl::FEventContext& EventContext, bool bAllowFileLoadedEvent)
|
|
{
|
|
using namespace UE::AssetRegistry::Impl;
|
|
if (!CanBroadcastEvents())
|
|
{
|
|
// By contract events (and PackageLoads) can only be sent on the game thread; some legacy systems depend on
|
|
// this and are not threadsafe. If we're not in the game thread, defer all events in the EventContext
|
|
// instead of broadcasting them on this thread
|
|
if (EventContext.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
// Broadcast should not be called on DeferredEvents; DeferredEvents should be moved to a separate EventContext
|
|
// and broadcast called on that separate EventContext outside of the lock.
|
|
FScopeLock DeferredEventsLock(&DeferredEventsCriticalSection);
|
|
check(&EventContext != &DeferredEvents);
|
|
DeferredEvents.Append(MoveTemp(EventContext));
|
|
RequestTick();
|
|
EventContext.Clear();
|
|
return;
|
|
}
|
|
|
|
if (EventContext.bScanStartedEventBroadcast)
|
|
{
|
|
// Raise event when the scan is started
|
|
ScanStartedEvent.Broadcast();
|
|
EventContext.bScanStartedEventBroadcast = false;
|
|
}
|
|
|
|
if (EventContext.PathEvents.Num())
|
|
{
|
|
if (PathsAddedEvent.IsBound() || PathsRemovedEvent.IsBound()
|
|
|| PathAddedEvent.IsBound() || PathRemovedEvent.IsBound())
|
|
{
|
|
// Batch add/remove events
|
|
TArray<FStringView> Params;
|
|
// Ensure loop batch condition is always false first iteration
|
|
bool bCurrentBatchIsAdd = EventContext.PathEvents[0].Get<1>() == FEventContext::EEvent::Added;
|
|
for (const TPair<FString, FEventContext::EEvent>& PathEvent : EventContext.PathEvents)
|
|
{
|
|
const FString& Path = PathEvent.Get<0>();
|
|
bool bEventIsAdd = PathEvent.Get<1>() == FEventContext::EEvent::Added;
|
|
if (bEventIsAdd != bCurrentBatchIsAdd)
|
|
{
|
|
(bCurrentBatchIsAdd ? PathsAddedEvent : PathsRemovedEvent).Broadcast(MakeArrayView(Params));
|
|
Params.Reset();
|
|
bCurrentBatchIsAdd = bEventIsAdd;
|
|
}
|
|
Params.Add(FStringView(Path));
|
|
}
|
|
if (Params.Num() != 0)
|
|
{
|
|
(bCurrentBatchIsAdd ? PathsAddedEvent : PathsRemovedEvent).Broadcast(MakeArrayView(Params));
|
|
}
|
|
|
|
// Legacy single events
|
|
if (PathAddedEvent.IsBound() || PathRemovedEvent.IsBound())
|
|
{
|
|
for (const TPair<FString, FEventContext::EEvent>& PathEvent : EventContext.PathEvents)
|
|
{
|
|
const FString& Path = PathEvent.Get<0>();
|
|
switch (PathEvent.Get<1>())
|
|
{
|
|
case FEventContext::EEvent::Added:
|
|
PathAddedEvent.Broadcast(Path);
|
|
break;
|
|
case FEventContext::EEvent::Removed:
|
|
PathRemovedEvent.Broadcast(Path);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
EventContext.PathEvents.Empty();
|
|
}
|
|
|
|
if (EventContext.AssetEvents.Num())
|
|
{
|
|
// Batch events so that if adds/updates are interspersed with removes, relative ordering of the add/remove is maintained
|
|
constexpr uint32 EventTypeCount = static_cast<uint32>(FEventContext::EEvent::MAX);
|
|
static_assert(EventTypeCount == 4, "Loop needs to be rewritten to correctly order new event types");
|
|
bool bHasListeners = AssetAddedEvent.IsBound() || AssetRemovedEvent.IsBound()
|
|
|| AssetUpdatedEvent.IsBound() || AssetUpdatedOnDiskEvent.IsBound();
|
|
if (!bHasListeners)
|
|
{
|
|
for (int32 i = 0; i < EventTypeCount; ++i)
|
|
{
|
|
if (BatchedAssetEvents[i].IsBound())
|
|
{
|
|
bHasListeners = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bHasListeners)
|
|
{
|
|
TArray<FAssetData> EventBatches[EventTypeCount];
|
|
|
|
// Pre-allocate memory for event batches
|
|
uint32 EventBatchCounters[EventTypeCount] = { 0 };
|
|
for (const TPair<FAssetData, FEventContext::EEvent>& AssetEvent : EventContext.AssetEvents)
|
|
{
|
|
FEventContext::EEvent Event = AssetEvent.Get<1>();
|
|
EventBatchCounters[static_cast<int32>(Event)]++;
|
|
}
|
|
|
|
for (int32 i = 0; i < UE_ARRAY_COUNT(EventBatches); ++i)
|
|
{
|
|
EventBatches[i].Reserve(EventBatchCounters[i]);
|
|
}
|
|
|
|
// Dispatch events
|
|
FEventContext::EEvent LastEvent = EventContext.AssetEvents[0].Get<1>();
|
|
auto FlushBatchedEvents = [&EventBatches, &Events = BatchedAssetEvents]()
|
|
{
|
|
for (int32 i = 0; i < UE_ARRAY_COUNT(EventBatches); ++i)
|
|
{
|
|
if (EventBatches[i].Num())
|
|
{
|
|
Events[i].Broadcast(EventBatches[i]);
|
|
EventBatches[i].Reset();
|
|
}
|
|
}
|
|
};
|
|
|
|
for (const TPair<FAssetData, FEventContext::EEvent>& AssetEvent : EventContext.AssetEvents)
|
|
{
|
|
const FAssetData& AssetData = AssetEvent.Get<0>();
|
|
FEventContext::EEvent Event = AssetEvent.Get<1>();
|
|
|
|
// Flush events when switching between removed and non-removed events
|
|
if ((Event == FEventContext::EEvent::Removed) != (LastEvent == FEventContext::EEvent::Removed))
|
|
{
|
|
FlushBatchedEvents();
|
|
}
|
|
EventBatches[static_cast<int32>(Event)].Add(AssetData);
|
|
LastEvent = Event;
|
|
}
|
|
// Flush last batch of events
|
|
FlushBatchedEvents();
|
|
|
|
// Single events
|
|
for (const TPair<FAssetData, FEventContext::EEvent>& AssetEvent : EventContext.AssetEvents)
|
|
{
|
|
const FAssetData& AssetData = AssetEvent.Get<0>();
|
|
switch (AssetEvent.Get<1>())
|
|
{
|
|
case FEventContext::EEvent::Added:
|
|
AssetAddedEvent.Broadcast(AssetData);
|
|
break;
|
|
case FEventContext::EEvent::Removed:
|
|
AssetRemovedEvent.Broadcast(AssetData);
|
|
break;
|
|
case FEventContext::EEvent::Updated:
|
|
AssetUpdatedEvent.Broadcast(AssetData);
|
|
break;
|
|
case FEventContext::EEvent::UpdatedOnDisk:
|
|
AssetUpdatedOnDiskEvent.Broadcast(AssetData);
|
|
break;
|
|
default:
|
|
checkNoEntry();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
EventContext.AssetEvents.Empty();
|
|
}
|
|
if (EventContext.VerseEvents.Num())
|
|
{
|
|
if (VerseAddedEvent.IsBound() || VerseRemovedEvent.IsBound())
|
|
{
|
|
for (const TPair<FName, FEventContext::EEvent>& VerseEvent : EventContext.VerseEvents)
|
|
{
|
|
const FName& VerseFilepath = VerseEvent.Get<0>();
|
|
switch (VerseEvent.Get<1>())
|
|
{
|
|
case FEventContext::EEvent::Added:
|
|
VerseAddedEvent.Broadcast(VerseFilepath);
|
|
break;
|
|
case FEventContext::EEvent::Removed:
|
|
VerseRemovedEvent.Broadcast(VerseFilepath);
|
|
break;
|
|
// (jcotton) We are not yet broadcasting Verse updating events as the only use case for VerseEvent broadcasts currently is to trigger a Verse-build
|
|
// and triggering a build on every change would be far too expensive.
|
|
case FEventContext::EEvent::Updated:
|
|
[[fallthrough]];
|
|
case FEventContext::EEvent::UpdatedOnDisk:
|
|
[[fallthrough]];
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
EventContext.VerseEvents.Empty();
|
|
}
|
|
if (EventContext.RequiredLoads.Num())
|
|
{
|
|
for (const FString& RequiredLoad : EventContext.RequiredLoads)
|
|
{
|
|
LoadPackage(nullptr, *RequiredLoad, 0);
|
|
}
|
|
EventContext.RequiredLoads.Empty();
|
|
}
|
|
if (EventContext.BlockedFiles.Num())
|
|
{
|
|
FilesBlockedEvent.Broadcast(EventContext.BlockedFiles);
|
|
EventContext.BlockedFiles.Empty();
|
|
}
|
|
|
|
if (EventContext.ProgressUpdateData.IsSet())
|
|
{
|
|
FileLoadProgressUpdatedEvent.Broadcast(*EventContext.ProgressUpdateData);
|
|
EventContext.ProgressUpdateData.Reset();
|
|
}
|
|
|
|
// FileLoadedEvent needs to come after all of the AssetEvents. Some systems do more expensive work for
|
|
// AssetEvents after receiving FileLoadedEvent, because they batched up that work for all assets in the initial load in
|
|
// their FileLoadedEvent handler. The AssetEvents precede the FileLoadedEvent in the broadcast that is sent from
|
|
// TickGatherer, so it is correct to make them precede it in the order in which we broadcast the events.
|
|
|
|
if (EventContext.bFileLoadedEventBroadcast || EventContext.bKnownGathersCompleteEventBroadcast)
|
|
{
|
|
bool bLocalFileLoadedEventBroadcast = EventContext.bFileLoadedEventBroadcast;
|
|
bool bLocalKnownGathersCompleteEventBroadcast = EventContext.bKnownGathersCompleteEventBroadcast;
|
|
|
|
if (!bAllowFileLoadedEvent)
|
|
{
|
|
// Do not send the file loaded event yet; pass the flag on instead
|
|
FScopeLock DeferredEventsLock(&DeferredEventsCriticalSection);
|
|
// Broadcast should not be called on DeferredEvents; DeferredEvents should be moved to a separate EventContext
|
|
// and broadcast called on that separate EventContext outside of the lock.
|
|
check(&EventContext != &DeferredEvents);
|
|
DeferredEvents.Append(MoveTemp(EventContext));
|
|
RequestTick();
|
|
EventContext.Clear();
|
|
check(!EventContext.bFileLoadedEventBroadcast && !EventContext.bKnownGathersCompleteEventBroadcast); // was cleared by Append and by Clear
|
|
check(!bLocalFileLoadedEventBroadcast || DeferredEvents.bFileLoadedEventBroadcast); // was set by Append
|
|
check(!bLocalKnownGathersCompleteEventBroadcast || DeferredEvents.bKnownGathersCompleteEventBroadcast); // was set by Append
|
|
return;
|
|
}
|
|
|
|
FEventContext CopiedDeferredEvents;
|
|
{
|
|
FScopeLock DeferredEventsLock(&DeferredEventsCriticalSection);
|
|
check(&EventContext != &DeferredEvents);
|
|
CopiedDeferredEvents = MoveTemp(DeferredEvents);
|
|
DeferredEvents.Clear();
|
|
}
|
|
if (!CopiedDeferredEvents.IsEmpty())
|
|
{
|
|
// Recursively send all of the deferred events, except for the completion events
|
|
// (FileLoaded, KnownGathersComplete). The completion events should not exist on DeferredEvents at this
|
|
// point, but it's not a problem if they do; merge them into our Local variables.
|
|
bLocalFileLoadedEventBroadcast |= CopiedDeferredEvents.bFileLoadedEventBroadcast;
|
|
CopiedDeferredEvents.bFileLoadedEventBroadcast = false;
|
|
|
|
bLocalKnownGathersCompleteEventBroadcast |= CopiedDeferredEvents.bKnownGathersCompleteEventBroadcast;
|
|
CopiedDeferredEvents.bKnownGathersCompleteEventBroadcast = false;
|
|
|
|
Broadcast(CopiedDeferredEvents, false /* bAllowFileLoadedEvent */);
|
|
}
|
|
// Now it is safe to broadcast the completion events. If other deferred events come in on another thread
|
|
// after we copied from DeferredEvents, that is okay; the contract for completion events is that they are
|
|
// guaranteed to be sent after any non-completion events sent before completion was triggered,
|
|
// but they can be before or after non-completion events that occurred after completion was triggered.
|
|
if (bLocalFileLoadedEventBroadcast)
|
|
{
|
|
double BroadcastTimeStart = FPlatformTime::Seconds();
|
|
UE_LOG(LogAssetRegistry, Display, TEXT("Starting OnFilesLoaded.Broadcast"));
|
|
FileLoadedEvent.Broadcast();
|
|
ScanEndedEvent.Broadcast();
|
|
UE_LOG(LogAssetRegistry, Display, TEXT("Completed OnFilesLoaded.Broadcast: %.3fs"),
|
|
(float) FPlatformTime::Seconds() - BroadcastTimeStart);
|
|
EventContext.bHasSentFileLoadedEventBroadcast = true;
|
|
EventContext.bFileLoadedEventBroadcast = false;
|
|
}
|
|
if (bLocalKnownGathersCompleteEventBroadcast)
|
|
{
|
|
KnownGathersCompleteEvent.Broadcast();
|
|
EventContext.bKnownGathersCompleteEventBroadcast = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
UAssetRegistryImpl::FFilesBlockedEvent& UAssetRegistryImpl::OnFilesBlocked()
|
|
{
|
|
return FilesBlockedEvent;
|
|
}
|
|
|
|
UAssetRegistryImpl::FPathsEvent& UAssetRegistryImpl::OnPathsAdded()
|
|
{
|
|
return PathsAddedEvent;
|
|
}
|
|
|
|
UAssetRegistryImpl::FPathsEvent& UAssetRegistryImpl::OnPathsRemoved()
|
|
{
|
|
return PathsRemovedEvent;
|
|
}
|
|
|
|
UAssetRegistryImpl::FPathAddedEvent& UAssetRegistryImpl::OnPathAdded()
|
|
{
|
|
return PathAddedEvent;
|
|
}
|
|
|
|
UAssetRegistryImpl::FPathRemovedEvent& UAssetRegistryImpl::OnPathRemoved()
|
|
{
|
|
return PathRemovedEvent;
|
|
}
|
|
|
|
IAssetRegistry::FAssetCollisionEvent& UAssetRegistryImpl::OnAssetCollision_Private()
|
|
{
|
|
return GuardedData.OnAssetCollision_Private();
|
|
}
|
|
|
|
UAssetRegistryImpl::FAssetAddedEvent& UAssetRegistryImpl::OnAssetAdded()
|
|
{
|
|
return AssetAddedEvent;
|
|
}
|
|
|
|
UAssetRegistryImpl::FAssetRemovedEvent& UAssetRegistryImpl::OnAssetRemoved()
|
|
{
|
|
return AssetRemovedEvent;
|
|
}
|
|
|
|
UAssetRegistryImpl::FAssetRenamedEvent& UAssetRegistryImpl::OnAssetRenamed()
|
|
{
|
|
return AssetRenamedEvent;
|
|
}
|
|
|
|
UAssetRegistryImpl::FAssetUpdatedEvent& UAssetRegistryImpl::OnAssetUpdated()
|
|
{
|
|
return AssetUpdatedEvent;
|
|
}
|
|
|
|
UAssetRegistryImpl::FAssetUpdatedEvent& UAssetRegistryImpl::OnAssetUpdatedOnDisk()
|
|
{
|
|
return AssetUpdatedOnDiskEvent;
|
|
}
|
|
|
|
UAssetRegistryImpl::FAssetsEvent& UAssetRegistryImpl::OnAssetsAdded()
|
|
{
|
|
return BatchedAssetEvents[static_cast<SIZE_T>(UE::AssetRegistry::Impl::FEventContext::EEvent::Added)];
|
|
}
|
|
|
|
UAssetRegistryImpl::FAssetsEvent& UAssetRegistryImpl::OnAssetsUpdated()
|
|
{
|
|
return BatchedAssetEvents[static_cast<SIZE_T>(UE::AssetRegistry::Impl::FEventContext::EEvent::Updated)];
|
|
}
|
|
|
|
UAssetRegistryImpl::FAssetsEvent& UAssetRegistryImpl::OnAssetsUpdatedOnDisk()
|
|
{
|
|
return BatchedAssetEvents[static_cast<SIZE_T>(UE::AssetRegistry::Impl::FEventContext::EEvent::UpdatedOnDisk)];
|
|
}
|
|
|
|
UAssetRegistryImpl::FAssetsEvent& UAssetRegistryImpl::OnAssetsRemoved()
|
|
{
|
|
return BatchedAssetEvents[static_cast<SIZE_T>(UE::AssetRegistry::Impl::FEventContext::EEvent::Removed)];
|
|
}
|
|
|
|
UAssetRegistryImpl::FInMemoryAssetCreatedEvent& UAssetRegistryImpl::OnInMemoryAssetCreated()
|
|
{
|
|
return InMemoryAssetCreatedEvent;
|
|
}
|
|
|
|
UAssetRegistryImpl::FInMemoryAssetDeletedEvent& UAssetRegistryImpl::OnInMemoryAssetDeleted()
|
|
{
|
|
return InMemoryAssetDeletedEvent;
|
|
}
|
|
|
|
UAssetRegistryImpl::FVerseAddedEvent& UAssetRegistryImpl::OnVerseAdded()
|
|
{
|
|
return VerseAddedEvent;
|
|
}
|
|
|
|
UAssetRegistryImpl::FVerseRemovedEvent& UAssetRegistryImpl::OnVerseRemoved()
|
|
{
|
|
return VerseRemovedEvent;
|
|
}
|
|
|
|
UAssetRegistryImpl::FFilesLoadedEvent& UAssetRegistryImpl::OnFilesLoaded()
|
|
{
|
|
return FileLoadedEvent;
|
|
}
|
|
|
|
UAssetRegistryImpl::FFileLoadProgressUpdatedEvent& UAssetRegistryImpl::OnFileLoadProgressUpdated()
|
|
{
|
|
return FileLoadProgressUpdatedEvent;
|
|
}
|
|
|
|
UAssetRegistryImpl::FScanStartedEvent& UAssetRegistryImpl::OnScanStarted()
|
|
{
|
|
return ScanStartedEvent;
|
|
}
|
|
|
|
UAssetRegistryImpl::FScanEndedEvent& UAssetRegistryImpl::OnScanEnded()
|
|
{
|
|
return ScanEndedEvent;
|
|
}
|
|
|
|
UAssetRegistryImpl::FKnownGathersCompleteEvent& UAssetRegistryImpl::OnKnownGathersComplete()
|
|
{
|
|
return KnownGathersCompleteEvent;
|
|
}
|
|
|
|
namespace UE::AssetRegistry
|
|
{
|
|
const FAssetData* GetMostImportantAsset(TConstArrayView<const FAssetData*> PackageAssetDatas, EGetMostImportantAssetFlags InFlags)
|
|
{
|
|
if (PackageAssetDatas.Num() == 1) // common case
|
|
{
|
|
return PackageAssetDatas[0];
|
|
}
|
|
|
|
// Find a candidate asset.
|
|
// If there's a "UAsset", then we use that as the asset.
|
|
// If not, then we look for a "TopLevelAsset", i.e. one that shows up in the content browser.
|
|
int32 TopLevelAssetCount = 0;
|
|
|
|
// If we have multiple TLAs, then we pick the "least" TLA.
|
|
// If we have NO TLAs, then we pick the "least" asset,
|
|
// both determined by class then name:
|
|
auto AssetDataLessThan = [](const FAssetData* LHS, const FAssetData* RHS)
|
|
{
|
|
int32 ClassCompare = LHS->AssetClassPath.Compare(RHS->AssetClassPath);
|
|
if (ClassCompare == 0)
|
|
{
|
|
return LHS->AssetName.LexicalLess(RHS->AssetName);
|
|
}
|
|
return ClassCompare < 0;
|
|
};
|
|
|
|
const FAssetData* LeastTopLevelAsset = nullptr;
|
|
const FAssetData* LeastAsset = nullptr;
|
|
for (const FAssetData* Asset : PackageAssetDatas)
|
|
{
|
|
if (Asset->AssetName.IsNone())
|
|
{
|
|
continue;
|
|
}
|
|
if (Asset->IsUAsset())
|
|
{
|
|
return Asset;
|
|
}
|
|
// This is after IsUAsset because Blueprints can be the UAsset but also be considered skipable.
|
|
if (!EnumHasAnyFlags(InFlags, EGetMostImportantAssetFlags::IgnoreSkipClasses))
|
|
{
|
|
if (FFiltering::ShouldSkipAsset(Asset->AssetClassPath, Asset->PackageFlags))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (Asset->IsTopLevelAsset())
|
|
{
|
|
TopLevelAssetCount++;
|
|
if (LeastTopLevelAsset == nullptr ||
|
|
AssetDataLessThan(Asset, LeastTopLevelAsset))
|
|
{
|
|
LeastTopLevelAsset = Asset;
|
|
}
|
|
}
|
|
if (LeastAsset == nullptr ||
|
|
AssetDataLessThan(Asset, LeastAsset))
|
|
{
|
|
LeastAsset = Asset;
|
|
}
|
|
}
|
|
|
|
if (EnumHasAnyFlags(InFlags, EGetMostImportantAssetFlags::RequireOneTopLevelAsset))
|
|
{
|
|
if (TopLevelAssetCount == 1)
|
|
{
|
|
return LeastTopLevelAsset;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
if (TopLevelAssetCount)
|
|
{
|
|
return LeastTopLevelAsset;
|
|
}
|
|
return LeastAsset;
|
|
}
|
|
|
|
|
|
void GetAssetForPackages(TConstArrayView<FName> PackageNames, TMap<FName, FAssetData>& OutPackageToAssetData)
|
|
{
|
|
FARFilter Filter;
|
|
for (FName PackageName : PackageNames)
|
|
{
|
|
Filter.PackageNames.Add(PackageName);
|
|
}
|
|
|
|
TArray<FAssetData> AssetDataList;
|
|
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
|
|
if (!AssetRegistry)
|
|
{
|
|
return;
|
|
}
|
|
AssetRegistry->GetAssets(Filter, AssetDataList);
|
|
|
|
if (AssetDataList.Num() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Algo::SortBy(AssetDataList, &FAssetData::PackageName, FNameFastLess());
|
|
|
|
TArray<const FAssetData*, TInlineAllocator<1>> PackageAssetDatas;
|
|
FName CurrentPackageName = AssetDataList[0].PackageName;
|
|
for (const FAssetData& AssetData : AssetDataList)
|
|
{
|
|
if (CurrentPackageName != AssetData.PackageName)
|
|
{
|
|
OutPackageToAssetData.FindOrAdd(CurrentPackageName) = *GetMostImportantAsset(PackageAssetDatas);
|
|
PackageAssetDatas.Reset();
|
|
CurrentPackageName = AssetData.PackageName;
|
|
}
|
|
|
|
PackageAssetDatas.Push(&AssetData);
|
|
}
|
|
|
|
OutPackageToAssetData.FindOrAdd(CurrentPackageName) = *GetMostImportantAsset(PackageAssetDatas);
|
|
|
|
}
|
|
|
|
bool ShouldSearchAllAssetsAtStart()
|
|
{
|
|
// Search at start for configurations that need the entire assetregistry and that do not load it from serialized:
|
|
// Need it: Editor IDE, CookCommandlet, other Allowlist Commandlets
|
|
// Possibly need it: editor running as -game or -server
|
|
// Do not need it: Commandlets not on the Allowlist
|
|
// Load it from serialized: Non-editor-executable
|
|
//
|
|
// This behavior can be overridden with commandline option.
|
|
//
|
|
// For the editor-executable configurations that do not search at start, the search will be triggered when
|
|
// SearchAllAssets or ScanPathsSynchronous is called.
|
|
|
|
bool bSearchAllAssetsAtStart = false;
|
|
if (GIsEditor)
|
|
{
|
|
if (!IsRunningCommandlet() || IsRunningCookCommandlet())
|
|
{
|
|
bSearchAllAssetsAtStart = true;
|
|
}
|
|
else
|
|
{
|
|
TArray<FString> CommandletsUsingAR;
|
|
GConfig->GetArray(TEXT("AssetRegistry"), TEXT("CommandletsUsingAR"), CommandletsUsingAR, GEngineIni);
|
|
FString CommandlineCommandlet;
|
|
FString CommandletToken(TEXT("commandlet"));
|
|
if (!CommandletsUsingAR.IsEmpty() &&
|
|
FParse::Value(FCommandLine::Get(), TEXT("-run="), CommandlineCommandlet))
|
|
{
|
|
if (CommandlineCommandlet.EndsWith(CommandletToken))
|
|
{
|
|
CommandlineCommandlet.LeftChopInline(CommandletToken.Len(), EAllowShrinking::No);
|
|
}
|
|
for (FString& CommandletUsingAR : CommandletsUsingAR)
|
|
{
|
|
if (CommandletUsingAR.EndsWith(CommandletToken))
|
|
{
|
|
CommandletUsingAR.LeftChopInline(CommandletToken.Len(), EAllowShrinking::No);
|
|
}
|
|
if (CommandletUsingAR == CommandlineCommandlet)
|
|
{
|
|
bSearchAllAssetsAtStart = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#if WITH_EDITOR
|
|
else
|
|
{
|
|
using namespace UE::AssetDataGather::Private;
|
|
bSearchAllAssetsAtStart = GGatherSettings.GetEditorGameScanMode() != EEditorGameScanMode::None;
|
|
}
|
|
#endif
|
|
#if WITH_EDITOR || !UE_BUILD_SHIPPING
|
|
bool bCommandlineAllAssetsAtStart;
|
|
if (FParse::Bool(FCommandLine::Get(), TEXT("AssetGatherAll="), bCommandlineAllAssetsAtStart))
|
|
{
|
|
bSearchAllAssetsAtStart = bCommandlineAllAssetsAtStart;
|
|
}
|
|
#endif // WITH_EDITOR || !UE_BUILD_SHIPPING
|
|
return bSearchAllAssetsAtStart;
|
|
}
|
|
|
|
namespace Impl
|
|
{
|
|
|
|
bool FInterruptionContext::ShouldExitEarly()
|
|
{
|
|
if (EarlyExitCallback && EarlyExitCallback())
|
|
{
|
|
OutInterrupted = true;
|
|
}
|
|
else if (TickStartTime > 0 && ((FPlatformTime::Seconds() - TickStartTime) > MaxRunningTime))
|
|
{
|
|
OutInterrupted = true;
|
|
}
|
|
return OutInterrupted;
|
|
}
|
|
|
|
}
|
|
|
|
FString CreateStandardFilename(const FString& InPath)
|
|
{
|
|
FString Result = FPaths::CreateStandardFilename(InPath);
|
|
|
|
// Follow most of the behavior of FPaths::CreateStandardFilename, but tweak it to also
|
|
// replace '\' -> '/' even when the path is not convertible to an engine-relative path.
|
|
// And add a special case tweak of that tweak, so that a windows-style network share \\
|
|
// is left unchanged and remains understandable by the OS.
|
|
bool bKeepLeadingSlashes = InPath.StartsWith(TEXTVIEW("\\\\"));
|
|
Result.ReplaceInline(TEXT("\\"), TEXT("/"), ESearchCase::CaseSensitive);
|
|
if (bKeepLeadingSlashes)
|
|
{
|
|
Result[0] = '\\';
|
|
Result[1] = '\\';
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
} // namespace AssetRegistry
|