Files
UnrealEngine/Engine/Plugins/Runtime/nDisplay/Source/DisplayClusterMessageInterceptor/Private/DisplayClusterMessageInterceptionModule.cpp
2025-05-18 13:04:45 +08:00

600 lines
20 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Modules/ModuleInterface.h"
#include "Modules/ModuleManager.h"
#include "Cluster/DisplayClusterClusterEvent.h"
#include "Cluster/IDisplayClusterClusterManager.h"
#include "DisplayClusterGameEngine.h"
#include "DisplayClusterMessageInterceptor.h"
#include "DisplayClusterMessageInterceptionSettings.h"
#include "Engine/Engine.h"
#include "HAL/IConsoleManager.h"
#include "Templates/Atomic.h"
#include "IDisplayCluster.h"
#include "IDisplayClusterCallbacks.h"
#include "IMessageBus.h"
#include "IMessagingModule.h"
#if defined(WITH_CONCERT)
#include "IConcertClient.h"
#include "IConcertSyncClient.h"
#include "IConcertSession.h"
#include "IConcertClientWorkspace.h"
#include "IConcertSyncClientModule.h"
#endif
#if WITH_EDITOR
#include "ISettingsModule.h"
#include "ISettingsSection.h"
#endif
#include <atomic>
#define LOCTEXT_NAMESPACE "DisplayClusterInterception"
namespace DisplayClusterInterceptionModuleUtils
{
static const FString EventSetup = TEXT("nDCISetup");
static const FString EventSync = TEXT("nDCIMUSync");
static const FString PackageSync = TEXT("nDCIPackageSync");
static const FString EventParameterSettings = TEXT("Settings");
}
class FSetBoolOnPreTick
{
public:
operator bool() const
{
FScopeLock Lock(&InternalsCS);
return bValue;
}
FSetBoolOnPreTick& operator=(bool bInValue)
{
FScopeLock Lock(&InternalsCS);
if (bInValue != bValue)
{
bPendingValue = bInValue;
}
return *this;
}
void ResetWithValue(bool bInValue)
{
FScopeLock Lock(&InternalsCS);
bValue = bInValue;
bPendingValue = {};
}
void UpdateOnPreTick()
{
FScopeLock Lock(&InternalsCS);
if (bPendingValue)
{
bValue = *bPendingValue;
bPendingValue = {};
}
}
private:
bool bValue = false;
TOptional<bool> bPendingValue = {};
mutable FCriticalSection InternalsCS;
};
/**
* Display Cluster Message Interceptor module
* Intercept a specified set of message bus messages that are received across all the display nodes
* to process them in sync across the cluster.
*/
class FDisplayClusterMessageInterceptionModule : public IModuleInterface
{
public:
virtual void StartupModule() override
{
if (IDisplayCluster::IsAvailable())
{
// Register for Cluster StartSession callback so everything is setup before launching interception
IDisplayCluster::Get().GetCallbacks().OnDisplayClusterStartSession().AddRaw(this, &FDisplayClusterMessageInterceptionModule::OnDisplayClusterStartSession);
IDisplayCluster::Get().GetCallbacks().OnDisplayClusterStartScene().AddRaw(this, &FDisplayClusterMessageInterceptionModule::OnNewSceneEvent);
}
// Setup console command to start/stop interception
StartMessageSyncCommand = MakeUnique<FAutoConsoleCommand>(
TEXT("nDisplay.MessageBusSync.Start"),
TEXT("Start MessageBus syncing"),
FConsoleCommandDelegate::CreateRaw(this, &FDisplayClusterMessageInterceptionModule::StartInterception)
);
StopMessageSyncCommand = MakeUnique<FAutoConsoleCommand>(
TEXT("nDisplay.MessageBusSync.Stop"),
TEXT("Stop MessageBus syncing"),
FConsoleCommandDelegate::CreateRaw(this, &FDisplayClusterMessageInterceptionModule::StopInterception)
);
#if WITH_EDITOR
if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"))
{
SettingsModule->RegisterSettings(
"Project", "Plugins", "nDisplay Message Interception",
LOCTEXT("InterceptionSettingsName", "nDisplay Message Interception"),
LOCTEXT("InterceptionSettingsDescription", "Configure nDisplay Message Interception."),
GetMutableDefault<UDisplayClusterMessageInterceptionSettings>()
);
}
#endif
}
virtual void ShutdownModule() override
{
#if WITH_EDITOR
if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"))
{
SettingsModule->UnregisterSettings("Project", "Plugins", "nDisplay Message Interception");
}
#endif
#if defined(WITH_CONCERT)
if (IConcertSyncClientModule::IsAvailable())
{
if (TSharedPtr<IConcertSyncClient> ConcertSyncClient = IConcertSyncClientModule::Get().GetClient(TEXT("MultiUser")))
{
IConcertClientRef ConcertClient = ConcertSyncClient->GetConcertClient();
ConcertClient->OnSessionStartup().RemoveAll(this);
ConcertClient->OnSessionShutdown().RemoveAll(this);
ConcertClient->OnSessionConnectionChanged().RemoveAll(this);
}
}
#endif
if (IDisplayCluster::IsAvailable())
{
// Unregister cluster event listening
IDisplayClusterClusterManager* ClusterManager = IDisplayCluster::Get().GetClusterMgr();
if (ClusterManager && ListenerDelegate.IsBound())
{
ClusterManager->RemoveClusterEventJsonListener(ListenerDelegate);
ListenerDelegate.Unbind();
}
// Unregister cluster session events
IDisplayCluster::Get().GetCallbacks().OnDisplayClusterStartSession().RemoveAll(this);
IDisplayCluster::Get().GetCallbacks().OnDisplayClusterStartScene().RemoveAll(this);
IDisplayCluster::Get().GetCallbacks().OnDisplayClusterEndSession().RemoveAll(this);
IDisplayCluster::Get().GetCallbacks().OnDisplayClusterPreTick().RemoveAll(this);
IDisplayCluster::Get().GetCallbacks().OnDisplayClusterFailoverNodeDown().RemoveAll(this);
}
Interceptor.Reset();
StartMessageSyncCommand.Reset();
StopMessageSyncCommand.Reset();
}
private:
void SetupForMultiUser()
{
#if defined(WITH_CONCERT)
if (TSharedPtr<IConcertSyncClient> ConcertSyncClient = IConcertSyncClientModule::Get().GetClient(TEXT("MultiUser")))
{
TSharedPtr<IConcertClientWorkspace> Workspace = ConcertSyncClient->GetWorkspace();
IConcertClientRef ConcertClient = ConcertSyncClient->GetConcertClient();
ConcertClient->OnSessionStartup().AddRaw(this, &FDisplayClusterMessageInterceptionModule::OnMultiUserStartup);
ConcertClient->OnSessionShutdown().AddRaw(this, &FDisplayClusterMessageInterceptionModule::OnMultiUserShutdown);
ConcertClient->OnSessionConnectionChanged().AddRaw(this, &FDisplayClusterMessageInterceptionModule::OnSessionConnectionChanged);
if (Workspace.IsValid())
{
Workspace->RemoveWorkspaceFinalizeDelegate(TEXT("nDisplay Interceptor"));
Workspace->OnWorkspaceSynchronized().RemoveAll(this);
}
}
else
{
UE_LOG(LogDisplayClusterInterception, Display, TEXT("No multi-user detected. Not intercepting initial activity sync."));
}
#else
UE_LOG(LogDisplayClusterInterception, Display, TEXT("No multi-user available."));
#endif
}
void OnNewSceneEvent()
{
IDisplayClusterClusterManager* ClusterManager = IDisplayCluster::Get().GetClusterMgr();
if (bStartInterceptionRequested && !bSettingsSynchronizationDone && ClusterManager && ListenerDelegate.IsBound())
{
// If we receive a NewScene event then our previous sync event was lost due to flushing of
// the event queues during EndScene / NewScene. Re-establish the event here:
ResendSyncEvent(ClusterManager);
}
UE_LOG(LogDisplayClusterInterception, Display, TEXT("New scene event"));
}
void ResendSyncEvent(IDisplayClusterClusterManager* ClusterManager)
{
// Primary node will send out its interceptor settings to the cluster so everyone uses the same things
if (ClusterManager->IsPrimary())
{
FString ExportedSettings;
const UDisplayClusterMessageInterceptionSettings* CurrentSettings = GetDefault<UDisplayClusterMessageInterceptionSettings>();
FMessageInterceptionSettings::StaticStruct()->ExportText(ExportedSettings, &CurrentSettings->InterceptionSettings, nullptr, nullptr, PPF_None, nullptr);
FDisplayClusterClusterEventJson SettingsEvent;
SettingsEvent.Category = DisplayClusterInterceptionModuleUtils::EventSetup;
SettingsEvent.Name = ClusterManager->GetNodeId();
SettingsEvent.bIsSystemEvent = true;
SettingsEvent.Parameters.FindOrAdd(DisplayClusterInterceptionModuleUtils::EventParameterSettings) = MoveTemp(ExportedSettings);
const bool bPrimaryOnly = true;
ClusterManager->EmitClusterEventJson(SettingsEvent, bPrimaryOnly);
}
}
void PackageSyncEvent(int64 ActivityId)
{
if (!CanFinalizeWorkspaceSync())
{
return;
}
UE_LOG(LogDisplayClusterInterception, Display, TEXT("Sending package sync event."));
IDisplayClusterClusterManager* ClusterManager = IDisplayCluster::Get().GetClusterMgr();
check(ClusterManager != nullptr);
bAllowPackages = false;
FDisplayClusterClusterEventJson SyncMessagesEvent;
SyncMessagesEvent.Category = DisplayClusterInterceptionModuleUtils::PackageSync;
SyncMessagesEvent.Type = LexToString(ActivityId);
SyncMessagesEvent.Name = ClusterManager->GetNodeId(); // which node got the message
SyncMessagesEvent.bIsSystemEvent = true; // nDisplay internal event
SyncMessagesEvent.bShouldDiscardOnRepeat = false; // Don' discard the events with the same cat/type/name
const bool bPrimaryOnly = false; // All nodes are broadcasting events to synchronize them across cluster
ClusterManager->EmitClusterEventJson(SyncMessagesEvent, bPrimaryOnly);
}
void WorkspaceSyncEvent()
{
if (bWasEverDisconnected)
{
return;
}
UE_LOG(LogDisplayClusterInterception, Display, TEXT("Sending activity sync event."));
IDisplayClusterClusterManager* ClusterManager = IDisplayCluster::Get().GetClusterMgr();
check(ClusterManager != nullptr);
FDisplayClusterClusterEventJson SyncMessagesEvent;
SyncMessagesEvent.Category = DisplayClusterInterceptionModuleUtils::EventSync;
SyncMessagesEvent.Type = TEXT("WorkspaceSync"); // Required by nDisplay or message is discarded.
SyncMessagesEvent.Name = ClusterManager->GetNodeId(); // which node got the message
SyncMessagesEvent.bIsSystemEvent = true; // nDisplay internal event
SyncMessagesEvent.bShouldDiscardOnRepeat = false; // Don' discard the events with the same cat/type/name
const bool bPrimaryOnly = false; // All nodes are broadcasting events to synchronize them across cluster
ClusterManager->EmitClusterEventJson(SyncMessagesEvent, bPrimaryOnly);
}
bool CanFinalizeWorkspaceSync() const
{
return CanFinalizeWorkspace || bWasEverDisconnected;
}
bool CanProcessPendingPackages() const
{
return bAllowPackages;
}
#if defined(WITH_CONCERT)
void OnSessionConnectionChanged(IConcertClientSession& InSession, EConcertConnectionStatus ConnectionStatus)
{
TSharedPtr<IConcertSyncClient> ConcertSyncClient = IConcertSyncClientModule::Get().GetClient(TEXT("MultiUser"));
check(ConcertSyncClient.IsValid());
TSharedPtr<IConcertClientWorkspace> Workspace = ConcertSyncClient->GetWorkspace();
if (ConnectionStatus == EConcertConnectionStatus::Connected)
{
ResetFinalizeSync();
Workspace->OnWorkspaceSynchronized().AddRaw(this, &FDisplayClusterMessageInterceptionModule::WorkspaceSyncEvent);
Workspace->OnActivityAddedOrUpdated().AddRaw(this, &FDisplayClusterMessageInterceptionModule::ActivityUpdated);
Workspace->AddWorkspaceFinalizeDelegate(TEXT("nDisplay Interceptor"),
FCanFinalizeWorkspaceDelegate::CreateRaw(
this, &FDisplayClusterMessageInterceptionModule::CanFinalizeWorkspaceSync));
Workspace->AddWorkspaceCanProcessPackagesDelegate(TEXT("nDisplay Interceptor"),
FCanProcessPendingPackages::CreateRaw(
this, &FDisplayClusterMessageInterceptionModule::CanProcessPendingPackages));
}
else if (ConnectionStatus == EConcertConnectionStatus::Disconnected)
{
// Once we disconnect from MU it is not possible to coordinate an activity sync any longer because other
// nodes may be still connected and not expected to re-connect. In this case we never reenter into an
// activity sync event.
//
bWasEverDisconnected = true;
CanFinalizeWorkspace = true;
bAllowPackages = true;
UE_LOG(LogDisplayClusterInterception, Display, TEXT("Disconnecting from session."));
Workspace->RemoveWorkspaceFinalizeDelegate(TEXT("nDisplay Interceptor"));
Workspace->RemoveWorkspaceCanProcessPackagesDelegate(TEXT("nDisplay Interceptor"));
Workspace->OnWorkspaceSynchronized().RemoveAll(this);
Workspace->OnActivityAddedOrUpdated().RemoveAll(this);
}
}
void ActivityUpdated(const FConcertClientInfo& InClientInfo, const FConcertSyncActivity& InActivity, const FStructOnScope& /*unused*/)
{
if (InActivity.EventType == EConcertSyncActivityEventType::Package)
{
PackageSyncEvent(InActivity.ActivityId);
}
}
void OnMultiUserStartup(TSharedRef<IConcertClientSession> InSession)
{
if (InSession->GetConnectionStatus() == EConcertConnectionStatus::Connected)
{
OnSessionConnectionChanged(*InSession, EConcertConnectionStatus::Connected);
}
}
void OnMultiUserShutdown(TSharedRef<IConcertClientSession> InSession)
{
CanFinalizeWorkspace = true;
}
#endif
void OnDisplayClusterStartSession()
{
if (IDisplayCluster::IsAvailable() && IDisplayCluster::Get().GetOperationMode() == EDisplayClusterOperationMode::Cluster)
{
// Create the message interceptor only when we're in cluster mode
Interceptor = MakeShared<FDisplayClusterMessageInterceptor, ESPMode::ThreadSafe>();
// Register cluster event listeners
IDisplayClusterClusterManager* ClusterManager = IDisplayCluster::Get().GetClusterMgr();
if (ClusterManager && !ListenerDelegate.IsBound())
{
ListenerDelegate = FOnClusterEventJsonListener::CreateRaw(this, &FDisplayClusterMessageInterceptionModule::HandleClusterEvent);
ClusterManager->AddClusterEventJsonListener(ListenerDelegate);
ResendSyncEvent(ClusterManager);
}
//Start with interception enabled
bStartInterceptionRequested = true;
// Register cluster session events
IDisplayCluster::Get().GetCallbacks().OnDisplayClusterEndSession().AddRaw(this, &FDisplayClusterMessageInterceptionModule::StopInterception);
IDisplayCluster::Get().GetCallbacks().OnDisplayClusterPreTick().AddRaw(this, &FDisplayClusterMessageInterceptionModule::HandleClusterPreTick);
IDisplayCluster::Get().GetCallbacks().OnDisplayClusterFailoverNodeDown().AddRaw(this, &FDisplayClusterMessageInterceptionModule::HandleClusterNodeFailed);
SetupForMultiUser();
}
}
void HandleClusterEvent(const FDisplayClusterClusterEventJson& InEvent)
{
if (InEvent.Category == DisplayClusterInterceptionModuleUtils::EventSetup)
{
HandleMessageInterceptorSetupEvent(InEvent);
}
else if (InEvent.Category == DisplayClusterInterceptionModuleUtils::EventSync)
{
HandleWorkspaceSyncEvent(InEvent);
}
else if (InEvent.Category == DisplayClusterInterceptionModuleUtils::PackageSync)
{
HandlePackageEvent(InEvent);
}
else
{
//All events except our settings synchronization one are passed to the interceptor
if (Interceptor)
{
Interceptor->HandleClusterEvent(InEvent);
}
}
}
void HandleClusterPreTick()
{
// StartInterception will be handled once settings synchronization is completed to ensure the same behavior across the cluster
if (bStartInterceptionRequested)
{
IDisplayClusterClusterManager* ClusterManager = IDisplayCluster::Get().GetClusterMgr();
if (Interceptor && ClusterManager)
{
if (bSettingsSynchronizationDone)
{
UE_LOG(LogDisplayClusterInterception, Display, TEXT("Sync received! Starting interception!"));
bStartInterceptionRequested = false;
Interceptor->Start(IMessagingModule::Get().GetDefaultBus().ToSharedRef());
}
}
}
if (Interceptor)
{
Interceptor->SyncMessages();
}
CanFinalizeWorkspace.UpdateOnPreTick();
}
void StartInterception()
{
bStartInterceptionRequested = true;
}
void StopInterception()
{
if (Interceptor)
{
Interceptor->Stop();
}
}
void HandleClusterNodeFailed(const FString& NodeId)
{
UE_LOG(LogDisplayClusterInterception, Log, TEXT("Handling '%s' node failure..."), *NodeId);
HandleWorkspaceNodeFailure(NodeId);
HandlePackageNodeFailure(NodeId);
if (Interceptor)
{
Interceptor->HandleClusterNodeFailure(NodeId);
}
}
void HandlePackageEvent(const FDisplayClusterClusterEventJson& InEvent)
{
UE_LOG(LogDisplayClusterInterception, Display, TEXT("Handle multi-user package event sync with id %s -> %s."), *InEvent.Type, *InEvent.Name);
FScopeLock Lock(&PackageActivitiesCS);
TSet<FString>& NodesReceivedPackage = PackageActivities.FindOrAdd(InEvent.Type);
NodesReceivedPackage.Add(InEvent.Name);
IDisplayClusterClusterManager* ClusterManager = IDisplayCluster::Get().GetClusterMgr();
if (NodesReceivedPackage.Num() >= (int32)ClusterManager->GetNodesAmount())
{
UE_LOG(LogDisplayClusterInterception, Display, TEXT("All nodes have received package event %s."), *InEvent.Type);
PackageActivities.Remove(InEvent.Type);
if (PackageActivities.Num() == 0)
{
UE_LOG(LogDisplayClusterInterception, Display, TEXT("Releasing the package lock."));
bAllowPackages = true;
}
}
}
void HandlePackageNodeFailure(const FString& NodeId)
{
FScopeLock Lock(&PackageActivitiesCS);
TSet<FString> ActivitiesToClean;
const int32 NodesAmount = (int32)IDisplayCluster::Get().GetClusterMgr()->GetNodesAmount();
// Remove this node from the awaiting list of all activities
for (TPair<FString, TSet<FString>>& PackageActivity : PackageActivities)
{
if (PackageActivity.Value.Remove(NodeId) > 0)
{
if (PackageActivity.Value.Num() >= NodesAmount)
{
UE_LOG(LogDisplayClusterInterception, Display, TEXT("All nodes have received package event %s."), *PackageActivity.Key);
ActivitiesToClean.Add(PackageActivity.Key);
}
}
}
// Forget activities that have been received already by all nodes
for (const FString& ActivityId : ActivitiesToClean)
{
PackageActivities.Remove(ActivityId);
}
// Release lock
if (PackageActivities.Num() == 0)
{
UE_LOG(LogDisplayClusterInterception, Display, TEXT("Releasing the package lock."));
bAllowPackages = true;
}
}
void HandleWorkspaceSyncEvent(const FDisplayClusterClusterEventJson& InEvent)
{
UE_LOG(LogDisplayClusterInterception, Display, TEXT("Handle multi-user workspace sync -> %s."), *InEvent.Name);
IDisplayClusterClusterManager* ClusterManager = IDisplayCluster::Get().GetClusterMgr();
FScopeLock Lock(&NodesReadyCS);
NodesReady.Add(InEvent.Name);
if (NodesReady.Num() >= (int32)ClusterManager->GetNodesAmount())
{
UE_LOG(LogDisplayClusterInterception, Display, TEXT("Allowing multi-user workspace sync on next pre-tick."));
CanFinalizeWorkspace = true;
}
}
void HandleWorkspaceNodeFailure(const FString& NodeId)
{
FScopeLock Lock(&NodesReadyCS);
if (NodesReady.Remove(NodeId) > 0)
{
if (NodesReady.Num() >= (int32)IDisplayCluster::Get().GetClusterMgr()->GetNodesAmount())
{
UE_LOG(LogDisplayClusterInterception, Display, TEXT("Allowing multi-user workspace sync on next pre-tick."));
CanFinalizeWorkspace = true;
}
}
}
void HandleMessageInterceptorSetupEvent(const FDisplayClusterClusterEventJson& InEvent)
{
if (Interceptor)
{
const FString& ExportedSettings = InEvent.Parameters.FindChecked(DisplayClusterInterceptionModuleUtils::EventParameterSettings);
FMessageInterceptionSettings::StaticStruct()->ImportText(*ExportedSettings, &SynchronizedSettings, nullptr, EPropertyPortFlags::PPF_None, GLog, FMessageInterceptionSettings::StaticStruct()->GetName());
IDisplayClusterClusterManager* ClusterManager = IDisplayCluster::Get().GetClusterMgr();
if (ClusterManager)
{
Interceptor->Setup(ClusterManager, SynchronizedSettings);
bSettingsSynchronizationDone = true;
UE_LOG(LogDisplayClusterInterception, Display, TEXT("Node '%s' received synchronization settings event"), *ClusterManager->GetNodeId());
}
}
}
private:
void ResetFinalizeSync()
{
UE_LOG(LogDisplayClusterInterception, Display, TEXT("Temporarily disabling multi-user workspace sync."));
CanFinalizeWorkspace.ResetWithValue(false);
// Note, we intentially do not reset NodesReady here because of the timing of joining the MU session thereby causing the node
// to forever not receive updates.
}
bool bWasEverDisconnected = false;
FSetBoolOnPreTick CanFinalizeWorkspace;
TSet<FString> NodesReady;
FCriticalSection NodesReadyCS;
std::atomic<bool> bAllowPackages = true;
TMap<FString, TSet<FString>> PackageActivities;
FCriticalSection PackageActivitiesCS;
/** MessageBus interceptor */
TSharedPtr<FDisplayClusterMessageInterceptor, ESPMode::ThreadSafe> Interceptor;
/** Settings to be used synchronized around the cluster */
FMessageInterceptionSettings SynchronizedSettings;
/** Cluster event listener delegate */
FOnClusterEventJsonListener ListenerDelegate;
/** Console commands handle. */
TUniquePtr<FAutoConsoleCommand> StartMessageSyncCommand;
TUniquePtr<FAutoConsoleCommand> StopMessageSyncCommand;
/** Request to start interception. Cached to be done once synchronization is done. */
bool bStartInterceptionRequested;
/** Flag to check if synchronization was done */
bool bSettingsSynchronizationDone = false;
};
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FDisplayClusterMessageInterceptionModule, DisplayClusterMessageInterception);