Browse Source

Merge branch 'stabilization/2110' into Prism/DeleteUpdateGemsUI

nggieber 3 years ago
parent
commit
2913d72d17
62 changed files with 834 additions and 439 deletions
  1. 5 5
      Assets/Editor/Translation/scriptcanvas_en_us.ts
  2. 2 0
      Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.h
  3. 1 1
      Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserFilterModel.cpp
  4. 3 1
      Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.cpp
  5. 16 16
      Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.cpp
  6. 0 1
      Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.h
  7. 5 2
      Code/Tools/ProjectManager/Source/DownloadController.cpp
  8. 3 1
      Code/Tools/ProjectManager/Source/DownloadController.h
  9. 113 73
      Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp
  10. 10 0
      Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h
  11. 1 0
      Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp
  12. 0 1
      Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.cpp
  13. 1 0
      Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/DefaultObjectSrg.azsli
  14. 4 3
      Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/Ibl.azsli
  15. 1 0
      Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Skin/SkinObjectSrg.azsli
  16. 1 1
      Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeRenderInner.azsl
  17. 1 0
      Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeRenderObjectSrg.azsli
  18. 1 1
      Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeRenderOuter.azsl
  19. 2 0
      Gems/Atom/Feature/Common/Code/Include/Atom/Feature/ReflectionProbe/ReflectionProbeFeatureProcessor.h
  20. 2 0
      Gems/Atom/Feature/Common/Code/Include/Atom/Feature/ReflectionProbe/ReflectionProbeFeatureProcessorInterface.h
  21. 4 0
      Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp
  22. 5 1
      Gems/Atom/Feature/Common/Code/Source/PostProcessing/BlendColorGradingLutsPass.cpp
  23. 22 6
      Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbe.cpp
  24. 15 2
      Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbe.h
  25. 18 1
      Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbeFeatureProcessor.cpp
  26. 11 0
      Gems/Atom/RHI/Code/Include/Atom/RHI/SwapChain.h
  27. 7 1
      Gems/Atom/RHI/Code/Source/RHI/FrameGraphAttachmentDatabase.cpp
  28. 66 79
      Gems/Atom/RHI/Code/Source/RHI/SwapChain.cpp
  29. 16 6
      Gems/Atom/RHI/Vulkan/Code/Source/RHI/SwapChain.cpp
  30. 1 0
      Gems/Atom/RHI/Vulkan/Code/Source/RHI/SwapChain.h
  31. 22 3
      Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialSourceData.h
  32. 3 3
      Gems/Atom/RPI/Code/Include/Atom/RPI.Public/RPISystem.h
  33. 1 2
      Gems/Atom/RPI/Code/Include/Atom/RPI.Public/RenderPipeline.h
  34. 5 10
      Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Scene.h
  35. 2 1
      Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/AssetCreator.h
  36. 4 1
      Gems/Atom/RPI/Code/Source/RPI.Edit/Material/LuaMaterialFunctorSourceData.cpp
  37. 160 39
      Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceData.cpp
  38. 15 11
      Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp
  39. 6 0
      Gems/Atom/RPI/Code/Source/RPI.Public/Pass/FullscreenTrianglePass.cpp
  40. 11 9
      Gems/Atom/RPI/Code/Source/RPI.Public/RPISystem.cpp
  41. 1 1
      Gems/Atom/RPI/Code/Source/RPI.Public/RenderPipeline.cpp
  42. 9 8
      Gems/Atom/RPI/Code/Source/RPI.Public/Scene.cpp
  43. 4 0
      Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAsset.cpp
  44. 5 3
      Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/ShaderCollection.cpp
  45. 3 4
      Gems/Atom/TestData/TestData/Materials/SkinTestCases/002_wrinkle_regression_test.material
  46. 8 6
      Gems/Atom/Tools/AtomToolsFramework/Code/Source/Document/AtomToolsDocumentSystemComponent.cpp
  47. 2 2
      Gems/Atom/Tools/AtomToolsFramework/Code/Source/Document/AtomToolsDocumentSystemComponent.h
  48. 34 29
      Gems/Atom/Tools/MaterialEditor/Code/Source/Document/MaterialDocument.cpp
  49. 1 10
      Gems/Atom/Tools/MaterialEditor/Code/Source/Document/MaterialDocument.h
  50. 20 0
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/ReflectionProbe/EditorReflectionProbeComponent.cpp
  51. 2 0
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/ReflectionProbe/EditorReflectionProbeComponent.h
  52. 17 2
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/ReflectionProbe/ReflectionProbeComponentController.cpp
  53. 6 0
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/ReflectionProbe/ReflectionProbeComponentController.h
  54. 6 0
      Gems/LyShine/Code/Editor/Animation/UiAnimViewDialog.cpp
  55. 1 0
      Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli
  56. 5 5
      Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.cpp
  57. 1 1
      Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.h
  58. 82 80
      Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp
  59. 12 0
      Registry/Platform/Mac/bootstrap_overrides.setreg
  60. 2 0
      Templates/PythonToolGem/Template/Code/CMakeLists.txt
  61. 4 1
      Templates/PythonToolGem/Template/gem.json
  62. 43 6
      scripts/build/Jenkins/Jenkinsfile

+ 5 - 5
Assets/Editor/Translation/scriptcanvas_en_us.ts

@@ -62164,7 +62164,7 @@ An Entity can be selected by using the pick button, or by dragging an Entity fro
     <message id="HANDLER_TAGGLOBALNOTIFICATIONBUS_ONENTITYTAGADDED_OUTPUT0_NAME">
         <source>HANDLER_TAGGLOBALNOTIFICATIONBUS_ONENTITYTAGADDED_OUTPUT0_NAME</source>
         <comment>Simple Type: EntityID C++ Type: const EntityId&amp;</comment>
-        <translation type="unfinished">Entity</translation>
+        <translation type="unfinished">EntityID</translation>
     </message>
     <message id="HANDLER_TAGGLOBALNOTIFICATIONBUS_ONENTITYTAGADDED_OUTPUT0_TOOLTIP">
         <source>HANDLER_TAGGLOBALNOTIFICATIONBUS_ONENTITYTAGADDED_OUTPUT0_TOOLTIP</source>
@@ -62202,7 +62202,7 @@ An Entity can be selected by using the pick button, or by dragging an Entity fro
     <message id="HANDLER_TAGGLOBALNOTIFICATIONBUS_ONENTITYTAGREMOVED_OUTPUT0_NAME">
         <source>HANDLER_TAGGLOBALNOTIFICATIONBUS_ONENTITYTAGREMOVED_OUTPUT0_NAME</source>
         <comment>Simple Type: EntityID C++ Type: const EntityId&amp;</comment>
-        <translation type="unfinished">Entity</translation>
+        <translation type="unfinished">EntityId</translation>
     </message>
     <message id="HANDLER_TAGGLOBALNOTIFICATIONBUS_ONENTITYTAGREMOVED_OUTPUT0_TOOLTIP">
         <source>HANDLER_TAGGLOBALNOTIFICATIONBUS_ONENTITYTAGREMOVED_OUTPUT0_TOOLTIP</source>
@@ -81852,7 +81852,7 @@ The element is removed from its current parent and added as a child of the new p
     <message id="HANDLER_SPAWNERCOMPONENTNOTIFICATIONBUS_ONENTITYSPAWNED_OUTPUT1_NAME">
         <source>HANDLER_SPAWNERCOMPONENTNOTIFICATIONBUS_ONENTITYSPAWNED_OUTPUT1_NAME</source>
         <comment>Simple Type: EntityID C++ Type: const EntityId&amp;</comment>
-        <translation type="unfinished">Entity</translation>
+        <translation type="unfinished">EntityID</translation>
     </message>
     <message id="HANDLER_SPAWNERCOMPONENTNOTIFICATIONBUS_ONENTITYSPAWNED_OUTPUT1_TOOLTIP">
         <source>HANDLER_SPAWNERCOMPONENTNOTIFICATIONBUS_ONENTITYSPAWNED_OUTPUT1_TOOLTIP</source>
@@ -89198,7 +89198,7 @@ The element is removed from its current parent and added as a child of the new p
     <message id="HANDLER_ENTITYBUS_ONENTITYACTIVATED_OUTPUT0_NAME">
         <source>HANDLER_ENTITYBUS_ONENTITYACTIVATED_OUTPUT0_NAME</source>
         <comment>Simple Type: EntityID C++ Type: const EntityId&amp;</comment>
-        <translation type="unfinished">Entity</translation>
+        <translation type="unfinished">EntityID</translation>
     </message>
     <message id="HANDLER_ENTITYBUS_ONENTITYACTIVATED_OUTPUT0_TOOLTIP">
         <source>HANDLER_ENTITYBUS_ONENTITYACTIVATED_OUTPUT0_TOOLTIP</source>
@@ -89236,7 +89236,7 @@ The element is removed from its current parent and added as a child of the new p
     <message id="HANDLER_ENTITYBUS_ONENTITYDEACTIVATED_OUTPUT0_NAME">
         <source>HANDLER_ENTITYBUS_ONENTITYDEACTIVATED_OUTPUT0_NAME</source>
         <comment>Simple Type: EntityID C++ Type: const EntityId&amp;</comment>
-        <translation type="unfinished">Entity</translation>
+        <translation type="unfinished">EntityID</translation>
     </message>
     <message id="HANDLER_ENTITYBUS_ONENTITYDEACTIVATED_OUTPUT0_TOOLTIP">
         <source>HANDLER_ENTITYBUS_ONENTITYDEACTIVATED_OUTPUT0_TOOLTIP</source>

+ 2 - 0
Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.h

@@ -6,6 +6,8 @@
  *
  */
 
+#pragma once
+
 #include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
 #include <AzFramework/XcbConnectionManager.h>
 #include <AzFramework/XcbEventHandler.h>

+ 1 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserFilterModel.cpp

@@ -20,7 +20,7 @@ AZ_PUSH_DISABLE_WARNING(4251, "-Wunknown-warning-option")
 AZ_POP_DISABLE_WARNING
 
 AZ_CVAR(
-    bool, ed_useNewAssetBrowserTableView, false, nullptr, AZ::ConsoleFunctorFlags::Null,
+    bool, ed_useNewAssetBrowserTableView, true, nullptr, AZ::ConsoleFunctorFlags::Null,
     "Use the new AssetBrowser TableView for searching assets.");
 namespace AzToolsFramework
 {

+ 3 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.cpp

@@ -606,6 +606,7 @@ namespace AzToolsFramework
 
         AzToolsFramework::ComponentModeFramework::EditorComponentModeNotificationBus::Handler::BusConnect(
             AzToolsFramework::GetEntityContextId());
+        ViewportEditorModeNotificationsBus::Handler::BusConnect(GetEntityContextId());
     }
 
     EntityPropertyEditor::~EntityPropertyEditor()
@@ -618,7 +619,8 @@ namespace AzToolsFramework
         AZ::EntitySystemBus::Handler::BusDisconnect();
         EditorEntityContextNotificationBus::Handler::BusDisconnect();
         AzToolsFramework::ComponentModeFramework::EditorComponentModeNotificationBus::Handler::BusDisconnect();
-
+        ViewportEditorModeNotificationsBus::Handler::BusDisconnect();
+        
         for (auto& entityId : m_overrideSelectedEntityIds)
         {
             DisconnectFromEntityBuses(entityId);

+ 16 - 16
Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.cpp

@@ -243,7 +243,7 @@ void AssetProcessorManagerTest::SetUp()
     m_mockApplicationManager->BusConnect();
 
     m_assetProcessorManager.reset(new AssetProcessorManager_Test(m_config.get()));
-    m_assertAbsorber.Clear();
+    m_errorAbsorber->Clear();
 
     m_isIdling = false;
 
@@ -334,9 +334,9 @@ TEST_F(AssetProcessorManagerTest, UnitTestForGettingJobInfoBySourceUUIDSuccess)
     EXPECT_STRCASEEQ(relFileName.toUtf8().data(), response.m_jobList[0].m_sourceFile.c_str());
     EXPECT_STRCASEEQ(tempPath.filePath("subfolder1").toUtf8().data(), response.m_jobList[0].m_watchFolder.c_str());
 
-    ASSERT_EQ(m_assertAbsorber.m_numWarningsAbsorbed, 0);
-    ASSERT_EQ(m_assertAbsorber.m_numErrorsAbsorbed, 0);
-    ASSERT_EQ(m_assertAbsorber.m_numAssertsAbsorbed, 0);
+    ASSERT_EQ(m_errorAbsorber->m_numWarningsAbsorbed, 0);
+    ASSERT_EQ(m_errorAbsorber->m_numErrorsAbsorbed, 0);
+    ASSERT_EQ(m_errorAbsorber->m_numAssertsAbsorbed, 0);
 }
 
 TEST_F(AssetProcessorManagerTest, WarningsAndErrorsReported_SuccessfullySavedToDatabase)
@@ -388,9 +388,9 @@ TEST_F(AssetProcessorManagerTest, WarningsAndErrorsReported_SuccessfullySavedToD
     ASSERT_EQ(response.m_jobList[0].m_warningCount, 11);
     ASSERT_EQ(response.m_jobList[0].m_errorCount, 22);
 
-    ASSERT_EQ(m_assertAbsorber.m_numWarningsAbsorbed, 0);
-    ASSERT_EQ(m_assertAbsorber.m_numErrorsAbsorbed, 0);
-    ASSERT_EQ(m_assertAbsorber.m_numAssertsAbsorbed, 0);
+    ASSERT_EQ(m_errorAbsorber->m_numWarningsAbsorbed, 0);
+    ASSERT_EQ(m_errorAbsorber->m_numErrorsAbsorbed, 0);
+    ASSERT_EQ(m_errorAbsorber->m_numAssertsAbsorbed, 0);
 }
 
 
@@ -1312,8 +1312,8 @@ void PathDependencyTest::SetUp()
 
 void PathDependencyTest::TearDown()
 {
-    ASSERT_EQ(m_assertAbsorber.m_numAssertsAbsorbed, 0);
-    ASSERT_EQ(m_assertAbsorber.m_numErrorsAbsorbed, 0);
+    ASSERT_EQ(m_errorAbsorber->m_numAssertsAbsorbed, 0);
+    ASSERT_EQ(m_errorAbsorber->m_numErrorsAbsorbed, 0);
 
     AssetProcessorManagerTest::TearDown();
 }
@@ -1617,7 +1617,7 @@ TEST_F(PathDependencyTest, AssetProcessed_Impl_SelfReferrentialProductDependency
     mainFile.m_products.push_back(productAssetId);
 
     // tell the APM that the asset has been processed and allow it to bubble through its event queue:
-    m_assertAbsorber.Clear();
+    m_errorAbsorber->Clear();
     m_assetProcessorManager->AssetProcessed(jobDetails.m_jobEntry, processJobResponse);
     ASSERT_TRUE(BlockUntilIdle(5000));
 
@@ -1627,8 +1627,8 @@ TEST_F(PathDependencyTest, AssetProcessed_Impl_SelfReferrentialProductDependency
     ASSERT_TRUE(dependencyContainer.empty());
 
     // We are testing 2 different dependencies, so we should get 2 warnings
-    ASSERT_EQ(m_assertAbsorber.m_numWarningsAbsorbed, 2);
-    m_assertAbsorber.Clear();
+    ASSERT_EQ(m_errorAbsorber->m_numWarningsAbsorbed, 2);
+    m_errorAbsorber->Clear();
 }
 
 // This test shows the process of deferring resolution of a path dependency works.
@@ -1945,8 +1945,8 @@ TEST_F(PathDependencyTest, WildcardDependencies_ExcludePathsExisting_ResolveCorr
     );
 
     // Test asset PrimaryFile1 has 4 conflict dependencies
-    ASSERT_EQ(m_assertAbsorber.m_numErrorsAbsorbed, 4);
-    m_assertAbsorber.Clear();
+    ASSERT_EQ(m_errorAbsorber->m_numErrorsAbsorbed, 4);
+    m_errorAbsorber->Clear();
 }
 
 TEST_F(PathDependencyTest, WildcardDependencies_Deferred_ResolveCorrectly)
@@ -2093,8 +2093,8 @@ TEST_F(PathDependencyTest, WildcardDependencies_ExcludedPathDeferred_ResolveCorr
     // Test asset PrimaryFile1 has 4 conflict dependencies
     // After test assets dep2 and dep3 are processed,
     // another 2 errors will be raised because of the confliction
-    ASSERT_EQ(m_assertAbsorber.m_numErrorsAbsorbed, 6);
-    m_assertAbsorber.Clear();
+    ASSERT_EQ(m_errorAbsorber->m_numErrorsAbsorbed, 6);
+    m_errorAbsorber->Clear();
 }
 
 void PathDependencyTest::RunWildcardTest(bool useCorrectDatabaseSeparator, AssetBuilderSDK::ProductPathDependencyType pathDependencyType, bool buildDependenciesFirst)

+ 0 - 1
Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.h

@@ -58,7 +58,6 @@ protected:
     AZStd::unique_ptr<AssetProcessorManager_Test> m_assetProcessorManager;
     AZStd::unique_ptr<AssetProcessor::MockApplicationManager> m_mockApplicationManager;
     AZStd::unique_ptr<AssetProcessor::PlatformConfiguration> m_config;
