// Copyright Epic Games, Inc. All Rights Reserved. #include "LightmassSwarm.h" #include "CPUSolver.h" #include "Misc/Paths.h" #include "Misc/ScopeLock.h" #include "Misc/OutputDeviceRedirector.h" #include "HAL/FileManager.h" #include "UnrealLightmass.h" namespace Lightmass { /** The first NUM_TASKTIMINGS roundtrip timings. */ FTiming FTiming::GTaskTimings[NUM_TASKTIMINGS]; /** Number of tasks requested so far. */ volatile int32 FTiming::GTaskRequestCounter = 0; /** Number of tasks received so far. */ volatile int32 FTiming::GTaskReceiveCounter = 0; static int32 SwarmConnectionDroppedExitCode = 2; /** * Constructs the Swarm wrapper used by Lightmass. * @param SwarmInterface The global SwarmInterface to use * @param JobGuid Guid that identifies the job we're working on * @param TaskQueueSize Number of tasks we should try to keep in the queue */ FLightmassSwarm::FLightmassSwarm( NSwarm::FSwarmInterface& SwarmInterface, const FGuid& InJobGuid, int32 TaskQueueSize ) : API( SwarmInterface ) , JobGuid(InJobGuid) , bIsDone(false) , QuitRequest(false) , TaskQueue(TaskQueueSize) , NumRequestedTasks(0) , TotalBytesRead(0) , TotalBytesWritten(0) , TotalSecondsRead(0.0) , TotalSecondsWritten(0.0) , TotalNumReads(0) , TotalNumWrites(0) { API.SetJobGuid( JobGuid ); NSwarm::TLogFlags ConnectionLogFlags = NSwarm::SWARM_LOG_NONE; if (GReportDetailedStats) { ConnectionLogFlags = NSwarm::SWARM_LOG_ALL; } FString OptionsFolder = FPaths::Combine(*FPaths::GameAgnosticSavedDir(), TEXT("Swarm")); OptionsFolder = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*OptionsFolder); bool bConnectionEstablished = API.OpenConnection(SwarmCallback, this, ConnectionLogFlags, *OptionsFolder) >= 0; checkf(bConnectionEstablished, TEXT("Tried to open a connection to Swarm, but failed")); } FLightmassSwarm::~FLightmassSwarm() { API.CloseConnection(); } /** * Opens a new channel alongside the root channel owned by the LightmassSwarm object * * @param ChannelName Name of the channel * @param ChannelFlags Flags (read, write, etc) for the channel * @param bPushChannel If true, this new channel will be auto-pushed onto the stack * * @return The channel that was opened, without disturbing the root channel */ int32 FLightmassSwarm::OpenChannel( const TCHAR* ChannelName, int32 ChannelFlags, bool bPushChannel ) { int32 NewChannel = API.OpenChannel( ChannelName, ( NSwarm::TChannelFlags )ChannelFlags ); if ((NewChannel == NSwarm::SWARM_ERROR_CONNECTION_NOT_FOUND) || (NewChannel == NSwarm::SWARM_ERROR_CONNECTION_DISCONNECTED)) { // The connection has dropped, exit with a special code exit(SwarmConnectionDroppedExitCode); } if (bPushChannel) { PushChannel(NewChannel); } return NewChannel; } /** * Closes a channel previously opened with OpenChannel * * @param Channel The channel to close */ void FLightmassSwarm::CloseChannel( int32 Channel ) { int32 ReturnCode = API.CloseChannel(Channel); if ((ReturnCode == NSwarm::SWARM_ERROR_CONNECTION_NOT_FOUND) || (ReturnCode == NSwarm::SWARM_ERROR_CONNECTION_DISCONNECTED)) { // The connection has dropped, exit with a special code exit(SwarmConnectionDroppedExitCode); } } /** * Pushes a new channel on the stack as the current channel to read from * * @param Channel New channel to read from */ void FLightmassSwarm::PushChannel(int32 Channel) { FScopeLock Lock(&SwarmAccess); ChannelStack.Push(Channel); } /** * Pops the top channel * * @param bCloseChannel If true, the channel will be closed when it is popped off */ void FLightmassSwarm::PopChannel(bool bCloseChannel) { int32 PoppedChannel; { FScopeLock Lock(&SwarmAccess); PoppedChannel = ChannelStack.Pop(); } if (bCloseChannel) { int32 ReturnCode = API.CloseChannel(PoppedChannel); if ((ReturnCode == NSwarm::SWARM_ERROR_CONNECTION_NOT_FOUND) || (ReturnCode == NSwarm::SWARM_ERROR_CONNECTION_DISCONNECTED)) { // The connection has dropped, exit with a special code exit(SwarmConnectionDroppedExitCode); } } } /** * Prefetch tasks into our work queue, which is where RequestTask gets * its work items from. */ void FLightmassSwarm::PrefetchTasks() { if ( GDebugMode == false ) { // Request the initial TaskQueueSize tasks from Swarm. for ( int32 RequestIndex=0; RequestIndex < TaskQueue.GetMaxNumElements(); ++RequestIndex ) { FTiming::NotifyTaskRequested(); FPlatformAtomics::InterlockedIncrement( &NumRequestedTasks ); SendMessage( NSwarm::FMessage( NSwarm::MESSAGE_TASK_REQUEST ) ); } } } /** * Thread-safe blocking call to request a new task from the local task queue. * It will block until there's a task in the local queue, or the timeout period has passed. * If a task is returned, it will asynchronously request a new task from Swarm to keep the queue full. * You must call AcceptTask() or RejectTask() if a task is returned. * * @param TaskGuid [out] When successful, contains the FGuid for the new task * @param WaitTime Timeout period in milliseconds, or -1 for INFINITE * @return true if a Task Guid is returned, false if the timeout period has passed or IsDone()/ReceivedQuitRequest() is true */ bool FLightmassSwarm::RequestTask( FGuid& TaskGuid, uint32 WaitTime ) { // If we've gotten all tasks there is, don't wait for more. if ( bIsDone ) { WaitTime = 0; } if ( !QuitRequest && TaskQueue.Pop( TaskGuid, WaitTime ) ) { if ( !bIsDone ) { if (GReportDetailedStats) { // Notify Swarm that we're starting work on this Task SendMessage( NSwarm::FTaskState( TaskGuid, NSwarm::TJobTaskState::JOB_TASK_STATE_RUNNING ) ); } // Request a new task from Swarm. FTiming::NotifyTaskRequested(); FPlatformAtomics::InterlockedIncrement( &NumRequestedTasks ); SendMessage( NSwarm::FMessage( NSwarm::MESSAGE_TASK_REQUEST ) ); } return true; } return false; } /** * Accepts a requested task. This will also notify Unreal. * @param TaskGuid The task that is being accepted */ void FLightmassSwarm::AcceptTask( const FGuid& TaskGuid ) { if (GReportDetailedStats) { SendMessage( NSwarm::FTaskState(TaskGuid, NSwarm::JOB_TASK_STATE_ACCEPTED) ); } } /** * Rejects a requested task. This will also notify Unreal. * @param TaskGuid The task that is being rejected */ void FLightmassSwarm::RejectTask( const FGuid& TaskGuid ) { SendMessage( NSwarm::FTaskState(TaskGuid, NSwarm::JOB_TASK_STATE_REJECTED) ); } /** * Tells Swarm that the task is completed and all results have been fully exported. This will also notify Unreal. * @param TaskGuid A guid that identifies the task that has been completed */ void FLightmassSwarm::TaskCompleted( const FGuid& TaskGuid ) { SendMessage( NSwarm::FTaskState(TaskGuid, NSwarm::JOB_TASK_STATE_COMPLETE_SUCCESS) ); } /** * Tells Swarm that the task has failed. This will also notify Unreal. * @param TaskGuid A guid that identifies the task that has failed */ void FLightmassSwarm::TaskFailed( const FGuid& TaskGuid ) { SendMessage( NSwarm::FTaskState(TaskGuid, NSwarm::JOB_TASK_STATE_COMPLETE_FAILURE) ); } /** * Sends a message to Swarm. Thread-safe access. * @param Message Swarm message to send. */ void FLightmassSwarm::SendMessage( const NSwarm::FMessage& Message ) { double StartTime = FPlatformTime::Seconds(); int32 ReturnCode = API.SendMessage( Message ); if ((ReturnCode == NSwarm::SWARM_ERROR_CONNECTION_NOT_FOUND) || (ReturnCode == NSwarm::SWARM_ERROR_CONNECTION_DISCONNECTED)) { // The connection has dropped, exit with a special code exit(SwarmConnectionDroppedExitCode); } GStatistics.SendMessageTime += FPlatformTime::Seconds() - StartTime; } /** * Sends an alert message to Swarm. Thread-safe access. * @param AlertType The type of alert. * @param ObjectGuid The GUID of the object associated with the alert. * @param TypeId The type of object. * @param MessageText The text of the message. */ void FLightmassSwarm::SendAlertMessage( NSwarm::TAlertLevel AlertLevel, const FGuid& ObjectGuid, const int32 TypeId, const TCHAR* MessageText) { double StartTime = FPlatformTime::Seconds(); NSwarm::FAlertMessage AlertMessage(JobGuid, AlertLevel, ObjectGuid, TypeId, MessageText); int32 ReturnCode = API.SendMessage( AlertMessage ); if ((ReturnCode == NSwarm::SWARM_ERROR_CONNECTION_NOT_FOUND) || (ReturnCode == NSwarm::SWARM_ERROR_CONNECTION_DISCONNECTED)) { // The connection has dropped, exit with a special code exit(SwarmConnectionDroppedExitCode); } GStatistics.SendMessageTime += FPlatformTime::Seconds() - StartTime; } /** * Sends text information to Swarm. */ void FLightmassSwarm::SendTextMessageImpl(const TCHAR* Buffer) { // Log it locally. GLog->Log( Buffer ); // Send to Swarm. SendMessage( NSwarm::FInfoMessage( Buffer ) ); } /** * Report to Swarm by sending back a file. * @param Filename File to send back */ void FLightmassSwarm::ReportFile( const TCHAR* Filename ) { //@TODO: Send back the log file to Instigator // int32 ReturnCode = API.AddChannel( Filename, Filename ); // if ((ReturnCode == NSwarm::SWARM_ERROR_CONNECTION_NOT_FOUND) || // (ReturnCode == NSwarm::SWARM_ERROR_CONNECTION_DISCONNECTED)) // { // // The connection has dropped, exit with a special code // exit(SwarmConnectionDroppedExitCode); // } } /** * Triggers the task queue enough times to release all blocked threads. */ void FLightmassSwarm::TriggerAllThreads() { TaskQueue.TriggerAll(); } /** * The callback function used by Swarm to communicate to Lightmass. * @param CallbackMessage Message sent from Swarm * @param UserParam User-defined parameter (specified in OpenConnection). Type-casted FLightmassSwarm pointer. */ void FLightmassSwarm::SwarmCallback( NSwarm::FMessage* CallbackMessage, void* UserParam ) { FLightmassSwarm* This = (FLightmassSwarm*) UserParam; // Always handle QUIT messages. if ( CallbackMessage->Type == NSwarm::MESSAGE_QUIT ) { This->QuitRequest = true; This->TriggerAllThreads(); return; } // Are we requesting a task? if ( This->NumRequestedTasks > 0 ) { switch ( CallbackMessage->Type ) { case NSwarm::MESSAGE_TASK_REQUEST_RESPONSE: { NSwarm::FTaskRequestResponse* Response = (NSwarm::FTaskRequestResponse*) CallbackMessage; if ( Response->ResponseType == NSwarm::RESPONSE_TYPE_RELEASE ) { // No more tasks to give us. This->bIsDone = true; // Decrement our request status and notify a thread. FPlatformAtomics::InterlockedDecrement( &This->NumRequestedTasks ); // Notify all waiting threads (which will make them shut down). This->TriggerAllThreads(); } else if ( Response->ResponseType == NSwarm::RESPONSE_TYPE_SPECIFICATION ) { FTiming::NotifyTaskReceived(); NSwarm::FTaskSpecification* TaskSpec = (NSwarm::FTaskSpecification*) Response; if ( This->QuitRequest || This->TaskQueue.Push(TaskSpec->TaskGuid) == false ) { UE_LOG(LogLightmass, Log, TEXT("SwarmCallback - Rejecting task! (Already shutting down, or task queue overflow.)")); // We got a task but we've already been told to quit, or our queue is erroneously full! // Reject the task. This->SendMessage( NSwarm::FTaskState(TaskSpec->TaskGuid, NSwarm::JOB_TASK_STATE_REJECTED) ); } // Decrement our request status and notify one thread. FPlatformAtomics::InterlockedDecrement( &This->NumRequestedTasks ); } break; } } } // Default response behavior. else { switch ( CallbackMessage->Type ) { case NSwarm::MESSAGE_TASK_REQUEST_RESPONSE: { NSwarm::FTaskRequestResponse* Response = (NSwarm::FTaskRequestResponse*) CallbackMessage; if ( Response->ResponseType == NSwarm::RESPONSE_TYPE_SPECIFICATION ) { UE_LOG(LogLightmass, Log, TEXT("SwarmCallback - Rejecting task! (it wasn't requested.)")); // We're not accepting any tasks at this time. NSwarm::FTaskSpecification* TaskSpec = (NSwarm::FTaskSpecification*) Response; This->SendMessage( NSwarm::FTaskState(TaskSpec->TaskGuid, NSwarm::JOB_TASK_STATE_REJECTED) ); break; } } } } } } //Lightmass