982406d4d5
ATOM-15728 Shader Hot Reload Fails in Debug Build The main change was to add OnAssetReady handlers to each of the asset classes. See comments in ShaderAsset::OnAssetReady for a detailed explanation. In short, OnAssetReloaded gets missed while assets are being reloaded at the same time on multiple threads, but OnAssetReady is always called whenever connecting to the AssetBus because of its AssetConnectionPolicy. The above change required the addition of a new AssetInitBus to call the PostLoadInit() functions. Because OnAssetReady connects to buses that are not mutex-protected, they have to be connected on the main thread. AssetInitBus::PostLoadInit is called every frame in RPISystem::SimulationTick. All Atom's asset handlers that need to do post-load initialization must connect to the AssetInitBus, and the asset will disconnect itself after initialization is complete. We also need the Shader class to handle OnShaderAssetReinitialized to properly handle the shader reload. With these changes I can click back and forth between "Blending On" and "Blending Off" many times (like 20 times) without issue.
390 lines
17 KiB
C++
390 lines
17 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 <Atom/RPI.Public/Shader/Shader.h>
|
|
|
|
#include <AzCore/IO/SystemFile.h>
|
|
|
|
#include <Atom/RHI/RHISystemInterface.h>
|
|
|
|
#include <Atom/RHI/PipelineStateCache.h>
|
|
#include <Atom/RHI/Factory.h>
|
|
|
|
#include <AtomCore/Instance/InstanceDatabase.h>
|
|
|
|
#include <AzCore/Interface/Interface.h>
|
|
#include <Atom/RPI.Public/Shader/ShaderReloadDebugTracker.h>
|
|
|
|
namespace AZ
|
|
{
|
|
namespace RPI
|
|
{
|
|
Data::Instance<Shader> Shader::FindOrCreate(const Data::Asset<ShaderAsset>& shaderAsset)
|
|
{
|
|
return Data::InstanceDatabase<Shader>::Instance().FindOrCreate(
|
|
Data::InstanceId::CreateFromAssetId(shaderAsset.GetId()),
|
|
shaderAsset);
|
|
}
|
|
|
|
Data::Instance<Shader> Shader::CreateInternal(ShaderAsset& shaderAsset)
|
|
{
|
|
Data::Instance<Shader> shader = aznew Shader();
|
|
const RHI::ResultCode resultCode = shader->Init(shaderAsset);
|
|
|
|
if (resultCode == RHI::ResultCode::Success)
|
|
{
|
|
return shader;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
Shader::~Shader()
|
|
{
|
|
Shutdown();
|
|
}
|
|
|
|
RHI::ResultCode Shader::Init(ShaderAsset& shaderAsset)
|
|
{
|
|
Data::AssetBus::Handler::BusDisconnect();
|
|
ShaderReloadNotificationBus::Handler::BusDisconnect();
|
|
ShaderVariantFinderNotificationBus::Handler::BusDisconnect();
|
|
|
|
RHI::RHISystemInterface* rhiSystem = RHI::RHISystemInterface::Get();
|
|
RHI::DrawListTagRegistry* drawListTagRegistry = rhiSystem->GetDrawListTagRegistry();
|
|
|
|
m_asset = { &shaderAsset, AZ::Data::AssetLoadBehavior::PreLoad };
|
|
m_pipelineStateType = shaderAsset.GetPipelineStateType();
|
|
|
|
{
|
|
AZStd::unique_lock<decltype(m_variantCacheMutex)> lock(m_variantCacheMutex);
|
|
m_shaderVariants.clear();
|
|
}
|
|
m_rootVariant.Init(shaderAsset, shaderAsset.GetRootVariant());
|
|
|
|
if (m_pipelineLibraryHandle.IsNull())
|
|
{
|
|
// We set up a pipeline library only once for the lifetime of the Shader instance.
|
|
// This should allow the Shader to be reloaded at runtime many times, and cache and reuse PipelineState objects rather than rebuild them.
|
|
// It also fixes a particular TDR crash that occurred on some hardware when hot-reloading shaders and building pipeline states
|
|
// in a new pipeline library every time.
|
|
|
|
RHI::PipelineStateCache* pipelineStateCache = rhiSystem->GetPipelineStateCache();
|
|
ConstPtr<RHI::PipelineLibraryData> serializedData = LoadPipelineLibrary();
|
|
RHI::PipelineLibraryHandle pipelineLibraryHandle = pipelineStateCache->CreateLibrary(serializedData.get());
|
|
|
|
if (pipelineLibraryHandle.IsNull())
|
|
{
|
|
AZ_Error("Shader", false, "Failed to create pipeline library from pipeline state cache.");
|
|
return RHI::ResultCode::Fail;
|
|
}
|
|
|
|
m_pipelineLibraryHandle = pipelineLibraryHandle;
|
|
m_pipelineStateCache = pipelineStateCache;
|
|
}
|
|
|
|
const Name& drawListName = shaderAsset.GetDrawListName();
|
|
if (!drawListName.IsEmpty())
|
|
{
|
|
m_drawListTag = drawListTagRegistry->AcquireTag(drawListName);
|
|
if (!m_drawListTag.IsValid())
|
|
{
|
|
AZ_Error("Shader", false, "Failed to acquire a DrawListTag. Entries are full.");
|
|
}
|
|
}
|
|
|
|
ShaderVariantFinderNotificationBus::Handler::BusConnect(m_asset.GetId());
|
|
Data::AssetBus::Handler::BusConnect(m_asset.GetId());
|
|
ShaderReloadNotificationBus::Handler::BusConnect(m_asset.GetId());
|
|
|
|
return RHI::ResultCode::Success;
|
|
}
|
|
|
|
void Shader::Shutdown()
|
|
{
|
|
ShaderVariantFinderNotificationBus::Handler::BusDisconnect();
|
|
Data::AssetBus::Handler::BusDisconnect();
|
|
ShaderReloadNotificationBus::Handler::BusDisconnect();
|
|
|
|
if (m_pipelineLibraryHandle.IsValid())
|
|
{
|
|
SavePipelineLibrary();
|
|
|
|
m_pipelineStateCache->ReleaseLibrary(m_pipelineLibraryHandle);
|
|
m_pipelineStateCache = nullptr;
|
|
m_pipelineLibraryHandle = {};
|
|
}
|
|
|
|
if (m_drawListTag.IsValid())
|
|
{
|
|
RHI::DrawListTagRegistry* drawListTagRegistry = RHI::RHISystemInterface::Get()->GetDrawListTagRegistry();
|
|
drawListTagRegistry->ReleaseTag(m_drawListTag);
|
|
m_drawListTag.Reset();
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
// AssetBus overrides
|
|
void Shader::OnAssetReloaded(Data::Asset<Data::AssetData> asset)
|
|
{
|
|
ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->Shader::OnAssetReloaded %s", this, asset.GetHint().c_str());
|
|
|
|
if (asset->GetId() == m_asset->GetId())
|
|
{
|
|
Data::Asset<ShaderAsset> newAsset = { asset.GetAs<ShaderAsset>(), AZ::Data::AssetLoadBehavior::PreLoad };
|
|
AZ_Assert(newAsset, "Reloaded ShaderAsset is null");
|
|
|
|
Init(*newAsset.Get());
|
|
ShaderReloadNotificationBus::Event(asset.GetId(), &ShaderReloadNotificationBus::Events::OnShaderReinitialized, *this);
|
|
}
|
|
}
|
|
///////////////////////////////////////////////////////////////////////
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
/// ShaderVariantFinderNotificationBus overrides
|
|
void Shader::OnShaderVariantAssetReady(Data::Asset<ShaderVariantAsset> shaderVariantAsset, bool isError)
|
|
{
|
|
AZ_Assert(shaderVariantAsset, "Reloaded ShaderVariantAsset is null");
|
|
const ShaderVariantStableId stableId = shaderVariantAsset->GetStableId();
|
|
const ShaderVariantId& shaderVariantId = shaderVariantAsset->GetShaderVariantId();
|
|
|
|
if (isError)
|
|
{
|
|
//Remark: We do not assert if the stableId == RootShaderVariantStableId, because we can not trust in the asset data
|
|
//on error. so it is possible that on error the stbleId == RootShaderVariantStableId;
|
|
if (stableId == RootShaderVariantStableId)
|
|
{
|
|
return;
|
|
}
|
|
AZStd::unique_lock<decltype(m_variantCacheMutex)> lock(m_variantCacheMutex);
|
|
m_shaderVariants.erase(stableId);
|
|
}
|
|
else
|
|
{
|
|
AZ_Assert(stableId != RootShaderVariantStableId,
|
|
"The root variant is expected to be updated by the ShaderAsset.");
|
|
AZStd::unique_lock<decltype(m_variantCacheMutex)> lock(m_variantCacheMutex);
|
|
|
|
auto iter = m_shaderVariants.find(stableId);
|
|
if (iter != m_shaderVariants.end())
|
|
{
|
|
ShaderVariant& shaderVariant = iter->second;
|
|
|
|
if (!shaderVariant.Init(*m_asset.Get(), shaderVariantAsset))
|
|
{
|
|
AZ_Error("Shader", false, "Failed to init shaderVariant with StableId=%u", shaderVariantAsset->GetStableId());
|
|
m_shaderVariants.erase(stableId);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//This is the first time the shader variant asset comes to life.
|
|
ShaderVariant newVariant;
|
|
newVariant.Init(*m_asset, shaderVariantAsset);
|
|
m_shaderVariants.emplace(stableId, newVariant);
|
|
}
|
|
}
|
|
|
|
//Even if there was an error, the interested parties should be notified.
|
|
ShaderReloadNotificationBus::Event(m_asset.GetId(), &ShaderReloadNotificationBus::Events::OnShaderVariantReinitialized, *this, shaderVariantId, stableId);
|
|
}
|
|
///////////////////////////////////////////////////////////////////
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
// ShaderReloadNotificationBus overrides...
|
|
void Shader::OnShaderAssetReinitialized(const Data::Asset<ShaderAsset>& shaderAsset)
|
|
{
|
|
ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->Shader::OnShaderAssetReinitialized %s", this, shaderAsset.GetHint().c_str());
|
|
|
|
Init(*m_asset.Get());
|
|
ShaderReloadNotificationBus::Event(shaderAsset.GetId(), &ShaderReloadNotificationBus::Events::OnShaderReinitialized, *this);
|
|
}
|
|
///////////////////////////////////////////////////////////////////
|
|
|
|
|
|
ConstPtr<RHI::PipelineLibraryData> Shader::LoadPipelineLibrary() const
|
|
{
|
|
if (IO::FileIOBase::GetInstance())
|
|
{
|
|
return Utils::LoadObjectFromFile<RHI::PipelineLibraryData>(GetPipelineLibraryPath());
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void Shader::SavePipelineLibrary() const
|
|
{
|
|
if (auto* fileIOBase = IO::FileIOBase::GetInstance())
|
|
{
|
|
RHI::ConstPtr<RHI::PipelineLibraryData> serializedData = m_pipelineStateCache->GetLibrarySerializedData(m_pipelineLibraryHandle);
|
|
if (serializedData)
|
|
{
|
|
const AZStd::string pipelineLibraryPath = GetPipelineLibraryPath();
|
|
|
|
char pipelineLibraryPathResolved[AZ_MAX_PATH_LEN] = { 0 };
|
|
fileIOBase->ResolvePath(pipelineLibraryPath.c_str(), pipelineLibraryPathResolved, AZ_MAX_PATH_LEN);
|
|
Utils::SaveObjectToFile(pipelineLibraryPathResolved, DataStream::ST_BINARY, serializedData.get());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AZ_Error("Shader", false, "FileIOBase is not initialized");
|
|
}
|
|
}
|
|
|
|
AZStd::string Shader::GetPipelineLibraryPath() const
|
|
{
|
|
const Data::InstanceId& instanceId = GetId();
|
|
Name platformName = RHI::Factory::Get().GetName();
|
|
Name shaderName = m_asset->GetName();
|
|
|
|
AZStd::string uuidString;
|
|
instanceId.m_guid.ToString<AZStd::string>(uuidString, false, false);
|
|
|
|
return AZStd::string::format("@user@/Atom/PipelineStateCache/%s/%s_%s_%d.bin", platformName.GetCStr(), shaderName.GetCStr(), uuidString.data(), instanceId.m_subId);
|
|
}
|
|
|
|
ShaderOptionGroup Shader::CreateShaderOptionGroup() const
|
|
{
|
|
return ShaderOptionGroup(m_asset->GetShaderOptionGroupLayout());
|
|
}
|
|
|
|
const ShaderVariant& Shader::GetVariant(const ShaderVariantId& shaderVariantId)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender);
|
|
Data::Asset<ShaderVariantAsset> shaderVariantAsset = m_asset->GetVariant(shaderVariantId);
|
|
if (!shaderVariantAsset || shaderVariantAsset->IsRootVariant())
|
|
{
|
|
return m_rootVariant;
|
|
}
|
|
|
|
return GetVariant(shaderVariantAsset->GetStableId());
|
|
}
|
|
|
|
const ShaderVariant& Shader::GetRootVariant()
|
|
{
|
|
return m_rootVariant;
|
|
}
|
|
|
|
ShaderVariantSearchResult Shader::FindVariantStableId(const ShaderVariantId& shaderVariantId) const
|
|
{
|
|
AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender);
|
|
ShaderVariantSearchResult variantSearchResult = m_asset->FindVariantStableId(shaderVariantId);
|
|
return variantSearchResult;
|
|
}
|
|
|
|
const ShaderVariant& Shader::GetVariant(ShaderVariantStableId shaderVariantStableId)
|
|
{
|
|
AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender);
|
|
|
|
if (!shaderVariantStableId.IsValid() || shaderVariantStableId == ShaderAsset::RootShaderVariantStableId)
|
|
{
|
|
return m_rootVariant;
|
|
}
|
|
|
|
{
|
|
AZStd::shared_lock<decltype(m_variantCacheMutex)> lock(m_variantCacheMutex);
|
|
|
|
auto findIt = m_shaderVariants.find(shaderVariantStableId);
|
|
if (findIt != m_shaderVariants.end())
|
|
{
|
|
// When rebuilding shaders we may be in a state where the ShaderAsset and root ShaderVariantAsset have been rebuilt and
|
|
// reloaded, but some (or all) shader variants haven't been built yet. Since we want to use the latest version of the
|
|
// shader code, ignore the old variants and fall back to the newer root variant instead. There's no need to report a
|
|
// warning here because m_asset->GetVariant below will report one.
|
|
if (findIt->second.GetShaderAssetBuildTimestamp() == m_asset->GetShaderAssetBuildTimestamp())
|
|
{
|
|
return findIt->second;
|
|
}
|
|
}
|
|
}
|
|
|
|
// By calling GetVariant, an asynchronous asset load request is enqueued if the variant
|
|
// is not fully ready.
|
|
Data::Asset<ShaderVariantAsset> shaderVariantAsset = m_asset->GetVariant(shaderVariantStableId);
|
|
if (!shaderVariantAsset || shaderVariantAsset == m_asset->GetRootVariant())
|
|
{
|
|
// Return the root variant when the requested variant is not ready.
|
|
return m_rootVariant;
|
|
}
|
|
|
|
AZStd::unique_lock<decltype(m_variantCacheMutex)> lock(m_variantCacheMutex);
|
|
|
|
// For performance reasons We are breaking this function into two locking steps.
|
|
// which means We must check again if the variant is already in the cache.
|
|
auto findIt = m_shaderVariants.find(shaderVariantStableId);
|
|
if (findIt != m_shaderVariants.end())
|
|
{
|
|
if (findIt->second.GetShaderAssetBuildTimestamp() == m_asset->GetShaderAssetBuildTimestamp())
|
|
{
|
|
return findIt->second;
|
|
}
|
|
else
|
|
{
|
|
// This is probably very rare, but if the variant was loaded on another thread and it's out of date
|
|
// we just return the root variant. Otherwise we could end up replacing the variant in the map below while
|
|
// it's being used for rendering.
|
|
AZ_Warning(
|
|
"Shader", false,
|
|
"Detected an uncommon state during shader reload. Returning the root variant instead of replacing the old one.");
|
|
return m_rootVariant;
|
|
}
|
|
}
|
|
|
|
ShaderVariant newVariant;
|
|
newVariant.Init(*m_asset, shaderVariantAsset);
|
|
m_shaderVariants.emplace(shaderVariantStableId, newVariant);
|
|
|
|
return m_shaderVariants.at(shaderVariantStableId);
|
|
}
|
|
|
|
RHI::PipelineStateType Shader::GetPipelineStateType() const
|
|
{
|
|
return m_pipelineStateType;
|
|
}
|
|
|
|
const RHI::PipelineState* Shader::AcquirePipelineState(const RHI::PipelineStateDescriptor& descriptor) const
|
|
{
|
|
return m_pipelineStateCache->AcquirePipelineState(m_pipelineLibraryHandle, descriptor);
|
|
}
|
|
|
|
const Data::Asset<ShaderResourceGroupAsset>& Shader::FindShaderResourceGroupAsset(const Name& shaderResourceGroupName) const
|
|
{
|
|
return m_asset->FindShaderResourceGroupAsset(shaderResourceGroupName);
|
|
}
|
|
|
|
const Data::Asset<ShaderResourceGroupAsset>& Shader::FindShaderResourceGroupAsset(uint32_t bindingSlot) const
|
|
{
|
|
return m_asset->FindShaderResourceGroupAsset(bindingSlot);
|
|
}
|
|
|
|
const Data::Asset<ShaderResourceGroupAsset>& Shader::FindFallbackShaderResourceGroupAsset() const
|
|
{
|
|
return m_asset->FindFallbackShaderResourceGroupAsset();
|
|
}
|
|
|
|
AZStd::array_view<Data::Asset<ShaderResourceGroupAsset>> Shader::GetShaderResourceGroupAssets() const
|
|
{
|
|
return m_asset->GetShaderResourceGroupAssets();
|
|
}
|
|
|
|
const Data::Asset<ShaderAsset>& Shader::GetAsset() const
|
|
{
|
|
return m_asset;
|
|
}
|
|
|
|
RHI::DrawListTag Shader::GetDrawListTag() const
|
|
{
|
|
return m_drawListTag;
|
|
}
|
|
} // namespace RPI
|
|
} // namespace AZ
|