Files
UnrealEngine/Engine/Plugins/XGEController/Source/Private/XGEControllerModule.cpp
2025-05-18 13:04:45 +08:00

619 lines
20 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "XGEControllerModule.h"
#include "Features/IModularFeatures.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformNamedPipe.h"
#include "HAL/PlatformFileManager.h"
#include "HAL/PlatformFile.h"
#include "Misc/CommandLine.h"
#include "Misc/ScopeLock.h"
#include "Misc/Paths.h"
#include "Misc/Guid.h"
#include "Modules/ModuleManager.h"
#include "Serialization/MemoryWriter.h"
#include "Modules/ModuleManager.h"
#include "ShaderCompiler.h"
// Comma separated list of executable file names which should be intercepted by XGE.
// Update this list if adding new tasks.
#define XGE_INTERCEPT_EXE_NAMES TEXT("ShaderCompileWorker")
#if PLATFORM_WINDOWS
#include "Windows/AllowWindowsPlatformTypes.h"
THIRD_PARTY_INCLUDES_START
#include <winreg.h>
THIRD_PARTY_INCLUDES_END
#include "Windows/HideWindowsPlatformTypes.h"
#define XGE_CONTROL_WORKER_NAME TEXT("XGEControlWorker")
#ifdef _M_ARM64EC
#define XGE_CONTROL_WORKER_FILENAME TEXT("XGEControlWorkerarm64ec.exe")
#elif PLATFORM_CPU_ARM_FAMILY
#define XGE_CONTROL_WORKER_FILENAME TEXT("XGEControlWorkerarm64.exe")
#else
#define XGE_CONTROL_WORKER_FILENAME TEXT("XGEControlWorker.exe")
#endif
FString GetControlWorkerBinariesPath()
{
return FPaths::EngineDir() / TEXT("Binaries/Win64/");
}
FString GetControlWorkerExePath()
{
return FPaths::EngineDir() / TEXT("Binaries/Win64/") / XGE_CONTROL_WORKER_FILENAME;
}
#else
#error XGE Controller is not supported on non-Windows platforms.
#endif
DEFINE_LOG_CATEGORY_STATIC(LogXGEController, Log, Log);
namespace XGEControllerVariables
{
int32 Enabled = 1;
FAutoConsoleVariableRef CVarXGEControllerEnabled(
TEXT("r.XGEController.Enabled"),
Enabled,
TEXT("Enables or disables the use of XGE for various build tasks in the engine.\n")
TEXT("0: Local builds only. \n")
TEXT("1: Distribute builds using XGE (default)."),
ECVF_ReadOnly); // Must be set on start-up, e.g. via config ini
float Timeout = 2.0f;
FAutoConsoleVariableRef CVarXGEControllerTimeout(
TEXT("r.XGEController.Timeout"),
Timeout,
TEXT("The time, in seconds, to wait after all tasks have been completed before shutting down the controller. (default: 2 seconds)."),
ECVF_Default);
int32 AvoidUsingLocalMachine = 1;
FAutoConsoleVariableRef CVarXGEControllerAvoidUsingLocalMachine(
TEXT("r.XGEController.AvoidUsingLocalMachine"),
AvoidUsingLocalMachine,
TEXT("Whether XGE tasks should avoid running on the local machine (to reduce the oversubscription with local async and out-of-process work).\n")
TEXT("0: Do not avoid. Distributed tasks will be spawned on all available XGE agents. Can cause oversubscription on the initiator machine. \n")
TEXT("1: Avoid spawning tasks on the local (initiator) machine except when running a commandlet or -buildmachine is passed (default).\n")
TEXT("2: Avoid spawning tasks on the local (initiator) machine."),
FConsoleVariableDelegate::CreateLambda(
[](IConsoleVariable* /*CVar*/) -> void
{
// If XGE has changed its support for local workers, notify shader compiling manager in case local shader compiling thread must be spun up.
if (GShaderCompilingManager)
{
GShaderCompilingManager->OnDistributedShaderCompilingChanged();
}
}
),
ECVF_Default); // This can be flipped any time XGEControlWorker is restarted
}
namespace XGEController
{
/** Whether XGE tasks should avoid using the local machine to reduce oversubscription */
bool AvoidUsingLocalMachine()
{
switch (XGEControllerVariables::AvoidUsingLocalMachine)
{
case 0:
return false;
case 2:
return true;
default:
break;
}
static bool bForceUsingLocalMachine = !FPlatformMisc::GetEnvironmentVariable(TEXT("UE-XGEControllerForceUsingLocalMachine")).IsEmpty();
return !bForceUsingLocalMachine && !GIsBuildMachine && !IsRunningCommandlet();
}
}
FXGEControllerModule::FXGEControllerModule()
: bSupported(false)
, bModuleInitialized(false)
, bControllerInitialized(false)
, ControlWorkerDirectory(FPaths::ConvertRelativePathToFull(GetControlWorkerBinariesPath()))
, RootWorkingDirectory(FString::Printf(TEXT("%sUnrealXGEWorkingDir/"), FPlatformProcess::UserTempDir()))
, WorkingDirectory(RootWorkingDirectory + FGuid::NewGuid().ToString(EGuidFormats::Digits))
, PipeName(FString::Printf(TEXT("UnrealEngine-XGE-%s"), *FGuid::NewGuid().ToString(EGuidFormats::Digits)))
, TasksCS(new FCriticalSection)
, bShutdown(false)
, bRestartWorker(false)
, LastEventTime(0)
{}
FXGEControllerModule::~FXGEControllerModule()
{
if (TasksCS)
{
delete TasksCS;
TasksCS = nullptr;
}
}
bool FXGEControllerModule::IsSupported()
{
if (bControllerInitialized)
{
return bSupported;
}
#if PLATFORM_WINDOWS
if (!FPlatformProcess::SupportsMultithreading())
{
return false; // current implementation requires worker threads
}
// Check the command line to see if the XGE controller has been enabled/disabled.
// This overrides the value of the console variable.
if (FParse::Param(FCommandLine::Get(), TEXT("xgecontroller")))
{
XGEControllerVariables::Enabled = 1;
}
if (FParse::Param(FCommandLine::Get(), TEXT("noxgecontroller")) ||
FParse::Param(FCommandLine::Get(), TEXT("noxgeshadercompile")) ||
FParse::Param(FCommandLine::Get(), TEXT("noshaderworker")))
{
XGEControllerVariables::Enabled = 0;
}
// Check for a valid installation of Incredibuild by seeing if xgconsole.exe exists.
if (XGEControllerVariables::Enabled == 1)
{
// Try to read from the registry
FString RegistryPathString;
if (!FWindowsPlatformMisc::QueryRegKey(HKEY_LOCAL_MACHINE, TEXT("SOFTWARE\\Xoreax\\IncrediBuild\\Builder"), TEXT("Folder"), RegistryPathString))
{
FWindowsPlatformMisc::QueryRegKey(HKEY_LOCAL_MACHINE, TEXT("SOFTWARE\\WOW6432Node\\Xoreax\\IncrediBuild\\Builder"), TEXT("Folder"), RegistryPathString);
}
if (!RegistryPathString.IsEmpty())
{
RegistryPathString = FPaths::Combine(RegistryPathString, TEXT("xgConsole.exe"));
}
// Try to find xgConsole.exe from the PATH environment variable
FString PathString;
{
FString EnvString = FPlatformMisc::GetEnvironmentVariable(TEXT("Path"));
int32 PathStart = EnvString.Find(TEXT("Xoreax\\IncrediBuild"), ESearchCase::IgnoreCase, ESearchDir::FromStart);
if (PathStart != INDEX_NONE)
{
// Move to the front of this string. The +1 is to change the return value from -1 to 0 (signifying our path was at the start of the string) or to move us past the ";"
PathStart = EnvString.Find(TEXT(";"), ESearchCase::CaseSensitive, ESearchDir::FromEnd, PathStart) + 1;
int32 PathEnd = EnvString.Find(TEXT(";"), ESearchCase::CaseSensitive, ESearchDir::FromStart, PathStart);
if (PathEnd == INDEX_NONE)
{
PathEnd = EnvString.Len();
}
FString Directory = EnvString.Mid(PathStart, PathEnd - PathStart);
PathString = FPaths::Combine(Directory, TEXT("xgConsole.exe"));
}
}
// List of possible paths to xgconsole.exe
const FString Paths[] =
{
*RegistryPathString,
TEXT("C:\\Program Files\\Xoreax\\IncrediBuild\\xgConsole.exe"),
TEXT("C:\\Program Files (x86)\\Xoreax\\IncrediBuild\\xgConsole.exe"),
*PathString
};
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
bool bFound = false;
for (const FString& Path : Paths)
{
if (!Path.IsEmpty() && PlatformFile.FileExists(*Path))
{
bFound = true;
XGConsolePath = Path;
break;
}
}
if (!bFound)
{
UE_LOG(LogXGEController, Log, TEXT("Cannot use XGE Controller as Incredibuild is not installed on this machine."));
XGEControllerVariables::Enabled = 0;
}
else
{
// xgConsole.exe has been found.
// Check we have a compatible version of XGE by finding the version registry key.
int32 Version = 0;
HKEY RegistryKey;
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT("SOFTWARE\\Xoreax\\IncrediBuild\\Builder"), 0, KEY_READ, &RegistryKey) == ERROR_SUCCESS ||
RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT("SOFTWARE\\WOW6432Node\\Xoreax\\IncrediBuild\\Builder"), 0, KEY_READ, &RegistryKey) == ERROR_SUCCESS)
{
DWORD Type;
BYTE Buffer[512] = {};
DWORD Size = sizeof(Buffer);
if (RegQueryValueEx(RegistryKey, TEXT("Version"), nullptr, &Type, Buffer, &Size) == ERROR_SUCCESS)
{
if (Type == REG_SZ && Size > 0 && FCString::IsNumeric((TCHAR*)Buffer))
{
Version = FCString::Atoi((TCHAR*)Buffer);
}
}
RegCloseKey(RegistryKey);
}
if (Version == 0)
{
UE_LOG(LogXGEController, Display, TEXT("Cannot determine XGE version. XGE Shader compilation with the interception interface may fail [%s]."), *XGConsolePath);
}
else if (Version < 1002867)
{
UE_LOG(LogXGEController, Display, TEXT("XGE version 8.01 (build 1867) or higher is required for XGE shader compilation with the interception interface [%s]."), *XGConsolePath);
XGEControllerVariables::Enabled = 0;
}
// check if build service is running - without this, the build will make no progress
const TCHAR* XGEBuildServiceExecutableName = TEXT("BuildService.exe");
if (!FPlatformProcess::IsApplicationRunning(XGEBuildServiceExecutableName))
{
UE_LOG(LogXGEController, Display, TEXT("XGE's background service (%s) is not running - service is likely disabled on this machine [%s]."), XGEBuildServiceExecutableName, *XGConsolePath);
XGEControllerVariables::Enabled = 0;
}
if (!PlatformFile.FileExists(*GetControlWorkerExePath()))
{
UE_LOG(LogXGEController, Display, TEXT("XGEControlWorker.exe does not exist, XGE may be disabled in your Build Configuration, cannot use XGE [%s]."), *XGConsolePath);
XGEControllerVariables::Enabled = 0;
}
}
}
return (bSupported = (XGEControllerVariables::Enabled == 1));
#else
// Not supported on other platforms.
return false;
#endif
}
void FXGEControllerModule::CleanWorkingDirectory()
{
// Only clean the directory if we are the only instance running,
// and we're not running in multi-process mode.
if (FPlatformProcess::IsFirstInstance() && !FParse::Param(FCommandLine::Get(), TEXT("Multiprocess")))
{
UE_LOG(LogXGEController, Log, TEXT("Cleaning working directory: %s"), *RootWorkingDirectory);
IFileManager::Get().DeleteDirectory(*RootWorkingDirectory, false, true);
}
}
void FXGEControllerModule::StartupModule()
{
check(!bModuleInitialized);
IModularFeatures::Get().RegisterModularFeature(GetModularFeatureType(), this);
bModuleInitialized = true;
}
void FXGEControllerModule::ShutdownModule()
{
check(bModuleInitialized);
IModularFeatures::Get().UnregisterModularFeature(GetModularFeatureType(), this);
if (IsSupported())
{
bShutdown = true;
if (bControllerInitialized)
{
WriteOutThreadEvent->Trigger();
// Wait for worker threads to exit
if (WriteOutThreadFuture.IsValid())
{
WriteOutThreadFuture.Wait();
WriteOutThreadFuture = TFuture<void>();
}
}
// Cancel any remaining tasks
for (auto Iter = DispatchedTasks.CreateIterator(); Iter; ++Iter)
{
FDistributedBuildTask* Task = Iter.Value();
Task->Cancel();
delete Task;
}
FDistributedBuildTask* Task;
while (PendingTasks.Dequeue(Task))
{
Task->Cancel();
delete Task;
}
PendingTasks.Empty();
DispatchedTasks.Empty();
}
CleanWorkingDirectory();
bModuleInitialized = false;
bControllerInitialized = false;
}
void FXGEControllerModule::InitializeController()
{
if (ensureAlwaysMsgf(!bControllerInitialized, TEXT("Multiple initialization of the XGE controller!")))
{
CleanWorkingDirectory();
bShutdown = false;
// The actual initialization happens in IsSupported() when it is called with bControllerInitialized being false
if (IsSupported())
{
WriteOutThreadFuture = Async(EAsyncExecution::Thread, [this]() { WriteOutThreadProc(); });
bControllerInitialized = true;
UE_LOG(LogXGEController, Display, TEXT("Initialized XGE controller. XGE tasks will %sbe spawned on this machine."),
XGEController::AvoidUsingLocalMachine() ? TEXT("not ") : TEXT("")
);
}
}
}
bool FXGEControllerModule::SupportsLocalWorkers()
{
// XGE supports local workers if it's not explicitly disabled
const bool bAvoidLocalWorkers = XGEController::AvoidUsingLocalMachine();
return !bAvoidLocalWorkers;
}
void FXGEControllerModule::WriteOutThreadProc()
{
do
{
bRestartWorker = false;
while (!AreTasksPending() && !bShutdown)
{
UE_LOG(LogXGEController, Verbose, TEXT("Waiting for new tasks."));
WriteOutThreadEvent->Wait();
}
if (bShutdown)
{
UE_LOG(LogXGEController, Verbose, TEXT("Shutting down communication with the XGE controller."));
return;
}
// To handle spaces in the engine path, we just pass the XGEController.exe filename to xgConsole,
// and set the working directory of xgConsole.exe to the engine binaries folder below.
FString XGConsoleArgs = FString::Printf(TEXT("/VIRTUALIZEDIRECTX /allowremote=\"%s\" %s /allowintercept=\"%s\" /title=\"Unreal Engine XGE Tasks\" /monitordirs=\"%s\" /command=\"%s -xgecontroller %s\""),
XGE_INTERCEPT_EXE_NAMES,
(XGEController::AvoidUsingLocalMachine() && (IsRunningCommandlet() || !GShaderCompilingManager->IgnoreAllThrottling())) ? TEXT("/avoidlocal=ON") : TEXT(""),
XGE_CONTROL_WORKER_NAME,
*WorkingDirectory,
XGE_CONTROL_WORKER_FILENAME,
*PipeName);
// Create the output pipe as a server...
if (!OutputNamedPipe.Create(FString::Printf(TEXT("\\\\.\\pipe\\%s-A"), *PipeName), true, false))
{
UE_LOG(LogXGEController, Fatal, TEXT("Failed to create the output XGE named pipe."));
}
const int32 PriorityModifier = 0; // normal by default. Interactive use case shouldn't be affected as the jobs will avoid local machine
// Start the controller process
uint32 XGConsoleProcID = 0;
UE_LOG(LogXGEController, Verbose, TEXT("Launching xgConsole"));
BuildProcessHandle = FPlatformProcess::CreateProc(*XGConsolePath, *XGConsoleArgs, false, false, true, &XGConsoleProcID, PriorityModifier, *ControlWorkerDirectory, nullptr);
if (!BuildProcessHandle.IsValid())
{
UE_LOG(LogXGEController, Fatal, TEXT("Failed to launch the XGE control worker process."));
}
// If the engine crashes, we don't get a chance to kill the build process.
// Start up the build monitor process to monitor for engine crashes.
uint32 BuildMonitorProcessID;
UE_LOG(LogXGEController, Verbose, TEXT("Launching XGEController to fan out tasks"));
FString XGMonitorArgs = FString::Printf(TEXT("-xgemonitor %d %d"), FPlatformProcess::GetCurrentProcessId(), XGConsoleProcID);
FProcHandle BuildMonitorHandle = FPlatformProcess::CreateProc(*GetControlWorkerExePath(), *XGMonitorArgs, true, false, false, &BuildMonitorProcessID, PriorityModifier, nullptr, nullptr);
FPlatformProcess::CloseProc(BuildMonitorHandle);
// Wait for the controller to connect to the output pipe
if (!OutputNamedPipe.OpenConnection())
{
UE_LOG(LogXGEController, Fatal, TEXT("Failed to open a connection on the output XGE named pipe."));
}
// Connect the input pipe (controller is the server)...
if (!InputNamedPipe.Create(FString::Printf(TEXT("\\\\.\\pipe\\%s-B"), *PipeName), false, false))
{
UE_LOG(LogXGEController, Fatal, TEXT("Failed to connect the input XGE named pipe."));
}
// Pass the xgConsole process ID to the XGE control worker, so it can terminate the build on exit
if (!OutputNamedPipe.WriteBytes(sizeof(XGConsoleProcID), &XGConsoleProcID))
{
UE_LOG(LogXGEController, Fatal, TEXT("Failed to pass xgConsole process ID to XGE control worker."));
}
LastEventTime = FPlatformTime::Cycles();
// Launch the output thread
ReadBackThreadFuture = Async(EAsyncExecution::Thread, [this]() { ReadBackThreadProc(); });
// Main Tasks Loop
TArray<uint8> WriteBuffer;
while (true)
{
WriteBuffer.Reset();
// Wait for new tasks to arrive, with a timeout...
while (!bShutdown && !bRestartWorker && !AreTasksPending())
{
uint32 LastTime = (uint32)FPlatformAtomics::InterlockedAdd(reinterpret_cast<volatile int32*>(&LastEventTime), 0);
float ElapsedSeconds = (FPlatformTime::Cycles() - LastTime) * FPlatformTime::GetSecondsPerCycle();
float SecondsToWait = XGEControllerVariables::Timeout - ElapsedSeconds;
if (!WriteOutThreadEvent->Wait(FMath::CeilToInt(SecondsToWait * 1000.0f)) && !AreTasksDispatchedOrPending())
{
// Timed out, and no more pending or dispatched tasks. End the current build.
bRestartWorker = true;
break;
}
}
if (bShutdown || bRestartWorker)
break;
// Take one task from the pending queue.
FDistributedBuildTask* Task = nullptr;
{
FScopeLock Lock(TasksCS);
PendingTasks.Dequeue(Task);
}
if (Task)
{
WriteBuffer.Reset();
WriteBuffer.AddUninitialized(sizeof(uint32));
FMemoryWriter Writer(WriteBuffer, false, true);
Writer << Task->ID;
Writer << Task->CommandData.Command;
const FString InputFileName = FPaths::GetCleanFilename(Task->CommandData.InputFileName);
const FString OutputFileName = FPaths::GetCleanFilename(Task->CommandData.OutputFileName);
FString WorkerParameters = FString::Printf(TEXT("\"%s/\" %d 0 \"%s\" \"%s\" -xge_int %s"),
*Task->CommandData.WorkingDirectory,
Task->CommandData.DispatcherPID,
*InputFileName,
*OutputFileName,
*Task->CommandData.ExtraCommandArgs);
Writer << WorkerParameters;
*reinterpret_cast<uint32*>(WriteBuffer.GetData()) = WriteBuffer.Num() - sizeof(uint32);
// Move the tasks to the dispatched tasks map before launching it
{
FScopeLock Lock(TasksCS);
DispatchedTasks.Add(Task->ID, Task);
}
if (!OutputNamedPipe.WriteBytes(WriteBuffer.Num(), WriteBuffer.GetData()))
{
// Error occurred whilst writing task args to the named pipe.
// It's likely the controller process was terminated.
bRestartWorker = true;
}
// Update the last event time.
FPlatformAtomics::InterlockedExchange(reinterpret_cast<volatile int32*>(&LastEventTime), FPlatformTime::Cycles());
}
}
// Destroy the output named pipe. This signals the worker to exit, if it hasn't already.
OutputNamedPipe.Destroy();
// Wait for the read back thread to exit. This will happen when the input pipe is closed by the worker.
if (ReadBackThreadFuture.IsValid())
{
ReadBackThreadFuture.Wait();
ReadBackThreadFuture = TFuture<void>();
}
// Wait for the build process
if (BuildProcessHandle.IsValid())
{
if (FPlatformProcess::IsProcRunning(BuildProcessHandle))
FPlatformProcess::WaitForProc(BuildProcessHandle);
FPlatformProcess::CloseProc(BuildProcessHandle);
}
// Reclaim dispatched (incomplete) tasks
for (auto Iter = DispatchedTasks.CreateIterator(); Iter; ++Iter)
PendingTasks.Enqueue(Iter.Value());
DispatchedTasks.Reset();
} while (!bShutdown);
}
void FXGEControllerModule::ReadBackThreadProc()
{
while (!bShutdown && !bRestartWorker)
{
FTaskResponse CompletedTaskResponse;
if (!InputNamedPipe.ReadBytes(sizeof(CompletedTaskResponse), &CompletedTaskResponse))
{
// The named pipe was closed or had an error.
// Instruct the write-out thread to restart the worker, then exit.
bRestartWorker = true;
}
else
{
// Update the last event time.
FPlatformAtomics::InterlockedExchange(reinterpret_cast<volatile int32*>(&LastEventTime), FPlatformTime::Cycles());
// We've read a completed task response from the controller.
// Find the task in the map and complete the promise.
FDistributedBuildTask* Task;
{
FScopeLock Lock(TasksCS);
Task = DispatchedTasks.FindAndRemoveChecked(CompletedTaskResponse.ID);
}
Task->Finalize(CompletedTaskResponse.ReturnCode);
delete Task;
}
WriteOutThreadEvent->Trigger();
}
InputNamedPipe.Destroy();
}
FString FXGEControllerModule::CreateUniqueFilePath()
{
check(bSupported);
return FString::Printf(TEXT("%s/%d-xge"), *WorkingDirectory, NextFileID.Increment());
}
TFuture<FDistributedBuildTaskResult> FXGEControllerModule::EnqueueTask(const FTaskCommandData& CommandData)
{
check(bSupported);
TPromise<FDistributedBuildTaskResult> Promise;
TFuture<FDistributedBuildTaskResult> Future = Promise.GetFuture();
// Enqueue the new task
FDistributedBuildTask* Task = new FDistributedBuildTask(NextTaskID.Increment(), CommandData, MoveTemp(Promise));
{
FScopeLock Lock(TasksCS);
PendingTasks.Enqueue(Task);
}
WriteOutThreadEvent->Trigger();
return MoveTemp(Future);
}
XGECONTROLLER_API FXGEControllerModule& FXGEControllerModule::Get()
{
return FModuleManager::LoadModuleChecked<FXGEControllerModule>(TEXT("XGEController"));
}
IMPLEMENT_MODULE(FXGEControllerModule, XGEController);