// Copyright Epic Games, Inc. All Rights Reserved. #include "MetasoundDocumentBuilderRegistry.h" #include "HAL/PlatformProperties.h" #include "MetasoundAssetManager.h" #include "MetasoundGlobals.h" #include "MetasoundSettings.h" #include "MetasoundTrace.h" #include "MetasoundUObjectRegistry.h" namespace Metasound::Engine { FDocumentBuilderRegistry::~FDocumentBuilderRegistry() { TMultiMap> BuildersToFinish; FScopeLock Lock(&BuildersCriticalSection); { BuildersToFinish = MoveTemp(Builders); Builders.Reset(); } UE_CLOG(!BuildersToFinish.IsEmpty(), LogMetaSound, Display, TEXT("BuilderRegistry is shutting down with the following %i active builder entries. Forcefully shutting down:"), BuildersToFinish.Num()); int32 NumStale = 0; for (const TPair>& Pair : BuildersToFinish) { const bool bIsValid = Pair.Value.IsValid(); if (bIsValid) { UE_CLOG(bIsValid, LogMetaSound, Display, TEXT("- %s"), *Pair.Value->GetFullName()); constexpr bool bForceUnregister = true; FinishBuildingInternal(*Pair.Value.Get(), bForceUnregister); } else { ++NumStale; } } UE_CLOG(NumStale > 0, LogMetaSound, Display, TEXT("BuilderRegistry is shutting down with %i stale entries"), NumStale); } void FDocumentBuilderRegistry::AddBuilderInternal(const FMetasoundFrontendClassName& InClassName, UMetaSoundBuilderBase* NewBuilder) const { FScopeLock Lock(&BuildersCriticalSection); // #if !NO_LOGGING // bool bLogDuplicateEntries = CanPostEventLog(ELogEvent::DuplicateEntries, ELogVerbosity::Error); // if (bLogDuplicateEntries) // { // bLogDuplicateEntries = Builders.Contains(InClassName); // } // #endif // !NO_LOGGING Builders.Add(InClassName, NewBuilder); // #if !NO_LOGGING // if (bLogDuplicateEntries) // { // TArray> Entries; // Builders.MultiFind(InClassName, Entries); // // // Don't print stale entries as during cook and some editor asset actions, // // these may be removed after a new valid builder is created. If stale // // entries leak, they will show up on registry logging upon destruction. // Entries.RemoveAllSwap([](const TWeakObjectPtr& Builder) { return !Builder.IsValid(); }); // // if (!Entries.IsEmpty()) // { // UE_LOG(LogMetaSound, Error, TEXT("More than one asset registered with class name '%s'. " // "Look-up may return builder that is not associated with desired object! \n" // "This can happen if asset was moved using revision control and original location was revived. \n" // "Remove all but one of the following assets and relink a duplicate or copied replacement asset:"), // *InClassName.ToString()); // for (const TWeakObjectPtr& BuilderPtr : Entries) // { // UE_LOG(LogMetaSound, Error, TEXT("- %s"), *BuilderPtr->GetConstBuilder().CastDocumentObjectChecked().GetPathName()); // } // } // } // #endif // !NO_LOGGING } bool FDocumentBuilderRegistry::CanPostEventLog(ELogEvent Event, ELogVerbosity::Type Verbosity) const { #if NO_LOGGING return false; #else // !NO_LOGGING if (const ELogVerbosity::Type* SetVerbosity = EventLogVerbosity.Find(Event)) { return *SetVerbosity >= Verbosity; } return true; #endif // !NO_LOGGING } #if WITH_EDITORONLY_DATA bool FDocumentBuilderRegistry::CookPages(FName PlatformName, FMetaSoundFrontendDocumentBuilder& Builder) const { METASOUND_LLM_SCOPE; METASOUND_TRACE_CPUPROFILER_EVENT_SCOPE(FDocumentBuilderRegistry::CookPages); bool bModified = false; const UMetaSoundSettings* Settings = GetDefault(); check(Settings); const TArray PlatformTargetPageIDs = Settings->GetCookedTargetPageIDs(PlatformName); checkf(!PlatformTargetPageIDs.IsEmpty(), TEXT("Must have at least one targeted page ID to cook MetaSound.")); const FString DebugName = Builder.GetDebugName(); auto StripPageEntries = [&]( TArray& PageIdsToResolve, TSet& ResolveTargetScratch, TFunctionRef RemovePageItem, const FName& ItemName, const FString& ItemType) { ResolveTargetScratch.Reset(); for (const FGuid& TargetPage : PlatformTargetPageIDs) { const FGuid PageID = ResolveTargetPageIDInternal(*Settings, PageIdsToResolve, TargetPage, PlatformName); ResolveTargetScratch.Add(PageID); } auto IsResolvedTarget = [&ResolveTargetScratch](const FGuid& PageID) { return ResolveTargetScratch.Contains(PageID); }; checkf(!ResolveTargetScratch.IsEmpty(), TEXT("Failed to resolve any valid target IDs, which will leave serialized page array in invalid state.")); PageIdsToResolve.RemoveAllSwap(IsResolvedTarget, EAllowShrinking::No); for (const FGuid& PageID : PageIdsToResolve) { const bool bRemovedPageItem = RemovePageItem(PageID); if (bRemovedPageItem) { UE_LOG(LogMetaSound, Display, TEXT("%s: Removed %s %s w/PageID '%s'"), *DebugName, ItemName.IsNone() ? TEXT("paged") : *ItemName.ToString(), *ItemType, *PageID.ToString()); } bModified |= bRemovedPageItem; } }; const FMetasoundFrontendDocument& Document = Builder.GetConstDocumentChecked(); TArray ResolvePageIDs; TSet ResolvedTargetScratchIDs; { // Strip graphs auto AddPageID = [&ResolvePageIDs](const FMetasoundFrontendGraph& Graph) { ResolvePageIDs.Add(Graph.PageID); }; Document.RootGraph.IterateGraphPages(AddPageID); auto RemoveGraphPage = [&Builder](const FGuid& InPageID) { return Builder.RemoveGraphPage(InPageID); }; const int32 NumInitGraphs = Document.RootGraph.GetConstGraphPages().Num(); StripPageEntries(ResolvePageIDs, ResolvedTargetScratchIDs, RemoveGraphPage, FName(), TEXT("graph")); const int32 NumRemainingGraphs = Document.RootGraph.GetConstGraphPages().Num(); checkf(NumRemainingGraphs > 0, TEXT("Document in MetaSound asset '%s' had all default values " "cooked away leaving it in an invalid state. " "Graph must always have at least one implementation."), *Builder.GetDebugName()); if (NumInitGraphs > NumRemainingGraphs) { UE_LOG(LogMetaSound, Display, TEXT("Cook removed %i graph page(s) from '%s'"), NumInitGraphs - NumRemainingGraphs, *Builder.GetDebugName()); } } { // Strip default input values for (const FMetasoundFrontendClassInput& GraphInput : Document.RootGraph.GetDefaultInterface().Inputs) { ResolvePageIDs.Reset(); auto AddPageID = [&ResolvePageIDs](const FGuid& PageID, const FMetasoundFrontendLiteral&) { ResolvePageIDs.Add(PageID); }; GraphInput.IterateDefaults(AddPageID); auto RemoveDefault = [&Builder, &GraphInput](const FGuid& InPageID) { const bool bClearInheritsDefault = false; return Builder.RemoveGraphInputDefault(GraphInput.Name, InPageID, bClearInheritsDefault); }; const int32 NumInitDefaults = GraphInput.GetDefaults().Num(); StripPageEntries(ResolvePageIDs, ResolvedTargetScratchIDs, RemoveDefault, GraphInput.Name, TEXT("input default")); const int32 NumRemainingDefaults = GraphInput.GetDefaults().Num(); checkf(NumRemainingDefaults > 0, TEXT("Input '%s' had all default values stripped leaving it in an invalid state. " "Input must always have at least one default value"), *GraphInput.Name.ToString()); if (NumInitDefaults > NumRemainingDefaults) { UE_LOG(LogMetaSound, Display, TEXT("Cook removed %i default input page value(s) from input '%s'"), NumInitDefaults - NumRemainingDefaults, *GraphInput.Name.ToString()); } } } return bModified; } FMetaSoundFrontendDocumentBuilder& FDocumentBuilderRegistry::FindOrBeginBuilding(TScriptInterface MetaSound) { UObject* Object = MetaSound.GetObject(); check(Object); return FindOrBeginBuilding(*Object).GetBuilder(); } #endif // WITH_EDITORONLY_DATA FMetaSoundFrontendDocumentBuilder* FDocumentBuilderRegistry::FindBuilder(TScriptInterface MetaSound) const { if (UMetaSoundBuilderBase* Builder = FindBuilderObject(MetaSound)) { return &Builder->GetBuilder(); } return nullptr; } FMetaSoundFrontendDocumentBuilder* FDocumentBuilderRegistry::FindBuilder(const FMetasoundFrontendClassName& InClassName, const FTopLevelAssetPath& AssetPath) const { if (UMetaSoundBuilderBase* Builder = FindBuilderObject(InClassName, AssetPath)) { return &Builder->GetBuilder(); } return nullptr; } UMetaSoundBuilderBase* FDocumentBuilderRegistry::FindBuilderObject(TScriptInterface MetaSound) const { UMetaSoundBuilderBase* FoundEntry = nullptr; if (const UObject* MetaSoundObject = MetaSound.GetObject()) { const FMetasoundFrontendDocument& Document = MetaSound->GetConstDocument(); const FMetasoundFrontendClassName& ClassName = Document.RootGraph.Metadata.GetClassName(); TArray> Entries; { FScopeLock Lock(&BuildersCriticalSection); Builders.MultiFind(ClassName, Entries); } for (const TWeakObjectPtr& BuilderPtr : Entries) { if (UMetaSoundBuilderBase* Builder = BuilderPtr.Get()) { // Can be invalid if look-up is called during asset removal/destruction or the entry was // prematurely "finished". Only return invalid entry if builder asset path cannot be // matched as this is likely the destroyed entry associated with the provided AssetPath. const FMetaSoundFrontendDocumentBuilder& DocBuilder = Builder->GetConstBuilder(); if (DocBuilder.IsValid()) { UObject& TestMetaSound = BuilderPtr->GetConstBuilder().CastDocumentObjectChecked(); if (&TestMetaSound == MetaSoundObject) { FoundEntry = Builder; break; } } else { FoundEntry = Builder; } } } } return FoundEntry; } UMetaSoundBuilderBase* FDocumentBuilderRegistry::FindBuilderObject(const FMetasoundFrontendClassName& InClassName, const FTopLevelAssetPath& AssetPath) const { TArray> Entries; { FScopeLock Lock(&BuildersCriticalSection); Builders.MultiFind(InClassName, Entries); } UMetaSoundBuilderBase* FoundEntry = nullptr; for (const TWeakObjectPtr& BuilderPtr : Entries) { if (UMetaSoundBuilderBase* Builder = BuilderPtr.Get()) { const FMetaSoundFrontendDocumentBuilder& DocBuilder = Builder->GetConstBuilder(); // Can be invalid if look-up is called during asset removal/destruction or the entry was // prematurely "finished". Only return invalid entry if builder asset path cannot be // matched as this is likely the destroyed entry associated with the provided AssetPath. if (DocBuilder.IsValid()) { const UObject& DocObject = DocBuilder.CastDocumentObjectChecked(); FTopLevelAssetPath ObjectPath; if (ObjectPath.TrySetPath(&DocObject)) { if (AssetPath.IsNull() || AssetPath == ObjectPath) { FoundEntry = Builder; break; } } else { FoundEntry = Builder; } } else { FoundEntry = Builder; } } } return FoundEntry; } TArray FDocumentBuilderRegistry::FindBuilderObjects(const FMetasoundFrontendClassName& InClassName) const { TArray FoundBuilders; TArray> Entries; { FScopeLock Lock(&BuildersCriticalSection); Builders.MultiFind(InClassName, Entries); } if (!Entries.IsEmpty()) { Algo::TransformIf(Entries, FoundBuilders, [](const TWeakObjectPtr& BuilderPtr) { return BuilderPtr.IsValid(); }, [](const TWeakObjectPtr& BuilderPtr) { return BuilderPtr.Get(); } ); } return FoundBuilders; } FMetaSoundFrontendDocumentBuilder* FDocumentBuilderRegistry::FindOutermostBuilder(const UObject& InSubObject) const { using namespace Metasound::Frontend; TScriptInterface DocumentInterface = InSubObject.GetOutermostObject(); check(DocumentInterface.GetObject()); return FindBuilder(DocumentInterface); } bool FDocumentBuilderRegistry::FinishBuilding(const FMetasoundFrontendClassName& InClassName, bool bForceUnregisterNodeClass) const { using namespace Metasound; using namespace Metasound::Engine; TArray FoundBuilders = FindBuilderObjects(InClassName); for (UMetaSoundBuilderBase* Builder : FoundBuilders) { FinishBuildingInternal(*Builder, bForceUnregisterNodeClass); } FScopeLock Lock(&BuildersCriticalSection); return Builders.Remove(InClassName) > 0; } bool FDocumentBuilderRegistry::FinishBuilding(const FMetasoundFrontendClassName& InClassName, const FTopLevelAssetPath& AssetPath, bool bForceUnregisterNodeClass) const { using namespace Metasound; using namespace Metasound::Engine; TWeakObjectPtr BuilderPtr; if (UMetaSoundBuilderBase* Builder = FindBuilderObject(InClassName, AssetPath)) { FinishBuildingInternal(*Builder, bForceUnregisterNodeClass); BuilderPtr = TWeakObjectPtr(Builder); } FScopeLock Lock(&BuildersCriticalSection); return Builders.RemoveSingle(InClassName, BuilderPtr) > 0; } void FDocumentBuilderRegistry::FinishBuildingInternal(UMetaSoundBuilderBase& Builder, bool bForceUnregisterNodeClass) const { using namespace Metasound; using namespace Metasound::Frontend; // If the builder has applied transactions to its document object that are not mirrored in the frontend registry, // unregister version in registry. This will ensure that future requests for the builder's associated asset will // register a fresh version from the object as the transaction history is intrinsically lost once this builder // is destroyed. It is also possible that the DocBuilder's underlying object can be invalid if object was force // deleted, so validity check is necessary. FMetaSoundFrontendDocumentBuilder& DocBuilder = Builder.GetBuilder(); if (DocBuilder.IsValid()) { if (Metasound::CanEverExecuteGraph()) { const int32 TransactionCount = DocBuilder.GetTransactionCount(); const int32 LastTransactionRegistered = Builder.GetLastTransactionRegistered(); if (bForceUnregisterNodeClass || LastTransactionRegistered != TransactionCount) { UObject& MetaSound = DocBuilder.CastDocumentObjectChecked(); if (FMetasoundAssetBase* MetaSoundAsset = IMetasoundUObjectRegistry::Get().GetObjectAsAssetBase(&MetaSound)) { MetaSoundAsset->UnregisterGraphWithFrontend(); } } } DocBuilder.FinishBuilding(); } } #if WITH_EDITOR FOnResolveEditorPage& FDocumentBuilderRegistry::GetOnResolveAuditionPageDelegate() { return OnResolveAuditionPage; } #endif // WITH_EDITOR FOnResolvePage& FDocumentBuilderRegistry::GetOnResolveProjectPageOverrideDelegate() { return OnResolveProjectPage; } bool FDocumentBuilderRegistry::ReloadBuilder(const FMetasoundFrontendClassName& InClassName) const { bool bReloaded = false; TArray ClassBuilders = FindBuilderObjects(InClassName); for (UMetaSoundBuilderBase* Builder : ClassBuilders) { Builder->Reload(); bReloaded = true; } return bReloaded; } FGuid FDocumentBuilderRegistry::ResolveTargetPageID(const FMetasoundFrontendGraphClass& InGraphClass) const { METASOUND_LLM_SCOPE; METASOUND_TRACE_CPUPROFILER_EVENT_SCOPE(FDocumentBuilderRegistry::ResolveTargetPageID_GraphClass); #if !WITH_EDITORONLY_DATA // No resolution required if only one item. This is the typical scenario and saves from tying up a lock // and calling resolution delegate(s). For larger graphs, this can add up, and most implementation // only has a single page value. With editor-only data, go ahead and take the perf hit in favor of // resolution reporting (for example if page data is invalid and needs to be fixed up). const TArray& GraphPages = InGraphClass.GetConstGraphPages(); if (GraphPages.Num() == 1) { return GraphPages.Last().PageID; } #endif // !WITH_EDITORONLY_DATA FScopeLock Lock(&TargetPageResolveScratchCritSec); TargetPageResolveScratch.Reset(); InGraphClass.IterateGraphPages([this](const FMetasoundFrontendGraph& PageGraph) { TargetPageResolveScratch.Add(PageGraph.PageID); }); return ResolveTargetPageIDInternal(TargetPageResolveScratch); } FGuid FDocumentBuilderRegistry::ResolveTargetPageID(const FMetasoundFrontendClassInput& InClassInput) const { METASOUND_LLM_SCOPE; METASOUND_TRACE_CPUPROFILER_EVENT_SCOPE(FDocumentBuilderRegistry::ResolveTargetPageID_ClassInput); #if !WITH_EDITORONLY_DATA // No resolution required if only one item. This is the typical scenario and saves from tying up a lock // and calling resolution delegate(s). For larger graphs, this can add up, and most implementation // only has a single page value. With editor-only data, go ahead and take the perf hit in favor of // resolution reporting (for example if page data is invalid and needs to be fixed up). const TArray& ClassDefaults = InClassInput.GetDefaults(); if (ClassDefaults.Num() == 1) { return ClassDefaults.Last().PageID; } #endif // !WITH_EDITORONLY_DATA FScopeLock Lock(&TargetPageResolveScratchCritSec); TargetPageResolveScratch.Reset(); InClassInput.IterateDefaults([this](const FGuid& PageID, const FMetasoundFrontendLiteral&) { TargetPageResolveScratch.Add(PageID); }); return ResolveTargetPageIDInternal(TargetPageResolveScratch); } FGuid FDocumentBuilderRegistry::ResolveTargetPageID(const TArray& InClassDefaults) const { METASOUND_LLM_SCOPE; METASOUND_TRACE_CPUPROFILER_EVENT_SCOPE(FDocumentBuilderRegistry::ResolveTargetPageID_ClassDefaults); #if !WITH_EDITORONLY_DATA // No resolution required if only one item. This is the typical scenario and saves from tying up a lock // and calling resolution delegate(s). For larger graphs, this can add up, and most implementation // only has a single page value. With editor-only data, go ahead and take the perf hit in favor of // resolution reporting (for example if page data is invalid and needs to be fixed up). if (InClassDefaults.Num() == 1) { return InClassDefaults.Last().PageID; } #endif // !WITH_EDITORONLY_DATA FScopeLock Lock(&TargetPageResolveScratchCritSec); TargetPageResolveScratch.Reset(); Algo::Transform(InClassDefaults, TargetPageResolveScratch, [](const FMetasoundFrontendClassInputDefault& ClassDefault) { return ClassDefault.PageID; }); return ResolveTargetPageIDInternal(TargetPageResolveScratch); } FGuid FDocumentBuilderRegistry::ResolveTargetPageIDInternal(const TArray& InPageIDsToResolve) const { FName PlatformName = FPlatformProperties::IniPlatformName(); #if WITH_EDITOR if (OnResolveAuditionPage.IsBound()) { FPageResolutionEditorResults PreviewInfo = OnResolveAuditionPage.Execute(InPageIDsToResolve); if (PreviewInfo.PageID.IsSet()) { return PreviewInfo.PageID.GetValue(); } PlatformName = PreviewInfo.PlatformName; } #endif // WITH_EDITOR if (OnResolveProjectPage.IsBound()) { const FGuid ResolvedPageID = OnResolveProjectPage.Execute(InPageIDsToResolve); check (InPageIDsToResolve.Contains(ResolvedPageID)); return ResolvedPageID; } if (const UMetaSoundSettings* Settings = GetDefault()) { const FGuid& TargetPageID = Settings->GetTargetPageSettings().UniqueId; return ResolveTargetPageIDInternal(*Settings, InPageIDsToResolve, TargetPageID, PlatformName); } return Frontend::DefaultPageID; } FGuid FDocumentBuilderRegistry::ResolveTargetPageIDInternal(const UMetaSoundSettings& Settings, const TArray& InPageIDsToResolve, const FGuid& TargetPageID, FName PlatformName) const { bool bResolved = false; FGuid ResolvedPageID = Frontend::DefaultPageID; constexpr bool bReverse = true; bool bFoundTarget = false; Settings.IteratePageSettings([&](const FMetaSoundPageSettings& PageSettings) { bFoundTarget |= PageSettings.UniqueId == TargetPageID; if (bFoundTarget && !bResolved) { const bool bAssetImplementsPage = InPageIDsToResolve.Contains(PageSettings.UniqueId); if (bAssetImplementsPage) { #if WITH_EDITOR const bool bIsCooked = !PageSettings.GetExcludeFromCook(PlatformName); if (bIsCooked) { bResolved = true; ResolvedPageID = PageSettings.UniqueId; } #else // !WITH_EDITOR bResolved = true; ResolvedPageID = PageSettings.UniqueId; #endif // !WITH_EDITOR } } }, bReverse); if (!bResolved) { const FGuid& AnyPageID = InPageIDsToResolve.Last(); #if !NO_LOGGING auto GetDisplayPageString = [&Settings](const FGuid& InPageID) { if (const FMetaSoundPageSettings* DisplayPage = Settings.FindPageSettings(InPageID)) { return DisplayPage->Name.ToString(); } return InPageID.ToString(); }; UE_LOG(LogMetaSound, Error, TEXT("Failed to resolve PageID for Target '%s': Setting to arbitrary Page '%s' (Target likely overridden by page not set as 'CanTarget/Targetable' for the current platform)"), *GetDisplayPageString(TargetPageID), *GetDisplayPageString(AnyPageID)); #endif // !NO_LOGGING ResolvedPageID = AnyPageID; } return ResolvedPageID; } void FDocumentBuilderRegistry::SetEventLogVerbosity(ELogEvent Event, ELogVerbosity::Type Verbosity) { EventLogVerbosity.FindOrAdd(Event) = Verbosity; } } // namespace Metasound::Engine