Bladeren bron

Merge branch 'development' into Atom/dmcdiar/ATOM-17874

Signed-off-by: dmcdiarmid-ly <[email protected]>

# Conflicts:
#	Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridrender-nomsaa_dx12_0.azshadervariant
#	Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridrender-nomsaa_null_0.azshadervariant
#	Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridrender-nomsaa_vulkan_0.azshadervariant
#	Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridrender.azshader
#	Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridrender_dx12_0.azshadervariant
#	Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridrender_null_0.azshadervariant
#	Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridrender_vulkan_0.azshadervariant
#	Gems/DiffuseProbeGrid/Code/Include/DiffuseProbeGrid/DiffuseProbeGridFeatureProcessorInterface.h
#	Gems/DiffuseProbeGrid/Code/Source/Components/DiffuseProbeGridComponentController.cpp
#	Gems/DiffuseProbeGrid/Code/Source/Components/DiffuseProbeGridComponentController.h
#	Gems/DiffuseProbeGrid/Code/Source/EditorComponents/EditorDiffuseProbeGridComponent.cpp
#	Gems/DiffuseProbeGrid/Code/Source/EditorComponents/EditorDiffuseProbeGridComponent.h
#	Gems/DiffuseProbeGrid/Code/Source/Render/DiffuseProbeGrid.h
#	Gems/DiffuseProbeGrid/Code/Source/Render/DiffuseProbeGridFeatureProcessor.cpp
#	Gems/DiffuseProbeGrid/Code/Source/Render/DiffuseProbeGridFeatureProcessor.h
dmcdiarmid-ly 2 jaren geleden
bovenliggende
commit
5ad6d2cb8d
100 gewijzigde bestanden met toevoegingen van 2684 en 1022 verwijderingen
  1. 6 4
      AutomatedTesting/Assets/TestAnim/scene_export_actor.py
  2. 6 4
      AutomatedTesting/Assets/TestAnim/scene_export_motion.py
  3. 7 6
      AutomatedTesting/Assets/ap_test_assets/script_reinsert.py
  4. 9 7
      AutomatedTesting/Editor/Scripts/auto_lod.py
  5. 7 6
      AutomatedTesting/Editor/Scripts/scene_mesh_to_prefab.py
  6. 3 1
      AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/export_chunks_builder.py
  7. 3 1
      AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoSceneFiles_OneWithPythonOneWithout_PythonOnlyRunsOnFirstScene/python_builder.py
  8. 7 6
      AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/script_basics/base_example.py
  9. 4 2
      AutomatedTesting/TestAssets/test_chunks_builder.py
  10. 2 2
      CMakeLists.txt
  11. 4 0
      Code/Editor/Controls/ConsoleSCB.cpp
  12. 18 0
      Code/Editor/Plugins/ComponentEntityEditorPlugin/ComponentEntityEditorPlugin.cpp
  13. 28 40
      Code/Editor/QtViewPaneManager.cpp
  14. 1 0
      Code/Editor/Style/Editor.qss
  15. 1 1
      Code/Editor/TrackView/AtomOutputFrameCapture.cpp
  16. 59 38
      Code/Framework/AzCore/AzCore/Settings/SettingsRegistryImpl.cpp
  17. 15 1
      Code/Framework/AzCore/AzCore/Settings/SettingsRegistryImpl.h
  18. 26 13
      Code/Framework/AzCore/AzCore/Settings/SettingsRegistryMergeUtils.cpp
  19. 2 2
      Code/Framework/AzFramework/AzFramework/DocumentPropertyEditor/DocumentAdapter.cpp
  20. 3 3
      Code/Framework/AzFramework/AzFramework/DocumentPropertyEditor/DocumentAdapter.h
  21. 5 3
      Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp
  22. 3 0
      Code/Framework/AzToolsFramework/AzToolsFramework/API/ViewPaneOptions.h
  23. 28 49
      Code/Framework/AzToolsFramework/AzToolsFramework/AssetDatabase/AssetDatabaseConnection.cpp
  24. 6 8
      Code/Framework/AzToolsFramework/AzToolsFramework/AssetDatabase/AssetDatabaseConnection.h
  25. 9 4
      Code/Framework/AzToolsFramework/AzToolsFramework/AssetEditor/AssetEditorWidget.cpp
  26. 1 0
      Code/Framework/AzToolsFramework/AzToolsFramework/AssetEditor/AssetEditorWidget.h
  27. 30 4
      Code/Framework/AzToolsFramework/AzToolsFramework/Slice/SliceDependencyBrowserComponent.cpp
  28. 320 283
      Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/DocumentPropertyEditor.cpp
  29. 42 31
      Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/DocumentPropertyEditor.h
  30. 123 0
      Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/DocumentPropertyEditorSettings.cpp
  31. 77 0
      Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/DocumentPropertyEditorSettings.h
  32. 518 0
      Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/FilterAdapter.cpp
  33. 123 0
      Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/FilterAdapter.h
  34. 1 1
      Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/IPropertyEditor.h
  35. 0 1
      Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/KeyQueryDPE.cpp
  36. 101 0
      Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/SettingsRegistrar.cpp
  37. 99 0
      Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/SettingsRegistrar.h
  38. 3 0
      Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyManagerComponent.cpp
  39. 1 1
      Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ReflectedPropertyEditor.cpp
  40. 2 1
      Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ReflectedPropertyEditor.hxx
  41. 3 3
      Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiManager.cpp
  42. 6 0
      Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake
  43. 49 26
      Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_linux.cpp
  44. 6 0
      Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_linux.h
  45. 2 0
      Code/Tools/AssetProcessor/assetprocessor_static_files.cmake
  46. 2 0
      Code/Tools/AssetProcessor/assetprocessor_test_files.cmake
  47. 26 12
      Code/Tools/AssetProcessor/native/AssetDatabase/AssetDatabase.cpp
  48. 2 2
      Code/Tools/AssetProcessor/native/AssetDatabase/AssetDatabase.h
  49. 21 4
      Code/Tools/AssetProcessor/native/AssetManager/SourceFileRelocator.cpp
  50. 146 0
      Code/Tools/AssetProcessor/native/AssetManager/Validators/LfsPointerFileValidator.cpp
  51. 50 0
      Code/Tools/AssetProcessor/native/AssetManager/Validators/LfsPointerFileValidator.h
  52. 117 60
      Code/Tools/AssetProcessor/native/AssetManager/assetProcessorManager.cpp
  53. 9 1
      Code/Tools/AssetProcessor/native/AssetManager/assetProcessorManager.h
  54. 8 5
      Code/Tools/AssetProcessor/native/tests/SourceFileRelocatorTests.cpp
  55. 29 25
      Code/Tools/AssetProcessor/native/tests/assetdatabase/AssetDatabaseTest.cpp
  56. 203 236
      Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.cpp
  57. 11 0
      Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.h
  58. 1 1
      Code/Tools/AssetProcessor/native/tests/assetmanager/JobDependencySubIdTests.cpp
  59. 5 1
      Code/Tools/AssetProcessor/native/tests/assetmanager/ModtimeScanningTests.cpp
  60. 1 0
      Code/Tools/AssetProcessor/native/tests/assetmanager/ModtimeScanningTests.h
  61. 142 0
      Code/Tools/AssetProcessor/native/tests/assetmanager/Validators/LfsPointerFileValidatorTests.cpp
  62. 30 0
      Code/Tools/AssetProcessor/native/tests/assetmanager/Validators/LfsPointerFileValidatorTests.h
  63. 2 2
      Code/Tools/AssetProcessor/native/ui/AssetDetailsPanel.cpp
  64. 3 3
      Code/Tools/AssetProcessor/native/ui/AssetDetailsPanel.h
  65. 8 8
      Code/Tools/AssetProcessor/native/ui/MainWindow.cpp
  66. 15 7
      Code/Tools/AssetProcessor/native/ui/SourceAssetDetailsPanel.cpp
  67. 31 29
      Code/Tools/AssetProcessor/native/unittests/AssetProcessingStateDataUnitTests.cpp
  68. 0 41
      Code/Tools/AssetProcessor/native/utilities/assetUtils.cpp
  69. 1 5
      Code/Tools/AssetProcessor/native/utilities/assetUtils.h
  70. 5 5
      Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp
  71. 5 5
      Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.h
  72. 4 1
      Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingResourceList.h
  73. 15 4
      Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.cpp
  74. 2 1
      Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.h
  75. 12 4
      Gems/Atom/Tools/ShaderManagementConsole/Code/Source/ShaderManagementConsoleApplication.cpp
  76. 1 1
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp
  77. 8 7
      Gems/Blast/Editor/Scripts/blast_chunk_processor.py
  78. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe1008_dx12_0.azshadervariant
  79. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe1008_null_0.azshadervariant
  80. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe1008_vulkan_0.azshadervariant
  81. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe144_dx12_0.azshadervariant
  82. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe144_null_0.azshadervariant
  83. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe144_vulkan_0.azshadervariant
  84. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe288_dx12_0.azshadervariant
  85. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe288_null_0.azshadervariant
  86. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe288_vulkan_0.azshadervariant
  87. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe432_dx12_0.azshadervariant
  88. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe432_null_0.azshadervariant
  89. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe432_vulkan_0.azshadervariant
  90. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe576_dx12_0.azshadervariant
  91. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe576_null_0.azshadervariant
  92. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe576_vulkan_0.azshadervariant
  93. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe720_dx12_0.azshadervariant
  94. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe720_null_0.azshadervariant
  95. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe720_vulkan_0.azshadervariant
  96. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe864_dx12_0.azshadervariant
  97. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe864_null_0.azshadervariant
  98. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe864_vulkan_0.azshadervariant
  99. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance.azshader
  100. BIN
      Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance_dx12_0.azshadervariant

+ 6 - 4
AutomatedTesting/Assets/TestAnim/scene_export_actor.py

@@ -205,10 +205,12 @@ def on_update_manifest(args):
         log_exception_traceback()
     except:
         log_exception_traceback()
-
-    global sceneJobHandler
-    sceneJobHandler.disconnect()
-    sceneJobHandler = None
+    finally:
+        global sceneJobHandler
+        # do not delete or set sceneJobHandler to None, just disconnect from it.
+        # this call is occuring while the scene Job Handler itself is in the callstack, so deleting it here
+        # would cause a crash.
+        sceneJobHandler.disconnect()
 
 # try to create SceneAPI handler for processing
 try:

+ 6 - 4
AutomatedTesting/Assets/TestAnim/scene_export_motion.py

@@ -50,10 +50,12 @@ def on_update_manifest(args):
         scene_export_utils.log_exception_traceback()
     except:
         scene_export_utils.log_exception_traceback()
-
-    global sceneJobHandler
-    sceneJobHandler.disconnect()
-    sceneJobHandler = None
+    finally:
+        global sceneJobHandler
+        # do not delete or set sceneJobHandler to None, just disconnect from it.
+        # this call is occuring while the scene Job Handler itself is in the callstack, so deleting it here
+        # would cause a crash.
+        sceneJobHandler.disconnect()
 
 # try to create SceneAPI handler for processing
 try:

+ 7 - 6
AutomatedTesting/Assets/ap_test_assets/script_reinsert.py

@@ -10,8 +10,10 @@ sceneJobHandler = None
 
 def clear_sceneJobHandler():
     global sceneJobHandler
+    # do not delete or set sceneJobHandler to None, just disconnect from it.
+    # this call is occuring while the scene Job Handler itself is in the callstack, so deleting it here
+    # would cause a crash.
     sceneJobHandler.disconnect()
-    sceneJobHandler = None
 
 def on_prepare_for_export(args): 
     print (f'on_prepare_for_export')
@@ -68,11 +70,10 @@ def on_update_manifest(args):
 try:
     import azlmbr.scene as sceneApi
     
-    if sceneJobHandler is None:
-        sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
-        sceneJobHandler.connect()
-        sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
-        sceneJobHandler.add_callback('OnPrepareForExport', on_prepare_for_export)
+    sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
+    sceneJobHandler.connect()
+    sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
+    sceneJobHandler.add_callback('OnPrepareForExport', on_prepare_for_export)
 
 except:
     sceneJobHandler = None

+ 9 - 7
AutomatedTesting/Editor/Scripts/auto_lod.py

@@ -109,16 +109,18 @@ def on_update_manifest(args):
         log_exception_traceback()
     except:
         log_exception_traceback()
-
-    global sceneJobHandler
-    sceneJobHandler = None
+    finally:
+        global sceneJobHandler
+        # do not delete or set sceneJobHandler to None, just disconnect from it.
+        # this call is occuring while the scene Job Handler itself is in the callstack, so deleting it here
+        # would cause a crash.
+        sceneJobHandler.disconnect()
 
 # try to create SceneAPI handler for processing
 try:
     import azlmbr.scene as sceneApi
-    if (sceneJobHandler == None):
-        sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
-        sceneJobHandler.connect()
-        sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
+    sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
+    sceneJobHandler.connect()
+    sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
 except:
     sceneJobHandler = None

+ 7 - 6
AutomatedTesting/Editor/Scripts/scene_mesh_to_prefab.py

@@ -235,18 +235,19 @@ def on_update_manifest(args):
         log_exception_traceback()
 
     global sceneJobHandler
+    # do not delete or set sceneJobHandler to None, just disconnect from it.
+    # this call is occuring while the scene Job Handler itself is in the callstack, so deleting it here
+    # would cause a crash.
     sceneJobHandler.disconnect()
-    sceneJobHandler = None
     return data
 
 
 # try to create SceneAPI handler for processing
 try:
     import azlmbr.scene as sceneApi
-
-    if sceneJobHandler is None:
-        sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
-        sceneJobHandler.connect()
-        sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
+    
+    sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
+    sceneJobHandler.connect()
+    sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
 except:
     sceneJobHandler = None

+ 3 - 1
AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/export_chunks_builder.py

@@ -63,8 +63,10 @@ def on_update_manifest(args):
     scene = args[0]
     result = update_manifest(scene)
     global mySceneJobHandler
+    # do not delete or set sceneJobHandler to None, just disconnect from it.
+    # this call is occuring while the scene Job Handler itself is in the callstack, so deleting it here
+    # would cause a crash.
     mySceneJobHandler.disconnect()
-    mySceneJobHandler = None
     return result
 
 def main():

+ 3 - 1
AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/TwoSceneFiles_OneWithPythonOneWithout_PythonOnlyRunsOnFirstScene/python_builder.py

@@ -31,8 +31,10 @@ def on_update_manifest(args):
     scene = args[0]
     result = output_test_data(scene)
     global mySceneJobHandler
+    # do not delete or set sceneJobHandler to None, just disconnect from it.
+    # this call is occuring while the scene Job Handler itself is in the callstack, so deleting it here
+    # would cause a crash.
     mySceneJobHandler.disconnect()
-    mySceneJobHandler = None
     return result
 
 def main():

+ 7 - 6
AutomatedTesting/Gem/PythonTests/assetpipeline/fbx_tests/assets/script_basics/base_example.py

@@ -10,8 +10,10 @@ sceneJobHandler = None
 
 def clear_sceneJobHandler():
     global sceneJobHandler
+    # do not delete or set sceneJobHandler to None, just disconnect from it.
+    # this call is occuring while the scene Job Handler itself is in the callstack, so deleting it here
+    # would cause a crash.
     sceneJobHandler.disconnect()
-    sceneJobHandler = None
 
 def on_prepare_for_export(args): 
     print (f'on_prepare_for_export')
@@ -68,11 +70,10 @@ def on_update_manifest(args):
 try:
     import azlmbr.scene as sceneApi
     
-    if sceneJobHandler is None:
-        sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
-        sceneJobHandler.connect()
-        sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
-        sceneJobHandler.add_callback('OnPrepareForExport', on_prepare_for_export)
+    sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
+    sceneJobHandler.connect()
+    sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
+    sceneJobHandler.add_callback('OnPrepareForExport', on_prepare_for_export)
 
 except:
     sceneJobHandler = None

+ 4 - 2
AutomatedTesting/TestAssets/test_chunks_builder.py

@@ -43,15 +43,17 @@ def on_update_manifest(args):
     scene = args[0]
     result = update_manifest(scene)
     global mySceneJobHandler
+    # do not delete or set sceneJobHandler to None, just disconnect from it.
+    # this call is occuring while the scene Job Handler itself is in the callstack, so deleting it here
+    # would cause a crash.
     mySceneJobHandler.disconnect()
-    mySceneJobHandler = None
     return result
 
 def main():
     global mySceneJobHandler
     mySceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
-    mySceneJobHandler.connect()
     mySceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
+    mySceneJobHandler.connect()
 
 if __name__ == "__main__":
     main()

+ 2 - 2
CMakeLists.txt

@@ -47,14 +47,14 @@ include(cmake/Subdirectories.cmake)
 include(cmake/TestImpactFramework/LYTestImpactFramework.cmake) # Put at end as nothing else depends on it
 
 # Gather the list of o3de_manifest external Subdirectories
-# into the LY_EXTERNAL_SUBDIRS_O3DE_MANIFEST_PROPERTY
+# into the O3DE_EXTERNAL_SUBDIRS_O3DE_MANIFEST_PROPERTY
 add_o3de_manifest_json_external_subdirectories()
 
 # Add the projects first so the Launcher can find them
 include(cmake/Projects.cmake)
 
 # Add external subdirectories listed in the engine.json.
-# LY_EXTERNAL_SUBDIRS is a cache variable so the user can add extra
+# O3DE_EXTERNAL_SUBDIRS is a cache variable so the user can add extra
 # external subdirectories.
 add_engine_json_external_subdirectories()
 

+ 4 - 0
Code/Editor/Controls/ConsoleSCB.cpp

@@ -389,6 +389,10 @@ void CConsoleSCB::RegisterViewClass()
     opts.showInMenu = true;
     opts.builtInActionId = ID_VIEW_CONSOLEWINDOW;
     opts.shortcut = QKeySequence(Qt::Key_QuoteLeft);
+    // Override the default behavior for component mode enter/exit and imgui enter/exit
+    // so that we don't disable and enable the Console window.
+    opts.isDisabledInComponentMode = false;
+    opts.isDisabledInImGuiMode = false;
 
     AzToolsFramework::RegisterViewPane<CConsoleSCB>(LyViewPane::Console, LyViewPane::CategoryTools, opts);
 }

+ 18 - 0
Code/Editor/Plugins/ComponentEntityEditorPlugin/ComponentEntityEditorPlugin.cpp

@@ -120,6 +120,11 @@ ComponentEntityEditorPlugin::ComponentEntityEditorPlugin([[maybe_unused]] IEdito
     ViewPaneOptions inspectorOptions;
     inspectorOptions.canHaveMultipleInstances = true;
     inspectorOptions.preferedDockingArea = Qt::RightDockWidgetArea;
+    // Override the default behavior for component mode enter/exit and imgui enter/exit
+    // so that we don't automatically disable and enable the entire Entity Inspector. This will be handled separately per-component.
+    inspectorOptions.isDisabledInComponentMode = false;
+    inspectorOptions.isDisabledInImGuiMode = false;
+
     RegisterViewPane<QComponentEntityEditorInspectorWindow>(
         LyViewPane::EntityInspector,
         LyViewPane::CategoryTools,
@@ -130,6 +135,11 @@ ComponentEntityEditorPlugin::ComponentEntityEditorPlugin([[maybe_unused]] IEdito
     pinnedInspectorOptions.preferedDockingArea = Qt::NoDockWidgetArea;
     pinnedInspectorOptions.paneRect = QRect(50, 50, 400, 700);
     pinnedInspectorOptions.showInMenu = false;
+    // Override the default behavior for component mode enter/exit and imgui enter/exit
+    // so that we don't automatically disable and enable the entire Pinned Entity Inspector. This will be handled separately per-component.
+    pinnedInspectorOptions.isDisabledInComponentMode = false;
+    pinnedInspectorOptions.isDisabledInImGuiMode = false;
+
     RegisterViewPane<QComponentEntityEditorInspectorWindow>(
         LyViewPane::EntityInspectorPinned,
         LyViewPane::CategoryTools,
@@ -145,6 +155,10 @@ ComponentEntityEditorPlugin::ComponentEntityEditorPlugin([[maybe_unused]] IEdito
         ViewPaneOptions outlinerOptions;
         outlinerOptions.canHaveMultipleInstances = true;
         outlinerOptions.preferedDockingArea = Qt::LeftDockWidgetArea;
+        // Override the default behavior for component mode enter/exit and imgui enter/exit
+        // so that we don't automatically disable and enable the Entity Outliner. This will be handled separately.
+        outlinerOptions.isDisabledInComponentMode = false;
+        outlinerOptions.isDisabledInImGuiMode = false;
 
         RegisterViewPane<QEntityOutlinerWindow>(
             LyViewPane::EntityOutliner,
@@ -164,6 +178,10 @@ ComponentEntityEditorPlugin::ComponentEntityEditorPlugin([[maybe_unused]] IEdito
         ViewPaneOptions outlinerOptions;
         outlinerOptions.canHaveMultipleInstances = true;
         outlinerOptions.preferedDockingArea = Qt::LeftDockWidgetArea;
+        // Override the default behavior for component mode enter/exit and imgui enter/exit
+        // so that we don't automatically disable and enable the Entity Outliner. This will be handled separately.
+        outlinerOptions.isDisabledInComponentMode = false;
+        outlinerOptions.isDisabledInImGuiMode = false;
 
         // this pane was originally introduced with this name, so layout settings are all saved with that name, despite the preview label being removed.
         outlinerOptions.saveKeyName = "Entity Outliner (PREVIEW)";

+ 28 - 40
Code/Editor/QtViewPaneManager.cpp

@@ -542,30 +542,11 @@ QString DockWidget::settingsKey(const QString& paneName)
     return QStringLiteral("ViewPane-") + paneName;
 }
 
-// run generic function on all widgets considered for greying out/disabling
-template<typename Fn>
-void SetDefaultActionsEnabled(
-    const bool enabled, QtViewPanes& registeredPanes, const Fn& fn)
+void EnableAllWidgetInstances(QList<DockWidget*>& widgetInstances, bool enable)
 {
-    for (QtViewPane& p : registeredPanes)
+    for (auto& dockWidget : widgetInstances)
     {
-        if (!p.m_dockWidgetInstances.empty())
-        {
-            for (auto& dockWidget : p.m_dockWidgetInstances)
-            {
-                const auto& paneName = dockWidget->PaneName();
-                // disable/fade all widgets other than those in the EntityInspector, EntityOutliner and Console
-                // note: The Console is not greyed out and the EntityInspector and EntityOutliner handle their
-                // own fading when entering/leaving ComponentMode
-                if (paneName != LyViewPane::EntityInspector &&
-                    paneName != LyViewPane::EntityInspectorPinned &&
-                    paneName != LyViewPane::Console &&
-                    paneName != LyViewPane::EntityOutliner)
-                {
-                    fn(dockWidget->widget(), enabled);
-                }
-            }
-        }
+        AzQtComponents::SetWidgetInteractEnabled(dockWidget->widget(), enable);
     }
 }
 
@@ -585,35 +566,42 @@ QtViewPaneManager::QtViewPaneManager(QObject* parent)
     m_windowRequest.BusConnect();
 
     m_componentModeNotifications->SetEnteredComponentModeFunc(
-        [this](const AzToolsFramework::ViewportEditorModesInterface&)
-    {
-        // gray out panels when entering ComponentMode
-        SetDefaultActionsEnabled(false, m_registeredPanes, [](QWidget* widget, bool on)
+        [this]([[maybe_unused]] const AzToolsFramework::ViewportEditorModesInterface& editorModes)
         {
-            AzQtComponents::SetWidgetInteractEnabled(widget, on);
+            for (QtViewPane& p : m_registeredPanes)
+            {
+                if (p.m_options.isDisabledInComponentMode)
+                {
+                    // By default, disable all widgets when entering Component Mode
+                    EnableAllWidgetInstances(p.m_dockWidgetInstances, false);
+                }
+            }
         });
-    });
 
     m_componentModeNotifications->SetLeftComponentModeFunc(
-        [this](const AzToolsFramework::ViewportEditorModesInterface&)
+        [this]([[maybe_unused]] const AzToolsFramework::ViewportEditorModesInterface& editorModes)
     {
-        // enable panels again when leaving ComponentMode
-        SetDefaultActionsEnabled(true, m_registeredPanes, [](QWidget* widget, bool on)
-        {
-            AzQtComponents::SetWidgetInteractEnabled(widget, on);
+            for (QtViewPane& p : m_registeredPanes)
+            {
+                if (p.m_options.isDisabledInComponentMode)
+                {
+                    // By default, enable all widgets again when leaving Component Mode
+                    EnableAllWidgetInstances(p.m_dockWidgetInstances, true);
+                }
+            }
         });
-    });
 
     m_windowRequest.SetEnableEditorUiFunc(
         [this](bool enable)
         {
-            // gray out panels when entering ImGui mode
-            SetDefaultActionsEnabled(
-                enable, m_registeredPanes,
-                [](QWidget* widget, bool on)
+            for (QtViewPane& p : m_registeredPanes)
+            {
+                if (p.m_options.isDisabledInImGuiMode)
                 {
-                    AzQtComponents::SetWidgetInteractEnabled(widget, on);
-                });
+                    // By default, disable/enable all widgets when entering/exiting IMGUI
+                    EnableAllWidgetInstances(p.m_dockWidgetInstances, enable);
+                }
+            }
         });
 }
 

+ 1 - 0
Code/Editor/Style/Editor.qss

@@ -95,6 +95,7 @@ Ui--AssetEditorHeader #Location
 }
 
 #AssetEditorWidgetPropertyEditor AzToolsFramework--PropertyRowWidget[getLevel="1"][hasChildRows="true"]
+, #AssetEditorWidgetPropertyEditor AzToolsFramework--DPERowWidget[getLevel="2"][hasChildRows="true"]
 {
     border-top: 1px solid rgba(255, 255, 255, 0.2);
 }

+ 1 - 1
Code/Editor/TrackView/AtomOutputFrameCapture.cpp

@@ -94,7 +94,7 @@ namespace TrackView
         m_captureFinishedCallback = AZStd::move(captureFinishedCallback);
 
         // note: "Output" (slot name) maps to MainPipeline.pass CopyToSwapChain
-        uint32_t frameCaptureId = AZ::Render::InvalidFrameCaptureId;
+        AZ::Render::FrameCaptureId frameCaptureId = AZ::Render::InvalidFrameCaptureId;
         AZ::Render::FrameCaptureRequestBus::BroadcastResult(
             frameCaptureId, &AZ::Render::FrameCaptureRequestBus::Events::CapturePassAttachmentWithCallback, m_passHierarchy,
             AZStd::string("Output"), attachmentReadbackCallback, AZ::RPI::PassAttachmentReadbackOption::Output);

+ 59 - 38
Code/Framework/AzCore/AzCore/Settings/SettingsRegistryImpl.cpp

@@ -58,7 +58,7 @@ namespace AZ
     {
         {
             // Push the file to be merged under protection of the Settings Mutex
-            AZStd::scoped_lock lock(m_settingsRegistry.m_settingMutex);
+            AZStd::scoped_lock lock(m_settingsRegistry.LockForWriting());
             m_settingsRegistry.m_mergeFilePathStack.emplace(m_mergeEventArgs.m_mergeFilePath);
         }
         m_settingsRegistry.m_preMergeEvent.Signal(mergeEventArgs);
@@ -70,7 +70,7 @@ namespace AZ
 
         {
             // Pop the file that finished merging under protection of the Settings Mutex
-            AZStd::scoped_lock lock(m_settingsRegistry.m_settingMutex);
+            AZStd::scoped_lock lock(m_settingsRegistry.LockForWriting());
             m_settingsRegistry.m_mergeFilePathStack.pop();
         }
     }
@@ -130,6 +130,8 @@ namespace AZ
         rapidjson::Pointer pointer(path.data(), path.length());
         if (pointer.IsValid())
         {
+            AZStd::scoped_lock lock(LockForReading());
+
             const rapidjson::Value* value = pointer.Get(m_settings);
             if constexpr (AZStd::is_same_v<T, bool>)
             {
@@ -197,7 +199,7 @@ namespace AZ
 
     void SettingsRegistryImpl::SetContext(SerializeContext* context)
     {
-        AZStd::scoped_lock lock(m_settingMutex);
+        AZStd::scoped_lock lock(LockForWriting());
 
         m_serializationSettings.m_serializeContext = context;
         m_deserializationSettings.m_serializeContext = context;
@@ -205,7 +207,7 @@ namespace AZ
 
     void SettingsRegistryImpl::SetContext(JsonRegistrationContext* context)
     {
-        AZStd::scoped_lock lock(m_settingMutex);
+        AZStd::scoped_lock lock(LockForWriting());
 
         m_serializationSettings.m_registrationContext = context;
         m_deserializationSettings.m_registrationContext = context;
@@ -224,7 +226,10 @@ namespace AZ
         rapidjson::Pointer pointer(path.data(), path.length());
         if (pointer.IsValid())
         {
-            AZStd::scoped_lock lock(m_settingMutex);
+            // During GetValue and Visit, we are not about to mutate
+            // the values in the registry, so do NOT call LockForWriting, just
+            // lock the mutex.
+            AZStd::scoped_lock lock(LockForReading());
             const rapidjson::Value* value = pointer.Get(m_settings);
             if (value)
             {
@@ -294,7 +299,7 @@ namespace AZ
     {
         PreMergeEventHandler preMergeHandler{ AZStd::move(callback) };
         {
-            AZStd::scoped_lock lock(m_settingMutex);
+            AZStd::scoped_lock lock(LockForWriting());
             preMergeHandler.Connect(m_preMergeEvent);
         }
         return preMergeHandler;
@@ -302,7 +307,7 @@ namespace AZ
 
     auto SettingsRegistryImpl::RegisterPreMergeEvent(PreMergeEventHandler& preMergeHandler) -> void
     {
-        AZStd::scoped_lock lock(m_settingMutex);
+        AZStd::scoped_lock lock(LockForWriting());
         preMergeHandler.Connect(m_preMergeEvent);
     }
 
@@ -310,7 +315,7 @@ namespace AZ
     {
         PostMergeEventHandler postMergeHandler{ AZStd::move(callback) };
         {
-            AZStd::scoped_lock lock(m_settingMutex);
+            AZStd::scoped_lock lock(LockForWriting());
             postMergeHandler.Connect(m_postMergeEvent);
         }
         return postMergeHandler;
@@ -318,13 +323,13 @@ namespace AZ
 
     auto SettingsRegistryImpl::RegisterPostMergeEvent(PostMergeEventHandler& postMergeHandler) -> void
     {
-        AZStd::scoped_lock lock(m_settingMutex);
+        AZStd::scoped_lock lock(LockForWriting());
         postMergeHandler.Connect(m_postMergeEvent);
     }
 
     void SettingsRegistryImpl::ClearMergeEvents()
     {
-        AZStd::scoped_lock lock(m_settingMutex);
+        AZStd::scoped_lock lock(LockForWriting());
         m_preMergeEvent.DisconnectAllHandlers();
         m_postMergeEvent.DisconnectAllHandlers();
     }
@@ -399,7 +404,7 @@ namespace AZ
         rapidjson::Pointer pointer(path.data(), path.length());
         if (pointer.IsValid())
         {
-            AZStd::scoped_lock lock(m_settingMutex);
+            AZStd::scoped_lock lock(LockForReading());
             return GetTypeNoLock(path);
         }
         return SettingsType{};
@@ -438,37 +443,31 @@ namespace AZ
 
     bool SettingsRegistryImpl::Get(bool& result, AZStd::string_view path) const
     {
-        AZStd::scoped_lock lock(m_settingMutex);
         return GetValueInternal(result, path);
     }
 
     bool SettingsRegistryImpl::Get(s64& result, AZStd::string_view path) const
     {
-        AZStd::scoped_lock lock(m_settingMutex);
         return GetValueInternal(result, path);
     }
 
     bool SettingsRegistryImpl::Get(u64& result, AZStd::string_view path) const
     {
-        AZStd::scoped_lock lock(m_settingMutex);
         return GetValueInternal(result, path);
     }
 
     bool SettingsRegistryImpl::Get(double& result, AZStd::string_view path) const
     {
-        AZStd::scoped_lock lock(m_settingMutex);
         return GetValueInternal(result, path);
     }
 
     bool SettingsRegistryImpl::Get(AZStd::string& result, AZStd::string_view path) const
     {
-        AZStd::scoped_lock lock(m_settingMutex);
         return GetValueInternal(result, path);
     }
 
     bool SettingsRegistryImpl::Get(FixedValueString& result, AZStd::string_view path) const
     {
-        AZStd::scoped_lock lock(m_settingMutex);
         return GetValueInternal(result, path);
     }
 
@@ -485,7 +484,7 @@ namespace AZ
         rapidjson::Pointer pointer(path.data(), path.length());
         if (pointer.IsValid())
         {
-            AZStd::scoped_lock lock(m_settingMutex);
+            AZStd::scoped_lock lock(LockForReading());
             const rapidjson::Value* value = pointer.Get(m_settings);
             if (value)
             {
@@ -498,7 +497,7 @@ namespace AZ
 
     bool SettingsRegistryImpl::Set(AZStd::string_view path, bool value)
     {
-        if (AZStd::scoped_lock lock(m_settingMutex); !SetValueInternal(path, value))
+        if (AZStd::scoped_lock lock(LockForWriting()); !SetValueInternal(path, value))
         {
             return false;
         }
@@ -508,7 +507,7 @@ namespace AZ
 
     bool SettingsRegistryImpl::Set(AZStd::string_view path, s64 value)
     {
-        if (AZStd::scoped_lock lock(m_settingMutex); !SetValueInternal(path, value))
+        if (AZStd::scoped_lock lock(LockForWriting()); !SetValueInternal(path, value))
         {
             return false;
         }
@@ -518,7 +517,7 @@ namespace AZ
 
     bool SettingsRegistryImpl::Set(AZStd::string_view path, u64 value)
     {
-        if (AZStd::scoped_lock lock(m_settingMutex); !SetValueInternal(path, value))
+        if (AZStd::scoped_lock lock(LockForWriting()); !SetValueInternal(path, value))
         {
             return false;
         }
@@ -528,7 +527,7 @@ namespace AZ
 
     bool SettingsRegistryImpl::Set(AZStd::string_view path, double value)
     {
-        if (AZStd::scoped_lock lock(m_settingMutex); !SetValueInternal(path, value))
+        if (AZStd::scoped_lock lock(LockForWriting()); !SetValueInternal(path, value))
         {
             return false;
         }
@@ -538,7 +537,7 @@ namespace AZ
 
     bool SettingsRegistryImpl::Set(AZStd::string_view path, AZStd::string_view value)
     {
-        if (AZStd::scoped_lock lock(m_settingMutex); !SetValueInternal(path, value))
+        if (AZStd::scoped_lock lock(LockForWriting()); !SetValueInternal(path, value))
         {
             return false;
         }
@@ -571,7 +570,7 @@ namespace AZ
             {
                 SettingsType anchorType;
                 {
-                    AZStd::scoped_lock lock(m_settingMutex);
+                    AZStd::scoped_lock lock(LockForWriting());
                     rapidjson::Value& setting = pointer.Create(m_settings, m_settings.GetAllocator());
                     setting = AZStd::move(store);
                     anchorType = GetTypeNoLock(path);
@@ -598,7 +597,7 @@ namespace AZ
             return false;
         }
 
-        AZStd::scoped_lock lock(m_settingMutex);
+        AZStd::scoped_lock lock(LockForWriting());
         return pointerPath.Erase(m_settings);
     }
 
@@ -726,7 +725,7 @@ namespace AZ
             {
                 rapidjson::Pointer pointer(AZ_SETTINGS_REGISTRY_HISTORY_KEY "/-");
                 AZ_Error("Settings Registry", false, R"(Anchor path "%.*s" is invalid.)", AZ_STRING_ARG(anchorKey));
-                AZStd::scoped_lock lock(m_settingMutex);
+                AZStd::scoped_lock lock(LockForWriting());
                 pointer.Create(m_settings, m_settings.GetAllocator()).SetObject()
                     .AddMember(rapidjson::StringRef("Error"), rapidjson::StringRef("Invalid anchor key."), m_settings.GetAllocator())
                     .AddMember(rapidjson::StringRef("Path"),
@@ -772,7 +771,8 @@ namespace AZ
 
         SettingsType anchorType;
         {
-            AZStd::scoped_lock lock(m_settingMutex);
+            AZStd::scoped_lock lock(LockForWriting());
+
             rapidjson::Value& anchorRoot = anchorPath.IsValid() ? anchorPath.Create(m_settings, m_settings.GetAllocator())
                 : m_settings;
 
@@ -824,7 +824,7 @@ namespace AZ
                     static_cast<int>(path.length()), path.data());
                 Pointer pointer(AZ_SETTINGS_REGISTRY_HISTORY_KEY "/-");
 
-                AZStd::scoped_lock lock(m_settingMutex);
+                AZStd::scoped_lock lock(LockForWriting());
                 Value pathValue(path.data(), aznumeric_caster(path.length()), m_settings.GetAllocator());
                 pointer.Create(m_settings, m_settings.GetAllocator()).SetObject()
                     .AddMember(StringRef("Error"), StringRef("Unable to read registry file."), m_settings.GetAllocator())
@@ -870,7 +870,7 @@ namespace AZ
         {
             AZ_Error("Settings Registry", false, "Folder path for the Setting Registry is too long: %.*s",
                 static_cast<int>(path.size()), path.data());
-            AZStd::scoped_lock lock(m_settingMutex);
+            AZStd::scoped_lock lock(LockForWriting());
             pointer.Create(m_settings, m_settings.GetAllocator()).SetObject()
                 .AddMember(StringRef("Error"), StringRef("Folder path for the Setting Registry is too long."), m_settings.GetAllocator())
                 .AddMember(StringRef("Path"), Value(path.data(), aznumeric_caster(path.length()), m_settings.GetAllocator()), m_settings.GetAllocator());
@@ -906,7 +906,7 @@ namespace AZ
                     if (fileList.size() >= MaxRegistryFolderEntries)
                     {
                         AZ_Error("Settings Registry", false, "Too many files in registry folder.");
-                        AZStd::scoped_lock lock(m_settingMutex);
+                        AZStd::scoped_lock lock(LockForWriting());
                         pointer.Create(m_settings, m_settings.GetAllocator()).SetObject()
                             .AddMember(StringRef("Error"), StringRef("Too many files in registry folder."), m_settings.GetAllocator())
                             .AddMember(StringRef("Path"), Value(folderPath.c_str(), aznumeric_caster(folderPath.Native().size()), m_settings.GetAllocator()), m_settings.GetAllocator())
@@ -1009,6 +1009,7 @@ namespace AZ
     SettingsRegistryInterface::VisitResponse SettingsRegistryImpl::Visit(Visitor& visitor, StackedString& path, AZStd::string_view valueName,
         const rapidjson::Value& value) const
     {
+        ++m_visitDepth;
         VisitResponse result;
         VisitArgs visitArgs(*this);
         switch (value.GetType())
@@ -1052,6 +1053,7 @@ namespace AZ
                     path.Push(fieldName);
                     if (Visit(visitor, path, fieldName, member.value) == VisitResponse::Done)
                     {
+                        --m_visitDepth;
                         return VisitResponse::Done;
                     }
                     path.Pop();
@@ -1063,6 +1065,7 @@ namespace AZ
                 visitArgs.m_fieldName = valueName;
                 if (visitor.Traverse(visitArgs, VisitAction::End) == VisitResponse::Done)
                 {
+                    --m_visitDepth;
                     return VisitResponse::Done;
                 }
             }
@@ -1085,6 +1088,7 @@ namespace AZ
                     entryName.remove_prefix(endIndex + 1);
                     if (Visit(visitor, path, entryName, entry) == VisitResponse::Done)
                     {
+                        --m_visitDepth;
                         return VisitResponse::Done;
                     }
                     counter++;
@@ -1097,6 +1101,7 @@ namespace AZ
                 visitArgs.m_fieldName = valueName;
                 if (visitor.Traverse(visitArgs, VisitAction::End) == VisitResponse::Done)
                 {
+                    --m_visitDepth;
                     return VisitResponse::Done;
                 }
             }
@@ -1148,6 +1153,8 @@ namespace AZ
             AZ_Assert(false, "Unsupported RapidJSON type: %i.", aznumeric_cast<int>(value.GetType()));
             result = VisitResponse::Done;
         }
+
+        --m_visitDepth;
         return result;
     }
 
@@ -1202,7 +1209,7 @@ namespace AZ
         AZ_Error("Settings Registry", false, R"(Two registry files in "%.*s" point to the same specialization: "%s" and "%s")",
             AZ_STRING_ARG(folderPath), lhs.m_relativePath.c_str(), rhs.m_relativePath.c_str());
 
-        AZStd::scoped_lock lock(m_settingMutex);
+        AZStd::scoped_lock lock(LockForWriting());
         historyPointer.Create(m_settings, m_settings.GetAllocator()).SetObject()
             .AddMember(StringRef("Error"), StringRef("Too many files in registry folder."), m_settings.GetAllocator())
             .AddMember(StringRef("Path"),
@@ -1363,7 +1370,7 @@ namespace AZ
                 }
             }
 
-            AZStd::scoped_lock lock(m_settingMutex);
+            AZStd::scoped_lock lock(LockForWriting());
             pointer.Create(m_settings, m_settings.GetAllocator()).SetObject()
                 .AddMember(StringRef("Error"), StringRef("Unable to parse registry file due to invalid json."), m_settings.GetAllocator())
                 .AddMember(StringRef("Path"), Value(path, m_settings.GetAllocator()), m_settings.GetAllocator())
@@ -1389,7 +1396,7 @@ namespace AZ
                     R"(To merge the supplied settings registry file, the settings within it must be placed within a JSON Object '{}')"
                     R"( in order to allow moving of its fields using the root-key as an anchor.)", path);
 
-                AZStd::scoped_lock lock(m_settingMutex);
+                AZStd::scoped_lock lock(LockForWriting());
                 pointer.Create(m_settings, m_settings.GetAllocator()).SetObject()
                     .AddMember(StringRef("Error"), StringRef("Cannot merge registry file with a root which is not a JSON Object,"
                         " an empty root key and a merge approach of JsonMergePatch. Otherwise the Settings Registry would be overridden."
@@ -1450,7 +1457,7 @@ namespace AZ
         SettingsType anchorType;
         if (rootKey.empty())
         {
-            AZStd::scoped_lock lock(m_settingMutex);
+            AZStd::scoped_lock lock(LockForWriting());
             mergeResult = JsonSerialization::ApplyPatch(m_settings, m_settings.GetAllocator(), jsonPatch, mergeApproach, applyPatchSettings);
             anchorType = GetTypeNoLock(rootKey);
         }
@@ -1459,7 +1466,7 @@ namespace AZ
             Pointer root(rootKey.data(), rootKey.length());
             if (root.IsValid())
             {
-                AZStd::scoped_lock lock(m_settingMutex);
+                AZStd::scoped_lock lock(LockForWriting());
                 Value& rootValue = root.Create(m_settings, m_settings.GetAllocator());
                 mergeResult = JsonSerialization::ApplyPatch(rootValue, m_settings.GetAllocator(), jsonPatch, mergeApproach, applyPatchSettings);
                 anchorType = GetTypeNoLock(rootKey);
@@ -1468,7 +1475,7 @@ namespace AZ
             {
                 AZ_Error("Settings Registry", false, R"(Failed to root path "%.*s" is invalid.)",
                     aznumeric_cast<int>(rootKey.length()), rootKey.data());
-                AZStd::scoped_lock lock(m_settingMutex);
+                AZStd::scoped_lock lock(LockForWriting());
                 pointer.Create(m_settings, m_settings.GetAllocator()).SetObject()
                     .AddMember(StringRef("Error"), StringRef("Invalid root key."), m_settings.GetAllocator())
                     .AddMember(StringRef("Path"), Value(path, m_settings.GetAllocator()), m_settings.GetAllocator());
@@ -1478,7 +1485,7 @@ namespace AZ
         if (mergeResult.GetProcessing() != JsonSerializationResult::Processing::Completed)
         {
             AZ_Error("Settings Registry", false, R"(Failed to fully merge registry file "%s".)", path);
-            AZStd::scoped_lock lock(m_settingMutex);
+            AZStd::scoped_lock lock(LockForWriting());
             pointer.Create(m_settings, m_settings.GetAllocator()).SetObject()
                 .AddMember(StringRef("Error"), StringRef("Failed to fully merge registry file."), m_settings.GetAllocator())
                 .AddMember(StringRef("Path"), Value(path, m_settings.GetAllocator()), m_settings.GetAllocator());
@@ -1486,7 +1493,7 @@ namespace AZ
         }
 
         {
-            AZStd::scoped_lock lock(m_settingMutex);
+            AZStd::scoped_lock lock(LockForWriting());
             pointer.Create(m_settings, m_settings.GetAllocator()).SetString(path, m_settings.GetAllocator());
         }
 
@@ -1514,4 +1521,18 @@ namespace AZ
     {
         m_useFileIo = useFileIo;
     }
+
+    AZStd::scoped_lock<AZStd::recursive_mutex> SettingsRegistryImpl::LockForWriting() const
+    {
+        // ensure that we aren't actively iterating over this data that is about to be
+        // invalid.
+        AZ_Assert(m_visitDepth == 0, "Attempt to mutate the Settings Registry while visiting, "
+            "this may invalidate visitor iterators and cause crashes.  Visit depth is %i", m_visitDepth);
+        return AZStd::scoped_lock(m_settingMutex);
+    }
+
+    AZStd::scoped_lock<AZStd::recursive_mutex> SettingsRegistryImpl::LockForReading() const
+    {
+        return AZStd::scoped_lock(m_settingMutex);
+    }
 } // namespace AZ

+ 15 - 1
Code/Framework/AzCore/AzCore/Settings/SettingsRegistryImpl.h

@@ -17,6 +17,7 @@
 #include <AzCore/std/containers/fixed_vector.h>
 #include <AzCore/std/containers/vector.h>
 #include <AzCore/std/parallel/mutex.h>
+#include <AzCore/std/parallel/scoped_lock.h>
 
 // Using a define instead of a static string to avoid the need for temporary buffers to composite the full paths.
 #define AZ_SETTINGS_REGISTRY_HISTORY_KEY "/Amazon/AzCore/Runtime/Registry/FileHistory"
@@ -117,7 +118,15 @@ namespace AZ
 
         void SignalNotifier(AZStd::string_view jsonPath, SettingsType type);
 
-        
+        //! Locks the m_settingMutex but also checks to make sure that someone is not currently
+        //! visiting/iterating over the registry, which is invalid if you're about to modify it
+        AZStd::scoped_lock<AZStd::recursive_mutex> LockForWriting() const;
+
+        //! For symmetry with the above, locks with intent to only read data.  This can be done
+        //! even during iteration/visiting.
+        AZStd::scoped_lock<AZStd::recursive_mutex> LockForReading() const;
+
+        // only use the setting mutex via the above functions.
         mutable AZStd::recursive_mutex m_settingMutex;
         mutable AZStd::recursive_mutex m_notifierMutex;
         NotifyEvent m_notifiers;
@@ -159,5 +168,10 @@ namespace AZ
         //! Stack tracking the files currently being merged
         //! This is protected by m_settingsMutex
         AZStd::stack<AZ::IO::FixedMaxPath> m_mergeFilePathStack;
+
+        // if this is nonzero, we are in a visit operation.  It can be used to detect illegal modifications
+        // of the tree during visit.
+        mutable int m_visitDepth = 0; // mutable due to it being a debugging value used in const.
+
     };
 } // namespace AZ

+ 26 - 13
Code/Framework/AzCore/AzCore/Settings/SettingsRegistryMergeUtils.cpp

@@ -796,20 +796,25 @@ namespace AZ::SettingsRegistryMergeUtils
 
     void MergeSettingsToRegistry_ManifestGemsPaths(SettingsRegistryInterface& registry)
     {
-        auto MergeGemPathToRegistry = [&registry](AZStd::string_view manifestKey,
-            AZStd::string_view gemName,
-            AZ::IO::PathView gemRootPath)
+        // cache a vector so that we don't mutate the registry while inside visitor iteration.
+        AZStd::vector<AZStd::pair<AZStd::string, AZ::IO::FixedMaxPath>> collectedGems;
+        auto CollectManifestGems =
+            [&collectedGems](AZStd::string_view manifestKey, AZStd::string_view gemName, AZ::IO::PathView gemRootPath)
         {
-            using FixedValueString = SettingsRegistryInterface::FixedValueString;
             if (manifestKey == GemNameKey)
             {
-                const auto manifestGemJsonPath = FixedValueString::format("%s/%.*s/Path",
-                    ManifestGemsRootKey, AZ_STRING_ARG(gemName));
-                registry.Set(manifestGemJsonPath, gemRootPath.LexicallyNormal().Native());
+                collectedGems.push_back(AZStd::make_pair(AZStd::string(gemName), AZ::IO::FixedMaxPath(gemRootPath)));
             }
         };
 
-        VisitAllManifestGems(registry, MergeGemPathToRegistry);
+        VisitAllManifestGems(registry, CollectManifestGems);
+
+        for (const auto& [gemName, gemRootPath] : collectedGems)
+        {
+            using FixedValueString = SettingsRegistryInterface::FixedValueString;
+            const auto manifestGemJsonPath = FixedValueString::format("%s/%.*s/Path", ManifestGemsRootKey, AZ_STRING_ARG(gemName));
+            registry.Set(manifestGemJsonPath, gemRootPath.LexicallyNormal().Native());
+        }
     }
 
     void MergeSettingsToRegistry_AddRuntimeFilePaths(SettingsRegistryInterface& registry)
@@ -994,13 +999,21 @@ namespace AZ::SettingsRegistryMergeUtils
     void MergeSettingsToRegistry_GemRegistries(SettingsRegistryInterface& registry, const AZStd::string_view platform,
         const SettingsRegistryInterface::Specializations& specializations, AZStd::vector<char>* scratchBuffer)
     {
-        auto MergeGemRootRegistryFolder = [&registry, &platform, &specializations, &scratchBuffer]
-        (AZStd::string_view, AZ::IO::FixedMaxPath gemPath)
+        // collect the paths first, then mutate the registry, so that we do not do any registry modifications while visiting it.
+        AZStd::vector<AZ::IO::FixedMaxPath> gemPaths;
+        auto CollectRegistryFolders = [&gemPaths]
+            (AZStd::string_view, AZ::IO::FixedMaxPath gemPath)
         {
-            registry.MergeSettingsFolder((gemPath / SettingsRegistryInterface::RegistryFolder).Native(),
-                specializations, platform, "", scratchBuffer);
+            gemPaths.push_back(gemPath);
         };
-        VisitActiveGems(registry, MergeGemRootRegistryFolder);
+        
+        VisitActiveGems(registry, CollectRegistryFolders);
+
+        for (const auto& gemPath : gemPaths)
+        {
+            registry.MergeSettingsFolder(
+                (gemPath / SettingsRegistryInterface::RegistryFolder).Native(), specializations, platform, "", scratchBuffer);
+        }
     }
 
     void MergeSettingsToRegistry_ProjectRegistry(SettingsRegistryInterface& registry, const AZStd::string_view platform,

+ 2 - 2
Code/Framework/AzFramework/AzFramework/DocumentPropertyEditor/DocumentAdapter.cpp

@@ -125,7 +125,7 @@ namespace AZ::DocumentPropertyEditor
         m_changedEvent.Signal(patch);
     }
 
-    Dom::Value DocumentAdapter::SendMessage(const AdapterMessage& message)
+    Dom::Value DocumentAdapter::SendAdapterMessage(const AdapterMessage& message)
     {
         // First, fire HandleMessage to allow descendants to handle the message.
         Dom::Value result = HandleMessage(message);
@@ -147,7 +147,7 @@ namespace AZ::DocumentPropertyEditor
         message.m_messageName = m_messageName;
         message.m_messageOrigin = m_messageOrigin;
         message.m_messageParameters = parameters;
-        return m_adapter->SendMessage(message);
+        return m_adapter->SendAdapterMessage(message);
     }
 
     Dom::Value BoundAdapterMessage::MarshalToDom() const

+ 3 - 3
Code/Framework/AzFramework/AzFramework/DocumentPropertyEditor/DocumentAdapter.h

@@ -129,7 +129,7 @@ namespace AZ::DocumentPropertyEditor
         //! The provided patch contains all the changes provided (i.e. it shall apply cleanly on top of the last
         //! GetContents() result).
         void ConnectChangedHandler(ChangedEvent::Handler& handler);
-        //! Connects a listener for the message event, fired when SendMessage is called.
+        //! Connects a listener for the message event, fired when SendAdapterMessage is called.
         //! is invoked. This can be used to prompt the view for a response, e.g. when asking for a confirmation dialog.
         void ConnectMessageHandler(MessageEvent::Handler& handler);
 
@@ -141,7 +141,7 @@ namespace AZ::DocumentPropertyEditor
         //! inspect and examine the message.
         //! AdapterMessage provides a Match method to facilitate checking the message against
         //! registered CallbackAttributes.
-        Dom::Value SendMessage(const AdapterMessage& message);
+        Dom::Value SendAdapterMessage(const AdapterMessage& message);
 
         //! If true, debug mode is enabled for all DocumentAdapters.
         //! \see SetDebugModeEnabled
@@ -159,7 +159,7 @@ namespace AZ::DocumentPropertyEditor
         //! \see AdapterBuilder for building out this DOM structure.
         virtual Dom::Value GenerateContents() = 0;
 
-        //! Called by SendMessage before the view is notified.
+        //! Called by SendAdapterMessage before the view is notified.
         //! This may be overridden to handle BoundAdapterMessages on fields.
         virtual Dom::Value HandleMessage(const AdapterMessage& message);
 

+ 5 - 3
Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.cpp

@@ -379,9 +379,11 @@ namespace AzFramework
 
             if (AZ::EditContext* editContext = serializeContext->GetEditContext())
             {
-                editContext->Class<EntitySpawnTicket>(
-                    "EntitySpawnTicket",
-                    "EntitySpawnTicket is an object used to spawn, identify, and track the spawned entities associated with the ticket.");
+                // Hide EntitySpawnTicket in editContext, as there is no proper edit time constructor
+                editContext->Class<EntitySpawnTicket>("EntitySpawnTicket",
+                        "EntitySpawnTicket is an object used to spawn, identify, and track the spawned entities associated with the ticket.")
+                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
+                        ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::Hide);
             }
         }
 

+ 3 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/API/ViewPaneOptions.h

@@ -41,6 +41,9 @@ namespace AzToolsFramework
 
         bool showOnToolsToolbar = false;                                ///< set to true if the view pane should create a button on the tools toolbar to open/close the pane
         AZStd::string toolbarIcon;                                      ///< path to the icon to use for the toolbar button - only used if showOnToolsToolbar is set to true
+
+        bool isDisabledInComponentMode = true;                          ///< set to false if the view pane should remain enabled during component mode
+        bool isDisabledInImGuiMode = true;                              ///< set to false if the view pane should remain enabled during imgui mode
     };
 
 } // namespace AzToolsFramework

+ 28 - 49
Code/Framework/AzToolsFramework/AzToolsFramework/AssetDatabase/AssetDatabaseConnection.cpp

@@ -694,12 +694,10 @@ namespace AzToolsFramework
             static const char* QUERY_SOURCEDEPENDENCY_BY_DEPENDSONSOURCE_STATEMENT =
                 "SELECT * from SourceDependency WHERE "
                 "DependsOnSource = :dependsOnSource AND "
-                "TypeOfDependency & :typeOfDependency AND "
-                "Source LIKE :dependentFilter;";
+                "TypeOfDependency & :typeOfDependency;";
 
             static const auto s_querySourcedependencyByDependsonsource = MakeSqlQuery(QUERY_SOURCEDEPENDENCY_BY_DEPENDSONSOURCE, QUERY_SOURCEDEPENDENCY_BY_DEPENDSONSOURCE_STATEMENT, LOG_NAME,
                 SqlParam<const char*>(":dependsOnSource"),
-                SqlParam<const char*>(":dependentFilter"),
                 SqlParam<AZ::u32>(":typeOfDependency"));
 
             static const char* QUERY_SOURCEDEPENDENCY_BY_DEPENDSONSOURCE_WILDCARD = "AzToolsFramework::AssetDatabase::QuerySourceDependencyByDependsOnSourceWildcard";
@@ -708,12 +706,10 @@ namespace AzToolsFramework
                 "((TypeOfDependency & :typeOfDependency AND "
                 "DependsOnSource = :dependsOnSource) OR "
                 "(TypeOfDependency = :wildCardDependency AND "
-                ":dependsOnSource LIKE DependsOnSource)) AND "
-                "Source LIKE :dependentFilter;";
+                ":dependsOnSource LIKE DependsOnSource));";
 
             static const auto s_querySourcedependencyByDependsonsourceWildcard = MakeSqlQuery(QUERY_SOURCEDEPENDENCY_BY_DEPENDSONSOURCE_WILDCARD, QUERY_SOURCEDEPENDENCY_BY_DEPENDSONSOURCE_WILDCARD_STATEMENT, LOG_NAME,
                 SqlParam<const char*>(":dependsOnSource"),
-                SqlParam<const char*>(":dependentFilter"),
                 SqlParam<AZ::u32>(":typeOfDependency"),
                 SqlParam<AZ::u32>(":wildCardDependency"));
 
@@ -729,32 +725,14 @@ namespace AzToolsFramework
             static const auto s_queryProductDependenciesThatDependOnProductBySourceId = MakeSqlQuery(QUERY_PRODUCTDEPENDENCIES_THAT_DEPEND_ON_PRODUCT_BY_SOURCEID, QUERY_PRODUCTDEPENDENCIES_THAT_DEPEND_ON_PRODUCT_BY_SOURCEID_STATEMENT, LOG_NAME,
                 SqlParam<AZ::s64>(":sourceid"));
 
-            static const char* QUERY_ALL_SOURCEDEPENDENCY_BY_DEPENDSONSOURCE = "AzToolsFramework::AssetDatabase::QueryAllSourceDependencyByDependsOnSource";
-            static const char* QUERY_ALL_SOURCEDEPENDENCY_BY_DEPENDSONSOURCE_STATEMENT =
-                "WITH RECURSIVE "
-                "    allSourceDeps AS ( "
-                "        SELECT * FROM SourceDependency "
-                "        WHERE DependsOnSource = :dependsOnSource "
-                "        AND TypeOfDependency & :typeOfDependency "
-                "        UNION "
-                "        SELECT SourceDependency.* FROM SourceDependency, allSourceDeps "
-                "        WHERE SourceDependency.DependsOnSource = allSourceDeps.Source "
-                "        AND SourceDependency.TypeOfDependency & :typeOfDependency "
-                "    ) "
-                "SELECT * FROM allSourceDeps;";
-
-            static const auto s_queryAllSourceDependencyByDependsOnSource = MakeSqlQuery(QUERY_ALL_SOURCEDEPENDENCY_BY_DEPENDSONSOURCE, QUERY_ALL_SOURCEDEPENDENCY_BY_DEPENDSONSOURCE_STATEMENT, LOG_NAME,
-                SqlParam<const char*>(":dependsOnSource"),
-                SqlParam<AZ::u32>(":typeOfDependency"));
-
             static const char* QUERY_DEPENDSONSOURCE_BY_SOURCE = "AzToolsFramework::AssetDatabase::QueryDependsOnSourceBySource";
             static const char* QUERY_DEPENDSONSOURCE_BY_SOURCE_STATEMENT =
                 "SELECT * from SourceDependency WHERE "
-                "Source = :source AND "
+                "SourceGuid = :source AND "
                 "TypeOfDependency & :typeOfDependency AND "
                 "DependsOnSource LIKE :dependencyFilter;";
-            static const auto s_queryDependsonsourceBySource = MakeSqlQuery(QUERY_DEPENDSONSOURCE_BY_SOURCE, QUERY_DEPENDSONSOURCE_BY_SOURCE_STATEMENT, LOG_NAME,
-                SqlParam<const char*>(":source"),
+            static const auto s_queryDependsOnSourceBySource = MakeSqlQuery(QUERY_DEPENDSONSOURCE_BY_SOURCE, QUERY_DEPENDSONSOURCE_BY_SOURCE_STATEMENT, LOG_NAME,
+                SqlParam<AZ::Uuid>(":source"),
                 SqlParam<const char*>(":dependencyFilter"),
                 SqlParam<AZ::u32>(":typeOfDependency"));
 
@@ -1226,9 +1204,9 @@ namespace AzToolsFramework
         //////////////////////////////////////////////////////////////////////////
         //SourceFileDependencyEntry
 
-        SourceFileDependencyEntry::SourceFileDependencyEntry(AZ::Uuid builderGuid, const char *source, const char* dependsOnSource, SourceFileDependencyEntry::TypeOfDependency dependencyType, AZ::u32 fromAssetId, const char* subIds)
+        SourceFileDependencyEntry::SourceFileDependencyEntry(AZ::Uuid builderGuid, AZ::Uuid sourceGuid, const char* dependsOnSource, SourceFileDependencyEntry::TypeOfDependency dependencyType, AZ::u32 fromAssetId, const char* subIds)
             : m_builderGuid(builderGuid)
-            , m_source(source)
+            , m_sourceGuid(sourceGuid)
             , m_dependsOnSource(dependsOnSource)
             , m_typeOfDependency(dependencyType)
             , m_fromAssetId(fromAssetId)
@@ -1239,8 +1217,16 @@ namespace AzToolsFramework
 
         AZStd::string SourceFileDependencyEntry::ToString() const
         {
-            return AZStd::string::format("SourceFileDependencyEntry id:%" PRId64 " builderGuid: %s source: %s dependsOnSource: %s type: %s fromAssetId: %u subIds: %s",
-                static_cast<int64_t>(m_sourceDependencyID), m_builderGuid.ToString<AZStd::string>().c_str(), m_source.c_str(), m_dependsOnSource.c_str(), m_typeOfDependency == DEP_SourceToSource ? "source" : "job", m_fromAssetId, m_subIds.c_str());
+            return AZStd::string::format(
+                "SourceFileDependencyEntry id:%" PRId64 " builderGuid: %s source: %s "
+                " dependsOnSource: %s type: %s fromAssetId: %u subIds: %s",
+                static_cast<int64_t>(m_sourceDependencyID),
+                m_builderGuid.ToFixedString().c_str(),
+                m_sourceGuid.ToFixedString().c_str(),
+                m_dependsOnSource.c_str(),
+                m_typeOfDependency == DEP_SourceToSource ? "source" : "job",
+                m_fromAssetId,
+                m_subIds.c_str());
         }
 
         auto SourceFileDependencyEntry::GetColumns()
@@ -1248,7 +1234,7 @@ namespace AzToolsFramework
             return MakeColumns(
                 MakeColumn("SourceDependencyID", m_sourceDependencyID),
                 MakeColumn("BuilderGuid", m_builderGuid),
-                MakeColumn("Source", m_source),
+                MakeColumn("SourceGuid", m_sourceGuid),
                 MakeColumn("DependsOnSource", m_dependsOnSource),
                 MakeColumn("TypeOfDependency", m_typeOfDependency),
                 MakeColumn("FromAssetId", m_fromAssetId),
@@ -1903,8 +1889,7 @@ namespace AzToolsFramework
             AddStatement(m_databaseConnection, s_querySourcedependencyBySourcedependencyid);
             AddStatement(m_databaseConnection, s_querySourcedependencyByDependsonsource);
             AddStatement(m_databaseConnection, s_querySourcedependencyByDependsonsourceWildcard);
-            AddStatement(m_databaseConnection, s_queryDependsonsourceBySource);
-            AddStatement(m_databaseConnection, s_queryAllSourceDependencyByDependsOnSource);
+            AddStatement(m_databaseConnection, s_queryDependsOnSourceBySource);
 
             AddStatement(m_databaseConnection, s_queryProductdependencyByProductdependencyid);
             AddStatement(m_databaseConnection, s_queryProductdependencyByProductid);
@@ -2110,8 +2095,7 @@ namespace AzToolsFramework
                     source = AZStd::move(combined);
                     handler(source);
                     return false;//one
-                }, {},
-                nullptr);
+                });
         }
 
         bool AssetDatabaseConnection::QuerySourceByProductID(AZ::s64 productid, sourceHandler handler)
@@ -2123,7 +2107,7 @@ namespace AzToolsFramework
                     source = AZStd::move(combined);
                     handler(source);
                     return false;//one
-                }, {});
+                });
         }
 
         bool AssetDatabaseConnection::QuerySourceBySourceGuid(AZ::Uuid sourceGuid, sourceHandler handler)
@@ -2558,31 +2542,26 @@ namespace AzToolsFramework
             return s_querySourcedependencyBySourcedependencyid.BindAndQuery(*m_databaseConnection, handler, &GetSourceDependencyResult, sourceDependencyID);
         }
 
-        bool AssetDatabaseConnection::QuerySourceDependencyByDependsOnSource(const char* dependsOnSource, const char* dependentFilter, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency dependencyType, sourceFileDependencyHandler handler)
+        bool AssetDatabaseConnection::QuerySourceDependencyByDependsOnSource(const char* dependsOnSource, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency dependencyType, sourceFileDependencyHandler handler)
         {
             if (dependencyType & AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceLikeMatch)
             {
-                return QuerySourceDependencyByDependsOnSourceWildcard(dependsOnSource, dependentFilter, handler);
+                return QuerySourceDependencyByDependsOnSourceWildcard(dependsOnSource, handler);
             }
-            return s_querySourcedependencyByDependsonsource.BindAndQuery(*m_databaseConnection, handler, &GetSourceDependencyResult, dependsOnSource, dependentFilter == nullptr ? "%" : dependentFilter, dependencyType);
+            return s_querySourcedependencyByDependsonsource.BindAndQuery(*m_databaseConnection, handler, &GetSourceDependencyResult, dependsOnSource, dependencyType);
         }
 
-        bool AssetDatabaseConnection::QuerySourceDependencyByDependsOnSourceWildcard(const char* dependsOnSource, const char* dependentFilter, sourceFileDependencyHandler handler)
+        bool AssetDatabaseConnection::QuerySourceDependencyByDependsOnSourceWildcard(const char* dependsOnSource, sourceFileDependencyHandler handler)
         {
             SourceFileDependencyEntry::TypeOfDependency matchDependency = SourceFileDependencyEntry::TypeOfDependency::DEP_SourceOrJob;
             SourceFileDependencyEntry::TypeOfDependency wildcardDependency = SourceFileDependencyEntry::TypeOfDependency::DEP_SourceLikeMatch;
 
-            return s_querySourcedependencyByDependsonsourceWildcard.BindAndQuery(*m_databaseConnection, handler, &GetSourceDependencyResult, dependsOnSource, dependentFilter == nullptr ? "%" : dependentFilter, matchDependency, wildcardDependency);
-        }
-
-        bool AssetDatabaseConnection::QueryDependsOnSourceBySourceDependency(const char* sourceDependency, const char* dependencyFilter, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency dependencyType, sourceFileDependencyHandler handler)
-        {
-            return s_queryDependsonsourceBySource.BindAndQuery(*m_databaseConnection, handler, &GetSourceDependencyResult, sourceDependency, dependencyFilter == nullptr ? "%" : dependencyFilter, dependencyType);
+            return s_querySourcedependencyByDependsonsourceWildcard.BindAndQuery(*m_databaseConnection, handler, &GetSourceDependencyResult, dependsOnSource, matchDependency, wildcardDependency);
         }
 
-        bool AssetDatabaseConnection::QueryAllSourceDependencyByDependsOnSource(const char* dependsOnSource, SourceFileDependencyEntry::TypeOfDependency dependencyType, sourceFileDependencyHandler handler)
+        bool AssetDatabaseConnection::QueryDependsOnSourceBySourceDependency(AZ::Uuid sourceGuid, const char* dependencyFilter, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency dependencyType, sourceFileDependencyHandler handler)
         {
-            return s_queryAllSourceDependencyByDependsOnSource.BindAndQuery(*m_databaseConnection, handler, &GetSourceDependencyResult, dependsOnSource, dependencyType);
+            return s_queryDependsOnSourceBySource.BindAndQuery(*m_databaseConnection, handler, &GetSourceDependencyResult, sourceGuid, dependencyFilter == nullptr ? "%" : dependencyFilter, dependencyType);
         }
 
         bool AzToolsFramework::AssetDatabase::AssetDatabaseConnection::QueryProductDependenciesThatDependOnProductBySourceId(AZ::s64 sourceId, productDependencyHandler handler)

+ 6 - 8
Code/Framework/AzToolsFramework/AzToolsFramework/AssetDatabase/AssetDatabaseConnection.h

@@ -69,6 +69,7 @@ namespace AzToolsFramework
             AddedSourceDependencySubIdsAndProductHashes,
             AddedFlagsColumnToProductTable,
             AddedStatsTable,
+            ChangedSourceDependencySourceColumn,
             //Add all new versions before this
             DatabaseVersionCount,
             LatestVersion = DatabaseVersionCount - 1
@@ -211,7 +212,7 @@ namespace AzToolsFramework
             };
 
             SourceFileDependencyEntry() = default;
-            SourceFileDependencyEntry(AZ::Uuid builderGuid, const char* source, const char* dependsOnSource, TypeOfDependency dependencyType, AZ::u32 fromAssetId, const char* subIds);
+            SourceFileDependencyEntry(AZ::Uuid builderGuid, AZ::Uuid sourceGuid, const char* dependsOnSource, TypeOfDependency dependencyType, AZ::u32 fromAssetId, const char* subIds);
 
             AZStd::string ToString() const;
             auto GetColumns();
@@ -219,7 +220,7 @@ namespace AzToolsFramework
             AZ::s64 m_sourceDependencyID = InvalidEntryId;
             AZ::Uuid m_builderGuid = AZ::Uuid::CreateNull();
             TypeOfDependency m_typeOfDependency = DEP_SourceToSource;
-            AZStd::string m_source;
+            AZ::Uuid m_sourceGuid = AZ::Uuid::CreateNull();
             AZStd::string m_dependsOnSource;
             AZ::u32 m_fromAssetId = false; // Indicates if the dependency was converted from an AssetId into a path before being stored in the DB
             AZStd::string m_subIds;
@@ -613,19 +614,16 @@ namespace AzToolsFramework
             //! Query sources which depend on 'dependsOnSource'.
             //! Reverse dependencies are incoming dependencies: what assets depend on me?
             //! Optional nullable 'dependentFilter' filters it to only resulting sources which are LIKE the filter.
-            bool QuerySourceDependencyByDependsOnSource(const char* dependsOnSource, const char* dependentFilter, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency dependencyType, sourceFileDependencyHandler handler);
+            bool QuerySourceDependencyByDependsOnSource(const char* dependsOnSource, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency dependencyType, sourceFileDependencyHandler handler);
 
             //! Attempt to match either DEP_SourcetoSource or DEP_JobToJob
             //! Then allow DEP_SourceLikeMatch with Wildcard characters
             //! Optional nullable 'dependentFilter' filters it to only resulting sources which are LIKE the filter.
-            bool QuerySourceDependencyByDependsOnSourceWildcard(const char* dependsOnSource, const char* dependentFilter, sourceFileDependencyHandler handler);
+            bool QuerySourceDependencyByDependsOnSourceWildcard(const char* dependsOnSource, sourceFileDependencyHandler handler);
 
             //! Query everything 'sourceDependency' depends on.
             //! Optional nullable 'dependentFilter' filters it to only resulting dependencies which are LIKE the filter.
-            bool QueryDependsOnSourceBySourceDependency(const char* sourceDependency, const char* dependencyFilter, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency dependencyType, sourceFileDependencyHandler handler);
-
-            // Recursive reverse dependency query (returns all the source dependencies which depend on 'dependsOnSource' and all the entries that depend on them, etc)
-            bool QueryAllSourceDependencyByDependsOnSource(const char* dependsOnSource, SourceFileDependencyEntry::TypeOfDependency dependencyType, sourceFileDependencyHandler handler);
+            bool QueryDependsOnSourceBySourceDependency(AZ::Uuid sourceGuid, const char* dependencyFilter, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency dependencyType, sourceFileDependencyHandler handler);
 
             // Returns all sources who's products depend on any of the products of the specified source
             bool QueryProductDependenciesThatDependOnProductBySourceId(AZ::s64 sourceId, productDependencyHandler handler);

+ 9 - 4
Code/Framework/AzToolsFramework/AzToolsFramework/AssetEditor/AssetEditorWidget.cpp

@@ -340,6 +340,9 @@ namespace AzToolsFramework
             auto serializeContext = AZ::EntityUtils::GetApplicationSerializeContext();
             serializeContext->CloneObjectInplace((*m_inMemoryAsset.GetData()), asset.GetData());
 
+            // Make sure the saved state key is reset since this could be a file opened with the same property editor isntance
+            m_savedStateKey = AZ::Crc32(&asset.GetId(), sizeof(AZ::Data::AssetId));
+
             UpdatePropertyEditor(m_inMemoryAsset);
 
             SetupHeader();
@@ -364,19 +367,18 @@ namespace AzToolsFramework
 
         void AssetEditorWidget::UpdatePropertyEditor(AZ::Data::Asset<AZ::Data::AssetData>& asset)
         {
-            AZ::Crc32 saveStateKey;
-            saveStateKey.Add(&asset.GetId(), sizeof(AZ::Data::AssetId));
-
             if (m_useDPE)
             {
                 m_adapter->SetValue(asset.Get(), asset.GetType());
                 m_dpe->SetAdapter(m_adapter);
                 m_dpe->setEnabled(true);
+
+                m_dpe->SetSavedStateKey(m_savedStateKey, "AssetEditor");
             }
             else
             {
                 m_propertyEditor->ClearInstances();
-                m_propertyEditor->SetSavedStateKey(saveStateKey);
+                m_propertyEditor->SetSavedStateKey(m_savedStateKey);
                 m_propertyEditor->AddInstance(asset.Get(), asset.GetType(), nullptr);
 
                 m_propertyEditor->InvalidateAll();
@@ -953,6 +955,9 @@ namespace AzToolsFramework
             }
             m_currentAsset = "New Asset";
 
+            // Make sure the saved state key is reset since this could be a file opened with the same property editor isntance
+            m_savedStateKey = AZ::Crc32(&newAssetId, sizeof(AZ::Data::AssetId));
+
             UpdatePropertyEditor(m_inMemoryAsset);
 
             ExpandAll();

+ 1 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/AssetEditor/AssetEditorWidget.h

@@ -179,6 +179,7 @@ namespace AzToolsFramework
             AZStd::unique_ptr< Ui::AssetEditorStatusBar > m_statusBar;
 
             AZ::DocumentPropertyEditor::ReflectionAdapter::PropertyChangeEvent::Handler m_propertyChangeHandler;
+            AZ::Crc32 m_savedStateKey;
 
             void PopulateGenericAssetTypes();
             void CreateAssetImpl(AZ::Data::AssetType assetType);

+ 30 - 4
Code/Framework/AzToolsFramework/AzToolsFramework/Slice/SliceDependencyBrowserComponent.cpp

@@ -208,9 +208,22 @@ namespace AzToolsFramework
     bool SliceDependencyBrowserComponent::GetSliceDependenciesByRelativeAssetPath(const AZStd::string& relativePath, AZStd::vector<AZStd::string>& dependencies) const
     {
         bool found = false;
+
+        AZ::Uuid sourceUuid;
+        m_databaseConnection->QuerySourceBySourceName(relativePath.c_str(), [&sourceUuid](AzToolsFramework::AssetDatabase::SourceDatabaseEntry& entry)
+        {
+            sourceUuid = entry.m_sourceGuid;
+            return false;
+        });
+
+        if (sourceUuid.IsNull())
+        {
+            return false;
+        }
+
         bool succeeded = m_databaseConnection->QueryDependsOnSourceBySourceDependency(
-            relativePath.c_str(),"%.slice", 
-            AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceOrJob, 
+            sourceUuid, "%.slice",
+            AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceOrJob,
             [&](AssetDatabase::SourceFileDependencyEntry& entry)
         {
             found = true;
@@ -224,12 +237,25 @@ namespace AzToolsFramework
     {
         bool found = false;
         bool succeeded = m_databaseConnection->QuerySourceDependencyByDependsOnSource(
-            relativePath.c_str(), "%.slice",
+            relativePath.c_str(),
             AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceOrJob,
             [&](AssetDatabase::SourceFileDependencyEntry& entry)
         {
+            AzToolsFramework::AssetDatabase::SourceDatabaseEntry sourceEntry;
+            m_databaseConnection->QuerySourceBySourceGuid(entry.m_sourceGuid, [&sourceEntry](AzToolsFramework::AssetDatabase::SourceDatabaseEntry& entry)
+            {
+                sourceEntry = entry;
+                return false;
+            });
+
+            if(!sourceEntry.m_sourceName.ends_with(".slice"))
+            {
+                // Filter out non-slice files
+                return true;
+            }
+
             found = true;
-            dependents.push_back(AZStd::move(entry.m_source));
+            dependents.push_back(AZStd::move(sourceEntry.m_sourceName));
             return true;
         });
         return found && succeeded;

+ 320 - 283
Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/DocumentPropertyEditor.cpp

@@ -76,6 +76,7 @@ namespace AzToolsFramework
                     m_expanderWidget->setCheckState(newCheckState);
                 }
             }
+
             emit expanderChanged(expanded);
         }
     }
@@ -373,7 +374,7 @@ namespace AzToolsFramework
     }
 
     DPERowWidget::DPERowWidget(int depth, DPERowWidget* parentRow)
-        : QWidget(nullptr) // parent will be set when the row is added to its layout
+        : QFrame(nullptr) // parent will be set when the row is added to its layout
         , m_parentRow(parentRow)
         , m_depth(depth)
         , m_columnLayout(new DPELayout(depth, this))
@@ -390,11 +391,11 @@ namespace AzToolsFramework
 
     void DPERowWidget::Clear()
     {
-        if (!m_widgetToPropertyHandler.empty())
+        if (!m_widgetToPropertyHandlerInfo.empty())
         {
             DocumentPropertyEditor* dpe = GetDPE();
             // propertyHandlers own their widgets, so don't destroy them here. Set them free!
-            for (auto propertyWidgetIter = m_widgetToPropertyHandler.begin(), endIter = m_widgetToPropertyHandler.end();
+            for (auto propertyWidgetIter = m_widgetToPropertyHandlerInfo.begin(), endIter = m_widgetToPropertyHandlerInfo.end();
                  propertyWidgetIter != endIter;
                  ++propertyWidgetIter)
             {
@@ -404,9 +405,9 @@ namespace AzToolsFramework
                 m_columnLayout->removeWidget(propertyWidget);
 
                 propertyWidgetIter->first->setParent(nullptr);
-                dpe->ReleaseHandler(AZStd::move(propertyWidgetIter->second));
+                dpe->ReleaseHandler(AZStd::move(propertyWidgetIter->second.hanlderInterface));
             }
-            m_widgetToPropertyHandler.clear();
+            m_widgetToPropertyHandlerInfo.clear();
         }
 
         // delete all remaining child widgets, this will also remove them from their layout
@@ -420,7 +421,7 @@ namespace AzToolsFramework
         m_domOrderedChildren.clear();
     }
 
-    void DPERowWidget::AddChildFromDomValue(const AZ::Dom::Value& childValue, int domIndex)
+    void DPERowWidget::AddChildFromDomValue(const AZ::Dom::Value& childValue, size_t domIndex)
     {
         // create a child widget from the given DOM value and add it to the correct layout
         auto childType = childValue.GetNodeName();
@@ -436,7 +437,7 @@ namespace AzToolsFramework
                 DPERowWidget* priorWidgetInLayout = nullptr;
 
                 // search for an existing row sibling with a lower dom index
-                for (int priorWidgetIndex = domIndex - 1; priorWidgetInLayout == nullptr && priorWidgetIndex >= 0; --priorWidgetIndex)
+                for (int priorWidgetIndex = static_cast<int>(domIndex) - 1; priorWidgetInLayout == nullptr && priorWidgetIndex >= 0; --priorWidgetIndex)
                 {
                     priorWidgetInLayout = qobject_cast<DPERowWidget*>(m_domOrderedChildren[priorWidgetIndex]);
                 }
@@ -464,7 +465,7 @@ namespace AzToolsFramework
                 AddDomChildWidget(domIndex, nullptr);
             }
         }
-        else
+        else // not a row, so it's a column widget
         {
             QWidget* addedWidget = nullptr;
             if (childType == AZ::Dpe::GetNodeName<AZ::Dpe::Nodes::Label>())
@@ -475,34 +476,8 @@ namespace AzToolsFramework
             }
             else if (childType == AZ::Dpe::GetNodeName<AZ::Dpe::Nodes::PropertyEditor>())
             {
-                auto dpeSystem = AZ::Interface<AzToolsFramework::PropertyEditorToolsSystemInterface>::Get();
-                auto handlerId = dpeSystem->GetPropertyHandlerForNode(childValue);
-                auto descriptionString = AZ::Dpe::Nodes::PropertyEditor::Description.ExtractFromDomNode(childValue).value_or("");
-                auto shouldDisable = AZ::Dpe::Nodes::PropertyEditor::Disabled.ExtractFromDomNode(childValue).value_or(false);
-
-                // if this row doesn't already have a tooltip, use the first valid
-                // tooltip from a child PropertyEditor (like the RPE)
-                if (!descriptionString.empty() && toolTip().isEmpty())
-                {
-                    setToolTip(QString::fromUtf8(descriptionString.data(), aznumeric_cast<int>(descriptionString.size())));
-                }
-
-                // if we found a valid handler, grab its widget to add to the column layout
-                if (handlerId)
-                {
-                    // store, then reference the unique_ptr that will manage the handler's lifetime
-                    auto handler = dpeSystem->CreateHandlerInstance(handlerId);
-                    handler->SetValueFromDom(childValue);
-                    addedWidget = handler->GetWidget();
-                    addedWidget->setEnabled(!shouldDisable);
-
-                    // only set the widget's tooltip if it doesn't already have its own
-                    if (!descriptionString.empty() && addedWidget->toolTip().isEmpty())
-                    {
-                        addedWidget->setToolTip(QString::fromUtf8(descriptionString.data(), aznumeric_cast<int>(descriptionString.size())));
-                    }
-                    m_widgetToPropertyHandler[addedWidget] = AZStd::move(handler);
-                }
+                auto handlerId = AZ::Interface<PropertyEditorToolsSystemInterface>::Get()->GetPropertyHandlerForNode(childValue);
+                addedWidget = CreateWidgetForHandler(handlerId, childValue);
             }
             else
             {
@@ -512,58 +487,7 @@ namespace AzToolsFramework
 
             if (addedWidget)
             {
-                addedWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
-
-                // search for an existing column sibling with a lower dom index
-                int priorColumnIndex = -1;
-                for (int searchIndex = domIndex - 1; (priorColumnIndex == -1 && searchIndex >= 0); --searchIndex)
-                {
-                    priorColumnIndex = m_columnLayout->indexOf(m_domOrderedChildren[searchIndex]);
-                }
-
-                // if the alignment attribute is present, add the widget with its appropriate alignment to the column layout
-                auto alignment = AZ::Dpe::Nodes::PropertyEditor::Alignment.ExtractFromDomNode(childValue);
-                if (alignment.has_value())
-                {
-                    Qt::Alignment widgetAlignment;
-                    switch (alignment.value())
-                    {
-                    case AZ::Dpe::Nodes::PropertyEditor::Align::AlignLeft:
-                        widgetAlignment = Qt::AlignLeft;
-                        break;
-                    case AZ::Dpe::Nodes::PropertyEditor::Align::AlignCenter:
-                        widgetAlignment = Qt::AlignCenter;
-                        break;
-                    case AZ::Dpe::Nodes::PropertyEditor::Align::AlignRight:
-                        widgetAlignment = Qt::AlignRight;
-                        break;
-                    }
-                    m_columnLayout->WidgetAlignment(addedWidget, widgetAlignment);
-                }
-
-                //! If the SharePrior attribute is present, add the previous widget to the column layout.
-                //! Set the SharePrior boolean so we know to create a new shared column layout, or add to an existing one
-                auto sharePrior = AZ::Dpe::Nodes::PropertyEditor::SharePriorColumn.ExtractFromDomNode(childValue);
-                if (sharePrior.has_value() && sharePrior.value())
-                {
-                    m_columnLayout->SharePriorColumn(m_columnLayout->itemAt(priorColumnIndex)->widget());
-                    m_columnLayout->SetSharePrior(true);
-                }
-                else
-                {
-                    m_columnLayout->SetSharePrior(false);
-                }
-
-                // If the UseMinimumWidth attribute is present, add the widget to set of widgets using their minimum width
-                auto minimumWidth = AZ::Dpe::Nodes::PropertyEditor::UseMinimumWidth.ExtractFromDomNode(childValue);
-                if (minimumWidth.has_value() && minimumWidth.value())
-                {
-                    m_columnLayout->AddMinimumWidthWidget(addedWidget);
-                }
-
-                m_columnLayout->insertWidget(priorColumnIndex + 1, addedWidget);
-                // insert after the found index; even if nothing were found and priorIndex is still -1,
-                // still insert one after it, at position 0
+                AddColumnWidget(addedWidget, domIndex, childValue);
             }
             AddDomChildWidget(domIndex, addedWidget);
         }
@@ -573,6 +497,8 @@ namespace AzToolsFramework
     {
         Clear();
 
+        m_domPath = BuildDomPath();
+
         // determine whether this node should be expanded
         auto forceExpandAttribute = AZ::Dpe::Nodes::Row::ForceAutoExpand.ExtractFromDomNode(domArray);
         if (forceExpandAttribute.has_value())
@@ -583,10 +509,15 @@ namespace AzToolsFramework
         else
         {
             // nothing forced, so the user's saved expansion state, if it exists, should be used
-            auto savedState = GetDPE()->GetSavedExpanderStateForRow(this);
-            if (savedState != DocumentPropertyEditor::ExpanderState::NotSet)
+            DocumentPropertyEditor* dpe = GetDPE();
+            if (dpe->IsRecursiveExpansionOngoing())
+            {
+                SetExpanded(true);
+                dpe->SetSavedExpanderStateForRow(m_domPath, true);
+            }
+            else if (dpe->HasSavedExpanderStateForRow(m_domPath))
             {
-                SetExpanded(savedState == DocumentPropertyEditor::ExpanderState::Expanded);
+                SetExpanded(dpe->GetSavedExpanderStateForRow(m_domPath));
             }
             else
             {
@@ -608,7 +539,7 @@ namespace AzToolsFramework
         for (size_t arrayIndex = 0, numIndices = domArray.ArraySize(); arrayIndex < numIndices; ++arrayIndex)
         {
             auto& childValue = domArray[arrayIndex];
-            AddChildFromDomValue(childValue, aznumeric_cast<int>(arrayIndex));
+            AddChildFromDomValue(childValue, arrayIndex);
         }
     }
 
@@ -616,21 +547,35 @@ namespace AzToolsFramework
     {
         const auto& fullPath = domOperation.GetDestinationPath();
         auto pathEntry = fullPath[pathIndex];
-        AZ_Assert(pathEntry.IsIndex() || pathEntry.IsEndOfArray(), "the direct children of a row must be referenced by index");
-        auto childCount = m_domOrderedChildren.size();
 
-        // if we're on the last entry in the path, this row widget is the direct owner
-        if (pathIndex == fullPath.Size() - 1)
+        const bool entryIsIndex = pathEntry.IsIndex() || pathEntry.IsEndOfArray();
+        const bool entryAtEnd = (pathIndex == fullPath.Size() - 1); // this is the last entry in the path
+
+        if (!entryIsIndex && entryAtEnd)
+        {
+            // patch isn't addressing a child index like a child row or widget, it's an attribute,
+            // refresh this row from its corresponding DOM node
+            auto subPath = fullPath;
+            subPath.Pop();
+            const auto valueAtSubPath = GetDPE()->GetAdapter()->GetContents()[subPath];
+            SetValueFromDom(valueAtSubPath);
+        }
+        else if (entryAtEnd)
         {
+            // if we're on the last entry in the path, this row widget is the direct owner
+            const auto childCount = m_domOrderedChildren.size();
             size_t childIndex = 0;
             if (pathEntry.IsIndex())
             {
                 // remove and replace operations must match an existing index. Add operations can be one past the current end.
-                AZ_Assert(
-                    (domOperation.GetType() == AZ::Dom::PatchOperation::Type::Add ? childIndex <= childCount : childIndex < childCount),
-                    "patch index is beyond the array bounds!");
-
-                childIndex = aznumeric_cast<int>(pathEntry.GetIndex());
+                childIndex = pathEntry.GetIndex();
+                const bool indexValid =
+                    (domOperation.GetType() == AZ::Dom::PatchOperation::Type::Add ? childIndex <= childCount : childIndex < childCount);
+                AZ_Assert(indexValid, "patch index is beyond the array bounds!");
+                if (!indexValid)
+                {
+                    return;
+                }
             }
             else if (domOperation.GetType() == AZ::Dom::PatchOperation::Type::Add)
             {
@@ -651,7 +596,7 @@ namespace AzToolsFramework
                 if (rowToRemove)
                 {
                     // we're removing a row, remove any associated saved expander state
-                    GetDPE()->RemoveExpanderStateForRow(rowToRemove);
+                    GetDPE()->RemoveExpanderStateForRow(rowToRemove->GetPath());
                 }
 
                 delete (*childIterator); // deleting the widget also automatically removes it from the layout
@@ -671,11 +616,12 @@ namespace AzToolsFramework
             if (domOperation.GetType() == AZ::Dom::PatchOperation::Type::Replace ||
                 domOperation.GetType() == AZ::Dom::PatchOperation::Type::Add)
             {
-                AddChildFromDomValue(domOperation.GetValue(), aznumeric_cast<int>(childIndex));
+                AddChildFromDomValue(domOperation.GetValue(), childIndex);
             }
         }
         else // not the direct owner of the entry to patch
         {
+            const auto childCount = m_domOrderedChildren.size();
             // find the next widget in the path and delegate the operation to them
             auto childIndex = (pathEntry.IsIndex() ? pathEntry.GetIndex() : childCount - 1);
             AZ_Assert(childIndex <= childCount, "DPE: Patch failed to apply, invalid child index specified");
@@ -715,14 +661,32 @@ namespace AzToolsFramework
                 const auto valueAtSubPath = GetDPE()->GetAdapter()->GetContents()[subPath];
 
                 // check if it's a PropertyHandler; if it is, just set it from the DOM directly
-                auto foundEntry = m_widgetToPropertyHandler.find(childWidget);
-                if (foundEntry != m_widgetToPropertyHandler.end())
+                auto foundEntry = m_widgetToPropertyHandlerInfo.find(childWidget);
+                if (foundEntry != m_widgetToPropertyHandlerInfo.end())
                 {
-                    foundEntry->second->SetValueFromDom(valueAtSubPath);
+                    auto handlerId = AZ::Interface<PropertyEditorToolsSystemInterface>::Get()->GetPropertyHandlerForNode(valueAtSubPath);
+
+                    // check if this patch has morphed the PropertyHandler into a different type
+                    if (handlerId != foundEntry->second.handlerId)
+                    {
+                        // CreateWidgetForHandler will add a new entry to m_widgetToPropertyHandlerInfo, kill the old entry
+                        GetDPE()->ReleaseHandler(AZStd::move(foundEntry->second.hanlderInterface));
+                        m_widgetToPropertyHandlerInfo.erase(foundEntry);
+
+                        // Replace the existing handler widget with one appropriate for the new type
+                        auto replacementWidget = CreateWidgetForHandler(handlerId, valueAtSubPath);
+                        AddColumnWidget(replacementWidget, childIndex, valueAtSubPath);
+                        AddDomChildWidget(childIndex, replacementWidget);
+                    }
+                    else
+                    {
+                        // handler is the same, set the existing handler with the new value
+                        foundEntry->second.hanlderInterface->SetValueFromDom(valueAtSubPath);
+                    }
                 }
                 else
                 {
-                    QLabel* changedLabel = qobject_cast<QLabel*>(childWidget);
+                    auto changedLabel = qobject_cast<AzQtComponents::ElidingLabel*>(childWidget);
                     AZ_Assert(changedLabel, "not a label, unknown widget discovered!");
                     if (changedLabel)
                     {
@@ -747,9 +711,9 @@ namespace AzToolsFramework
         return theDPE;
     }
 
-    void DPERowWidget::AddDomChildWidget(int domIndex, QWidget* childWidget)
+    void DPERowWidget::AddDomChildWidget(size_t domIndex, QWidget* childWidget)
     {
-        if (domIndex >= 0 && m_domOrderedChildren.size() > domIndex)
+        if (m_domOrderedChildren.size() > domIndex)
         {
             delete m_domOrderedChildren[domIndex];
             m_domOrderedChildren[domIndex] = childWidget;
@@ -765,6 +729,95 @@ namespace AzToolsFramework
         }
     }
 
+    void DPERowWidget::AddColumnWidget(QWidget* columnWidget, size_t domIndex, const AZ::Dom::Value& domValue)
+    {
+        columnWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
+
+        // search for an existing column sibling with a lower dom index
+        int priorColumnIndex = -1;
+        for (int searchIndex = static_cast<int>(domIndex) - 1; (priorColumnIndex == -1 && searchIndex >= 0); --searchIndex)
+        {
+            priorColumnIndex = m_columnLayout->indexOf(m_domOrderedChildren[searchIndex]);
+        }
+
+        // if the alignment attribute is present, add the widget with its appropriate alignment to the column layout
+        auto alignment = AZ::Dpe::Nodes::PropertyEditor::Alignment.ExtractFromDomNode(domValue);
+        if (alignment.has_value())
+        {
+            Qt::Alignment widgetAlignment;
+            switch (alignment.value())
+            {
+            case AZ::Dpe::Nodes::PropertyEditor::Align::AlignLeft:
+                widgetAlignment = Qt::AlignLeft;
+                break;
+            case AZ::Dpe::Nodes::PropertyEditor::Align::AlignCenter:
+                widgetAlignment = Qt::AlignCenter;
+                break;
+            case AZ::Dpe::Nodes::PropertyEditor::Align::AlignRight:
+                widgetAlignment = Qt::AlignRight;
+                break;
+            }
+            m_columnLayout->WidgetAlignment(columnWidget, widgetAlignment);
+        }
+
+        //! If the sharePrior attribute is present, add the previous widget to the column layout.
+        //! Set the SharePrior boolean so we know to create a new shared column layout, or add to an existing one
+        auto sharePrior = AZ::Dpe::Nodes::PropertyEditor::SharePriorColumn.ExtractFromDomNode(domValue).value_or(false);
+        if (sharePrior)
+        {
+            m_columnLayout->SharePriorColumn(m_columnLayout->itemAt(priorColumnIndex)->widget());
+            m_columnLayout->SetSharePrior(true);
+        }
+        else
+        {
+            m_columnLayout->SetSharePrior(false);
+        }
+
+        // If the UseMinimumWidth attribute is present, add the widget to set of widgets using their minimum width
+        auto minimumWidth = AZ::Dpe::Nodes::PropertyEditor::UseMinimumWidth.ExtractFromDomNode(domValue);
+        if (minimumWidth.has_value() && minimumWidth.value())
+        {
+            m_columnLayout->AddMinimumWidthWidget(columnWidget);
+        }
+
+        // insert after the found index; even if nothing were found and priorIndex is -1,
+        // insert one after it, at position 0
+        m_columnLayout->insertWidget(priorColumnIndex + 1, columnWidget);
+    }
+
+    QWidget* DPERowWidget::CreateWidgetForHandler(
+        PropertyEditorToolsSystemInterface::PropertyHandlerId handlerId, const AZ::Dom::Value& domValue)
+    {
+        QWidget* createdWidget = nullptr;
+        // if we found a valid handler, grab its widget to add to the column layout
+        if (handlerId)
+        {
+            auto descriptionString = AZ::Dpe::Nodes::PropertyEditor::Description.ExtractFromDomNode(domValue).value_or("");
+            auto shouldDisable = AZ::Dpe::Nodes::PropertyEditor::Disabled.ExtractFromDomNode(domValue).value_or(false);
+
+            // if this row doesn't already have a tooltip, use the first valid
+            // tooltip from a child PropertyEditor (like the RPE)
+            if (!descriptionString.empty() && toolTip().isEmpty())
+            {
+                setToolTip(QString::fromUtf8(descriptionString.data(), aznumeric_cast<int>(descriptionString.size())));
+            }
+
+            // store, then reference the unique_ptr that will manage the handler's lifetime
+            auto handler = AZ::Interface<PropertyEditorToolsSystemInterface>::Get()->CreateHandlerInstance(handlerId);
+            handler->SetValueFromDom(domValue);
+            createdWidget = handler->GetWidget();
+            createdWidget->setEnabled(!shouldDisable);
+
+            // only set the widget's tooltip if it doesn't already have its own
+            if (!descriptionString.empty() && createdWidget->toolTip().isEmpty())
+            {
+                createdWidget->setToolTip(QString::fromUtf8(descriptionString.data(), aznumeric_cast<int>(descriptionString.size())));
+            }
+            m_widgetToPropertyHandlerInfo[createdWidget] = { handlerId, AZStd::move(handler) };
+        }
+        return createdWidget;
+    }
+
     DPERowWidget* DPERowWidget::GetLastDescendantInLayout()
     {
         // search for the last row child, which will be the last in the vertical layout for this level
@@ -789,6 +842,48 @@ namespace AzToolsFramework
         return lastDescendant;
     }
 
+    AZ::Dom::Path DPERowWidget::BuildDomPath()
+    {
+        auto pathToRoot = GetDPE()->GetPathToRoot(this);
+        AZ::Dom::Path rowPath = AZ::Dom::Path();
+
+        for (auto reversePathEntry : pathToRoot | AZStd::views::reverse)
+        {
+            rowPath.Push(reversePathEntry);
+        }
+
+        return rowPath;
+    }
+
+    void DPERowWidget::SaveExpanderStatesForChildRows(bool isExpanded)
+    {
+        AZStd::stack<DPERowWidget*> stack;
+
+        const auto pushAllChildRowsToStack = [&stack](AZStd::deque<QWidget*> children)
+        {
+            for (auto& child : children)
+            {
+                DPERowWidget* row = qobject_cast<DPERowWidget*>(child);
+                if (row)
+                {
+                    stack.push(row);
+                }
+            }
+        };
+
+        pushAllChildRowsToStack(m_domOrderedChildren);
+
+        while (!stack.empty())
+        {
+            DPERowWidget* row = stack.top();
+            stack.pop();
+
+            pushAllChildRowsToStack(row->m_domOrderedChildren);
+
+            GetDPE()->SetSavedExpanderStateForRow(row->GetPath(), isExpanded);
+        }
+    }
+
     void DPERowWidget::SetExpanded(bool expanded, bool recurseToChildRows)
     {
         m_columnLayout->SetExpanded(expanded);
@@ -813,8 +908,17 @@ namespace AzToolsFramework
 
     void DPERowWidget::onExpanderChanged(int expanderState)
     {
-        if (expanderState == Qt::Unchecked)
+        DocumentPropertyEditor* dpe = GetDPE();
+        bool isExpanded = expanderState != Qt::Unchecked;
+
+        if (!isExpanded)
         {
+            if (QGuiApplication::keyboardModifiers().testFlag(Qt::ShiftModifier))
+            {
+                // Store collapsed state for all children before deletion if shift was pressed
+                SaveExpanderStatesForChildRows(false);
+            }
+
             // expander is collapsed; search for row children and delete them,
             // which will zero out their QPointer in the deque, and remove them from the layout
             for (auto& currentChild : m_domOrderedChildren)
@@ -829,7 +933,13 @@ namespace AzToolsFramework
         }
         else
         {
-            auto myValue = GetDPE()->GetDomValueForRow(this);
+            if (QGuiApplication::keyboardModifiers().testFlag(Qt::ShiftModifier))
+            {
+                // Flag DPE as in the middle of a recursive expand operation if shift was pressed
+                dpe->SetRecursiveExpansionOngoing(true);
+            }
+
+            auto myValue = dpe->GetDomValueForRow(this);
             AZ_Assert(myValue.ArraySize() == m_domOrderedChildren.size(), "known child count does not match child count!");
             for (int valueIndex = 0; valueIndex < m_domOrderedChildren.size(); ++valueIndex)
             {
@@ -838,11 +948,26 @@ namespace AzToolsFramework
                     AddChildFromDomValue(myValue[valueIndex], valueIndex);
                 }
             }
+
+            dpe->SetRecursiveExpansionOngoing(false);
         }
-        GetDPE()->SetSavedExpanderStateForRow(
-            this,
-            (expanderState == Qt::Unchecked ? DocumentPropertyEditor::ExpanderState::Collapsed
-                                            : DocumentPropertyEditor::ExpanderState::Expanded));
+
+        dpe->SetSavedExpanderStateForRow(m_domPath, isExpanded);
+    }
+
+    const AZ::Dom::Path DPERowWidget::GetPath() const
+    {
+        return m_domPath;
+    }
+
+    bool DPERowWidget::HasChildRows() const
+    {
+        return !m_domOrderedChildren.empty();
+    }
+
+    int DPERowWidget::GetLevel() const
+    {
+        return m_depth;
     }
 
     DocumentPropertyEditor::DocumentPropertyEditor(QWidget* parentWidget)
@@ -899,137 +1024,112 @@ namespace AzToolsFramework
             });
         m_adapter->ConnectMessageHandler(m_domMessageHandler);
 
+        // Free the settings ptr which in turn saves any in-memory settings to disk
+        m_dpeSettings.reset();
+
         // populate the view from the full adapter contents, just like a reset
         HandleReset();
     }
 
     void DocumentPropertyEditor::Clear()
     {
-        for (auto row : m_domOrderedRows)
-        {
-            delete row;
-        }
-
-        m_domOrderedRows.clear();
-        m_expanderPaths.clear();
+        delete m_rootNode;
     }
 
     void DocumentPropertyEditor::AddAfterWidget(QWidget* precursor, QWidget* widgetToAdd)
     {
-        int foundIndex = m_layout->indexOf(precursor);
-        if (foundIndex >= 0)
+        if (precursor == m_rootNode)
         {
-            m_layout->insertWidget(foundIndex + 1, widgetToAdd);
+            m_layout->insertWidget(0, widgetToAdd);
         }
-    }
-
-    void DocumentPropertyEditor::SetSavedExpanderStateForRow(DPERowWidget* row, DocumentPropertyEditor::ExpanderState expanderState)
-    {
-        // Get the index of each dom child going up the chain. We can then reverse this
-        // and use these indices to mark a path to expanded/collapsed nodes
-        AZStd::vector<size_t> reversePath = GetPathToRoot(row);
-
-        if (!reversePath.empty())
+        else
         {
-            // create new pathNodes when necessary implicitly by indexing into each map,
-            // then set the expander state on the final node
-            auto reverseIter = reversePath.rbegin();
-            auto* currPathNode = &m_expanderPaths[*(reverseIter++)];
+            int foundIndex = m_layout->indexOf(precursor);
+            const bool validInsert = (foundIndex >= 0);
+            AZ_Assert(validInsert, "AddAfterWidget: no existing widget found!");
 
-            while (reverseIter != reversePath.rend())
+            if (validInsert)
             {
-                currPathNode = &currPathNode->nextNode[*(reverseIter++)];
+                m_layout->insertWidget(foundIndex + 1, widgetToAdd);
             }
-            currPathNode->expanderState = expanderState;
         }
     }
 
-    DocumentPropertyEditor::ExpanderState DocumentPropertyEditor::GetSavedExpanderStateForRow(DPERowWidget* row) const
+    void DocumentPropertyEditor::SetSavedStateKey(AZ::u32 key, AZStd::string propertyEditorName)
     {
-        // default to NotSet; if a particular index is not recorded in m_expanderPaths,
-        // it is considered not set
-        ExpanderState retval = ExpanderState::NotSet;
+        // We need to append some alphabetical characters to the key or it will be treated as a very large json array index
+        AZStd::string_view keyStr = AZStd::string::format("uuid%s", AZStd::to_string(key).c_str());
+        m_dpeSettings = AZStd::make_unique<DocumentPropertyEditorSettings>(keyStr, propertyEditorName);
 
-        AZStd::vector<size_t> reversePath = GetPathToRoot(row);
-        auto reverseIter = reversePath.rbegin();
-        if (!reversePath.empty())
+        if (m_dpeSettings && m_dpeSettings->WereSettingsLoaded())
         {
-            auto firstNodeIter = m_expanderPaths.find(*(reverseIter++));
-            if (firstNodeIter != m_expanderPaths.end())
-            {
-                const ExpanderPathNode* currPathNode = &firstNodeIter->second;
-
-                // search the existing path tree to see if there's an expander entry for the given node
-                while (currPathNode && reverseIter != reversePath.rend())
+            m_dpeSettings->SetCleanExpanderStateCallback(
+                [this](DocumentPropertyEditorSettings::ExpanderStateMap& storedStates)
                 {
-                    auto nextPathNodeIter = currPathNode->nextNode.find((*reverseIter++));
-                    if (nextPathNodeIter != currPathNode->nextNode.end())
-                    {
-                        currPathNode = &(nextPathNodeIter->second);
-                    }
-                    else
-                    {
-                        currPathNode = nullptr;
-                    }
-                }
-                if (currPathNode)
-                {
-                    // full path exists in the tree, return its expander state
-                    retval = currPathNode->expanderState;
-                }
-            }
+                    const auto& rootValue = m_adapter->GetContents();
+                    auto numErased = AZStd::erase_if(storedStates,
+                        [&rootValue](const AZStd::pair<AZStd::string, bool>& statePair)
+                        {
+                            return !rootValue.FindChild(AZ::Dom::Path(statePair.first)) ? true : false;
+                        });
+                    return numErased > 0;
+                });
+
+            // We need to rebuild the view using the stored expander states
+            HandleReset();
         }
-        return retval;
     }
 
-    void DocumentPropertyEditor::RemoveExpanderStateForRow(DPERowWidget* row)
+    void DocumentPropertyEditor::SetSavedExpanderStateForRow(const AZ::Dom::Path& rowPath, bool isExpanded)
     {
-        AZStd::vector<size_t> reversePath = GetPathToRoot(row);
-        const auto pathLength = reversePath.size();
-        auto reverseIter = reversePath.rbegin();
+        if (m_dpeSettings)
+        {
+            m_dpeSettings->SetExpanderStateForRow(rowPath, isExpanded);
+        }
+    }
 
-        if (pathLength > 0)
+    bool DocumentPropertyEditor::GetSavedExpanderStateForRow(const AZ::Dom::Path& rowPath) const
+    {
+        if (m_dpeSettings)
         {
-            auto firstNodeIter = m_expanderPaths.find(*(reverseIter++));
-            if (firstNodeIter != m_expanderPaths.end())
-            {
-                if (pathLength == 1)
-                {
-                    m_expanderPaths.erase(firstNodeIter);
-                }
-                else
-                {
-                    ExpanderPathNode* currPathNode = &firstNodeIter->second;
-                    auto nextPathNodeIter = currPathNode->nextNode.find((*reverseIter++));
-                    while (reverseIter != reversePath.rend() && nextPathNodeIter != currPathNode->nextNode.end())
-                    {
-                        currPathNode = &(nextPathNodeIter->second);
-                        nextPathNodeIter = currPathNode->nextNode.find((*reverseIter++));
-                    }
+            return m_dpeSettings->GetExpanderStateForRow(rowPath);
+        }
+        return false;
+    }
 
-                    // if we reached the end of the row path, and have valid expander state at that location,
-                    // prune the entry and all its children from the expander tree
-                    if (reverseIter == reversePath.rend() && nextPathNodeIter != currPathNode->nextNode.end())
-                    {
-                        currPathNode->nextNode.erase(nextPathNodeIter);
-                    }
-                }
-            }
+    bool DocumentPropertyEditor::HasSavedExpanderStateForRow(const AZ::Dom::Path& rowPath) const
+    {
+        if (m_dpeSettings)
+        {
+            return m_dpeSettings->HasSavedExpanderStateForRow(rowPath);
+        }
+        return false;
+    }
+
+    void DocumentPropertyEditor::RemoveExpanderStateForRow(const AZ::Dom::Path& rowPath)
+    {
+        if (m_dpeSettings)
+        {
+            return m_dpeSettings->RemoveExpanderStateForRow(rowPath);
         }
     }
 
     void DocumentPropertyEditor::ExpandAll()
     {
-        for (auto row : m_domOrderedRows)
+        for (auto child : m_rootNode->m_domOrderedChildren)
         {
+            // all direct children of the root are rows
+            auto row = static_cast<DPERowWidget*>(child);
             row->SetExpanded(true, true);
         }
     }
 
     void DocumentPropertyEditor::CollapseAll()
     {
-        for (auto row : m_domOrderedRows)
+        for (auto child : m_rootNode->m_domOrderedChildren)
         {
+            // all direct children of the root are rows
+            auto row = static_cast<DPERowWidget*>(child);
             row->SetExpanded(false, true);
         }
     }
@@ -1075,30 +1175,6 @@ namespace AzToolsFramework
         return m_layout;
     }
 
-    void DocumentPropertyEditor::AddRowFromValue(const AZ::Dom::Value& domValue, int rowIndex)
-    {
-        const bool indexInRange = (rowIndex <= m_domOrderedRows.size());
-        AZ_Assert(indexInRange, "rowIndex cannot be more than one past the existing end!")
-
-        if (indexInRange)
-        {
-            auto newRow = new DPERowWidget(0, nullptr);
-
-            if (rowIndex == 0)
-            {
-                m_domOrderedRows.push_front(newRow);
-                m_layout->insertWidget(0, newRow);
-            }
-            else
-            {
-                auto priorRowPosition = m_domOrderedRows.begin() + (rowIndex - 1);
-                AddAfterWidget((*priorRowPosition)->GetLastDescendantInLayout(), newRow);
-                m_domOrderedRows.insert(priorRowPosition + 1, newRow);
-            }
-            newRow->SetValueFromDom(domValue);
-        }
-    }
-
     AZStd::vector<size_t> DocumentPropertyEditor::GetPathToRoot(DPERowWidget* row) const
     {
         AZStd::vector<size_t> pathToRoot;
@@ -1120,11 +1196,17 @@ namespace AzToolsFramework
             thisRow = parentRow;
             parentRow = parentRow->m_parentRow;
         }
+        return pathToRoot;
+    }
 
-        // we've reached the top of the DPERowWidget chain, now we need to get that first row's index from the m_domOrderedRows
-        pushPathPiece(m_domOrderedRows, thisRow);
+    bool DocumentPropertyEditor::IsRecursiveExpansionOngoing() const
+    {
+        return m_isRecursiveExpansionOngoing;
+    }
 
-        return pathToRoot;
+    void DocumentPropertyEditor::SetRecursiveExpansionOngoing(bool isExpanding)
+    {
+        m_isRecursiveExpansionOngoing = isExpanding;
     }
 
     void DocumentPropertyEditor::HandleReset()
@@ -1132,6 +1214,11 @@ namespace AzToolsFramework
         // clear any pre-existing DPERowWidgets
         Clear();
 
+        // invisible root node has a "depth" of -1; its children are all at indent 0
+        m_rootNode = new DPERowWidget(-1, nullptr);
+        m_rootNode->setParent(this);
+        m_rootNode->hide();
+
         auto topContents = m_adapter->GetContents();
 
         for (size_t arrayIndex = 0, numIndices = topContents.ArraySize(); arrayIndex < numIndices; ++arrayIndex)
@@ -1143,67 +1230,17 @@ namespace AzToolsFramework
 
             if (isRow)
             {
-                AddRowFromValue(rowValue, aznumeric_cast<int>(arrayIndex));
+                m_rootNode->AddChildFromDomValue(topContents[arrayIndex], arrayIndex);
             }
         }
         m_layout->addStretch();
     }
+
     void DocumentPropertyEditor::HandleDomChange(const AZ::Dom::Patch& patch)
     {
         for (auto operationIterator = patch.begin(), endIterator = patch.end(); operationIterator != endIterator; ++operationIterator)
         {
-            const auto& patchPath = operationIterator->GetDestinationPath();
-            if (patchPath.Size() == 0)
-            {
-                // an empty path indicates a change to the top-level of the DOM, which is the adapter.
-                // Currently, this is meaningless to the DPE so just return.
-                return;
-            }
-            auto firstAddressEntry = patchPath[0];
-
-            const bool isIndex = (firstAddressEntry.IsIndex() || firstAddressEntry.IsEndOfArray());
-            AZ_Assert(isIndex, "first entry in a DPE patch must be the index of the first row");
-            auto rowIndex = (firstAddressEntry.IsIndex() ? firstAddressEntry.GetIndex() : m_domOrderedRows.size());
-
-            const bool indexInRange =
-                (rowIndex < m_domOrderedRows.size() ||
-                 (rowIndex <= m_domOrderedRows.size() && operationIterator->GetType() == AZ::Dom::PatchOperation::Type::Add));
-            AZ_Assert(indexInRange, "received a patch for a row that doesn't exist");
-
-            if (!(isIndex && indexInRange))
-            {
-                // invalid input, bail
-                return;
-            }
-
-            // if there is only one level in the path, this operation is for the top layout
-            if (patchPath.Size() == 1)
-            {
-                if (operationIterator->GetType() == AZ::Dom::PatchOperation::Type::Add)
-                {
-                    AddRowFromValue(operationIterator->GetValue(), aznumeric_cast<int>(rowIndex));
-                }
-                else
-                {
-                    auto& rowWidget = m_domOrderedRows[aznumeric_cast<int>(firstAddressEntry.GetIndex())];
-                    if (operationIterator->GetType() == AZ::Dom::PatchOperation::Type::Replace)
-                    {
-                        rowWidget->SetValueFromDom(operationIterator->GetValue());
-                    }
-                    else if (operationIterator->GetType() == AZ::Dom::PatchOperation::Type::Remove)
-                    {
-                        delete rowWidget;
-                        rowWidget = nullptr;
-                    }
-                }
-            }
-            else
-            {
-                // delegate the action to the rowWidget, which will, in turn, delegate to the next row in the path, if available
-                auto rowWidget = m_domOrderedRows[aznumeric_cast<int>(firstAddressEntry.GetIndex())];
-                constexpr size_t pathDepth = 1; // top level has been handled, start the next operation at path depth 1
-                rowWidget->HandleOperationAtPath(*operationIterator, pathDepth);
-            }
+            m_rootNode->HandleOperationAtPath(*operationIterator, 0);
         }
     }
 

+ 42 - 31
Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/DocumentPropertyEditor.h

@@ -9,12 +9,10 @@
 #pragma once
 
 #if !defined(Q_MOC_RUN)
-#include <AzCore/DOM/Backends/JSON/JsonBackend.h>
-#include <AzCore/Interface/Interface.h>
-#include <AzCore/Memory/SystemAllocator.h>
 #include <AzFramework/DocumentPropertyEditor/DocumentAdapter.h>
 #include <AzToolsFramework/UI/DocumentPropertyEditor/IPropertyEditor.h>
 #include <AzToolsFramework/UI/DocumentPropertyEditor/PropertyHandlerWidget.h>
+#include <AzToolsFramework/UI/DocumentPropertyEditor/DocumentPropertyEditorSettings.h>
 
 #include <QHBoxLayout>
 #include <QScrollArea>
@@ -91,9 +89,12 @@ namespace AzToolsFramework
         mutable QSize m_cachedMinLayoutSize;
     };
 
-    class DPERowWidget : public QWidget
+    class DPERowWidget : public QFrame
     {
         Q_OBJECT
+        Q_PROPERTY(bool hasChildRows READ HasChildRows);
+        Q_PROPERTY(int getLevel READ GetLevel);
+
         friend class DocumentPropertyEditor;
 
     public:
@@ -101,7 +102,7 @@ namespace AzToolsFramework
         ~DPERowWidget();
 
         void Clear(); //!< destroy all layout contents and clear DOM children
-        void AddChildFromDomValue(const AZ::Dom::Value& childValue, int domIndex);
+        void AddChildFromDomValue(const AZ::Dom::Value& childValue, size_t domIndex);
 
         //! clears and repopulates all children from a given DOM array
         void SetValueFromDom(const AZ::Dom::Value& domArray);
@@ -115,22 +116,41 @@ namespace AzToolsFramework
         void SetExpanded(bool expanded, bool recurseToChildRows = false);
         bool IsExpanded() const;
 
+        const AZ::Dom::Path GetPath() const;
+
+        bool HasChildRows() const;
+        int GetLevel() const;
+
     protected slots:
         void onExpanderChanged(int expanderState);
 
     protected:
         DocumentPropertyEditor* GetDPE() const;
-        void AddDomChildWidget(int domIndex, QWidget* childWidget);
+        void AddDomChildWidget(size_t domIndex, QWidget* childWidget);
+        void AddColumnWidget(QWidget* columnWidget, size_t domIndex, const AZ::Dom::Value& domValue);
+
+        AZ::Dom::Path BuildDomPath();
+        void SaveExpanderStatesForChildRows(bool isExpanded);
+
+        QWidget* CreateWidgetForHandler(PropertyEditorToolsSystemInterface::PropertyHandlerId handlerId, const AZ::Dom::Value& domValue);
 
         DPERowWidget* m_parentRow = nullptr;
         int m_depth = 0; //!< number of levels deep in the tree. Used for indentation
         DPELayout* m_columnLayout = nullptr;
 
+        // This widget's indexed path from the root
+        AZ::Dom::Path m_domPath;
+
         //! widget children in DOM specified order; mix of row and column widgets
         AZStd::deque<QWidget*> m_domOrderedChildren;
 
         // a map from the propertyHandler widgets to the propertyHandlers that created them
-        AZStd::unordered_map<QWidget*, AZStd::unique_ptr<PropertyHandlerWidgetInterface>> m_widgetToPropertyHandler;
+        struct HandlerInfo
+        {
+            PropertyEditorToolsSystemInterface::PropertyHandlerId handlerId = nullptr;
+            AZStd::unique_ptr<PropertyHandlerWidgetInterface> hanlderInterface;
+        };
+        AZStd::unordered_map<QWidget*, HandlerInfo> m_widgetToPropertyHandlerInfo;
     };
 
     class DocumentPropertyEditor
@@ -151,20 +171,15 @@ namespace AzToolsFramework
         }
         void AddAfterWidget(QWidget* precursor, QWidget* widgetToAdd);
 
-        enum class ExpanderState : uint8_t
-        {
-            NotSet,
-            Collapsed,
-            Expanded
-        };
-
-        void SetSavedExpanderStateForRow(DPERowWidget* row, ExpanderState expanderState);
-        ExpanderState GetSavedExpanderStateForRow(DPERowWidget* row) const;
-        void RemoveExpanderStateForRow(DPERowWidget* row);
+        void SetSavedExpanderStateForRow(const AZ::Dom::Path& rowPath, bool isExpanded);
+        bool GetSavedExpanderStateForRow(const AZ::Dom::Path& rowPath) const;
+        bool HasSavedExpanderStateForRow(const AZ::Dom::Path& rowPath) const;
+        void RemoveExpanderStateForRow(const AZ::Dom::Path& rowPath);
         void ExpandAll();
         void CollapseAll();
 
-        // IPropertyEditor overrides to be added ...
+        // IPropertyEditor overrides
+        void SetSavedStateKey(AZ::u32 key, AZStd::string propertyEditorName = {}) override;
 
         AZ::Dom::Value GetDomValueForRow(DPERowWidget* row) const;
 
@@ -177,6 +192,10 @@ namespace AzToolsFramework
 
         static bool ShouldReplaceRPE();
 
+        AZStd::vector<size_t> GetPathToRoot(DPERowWidget* row) const;
+        bool IsRecursiveExpansionOngoing() const;
+        void SetRecursiveExpansionOngoing(bool isExpanding);
+
     public slots:
         //! set the DOM adapter for this DPE to inspect
         void SetAdapter(AZ::DocumentPropertyEditor::DocumentAdapterPtr theAdapter);
@@ -184,12 +203,11 @@ namespace AzToolsFramework
 
     protected:
         QVBoxLayout* GetVerticalLayout();
-        void AddRowFromValue(const AZ::Dom::Value& domValue, int rowIndex);
-        AZStd::vector<size_t> GetPathToRoot(DPERowWidget* row) const;
 
         void HandleReset();
         void HandleDomChange(const AZ::Dom::Patch& patch);
         void HandleDomMessage(const AZ::DocumentPropertyEditor::AdapterMessage& message, AZ::Dom::Value& value);
+
         void CleanupReleasedHandlers();
 
         AZ::DocumentPropertyEditor::DocumentAdapterPtr m_adapter;
@@ -199,20 +217,13 @@ namespace AzToolsFramework
 
         QVBoxLayout* m_layout = nullptr;
 
+        AZStd::unique_ptr<DocumentPropertyEditorSettings> m_dpeSettings;
+        bool m_isRecursiveExpansionOngoing = false;
+
         bool m_spawnDebugView = false;
 
         QTimer* m_handlerCleanupTimer;
         AZStd::vector<AZStd::unique_ptr<PropertyHandlerWidgetInterface>> m_unusedHandlers;
-        AZStd::deque<DPERowWidget*> m_domOrderedRows;
-
-        //! tree nodes to keep track of expander state explicitly changed by the user
-        struct ExpanderPathNode
-        {
-            ExpanderState expanderState = ExpanderState::NotSet;
-            AZStd::unordered_map<size_t, ExpanderPathNode> nextNode;
-        };
-
-        //! hierarchical dom index to expander state tree
-        AZStd::unordered_map<size_t, ExpanderPathNode> m_expanderPaths;
+        DPERowWidget* m_rootNode = nullptr;
     };
 } // namespace AzToolsFramework

+ 123 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/DocumentPropertyEditorSettings.cpp

@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include "DocumentPropertyEditor.h"
+
+#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
+
+namespace AzToolsFramework
+{
+    DocumentPropertyEditorSettings::DocumentPropertyEditorSettings(
+        const AZStd::string& settingsRegistryKey,
+        const AZStd::string& propertyEditorName)
+    {
+        m_settingsRegistryBasePath = AZStd::string::format("%s/%s", RootSettingsRegistryPath, propertyEditorName.c_str());
+        m_fullSettingsRegistryPath = AZStd::string::format("%s/%s", m_settingsRegistryBasePath.c_str(), settingsRegistryKey.c_str());
+
+        m_settingsFilepath.Append(RootSettingsFilepath)
+            .Append(AZStd::string::format("%s_settings", propertyEditorName.c_str()))
+            .ReplaceExtension(SettingsRegistrar::SettingsRegistryFileExt);
+
+        m_wereSettingsLoaded = LoadExpanderStates();
+    }
+
+    DocumentPropertyEditorSettings::~DocumentPropertyEditorSettings()
+    {
+        SaveAndCleanExpanderStates();
+    }
+
+    void DocumentPropertyEditorSettings::SaveExpanderStates()
+    {
+        m_settingsRegistrar.StoreObjectSettings(m_fullSettingsRegistryPath, this);
+
+        // Configuration that the dumper util will use when collecting our data from the SettingsRegistry
+        AZ::SettingsRegistryMergeUtils::DumperSettings dumperSettings;
+        dumperSettings.m_prettifyOutput = true;
+        dumperSettings.m_includeFilter = [pathFilter = m_settingsRegistryBasePath](AZStd::string_view path)
+        {
+            return AZ::SettingsRegistryMergeUtils::IsPathAncestorDescendantOrEqual(pathFilter, path);
+        };
+        dumperSettings.m_jsonPointerPrefix = m_settingsRegistryBasePath;
+
+        auto outcome = m_settingsRegistrar.SaveSettingsToFile(m_settingsFilepath, dumperSettings, m_settingsRegistryBasePath);
+        if (!outcome.IsSuccess())
+        {
+            AZ_Warning("DocumentPropertyEditorSettings", false, outcome.GetError().c_str());
+        }
+    }
+
+    bool DocumentPropertyEditorSettings::LoadExpanderStates()
+    {
+        auto loadOutcome = m_settingsRegistrar.LoadSettingsFromFile(m_settingsFilepath);
+        if (loadOutcome.IsSuccess())
+        {
+            auto getOutcome = m_settingsRegistrar.GetObjectSettings(this, m_fullSettingsRegistryPath);
+            if (!getOutcome.IsSuccess())
+            {
+                AZ_Warning("DocumentPropertyEditorSettings", false, getOutcome.GetError().c_str());
+                return false;
+            }
+        }
+        else
+        {
+            AZ_Warning("DocumentPropertyEditorSettings", false, loadOutcome.GetError().c_str());
+            return false;
+        }
+
+        return true;
+    }
+
+    void DocumentPropertyEditorSettings::SaveAndCleanExpanderStates()
+    {
+        // Attempt to remove old settings from registry if the local state was successfully cleaned
+        // This way we save and dump to file the most accurate expander settings
+        if (m_cleanExpanderStateCallback && m_cleanExpanderStateCallback(m_expandedElementStates))
+        {
+            if (!m_settingsRegistrar.RemoveSettingFromRegistry(m_fullSettingsRegistryPath))
+            {
+                AZ_Warning("DocumentPropertyEditorSettings", false,
+                    "Failed to clean registry state %s before saving", m_fullSettingsRegistryPath.c_str());
+            }
+        }
+
+        if (!m_expandedElementStates.empty())
+        {
+            SaveExpanderStates();
+        }
+    }
+
+    void DocumentPropertyEditorSettings::SetExpanderStateForRow(const AZ::Dom::Path& rowPath, bool isExpanded)
+    {      
+        m_expandedElementStates[rowPath.ToString()] = isExpanded;
+    }
+
+    bool DocumentPropertyEditorSettings::GetExpanderStateForRow(const AZ::Dom::Path& rowPath)
+    {
+        AZStd::string strPath = rowPath.ToString();
+        if (m_expandedElementStates.contains(strPath))
+        {
+            return m_expandedElementStates[strPath];
+        }
+        return false;
+    }
+
+    bool DocumentPropertyEditorSettings::HasSavedExpanderStateForRow(const AZ::Dom::Path& rowPath) const
+    {
+        return m_expandedElementStates.contains(rowPath.ToString());
+    }
+
+    void DocumentPropertyEditorSettings::RemoveExpanderStateForRow(const AZ::Dom::Path& rowPath)
+    {
+        m_expandedElementStates.erase(rowPath.ToString());
+    }
+
+    void DocumentPropertyEditorSettings::SetCleanExpanderStateCallback(CleanExpanderStateCallback function)
+    {
+        m_cleanExpanderStateCallback = function;
+    }
+} // namespace AzToolsFramework

+ 77 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/DocumentPropertyEditorSettings.h

@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/Serialization/SerializeContext.h>
+#include <AzToolsFramework/UI/DocumentPropertyEditor/SettingsRegistrar.h>
+
+namespace AzToolsFramework
+{
+    //! This serializable class stores and loads the DocumentPropertyEditor settings such as tree node expansion state.
+    class DocumentPropertyEditorSettings
+    {
+    public:
+        AZ_RTTI(DocumentPropertyEditorSettings, "{7DECB0A1-A1AB-41B2-B31F-E52D3C3014A6}");
+
+        using ExpanderStateMap = AZStd::unordered_map<AZStd::string, bool>;
+        using CleanExpanderStateCallback = AZStd::function<bool(ExpanderStateMap&)>;
+
+        // Default ctor is required by SerializeContext but is not intended for use otherwise
+        DocumentPropertyEditorSettings() = default;
+        DocumentPropertyEditorSettings(const AZStd::string& settingsRegistryKey, const AZStd::string& propertyEditorName);
+
+        virtual ~DocumentPropertyEditorSettings();
+
+        static void Reflect(AZ::ReflectContext* context)
+        {
+            AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
+            if (serializeContext)
+            {
+                serializeContext->Class<DocumentPropertyEditorSettings>()->Version(0)->Field(
+                    "ExpandedElements", &DocumentPropertyEditorSettings::m_expandedElementStates);
+            }
+        }
+
+        void SetExpanderStateForRow(const AZ::Dom::Path& rowPath, bool isExpanded);
+        bool GetExpanderStateForRow(const AZ::Dom::Path& rowPath);
+        bool HasSavedExpanderStateForRow(const AZ::Dom::Path& rowPath) const;
+        void RemoveExpanderStateForRow(const AZ::Dom::Path& rowPath);
+
+        bool WereSettingsLoaded() const { return m_wereSettingsLoaded; };
+
+        void SetCleanExpanderStateCallback(CleanExpanderStateCallback function);
+
+        //! Root filepath for DocumentPropertyEditor settings files
+        static constexpr const char* RootSettingsFilepath = "user/Registry/DocumentPropertyEditor";
+
+        //! Root SettingsRegistry path where DPE settings are stored
+        static constexpr const char* RootSettingsRegistryPath = "/O3DE/DocumentPropertyEditor";
+
+        //! Serialized map of expanded element states
+        ExpanderStateMap m_expandedElementStates;
+
+    private:
+        void SaveExpanderStates();
+        bool LoadExpanderStates();
+
+        void SaveAndCleanExpanderStates();
+
+        //! Optional callback to clean locally stored state before saving
+        CleanExpanderStateCallback m_cleanExpanderStateCallback;
+
+        bool m_wereSettingsLoaded = false;
+
+        SettingsRegistrar m_settingsRegistrar;
+
+        AZ::IO::Path m_settingsFilepath;
+
+        AZStd::string m_fullSettingsRegistryPath;
+        AZStd::string m_settingsRegistryBasePath;
+    };
+} // namespace AzToolsFramework

+ 518 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/FilterAdapter.cpp

@@ -0,0 +1,518 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+#include <AzFramework/DocumentPropertyEditor/AdapterBuilder.h>
+#include <AzToolsFramework/UI/DocumentPropertyEditor/FilterAdapter.h>
+
+namespace AZ::DocumentPropertyEditor
+{
+    RowFilterAdapter::RowFilterAdapter() 
+    {
+    }
+
+    RowFilterAdapter::~RowFilterAdapter()
+    {
+        delete m_root;
+    }
+
+    void RowFilterAdapter::SetSourceAdapter(DocumentAdapterPtr sourceAdapter)
+    {
+        m_sourceAdapter = sourceAdapter;
+        m_resetHandler = DocumentAdapter::ResetEvent::Handler(
+            [this]()
+            {
+                this->HandleReset();
+            });
+        m_sourceAdapter->ConnectResetHandler(m_resetHandler);
+
+        m_changedHandler = DocumentAdapter::ChangedEvent::Handler(
+            [this](const Dom::Patch& patch)
+            {
+                this->HandleDomChange(patch);
+            });
+        m_sourceAdapter->ConnectChangedHandler(m_changedHandler);
+
+        m_domMessageHandler = AZ::DocumentPropertyEditor::DocumentAdapter::MessageEvent::Handler(
+            [this](const AZ::DocumentPropertyEditor::AdapterMessage& message, AZ::Dom::Value& value)
+            {
+                this->HandleDomMessage(message, value);
+            });
+        m_sourceAdapter->ConnectMessageHandler(m_domMessageHandler);
+
+        // populate the filter data from the source's full adapter contents, just like a reset
+        HandleReset();
+    }
+
+    bool RowFilterAdapter::IsRow(const Dom::Value& domValue)
+    {
+        return (domValue.IsNode() && domValue.GetNodeName() == Dpe::GetNodeName<Dpe::Nodes::Row>());
+
+    }
+
+    bool RowFilterAdapter::IsRow(const Dom::Path& sourcePath) const
+    {
+        const auto& sourceRoot = m_sourceAdapter->GetContents();
+        auto sourceNode = sourceRoot[sourcePath];
+        return IsRow(sourceNode);
+    }
+
+    void RowFilterAdapter::SetFilterActive(bool activateFilter)
+    {
+        if (m_filterActive != activateFilter)
+        {
+            m_filterActive = activateFilter;
+            NotifyResetDocument();
+        }
+    }
+
+    void RowFilterAdapter::InvalidateFilter()
+    {
+        AZStd::function<void(MatchInfoNode*)> updateChildren = [&](MatchInfoNode* parentNode)
+        {
+            for (auto* childNode : parentNode->m_childMatchState)
+            {
+                if (childNode)
+                {
+                    UpdateMatchState(childNode);
+                    updateChildren(childNode);
+                }
+            }
+        };
+
+        // recursively update each node, then its children
+        updateChildren(m_root);
+
+        if (m_filterActive)
+        {
+            NotifyResetDocument();
+        }
+    }
+
+    Dom::Value RowFilterAdapter::GenerateContents()
+    {
+        auto filteredContents = m_sourceAdapter->GetContents();
+        if (m_filterActive)
+        {
+            // cull row children based on their MatchInfoNode
+            AZStd::function<void(Dom::Value& rowValue, const MatchInfoNode* rowMatchNode)> cullUnmatchedChildRows =
+            [&](Dom::Value& rowValue, const MatchInfoNode* rowMatchNode)
+            {
+                auto& matchChildren = rowMatchNode->m_childMatchState;
+                const bool sizesMatch = (rowValue.ArraySize() == matchChildren.size());
+                AZ_Assert(sizesMatch, "DOM value and cached match node should have the same number of children!");
+
+                if (sizesMatch)
+                {
+                    auto valueIter = rowValue.MutableArrayBegin();
+                    for (auto matchIter = matchChildren.begin(); matchIter != matchChildren.end(); ++matchIter)
+                    {
+                        // non-row children will have a null MatchInfo -- we only need to worry about culling row values
+                        auto currMatch = *matchIter;
+                        if (currMatch != nullptr)
+                        {
+                            if (currMatch->m_matchesSelf)
+                            {
+                                // node matches directly. All descendents automatically match if m_includeAllMatchDescendents is set
+                                if (!m_includeAllMatchDescendents)
+                                {
+                                    cullUnmatchedChildRows(*valueIter, *matchIter);
+                                }
+                            }
+                            else if (!currMatch->m_matchingDescendents.empty())
+                            {
+                                // all nodes with matching descendents must be included so that there is a path to the matching node
+                                cullUnmatchedChildRows(*valueIter, *matchIter);
+                            }
+                            else
+                            {
+                                // neither the node nor its children match, cull the value from the returned tree
+                                valueIter = rowValue.ArrayErase(valueIter);
+                                continue; // we've already moved valueIter forward, skip the ++valueIter below
+                            }
+                        }
+                        ++valueIter;
+                    }
+                }
+            };
+            // filter downwards from root
+            cullUnmatchedChildRows(filteredContents, m_root);
+        }
+        return filteredContents;
+    }
+
+    void RowFilterAdapter::HandleReset()
+    {
+        delete m_root;
+        m_root = NewMatchInfoNode(nullptr);
+        const auto& sourceContents = m_sourceAdapter->GetContents();
+
+        // we can assume all direct children of an adapter must be rows; populate each of them
+        for (size_t topIndex = 0; topIndex < sourceContents.ArraySize(); ++topIndex)
+        {
+            PopulateNodesAtPath(Dom::Path({Dom::PathEntry(topIndex)}), false);
+        }
+        NotifyResetDocument();
+    }
+
+    void RowFilterAdapter::HandleDomChange(const Dom::Patch& patch)
+    {
+        const auto& sourceContents = m_sourceAdapter->GetContents();
+        for (auto operationIterator = patch.begin(), endIterator = patch.end(); operationIterator != endIterator; ++operationIterator)
+        {
+            const auto& patchPath = operationIterator->GetDestinationPath();
+            auto rowPath = GetRowPath(patchPath);
+
+            if (operationIterator->GetType() == AZ::Dom::PatchOperation::Type::Remove)
+            {
+                auto matchingRow = GetMatchNodeAtPath(rowPath);
+                if (rowPath == patchPath)
+                {
+                    // node being removed is a row. We need to remove it from our graph
+                    auto& parentContainer = matchingRow->m_parentNode->m_childMatchState;
+                    parentContainer.erase(parentContainer.begin() + (patchPath.end() - 1)->GetIndex());
+
+                    // update former parent's match state, as its m_matchingDescendents may have changed
+                    UpdateMatchState(matchingRow->m_parentNode);
+                }
+                else
+                {
+                    // removed node wasn't a row, so we need to re-cache the owning row's cached info
+                    CacheDomInfoForNode(sourceContents[rowPath], matchingRow);
+                    UpdateMatchState(matchingRow);
+                }
+            }
+            else if (operationIterator->GetType() == AZ::Dom::PatchOperation::Type::Replace)
+            {
+                // replace operations can change child values from or into rows. Handle all cases
+                auto existingMatchInfo = GetMatchNodeAtPath(patchPath);
+                if (patchPath == rowPath)
+                {
+                    // replacement value is a row, populate it
+                    PopulateNodesAtPath(patchPath, true);
+                    if (!existingMatchInfo)
+                    {
+                        // this value wasn't a row before, so its parent needs to recache its info
+                        auto parentNode = GetMatchNodeAtPath(patchPath)->m_parentNode;
+                        auto parentPath = patchPath;
+                        parentPath.Pop();
+                        CacheDomInfoForNode(sourceContents[parentPath], parentNode);
+                        UpdateMatchState(parentNode);
+                    }
+                }
+                else
+                {
+                    // not a row now. Check if it was before
+                    if (existingMatchInfo)
+                    {
+                        // node being replaced was a row. We need to remove it from our graph
+                        auto& parentContainer = existingMatchInfo->m_parentNode->m_childMatchState;
+                        parentContainer.erase(parentContainer.begin() + (patchPath.end() - 1)->GetIndex());
+
+                        // update former parent's match state, as its m_matchingDescendents may have changed
+                        UpdateMatchState(existingMatchInfo->m_parentNode);
+                    }
+                    else
+                    {
+                        // replaced node wasn't a row, so we need to re-cache the owning row's cached info
+                        auto matchingRow = GetMatchNodeAtPath(rowPath);
+                        CacheDomInfoForNode(sourceContents[rowPath], matchingRow);
+                        UpdateMatchState(matchingRow);
+                    }
+                }
+
+            }
+            else if (operationIterator->GetType() == AZ::Dom::PatchOperation::Type::Add)
+            {
+                if (rowPath == patchPath)
+                {
+                    // given path is a new row, populate our tree with match information for this node and any row descendents
+                    PopulateNodesAtPath(rowPath, false);
+                }
+                else
+                {
+                    // added node wasn't a row, so we need to re-cache the owning row's cached info
+                    auto matchingRow = GetMatchNodeAtPath(rowPath);
+                    CacheDomInfoForNode(sourceContents[rowPath], matchingRow);
+                    UpdateMatchState(matchingRow);
+                }
+            }
+            else
+            {
+                AZ_Error("FilterAdapter", false, "patch operation not supported yet");
+            }
+        }
+        NotifyResetDocument();
+    }
+
+    void RowFilterAdapter::HandleDomMessage(const AZ::DocumentPropertyEditor::AdapterMessage& message,[[maybe_unused]] Dom::Value& value)
+    {
+        // forward all messages unaltered
+        DocumentAdapter::SendAdapterMessage(message);
+    }
+
+    RowFilterAdapter::MatchInfoNode* RowFilterAdapter::GetMatchNodeAtPath(const Dom::Path& sourcePath)
+    {
+        RowFilterAdapter::MatchInfoNode* currMatchState = m_root;
+        if (sourcePath.Size() < 1)
+        {
+            // path is empty, return the root node
+            return currMatchState;
+        }
+
+        for (const auto& pathEntry : sourcePath)
+        {
+            if (!pathEntry.IsIndex() || !currMatchState)
+            {
+                return nullptr;
+            }
+            const auto index = pathEntry.GetIndex();
+            if (index >= currMatchState->m_childMatchState.size())
+            {
+                return nullptr;
+            }
+            currMatchState = currMatchState->m_childMatchState[index];
+        }
+
+        return currMatchState;
+    }
+
+    void RowFilterAdapter::PopulateNodesAtPath(const Dom::Path& sourcePath, bool replaceExisting)
+    {
+        Dom::Path startingPath = GetRowPath(sourcePath);
+
+        if (startingPath != sourcePath)
+        {
+            // path to add isn't a row, it's a column or an attribute. We're done!
+            return;
+        }
+        else
+        {
+            // path is a node, populate its children (if any), dump its node info to the match string
+            AZStd::function<void(MatchInfoNode*, const Dom::Value&)> recursivelyRegenerateMatches =
+            [&](MatchInfoNode* matchState, const Dom::Value& value)
+            {
+                // precondition: value is a node of type row, matchState is blank, other than its m_parentNode
+                CacheDomInfoForNode(value, matchState);
+
+                const auto childCount = value.ArraySize();
+                matchState->m_childMatchState.resize(childCount, nullptr);
+                for (size_t arrayIndex = 0; arrayIndex < childCount; ++arrayIndex)
+                {
+                    auto& childValue = value[arrayIndex];
+                    if (IsRow(childValue))
+                    {
+                        matchState->m_childMatchState[arrayIndex] = NewMatchInfoNode(matchState);
+                        auto& addedChild = matchState->m_childMatchState[arrayIndex];
+                        recursivelyRegenerateMatches(addedChild, childValue);
+                    }
+                }
+                // update ours and our ancestors' matching states
+                UpdateMatchState(matchState);
+            };
+
+            // we should start with the parentPath, so peel off the new index from the end of the address
+            size_t newRowIndex = startingPath[startingPath.Size() - 1].GetIndex();
+            startingPath.Pop();
+            auto parentMatchState = GetMatchNodeAtPath(startingPath);
+            
+            if (replaceExisting)
+            {
+                // destroy existing entry, if present
+                const bool hasEntryToReplace = (parentMatchState->m_childMatchState.size() > newRowIndex);
+                AZ_Assert(hasEntryToReplace, "PopulateNodesAtPath was called with replaceExisting, but no existing entry exists!");
+                if (hasEntryToReplace)
+                {
+                    delete parentMatchState->m_childMatchState[newRowIndex];
+                    parentMatchState->m_childMatchState[newRowIndex] = NewMatchInfoNode(parentMatchState);
+                }
+                else
+                {
+                    // this shouldn't happen!
+                    return;
+                }
+            }
+            else
+            {
+                // inserting or appending child entry, add it to the correct position
+                parentMatchState->m_childMatchState.insert(
+                    parentMatchState->m_childMatchState.begin() + newRowIndex,
+                    NewMatchInfoNode(parentMatchState));
+            }
+            auto addedChild = parentMatchState->m_childMatchState[newRowIndex];
+
+            // recursively add descendents and cache their match status
+            const auto& sourceContents = m_sourceAdapter->GetContents();
+            recursivelyRegenerateMatches(addedChild, sourceContents[sourcePath]);
+        }
+    }
+
+    void RowFilterAdapter::UpdateMatchState(MatchInfoNode* rowState)
+    {
+        auto includeInFilter = [](MatchInfoNode* node) -> bool
+        {
+            return (node->m_matchesSelf || !node->m_matchingDescendents.empty());
+        };
+        bool usedToMatch = includeInFilter(rowState);
+
+        rowState->m_matchesSelf = MatchesFilter(rowState); //update own match state
+
+        auto& matchingChildren = rowState->m_matchingDescendents;
+        for (auto childIter = matchingChildren.begin(); childIter != matchingChildren.end(); /* omitted */)
+        {
+            // it's a child's job to update its parents, but a parent still has to check if any
+            // formerly matching children have been removed
+            auto& currChildren = rowState->m_childMatchState;
+            if (AZStd::find(currChildren.begin(), currChildren.end(), *childIter) == currChildren.end())
+            {
+                childIter = matchingChildren.erase(childIter);
+            }
+            else
+            {
+                ++childIter;
+            }
+
+        }
+
+        // update each ancestor until updating their m_matchingDescendents doesn't change their inclusion state
+        MatchInfoNode* currState = rowState;
+        bool nowMatches = includeInFilter(currState);
+        while (currState->m_parentNode && nowMatches != usedToMatch)
+        {
+            usedToMatch = includeInFilter(currState->m_parentNode);
+            if (nowMatches)
+            {
+                currState->m_parentNode->m_matchingDescendents.insert(currState);
+            }
+            else
+            {
+                currState->m_parentNode->m_matchingDescendents.erase(currState);
+            }
+            nowMatches = includeInFilter(currState->m_parentNode);
+            currState = currState->m_parentNode;
+        }
+    }
+
+    Dom::Path RowFilterAdapter::GetRowPath(const Dom::Path& sourcePath) const
+    {
+        Dom::Path rowPath;
+        const auto& contents = m_sourceAdapter->GetContents();
+        const auto* currDomValue = &contents;
+        for (const auto& pathEntry : sourcePath)
+        {
+            currDomValue = &(*currDomValue)[pathEntry];
+            if (IsRow(*currDomValue))
+            {
+                rowPath.Push(pathEntry);
+            }
+        }
+        return rowPath;
+    }
+
+    ValueStringFilter::ValueStringFilter()
+        : RowFilterAdapter()
+    {
+    }
+
+    void ValueStringFilter::SetFilterString(QString filterString)
+    {
+        if (m_filterString != filterString)
+        {
+            m_filterString = AZStd::move(filterString);
+            InvalidateFilter();
+            SetFilterActive(!m_filterString.isEmpty());
+        }
+    }
+
+    RowFilterAdapter::MatchInfoNode* ValueStringFilter::NewMatchInfoNode(MatchInfoNode* parentRow) const
+    {
+        return new StringMatchNode(parentRow);
+    }
+
+    void ValueStringFilter::CacheDomInfoForNode(const Dom::Value& domValue, MatchInfoNode* matchNode) const
+    {
+        auto actualNode = static_cast<StringMatchNode*>(matchNode);
+        const bool nodeIsRow = IsRow(domValue);
+        AZ_Assert(nodeIsRow, "Only row nodes should be cached by a RowFilterAdapter");
+        if (nodeIsRow)
+        {
+            actualNode->m_matchableDomTerms.clear();
+            for (auto childIter = domValue.ArrayBegin(), endIter = domValue.ArrayEnd(); childIter != endIter; ++childIter)
+            {
+                auto& currChild = *childIter;
+                if (currChild.IsNode())
+                {
+                    auto childName = currChild.GetNodeName();
+                    if (childName != Dpe::GetNodeName<Dpe::Nodes::Row>()) // don't cache child rows, they have they're own entries
+                    {
+                        static const Name valueName("Value");
+                        auto foundValue = currChild.FindMember(valueName);
+                        if (foundValue != currChild.MemberEnd())
+                        {
+                            actualNode->AddStringifyValue(foundValue->second);
+                        }
+
+                        if (m_includeDescriptions)
+                        {
+                            static const Name descriptionName("Description");
+                            auto foundDescription = currChild.FindMember(descriptionName);
+                            if (foundDescription != currChild.MemberEnd())
+                            {
+                                actualNode->AddStringifyValue(foundDescription->second);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    bool ValueStringFilter::MatchesFilter(MatchInfoNode* matchNode) const
+    {
+        auto actualNode = static_cast<StringMatchNode*>(matchNode);
+        return (m_filterString.isEmpty() || actualNode->m_matchableDomTerms.contains(m_filterString, Qt::CaseInsensitive));
+    }
+
+    void ValueStringFilter::StringMatchNode::AddStringifyValue(const Dom::Value& domValue)
+    {
+        QString stringifiedValue;
+
+        if (domValue.IsNull())
+        {
+            stringifiedValue = QStringLiteral("null");
+        }
+        else if (domValue.IsBool())
+        {
+            stringifiedValue = (domValue.GetBool() ? QStringLiteral("true") : QStringLiteral("false"));
+        }
+        else if (domValue.IsInt())
+        {
+            stringifiedValue = QString::number(domValue.GetInt64());
+        }
+        else if (domValue.IsUint())
+        {
+            stringifiedValue = QString::number(domValue.GetUint64());
+        }
+        else if (domValue.IsDouble())
+        {
+            stringifiedValue = QString::number(domValue.GetDouble());
+        }
+        else if (domValue.IsString())
+        {
+            AZStd::string_view stringView = domValue.GetString();
+            stringifiedValue = QString::fromUtf8(stringView.data(), aznumeric_cast<int>(stringView.size()));
+        }
+
+        if (!stringifiedValue.isEmpty())
+        {
+            if (!m_matchableDomTerms.isEmpty())
+            {
+                m_matchableDomTerms.append(QChar::Space);
+            }
+            m_matchableDomTerms.append(stringifiedValue);
+        }
+    }
+} // namespace AZ::DocumentPropertyEditor

+ 123 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/FilterAdapter.h

@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzFramework/DocumentPropertyEditor/DocumentAdapter.h>
+#include <QString>
+#include <AzCore/std/containers/vector.h>
+#include <AzCore/std/containers/set.h>
+
+namespace AZ::DocumentPropertyEditor
+{
+    class RowFilterAdapter : public DocumentAdapter
+    {
+    public:
+        RowFilterAdapter();
+        ~RowFilterAdapter();
+
+        void SetSourceAdapter(DocumentAdapterPtr sourceAdapter);
+
+    protected:
+        struct MatchInfoNode
+        {
+            ~MatchInfoNode()
+            {
+                for (auto child : m_childMatchState)
+                {
+                    delete child;
+                }
+                m_childMatchState.clear();
+            }
+
+            bool m_matchesSelf = false;
+            AZStd::set<MatchInfoNode*> m_matchingDescendents;
+            MatchInfoNode* m_parentNode = nullptr;
+
+            //! Deque where only row node children are populate, other children are null entries
+            AZStd::deque<MatchInfoNode*> m_childMatchState;
+
+        protected:
+            MatchInfoNode(MatchInfoNode* parentNode)
+                : m_parentNode(parentNode)
+            {
+            }
+        };
+
+        // pure virtual methods for new RowFilterAdapters
+        virtual MatchInfoNode* NewMatchInfoNode(MatchInfoNode* parentRow) const = 0;
+        virtual void CacheDomInfoForNode(const Dom::Value& domValue, MatchInfoNode* matchNode) const = 0;
+        virtual bool MatchesFilter(MatchInfoNode* matchNode) const = 0;
+
+        static bool IsRow(const Dom::Value& domValue);
+        bool IsRow(const Dom::Path& sourcePath) const;
+
+        void SetFilterActive(bool activateFilter);
+        void InvalidateFilter();
+        
+        Dom::Value GenerateContents() override;
+
+        DocumentAdapterPtr m_sourceAdapter;
+
+        void HandleReset();
+        void HandleDomChange(const Dom::Patch& patch);
+        void HandleDomMessage(const AZ::DocumentPropertyEditor::AdapterMessage& message, Dom::Value& value);
+
+        DocumentAdapter::ResetEvent::Handler m_resetHandler;
+        ChangedEvent::Handler m_changedHandler;
+        MessageEvent::Handler m_domMessageHandler;
+
+        MatchInfoNode* GetMatchNodeAtPath(const Dom::Path& sourcePath);
+
+        /*! populates the MatchInfoNode nodes for the given path and any descendent row children created.
+         *  All new nodes have their m_matchesSelf, m_matchingDescendents, and  m_matchableDomTerms set */
+        void PopulateNodesAtPath(const Dom::Path& sourcePath, bool replaceExisting);
+
+        /*! updates the match states (m_matchesSelf, m_matchingDescendents) for the given row node,
+            and updates the m_matchingDescendents state for all its ancestors
+            \param rowState the row to operate on */
+        void UpdateMatchState(MatchInfoNode* rowState);
+
+        //! returns the first path in the ancestry of sourcePath that is of type Row, including self
+        Dom::Path GetRowPath(const Dom::Path& sourcePath) const;
+
+        //! indicates whether all children of a direct match are considered matching as well
+        bool m_includeAllMatchDescendents = true;
+        bool m_filterActive = false;
+
+        MatchInfoNode* m_root = nullptr;
+    };
+
+    class ValueStringFilter : public RowFilterAdapter
+    {
+    public:
+        ValueStringFilter();
+        void SetFilterString(QString filterString);
+
+        struct StringMatchNode : public RowFilterAdapter::MatchInfoNode
+        {
+            StringMatchNode(MatchInfoNode* parentRow)
+                : MatchInfoNode(parentRow)
+            {
+            }
+
+            void AddStringifyValue(const Dom::Value& domValue);
+
+            QString m_matchableDomTerms;
+        };
+
+    protected:
+        // pure virtual overrides
+        virtual MatchInfoNode* NewMatchInfoNode(MatchInfoNode* parentRow) const  override;
+        virtual void CacheDomInfoForNode(const Dom::Value& domValue, MatchInfoNode* matchNode) const override;
+        virtual bool MatchesFilter(MatchInfoNode* matchNode) const override;
+
+        bool m_includeDescriptions = true;
+        QString m_filterString;
+    };
+} // namespace AZ::DocumentPropertyEditor

+ 1 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/IPropertyEditor.h

@@ -55,7 +55,7 @@ namespace AzToolsFramework
             using DynamicEditDataProvider = AZStd::function<const AZ::Edit::ElementData*(const void* /*objectPtr*/, const AZ::SerializeContext::ClassData* /*classData*/)>;
             virtual void SetDynamicEditDataProvider([[maybe_unused]] DynamicEditDataProvider provider) {};
 
-            virtual void SetSavedStateKey([[maybe_unused]] AZ::u32 key) {};
+            virtual void SetSavedStateKey([[maybe_unused]] AZ::u32 key, [[maybe_unused]] AZStd::string propertyEditorName = {}) {}
 
             virtual bool HasFilteredOutNodes() const
             {

+ 0 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/KeyQueryDPE.cpp

@@ -7,7 +7,6 @@
  */
 
 #include "KeyQueryDPE.h"
-#include "AzToolsFramework/UI/DocumentPropertyEditor/ui_KeyQueryDPE.h"
 
 namespace AzToolsFramework
 {

+ 101 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/SettingsRegistrar.cpp

@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include "SettingsRegistrar.h"
+
+#include <AzCore/IO/ByteContainerStream.h>
+#include <AzCore/Settings/SettingsRegistry.h>
+
+namespace AzToolsFramework
+{
+    AZ::Outcome<void, AZStd::string> SettingsRegistrar::SaveSettingsToFile(
+        AZ::IO::PathView relativeFilepath,
+        AZ::SettingsRegistryMergeUtils::DumperSettings dumperSettings,
+        const AZStd::string& anchorKey,
+        AZ::SettingsRegistryInterface* registry) const
+    {
+        registry = !registry ? AZ::SettingsRegistry::Get() : registry;
+        if (!registry)
+        {
+            return AZ::Failure(AZStd::string::format("Failed to access global settings registry"));
+        }
+
+        constexpr const char* setregFileExt = ".setreg";
+        if (relativeFilepath.Extension() != setregFileExt)
+        {
+            return AZ::Failure(AZStd::string::format(
+                "Failed to save settings to file '%s': file must be of type '.setreg'", relativeFilepath.FixedMaxPathString().c_str()));
+        }
+
+        AZ::IO::FixedMaxPath fullSettingsPath = AZ::Utils::GetProjectPath();
+        fullSettingsPath /= relativeFilepath;
+        const char* posixSettingsPath = fullSettingsPath.AsPosix().c_str();
+
+        AZStd::string stringBuffer;
+        AZ::IO::ByteContainerStream stringStream(&stringBuffer);
+        if (!AZ::SettingsRegistryMergeUtils::DumpSettingsRegistryToStream(*registry, anchorKey, stringStream, dumperSettings))
+        {
+            return AZ::Failure(AZStd::string::format(
+                "Failed to save settings to file '%s': failed to retrieve settings from registry", posixSettingsPath));
+        }
+
+        constexpr auto openMode = AZ::IO::SystemFile::SF_OPEN_CREATE
+            | AZ::IO::SystemFile::SF_OPEN_CREATE_PATH
+            | AZ::IO::SystemFile::SF_OPEN_WRITE_ONLY;
+        if (AZ::IO::SystemFile outputFile; outputFile.Open(posixSettingsPath, openMode))
+        {
+            if(outputFile.Write(stringBuffer.data(), stringBuffer.size()) != stringBuffer.size())
+            {
+                return AZ::Failure(AZStd::string::format(
+                    "Failed to save settings to file '%s': incomplete contents written", posixSettingsPath));
+            }
+        }
+
+        return AZ::Success();
+    }
+
+    AZ::Outcome<void, AZStd::string> SettingsRegistrar::LoadSettingsFromFile(
+        AZ::IO::PathView relativeFilepath,
+        AZStd::string_view anchorKey,
+        AZ::SettingsRegistryInterface* registry,
+        AZ::SettingsRegistryInterface::Format format) const
+    {
+        registry = !registry ? AZ::SettingsRegistry::Get() : registry;
+        if (!registry)
+        {
+            return AZ::Failure(AZStd::string::format("Failed to access global settings registry"));
+        }
+
+        AZ::IO::FixedMaxPath fullSettingsPath = AZ::Utils::GetProjectPath();
+        fullSettingsPath /= relativeFilepath;
+
+        if (!AZ::IO::SystemFile::Exists(fullSettingsPath.c_str()))
+        {
+            return AZ::Failure(AZStd::string::format("Settings file does not exist: '%s'", fullSettingsPath.c_str()));
+        }
+
+        if (registry->MergeSettingsFile(fullSettingsPath.Native(), format, anchorKey))
+        {
+            return AZ::Success();
+        }
+        else
+        {
+            return AZ::Failure(AZStd::string::format("Failed to merge settings file '%s': check log for errors", fullSettingsPath.c_str()));
+        }
+    }
+
+    bool SettingsRegistrar::RemoveSettingFromRegistry(AZStd::string_view registryPath, AZ::SettingsRegistryInterface* registry) const
+    {
+        registry = !registry ? AZ::SettingsRegistry::Get() : registry;
+        if (!registry)
+        {
+            return false;
+        }
+        return registry->Remove(registryPath);
+    }
+} // namespace AzToolsFramework

+ 99 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/UI/DocumentPropertyEditor/SettingsRegistrar.h

@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/std/string/string_view.h>
+#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
+#include <AzCore/Utils/Utils.h>
+
+namespace AzToolsFramework
+{
+    //! Class wrapping file management required to save SettingsRegistry data to file as well as load
+    //! json data from file to the SettingsRegistry.
+    class SettingsRegistrar
+    {
+    public:
+        //! Saves all settings stored at the given SettingsRegistry key into a file at the given filepath. The
+        //! file and directory structure will be created if it does now exist. The file open mode is set to
+        //! overwrite existing file contents.
+        //! The path passed to this function is expected to be relative to the project root.
+        //! The DumperSettings can be used to narrow down the settings data dumped to file by
+        //! providing an filter function.
+        //! Specifying a json pointer path prefix in the DumperSettings may be required in order
+        //! for the dumped json data to match the in-memory path structure.
+        //! The anchor key specifies a path in the registry where the settings will be saved.
+        AZ::Outcome<void, AZStd::string> SaveSettingsToFile(
+            AZ::IO::PathView relativeFilepath,
+            AZ::SettingsRegistryMergeUtils::DumperSettings dumperSettings,
+            const AZStd::string& anchorKey = {},
+            AZ::SettingsRegistryInterface* registry = nullptr) const;
+
+        //! Loads settings from the provided '.setreg' file and merges settings into the SettingsRegistry
+        //! at the given anchor key.
+        //! The path passed to this function is expected to be relative to the project root.
+        AZ::Outcome<void, AZStd::string> LoadSettingsFromFile(
+            AZ::IO::PathView relativeFilepath,
+            AZStd::string_view anchorKey = {},
+            AZ::SettingsRegistryInterface* registry = nullptr,
+            AZ::SettingsRegistryInterface::Format format = AZ::SettingsRegistryInterface::Format::JsonMergePatch) const;
+
+        //! Attempts to retrieve settings stored at the given registry path and put them in the provided object.
+        //! The provided object is expected to be intialized before being passed to this function and it must
+        //! be AZ_RTTI enabled.
+        template<typename AzRttiEnabled_T>
+        AZ::Outcome<void, AZStd::string> GetObjectSettings(
+            AzRttiEnabled_T* outSettingsObject,
+            AZStd::string_view registryPath,
+            AZ::SettingsRegistryInterface* registry = nullptr) const
+        {
+            registry = !registry ? AZ::SettingsRegistry::Get() : registry;
+            if (!registry)
+            {
+                return AZ::Failure(AZStd::string::format("Failed to access global settings registry for data retrieval"));
+            }
+
+            if (registry->GetObject(*outSettingsObject, registryPath))
+            {
+                return AZ::Success();
+            }
+            else
+            {
+                return AZ::Failure(AZStd::string::format("Failed to retrieve settings at path '%s'", registryPath.data()));
+            }
+        }
+
+        //! Attempts to store reflected properties of the given AZ_RTTI enabled object at the given registry path.
+        template<typename AzRttiEnabled_T>
+        AZ::Outcome<void, AZStd::string> StoreObjectSettings(
+            AZStd::string_view registryPath,
+            AzRttiEnabled_T* settingsObject,
+            AZ::SettingsRegistryInterface* registry = nullptr) const
+        {
+            registry = !registry ? AZ::SettingsRegistry::Get() : registry;
+            if (!registry)
+            {
+                return AZ::Failure(AZStd::string::format("Failed to access global settings registry for data storage"));
+            }
+
+            if (registry->SetObject(registryPath, *settingsObject))
+            {
+                return AZ::Success();
+            }
+            else
+            {
+                return AZ::Failure(AZStd::string::format("Failed to store settings at path '%s'", registryPath.data()));
+            }
+        }
+
+        bool RemoveSettingFromRegistry(AZStd::string_view registryPath, AZ::SettingsRegistryInterface* registry = nullptr) const;
+
+        //! The file extension expected for SettingsRegistry files.
+        static constexpr const char* SettingsRegistryFileExt = AZ::SettingsRegistryInterface::Extension;
+    };
+} // namespace AzToolsFramework

+ 3 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyManagerComponent.cpp

@@ -9,6 +9,7 @@
 #include <AzCore/Serialization/EditContext.h>
 #include <AzCore/Component/ComponentApplicationBus.h>
 #include <AzToolsFramework/ToolsComponents/EditorEntityIdContainer.h>
+#include <AzToolsFramework/UI/DocumentPropertyEditor/DocumentPropertyEditor.h>
 #include <AzToolsFramework/UI/PropertyEditor/PropertyAudioCtrlTypes.h>
 #include <AzToolsFramework/UI/PropertyEditor/GenericComboBoxCtrl.h>
 
@@ -297,6 +298,8 @@ namespace AzToolsFramework
             AzToolsFramework::CReflectedVarAudioControl::Reflect(context);
             ReflectPropertyEditor(context);
 
+            DocumentPropertyEditorSettings::Reflect(context);
+
             // reflect data for script, serialization, editing...
             if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
             {

+ 1 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ReflectedPropertyEditor.cpp

@@ -1200,7 +1200,7 @@ namespace AzToolsFramework
         m_impl->m_queuedTabOrderRefresh = false;
     }
 
-    void ReflectedPropertyEditor::SetSavedStateKey(AZ::u32 key)
+    void ReflectedPropertyEditor::SetSavedStateKey(AZ::u32 key, [[maybe_unused]] AZStd::string propertyEditorName)
     {
         if (m_impl->m_savedStateKey != key)
         {

+ 2 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ReflectedPropertyEditor.hxx

@@ -79,7 +79,8 @@ namespace AzToolsFramework
 
         void SetFilterString(AZStd::string str) override;
         AZStd::string GetFilterString();
-        void SetSavedStateKey(AZ::u32 key) override; // a settings key which is used to store and load the set of things that are expanded or not and other settings
+        // a settings key which is used to store and load the set of things that are expanded or not and other settings
+        void SetSavedStateKey([[maybe_unused]] AZ::u32 key, [[maybe_unused]] AZStd::string propertyEditorName = "") override;
 
         void QueueInvalidation(PropertyModificationRefreshLevel level) override;
         //will force any queued invalidations to happen immediately

+ 3 - 3
Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiManager.cpp

@@ -152,8 +152,8 @@ namespace AzToolsFramework::ViewportUi
     {
         if (auto clusterIt = m_clusterButtonGroups.find(clusterId); clusterIt != m_clusterButtonGroups.end())
         {
-            m_clusterButtonGroups.erase(clusterIt);
             m_viewportUi->RemoveViewportUiElement(clusterIt->second->GetViewportUiElementId());
+            m_clusterButtonGroups.erase(clusterIt);
         }
     }
 
@@ -161,8 +161,8 @@ namespace AzToolsFramework::ViewportUi
     {
         if (auto switcherIt = m_switcherButtonGroups.find(switcherId); switcherIt != m_switcherButtonGroups.end())
         {
-            m_switcherButtonGroups.erase(switcherIt);
             m_viewportUi->RemoveViewportUiElement(switcherIt->second->GetViewportUiElementId());
+            m_switcherButtonGroups.erase(switcherIt);
         }
     }
 
@@ -246,8 +246,8 @@ namespace AzToolsFramework::ViewportUi
     {
         if (auto textFieldIt = m_textFields.find(textFieldId); textFieldIt != m_textFields.end())
         {
-            m_textFields.erase(textFieldIt);
             m_viewportUi->RemoveViewportUiElement(textFieldIt->second->m_viewportId);
+            m_textFields.erase(textFieldIt);
         }
     }
 

+ 6 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake

@@ -368,6 +368,8 @@ set(FILES
     UI/Docking/DockWidgetUtils.h
     UI/DocumentPropertyEditor/ContainerActionButtonHandler.cpp
     UI/DocumentPropertyEditor/ContainerActionButtonHandler.h
+    UI/DocumentPropertyEditor/FilterAdapter.cpp
+    UI/DocumentPropertyEditor/FilterAdapter.h
     UI/DocumentPropertyEditor/PropertyEditorToolsSystemInterface.h
     UI/DocumentPropertyEditor/PropertyEditorToolsSystem.cpp
     UI/DocumentPropertyEditor/PropertyEditorToolsSystem.h
@@ -375,10 +377,14 @@ set(FILES
     UI/DocumentPropertyEditor/PropertyHandlerWidget.h
     UI/DocumentPropertyEditor/DocumentPropertyEditor.cpp
     UI/DocumentPropertyEditor/DocumentPropertyEditor.h
+    UI/DocumentPropertyEditor/DocumentPropertyEditorSettings.cpp
+    UI/DocumentPropertyEditor/DocumentPropertyEditorSettings.h
     UI/DocumentPropertyEditor/IPropertyEditor.h
     UI/DocumentPropertyEditor/KeyQueryDPE.cpp
     UI/DocumentPropertyEditor/KeyQueryDPE.h
     UI/DocumentPropertyEditor/KeyQueryDPE.ui
+    UI/DocumentPropertyEditor/SettingsRegistrar.cpp
+    UI/DocumentPropertyEditor/SettingsRegistrar.h
     UI/DPEDebugViewer/DPEDebugModel.cpp
     UI/DPEDebugViewer/DPEDebugModel.h
     UI/DPEDebugViewer/DPEDebugTextView.cpp

+ 49 - 26
Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_linux.cpp

@@ -6,6 +6,7 @@
  *
  */
 
+#include <AzCore/IO/Path/Path.h>
 #include <AzCore/std/string/fixed_string.h>
 #include <native/FileWatcher/FileWatcher.h>
 #include <native/FileWatcher/FileWatcher_platform.h>
@@ -56,6 +57,34 @@ void FileWatcher::PlatformImplementation::Finalize()
     m_inotifyHandle = -1;
 }
 
+bool FileWatcher::PlatformImplementation::TryToWatch(const QString &pathStr, int* errnoPtr)
+{
+    AZ::IO::FixedMaxPathString path(pathStr.toUtf8().constData());
+    int watchHandle = inotify_add_watch(
+        m_inotifyHandle, path.c_str(), IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE);
+    const auto err = errno; // Only contains relevant information if we actually failed
+
+    if (watchHandle < 0)
+    {
+        [[maybe_unused]] const char* extraStr = (err == ENOSPC ? " (try increasing fs.inotify.max_user_watches with sysctl)" : "");
+        [[maybe_unused]] AZStd::fixed_string<255> errorString;
+        AZ_Warning(
+            "FileWatcher", false,
+            "inotify_add_watch failed for path %s with error %d: %s%s",
+            path.c_str(), err, strerror_r(err, errorString.data(), errorString.capacity()), extraStr);
+        if (errnoPtr)
+        {
+            *errnoPtr = err;
+        }
+        return false;
+    }
+    {
+        QMutexLocker lock{&m_handleToFolderMapLock};
+        m_handleToFolderMap[watchHandle] = pathStr;
+    }
+    return true;
+}
+
 void FileWatcher::PlatformImplementation::AddWatchFolder(QString folder, bool recursive)
 {
     if (m_inotifyHandle < 0)
@@ -66,43 +95,37 @@ void FileWatcher::PlatformImplementation::AddWatchFolder(QString folder, bool re
     // Clean up the path before accepting it as a watch folder
     QString cleanPath = QDir::cleanPath(folder);
 
-    // Add the folder to watch and track it
-    int watchHandle = inotify_add_watch(m_inotifyHandle,
-                                        cleanPath.toUtf8().constData(),
-                                        IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE);
-
-    if (watchHandle < 0)
+    if (!TryToWatch(cleanPath))
     {
-        [[maybe_unused]] const auto err = errno;
-        [[maybe_unused]] AZStd::fixed_string<255> errorString;
-        AZ_Warning("FileWatcher", false, "inotify_add_watch failed for path %s: %s", cleanPath.toUtf8().constData(), strerror_r(err, errorString.data(), errorString.capacity()));
         return;
     }
-    {
-        QMutexLocker lock{&m_handleToFolderMapLock};
-        m_handleToFolderMap[watchHandle] = cleanPath;
-    }
 
     // Add all the contents (files and directories) to watch and track them
-    QDirIterator dirIter(folder, QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files, (recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags) | QDirIterator::FollowSymlinks);
+    QDirIterator dirIter(
+        folder, QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files,
+        (recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags) | QDirIterator::FollowSymlinks);
 
     while (dirIter.hasNext())
     {
         QString dirName = dirIter.next();
-
-        watchHandle = inotify_add_watch(m_inotifyHandle,
-                                            dirName.toUtf8().constData(),
-                                            IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE);
-        if (watchHandle < 0)
+        int theErrno;
+        if (!TryToWatch(dirName, &theErrno))
         {
-            [[maybe_unused]] const auto err = errno;
-            [[maybe_unused]] AZStd::fixed_string<255> errorString;
-            AZ_Warning("FileWatcher", false, "inotify_add_watch failed for path %s: %s", dirName.toUtf8().constData(), strerror_r(err, errorString.data(), errorString.capacity()));
-            return;
+            switch (theErrno)
+            {
+            case EACCES:
+            case EBADF:
+            case ENOENT:
+                // Errors specific to the file: try next one
+                continue;
+            default:
+                // Other errors are usually non-recoverable: bail out to avoid warning spam
+                AZ_Warning(
+                    "FileWatcher", false,
+                    "Giving up on directory %s due to errors", cleanPath.toUtf8().constData());
+                return;
+            }
         }
-
-        QMutexLocker lock{&m_handleToFolderMapLock};
-        m_handleToFolderMap[watchHandle] = dirName;
     }
 }
 

+ 6 - 0
Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_linux.h

@@ -20,6 +20,12 @@ public:
     void AddWatchFolder(QString folder, bool recursive);
     void RemoveWatchFolder(int watchHandle);
 
+    //! Try to watch the given path.
+    //! @param errnoPtr If provided, gets set to the errno right after the inotify_add_watch() call.
+    //!                 Note: only contains valid information if we actually failed.
+    //! @return Was the watch successful?
+    bool TryToWatch(const QString &path, int* errnoPtr = nullptr);
+
     int                         m_inotifyHandle = -1;
     QMutex                      m_handleToFolderMapLock;
     QHash<int, QString>         m_handleToFolderMap;

+ 2 - 0
Code/Tools/AssetProcessor/assetprocessor_static_files.cmake

@@ -25,6 +25,8 @@ set(FILES
     native/AssetManager/assetScannerWorker.h
     native/AssetManager/FileStateCache.cpp
     native/AssetManager/FileStateCache.h
+    native/AssetManager/Validators/LfsPointerFileValidator.cpp
+    native/AssetManager/Validators/LfsPointerFileValidator.h
     native/AssetManager/PathDependencyManager.cpp
     native/AssetManager/PathDependencyManager.h
     native/AssetManager/SourceFileRelocator.cpp

+ 2 - 0
Code/Tools/AssetProcessor/assetprocessor_test_files.cmake

@@ -43,6 +43,8 @@ set(FILES
     native/tests/assetmanager/AssetManagerTestingBase.h
     native/tests/assetmanager/IntermediateAssetTests.cpp
     native/tests/assetmanager/IntermediateAssetTests.h
+    native/tests/assetmanager/Validators/LfsPointerFileValidatorTests.cpp
+    native/tests/assetmanager/Validators/LfsPointerFileValidatorTests.h
     native/tests/utilities/assetUtilsTest.cpp
     native/tests/platformconfiguration/platformconfigurationtests.cpp
     native/tests/platformconfiguration/platformconfigurationtests.h

+ 26 - 12
Code/Tools/AssetProcessor/native/AssetDatabase/AssetDatabase.cpp

@@ -116,7 +116,7 @@ namespace AssetProcessor
             "CREATE TABLE IF NOT EXISTS SourceDependency("
             "    SourceDependencyID            INTEGER PRIMARY KEY AUTOINCREMENT, "
             "    BuilderGuid                   BLOB NOT NULL, "
-            "    Source                        TEXT NOT NULL collate nocase, "
+            "    SourceGuid                    BLOB NOT NULL, "
             "    DependsOnSource               TEXT NOT NULL collate nocase, "
             "    SubIds                        TEXT NOT NULL collate nocase, "
             "    TypeOfDependency              INTEGER NOT NULL DEFAULT 0,"
@@ -181,7 +181,7 @@ namespace AssetProcessor
             "CREATE INDEX IF NOT EXISTS DependsOnSource_SourceDependency ON SourceDependency (DependsOnSource);";
         static const char* CREATEINDEX_BUILDERGUID_SOURCE_SOURCEDEPENDENCY = "AssetProcesser::CreateIndexBuilderGuid_Source_SourceDependency";
         static const char* CREATEINDEX_BUILDERGUID_SOURCE_SOURCEDEPENDENCY_STATEMENT =
-            "CREATE INDEX IF NOT EXISTS BuilderGuid_Source_SourceDependency ON SourceDependency (BuilderGuid, Source);";
+            "CREATE INDEX IF NOT EXISTS BuilderGuid_Source_SourceDependency ON SourceDependency (BuilderGuid, SourceGuid);";
         static const char* CREATEINDEX_TYPEOFDEPENDENCY_SOURCEDEPENDENCY = "AssetProcessor::CreateIndexTypeOfDependency_SourceDependency";
         static const char* CREATEINDEX_TYPEOFDEPENDENCY_SOURCEDEPENDENCY_STATEMENT =
             "CREATE INDEX IF NOT EXISTS TypeOfDependency_SourceDependency ON SourceDependency (TypeOfDependency);";
@@ -481,11 +481,11 @@ namespace AssetProcessor
 
         static const char* INSERT_SOURCE_DEPENDENCY = "AssetProcessor::InsertSourceDependency";
         static const char* INSERT_SOURCE_DEPENDENCY_STATEMENT =
-            "INSERT INTO SourceDependency (BuilderGuid, Source, DependsOnSource, TypeOfDependency, FromAssetId, SubIds) "
+            "INSERT INTO SourceDependency (BuilderGuid, SourceGuid, DependsOnSource, TypeOfDependency, FromAssetId, SubIds) "
             "VALUES (:builderGuid, :source, :dependsOnSource, :typeofdependency, :fromAssetId, :subIds);";
         static const auto s_InsertSourceDependencyQuery = MakeSqlQuery(INSERT_SOURCE_DEPENDENCY, INSERT_SOURCE_DEPENDENCY_STATEMENT, LOG_NAME,
             SqlParam<AZ::Uuid>(":builderGuid"),
-            SqlParam<const char*>(":source"),
+            SqlParam<AZ::Uuid>(":source"),
             SqlParam<const char*>(":dependsOnSource"),
             SqlParam<AZ::s32>(":typeofdependency"),
             SqlParam<AZ::s32>(":fromAssetId"),
@@ -810,6 +810,9 @@ namespace AssetProcessor
             "ALTER TABLE Products "
             "ADD Flags INTEGER NOT NULL DEFAULT 1;";
 
+        static const char* CREATEINDEX_SOURCEDEPENDENCY_SOURCEGUID = "AssetProcessor::CreateIndexSourceGuidSourceDependency";
+        static const char* CREATEINDEX_SOURCEDEPENDENCY_SOURCEGUID_STATEMENT =
+            "CREATE INDEX IF NOT EXISTS SourceGuid_SourceDependency ON SourceDependency (SourceGuid);";
     }
 
     AssetDatabaseConnection::AssetDatabaseConnection()
@@ -1117,10 +1120,19 @@ namespace AssetProcessor
             if (m_databaseConnection->ExecuteOneOffStatement(CREATE_STATS_TABLE))
             {
                 foundVersion = DatabaseVersion::AddedStatsTable;
-                AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Upgraded Asset Database to version %i (AddedStatsTable)\n", foundVersion)
+                AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Upgraded Asset Database to version %i (AddedStatsTable)\n", foundVersion);
             }
         }
 
+        if(foundVersion == DatabaseVersion::AddedStatsTable)
+        {
+            // Version update - change SourceDependency Source to SourceGuid column
+            // Do nothing so the whole database is dropped.
+            // Unfortunately we have to reprocess all assets because of the way the fingerprinting algorithm works,
+            // changing from storing the path to the UUID changes the fingerprint, resulting in all assets reprocessing anyway
+            AZ_TracePrintf(AssetProcessor::ConsoleChannel, "Asset database version updated to ChangedSourceDependencySourceColumn, database will be cleared as migration is not possible for this update\n", foundVersion);
+        }
+
         if (foundVersion == CurrentDatabaseVersion())
         {
             dropAllTables = false;
@@ -1407,11 +1419,13 @@ namespace AssetProcessor
         m_createStatements.push_back(CREATEINDEX_SCANFOLDERS_FILES);
 
         m_databaseConnection->AddStatement(CREATEINDEX_SOURCEDEPENDENCY_SOURCE, CREATEINDEX_SOURCEDEPENDENCY_SOURCE_STATEMENT);
-        m_createStatements.push_back(CREATEINDEX_SOURCEDEPENDENCY_SOURCE);
 
         m_databaseConnection->AddStatement(DROPINDEX_BUILDERGUID_SOURCE_SOURCEDEPENDENCY, DROPINDEX_BUILDERGUID_SOURCE_SOURCEDEPENDENCY_STATEMENT);
         m_createStatements.push_back(DROPINDEX_BUILDERGUID_SOURCE_SOURCEDEPENDENCY);
 
+        m_databaseConnection->AddStatement(CREATEINDEX_SOURCEDEPENDENCY_SOURCEGUID, CREATEINDEX_SOURCEDEPENDENCY_SOURCEGUID_STATEMENT);
+        m_createStatements.push_back(CREATEINDEX_SOURCEDEPENDENCY_SOURCEGUID);
+
         m_databaseConnection->AddStatement(DELETE_AUTO_SUCCEED_JOBS, DELETE_AUTO_SUCCEED_JOBS_STATEMENT);
     }
 
@@ -2593,7 +2607,7 @@ namespace AssetProcessor
     bool AssetDatabaseConnection::SetSourceFileDependency(SourceFileDependencyEntry& entry)
     {
         //first make sure its not already in the database
-        if (!s_InsertSourceDependencyQuery.BindAndStep(*m_databaseConnection, entry.m_builderGuid, entry.m_source.c_str(), entry.m_dependsOnSource.c_str(), entry.m_typeOfDependency, entry.m_fromAssetId, entry.m_subIds.c_str()))
+        if (!s_InsertSourceDependencyQuery.BindAndStep(*m_databaseConnection, entry.m_builderGuid, entry.m_sourceGuid, entry.m_dependsOnSource.c_str(), entry.m_typeOfDependency, entry.m_fromAssetId, entry.m_subIds.c_str()))
         {
             return false;
         }
@@ -2634,10 +2648,10 @@ namespace AssetProcessor
         return s_DeleteSourceDependencySourcedependencyidQuery.BindAndStep(*m_databaseConnection, sourceFileDependencyId);
     }
 
-    bool AssetDatabaseConnection::GetSourceFileDependenciesByBuilderGUIDAndSource(const AZ::Uuid& builderGuid, const char* source, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency typeOfDependency, SourceFileDependencyEntryContainer& container)
+    bool AssetDatabaseConnection::GetSourceFileDependenciesByBuilderGUIDAndSource(const AZ::Uuid& builderGuid, AZ::Uuid sourceGuid, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency typeOfDependency, SourceFileDependencyEntryContainer& container)
     {
         bool found = false;
-        bool succeeded = QueryDependsOnSourceBySourceDependency(source, nullptr, typeOfDependency,
+        bool succeeded = QueryDependsOnSourceBySourceDependency(sourceGuid, nullptr, typeOfDependency,
             [&](SourceFileDependencyEntry& entry)
         {
             if (builderGuid == entry.m_builderGuid)
@@ -2653,7 +2667,7 @@ namespace AssetProcessor
     bool AssetDatabaseConnection::GetSourceFileDependenciesByDependsOnSource(const QString& dependsOnSource, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency typeOfDependency, SourceFileDependencyEntryContainer& container)
     {
         bool found = false;
-        bool succeeded = QuerySourceDependencyByDependsOnSource(dependsOnSource.toUtf8().constData(), nullptr, typeOfDependency,
+        bool succeeded = QuerySourceDependencyByDependsOnSource(dependsOnSource.toUtf8().constData(), typeOfDependency,
             [&](SourceFileDependencyEntry& entry)
         {
             found = true;
@@ -2664,12 +2678,12 @@ namespace AssetProcessor
     }
 
     bool AssetDatabaseConnection::GetDependsOnSourceBySource(
-        const char* source,
+        AZ::Uuid sourceUuid,
         AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency typeOfDependency,
         AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer& container)
     {
         bool found = false;
-        bool succeeded = QueryDependsOnSourceBySourceDependency(source, nullptr, typeOfDependency,
+        bool succeeded = QueryDependsOnSourceBySourceDependency(sourceUuid, nullptr, typeOfDependency,
             [&](SourceFileDependencyEntry& entry)
         {
             found = true;

+ 2 - 2
Code/Tools/AssetProcessor/native/AssetDatabase/AssetDatabase.h

@@ -171,9 +171,9 @@ namespace AssetProcessor
         // The following functions are all search functions (as opposed to the above functions which fetch or operate on specific rows)
         // They tend to take a "Type of Dependency" filter - you can use DEP_Any to query all kinds of dependencies.
         /// Given a source file, what does it DEPEND ON?
-        bool GetDependsOnSourceBySource(const char* source, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency typeOfDependency, AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer& container);
+        bool GetDependsOnSourceBySource(AZ::Uuid sourceUuid, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency typeOfDependency, AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer& container);
         /// Given a source file and a builder UUID, does it DEPEND ON?
-        bool GetSourceFileDependenciesByBuilderGUIDAndSource(const AZ::Uuid& builderGuid, const char* source, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency typeOfDependency, AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer& container);
+        bool GetSourceFileDependenciesByBuilderGUIDAndSource(const AZ::Uuid& builderGuid, AZ::Uuid sourceGuid, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency typeOfDependency, AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer& container);
         /// Given a source file, what depends ON IT? ('reverse dependency')
         bool GetSourceFileDependenciesByDependsOnSource(const QString& dependsOnSource, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency typeOfDependency, AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer& container);
 

+ 21 - 4
Code/Tools/AssetProcessor/native/AssetManager/SourceFileRelocator.cpp

@@ -453,7 +453,6 @@ Please note that only those seed files will get updated that are active for your
         for (auto& relocationInfo : relocationContainer)
         {
             m_stateData->QuerySourceDependencyByDependsOnSource(relocationInfo.m_sourceEntry.m_sourceName.c_str(),
-                nullptr,
                 AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any, [&relocationInfo](AzToolsFramework::AssetDatabase::SourceFileDependencyEntry& dependencyEntry)
                 {
                     relocationInfo.m_sourceDependencyEntries.push_back(dependencyEntry);
@@ -748,9 +747,18 @@ Please note that only those seed files will get updated that are active for your
 
                 for (const auto& sourceDependency : relocationInfo.m_sourceDependencyEntries)
                 {
-                    report = AZStd::string::format("%s\t\tPATH: %s, TYPE: %d, %s\n", report.c_str(), sourceDependency.m_source.c_str(), sourceDependency.m_typeOfDependency, sourceDependency.m_fromAssetId ? "AssetId-based" : "Path-based");
+                    AzToolsFramework::AssetDatabase::SourceDatabaseEntry sourceEntry;
+                    m_stateData->QuerySourceBySourceGuid(
+                        sourceDependency.m_sourceGuid,
+                        [&sourceEntry](AzToolsFramework::AssetDatabase::SourceDatabaseEntry& entry)
+                        {
+                            sourceEntry = entry;
+                            return false;
+                        });
+
+                    report = AZStd::string::format("%s\t\tUUID: %s, TYPE: %d, %s\n", report.c_str(), sourceEntry.m_sourceName.c_str(), sourceDependency.m_typeOfDependency, sourceDependency.m_fromAssetId ? "AssetId-based" : "Path-based");
                     AZStd::string fileExtension;
-                    AZ::StringFunc::Path::GetExtension(sourceDependency.m_source.c_str(), fileExtension, false);
+                    AZ::StringFunc::Path::GetExtension(sourceEntry.m_sourceName.c_str(), fileExtension, false);
 
                     auto found = m_additionalHelpTextMap.find(fileExtension);
                     if (found != m_additionalHelpTextMap.end())
@@ -1345,7 +1353,16 @@ Please note that only those seed files will get updated that are active for your
 
             for (const auto& sourceDependency : relocationInfo.m_sourceDependencyEntries)
             {
-                AZStd::string fullPath = m_platformConfig->FindFirstMatchingFile(sourceDependency.m_source.c_str()).toUtf8().constData();
+                AzToolsFramework::AssetDatabase::SourceDatabaseEntry sourceEntry;
+                m_stateData->QuerySourceBySourceGuid(
+                    sourceDependency.m_sourceGuid,
+                    [&sourceEntry](AzToolsFramework::AssetDatabase::SourceDatabaseEntry& entry)
+                    {
+                        sourceEntry = entry;
+                        return false;
+                    });
+
+                AZStd::string fullPath = m_platformConfig->FindFirstMatchingFile(sourceEntry.m_sourceName.c_str()).toUtf8().constData();
 
                 fullPath = pathFixupFunc(fullPath.c_str());
 

+ 146 - 0
Code/Tools/AssetProcessor/native/AssetManager/Validators/LfsPointerFileValidator.cpp

@@ -0,0 +1,146 @@
+/*
+* Copyright (c) Contributors to the Open 3D Engine Project.
+* For complete copyright and license terms please see the LICENSE at the root of this distribution.
+*
+* SPDX-License-Identifier: Apache-2.0 OR MIT
+*
+*/
+
+#include <native/AssetManager/Validators/LfsPointerFileValidator.h>
+#include <native/assetprocessor.h>
+
+#include <AzFramework/FileFunc/FileFunc.h>
+#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
+
+namespace AssetProcessor
+{
+    LfsPointerFileValidator::LfsPointerFileValidator(const AZStd::vector<AZStd::string>& scanDirectories)
+    {
+        for (const AZStd::string& directory : scanDirectories)
+        {
+            ParseGitAttributesFile(directory.c_str());
+        }
+    }
+
+    void LfsPointerFileValidator::ParseGitAttributesFile(const AZStd::string& directory)
+    {
+        constexpr const char* gitAttributesFileName = ".gitattributes";
+        AZStd::string gitAttributesFilePath = AZStd::string::format("%s/%s", directory.c_str(), gitAttributesFileName);
+        if (!AzFramework::StringFunc::Path::Normalize(gitAttributesFilePath))
+        {
+            AZ_Error(AssetProcessor::DebugChannel, false,
+                "Failed to normalize %s file path %s.", gitAttributesFileName, gitAttributesFilePath.c_str());
+        }
+
+        if (!AZ::IO::FileIOBase::GetInstance()->Exists(gitAttributesFilePath.c_str()))
+        {
+            return;
+        }
+
+        // A gitattributes file is a simple text file that gives attributes to pathnames.
+        // Each line in gitattributes file is of form: pattern attr1 attr2 ...
+        // Example for LFS pointer file attributes: *.DLL filter=lfs diff=lfs merge=lfs -text
+        auto result = AzFramework::FileFunc::ReadTextFileByLine(gitAttributesFilePath, [this](const char* line) -> bool
+        {
+            // Skip any empty or comment lines
+            if (strlen(line) && line[0] != '#')
+            {
+                AZStd::regex lineRegex("^([^ ]+) filter=lfs diff=lfs merge=lfs -text\\n$");
+                AZStd::cmatch matchResult;
+                if (AZStd::regex_search(line, matchResult, lineRegex) && matchResult.size() == 2)
+                {
+                    // The current line matches the LFS attributes format. Record the LFS pointer file path pattern.
+                    m_lfsPointerFilePatterns.insert(matchResult[1]);
+                }
+            }
+
+            return true;
+        });
+
+        AZ_Error(AssetProcessor::DebugChannel, result.IsSuccess(), result.GetError().c_str());
+    }
+
+    bool LfsPointerFileValidator::IsLfsPointerFile(const AZStd::string& filePath)
+    {
+        return AZ::IO::FileIOBase::GetInstance()->Exists(filePath.c_str()) &&
+            CheckLfsPointerFilePathPattern(filePath) &&
+            CheckLfsPointerFileContent(filePath);
+    }
+
+    AZStd::set<AZStd::string> LfsPointerFileValidator::GetLfsPointerFilePathPatterns()
+    {
+        return m_lfsPointerFilePatterns;
+    }
+
+    bool LfsPointerFileValidator::CheckLfsPointerFilePathPattern(const AZStd::string& filePath)
+    {
+        bool matches = false;
+        for (const AZStd::string& pattern : GetLfsPointerFilePathPatterns())
+        {
+            if (AZStd::wildcard_match(pattern, filePath.c_str()))
+            {
+                // The file path matches one of the known LFS pointer file path patterns.
+                matches = true;
+                break;
+            }
+        }
+
+        return matches;
+    }
+
+    bool LfsPointerFileValidator::CheckLfsPointerFileContent(const AZStd::string& filePath)
+    {
+        // Content rules for LFS pointer files (https://github.com/git-lfs/git-lfs/blob/main/docs/spec.md):
+        // 1. Pointer files are text files which MUST contain only UTF-8 characters.
+        // 2. Each line MUST be of the format{key} {value}\n(trailing unix newline). The required keys are: "version", "oid" and "size".
+        // 3. Only a single space character between {key} and {value}.
+        // 4. Keys MUST only use the characters [a-z] [0-9] . -.
+        // 5. The first key is always version.
+        // 6. Lines of key/value pairs MUST be sorted alphabetically in ascending order (with the exception of version, which is always first).
+        // 7. Values MUST NOT contain return or newline characters.
+        // 8. Pointer files MUST be stored in Git with their executable bit matching that of the replaced file.
+        // 9. Pointer files are unique: that is, there is exactly one valid encoding for a pointer file.
+        AZStd::vector<AZStd::string> fileKeys;
+        bool contentCheckSucceeded = true;
+        auto result = AzFramework::FileFunc::ReadTextFileByLine(filePath,
+            [&fileKeys, &contentCheckSucceeded](const char* line) -> bool
+        {
+            constexpr const char* lfsVersionKey = "version";
+            AZStd::regex lineRegex("^([a-z0-9\\.-]+) ([^\\r\\n]+)\\n$");
+            AZStd::cmatch matchResult;
+            if (!AZStd::regex_search(line, matchResult, lineRegex) ||
+                matchResult.size() <= 2 ||
+                (fileKeys.size() == 0 && matchResult[1] != lfsVersionKey) ||
+                (fileKeys.size() > 1 && matchResult[1] < fileKeys[fileKeys.size() - 1]))
+            {
+                // The current line doesn't match the LFS pointer file content rules above.
+                // Return early in this case since the file is not an LFS content file.
+                contentCheckSucceeded = false;
+                return false;
+            }
+
+            fileKeys.emplace_back(matchResult[1]);
+            return true;
+        });
+        contentCheckSucceeded &= result.IsSuccess();
+
+        if (contentCheckSucceeded)
+        {
+            // Check whether all the required keys exist in the LFS pointer file.
+            const AZStd::vector<AZStd::string> RequiredKeys = { "version", "oid", "size" };
+            size_t requiredKeyIndex = 0, fileKeyIndex = 0;
+            while (requiredKeyIndex < RequiredKeys.size() && fileKeyIndex < fileKeys.size())
+            {
+                if (RequiredKeys[requiredKeyIndex] == fileKeys[fileKeyIndex])
+                {
+                    ++requiredKeyIndex;
+                }
+
+                ++fileKeyIndex;
+            }
+            contentCheckSucceeded &= (requiredKeyIndex == RequiredKeys.size());
+        }
+
+        return contentCheckSucceeded;
+    }
+}

+ 50 - 0
Code/Tools/AssetProcessor/native/AssetManager/Validators/LfsPointerFileValidator.h

@@ -0,0 +1,50 @@
+/*
+* Copyright (c) Contributors to the Open 3D Engine Project.
+* For complete copyright and license terms please see the LICENSE at the root of this distribution.
+*
+* SPDX-License-Identifier: Apache-2.0 OR MIT
+*
+*/
+#pragma once
+
+#include <AzCore/std/containers/set.h>
+#include <AzCore/std/containers/vector.h>
+#include <AzCore/std/string/string.h>
+
+namespace AssetProcessor
+{
+    //! Class for validating LFS pointer files based on the provided .gitattributes files.
+    class LfsPointerFileValidator
+    {
+    public:
+        LfsPointerFileValidator() = default;
+        LfsPointerFileValidator(const AZStd::vector<AZStd::string>& scanDirectories);
+
+        //! Read the .gitattributes file under the specified directory to retrieve the LFS pointer file path patterns.
+        //! @param directory Directory to find the .gitattributes file.
+        void ParseGitAttributesFile(const AZStd::string& directory);
+
+        //! Check whether the specified file is an LFS pointer file.
+        //! @param filePath Path to the file to check.
+        //! @return Return true if the file is an LFS pointer file.
+        bool IsLfsPointerFile(const AZStd::string& filePath);
+
+        //! Retrieve the LFS pointer file path patterns.
+        //! @return List of LFS pointer file path patterns.
+        AZStd::set<AZStd::string> GetLfsPointerFilePathPatterns();
+
+    private:
+        //! Check whether the file content matches the LFS pointer file content rules.
+        //! @param filePath Path to the file to check.
+        //! @return Return true if the file content matches all the LFS pointer file content rules.
+        bool CheckLfsPointerFileContent(const AZStd::string& filePath);
+
+        //! Check whether the specified file path matches any of the known LFS pointer file path patterns.
+        //! @param filePath Path to the file to check.
+        //! @return Return true if the file matches any of the known LFS pointer file path patterns.
+        bool CheckLfsPointerFilePathPattern(const AZStd::string& filePath);
+
+        AZStd::set<AZStd::string> m_lfsPointerFilePatterns; //!< List of the known LFS pointer file path patterns.
+
+    };
+}

+ 117 - 60
Code/Tools/AssetProcessor/native/AssetManager/assetProcessorManager.cpp

@@ -14,6 +14,8 @@
 #include <AssetBuilderSDK/AssetBuilderBusses.h>
 #include <AssetBuilderSDK/AssetBuilderSDK.h>
 
+#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
+
 #include <AzFramework/FileFunc/FileFunc.h>
 
 #include <AzToolsFramework/Debug/TraceContext.h>
@@ -24,6 +26,7 @@
 #include <AzToolsFramework/API/AssetDatabaseBus.h>
 
 #include <native/AssetManager/PathDependencyManager.h>
+#include <native/AssetManager/Validators/LfsPointerFileValidator.h>
 #include <native/utilities/BuilderConfigurationBus.h>
 #include <native/utilities/StatsCapture.h>
 
@@ -636,6 +639,25 @@ namespace AssetProcessor
             return;
         }
 
+        QString absolutePathToFile = jobEntry.GetAbsoluteSourcePath();
+
+        // Set the thread local job ID so that JobLogTraceListener can capture the error and write it to the corresponding job log.
+        // The error message will be available in the Event Log Details table when users click on the failed job in the Asset Proessor GUI.
+        AssetProcessor::SetThreadLocalJobId(jobEntry.m_jobRunKey);
+        AssetUtilities::JobLogTraceListener jobLogTraceListener(jobEntry);
+
+        if (IsLfsPointerFile(absolutePathToFile.toUtf8().constData()))
+        {
+            AZ_Error(AssetProcessor::ConsoleChannel, false,
+                "%s is a git large file storage (LFS) file. "
+                "This is a placeholder file used by the git source control system to manage content. "
+                "This issue usually happens if you've downloaded all of O3DE as a zip file. "
+                "Please sync all of the files from the LFS endpoint following https://www.o3de.org/docs/welcome-guide/setup/setup-from-github/#fork-and-clone.",
+                jobEntry.GetAbsoluteSourcePath().toUtf8().constData());
+        }
+
+        AssetProcessor::SetThreadLocalJobId(0);
+
         // wipe the times so that it will try again next time.
         // note:  Leave the prior successful products where they are, though.
 
@@ -645,7 +667,6 @@ namespace AssetProcessor
         //create/update the source record for this job
         AzToolsFramework::AssetDatabase::SourceDatabaseEntry source;
         AzToolsFramework::AssetDatabase::SourceDatabaseEntryContainer sources;
-        QString absolutePathToFile = jobEntry.GetAbsoluteSourcePath();
         if (m_stateData->GetSourcesBySourceName(jobEntry.m_databaseSourceName, sources))
         {
             AZ_Assert(sources.size() == 1, "Should have only found one source!!!");
@@ -793,6 +814,38 @@ namespace AssetProcessor
         QueueIdleCheck();
     }
 
+    bool AssetProcessorManager::IsLfsPointerFile(const AZStd::string& filePath)
+    {
+        if (!m_lfsPointerFileValidator)
+        {
+            m_lfsPointerFileValidator = AZStd::make_unique<LfsPointerFileValidator>(GetPotentialRepositoryRoots());
+        }
+
+        return m_lfsPointerFileValidator->IsLfsPointerFile(filePath);
+    }
+
+    AZStd::vector<AZStd::string> AssetProcessorManager::GetPotentialRepositoryRoots()
+    {
+        AZStd::vector<AZStd::string> scanDirectories;
+        if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr)
+        {
+            scanDirectories.emplace_back(AZ::Utils::GetEnginePath(settingsRegistry).c_str());
+            scanDirectories.emplace_back(AZ::Utils::GetProjectPath(settingsRegistry).c_str());
+
+            auto RetrieveActiveGemRootDirectories = [&scanDirectories](AZStd::string_view, AZStd::string_view gemPath)
+            {
+                scanDirectories.emplace_back(gemPath.data());
+            };
+            AZ::SettingsRegistryMergeUtils::VisitActiveGems(*settingsRegistry, RetrieveActiveGemRootDirectories);
+        }
+        else
+        {
+            AZ_Error(AssetProcessor::ConsoleChannel, false, "Failed to retrieve the registered setting registry.");
+        }
+
+        return AZStd::move(scanDirectories);
+    }
+
     AssetProcessorManager::ConflictResult AssetProcessorManager::CheckIntermediateProductConflict(
         bool isIntermediateProduct,
         const char* searchSourcePath)
@@ -1955,9 +2008,13 @@ namespace AssetProcessor
         for (SourceFileDependencyEntry& existingEntry : results)
         {
             // this row is [Source] --> [Depends on Source].
-            QString absolutePath = m_platformConfig->FindFirstMatchingFile(QString::fromUtf8(existingEntry.m_source.c_str()));
-            if (!absolutePath.isEmpty())
+
+            SourceInfo sourceInfo;
+
+            if (SearchSourceInfoBySourceUUID(existingEntry.m_sourceGuid, sourceInfo))
             {
+                QString absolutePath = QDir(sourceInfo.m_watchFolder).absoluteFilePath(sourceInfo.m_sourceRelativeToWatchFolder);
+
                 AssessFileInternal(absolutePath, false);
             }
             // also, update it in the database to be missing, ie, add the "missing file" prefix:
@@ -1969,8 +2026,12 @@ namespace AssetProcessor
         // now that the right hand column (in terms of [thing] -> [depends on thing]) has been updated, eliminate anywhere its on the left
         // hand side:
         results.clear();
-        m_stateData->GetDependsOnSourceBySource(databaseSourceFile.toUtf8().constData(), SourceFileDependencyEntry::DEP_Any, results);
-        m_stateData->RemoveSourceFileDependencies(results);
+
+        if (!sources.empty())
+        {
+            m_stateData->GetDependsOnSourceBySource(sources[0].m_sourceGuid, SourceFileDependencyEntry::DEP_Any, results);
+            m_stateData->RemoveSourceFileDependencies(results);
+        }
 
         Q_EMIT SourceDeleted(databaseSourceFile); // note that this removes it from the RC Queue Model, also
     }
@@ -3506,7 +3567,7 @@ namespace AssetProcessor
         // note that for jobs, we only query source dependencies, here, not Source and Job dependencies.
         // this is because we want to take the fingerprint of SOURCE FILES for source dependencies
         // but for jobs we want the fingerprint of the job itself, not that job's source files.
-        QueryAbsolutePathDependenciesRecursive(job.m_jobEntry.m_databaseSourceName, job.m_fingerprintFiles, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceToSource, false);
+        QueryAbsolutePathDependenciesRecursive(job.m_jobEntry.m_sourceFileUUID, job.m_fingerprintFiles, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceToSource);
 
         // Add metadata files for all the fingerprint files
         auto fingerprintFilesCopy = job.m_fingerprintFiles;
@@ -3709,7 +3770,7 @@ namespace AssetProcessor
         {
             // this scope exists only to narrow the range of m_sourceUUIDToSourceNameMapMutex
             AZStd::lock_guard<AZStd::mutex> lock(m_sourceUUIDToSourceInfoMapMutex);
-            m_sourceUUIDToSourceInfoMap.insert(AZStd::make_pair(sourceUUID, newSourceInfo));
+            m_sourceUUIDToSourceInfoMap[sourceUUID] = newSourceInfo; // Don't use insert, there may be an outdated entry from a previously overriden file
         }
 
         // insert the new entry into the analysis tracker:
@@ -4181,7 +4242,7 @@ namespace AssetProcessor
                 for (const auto& thisEntry : resolvedDependencyList)
                 {
                     SourceFileDependencyEntry newDependencyEntry(
-                        builderId, entry.m_sourceFileInfo.m_databasePath.toUtf8().constData(), thisEntry.toUtf8().constData(),
+                        builderId, entry.m_sourceFileInfo.m_uuid, thisEntry.toUtf8().constData(),
                         JobDependencyType,
                         false,
                         subIds.c_str());
@@ -4195,7 +4256,7 @@ namespace AssetProcessor
                     resolvedDatabaseName.toUtf8().constData()); result.second)
                 {
                     SourceFileDependencyEntry newDependencyEntry(
-                        builderId, entry.m_sourceFileInfo.m_databasePath.toUtf8().constData(), resolvedDatabaseName.toUtf8().constData(),
+                        builderId, entry.m_sourceFileInfo.m_uuid, resolvedDatabaseName.toUtf8().constData(),
                         jobDependency.m_jobDependency.m_sourceFile.m_sourceDependencyType ==
                         AssetBuilderSDK::SourceFileDependency::SourceFileDependencyType::Wildcards
                         ? SourceFileDependencyEntry::DEP_SourceLikeMatch
@@ -4251,7 +4312,7 @@ namespace AssetProcessor
                     // add the new dependency:
                     SourceFileDependencyEntry newDependencyEntry(
                         sourceDependency.first,
-                        entry.m_sourceFileInfo.m_databasePath.toUtf8().constData(),
+                        entry.m_sourceFileInfo.m_uuid,
                         thisEntry.toUtf8().constData(),
                         SourceFileDependencyEntry::DEP_SourceToSource,
                         false,
@@ -4285,7 +4346,7 @@ namespace AssetProcessor
             {
                 SourceFileDependencyEntry newDependencyEntry(
                     sourceDependency.first,
-                    entry.m_sourceFileInfo.m_databasePath.toUtf8().constData(),
+                    entry.m_sourceFileInfo.m_uuid,
                     resolvedDatabaseName.toUtf8().constData(),
                     sourceDependency.second.m_sourceDependencyType ==
                     AssetBuilderSDK::SourceFileDependency::SourceFileDependencyType::Wildcards
@@ -4303,7 +4364,7 @@ namespace AssetProcessor
         // them with all of the  new ones for the given source file:
         AZStd::unordered_set<AZ::s64> oldDependencies;
         m_stateData->QueryDependsOnSourceBySourceDependency(
-            entry.m_sourceFileInfo.m_databasePath.toUtf8().constData(), // find all rows in the database where this is the source column
+            entry.m_sourceFileInfo.m_uuid, // find all rows in the database where this is the source column
             nullptr, // no filter
             SourceFileDependencyEntry::DEP_Any, // significant line in this code block
             [&](SourceFileDependencyEntry& existingEntry)
@@ -4336,10 +4397,13 @@ namespace AssetProcessor
             resultEntry.m_dependsOnSource = databaseNameEncoded;
             // we also have to re-queue the source for analysis, if it exists, since it means something it depends on
             // has suddenly appeared on disk:
-            QString absPath = m_platformConfig->FindFirstMatchingFile(QString::fromUtf8(resultEntry.m_source.c_str()));
-            if (!absPath.isEmpty())
+
+            SourceInfo info;
+            if (SearchSourceInfoBySourceUUID(resultEntry.m_sourceGuid, info))
             {
                 // add it to the queue for analysis:
+                QString absPath = QDir(info.m_watchFolder).absoluteFilePath(info.m_sourceRelativeToWatchFolder);
+
                 AssessFileInternal(absPath, false, false);
             }
         }
@@ -4611,12 +4675,14 @@ namespace AssetProcessor
                 }
             }
 
-            QString relativeDatabaseName = QString::fromUtf8(entry.m_source.c_str());
-            QString absolutePath = m_platformConfig->FindFirstMatchingFile(relativeDatabaseName);
-            if (!absolutePath.isEmpty())
+            SourceInfo info;
+            if (SearchSourceInfoBySourceUUID(entry.m_sourceGuid, info))
             {
+                auto absolutePath = QDir(info.m_watchFolder).absoluteFilePath(info.m_sourceRelativeToWatchFolder);
+                // add it to the queue for analysis:
                 absoluteSourceFilePathQueue.insert(absolutePath);
             }
+
             return true;
         };
 
@@ -4634,12 +4700,12 @@ namespace AssetProcessor
 
         if (m_platformConfig->ConvertToRelativePath(sourcePath, databasePath, scanFolder))
         {
-           m_stateData->QuerySourceDependencyByDependsOnSource(databasePath.toUtf8().constData(), nullptr, SourceFileDependencyEntry::DEP_Any, callbackFunction);
+           m_stateData->QuerySourceDependencyByDependsOnSource(databasePath.toUtf8().constData(), SourceFileDependencyEntry::DEP_Any, callbackFunction);
         }
 
         // We'll also check with the absolute path, because we support absolute path dependencies
         m_stateData->QuerySourceDependencyByDependsOnSource(
-            sourcePath.toUtf8().constData(), nullptr, SourceFileDependencyEntry::DEP_Any, callbackFunctionAbsoluteCheck);
+            sourcePath.toUtf8().constData(), SourceFileDependencyEntry::DEP_Any, callbackFunctionAbsoluteCheck);
 
         return absoluteSourceFilePathQueue.values();
     }
@@ -4866,11 +4932,13 @@ namespace AssetProcessor
     {
         AZStd::string concatenatedFingerprints;
 
+        auto sourceUuid = AssetUtilities::CreateSafeSourceUUIDFromName(fileDatabaseName.c_str());
+
         // QSet is not ordered.
         SourceFilesForFingerprintingContainer knownDependenciesAbsolutePaths;
         // this automatically adds the input file to the list:
-        QueryAbsolutePathDependenciesRecursive(QString::fromUtf8(fileDatabaseName.c_str()), knownDependenciesAbsolutePaths,
-            AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any, false);
+        QueryAbsolutePathDependenciesRecursive(sourceUuid, knownDependenciesAbsolutePaths,
+            AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any);
         AddMetadataFilesForFingerprinting(QString::fromUtf8(fileAbsolutePath.c_str()), knownDependenciesAbsolutePaths);
 
         // reserve 17 chars for each since its a 64 bit hex number, and then one more for the dash inbetween each.
@@ -5088,27 +5156,17 @@ namespace AssetProcessor
         }
     }
 
-    void AssetProcessorManager::QueryAbsolutePathDependenciesRecursive(QString inputDatabasePath, SourceFilesForFingerprintingContainer& finalDependencyList, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency dependencyType, bool reverseQuery)
+    void AssetProcessorManager::QueryAbsolutePathDependenciesRecursive(AZ::Uuid sourceUuid, SourceFilesForFingerprintingContainer& finalDependencyList, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency dependencyType)
     {
         // then we add database dependencies.  We have to query this recursively so that we get dependencies of dependencies:
-        QSet<QString> results;
-        QSet<QString> queryQueue;
-        queryQueue.insert(inputDatabasePath);
+        AZStd::unordered_set<AZ::Uuid> results;
+        AZStd::queue<AZ::Uuid> queryQueue;
+        queryQueue.push(sourceUuid);
 
-        while (!queryQueue.isEmpty())
+        while (!queryQueue.empty())
         {
-            QString toSearch = *queryQueue.begin();
-            queryQueue.erase(queryQueue.begin());
-
-            if (toSearch.startsWith(PlaceHolderFileName))
-            {
-                if (!reverseQuery)
-                {
-                    // a placeholder means that it could not be resolved because the file does not exist.
-                    // we still add it to the queue so recursion can happen:
-                    toSearch = toSearch.mid(static_cast<int>(strlen(PlaceHolderFileName)));
-                }
-            }
+            AZ::Uuid toSearch = queryQueue.front();
+            queryQueue.pop();
 
             // if we've already queried it, dont do it again (breaks recursion)
             if (results.contains(toSearch))
@@ -5117,43 +5175,42 @@ namespace AssetProcessor
             }
             results.insert(toSearch);
 
-            auto callbackFunction = [&](AzToolsFramework::AssetDatabase::SourceFileDependencyEntry& entry)
+            auto callbackFunction = [&queryQueue](AzToolsFramework::AssetDatabase::SourceFileDependencyEntry& entry)
             {
-                if (reverseQuery)
+                AZStd::string dependsOnSource = entry.m_dependsOnSource;
+                if (AZ::StringFunc::StartsWith(dependsOnSource, PlaceHolderFileName))
                 {
-                    queryQueue.insert(QString::fromUtf8(entry.m_source.c_str()));
+                    // a placeholder means that it could not be resolved because the file does not exist.
+                    // we still add it to the queue so recursion can happen:
+                    dependsOnSource = dependsOnSource.substr(static_cast<int>(strlen(PlaceHolderFileName)));
                 }
-                else
+
+                auto uuid = AZ::Uuid::CreateStringPermissive(dependsOnSource.c_str());
+
+                if(uuid.IsNull())
                 {
-                    queryQueue.insert(QString::fromUtf8(entry.m_dependsOnSource.c_str()));
+                    uuid = AssetUtilities::CreateSafeSourceUUIDFromName(entry.m_dependsOnSource.c_str());
                 }
+
+                queryQueue.push(uuid);
                 return true;
             };
 
-            if (reverseQuery)
-            {
-                m_stateData->QuerySourceDependencyByDependsOnSource(toSearch.toUtf8().constData(), nullptr, dependencyType, callbackFunction);
-            }
-            else
-            {
-                m_stateData->QueryDependsOnSourceBySourceDependency(toSearch.toUtf8().constData(), nullptr, dependencyType, callbackFunction);
-            }
+            m_stateData->QueryDependsOnSourceBySourceDependency(toSearch, nullptr, dependencyType, callbackFunction);
         }
 
-        for (const QString& dep : results)
+        for (AZ::Uuid dep : results)
         {
-            // note that 'results' contains the database paths (or placeholder ones), we need to find the real absolute ones
-            if (dep.startsWith(PlaceHolderFileName))
-            {
-                continue;
-            }
+            SourceInfo info;
 
-            QString firstMatchingFile = m_platformConfig->FindFirstMatchingFile(dep);
-            if (firstMatchingFile.isEmpty())
+            if (!SearchSourceInfoBySourceUUID(dep, info))
             {
                 continue;
             }
-            finalDependencyList.insert(AZStd::make_pair(firstMatchingFile.toUtf8().constData(), dep.toUtf8().constData()));
+
+            QString absolutePath = QDir(info.m_watchFolder).absoluteFilePath(info.m_sourceRelativeToWatchFolder);
+
+            finalDependencyList.insert(AZStd::make_pair(absolutePath.toUtf8().constData(), dep.ToFixedString().c_str()));
         }
     }
 

+ 9 - 1
Code/Tools/AssetProcessor/native/AssetManager/assetProcessorManager.h

@@ -87,6 +87,7 @@ namespace AssetProcessor
     class PlatformConfiguration;
     class ScanFolderInfo;
     class PathDependencyManager;
+    class LfsPointerFileValidator;
 
     //! The Asset Processor Manager is the heart of the pipeline
     //! It is what makes the critical decisions about what should and should not be processed
@@ -404,6 +405,9 @@ namespace AssetProcessor
         //!  Adds the source to the database and returns the corresponding sourceDatabase Entry
         void AddSourceToDatabase(AzToolsFramework::AssetDatabase::SourceDatabaseEntry& sourceDatabaseEntry, const ScanFolderInfo* scanFolder, QString relativeSourceFilePath);
 
+        // ! Get the engine, project and active gem root directories which could potentially be separate repositories.
+        AZStd::vector<AZStd::string> GetPotentialRepositoryRoots();
+
     protected:
         // given a set of file info that definitely exist, warm the file cache up so
         // that we only query them once.
@@ -459,6 +463,9 @@ namespace AssetProcessor
 
         void UpdateForCacheServer(JobDetails& jobDetails);
 
+        //! Check whether the specified file is an LFS pointer file.
+        bool IsLfsPointerFile(const AZStd::string& filePath);
+
         AssetProcessor::PlatformConfiguration* m_platformConfig = nullptr;
 
         bool m_queuedExamination = false;
@@ -516,6 +523,7 @@ namespace AssetProcessor
 
         AZStd::unique_ptr<PathDependencyManager> m_pathDependencyManager;
         AZStd::unique_ptr<SourceFileRelocator> m_sourceFileRelocator;
+        AZStd::unique_ptr<LfsPointerFileValidator> m_lfsPointerFileValidator;
 
         JobDiagnosticTracker m_jobDiagnosticTracker{};
 
@@ -555,7 +563,7 @@ namespace AssetProcessor
           * if a source file is missing from disk, it will not be included in the result set, since this returns
           * full absolute paths.
           */
-        void QueryAbsolutePathDependenciesRecursive(QString inputDatabasePath, SourceFilesForFingerprintingContainer& finalDependencyList, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency dependencyType, bool reverseQuery);
+        void QueryAbsolutePathDependenciesRecursive(AZ::Uuid sourceUuid, SourceFilesForFingerprintingContainer& finalDependencyList, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency dependencyType);
 
         // we can't write a job to the database as not needing analysis the next time around,
         // until all jobs related to it are finished.  This is becuase the jobs themselves are not written to the database

+ 8 - 5
Code/Tools/AssetProcessor/native/tests/SourceFileRelocatorTests.cpp

@@ -119,8 +119,8 @@ namespace UnitTests
             ASSERT_TRUE(m_data->m_connection->SetSource(sourceFile9));
             ASSERT_TRUE(m_data->m_connection->SetSource(sourceFile10));
 
-            SourceFileDependencyEntry dependency1{ AZ::Uuid::CreateRandom(), "subfolder1/somefile.tif", "subfolder1/otherfile.tif", SourceFileDependencyEntry::TypeOfDependency::DEP_SourceToSource, false, "" };
-            SourceFileDependencyEntry dependency2{ AZ::Uuid::CreateRandom(), "subfolder1/otherfile.tif", "otherfile.tif", SourceFileDependencyEntry::TypeOfDependency::DEP_JobToJob, false, "" };
+            SourceFileDependencyEntry dependency1{ AZ::Uuid::CreateRandom(), m_data->m_dependency1Uuid, "subfolder1/otherfile.tif", SourceFileDependencyEntry::TypeOfDependency::DEP_SourceToSource, false, "" };
+            SourceFileDependencyEntry dependency2{ AZ::Uuid::CreateRandom(), m_data->m_dependency2Uuid, "otherfile.tif", SourceFileDependencyEntry::TypeOfDependency::DEP_JobToJob, false, "" };
             ASSERT_TRUE(m_data->m_connection->SetSourceFileDependency(dependency1));
             ASSERT_TRUE(m_data->m_connection->SetSourceFileDependency(dependency2));
 
@@ -394,6 +394,9 @@ namespace UnitTests
             AZStd::unique_ptr<SourceFileRelocator> m_reporter;
             AZStd::unique_ptr<MockPerforceComponent> m_perforceComponent;
 
+            AZ::Uuid m_dependency1Uuid{ "{2C083160-DD50-459A-9482-CE663F4B558B}" };
+            AZ::Uuid m_dependency2Uuid{ "{013BF607-A52A-4D1A-B2F4-AA8222C1BD68}" };
+
             AZ::JobManager* m_jobManager = nullptr;
             AZ::JobContext* m_jobContext = nullptr;
         };
@@ -638,14 +641,14 @@ namespace UnitTests
 
         m_data->m_reporter->PopulateDependencies(entryContainer);
 
-        AZStd::vector<AZStd::string> databaseSourceNames;
+        AZStd::vector<AZ::Uuid> databaseSourceNames;
         AZStd::vector<AZ::s64> databaseProductDependencyNames;
 
         for(const auto& relocationInfo : entryContainer)
         {
             for (const auto& dependencyEntry : relocationInfo.m_sourceDependencyEntries)
             {
-                databaseSourceNames.push_back(dependencyEntry.m_source);
+                databaseSourceNames.push_back(dependencyEntry.m_sourceGuid);
             }
         }
 
@@ -657,7 +660,7 @@ namespace UnitTests
             }
         }
 
-        ASSERT_THAT(databaseSourceNames, testing::UnorderedElementsAreArray({ "subfolder1/otherfile.tif" }));
+        ASSERT_THAT(databaseSourceNames, testing::UnorderedElementsAreArray({ m_data->m_dependency2Uuid }));
         ASSERT_THAT(databaseProductDependencyNames, testing::UnorderedElementsAreArray({ 2 }));
     }
 

+ 29 - 25
Code/Tools/AssetProcessor/native/tests/assetdatabase/AssetDatabaseTest.cpp

@@ -2041,12 +2041,12 @@ namespace UnitTests
         AZ::Uuid builderGuid = AZ::Uuid::CreateRandom();
 
         // emit 20,000 source dependencies for the same origin file:
-        AZStd::string originFile("myfile.txt");
+        AZ::Uuid originUuid{ "{3C1C9062-7246-443A-A6DF-A001D31B941A}" };
 
         for (AZ::u32 sourceIndex = 0; sourceIndex < 20000; ++sourceIndex)
         {
             AZStd::string dependentFile = AZStd::string::format("otherfile%i.txt", sourceIndex);
-            SourceFileDependencyEntry entry(builderGuid, originFile.c_str(), dependentFile.c_str(), SourceFileDependencyEntry::DEP_SourceToSource, true, "");
+            SourceFileDependencyEntry entry(builderGuid, originUuid, dependentFile.c_str(), SourceFileDependencyEntry::DEP_SourceToSource, true, "");
             resultSourceDependencies.emplace_back(AZStd::move(entry));
         }
 
@@ -2056,7 +2056,7 @@ namespace UnitTests
 
         // read them back
         resultSourceDependencies.clear();
-        EXPECT_TRUE(m_data->m_connection.GetSourceFileDependenciesByBuilderGUIDAndSource(builderGuid, originFile.c_str(), SourceFileDependencyEntry::DEP_SourceToSource, resultSourceDependencies));
+        EXPECT_TRUE(m_data->m_connection.GetSourceFileDependenciesByBuilderGUIDAndSource(builderGuid, originUuid, SourceFileDependencyEntry::DEP_SourceToSource, resultSourceDependencies));
         EXPECT_EQ(resultSourceDependencies.size(), 20000);
     }
 
@@ -2066,16 +2066,19 @@ namespace UnitTests
         AZ::Uuid builderGuid1 = AZ::Uuid::CreateRandom();
         AZ::Uuid builderGuid2 = AZ::Uuid::CreateRandom();
 
+        AZ::Uuid file1Uuid{ "{5AA73EF6-5E14-41F3-B458-4FA19D495696}" };
+        AZ::Uuid file2Uuid{ "{A3FF1BD5-7D6F-4241-8398-1DC6239AD97A}" };
+
         SourceFileDependencyEntryContainer entries;
 
         // add the two different kinds of dependencies.
-        entries.push_back(SourceFileDependencyEntry(builderGuid1, "file1.txt", "file1dependson1.txt", SourceFileDependencyEntry::DEP_SourceToSource, true, ""));
-        entries.push_back(SourceFileDependencyEntry(builderGuid2, "file1.txt", "file1dependson2.txt", SourceFileDependencyEntry::DEP_SourceToSource, true, ""));
-        entries.push_back(SourceFileDependencyEntry(builderGuid1, "file1.txt", "file1dependson1job.txt", SourceFileDependencyEntry::DEP_JobToJob, true, ""));
-        entries.push_back(SourceFileDependencyEntry(builderGuid2, "file1.txt", "file1dependson2job.txt", SourceFileDependencyEntry::DEP_JobToJob, true, ""));
+        entries.push_back(SourceFileDependencyEntry(builderGuid1, file1Uuid, "file1dependson1.txt", SourceFileDependencyEntry::DEP_SourceToSource, true, ""));
+        entries.push_back(SourceFileDependencyEntry(builderGuid2, file1Uuid, "file1dependson2.txt", SourceFileDependencyEntry::DEP_SourceToSource, true, ""));
+        entries.push_back(SourceFileDependencyEntry(builderGuid1, file1Uuid, "file1dependson1job.txt", SourceFileDependencyEntry::DEP_JobToJob, true, ""));
+        entries.push_back(SourceFileDependencyEntry(builderGuid2, file1Uuid, "file1dependson2job.txt", SourceFileDependencyEntry::DEP_JobToJob, true, ""));
 
-        entries.push_back(SourceFileDependencyEntry(builderGuid1, "file2.txt", "file2dependson1.txt", SourceFileDependencyEntry::DEP_SourceToSource, true, ""));
-        entries.push_back(SourceFileDependencyEntry(builderGuid1, "file2.txt", "file2dependson1job.txt", SourceFileDependencyEntry::DEP_JobToJob, true, ""));
+        entries.push_back(SourceFileDependencyEntry(builderGuid1, file2Uuid, "file2dependson1.txt", SourceFileDependencyEntry::DEP_SourceToSource, true, ""));
+        entries.push_back(SourceFileDependencyEntry(builderGuid1, file2Uuid, "file2dependson1job.txt", SourceFileDependencyEntry::DEP_JobToJob, true, ""));
 
         ASSERT_TRUE(m_data->m_connection.SetSourceFileDependencies(entries));
 
@@ -2087,27 +2090,28 @@ namespace UnitTests
             return element.m_dependsOnSource == searchFor;
         };
 
-        auto SearchPredicateReverse = [&searchFor](const SourceFileDependencyEntry& element)
+        AZ::Uuid searchUuid;
+        auto SearchPredicateReverse = [&searchUuid](const SourceFileDependencyEntry& element)
         {
-            return element.m_source == searchFor;
+            return element.m_sourceGuid == searchUuid;
         };
 
         // ask for only the source-to-source dependencies of file1.txt for builder1
-        EXPECT_TRUE(m_data->m_connection.GetSourceFileDependenciesByBuilderGUIDAndSource(builderGuid1, "file1.txt", SourceFileDependencyEntry::DEP_SourceToSource, resultEntries));
+        EXPECT_TRUE(m_data->m_connection.GetSourceFileDependenciesByBuilderGUIDAndSource(builderGuid1, file1Uuid, SourceFileDependencyEntry::DEP_SourceToSource, resultEntries));
         EXPECT_EQ(resultEntries.size(), 1);
         searchFor = "file1dependson1.txt";
         EXPECT_NE(AZStd::find_if(resultEntries.begin(), resultEntries.end(), SearchPredicate), resultEntries.end());
         resultEntries.clear();
 
         // ask for only the source-to-source dependencies of file1.txt for builder2
-        EXPECT_TRUE(m_data->m_connection.GetSourceFileDependenciesByBuilderGUIDAndSource(builderGuid2, "file1.txt", SourceFileDependencyEntry::DEP_SourceToSource, resultEntries));
+        EXPECT_TRUE(m_data->m_connection.GetSourceFileDependenciesByBuilderGUIDAndSource(builderGuid2, file1Uuid, SourceFileDependencyEntry::DEP_SourceToSource, resultEntries));
         EXPECT_EQ(resultEntries.size(), 1);
         searchFor = "file1dependson2.txt";
         EXPECT_NE(AZStd::find_if(resultEntries.begin(), resultEntries.end(), SearchPredicate), resultEntries.end());
         resultEntries.clear();
 
         // ask for the source-to-source dependencies of file1.txt for ANY builder, we shiould get both.
-        EXPECT_TRUE(m_data->m_connection.GetDependsOnSourceBySource("file1.txt", SourceFileDependencyEntry::DEP_SourceToSource, resultEntries));
+        EXPECT_TRUE(m_data->m_connection.GetDependsOnSourceBySource(file1Uuid, SourceFileDependencyEntry::DEP_SourceToSource, resultEntries));
         EXPECT_EQ(resultEntries.size(), 2);
         searchFor = "file1dependson1.txt";
         EXPECT_NE(AZStd::find_if(resultEntries.begin(), resultEntries.end(), SearchPredicate), resultEntries.end());
@@ -2116,21 +2120,21 @@ namespace UnitTests
         resultEntries.clear();
 
         // now ask for the job-to-job dependencies for builder 1
-        EXPECT_TRUE(m_data->m_connection.GetSourceFileDependenciesByBuilderGUIDAndSource(builderGuid1, "file1.txt", SourceFileDependencyEntry::DEP_JobToJob, resultEntries));
+        EXPECT_TRUE(m_data->m_connection.GetSourceFileDependenciesByBuilderGUIDAndSource(builderGuid1, file1Uuid, SourceFileDependencyEntry::DEP_JobToJob, resultEntries));
         EXPECT_EQ(resultEntries.size(), 1);
         searchFor = "file1dependson1job.txt";
         EXPECT_NE(AZStd::find_if(resultEntries.begin(), resultEntries.end(), SearchPredicate), resultEntries.end());
         resultEntries.clear();
 
         // now ask for the job-to-job dependencies for builder 2
-        EXPECT_TRUE(m_data->m_connection.GetSourceFileDependenciesByBuilderGUIDAndSource(builderGuid2, "file1.txt", SourceFileDependencyEntry::DEP_JobToJob, resultEntries));
+        EXPECT_TRUE(m_data->m_connection.GetSourceFileDependenciesByBuilderGUIDAndSource(builderGuid2, file1Uuid, SourceFileDependencyEntry::DEP_JobToJob, resultEntries));
         EXPECT_EQ(resultEntries.size(), 1);
         searchFor = "file1dependson2job.txt";
         EXPECT_NE(AZStd::find_if(resultEntries.begin(), resultEntries.end(), SearchPredicate), resultEntries.end());
         resultEntries.clear();
 
         // now ask for the job-to-job dependencies for any builder
-        EXPECT_TRUE(m_data->m_connection.GetDependsOnSourceBySource( "file1.txt", SourceFileDependencyEntry::DEP_JobToJob, resultEntries));
+        EXPECT_TRUE(m_data->m_connection.GetDependsOnSourceBySource(file1Uuid, SourceFileDependencyEntry::DEP_JobToJob, resultEntries));
         EXPECT_EQ(resultEntries.size(), 2);
         searchFor = "file1dependson1job.txt";
         EXPECT_NE(AZStd::find_if(resultEntries.begin(), resultEntries.end(), SearchPredicate), resultEntries.end());
@@ -2141,7 +2145,7 @@ namespace UnitTests
         // now ask for the reverse dependencies - we should find one source-to-source
         EXPECT_TRUE(m_data->m_connection.GetSourceFileDependenciesByDependsOnSource("file1dependson1.txt", SourceFileDependencyEntry::DEP_SourceToSource, resultEntries));
         EXPECT_EQ(resultEntries.size(), 1);
-        searchFor = "file1.txt";
+        searchUuid = file1Uuid;
         EXPECT_NE(AZStd::find_if(resultEntries.begin(), resultEntries.end(), SearchPredicateReverse), resultEntries.end());
         resultEntries.clear();
 
@@ -2153,12 +2157,12 @@ namespace UnitTests
         // now ask for the reverse dependencies - we should find one 'any' type.
         EXPECT_TRUE(m_data->m_connection.GetSourceFileDependenciesByDependsOnSource("file1dependson1.txt", SourceFileDependencyEntry::DEP_Any, resultEntries));
         EXPECT_EQ(resultEntries.size(), 1);
-        searchFor = "file1.txt";
+        searchUuid = file1Uuid;
         EXPECT_NE(AZStd::find_if(resultEntries.begin(), resultEntries.end(), SearchPredicateReverse), resultEntries.end());
         resultEntries.clear();
 
         // now try the other file - remember the ID for later
-        ASSERT_TRUE(m_data->m_connection.GetSourceFileDependenciesByBuilderGUIDAndSource(builderGuid1, "file2.txt", SourceFileDependencyEntry::DEP_SourceToSource, resultEntries));
+        ASSERT_TRUE(m_data->m_connection.GetSourceFileDependenciesByBuilderGUIDAndSource(builderGuid1, file2Uuid, SourceFileDependencyEntry::DEP_SourceToSource, resultEntries));
         EXPECT_EQ(resultEntries.size(), 1);
         searchFor = "file2dependson1.txt";
         EXPECT_NE(AZStd::find_if(resultEntries.begin(), resultEntries.end(), SearchPredicate), resultEntries.end());
@@ -2166,10 +2170,10 @@ namespace UnitTests
         resultEntries.clear();
 
         // and with Job-to-job dependencies
-        EXPECT_TRUE(m_data->m_connection.GetSourceFileDependenciesByBuilderGUIDAndSource(builderGuid1, "file2.txt", SourceFileDependencyEntry::DEP_JobToJob, resultEntries));
+        EXPECT_TRUE(m_data->m_connection.GetSourceFileDependenciesByBuilderGUIDAndSource(builderGuid1, file2Uuid, SourceFileDependencyEntry::DEP_JobToJob, resultEntries));
         ASSERT_EQ(resultEntries.size(), 1);
         EXPECT_EQ(resultEntries[0].m_builderGuid, builderGuid1);
-        EXPECT_STREQ(resultEntries[0].m_source.c_str(), "file2.txt");
+        EXPECT_EQ(resultEntries[0].m_sourceGuid, file2Uuid);
         EXPECT_NE(resultEntries[0].m_sourceDependencyID, AzToolsFramework::AssetDatabase::InvalidEntryId);
         EXPECT_STREQ(resultEntries[0].m_dependsOnSource.c_str(), "file2dependson1job.txt");
         EXPECT_EQ(resultEntries[0].m_typeOfDependency,  SourceFileDependencyEntry::DEP_JobToJob);
@@ -2180,14 +2184,14 @@ namespace UnitTests
         EXPECT_TRUE(m_data->m_connection.GetSourceFileDependencyBySourceDependencyId(entryIdSource, resultValue));
         EXPECT_EQ(resultValue.m_sourceDependencyID, entryIdSource);
         EXPECT_EQ(resultValue.m_typeOfDependency, SourceFileDependencyEntry::DEP_SourceToSource);
-        EXPECT_STREQ(resultValue.m_source.c_str(), "file2.txt");
+        EXPECT_EQ(resultValue.m_sourceGuid, file2Uuid);
         EXPECT_STREQ(resultValue.m_dependsOnSource.c_str(), "file2dependson1.txt");
         EXPECT_EQ(resultValue.m_builderGuid, builderGuid1);
 
         EXPECT_TRUE(m_data->m_connection.GetSourceFileDependencyBySourceDependencyId(entryIdJob, resultValue));
         EXPECT_EQ(resultValue.m_sourceDependencyID, entryIdJob);
         EXPECT_EQ(resultValue.m_typeOfDependency, SourceFileDependencyEntry::DEP_JobToJob);
-        EXPECT_STREQ(resultValue.m_source.c_str(), "file2.txt");
+        EXPECT_EQ(resultValue.m_sourceGuid, file2Uuid);
         EXPECT_STREQ(resultValue.m_dependsOnSource.c_str(), "file2dependson1job.txt");
         EXPECT_EQ(resultValue.m_builderGuid, builderGuid1);
 
@@ -2604,7 +2608,7 @@ namespace UnitTests
             ASSERT_TRUE(m_data->m_connection.QueryStatsTable(countAllStats));
             ASSERT_EQ(entryCount, StatCountPerPrefix * prefixes.size());
         }
-        
+
         //! Query StatName like prefixes
         for (const auto& prefix : prefixes)
         {

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

@@ -76,6 +76,7 @@ void AssetProcessorManagerTest::SetUp()
     using namespace testing;
     using ::testing::NiceMock;
     using namespace AssetProcessor;
+    using namespace AzToolsFramework::AssetDatabase;
 
     AssetProcessorTest::SetUp();
 
@@ -145,7 +146,7 @@ void AssetProcessorManagerTest::SetUp()
     m_config->EnablePlatform({ "pc", { "host", "renderer", "desktop" } }, true);
 
     m_config->AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder1"), "subfolder1", "subfolder1", false, true, m_config->GetEnabledPlatforms(), 1));
-    m_config->AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder2/redirected"), "subfolder2", "subfolder2", false, true, m_config->GetEnabledPlatforms()));
+    m_config->AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder2"), "subfolder2", "subfolder2", false, true, m_config->GetEnabledPlatforms()));
     m_config->AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder3"), "subfolder3", "subfolder3", false, true, m_config->GetEnabledPlatforms(), 1));
     m_config->AddScanFolder(ScanFolderInfo(tempPath.filePath("subfolder4"), "subfolder4", "subfolder4", false, true, m_config->GetEnabledPlatforms(), 1));
     m_config->AddMetaDataType("assetinfo", "");
@@ -169,6 +170,8 @@ void AssetProcessorManagerTest::SetUp()
     {
         m_isIdling = newState;
     });
+
+    PopulateDatabase();
 }
 
 void AssetProcessorManagerTest::TearDown()
@@ -198,6 +201,41 @@ void AssetProcessorManagerTest::TearDown()
     AssetProcessor::AssetProcessorTest::TearDown();
 }
 
+void AssetProcessorManagerTest::CreateSourceAndFile(const char* tempFolderRelativePath)
+{
+    QDir tempPath(m_tempDir.path());
+
+    auto absolutePath = tempPath.absoluteFilePath(tempFolderRelativePath);
+
+    auto scanFolder = m_config->GetScanFolderForFile(absolutePath);
+
+    QString relPath;
+    m_config->ConvertToRelativePath(absolutePath, scanFolder, relPath);
+
+    auto uuid = AssetUtilities::CreateSafeSourceUUIDFromName(relPath.toUtf8().constData());
+
+    AzToolsFramework::AssetDatabase::SourceDatabaseEntry source(scanFolder->ScanFolderID(), relPath.toUtf8().constData(), uuid, "fingerprint");
+    ASSERT_TRUE(m_assetProcessorManager->m_stateData->SetSource(source));
+
+    ASSERT_TRUE(UnitTestUtils::CreateDummyFile(absolutePath));
+}
+
+void AssetProcessorManagerTest::PopulateDatabase()
+{
+    using namespace AzToolsFramework::AssetDatabase;
+
+    QDir tempPath(m_tempDir.path());
+
+    AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry scanFolder(
+        tempPath.absoluteFilePath("subfolder1").toUtf8().constData(), "temp path", "temp path");
+    ASSERT_TRUE(m_assetProcessorManager->m_stateData->SetScanFolder(scanFolder));
+
+    CreateSourceAndFile("subfolder1/a.txt");
+    CreateSourceAndFile("subfolder1/b.txt");
+    CreateSourceAndFile("subfolder1/c.txt");
+    CreateSourceAndFile("subfolder1/d.txt");
+}
+
 TEST_F(AssetProcessorManagerTest, UnitTestForGettingJobInfoBySourceUUIDSuccess)
 {
     // Here we first mark a job for an asset complete and than fetch jobs info using the job log api to verify
@@ -758,7 +796,7 @@ TEST_F(BuilderDirtiness, BuilderDirtiness_NewAnalysisFingerprint_IsNotANewBuilde
 
 TEST_F(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_BasicTest)
 {
-    using SourceFileDependencyEntry = AzToolsFramework::AssetDatabase::SourceFileDependencyEntry;
+    using namespace AzToolsFramework::AssetDatabase;
 
     //  A depends on B, which depends on both C and D
 
@@ -772,19 +810,19 @@ TEST_F(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_BasicTe
     SourceFileDependencyEntry newEntry1;  // a depends on B
     newEntry1.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
     newEntry1.m_builderGuid = AZ::Uuid::CreateRandom();
-    newEntry1.m_source = "a.txt";
+    newEntry1.m_sourceGuid = m_aUuid;
     newEntry1.m_dependsOnSource = "b.txt";
 
     SourceFileDependencyEntry newEntry2; // b depends on C
     newEntry2.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
     newEntry2.m_builderGuid = AZ::Uuid::CreateRandom();
-    newEntry2.m_source = "b.txt";
+    newEntry2.m_sourceGuid = m_bUuid;
     newEntry2.m_dependsOnSource = "c.txt";
 
     SourceFileDependencyEntry newEntry3;  // b also depends on D
     newEntry3.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
     newEntry3.m_builderGuid = AZ::Uuid::CreateRandom();
-    newEntry3.m_source = "b.txt";
+    newEntry3.m_sourceGuid = m_bUuid;
     newEntry3.m_dependsOnSource = "d.txt";
 
     ASSERT_TRUE(m_assetProcessorManager->m_stateData->SetSourceFileDependency(newEntry1));
@@ -792,7 +830,7 @@ TEST_F(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_BasicTe
     ASSERT_TRUE(m_assetProcessorManager->m_stateData->SetSourceFileDependency(newEntry3));
 
     AssetProcessor::SourceFilesForFingerprintingContainer dependencies;
-    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive("a.txt", dependencies, SourceFileDependencyEntry::DEP_SourceToSource, false );
+    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive(m_aUuid, dependencies, SourceFileDependencyEntry::DEP_SourceToSource );
     EXPECT_EQ(dependencies.size(), 4); // a depends on b, c, and d - with the latter two being indirect.
 
     EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/a.txt").toUtf8().constData()), dependencies.end());
@@ -800,14 +838,14 @@ TEST_F(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_BasicTe
     EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/c.txt").toUtf8().constData()), dependencies.end());
     EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/d.txt").toUtf8().constData()), dependencies.end());
 
-    // make sure the corresponding values in the map are also correct (ie, database path only)
-    EXPECT_STREQ(dependencies[tempPath.absoluteFilePath("subfolder1/a.txt").toUtf8().constData()].c_str(), "a.txt");
-    EXPECT_STREQ(dependencies[tempPath.absoluteFilePath("subfolder1/b.txt").toUtf8().constData()].c_str(), "b.txt");
-    EXPECT_STREQ(dependencies[tempPath.absoluteFilePath("subfolder1/c.txt").toUtf8().constData()].c_str(), "c.txt");
-    EXPECT_STREQ(dependencies[tempPath.absoluteFilePath("subfolder1/d.txt").toUtf8().constData()].c_str(), "d.txt");
+    // make sure the corresponding values in the map are also correct
+    EXPECT_STREQ(dependencies[tempPath.absoluteFilePath("subfolder1/a.txt").toUtf8().constData()].c_str(), m_aUuid.ToFixedString().c_str());
+    EXPECT_STREQ(dependencies[tempPath.absoluteFilePath("subfolder1/b.txt").toUtf8().constData()].c_str(), m_bUuid.ToFixedString().c_str());
+    EXPECT_STREQ(dependencies[tempPath.absoluteFilePath("subfolder1/c.txt").toUtf8().constData()].c_str(), m_cUuid.ToFixedString().c_str());
+    EXPECT_STREQ(dependencies[tempPath.absoluteFilePath("subfolder1/d.txt").toUtf8().constData()].c_str(), m_dUuid.ToFixedString().c_str());
 
     dependencies.clear();
-    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive("b.txt", dependencies, SourceFileDependencyEntry::DEP_SourceToSource, false);
+    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive(m_bUuid, dependencies, SourceFileDependencyEntry::DEP_SourceToSource);
     EXPECT_EQ(dependencies.size(), 3); // b  depends on c, and d
     EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/b.txt").toUtf8().constData()), dependencies.end());
     EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/c.txt").toUtf8().constData()), dependencies.end());
@@ -817,7 +855,7 @@ TEST_F(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_BasicTe
     ASSERT_TRUE(m_assetProcessorManager->m_stateData->RemoveSourceFileDependency(newEntry2.m_sourceDependencyID));
 
     dependencies.clear();
-    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive("a.txt", dependencies, SourceFileDependencyEntry::DEP_SourceToSource, false);
+    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive(m_aUuid, dependencies, SourceFileDependencyEntry::DEP_SourceToSource);
     EXPECT_EQ(dependencies.size(), 3); // a depends on b and d, but no longer c
     EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/a.txt").toUtf8().constData()), dependencies.end());
     EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/b.txt").toUtf8().constData()), dependencies.end());
@@ -839,21 +877,21 @@ TEST_F(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_WithDif
     SourceFileDependencyEntry newEntry1;  // a depends on B as a SOURCE dependency.
     newEntry1.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
     newEntry1.m_builderGuid = AZ::Uuid::CreateRandom();
-    newEntry1.m_source = "a.txt";
+    newEntry1.m_sourceGuid = m_aUuid;
     newEntry1.m_dependsOnSource = "b.txt";
     newEntry1.m_typeOfDependency = SourceFileDependencyEntry::DEP_SourceToSource;
 
     SourceFileDependencyEntry newEntry2; // b depends on C as a JOB dependency
     newEntry2.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
     newEntry2.m_builderGuid = AZ::Uuid::CreateRandom();
-    newEntry2.m_source = "b.txt";
+    newEntry2.m_sourceGuid = m_bUuid;
     newEntry2.m_dependsOnSource = "c.txt";
     newEntry2.m_typeOfDependency = SourceFileDependencyEntry::DEP_JobToJob;
 
     SourceFileDependencyEntry newEntry3;  // b also depends on D as a SOURCE dependency
     newEntry3.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
     newEntry3.m_builderGuid = AZ::Uuid::CreateRandom();
-    newEntry3.m_source = "b.txt";
+    newEntry3.m_sourceGuid = m_bUuid;
     newEntry3.m_dependsOnSource = "d.txt";
     newEntry3.m_typeOfDependency = SourceFileDependencyEntry::DEP_SourceToSource;
 
@@ -862,7 +900,7 @@ TEST_F(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_WithDif
     ASSERT_TRUE(m_assetProcessorManager->m_stateData->SetSourceFileDependency(newEntry3));
 
     AssetProcessor::SourceFilesForFingerprintingContainer dependencies;
-    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive("a.txt", dependencies, SourceFileDependencyEntry::DEP_SourceToSource, false);
+    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive(m_aUuid, dependencies, SourceFileDependencyEntry::DEP_SourceToSource);
     // note that a depends on b, c, and d - with the latter two being indirect.
     // however, since b's dependency on C is via JOB, and we're asking for SOURCE only, we should not see C.
     EXPECT_EQ(dependencies.size(), 3);
@@ -872,7 +910,7 @@ TEST_F(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_WithDif
     EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/d.txt").toUtf8().constData()), dependencies.end());
 
     dependencies.clear();
-    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive("b.txt", dependencies, SourceFileDependencyEntry::DEP_JobToJob, false);
+    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive(m_bUuid, dependencies, SourceFileDependencyEntry::DEP_JobToJob);
     // b  depends on c, and d  - but we're asking for job dependencies only, so we should not get anything except C and B
     EXPECT_EQ(dependencies.size(), 2);
     EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/b.txt").toUtf8().constData()), dependencies.end());
@@ -880,7 +918,7 @@ TEST_F(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_WithDif
 
     // now ask for ALL kinds and you should get the full tree.
     dependencies.clear();
-    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive("a.txt", dependencies, SourceFileDependencyEntry::DEP_Any, false);
+    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive(m_aUuid, dependencies, SourceFileDependencyEntry::DEP_Any);
     EXPECT_EQ(dependencies.size(), 4);
     EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/a.txt").toUtf8().constData()), dependencies.end());
     EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/b.txt").toUtf8().constData()), dependencies.end());
@@ -888,77 +926,6 @@ TEST_F(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_WithDif
     EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/d.txt").toUtf8().constData()), dependencies.end());
 }
 
-// ------------------------------------------------------------------------------------------------
-//                         QueryAbsolutePathDependenciesRecursive REVERSE section
-// ------------------------------------------------------------------------------------------------
-
-TEST_F(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_Reverse_BasicTest)
-{
-    using SourceFileDependencyEntry = AzToolsFramework::AssetDatabase::SourceFileDependencyEntry;
-
-    //  A depends on B, which depends on both C and D
-
-    QDir tempPath(m_tempDir.path());
-
-    UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("subfolder1/a.txt"), QString("tempdata\n"));
-    UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("subfolder1/b.txt"), QString("tempdata\n"));
-    UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("subfolder1/c.txt"), QString("tempdata\n"));
-    UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("subfolder1/d.txt"), QString("tempdata\n"));
-
-    SourceFileDependencyEntry newEntry1;  // a depends on B
-    newEntry1.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
-    newEntry1.m_builderGuid = AZ::Uuid::CreateRandom();
-    newEntry1.m_source = "a.txt";
-    newEntry1.m_dependsOnSource = "b.txt";
-
-    SourceFileDependencyEntry newEntry2; // b depends on C
-    newEntry2.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
-    newEntry2.m_builderGuid = AZ::Uuid::CreateRandom();
-    newEntry2.m_source = "b.txt";
-    newEntry2.m_dependsOnSource = "c.txt";
-
-    SourceFileDependencyEntry newEntry3;  // b also depends on D
-    newEntry3.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
-    newEntry3.m_builderGuid = AZ::Uuid::CreateRandom();
-    newEntry3.m_source = "b.txt";
-    newEntry3.m_dependsOnSource = "d.txt";
-
-    ASSERT_TRUE(m_assetProcessorManager->m_stateData->SetSourceFileDependency(newEntry1));
-    ASSERT_TRUE(m_assetProcessorManager->m_stateData->SetSourceFileDependency(newEntry2));
-    ASSERT_TRUE(m_assetProcessorManager->m_stateData->SetSourceFileDependency(newEntry3));
-
-    AssetProcessor::SourceFilesForFingerprintingContainer dependencies;
-    // sanity: what Depends on a?  the only result should be a itself.
-    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive("a.txt", dependencies, SourceFileDependencyEntry::DEP_SourceToSource, true /*reverse*/);
-    EXPECT_EQ(dependencies.size(), 1);
-    EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/a.txt").toUtf8().constData()), dependencies.end());
-
-    dependencies.clear();
-    // what depends on d?  b and a should (indirectly)
-    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive("d.txt", dependencies, SourceFileDependencyEntry::DEP_SourceToSource, true);
-    EXPECT_EQ(dependencies.size(), 3);
-    EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/a.txt").toUtf8().constData()), dependencies.end());
-    EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/b.txt").toUtf8().constData()), dependencies.end());
-    EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/d.txt").toUtf8().constData()), dependencies.end());
-
-    // what depends on c?  b and a should.
-    dependencies.clear();
-    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive("c.txt", dependencies, SourceFileDependencyEntry::DEP_SourceToSource, true);
-    EXPECT_EQ(dependencies.size(), 3); // b  depends on c, and d
-    EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/c.txt").toUtf8().constData()), dependencies.end());
-    EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/b.txt").toUtf8().constData()), dependencies.end());
-    EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/a.txt").toUtf8().constData()), dependencies.end());
-
-    // eliminate b --> c
-    ASSERT_TRUE(m_assetProcessorManager->m_stateData->RemoveSourceFileDependency(newEntry2.m_sourceDependencyID));
-
-    // what depends on c?  nothing.
-    dependencies.clear();
-    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive("c.txt", dependencies, SourceFileDependencyEntry::DEP_SourceToSource, true);
-    EXPECT_EQ(dependencies.size(), 1); // a depends on b and d, but no longer c
-    EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/c.txt").toUtf8().constData()), dependencies.end());
-}
-
 // since we need these files to still produce a 0-based fingerprint, we need them to
 // still do a best guess at absolute path, when they are missing.
 TEST_F(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_MissingFiles_ReturnsNoPathWithPlaceholders)
@@ -973,22 +940,29 @@ TEST_F(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_Missing
     UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("subfolder1/d.txt"), QString("tempdata\n"));
     // note that we don't actually create b and c here, they are missing.
 
+    AzToolsFramework::AssetDatabase::SourceDatabaseEntry entry;
+    m_assetProcessorManager->m_stateData->GetSourceBySourceGuid(m_bUuid, entry);
+    m_assetProcessorManager->m_stateData->RemoveSource(entry.m_sourceID);
+
+    m_assetProcessorManager->m_stateData->GetSourceBySourceGuid(m_cUuid, entry);
+    m_assetProcessorManager->m_stateData->RemoveSource(entry.m_sourceID);
+
     SourceFileDependencyEntry newEntry1;  // a depends on B
     newEntry1.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
     newEntry1.m_builderGuid = AZ::Uuid::CreateRandom();
-    newEntry1.m_source = "a.txt";
+    newEntry1.m_sourceGuid = m_aUuid;
     newEntry1.m_dependsOnSource = "b.txt";
 
     SourceFileDependencyEntry newEntry2; // b depends on C
     newEntry2.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
     newEntry2.m_builderGuid = AZ::Uuid::CreateRandom();
-    newEntry2.m_source = "b.txt";
+    newEntry2.m_sourceGuid = m_bUuid;
     newEntry2.m_dependsOnSource = "c.txt";
 
     SourceFileDependencyEntry newEntry3;  // b also depends on D
     newEntry3.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
     newEntry3.m_builderGuid = AZ::Uuid::CreateRandom();
-    newEntry3.m_source = "b.txt";
+    newEntry3.m_sourceGuid = m_bUuid;
     newEntry3.m_dependsOnSource = "d.txt";
 
     ASSERT_TRUE(m_assetProcessorManager->m_stateData->SetSourceFileDependency(newEntry1));
@@ -996,22 +970,22 @@ TEST_F(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_Missing
     ASSERT_TRUE(m_assetProcessorManager->m_stateData->SetSourceFileDependency(newEntry3));
 
     AssetProcessor::SourceFilesForFingerprintingContainer dependencies;
-    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive("a.txt", dependencies, SourceFileDependencyEntry::DEP_SourceToSource, false);
-    EXPECT_EQ(dependencies.size(), 2); // a depends on b, c, and d - with the latter two being indirect.
+    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive(m_aUuid, dependencies, SourceFileDependencyEntry::DEP_SourceToSource);
+    EXPECT_EQ(dependencies.size(), 2); // b and c don't exist, so only expect a and d
 
     EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/a.txt").toUtf8().constData()), dependencies.end());
     EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/d.txt").toUtf8().constData()), dependencies.end());
 
     dependencies.clear();
-    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive("b.txt", dependencies, SourceFileDependencyEntry::DEP_SourceToSource, false);
-    EXPECT_EQ(dependencies.size(), 1); // b  depends on c, and d
+    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive(m_bUuid, dependencies, SourceFileDependencyEntry::DEP_SourceToSource);
+    EXPECT_EQ(dependencies.size(), 1); // c doesn't exist, so only expect d
     EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/d.txt").toUtf8().constData()), dependencies.end());
 
     // eliminate b --> c
     ASSERT_TRUE(m_assetProcessorManager->m_stateData->RemoveSourceFileDependency(newEntry2.m_sourceDependencyID));
 
     dependencies.clear();
-    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive("a.txt", dependencies, SourceFileDependencyEntry::DEP_SourceToSource, false);
+    m_assetProcessorManager->QueryAbsolutePathDependenciesRecursive(m_aUuid, dependencies, SourceFileDependencyEntry::DEP_SourceToSource);
     EXPECT_EQ(dependencies.size(), 2); // a depends on b and d, but no longer c
     EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/a.txt").toUtf8().constData()), dependencies.end());
     EXPECT_NE(dependencies.find(tempPath.absoluteFilePath("subfolder1/d.txt").toUtf8().constData()), dependencies.end());
@@ -1052,7 +1026,7 @@ TEST_F(AssetProcessorManagerTest, BuilderSDK_API_CreateJobs_HasValidParameters_W
 {
     QDir tempPath(m_tempDir.path());
     // here we push a file change through APM and make sure that "CreateJobs" has correct parameters, with no output redirection
-    QString absPath(tempPath.absoluteFilePath("subfolder2/redirected/test_text.txt"));
+    QString absPath(tempPath.absoluteFilePath("subfolder2/test_text.txt"));
     UnitTestUtils::CreateDummyFile(absPath);
 
     m_mockApplicationManager->ResetMockBuilderCreateJobCalls();
@@ -1073,7 +1047,7 @@ TEST_F(AssetProcessorManagerTest, BuilderSDK_API_CreateJobs_HasValidParameters_W
     // subfolder2 has its output redirected in the cache
     // this test makes sure that the CreateJobs API is completely unaffected by that and none of the internal database stuff
     // is reflected by the API.
-    EXPECT_STREQ(req.m_watchFolder.c_str(), tempPath.absoluteFilePath("subfolder2/redirected").toUtf8().constData());
+    EXPECT_STREQ(req.m_watchFolder.c_str(), tempPath.absoluteFilePath("subfolder2").toUtf8().constData());
     EXPECT_STREQ(req.m_sourceFile.c_str(), "test_text.txt"); // only the name should be there, no output prefix.
 
     EXPECT_NE(req.m_sourceFileUUID, AZ::Uuid::CreateNull());
@@ -2417,91 +2391,6 @@ TEST_F(PathDependencyTest, MixedPathDependencies_Deferred_ResolveCorrectly)
     );
 }
 
-// This test ensures product path *product* file dependencies are matched by exact path
-// Dep1 is output as test.asset#, Dep2 is output as redirected/test.asset#
-// Dependencies on test.asset# should point to dep1 and never dep2
-TEST_F(PathDependencyTest, AssetProcessed_Impl_DeferredPathResolution_CorrectlyMatchesWithScanFolderPrefix)
-{
-    using namespace AssetProcessor;
-    using namespace AssetBuilderSDK;
-
-    // -------- Make main test asset, with dependencies on products that don't exist yet -----
-    TestAsset primaryFile("test_text");
-    ASSERT_TRUE(ProcessAsset(primaryFile, { { ".asset" }, {} }, { {"test.asset1", ProductPathDependencyType::ProductFile}, {"test.asset2", ProductPathDependencyType::ProductFile} }));
-
-    // create dependees
-    TestAsset dep1("test");
-    TestAsset dep2("test");
-
-    ASSERT_TRUE(ProcessAsset(dep1, { {".asset1"}, {".asset2"} }, {}, "subfolder1/"));
-    ASSERT_TRUE(ProcessAsset(dep2, { {".asset1"}, {".asset2"} }, {}, "subfolder2/redirected/"));
-
-    // ---------- Verify that the dependency was recorded, and did not keep the path after resolution ----------
-    AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer dependencyContainer;
-    ASSERT_TRUE(m_sharedConnection->GetProductDependencies(dependencyContainer));
-
-    VerifyDependencies(dependencyContainer, { dep1.m_products[0], dep2.m_products[1] });
-}
-
-// This test ensures product path *source* file dependencies are matched by exact path
-TEST_F(PathDependencyTest, SourceFileDependencyWithPrefix_Deferred_ResolvesCorrectly)
-{
-    using namespace AssetProcessor;
-    using namespace AssetBuilderSDK;
-
-    // -------- Make main test asset, with dependencies on products that don't exist yet -----
-    TestAsset primaryFile("test_text");
-
-    ASSERT_TRUE(ProcessAsset(primaryFile, { { ".asset" }, {} }, { {"test.txt", ProductPathDependencyType::SourceFile} }));
-
-    // create dependees
-    TestAsset dep1("test");
-    TestAsset dep2("test");
-
-    ASSERT_TRUE(ProcessAsset(dep1, { {".asset1"}, {".asset2"} }, {}, "subfolder1/"));
-    ASSERT_TRUE(ProcessAsset(dep2, { {".asset1"}, {".asset2"} }, {}, "subfolder2/redirected/"));
-
-    // ---------- Verify that the dependency was recorded, and did not keep the path after resolution ----------
-    AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer dependencyContainer;
-    ASSERT_TRUE(m_sharedConnection->GetProductDependencies(dependencyContainer));
-
-    VerifyDependencies(dependencyContainer,
-        {
-            dep2.m_products[0],
-            dep2.m_products[1],
-        }
-    );
-}
-
-TEST_F(PathDependencyTest, SourceFileDependencyWithPrefix_Existing_ResolvesCorrectly)
-{
-    using namespace AssetProcessor;
-    using namespace AssetBuilderSDK;
-
-    // create dependees
-    TestAsset dep1("test");
-    TestAsset dep2("test");
-
-    ASSERT_TRUE(ProcessAsset(dep1, { {".asset1"}, {".asset2"} }, {}, "subfolder1/"));
-    ASSERT_TRUE(ProcessAsset(dep2, { {".asset1"}, {".asset2"} }, {}, "subfolder2/redirected/"));
-
-    // -------- Make main test asset, with dependencies on products we just created -----
-    TestAsset primaryFile("test_text");
-
-    ASSERT_TRUE(ProcessAsset(primaryFile, { { ".asset" }, {} }, { {"test.txt", ProductPathDependencyType::SourceFile} }));
-
-    // ---------- Verify that the dependency was recorded, and did not keep the path after resolution ----------
-    AzToolsFramework::AssetDatabase::ProductDependencyDatabaseEntryContainer dependencyContainer;
-    ASSERT_TRUE(m_sharedConnection->GetProductDependencies(dependencyContainer));
-
-    VerifyDependencies(dependencyContainer,
-        {
-            dep2.m_products[0],
-            dep2.m_products[1],
-        }
-    );
-}
-
 void MultiplatformPathDependencyTest::SetUp()
 {
     AssetProcessorManagerTest::SetUp();
@@ -2977,14 +2866,14 @@ void SourceFileDependenciesTest::SetupData(
 
     if (createFile1Dummies)
     {
-        ASSERT_TRUE(UnitTestUtils::CreateDummyFile(m_dependsOnFile1_Source, QString("tempdata\n")));
-        ASSERT_TRUE(UnitTestUtils::CreateDummyFile(m_dependsOnFile1_Job, QString("tempdata\n")));
+        CreateSourceAndFile("subfolder1/a.txt");
+        CreateSourceAndFile("subfolder1/c.txt");
     }
 
     if (createFile2Dummies)
     {
-        ASSERT_TRUE(UnitTestUtils::CreateDummyFile(m_dependsOnFile2_Source, QString("tempdata\n")));
-        ASSERT_TRUE(UnitTestUtils::CreateDummyFile(m_dependsOnFile2_Job, QString("tempdata\n")));
+        CreateSourceAndFile("subfolder1/b.txt");
+        CreateSourceAndFile("subfolder1/d.txt");
     }
 
     // construct the dummy job to feed to the database updater function:
@@ -2996,6 +2885,7 @@ void SourceFileDependenciesTest::SetupData(
     if (primeMap)
     {
         // note that we have to "prime" the map with the UUIDs to the source info for this to work:
+        m_assetProcessorManager->m_sourceUUIDToSourceInfoMap[job.m_sourceFileInfo.m_uuid] = { m_watchFolderPath, job.m_sourceFileInfo.m_databasePath, job.m_sourceFileInfo.m_databasePath };
         m_assetProcessorManager->m_sourceUUIDToSourceInfoMap[m_uuidOfA] = { m_watchFolderPath, "a.txt", "a.txt" };
         m_assetProcessorManager->m_sourceUUIDToSourceInfoMap[m_uuidOfB] = { m_watchFolderPath, "b.txt", "b.txt" };
         m_assetProcessorManager->m_sourceUUIDToSourceInfoMap[m_uuidOfC] = { m_watchFolderPath, "c.txt", "c.txt" };
@@ -3023,6 +2913,19 @@ void SourceFileDependenciesTest::SetupData(
     m_assetProcessorManager->UpdateSourceFileDependenciesDatabase(job);
 }
 
+void SourceFileDependenciesTest::PopulateDatabase()
+{
+    using namespace AzToolsFramework::AssetDatabase;
+
+    QDir tempPath(m_tempDir.path());
+
+    AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry scanFolder(
+        tempPath.absoluteFilePath("subfolder1").toUtf8().constData(), "temp path", "temp path");
+    ASSERT_TRUE(m_assetProcessorManager->m_stateData->SetScanFolder(scanFolder));
+
+    CreateSourceAndFile("subFolder1/assetProcessorManagerTest.txt");
+}
+
 AssetBuilderSDK::SourceFileDependency SourceFileDependenciesTest::MakeSourceDependency(const char* file, bool wildcard)
 {
     return { file, AZ::Uuid::CreateNull(),
@@ -3049,7 +2952,7 @@ auto SourceFileDependenciesTest::GetDependencyList()
 {
     AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer deps;
     this->m_assetProcessorManager->m_stateData->GetSourceFileDependenciesByBuilderGUIDAndSource(
-        m_dummyBuilderUuid, "assetProcessorManagerTest.txt",
+        m_dummyBuilderUuid, m_sourceFileUuid,
         AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::TypeOfDependency::DEP_Any, deps);
 
     AZStd::vector<AZStd::string> list;
@@ -3070,7 +2973,7 @@ TEST_F(SourceFileDependenciesTest, UpdateSourceFileDependenciesDatabase_BasicTes
     // the rest of this test now performs a series of queries to verify the database was correctly set.
     // this indirectly verifies the QueryAbsolutePathDependenciesRecursive function also but it has its own dedicated tests, above.
     AssetProcessor::SourceFilesForFingerprintingContainer deps;
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceToSource, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceToSource);
     // the above function includes the actual source, as an absolute path.
     EXPECT_EQ(deps.size(), 3);
     EXPECT_NE(deps.find(m_absPath.toUtf8().constData()), deps.end());
@@ -3078,7 +2981,7 @@ TEST_F(SourceFileDependenciesTest, UpdateSourceFileDependenciesDatabase_BasicTes
     EXPECT_NE(deps.find(m_dependsOnFile2_Source.toUtf8().constData()), deps.end());
 
     deps.clear();
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_JobToJob, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_JobToJob);
     // the above function includes the actual source, as an absolute path.
     EXPECT_EQ(deps.size(), 3);
     EXPECT_NE(deps.find(m_absPath.toUtf8().constData()), deps.end());
@@ -3086,7 +2989,7 @@ TEST_F(SourceFileDependenciesTest, UpdateSourceFileDependenciesDatabase_BasicTes
     EXPECT_NE(deps.find(m_dependsOnFile2_Job.toUtf8().constData()), deps.end());
 
     deps.clear();
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any);
     // the above function includes the actual source, as an absolute path.
     EXPECT_EQ(deps.size(), 5);
     EXPECT_NE(deps.find(m_absPath.toUtf8().constData()), deps.end());
@@ -3111,21 +3014,21 @@ TEST_F(SourceFileDependenciesTest, UpdateSourceFileDependenciesDatabase_UpdateTe
 
     // now make sure that the same queries omit b and d:
     AssetProcessor::SourceFilesForFingerprintingContainer deps;
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceToSource, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceToSource);
     // the above function includes the actual source, as an absolute path.
     EXPECT_EQ(deps.size(), 2);
     EXPECT_NE(deps.find(m_absPath.toUtf8().constData()), deps.end());
     EXPECT_NE(deps.find(m_dependsOnFile1_Source.toUtf8().constData()), deps.end());
 
     deps.clear();
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_JobToJob, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_JobToJob);
     // the above function includes the actual source, as an absolute path.
     EXPECT_EQ(deps.size(), 2);
     EXPECT_NE(deps.find(m_absPath.toUtf8().constData()), deps.end());
     EXPECT_NE(deps.find(m_dependsOnFile1_Job.toUtf8().constData()), deps.end());
 
     deps.clear();
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any);
     // the above function includes the actual source, as an absolute path.
     EXPECT_EQ(deps.size(), 3);
     EXPECT_NE(deps.find(m_absPath.toUtf8().constData()), deps.end());
@@ -3143,7 +3046,7 @@ TEST_F(SourceFileDependenciesTest, UpdateSourceFileDependenciesDatabase_MissingF
     // the rest of this test now performs a series of queries to verify the database was correctly set.
     // this indirectly verifies the QueryAbsolutePathDependenciesRecursive function also but it has its own dedicated tests, above.
     AssetProcessor::SourceFilesForFingerprintingContainer deps;
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceToSource, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceToSource);
 
     // we should find all of the deps, but not the placeholders.
 
@@ -3152,14 +3055,14 @@ TEST_F(SourceFileDependenciesTest, UpdateSourceFileDependenciesDatabase_MissingF
     EXPECT_NE(deps.find(m_dependsOnFile2_Source.toUtf8().constData()), deps.end()); // b
 
     deps.clear();
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_JobToJob, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_JobToJob);
     // the above function includes the actual source, as an absolute path.
     EXPECT_EQ(deps.size(), 2);
     EXPECT_NE(deps.find(m_absPath.toUtf8().constData()), deps.end());
     EXPECT_NE(deps.find(m_dependsOnFile2_Job.toUtf8().constData()), deps.end()); // d
 
     deps.clear();
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any);
     // the above function includes the actual source, as an absolute path.
     EXPECT_EQ(deps.size(), 3);
     EXPECT_NE(deps.find(m_absPath.toUtf8().constData()), deps.end());
@@ -3177,7 +3080,7 @@ TEST_F(SourceFileDependenciesTest, UpdateSourceFileDependenciesDatabase_MissingF
     // the rest of this test now performs a series of queries to verify the database was correctly set.
     // this indirectly verifies the QueryAbsolutePathDependenciesRecursive function also but it has its own dedicated tests, above.
     AssetProcessor::SourceFilesForFingerprintingContainer deps;
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceToSource, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceToSource);
 
     // we should find all of the deps, but a and c are missing and thus should not appear.
     EXPECT_EQ(deps.size(), 2);
@@ -3185,13 +3088,13 @@ TEST_F(SourceFileDependenciesTest, UpdateSourceFileDependenciesDatabase_MissingF
     EXPECT_NE(deps.find(m_dependsOnFile1_Source.toUtf8().constData()), deps.end());   // a
 
     deps.clear();
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_JobToJob, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_JobToJob);
     EXPECT_EQ(deps.size(), 2);
     EXPECT_NE(deps.find(m_absPath.toUtf8().constData()), deps.end());
     EXPECT_NE(deps.find(m_dependsOnFile1_Job.toUtf8().constData()), deps.end());  // c
 
     deps.clear();
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any);
     EXPECT_EQ(deps.size(), 3);
     EXPECT_NE(deps.find(m_absPath.toUtf8().constData()), deps.end());
     EXPECT_NE(deps.find(m_dependsOnFile1_Source.toUtf8().constData()), deps.end());  // a
@@ -3226,7 +3129,7 @@ TEST_F(SourceFileDependenciesTest, UpdateSourceFileDependenciesDatabase_MissingF
 
     // b should no longer be a placeholder, so both A and B should be present as their actual path.
     AssetProcessor::SourceFilesForFingerprintingContainer deps;
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceToSource, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceToSource);
     EXPECT_EQ(deps.size(), 3);
     EXPECT_NE(deps.find(m_absPath.toUtf8().constData()), deps.end());
     EXPECT_NE(deps.find(m_dependsOnFile1_Source.toUtf8().constData()), deps.end());   // a
@@ -3234,7 +3137,7 @@ TEST_F(SourceFileDependenciesTest, UpdateSourceFileDependenciesDatabase_MissingF
 
     // but d should still be a placeholder, since we have not declared it yet.
     deps.clear();
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_JobToJob, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_JobToJob);
     EXPECT_EQ(deps.size(), 2);
     EXPECT_NE(deps.find(m_absPath.toUtf8().constData()), deps.end());
     EXPECT_NE(deps.find(m_dependsOnFile1_Job.toUtf8().constData()), deps.end());  // c
@@ -3256,7 +3159,7 @@ TEST_F(SourceFileDependenciesTest, UpdateSourceFileDependenciesDatabase_MissingF
 
     // all files should now be present:
     deps.clear();
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any);
     EXPECT_EQ(deps.size(), 5);
     EXPECT_NE(deps.find(m_absPath.toUtf8().constData()), deps.end());
     EXPECT_NE(deps.find(m_dependsOnFile1_Source.toUtf8().constData()), deps.end());
@@ -3290,14 +3193,14 @@ TEST_F(SourceFileDependenciesTest, UpdateSourceFileDependenciesDatabase_MissingF
 
     // a should no longer be a placeholder
     AssetProcessor::SourceFilesForFingerprintingContainer deps;
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceToSource, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceToSource);
     EXPECT_EQ(deps.size(), 3);
     EXPECT_NE(deps.find(m_absPath.toUtf8().constData()), deps.end());
     EXPECT_NE(deps.find(m_dependsOnFile1_Source.toUtf8().constData()), deps.end());   // a
     EXPECT_NE(deps.find(m_dependsOnFile2_Source.toUtf8().constData()), deps.end());   // b
     deps.clear();
 
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_JobToJob, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_JobToJob);
     EXPECT_EQ(deps.size(), 2);
     EXPECT_NE(deps.find(m_absPath.toUtf8().constData()), deps.end());
     EXPECT_NE(deps.find(m_dependsOnFile2_Job.toUtf8().constData()), deps.end());  // d
@@ -3319,7 +3222,7 @@ TEST_F(SourceFileDependenciesTest, UpdateSourceFileDependenciesDatabase_MissingF
 
     // all files should now be present:
     deps.clear();
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("assetProcessorManagerTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(m_sourceFileUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any);
     EXPECT_EQ(deps.size(), 5);
     EXPECT_NE(deps.find(m_absPath.toUtf8().constData()), deps.end());
     EXPECT_NE(deps.find(m_dependsOnFile1_Source.toUtf8().constData()), deps.end());
@@ -3672,6 +3575,62 @@ TEST_F(AssetProcessorManagerTest, SourceFileProcessFailure_ClearsFingerprint)
     ASSERT_EQ(source.m_analysisFingerprint, "");
 }
 
+TEST_F(AssetProcessorManagerTest, SourceFileProcessFailure_ValidLfsPointerFile_ReceiveLFSPointerFileError)
+{
+    // Override the project and engine root directories in the setting registry to create a custom .gitattributes file for testing.
+    auto settingsRegistry = AZ::SettingsRegistry::Get();
+    ASSERT_TRUE(settingsRegistry);
+    AZ::IO::FixedMaxPathString engineRoot, projectRoot;
+    settingsRegistry->Get(engineRoot, AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
+    settingsRegistry->Get(projectRoot, AZ::SettingsRegistryMergeUtils::FilePathKey_ProjectPath);
+    settingsRegistry->Set(AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder, m_tempDir.path().toUtf8().data());
+    settingsRegistry->Set(AZ::SettingsRegistryMergeUtils::FilePathKey_ProjectPath, m_tempDir.path().toUtf8().data());
+
+    QDir tempPath(m_tempDir.path());
+    QString gitAttributesPath = tempPath.absoluteFilePath(".gitattributes");
+    ASSERT_TRUE(UnitTestUtils::CreateDummyFile(gitAttributesPath, QString(
+        "#\n"
+        "# Git LFS(see https ://git-lfs.github.com/)\n"
+        "#\n"
+        "*.txt filter=lfs diff=lfs merge=lfs -text\n")));
+
+    QString sourcePath = tempPath.absoluteFilePath("subfolder1/test.txt");
+    ASSERT_TRUE(UnitTestUtils::CreateDummyFile(sourcePath, QString(
+        "version https://git-lfs.github.com/spec/v1\n"
+        "oid sha256:ee4799379bfcfa99e95afd6494da51fbeda95f21ea71d267ae7102f048edec85\n"
+        "size 63872\n")));
+
+    constexpr int idleWaitTime = 5000;
+    using namespace AzToolsFramework::AssetDatabase;
+
+    QList<AssetProcessor::JobDetails> processResults;
+    auto assetConnection = QObject::connect(m_assetProcessorManager.get(), &AssetProcessorManager::AssetToProcess, [&processResults](JobDetails details)
+        {
+            processResults.push_back(AZStd::move(details));
+        });
+
+    // Add the test file and signal a failed event
+    m_assetProcessorManager.get()->AssessAddedFile(sourcePath);
+    ASSERT_TRUE(BlockUntilIdle(idleWaitTime));
+
+    for(const auto& processResult : processResults)
+    {
+        AssetBuilderSDK::ProcessJobResponse response;
+        response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
+
+        m_assetProcessorManager->AssetFailed(processResult.m_jobEntry);
+    }
+
+    ASSERT_TRUE(BlockUntilIdle(idleWaitTime));
+
+    // An error message should be thrown for the valid LFS pointer file.
+    ASSERT_EQ(m_errorAbsorber->m_numErrorsAbsorbed, 1);
+
+    // Revert the project and engine root directories in the setting registry.
+    settingsRegistry->Set(AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder, engineRoot);
+    settingsRegistry->Set(AZ::SettingsRegistryMergeUtils::FilePathKey_ProjectPath, projectRoot);
+}
+
 //////////////////////////////////////////////////////////////////////////
 
 void FingerprintTest::SetUp()
@@ -3766,11 +3725,12 @@ TEST_F(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_WildcardM
     ASSERT_TRUE(UnitTestUtils::CreateDummyFile(dependsOnFilec_Job, QString("tempdata\n")));
 
     // construct the dummy job to feed to the database updater function:
+    AZ::Uuid wildcardTestUuid = AssetUtilities::CreateSafeSourceUUIDFromName("wildcardTest.txt");
     AssetProcessorManager::JobToProcessEntry job;
     job.m_sourceFileInfo.m_databasePath = "wildcardTest.txt";
     job.m_sourceFileInfo.m_pathRelativeToScanFolder = "wildcardTest.txt";
     job.m_sourceFileInfo.m_scanFolder = scanFolder;
-    job.m_sourceFileInfo.m_uuid = AssetUtilities::CreateSafeSourceUUIDFromName(job.m_sourceFileInfo.m_databasePath.toUtf8().data());
+    job.m_sourceFileInfo.m_uuid = wildcardTestUuid;
 
     // each file we will take a different approach to publishing:  rel path, and UUID:
     job.m_sourceFileDependencies.push_back(AZStd::make_pair<AZ::Uuid, AssetBuilderSDK::SourceFileDependency>(dummyBuilderUUID, { "b*.txt", AZ::Uuid::CreateNull(), AssetBuilderSDK::SourceFileDependency::SourceFileDependencyType::Wildcards }));
@@ -3787,24 +3747,27 @@ TEST_F(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_WildcardM
 
     m_assetProcessorManager.get()->UpdateSourceFileDependenciesDatabase(job);
 
+    AzToolsFramework::AssetDatabase::SourceDatabaseEntry wildcard(scanFolder->ScanFolderID(), "wildcardTest.txt", wildcardTestUuid, "fingerprint");
+    m_assetProcessorManager->m_stateData->SetSource(wildcard);
+
     AssetProcessor::SourceFilesForFingerprintingContainer deps;
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("wildcardTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceToSource, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(wildcardTestUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceToSource);
     EXPECT_EQ(deps.size(), 2);
     EXPECT_NE(deps.find(dependsOnFileb_Source.toUtf8().constData()), deps.end());
     deps.clear();
 
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("wildcardTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_JobToJob, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(wildcardTestUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_JobToJob);
     EXPECT_EQ(deps.size(), 2);
     EXPECT_NE(deps.find(dependsOnFilec_Job.toUtf8().constData()), deps.end());
     deps.clear();
 
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("wildcardTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceOrJob, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(wildcardTestUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceOrJob);
     EXPECT_EQ(deps.size(), 3);
     EXPECT_NE(deps.find(dependsOnFilec_Job.toUtf8().constData()), deps.end());
     EXPECT_NE(deps.find(dependsOnFileb_Source.toUtf8().constData()), deps.end());
     deps.clear();
 
-    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(QString::fromUtf8("wildcardTest.txt"), deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceLikeMatch, false);
+    m_assetProcessorManager.get()->QueryAbsolutePathDependenciesRecursive(wildcardTestUuid, deps, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceLikeMatch);
     EXPECT_EQ(deps.size(), 1);
     deps.clear();
 
@@ -3815,7 +3778,7 @@ TEST_F(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_WildcardM
         return true;
     };
 
-    m_assetProcessorManager.get()->m_stateData->QueryDependsOnSourceBySourceDependency("wildcardTest.txt", nullptr, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceLikeMatch, callbackFunction);
+    m_assetProcessorManager.get()->m_stateData->QueryDependsOnSourceBySourceDependency(wildcardTestUuid, nullptr, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceLikeMatch, callbackFunction);
     EXPECT_EQ(wildcardDeps.size(), 2);
 
     // The database should have the wildcard record and the individual dependency on b and c at this point, now we add new files
@@ -4402,12 +4365,12 @@ void WildcardSourceDependencyTest::SetUp()
         m_config->AddExcludeRecognizer(excludeFile);
     }
 
-    UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("subfolder1/1a.foo"));
-    UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("subfolder1/1b.foo"));
-    UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("subfolder2/redirected/a.foo"));
-    UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("subfolder2/redirected/b.foo"));
-    UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("subfolder2/redirected/folder/one/c.foo"));
-    UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("subfolder2/redirected/folder/one/d.foo"));
+    CreateSourceAndFile("subfolder1/1a.foo");
+    CreateSourceAndFile("subfolder1/1b.foo");
+    CreateSourceAndFile("subfolder2/a.foo");
+    CreateSourceAndFile("subfolder2/b.foo");
+    CreateSourceAndFile("subfolder2/folder/one/c.foo");
+    CreateSourceAndFile("subfolder2/folder/one/d.foo");
 
     // Add a file that is not in a scanfolder.  Should always be ignored
     UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("not/a/scanfolder/e.foo"));
@@ -4416,10 +4379,10 @@ void WildcardSourceDependencyTest::SetUp()
     UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("no_recurse/one/two/three/f.foo"));
 
     // Add a file to an ignored folder
-    UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("subfolder2/redirected/folder/ignored/g.foo"));
+    UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("subfolder2/folder/ignored/g.foo"));
 
     // Add an ignored file
-    UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("subfolder2/redirected/folder/one/z.foo"));
+    UnitTestUtils::CreateDummyFile(tempPath.absoluteFilePath("subfolder2/folder/one/z.foo"));
 
     // Add a file in the cache
     AZStd::string projectCacheRootValue;
@@ -4430,23 +4393,27 @@ void WildcardSourceDependencyTest::SetUp()
 
     AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer dependencies;
 
+    auto aUuid = AssetUtilities::CreateSafeSourceUUIDFromName("a.foo");
+    auto bUuid = AssetUtilities::CreateSafeSourceUUIDFromName("b.foo");
+    auto dUuid = AssetUtilities::CreateSafeSourceUUIDFromName("folder/one/d.foo");
+
     // Relative path wildcard dependency
     dependencies.push_back(AzToolsFramework::AssetDatabase::SourceFileDependencyEntry(
-        AZ::Uuid::CreateRandom(), "a.foo", "%a.foo",
+        AZ::Uuid::CreateRandom(), aUuid, "%a.foo",
         AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceLikeMatch, 0, ""));
 
     // Absolute path wildcard dependency
     dependencies.push_back(AzToolsFramework::AssetDatabase::SourceFileDependencyEntry(
-        AZ::Uuid::CreateRandom(), "b.foo", tempPath.absoluteFilePath("%b.foo").toUtf8().constData(),
+        AZ::Uuid::CreateRandom(), bUuid, tempPath.absoluteFilePath("%b.foo").toUtf8().constData(),
         AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceLikeMatch, 0, ""));
 
     // Test what happens when we have 2 dependencies on the same file
     dependencies.push_back(AzToolsFramework::AssetDatabase::SourceFileDependencyEntry(
-        AZ::Uuid::CreateRandom(), "folder/one/d.foo", "%c.foo",
+        AZ::Uuid::CreateRandom(), dUuid, "%c.foo",
         AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceLikeMatch, 0, ""));
 
     dependencies.push_back(AzToolsFramework::AssetDatabase::SourceFileDependencyEntry(
-        AZ::Uuid::CreateRandom(), "folder/one/d.foo", tempPath.absoluteFilePath("%c.foo").toUtf8().constData(),
+        AZ::Uuid::CreateRandom(), dUuid, tempPath.absoluteFilePath("%c.foo").toUtf8().constData(),
         AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceLikeMatch, 0, ""));
 
 #ifdef AZ_PLATFORM_WINDOWS
@@ -4457,7 +4424,7 @@ void WildcardSourceDependencyTest::SetUp()
     // This only applies to windows because on other OSes if the dependency starts with /, then its an abs path dependency
     auto test = (tempPath.absolutePath().left(1) + "%.foo");
     dependencies.push_back(AzToolsFramework::AssetDatabase::SourceFileDependencyEntry(
-        AZ::Uuid::CreateRandom(), "folder/one/d.foo",
+        AZ::Uuid::CreateRandom(), dUuid,
         (test).toUtf8().constData(),
         AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_SourceLikeMatch, 0, ""));
 #endif
@@ -4498,7 +4465,7 @@ TEST_F(WildcardSourceDependencyTest, Absolute_WithFolder)
     AZStd::vector<AZStd::string> resolvedPaths;
     QDir tempPath(m_tempDir.path());
 
-    ASSERT_TRUE(Test(tempPath.absoluteFilePath("subfolder2/redirected/*.foo").toUtf8().constData(), resolvedPaths));
+    ASSERT_TRUE(Test(tempPath.absoluteFilePath("subfolder2/*.foo").toUtf8().constData(), resolvedPaths));
     ASSERT_THAT(resolvedPaths, ::testing::UnorderedElementsAre("a.foo", "b.foo", "folder/one/c.foo", "folder/one/d.foo"));
 }
 
@@ -4621,7 +4588,7 @@ TEST_F(WildcardSourceDependencyTest, FilesAddedAfterInitialCache)
     }
 
     // Add a file to a new ignored folder
-    QString newFilePath = tempPath.absoluteFilePath("subfolder2/redirected/folder/two/ignored/three/new.foo");
+    QString newFilePath = tempPath.absoluteFilePath("subfolder2/folder/two/ignored/three/new.foo");
     UnitTestUtils::CreateDummyFile(newFilePath);
 
     excludedFolderCacheInterface->FileAdded(newFilePath);
@@ -4629,7 +4596,7 @@ TEST_F(WildcardSourceDependencyTest, FilesAddedAfterInitialCache)
     const auto& excludedFolders = excludedFolderCacheInterface->GetExcludedFolders();
 
     ASSERT_EQ(excludedFolders.size(), 3);
-    ASSERT_THAT(excludedFolders, ::testing::Contains(AZStd::string(tempPath.absoluteFilePath("subfolder2/redirected/folder/two/ignored").toUtf8().constData())));
+    ASSERT_THAT(excludedFolders, ::testing::Contains(AZStd::string(tempPath.absoluteFilePath("subfolder2/folder/two/ignored").toUtf8().constData())));
 }
 
 TEST_F(WildcardSourceDependencyTest, FilesRemovedAfterInitialCache)
@@ -4638,7 +4605,7 @@ TEST_F(WildcardSourceDependencyTest, FilesRemovedAfterInitialCache)
     QDir tempPath(m_tempDir.path());
 
     // Add a file to a new ignored folder
-    QString newFilePath = tempPath.absoluteFilePath("subfolder2/redirected/folder/two/ignored/three/new.foo");
+    QString newFilePath = tempPath.absoluteFilePath("subfolder2/folder/two/ignored/three/new.foo");
     UnitTestUtils::CreateDummyFile(newFilePath);
 
     auto excludedFolderCacheInterface = AZ::Interface<ExcludedFolderCacheInterface>::Get();
@@ -4651,7 +4618,7 @@ TEST_F(WildcardSourceDependencyTest, FilesRemovedAfterInitialCache)
         ASSERT_EQ(excludedFolders.size(), 3);
     }
 
-    m_fileStateCache->SignalDeleteEvent(tempPath.absoluteFilePath("subfolder2/redirected/folder/two/ignored"));
+    m_fileStateCache->SignalDeleteEvent(tempPath.absoluteFilePath("subfolder2/folder/two/ignored"));
 
     const auto& excludedFolders = excludedFolderCacheInterface->GetExcludedFolders();
 
@@ -4664,7 +4631,7 @@ TEST_F(WildcardSourceDependencyTest, NewFile_MatchesSavedRelativeDependency)
 
     auto matches = FileAddedTest(tempPath.absoluteFilePath("subfolder1/1a.foo"));
 
-    ASSERT_THAT(matches, ::testing::UnorderedElementsAre(tempPath.absoluteFilePath("subfolder2/redirected/a.foo").toUtf8().constData()));
+    ASSERT_THAT(matches, ::testing::UnorderedElementsAre(tempPath.absoluteFilePath("subfolder2/a.foo").toUtf8().constData()));
 }
 
 TEST_F(WildcardSourceDependencyTest, NewFile_MatchesSavedAbsoluteDependency)
@@ -4673,14 +4640,14 @@ TEST_F(WildcardSourceDependencyTest, NewFile_MatchesSavedAbsoluteDependency)
 
     auto matches = FileAddedTest(tempPath.absoluteFilePath("subfolder1/1b.foo"));
 
-    ASSERT_THAT(matches, ::testing::UnorderedElementsAre(tempPath.absoluteFilePath("subfolder2/redirected/b.foo").toUtf8().constData()));
+    ASSERT_THAT(matches, ::testing::UnorderedElementsAre(tempPath.absoluteFilePath("subfolder2/b.foo").toUtf8().constData()));
 }
 
 TEST_F(WildcardSourceDependencyTest, NewFile_MatchesDuplicatedDependenciesOnce)
 {
     QDir tempPath(m_tempDir.path());
 
-    auto matches = FileAddedTest(tempPath.absoluteFilePath("subfolder2/redirected/folder/one/c.foo"));
+    auto matches = FileAddedTest(tempPath.absoluteFilePath("subfolder2/folder/one/c.foo"));
 
-    ASSERT_THAT(matches, ::testing::UnorderedElementsAre(tempPath.absoluteFilePath("subfolder2/redirected/folder/one/d.foo").toUtf8().constData()));
+    ASSERT_THAT(matches, ::testing::UnorderedElementsAre(tempPath.absoluteFilePath("subfolder2/folder/one/d.foo").toUtf8().constData()));
 }

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

@@ -168,6 +168,9 @@ protected:
     void SetUp() override;
     void TearDown() override;
 
+    virtual void CreateSourceAndFile(const char* tempFolderRelativePath);
+    virtual void PopulateDatabase();
+
     QTemporaryDir m_tempDir;
 
     AZStd::unique_ptr<AssetProcessorManager_Test> m_assetProcessorManager;
@@ -178,6 +181,11 @@ protected:
     AZStd::atomic_bool m_isIdling;
     QMetaObject::Connection m_idleConnection;
 
+    AZ::Uuid m_aUuid = AssetUtilities::CreateSafeSourceUUIDFromName("a.txt");
+    AZ::Uuid m_bUuid = AssetUtilities::CreateSafeSourceUUIDFromName("b.txt");
+    AZ::Uuid m_cUuid = AssetUtilities::CreateSafeSourceUUIDFromName("c.txt");
+    AZ::Uuid m_dUuid = AssetUtilities::CreateSafeSourceUUIDFromName("d.txt");
+
     struct StaticData
     {
         AZStd::string m_databaseLocation;
@@ -222,6 +230,8 @@ struct SourceFileDependenciesTest : AssetProcessorManagerTest
         bool primeMap,
         AssetProcessor::AssetProcessorManager::JobToProcessEntry& job);
 
+    void PopulateDatabase() override;
+
     auto GetDependencyList();
 
     AssetBuilderSDK::SourceFileDependency MakeSourceDependency(const char* file, bool wildcard = false);
@@ -239,6 +249,7 @@ struct SourceFileDependenciesTest : AssetProcessorManagerTest
     const AssetProcessor::ScanFolderInfo* m_scanFolder = nullptr;
 
     AZ::Uuid m_dummyBuilderUuid;
+    AZ::Uuid m_sourceFileUuid = AssetUtilities::CreateSafeSourceUUIDFromName("assetProcessorManagerTest.txt");
     AZ::Uuid m_uuidOfA = AssetUtilities::CreateSafeSourceUUIDFromName("a.txt");
     AZ::Uuid m_uuidOfB = AssetUtilities::CreateSafeSourceUUIDFromName("b.txt");
     AZ::Uuid m_uuidOfC = AssetUtilities::CreateSafeSourceUUIDFromName("c.txt");

+ 1 - 1
Code/Tools/AssetProcessor/native/tests/assetmanager/JobDependencySubIdTests.cpp

@@ -44,7 +44,7 @@ namespace UnitTests
         ASSERT_TRUE(m_stateData->SetProduct(product2));
 
         SourceFileDependencyEntry dependency1{ AZ::Uuid::CreateRandom(),
-                                               source2.m_sourceName.c_str(),
+                                               source2.m_sourceGuid,
                                                source1.m_sourceName.c_str(),
                                                SourceFileDependencyEntry::DEP_JobToJob,
                                                0,

+ 5 - 1
Code/Tools/AssetProcessor/native/tests/assetmanager/ModtimeScanningTests.cpp

@@ -44,6 +44,10 @@ namespace UnitTests
             });
     }
 
+    void ModtimeScanningTest::PopulateDatabase()
+    {
+    }
+
     void ModtimeScanningTest::SetUp()
     {
         using namespace AssetProcessor;
@@ -482,7 +486,7 @@ namespace UnitTests
         SourceFileDependencyEntry newEntry1;
         newEntry1.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
         newEntry1.m_builderGuid = AZ::Uuid::CreateRandom();
-        newEntry1.m_source = m_data->m_absolutePath[0].toUtf8().constData();
+        newEntry1.m_sourceGuid = AZ::Uuid{ "{C0BD819A-F84E-4A56-A6A5-917AE3ECDE53}" };
         newEntry1.m_dependsOnSource = m_data->m_absolutePath[1].toUtf8().constData();
         newEntry1.m_typeOfDependency = SourceFileDependencyEntry::DEP_SourceToSource;
 

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

@@ -15,6 +15,7 @@ namespace UnitTests
     struct ModtimeScanningTest : AssetProcessorManagerTest
     {
         void SetUpAssetProcessorManager();
+        void PopulateDatabase() override;
         void SetUp() override;
         void TearDown() override;
 

+ 142 - 0
Code/Tools/AssetProcessor/native/tests/assetmanager/Validators/LfsPointerFileValidatorTests.cpp

@@ -0,0 +1,142 @@
+/*
+* Copyright (c) Contributors to the Open 3D Engine Project.
+* For complete copyright and license terms please see the LICENSE at the root of this distribution.
+*
+* SPDX-License-Identifier: Apache-2.0 OR MIT
+*
+*/
+#include <native/tests/assetmanager/Validators/LfsPointerFileValidatorTests.h>
+#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
+#include <AzCore/Utils/Utils.h>
+
+namespace UnitTests
+{
+    void LfsPointerFileValidatorTests::SetUp()
+    {
+        AssetManagerTestingBase::SetUp();
+
+        m_tempFolder = m_tempDir.GetDirectory();
+        CreateTestFile((m_tempFolder / ".gitattributes").Native(),
+            "#\n"
+            "# Git LFS(see https ://git-lfs.github.com/)\n"
+            "#\n"
+            "*.test filter=lfs diff=lfs merge=lfs -text\n"
+        );
+
+        m_validator = AssetProcessor::LfsPointerFileValidator({ m_tempDir.GetDirectory() });
+    }
+
+    void LfsPointerFileValidatorTests::TearDown()
+    {
+        RemoveTestFile((m_tempFolder / ".gitattributes").Native());
+        AssetManagerTestingBase::TearDown();
+    }
+
+    bool LfsPointerFileValidatorTests::CreateTestFile(const AZStd::string& filePath, const AZStd::string& content)
+    {
+        AZ::IO::HandleType fileHandle;
+        if (!AZ::IO::FileIOBase::GetInstance()->Open(filePath.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeText, fileHandle))
+        {
+            return false;
+        }
+
+        AZ::IO::FileIOBase::GetInstance()->Write(fileHandle, content.c_str(), content.size());
+        AZ::IO::FileIOBase::GetInstance()->Close(fileHandle);
+        return true;
+    }
+
+    bool LfsPointerFileValidatorTests::RemoveTestFile(const AZStd::string& filePath)
+    {
+        if (AZ::IO::FileIOBase::GetInstance()->Exists(filePath.c_str()))
+        {
+            return AZ::IO::FileIOBase::GetInstance()->Remove(filePath.c_str());
+        }
+
+        return true;
+    }
+
+    TEST_F(LfsPointerFileValidatorTests, GetLfsPointerFilePathPatterns_GitAttributesFileExists_ReturnLfsPointerFilePathPatterns)
+    {
+        AZStd::set<AZStd::string> lfsPointerFilePathPatterns = m_validator.GetLfsPointerFilePathPatterns();
+        ASSERT_EQ(lfsPointerFilePathPatterns.size(), 1);
+        ASSERT_EQ(*lfsPointerFilePathPatterns.begin(), "*.test");
+    }
+
+    TEST_F(LfsPointerFileValidatorTests, IsLfsPointerFile_ValidLfsPointerFile_CheckSucceed)
+    {
+        AZStd::string testFilePath = (m_tempFolder / "file.test").Native();
+        CreateTestFile(testFilePath,
+            "version https://git-lfs.github.com/spec/v1\n"
+            "oid sha256:ee4799379bfcfa99e95afd6494da51fbeda95f21ea71d267ae7102f048edec85\n"
+            "size 63872\n");
+
+        ASSERT_TRUE(m_validator.IsLfsPointerFile(testFilePath));
+
+        RemoveTestFile(testFilePath);
+    }
+
+    TEST_F(LfsPointerFileValidatorTests, IsLfsPointerFile_NonLfsPointerFileType_CheckFail)
+    {
+        AZStd::string testFilePath = (m_tempFolder / "file.test1").Native();
+        CreateTestFile(testFilePath,
+            "version https://git-lfs.github.com/spec/v1\n"
+            "oid sha256:ee4799379bfcfa99e95afd6494da51fbeda95f21ea71d267ae7102f048edec85\n"
+            "size 63872\n");
+
+        ASSERT_FALSE(m_validator.IsLfsPointerFile(testFilePath));
+
+        RemoveTestFile(testFilePath);
+    }
+
+    TEST_F(LfsPointerFileValidatorTests, IsLfsPointerFile_InvalidFirstKey_CheckFail)
+    {
+        AZStd::string testFilePath = (m_tempFolder / "file.test").Native();
+        CreateTestFile(testFilePath,
+            "oid sha256:ee4799379bfcfa99e95afd6494da51fbeda95f21ea71d267ae7102f048edec85\n"
+            "size 63872\n"
+            "version https://git-lfs.github.com/spec/v1\n");
+
+        ASSERT_FALSE(m_validator.IsLfsPointerFile(testFilePath));
+
+        RemoveTestFile(testFilePath);
+    }
+
+    TEST_F(LfsPointerFileValidatorTests, IsLfsPointerFile_InvalidKeyCharacter_CheckFail)
+    {
+        AZStd::string testFilePath = (m_tempFolder / "file.test").Native();
+        CreateTestFile(testFilePath,
+            "version https://git-lfs.github.com/spec/v1\n"
+            "oid+ sha256:ee4799379bfcfa99e95afd6494da51fbeda95f21ea71d267ae7102f048edec85\n"
+            "size 63872\n");
+
+        ASSERT_FALSE(m_validator.IsLfsPointerFile(testFilePath));
+
+        RemoveTestFile(testFilePath);
+    }
+
+    TEST_F(LfsPointerFileValidatorTests, IsLfsPointerFile_UnorderedKeys_CheckFail)
+    {
+        AZStd::string testFilePath = (m_tempFolder / "file.test").Native();
+        CreateTestFile(testFilePath,
+            "version https://git-lfs.github.com/spec/v1\n"
+            "size 63872\n"
+            "oid sha256:ee4799379bfcfa99e95afd6494da51fbeda95f21ea71d267ae7102f048edec85\n");
+
+        ASSERT_FALSE(m_validator.IsLfsPointerFile(testFilePath));
+
+        RemoveTestFile(testFilePath);
+    }
+
+    TEST_F(LfsPointerFileValidatorTests, IsLfsPointerFile_MissingRequiredKey_CheckFail)
+    {
+        AZStd::string testFilePath = (m_tempFolder / "file.test").Native();
+        CreateTestFile(testFilePath,
+            "version https://git-lfs.github.com/spec/v1\n"
+            "bla 63872\n"
+            "oid sha256:ee4799379bfcfa99e95afd6494da51fbeda95f21ea71d267ae7102f048edec85\n");
+
+        ASSERT_FALSE(m_validator.IsLfsPointerFile(testFilePath));
+
+        RemoveTestFile(testFilePath);
+    }
+}

+ 30 - 0
Code/Tools/AssetProcessor/native/tests/assetmanager/Validators/LfsPointerFileValidatorTests.h

@@ -0,0 +1,30 @@
+/*
+* Copyright (c) Contributors to the Open 3D Engine Project.
+* For complete copyright and license terms please see the LICENSE at the root of this distribution.
+*
+* SPDX-License-Identifier: Apache-2.0 OR MIT
+*
+*/
+
+#pragma once
+
+#include <native/tests/assetmanager/AssetManagerTestingBase.h>
+#include <native/AssetManager/Validators/LfsPointerFileValidator.h>
+
+namespace UnitTests
+{
+    struct LfsPointerFileValidatorTests
+        : AssetManagerTestingBase
+    {
+        void SetUp() override;
+        void TearDown() override;
+
+        bool CreateTestFile(const AZStd::string& filePath, const AZStd::string& content);
+        bool RemoveTestFile(const AZStd::string& filePath);
+
+        AZ::IO::Path m_tempFolder;
+        AssetProcessor::LfsPointerFileValidator m_validator;
+    };
+}
+
+

+ 2 - 2
Code/Tools/AssetProcessor/native/ui/AssetDetailsPanel.cpp

@@ -58,10 +58,10 @@ namespace AssetProcessor
         {
             return;
         }
-        
+
         AssetDatabaseConnection assetDatabaseConnection;
         assetDatabaseConnection.OpenDatabase();
-        
+
         AzToolsFramework::AssetDatabase::SourceDatabaseEntry sourceDetails;
         assetDatabaseConnection.QuerySourceBySourceName(
             source.c_str(),

+ 3 - 3
Code/Tools/AssetProcessor/native/ui/AssetDetailsPanel.h

@@ -48,7 +48,7 @@ namespace AssetProcessor
 
         void GoToSource(const AZStd::string& source);
         void GoToProduct(const AZStd::string& product);
-        
+
         void SetIntermediateAssetFolderId(AZStd::optional<AZ::s64> intermediateAssetFolderId)
         {
             m_intermediateAssetFolderId = intermediateAssetFolderId;
@@ -58,7 +58,7 @@ namespace AssetProcessor
         QTreeView* m_sourceTreeView = nullptr;
         SourceAssetTreeModel* m_sourceTreeModel = nullptr;
         AssetTreeFilterModel* m_sourceFilterModel = nullptr;
-        
+
         QTreeView* m_intermediateTreeView = nullptr;
         SourceAssetTreeModel* m_intermediateTreeModel = nullptr;
         AssetTreeFilterModel* m_intermediateFilterModel = nullptr;
@@ -67,7 +67,7 @@ namespace AssetProcessor
         ProductAssetTreeModel* m_productTreeModel = nullptr;
         AssetTreeFilterModel* m_productFilterModel = nullptr;
         QTabWidget* m_assetsTab = nullptr;
-        
+
         AZStd::optional<AZ::s64> m_intermediateAssetFolderId;
     };
 } // namespace AssetProcessor

+ 8 - 8
Code/Tools/AssetProcessor/native/ui/MainWindow.cpp

@@ -467,7 +467,7 @@ void MainWindow::Activate()
         ui->ProductAssetsTreeView,
         m_productModel,
         m_productAssetTreeFilterModel,
-        ui->assetsTabWidget);    
+        ui->assetsTabWidget);
     ui->productAssetDetailsPanel->RegisterAssociatedWidgets(
         ui->SourceAssetsTreeView,
         m_sourceModel,
@@ -1970,7 +1970,7 @@ void MainWindow::ShowJobViewContextMenu(const QPoint& pos)
     {
         ui->dialogStack->setCurrentIndex(static_cast<int>(DialogStackIndex::Assets));
         ui->buttonList->setCurrentIndex(static_cast<int>(DialogStackIndex::Assets));
-        ui->sourceAssetDetailsPanel->GoToSource(item->m_elementId.GetInputAssetName().toUtf8().constData());
+        ui->sourceAssetDetailsPanel->GoToSource(AZStd::string(item->m_elementId.GetInputAssetName().toUtf8().constData()));
     });
 
     if (item->m_jobState != AzToolsFramework::AssetSystem::JobStatus::Completed)
@@ -2003,7 +2003,7 @@ void MainWindow::ShowJobViewContextMenu(const QPoint& pos)
             }
         };
         connect(productAssetMenu.m_listWidget, &QListWidget::itemClicked, this, productMenuItemClicked);
-        
+
         auto intermediateMenuItemClicked = [this, &menu](QListWidgetItem* item)
         {
             if (item)
@@ -2016,7 +2016,7 @@ void MainWindow::ShowJobViewContextMenu(const QPoint& pos)
             }
         };
         connect(intermediateAssetMenu.m_listWidget, &QListWidget::itemClicked, this, intermediateMenuItemClicked);
-        
+
         int intermediateCount = 0;
         int productCount = 0;
         m_sharedDbConnection->QueryJobByJobRunKey(
@@ -2056,7 +2056,7 @@ void MainWindow::ShowJobViewContextMenu(const QPoint& pos)
         {
             ResizeAssetRightClickMenuList(productAssetMenu.m_listWidget, productCount);
         }
-        
+
         if (intermediateCount == 0)
         {
             CreateDisabledAssetRightClickMenu(&menu, intermediateAssetMenu.m_assetMenu, intermediateMenuTitle, tr("This job created no intermediate product assets."));
@@ -2184,7 +2184,7 @@ void MainWindow::ShowIntermediateAssetContextMenu(const QPoint& pos)
         });
     });
     sourceAssetAction->setToolTip(tr("Show the source asset for this intermediate asset."));
-    
+
     BuildSourceAssetTreeContextMenu(menu, *cachedAsset);
 
     menu.exec(ui->SourceAssetsTreeView->viewport()->mapToGlobal(pos));
@@ -2220,7 +2220,7 @@ void MainWindow::BuildSourceAssetTreeContextMenu(QMenu& menu, const AssetProcess
 
 
     QString jobMenuText(tr("View job..."));
-    
+
     AssetRightClickMenuResult productAssetMenu(SetupProductAssetRightClickMenu(&menu));
     AssetRightClickMenuResult intermediateAssetMenu(SetupIntermediateAssetRightClickMenu(&menu));
 
@@ -2248,7 +2248,7 @@ void MainWindow::BuildSourceAssetTreeContextMenu(QMenu& menu, const AssetProcess
         }
     };
     connect(intermediateAssetMenu.m_listWidget, &QListWidget::itemClicked, this, intermediateMenuItemClicked);
-    
+
     int intermediateCount = 0;
     int productCount = 0;
     AZStd::string sourceName(sourceItemData->m_assetDbName);

+ 15 - 7
Code/Tools/AssetProcessor/native/ui/SourceAssetDetailsPanel.cpp

@@ -147,7 +147,7 @@ namespace AssetProcessor
                             intermediateAssetSourcePath = sourceEntry.m_sourceName;
                             return true;
                         });
-                    
+
                     AZStd::string sourceIntermediateAssetPath = AssetUtilities::StripAssetPlatformNoCopy(productEntry.m_productName);
 
                     // Qt handles cleanup automatically, setting this as the parent means
@@ -202,7 +202,7 @@ namespace AssetProcessor
         m_ui->outgoingSourceDependenciesTable->setRowCount(0);
         int sourceDependencyCount = 0;
         assetDatabaseConnection.QueryDependsOnSourceBySourceDependency(
-            sourceItemData->m_sourceInfo.m_sourceName.c_str(),
+            sourceItemData->m_sourceInfo.m_sourceGuid,
             nullptr,
             AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any,
             [&](AzToolsFramework::AssetDatabase::SourceFileDependencyEntry& sourceFileDependencyEntry)
@@ -211,7 +211,7 @@ namespace AssetProcessor
 
                 // Some outgoing source dependencies are wildcard, or unresolved paths.
                 // Only add a button to link to rows that actually exist.
-                
+
                 AzToolsFramework::AssetDatabase::SourceDatabaseEntry dependencyDetails;
                 assetDatabaseConnection.QuerySourceBySourceName(
                     sourceFileDependencyEntry.m_dependsOnSource.c_str(),
@@ -256,25 +256,33 @@ namespace AssetProcessor
         int sourceDependencyCount = 0;
         assetDatabaseConnection.QuerySourceDependencyByDependsOnSource(
             sourceItemData->m_sourceInfo.m_sourceName.c_str(),
-            nullptr,
             AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any,
             [&](AzToolsFramework::AssetDatabase::SourceFileDependencyEntry& sourceFileDependencyEntry)
             {
                 m_ui->incomingSourceDependenciesTable->insertRow(sourceDependencyCount);
 
+                AZStd::string sourceName;
+                assetDatabaseConnection.QuerySourceBySourceGuid(
+                    sourceFileDependencyEntry.m_sourceGuid,
+                    [&sourceName](const auto& entry)
+                    {
+                        sourceName = entry.m_sourceName;
+                        return false;
+                    });
+
                 // Qt handles cleanup automatically, setting this as the parent means
                 // when this panel is torn down, these widgets will be destroyed.
                 GoToButton* rowGoToButton = new GoToButton(this);
                 connect(
                     rowGoToButton->m_ui->goToPushButton,
                     &QPushButton::clicked,
-                    [=]
+                    [this, sourceName]
                     {
-                        GoToSource(sourceFileDependencyEntry.m_source);
+                        GoToSource(sourceName);
                     });
                 m_ui->incomingSourceDependenciesTable->setCellWidget(sourceDependencyCount, 0, rowGoToButton);
 
-                QTableWidgetItem* rowName = new QTableWidgetItem(sourceFileDependencyEntry.m_source.c_str());
+                QTableWidgetItem* rowName = new QTableWidgetItem(QString(sourceName.c_str()));
                 m_ui->incomingSourceDependenciesTable->setItem(sourceDependencyCount, 1, rowName);
 
                 ++sourceDependencyCount;

+ 31 - 29
Code/Tools/AssetProcessor/native/unittests/AssetProcessingStateDataUnitTests.cpp

@@ -122,7 +122,7 @@ void AssetProcessingStateDataUnitTest::DataTest(AssetProcessor::AssetDatabaseCon
             }
             return false;
         };
-    
+
     auto ScanFoldersContainPortableKey = [](const ScanFolderDatabaseEntryContainer& scanFolders, const char* portableKey) -> bool
     {
         for (const auto& scanFolder : scanFolders)
@@ -884,7 +884,7 @@ void AssetProcessingStateDataUnitTest::DataTest(AssetProcessor::AssetDatabaseCon
     UNIT_TEST_EXPECT_TRUE(stateData->CreateOrUpdateLegacySubID(legacyEntry));
     AZ::s64 newPK = legacyEntry.m_subIDsEntryID;
     UNIT_TEST_EXPECT_TRUE(newPK != AzToolsFramework::AssetDatabase::InvalidEntryId); // it should have also updated the PK
-    
+
     legacyEntry = LegacySubIDsEntry(AzToolsFramework::AssetDatabase::InvalidEntryId, product.m_productID, 4);
     UNIT_TEST_EXPECT_TRUE(stateData->CreateOrUpdateLegacySubID(legacyEntry));
     UNIT_TEST_EXPECT_TRUE(legacyEntry.m_subIDsEntryID != AzToolsFramework::AssetDatabase::InvalidEntryId); // it should have also updated the PK
@@ -903,7 +903,7 @@ void AssetProcessingStateDataUnitTest::DataTest(AssetProcessor::AssetDatabaseCon
     };
     UNIT_TEST_EXPECT_TRUE(stateData->QueryLegacySubIdsByProductID(product.m_productID, handler));
     UNIT_TEST_EXPECT_TRUE(entriesReturned.size() == 2);
-    
+
     bool foundSubID3 = false;
     bool foundSubID4 = false;
     for (const LegacySubIDsEntry& entryFound : entriesReturned)
@@ -935,7 +935,7 @@ void AssetProcessingStateDataUnitTest::DataTest(AssetProcessor::AssetDatabaseCon
     entriesReturned[0].m_subID = 6;
     UNIT_TEST_EXPECT_TRUE(stateData->CreateOrUpdateLegacySubID(entriesReturned[0]));
     entriesReturned.clear();
-    
+
     UNIT_TEST_EXPECT_TRUE(stateData->QueryLegacySubIdsByProductID(product2.m_productID, handler));
     UNIT_TEST_EXPECT_TRUE(entriesReturned.size() == 1);
     UNIT_TEST_EXPECT_TRUE(entriesReturned[0].m_subIDsEntryID != AzToolsFramework::AssetDatabase::InvalidEntryId);
@@ -945,16 +945,16 @@ void AssetProcessingStateDataUnitTest::DataTest(AssetProcessor::AssetDatabaseCon
     // test delete by product ID
     UNIT_TEST_EXPECT_TRUE(stateData->RemoveLegacySubIDsByProductID(product2.m_productID));
     entriesReturned.clear();
-    
+
     UNIT_TEST_EXPECT_TRUE(stateData->QueryLegacySubIdsByProductID(product2.m_productID, handler));
     UNIT_TEST_EXPECT_TRUE(entriesReturned.empty());
-    
+
     // test delete by PK.  The prior entries should be here for product1. This also makes sure the above
     // delete statement didn't delete more than it should have.
-    
+
     UNIT_TEST_EXPECT_TRUE(stateData->QueryLegacySubIdsByProductID(product.m_productID, handler));
     UNIT_TEST_EXPECT_TRUE(entriesReturned.size() == 2);
-    
+
     AZ::s64 toRemove = entriesReturned[0].m_subIDsEntryID;
     AZ::u32 removingSubID = entriesReturned[0].m_subID;
 
@@ -1133,7 +1133,7 @@ void AssetProcessingStateDataUnitTest::DataTest(AssetProcessor::AssetDatabaseCon
     UNIT_TEST_EXPECT_TRUE(ProductDependenciesContainDependencySoureGuid(productDependencies, productDependency.m_dependencySourceGuid));
     UNIT_TEST_EXPECT_TRUE(ProductDependenciesContainDependencySubID(productDependencies, productDependency.m_dependencySubID));
     UNIT_TEST_EXPECT_TRUE(ProductDependenciesContainDependencyFlags(productDependencies, productDependency.m_dependencyFlags));
-    
+
     // Setup some more dependencies
 
     //Product2 -> Product3
@@ -1155,10 +1155,10 @@ void AssetProcessingStateDataUnitTest::DataTest(AssetProcessor::AssetDatabaseCon
     /* Dependency Tree
     *
     * Product -> Product2 -> Product3 -> Product5 -> Product 6->
-    *                    \                          
+    *                    \
     *                     -> Product4
     */
-    
+
     // Direct Deps
 
     // Product -> Product2
@@ -1235,7 +1235,7 @@ void AssetProcessingStateDataUnitTest::DataTest(AssetProcessor::AssetDatabaseCon
     UNIT_TEST_EXPECT_TRUE(products.size() == 1);
     UNIT_TEST_EXPECT_TRUE(ProductsContainProductID(products, product6.m_productID));
 
-    // Product6 -> 
+    // Product6 ->
     products.clear();
     UNIT_TEST_EXPECT_FALSE(stateData->GetAllProductDependencies(product6.m_productID, products));
     UNIT_TEST_EXPECT_TRUE(products.size() == 0);
@@ -1313,7 +1313,7 @@ void AssetProcessingStateDataUnitTest::BuilderInfoTest(AssetProcessor::AssetData
         results.push_back(AZStd::move(element));
         return true; // returning false would stop iterating.  We want all results, so we return true.
     };
-    
+
     UNIT_TEST_EXPECT_TRUE(stateData->QueryBuilderInfoTable(resultGatherer));
     UNIT_TEST_EXPECT_TRUE(results.empty());
 
@@ -1373,66 +1373,68 @@ void AssetProcessingStateDataUnitTest::SourceDependencyTest(AssetProcessor::Asse
     using SourceFileDependencyEntryContainer = AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer;
 
     //  A depends on B, which depends on both C and D
+    AZ::Uuid aUuid{ "{B3FCF51E-BDB3-430D-B360-E57913725250}" };
+    AZ::Uuid bUuid{ "{E040466C-8B26-4ABB-9E7A-2FF9D1660DB6}" };
 
     SourceFileDependencyEntry newEntry1;  // a depends on B
     newEntry1.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
     newEntry1.m_builderGuid = AZ::Uuid::CreateRandom();
-    newEntry1.m_source = "a.txt";
+    newEntry1.m_sourceGuid = aUuid;
     newEntry1.m_dependsOnSource = "b.txt";
-    
+
     SourceFileDependencyEntry newEntry2; // b depends on C
     newEntry2.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
     newEntry2.m_builderGuid = AZ::Uuid::CreateRandom();
-    newEntry2.m_source = "b.txt";
+    newEntry2.m_sourceGuid = bUuid;
     newEntry2.m_dependsOnSource = "c.txt";
 
     SourceFileDependencyEntry newEntry3;  // b also depends on D
     newEntry3.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
     newEntry3.m_builderGuid = AZ::Uuid::CreateRandom();
-    newEntry3.m_source = "b.txt";
+    newEntry3.m_sourceGuid = bUuid;
     newEntry3.m_dependsOnSource = "d.txt";
 
     UNIT_TEST_EXPECT_TRUE(stateData->SetSourceFileDependency(newEntry1));
     UNIT_TEST_EXPECT_TRUE(stateData->SetSourceFileDependency(newEntry2));
     UNIT_TEST_EXPECT_TRUE(stateData->SetSourceFileDependency(newEntry3));
-    
+
     SourceFileDependencyEntryContainer results;
 
     // what depends on b?  a does.
     UNIT_TEST_EXPECT_TRUE(stateData->GetSourceFileDependenciesByDependsOnSource("b.txt", SourceFileDependencyEntry::DEP_Any, results));
     UNIT_TEST_EXPECT_TRUE(results.size() == 1);
-    UNIT_TEST_EXPECT_TRUE(results[0].m_source == "a.txt");
+    UNIT_TEST_EXPECT_TRUE(results[0].m_sourceGuid == aUuid);
     UNIT_TEST_EXPECT_TRUE(results[0].m_builderGuid == newEntry1.m_builderGuid);
     UNIT_TEST_EXPECT_TRUE(results[0].m_sourceDependencyID == newEntry1.m_sourceDependencyID);
 
     // what does B depend on?
     results.clear();
-    UNIT_TEST_EXPECT_TRUE(stateData->GetDependsOnSourceBySource("b.txt", SourceFileDependencyEntry::DEP_Any, results));
+    UNIT_TEST_EXPECT_TRUE(stateData->GetDependsOnSourceBySource(bUuid, SourceFileDependencyEntry::DEP_Any, results));
     // b depends on 2 things: c and d
     UNIT_TEST_EXPECT_TRUE(results.size() == 2);
-    UNIT_TEST_EXPECT_TRUE(results[0].m_source == "b.txt");  // note that both of these are B, since its B that has the dependency on the others.
-    UNIT_TEST_EXPECT_TRUE(results[1].m_source == "b.txt");
-    UNIT_TEST_EXPECT_TRUE(results[0].m_dependsOnSource == "c.txt"); 
+    UNIT_TEST_EXPECT_TRUE(results[0].m_sourceGuid == bUuid);  // note that both of these are B, since its B that has the dependency on the others.
+    UNIT_TEST_EXPECT_TRUE(results[1].m_sourceGuid == bUuid);
+    UNIT_TEST_EXPECT_TRUE(results[0].m_dependsOnSource == "c.txt");
     UNIT_TEST_EXPECT_TRUE(results[1].m_dependsOnSource == "d.txt");
 
     // what does b depend on, but filtered to only one builder?
     results.clear();
-    UNIT_TEST_EXPECT_TRUE(stateData->GetSourceFileDependenciesByBuilderGUIDAndSource(newEntry2.m_builderGuid, "b.txt", SourceFileDependencyEntry::DEP_SourceToSource, results));
+    UNIT_TEST_EXPECT_TRUE(stateData->GetSourceFileDependenciesByBuilderGUIDAndSource(newEntry2.m_builderGuid, bUuid, SourceFileDependencyEntry::DEP_SourceToSource, results));
     // b depends on 1 thing from that builder: c
     UNIT_TEST_EXPECT_TRUE(results.size() == 1);
-    UNIT_TEST_EXPECT_TRUE(results[0].m_source == "b.txt");
+    UNIT_TEST_EXPECT_TRUE(results[0].m_sourceGuid == bUuid);
     UNIT_TEST_EXPECT_TRUE(results[0].m_dependsOnSource == "c.txt");
 
     // make sure that we can look these up by ID (a)
     UNIT_TEST_EXPECT_TRUE(stateData->GetSourceFileDependencyBySourceDependencyId(newEntry1.m_sourceDependencyID, results[0]));
-    UNIT_TEST_EXPECT_TRUE(results[0].m_source == "a.txt");
+    UNIT_TEST_EXPECT_TRUE(results[0].m_sourceGuid == aUuid);
     UNIT_TEST_EXPECT_TRUE(results[0].m_builderGuid == newEntry1.m_builderGuid);
     UNIT_TEST_EXPECT_TRUE(results[0].m_sourceDependencyID == newEntry1.m_sourceDependencyID);
 
     // remove D, b now should only depend on C
     results.clear();
     UNIT_TEST_EXPECT_TRUE(stateData->RemoveSourceFileDependency(newEntry3.m_sourceDependencyID));
-    UNIT_TEST_EXPECT_TRUE(stateData->GetDependsOnSourceBySource("b.txt", SourceFileDependencyEntry::DEP_Any, results));
+    UNIT_TEST_EXPECT_TRUE(stateData->GetDependsOnSourceBySource(bUuid, SourceFileDependencyEntry::DEP_Any, results));
     UNIT_TEST_EXPECT_TRUE(results.size() == 1);
     UNIT_TEST_EXPECT_TRUE(results[0].m_dependsOnSource == "c.txt");
 
@@ -1464,7 +1466,7 @@ void AssetProcessingStateDataUnitTest::SourceFingerprintTest(AssetProcessor::Ass
     sourceFile1.m_sourceGuid = AZ::Uuid::CreateRandom();
     sourceFile1.m_sourceName = "a.txt";
     UNIT_TEST_EXPECT_TRUE(stateData->SetSource(sourceFile1));
-    
+
     SourceDatabaseEntry sourceFile2;
     sourceFile2.m_analysisFingerprint = "54321";
     sourceFile2.m_scanFolderPK = scanFolder.m_scanFolderID;
@@ -1492,7 +1494,7 @@ void AssetProcessingStateDataUnitTest::AssetProcessingStateDataTest()
     QDir dirPath;
 
     // intentional scope to contain QTemporaryDir since it cleans up on destruction!
-    { 
+    {
         QTemporaryDir tempDir;
         ProductDatabaseEntryContainer products;
         dirPath = QDir(tempDir.path());

+ 0 - 41
Code/Tools/AssetProcessor/native/utilities/assetUtils.cpp

@@ -1319,47 +1319,6 @@ namespace AssetUtilities
         return productName.toLower();
     }
 
-    static void CollectDependenciesRecursively(AssetProcessor::AssetDatabaseConnection& databaseConnection, const AZ::Uuid& assetId,
-        AZStd::unordered_set<AZ::Uuid>& uuidSet, AZStd::vector<AZ::Uuid>& dependecyList)
-    {
-        if (uuidSet.count(assetId))
-        {
-            return;
-        }
-        dependecyList.push_back(assetId);
-        uuidSet.insert(assetId);
-        AzToolsFramework::AssetDatabase::SourceDatabaseEntry entry;
-        if (!databaseConnection.GetSourceBySourceGuid(assetId, entry))
-        {
-            return;
-        }
-
-        AzToolsFramework::AssetDatabase::SourceFileDependencyEntryContainer container;
-        if (!databaseConnection.GetDependsOnSourceBySource(entry.m_sourceName.c_str(), AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any, container))
-        {
-            return;
-        }
-        for (const auto& sourceFileEntry : container)
-        {
-            databaseConnection.QuerySourceBySourceName(sourceFileEntry.m_dependsOnSource.c_str(), [&](AzToolsFramework::AssetDatabase::SourceDatabaseEntry& entry)
-                {
-                    CollectDependenciesRecursively(databaseConnection, entry.m_sourceGuid, uuidSet, dependecyList);
-                    return true;
-                });
-        }
-    }
-
-    AZStd::vector<AZ::Uuid> CollectAssetAndDependenciesRecursively(AssetProcessor::AssetDatabaseConnection& databaseConnection, const AZStd::vector<AZ::Uuid>& assetList)
-    {
-        AZStd::unordered_set<AZ::Uuid> uuidSet; // Used to guarantee uniqueness and prevent infinite recursion.
-        AZStd::vector<AZ::Uuid> completeAssetList;
-        for (const AZ::Uuid& assetId : assetList)
-        {
-            CollectDependenciesRecursively(databaseConnection, assetId, uuidSet, completeAssetList);
-        }
-        return completeAssetList;
-    }
-
     bool UpdateToCorrectCase(const QString& rootPath, QString& relativePathFromRoot)
     {
         // normalize the input string:

+ 1 - 5
Code/Tools/AssetProcessor/native/utilities/assetUtils.h

@@ -246,10 +246,6 @@ namespace AssetUtilities
 
     QString GuessProductNameInDatabase(QString path, QString platform, AssetProcessor::AssetDatabaseConnection* databaseConnection);
 
-    //! Given a list of source asset Uuids, it returns a list that contains the same source assets Uuids along with all of their dependencies
-    //! which are discovered recursively. All the returned Uuids are unique, meaning they appear once in the returned list.
-    AZStd::vector<AZ::Uuid> CollectAssetAndDependenciesRecursively(AssetProcessor::AssetDatabaseConnection& databaseConnection, const AZStd::vector<AZ::Uuid>& assetList);
-
     //! A utility function which checks the given path starting at the root and updates the relative path to be the actual case correct path.
     bool UpdateToCorrectCase(const QString& rootPath, QString& relativePathFromRoot);
 
@@ -273,7 +269,7 @@ namespace AssetUtilities
     //! Finds all the sources (up and down) in an intermediate output chain
     AZStd::vector<AZStd::string> GetAllIntermediateSources(
         AZ::IO::PathView relativeSourcePath, AZStd::shared_ptr<AssetProcessor::AssetDatabaseConnection> db);
-    
+
     //! Given a source path for an intermediate asset, constructs the product path.
     //! This does not verify either exist, it just manipulates the string.
     AZStd::string GetRelativeProductPathForIntermediateSourcePath(AZStd::string_view relativeSourcePath);

+ 5 - 5
Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp

@@ -388,7 +388,7 @@ namespace AZ
             return !AZ::RHI::IsNullRHI();
         }
 
-        uint32_t FrameCaptureSystemComponent::CaptureScreenshotForWindow(const AZStd::string& filePath, AzFramework::NativeWindowHandle windowHandle)
+        FrameCaptureId FrameCaptureSystemComponent::CaptureScreenshotForWindow(const AZStd::string& filePath, AzFramework::NativeWindowHandle windowHandle)
         {
             if (!CanCapture())
             {
@@ -434,7 +434,7 @@ namespace AZ
             return captureHandle.GetCaptureStateIndex();
         }
 
-        uint32_t FrameCaptureSystemComponent::CaptureScreenshot(const AZStd::string& filePath)
+        FrameCaptureId FrameCaptureSystemComponent::CaptureScreenshot(const AZStd::string& filePath)
         {
             AzFramework::NativeWindowHandle windowHandle = AZ::RPI::ViewportContextRequests::Get()->GetDefaultViewportContext()->GetWindowHandle();
             if (windowHandle)
@@ -445,7 +445,7 @@ namespace AZ
             return InvalidFrameCaptureId;
         }
 
-        uint32_t FrameCaptureSystemComponent::CaptureScreenshotWithPreview(const AZStd::string& outputFilePath)
+        FrameCaptureId FrameCaptureSystemComponent::CaptureScreenshotWithPreview(const AZStd::string& outputFilePath)
         {
             if (!CanCapture())
             {
@@ -573,7 +573,7 @@ namespace AZ
             return CaptureHandle::Null();
         }
 
-        uint32_t FrameCaptureSystemComponent::CapturePassAttachment(
+        FrameCaptureId FrameCaptureSystemComponent::CapturePassAttachment(
             const AZStd::vector<AZStd::string>& passHierarchy,
             const AZStd::string& slot,
             const AZStd::string& outputFilePath,
@@ -594,7 +594,7 @@ namespace AZ
             return InvalidFrameCaptureId;
         }
 
-        uint32_t FrameCaptureSystemComponent::CapturePassAttachmentWithCallback(
+        FrameCaptureId FrameCaptureSystemComponent::CapturePassAttachmentWithCallback(
             const AZStd::vector<AZStd::string>& passHierarchy,
             const AZStd::string& slotName,
             RPI::AttachmentReadback::CallbackFunction callback,

+ 5 - 5
Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.h

@@ -41,12 +41,12 @@ namespace AZ
 
             // FrameCaptureRequestBus overrides ...
             bool CanCapture() const override;
-            uint32_t CaptureScreenshot(const AZStd::string& filePath) override;
-            uint32_t CaptureScreenshotForWindow(const AZStd::string& filePath, AzFramework::NativeWindowHandle windowHandle) override;
-            uint32_t CaptureScreenshotWithPreview(const AZStd::string& outputFilePath) override;
-            uint32_t CapturePassAttachment(const AZStd::vector<AZStd::string>& passHierarchy, const AZStd::string& slotName, const AZStd::string& outputFilePath,
+            FrameCaptureId CaptureScreenshot(const AZStd::string& filePath) override;
+            FrameCaptureId CaptureScreenshotForWindow(const AZStd::string& filePath, AzFramework::NativeWindowHandle windowHandle) override;
+            FrameCaptureId CaptureScreenshotWithPreview(const AZStd::string& outputFilePath) override;
+            FrameCaptureId CapturePassAttachment(const AZStd::vector<AZStd::string>& passHierarchy, const AZStd::string& slotName, const AZStd::string& outputFilePath,
                 RPI::PassAttachmentReadbackOption option) override;
-            uint32_t CapturePassAttachmentWithCallback(const AZStd::vector<AZStd::string>& passHierarchy, const AZStd::string& slotName
+            FrameCaptureId CapturePassAttachmentWithCallback(const AZStd::vector<AZStd::string>& passHierarchy, const AZStd::string& slotName
                 , RPI::AttachmentReadback::CallbackFunction callback, RPI::PassAttachmentReadbackOption option) override;
 
             // FrameCaptureTestRequestBus overrides ...

+ 4 - 1
Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingResourceList.h

@@ -144,6 +144,9 @@ namespace AZ
                     m_indirectionList.SetEntry(itLast->second.m_indirectionIndex, resourceIndex);                 
                 }
 
+                // cache the indirection index so that its okay to erase the iterator
+                uint32_t cachedIndirectionIndex = it->second.m_indirectionIndex;
+
                 // remove the last entry from the resource list
                 m_resources.pop_back();
 
@@ -151,7 +154,7 @@ namespace AZ
                 m_resourceMap.erase(it);
 
                 // remove the entry from the indirection list
-                m_indirectionList.RemoveEntry(it->second.m_indirectionIndex);
+                m_indirectionList.RemoveEntry(cachedIndirectionIndex);
             }
         }
 

+ 15 - 4
Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.cpp

@@ -130,8 +130,19 @@ namespace AtomToolsFramework
             m_currentCaptureRequest = m_captureRequestQueue.front();
             m_captureRequestQueue.pop();
 
-            m_state.reset();
-            m_state.reset(new PreviewRendererLoadState(this));
+            bool canCapture = false;
+            AZ::Render::FrameCaptureRequestBus::BroadcastResult(canCapture, &AZ::Render::FrameCaptureRequestBus::Events::CanCapture);
+
+            // if we're not on a device that can capture, immediately trigger the "failed" state.
+            if (!canCapture)
+            {
+                CancelCaptureRequest();
+            }
+            else
+            {
+                m_state.reset();
+                m_state.reset(new PreviewRendererLoadState(this));
+            }
         }
     }
 
@@ -183,7 +194,7 @@ namespace AtomToolsFramework
         m_currentCaptureRequest.m_content->Update();
     }
 
-    uint32_t PreviewRenderer::StartCapture()
+    AZ::Render::FrameCaptureId PreviewRenderer::StartCapture()
     {
         auto captureCompleteCallback = m_currentCaptureRequest.m_captureCompleteCallback;
         auto captureFailedCallback = m_currentCaptureRequest.m_captureFailedCallback;
@@ -216,7 +227,7 @@ namespace AtomToolsFramework
 
         m_renderPipeline->AddToRenderTickOnce();
 
-        uint32_t frameCaptureId = AZ::Render::InvalidFrameCaptureId;
+        AZ::Render::FrameCaptureId frameCaptureId = AZ::Render::InvalidFrameCaptureId;
         AZ::Render::FrameCaptureRequestBus::BroadcastResult(
             frameCaptureId, &AZ::Render::FrameCaptureRequestBus::Events::CapturePassAttachmentWithCallback, m_passHierarchy,
             AZStd::string("Output"), captureCallback, AZ::RPI::PassAttachmentReadbackOption::Output);

+ 2 - 1
Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.h

@@ -8,6 +8,7 @@
 
 #pragma once
 
+#include <Atom/Feature/Utils/FrameCaptureBus.h>
 #include <Atom/RPI.Public/Base.h>
 #include <Atom/RPI.Public/Pass/AttachmentReadback.h>
 #include <AtomToolsFramework/PreviewRenderer/PreviewContent.h>
@@ -47,7 +48,7 @@ namespace AtomToolsFramework
 
         void PoseContent();
 
-        uint32_t StartCapture();
+        AZ::Render::FrameCaptureId StartCapture();
         void EndCapture();
 
     private:

+ 12 - 4
Gems/Atom/Tools/ShaderManagementConsole/Code/Source/ShaderManagementConsoleApplication.cpp

@@ -141,13 +141,21 @@ namespace ShaderManagementConsole
         AZStd::list<AZStd::string> materialTypeSources;
 
         assetDatabaseConnection.QuerySourceDependencyByDependsOnSource(
-            shaderFilePath.c_str(), nullptr, AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any,
+            shaderFilePath.c_str(), AzToolsFramework::AssetDatabase::SourceFileDependencyEntry::DEP_Any,
             [&](AzToolsFramework::AssetDatabase::SourceFileDependencyEntry& sourceFileDependencyEntry)
             {
-                if (AzFramework::StringFunc::Path::IsExtension(
-                        sourceFileDependencyEntry.m_source.c_str(), AZ::RPI::MaterialTypeSourceData::Extension))
+                AZStd::string relativeSourcePath;
+                assetDatabaseConnection.QuerySourceBySourceGuid(
+                    sourceFileDependencyEntry.m_sourceGuid,
+                    [&relativeSourcePath](AzToolsFramework::AssetDatabase::SourceDatabaseEntry& entry)
+                    {
+                        relativeSourcePath = entry.m_sourceName;
+                        return false;
+                    });
+
+                if (AzFramework::StringFunc::Path::IsExtension(relativeSourcePath.c_str(), AZ::RPI::MaterialTypeSourceData::Extension))
                 {
-                    materialTypeSources.push_back(sourceFileDependencyEntry.m_source);
+                    materialTypeSources.push_back(relativeSourcePath);
                 }
                 return true;
             });

+ 1 - 1
Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp

@@ -270,7 +270,7 @@ namespace AZ
             AzFramework::StringFunc::Path::GetFolderPath(resolvedOutputFilePath, lutGenerationCacheFolder);
             AZ::IO::SystemFile::CreateDir(lutGenerationCacheFolder.c_str());
 
-            uint32_t frameCaptureId = AZ::Render::InvalidFrameCaptureId;
+            AZ::Render::FrameCaptureId frameCaptureId = AZ::Render::InvalidFrameCaptureId;
             AZ::Render::FrameCaptureRequestBus::BroadcastResult(
                 frameCaptureId,
                 &AZ::Render::FrameCaptureRequestBus::Events::CapturePassAttachment,

+ 8 - 7
Gems/Blast/Editor/Scripts/blast_chunk_processor.py

@@ -140,17 +140,18 @@ def on_update_manifest(args):
         scene = args[0]
         return update_manifest(scene)
     except:
-        global sceneJobHandler
-        sceneJobHandler = None
         log_exception_traceback()
+    finally:
+        global sceneJobHandler
+        sceneJobHandler.disconnect()
 
 # try to create SceneAPI handler for processing
 try:
     import azlmbr.scene as sceneApi
-    if (sceneJobHandler == None):
-        sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
-        sceneJobHandler.connect()
-        sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
-        sceneJobHandler.add_callback('OnPrepareForExport', on_prepare_for_export)
+    sceneJobHandler = sceneApi.ScriptBuildingNotificationBusHandler()
+    sceneJobHandler.add_callback('OnUpdateManifest', on_update_manifest)
+    sceneJobHandler.add_callback('OnPrepareForExport', on_prepare_for_export)
+    sceneJobHandler.connect()
+        
 except:
     sceneJobHandler = None

BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe1008_dx12_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe1008_null_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe1008_vulkan_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe144_dx12_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe144_null_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe144_vulkan_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe288_dx12_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe288_null_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe288_vulkan_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe432_dx12_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe432_null_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe432_vulkan_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe576_dx12_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe576_null_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe576_vulkan_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe720_dx12_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe720_null_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe720_vulkan_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe864_dx12_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe864_null_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance-numraysperprobe864_vulkan_0.azshadervariant


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance.azshader


BIN
Gems/DiffuseProbeGrid/Assets/Shaders/DiffuseGlobalIllumination/diffuseprobegridblenddistance_dx12_0.azshadervariant


Some files were not shown because too many files changed in this diff