-    UnitTestUtils::AssertAbsorber m_assertAbsorber; // absorb asserts/warnings/errors so that the unit test output is not cluttered
     QString m_gameName;
     QDir m_normalizedCacheRootDir;
     AZStd::atomic_bool m_isIdling;

+ 5 - 2
Code/Tools/ProjectManager/Source/DownloadController.cpp

@@ -41,9 +41,11 @@ namespace O3DE::ProjectManager
     void DownloadController::AddGemDownload(const QString& gemName)
     {
         m_gemNames.push_back(gemName);
+        emit GemDownloadAdded(gemName);
+
         if (m_gemNames.size() == 1)
         {
-            m_worker->SetGemToDownload(m_gemNames[0], false);
+            m_worker->SetGemToDownload(m_gemNames.front(), false);
             m_workerThread.start();
         }
     }
@@ -62,6 +64,7 @@ namespace O3DE::ProjectManager
             else
             {
                 m_gemNames.erase(findResult);
+                emit GemDownloadRemoved(gemName);
             }
         }
     }
@@ -69,7 +72,7 @@ namespace O3DE::ProjectManager
     void DownloadController::UpdateUIProgress(int progress)
     {
         m_lastProgress = progress;
-        emit GemDownloadProgress(progress);
+        emit GemDownloadProgress(m_gemNames.front(), progress);
     }
 
     void DownloadController::HandleResults(const QString& result)

+ 3 - 1
Code/Tools/ProjectManager/Source/DownloadController.h

@@ -59,7 +59,9 @@ namespace O3DE::ProjectManager
     signals:
         void StartGemDownload(const QString& gemName);
         void Done(const QString& gemName, bool success = true);
-        void GemDownloadProgress(int percentage);
+        void GemDownloadAdded(const QString& gemName);
+        void GemDownloadRemoved(const QString& gemName);
+        void GemDownloadProgress(const QString& gemName, int percentage);
 
     private:
         DownloadWorker* m_worker;

+ 113 - 73
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp

@@ -30,6 +30,7 @@ namespace O3DE::ProjectManager
         m_layout->setMargin(5);
         m_layout->setAlignment(Qt::AlignTop);
         setLayout(m_layout);
+        setMinimumHeight(400);
 
         QHBoxLayout* hLayout = new QHBoxLayout();
 
@@ -119,6 +120,12 @@ namespace O3DE::ProjectManager
         setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog);
     }
 
+    CartOverlayWidget::~CartOverlayWidget()
+    {
+        // disconnect from all download controller signals
+        disconnect(m_downloadController, nullptr, this, nullptr);
+    }
+
     void CartOverlayWidget::CreateGemSection(const QString& singularTitle, const QString& pluralTitle, GetTagIndicesCallback getTagIndices)
     {
         QWidget* widget = new QWidget();
@@ -162,13 +169,13 @@ namespace O3DE::ProjectManager
 
     void CartOverlayWidget::CreateDownloadSection()
     {
-        QWidget* widget = new QWidget();
-        widget->setFixedWidth(s_width);
-        m_layout->addWidget(widget);
+        m_downloadSectionWidget = new QWidget();
+        m_downloadSectionWidget->setFixedWidth(s_width);
+        m_layout->addWidget(m_downloadSectionWidget);
 
         QVBoxLayout* layout = new QVBoxLayout();
         layout->setAlignment(Qt::AlignTop);
-        widget->setLayout(layout);
+        m_downloadSectionWidget->setLayout(layout);
 
         QLabel* titleLabel = new QLabel();
         titleLabel->setObjectName("GemCatalogCartOverlaySectionLabel");
@@ -187,88 +194,121 @@ namespace O3DE::ProjectManager
         QLabel* processingQueueLabel = new QLabel("Processing Queue");
         gemDownloadLayout->addWidget(processingQueueLabel);
 
-        QWidget* downloadingItemWidget = new QWidget();
-        downloadingItemWidget->setObjectName("GemCatalogCartOverlayGemDownloadBG");
-        gemDownloadLayout->addWidget(downloadingItemWidget);
+        m_downloadingListWidget = new QWidget();
+        m_downloadingListWidget->setObjectName("GemCatalogCartOverlayGemDownloadBG");
+        gemDownloadLayout->addWidget(m_downloadingListWidget);
         QVBoxLayout* downloadingItemLayout = new QVBoxLayout();
         downloadingItemLayout->setAlignment(Qt::AlignTop);
-        downloadingItemWidget->setLayout(downloadingItemLayout);
+        m_downloadingListWidget->setLayout(downloadingItemLayout);
 
-        auto update = [=](int downloadProgress)
+        QLabel* downloadsInProgessLabel = new QLabel("");
+        downloadsInProgessLabel->setObjectName("NumDownloadsInProgressLabel");
+        downloadingItemLayout->addWidget(downloadsInProgessLabel);
+
+        if (m_downloadController->IsDownloadQueueEmpty())
+        {
+            m_downloadSectionWidget->hide();
+        }
+        else
         {
-            if (m_downloadController->IsDownloadQueueEmpty())
+            // Setup gem download rows for gems that are already in the queue
+            const AZStd::vector<QString>& downloadQueue = m_downloadController->GetDownloadQueue();
+
+            for (const QString& gemName : downloadQueue)
             {
-                widget->hide();
+                GemDownloadAdded(gemName);
             }
-            else
-            {
-                widget->setUpdatesEnabled(false);
-                // remove items
-                QLayoutItem* layoutItem = nullptr;
-                while ((layoutItem = downloadingItemLayout->takeAt(0)) != nullptr)
-                {
-                    if (layoutItem->layout())
-                    {
-                        // Gem info row
-                        QLayoutItem* rowLayoutItem = nullptr;
-                        while ((rowLayoutItem = layoutItem->layout()->takeAt(0)) != nullptr)
-                        {
-                            rowLayoutItem->widget()->deleteLater();
-                        }
-                        layoutItem->layout()->deleteLater();
-                    }
-                    if (layoutItem->widget())
-                    {
-                        layoutItem->widget()->deleteLater();
-                    }
-                }
-
-                // Setup gem download rows
-                const AZStd::vector<QString>& downloadQueue = m_downloadController->GetDownloadQueue();
-
-                QLabel* downloadsInProgessLabel = new QLabel("");
-                downloadsInProgessLabel->setText(
-                    QString("%1 %2").arg(downloadQueue.size()).arg(downloadQueue.size() == 1 ? tr("download in progress...") : tr("downloads in progress...")));
-                downloadingItemLayout->addWidget(downloadsInProgessLabel);
-
-                for (int downloadingGemNumber = 0; downloadingGemNumber < downloadQueue.size(); ++downloadingGemNumber)
-                {
-                    QHBoxLayout* nameProgressLayout = new QHBoxLayout();
-
-                    const QString& gemName = downloadQueue[downloadingGemNumber];
-                    TagWidget* newTag = new TagWidget({gemName, gemName});
-                    nameProgressLayout->addWidget(newTag);
+        }
 
-                    QLabel* progress = new QLabel(downloadingGemNumber == 0? QString("%1%").arg(downloadProgress) : tr("Queued"));
-                    nameProgressLayout->addWidget(progress);
+        // connect to download controller data changed
+        connect(m_downloadController, &DownloadController::GemDownloadAdded, this, &CartOverlayWidget::GemDownloadAdded);
+        connect(m_downloadController, &DownloadController::GemDownloadRemoved, this, &CartOverlayWidget::GemDownloadRemoved);
+        connect(m_downloadController, &DownloadController::GemDownloadProgress, this, &CartOverlayWidget::GemDownloadProgress);
+        connect(m_downloadController, &DownloadController::Done, this, &CartOverlayWidget::GemDownloadComplete);
+    }
 
-                    QSpacerItem* spacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum);
-                    nameProgressLayout->addSpacerItem(spacer);
+    void CartOverlayWidget::GemDownloadAdded(const QString& gemName)
+    {
+        // Containing widget for the current download item
+        QWidget* newGemDownloadWidget = new QWidget();
+        newGemDownloadWidget->setObjectName(gemName);
+        QVBoxLayout* downloadingGemLayout = new QVBoxLayout(newGemDownloadWidget);
+        newGemDownloadWidget->setLayout(downloadingGemLayout);
+
+        // Gem name, progress string, cancel
+        QHBoxLayout* nameProgressLayout = new QHBoxLayout(newGemDownloadWidget);
+        TagWidget* newTag = new TagWidget({gemName, gemName}, newGemDownloadWidget);
+        nameProgressLayout->addWidget(newTag);
+        QLabel* progress = new QLabel(tr("Queued"), newGemDownloadWidget);
+        progress->setObjectName("DownloadProgressLabel");
+        nameProgressLayout->addWidget(progress);
+        nameProgressLayout->addStretch();
+        QLabel* cancelText = new QLabel(tr("<a href=\"%1\">Cancel</a>").arg(gemName), newGemDownloadWidget);
+        cancelText->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
+        connect(cancelText, &QLabel::linkActivated, this, &CartOverlayWidget::OnCancelDownloadActivated);
+        nameProgressLayout->addWidget(cancelText);
+        downloadingGemLayout->addLayout(nameProgressLayout);
+
+        // Progress bar
+        QProgressBar* downloadProgessBar = new QProgressBar(newGemDownloadWidget);
+        downloadProgessBar->setObjectName("DownloadProgressBar");
+        downloadingGemLayout->addWidget(downloadProgessBar);
+        downloadProgessBar->setValue(0);
+
+        m_downloadingListWidget->layout()->addWidget(newGemDownloadWidget);
+
+        const AZStd::vector<QString>& downloadQueue = m_downloadController->GetDownloadQueue();
+        QLabel* numDownloads = m_downloadingListWidget->findChild<QLabel*>("NumDownloadsInProgressLabel");
+        numDownloads->setText(QString("%1 %2")
+                                  .arg(downloadQueue.size())
+                                  .arg(downloadQueue.size() == 1 ? tr("download in progress...") : tr("downloads in progress...")));
+
+        m_downloadingListWidget->show();
+    }
 
-                    QLabel* cancelText = new QLabel(QString("<a href=\"%1\">Cancel</a>").arg(gemName));
-                    cancelText->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
-                    connect(cancelText, &QLabel::linkActivated, this, &CartOverlayWidget::OnCancelDownloadActivated);
-                    nameProgressLayout->addWidget(cancelText);
-                    downloadingItemLayout->addLayout(nameProgressLayout);
+    void CartOverlayWidget::GemDownloadRemoved(const QString& gemName)
+    {
+        QWidget* gemToRemove = m_downloadingListWidget->findChild<QWidget*>(gemName);
+        if (gemToRemove)
+        {
+            gemToRemove->deleteLater();
+        }
 
-                    QProgressBar* downloadProgessBar = new QProgressBar();
-                    downloadingItemLayout->addWidget(downloadProgessBar);
-                    downloadProgessBar->setValue(downloadingGemNumber == 0 ? downloadProgress : 0);
-                }
+        if (m_downloadController->IsDownloadQueueEmpty())
+        {
+            m_downloadSectionWidget->hide();
+        }
+        else
+        {
+            size_t downloadQueueSize = m_downloadController->GetDownloadQueue().size();
+            QLabel* numDownloads = m_downloadingListWidget->findChild<QLabel*>("NumDownloadsInProgressLabel");
+            numDownloads->setText(QString("%1 %2")
+                                      .arg(downloadQueueSize)
+                                      .arg(downloadQueueSize == 1 ? tr("download in progress...") : tr("downloads in progress...")));
+        }
+    }
 
-                widget->setUpdatesEnabled(true);
-                widget->show();
+    void CartOverlayWidget::GemDownloadProgress(const QString& gemName, int percentage)
+    {
+        QWidget* gemToUpdate = m_downloadingListWidget->findChild<QWidget*>(gemName);
+        if (gemToUpdate)
+        {
+            QLabel* progressLabel = gemToUpdate->findChild<QLabel*>("DownloadProgressLabel");
+            if (progressLabel)
+            {
+                progressLabel->setText(QString("%1%").arg(percentage));
             }
-        };
+            QProgressBar* progressBar = gemToUpdate->findChild<QProgressBar*>("DownloadProgressBar");
+            if (progressBar)
+            {
+                progressBar->setValue(percentage);
+            }
+        }
+    }
 
-        auto downloadEnded = [=](const QString& /*gemName*/, bool /*success*/)
-        {
-            update(0); // update the list to remove the gem that has finished
-        };
-        // connect to download controller data changed
-        connect(m_downloadController, &DownloadController::GemDownloadProgress, this, update);
-        connect(m_downloadController, &DownloadController::Done, this, downloadEnded);
-        update(0);
+    void CartOverlayWidget::GemDownloadComplete(const QString& gemName, bool /*success*/)
+    {
+        GemDownloadRemoved(gemName); // update the list to remove the gem that has finished
     }
 
     QVector<Tag> CartOverlayWidget::GetTagsFromModelIndices(const QVector<QModelIndex>& gems) const

+ 10 - 0
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h

@@ -34,6 +34,13 @@ namespace O3DE::ProjectManager
 
     public:
         CartOverlayWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent = nullptr);
+        ~CartOverlayWidget();
+
+    public slots:
+        void GemDownloadAdded(const QString& gemName);
+        void GemDownloadRemoved(const QString& gemName);
+        void GemDownloadProgress(const QString& gemName, int percentage);
+        void GemDownloadComplete(const QString& gemName, bool success);
 
     private:
         QVector<Tag> GetTagsFromModelIndices(const QVector<QModelIndex>& gems) const;
@@ -47,6 +54,9 @@ namespace O3DE::ProjectManager
         GemModel* m_gemModel = nullptr;
         DownloadController* m_downloadController = nullptr;
 
+        QWidget* m_downloadSectionWidget = nullptr;
+        QWidget* m_downloadingListWidget = nullptr;
+
         inline constexpr static int s_width = 240;
     };
 

+ 1 - 0
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp

@@ -268,6 +268,7 @@ namespace O3DE::ProjectManager
                 if (added && GemModel::GetDownloadStatus(modelIndex) == GemInfo::DownloadStatus::NotDownloaded)
                 {
                     m_downloadController->AddGemDownload(GemModel::GetName(modelIndex));
+                    GemModel::SetDownloadStatus(*m_proxyModel, m_proxyModel->mapFromSource(modelIndex), GemInfo::DownloadStatus::Downloading);
                 }
             }
 

+ 0 - 1
Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.cpp

@@ -221,7 +221,6 @@ namespace O3DE::ProjectManager
         ResetGemStatusFilter();
         ResetGemOriginFilter();
         ResetTypeFilter();
-        ResetPlatformFilter();
         ResetFeatureFilter();
     }
 

+ 1 - 0
Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/DefaultObjectSrg.azsli

@@ -37,6 +37,7 @@ ShaderResourceGroup ObjectSrg : SRG_PerObject
         float m_padding;
         bool m_useReflectionProbe;
         bool m_useParallaxCorrection;
+        float m_exposure;
     };
 
     ReflectionProbeData m_reflectionProbeData;

+ 4 - 3
Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PBR/Lights/Ibl.azsli

@@ -85,12 +85,12 @@ void ApplyIBL(Surface surface, inout LightingData lightingData)
 
     if(useIbl)
     {
-        float iblExposureFactor = pow(2.0, SceneSrg::m_iblExposure);
+        float globalIblExposure = pow(2.0, SceneSrg::m_iblExposure);
         
         if(useDiffuseIbl)
         {
             float3 iblDiffuse = GetIblDiffuse(surface.normal, surface.albedo, lightingData.diffuseResponse);
-            lightingData.diffuseLighting += (iblDiffuse * iblExposureFactor * lightingData.diffuseAmbientOcclusion);
+            lightingData.diffuseLighting += (iblDiffuse * globalIblExposure * lightingData.diffuseAmbientOcclusion);
         }
 
         if(useSpecularIbl)
@@ -116,7 +116,8 @@ void ApplyIBL(Surface surface, inout LightingData lightingData)
                 iblSpecular = iblSpecular * (1.0 - clearCoatResponse) * (1.0 - clearCoatResponse) + clearCoatIblSpecular;
             }
 
-            lightingData.specularLighting += (iblSpecular * iblExposureFactor);
+            float exposure = ObjectSrg::m_reflectionProbeData.m_useReflectionProbe ? pow(2.0, ObjectSrg::m_reflectionProbeData.m_exposure) : globalIblExposure;
+            lightingData.specularLighting += (iblSpecular * exposure);
         }
     }
 }

+ 1 - 0
Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Skin/SkinObjectSrg.azsli

@@ -46,6 +46,7 @@ ShaderResourceGroup ObjectSrg : SRG_PerObject
         float m_padding;
         bool m_useReflectionProbe;
         bool m_useParallaxCorrection;
+        float m_exposure;
     };
 
     ReflectionProbeData m_reflectionProbeData;

+ 1 - 1
Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeRenderInner.azsl

@@ -81,7 +81,7 @@ PSOutput MainPS(VSOutput IN, in uint sampleIndex : SV_SampleIndex)
     }
 
     // apply exposure setting
-    specular *= pow(2.0, SceneSrg::m_iblExposure);
+    specular *= pow(2.0, ObjectSrg::m_exposure);
 
     PSOutput OUT;
     OUT.m_color = float4(specular, 1.0f);

+ 1 - 0
Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeRenderObjectSrg.azsli

