Fixed race conditions that could prevent successful hot-reload of shaders.

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.
This commit is contained in:
Chris Santora
2021-06-14 20:36:12 -07:00
parent 406792606b
commit 982406d4d5
12 changed files with 179 additions and 50 deletions
@@ -18,6 +18,7 @@
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/Component/TickBus.h>
namespace AZ
{
@@ -47,6 +48,7 @@ namespace AZ
{
MaterialReloadNotificationBus::Handler::BusDisconnect();
Data::AssetBus::Handler::BusDisconnect();
AssetInitBus::Handler::BusDisconnect();
}
const Data::Asset<MaterialTypeAsset>& MaterialAsset::GetMaterialTypeAsset() const
@@ -97,6 +99,8 @@ namespace AZ
{
if (!m_materialTypeAsset.Get())
{
AssetInitBus::Handler::BusDisconnect();
// Any MaterialAsset with invalid MaterialTypeAsset is not a successfully-loaded asset.
return false;
}
@@ -104,6 +108,8 @@ namespace AZ
{
Data::AssetBus::Handler::BusConnect(m_materialTypeAsset.GetId());
MaterialReloadNotificationBus::Handler::BusConnect(m_materialTypeAsset.GetId());
AssetInitBus::Handler::BusDisconnect();
return true;
}
@@ -116,11 +122,9 @@ namespace AZ
// Ultimately it's the Material that cares about these changes, so we just forward any signal we get.
MaterialReloadNotificationBus::Event(GetId(), &MaterialReloadNotifications::OnMaterialAssetReinitialized, Data::Asset<MaterialAsset>{this, AZ::Data::AssetLoadBehavior::PreLoad});
}
void MaterialAsset::OnAssetReloaded(Data::Asset<Data::AssetData> asset)
void MaterialAsset::ReinitializeMaterialTypeAsset(Data::Asset<Data::AssetData> asset)
{
ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->MaterialAsset::OnAssetReloaded %s", this, asset.GetHint().c_str());
Data::Asset<MaterialTypeAsset> newMaterialTypeAsset = { asset.GetAs<MaterialTypeAsset>(), AZ::Data::AssetLoadBehavior::PreLoad };
if (newMaterialTypeAsset)
@@ -135,16 +139,31 @@ namespace AZ
}
}
void MaterialAsset::OnAssetReloaded(Data::Asset<Data::AssetData> asset)
{
ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->MaterialAsset::OnAssetReloaded %s", this, asset.GetHint().c_str());
ReinitializeMaterialTypeAsset(asset);
}
void MaterialAsset::OnAssetReady(Data::Asset<Data::AssetData> asset)
{
// Regarding why we listen to both OnAssetReloaded and OnAssetReady, see explanation in ShaderAsset::OnAssetReady.
ShaderReloadDebugTracker::ScopedSection reloadSection("{%p}->MaterialAsset::OnAssetReady %s", this, asset.GetHint().c_str());
ReinitializeMaterialTypeAsset(asset);
}
Data::AssetHandler::LoadResult MaterialAssetHandler::LoadAssetData(
const AZ::Data::Asset<AZ::Data::AssetData>& asset,
AZStd::shared_ptr<AZ::Data::AssetDataStream> stream,
const AZ::Data::AssetFilterCB& assetLoadFilterCB)
{
Data::AssetHandler::LoadResult baseResult = Base::LoadAssetData(asset, stream, assetLoadFilterCB);
bool postLoadResult = asset.GetAs<MaterialAsset>()->PostLoadInit();
return ((baseResult == Data::AssetHandler::LoadResult::LoadComplete) && postLoadResult) ?
Data::AssetHandler::LoadResult::LoadComplete :
Data::AssetHandler::LoadResult::Error;
if (Base::LoadAssetData(asset, stream, assetLoadFilterCB) == Data::AssetHandler::LoadResult::LoadComplete)
{
asset.GetAs<MaterialAsset>()->AssetInitBus::Handler::BusConnect();
return Data::AssetHandler::LoadResult::LoadComplete;
}
return Data::AssetHandler::LoadResult::Error;
}
} // namespace RPI