// Copyright Epic Games, Inc. All Rights Reserved. #include "Blueprints/DisplayClusterBlueprint.h" #include "Blueprints/DisplayClusterBlueprintGeneratedClass.h" #include "DisplayClusterRootActor.h" #include "Components/DisplayClusterCameraComponent.h" #include "Components/DisplayClusterICVFXCameraComponent.h" #include "Components/DisplayClusterScreenComponent.h" #include "IDisplayClusterConfiguration.h" #include "EngineAnalytics.h" #include "Engine/SCS_Node.h" #include "Containers/Set.h" #include "Misc/DisplayClusterLog.h" #include "UObject/ObjectSaveContext.h" UDisplayClusterBlueprint::UDisplayClusterBlueprint() : ConfigData(nullptr), AssetVersion(0) { BlueprintType = BPTYPE_Normal; #if WITH_EDITORONLY_DATA bRunConstructionScriptOnInteractiveChange = false; #endif } #if WITH_EDITOR UClass* UDisplayClusterBlueprint::GetBlueprintClass() const { return UDisplayClusterBlueprintGeneratedClass::StaticClass(); } void UDisplayClusterBlueprint::GetReparentingRules(TSet& AllowedChildrenOfClasses, TSet& DisallowedChildrenOfClasses) const { AllowedChildrenOfClasses.Add(ADisplayClusterRootActor::StaticClass()); } #endif void UDisplayClusterBlueprint::UpdateConfigExportProperty() { bool bConfigExported = false; if (UDisplayClusterConfigurationData* Config = GetOrLoadConfig()) { PrepareConfigForExport(); FString PrettyConfig; bConfigExported = IDisplayClusterConfiguration::Get().ConfigAsString(Config, PrettyConfig); if (bConfigExported) { // We cache a somewhat minified version of the config so that the context view of the asset registry data is less bloated. ConfigExport.Empty(PrettyConfig.Len()); for (auto CharIt = PrettyConfig.CreateConstIterator(); CharIt; ++CharIt) { const TCHAR Char = *CharIt; // Remove tabs, carriage returns and newlines. if ((Char == TCHAR('\t')) || (Char == TCHAR('\r')) || (Char == TCHAR('\n'))) { continue; } ConfigExport.AppendChar(Char); } } } if (!bConfigExported) { ConfigExport = TEXT(""); } } void UDisplayClusterBlueprint::UpdateSummaryProperty() { if (!ConfigData || !ConfigData->Cluster) { Summary = TEXT("No data!"); return; } TArray Lines; // Description if (ConfigData->Info.Description.Len()) { Lines.Add(ConfigData->Info.Description); Lines.Add(TEXT("")); } // Settings Lines.Add(TEXT("Settings:")); Lines.Add(TEXT("--------")); Lines.Add(TEXT("")); Lines.Add(FString::Printf(TEXT("Sync Policy: %s"), *ConfigData->Cluster->Sync.RenderSyncPolicy.Type)); Lines.Add(FString::Printf(TEXT("Follow Local Player Camera: %s"), ConfigData->bFollowLocalPlayerCamera ? TEXT("Yes") : TEXT("No"))); Lines.Add(FString::Printf(TEXT("Viewports Screen %% Multiplier: %.2f"), ConfigData->RenderFrameSettings.ClusterICVFXOuterViewportBufferRatioMult)); Lines.Add(TEXT("")); // Cluster Lines.Add(TEXT("Cluster:")); Lines.Add(TEXT("-------")); Lines.Add(TEXT("")); TSet Hosts; int32 NumNodes = 0; int32 NumHeadlessNodes = 0; int32 NumFullscreenNodes = 0; int32 NumViewports = 0; TSet ViewportMedias; TSet NodeMedias; TSet IcvfxCameraMedias; for (const TPair>& NodePair : ConfigData->Cluster->Nodes) { const FString& NodeId = NodePair.Key; const TObjectPtr Node = NodePair.Value; if (!ensure(Node)) { continue; } Hosts.Add(Node->Host); NumNodes++; if (Node->MediaSettings.bEnable) { for (const FDisplayClusterConfigurationMediaOutput& MediaOutput : Node->MediaSettings.MediaOutputs) { if (MediaOutput.MediaOutput) { FString MediaName = MediaOutput.MediaOutput.GetClass()->GetName(); MediaName.RemoveFromEnd(TEXT("Output")); NodeMedias.Add(MediaName); } } } if (Node->bRenderHeadless) { NumHeadlessNodes++; } else { if (Node->bIsFullscreen) { NumFullscreenNodes++; } } for (const TPair>& ViewportPair : Node->Viewports) { TObjectPtr Viewport = ViewportPair.Value; if (!ensure(Viewport)) { continue; } NumViewports++; if (Viewport->RenderSettings.Media.bEnable) { if (Viewport->RenderSettings.Media.MediaInput.MediaSource) { FString MediaName = Viewport->RenderSettings.Media.MediaInput.MediaSource->GetClass()->GetName(); MediaName.RemoveFromEnd(TEXT("Source")); ViewportMedias.Add(MediaName); } for (const FDisplayClusterConfigurationMediaOutput& MediaOutput : Viewport->RenderSettings.Media.MediaOutputs) { if (!MediaOutput.MediaOutput) { continue; } FString MediaName = MediaOutput.MediaOutput->GetClass()->GetName(); MediaName.RemoveFromEnd(TEXT("Output")); ViewportMedias.Add(MediaName); } } } } Lines.Add(FString::Printf(TEXT("Hosts: %d"), Hosts.Num())); Lines.Add(FString::Printf(TEXT("Nodes: %d (%d Headless, %d Fullscreen)"), NumNodes, NumHeadlessNodes, NumFullscreenNodes)); Lines.Add(FString::Printf(TEXT("Viewports: %d"), NumViewports)); // Here we find the icvfx camera templates in the blueprint, using the SimpleConstructionScript. if (SimpleConstructionScript) { TMap CamerasByMediaOrSplit; for (USCS_Node* Node : SimpleConstructionScript->GetAllNodes()) { if (UDisplayClusterICVFXCameraComponent* IcvfxCamera = Cast(Node->ComponentTemplate)) { // We're intentionally including disabled cameras in the count, but not disabled Media in them. const FDisplayClusterConfigurationMediaICVFX& MediaSettings = IcvfxCamera->GetCameraSettingsICVFX().RenderSettings.Media; if (MediaSettings.bEnable) { switch (MediaSettings.SplitType) { case EDisplayClusterConfigurationMediaSplitType::FullFrame: { int32& FullFrameCameras = CamerasByMediaOrSplit.FindOrAdd(TEXT("Full Frame"), 0); FullFrameCameras++; // Gather the media types used. for (const FDisplayClusterConfigurationMediaOutputGroup& OutputGroup : MediaSettings.MediaOutputGroups) { if (!OutputGroup.MediaOutput) { continue; } FString MediaName = OutputGroup.MediaOutput->GetClass()->GetName(); MediaName.RemoveFromEnd(TEXT("Output")); IcvfxCameraMedias.Add(MediaName); } for (const FDisplayClusterConfigurationMediaInputGroup& InputGroup : MediaSettings.MediaInputGroups) { if (!InputGroup.MediaSource) { continue; } FString MediaName = InputGroup.MediaSource->GetClass()->GetName(); MediaName.RemoveFromEnd(TEXT("Source")); IcvfxCameraMedias.Add(MediaName); } break; } case EDisplayClusterConfigurationMediaSplitType::UniformTiles: { const FString TiledString = FString::Printf(TEXT("Tiled %dx%d"), MediaSettings.TiledSplitLayout.X, MediaSettings.TiledSplitLayout.Y); int32& TiledCameras = CamerasByMediaOrSplit.FindOrAdd(TiledString, 0); TiledCameras++; // Gather the media types used. for (const FDisplayClusterConfigurationMediaTiledInputGroup& InputGroup : MediaSettings.TiledMediaInputGroups) { for (const FDisplayClusterConfigurationMediaUniformTileInput& Tile : InputGroup.Tiles) { if (!Tile.MediaSource) { continue; } FString MediaName = Tile.MediaSource->GetClass()->GetName(); MediaName.RemoveFromEnd(TEXT("Source")); IcvfxCameraMedias.Add(MediaName); } } for (const FDisplayClusterConfigurationMediaTiledOutputGroup& OutputGroup : MediaSettings.TiledMediaOutputGroups) { for (const FDisplayClusterConfigurationMediaUniformTileOutput& Tile : OutputGroup.Tiles) { if (!Tile.MediaOutput) { continue; } FString MediaName = Tile.MediaOutput->GetClass()->GetName(); MediaName.RemoveFromEnd(TEXT("Output")); IcvfxCameraMedias.Add(MediaName); } } break; } default: checkNoEntry(); } } else { int32& NoMediaCameras = CamerasByMediaOrSplit.FindOrAdd(TEXT("No Media"), 0); NoMediaCameras++; } } } FString IcvfxCamerasLine = TEXT("ICVFX Cameras: "); if (CamerasByMediaOrSplit.Num()) { TArray TypeValues; for (const TPair& IcvfxCameraPair : CamerasByMediaOrSplit) { TypeValues.Add(FString::Printf(TEXT("%d (%s)"), IcvfxCameraPair.Value, *IcvfxCameraPair.Key)); } IcvfxCamerasLine += FString::Join(TypeValues, TEXT(", ")); } else { IcvfxCamerasLine += TEXT("None"); } Lines.Add(IcvfxCamerasLine); } Lines.Add(TEXT("")); // Media if (IcvfxCameraMedias.Num() || ViewportMedias.Num() || NodeMedias.Num()) { Lines.Add(TEXT("Media:")); Lines.Add(TEXT("------")); Lines.Add(TEXT("")); if (NodeMedias.Num()) { Lines.Add(TEXT("Node Media: ") + FString::Join(NodeMedias, TEXT(", "))); } if (ViewportMedias.Num()) { Lines.Add(TEXT("Viewport Media: ") + FString::Join(ViewportMedias, TEXT(", "))); } if (IcvfxCameraMedias.Num()) { Lines.Add(TEXT("ICVFX Camera Media: ") + FString::Join(IcvfxCameraMedias, TEXT(", "))); } } Summary = FString::Join(Lines, TEXT("\n")); } namespace DisplayClusterBlueprint { void SendAnalytics(const FString& EventName, const UDisplayClusterConfigurationData* const ConfigData) { if (!FEngineAnalytics::IsAvailable()) { return; } // Gather attributes related to this config TArray EventAttributes; if (ConfigData) { if (ConfigData->Cluster) { // Number of Nodes EventAttributes.Add(FAnalyticsEventAttribute(TEXT("NumNodes"), ConfigData->Cluster->Nodes.Num())); // Number of Viewports TSet UniquelyNamedViewports; for (auto NodesIt = ConfigData->Cluster->Nodes.CreateConstIterator(); NodesIt; ++NodesIt) { for (auto ViewportsIt = ConfigData->Cluster->Nodes.CreateConstIterator(); ViewportsIt; ++ViewportsIt) { UniquelyNamedViewports.Add(ViewportsIt->Key); } } // Number of uniquely named viewports EventAttributes.Add(FAnalyticsEventAttribute(TEXT("NumUniquelyNamedViewports"), UniquelyNamedViewports.Num())); } } FEngineAnalytics::GetProvider().RecordEvent(EventName, EventAttributes); } } void UDisplayClusterBlueprint::PreSave(FObjectPreSaveContext SaveContext) { Super::PreSave(SaveContext); UpdateConfigExportProperty(); UpdateSummaryProperty(); DisplayClusterBlueprint::SendAnalytics(TEXT("Usage.nDisplay.ConfigSaved"), ConfigData); #if WITH_EDITOR // Child blueprints need to re-generate their config export property as well // Note: Using GetDerivedClasses will only get loaded classes, which is the normal case, // and the rest will be caught when they get loaded as an out of date exported config // will be detected. if (GIsEditor) { TArray ChildClasses; GetDerivedClasses(GeneratedClass, ChildClasses); for (UClass* ChildClass : ChildClasses) { // CLASS_NewerVersionExists suggests there is a newer class that will update the asset, so we skip it. if (ChildClass->HasAnyClassFlags(CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists)) { continue; } UDisplayClusterBlueprint* ChildDCBP = Cast(ChildClass->ClassGeneratedBy); check(ChildDCBP); // We already know it is a derived class // Only mark as dirty if the config needs updating, to avoid unnecessary re-saves. const FString OriginalChildConfigExport = ChildDCBP->ConfigExport; ChildDCBP->UpdateConfigExportProperty(); if (!ChildDCBP->ConfigExport.Equals(OriginalChildConfigExport)) { if (ChildDCBP->MarkPackageDirty()) { UE_LOG(LogDisplayClusterBlueprint, Display, TEXT("ConfigExport of the child nDisplay blueprint actor '%s' is not up to date in the asset, so the package was marked as dirty and should be re-saved."), *ChildDCBP->GetOutermost()->GetName() // If MarkPackageDirty succeeded then GetOutermost() must exist. ); } } } } #endif // WITH_EDITOR } void UDisplayClusterBlueprint::PostLoad() { Super::PostLoad(); #if WITH_EDITOR // If the exported config is out of date, mark the package as dirty for the user // to re-save. This may happen, for example, when the parent blueprint is updated, // or when the config export logic has changed. if (GIsEditor) { const FString LoadedConfigExport = ConfigExport; UpdateConfigExportProperty(); UpdateSummaryProperty(); // Note: No need to mark the asset dirty if the generated Summary has changed since it is not being used externally. if (!ConfigExport.Equals(LoadedConfigExport)) { if (MarkPackageDirty()) { UE_LOG(LogDisplayClusterBlueprint, Display, TEXT("ConfigExport of the nDisplay actor '%s' was not up to date in the asset, so the package was marked as dirty and should be re-saved."), *GetOutermost()->GetName() // If MarkPackageDirty succeeded then GetOutermost() must exist. ); } } } #endif // WITH_EDITOR } UDisplayClusterBlueprintGeneratedClass* UDisplayClusterBlueprint::GetGeneratedClass() const { return Cast(*GeneratedClass); } UDisplayClusterConfigurationData* UDisplayClusterBlueprint::GetOrLoadConfig() { if (GeneratedClass) { if (ADisplayClusterRootActor* CDO = Cast(GeneratedClass->GetDefaultObject(false))) { ConfigData = CDO->GetConfigData(); } } return ConfigData; } void UDisplayClusterBlueprint::SetConfigData(UDisplayClusterConfigurationData* InConfigData, bool bForceRecreate) { #if WITH_EDITOR Modify(); #endif if (GeneratedClass) { if (ADisplayClusterRootActor* CDO = Cast(GeneratedClass->GetDefaultObject(false))) { CDO->UpdateConfigDataInstance(InConfigData, bForceRecreate); GetOrLoadConfig(); } } #if WITH_EDITORONLY_DATA if(InConfigData) { InConfigData->SaveConfig(); } #endif } const FString& UDisplayClusterBlueprint::GetConfigPath() const { static FString EmptyString; #if WITH_EDITORONLY_DATA return ConfigData ? ConfigData->PathToConfig : EmptyString; #else return EmptyString; #endif } void UDisplayClusterBlueprint::SetConfigPath(const FString& InPath) { #if WITH_EDITORONLY_DATA if(UDisplayClusterConfigurationData* LoadedConfigData = GetOrLoadConfig()) { LoadedConfigData->PathToConfig = InPath; LoadedConfigData->SaveConfig(); } #endif } void UDisplayClusterBlueprint::PrepareConfigForExport() { if (!ensure(GeneratedClass)) { return; } ADisplayClusterRootActor* CDO = CastChecked(GeneratedClass->GetDefaultObject(false)); UDisplayClusterConfigurationData* Data = GetOrLoadConfig(); check(Data); // Components to export TArray CameraComponents; TArray ScreenComponents; TArray XformComponents; // Auxiliary map for building hierarchy TMap ParentComponentsMap; // Make sure the 'Scene' object is there. Otherwise instantiate it. // Could be null on assets used during 4.27 development, before scene was added back in. const EObjectFlags CommonFlags = RF_Public | RF_Transactional; if (Data->Scene == nullptr) { Data->Scene = NewObject( this, UDisplayClusterConfigurationScene::StaticClass(), NAME_None, IsTemplate() ? RF_ArchetypeObject | CommonFlags : CommonFlags); } // Extract CDO cameras (no screens in the CDO) // Get list of cameras CDO->GetComponents(CameraComponents); // Extract BP components const TArray& Nodes = SimpleConstructionScript->GetAllNodes(); for (const USCS_Node* const Node : Nodes) { // Fill ID info for all descendants GatherParentComponentsInfo(Node, ParentComponentsMap); // Cameras if (Node->ComponentClass->IsChildOf(UDisplayClusterCameraComponent::StaticClass())) { UDisplayClusterCameraComponent* ComponentTemplate = CastChecked(Node->GetActualComponentTemplate(CastChecked(GeneratedClass))); CameraComponents.Add(ComponentTemplate); } // Screens else if (Node->ComponentClass->IsChildOf(UDisplayClusterScreenComponent::StaticClass())) { UDisplayClusterScreenComponent* ComponentTemplate = CastChecked(Node->GetActualComponentTemplate(CastChecked(GeneratedClass))); ScreenComponents.Add(ComponentTemplate); } // All other components will be exported as Xforms else if (Node->ComponentClass->IsChildOf(USceneComponent::StaticClass())) { USceneComponent* ComponentTemplate = CastChecked(Node->GetActualComponentTemplate(CastChecked(GeneratedClass))); XformComponents.Add(ComponentTemplate); } } // Save asset path Data->Info.AssetPath = GetPathName(); // Prepare the target containers Data->Scene->Cameras.Empty(CameraComponents.Num()); Data->Scene->Screens.Empty(ScreenComponents.Num()); Data->Scene->Xforms.Empty(XformComponents.Num()); // Export cameras for (const UDisplayClusterCameraComponent* const CfgComp : CameraComponents) { UDisplayClusterConfigurationSceneComponentCamera* SceneComp = NewObject(Data->Scene, CfgComp->GetFName(), RF_Public); // Save the properties SceneComp->bSwapEyes = CfgComp->GetSwapEyes(); SceneComp->InterpupillaryDistance = CfgComp->GetInterpupillaryDistance(); // Safe to cast -- values match. SceneComp->StereoOffset = (EDisplayClusterConfigurationEyeStereoOffset)CfgComp->GetStereoOffset(); FString* ParentId = ParentComponentsMap.Find(CfgComp); SceneComp->ParentId = (ParentId ? *ParentId : FString()); SceneComp->Location = CfgComp->GetRelativeLocation(); SceneComp->Rotation = CfgComp->GetRelativeRotation(); // Store the object Data->Scene->Cameras.Emplace(GetObjectNameFromSCSNode(SceneComp), SceneComp); } // Export screens for (const UDisplayClusterScreenComponent* const CfgComp : ScreenComponents) { UDisplayClusterConfigurationSceneComponentScreen* SceneComp = NewObject(Data->Scene, CfgComp->GetFName()); // Save the properties FString* ParentId = ParentComponentsMap.Find(CfgComp); SceneComp->ParentId = (ParentId ? *ParentId : FString()); SceneComp->Location = CfgComp->GetRelativeLocation(); SceneComp->Rotation = CfgComp->GetRelativeRotation(); const FVector RelativeCompScale = CfgComp->GetRelativeScale3D(); SceneComp->Size = FVector2D(RelativeCompScale.Y, RelativeCompScale.Z); // Store the object Data->Scene->Screens.Emplace(GetObjectNameFromSCSNode(SceneComp), SceneComp); } // Export xforms for (const USceneComponent* const CfgComp : XformComponents) { UDisplayClusterConfigurationSceneComponentXform* SceneComp = NewObject(Data->Scene, CfgComp->GetFName()); // Save the properties FString* ParentId = ParentComponentsMap.Find(CfgComp); SceneComp->ParentId = (ParentId ? *ParentId : FString()); SceneComp->Location = CfgComp->GetRelativeLocation(); SceneComp->Rotation = CfgComp->GetRelativeRotation(); // Store the object Data->Scene->Xforms.Emplace(GetObjectNameFromSCSNode(SceneComp), SceneComp); } // Avoid empty string keys in the config data maps CleanupConfigMaps(Data); } FString UDisplayClusterBlueprint::GetObjectNameFromSCSNode(const UObject* const Object) const { FString OutCompName; if (Object) { OutCompName = Object->GetName(); OutCompName.RemoveFromEnd(TEXT("_GEN_VARIABLE")); } return OutCompName; } void UDisplayClusterBlueprint::GatherParentComponentsInfo(const USCS_Node* const InNode, TMap& OutParentsMap) const { if (InNode && InNode->ComponentClass->IsChildOf(UActorComponent::StaticClass())) { // Save current node to the map UActorComponent* ParentNode = InNode->GetActualComponentTemplate(CastChecked(GeneratedClass)); if (!OutParentsMap.Contains(ParentNode)) { OutParentsMap.Emplace(ParentNode); } // Now iterate through the children nodes for (USCS_Node* ChildNode : InNode->ChildNodes) { UActorComponent* ChildComponentTemplate = CastChecked(ChildNode->GetActualComponentTemplate(CastChecked(GeneratedClass))); if (ChildComponentTemplate) { OutParentsMap.Emplace(ChildComponentTemplate, GetObjectNameFromSCSNode(InNode->ComponentTemplate)); } GatherParentComponentsInfo(ChildNode, OutParentsMap); } } } void UDisplayClusterBlueprint::CleanupConfigMaps(UDisplayClusterConfigurationData* Data) const { check(Data && Data->Cluster); static const FString InvalidKey = FString(); // Set of the maps we're going to process TSet*> MapsToProcess; // Pre-allocate some memory. Not a precise amount of elements, but should be enough in most cases MapsToProcess.Reserve(3 + 4 * Data->Cluster->Nodes.Num()); // Add single instance maps MapsToProcess.Add(&Data->CustomParameters); MapsToProcess.Add(&Data->Cluster->Sync.InputSyncPolicy.Parameters); MapsToProcess.Add(&Data->Cluster->Sync.RenderSyncPolicy.Parameters); // Add per-node and per-viewport maps Data->Cluster->Nodes.Remove(InvalidKey); for (TPair Node : Data->Cluster->Nodes) { check(Node.Value); // Per-node maps Node.Value->Postprocess.Remove(InvalidKey); for (TPair PostOpIt : Node.Value->Postprocess) { MapsToProcess.Add(&PostOpIt.Value.Parameters); } // Per-viewport maps Node.Value->Viewports.Remove(InvalidKey); for (TPair ViewportIt : Node.Value->Viewports) { check(ViewportIt.Value); MapsToProcess.Add(&ViewportIt.Value->ProjectionPolicy.Parameters); } } // Finally, remove all the pairs with empty keys for (TMap* Map : MapsToProcess) { Map->Remove(InvalidKey); } } void UDisplayClusterBlueprint::GetAssetRegistryTags(FAssetRegistryTagsContext Context) const { Super::GetAssetRegistryTags(Context); // Add ConfigExport to the tags so that it is asset searchable. Context.AddTag(FAssetRegistryTag(TEXT("ConfigExport"), ConfigExport, FAssetRegistryTag::TT_Hidden)); }