@@ -17,6 +17,7 @@ ShaderResourceGroup ObjectSrg : SRG_PerObject
     float3 m_outerObbHalfLengths;
     float3 m_innerObbHalfLengths;
     bool m_useParallaxCorrection;
+    float m_exposure;
     TextureCube m_reflectionCubeMap;
 
     float4x4 GetWorldMatrix()

+ 1 - 1
Gems/Atom/Feature/Common/Assets/Shaders/Reflections/ReflectionProbeRenderOuter.azsl

@@ -104,7 +104,7 @@ PSOutput MainPS(VSOutput IN, in uint sampleIndex : SV_SampleIndex)
     blendWeight /= max(1.0f, blendWeightAllProbes);
 
     // apply exposure setting
-    specular *= pow(2.0, SceneSrg::m_iblExposure);
+    specular *= pow(2.0, ObjectSrg::m_exposure);
 
     // apply blend weight for additive blending
     specular *= blendWeight;

+ 2 - 0
Gems/Atom/Feature/Common/Code/Include/Atom/Feature/ReflectionProbe/ReflectionProbeFeatureProcessor.h

@@ -39,6 +39,8 @@ namespace AZ
             bool IsCubeMapReferenced(const AZStd::string& relativePath) override;
             bool IsValidProbeHandle(const ReflectionProbeHandle& probe) const override { return (probe.get() != nullptr); }
             void ShowProbeVisualization(const ReflectionProbeHandle& probe, bool showVisualization) override;
+            void SetRenderExposure(const ReflectionProbeHandle& probe, float renderExposure) override;
+            void SetBakeExposure(const ReflectionProbeHandle& probe, float bakeExposure) override;
 
             // FeatureProcessor overrides
             void Activate() override;

+ 2 - 0
Gems/Atom/Feature/Common/Code/Include/Atom/Feature/ReflectionProbe/ReflectionProbeFeatureProcessorInterface.h

@@ -50,6 +50,8 @@ namespace AZ
             virtual bool IsCubeMapReferenced(const AZStd::string& relativePath) = 0;
             virtual bool IsValidProbeHandle(const ReflectionProbeHandle& probe) const = 0;
             virtual void ShowProbeVisualization(const ReflectionProbeHandle& probe, bool showVisualization) = 0;
+            virtual void SetRenderExposure(const ReflectionProbeHandle& probe, float renderExposure) = 0;
+            virtual void SetBakeExposure(const ReflectionProbeHandle& probe, float bakeExposure) = 0;
         };
     } // namespace Render
 } // namespace AZ

+ 4 - 0
Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp

@@ -1178,6 +1178,9 @@ namespace AZ
                 AZ::RHI::ShaderInputConstantIndex useParallaxCorrectionConstantIndex = m_shaderResourceGroup->FindShaderInputConstantIndex(Name("m_reflectionProbeData.m_useParallaxCorrection"));
                 AZ_Error("MeshDataInstance", useParallaxCorrectionConstantIndex.IsValid(), "Failed to find ReflectionProbe constant index");
 
+                AZ::RHI::ShaderInputConstantIndex exposureConstantIndex = m_shaderResourceGroup->FindShaderInputConstantIndex(Name("m_reflectionProbeData.m_exposure"));
+                AZ_Error("MeshDataInstance", exposureConstantIndex.IsValid(), "Failed to find ReflectionProbe constant index");
+
                 // retrieve probe cubemap index
                 Name reflectionCubeMapImageName = Name("m_reflectionProbeCubeMap");
                 RHI::ShaderInputImageIndex reflectionCubeMapImageIndex = m_shaderResourceGroup->FindShaderInputImageIndex(reflectionCubeMapImageName);
@@ -1198,6 +1201,7 @@ namespace AZ
                     m_shaderResourceGroup->SetConstant(innerObbHalfLengthsConstantIndex, reflectionProbes[0]->GetInnerObbWs().GetHalfLengths());
                     m_shaderResourceGroup->SetConstant(useReflectionProbeConstantIndex, true);
                     m_shaderResourceGroup->SetConstant(useParallaxCorrectionConstantIndex, reflectionProbes[0]->GetUseParallaxCorrection());
+                    m_shaderResourceGroup->SetConstant(exposureConstantIndex, reflectionProbes[0]->GetRenderExposure());
 
                     m_shaderResourceGroup->SetImage(reflectionCubeMapImageIndex, reflectionProbes[0]->GetCubeMapImage());
                 }

+ 5 - 1
Gems/Atom/Feature/Common/Code/Source/PostProcessing/BlendColorGradingLutsPass.cpp

@@ -46,7 +46,11 @@ namespace AZ
 
         void BlendColorGradingLutsPass::InitializeShaderVariant()
         {
-            AZ_Assert(m_shader != nullptr, "BlendColorGradingLutsPass %s has a null shader when calling InitializeShaderVariant.", GetPathName().GetCStr());
+            if (m_shader == nullptr)
+            {
+                AZ_Assert(false, "BlendColorGradingLutsPass %s has a null shader when calling InitializeShaderVariant.", GetPathName().GetCStr());
+                return;
+            }
 
             // Total variations is MaxBlendLuts plus one for the fallback case that none of the LUTs are found,
             // and hence zero LUTs are blended resulting in an identity LUT.

+ 22 - 6
Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbe.cpp

@@ -120,15 +120,17 @@ namespace AZ
                     m_scene->RemoveRenderPipeline(m_environmentCubeMapPipelineId);
                     m_environmentCubeMapPass = nullptr;
 
-                    // restore exposure
-                    sceneSrg->SetConstant(m_iblExposureConstantIndex, m_previousExposure);
+                    // restore exposures
+                    sceneSrg->SetConstant(m_globalIblExposureConstantIndex, m_previousGlobalIblExposure);
+                    sceneSrg->SetConstant(m_skyBoxExposureConstantIndex, m_previousSkyBoxExposure);
 
                     m_buildingCubeMap = false;
                 }
                 else
                 {
-                    // set exposure to 0.0 while baking the cubemap
-                    sceneSrg->SetConstant(m_iblExposureConstantIndex, 0.0f);
+                    // set exposures to the user specified value while baking the cubemap
+                    sceneSrg->SetConstant(m_globalIblExposureConstantIndex, m_bakeExposure);
+                    sceneSrg->SetConstant(m_skyBoxExposureConstantIndex, m_bakeExposure);
                 }
             }
 
@@ -162,6 +164,7 @@ namespace AZ
                 m_renderOuterSrg->SetConstant(m_reflectionRenderData->m_outerObbHalfLengthsRenderConstantIndex, m_outerObbWs.GetHalfLengths());
                 m_renderOuterSrg->SetConstant(m_reflectionRenderData->m_innerObbHalfLengthsRenderConstantIndex, m_innerObbWs.GetHalfLengths());
                 m_renderOuterSrg->SetConstant(m_reflectionRenderData->m_useParallaxCorrectionRenderConstantIndex, m_useParallaxCorrection);
+                m_renderOuterSrg->SetConstant(m_reflectionRenderData->m_exposureConstantIndex, m_renderExposure);
                 m_renderOuterSrg->SetImage(m_reflectionRenderData->m_reflectionCubeMapRenderImageIndex, m_cubeMapImage);
                 m_renderOuterSrg->Compile();
 
@@ -172,6 +175,7 @@ namespace AZ
                 m_renderInnerSrg->SetConstant(m_reflectionRenderData->m_outerObbHalfLengthsRenderConstantIndex, m_outerObbWs.GetHalfLengths());
                 m_renderInnerSrg->SetConstant(m_reflectionRenderData->m_innerObbHalfLengthsRenderConstantIndex, m_innerObbWs.GetHalfLengths());
                 m_renderInnerSrg->SetConstant(m_reflectionRenderData->m_useParallaxCorrectionRenderConstantIndex, m_useParallaxCorrection);
+                m_renderInnerSrg->SetConstant(m_reflectionRenderData->m_exposureConstantIndex, m_renderExposure);
                 m_renderInnerSrg->SetImage(m_reflectionRenderData->m_reflectionCubeMapRenderImageIndex, m_cubeMapImage);
                 m_renderInnerSrg->Compile();
 
@@ -303,9 +307,10 @@ namespace AZ
             const RPI::Ptr<RPI::ParentPass>& rootPass = environmentCubeMapPipeline->GetRootPass();
             rootPass->AddChild(m_environmentCubeMapPass);
 
-            // store the current IBL exposure value
+            // store the current IBL exposure values
             Data::Instance<RPI::ShaderResourceGroup> sceneSrg = m_scene->GetShaderResourceGroup();
-            m_previousExposure = sceneSrg->GetConstant<float>(m_iblExposureConstantIndex);
+            m_previousGlobalIblExposure = sceneSrg->GetConstant<float>(m_globalIblExposureConstantIndex);
+            m_previousSkyBoxExposure = sceneSrg->GetConstant<float>(m_skyBoxExposureConstantIndex);
 
             m_scene->AddRenderPipeline(environmentCubeMapPipeline);
         }
@@ -326,6 +331,17 @@ namespace AZ
             m_meshFeatureProcessor->SetVisible(m_visualizationMeshHandle, showVisualization);
         }
 
+        void ReflectionProbe::SetRenderExposure(float renderExposure)
+        {
+            m_renderExposure = renderExposure;
+            m_updateSrg = true;
+        }
+
+        void ReflectionProbe::SetBakeExposure(float bakeExposure)
+        {
+            m_bakeExposure = bakeExposure;
+        }
+
         const RHI::DrawPacket* ReflectionProbe::BuildDrawPacket(
             const Data::Instance<RPI::ShaderResourceGroup>& srg,
             const RPI::Ptr<RPI::PipelineStateForDraw>& pipelineState,

+ 15 - 2
Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbe.h

@@ -61,6 +61,7 @@ namespace AZ
             RHI::ShaderInputNameIndex m_outerObbHalfLengthsRenderConstantIndex = "m_outerObbHalfLengths";
             RHI::ShaderInputNameIndex m_innerObbHalfLengthsRenderConstantIndex = "m_innerObbHalfLengths";
             RHI::ShaderInputNameIndex m_useParallaxCorrectionRenderConstantIndex = "m_useParallaxCorrection";
+            RHI::ShaderInputNameIndex m_exposureConstantIndex = "m_exposure";
             RHI::ShaderInputNameIndex m_reflectionCubeMapRenderImageIndex = "m_reflectionCubeMap";
         };
 
@@ -106,6 +107,14 @@ namespace AZ
             // enables or disables rendering of the visualization sphere
             void ShowVisualization(bool showVisualization);
 
+            // the exposure to use when rendering meshes with this probe's cubemap
+            void SetRenderExposure(float renderExposure);
+            float GetRenderExposure() const { return m_renderExposure; }
+
+            // the exposure to use when baking the probe cubemap
+            void SetBakeExposure(float bakeExposure);
+            float GetBakeExposure() const { return m_bakeExposure; }
+
         private:
 
             AZ_DISABLE_COPY_MOVE(ReflectionProbe);
@@ -157,6 +166,8 @@ namespace AZ
             RHI::ConstPtr<RHI::DrawPacket> m_blendWeightDrawPacket;
             RHI::ConstPtr<RHI::DrawPacket> m_renderOuterDrawPacket;
             RHI::ConstPtr<RHI::DrawPacket> m_renderInnerDrawPacket;
+            float m_renderExposure = 0.0f;
+            float m_bakeExposure = 0.0f;
             bool m_updateSrg = false;
 
             const RHI::DrawItemSortKey InvalidSortKey = static_cast<RHI::DrawItemSortKey>(-1);
@@ -169,8 +180,10 @@ namespace AZ
             RPI::Ptr<RPI::EnvironmentCubeMapPass> m_environmentCubeMapPass = nullptr;
             RPI::RenderPipelineId m_environmentCubeMapPipelineId;
             BuildCubeMapCallback m_callback;
-            RHI::ShaderInputNameIndex m_iblExposureConstantIndex = "m_iblExposure";
-            float m_previousExposure = 0.0f;
+            RHI::ShaderInputNameIndex m_globalIblExposureConstantIndex = "m_iblExposure";
+            RHI::ShaderInputNameIndex m_skyBoxExposureConstantIndex = "m_cubemapExposure";
+            float m_previousGlobalIblExposure = 0.0f;
+            float m_previousSkyBoxExposure = 0.0f;
             bool m_buildingCubeMap = false;
         };
 

+ 18 - 1
Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbeFeatureProcessor.cpp

@@ -283,6 +283,18 @@ namespace AZ
             probe->ShowVisualization(showVisualization);
         }
 
+        void ReflectionProbeFeatureProcessor::SetRenderExposure(const ReflectionProbeHandle& probe, float renderExposure)
+        {
+            AZ_Assert(probe.get(), "SetRenderExposure called with an invalid handle");
+            probe->SetRenderExposure(renderExposure);
+        }
+
+        void ReflectionProbeFeatureProcessor::SetBakeExposure(const ReflectionProbeHandle& probe, float bakeExposure)
+        {
+            AZ_Assert(probe.get(), "SetBakeExposure called with an invalid handle");
+            probe->SetBakeExposure(bakeExposure);
+        }
+
         void ReflectionProbeFeatureProcessor::FindReflectionProbes(const Vector3& position, ReflectionProbeVector& reflectionProbes)
         {
             reflectionProbes.clear();
@@ -431,7 +443,12 @@ namespace AZ
         {
             // load shader
             shader = RPI::LoadCriticalShader(filePath);
-            AZ_Error("ReflectionProbeFeatureProcessor", shader, "Failed to find asset for shader [%s]", filePath);
+
+            if (shader == nullptr)
+            {
+                AZ_Error("ReflectionProbeFeatureProcessor", false, "Failed to find asset for shader [%s]", filePath);
+                return;
+            }
 
             // store drawlist tag
             drawListTag = shader->GetDrawListTag();

+ 11 - 0
Gems/Atom/RHI/Code/Include/Atom/RHI/SwapChain.h

@@ -75,6 +75,9 @@ namespace AZ
             //! Return True if the swap chain prefers exclusive full screen mode and a transition happened, false otherwise.
             virtual bool SetExclusiveFullScreenState([[maybe_unused]]bool fullScreenState) { return false; }
 
+            //! Recreate the swapchain if it becomes invalid during presenting. This should happen at the end of the frame
+            //! due to images being used as attachments in the frame graph.
+            virtual void ProcessRecreation() {};
         protected:
             SwapChain();
 
@@ -98,6 +101,14 @@ namespace AZ
 
             //////////////////////////////////////////////////////////////////////////
 
+            //! Shutdown and clear all the images.
+            void ShutdownImages();
+
+            //! Initialized all the images.
+            ResultCode InitImages();
+
+            //! Flag indicating if swapchain recreation is needed at the end of the frame.
+            bool m_pendingRecreation = false;
         private:
 
             bool ValidateDescriptor(const SwapChainDescriptor& descriptor) const;

+ 7 - 1
Gems/Atom/RHI/Code/Source/RHI/FrameGraphAttachmentDatabase.cpp

@@ -134,7 +134,6 @@ namespace AZ
             m_scopeAttachmentLookup.clear();
             m_imageAttachments.clear();
             m_bufferAttachments.clear();
-            m_swapChainAttachments.clear();
             m_importedImageAttachments.clear();
             m_importedBufferAttachments.clear();
             m_transientImageAttachments.clear();
@@ -153,6 +152,13 @@ namespace AZ
                 delete attachment;
             }
             m_attachments.clear();
+
+            for (auto swapchainAttachment : m_swapChainAttachments)
+            {
+                swapchainAttachment->GetSwapChain()->ProcessRecreation();
+            }
+
+            m_swapChainAttachments.clear();
         }
 
         ImageDescriptor FrameGraphAttachmentDatabase::GetImageDescriptor(const AttachmentId& attachmentId) const

+ 66 - 79
Gems/Atom/RHI/Code/Source/RHI/SwapChain.cpp

@@ -58,43 +58,68 @@ namespace AZ
                 // Overwrite descriptor dimensions with the native ones (the ones assigned by the platform) returned by InitInternal.
                 m_descriptor.m_dimensions = nativeDimensions;
 
-                m_images.reserve(m_descriptor.m_dimensions.m_imageCount);
+                resultCode = InitImages();
+            }
 
-                for (uint32_t imageIdx = 0; imageIdx < m_descriptor.m_dimensions.m_imageCount; ++imageIdx)
-                {
-                    m_images.emplace_back(RHI::Factory::Get().CreateImage());
-                }
+            return resultCode;
+        }
 
-                InitImageRequest request;
+        void SwapChain::ShutdownImages()
+        {
+            // Shutdown existing set of images.
+            uint32_t imageSize = aznumeric_cast<uint32_t>(m_images.size());
+            for (uint32_t imageIdx = 0; imageIdx < imageSize; ++imageIdx)
+            {
+                m_images[imageIdx]->Shutdown();
+            }
 
-                RHI::ImageDescriptor& imageDescriptor = request.m_descriptor;
-                imageDescriptor.m_dimension = RHI::ImageDimension::Image2D;
-                imageDescriptor.m_bindFlags = RHI::ImageBindFlags::Color;
-                imageDescriptor.m_size.m_width = m_descriptor.m_dimensions.m_imageWidth;
-                imageDescriptor.m_size.m_height = m_descriptor.m_dimensions.m_imageHeight;
-                imageDescriptor.m_format = m_descriptor.m_dimensions.m_imageFormat;
+            m_images.clear();
+        }
 
