You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderVariantAssetBuilder.cpp

1138 lines
62 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <ShaderVariantAssetBuilder.h>
#include <Atom/RPI.Reflect/Shader/ShaderAsset.h>
#include <Atom/RPI.Reflect/Shader/ShaderVariantAsset.h>
#include <Atom/RPI.Reflect/Shader/ShaderVariantTreeAsset.h>
#include <Atom/RPI.Reflect/Shader/ShaderOptionGroup.h>
#include <Atom/RPI.Edit/Shader/ShaderVariantListSourceData.h>
#include <Atom/RPI.Edit/Shader/ShaderVariantAssetCreator.h>
#include <Atom/RPI.Edit/Shader/ShaderVariantTreeAssetCreator.h>
#include <Atom/RPI.Edit/Common/JsonUtils.h>
#include <AzCore/Serialization/Json/JsonUtils.h>
#include <Atom/RPI.Reflect/Shader/ShaderVariantKey.h>
#include <Atom/RHI.Edit/Utils.h>
#include <Atom/RHI.Edit/ShaderPlatformInterface.h>
#include <Atom/RPI.Edit/Common/JsonReportingHelper.h>
#include <Atom/RPI.Edit/Common/AssetUtils.h>
#include <Atom/RHI.Reflect/ConstantsLayout.h>
#include <Atom/RHI.Reflect/PipelineLayoutDescriptor.h>
#include <Atom/RHI.Reflect/ShaderStageFunction.h>
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
#include <AzToolsFramework/Debug/TraceContext.h>
#include <AzFramework/API/ApplicationAPI.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzFramework/IO/LocalFileIO.h>
#include <AzFramework/Platform/PlatformDefaults.h>
#include <AzCore/Asset/AssetManager.h>
#include <AzCore/JSON/document.h>
#include <AzCore/IO/FileIO.h>
#include <AzCore/IO/IOUtils.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/std/algorithm.h>
#include <AzCore/std/string/string.h>
#include <AzCore/std/sort.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include "ShaderAssetBuilder.h"
#include "ShaderBuilderUtility.h"
#include "SrgLayoutUtility.h"
#include "AzslData.h"
#include "AzslCompiler.h"
#include <CommonFiles/Preprocessor.h>
#include <CommonFiles/GlobalBuildOptions.h>
#include <ShaderPlatformInterfaceRequest.h>
#include "AtomShaderConfig.h"
namespace AZ
{
namespace ShaderBuilder
{
static constexpr char ShaderVariantAssetBuilderName[] = "ShaderVariantAssetBuilder";
//! Adds source file dependencies for every place a referenced file may appear, and detects if one of
//! those possible paths resolves to the expected file.
//! @param currentFilePath - the full path to the file being processed
//! @param referencedParentPath - the path to a reference file, which may be relative to the @currentFilePath, or may be a full asset path.
//! @param sourceFileDependencies - new source file dependencies will be added to this list
//! @param foundSourceFile - if one of the source file dependencies is found, the highest priority one will be indicated here, otherwise this will be empty.
//! @return true if the referenced file was found and @foundSourceFile was set
bool LocateReferencedSourceFile(
AZStd::string_view currentFilePath, AZStd::string_view referencedParentPath,
AZStd::vector<AssetBuilderSDK::SourceFileDependency>& sourceFileDependencies,
AZStd::string& foundSourceFile)
{
foundSourceFile.clear();
bool found = false;
AZStd::vector<AZStd::string> possibleDependencies = RPI::AssetUtils::GetPossibleDepenencyPaths(currentFilePath, referencedParentPath);
for (auto& file : possibleDependencies)
{
AssetBuilderSDK::SourceFileDependency sourceFileDependency;
sourceFileDependency.m_sourceFileDependencyPath = file;
sourceFileDependencies.push_back(sourceFileDependency);
if (!found)
{
AZ::Data::AssetInfo sourceInfo;
AZStd::string watchFolder;
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(found, &AzToolsFramework::AssetSystem::AssetSystemRequest::GetSourceInfoBySourcePath, file.c_str(), sourceInfo, watchFolder);
if (found)
{
foundSourceFile = file;
}
}
}
return found;
}
//! Returns true if @sourceFileFullPath starts with a valid asset processor scan folder, false otherwise.
//! In case of true, it splits @sourceFileFullPath into @scanFolderFullPath and @filePathFromScanFolder.
//! @sourceFileFullPath The full path to a source asset file.
//! @scanFolderFullPath [out] Gets the full path of the scan folder where the source file is located.
//! @filePathFromScanFolder [out] Get the file path relative to @scanFolderFullPath.
static bool SplitSourceAssetPathIntoScanFolderFullPathAndRelativeFilePath(const AZStd::string& sourceFileFullPath, AZStd::string& scanFolderFullPath, AZStd::string& filePathFromScanFolder)
{
AZStd::vector<AZStd::string> scanFolders;
bool success = false;
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(success, &AzToolsFramework::AssetSystem::AssetSystemRequest::GetAssetSafeFolders, scanFolders);
if (!success)
{
AZ_Error(ShaderVariantAssetBuilderName, false, "Couldn't get the scan folders");
return false;
}
for (AZStd::string scanFolder : scanFolders)
{
AzFramework::StringFunc::Path::Normalize(scanFolder);
if (!AZ::StringFunc::StartsWith(sourceFileFullPath, scanFolder))
{
continue;
}
const size_t scanFolderSize = scanFolder.size();
const size_t sourcePathSize = sourceFileFullPath.size();
scanFolderFullPath = scanFolder;
filePathFromScanFolder = sourceFileFullPath.substr(scanFolderSize + 1, sourcePathSize - scanFolderSize - 1);
return true;
}
return false;
}
//! Validates if a given .shadervariantlist file is located at the correct path for a given .shader full path.
//! There are two valid paths:
//! 1- Lower Precedence: The same folder where the .shader file is located.
//! 2- Higher Precedence: <project-path>/ShaderVariants/<Same Scan Folder Subpath as the .shader file>.
//! The "Higher Precedence" path gives the option to game projects to override what variants to generate. If this
//! file exists then the "Lower Precedence" path is disregarded.
//! A .shader full path is located under an AP scan folder.
//! Example: "<atom-gem-path>/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.shader"
//! - In this example the Scan Folder is "<atom-gem-path>/Gems/Atom/Feature/Common/Assets", while the subfolder is "Materials/Types".
//! The "Higher Precedence" expected valid location for the .shadervariantlist would be:
//! - <atom-gem-path>/<GameProject>/ShaderVariants/Materials/Types/StandardPBR_ForwardPass.shadervariantlist.
//! The "Lower Precedence" valid location would be:
//! - <atom-gem-path>/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.shadervariantlist.
//! @shouldExitEarlyFromProcessJob [out] Set to true if ProcessJob should do no work but return successfully.
//! Set to false if ProcessJob should do work and create assets.
//! When @shaderVariantListFileFullPath is provided by a Gem/Feature instead of the Game Project
//! We check if the game project already defined the shader variant list, and if it did it means
//! ProcessJob should do no work, but return successfully nonetheless.
static bool ValidateShaderVariantListLocation(const AZStd::string& shaderVariantListFileFullPath,
const AZStd::string& shaderFileFullPath, bool& shouldExitEarlyFromProcessJob)
{
AZStd::string scanFolderFullPath;
AZStd::string shaderProductFileRelativePath;
if (!SplitSourceAssetPathIntoScanFolderFullPathAndRelativeFilePath(shaderFileFullPath, scanFolderFullPath, shaderProductFileRelativePath))
{
AZ_Error(ShaderVariantAssetBuilderName, false, "Couldn't get the scan folder for shader [%s]", shaderFileFullPath.c_str());
return false;
}
AZ_TracePrintf(ShaderVariantAssetBuilderName, "For shader [%s], Scan folder full path [%s], relative file path [%s]", shaderFileFullPath.c_str(), scanFolderFullPath.c_str(), shaderProductFileRelativePath.c_str());
AZStd::string shaderVariantListFileRelativePath = shaderProductFileRelativePath;
AzFramework::StringFunc::Path::ReplaceExtension(shaderVariantListFileRelativePath, RPI::ShaderVariantListSourceData::Extension);
AZ::IO::FixedMaxPath gameProjectPath = AZ::Utils::GetProjectPath();
auto expectedHigherPrecedenceFileFullPath = (gameProjectPath
/ RPI::ShaderVariantTreeAsset::CommonSubFolder / shaderProductFileRelativePath).LexicallyNormal();
expectedHigherPrecedenceFileFullPath.ReplaceExtension(AZ::RPI::ShaderVariantListSourceData::Extension);
auto normalizedShaderVariantListFileFullPath = AZ::IO::FixedMaxPath(shaderVariantListFileFullPath).LexicallyNormal();
if (expectedHigherPrecedenceFileFullPath == normalizedShaderVariantListFileFullPath)
{
// Whenever the Game Project declares a *.shadervariantlist file we always do work.
shouldExitEarlyFromProcessJob = false;
return true;
}
AZ::Data::AssetInfo assetInfo;
AZStd::string watchFolder;
bool foundHigherPrecedenceAsset = false;
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(foundHigherPrecedenceAsset
, &AzToolsFramework::AssetSystem::AssetSystemRequest::GetSourceInfoBySourcePath
, expectedHigherPrecedenceFileFullPath.c_str(), assetInfo, watchFolder);
if (foundHigherPrecedenceAsset)
{
AZ_TracePrintf(ShaderVariantAssetBuilderName, "The shadervariantlist [%s] has been overriden by the game project with [%s]",
normalizedShaderVariantListFileFullPath.c_str(), expectedHigherPrecedenceFileFullPath.c_str());
shouldExitEarlyFromProcessJob = true;
return true;
}
// Check the "Lower Precedence" case, .shader path == .shadervariantlist path.
AZ::IO::Path normalizedShaderFileFullPath = AZ::IO::Path(shaderFileFullPath).LexicallyNormal();
auto normalizedShaderFileFullPathWithoutExtension = normalizedShaderFileFullPath;
normalizedShaderFileFullPathWithoutExtension.ReplaceExtension("");
auto normalizedShaderVariantListFileFullPathWithoutExtension = normalizedShaderVariantListFileFullPath;
normalizedShaderVariantListFileFullPathWithoutExtension.ReplaceExtension("");
if (normalizedShaderFileFullPathWithoutExtension != normalizedShaderVariantListFileFullPathWithoutExtension)
{
AZ_Error(ShaderVariantAssetBuilderName, false, "For shader file at path [%s], the shader variant list [%s] is expected to be located at [%s.%s] or [%s]"
, normalizedShaderFileFullPath.c_str(), normalizedShaderVariantListFileFullPath.c_str(),
normalizedShaderFileFullPathWithoutExtension.c_str(), RPI::ShaderVariantListSourceData::Extension,
expectedHigherPrecedenceFileFullPath.c_str());
return false;
}
shouldExitEarlyFromProcessJob = false;
return true;
}
// We treat some issues as warnings and return "Success" from CreateJobs allows us to report the dependency.
// If/when a valid dependency file appears, that will trigger the ShaderVariantAssetBuilder to run again.
// Since CreateJobs will pass, we forward this message to ProcessJob which will report it as an error.
struct LoadResult
{
enum class Code
{
Error,
DeferredError,
Success
};
Code m_code;
AZStd::string m_deferredMessage; // Only used when m_code == DeferredError
};
static LoadResult LoadShaderVariantList(const AZStd::string& variantListFullPath, RPI::ShaderVariantListSourceData& shaderVariantList, AZStd::string& shaderSourceFileFullPath,
bool& shouldExitEarlyFromProcessJob)
{
// Need to get the name of the shader file from the template so that we can preprocess the shader data and setup
// source file dependencies.
if (!RPI::JsonUtils::LoadObjectFromFile(variantListFullPath, shaderVariantList))
{
AZ_Error(ShaderVariantAssetBuilderName, false, "Failed to parse Shader Variant List Descriptor JSON from [%s]", variantListFullPath.c_str());
return LoadResult{LoadResult::Code::Error};
}
const AZStd::string resolvedShaderPath = AZ::RPI::AssetUtils::ResolvePathReference(variantListFullPath, shaderVariantList.m_shaderFilePath);
if (!AZ::IO::LocalFileIO::GetInstance()->Exists(resolvedShaderPath.c_str()))
{
return LoadResult{LoadResult::Code::DeferredError, AZStd::string::format("The shader path [%s] was not found.", resolvedShaderPath.c_str())};
}
shaderSourceFileFullPath = resolvedShaderPath;
if (!ValidateShaderVariantListLocation(variantListFullPath, shaderSourceFileFullPath, shouldExitEarlyFromProcessJob))
{
return LoadResult{LoadResult::Code::Error};
}
if (shouldExitEarlyFromProcessJob)
{
return LoadResult{LoadResult::Code::Success};
}
auto resultOutcome = RPI::ShaderVariantTreeAssetCreator::ValidateStableIdsAreUnique(shaderVariantList.m_shaderVariants);
if (!resultOutcome.IsSuccess())
{
AZ_Error(ShaderVariantAssetBuilderName, false, "Variant info validation error: %s", resultOutcome.GetError().c_str());
return LoadResult{LoadResult::Code::Error};
}
if (!IO::FileIOBase::GetInstance()->Exists(shaderSourceFileFullPath.c_str()))
{
return LoadResult{LoadResult::Code::DeferredError, AZStd::string::format("ShaderSourceData file does not exist: %s.", shaderSourceFileFullPath.c_str())};
}
return LoadResult{LoadResult::Code::Success};
} // LoadShaderVariantListAndAzslSource
void ShaderVariantAssetBuilder::CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response) const
{
AZStd::string variantListFullPath;
AzFramework::StringFunc::Path::ConstructFull(request.m_watchFolder.data(), request.m_sourceFile.data(), variantListFullPath, true);
AZ_TracePrintf(ShaderVariantAssetBuilderName, "CreateJobs for Shader Variant List \"%s\"\n", variantListFullPath.data());
RPI::ShaderVariantListSourceData shaderVariantList;
AZStd::string shaderSourceFileFullPath;
bool shouldExitEarlyFromProcessJob = false;
const LoadResult loadResult = LoadShaderVariantList(variantListFullPath, shaderVariantList, shaderSourceFileFullPath, shouldExitEarlyFromProcessJob);
if (loadResult.m_code == LoadResult::Code::Error)
{
response.m_result = AssetBuilderSDK::CreateJobsResultCode::Failed;
return;
}
AZStd::string foundShaderFile;
LocateReferencedSourceFile(variantListFullPath, shaderVariantList.m_shaderFilePath, response.m_sourceFileDependencyList, foundShaderFile);
if (loadResult.m_code == LoadResult::Code::DeferredError || shouldExitEarlyFromProcessJob)
{
for (const AssetBuilderSDK::PlatformInfo& info : request.m_enabledPlatforms)
{
// Let's create fake jobs that will fail ProcessJob, but are useful to establish dependency on the shader file.
AssetBuilderSDK::JobDescriptor jobDescriptor;
jobDescriptor.m_priority = -5000;
jobDescriptor.m_critical = false;
jobDescriptor.m_jobKey = ShaderVariantAssetBuilderJobKey;
jobDescriptor.SetPlatformIdentifier(info.m_identifier.data());
if (!foundShaderFile.empty())
{
AssetBuilderSDK::JobDependency jobDependency;
jobDependency.m_jobKey = ShaderAssetBuilder::ShaderAssetBuilderJobKey;
jobDependency.m_platformIdentifier = info.m_identifier;
jobDependency.m_type = AssetBuilderSDK::JobDependencyType::Order;
jobDependency.m_sourceFile.m_sourceFileDependencyPath = foundShaderFile;
jobDescriptor.m_jobDependencyList.push_back(jobDependency);
}
if (loadResult.m_code == LoadResult::Code::DeferredError)
{
jobDescriptor.m_jobParameters.emplace(ShaderVariantLoadErrorParam, loadResult.m_deferredMessage);
}
if (shouldExitEarlyFromProcessJob)
{
// The value doesn't matter, what matters is the presence of the key which will
// signal that no assets should be produced on behalf of this shadervariantlist because
// the game project overrode it.
jobDescriptor.m_jobParameters.emplace(ShouldExitEarlyFromProcessJobParam, variantListFullPath);
}
response.m_createJobOutputs.push_back(jobDescriptor);
}
response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
return;
}
for (const AssetBuilderSDK::PlatformInfo& info : request.m_enabledPlatforms)
{
AZ_TraceContext("For platform", info.m_identifier.data());
// First job is for the ShaderVariantTreeAsset.
{
AssetBuilderSDK::JobDescriptor jobDescriptor;
// The ShaderVariantTreeAsset is high priority, but must be generated after the ShaderAsset
jobDescriptor.m_priority = 1;
jobDescriptor.m_critical = false;
jobDescriptor.m_jobKey = GetShaderVariantTreeAssetJobKey();
jobDescriptor.SetPlatformIdentifier(info.m_identifier.data());
if (!foundShaderFile.empty())
{
AssetBuilderSDK::JobDependency jobDependency;
jobDependency.m_jobKey = ShaderAssetBuilder::ShaderAssetBuilderJobKey;
jobDependency.m_platformIdentifier = info.m_identifier;
jobDependency.m_type = AssetBuilderSDK::JobDependencyType::Order;
jobDependency.m_sourceFile.m_sourceFileDependencyPath = foundShaderFile;
jobDescriptor.m_jobDependencyList.push_back(jobDependency);
}
jobDescriptor.m_jobParameters.emplace(ShaderSourceFilePathJobParam, shaderSourceFileFullPath);
response.m_createJobOutputs.push_back(jobDescriptor);
}
// One job for each variant. Each job will produce one ".azshadervariant" per RHI per supervariant.
for (const AZ::RPI::ShaderVariantListSourceData::VariantInfo& variantInfo : shaderVariantList.m_shaderVariants)
{
AZStd::string variantInfoAsJsonString;
[[maybe_unused]] const bool convertSuccess = AZ::RPI::JsonUtils::SaveObjectToJsonString(variantInfo, variantInfoAsJsonString);
AZ_Assert(convertSuccess, "Failed to convert VariantInfo to json string");
AssetBuilderSDK::JobDescriptor jobDescriptor;
// There can be tens/hundreds of thousands of shader variants. By default each shader will get
// a root variant that can be used at runtime. In order to prevent the AssetProcessor from
// being overtaken by shader variant compilation We mark all non-root shader variant generation
// as non critical and very low priority.
jobDescriptor.m_priority = -5000;
jobDescriptor.m_critical = false;
jobDescriptor.m_jobKey = GetShaderVariantAssetJobKey(RPI::ShaderVariantStableId{variantInfo.m_stableId});
jobDescriptor.SetPlatformIdentifier(info.m_identifier.data());
// The ShaderVariantAssets are job dependent on the ShaderVariantTreeAsset.
AssetBuilderSDK::SourceFileDependency fileDependency;
fileDependency.m_sourceFileDependencyPath = variantListFullPath;
AssetBuilderSDK::JobDependency variantTreeJobDependency;
variantTreeJobDependency.m_jobKey = GetShaderVariantTreeAssetJobKey();
variantTreeJobDependency.m_platformIdentifier = info.m_identifier;
variantTreeJobDependency.m_sourceFile = fileDependency;
variantTreeJobDependency.m_type = AssetBuilderSDK::JobDependencyType::Order;
jobDescriptor.m_jobDependencyList.emplace_back(variantTreeJobDependency);
jobDescriptor.m_jobParameters.emplace(ShaderVariantJobVariantParam, variantInfoAsJsonString);
jobDescriptor.m_jobParameters.emplace(ShaderSourceFilePathJobParam, shaderSourceFileFullPath);
response.m_createJobOutputs.push_back(jobDescriptor);
}
}
response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
} // CreateJobs
void ShaderVariantAssetBuilder::ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response) const
{
const auto& jobParameters = request.m_jobDescription.m_jobParameters;
if (jobParameters.contains(ShaderVariantLoadErrorParam))
{
AZ_Error(ShaderVariantAssetBuilderName, false, "Error during CreateJobs: %s", jobParameters.at(ShaderVariantLoadErrorParam).c_str());
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
if (jobParameters.contains(ShouldExitEarlyFromProcessJobParam))
{
AZ_TracePrintf(ShaderVariantAssetBuilderName, "Doing nothing on behalf of [%s] because it's been overridden by game project.", jobParameters.at(ShouldExitEarlyFromProcessJobParam).c_str());
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
return;
}
AssetBuilderSDK::JobCancelListener jobCancelListener(request.m_jobId);
if (jobCancelListener.IsCancelled())
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
return;
}
if (request.m_jobDescription.m_jobKey == GetShaderVariantTreeAssetJobKey())
{
ProcessShaderVariantTreeJob(request, response);
}
else
{
ProcessShaderVariantJob(request, response);
}
}
static RPI::Ptr<RPI::ShaderOptionGroupLayout> LoadShaderOptionsGroupLayoutFromShaderAssetBuilder(
const RHI::ShaderPlatformInterface* shaderPlatformInterface,
const AssetBuilderSDK::PlatformInfo& platformInfo,
const AzslCompiler& azslCompiler,
const AZStd::string& shaderSourceFileFullPath,
const RPI::SupervariantIndex supervariantIndex)
{
auto optionsGroupPathOutcome = ShaderBuilderUtility::ObtainBuildArtifactPathFromShaderAssetBuilder(
shaderPlatformInterface->GetAPIUniqueIndex(), platformInfo.m_identifier, shaderSourceFileFullPath, supervariantIndex.GetIndex(),
AZ::RPI::ShaderAssetSubId::OptionsJson);
if (!optionsGroupPathOutcome.IsSuccess())
{
AZ_Error(ShaderVariantAssetBuilderName, false, "%s", optionsGroupPathOutcome.GetError().c_str());
return nullptr;
}
auto optionsGroupJsonPath = optionsGroupPathOutcome.TakeValue();
RPI::Ptr<RPI::ShaderOptionGroupLayout> shaderOptionGroupLayout = RPI::ShaderOptionGroupLayout::Create();
// The shader options define what options are available, what are the allowed values/range
// for each option and what is its default value.
auto jsonOutcome = JsonSerializationUtils::ReadJsonFile(optionsGroupJsonPath, AZ::RPI::JsonUtils::DefaultMaxFileSize);
if (!jsonOutcome.IsSuccess())
{
AZ_Error(ShaderVariantAssetBuilderName, false, "%s", jsonOutcome.GetError().c_str());
return nullptr;
}
if (!azslCompiler.ParseOptionsPopulateOptionGroupLayout(jsonOutcome.GetValue(), shaderOptionGroupLayout))
{
AZ_Error(ShaderVariantAssetBuilderName, false, "Failed to find a valid list of shader options!");
return nullptr;
}
return shaderOptionGroupLayout;
}
static void LoadShaderFunctionsFromShaderAssetBuilder(
const RHI::ShaderPlatformInterface* shaderPlatformInterface, const AssetBuilderSDK::PlatformInfo& platformInfo,
const AzslCompiler& azslCompiler, const AZStd::string& shaderSourceFileFullPath,
const RPI::SupervariantIndex supervariantIndex,
AzslFunctions& functions)
{
auto functionsJsonPathOutcome = ShaderBuilderUtility::ObtainBuildArtifactPathFromShaderAssetBuilder(
shaderPlatformInterface->GetAPIUniqueIndex(), platformInfo.m_identifier, shaderSourceFileFullPath, supervariantIndex.GetIndex(),
AZ::RPI::ShaderAssetSubId::IaJson);
if (!functionsJsonPathOutcome.IsSuccess())
{
AZ_Error(ShaderVariantAssetBuilderName, false, "%s", functionsJsonPathOutcome.GetError().c_str());
return;
}
auto functionsJsonPath = functionsJsonPathOutcome.TakeValue();
auto jsonOutcome = JsonSerializationUtils::ReadJsonFile(functionsJsonPath, AZ::RPI::JsonUtils::DefaultMaxFileSize);
if (!jsonOutcome.IsSuccess())
{
AZ_Error(ShaderVariantAssetBuilderName, false, "%s", jsonOutcome.GetError().c_str());
return;
}
if (!azslCompiler.ParseIaPopulateFunctionData(jsonOutcome.GetValue(), functions))
{
functions.clear();
AZ_Error(ShaderVariantAssetBuilderName, false, "Failed to find shader functions.");
return;
}
}
static bool LoadSrgLayoutListFromShaderAssetBuilder(
const RHI::ShaderPlatformInterface* shaderPlatformInterface,
const AssetBuilderSDK::PlatformInfo& platformInfo,
const AzslCompiler& azslCompiler, const AZStd::string& shaderSourceFileFullPath,
const RPI::SupervariantIndex supervariantIndex,
const bool platformUsesRegisterSpaces,
RPI::ShaderResourceGroupLayoutList& srgLayoutList,
RootConstantData& rootConstantData)
{
auto srgJsonPathOutcome = ShaderBuilderUtility::ObtainBuildArtifactPathFromShaderAssetBuilder(
shaderPlatformInterface->GetAPIUniqueIndex(), platformInfo.m_identifier, shaderSourceFileFullPath, supervariantIndex.GetIndex(), AZ::RPI::ShaderAssetSubId::SrgJson);
if (!srgJsonPathOutcome.IsSuccess())
{
AZ_Error(ShaderVariantAssetBuilderName, false, "%s", srgJsonPathOutcome.GetError().c_str());
return false;
}
auto srgJsonPath = srgJsonPathOutcome.TakeValue();
auto jsonOutcome = JsonSerializationUtils::ReadJsonFile(srgJsonPath, AZ::RPI::JsonUtils::DefaultMaxFileSize);
if (!jsonOutcome.IsSuccess())
{
AZ_Error(ShaderVariantAssetBuilderName, false, "%s", jsonOutcome.GetError().c_str());
return false;
}
SrgDataContainer srgData;
if (!azslCompiler.ParseSrgPopulateSrgData(jsonOutcome.GetValue(), srgData))
{
AZ_Error(ShaderVariantAssetBuilderName, false, "Failed to parse srg data");
return false;
}
// Add all Shader Resource Group Assets that were defined in the shader code to the shader asset
if (!SrgLayoutUtility::LoadShaderResourceGroupLayouts(ShaderVariantAssetBuilderName, srgData, platformUsesRegisterSpaces, srgLayoutList))
{
AZ_Error(ShaderVariantAssetBuilderName, false, "Failed to load ShaderResourceGroupLayouts");
return false;
}
for (auto srgLayout : srgLayoutList)
{
if (!srgLayout->Finalize())
{
AZ_Error(ShaderVariantAssetBuilderName, false,
"Failed to finalize SrgLayout %s", srgLayout->GetName().GetCStr());
return false;
}
}
// Access the root constants reflection
if (!azslCompiler.ParseSrgPopulateRootConstantData(
jsonOutcome.GetValue(),
rootConstantData)) // consuming data from --srg ("RootConstantBuffer" subjson section)
{
AZ_Error(ShaderVariantAssetBuilderName, false, "Failed to obtain root constant data reflection");
return false;
}
return true;
}
static bool LoadBindingDependenciesFromShaderAssetBuilder(
const RHI::ShaderPlatformInterface* shaderPlatformInterface,
const AssetBuilderSDK::PlatformInfo& platformInfo,
const AzslCompiler& azslCompiler, const AZStd::string& shaderSourceFileFullPath,
const RPI::SupervariantIndex supervariantIndex,
BindingDependencies& bindingDependencies)
{
auto bindingsJsonPathOutcome = ShaderBuilderUtility::ObtainBuildArtifactPathFromShaderAssetBuilder(
shaderPlatformInterface->GetAPIUniqueIndex(), platformInfo.m_identifier, shaderSourceFileFullPath, supervariantIndex.GetIndex(), AZ::RPI::ShaderAssetSubId::BindingdepJson);
if (!bindingsJsonPathOutcome.IsSuccess())
{
AZ_Error(ShaderVariantAssetBuilderName, false, "%s", bindingsJsonPathOutcome.GetError().c_str());
return false;
}
auto bindingsJsonPath = bindingsJsonPathOutcome.TakeValue();
auto jsonOutcome = JsonSerializationUtils::ReadJsonFile(bindingsJsonPath, AZ::RPI::JsonUtils::DefaultMaxFileSize);
if (!jsonOutcome.IsSuccess())
{
AZ_Error(ShaderVariantAssetBuilderName, false, "%s", jsonOutcome.GetError().c_str());
return false;
}
if (!azslCompiler.ParseBindingdepPopulateBindingDependencies(jsonOutcome.GetValue(), bindingDependencies))
{
AZ_Error(ShaderVariantAssetBuilderName, false, "Failed to parse binding dependencies data");
return false;
}
return true;
}
// Returns the content of the hlsl file for the given supervariant as produced by ShaderAsssetBuilder.
// In addition to the content it also returns the full path of the hlsl file in @hlslSourcePath.
static AZStd::string LoadHlslFileFromShaderAssetBuilder(
const RHI::ShaderPlatformInterface* shaderPlatformInterface, const AssetBuilderSDK::PlatformInfo& platformInfo,
const AZStd::string& shaderSourceFileFullPath, const RPI::SupervariantIndex supervariantIndex, AZStd::string& hlslSourcePath)
{
auto hlslSourcePathOutcome = ShaderBuilderUtility::ObtainBuildArtifactPathFromShaderAssetBuilder(
shaderPlatformInterface->GetAPIUniqueIndex(), platformInfo.m_identifier, shaderSourceFileFullPath, supervariantIndex.GetIndex(),
AZ::RPI::ShaderAssetSubId::GeneratedHlslSource);
if (!hlslSourcePathOutcome.IsSuccess())
{
AZ_Error(ShaderVariantAssetBuilderName, false, "%s", hlslSourcePathOutcome.GetError().c_str());
return "";
}
hlslSourcePath = hlslSourcePathOutcome.TakeValue();
Outcome<AZStd::string, AZStd::string> hlslSourceOutcome = Utils::ReadFile(hlslSourcePath, AZ::RPI::JsonUtils::DefaultMaxFileSize);
if (!hlslSourceOutcome.IsSuccess())
{
AZ_Error(
ShaderVariantAssetBuilderName, false, "Failed to obtain shader source from %s. [%s]", hlslSourcePath.c_str(),
hlslSourceOutcome.TakeError().c_str());
return "";
}
return hlslSourceOutcome.TakeValue();
}
void ShaderVariantAssetBuilder::ProcessShaderVariantTreeJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response) const
{
AZStd::string variantListFullPath;
AzFramework::StringFunc::Path::ConstructFull(request.m_watchFolder.data(), request.m_sourceFile.data(), variantListFullPath, true);
RPI::ShaderVariantListSourceData shaderVariantListDescriptor;
if (!RPI::JsonUtils::LoadObjectFromFile(variantListFullPath, shaderVariantListDescriptor))
{
AZ_Assert(false, "Failed to parse Shader Variant List Descriptor JSON [%s]", variantListFullPath.c_str());
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
const AZStd::string& shaderSourceFileFullPath = request.m_jobDescription.m_jobParameters.at(ShaderSourceFilePathJobParam);
//For debugging purposes will create a dummy azshadervarianttree file.
AZStd::string shaderName;
AzFramework::StringFunc::Path::GetFileName(shaderSourceFileFullPath.c_str(), shaderName);
// No error checking because the same calls were already executed during CreateJobs()
auto descriptorParseOutcome = ShaderBuilderUtility::LoadShaderDataJson(shaderSourceFileFullPath);
RPI::ShaderSourceData shaderSourceDescriptor = descriptorParseOutcome.TakeValue();
RPI::Ptr<RPI::ShaderOptionGroupLayout> shaderOptionGroupLayout;
// Request the list of valid shader platform interfaces for the target platform.
AZStd::vector<RHI::ShaderPlatformInterface*> platformInterfaces =
ShaderBuilderUtility::DiscoverEnabledShaderPlatformInterfaces(request.m_platformInfo, shaderSourceDescriptor);
if (platformInterfaces.empty())
{
// No work to do. Exit gracefully.
AZ_TracePrintf(
ShaderVariantAssetBuilderName,
"No azshadervarianttree is produced on behalf of %s because all valid RHI backends were disabled for this shader.\n",
shaderSourceFileFullPath.c_str());
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
return;
}
// set the input file for eventual error messages, but the compiler won't be called on it.
AZStd::string azslFullPath;
ShaderBuilderUtility::GetAbsolutePathToAzslFile(shaderSourceFileFullPath, shaderSourceDescriptor.m_source, azslFullPath);
AzslCompiler azslc(azslFullPath);
AZStd::string previousLoopApiName;
for (RHI::ShaderPlatformInterface* shaderPlatformInterface : platformInterfaces)
{
auto thisLoopApiName = shaderPlatformInterface->GetAPIName().GetStringView();
RPI::Ptr<RPI::ShaderOptionGroupLayout> loopLocal_ShaderOptionGroupLayout =
LoadShaderOptionsGroupLayoutFromShaderAssetBuilder(
shaderPlatformInterface, request.m_platformInfo, azslc, shaderSourceFileFullPath, RPI::DefaultSupervariantIndex);
if (!loopLocal_ShaderOptionGroupLayout)
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
if (shaderOptionGroupLayout && shaderOptionGroupLayout->GetHash() != loopLocal_ShaderOptionGroupLayout->GetHash())
{
AZ_Error(ShaderVariantAssetBuilderName, false, "There was a discrepancy in shader options between %s and %s", previousLoopApiName.c_str(), thisLoopApiName.data());
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
shaderOptionGroupLayout = loopLocal_ShaderOptionGroupLayout;
previousLoopApiName = thisLoopApiName;
}
RPI::ShaderVariantTreeAssetCreator shaderVariantTreeAssetCreator;
shaderVariantTreeAssetCreator.Begin(Uuid::CreateRandom());
shaderVariantTreeAssetCreator.SetShaderOptionGroupLayout(*shaderOptionGroupLayout);
shaderVariantTreeAssetCreator.SetVariantInfos(shaderVariantListDescriptor.m_shaderVariants);
Data::Asset<RPI::ShaderVariantTreeAsset> shaderVariantTreeAsset;
if (!shaderVariantTreeAssetCreator.End(shaderVariantTreeAsset))
{
AZ_Error(ShaderVariantAssetBuilderName, false, "Failed to build Shader Variant Tree Asset");
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
AZStd::string filename = AZStd::string::format("%s.%s", shaderName.c_str(), RPI::ShaderVariantTreeAsset::Extension);
AZStd::string assetPath;
AzFramework::StringFunc::Path::ConstructFull(request.m_tempDirPath.c_str(), filename.c_str(), assetPath, true);
if (!AZ::Utils::SaveObjectToFile(assetPath, AZ::DataStream::ST_BINARY, shaderVariantTreeAsset.Get()))
{
AZ_Error(ShaderVariantAssetBuilderName, false, "Failed to save Shader Variant Tree Asset to \"%s\"", assetPath.c_str());
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
AssetBuilderSDK::JobProduct assetProduct;
assetProduct.m_productSubID = RPI::ShaderVariantTreeAsset::ProductSubID;
assetProduct.m_productFileName = assetPath;
assetProduct.m_productAssetType = azrtti_typeid<RPI::ShaderVariantTreeAsset>();
assetProduct.m_dependenciesHandled = true; // This builder has no dependencies to output
response.m_outputProducts.push_back(assetProduct);
AZ_TracePrintf(ShaderVariantAssetBuilderName, "Shader Variant Tree Asset [%s] compiled successfully.\n", assetPath.c_str());
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
}
void ShaderVariantAssetBuilder::ProcessShaderVariantJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response) const
{
AssetBuilderSDK::JobCancelListener jobCancelListener(request.m_jobId);
AZStd::string fullPath;
AzFramework::StringFunc::Path::ConstructFull(request.m_watchFolder.data(), request.m_sourceFile.data(), fullPath, true);
const auto& jobParameters = request.m_jobDescription.m_jobParameters;
const AZStd::string& shaderSourceFileFullPath = jobParameters.at(ShaderSourceFilePathJobParam);
AZStd::string shaderFileName;
AzFramework::StringFunc::Path::GetFileName(shaderSourceFileFullPath.c_str(), shaderFileName);
const AZStd::string& variantJsonString = jobParameters.at(ShaderVariantJobVariantParam);
RPI::ShaderVariantListSourceData::VariantInfo variantInfo;
[[maybe_unused]] const bool fromJsonStringSuccess = AZ::RPI::JsonUtils::LoadObjectFromJsonString(variantJsonString, variantInfo);
AZ_Assert(fromJsonStringSuccess, "Failed to convert json string to VariantInfo");
RPI::ShaderSourceData shaderSourceDescriptor;
AZStd::shared_ptr<ShaderFiles> sources = ShaderBuilderUtility::PrepareSourceInput(ShaderVariantAssetBuilderName, shaderSourceFileFullPath, shaderSourceDescriptor);
// set the input file for eventual error messages, but the compiler won't be called on it.
AzslCompiler azslc(sources->m_azslSourceFullPath);
// Request the list of valid shader platform interfaces for the target platform.
AZStd::vector<RHI::ShaderPlatformInterface*> platformInterfaces =
ShaderBuilderUtility::DiscoverEnabledShaderPlatformInterfaces(request.m_platformInfo, shaderSourceDescriptor);
if (platformInterfaces.empty())
{
// No work to do. Exit gracefully.
AZ_TracePrintf(ShaderVariantAssetBuilderName,
"No azshader is produced on behalf of %s because all valid RHI backends were disabled for this shader.\n",
shaderSourceFileFullPath.c_str());
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
return;
}
const AZ::u64 shaderVariantAssetBuildTimestamp = AZStd::GetTimeUTCMilliSecond();
auto supervariantList = ShaderBuilderUtility::GetSupervariantListFromShaderSourceData(shaderSourceDescriptor);
GlobalBuildOptions buildOptions = ReadBuildOptions(ShaderVariantAssetBuilderName);
// At this moment We have global build options that should be merged with the build options that are common
// to all the supervariants of this shader.
buildOptions.m_compilerArguments.Merge(shaderSourceDescriptor.m_compiler);
//! The ShaderOptionGroupLayout is common across all RHIs & Supervariants
RPI::Ptr<RPI::ShaderOptionGroupLayout> shaderOptionGroupLayout = nullptr;
// Generate shaders for each of those ShaderPlatformInterfaces.
for (RHI::ShaderPlatformInterface* shaderPlatformInterface : platformInterfaces)
{
AZ_TraceContext("ShaderPlatformInterface", shaderPlatformInterface->GetAPIName().GetCStr());
// Loop through all the Supervariants.
uint32_t supervariantIndexCounter = 0;
for (const auto& supervariantInfo : supervariantList)
{
RPI::SupervariantIndex supervariantIndex(supervariantIndexCounter);
// Check if we were canceled before we do any heavy processing of
// the shader variant data.
if (jobCancelListener.IsCancelled())
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
return;
}
AZStd::string shaderStemNamePrefix = shaderFileName;
if (supervariantIndex.GetIndex() > 0)
{
shaderStemNamePrefix += supervariantInfo.m_name.GetStringView();
}
// We need these additional pieces of information To build a shader variant asset:
// 1- ShaderOptionsGroupLayout (Need to load it once, because it's the same acrosss all supervariants + RHIs)
// 2- entryFunctions
// 3- hlsl code.
// 1- ShaderOptionsGroupLayout
if (!shaderOptionGroupLayout)
{
shaderOptionGroupLayout =
LoadShaderOptionsGroupLayoutFromShaderAssetBuilder(
shaderPlatformInterface, request.m_platformInfo, azslc, shaderSourceFileFullPath, supervariantIndex);
if (!shaderOptionGroupLayout)
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
}
// 2- entryFunctions.
AzslFunctions azslFunctions;
LoadShaderFunctionsFromShaderAssetBuilder(
shaderPlatformInterface, request.m_platformInfo, azslc, shaderSourceFileFullPath, supervariantIndex, azslFunctions);
if (azslFunctions.empty())
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
MapOfStringToStageType shaderEntryPoints;
if (shaderSourceDescriptor.m_programSettings.m_entryPoints.empty())
{
AZ_Error(ShaderVariantAssetBuilderName, false, "ProgramSettings must specify entry points.");
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
for (const auto& entryPoint : shaderSourceDescriptor.m_programSettings.m_entryPoints)
{
shaderEntryPoints[entryPoint.m_name] = entryPoint.m_type;
}
// 3- hlslCode
AZStd::string hlslSourcePath;
AZStd::string hlslCode = LoadHlslFileFromShaderAssetBuilder(
shaderPlatformInterface, request.m_platformInfo, shaderSourceFileFullPath, supervariantIndex, hlslSourcePath);
if (hlslCode.empty() || hlslSourcePath.empty())
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
//! It is important to keep this refcounted pointer outside of the if block to prevent it from being destroyed.
RHI::Ptr<RHI::PipelineLayoutDescriptor> pipelineLayoutDescriptor;
if (shaderPlatformInterface->VariantCompilationRequiresSrgLayoutData())
{
AZStd::string azslcCompilerParameters =
shaderPlatformInterface->GetAzslCompilerParameters(buildOptions.m_compilerArguments);
const bool platformUsesRegisterSpaces =
(AzFramework::StringFunc::Find(azslcCompilerParameters, "--use-spaces") != AZStd::string::npos);
RPI::ShaderResourceGroupLayoutList srgLayoutList;
RootConstantData rootConstantData;
if (!LoadSrgLayoutListFromShaderAssetBuilder(
shaderPlatformInterface, request.m_platformInfo, azslc, shaderSourceFileFullPath, supervariantIndex,
platformUsesRegisterSpaces,
srgLayoutList,
rootConstantData))
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
BindingDependencies bindingDependencies;
if (!LoadBindingDependenciesFromShaderAssetBuilder(
shaderPlatformInterface, request.m_platformInfo, azslc, shaderSourceFileFullPath, supervariantIndex,
bindingDependencies))
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
pipelineLayoutDescriptor =
ShaderBuilderUtility::BuildPipelineLayoutDescriptorForApi(
ShaderVariantAssetBuilderName, srgLayoutList, shaderEntryPoints, buildOptions.m_compilerArguments, rootConstantData,
shaderPlatformInterface, bindingDependencies);
if (!pipelineLayoutDescriptor)
{
AZ_Error(
ShaderVariantAssetBuilderName, false, "Failed to build pipeline layout descriptor for api=[%s]",
shaderPlatformInterface->GetAPIName().GetCStr());
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
}
// Setup the shader variant creation context:
ShaderVariantCreationContext shaderVariantCreationContext =
{
*shaderPlatformInterface, request.m_platformInfo, buildOptions.m_compilerArguments, request.m_tempDirPath,
shaderVariantAssetBuildTimestamp,
shaderSourceDescriptor,
*shaderOptionGroupLayout.get(),
shaderEntryPoints,
Uuid::CreateRandom(),
shaderStemNamePrefix,
hlslSourcePath, hlslCode
};
AZStd::optional<RHI::ShaderPlatformInterface::ByProducts> outputByproducts;
auto shaderVariantAssetOutcome = CreateShaderVariantAsset(variantInfo, shaderVariantCreationContext, outputByproducts);
if (!shaderVariantAssetOutcome.IsSuccess())
{
AZ_Error(ShaderVariantAssetBuilderName, false, "%s\n", shaderVariantAssetOutcome.GetError().c_str());
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
Data::Asset<RPI::ShaderVariantAsset> shaderVariantAsset = shaderVariantAssetOutcome.TakeValue();
// Time to save the asset in the tmp folder so it ends up in the Cache folder.
const uint32_t productSubID = RPI::ShaderVariantAsset::MakeAssetProductSubId(
shaderPlatformInterface->GetAPIUniqueIndex(), supervariantIndex.GetIndex(),
shaderVariantAsset->GetStableId());
AssetBuilderSDK::JobProduct assetProduct;
if (!SerializeOutShaderVariantAsset(shaderVariantAsset, shaderStemNamePrefix,
request.m_tempDirPath, *shaderPlatformInterface, productSubID,
assetProduct))
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
response.m_outputProducts.push_back(assetProduct);
if (outputByproducts)
{
// add byproducts as job output products:
uint32_t subProductType = RPI::ShaderVariantAsset::ShaderVariantAssetSubProductType;
for (const AZStd::string& byproduct : outputByproducts.value().m_intermediatePaths)
{
AssetBuilderSDK::JobProduct jobProduct;
jobProduct.m_productFileName = byproduct;
jobProduct.m_productAssetType = Uuid::CreateName("DebugInfoByProduct-PdbOrDxilTxt");
jobProduct.m_productSubID = RPI::ShaderVariantAsset::MakeAssetProductSubId(
shaderPlatformInterface->GetAPIUniqueIndex(), supervariantIndex.GetIndex(), shaderVariantAsset->GetStableId(),
subProductType++);
response.m_outputProducts.push_back(AZStd::move(jobProduct));
}
}
supervariantIndexCounter++;
} // End of supervariant for block
}
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
}
bool ShaderVariantAssetBuilder::SerializeOutShaderVariantAsset(
const Data::Asset<RPI::ShaderVariantAsset> shaderVariantAsset, const AZStd::string& shaderStemNamePrefix,
const AZStd::string& tempDirPath,
const RHI::ShaderPlatformInterface& shaderPlatformInterface, const uint32_t productSubID, AssetBuilderSDK::JobProduct& assetProduct)
{
AZStd::string filename = AZStd::string::format(
"%s_%s_%u.%s", shaderStemNamePrefix.c_str(), shaderPlatformInterface.GetAPIName().GetCStr(),
shaderVariantAsset->GetStableId().GetIndex(), RPI::ShaderVariantAsset::Extension);
AZStd::string assetPath;
AzFramework::StringFunc::Path::ConstructFull(tempDirPath.c_str(), filename.c_str(), assetPath, true);
if (!AZ::Utils::SaveObjectToFile(assetPath, AZ::DataStream::ST_BINARY, shaderVariantAsset.Get()))
{
AZ_Error(ShaderVariantAssetBuilderName, false, "Failed to save Shader Variant Asset to \"%s\"", assetPath.c_str());
return false;
}
assetProduct.m_productSubID = productSubID;
assetProduct.m_productFileName = assetPath;
assetProduct.m_productAssetType = azrtti_typeid<RPI::ShaderVariantAsset>();
assetProduct.m_dependenciesHandled = true; // This builder has no dependencies to output
AZ_TracePrintf(ShaderVariantAssetBuilderName, "Shader Variant Asset [%s] compiled successfully.\n", assetPath.c_str());
return true;
}
AZ::Outcome<Data::Asset<RPI::ShaderVariantAsset>, AZStd::string> ShaderVariantAssetBuilder::CreateShaderVariantAsset(
const RPI::ShaderVariantListSourceData::VariantInfo& shaderVariantInfo,
ShaderVariantCreationContext& creationContext,
AZStd::optional<RHI::ShaderPlatformInterface::ByProducts>& outputByproducts)
{
// Temporary structure used for sorting and caching intermediate results
struct OptionCache
{
AZ::Name m_optionName;
AZ::Name m_valueName;
RPI::ShaderOptionIndex m_optionIndex; // Cached m_optionName
RPI::ShaderOptionValue m_value; // Cached m_valueName
};
AZStd::vector<OptionCache> optionList;
// We can not have more options than the number of options in the layout:
optionList.reserve(creationContext.m_shaderOptionGroupLayout.GetShaderOptionCount());
// This loop will validate and cache the indices for each option value:
for (const auto& shaderOption : shaderVariantInfo.m_options)
{
Name optionName{shaderOption.first};
Name optionValue{shaderOption.second};
RPI::ShaderOptionIndex optionIndex = creationContext.m_shaderOptionGroupLayout.FindShaderOptionIndex(optionName);
if (optionIndex.IsNull())
{
return AZ::Failure(AZStd::string::format("Invalid shader option: %s", optionName.GetCStr()));
}
const RPI::ShaderOptionDescriptor& option = creationContext.m_shaderOptionGroupLayout.GetShaderOption(optionIndex);
RPI::ShaderOptionValue value = option.FindValue(optionValue);
if (value.IsNull())
{
return AZ::Failure(
AZStd::string::format("Invalid value (%s) for shader option: %s", optionValue.GetCStr(), optionName.GetCStr()));
}
optionList.push_back(OptionCache{optionName, optionValue, optionIndex, value});
}
// Create one instance of the shader variant
RPI::ShaderOptionGroup optionGroup(&creationContext.m_shaderOptionGroupLayout);
//! Contains the series of #define macro values that define a variant. Can be empty (root variant).
//! If this string is NOT empty, a new temporary hlsl file will be created that will be the combination
//! of this string + @m_hlslSourceContent.
AZStd::string hlslCodeToPrependForVariant;
// We want to go over all options listed in the variant and set their respective values
// This loop will populate the optionGroup and m_shaderCodePrefix in order of the option priority
for (const auto& optionCache : optionList)
{
const RPI::ShaderOptionDescriptor& option = creationContext.m_shaderOptionGroupLayout.GetShaderOption(optionCache.m_optionIndex);
// Assign the option value specified in the variant:
option.Set(optionGroup, optionCache.m_value);
// Populate all shader option defines. We have already confirmed they're valid.
hlslCodeToPrependForVariant += AZStd::string::format(
"#define %s_OPTION_DEF %s\n", optionCache.m_optionName.GetCStr(), optionCache.m_valueName.GetCStr());
}
AZStd::string variantShaderSourcePath;
// Check if we need to prepend any code prefix
if (!hlslCodeToPrependForVariant.empty())
{
// Prepend any shader code prefix that we should apply to this variant
// and save it back to a file.
AZStd::string variantShaderSourceString(hlslCodeToPrependForVariant);
variantShaderSourceString += creationContext.m_hlslSourceContent;
AZStd::string shaderAssetName = AZStd::string::format(
"%s_%s_%u.hlsl", creationContext.m_shaderStemNamePrefix.c_str(),
creationContext.m_shaderPlatformInterface.GetAPIName().GetCStr(), shaderVariantInfo.m_stableId);
AzFramework::StringFunc::Path::Join(
creationContext.m_tempDirPath.c_str(), shaderAssetName.c_str(), variantShaderSourcePath, true, true);
auto outcome = Utils::WriteFile(variantShaderSourceString, variantShaderSourcePath);
if (!outcome.IsSuccess())
{
return AZ::Failure(AZStd::string::format("Failed to create file %s", variantShaderSourcePath.c_str()));
}
}
else
{
variantShaderSourcePath = creationContext.m_hlslSourcePath;
}
AZ_TracePrintf(ShaderVariantAssetBuilderName, "Variant StableId: %u", shaderVariantInfo.m_stableId);
AZ_TracePrintf(ShaderVariantAssetBuilderName, "Variant Shader Options: %s", optionGroup.ToString().c_str());
const RPI::ShaderVariantStableId shaderVariantStableId{shaderVariantInfo.m_stableId};
// By this time the optionGroup was populated with all option values for the variant and
// the m_shaderCodePrefix contains all option related preprocessing macros
// Let's add the requested variant:
RPI::ShaderVariantAssetCreator variantCreator;
RPI::ShaderOptionGroup shaderOptions{&creationContext.m_shaderOptionGroupLayout, optionGroup.GetShaderVariantId()};
variantCreator.Begin(
creationContext.m_shaderVariantAssetId, optionGroup.GetShaderVariantId(), shaderVariantStableId,
shaderOptions.IsFullySpecified());
variantCreator.SetBuildTimestamp(creationContext.m_assetBuildTimestamp);
const AZStd::unordered_map<AZStd::string, RPI::ShaderStageType>& shaderEntryPoints = creationContext.m_shaderEntryPoints;
for (const auto& shaderEntryPoint : shaderEntryPoints)
{
auto shaderEntryName = shaderEntryPoint.first;
auto shaderStageType = shaderEntryPoint.second;
AZ_TracePrintf(ShaderVariantAssetBuilderName, "Entry Point: %s", shaderEntryName.c_str());
AZ_TracePrintf(ShaderVariantAssetBuilderName, "Begin compiling shader function \"%s\"", shaderEntryName.c_str());
auto assetBuilderShaderType = ShaderBuilderUtility::ToAssetBuilderShaderType(shaderStageType);
// Compile HLSL to the platform specific shader.
RHI::ShaderPlatformInterface::StageDescriptor descriptor;
bool shaderWasCompiled = creationContext.m_shaderPlatformInterface.CompilePlatformInternal(
creationContext.m_platformInfo, variantShaderSourcePath, shaderEntryName, assetBuilderShaderType,
creationContext.m_tempDirPath, descriptor, creationContext.m_shaderCompilerArguments);
if (!shaderWasCompiled)
{
return AZ::Failure(AZStd::string::format("Could not compile the shader function %s", shaderEntryName.c_str()));
}
// bubble up the byproducts to the caller by moving them to the context.
outputByproducts.emplace(AZStd::move(descriptor.m_byProducts));
RHI::Ptr<RHI::ShaderStageFunction> shaderStageFunction = creationContext.m_shaderPlatformInterface.CreateShaderStageFunction(descriptor);
variantCreator.SetShaderFunction(ToRHIShaderStage(assetBuilderShaderType), shaderStageFunction);
if (descriptor.m_byProducts.m_dynamicBranchCount != AZ::RHI::ShaderPlatformInterface::ByProducts::UnknownDynamicBranchCount)
{
AZ_TracePrintf(
ShaderVariantAssetBuilderName, "Finished compiling shader function. Number of dynamic branches: %u",
descriptor.m_byProducts.m_dynamicBranchCount);
}
else
{
AZ_TracePrintf(
ShaderVariantAssetBuilderName, "Finished compiling shader function. Number of dynamic branches: unknown");
}
}
Data::Asset<RPI::ShaderVariantAsset> shaderVariantAsset;
variantCreator.End(shaderVariantAsset);
return AZ::Success(AZStd::move(shaderVariantAsset));
}
} // ShaderBuilder
} // AZ