1307 lines
43 KiB
C++
1307 lines
43 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
|
|
// ShaderCompileWorker.cpp : Defines the entry point for the console application.
|
|
//
|
|
|
|
#if UE_ENABLE_INCLUDE_ORDER_DEPRECATED_IN_5_4
|
|
#include "CoreMinimal.h"
|
|
#endif
|
|
#include "Misc/Compression.h"
|
|
#include "RequiredProgramMainCPPInclude.h"
|
|
#include "ShaderCompilerCore.h"
|
|
#include "ShaderCompilerCommon.h"
|
|
#include "ShaderCompilerJobTypes.h"
|
|
#include "ShaderCore.h"
|
|
#include "HAL/ExceptionHandling.h"
|
|
#include "Interfaces/IShaderFormat.h"
|
|
#include "Interfaces/IShaderFormatModule.h"
|
|
#include "Interfaces/ITargetPlatformManagerModule.h"
|
|
#include "RHIShaderFormatDefinitions.inl"
|
|
#include "ShaderCompilerCommon.h"
|
|
#include "ShaderCompileWorkerUtil.h"
|
|
#include "Serialization/MemoryReader.h"
|
|
#include "SocketSubsystem.h"
|
|
|
|
#if PLATFORM_MAC
|
|
#include <mach-o/dyld.h>
|
|
#elif PLATFORM_LINUX
|
|
#include <link.h>
|
|
#endif
|
|
|
|
#define DEBUG_USING_CONSOLE 0
|
|
|
|
static double GLastCompileTime = 0.0;
|
|
static int32 GNumProcessedJobs = 0;
|
|
|
|
enum class EXGEMode
|
|
{
|
|
None,
|
|
Xml,
|
|
Intercept
|
|
};
|
|
|
|
static EXGEMode GXGEMode = EXGEMode::None;
|
|
|
|
inline bool IsUsingXGE()
|
|
{
|
|
return GXGEMode != EXGEMode::None;
|
|
}
|
|
|
|
static FShaderCompileWorkerDiagnostics GWorkerDiagnostics;
|
|
|
|
static void OnXGEJobCompleted(const TCHAR* WorkingDirectory)
|
|
{
|
|
if (GXGEMode == EXGEMode::Xml)
|
|
{
|
|
// To signal compilation completion, create a zero length file in the working directory.
|
|
// This is only required in Xml mode.
|
|
delete IFileManager::Get().CreateFileWriter(*FString::Printf(TEXT("%s/Success"), WorkingDirectory), FILEWRITE_EvenIfReadOnly);
|
|
}
|
|
}
|
|
|
|
#if PLATFORM_WINDOWS // Currently only implemented for windows
|
|
HMODULE GetUbaModule()
|
|
{
|
|
static HMODULE UbaDetoursModule = GetModuleHandleW(L"UbaDetours.dll");
|
|
return UbaDetoursModule;
|
|
}
|
|
#endif
|
|
|
|
inline bool IsUsingUBA()
|
|
{
|
|
static bool UbaLoaded;
|
|
if (UbaLoaded)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
#if PLATFORM_WINDOWS // Currently only implemented for windows
|
|
UbaLoaded = GetUbaModule() != nullptr;
|
|
#elif PLATFORM_MAC
|
|
for (int i = 0, e = _dyld_image_count() && !UbaLoaded; i != e; i++)
|
|
{
|
|
UbaLoaded = strstr(_dyld_get_image_name(i), "UbaDetours.dylib") != nullptr;
|
|
}
|
|
#elif PLATFORM_LINUX
|
|
dl_iterate_phdr([](struct dl_phdr_info *info, size_t size, void *data) { UbaLoaded = UbaLoaded || (info->dlpi_name && strstr(info->dlpi_name, "UbaDetours.so") != nullptr); return 0; }, nullptr);
|
|
#endif
|
|
|
|
return UbaLoaded;
|
|
}
|
|
|
|
#if USING_CODE_ANALYSIS
|
|
[[noreturn]] static inline void ExitWithoutCrash(FSCWErrorCode::ECode ErrorCode, const FString& Message);
|
|
#endif
|
|
|
|
static inline void ExitWithoutCrash(FSCWErrorCode::ECode ErrorCode, const FString& Message)
|
|
{
|
|
GWorkerDiagnostics.ErrorCode = ErrorCode;
|
|
FCString::Snprintf(GErrorExceptionDescription, sizeof(GErrorExceptionDescription), TEXT("%s"), *Message);
|
|
UE_LOG(LogShaders, Fatal, TEXT("%s"), *Message);
|
|
}
|
|
|
|
static const TArray<const IShaderFormat*>& GetShaderFormats()
|
|
{
|
|
static bool bInitialized = false;
|
|
static TArray<const IShaderFormat*> Results;
|
|
|
|
if (!bInitialized)
|
|
{
|
|
bInitialized = true;
|
|
Results.Empty(Results.Num());
|
|
|
|
TArray<FName> Modules;
|
|
FModuleManager::Get().FindModules(SHADERFORMAT_MODULE_WILDCARD, Modules);
|
|
|
|
if (!Modules.Num())
|
|
{
|
|
ExitWithoutCrash(FSCWErrorCode::NoTargetShaderFormatsFound, TEXT("No target shader formats found!"));
|
|
}
|
|
|
|
for (int32 Index = 0; Index < Modules.Num(); Index++)
|
|
{
|
|
IShaderFormat* Format = FModuleManager::LoadModuleChecked<IShaderFormatModule>(Modules[Index]).GetShaderFormat();
|
|
if (Format != nullptr)
|
|
{
|
|
Results.Add(Format);
|
|
}
|
|
}
|
|
}
|
|
return Results;
|
|
}
|
|
|
|
|
|
static void UpdateFileSize(FArchive& OutputFile, int64 FileSizePosition)
|
|
{
|
|
int64 Current = OutputFile.Tell();
|
|
OutputFile.Seek(FileSizePosition);
|
|
OutputFile << Current;
|
|
OutputFile.Seek(Current);
|
|
};
|
|
|
|
static const TCHAR* GetLocalHostname(int32* OutHostnameLength = nullptr)
|
|
{
|
|
static FString Hostname;
|
|
if (Hostname.IsEmpty())
|
|
{
|
|
ISocketSubsystem::Get()->GetHostName(Hostname);
|
|
}
|
|
if (OutHostnameLength)
|
|
{
|
|
*OutHostnameLength = Hostname.Len();
|
|
}
|
|
return *Hostname;
|
|
}
|
|
|
|
static int64 WriteOutputFileHeader(FArchive& OutputFile, int32 CallstackLength, const TCHAR* Callstack,
|
|
int32 ExceptionInfoLength, const TCHAR* ExceptionInfo)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(WriteOutputFileHeader);
|
|
|
|
int64 FileSizePosition = 0;
|
|
int32 OutputVersion = ShaderCompileWorkerOutputVersion;
|
|
OutputFile << OutputVersion;
|
|
|
|
int64 FileSize = 0;
|
|
// Get the position of the Size value to be patched in as the shader progresses
|
|
FileSizePosition = OutputFile.Tell();
|
|
OutputFile << FileSize;
|
|
|
|
OutputFile << GWorkerDiagnostics;
|
|
|
|
OutputFile << GNumProcessedJobs;
|
|
|
|
// Note: Can't use FStrings here as SEH can't be used with destructors
|
|
OutputFile << CallstackLength;
|
|
|
|
OutputFile << ExceptionInfoLength;
|
|
|
|
int32 HostnameLength = 0;
|
|
const TCHAR* Hostname = GetLocalHostname(&HostnameLength);
|
|
OutputFile << HostnameLength;
|
|
|
|
if (GWorkerDiagnostics.ErrorCode != FSCWErrorCode::Success)
|
|
{
|
|
if (CallstackLength > 0)
|
|
{
|
|
OutputFile.Serialize((void*)Callstack, CallstackLength * sizeof(TCHAR));
|
|
}
|
|
|
|
if (ExceptionInfoLength > 0)
|
|
{
|
|
OutputFile.Serialize((void*)ExceptionInfo, ExceptionInfoLength * sizeof(TCHAR));
|
|
}
|
|
|
|
if (HostnameLength > 0)
|
|
{
|
|
OutputFile.Serialize((void*)Hostname, HostnameLength * sizeof(TCHAR));
|
|
}
|
|
|
|
// Store available and used physical memory of host machine on OOM error
|
|
if (GWorkerDiagnostics.ErrorCode == FSCWErrorCode::OutOfMemory)
|
|
{
|
|
FPlatformMemoryStats MemoryStats = FPlatformMemory::GetStats();
|
|
|
|
OutputFile
|
|
<< MemoryStats.AvailablePhysical
|
|
<< MemoryStats.AvailableVirtual
|
|
<< MemoryStats.UsedPhysical
|
|
<< MemoryStats.PeakUsedPhysical
|
|
<< MemoryStats.UsedVirtual
|
|
<< MemoryStats.PeakUsedVirtual
|
|
;
|
|
}
|
|
}
|
|
|
|
// Reset error code as it can be receive a new value now
|
|
FSCWErrorCode::Reset();
|
|
|
|
UpdateFileSize(OutputFile, FileSizePosition);
|
|
return FileSizePosition;
|
|
}
|
|
|
|
|
|
class FWorkLoop
|
|
{
|
|
public:
|
|
// If we have been idle for 20 seconds then exit. Can be overriden from the cmd line with -TimeToLive=N where N is in seconds (and a float value)
|
|
float TimeToLive = 20.0f;
|
|
int32 NumberToProcess = -1;
|
|
bool DisableFileWrite = false;
|
|
bool KeepInput = false;
|
|
TArray<FString> DebugSourceFiles;
|
|
|
|
FWorkLoop(const TCHAR* ParentProcessIdText,const TCHAR* InWorkingDirectory,const TCHAR* InInputFilename,const TCHAR* InOutputFilename, TMap<FName, uint32>& InFormatVersionMap)
|
|
: ParentProcessId(FCString::Atoi(ParentProcessIdText))
|
|
, WorkingDirectory(InWorkingDirectory)
|
|
, InputFilename(InInputFilename)
|
|
, OutputFilename(InOutputFilename)
|
|
, InputFilePath(FString(InWorkingDirectory) / InInputFilename)
|
|
, OutputFilePath(FString(InWorkingDirectory) / InOutputFilename)
|
|
, FormatVersionMap(InFormatVersionMap)
|
|
{
|
|
TArray<FString> Tokens, Switches;
|
|
FCommandLine::Parse(FCommandLine::Get(), Tokens, Switches);
|
|
for (FString& Switch : Switches)
|
|
{
|
|
if (Switch.StartsWith(TEXT("TimeToLive=")))
|
|
{
|
|
TimeToLive = FCString::Atof(Switch.GetCharArray().GetData() + 11);
|
|
}
|
|
else if (Switch.Equals(TEXT("DisableFileWrite")))
|
|
{
|
|
DisableFileWrite = true;
|
|
}
|
|
else if (Switch.Equals(TEXT("KeepInput")))
|
|
{
|
|
KeepInput = true;
|
|
}
|
|
else if (Switch.StartsWith(TEXT("NumJobs=")))
|
|
{
|
|
NumberToProcess = FCString::Atoi(Switch.GetCharArray().GetData() + 8);
|
|
}
|
|
else if (Switch.StartsWith(TEXT("debugsourcefiles=")))
|
|
{
|
|
FString DelimitedSourceFiles = Switch.RightChop(17);
|
|
DelimitedSourceFiles.ParseIntoArray(DebugSourceFiles, TEXT(","));
|
|
for (FString& DebugSourceFile : DebugSourceFiles)
|
|
{
|
|
// debug source files are given as relative paths from working directory, make them absolute here and normalize
|
|
DebugSourceFile = InWorkingDirectory / DebugSourceFile;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Loop(FString& CrashOutputFile)
|
|
{
|
|
UE_LOG(LogShaders, Log, TEXT("Entering job loop"));
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(Loop);
|
|
|
|
int32 NumberProcessed = 0;
|
|
|
|
while (true)
|
|
{
|
|
TArray<FShaderCompileJob> SingleJobs;
|
|
TArray<FShaderPipelineCompileJob> PipelineJobs;
|
|
TArray<FString> PipelineJobNames;
|
|
|
|
// Read & Process Input
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(ReadInput);
|
|
|
|
TUniquePtr<FArchive> InputFilePtr = TUniquePtr<FArchive>(OpenInputFile());
|
|
if (!InputFilePtr)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Record time since SCW entered the entry point
|
|
const double BatchProcessStartTime = FPlatformTime::Seconds();
|
|
GWorkerDiagnostics.BatchPreparationTime = BatchProcessStartTime - (NumberProcessed > 0 ? GLastCompileTime : GWorkerDiagnostics.EntryPointTimestamp);
|
|
GWorkerDiagnostics.BatchIndex = NumberProcessed;
|
|
|
|
UE_LOG(LogShaders, Log, TEXT("Processing shader"));
|
|
|
|
ProcessInputFromArchive(InputFilePtr.Get(), SingleJobs, PipelineJobs, PipelineJobNames);
|
|
|
|
GLastCompileTime = FPlatformTime::Seconds();
|
|
GWorkerDiagnostics.BatchProcessTime = GLastCompileTime - BatchProcessStartTime;
|
|
}
|
|
|
|
// Prepare for output
|
|
if (DisableFileWrite)
|
|
{
|
|
// write to in-memory bytestream instead for debugging purposes
|
|
TArray<uint8> MemBlock;
|
|
FMemoryWriter MemWriter(MemBlock);
|
|
WriteToOutputArchive(MemWriter, SingleJobs, PipelineJobs, PipelineJobNames);
|
|
}
|
|
else
|
|
{
|
|
// Write worker output file
|
|
{
|
|
TUniquePtr<FArchive> OutputFilePtr = TUniquePtr<FArchive>(CreateOutputArchive());
|
|
check(OutputFilePtr);
|
|
WriteToOutputArchive(*OutputFilePtr, SingleJobs, PipelineJobs, PipelineJobNames);
|
|
}
|
|
|
|
// Change the output file name to requested one
|
|
IFileManager::Get().Move(*OutputFilePath, *TempFilePath);
|
|
}
|
|
|
|
if (IsUsingXGE())
|
|
{
|
|
// To signal compilation completion, create a zero length file in the working directory.
|
|
OnXGEJobCompleted(*WorkingDirectory);
|
|
|
|
// We only do one pass per process when using XGE.
|
|
break;
|
|
}
|
|
|
|
if (IsUsingUBA())
|
|
{
|
|
#if PLATFORM_WINDOWS
|
|
using ubachar = TCHAR;
|
|
#else
|
|
using ubachar = char;
|
|
#endif
|
|
|
|
using UbaRequestNextProcessFunc = bool(uint32 prevExitCode, ubachar* outArguments, uint32 outArgumentsCapacity);
|
|
static UbaRequestNextProcessFunc* RequestNextProcess = nullptr;
|
|
if (!RequestNextProcess)
|
|
{
|
|
#if PLATFORM_WINDOWS
|
|
if (HMODULE UbaDetoursModule = GetUbaModule())
|
|
{
|
|
RequestNextProcess = (UbaRequestNextProcessFunc*)(void*)GetProcAddress(UbaDetoursModule, "UbaRequestNextProcess");
|
|
}
|
|
#elif PLATFORM_MAC
|
|
if (void* UbaDetoursHandle = dlopen("libUbaDetours.dylib", RTLD_LAZY))
|
|
{
|
|
RequestNextProcess = (UbaRequestNextProcessFunc*)(void*)dlsym(UbaDetoursHandle, "UbaRequestNextProcess");
|
|
}
|
|
#elif PLATFORM_LINUX
|
|
if (void* UbaDetoursHandle = dlopen("libUbaDetours.so", RTLD_LAZY))
|
|
{
|
|
RequestNextProcess = (UbaRequestNextProcessFunc*)(void*)dlsym(UbaDetoursHandle, "UbaRequestNextProcess");
|
|
}
|
|
#endif
|
|
if (!RequestNextProcess)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Request new process
|
|
ubachar Temp[1024];
|
|
if (!RequestNextProcess(0, Temp, 1024))
|
|
{
|
|
break; // No process available, exit loop
|
|
}
|
|
|
|
const TCHAR* Arguments;
|
|
#if PLATFORM_WINDOWS
|
|
Arguments = Temp;
|
|
#else
|
|
auto Temp2 = StringCast<TCHAR>(Temp);
|
|
Arguments = Temp2.Get();
|
|
#endif
|
|
|
|
// We got a new process, change inputs and outputs and run again
|
|
|
|
TArray<FString> Tokens;
|
|
TArray<FString> Switches;
|
|
FCommandLine::Parse(Arguments, Tokens, Switches);
|
|
if (Tokens.Num() < 5)
|
|
{
|
|
UE_LOG(LogShaders, Error, TEXT("Did not get enough arguments for reuse: %s"), Arguments);
|
|
break;
|
|
}
|
|
|
|
WorkingDirectory = Tokens[0];
|
|
InputFilename = Tokens[3];
|
|
OutputFilename = Tokens[4];
|
|
|
|
InputFilePath = WorkingDirectory / InputFilename;
|
|
OutputFilePath = WorkingDirectory / OutputFilename;
|
|
|
|
CrashOutputFile = OutputFilePath;
|
|
continue;
|
|
}
|
|
|
|
if (TimeToLive == 0)
|
|
{
|
|
UE_LOG(LogShaders, Log, TEXT("TimeToLive set to 0, exiting after single job"));
|
|
break;
|
|
}
|
|
|
|
NumberProcessed++;
|
|
if (NumberToProcess > 0 && NumberProcessed > NumberToProcess)
|
|
{
|
|
UE_LOG(LogShaders, Log, TEXT("NumJobs limit hit"));
|
|
break;
|
|
}
|
|
|
|
#if ENABLE_LOW_LEVEL_MEM_TRACKER
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UpdateStatsPerFrame);
|
|
|
|
FLowLevelMemTracker::Get().UpdateStatsPerFrame();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
UE_LOG(LogShaders, Log, TEXT("Exiting job loop"));
|
|
}
|
|
|
|
private:
|
|
const int32 ParentProcessId;
|
|
FString WorkingDirectory;
|
|
FString InputFilename;
|
|
FString OutputFilename;
|
|
|
|
FString InputFilePath;
|
|
FString OutputFilePath;
|
|
TMap<FName, uint32> FormatVersionMap;
|
|
FString TempFilePath;
|
|
|
|
/** Opens an input file, trying multiple times if necessary. */
|
|
FArchive* OpenInputFile()
|
|
{
|
|
FArchive* InputFile = nullptr;
|
|
bool bFirstOpenTry = true;
|
|
while(!InputFile && !IsEngineExitRequested())
|
|
{
|
|
// Try to open the input file that we are going to process
|
|
InputFile = IFileManager::Get().CreateFileReader(*InputFilePath,FILEREAD_Silent);
|
|
|
|
if(!InputFile && !bFirstOpenTry)
|
|
{
|
|
CheckExitConditions();
|
|
// Give up CPU time while we are waiting
|
|
FPlatformProcess::Sleep(0.01f);
|
|
}
|
|
bFirstOpenTry = false;
|
|
}
|
|
return InputFile;
|
|
}
|
|
|
|
void VerifyFormatVersions(TMap<FName, uint32>& ReceivedFormatVersionMap)
|
|
{
|
|
for (auto Pair : ReceivedFormatVersionMap)
|
|
{
|
|
auto* Found = FormatVersionMap.Find(Pair.Key);
|
|
if (Found)
|
|
{
|
|
if (Pair.Value != *Found)
|
|
{
|
|
ExitWithoutCrash(FSCWErrorCode::BadShaderFormatVersion, FString::Printf(TEXT("Mismatched shader version for format %s: Found version %u but expected %u; did you forget to build ShaderCompilerWorker?"), *Pair.Key.ToString(), *Found, Pair.Value));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ProcessInputFromArchive(FArchive* InputFilePtr, TArray<FShaderCompileJob>& OutSingleJobs, TArray<FShaderPipelineCompileJob>& OutPipelineJobs, TArray<FString>& OutPipelineNames)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(ProcessInputFromArchive);
|
|
|
|
int32 InputVersion;
|
|
*InputFilePtr << InputVersion;
|
|
if (ShaderCompileWorkerInputVersion != InputVersion)
|
|
{
|
|
ExitWithoutCrash(FSCWErrorCode::BadInputVersion, FString::Printf(TEXT("Exiting due to ShaderCompilerWorker expecting input version %d, got %d instead! Did you forget to build ShaderCompilerWorker?"), ShaderCompileWorkerInputVersion, InputVersion));
|
|
}
|
|
|
|
FString CompressionFormatString;
|
|
*InputFilePtr << CompressionFormatString;
|
|
FName CompressionFormat(*CompressionFormatString);
|
|
|
|
bool bWasCompressed = (CompressionFormat != NAME_None);
|
|
|
|
TArray<uint8> UncompressedData;
|
|
if (bWasCompressed)
|
|
{
|
|
int32 UncompressedDataSize = 0;
|
|
*InputFilePtr << UncompressedDataSize;
|
|
|
|
if (UncompressedDataSize == 0)
|
|
{
|
|
ExitWithoutCrash(FSCWErrorCode::BadInputFile, TEXT("Exiting due to bad input file to ShaderCompilerWorker (uncompressed size is 0)! Did you forget to build ShaderCompilerWorker?"));
|
|
// unreachable
|
|
return;
|
|
}
|
|
|
|
UncompressedData.SetNumUninitialized(UncompressedDataSize);
|
|
TArray<uint8> CompressedData;
|
|
*InputFilePtr << CompressedData;
|
|
if (!FCompression::UncompressMemory(CompressionFormat, UncompressedData.GetData(), UncompressedDataSize, CompressedData.GetData(), CompressedData.Num()))
|
|
{
|
|
ExitWithoutCrash(FSCWErrorCode::BadInputFile, FString::Printf(TEXT("Exiting due to bad input file to ShaderCompilerWorker (cannot uncompress with the format %s)! Did you forget to build ShaderCompilerWorker?"), *CompressionFormatString));
|
|
// unreachable
|
|
return;
|
|
}
|
|
}
|
|
FMemoryReader InputMemory(UncompressedData);
|
|
FArchive& InputFile = bWasCompressed ? InputMemory : *InputFilePtr;
|
|
|
|
TMap<FName, uint32> ReceivedFormatVersionMap;
|
|
InputFile << ReceivedFormatVersionMap;
|
|
|
|
bool bIsDebugCompile = !DebugSourceFiles.IsEmpty();
|
|
|
|
// when running a debug compile we are more permissive w.r.t. format versions.
|
|
// (instead of requiring all versions match, we just check the version for the debug compilation
|
|
// that we will execute)
|
|
if (!bIsDebugCompile)
|
|
{
|
|
VerifyFormatVersions(ReceivedFormatVersionMap);
|
|
}
|
|
|
|
// Initialize shader hash cache before reading any includes.
|
|
InitializeShaderHashCache();
|
|
|
|
// Array of string used as const TCHAR* during compilation process.
|
|
TArray<TUniquePtr<FString>> AllocatedStrings;
|
|
auto DeserializeConstTCHAR = [&AllocatedStrings](FArchive& Archive)
|
|
{
|
|
FString Name;
|
|
Archive << Name;
|
|
|
|
const TCHAR* CharName = nullptr;
|
|
if (Name.Len() != 0)
|
|
{
|
|
if (AllocatedStrings.GetSlack() == 0)
|
|
{
|
|
AllocatedStrings.Reserve(AllocatedStrings.Num() + 1024);
|
|
}
|
|
|
|
AllocatedStrings.Add(MakeUnique<FString>(Name));
|
|
CharName = **AllocatedStrings.Last();
|
|
}
|
|
return CharName;
|
|
};
|
|
|
|
// Array of string used as const ANSICHAR* during compilation process.
|
|
TArray<TUniquePtr<TArray<ANSICHAR>>> AllocatedAnsiStrings;
|
|
auto DeserializeConstANSICHAR = [&AllocatedAnsiStrings](FArchive& Archive)
|
|
{
|
|
FString Name;
|
|
Archive << Name;
|
|
|
|
const ANSICHAR* CharName = nullptr;
|
|
if (Name.Len() != 0)
|
|
{
|
|
if (AllocatedAnsiStrings.GetSlack() == 0)
|
|
{
|
|
AllocatedAnsiStrings.Reserve(AllocatedAnsiStrings.Num() + 1024);
|
|
}
|
|
|
|
TArray<ANSICHAR> AnsiString;
|
|
AnsiString.SetNumZeroed(Name.Len() + 1);
|
|
ANSICHAR* Dest = &AnsiString[0];
|
|
FCStringAnsi::Strncpy(Dest, TCHAR_TO_ANSI(*Name), Name.Len() + 1);
|
|
|
|
AllocatedAnsiStrings.Add(MakeUnique<TArray<ANSICHAR>>(AnsiString));
|
|
CharName = &(*AllocatedAnsiStrings.Last())[0];
|
|
}
|
|
return CharName;
|
|
};
|
|
|
|
// Shared environments
|
|
TArray<FShaderCompilerEnvironment> SharedEnvironments;
|
|
{
|
|
int32 NumSharedEnvironments = 0;
|
|
InputFile << NumSharedEnvironments;
|
|
SharedEnvironments.Empty(NumSharedEnvironments);
|
|
SharedEnvironments.AddDefaulted(NumSharedEnvironments);
|
|
|
|
for (int32 EnvironmentIndex = 0; EnvironmentIndex < NumSharedEnvironments; EnvironmentIndex++)
|
|
{
|
|
InputFile << SharedEnvironments[EnvironmentIndex];
|
|
}
|
|
}
|
|
|
|
// All the shader parameter structures
|
|
// Note: this is a bit more complicated, purposefully to avoid switch const TCHAR* to FString in runtime FShaderParametersMetadata.
|
|
TArray<TUniquePtr<FShaderParametersMetadata>> ParameterStructures;
|
|
{
|
|
int32 NumParameterStructures = 0;
|
|
InputFile << NumParameterStructures;
|
|
ParameterStructures.Reserve(NumParameterStructures);
|
|
|
|
for (int32 StructIndex = 0; StructIndex < NumParameterStructures; StructIndex++)
|
|
{
|
|
const TCHAR* LayoutName;
|
|
const TCHAR* StructTypeName;
|
|
const TCHAR* ShaderVariableName;
|
|
FShaderParametersMetadata::EUseCase UseCase;
|
|
const ANSICHAR* StructFileName;
|
|
int32 StructFileLine;
|
|
uint32 Size;
|
|
int32 MemberCount;
|
|
|
|
LayoutName = DeserializeConstTCHAR(InputFile);
|
|
StructTypeName = DeserializeConstTCHAR(InputFile);
|
|
ShaderVariableName = DeserializeConstTCHAR(InputFile);
|
|
InputFile << UseCase;
|
|
StructFileName = DeserializeConstANSICHAR(InputFile);
|
|
InputFile << StructFileLine;
|
|
InputFile << Size;
|
|
InputFile << MemberCount;
|
|
|
|
TArray<FShaderParametersMetadata::FMember> Members;
|
|
Members.Reserve(MemberCount);
|
|
|
|
for (int32 MemberIndex = 0; MemberIndex < MemberCount; MemberIndex++)
|
|
{
|
|
const TCHAR* Name;
|
|
const TCHAR* ShaderType;
|
|
int32 FileLine;
|
|
uint32 Offset;
|
|
uint8 BaseType;
|
|
uint8 PrecisionModifier;
|
|
uint32 NumRows;
|
|
uint32 NumColumns;
|
|
uint32 NumElements;
|
|
int32 StructMetadataIndex;
|
|
|
|
static_assert(sizeof(BaseType) == sizeof(EUniformBufferBaseType), "Cast failure.");
|
|
static_assert(sizeof(PrecisionModifier) == sizeof(EShaderPrecisionModifier::Type), "Cast failure.");
|
|
|
|
Name = DeserializeConstTCHAR(InputFile);
|
|
ShaderType = DeserializeConstTCHAR(InputFile);
|
|
InputFile << FileLine;
|
|
InputFile << Offset;
|
|
InputFile << BaseType;
|
|
InputFile << PrecisionModifier;
|
|
InputFile << NumRows;
|
|
InputFile << NumColumns;
|
|
InputFile << NumElements;
|
|
InputFile << StructMetadataIndex;
|
|
|
|
if (ShaderType == nullptr)
|
|
{
|
|
ShaderType = TEXT("");
|
|
}
|
|
|
|
const FShaderParametersMetadata* StructMetadata = nullptr;
|
|
if (StructMetadataIndex != INDEX_NONE)
|
|
{
|
|
StructMetadata = ParameterStructures[StructMetadataIndex].Get();
|
|
}
|
|
|
|
FShaderParametersMetadata::FMember Member(
|
|
Name,
|
|
ShaderType,
|
|
FileLine,
|
|
Offset,
|
|
EUniformBufferBaseType(BaseType),
|
|
EShaderPrecisionModifier::Type(PrecisionModifier),
|
|
NumRows,
|
|
NumColumns,
|
|
NumElements,
|
|
StructMetadata);
|
|
Members.Add(Member);
|
|
}
|
|
|
|
ParameterStructures.Add(MakeUnique<FShaderParametersMetadata>(
|
|
UseCase,
|
|
EUniformBufferBindingFlags::Shader,
|
|
/* InLayoutName = */ LayoutName,
|
|
/* InStructTypeName = */ StructTypeName,
|
|
/* InShaderVariableName = */ ShaderVariableName,
|
|
/* InStaticSlotName = */ nullptr,
|
|
StructFileName,
|
|
StructFileLine,
|
|
Size,
|
|
Members,
|
|
/* bCompleteInitialization = */ true));
|
|
}
|
|
}
|
|
|
|
GNumProcessedJobs = 0;
|
|
|
|
auto SetupDebugCompile = [this, &ReceivedFormatVersionMap](FShaderCompileJob& Job, const TCHAR* SourcePath, const TCHAR* SecondarySourcePath = nullptr)
|
|
{
|
|
Job.Input.DebugInfoFlags &= EShaderDebugInfoFlags::CompileFromDebugUSF;
|
|
|
|
checkf(ReceivedFormatVersionMap[Job.Input.ShaderFormat] == FormatVersionMap[Job.Input.ShaderFormat],
|
|
TEXT("Format mismatch for shader format %s; ensure SDK/tools for this platform match the build where this debug compile input was generated.\n")
|
|
TEXT("This check is skippable, with the caveat that this debug compilation may fail for unexpected reasons and/or results may be different."),
|
|
*Job.Input.ShaderFormat.ToString());
|
|
|
|
Job.Input.DebugInfoFlags &= EShaderDebugInfoFlags::CompileFromDebugUSF;
|
|
FString DebugSource;
|
|
bool bSuccess = FFileHelper::LoadFileToString(DebugSource, SourcePath);
|
|
|
|
if (bSuccess)
|
|
{
|
|
// strip comments from source when loading from a debug source file. some backends don't handle the comments that the debug dump inserts properly.
|
|
FAnsiString Stripped;
|
|
// Since we are directly assigning the stripped result to an FShaderSource object which automatically appends SIMD patting as part of the Set function,
|
|
// we don't need to also add padding during the convert-and-strip operation.
|
|
ShaderConvertAndStripComments(DebugSource, Stripped.GetCharArray(), EConvertAndStripFlags::NoSimdPadding);
|
|
Job.PreprocessOutput.EditSource() = MoveTemp(Stripped);
|
|
}
|
|
|
|
if (SecondarySourcePath != nullptr)
|
|
{
|
|
FString SecondaryDebugSource;
|
|
bSuccess = FFileHelper::LoadFileToString(SecondaryDebugSource, SecondarySourcePath);
|
|
if (bSuccess)
|
|
{
|
|
// strip comments from source when loading from a debug source file. some backends don't handle the comments that the debug dump inserts properly.
|
|
FAnsiString SecondaryStripped;
|
|
// Since we are directly assigning the stripped result to an FShaderSource object which automatically appends SIMD patting as part of the Set function,
|
|
// we don't need to also add padding during the convert-and-strip operation.
|
|
ShaderConvertAndStripComments(SecondaryDebugSource, SecondaryStripped.GetCharArray(), EConvertAndStripFlags::NoSimdPadding);
|
|
Job.SecondaryPreprocessOutput->EditSource() = MoveTemp(SecondaryStripped);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Individual jobs
|
|
{
|
|
int32 SingleJobHeader = ShaderCompileWorkerSingleJobHeader;
|
|
InputFile << SingleJobHeader;
|
|
if (ShaderCompileWorkerSingleJobHeader != SingleJobHeader)
|
|
{
|
|
ExitWithoutCrash(FSCWErrorCode::BadSingleJobHeader, FString::Printf(TEXT("Exiting due to ShaderCompilerWorker expecting job header %d, got %d instead! Did you forget to build ShaderCompilerWorker?"), ShaderCompileWorkerSingleJobHeader, SingleJobHeader));
|
|
}
|
|
|
|
int32 NumSingleJobs = 0;
|
|
InputFile << NumSingleJobs;
|
|
|
|
FlushShaderFileCache();
|
|
|
|
OutSingleJobs.Reserve(NumSingleJobs);
|
|
|
|
for (int32 BatchIndex = 0; BatchIndex < NumSingleJobs; BatchIndex++)
|
|
{
|
|
FShaderCompileJob& Job = OutSingleJobs.AddDefaulted_GetRef();
|
|
// Deserialize the job's inputs.
|
|
Job.SerializeWorkerInput(InputFile);
|
|
Job.Input.DeserializeSharedInputs(InputFile, SharedEnvironments, ParameterStructures);
|
|
if (bIsDebugCompile)
|
|
{
|
|
bool bHasSecondary = Job.SecondaryPreprocessOutput.IsValid();
|
|
// this should only be used in debug compile mode; in this case we expect either a single job or a single pipeline job
|
|
if (bHasSecondary)
|
|
{
|
|
// jobs with secondary compiles should have two source files
|
|
check(DebugSourceFiles.Num() == 2 && NumSingleJobs == 1);
|
|
SetupDebugCompile(Job, *DebugSourceFiles[0], *DebugSourceFiles[1]);
|
|
}
|
|
else
|
|
{
|
|
// normal jobs should just have a single source file
|
|
check(DebugSourceFiles.Num() == 1 && NumSingleJobs == 1);
|
|
SetupDebugCompile(Job, *DebugSourceFiles[0]);
|
|
}
|
|
}
|
|
|
|
// Process the job.
|
|
CompileShader(GetShaderFormats(), Job, WorkingDirectory, &GNumProcessedJobs);
|
|
if (bIsDebugCompile)
|
|
{
|
|
// if we're in debug compile mode and we have a single job, skip the pipeline job section below so
|
|
// we can check that in either debug compile case we have the correct number of debug source files.
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shader pipeline jobs
|
|
{
|
|
int32 PipelineJobHeader = ShaderCompileWorkerPipelineJobHeader;
|
|
InputFile << PipelineJobHeader;
|
|
if (ShaderCompileWorkerPipelineJobHeader != PipelineJobHeader)
|
|
{
|
|
ExitWithoutCrash(FSCWErrorCode::BadPipelineJobHeader, FString::Printf(TEXT("Exiting due to ShaderCompilerWorker expecting pipeline job header %d, got %d instead! Did you forget to build ShaderCompilerWorker?"), ShaderCompileWorkerSingleJobHeader, PipelineJobHeader));
|
|
}
|
|
|
|
int32 NumPipelines = 0;
|
|
InputFile << NumPipelines;
|
|
|
|
OutPipelineNames.Reserve(NumPipelines);
|
|
OutPipelineJobs.Reserve(NumPipelines);
|
|
|
|
// ensure that we are only compiling a single pipeline if debug source files were specified
|
|
check(!bIsDebugCompile || NumPipelines == 1);
|
|
|
|
for (int32 Index = 0; Index < NumPipelines; ++Index)
|
|
{
|
|
FString& PipelineName = OutPipelineNames.AddDefaulted_GetRef();
|
|
InputFile << PipelineName;
|
|
|
|
int32 NumStages = 0;
|
|
InputFile << NumStages;
|
|
FShaderPipelineCompileJob& PipelineJob = OutPipelineJobs.Emplace_GetRef(NumStages);
|
|
|
|
// if debug source files are specified ensure the number of files matches the number of pipeline stages
|
|
check(!bIsDebugCompile || DebugSourceFiles.Num() == NumStages);
|
|
|
|
for (int32 StageIndex = 0; StageIndex < NumStages; ++StageIndex)
|
|
{
|
|
// Deserialize the job's inputs.
|
|
FShaderCompileJob* Job = PipelineJob.StageJobs[StageIndex]->GetSingleShaderJob();
|
|
Job->SerializeWorkerInput(InputFile);
|
|
Job->Input.DeserializeSharedInputs(InputFile, SharedEnvironments, ParameterStructures);
|
|
|
|
if (bIsDebugCompile)
|
|
{
|
|
SetupDebugCompile(*Job, *DebugSourceFiles[StageIndex]);
|
|
}
|
|
|
|
// SCW doesn't run DDPI, GShaderHasCache Initialize is run at start with no knowledge of the CustomPlatforms
|
|
// CustomPlatforms are known when we parse the WorkerInput so we populate the Directory here
|
|
if (IsCustomPlatform((EShaderPlatform)Job->Input.Target.Platform))
|
|
{
|
|
const EShaderPlatform ShaderPlatform = ShaderFormatNameToShaderPlatform(Job->Input.ShaderFormat);
|
|
UpdateIncludeDirectoryForPreviewPlatform((EShaderPlatform)Job->Input.Target.Platform, ShaderPlatform);
|
|
}
|
|
}
|
|
|
|
CompileShaderPipeline(GetShaderFormats(), &PipelineJob, WorkingDirectory, &GNumProcessedJobs);
|
|
}
|
|
}
|
|
}
|
|
|
|
FArchive* CreateOutputArchive()
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(CreateOutputArchive);
|
|
|
|
FArchive* OutputFilePtr = nullptr;
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
bool bResult = false;
|
|
|
|
// It seems XGE does not support deleting files.
|
|
// Don't delete the input file if we are running under Incredibuild (or if the cmdline args explicitly told us to keep it).
|
|
// In xml mode, we signal completion by creating a zero byte "Success" file after the output file has been fully written.
|
|
// In intercept mode, completion is signaled by this process terminating.
|
|
// For UBA we can't delete the file when running remotely because there might be a crash or disconnect happening before result is sent back and then we can't retry
|
|
if (!IsUsingXGE() && !KeepInput && !IsUsingUBA())
|
|
{
|
|
do
|
|
{
|
|
// Remove the input file so that it won't get processed more than once
|
|
bResult = IFileManager::Get().Delete(*InputFilePath);
|
|
}
|
|
while (!bResult && (FPlatformTime::Seconds() - StartTime < 2));
|
|
|
|
if (!bResult)
|
|
{
|
|
ExitWithoutCrash(FSCWErrorCode::CantDeleteInputFile, FString::Printf(TEXT("Couldn't delete input file %s, is it readonly?"), *InputFilePath));
|
|
}
|
|
}
|
|
|
|
// To make sure that the process waiting for results won't read unfinished output file,
|
|
// we use a temp file name during compilation.
|
|
do
|
|
{
|
|
FGuid Guid;
|
|
FPlatformMisc::CreateGuid(Guid);
|
|
TempFilePath = WorkingDirectory + Guid.ToString() + TEXT(".tmp");
|
|
} while (IFileManager::Get().FileSize(*TempFilePath) != INDEX_NONE);
|
|
|
|
const double StartTime2 = FPlatformTime::Seconds();
|
|
|
|
do
|
|
{
|
|
// Create the output file.
|
|
OutputFilePtr = IFileManager::Get().CreateFileWriter(*TempFilePath,FILEWRITE_EvenIfReadOnly);
|
|
}
|
|
while (!OutputFilePtr && (FPlatformTime::Seconds() - StartTime2 < 2));
|
|
|
|
if (!OutputFilePtr)
|
|
{
|
|
ExitWithoutCrash(FSCWErrorCode::CantSaveOutputFile, FString::Printf(TEXT("Couldn't save output file %s"), *TempFilePath));
|
|
}
|
|
|
|
return OutputFilePtr;
|
|
}
|
|
|
|
void WriteToOutputArchive(FArchive& OutputFile, TArray<FShaderCompileJob>& SingleJobs, TArray<FShaderPipelineCompileJob>& PipelineJobs, TArray<FString>& PipelineNames)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(WriteToOutputArchive);
|
|
|
|
int64 FileSizePosition = 0;
|
|
if (FSCWErrorCode::IsSet())
|
|
{
|
|
GWorkerDiagnostics.ErrorCode = FSCWErrorCode::Get();
|
|
FileSizePosition = WriteOutputFileHeader(OutputFile, 0, nullptr, FSCWErrorCode::GetInfo().Len(), *FSCWErrorCode::GetInfo());
|
|
}
|
|
else
|
|
{
|
|
GWorkerDiagnostics.ErrorCode = FSCWErrorCode::Success;
|
|
FileSizePosition = WriteOutputFileHeader(OutputFile, 0, nullptr, 0, nullptr);
|
|
}
|
|
|
|
{
|
|
int32 SingleJobHeader = ShaderCompileWorkerSingleJobHeader;
|
|
OutputFile << SingleJobHeader;
|
|
|
|
int32 NumBatches = SingleJobs.Num();
|
|
OutputFile << NumBatches;
|
|
|
|
for (int32 JobIndex = 0; JobIndex < SingleJobs.Num(); JobIndex++)
|
|
{
|
|
SingleJobs[JobIndex].SerializeWorkerOutput(OutputFile);
|
|
UpdateFileSize(OutputFile, FileSizePosition);
|
|
}
|
|
}
|
|
|
|
{
|
|
int32 PipelineJobHeader = ShaderCompileWorkerPipelineJobHeader;
|
|
OutputFile << PipelineJobHeader;
|
|
int32 NumBatches = PipelineJobs.Num();
|
|
OutputFile << NumBatches;
|
|
|
|
for (int32 JobIndex = 0; JobIndex < PipelineJobs.Num(); JobIndex++)
|
|
{
|
|
auto& PipelineJob = PipelineJobs[JobIndex];
|
|
OutputFile << PipelineNames[JobIndex];
|
|
bool bSucceeded = (bool)PipelineJob.bSucceeded;
|
|
OutputFile << bSucceeded;
|
|
int32 NumStageJobs = PipelineJob.StageJobs.Num();
|
|
OutputFile << NumStageJobs;
|
|
|
|
for (int32 Index = 0; Index < NumStageJobs; ++Index)
|
|
{
|
|
PipelineJob.StageJobs[Index]->SerializeWorkerOutput(OutputFile);
|
|
UpdateFileSize(OutputFile, FileSizePosition);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Called in the idle loop, checks for conditions under which the helper should exit */
|
|
void CheckExitConditions()
|
|
{
|
|
if (!InputFilename.Contains(TEXT("Only")))
|
|
{
|
|
FPlatformMisc::RequestExit(false, TEXT("ShaderCompileWorker - InputFilename did not contain 'Only', exiting after one job."));
|
|
}
|
|
|
|
#if PLATFORM_MAC || PLATFORM_LINUX
|
|
if (!FPlatformMisc::IsDebuggerPresent() && ParentProcessId > 0)
|
|
{
|
|
// If the parent process is no longer running, exit
|
|
if (!FPlatformProcess::IsApplicationRunning(ParentProcessId))
|
|
{
|
|
FString FilePath = FString(WorkingDirectory) + InputFilename;
|
|
checkf(IFileManager::Get().FileSize(*FilePath) == INDEX_NONE, TEXT("Exiting due to the parent process no longer running! FilePath=%s"), *FilePath);
|
|
FPlatformMisc::RequestExit(false, TEXT("ShaderCompileWorker - Parent process no longer running, exiting"));
|
|
}
|
|
}
|
|
|
|
const double CurrentTime = FPlatformTime::Seconds();
|
|
if (CurrentTime - GLastCompileTime > TimeToLive)
|
|
{
|
|
UE_LOG(LogShaders, Log, TEXT("No jobs found for %f seconds, exiting"), (float)(CurrentTime - GLastCompileTime));
|
|
FPlatformMisc::RequestExit(false, TEXT("ShaderCompileWorker - No jobs found for seconds, exiting"));
|
|
}
|
|
#else
|
|
// Don't do these if the debugger is present
|
|
//@todo - don't do these if Unreal is being debugged either
|
|
if (!IsDebuggerPresent())
|
|
{
|
|
if (ParentProcessId > 0)
|
|
{
|
|
FString FilePath = FString(WorkingDirectory) + InputFilename;
|
|
|
|
bool bParentStillRunning = true;
|
|
HANDLE ParentProcessHandle = OpenProcess(SYNCHRONIZE, false, ParentProcessId);
|
|
// If we couldn't open the process then it is no longer running, exit
|
|
if (ParentProcessHandle == nullptr)
|
|
{
|
|
checkf(IFileManager::Get().FileSize(*FilePath) == INDEX_NONE, TEXT("Exiting due to OpenProcess(ParentProcessId) failing! FilePath=%s"), *FilePath);
|
|
FPlatformMisc::RequestExit(false, TEXT("ShaderCompileWorker - Couldn't OpenProcess, Parent process no longer running, exiting"));
|
|
}
|
|
else
|
|
{
|
|
// If we did open the process, that doesn't mean it is still running
|
|
// The process object stays alive as long as there are handles to it
|
|
// We need to check if the process has signaled, which indicates that it has exited
|
|
uint32 WaitResult = WaitForSingleObject(ParentProcessHandle, 0);
|
|
if (WaitResult != WAIT_TIMEOUT)
|
|
{
|
|
checkf(IFileManager::Get().FileSize(*FilePath) == INDEX_NONE, TEXT("Exiting due to WaitForSingleObject(ParentProcessHandle) signaling! FilePath=%s"), *FilePath);
|
|
FPlatformMisc::RequestExit(false, TEXT("ShaderCompileWorker - WaitForSingleObject signaled, Parent process no longer running, exiting"));
|
|
}
|
|
CloseHandle(ParentProcessHandle);
|
|
}
|
|
}
|
|
|
|
const double CurrentTime = FPlatformTime::Seconds();
|
|
// If we have been idle for 20 seconds then exit
|
|
if (CurrentTime - GLastCompileTime > TimeToLive)
|
|
{
|
|
UE_LOG(LogShaders, Log, TEXT("No jobs found for %f seconds, exiting"), (float)(CurrentTime - GLastCompileTime));
|
|
FPlatformMisc::RequestExit(false, TEXT("ShaderCompileWorker - No jobs found for %f seconds, exiting"));
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Main entrypoint, guarded by a try ... except.
|
|
* This expects 4 parameters:
|
|
* The image path and name
|
|
* The working directory path, which has to be unique to the instigating process and thread.
|
|
* The parent process Id
|
|
* The thread Id corresponding to this worker
|
|
*/
|
|
static int32 GuardedMain(int32 argc, TCHAR* argv[], FString& CrashOutputFile)
|
|
{
|
|
FString ExtraCmdLine = TEXT("-NOPACKAGECACHE -ReduceThreadUsage -cpuprofilertrace -nocrashreports");
|
|
|
|
// When executing tasks remotely through XGE, enumerating files requires tcp/ip round-trips with
|
|
// the initiator, which can slow down engine initialization quite drastically.
|
|
// The idea here is to save the Ini and Modules manager state and reuse them on the workers
|
|
// to avoid all those directory enumeration during engine init.
|
|
FString IniBootstrapFilename;
|
|
FString ModulesBootstrapFilename;
|
|
|
|
// Register out-of-memory delegate to report error code on exit
|
|
FCoreDelegates::GetOutOfMemoryDelegate().AddLambda(
|
|
[]()
|
|
{
|
|
FSCWErrorCode::Report(FSCWErrorCode::OutOfMemory);
|
|
}
|
|
);
|
|
|
|
if (IsUsingXGE())
|
|
{
|
|
// Tie the bootstrap filenames to the xge job id to refresh bootstraps state every time a new build starts
|
|
// This allows the ini/modules and shadercompilerworker binaries to change between builds.
|
|
FGuid XGJobID;
|
|
if (FGuid::Parse(FPlatformMisc::GetEnvironmentVariable(TEXT("xgJobID")), XGJobID))
|
|
{
|
|
FString XGJobIDString = XGJobID.ToString(EGuidFormats::DigitsWithHyphens);
|
|
IniBootstrapFilename = FString::Printf(TEXT("%s/Bootstrap-%s.inis"), argv[1], *XGJobIDString);
|
|
ModulesBootstrapFilename = FString::Printf(TEXT("%s/Bootstrap-%s.modules"), argv[1], *XGJobIDString);
|
|
|
|
ExtraCmdLine.Appendf(TEXT(" -IniBootstrap=\"%s\" -ModulesBootstrap=\"%s\""), *IniBootstrapFilename, *ModulesBootstrapFilename);
|
|
|
|
// Use Windows API directly because required CreateFile flags are not supported by our current OS abstraction
|
|
#if PLATFORM_WINDOWS
|
|
// This is advantageous to have only a single worker do the init work instead of having all workers
|
|
// do a stampede of the initiator's machine all trying to enumerate directories at the same time.
|
|
// I've seen incoming TCP connections going through the roof (350 connections for 150 virtual CPUs)
|
|
// coming from workers doing all the same directory enumerations.
|
|
// This is not strictly required, but will improve performance when successful.
|
|
// Most likely a local worker will win the race and do a fast init.
|
|
FString MutexFilename = FString::Printf(TEXT("%s/Bootstrap-%s.mutex"), argv[1], *XGJobIDString);
|
|
|
|
// We need to implement a mutex scheme through a file for it to work with XGE's file virtualization layer.
|
|
// The first process to successfully create this file will have the honor of doing the complete initialization.
|
|
HANDLE MutexHandle =
|
|
CreateFileW(
|
|
*MutexFilename,
|
|
GENERIC_WRITE,
|
|
0,
|
|
nullptr,
|
|
CREATE_NEW,
|
|
FILE_ATTRIBUTE_NORMAL,
|
|
nullptr);
|
|
|
|
if (MutexHandle != INVALID_HANDLE_VALUE)
|
|
{
|
|
// We won the race, proceed to initialization.
|
|
CloseHandle(MutexHandle);
|
|
}
|
|
else
|
|
{
|
|
// Wait until the race winner writes the last bootstrap file
|
|
// Due to a bug in XGE, some workers might never see the new file appear, we must proceed after some timeout value.
|
|
for (int32 Index = 0; Index < 10 && !FPaths::FileExists(ModulesBootstrapFilename); ++Index)
|
|
{
|
|
Sleep(100);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
GEngineLoop.PreInit(argc, argv, *ExtraCmdLine);
|
|
#if DEBUG_USING_CONSOLE
|
|
GLogConsole->Show( true );
|
|
#endif
|
|
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(Main);
|
|
|
|
auto AtomicSave =
|
|
[](const FString& Filename, TFunctionRef<void (const FString& TmpFile)> SaveFunction)
|
|
{
|
|
if (!Filename.IsEmpty() && !FPaths::FileExists(Filename))
|
|
{
|
|
// Use a tmp file for atomic publication and avoid reading incomplete state from other workers
|
|
FString TmpFile = FString::Printf(TEXT("%s-%s"), *Filename, *FGuid::NewGuid().ToString());
|
|
SaveFunction(TmpFile);
|
|
const bool bReplace = false;
|
|
const bool bDoNotRetryOrError = true;
|
|
const bool bEvenIfReadOnly = false;
|
|
const bool bAttributes = false;
|
|
IFileManager::Get().Move(*Filename, *TmpFile, bReplace, bEvenIfReadOnly, bAttributes, bDoNotRetryOrError);
|
|
// In case this process lost the race and wasn't able to move the file, discard the tmp file.
|
|
IFileManager::Get().Delete(*TmpFile);
|
|
}
|
|
};
|
|
|
|
AtomicSave(IniBootstrapFilename, [](const FString& TmpFile) { GConfig->SaveCurrentStateForBootstrap(*TmpFile); });
|
|
AtomicSave(ModulesBootstrapFilename, [](const FString& TmpFile) { FModuleManager::Get().SaveCurrentStateForBootstrap(*TmpFile); });
|
|
|
|
// Explicitly load ShaderPreprocessor module so it will run its initialization step
|
|
FModuleManager::LoadModuleChecked<IModuleInterface>(TEXT("ShaderPreprocessor"));
|
|
|
|
// We just enumerate the shader formats here for debugging.
|
|
const TArray<const class IShaderFormat*>& ShaderFormats = GetShaderFormats();
|
|
check(ShaderFormats.Num());
|
|
TMap<FName, uint32> FormatVersionMap;
|
|
for (int32 Index = 0; Index < ShaderFormats.Num(); Index++)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(ShaderFormat);
|
|
|
|
TArray<FName> OutFormats;
|
|
ShaderFormats[Index]->GetSupportedFormats(OutFormats);
|
|
check(OutFormats.Num());
|
|
for (int32 InnerIndex = 0; InnerIndex < OutFormats.Num(); InnerIndex++)
|
|
{
|
|
UE_LOG(LogShaders, Display, TEXT("Available Shader Format %s"), *OutFormats[InnerIndex].ToString());
|
|
uint32 Version = ShaderFormats[Index]->GetVersion(OutFormats[InnerIndex]);
|
|
FormatVersionMap.Add(OutFormats[InnerIndex], Version);
|
|
}
|
|
}
|
|
|
|
GLastCompileTime = FPlatformTime::Seconds();
|
|
|
|
#if PLATFORM_WINDOWS
|
|
//@todo - would be nice to change application name or description to have the ThreadId in it for debugging purposes
|
|
SetConsoleTitle(argv[3]);
|
|
#endif
|
|
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FWorkLoop);
|
|
|
|
FWorkLoop WorkLoop(argv[2], argv[1], argv[4], argv[5], FormatVersionMap);
|
|
WorkLoop.Loop(CrashOutputFile);
|
|
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int32 GuardedMainWrapper(int32 ArgC, TCHAR* ArgV[], FString& CrashOutputFile)
|
|
{
|
|
FTaskTagScope Scope(ETaskTag::EGameThread);
|
|
// We need to know whether we are using XGE now, in case an exception
|
|
// is thrown before we parse the command line inside GuardedMain.
|
|
if ((ArgC > 6) && FCString::Strcmp(ArgV[6], TEXT("-xge_int")) == 0)
|
|
{
|
|
GXGEMode = EXGEMode::Intercept;
|
|
}
|
|
else if ((ArgC > 6) && FCString::Strcmp(ArgV[6], TEXT("-xge_xml")) == 0)
|
|
{
|
|
GXGEMode = EXGEMode::Xml;
|
|
}
|
|
else
|
|
{
|
|
GXGEMode = EXGEMode::None;
|
|
}
|
|
|
|
int32 ReturnCode = 0;
|
|
#if PLATFORM_WINDOWS
|
|
if (FPlatformMisc::IsDebuggerPresent())
|
|
#endif
|
|
{
|
|
ReturnCode = GuardedMain(ArgC, ArgV, CrashOutputFile);
|
|
}
|
|
#if PLATFORM_WINDOWS
|
|
else
|
|
{
|
|
// Don't want 32 dialogs popping up when SCW fails
|
|
GUseCrashReportClient = false;
|
|
FString ExceptionMsg;
|
|
FString ExceptionCallStack;
|
|
__try
|
|
{
|
|
GIsGuarded = 1;
|
|
ReturnCode = GuardedMain(ArgC, ArgV, CrashOutputFile);
|
|
GIsGuarded = 0;
|
|
}
|
|
__except(HandleShaderCompileException(GetExceptionInformation(), ExceptionMsg, ExceptionCallStack))
|
|
{
|
|
// Put app into critical error mode to allow dumping logs from memory to disk
|
|
GIsCriticalError = true;
|
|
|
|
if (TUniquePtr<FArchive> OutputFile = TUniquePtr<FArchive>(IFileManager::Get().CreateFileWriter(*CrashOutputFile, FILEWRITE_EvenIfReadOnly)))
|
|
{
|
|
if (GWorkerDiagnostics.ErrorCode == FSCWErrorCode::Success)
|
|
{
|
|
if (FSCWErrorCode::IsSet())
|
|
{
|
|
// Use the value set inside the shader format
|
|
GWorkerDiagnostics.ErrorCode = FSCWErrorCode::Get();
|
|
}
|
|
else
|
|
{
|
|
// Something else failed before we could set the error code, so mark it as a General Crash
|
|
GWorkerDiagnostics.ErrorCode = FSCWErrorCode::GeneralCrash;
|
|
}
|
|
}
|
|
|
|
int64 FileSizePosition = WriteOutputFileHeader(*OutputFile, ExceptionCallStack.Len(), *ExceptionCallStack, ExceptionMsg.Len(), *ExceptionMsg);
|
|
|
|
int32 NumBatches = 0;
|
|
*OutputFile << NumBatches;
|
|
*OutputFile << NumBatches;
|
|
|
|
UpdateFileSize(*OutputFile, FileSizePosition);
|
|
}
|
|
|
|
if (IsUsingXGE())
|
|
{
|
|
ReturnCode = 1;
|
|
OnXGEJobCompleted(ArgV[1]);
|
|
}
|
|
else if (GetUbaModule())
|
|
{
|
|
ReturnCode = GWorkerDiagnostics.ErrorCode;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
FEngineLoop::AppPreExit();
|
|
FModuleManager::Get().UnloadModulesAtShutdown();
|
|
FEngineLoop::AppExit();
|
|
|
|
return ReturnCode;
|
|
}
|
|
|
|
IMPLEMENT_APPLICATION(ShaderCompileWorker, "ShaderCompileWorker")
|
|
|
|
|
|
/**
|
|
* Application entry point
|
|
*
|
|
* @param ArgC Command-line argument count
|
|
* @param ArgV Argument strings
|
|
*/
|
|
|
|
INT32_MAIN_INT32_ARGC_TCHAR_ARGV()
|
|
{
|
|
// Record timestamp when entry point is entered
|
|
GWorkerDiagnostics.EntryPointTimestamp = FPlatformTime::Seconds();
|
|
|
|
// Redirect for special XGE utilities...
|
|
extern bool XGEMain(int ArgC, TCHAR* ArgV[], int32& ReturnCode);
|
|
{
|
|
int32 ReturnCode;
|
|
if (XGEMain(ArgC, ArgV, ReturnCode))
|
|
{
|
|
return ReturnCode;
|
|
}
|
|
}
|
|
|
|
FString OutputFilePath;
|
|
|
|
if (ArgC < 6)
|
|
{
|
|
printf("ShaderCompileWorker (v%d) is called by UnrealEditor, it requires specific command line arguments.\n", ShaderCompileWorkerOutputVersion);
|
|
return -1;
|
|
}
|
|
|
|
// Game exe can pass any number of parameters through with appGetSubprocessCommandline
|
|
// so just make sure we have at least the minimum number of parameters.
|
|
check(ArgC >= 6);
|
|
|
|
OutputFilePath = ArgV[1];
|
|
OutputFilePath += ArgV[5];
|
|
|
|
return GuardedMainWrapper(ArgC, ArgV, OutputFilePath);
|
|
}
|