-                for (uint32_t imageIdx = 0; imageIdx < m_descriptor.m_dimensions.m_imageCount; ++imageIdx)
-                {
-                    request.m_image = m_images[imageIdx].get();
-                    request.m_imageIndex = imageIdx;
+        ResultCode SwapChain::InitImages()
+        {
+            ResultCode resultCode = ResultCode::Success;
+
+            m_images.reserve(m_descriptor.m_dimensions.m_imageCount);
+
+            // If the new display mode has more buffers, add them.
+            for (uint32_t i = 0; i < m_descriptor.m_dimensions.m_imageCount; ++i)
+            {
+                m_images.emplace_back(RHI::Factory::Get().CreateImage());
+            }
+
+            InitImageRequest request;
+
+            RHI::ImageDescriptor& imageDescriptor = request.m_descriptor;
+            imageDescriptor.m_dimension = RHI::ImageDimension::Image2D;
+            imageDescriptor.m_bindFlags = RHI::ImageBindFlags::Color;
+            imageDescriptor.m_size.m_width = m_descriptor.m_dimensions.m_imageWidth;
+            imageDescriptor.m_size.m_height = m_descriptor.m_dimensions.m_imageHeight;
+            imageDescriptor.m_format = m_descriptor.m_dimensions.m_imageFormat;
 
-                    resultCode = ImagePoolBase::InitImage(
-                        request.m_image,
-                        imageDescriptor,
-                        [this, &request]()
+            for (uint32_t imageIdx = 0; imageIdx < m_descriptor.m_dimensions.m_imageCount; ++imageIdx)
+            {
+                request.m_image = m_images[imageIdx].get();
+                request.m_imageIndex = imageIdx;
+
+                resultCode = ImagePoolBase::InitImage(
+                    request.m_image, imageDescriptor,
+                    [this, &request]()
                     {
                         return InitImageInternal(request);
                     });
 
-                    if (resultCode != ResultCode::Success)
-                    {
-                        Shutdown();
-                        break;
-                    }
+                if (resultCode != ResultCode::Success)
+                {
+                    AZ_Error("Swapchain", false, "Failed to initialize images.");
+                    Shutdown();
+                    break;
                 }
             }
 
+            // Reset the current index back to 0 so we match the platform swap chain.
+            m_currentImageIndex = 0;
+
             return resultCode;
         }
 
@@ -105,63 +130,15 @@ namespace AZ
         }
 
         ResultCode SwapChain::Resize(const RHI::SwapChainDimensions& dimensions)
-        {     
-            // Shutdown existing set of images.
-            for (uint32_t imageIdx = 0; imageIdx < GetImageCount(); ++imageIdx)
-            {
-                m_images[imageIdx]->Shutdown();
-            }
+        {
+            ShutdownImages();
 
             SwapChainDimensions nativeDimensions = dimensions;
             ResultCode resultCode = ResizeInternal(dimensions, &nativeDimensions);
             if (resultCode == ResultCode::Success)
             {
                 m_descriptor.m_dimensions = nativeDimensions;
-                m_images.reserve(m_descriptor.m_dimensions.m_imageCount);
-
-                // If the new display mode has more buffers, add them.
-                while (m_images.size() < static_cast<size_t>(m_descriptor.m_dimensions.m_imageCount))
-                {
-                    m_images.emplace_back(RHI::Factory::Get().CreateImage());
-                }
-
-                // If it has fewer, trim down.
-                while (m_images.size() > static_cast<size_t>(m_descriptor.m_dimensions.m_imageCount))
-                {
-                    m_images.pop_back();
-                }
-
-                InitImageRequest request;
-
-                RHI::ImageDescriptor& imageDescriptor = request.m_descriptor;
-                imageDescriptor.m_dimension = RHI::ImageDimension::Image2D;
-                imageDescriptor.m_bindFlags = RHI::ImageBindFlags::Color;
-                imageDescriptor.m_size.m_width = m_descriptor.m_dimensions.m_imageWidth;
-                imageDescriptor.m_size.m_height = m_descriptor.m_dimensions.m_imageHeight;
-                imageDescriptor.m_format = m_descriptor.m_dimensions.m_imageFormat;
-
-                for (uint32_t imageIdx = 0; imageIdx < GetImageCount(); ++imageIdx)
-                {
-                    request.m_image = m_images[imageIdx].get();
-                    request.m_imageIndex = imageIdx;
-
-                    resultCode = ImagePoolBase::InitImage(
-                        request.m_image,
-                        imageDescriptor,
-                        [this, &request]()
-                    {
-                        return InitImageInternal(request);
-                    });
-
-                    if (resultCode != ResultCode::Success)
-                    {
-                        Shutdown();
-                        break;
-                    }
-                }
-
-                // Reset the current index back to 0 so we match the platform swap chain.
-                m_currentImageIndex = 0;
+                resultCode = InitImages();
             }
 
             return resultCode;
@@ -188,7 +165,7 @@ namespace AZ
 
         uint32_t SwapChain::GetImageCount() const
         {
-            return static_cast<uint32_t>(m_images.size());
+            return aznumeric_cast<uint32_t>(m_images.size());
         }
 
         uint32_t SwapChain::GetCurrentImageIndex() const
@@ -209,8 +186,18 @@ namespace AZ
         void SwapChain::Present()
         {
             AZ_TRACE_METHOD();
-            m_currentImageIndex = PresentInternal();
-            AZ_Assert(m_currentImageIndex < m_images.size(), "Invalid image index");
+            // Due to swapchain recreation, the images are refreshed.
+            // There is no need to present swapchain for this frame.
+            const uint32_t imageCount = aznumeric_cast<uint32_t>(m_images.size());
+            if (imageCount == 0)
+            {
+                return;
+            }
+            else
+            {
+                m_currentImageIndex = PresentInternal();
+                AZ_Assert(m_currentImageIndex < imageCount, "Invalid image index");
+            }
         }
     }
 }

+ 16 - 6
Gems/Atom/RHI/Vulkan/Code/Source/RHI/SwapChain.cpp

@@ -59,6 +59,19 @@ namespace AZ
             m_swapChainBarrier.m_isValid = true;
         }
 
+        void SwapChain::ProcessRecreation()
+        {
+            if (m_pendingRecreation)
+            {
+                ShutdownImages();
+                InvalidateNativeSwapChain();
+                CreateSwapchain();
+                InitImages();
+
+                m_pendingRecreation = false;
+            }
+        }
+
         void SwapChain::SetVerticalSyncIntervalInternal(uint32_t previousVsyncInterval)
         {
             if (GetDescriptor().m_verticalSyncInterval == 0 || previousVsyncInterval == 0)
@@ -231,8 +244,7 @@ namespace AZ
                 // VK_SUBOPTIMAL_KHR is treated as success, but we better update the surface info as well.
                 if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR)
                 {
-                    InvalidateNativeSwapChain();
-                    CreateSwapchain();
+                    m_pendingRecreation = true;
                 }
                 else
                 {
@@ -246,18 +258,16 @@ namespace AZ
                 }
             };
 
-            m_presentationQueue->QueueCommand(AZStd::move(presentCommand));
-
             uint32_t acquiredImageIndex = GetCurrentImageIndex();
             RHI::ResultCode result = AcquireNewImage(&acquiredImageIndex);
             if (result == RHI::ResultCode::Fail)
             {
-                InvalidateNativeSwapChain();
-                CreateSwapchain();
+                m_pendingRecreation = true;
                 return 0;
             }
             else
             {
+                m_presentationQueue->QueueCommand(AZStd::move(presentCommand));
                 return acquiredImageIndex;
             }
         }

+ 1 - 0
Gems/Atom/RHI/Vulkan/Code/Source/RHI/SwapChain.h

@@ -51,6 +51,7 @@ namespace AZ
 
             void QueueBarrier(const VkPipelineStageFlags src, const VkPipelineStageFlags dst, const VkImageMemoryBarrier& imageBarrier);
 
+            void ProcessRecreation() override;
         private:
             SwapChain() = default;
 

+ 22 - 3
Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialSourceData.h

@@ -31,6 +31,7 @@ namespace AZ
         static constexpr const char UvGroupName[] = "uvSets";
 
         class MaterialAsset;
+        class MaterialAssetCreator;
 
         //! This is a simple data structure for serializing in/out material source files.
         class MaterialSourceData final
@@ -78,15 +79,33 @@ namespace AZ
 
             //! Creates a MaterialAsset from the MaterialSourceData content.
             //! @param assetId ID for the MaterialAsset
-            //! @param materialSourceFilePath Indicates the path of the .material file that the MaterialSourceData represents. Used for resolving file-relative paths.
+            //! @param materialSourceFilePath Indicates the path of the .material file that the MaterialSourceData represents. Used for
+            //! resolving file-relative paths.
             //! @param elevateWarnings Indicates whether to treat warnings as errors
             //! @param includeMaterialPropertyNames Indicates whether to save material property names into the material asset file
             Outcome<Data::Asset<MaterialAsset>> CreateMaterialAsset(
                 Data::AssetId assetId,
                 AZStd::string_view materialSourceFilePath = "",
                 bool elevateWarnings = true,
-                bool includeMaterialPropertyNames = true
-            ) const;
+                bool includeMaterialPropertyNames = true) const;
+
+            //! Creates a MaterialAsset from the MaterialSourceData content.
+            //! @param assetId ID for the MaterialAsset
+            //! @param materialSourceFilePath Indicates the path of the .material file that the MaterialSourceData represents. Used for
+            //! resolving file-relative paths.
+            //! @param elevateWarnings Indicates whether to treat warnings as errors
+            //! @param includeMaterialPropertyNames Indicates whether to save material property names into the material asset file
+            //! @param sourceDependencies if not null, will be populated with a set of all of the loaded material and material type paths
+            Outcome<Data::Asset<MaterialAsset>> CreateMaterialAssetFromSourceData(
+                Data::AssetId assetId,
+                AZStd::string_view materialSourceFilePath = "",
+                bool elevateWarnings = true,
+                bool includeMaterialPropertyNames = true,
+                AZStd::unordered_set<AZStd::string>* sourceDependencies = nullptr) const;
+
+        private:
+            void ApplyPropertiesToAssetCreator(
+                AZ::RPI::MaterialAssetCreator& materialAssetCreator, const AZStd::string_view& materialSourceFilePath) const;
         };
     } // namespace RPI
 } // namespace AZ

+ 3 - 3
Gems/Atom/RPI/Code/Include/Atom/RPI.Public/RPISystem.h

@@ -97,8 +97,7 @@ namespace AZ
             // SystemTickBus::OnTick
             void OnSystemTick() override;
 
-            // Fill system time and game time information for simulation or rendering
-            void FillTickTimeInfo();
+            float GetCurrentTime();
 
             // The set of core asset handlers registered by the system.
             AZStd::vector<AZStd::unique_ptr<Data::AssetHandler>> m_assetHandlers;
@@ -124,7 +123,8 @@ namespace AZ
             // The job policy used for feature processor's rendering prepare
             RHI::JobPolicy m_prepareRenderJobPolicy = RHI::JobPolicy::Parallel;
 
-            TickTimeInfo m_tickTime;
+            ScriptTimePoint m_startTime;
+            float m_currentSimulationTime = 0.0f;
 
             RPISystemDescriptor m_descriptor;
 

+ 1 - 2
Gems/Atom/RPI/Code/Include/Atom/RPI.Public/RenderPipeline.h

@@ -32,7 +32,6 @@ namespace AZ
     namespace RPI
     {
         class Scene;
-        struct TickTimeInfo;
         class ShaderResourceGroup;
         class AnyAsset;
         class WindowContext;
@@ -203,7 +202,7 @@ namespace AZ
             void OnRemovedFromScene(Scene* scene);
 
             // Called when this pipeline is about to be rendered
-            void OnStartFrame(const TickTimeInfo& tick);
+            void OnStartFrame(float time);
 
             // Called when the rendering of current frame is finished.
             void OnFrameEnd();

+ 5 - 10
Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Scene.h

@@ -48,14 +48,6 @@ namespace AZ
         // Callback function to modify values of a ShaderResourceGroup
         using ShaderResourceGroupCallback = AZStd::function<void(ShaderResourceGroup*)>;
 
-        //! A structure for ticks which contains system time and game time.
-        struct TickTimeInfo
-        {
-            float m_currentGameTime;
-            float m_gameDeltaTime = 0;
-        };
-
-
         class Scene final
             : public SceneRequestBus::Handler
         {
@@ -179,12 +171,14 @@ namespace AZ
                         
             // Cpu simulation which runs all active FeatureProcessor Simulate() functions.
             // @param jobPolicy if it's JobPolicy::Parallel, the function will spawn a job thread for each FeatureProcessor's simulation.
-            void Simulate(const TickTimeInfo& tickInfo, RHI::JobPolicy jobPolicy);
+            // @param simulationTime the number of seconds since the application started
+            void Simulate(RHI::JobPolicy jobPolicy, float simulationTime);
 
             // Collect DrawPackets from FeatureProcessors
             // @param jobPolicy if it's JobPolicy::Parallel, the function will spawn a job thread for each FeatureProcessor's
             // PrepareRender.
-            void PrepareRender(const TickTimeInfo& tickInfo, RHI::JobPolicy jobPolicy);
+            // @param simulationTime the number of seconds since the application started; this is the same time value that was passed to Simulate()
+            void PrepareRender(RHI::JobPolicy jobPolicy, float simulationTime);
 
             // Function called when the current frame is finished rendering.
             void OnFrameEnd();
@@ -267,6 +261,7 @@ namespace AZ
             // Registry which allocates draw filter tag for RenderPipeline
             RHI::Ptr<RHI::DrawFilterTagRegistry> m_drawFilterTagRegistry;
 
+            RHI::ShaderInputConstantIndex m_timeInputIndex;
             float m_simulationTime;
         };
 

+ 2 - 1
Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/AssetCreator.h

@@ -118,7 +118,7 @@ namespace AZ
 
             ResetIssueCounts(); // Because the asset creator can be used multiple times
 
-            m_asset = Data::AssetManager::Instance().CreateAsset<AssetDataT>(assetId, AZ::Data::AssetLoadBehavior::PreLoad);
+            m_asset = Data::Asset<AssetDataT>(assetId, aznew AssetDataT, AZ::Data::AssetLoadBehavior::PreLoad);
             m_beginCalled = true;
 
             if (!m_asset)
@@ -138,6 +138,7 @@ namespace AZ
             }
             else
             {
+                Data::AssetManager::Instance().AssignAssetData(m_asset);
                 result = AZStd::move(m_asset);
                 success = true;
             }

+ 4 - 1
Gems/Atom/RPI/Code/Source/RPI.Edit/Material/LuaMaterialFunctorSourceData.cpp

