Browse Source

Fix the first-time Editor crash bugs (#16197)

* Switch streaming texture loads to use helper that ensures assets are compiled first.

Signed-off-by: Mike Balfour <[email protected]>

* Initialize variable in case bus listener doesn't exist.

Signed-off-by: Mike Balfour <[email protected]>

* Slightly reduce spam from unregistered assets. Only warn on asset creation, not on every change.

Signed-off-by: Mike Balfour <[email protected]>

* Improve SC warning message so that it's easier to know which node is broken.

Signed-off-by: Mike Balfour <[email protected]>

* Move critical asset signal to after the asset catalog load, just in case anything tries to use the catalog in response to the signal.

Signed-off-by: Mike Balfour <[email protected]>

* Consolidate into LoadCriticalAsset<> for compile & load code paths.

Signed-off-by: Mike Balfour <[email protected]>

* Mark more assets as critical loads on first-time editor startup.

Signed-off-by: Mike Balfour <[email protected]>

* Fix error caused from accessing the mesh notification bus on multiple threads.

Signed-off-by: Mike Balfour <[email protected]>

* Better error-handling for 0-length data streams.
This can happen when loading an asset that got force-added to the asset catalog even when the file doesn't exist on disk yet.

Signed-off-by: Mike Balfour <[email protected]>

* Fix typo and bug where searches by asset id would always come back as compiled for invalid asset ids.

Signed-off-by: Mike Balfour <[email protected]>

* Changed RequestAssetStatus messages to all consistently use AZ_Info instead of a combination of AZ_Trace / AZ_TracePrintf.

Signed-off-by: Mike Balfour <[email protected]>

* Add info message to let us know when fallback assets are used.

Signed-off-by: Mike Balfour <[email protected]>

* Guard against invalid load attempts.

Signed-off-by: Mike Balfour <[email protected]>

* Refactor the compile code so that it's not a part of the templated functions, and change async loaded to support compiling as needed.

Signed-off-by: Mike Balfour <[email protected]>

* Add lookup support for the "common" platform and currently-processing jobs.

Signed-off-by: Mike Balfour <[email protected]>

* Prevent crashes when a mesh is missing one or more required streams.

Signed-off-by: Mike Balfour <[email protected]>

* First pass at default fallback model asset support.

Signed-off-by: Mike Balfour <[email protected]>

* Improved the data on the unit cube.

Signed-off-by: Mike Balfour <[email protected]>

* Revert change to RCJobListModel, not proven to be any more or less correct than before.

Signed-off-by: Mike Balfour <[email protected]>

* Remove extra linefeed.

Signed-off-by: Mike Balfour <[email protected]>

* Use a constant with a comment instead of a hard-coded number.

Signed-off-by: Mike Balfour <[email protected]>

* Fixed up cut-n-paste errors.

Signed-off-by: Mike Balfour <[email protected]>

* Add job dependency on DefaultVertexBufferPool to ensure correct build ordering.

Signed-off-by: Mike Balfour <[email protected]>

* Fix up mesh reloads to handle missing assets correctly.

Signed-off-by: Mike Balfour <[email protected]>

* Avoid using the CompileAssetSync since model compiles aren't guaranteed to succeed anyways and will just cause a long stall on startup.

Signed-off-by: Mike Balfour <[email protected]>

* Added "breadcrumb" comments to make it clear why asset reloading is disabled.

Signed-off-by: Mike Balfour <[email protected]>

* Update Code/Tools/AssetProcessor/native/resourcecompiler/rccontroller.cpp

Co-authored-by: lumberyard-employee-dm <[email protected]>
Signed-off-by: Mike Balfour <[email protected]>

* Update Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAssetHelpers.cpp

Co-authored-by: lumberyard-employee-dm <[email protected]>
Signed-off-by: Mike Balfour <[email protected]>

* Update Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/Mesh/MeshComponentBus.h

agh, thanks!

Co-authored-by: lumberyard-employee-dm <[email protected]>
Signed-off-by: Mike Balfour <[email protected]>

* PR feedback

Signed-off-by: Mike Balfour <[email protected]>

* PR + UX feedback.
Changed missing model to X instead of cube to make it look even less valid.

Signed-off-by: Mike Balfour <[email protected]>

* PR feedback

Signed-off-by: Mike Balfour <[email protected]>

* Fix non-unity build error

Signed-off-by: Mike Balfour <[email protected]>

---------

Signed-off-by: Mike Balfour <[email protected]>
Co-authored-by: lumberyard-employee-dm <[email protected]>
Mike Balfour 2 years ago
parent
commit
05ec49bc60
36 changed files with 790 additions and 209 deletions
  1. 3 1
      Code/Editor/CryEdit.cpp
  2. 2 2
      Code/Framework/AzFramework/AzFramework/Asset/AssetCatalog.cpp
  3. 1 1
      Code/Framework/AzToolsFramework/AzToolsFramework/AssetDatabase/AssetDatabaseConnection.cpp
  4. 4 4
      Code/Tools/AssetProcessor/native/AssetManager/AssetRequestHandler.cpp
  5. 3 6
      Code/Tools/AssetProcessor/native/AssetManager/assetProcessorManager.cpp
  6. 12 2
      Code/Tools/AssetProcessor/native/resourcecompiler/rccontroller.cpp
  7. 2 10
      Gems/Atom/Bootstrap/Code/Source/BootstrapSystemComponent.cpp
  8. 1 0
      Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessor.h
  9. 3 24
      Gems/Atom/Feature/Common/Code/Source/ImageBasedLights/ImageBasedLightFeatureProcessor.cpp
  10. 25 15
      Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp
  11. 2 2
      Gems/Atom/Feature/Common/Code/Source/OcclusionCullingPlane/OcclusionCullingPlane.cpp
  12. 2 32
      Gems/Atom/Feature/Common/Code/Source/ScreenSpace/DeferredFogSettings.cpp
  13. 20 15
      Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Asset/AssetUtils.h
  14. 17 1
      Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Asset/AssetUtils.inl
  15. 5 0
      Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Buffer/BufferAsset.h
  16. 37 0
      Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelAsset.h
  17. 59 0
      Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelAssetHelpers.h
  18. 5 0
      Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelLodAsset.h
  19. 1 0
      Gems/Atom/RPI/Code/Source/RPI.Builders/BuilderModule.cpp
  20. 57 1
      Gems/Atom/RPI/Code/Source/RPI.Builders/Model/ModelAssetBuilderComponent.cpp
  21. 28 0
      Gems/Atom/RPI/Code/Source/RPI.Builders/Model/ModelAssetBuilderComponent.h
  22. 1 28
      Gems/Atom/RPI/Code/Source/RPI.Public/RPIUtils.cpp
  23. 30 3
      Gems/Atom/RPI/Code/Source/RPI.Reflect/Asset/AssetUtils.cpp
  24. 12 5
      Gems/Atom/RPI/Code/Source/RPI.Reflect/Image/StreamingImageAssetHandler.cpp
  25. 112 0
      Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAsset.cpp
  26. 294 0
      Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAssetHelpers.cpp
  27. 2 0
      Gems/Atom/RPI/Code/atom_rpi_reflect_files.cmake
  28. 3 4
      Gems/Atom/Tools/AtomToolsFramework/Code/Source/Application/AtomToolsApplication.cpp
  29. 4 0
      Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/Mesh/MeshComponentBus.h
  30. 1 1
      Gems/AtomLyIntegration/EditorModeFeedback/Code/Source/EditorModeFeedbackFeatureProcessor.cpp
  31. 1 35
      Gems/AtomTressFX/Code/Rendering/HairCommon.cpp
  32. 0 4
      Gems/AtomTressFX/Code/Rendering/HairCommon.h
  33. 3 2
      Gems/AtomTressFX/Code/Rendering/HairRenderObject.cpp
  34. 26 7
      Gems/DiffuseProbeGrid/Code/Source/Render/DiffuseProbeGridFeatureProcessor.cpp
  35. 9 3
      Gems/DiffuseProbeGrid/Code/Source/Render/DiffuseProbeGridPreparePass.cpp
  36. 3 1
      Gems/ScriptCanvas/Code/Include/ScriptCanvas/Libraries/Core/MethodOverloaded.cpp

+ 3 - 1
Code/Editor/CryEdit.cpp

@@ -1312,7 +1312,6 @@ void CCryEditApp::CompileCriticalAssets() const
     // Also reload the "assetcatalog.xml" if it exists
     if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr)
     {
-        AZ::ComponentApplicationLifecycle::SignalEvent(*settingsRegistry, "CriticalAssetsCompiled", R"({})");
         // Reload the assetcatalog.xml at this point again
         // Start Monitoring Asset changes over the network and load the AssetCatalog
         auto LoadCatalog = [settingsRegistry](AZ::Data::AssetCatalogRequests* assetCatalogRequests)
@@ -1325,6 +1324,9 @@ void CCryEditApp::CompileCriticalAssets() const
             }
         };
         AZ::Data::AssetCatalogRequestBus::Broadcast(AZStd::move(LoadCatalog));
+
+        // Only signal the event *after* the asset catalog has been loaded.
+        AZ::ComponentApplicationLifecycle::SignalEvent(*settingsRegistry, "CriticalAssetsCompiled", R"({})");
     }
 
     CCryEditApp::OutputStartupMessage(QString("Asset Processor is now ready."));

+ 2 - 2
Code/Framework/AzFramework/AzFramework/Asset/AssetCatalog.cpp

@@ -841,9 +841,9 @@ namespace AzFramework
                     }
 
 #if defined(AZ_ENABLE_TRACING)
