029bf03870
Using a normal job dependency was overkill. As a result, modifying a shader would trigger a pass to rebuild, and that would cause the entire render pipeline to reinitialize. This causes a lot of unnecessary churn that made debugging shader hot reloads difficult. When a builder depends on knowing the UUID of another asset, it does need to have a job dependency on that asset. But when the builder doesn’t actually consume any data from inside the asset, and it just needs the ID, it is best to use JobDependencyType::OrderOnce to simply make sure the AP knows about the asset. Also, the call to GetSourceInfoBySourcePath was an unnecessary check because the AP does not require the file to exist at the time a dependency is reported. GetSourceInfoBySourcePath will only succeed after the AP has scanned the requested source file. We can’t just assume that a false result indicates the file does not exist, it just isn’t known yet. There are a couple additional use cases to be aware of (from @antonmic)... 1. Passes are critical assets, and some passes depend on shaders, so those shaders need to be processed, that's one reason for the dependency (otherwise you can start the engine, load all critical passes, try to load the shader, shader isn't ready, and bad things happen) 2. If a shader is deleted/removed, the pass should try to rebuild and fail. This was a recent issue that I fixed, you could have some odd condition where you delete a shader, but the pass that references that shader doesn't update or throw any errors because it doesn't rebuild, so the user is unaware that they need to change the .pass file and bad things happen OrderOnce is sufficient for both these cases. #1 is related to first time processing which exactly the time when OrderOnce would be applied. I tested #2 as well; deleting a .shader file with OrderOnce dependency did trigger the .pass file to rebuild and fail. Testing: Ran an ASV pass test with both an exiting cache and after deleting the local cache. Started the Editor with a clean cache and didn't encounter any startup issues. Was able to successfully load a level the first time. Deleted SkyBox.shader and saw SkyBox.pass fail in the AP. Signed-off-by: santorac <55155825+santorac@users.noreply.github.com>
352 lines
14 KiB
C++
352 lines
14 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 <Pass/PassBuilder.h>
|
|
#include <Atom/RPI.Edit/Common/AssetUtils.h>
|
|
|
|
#include <AzCore/Asset/AssetManagerBus.h>
|
|
#include <AzCore/Serialization/Json/JsonUtils.h>
|
|
#include <AzCore/StringFunc/StringFunc.h>
|
|
|
|
#include <Atom/RPI.Reflect/Asset/AssetReference.h>
|
|
#include <Atom/RPI.Reflect/Pass/PassAsset.h>
|
|
|
|
#include <AzFramework/IO/LocalFileIO.h>
|
|
#include <AzFramework/StringFunc/StringFunc.h>
|
|
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
|
|
|
|
#include <AssetBuilderSDK/AssetBuilderSDK.h>
|
|
|
|
namespace AZ
|
|
{
|
|
namespace RPI
|
|
{
|
|
namespace
|
|
{
|
|
[[maybe_unused]] static const char* PassBuilderName = "PassBuilder";
|
|
static const char* PassBuilderJobKey = "Pass Asset Builder";
|
|
static const char* PassAssetExtension = "pass";
|
|
}
|
|
|
|
namespace PassBuilderNamespace
|
|
{
|
|
enum PassDependencies
|
|
{
|
|
Shader,
|
|
AttachmentImage,
|
|
Count
|
|
};
|
|
|
|
static const AZStd::tuple<const char*, const char*> DependencyExtensionJobKeyTable[PassDependencies::Count] =
|
|
{
|
|
{".shader", "Shader Asset"},
|
|
{".attimage", "Any Asset Builder"}
|
|
};
|
|
}
|
|
|
|
void PassBuilder::RegisterBuilder()
|
|
{
|
|
AssetBuilderSDK::AssetBuilderDesc builder;
|
|
builder.m_name = PassBuilderJobKey;
|
|
builder.m_version = 15; // Changed dependency type to OrderOnce
|
|
builder.m_busId = azrtti_typeid<PassBuilder>();
|
|
builder.m_createJobFunction = AZStd::bind(&PassBuilder::CreateJobs, this, AZStd::placeholders::_1, AZStd::placeholders::_2);
|
|
builder.m_processJobFunction = AZStd::bind(&PassBuilder::ProcessJob, this, AZStd::placeholders::_1, AZStd::placeholders::_2);
|
|
|
|
// Match *.pass extension
|
|
builder.m_patterns.emplace_back(
|
|
AssetBuilderSDK::AssetBuilderPattern(
|
|
AZStd::string("*.") + PassAssetExtension,
|
|
AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard
|
|
)
|
|
);
|
|
|
|
BusConnect(builder.m_busId);
|
|
|
|
AssetBuilderSDK::AssetBuilderBus::Broadcast(&AssetBuilderSDK::AssetBuilderBus::Handler::RegisterBuilderInformation, builder);
|
|
}
|
|
|
|
PassBuilder::~PassBuilder()
|
|
{
|
|
BusDisconnect();
|
|
}
|
|
|
|
void PassBuilder::ShutDown()
|
|
{
|
|
m_isShuttingDown = true;
|
|
}
|
|
|
|
// --- Code related to dependency shader asset handling ---
|
|
|
|
// Helper class to pass parameters to the AddDependency and FindReferencedAssets functions below
|
|
struct FindPassReferenceAssetParams
|
|
{
|
|
void* passAssetObject = nullptr;
|
|
Uuid passAssetUuid;
|
|
SerializeContext* serializeContext = nullptr;
|
|
AZStd::string_view passAssetSourceFile; // File path of the pass asset
|
|
AZStd::string_view dependencySourceFile; // File pass of the asset the pass asset depends on
|
|
const char* jobKey = nullptr; // Job key for adding job dependency
|
|
};
|
|
|
|
// Helper function to get a file reference and create a corresponding job dependency
|
|
bool AddDependency(FindPassReferenceAssetParams& params, AssetBuilderSDK::JobDescriptor* job)
|
|
{
|
|
// We use an OrderOnce job dependency to ensure that the Asset Processor knows about the
|
|
// referenced asset, so we can make an AssetId for it later in ProcessJob. OrderOnce is
|
|
// enough because we don't need to read any data from the asset, we just needs its ID.
|
|
AssetBuilderSDK::JobDependency jobDependency;
|
|
jobDependency.m_jobKey = params.jobKey;
|
|
jobDependency.m_type = AssetBuilderSDK::JobDependencyType::OrderOnce;
|
|
jobDependency.m_sourceFile.m_sourceFileDependencyPath = params.dependencySourceFile;
|
|
job->m_jobDependencyList.push_back(jobDependency);
|
|
AZ_TracePrintf(PassBuilderName, "Creating job dependency on file [%.*s] \n", AZ_STRING_ARG(params.dependencySourceFile));
|
|
return true;
|
|
}
|
|
|
|
bool SetJobKeyForExtension(const AZStd::string& filePath, FindPassReferenceAssetParams& params)
|
|
{
|
|
AZStd::string extension;
|
|
StringFunc::Path::GetExtension(filePath.c_str(), extension);
|
|
for (const auto& [dependencyExtension, jobKey] : PassBuilderNamespace::DependencyExtensionJobKeyTable)
|
|
{
|
|
if (extension == dependencyExtension)
|
|
{
|
|
params.jobKey = jobKey;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
AZ_Error(PassBuilderName, false, "PassBuilder found a dependency with extension '%s', but does not know the corresponding job key. Add the job key for that extension to SetJobKeyForExtension in PassBuilder.cpp", extension.c_str());
|
|
params.jobKey = "Unknown";
|
|
return false;
|
|
}
|
|
|
|
// Helper function to find all assetId's and object references
|
|
bool FindReferencedAssets(
|
|
FindPassReferenceAssetParams& params, AssetBuilderSDK::JobDescriptor* job, AZStd::vector<AssetBuilderSDK::ProductDependency>* productDependencies)
|
|
{
|
|
SerializeContext::ErrorHandler errorLogger;
|
|
errorLogger.Reset();
|
|
|
|
bool success = true;
|
|
|
|
// This callback will check whether the given element is an asset reference. If so, it will add it to the list of asset references
|
|
auto beginCallback = [&](void* ptr, const SerializeContext::ClassData* classData, [[maybe_unused]] const SerializeContext::ClassElement* classElement)
|
|
{
|
|
// If the enumerated element is an asset reference
|
|
if (classData->m_typeId == azrtti_typeid<AssetReference>())
|
|
{
|
|
AssetReference* assetReference = reinterpret_cast<AssetReference*>(ptr);
|
|
|
|
// If the asset id isn't already provided, get it using the source file path
|
|
if (!assetReference->m_assetId.IsValid() && !assetReference->m_filePath.empty())
|
|
{
|
|
const AZStd::string& path = assetReference->m_filePath;
|
|
uint32_t subId = 0;
|
|
|
|
if (job != nullptr) // Create Job Phase
|
|
{
|
|
params.dependencySourceFile = path;
|
|
success &= SetJobKeyForExtension(path, params);
|
|
success &= AddDependency(params, job);
|
|
}
|
|
else // Process Job Phase
|
|
{
|
|
auto assetIdOutcome = AssetUtils::MakeAssetId(path, subId);
|
|
|
|
if (assetIdOutcome)
|
|
{
|
|
assetReference->m_assetId = assetIdOutcome.GetValue();
|
|
productDependencies->push_back(
|
|
AssetBuilderSDK::ProductDependency{assetReference->m_assetId, AZ::Data::ProductDependencyInfo::CreateFlags(Data::AssetLoadBehavior::NoLoad)}
|
|
);
|
|
}
|
|
else
|
|
{
|
|
AZ_Error(PassBuilderName, false, "Could not get AssetId for [%s]", assetReference->m_filePath.c_str());
|
|
success = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
// Setup enumeration context
|
|
SerializeContext::EnumerateInstanceCallContext callContext(
|
|
AZStd::move(beginCallback),
|
|
nullptr,
|
|
params.serializeContext,
|
|
SerializeContext::ENUM_ACCESS_FOR_READ,
|
|
&errorLogger
|
|
);
|
|
|
|
// Recursively iterate over all elements in the object to find asset references with the above callback
|
|
params.serializeContext->EnumerateInstance(
|
|
&callContext
|
|
, params.passAssetObject
|
|
, params.passAssetUuid
|
|
, nullptr
|
|
, nullptr
|
|
);
|
|
|
|
return success;
|
|
}
|
|
|
|
// --- Code related to dependency shader asset handling ---
|
|
|
|
void PassBuilder::CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response) const
|
|
{
|
|
// --- Handle shutdown case ---
|
|
|
|
if (m_isShuttingDown)
|
|
{
|
|
response.m_result = AssetBuilderSDK::CreateJobsResultCode::ShuttingDown;
|
|
return;
|
|
}
|
|
|
|
// --- Get serialization context ---
|
|
|
|
SerializeContext* serializeContext = nullptr;
|
|
ComponentApplicationBus::BroadcastResult(serializeContext, &ComponentApplicationBus::Events::GetSerializeContext);
|
|
if (!serializeContext)
|
|
{
|
|
AZ_Assert(false, "No serialize context");
|
|
return;
|
|
}
|
|
|
|
// --- Load PassAsset ---
|
|
|
|
AZStd::string fullPath;
|
|
AzFramework::StringFunc::Path::ConstructFull(request.m_watchFolder.c_str(), request.m_sourceFile.c_str(), fullPath, true);
|
|
|
|
PassAsset passAsset;
|
|
AZ::Outcome<void, AZStd::string> loadResult = JsonSerializationUtils::LoadObjectFromFile(passAsset, fullPath);
|
|
|
|
if (!loadResult.IsSuccess())
|
|
{
|
|
AZ_Error(PassBuilderName, false, "Failed to load pass asset [%s]", request.m_sourceFile.c_str());
|
|
AZ_Error(PassBuilderName, false, "Loading issues: %s", loadResult.GetError().data());
|
|
return;
|
|
}
|
|
|
|
AssetBuilderSDK::JobDescriptor job;
|
|
job.m_jobKey = PassBuilderJobKey;
|
|
job.m_critical = true; // Passes are a critical part of the rendering system
|
|
|
|
// --- Find all dependencies ---
|
|
|
|
AZStd::unordered_set<Data::AssetId> dependentList;
|
|
Uuid passAssetUuid = AzTypeInfo<PassAsset>::Uuid();
|
|
|
|
FindPassReferenceAssetParams params;
|
|
params.passAssetObject = &passAsset;
|
|
params.passAssetSourceFile = request.m_sourceFile;
|
|
params.passAssetUuid = passAssetUuid;
|
|
params.serializeContext = serializeContext;
|
|
params.jobKey = "Unknown";
|
|
|
|
if (!FindReferencedAssets(params, &job, nullptr))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// --- Create a job per platform ---
|
|
|
|
for (const AssetBuilderSDK::PlatformInfo& platformInfo : request.m_enabledPlatforms)
|
|
{
|
|
for (auto& jobDependency : job.m_jobDependencyList)
|
|
{
|
|
jobDependency.m_platformIdentifier = platformInfo.m_identifier.c_str();
|
|
}
|
|
job.SetPlatformIdentifier(platformInfo.m_identifier.c_str());
|
|
response.m_createJobOutputs.push_back(job);
|
|
}
|
|
|
|
response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
|
|
}
|
|
|
|
void PassBuilder::ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response) const
|
|
{
|
|
// --- Handle job cancellation and shutdown cases ---
|
|
|
|
AssetBuilderSDK::JobCancelListener jobCancelListener(request.m_jobId);
|
|
if (jobCancelListener.IsCancelled() || m_isShuttingDown)
|
|
{
|
|
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
|
|
return;
|
|
}
|
|
|
|
// --- Get serialization context ---
|
|
|
|
SerializeContext* serializeContext = nullptr;
|
|
ComponentApplicationBus::BroadcastResult(serializeContext, &ComponentApplicationBus::Events::GetSerializeContext);
|
|
if (!serializeContext)
|
|
{
|
|
AZ_Assert(false, "No serialize context");
|
|
return;
|
|
}
|
|
|
|
// --- Load PassAsset ---
|
|
|
|
PassAsset passAsset;
|
|
AZ::Outcome<void, AZStd::string> loadResult = JsonSerializationUtils::LoadObjectFromFile(passAsset, request.m_fullPath);
|
|
|
|
if (!loadResult.IsSuccess())
|
|
{
|
|
AZ_Error(PassBuilderName, false, "Failed to load pass asset [%s]", request.m_fullPath.c_str());
|
|
AZ_Error(PassBuilderName, false, "Loading issues: %s", loadResult.GetError().data());
|
|
return;
|
|
}
|
|
|
|
// --- Find all dependencies ---
|
|
|
|
Uuid passAssetUuid = AzTypeInfo<PassAsset>::Uuid();
|
|
|
|
FindPassReferenceAssetParams params;
|
|
params.passAssetObject = &passAsset;
|
|
params.passAssetSourceFile = request.m_sourceFile;
|
|
params.passAssetUuid = passAssetUuid;
|
|
params.serializeContext = serializeContext;
|
|
params.jobKey = "Unknown";
|
|
|
|
AZStd::vector<AssetBuilderSDK::ProductDependency> productDependencies;
|
|
if (!FindReferencedAssets(params, nullptr, &productDependencies))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// --- Get destination file name and path ---
|
|
|
|
AZStd::string destFileName;
|
|
AZStd::string destPath;
|
|
AzFramework::StringFunc::Path::GetFullFileName(request.m_fullPath.c_str(), destFileName);
|
|
AzFramework::StringFunc::Path::ConstructFull(request.m_tempDirPath.c_str(), destFileName.c_str(), destPath, true);
|
|
|
|
// --- Save the asset to binary format for production ---
|
|
|
|
bool result = Utils::SaveObjectToFile(destPath, DataStream::ST_BINARY, &passAsset, passAssetUuid, serializeContext);
|
|
if (result == false)
|
|
{
|
|
AZ_Error(PassBuilderName, false, "Failed to save asset to %s", destPath.c_str());
|
|
return;
|
|
}
|
|
|
|
// --- Save output product(s) to response ---
|
|
|
|
AssetBuilderSDK::JobProduct jobProduct(destPath, PassAsset::RTTI_Type(), 0);
|
|
jobProduct.m_dependencies = productDependencies;
|
|
jobProduct.m_dependenciesHandled = true;
|
|
response.m_outputProducts.push_back(jobProduct);
|
|
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
|
|
}
|
|
|
|
} // namespace RPI
|
|
} // namespace AZ
|