@@ -137,7 +137,10 @@ namespace AZ
             }
             else if (!m_luaSourceFile.empty())
             {
-                auto loadOutcome = RPI::AssetUtils::LoadAsset<ScriptAsset>(materialTypeSourceFilePath, m_luaSourceFile);
+                // The sub ID for script assets must be explicit.
+                // LUA source files output a compiled as well as an uncompiled asset, sub Ids of 1 and 2.
+                auto loadOutcome =
+                    RPI::AssetUtils::LoadAsset<ScriptAsset>(materialTypeSourceFilePath, m_luaSourceFile, ScriptAsset::CompiledAssetSubId);
                 if (!loadOutcome)
                 {
                     AZ_Error("LuaMaterialFunctorSourceData", false, "Could not load script file '%s'", m_luaSourceFile.c_str());

+ 160 - 39
Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceData.cpp

@@ -17,6 +17,7 @@
 #include <Atom/RPI.Edit/Common/AssetUtils.h>
 #include <Atom/RPI.Edit/Common/JsonFileLoadContext.h>
 #include <Atom/RPI.Edit/Common/JsonReportingHelper.h>
+#include <Atom/RPI.Edit/Common/JsonUtils.h>
 
 #include <Atom/RPI.Reflect/Material/MaterialAssetCreator.h>
 #include <Atom/RPI.Reflect/Material/MaterialPropertiesLayout.h>
@@ -126,7 +127,8 @@ namespace AZ
             return changesWereApplied ? ApplyVersionUpdatesResult::UpdatesApplied : ApplyVersionUpdatesResult::NoUpdates;
         }
 
-        Outcome<Data::Asset<MaterialAsset> > MaterialSourceData::CreateMaterialAsset(Data::AssetId assetId, AZStd::string_view materialSourceFilePath, bool elevateWarnings, bool includeMaterialPropertyNames) const
+        Outcome<Data::Asset<MaterialAsset>> MaterialSourceData::CreateMaterialAsset(
+            Data::AssetId assetId, AZStd::string_view materialSourceFilePath, bool elevateWarnings, bool includeMaterialPropertyNames) const
         {
             MaterialAssetCreator materialAssetCreator;
             materialAssetCreator.SetElevateWarnings(elevateWarnings);
@@ -172,6 +174,128 @@ namespace AZ
                 materialAssetCreator.Begin(assetId, *parentMaterialAsset.GetValue().Get(), includeMaterialPropertyNames);
             }
 
+            ApplyPropertiesToAssetCreator(materialAssetCreator, materialSourceFilePath);
+
+            Data::Asset<MaterialAsset> material;
+            if (materialAssetCreator.End(material))
+            {
+                return Success(material);
+            }
+            else
+            {
+                return Failure();
+            }
+        }
+
+        Outcome<Data::Asset<MaterialAsset>> MaterialSourceData::CreateMaterialAssetFromSourceData(
+            Data::AssetId assetId,
+            AZStd::string_view materialSourceFilePath,
+            bool elevateWarnings,
+            bool includeMaterialPropertyNames,
+            AZStd::unordered_set<AZStd::string>* sourceDependencies) const
+        {
+            const auto materialTypeSourcePath = AssetUtils::ResolvePathReference(materialSourceFilePath, m_materialType);
+            const auto materialTypeAssetId = AssetUtils::MakeAssetId(materialTypeSourcePath, 0);
+            if (!materialTypeAssetId.IsSuccess())
+            {
+                AZ_Error("MaterialSourceData", false, "Failed to create material type asset ID: '%s'.", materialTypeSourcePath.c_str());
+                return Failure();
+            }
+
+            MaterialTypeSourceData materialTypeSourceData;
+            if (!AZ::RPI::JsonUtils::LoadObjectFromFile(materialTypeSourcePath, materialTypeSourceData))
+            {
+                AZ_Error("MaterialSourceData", false, "Failed to load MaterialTypeSourceData: '%s'.", materialTypeSourcePath.c_str());
+                return Failure();
+            }
+
+            materialTypeSourceData.ResolveUvEnums();
+
+            const auto materialTypeAsset =
+                materialTypeSourceData.CreateMaterialTypeAsset(materialTypeAssetId.GetValue(), materialTypeSourcePath, elevateWarnings);
+            if (!materialTypeAsset.IsSuccess())
+            {
+                AZ_Error("MaterialSourceData", false, "Failed to create material type asset from source data: '%s'.", materialTypeSourcePath.c_str());
+                return Failure();
+            }
+
+            // Track all of the material and material type assets loaded while trying to create a material asset from source data. This will
+            // be used for evaluating circular dependencies and returned for external monitoring or other use.
+            AZStd::unordered_set<AZStd::string> dependencies;
+            dependencies.insert(materialSourceFilePath);
+            dependencies.insert(materialTypeSourcePath);
+
+            // Load and build a stack of MaterialSourceData from all of the parent materials in the hierarchy. Properties from the source
+            // data will be applied in reverse to the asset creator.
+            AZStd::vector<MaterialSourceData> parentSourceDataStack;
+
+            AZStd::string parentSourceRelPath = m_parentMaterial;
+            AZStd::string parentSourceAbsPath = AssetUtils::ResolvePathReference(materialSourceFilePath, parentSourceRelPath);
+            while (!parentSourceRelPath.empty())
+            {
+                if (!dependencies.insert(parentSourceAbsPath).second)
+                {
+                    AZ_Error("MaterialSourceData", false, "Detected circular dependency between materials: '%s' and '%s'.", materialSourceFilePath.data(), parentSourceAbsPath.c_str());
+                    return Failure();
+                }
+
+                MaterialSourceData parentSourceData;
+                if (!AZ::RPI::JsonUtils::LoadObjectFromFile(parentSourceAbsPath, parentSourceData))
+                {
+                    AZ_Error("MaterialSourceData", false, "Failed to load MaterialSourceData for parent material: '%s'.", parentSourceAbsPath.c_str());
+                    return Failure();
+                }
+
+                // Make sure that all materials in the hierarchy share the same material type
+                const auto parentTypeAssetId = AssetUtils::MakeAssetId(parentSourceAbsPath, parentSourceData.m_materialType, 0);
+                if (!parentTypeAssetId)
+                {
+                    AZ_Error("MaterialSourceData", false, "Parent material asset ID wasn't found: '%s'.", parentSourceAbsPath.c_str());
+                    return Failure();
+                }
+
+                if (parentTypeAssetId.GetValue() != materialTypeAssetId.GetValue())
+                {
+                    AZ_Error("MaterialSourceData", false, "This material and its parent material do not share the same material type.");
+                    return Failure();
+                }
+
+                // Get the location of the next parent material and push the source data onto the stack 
+                parentSourceRelPath = parentSourceData.m_parentMaterial;
+                parentSourceAbsPath = AssetUtils::ResolvePathReference(parentSourceAbsPath, parentSourceRelPath);
+                parentSourceDataStack.emplace_back(AZStd::move(parentSourceData));
+            }
+
+            // Create the material asset from all the previously loaded source data 
+            MaterialAssetCreator materialAssetCreator;
+            materialAssetCreator.SetElevateWarnings(elevateWarnings);
+            materialAssetCreator.Begin(assetId, *materialTypeAsset.GetValue().Get(), includeMaterialPropertyNames);
+
+            while (!parentSourceDataStack.empty())
+            {
+                parentSourceDataStack.back().ApplyPropertiesToAssetCreator(materialAssetCreator, materialSourceFilePath);
+                parentSourceDataStack.pop_back();
+            }
+
+            ApplyPropertiesToAssetCreator(materialAssetCreator, materialSourceFilePath);
+
+            Data::Asset<MaterialAsset> material;
+            if (materialAssetCreator.End(material))
+            {
+                if (sourceDependencies)
+                {
+                    sourceDependencies->insert(dependencies.begin(), dependencies.end());
+                }
+
+                return Success(material);
+            }
+
+            return Failure();
+        }
+
+        void MaterialSourceData::ApplyPropertiesToAssetCreator(
+            AZ::RPI::MaterialAssetCreator& materialAssetCreator, const AZStd::string_view& materialSourceFilePath) const
+        {
             for (auto& group : m_properties)
             {
                 for (auto& property : group.second)
@@ -183,43 +307,49 @@ namespace AZ
                     }
                     else
                     {
-                        MaterialPropertyIndex propertyIndex = materialAssetCreator.m_materialPropertiesLayout->FindPropertyIndex(propertyId.GetFullName());
+                        MaterialPropertyIndex propertyIndex =
+                            materialAssetCreator.m_materialPropertiesLayout->FindPropertyIndex(propertyId.GetFullName());
                         if (propertyIndex.IsValid())
                         {
-                            const MaterialPropertyDescriptor* propertyDescriptor = materialAssetCreator.m_materialPropertiesLayout->GetPropertyDescriptor(propertyIndex);
+                            const MaterialPropertyDescriptor* propertyDescriptor =
+                                materialAssetCreator.m_materialPropertiesLayout->GetPropertyDescriptor(propertyIndex);
                             switch (propertyDescriptor->GetDataType())
                             {
                             case MaterialPropertyDataType::Image:
-                            {
-                                Outcome<Data::Asset<ImageAsset>> imageAssetResult = MaterialUtils::GetImageAssetReference(materialSourceFilePath, property.second.m_value.GetValue<AZStd::string>());
-
-                                if (imageAssetResult.IsSuccess())
-                                {
-                                    auto& imageAsset = imageAssetResult.GetValue();
-                                    // Load referenced images when load material
-                                    imageAsset.SetAutoLoadBehavior(Data::AssetLoadBehavior::PreLoad);
-                                    materialAssetCreator.SetPropertyValue(propertyId.GetFullName(), imageAsset);
-                                }
-                                else
                                 {
-                                    materialAssetCreator.ReportError("Material property '%s': Could not find the image '%s'", propertyId.GetFullName().GetCStr(), property.second.m_value.GetValue<AZStd::string>().data());
+                                    Outcome<Data::Asset<ImageAsset>> imageAssetResult = MaterialUtils::GetImageAssetReference(
+                                        materialSourceFilePath, property.second.m_value.GetValue<AZStd::string>());
+
+                                    if (imageAssetResult.IsSuccess())
+                                    {
+                                        auto& imageAsset = imageAssetResult.GetValue();
+                                        // Load referenced images when load material
+                                        imageAsset.SetAutoLoadBehavior(Data::AssetLoadBehavior::PreLoad);
+                                        materialAssetCreator.SetPropertyValue(propertyId.GetFullName(), imageAsset);
+                                    }
+                                    else
+                                    {
+                                        materialAssetCreator.ReportError(
+                                            "Material property '%s': Could not find the image '%s'", propertyId.GetFullName().GetCStr(),
+                                            property.second.m_value.GetValue<AZStd::string>().data());
+                                    }
                                 }
-                            }
-                            break;
+                                break;
                             case MaterialPropertyDataType::Enum:
-                            {
-                                AZ::Name enumName = AZ::Name(property.second.m_value.GetValue<AZStd::string>());
-                                uint32_t enumValue = propertyDescriptor->GetEnumValue(enumName);
-                                if (enumValue == MaterialPropertyDescriptor::InvalidEnumValue)
                                 {
-                                    materialAssetCreator.ReportError("Enum value '%s' couldn't be found in the 'enumValues' list", enumName.GetCStr());
+                                    AZ::Name enumName = AZ::Name(property.second.m_value.GetValue<AZStd::string>());
+                                    uint32_t enumValue = propertyDescriptor->GetEnumValue(enumName);
+                                    if (enumValue == MaterialPropertyDescriptor::InvalidEnumValue)
+                                    {
+                                        materialAssetCreator.ReportError(
+                                            "Enum value '%s' couldn't be found in the 'enumValues' list", enumName.GetCStr());
+                                    }
+                                    else
+                                    {
+                                        materialAssetCreator.SetPropertyValue(propertyId.GetFullName(), enumValue);
+                                    }
                                 }
-                                else
-                                {
-                                    materialAssetCreator.SetPropertyValue(propertyId.GetFullName(), enumValue);
-                                }
-                            }
-                            break;
+                                break;
                             default:
                                 materialAssetCreator.SetPropertyValue(propertyId.GetFullName(), property.second.m_value);
                                 break;
@@ -227,21 +357,12 @@ namespace AZ
                         }
                         else
                         {
-                            materialAssetCreator.ReportWarning("Can not find property id '%s' in MaterialPropertyLayout", propertyId.GetFullName().GetStringView().data());
+                            materialAssetCreator.ReportWarning(
+                                "Can not find property id '%s' in MaterialPropertyLayout", propertyId.GetFullName().GetStringView().data());
                         }
                     }
                 }
             }
-
-            Data::Asset<MaterialAsset> material;
-            if (materialAssetCreator.End(material))
-            {
-                return Success(material);
-            }
-            else
-            {
-                return Failure();
-            }
         }
 
     } // namespace RPI

+ 15 - 11
Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp

@@ -393,11 +393,12 @@ namespace AZ
             for (const ShaderVariantReferenceData& shaderRef : m_shaderCollection)
             {
                 const auto& shaderFile = shaderRef.m_shaderFilePath;
-                const auto& shaderAsset = AssetUtils::LoadAsset<ShaderAsset>(materialTypeSourceFilePath, shaderFile, 0);
+                auto shaderAssetResult = AssetUtils::LoadAsset<ShaderAsset>(materialTypeSourceFilePath, shaderFile, 0);
 
-                if (shaderAsset)
+                if (shaderAssetResult)
                 {
-                    auto optionsLayout = shaderAsset.GetValue()->GetShaderOptionGroupLayout();
+                    auto shaderAsset = shaderAssetResult.GetValue();
+                    auto optionsLayout = shaderAsset->GetShaderOptionGroupLayout();
                     ShaderOptionGroup options{ optionsLayout };
                     for (auto& iter : shaderRef.m_shaderOptionValues)
                     {
@@ -408,12 +409,11 @@ namespace AZ
                     }
 
                     materialTypeAssetCreator.AddShader(
-                        shaderAsset.GetValue(), options.GetShaderVariantId(),
-                        shaderRef.m_shaderTag.IsEmpty() ? Uuid::CreateRandom().ToString<AZ::Name>() : shaderRef.m_shaderTag
-                    );
+                        shaderAsset, options.GetShaderVariantId(),
+                        shaderRef.m_shaderTag.IsEmpty() ? Uuid::CreateRandom().ToString<AZ::Name>() : shaderRef.m_shaderTag);
 
                     // Gather UV names
-                    const ShaderInputContract& shaderInputContract = shaderAsset.GetValue()->GetInputContract();
+                    const ShaderInputContract& shaderInputContract = shaderAsset->GetInputContract();
                     for (const ShaderInputContract::StreamChannelInfo& channel : shaderInputContract.m_streamChannels)
                     {
                         const RHI::ShaderSemantic& semantic = channel.m_semantic;
@@ -493,15 +493,19 @@ namespace AZ
                         {
                         case MaterialPropertyDataType::Image:
                         {
-                            Outcome<Data::Asset<ImageAsset>> imageAssetResult = MaterialUtils::GetImageAssetReference(materialTypeSourceFilePath, property.m_value.GetValue<AZStd::string>());
+                            auto imageAssetResult = MaterialUtils::GetImageAssetReference(
+                                materialTypeSourceFilePath, property.m_value.GetValue<AZStd::string>());
 
-                            if (imageAssetResult.IsSuccess())
+                            if (imageAssetResult)
                             {
-                                materialTypeAssetCreator.SetPropertyValue(propertyId.GetFullName(), imageAssetResult.GetValue());
+                                auto imageAsset = imageAssetResult.GetValue();
+                                materialTypeAssetCreator.SetPropertyValue(propertyId.GetFullName(), imageAsset);
                             }
                             else
                             {
-                                materialTypeAssetCreator.ReportError("Material property '%s': Could not find the image '%s'", propertyId.GetFullName().GetCStr(), property.m_value.GetValue<AZStd::string>().data());
+                                materialTypeAssetCreator.ReportError(
+                                    "Material property '%s': Could not find the image '%s'", propertyId.GetFullName().GetCStr(),
+                                    property.m_value.GetValue<AZStd::string>().data());
                             }
                         }
                         break;

+ 6 - 0
Gems/Atom/RPI/Code/Source/RPI.Public/Pass/FullscreenTrianglePass.cpp

@@ -136,6 +136,12 @@ namespace AZ
             RHI::DrawLinear draw = RHI::DrawLinear();
             draw.m_vertexCount = 3;
 
+            if (m_shader == nullptr)
+            {
+                AZ_Error("PassSystem", false, "[FullscreenTrianglePass]: Shader not loaded!");
+                return;
+            }
+
             RHI::PipelineStateDescriptorForDraw pipelineStateDescriptor;
 
             // [GFX TODO][ATOM-872] The pass should be able to drive the shader variant

+ 11 - 9
Gems/Atom/RPI/Code/Source/RPI.Public/RPISystem.cpp

@@ -268,21 +268,23 @@ namespace AZ
 
             AssetInitBus::Broadcast(&AssetInitBus::Events::PostLoadInit);
 
-            // Update tick time info
-            FillTickTimeInfo();
+            m_currentSimulationTime = GetCurrentTime();
 
             for (auto& scene : m_scenes)
             {
-                scene->Simulate(m_tickTime, m_simulationJobPolicy);
+                scene->Simulate(m_simulationJobPolicy, m_currentSimulationTime);
             }
         }
 
-        void RPISystem::FillTickTimeInfo()
+        float RPISystem::GetCurrentTime()
         {
-            AZ::TickRequestBus::BroadcastResult(m_tickTime.m_gameDeltaTime, &AZ::TickRequestBus::Events::GetTickDeltaTime);
-            ScriptTimePoint currentTime;
-            AZ::TickRequestBus::BroadcastResult(currentTime, &AZ::TickRequestBus::Events::GetTimeAtCurrentTick);
-            m_tickTime.m_currentGameTime = static_cast<float>(currentTime.GetSeconds());
+            ScriptTimePoint timeAtCurrentTick;
+            AZ::TickRequestBus::BroadcastResult(timeAtCurrentTick, &AZ::TickRequestBus::Events::GetTimeAtCurrentTick);
+
+            // We subtract the start time to maximize precision of the time value, since we will be converting it to a float.
+            double currentTime = timeAtCurrentTick.GetSeconds() - m_startTime.GetSeconds();
+
+            return aznumeric_cast<float>(currentTime);
         }
 
         void RPISystem::RenderTick()
@@ -301,7 +303,7 @@ namespace AZ
             // [GFX TODO] We may parallel scenes' prepare render.
             for (auto& scenePtr : m_scenes)
             {
-                scenePtr->PrepareRender(m_tickTime, m_prepareRenderJobPolicy);
+                scenePtr->PrepareRender(m_prepareRenderJobPolicy, m_currentSimulationTime);
             }
 
             m_rhiSystem.FrameUpdate(

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

@@ -375,7 +375,7 @@ namespace AZ
             m_scene->RemoveRenderPipeline(m_nameId);
         }
 
-        void RenderPipeline::OnStartFrame([[maybe_unused]] const TickTimeInfo& tick)
+        void RenderPipeline::OnStartFrame([[maybe_unused]] float time)
         {
             AZ_PROFILE_SCOPE(RPI, "RenderPipeline: OnStartFrame");
 

+ 9 - 8
Gems/Atom/RPI/Code/Source/RPI.Public/Scene.cpp

@@ -44,6 +44,9 @@ namespace AZ
             {
                 auto shaderAsset = RPISystemInterface::Get()->GetCommonShaderAssetForSrgs();
                 scene->m_srg = ShaderResourceGroup::Create(shaderAsset, sceneSrgLayout->GetName());
+                
+                // Set value for constants defined in SceneTimeSrg.azsli
+                scene->m_timeInputIndex = scene->m_srg->FindShaderInputConstantIndex(Name{ "m_time" });
             }
 
             scene->m_name = sceneDescriptor.m_nameId;
@@ -410,11 +413,11 @@ namespace AZ
             //[GFX TODO]: the completion job should start here
         }
 
-        void Scene::Simulate([[maybe_unused]] const TickTimeInfo& tickInfo, RHI::JobPolicy jobPolicy)
+        void Scene::Simulate(RHI::JobPolicy jobPolicy, float simulationTime)
         {
             AZ_PROFILE_SCOPE(RPI, "Scene: Simulate");
 
-            m_simulationTime = tickInfo.m_currentGameTime;
+            m_simulationTime = simulationTime;
 
             // If previous simulation job wasn't done, wait for it to finish.
             if (m_taskGraphActive)
@@ -483,11 +486,9 @@ namespace AZ
         {
             if (m_srg)
             {
-                // Set value for constants defined in SceneTimeSrg.azsli
-                RHI::ShaderInputConstantIndex timeIndex = m_srg->FindShaderInputConstantIndex(Name{ "m_time" });
-                if (timeIndex.IsValid())
+                if (m_timeInputIndex.IsValid())
                 {
-                    m_srg->SetConstant(timeIndex, m_simulationTime);
+                    m_srg->SetConstant(m_timeInputIndex, m_simulationTime);
                 }
 
                 // signal any handlers to update values for their partial scene srg
@@ -620,7 +621,7 @@ namespace AZ
             WaitAndCleanCompletionJob(finalizeDrawListsCompletion);
         }
 
-        void Scene::PrepareRender(const TickTimeInfo& tickInfo, RHI::JobPolicy jobPolicy)
+        void Scene::PrepareRender(RHI::JobPolicy jobPolicy, float simulationTime)
         {
             AZ_PROFILE_SCOPE(RPI, "Scene: PrepareRender");
 
@@ -644,7 +645,7 @@ namespace AZ
                     if (pipeline->NeedsRender())
                     {
                         activePipelines.push_back(pipeline);
-                        pipeline->OnStartFrame(tickInfo);
+                        pipeline->OnStartFrame(simulationTime);
                     }
                 }
             }

+ 4 - 0
Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAsset.cpp

@@ -188,6 +188,10 @@ namespace AZ
         void MaterialTypeAsset::SetReady()
         {
             m_status = AssetStatus::Ready;
+
+            // If this was created dynamically using MaterialTypeAssetCreator (which is what calls SetReady()),
+            // we need to connect to the AssetBus for reloads.
+            PostLoadInit();
         }
 
         bool MaterialTypeAsset::PostLoadInit()

+ 5 - 3
Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/ShaderCollection.cpp

@@ -126,8 +126,8 @@ namespace AZ
         }
 
         ShaderCollection::Item::Item()
+            : m_renderStatesOverlay(RHI::GetInvalidRenderStates())
         {
-            m_renderStatesOverlay = RHI::GetInvalidRenderStates();
         }
 
         ShaderCollection::Item& ShaderCollection::operator[](size_t i)
@@ -156,7 +156,8 @@ namespace AZ
         }
 
         ShaderCollection::Item::Item(const Data::Asset<ShaderAsset>& shaderAsset, const AZ::Name& shaderTag, ShaderVariantId variantId)
-            : m_shaderAsset(shaderAsset)
+            : m_renderStatesOverlay(RHI::GetInvalidRenderStates())
+            , m_shaderAsset(shaderAsset)
             , m_shaderVariantId(variantId)
             , m_shaderTag(shaderTag)
             , m_shaderOptionGroup(shaderAsset->GetShaderOptionGroupLayout(), variantId)
@@ -164,7 +165,8 @@ namespace AZ
         }
 
         ShaderCollection::Item::Item(Data::Asset<ShaderAsset>&& shaderAsset, const AZ::Name& shaderTag, ShaderVariantId variantId)