-                    if (message.m_assetType == AZ::Data::s_invalidAssetType)
+                    if (isNewAsset && (message.m_assetType == AZ::Data::s_invalidAssetType))
                     {
-                        AZ_TracePrintf(
+                        AZ_Info(
                             "AssetCatalog",
                             "Received `AssetNotificationMessage` network message with no AssetType.  Asset \"%s\" will be registered without a type.\n",
                             relativePath.c_str());

+ 1 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/AssetDatabase/AssetDatabaseConnection.cpp

@@ -598,7 +598,7 @@ namespace AzToolsFramework
                 "ON ScanFolders.ScanFolderID = Sources.ScanFolderPK  WHERE "
                 "Products.SubID = :productsubid AND "
                 "(Sources.SourceGuid = :sourceguid OR "
-                "Products.LegacyGuid = :soruceguid) AND "
+                "Products.LegacyGuid = :sourceguid) AND "
                 "Jobs.Platform = :platform;";
 
             static const auto s_queryCombinedBySourceguidProductsubidPlatform = MakeSqlQuery(QUERY_COMBINED_BY_SOURCEGUID_PRODUCTSUBID_PLATFORM, QUERY_COMBINED_BY_SOURCEGUID_PRODUCTSUBID_PLATFORM_STATEMENT, LOG_NAME,

+ 4 - 4
Code/Tools/AssetProcessor/native/AssetManager/AssetRequestHandler.cpp

@@ -403,12 +403,12 @@ void AssetRequestHandler::ProcessAssetRequest(MessageData<RequestAssetStatus> me
 {
     if ((messageData.m_message->m_searchTerm.empty())&&(!messageData.m_message->m_assetId.IsValid()))
     {
-        AZ_TracePrintf(AssetProcessor::DebugChannel, "Failed to decode incoming RequestAssetStatus - both path and uuid is empty\n");
+        AZ_Info(AssetProcessor::DebugChannel, "Failed to decode incoming RequestAssetStatus - both path and uuid is empty\n");
         SendAssetStatus(messageData.m_key, RequestAssetStatus::MessageType, AssetStatus_Unknown);
         return;
     }
     AssetRequestLine newLine(messageData.m_platform, QString::fromUtf8(messageData.m_message->m_searchTerm.c_str()), messageData.m_message->m_assetId, messageData.m_message->m_isStatusRequest, messageData.m_message->m_searchType);
-    AZ_TracePrintf(AssetProcessor::DebugChannel, "GetAssetStatus/CompileAssetSync: %s.\n", newLine.GetDisplayString().toUtf8().constData());
+    AZ_Info(AssetProcessor::DebugChannel, "GetAssetStatus/CompileAssetSync: %s.\n", newLine.GetDisplayString().toUtf8().constData());
 
     QString assetPath = QString::fromUtf8(messageData.m_message->m_searchTerm.c_str());  // utf8-decode just once here, reuse below
     m_pendingAssetRequests.insert(messageData.m_key, newLine);
@@ -478,11 +478,11 @@ void AssetRequestHandler::OnRequestAssetExistsResponse(NetworkRequestID groupID,
 
     if (located == m_pendingAssetRequests.end())
     {
-        AZ_Trace(AssetProcessor::DebugChannel, "OnRequestAssetExistsResponse: No such compile group found, ignoring.\n");
+        AZ_Info(AssetProcessor::DebugChannel, "OnRequestAssetExistsResponse: No such compile group found, ignoring.\n");
         return;
     }
 
-    AZ_Trace(AssetProcessor::DebugChannel, "GetAssetStatus / CompileAssetSync: Asset %s is %s.\n",
+    AZ_Info(AssetProcessor::DebugChannel, "GetAssetStatus / CompileAssetSync: Asset %s is %s.\n",
         located.value().GetDisplayString().toUtf8().constData(),
         exists ? "compiled already" : "missing" );
 

+ 3 - 6
Code/Tools/AssetProcessor/native/AssetManager/assetProcessorManager.cpp

@@ -616,12 +616,9 @@ namespace AssetProcessor
                 platform.toUtf8().constData(),
                 AzToolsFramework::AssetSystem::JobStatus::Any);
 
-            if (foundOne)
-            {
-                // the source exists.
-                Q_EMIT SendAssetExistsResponse(groupID, true);
-                return;
-            }
+            // respond with whether or not the entry was found in the DB.
+            Q_EMIT SendAssetExistsResponse(groupID, foundOne);
+            return;
         }
 
         // otherwise, we have to guess

+ 12 - 2
Code/Tools/AssetProcessor/native/resourcecompiler/rccontroller.cpp

@@ -383,12 +383,22 @@ namespace AssetProcessor
         if (results.isEmpty())
         {
             // nothing found
-            Q_EMIT CompileGroupCreated(groupID, AzFramework::AssetSystem::AssetStatus_Unknown);
+            AZ_Info(
+                AssetProcessor::DebugChannel,
+                "OnRequestCompileGroup:  %s - %s requested, but no matching source assets found.\n",
+                searchTerm.toUtf8().constData(),
+                assetId.ToString<AZStd::string>().c_str());
 
-            AZ_TracePrintf(AssetProcessor::DebugChannel, "OnRequestCompileGroup:  %s - %s requested, but no matching source assets found.\n", searchTerm.toUtf8().constData(), assetId.ToString<AZStd::string>().c_str());
+            Q_EMIT CompileGroupCreated(groupID, AzFramework::AssetSystem::AssetStatus_Unknown);
         }
         else
         {
+            AZ_Info(
+                AssetProcessor::DebugChannel,
+                "GetAssetStatus: OnRequestCompileGroup:  %s - %s requested and queued, found %d results.\n",
+                searchTerm.toUtf8().constData(),
+                assetId.ToFixedString().c_str(), results.size());
+
             // it is not necessary to denote the search terms or list of results here because
             // PerformHeursticSearch already prints out the results.
             m_RCQueueSortModel.OnEscalateJobs(escalationList);

+ 2 - 10
Gems/Atom/Bootstrap/Code/Source/BootstrapSystemComponent.cpp

@@ -487,17 +487,9 @@ namespace AZ
                                                     AZStd::string_view pipelineName, AZ::RPI::ViewType viewType, AZ::RHI::MultisampleState& multisampleState)
             {
                 // Create a render pipeline from the specified asset for the window context and add the pipeline to the scene.
-                // When running with no Asset Processor (for example in release), CompileAssetSync will return AssetStatus_Unknown.
-                AzFramework::AssetSystem::AssetStatus status = AzFramework::AssetSystem::AssetStatus_Unknown;
-                AzFramework::AssetSystemRequestBus::BroadcastResult(
-                    status, &AzFramework::AssetSystemRequestBus::Events::CompileAssetSync, pipelineName.data());
-                AZ_Assert(
-                    status == AzFramework::AssetSystem::AssetStatus_Compiled || status == AzFramework::AssetSystem::AssetStatus_Unknown,
-                    "Could not compile the default render pipeline at '%s'",
-                    pipelineName.data());
-
+                // When running with an Asset Processor, this will attempt to compile the asset before loading it.
                 Data::Asset<RPI::AnyAsset> pipelineAsset =
-                    RPI::AssetUtils::LoadAssetByProductPath<RPI::AnyAsset>(pipelineName.data(), RPI::AssetUtils::TraceLevel::Error);
+                    RPI::AssetUtils::LoadCriticalAsset<RPI::AnyAsset>(pipelineName.data(), RPI::AssetUtils::TraceLevel::Error);
                 if (pipelineAsset)
                 {
                     RPI::RenderPipelineDescriptor renderPipelineDescriptor =

+ 1 - 0
Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessor.h

@@ -58,6 +58,7 @@ namespace AZ
                 void OnAssetError(Data::Asset<Data::AssetData> asset) override;
 
                 // AssetCatalogEventBus::Handler overrides...
+                void OnCatalogAssetRemoved(const AZ::Data::AssetId& assetId, const AZ::Data::AssetInfo& assetInfo) override;
                 void OnCatalogAssetChanged(const AZ::Data::AssetId& assetId) override;
                 void OnCatalogAssetAdded(const AZ::Data::AssetId& assetId) override;
 

+ 3 - 24
Gems/Atom/Feature/Common/Code/Source/ImageBasedLights/ImageBasedLightFeatureProcessor.cpp

@@ -9,6 +9,7 @@
 #include <Atom/Feature/ImageBasedLights/ImageBasedLightFeatureProcessor.h>
 
 #include <Atom/RPI.Public/Scene.h>
+#include <Atom/RPI.Public/RPIUtils.h>
 #include <Atom/RPI.Public/Image/StreamingImage.h>
 
 namespace AZ
@@ -87,32 +88,10 @@ namespace AZ
             const constexpr char* DefaultSpecularCubeMapPath = "textures/default/default_iblglobalcm_iblspecular.dds.streamingimage";
             const constexpr char* DefaultDiffuseCubeMapPath = "textures/default/default_iblglobalcm_ibldiffuse.dds.streamingimage";
 
-            Data::AssetId specularAssetId;
-            Data::AssetCatalogRequestBus::BroadcastResult(
-                specularAssetId,
-                &Data::AssetCatalogRequestBus::Events::GetAssetIdByPath,
-                DefaultSpecularCubeMapPath,
-                azrtti_typeid<AZ::RPI::StreamingImageAsset>(),
-                false);
-
-            Data::AssetId diffuseAssetId;
-            Data::AssetCatalogRequestBus::BroadcastResult(
-                diffuseAssetId,
-                &Data::AssetCatalogRequestBus::Events::GetAssetIdByPath,
-                DefaultDiffuseCubeMapPath,
-                azrtti_typeid<AZ::RPI::StreamingImageAsset>(),
-                false);
-
-            auto specularAsset = Data::AssetManager::Instance().GetAsset<RPI::StreamingImageAsset>(specularAssetId, AZ::Data::AssetLoadBehavior::PreLoad);
-            auto diffuseAsset = Data::AssetManager::Instance().GetAsset<RPI::StreamingImageAsset>(diffuseAssetId, AZ::Data::AssetLoadBehavior::PreLoad);
-
-            specularAsset.BlockUntilLoadComplete();
-            diffuseAsset.BlockUntilLoadComplete();
-
-            m_defaultSpecularImage = RPI::StreamingImage::FindOrCreate(specularAsset);
+            m_defaultSpecularImage = RPI::LoadStreamingTexture(DefaultSpecularCubeMapPath);
             AZ_Assert(m_defaultSpecularImage, "Failed to load default specular cubemap");
 
-            m_defaultDiffuseImage = RPI::StreamingImage::FindOrCreate(diffuseAsset);
+            m_defaultDiffuseImage = RPI::LoadStreamingTexture(DefaultDiffuseCubeMapPath);
             AZ_Assert(m_defaultDiffuseImage, "Failed to load default diffuse cubemap");
         }
 

+ 25 - 15
Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp

@@ -825,6 +825,9 @@ namespace AZ
         {
             Data::Asset<RPI::ModelAsset> modelAsset = asset;
 
+            // Update our model asset reference to contain the latest loaded version.
+            m_modelAsset = asset;
+
             // Assign the fully loaded asset back to the mesh handle to not only hold asset id, but the actual data as well.
             m_parent->m_originalModelAsset = asset;
 
@@ -886,29 +889,27 @@ namespace AZ
             AzFramework::AssetSystemRequestBus::Broadcast(
                 &AzFramework::AssetSystem::AssetSystemRequests::EscalateAssetByUuid, m_modelAsset.GetId().m_guid);
         }
+
+        void ModelDataInstance::MeshLoader::OnCatalogAssetRemoved(
+            const AZ::Data::AssetId& assetId, [[maybe_unused]] const AZ::Data::AssetInfo& assetInfo)
+        {
+            OnCatalogAssetChanged(assetId);
+        }
         
-        void ModelDataInstance::MeshLoader::OnCatalogAssetChanged(const AZ::Data::AssetId& assetId)
+        void ModelDataInstance::MeshLoader::OnCatalogAssetAdded(const AZ::Data::AssetId& assetId)
         {
-            if (assetId == m_modelAsset.GetId())
-            {
-                Data::Asset<RPI::ModelAsset> modelAssetReference = m_modelAsset;
-
-                // If the asset was modified, reload it
-                AZ::SystemTickBus::QueueFunction(
-                    [=]() mutable
-                    {
-                        ModelReloaderSystemInterface::Get()->ReloadModel(modelAssetReference, m_modelReloadedEventHandler);
-                    });
-            }
+            // If the asset didn't exist in the catalog when it first attempted to load, we need to try loading it again
+            OnCatalogAssetChanged(assetId);
         }
 
-        void ModelDataInstance::MeshLoader::OnCatalogAssetAdded(const AZ::Data::AssetId& assetId)
+        void ModelDataInstance::MeshLoader::OnCatalogAssetChanged(const AZ::Data::AssetId& assetId)
         {
             if (assetId == m_modelAsset.GetId())
             {
                 Data::Asset<RPI::ModelAsset> modelAssetReference = m_modelAsset;
-                
-                // If the asset didn't exist in the catalog when it first attempted to load, we need to try loading it again
+
+                // If the asset was modified, reload it. This will also cause a model to change back to the default missing
+                // asset if it was removed, and it will replace the default missing asset with the real asset if it was added.
                 AZ::SystemTickBus::QueueFunction(
                     [=]() mutable
                     {
@@ -1151,6 +1152,15 @@ namespace AZ
                     material->GetAsset()->GetMaterialTypeAsset()->GetUvNameMap());
                 AZ_Assert(result, "Failed to retrieve mesh stream buffer views");
 
+                // The code below expects streams for positions, normals, tangents, bitangents, and uvs.
+                constexpr size_t NumExpectedStreams = 5;
+                if (streamBufferViews.size() < NumExpectedStreams)
+                {
+                    AZ_Warning("MeshFeatureProcessor", false, "Model is missing one or more expected streams "
+                        "(positions, normals, tangents, bitangents, uvs), skipping the raytracing data generation.");
+                    continue;
+                }
+
                 // note that the element count is the size of the entire buffer, even though this mesh may only
                 // occupy a portion of the vertex buffer.  This is necessary since we are accessing it using
                 // a ByteAddressBuffer in the raytracing shaders and passing the byte offset to the shader in a constant buffer.

+ 2 - 2
Gems/Atom/Feature/Common/Code/Source/OcclusionCullingPlane/OcclusionCullingPlane.cpp

@@ -29,7 +29,7 @@ namespace AZ
             m_meshFeatureProcessor = scene->GetFeatureProcessor<Render::MeshFeatureProcessorInterface>();
 
             // load visualization plane model and material
-            m_visualizationModelAsset = AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::ModelAsset>(
+            m_visualizationModelAsset = AZ::RPI::AssetUtils::LoadCriticalAsset<AZ::RPI::ModelAsset>(
                 "Models/OcclusionCullingPlane.azmodel",
                 AZ::RPI::AssetUtils::TraceLevel::Assert);
 
@@ -54,7 +54,7 @@ namespace AZ
             }
 
             RPI::AssetUtils::TraceLevel traceLevel = AZ::RPI::AssetUtils::TraceLevel::Assert;
-            m_visualizationMaterialAsset = AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::MaterialAsset>(materialAssetPath.c_str(), traceLevel);
+            m_visualizationMaterialAsset = AZ::RPI::AssetUtils::LoadCriticalAsset<AZ::RPI::MaterialAsset>(materialAssetPath.c_str(), traceLevel);
             m_visualizationMaterialAsset.QueueLoad();
             Data::AssetBus::MultiHandler::BusConnect(m_visualizationMaterialAsset.GetId());
         }

+ 2 - 32
Gems/Atom/Feature/Common/Code/Source/ScreenSpace/DeferredFogSettings.cpp

@@ -11,6 +11,7 @@
 
 #include <Atom/RPI.Public/RenderPipeline.h>
 #include <Atom/RPI.Public/Shader/ShaderResourceGroup.h>
+#include <Atom/RPI.Public/RPIUtils.h>
 
 #include <ScreenSpace/DeferredFogSettings.h>
 #include <ScreenSpace/DeferredFogPass.h>
@@ -25,41 +26,10 @@ namespace AZ
         DeferredFogSettings::DeferredFogSettings()
             : PostProcessBase(nullptr) {}
 
-        // [GXF TODO][ATOM-13418]
-        // Move this method to be a global utility function - also implement similar method using AssetId.
         AZ::Data::Instance<AZ::RPI::StreamingImage> DeferredFogSettings::LoadStreamingImage(
             const char* textureFilePath, [[maybe_unused]] const char* sampleName)
         {
-            using namespace AZ;
-
-            Data::AssetId streamingImageAssetId;
-            Data::AssetCatalogRequestBus::BroadcastResult(
-                streamingImageAssetId, &Data::AssetCatalogRequestBus::Events::GetAssetIdByPath,
-                textureFilePath, azrtti_typeid<RPI::StreamingImageAsset>(), false);
-            if (!streamingImageAssetId.IsValid())
-            {
-                AZ_Error(sampleName, false, "Failed to get streaming image asset id with path %s", textureFilePath);
-                return nullptr;
-            }
-
-            auto streamingImageAsset = Data::AssetManager::Instance().GetAsset<RPI::StreamingImageAsset>(
-                streamingImageAssetId, AZ::Data::AssetLoadBehavior::PreLoad);
-            streamingImageAsset.BlockUntilLoadComplete();
-
-            if (!streamingImageAsset.IsReady())
-            {
-                AZ_Error(sampleName, false, "Failed to get streaming image asset '%s'", textureFilePath);
-                return nullptr;
-            }
-
-            auto image = RPI::StreamingImage::FindOrCreate(streamingImageAsset);
-            if (!image)
-            {
-                AZ_Error(sampleName, false, "Failed to find or create an image instance from image asset '%s'", textureFilePath);
-                return nullptr;
-            }
-
-            return image;
+            return AZ::RPI::LoadStreamingTexture(textureFilePath);
         }
 
         void DeferredFogSettings::OnSettingsChanged()

+ 20 - 15
Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Asset/AssetUtils.h

@@ -30,7 +30,23 @@ namespace AZ
             // Declarations...
 
             //! Finds the AssetId for a given product file path.
-            Data::AssetId GetAssetIdForProductPath(const char* productPath, TraceLevel reporting = TraceLevel::Warning, Data::AssetType assetType = Data::s_invalidAssetType, bool autoGenerateId = false);
+            Data::AssetId GetAssetIdForProductPath(const char* productPath, TraceLevel reporting = TraceLevel::Warning, Data::AssetType assetType = Data::s_invalidAssetType);
+
+            //! Tries to compile the asset at the given product path.
+            //! This will actively try to compile the asset every time it is called, it won't skip compilation just because the
+            //! asset exists. This should only be used for assets that need to be at their most up-to-date version of themselves
+            //! before getting loaded into the engine, as it can take seconds to minutes for this call to return. It is synchronously
+            //! asking the Asset Processor to compile the asset, and then blocks until it gets a result. If the AP is busy, it can
+            //! take a while to get a result even if the asset is already up-to-date.
+            //! In release builds where the AP isn't connected this will immediately return with "Unknown".
+            //! @param assetProductFilePath - the relative file path to the product asset (ex: default/models/sphere.azmodel)
+            //! @param reporting - the reporting level to use for problems.
+            //! @return true if the compilation is successful or unknown, false if an error was detected
+            //! "Unknown" is considered a successful result because if there's no Asset Processor, there's no way to truly
+            //! know the compile state of the asset. If the AP is connected, there *should* always be a result
+            //! (Compiled, Failed, Missing, etc), but if this is called *before* the AP is connected, it's possible to get Unknown
+            //! even when you think the AP is (or will be) connected.
+            bool TryToCompileAsset(const AZStd::string& assetProductFilePath, TraceLevel reporting);
 
             //! Gets an Asset<AssetDataT> reference for a given product file path. This function does not cause the asset to load.
             //! @return a null asset if the asset could not be found.
@@ -133,21 +149,10 @@ namespace AZ
             template<typename AssetDataT>
             Data::Asset<AssetDataT> LoadCriticalAsset(const AZStd::string& assetFilePath, TraceLevel reporting)
             {
-                bool apConnected = false;
-                AzFramework::AssetSystemRequestBus::BroadcastResult(
-                    apConnected, &AzFramework::AssetSystemRequestBus::Events::ConnectedWithAssetProcessor);
-                if (apConnected)
-                {
-                    AzFramework::AssetSystem::AssetStatus status = AzFramework::AssetSystem::AssetStatus_Unknown;
-                    AzFramework::AssetSystemRequestBus::BroadcastResult(
-                        status, &AzFramework::AssetSystemRequestBus::Events::CompileAssetSync, assetFilePath);
-                    if (status != AzFramework::AssetSystem::AssetStatus_Compiled && status != AzFramework::AssetSystem::AssetStatus_Unknown)
-                    {
-                        AssetUtilsInternal::ReportIssue(reporting, AZStd::string::format("Could not compile asset '%s'", assetFilePath.c_str()).c_str());
-                        return {};
-                    }
-                }
+                TryToCompileAsset(assetFilePath, reporting);
 
+                // Whether or not we were able to successfully compile the asset, we'll still try to load it.
+                // A failed compile could mean that the asset relies on intermediate assets that haven't been created yet.
                 return LoadAssetByProductPath<AssetDataT>(assetFilePath.c_str(), reporting);
             }
 

+ 17 - 1
Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Asset/AssetUtils.inl

@@ -19,7 +19,23 @@ namespace AZ
             {
                 AsyncAssetLoader loader(callback);
 
-                Data::AssetId assetId = GetAssetIdForProductPath(path.c_str(), TraceLevel::Error, azrtti_typeid<AssetDataT>(), true);
+                // Try to get an asset id for the requested path. Don't print an error yet if it isn't found though.
+                Data::AssetId assetId = GetAssetIdForProductPath(path.c_str(), TraceLevel::None, azrtti_typeid<AssetDataT>());
+
+                // If the asset id isn't valid for this path, it's possible that the asset hasn't been compiled yet.
+                if (!assetId.IsValid())
+                {
+                    // Try compiling the asset and getting the id again.
+                    AzFramework::AssetSystem::AssetStatus status = AzFramework::AssetSystem::AssetStatus_Unknown;
+                    AzFramework::AssetSystemRequestBus::BroadcastResult(
+                        status, &AzFramework::AssetSystemRequestBus::Events::CompileAssetSync, path);
+
+                    // This time, print an error if the asset id can't be determined.
+                    assetId = GetAssetIdForProductPath(path.c_str(), TraceLevel::Error, azrtti_typeid<AssetDataT>());
+                }
+
+                // We'll start the load whether or not the asset id is valid. It will immediately call the callback with failure if
+                // the asset id is invalid.
                 loader.StartLoad<AssetDataT>(assetId);
                 return loader;
             }

+ 5 - 0
Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Buffer/BufferAsset.h

@@ -63,6 +63,11 @@ namespace AZ
             // AssetData overrides...
             bool HandleAutoReload() override
             {
+                // Automatic asset reloads via the AssetManager are disabled for Atom models and their dependent assets because reloads
+                // need to happen in a specific order to refresh correctly. They require more complex code than what the default
+                // AssetManager reloading provides. See ModelReloader() for the actual handling of asset reloads.
+                // Models need to be loaded via the MeshFeatureProcessor to reload correctly, and reloads can be listened
+                // to by using MeshFeatureProcessor::ConnectModelChangeEventHandler().
                 return false;
             }
 

+ 37 - 0
Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelAsset.h

@@ -30,6 +30,7 @@ namespace AZ
             : public AZ::Data::AssetData
         {
             friend class ModelAssetCreator;
+            friend class ModelAssetHelpers;
 
         public:
             static const char* DisplayName;
@@ -77,9 +78,28 @@ namespace AZ
                 float& distanceNormalized, AZ::Vector3& normal) const;
 
         private:
+            //! Initialize the ModelAsset with the given set of data.
+            //! This is used by ModelAssetHelpers to overwrite an already-created ModelAsset.
+            //! @param name The name to associate with the model
+            //! @param lodAssets The list of LodAssets to use with the model
+            //! @param materialSlots The map of slots to materials for the model
+            //! @param fallbackSlot The slot to use as a fallback material
+            //! @param tags The set of tags to associate with the model.
+            void InitData(
+                AZ::Name name,
+                AZStd::span<Data::Asset<ModelLodAsset>> lodAssets,
+                const ModelMaterialSlotMap& materialSlots,
+                const ModelMaterialSlot& fallbackSlot,
+                AZStd::span<AZ::Name> tags);
+
             // AssetData overrides...
             bool HandleAutoReload() override
             {
+                // Automatic asset reloads via the AssetManager are disabled for Atom models and their dependent assets because reloads
+                // need to happen in a specific order to refresh correctly. They require more complex code than what the default
+                // AssetManager reloading provides. See ModelReloader() for the actual handling of asset reloads.
+                // Models need to be loaded via the MeshFeatureProcessor to reload correctly, and reloads can be listened
+                // to by using MeshFeatureProcessor::ConnectModelChangeEventHandler().
                 return false;
             }
 
@@ -126,6 +146,23 @@ namespace AZ
         public:
             AZ_RTTI(ModelAssetHandler, "{993B8CE3-1BBF-4712-84A0-285DB9AE808F}", AssetHandler<ModelAsset>);
 
+            //! Called when an asset requested to load is actually missing from the catalog when we are trying to resolve it
+            //! from an ID to a file name and other streaming info.
+            //! The AssetId that this returns should reference asset data to use as a fallback asset until the correct asset
+            //! is compiled by the Asset Processor and loaded (or not, if it's a missing or failed asset).
+            //! Missing assets don't support asset dependencies because they're substituted in at the asset stream load level,
+            //! so the substitute asset must be standalone. All processed ModelAsset models have dependencies on LODs, buffers, and
+            //! materials, so they can't be used as substitutes. Instead, this generates an in-memory unit cube model with no materials
+            //! as a no-dependency asset that can be used until the real one appears.
+            Data::AssetId AssetMissingInCatalog(const Data::Asset<Data::AssetData>& asset) override;
+
+            Data::AssetHandler::LoadResult LoadAssetData(
+                const AZ::Data::Asset<AZ::Data::AssetData>& asset,
+                AZStd::shared_ptr<AZ::Data::AssetDataStream> stream,
+                const AZ::Data::AssetFilterCB& assetLoadFilterCB) override;
+
+        private:
+            static const Data::AssetId s_defaultModelAssetId;
         };
     } //namespace RPI
 } // namespace AZ

+ 59 - 0
Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelAssetHelpers.h

@@ -0,0 +1,59 @@
+/*
+ * 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
+ *
+ */
+
+#pragma once
+
+#include <Atom/RPI.Reflect/Model/ModelAsset.h>
+
+namespace AZ
+{
+    namespace RPI
+    {
+        //! ModelAssetHelpers is a collection of helper methods for generating or manipulating model assets.
+        class ModelAssetHelpers
+        {
+        public:
+            //! Given an empty created ModelAsset, fill it with a valid unit cube model.
+            //! This model won't have a material on it so it requires a separate Material component to be displayable.
+            //! @param modelAsset An empty modelAsset that will get filled in with unit cube data.
+            static void CreateUnitCube(ModelAsset* modelAsset);
+
+            //! Given an empty created ModelAsset, fill it with a valid unit X-shaped model.
+            //! This model won't have a material on it so it requires a separate Material component to be displayable.
+            //! @param modelAsset An empty modelAsset that will get filled in with unit X data.
+            static void CreateUnitX(ModelAsset* modelAsset);
+
+        private:
+            //! Create a BufferAsset from the given data buffer.
+            //! @param data The data buffer to use for the BufferAsset
+            //! @param elementCount The number of elements in the data buffer
+            //! @param elementSize The size of each element in the data buffer in bytes
+            static Data::Asset<RPI::BufferAsset> CreateBufferAsset(
+                const void* data, const uint32_t elementCount, const uint32_t elementSize);
+
+            //! Create a model from the given data buffers.
+            //! @param modelAsset An empty modelAsset that will get filled in with the model data.
+            //! @param name The name to use for the model
+            //! @param indices The index buffer
+            //! @param positions The position buffer
+            //! @param normals The normal buffer
+            //! @param tangents The tangent buffer
+            //! @param bitangents The bitangent buffer
+            //! @param uvs The UV buffer
+            static void CreateModel(
+                ModelAsset* modelAsset,
+                const AZ::Name& name,
+                AZStd::span<const uint32_t> indices,
+                AZStd::span<const float_t> positions,
+                AZStd::span<const float> normals,
+                AZStd::span<const float> tangents,
+                AZStd::span<const float> bitangents,
+                AZStd::span<const float> uvs);
+        };
+    } //namespace RPI
+} // namespace AZ

+ 5 - 0
Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Model/ModelLodAsset.h

@@ -153,6 +153,11 @@ namespace AZ
             // AssetData overrides...
             bool HandleAutoReload() override
             {
+                // Automatic asset reloads via the AssetManager are disabled for Atom models and their dependent assets because reloads
+                // need to happen in a specific order to refresh correctly. They require more complex code than what the default
+                // AssetManager reloading provides. See ModelReloader() for the actual handling of asset reloads.
+                // Models need to be loaded via the MeshFeatureProcessor to reload correctly, and reloads can be listened
+                // to by using MeshFeatureProcessor::ConnectModelChangeEventHandler().
                 return false;
             }
             

+ 1 - 0
Gems/Atom/RPI/Code/Source/RPI.Builders/BuilderModule.cpp

@@ -32,6 +32,7 @@ namespace AZ
             {
                 m_descriptors.push_back(ModelExporterComponent::CreateDescriptor());
                 m_descriptors.push_back(ModelAssetBuilderComponent::CreateDescriptor());
+                m_descriptors.push_back(ModelAssetDependenciesComponent::CreateDescriptor());
                 m_descriptors.push_back(MaterialAssetBuilderComponent::CreateDescriptor());
                 m_descriptors.push_back(MaterialAssetDependenciesComponent::CreateDescriptor());
                 m_descriptors.push_back(BuilderComponent::CreateDescriptor());

+ 57 - 1
Gems/Atom/RPI/Code/Source/RPI.Builders/Model/ModelAssetBuilderComponent.cpp

@@ -163,7 +163,7 @@ namespace AZ
         SceneAPI::Events::ProcessingResult ModelAssetBuilderComponent::BuildModel(ModelAssetBuilderContext& context)
         {
             {
-                auto assetIdOutcome = RPI::AssetUtils::MakeAssetId("ResourcePools/DefaultVertexBufferPool.resourcepool", 0);
+                auto assetIdOutcome = RPI::AssetUtils::MakeAssetId(s_defaultVertexBufferPoolSourcePath, 0);
                 if (!assetIdOutcome.IsSuccess())
                 {
                     return SceneAPI::Events::ProcessingResult::Failure;
@@ -2409,5 +2409,61 @@ namespace AZ
 
             return transform;
         }
+
+        void ModelAssetDependenciesComponent::Reflect(ReflectContext* context)
+        {
+            if (auto* serialize = azrtti_cast<SerializeContext*>(context))
+            {
+                serialize->Class<ModelAssetDependenciesComponent, Component>()
+                    // If you have made changes to the model code and need to force scene files to reprocess,
+                    // change the version number in ModelAssetBuilderComponent, not this version number.
+                    ->Version(0)
+                    ->Attribute(
+                        Edit::Attributes::SystemComponentTags, AZStd::vector<Crc32>({ AssetBuilderSDK::ComponentTags::AssetBuilder }));
+            }
+        }
+
+        void ModelAssetDependenciesComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
+        {
+            provided.push_back(AZ_CRC_CE("ModelAssetDependenciesService"));
+        }
+
+        void ModelAssetDependenciesComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
+        {
+            incompatible.push_back(AZ_CRC_CE("ModelAssetDependenciesService"));
+        }
+
+        void ModelAssetDependenciesComponent::Activate()
+        {
+            SceneAPI::SceneBuilderDependencyBus::Handler::BusConnect();
+        }
+
+        void ModelAssetDependenciesComponent::Deactivate()
+        {
+            SceneAPI::SceneBuilderDependencyBus::Handler::BusDisconnect();
+        }
+
+        void ModelAssetDependenciesComponent::ReportJobDependencies(
+            SceneAPI::JobDependencyList& jobDependencyList, const char* platformIdentifier)
+        {
+            // Currently, the only implicit job dependency in model building is the dependency on the DefaultVertexBufferPool asset.
+            // It needs to get listed here to ensure that models aren't marked as complete and loadable by the engine before the
+            // DefaultVertexBufferPool has been processed.
+
+            AssetBuilderSDK::SourceFileDependency defaultVertexBufferPoolSource;
+            defaultVertexBufferPoolSource.m_sourceFileDependencyPath = ModelAssetBuilderComponent::s_defaultVertexBufferPoolSourcePath;
+
+            constexpr AZ::u32 ResourcePoolDefaultSubId = 0;
+
+            AssetBuilderSDK::JobDependency jobDependency;
+            jobDependency.m_jobKey = "Model Asset Builder (Default Vertex Buffer Pool)";
+            jobDependency.m_sourceFile = defaultVertexBufferPoolSource;
+            jobDependency.m_platformIdentifier = platformIdentifier;
+            jobDependency.m_productSubIds.push_back(ResourcePoolDefaultSubId);
+            jobDependency.m_type = AssetBuilderSDK::JobDependencyType::Order;
+
+            jobDependencyList.push_back(jobDependency);
+        }
+
     } // namespace RPI
 } // namespace AZ

+ 28 - 0
Gems/Atom/RPI/Code/Source/RPI.Builders/Model/ModelAssetBuilderComponent.h

@@ -23,6 +23,7 @@
 #include <SceneAPI/SceneCore/DataTypes/GraphData/ISkinWeightData.h>
 #include <SceneAPI/SceneCore/DataTypes/Rules/ISkinRule.h>
 #include <SceneAPI/SceneCore/Containers/SceneGraph.h>
+#include <SceneAPI/SceneCore/SceneBuilderDependencyBus.h>
 
 #include <Model/ModelExporterContexts.h>
 
@@ -132,6 +133,7 @@ namespace AZ
             using ProductMeshContentList = AZStd::vector<ProductMeshContent>;
 
             static constexpr inline const char* s_builderName = "Atom Model Builder";
+            static constexpr inline const char* s_defaultVertexBufferPoolSourcePath = "ResourcePools/DefaultVertexBufferPool.resourcepool";
 
         private:
 
@@ -399,5 +401,31 @@ namespace AZ
                 AZStd::unordered_map<AZStd::string, uint16_t>& jointNameToIndexMap,
                 size_t vertexIndex) const;
         };
+
+        //! Report dependencies for the ModelAssetBuilderComponent.
+        //! Specifically, this reports the dependency on DefaultVertexBufferPool.resourcepool.
+        class ModelAssetDependenciesComponent
+            : public Component
+            , public SceneAPI::SceneBuilderDependencyBus::Handler
+        {
+        public:
+            AZ_COMPONENT(ModelAssetDependenciesComponent, "{CE1E8C2B-A05F-4AED-8771-88E58377A82B}");
+
+            static void Reflect(ReflectContext* context);
+
+            ModelAssetDependenciesComponent() = default;
+            ~ModelAssetDependenciesComponent() override = default;
+
+            static void GetProvidedServices(ComponentDescriptor::DependencyArrayType& provided);
+            static void GetIncompatibleServices(ComponentDescriptor::DependencyArrayType& incompatible);
+
+            // AZ::Component overrides...
+            void Activate() override;
+            void Deactivate() override;
+
+            // SceneAPI::SceneBuilderDependencyBus::Handler overrides...
+            void ReportJobDependencies(SceneAPI::JobDependencyList& jobDependencyList, const char* platformIdentifier) override;
+        };
+
     } // namespace RPI
 } // namespace AZ

+ 1 - 28
Gems/Atom/RPI/Code/Source/RPI.Public/RPIUtils.cpp

@@ -648,34 +648,7 @@ namespace AZ
 
         AZ::Data::Instance<RPI::StreamingImage> LoadStreamingTexture(AZStd::string_view path)
         {
-            AzFramework::AssetSystem::AssetStatus status = AzFramework::AssetSystem::AssetStatus_Unknown;
-            AzFramework::AssetSystemRequestBus::BroadcastResult(
-                status, &AzFramework::AssetSystemRequestBus::Events::CompileAssetSync, path);
-
-            // When running with no Asset Processor (for example in release), CompileAssetSync will return AssetStatus_Unknown.
-            AZ_Error(
-                "RPIUtils",
-                status == AzFramework::AssetSystem::AssetStatus_Compiled || status == AzFramework::AssetSystem::AssetStatus_Unknown,
-                "Could not compile image at '%s'",
-                path.data());
-
-            Data::AssetId streamingImageAssetId;
-            Data::AssetCatalogRequestBus::BroadcastResult(
-                streamingImageAssetId,
-                &Data::AssetCatalogRequestBus::Events::GetAssetIdByPath,
-                path.data(),
-                azrtti_typeid<RPI::StreamingImageAsset>(),
-                false);
-            if (!streamingImageAssetId.IsValid())
-            {
-                AZ_Error("RPI Utils", false, "Failed to get streaming image asset id with path " AZ_STRING_FORMAT, AZ_STRING_ARG(path));
-                return AZ::Data::Instance<RPI::StreamingImage>();
-            }
-
-            auto streamingImageAsset = Data::AssetManager::Instance().GetAsset<RPI::StreamingImageAsset>(
-                streamingImageAssetId, AZ::Data::AssetLoadBehavior::PreLoad);
-
-            streamingImageAsset.BlockUntilLoadComplete();
+            auto streamingImageAsset = RPI::AssetUtils::LoadCriticalAsset<RPI::StreamingImageAsset>(path);
 
             if (!streamingImageAsset.IsReady())
             {

+ 30 - 3
Gems/Atom/RPI/Code/Source/RPI.Reflect/Asset/AssetUtils.cpp

@@ -33,19 +33,46 @@ namespace AZ
                 }
             }
 
-            Data::AssetId GetAssetIdForProductPath(const char* productPath, TraceLevel reporting, Data::AssetType assetType, bool autoGenerateId)
+            bool TryToCompileAsset(const AZStd::string& assetFilePath, TraceLevel reporting)
             {
+                AzFramework::AssetSystem::AssetStatus status = AzFramework::AssetSystem::AssetStatus_Unknown;
+                AzFramework::AssetSystemRequestBus::BroadcastResult(
+                        status, &AzFramework::AssetSystemRequestBus::Events::CompileAssetSync, assetFilePath);
+
+                if ((status != AzFramework::AssetSystem::AssetStatus_Compiled) && (status != AzFramework::AssetSystem::AssetStatus_Unknown))
+                {
+                    AssetUtilsInternal::ReportIssue(
+                        reporting,
+                        AZStd::string::format(
+                            "Could not compile asset '%s', status = %u.", assetFilePath.c_str(), static_cast<uint32_t>(status))
+                            .c_str());
+
+                    return false;
+                }
+
+                return true;
+            }
+
+            Data::AssetId GetAssetIdForProductPath(const char* productPath, TraceLevel reporting, Data::AssetType assetType)
+            {
+                // Don't create a new entry in the asset catalog for this asset if it doesn't exist.
+                // Since we only have a product path and not an asset id, any entry we create will have an incorrect id,
+                // incorrect size and dependency information, and will point to a file that doesn't exist. Any attempt to use
+                // that id will fail.
+                constexpr bool AutoGenerateId = false;
+
                 Data::AssetId assetId;
                 Data::AssetCatalogRequestBus::BroadcastResult(
                     assetId,
                     &Data::AssetCatalogRequestBus::Events::GetAssetIdByPath,
                     productPath,
                     assetType,
-                    autoGenerateId);
+                    AutoGenerateId);
 
                 if (!assetId.IsValid())
                 {
-                    AZStd::string errorMessage = AZStd::string::format("Unable to find product asset '%s'. Has the source asset finished building?", productPath);
+                    AZStd::string errorMessage = AZStd::string::format(
+                        "Unable to find product asset '%s'. Has the source asset finished building?", productPath);
                     AssetUtilsInternal::ReportIssue(reporting, errorMessage.c_str());
                 }
 

+ 12 - 5
Gems/Atom/RPI/Code/Source/RPI.Reflect/Image/StreamingImageAssetHandler.cpp

@@ -38,10 +38,13 @@ namespace AZ
             Data::AssetHandler::LoadResult loadResult = Data::AssetHandler::LoadResult::Error;
             if (assetData)
             {
-                loadResult = Utils::LoadObjectFromStreamInPlace<StreamingImageAsset>(*stream, *assetData, m_serializeContext) ?
-                    Data::AssetHandler::LoadResult::LoadComplete :
-                    Data::AssetHandler::LoadResult::Error;
-                
+                if (stream->GetLength() > 0)
+                {
+                    loadResult = Utils::LoadObjectFromStreamInPlace<StreamingImageAsset>(*stream, *assetData, m_serializeContext)
+                        ? Data::AssetHandler::LoadResult::LoadComplete
+                        : Data::AssetHandler::LoadResult::Error;
+                }
+               
                 if (loadResult == Data::AssetHandler::LoadResult::LoadComplete)
                 {
                     // ImageMipChainAsset has some internal variables need to initialized after it was loaded.
@@ -223,9 +226,13 @@ namespace AZ
 
         Data::AssetId StreamingImageAssetHandler::AssetMissingInCatalog(const Data::Asset<Data::AssetData>& asset)
         {
+            AZ_Info("Streaming Image",
+                "Streaming Image id " AZ_STRING_FORMAT " not found in asset catalog, using fallback image.\n",
+                AZ_STRING_ARG(asset.GetId().ToFixedString()));
+
             // Find out if the asset is missing completely, or just still processing
             // and escalate the asset to the top of the list
-            AzFramework::AssetSystem::AssetStatus missingAssetStatus;
+            AzFramework::AssetSystem::AssetStatus missingAssetStatus = AzFramework::AssetSystem::AssetStatus::AssetStatus_Unknown;
             AzFramework::AssetSystemRequestBus::BroadcastResult(
                 missingAssetStatus, &AzFramework::AssetSystem::AssetSystemRequests::GetAssetStatusById, asset.GetId().m_guid);
 

+ 112 - 0
Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAsset.cpp

@@ -7,6 +7,7 @@
  */
 
 #include <Atom/RPI.Reflect/Model/ModelAsset.h>
+#include <Atom/RPI.Reflect/Model/ModelAssetHelpers.h>
 #include <Atom/RPI.Reflect/Model/ModelKdTree.h>
 #include <AzCore/Asset/AssetSerializer.h>
 #include <AzCore/Jobs/JobFunction.h>
@@ -17,6 +18,8 @@
 #include <AzCore/Serialization/SerializeContext.h>
 #include <AzCore/Serialization/EditContext.h>
 
+#include <AzFramework/Asset/AssetSystemBus.h>
+
 namespace AZ
 {
     namespace RPI
@@ -336,5 +339,114 @@ namespace AZ
             return modelTriangleCount;
         }
 
+        void ModelAsset::InitData(
+            AZ::Name name,
+            AZStd::span<Data::Asset<ModelLodAsset>> lodAssets,
+            const ModelMaterialSlotMap& materialSlots,
+            const ModelMaterialSlot& fallbackSlot,
+            AZStd::span<AZ::Name> tags)
+        {
+            AZ_Assert(!m_isKdTreeCalculationRunning, "Overwriting a ModelAsset while it is calculating its kd tree.");
+
+            // Clear out the current AABB, we'll reset it with the data from the LOD assets.
+            m_aabb = AZ::Aabb::CreateNull();
+
+            // Copy the trivially-copyable data.
+            m_name = name;
+            m_materialSlots = materialSlots;
+            m_fallbackSlot = fallbackSlot;
+
+            // Clear out the runtime-calculated data.
+            m_kdTree = {};
+            m_isKdTreeCalculationRunning = false;
+            m_modelTriangleCount = {};
+
+            // Clear out tags and LOD Assets, we'll set those individually.
+            m_lodAssets.clear();
+            m_tags = {};
+
+            for (const auto& lodAsset : lodAssets)
+            {
+                m_lodAssets.push_back(lodAsset);
+                if (lodAsset.IsReady())
+                {
+                    m_aabb.AddAabb(lodAsset->GetAabb());
+                }
+            }
+
+            for (const auto& tag : tags)
+            {
+                m_tags.push_back(tag);
+            }
+        }
+
+        // Create a stable ID for our default fallback model.
+        const AZ::Data::AssetId ModelAssetHandler::s_defaultModelAssetId{ "{D676DD3C-0560-4F39-99E0-B6DCBC7CEDAA}", 0 };
+
+        Data::AssetHandler::LoadResult ModelAssetHandler::LoadAssetData(
+            const AZ::Data::Asset<AZ::Data::AssetData>& asset,
+            AZStd::shared_ptr<AZ::Data::AssetDataStream> stream,
+            const AZ::Data::AssetFilterCB& assetLoadFilterCB)
+        {
+            // If there's a 0-length stream, this must be trying to load our default fallback model.
+            // Fill in the asset data with a generated unit X-shaped model.
+            // We need to generate the data instead of load a fallback asset because model assets have dependencies
+            // on buffer and material assets, and fallback assets need to not have any dependencies to be able to load
+            // correctly when used as fallbacks. (The AssetManager doesn't currently support handling dependency pre-loading
+            // for fallback assets)
+            if (stream->GetLength() == 0)
+            {
+                auto assetData = asset.GetAs<AZ::RPI::ModelAsset>();
+                if (assetData)
+                {
+                    ModelAssetHelpers::CreateUnitX(assetData);
+                }
+
+                return Data::AssetHandler::LoadResult::LoadComplete;
+            }
+
+            return AssetHandler::LoadAssetData(asset, stream, assetLoadFilterCB);
+        }
+
+        Data::AssetId ModelAssetHandler::AssetMissingInCatalog(const Data::Asset<Data::AssetData>& asset)
+        {
+            AZ_Info(
+                "Model",
+                "Model id " AZ_STRING_FORMAT " not found in asset catalog, using fallback model.\n",
+                AZ_STRING_ARG(asset.GetId().ToFixedString()));
+
+            // Find out if the asset is missing completely or just still processing
+            // and escalate the asset to the top of the list if it's queued.
+            AzFramework::AssetSystem::AssetStatus missingAssetStatus = AzFramework::AssetSystem::AssetStatus::AssetStatus_Unknown;
+            AzFramework::AssetSystemRequestBus::BroadcastResult(
+                missingAssetStatus, &AzFramework::AssetSystem::AssetSystemRequests::GetAssetStatusById, asset.GetId().m_guid);
+
+            if (missingAssetStatus == AzFramework::AssetSystem::AssetStatus::AssetStatus_Queued)
+            {
+                bool sendSucceeded = false;
+                AzFramework::AssetSystemRequestBus::BroadcastResult(
+                    sendSucceeded, &AzFramework::AssetSystem::AssetSystemRequests::EscalateAssetByUuid, asset.GetId().m_guid);
+            }
+
+            // Make sure the default model asset has an entry in the asset catalog so that the asset system will try to load it.
+            // Note that we specifically give it a 0-byte size and a non-empty path so that the load will just trivially succeed
+            // with a 0-byte asset stream. This will enable us to detect it in LoadAssetData and fill in the data with a generated
+            // unit cube. We can't use an on-disk model asset because the AssetMissing system currently doesn't correctly handle
+            // assets with dependent assets (like azmodel), so we need to just load an "empty" asset and then fill it in with data
+            // in LoadAssetData.
+            {
+                Data::AssetInfo assetInfo;
+                assetInfo.m_assetId = s_defaultModelAssetId;
+                assetInfo.m_assetType = azrtti_typeid<AZ::RPI::ModelAsset>();
+                assetInfo.m_relativePath = "default_fallback_model";
+                assetInfo.m_sizeBytes = 0;
+                AZ::Data::AssetCatalogRequestBus::Broadcast(
+                    &AZ::Data::AssetCatalogRequestBus::Events::RegisterAsset, assetInfo.m_assetId, assetInfo);
+            }
+
+            return s_defaultModelAssetId;
+        }
+
+
     } // namespace RPI
 } // namespace AZ

+ 294 - 0
Gems/Atom/RPI/Code/Source/RPI.Reflect/Model/ModelAssetHelpers.cpp

@@ -0,0 +1,294 @@
+/*
+ * 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 <Atom/RPI.Reflect/Buffer/BufferAssetCreator.h>
+#include <Atom/RPI.Reflect/Model/ModelAssetHelpers.h>
+#include <Atom/RPI.Reflect/Model/ModelLodAssetCreator.h>
+#include <Atom/RPI.Reflect/ResourcePoolAssetCreator.h>
+
+namespace AZ
+{
+    namespace RPI
+    {
+        AZ::Data::Asset<AZ::RPI::BufferAsset> ModelAssetHelpers::CreateBufferAsset(
+            const void* data, const uint32_t elementCount, const uint32_t elementSize)
+        {
+            // Create a buffer pool asset for use with the buffer asset
+            AZ::Data::Asset<AZ::RPI::ResourcePoolAsset> bufferPoolAsset;
+            {
+                AZ::Data::AssetId bufferPoolId = AZ::Uuid::CreateRandom();
+                bufferPoolAsset = AZ::Data::AssetManager::Instance().CreateAsset(
+                    bufferPoolId, azrtti_typeid<AZ::RPI::ResourcePoolAsset>(), Data::AssetLoadBehavior::PreLoad);
+
+                auto bufferPoolDesc = AZStd::make_unique<AZ::RHI::BufferPoolDescriptor>();
+                bufferPoolDesc->m_bindFlags = AZ::RHI::BufferBindFlags::InputAssembly;
+                bufferPoolDesc->m_heapMemoryLevel = AZ::RHI::HeapMemoryLevel::Host;
+
+                AZ::RPI::ResourcePoolAssetCreator creator;
+                creator.Begin(bufferPoolId);
+                creator.SetPoolDescriptor(AZStd::move(bufferPoolDesc));
+                creator.SetPoolName("ModelAssetHelperBufferPool");
+                creator.End(bufferPoolAsset);
+            }
+
+            // Create a buffer asset that contains a copy of the input data.
+            AZ::Data::Asset<AZ::RPI::BufferAsset> asset;
+            {
+                AZ::Data::AssetId bufferId = AZ::Uuid::CreateRandom();
+                asset = AZ::Data::AssetManager::Instance().CreateAsset(
+                    bufferId, azrtti_typeid<AZ::RPI::BufferAsset>(), Data::AssetLoadBehavior::PreLoad);
+
+                AZ::RHI::BufferDescriptor bufferDescriptor;
+                bufferDescriptor.m_bindFlags = AZ::RHI::BufferBindFlags::InputAssembly;
+                bufferDescriptor.m_byteCount = elementCount * elementSize;
+
+                AZ::RPI::BufferAssetCreator creator;
+                creator.Begin(bufferId);
+                creator.SetPoolAsset(bufferPoolAsset);
+                creator.SetBuffer(data, bufferDescriptor.m_byteCount, bufferDescriptor);
+                creator.SetBufferViewDescriptor(AZ::RHI::BufferViewDescriptor::CreateStructured(0, elementCount, elementSize));
+                creator.End(asset);
+            }
+
+            return asset;
+        }
+
+        void ModelAssetHelpers::CreateModel(
+            ModelAsset* modelAsset,
+            const AZ::Name& name,
+            AZStd::span<const uint32_t> indices,
+            AZStd::span<const float_t> positions,
+            AZStd::span<const float> normals,
+            AZStd::span<const float> tangents,
+            AZStd::span<const float> bitangents,
+            AZStd::span<const float> uvs)
+        {
+            // First build a model LOD asset that contains a mesh for the given data.
+            AZ::Data::Asset<AZ::RPI::ModelLodAsset> lodAsset;
+
+            AZ::Data::AssetId lodAssetId = AZ::Uuid::CreateRandom();
+            lodAsset = AZ::Data::AssetManager::Instance().CreateAsset(
+                lodAssetId, azrtti_typeid<AZ::RPI::ModelLodAsset>(), Data::AssetLoadBehavior::PreLoad);
+
+            AZ::RPI::ModelLodAssetCreator creator;
+            creator.Begin(lodAssetId);
+
+            const uint32_t positionElementCount = aznumeric_cast<uint32_t>(positions.size() / 3);
+            const uint32_t indexElementCount = aznumeric_cast<uint32_t>(indices.size());
+            const uint32_t uvElementCount = aznumeric_cast<uint32_t>(uvs.size() / 2);
+            const uint32_t normalElementCount = aznumeric_cast<uint32_t>(normals.size() / 3);
+            const uint32_t tangentElementCount = aznumeric_cast<uint32_t>(tangents.size() / 4);
+            const uint32_t bitangentElementCount = aznumeric_cast<uint32_t>(bitangents.size() / 3);
+
+            constexpr uint32_t PositionElementSize = aznumeric_cast<uint32_t>(sizeof(float) * 3);
+            constexpr uint32_t IndexElementSize = aznumeric_cast<uint32_t>(sizeof(uint32_t));
+            constexpr uint32_t UvElementSize = aznumeric_cast<uint32_t>(sizeof(float) * 2);
+            constexpr uint32_t NormalElementSize = aznumeric_cast<uint32_t>(sizeof(float) * 3);
+            constexpr uint32_t TangentElementSize = aznumeric_cast<uint32_t>(sizeof(float) * 4);
+            constexpr uint32_t BitangentElementSize = aznumeric_cast<uint32_t>(sizeof(float) * 3);
+
+            // Calculate the Aabb for the given positions.
+            AZ::Aabb aabb = AZ::Aabb::CreateNull();
+            for (uint32_t i = 0; i < positions.size(); i += 3)
+            {
+                aabb.AddPoint(AZ::Vector3(positions[i], positions[i + 1], positions[i + 2]));
+            }
+
+            // Set up a single-mesh asset with only position data.
+            creator.BeginMesh();
+            creator.SetMeshAabb(AZStd::move(aabb));
+            creator.SetMeshMaterialSlot(0);
+            creator.SetMeshIndexBuffer({ CreateBufferAsset(indices.data(), indexElementCount, IndexElementSize),
+                                         AZ::RHI::BufferViewDescriptor::CreateTyped(0, indexElementCount, AZ::RHI::Format::R32_UINT) });
+            creator.AddMeshStreamBuffer(
+                AZ::RHI::ShaderSemantic(AZ::Name("POSITION")),
+                AZ::Name(),
+                { CreateBufferAsset(positions.data(), positionElementCount, PositionElementSize),
+                  AZ::RHI::BufferViewDescriptor::CreateTyped(0, positionElementCount, AZ::RHI::Format::R32G32B32_FLOAT) });
+            creator.AddMeshStreamBuffer(
+                AZ::RHI::ShaderSemantic(AZ::Name("NORMAL")),
+                AZ::Name(),
+                { CreateBufferAsset(normals.data(), normalElementCount, NormalElementSize),
+                  AZ::RHI::BufferViewDescriptor::CreateTyped(0, normalElementCount, AZ::RHI::Format::R32G32B32_FLOAT) });
+            creator.AddMeshStreamBuffer(
+                AZ::RHI::ShaderSemantic(AZ::Name("TANGENT")),
+                AZ::Name(),
+                { CreateBufferAsset(tangents.data(), tangentElementCount, TangentElementSize),
+                  AZ::RHI::BufferViewDescriptor::CreateTyped(0, tangentElementCount, AZ::RHI::Format::R32G32B32A32_FLOAT) });
+            creator.AddMeshStreamBuffer(
+                AZ::RHI::ShaderSemantic(AZ::Name("BITANGENT")),
+                AZ::Name(),
+                { CreateBufferAsset(bitangents.data(), bitangentElementCount, BitangentElementSize),
+                  AZ::RHI::BufferViewDescriptor::CreateTyped(0, bitangentElementCount, AZ::RHI::Format::R32G32B32_FLOAT) });
+            creator.AddMeshStreamBuffer(
+                AZ::RHI::ShaderSemantic(AZ::Name("UV")),
+                AZ::Name(),
+                { CreateBufferAsset(uvs.data(), uvElementCount, UvElementSize),
+                  AZ::RHI::BufferViewDescriptor::CreateTyped(0, uvElementCount, AZ::RHI::Format::R32G32_FLOAT) });
+            creator.EndMesh();
+            creator.End(lodAsset);
+
+            // Create a model asset that contains the single LOD built above.
+            modelAsset->InitData(
+                name,
+                AZStd::span<AZ::Data::Asset<AZ::RPI::ModelLodAsset>>(&lodAsset, 1),
+                {}, // no material slots
+                {}, // no fallback material
+                {} // no tags
+            );
+        }
+
+        void ModelAssetHelpers::CreateUnitCube(ModelAsset* modelAsset)
+        {
+            // Build a mesh containing a unit cube.
+            // The vertices are duplicated for each face so that we can have correct per-face normals and UVs.
+
+            constexpr int ValuesPerPositionEntry = 3;
+            constexpr int ValuesPerUvEntry = 2;
+            constexpr int ValuesPerNormalEntry = 3;
+            constexpr int ValuesPerTangentEntry = 4;
+            constexpr int ValuesPerBitangentEntry = 3;
+
+            constexpr int VerticesPerFace = 6;
+            constexpr int MeshFaces = 6;
+            constexpr int ValuesPerFace = 4;
+
+            // 6 vertices per face, 6 faces.
+            constexpr AZStd::array<uint32_t, VerticesPerFace * MeshFaces> indices = {
+                 0,  1,  2,  0,  2,  3,   // front face
+                 4,  5,  6,  4,  6,  7,   // right face
+                 8,  9, 10,  8, 10, 11,   // back face
+                12, 13, 14, 12, 14, 15,   // left face
+                16, 17, 18, 16, 18, 19,   // top face
+                20, 21, 22, 20, 22, 23    // bottom face
+            };
+
+            // 3 values per position, 4 positions per face, 6 faces
+            constexpr AZStd::array<float, ValuesPerPositionEntry * ValuesPerFace * MeshFaces> positions = {
+                -0.5f, -0.5f, -0.5f, +0.5f, -0.5f, -0.5f, +0.5f, -0.5f, +0.5f, -0.5f, -0.5f, +0.5f,     // front
+                +0.5f, -0.5f, -0.5f, +0.5f, +0.5f, -0.5f, +0.5f, +0.5f, +0.5f, +0.5f, -0.5f, +0.5f,     // right
+                +0.5f, +0.5f, -0.5f, -0.5f, +0.5f, -0.5f, -0.5f, +0.5f, +0.5f, +0.5f, +0.5f, +0.5f,     // back
+                -0.5f, +0.5f, -0.5f, -0.5f, -0.5f, -0.5f, -0.5f, -0.5f, +0.5f, -0.5f, +0.5f, +0.5f,     // left
+                -0.5f, -0.5f, +0.5f, +0.5f, -0.5f, +0.5f, +0.5f, +0.5f, +0.5f, -0.5f, +0.5f, +0.5f,     // top
+                -0.5f, +0.5f, -0.5f, +0.5f, +0.5f, -0.5f, +0.5f, -0.5f, -0.5f, -0.5f, -0.5f, -0.5f,     // bottom
+            };
+
+            // 2 values per position, 4 positions per face, 6 faces
+            // This aribtrarily maps the UVs to use the full texture on each face.
+            // This choice can be changed if a different mapping would be more usable.
+            constexpr AZStd::array<float, ValuesPerUvEntry * ValuesPerFace * MeshFaces> uvs = {
+                0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,     // front
+                0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,     // right
+                0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,     // back
+                0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,     // left
+                0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,     // top
+                0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,     // bottom
+            };
+
+            // 3 values per position, 4 positions per face, 6 faces
+            constexpr AZStd::array<float, ValuesPerNormalEntry * ValuesPerFace * MeshFaces> normals = {
+                +0.0f, -1.0f, +0.0f, +0.0f, -1.0f, +0.0f, +0.0f, -1.0f, +0.0f, +0.0f, -1.0f, +0.0f,     // front (-Y)
+                +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f,     // right (+X)
+                +0.0f, +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, +1.0f, +0.0f,     // back (+Y)
+                -1.0f, +0.0f, +0.0f, -1.0f, +0.0f, +0.0f, -1.0f, +0.0f, +0.0f, -1.0f, +0.0f, +0.0f,     // left (-X)
+                +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, +1.0f,     // top (+Z)
+                +0.0f, +0.0f, -1.0f, +0.0f, +0.0f, -1.0f, +0.0f, +0.0f, -1.0f, +0.0f, +0.0f, -1.0f,     // bottom (-Z)
+            };
+
+            // 4 values per position, 4 positions per face, 6 faces
+            constexpr AZStd::array<float, ValuesPerTangentEntry * ValuesPerFace * MeshFaces> tangents = {
+                0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // front (+Z)
+                0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // right (+Z)
+                0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // back (+Z)
+                0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // left (+Z)
+                0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, // top (+Y)
+                0.0f, -1.0f, 0.0f, 1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, -1.0f, 0.0f, 1.0f, // bottom (-Y)
+            };
+
+            // 3 values per position, 4 positions per face, 6 faces
+            constexpr AZStd::array<float, ValuesPerBitangentEntry * ValuesPerFace * MeshFaces> bitangents = {
+                +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, // front (+X)
+                +0.0f, +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, // right (+Y)
+                -1.0f, +0.0f, +0.0f, -1.0f, +0.0f, +0.0f, -1.0f, +0.0f, +0.0f, -1.0f, +0.0f, +0.0f, // back (-X)
+                +0.0f, -1.0f, +0.0f, +0.0f, -1.0f, +0.0f, +0.0f, -1.0f, +0.0f, +0.0f, -1.0f, +0.0f, // left (-Y)
+                +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, // top (+X)
+                +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, +1.0f, +0.0f, +0.0f, // bottom (+X)
+            };
+
+            CreateModel(modelAsset, AZ::Name("UnitCube"), indices, positions, normals, tangents, bitangents, uvs);
+        }
+
+        void ModelAssetHelpers::CreateUnitX(ModelAsset* modelAsset)
+        {
+            // Build a mesh containing a unit X model.
+            // To make the X double-sided regardless of material, we'll create two faces for each branch of the X.
+
+            constexpr int ValuesPerPositionEntry = 3;
+            constexpr int ValuesPerUvEntry = 2;
+            constexpr int ValuesPerNormalEntry = 3;
+            constexpr int ValuesPerTangentEntry = 4;
+            constexpr int ValuesPerBitangentEntry = 3;
+
+            constexpr int VerticesPerFace = 6;
+            constexpr int MeshFaces = 4;
+            constexpr int ValuesPerFace = 4;
+
+            // 6 vertices per face, 4 faces.
+            constexpr AZStd::array<uint32_t, VerticesPerFace * MeshFaces> indices = {
+                0,  1,  2,  0,  2,  3, // / face of X
+                4,  5,  6,  4,  6,  7, // \ face of X
+                8,  9,  10, 8,  10, 11, // / face of X (back)
+                12, 13, 14, 12, 14, 15, // \ face of X (back)
+            };
+
+            // 3 values per position, 4 positions per face, 2 faces
+            constexpr AZStd::array<float, ValuesPerPositionEntry * ValuesPerFace * MeshFaces> positions = {
+                -0.5f, -0.5f, -0.5f, +0.5f, +0.5f, -0.5f, +0.5f, +0.5f, +0.5f, -0.5f, -0.5f, +0.5f, //   / face of X
+                -0.5f, +0.5f, -0.5f, +0.5f, -0.5f, -0.5f, +0.5f, -0.5f, +0.5f, -0.5f, +0.5f, +0.5f, //   \ face of X
+                +0.5f, +0.5f, -0.5f, -0.5f, -0.5f, -0.5f, -0.5f, -0.5f, +0.5f, +0.5f, +0.5f, +0.5f, //   / face of X (back)
+                +0.5f, -0.5f, -0.5f, -0.5f, +0.5f, -0.5f, -0.5f, +0.5f, +0.5f, +0.5f, -0.5f, +0.5f, //   \ face of X (back)
+            };
+
+            // 2 values per position, 4 positions per face, 2 faces
+            // This aribtrarily maps the UVs to use the full texture on each face.
+            // This choice can be changed if a different mapping would be more usable.
+            constexpr AZStd::array<float, ValuesPerUvEntry * ValuesPerFace * MeshFaces> uvs = {
+                0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, //   / face of X
+                0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, //   \ face of X
+                0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, //   / face of X (back)
+                0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, //   \ face of X (back
+            };
+
+            // 3 values per position, 4 positions per face, 2 faces
+            constexpr AZStd::array<float, ValuesPerNormalEntry * ValuesPerFace * MeshFaces> normals = {
+                +0.5f, -0.5f, +0.0f, +0.5f, -0.5f, +0.0f, +0.5f, -0.5f, +0.0f, +0.5f, -0.5f, +0.0f, //   / face of X
+                -0.5f, -0.5f, +0.0f, -0.5f, -0.5f, +0.0f, -0.5f, -0.5f, +0.0f, -0.5f, -0.5f, +0.0f, //   \ face of X
+                -0.5f, +0.5f, +0.0f, -0.5f, +0.5f, +0.0f, -0.5f, +0.5f, +0.0f, -0.5f, +0.5f, +0.0f, //   / face of X (back)
+                +0.5f, +0.5f, +0.0f, +0.5f, +0.5f, +0.0f, +0.5f, +0.5f, +0.0f, +0.5f, +0.5f, +0.0f, //   \ face of X (back)
+            };
+
+            // 4 values per position, 4 positions per face, 2 faces
+            constexpr AZStd::array<float, ValuesPerTangentEntry * ValuesPerFace * MeshFaces> tangents = {
+                0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, //   / face of X
+                0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, //   \ face of X
+                0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, //   / face of X (back)
+                0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, //   \ face of X (back)
+            };
+
+            // 3 values per position, 4 positions per face, 2 faces
+            constexpr AZStd::array<float, ValuesPerBitangentEntry * ValuesPerFace * MeshFaces> bitangents = {
+                +0.5f, +0.5f, +0.0f, +0.5f, +0.5f, +0.0f, +0.5f, +0.5f, +0.0f, +0.5f, +0.5f, +0.0f, //   / face of X
+                -0.5f, +0.5f, +0.0f, -0.5f, +0.5f, +0.0f, -0.5f, +0.5f, +0.0f, -0.5f, +0.5f, +0.0f, //   \ face of X
+                -0.5f, -0.5f, +0.0f, -0.5f, -0.5f, +0.0f, -0.5f, -0.5f, +0.0f, -0.5f, -0.5f, +0.0f, //   / face of X (back)
+                +0.5f, -0.5f, +0.0f, +0.5f, -0.5f, +0.0f, +0.5f, -0.5f, +0.0f, +0.5f, -0.5f, +0.0f, //   \ face of X (back)
+            };
+
+            CreateModel(modelAsset, AZ::Name("UnitX"), indices, positions, normals, tangents, bitangents, uvs);
+        }
+    } // namespace RPI
+} // namespace AZ

+ 2 - 0
Gems/Atom/RPI/Code/atom_rpi_reflect_files.cmake

@@ -19,6 +19,7 @@ set(FILES
     Include/Atom/RPI.Reflect/Buffer/BufferAssetCreator.h
     Include/Atom/RPI.Reflect/Buffer/BufferAssetView.h
     Include/Atom/RPI.Reflect/Model/ModelAsset.h
+    Include/Atom/RPI.Reflect/Model/ModelAssetHelpers.h
     Include/Atom/RPI.Reflect/Model/ModelKdTree.h
     Include/Atom/RPI.Reflect/Model/ModelLodAsset.h
     Include/Atom/RPI.Reflect/Model/ModelLodIndex.h
@@ -108,6 +109,7 @@ set(FILES
     Source/RPI.Reflect/Buffer/BufferAssetCreator.cpp
     Source/RPI.Reflect/Buffer/BufferAssetView.cpp
     Source/RPI.Reflect/Model/ModelAsset.cpp
+    Source/RPI.Reflect/Model/ModelAssetHelpers.cpp
     Source/RPI.Reflect/Model/ModelKdTree.cpp
     Source/RPI.Reflect/Model/ModelLodAsset.cpp
     Source/RPI.Reflect/Model/ModelAssetCreator.cpp

+ 3 - 4
Gems/Atom/Tools/AtomToolsFramework/Code/Source/Application/AtomToolsApplication.cpp

@@ -366,10 +366,6 @@ namespace AtomToolsFramework
             AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::ExitMainLoop);
         }
 
-        AZ_TracePrintf("AtomToolsApplication", "CriticalAssetsCompiled\n");
-
-        AZ::ComponentApplicationLifecycle::SignalEvent(*m_settingsRegistry, "CriticalAssetsCompiled", R"({})");
-
         // Reload the assetcatalog.xml at this point again
         // Start Monitoring Asset changes over the network and load the AssetCatalog
         auto LoadCatalog = [settingsRegistry = m_settingsRegistry.get()](AZ::Data::AssetCatalogRequests* assetCatalogRequests)
@@ -382,6 +378,9 @@ namespace AtomToolsFramework
             }
         };
         AZ::Data::AssetCatalogRequestBus::Broadcast(AZStd::move(LoadCatalog));
+
+        // Only signal the event *after* the asset catalog has been loaded.
+        AZ::ComponentApplicationLifecycle::SignalEvent(*m_settingsRegistry, "CriticalAssetsCompiled", R"({})");
     }
 
     void AtomToolsApplication::SaveSettings()

+ 4 - 0
Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/Mesh/MeshComponentBus.h

@@ -106,6 +106,10 @@ namespace AZ
             : public ComponentBus
         {
         public:
+            // Notifications can be triggered from job threads, so this uses a mutex to guard against
+            // listeners joining or leaving the ebus on other threads mid-notification.
+            using MutexType = AZStd::recursive_mutex;
+
             //! Notifies listeners when a model has been loaded.
             //! If the model is already loaded when first connecting to the MeshComponentNotificationBus,
             //! the OnModelReady event will occur when connecting.

+ 1 - 1
Gems/AtomLyIntegration/EditorModeFeedback/Code/Source/EditorModeFeedbackFeatureProcessor.cpp

@@ -28,7 +28,7 @@ namespace AZ
         static Data::Instance<RPI::Material> CreateMaskMaterial()
         {
             const AZStd::string path = "shaders/editormodemask.azmaterial";
-            const auto materialAsset = GetAssetFromPath<RPI::MaterialAsset>(path, Data::AssetLoadBehavior::PreLoad, true);
+            const auto materialAsset = RPI::AssetUtils::LoadCriticalAsset<RPI::MaterialAsset>(path);
             const auto maskMaterial = RPI::Material::FindOrCreate(materialAsset);
             return maskMaterial;
         }

+ 1 - 35
Gems/AtomTressFX/Code/Rendering/HairCommon.cpp

@@ -11,6 +11,7 @@
 
 #include <Atom/RPI.Public/Shader/Shader.h>
 #include <Atom/RPI.Public/Image/StreamingImage.h>
+#include <Atom/RPI.Public/RPIUtils.h>
 #include <Atom/RPI.Reflect/Buffer/BufferAssetView.h>
 
 // Hair specific
@@ -155,41 +156,6 @@ namespace AZ
                 }
                 return rhiImage;
             }
-
-            Data::Instance<RPI::StreamingImage> UtilityClass::LoadStreamingImage(
-                const char* textureFilePath, [[maybe_unused]] const char* sampleName)
-            {
-                Data::AssetId streamingImageAssetId;
-                Data::AssetCatalogRequestBus::BroadcastResult(
-                    streamingImageAssetId, &Data::AssetCatalogRequestBus::Events::GetAssetIdByPath,
-                    textureFilePath, azrtti_typeid<RPI::StreamingImageAsset>(), false);
-
-                if (!streamingImageAssetId.IsValid())
-                {
-                    AZ_Error(sampleName, false, "Failed to get streaming image asset id with path %s", textureFilePath);
-                    return nullptr;
-                }
-
-                auto streamingImageAsset = Data::AssetManager::Instance().GetAsset<RPI::StreamingImageAsset>(
-                    streamingImageAssetId,
-                    Data::AssetLoadBehavior::PreLoad);
-                streamingImageAsset.BlockUntilLoadComplete();
-
-                if (!streamingImageAsset.IsReady())
-                {
-                    AZ_Error(sampleName, false, "Failed to get streaming image asset '%s'", textureFilePath);
-                    return nullptr;
-                }
-
-                auto image = RPI::StreamingImage::FindOrCreate(streamingImageAsset);
-                if (!image)
-                {
-                    AZ_Error(sampleName, false, "Failed to find or create an image instance from image asset '%s'", textureFilePath);
-                    return nullptr;
-                }
-                return image;
-            }
-
         } // namespace Hair
     } // namespace Render
 } // namespace AZ

+ 0 - 4
Gems/AtomTressFX/Code/Rendering/HairCommon.h

@@ -77,10 +77,6 @@ namespace AZ
                     Data::Instance<RPI::ShaderResourceGroup> srg=nullptr
                 );
 
-                static Data::Instance<RPI::StreamingImage> LoadStreamingImage(
-                    const char* textureFilePath, [[maybe_unused]] const char* sampleName
-                );
-
                 static Data::Instance<RHI::ImagePool> CreateImagePool(
                     RHI::ImagePoolDescriptor& imagePoolDesc);
 

+ 3 - 2
Gems/AtomTressFX/Code/Rendering/HairRenderObject.cpp

@@ -7,6 +7,7 @@
  */
 #include <Atom/RHI/Factory.h>
 
+#include <Atom/RPI.Public/RPIUtils.h>
 #include <Atom/RPI.Public/Shader/Shader.h>
 #include <Atom/RPI.Reflect/Buffer/BufferAssetView.h>
 
@@ -632,7 +633,7 @@ namespace AZ
                     {
                         baseAlbedoName = pRenderSettings->m_BaseAlbedoName;
                     }
-                    m_baseAlbedo = UtilityClass::LoadStreamingImage(baseAlbedoName.c_str(), "Hair Gem");
+                    m_baseAlbedo = AZ::RPI::LoadStreamingTexture(baseAlbedoName.c_str());
                 }
                 if (!m_strandAlbedo)
                 {
@@ -641,7 +642,7 @@ namespace AZ
                     {
                         strandAlbedoName = pRenderSettings->m_StrandAlbedoName;
                     }
-                    m_strandAlbedo = UtilityClass::LoadStreamingImage(strandAlbedoName.c_str(), "Hair Gem");
+                    m_strandAlbedo = AZ::RPI::LoadStreamingTexture(strandAlbedoName.c_str());
                 }
 
                 // Bind the Srg resources

+ 26 - 7
Gems/DiffuseProbeGrid/Code/Source/Render/DiffuseProbeGridFeatureProcessor.cpp

@@ -123,16 +123,31 @@ namespace AZ
                 m_visualizationBufferPools->Init(device);
 
                 // load probe visualization model, the BLAS will be created in OnAssetReady()
-                m_visualizationModelAsset = AZ::RPI::AssetUtils::GetAssetByProductPath<AZ::RPI::ModelAsset>(
-                    "Models/DiffuseProbeSphere.azmodel",
-                    AZ::RPI::AssetUtils::TraceLevel::Warning);
 
-                if (!m_visualizationModelAsset.IsReady())
+                // The asset ID for our visualization model has the ID from the lowercased relative path of the source asset
+                // and a sub ID that's generated based on the asset name.
+                // The asset sub id is hardcoded here because the sub id is generated based on the asset name
+                // and the generation method for models currently only exists in ModelAssetBuilderComponent::CreateAssetId().
+                // It isn't exposed to the engine.
+                // Note that there's technically a bug where if the DiffuseProbeSphere asset hasn't been processed by the Asset
+                // Processor by the time this loads, it will load the default missing asset (a cube) instead of the sphere asset
+                // until the next run of the Editor. This could be fixed by using the MeshFeatureProcessor to load the asset and
+                // using ConnectModelChangeEventHandler() to listen for model changes to refresh the visualization.
+                // However, since that will just cause the visualization to change from a cube to a sphere on the first run of the
+                // Editor, handling the edge case might be overkill.
+                Data::AssetId modelAssetId = Data::AssetId(AZ::Uuid::CreateName("models/diffuseprobesphere.fbx"), 268692035);
+                m_visualizationModelAsset =
+                    Data::AssetManager::Instance().GetAsset<AZ::RPI::ModelAsset>(modelAssetId, Data::AssetLoadBehavior::PreLoad);
+
+                if (m_visualizationModelAsset.GetId().IsValid())
                 {
-                    m_visualizationModelAsset.QueueLoad();
-                }
+                    if (!m_visualizationModelAsset.IsReady())
+                    {
+                        m_visualizationModelAsset.QueueLoad();
+                    }
 
-                Data::AssetBus::MultiHandler::BusConnect(m_visualizationModelAsset.GetId());
+                    Data::AssetBus::MultiHandler::BusConnect(m_visualizationModelAsset.GetId());
+                }
             }
 
             // query buffer attachmentId
