Files
UnrealEngine/Engine/Source/Developer/Apple/MetalShaderFormat/Private/MetalShaderFormat.cpp
2025-05-18 13:04:45 +08:00

1200 lines
42 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MetalShaderFormat.h"
#include "Modules/ModuleInterface.h"
#include "Modules/ModuleManager.h"
#include "Interfaces/IShaderFormat.h"
#include "Interfaces/IShaderFormatModule.h"
#include "ShaderCore.h"
#include "ShaderCodeArchive.h"
#include "ShaderPreprocessTypes.h"
#include "hlslcc.h"
#include "MetalShaderResources.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformFileManager.h"
#include "Serialization/Archive.h"
#include "Misc/ConfigCacheIni.h"
#include "MetalBackend.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "FileUtilities/ZipArchiveWriter.h"
#include "MetalShaderCompiler.h"
#include "DataDrivenShaderPlatformInfo.h"
DEFINE_LOG_CATEGORY(LogMetalCompilerSetup)
DEFINE_LOG_CATEGORY(LogMetalShaderCompiler)
#define WRITE_METAL_SHADER_SOURCE_ARCHIVE 0
// Set this cvar to get additional logging information about Metal toolchain setup.
static int32 GCheckCompilerToolChainSetup = 0;
static FAutoConsoleVariableRef CVarCheckCompilerToolChainSetup(
TEXT("Metal.CheckCompilerToolChainSetup"),
GCheckCompilerToolChainSetup,
TEXT("Should we check the Metal Compiler ToolChain Setup."),
ECVF_Default
);
extern bool PreprocessMetalShader(const FShaderCompilerInput& Input, const FShaderCompilerEnvironment& Environment, FShaderPreprocessOutput& PreprocessOutput);
extern void CompileMetalShader(const FShaderCompilerInput& Input, const FShaderPreprocessOutput& InPreprocessOutput, FShaderCompilerOutput& Output);
extern void OutputMetalDebugData(const FShaderCompilerInput& Input, const FShaderPreprocessOutput& PreprocessOutput, const FShaderCompilerOutput& Output);
extern bool StripShader_Metal(TArray<uint8>& Code, class FString const& DebugPath, bool const bNative);
extern uint64 AppendShader_Metal(class FString const& ArchivePath, const FSHAHash& Hash, TArray<uint8>& Code);
extern bool FinalizeLibrary_Metal(class FName const& Format, class FString const& ArchivePath, class FString const& LibraryPath, TSet<uint64> const& Shaders, class FString const& DebugOutputDir);
/** Version for shader format, this becomes part of the DDC key. */
static const FGuid UE_SHADER_METAL_VER = FGuid("B0DC25EF-C34D-437A-94E5-5E5146AF1B9A");
class FMetalShaderFormat : public UE::ShaderCompilerCommon::FBaseShaderFormat
{
public:
FMetalShaderFormat()
{
FMetalCompilerToolchain::CreateAndInit();
}
virtual ~FMetalShaderFormat()
{
FMetalCompilerToolchain::Destroy();
}
virtual uint32 GetVersion(FName Format) const override final
{
// If there's no compiler on this machine, this is irrelevant so just return 0
if (!FMetalCompilerToolchain::Get()->IsCompilerAvailable())
{
return 0;
}
bool bUseFullMetalVersion = false;
EShaderPlatform ShaderPlatform = FMetalCompilerToolchain::MetalShaderFormatToLegacyShaderPlatform(Format);
if (FMetalCompilerToolchain::IsMobile(ShaderPlatform))
{
GConfig->GetBool(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("UseFullMetalVersionInShaderVersion"), bUseFullMetalVersion, GEngineIni);
}
else
{
GConfig->GetBool(TEXT("/Script/MacTargetPlatform.MacTargetSettings"), TEXT("UseFullMetalVersionInShaderVersion"), bUseFullMetalVersion, GEngineIni);
}
FMetalCompilerToolchain::PackedVersion MetalVersionNumber = FMetalCompilerToolchain::Get()->GetCompilerVersion(ShaderPlatform);
uint16 HashValue = MetalVersionNumber.Major;
if (bUseFullMetalVersion)
{
// Use entire Metal version if .ini settings instruct us to do so (e.g. p4 dev build)
HashValue ^= MetalVersionNumber.Minor;
HashValue ^= MetalVersionNumber.Patch;
}
else
{
// Only use Metal major version (e.g. Installed build)
// Since Metal minor/patch version changes every Xcode minor version, we don't want users to rebuild shaders for every minor version update
}
uint32 Result = GetTypeHash(HashValue);
Result = HashCombine(Result, GetTypeHash(HLSLCC_VersionMinor));
Result = HashCombine(Result, GetTypeHash(UE_SHADER_METAL_VER));
return Result;
}
virtual void GetSupportedFormats(TArray<FName>& OutFormats) const override final
{
OutFormats.Add(NAME_SF_METAL_ES3_1_IOS);
OutFormats.Add(NAME_SF_METAL_SM5_IOS);
OutFormats.Add(NAME_SF_METAL_ES3_1_TVOS);
OutFormats.Add(NAME_SF_METAL_SM5_TVOS);
OutFormats.Add(NAME_SF_METAL_SM5);
OutFormats.Add(NAME_SF_METAL_SM6);
OutFormats.Add(NAME_SF_METAL_SIM);
OutFormats.Add(NAME_SF_METAL_ES3_1);
}
void CheckShaderFormat(FName Format) const
{
check(Format == NAME_SF_METAL_ES3_1_IOS
|| Format == NAME_SF_METAL_SM5_IOS
|| Format == NAME_SF_METAL_ES3_1_TVOS
|| Format == NAME_SF_METAL_SM5_TVOS
|| Format == NAME_SF_METAL_SM5
|| Format == NAME_SF_METAL_SM6
|| Format == NAME_SF_METAL_SIM
|| Format == NAME_SF_METAL_ES3_1);
}
virtual bool PreprocessShader(
const FShaderCompilerInput& Input,
const FShaderCompilerEnvironment& Environment,
FShaderPreprocessOutput& PreprocessOutput) const override final
{
CheckShaderFormat(Input.ShaderFormat);
return PreprocessMetalShader(Input, Environment, PreprocessOutput);
}
virtual void CompilePreprocessedShader(
const FShaderCompilerInput& Input,
const FShaderPreprocessOutput& PreprocessOutput,
FShaderCompilerOutput& Output,
const FString& WorkingDirectory) const override final
{
CheckShaderFormat(Input.ShaderFormat);
CompileMetalShader(Input, PreprocessOutput, Output);
}
virtual bool CanStripShaderCode(bool const bNativeFormat) const override final
{
return CanCompileBinaryShaders() && bNativeFormat;
}
virtual bool StripShaderCode( TArray<uint8>& Code, FString const& DebugOutputDir, bool const bNative ) const override final
{
return StripShader_Metal(Code, DebugOutputDir, bNative);
}
virtual bool SupportsShaderArchives() const override
{
return CanCompileBinaryShaders();
}
virtual bool CreateShaderArchive(FString const& LibraryName,
FName ShaderFormatAndShaderPlatformName,
const FString& WorkingDirectory,
const FString& OutputDir,
const FString& DebugOutputDir,
const FSerializedShaderArchive& InSerializedShaders,
const TArray<FSharedBuffer>& ShaderCode,
TArray<FString>* OutputFiles) const override final
{
int32 NumShadersPerLibrary = 10000;
check(LibraryName.Len() > 0);
TArray<FString> Components;
FString ShaderPlatform = ShaderFormatAndShaderPlatformName.ToString();
ShaderPlatform.ParseIntoArray(Components, TEXT("-"));
check(Components.Num() == 2);
FName ShaderFormatName(Components[0]);
check(ShaderFormatName == NAME_SF_METAL_ES3_1_IOS || ShaderFormatName == NAME_SF_METAL_SM5_IOS || ShaderFormatName == NAME_SF_METAL_ES3_1_TVOS || ShaderFormatName == NAME_SF_METAL_SM5_TVOS || ShaderFormatName == NAME_SF_METAL_SM5 || ShaderFormatName == NAME_SF_METAL_SM6 || ShaderFormatName == NAME_SF_METAL_SIM || ShaderFormatName == NAME_SF_METAL_ES3_1);
// SM6 needs a lower limit of shaders per library as the packing process takes a significant amount of RAM
if(ShaderFormatName == NAME_SF_METAL_SM6)
{
NumShadersPerLibrary = 1000;
}
const FString ArchivePath = (WorkingDirectory / ShaderFormatAndShaderPlatformName.GetPlainNameString());
IFileManager::Get().DeleteDirectory(*ArchivePath, false, true);
IFileManager::Get().MakeDirectory(*ArchivePath);
FSerializedShaderArchive SerializedShaders(InSerializedShaders);
check(SerializedShaders.GetNumShaders() == ShaderCode.Num());
TArray<uint8> StrippedShaderCode;
TArray<uint8> TempShaderCode;
TArray<TSet<uint64>> SubLibraries;
for (int32 ShaderIndex = 0; ShaderIndex < SerializedShaders.GetNumShaders(); ++ShaderIndex)
{
SerializedShaders.DecompressShader(ShaderIndex, ShaderCode, TempShaderCode);
StripShader_Metal(TempShaderCode, DebugOutputDir, true);
uint64 ShaderId = AppendShader_Metal(ArchivePath, SerializedShaders.ShaderHashes[ShaderIndex], TempShaderCode);
uint32 LibraryIndex = ShaderIndex / NumShadersPerLibrary;
if (ShaderId)
{
if (SubLibraries.Num() <= (int32)LibraryIndex)
{
SubLibraries.Add(TSet<uint64>());
}
SubLibraries[LibraryIndex].Add(ShaderId);
}
FShaderCodeEntry& ShaderEntry = SerializedShaders.ShaderEntries[ShaderIndex];
ShaderEntry.Size = TempShaderCode.Num();
ShaderEntry.UncompressedSize = TempShaderCode.Num();
StrippedShaderCode.Append(TempShaderCode);
}
SerializedShaders.Finalize();
bool bOK = false;
FString LibraryPlatformName = FString::Printf(TEXT("%s_%s"), *LibraryName, *ShaderFormatAndShaderPlatformName.GetPlainNameString());
LibraryPlatformName.ToLowerInline();
volatile int32 CompiledLibraries = 0;
TArray<FGraphEventRef> Tasks;
for (uint32 Index = 0; Index < (uint32)SubLibraries.Num(); Index++)
{
TSet<uint64>& PartialShaders = SubLibraries[Index];
FString LibraryPath = (OutputDir / LibraryPlatformName) + FString::Printf(TEXT(".%d"), Index) + FMetalCompilerToolchain::MetalLibraryExtension;
if (OutputFiles)
{
OutputFiles->Add(LibraryPath);
}
// Enqueue the library compilation as a task so we can go wide
FGraphEventRef CompletionFence = FFunctionGraphTask::CreateAndDispatchWhenReady([ShaderFormatName, ArchivePath, LibraryPath, PartialShaders, DebugOutputDir, &CompiledLibraries]()
{
if (FinalizeLibrary_Metal(ShaderFormatName, ArchivePath, LibraryPath, PartialShaders, DebugOutputDir))
{
FPlatformAtomics::InterlockedIncrement(&CompiledLibraries);
}
}, TStatId(), NULL, ENamedThreads::AnyThread);
Tasks.Add(CompletionFence);
}
#if WITH_ENGINE
FGraphEventRef DebugDataCompletionFence = FFunctionGraphTask::CreateAndDispatchWhenReady([ShaderFormatAndShaderPlatformName, OutputDir, LibraryPlatformName, DebugOutputDir]()
{
//TODO add a check in here - this will only work if we have shader archiving with debug info set.
//We want to archive all the metal shader source files so that they can be unarchived into a debug location
//This allows the debugging of optimised metal shaders within the xcode tool set
//Currently using the 'tar' system tool to create a compressed tape archive
//Place the archive in the same position as the .metallib file
FString CompressedDir = (OutputDir / TEXT("../MetaData/ShaderDebug/"));
IFileManager::Get().MakeDirectory(*CompressedDir, true);
FString CompressedPath = (CompressedDir / LibraryPlatformName) + TEXT(".zip");
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
IFileHandle* ZipFile = PlatformFile.OpenWrite(*CompressedPath);
if (ZipFile)
{
FZipArchiveWriter* ZipWriter = new FZipArchiveWriter(ZipFile);
//Find the metal source files
TArray<FString> FilesToArchive;
IFileManager::Get().FindFilesRecursive(FilesToArchive, *DebugOutputDir, TEXT("*.metal"), true, false, false);
//Write the local file names into the target file
const FString DebugDir = DebugOutputDir / *ShaderFormatAndShaderPlatformName.GetPlainNameString();
for (FString FileName : FilesToArchive)
{
TArray<uint8> FileData;
FFileHelper::LoadFileToArray(FileData, *FileName);
FPaths::MakePathRelativeTo(FileName, *DebugDir);
ZipWriter->AddFile(FileName, FileData, FDateTime::Now());
}
delete ZipWriter;
ZipWriter = nullptr;
}
else
{
UE_LOG(LogShaders, Error, TEXT("Failed to create Metal debug .zip output file \"%s\". Debug .zip export will be disabled."), *CompressedPath);
}
}, TStatId(), NULL, ENamedThreads::AnyThread);
Tasks.Add(DebugDataCompletionFence);
#endif // WITH_ENGINE
// Wait for tasks
for (auto& Task : Tasks)
{
FTaskGraphInterface::Get().WaitUntilTaskCompletes(Task);
}
if (CompiledLibraries == SubLibraries.Num())
{
FString BinaryShaderFile = (OutputDir / LibraryPlatformName) + FMetalCompilerToolchain::MetalMapExtension;
BinaryShaderFile.ToLowerInline();
FArchive* BinaryShaderAr = IFileManager::Get().CreateFileWriter(*BinaryShaderFile);
if (BinaryShaderAr != NULL)
{
FMetalShaderLibraryHeader Header;
Header.Format = ShaderFormatName.GetPlainNameString();
Header.NumLibraries = SubLibraries.Num();
Header.NumShadersPerLibrary = NumShadersPerLibrary;
*BinaryShaderAr << Header;
*BinaryShaderAr << SerializedShaders;
*BinaryShaderAr << StrippedShaderCode;
BinaryShaderAr->Flush();
delete BinaryShaderAr;
if (OutputFiles)
{
OutputFiles->Add(BinaryShaderFile);
}
bOK = true;
}
}
return bOK;
//Map.Format = Format.GetPlainNameString();
}
virtual void ModifyShaderCompilerInput(FShaderCompilerInput& Input) const override
{
// Work out which standard we need, this is dependent on the shader platform.
const bool bIsMobile = FMetalCompilerToolchain::Get()->IsMobile((EShaderPlatform)Input.Target.Platform);
if (bIsMobile)
{
Input.Environment.SetDefine(TEXT("IOS"), 1);
}
else
{
Input.Environment.SetDefine(TEXT("MAC"), 1);
}
Input.Environment.SetDefine(TEXT("COMPILER_METAL"), 1);
if (Input.ShaderFormat == NAME_SF_METAL_ES3_1_IOS ||
Input.ShaderFormat == NAME_SF_METAL_ES3_1_TVOS ||
Input.ShaderFormat == NAME_SF_METAL_SIM ||
Input.ShaderFormat == NAME_SF_METAL_ES3_1)
{
Input.Environment.SetDefine(TEXT("METAL_ES3_1_PROFILE"), 1);
}
else if (Input.ShaderFormat == NAME_SF_METAL_SM5_IOS || Input.ShaderFormat == NAME_SF_METAL_SM5_TVOS)
{
Input.Environment.SetDefine(TEXT("METAL_SM5_IOS_TVOS_PROFILE"), 1);
}
else if (Input.ShaderFormat == NAME_SF_METAL_SM5)
{
Input.Environment.SetDefine(TEXT("METAL_SM5_PROFILE"), 1);
}
else if (Input.ShaderFormat == NAME_SF_METAL_SM6)
{
Input.Environment.SetDefine(TEXT("METAL_SM6_PROFILE"), 1);
}
Input.Environment.SetDefine(TEXT("COMPILER_HLSLCC"), 2);
#if UE_METAL_USE_METAL_SHADER_CONVERTER
const bool bUseMetalShaderConverter = FDataDrivenShaderPlatformInfo::GetSupportsBindless(Input.Target.GetPlatform());
if (bUseMetalShaderConverter)
{
Input.Environment.SetDefine(TEXT("COMPILER_METAL_SHADER_CONVERTER"), 1);
}
#endif
#if !UE_METAL_USE_METAL_SHADER_CONVERTER
if (Input.Environment.FullPrecisionInPS || (IsValidRef(Input.SharedEnvironment) && Input.SharedEnvironment->FullPrecisionInPS))
{
Input.Environment.SetDefine(TEXT("FORCE_FLOATS"), (uint32)1);
}
#else
// We can use 16bits types with Msc (since we do not use the frontend).
if (Input.Environment.CompilerFlags.Contains(CFLAG_AllowRealTypes))
{
Input.Environment.SetDefine(TEXT("PLATFORM_SUPPORTS_REAL_TYPES"), 1);
}
#endif
if (Input.Environment.CompilerFlags.Contains(CFLAG_AvoidFlowControl)
|| Input.Environment.CompilerFlags.Contains(CFLAG_PreferFlowControl))
{
Input.Environment.SetDefine(TEXT("COMPILER_SUPPORTS_ATTRIBUTES"), (uint32)0);
}
else
{
Input.Environment.SetDefine(TEXT("COMPILER_SUPPORTS_ATTRIBUTES"), (uint32)1);
}
bool bUsesInlineRayTracing = Input.Environment.CompilerFlags.Contains(CFLAG_InlineRayTracing);
if (bUsesInlineRayTracing)
{
Input.Environment.SetDefine(TEXT("PLATFORM_SUPPORTS_INLINE_RAY_TRACING"), 1);
}
Input.Environment.SetDefine(TEXT("COMPILER_SUPPORTS_DUAL_SOURCE_BLENDING_SLOT_DECORATION"), (uint32)1);
}
virtual bool CanCompileBinaryShaders() const override final
{
#if PLATFORM_MAC
return FPlatformMisc::IsSupportedXcodeVersionInstalled();
#else
return FMetalCompilerToolchain::Get()->IsCompilerAvailable();
#endif
}
virtual const TCHAR* GetPlatformIncludeDirectory() const
{
return TEXT("Metal");
}
};
/**
* Module for Metal shaders
*/
static IShaderFormat* Singleton = nullptr;
class FMetalShaderFormatModule : public IShaderFormatModule
{
public:
virtual ~FMetalShaderFormatModule()
{
Singleton = nullptr;
}
virtual IShaderFormat* GetShaderFormat()
{
return Singleton;
}
virtual void StartupModule() override
{
Singleton = new FMetalShaderFormat();
}
virtual void ShutdownModule() override
{
delete Singleton;
Singleton = nullptr;
}
};
IMPLEMENT_MODULE(FMetalShaderFormatModule, MetalShaderFormat);
static FMetalCompilerToolchain::EMetalToolchainStatus ParseCompilerVersionAndTarget(const FString& OutputOfMetalDashV, FString& OutVersionString, FMetalCompilerToolchain::PackedVersion& OutPackedVersionNumber, FMetalCompilerToolchain::PackedVersion& OutPackedTargetNumber)
{
/*
Output of metal -v might look like this:
Apple LLVM version 902.11 (metalfe-902.11.1)
Target: air64-apple-darwin19.5.0
Thread model: posix
InstalledDir: C:\Program Files\Metal Developer Tools\ios\bin
*/
// Default initialize output parameters
OutVersionString.Empty();
OutPackedVersionNumber = {};
OutPackedTargetNumber = {};
TArray<FString> Lines;
OutputOfMetalDashV.ParseIntoArrayLines(Lines);
int32 VersionLineIndex = 0;
for (int32 Index = 0; Index < Lines.Num(); ++Index)
{
if (Lines[Index].StartsWith(TEXT("Apple ")) && Lines[Index].Contains(TEXT(" version ")) && Lines[Index].EndsWith(TEXT(")")))
{
VersionLineIndex = Index;
break;
}
}
if (VersionLineIndex < Lines.Num())
{
OutVersionString = Lines[VersionLineIndex];
FString& Version = Lines[VersionLineIndex];
check(!Version.IsEmpty());
int32 Major = 0, Minor = 0;
int32 NumResults = 0;
#if !PLATFORM_WINDOWS
char AppleToolName[PATH_MAX] = { '\0' };
char SupplementaryVersionName[PATH_MAX] = { '\0' };
NumResults = sscanf(TCHAR_TO_ANSI(*Version), "Apple %s version %d.%d (metalfe-%s)", AppleToolName, &Major, &Minor, SupplementaryVersionName);
#else
TCHAR AppleToolName[WINDOWS_MAX_PATH] = { '\0' };
TCHAR SupplementaryVersionName[WINDOWS_MAX_PATH] = { '\0' };
NumResults = swscanf_s(*Version, TEXT("Apple %ls version %d.%d (metalfe-%ls)"), AppleToolName, WINDOWS_MAX_PATH, &Major, &Minor, SupplementaryVersionName, WINDOWS_MAX_PATH);
#endif
if (NumResults != 4)
{
UE_LOG(LogMetalCompilerSetup, Warning, TEXT("Metal version string format unrecoginzed"));
UE_LOG(LogMetalCompilerSetup, Warning, TEXT("Expecting: Apple LLVM version 902.11 (metalfe-902.11.1)"));
UE_LOG(LogMetalCompilerSetup, Warning, TEXT("Obtained: %s"), *Version);
}
OutPackedVersionNumber.Major = Major;
OutPackedVersionNumber.Minor = Minor;
// The version name in brackets is too irregular to extract a useful patch version
// Sometimes (metalfe-31001.667.2), sometimes (metalfe-31001.643.2.1), sometimes (metalfe-31001.362-windows)
OutPackedVersionNumber.Patch = 0;
}
if (OutPackedVersionNumber.Version == 0)
{
return FMetalCompilerToolchain::EMetalToolchainStatus::CouldNotParseCompilerVersion;
}
if (VersionLineIndex + 1 < Lines.Num())
{
const FString& FormatVersion = Lines[VersionLineIndex + 1];
int32 Major = 0, Minor = 0, Patch = 0;
int32 NumResults = 0;
#if !PLATFORM_WINDOWS
NumResults = sscanf(TCHAR_TO_ANSI(*FormatVersion), "Target: air64-apple-darwin%d.%d.%d", &Major, &Minor, &Patch);
#else
NumResults = swscanf_s(*FormatVersion, TEXT("Target: air64-apple-darwin%d.%d.%d"), &Major, &Minor, &Patch);
#endif
OutPackedTargetNumber.Major = Major;
OutPackedTargetNumber.Minor = Minor;
OutPackedTargetNumber.Patch = Patch;
}
if (OutPackedTargetNumber.Version == 0)
{
return FMetalCompilerToolchain::EMetalToolchainStatus::CouldNotParseTargetVersion;
}
return FMetalCompilerToolchain::EMetalToolchainStatus::Success;
}
static FMetalCompilerToolchain::EMetalToolchainStatus ParseLibraryToolpath(const FString& OutputOfMetalSearchDirs, FString& LibraryPath)
{
static FString LibraryPrefix(TEXT("libraries: =%s"));
TArray<FString> Lines;
OutputOfMetalSearchDirs.ParseIntoArrayLines(Lines);
{
int32 LibrariesLineIndex = 0;
for (int32 Index = 0; Index < Lines.Num(); ++Index)
{
if (Lines[Index].StartsWith(TEXT("libraries: =")))
{
LibrariesLineIndex = Index;
break;
}
}
FString& LibraryLine = Lines[LibrariesLineIndex];
LibraryPath = LibraryLine.RightChop(LibraryPrefix.Len());
if (!FPaths::DirectoryExists(LibraryPath))
{
return FMetalCompilerToolchain::EMetalToolchainStatus::CouldNotFindMetalStdLib;
}
FPaths::Combine(LibraryPath, TEXT("include"), TEXT("metal"));
if (!FPaths::DirectoryExists(LibraryPath))
{
return FMetalCompilerToolchain::EMetalToolchainStatus::CouldNotFindMetalStdLib;
}
}
return FMetalCompilerToolchain::EMetalToolchainStatus::Success;
}
FMetalCompilerToolchain* FMetalCompilerToolchain::Singleton = nullptr;
FString FMetalCompilerToolchain::MetalExtention(TEXT(".metal"));
FString FMetalCompilerToolchain::MetalLibraryExtension(TEXT(".metallib"));
FString FMetalCompilerToolchain::MetalObjectExtension(TEXT(".air"));
#if PLATFORM_WINDOWS
FString FMetalCompilerToolchain::MetalFrontendBinary(TEXT("metal.exe"));
FString FMetalCompilerToolchain::MetalArBinary(TEXT("metal-ar.exe"));
FString FMetalCompilerToolchain::MetalLibraryBinary(TEXT("metallib.exe"));
FString FMetalCompilerToolchain::AirPackBinary(TEXT("air-pack.exe"));
#else
FString FMetalCompilerToolchain::MetalFrontendBinary(TEXT("metal"));
FString FMetalCompilerToolchain::MetalArBinary(TEXT("metal-ar"));
FString FMetalCompilerToolchain::MetalLibraryBinary(TEXT("metallib"));
FString FMetalCompilerToolchain::AirPackBinary(TEXT("air-pack"));
#endif
FString FMetalCompilerToolchain::MetalMapExtension(TEXT(".metalmap"));
FString FMetalCompilerToolchain::XcrunPath(TEXT("/usr/bin/xcrun"));
FString FMetalCompilerToolchain::MetalMacSDK(TEXT("macosx"));
FString FMetalCompilerToolchain::MetalMobileSDK(TEXT("iphoneos"));
FString FMetalCompilerToolchain::WindowsToolchainVersion(TEXT("5.3"));
FString FMetalCompilerToolchain::WindowsToolchainSubversion(TEXT("32023"));
// Static methods
void FMetalCompilerToolchain::CreateAndInit()
{
Singleton = new FMetalCompilerToolchain;
Singleton->Init();
}
void FMetalCompilerToolchain::Destroy()
{
Singleton->Teardown();
delete Singleton;
Singleton = nullptr;
}
EShaderPlatform FMetalCompilerToolchain::MetalShaderFormatToLegacyShaderPlatform(FName ShaderFormat)
{
if (ShaderFormat == NAME_SF_METAL_ES3_1_IOS) return SP_METAL_ES3_1_IOS;
if (ShaderFormat == NAME_SF_METAL_SM5_IOS) return SP_METAL_SM5_IOS;
if (ShaderFormat == NAME_SF_METAL_ES3_1_TVOS) return SP_METAL_ES3_1_TVOS;
if (ShaderFormat == NAME_SF_METAL_SM5_TVOS) return SP_METAL_SM5_TVOS;
if (ShaderFormat == NAME_SF_METAL_SM5) return SP_METAL_SM5;
if (ShaderFormat == NAME_SF_METAL_SM6) return SP_METAL_SM6;
if (ShaderFormat == NAME_SF_METAL_SIM) return SP_METAL_SIM;
if (ShaderFormat == NAME_SF_METAL_ES3_1) return SP_METAL_ES3_1;
return SP_NumPlatforms;
}
// Instance methods
FMetalCompilerToolchain::PackedVersion FMetalCompilerToolchain::GetCompilerVersion(EShaderPlatform Platform) const
{
if (this->IsMobile(Platform))
{
return this->MetalCompilerVersion[AppleSDKMobile];
}
return this->MetalCompilerVersion[AppleSDKMac];
}
FMetalCompilerToolchain::PackedVersion FMetalCompilerToolchain::GetTargetVersion(EShaderPlatform Platform) const
{
if (this->IsMobile(Platform))
{
return this->MetalTargetVersion[AppleSDKMobile];
}
return this->MetalTargetVersion[AppleSDKMac];
}
const FString& FMetalCompilerToolchain::GetCompilerVersionString(EShaderPlatform Platform) const
{
if (this->IsMobile(Platform))
{
return this->MetalCompilerVersionString[AppleSDKMobile];
}
return this->MetalCompilerVersionString[AppleSDKMac];
}
void FMetalCompilerToolchain::Init()
{
bToolchainAvailable = false;
bToolchainBinariesPresent = false;
bSkipPCH = true;
#if PLATFORM_MAC
EMetalToolchainStatus Result = DoMacNativeSetup();
#else
EMetalToolchainStatus Result = DoWindowsSetup();
#endif
if (Result != EMetalToolchainStatus::Success)
{
if (GCheckCompilerToolChainSetup > 0)
{
UE_LOG(LogMetalCompilerSetup, Warning, TEXT("Metal compiler not found. Shaders will be stored as text."));
}
bToolchainAvailable = false;
}
else
{
Result = FetchCompilerVersion();
if (Result != EMetalToolchainStatus::Success)
{
UE_LOG(LogMetalCompilerSetup, Log, TEXT("Could not parse compiler version."));
}
Result = FetchMetalStandardLibraryPath();
if (Result != EMetalToolchainStatus::Success)
{
UE_LOG(LogMetalCompilerSetup, Warning, TEXT("Could not parse metal_stdlib path. Will not use PCH."));
bSkipPCH = true;
// This is not really an error since we can compile without the PCH just fine.
Result = EMetalToolchainStatus::Success;
}
else
{
// This is forced off for now. If we wish to re-enable it a lot of testing should be done.
//bSkipPCH = false;
}
bToolchainAvailable = true;
}
if (GCheckCompilerToolChainSetup > 0)
{
if (Result == EMetalToolchainStatus::Success)
{
check(IsCompilerAvailable());
UE_LOG(LogMetalCompilerSetup, Log, TEXT("Metal toolchain setup complete."));
UE_LOG(LogMetalCompilerSetup, Log, TEXT("Using Local Metal compiler"));
if (!MetalFrontendBinaryCommand[AppleSDKMac].IsEmpty())
{
UE_LOG(LogMetalCompilerSetup, Log, TEXT("Mac metalfe found at %s"), *MetalFrontendBinaryCommand[AppleSDKMac]);
}
if (!MetalFrontendBinaryCommand[AppleSDKMobile].IsEmpty())
{
UE_LOG(LogMetalCompilerSetup, Log, TEXT("Mobile metalfe found at %s"), *MetalFrontendBinaryCommand[AppleSDKMobile]);
}
UE_LOG(LogMetalCompilerSetup, Log, TEXT("Mac metalfe version %s"), *MetalCompilerVersionString[AppleSDKMac]);
UE_LOG(LogMetalCompilerSetup, Log, TEXT("Mobile metalfe version %s"), *MetalCompilerVersionString[AppleSDKMobile]);
}
else
{
UE_LOG(LogMetalCompilerSetup, Warning, TEXT("Failed to set up Metal toolchain. See log above. Shaders will not be compiled offline."));
}
}
}
void FMetalCompilerToolchain::Teardown()
{
// remove temporaries
if (this->LocalTempFolder.IsEmpty())
{
return;
}
if (!FPaths::DirectoryExists(this->LocalTempFolder))
{
return;
}
bool bSuccess = IFileManager::Get().DeleteDirectory(*this->LocalTempFolder, false, true);
if (!bSuccess)
{
UE_LOG(LogMetalCompilerSetup, Warning, TEXT("Could not delete temporary %s"), *this->LocalTempFolder);
}
}
FMetalCompilerToolchain::EMetalToolchainStatus FMetalCompilerToolchain::FetchCompilerVersion()
{
EMetalToolchainStatus Result = EMetalToolchainStatus::Success;
{
int32 ReturnCode = 0;
FString StdOut;
// metal -v writes its output to stderr
// But the underlying (windows) implementation of CreateProc puts everything into one pipe, which is written to StdOut.
bool bResult = this->ExecMetalFrontend(AppleSDKMac, TEXT("-v --target=air64-apple-darwin18.7.0"), &ReturnCode, &StdOut, &StdOut);
check(bResult);
if (ReturnCode > 0)
{
return EMetalToolchainStatus::CouldNotParseCompilerVersion;
}
Result = ParseCompilerVersionAndTarget(StdOut, this->MetalCompilerVersionString[AppleSDKMac], this->MetalCompilerVersion[AppleSDKMac], this->MetalTargetVersion[AppleSDKMac]);
if (Result != EMetalToolchainStatus::Success)
{
return Result;
}
}
{
int32 ReturnCode = 0;
FString StdOut;
// metal -v writes its output to stderr
bool bResult = this->ExecMetalFrontend(AppleSDKMobile, TEXT("-v --target=air64-apple-darwin18.7.0"), &ReturnCode, &StdOut, &StdOut);
check(bResult);
if (ReturnCode > 0)
{
return EMetalToolchainStatus::CouldNotParseCompilerVersion;
}
Result = ParseCompilerVersionAndTarget(StdOut, this->MetalCompilerVersionString[AppleSDKMobile], this->MetalCompilerVersion[AppleSDKMobile], this->MetalTargetVersion[AppleSDKMobile]);
}
return Result;
}
FMetalCompilerToolchain::EMetalToolchainStatus FMetalCompilerToolchain::FetchMetalStandardLibraryPath()
{
// if we've already decided to skip compiling a PCH we don't need this path at all.
if (this->bSkipPCH)
{
return EMetalToolchainStatus::Success;
}
EMetalToolchainStatus Result = EMetalToolchainStatus::Success;
{
int32 ReturnCode = 0;
FString StdOut, StdErr;
bool bResult = this->ExecMetalFrontend(AppleSDKMac, TEXT("--print-search-dirs"), &ReturnCode, &StdOut, &StdErr);
check(bResult);
Result = ParseLibraryToolpath(StdOut, this->MetalStandardLibraryPath[AppleSDKMac]);
if (Result != EMetalToolchainStatus::Success)
{
return Result;
}
}
{
int32 ReturnCode = 0;
FString StdOut, StdErr;
bool bResult = this->ExecMetalFrontend(AppleSDKMobile, TEXT("--print-search-dirs"), &ReturnCode, &StdOut, &StdErr);
check(bResult);
Result = ParseLibraryToolpath(StdOut, this->MetalStandardLibraryPath[AppleSDKMobile]);
}
return Result;
}
#if PLATFORM_MAC
FMetalCompilerToolchain::EMetalToolchainStatus FMetalCompilerToolchain::DoMacNativeSetup()
{
FString ToolchainBase;
if (FParse::Value(FCommandLine::Get(), TEXT("-MetalToolchainOverride="), ToolchainBase))
{
const bool bUseOverride = (!ToolchainBase.IsEmpty() && FPaths::DirectoryExists(ToolchainBase));
if (bUseOverride)
{
MetalFrontendBinaryCommand[AppleSDKMac] = ToolchainBase / TEXT("macos") / TEXT("bin") / MetalFrontendBinary;
MetalFrontendBinaryCommand[AppleSDKMobile] = ToolchainBase / TEXT("ios") / TEXT("bin") / MetalFrontendBinary;
const bool bIsFrontendPresent = FPaths::FileExists(MetalFrontendBinaryCommand[AppleSDKMac]) && FPaths::FileExists(MetalFrontendBinaryCommand[AppleSDKMobile]);
if (!bIsFrontendPresent)
{
UE_LOG(LogMetalCompilerSetup, Warning, TEXT("Missing Metal frontend in %s."), *ToolchainBase);
return EMetalToolchainStatus::ToolchainNotFound;
}
MetalArBinaryCommand[AppleSDKMac] = ToolchainBase / TEXT("macos") / TEXT("bin") / MetalArBinary;
MetalArBinaryCommand[AppleSDKMobile] = ToolchainBase / TEXT("ios") / TEXT("bin") / MetalArBinary;
MetalLibBinaryCommand[AppleSDKMac] = ToolchainBase / TEXT("macos") / TEXT("bin") / MetalLibraryBinary;
MetalLibBinaryCommand[AppleSDKMobile] = ToolchainBase / TEXT("ios") / TEXT("bin") / MetalLibraryBinary;
AirPackBinaryCommand[AppleSDKMac] = ToolchainBase / TEXT("macos") / TEXT("bin") / AirPackBinary;
AirPackBinaryCommand[AppleSDKMobile] = ToolchainBase / TEXT("ios") / TEXT("bin") / AirPackBinary;
if (!FPaths::FileExists(MetalArBinaryCommand[AppleSDKMac]) ||
!FPaths::FileExists(MetalArBinaryCommand[AppleSDKMobile]) ||
!FPaths::FileExists(MetalLibBinaryCommand[AppleSDKMac]) ||
!FPaths::FileExists(MetalLibBinaryCommand[AppleSDKMobile]) ||
!FPaths::FileExists(MetalLibBinaryCommand[AppleSDKMobile]) ||
!FPaths::FileExists(AirPackBinaryCommand[AppleSDKMac]) ||
!FPaths::FileExists(AirPackBinaryCommand[AppleSDKMobile]))
{
UE_LOG(LogMetalCompilerSetup, Warning, TEXT("Missing toolchain binaries in %s."), *ToolchainBase);
return EMetalToolchainStatus::ToolchainNotFound;
}
this->bToolchainBinariesPresent = true;
return EMetalToolchainStatus::Success;
}
}
int32 ReturnCode = 0;
FString StdOut, StdErr;
bool bSuccess = this->ExecGenericCommand(*XcrunPath, *FString::Printf(TEXT("--sdk %s --find %s"), *this->MetalMacSDK, *this->MetalFrontendBinary), &ReturnCode, &StdOut, &StdErr);
bSuccess |= FPaths::FileExists(StdOut);
if(!bSuccess || ReturnCode > 0)
{
UE_LOG(LogMetalCompilerSetup, Warning, TEXT("Missing Mac Metal toolchain (macos SDK not found)."));
return EMetalToolchainStatus::ToolchainNotFound;
}
bSuccess = this->ExecGenericCommand(*XcrunPath, *FString::Printf(TEXT("--sdk %s --find %s"), *this->MetalMobileSDK, *this->MetalFrontendBinary), &ReturnCode, &StdOut, &StdErr);
bSuccess |= FPaths::FileExists(StdOut);
if(!bSuccess || ReturnCode > 0)
{
UE_LOG(LogMetalCompilerSetup, Warning, TEXT("Missing Mobile Metal toolchain (iphoneos SDK not found)."));
return EMetalToolchainStatus::ToolchainNotFound;
}
this->bToolchainBinariesPresent = true;
return EMetalToolchainStatus::Success;
}
#endif
#if PLATFORM_WINDOWS
FMetalCompilerToolchain::EMetalToolchainStatus FMetalCompilerToolchain::DoWindowsSetup()
{
int32 Result = 0;
FString ToolchainBase;
static const FString SDKRootEnvFar(TEXT("UE_SDKS_ROOT"));
FString SDKPath = FPlatformMisc::GetEnvironmentVariable(*SDKRootEnvFar);
FString ProgramFilesPath = FPlatformMisc::GetEnvironmentVariable(TEXT("ProgramFiles"));;
if (SDKPath.Len() != 0)
{
FString HostPlatform(TEXT("HostWin64"));
ToolchainBase = FPaths::Combine(*SDKPath, *HostPlatform, TEXT("Win64"), TEXT("MetalDeveloperTools"), WindowsToolchainVersion, TEXT("metal"), WindowsToolchainSubversion);
}
const bool bUseAutoSDK = (!ToolchainBase.IsEmpty() && FPaths::DirectoryExists(ToolchainBase));
if(!bUseAutoSDK)
{
// We expect WindowsMetalToolchainOverride to contains the path up to the folder that contains bin & lib
GConfig->GetString(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("WindowsMetalToolchainOverride"), ToolchainBase, GEngineIni);
const bool bUseOverride = (!ToolchainBase.IsEmpty() && FPaths::DirectoryExists(ToolchainBase));
if (!bUseOverride)
{
ToolchainBase = FPaths::Combine(*ProgramFilesPath, TEXT("Metal Developer Tools"), TEXT("metal"), WindowsToolchainSubversion);
}
}
FString MacBinaryBasePath;
FString IOSBinaryBasePath;
// Starting Metal Developers Tools 5.0 we have a WindowsToolchainSubversion and mac & ios use the same binary on Windows
if (WindowsToolchainSubversion.IsEmpty())
{
MacBinaryBasePath = ToolchainBase / TEXT("macos") / TEXT("bin");
IOSBinaryBasePath = ToolchainBase / TEXT("ios") / TEXT("bin");
}
else
{
MacBinaryBasePath = ToolchainBase / TEXT("bin");
IOSBinaryBasePath = MacBinaryBasePath;
}
MetalFrontendBinaryCommand[AppleSDKMac] = MacBinaryBasePath / MetalFrontendBinary;
MetalFrontendBinaryCommand[AppleSDKMobile] = IOSBinaryBasePath / MetalFrontendBinary;
bool bUseLocalMetalToolchain = FPaths::FileExists(MetalFrontendBinaryCommand[AppleSDKMac]) && FPaths::FileExists(MetalFrontendBinaryCommand[AppleSDKMobile]);
if (!bUseLocalMetalToolchain)
{
if (GCheckCompilerToolChainSetup > 0)
{
UE_LOG(LogMetalCompilerSetup, Display, TEXT("Searching for Metal toolchain, but it doesn't appear to be installed."));
UE_LOG(LogMetalCompilerSetup, Display, TEXT("Searched for %s and %s"), *MetalFrontendBinaryCommand[AppleSDKMac], *MetalFrontendBinaryCommand[AppleSDKMobile]);
}
return EMetalToolchainStatus::ToolchainNotFound;
}
MetalArBinaryCommand[AppleSDKMac] = MacBinaryBasePath / MetalArBinary;
MetalArBinaryCommand[AppleSDKMobile] = IOSBinaryBasePath / MetalArBinary;
MetalLibBinaryCommand[AppleSDKMac] = MacBinaryBasePath / MetalLibraryBinary;
MetalLibBinaryCommand[AppleSDKMobile] = IOSBinaryBasePath / MetalLibraryBinary;
AirPackBinaryCommand[AppleSDKMac] = MacBinaryBasePath / AirPackBinary;
AirPackBinaryCommand[AppleSDKMobile] = IOSBinaryBasePath / AirPackBinary;
if (!FPaths::FileExists(MetalArBinaryCommand[AppleSDKMac]) ||
!FPaths::FileExists(MetalArBinaryCommand[AppleSDKMobile]) ||
!FPaths::FileExists(MetalLibBinaryCommand[AppleSDKMac]) ||
!FPaths::FileExists(MetalLibBinaryCommand[AppleSDKMobile]) ||
!FPaths::FileExists(AirPackBinaryCommand[AppleSDKMac]) ||
!FPaths::FileExists(AirPackBinaryCommand[AppleSDKMobile]))
{
if (GCheckCompilerToolChainSetup > 0)
{
UE_LOG(LogMetalCompilerSetup, Warning, TEXT("Missing toolchain binaries."))
}
return EMetalToolchainStatus::ToolchainNotFound;
}
this->bToolchainBinariesPresent = true;
return EMetalToolchainStatus::Success;
}
#endif
bool FMetalCompilerToolchain::ExecMetalFrontend(EAppleSDKType SDK, const TCHAR* Parameters, int32* OutReturnCode, FString* OutStdOut, FString* OutStdErr) const
{
check(this->bToolchainBinariesPresent);
#if PLATFORM_MAC
if (this->MetalFrontendBinaryCommand[SDK].IsEmpty())
{
FString BuiltParams = FString::Printf(TEXT("--sdk %s %s %s"), *SDKToString(SDK), *this->MetalFrontendBinary, Parameters);
return ExecGenericCommand(*XcrunPath, *BuiltParams, OutReturnCode, OutStdOut, OutStdErr);
}
else
#endif
return ExecGenericCommand(*this->MetalFrontendBinaryCommand[SDK], Parameters, OutReturnCode, OutStdOut, OutStdErr);
}
bool FMetalCompilerToolchain::ExecMetalLib(EAppleSDKType SDK, const TCHAR* Parameters, int32* OutReturnCode, FString* OutStdOut, FString* OutStdErr) const
{
check(this->bToolchainBinariesPresent);
#if PLATFORM_MAC
if (this->MetalLibBinaryCommand[SDK].IsEmpty())
{
FString BuiltParams = FString::Printf(TEXT("--sdk %s %s %s"), *SDKToString(SDK), *this->MetalLibraryBinary, Parameters);
return ExecGenericCommand(*XcrunPath, *BuiltParams, OutReturnCode, OutStdOut, OutStdErr);
}
else
#endif
return ExecGenericCommand(*this->MetalLibBinaryCommand[SDK], Parameters, OutReturnCode, OutStdOut, OutStdErr);
}
bool FMetalCompilerToolchain::ExecAirPack(EAppleSDKType SDK, const TCHAR* Parameters, int32* OutReturnCode, FString* OutStdOut, FString* OutStdErr) const
{
check(this->bToolchainBinariesPresent);
#if PLATFORM_MAC
if (this->AirPackBinaryCommand[SDK].IsEmpty())
{
FString BuiltParams = FString::Printf(TEXT("--sdk %s %s %s"), *SDKToString(SDK), *this->AirPackBinary, Parameters);
return ExecGenericCommand(*XcrunPath, *BuiltParams, OutReturnCode, OutStdOut, OutStdErr);
}
else
#endif
return ExecGenericCommand(*this->AirPackBinaryCommand[SDK], Parameters, OutReturnCode, OutStdOut, OutStdErr);
}
bool FMetalCompilerToolchain::ExecMetalAr(EAppleSDKType SDK, const TCHAR* ScriptFile, int32* OutReturnCode, FString* OutStdOut, FString* OutStdErr) const
{
check(this->bToolchainBinariesPresent);
// WARNING: This phase may be run in parallel so we must not collide our scripts
// metal-ar is really llvm-ar, which acts like the standard ar. Since we usually end up with a ton of objects we are archiving we'd like to script it
// Unfortunately ar reads its script from stdin (when the -M arg is present) instead of being provided a file
// So on windows we'll spawn cmd.exe and pipe the script file into metal-ar.exe -M
#if PLATFORM_MAC
FString Command;
if (this->MetalArBinaryCommand[SDK].IsEmpty())
{
Command = FString::Printf(TEXT("-c \"%s -sdk %s '%s' -M < '%s'\""), *XcrunPath, *SDKToString(SDK), *this->MetalArBinary, ScriptFile);
}
else
{
Command = FString::Printf(TEXT("-c \"'%s' -M < '%s'\""), *this->MetalArBinaryCommand[SDK], ScriptFile);
}
bool bSuccess = ExecGenericCommand(TEXT("/bin/sh"), *Command, OutReturnCode, OutStdOut, OutStdErr);
#else
FString Command = FString::Printf(TEXT("/C type \"%s\" | \"%s\" -M"), ScriptFile, *this->MetalArBinaryCommand[SDK]);
bool bSuccess = ExecGenericCommand(TEXT("cmd.exe"), *Command, OutReturnCode, OutStdOut, OutStdErr, true /*bIsConsoleApp*/);
#endif
if (!bSuccess)
{
UE_LOG(LogMetalShaderCompiler, Error, TEXT("Error creating .metalar. %s."), **OutStdOut);
UE_LOG(LogMetalShaderCompiler, Error, TEXT("Error creating .metalar. %s."), **OutStdErr);
}
return bSuccess;
}
bool FMetalCompilerToolchain::ExecGenericCommand(const TCHAR* Command, const TCHAR* Params, int32* OutReturnCode, FString* OutStdOut, FString* OutStdErr, bool bIsConsoleApp) const
{
#if PLATFORM_WINDOWS
if(bIsConsoleApp)
{
// Why do we have our own implementation here? Because metal.exe wants to create a console window.
// So if we don't specify the options to CreateProc we end up with tons and tons of windows appearing and disappearing during a cook.
void* OutputReadPipe = nullptr;
void* OutputWritePipe = nullptr;
FPlatformProcess::CreatePipe(OutputReadPipe, OutputWritePipe);
FProcHandle Proc = FPlatformProcess::CreateProc(Command, Params, false, true, true, nullptr, -1, nullptr, OutputWritePipe, nullptr);
if (!Proc.IsValid())
{
FPlatformProcess::ClosePipe(OutputReadPipe, OutputWritePipe);
return false;
}
int32 RC;
FPlatformProcess::WaitForProc(Proc);
FPlatformProcess::GetProcReturnCode(Proc, &RC);
if (OutStdOut)
{
*OutStdOut = FPlatformProcess::ReadPipe(OutputReadPipe);
}
FPlatformProcess::ClosePipe(OutputReadPipe, OutputWritePipe);
FPlatformProcess::CloseProc(Proc);
if (OutReturnCode)
{
*OutReturnCode = RC;
}
return RC == 0;
}
else
#endif
{
// Otherwise use the API
return FPlatformProcess::ExecProcess(Command, Params, OutReturnCode, OutStdOut, OutStdErr);
}
}
bool FMetalCompilerToolchain::CompileMetalShader(FMetalShaderBytecodeJob& Job, FMetalShaderBytecode& Output) const
{
// The local files
const FString& LocalInputMetalFilePath = Job.InputFile;
const FString& LocalOutputMetalAIRFilePath = Job.OutputObjectFile;
const FString& LocalOutputMetalLibFilePath = Job.OutputFile;
EAppleSDKType SDK = FMetalCompilerToolchain::MetalFormatToSDK(Job.ShaderFormat);
// .metal -> .air
FString IncludeArgs = Job.IncludeDir.Len() ? FString::Printf(TEXT("-I %s"), *Job.IncludeDir) : TEXT("");
{
// Invoke the metal frontend.
FString MetalParams = FString::Printf(TEXT("%s %s %s %s -Wno-null-character -fbracket-depth=1024 %s %s %s %s %s -fmodules-cache-path=%s -o %s"), *Job.MinOSVersion, *Job.PreserveInvariance, *Job.DebugInfo, *Job.MathMode, TEXT("-c"), *Job.Standard, *Job.Defines, *IncludeArgs, *LocalInputMetalFilePath, *Job.ModuleCacheDirectory, *LocalOutputMetalAIRFilePath);
if (Job.bOptimizeForSize)
{
MetalParams += TEXT(" -Os");
}
// We don't use incremental build and this void creating temporary files in C drive in Windows
MetalParams += TEXT(" -fno-temp-file");
bool bSuccess = this->ExecMetalFrontend(SDK, *MetalParams, &Job.ReturnCode, &Job.Results, &Job.Errors);
// Delete the cache directory because it takes a approx 5mb per compile
IFileManager::Get().DeleteDirectory(*Job.ModuleCacheDirectory, false, true);
if (!bSuccess || (Job.ReturnCode != 0))
{
Job.Message = FString::Printf(TEXT("Failed to compile %s to bytecode %s, code: %d, output: %s %s"), *LocalInputMetalFilePath, *LocalOutputMetalAIRFilePath, Job.ReturnCode, *Job.Results, *Job.Errors);
return false;
}
}
{
// If we have succeeded, now we can create a metallib out of the AIR.
// TODO do we want to do this in every case? Should be able to skip if we are using Shader Libraries at the high level
FString MetalLibParams = FString::Printf(TEXT("-o %s %s"), *LocalOutputMetalLibFilePath, *LocalOutputMetalAIRFilePath);
bool bSuccess = this->ExecMetalLib(SDK, *MetalLibParams, &Job.ReturnCode, &Job.Results, &Job.Errors);
if (!bSuccess || (Job.ReturnCode != 0))
{
Job.Message = FString::Printf(TEXT("Failed to package %s into %s, code: %d, output: %s %s"), *LocalOutputMetalAIRFilePath, *LocalOutputMetalLibFilePath, Job.ReturnCode, *Job.Results, *Job.Errors);
return false;
}
}
// At this point we have an .air file and a .metallib file
if (Job.bRetainObjectFile)
{
// Retain the .air. This usually means we are using shared native libraries.
bool bSuccess = FFileHelper::LoadFileToArray(Output.ObjectFile, *LocalOutputMetalAIRFilePath);
// Clean up the .air file
IFileManager::Get().Delete(*LocalOutputMetalAIRFilePath);
if (!bSuccess)
{
Job.Message = FString::Printf(TEXT("Failed to store AIR %s"), *LocalOutputMetalAIRFilePath);
return false;
}
}
else
{
// Clean up the .air file
IFileManager::Get().Delete(*LocalOutputMetalAIRFilePath);
}
{
// Retain the .metallib
Output.NativePath = LocalInputMetalFilePath;
bool bSuccess = FFileHelper::LoadFileToArray(Output.OutputFile, *LocalOutputMetalLibFilePath);
// Clean up the .metallib file
IFileManager::Get().Delete(*LocalOutputMetalLibFilePath);
if (!bSuccess)
{
Job.Message = FString::Printf(TEXT("Failed to store metallib %s"), *LocalOutputMetalLibFilePath);
return false;
}
}
return true;
}