-            : m_shaderAsset(AZStd::move(shaderAsset))
+            : m_renderStatesOverlay(RHI::GetInvalidRenderStates())
+            , m_shaderAsset(AZStd::move(shaderAsset))
             , m_shaderVariantId(variantId)
             , m_shaderTag(shaderTag)
             , m_shaderOptionGroup(shaderAsset->GetShaderOptionGroupLayout(), variantId)

+ 3 - 4
Gems/Atom/TestData/TestData/Materials/SkinTestCases/002_wrinkle_regression_test.material

@@ -33,7 +33,7 @@
         },
         "subsurfaceScattering": {
             "enableSubsurfaceScattering": true,
-            "influenceMap": "Objects/Lucy/Lucy_thickness.tif",
+            "influenceMap": "TestData/Textures/checker8x8_gray_512.png",
             "scatterDistance": 15.0,
             "subsurfaceScatterFactor": 0.4300000071525574,
             "thicknessMap": "Objects/Lucy/Lucy_thickness.tif",
@@ -47,8 +47,7 @@
                 0.3182879388332367,
                 0.16388189792633058,
                 1.0
-            ],
-            "useInfluenceMap": false
+            ]
         },
         "wrinkleLayers": {
             "baseColorMap1": "TestData/Textures/cc0/Lava004_1K_Color.jpg",
@@ -61,4 +60,4 @@
             "normalMap2": "TestData/Textures/TextureHaven/4k_castle_brick_02_red/4k_castle_brick_02_red_normal.png"
         }
     }
-}
+}

+ 8 - 6
Gems/Atom/Tools/AtomToolsFramework/Code/Source/Document/AtomToolsDocumentSystemComponent.cpp

@@ -159,7 +159,7 @@ namespace AtomToolsFramework
 
     void AtomToolsDocumentSystemComponent::OnDocumentExternallyModified(const AZ::Uuid& documentId)
     {
-        m_documentIdsToReopen.insert(documentId);
+        m_documentIdsWithExternalChanges.insert(documentId);
         if (!AZ::TickBus::Handler::BusIsConnected())
         {
             AZ::TickBus::Handler::BusConnect();
@@ -168,7 +168,7 @@ namespace AtomToolsFramework
 
     void AtomToolsDocumentSystemComponent::OnDocumentDependencyModified(const AZ::Uuid& documentId)
     {
-        m_documentIdsToReopen.insert(documentId);
+        m_documentIdsWithDependencyChanges.insert(documentId);
         if (!AZ::TickBus::Handler::BusIsConnected())
         {
             AZ::TickBus::Handler::BusConnect();
@@ -177,7 +177,7 @@ namespace AtomToolsFramework
 
     void AtomToolsDocumentSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
     {
-        for (const AZ::Uuid& documentId : m_documentIdsToReopen)
+        for (const AZ::Uuid& documentId : m_documentIdsWithExternalChanges)
         {
             AZStd::string documentPath;
             AtomToolsDocumentRequestBus::EventResult(documentPath, documentId, &AtomToolsDocumentRequestBus::Events::GetAbsolutePath);
@@ -191,6 +191,8 @@ namespace AtomToolsFramework
                 continue;
             }
 
+            m_documentIdsWithDependencyChanges.erase(documentId);
+
             AtomToolsFramework::TraceRecorder traceRecorder(m_maxMessageBoxLineCount);
 
             bool openResult = false;
@@ -204,7 +206,7 @@ namespace AtomToolsFramework
             }
         }
 
-        for (const AZ::Uuid& documentId : m_documentIdsToReopen)
+        for (const AZ::Uuid& documentId : m_documentIdsWithDependencyChanges)
         {
             AZStd::string documentPath;
             AtomToolsDocumentRequestBus::EventResult(documentPath, documentId, &AtomToolsDocumentRequestBus::Events::GetAbsolutePath);
@@ -231,8 +233,8 @@ namespace AtomToolsFramework
             }
         }
 
-        m_documentIdsToReopen.clear();
-        m_documentIdsToReopen.clear();
+        m_documentIdsWithDependencyChanges.clear();
+        m_documentIdsWithExternalChanges.clear();
         AZ::TickBus::Handler::BusDisconnect();
     }
 

+ 2 - 2
Gems/Atom/Tools/AtomToolsFramework/Code/Source/Document/AtomToolsDocumentSystemComponent.h

@@ -85,8 +85,8 @@ namespace AtomToolsFramework
         AZStd::intrusive_ptr<AtomToolsDocumentSystemSettings> m_settings;
         AZStd::function<AtomToolsDocument*()> m_documentCreator;
         AZStd::unordered_map<AZ::Uuid, AZStd::shared_ptr<AtomToolsDocument>> m_documentMap;
-        AZStd::unordered_set<AZ::Uuid> m_documentIdsToRebuild;
-        AZStd::unordered_set<AZ::Uuid> m_documentIdsToReopen;
+        AZStd::unordered_set<AZ::Uuid> m_documentIdsWithExternalChanges;
+        AZStd::unordered_set<AZ::Uuid> m_documentIdsWithDependencyChanges;
         const size_t m_maxMessageBoxLineCount = 15;
     };
 } // namespace AtomToolsFramework

+ 34 - 29
Gems/Atom/Tools/MaterialEditor/Code/Source/Document/MaterialDocument.cpp

@@ -567,26 +567,26 @@ namespace MaterialEditor
         }
     }
 
-    void MaterialDocument::SourceFileChanged(AZStd::string relativePath, AZStd::string scanFolder, AZ::Uuid sourceUUID)
+    void MaterialDocument::SourceFileChanged(AZStd::string relativePath, AZStd::string scanFolder, [[maybe_unused]] AZ::Uuid sourceUUID)
     {
-        if (m_sourceAssetId.m_guid == sourceUUID)
+        auto sourcePath = AZ::RPI::AssetUtils::ResolvePathReference(scanFolder, relativePath);
+
+        if (m_absolutePath == sourcePath)
         {
             // ignore notifications caused by saving the open document
             if (!m_saveTriggeredInternally)
             {
                 AZ_TracePrintf("MaterialDocument", "Material document changed externally: '%s'.\n", m_absolutePath.c_str());
-                AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentExternallyModified, m_id);
+                AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(
+                    &AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentExternallyModified, m_id);
             }
             m_saveTriggeredInternally = false;
         }
-    }
-
-    void MaterialDocument::OnAssetReloaded(AZ::Data::Asset<AZ::Data::AssetData> asset)
-    {
-        if (m_dependentAssetIds.find(asset->GetId()) != m_dependentAssetIds.end())
+        else if (m_sourceDependencies.find(sourcePath) != m_sourceDependencies.end())
         {
             AZ_TracePrintf("MaterialDocument", "Material document dependency changed: '%s'.\n", m_absolutePath.c_str());
-            AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(&AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentDependencyModified, m_id);
+            AtomToolsFramework::AtomToolsDocumentNotificationBus::Broadcast(
+                &AtomToolsFramework::AtomToolsDocumentNotificationBus::Events::OnDocumentDependencyModified, m_id);
         }
     }
 
@@ -655,7 +655,6 @@ namespace MaterialEditor
             return false;
         }
 
-        m_sourceAssetId = sourceAssetInfo.m_assetId;
         m_relativePath = sourceAssetInfo.m_relativePath;
         if (!AzFramework::StringFunc::Path::Normalize(m_relativePath))
         {
@@ -722,14 +721,15 @@ namespace MaterialEditor
         // we can create the asset dynamically from the source data.
         // Long term, the material document should not be concerned with assets at all. The viewport window should be the
         // only thing concerned with assets or instances.
-        auto createResult = m_materialSourceData.CreateMaterialAsset(Uuid::CreateRandom(), m_absolutePath, true);
-        if (!createResult)
+        auto materialAssetResult =
+            m_materialSourceData.CreateMaterialAssetFromSourceData(Uuid::CreateRandom(), m_absolutePath, true, true, &m_sourceDependencies);
+        if (!materialAssetResult)
         {
             AZ_Error("MaterialDocument", false, "Material asset could not be created from source data: '%s'.", m_absolutePath.c_str());
             return false;
         }
 
-        m_materialAsset = createResult.GetValue();
+        m_materialAsset = materialAssetResult.GetValue();
         if (!m_materialAsset.IsReady())
         {
             AZ_Error("MaterialDocument", false, "Material asset is not ready: '%s'.", m_absolutePath.c_str());
@@ -743,28 +743,35 @@ namespace MaterialEditor
             return false;
         }
 
-        // track material type asset to notify when dependencies change
-        m_dependentAssetIds.insert(materialTypeAsset->GetId());
-        AZ::Data::AssetBus::MultiHandler::BusConnect(materialTypeAsset->GetId());
-
         AZStd::array_view<AZ::RPI::MaterialPropertyValue> parentPropertyValues = materialTypeAsset->GetDefaultPropertyValues();
         AZ::Data::Asset<MaterialAsset> parentMaterialAsset;
         if (!m_materialSourceData.m_parentMaterial.empty())
         {
-            // There is a parent for this material
-            auto parentMaterialResult = AssetUtils::LoadAsset<MaterialAsset>(m_absolutePath, m_materialSourceData.m_parentMaterial);
-            if (!parentMaterialResult)
+            AZ::RPI::MaterialSourceData parentMaterialSourceData;
+            const auto parentMaterialFilePath = AssetUtils::ResolvePathReference(m_absolutePath, m_materialSourceData.m_parentMaterial);
+            if (!AZ::RPI::JsonUtils::LoadObjectFromFile(parentMaterialFilePath, parentMaterialSourceData))
             {
-                AZ_Error("MaterialDocument", false, "Parent material asset could not be loaded: '%s'.", m_materialSourceData.m_parentMaterial.c_str());
+                AZ_Error("MaterialDocument", false, "Material parent source data could not be loaded for: '%s'.", parentMaterialFilePath.c_str());
                 return false;
             }
 
-            parentMaterialAsset = parentMaterialResult.GetValue();
-            parentPropertyValues = parentMaterialAsset->GetPropertyValues();
+            const auto parentMaterialAssetIdResult = AssetUtils::MakeAssetId(parentMaterialFilePath, 0);
+            if (!parentMaterialAssetIdResult)
+            {
+                AZ_Error("MaterialDocument", false, "Material parent asset ID could not be created: '%s'.", parentMaterialFilePath.c_str());
+                return false;
+            }
 
-            // track parent material asset to notify when dependencies change
-            m_dependentAssetIds.insert(parentMaterialAsset->GetId());
-            AZ::Data::AssetBus::MultiHandler::BusConnect(parentMaterialAsset->GetId());
+            auto parentMaterialAssetResult = parentMaterialSourceData.CreateMaterialAssetFromSourceData(
+                parentMaterialAssetIdResult.GetValue(), parentMaterialFilePath, true, true);
+            if (!parentMaterialAssetResult)
+            {
+                AZ_Error("MaterialDocument", false, "Material parent asset could not be created from source data: '%s'.", parentMaterialFilePath.c_str());
+                return false;
+            }
+
+            parentMaterialAsset = parentMaterialAssetResult.GetValue();
+            parentPropertyValues = parentMaterialAsset->GetPropertyValues();
         }
 
         // Creating a material from a material asset will fail if a texture is referenced but not loaded 
@@ -913,15 +920,13 @@ namespace MaterialEditor
     void MaterialDocument::Clear()
     {
         AZ::TickBus::Handler::BusDisconnect();
-        AZ::Data::AssetBus::MultiHandler::BusDisconnect();
         AzToolsFramework::AssetSystemBus::Handler::BusDisconnect();
 
         m_materialAsset = {};
         m_materialInstance = {};
         m_absolutePath.clear();
         m_relativePath.clear();
-        m_sourceAssetId = {};
-        m_dependentAssetIds.clear();
+        m_sourceDependencies.clear();
         m_saveTriggeredInternally = {};
         m_compilePending = {};
         m_properties.clear();

+ 1 - 10
Gems/Atom/Tools/MaterialEditor/Code/Source/Document/MaterialDocument.h

@@ -29,7 +29,6 @@ namespace MaterialEditor
         : public AtomToolsFramework::AtomToolsDocument
         , public MaterialDocumentRequestBus::Handler
         , private AZ::TickBus::Handler
-        , private AZ::Data::AssetBus::MultiHandler
         , private AzToolsFramework::AssetSystemBus::Handler
     {
     public:
@@ -105,11 +104,6 @@ namespace MaterialEditor
         void SourceFileChanged(AZStd::string relativePath, AZStd::string scanFolder, AZ::Uuid sourceUUID) override;
         //////////////////////////////////////////////////////////////////////////
 
-        //////////////////////////////////////////////////////////////////////////
-        // AZ::Data::AssetBus::Router overrides...
-        void OnAssetReloaded(AZ::Data::Asset<AZ::Data::AssetData> asset) override;
-        //////////////////////////////////////////////////////////////////////////
-
         bool SavePropertiesToSourceData(AZ::RPI::MaterialSourceData& sourceData, PropertyFilterFunction propertyFilter) const;
 
         bool OpenInternal(AZStd::string_view loadPath);
@@ -137,11 +131,8 @@ namespace MaterialEditor
         // Material instance being edited
         AZ::Data::Instance<AZ::RPI::Material> m_materialInstance;
 
-        // Asset used to open document
-        AZ::Data::AssetId m_sourceAssetId;
-
         // Set of assets that can trigger a document reload
-        AZStd::unordered_set<AZ::Data::AssetId> m_dependentAssetIds;
+        AZStd::unordered_set<AZStd::string> m_sourceDependencies;
 
         // Track if document saved itself last to skip external modification notification
         bool m_saveTriggeredInternally = false;

+ 20 - 0
Gems/AtomLyIntegration/CommonFeatures/Code/Source/ReflectionProbe/EditorReflectionProbeComponent.cpp

@@ -40,6 +40,7 @@ namespace AZ
                     ->Field("bakedCubeMapQualityLevel", &EditorReflectionProbeComponent::m_bakedCubeMapQualityLevel)
                     ->Field("bakedCubeMapRelativePath", &EditorReflectionProbeComponent::m_bakedCubeMapRelativePath)
                     ->Field("authoredCubeMapAsset", &EditorReflectionProbeComponent::m_authoredCubeMapAsset)
+                    ->Field("bakeExposure", &EditorReflectionProbeComponent::m_bakeExposure)
                 ;
 
                 if (AZ::EditContext* editContext = serializeContext->GetEditContext())
@@ -62,6 +63,13 @@ namespace AZ
                                 ->Attribute(AZ::Edit::Attributes::ButtonText, "Bake Reflection Probe")
                                 ->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorReflectionProbeComponent::BakeReflectionProbe)
                                 ->Attribute(AZ::Edit::Attributes::Visibility, &EditorReflectionProbeComponent::GetBakedCubemapVisibilitySetting)
+                            ->DataElement(AZ::Edit::UIHandlers::Slider, &EditorReflectionProbeComponent::m_bakeExposure, "Bake Exposure", "Exposure to use when baking the cubemap")
+                                ->Attribute(AZ::Edit::Attributes::SoftMin, -16.0f)
+                                ->Attribute(AZ::Edit::Attributes::SoftMax, 16.0f)
+                                ->Attribute(AZ::Edit::Attributes::Min, -20.0f)
+                                ->Attribute(AZ::Edit::Attributes::Max, 20.0f)
+                                ->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorReflectionProbeComponent::OnBakeExposureChanged)
+                                ->Attribute(AZ::Edit::Attributes::Visibility, &EditorReflectionProbeComponent::GetBakedCubemapVisibilitySetting)
                         ->ClassElement(AZ::Edit::ClassElements::Group, "Cubemap")
                             ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
                             ->DataElement(AZ::Edit::UIHandlers::Default, &EditorReflectionProbeComponent::m_useBakedCubemap, "Use Baked Cubemap", "Selects between a cubemap that captures the environment at location in the scene or a preauthored cubemap")
@@ -111,6 +119,11 @@ namespace AZ
                                 ->Attribute(AZ::Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
                             ->DataElement(AZ::Edit::UIHandlers::CheckBox, &ReflectionProbeComponentConfig::m_showVisualization, "Show Visualization", "Show the reflection probe visualization sphere")
                                 ->Attribute(AZ::Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
+                            ->DataElement(AZ::Edit::UIHandlers::Slider, &ReflectionProbeComponentConfig::m_renderExposure, "Exposure", "Exposure to use when rendering meshes with the cubemap")
+                                ->Attribute(AZ::Edit::Attributes::SoftMin, -5.0f)
+                                ->Attribute(AZ::Edit::Attributes::SoftMax, 5.0f)
+                                ->Attribute(AZ::Edit::Attributes::Min, -20.0f)
+                                ->Attribute(AZ::Edit::Attributes::Max, 20.0f)
                         ;
                 }
             }
@@ -275,6 +288,13 @@ namespace AZ
             return AZ::Edit::PropertyRefreshLevels::None;
         }
 
