// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. // .. #include "MetalShaderFormat.h" #include "Core.h" #include "ShaderCore.h" #include "MetalShaderResources.h" #include "ShaderCompilerCommon.h" #if PLATFORM_WINDOWS #include "AllowWindowsPlatformTypes.h" #include "Windows/PreWindowsApi.h" #include #include #include #include "Windows/PostWindowsApi.h" #include "Windows/MinWindows.h" #include "HideWindowsPlatformTypes.h" #endif #include "ShaderPreprocessor.h" #include "hlslcc.h" #include "MetalBackend.h" DEFINE_LOG_CATEGORY_STATIC(LogMetalShaderCompiler, Log, All); /*------------------------------------------------------------------------------ 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; } /** * 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. */ static void BuildMetalShaderOutput( FShaderCompilerOutput& ShaderOutput, const FShaderCompilerInput& ShaderInput, const ANSICHAR* InShaderSource, int32 SourceLen, bool bIsMobile, TArray& OutErrors ) { const ANSICHAR* USFSource = InShaderSource; CrossCompiler::FHlslccHeader CCHeader; if (!CCHeader.Read(USFSource, SourceLen)) { UE_LOG(LogMetalShaderCompiler, Fatal, TEXT("Bad hlslcc header found")); } FMetalCodeHeader Header = {0}; FShaderParameterMap& ParameterMap = ShaderOutput.ParameterMap; EShaderFrequency Frequency = (EShaderFrequency)ShaderOutput.Target.Frequency; 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 |= (1 << AttributeIndex); } } } // Then the list of outputs. static const FString TargetPrefix = "out_Target"; static const FString GL_FragDepth = "gl_FragDepth"; // 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 |= (1 << TargetIndex); } // Handle depth writes. else if (Output.Name.Equals(GL_FragDepth)) { Header.Bindings.InOutMask |= 0x8000; } } } bool bHasRegularUniformBuffers = false; // Then 'normal' uniform buffers. for (auto& UniformBlock : CCHeader.UniformBlocks) { uint16 UBIndex = UniformBlock.Index; if (UBIndex >= Header.Bindings.NumUniformBuffers) { Header.Bindings.NumUniformBuffers = UBIndex + 1; } UsedUniformBufferSlots[UBIndex] = true; ParameterMap.AddParameterAllocation(*UniformBlock.Name, UBIndex, 0, 0); bHasRegularUniformBuffers = true; } // Packed global uniforms const uint16 BytesPerComponent = 4; TMap PackedGlobalArraySize; for (auto& PackedGlobal : CCHeader.PackedGlobals) { ParameterMap.AddParameterAllocation( *PackedGlobal.Name, PackedGlobal.PackedType, PackedGlobal.Offset * BytesPerComponent, PackedGlobal.Count * BytesPerComponent ); uint16& Size = PackedGlobalArraySize.FindOrAdd(PackedGlobal.PackedType); Size = FMath::Max(BytesPerComponent * (PackedGlobal.Offset + PackedGlobal.Count), Size); } // Packed Uniform Buffers TMap > PackedUniformBuffersSize; for (auto& PackedUB : CCHeader.PackedUBs) { check(PackedUB.Attribute.Index == Header.Bindings.NumUniformBuffers); UsedUniformBufferSlots[PackedUB.Attribute.Index] = true; ParameterMap.AddParameterAllocation(*PackedUB.Attribute.Name, Header.Bindings.NumUniformBuffers++, 0, 0); // Nothing else... //for (auto& Member : PackedUB.Members) //{ //} } // Packed Uniform Buffers copy lists & setup sizes for each UB/Precision entry for (auto& PackedUBCopy : CCHeader.PackedUBCopies) { CrossCompiler::FUniformBufferCopyInfo CopyInfo; CopyInfo.SourceUBIndex = PackedUBCopy.SourceUB; CopyInfo.SourceOffsetInFloats = PackedUBCopy.SourceOffset; CopyInfo.DestUBIndex = PackedUBCopy.DestUB; CopyInfo.DestUBTypeName = PackedUBCopy.DestPackedType; CopyInfo.DestUBTypeIndex = CrossCompiler::PackedTypeNameToTypeIndex(CopyInfo.DestUBTypeName); CopyInfo.DestOffsetInFloats = PackedUBCopy.DestOffset; CopyInfo.SizeInFloats = PackedUBCopy.Count; Header.UniformBuffersCopyInfo.Add(CopyInfo); auto& UniformBufferSize = PackedUniformBuffersSize.FindOrAdd(CopyInfo.DestUBIndex); uint16& Size = UniformBufferSize.FindOrAdd(CopyInfo.DestUBTypeName); Size = FMath::Max(BytesPerComponent * (CopyInfo.DestOffsetInFloats + CopyInfo.SizeInFloats), Size); } for (auto& PackedUBCopy : CCHeader.PackedUBGlobalCopies) { CrossCompiler::FUniformBufferCopyInfo CopyInfo; CopyInfo.SourceUBIndex = PackedUBCopy.SourceUB; CopyInfo.SourceOffsetInFloats = PackedUBCopy.SourceOffset; CopyInfo.DestUBIndex = PackedUBCopy.DestUB; CopyInfo.DestUBTypeName = PackedUBCopy.DestPackedType; CopyInfo.DestUBTypeIndex = CrossCompiler::PackedTypeNameToTypeIndex(CopyInfo.DestUBTypeName); CopyInfo.DestOffsetInFloats = PackedUBCopy.DestOffset; CopyInfo.SizeInFloats = PackedUBCopy.Count; Header.UniformBuffersCopyInfo.Add(CopyInfo); uint16& Size = PackedGlobalArraySize.FindOrAdd(CopyInfo.DestUBTypeName); Size = FMath::Max(BytesPerComponent * (CopyInfo.DestOffsetInFloats + CopyInfo.SizeInFloats), Size); } Header.Bindings.bHasRegularUniformBuffers = bHasRegularUniformBuffers; // 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 = CrossCompiler::PackedTypeNameToTypeIndex(TypeName); Header.Bindings.PackedGlobalArrays.Add(Info); } // Setup Packed Uniform Buffers info Header.Bindings.PackedUniformBuffers.Reserve(PackedUniformBuffersSize.Num()); for (auto Iterator = PackedUniformBuffersSize.CreateIterator(); Iterator; ++Iterator) { int BufferIndex = Iterator.Key(); auto& ArraySizes = Iterator.Value(); TArray InfoArray; InfoArray.Reserve(ArraySizes.Num()); for (auto IterSizes = ArraySizes.CreateIterator(); IterSizes; ++IterSizes) { ANSICHAR TypeName = IterSizes.Key(); uint16 Size = IterSizes.Value(); Size = (Size + 0xf) & (~0xf); CrossCompiler::FPackedArrayInfo Info; Info.Size = Size; Info.TypeName = TypeName; Info.TypeIndex = CrossCompiler::PackedTypeNameToTypeIndex(TypeName); InfoArray.Add(Info); } Header.Bindings.PackedUniformBuffers.Add(InfoArray); } // Then samplers. TMap SamplerMap; for (auto& Sampler : CCHeader.Samplers) { ParameterMap.AddParameterAllocation( *Sampler.Name, 0, Sampler.Offset, Sampler.Count ); Header.Bindings.NumSamplers = FMath::Max( Header.Bindings.NumSamplers, Sampler.Offset + Sampler.Count ); for (auto& SamplerState : Sampler.SamplerStates) { SamplerMap.Add(SamplerState, Sampler.Count); } } // Then UAVs (images in Metal) for (auto& UAV : CCHeader.UAVs) { ParameterMap.AddParameterAllocation( *UAV.Name, 0, UAV.Offset, UAV.Count ); Header.Bindings.NumUAVs = FMath::Max( Header.Bindings.NumSamplers, UAV.Offset + UAV.Count ); } for (auto& SamplerState : CCHeader.SamplerStates) { ParameterMap.AddParameterAllocation( *SamplerState.Name, 0, SamplerState.Index, SamplerMap[SamplerState.Name] ); } Header.NumThreadsX = CCHeader.NumThreads[0]; Header.NumThreadsY = CCHeader.NumThreads[1]; Header.NumThreadsZ = CCHeader.NumThreads[2]; // Build the SRT for this shader. { // Build the generic SRT for this shader. FShaderResourceTable GenericSRT; BuildResourceTableMapping(ShaderInput.Environment.ResourceTableMap, ShaderInput.Environment.ResourceTableLayoutHashes, UsedUniformBufferSlots, ShaderOutput.ParameterMap, GenericSRT); // Copy over the bits indicating which resource tables are active. Header.Bindings.ShaderResourceTable.ResourceTableBits = GenericSRT.ResourceTableBits; Header.Bindings.ShaderResourceTable.ResourceTableLayoutHashes = GenericSRT.ResourceTableLayoutHashes; // Now build our token streams. BuildResourceTableTokenStream(GenericSRT.TextureMap, GenericSRT.MaxBoundResourceTable, Header.Bindings.ShaderResourceTable.TextureMap); BuildResourceTableTokenStream(GenericSRT.ShaderResourceViewMap, GenericSRT.MaxBoundResourceTable, Header.Bindings.ShaderResourceTable.ShaderResourceViewMap); BuildResourceTableTokenStream(GenericSRT.SamplerMap, GenericSRT.MaxBoundResourceTable, Header.Bindings.ShaderResourceTable.SamplerMap); BuildResourceTableTokenStream(GenericSRT.UnorderedAccessViewMap, GenericSRT.MaxBoundResourceTable, Header.Bindings.ShaderResourceTable.UnorderedAccessViewMap); Header.Bindings.NumUniformBuffers = FMath::Max((uint8)GetNumUniformBuffersUsed(GenericSRT), Header.Bindings.NumUniformBuffers); } const int32 MaxSamplers = GetFeatureLevelMaxTextureSamplers(ERHIFeatureLevel::ES3_1); Header.ShaderName = CCHeader.Name.GetCharArray(); if (Header.Bindings.NumSamplers > MaxSamplers) { ShaderOutput.bSucceeded = false; FShaderCompilerError* NewError = new(ShaderOutput.Errors) FShaderCompilerError(); NewError->StrippedErrorMessage = FString::Printf(TEXT("shader uses %d samplers exceeding the limit of %d"), Header.Bindings.NumSamplers, MaxSamplers); } else if(ShaderInput.Environment.CompilerFlags.Contains(CFLAG_Debug)) { // Write out the header and shader source code. FMemoryWriter Ar(ShaderOutput.Code, true); uint8 PrecompiledFlag = 0; Ar << PrecompiledFlag; Ar << Header; Ar.Serialize((void*)USFSource, SourceLen + 1 - (USFSource - InShaderSource)); ShaderOutput.NumInstructions = 0; ShaderOutput.NumTextureSamplers = Header.Bindings.NumSamplers; ShaderOutput.bSucceeded = true; } else { // at this point, the shader source is ready to be compiled FString InputFilename = FPaths::CreateTempFilename(*FPaths::EngineIntermediateDir(), TEXT("ShaderIn"), TEXT("")); FString ObjFilename = InputFilename + TEXT(".o"); FString ArFilename = InputFilename + TEXT(".ar"); FString OutputFilename = InputFilename + TEXT(".lib"); InputFilename = InputFilename + TEXT(".metal"); // write out shader source FFileHelper::SaveStringToFile(FString(USFSource), *InputFilename); int32 ReturnCode = 0; FString Results; FString Errors; bool bCompileAtRuntime = true; bool bSucceeded = false; #if PLATFORM_MAC FString XcodePath = FPlatformMisc::GetXcodePath(); if (XcodePath.Len() > 0 && (bIsMobile || FPlatformMisc::MacOSXVersionCompare(10, 11, 0) >= 0)) { FString MetalToolsPath = FString::Printf(TEXT("%s/Toolchains/XcodeDefault.xctoolchain/usr/bin"), *XcodePath); FString MetalPath = MetalToolsPath + TEXT("/metal"); if (IFileManager::Get().FileSize(*MetalPath) <= 0) { MetalToolsPath = FString::Printf(TEXT("%s/Platforms/iPhoneOS.platform/usr/bin"), *XcodePath); MetalPath = MetalToolsPath + TEXT("/metal"); } // metal commandlines FString Standard = (bIsMobile ? TEXT("-std=ios-metal1.0") : TEXT("-std=osx-metal1.1 -Wno-null-character -ffmast-math")); FString Params = FString::Printf(TEXT("%s %s -o %s"), *Standard, *InputFilename, *ObjFilename); FPlatformProcess::ExecProcess( *MetalPath, *Params, &ReturnCode, &Results, &Errors ); // handle compile error if (ReturnCode != 0 || IFileManager::Get().FileSize(*ObjFilename) <= 0) { FShaderCompilerError* Error = new(OutErrors) FShaderCompilerError(); Error->ErrorFile = InputFilename; Error->ErrorLineString = TEXT("0"); Error->StrippedErrorMessage = Results + Errors; bSucceeded = false; } else { Params = FString::Printf(TEXT("r %s %s"), *ArFilename, *ObjFilename); FString MetalArPath = MetalToolsPath + TEXT("/metal-ar"); FPlatformProcess::ExecProcess( *MetalArPath, *Params, &ReturnCode, &Results, &Errors ); // handle compile error if (ReturnCode != 0 || IFileManager::Get().FileSize(*ArFilename) <= 0) { FShaderCompilerError* Error = new(OutErrors) FShaderCompilerError(); Error->ErrorFile = InputFilename; Error->ErrorLineString = TEXT("0"); Error->StrippedErrorMessage = Results + Errors; bSucceeded = false; } else { Params = FString::Printf(TEXT("-o %s %s"), *OutputFilename, *ArFilename); FString MetalLibPath = MetalToolsPath + TEXT("/metallib"); FPlatformProcess::ExecProcess( *MetalLibPath, *Params, &ReturnCode, &Results, &Errors ); // handle compile error if (ReturnCode != 0 || IFileManager::Get().FileSize(*OutputFilename) <= 0) { FShaderCompilerError* Error = new(OutErrors) FShaderCompilerError(); Error->ErrorFile = InputFilename; Error->ErrorLineString = TEXT("0"); Error->StrippedErrorMessage = Results + Errors; bSucceeded = false; } else { bCompileAtRuntime = false; // Write out the header and compiled shader code FMemoryWriter Ar(ShaderOutput.Code, true); uint8 PrecompiledFlag = 1; Ar << PrecompiledFlag; Ar << Header; // load output TArray CompiledShader; FFileHelper::LoadFileToArray(CompiledShader, *OutputFilename); // jam it into the output bytes Ar.Serialize(CompiledShader.GetData(), CompiledShader.Num()); ShaderOutput.NumInstructions = 0; ShaderOutput.NumTextureSamplers = Header.Bindings.NumSamplers; ShaderOutput.bSucceeded = true; bSucceeded = true; } } } } #else // Assume success for non-Mac bSucceeded = true; #endif // PLATFORM_MAC if (bCompileAtRuntime) { // Write out the header and shader source code. FMemoryWriter Ar(ShaderOutput.Code, true); uint8 PrecompiledFlag = 0; Ar << PrecompiledFlag; Ar << Header; Ar.Serialize((void*)USFSource, SourceLen + 1 - (USFSource - InShaderSource)); ShaderOutput.NumInstructions = 0; ShaderOutput.NumTextureSamplers = Header.Bindings.NumSamplers; ShaderOutput.bSucceeded = bSucceeded || ShaderOutput.bSucceeded; } else { IFileManager::Get().Delete(*InputFilename); IFileManager::Get().Delete(*ObjFilename); IFileManager::Get().Delete(*ArFilename); IFileManager::Get().Delete(*OutputFilename); } } } /*------------------------------------------------------------------------------ External interface. ------------------------------------------------------------------------------*/ static FString CreateCommandLineHLSLCC( const FString& ShaderFile, const FString& OutputFile, const FString& EntryPoint, EHlslShaderFrequency Frequency, uint32 CCFlags ) { const TCHAR* VersionSwitch = TEXT("-metal"); return CrossCompiler::CreateBatchFileContents(ShaderFile, OutputFile, Frequency, EntryPoint, VersionSwitch, CCFlags, TEXT("")); } void CompileShader_Metal(const FShaderCompilerInput& Input,FShaderCompilerOutput& Output,const FString& WorkingDirectory) { FString PreprocessedShader; FShaderCompilerDefinitions AdditionalDefines; EHlslCompileTarget HlslCompilerTarget = HCT_FeatureLevelES3_1; AdditionalDefines.SetDefine(TEXT("COMPILER_HLSLCC"), 1 ); // @todo - Zebra - Work out which standard we need, this is dependent on the shader platform. // For now only SP_METAL will compile for iOS, with MRT & SM5 only for Mac. const bool bIsMobile = (Input.Target.Platform == SP_METAL); if (bIsMobile) { AdditionalDefines.SetDefine(TEXT("IOS"), 1); } else { AdditionalDefines.SetDefine(TEXT("MAC"), 1); } AdditionalDefines.SetDefine(TEXT("row_major"), TEXT("")); AdditionalDefines.SetDefine(TEXT("COMPILER_METAL"), 1); static FName NAME_SF_METAL(TEXT("SF_METAL")); static FName NAME_SF_METAL_MRT(TEXT("SF_METAL_MRT")); static FName NAME_SF_METAL_SM5(TEXT("SF_METAL_SM5")); if (Input.ShaderFormat == NAME_SF_METAL) { AdditionalDefines.SetDefine(TEXT("METAL_PROFILE"), 1); } else if (Input.ShaderFormat == NAME_SF_METAL_MRT) { AdditionalDefines.SetDefine(TEXT("METAL_MRT_PROFILE"), 1); } else if (Input.ShaderFormat == NAME_SF_METAL_SM5) { AdditionalDefines.SetDefine(TEXT("METAL_SM5_PROFILE"), 1); } else { Output.bSucceeded = false; new(Output.Errors) FShaderCompilerError(*FString::Printf(TEXT("Invalid shader format '%s' passed to compiler."), *Input.ShaderFormat.ToString())); return; } const bool bDumpDebugInfo = (Input.DumpDebugInfoPath != TEXT("") && IFileManager::Get().DirectoryExists(*Input.DumpDebugInfoPath)); if(Input.Environment.CompilerFlags.Contains(CFLAG_AvoidFlowControl)) { AdditionalDefines.SetDefine(TEXT("COMPILER_SUPPORTS_ATTRIBUTES"), (uint32)1); } else { AdditionalDefines.SetDefine(TEXT("COMPILER_SUPPORTS_ATTRIBUTES"), (uint32)0); } if (PreprocessShader(PreprocessedShader, Output, Input, AdditionalDefines)) { char* MetalShaderSource = NULL; char* ErrorLog = NULL; const EHlslShaderFrequency FrequencyTable[] = { HSF_VertexShader, HSF_InvalidFrequency, HSF_InvalidFrequency, HSF_PixelShader, HSF_InvalidFrequency, HSF_ComputeShader }; const EHlslShaderFrequency Frequency = FrequencyTable[Input.Target.Frequency]; if (Frequency == HSF_InvalidFrequency) { Output.bSucceeded = false; FShaderCompilerError* NewError = new(Output.Errors) FShaderCompilerError(); NewError->StrippedErrorMessage = FString::Printf( TEXT("%s shaders not supported for use in Metal."), CrossCompiler::GetFrequencyName((EShaderFrequency)Input.Target.Frequency) ); return; } // This requires removing the HLSLCC_NoPreprocess flag later on! if (!RemoveUniformBuffersFromSource(PreprocessedShader)) { return; } // Write out the preprocessed file and a batch file to compile it if requested (DumpDebugInfoPath is valid) if (bDumpDebugInfo) { FArchive* FileWriter = IFileManager::Get().CreateFileWriter(*(Input.DumpDebugInfoPath / Input.SourceFilename + TEXT(".usf"))); if (FileWriter) { auto AnsiSourceFile = StringCast(*PreprocessedShader); FileWriter->Serialize((ANSICHAR*)AnsiSourceFile.Get(), AnsiSourceFile.Length()); FileWriter->Close(); delete FileWriter; } } uint32 CCFlags = HLSLCC_NoPreprocess | HLSLCC_PackUniforms; if (!bIsMobile) { CCFlags |= HLSLCC_FixAtomicReferences; } //CCFlags |= HLSLCC_FlattenUniformBuffers | HLSLCC_FlattenUniformBufferStructures; if (bDumpDebugInfo) { const FString MetalFile = (Input.DumpDebugInfoPath / TEXT("Output.metal")); const FString USFFile = (Input.DumpDebugInfoPath / Input.SourceFilename) + TEXT(".usf"); const FString CCBatchFileContents = CreateCommandLineHLSLCC(USFFile, MetalFile, *Input.EntryPointName, Frequency, CCFlags); if (!CCBatchFileContents.IsEmpty()) { FFileHelper::SaveStringToFile(CCBatchFileContents, *(Input.DumpDebugInfoPath / TEXT("CrossCompile.bat"))); } } // Required as we added the RemoveUniformBuffersFromSource() function (the cross-compiler won't be able to interpret comments w/o a preprocessor) CCFlags &= ~HLSLCC_NoPreprocess; FMetalCodeBackend MetalBackEnd(CCFlags, HlslCompilerTarget); FMetalLanguageSpec MetalLanguageSpec; int32 Result = 0; FHlslCrossCompilerContext CrossCompilerContext(CCFlags, Frequency, HlslCompilerTarget); if (CrossCompilerContext.Init(TCHAR_TO_ANSI(*Input.SourceFilename), &MetalLanguageSpec)) { Result = CrossCompilerContext.Run( TCHAR_TO_ANSI(*PreprocessedShader), TCHAR_TO_ANSI(*Input.EntryPointName), &MetalBackEnd, &MetalShaderSource, &ErrorLog ) ? 1 : 0; } int32 SourceLen = MetalShaderSource ? FCStringAnsi::Strlen(MetalShaderSource) : 0; if(MetalShaderSource) { uint32 Len = FCStringAnsi::Strlen(TCHAR_TO_ANSI(*Input.SourceFilename)) + FCStringAnsi::Strlen(TCHAR_TO_ANSI(*Input.EntryPointName)) + FCStringAnsi::Strlen(MetalShaderSource) + 20; char* Dest = (char*)malloc(Len); FCStringAnsi::Snprintf(Dest, Len, "// ! %s.usf:%s\n%s", (const char*)TCHAR_TO_ANSI(*Input.SourceFilename), (const char*)TCHAR_TO_ANSI(*Input.EntryPointName), (const char*)MetalShaderSource); free(MetalShaderSource); MetalShaderSource = Dest; SourceLen = FCStringAnsi::Strlen(MetalShaderSource); } if (bDumpDebugInfo) { if (SourceLen > 0) { FArchive* FileWriter = IFileManager::Get().CreateFileWriter(*(Input.DumpDebugInfoPath / Input.SourceFilename + TEXT(".metal"))); if (FileWriter) { FileWriter->Serialize(MetalShaderSource, SourceLen + 1); FileWriter->Close(); delete FileWriter; } } } if (Result != 0) { Output.Target = Input.Target; BuildMetalShaderOutput(Output, Input, MetalShaderSource, SourceLen, bIsMobile, Output.Errors); } else { FString Tmp = ANSI_TO_TCHAR(ErrorLog); TArray ErrorLines; Tmp.ParseIntoArray(ErrorLines, TEXT("\n"), true); for (int32 LineIndex = 0; LineIndex < ErrorLines.Num(); ++LineIndex) { const FString& Line = ErrorLines[LineIndex]; CrossCompiler::ParseHlslccError(Output.Errors, Line); } } if (MetalShaderSource) { free(MetalShaderSource); } if (ErrorLog) { free(ErrorLog); } } }