@@ -891,6 +906,10 @@ namespace AZ
 
             m_visualizationModel = RPI::Model::FindOrCreate(modelAsset);
             AZ_Assert(m_visualizationModel.get(), "Failed to load DiffuseProbeGrid visualization model");
+            if (!m_visualizationModel)
+            {
+                return;
+            }
 
             const AZStd::span<const Data::Instance<RPI::ModelLod>>& modelLods = m_visualizationModel->GetLods();
             AZ_Assert(!modelLods.empty(), "Invalid DiffuseProbeGrid visualization model");

+ 9 - 3
Gems/DiffuseProbeGrid/Code/Source/Render/DiffuseProbeGridPreparePass.cpp

@@ -20,6 +20,9 @@ namespace AZ
 {
     namespace Render
     {
+        constexpr AZStd::string_view DiffuseProbeGridPrepareShaderProductAssetPath =
+            "shaders/diffuseglobalillumination/diffuseprobegridprepare.azshader";
+
         RPI::Ptr<DiffuseProbeGridPreparePass> DiffuseProbeGridPreparePass::Create(const RPI::PassDescriptor& descriptor)
         {
             RPI::Ptr<DiffuseProbeGridPreparePass> pass = aznew DiffuseProbeGridPreparePass(descriptor);
@@ -44,8 +47,7 @@ namespace AZ
         {
             // load shader
             // Note: the shader may not be available on all platforms
-            AZStd::string shaderFilePath = "Shaders/DiffuseGlobalIllumination/DiffuseProbeGridPrepare.azshader";
-            m_shader = RPI::LoadCriticalShader(shaderFilePath);
+            m_shader = RPI::LoadCriticalShader(DiffuseProbeGridPrepareShaderProductAssetPath);
             if (m_shader == nullptr)
             {
                 return;
@@ -64,7 +66,11 @@ namespace AZ
             const auto outcome = RPI::GetComputeShaderNumThreads(m_shader->GetAsset(), m_dispatchArgs);
             if (!outcome.IsSuccess())
             {
-                AZ_Error("PassSystem", false, "[DiffuseProbeGridPreparePass '%s']: Shader '%s' contains invalid numthreads arguments:\n%s", GetPathName().GetCStr(), shaderFilePath.c_str(), outcome.GetError().c_str());
+                AZ_Error("PassSystem", false,
+                    "[DiffuseProbeGridPreparePass '%s']: Shader '" AZ_STRING_FORMAT "' contains invalid numthreads arguments:\n%s",
+                    GetPathName().GetCStr(),
+                    AZ_STRING_ARG(DiffuseProbeGridPrepareShaderProductAssetPath),
+                    outcome.GetError().c_str());
             }
         }
 

+ 3 - 1
Gems/ScriptCanvas/Code/Include/ScriptCanvas/Libraries/Core/MethodOverloaded.cpp

@@ -436,7 +436,9 @@ namespace ScriptCanvas
 
                     if (m_overloadSelection.m_availableIndexes.empty())
                     {
-                        AZ_Warning("ScriptCanvas", false, "Method Overloaded[%s] to an invalid configuration.", GetRawMethodName().c_str());
+                        AZ_Warning(
+                            "ScriptCanvas", false, "Method [%s] is overloaded with an invalid configuration.",
+                            lookUpMethod->m_name.c_str());
 
                         /* Debug information to spew out the non-found configuration. Useful in debugging, and kind of tedious to write.
                            Keeping here as a quick ref commented out, since not useful in the general case.