+        AZ::u32 EditorReflectionProbeComponent::OnBakeExposureChanged()
+        {
+            m_controller.SetBakeExposure(m_bakeExposure);
+
+            return AZ::Edit::PropertyRefreshLevels::None;
+        }
+
         AZ::u32 EditorReflectionProbeComponent::GetBakedCubemapVisibilitySetting()
         {
             // controls specific to baked cubemaps call this to determine their visibility

+ 2 - 0
Gems/AtomLyIntegration/CommonFeatures/Code/Source/ReflectionProbe/EditorReflectionProbeComponent.h

@@ -55,6 +55,7 @@ namespace AZ
             // change notifications
             AZ::u32 OnUseBakedCubemapChanged();
             AZ::u32 OnAuthoredCubemapChanged();
+            AZ::u32 OnBakeExposureChanged();
 
             // retrieves visibility for baked or authored cubemap controls
             AZ::u32 GetBakedCubemapVisibilitySetting();
@@ -77,6 +78,7 @@ namespace AZ
             AZStd::string m_bakedCubeMapRelativePath;
             Data::Asset<RPI::StreamingImageAsset> m_bakedCubeMapAsset;
             Data::Asset<RPI::StreamingImageAsset> m_authoredCubeMapAsset;
+            float m_bakeExposure = 0.0f;
 
             // flag indicating if a cubemap bake is currently in progress
             AZStd::atomic_bool m_bakeInProgress = false;

+ 17 - 2
Gems/AtomLyIntegration/CommonFeatures/Code/Source/ReflectionProbe/ReflectionProbeComponentController.cpp

@@ -35,7 +35,7 @@ namespace AZ
             if (auto* serializeContext = azrtti_cast<SerializeContext*>(context))
             {
                 serializeContext->Class<ReflectionProbeComponentConfig>()
-                    ->Version(0)
+                    ->Version(1)
                     ->Field("OuterHeight", &ReflectionProbeComponentConfig::m_outerHeight)
                     ->Field("OuterLength", &ReflectionProbeComponentConfig::m_outerLength)
                     ->Field("OuterWidth", &ReflectionProbeComponentConfig::m_outerWidth)
@@ -49,7 +49,9 @@ namespace AZ
                     ->Field("AuthoredCubeMapAsset", &ReflectionProbeComponentConfig::m_authoredCubeMapAsset)
                     ->Field("EntityId", &ReflectionProbeComponentConfig::m_entityId)
                     ->Field("UseParallaxCorrection", &ReflectionProbeComponentConfig::m_useParallaxCorrection)
-                    ->Field("ShowVisualization", &ReflectionProbeComponentConfig::m_showVisualization);
+                    ->Field("ShowVisualization", &ReflectionProbeComponentConfig::m_showVisualization)
+                    ->Field("RenderExposure", &ReflectionProbeComponentConfig::m_renderExposure)
+                    ->Field("BakeExposure", &ReflectionProbeComponentConfig::m_bakeExposure);
             }
         }
 
@@ -157,6 +159,9 @@ namespace AZ
                 cubeMapAsset.QueueLoad();
                 Data::AssetBus::MultiHandler::BusConnect(cubeMapAsset.GetId());
             }
+
+            // set cubemap render exposure
+            m_featureProcessor->SetRenderExposure(m_handle, m_configuration.m_renderExposure);
         }
 
         void ReflectionProbeComponentController::Deactivate()
@@ -284,6 +289,16 @@ namespace AZ
             m_configuration.m_innerHeight = AZStd::min(m_configuration.m_innerHeight, m_configuration.m_outerHeight);
         }
 
+        void ReflectionProbeComponentController::SetBakeExposure(float bakeExposure)
+        {
+            if (!m_featureProcessor)
+            {
+                return;
+            }
+
+            m_featureProcessor->SetBakeExposure(m_handle, bakeExposure);
+        }
+
         void ReflectionProbeComponentController::BakeReflectionProbe(BuildCubeMapCallback callback, const AZStd::string& relativePath)
         {
             if (!m_featureProcessor)

+ 6 - 0
Gems/AtomLyIntegration/CommonFeatures/Code/Source/ReflectionProbe/ReflectionProbeComponentController.h

@@ -68,6 +68,9 @@ namespace AZ
             Data::Asset<RPI::StreamingImageAsset> m_bakedCubeMapAsset;
             Data::Asset<RPI::StreamingImageAsset> m_authoredCubeMapAsset;
             AZ::u64 m_entityId{ EntityId::InvalidEntityId };
+
+            float m_renderExposure = 0.0f;
+            float m_bakeExposure = 0.0f;
         };
 
         class ReflectionProbeComponentController final
@@ -99,6 +102,9 @@ namespace AZ
             // returns the outer extent Aabb for this reflection
             AZ::Aabb GetAabb() const;
 
+            // set the exposure to use when baking the cubemap
+            void SetBakeExposure(float bakeExposure);
+
             // initiate the reflection probe bake, invokes callback when complete
             void BakeReflectionProbe(BuildCubeMapCallback callback, const AZStd::string& relativePath);
 

+ 6 - 0
Gems/LyShine/Code/Editor/Animation/UiAnimViewDialog.cpp

@@ -257,6 +257,7 @@ BOOL CUiAnimViewDialog::OnInitDialog()
     m_wndSplitter->addWidget(m_wndDopeSheet);
     m_wndSplitter->setStretchFactor(0, 1);
     m_wndSplitter->setStretchFactor(1, 10);
+    m_wndSplitter->setChildrenCollapsible(false);
     l->addWidget(m_wndSplitter);
     w->setLayout(l);
     setCentralWidget(w);
@@ -283,6 +284,11 @@ BOOL CUiAnimViewDialog::OnInitDialog()
     m_wndCurveEditorDock->setVisible(false);
     m_wndCurveEditorDock->setEnabled(false);
 
+    // In order to prevent the track editor view from collapsing and becoming invisible, we use the
+    // minimum size of the curve editor for the track editor as well. Since both editors use the same
+    // view widget in the UI animation editor when not in 'Both' mode, the sizes can be identical.
+    m_wndDopeSheet->setMinimumSize(m_wndCurveEditor->minimumSizeHint());
+
     InitSequences();
 
     m_lazyInitDone = false;

+ 1 - 0
Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli

@@ -56,6 +56,7 @@ ShaderResourceGroup ObjectSrg : SRG_PerObject
         float m_padding;
         bool m_useReflectionProbe;
         bool m_useParallaxCorrection;
+        float m_exposure;
     };
 
     ReflectionProbeData m_reflectionProbeData;

+ 5 - 5
Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.cpp

@@ -74,7 +74,7 @@ namespace Terrain
 
                     ->DataElement(
                         AZ::Edit::UIHandlers::Default, &TerrainSurfaceMaterialsListConfig::m_surfaceMaterials,
-                        "Gradient to Material Mappings", "Maps surfaces to materials.");
+                        "Material Mappings", "Maps surfaces to materials.");
             }
         }
     }
@@ -123,7 +123,7 @@ namespace Terrain
             {
                 surfaceMaterialMapping.m_active = false;
                 surfaceMaterialMapping.m_materialAsset.QueueLoad();
-                AZ::Data::AssetBus::Handler::BusConnect(surfaceMaterialMapping.m_materialAsset.GetId());
+                AZ::Data::AssetBus::MultiHandler::BusConnect(surfaceMaterialMapping.m_materialAsset.GetId());
             }
         }
     }
@@ -136,7 +136,7 @@ namespace Terrain
         {
             if (surfaceMaterialMapping.m_materialAsset.GetId().IsValid())
             {
-                AZ::Data::AssetBus::Handler::BusDisconnect(surfaceMaterialMapping.m_materialAsset.GetId());
+                AZ::Data::AssetBus::MultiHandler::BusDisconnect(surfaceMaterialMapping.m_materialAsset.GetId());
                 surfaceMaterialMapping.m_materialAsset.Release();
                 surfaceMaterialMapping.m_materialInstance.reset();
                 surfaceMaterialMapping.m_activeMaterialAssetId = AZ::Data::AssetId();
@@ -202,7 +202,7 @@ namespace Terrain
                 // Don't disconnect from the AssetBus if this material is mapped more than once.
                 if (CountMaterialIDInstances(surfaceMaterialMapping.m_activeMaterialAssetId) == 1)
                 {
-                    AZ::Data::AssetBus::Handler::BusDisconnect(surfaceMaterialMapping.m_activeMaterialAssetId);
+                    AZ::Data::AssetBus::MultiHandler::BusDisconnect(surfaceMaterialMapping.m_activeMaterialAssetId);
                 }
 
                 surfaceMaterialMapping.m_activeMaterialAssetId = AZ::Data::AssetId();
@@ -238,7 +238,7 @@ namespace Terrain
             // All materials have been deactivated, stop listening for requests and notifications.
             m_cachedAabb = AZ::Aabb::CreateNull();
             LmbrCentral::ShapeComponentNotificationsBus::Handler::BusDisconnect();
-            TerrainAreaMaterialRequestBus::Handler::BusConnect(GetEntityId());
+            TerrainAreaMaterialRequestBus::Handler::BusDisconnect();
         }
     }
 

+ 1 - 1
Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.h

@@ -53,7 +53,7 @@ namespace Terrain
     class TerrainSurfaceMaterialsListComponent
         : public AZ::Component
         , private TerrainAreaMaterialRequestBus::Handler
