Fix PipelineLibraries (DX12 backend). (#3768)
* Fix loading of PipelineLibraries from disk for DX12 backend. Signed-off-by: moudgils <moudgils@amazon.com> * Disabled Saving out PipelineLibraries (for DX12) if pix or Renderdoc is enabled. Addressed some feedback Signed-off-by: moudgils <moudgils@amazon.com> * Fixed an issue withe loading PipelineLibraries and added a cleaner abstraction to not save empty libraries for dx12 Signed-off-by: moudgils <moudgils@amazon.com>
This commit is contained in:
@@ -19,25 +19,24 @@ namespace AZ
|
||||
/// A handle typed to the pipeline library. Used by the PipelineStateCache to abstract access.
|
||||
using PipelineLibraryHandle = Handle<uint32_t, class PipelineLibrary>;
|
||||
|
||||
/**
|
||||
* PipelineState initialization is an expensive operation on certain platforms. If multiple pipeline states
|
||||
* are created with little variation between them, the contents are still duplicated. This class is an allocation
|
||||
* context for pipeline states, provided at PipelineState::Init, which will perform de-duplication of
|
||||
* internal pipeline state components and cache the results.
|
||||
*
|
||||
* Practically speaking, if many pipeline states are created with shared data between them (e.g. permutations
|
||||
* of the same shader), then providing a PipelineLibrary instance will reduce the memory footprint and cost
|
||||
* of compilation.
|
||||
*
|
||||
* Additionally, the PipelineLibrary is able to serialize the internal driver-contents to and from an opaque
|
||||
* data blob. This enables building up a pipeline state cache on disk, which can dramatically reduce pipeline
|
||||
* state compilation cost when run from a pre-warmed cache.
|
||||
*
|
||||
* PipelineLibrary is thread-safe, in the sense that it will take a lock during compilation. It is possible
|
||||
* to initialize pipeline states across threads using the same PipelineLibrary instance, but this will
|
||||
* result in the two calls serializing on the mutex. Instead, see PipelineStateCache which stores
|
||||
* a PipelineLibrary instance per thread to avoid this contention.
|
||||
*/
|
||||
|
||||
//! PipelineState initialization is an expensive operation on certain platforms. If multiple pipeline states
|
||||
//! are created with little variation between them, the contents are still duplicated. This class is an allocation
|
||||
//! context for pipeline states, provided at PipelineState::Init, which will perform de-duplication of
|
||||
//! internal pipeline state components and cache the results.
|
||||
//!
|
||||
//! Practically speaking, if many pipeline states are created with shared data between them (e.g. permutations
|
||||
//! of the same shader), then providing a PipelineLibrary instance will reduce the memory footprint and cost
|
||||
//! of compilation.
|
||||
//!
|
||||
//! Additionally, the PipelineLibrary is able to serialize the internal driver-contents to and from an opaque
|
||||
//! data blob. This enables building up a pipeline state cache on disk, which can dramatically reduce pipeline
|
||||
//! state compilation cost when run from a pre-warmed cache.
|
||||
//!
|
||||
//! PipelineLibrary is thread-safe, in the sense that it will take a lock during compilation. It is possible
|
||||
//! to initialize pipeline states across threads using the same PipelineLibrary instance, but this will
|
||||
//! result in the two calls serializing on the mutex. Instead, see PipelineStateCache which stores
|
||||
//! a PipelineLibrary instance per thread to avoid this contention.
|
||||
class PipelineLibrary
|
||||
: public DeviceObject
|
||||
{
|
||||
@@ -45,34 +44,31 @@ namespace AZ
|
||||
AZ_RTTI(PipelineLibrary, "{843579BE-57E4-4527-AB00-C0217885AEA9}");
|
||||
virtual ~PipelineLibrary() = default;
|
||||
|
||||
/**
|
||||
* Initializes the pipeline library from a platform-specific data payload. This data is generated
|
||||
* by calling GetSerializedData in a previous run of the application. When run for the first
|
||||
* time, the serialized data should be empty. When the application completes, the library can be
|
||||
* serialized and the contents saved to disk. Subsequent loads will experience much faster pipeline
|
||||
* state creation times (on supported platforms). On success, the library is transitioned to the
|
||||
* initialized state. On failure, the library remains uninitialized.
|
||||
* @param serializedData The initial serialized data used to initialize the library. It can be null.
|
||||
*/
|
||||
//! Initializes the pipeline library from a platform-specific data payload. This data is generated
|
||||
//! by calling GetSerializedData in a previous run of the application. When run for the first
|
||||
//! time, the serialized data should be empty. When the application completes, the library can be
|
||||
//! serialized and the contents saved to disk. Subsequent loads will experience much faster pipeline
|
||||
//! state creation times (on supported platforms). On success, the library is transitioned to the
|
||||
//! initialized state. On failure, the library remains uninitialized.
|
||||
//! @param serializedData The initial serialized data used to initialize the library. It can be null.
|
||||
ResultCode Init(Device& device, const PipelineLibraryData* serializedData);
|
||||
|
||||
/**
|
||||
* Merges the contents of other libraries into this library. This method must be called
|
||||
* on an initialized library. A common use case for this method is to construct thread-local
|
||||
* libraries and merge them into a single unified library. The serialized data can then be
|
||||
* extracted from the unified library. An error code is returned on failure and the behavior
|
||||
* is as if the method was never called.
|
||||
*/
|
||||
//! Merges the contents of other libraries into this library. This method must be called
|
||||
//! on an initialized library. A common use case for this method is to construct thread-local
|
||||
//! libraries and merge them into a single unified library. The serialized data can then be
|
||||
//! extracted from the unified library. An error code is returned on failure and the behavior
|
||||
//! is as if the method was never called.
|
||||
ResultCode MergeInto(AZStd::array_view<const PipelineLibrary*> librariesToMerge);
|
||||
|
||||
/**
|
||||
* Serializes the platform-specific data and returns it as a new PipelineLibraryData instance.
|
||||
* The data is opaque to the user and can only be used to re-initialize the library. Use
|
||||
* this method to extract serialized data prior to application shutdown, save it to disk, and
|
||||
* use it when initializing on subsequent runs.
|
||||
*/
|
||||
//! Serializes the platform-specific data and returns it as a new PipelineLibraryData instance.
|
||||
//! The data is opaque to the user and can only be used to re-initialize the library. Use
|
||||
//! this method to extract serialized data prior to application shutdown, save it to disk, and
|
||||
//! use it when initializing on subsequent runs.
|
||||
ConstPtr<PipelineLibraryData> GetSerializedData() const;
|
||||
|
||||
//! Returns whether the current library need to be merged
|
||||
virtual bool IsMergeRequired() const;
|
||||
|
||||
private:
|
||||
bool ValidateIsInitialized() const;
|
||||
|
||||
|
||||
@@ -72,5 +72,10 @@ namespace AZ
|
||||
|
||||
return GetSerializedDataInternal();
|
||||
}
|
||||
|
||||
bool PipelineLibrary::IsMergeRequired() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,16 +166,12 @@ namespace AZ
|
||||
}
|
||||
|
||||
AZStd::unique_lock<AZStd::shared_mutex> lock(m_mutex);
|
||||
|
||||
const GlobalLibraryEntry& entry = m_globalLibrarySet[handle.GetIndex()];
|
||||
|
||||
/**
|
||||
* Each thread has its own PipelineLibrary instance. To produce the final serialized data, we
|
||||
* coalesce data from each individual library by merging the thread-local ones into a single
|
||||
* global (temporary) library. The data is then extracted from this global library and returned.
|
||||
* This operation is designed to happen once at application shutdown; certainly not every frame.
|
||||
*/
|
||||
|
||||
//! Each thread has its own PipelineLibrary instance. To produce the final serialized data, we
|
||||
//! coalesce data from each individual library by merging the thread-local ones into a single
|
||||
//! global (temporary) library. The data is then extracted from this global library and returned.
|
||||
//! This operation is designed to happen once at application shutdown; certainly not every frame.
|
||||
AZStd::vector<const PipelineLibrary*> threadLibraries;
|
||||
m_threadLibrarySet.ForEach([handle, &threadLibraries](const ThreadLibrarySet& threadLibrarySet)
|
||||
{
|
||||
@@ -188,16 +184,26 @@ namespace AZ
|
||||
}
|
||||
});
|
||||
|
||||
Ptr<PipelineLibrary> pipelineLibrary = Factory::Get().CreatePipelineLibrary();
|
||||
ResultCode resultCode = pipelineLibrary->Init(*m_device, entry.m_serializedData.get());
|
||||
|
||||
if (resultCode == ResultCode::Success)
|
||||
bool doesPSODataExist = entry.m_serializedData.get();
|
||||
for (const RHI::PipelineLibrary* libraryBase : threadLibraries)
|
||||
{
|
||||
resultCode = pipelineLibrary->MergeInto(threadLibraries);
|
||||
const PipelineLibrary* library = static_cast<const PipelineLibrary*>(libraryBase);
|
||||
doesPSODataExist |= library->IsMergeRequired();
|
||||
}
|
||||
|
||||
if (doesPSODataExist)
|
||||
{
|
||||
Ptr<PipelineLibrary> pipelineLibrary = Factory::Get().CreatePipelineLibrary();
|
||||
ResultCode resultCode = pipelineLibrary->Init(*m_device, entry.m_serializedData.get());
|
||||
|
||||
if (resultCode == ResultCode::Success)
|
||||
{
|
||||
return pipelineLibrary->GetSerializedData();
|
||||
resultCode = pipelineLibrary->MergeInto(threadLibraries);
|
||||
|
||||
if (resultCode == ResultCode::Success)
|
||||
{
|
||||
return pipelineLibrary->GetSerializedData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
|
||||
#include <d3dx12.h>
|
||||
|
||||
// This define is enabled if winpixeventruntime SDK is downloaded and it's path is hooked up to Environment var ATOM_PIX_PATH.
|
||||
// Enabling this define will allow the runtime code to add PIX markers which will hel pwith pix and renderdoc gpu captures
|
||||
// This define is enabled if LY_PIX_ENABLED is enabled during configure. You can use LY_PIX_PATH to point where pix is downloaded.
|
||||
// Enabling this define will allow the runtime code to add PIX markers which will help with pix and renderdoc gpu captures
|
||||
#ifdef USE_PIX
|
||||
#include <WinPixEventRuntime/pix3.h>
|
||||
#else
|
||||
|
||||
@@ -66,15 +66,20 @@ namespace AZ
|
||||
switch (hr)
|
||||
{
|
||||
case D3D12_ERROR_DRIVER_VERSION_MISMATCH:
|
||||
case DXGI_ERROR_UNSUPPORTED:
|
||||
AZ_Warning("PipelineLibrary", false, "Failed to use pipeline library blob due to driver version mismatch. Contents will be rebuilt.");
|
||||
break;
|
||||
case DXGI_ERROR_UNSUPPORTED:
|
||||
AZ_Warning("PipelineLibrary", false, "Failed to use pipeline library blob due to the specified device interface or feature level not supported on this system. Contents will be rebuilt.");
|
||||
break;
|
||||
case D3D12_ERROR_ADAPTER_NOT_FOUND:
|
||||
AZ_Warning("PipelineLibrary", false, "Failed to use pipeline library blob due to mismatched hardware. Contents will be rebuilt.");
|
||||
break;
|
||||
case E_INVALIDARG:
|
||||
AZ_Assert(false, "Failed to use pipeline library blob due to invalid arguments. Contents will be rebuilt.");
|
||||
break;
|
||||
case DXGI_ERROR_DEVICE_REMOVED:
|
||||
AZ_Assert(false, "Failed to use pipeline library blob due to DXGI_ERROR_DEVICE_REMOVED.");
|
||||
break;
|
||||
default:
|
||||
AZ_Warning("PipelineLibrary", false, "Failed to use pipeline library blob for unknown reason. Contents will be rebuilt.");
|
||||
}
|
||||
@@ -200,23 +205,29 @@ namespace AZ
|
||||
|
||||
RHI::ResultCode PipelineLibrary::MergeIntoInternal([[maybe_unused]] AZStd::array_view<const RHI::PipelineLibrary*> pipelineLibraries)
|
||||
{
|
||||
#if defined(USE_PIX) || defined(USE_RENDERDOC)
|
||||
// StorePipeline api does not function properly if Pix or RenderDoc is enabled
|
||||
return RHI::ResultCode::Fail;
|
||||
#else
|
||||
|
||||
#if defined (AZ_DX12_USE_PIPELINE_LIBRARY)
|
||||
AZStd::lock_guard<AZStd::mutex> lock(m_mutex);
|
||||
|
||||
for (const RHI::PipelineLibrary* libraryBase : pipelineLibraries)
|
||||
{
|
||||
const PipelineLibrary* library = static_cast<const PipelineLibrary*>(libraryBase);
|
||||
|
||||
for (const auto& pipelineStateEntry : library->m_pipelineStates)
|
||||
{
|
||||
m_library->StorePipeline(pipelineStateEntry.first.c_str(), pipelineStateEntry.second.get());
|
||||
if (m_pipelineStates.find(pipelineStateEntry.first) == m_pipelineStates.end())
|
||||
{
|
||||
m_library->StorePipeline(pipelineStateEntry.first.c_str(), pipelineStateEntry.second.get());
|
||||
|
||||
m_pipelineStates.emplace(pipelineStateEntry.first, pipelineStateEntry.second);
|
||||
m_pipelineStates.emplace(pipelineStateEntry.first, pipelineStateEntry.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return RHI::ResultCode::Success;
|
||||
#endif
|
||||
}
|
||||
|
||||
RHI::ConstPtr<RHI::PipelineLibraryData> PipelineLibrary::GetSerializedDataInternal() const
|
||||
@@ -238,5 +249,10 @@ namespace AZ
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PipelineLibrary::IsMergeRequired() const
|
||||
{
|
||||
return !m_pipelineStates.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ namespace AZ
|
||||
void ShutdownInternal() override;
|
||||
RHI::ResultCode MergeIntoInternal(AZStd::array_view<const RHI::PipelineLibrary*> libraries) override;
|
||||
RHI::ConstPtr<RHI::PipelineLibraryData> GetSerializedDataInternal() const override;
|
||||
bool IsMergeRequired() const;
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
ID3D12DeviceX* m_dx12Device = nullptr;
|
||||
|
||||
@@ -210,7 +210,7 @@ namespace AZ
|
||||
|
||||
// ImageHeight must be bigger than or equal to the Image's row count. Images with a RowCount that is less than the ImageHeight indicates a block compression.
|
||||
// Images with a RowCount which is higher than the ImageHeight indicates a planar image, which is not supported for streaming images.
|
||||
AZ_Error("StreamingImage", subresourceLayout.m_size.m_height < subresourceLayout.m_rowCount, "AsyncUploadQueue::QueueUpload expects ImageHeight '%d' to be bigger than or equal to the image's RowCount '%d'.", subresourceLayout.m_size.m_height, subresourceLayout.m_rowCount);
|
||||
AZ_Error("StreamingImage", subresourceLayout.m_size.m_height >= subresourceLayout.m_rowCount, "AsyncUploadQueue::QueueUpload expects ImageHeight '%d' to be bigger than or equal to the image's RowCount '%d'.", subresourceLayout.m_size.m_height, subresourceLayout.m_rowCount);
|
||||
|
||||
// The final staging size for each CopyTextureRegion command
|
||||
uint32_t stagingSize = stagingSlicePitch;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
#include <Atom/RHI/PipelineLibrary.h>
|
||||
|
||||
#include <AtomCore/Instance/InstanceData.h>
|
||||
|
||||
#include <AzCore/IO/SystemFile.h>
|
||||
#include <AzCore/Memory/SystemAllocator.h>
|
||||
|
||||
namespace AZ
|
||||
@@ -175,9 +175,6 @@ namespace AZ
|
||||
// And of course we don't need to handle OnShaderReinitialized because this *is* this Shader.
|
||||
///////////////////////////////////////////////////////////////////
|
||||
|
||||
//! Returns the path to the pipeline library cache file.
|
||||
AZStd::string GetPipelineLibraryPath() const;
|
||||
|
||||
//! A strong reference to the shader asset.
|
||||
Data::Asset<ShaderAsset> m_asset;
|
||||
|
||||
@@ -206,6 +203,9 @@ namespace AZ
|
||||
|
||||
//! DrawListTag associated with this shader.
|
||||
RHI::DrawListTag m_drawListTag;
|
||||
|
||||
//! PipelineLibrary file name
|
||||
char m_pipelineLibraryPath[AZ_MAX_PATH_LEN] = { 0 };
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,18 +7,14 @@
|
||||
*/
|
||||
#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 <Atom/RHI/PipelineStateCache.h>
|
||||
#include <Atom/RHI/RHISystemInterface.h>
|
||||
#include <AtomCore/Instance/InstanceDatabase.h>
|
||||
|
||||
#include <AzCore/Interface/Interface.h>
|
||||
#include <Atom/RPI.Public/Shader/ShaderSystemInterface.h>
|
||||
#include <Atom/RPI.Public/Shader/ShaderReloadDebugTracker.h>
|
||||
#include <Atom/RPI.Public/Shader/ShaderSystemInterface.h>
|
||||
#include <AzCore/Interface/Interface.h>
|
||||
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
@@ -75,6 +71,29 @@ namespace AZ
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
static bool GetPipelineLibraryPath(char* pipelineLibraryPath, size_t pipelineLibraryPathLength, const ShaderAsset& shaderAsset)
|
||||
{
|
||||
if (auto* fileIOBase = IO::FileIOBase::GetInstance())
|
||||
{
|
||||
const Data::AssetId& assetId = shaderAsset.GetId();
|
||||
|
||||
Name platformName = RHI::Factory::Get().GetName();
|
||||
Name shaderName = shaderAsset.GetName();
|
||||
|
||||
AZStd::string uuidString;
|
||||
assetId.m_guid.ToString<AZStd::string>(uuidString, false, false);
|
||||
|
||||
char pipelineLibraryPathTemp[AZ_MAX_PATH_LEN];
|
||||
azsnprintf(
|
||||
pipelineLibraryPathTemp, AZ_MAX_PATH_LEN, "@user@/Atom/PipelineStateCache/%s/%s_%s_%d.bin", platformName.GetCStr(),
|
||||
shaderName.GetCStr(), uuidString.data(), assetId.m_subId);
|
||||
|
||||
fileIOBase->ResolvePath(pipelineLibraryPathTemp, pipelineLibraryPath, pipelineLibraryPathLength);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
RHI::ResultCode Shader::Init(ShaderAsset& shaderAsset)
|
||||
{
|
||||
Data::AssetBus::Handler::BusDisconnect();
|
||||
@@ -87,6 +106,8 @@ namespace AZ
|
||||
m_asset = { &shaderAsset, AZ::Data::AssetLoadBehavior::PreLoad };
|
||||
m_pipelineStateType = shaderAsset.GetPipelineStateType();
|
||||
|
||||
GetPipelineLibraryPath(m_pipelineLibraryPath, AZ_MAX_PATH_LEN, *m_asset);
|
||||
|
||||
{
|
||||
AZStd::unique_lock<decltype(m_variantCacheMutex)> lock(m_variantCacheMutex);
|
||||
m_shaderVariants.clear();
|
||||
@@ -123,7 +144,7 @@ namespace AZ
|
||||
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());
|
||||
@@ -249,49 +270,28 @@ namespace AZ
|
||||
}
|
||||
}
|
||||
///////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
ConstPtr<RHI::PipelineLibraryData> Shader::LoadPipelineLibrary() const
|
||||
{
|
||||
if (IO::FileIOBase::GetInstance())
|
||||
{
|
||||
if (m_pipelineLibraryPath[0] != 0)
|
||||
{
|
||||
return Utils::LoadObjectFromFile<RHI::PipelineLibraryData>(GetPipelineLibraryPath());
|
||||
return Utils::LoadObjectFromFile<RHI::PipelineLibraryData>(m_pipelineLibraryPath);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Shader::SavePipelineLibrary() const
|
||||
{
|
||||
if (auto* fileIOBase = IO::FileIOBase::GetInstance())
|
||||
if (m_pipelineLibraryPath[0] != 0)
|
||||
{
|
||||
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());
|
||||
Utils::SaveObjectToFile<RHI::PipelineLibraryData>(m_pipelineLibraryPath, 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());
|
||||
|
||||
Reference in New Issue
Block a user