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/AzslBuilder.cpp

403 lines
23 KiB
C++

/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "AzslBuilder.h"
#include "ShaderBuilderUtility.h"
#include "ShaderPlatformInterfaceRequest.h"
#include <CommonFiles/GlobalBuildOptions.h>
#include <Atom/RPI.Edit/Common/AssetUtils.h>
#include <AzCore/IO/SystemFile.h>
#include <math.h>
namespace AZ
{
namespace ShaderBuilder
{
enum class JobParameterIndices : uint32_t
{
ApiName,
PreprocessedCode,
PreprocessorError,
SkipJob
};
AZ::Uuid AzslBuilder::GetUUID()
{
return AZ::Uuid::CreateString("{72DCFC95-1B9E-4A8D-8633-D497CACD98AB}");
}
RHI::ShaderPlatformInterface* GetShaderPlatformInterfaceForApi(const AZStd::string& apiNameFilter, const AssetBuilderSDK::PlatformInfo& currentPlatform)
{
AZStd::vector<RHI::ShaderPlatformInterface*> platformInterfaces;
ShaderPlatformInterfaceRequestBus::BroadcastResult(platformInterfaces, &ShaderPlatformInterfaceRequest::GetShaderPlatformInterface, currentPlatform);
for (RHI::ShaderPlatformInterface* oneInterface : platformInterfaces)
{
if (oneInterface && oneInterface->GetAPIName().GetStringView() == apiNameFilter)
{
return oneInterface;
}
}
return nullptr;
}
PreprocessorData PreprocessSource(const AZStd::string& inputFile, const AZStd::string& originalPath, const PreprocessorOptions& options)
{
// run mcpp
PreprocessorData output;
PreprocessFile(inputFile, output, options, true, true);
// do not let the filename.api.azsl.prepend be the filename that will be regarded as the source.
// because SRG assets are located using the 'containingFile' so we need to preserve the true origin:
MutateLineDirectivesFileOrigin(output.code, originalPath);
RHI::ReportErrorMessages(AzslBuilder::BuilderName, output.diagnostics);
return output;
}
void AddAzslBuilderJobDependency(AssetBuilderSDK::JobDescriptor& jobDescriptor, const AZStd::string& platformInfoIdentifier, AZStd::string_view apiName, AZStd::string_view fullFilePath)
{
AssetBuilderSDK::SourceFileDependency fileDependency;
fileDependency.m_sourceFileDependencyPath = fullFilePath;
AssetBuilderSDK::JobDependency dependency;
dependency.m_jobKey = AzslBuilder::JobKey;
dependency.m_jobKey += " ";
dependency.m_jobKey += apiName;
dependency.m_platformIdentifier = platformInfoIdentifier;
dependency.m_sourceFile = fileDependency;
dependency.m_type = AssetBuilderSDK::JobDependencyType::Order;
jobDescriptor.m_jobDependencyList.emplace_back(dependency);
}
void AzslBuilder::CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response) const
{
AZStd::string fullPath;
AzFramework::StringFunc::Path::ConstructFull(request.m_watchFolder.data(), request.m_sourceFile.data(), fullPath, true);
// this builder may take as input:
// .shader .azsl .azsli .srgi
// it will not behave strictly exactly the same for each type
// Only *.srgi files are supposed to include files that define "partial" qualified SRGs.
const bool isSrgi = AzFramework::StringFunc::Path::IsExtension(fullPath.c_str(), SrgIncludeExtension);
// .azsli needs "skip check"
const bool isAzsli = AzFramework::StringFunc::Path::IsExtension(fullPath.c_str(), "azsli");
// .shader files must be opened to get their build options and the referenced azsl file
const bool isShader = AzFramework::StringFunc::Path::IsExtension(fullPath.c_str(), RPI::ShaderSourceData::Extension);
// .azsl must not be skipped, otherwise we're creating a risk of two .shader referring to the same .azsl racing for its output product
// To avoid the warning:
// "No job was found to match the job dependency criteria declared by file "..."
// We will schedule the job, but will do nothing
//bool shouldSkipFile = false;
// 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.
//bool gotPreprocessingError = false;
// The following if-block will be removed once [GFX TODO][ATOM-5302] is addressed, and
// azslc allows redundant SrgSemantics for "partial" qualified SRGs.
if (isAzsli)
{
auto skipCheck = ShaderBuilderUtility::ShouldSkipFileForSrgProcessing(BuilderName, fullPath);
if (skipCheck != ShaderBuilderUtility::SrgSkipFileResult::ContinueProcess)
{
response.m_result = skipCheck == ShaderBuilderUtility::SrgSkipFileResult::Error ?
AssetBuilderSDK::CreateJobsResultCode::Failed : AssetBuilderSDK::CreateJobsResultCode::Success;
return;
}
}
if (isShader)
{
// Need to get the path to the shader file from the template, so that we can preprocess the shader data and setup
// source file dependencies.
auto descriptorParseOutput = ShaderBuilderUtility::LoadShaderDataJson(fullPath);
if (!descriptorParseOutput.IsSuccess())
{
AZ_Error(BuilderName, false, "Failed to parse Shader Descriptor JSON: %s", descriptorParseOutput.GetError().c_str());
return;
}
// update the value of fullPath to mean directly, the azsl file:
ShaderBuilderUtility::GetAbsolutePathToAzslFile(fullPath, descriptorParseOutput.GetValue().m_source, fullPath);
}
GlobalBuildOptions buildOptions = ReadBuildOptions(BuilderName);
for (const AssetBuilderSDK::PlatformInfo& info : request.m_enabledPlatforms)
{
AZ_TraceContext("For platform", info.m_identifier.data());
// Get the platform interfaces to be able to access the prepend file
AZStd::vector<RHI::ShaderPlatformInterface*> platformInterfaces = ShaderBuilderUtility::DiscoverValidShaderPlatformInterfaces(info);
// Preprocess the shader file, per activated platform.
for (RHI::ShaderPlatformInterface* shaderPlatformInterface : platformInterfaces)
{
auto apiNameAsStringView = shaderPlatformInterface->GetAPIName().GetStringView();
AssetBuilderSDK::JobDescriptor jobDescriptor;
jobDescriptor.m_priority = 2;
// [GFX TODO][ATOM-2830] Set 'm_critical' back to 'false' once proper fix for Atom startup issues are in
jobDescriptor.m_critical = true;
jobDescriptor.m_jobKey = JobKey;
jobDescriptor.m_jobKey += " ";
jobDescriptor.m_jobKey += apiNameAsStringView;
jobDescriptor.SetPlatformIdentifier(info.m_identifier.data());
jobDescriptor.m_jobParameters[(u32)JobParameterIndices::ApiName] = apiNameAsStringView;
if (isShader)
{
// add a job dependency on the azsl run (of that same job: AzslBuilder - because it also runs on .azsl)
AddAzslBuilderJobDependency(jobDescriptor, info.m_identifier, apiNameAsStringView, fullPath);
}
// execute azsl prepending here, before preprocess, in order to support macros in AzslcHeader.azsli
AZStd::string prependedAzslSourceCode;
RHI::PrependArguments args;
args.m_sourceFile = fullPath.c_str();
args.m_prependFile = shaderPlatformInterface->GetAzslHeader(info);
args.m_addSuffixToFileName = shaderPlatformInterface->GetAPIName().GetCStr();
args.m_destinationStringOpt = &prependedAzslSourceCode;
if (RHI::PrependFile(args) == fullPath) // error case. it returns the combined-file's name on success, or original path on failure, but here we use the "direct to string" mode so we don't need to store the returned name.
{
response.m_result = AssetBuilderSDK::CreateJobsResultCode::Failed;
return;
}
AZStd::string originalLocation;
// extract the (full) directory chain from the path: (eg from "d:/p/f.e" extract "d:/p/")
AzFramework::StringFunc::Path::GetFullPath(fullPath.c_str(), originalLocation);
// have to go through filesystem because we have no way to pipe data through mcpp (because of single threaded static link call and buffer limits)
// we can't use a temporary folder because CreateJobs API does not warrant side effects, and does not prepare a temp folder.
// we can't use the OS temp folder anyway, because many includes (eg #include "../RPI/Shadow.h") are relative and will only work from the original location
AZStd::string prependedPath = ShaderBuilderUtility::DumpAzslPrependedCode(
BuilderName, prependedAzslSourceCode, originalLocation, ShaderBuilderUtility::ExtractStemName(fullPath.c_str()),
shaderPlatformInterface->GetAPIName().GetStringView());
// run mcpp
PreprocessorData preprocessorData = PreprocessSource(prependedPath, fullPath, buildOptions.m_preprocessorSettings);
jobDescriptor.m_jobParameters[(u32)JobParameterIndices::PreprocessorError] = preprocessorData.diagnostics; // save for ProcessJob
jobDescriptor.m_jobParameters[(u32)JobParameterIndices::PreprocessedCode] = preprocessorData.code; // save for ProcessJob
AZ::IO::SystemFile::Delete(prependedPath.c_str()); // don't let that intermediate file dirty a folder under source version control.
for (AZStd::string includePath : preprocessorData.includedPaths)
{
// m_sourceFileDependencyList does not support paths with "." or ".." for relative lookup, but the preprocessor
// may produce path strings like "C:/a/b/c/../../d/file.azsli" so we have to normalize
AzFramework::StringFunc::Path::Normalize(includePath);
AssetBuilderSDK::SourceFileDependency includeFileDependency;
includeFileDependency.m_sourceFileDependencyPath = includePath;
response.m_sourceFileDependencyList.emplace_back(includeFileDependency);
}
response.m_createJobOutputs.push_back(jobDescriptor);
} // all RHI platforms
} // for all request.m_enabledPlatforms
response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
}
enum class ErrorStrategy { HardFailIfAbsent, ReturnEmptyIfAbsent };
static AZStd::string GetJobParameterValue(const AssetBuilderSDK::ProcessJobRequest& request, JobParameterIndices index, [[maybe_unused]] ErrorStrategy failBehavior)
{
auto iterator = request.m_jobDescription.m_jobParameters.find(static_cast<u32>(index));
if (iterator == request.m_jobDescription.m_jobParameters.end())
{
AZ_Error(AzslBuilder::BuilderName, failBehavior != ErrorStrategy::HardFailIfAbsent, "Saved data is missing in job parameters. for index [%d]", index);
return {};
}
return iterator->second;
}
// eg: ("D:/p/x.a", "D:/p/x.b") -> yes
static bool HasSameFileName(const AZStd::string& lhsPath, const AZStd::string& rhsPath)
{
using namespace StringFunc::Path;
AZStd::string stem1;
GetFileName(lhsPath.c_str(), stem1);
AZStd::string stem2;
GetFileName(rhsPath.c_str(), stem2);
return stem1 == stem2;
}
void AzslBuilder::ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response) const
{
if (request.m_jobDescription.m_jobParameters.find((u32)JobParameterIndices::SkipJob) != request.m_jobDescription.m_jobParameters.end())
{
AZ_TracePrintf(BuilderName, "Early out because this file was determined to not need an independent build\n");
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
return;
}
// report the deffered diagnostics:
const AZStd::string& preprocessorErrors = GetJobParameterValue(request, JobParameterIndices::PreprocessorError, ErrorStrategy::ReturnEmptyIfAbsent);
if (!preprocessorErrors.empty())
{
bool foundErrors = RHI::ReportErrorMessages(BuilderName, preprocessorErrors.c_str());
if (foundErrors)
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
}
const AZStd::sys_time_t startTime = AZStd::GetTimeNowTicks();
AZStd::string fullSourcePath;
AzFramework::StringFunc::Path::ConstructFull(request.m_watchFolder.c_str(), request.m_sourceFile.c_str(), fullSourcePath, true);
// extract "name" from "P:/F/name.x"
AZStd::string sourceStemName;
AzFramework::StringFunc::Path::GetFileName(fullSourcePath.c_str(), sourceStemName);
GlobalBuildOptions buildOptions = ReadBuildOptions(BuilderName);
// get the shader platform interface that matches this job's API
const AZStd::string& apiName = GetJobParameterValue(request, JobParameterIndices::ApiName, ErrorStrategy::HardFailIfAbsent);
const AZStd::string& preprocessedCode = GetJobParameterValue(request, JobParameterIndices::PreprocessedCode, ErrorStrategy::ReturnEmptyIfAbsent);
RHI::ShaderPlatformInterface* platformInterface = GetShaderPlatformInterfaceForApi(apiName, request.m_platformInfo);
if (!platformInterface)
{
AZ_Error(BuilderName, false, "Could not retreive Shader Platform Interface");
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
const bool isSrgi = AzFramework::StringFunc::Path::IsExtension(fullSourcePath.c_str(), SrgIncludeExtension);
const bool isAzsli = AzFramework::StringFunc::Path::IsExtension(fullSourcePath.c_str(), "azsli");
const bool isShader = AzFramework::StringFunc::Path::IsExtension(fullSourcePath.c_str(), RPI::ShaderSourceData::Extension);
if (isShader)
{
// read .shader -> access azsl path -> make absolute
RPI::ShaderSourceData shaderAssetSource;
AZStd::shared_ptr<ShaderFiles> inputFiles = ShaderBuilderUtility::PrepareSourceInput(BuilderName, fullSourcePath, shaderAssetSource);
if (!inputFiles)
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
if (shaderAssetSource.IsRhiBackendDisabled(platformInterface->GetAPIName()))
{
// Gracefully do nothing and return success.
AZ_TracePrintf(
BuilderName, "Skipping shader compilation [%s] for API [%s]\n", fullSourcePath.c_str(),
platformInterface->GetAPIName().GetCStr());
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
return;
}
// Save .shader file name
AzFramework::StringFunc::Path::GetFileName(request.m_sourceFile.data(), inputFiles->m_shaderFileName);
// Verify the presence of potential differences between the global options, and local options:
bool mustRebuild = buildOptions.m_compilerArguments.HasDifferentAzslcArguments(shaderAssetSource.m_compiler);
// Merge compiler options coming from 2 source: global options (from project Config/), and .shader options.
// We define a merge behavior that is: ".shader wins if set" (local overrides global)
buildOptions.m_compilerArguments.Merge(shaderAssetSource.m_compiler);
// Earlier, we declared a job dependency on the .azsl's job, let's access the produced assets:
uint32_t subId = ShaderBuilderUtility::MakeAzslBuildProductSubId(
RPI::ShaderAssetSubId::GeneratedHlslSource, platformInterface->GetAPIType());
auto assetIdOutcome = RPI::AssetUtils::MakeAssetId(inputFiles->m_azslSourceFullPath, subId);
AZ_Warning(BuilderName, assetIdOutcome.IsSuccess(), "Product of dependency %s not found: this is an oddity but build can continue.", inputFiles->m_azslSourceFullPath.c_str());
if (assetIdOutcome.IsSuccess())
{
// The .azsl build job didn't know about the build options listed in the .shader
// so it produced "generic" artifacts xxx.ia.json, xxx.hlsl, etc.
if (!mustRebuild)
{
// They are in fact sufficient. nothing more to do
AZ_TracePrintf(BuilderName, "Product output already built by %s. exiting.", inputFiles->m_azslSourceFullPath.c_str());
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
return;
}
// Otherwise, let's go again, but we need to modify the output's name to avoid product conflicts.*
AZ_TracePrintf(BuilderName, "Product output already built by %s is not reusable because of incompatible azslc CompilerHints: launching independent build", inputFiles->m_azslSourceFullPath.c_str());
}
if (HasSameFileName(fullSourcePath, inputFiles->m_azslSourceFullPath))
{
// let's add a "distinguisher" to the names of the outproduct artifacts of this build round.*
// Because otherwise the asset processor is not going to accept an overwrite of the ones output by the .azsl job
static constexpr char RebuildSuffix[] = ".shader-w-diff-azslc-opts";
sourceStemName += RebuildSuffix;
}
}
AZStd::string preprocessedPath = ShaderBuilderUtility::DumpPreprocessedCode(
BuilderName,
preprocessedCode,
request.m_tempDirPath,
sourceStemName,
apiName);
AZ_TraceContext("Platform API", apiName);
AssetBuilderSDK::JobCancelListener jobCancelListener(request.m_jobId);
if (jobCancelListener.IsCancelled())
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
return;
}
// compiler setup
ShaderBuilder::AzslCompiler azslc(preprocessedPath);
AZStd::string compilerParameters = platformInterface->GetAzslCompilerParameters(buildOptions.m_compilerArguments);
compilerParameters += " ";
compilerParameters += platformInterface->GetAzslCompilerWarningParameters(buildOptions.m_compilerArguments);
AtomShaderConfig::AddParametersFromConfigFile(compilerParameters, request.m_platformInfo);
if (isSrgi || isAzsli)
{
// When compiling srgi or azsli files, the SRGs may appear as unused. It is necessary
// to remove the flag --strip-unused-srgs in case it is present in the compiler parameters.
AzFramework::StringFunc::Replace(compilerParameters, " --strip-unused-srgs", "");
}
AZStd::string outputName = AZStd::string::format("%s.%s.hlsl", sourceStemName.c_str(), apiName.c_str());
AzFramework::StringFunc::Path::Join(request.m_tempDirPath.c_str(), outputName.c_str(), outputName, true);
auto emitFullOutcome = azslc.EmitFullData(compilerParameters, outputName);
if (!emitFullOutcome.IsSuccess())
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
for (int i = 0; i < emitFullOutcome.GetValue().size(); ++i)
{
AssetBuilderSDK::JobProduct jobProduct;
jobProduct.m_productFileName = emitFullOutcome.GetValue()[i];
static const AZ::Uuid AzslOutcomeType = "{6977AEB1-17AD-4992-957B-23BB2E85B18B}";
jobProduct.m_productAssetType = AzslOutcomeType;
jobProduct.m_productSubID = ShaderBuilderUtility::MakeAzslBuildProductSubId(ShaderBuilderUtility::AzslSubProducts::SubList[i], platformInterface->GetAPIType());
jobProduct.m_dependenciesHandled = true;
// Note that the output products are not traditional product assets that will be used by the game project.
// They are artifacts that are produced once, cached, and used later by other AssetBuilders as a way to centralize build organization.
response.m_outputProducts.push_back(AZStd::move(jobProduct));
}
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
const AZStd::sys_time_t endTime = AZStd::GetTimeNowTicks();
const AZStd::sys_time_t deltaTime = endTime - startTime;
const float elapsedTimeSeconds = (float)(deltaTime) / (float)AZStd::GetTimeTicksPerSecond();
AZ_TracePrintf(BuilderName, "Finished compiling %s in %.2f seconds\n", request.m_sourceFile.c_str(), elapsedTimeSeconds);
ShaderBuilderUtility::LogProfilingData(BuilderName, sourceStemName);
} // end ProcessJob
} // ShaderBuilder
} // AZ