-        , private AZ::Data::AssetBus::Handler
+        , private AZ::Data::AssetBus::MultiHandler
         , private LmbrCentral::ShapeComponentNotificationsBus::Handler
     {
     public:

+ 82 - 80
Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp

@@ -6,15 +6,13 @@
  *
  */
 
+#include <AzTest/AzTest.h>
+
 #include <AzCore/Component/ComponentApplication.h>
 #include <AzCore/Component/TransformBus.h>
 #include <AzCore/Memory/MemoryComponent.h>
 
-#include <AzFramework/Terrain/TerrainDataRequestBus.h>
-
 #include <Components/TerrainLayerSpawnerComponent.h>
-#include <LmbrCentral/Shape/BoxShapeComponentBus.h>
-#include <AzTest/AzTest.h>
 
 #include <Terrain/MockTerrain.h>
 #include <MockAxisAlignedBoxShapeComponent.h>
@@ -23,21 +21,12 @@ using ::testing::NiceMock;
 using ::testing::AtLeast;
 using ::testing::_;
 
-using ::testing::NiceMock;
-using ::testing::AtLeast;
-using ::testing::_;
-
 class LayerSpawnerComponentTest
     : public ::testing::Test
 {
 protected:
     AZ::ComponentApplication m_app;
 
-    AZStd::unique_ptr<AZ::Entity> m_entity;
-    Terrain::TerrainLayerSpawnerComponent* m_layerSpawnerComponent;
-    UnitTest::MockAxisAlignedBoxShapeComponent* m_shapeComponent;
-    AZStd::unique_ptr<NiceMock<UnitTest::MockTerrainSystemService>> m_terrainSystem;
-
     void SetUp() override
     {
         AZ::ComponentApplication::Descriptor appDesc;
@@ -50,78 +39,86 @@ protected:
 
     void TearDown() override
     {
-        m_entity.reset();
-        m_terrainSystem.reset();
         m_app.Destroy();
     }
 
-    void CreateEntity()
+    AZStd::unique_ptr<AZ::Entity> CreateEntity()
     {
-        m_entity = AZStd::make_unique<AZ::Entity>();
-        m_entity->Init();
+        auto entity = AZStd::make_unique<AZ::Entity>();
+        entity->Init();
 
-        ASSERT_TRUE(m_entity);
-    }
-
-    void AddLayerSpawnerAndShapeComponentToEntity()
-    {
-        AddLayerSpawnerAndShapeComponentToEntity(Terrain::TerrainLayerSpawnerConfig());
+        return entity;
     }
 
-    void AddLayerSpawnerAndShapeComponentToEntity(const Terrain::TerrainLayerSpawnerConfig& config)
+    Terrain::TerrainLayerSpawnerComponent* AddLayerSpawnerToEntity(AZ::Entity* entity, const Terrain::TerrainLayerSpawnerConfig& config)
     {
-        m_layerSpawnerComponent = m_entity->CreateComponent<Terrain::TerrainLayerSpawnerComponent>(config);
-        m_app.RegisterComponentDescriptor(m_layerSpawnerComponent->CreateDescriptor());
+        auto layerSpawnerComponent = entity->CreateComponent<Terrain::TerrainLayerSpawnerComponent>(config);
+        m_app.RegisterComponentDescriptor(layerSpawnerComponent->CreateDescriptor());
 
-        m_shapeComponent = m_entity->CreateComponent<UnitTest::MockAxisAlignedBoxShapeComponent>();
-        m_app.RegisterComponentDescriptor(m_shapeComponent->CreateDescriptor());
-
-        ASSERT_TRUE(m_layerSpawnerComponent);
-        ASSERT_TRUE(m_shapeComponent);
+        return layerSpawnerComponent;
     }
 
-    void CreateMockTerrainSystem()
+    UnitTest::MockAxisAlignedBoxShapeComponent* AddShapeComponentToEntity(AZ::Entity* entity)
     {
-        m_terrainSystem = AZStd::make_unique<NiceMock<UnitTest::MockTerrainSystemService>>();
+        UnitTest::MockAxisAlignedBoxShapeComponent* shapeComponent = entity->CreateComponent<UnitTest::MockAxisAlignedBoxShapeComponent>();
+        m_app.RegisterComponentDescriptor(shapeComponent->CreateDescriptor());
+
+        return shapeComponent;
     }
 };
 
-TEST_F(LayerSpawnerComponentTest, ActivatEntityActivateSuccess)
+TEST_F(LayerSpawnerComponentTest, ActivateEntityWithoutShapeFails)
+{
+    auto entity = CreateEntity();
+
+    AddLayerSpawnerToEntity(entity.get(), Terrain::TerrainLayerSpawnerConfig());
+
+    const AZ::Entity::DependencySortOutcome sortOutcome = entity->EvaluateDependenciesGetDetails();
+    EXPECT_FALSE(sortOutcome.IsSuccess());
+
+    entity.reset();
+}
+
+TEST_F(LayerSpawnerComponentTest, ActivateEntityActivateSuccess)
 {
-    CreateEntity();
-    AddLayerSpawnerAndShapeComponentToEntity();
+    auto entity = CreateEntity();
+
+    AddLayerSpawnerToEntity(entity.get(), Terrain::TerrainLayerSpawnerConfig());
+    AddShapeComponentToEntity(entity.get());
 
-    m_entity->Activate();
-    EXPECT_EQ(m_entity->GetState(), AZ::Entity::State::Active);
-     
-    m_entity->Deactivate();
+    entity->Activate();
+    EXPECT_EQ(entity->GetState(), AZ::Entity::State::Active);
+
+    entity.reset();
 }
 
 TEST_F(LayerSpawnerComponentTest, LayerSpawnerDefaultValuesCorrect)
 {
-    CreateEntity();
-    AddLayerSpawnerAndShapeComponentToEntity();
+    auto entity = CreateEntity();
+    AddLayerSpawnerToEntity(entity.get(), Terrain::TerrainLayerSpawnerConfig());
+    AddShapeComponentToEntity(entity.get());
 
-    m_entity->Activate();
+    entity->Activate();
 
     AZ::u32 priority = 999, layer = 999;
-    Terrain::TerrainSpawnerRequestBus::Event(m_entity->GetId(), &Terrain::TerrainSpawnerRequestBus::Events::GetPriority, layer, priority);
+    Terrain::TerrainSpawnerRequestBus::Event(entity->GetId(), &Terrain::TerrainSpawnerRequestBus::Events::GetPriority, layer, priority);
 
     EXPECT_EQ(0, priority);
     EXPECT_EQ(1, layer);
 
     bool useGroundPlane = false;
 
-    Terrain::TerrainSpawnerRequestBus::EventResult(useGroundPlane, m_entity->GetId(),  &Terrain::TerrainSpawnerRequestBus::Events::GetUseGroundPlane);
+    Terrain::TerrainSpawnerRequestBus::EventResult(
+        useGroundPlane, entity->GetId(), &Terrain::TerrainSpawnerRequestBus::Events::GetUseGroundPlane);
 
     EXPECT_TRUE(useGroundPlane);
 
-    m_entity->Deactivate();
+    entity.reset();
 }
 
 TEST_F(LayerSpawnerComponentTest, LayerSpawnerConfigValuesCorrect)
 {
-    CreateEntity();
+    auto entity = CreateEntity();
 
     constexpr static AZ::u32 testPriority = 15;
     constexpr static AZ::u32 testLayer = 0;
@@ -131,12 +128,13 @@ TEST_F(LayerSpawnerComponentTest, LayerSpawnerConfigValuesCorrect)
     config.m_priority = testPriority;
     config.m_useGroundPlane = false;
 
-    AddLayerSpawnerAndShapeComponentToEntity(config);
+    AddLayerSpawnerToEntity(entity.get(), config);
+    AddShapeComponentToEntity(entity.get());
 
-    m_entity->Activate();
+    entity->Activate();
 
     AZ::u32 priority = 999, layer = 999;
-    Terrain::TerrainSpawnerRequestBus::Event(m_entity->GetId(), &Terrain::TerrainSpawnerRequestBus::Events::GetPriority, layer, priority);
+    Terrain::TerrainSpawnerRequestBus::Event(entity->GetId(), &Terrain::TerrainSpawnerRequestBus::Events::GetPriority, layer, priority);
 
     EXPECT_EQ(testPriority, priority);
     EXPECT_EQ(testLayer, layer);
@@ -144,82 +142,86 @@ TEST_F(LayerSpawnerComponentTest, LayerSpawnerConfigValuesCorrect)
     bool useGroundPlane = true;
 
     Terrain::TerrainSpawnerRequestBus::EventResult(
-        useGroundPlane, m_entity->GetId(), &Terrain::TerrainSpawnerRequestBus::Events::GetUseGroundPlane);
+        useGroundPlane, entity->GetId(), &Terrain::TerrainSpawnerRequestBus::Events::GetUseGroundPlane);
 
     EXPECT_FALSE(useGroundPlane);
 
-    m_entity->Deactivate();
+    entity.reset();
 }
 
 TEST_F(LayerSpawnerComponentTest, LayerSpawnerRegisterAreaUpdatesTerrainSystem)
 {
-    CreateEntity();
+    auto entity = CreateEntity();
 
-    CreateMockTerrainSystem();
+    NiceMock<UnitTest::MockTerrainSystemService> terrainSystem;
 
     // The Activate call should register the area.
-    EXPECT_CALL(*m_terrainSystem, RegisterArea(_)).Times(1);
+    EXPECT_CALL(terrainSystem, RegisterArea(_)).Times(1);
 
-    AddLayerSpawnerAndShapeComponentToEntity();
+    AddLayerSpawnerToEntity(entity.get(), Terrain::TerrainLayerSpawnerConfig());
+    AddShapeComponentToEntity(entity.get());
 
-    m_entity->Activate();
+    entity->Activate();
 
-    m_entity->Deactivate();
+    entity.reset();
 }
 
 TEST_F(LayerSpawnerComponentTest, LayerSpawnerUnregisterAreaUpdatesTerrainSystem)
 {
-    CreateEntity();
+    auto entity = CreateEntity();
 
-    CreateMockTerrainSystem();
+    NiceMock<UnitTest::MockTerrainSystemService> terrainSystem;
 
     // The Deactivate call should unregister the area.
-    EXPECT_CALL(*m_terrainSystem, UnregisterArea(_)).Times(1);
+    EXPECT_CALL(terrainSystem, UnregisterArea(_)).Times(1);
 
-    AddLayerSpawnerAndShapeComponentToEntity();
+    AddLayerSpawnerToEntity(entity.get(), Terrain::TerrainLayerSpawnerConfig());
+    AddShapeComponentToEntity(entity.get());
 
-    m_entity->Activate();
+    entity->Activate();
 
-    m_entity->Deactivate();
+    entity.reset();
 }
 
 TEST_F(LayerSpawnerComponentTest, LayerSpawnerTransformChangedUpdatesTerrainSystem)
 {
-    CreateEntity();
+    auto entity = CreateEntity();
 
-    CreateMockTerrainSystem();
+    NiceMock<UnitTest::MockTerrainSystemService> terrainSystem;
 
     // The TransformChanged call should refresh the area.
-    EXPECT_CALL(*m_terrainSystem, RefreshArea(_, _)).Times(1);
+    EXPECT_CALL(terrainSystem, RefreshArea(_, _)).Times(1);
 
-    AddLayerSpawnerAndShapeComponentToEntity();
+    AddLayerSpawnerToEntity(entity.get(), Terrain::TerrainLayerSpawnerConfig());
+    AddShapeComponentToEntity(entity.get());
 
-    m_entity->Activate();
+    entity->Activate();
 
     // The component gets transform change notifications via the shape bus.
     LmbrCentral::ShapeComponentNotificationsBus::Event(
-        m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged,
+        entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged,
         LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::TransformChanged);
 
-    m_entity->Deactivate();
+    entity.reset();
 }
 
 TEST_F(LayerSpawnerComponentTest, LayerSpawnerShapeChangedUpdatesTerrainSystem)
 {
-    CreateEntity();
+    auto entity = CreateEntity();
 
-    CreateMockTerrainSystem();
+    NiceMock<UnitTest::MockTerrainSystemService> terrainSystem;
 
     // The ShapeChanged call should refresh the area.
-    EXPECT_CALL(*m_terrainSystem, RefreshArea(_, _)).Times(1);
+    EXPECT_CALL(terrainSystem, RefreshArea(_, _)).Times(1);
 
-    AddLayerSpawnerAndShapeComponentToEntity();
+    AddLayerSpawnerToEntity(entity.get(), Terrain::TerrainLayerSpawnerConfig());
+    AddShapeComponentToEntity(entity.get());
 
-    m_entity->Activate();
+    entity->Activate();
 
-   LmbrCentral::ShapeComponentNotificationsBus::Event(
-        m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged,
+    LmbrCentral::ShapeComponentNotificationsBus::Event(
+        entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged,
         LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::ShapeChanged);
 
-    m_entity->Deactivate();
+    entity.reset();
 }

+ 12 - 0
Registry/Platform/Mac/bootstrap_overrides.setreg

@@ -0,0 +1,12 @@
+{
+    "Amazon": {
+        "AzCore": {
+            "Bootstrap": {
+                // The first time an application is launched on MacOS, each
+                // dynamic library is inspected by the OS before being loaded.
+                // This can take a while on some Macs.
+                "launch_ap_timeout": 300
+            }
+        }
+    }
+}

+ 2 - 0
Templates/PythonToolGem/Template/Code/CMakeLists.txt

@@ -53,6 +53,8 @@ if(PAL_TRAIT_BUILD_HOST_TOOLS)
         BUILD_DEPENDENCIES
             PUBLIC
                 Gem::${Name}.Editor.Static
+        RUNTIME_DEPENDENCIES
+            Gem::QtForPython.Editor
     )
 
     # By default, we will specify that the above target ${Name} would be used by

+ 4 - 1
Templates/PythonToolGem/Template/gem.json

@@ -13,5 +13,8 @@
         "${Name}"
     ],
     "icon_path": "preview.png",
-    "requirements": ""
+    "requirements": "",
+    "dependencies": [
+        "QtForPython"
+    ]
 }

+ 43 - 6
scripts/build/Jenkins/Jenkinsfile

@@ -16,7 +16,7 @@ EMPTY_JSON = readJSON text: '{}'
 ENGINE_REPOSITORY_NAME = 'o3de'
 
 // Branches with build snapshots
-BUILD_SNAPSHOTS = ['development', 'stabilization/2106']
+BUILD_SNAPSHOTS = ['development', 'stabilization/2110']
 
 // Build snapshots with empty snapshot (for use with 'SNAPSHOT' pipeline paramater)
 BUILD_SNAPSHOTS_WITH_EMPTY = BUILD_SNAPSHOTS + ''
@@ -102,6 +102,10 @@ def IsJobEnabled(branchName, buildTypeMap, pipelineName, platformName) {
     }
 }
 
+def IsAPLogUpload(branchName, jobName) {
+    return !IsPullRequest(branchName) && jobName.toLowerCase().contains('asset') && env.AP_LOGS_S3_BUCKET
+}
+
 def GetRunningPipelineName(JENKINS_JOB_NAME) {
     // If the job name has an underscore
     def job_parts = JENKINS_JOB_NAME.tokenize('/')[0].tokenize('_')
@@ -267,7 +271,7 @@ def CheckoutRepo(boolean disableSubmodules = false) {
     palRm('commitdate')
 }
 
-def HandleDriveMount(String snapshot, String repositoryName, String projectName, String pipeline, String branchName, String platform, String buildType, String workspace, boolean recreateVolume = false) {   
+def HandleDriveMount(String snapshot, String repositoryName, String projectName, String pipeline, String branchName, String platform, String buildType, String workspace, boolean recreateVolume = false) {
     unstash name: 'incremental_build_script'
 
     def pythonCmd = ''
@@ -277,9 +281,7 @@ def HandleDriveMount(String snapshot, String repositoryName, String projectName,
     if(recreateVolume) {
         palSh("${pythonCmd} ${INCREMENTAL_BUILD_SCRIPT_PATH} --action delete --repository_name ${repositoryName} --project ${projectName} --pipeline ${pipeline} --branch ${branchName} --platform ${platform} --build_type ${buildType}", 'Deleting volume', winSlashReplacement=false)
     }
-    timeout(5) {
-        palSh("${pythonCmd} ${INCREMENTAL_BUILD_SCRIPT_PATH} --action mount --snapshot ${snapshot} --repository_name ${repositoryName} --project ${projectName} --pipeline ${pipeline} --branch ${branchName} --platform ${platform} --build_type ${buildType}", 'Mounting volume', winSlashReplacement=false)
-    }
+    palSh("${pythonCmd} ${INCREMENTAL_BUILD_SCRIPT_PATH} --action mount --snapshot ${snapshot} --repository_name ${repositoryName} --project ${projectName} --pipeline ${pipeline} --branch ${branchName} --platform ${platform} --build_type ${buildType}", 'Mounting volume', winSlashReplacement=false)
 
     if(env.IS_UNIX) {
         sh label: 'Setting volume\'s ownership',
@@ -431,6 +433,27 @@ def ExportTestScreenshots(Map options, String branchName, String platformName, S
     }
 }
 
+def UploadAPLogs(Map options, String branchName, String platformName, String jobName, String workspace, Map params) {
+    dir("${workspace}/${ENGINE_REPOSITORY_NAME}") {
+        projects = params.CMAKE_LY_PROJECTS.split(",")
+        projects.each{ project ->
+            def apLogsPath = "${project}/user/log"
+            def s3UploadScriptPath = "scripts/build/tools/upload_to_s3.py"
+            if(env.IS_UNIX) {
+                pythonPath = "${options.PYTHON_DIR}/python.sh"
+            }
+            else {
+                pythonPath = "${options.PYTHON_DIR}/python.cmd"
+            }
+            def command = "${pythonPath} -u ${s3UploadScriptPath} --base_dir ${apLogsPath} " +
+                          "--file_regex \".*\" --bucket ${env.AP_LOGS_S3_BUCKET} " +
+                          "--search_subdirectories True --key_prefix ${env.JENKINS_JOB_NAME}/${branchName}/${env.BUILD_NUMBER}/${platformName}/${jobName} " +
+                          '--extra_args {\\"ACL\\":\\"bucket-owner-full-control\\"}'
+            palSh(command, "Uploading AP logs for job ${jobName} for branch ${branchName}", false)
+            }
+        }
+    }
+
 def PostBuildCommonSteps(String workspace, boolean mount = true) {
     echo 'Starting post-build common steps...'
 
@@ -494,6 +517,14 @@ def CreateExportTestScreenshotsStage(Map pipelineConfig, String branchName, Stri
     }
 }
 
+def CreateUploadAPLogsStage(Map pipelineConfig, String branchName, String platformName, String jobName, String workspace, Map params) {
+    return {
+        stage("${jobName}_upload_ap_logs") {
+            UploadAPLogs(pipelineConfig, branchName, platformName, jobName, workspace, params)
+        }
+    }
+}
+
 def CreateTeardownStage(Map environmentVars) {
     return {
         stage('Teardown') {
@@ -518,9 +549,11 @@ def CreateSingleNode(Map pipelineConfig, def platform, def build_job, Map envVar
                         CreateSetupStage(pipelineConfig, snapshot, repositoryName, projectName, pipelineName, branchName, platform.key, build_job.key, envVars, onlyMountEBSVolume).call()
 
                         if(build_job.value.steps) { //this is a pipe with many steps so create all the build stages
+                            pipelineEnvVars = GetBuildEnvVars(platform.value.PIPELINE_ENV ?: EMPTY_JSON, build_job.value.PIPELINE_ENV ?: EMPTY_JSON, pipelineName)
                             build_job.value.steps.each { build_step ->
                                 build_job_name = build_step
-                                envVars = GetBuildEnvVars(platform.value.PIPELINE_ENV ?: EMPTY_JSON, platform.value.build_types[build_step].PIPELINE_ENV ?: EMPTY_JSON, pipelineName)
+                                // This addition of maps makes it that the right operand will override entries if they overlap with the left operand
+                                envVars = pipelineEnvVars + GetBuildEnvVars(platform.value.PIPELINE_ENV ?: EMPTY_JSON, platform.value.build_types[build_step].PIPELINE_ENV ?: EMPTY_JSON, pipelineName)
                                 try {
                                     CreateBuildStage(pipelineConfig,  platform.key, build_step, envVars).call()
                                 }
@@ -543,6 +576,9 @@ def CreateSingleNode(Map pipelineConfig, def platform, def build_job, Map envVar
                                 error "Node disconnected during build: ${e}"  // Error raised to retry stage on a new node
                             }
                         }
+                        if (IsAPLogUpload(branchName, build_job_name)) {
+                            CreateUploadAPLogsStage(pipelineConfig, branchName, platform.key, build_job_name, envVars['WORKSPACE'], platform.value.build_types[build_job_name].PARAMETERS).call()
+                        }
                         // All other errors will be raised outside the retry block
                         currentResult = envVars['ON_FAILURE_MARK'] ?: 'FAILURE'
                         currentException = e.toString()
@@ -770,6 +806,7 @@ try {
         platform.value.build_types.each { build_job ->
             if (IsJobEnabled(branchName, build_job, pipelineName, platform.key)) {   // User can filter jobs, jobs are tagged by pipeline
                 def envVars = GetBuildEnvVars(platform.value.PIPELINE_ENV ?: EMPTY_JSON, build_job.value.PIPELINE_ENV ?: EMPTY_JSON, pipelineName)
+                envVars['JENKINS_JOB_NAME'] = env.JOB_NAME // Save original Jenkins job name to JENKINS_JOB_NAME
                 envVars['JOB_NAME'] = "${branchName}_${platform.key}_${build_job.key}" // backwards compatibility, some scripts rely on this
                 someBuildHappened = true