// Copyright Epic Games, Inc. All Rights Reserved. // . #include "SpirvCommon.h" #include "Serialization/MemoryWriter.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" // A collection of states and data that is locked in at the top level call and doesn't change throughout the compilation process class FSpirvShaderCompilerInternalState { public: FSpirvShaderCompilerInternalState(const FShaderCompilerInput& InInput, const FShaderParameterParser* InParameterParser) : Input(InInput) , ParameterParser(InParameterParser) , bUseBindlessUniformBuffer(InInput.IsRayTracingShader() && ((EShaderFrequency)InInput.Target.Frequency != SF_RayGen)) , bIsRayHitGroupShader(InInput.IsRayTracingShader() && ((EShaderFrequency)InInput.Target.Frequency == SF_RayHitGroup)) , bSupportsBindless(InInput.IsBindlessEnabled()) , bDebugDump(InInput.DumpDebugInfoEnabled()) { if (bIsRayHitGroupShader) { UE::ShaderCompilerCommon::ParseRayTracingEntryPoint(Input.EntryPointName, ClosestHitEntry, AnyHitEntry, IntersectionEntry); checkf(!ClosestHitEntry.IsEmpty(), TEXT("All hit groups must contain at least a closest hit shader module")); } } virtual ~FSpirvShaderCompilerInternalState() { } const FShaderCompilerInput& Input; const FShaderParameterParser* ParameterParser; const bool bUseBindlessUniformBuffer; const bool bIsRayHitGroupShader; const bool bSupportsBindless; const bool bDebugDump; // Ray tracing specific states enum class EHitGroupShaderType { None, ClosestHit, AnyHit, Intersection }; EHitGroupShaderType HitGroupShaderType = EHitGroupShaderType::None; FString ClosestHitEntry; FString AnyHitEntry; FString IntersectionEntry; TArray AllBindlessUBs; uint32 ShaderRecordGlobalsSize = 0; // Forwarded calls for convenience inline EShaderFrequency GetShaderFrequency() const { return static_cast(Input.Target.Frequency); } inline const FString& GetEntryPointName() const { if (bIsRayHitGroupShader) { switch (HitGroupShaderType) { case EHitGroupShaderType::AnyHit: return AnyHitEntry; case EHitGroupShaderType::Intersection: return IntersectionEntry; case EHitGroupShaderType::ClosestHit: return ClosestHitEntry; case EHitGroupShaderType::None: [[fallthrough]]; default: return Input.EntryPointName; }; } else { return Input.EntryPointName; } } inline bool IsRayTracingShader() const { return Input.IsRayTracingShader(); } inline bool UseRootParametersStructure() const { // Only supported for RayGen currently return (GetShaderFrequency() == SF_RayGen) && (Input.RootParametersStructure != nullptr); } inline FString GetDebugName() const { return Input.DumpDebugInfoPath.Right(Input.DumpDebugInfoPath.Len() - Input.DumpDebugInfoRootPath.Len()); } inline bool HasMultipleEntryPoints() const { return !ClosestHitEntry.IsEmpty() && (!AnyHitEntry.IsEmpty() || !IntersectionEntry.IsEmpty()); } inline FString GetSPVExtension() const { switch (HitGroupShaderType) { case EHitGroupShaderType::AnyHit: return TEXT("anyhit.spv"); case EHitGroupShaderType::Intersection: return TEXT("intersection.spv"); case EHitGroupShaderType::ClosestHit: return TEXT("closesthit.spv"); case EHitGroupShaderType::None: [[fallthrough]]; default: return TEXT("spv"); }; } inline bool ShouldStripReflect() const { return (IsRayTracingShader() || (IsAndroid() && Input.Environment.GetCompileArgument(TEXT("STRIP_REFLECT_ANDROID"), true))); } // Provided by the platform that is compiling Spirv virtual bool IsSM6() const = 0; virtual bool IsSM5() const = 0; virtual bool IsMobileES31() const = 0; virtual CrossCompiler::FShaderConductorOptions::ETargetEnvironment GetMinimumTargetEnvironment() const = 0; virtual bool IsAndroid() const = 0; virtual bool SupportsOfflineCompiler() const = 0; }; // Data structures that will get serialized into ShaderCompilerOutput struct SpirvShaderCompilerSerializedOutput { SpirvShaderCompilerSerializedOutput() : Header(FVulkanShaderHeader::EZero) { FMemory::Memzero(PackedResourceCounts); } FVulkanShaderHeader Header; // TODO: Convert descriptors into more generic Spirv information FShaderResourceTable ShaderResourceTable; FSpirv Spirv; uint32 SpirvCRC = 0; const ANSICHAR* SpirvEntryPointName = nullptr; FShaderCodePackedResourceCounts PackedResourceCounts; TSet UsedBindlessUB; }; // -------------------------- namespace SpirvShaderCompiler { static const FString kBindlessCBPrefix = TEXT("__BindlessCB"); static const FString kBindlessHeapSuffix = TEXT("_Heap"); static FString GetBindlessUBNameFromHeap(const FString& HeapName) { check(HeapName.StartsWith(kBindlessCBPrefix)); check(HeapName.EndsWith(kBindlessHeapSuffix)); int32 NameStart = HeapName.Find(TEXT("_"), ESearchCase::IgnoreCase, ESearchDir::FromStart, kBindlessCBPrefix.Len() + 1); check(NameStart != INDEX_NONE); NameStart++; return HeapName.Mid(NameStart, HeapName.Len() - NameStart - kBindlessHeapSuffix.Len()); } static uint32 GetUBLayoutHash(const FShaderCompilerInput& ShaderInput, const FString& UBName) { uint32 LayoutHash = 0; const FUniformBufferEntry* UniformBufferEntry = ShaderInput.Environment.UniformBufferMap.Find(UBName); if (UniformBufferEntry) { LayoutHash = UniformBufferEntry->LayoutHash; } else if ((UBName == FShaderParametersMetadata::kRootUniformBufferBindingName) && ShaderInput.RootParametersStructure) { LayoutHash = ShaderInput.RootParametersStructure->GetLayoutHash(); } return LayoutHash; } // Types of Global Samplers (see Common.ush for types) // Must match EGlobalSamplerType in VulkanShaderResources.h // and declarations in VulkanCommon.ush static FVulkanShaderHeader::EGlobalSamplerType GetGlobalSamplerType(const FString& ResourceName) { #define VULKAN_GLOBAL_SAMPLER_NAME(FilterWrapName) if (ResourceName.EndsWith(TEXT(#FilterWrapName))) return FVulkanShaderHeader::EGlobalSamplerType::FilterWrapName if (ResourceName.StartsWith(TEXT("VulkanGlobal"))) { VULKAN_GLOBAL_SAMPLER_NAME(PointClampedSampler); VULKAN_GLOBAL_SAMPLER_NAME(PointWrappedSampler); VULKAN_GLOBAL_SAMPLER_NAME(BilinearClampedSampler); VULKAN_GLOBAL_SAMPLER_NAME(BilinearWrappedSampler); VULKAN_GLOBAL_SAMPLER_NAME(TrilinearClampedSampler); VULKAN_GLOBAL_SAMPLER_NAME(TrilinearWrappedSampler); } return FVulkanShaderHeader::EGlobalSamplerType::Invalid; #undef VULKAN_GLOBAL_SAMPLER_NAME } static bool HasDerivatives(const FSpirv& Spirv) { for (FSpirvConstIterator Iter = Spirv.begin(); Iter != Spirv.end(); ++Iter) { switch (Iter.Opcode()) { case SpvOpCapability: { const uint32 Capability = Iter.Operand(1); if ((Capability == SpvCapabilityComputeDerivativeGroupLinearNV) || (Capability == SpvCapabilityComputeDerivativeGroupQuadsNV)) { return true; } } break; case SpvOpExtension: case SpvOpEntryPoint: // By the time we've reached extensions/entrypoints, we're done listing capabilities return false; default: break; } } return false; } static void FillShaderResourceUsageFlags(const FSpirvShaderCompilerInternalState& InternalState, SpirvShaderCompilerSerializedOutput& SerializedOutput) { FShaderCodePackedResourceCounts& PackedResourceCounts = SerializedOutput.PackedResourceCounts; if (InternalState.Input.Target.GetFrequency() == SF_Compute && InternalState.Input.Environment.CompilerFlags.Contains(CFLAG_CheckForDerivativeOps)) { if (!HasDerivatives(SerializedOutput.Spirv)) { PackedResourceCounts.UsageFlags |= EShaderResourceUsageFlags::NoDerivativeOps; } } if (InternalState.bSupportsBindless) { PackedResourceCounts.UsageFlags |= EShaderResourceUsageFlags::BindlessResources; PackedResourceCounts.UsageFlags |= EShaderResourceUsageFlags::BindlessSamplers; } if (InternalState.Input.Environment.CompilerFlags.Contains(CFLAG_ShaderBundle)) { PackedResourceCounts.UsageFlags |= EShaderResourceUsageFlags::ShaderBundle; } // TODO: When DiagnosticBuffer is supported: // PackedResourceCounts.UsageFlags |= EShaderResourceUsageFlags::DiagnosticBuffer; } static void BuildShaderOutput( SpirvShaderCompilerSerializedOutput& SerializedOutput, FShaderCompilerOutput& ShaderOutput, const FSpirvShaderCompilerInternalState& InternalState, const FSpirvReflectBindings& SpirvReflectBindings, const FString& DebugName, TBitArray<>& UsedUniformBufferSlots ) { auto ParseNumber = [](const T * Str, bool bEmptyIsZero = false) { check(Str); uint32 Num = 0; int32 Len = 0; // Find terminating character for (int32 Index = 0; Index < 128; Index++) { if (Str[Index] == 0) { Len = Index; break; } } if (Len == 0) { if (bEmptyIsZero) { return 0u; } else { check(0); } } // Find offset to integer type int32 Offset = -1; for (int32 Index = 0; Index < Len; Index++) { if (*(Str + Index) >= '0' && *(Str + Index) <= '9') { Offset = Index; break; } } // Check if we found a number check(Offset >= 0); Str += Offset; while (*(Str) && *Str >= '0' && *Str <= '9') { Num = Num * 10 + *Str++ - '0'; } return Num; }; const FShaderCompilerInput& ShaderInput = InternalState.Input; const EShaderFrequency Frequency = InternalState.GetShaderFrequency(); FVulkanShaderHeader& Header = SerializedOutput.Header; Header.SpirvCRC = SerializedOutput.SpirvCRC; Header.RayTracingPayloadType = ShaderInput.Environment.GetCompileArgument(TEXT("RT_PAYLOAD_TYPE"), 0u); Header.RayTracingPayloadSize = ShaderInput.Environment.GetCompileArgument(TEXT("RT_PAYLOAD_MAX_SIZE"), 0u); // :todo-jn: Hash entire SPIRV for now, could eventually be removed since we use ShaderKeys FSHA1::HashBuffer(SerializedOutput.Spirv.Data.GetData(), SerializedOutput.Spirv.GetByteSize(), (uint8*)&Header.SourceHash); // Flattens the array dimensions of the interface variable (aka shader attribute), e.g. from float4[2][3] -> float4[6] auto FlattenAttributeArrayDimension = [](const SpvReflectInterfaceVariable& Attribute, uint32 FirstArrayDim = 0) { uint32 FlattenedArrayDim = 1; for (uint32 ArrayDimIndex = FirstArrayDim; ArrayDimIndex < Attribute.array.dims_count; ++ArrayDimIndex) { FlattenedArrayDim *= Attribute.array.dims[ArrayDimIndex]; } return FlattenedArrayDim; }; // Only process input attributes for vertex shaders. if (Frequency == SF_Vertex) { static const FString AttributePrefix = TEXT("ATTRIBUTE"); for (const SpvReflectInterfaceVariable* Attribute : SpirvReflectBindings.InputAttributes) { if (CrossCompiler::FShaderConductorContext::IsIntermediateSpirvOutputVariable(Attribute->name)) { continue; } if (!Attribute->semantic) { continue; } const FString InputAttrName(ANSI_TO_TCHAR(Attribute->semantic)); if (InputAttrName.StartsWith(AttributePrefix)) { const uint32 AttributeIndex = ParseNumber(*InputAttrName + AttributePrefix.Len(), /*bEmptyIsZero:*/ true); const uint32 FlattenedArrayDim = FlattenAttributeArrayDimension(*Attribute); for (uint32 Index = 0; Index < FlattenedArrayDim; ++Index) { const uint32 BitIndex = (AttributeIndex + Index); Header.InOutMask |= (1u << BitIndex); } } } } // Only process output attributes for pixel shaders. if (Frequency == SF_Pixel) { static const FString TargetPrefix = "SV_Target"; for (const SpvReflectInterfaceVariable* Attribute : SpirvReflectBindings.OutputAttributes) { // Only depth writes for pixel shaders must be tracked. if (Attribute->built_in == SpvBuiltInFragDepth) { const uint32 BitIndex = (CrossCompiler::FShaderBindingInOutMask::DepthStencilMaskIndex); Header.InOutMask |= (1u << BitIndex); } else { // Only targets for pixel shaders must be tracked. const FString OutputAttrName(ANSI_TO_TCHAR(Attribute->semantic)); if (OutputAttrName.StartsWith(TargetPrefix)) { const uint32 TargetIndex = ParseNumber(*OutputAttrName + TargetPrefix.Len(), /*bEmptyIsZero:*/ true); const uint32 FlattenedArrayDim = FlattenAttributeArrayDimension(*Attribute); for (uint32 Index = 0; Index < FlattenedArrayDim; ++Index) { const uint32 BitIndex = (TargetIndex + Index); Header.InOutMask |= (1u << BitIndex); } } } } } // Build the SRT for this shader. { checkf(Header.UniformBufferInfos.Num() == (UsedUniformBufferSlots.FindLast(true) + 1), TEXT("Some of the Uniform Buffers containing constants weren't flag as in-use. This might lead to duplicate indices being assigned.")); FShaderCompilerResourceTable CompilerSRT; if (!BuildResourceTableMapping(ShaderInput.Environment.ResourceTableMap, ShaderInput.Environment.UniformBufferMap, UsedUniformBufferSlots, ShaderOutput.ParameterMap, CompilerSRT)) { ShaderOutput.Errors.Add(TEXT("Internal error on BuildResourceTableMapping.")); return; } UE::ShaderCompilerCommon::BuildShaderResourceTable(CompilerSRT, SerializedOutput.ShaderResourceTable); // The previous step also added resource only UBs starting at the first free slot in UsedUniformBufferSlots // We need to add the hashes for their layouts in the same slots of our UniformBufferInfos in the header { const int32 NumUBSlots = CompilerSRT.MaxBoundResourceTable + 1; if (Header.UniformBufferInfos.Num() < NumUBSlots) { Header.UniformBufferInfos.SetNumZeroed(NumUBSlots); } TArray UBParameterNames = ShaderOutput.ParameterMap.GetAllParameterNamesOfType(EShaderParameterType::UniformBuffer); for (const FStringView& ParameterName : UBParameterNames) { TOptional Allocation = ShaderOutput.ParameterMap.FindParameterAllocation(ParameterName); check(Allocation.IsSet()); const uint32 UniformBufferIndex = Allocation.GetValue().BufferIndex; FVulkanShaderHeader::FUniformBufferInfo& UniformBufferInfo = Header.UniformBufferInfos[UniformBufferIndex]; UniformBufferInfo.bHasResources = 1; const bool bIsRootParamStructure = (ParameterName == FShaderParametersMetadata::kRootUniformBufferBindingName) && ShaderInput.RootParametersStructure; if (bIsRootParamStructure) { check(UniformBufferIndex == FShaderParametersMetadata::kRootCBufferBindingIndex); const uint32 UBLayoutHash = CompilerSRT.ResourceTableLayoutHashes[UniformBufferIndex]; checkf(!UBLayoutHash || (UBLayoutHash == ShaderInput.RootParametersStructure->GetLayoutHash()), TEXT("Resource table layout hash for RootParametersStructure (0x%08X) should be unset (0x0) or identical to shader input (0x%08X)!"), UBLayoutHash, ShaderInput.RootParametersStructure->GetLayoutHash()); CompilerSRT.ResourceTableLayoutHashes[UniformBufferIndex] = ShaderInput.RootParametersStructure->GetLayoutHash(); } else { const uint32 UBLayoutHash = CompilerSRT.ResourceTableLayoutHashes[UniformBufferIndex]; checkf(!UniformBufferInfo.LayoutHash || (UniformBufferInfo.LayoutHash == UBLayoutHash), TEXT("Existing layout hash (0x%08X) should be unset (resource only UB) or identical to resource table (0x%08X)!"), UniformBufferInfo.LayoutHash, UBLayoutHash); UniformBufferInfo.LayoutHash = UBLayoutHash; } } } } ShaderOutput.bSucceeded = true; // guard disassembly of SPIRV code on bExtractShaderSource setting since presumably this isn't that cheap. // this roughly will maintain existing behaviour, except the debug usf will be this version of the code // instead of the output of preprocessing if this setting is enabled (which is probably fine since this is only // ever set in editor) if (ShaderInput.ExtraSettings.bExtractShaderSource) { TArray AssemblyText; if (CrossCompiler::FShaderConductorContext::Disassemble(CrossCompiler::EShaderConductorIR::Spirv, SerializedOutput.Spirv.GetByteData(), SerializedOutput.Spirv.GetByteSize(), AssemblyText)) { ShaderOutput.ModifiedShaderSource = FString(AssemblyText.GetData()); } } if (ShaderInput.ExtraSettings.OfflineCompilerPath.Len() > 0) { if (InternalState.SupportsOfflineCompiler()) { CompileShaderOffline(ShaderInput, ShaderOutput, (const ANSICHAR*)SerializedOutput.Spirv.GetByteData(), SerializedOutput.Spirv.GetByteSize(), true, SerializedOutput.SpirvEntryPointName); } } // Ray generation shaders rely on a different binding model that aren't compatible with global uniform buffers. if (!InternalState.IsRayTracingShader()) { CullGlobalUniformBuffers(ShaderInput.Environment.UniformBufferMap, ShaderOutput.ParameterMap); } #if VULKAN_ENABLE_BINDING_DEBUG_NAMES Header.DebugName = DebugName; #else if (ShaderInput.Environment.CompilerFlags.Contains(CFLAG_ExtraShaderData)) { Header.DebugName = ShaderInput.GenerateShaderName(); } #endif } #if PLATFORM_MAC || PLATFORM_WINDOWS || PLATFORM_LINUX static void GatherSpirvReflectionBindings( spv_reflect::ShaderModule& Reflection, FSpirvReflectBindings& OutBindings, TSet& OutBindlessUB, const FSpirvShaderCompilerInternalState& InternalState) { // Change descriptor set numbers TArray DescriptorSets; uint32 NumDescriptorSets = 0; // If bindless is supported, then offset the descriptor set to fit the bindless heaps at the beginning const EShaderFrequency ShaderFrequency = InternalState.GetShaderFrequency(); const uint32 StageIndex = (uint32)ShaderStage::GetStageForFrequency(ShaderFrequency); const uint32 DescSetNo = InternalState.bSupportsBindless ? VulkanBindless::MaxNumSets + StageIndex : StageIndex; SpvReflectResult SpvResult = Reflection.EnumerateDescriptorSets(&NumDescriptorSets, nullptr); check(SpvResult == SPV_REFLECT_RESULT_SUCCESS); if (NumDescriptorSets > 0) { DescriptorSets.SetNum(NumDescriptorSets); SpvResult = Reflection.EnumerateDescriptorSets(&NumDescriptorSets, DescriptorSets.GetData()); check(SpvResult == SPV_REFLECT_RESULT_SUCCESS); for (const SpvReflectDescriptorSet* DescSet : DescriptorSets) { Reflection.ChangeDescriptorSetNumber(DescSet, DescSetNo); } } OutBindings.GatherInputAttributes(Reflection); OutBindings.GatherOutputAttributes(Reflection); OutBindings.GatherDescriptorBindings(Reflection); // Storage buffers always occupy a UAV binding slot, so move all SBufferSRVs into the SBufferUAVs array OutBindings.SBufferUAVs.Append(OutBindings.SBufferSRVs); OutBindings.SBufferSRVs.Empty(); // Change indices of input attributes by their name suffix. Only in the vertex shader stage, "ATTRIBUTE" semantics have a special meaning for shader attributes. if (ShaderFrequency == SF_Vertex) { OutBindings.AssignInputAttributeLocationsBySemanticIndex(Reflection, CrossCompiler::FShaderConductorContext::GetIdentifierTable().InputAttribute); } // Patch resource heaps descriptor set numbers if (InternalState.bSupportsBindless) { // Move the bindless heap to its dedicated descriptor set and remove it from our regular binding arrays auto MoveBindlessHeaps = [&](TArray& BindingArray, const TCHAR* HeapPrefix, uint32 BinldessDescSetNo) { for (int32 Index = BindingArray.Num() - 1; Index >= 0; --Index) { const SpvReflectDescriptorBinding* pBinding = BindingArray[Index]; const FString BindingName(ANSI_TO_TCHAR(pBinding->name)); if (BindingName.StartsWith(HeapPrefix)) { const uint32 Binding = 0; // single bindless heap per descriptor set Reflection.ChangeDescriptorBindingNumbers(pBinding, Binding, BinldessDescSetNo); BindingArray.RemoveAtSwap(Index); } } }; // Remove sampler heaps from binding arrays MoveBindlessHeaps(OutBindings.Samplers, FShaderParameterParser::kBindlessSamplerArrayPrefix, VulkanBindless::BindlessSamplerSet); // Remove resource heaps from binding arrays MoveBindlessHeaps(OutBindings.SBufferUAVs, FShaderParameterParser::kBindlessUAVArrayPrefix, VulkanBindless::BindlessStorageBufferSet); MoveBindlessHeaps(OutBindings.SBufferUAVs, FShaderParameterParser::kBindlessSRVArrayPrefix, VulkanBindless::BindlessStorageBufferSet); // try with both prefixes, they were merged earlier MoveBindlessHeaps(OutBindings.TextureSRVs, FShaderParameterParser::kBindlessSRVArrayPrefix, VulkanBindless::BindlessSampledImageSet); MoveBindlessHeaps(OutBindings.TextureUAVs, FShaderParameterParser::kBindlessUAVArrayPrefix, VulkanBindless::BindlessStorageImageSet); MoveBindlessHeaps(OutBindings.TextureUAVs, FShaderParameterParser::kBindlessSRVArrayPrefix, VulkanBindless::BindlessStorageImageSet); // try with both prefixes, R64 SRV textures are read as storage images MoveBindlessHeaps(OutBindings.TBufferSRVs, FShaderParameterParser::kBindlessSRVArrayPrefix, VulkanBindless::BindlessUniformTexelBufferSet); MoveBindlessHeaps(OutBindings.TBufferUAVs, FShaderParameterParser::kBindlessUAVArrayPrefix, VulkanBindless::BindlessStorageTexelBufferSet); MoveBindlessHeaps(OutBindings.AccelerationStructures, FShaderParameterParser::kBindlessSRVArrayPrefix, VulkanBindless::BindlessAccelerationStructureSet); // Move uniform buffers to the correct set { const uint32 BindingOffset = (StageIndex * VulkanBindless::MaxUniformBuffersPerStage); for (int32 Index = OutBindings.UniformBuffers.Num() - 1; Index >= 0; --Index) { const SpvReflectDescriptorBinding* pBinding = OutBindings.UniformBuffers[Index]; const FString BindingName(ANSI_TO_TCHAR(pBinding->name)); if (BindingName.StartsWith(kBindlessCBPrefix)) { check(InternalState.bUseBindlessUniformBuffer); Reflection.ChangeDescriptorBindingNumbers(pBinding, 0, VulkanBindless::BindlessUniformBufferSet); const FString BindlessUBName = GetBindlessUBNameFromHeap(BindingName); checkf(InternalState.AllBindlessUBs.Contains(BindlessUBName), TEXT("Bindless Uniform Buffer was found in SPIRV but not tracked in internal state")); OutBindlessUB.Add(BindlessUBName); OutBindings.UniformBuffers.RemoveAtSwap(Index); } else { Reflection.ChangeDescriptorBindingNumbers(pBinding, BindingOffset + pBinding->binding, VulkanBindless::BindlessSingleUseUniformBufferSet); } } } } } static uint32 CalculateSpirvInstructionCount(FSpirv& Spirv) { // Count instructions inside functions bool bInsideFunction = false; uint32 ApproxInstructionCount = 0; for (FSpirvConstIterator Iter = Spirv.cbegin(); Iter != Spirv.cend(); ++Iter) { switch (Iter.Opcode()) { case SpvOpFunction: { check(!bInsideFunction); bInsideFunction = true; } break; case SpvOpFunctionEnd: { check(bInsideFunction); bInsideFunction = false; } break; case SpvOpLabel: case SpvOpAccessChain: case SpvOpSelectionMerge: case SpvOpCompositeConstruct: case SpvOpCompositeInsert: case SpvOpCompositeExtract: // Skip a few ops that show up often but don't result in much work on their own break; default: { if (bInsideFunction) { ++ApproxInstructionCount; } } break; } } check(!bInsideFunction); return ApproxInstructionCount; } static bool BuildShaderOutputFromSpirv( CrossCompiler::FShaderConductorContext& CompilerContext, const FSpirvShaderCompilerInternalState& InternalState, SpirvShaderCompilerSerializedOutput& SerializedOutput, FShaderCompilerOutput& Output ) { // Reflect SPIR-V module with SPIRV-Reflect library const size_t SpirvDataSize = SerializedOutput.Spirv.GetByteSize(); spv_reflect::ShaderModule Reflection(SpirvDataSize, SerializedOutput.Spirv.GetByteData(), SPV_REFLECT_RETURN_FLAG_SAMPLER_IMAGE_USAGE); check(Reflection.GetResult() == SPV_REFLECT_RESULT_SUCCESS); // Ray tracing shaders are not being rewritten to remove unreferenced entry points due to a bug in dxc. // An issue prevents multiple entrypoints in the same spirv module, so limit ourselves to one entrypoint at a time // Change final entry point name in SPIR-V module { checkf(Reflection.GetEntryPointCount() == 1, TEXT("Too many entry points in SPIR-V module: Expected 1, but got %d"), Reflection.GetEntryPointCount()); const SpvReflectResult Result = Reflection.ChangeEntryPointName(0, "main_00000000_00000000"); check(Result == SPV_REFLECT_RESULT_SUCCESS); } FSpirvReflectBindings Bindings; GatherSpirvReflectionBindings(Reflection, Bindings, SerializedOutput.UsedBindlessUB, InternalState); const FString UBOGlobalsNameSpv(ANSI_TO_TCHAR(CrossCompiler::FShaderConductorContext::GetIdentifierTable().GlobalsUniformBuffer)); const FString UBORootParamNameSpv(FShaderParametersMetadata::kRootUniformBufferBindingName); TBitArray<> UsedUniformBufferSlots; const int32 MaxNumBits = VulkanBindless::MaxUniformBuffersPerStage * SF_NumFrequencies; UsedUniformBufferSlots.Init(false, MaxNumBits); // Final descriptor binding numbers for all other resource types { const ShaderStage::EStage UEStage = ShaderStage::GetStageForFrequency(InternalState.GetShaderFrequency()); const int32 StageOffset = InternalState.bSupportsBindless ? (UEStage * VulkanBindless::MaxUniformBuffersPerStage) : 0; const uint32_t DescSetNumber = InternalState.bSupportsBindless ? (uint32_t)VulkanBindless::BindlessSingleUseUniformBufferSet : (uint32_t)UEStage; auto AddShaderValidationType = [](uint32_t VulkanBindingIndex, const FShaderParameterParser::FParsedShaderParameter* ParsedParam, FShaderCompilerOutput& Output) { /*if (ParsedParam) { if (IsResourceBindingTypeSRV(ParsedParam->ParsedTypeDecl)) { AddShaderValidationSRVType(VulkanBindingIndex, ParsedParam->ParsedTypeDecl, Output); } else { AddShaderValidationUAVType(VulkanBindingIndex, ParsedParam->ParsedTypeDecl, Output); } }*/ }; auto AddReflectionInfos = [&](TArray& BindingArray, const VkDescriptorType DescriptorType, int32 BindingTypeCount, bool bIsPackedUniformBuffer=false) { for (const SpvReflectDescriptorBinding* Binding : BindingArray) { checkf(!InternalState.bSupportsBindless || (DescriptorType == VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER), TEXT("Bindless shaders should only have uniform buffers.")); const FString ResourceName(ANSI_TO_TCHAR(Binding->name)); const bool bIsGlobalOrRootBuffer = ((UBOGlobalsNameSpv == ResourceName) || (UBORootParamNameSpv == ResourceName)); if ((bIsPackedUniformBuffer && !bIsGlobalOrRootBuffer) || ((!bIsPackedUniformBuffer) && bIsGlobalOrRootBuffer)) { continue; } const int32 BindingSlot = SerializedOutput.Header.Bindings.Num(); const int32 BindingIndex = StageOffset + BindingSlot; FVulkanShaderHeader::FBindingInfo& BindingInfo = SerializedOutput.Header.Bindings.AddZeroed_GetRef(); BindingInfo.DescriptorType = DescriptorType; #if VULKAN_ENABLE_BINDING_DEBUG_NAMES BindingInfo.DebugName = ResourceName; #endif const SpvReflectResult SpvResult = Reflection.ChangeDescriptorBindingNumbers(Binding, BindingIndex, DescSetNumber); check(SpvResult == SPV_REFLECT_RESULT_SUCCESS); const int32 ReflectionSlot = BindingSlot; check(InternalState.ParameterParser); const FShaderParameterParser::FParsedShaderParameter* ParsedParam = InternalState.ParameterParser->FindParameterInfosUnsafe(ResourceName); switch (DescriptorType) { case VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER: case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER: case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE: HandleReflectedShaderUAV(ResourceName, BindingTypeCount, ReflectionSlot, 1, Output); AddShaderValidationType(BindingTypeCount, ParsedParam, Output); break; case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE: case VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER: HandleReflectedShaderResource(ResourceName, BindingTypeCount, ReflectionSlot, 1, Output); AddShaderValidationType(BindingTypeCount, ParsedParam, Output); break; case VK_DESCRIPTOR_TYPE_SAMPLER: { // Regular samplers need reflection to get bindings, global samplers get bound automagically. FVulkanShaderHeader::EGlobalSamplerType GlobalSamplerType = GetGlobalSamplerType(ResourceName); if (GlobalSamplerType == FVulkanShaderHeader::EGlobalSamplerType::Invalid) { HandleReflectedShaderSampler(ResourceName, ReflectionSlot, Output); } else { FVulkanShaderHeader::FGlobalSamplerInfo& GlobalSamplerInfo = SerializedOutput.Header.GlobalSamplerInfos.AddZeroed_GetRef(); GlobalSamplerInfo.BindingIndex = BindingSlot; GlobalSamplerInfo.Type = GlobalSamplerType; } } break; case VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR: HandleReflectedShaderResource(ResourceName, BindingTypeCount, ReflectionSlot, 1, Output); AddShaderValidationType(BindingTypeCount, ParsedParam, Output); break; case VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT: { SerializedOutput.Header.InputAttachmentsMask |= (1u << Binding->input_attachment_index); FVulkanShaderHeader::FInputAttachmentInfo& InputAttachmentInfo = SerializedOutput.Header.InputAttachmentInfos.AddZeroed_GetRef(); InputAttachmentInfo.BindingIndex = BindingSlot; InputAttachmentInfo.Type = (FVulkanShaderHeader::EAttachmentType)Binding->input_attachment_index; } break; case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER: { if (bIsPackedUniformBuffer) { // Use the given global ResourceName instead of patching it to _Globals_h if (InternalState.UseRootParametersStructure()) { check(ReflectionSlot == FShaderParametersMetadata::kRootCBufferBindingIndex); HandleReflectedUniformBuffer(ResourceName, ReflectionSlot, Output); } // Register all uniform buffer members of Globals as loose data for (uint32 MemberIndex = 0; MemberIndex < Binding->block.member_count; ++MemberIndex) { const SpvReflectBlockVariable& Member = Binding->block.members[MemberIndex]; FString MemberName(Member.name); FStringView AdjustedMemberName(MemberName); const EShaderParameterType BindlessParameterType = FShaderParameterParser::ParseAndRemoveBindlessParameterPrefix(AdjustedMemberName); // Add all members of global ub, and only bindless samplers/resources for root param if (!InternalState.UseRootParametersStructure() || BindlessParameterType != EShaderParameterType::LooseData) { check(BindingTypeCount == 0); // Global constants should always be the first UB HandleReflectedGlobalConstantBufferMember( MemberName, BindingTypeCount, Member.absolute_offset, Member.size, Output ); } SerializedOutput.Header.PackedGlobalsSize = FMath::Max((Member.absolute_offset + Member.size), SerializedOutput.Header.PackedGlobalsSize); SerializedOutput.Header.PackedGlobalsSize = Align(SerializedOutput.Header.PackedGlobalsSize, 16u); } } else { check(BindingTypeCount == ReflectionSlot); check(!UsedUniformBufferSlots[ReflectionSlot]); HandleReflectedUniformBuffer(ResourceName, ReflectionSlot, Output); AddShaderValidationUBSize(BindingTypeCount, Binding->block.padded_size, Output); const EUniformBufferMemberReflectionReason Reason = ShouldReflectUniformBufferMembers(InternalState.Input, ResourceName); if (Reason != EUniformBufferMemberReflectionReason::None) { // Register uniform buffer members that are in use for (uint32 MemberIndex = 0; MemberIndex < Binding->block.member_count; ++MemberIndex) { const SpvReflectBlockVariable& Member = Binding->block.members[MemberIndex]; if ((Member.flags & SPV_REFLECT_VARIABLE_FLAGS_UNUSED) != 0) { continue; } const FString MemberName(Member.name); HandleReflectedUniformBufferConstantBufferMember( Reason, ResourceName, ReflectionSlot, MemberName, Member.absolute_offset, Member.size, Output ); } } } check(!UsedUniformBufferSlots[ReflectionSlot]); UsedUniformBufferSlots[ReflectionSlot] = true; FVulkanShaderHeader::FUniformBufferInfo& UniformBufferInfo = SerializedOutput.Header.UniformBufferInfos.AddZeroed_GetRef(); UniformBufferInfo.LayoutHash = GetUBLayoutHash(InternalState.Input, ResourceName); check(SerializedOutput.Header.Bindings.Num() == SerializedOutput.Header.UniformBufferInfos.Num()); } break; default: check(false); break; }; BindingTypeCount++; } return BindingTypeCount; }; // Process Globals first (PackedUniformBuffer) and then regular UBs const int32 GlobalUBCount = AddReflectionInfos(Bindings.UniformBuffers, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, true); const int32 UBOBindings = AddReflectionInfos(Bindings.UniformBuffers, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, GlobalUBCount); SerializedOutput.Header.NumBoundUniformBuffers = UBOBindings; SerializedOutput.PackedResourceCounts.NumCBs = (uint8)UBOBindings; AddReflectionInfos(Bindings.InputAttachments, VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 0); int32 UAVBindings = 0; UAVBindings = AddReflectionInfos(Bindings.TBufferUAVs, VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, UAVBindings); UAVBindings = AddReflectionInfos(Bindings.SBufferUAVs, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, UAVBindings); UAVBindings = AddReflectionInfos(Bindings.TextureUAVs, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, UAVBindings); SerializedOutput.PackedResourceCounts.NumUAVs = (uint8)UAVBindings; int32 SRVBindings = 0; SRVBindings = AddReflectionInfos(Bindings.TBufferSRVs, VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, SRVBindings); checkf(Bindings.SBufferSRVs.IsEmpty(), TEXT("GatherSpirvReflectionBindings should have dumped all SBufferSRVs into SBufferUAVs.")); SRVBindings = AddReflectionInfos(Bindings.TextureSRVs, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, SRVBindings); SerializedOutput.PackedResourceCounts.NumSRVs = (uint8)SRVBindings; Output.NumTextureSamplers = AddReflectionInfos(Bindings.Samplers, VK_DESCRIPTOR_TYPE_SAMPLER, 0); SerializedOutput.PackedResourceCounts.NumSamplers = (uint8)Output.NumTextureSamplers; AddReflectionInfos(Bindings.AccelerationStructures, VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR, 0); } Output.Target = InternalState.Input.Target; // Overwrite updated SPIRV code SerializedOutput.Spirv.Data = TArray(Reflection.GetCode(), Reflection.GetCodeSize() / 4); // We have to strip out most debug instructions (except OpName) for Vulkan mobile if (InternalState.ShouldStripReflect()) { const char* OptArgs[] = { "--strip-reflect", "-O"}; if (!CompilerContext.OptimizeSpirv(SerializedOutput.Spirv.Data, OptArgs, UE_ARRAY_COUNT(OptArgs))) { Output.Errors.Add(TEXT("Failed to strip debug instructions from SPIR-V module")); return false; } } // For Android run an additional pass to patch spirv to be compatible across drivers if (InternalState.IsAndroid()) { const char* OptArgs[] = { "--android-driver-patch", // FORT-733360: Some Adreno drivers have bugs for interpolators, which are arrays, // hence we need to get rid of them. "--adv-interface-variable-scalar-replacement=skip-matrices" }; if (!CompilerContext.OptimizeSpirv(SerializedOutput.Spirv.Data, OptArgs, UE_ARRAY_COUNT(OptArgs))) { Output.Errors.Add(TEXT("Failed to apply driver patches for Android")); return false; } } // :todo-jn: We don't store the CRC of each member of the hit group, leave the entrypoint untouched on the extra modules if (InternalState.HasMultipleEntryPoints() && (InternalState.HitGroupShaderType != FSpirvShaderCompilerInternalState::EHitGroupShaderType::ClosestHit)) { SerializedOutput.SpirvEntryPointName = "main_00000000_00000000"; } else { SerializedOutput.SpirvEntryPointName = PatchSpirvEntryPointWithCRC(SerializedOutput.Spirv, SerializedOutput.SpirvCRC); } Output.NumInstructions = CalculateSpirvInstructionCount(SerializedOutput.Spirv); BuildShaderOutput( SerializedOutput, Output, InternalState, Bindings, InternalState.GetDebugName(), UsedUniformBufferSlots ); if (InternalState.bDebugDump) { FString SPVExt(InternalState.GetSPVExtension()); FString SPVASMExt(SPVExt + TEXT("asm")); // Write meta data to debug output file and write SPIR-V dump in binary and text form DumpDebugShaderBinary(InternalState.Input, SerializedOutput.Spirv.GetByteData(), SerializedOutput.Spirv.GetByteSize(), SPVExt); DumpDebugShaderDisassembledSpirv(InternalState.Input, SerializedOutput.Spirv.GetByteData(), SerializedOutput.Spirv.GetByteSize(), SPVASMExt); } return true; } // Replaces OpImageFetch with OpImageRead for 64bit samplers static void Patch64bitSamplers(FSpirv& Spirv) { uint32_t ULongSampledTypeId = 0; uint32_t LongSampledTypeId = 0; TArray> ImageTypeIDs; TArray> LoadedIDs; // Count instructions inside functions for (FSpirvIterator Iter = Spirv.begin(); Iter != Spirv.end(); ++Iter) { switch (Iter.Opcode()) { case SpvOpTypeInt: { // Operands: // 1 - Result Id // 2 - Width specifies how many bits wide the type is // 3 - Signedness: 0 indicates unsigned const uint32_t IntWidth = Iter.Operand(2); if (IntWidth == 64) { const uint32_t IntSignedness = Iter.Operand(3); if (IntSignedness == 1) { check(LongSampledTypeId == 0); LongSampledTypeId = Iter.Operand(1); } else { check(ULongSampledTypeId == 0); ULongSampledTypeId = Iter.Operand(1); } } } break; case SpvOpTypeImage: { // Operands: // 1 - Result Id // 2 - Sampled Type is the type of the components that result from sampling or reading from this image type // 3 - Dim is the image dimensionality (Dim). // 4 - Depth : 0 indicates not a depth image, 1 indicates a depth image, 2 means no indication as to whether this is a depth or non-depth image // 5 - Arrayed : 0 indicates non-arrayed content, 1 indicates arrayed content // 6 - MS : 0 indicates single-sampled content, 1 indicates multisampled content // 7 - Sampled : 0 indicates this is only known at run time, not at compile time, 1 indicates used with sampler, 2 indicates used without a sampler (a storage image) // 8 - Image Format if ((Iter.Operand(7) == 1) && (Iter.Operand(6) == 0) && (Iter.Operand(5) == 0)) { // Patch the node info and the SPIRV const uint32_t SampledTypeId = Iter.Operand(2); const uint32_t WithoutSampler = 2; if (SampledTypeId == LongSampledTypeId) { uint32* CurrentOpPtr = *Iter; CurrentOpPtr[7] = WithoutSampler; CurrentOpPtr[8] = (uint32_t)SpvImageFormatR64i; ImageTypeIDs.Add(Iter.Operand(1)); } else if (SampledTypeId == ULongSampledTypeId) { uint32* CurrentOpPtr = *Iter; CurrentOpPtr[7] = WithoutSampler; CurrentOpPtr[8] = (uint32_t)SpvImageFormatR64ui; ImageTypeIDs.Add(Iter.Operand(1)); } } } break; case SpvOpLoad: { // Operands: // 1 - Result Type Id // 2 - Result Id // 3 - Pointer // Find loaded images of this type if (ImageTypeIDs.Find(Iter.Operand(1)) != INDEX_NONE) { LoadedIDs.Add(Iter.Operand(2)); } } break; case SpvOpImageFetch: { // Operands: // 1 - Result Type Id // 2 - Result Id // 3 - Image Id // 4 - Coordinate // 5 - Image Operands // If this is one of the modified images, patch the node and the SPIRV. if (LoadedIDs.Find(Iter.Operand(3)) != INDEX_NONE) { const uint32_t OldWordCount = Iter.WordCount(); const uint32_t NewWordCount = 5; check(OldWordCount >= NewWordCount); const uint32_t EncodedOpImageRead = (NewWordCount << 16) | ((uint32_t)SpvOpImageRead & 0xFFFF); uint32* CurrentOpPtr = *Iter; (*CurrentOpPtr) = EncodedOpImageRead; // Remove unsupported image operands (mostly force LOD 0) const uint32_t NopWordCount = 1; const uint32_t EncodedOpNop = (NopWordCount << 16) | ((uint32_t)SpvOpNop & 0xFFFF); for (uint32_t ImageOperandIndex = NewWordCount; ImageOperandIndex < OldWordCount; ++ImageOperandIndex) { CurrentOpPtr[ImageOperandIndex] = EncodedOpNop; } } } break; default: break; } } } static void SpirvCreateDXCCompileBatchFiles( const CrossCompiler::FShaderConductorContext& CompilerContext, const FSpirvShaderCompilerInternalState& InternalState, const CrossCompiler::FShaderConductorOptions& Options) { const FString USFFilename = InternalState.Input.GetSourceFilename(); const FString SPVFilename = FPaths::GetBaseFilename(USFFilename) + TEXT(".DXC.spv"); const FString GLSLFilename = FPaths::GetBaseFilename(USFFilename) + TEXT(".SPV.glsl"); FString DxcPath = FPaths::ConvertRelativePathToFull(FPaths::EngineDir()); DxcPath = FPaths::Combine(DxcPath, TEXT("Binaries/ThirdParty/ShaderConductor/Win64")); FPaths::MakePlatformFilename(DxcPath); FString DxcFilename = FPaths::Combine(DxcPath, TEXT("dxc.exe")); FPaths::MakePlatformFilename(DxcFilename); // CompileDXC.bat { const FString DxcArguments = CompilerContext.GenerateDxcArguments(Options); FString BatchFileContents = FString::Printf( TEXT( "@ECHO OFF\n" "SET DXC=\"%s\"\n" "SET SPIRVCROSS=\"spirv-cross.exe\"\n" "IF NOT EXIST %%DXC%% (\n" "\tECHO Couldn't find dxc.exe under \"%s\"\n" "\tGOTO :END\n" ")\n" "ECHO Compiling with DXC...\n" "%%DXC%% %s -Fo %s %s\n" "WHERE %%SPIRVCROSS%%\n" "IF %%ERRORLEVEL%% NEQ 0 (\n" "\tECHO spirv-cross.exe not found in Path environment variable, please build it from source https://github.com/KhronosGroup/SPIRV-Cross\n" "\tGOTO :END\n" ")\n" "ECHO Translating SPIRV back to glsl...\n" "%%SPIRVCROSS%% --vulkan-semantics --output %s %s\n" ":END\n" "PAUSE\n" ), *DxcFilename, *DxcPath, *DxcArguments, *SPVFilename, *USFFilename, *GLSLFilename, *SPVFilename ); FFileHelper::SaveStringToFile(BatchFileContents, *(InternalState.Input.DumpDebugInfoPath / TEXT("CompileDXC.bat"))); } } // Quick and dirty way to get the location of the entrypoint in the source // NOTE: Preprocessed shaders have macros resolved and comments removed, it makes this easier... static FString ParseEntrypointDecl(FShaderSource::FViewType PreprocessedShader, FStringView Entrypoint) { FShaderSource::FStringType EntrypointConverted(Entrypoint); auto SkipWhitespace = [&](int32& Index) { while (FChar::IsWhitespace(PreprocessedShader[Index])) { ++Index; } }; auto EraseDebugLines = [](FString& EntryPointDecl) { int32 HashIndex; while (EntryPointDecl.FindChar(TEXT('#'), HashIndex)) { while ((HashIndex < EntryPointDecl.Len()) && (!FChar::IsLinebreak(EntryPointDecl[HashIndex]))) { EntryPointDecl[HashIndex] = TEXT(' '); ++HashIndex; } } }; FString EntryPointDecl; // Go through all the case sensitive matches in the source int32 EntrypointIndex = PreprocessedShader.Find(EntrypointConverted); check(EntrypointIndex != INDEX_NONE); while (EntrypointIndex != INDEX_NONE) { // This should be the beginning of a new word if ((EntrypointIndex == 0) || !FChar::IsWhitespace(PreprocessedShader[EntrypointIndex - 1])) { EntrypointIndex = PreprocessedShader.Find(EntrypointConverted, EntrypointIndex + 1); continue; } // The next thing after the entrypoint should its parameters // White space is allowed, so skip any that is found int32 ParamsStart = EntrypointIndex + Entrypoint.Len(); SkipWhitespace(ParamsStart); if (PreprocessedShader[ParamsStart] != '(') { EntrypointIndex = PreprocessedShader.Find(EntrypointConverted, ParamsStart); continue; } int32 ParamsEnd = PreprocessedShader.Find(ANSITEXTVIEW(")"), ParamsStart + 1); check(ParamsEnd != INDEX_NONE); if (ParamsEnd == INDEX_NONE) { // Suspicious EntrypointIndex = PreprocessedShader.Find(EntrypointConverted, ParamsStart); continue; } // Make sure to grab everything up to the function content int32 DeclEnd = ParamsEnd + 1; while (PreprocessedShader[DeclEnd] != '{' && (PreprocessedShader[DeclEnd] != ';')) { ++DeclEnd; } if (PreprocessedShader[DeclEnd] != '{') { EntrypointIndex = PreprocessedShader.Find(EntrypointConverted, DeclEnd); continue; } // Now back up to pick up the return value, the attributes and everything else that can come with it, like "[numthreads(1,1,1)]" int32 DeclBegin = EntrypointIndex - 1; while ( (DeclBegin > 0) && (PreprocessedShader[DeclBegin] != ';') && (PreprocessedShader[DeclBegin] != '}')) { --DeclBegin; } ++DeclBegin; EntryPointDecl = FString::ConstructFromPtrSize(&PreprocessedShader[DeclBegin], DeclEnd - DeclBegin); EraseDebugLines(EntryPointDecl); EntryPointDecl.TrimStartAndEndInline(); break; } return EntryPointDecl; } static uint8 ParseWaveSize( const FSpirvShaderCompilerInternalState& InternalState, FShaderSource::FViewType PreprocessedShader ) { uint8 WaveSize = 0; if (!InternalState.IsRayTracingShader()) { const FString EntrypointDecl = ParseEntrypointDecl(PreprocessedShader, InternalState.GetEntryPointName()); const FString WaveSizeMacro(TEXT("VULKAN_WAVESIZE(")); int32 WaveSizeIndex = EntrypointDecl.Find(*WaveSizeMacro, ESearchCase::CaseSensitive); while (WaveSizeIndex != INDEX_NONE) { const int32 StartNumber = WaveSizeIndex + WaveSizeMacro.Len(); const int32 EndNumber = EntrypointDecl.Find(TEXT(")"), ESearchCase::CaseSensitive, ESearchDir::FromStart, StartNumber); check(EndNumber != INDEX_NONE); FString WaveSizeValue = FString::ConstructFromPtrSize(&EntrypointDecl[StartNumber], EndNumber - StartNumber); WaveSizeValue.RemoveSpacesInline(); if (WaveSizeValue != TEXT("N")) // skip the macro decl { float FloatResult = 0.0; if (FMath::Eval(WaveSizeValue, FloatResult)) { checkf((FloatResult >= 0.0f) && (FloatResult < (float)MAX_uint8), TEXT("Specified wave size is too large for 8bit uint!")); WaveSize = static_cast(FloatResult); } else { check(WaveSizeValue.IsNumeric()); const int32 ConvertedWaveSize = FCString::Atoi(*WaveSizeValue); checkf((ConvertedWaveSize > 0) && (ConvertedWaveSize < MAX_uint8), TEXT("Specified wave size is too large for 8bit uint!")); WaveSize = (uint8)ConvertedWaveSize; } break; } WaveSizeIndex = EntrypointDecl.Find(*WaveSizeMacro, ESearchCase::CaseSensitive, ESearchDir::FromStart, EndNumber); } } // Take note of preferred wave size flag if none was specified in HLSL if ((WaveSize == 0) && InternalState.Input.Environment.CompilerFlags.Contains(CFLAG_Wave32)) { WaveSize = 32; } return WaveSize; } static bool CompileWithShaderConductor( const FSpirvShaderCompilerInternalState& InternalState, FShaderSource::FViewType PreprocessedShader, SpirvShaderCompilerSerializedOutput& SerializedOutput, FShaderCompilerOutput& Output ) { const FShaderCompilerInput& Input = InternalState.Input; CrossCompiler::FShaderConductorContext CompilerContext; // Inject additional macro definitions to circumvent missing features: external textures FShaderCompilerDefinitions AdditionalDefines; TArray ExtraDxcArgs; if (InternalState.IsSM6()) { ExtraDxcArgs.Add(TEXT("-fvk-allow-rwstructuredbuffer-arrays")); } // Fix issues when reading matrices directly for ByteAddrBuffer // By default the compiler will emit column-major loads and this flag makes sure to revert to the original behavior of row-major. ExtraDxcArgs.Add(TEXT("-fspv-use-legacy-buffer-matrix-order")); // Load shader source into compiler context CompilerContext.LoadSource(PreprocessedShader, Input.VirtualSourceFilePath, InternalState.GetEntryPointName(), InternalState.GetShaderFrequency(), &AdditionalDefines, &ExtraDxcArgs); // Initialize compilation options for ShaderConductor CrossCompiler::FShaderConductorOptions Options; Options.TargetEnvironment = InternalState.GetMinimumTargetEnvironment(); Options.bWarningsAsErrors = Input.Environment.CompilerFlags.Contains(CFLAG_WarningsAsErrors); // VK_EXT_scalar_block_layout is required by raytracing and by Nanite (so expect it to be present in SM6/Vulkan_1_3) Options.bDisableScalarBlockLayout = !(InternalState.IsRayTracingShader() || InternalState.IsSM6()); if (InternalState.IsRayTracingShader() || InternalState.IsSM6()) { // Use SM 6.6 as the baseline for Vulkan SM6 shaders Options.ShaderModel.Major = 6; Options.ShaderModel.Minor = 6; } if (Input.Environment.CompilerFlags.Contains(CFLAG_AllowRealTypes)) { Options.bEnable16bitTypes = true; } // Enable HLSL 2021 if specified if (Input.Environment.CompilerFlags.Contains(CFLAG_HLSL2021)) { Options.HlslVersion = 2021; } if (InternalState.bDebugDump) { SpirvCreateDXCCompileBatchFiles(CompilerContext, InternalState, Options); } // Before the shader rewritter removes all traces of it, pull any WAVESIZE directives from the shader source SerializedOutput.Header.WaveSize = ParseWaveSize(InternalState, PreprocessedShader); // Compile HLSL source to SPIR-V binary if (!CompilerContext.CompileHlslToSpirv(Options, SerializedOutput.Spirv.Data)) { CompilerContext.FlushErrors(Output.Errors); return false; } // If this shader samples R64 image formats, they need to get converted to STORAGE_IMAGE // todo-jnmo: Scope this with a CFLAG if it affects compilation times Patch64bitSamplers(SerializedOutput.Spirv); // Build shader output and binding table Output.bSucceeded = BuildShaderOutputFromSpirv(CompilerContext, InternalState, SerializedOutput, Output); // Flush warnings CompilerContext.FlushErrors(Output.Errors); // Return code reflection if requested for shader analysis if (Input.Environment.CompilerFlags.Contains(CFLAG_OutputAnalysisArtifacts) && Output.bSucceeded) { const TArray& SpirvData = SerializedOutput.Spirv.Data; FGenericShaderStat ShaderReflection; if (CrossCompiler::FShaderConductorContext::Disassemble(CrossCompiler::EShaderConductorIR::Spirv, SpirvData.GetData(), SpirvData.Num() * SpirvData.GetTypeSize(), ShaderReflection)) { ShaderReflection.StatName = FName(FString::Printf(TEXT("%s (%s)"), *ShaderReflection.StatName.ToString(), *InternalState.Input.EntryPointName)); Output.ShaderStatistics.Add(MoveTemp(ShaderReflection)); } } return true; } #endif // PLATFORM_MAC || PLATFORM_WINDOWS || PLATFORM_LINUX static void ModifyCompilerInput(FSpirvShaderCompilerInternalState& InternalState, FShaderCompilerInput& Input) { Input.Environment.SetDefine(TEXT("COMPILER_HLSLCC"), 1); Input.Environment.SetDefine(TEXT("COMPILER_VULKAN"), 1); if (InternalState.IsMobileES31()) { Input.Environment.SetDefine(TEXT("ES3_1_PROFILE"), 1); Input.Environment.SetDefine(TEXT("VULKAN_PROFILE"), 1); } else if (InternalState.IsSM6()) { Input.Environment.SetDefine(TEXT("VULKAN_PROFILE_SM6"), 1); Input.Environment.SetDefine(TEXT("PLATFORM_SUPPORTS_CALLABLE_SHADERS"), 1); } else if (InternalState.IsSM5()) { Input.Environment.SetDefine(TEXT("VULKAN_PROFILE_SM5"), 1); } Input.Environment.SetDefine(TEXT("row_major"), TEXT("")); Input.Environment.SetDefine(TEXT("COMPILER_SUPPORTS_ATTRIBUTES"), (uint32)1); Input.Environment.SetDefine(TEXT("COMPILER_SUPPORTS_DUAL_SOURCE_BLENDING_SLOT_DECORATION"), (uint32)1); Input.Environment.SetDefine(TEXT("PLATFORM_SUPPORTS_ROV"), 0); // Disabled until DXC->SPRIV ROV support is implemented if (Input.Environment.FullPrecisionInPS || (Input.SharedEnvironment.IsValid() && Input.SharedEnvironment->FullPrecisionInPS)) { Input.Environment.SetDefine(TEXT("FORCE_FLOATS"), (uint32)1); } if (Input.Environment.CompilerFlags.Contains(CFLAG_InlineRayTracing)) { Input.Environment.SetDefine(TEXT("PLATFORM_SUPPORTS_INLINE_RAY_TRACING"), 1); // Support is only garanteed on desktop currently Input.Environment.SetDefine(TEXT("VULKAN_SUPPORTS_RAY_TRACING_POSITION_FETCH"), InternalState.IsAndroid() ? 0 : 1); } if (Input.Environment.CompilerFlags.Contains(CFLAG_AllowRealTypes)) { Input.Environment.SetDefine(TEXT("PLATFORM_SUPPORTS_REAL_TYPES"), 1); } // We have ETargetEnvironment::Vulkan_1_1 by default as a min spec now { Input.Environment.SetDefine(TEXT("PLATFORM_SUPPORTS_SM6_0_WAVE_OPERATIONS"), 1); Input.Environment.SetDefine(TEXT("VULKAN_SUPPORTS_SUBGROUP_SIZE_CONTROL"), 1); } Input.Environment.SetDefine(TEXT("BINDLESS_SRV_ARRAY_PREFIX"), FShaderParameterParser::kBindlessSRVArrayPrefix); Input.Environment.SetDefine(TEXT("BINDLESS_UAV_ARRAY_PREFIX"), FShaderParameterParser::kBindlessUAVArrayPrefix); Input.Environment.SetDefine(TEXT("BINDLESS_SAMPLER_ARRAY_PREFIX"), FShaderParameterParser::kBindlessSamplerArrayPrefix); if (InternalState.IsAndroid()) { // On most Android devices uint64_t is unsupported so we emulate as 2 uint32_t's Input.Environment.SetDefine(TEXT("EMULATE_VKDEVICEADRESS"), 1); } if (Input.IsRayTracingShader()) { // Name of the structure in raytracing shader records in VulkanCommon.usf Input.RequiredSymbols.Add(TEXT("HitGroupSystemRootConstants")); // Always remove dead code for ray tracing shaders regardless of cvar settings, // we can't support multiple entrypoints remaining in the binaries Input.Environment.CompilerFlags.Add(CFLAG_RemoveDeadCode); } } static void UpdateBindlessUBs(const FSpirvShaderCompilerInternalState& InternalState, SpirvShaderCompilerSerializedOutput& SerializedOutput, FShaderCompilerOutput& Output) { checkf(SerializedOutput.Header.Bindings.Num() == 0, TEXT("Shaders using bindless UBs should have no other bindings.")); for (int32 CBIndex = 0; CBIndex < InternalState.AllBindlessUBs.Num(); CBIndex++) { const FString& CBName = InternalState.AllBindlessUBs[CBIndex]; // It's possible SPIRV compilation has optimized out a buffer from every shader in the group if (SerializedOutput.UsedBindlessUB.Contains(CBName)) { FVulkanShaderHeader::FUniformBufferInfo& Info = SerializedOutput.Header.UniformBufferInfos.AddZeroed_GetRef(); Info.LayoutHash = SpirvShaderCompiler::GetUBLayoutHash(InternalState.Input, CBName); Info.BindlessCBIndex = CBIndex; const int32 UBIndex = SerializedOutput.Header.UniformBufferInfos.Num() - 1; Output.ParameterMap.AddParameterAllocation(CBName, UBIndex, 0, 1, EShaderParameterType::UniformBuffer); } } } // :todo-jn: TEMPORARY EXPERIMENT - will eventually move into preprocessing step static TArray ConvertUBToBindless(FString& PreprocessedShaderSource) { // Fill a map so we pull our bindless sampler/resource indices from the right struct // :todo-jn: Do we not have the layout somewhere instead of calculating offsets? there must be a better way... auto GenerateNewDecl = [](const int32 CBIndex, const FString& Members, const FString& CBName) { const FString PrefixedCBName = FString::Printf(TEXT("%s%d_%s"), *SpirvShaderCompiler::kBindlessCBPrefix, CBIndex, *CBName); const FString BindlessCBType = PrefixedCBName + TEXT("_Type"); const FString BindlessCBHeapName = PrefixedCBName + SpirvShaderCompiler::kBindlessHeapSuffix; const FString PaddingName = FString::Printf(TEXT("%s_Padding"), *CBName); FString CBDecl; CBDecl.Reserve(Members.Len() * 3); // start somewhere approx less bad // Declare the struct CBDecl += TEXT("struct ") + BindlessCBType + TEXT(" \n{\n") + Members + TEXT("\n};\n"); // Declare the safetype and bindless array for this cb CBDecl += FString::Printf(TEXT("ConstantBuffer<%s> %s[];\n"), *BindlessCBType, *BindlessCBHeapName); // Now bring in the CB CBDecl += FString::Printf(TEXT("static const %s %s = %s[VulkanHitGroupSystemParameters.BindlessUniformBuffers[%d]];\n"), *BindlessCBType, *PrefixedCBName, *BindlessCBHeapName, CBIndex); // Now create a global scope var for each value (as the cbuffer would provide) to patch in seemlessly with the rest of the code uint32 MemberOffset = 0; const TCHAR* MemberSearchPtr = *Members; const uint32 LastMemberSemicolonIndex = Members.Find(TEXT(";"), ESearchCase::CaseSensitive, ESearchDir::FromEnd, -1); check(LastMemberSemicolonIndex != INDEX_NONE); const TCHAR* LastMemberSemicolon = &Members[LastMemberSemicolonIndex]; do { const TCHAR* MemberTypeStartPtr = nullptr; const TCHAR* MemberTypeEndPtr = nullptr; ParseHLSLTypeName(MemberSearchPtr, MemberTypeStartPtr, MemberTypeEndPtr); const FString MemberTypeName = FString::ConstructFromPtrSize(MemberTypeStartPtr, MemberTypeEndPtr - MemberTypeStartPtr); FString MemberName; MemberSearchPtr = ParseHLSLSymbolName(MemberTypeEndPtr, MemberName); check(MemberName.Len() > 0); if (MemberName.StartsWith(PaddingName)) { while (*MemberSearchPtr && *MemberSearchPtr != ';') { MemberSearchPtr++; } } else { // Skip over trailing tokens and pick up arrays FString ArrayDecl; while (*MemberSearchPtr && *MemberSearchPtr != ';') { if (*MemberSearchPtr == '[') { ArrayDecl.AppendChar(*MemberSearchPtr); MemberSearchPtr++; while (*MemberSearchPtr && *MemberSearchPtr != ']') { ArrayDecl.AppendChar(*MemberSearchPtr); MemberSearchPtr++; } ArrayDecl.AppendChar(*MemberSearchPtr); } MemberSearchPtr++; } CBDecl += FString::Printf(TEXT("static const %s %s%s = %s.%s;\n"), *MemberTypeName, *MemberName, *ArrayDecl, *PrefixedCBName, *MemberName); } MemberSearchPtr++; } while (MemberSearchPtr < LastMemberSemicolon); return CBDecl; }; // replace "cbuffer" decl with a struct filled from bindless constant buffer TArray BindlessUBs; { const FString UniformBufferDeclIdentifier = TEXT("cbuffer"); int32 SearchIndex = PreprocessedShaderSource.Find(UniformBufferDeclIdentifier, ESearchCase::CaseSensitive, ESearchDir::FromStart, -1); while (SearchIndex != INDEX_NONE) { FString StructName; const TCHAR* StructNameEndPtr = ParseHLSLSymbolName(&PreprocessedShaderSource[SearchIndex + UniformBufferDeclIdentifier.Len()], StructName); check(StructName.Len() > 0); const int32 CBIndex = BindlessUBs.Add(StructName); check(CBIndex < 16); const TCHAR* OpeningBracePtr = FCString::Strstr(&PreprocessedShaderSource[SearchIndex + UniformBufferDeclIdentifier.Len()], TEXT("{")); check(OpeningBracePtr); const TCHAR* ClosingBracePtr = FindMatchingClosingBrace(OpeningBracePtr + 1); check(ClosingBracePtr); const int32 ClosingBraceIndex = ClosingBracePtr - (*PreprocessedShaderSource); const FString Members = FString::ConstructFromPtrSize(OpeningBracePtr + 1, ClosingBracePtr - OpeningBracePtr - 1); const FString NewDecl = GenerateNewDecl(CBIndex, Members, StructName); const int32 OldDeclLen = ClosingBraceIndex - SearchIndex + 1; PreprocessedShaderSource.RemoveAt(SearchIndex, OldDeclLen, EAllowShrinking::No); PreprocessedShaderSource.InsertAt(SearchIndex, NewDecl); SearchIndex = PreprocessedShaderSource.Find(UniformBufferDeclIdentifier, ESearchCase::CaseSensitive, ESearchDir::FromStart, SearchIndex + NewDecl.Len()); } } return BindlessUBs; } static bool CompileShaderGroup( FSpirvShaderCompilerInternalState& InternalState, const FShaderSource::FStringType& OriginalPreprocessedShaderSource, FShaderCompilerOutput& MergedOutput ) { checkf(InternalState.bSupportsBindless && InternalState.bUseBindlessUniformBuffer, TEXT("Ray tracing requires full bindless in Vulkan.")); // Compile each one of the shader modules seperately and create one big blob for the engine auto CompilePartialExport = [&OriginalPreprocessedShaderSource, &InternalState, &MergedOutput]( FSpirvShaderCompilerInternalState::EHitGroupShaderType HitGroupShaderType, const TCHAR* PartialFileExtension, SpirvShaderCompilerSerializedOutput& PartialSerializedOutput) { InternalState.HitGroupShaderType = HitGroupShaderType; FShaderCompilerOutput TempOutput; const bool bIsClosestHit = (HitGroupShaderType == FSpirvShaderCompilerInternalState::EHitGroupShaderType::ClosestHit); FShaderCompilerOutput& PartialOutput = bIsClosestHit ? MergedOutput : TempOutput; FShaderSource::FViewType OrigSourceView(OriginalPreprocessedShaderSource); FShaderSource PartialPreprocessedShaderSource(OrigSourceView); UE::ShaderCompilerCommon::RemoveDeadCode(PartialPreprocessedShaderSource, InternalState.GetEntryPointName(), PartialOutput.Errors); if (InternalState.bDebugDump) { DumpDebugShaderText(InternalState.Input, PartialPreprocessedShaderSource.GetView().GetData(), *FString::Printf(TEXT("%s.hlsl"), PartialFileExtension)); } const bool bPartialSuccess = SpirvShaderCompiler::CompileWithShaderConductor(InternalState, PartialPreprocessedShaderSource.GetView(), PartialSerializedOutput, PartialOutput); if (!bIsClosestHit) { MergedOutput.NumInstructions = FMath::Max(MergedOutput.NumInstructions, PartialOutput.NumInstructions); MergedOutput.NumTextureSamplers = FMath::Max(MergedOutput.NumTextureSamplers, PartialOutput.NumTextureSamplers); MergedOutput.Errors.Append(MoveTemp(PartialOutput.Errors)); } return bPartialSuccess; }; bool bSuccess = false; // Closest Hit Module, always present SpirvShaderCompilerSerializedOutput ClosestHitSerializedOutput; { bSuccess = CompilePartialExport(FSpirvShaderCompilerInternalState::EHitGroupShaderType::ClosestHit, TEXT("closest"), ClosestHitSerializedOutput); } // Any Hit Module, optional const bool bHasAnyHitModule = !InternalState.AnyHitEntry.IsEmpty(); SpirvShaderCompilerSerializedOutput AnyHitSerializedOutput; if (bSuccess && bHasAnyHitModule) { bSuccess = CompilePartialExport(FSpirvShaderCompilerInternalState::EHitGroupShaderType::AnyHit, TEXT("anyhit"), AnyHitSerializedOutput); } // Intersection Module, optional const bool bHasIntersectionModule = !InternalState.IntersectionEntry.IsEmpty(); SpirvShaderCompilerSerializedOutput IntersectionSerializedOutput; if (bSuccess && bHasIntersectionModule) { bSuccess = CompilePartialExport(FSpirvShaderCompilerInternalState::EHitGroupShaderType::Intersection, TEXT("intersection"), IntersectionSerializedOutput); } // Collapse the bindless UB usage into one set and then update the headers ClosestHitSerializedOutput.UsedBindlessUB.Append(AnyHitSerializedOutput.UsedBindlessUB); ClosestHitSerializedOutput.UsedBindlessUB.Append(IntersectionSerializedOutput.UsedBindlessUB); UpdateBindlessUBs(InternalState, ClosestHitSerializedOutput, MergedOutput); { // :todo-jn: Having multiple entrypoints in a single SPIRV blob crashes on FLumenHardwareRayTracingMaterialHitGroup for some reason // Adjust the header before we write it out ClosestHitSerializedOutput.Header.RayGroupAnyHit = bHasAnyHitModule ? FVulkanShaderHeader::ERayHitGroupEntrypoint::SeparateBlob : FVulkanShaderHeader::ERayHitGroupEntrypoint::NotPresent; ClosestHitSerializedOutput.Header.RayGroupIntersection = bHasIntersectionModule ? FVulkanShaderHeader::ERayHitGroupEntrypoint::SeparateBlob : FVulkanShaderHeader::ERayHitGroupEntrypoint::NotPresent; check(ClosestHitSerializedOutput.Spirv.Data.Num() != 0); FMemoryWriter Ar(MergedOutput.ShaderCode.GetWriteAccess(), true); Ar << ClosestHitSerializedOutput.Header; Ar << ClosestHitSerializedOutput.ShaderResourceTable; { uint32 SpirvCodeSizeBytes = ClosestHitSerializedOutput.Spirv.GetByteSize(); Ar << SpirvCodeSizeBytes; Ar.Serialize((uint8*)ClosestHitSerializedOutput.Spirv.Data.GetData(), SpirvCodeSizeBytes); } if (bHasAnyHitModule) { uint32 SpirvCodeSizeBytes = AnyHitSerializedOutput.Spirv.GetByteSize(); Ar << SpirvCodeSizeBytes; Ar.Serialize((uint8*)AnyHitSerializedOutput.Spirv.Data.GetData(), SpirvCodeSizeBytes); } if (bHasIntersectionModule) { uint32 SpirvCodeSizeBytes = IntersectionSerializedOutput.Spirv.GetByteSize(); Ar << SpirvCodeSizeBytes; Ar.Serialize((uint8*)IntersectionSerializedOutput.Spirv.Data.GetData(), SpirvCodeSizeBytes); } } // Return code reflection if requested for shader analysis if (InternalState.Input.Environment.CompilerFlags.Contains(CFLAG_OutputAnalysisArtifacts) && bSuccess) { { const TArray& SpirvData = ClosestHitSerializedOutput.Spirv.Data; FGenericShaderStat ClosestHitReflection; if (CrossCompiler::FShaderConductorContext::Disassemble(CrossCompiler::EShaderConductorIR::Spirv, SpirvData.GetData(), SpirvData.Num() * SpirvData.GetTypeSize(), ClosestHitReflection)) { ClosestHitReflection.StatName = FName(FString::Printf(TEXT("%s (%s)"), *ClosestHitReflection.StatName.ToString(), *InternalState.GetEntryPointName())); MergedOutput.ShaderStatistics.Add(MoveTemp(ClosestHitReflection)); } } if (bHasAnyHitModule) { const TArray& SpirvData = AnyHitSerializedOutput.Spirv.Data; FGenericShaderStat AnyHitReflection; if (CrossCompiler::FShaderConductorContext::Disassemble(CrossCompiler::EShaderConductorIR::Spirv, SpirvData.GetData(), SpirvData.Num() * SpirvData.GetTypeSize(), AnyHitReflection)) { AnyHitReflection.StatName = FName(FString::Printf(TEXT("%s (%s)"), *AnyHitReflection.StatName.ToString(), *InternalState.AnyHitEntry)); MergedOutput.ShaderStatistics.Add(MoveTemp(AnyHitReflection)); } } if (bHasIntersectionModule) { const TArray& SpirvData = IntersectionSerializedOutput.Spirv.Data; FGenericShaderStat IntersectionReflection; if (CrossCompiler::FShaderConductorContext::Disassemble(CrossCompiler::EShaderConductorIR::Spirv, SpirvData.GetData(), SpirvData.Num()* SpirvData.GetTypeSize(), IntersectionReflection)) { IntersectionReflection.StatName = FName(FString::Printf(TEXT("%s (%s)"), *IntersectionReflection.StatName.ToString(), *InternalState.IntersectionEntry)); MergedOutput.ShaderStatistics.Add(MoveTemp(IntersectionReflection)); } } } MergedOutput.bSucceeded = bSuccess; return bSuccess; } }; // SpirvShaderCompiler