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

383 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
// .
#include "RHIShaderFormatDefinitions.inl"
#include "ShaderCompilerCommon.h"
#include "ShaderCompilerDefinitions.h"
#include "ShaderParameterParser.h"
#include "ShaderPreprocessTypes.h"
#include "SpirvReflectCommon.h"
#include "VulkanCommon.h"
#include "VulkanThirdParty.h"
#include "VulkanBackend.h"
#include "VulkanShaderResources.h"
#include "SpirVShaderCompiler.inl"
inline bool IsVulkanShaderFormat(FName ShaderFormat)
{
return ShaderFormat == NAME_VULKAN_ES3_1_ANDROID
|| ShaderFormat == NAME_VULKAN_ES3_1
|| ShaderFormat == NAME_VULKAN_SM5
|| ShaderFormat == NAME_VULKAN_SM6
|| ShaderFormat == NAME_VULKAN_SM5_ANDROID;
}
inline bool IsAndroidShaderFormat(FName ShaderFormat)
{
return ShaderFormat == NAME_VULKAN_ES3_1_ANDROID
|| ShaderFormat == NAME_VULKAN_SM5_ANDROID;
}
enum class EVulkanShaderVersion
{
ES3_1,
ES3_1_ANDROID,
SM5,
SM5_ANDROID,
SM6,
Invalid,
};
DEFINE_LOG_CATEGORY_STATIC(LogVulkanShaderCompiler, Log, All);
class FVulkanShaderCompilerInternalState : public FSpirvShaderCompilerInternalState
{
EVulkanShaderVersion FormatToVersion(FName Format)
{
if (Format == NAME_VULKAN_ES3_1)
{
return EVulkanShaderVersion::ES3_1;
}
else if (Format == NAME_VULKAN_ES3_1_ANDROID)
{
return EVulkanShaderVersion::ES3_1_ANDROID;
}
else if (Format == NAME_VULKAN_SM5_ANDROID)
{
return EVulkanShaderVersion::SM5_ANDROID;
}
else if (Format == NAME_VULKAN_SM5)
{
return EVulkanShaderVersion::SM5;
}
else if (Format == NAME_VULKAN_SM6)
{
return EVulkanShaderVersion::SM6;
}
else
{
FString FormatStr = Format.ToString();
checkf(0, TEXT("Invalid shader format passed to Vulkan shader compiler: %s"), *FormatStr);
return EVulkanShaderVersion::Invalid;
}
}
public:
FVulkanShaderCompilerInternalState(const FShaderCompilerInput& InInput, const FShaderParameterParser* InParameterParser)
: FSpirvShaderCompilerInternalState(InInput, InParameterParser)
{
Version = FormatToVersion(Input.ShaderFormat);
if (Version == EVulkanShaderVersion::SM6)
{
MinimumTargetEnvironment = CrossCompiler::FShaderConductorOptions::ETargetEnvironment::Vulkan_1_3;
}
else if (Input.IsRayTracingShader() || Input.Environment.CompilerFlags.Contains(CFLAG_InlineRayTracing))
{
MinimumTargetEnvironment = CrossCompiler::FShaderConductorOptions::ETargetEnvironment::Vulkan_1_2;
}
else
{
MinimumTargetEnvironment = CrossCompiler::FShaderConductorOptions::ETargetEnvironment::Vulkan_1_1;
}
bIsAndroid = IsAndroidShaderFormat(Input.ShaderFormat);
bSupportsOfflineCompiler =
Input.ShaderFormat == NAME_VULKAN_ES3_1_ANDROID ||
Input.ShaderFormat == NAME_VULKAN_ES3_1 ||
Input.ShaderFormat == NAME_VULKAN_SM5_ANDROID;;
}
bool IsSM6() const override final
{
return (Version == EVulkanShaderVersion::SM6);
}
bool IsSM5() const override final
{
return (Version == EVulkanShaderVersion::SM5) || (Version == EVulkanShaderVersion::SM5_ANDROID);
}
bool IsMobileES31() const override final
{
return (Version == EVulkanShaderVersion::ES3_1 || Version == EVulkanShaderVersion::ES3_1_ANDROID);
}
CrossCompiler::FShaderConductorOptions::ETargetEnvironment GetMinimumTargetEnvironment() const override final
{
return MinimumTargetEnvironment;
}
bool IsAndroid() const override final
{
return bIsAndroid;
}
bool SupportsOfflineCompiler() const override final
{
return bSupportsOfflineCompiler;
}
private:
EVulkanShaderVersion Version;
CrossCompiler::FShaderConductorOptions::ETargetEnvironment MinimumTargetEnvironment;
bool bIsAndroid = false;
bool bSupportsOfflineCompiler = false;;
};
void ModifyVulkanCompilerInput(FShaderCompilerInput& Input)
{
FVulkanShaderCompilerInternalState InternalState(Input, nullptr);
SpirvShaderCompiler::ModifyCompilerInput(InternalState, Input);
}
// Helper function to know how much space to set aside in the shader record for a global
static uint32 GetSizeForType(FStringView TypeName, FStringView ArraySize)
{
static TMap<FStringView, uint32> SizeForTypeMap;
if (SizeForTypeMap.Num() == 0)
{
SizeForTypeMap.Add(FStringView(TEXT("uint")), 4);
SizeForTypeMap.Add(FStringView(TEXT("uint2")), 8);
SizeForTypeMap.Add(FStringView(TEXT("uint3")), 12);
SizeForTypeMap.Add(FStringView(TEXT("uint4")), 16);
SizeForTypeMap.Add(FStringView(TEXT("float")), 4);
SizeForTypeMap.Add(FStringView(TEXT("float2")), 8);
SizeForTypeMap.Add(FStringView(TEXT("float3")), 12);
SizeForTypeMap.Add(FStringView(TEXT("float4")), 16);
}
checkf(ArraySize.Len() == 0, TEXT("Need to add array support!")); // :todo-jn: Add array parsing
const uint32* TypeSize = SizeForTypeMap.Find(TypeName);
checkf(TypeSize, TEXT("Missing type size for %.*s"), TypeName.Len(), TypeName.GetData());
return *TypeSize;
}
// :todo-jn: TEMPORARY EXPERIMENT - will eventually move into preprocessing step
static uint32 ConvertGlobalsToShaderRecord(const FShaderParameterParser& ShaderParameterParser, const TMap<FStringView, FStringView>& ReplacedGlobals, FString& PreprocessedShaderSource, FShaderCompilerOutput& Output)
{
uint32 ShaderRecordGlobalsSize = 0;
uint32 ShaderRecordParamCount = 0;
FString ShaderRecordGlobalsString;
for (const auto& ParamDecl : ReplacedGlobals)
{
ShaderRecordGlobalsString += ParamDecl.Value;
const FString ParamName(ParamDecl.Key);
const FShaderParameterParser::FParsedShaderParameter& Info = ShaderParameterParser.FindParameterInfos(ParamName);
const uint32 ParamSize = GetSizeForType(Info.ParsedType, Info.ParsedArraySize);
HandleReflectedGlobalConstantBufferMember(
ParamName,
ShaderRecordParamCount++,
ShaderRecordGlobalsSize,
ParamSize,
Output
);
ShaderRecordGlobalsSize += ParamSize;
}
if (ShaderRecordGlobalsString.Len())
{
const int32 ReplacementCount = PreprocessedShaderSource.ReplaceInline(TEXT("uint VulkanShaderRecordDummyGlobals;"), *ShaderRecordGlobalsString, ESearchCase::CaseSensitive);
checkf(ReplacementCount == 1, TEXT("VulkanShaderRecordDummyGlobals was replaced %d times!"), ReplacementCount);
}
return ShaderRecordGlobalsSize;
}
struct FVulkanShaderParameterParserPlatformConfiguration : public FShaderParameterParser::FPlatformConfiguration
{
FVulkanShaderParameterParserPlatformConfiguration(const FShaderCompilerInput& Input, TMap<FStringView, FStringView>& InReplacedGlobals)
: FShaderParameterParser::FPlatformConfiguration()
, bIsRayTracingShader(Input.IsRayTracingShader())
, HitGroupSystemIndexBufferName(FShaderParameterParser::kBindlessSRVPrefix + FString(TEXT("HitGroupSystemIndexBuffer")))
, HitGroupSystemVertexBufferName(FShaderParameterParser::kBindlessSRVPrefix + FString(TEXT("HitGroupSystemVertexBuffer")))
, ReplacedGlobals(InReplacedGlobals)
{
EnumAddFlags(Flags, EShaderParameterParserConfigurationFlags::SupportsBindless | EShaderParameterParserConfigurationFlags::BindlessUsesArrays);
// Create a _RootShaderParameters and bind it in slot 0 like any other uniform buffer
if (Input.Target.GetFrequency() == SF_RayGen && Input.RootParametersStructure != nullptr)
{
ConstantBufferType = TEXTVIEW("cbuffer");
EnumAddFlags(Flags, EShaderParameterParserConfigurationFlags::UseStableConstantBuffer);
}
// Place loose data params in the shader record for shaders with bindless UBs
if (bIsRayTracingShader && (Input.Target.GetFrequency() != SF_RayGen))
{
EnumAddFlags(Flags, EShaderParameterParserConfigurationFlags::ReplaceGlobals);
}
}
virtual FString GenerateBindlessAccess(EBindlessConversionType BindlessType, FStringView FullTypeString, FStringView ArrayNameOverride, FStringView IndexString) const final
{
// GetSRVFromHeap(Type, Index) VULKAN_SRV_HEAP(Type)[VULKAN_HEAP_ACCESS(Index)]
// GetUAVFromHeap(Type, Index) VULKAN_UAV_HEAP(Type)[VULKAN_HEAP_ACCESS(Index)]
// GetSamplerFromHeap(Type, Index) VULKAN_SAMPLER_HEAP(Type)[VULKAN_HEAP_ACCESS(Index)]
if (bIsRayTracingShader)
{
if (BindlessType == EBindlessConversionType::SRV)
{
// Patch the HitGroupSystemIndexBuffer/HitGroupSystemVertexBuffer indices to use the ones contained in the shader record
if (IndexString == HitGroupSystemIndexBufferName)
{
IndexString = TEXTVIEW("VulkanHitGroupSystemParameters.BindlessHitGroupSystemIndexBuffer");
}
else if (IndexString == HitGroupSystemVertexBufferName)
{
IndexString = TEXTVIEW("VulkanHitGroupSystemParameters.BindlessHitGroupSystemVertexBuffer");
}
}
// Raytracing shaders need NonUniformResourceIndex because bindless index can be divergent in hit/miss/callable shaders
return FString::Printf(TEXT("%.*s[NonUniformResourceIndex(%.*s)]"),
ArrayNameOverride.Len(), ArrayNameOverride.GetData(),
IndexString.Len(), IndexString.GetData());
}
return FString::Printf(TEXT("%.*s[%.*s]"),
ArrayNameOverride.Len(), ArrayNameOverride.GetData(),
IndexString.Len(), IndexString.GetData());
}
// Fill the global with the value stored in the shader record
virtual FString ReplaceGlobal(FStringView FullDeclString, FStringView ParamName) const final
{
ReplacedGlobals.Add(ParamName, FullDeclString);
FString NewDecl(FullDeclString);
NewDecl = TEXT("static ") + NewDecl;
NewDecl.InsertAt(NewDecl.Find(TEXT(";")), FString::Printf(TEXT(" = VulkanHitGroupSystemParameters.Globals.%.*s"), ParamName.Len(), ParamName.GetData()));
return NewDecl;
}
const bool bIsRayTracingShader;
const FString HitGroupSystemIndexBufferName;
const FString HitGroupSystemVertexBufferName;
TMap<FStringView, FStringView>& ReplacedGlobals;
};
void CompileVulkanShader(const FShaderCompilerInput& Input, const FShaderPreprocessOutput& InPreprocessOutput, FShaderCompilerOutput& Output, const class FString& WorkingDirectory)
{
check(IsVulkanShaderFormat(Input.ShaderFormat));
FString EntryPointName = Input.EntryPointName;
FString PreprocessedSource(InPreprocessOutput.GetSourceViewWide());
TMap<FStringView, FStringView> ReplacedGlobals; // Note: these FStringView point to memory in FShaderParameterParser
FVulkanShaderParameterParserPlatformConfiguration PlatformConfiguration(Input, ReplacedGlobals);
FShaderParameterParser ShaderParameterParser(PlatformConfiguration);
if (!ShaderParameterParser.ParseAndModify(Input, Output.Errors, PreprocessedSource))
{
// The FShaderParameterParser will add any relevant errors.
return;
}
FVulkanShaderCompilerInternalState InternalState(Input, &ShaderParameterParser);
if (InternalState.bUseBindlessUniformBuffer)
{
InternalState.ShaderRecordGlobalsSize = ConvertGlobalsToShaderRecord(ShaderParameterParser, ReplacedGlobals, PreprocessedSource, Output);
InternalState.AllBindlessUBs = SpirvShaderCompiler::ConvertUBToBindless(PreprocessedSource);
}
if (ShaderParameterParser.DidModifyShader() || InternalState.AllBindlessUBs.Num() || InternalState.ShaderRecordGlobalsSize)
{
Output.ModifiedShaderSource = PreprocessedSource;
}
bool bSuccess = false;
// Convert to ANSI prior to calling into ShaderConductor. This copy would have been incurred
// by SC itself anyways, but would (will?) also be unnecessary if (when) shader parameter parser
// is modified to operate on ANSI strings.
const FShaderSource::FStringType PreprocessedSourceToCompile(PreprocessedSource);
#if PLATFORM_MAC || PLATFORM_WINDOWS || PLATFORM_LINUX
// HitGroup shaders might have multiple entrypoints that we combine into a single blob
if (InternalState.HasMultipleEntryPoints())
{
bSuccess = SpirvShaderCompiler::CompileShaderGroup(InternalState, PreprocessedSourceToCompile, Output);
}
else
{
// Compile regular shader via ShaderConductor (DXC)
SpirvShaderCompilerSerializedOutput SerializedOutput;
bSuccess = SpirvShaderCompiler::CompileWithShaderConductor(InternalState, PreprocessedSourceToCompile, SerializedOutput, Output);
if (InternalState.bUseBindlessUniformBuffer)
{
SpirvShaderCompiler::UpdateBindlessUBs(InternalState, SerializedOutput, Output);
}
// Write out the header and shader source code (except for the extra shaders in hit groups)
checkf(!(bSuccess && SerializedOutput.Spirv.Data.Num() == 0), TEXT("shader compilation was reported as successful but SPIR-V module is empty"));
FMemoryWriter Ar(Output.ShaderCode.GetWriteAccess(), true);
Ar << SerializedOutput.Header;
Ar << SerializedOutput.ShaderResourceTable;
uint32 SpirvCodeSizeBytes = SerializedOutput.Spirv.GetByteSize();
Ar << SpirvCodeSizeBytes;
if (SerializedOutput.Spirv.Data.Num() > 0)
{
Ar.Serialize((uint8*)SerializedOutput.Spirv.Data.GetData(), SpirvCodeSizeBytes);
}
SpirvShaderCompiler::FillShaderResourceUsageFlags(InternalState, SerializedOutput);
Output.ShaderCode.AddOptionalData(SerializedOutput.PackedResourceCounts);
}
#endif // PLATFORM_MAC || PLATFORM_WINDOWS || PLATFORM_LINUX
if (InternalState.bUseBindlessUniformBuffer)
{
// HACK: Because of heavy code alterations with bindless ray tracing shaders, line numbers will be all over the place. Remove the tag that leads to remapping...
for (FShaderCompilerError& ErrorMsg : Output.Errors)
{
ErrorMsg.StrippedErrorMessage.ReplaceInline(TEXT("__UE_FILENAME_SENTINEL"), *Input.GenerateShaderName());
}
}
if (Input.Environment.CompilerFlags.Contains(CFLAG_ExtraShaderData))
{
Output.ShaderCode.AddOptionalData(FShaderCodeName::Key, TCHAR_TO_UTF8(*Input.GenerateShaderName()));
}
Output.SerializeShaderCodeValidation();
ShaderParameterParser.ValidateShaderParameterTypes(Input, InternalState.IsMobileES31(), Output);
if (EnumHasAnyFlags(Input.DebugInfoFlags, EShaderDebugInfoFlags::CompileFromDebugUSF))
{
for (const auto& Error : Output.Errors)
{
FPlatformMisc::LowLevelOutputDebugStringf(TEXT("%s\n"), *Error.GetErrorStringWithLineMarker());
}
ensure(bSuccess);
}
}