// Copyright Epic Games, Inc. All Rights Reserved. #include "MetalShaderCompiler.h" #if UE_ENABLE_INCLUDE_ORDER_DEPRECATED_IN_5_4 #include "CoreMinimal.h" #endif #include "HAL/PlatformFileManager.h" #include "MetalShaderFormat.h" #include "MetalShaderResources.h" #include "Misc/ConfigCacheIni.h" #include "Misc/Compression.h" #include "Misc/EngineVersion.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "ShaderCompilerCommon.h" #include "ShaderCompilerDefinitions.h" #include "ShaderCore.h" #include "ShaderParameterParser.h" #include "ShaderPreprocessTypes.h" #include "Serialization/MemoryWriter.h" #include "Serialization/MemoryReader.h" #include "DataDrivenShaderPlatformInfo.h" #include "MetalCompileShaderSPIRV.h" #include "MetalCompileShaderMSC.h" #if PLATFORM_MAC || PLATFORM_WINDOWS THIRD_PARTY_INCLUDES_START #include "metal_irconverter.h" THIRD_PARTY_INCLUDES_END #endif #if PLATFORM_WINDOWS #include "Windows/AllowWindowsPlatformTypes.h" THIRD_PARTY_INCLUDES_START #include #include #include THIRD_PARTY_INCLUDES_END #include "Windows/HideWindowsPlatformTypes.h" #endif #include "ShaderPreprocessor.h" #include "MetalBackend.h" #include "MetalShaderCompiler.h" #include #if !PLATFORM_WINDOWS #if PLATFORM_TCHAR_IS_CHAR16 #define FP_TEXT_PASTE(x) L ## x #define WTEXT(x) FP_TEXT_PASTE(x) #else #define WTEXT TEXT #endif #endif static bool CompileProcessAllowsRuntimeShaderCompiling(const FShaderCompilerInput& InputCompilerEnvironment) { bool bArchiving = InputCompilerEnvironment.Environment.CompilerFlags.Contains(CFLAG_Archive); bool bDebug = InputCompilerEnvironment.Environment.CompilerFlags.Contains(CFLAG_Debug); return !bArchiving && bDebug; } constexpr uint16 GMetalMaxUniformBufferSlots = 32; constexpr int32 GMetalDefaultShadingLanguageVersion = 0; /*------------------------------------------------------------------------------ Shader compiling. ------------------------------------------------------------------------------*/ static inline uint32 ParseNumber(const TCHAR* Str) { uint32 Num = 0; while (*Str && *Str >= '0' && *Str <= '9') { Num = Num * 10 + *Str++ - '0'; } return Num; } static inline uint32 ParseNumber(const ANSICHAR* Str) { uint32 Num = 0; while (*Str && *Str >= '0' && *Str <= '9') { Num = Num * 10 + *Str++ - '0'; } return Num; } struct FHlslccMetalHeader : public CrossCompiler::FHlslccHeader { FHlslccMetalHeader(uint32 const Version); virtual ~FHlslccMetalHeader(); // After the standard header, different backends can output their own info virtual bool ParseCustomHeaderEntries(const ANSICHAR*& ShaderSource) override; TMap> ArgumentBuffers; int8 SideTable; uint32 RayTracingInstanceIndexBuffer; uint32 Version; }; FHlslccMetalHeader::FHlslccMetalHeader(uint32 const InVersion) { SideTable = -1; RayTracingInstanceIndexBuffer = UINT_MAX; Version = InVersion; } FHlslccMetalHeader::~FHlslccMetalHeader() { } bool FHlslccMetalHeader::ParseCustomHeaderEntries(const ANSICHAR*& ShaderSource) { #define DEF_PREFIX_STR(Str) \ static const ANSICHAR* Str##Prefix = "// @" #Str ": "; \ static const int32 Str##PrefixLen = FCStringAnsi::Strlen(Str##Prefix) DEF_PREFIX_STR(ArgumentBuffers); DEF_PREFIX_STR(SideTable); DEF_PREFIX_STR(RayTracingInstanceIndexBuffer); #undef DEF_PREFIX_STR const ANSICHAR* SideTableString = FCStringAnsi::Strstr(ShaderSource, SideTablePrefix); if (SideTableString) { ShaderSource = SideTableString; ShaderSource += SideTablePrefixLen; while (*ShaderSource && *ShaderSource != '\n') { if (*ShaderSource == '(') { ShaderSource++; if (*ShaderSource && *ShaderSource != '\n') { SideTable = (int8)ParseNumber(ShaderSource); } } else { ShaderSource++; } } if (*ShaderSource && !CrossCompiler::Match(ShaderSource, '\n')) { return false; } if (SideTable < 0) { UE_LOG(LogMetalShaderCompiler, Fatal, TEXT("Couldn't parse the SideTable buffer index for bounds checking")); return false; } } const ANSICHAR* RayTracingInstanceIndexBufferString = FCStringAnsi::Strstr(ShaderSource, RayTracingInstanceIndexBufferPrefix); if (RayTracingInstanceIndexBufferString) { ShaderSource += RayTracingInstanceIndexBufferPrefixLen; if (!CrossCompiler::ParseIntegerNumber(ShaderSource, RayTracingInstanceIndexBuffer)) { return false; } if (*ShaderSource && !CrossCompiler::Match(ShaderSource, '\n')) { return false; } } const ANSICHAR* ArgumentTable = FCStringAnsi::Strstr(ShaderSource, ArgumentBuffersPrefix); if (ArgumentTable) { ShaderSource = ArgumentTable; ShaderSource += ArgumentBuffersPrefixLen; while (*ShaderSource && *ShaderSource != '\n') { int32 ArgumentBufferIndex = -1; if (!CrossCompiler::ParseIntegerNumber(ShaderSource, ArgumentBufferIndex)) { return false; } check(ArgumentBufferIndex >= 0); if (!CrossCompiler::Match(ShaderSource, '[')) { return false; } TArray Mask; while (*ShaderSource && *ShaderSource != ']') { int32 MaskIndex = -1; if (!CrossCompiler::ParseIntegerNumber(ShaderSource, MaskIndex)) { return false; } check(MaskIndex >= 0); Mask.Add((uint8)MaskIndex); if (!CrossCompiler::Match(ShaderSource, ',') && *ShaderSource != ']') { return false; } } if (!CrossCompiler::Match(ShaderSource, ']')) { return false; } if (!CrossCompiler::Match(ShaderSource, ',') && *ShaderSource != '\n') { return false; } ArgumentBuffers.Add((uint8)ArgumentBufferIndex, Mask); } } return true; } /** * Construct the final microcode from the compiled and verified shader source. * @param ShaderOutput - Where to store the microcode and parameter map. * @param InShaderSource - Metal source with input/output signature. * @param SourceLen - The length of the Metal source code. */ void BuildMetalShaderOutput( FShaderCompilerOutput& ShaderOutput, const FShaderCompilerInput& ShaderInput, const ANSICHAR* InShaderSource, uint32 SourceLen, uint32 SourceCRCLen, uint32 SourceCRC, uint32 Version, TCHAR const* Standard, TCHAR const* MinOSVersion, TArray& OutErrors, uint32 TypedBuffers, uint32 InvariantBuffers, uint32 TypedUAVs, uint32 ConstantBuffers, bool bAllowFastIntriniscs #if UE_METAL_USE_METAL_SHADER_CONVERTER , uint32 NumCBVs, uint32 OutputSizeVS, uint32 MaxInputPrimitivesPerMeshThreadgroupGS, const bool bUsesDiscard, char const* ShaderReflectionJSON, FMetalShaderBytecode const& CompiledShaderBytecode #endif ) { ShaderOutput.bSucceeded = false; const ANSICHAR* USFSource = InShaderSource; uint32 NumLines = 0; const ANSICHAR* Main = FCStringAnsi::Strstr(USFSource, "Main_"); while (Main && *Main) { if (*Main == '\n') { NumLines++; } Main++; } FHlslccMetalHeader CCHeader(Version); if (!CCHeader.Read(USFSource, SourceLen)) { UE_LOG(LogMetalShaderCompiler, Fatal, TEXT("Bad hlslcc header found")); } const EShaderFrequency Frequency = ShaderOutput.Target.GetFrequency(); const bool bBindlessEnabled = ShaderInput.IsBindlessEnabled(); //TODO read from toolchain const bool bIsMobile = (ShaderInput.Target.Platform == SP_METAL_ES3_1_IOS || ShaderInput.Target.Platform == SP_METAL_SM5_IOS || ShaderInput.Target.Platform == SP_METAL_ES3_1_TVOS || ShaderInput.Target.Platform == SP_METAL_SM5_TVOS || ShaderInput.Target.Platform == SP_METAL_SIM); bool bNoFastMath = ShaderInput.Environment.CompilerFlags.Contains(CFLAG_NoFastMath); const bool bUsingWPO = ShaderInput.Environment.GetCompileArgument(TEXT("USES_WORLD_POSITION_OFFSET"), false); if (bUsingWPO && (ShaderInput.Target.Platform == SP_METAL_SM5_IOS || ShaderInput.Target.Platform == SP_METAL_SM5_TVOS) && Frequency == SF_Vertex) { // WPO requires that we make all multiply/sincos instructions invariant :( bNoFastMath = true; } FMetalCodeHeader Header; FShaderResourceTable SRT; Header.CompileFlags = (ShaderInput.Environment.CompilerFlags.Contains(CFLAG_Debug) ? (1 << CFLAG_Debug) : 0); Header.CompileFlags |= (bNoFastMath ? (1 << CFLAG_NoFastMath) : 0); Header.CompileFlags |= (ShaderInput.Environment.CompilerFlags.Contains(CFLAG_ExtraShaderData) ? (1 << CFLAG_ExtraShaderData) : 0); Header.CompileFlags |= (ShaderInput.Environment.CompilerFlags.Contains(CFLAG_ZeroInitialise) ? (1 << CFLAG_ZeroInitialise) : 0); Header.CompileFlags |= (ShaderInput.Environment.CompilerFlags.Contains(CFLAG_BoundsChecking) ? (1 << CFLAG_BoundsChecking) : 0); Header.CompileFlags |= (ShaderInput.Environment.CompilerFlags.Contains(CFLAG_Archive) ? (1 << CFLAG_Archive) : 0); Header.Version = Version; Header.SideTable = -1; Header.SourceLen = SourceCRCLen; Header.SourceCRC = SourceCRC; Header.Bindings.ConstantBuffers = ConstantBuffers; FShaderParameterMap& ParameterMap = ShaderOutput.ParameterMap; TBitArray<> UsedUniformBufferSlots; UsedUniformBufferSlots.Init(false,32); // Write out the magic markers. Header.Frequency = Frequency; // Only inputs for vertex shaders must be tracked. if (Frequency == SF_Vertex) { static const FString AttributePrefix = TEXT("in_ATTRIBUTE"); for (auto& Input : CCHeader.Inputs) { // Only process attributes. if (Input.Name.StartsWith(AttributePrefix)) { uint8 AttributeIndex = ParseNumber(*Input.Name + AttributePrefix.Len()); Header.Bindings.InOutMask.EnableField(AttributeIndex); } } } // Then the list of outputs. static const FString TargetPrefix = "FragColor"; static const FString TargetPrefix2 = "SV_Target"; static const FString DepthTargetPrefix = "SV_Depth"; // Only outputs for pixel shaders must be tracked. if (Frequency == SF_Pixel) { for (auto& Output : CCHeader.Outputs) { // Handle targets. if (Output.Name.StartsWith(TargetPrefix)) { uint8 TargetIndex = ParseNumber(*Output.Name + TargetPrefix.Len()); Header.Bindings.InOutMask.EnableField(TargetIndex); } else if (Output.Name.StartsWith(TargetPrefix2)) { uint8 TargetIndex = ParseNumber(*Output.Name + TargetPrefix2.Len()); Header.Bindings.InOutMask.EnableField(TargetIndex); } else if (Output.Name.StartsWith(DepthTargetPrefix)) { Header.Bindings.InOutMask.EnableField(CrossCompiler::FShaderBindingInOutMask::DepthStencilMaskIndex); } } // For fragment shaders that discard but don't output anything we need at least a depth-stencil surface, so we need a way to validate this at runtime. if (FCStringAnsi::Strstr(USFSource, "[[ depth(") != nullptr || FCStringAnsi::Strstr(USFSource, "[[depth(") != nullptr) { Header.Bindings.InOutMask.EnableField(CrossCompiler::FShaderBindingInOutMask::DepthStencilMaskIndex); } // For fragment shaders that discard but don't output anything we need at least a depth-stencil surface, so we need a way to validate this at runtime. if (FCStringAnsi::Strstr(USFSource, "discard_fragment()") != nullptr) { EnumAddFlags(Header.Bindings.Flags, EMetalBindingsFlags::PixelDiscard); } } // Then 'normal' uniform buffers. bool bOutOfBounds = false; for (auto& UniformBlock : CCHeader.UniformBlocks) { uint16 UBIndex = UniformBlock.Index; if (UBIndex >= Header.Bindings.NumUniformBuffers) { Header.Bindings.NumUniformBuffers = UBIndex + 1; } if (UBIndex >= GMetalMaxUniformBufferSlots) { bOutOfBounds = true; new(OutErrors) FShaderCompilerError(*FString::Printf(TEXT("Uniform buffer index (%d) exceeded upper bound of slots (%d) for Metal API: %s"), UBIndex, GMetalMaxUniformBufferSlots, *UniformBlock.Name)); continue; } UsedUniformBufferSlots[UBIndex] = true; HandleReflectedUniformBuffer(UniformBlock.Name, UBIndex, ShaderOutput); } if (bOutOfBounds) { ShaderOutput.bSucceeded = false; return; } // Packed global uniforms const uint16 BytesPerComponent = 4; TMap PackedGlobalArraySize; for (auto& PackedGlobal : CCHeader.PackedGlobals) { HandleReflectedGlobalConstantBufferMember( PackedGlobal.Name, PackedGlobal.PackedType, PackedGlobal.Offset * BytesPerComponent, PackedGlobal.Count * BytesPerComponent, ShaderOutput ); uint16& Size = PackedGlobalArraySize.FindOrAdd(PackedGlobal.PackedType); Size = FMath::Max(BytesPerComponent * (PackedGlobal.Offset + PackedGlobal.Count), Size); } bool bUseMetalShaderConverter = false; #if UE_METAL_USE_METAL_SHADER_CONVERTER bUseMetalShaderConverter = ShaderInput.Target.GetPlatform() == EShaderPlatform::SP_METAL_SM6 && bBindlessEnabled; #endif // Packed Uniform Buffers TMap > PackedUniformBuffersSize; for (auto& PackedUB : CCHeader.PackedUBs) { for (auto& Member : PackedUB.Members) { uint32 ConstantBufferIndex = bBindlessEnabled ? 0 : (uint32)CrossCompiler::EPackedTypeName::HighP; // We need to distinguish Root/Global CBs when ShaderConverter is used (mainly for RT support); // therefore we perform CB reflection during the previous compilation stage of the pipeline (and keep // the vanilla path for SPIRV-Cross). if (!bUseMetalShaderConverter) { HandleReflectedGlobalConstantBufferMember( Member.Name, ConstantBufferIndex, Member.Offset * BytesPerComponent, Member.Count * BytesPerComponent, ShaderOutput ); } uint16& Size = PackedUniformBuffersSize.FindOrAdd(PackedUB.Attribute.Index).FindOrAdd((CrossCompiler::EPackedTypeName)ConstantBufferIndex); Size = FMath::Max(BytesPerComponent * (Member.Offset + Member.Count), Size); } } // Setup Packed Array info Header.Bindings.PackedGlobalArrays.Reserve(PackedGlobalArraySize.Num()); for (auto Iterator = PackedGlobalArraySize.CreateIterator(); Iterator; ++Iterator) { ANSICHAR TypeName = Iterator.Key(); uint16 Size = Iterator.Value(); Size = (Size + 0xf) & (~0xf); CrossCompiler::FPackedArrayInfo Info; Info.Size = Size; Info.TypeName = TypeName; Info.TypeIndex = (uint8)CrossCompiler::PackedTypeNameToTypeIndex(TypeName); Header.Bindings.PackedGlobalArrays.Add(Info); } // In this mode there should only be 0 or 1 packed UB that contains all the aligned & named global uniform parameters check(PackedUniformBuffersSize.Num() <= 1); for (auto Iterator = PackedUniformBuffersSize.CreateIterator(); Iterator; ++Iterator) { int BufferIndex = Iterator.Key(); auto& ArraySizes = Iterator.Value(); for (auto IterSizes = ArraySizes.CreateIterator(); IterSizes; ++IterSizes) { CrossCompiler::EPackedTypeName TypeName = IterSizes.Key(); uint16 Size = IterSizes.Value(); Size = (Size + 0xf) & (~0xf); CrossCompiler::FPackedArrayInfo Info; Info.Size = Size; Info.TypeName = (ANSICHAR)TypeName; Info.TypeIndex = BufferIndex; Header.Bindings.PackedGlobalArrays.Add(Info); } } uint32 NumTextures = 0; // Then samplers. TMap SamplerMap; for (auto& Sampler : CCHeader.Samplers) { HandleReflectedShaderResource(Sampler.Name, Sampler.Offset, Sampler.Count, ShaderOutput); NumTextures += Sampler.Count; for (auto& SamplerState : Sampler.SamplerStates) { SamplerMap.Add(SamplerState, Sampler.Count); } } Header.Bindings.NumSamplers = CCHeader.SamplerStates.Num(); // Then UAVs (images in Metal) for (auto& UAV : CCHeader.UAVs) { HandleReflectedShaderUAV(UAV.Name, UAV.Offset, UAV.Count, ShaderOutput); Header.Bindings.NumUAVs = FMath::Max( Header.Bindings.NumSamplers, UAV.Offset + UAV.Count ); } for (auto& SamplerState : CCHeader.SamplerStates) { if (!SamplerMap.Contains(SamplerState.Name)) { SamplerMap.Add(SamplerState.Name, 1); } HandleReflectedShaderSampler(SamplerState.Name, SamplerState.Index, SamplerMap[SamplerState.Name], ShaderOutput); } #if UE_METAL_USE_METAL_SHADER_CONVERTER if (bUseMetalShaderConverter) { #if PLATFORM_MAC || PLATFORM_WINDOWS #if PLATFORM_MAC // Xcode 16 is the minimum supported version for MSC if(FPlatformMisc::XCodeVersionCompare(16, 0, 0) < 0) { UE_LOG(LogMetalShaderCompiler, Fatal, TEXT("Compiling shaders for Metal SM6 requires XCode 16.0 or above")); } #endif EnumAddFlags(Header.Bindings.Flags, EMetalBindingsFlags::UseMetalShaderConverter); // Only needed for VS Input (to generate the stage-in function used to convert inputs). if (Frequency == SF_Vertex) { Header.Bindings.IRConverterReflectionJSON = ANSI_TO_TCHAR(ShaderReflectionJSON); IRShaderReflectionReleaseString(ShaderReflectionJSON); check(ShaderReflectionJSON && Header.Bindings.IRConverterReflectionJSON.Len() > 0); } else { Header.Bindings.IRConverterReflectionJSON = TEXT(""); } Header.Bindings.RSNumCBVs = NumCBVs; Header.Bindings.OutputSizeVS = OutputSizeVS; if (bUsesDiscard) { EnumAddFlags(Header.Bindings.Flags, EMetalBindingsFlags::PixelDiscard); } else { EnumRemoveFlags(Header.Bindings.Flags, EMetalBindingsFlags::PixelDiscard); } #if PLATFORM_SUPPORTS_GEOMETRY_SHADERS Header.Bindings.MaxInputPrimitivesPerMeshThreadgroupGS = MaxInputPrimitivesPerMeshThreadgroupGS; #endif if (bBindlessEnabled) { if (Frequency == SF_Pixel) { // BINDLESS HACK: If the PS writes to UAVs only, we need to set the reflected number // of UAVs to a dummy value (to make sure the RHI binds a dummy depth RT). if (Header.Bindings.InOutMask.Bitmask == 0) { Header.Bindings.NumUAVs = 1; } } } #else UE_LOG(LogMetalShaderCompiler, Fatal, TEXT("Attempting to build using MSC on unsupported platform")); #endif } #endif Header.NumThreadsX = CCHeader.NumThreads[0]; Header.NumThreadsY = CCHeader.NumThreads[1]; Header.NumThreadsZ = CCHeader.NumThreads[2]; if (Frequency == SF_Compute) { Header.RayTracing.InstanceIndexBuffer = CCHeader.RayTracingInstanceIndexBuffer; } Header.bDeviceFunctionConstants = (FCStringAnsi::Strstr(USFSource, "#define __METAL_DEVICE_CONSTANT_INDEX__ 1") != nullptr) ? 1 : 0; Header.SideTable = CCHeader.SideTable; Header.Bindings.ArgumentBufferMasks = CCHeader.ArgumentBuffers; Header.Bindings.ArgumentBuffers = 0; for (auto const& Pair : Header.Bindings.ArgumentBufferMasks) { Header.Bindings.ArgumentBuffers |= (1 << Pair.Key); } // Build the SRT for this shader. { // Build the generic SRT for this shader. FShaderCompilerResourceTable GenericSRT; BuildResourceTableMapping(ShaderInput.Environment.ResourceTableMap, ShaderInput.Environment.UniformBufferMap, UsedUniformBufferSlots, ShaderOutput.ParameterMap, GenericSRT); CullGlobalUniformBuffers(ShaderInput.Environment.UniformBufferMap, ShaderOutput.ParameterMap); UE::ShaderCompilerCommon::BuildShaderResourceTable(GenericSRT, SRT); Header.Bindings.NumUniformBuffers = FMath::Max((uint8)GetNumUniformBuffersUsed(GenericSRT), Header.Bindings.NumUniformBuffers); } FString MetalCode = FString(USFSource); if (ShaderInput.Environment.CompilerFlags.Contains(CFLAG_Debug) && !bUseMetalShaderConverter) { MetalCode.InsertAt(0, FString::Printf(TEXT("// %s\n"), *CCHeader.Name)); } if (Header.Bindings.NumSamplers > MaxMetalSamplers) { ShaderOutput.bSucceeded = false; FShaderCompilerError* NewError = new(OutErrors) FShaderCompilerError(); FString SamplerList; for (int32 i = 0; i < CCHeader.SamplerStates.Num(); i++) { auto const& Sampler = CCHeader.SamplerStates[i]; SamplerList += FString::Printf(TEXT("%d:%s\n"), Sampler.Index, *Sampler.Name); } NewError->StrippedErrorMessage = FString::Printf(TEXT("shader uses %d (%d) samplers exceeding the limit of %d\nSamplers:\n%s"), Header.Bindings.NumSamplers, CCHeader.SamplerStates.Num(), MaxMetalSamplers, *SamplerList); } // TODO read from toolchain? this check isn't really doing exactly what it says else if(CompileProcessAllowsRuntimeShaderCompiling(ShaderInput) && !bUseMetalShaderConverter) { // Write out the header and shader source code. FMemoryWriter Ar(ShaderOutput.ShaderCode.GetWriteAccess(), true); uint8 PrecompiledFlag = 0; Ar << PrecompiledFlag; Header.Serialize(Ar, SRT); Ar.Serialize((void*)USFSource, SourceLen + 1 - (USFSource - InShaderSource)); ShaderOutput.ModifiedShaderSource = MetalCode; if(!bBindlessEnabled) { ShaderOutput.NumInstructions = NumLines; } ShaderOutput.NumTextureSamplers = Header.Bindings.NumSamplers; ShaderOutput.bSucceeded = true; } else { // TODO technically should probably check the version of the metal compiler to make sure it's recent enough to support -MO. FString DebugInfo = TEXT(""); if (ShaderInput.Environment.CompilerFlags.Contains(CFLAG_ExtraShaderData)) { DebugInfo = TEXT("-gline-tables-only -MO"); } FString MathMode = bNoFastMath ? TEXT("-fno-fast-math") : TEXT("-ffast-math"); // at this point, the shader source is ready to be compiled // we will need a working temp directory. const FString& TempDir = FMetalCompilerToolchain::Get()->GetLocalTempDir(); int32 ReturnCode = 0; FString Results; FString Errors; bool bSucceeded = false; EShaderPlatform ShaderPlatform = EShaderPlatform(ShaderInput.Target.Platform); const bool bMetalCompilerAvailable = FMetalCompilerToolchain::Get()->IsCompilerAvailable(); bool bDebugInfoSucceded = false; FMetalShaderBytecode Bytecode; FMetalShaderDebugInfo DebugCode; FString HashedName = FString::Printf(TEXT("%u_%u"), SourceCRCLen, SourceCRC); if(!bMetalCompilerAvailable) { // No Metal Compiler - just put the source code directly into /tmp and report error - we are now using text shaders when this was not the requested configuration // Move it into place using an atomic move - ensures only one compile "wins" FString InputFilename = (TempDir / HashedName) + FMetalCompilerToolchain::MetalExtention; FString SaveFile = FPaths::CreateTempFilename(*TempDir, TEXT("ShaderTemp"), TEXT("")); FFileHelper::SaveStringToFile(MetalCode, *SaveFile); IFileManager::Get().Move(*InputFilename, *SaveFile, false, false, true, true); IFileManager::Get().Delete(*SaveFile); TCHAR const* Message = nullptr; if (PLATFORM_MAC) { Message = TEXT("Xcode's metal shader compiler was not found, verify Xcode has been installed on this Mac and that it has been selected in Xcode > Preferences > Locations > Command-line Tools."); } FShaderCompilerError* Error = new(OutErrors) FShaderCompilerError(); Error->ErrorVirtualFilePath = InputFilename; Error->ErrorLineString = TEXT("0"); Error->StrippedErrorMessage = FString(Message); } #if UE_METAL_USE_METAL_SHADER_CONVERTER else if (bUseMetalShaderConverter) { // The base name (which is /CRCHash_Length) FString BaseFileName = FPaths::Combine(TempDir, HashedName); FString MetalFileName = BaseFileName + FMetalCompilerToolchain::MetalExtention; Bytecode.NativePath = MetalFileName; Bytecode.ObjectFile = CompiledShaderBytecode.ObjectFile; Bytecode.OutputFile = CompiledShaderBytecode.OutputFile; bSucceeded = true; } #endif else { // Compiler available - more intermediate files will be created. // TODO How to handle multiple streams on the same machine? Needs more uniqueness in the temp dir const FString& CompilerVersionString = FMetalCompilerToolchain::Get()->GetCompilerVersionString(ShaderPlatform); // The base name (which is /CRCHash_Length) FString BaseFileName = FPaths::Combine(TempDir, HashedName); // The actual metal shadertext, a .metal file FString MetalFileName = BaseFileName + FMetalCompilerToolchain::MetalExtention; // The compiled shader, as AIR. An .air file. FString AIRFileName = BaseFileName + FMetalCompilerToolchain::MetalObjectExtension; // A metallib containing just this shader (gross). A .metallib file. FString MetallibFileName = BaseFileName + FMetalCompilerToolchain::MetalLibraryExtension; // 'MetalCode' contains the cross compiled MetalSL version of the shader. // Save it out. { // This is the previous behavior, which attempts an atomic move in case there is a race here. // TODO can we ever actually race on this? TempDir should be process specific now. FString SaveFile = FPaths::CreateTempFilename(*TempDir, TEXT("ShaderTemp"), TEXT("")); bool bSuccess = FFileHelper::SaveStringToFile(MetalCode, *SaveFile); if (!bSuccess) { UE_LOG(LogMetalShaderCompiler, Fatal, TEXT("Failed to write Metal shader out to %s\nShaderText:\n%s"), *SaveFile, *MetalCode); } bSuccess = IFileManager::Get().Move(*MetalFileName, *SaveFile, false, false, true, true); if (!bSuccess && !FPaths::FileExists(MetalFileName)) { UE_LOG(LogMetalShaderCompiler, Fatal, TEXT("Failed to move %s to %s"), *SaveFile, *MetalFileName); } if (FPaths::FileExists(SaveFile)) { IFileManager::Get().Delete(*SaveFile); } } // For iOS 14.0+ this is required. Version==0 is IOSMetalSLStandard_Minimum const bool bPreserveInvariance = Frequency == SF_Vertex && (Version == 0 || Version > 5); // TODO This is the actual MetalSL -> AIR piece FMetalShaderBytecodeJob Job; Job.IncludeDir = TempDir; Job.ShaderFormat = ShaderInput.ShaderFormat; Job.TmpFolder = TempDir; Job.InputFile = MetalFileName; Job.OutputFile = MetallibFileName; Job.OutputObjectFile = AIRFileName; Job.CompilerVersion = CompilerVersionString; Job.MinOSVersion = MinOSVersion; Job.PreserveInvariance = bPreserveInvariance ? TEXT("-fpreserve-invariance") : TEXT(""); Job.DebugInfo = DebugInfo; Job.MathMode = MathMode; Job.Standard = Standard; Job.SourceCRCLen = SourceCRCLen; Job.SourceCRC = SourceCRC; Job.bRetainObjectFile = ShaderInput.Environment.CompilerFlags.Contains(CFLAG_Archive); Job.bOptimizeForSize = ShaderInput.Environment.GetCompileArgument(TEXT("METAL_OPTIMIZE_FOR_SIZE"), false); Job.bCompileAsPCH = false; Job.ReturnCode = 0; // Module cache path to ensure that we don't have clashes across multiple instances. FString InputHashString; BytesToHex(ShaderInput.Hash.GetBytes(), 8, InputHashString); const FString& ModulesCachePath = TempDir / TEXT("ModuleCache") / InputHashString; Job.ModuleCacheDirectory = ModulesCachePath; bSucceeded = FMetalCompilerToolchain::Get()->CompileMetalShader(Job, Bytecode); if (bSucceeded) { if (!bIsMobile && !ShaderInput.Environment.CompilerFlags.Contains(CFLAG_Archive)) { uint32 CodeSize = FCStringAnsi::Strlen(TCHAR_TO_UTF8(*MetalCode)) + 1; int32 CompressedSize = FCompression::CompressMemoryBound(NAME_Zlib, CodeSize); DebugCode.CompressedData.SetNum(CompressedSize); if (FCompression::CompressMemory(NAME_Zlib, DebugCode.CompressedData.GetData(), CompressedSize, TCHAR_TO_UTF8(*MetalCode), CodeSize)) { DebugCode.UncompressedSize = CodeSize; DebugCode.CompressedData.SetNum(CompressedSize); DebugCode.CompressedData.Shrink(); } } } else { FShaderCompilerError* Error = new(OutErrors) FShaderCompilerError(); Error->ErrorVirtualFilePath = MetalFileName; Error->ErrorLineString = TEXT("0"); Error->StrippedErrorMessage = Job.Message; } // Clean up the .metal file after building it IFileManager::Get().Delete(*MetalFileName); } if (bSucceeded) { // Write out the header and compiled shader code FMemoryWriter Ar(ShaderOutput.ShaderCode.GetWriteAccess(), true); uint8 PrecompiledFlag = 1; Ar << PrecompiledFlag; Header.Serialize(Ar, SRT); // jam it into the output bytes Ar.Serialize(Bytecode.OutputFile.GetData(), Bytecode.OutputFile.Num()); if (ShaderInput.Environment.CompilerFlags.Contains(CFLAG_Archive)) { ShaderOutput.ShaderCode.AddOptionalData(EShaderOptionalDataKey::ObjectFile, Bytecode.ObjectFile.GetData(), Bytecode.ObjectFile.Num()); } if (bDebugInfoSucceded && !ShaderInput.Environment.CompilerFlags.Contains(CFLAG_Archive) && DebugCode.CompressedData.Num()) { ShaderOutput.ShaderCode.AddOptionalData(EShaderOptionalDataKey::CompressedDebugCode, DebugCode.CompressedData.GetData(), DebugCode.CompressedData.Num()); ShaderOutput.ShaderCode.AddOptionalData(EShaderOptionalDataKey::NativePath, TCHAR_TO_UTF8(*Bytecode.NativePath)); ShaderOutput.ShaderCode.AddOptionalData(EShaderOptionalDataKey::UncompressedSize, (const uint8*)&DebugCode.UncompressedSize, sizeof(DebugCode.UncompressedSize)); } if (ShaderInput.Environment.CompilerFlags.Contains(CFLAG_ExtraShaderData)) { // store data we can pickup later with ShaderCode.FindOptionalData(FShaderCodeName::Key), could be removed for shipping ShaderOutput.ShaderCode.AddOptionalData(FShaderCodeName::Key, TCHAR_TO_UTF8(*ShaderInput.GenerateShaderName())); if (DebugCode.CompressedData.Num() == 0) { ShaderOutput.ShaderCode.AddOptionalData(EShaderOptionalDataKey::SourceCode, TCHAR_TO_UTF8(*MetalCode)); ShaderOutput.ShaderCode.AddOptionalData(EShaderOptionalDataKey::NativePath, TCHAR_TO_UTF8(*Bytecode.NativePath)); } } ShaderOutput.NumTextureSamplers = Header.Bindings.NumSamplers; } ShaderOutput.ModifiedShaderSource = MetalCode; if(!bBindlessEnabled) { ShaderOutput.NumInstructions = NumLines; } ShaderOutput.bSucceeded = bSucceeded; } } /*------------------------------------------------------------------------------ External interface. ------------------------------------------------------------------------------*/ bool PreprocessMetalShader(const FShaderCompilerInput& Input, const FShaderCompilerEnvironment& Environment, FShaderPreprocessOutput& PreprocessOutput) { const EShaderFrequency Frequency = (EShaderFrequency)Input.Target.Frequency; if (!(Frequency == SF_Vertex || Frequency == SF_Pixel || Frequency == SF_Compute #if UE_METAL_USE_METAL_SHADER_CONVERTER || Frequency == SF_Geometry || Frequency == SF_Mesh || Frequency == SF_Amplification #endif )) { PreprocessOutput.LogError(FString::Printf( TEXT("%s shaders not supported for use in Metal."), CrossCompiler::GetFrequencyName(Frequency) )); return false; } return UE::ShaderCompilerCommon::ExecuteShaderPreprocessingSteps(PreprocessOutput, Input, Environment); } void CompileMetalShader(const FShaderCompilerInput& Input, const FShaderPreprocessOutput& InPreprocessOutput, FShaderCompilerOutput& Output) { FString EntryPointName = Input.EntryPointName; FString PreprocessedSource(InPreprocessOutput.GetSourceViewWide()); FShaderParameterParser::FPlatformConfiguration PlatformConfiguration; FShaderParameterParser ShaderParameterParser(PlatformConfiguration); if (!ShaderParameterParser.ParseAndModify(Input, Output.Errors, PreprocessedSource)) { // The FShaderParameterParser will add any relevant errors. return; } if (ShaderParameterParser.DidModifyShader()) { Output.ModifiedShaderSource = PreprocessedSource; } EMetalGPUSemantics Semantics = EMetalGPUSemanticsMobile; if (Input.ShaderFormat == NAME_SF_METAL_SM5_IOS || Input.ShaderFormat == NAME_SF_METAL_SM5_TVOS) { Semantics = EMetalGPUSemanticsTBDRDesktop; } else if (Input.ShaderFormat == NAME_SF_METAL_ES3_1 || Input.ShaderFormat == NAME_SF_METAL_SM5 || Input.ShaderFormat == NAME_SF_METAL_SM6) { Semantics = EMetalGPUSemanticsImmediateDesktop; } uint32 VersionEnum = GMetalDefaultShadingLanguageVersion; bool bFoundVersion = Input.Environment.GetCompileArgument(TEXT("SHADER_LANGUAGE_VERSION"), VersionEnum); if (!bFoundVersion) { new(Output.Errors) FShaderCompilerError(*FString::Printf(TEXT("Missing SHADER_LANGUAGE_VERSION compile argument; Falling back to default value %d"), GMetalDefaultShadingLanguageVersion)); } // TODO read from toolchain const bool bIsMobile = FMetalCompilerToolchain::Get()->IsMobile((EShaderPlatform)Input.Target.Platform); const bool bAppleTV = (Input.ShaderFormat == NAME_SF_METAL_ES3_1_TVOS || Input.ShaderFormat == NAME_SF_METAL_SM5_TVOS); const bool bIsSimulator = (Input.ShaderFormat == NAME_SF_METAL_SIM); FString MinOSVersion; FString StandardVersion; switch (VersionEnum) { case 9: StandardVersion = TEXT("3.1"); if (bAppleTV) { MinOSVersion = TEXT("-mtvos-version-min=17.0"); } else if (bIsMobile) { if (bIsSimulator) { MinOSVersion = TEXT("-miphonesimulator-version-min=17.0"); } else { MinOSVersion = TEXT("-mios-version-min=17.0"); } } else { MinOSVersion = TEXT("-mmacosx-version-min=14"); } break; case 8: StandardVersion = TEXT("3.0"); if (bAppleTV) { MinOSVersion = TEXT("-mtvos-version-min=16.0"); } else if (bIsMobile) { if (bIsSimulator) { MinOSVersion = TEXT("-miphonesimulator-version-min=16.0"); } else { MinOSVersion = TEXT("-mios-version-min=16.0"); } } else { MinOSVersion = TEXT("-mmacosx-version-min=13"); } break; case 7: StandardVersion = TEXT("2.4"); if (bAppleTV) { MinOSVersion = TEXT("-mtvos-version-min=15.0"); } else if (bIsMobile) { if (bIsSimulator) { MinOSVersion = TEXT("-miphonesimulator-version-min=15.0"); } else { MinOSVersion = TEXT("-mios-version-min=15.0"); } } else { MinOSVersion = TEXT("-mmacosx-version-min=12"); } break; case 6: StandardVersion = TEXT("2.4"); if (bAppleTV) { MinOSVersion = TEXT("-mtvos-version-min=15.0"); } else if (bIsMobile) { if (bIsSimulator) { MinOSVersion = TEXT("-miphonesimulator-version-min=15.0"); } else { MinOSVersion = TEXT("-mios-version-min=15.0"); } } else { // TODO - This is a workaround for an issue with the Apple Shader Compiler // leading to corruption on M1/AMD when > 2.3 versions are used. // This should be bumped to 2.4 after it's resolved StandardVersion = TEXT("2.3"); MinOSVersion = TEXT("-mmacosx-version-min=11"); } break; case 5: // Fall through case 0: StandardVersion = TEXT("2.4"); if (bAppleTV) { MinOSVersion = TEXT("-mtvos-version-min=15.0"); } else if (bIsMobile) { if (bIsSimulator) { MinOSVersion = TEXT("-miphonesimulator-version-min=15.0"); } else { MinOSVersion = TEXT("-mios-version-min=15.0"); } } else { // TODO - This is a workaround for an issue with the Apple Shader Compiler // leading to corruption on M1/AMD when > 2.3 versions are used. // This should be bumped to 2.4 after it's resolved StandardVersion = TEXT("2.2"); MinOSVersion = TEXT("-mmacosx-version-min=10.15"); } //EIOSMetalShaderStandard::IOSMetalSLStandard_Minimum break; default: Output.bSucceeded = false; { FString EngineIdentifier = FEngineVersion::Current().ToString(EVersionComponent::Minor); FShaderCompilerError* NewError = new(Output.Errors) FShaderCompilerError(); NewError->StrippedErrorMessage = FString::Printf(TEXT("Minimum Metal Version is 2.4 in UE %s"), *EngineIdentifier); return; } break; } TCHAR const* StandardPlatform = bIsMobile ? TEXT("ios") : TEXT("macos"); FString Standard; if (VersionEnum >= 8) //-V547 { Standard = FString::Printf(TEXT("-std=metal%s"), *StandardVersion); } else { Standard = FString::Printf(TEXT("-std=%s-metal%s"), StandardPlatform, *StandardVersion); } if (EnumHasAnyFlags(Input.DebugInfoFlags, EShaderDebugInfoFlags::CompileFromDebugUSF)) { // force debug output on when compiling a debug dump usf const_cast(Input).DumpDebugInfoPath = FPaths::GetPath(Input.VirtualSourceFilePath); } const bool bDumpDebugInfo = Input.DumpDebugInfoEnabled(); // Allow the shader pipeline to override the platform default in here. uint32 MaxUnrollLoops = 32; if (Input.Environment.CompilerFlags.Contains(CFLAG_AvoidFlowControl)) { MaxUnrollLoops = 1024; // Max. permitted by hlslcc } else if (Input.Environment.CompilerFlags.Contains(CFLAG_PreferFlowControl)) { MaxUnrollLoops = 0; } #if UE_METAL_USE_METAL_SHADER_CONVERTER if (Input.IsBindlessEnabled() && Input.ShaderFormat == NAME_SF_METAL_SM6) { FMetalCompileShaderMSC::DoCompileMetalShader(Input, Output, PreprocessedSource, VersionEnum, Semantics, MaxUnrollLoops, (EShaderFrequency)Input.Target.Frequency, bDumpDebugInfo, Standard, MinOSVersion); } else #endif { FMetalCompileShaderSPIRV::DoCompileMetalShader(Input, Output, PreprocessedSource, VersionEnum, Semantics, MaxUnrollLoops, (EShaderFrequency)Input.Target.Frequency, bDumpDebugInfo, Standard, MinOSVersion); } ShaderParameterParser.ValidateShaderParameterTypes(Input, bIsMobile, Output); } bool StripShader_Metal(TArray& Code, class FString const& DebugPath, bool const bNative) { bool bSuccess = false; FShaderCodeReader ShaderCode(Code); FMemoryReader Ar(Code, true); Ar.SetLimitSize(ShaderCode.GetActualShaderCodeSize()); // was the shader already compiled offline? uint8 OfflineCompiledFlag; Ar << OfflineCompiledFlag; if(bNative && OfflineCompiledFlag == 1) { // get the header FMetalCodeHeader Header; FShaderResourceTable SRT; Header.Serialize(Ar, SRT); const FString ShaderName = ShaderCode.FindOptionalData(FShaderCodeName::Key); // Must be compiled for archiving or something is very wrong. if(bNative == false || Header.CompileFlags & (1 << CFLAG_Archive)) { bSuccess = true; // remember where the header ended and code (precompiled or source) begins int32 CodeOffset = Ar.Tell(); const uint8* SourceCodePtr = (uint8*)Code.GetData() + CodeOffset; // Copy the non-optional shader bytecode TArray SourceCode; SourceCode.Append(SourceCodePtr, ShaderCode.GetActualShaderCodeSize() - CodeOffset); const ANSICHAR* ShaderSource = ShaderCode.FindOptionalData(EShaderOptionalDataKey::SourceCode); const size_t ShaderSourceLength = ShaderSource ? FCStringAnsi::Strlen(ShaderSource) : 0; bool const bHasShaderSource = ShaderSourceLength > 0; const ANSICHAR* ShaderPath = ShaderCode.FindOptionalData(EShaderOptionalDataKey::NativePath); bool const bHasShaderPath = (ShaderPath && FCStringAnsi::Strlen(ShaderPath) > 0); if (bHasShaderSource && bHasShaderPath) { FString DebugFilePath = DebugPath / FString(ShaderPath); FString DebugFolderPath = FPaths::GetPath(DebugFilePath); if (IFileManager::Get().MakeDirectory(*DebugFolderPath, true)) { FString TempPath = FPaths::CreateTempFilename(*DebugFolderPath, TEXT("MetalShaderFile-"), TEXT(".metal")); IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); IFileHandle* FileHandle = PlatformFile.OpenWrite(*TempPath); if (FileHandle) { FileHandle->Write((const uint8 *)ShaderSource, ShaderSourceLength); delete FileHandle; IFileManager::Get().Move(*DebugFilePath, *TempPath, true, false, true, false); IFileManager::Get().Delete(*TempPath); } else { UE_LOG(LogShaders, Error, TEXT("Shader stripping failed: shader %s (Len: %0.8x, CRC: %0.8x) failed to create file %s!"), *ShaderName, Header.SourceLen, Header.SourceCRC, *TempPath); } } } if (bNative) { int32 ObjectSize = 0; const uint8* ShaderObject = ShaderCode.FindOptionalDataAndSize(EShaderOptionalDataKey::ObjectFile, ObjectSize); // If ShaderObject and ObjectSize is zero then the code has already been stripped - source code should be the byte code if(ShaderObject && ObjectSize) { TArray ObjectCodeArray; ObjectCodeArray.Append(ShaderObject, ObjectSize); SourceCode = ObjectCodeArray; } } // Strip any optional data if (bNative || ShaderCode.GetOptionalDataSize() > 0) { // Write out the header and compiled shader code FShaderCode NewCode; FMemoryWriter NewAr(NewCode.GetWriteAccess(), true); NewAr << OfflineCompiledFlag; Header.Serialize(NewAr, SRT); // jam it into the output bytes NewAr.Serialize(SourceCode.GetData(), SourceCode.Num()); Code = NewCode.GetReadView(); } } else { UE_LOG(LogShaders, Error, TEXT("Shader stripping failed: shader %s (Len: %0.8x, CRC: %0.8x) was not compiled for archiving into a native library (Native: %s, Compile Flags: %0.8x)!"), *ShaderName, Header.SourceLen, Header.SourceCRC, bNative ? TEXT("true") : TEXT("false"), (uint32)Header.CompileFlags); } } else { UE_LOG(LogShaders, Error, TEXT("Shader stripping failed: shader %s (Native: %s, Offline Compiled: %d) was not compiled to bytecode for native archiving!"), *DebugPath, bNative ? TEXT("true") : TEXT("false"), OfflineCompiledFlag); } return bSuccess; } uint64 AppendShader_Metal(FString const& WorkingDir, const FSHAHash& Hash, TArray& InShaderCode) { uint64 Id = 0; const bool bCompilerAvailable = FMetalCompilerToolchain::Get()->IsCompilerAvailable(); if (bCompilerAvailable) { // Parse the existing data and extract the source code. We have to recompile it FShaderCodeReader ShaderCode(InShaderCode); FMemoryReader Ar(InShaderCode, true); Ar.SetLimitSize(ShaderCode.GetActualShaderCodeSize()); // was the shader already compiled offline? uint8 OfflineCompiledFlag; Ar << OfflineCompiledFlag; if (OfflineCompiledFlag == 1) { // get the header FMetalCodeHeader Header; FShaderResourceTable SRT; Header.Serialize(Ar, SRT); const FString ShaderName = ShaderCode.FindOptionalData(FShaderCodeName::Key); // Must be compiled for archiving or something is very wrong. if(Header.CompileFlags & (1 << CFLAG_Archive)) { // remember where the header ended and code (precompiled or source) begins int32 CodeOffset = Ar.Tell(); const uint8* SourceCodePtr = (uint8*)InShaderCode.GetData() + CodeOffset; // Copy the non-optional shader bytecode int32 ObjectCodeDataSize = 0; uint8 const* Object = ShaderCode.FindOptionalDataAndSize(EShaderOptionalDataKey::ObjectFile, ObjectCodeDataSize); // 'o' segment missing this is a pre stripped shader if(!Object) { ObjectCodeDataSize = ShaderCode.GetActualShaderCodeSize() - CodeOffset; Object = SourceCodePtr; } TArrayView ObjectCodeArray(Object, ObjectCodeDataSize); // Object code segment FString ObjFilename = WorkingDir / FString::Printf(TEXT("Main_%0.8x_%0.8x.o"), Header.SourceLen, Header.SourceCRC); bool const bHasObjectData = (ObjectCodeDataSize > 0) || IFileManager::Get().FileExists(*ObjFilename); if (bHasObjectData) { // metal commandlines int32 ReturnCode = 0; FString Results; FString Errors; bool bHasObjectFile = IFileManager::Get().FileExists(*ObjFilename); if (ObjectCodeDataSize > 0) { // write out shader object code source (IR) for archiving to a single library file later if( FFileHelper::SaveArrayToFile(ObjectCodeArray, *ObjFilename) ) { bHasObjectFile = true; } } if (bHasObjectFile) { Id = ((uint64)Header.SourceLen << 32) | Header.SourceCRC; // This is going to get serialised into the shader resource archive we don't anything but the header info now with the archive flag set Header.CompileFlags |= (1 << CFLAG_Archive); // Write out the header and compiled shader code FShaderCode NewCode; FMemoryWriter NewAr(NewCode.GetWriteAccess(), true); NewAr << OfflineCompiledFlag; Header.Serialize(NewAr, SRT); InShaderCode = NewCode.GetReadView(); UE_LOG(LogShaders, Verbose, TEXT("Archiving succeeded: shader %s (Len: %0.8x, CRC: %0.8x, SHA: %s)"), *ShaderName, Header.SourceLen, Header.SourceCRC, *Hash.ToString()); } else { UE_LOG(LogShaders, Error, TEXT("Archiving failed: failed to write temporary file %s for shader %s (Len: %0.8x, CRC: %0.8x, SHA: %s)"), *ObjFilename, *ShaderName, Header.SourceLen, Header.SourceCRC, *Hash.ToString()); } } else { UE_LOG(LogShaders, Error, TEXT("Archiving failed: shader %s (Len: %0.8x, CRC: %0.8x, SHA: %s) has no object data"), *ShaderName, Header.SourceLen, Header.SourceCRC, *Hash.ToString()); } } else { UE_LOG(LogShaders, Error, TEXT("Archiving failed: shader %s (Len: %0.8x, CRC: %0.8x, SHA: %s) was not compiled for archiving (Compile Flags: %0.8x)!"), *ShaderName, Header.SourceLen, Header.SourceCRC, *Hash.ToString(), (uint32)Header.CompileFlags); } } else { UE_LOG(LogShaders, Error, TEXT("Archiving failed: shader SHA: %s was not compiled to bytecode (%d)!"), *Hash.ToString(), OfflineCompiledFlag); } } else { UE_LOG(LogShaders, Error, TEXT("Archiving failed: no Xcode install on the local machine or a remote Mac.")); } return Id; } bool FinalizeLibrary_Metal(FName const& Format, FString const& WorkingDir, FString const& LibraryPath, TSet const& Shaders, class FString const& DebugOutputDir) { bool bOK = false; FString FullyQualifiedWorkingDir = FPaths::ConvertRelativePathToFull(WorkingDir); const FMetalCompilerToolchain* Toolchain = FMetalCompilerToolchain::Get(); const bool bCompilerAvailable = Toolchain->IsCompilerAvailable(); EShaderPlatform Platform = FMetalCompilerToolchain::MetalShaderFormatToLegacyShaderPlatform(Format); const EAppleSDKType SDK = FMetalCompilerToolchain::MetalShaderPlatformToSDK(Platform); bool bCompiledWithMetalShaderConverter = FDataDrivenShaderPlatformInfo::GetSupportsBindless(Platform); if (bCompilerAvailable) { int32 ReturnCode = 0; FString Results; FString Errors; // WARNING: This may be called from multiple threads using the same WorkingDir. All the temporary files must be uniquely named. // The local path that will end up with the archive. FString LocalArchivePath = FPaths::CreateTempFilename(*FullyQualifiedWorkingDir, TEXT("MetalArchive"), TEXT("")) + TEXT(".metalar"); IFileManager::Get().Delete(*LocalArchivePath); IFileManager::Get().Delete(*LibraryPath); UE_LOG(LogMetalShaderCompiler, Display, TEXT("Creating Native Library %s"), *LibraryPath); bool bArchiveFileValid = false; #if UE_METAL_USE_METAL_SHADER_CONVERTER if (bCompiledWithMetalShaderConverter) { // Merge .metallib into a single .metallib. // Number of air per batch (limited by PlatformProcessLimits::MaxArgvParameters). static constexpr int32 NumArgcPerBatch = 96; TArray AirPackArgsBatches; FString AirPackArgs; uint32 CurPackArgsArgc = 0; uint32 Index = 0; for (auto Shader : Shaders) { uint32 Len = (Shader >> 32); uint32 CRC = (Shader & 0xffffffff); FString FileName = FString::Printf(TEXT("Main_%0.8x_%0.8x.o"), Len, CRC); UE_LOG(LogMetalShaderCompiler, Verbose, TEXT("[%d/%d] %s %s"), ++Index, Shaders.Num(), *Format.GetPlainNameString(), *FileName); FString SourceFilePath = FString::Printf(TEXT("\"%s/%s\""), *FullyQualifiedWorkingDir, *FileName); AirPackArgs += FString::Printf(TEXT("%s "), *SourceFilePath); CurPackArgsArgc++; if (CurPackArgsArgc > NumArgcPerBatch) { AirPackArgsBatches.Add(AirPackArgs); AirPackArgs.Empty(); // TODO: Might switch to SetNum(0); as Empty() implicitly reallocs (IIRC) CurPackArgsArgc = 0; } } // Add pending batch to the list. if (AirPackArgs.Len() > 0) { AirPackArgsBatches.Add(AirPackArgs); } bArchiveFileValid = (Shaders.Num() > 0); { // handle compile error if (ReturnCode == 0 && bArchiveFileValid) { // AirPack each batch. FString BatchMergeArgs; for (int32 BatchIdx = 0; BatchIdx < AirPackArgsBatches.Num(); BatchIdx++) { FString BatchOutputFile = FString::Printf(TEXT("AirPackBatch_%d_"), BatchIdx); FString BatchOutputPath = FPaths::CreateTempFilename(*FullyQualifiedWorkingDir, *BatchOutputFile); BatchMergeArgs += FString::Printf(TEXT("\"%s\" "), *BatchOutputPath); UE_LOG(LogMetalShaderCompiler, Display, TEXT("[%d/%d] %s"), (BatchIdx + 1), AirPackArgsBatches.Num(), *BatchOutputPath); FString AirPackParams = FString::Printf(TEXT("-pack-metallibs internal -pack-descriptors internal -pack-reflections internal %s -o \"%s\""), *AirPackArgsBatches[BatchIdx], *BatchOutputPath); ReturnCode = 0; Results.Empty(); Errors.Empty(); bool bSuccess = Toolchain->ExecAirPack(SDK, *AirPackParams, &ReturnCode, &Results, &Errors); // handle compile error if (!bSuccess || ReturnCode != 0) { UE_LOG(LogShaders, Error, TEXT("Archiving failed: air-pack failed with code %d: %s %s"), ReturnCode, *Results, *Errors); } } // Final pass: merge all batches into a single lib. UE_LOG(LogMetalShaderCompiler, Display, TEXT("Post-processing archive for shader platform: %s"), *Format.GetPlainNameString()); FString LocalMetalLibPath = LibraryPath; if (FPaths::FileExists(LocalMetalLibPath)) { UE_LOG(LogMetalShaderCompiler, Warning, TEXT("Archiving warning: target metallib already exists and will be overwritten: %s"), *LocalMetalLibPath); } FString AirPackParams = FString::Printf(TEXT("-pack-metallibs internal -pack-descriptors internal -pack-reflections internal %s -o \"%s\""), *BatchMergeArgs, *LocalMetalLibPath); ReturnCode = 0; Results.Empty(); Errors.Empty(); bool bSuccess = Toolchain->ExecAirPack(SDK, *AirPackParams, &ReturnCode, &Results, &Errors); // handle compile error if (bSuccess && ReturnCode == 0) { check(LocalMetalLibPath == LibraryPath); bOK = (IFileManager::Get().FileSize(*LibraryPath) > 0); if (!bOK) { UE_LOG(LogShaders, Error, TEXT("Archiving failed: failed to copy to local destination: %s"), *LibraryPath); } } else { UE_LOG(LogShaders, Error, TEXT("Archiving failed: air-pack failed with code %d: %s %s"), ReturnCode, *Results, *Errors); } } else { UE_LOG(LogShaders, Error, TEXT("Archiving failed: no valid input for air-pack.")); } } } else #endif { // Archive build phase - like unix ar, build metal archive from all the object files UE_LOG(LogMetalShaderCompiler, Display, TEXT("Archiving %d shaders for shader platform: %s"), Shaders.Num(), *Format.GetPlainNameString()); /* AR type utils can use 'M' scripts They look like this: CREATE MyArchive.metalar ADDMOD Shader1.metal ADDMOD Shader2.metal SAVE END */ FString M_Script = FString::Printf(TEXT("CREATE \"%s\"\n"), *FPaths::ConvertRelativePathToFull(LocalArchivePath)); uint32 Index = 0; for (auto Shader : Shaders) { uint32 Len = (Shader >> 32); uint32 CRC = (Shader & 0xffffffff); FString FileName = FString::Printf(TEXT("Main_%0.8x_%0.8x.o"), Len, CRC); UE_LOG(LogMetalShaderCompiler, Verbose, TEXT("[%d/%d] %s %s"), ++Index, Shaders.Num(), *Format.GetPlainNameString(), *FileName); FString SourceFilePath = FString::Printf(TEXT("\"%s/%s\""), *FullyQualifiedWorkingDir, *FileName); M_Script += FString::Printf(TEXT("ADDMOD %s\n"), *SourceFilePath); } M_Script += FString(TEXT("SAVE\n")); M_Script += FString(TEXT("END\n")); FString LocalScriptFilePath = FPaths::CreateTempFilename(*FullyQualifiedWorkingDir, TEXT("MetalArScript"), TEXT(".M")); FFileHelper::SaveStringToFile(M_Script, *LocalScriptFilePath); if (!FPaths::FileExists(LocalScriptFilePath)) { UE_LOG(LogMetalShaderCompiler, Error, TEXT("Failed to create metal-ar .M script at %s"), *LocalScriptFilePath); return false; } FPaths::MakePlatformFilename(LocalScriptFilePath); bool bSuccess = Toolchain->ExecMetalAr(SDK, *LocalScriptFilePath, &ReturnCode, &Results, &Errors); bArchiveFileValid = FPaths::FileExists(LocalArchivePath); if (ReturnCode != 0 || !bArchiveFileValid) { UE_LOG(LogMetalShaderCompiler, Error, TEXT("Archiving failed: metal-ar failed with code %d: %s %s"), ReturnCode, *Results, *Errors); return false; } } // Lib build phase, metalar to metallib #if UE_METAL_USE_METAL_SHADER_CONVERTER if (!bCompiledWithMetalShaderConverter) #endif { // handle compile error if (ReturnCode == 0 && bArchiveFileValid) { UE_LOG(LogMetalShaderCompiler, Display, TEXT("Post-processing archive for shader platform: %s"), *Format.GetPlainNameString()); FString LocalMetalLibPath = LibraryPath; if (FPaths::FileExists(LocalMetalLibPath)) { UE_LOG(LogMetalShaderCompiler, Warning, TEXT("Archiving warning: target metallib already exists and will be overwritten: %s"), *LocalMetalLibPath); } FString MetallibParams = FString::Printf(TEXT("-o \"%s\" \"%s\""), *LocalMetalLibPath, *LocalArchivePath); ReturnCode = 0; Results.Empty(); Errors.Empty(); bool bSuccess = Toolchain->ExecMetalLib(SDK, *MetallibParams, &ReturnCode, &Results, &Errors); // handle compile error if (bSuccess && ReturnCode == 0) { check(LocalMetalLibPath == LibraryPath); bOK = (IFileManager::Get().FileSize(*LibraryPath) > 0); if (!bOK) { UE_LOG(LogShaders, Error, TEXT("Archiving failed: failed to copy to local destination: %s"), *LibraryPath); } } else { UE_LOG(LogShaders, Error, TEXT("Archiving failed: metallib failed with code %d: %s %s"), ReturnCode, *Results, *Errors); } } else { UE_LOG(LogShaders, Error, TEXT("Archiving failed: no valid input for metallib.")); } } } else { UE_LOG(LogShaders, Error, TEXT("Archiving failed: no Xcode install.")); } return bOK; } static void ReplaceString(FAnsiString& Str, int32 Pos, int Len, const FAnsiString& NewStr) { Str.RemoveAt(Pos, Len, EAllowShrinking::No); Str.InsertAt(Pos, NewStr); } // Replace the special texture "gl_LastFragData" to a native subpass fetch operation. Returns true if the input source has been modified. bool PatchSpecialTextureInHlslSource(FAnsiString& SourceData, uint32* OutSubpassInputsDim, uint32 SubpassInputDimCount) { bool bSourceDataWasModified = false; // Invalidate output parameter for dimension of subpass input attachemnt at slot 0 (primary slot for "gl_LastFragData"). FMemory::Memzero(OutSubpassInputsDim, sizeof(uint32) * SubpassInputDimCount); // Check if special texture is present in the code const char* GSpecialTextureLastFragData = "gl_LastFragData"; if (SourceData.Find(GSpecialTextureLastFragData, ESearchCase::CaseSensitive) != INDEX_NONE) { struct FHlslVectorType { const char* TypenameIdent; const char* TypenameSuffix; uint32 Dimension; }; const FHlslVectorType FragDeclTypes[4] = { { "float4", "RGBA", 4 }, { "float", "R", 1 }, { "half4", "RGBA", 4 }, { "half", "R", 1 } }; // Replace declaration of special texture with corresponding 'SubpassInput' declaration with respective dimension, i.e. float, float4, etc. for (uint32 SubpassIndex = 0; SubpassIndex < SubpassInputDimCount; SubpassIndex++) { for (const FHlslVectorType& FragDeclType : FragDeclTypes) { // Try to find "Texture2D" or "Texture2D< T >" (where T is the vector type), because a rewritten HLSL might have changed the formatting. FAnsiString LastFragDataN = FAnsiString::Printf("%s%s_%u", GSpecialTextureLastFragData, FragDeclType.TypenameSuffix, SubpassIndex); FAnsiString FragDecl = FAnsiString::Printf("Texture2D<%s> %s;", FragDeclType.TypenameIdent, *LastFragDataN); int32 FragDeclIncludePos = SourceData.Find(FragDecl, ESearchCase::CaseSensitive); if (FragDeclIncludePos == INDEX_NONE) { FragDecl = FAnsiString::Printf("Texture2D< %s > %s;", FragDeclType.TypenameIdent, *LastFragDataN); FragDeclIncludePos = SourceData.Find(FragDecl, ESearchCase::CaseSensitive); } if (FragDeclIncludePos != INDEX_NONE) { // Replace declaration of Texture2D with SubpassInput ReplaceString(SourceData, FragDeclIncludePos, FragDecl.Len(), FAnsiString::Printf("[[vk::input_attachment_index(%d)]] SubpassInput<%s> %s;", SubpassIndex, FragDeclType.TypenameIdent, *LastFragDataN)); OutSubpassInputsDim[SubpassIndex] = FragDeclType.Dimension; // Replace all uses of special texture by 'SubpassLoad' operation FAnsiString FragLoad = FAnsiString::Printf("%s.Load(uint3(0, 0, 0), 0)", *LastFragDataN); for (int32 FragLoadIncludePos = 0; (FragLoadIncludePos = SourceData.Find(FragLoad, ESearchCase::CaseSensitive, ESearchDir::FromStart, FragLoadIncludePos)) != INDEX_NONE;) { ReplaceString(SourceData, FragLoadIncludePos, FragLoad.Len(), FAnsiString::Printf("%s.SubpassLoad()", *LastFragDataN)); } // Mark source data as being modified bSourceDataWasModified = true; break; } } } } return bSourceDataWasModified; }